@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplekex/shallot",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -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 aa = 1.0 - smoothstep(input.halfWidth - 0.5, input.halfWidth + 0.5, dist);
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);
@@ -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 = 64;
31
- const SDF_EXPONENT = 9;
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 clearFonts(): void {
49
+ export function resetFonts(): void {
48
50
  fontUrls.length = 0;
49
51
  loadedFonts.length = 0;
50
52
  }
51
53
 
52
- async function loadAllFonts(): Promise<void> {
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 sdfExponent = 9.0;
503
- let isOutside = sdfValue < 0.5;
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
- getCount: () => number;
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: "text",
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.getCount();
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
- count: number;
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
- let glyphCount = 0;
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] = offsetX + glyph.x;
679
- staging[offset + 1] = offsetY + glyph.y;
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
- return;
775
+ font(DEFAULT_FONT);
730
776
  }
731
777
 
732
- await loadAllFonts();
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
- count: 0,
814
+ ranges,
762
815
  };
763
816
 
764
817
  state.setResource(Glyphs, textState);
765
818
 
766
- registerDraw(
767
- state,
768
- createTextDraw({
769
- scene: render.scene,
770
- glyphs: textState.buffer,
771
- atlas: atlases[0].textureView,
772
- sampler,
773
- matrices: render.matrices,
774
- getCount: () => textState.count,
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);