@vectojs/core 0.2.6 → 0.2.8
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/dist/{chunk-SSOP3F5F.mjs → chunk-FNXOS7IA.mjs} +161 -15
- package/dist/{chunk-SL654OMF.mjs → chunk-G6GQO7W2.mjs} +26 -0
- package/dist/{chunk-GADBN53Z.js → chunk-XB3ZY5MF.js} +82 -1
- package/dist/{chunk-BWLNEJJW.js → chunk-XLZDOFSB.js} +175 -29
- package/dist/{chunk-4BO5XBJL.js → chunk-XRWZS2DM.js} +34 -8
- package/dist/{chunk-PK5EYWIT.mjs → chunk-YBW7ECZP.mjs} +82 -1
- package/dist/components/TextEntity.d.ts +17 -1
- package/dist/index.js +288 -143
- package/dist/index.mjs +171 -26
- package/dist/layout/LayoutEngine.d.ts +24 -0
- package/dist/layout.js +2 -2
- package/dist/layout.mjs +1 -1
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/text/MSDFTextEntity.d.ts +6 -1
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +31 -0
- package/dist/tree/Scene.d.ts +29 -2
- package/package.json +5 -3
|
@@ -32,25 +32,48 @@ function computeLineSegments(top, bottom, maxWidth, exclusions) {
|
|
|
32
32
|
}
|
|
33
33
|
var LayoutEngine = (_class = class {
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Horizontal alignment. `'justify'` stretches inter-word spaces (or, for
|
|
37
|
+
* space-less CJK lines, inter-character gaps) so wrapped lines end flush at
|
|
38
|
+
* `maxWidth`; the last line of each paragraph stays ragged. Only applies to
|
|
39
|
+
* the object layout path without exclusion shapes.
|
|
40
|
+
*/
|
|
41
|
+
__init() {this.textAlign = "left"}
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
__init2() {this.preserveLeadingSpaces = false}
|
|
37
44
|
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
__init3() {this.wordCache = /* @__PURE__ */ new Map()}
|
|
47
|
+
__init4() {this.graphemeCache = /* @__PURE__ */ new Map()}
|
|
41
48
|
// Paragraph-level memo so re-`prepare()` of mostly-unchanged text (streaming
|
|
42
49
|
// append, live logs) reuses untouched paragraphs by reference instead of
|
|
43
50
|
// re-segmenting/re-measuring the whole document — turning per-token cost from
|
|
44
51
|
// O(document) into O(changed paragraph). Keyed by fontSize + text; invalidated
|
|
45
52
|
// when the font atlas (which drives glyph widths) changes.
|
|
46
|
-
|
|
53
|
+
__init5() {this.paragraphCache = /* @__PURE__ */ new Map()}
|
|
47
54
|
// Same memo for the rich path ({@link prepareRich}); keyed by fontSize + text +
|
|
48
55
|
// a per-paragraph *value* signature of the inline styles, so a streaming
|
|
49
56
|
// typewriter that appends styled runs reuses its untouched paragraphs.
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
__init6() {this.richParagraphCache = /* @__PURE__ */ new Map()}
|
|
58
|
+
__init7() {this.lastAtlas = null}
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
__init8() {this._hyphenate = null}
|
|
61
|
+
/**
|
|
62
|
+
* Optional hyphenator: given a word, return its break parts (e.g.
|
|
63
|
+
* `['hyphen', 'ation']`). Used at wrap time when a word doesn't fit; a
|
|
64
|
+
* visible '-' is drawn at the chosen break. Soft hyphens (U+00AD) in the
|
|
65
|
+
* source work without any hyphenator. Setting this clears the prepared
|
|
66
|
+
* caches (break opportunities are baked in during prepare()).
|
|
67
|
+
*/
|
|
68
|
+
get hyphenate() {
|
|
69
|
+
return this._hyphenate;
|
|
70
|
+
}
|
|
71
|
+
set hyphenate(fn) {
|
|
72
|
+
this._hyphenate = fn;
|
|
73
|
+
this.paragraphCache.clear();
|
|
74
|
+
this.richParagraphCache.clear();
|
|
75
|
+
}
|
|
76
|
+
constructor(maxWidth, maxHeight, measurer) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);
|
|
54
77
|
this.maxWidth = maxWidth;
|
|
55
78
|
this.maxHeight = maxHeight;
|
|
56
79
|
this.measurer = _nullishCoalesce(measurer, () => ( null));
|
|
@@ -157,7 +180,13 @@ var LayoutEngine = (_class = class {
|
|
|
157
180
|
const word = segment.segment;
|
|
158
181
|
const glyphs = [];
|
|
159
182
|
let width = 0;
|
|
183
|
+
let breakPoints;
|
|
160
184
|
for (const char of this.getGraphemes(word)) {
|
|
185
|
+
if (char === "\xAD") {
|
|
186
|
+
(breakPoints ??= []).push(glyphs.length);
|
|
187
|
+
shapedCharIdx += char.length;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
161
190
|
const visualStart = shapedCharIdx;
|
|
162
191
|
const visualEnd = shapedCharIdx + char.length;
|
|
163
192
|
const rawStart = indexMap[visualStart];
|
|
@@ -182,11 +211,23 @@ var LayoutEngine = (_class = class {
|
|
|
182
211
|
width += w;
|
|
183
212
|
shapedCharIdx += char.length;
|
|
184
213
|
}
|
|
214
|
+
if (!breakPoints && this._hyphenate && segment.isWordLike && glyphs.length > 3) {
|
|
215
|
+
const parts = this._hyphenate(word);
|
|
216
|
+
if (parts.length > 1) {
|
|
217
|
+
breakPoints = [];
|
|
218
|
+
let count = 0;
|
|
219
|
+
for (let pi = 0; pi < parts.length - 1; pi++) {
|
|
220
|
+
for (const _g of this.getGraphemes(parts[pi])) count++;
|
|
221
|
+
breakPoints.push(count);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
185
225
|
words.push({
|
|
186
226
|
glyphs,
|
|
187
227
|
width,
|
|
188
228
|
isWordLike: segment.isWordLike,
|
|
189
|
-
isWhitespace: word.trim().length === 0
|
|
229
|
+
isWhitespace: word.trim().length === 0,
|
|
230
|
+
breakPoints
|
|
190
231
|
});
|
|
191
232
|
}
|
|
192
233
|
const prepared = {
|
|
@@ -200,7 +241,12 @@ var LayoutEngine = (_class = class {
|
|
|
200
241
|
paragraphs.push(prepared);
|
|
201
242
|
offset += paragraph.length + 1;
|
|
202
243
|
}
|
|
203
|
-
return {
|
|
244
|
+
return {
|
|
245
|
+
paragraphs,
|
|
246
|
+
fontSize,
|
|
247
|
+
fallbackToCanvas: fallbackToCanvas || void 0,
|
|
248
|
+
hyphenWidth: this.glyphWidth(this.glyphKeyFor("-", fontAtlas), fontAtlas, fontSize)
|
|
249
|
+
};
|
|
204
250
|
}
|
|
205
251
|
/**
|
|
206
252
|
* **Cold pass for rich text.** Like {@link prepare}, but takes an array of
|
|
@@ -274,7 +320,13 @@ var LayoutEngine = (_class = class {
|
|
|
274
320
|
const word = segment.segment;
|
|
275
321
|
const glyphs = [];
|
|
276
322
|
let width = 0;
|
|
323
|
+
let breakPoints;
|
|
277
324
|
for (const char of this.getGraphemes(word)) {
|
|
325
|
+
if (char === "\xAD") {
|
|
326
|
+
(breakPoints ??= []).push(glyphs.length);
|
|
327
|
+
shapedCharIdx += char.length;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
278
330
|
const visualStart = shapedCharIdx;
|
|
279
331
|
const visualEnd = shapedCharIdx + char.length;
|
|
280
332
|
const rawStart = indexMap[visualStart];
|
|
@@ -302,11 +354,23 @@ var LayoutEngine = (_class = class {
|
|
|
302
354
|
width += w;
|
|
303
355
|
shapedCharIdx += char.length;
|
|
304
356
|
}
|
|
357
|
+
if (!breakPoints && this._hyphenate && segment.isWordLike && glyphs.length > 3) {
|
|
358
|
+
const parts = this._hyphenate(word);
|
|
359
|
+
if (parts.length > 1) {
|
|
360
|
+
breakPoints = [];
|
|
361
|
+
let count = 0;
|
|
362
|
+
for (let pi = 0; pi < parts.length - 1; pi++) {
|
|
363
|
+
for (const _g of this.getGraphemes(parts[pi])) count++;
|
|
364
|
+
breakPoints.push(count);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
305
368
|
words.push({
|
|
306
369
|
glyphs,
|
|
307
370
|
width,
|
|
308
371
|
isWordLike: segment.isWordLike,
|
|
309
|
-
isWhitespace: word.trim().length === 0
|
|
372
|
+
isWhitespace: word.trim().length === 0,
|
|
373
|
+
breakPoints
|
|
310
374
|
});
|
|
311
375
|
}
|
|
312
376
|
const prepared = {
|
|
@@ -350,7 +414,7 @@ var LayoutEngine = (_class = class {
|
|
|
350
414
|
let si = 0;
|
|
351
415
|
let currentLineNodes = [];
|
|
352
416
|
let paragraphBaseLevel = 0;
|
|
353
|
-
const commitLine = () => {
|
|
417
|
+
const commitLine = (justifyTo) => {
|
|
354
418
|
if (currentLineNodes.length === 0) return;
|
|
355
419
|
const runs = [];
|
|
356
420
|
let currentRun = [];
|
|
@@ -375,6 +439,37 @@ var LayoutEngine = (_class = class {
|
|
|
375
439
|
node.isRTL = node.level % 2 === 1;
|
|
376
440
|
x += node.width;
|
|
377
441
|
}
|
|
442
|
+
if (justifyTo !== void 0 && runs.length === 1) {
|
|
443
|
+
let lastContent = run.length - 1;
|
|
444
|
+
while (lastContent >= 0 && run[lastContent].char.trim() === "") lastContent--;
|
|
445
|
+
if (lastContent > 0) {
|
|
446
|
+
const contentEnd = run[lastContent].x + run[lastContent].width;
|
|
447
|
+
const slack = justifyTo - contentEnd;
|
|
448
|
+
if (slack > 0 && slack <= (justifyTo - runStartX) * 0.5) {
|
|
449
|
+
const spaceIdx = [];
|
|
450
|
+
for (let k = 1; k < lastContent; k++) {
|
|
451
|
+
if (run[k].char.trim() === "") spaceIdx.push(k);
|
|
452
|
+
}
|
|
453
|
+
if (spaceIdx.length > 0) {
|
|
454
|
+
const extra = slack / spaceIdx.length;
|
|
455
|
+
let shift = 0;
|
|
456
|
+
let nextSpace = 0;
|
|
457
|
+
for (let k = 0; k <= lastContent; k++) {
|
|
458
|
+
run[k].x += shift;
|
|
459
|
+
if (nextSpace < spaceIdx.length && k === spaceIdx[nextSpace]) {
|
|
460
|
+
run[k].width += extra;
|
|
461
|
+
shift += extra;
|
|
462
|
+
nextSpace++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
const extra = slack / lastContent;
|
|
467
|
+
for (let k = 1; k <= lastContent; k++) run[k].x += extra * k;
|
|
468
|
+
}
|
|
469
|
+
if (justifyTo > maxLineWidth) maxLineWidth = justifyTo;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
378
473
|
for (const node of run) {
|
|
379
474
|
layoutNodes.push(node);
|
|
380
475
|
}
|
|
@@ -394,6 +489,8 @@ var LayoutEngine = (_class = class {
|
|
|
394
489
|
}
|
|
395
490
|
return false;
|
|
396
491
|
};
|
|
492
|
+
const justifyTarget = this.textAlign === "justify" && !hasEx ? this.maxWidth : void 0;
|
|
493
|
+
const hyphenWidth = _nullishCoalesce(prepared.hyphenWidth, () => ( fontSize * 0.3));
|
|
397
494
|
for (const paragraph of prepared.paragraphs) {
|
|
398
495
|
if (paragraph.isEmpty) {
|
|
399
496
|
commitLine();
|
|
@@ -411,16 +508,65 @@ var LayoutEngine = (_class = class {
|
|
|
411
508
|
}
|
|
412
509
|
const lineHeight = pMax * 1.5;
|
|
413
510
|
if (!startLine(lineHeight)) break;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
511
|
+
const wordQueue = paragraph.words.slice();
|
|
512
|
+
for (let qi = 0; qi < wordQueue.length; qi++) {
|
|
513
|
+
const word = wordQueue[qi];
|
|
514
|
+
if (currentX + word.width > segs[si].x1) {
|
|
515
|
+
if (!hasEx && word.breakPoints && word.breakPoints.length > 0) {
|
|
516
|
+
const avail = segs[si].x1 - currentX;
|
|
517
|
+
let chosen = -1;
|
|
518
|
+
let prefixWidth = 0;
|
|
519
|
+
let acc = 0;
|
|
520
|
+
let bpIdx = 0;
|
|
521
|
+
for (let g = 0; g < word.glyphs.length && bpIdx < word.breakPoints.length; g++) {
|
|
522
|
+
acc += word.glyphs[g].width;
|
|
523
|
+
if (g + 1 === word.breakPoints[bpIdx]) {
|
|
524
|
+
if (acc + hyphenWidth <= avail) {
|
|
525
|
+
chosen = word.breakPoints[bpIdx];
|
|
526
|
+
prefixWidth = acc;
|
|
527
|
+
}
|
|
528
|
+
bpIdx++;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (chosen > 0) {
|
|
532
|
+
const anchorGlyph = word.glyphs[chosen - 1];
|
|
533
|
+
const prefix = {
|
|
534
|
+
glyphs: [
|
|
535
|
+
...word.glyphs.slice(0, chosen),
|
|
536
|
+
{
|
|
537
|
+
char: "-",
|
|
538
|
+
width: hyphenWidth,
|
|
539
|
+
level: anchorGlyph.level,
|
|
540
|
+
sourceIndex: anchorGlyph.sourceIndex,
|
|
541
|
+
sourceLength: 0
|
|
542
|
+
}
|
|
543
|
+
],
|
|
544
|
+
width: prefixWidth + hyphenWidth,
|
|
545
|
+
isWordLike: true,
|
|
546
|
+
isWhitespace: false
|
|
547
|
+
};
|
|
548
|
+
const rest = {
|
|
549
|
+
glyphs: word.glyphs.slice(chosen),
|
|
550
|
+
width: word.width - prefixWidth,
|
|
551
|
+
isWordLike: true,
|
|
552
|
+
isWhitespace: false,
|
|
553
|
+
breakPoints: word.breakPoints.filter((bp) => bp > chosen).map((bp) => bp - chosen)
|
|
554
|
+
};
|
|
555
|
+
wordQueue.splice(qi, 1, prefix, rest);
|
|
556
|
+
qi--;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (currentX > segs[si].x0) {
|
|
561
|
+
if (word.isWordLike === false && word.isWhitespace) continue;
|
|
562
|
+
if (si < segs.length - 1) {
|
|
563
|
+
si++;
|
|
564
|
+
currentX = segs[si].x0;
|
|
565
|
+
} else {
|
|
566
|
+
commitLine(justifyTarget);
|
|
567
|
+
currentY += lineHeight;
|
|
568
|
+
if (!startLine(lineHeight)) break;
|
|
569
|
+
}
|
|
424
570
|
}
|
|
425
571
|
}
|
|
426
572
|
for (const glyph of word.glyphs) {
|
|
@@ -433,7 +579,7 @@ var LayoutEngine = (_class = class {
|
|
|
433
579
|
si++;
|
|
434
580
|
currentX = segs[si].x0;
|
|
435
581
|
} else {
|
|
436
|
-
commitLine();
|
|
582
|
+
commitLine(justifyTarget);
|
|
437
583
|
currentY += lineHeight;
|
|
438
584
|
if (!startLine(lineHeight)) break;
|
|
439
585
|
}
|
|
@@ -549,20 +695,20 @@ var LayoutEngine = (_class = class {
|
|
|
549
695
|
}
|
|
550
696
|
}
|
|
551
697
|
}, _class);
|
|
552
|
-
var LayoutResultBuffer = (_class2 = class _LayoutResultBuffer {constructor() { _class2.prototype.
|
|
698
|
+
var LayoutResultBuffer = (_class2 = class _LayoutResultBuffer {constructor() { _class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this); }
|
|
553
699
|
static __initStatic() {this.CAPACITY = 16384}
|
|
554
700
|
/** X positions of each glyph. */
|
|
555
|
-
|
|
701
|
+
__init9() {this.xs = new Float32Array(_LayoutResultBuffer.CAPACITY)}
|
|
556
702
|
/** Y positions of each glyph. */
|
|
557
|
-
|
|
703
|
+
__init10() {this.ys = new Float32Array(_LayoutResultBuffer.CAPACITY)}
|
|
558
704
|
/** Widths of each glyph. */
|
|
559
|
-
|
|
705
|
+
__init11() {this.ws = new Float32Array(_LayoutResultBuffer.CAPACITY)}
|
|
560
706
|
/** Heights of each glyph. */
|
|
561
|
-
|
|
707
|
+
__init12() {this.hs = new Float32Array(_LayoutResultBuffer.CAPACITY)}
|
|
562
708
|
/** Character for each glyph slot. */
|
|
563
|
-
|
|
709
|
+
__init13() {this.chars = Array.from({ length: _LayoutResultBuffer.CAPACITY })}
|
|
564
710
|
/** Number of valid glyphs written in this buffer. */
|
|
565
|
-
|
|
711
|
+
__init14() {this.count = 0}
|
|
566
712
|
/** Reset the buffer for reuse. Does NOT free memory. */
|
|
567
713
|
reset() {
|
|
568
714
|
this.count = 0;
|
|
@@ -434,6 +434,7 @@ var Entity = (_class4 = class {
|
|
|
434
434
|
startTime: -1,
|
|
435
435
|
startProps: {}
|
|
436
436
|
});
|
|
437
|
+
_optionalChain([this, 'access', _40 => _40.scene, 'optionalAccess', _41 => _41.markDirty, 'call', _42 => _42()]);
|
|
437
438
|
return this;
|
|
438
439
|
}
|
|
439
440
|
/** Write a driver-computed value to a backing field without re-triggering the setter. */
|
|
@@ -489,10 +490,10 @@ var Entity = (_class4 = class {
|
|
|
489
490
|
const active = driver;
|
|
490
491
|
const onDone = active.onDone;
|
|
491
492
|
active.onDone = void 0;
|
|
492
|
-
_optionalChain([onDone, 'optionalCall',
|
|
493
|
+
_optionalChain([onDone, 'optionalCall', _43 => _43()]);
|
|
493
494
|
}
|
|
494
495
|
_spawnDriver(prop, to, cfg) {
|
|
495
|
-
if (prop !== "opacity" && _optionalChain([this, 'access',
|
|
496
|
+
if (prop !== "opacity" && _optionalChain([this, 'access', _44 => _44.scene, 'optionalAccess', _45 => _45.prefersReducedMotion])) {
|
|
496
497
|
const existing2 = this._drivers.get(prop);
|
|
497
498
|
if (existing2) this._settleDriver(existing2);
|
|
498
499
|
this._drivers.delete(prop);
|
|
@@ -508,11 +509,11 @@ var Entity = (_class4 = class {
|
|
|
508
509
|
const from = this._currentOf(prop);
|
|
509
510
|
const driver = isTweenConfig(cfg) ? new TweenDriver(from, to, cfg) : new SpringDriver(from, to, cfg === "spring" ? {} : cfg);
|
|
510
511
|
this._drivers.set(prop, driver);
|
|
511
|
-
_optionalChain([this, 'access',
|
|
512
|
+
_optionalChain([this, 'access', _46 => _46.scene, 'optionalAccess', _47 => _47.markDirty, 'call', _48 => _48()]);
|
|
512
513
|
}
|
|
513
514
|
/** Assignment path when a declarative transition is configured for `prop`. */
|
|
514
515
|
_animateProp(prop, to) {
|
|
515
|
-
const cfg = _optionalChain([this, 'access',
|
|
516
|
+
const cfg = _optionalChain([this, 'access', _49 => _49._transitions, 'optionalAccess', _50 => _50.get, 'call', _51 => _51(prop)]);
|
|
516
517
|
if (!cfg) {
|
|
517
518
|
this._applyAnimated(prop, to);
|
|
518
519
|
return;
|
|
@@ -562,7 +563,7 @@ var Entity = (_class4 = class {
|
|
|
562
563
|
this._applyAnimated(prop, driver.value);
|
|
563
564
|
}
|
|
564
565
|
}
|
|
565
|
-
_optionalChain([this, 'access',
|
|
566
|
+
_optionalChain([this, 'access', _52 => _52.scene, 'optionalAccess', _53 => _53.markDirty, 'call', _54 => _54()]);
|
|
566
567
|
}
|
|
567
568
|
/**
|
|
568
569
|
* Advance the entity's internal state for one frame.
|
|
@@ -616,7 +617,7 @@ var Entity = (_class4 = class {
|
|
|
616
617
|
* @example entity.on('click', (e) => console.log('clicked', e));
|
|
617
618
|
*/
|
|
618
619
|
on(event, callback, options) {
|
|
619
|
-
const map = _optionalChain([options, 'optionalAccess',
|
|
620
|
+
const map = _optionalChain([options, 'optionalAccess', _55 => _55.capture]) ? this.captureListeners : this.listeners;
|
|
620
621
|
if (!map.has(event)) {
|
|
621
622
|
map.set(event, []);
|
|
622
623
|
}
|
|
@@ -632,7 +633,7 @@ var Entity = (_class4 = class {
|
|
|
632
633
|
* @returns `this` for method chaining.
|
|
633
634
|
*/
|
|
634
635
|
off(event, callback, options) {
|
|
635
|
-
const handlers = (_optionalChain([options, 'optionalAccess',
|
|
636
|
+
const handlers = (_optionalChain([options, 'optionalAccess', _56 => _56.capture]) ? this.captureListeners : this.listeners).get(event);
|
|
636
637
|
if (handlers) {
|
|
637
638
|
const idx = handlers.indexOf(callback);
|
|
638
639
|
if (idx !== -1) handlers.splice(idx, 1);
|
|
@@ -892,6 +893,19 @@ var Entity = (_class4 = class {
|
|
|
892
893
|
getBatchRect() {
|
|
893
894
|
return null;
|
|
894
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Opt into DOM content projection for entities that render static text.
|
|
898
|
+
* The {@link Scene} mirrors the returned text as a transparent DOM node
|
|
899
|
+
* positioned over the drawn glyphs, making canvas text findable (Ctrl+F),
|
|
900
|
+
* readable by screen readers and crawlers, translatable, and — when
|
|
901
|
+
* `selectable` is set — natively selectable. Returns `null` by default.
|
|
902
|
+
* Read on the a11y sync cadence, so text changes propagate automatically.
|
|
903
|
+
*
|
|
904
|
+
* @returns The projection descriptor, or `null` to project nothing.
|
|
905
|
+
*/
|
|
906
|
+
getContentProjection() {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
895
909
|
/**
|
|
896
910
|
* Whether this entity still has a queued/running tween animation, or an
|
|
897
911
|
* active {@link setTransition}/{@link animateTo}/{@link springTo} property
|
|
@@ -1063,10 +1077,22 @@ var MSDFTextEntity = (_class6 = class extends Entity {
|
|
|
1063
1077
|
if (res.seqId < this.lastRenderedSeqId) return;
|
|
1064
1078
|
this.lastRenderedSeqId = res.seqId;
|
|
1065
1079
|
this.layoutResult = res;
|
|
1066
|
-
_optionalChain([this, 'access',
|
|
1080
|
+
_optionalChain([this, 'access', _57 => _57.scene, 'optionalAccess', _58 => _58.markDirty, 'call', _59 => _59()]);
|
|
1067
1081
|
}
|
|
1068
1082
|
});
|
|
1069
1083
|
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
1086
|
+
* readers, crawlers, and translation see the same string the canvas draws.
|
|
1087
|
+
*/
|
|
1088
|
+
getContentProjection() {
|
|
1089
|
+
if (!this.text) return null;
|
|
1090
|
+
return {
|
|
1091
|
+
text: this.text,
|
|
1092
|
+
font: `${this.fontSize}px ${this.fallbackFont}`,
|
|
1093
|
+
lineHeight: this.lineHeight
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1070
1096
|
isPointInside(globalX, globalY) {
|
|
1071
1097
|
if (!this.layoutResult) return false;
|
|
1072
1098
|
const local = this.worldToLocal(globalX, globalY);
|
|
@@ -34,6 +34,10 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
34
34
|
this.height = size?.height ?? (typeof window !== "undefined" ? window.innerHeight : canvas.height || 0);
|
|
35
35
|
canvas.width = this.width * dpr;
|
|
36
36
|
canvas.height = this.height * dpr;
|
|
37
|
+
if (canvas.style) {
|
|
38
|
+
canvas.style.width = `${this.width}px`;
|
|
39
|
+
canvas.style.height = `${this.height}px`;
|
|
40
|
+
}
|
|
37
41
|
const ctx = canvas.getContext("2d");
|
|
38
42
|
this.ctx = ctx;
|
|
39
43
|
if (ctx) ctx.scale(dpr, dpr);
|
|
@@ -821,6 +825,19 @@ void main() {
|
|
|
821
825
|
vec4 t = texture(u_tex, v_uv);
|
|
822
826
|
outColor = vec4(t.rgb * v_tint.rgb, t.a * v_tint.a);
|
|
823
827
|
}`;
|
|
828
|
+
var CIRCLE_QUAD_FRAG = `#version 300 es
|
|
829
|
+
precision mediump float;
|
|
830
|
+
in vec2 v_uv;
|
|
831
|
+
in vec4 v_tint;
|
|
832
|
+
out vec4 outColor;
|
|
833
|
+
void main() {
|
|
834
|
+
vec2 c = v_uv - 0.5;
|
|
835
|
+
float d = length(c);
|
|
836
|
+
float aa = fwidth(d);
|
|
837
|
+
float alpha = 1.0 - smoothstep(0.5 - aa, 0.5, d);
|
|
838
|
+
if (alpha <= 0.0) discard;
|
|
839
|
+
outColor = vec4(v_tint.rgb, v_tint.a * alpha);
|
|
840
|
+
}`;
|
|
824
841
|
var MSDF_FRAG = `#version 300 es
|
|
825
842
|
precision mediump float;
|
|
826
843
|
uniform sampler2D u_tex;
|
|
@@ -887,7 +904,9 @@ function createWebGLPointRenderer(canvas) {
|
|
|
887
904
|
const rectProgram = link(gl, RECT_VERT, RECT_FRAG);
|
|
888
905
|
const spriteProgram = link(gl, SPRITE_VERT, SPRITE_FRAG);
|
|
889
906
|
const msdfProgram = link(gl, SPRITE_VERT, MSDF_FRAG);
|
|
890
|
-
|
|
907
|
+
const circleQuadProgram = link(gl, SPRITE_VERT, CIRCLE_QUAD_FRAG);
|
|
908
|
+
if (!pointProgram || !rectProgram || !spriteProgram || !msdfProgram || !circleQuadProgram)
|
|
909
|
+
return null;
|
|
891
910
|
gl.enable(gl.BLEND);
|
|
892
911
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
893
912
|
const pAPos = gl.getAttribLocation(pointProgram, "a_pos");
|
|
@@ -947,6 +966,20 @@ function createWebGLPointRenderer(canvas) {
|
|
|
947
966
|
gl.vertexAttribPointer(gAUv, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 8);
|
|
948
967
|
gl.enableVertexAttribArray(gATint);
|
|
949
968
|
gl.vertexAttribPointer(gATint, 4, gl.FLOAT, false, SPRITE_VERT_STRIDE, 16);
|
|
969
|
+
const cAPos = gl.getAttribLocation(circleQuadProgram, "a_pos");
|
|
970
|
+
const cAUv = gl.getAttribLocation(circleQuadProgram, "a_uv");
|
|
971
|
+
const cATint = gl.getAttribLocation(circleQuadProgram, "a_tint");
|
|
972
|
+
const cURes = gl.getUniformLocation(circleQuadProgram, "u_resolution");
|
|
973
|
+
const circleQuadBuffer = gl.createBuffer();
|
|
974
|
+
const circleQuadVAO = gl.createVertexArray();
|
|
975
|
+
gl.bindVertexArray(circleQuadVAO);
|
|
976
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, circleQuadBuffer);
|
|
977
|
+
gl.enableVertexAttribArray(cAPos);
|
|
978
|
+
gl.vertexAttribPointer(cAPos, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 0);
|
|
979
|
+
gl.enableVertexAttribArray(cAUv);
|
|
980
|
+
gl.vertexAttribPointer(cAUv, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 8);
|
|
981
|
+
gl.enableVertexAttribArray(cATint);
|
|
982
|
+
gl.vertexAttribPointer(cATint, 4, gl.FLOAT, false, SPRITE_VERT_STRIDE, 16);
|
|
950
983
|
gl.bindVertexArray(null);
|
|
951
984
|
let texture = null;
|
|
952
985
|
let textureSource = null;
|
|
@@ -961,10 +994,16 @@ function createWebGLPointRenderer(canvas) {
|
|
|
961
994
|
let spriteCount = 0;
|
|
962
995
|
let glyphData = new Float32Array(FLOATS_PER_SPRITE_VERT * VERTS_PER_SPRITE * 1024);
|
|
963
996
|
let glyphCount = 0;
|
|
997
|
+
let circleQuadData = new Float32Array(
|
|
998
|
+
FLOATS_PER_SPRITE_VERT * VERTS_PER_SPRITE * 64
|
|
999
|
+
);
|
|
1000
|
+
let circleQuadCount = 0;
|
|
964
1001
|
let logicalW = 0;
|
|
965
1002
|
let logicalH = 0;
|
|
966
1003
|
let dpr = 1;
|
|
967
1004
|
let destroyed = false;
|
|
1005
|
+
const pointSizeRange = typeof gl.getParameter === "function" ? gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE) : null;
|
|
1006
|
+
const maxPointSize = pointSizeRange ? pointSizeRange[1] : Infinity;
|
|
968
1007
|
const drawGlyphs = () => {
|
|
969
1008
|
if (glyphCount === 0 || !msdfTexture) return;
|
|
970
1009
|
const floats = glyphCount * VERTS_PER_SPRITE * FLOATS_PER_SPRITE_VERT;
|
|
@@ -997,6 +1036,7 @@ function createWebGLPointRenderer(canvas) {
|
|
|
997
1036
|
rectCount = 0;
|
|
998
1037
|
spriteCount = 0;
|
|
999
1038
|
glyphCount = 0;
|
|
1039
|
+
circleQuadCount = 0;
|
|
1000
1040
|
gl.clearColor(0, 0, 0, 0);
|
|
1001
1041
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1002
1042
|
},
|
|
@@ -1104,6 +1144,35 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1104
1144
|
glyphCount++;
|
|
1105
1145
|
},
|
|
1106
1146
|
addCircle(x, y, radius, color, alpha = 1) {
|
|
1147
|
+
const needsQuad = logicalW > 0 && (x < radius || y < radius || x > logicalW - radius || y > logicalH - radius) || radius * 2 * dpr > maxPointSize;
|
|
1148
|
+
if (needsQuad) {
|
|
1149
|
+
const stride = FLOATS_PER_SPRITE_VERT * VERTS_PER_SPRITE;
|
|
1150
|
+
circleQuadData = grow(circleQuadData, (circleQuadCount + 1) * stride);
|
|
1151
|
+
const [qr, qg, qb, qa] = parseColorToRGBA(color);
|
|
1152
|
+
const al = qa * alpha;
|
|
1153
|
+
const quad = [
|
|
1154
|
+
[x - radius, y - radius, 0, 0],
|
|
1155
|
+
[x + radius, y - radius, 1, 0],
|
|
1156
|
+
[x + radius, y + radius, 1, 1],
|
|
1157
|
+
[x - radius, y + radius, 0, 1]
|
|
1158
|
+
];
|
|
1159
|
+
const order = [0, 1, 2, 0, 2, 3];
|
|
1160
|
+
let o2 = circleQuadCount * stride;
|
|
1161
|
+
for (const i of order) {
|
|
1162
|
+
const [vx, vy, vu, vv] = quad[i];
|
|
1163
|
+
circleQuadData[o2] = vx;
|
|
1164
|
+
circleQuadData[o2 + 1] = vy;
|
|
1165
|
+
circleQuadData[o2 + 2] = vu;
|
|
1166
|
+
circleQuadData[o2 + 3] = vv;
|
|
1167
|
+
circleQuadData[o2 + 4] = qr;
|
|
1168
|
+
circleQuadData[o2 + 5] = qg;
|
|
1169
|
+
circleQuadData[o2 + 6] = qb;
|
|
1170
|
+
circleQuadData[o2 + 7] = al;
|
|
1171
|
+
o2 += FLOATS_PER_SPRITE_VERT;
|
|
1172
|
+
}
|
|
1173
|
+
circleQuadCount++;
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1107
1176
|
pointData = grow(pointData, (pointCount + 1) * FLOATS_PER_POINT);
|
|
1108
1177
|
const [r, g, b, a] = parseColorToRGBA(color);
|
|
1109
1178
|
const o = pointCount * FLOATS_PER_POINT;
|
|
@@ -1167,6 +1236,15 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1167
1236
|
gl.uniform1f(pUDpr, dpr);
|
|
1168
1237
|
gl.drawArrays(gl.POINTS, 0, pointCount);
|
|
1169
1238
|
}
|
|
1239
|
+
if (circleQuadCount > 0) {
|
|
1240
|
+
const floats = circleQuadCount * VERTS_PER_SPRITE * FLOATS_PER_SPRITE_VERT;
|
|
1241
|
+
gl.useProgram(circleQuadProgram);
|
|
1242
|
+
gl.bindVertexArray(circleQuadVAO);
|
|
1243
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, circleQuadBuffer);
|
|
1244
|
+
gl.bufferData(gl.ARRAY_BUFFER, circleQuadData.subarray(0, floats), gl.DYNAMIC_DRAW);
|
|
1245
|
+
gl.uniform2f(cURes, logicalW, logicalH);
|
|
1246
|
+
gl.drawArrays(gl.TRIANGLES, 0, circleQuadCount * VERTS_PER_SPRITE);
|
|
1247
|
+
}
|
|
1170
1248
|
if (spriteCount > 0 && texture) {
|
|
1171
1249
|
const floats = spriteCount * VERTS_PER_SPRITE * FLOATS_PER_SPRITE_VERT;
|
|
1172
1250
|
gl.useProgram(spriteProgram);
|
|
@@ -1189,14 +1267,17 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1189
1267
|
gl.deleteBuffer(rectBuffer);
|
|
1190
1268
|
gl.deleteBuffer(spriteBuffer);
|
|
1191
1269
|
gl.deleteBuffer(glyphBuffer);
|
|
1270
|
+
gl.deleteBuffer(circleQuadBuffer);
|
|
1192
1271
|
gl.deleteVertexArray(pointVAO);
|
|
1193
1272
|
gl.deleteVertexArray(rectVAO);
|
|
1194
1273
|
gl.deleteVertexArray(spriteVAO);
|
|
1195
1274
|
gl.deleteVertexArray(glyphVAO);
|
|
1275
|
+
gl.deleteVertexArray(circleQuadVAO);
|
|
1196
1276
|
gl.deleteProgram(pointProgram);
|
|
1197
1277
|
gl.deleteProgram(rectProgram);
|
|
1198
1278
|
gl.deleteProgram(spriteProgram);
|
|
1199
1279
|
gl.deleteProgram(msdfProgram);
|
|
1280
|
+
gl.deleteProgram(circleQuadProgram);
|
|
1200
1281
|
if (texture) gl.deleteTexture(texture);
|
|
1201
1282
|
if (msdfTexture) gl.deleteTexture(msdfTexture);
|
|
1202
1283
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Entity } from '../tree/Entity';
|
|
1
|
+
import { Entity, type ContentProjection } from '../tree/Entity';
|
|
2
2
|
import { IRenderer } from '../renderer/IRenderer';
|
|
3
3
|
export declare class TextEntity extends Entity {
|
|
4
4
|
text: string;
|
|
@@ -13,6 +13,11 @@ export declare class TextEntity extends Entity {
|
|
|
13
13
|
lineWidth: number;
|
|
14
14
|
private isHovered;
|
|
15
15
|
constructor(text: string, atlas: any, maxWidth: number, fontSize?: number);
|
|
16
|
+
/**
|
|
17
|
+
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
18
|
+
* readers, crawlers, and translation see the same string the canvas draws.
|
|
19
|
+
*/
|
|
20
|
+
getContentProjection(): ContentProjection | null;
|
|
16
21
|
/**
|
|
17
22
|
* Replace the text content. Runs the **cold** measurement pass (re-segment +
|
|
18
23
|
* re-measure) since the glyphs changed, then re-lays out.
|
|
@@ -28,6 +33,17 @@ export declare class TextEntity extends Entity {
|
|
|
28
33
|
* @returns `this` for chaining.
|
|
29
34
|
*/
|
|
30
35
|
setMaxWidth(maxWidth: number): this;
|
|
36
|
+
/**
|
|
37
|
+
* Set horizontal alignment (`'justify'` stretches wrapped lines flush to
|
|
38
|
+
* the wrap width; the last line stays ragged) and reflow.
|
|
39
|
+
*/
|
|
40
|
+
setTextAlign(align: 'left' | 'justify'): this;
|
|
41
|
+
/**
|
|
42
|
+
* Plug a hyphenator (word → parts). Break opportunities are baked in during
|
|
43
|
+
* the cold pass, so this re-prepares the current text. Soft hyphens
|
|
44
|
+
* (U+00AD) in the text work without one.
|
|
45
|
+
*/
|
|
46
|
+
setHyphenator(fn: ((word: string) => string[]) | null): this;
|
|
31
47
|
/** Hot pass: place the cached {@link PreparedText} and refresh the a11y box. */
|
|
32
48
|
private applyLayout;
|
|
33
49
|
isPointInside(globalX: number, globalY: number): boolean;
|