From 12e9fcd609ac995fefc7173ff1364a4216172a8b Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Sun, 30 Aug 2026 11:38:48 +0200 Subject: [PATCH] Gesture widget: ghost path back, for v2 keys only The relay's ack path is drawn under the stroke when the key's version nibble is >= 2 (ADR-0014's ordered turning chain decodes to the stroke itself); a v1 key's blob stays hidden. Safe to ship ahead of the relay: nothing shows until the relay encodes v2. Co-Authored-By: Claude Fable 5 --- gesture.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/gesture.js b/gesture.js index a04718f..bef2a65 100644 --- a/gesture.js +++ b/gesture.js @@ -12,10 +12,11 @@ // // When a relay URL is given, the widget also speaks the redoal-relay // protocol (ADR-0013 in the redoal repo): announce the stroke, receive -// an ack carrying the stroke's gesture key (the decoded path it also -// carries is ignored: decode_key's sketch is too rough to show), and -// receive echoes of similar strokes other visitors drew, shown as -// thumbnails. +// an ack carrying the stroke's gesture key plus the key's own decoded +// path - "what the network heard", drawn as a ghost under the stroke +// when the key is v2 or later (ADR-0014's ordered turning chain; a v1 +// key's histogram decode is a blob and stays hidden) - and receive +// echoes of similar strokes other visitors drew, shown as thumbnails. // Everything network is best-effort: no relay, refused connection, or // a dropped socket all degrade to a plain offline drawing input. // @@ -28,10 +29,12 @@ const THEMES = { dark: { stroke: '#8ec2c0', echo: '#8ec2c0', + ghost: 'rgba(255, 255, 255, 0.28)', }, light: { stroke: '#47807e', echo: '#47807e', + ghost: 'rgba(29, 29, 27, 0.30)', }, }; @@ -45,6 +48,7 @@ class GestureWidget { this.container = container; this.hidden = hidden || null; this.relayUrl = relayUrl || ''; + this.ghost = null; this.points = []; this.drawing = false; this.stopped = false; @@ -109,8 +113,9 @@ class GestureWidget { e.preventDefault(); this.canvas.setPointerCapture(e.pointerId); // One stroke only - a new pointerdown replaces the old drawing - // (and any ambient trails from the previous round). + // (and any ghost from the previous round). this.drawing = true; + this.ghost = null; this.points = [this.pos(e)]; this.render(); } @@ -212,6 +217,10 @@ class GestureWidget { if (this.points.length >= 2) { this.setValue({ points: this.normalized(), key: msg.key }); } + // Version nibble is the key's first hex digit. + const version = parseInt((msg.key || '0')[0], 16); + this.ghost = version >= 2 && Array.isArray(msg.path) ? msg.path : null; + this.render(); } else if (msg.type === 'echo') { this.addEchoThumbnail(msg); } @@ -268,6 +277,9 @@ class GestureWidget { if (!this.ctx) return; const t = this.theme(); this.ctx.clearRect(0, 0, this.cssWidth, this.cssHeight); + if (this.ghost) { + this.drawPath(this.ctx, this.ghost, this.cssWidth, this.cssHeight, t.ghost, 2, 0.2); + } if (this.points.length >= 1) { this.ctx.strokeStyle = t.stroke; this.ctx.lineWidth = 2;