@@ -28,6 +28,82 @@ type GlyphPath = {
2828
2929const clamp01 = ( value : number ) => Math . min ( 1 , Math . max ( 0 , value ) )
3030const fontCache = new Map < string , Promise < Font > > ( )
31+ const DRAW_TEXT_TRACKER_KEY = "__frameScript_DrawTextTracker"
32+
33+ type DrawTextTracker = {
34+ pending : number
35+ start : ( ) => ( ) => void
36+ wait : ( ) => Promise < void >
37+ }
38+
39+ const getDrawTextTracker = ( ) => {
40+ const g = globalThis as unknown as Record < string , unknown >
41+ const existing = g [ DRAW_TEXT_TRACKER_KEY ] as DrawTextTracker | undefined
42+ if ( existing ) return existing
43+
44+ let pending = 0
45+ const waiters = new Set < ( ) => void > ( )
46+
47+ const notifyIfReady = ( ) => {
48+ if ( pending !== 0 ) return
49+ for ( const resolve of Array . from ( waiters ) ) {
50+ resolve ( )
51+ }
52+ waiters . clear ( )
53+ }
54+
55+ const tracker : DrawTextTracker = {
56+ get pending ( ) {
57+ return pending
58+ } ,
59+ start : ( ) => {
60+ pending += 1
61+ let done = false
62+ return ( ) => {
63+ if ( done ) return
64+ done = true
65+ pending = Math . max ( 0 , pending - 1 )
66+ notifyIfReady ( )
67+ }
68+ } ,
69+ wait : ( ) => {
70+ if ( pending === 0 ) return Promise . resolve ( )
71+ return new Promise < void > ( ( resolve ) => {
72+ waiters . add ( resolve )
73+ } )
74+ } ,
75+ }
76+
77+ g [ DRAW_TEXT_TRACKER_KEY ] = tracker
78+ return tracker
79+ }
80+
81+ const installDrawTextApi = ( ) => {
82+ if ( typeof window === "undefined" ) return
83+ const tracker = getDrawTextTracker ( )
84+ const waitDrawTextReady = async ( ) => {
85+ while ( true ) {
86+ if ( tracker . pending === 0 ) {
87+ if ( typeof window . requestAnimationFrame !== "function" ) {
88+ return
89+ }
90+ await new Promise < void > ( ( resolve ) => window . requestAnimationFrame ( ( ) => resolve ( ) ) )
91+ if ( tracker . pending === 0 ) return
92+ }
93+ await tracker . wait ( )
94+ }
95+ }
96+
97+ ; ( window as any ) . __frameScript = {
98+ ...( window as any ) . __frameScript ,
99+ waitDrawTextReady,
100+ getDrawTextPending : ( ) => tracker . pending ,
101+ }
102+ }
103+
104+ if ( typeof window !== "undefined" ) {
105+ installDrawTextApi ( )
106+ }
31107
32108const buildFontUrl = ( path : string ) => {
33109 const url = new URL ( "http://localhost:3000/file" )
@@ -103,12 +179,32 @@ export const DrawText = ({
103179 const [ boxSize , setBoxSize ] = useState ( { width : 0 , height : 0 } )
104180 const pathRefs = useRef < Array < SVGPathElement | null > > ( [ ] )
105181 const [ glyphLengths , setGlyphLengths ] = useState < number [ ] > ( [ ] )
182+ const [ glyphLoadId , setGlyphLoadId ] = useState ( 0 )
183+ const loadIdRef = useRef ( 0 )
184+ const pendingFinishRef = useRef < ( ( ) => void ) | null > ( null )
185+
186+ const beginPending = ( ) => {
187+ loadIdRef . current += 1
188+ if ( ! pendingFinishRef . current ) {
189+ pendingFinishRef . current = getDrawTextTracker ( ) . start ( )
190+ }
191+ return loadIdRef . current
192+ }
193+
194+ const endPending = ( ) => {
195+ if ( pendingFinishRef . current ) {
196+ pendingFinishRef . current ( )
197+ pendingFinishRef . current = null
198+ }
199+ }
106200
107201 useEffect ( ( ) => {
202+ const loadId = beginPending ( )
108203 let cancelled = false
109204 const lines = text . split ( / \r ? \n / )
110205 if ( ! fontUrl || lines . length === 0 ) {
111206 setGlyphs ( [ ] )
207+ setGlyphLoadId ( loadId )
112208 setViewBox ( ( prev ) => ( prev === "0 0 0 0" ? prev : "0 0 0 0" ) )
113209 setBoxSize ( ( prev ) => ( prev . width === 0 && prev . height === 0 ? prev : { width : 0 , height : 0 } ) )
114210 return
@@ -167,12 +263,14 @@ export const DrawText = ({
167263 prev . width === width && prev . height === height ? prev : { width, height } ,
168264 )
169265 setGlyphs ( nextGlyphs )
266+ setGlyphLoadId ( loadId )
170267 setGlyphLengths ( [ ] )
171268 } )
172269 . catch ( ( error ) => {
173270 if ( cancelled ) return
174271 console . error ( "DrawText: failed to load font" , error )
175272 setGlyphs ( [ ] )
273+ setGlyphLoadId ( loadId )
176274 setViewBox ( ( prev ) => ( prev === "0 0 0 0" ? prev : "0 0 0 0" ) )
177275 setBoxSize ( ( prev ) => ( prev . width === 0 && prev . height === 0 ? prev : { width : 0 , height : 0 } ) )
178276 } )
@@ -208,6 +306,23 @@ export const DrawText = ({
208306 } )
209307 } , [ glyphs ] )
210308
309+ useEffect ( ( ) => {
310+ if ( glyphLoadId !== loadIdRef . current ) return
311+ if ( glyphs . length === 0 ) {
312+ endPending ( )
313+ return
314+ }
315+ if ( glyphLengths . length === glyphs . length ) {
316+ endPending ( )
317+ }
318+ } , [ glyphLoadId , glyphLengths , glyphs ] )
319+
320+ useEffect ( ( ) => {
321+ return ( ) => {
322+ endPending ( )
323+ }
324+ } , [ ] )
325+
211326 const drawCount = glyphs . reduce (
212327 ( count , glyph , index ) => count + ( ! glyph . isGap && ( glyphLengths [ index ] ?? 0 ) > 0 ? 1 : 0 ) ,
213328 0 ,
0 commit comments