IrisBackgroundsBangladesh Flag
Bangladesh Flag
A full-bleed, cover-fit Bangladesh flag held to its official 10:6 proportions, its cloth stirred by the pointer's own motion.
Bangladesh Flag
Cover-fit the way an object-fit: cover image is, not letterboxed: the flag's own rectangle always holds the real 10:6 proportion, cropped rather than stretched on whichever axis overflows, so it drops behind any box as a true full-bleed background. The disc sits exactly where the spec puts it — radius one-fifth of the length, centred on the vertical at nine-twentieths — both computed in one isotropic unit space, so it stays a true circle rather than an ellipse that happens to look round.
The cloth is pinned at the hoist and free toward the fly, so its silhouette ripples rather than a flat texture painted on a rectangle — fold shading comes from the wave's own slope. The pointer is wind, but only while it's actually moving: interaction strength is driven by the cursor's own on-screen speed rather than mere presence, so resting it on the cloth and holding still lets the gust fall silent, and dragging stirs a local gust and an expanding ripple centred on the cursor again.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { BangladeshFlagField } from "./BangladeshFlagField";
export default function Example() {
return (
// Fills its nearest positioned ancestor (it renders itself `absolute
// inset-0`) — give it a sized, relatively positioned box. The flag
// itself stays 10:6 no matter what shape this box is.
<div className="relative isolate h-[32rem] w-full overflow-hidden rounded-2xl">
<BangladeshFlagField />
</div>
);
}Component
The real source, exactly as it ships — multiple files, kept together.
"use client";
import { useEffect, useRef } from "react";
import { mountShaderSurface } from "@/lib/shader-surface";
/**
* A live WebGL Bangladesh flag — a drop-in full-bleed background, cover-fit
* the way an `object-fit: cover` image is: its own rectangle is always held
* to the real 10:6 proportion, and whichever axis is the tighter constraint
* sets the scale, with the flag cropped (never stretched) on the other. That
* is what makes it usable as a background at all — anyone can drop it behind
* any box, any shape, and get a correctly proportioned flag filling it edge
* to edge, not a smaller flag floating on a letterboxed backdrop.
*
* Flown as cloth rather than drawn flat: pinned at its own hoist (length 0)
* and free everywhere else, the same rig every "waving flag" shader uses — a
* per-column vertical displacement whose amplitude grows from zero at the
* pole to full at the fly, so the silhouette itself ripples. Shading comes
* from the local slope of that same displacement (a cheap finite difference,
* since this surface has no derivatives extension): a column tilting toward
* the light brightens, one tilting away darkens, which is what reads as
* folds rather than a moving stripe.
*
* The pointer is wind, not a light — but only while it is actually moving.
* Interaction strength is driven by the pointer's own on-screen speed (eased
* fast up, decayed down), not by presence, so resting the cursor over the
* cloth and holding it still lets the gust fall silent on its own; dragging
* stirs a local gust and an expanding ripple centred on the cursor again.
* The ambient flutter never stops regardless, which is also why a
* reduced-motion visitor's single still frame is a real composed moment
* rather than a flat swatch.
*
* Geometry is the official spec, not eyeballed: circle radius = length/5,
* its centre on the vertical drawn at 9/20 of the length. Both are computed
* in one physical coordinate space (length 10, width 6) that the cover-fit
* keeps isotropic, so the circle is a true circle at any container size.
*
* Drop it into any `position: relative`/`isolate` parent — it fills the box
* completely. Built on `lib/shader-surface.ts`, so every degradation path is
* already handled: no WebGL, a blocked or lost context, a hidden tab, or
* `prefers-reduced-motion` all leave the CSS cover-fit floor underneath
* visible — itself an exact, proportioned flag rather than an approximating
* gradient.
*
* Reading guard: when `guardSelector` resolves to an element, the field
* measures that block every frame and clamps its own luminance under a
* ceiling inside that region. `null` (the default) turns the guard off —
* for decorative use where nothing sits on top.
*/
/* The spec, in one physical unit system (length 10, width 6) — the numbers
below are what data/... FLAG_L/FLAG_W make isotropic, so a "unit" is the
same physical size along both axes and CIRCLE_R is a true radius. */
const FRAG = `
uniform vec2 u_res;
uniform float u_time;
uniform float u_scale;
uniform vec3 u_pointer; /* x, y (0 bottom .. 1 top), presence 0..1 */
uniform float u_motion; /* 0..1 — how fast the pointer is CURRENTLY moving,
not just whether it's present; see the JS side */
uniform vec3 u_green;
uniform vec3 u_red;
uniform vec3 u_ground;
uniform vec4 u_readA;
uniform float u_guard;
const float FLAG_L = 10.0;
const float FLAG_W = 6.0;
const float FLAG_ASPECT = FLAG_L / FLAG_W;
const float CIRCLE_CX = 4.5; /* 9/20 of the length */
const float CIRCLE_CY = 3.0; /* width / 2 */
const float CIRCLE_R = 2.0; /* length / 5 */
/* A touch beyond exact cover size, so the wave's own vertical travel at
the fly and the pointer's ripple never peek the backdrop in at a canvas
edge — the same reason a cover-fit video is usually scaled a hair past
the minimum. */
const float OVERSCAN = 1.22;
/* The ambient wave: pinned at the hoist (lx = 0), free at the fly — a
function of length-position and time only, so its slope one step over is
a cheap finite difference and every column stays coherent with its
neighbours instead of each pixel improvising its own phase. */
float baseWave(float lx, float t) {
float f = clamp(lx / FLAG_L, 0.0, 1.0);
/* suppressed through the disc's own span (roughly f 0.25..0.65) and
reserved for the free fly beyond it — a circle sitting mid-cloth
should bend gently, not tear into a teardrop */
float amp = pow(f, 1.6) * 0.34;
float w = sin(lx * 0.5 - t * 2.1)
+ 0.35 * sin(lx * 1.05 - t * 3.4 + 1.7)
+ 0.18 * fbm(vec2(lx * 0.16 + t * 0.09, t * 0.07));
return w * amp;
}
void main() {
vec2 res = u_res / u_scale;
vec2 uv = gl_FragCoord.xy / u_scale / res; /* 0..1, y up */
float canvasAspect = res.x / max(res.y, 1.0);
vec2 P = (uv - 0.5) * vec2(canvasAspect, 1.0);
/* cover-fit the flag's own 10:6 rectangle over the canvas box — full
width and height, the same rule object-fit: cover uses on an image:
whichever axis is the tighter constraint sets the scale, and the flag
overflows (and gets cropped by the canvas edges) on the other axis, so
this drops into any box as a true full-bleed background. halfW/halfH
keep exactly a 10:6 ratio regardless, so the physical scale below
(SCALE) is the same along both axes: a unit is a unit. */
float halfH = max(0.5, canvasAspect / (2.0 * FLAG_ASPECT)) * OVERSCAN;
float halfW = halfH * FLAG_ASPECT;
float SCALE = FLAG_L / (2.0 * halfW);
float fx = (P.x + halfW) * SCALE;
float fyBase = (P.y + halfH) * SCALE;
vec2 ptrP = (u_pointer.xy - 0.5) * vec2(canvasAspect, 1.0);
float ptrFx = (ptrP.x + halfW) * SCALE;
float ptrFy = (ptrP.y + halfH) * SCALE;
float t = u_time;
float baseH = baseWave(fx, t);
float slope = (baseWave(fx + 0.08, t) - baseH) / 0.08;
/* wind: the pointer stirs a local gust — extra amplitude on the ambient
wave plus an expanding ripple, both centred on the touch itself. Gated
on u_motion rather than u_pointer.z: a cursor that stopped moving is
just resting on the cloth, not blowing on it, so the gust falls silent
under it instead of holding steady for as long as it hovers. */
float gdx = fx - ptrFx;
float gdy = fyBase - ptrFy;
float gd2 = gdx * gdx + gdy * gdy;
float gd = sqrt(gd2);
float gust = u_motion * exp(-gd2 / 4.6);
float ripple = sin(gd * 2.6 - t * 8.5) * exp(-gd * 0.75) * gust * 0.85;
float dispY = baseH * (1.0 + gust * 2.6) + ripple;
float fyUsed = fyBase + dispY;
/* one screen pixel's size in this physical unit system, so edges stay a
crisp ~1.5px regardless of how small or large the field is drawn */
float aa = max(SCALE / res.y, 0.01) * 1.5;
float edgeL = smoothstep(-aa, aa, fx) * smoothstep(-aa, aa, FLAG_L - fx);
float edgeW = smoothstep(-aa, aa, fyUsed) * smoothstep(-aa, aa, FLAG_W - fyUsed);
float insideRect = edgeL * edgeW;
float dCircle = length(vec2(fx - CIRCLE_CX, fyUsed - CIRCLE_CY));
float insideCircle = 1.0 - smoothstep(CIRCLE_R - aa, CIRCLE_R + aa, dCircle);
vec3 flagCol = mix(u_green, u_red, insideCircle);
/* folds: a directional term off the wave's own slope, plus a fine
wrinkle texture so the cloth doesn't read as flat plastic */
float wrinkle = fbm(vec2(fx * 1.7 + t * 0.06, fyUsed * 2.1));
float light = clamp(0.74 - slope * 0.6 + wrinkle * 0.14 + gust * 0.16, 0.34, 1.3);
flagCol *= light;
flagCol += vec3(1.0) * gust * 0.09 * insideRect;
/* ---- the backdrop the flag floats over: the ground, a soft bleed of
the flag's own colours, and a contact shadow beneath it ---- */
float glowD2 = dot(P, P);
vec3 bg = u_ground;
bg += u_green * exp(-glowD2 * 1.3) * 0.11;
bg += u_red * exp(-glowD2 * 3.4) * 0.05;
vec2 shadowP = vec2(P.x, P.y + halfH * 1.05) * vec2(1.0, 2.6);
bg *= 1.0 - 0.28 * exp(-dot(shadowP, shadowP) * 5.0);
vec3 col = mix(bg, flagCol, insideRect);
col = max(col, 0.0);
/* ---- the reading guard (see SilkField for the full rationale) ---- */
vec2 rd = abs(uv - u_readA.xy) / max(u_readA.zw, vec2(0.02));
float m = mix(max(rd.x, rd.y), length(rd), 0.4);
float band = 1.0 - smoothstep(0.72, 2.1, m);
col = mix(col, holdUnder(col, 0.09), band * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
/* The flag's own fixed colours — a national flag's palette, not a site
theme token, so these are set directly rather than read through
`colors` (same treatment ArcLightsField gives u_touch). sRGB 0..1,
the official bottle-green/red pair. */
const GREEN: [number, number, number] = [0.0, 0.4157, 0.3059]; // #006A4E
const RED: [number, number, number] = [0.9569, 0.1647, 0.2549]; // #F42A41
/* The CSS floor's flag, as a background-size: cover SVG — the browser's
own cover algorithm rather than a hand-rolled one, at the same 10:6
viewBox and circle geometry the shader draws (radius length/5, centred
at 9/20 of the length). */
const FLAG_SVG_ENCODED = encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 6">' +
'<rect width="10" height="6" fill="#006A4E"/>' +
'<circle cx="4.5" cy="3" r="2" fill="#F42A41"/>' +
"</svg>"
);
export interface BangladeshFlagFieldProps {
className?: string;
/**
* CSS selector for the block the reading guard should keep readable,
* resolved against `document`. `null` (the default) turns the guard off.
*/
guardSelector?: string | null;
}
export function BangladeshFlagField({
className,
guardSelector = null,
}: BangladeshFlagFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const guardOn = guardSelector != null;
let guardEl: Element | null | undefined;
const readGuardEl = () => {
if (guardEl === undefined) {
guardEl = guardOn ? document.querySelector(guardSelector as string) : null;
}
return guardEl;
};
/* Motion, not presence: shader-surface's u_pointer.z is 1 for as long
as the cursor rests anywhere over the canvas, which would keep the
gust blowing on a cursor that's simply stopped. Tracked here instead
from frame to frame — the pointer's own on-screen speed, clamped and
smoothed with a fast attack (so a flick registers immediately) and a
slower decay (so it fades rather than cutting off the instant the
cursor stops), reaching genuine zero within a few frames of no
movement rather than holding at some idle floor. */
let prevPX: number | null = null;
let prevPY: number | null = null;
let prevTime = 0;
let motion = 0;
return mountShaderSurface(canvas, {
fragment: FRAG,
colors: { u_ground: "--iris-bg-sunken" },
uniforms: ["u_green", "u_red", "u_readA", "u_guard", "u_motion"],
onInit: (gl, u) => {
if (u.u_green) gl.uniform3f(u.u_green, ...GREEN);
if (u.u_red) gl.uniform3f(u.u_red, ...RED);
if (u.u_readA) gl.uniform4f(u.u_readA, 0.5, 0.5, 0.44, 0.32);
if (u.u_guard) gl.uniform1f(u.u_guard, guardOn ? 1 : 0);
if (u.u_motion) gl.uniform1f(u.u_motion, 0);
},
onFrame: (gl, u, s) => {
const { x: px, y: py, presence } = s.pointer;
const dt = Math.max(s.time - prevTime, 1 / 120);
let target = 0;
if (presence > 0.01 && prevPX !== null && prevPY !== null) {
const speed = Math.hypot(px - prevPX, py - prevPY) / dt;
target = Math.min(speed * 2.2, 1);
}
motion += (target - motion) * (target > motion ? 0.6 : 0.18);
if (motion < 0.004) motion = 0;
prevPX = px;
prevPY = py;
prevTime = s.time;
if (u.u_motion) gl.uniform1f(u.u_motion, motion);
if (!guardOn || !u.u_readA) return;
let cx = 0.5, cy = 0.5, hw = 0.44, hh = 0.32;
const el = readGuardEl();
const { rect } = s;
if (el && rect.width > 0 && rect.height > 0) {
const r = el.getBoundingClientRect();
const padX = rect.width * 0.09;
const padY = rect.height * 0.11;
cx = (r.left + r.width / 2 - rect.left) / rect.width;
cy = 1 - (r.top + r.height / 2 - rect.top) / rect.height;
hw = (r.width / 2 + padX) / rect.width;
hh = (r.height / 2 + padY) / rect.height;
}
gl.uniform4f(u.u_readA, cx, cy, hw, hh);
},
onPainted: () => canvas.setAttribute("data-shader", "on"),
/* No onIdle — same contract as every other field: once painted, the
last frame stays on screen while the surface is parked off-view. */
onLost: () => canvas.removeAttribute("data-shader"),
});
}, [guardSelector]);
return (
<div
className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
aria-hidden="true"
>
{/* The CSS floor: an exact, proportioned flag — real object-fit:
cover on an SVG background, so it fills the box full width and
height exactly like the shader does, cropping instead of
letterboxing. This is what the pre-paint frame and the no-WebGL /
reduced-motion fallback show, not a placeholder. */}
<div
className="absolute inset-0"
style={{
backgroundImage: `url("data:image/svg+xml,${FLAG_SVG_ENCODED}")`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
<canvas
ref={canvasRef}
className="absolute inset-0 h-full w-full opacity-0 transition-opacity duration-[--duration-slow] ease-[--ease-standard] data-[shader=on]:opacity-100"
/>
</div>
);
}Custom work
Need one that isn't in the catalogue?
Describe what you're after and I'll reply by email — no promise on turnaround yet, this is a new channel, not a service with a set price or queue.
Fardin Omor Afnan
Reads and answers every request himself