baked.design

Two dots

Colours, motion and personality. Changes show on the agent as you make them.

No body and no mascot, just two dots. They follow your cursor, read line by line while the agent works, look up and away while it thinks, smile when it's done and go flat when something breaks.

Patience, confidence and chattiness change everything about how it behaves. A patient agent blinks slowly and settles, a confident one snaps to where it looks, an unsure one glances away, and a chatty one leans in while you type and looks around when it's bored.

Install with AI

Paste this into Claude Code, Cursor or any AI coding tool.

Add the Two dots component from https://baked.design/interfaces/two-dots.md to this project. Follow the install steps on that page and use the code exactly as written.

Install by hand

Copy this file into your project.

two-dots.js
// Two dots: an AI agent that is only a pair of eyes, with lasers, by Baked Design Studio
// (baked.design/interfaces/two-dots). The eyes follow you, and fire solid red laser beams, exactly as wide as
// the eyes, that meet on your cursor in a red dot. While it works the beams sweep the page like a scanner,
// burning lines as it reads. While it thinks the eyes bounce in turn inside a spinning ring of dots, sound ripples pulse out while it listens, it smiles and lets off
// fireworks when it's done, and it glitches when something breaks. Personality (patience, confidence,
// chattiness) changes how it moves.
//
//   import { mount } from "./two-dots.js";
//   const eyes = mount(document.querySelector("#agent"), { lasers: "auto", laserColor: "#ff2d55" });
//   eyes.set("working");              // idle · listening · thinking · working · done · error
//   eyes.hear();                      // call on each keystroke while listening
//   eyes.tune({ chattiness: 1, laserWidth: 0.8 });
//
// Options (all can change later with tune): eyeColor (defaults to your text colour), size, gap, lasers
// ("auto" | "always" | "off"), laserColor, laserWidth (beam thickness as a share of the eye: 1 = as wide as
// the eye), glow (0 to 1), sparks, ripples, glitch, patience,
// confidence, chattiness (0 to 1), and gaze: a spring { visualDuration, bounce }. No dependencies.

const STATES = ["idle", "listening", "thinking", "working", "done", "error"];
const NSV = "http://www.w3.org/2000/svg";
const mk = (tag, attrs = {}, parent) => { const e = document.createElementNS(NSV, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); parent?.append(e); return e; };
const f = (n) => n.toFixed(2);
let uid = 0;

// what each state wants from the eyes: size, where they look (null = at you), how open
const POSE = {
  idle: { w: 20, h: 32, gaze: null }, listening: { w: 22, h: 38, gaze: null }, thinking: { w: 20, h: 26, gaze: [-0.55, -0.7] },
  working: { w: 24, h: 15, gaze: null }, done: { w: 22, h: 30, gaze: [0, -0.1] }, error: { w: 24, h: 4, gaze: [0, 0.35] },
};

export function mount(el, options = {}) {
  const p = {
    eyeColor: "", size: 1, gap: 30, lasers: "auto", laserColor: "#ff2d55", laserWidth: 1, glow: 0.7,
    sparks: true, ripples: true, glitch: true, patience: 0.5, confidence: 0.5, chattiness: 0.5,
    gaze: { visualDuration: 0.3, bounce: 0.15 }, ...options,
  };
  const still = matchMedia("(prefers-reduced-motion: reduce)");
  const id = `td${++uid}`;
  const svg = mk("svg", { viewBox: "-100 -60 200 120", "aria-hidden": "true" });
  Object.assign(svg.style, { width: "100%", height: "100%", display: "block", overflow: "visible" });
  const defs = mk("defs", {}, svg);
  const glowF = mk("filter", { id: `${id}g`, x: "-50%", y: "-50%", width: "200%", height: "200%" }, defs);
  const blur = mk("feGaussianBlur", { stdDeviation: 3, result: "b" }, glowF);
  const merge = mk("feMerge", {}, glowF); mk("feMergeNode", { in: "b" }, merge); mk("feMergeNode", { in: "SourceGraphic" }, merge);

  const trails = mk("g", {}, svg);     // burnt scan lines
  const beams = mk("g", {}, svg);      // solid beams: no glow, no outline
  const ripples = mk("g", { fill: "none" }, svg);
  const hit = mk("g", {}, svg);
  const hitDot = mk("circle", { r: 2.4 }, hit);
  const sparkG = mk("g", { filter: `url(#${id}g)` }, svg);
  // thinking: a ring of dots spinning round the eyes, like a loader, with a fading tail
  const spinG = mk("g", {}, svg);
  const spin = Array.from({ length: 14 }, () => mk("circle", { r: 2 }, spinG));
  const eyesG = mk("g", {}, svg);
  const eyes = [-1, 1].map((side) => {
    const g = mk("g", {}, eyesG);
    const ghostR = mk("rect", { fill: "#ff2d55", opacity: 0 }, g), ghostC = mk("rect", { fill: "#00e5ff", opacity: 0 }, g);
    const pill = mk("rect", {}, g);
    const hot = mk("circle", { opacity: 0 }, g);   // the eye's white-hot middle while it fires, where the beam starts
    const arc = mk("path", { fill: "none", "stroke-width": 5, "stroke-linecap": "round", d: "M-11,5 Q0,-9 11,5" }, g);
    // each beam: one solid shape wrapped around the eye and the dot where it lands (see hull below)
    const beam = { body: mk("polygon", { opacity: 0 }, beams) };
    return { side, g, pill, hot, arc, ghostR, ghostC, beam, x: 0, y: 0 };
  });
  el.append(svg);

  let state = STATES.includes(options.state) ? options.state : "idle";
  let t = 0, last = 0, raf = 0, seen = true, heard = 0, since = 9, nextBlink = 2, blink = 0, wander = [0, 0], nextWander = 3;
  let aim = [0, 0], aimPt = null, lastMove = -1e9, over = false;
  const cur = { w: 20, h: 32, gx: 0, gy: 0, smile: 0, lean: 0, laser: 0, think: 0 }, vel = { gx: 0, gy: 0 };
  const parts = [];     // sparks and fireworks: { x, y, vx, vy, life, max, r, c, orbit? }
  const scars = [];     // scan lines the lasers have burnt: { y, x0, x1, life }
  const rings = [];     // listening ripples: { x, y, r, life }

  const ink = () => p.eyeColor || getComputedStyle(el).color;
  function spark(x, y, n, speed, colors, life = 1.1) {
    for (let i = 0; i < n && parts.length < 160; i++) {
      const a = Math.random() * Math.PI * 2, v = speed * (0.4 + Math.random() * 0.8);
      parts.push({ x, y, vx: Math.cos(a) * v, vy: Math.sin(a) * v - speed * 0.3, life, max: life, r: 0.8 + Math.random() * 1.6, c: colors[i % colors.length], node: null });
    }
  }
  function laserWanted() {
    if (p.lasers === "off" || state === "thinking" || state === "done") return 0;
    if (p.lasers === "always") return 1;
    if (state === "working" || state === "listening") return 1;
    if (state === "error") return 0;   // an error glitches the eyes; the lasers go out
    return over && performance.now() - lastMove < 1800 ? 1 : 0;   // idle: only while you're pointing at it
  }
  function target(now) {
    const pose = POSE[state];
    let gaze = pose.gaze ?? aim;
    const idleFor = (now - lastMove) / 1000;
    if ((state === "idle" || state === "listening") && idleFor > 2.5 + 8 * (1 - p.chattiness)) gaze = wander;
    if (state === "thinking") { const s = 1.5 + (1 - p.patience) * 1.2; gaze = [0.5 * Math.cos(t * s), -0.3 + 0.28 * Math.sin(t * s)]; }   // a slow look round
    if (state === "working") { const k = (t * (0.55 + (1 - p.patience) * 0.4)) % 1; gaze = [-0.8 + 1.6 * k, -0.5]; }   // eyes up, beams angled down onto the page
    const size = (0.85 + p.confidence * 0.25) * p.size;
    return { w: pose.w * size, h: pose.h * size * (1 + 0.15 * heard), gx: gaze[0], gy: gaze[1], smile: state === "done" ? 1 : 0,
      lean: state === "listening" ? aim[0] * (0.4 + p.chattiness * 0.6) : 0, laser: laserWanted() };
  }
  // where the beams meet: your cursor, or the scanner's sweep while working
  function laserPoint() {
    if (state === "working") { const k = (t * (0.55 + (1 - p.patience) * 0.4)) % 1, row = Math.floor(t * (0.55 + (1 - p.patience) * 0.4)) % 3; return [-90 + 180 * k, 32 + row * 11]; }
    if (aimPt) return aimPt;
    return [cur.gx * 60, 40 + cur.gy * 20];
  }

  function draw(dt) {
    const color = ink(), lc = p.laserColor;
    blur.setAttribute("stdDeviation", f(0.5 + p.glow * 3.5));
    const glitching = state === "error" && p.glitch;
    const shakeX = state === "error" ? Math.sin(t * 38) * 6 * Math.max(0, 1 - since * 1.6) : 0;
    const open = 1 - blink;
    eyes.forEach((E) => {
      const { side, g, pill, hot, arc, ghostR, ghostC } = E;
      // thinking: the eyes bounce in turn, stretching as they rise and squashing as they land
      const b = Math.sin(t * (6 + (1 - p.patience) * 3) + (side > 0 ? Math.PI : 0)) * cur.think;
      E.x = side * p.gap + cur.gx * 22 + cur.lean * 10 + shakeX; E.y = cur.gy * 14 - cur.lean * cur.lean * 4 - Math.max(0, b) * 8;
      const h = Math.max(3, cur.h * open * (1 - cur.smile) * (1 + 0.16 * b)), w = cur.w * (1 - 0.08 * b);
      for (const r of [pill, ghostR, ghostC]) { r.setAttribute("x", f(-w / 2)); r.setAttribute("y", f(-h / 2)); r.setAttribute("width", f(w)); r.setAttribute("height", f(h)); r.setAttribute("rx", f(Math.min(w, h) / 2)); }
      // the eyes stay their own colour (black on a light page); only the beams are red
      pill.setAttribute("fill", color); arc.setAttribute("stroke", color); hot.style.opacity = 0;
      pill.style.opacity = 1 - cur.smile; arc.style.opacity = cur.smile;
      // the glitch: red and cyan copies of each eye, jumping about
      const gx = glitching && Math.random() < 0.6 ? (Math.random() - 0.5) * 8 : 0;
      ghostR.setAttribute("transform", `translate(${f(gx)},0)`); ghostC.setAttribute("transform", `translate(${f(-gx)},${f(gx * 0.3)})`);
      ghostR.style.opacity = ghostC.style.opacity = glitching ? 0.85 : 0;
      E.w = w; E.h = h; E.rot = state === "error" ? side * -6 : cur.lean * side * 4;
      g.setAttribute("transform", `translate(${f(E.x)},${f(E.y)}) rotate(${f(E.rot)})`);
    });
    // the lasers: a solid red beam from each eye to the same point
    const [tx, ty] = laserPoint(), on = cur.laser;
    // Each beam is the convex hull of the eye's outline and the dot where it lands: like a rubber band
    // stretched round both. Its sides touch the eye exactly where the eye's edge turns away, from any angle and
    // for any eye shape, so the beam and the eye read as one piece with nothing sticking out.
    let w1 = 0;
    eyes.forEach((E) => {
      // pulled in a hair (0.3 units) so the eye's own smoothed edge never shows a red fringe from the beam behind it
      const k = p.laserWidth, inset = 0.3, r = Math.max(0.5, (Math.min(E.w, E.h) / 2) * k - inset), hw = Math.max(0, (E.w / 2) * k - inset - r), hh = Math.max(0, (E.h / 2) * k - inset - r);
      const a = (E.rot * Math.PI) / 180, ca = Math.cos(a), sa = Math.sin(a), pts = [];
      // the pill (a stadium): two half-circles joined by straight sides, sampled finely, in the eye's own frame
      for (let i = 0; i < 72; i++) {
        const th = (i / 72) * Math.PI * 2, cx = Math.cos(th), cy = Math.sin(th);
        const lx = cx * r + (cx >= 0 ? hw : -hw), ly = cy * r + (cy >= 0 ? hh : -hh);
        pts.push([E.x + lx * ca - ly * sa, E.y + lx * sa + ly * ca]);
      }
      const end = r * 0.55; w1 = Math.max(w1, end);
      for (let i = 0; i < 36; i++) { const th = (i / 36) * Math.PI * 2; pts.push([tx + Math.cos(th) * end, ty + Math.sin(th) * end]); }
      E.beam.body.setAttribute("points", hull(pts).map(([x, y]) => `${f(x)},${f(y)}`).join(" "));
      E.beam.body.setAttribute("fill", lc); E.beam.body.style.opacity = f(on);
    });
    // where they land: a solid red disc a little wider than the beams
    hit.style.opacity = f(on);
    hitDot.setAttribute("cx", f(tx)); hitDot.setAttribute("cy", f(ty)); hitDot.setAttribute("r", f(w1 * 1.35)); hitDot.setAttribute("fill", lc);
    // burnt scan lines while working, and embers where the beams land
    if (state === "working" && on > 0.5 && !still.matches) {
      const s = scars[scars.length - 1];
      if (s && Math.abs(s.y - ty) < 0.5 && tx > s.x1 - 1) s.x1 = tx; else scars.push({ y: ty, x0: tx, x1: tx, life: 1, node: null });
      if (p.sparks && Math.random() < 0.5) spark(tx, ty, 1, 30, [lc, "#ffd60a"], 0.5);
    } else if (on > 0.5 && p.sparks && Math.random() < 0.15 && !still.matches) spark(tx, ty, 1, 24, [lc], 0.4);
    for (let i = scars.length - 1; i >= 0; i--) {
      const s = scars[i]; s.life -= dt * 0.35;
      if (!s.node) s.node = mk("line", { "stroke-linecap": "round" }, trails);
      s.node.setAttribute("x1", f(s.x0)); s.node.setAttribute("x2", f(s.x1)); s.node.setAttribute("y1", f(s.y)); s.node.setAttribute("y2", f(s.y));
      s.node.setAttribute("stroke-linecap", "round"); s.node.setAttribute("stroke", lc); s.node.setAttribute("stroke-width", f(Math.max(1.2, cur.w * p.laserWidth * 0.28))); s.node.style.opacity = f(Math.max(0, s.life) * 0.7);
      if (s.life <= 0) { s.node.remove(); scars.splice(i, 1); }
    }
    // the spinner ring: brightest at its head, fading behind it
    const head = (t * 0.55) % 1, cy0 = (eyes[0].y + eyes[1].y) / 2 - 2, rx = p.gap + 30, ry = 30;
    spin.forEach((c, i) => {
      const u = i / spin.length, a = (u + head) * Math.PI * 2, tail = (1 - u) ** 2;
      c.setAttribute("cx", f(Math.cos(a) * rx)); c.setAttribute("cy", f(cy0 + Math.sin(a) * ry)); c.setAttribute("r", f(1 + 1.6 * tail));
      c.setAttribute("fill", i % 3 ? color : lc); c.style.opacity = f(cur.think * tail);
    });
    for (let i = parts.length - 1; i >= 0; i--) {
      const q = parts[i]; q.life -= dt;
      if (q.orbit) { const E = eyes[q.orbit.e]; q.orbit.a += q.orbit.s * dt; q.x = E.x + Math.cos(q.orbit.a) * q.orbit.r; q.y = E.y + Math.sin(q.orbit.a) * q.orbit.r * 0.7; if (state !== "thinking") q.life = Math.min(q.life, 0.3); }
      else { q.vy += 90 * dt; q.x += q.vx * dt; q.y += q.vy * dt; }
      if (!q.node) q.node = mk("circle", {}, sparkG);
      q.node.setAttribute("cx", f(q.x)); q.node.setAttribute("cy", f(q.y)); q.node.setAttribute("r", f(q.r)); q.node.setAttribute("fill", q.c);
      q.node.style.opacity = f(Math.max(0, q.life / q.max));
      if (q.life <= 0) { q.node.remove(); parts.splice(i, 1); }
    }
    // listening: ripples out from the eyes
    for (let i = rings.length - 1; i >= 0; i--) {
      const r = rings[i]; r.life -= dt * 1.4; r.r += dt * 60;
      if (!r.node) r.node = mk("circle", { "stroke-width": 1.2 }, ripples);
      r.node.setAttribute("cx", f(r.x)); r.node.setAttribute("cy", f(r.y)); r.node.setAttribute("r", f(r.r)); r.node.setAttribute("stroke", color);
      r.node.style.opacity = f(Math.max(0, r.life) * 0.5);
      if (r.life <= 0) { r.node.remove(); rings.splice(i, 1); }
    }
    svg.style.transform = glitching && Math.random() < 0.3 ? `translate(${f((Math.random() - 0.5) * 4)}px,0)` : "";
  }

  // convex hull (Andrew's monotone chain)
  function hull(pts) {
    pts = pts.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
    const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
    const lo = [], up = [];
    for (const q of pts) { while (lo.length >= 2 && cross(lo[lo.length - 2], lo[lo.length - 1], q) <= 0) lo.pop(); lo.push(q); }
    for (let i = pts.length - 1; i >= 0; i--) { const q = pts[i]; while (up.length >= 2 && cross(up[up.length - 2], up[up.length - 1], q) <= 0) up.pop(); up.push(q); }
    return lo.slice(0, -1).concat(up.slice(0, -1));
  }
  function frame(now) {
    raf = 0;
    const dt = Math.min(0.05, (now - (last || now)) / 1000); last = now;
    t += dt; since += dt; heard = Math.max(0, heard - dt * 3);
    nextBlink -= dt;
    if (nextBlink <= 0 && state !== "error" && state !== "done") { blink = 1; nextBlink = 1.4 + p.patience * 4 + Math.random() * 2; }
    blink = Math.max(0, blink - dt * (6 + (1 - p.patience) * 6));
    nextWander -= dt;
    if (nextWander <= 0) { wander = [Math.random() * 1.6 - 0.8, Math.random() * 1 - 0.5]; nextWander = 0.8 + (1 - p.chattiness) * 2.5 + Math.random(); }
    // gaze rides a spring you can tune; everything else eases
    const goal = target(now), vd = Math.max(0.05, p.gaze?.visualDuration ?? 0.3), z = Math.max(0.05, 1 - (p.gaze?.bounce ?? 0.15)), w = (2 * Math.PI) / vd;
    for (const k of ["gx", "gy"]) { const a = w * w * (goal[k] - cur[k]) - 2 * z * w * vel[k]; vel[k] += a * dt; cur[k] += vel[k] * dt; }
    const e = 1 - Math.exp(-dt * (4 + p.confidence * 16 - p.patience * 2));
    for (const k of ["w", "h", "lean"]) cur[k] += (goal[k] - cur[k]) * e;
    cur.smile += (goal.smile - cur.smile) * (1 - Math.exp(-dt * 10));
    cur.laser += (goal.laser - cur.laser) * (1 - Math.exp(-dt * (state === "error" ? 40 : 12)));
    cur.think += ((state === "thinking" ? 1 : 0) - cur.think) * (1 - Math.exp(-dt * 6));
    draw(dt);
    const busy = parts.length || scars.length || rings.length;
    if ((seen || busy) && !document.hidden && !still.matches) raf = requestAnimationFrame(frame);
  }
  const wake = () => { if (!raf) { last = 0; raf = requestAnimationFrame(frame); } };
  const settle = () => { const g = target(performance.now()); Object.assign(cur, g); blink = 0; draw(0); };

  const onMove = (e) => {
    const r = el.getBoundingClientRect(); if (!r.width) return;
    aim = [Math.max(-1, Math.min(1, (e.clientX - (r.left + r.width / 2)) / (r.width * 1.2))), Math.max(-1, Math.min(1, (e.clientY - (r.top + r.height / 2)) / (r.height * 1.5)))];
    const stage = el.closest(".kstage") || el, sr = stage.getBoundingClientRect();
    over = e.clientX >= sr.left && e.clientX <= sr.right && e.clientY >= sr.top && e.clientY <= sr.bottom;
    const m = svg.getScreenCTM(); if (m) { const pt = new DOMPoint(e.clientX, e.clientY).matrixTransform(m.inverse()); aimPt = [pt.x, pt.y]; }
    lastMove = performance.now();
    if (still.matches) settle(); else wake();
  };
  addEventListener("pointermove", onMove, { passive: true });
  const io = new IntersectionObserver(([en]) => { seen = en.isIntersecting; if (seen) wake(); });
  io.observe(el);
  const onVis = () => !document.hidden && wake();
  document.addEventListener("visibilitychange", onVis);

  settle(); wake();
  return {
    get state() { return state; },
    set(next) {
      if (!STATES.includes(next) || next === state) return;
      state = next; since = 0;
      if (next === "done" && p.sparks && !still.matches) eyes.forEach((E) => spark(E.x, E.y - 6, 26, 70, [p.laserColor, "#ffd60a", "#34c759", "#0a84ff", "#fff"], 1.4));
      if (still.matches) settle(); else wake();
    },
    hear() {
      heard = Math.min(1, heard + 0.6); lastMove = performance.now();
      if (p.ripples && !still.matches && rings.length < 8) eyes.forEach((E) => rings.push({ x: E.x, y: E.y, r: 8, life: 1, node: null }));
      if (p.chattiness > 0.6) blink = 0.6;
      wake();
    },
    tune(next) { Object.assign(p, next); if (still.matches) settle(); else wake(); },
    destroy() { cancelAnimationFrame(raf); io.disconnect(); removeEventListener("pointermove", onMove); document.removeEventListener("visibilitychange", onVis); svg.remove(); },
  };
}

Use

Example
import { mount } from "./two-dots.js";

const eyes = mount(document.querySelector("#agent"), { patience: 0.3, confidence: 0.9 });
input.addEventListener("input", () => { eyes.set("listening"); eyes.hear(); });

Notes

All interfaces