@three-blocks/devtools 11.0.0 → 11.1.0-alpha.395.g9b8ccb48a515
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/NOTICE +26 -0
- package/README.md +117 -170
- package/dist/asset-optimize-YJGWKD4Y.js +1 -0
- package/dist/asset-transforms-ZVZ4RD4L.js +11 -0
- package/dist/browser/shader-capture.js +2 -2
- package/dist/browser-smoke-4MYMHKWB.js +1 -0
- package/dist/chunk-4SVF5BRM.js +2 -0
- package/dist/chunk-7X4DHW36.js +1 -0
- package/dist/chunk-BBODS5RH.js +5 -0
- package/dist/{chunk-IILE5RBI.js → chunk-CMMYXGQ3.js} +1 -2
- package/dist/chunk-DCLDEPPB.js +253 -0
- package/dist/{chunk-5L2VS3BN.js → chunk-ECQE4D2U.js} +1 -2
- package/dist/chunk-IWQV456Z.js +3 -0
- package/dist/chunk-LSH64HEU.js +80 -0
- package/dist/{chunk-X5OFYHIY.js → chunk-MIJXPQPM.js} +2 -3
- package/dist/chunk-NDIPQLTU.js +16 -0
- package/dist/{chunk-NPLE7PPF.js → chunk-NJITVI23.js} +1 -2
- package/dist/chunk-O5FU2ASS.js +16 -0
- package/dist/chunk-QQKHDH26.js +1 -0
- package/dist/chunk-S5LI2C2H.js +5 -0
- package/dist/chunk-SBUKHO24.js +1 -0
- package/dist/chunk-TK566QET.js +1 -0
- package/dist/{chunk-65VEJRRT.js → chunk-WNSGAV6N.js} +1 -2
- package/dist/chunk-ZG3LO3VB.js +1 -0
- package/dist/{chunk-UDUICSME.js → chunk-ZTHZE36A.js} +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +26 -26
- package/dist/environment-bake-ON5I27N7.js +1 -0
- package/dist/index.d.ts +24 -12
- package/dist/index.js +1 -279
- package/dist/{shader-autowire-SPE5DM2X.js → shader-autowire-D2EOG6GF.js} +1 -2
- package/dist/shader-browser-6ICBHK3S.js +1 -0
- package/dist/shader-capture-server.d.ts +1 -1
- package/dist/shader-capture-server.js +1 -1
- package/dist/{shader-config-EOFU5D3Y.js → shader-config-AKZEKIRB.js} +1 -2
- package/dist/{shader-parity-QXJWR6OZ.js → shader-parity-7DEPQEDI.js} +1 -1
- package/dist/{shader-project-runner-S2AID2RJ.js → shader-project-runner-NV4GKHDX.js} +1 -2
- package/dist/shader-watch-JBUA4LP5.js +1 -0
- package/dist/shader-workflow-5EHHR32D.js +1 -0
- package/dist/status-GDVSLQWV.js +1 -0
- package/dist/text-atlas-YYWV7S6F.js +1 -0
- package/dist/transform.cjs +1 -1
- package/dist/transform.js +1 -1
- package/dist/webpack-loader.cjs +1 -1
- package/dist/webpack-loader.js +1 -1
- package/node/index.d.mts +70 -0
- package/node/index.mjs +407 -0
- package/node/ipc.mjs +31 -0
- package/node/layout.mjs +546 -0
- package/node/loader.mjs +103 -0
- package/node/media.mjs +454 -0
- package/node/recording.mjs +67 -0
- package/node/worker.mjs +1314 -0
- package/package.json +27 -11
- package/dist/asset-optimize-GG72JCOX.js +0 -6
- package/dist/browser-smoke-KFF7XV7F.js +0 -254
- package/dist/chunk-45AVKQQK.js +0 -80
- package/dist/chunk-5FT6AZD6.js +0 -82
- package/dist/chunk-7N6TU4JG.js +0 -17
- package/dist/chunk-EI7MMDWY.js +0 -1
- package/dist/chunk-HOZDD6PW.js +0 -2
- package/dist/chunk-LKXY5YU2.js +0 -17
- package/dist/chunk-MIUIIPS4.js +0 -1
- package/dist/chunk-NF4RFSUJ.js +0 -18
- package/dist/chunk-QJFPNM6W.js +0 -2
- package/dist/chunk-ZT6T2W4B.js +0 -2
- package/dist/chunk-ZZLAZ7GJ.js +0 -6
- package/dist/environment-bake-BGMGFAVK.js +0 -4
- package/dist/shader-browser-H5CB2ZQX.js +0 -2
- package/dist/shader-browser-UXXR6QI2.js +0 -1
- package/dist/shader-parity-XPKQQK26.js +0 -2
- package/dist/shader-watch-EORZ2SEC.js +0 -2
- package/dist/shader-workflow-7NQFDQI5.js +0 -2
- package/dist/status-PTBB4SW2.js +0 -2
- package/dist/text-atlas-UORTLOQJ.js +0 -2
- /package/dist/{chunk-TRHSN2DJ.cjs → chunk-LMLJIVNY.cjs} +0 -0
package/node/layout.mjs
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import Yoga from 'yoga-layout';
|
|
2
|
+
import { createCanvas, GlobalFonts, ImageData, loadImage } from '@napi-rs/canvas';
|
|
3
|
+
import { transform } from 'lightningcss';
|
|
4
|
+
import { parse as parseSelector, stringify as stringifySelector } from 'css-what';
|
|
5
|
+
|
|
6
|
+
const inherited = [ 'font-family', 'font-size', 'font-weight', 'font-style', 'line-height', 'letter-spacing', 'color', 'text-align', 'text-transform', 'visibility' ];
|
|
7
|
+
const camel = name => name.replace( /-([a-z])/g, ( _, letter ) => letter.toUpperCase() );
|
|
8
|
+
const kebab = name => name.replace( /[A-Z]/g, letter => `-${letter.toLowerCase()}` );
|
|
9
|
+
const rect = ( x, y, width, height ) => ( { x, y, width, height, left: x, top: y, right: x + width, bottom: y + height, toJSON() { return { x, y, width, height }; } } );
|
|
10
|
+
// Skia's canvas font parser treats intermediate CSS weights (e.g. 650) as sizes.
|
|
11
|
+
const fontFor = style => `${style[ 'font-style' ] || 'normal'} ${Number( style[ 'font-weight' ] ) ? Math.max( 100, Math.min( 900, Math.round( Number( style[ 'font-weight' ] ) / 100 ) * 100 ) ) : style[ 'font-weight' ]} ${style[ 'font-size' ]} ${style[ 'font-family' ]}`;
|
|
12
|
+
|
|
13
|
+
// CSS block/flex layout and measured text for renderer hosts and DOM-synced text.
|
|
14
|
+
// ponytail: supports block/flex, overlapping cells and fixed-column grids used by capture pages; complex grids/floats need a native layout engine extension.
|
|
15
|
+
export async function installNativeLayout( dom ) {
|
|
16
|
+
|
|
17
|
+
const { document } = dom;
|
|
18
|
+
for ( const link of document.querySelectorAll( 'link[rel="stylesheet"]' ) ) {
|
|
19
|
+
|
|
20
|
+
const response = await fetch( link.getAttribute( 'href' ) );
|
|
21
|
+
if ( ! response.ok ) throw new Error( `Stylesheet ${link.href}: HTTP ${response.status}` );
|
|
22
|
+
const style = document.createElement( 'style' ); style.textContent = await response.text(); style.dataset.nativeURL = response.url;
|
|
23
|
+
link.after( style );
|
|
24
|
+
|
|
25
|
+
}
|
|
26
|
+
const context = createCanvas( 1, 1 ).getContext( '2d' );
|
|
27
|
+
const colors = new Map();
|
|
28
|
+
const backgroundImages = new Map();
|
|
29
|
+
const imageURL = value => /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*?))\s*\)/.exec( value ?? '' )?.slice( 1 ).find( value => value !== undefined );
|
|
30
|
+
const rgba = value => {
|
|
31
|
+
|
|
32
|
+
if ( colors.has( value ) ) return colors.get( value );
|
|
33
|
+
const mix = /^color-mix\(in srgb,\s*(.+?)\s+([\d.]+)%,\s*(.+?)(?:\s+([\d.]+)%)?\)$/.exec( value );
|
|
34
|
+
let result;
|
|
35
|
+
if ( mix ) {
|
|
36
|
+
|
|
37
|
+
const a = rgba( mix[ 1 ] ), b = rgba( mix[ 3 ] );
|
|
38
|
+
let weight = Number( mix[ 2 ] ) / 100, other = mix[ 4 ] === undefined ? 1 - weight : Number( mix[ 4 ] ) / 100;
|
|
39
|
+
const total = weight + other; if ( total > 1 ) { weight /= total; other /= total; }
|
|
40
|
+
const alpha = a[ 3 ] * weight + b[ 3 ] * other;
|
|
41
|
+
result = [ ...a.slice( 0, 3 ).map( ( channel, i ) => alpha ? Math.round( ( channel * a[ 3 ] * weight + b[ i ] * b[ 3 ] * other ) / alpha ) : 0 ), alpha ];
|
|
42
|
+
|
|
43
|
+
} else {
|
|
44
|
+
|
|
45
|
+
context.clearRect( 0, 0, 1, 1 ); context.fillStyle = '#000'; context.fillStyle = value; context.fillRect( 0, 0, 1, 1 );
|
|
46
|
+
result = [ ...context.getImageData( 0, 0, 1, 1 ).data ];
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
colors.set( value, result ); return result;
|
|
50
|
+
|
|
51
|
+
};
|
|
52
|
+
const color = value => { const [ r, g, b, a ] = rgba( value ); return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`; };
|
|
53
|
+
const children = element => element.shadowRoot?.children ?? ( element.localName === 'slot' ? element.assignedElements( { flatten: true } ) : element.children );
|
|
54
|
+
let boxes = new Map(), styles = new Map(), dirty = true;
|
|
55
|
+
const observer = new dom.MutationObserver( () => { dirty = true; } );
|
|
56
|
+
observer.observe( document, { subtree: true, childList: true, attributes: true, characterData: true } );
|
|
57
|
+
globalThis.__THREE_BLOCKS_INVALIDATE_LAYOUT__ = () => { dirty = true; };
|
|
58
|
+
globalThis.addEventListener( 'resize', () => { dirty = true; } );
|
|
59
|
+
const compiledStyles = new WeakMap();
|
|
60
|
+
const layers = new Map();
|
|
61
|
+
const layerOrder = name => { if ( ! layers.has( name ) ) layers.set( name, layers.size ); return layers.get( name ); };
|
|
62
|
+
const registeredProperties = {};
|
|
63
|
+
const rules = root => [ ...root.querySelectorAll( 'style' ) ].flatMap( style => {
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
|
|
67
|
+
let cached = compiledStyles.get( style );
|
|
68
|
+
if ( cached?.source !== style.textContent ) {
|
|
69
|
+
|
|
70
|
+
let sheet;
|
|
71
|
+
try { sheet = style.sheet; }
|
|
72
|
+
catch {
|
|
73
|
+
let css = transform( { filename: 'native.css', code: Buffer.from( style.textContent ), targets: { chrome: 90 << 16 } } ).code.toString();
|
|
74
|
+
const offsets = [ 0 ];
|
|
75
|
+
for ( let index = 0; index < css.length; index ++ ) if ( css[ index ] === '\n' ) offsets.push( index + 1 );
|
|
76
|
+
const replacements = [];
|
|
77
|
+
// CSSOM lacks cascade layers. Use Lightning CSS's parsed locations to
|
|
78
|
+
// lower only those headers to grouping rules, retaining their priority.
|
|
79
|
+
transform( { code: Buffer.from( css ), visitor: { Rule( rule ) {
|
|
80
|
+
if ( rule.type !== 'layer-block' && rule.type !== 'layer-statement' ) return;
|
|
81
|
+
const { line, column } = rule.value.loc;
|
|
82
|
+
const start = offsets[ line ] + column - 1;
|
|
83
|
+
const end = start + css.slice( start ).search( /[;{]/ ) + 1;
|
|
84
|
+
if ( rule.type === 'layer-statement' ) {
|
|
85
|
+
for ( const name of rule.value.names ) layerOrder( name.join( '.' ) );
|
|
86
|
+
replacements.push( { start, end, text: '' } );
|
|
87
|
+
} else {
|
|
88
|
+
const order = layerOrder( rule.value.name?.join( '.' ) ?? `anonymous-${layers.size}` );
|
|
89
|
+
replacements.push( { start, end, text: `@media (--three-blocks-layer: ${order}) {` } );
|
|
90
|
+
}
|
|
91
|
+
} } } );
|
|
92
|
+
for ( const replacement of replacements.sort( ( a, b ) => b.start - a.start ) ) css = css.slice( 0, replacement.start ) + replacement.text + css.slice( replacement.end );
|
|
93
|
+
const flattened = document.createElement( 'style' ); flattened.textContent = css;
|
|
94
|
+
sheet = flattened.sheet;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const collect = ( rules, layer, media = [] ) => rules.flatMap( rule => {
|
|
98
|
+
if ( rule.media ) {
|
|
99
|
+
const marker = /^\(--three-blocks-layer: (\d+)\)$/.exec( rule.media.mediaText );
|
|
100
|
+
return marker ? collect( [ ...rule.cssRules ], Number( marker[ 1 ] ), media ) : collect( [ ...rule.cssRules ], layer, [ ...media, rule.media.mediaText ] );
|
|
101
|
+
}
|
|
102
|
+
if ( rule.selectorText?.startsWith( '@property ' ) ) {
|
|
103
|
+
const initial = rule.style.getPropertyValue( 'initial-value' );
|
|
104
|
+
if ( initial ) registeredProperties[ rule.selectorText.slice( 10 ) ] = initial;
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return [ Object.assign( rule, { layer, media, selectors: rule.selectorText ? parseSelector( rule.selectorText ).map( selector => stringifySelector( [ selector ] ) ) : [] } ) ];
|
|
108
|
+
} );
|
|
109
|
+
const parsed = collect( [ ...sheet.cssRules ] );
|
|
110
|
+
cached = { source: style.textContent, rules: parsed }; compiledStyles.set( style, cached );
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
return cached.rules.map( rule => ( { rule, base: style.dataset.nativeURL || location.href } ) );
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
catch ( error ) { throw new Error( `Native CSS could not be parsed: ${error.message}` ); }
|
|
117
|
+
|
|
118
|
+
} );
|
|
119
|
+
const length = ( value, font = 16 ) => {
|
|
120
|
+
|
|
121
|
+
if ( ! value || value === 'auto' || value === 'none' ) return undefined;
|
|
122
|
+
if ( typeof value === 'number' ) return value;
|
|
123
|
+
if ( value.startsWith( 'calc(' ) ) {
|
|
124
|
+
|
|
125
|
+
const source = value.replace( /calc\(/g, '(' );
|
|
126
|
+
const tokens = source.match( /(?:\d*\.)?\d+(?:px|rem|em|vw|vh)?|[()+*/-]/g ) ?? [];
|
|
127
|
+
if ( tokens.join( '' ) !== source.replace( /\s/g, '' ) ) return undefined;
|
|
128
|
+
let cursor = 0;
|
|
129
|
+
const atom = () => {
|
|
130
|
+
|
|
131
|
+
const token = tokens[ cursor ++ ];
|
|
132
|
+
if ( token === '-' ) return -atom();
|
|
133
|
+
if ( token === '+' ) return atom();
|
|
134
|
+
if ( token !== '(' ) return length( token, font );
|
|
135
|
+
const result = sum();
|
|
136
|
+
return tokens[ cursor ++ ] === ')' ? result : NaN;
|
|
137
|
+
|
|
138
|
+
};
|
|
139
|
+
const product = () => { let result = atom(); while ( /^(\*|\/)$/.test( tokens[ cursor ] ) ) { const op = tokens[ cursor ++ ], next = atom(); result = op === '*' ? result * next : result / next; } return result; };
|
|
140
|
+
const sum = () => { let result = product(); while ( /^(\+|-)$/.test( tokens[ cursor ] ) ) { const op = tokens[ cursor ++ ], next = product(); result = op === '+' ? result + next : result - next; } return result; };
|
|
141
|
+
const result = sum();
|
|
142
|
+
return cursor === tokens.length && Number.isFinite( result ) ? result : undefined;
|
|
143
|
+
|
|
144
|
+
}
|
|
145
|
+
const expression = /^(clamp|min|max)\((.*)\)$/.exec( value );
|
|
146
|
+
if ( expression ) {
|
|
147
|
+
|
|
148
|
+
const values = expression[ 2 ].split( ',' ).map( part => length( part.trim(), font ) );
|
|
149
|
+
if ( expression[ 1 ] === 'min' ) return Math.min( ...values );
|
|
150
|
+
if ( expression[ 1 ] === 'max' ) return Math.max( ...values );
|
|
151
|
+
return Math.max( values[ 0 ], Math.min( values[ 1 ], values[ 2 ] ) );
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
if ( value.endsWith( '%' ) ) return value;
|
|
155
|
+
const number = parseFloat( value );
|
|
156
|
+
if ( ! Number.isFinite( number ) ) return undefined;
|
|
157
|
+
if ( value.endsWith( 'ch' ) ) return number * context.measureText( '0' ).width;
|
|
158
|
+
return number * ( value.endsWith( 'rem' ) ? 16 : value.endsWith( 'em' ) ? font : value.endsWith( 'vw' ) ? innerWidth / 100 : value.endsWith( 'vh' ) ? innerHeight / 100 : 1 );
|
|
159
|
+
|
|
160
|
+
};
|
|
161
|
+
const computed = element => {
|
|
162
|
+
|
|
163
|
+
if ( styles.has( element ) ) return styles.get( element );
|
|
164
|
+
const parent = element.parentElement ?? element.getRootNode()?.host;
|
|
165
|
+
const parentStyle = parent ? computed( parent ) : {};
|
|
166
|
+
const result = { display: element.hidden || /^(STYLE|SCRIPT|LINK|HEAD|META)$/.test( element.tagName ) ? 'none' : 'block', 'font-size': '16px', 'font-weight': '400', 'font-family': 'sans-serif', 'line-height': 'normal', 'letter-spacing': '0px', color: 'rgb(0, 0, 0)', opacity: '1', 'text-align': 'left', 'text-transform': 'none', visibility: 'visible' };
|
|
167
|
+
for ( const name of inherited ) if ( parentStyle[ name ] ) result[ name ] = parentStyle[ name ];
|
|
168
|
+
Object.assign( result, registeredProperties );
|
|
169
|
+
for ( const [ name, value ] of Object.entries( parentStyle ) ) if ( name.startsWith( '--' ) ) result[ name ] = value;
|
|
170
|
+
const ranks = {};
|
|
171
|
+
const apply = ( declaration, priority, base = location.href, layer ) => {
|
|
172
|
+
|
|
173
|
+
for ( let i = 0; i < declaration.length; i ++ ) {
|
|
174
|
+
|
|
175
|
+
const name = declaration[ i ];
|
|
176
|
+
let value = declaration.getPropertyValue( name );
|
|
177
|
+
const url = /^background(?:-image)?$/.test( name ) ? imageURL( value ) : undefined;
|
|
178
|
+
if ( url !== undefined ) value = value.replace( url, new URL( url, base ).href );
|
|
179
|
+
const important = declaration.getPropertyPriority?.( name ) === 'important' || /!important\s*$/.test( value );
|
|
180
|
+
const rank = priority + ( important ? 1e9 - ( layer ?? 1000 ) * 10000 : ( layer ?? 1000 ) * 10000 );
|
|
181
|
+
if ( rank >= ( ranks[ name ] ?? -1 ) ) { ranks[ name ] = rank; result[ name ] = value.replace( /\s*!important\s*$/, '' ); }
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
};
|
|
186
|
+
const scopedRules = rules( element.getRootNode() );
|
|
187
|
+
if ( element.shadowRoot ) scopedRules.push( ...rules( element.shadowRoot ).filter( ( { rule } ) => rule.selectorText === ':host' ) );
|
|
188
|
+
for ( const { rule, base } of scopedRules ) {
|
|
189
|
+
|
|
190
|
+
if ( ! rule.selectorText || rule.media.some( query => ! matchMedia( query ).matches ) ) continue;
|
|
191
|
+
for ( const selector of rule.selectors ) {
|
|
192
|
+
|
|
193
|
+
if ( selector.includes( '::' ) ) continue;
|
|
194
|
+
try {
|
|
195
|
+
|
|
196
|
+
if ( selector === ':host' ? element.shadowRoot : element.matches( selector ) ) apply( rule.style, ( selector.match( /#/g )?.length ?? 0 ) * 100 + ( selector.match( /[.[:]/g )?.length ?? 0 ) * 10 + ( selector.match( /(?:^|\s|>)[a-z]/gi )?.length ?? 0 ), base, rule.layer );
|
|
197
|
+
|
|
198
|
+
} catch { /* Selectors unsupported by the DOM are not a match. */ }
|
|
199
|
+
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
}
|
|
203
|
+
apply( element.style, 1000 );
|
|
204
|
+
if ( result.font ) {
|
|
205
|
+
|
|
206
|
+
const font = /^(?:(.*?)\s+)?(clamp\([^)]*\)|[\d.]+(?:px|rem|em|vw|vh))(?:\/([^\s]+))?\s+(.+)$/.exec( result.font );
|
|
207
|
+
if ( font ) {
|
|
208
|
+
|
|
209
|
+
for ( const [ name, value ] of Object.entries( { 'font-size': font[ 2 ], 'font-family': font[ 4 ], 'line-height': font[ 3 ] || 'normal', 'font-weight': font[ 1 ] || '400' } ) ) if ( ( ranks[ name ] ?? -1 ) <= ranks.font ) result[ name ] = value;
|
|
210
|
+
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
}
|
|
214
|
+
const resolveVars = ( value, seen = new Set() ) => {
|
|
215
|
+
let output = '', cursor = 0, start;
|
|
216
|
+
while ( ( start = value.indexOf( 'var(', cursor ) ) !== -1 ) {
|
|
217
|
+
let end = start + 4, depth = 1;
|
|
218
|
+
for ( ; end < value.length && depth; end ++ ) { if ( value[ end ] === '(' ) depth ++; else if ( value[ end ] === ')' ) depth --; }
|
|
219
|
+
if ( depth ) return ''; // Invalid declarations cannot become measured CSS.
|
|
220
|
+
const argumentsText = value.slice( start + 4, end - 1 ), separator = argumentsText.indexOf( ',' );
|
|
221
|
+
const key = ( separator === -1 ? argumentsText : argumentsText.slice( 0, separator ) ).trim();
|
|
222
|
+
const fallback = separator === -1 ? '' : argumentsText.slice( separator + 1 ).trim();
|
|
223
|
+
const replacement = seen.has( key ) || result[ key ] === 'initial' ? fallback : result[ key ] ?? fallback;
|
|
224
|
+
output += value.slice( cursor, start ) + resolveVars( replacement, new Set( [ ...seen, key ] ) ); cursor = end;
|
|
225
|
+
}
|
|
226
|
+
return output + value.slice( cursor );
|
|
227
|
+
};
|
|
228
|
+
for ( const [ name, value ] of Object.entries( result ) ) {
|
|
229
|
+
|
|
230
|
+
const resolved = resolveVars( value );
|
|
231
|
+
result[ name ] = resolved === 'inherit' || resolved === 'unset' && inherited.includes( name ) ? parentStyle[ name ] ?? '' : resolved;
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
result.color = color( result.color );
|
|
235
|
+
for ( const name of [ 'background', 'background-color', '-webkit-text-fill-color' ] ) if ( result[ name ]?.startsWith( 'color-mix(' ) ) result[ name ] = color( result[ name ] );
|
|
236
|
+
const fontSize = length( result[ 'font-size' ], parseFloat( parentStyle[ 'font-size' ] ) || 16 ) ?? 16;
|
|
237
|
+
result[ 'font-size' ] = `${fontSize}px`;
|
|
238
|
+
styles.set( element, result ); return result;
|
|
239
|
+
|
|
240
|
+
};
|
|
241
|
+
const textLines = ( element, availableWidth, style ) => {
|
|
242
|
+
|
|
243
|
+
const fontSize = parseFloat( style[ 'font-size' ] );
|
|
244
|
+
const font = fontFor( style );
|
|
245
|
+
const spacing = length( style[ 'letter-spacing' ], fontSize ) ?? 0;
|
|
246
|
+
const measure = value => { context.font = font; return context.measureText( value ).width + Math.max( 0, value.length - 1 ) * spacing; };
|
|
247
|
+
const text = element.textContent ?? '';
|
|
248
|
+
const lines = []; let start = 0;
|
|
249
|
+
while ( start < text.length ) {
|
|
250
|
+
|
|
251
|
+
let end = start + 1, lastSpace = -1;
|
|
252
|
+
while ( end <= text.length && ( measure( text.slice( start, end ) ) <= availableWidth || end === start + 1 ) ) {
|
|
253
|
+
|
|
254
|
+
if ( /\s/.test( text[ end - 1 ] ) ) lastSpace = end;
|
|
255
|
+
end ++;
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
end --;
|
|
259
|
+
if ( end < text.length && lastSpace > start ) end = lastSpace;
|
|
260
|
+
lines.push( { start, end, text: text.slice( start, end ), width: measure( text.slice( start, end ) ) } ); start = end;
|
|
261
|
+
|
|
262
|
+
}
|
|
263
|
+
const lineHeight = style[ 'line-height' ] === 'normal' ? fontSize * 1.2 : /^\d*\.?\d+$/.test( style[ 'line-height' ] ) ? Number( style[ 'line-height' ] ) * fontSize : length( style[ 'line-height' ], fontSize );
|
|
264
|
+
return { lines, lineHeight, measure };
|
|
265
|
+
|
|
266
|
+
};
|
|
267
|
+
const layout = () => {
|
|
268
|
+
|
|
269
|
+
if ( observer.takeRecords().length ) dirty = true;
|
|
270
|
+
if ( ! dirty ) return;
|
|
271
|
+
dirty = false; boxes = new Map(); styles = new Map();
|
|
272
|
+
const records = [], fixed = [];
|
|
273
|
+
const make = element => {
|
|
274
|
+
|
|
275
|
+
const node = Yoga.Node.create(); const style = computed( element );
|
|
276
|
+
context.font = fontFor( style );
|
|
277
|
+
if ( element.shadowRoot ) observer.observe( element.shadowRoot, { subtree: true, childList: true, attributes: true, characterData: true } );
|
|
278
|
+
const font = parseFloat( style[ 'font-size' ] );
|
|
279
|
+
node.setFlexDirection( style.display === 'flex' && style[ 'flex-direction' ] !== 'column' ? Yoga.FLEX_DIRECTION_ROW : Yoga.FLEX_DIRECTION_COLUMN );
|
|
280
|
+
if ( style.display === 'grid' && /^repeat\(\d+,\s*[\d.]+px\)$/.test( style[ 'grid-template-columns' ] ?? '' ) ) { node.setFlexDirection( Yoga.FLEX_DIRECTION_ROW ); node.setFlexWrap( Yoga.WRAP_WRAP ); }
|
|
281
|
+
node.setFlexShrink( Number( style[ 'flex-shrink' ] ?? 0 ) );
|
|
282
|
+
if ( style.display === 'none' || /^(STYLE|SCRIPT|LINK|HEAD|META)$/.test( element.tagName ) ) node.setDisplay( Yoga.DISPLAY_NONE );
|
|
283
|
+
if ( [ 'absolute', 'fixed' ].includes( style.position ) ) node.setPositionType( Yoga.POSITION_TYPE_ABSOLUTE );
|
|
284
|
+
for ( const name of [ 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height' ] ) {
|
|
285
|
+
|
|
286
|
+
let value = length( style[ name ], font );
|
|
287
|
+
// Yoga expresses min(fixed, percentage) as a fixed size plus its
|
|
288
|
+
// percentage maximum, preserving the containing block's real width.
|
|
289
|
+
const minimum = /^min\(\s*([^,]+),\s*([\d.]+%)\s*\)$/.exec( style[ name ] ?? '' );
|
|
290
|
+
if ( minimum && [ 'width', 'height' ].includes( name ) ) {
|
|
291
|
+
|
|
292
|
+
value = length( minimum[ 1 ], font );
|
|
293
|
+
node[ name === 'width' ? 'setMaxWidth' : 'setMaxHeight' ]( minimum[ 2 ] );
|
|
294
|
+
|
|
295
|
+
}
|
|
296
|
+
if ( value === undefined && element.tagName === 'CANVAS' && [ 'width', 'height' ].includes( name ) ) value = element[ name ] / devicePixelRatio;
|
|
297
|
+
if ( value === undefined && element.tagName === 'IMG' && [ 'width', 'height' ].includes( name ) ) value = element[ name ];
|
|
298
|
+
if ( value === undefined && element.localName === 'svg' && [ 'width', 'height' ].includes( name ) ) value = length( element.getAttribute( name ), font );
|
|
299
|
+
if ( value !== undefined ) node[ `set${camel( name )[ 0 ].toUpperCase()}${camel( name ).slice( 1 )}` ]( value );
|
|
300
|
+
|
|
301
|
+
}
|
|
302
|
+
for ( const [ side, edge ] of [ [ 'top', Yoga.EDGE_TOP ], [ 'right', Yoga.EDGE_RIGHT ], [ 'bottom', Yoga.EDGE_BOTTOM ], [ 'left', Yoga.EDGE_LEFT ] ] ) {
|
|
303
|
+
|
|
304
|
+
for ( const property of [ 'margin', 'padding' ] ) {
|
|
305
|
+
|
|
306
|
+
const shorthand = ( style[ property ] ?? '' ).split( /\s+(?![^()]*\))/ );
|
|
307
|
+
const index = [ 'top', 'right', 'bottom', 'left' ].indexOf( side );
|
|
308
|
+
const value = length( style[ `${property}-${side}` ] ?? shorthand[ index ] ?? shorthand[ index % 2 ] ?? shorthand[ 0 ], font );
|
|
309
|
+
if ( value !== undefined ) node[ property === 'margin' ? 'setMargin' : 'setPadding' ]( edge, value );
|
|
310
|
+
|
|
311
|
+
}
|
|
312
|
+
const inset = ( style.inset ?? '' ).split( /\s+/ );
|
|
313
|
+
const index = [ 'top', 'right', 'bottom', 'left' ].indexOf( side );
|
|
314
|
+
const position = length( style[ side ] ?? inset[ index ] ?? inset[ index % 2 ] ?? inset[ 0 ], font );
|
|
315
|
+
if ( position !== undefined ) node.setPosition( edge, position );
|
|
316
|
+
|
|
317
|
+
}
|
|
318
|
+
const aligns = { center: Yoga.ALIGN_CENTER, 'flex-start': Yoga.ALIGN_FLEX_START, 'flex-end': Yoga.ALIGN_FLEX_END, stretch: Yoga.ALIGN_STRETCH };
|
|
319
|
+
const align = style[ 'align-items' ] ?? style[ 'place-items' ];
|
|
320
|
+
if ( aligns[ align ] !== undefined ) node.setAlignItems( aligns[ align ] );
|
|
321
|
+
if ( style.display === 'grid' && align === 'center' ) node.setJustifyContent( Yoga.JUSTIFY_CENTER );
|
|
322
|
+
if ( style.display === 'grid' && style[ 'align-content' ] === 'center' ) node.setJustifyContent( Yoga.JUSTIFY_CENTER );
|
|
323
|
+
if ( style[ 'justify-content' ] === 'space-between' ) node.setJustifyContent( Yoga.JUSTIFY_SPACE_BETWEEN );
|
|
324
|
+
if ( style.gap ) node.setGap( Yoga.GUTTER_ALL, length( style.gap, font ) );
|
|
325
|
+
if ( style[ 'grid-area' ] === '1 / 1' ) { node.setPositionType( Yoga.POSITION_TYPE_ABSOLUTE ); node.setPosition( Yoga.EDGE_LEFT, 0 ); node.setPosition( Yoga.EDGE_TOP, 0 ); node.setWidth( '100%' ); }
|
|
326
|
+
if ( style[ 'grid-column' ] === '1 / -1' ) node.setWidth( '100%' );
|
|
327
|
+
if ( style[ 'justify-content' ] === 'center' ) node.setJustifyContent( Yoga.JUSTIFY_CENTER );
|
|
328
|
+
if ( style[ 'flex-grow' ] ) node.setFlexGrow( Number( style[ 'flex-grow' ] ) );
|
|
329
|
+
const text = style.display !== 'none' && ( element.hasAttribute( 'data-canvas-text' ) || children( element ).length === 0 && element.textContent.trim() );
|
|
330
|
+
if ( text ) node.setMeasureFunc( width => {
|
|
331
|
+
|
|
332
|
+
const { lines, lineHeight } = textLines( element, Number.isFinite( width ) ? width : Infinity, style );
|
|
333
|
+
return { width: Math.max( 0, ...lines.map( line => line.width ) ), height: lines.length * lineHeight };
|
|
334
|
+
|
|
335
|
+
} );
|
|
336
|
+
else if ( element.localName !== 'svg' ) for ( const child of children( element ) ) {
|
|
337
|
+
|
|
338
|
+
const childNode = make( child );
|
|
339
|
+
if ( computed( child ).position === 'fixed' ) fixed.push( childNode );
|
|
340
|
+
else node.insertChild( childNode, node.getChildCount() );
|
|
341
|
+
|
|
342
|
+
}
|
|
343
|
+
records.push( { element, node, style, text } ); return node;
|
|
344
|
+
|
|
345
|
+
};
|
|
346
|
+
const root = make( document.body ); root.setWidth( innerWidth ); root.setMinHeight( innerHeight );
|
|
347
|
+
for ( const node of fixed ) root.insertChild( node, root.getChildCount() );
|
|
348
|
+
try {
|
|
349
|
+
|
|
350
|
+
root.calculateLayout( innerWidth, innerHeight, Yoga.DIRECTION_LTR );
|
|
351
|
+
const byNode = new Map( records.map( record => [ record.node, record.style ] ) );
|
|
352
|
+
for ( const { element, node, style, text } of records ) {
|
|
353
|
+
|
|
354
|
+
const box = node.getComputedLayout(); let x = 0, y = 0;
|
|
355
|
+
for ( let parent = node; parent; parent = parent.getParent() ) {
|
|
356
|
+
|
|
357
|
+
x += parent.getComputedLeft(); y += parent.getComputedTop();
|
|
358
|
+
const translate = /^translate(X|Y)?\((.*)\)$/.exec( byNode.get( parent )?.transform ?? '' );
|
|
359
|
+
if ( ! translate ) continue;
|
|
360
|
+
const [ first, second = '0' ] = translate[ 2 ].startsWith( 'clamp(' ) ? [ translate[ 2 ] ] : translate[ 2 ].split( /,\s*|\s+(?![^()]*\))/ );
|
|
361
|
+
const distance = ( value, size ) => value.endsWith( '%' ) ? parseFloat( value ) * size / 100 : length( value, parseFloat( style[ 'font-size' ] ) ) ?? 0;
|
|
362
|
+
if ( translate[ 1 ] !== 'Y' ) x += distance( first, parent.getComputedWidth() );
|
|
363
|
+
if ( translate[ 1 ] !== 'X' ) y += distance( translate[ 1 ] === 'Y' ? first : second, parent.getComputedHeight() );
|
|
364
|
+
|
|
365
|
+
}
|
|
366
|
+
boxes.set( element, { rect: rect( x - ( globalThis.scrollX || 0 ), y - ( globalThis.scrollY || 0 ), box.width, box.height ), style, ...( text ? textLines( element, box.width, style ) : {} ) } );
|
|
367
|
+
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
} finally { root.freeRecursive(); }
|
|
371
|
+
|
|
372
|
+
};
|
|
373
|
+
globalThis.getComputedStyle = element => {
|
|
374
|
+
|
|
375
|
+
layout(); const style = computed( element );
|
|
376
|
+
return new Proxy( style, { get: ( object, key ) => key === 'getPropertyValue' ? name => object[ name ] ?? '' : object[ kebab( String( key ) ) ] ?? '' } );
|
|
377
|
+
|
|
378
|
+
};
|
|
379
|
+
dom.HTMLElement.prototype.getBoundingClientRect = function () { layout(); return boxes.get( this )?.rect ?? rect( 0, 0, 0, 0 ); };
|
|
380
|
+
for ( const [ name, axis ] of Object.entries( { clientWidth: 'width', offsetWidth: 'width', clientHeight: 'height', offsetHeight: 'height' } ) ) Object.defineProperty( dom.HTMLElement.prototype, name, { configurable: true, get() { return this.getBoundingClientRect()[ axis ]; } } );
|
|
381
|
+
Object.defineProperty( dom.HTMLElement.prototype, 'offsetParent', { configurable: true, get() {
|
|
382
|
+
|
|
383
|
+
for ( let parent = this.parentElement; parent; parent = parent.parentElement ) if ( parent === document.body || /absolute|relative|fixed/.test( computed( parent ).position ?? '' ) ) return parent;
|
|
384
|
+
return null;
|
|
385
|
+
|
|
386
|
+
} } );
|
|
387
|
+
for ( const [ name, axis ] of [ [ 'offsetTop', 'top' ], [ 'offsetLeft', 'left' ] ] ) Object.defineProperty( dom.HTMLElement.prototype, name, { configurable: true, get() { return this.getBoundingClientRect()[ axis ] - ( this.offsetParent?.getBoundingClientRect()[ axis ] ?? 0 ); } } );
|
|
388
|
+
for ( const [ name, axis, edge, start ] of [ [ 'scrollWidth', 'width', 'right', 'left' ], [ 'scrollHeight', 'height', 'bottom', 'top' ] ] ) Object.defineProperty( dom.HTMLElement.prototype, name, { configurable: true, get() {
|
|
389
|
+
|
|
390
|
+
layout(); const box = this.getBoundingClientRect();
|
|
391
|
+
const extent = element => Math.max( boxes.get( element )?.rect[ edge ] ?? box[ start ], ...[ ...children( element ) ].map( extent ) );
|
|
392
|
+
return Math.max( box[ axis ], extent( this ) - box[ start ] );
|
|
393
|
+
|
|
394
|
+
} } );
|
|
395
|
+
const createRange = document.createRange.bind( document );
|
|
396
|
+
document.createRange = () => {
|
|
397
|
+
|
|
398
|
+
const range = createRange(); let node, start = 0, end = 0;
|
|
399
|
+
range.setStart = ( value, offset ) => { node = value; start = offset; };
|
|
400
|
+
range.setEnd = ( value, offset ) => { if ( value !== node ) throw new Error( 'Native text Range must stay inside one text node.' ); end = offset; };
|
|
401
|
+
range.getClientRects = () => {
|
|
402
|
+
|
|
403
|
+
layout(); const element = node.parentElement.closest( '[data-canvas-text]' ) ?? node.parentElement;
|
|
404
|
+
const record = boxes.get( element );
|
|
405
|
+
const lineIndex = record?.lines?.findIndex( line => start >= line.start && start < line.end ) ?? -1;
|
|
406
|
+
const values = [];
|
|
407
|
+
if ( lineIndex >= 0 ) {
|
|
408
|
+
|
|
409
|
+
const line = record.lines[ lineIndex ];
|
|
410
|
+
const leading = record.measure( element.textContent.slice( line.start, start ) );
|
|
411
|
+
const width = record.measure( element.textContent.slice( line.start, end ) ) - leading;
|
|
412
|
+
const align = record.style[ 'text-align' ];
|
|
413
|
+
const offset = align === 'center' ? ( record.rect.width - line.width ) / 2 : align === 'right' ? record.rect.width - line.width : 0;
|
|
414
|
+
values.push( rect( record.rect.x + offset + leading, record.rect.y + lineIndex * record.lineHeight, width, record.lineHeight ) );
|
|
415
|
+
|
|
416
|
+
}
|
|
417
|
+
values.item = index => values[ index ] ?? null; return values;
|
|
418
|
+
|
|
419
|
+
};
|
|
420
|
+
return range;
|
|
421
|
+
|
|
422
|
+
};
|
|
423
|
+
const fontLoads = new Map();
|
|
424
|
+
const loadFonts = async () => {
|
|
425
|
+
|
|
426
|
+
for ( const { rule, base } of rules( document ) ) {
|
|
427
|
+
|
|
428
|
+
if ( rule.type !== 5 ) continue;
|
|
429
|
+
const family = rule.style.getPropertyValue( 'font-family' ).replace( /['"]/g, '' );
|
|
430
|
+
const source = /url\(['"]?([^)'"\s]+)/.exec( rule.style.getPropertyValue( 'src' ) );
|
|
431
|
+
if ( ! source ) continue;
|
|
432
|
+
const url = new URL( source[ 1 ], base ).href;
|
|
433
|
+
if ( ! fontLoads.has( url ) ) fontLoads.set( url, ( async () => {
|
|
434
|
+
|
|
435
|
+
const response = await fetch( url );
|
|
436
|
+
if ( ! response.ok || ! GlobalFonts.register( Buffer.from( await response.arrayBuffer() ), family ) ) throw new Error( `Native font failed to load: ${url}` );
|
|
437
|
+
dirty = true;
|
|
438
|
+
|
|
439
|
+
} )() );
|
|
440
|
+
|
|
441
|
+
}
|
|
442
|
+
await Promise.all( fontLoads.values() ); return [];
|
|
443
|
+
|
|
444
|
+
};
|
|
445
|
+
document.fonts = Object.assign( new EventTarget(), { check: specification => GlobalFonts.has( specification.replace( /^.*?\d+(?:\.\d+)?px\s*/, '' ).replace( /['"]/g, '' ) ), load: loadFonts } );
|
|
446
|
+
Object.defineProperty( document.fonts, 'ready', { get: () => loadFonts() } );
|
|
447
|
+
|
|
448
|
+
// Compose actual canvas readbacks with the same measured DOM used by text sync.
|
|
449
|
+
// This also applies the page background to transparent canvas screenshots.
|
|
450
|
+
return async ( bounds, readPixels, omitBackground = false ) => {
|
|
451
|
+
|
|
452
|
+
await document.fonts.ready; layout();
|
|
453
|
+
const scale = devicePixelRatio;
|
|
454
|
+
const output = createCanvas( Math.round( bounds.width * scale ), Math.round( bounds.height * scale ) );
|
|
455
|
+
const paint = output.getContext( '2d' );
|
|
456
|
+
if ( ! omitBackground ) { paint.fillStyle = '#fff'; paint.fillRect( 0, 0, output.width, output.height ); }
|
|
457
|
+
paint.scale( scale, scale ); paint.translate( -bounds.x, -bounds.y );
|
|
458
|
+
const draw = async element => {
|
|
459
|
+
|
|
460
|
+
const record = boxes.get( element );
|
|
461
|
+
if ( ! record ) return;
|
|
462
|
+
const { rect: box, style } = record;
|
|
463
|
+
if ( style.display === 'none' || style.visibility === 'hidden' || Number( style.opacity ) === 0 ) return;
|
|
464
|
+
paint.save(); paint.globalAlpha *= Number( style.opacity ?? 1 );
|
|
465
|
+
const rotation = /^rotate\((-?[\d.]+)deg\)$/.exec( style.transform ?? '' );
|
|
466
|
+
if ( rotation ) { paint.translate( box.x + box.width / 2, box.y + box.height / 2 ); paint.rotate( Number( rotation[ 1 ] ) * Math.PI / 180 ); paint.translate( -box.x - box.width / 2, -box.y - box.height / 2 ); }
|
|
467
|
+
const background = style[ 'background-color' ] || style.background;
|
|
468
|
+
if ( background && ! /gradient|url\(/.test( background ) ) {
|
|
469
|
+
|
|
470
|
+
paint.fillStyle = background;
|
|
471
|
+
paint.beginPath(); paint.roundRect( box.x, box.y, box.width, box.height, Math.min( parseFloat( style[ 'border-radius' ] ) || 0, box.width / 2, box.height / 2 ) ); paint.fill();
|
|
472
|
+
|
|
473
|
+
}
|
|
474
|
+
const url = imageURL( style[ 'background-image' ] || style.background );
|
|
475
|
+
if ( url && box.width > 0 && box.height > 0 ) {
|
|
476
|
+
|
|
477
|
+
if ( ! backgroundImages.has( url ) ) backgroundImages.set( url, ( async () => {
|
|
478
|
+
|
|
479
|
+
const response = await fetch( url );
|
|
480
|
+
if ( ! response.ok ) throw new Error( `Background image ${url}: HTTP ${response.status}` );
|
|
481
|
+
return loadImage( Buffer.from( await response.arrayBuffer() ) );
|
|
482
|
+
|
|
483
|
+
} )() );
|
|
484
|
+
const image = await backgroundImages.get( url );
|
|
485
|
+
const size = style[ 'background-size' ] || style.background?.split( '/' )[ 1 ]?.trim();
|
|
486
|
+
const scale = size === 'contain' ? Math.min( box.width / image.width, box.height / image.height ) : size === 'cover' ? Math.max( box.width / image.width, box.height / image.height ) : 1;
|
|
487
|
+
const position = style[ 'background-position' ] || ( /\bcenter\b/.test( style.background ?? '' ) ? 'center' : '0% 0%' );
|
|
488
|
+
const [ x, y = x === 'center' ? 'center' : '50%' ] = position.split( /\s+/ );
|
|
489
|
+
const offset = ( value, available ) => value === 'center' ? available / 2 : /^(right|bottom)$/.test( value ) ? available : value.endsWith( '%' ) ? available * parseFloat( value ) / 100 : length( value ) || 0;
|
|
490
|
+
const repeat = style[ 'background-repeat' ] || /\b(no-repeat|repeat-x|repeat-y)\b/.exec( style.background ?? '' )?.[ 0 ] || 'repeat';
|
|
491
|
+
const pattern = paint.createPattern( image, repeat );
|
|
492
|
+
pattern.setTransform( { a: scale, b: 0, c: 0, d: scale, e: box.x + offset( x, box.width - image.width * scale ), f: box.y + offset( y, box.height - image.height * scale ) } );
|
|
493
|
+
paint.fillStyle = pattern; paint.fillRect( box.x, box.y, box.width, box.height );
|
|
494
|
+
|
|
495
|
+
}
|
|
496
|
+
const border = /([\d.]+)px\s+solid\s+(.+)/.exec( style.border ?? '' );
|
|
497
|
+
if ( border ) {
|
|
498
|
+
|
|
499
|
+
paint.lineWidth = Number( border[ 1 ] ); paint.strokeStyle = border[ 2 ];
|
|
500
|
+
paint.beginPath(); paint.roundRect( box.x, box.y, box.width, box.height, Math.min( parseFloat( style[ 'border-radius' ] ) || 0, box.width / 2, box.height / 2 ) ); paint.stroke();
|
|
501
|
+
|
|
502
|
+
}
|
|
503
|
+
if ( /hidden|auto|scroll/.test( style.overflow ?? '' ) ) { paint.beginPath(); paint.rect( box.x, box.y, box.width, box.height ); paint.clip(); }
|
|
504
|
+
const inset = /^inset\(([\d.]+)%\)$/.exec( style[ 'clip-path' ] ?? '' );
|
|
505
|
+
if ( inset ) { const ratio = Number( inset[ 1 ] ) / 100; paint.beginPath(); paint.rect( box.x + box.width * ratio, box.y + box.height * ratio, box.width * ( 1 - 2 * ratio ), box.height * ( 1 - 2 * ratio ) ); paint.clip(); }
|
|
506
|
+
if ( [ 'CANVAS', 'IMG', 'VIDEO' ].includes( element.tagName ) && box.width > 0 && box.height > 0 ) {
|
|
507
|
+
|
|
508
|
+
const image = element.tagName === 'CANVAS' ? await readPixels( element ) : element._toRGBA8?.();
|
|
509
|
+
if ( ! image?.width || ! image.height ) { paint.restore(); return; }
|
|
510
|
+
const data = new Uint8ClampedArray( image.data );
|
|
511
|
+
const layer = createCanvas( image.width, image.height );
|
|
512
|
+
layer.getContext( '2d' ).putImageData( new ImageData( data, image.width, image.height ), 0, 0 );
|
|
513
|
+
paint.drawImage( layer, box.x, box.y, box.width, box.height );
|
|
514
|
+
|
|
515
|
+
} else if ( element.localName === 'svg' && box.width > 0 && box.height > 0 ) {
|
|
516
|
+
|
|
517
|
+
const svg = element.cloneNode( true ); svg.setAttribute( 'xmlns', 'http://www.w3.org/2000/svg' );
|
|
518
|
+
const image = await loadImage( Buffer.from( svg.outerHTML.replace( /currentColor/g, style.color ) ) );
|
|
519
|
+
paint.drawImage( image, box.x, box.y, box.width, box.height );
|
|
520
|
+
|
|
521
|
+
} else if ( record.lines ) {
|
|
522
|
+
|
|
523
|
+
const fontSize = parseFloat( style[ 'font-size' ] );
|
|
524
|
+
paint.font = fontFor( style );
|
|
525
|
+
paint.fillStyle = style[ '-webkit-text-fill-color' ] || style.color;
|
|
526
|
+
paint.textBaseline = 'middle'; paint.letterSpacing = style[ 'letter-spacing' ];
|
|
527
|
+
for ( const [ index, line ] of record.lines.entries() ) {
|
|
528
|
+
|
|
529
|
+
const offset = style[ 'text-align' ] === 'center' ? ( box.width - line.width ) / 2 : style[ 'text-align' ] === 'right' ? box.width - line.width : 0;
|
|
530
|
+
const text = style[ 'text-transform' ] === 'uppercase' ? line.text.toUpperCase() : line.text;
|
|
531
|
+
paint.fillText( text, box.x + offset, box.y + ( index + 0.5 ) * record.lineHeight );
|
|
532
|
+
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
}
|
|
536
|
+
const ordered = [ ...children( element ) ].sort( ( a, b ) => ( Number( computed( a )[ 'z-index' ] ) || 0 ) - ( Number( computed( b )[ 'z-index' ] ) || 0 ) );
|
|
537
|
+
for ( const child of ordered ) await draw( child );
|
|
538
|
+
paint.restore();
|
|
539
|
+
|
|
540
|
+
};
|
|
541
|
+
await draw( document.body );
|
|
542
|
+
return { width: output.width, height: output.height, data: output.getContext( '2d' ).getImageData( 0, 0, output.width, output.height ).data };
|
|
543
|
+
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
}
|
package/node/loader.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Load the project's browser-transformed modules, including its import map.
|
|
2
|
+
// Node's normal resolver remains responsible only for the runner's own modules.
|
|
3
|
+
let pageURL;
|
|
4
|
+
let imports;
|
|
5
|
+
let modules;
|
|
6
|
+
let scopes;
|
|
7
|
+
let network;
|
|
8
|
+
let networkId = 0;
|
|
9
|
+
|
|
10
|
+
// Version-gated platform fixes; leave upstream packages and shader recipes intact.
|
|
11
|
+
export function patchNativeThreeCode( source ) {
|
|
12
|
+
|
|
13
|
+
const canvasMarker = 'three-blocks-native-canvas-target-r185';
|
|
14
|
+
if ( /this\.isWebGLBackend\s*=\s*(?:true|!0)\b/.test( source ) && ! source.includes( canvasMarker ) ) {
|
|
15
|
+
|
|
16
|
+
const assignment = /this\.gl\s*=\s*[\w$]+(?=\s*[,;)}])/g;
|
|
17
|
+
if ( ! assignment.test( source ) ) throw new Error( 'Native Three.js CanvasTarget compatibility anchor changed.' );
|
|
18
|
+
source = source.replace( assignment, value => `/*${canvasMarker}*/(${value},globalThis.__THREE__===\`185\`&&globalThis.__THREE_BLOCKS_NATIVE_CANVAS_TARGET__?.(this),this.gl)` );
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
// r185 destroys the device before its timestamp pool's pending map settles.
|
|
22
|
+
// Keep actual errors visible; an AbortError after intentional disposal is expected.
|
|
23
|
+
const marker = 'three-blocks-native-timestamp-dispose-r185';
|
|
24
|
+
const label = 'Error resolving queries:';
|
|
25
|
+
if ( ! source.includes( 'WebGPUTimestampQueryPool' ) || ! source.includes( label ) || source.includes( marker ) ) return source;
|
|
26
|
+
const call = /((?:\(\s*0\s*,[^()]{1,400}\)|[\w$]+)\s*\(\s*\\?["'`]Error resolving queries:\\?["'`]\s*,\s*([\w$]+)\s*\))/;
|
|
27
|
+
const edits = [];
|
|
28
|
+
for ( let index = source.indexOf( label ); index >= 0; index = source.indexOf( label, index + label.length ) ) {
|
|
29
|
+
|
|
30
|
+
// Match only the call's neighborhood, never a megabyte-long inline source map.
|
|
31
|
+
const start = Math.max( 0, index - 450 );
|
|
32
|
+
const match = call.exec( source.slice( start, index + label.length + 80 ) );
|
|
33
|
+
if ( ! match ) throw new Error( 'Native Three.js timestamp-disposal compatibility anchor changed.' );
|
|
34
|
+
edits.push( { index: start + match.index, length: match[ 0 ].length, replacement: `/*${marker}*/((globalThis.__THREE__ === \`185\` && this.isDisposed && ${match[ 2 ]}?.name === \`AbortError\`) ? void 0 : ${match[ 0 ]})` } );
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
for ( const edit of edits.reverse() ) source = source.slice( 0, edit.index ) + edit.replacement + source.slice( edit.index + edit.length );
|
|
38
|
+
return source;
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function initialize( data ) {
|
|
43
|
+
|
|
44
|
+
( { pageURL, imports, modules, scopes = {}, network } = data );
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolve( specifier, context, nextResolve ) {
|
|
49
|
+
|
|
50
|
+
if ( /^(https?|blob|data):/.test( context.parentURL ?? '' ) ) {
|
|
51
|
+
|
|
52
|
+
const scoped = Object.keys( scopes ).sort( ( a, b ) => b.length - a.length ).find( scope => context.parentURL.startsWith( new URL( scope, pageURL ).href ) );
|
|
53
|
+
const mappings = { ...imports, ...scopes[ scoped ] };
|
|
54
|
+
|
|
55
|
+
const key = Object.keys( mappings ).sort( ( a, b ) => b.length - a.length ).find(
|
|
56
|
+
key => specifier === key || key.endsWith( '/' ) && specifier.startsWith( key )
|
|
57
|
+
);
|
|
58
|
+
if ( key ) specifier = new URL( mappings[ key ] + specifier.slice( key.length ), pageURL ).href;
|
|
59
|
+
else if ( /^[./]/.test( specifier ) ) specifier = new URL( specifier, context.parentURL ).href;
|
|
60
|
+
else if ( ! /^https?:/.test( specifier ) ) throw new Error( `Unmapped browser import: ${specifier}` );
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
if ( modules[ specifier ] !== undefined || /^https?:/.test( specifier ) ) return { url: specifier, shortCircuit: true };
|
|
64
|
+
return nextResolve( specifier, context );
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function load( url, context, nextLoad ) {
|
|
69
|
+
|
|
70
|
+
if ( modules[ url ] === undefined && ! /^https?:/.test( url ) ) return nextLoad( url, context );
|
|
71
|
+
let source = modules[ url ];
|
|
72
|
+
if ( source === undefined ) {
|
|
73
|
+
|
|
74
|
+
const id = `module-${++ networkId}-${pageURL}`;
|
|
75
|
+
network?.postMessage( { event: 'network', phase: 'request', id, url, method: 'GET', headers: {}, resourceType: 'script' } );
|
|
76
|
+
try {
|
|
77
|
+
|
|
78
|
+
let response = await fetch( url );
|
|
79
|
+
// Vite can discover a dependency after navigation and invalidate its hash.
|
|
80
|
+
// Resolve the current optimized file once; ordinary HTTP failures stay fatal.
|
|
81
|
+
if ( response.status === 504 && response.statusText === 'Outdated Optimize Dep' ) {
|
|
82
|
+
|
|
83
|
+
const current = new URL( url ); current.searchParams.delete( 'v' );
|
|
84
|
+
response = await fetch( current );
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
network?.postMessage( { event: 'network', phase: 'response', id, status: response.status, statusText: response.statusText, headers: Object.fromEntries( response.headers ) } );
|
|
88
|
+
if ( ! response.ok ) throw new Error( `Module ${url}: HTTP ${response.status}` );
|
|
89
|
+
source = await response.text();
|
|
90
|
+
network?.postMessage( { event: 'network', phase: 'finished', id, body: Buffer.from( source ) } );
|
|
91
|
+
|
|
92
|
+
} catch ( error ) {
|
|
93
|
+
|
|
94
|
+
network?.postMessage( { event: 'network', phase: 'failed', id, error: String( error ) } );
|
|
95
|
+
throw error;
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
// WASM loaders must choose the browser branch, just as in the transformed app.
|
|
101
|
+
return { format: 'module', shortCircuit: true, source: `const process = undefined;\n${patchNativeThreeCode( source )}` };
|
|
102
|
+
|
|
103
|
+
}
|