Tejasvi Soi
← Playground

khat.خط

28 September, 2026 · a canvas for two, yours to copy

khat is a shared scribble canvas for two people. One drawing pad, always in sync, so a doodle left on my phone shows up on my girlfriend's.

No feed, no login, no notifications. It behaves like a shared fridge door. It is one HTML file and a storage bucket, so you can copy the whole thing and run your own in about ten minutes. Steps are below.

miss you

The tools

I started with seven fancy pens. They looked great and got in the way. A note between two people needs three things.

Pen. A clean line for handwriting and little drawings.
Marker. A fat, confident stroke for big feelings.
Eraser. Take it back. Undo is a tap or the keyboard away too.

The bar changes shape

Instead of covering the canvas with panels, the toolbar itself morphs. Tap size and it becomes sliders, tap color and it becomes a palette, then a full wheel.

Toolbar: pen, marker, eraser, size, color
Draw
Toolbar as size and opacity sliders
Size and opacity
Toolbar as color palette
Palette
Toolbar as a color wheel with hex
Wheel

How it works

There is no database of strokes and no realtime server. The whole canvas is one PNG in cloud storage. When your hand rests, it uploads the picture. Every few seconds, when you are not drawing, it pulls the latest one back down. Simple enough to fit in a single file.

Add it as an iOS widget

A home screen widget shows the latest scribble and opens the canvas when you tap it. It runs in Scriptable, a free app for running small scripts on iOS. Do this on each phone.

First get the canvas running, the widget depends on it: a Supabase project, a Storage bucket named exactly scribbles set to Public so the image loads without a login, and the canvas deployed to a URL. The widget reads shared.png from that bucket and opens that URL when tapped. Source is on GitHub.

  1. Install Scriptable.
    Get it free from the App Store.
  2. Make a new script named khat.
    Open Scriptable, tap the plus, then tap the title at the top and rename it to exactly khat. The name has to match the code.
  3. Paste the widget code.
    Select all in the new script and paste this in.
    // ── CONFIG ────────────────────────────────────────────────────────────────
    const SUPABASE_URL  = "https://YOUR-PROJECT.supabase.co";
    const WEB_APP_URL   = "https://your-canvas-url";
    // ─────────────────────────────────────────────────────────────────────────
    
    const SCRIPT_NAME = "khat"; // MUST match this script's name in Scriptable exactly
    
    const IMAGE_URL = `${SUPABASE_URL}/storage/v1/object/public/scribbles/shared.png`;
    const CANVAS_BG = "#fdf6d3"; // warm notepad yellow, matches the web canvas
    
    // When tapped, open canvas as full-screen WebView inside Scriptable
    if (!config.runsInWidget) {
      const wv = new WebView();
      await wv.loadURL(WEB_APP_URL);
      await wv.present(true);
      Script.complete();
      return;
    }
    
    const widget = new ListWidget();
    widget.url = `scriptable:///run?scriptName=${encodeURIComponent(SCRIPT_NAME)}`;
    widget.setPadding(22, 20, 16, 16);
    // Transparent so the home screen wallpaper shows around the note
    widget.backgroundColor = new Color("#000000", 0);
    widget.refreshAfterDate = new Date(Date.now() + 5 * 60 * 1000);
    
    let img = null;
    try {
      img = await new Request(IMAGE_URL + "?t=" + Date.now()).loadImage();
    } catch (e) { /* no scribble yet */ }
    
    widget.backgroundImage = renderCard(img, widgetSize());
    
    // ── Top-left: love notes pill ───────────────────────────────────────────────
    const topRow = widget.addStack();
    topRow.layoutHorizontally();
    
    const lovePill = topRow.addStack();
    lovePill.layoutHorizontally();
    lovePill.centerAlignContent();
    lovePill.setPadding(5, 10, 5, 10);
    lovePill.cornerRadius = 30;
    lovePill.backgroundColor = new Color("#1a1014", 0.5);
    
    const heart = lovePill.addText("♡");
    heart.textColor = new Color("#ff9ebb");
    heart.font = Font.boldSystemFont(11);
    lovePill.addSpacer(5);
    const loveLabel = lovePill.addText("love notes");
    loveLabel.textColor = new Color("#ffe1ea");
    loveLabel.font = Font.semiboldSystemFont(11);
    
    topRow.addSpacer();
    widget.addSpacer();
    
    // ── Bottom-right: time-based greeting pill ──────────────────────────────────
    const bottomRow = widget.addStack();
    bottomRow.layoutHorizontally();
    bottomRow.addSpacer();
    
    const g = greeting();
    const greetPill = bottomRow.addStack();
    greetPill.layoutHorizontally();
    greetPill.centerAlignContent();
    greetPill.setPadding(4, 9, 4, 9);
    greetPill.cornerRadius = 30;
    greetPill.backgroundColor = new Color("#1a1014", 0.45);
    
    const emo = greetPill.addText(g.emoji);
    emo.font = Font.systemFont(10);
    greetPill.addSpacer(4);
    const greetLabel = greetPill.addText(g.text);
    greetLabel.textColor = new Color("#ffffff", 0.92);
    greetLabel.font = Font.mediumSystemFont(10);
    
    Script.setWidget(widget);
    Script.complete();
    
    // ── Render the yellow note card on a transparent background ─────────────────
    function renderCard(scribble, [w, h]) {
      const ctx = new DrawContext();
      ctx.size = new Size(w, h);
      ctx.opaque = false;              // transparent outside the card
      ctx.respectScreenScale = true;
    
      const inset  = 9;
      const radius = 20;
      const card   = new Rect(inset, inset + 4, w - inset * 2, h - inset * 2 - 4);
    
      // Soft drop shadow so the note feels lifted off the wallpaper
      for (let i = 4; i >= 1; i--) {
        ctx.setFillColor(new Color("#000000", 0.05));
        fillRounded(ctx, card.minX, card.minY + i * 1.5, card.width, card.height, radius);
      }
    
      // Paper card
      ctx.setFillColor(new Color(CANVAS_BG));
      fillRounded(ctx, card.minX, card.minY, card.width, card.height, radius);
    
      // Scribble aspect-fit inside the card (inset keeps the rounded corners clean)
      if (scribble) {
        const pad = 10;
        const availW = card.width  - pad * 2;
        const availH = card.height - pad * 2;
        const s = Math.min(availW / scribble.size.width, availH / scribble.size.height);
        const dw = scribble.size.width  * s;
        const dh = scribble.size.height * s;
        ctx.drawImageInRect(
          scribble,
          new Rect(card.minX + (card.width - dw) / 2, card.minY + (card.height - dh) / 2, dw, dh)
        );
      } else {
        ctx.setTextColor(new Color("#b6a55a"));
        ctx.setFont(Font.semiboldSystemFont(13));
        ctx.setTextAlignedCenter();
        ctx.drawTextInRect("✏️  a blank page", new Rect(card.minX, card.minY + card.height / 2 - 12, card.width, 24));
      }
    
      // Washi tape straddling the top edge of the card
      const tapeW = Math.min(card.width * 0.36, 120);
      const tapeH = 18;
      const tapeX = card.minX + (card.width - tapeW) / 2;
      const tapeY = card.minY - tapeH / 2;
      ctx.setFillColor(new Color("#ff9ebb", 0.55));
      fillRounded(ctx, tapeX, tapeY, tapeW, tapeH, 3);
    
      return ctx.getImage();
    }
    
    // ── Helpers ─────────────────────────────────────────────────────────────────
    function fillRounded(ctx, x, y, w, h, r) {
      const p = new Path();
      p.addRoundedRect(new Rect(x, y, w, h), r, r);
      ctx.addPath(p);
      ctx.fillPath();
    }
    
    function widgetSize() {
      switch (config.widgetFamily) {
        case "small": return [158, 158];
        case "large": return [338, 354];
        default:      return [338, 158]; // medium
      }
    }
    
    function greeting() {
      const h = new Date().getHours();
      if (h < 5)  return { text: "still up?",      emoji: "🌙" };
      if (h < 12) return { text: "good morning",   emoji: "☀️" };
      if (h < 17) return { text: "good afternoon", emoji: "🌼" };
      if (h < 21) return { text: "good evening",   emoji: "🌇" };
      return            { text: "good night",      emoji: "🌙" };
    }
    
  4. Fill in your two links.
    Set SUPABASE_URL to your Supabase Project URL and WEB_APP_URL to wherever you deployed the canvas.
  5. Add the widget to your home screen.
    Long press the home screen, tap the plus, search Scriptable, pick a size, then Add Widget.
  6. Point the widget at the script.
    Long press the new widget, tap Edit Widget, and set Script to khat.

The widget refreshes on iOS's own schedule, every few minutes. Tap it any time to open the canvas and see the latest right away.