@@ -117,6 +117,22 @@ function paceIndex(p: number): number {
117117 return best ;
118118}
119119
120+ /**
121+ * A cheap identity for "what the ticker is currently showing".
122+ *
123+ * The ledger is capped, so once a busy day passes the cap its *length* stops
124+ * changing while new lines keep arriving — length alone would silently freeze
125+ * the ticker for the rest of the day. The last entry, on the other hand, is
126+ * new every time something is appended, and the ticker only ever shows the
127+ * tail, so this is exactly as sensitive as the display is.
128+ */
129+ function logSig ( log : { t : number ; text : string } [ ] ) : string {
130+ const last = log [ log . length - 1 ] ;
131+ return last ? `${ log . length } |${ last . t } |${ last . text } ` : '0' ;
132+ }
133+ /** Never equal to any real signature: forces the next push through. */
134+ const LOG_FORCE = '!' ;
135+
120136const randomSeed = ( ) => Math . floor ( Math . random ( ) * 4294967296 ) >>> 0 ;
121137const stepSeed = ( seed : number , d : 1 | - 1 ) => ( ( seed + d + 4294967296 ) % 4294967296 ) >>> 0 ;
122138
@@ -205,7 +221,9 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
205221 const ladderRef = useRef < number [ ] > ( [ 1 ] ) ;
206222 const dirtyRef = useRef ( true ) ;
207223 const reducedRef = useRef ( false ) ;
208- const logLenRef = useRef ( 0 ) ;
224+ /** Identity of the ledger's last line, so "is there anything new?" survives
225+ * the log's own cap. See `logSig` below. */
226+ const logSigRef = useRef ( '' ) ;
209227 const seedRef = useRef ( 0 ) ;
210228 const paceRef = useRef ( 1 ) ;
211229
@@ -227,7 +245,15 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
227245 const q = url . searchParams ;
228246 // A rolled-over day has left its deep-linked hour far behind; keeping it
229247 // would make a reload of this URL show something else entirely.
248+ //
249+ // Otherwise the hour is in the address bar exactly when it is a decision:
250+ // a *held* moment is a framing worth sharing, so it is written down.
251+ // LIVE and playback are not moments at all — they keep the URL clean, and
252+ // a link to them opens on the visitor's own clock.
230253 if ( dropT ) q . delete ( 't' ) ;
254+ else if ( modeRef . current === 'player' && ! playingRef . current ) {
255+ q . set ( 't' , clamp ( tRef . current , 0 , 24 ) . toFixed ( 1 ) ) ;
256+ } else q . delete ( 't' ) ;
231257 if ( nextSeed === todayRef . current ) q . delete ( 'seed' ) ;
232258 else q . set ( 'seed' , String ( nextSeed >>> 0 ) ) ;
233259 if ( nextPace === 1 ) q . delete ( 'pace' ) ;
@@ -302,7 +328,7 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
302328 sceneRef . current = buildGenesisScene ( world . map ) ;
303329 ambRef . current = makeAmbient ( pace0 ) ;
304330 settleAmbient ( sceneRef . current , ambRef . current , snapRef . current ) ;
305- logLenRef . current = snapRef . current . log . length ;
331+ logSigRef . current = logSig ( snapRef . current . log ) ;
306332 setLines ( snapRef . current . log . slice ( - 3 ) ) ;
307333
308334 // Dev hooks for the screenshot harness: start the transport rolling at a
@@ -396,8 +422,9 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
396422
397423 const pushLog = ( ) => {
398424 const snap = snapRef . current ! ;
399- if ( snap . log . length === logLenRef . current ) return ;
400- logLenRef . current = snap . log . length ;
425+ const sig = logSig ( snap . log ) ;
426+ if ( sig === logSigRef . current ) return ;
427+ logSigRef . current = sig ;
401428 setLines ( snap . log . slice ( - 3 ) ) ;
402429 } ;
403430
@@ -413,7 +440,7 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
413440 // cheap, and it is the only way the derived state stays honest.
414441 snapRef . current = world . snapshotAt ( world . map , world . timeline , t ) ;
415442 resetAmbient ( scene , amb , snapRef . current ) ;
416- logLenRef . current = - 1 ;
443+ logSigRef . current = LOG_FORCE ;
417444 } else {
418445 world . advance ( snap , world . timeline , t ) ;
419446 }
@@ -459,7 +486,7 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
459486 resetAmbient ( scene , amb , snapRef . current ) ;
460487 settleAmbient ( scene , amb , snapRef . current ) ;
461488 camRef . current = clampCam ( fitCam ( ) ) ;
462- logLenRef . current = - 1 ;
489+ logSigRef . current = LOG_FORCE ;
463490 pushLog ( ) ;
464491 setSeed ( s ) ;
465492 setValley ( world . map . valleyName ) ;
@@ -496,7 +523,7 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
496523
497524 // The ledger starts over: yesterday's closing line goes out with the
498525 // light, and the founding of the new valley fades in with the pre-dawn.
499- logLenRef . current = - 1 ;
526+ logSigRef . current = LOG_FORCE ;
500527 pushLog ( ) ;
501528 setSeed ( nextSeed ) ;
502529 setValley ( world . map . valleyName ) ;
@@ -608,7 +635,11 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
608635 if ( raw >= 24 ) roll ( stepSeed ( seedRef . current , 1 ) , raw - 24 , false ) ;
609636 else applyT ( raw ) ;
610637 }
611- if ( running && tRef . current >= PREGEN_AT ) ensurePending ( comingSeed ( ) ) ;
638+ // A pre-built world is only worth its ~35MB of baked canvases while
639+ // midnight is actually coming. Scrub or pause back into the day and it is
640+ // dropped again; the next approach to 23:30 rebuilds it.
641+ if ( tRef . current < PREGEN_AT ) pending = null ;
642+ else if ( running ) ensurePending ( comingSeed ( ) ) ;
612643 syncAmbient ( scene , amb , snapRef . current ! ) ;
613644 if ( running && ! reducedRef . current ) {
614645 clock += dt ;
@@ -692,22 +723,36 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
692723 setMode ( 'player' ) ;
693724 } , [ ] ) ;
694725
726+ /** Dragging the scrubber fires a change per pixel; the address bar only has
727+ * to catch up once the hand comes to rest. */
728+ const momentTimer = useRef ( 0 ) ;
729+ const syncMoment = useCallback ( ( ) => {
730+ window . clearTimeout ( momentTimer . current ) ;
731+ momentTimer . current = window . setTimeout (
732+ ( ) => syncUrl ( seedRef . current , paceRef . current , false ) ,
733+ 220
734+ ) ;
735+ } , [ syncUrl ] ) ;
736+ useEffect ( ( ) => ( ) => window . clearTimeout ( momentTimer . current ) , [ ] ) ;
737+
695738 const seek = useCallback (
696739 ( t : number ) => {
697740 detach ( ) ;
698741 api . current ?. applyT ( t ) ;
699742 setTDisp ( clamp ( t , 0 , 24 ) ) ;
700743 api . current ?. paint ( ) ;
744+ syncMoment ( ) ;
701745 } ,
702- [ detach ]
746+ [ detach , syncMoment ]
703747 ) ;
704748
705749 const togglePlay = useCallback ( ( ) => {
706750 detach ( ) ;
707751 playingRef . current = ! playingRef . current ;
708752 setPlaying ( playingRef . current ) ;
709753 api . current ?. paint ( ) ;
710- } , [ detach ] ) ;
754+ syncMoment ( ) ;
755+ } , [ detach , syncMoment ] ) ;
711756
712757 const cycleSpeed = useCallback ( ( ) => {
713758 setSpeedIdx ( ( i ) => {
@@ -744,7 +789,9 @@ export default function TheGenesis({ embed = false }: GenesisProps) {
744789 api . current ?. applyT ( wallClockHours ( ) ) ;
745790 setTDisp ( wallClockHours ( ) ) ;
746791 api . current ?. paint ( ) ;
747- } , [ ] ) ;
792+ // Back on the wall clock, so the deep-linked hour goes out of the URL.
793+ syncMoment ( ) ;
794+ } , [ syncMoment ] ) ;
748795
749796 useEffect ( ( ) => {
750797 const onKey = ( e : KeyboardEvent ) => {
@@ -1249,9 +1296,29 @@ const CSS = `
12491296 .gen-hint { display: none; }
12501297}
12511298
1299+ /* A phone cannot hold the transport in one pill without clipping the end off
1300+ it, so the bar stops being a pill: the controls keep the top row, the day
1301+ gets the full width underneath to scrub along, and the buttons grow to
1302+ something a thumb can actually hit. */
12521303@media (max-width: 620px) {
1304+ .gen-bar {
1305+ left: 10px;
1306+ right: 10px;
1307+ bottom: 12px;
1308+ transform: none;
1309+ max-width: none;
1310+ flex-wrap: wrap;
1311+ gap: 8px;
1312+ padding: 8px 10px;
1313+ border-radius: 20px;
1314+ }
12531315 .gen-clock { display: none; }
1254- .gen-ticker { max-width: 62vw; bottom: 74px; }
1316+ .gen-ico { width: 40px; height: 40px; }
1317+ .gen-speed, .gen-pace, .gen-live { height: 40px; }
1318+ /* The one control that has to sit at the far end of the row. */
1319+ .gen-live { margin-left: auto; }
1320+ .gen-scrub { order: 2; flex: 1 0 100%; min-width: 0; height: 26px; }
1321+ .gen-ticker { max-width: 62vw; bottom: 108px; }
12551322 .gen-wid span { display: none; }
12561323 .gen-world button { font-size: 0.68rem; padding: 0 7px; }
12571324 .gen-corner { right: 10px; top: 10px; gap: 6px; }
0 commit comments