Rainy Window
Full-page experience · Open playground
Rain beads, merges, and runs down fogged glass over a drawn city. Drag across the pane to wipe the mist and gather droplets; release to leave the collected water on the glass. The original controls adjust the rain and mist and toggle the effect.
Open in the editor · Effects gallery
How it works
Section titled “How it works”useTexture(rainDrops(...)) owns a live droplet resource. Its output supplies a displacement map for a cached city Layer. A captured pointer drag sends commands to the resource. The droplet field and the mist are related but distinct: wiping changes the film as well as moving water, while moving drops leave trails.
Integration boundaries
Section titled “Integration boundaries”This is a Pibbl-centered Canvas scene, not a filter over live HTML. To distort a photograph, draw a loaded image inside the Pibbl composition; Canvas cannot capture an arbitrary DOM subtree. The included city is drawn in code, so this example requires no image assets.
The host uses an 880 by 700 logical canvas. CSS can scale its presentation; keep its aspect ratio so the scene and its controls remain aligned. Pibbl owns scheduling and pointer capture. Dispose the mount before replacing its canvas. The example starts automatically and provides Stop. With reduced motion enabled, Run example starts it explicitly.
APIs and agent guidance
Section titled “APIs and agent guidance”- Rain and other texture generators
- Texture resource types
- Detailed texture integration guidance
- Canvas and HTML integration boundaries
Verification
Section titled “Verification”Run the scene, drag through the mist, release, and observe the cleared trail and gathered droplet. Toggle the effect to compare the underlying city. Stop and restart to verify a new mount without a second animation loop.
Related APIs
Section titled “Related APIs”Complete source
Section titled “Complete source”Host setup for this source: use a canvas with width 880 and height 700, compile with jsx: "react-jsx" and jsxImportSource: "@pibbl/core", import its default mount, and call const controller = mount(canvas) after attaching the canvas. Call controller.dispose() before removing it.
rainy-window.example.tsx
Section titled “rainy-window.example.tsx”import { pibbl, Circle, Clip, Group, Layer, Rectangle, Text, useConst, useRef, useSignal, useTexture, type PibblController, type PibblPointerEvent } from "@pibbl/core";import { blur, displacement, grayscale, opacity } from "@pibbl/core/filters";import { rainDrops } from "@pibbl/core/textures";
const pane = { x: 28, y: 104, width: 824, height: 420 };
function City() { return <Group> <Rectangle style={{ left: pane.x, top: pane.y, width: pane.width, height: pane.height, fill: "#304850" }} /> {Array.from({ length: 28 }, (_, i) => <Rectangle key={`sky-${i}`} style={{ left: pane.x, top: pane.y + i * 15, width: pane.width, height: 15, fill: `rgb(${48 + i * 2} ${72 + i * 2} ${80 + i})` }} />)} {Array.from({ length: 11 }, (_, i) => { const x = 28 + i * 79, top = 170 + (i * 53) % 100; return <Group key={i}> <Rectangle style={{ left: x, top, width: Math.min(73, 852 - x), height: 524 - top, fill: i % 2 ? "#344044" : "#26373d" }} /> {Array.from({ length: 24 }, (_, j) => x + 16 + j % 4 * 16 <= 852 && <Rectangle key={j} style={{ left: x + 9 + j % 4 * 16, top: top + 16 + Math.floor(j / 4) * 29, width: 7, height: 13, fill: (i * 7 + j * 3) % 5 < 2 ? "#e6bc7c" : "#5e7274" }} />)} </Group>; })} <Rectangle style={{ left: 28, top: 439, width: 824, height: 85, fill: "#344b52" }} /> {Array.from({ length: 44 }, (_, i) => <Rectangle key={`road-${i}`} style={{ left: 28 + (i * 137) % 800, top: 451 + i % 8 * 8, width: 12 + i % 5 * 10, height: 2, fill: i % 3 ? "#6b8b8e" : "#c5a47a" }} />)} <Rectangle style={{ left: 557, top: 352, width: 192, height: 69, fill: "#163c42" }} /> <Text style={{ left: 579, top: 397, font: "700 28px sans-serif", fill: "#f1d29b" }}>NIGHT CAFÉ</Text> <Rectangle style={{ left: 133, top: 261, width: 5, height: 222, fill: "#142b34" }} /> <Circle style={{ cx: 135, cy: 265, radius: 16, fill: "#f7d8a0" }} /> <Rectangle style={{ left: 452, top: 285, width: 4, height: 210, fill: "#142b34" }} /> <Circle style={{ cx: 454, cy: 289, radius: 12, fill: "#f7d8a0" }} /> </Group>;}
function RainyWindow() { const panePath = useConst(() => new Path2D("M28 104H852V524H28Z")); const paused = useSignal(false), enabled = useSignal(true), rainRate = useSignal(24), gravity = useSignal(1); const condensation = useSignal(0.85), persistence = useSignal(20), irregularity = useSignal(1), mistBlur = useSignal(16), weather = useSignal(0); const dragging = useRef<number>(); const rain = useTexture(rainDrops({ seed: 19, initialDrops: 160, rainRate: rainRate.get(), gravity: gravity.get(), paused: paused.get(), condensation: condensation.get(), trailPersistence: persistence.get(), trailStrength: 0.9, irregularity: irregularity.get() }), { resolution: { width: 704, height: 352 }, }); const map = rain.paint({ mode: "stretch" }); const move = (event: PibblPointerEvent) => { if (dragging.current !== event.pointerId) return; rain.send({ type: "finger", x: Math.max(0, Math.min(1, (event.x - pane.x) / pane.width)), y: Math.max(0, Math.min(1, (event.y - pane.y) / pane.height)), radius: 0.045 }); }; const begin = (event: PibblPointerEvent) => { if (event.button !== 0 || dragging.current !== undefined) return; event.preventDefault(); dragging.current = event.pointerId; event.currentTarget.setPointerCapture(event.pointerId); move(event); }; const end = (event: PibblPointerEvent) => { if (dragging.current !== event.pointerId) return; if (event.type === "pointerup") move(event); dragging.current = undefined; rain.send({ type: "release" }); if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); }; const conditions = [ { label: "MISTED", mist: 0.85, trails: 20, rain: 24, gravity: 1 }, { label: "FRESH RAIN", mist: 0.15, trails: 8, rain: 55, gravity: 1.5 }, { label: "STREAKED", mist: 1, trails: 45, rain: 12, gravity: 2 }, ]; const buttons = [ { label: paused.get() ? "RESUME" : "PAUSE", click: () => paused.update(v => !v) }, { label: conditions[weather.get()].label, click: () => { const next = (weather.get() + 1) % conditions.length, condition = conditions[next]; weather.set(next); condensation.set(condition.mist); persistence.set(condition.trails); rainRate.set(condition.rain); gravity.set(condition.gravity); rain.send({ type: "reset" }); } }, { label: `MIST ${Math.round(condensation.get() * 100)}%`, click: () => condensation.update(v => v >= 1 ? 0 : v >= 0.6 ? 1 : v >= 0.3 ? 0.85 : 0.4) }, { label: `TRAILS ${persistence.get()}s`, click: () => persistence.update(v => v >= 45 ? 0 : v >= 20 ? 45 : v >= 8 ? 20 : 8) }, { label: `SHAPE ${irregularity.get() === 0 ? "SMOOTH" : irregularity.get() === 1 ? "ROUGH" : "SOFT"}`, click: () => irregularity.update(v => v === 1 ? 0 : v === 0 ? 0.5 : 1) }, { label: `RAIN ${rainRate.get()}/s`, click: () => rainRate.update(v => v === 24 ? 80 : v === 80 ? 0 : 24) }, { label: `GRAVITY ${gravity.get()}×`, click: () => gravity.update(v => v === 1 ? 3 : v === 3 ? 0 : 1) }, { label: `MIST BLUR ${mistBlur.get()}`, click: () => mistBlur.update(v => v === 16 ? 24 : v === 24 ? 0 : v === 0 ? 8 : 16) }, { label: enabled.get() ? "GLASS ON" : "GLASS OFF", click: () => enabled.update(v => !v) }, { label: "RESET", click: () => rain.send({ type: "reset" }) }, ]; return <Group> <Rectangle style={{ width: 880, height: 700, fill: "#101f27" }} /> <Text style={{ left: 28, top: 38, font: "12px monospace", fill: "#a4b9be" }}>WEATHER STUDIES / 01</Text> <Text style={{ left: 28, top: 76, font: "600 30px sans-serif", fill: "#edf0e6" }}>Rain on the window</Text> <Text style={{ left: 852, top: 73, textAlign: "right", font: "13px sans-serif", fill: "#a4b9be" }}>Click and drag to wipe the glass.</Text> <Clip style={{ d: panePath }}> <Group style={{ filter: enabled.get() ? displacement({ map, strength: 18, blurRadius: mistBlur, region: pane }) : [] }}> <Layer style={{ width: 880, height: 540 }}> <Group style={{ filter: blur({ radius: 0.6 }) }}><City /></Group> </Layer> </Group> {enabled.get() && <Rectangle pointerEvents="none" style={{ left: pane.x, top: pane.y, width: pane.width, height: pane.height, fill: map, filter: [grayscale({ amount: 1 }), opacity({ amount: 0.08 })] }} />} </Clip> <Rectangle style={{ left: pane.x, top: pane.y, width: pane.width, height: pane.height, fill: "rgba(0,0,0,0)", stroke: "#74858a", strokeWidth: 1, cursor: "grab" }} onPointerDown={begin} onPointerMove={move} onPointerUp={end} onPointerCancel={end} onLostPointerCapture={end} /> {buttons.map((button, i) => <Group key={i}> <Rectangle style={{ left: 28 + i % 5 * 166, top: 548 + Math.floor(i / 5) * 52, width: 158, height: 40, cornerRadius: 5, fill: "#293c45", cursor: "pointer" }} onClick={button.click} /> <Text pointerEvents="none" style={{ left: 107 + i % 5 * 166, top: 573 + Math.floor(i / 5) * 52, textAlign: "center", font: "700 11px sans-serif", fill: "#edf0e6" }}>{button.label}</Text> </Group>)} <Text style={{ left: 28, top: 674, font: "12px sans-serif", fill: "#a4b9be" }}>Drag a finger through the mist to collect water. Release to leave the gathered droplet on the glass.</Text> </Group>;}
export default function mount(canvas: HTMLCanvasElement): PibblController { return pibbl(canvas, <RainyWindow />);}Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.