88 */
99
1010import { parse } from "posecode-parser" ;
11- import { createViewer } from "posecode-render" ;
11+ import type { Viewer } from "posecode-render" ;
1212import { buildNiceShareHash , resolveSharedSource } from "./nice-share.js" ;
1313import { createPosecodeEditor , type PosecodeEditor } from "./editor.js" ;
1414import { PRESETS } from "./presets.js" ;
@@ -36,9 +36,11 @@ const shareBtn = $<HTMLButtonElement>("share");
3636const tabEditor = $ < HTMLButtonElement > ( "tab-editor" ) ;
3737const tabViewer = $ < HTMLButtonElement > ( "tab-viewer" ) ;
3838
39- const viewer = createViewer ( canvas ) ;
40- // Exposed for capture/e2e tooling (frame capture drives README GIFs).
41- ( window as unknown as Record < string , unknown > ) . __posecodeViewer = viewer ;
39+ // Three.js is heavy (~530 kB). Like the landing page, we load the renderer
40+ // *after* the editor shell paints (dynamic import → its own chunk) so first
41+ // paint and interactivity aren't blocked by it. Until `boot()` resolves the
42+ // import, `viewer` is null and viewer-dependent work is skipped/deferred.
43+ let viewer : Viewer | null = null ;
4244let scrubbing = false ;
4345let repeat = 1 ;
4446let rep = 1 ;
@@ -59,32 +61,35 @@ function paintScrub(): void {
5961 scrub . style . setProperty ( "--pct" , `${ Number ( scrub . value ) / 10 } %` ) ;
6062}
6163
62- viewer . onPhase ( ( { phaseName, cue } ) => {
63- phaseEl . textContent = phaseName === "reset" ? "" : phaseName ;
64- cueEl . textContent = cue ?? "" ;
65- highlightChip ( phaseName ) ;
66- // Light up the step block driving this phase (cleared between loops / on reset).
67- const range = phaseName === "reset" ? undefined : phaseRanges . get ( phaseName ) ;
68- editorApi ?. highlightPhase ( range ? range . from : null , range ?. to ) ;
69- } ) ;
70- viewer . onTick ( ( time , duration ) => {
71- if ( ! scrubbing ) {
72- scrub . value = String ( Math . round ( ( time / ( duration || 1 ) ) * 1000 ) ) ;
73- paintScrub ( ) ;
74- }
75- clock . textContent = `${ time . toFixed ( 1 ) } s` ;
76- } ) ;
77- viewer . onLoop ( ( ) => {
78- rep = ( rep % repeat ) + 1 ;
79- updateReps ( ) ;
80- } ) ;
64+ /** Wire the viewer's playback callbacks. Runs once, after the renderer loads. */
65+ function wireViewer ( v : Viewer ) : void {
66+ v . onPhase ( ( { phaseName, cue } ) => {
67+ phaseEl . textContent = phaseName === "reset" ? "" : phaseName ;
68+ cueEl . textContent = cue ?? "" ;
69+ highlightChip ( phaseName ) ;
70+ // Light up the step block driving this phase (cleared between loops / on reset).
71+ const range = phaseName === "reset" ? undefined : phaseRanges . get ( phaseName ) ;
72+ editorApi ?. highlightPhase ( range ? range . from : null , range ?. to ) ;
73+ } ) ;
74+ v . onTick ( ( time , duration ) => {
75+ if ( ! scrubbing ) {
76+ scrub . value = String ( Math . round ( ( time / ( duration || 1 ) ) * 1000 ) ) ;
77+ paintScrub ( ) ;
78+ }
79+ clock . textContent = `${ time . toFixed ( 1 ) } s` ;
80+ } ) ;
81+ v . onLoop ( ( ) => {
82+ rep = ( rep % repeat ) + 1 ;
83+ updateReps ( ) ;
84+ } ) ;
85+ }
8186
8287function updateReps ( ) : void {
8388 reps . textContent = repeat > 1 ? `rep ${ rep } / ${ repeat } ` : "" ;
8489}
8590
8691function buildRibbonAndMarkers ( ) : void {
87- const tl = viewer . getTimeline ( ) ;
92+ const tl = viewer ? .getTimeline ( ) ;
8893 ribbon . innerHTML = "" ;
8994 markers . innerHTML = "" ;
9095 if ( ! tl ) return ;
@@ -94,7 +99,7 @@ function buildRibbonAndMarkers(): void {
9499 chip . textContent = seg . name ;
95100 chip . dataset . name = seg . name ;
96101 chip . title = seg . cue ?? "" ;
97- chip . addEventListener ( "click" , ( ) => viewer . seek ( seg . start + 1e-3 ) ) ;
102+ chip . addEventListener ( "click" , ( ) => viewer ? .seek ( seg . start + 1e-3 ) ) ;
98103 ribbon . append ( chip ) ;
99104
100105 if ( seg . start > 0 ) {
@@ -154,13 +159,15 @@ function recompile(): void {
154159 // error state. Show a hint instead of parse errors and stop the playback.
155160 warnings . innerHTML =
156161 '<div class="row hint">Blank editor. Paste a movement from your AI chat, or pick one from the library.</div>' ;
157- viewer . pause ( ) ;
162+ viewer ? .pause ( ) ;
158163 setPlaying ( false ) ;
159164 return ;
160165 }
161166 const { ir, errors, warnings : warns } = parse ( source ) ;
162167 renderWarnings ( warnings , errors , warns ) ;
163- if ( ir ) {
168+ // Before the renderer finishes loading, we still parse + surface warnings;
169+ // the viewer-dependent work re-runs once `boot()` calls recompile() again.
170+ if ( ir && viewer ) {
164171 viewer . load ( ir ) ;
165172 viewer . setLoop ( loop . checked ) ;
166173 viewer . play ( ) ;
@@ -353,18 +360,19 @@ setMobileView("viewer");
353360
354361// --- Transport ---
355362playpause . addEventListener ( "click" , ( ) => {
356- setPlaying ( viewer . toggle ( ) ) ;
363+ if ( viewer ) setPlaying ( viewer . toggle ( ) ) ;
357364} ) ;
358365scrub . addEventListener ( "input" , ( ) => {
366+ if ( ! viewer ) return ;
359367 scrubbing = true ;
360368 paintScrub ( ) ;
361369 viewer . seek ( ( Number ( scrub . value ) / 1000 ) * viewer . duration ) ;
362370} ) ;
363371scrub . addEventListener ( "change" , ( ) => {
364372 scrubbing = false ;
365373} ) ;
366- loop . addEventListener ( "change" , ( ) => viewer . setLoop ( loop . checked ) ) ;
367- speed . addEventListener ( "change" , ( ) => viewer . setSpeed ( Number ( speed . value ) ) ) ;
374+ loop . addEventListener ( "change" , ( ) => viewer ? .setLoop ( loop . checked ) ) ;
375+ speed . addEventListener ( "change" , ( ) => viewer ? .setSpeed ( Number ( speed . value ) ) ) ;
368376
369377// --- Button label feedback ---
370378// Swap a button's label (the inner `.lbl` span when present, else the button
@@ -476,4 +484,17 @@ editorApi = createPosecodeEditor($("editor"), {
476484 doc : initialDoc ,
477485 onChange : scheduleRecompile ,
478486} ) ;
487+ // Parse + surface warnings immediately so the editor is useful at first paint,
488+ // even though the 3D figure appears a beat later once the renderer chunk loads.
479489recompile ( ) ;
490+
491+ // Boot the renderer after first paint: dynamic import keeps Three.js off the
492+ // critical path (its own chunk), mirroring the landing page. Once the viewer
493+ // exists we wire its callbacks and recompile so the current doc animates.
494+ void import ( "posecode-render" ) . then ( ( { createViewer } ) => {
495+ viewer = createViewer ( canvas ) ;
496+ // Exposed for capture/e2e tooling (frame capture drives README GIFs).
497+ ( window as unknown as Record < string , unknown > ) . __posecodeViewer = viewer ;
498+ wireViewer ( viewer ) ;
499+ recompile ( ) ;
500+ } ) ;
0 commit comments