1/* Open in Val Town: https://www.val.town/x/stevekrouse/logo/code/frontend/components/Canvas.tsx */ 2import { jsx as _jsx, jsxs as _jsxs } from "https://esm.sh/[email protected]/jsx-runtime"; 3/** @jsxImportSource https://esm.sh/[email protected] */ 4import { useEffect, useRef } from "https://esm.sh/[email protected]"; 5const CSS_NAMED_OK = /^[a-z]+$/i; 6function normalizeColor(c) { 7 // Allow hex or simple named colors; fall back to a default otherwise. 8 if (c.startsWith("#")) 9 return c; 10 if (CSS_NAMED_OK.test(c)) 11 return c; 12 return "#111827"; 13} 14function lerp(a, b, t) { 15 return a + (b - a) * t; 16} 17/** 18 * Walk the timeline of steps and compute what the canvas should look like at 19 * `elapsed` seconds: which segments are fully drawn, the partially-drawn move, 20 * and where/how the turtle is oriented right now. 21 */ 22function frameAt(result, elapsed) { 23 const segments = []; 24 let partial = null; 25 let turtle = { 26 pos: { x: 0, y: 0 }, 27 heading: 0, 28 visible: true, 29 }; 30 let t = elapsed; 31 let done = true; 32 for (const step of result.steps) { 33 if (step.kind === "instant") { 34 if (step.clear) { 35 segments.length = 0; 36 partial = null; 37 } 38 continue; 39 } 40 if (t >= step.duration) { 41 // This step has fully completed. 42 applyCompleted(step, segments); 43 turtle = turtleAfter(step, turtle); 44 t -= step.duration; 45 continue; 46 } 47 // We're mid-step: render its partial progress and stop. 48 const frac = step.duration > 0 ? t / step.duration : 1; 49 done = false; 50 if (step.kind === "move") { 51 const here = { 52 x: lerp(step.from.x, step.to.x, frac), 53 y: lerp(step.from.y, step.to.y, frac), 54 }; 55 if (step.penDown && (step.from.x !== step.to.x || step.from.y !== step.to.y)) { 56 partial = { 57 from: step.from, 58 to: here, 59 color: step.color, 60 width: step.width, 61 }; 62 } 63 turtle = { pos: here, heading: turtle?.heading ?? 0, visible: true }; 64 } 65 else if (step.kind === "turn") { 66 turtle = { 67 pos: step.at, 68 heading: lerp(step.fromHeading, step.toHeading, frac), 69 visible: true, 70 }; 71 } 72 return { segments, partial, turtle, done, line: step.line }; 73 } 74 // Ran past the end of the timeline: everything is drawn. 75 turtle = { 76 pos: { x: result.turtle.x, y: result.turtle.y }, 77 heading: result.turtle.heading, 78 visible: result.turtle.visible, 79 }; 80 return { segments, partial: null, turtle, done, line: null }; 81} 82function applyCompleted(step, segments) { 83 if (step.kind === "move" && step.penDown 84 && (step.from.x !== step.to.x || step.from.y !== step.to.y)) { 85 segments.push({ 86 from: step.from, 87 to: step.to, 88 color: step.color, 89 width: step.width, 90 }); 91 } 92} 93function turtleAfter(step, prev) { 94 const heading = prev?.heading ?? 0; 95 if (step.kind === "move") { 96 // A move doesn't change heading; carry the previous one forward. 97 return { pos: step.to, heading, visible: true }; 98 } 99 if (step.kind === "turn") { 100 return { pos: step.at, heading: step.toHeading, visible: true }; 101 } 102 return prev; 103} 104/** Two steps are equal when they'd draw the same thing (source line ignored). */ 105function stepsEqual(a, b) { 106 if (a.kind !== b.kind || a.duration !== b.duration) 107 return false; 108 if (a.kind === "move" && b.kind === "move") { 109 return a.from.x === b.from.x && a.from.y === b.from.y && 110 a.to.x === b.to.x && a.to.y === b.to.y && 111 a.penDown === b.penDown && a.color === b.color && a.width === b.width; 112 } 113 if (a.kind === "turn" && b.kind === "turn") { 114 return a.at.x === b.at.x && a.at.y === b.at.y && 115 a.fromHeading === b.fromHeading && a.toHeading === b.toHeading; 116 } 117 if (a.kind === "instant" && b.kind === "instant") 118 return !!a.clear === !!b.clear; 119 return false;
120} 121/** Seconds of leading animation two timelines share before they first diverge. */ 122function matchedPrefixDuration(prev, next) { 123 let dur = 0; 124 const n = Math.min(prev.length, next.length); 125 for (let i = 0; i < n; i++) { 126 if (!stepsEqual(prev[i], next[i])) 127 break; 128 dur += next[i].duration; 129 } 130 return dur; 131} 132/** 133 * The right-hand canvas. The turtle starts in the dead center of the canvas 134 * and animates through the drawing at the speed set in the code. 135 */ 136export function Canvas({ result, error, onActiveLine }) { 137 const canvasRef = useRef(null); 138 const wrapRef = useRef(null); 139 const resultRef = useRef(result); 140 resultRef.current = result; 141 // Latest callback + last reported line, so the RAF loop reports the running 142 // line to the editor only when it actually changes. 143 const onActiveLineRef = useRef(onActiveLine); 144 onActiveLineRef.current = onActiveLine; 145 const lastLineRef = useRef(null); 146 // Animation clock. When an edit only changes the tail of the program, keep the 147 // shared prefix "already drawn" and animate just from the first change â so the 148 // turtle continues from where it was instead of replaying from the start. 149 const startRef = useRef(performance.now()); 150 const prevStepsRef = useRef([]); 151 useEffect(() => { 152 const matched = matchedPrefixDuration(prevStepsRef.current, result.steps); 153 startRef.current = performance.now() - matched * 1000; 154 prevStepsRef.current = result.steps; 155 }, [result]); 156 useEffect(() => { 157 const canvas = canvasRef.current; 158 const wrap = wrapRef.current; 159 if (!canvas || !wrap) 160 return; 161 const ctx = canvas.getContext("2d"); 162 if (!ctx) 163 return; 164 let raf = 0; 165 const render = () => { 166 const dpr = window.devicePixelRatio || 1; 167 const { clientWidth: w, clientHeight: h } = wrap; 168 if (canvas.width !== w * dpr || canvas.height !== h * dpr) { 169 canvas.width = w * dpr; 170 canvas.height = h * dpr; 171 canvas.style.width = `${w}px`; 172 canvas.style.height = `${h}px`; 173 } 174 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 175 ctx.clearRect(0, 0, w, h); 176 const cx = w / 2; 177 const cy = h / 2; 178 drawGrid(ctx, w, h, cx, cy); 179 const r = resultRef.current; 180 const elapsed = (performance.now() - startRef.current) / 1000; 181 const frame = frameAt(r, elapsed); 182 // Report the running source line to the editor (only on change). 183 const cb = onActiveLineRef.current; 184 if (cb && frame.line !== lastLineRef.current) { 185 lastLineRef.current = frame.line; 186 cb(frame.line); 187 } 188 for (const seg of frame.segments) 189 strokeSeg(ctx, seg, cx, cy); 190 if (frame.partial) 191 strokeSeg(ctx, frame.partial, cx, cy); 192 if (frame.turtle && frame.turtle.visible) { 193 drawTurtle(ctx, cx + frame.turtle.pos.x, cy + frame.turtle.pos.y, frame.turtle.heading); 194 } 195 raf = requestAnimationFrame(render); 196 }; 197 raf = requestAnimationFrame(render); 198 return () => cancelAnimationFrame(raf); 199 }, []); 200 return (_jsxs("div", { ref: wrapRef, class: "relative w-full h-full bg-white", children: [_jsx("canvas", { ref: canvasRef, class: "block" }), error && (_jsx("div", { class: "absolute bottom-4 left-1/2 -translate-x-1/2 max-w-[90%] max-h-32 overflow-auto bg-red-50 border border-red-300 text-red-700 text-sm px-3 py-2 rounded-lg shadow whitespace-pre-wrap break-words", children: error }))] })); 201} 202function strokeSeg(ctx, seg, cx, cy) { 203 ctx.beginPath(); 204 ctx.strokeStyle = normalizeColor(seg.color); 205 ctx.lineWidth = seg.width; 206 ctx.lineCap = "round"; 207 ctx.moveTo(cx + seg.from.x, cy + seg.from.y); 208 ctx.lineTo(cx + seg.to.x, cy + seg.to.y); 209 ctx.stroke(); 210} 211function drawGrid(ctx, w, h, cx, cy) { 212 const step = 40; 213 ctx.save(); 214 ctx.strokeStyle = "#f1f5f9"; 215 ctx.lineWidth = 1; 216 for (let x = cx % step; x < w; x += step) { 217 ctx.beginPath(); 218 ctx.moveTo(x, 0); 219 ctx.lineTo(x, h); 220 ctx.stroke(); 221 } 222 for (let y = cy % step; y < h; y += step) { 223 ctx.beginPath(); 224 ctx.moveTo(0, y); 225 ctx.lineTo(w, y); 226 ctx.stroke(); 227 } 228 // Center axes, slightly darker. 229 ctx.strokeStyle = "#e2e8f0"; 230 ctx.beginPath(); 231 ctx.moveTo(cx, 0); 232 ctx.lineTo(cx, h); 233 ctx.moveTo(0, cy); 234 ctx.lineTo(w, cy); 235 ctx.stroke();
236 ctx.restore(); 237} 238/** A little triangular turtle pointing in its heading direction. */ 239function drawTurtle(ctx, x, y, headingDeg) { 240 ctx.save(); 241 ctx.translate(x, y); 242 // heading 0 = up; canvas rotation is clockwise-positive from +x axis. 243 ctx.rotate((headingDeg * Math.PI) / 180); 244 ctx.beginPath(); 245 ctx.moveTo(0, -12); // nose 246 ctx.lineTo(8, 9); 247 ctx.lineTo(0, 5); 248 ctx.lineTo(-8, 9); 249 ctx.closePath(); 250 ctx.fillStyle = "#16a34a"; 251 ctx.fill(); 252 ctx.strokeStyle = "#15803d"; 253 ctx.lineWidth = 1.5; 254 ctx.stroke(); 255 ctx.restore(); 256}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.