fix: planchette idle wander was confined to a tiny central zone

The wander amplitude (w*0.16, h*0.14) was a fixed fraction of the canvas
with no relationship to the outer letter ring's actual radius
(min(w*0.4, h*0.52) — set in computeLayout's arc() calls), so idle drift
could never reach anywhere near the outer letters (A/M/N/Z, the arc tops).
Tied the wander radius directly to that same ring radius/squash factor
instead, so it genuinely roams the whole board.

Also added an ambient "letter brushing" effect: while idly wandering (not
actively spelling a real reply), a letter the planchette drifts near gets
a faint glow — visibly dimmer than the bright hover/dwell glow used for
real spelled letters, so a passing brush never reads as the spirit
actually saying something. Purely cosmetic, no game-state changes.
This commit is contained in:
Indiana
2026-07-25 17:35:42 +00:00
parent 5325a7c782
commit a4103818d3

View File

@@ -162,18 +162,42 @@ export function PlanchetteBoard({ machine, hue = 150 }: PlanchetteBoardProps) {
lastKnock = knockKey
// --- target: spelled letter, else idle wander ---
// Wander radius is tied directly to the outer letter ring's own
// radius/squash (computeLayout's `arc('ABCDEFGHIJKLM', ...)` call) so
// the drift genuinely reaches the board's edges — not a fixed
// fraction of the canvas that happens to undershoot the ring.
const outerRingRadius = Math.min(w * 0.4, h * 0.52)
let target: Point | null = null
if (snap.current) target = letterTarget(snap.current)
if (!target) {
target = {
x: layout.center.x + wander(t, 1.3) * w * 0.16,
y: layout.center.y + wander(t * 0.83, 4.1) * h * 0.14,
x: layout.center.x + wander(t, 1.3) * outerRingRadius * 0.94,
y: layout.center.y + wander(t * 0.83, 4.1) * outerRingRadius * 0.72 * 0.94,
}
}
const speed = snap.current ? 0.16 : 0.03
pos.x += (target.x - pos.x) * speed
pos.y += (target.y - pos.y) * speed
// --- ambient letter-brushing: during idle wander (not actively
// spelling), a letter the planchette happens to drift near gets a
// soft "passively brushed" glow — distinct from the brighter
// hover/dwell glow used for real spelling, so it never reads as the
// spirit actually saying something. Purely cosmetic, no game state.
let brushed: string | null = null
if (!snap.current) {
let best = Infinity
for (const [ch, p] of layout.letters) {
const d = Math.hypot(p.x - pos.x, p.y - pos.y)
if (d < best) {
best = d
brushed = ch
}
}
const BRUSH_RADIUS = Math.max(26, h * 0.06)
if (best > BRUSH_RADIUS) brushed = null
}
// --- smoke trail ---
const moving = Math.abs(target.x - pos.x) + Math.abs(target.y - pos.y) > 2
if ((moving || snap.current) && smoke.length < 220) {
@@ -233,15 +257,15 @@ export function PlanchetteBoard({ machine, hue = 150 }: PlanchetteBoardProps) {
ctx.fillText('G O O D B Y E', layout.goodbye.x, layout.goodbye.y)
// Letters + numbers.
const hovered = snap.current
const hovered = snap.current ?? brushed
const dwelling = snap.phase === 'dwelling' ? snap.current : null
ctx.font = `600 ${Math.max(13, h * 0.04)}px Cinzel, Georgia, serif`
for (const [ch, p] of layout.letters) {
drawGlyph(ctx, ch, p, hovered === ch, dwelling === ch, accent, hueDeg)
drawGlyph(ctx, ch, p, hovered === ch, dwelling === ch, accent, hueDeg, ch === brushed)
}
ctx.font = `600 ${Math.max(12, h * 0.034)}px "IBM Plex Mono", monospace`
for (const [ch, p] of layout.numbers) {
drawGlyph(ctx, ch, p, hovered === ch, dwelling === ch, accent, hueDeg)
drawGlyph(ctx, ch, p, hovered === ch, dwelling === ch, accent, hueDeg, false)
}
// Smoke (additive).
@@ -325,6 +349,7 @@ function drawGlyph(
dwell: boolean,
accent: string,
hueDeg: number,
soft: boolean,
): void {
if (hovered) {
ctx.save()
@@ -335,13 +360,18 @@ function drawGlyph(
ctx.translate(-p.x, -p.y)
}
ctx.shadowColor = accent
ctx.shadowBlur = dwell ? 28 : 16
// Ambient brush (idle drift passing near, `soft`) gets a faint glow —
// deliberately dimmer than a real spelled letter, so it never reads as
// the spirit actually saying something, just the planchette stirring.
ctx.shadowBlur = dwell ? 28 : soft ? 9 : 16
ctx.fillStyle = accent
ctx.globalAlpha = soft ? 0.6 : 1
ctx.fillText(ch, p.x, p.y)
ctx.globalAlpha = 1
ctx.restore()
ctx.beginPath()
ctx.strokeStyle = `hsla(${hueDeg} 90% 75% / ${dwell ? 0.85 : 0.5})`
ctx.arc(p.x, p.y, dwell ? 19 : 15, 0, Math.PI * 2)
ctx.strokeStyle = `hsla(${hueDeg} 90% 75% / ${dwell ? 0.85 : soft ? 0.28 : 0.5})`
ctx.arc(p.x, p.y, dwell ? 19 : soft ? 13 : 15, 0, Math.PI * 2)
ctx.stroke()
} else {
ctx.fillStyle = `hsla(${hueDeg} 40% 82% / 0.75)`