canvas-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 93fdaafeda16
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 788ba0fa01083b300ad50913
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/seed.ts
// Draws what a new board starts with: a template, a diagram laid out with dagre, or columns of
// sticky notes in frames. Every id, seed and nonce comes from the board's input, so two people who
// open a fresh board at the same moment draw exactly the same elements and nothing is doubled.
import { convertToExcalidrawElements } from "@excalidraw/excalidraw";
import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
import type { ExcalidrawArrowElement, ExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import { COLORS, expandSeed, type Column, type Graph, type Seed } from "../../functions/lib/canvas";
import { edgePoint, layoutGraph } from "./layout";

const NOTE_W = 240;
const NOTE_H = 120;
const GAP = 16;
const PAD = 20;
const COL_GAP = 48;
const LEFT = 40;

function hash(s: string): number {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
  return (h >>> 0) % 2 ** 31;
}

function columnSkeleton(columns: Column[], top: number): ExcalidrawElementSkeleton[] {
  const out: ExcalidrawElementSkeleton[] = [];
  const tallest = Math.max(1, ...columns.map((c) => c.notes.length));
  const height = Math.max(200, PAD * 2 + tallest * NOTE_H + (tallest - 1) * GAP);
  columns.forEach((col, i) => {
    const x = LEFT + i * (NOTE_W + PAD * 2 + COL_GAP);
    const children: string[] = [];
    col.notes.forEach((note, j) => {
      const id = `s-c${i}n${j}`;
      children.push(id);
      out.push({
        type: "rectangle", id, x: x + PAD, y: top + PAD + j * (NOTE_H + GAP), width: NOTE_W, height: NOTE_H,
        backgroundColor: COLORS[col.color], fillStyle: "solid", strokeColor: "transparent", roughness: 0, roundness: { type: 3 },
        // A label takes its shape's outline color, and notes have no outline, so give the text its own.
        ...(note ? { label: { text: note, fontSize: 20, strokeColor: "#1e1e1e" } } : {}),
      });
    });
    out.push({ type: "frame", id: `s-c${i}`, ...(col.name ? { name: col.name } : {}), children, x, y: top, width: NOTE_W + PAD * 2, height });
  });
  return out;
}

function graphSkeleton(graph: Graph, top: number): ExcalidrawElementSkeleton[] {
  const laid = layoutGraph(graph);
  const out: ExcalidrawElementSkeleton[] = [];
  const nodes = new Map(laid.nodes.map((n, i) => [n.id, { ...n, x: LEFT + n.x, y: top + n.y, key: `s-n${i}` }]));
  for (const n of nodes.values()) {
    out.push({
      type: n.shape, id: n.key, x: n.x, y: n.y, width: n.width, height: n.height,
      backgroundColor: COLORS[n.color], fillStyle: "solid", strokeColor: "#1e1e1e", roughness: 1,
      ...(n.shape === "rectangle" ? { roundness: { type: 3 } } : {}),
      label: { text: n.label, fontSize: 20 },
    });
  }
  // Excalidraw binds the arrows to the shapes but draws them where they are given, so aim each one
  // from the edge of its first shape to the edge of its second.
  graph.edges.forEach((e, i) => {
    const from = nodes.get(e.from)!;
    const to = nodes.get(e.to)!;
    const p = edgePoint(from, to.x + to.width / 2, to.y + to.height / 2);
    const q = edgePoint(to, from.x + from.width / 2, from.y + from.height / 2);
    out.push({
      type: "arrow", id: `s-a${i}`, x: p.x, y: p.y, width: Math.abs(q.x - p.x), height: Math.abs(q.y - p.y),
      points: [[0, 0], [q.x - p.x, q.y - p.y]] as unknown as ExcalidrawArrowElement["points"],
      strokeColor: "#1e1e1e", start: { id: from.key }, end: { id: to.key },
      ...(e.label ? { label: { text: e.label, fontSize: 16 } } : {}),
    });
  });
  return out;
}

/** The elements a seed draws, with ids, seeds and nonces derived from the input. */
export function seedElements(seed: Seed): ExcalidrawElement[] {
  const { graph, columns, heading } = expandSeed(seed);
  const skeleton: ExcalidrawElementSkeleton[] = [];
  let top = 0;
  if (heading) {
    skeleton.push({ type: "text", id: "s-heading", x: LEFT, y: 0, text: heading, fontSize: 36 });
    top = 130;
  }
  if (columns?.length) skeleton.push(...columnSkeleton(columns, top));
  if (graph?.nodes.length) skeleton.push(...graphSkeleton(graph, top));
  const elements = convertToExcalidrawElements(skeleton, { regenerateIds: false }) as ExcalidrawElement[];

  // Labels get random ids; name them after their container so every device agrees.
  const rename = new Map<string, string>();
  for (const el of elements) {
    if (el.type === "text" && el.containerId) rename.set(el.id, `${el.containerId}-label`);
  }
  return elements.map((el) => {
    const id = rename.get(el.id) ?? el.id;
    return {
      ...el,
      id,
      seed: hash(id),
      versionNonce: hash(`${id}:nonce`),
      ...(el.boundElements ? { boundElements: el.boundElements.map((b) => ({ ...b, id: rename.get(b.id) ?? b.id })) } : {}),
    } as ExcalidrawElement;
  });
}

Change history

Point OpenApps links to openapps.free
93fdaafeda · 18 hours ago
Bind AGENT_ADDRESS in policy constants
bf8f43b096 · 20 hours ago
Use functions.openapps.xyz alias, add OpenApp contribution strip
56f1a381b2 · 20 hours ago
Bounded source sync
9ec707aeb8 · 1 day ago
Bounded source sync
5645481c23 · 1 day ago
Bounded source sync
c38fe71874 · 1 day ago
Bounded source sync
28c0350b40 · 1 day ago