@ev-ry/fx 0.1.0-rc.3 → 0.1.0-rc.4
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 +18 -18
- package/README.md +9 -8
- package/build-report.json +44 -44
- package/docs/RELEASE-NOTES.md +21 -11
- package/examples/evry-website.md +23 -23
- package/package.json +57 -57
- package/src/dom-attachment.d.ts +72 -72
- package/src/dom-free-loader.js +17 -11
- package/src/dom-free-script.js +17 -11
- package/src/dom-free.js +27 -26
- package/src/dom-image-surface.js +25 -12
- package/src/dom-image-swap.js +30 -27
- package/src/dom-raster-cache.js +33 -33
- package/src/dom-text-surface.js +53 -53
- package/src/font-rasterizer.js +20 -20
- package/src/insertion-range.js +29 -29
- package/src/raster-texture-material.js +79 -79
- package/src/raster-texture-mesh.js +35 -35
- package/src/runtime-font-engine.js +43 -43
- package/src/text-edit-effect.js +101 -101
- package/src/text-edit-motions.js +15 -15
- package/src/text-effect-options.js +28 -28
- package/src/text-effect-path.js +61 -61
- package/src/text-motion-primitives.js +98 -98
- package/src/text-motion-recipes.js +71 -71
- package/src/triangle-effect.js +195 -195
package/src/dom-text-surface.js
CHANGED
|
@@ -7,8 +7,8 @@ import {textEditMotions} from './text-edit-motions.js';
|
|
|
7
7
|
import {normalizeTextEffectOptions} from './text-effect-options.js';
|
|
8
8
|
import {adoptDOMFont,domFontAtSize,matchesDOMFont} from './dom-surface-font.js';
|
|
9
9
|
import {DOMRichText} from './dom-rich-text.js';
|
|
10
|
-
import {createDOMTextFingerprint} from './dom-text-fingerprint.js';
|
|
11
|
-
import {createTextPaintMask} from './dom-text-paint-mask.js';
|
|
10
|
+
import {createDOMTextFingerprint} from './dom-text-fingerprint.js';
|
|
11
|
+
import {createTextPaintMask} from './dom-text-paint-mask.js';
|
|
12
12
|
|
|
13
13
|
// Presentation attachment: the original text node, parent control and semantics
|
|
14
14
|
// remain owned by the host. The deliberately narrow first contract is a plain,
|
|
@@ -25,8 +25,8 @@ export function attachTextSurface(element,THREE,options={}){
|
|
|
25
25
|
let enabled=options.enabled!==false,renderer,canvas,engine,view,effect,scene,content,camera;
|
|
26
26
|
let frame=null,revision=0,preparing=false,dirty=true,layout=null,committed=null,reason=null,fatal=false,intersecting=true;
|
|
27
27
|
let mode='native',draws=0,builds=0,submittedSize='',pendingPhase=options.initialPhase??null,phase='enter',lastSeed=null;
|
|
28
|
-
let effectStarted=null,pendingStarted=null,settleStarted=null;
|
|
29
|
-
let nativeMask;
|
|
28
|
+
let effectStarted=null,pendingStarted=null,settleStarted=null;
|
|
29
|
+
let nativeMask;
|
|
30
30
|
function preserveMotion(){if(pendingPhase===null&&effectStarted!==null&&window.performance.now()-effectStarted<settings.duration){pendingPhase=phase;pendingStarted=effectStarted;}}
|
|
31
31
|
let resolveReady;const ready=new Promise(resolve=>{resolveReady=resolve;});
|
|
32
32
|
let rich=null;
|
|
@@ -46,12 +46,12 @@ export function attachTextSurface(element,THREE,options={}){
|
|
|
46
46
|
}
|
|
47
47
|
ownedStyle.delete(name);originalStyle.delete(name);
|
|
48
48
|
}
|
|
49
|
-
function exitOwnsPaint(why=reason){return (pendingPhase??phase)==='exit'&&!fatal&&enabled&&!forced.matches&&(!why||/not visible|not connected|native resting presentation|hidden after exit/i.test(why));}
|
|
50
|
-
function native(why=null){
|
|
51
|
-
nativeMask?.release();
|
|
49
|
+
function exitOwnsPaint(why=reason){return (pendingPhase??phase)==='exit'&&!fatal&&enabled&&!forced.matches&&(!why||/not visible|not connected|native resting presentation|hidden after exit/i.test(why));}
|
|
50
|
+
function native(why=null){
|
|
51
|
+
nativeMask?.release();
|
|
52
52
|
rich?.restore();
|
|
53
|
-
mode='native';reason=why;restoreStyle('-webkit-text-fill-color');restoreStyle('text-shadow');
|
|
54
|
-
if(exitOwnsPaint(why))nativeMask?.hold();
|
|
53
|
+
mode='native';reason=why;restoreStyle('-webkit-text-fill-color');restoreStyle('text-shadow');
|
|
54
|
+
if(exitOwnsPaint(why))nativeMask?.hold();
|
|
55
55
|
if(canvas)canvas.style.display='none';
|
|
56
56
|
renderer?.invalidate?.();
|
|
57
57
|
}
|
|
@@ -61,27 +61,27 @@ export function attachTextSurface(element,THREE,options={}){
|
|
|
61
61
|
resolveReady({mode,reason});
|
|
62
62
|
}
|
|
63
63
|
function request(){if(!life.disposed&&!fatal&&!document.hidden&&intersecting&&frame===null)frame=window.requestAnimationFrame(render);}
|
|
64
|
-
function suspend(){
|
|
65
|
-
if(pendingPhase!==null&&pendingStarted===null)pendingStarted=window.performance.now();
|
|
66
|
-
if(frame!==null)window.cancelAnimationFrame(frame);frame=null;
|
|
67
|
-
const hideExit=exitOwnsPaint();
|
|
68
|
-
// The last canvas frame and restored native text must not flash on reentry.
|
|
69
|
-
// A queued/current exit owns native paint even while no frames are drawn.
|
|
70
|
-
if(renderer?.globalCanvas||hideExit)native(reason??'not visible');
|
|
71
|
-
if(canvas)canvas.style.display='none';renderer?.invalidate?.();
|
|
72
|
-
}
|
|
73
|
-
function refresh(){
|
|
74
|
-
if(life.disposed)return;revision++;dirty=true;request();
|
|
75
|
-
}
|
|
76
|
-
function play(next='enter'){
|
|
77
|
-
if(life.disposed)throw Error('Text surface disposed');if(!['enter','exit'].includes(next))throw TypeError('Expected enter or exit');
|
|
78
|
-
pendingPhase=next;if(next==='enter')lastSeed=null;const box=element.getBoundingClientRect();
|
|
79
|
-
const outside=box.bottom<=0||box.top>=window.innerHeight||box.right<=0||box.left>=window.innerWidth;
|
|
80
|
-
pendingStarted=document.hidden||!intersecting||outside?window.performance.now():null;
|
|
81
|
-
// Do not spend a cold preparation frame waiting for IO's first delivery.
|
|
82
|
-
if(outside&&window.IntersectionObserver)intersecting=false;
|
|
83
|
-
if(document.hidden||!intersecting)suspend();refresh();
|
|
84
|
-
}
|
|
64
|
+
function suspend(){
|
|
65
|
+
if(pendingPhase!==null&&pendingStarted===null)pendingStarted=window.performance.now();
|
|
66
|
+
if(frame!==null)window.cancelAnimationFrame(frame);frame=null;
|
|
67
|
+
const hideExit=exitOwnsPaint();
|
|
68
|
+
// The last canvas frame and restored native text must not flash on reentry.
|
|
69
|
+
// A queued/current exit owns native paint even while no frames are drawn.
|
|
70
|
+
if(renderer?.globalCanvas||hideExit)native(reason??'not visible');
|
|
71
|
+
if(canvas)canvas.style.display='none';renderer?.invalidate?.();
|
|
72
|
+
}
|
|
73
|
+
function refresh(){
|
|
74
|
+
if(life.disposed)return;revision++;dirty=true;request();
|
|
75
|
+
}
|
|
76
|
+
function play(next='enter'){
|
|
77
|
+
if(life.disposed)throw Error('Text surface disposed');if(!['enter','exit'].includes(next))throw TypeError('Expected enter or exit');
|
|
78
|
+
pendingPhase=next;if(next==='enter')lastSeed=null;const box=element.getBoundingClientRect();
|
|
79
|
+
const outside=box.bottom<=0||box.top>=window.innerHeight||box.right<=0||box.left>=window.innerWidth;
|
|
80
|
+
pendingStarted=document.hidden||!intersecting||outside?window.performance.now():null;
|
|
81
|
+
// Do not spend a cold preparation frame waiting for IO's first delivery.
|
|
82
|
+
if(outside&&window.IntersectionObserver)intersecting=false;
|
|
83
|
+
if(document.hidden||!intersecting)suspend();refresh();
|
|
84
|
+
}
|
|
85
85
|
function slot(){
|
|
86
86
|
const children=[...element.childNodes].filter(node=>node!==canvas&&node.nodeType!==8);
|
|
87
87
|
if(children.some(node=>node.nodeType!==3)||children.length>1)return null;
|
|
@@ -202,23 +202,23 @@ export function attachTextSurface(element,THREE,options={}){
|
|
|
202
202
|
let nativeAlpha=0,meshFade=0;
|
|
203
203
|
if(active&&phase!=='exit'&&resting==='native'&&effectStarted!==null)
|
|
204
204
|
nativeAlpha=Math.max(0,Math.min(1,(now-(effectStarted+settings.duration-175))/350));
|
|
205
|
-
if(!active&&phase!=='exit'&&resting==='native'){
|
|
205
|
+
if(!active&&phase!=='exit'&&resting==='native'){
|
|
206
206
|
// Use the effect clock, not the first idle frame. Slow frames must not
|
|
207
207
|
// restart the handoff or jump native alpha back to its halfway point.
|
|
208
|
-
// Cancellation/settings refresh have no entry clock. Keep their
|
|
209
|
-
// restored native paint instead of beginning a second handoff.
|
|
210
|
-
const instant=effectStarted===null||effectName==='none'||reduced.matches;
|
|
211
|
-
if(settleStarted===null)settleStarted=effectStarted===null?now:effectStarted+settings.duration;
|
|
212
|
-
meshFade=instant?1:Math.min(1,(now-settleStarted)/350);
|
|
213
|
-
nativeAlpha=instant?1:Math.min(1,(now-settleStarted+175)/350);
|
|
208
|
+
// Cancellation/settings refresh have no entry clock. Keep their
|
|
209
|
+
// restored native paint instead of beginning a second handoff.
|
|
210
|
+
const instant=effectStarted===null||effectName==='none'||reduced.matches;
|
|
211
|
+
if(settleStarted===null)settleStarted=effectStarted===null?now:effectStarted+settings.duration;
|
|
212
|
+
meshFade=instant?1:Math.min(1,(now-settleStarted)/350);
|
|
213
|
+
nativeAlpha=instant?1:Math.min(1,(now-settleStarted+175)/350);
|
|
214
214
|
if(meshFade>=1){renderer.render(scene,camera);native('native resting presentation');resolveReady({mode,reason});return;}
|
|
215
215
|
}else settleStarted=null;
|
|
216
216
|
view.uniforms.presentationOpacity.value=1-meshFade;rich?.setPresentationOpacity(1-meshFade);
|
|
217
|
-
const handingOffNative=mode==='native';
|
|
218
|
-
renderer.render(scene,camera);draws++;
|
|
219
|
-
// Transfer temporary offscreen ownership to the regular mesh paint mask
|
|
220
|
-
// in the same task, before the browser can paint native glyphs.
|
|
221
|
-
nativeMask?.release();
|
|
217
|
+
const handingOffNative=mode==='native';
|
|
218
|
+
renderer.render(scene,camera);draws++;
|
|
219
|
+
// Transfer temporary offscreen ownership to the regular mesh paint mask
|
|
220
|
+
// in the same task, before the browser can paint native glyphs.
|
|
221
|
+
nativeMask?.release();
|
|
222
222
|
canvas.style.display='block';if(!rich)ownStyle('-webkit-text-fill-color',nativeAlpha>0?`color-mix(in srgb, ${window.getComputedStyle(element).color} ${nativeAlpha*100}%, transparent)`:'transparent');ownStyle('text-shadow','none');mode='mesh';reason=null;
|
|
223
223
|
rich?.hide(nativeAlpha);
|
|
224
224
|
// Commit the shared canvas before the browser paints hidden native text.
|
|
@@ -232,8 +232,8 @@ export function attachTextSurface(element,THREE,options={}){
|
|
|
232
232
|
try{
|
|
233
233
|
renderer=new THREE.WebGLRenderer({antialias:true,alpha:true});life.own(()=>{renderer.dispose();renderer.forceContextLoss();});
|
|
234
234
|
renderer.setSurface?.(element,{escapeHost:element.closest('button,a,[role="button"]')||element});
|
|
235
|
-
canvas=renderer.domElement;canvas.setAttribute('aria-hidden','true');canvas.setAttribute('data-thd-text-surface','');
|
|
236
|
-
nativeMask=createTextPaintMask(element,canvas);life.own(()=>nativeMask.release());
|
|
235
|
+
canvas=renderer.domElement;canvas.setAttribute('aria-hidden','true');canvas.setAttribute('data-thd-text-surface','');
|
|
236
|
+
nativeMask=createTextPaintMask(element,canvas);life.own(()=>nativeMask.release());
|
|
237
237
|
fingerprint=createDOMTextFingerprint(element,canvas);
|
|
238
238
|
Object.assign(canvas.style,{position:'absolute',pointerEvents:'none',display:'none',margin:'0',padding:'0',border:'0',maxWidth:'none',maxHeight:'none'});
|
|
239
239
|
element.append(canvas);life.own(()=>canvas.remove());
|
|
@@ -266,8 +266,8 @@ export function attachTextSurface(element,THREE,options={}){
|
|
|
266
266
|
life.listen(window,'resize',refresh);
|
|
267
267
|
for(const event of ['loadingdone','loadingerror'])life.listen(document.fonts,event,()=>{fontRevision++;refresh();});
|
|
268
268
|
life.listen(forced,'change',()=>{native();refresh();});life.listen(reduced,'change',refresh);
|
|
269
|
-
if(pendingPhase!==null)play(pendingPhase);else request();
|
|
270
|
-
}catch(error){fail(error);}
|
|
269
|
+
if(pendingPhase!==null)play(pendingPhase);else request();
|
|
270
|
+
}catch(error){fail(error);}
|
|
271
271
|
return {element,ready,
|
|
272
272
|
refresh,
|
|
273
273
|
update(next={}){
|
|
@@ -276,16 +276,16 @@ export function attachTextSurface(element,THREE,options={}){
|
|
|
276
276
|
validResting(next.resting??resting);resting=next.resting??resting;
|
|
277
277
|
const normalized=normalizeTextEffectOptions(next.inputEffectOptions?{formation:-1,...next.inputEffectOptions}:settings,candidate==='none'?'dust-wind':candidate);
|
|
278
278
|
effectName=candidate;settings=normalized;if('enabled' in next)enabled=next.enabled!==false;
|
|
279
|
-
effect?.cancel();rich?.cancel();pendingPhase=null;pendingStarted=null;effectStarted=null;phase='enter';if(content)content.visible=true;native();refresh();
|
|
279
|
+
effect?.cancel();rich?.cancel();pendingPhase=null;pendingStarted=null;effectStarted=null;phase='enter';if(content)content.visible=true;native();refresh();
|
|
280
280
|
},
|
|
281
|
-
cancel(){if(life.disposed)return;effect?.cancel();rich?.cancel();pendingPhase=null;pendingStarted=null;effectStarted=null;phase='enter';if(content)content.visible=true;native();refresh();},
|
|
282
|
-
play,
|
|
281
|
+
cancel(){if(life.disposed)return;effect?.cancel();rich?.cancel();pendingPhase=null;pendingStarted=null;effectStarted=null;phase='enter';if(content)content.visible=true;native();refresh();},
|
|
282
|
+
play,
|
|
283
283
|
stats:()=>{const started=pendingPhase!==null?pendingStarted:effectStarted,currentPhase=pendingPhase??phase;
|
|
284
|
-
const duration=reduced.matches||effectName==='none'?0:settings.duration;
|
|
285
|
-
const finishAt=started===null?null:started+(duration===0?0:duration+(currentPhase==='enter'&&resting==='native'?350:0));
|
|
284
|
+
const duration=reduced.matches||effectName==='none'?0:settings.duration;
|
|
285
|
+
const finishAt=started===null?null:started+(duration===0?0:duration+(currentPhase==='enter'&&resting==='native'?350:0));
|
|
286
286
|
const suspended=document.hidden||!intersecting;
|
|
287
|
-
const completed=started!==null&&(suspended?window.performance.now()>=finishAt:pendingPhase===null&&(currentPhase==='exit'?content?.visible===false:resting==='mesh'?mode==='mesh'&&!(rich?rich.stats().active:effect?.active):mode==='native'&&reason==='native resting presentation'));
|
|
288
|
-
return {completed,timeline:{phase:currentPhase,started,ends:started===null?null:started+duration,finishAt},disposed:life.disposed,mode,reason,committed,preparing,draws,builds,pending:frame!==null,suspended:document.hidden||!intersecting,triangles:rich?rich.stats().triangles:view?.triangleCount??0,
|
|
287
|
+
const completed=started!==null&&(suspended?window.performance.now()>=finishAt:pendingPhase===null&&(currentPhase==='exit'?content?.visible===false:resting==='mesh'?mode==='mesh'&&!(rich?rich.stats().active:effect?.active):mode==='native'&&reason==='native resting presentation'));
|
|
288
|
+
return {completed,timeline:{phase:currentPhase,started,ends:started===null?null:started+duration,finishAt},disposed:life.disposed,mode,reason,committed,preparing,draws,builds,pending:frame!==null,suspended:document.hidden||!intersecting,triangles:rich?rich.stats().triangles:view?.triangleCount??0,
|
|
289
289
|
width:layout?.width??0,height:layout?.height??0,fontFamily:engine?.face?.family,displayFontSize:layout?.size,divisions:engine?.divisions,effect:effect?.stats(),rich:rich?.stats()??null};},
|
|
290
290
|
destroy(){if(life.disposed)return;native('destroyed');resolveReady({mode,reason});life.destroy();}
|
|
291
291
|
};
|
package/src/font-rasterizer.js
CHANGED
|
@@ -11,14 +11,14 @@ export class FontRasterizer {
|
|
|
11
11
|
this.cache = new Map();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
configure(direction = 'ltr') {
|
|
15
|
-
this.context.font = this.font;
|
|
16
|
-
// Build the cached source for legibility, independently of the GPU's cheaper
|
|
17
|
-
// moving-particle sampler. Reapply after canvas resize resets drawing state.
|
|
18
|
-
if ('textRendering' in this.context) this.context.textRendering = this.textRendering ?? 'optimizeLegibility';
|
|
19
|
-
if ('fontKerning' in this.context) this.context.fontKerning = this.fontKerning ?? 'normal';
|
|
20
|
-
if ('letterSpacing' in this.context) this.context.letterSpacing = `${this.letterSpacing || 0}px`;
|
|
21
|
-
if ('wordSpacing' in this.context) this.context.wordSpacing = `${this.wordSpacing || 0}px`;
|
|
14
|
+
configure(direction = 'ltr') {
|
|
15
|
+
this.context.font = this.font;
|
|
16
|
+
// Build the cached source for legibility, independently of the GPU's cheaper
|
|
17
|
+
// moving-particle sampler. Reapply after canvas resize resets drawing state.
|
|
18
|
+
if ('textRendering' in this.context) this.context.textRendering = this.textRendering ?? 'optimizeLegibility';
|
|
19
|
+
if ('fontKerning' in this.context) this.context.fontKerning = this.fontKerning ?? 'normal';
|
|
20
|
+
if ('letterSpacing' in this.context) this.context.letterSpacing = `${this.letterSpacing || 0}px`;
|
|
21
|
+
if ('wordSpacing' in this.context) this.context.wordSpacing = `${this.wordSpacing || 0}px`;
|
|
22
22
|
this.context.textAlign = 'left'; this.context.textBaseline = 'alphabetic';
|
|
23
23
|
// Layout already resolves bidi and mirrors punctuation. Do not mirror it again
|
|
24
24
|
// when Canvas draws an isolated, direction-neutral glyph such as a bracket.
|
|
@@ -29,24 +29,24 @@ export class FontRasterizer {
|
|
|
29
29
|
const key = direction + ':' + text;
|
|
30
30
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
31
31
|
this.configure(direction);
|
|
32
|
-
const metric = this.context.measureText(text);
|
|
33
|
-
let horizontalScale = 1;
|
|
34
|
-
if (this.displayFontSize > 0 && metric.width > 0) {
|
|
35
|
-
const factor = this.displayFontSize / 200;
|
|
36
|
-
this.context.font = this.font.replace('200px', `${this.displayFontSize}px`);
|
|
37
|
-
if ('letterSpacing' in this.context) this.context.letterSpacing = `${(this.letterSpacing || 0) * factor}px`;
|
|
38
|
-
if ('wordSpacing' in this.context) this.context.wordSpacing = `${(this.wordSpacing || 0) * factor}px`;
|
|
39
|
-
const advance = this.context.measureText(text).width / factor;
|
|
40
|
-
if (Number.isFinite(advance) && advance > 0) horizontalScale = advance / metric.width;
|
|
41
|
-
this.configure(direction);
|
|
42
|
-
}
|
|
32
|
+
const metric = this.context.measureText(text);
|
|
33
|
+
let horizontalScale = 1;
|
|
34
|
+
if (this.displayFontSize > 0 && metric.width > 0) {
|
|
35
|
+
const factor = this.displayFontSize / 200;
|
|
36
|
+
this.context.font = this.font.replace('200px', `${this.displayFontSize}px`);
|
|
37
|
+
if ('letterSpacing' in this.context) this.context.letterSpacing = `${(this.letterSpacing || 0) * factor}px`;
|
|
38
|
+
if ('wordSpacing' in this.context) this.context.wordSpacing = `${(this.wordSpacing || 0) * factor}px`;
|
|
39
|
+
const advance = this.context.measureText(text).width / factor;
|
|
40
|
+
if (Number.isFinite(advance) && advance > 0) horizontalScale = advance / metric.width;
|
|
41
|
+
this.configure(direction);
|
|
42
|
+
}
|
|
43
43
|
const padding = 2;
|
|
44
44
|
const drawOffsetX = Math.ceil(Math.max(0, (metric.actualBoundingBoxLeft || 0)*horizontalScale)) + padding;
|
|
45
45
|
const baseline = Math.ceil(Math.max(this.ascent, metric.actualBoundingBoxAscent || 0)) + padding;
|
|
46
46
|
const width = Math.max(2, Math.ceil(Math.max(metric.width, metric.actualBoundingBoxRight || 0)*horizontalScale + drawOffsetX + padding));
|
|
47
47
|
const height = Math.max(2, Math.ceil(baseline + Math.max(this.descent, metric.actualBoundingBoxDescent || 0) + padding));
|
|
48
48
|
this.canvas.width = width; this.canvas.height = height; this.configure(direction);
|
|
49
|
-
this.context.save();this.context.translate(drawOffsetX,baseline);this.context.scale(horizontalScale,1);
|
|
49
|
+
this.context.save();this.context.translate(drawOffsetX,baseline);this.context.scale(horizontalScale,1);
|
|
50
50
|
this.context.fillText(text,0,0);this.context.restore();
|
|
51
51
|
const rgba = this.context.getImageData(0, 0, width, height).data;
|
|
52
52
|
const result = { width, height, baseline, drawOffsetX, advance: metric.width*horizontalScale, rgba };
|
package/src/insertion-range.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
// UTF-16 edit range shared by both adapters. The selection disambiguates repeats.
|
|
2
|
-
export function insertedRange(before,after,inputType='insertText'){
|
|
3
|
-
if(!before||!after||!/^insert/.test(inputType)||before.text===after.text)return null;
|
|
4
|
-
const start=before.start??0,end=before.end??start;
|
|
5
|
-
const length=after.text.length-(before.text.length-(end-start));
|
|
6
|
-
if(length>0&&after.text.slice(0,start)===before.text.slice(0,start)&&after.text.slice(start+length)===before.text.slice(end))return {start,end:start+length};
|
|
7
|
-
let left=0,right=0;
|
|
8
|
-
while(left<before.text.length&&left<after.text.length&&before.text[left]===after.text[left])left++;
|
|
9
|
-
while(right<before.text.length-left&&right<after.text.length-left&&before.text[before.text.length-1-right]===after.text[after.text.length-1-right])right++;
|
|
10
|
-
return after.text.length-right>left?{start:left,end:after.text.length-right}:null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Keep the surviving pieces of an older insertion when a later edit shifts it.
|
|
14
|
-
export function remapInsertionRange(range,start,oldEnd,delta){
|
|
15
|
-
const pieces=[];
|
|
16
|
-
if(range.start<start)pieces.push({start:range.start,end:Math.min(range.end,start)});
|
|
17
|
-
if(range.end>oldEnd)pieces.push({start:Math.max(range.start,oldEnd)+delta,end:range.end+delta});
|
|
18
|
-
return pieces.filter(piece=>piece.end>piece.start);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function removedRange(before,after,inputType=''){
|
|
22
|
-
if(!before||!after||!/^delete/.test(inputType))return null;
|
|
23
|
-
const length=before.text.length-after.text.length;if(length<=0)return null;
|
|
24
|
-
const caret=before.start??0;
|
|
25
|
-
const start=before.end>caret?caret:/Backward$/.test(inputType)?Math.max(0,caret-length):caret;
|
|
26
|
-
if(before.text.slice(0,start)+before.text.slice(start+length)===after.text)return {start,end:start+length};
|
|
27
|
-
let left=0;while(left<after.text.length&&before.text[left]===after.text[left])left++;
|
|
28
|
-
return {start:left,end:left+length};
|
|
29
|
-
}
|
|
1
|
+
// UTF-16 edit range shared by both adapters. The selection disambiguates repeats.
|
|
2
|
+
export function insertedRange(before,after,inputType='insertText'){
|
|
3
|
+
if(!before||!after||!/^insert/.test(inputType)||before.text===after.text)return null;
|
|
4
|
+
const start=before.start??0,end=before.end??start;
|
|
5
|
+
const length=after.text.length-(before.text.length-(end-start));
|
|
6
|
+
if(length>0&&after.text.slice(0,start)===before.text.slice(0,start)&&after.text.slice(start+length)===before.text.slice(end))return {start,end:start+length};
|
|
7
|
+
let left=0,right=0;
|
|
8
|
+
while(left<before.text.length&&left<after.text.length&&before.text[left]===after.text[left])left++;
|
|
9
|
+
while(right<before.text.length-left&&right<after.text.length-left&&before.text[before.text.length-1-right]===after.text[after.text.length-1-right])right++;
|
|
10
|
+
return after.text.length-right>left?{start:left,end:after.text.length-right}:null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Keep the surviving pieces of an older insertion when a later edit shifts it.
|
|
14
|
+
export function remapInsertionRange(range,start,oldEnd,delta){
|
|
15
|
+
const pieces=[];
|
|
16
|
+
if(range.start<start)pieces.push({start:range.start,end:Math.min(range.end,start)});
|
|
17
|
+
if(range.end>oldEnd)pieces.push({start:Math.max(range.start,oldEnd)+delta,end:range.end+delta});
|
|
18
|
+
return pieces.filter(piece=>piece.end>piece.start);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removedRange(before,after,inputType=''){
|
|
22
|
+
if(!before||!after||!/^delete/.test(inputType))return null;
|
|
23
|
+
const length=before.text.length-after.text.length;if(length<=0)return null;
|
|
24
|
+
const caret=before.start??0;
|
|
25
|
+
const start=before.end>caret?caret:/Backward$/.test(inputType)?Math.max(0,caret-length):caret;
|
|
26
|
+
if(before.text.slice(0,start)+before.text.slice(start+length)===after.text)return {start,end:start+length};
|
|
27
|
+
let left=0;while(left<after.text.length&&before.text[left]===after.text[left])left++;
|
|
28
|
+
return {start:left,end:left+length};
|
|
29
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
// Text and image surfaces share one texture per raster (not per triangle).
|
|
2
|
-
// References include deletion ghosts, which can outlive their source scene mesh.
|
|
3
|
-
const surfaces=new WeakMap(),materials=new WeakMap();
|
|
4
|
-
export function attachRasterTexture(THREE,material,raster,{lodBias=raster.mask===false?0:-.75}={}){
|
|
5
|
-
if(!Number.isFinite(lodBias))throw TypeError('A finite raster LOD bias is required');
|
|
6
|
-
let surface=surfaces.get(raster);
|
|
7
|
-
if(!surface){
|
|
8
|
-
const texture=new THREE.DataTexture(raster.rgba,raster.width,raster.height,THREE.RGBAFormat,THREE.UnsignedByteType);
|
|
9
|
-
if(raster.mask===false)texture.colorSpace=THREE.SRGBColorSpace;
|
|
10
|
-
// One bilinear mip fetch while moving; the shader adds the adjacent mip only
|
|
11
|
-
// near the resting pose. Sampling policy never mutates a shared texture.
|
|
12
|
-
texture.minFilter=THREE.LinearMipmapNearestFilter;texture.magFilter=THREE.LinearFilter;texture.generateMipmaps=true;texture.needsUpdate=true;
|
|
13
|
-
surface={texture,refs:0,raster};surfaces.set(raster,surface);
|
|
14
|
-
}
|
|
15
|
-
material.uniforms.rasterMap={value:surface.texture};
|
|
16
|
-
material.uniforms.rasterSize={value:new THREE.Vector2(raster.width,raster.height)};
|
|
17
|
-
material.uniforms.rasterLodBias={value:lodBias};
|
|
18
|
-
return lease(material,surface);
|
|
19
|
-
}
|
|
20
|
-
// Derivatives describe this triangle's current projected texture footprint.
|
|
21
|
-
// Explicit LOD is core in WebGL2 (Three aliases texture2DLodEXT) and optional in
|
|
22
|
-
// WebGL1. Without it, keep a single conventional texture lookup as a safe fallback.
|
|
23
|
-
const rasterSamplingShader=`
|
|
24
|
-
uniform vec2 rasterSize;
|
|
25
|
-
uniform float rasterLodBias;
|
|
26
|
-
varying float vRasterQuality;
|
|
27
|
-
vec4 thdRasterSample(vec2 uv){
|
|
28
|
-
#if __VERSION__ >= 300 || defined(GL_EXT_shader_texture_lod)
|
|
29
|
-
vec2 dx=dFdx(uv*rasterSize),dy=dFdy(uv*rasterSize);
|
|
30
|
-
float footprint=max(max(dot(dx,dx),dot(dy,dy)),0.00000001);
|
|
31
|
-
float lastLevel=floor(log2(max(max(rasterSize.x,rasterSize.y),1.0)));
|
|
32
|
-
float lod=clamp(0.5*log2(footprint)+rasterLodBias,0.0,lastLevel);
|
|
33
|
-
float primary=floor(lod+0.5);
|
|
34
|
-
vec4 sampleColor=texture2DLodEXT(rasterMap,uv,primary);
|
|
35
|
-
float weight=abs(lod-primary)*clamp(vRasterQuality,0.0,1.0);
|
|
36
|
-
if(weight>0.0){
|
|
37
|
-
float adjacent=primary<lod?primary+1.0:primary-1.0;
|
|
38
|
-
sampleColor=mix(sampleColor,texture2DLodEXT(rasterMap,uv,adjacent),weight);
|
|
39
|
-
}
|
|
40
|
-
return sampleColor;
|
|
41
|
-
#else
|
|
42
|
-
return texture2D(rasterMap,uv);
|
|
43
|
-
#endif
|
|
44
|
-
}
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
|
-
/** Add the shared sampler to text/image shaders that already declare rasterMap
|
|
48
|
-
* and vRasterUV. The default is settled; TriangleEffect supplies each facet's
|
|
49
|
-
* existing phase without another clock, attribute buffer or per-frame JS work. */
|
|
50
|
-
export function applyRasterSamplingShader(material){
|
|
51
|
-
material.defines={...material.defines,THD_RASTER_TEXTURE:1};
|
|
52
|
-
material.extensions={...material.extensions,derivatives:true,shaderTextureLOD:true};
|
|
53
|
-
material.vertexShader='varying float vRasterQuality;\n'+material.vertexShader;
|
|
54
|
-
material.vertexShader=material.vertexShader.replace(/void main\(\)\s*\{/,'void main(){ vRasterQuality=1.0;');
|
|
55
|
-
const main=material.fragmentShader.indexOf('void main');
|
|
56
|
-
material.fragmentShader=material.fragmentShader.slice(0,main)+rasterSamplingShader+material.fragmentShader.slice(main);
|
|
57
|
-
material.fragmentShader=material.fragmentShader.replace('texture2D(rasterMap,vRasterUV)','thdRasterSample(vRasterUV)');
|
|
58
|
-
}
|
|
59
|
-
function lease(material,surface){
|
|
60
|
-
surface.refs++;materials.set(material,surface);let released=false;
|
|
61
|
-
return ()=>{if(released)return;released=true;materials.delete(material);if(--surface.refs===0){surface.texture.dispose();surfaces.delete(surface.raster);}};
|
|
62
|
-
}
|
|
63
|
-
export function retainRasterTexture(source,target){
|
|
64
|
-
const surface=materials.get(source);if(!surface)return null;
|
|
65
|
-
// ShaderMaterial.clone clones texture uniforms. Use the shared source instead
|
|
66
|
-
// of uploading an identical new image for each partial deletion.
|
|
67
|
-
if(target.uniforms.rasterMap.value!==surface.texture)target.uniforms.rasterMap.value.dispose();
|
|
68
|
-
target.uniforms.rasterMap.value=surface.texture;
|
|
69
|
-
return lease(target,surface);
|
|
70
|
-
}
|
|
71
|
-
export function applyRasterTextureShader(material,{mask=true}={}){
|
|
72
|
-
material.vertexShader='attribute vec2 rasterUV; varying vec2 vRasterUV;\n'+material.vertexShader;
|
|
73
|
-
material.vertexShader=material.vertexShader.replace('void main() {','void main() { vRasterUV=rasterUV;');
|
|
74
|
-
material.fragmentShader='uniform sampler2D rasterMap; varying vec2 vRasterUV;\n'+material.fragmentShader;
|
|
75
|
-
material.fragmentShader=material.fragmentShader.replace('float coverage =', 'vec4 rasterSample=texture2D(rasterMap,vRasterUV); float coverage =');
|
|
76
|
-
material.fragmentShader=material.fragmentShader.replace('max(max(vColor.r, vColor.g), vColor.b) * brightness','rasterSample.a * max(max(vColor.r, vColor.g), vColor.b) * min(brightness, 1.0)');
|
|
77
|
-
if(!mask)material.fragmentShader=material.fragmentShader.replace('vec4(tint * min(1.0, diffuse + sheen), coverage)', 'vec4(rasterSample.rgb * tint * min(1.0, diffuse + sheen), coverage)');
|
|
78
|
-
applyRasterSamplingShader(material);
|
|
79
|
-
}
|
|
1
|
+
// Text and image surfaces share one texture per raster (not per triangle).
|
|
2
|
+
// References include deletion ghosts, which can outlive their source scene mesh.
|
|
3
|
+
const surfaces=new WeakMap(),materials=new WeakMap();
|
|
4
|
+
export function attachRasterTexture(THREE,material,raster,{lodBias=raster.mask===false?0:-.75}={}){
|
|
5
|
+
if(!Number.isFinite(lodBias))throw TypeError('A finite raster LOD bias is required');
|
|
6
|
+
let surface=surfaces.get(raster);
|
|
7
|
+
if(!surface){
|
|
8
|
+
const texture=new THREE.DataTexture(raster.rgba,raster.width,raster.height,THREE.RGBAFormat,THREE.UnsignedByteType);
|
|
9
|
+
if(raster.mask===false)texture.colorSpace=THREE.SRGBColorSpace;
|
|
10
|
+
// One bilinear mip fetch while moving; the shader adds the adjacent mip only
|
|
11
|
+
// near the resting pose. Sampling policy never mutates a shared texture.
|
|
12
|
+
texture.minFilter=THREE.LinearMipmapNearestFilter;texture.magFilter=THREE.LinearFilter;texture.generateMipmaps=true;texture.needsUpdate=true;
|
|
13
|
+
surface={texture,refs:0,raster};surfaces.set(raster,surface);
|
|
14
|
+
}
|
|
15
|
+
material.uniforms.rasterMap={value:surface.texture};
|
|
16
|
+
material.uniforms.rasterSize={value:new THREE.Vector2(raster.width,raster.height)};
|
|
17
|
+
material.uniforms.rasterLodBias={value:lodBias};
|
|
18
|
+
return lease(material,surface);
|
|
19
|
+
}
|
|
20
|
+
// Derivatives describe this triangle's current projected texture footprint.
|
|
21
|
+
// Explicit LOD is core in WebGL2 (Three aliases texture2DLodEXT) and optional in
|
|
22
|
+
// WebGL1. Without it, keep a single conventional texture lookup as a safe fallback.
|
|
23
|
+
const rasterSamplingShader=`
|
|
24
|
+
uniform vec2 rasterSize;
|
|
25
|
+
uniform float rasterLodBias;
|
|
26
|
+
varying float vRasterQuality;
|
|
27
|
+
vec4 thdRasterSample(vec2 uv){
|
|
28
|
+
#if __VERSION__ >= 300 || defined(GL_EXT_shader_texture_lod)
|
|
29
|
+
vec2 dx=dFdx(uv*rasterSize),dy=dFdy(uv*rasterSize);
|
|
30
|
+
float footprint=max(max(dot(dx,dx),dot(dy,dy)),0.00000001);
|
|
31
|
+
float lastLevel=floor(log2(max(max(rasterSize.x,rasterSize.y),1.0)));
|
|
32
|
+
float lod=clamp(0.5*log2(footprint)+rasterLodBias,0.0,lastLevel);
|
|
33
|
+
float primary=floor(lod+0.5);
|
|
34
|
+
vec4 sampleColor=texture2DLodEXT(rasterMap,uv,primary);
|
|
35
|
+
float weight=abs(lod-primary)*clamp(vRasterQuality,0.0,1.0);
|
|
36
|
+
if(weight>0.0){
|
|
37
|
+
float adjacent=primary<lod?primary+1.0:primary-1.0;
|
|
38
|
+
sampleColor=mix(sampleColor,texture2DLodEXT(rasterMap,uv,adjacent),weight);
|
|
39
|
+
}
|
|
40
|
+
return sampleColor;
|
|
41
|
+
#else
|
|
42
|
+
return texture2D(rasterMap,uv);
|
|
43
|
+
#endif
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
/** Add the shared sampler to text/image shaders that already declare rasterMap
|
|
48
|
+
* and vRasterUV. The default is settled; TriangleEffect supplies each facet's
|
|
49
|
+
* existing phase without another clock, attribute buffer or per-frame JS work. */
|
|
50
|
+
export function applyRasterSamplingShader(material){
|
|
51
|
+
material.defines={...material.defines,THD_RASTER_TEXTURE:1};
|
|
52
|
+
material.extensions={...material.extensions,derivatives:true,shaderTextureLOD:true};
|
|
53
|
+
material.vertexShader='varying float vRasterQuality;\n'+material.vertexShader;
|
|
54
|
+
material.vertexShader=material.vertexShader.replace(/void main\(\)\s*\{/,'void main(){ vRasterQuality=1.0;');
|
|
55
|
+
const main=material.fragmentShader.indexOf('void main');
|
|
56
|
+
material.fragmentShader=material.fragmentShader.slice(0,main)+rasterSamplingShader+material.fragmentShader.slice(main);
|
|
57
|
+
material.fragmentShader=material.fragmentShader.replace('texture2D(rasterMap,vRasterUV)','thdRasterSample(vRasterUV)');
|
|
58
|
+
}
|
|
59
|
+
function lease(material,surface){
|
|
60
|
+
surface.refs++;materials.set(material,surface);let released=false;
|
|
61
|
+
return ()=>{if(released)return;released=true;materials.delete(material);if(--surface.refs===0){surface.texture.dispose();surfaces.delete(surface.raster);}};
|
|
62
|
+
}
|
|
63
|
+
export function retainRasterTexture(source,target){
|
|
64
|
+
const surface=materials.get(source);if(!surface)return null;
|
|
65
|
+
// ShaderMaterial.clone clones texture uniforms. Use the shared source instead
|
|
66
|
+
// of uploading an identical new image for each partial deletion.
|
|
67
|
+
if(target.uniforms.rasterMap.value!==surface.texture)target.uniforms.rasterMap.value.dispose();
|
|
68
|
+
target.uniforms.rasterMap.value=surface.texture;
|
|
69
|
+
return lease(target,surface);
|
|
70
|
+
}
|
|
71
|
+
export function applyRasterTextureShader(material,{mask=true}={}){
|
|
72
|
+
material.vertexShader='attribute vec2 rasterUV; varying vec2 vRasterUV;\n'+material.vertexShader;
|
|
73
|
+
material.vertexShader=material.vertexShader.replace('void main() {','void main() { vRasterUV=rasterUV;');
|
|
74
|
+
material.fragmentShader='uniform sampler2D rasterMap; varying vec2 vRasterUV;\n'+material.fragmentShader;
|
|
75
|
+
material.fragmentShader=material.fragmentShader.replace('float coverage =', 'vec4 rasterSample=texture2D(rasterMap,vRasterUV); float coverage =');
|
|
76
|
+
material.fragmentShader=material.fragmentShader.replace('max(max(vColor.r, vColor.g), vColor.b) * brightness','rasterSample.a * max(max(vColor.r, vColor.g), vColor.b) * min(brightness, 1.0)');
|
|
77
|
+
if(!mask)material.fragmentShader=material.fragmentShader.replace('vec4(tint * min(1.0, diffuse + sheen), coverage)', 'vec4(rasterSample.rgb * tint * min(1.0, diffuse + sheen), coverage)');
|
|
78
|
+
applyRasterSamplingShader(material);
|
|
79
|
+
}
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
// Shared raster -> UV triangle grid for font masks and RGBA images. No color
|
|
2
|
-
// averaging, alpha quantization or triangle merging. All filtering is on the GPU.
|
|
3
|
-
export function rasterToTextureMesh(rgba,width,height,divisions,{filterRadius=.5,aspect=width/height}={}){
|
|
4
|
-
if(!Number.isInteger(width)||width<1||!Number.isInteger(height)||height<1||rgba.length!==width*height*4)throw RangeError('Invalid raster');
|
|
5
|
-
if(!Number.isInteger(divisions)||divisions<1||divisions>256)throw RangeError('Invalid divisions');
|
|
6
|
-
if(!Number.isFinite(filterRadius)||filterRadius<.5)throw RangeError('Invalid texture filter support');
|
|
7
|
-
if(!Number.isFinite(aspect)||aspect<=0)throw RangeError('Invalid grid aspect');
|
|
8
|
-
const rows=divisions,columns=Math.max(1,Math.round(aspect*rows));
|
|
9
|
-
const cw=width/columns,ch=height/rows,occupied=new Uint8Array(rows*columns);
|
|
10
|
-
// Conservative filter support: half a texel for bilinear filtering by default;
|
|
11
|
-
// image surfaces may request the wider footprint used by minified mipmaps.
|
|
12
|
-
// A thin line or isolated pixel cannot be lost to sparse point sampling.
|
|
13
|
-
for(let y=0;y<height;y++)for(let x=0;x<width;x++){
|
|
14
|
-
if(!rgba[(y*width+x)*4+3])continue;
|
|
15
|
-
const x0=Math.max(0,x-filterRadius),x1=Math.min(width,x+1+filterRadius),y0=Math.max(0,y-filterRadius),y1=Math.min(height,y+1+filterRadius);
|
|
16
|
-
for(let row=Math.floor(y0/ch);row<=Math.min(rows-1,Math.floor(y1/ch));row++)for(let col=Math.floor(x0/cw);col<=Math.min(columns-1,Math.floor(x1/cw));col++){
|
|
17
|
-
const at=row*columns+col;if(occupied[at]===3)continue;
|
|
18
|
-
const left=Math.max(0,x0/cw-col),right=Math.min(1,x1/cw-col),top=Math.max(0,y0/ch-row),bottom=Math.min(1,y1/ch-row);
|
|
19
|
-
const even=(row+col)%2===0;
|
|
20
|
-
if(even?left+top<=1:top<=right)occupied[at]|=1;
|
|
21
|
-
if(even?right+bottom>=1:bottom>=left)occupied[at]|=2;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
let triangleCount=0;for(const bits of occupied)triangleCount+=(bits&1?1:0)+(bits&2?1:0);
|
|
25
|
-
const coordinates=new Float64Array(triangleCount*6),uvs=new Float32Array(triangleCount*6),coverage=new Uint8Array(triangleCount).fill(6);
|
|
26
|
-
let at=0;
|
|
27
|
-
for(let row=0;row<rows;row++)for(let col=0;col<columns;col++){
|
|
28
|
-
const left=col*cw,right=(col+1)*cw,top=row*ch,bottom=(row+1)*ch,even=(row+col)%2===0;
|
|
29
|
-
for(let side=0;side<2;side++)if(occupied[row*columns+col]&(1<<side)){
|
|
30
|
-
const points=even?[right,top,side?right:left,bottom,left,side?bottom:top]:[left,top,side?left:right,bottom,right,side?bottom:top];
|
|
31
|
-
coordinates.set(points,at);for(let i=0;i<6;i++)uvs[at+i]=points[i]/(i%2?height:width);at+=6;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return {coordinates,uvs,coverage,triangleCount};
|
|
35
|
-
}
|
|
1
|
+
// Shared raster -> UV triangle grid for font masks and RGBA images. No color
|
|
2
|
+
// averaging, alpha quantization or triangle merging. All filtering is on the GPU.
|
|
3
|
+
export function rasterToTextureMesh(rgba,width,height,divisions,{filterRadius=.5,aspect=width/height}={}){
|
|
4
|
+
if(!Number.isInteger(width)||width<1||!Number.isInteger(height)||height<1||rgba.length!==width*height*4)throw RangeError('Invalid raster');
|
|
5
|
+
if(!Number.isInteger(divisions)||divisions<1||divisions>256)throw RangeError('Invalid divisions');
|
|
6
|
+
if(!Number.isFinite(filterRadius)||filterRadius<.5)throw RangeError('Invalid texture filter support');
|
|
7
|
+
if(!Number.isFinite(aspect)||aspect<=0)throw RangeError('Invalid grid aspect');
|
|
8
|
+
const rows=divisions,columns=Math.max(1,Math.round(aspect*rows));
|
|
9
|
+
const cw=width/columns,ch=height/rows,occupied=new Uint8Array(rows*columns);
|
|
10
|
+
// Conservative filter support: half a texel for bilinear filtering by default;
|
|
11
|
+
// image surfaces may request the wider footprint used by minified mipmaps.
|
|
12
|
+
// A thin line or isolated pixel cannot be lost to sparse point sampling.
|
|
13
|
+
for(let y=0;y<height;y++)for(let x=0;x<width;x++){
|
|
14
|
+
if(!rgba[(y*width+x)*4+3])continue;
|
|
15
|
+
const x0=Math.max(0,x-filterRadius),x1=Math.min(width,x+1+filterRadius),y0=Math.max(0,y-filterRadius),y1=Math.min(height,y+1+filterRadius);
|
|
16
|
+
for(let row=Math.floor(y0/ch);row<=Math.min(rows-1,Math.floor(y1/ch));row++)for(let col=Math.floor(x0/cw);col<=Math.min(columns-1,Math.floor(x1/cw));col++){
|
|
17
|
+
const at=row*columns+col;if(occupied[at]===3)continue;
|
|
18
|
+
const left=Math.max(0,x0/cw-col),right=Math.min(1,x1/cw-col),top=Math.max(0,y0/ch-row),bottom=Math.min(1,y1/ch-row);
|
|
19
|
+
const even=(row+col)%2===0;
|
|
20
|
+
if(even?left+top<=1:top<=right)occupied[at]|=1;
|
|
21
|
+
if(even?right+bottom>=1:bottom>=left)occupied[at]|=2;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let triangleCount=0;for(const bits of occupied)triangleCount+=(bits&1?1:0)+(bits&2?1:0);
|
|
25
|
+
const coordinates=new Float64Array(triangleCount*6),uvs=new Float32Array(triangleCount*6),coverage=new Uint8Array(triangleCount).fill(6);
|
|
26
|
+
let at=0;
|
|
27
|
+
for(let row=0;row<rows;row++)for(let col=0;col<columns;col++){
|
|
28
|
+
const left=col*cw,right=(col+1)*cw,top=row*ch,bottom=(row+1)*ch,even=(row+col)%2===0;
|
|
29
|
+
for(let side=0;side<2;side++)if(occupied[row*columns+col]&(1<<side)){
|
|
30
|
+
const points=even?[right,top,side?right:left,bottom,left,side?bottom:top]:[left,top,side?left:right,bottom,right,side?bottom:top];
|
|
31
|
+
coordinates.set(points,at);for(let i=0;i<6;i++)uvs[at+i]=points[i]/(i%2?height:width);at+=6;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {coordinates,uvs,coverage,triangleCount};
|
|
35
|
+
}
|