# Rainy Window
[Full-page experience](/experience/rainy-window/) · [Open playground](/playground/#/examples/composition/rainy-window)


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](/playground/#/examples/composition/rainy-window) · [Effects gallery](/catalogs/)

## 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

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

- [Rain and other texture generators](/reference/types/textures/)
- [Texture resource types](/reference/types/texture-resources/)
- [Detailed texture integration guidance](/agents/topics/textures/)
- [Canvas and HTML integration boundaries](/guides/add-to-your-site/)

## 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

- [useTexture](/reference/types/texture-resources/)
- [rainDrops](/reference/types/textures/)
- [displacement](/reference/types/filters/)
- [Layer](/reference/components/layer/)

## 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

```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

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.
