# Growing Reveal
[Full-page experience](/experience/growing-reveal/) · [Open playground](/playground/#/examples/composition/growing-reveal)

Paint a living alpha mask that reveals illustrated landscapes and colorful typography.

## Try it

Paint the artwork, pause the mask, and switch between landscape and typography.

## What to learn

- Reuse procedural texture output in Canvas composition.
- Keep texture generation independent of display size.

## Integration boundaries

- Uses the optional @pibbl/core/textures package and the public render-layer lifecycle.
- Texture renderer sources are borrowed for the synchronous callback; Pibbl owns presentation.

This is a Pibbl Canvas example. Its mount owns its runtime resources; call its controller's dispose method when removing it. The preview starts automatically unless reduced motion is enabled; Stop disposes the mount. Preserve the source's logical canvas size and aspect ratio. Native HTML is not captured by Canvas filters.

## Related APIs

- [ReactionDiffusion](/reference/components/reaction-diffusion/)
- [defineRenderLayer](/reference/functions/define-render-layer/)
- [Group](/reference/components/group/)
- [useSignal](/reference/hooks/use-signal/)

## Complete source

Host setup for this source: use a canvas with width 880 and height 630, 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.

### growing-reveal.example.tsx

```tsx
/** @jsxImportSource @pibbl/core */
import {
  pibbl,
  Group,
  Rectangle,
  Text,
  FocusManagement,
  useSignal,
  useReaction,
  type PibblController,
} from "@pibbl/core";
import { ReactionDiffusion, type TextureRenderer } from "@pibbl/core/textures";

// The artwork and raster alpha mask are composed locally. No new core mask API.
const landscape: TextureRenderer = (ctx, texture) => {
  const { width: w, height: h } = texture;
  const sky = ctx.createLinearGradient(0, 0, 0, h);
  sky.addColorStop(0, "#f2b1a1");
  sky.addColorStop(0.5, "#efdbc0");
  sky.addColorStop(1, "#5fa398");
  ctx.fillStyle = sky;
  ctx.fillRect(0, 0, w, h);
  ctx.fillStyle = "#fff2ba";
  ctx.beginPath();
  ctx.arc(w * 0.72, h * 0.28, h * 0.19, 0, Math.PI * 2);
  ctx.fill();
  for (let layer = 0; layer < 4; layer++) {
    ctx.fillStyle = ["#a36e88", "#6b6882", "#366572", "#184e5b"][layer];
    ctx.beginPath();
    ctx.moveTo(0, h);
    for (let x = 0; x <= w; x += 4)
      ctx.lineTo(
        x,
        h * (0.47 + layer * 0.12) +
          Math.sin((x / w) * 9 + layer * 2) * h * 0.13 +
          Math.sin((x / w) * 21 + layer) * h * 0.025,
      );
    ctx.lineTo(w, h);
    ctx.closePath();
    ctx.fill();
  }
  ctx.globalCompositeOperation = "destination-in";
  ctx.drawImage(texture.mask, 0, 0, w, h);
};
const typography: TextureRenderer = (ctx, texture) => {
  const gradient = ctx.createLinearGradient(
    0,
    0,
    texture.width,
    texture.height,
  );
  gradient.addColorStop(0, "#ecb984");
  gradient.addColorStop(0.5, "#ef8da8");
  gradient.addColorStop(1, "#8cbbbc");
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, texture.width, texture.height);
  ctx.fillStyle = "#203148";
  ctx.font = "900 100px Georgia";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("STAY", texture.width / 2, texture.height * 0.36);
  ctx.fillText("CURIOUS", texture.width / 2, texture.height * 0.68);
  ctx.globalCompositeOperation = "destination-in";
  ctx.drawImage(texture.mask, 0, 0, texture.width, texture.height);
};
function Button({
  x,
  label,
  active,
  onClick,
}: {
  x: number;
  label: string;
  active?: boolean;
  onClick: () => void;
}) {
  return (
    <Group style={{ translateX: x }}>
      <Rectangle
        keyboardFocusable
        onClick={onClick}
        style={{
          width: 146,
          height: 38,
          cornerRadius: 19,
          fill: active ? "#edbea7" : "#303343",
          cursor: "pointer",
        }}
      />
      <Text
        pointerEvents="none"
        style={{
          left: 73,
          top: 24,
          textBaseline: "alphabetic",
          textAlign: "center",
          font: "600 12px Arial",
          fill: active ? "#2c2732" : "#ede5e2",
        }}
      >
        {label}
      </Text>
    </Group>
  );
}
function GrowingReveal({ canvas }: { canvas: HTMLCanvasElement }) {
  useReaction(() => {
    const previous = canvas.style.touchAction;
    canvas.style.touchAction = "none";
    return () => {
      if (canvas.style.touchAction === "none")
        canvas.style.touchAction = previous;
    };
  }, [canvas]);
  const state = useSignal({ paused: false, reset: 0, artwork: 0, brush: 12 });
  const s = state.get();
  return (
    <FocusManagement>
      <Group>
        <Rectangle
          pointerEvents="none"
          style={{ width: 880, height: 630, fill: "#171c2b" }}
        />
        <Text
          pointerEvents="none"
          style={{
            left: 28,
            top: 30,
            textBaseline: "alphabetic",
            font: "600 11px Arial",
            fill: "#d3a9b3",
          }}
        >
          TEXTURE STUDIES / 03
        </Text>
        <Text
          pointerEvents="none"
          style={{
            left: 28,
            top: 70,
            textBaseline: "alphabetic",
            font: "600 34px Georgia",
            fill: "#f5e5dc",
          }}
        >
          Growing Reveal
        </Text>
        <Text
          pointerEvents="none"
          style={{
            left: 28,
            top: 95,
            textBaseline: "alphabetic",
            font: "13px Arial",
            fill: "#aeb0c4",
          }}
        >
          Paint a window into another world. The living mask grows, divides, and
          recedes.
        </Text>
        <Group style={{ translateX: 24, translateY: 118 }}>
          <Rectangle
            pointerEvents="none"
            style={{ width: 832, height: 388, fill: "#0d1422" }}
          />
          <ReactionDiffusion
            seed={18}
            preset="stripes"
            paused={s.paused}
            resetRevision={s.reset}
            render={s.artwork === 0 ? landscape : typography}
            resolution={{ width: 256, height: 144 }}
            brush={{ radius: s.brush, strength: 1 }}
            style={{ width: 832, height: 388 }}
          />
        </Group>
        <Group style={{ translateX: 24, translateY: 526 }}>
          <Button
            x={0}
            label="Landscape"
            active={s.artwork === 0}
            onClick={() => state.update((v) => ({ ...v, artwork: 0 }))}
          />
          <Button
            x={160}
            label="Stay curious"
            active={s.artwork === 1}
            onClick={() => state.update((v) => ({ ...v, artwork: 1 }))}
          />
          <Button
            x={352}
            label={s.paused ? "Resume" : "Pause"}
            active={s.paused}
            onClick={() => state.update((v) => ({ ...v, paused: !v.paused }))}
          />
          <Button
            x={512}
            label={`Brush ${s.brush}`}
            onClick={() =>
              state.update((v) => ({ ...v, brush: v.brush === 12 ? 24 : 12 }))
            }
          />
          <Button
            x={672}
            label="Begin again"
            onClick={() => state.update((v) => ({ ...v, reset: v.reset + 1 }))}
          />
        </Group>
        <Text
          pointerEvents="none"
          style={{
            left: 28,
            top: 602,
            textBaseline: "alphabetic",
            font: "11px Arial",
            fill: "#929bae",
          }}
        >
          One simulation → one alpha mask → your artwork. Switching artwork
          preserves the living pattern.
        </Text>
      </Group>
    </FocusManagement>
  );
}
export default function mount(canvas: HTMLCanvasElement): PibblController {
  return pibbl(canvas, <GrowingReveal canvas={canvas} />);
}

```

## Documentation version

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