Skip to content

Shared doodle strip

Draw on the pad, then add your doodle to a row everyone shares. Strokes stay in your browser. Only the finished 64×64 image goes into shared data.

Live exampleRoom connecting…
Remix
Test it with another person
  1. Keep this page open in your normal window.
  2. Copy the private-window link, then paste it into a private or incognito window.
  3. Interact in either window and watch the other one update.

Copy the code

Both versions use the same shared data and behavior. The live playground runs the Vanilla HTML version.

Save this as index.html, or open it in the playground to test and change it.

Remix

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Shared doodle strip</title>
  <style>
    :root {
      color: #1c1c1c;
      background: #f4efe5;
      font-family: ui-sans-serif, system-ui, sans-serif;
    }
    * { box-sizing: border-box; }
    body { display: grid; min-height: 100vh; margin: 0; padding: 1.5rem; place-items: center; }
    main { display: grid; gap: 0.75rem; justify-items: start; }
    .pad {
      width: 192px;
      height: 192px;
      border: 1.5px solid #1c1c1c;
      background: white;
      image-rendering: pixelated;
      touch-action: none;
      cursor: crosshair;
    }
    .strip {
      display: flex;
      gap: 6px;
      max-width: 100%;
      min-height: 64px;
      overflow-x: auto;
    }
    .strip img {
      width: 64px;
      height: 64px;
      border: 1px solid #1c1c1c;
      background: white;
      image-rendering: pixelated;
    }
  </style>
</head>
<body>
  <main>
    <canvas id="doodle-pad" class="pad" width="64" height="64"></canvas>
    <div>
      <button id="doodle-clear">Clear</button>
      <button id="doodle-add">Add to row</button>
    </div>
    <div id="doodle-strip" class="strip"></div>
  </main>

  <script type="module">
    import { playhtml } from "https://unpkg.com/playhtml@latest";

    const SIZE = 64;
    const MAX_DOODLES = 20;
    const pad = document.getElementById("doodle-pad");
    const pen = pad.getContext("2d");
    pen.lineWidth = 4;
    pen.lineCap = "round";
    pen.lineJoin = "round";

    // Pointer position in canvas pixels (the canvas is drawn 3x larger).
    function toPad(event) {
      const rect = pad.getBoundingClientRect();
      return [
        ((event.clientX - rect.left) / rect.width) * SIZE,
        ((event.clientY - rect.top) / rect.height) * SIZE,
      ];
    }

    let drawing = false;
    pad.addEventListener("pointerdown", (event) => {
      drawing = true;
      pad.setPointerCapture(event.pointerId);
      pen.beginPath();
      pen.moveTo(...toPad(event));
      pen.lineTo(...toPad(event)); // a click leaves a dot
      pen.stroke();
    });
    pad.addEventListener("pointermove", (event) => {
      if (!drawing) return;
      pen.lineTo(...toPad(event));
      pen.stroke();
    });
    pad.addEventListener("pointerup", () => (drawing = false));

    const clearPad = () => pen.clearRect(0, 0, SIZE, SIZE);
    document.getElementById("doodle-clear").addEventListener("click", clearPad);

    // The shared row. Only finished doodles are shared, never the strokes.
    const strip = playhtml.register("doodle-strip", {
      defaultData: { doodles: [] },
      update: ({ element, data }) => {
        element.replaceChildren(
          ...data.doodles.map((doodle) => {
            const img = document.createElement("img");
            img.src = doodle.src;
            img.alt = "";
            return img;
          }),
        );
      },
    });

    document.getElementById("doodle-add").addEventListener("click", () => {
      const src = pad.toDataURL("image/png");
      strip.setData((data) => {
        data.doodles.push({ id: crypto.randomUUID(), src });
        // Keep the newest 20. Shared arrays support splice, not shift.
        if (data.doodles.length > MAX_DOODLES) {
          data.doodles.splice(0, data.doodles.length - MAX_DOODLES);
        }
      });
      clearPad();
    });

    await playhtml.init({ developmentMode: true });
  </script>
</body>
</html>

The canvas is ordinary local state. Writing every pointer move to shared data would sync hundreds of tiny updates per doodle. Instead, “Add to row” turns the canvas into a small PNG data URL and pushes one entry:

strip.setData((data) => {
  data.doodles.push({ id: crypto.randomUUID(), src });
  if (data.doodles.length > MAX_DOODLES) {
    data.doodles.splice(0, data.doodles.length - MAX_DOODLES);
  }
});

Shared data persists, so a list that only grows keeps getting slower to load. The strip keeps the newest 20 doodles. Shared arrays support splice, not shift, so trim from the front with splice.

See data best practices for mutator writes and list limits.