@@ -38,15 +38,22 @@ setStartBtn.addEventListener('click', ()=>{
3838/* ---------- flip (front/back view) + symbol overlay toggles ---------- */
3939const flipBtn = document . getElementById ( 'flipBtn' ) ;
4040const viewLabel = document . getElementById ( 'viewLabel' ) ;
41- flipBtn . addEventListener ( 'click' , ( ) => {
42- // keep the same cells in view: mirror the pan offset about the pivot
43- // column's screen x (pinned by long-press), or the stage centre when none
44- // is pinned. For a column centre at screen X, front: X = tx+(c+.5)s and
45- // back: X = tx+N*s-(c+.5)s, so preserving X gives tx' = 2X - tx - N*s.
41+ // desktop = fine pointer + hover (mouse/trackpad); phones/tablets are coarse
42+ export const isDesktop = window . matchMedia ( '(hover: hover) and (pointer: fine)' ) . matches ;
43+
44+ /**
45+ * flipView(pivotCol?) - toggle front/back, mirroring the pan offset about a
46+ * column's screen x so the same cells stay in view: pivotCol if given, else
47+ * the long-press-pinned view.pivotCol, else the stage centre. For a column
48+ * centre at screen X, front: X = tx+(c+.5)s and back: X = tx+N*s-(c+.5)s,
49+ * so preserving X gives tx' = 2X - tx - N*s.
50+ */
51+ function flipView ( pivotCol ) {
4652 const gs = N * view . base * view . scale , cs = view . base * view . scale ;
53+ const col = pivotCol != null ? pivotCol : view . pivotCol ;
4754 let X = stage . clientWidth / 2 ;
48- if ( view . pivotCol != null ) {
49- const off = ( view . pivotCol + 0.5 ) * cs ;
55+ if ( col != null ) {
56+ const off = ( col + 0.5 ) * cs ;
5057 X = view . backView ? view . tx + gs - off : view . tx + off ;
5158 }
5259 view . backView = ! view . backView ;
@@ -55,11 +62,32 @@ flipBtn.addEventListener('click', ()=>{
5562 flipBtn . classList . toggle ( 'on' , view . backView ) ;
5663 flipBtn . setAttribute ( 'aria-pressed' , String ( view . backView ) ) ;
5764 viewLabel . textContent = view . backView ? 'Back' : 'Front' ;
58- const lbl = view . backView ? 'Flip to front view' : 'Flip to back view' ;
65+ const lbl = ( view . backView ? 'Flip to front view' : 'Flip to back view' ) + ( isDesktop ? ' (F)' : '' ) ;
5966 flipBtn . setAttribute ( 'aria-label' , lbl ) ;
6067 flipBtn . title = lbl ;
6168 draw ( ) ;
62- } ) ;
69+ }
70+ flipBtn . addEventListener ( 'click' , ( ) => flipView ( ) ) ;
71+
72+ // desktop: F flips about the column under the mouse cursor (no pinning
73+ // needed); falls back to the pinned column / centre when the cursor isn't
74+ // over the grid
75+ let lastMouse = null ;
76+ if ( isDesktop ) {
77+ flipBtn . title = 'Flip to back view (F)' ;
78+ flipBtn . setAttribute ( 'aria-label' , 'Flip to back view (F)' ) ;
79+ stage . addEventListener ( 'pointermove' , e => { if ( e . pointerType === 'mouse' ) lastMouse = { x :e . clientX , y :e . clientY } ; } ) ;
80+ stage . addEventListener ( 'pointerleave' , ( ) => { lastMouse = null ; } ) ;
81+ window . addEventListener ( 'keydown' , e => {
82+ if ( e . key !== 'f' && e . key !== 'F' ) return ;
83+ if ( e . ctrlKey || e . metaKey || e . altKey ) return ;
84+ const t = e . target ;
85+ if ( t && ( t . tagName === 'INPUT' || t . tagName === 'SELECT' || t . tagName === 'TEXTAREA' || t . isContentEditable ) ) return ;
86+ e . preventDefault ( ) ;
87+ const i = lastMouse ? cellAtClient ( lastMouse . x , lastMouse . y ) : - 1 ;
88+ flipView ( i >= 0 ? i % N : undefined ) ;
89+ } ) ;
90+ }
6391
6492const symbolsBtn = document . getElementById ( 'symbolsBtn' ) ;
6593symbolsBtn . addEventListener ( 'click' , ( ) => {
@@ -69,6 +97,19 @@ symbolsBtn.addEventListener('click', ()=>{
6997 draw ( ) ;
7098} ) ;
7199
100+ /* ---------- device-specific hint copy ---------- */
101+ {
102+ const hint = document . querySelector ( 'header .hint' ) ;
103+ const zoomHint = document . getElementById ( 'zoomHint' ) ;
104+ if ( isDesktop ) {
105+ if ( hint ) hint . innerHTML = 'click a colour to isolate it<br>scroll to zoom' ;
106+ if ( zoomHint ) zoomHint . textContent = 'drag to pan · double-click to zoom · F flips around the column under the cursor' ;
107+ } else {
108+ if ( hint ) hint . innerHTML = 'tap a colour to isolate it<br>pinch to zoom' ;
109+ if ( zoomHint ) zoomHint . textContent = 'drag to pan · double-tap to zoom · hold a column to pin it for flipping' ;
110+ }
111+ }
112+
72113/* ---------- marking gesture state ---------- */
73114let gesture = null ; // {startCell, dir, touched:Set, longPressTimer, moved}
74115const LONG_PRESS_MS = 500 ;
0 commit comments