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:
@@ -162,18 +162,42 @@ export function PlanchetteBoard({ machine, hue = 150 }: PlanchetteBoardProps) {
|
|||||||
lastKnock = knockKey
|
lastKnock = knockKey
|
||||||
|
|
||||||
// --- target: spelled letter, else idle wander ---
|
// --- 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
|
let target: Point | null = null
|
||||||
if (snap.current) target = letterTarget(snap.current)
|
if (snap.current) target = letterTarget(snap.current)
|
||||||
if (!target) {
|
if (!target) {
|
||||||
target = {
|
target = {
|
||||||
x: layout.center.x + wander(t, 1.3) * w * 0.16,
|
x: layout.center.x + wander(t, 1.3) * outerRingRadius * 0.94,
|
||||||
y: layout.center.y + wander(t * 0.83, 4.1) * h * 0.14,
|
y: layout.center.y + wander(t * 0.83, 4.1) * outerRingRadius * 0.72 * 0.94,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const speed = snap.current ? 0.16 : 0.03
|
const speed = snap.current ? 0.16 : 0.03
|
||||||
pos.x += (target.x - pos.x) * speed
|
pos.x += (target.x - pos.x) * speed
|
||||||
pos.y += (target.y - pos.y) * 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 ---
|
// --- smoke trail ---
|
||||||
const moving = Math.abs(target.x - pos.x) + Math.abs(target.y - pos.y) > 2
|
const moving = Math.abs(target.x - pos.x) + Math.abs(target.y - pos.y) > 2
|
||||||
if ((moving || snap.current) && smoke.length < 220) {
|
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)
|
ctx.fillText('G O O D B Y E', layout.goodbye.x, layout.goodbye.y)
|
||||||
|
|
||||||
// Letters + numbers.
|
// Letters + numbers.
|
||||||
const hovered = snap.current
|
const hovered = snap.current ?? brushed
|
||||||
const dwelling = snap.phase === 'dwelling' ? snap.current : null
|
const dwelling = snap.phase === 'dwelling' ? snap.current : null
|
||||||
ctx.font = `600 ${Math.max(13, h * 0.04)}px Cinzel, Georgia, serif`
|
ctx.font = `600 ${Math.max(13, h * 0.04)}px Cinzel, Georgia, serif`
|
||||||
for (const [ch, p] of layout.letters) {
|
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`
|
ctx.font = `600 ${Math.max(12, h * 0.034)}px "IBM Plex Mono", monospace`
|
||||||
for (const [ch, p] of layout.numbers) {
|
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).
|
// Smoke (additive).
|
||||||
@@ -325,6 +349,7 @@ function drawGlyph(
|
|||||||
dwell: boolean,
|
dwell: boolean,
|
||||||
accent: string,
|
accent: string,
|
||||||
hueDeg: number,
|
hueDeg: number,
|
||||||
|
soft: boolean,
|
||||||
): void {
|
): void {
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
@@ -335,13 +360,18 @@ function drawGlyph(
|
|||||||
ctx.translate(-p.x, -p.y)
|
ctx.translate(-p.x, -p.y)
|
||||||
}
|
}
|
||||||
ctx.shadowColor = accent
|
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.fillStyle = accent
|
||||||
|
ctx.globalAlpha = soft ? 0.6 : 1
|
||||||
ctx.fillText(ch, p.x, p.y)
|
ctx.fillText(ch, p.x, p.y)
|
||||||
|
ctx.globalAlpha = 1
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.strokeStyle = `hsla(${hueDeg} 90% 75% / ${dwell ? 0.85 : 0.5})`
|
ctx.strokeStyle = `hsla(${hueDeg} 90% 75% / ${dwell ? 0.85 : soft ? 0.28 : 0.5})`
|
||||||
ctx.arc(p.x, p.y, dwell ? 19 : 15, 0, Math.PI * 2)
|
ctx.arc(p.x, p.y, dwell ? 19 : soft ? 13 : 15, 0, Math.PI * 2)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = `hsla(${hueDeg} 40% 82% / 0.75)`
|
ctx.fillStyle = `hsla(${hueDeg} 40% 82% / 0.75)`
|
||||||
|
|||||||
Reference in New Issue
Block a user