@ev-ry/fx 0.1.0-rc.1 → 0.1.0-rc.3
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/QUICKSTART.fa.md +99 -87
- package/README.md +120 -116
- package/build-report.json +48 -43
- package/docs/GUIDE.md +122 -112
- package/docs/RELEASE-NOTES.md +74 -39
- package/examples/evry-website.md +58 -0
- package/package.json +1 -1
- package/src/dom-attachment.d.ts +1 -1
- package/src/dom-free.d.ts +14 -14
- package/src/dom-free.js +61 -38
- package/src/dom-image-raster.js +14 -0
- package/src/dom-image-surface.js +150 -103
- package/src/dom-image-swap.js +5 -2
- package/src/dom-once.js +39 -34
- package/src/dom-reveal.js +83 -73
- package/src/dom-rich-text.js +133 -116
- package/src/dom-surface-font.js +4 -4
- package/src/dom-svg-surface.js +41 -39
- package/src/dom-text-fingerprint.js +27 -27
- package/src/dom-text-paint-mask.js +24 -0
- package/src/dom-text-surface.js +280 -236
- package/src/font-rasterizer.js +18 -7
- package/src/image-surface.js +161 -156
- package/src/render-owner.js +17 -12
- package/src/text-motion-primitives.js +7 -0
- package/src/text-scene.js +41 -39
- package/src/triangle-effect.js +2 -1
- package/src/viewport-render-owner.js +322 -316
package/src/dom-svg-surface.js
CHANGED
|
@@ -1,42 +1,44 @@
|
|
|
1
|
-
import {revealOpacity} from './dom-reveal.js';
|
|
2
|
-
import {attachImageSurface} from './dom-image-surface.js';
|
|
3
|
-
function snapshot(element){
|
|
4
|
-
if(element.querySelector('script,foreignObject,use,image,text,animate,animateTransform,animateMotion'))throw Error('Unsupported SVG content');
|
|
5
|
-
const view=element.ownerDocument.defaultView,clone=element.cloneNode(true),nodes=[element,...element.querySelectorAll('*')],copies=[clone,...clone.querySelectorAll('*')];
|
|
6
|
-
for(let i=0;i<nodes.length;i++){
|
|
7
|
-
const css=view.getComputedStyle(nodes[i]),copy=copies[i];
|
|
8
|
-
if(css.filter!=='none'||css.clipPath!=='none'||css.maskImage!=='none'||css.transform!=='none'&&!nodes[i].hasAttribute('transform'))throw Error('Unsupported SVG geometry');
|
|
9
|
-
copy.removeAttribute('class');copy.removeAttribute('style');
|
|
10
|
-
for(const prop of ['fill','stroke','stroke-width','stroke-linecap','stroke-linejoin','stroke-miterlimit','stroke-dasharray','stroke-dashoffset','fill-rule','fill-opacity','stroke-opacity','opacity','color','paint-order','vector-effect','display']){const value=prop==='opacity'&&i===0&&revealOpacity.has(element)?revealOpacity.get(element):css.getPropertyValue(prop);if(/url\(/i.test(value))throw Error('SVG paint references unsupported');if(value)copy.style.setProperty(prop,value);}
|
|
11
|
-
}
|
|
12
|
-
clone.querySelectorAll('style').forEach(e=>e.remove());clone.setAttribute('xmlns','http://www.w3.org/2000/svg');const r=element.getBoundingClientRect();clone.setAttribute('width',r.width);clone.setAttribute('height',r.height);return new view.XMLSerializer().serializeToString(clone);
|
|
13
|
-
}
|
|
14
|
-
export function attachSVGSurface(element,THREE,options={}){
|
|
15
|
-
if(element?.localName!=='svg'||!element.parentElement)throw TypeError('A mounted SVG is required');
|
|
16
|
-
const document=element.ownerDocument,view=document.defaultView,parent=element.parentElement,oldVisibility=element.style.visibility;
|
|
17
|
-
const image=document.createElement('img');image.alt='';image.setAttribute('aria-hidden','true');Object.assign(image.style,{position:'absolute',pointerEvents:'none',opacity:'0'});parent.append(image);
|
|
18
|
-
let disposed=false,revision=0,timer=null,reason=null,xml='',surface,resolve;const ready=new Promise(r=>resolve=r);
|
|
19
|
-
function restore(){if(element.style.visibility==='hidden')element.style.visibility=oldVisibility;}
|
|
20
|
-
function present(state){
|
|
21
|
-
if(disposed)return;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if(state.mode==='mesh'||state.reason)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
1
|
+
import {revealOpacity} from './dom-reveal.js';
|
|
2
|
+
import {attachImageSurface} from './dom-image-surface.js';
|
|
3
|
+
function snapshot(element){
|
|
4
|
+
if(element.querySelector('script,foreignObject,use,image,text,animate,animateTransform,animateMotion'))throw Error('Unsupported SVG content');
|
|
5
|
+
const view=element.ownerDocument.defaultView,clone=element.cloneNode(true),nodes=[element,...element.querySelectorAll('*')],copies=[clone,...clone.querySelectorAll('*')];
|
|
6
|
+
for(let i=0;i<nodes.length;i++){
|
|
7
|
+
const css=view.getComputedStyle(nodes[i]),copy=copies[i];
|
|
8
|
+
if(css.filter!=='none'||css.clipPath!=='none'||css.maskImage!=='none'||css.transform!=='none'&&!nodes[i].hasAttribute('transform'))throw Error('Unsupported SVG geometry');
|
|
9
|
+
copy.removeAttribute('class');copy.removeAttribute('style');
|
|
10
|
+
for(const prop of ['fill','stroke','stroke-width','stroke-linecap','stroke-linejoin','stroke-miterlimit','stroke-dasharray','stroke-dashoffset','fill-rule','fill-opacity','stroke-opacity','opacity','color','paint-order','vector-effect','display']){const value=prop==='opacity'&&i===0&&revealOpacity.has(element)?revealOpacity.get(element):css.getPropertyValue(prop);if(/url\(/i.test(value))throw Error('SVG paint references unsupported');if(value)copy.style.setProperty(prop,value);}
|
|
11
|
+
}
|
|
12
|
+
clone.querySelectorAll('style').forEach(e=>e.remove());clone.setAttribute('xmlns','http://www.w3.org/2000/svg');const r=element.getBoundingClientRect();clone.setAttribute('width',r.width);clone.setAttribute('height',r.height);return new view.XMLSerializer().serializeToString(clone);
|
|
13
|
+
}
|
|
14
|
+
export function attachSVGSurface(element,THREE,options={}){
|
|
15
|
+
if(element?.localName!=='svg'||!element.parentElement)throw TypeError('A mounted SVG is required');
|
|
16
|
+
const document=element.ownerDocument,view=document.defaultView,parent=element.parentElement,oldVisibility=element.style.visibility;
|
|
17
|
+
const image=document.createElement('img');image.alt='';image.setAttribute('aria-hidden','true');Object.assign(image.style,{position:'absolute',pointerEvents:'none',opacity:'0'});parent.append(image);
|
|
18
|
+
let disposed=false,revision=0,timer=null,reason=null,xml='',surface,resolve;const ready=new Promise(r=>resolve=r);
|
|
19
|
+
function restore(){if(element.style.visibility==='hidden')element.style.visibility=oldVisibility;}
|
|
20
|
+
function present(state){
|
|
21
|
+
if(disposed)return;
|
|
22
|
+
// The image adapter exposes native pixels before its settled mesh finishes
|
|
23
|
+
// fading. An explicit hidden:false must win over the remaining mesh mode.
|
|
24
|
+
if(state.hidden??(state.mode==='mesh'||state.reason==='hidden after exit')){if(element.style.visibility!=='hidden')element.style.visibility='hidden';}else restore();
|
|
25
|
+
reason=state.reason;
|
|
26
|
+
if(state.mode==='mesh'||state.reason)resolve(state);
|
|
27
|
+
}
|
|
28
|
+
async function refresh(){if(disposed)return;const token=++revision;clearTimeout(timer);
|
|
29
|
+
try{const next=snapshot(element),r=element.getBoundingClientRect(),p=parent.getBoundingClientRect();if(!(r.width>0&&r.height>0))throw Error('SVG not visible');
|
|
30
|
+
Object.assign(image.style,{left:(r.left-p.left-parent.clientLeft+parent.scrollLeft)+'px',top:(r.top-p.top-parent.clientTop+parent.scrollTop)+'px',width:r.width+'px',height:r.height+'px'});
|
|
31
|
+
if(next!==xml){xml=next;if(!surface)restore();image.src='data:image/svg+xml;charset=utf-8,'+encodeURIComponent(xml);}
|
|
32
|
+
if(!surface)surface=attachImageSurface(image,THREE,{...options,onPresentation:present});
|
|
33
|
+
await surface.refresh();if(disposed||token!==revision)return;
|
|
34
|
+
const settle=()=>{if(disposed||token!==revision)return;const s=surface.stats();if(s.mode==='mesh'||s.reason)present(s);else timer=setTimeout(settle,16);};settle();
|
|
35
|
+
}catch(error){xml='';surface?.destroy();surface=null;restore();reason=error.message;resolve({mode:'native',reason});}
|
|
36
|
+
}
|
|
37
|
+
const styleKey=()=>element.style.cssText.replace(/(?:^|;)\s*visibility\s*:[^;]*/g,'');let lastStyle=styleKey();
|
|
38
|
+
const observer=new view.MutationObserver(records=>{const next=styleKey();if(records.some(r=>r.target!==element||r.attributeName!=='style')||next!==lastStyle){lastStyle=next;refresh();}});
|
|
39
|
+
observer.observe(element,{subtree:true,attributes:true,childList:true,characterData:true});for(let a=parent;a;a=a.parentElement)observer.observe(a,{attributes:true,attributeFilter:['class','style']});
|
|
40
|
+
const resize=new view.ResizeObserver(refresh);resize.observe(element);refresh();
|
|
41
|
+
return {element,ready,refresh,cancel:()=>surface?.cancel(),play(phase='enter'){if(!['enter','exit'].includes(phase))throw TypeError('Expected enter or exit');if(phase==='enter'&&surface?.stats().image?.source)element.style.visibility='hidden';surface?.play(phase);},update(next){options={...options,...next};surface?.update(next);},stats:()=>({...surface?.stats(),disposed,reason:reason||surface?.stats().reason,mode:reason?'native':surface?.stats().mode||'native'}),destroy(){if(disposed)return;disposed=true;revision++;clearTimeout(timer);observer.disconnect();resize.disconnect();surface?.destroy();image.remove();restore();resolve({mode:'native',reason:'destroyed'});}};
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
// Validate native layout with whole-node line boxes before the expensive
|
|
2
|
-
// per-grapheme placement pass. Coordinates are relative to the slot, so page
|
|
3
|
-
// scrolling does not invalidate text geometry. Node identity matters: identical
|
|
4
|
-
// replacement markup still needs fresh run references.
|
|
5
|
-
const fields=['display','visibility','fontFamily','fontSize','fontWeight','fontStyle','fontStretch','fontFeatureSettings','fontVariationSettings','fontKerning','fontVariant','fontOpticalSizing','lineHeight','direction','unicodeBidi','whiteSpace','letterSpacing','wordSpacing','verticalAlign','textAlign','textIndent','textTransform','textDecorationLine','writingMode','textOrientation','wordBreak','overflowWrap','hyphens','tabSize','opacity','transform','overflowX','overflowY','color'];
|
|
6
|
-
export function createDOMTextFingerprint(element,canvas){
|
|
7
|
-
const document=element.ownerDocument,window=document.defaultView,ids=new WeakMap();let sequence=0;
|
|
8
|
-
const id=node=>{if(!ids.has(node))ids.set(node,++sequence);return ids.get(node);};
|
|
9
|
-
return ()=>{
|
|
10
|
-
const box=element.getBoundingClientRect(),left=box.left+element.clientLeft,top=box.top+element.clientTop;
|
|
11
|
-
const parts=[element.isConnected,element.clientWidth||box.width,element.clientHeight||box.height,element.clientLeft,element.clientTop];
|
|
12
|
-
function visit(node){
|
|
13
|
-
if(node===canvas)return;
|
|
14
|
-
if(node.nodeType===3){
|
|
15
|
-
const range=document.createRange();range.selectNodeContents(node);
|
|
16
|
-
parts.push(['text',id(node),node.data,[...range.getClientRects()].map(r=>[r.left-left,r.top-top,r.width,r.height])]);return;
|
|
17
|
-
}
|
|
18
|
-
if(node.nodeType!==1)return;
|
|
19
|
-
const style=window.getComputedStyle(node);
|
|
20
|
-
parts.push(['element',id(node),node.localName,node.isContentEditable,fields.map(name=>style[name])]);
|
|
21
|
-
for(const pseudo of ['::before','::after']){const css=window.getComputedStyle(node,pseudo);parts.push([css.display,css.content]);}
|
|
22
|
-
for(const child of node.childNodes)visit(child);
|
|
23
|
-
parts.push('end');
|
|
24
|
-
}
|
|
25
|
-
visit(element);return JSON.stringify(parts);
|
|
26
|
-
};
|
|
27
|
-
}
|
|
1
|
+
// Validate native layout with whole-node line boxes before the expensive
|
|
2
|
+
// per-grapheme placement pass. Coordinates are relative to the slot, so page
|
|
3
|
+
// scrolling does not invalidate text geometry. Node identity matters: identical
|
|
4
|
+
// replacement markup still needs fresh run references.
|
|
5
|
+
const fields=['display','visibility','fontFamily','fontSize','fontWeight','fontStyle','fontStretch','fontFeatureSettings','fontVariationSettings','fontKerning','textRendering','fontVariant','fontOpticalSizing','lineHeight','direction','unicodeBidi','whiteSpace','letterSpacing','wordSpacing','verticalAlign','textAlign','textIndent','textTransform','textDecorationLine','writingMode','textOrientation','wordBreak','overflowWrap','hyphens','tabSize','opacity','transform','overflowX','overflowY','color'];
|
|
6
|
+
export function createDOMTextFingerprint(element,canvas){
|
|
7
|
+
const document=element.ownerDocument,window=document.defaultView,ids=new WeakMap();let sequence=0;
|
|
8
|
+
const id=node=>{if(!ids.has(node))ids.set(node,++sequence);return ids.get(node);};
|
|
9
|
+
return ()=>{
|
|
10
|
+
const box=element.getBoundingClientRect(),left=box.left+element.clientLeft,top=box.top+element.clientTop;
|
|
11
|
+
const parts=[element.isConnected,element.clientWidth||box.width,element.clientHeight||box.height,element.clientLeft,element.clientTop];
|
|
12
|
+
function visit(node){
|
|
13
|
+
if(node===canvas)return;
|
|
14
|
+
if(node.nodeType===3){
|
|
15
|
+
const range=document.createRange();range.selectNodeContents(node);
|
|
16
|
+
parts.push(['text',id(node),node.data,[...range.getClientRects()].map(r=>[r.left-left,r.top-top,r.width,r.height])]);return;
|
|
17
|
+
}
|
|
18
|
+
if(node.nodeType!==1)return;
|
|
19
|
+
const style=window.getComputedStyle(node);
|
|
20
|
+
parts.push(['element',id(node),node.localName,node.isContentEditable,fields.map(name=>style[name])]);
|
|
21
|
+
for(const pseudo of ['::before','::after']){const css=window.getComputedStyle(node,pseudo);parts.push([css.display,css.content]);}
|
|
22
|
+
for(const child of node.childNodes)visit(child);
|
|
23
|
+
parts.push('end');
|
|
24
|
+
}
|
|
25
|
+
visit(element);return JSON.stringify(parts);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Hide native text without hiding the canvas or changing layout/raster colors.
|
|
2
|
+
// This temporary owner covers cold rich text before its measured runs exist.
|
|
3
|
+
export function createTextPaintMask(element,canvas){
|
|
4
|
+
const document=element.ownerDocument,window=document.defaultView,styles=new Map();
|
|
5
|
+
function hold(){
|
|
6
|
+
const parents=new Set(),walker=document.createTreeWalker(element,window.NodeFilter.SHOW_TEXT);
|
|
7
|
+
while(walker.nextNode())if(walker.currentNode.data.trim()&&!canvas?.contains(walker.currentNode))parents.add(walker.currentNode.parentElement);
|
|
8
|
+
for(const node of parents){
|
|
9
|
+
let values=styles.get(node);if(!values){values=new Map();styles.set(node,values);}
|
|
10
|
+
for(const [name,value] of [['-webkit-text-fill-color','transparent'],['text-shadow','none']]){
|
|
11
|
+
if(!values.has(name))values.set(name,[node.style.getPropertyValue(name),node.style.getPropertyPriority(name),value]);
|
|
12
|
+
if(node.style.getPropertyValue(name)!==value||node.style.getPropertyPriority(name)!=='important')node.style.setProperty(name,value,'important');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function release(){
|
|
17
|
+
for(const [node,values] of styles)for(const [name,[original,priority,owned]] of values){
|
|
18
|
+
if(node.style.getPropertyValue(name)!==owned||node.style.getPropertyPriority(name)!=='important')continue;
|
|
19
|
+
if(original)node.style.setProperty(name,original,priority);else node.style.removeProperty(name);
|
|
20
|
+
}
|
|
21
|
+
styles.clear();
|
|
22
|
+
}
|
|
23
|
+
return {hold,release};
|
|
24
|
+
}
|