From 044a7f804a73e72abab335778ee655916009c7dd Mon Sep 17 00:00:00 2001 From: Kresna <13603341+slaveofcode@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:22:32 +0700 Subject: [PATCH] fix(onet): make the connecting line visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The line was drawn but invisible: strokeWidth 0.14 combined with vectorEffect='non-scaling-stroke' meant 0.14 SCREEN pixels, not 0.14 cells. The overlay was also mispositioned — its hand-picked -8%/116% offsets didn't match the viewBox (8 columns needs -12.5%/125%) and ignored the grid's gap and padding. Now the path is converted to pixel centres measured from the real cells (extrapolating the cell pitch for the one-cell margin), drawn in the grid's own pixel space with overflow visible, so it lines up exactly at any board size and routes that leave the board still show. Added a dark halo behind the line plus endpoint dots for contrast on any tile, and lengthened the flash from 240ms to 420ms so it is actually seen. --- src/islands/games/OnetGame.tsx | 69 +++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/src/islands/games/OnetGame.tsx b/src/islands/games/OnetGame.tsx index b2a54c5..fa55664 100644 --- a/src/islands/games/OnetGame.tsx +++ b/src/islands/games/OnetGame.tsx @@ -55,7 +55,7 @@ export default function OnetGame({ lang = 'en' }: { lang?: Lang }) { const [grid, setGrid] = useState(() => emptyGrid(DIFFICULTIES[1])); const [ready, setReady] = useState(false); const [selected, setSelected] = useState(null); - const [path, setPath] = useState(null); + const [pathPx, setPathPx] = useState<{ x: number; y: number }[] | null>(null); const [hintPair, setHintPair] = useState<[Pos, Pos] | null>(null); const [score, setScore] = useState(0); const [streak, setStreak] = useState(0); @@ -67,6 +67,28 @@ export default function OnetGame({ lang = 'en' }: { lang?: Lang }) { const pathTimer = useRef | null>(null); const noticeTimer = useRef | null>(null); + const gridRef = useRef(null); + + /** + * Convert board cells to pixel centres inside the grid, measured from the + * real cells so gaps/padding/board size can't push the line out of line. + * Handles the one-cell margin (r or c of -1 / rows / cols) by extrapolating + * from the measured cell pitch. + */ + const toPixels = useCallback((pts: Pos[], cols: number): { x: number; y: number }[] | null => { + const el = gridRef.current; + const cells = el?.querySelectorAll(':scope > button'); + if (!el || !cells || cells.length < cols + 2) return null; + const first = cells[0]; + const pitchX = cells[1].offsetLeft - first.offsetLeft; + const pitchY = cells[cols].offsetTop - first.offsetTop; + const w = first.offsetWidth; + const h = first.offsetHeight; + return pts.map((p) => ({ + x: first.offsetLeft + p.c * pitchX + w / 2, + y: first.offsetTop + p.r * pitchY + h / 2, + })); + }, []); useEffect(() => { try { setBest(JSON.parse(localStorage.getItem(BEST_KEY) ?? '{}')); } catch { /* blocked */ } @@ -109,7 +131,7 @@ export default function OnetGame({ lang = 'en' }: { lang?: Lang }) { setDiff(d); setTimed(useTimer); setSelected(null); - setPath(null); + setPathPx(null); setHintPair(null); setScore(0); setStreak(0); @@ -153,8 +175,8 @@ export default function OnetGame({ lang = 'en' }: { lang?: Lang }) { const found = findPath(grid, selected, here); if (!found) { setSelected(here); setStreak(0); return; } - // Show the connecting line briefly, then clear the pair. - setPath(found); + // Show the connecting line, then clear the pair once it has been seen. + setPathPx(toPixels(found, diff.cols)); const cleared = removePair(grid, selected, here); setSelected(null); const nextStreak = streak + 1; @@ -162,9 +184,9 @@ export default function OnetGame({ lang = 'en' }: { lang?: Lang }) { setScore((s) => s + pairScore(nextStreak)); if (pathTimer.current) clearTimeout(pathTimer.current); pathTimer.current = setTimeout(() => { - setPath(null); + setPathPx(null); setGrid(settle(cleared, elapsed)); - }, 240); + }, 420); }; const useHint = () => { @@ -243,6 +265,7 @@ export default function OnetGame({ lang = 'en' }: { lang?: Lang }) {
@@ -271,24 +294,26 @@ export default function OnetGame({ lang = 'en' }: { lang?: Lang }) { }), )} - {/* Connecting line, drawn in cell units with a one-cell margin so - routes that leave the board are visible. */} - {path && ( - + {/* Connecting line. Drawn in the grid's own pixel space (no + viewBox) from measured cell positions, so it lines up exactly + whatever the board size, gap or padding — and overflow-visible + lets routes that leave the board still show. */} + {pathPx && pathPx.length > 1 && ( + + {/* Dark halo first so the line reads on any tile colour. */} + `${p.x},${p.y}`).join(' ')} + fill="none" stroke="rgba(0,0,0,0.55)" strokeWidth={9} + strokeLinecap="round" strokeLinejoin="round" + /> `${p.c + 0.5},${p.r + 0.5}`).join(' ')} - fill="none" - stroke="rgb(74,222,128)" - strokeWidth={0.14} - strokeLinecap="round" - strokeLinejoin="round" - vectorEffect="non-scaling-stroke" + points={pathPx.map((p) => `${p.x},${p.y}`).join(' ')} + fill="none" stroke="rgb(52,211,153)" strokeWidth={5} + strokeLinecap="round" strokeLinejoin="round" /> + {[pathPx[0], pathPx[pathPx.length - 1]].map((p, i) => ( + + ))} )}