@@ -128,7 +128,7 @@ addEventListener('scroll', trackSection, { passive: true })
128128
129129/**
130130 * Below the layout change there is no side for the model to stand on, so each
131- * section with a view has a block of its own (.fig) and the one canvas moves
131+ * section with a view has a stage of its own (.fig) and the one canvas moves
132132 * into whichever block is coming on screen. Moving the element keeps its GL
133133 * context, and inside a block it scrolls with the page natively. The framing
134134 * follows the block rather than the section the reader is in, and nothing is
@@ -141,22 +141,12 @@ const nearness = new Map()
141141const visible = new Set ( )
142142let host = null
143143
144- /** Mockup round: which of the three layouts this page is showing. */
145- const variant = ( document . documentElement . className . match ( / \b m - ( [ a b c ] ) \b / ) || [ ] ) [ 1 ] || 'a'
146-
147- /**
148- * How each layout wants the view placed in its block, as a vertical shift of
149- * the image in normalised coordinates: an opener keeps its heading's room
150- * clear at the foot of the block.
151- */
152- const LENS = { a : 0 , b : 0.3 , c : 0 }
153-
154144function hostFig ( fig ) {
155145 if ( fig === host ) return
156146 host = fig
157147 fig . prepend ( canvas )
158148 hostNote = fig . querySelector ( '.fig-note' )
159- scene . setLens ?. ( { shift : Number ( fig . dataset . frame ) === FUN ? 0 : LENS [ variant ] , phone : true } )
149+ scene . setPhone ?. ( true )
160150 scene . setFrame ( Number ( fig . dataset . frame ) )
161151 measureFun ( )
162152}
@@ -195,22 +185,14 @@ phone.addEventListener('change', () => {
195185 }
196186 canvasHome . parent . insertBefore ( canvas , canvasHome . next )
197187 host = null
198- scene . setLens ?. ( { shift : 0 } )
188+ read ( null )
189+ scene . setPhone ?. ( false )
199190 scene . setFrame ( active )
200191 measureFun ( )
201192} )
202193
203194let hostNote = null
204195
205- // Mockup round only: the chooser between the three phone layouts.
206- {
207- const chooser = document . createElement ( 'nav' )
208- chooser . className = 'mock-switch'
209- chooser . setAttribute ( 'aria-label' , 'Mockup layouts' )
210- chooser . innerHTML = [ 'a' , 'b' , 'c' ] . map ( ( m ) => `<a href="?m=${ m } "${ m === variant ? ' class="on"' : '' } >${ m } </a>` ) . join ( '' )
211- document . body . append ( chooser )
212- }
213-
214196addEventListener ( 'pointermove' , ( event ) => {
215197 scene . setPointer ( ( event . clientX / innerWidth - 0.5 ) * 2 , ( event . clientY / innerHeight - 0.5 ) * 2 )
216198} , { passive : true } )
@@ -242,6 +224,7 @@ function resize () {
242224 const rail = document . querySelector ( '.rail' )
243225 if ( rail ) document . documentElement . style . setProperty ( '--bar-h' , `${ Math . ceil ( rail . getBoundingClientRect ( ) . height ) } px` )
244226 measureFun ( )
227+ watchReading ( )
245228 rules . resize ( )
246229 trackSection ( )
247230}
@@ -253,9 +236,8 @@ addEventListener('orientationchange', resize)
253236/**
254237 * At rest the section's model is its general view. Pointing at a project, or
255238 * tabbing to its link, puts that project's description in the caption under
256- * the model, and the two with data behind them bring up their own views. With
257- * no hover to point with, the view takes turns on its own while the section is
258- * on screen, so a phone still sees both.
239+ * the model, and the two with data behind them bring up their own views. A
240+ * phone has no pointing, and reads the list as it scrolls instead (below).
259241 */
260242const projectRows = [ ...document . querySelectorAll ( '.project' ) ]
261243const vizRows = projectRows . filter ( ( row ) => row . dataset . viz )
@@ -323,30 +305,41 @@ for (const row of projectRows) {
323305showViz ( null )
324306
325307/**
326- * On a phone there is no pointing, so a project with data behind it carries a
327- * button that brings its view up in the section's block, and its title stays
328- * the link it always was. Tapping it again, or another, puts the view back.
308+ * On a phone there is no pointing, but the section's stage holds at the top
309+ * of the screen while its list scrolls up beneath it. So the row passing just
310+ * under the stage is the one being read, and the stage shows its view: a
311+ * project with data brings its own up, any other puts the landscape back. In
312+ * the gaps between rows the last one read keeps the stage, and above the
313+ * first, while the heading is still being read, the landscape has it.
329314 */
330- let picked = null
331- const peeks = vizRows . map ( ( row ) => {
332- const button = document . createElement ( 'button' )
333- button . type = 'button'
334- button . className = 'peek'
335- button . setAttribute ( 'aria-pressed' , 'false' )
336- button . textContent = 'Show the data'
337- button . addEventListener ( 'click' , ( ) => pick ( picked === row ? null : row ) )
338- row . querySelector ( 'h3' ) . after ( button )
339- return button
340- } )
315+ let reading = null
316+ let readingWatch = null
341317
342- function pick ( row ) {
343- picked = row
344- showViz ( row ?. dataset . viz ?? null )
318+ function read ( row ) {
319+ if ( row === reading ) return
320+ reading = row
345321 shownLive = null
346- vizRows . forEach ( ( r , i ) => {
347- peeks [ i ] . setAttribute ( 'aria-pressed' , String ( r === row ) )
348- peeks [ i ] . textContent = r === row ? 'Showing, above' : 'Show the data'
349- } )
322+ showViz ( row ?. dataset . viz ?? null )
323+ }
324+
325+ /**
326+ * The reading line, a pixel tall, a little below the stage's foot. Its place
327+ * depends on the bar and the screen's height, so it is drawn again on resize.
328+ */
329+ function watchReading ( ) {
330+ readingWatch ?. disconnect ( )
331+ if ( ! phone . matches || ! projectRows . length ) return
332+ const rail = document . querySelector ( '.rail' )
333+ const stage = funSection ?. querySelector ( '.fig' )
334+ const foot = ( rail ?. getBoundingClientRect ( ) . height ?? 0 ) + ( stage ?. getBoundingClientRect ( ) . height ?? 0 ) + 16
335+ const line = Math . min ( innerHeight - 2 , Math . round ( foot ) )
336+ readingWatch = new IntersectionObserver ( ( entries ) => {
337+ for ( const entry of entries ) {
338+ if ( entry . isIntersecting ) read ( entry . target )
339+ else if ( entry . target === projectRows [ 0 ] && entry . boundingClientRect . top > line ) read ( null )
340+ }
341+ } , { rootMargin : `${ - line } px 0px ${ - ( innerHeight - line - 1 ) } px 0px` } )
342+ for ( const row of projectRows ) readingWatch . observe ( row )
350343}
351344
352345/**
@@ -362,11 +355,9 @@ document.fonts?.ready.then(() => measureFun())
362355
363356function measureFun ( ) {
364357 if ( ! funSection || ! projectRows . length ) return
365- // On a phone the views fill their own block, inside a margin, and leave
366- // the foot of an opener to its heading.
358+ // On a phone the views fill their own stage, inside a margin.
367359 if ( phone . matches ) {
368- const foot = variant === 'b' ? - 0.05 : - 0.74
369- scene . setFitBox ?. ( { top : 0.7 , bottom : foot , left : - 0.84 , right : 0.84 } )
360+ scene . setFitBox ?. ( { top : 0.7 , bottom : - 0.74 , left : - 0.84 , right : 0.84 } )
370361 return
371362 }
372363 const origin = funSection . getBoundingClientRect ( ) . top
@@ -490,7 +481,7 @@ function tick (now) {
490481 // a phone at the head of its block.
491482 const onPhone = phone . matches
492483 const liveLine = onPhone ? ( Number ( host ?. dataset . frame ) === FUN ? hostNote : null ) : captionLive
493- const describing = onPhone ? Boolean ( picked ) : Boolean ( pointedRow ?. dataset . viz ) && active === FUN
484+ const describing = onPhone ? Boolean ( reading ?. dataset . viz ) : Boolean ( pointedRow ?. dataset . viz ) && active === FUN
494485 if ( describing && liveLine ) {
495486 const live = scene . live ?? ''
496487 if ( live !== shownLive ) {
0 commit comments