@multiplekex/shallot 0.2.0 → 0.2.2
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/package.json +1 -1
- package/src/extras/lines/index.ts +2 -1
- package/src/extras/text/index.ts +104 -47
- package/src/standard/tween/tween.ts +1 -1
package/package.json
CHANGED
|
@@ -189,7 +189,8 @@ struct FragmentOutput {
|
|
|
189
189
|
@fragment
|
|
190
190
|
fn fs(input: VertexOutput) -> FragmentOutput {
|
|
191
191
|
let dist = abs(input.dist);
|
|
192
|
-
let
|
|
192
|
+
let aaWidth = fwidth(input.dist);
|
|
193
|
+
let aa = 1.0 - smoothstep(input.halfWidth - aaWidth, input.halfWidth + aaWidth, dist);
|
|
193
194
|
var out: FragmentOutput;
|
|
194
195
|
out.color = vec4(input.color.rgb, input.color.a * aa);
|
|
195
196
|
out.mask = select(0.0, 1.0, aa > 0.01);
|
package/src/extras/text/index.ts
CHANGED
|
@@ -27,12 +27,14 @@ import { Transform } from "../../standard/transforms";
|
|
|
27
27
|
|
|
28
28
|
const MAX_GLYPHS = 50000;
|
|
29
29
|
const GLYPH_FLOATS = 16;
|
|
30
|
-
const SDF_SIZE =
|
|
31
|
-
const SDF_EXPONENT =
|
|
32
|
-
|
|
30
|
+
const SDF_SIZE = 96;
|
|
31
|
+
const SDF_EXPONENT = 6;
|
|
33
32
|
const fontUrls: string[] = [];
|
|
34
33
|
const loadedFonts: (Font | null)[] = [];
|
|
35
34
|
|
|
35
|
+
export const DEFAULT_FONT =
|
|
36
|
+
"https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf";
|
|
37
|
+
|
|
36
38
|
export function font(url: string): number {
|
|
37
39
|
const id = fontUrls.length;
|
|
38
40
|
fontUrls.push(url);
|
|
@@ -44,12 +46,12 @@ export function getFont(id: number): Font | null {
|
|
|
44
46
|
return loadedFonts[id] ?? null;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
export function
|
|
49
|
+
export function resetFonts(): void {
|
|
48
50
|
fontUrls.length = 0;
|
|
49
51
|
loadedFonts.length = 0;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
async function
|
|
54
|
+
async function loadFonts(): Promise<void> {
|
|
53
55
|
await Promise.all(
|
|
54
56
|
fontUrls.map(async (url, id) => {
|
|
55
57
|
loadedFonts[id] = await loadFont(url);
|
|
@@ -252,6 +254,7 @@ function createGlyphAtlas(device: GPUDevice, font: Font): GlyphAtlas {
|
|
|
252
254
|
device,
|
|
253
255
|
sdfSize: SDF_SIZE,
|
|
254
256
|
exponent: SDF_EXPONENT,
|
|
257
|
+
curveSubdivisions: 24,
|
|
255
258
|
});
|
|
256
259
|
|
|
257
260
|
return {
|
|
@@ -499,17 +502,8 @@ struct FragmentOutput {
|
|
|
499
502
|
fn fs(input: VertexOutput) -> FragmentOutput {
|
|
500
503
|
let sdfValue = textureSample(atlasTexture, atlasSampler, input.uv).r;
|
|
501
504
|
|
|
502
|
-
let
|
|
503
|
-
let
|
|
504
|
-
let processedAlpha = select(1.0 - sdfValue, sdfValue, isOutside);
|
|
505
|
-
let normalizedDist = 1.0 - pow(2.0 * processedAlpha, 1.0 / sdfExponent);
|
|
506
|
-
|
|
507
|
-
let maxDimension = max(input.glyphDimensions.x, input.glyphDimensions.y);
|
|
508
|
-
let absDist = normalizedDist * maxDimension;
|
|
509
|
-
let signedDist = select(-absDist, absDist, isOutside);
|
|
510
|
-
|
|
511
|
-
let aaDist = length(fwidth(input.localUV * input.glyphDimensions)) * 0.5;
|
|
512
|
-
let alpha = smoothstep(aaDist, -aaDist, signedDist);
|
|
505
|
+
let aaWidth = fwidth(sdfValue) * 0.707;
|
|
506
|
+
let alpha = smoothstep(0.5 - aaWidth, 0.5 + aaWidth, sdfValue);
|
|
513
507
|
|
|
514
508
|
if alpha < 0.01 {
|
|
515
509
|
discard;
|
|
@@ -578,7 +572,8 @@ export interface TextConfig {
|
|
|
578
572
|
atlas: GPUTextureView;
|
|
579
573
|
sampler: GPUSampler;
|
|
580
574
|
matrices: GPUBuffer;
|
|
581
|
-
|
|
575
|
+
fontIndex: number;
|
|
576
|
+
getRange: () => { start: number; count: number };
|
|
582
577
|
}
|
|
583
578
|
|
|
584
579
|
function createTextDraw(config: TextConfig): Draw {
|
|
@@ -586,14 +581,14 @@ function createTextDraw(config: TextConfig): Draw {
|
|
|
586
581
|
let bindGroup: GPUBindGroup | null = null;
|
|
587
582
|
|
|
588
583
|
return {
|
|
589
|
-
id:
|
|
584
|
+
id: `text-${config.fontIndex}`,
|
|
590
585
|
pass: Pass.Overlay,
|
|
591
|
-
order: 2,
|
|
586
|
+
order: 2 + config.fontIndex,
|
|
592
587
|
|
|
593
588
|
execute() {},
|
|
594
589
|
|
|
595
590
|
draw(pass: GPURenderPassEncoder, ctx: SharedPassContext) {
|
|
596
|
-
const count = config.
|
|
591
|
+
const { start, count } = config.getRange();
|
|
597
592
|
if (count === 0) return;
|
|
598
593
|
|
|
599
594
|
if (!pipeline) {
|
|
@@ -615,21 +610,45 @@ function createTextDraw(config: TextConfig): Draw {
|
|
|
615
610
|
|
|
616
611
|
pass.setPipeline(pipeline);
|
|
617
612
|
pass.setBindGroup(0, bindGroup);
|
|
618
|
-
pass.draw(count * 6);
|
|
613
|
+
pass.draw(count * 6, 1, start * 6, 0);
|
|
619
614
|
},
|
|
620
615
|
};
|
|
621
616
|
}
|
|
622
617
|
|
|
618
|
+
export interface FontRange {
|
|
619
|
+
start: number;
|
|
620
|
+
count: number;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
623
|
export interface Glyphs {
|
|
624
624
|
atlases: GlyphAtlas[];
|
|
625
625
|
sampler: GPUSampler;
|
|
626
626
|
buffer: GPUBuffer;
|
|
627
627
|
staging: Float32Array;
|
|
628
|
-
|
|
628
|
+
ranges: FontRange[];
|
|
629
629
|
}
|
|
630
630
|
|
|
631
631
|
export const Glyphs = resource<Glyphs>("glyphs");
|
|
632
632
|
|
|
633
|
+
interface PendingGlyph {
|
|
634
|
+
eid: number;
|
|
635
|
+
fontId: number;
|
|
636
|
+
x: number;
|
|
637
|
+
y: number;
|
|
638
|
+
width: number;
|
|
639
|
+
height: number;
|
|
640
|
+
texelWidth: number;
|
|
641
|
+
texelHeight: number;
|
|
642
|
+
u0: number;
|
|
643
|
+
v0: number;
|
|
644
|
+
u1: number;
|
|
645
|
+
v1: number;
|
|
646
|
+
r: number;
|
|
647
|
+
g: number;
|
|
648
|
+
b: number;
|
|
649
|
+
a: number;
|
|
650
|
+
}
|
|
651
|
+
|
|
633
652
|
const TextSystem: System = {
|
|
634
653
|
group: "draw",
|
|
635
654
|
|
|
@@ -639,10 +658,10 @@ const TextSystem: System = {
|
|
|
639
658
|
if (!compute || !text) return;
|
|
640
659
|
|
|
641
660
|
const { device } = compute;
|
|
642
|
-
const { atlases, staging } = text;
|
|
661
|
+
const { atlases, staging, ranges } = text;
|
|
643
662
|
const stagingU32 = new Uint32Array(staging.buffer);
|
|
644
663
|
|
|
645
|
-
|
|
664
|
+
const glyphsByFont: PendingGlyph[][] = atlases.map(() => []);
|
|
646
665
|
|
|
647
666
|
for (const eid of state.query([Text, Transform])) {
|
|
648
667
|
if (!Text.visible[eid]) continue;
|
|
@@ -652,6 +671,7 @@ const TextSystem: System = {
|
|
|
652
671
|
|
|
653
672
|
const fontId = Text.font[eid];
|
|
654
673
|
const atlas = atlases[fontId] ?? atlases[0];
|
|
674
|
+
const actualFontId = atlases[fontId] ? fontId : 0;
|
|
655
675
|
if (!atlas) continue;
|
|
656
676
|
|
|
657
677
|
ensureString(atlas, content);
|
|
@@ -671,14 +691,42 @@ const TextSystem: System = {
|
|
|
671
691
|
const a = Text.opacity[eid];
|
|
672
692
|
|
|
673
693
|
for (const glyph of layout.glyphs) {
|
|
694
|
+
glyphsByFont[actualFontId].push({
|
|
695
|
+
eid,
|
|
696
|
+
fontId: actualFontId,
|
|
697
|
+
x: offsetX + glyph.x,
|
|
698
|
+
y: offsetY + glyph.y,
|
|
699
|
+
width: glyph.width,
|
|
700
|
+
height: glyph.height,
|
|
701
|
+
texelWidth: glyph.texelWidth,
|
|
702
|
+
texelHeight: glyph.texelHeight,
|
|
703
|
+
u0: glyph.u0,
|
|
704
|
+
v0: glyph.v0,
|
|
705
|
+
u1: glyph.u1,
|
|
706
|
+
v1: glyph.v1,
|
|
707
|
+
r,
|
|
708
|
+
g,
|
|
709
|
+
b,
|
|
710
|
+
a,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let glyphCount = 0;
|
|
716
|
+
for (let fontIdx = 0; fontIdx < atlases.length; fontIdx++) {
|
|
717
|
+
const fontGlyphs = glyphsByFont[fontIdx];
|
|
718
|
+
ranges[fontIdx].start = glyphCount;
|
|
719
|
+
ranges[fontIdx].count = fontGlyphs.length;
|
|
720
|
+
|
|
721
|
+
for (const glyph of fontGlyphs) {
|
|
674
722
|
if (glyphCount >= MAX_GLYPHS) break;
|
|
675
723
|
|
|
676
724
|
const offset = glyphCount * GLYPH_FLOATS;
|
|
677
725
|
|
|
678
|
-
staging[offset + 0] =
|
|
679
|
-
staging[offset + 1] =
|
|
726
|
+
staging[offset + 0] = glyph.x;
|
|
727
|
+
staging[offset + 1] = glyph.y;
|
|
680
728
|
staging[offset + 2] = 0;
|
|
681
|
-
stagingU32[offset + 3] = eid;
|
|
729
|
+
stagingU32[offset + 3] = glyph.eid;
|
|
682
730
|
|
|
683
731
|
staging[offset + 4] = glyph.width;
|
|
684
732
|
staging[offset + 5] = glyph.height;
|
|
@@ -690,17 +738,15 @@ const TextSystem: System = {
|
|
|
690
738
|
staging[offset + 10] = glyph.u1;
|
|
691
739
|
staging[offset + 11] = glyph.v1;
|
|
692
740
|
|
|
693
|
-
staging[offset + 12] = r;
|
|
694
|
-
staging[offset + 13] = g;
|
|
695
|
-
staging[offset + 14] = b;
|
|
696
|
-
staging[offset + 15] = a;
|
|
741
|
+
staging[offset + 12] = glyph.r;
|
|
742
|
+
staging[offset + 13] = glyph.g;
|
|
743
|
+
staging[offset + 14] = glyph.b;
|
|
744
|
+
staging[offset + 15] = glyph.a;
|
|
697
745
|
|
|
698
746
|
glyphCount++;
|
|
699
747
|
}
|
|
700
748
|
}
|
|
701
749
|
|
|
702
|
-
text.count = glyphCount;
|
|
703
|
-
|
|
704
750
|
if (glyphCount > 0) {
|
|
705
751
|
device.queue.writeBuffer(
|
|
706
752
|
text.buffer,
|
|
@@ -726,10 +772,15 @@ export const TextPlugin: Plugin = {
|
|
|
726
772
|
if (!compute || !render) return;
|
|
727
773
|
|
|
728
774
|
if (fontUrls.length === 0) {
|
|
729
|
-
|
|
775
|
+
font(DEFAULT_FONT);
|
|
730
776
|
}
|
|
731
777
|
|
|
732
|
-
|
|
778
|
+
try {
|
|
779
|
+
await loadFonts();
|
|
780
|
+
} catch (e) {
|
|
781
|
+
console.warn("[TextPlugin] Failed to load fonts:", e);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
733
784
|
|
|
734
785
|
const { device } = compute;
|
|
735
786
|
|
|
@@ -749,6 +800,8 @@ export const TextPlugin: Plugin = {
|
|
|
749
800
|
minFilter: "linear",
|
|
750
801
|
});
|
|
751
802
|
|
|
803
|
+
const ranges: FontRange[] = atlases.map(() => ({ start: 0, count: 0 }));
|
|
804
|
+
|
|
752
805
|
const textState: Glyphs = {
|
|
753
806
|
atlases,
|
|
754
807
|
sampler,
|
|
@@ -758,21 +811,25 @@ export const TextPlugin: Plugin = {
|
|
|
758
811
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
759
812
|
}),
|
|
760
813
|
staging: new Float32Array(MAX_GLYPHS * GLYPH_FLOATS),
|
|
761
|
-
|
|
814
|
+
ranges,
|
|
762
815
|
};
|
|
763
816
|
|
|
764
817
|
state.setResource(Glyphs, textState);
|
|
765
818
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
819
|
+
for (let i = 0; i < atlases.length; i++) {
|
|
820
|
+
const fontIndex = i;
|
|
821
|
+
registerDraw(
|
|
822
|
+
state,
|
|
823
|
+
createTextDraw({
|
|
824
|
+
scene: render.scene,
|
|
825
|
+
glyphs: textState.buffer,
|
|
826
|
+
atlas: atlases[i].textureView,
|
|
827
|
+
sampler,
|
|
828
|
+
matrices: render.matrices,
|
|
829
|
+
fontIndex,
|
|
830
|
+
getRange: () => ranges[fontIndex],
|
|
831
|
+
})
|
|
832
|
+
);
|
|
833
|
+
}
|
|
777
834
|
},
|
|
778
835
|
};
|
|
@@ -206,7 +206,7 @@ export function ensureResolved(state: State, tweenEid: number): void {
|
|
|
206
206
|
const elapsed = Tween.elapsed[tweenEid];
|
|
207
207
|
const duration = Tween.duration[tweenEid];
|
|
208
208
|
|
|
209
|
-
if (elapsed >= duration) return;
|
|
209
|
+
if (duration > 0 && elapsed >= duration) return;
|
|
210
210
|
|
|
211
211
|
const targetEid = state.getFirstRelationTarget(tweenEid, TweenTarget);
|
|
212
212
|
const binding = getFieldAccessor(tweenEid);
|