ah ok progress bar moves, but timer doesn't
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Thu, 27 Aug 2026 10:20:56 +0000 (10:20 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Thu, 27 Aug 2026 10:20:56 +0000 (10:20 +0000)
developer/authored/Manuscript.copy/Core/stage_manager.js

index 2009bbf..cb5abd3 100644 (file)
   /* ---------------------------------------------------------------
      The white page ,and the blank that follows it.
 
-     Two faults ,and they were being treated as one.
-
-     The first is that the canvas takes its colour from the root ,and until
-     something sets it the browser paints its own default. The colour was being
-     applied when the theme compiled ,in the element phase — after the whole
-     document had parsed. On a book carrying MathJax that is seconds of white.
-     But the colour is knowable far earlier than that: RT.theme_preference runs
-     in the head ,at parse time ,and RT.load's document.write ordering means
-     every theme is loaded before it. So the theme applies its screen colour
-     the moment it resolves ,and the canvas is never white at all. Remembering
-     the last colour was a workaround for a problem that did not need one; it
-     is kept only for the window before the theme call ,where it costs nothing.
-
-     The second is what the reader is shown while the work is done. Hiding the
-     root hides the progress panel with it — visibility inherits — and leaves
-     the canvas background in doubt ,since a root with visibility hidden is a
-     poor thing to be relying on to paint. So the lock is a class on the root
-     that hides the contents of the body and excepts the panel. Backgrounds
-     paint normally ,because nothing about the root is hidden any more.
-
-     Both live in one stylesheet ,written into the head at parse time.
+     The canvas takes its colour from the root ,and until something sets it the
+     browser paints its own default. The colour was being applied when the
+     theme compiled ,in the element phase — after the whole document had
+     parsed. On a book carrying MathJax that is seconds of white. But the
+     colour is knowable far earlier: RT.theme_preference runs in the head ,at
+     parse time ,and RT.load's document.write ordering means every theme is
+     loaded before it. So the theme applies its screen colour the moment it
+     resolves ,and the canvas is never white at all.
+
+     The colour is written to the root as an inline style ,which is how every
+     other element in this engine is styled. An earlier version of this put it
+     in a style sheet injected into the head. That was wrong twice over: the
+     engine keeps no style sheet ,so it introduced the one surface where
+     unrelated rules would accumulate ,and a sheet built as a string in a
+     source file is worse than a file for the purpose ,being invisible to
+     anyone looking for one.
+
+     The root's background reaches the canvas whether or not the root is
+     visible ,which is what makes the lock below and this colour independent of
+     each other.
+
+     Remembering the last colour is kept ,for the window before the theme call
+     ,where it costs nothing.
   --------------------------------------------------------------- */
 
-  const boot_style_id = 'RT·boot-style';
   const screen_color_key = 'RT-Manuscript·screen_color';
-  const lock_class = 'RT·locked';
-
-  function boot_style(){
-    let el = document.getElementById(boot_style_id);
-    if(el) return el;
-    el = document.createElement('style');
-    el.id = boot_style_id;
-    el.textContent = boot_style_text('');
-    (document.head || document.documentElement).appendChild(el);
-    return el;
-  }
-
-  /* The panel is animated by CSS and not by script ,which is the whole of why
-     it works. A phase holds the main thread from beginning to end ,so anything
-     driven from script stops dead for the length of it — which is exactly the
-     interval the reader most needs to see movement in. Transform and opacity
-     animate off the main thread ,so they keep running while a phase blocks.
-
-     The bar creeps rather than reports. It is not tied to the phases and does
-     not claim to be: it eases toward the end without arriving ,which is honest
-     about not knowing how long the work will take ,and it is always moving ,
-     which is the one thing the reader needs to see.
-
-     The panel fades in on a delay ,so a book that formats quickly never shows
-     it. That is done in the animation rather than in a timer for the same
-     reason as the bar: a timer would not fire. */
-  function boot_style_text(screen_color){
-    return (screen_color ? 'html ,body{ background-color:' + screen_color + '; }\n' : '')
-      + 'html.' + lock_class + ' body > *{ visibility:hidden; }\n'
-      + 'html.' + lock_class + ' #RT·progress{ visibility:visible; }\n'
-      + '#RT·progress{ position:fixed; top:0; left:0; right:0; bottom:0;'
-      + ' z-index:2147483647; display:flex; flex-direction:column;'
-      + ' align-items:center; justify-content:center; gap:1.1rem;'
-      + ' pointer-events:none; color:#8a8a8a; opacity:0;'
-      + " font:400 1rem/1.4 'Noto Sans JP' ,Arial ,sans-serif;"
-      + ' animation:RT·progress-appear 300ms ease-out 400ms forwards; }\n'
-      + '#RT·progress .RT·progress-label{ letter-spacing:0.08em; }\n'
-      + '#RT·progress .RT·progress-dot{ opacity:0.15;'
-      + ' animation:RT·progress-blink 1.4s ease-in-out infinite; }\n'
-      + '#RT·progress .RT·progress-dot:nth-child(2){ animation-delay:0.2s; }\n'
-      + '#RT·progress .RT·progress-dot:nth-child(3){ animation-delay:0.4s; }\n'
-      + '#RT·progress .RT·progress-track{ width:min(18rem ,60vw); height:2px;'
-      + ' background:currentColor; opacity:0.2; overflow:hidden; }\n'
-      + '#RT·progress .RT·progress-bar{ width:100%; height:100%;'
-      + ' background:currentColor; transform:scaleX(0);'
-      + ' transform-origin:left center;'
-      + ' animation:RT·progress-creep 40s cubic-bezier(0 ,0.7 ,0.15 ,1) forwards; }\n'
-      + '#RT·progress .RT·progress-time{ font-size:0.8rem; opacity:0.5;'
-      + ' font-variant-numeric:tabular-nums; }\n'
-      + '@keyframes RT·progress-appear{ to{ opacity:1; } }\n'
-      + '@keyframes RT·progress-blink{ 0% ,100%{ opacity:0.15; } 50%{ opacity:0.9; } }\n'
-      + '@keyframes RT·progress-creep{ to{ transform:scaleX(0.96); } }\n';
-  }
 
-  /* The screen colour ,written where it takes effect before the first paint.
-     Given to this from the theme the moment the theme resolves ,and again from
-     the layout configuration later ,which is the same colour by a longer road
-     and costs nothing to repeat. */
   window.RT.screen_color_apply = function(color){
     if(!color) return;
-    boot_style().textContent = boot_style_text(color);
+    document.documentElement.style.backgroundColor = color;
+    if(document.body) document.body.style.backgroundColor = color;
     try{ localStorage.setItem(screen_color_key ,color); }catch(e){}
   };
 
   }
 
   function prepaint_screen(){
-    boot_style();
-    const color = screen_color_read();
-    if(color) window.RT.screen_color_apply(color);
+    window.RT.screen_color_apply(screen_color_read());
   }
 
   /* ---------------------------------------------------------------
      eight seconds again.
 
      The panel is raised as early as there is a body to hang it on ,which is
-     during parsing and well before the pipeline begins. It is not raised on a
-     timer ,because a timer does not fire while a phase holds the thread ,and
-     the phases are the whole of the wait.
+     during parsing and well before the pipeline begins. Not on a timer: a
+     timer does not fire while a phase holds the thread ,and the phases are the
+     whole of the wait.
+
+     Movement is animated ,and the animations are built through the animation
+     interface rather than declared in key frames ,because key frames cannot be
+     written as an inline style and the engine keeps no style sheet to put them
+     in. The interface takes the same key frames as an argument and hands
+     transform and opacity to the compositor exactly as a declared animation
+     would ,which is the property that matters here: a phase holds the main
+     thread for its whole length ,so anything driven from script stops dead for
+     precisely the interval the reader most needs to see movement in.
+
+     The bar creeps rather than reports. It is not tied to the phases and does
+     not claim to be: it eases toward the end without arriving ,which is honest
+     about not knowing how long the work will take ,and it is always moving.
   --------------------------------------------------------------- */
 
-  const progress = { panel: null ,time: null ,start: 0 ,timer: 0 };
+  const progress = { panel: null ,time: null ,bar: null ,start: 0 ,timer: 0 };
+
+  // Every animation this file starts ,so each is honoured or skipped together.
+  function animate(el ,frame_seq ,timing){
+    if(!el || typeof el.animate !== 'function') return null;
+    try{ return el.animate(frame_seq ,timing); }
+    catch(e){ return null; }
+  }
+
+  function style_write(el ,dict){
+    for(const key in dict) el.style[key] = dict[key];
+    return el;
+  }
 
   function progress_make(){
     const panel = document.createElement('div');
     panel.id = 'RT·progress';
+    style_write(panel ,{
+      position: 'fixed' ,top: '0' ,left: '0' ,right: '0' ,bottom: '0'
+      ,zIndex: '2147483647'
+      /* The root is hidden and visibility inherits ,so the panel says
+         otherwise for itself. */
+      ,visibility: 'visible'
+      ,display: 'flex' ,flexDirection: 'column'
+      ,alignItems: 'center' ,justifyContent: 'center' ,gap: '1.1rem'
+      ,pointerEvents: 'none' ,color: '#8a8a8a'
+      ,font: "400 1rem/1.4 'Noto Sans JP' ,Arial ,sans-serif"
+      ,opacity: '0'
+    });
 
     const label = document.createElement('div');
-    label.className = 'RT·progress-label';
+    style_write(label ,{ letterSpacing: '0.08em' });
     label.appendChild(document.createTextNode('Loading'));
+
+    const dot_seq = [];
     for(let i = 0; i < 3; i++){
       const dot = document.createElement('span');
-      dot.className = 'RT·progress-dot';
       dot.textContent = ' .';
+      style_write(dot ,{ opacity: '0.15' });
       label.appendChild(dot);
+      dot_seq.push(dot);
     }
 
+    /* The dimming is in the track's own colour ,not in an opacity over it. An
+       opacity establishes a group ,and the bar is inside it ,so a translucent
+       track made the bar translucent too — and both were drawn in the same
+       colour ,which left the filled part of the groove identical to the empty
+       part. The bar swept across for the whole of a long load and could not be
+       seen doing it. */
     const track = document.createElement('div');
-    track.className = 'RT·progress-track';
+    style_write(track ,{
+      width: 'min(18rem ,60vw)' ,height: '3px' ,borderRadius: '2px'
+      ,background: 'rgba(138 ,138 ,138 ,0.22)' ,overflow: 'hidden'
+    });
+
     const bar = document.createElement('div');
-    bar.className = 'RT·progress-bar';
+    style_write(bar ,{
+      width: '100%' ,height: '100%' ,borderRadius: '2px'
+      ,background: '#c9c9c9'
+      ,transform: 'scaleX(0)' ,transformOrigin: 'left center'
+      ,willChange: 'transform'
+    });
     track.appendChild(bar);
 
     const time = document.createElement('div');
-    time.className = 'RT·progress-time';
+    style_write(time ,{
+      fontSize: '0.85rem' ,color: '#8f8f8f' ,fontVariantNumeric: 'tabular-nums'
+    });
 
     panel.appendChild(label);
     panel.appendChild(track);
     panel.appendChild(time);
 
+    /* Raised on a delay ,so a book that formats quickly never shows it. In the
+       animation and not in a timer ,for the same reason as everything else
+       here. */
+    animate(panel ,[{ opacity: 0 } ,{ opacity: 1 }]
+      ,{ duration: 300 ,delay: 400 ,easing: 'ease-out' ,fill: 'both' });
+
+    dot_seq.forEach((dot ,i) => animate(dot
+      ,[{ opacity: 0.15 } ,{ opacity: 0.9 } ,{ opacity: 0.15 }]
+      ,{ duration: 1400 ,delay: i * 200 ,iterations: Infinity }));
+
+    progress.bar_animation = animate(bar
+      ,[{ transform: 'scaleX(0)' } ,{ transform: 'scaleX(0.96)' }]
+      ,{ duration: 45000 ,easing: 'cubic-bezier(0 ,0.75 ,0.2 ,1)' ,fill: 'forwards' });
+
+    // Nothing animates where the interface is absent ,so the panel is at least
+    // legible rather than invisible at zero opacity.
+    if(!progress.bar_animation) style_write(panel ,{ opacity: '1' });
+
     progress.panel = panel;
     progress.time = time;
+    progress.bar = bar;
     return panel;
   }
 
-  /* Raised on the first frame at which a body exists. Frames are served while
-     the document is still parsing ,so on a long head — a book carrying MathJax
-     has one — the panel is up before the pipeline has been reached. */
   function progress_raise(){
     if(progress.panel) return;
     progress.start = performance.now();
 
   function progress_end(){
     const panel = progress.panel;
+    const bar = progress.bar;
+    const bar_animation = progress.bar_animation;
     progress.panel = null;
     progress.time = null;
+    progress.bar = null;
+    progress.bar_animation = null;
     if(progress.timer){ clearTimeout(progress.timer); progress.timer = 0; }
     if(!panel || !panel.parentNode) return;
 
-    /* Filled and faded rather than snatched away. An animation outranks an
-       inline declaration ,so each is stood down before its property is set. */
-    const bar = panel.querySelector('.RT·progress-bar');
-    if(bar){
-      bar.style.animation = 'none';
-      bar.style.transition = 'transform 160ms ease-out';
-      bar.style.transform = 'scaleX(1)';
+    // Filled and faded rather than snatched away.
+    if(bar && bar_animation){
+      bar_animation.cancel();
+      style_write(bar ,{ transform: 'scaleX(1)' });
+      animate(bar ,[{ transform: 'scaleX(0.96)' } ,{ transform: 'scaleX(1)' }]
+        ,{ duration: 160 ,easing: 'ease-out' });
     }
-    panel.style.animation = 'none';
-    panel.style.transition = 'opacity 220ms ease-out';
-    panel.style.opacity = '0';
-    progress.timer = setTimeout(() => panel.remove() ,260);
+
+    const fade = animate(panel ,[{ opacity: 1 } ,{ opacity: 0 }]
+      ,{ duration: 220 ,delay: 120 ,easing: 'ease-out' ,fill: 'forwards' });
+
+    if(fade) fade.onfinish = () => panel.remove();
+    else progress.timer = setTimeout(() => panel.remove() ,260);
   }
 
   function lock_layout(){
     is_layout_locked = true;
-    document.documentElement.classList.add(lock_class);
+    document.documentElement.style.visibility = 'hidden';
   }
 
   /* The safety net must not fire while the pipeline is still working.
 
     progress_end();
 
-    document.documentElement.classList.remove(lock_class);
+    document.documentElement.style.visibility = '';
     window.removeEventListener("load" ,unlock_on_load);
     document.dispatchEvent(new Event("RT_layout_complete"));
   }
 
       next_frame(function(){
         window.RT.Debug.log('stage' ,'phase: ' + phase_name);
+
+        /* Per phase timing ,under the existing token. Forty seconds spent
+           somewhere is not a thing to guess at ,and the phase boundaries are
+           already here to be measured between. Costs two clock reads when the
+           token is off. */
+        const phase_start = performance.now();
         run_phase(phase_name);
+        window.RT.Debug.log('stage' ,'  ' + phase_name + ' took '
+          + ((performance.now() - phase_start) / 1000).toFixed(2) + ' s');
 
         /* The reader is let in here ,and the remaining phases go on behind
            them. Scroll is settled first ,or the reader would be shown the top