@simpludify/web 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @simpludify/web
2
2
 
3
- Experimental v0.1.0 browser SDK for Simpludify, a code-first TypeScript 2D engine. APIs may change between 0.x releases. Includes Game, PixiJS rendering, browser input and Canvas/DOM UI adapters; re-exports the host-independent Core API. Core and PixiJS are installed as runtime dependencies, not bundled into this package.
3
+ Experimental v0.1.3 browser SDK for Simpludify, a code-first TypeScript 2D engine. APIs may change between 0.x releases. Includes Game, PixiJS rendering, browser input and Canvas/DOM UI adapters; re-exports the host-independent Core API. Core and PixiJS are installed as runtime dependencies, not bundled into this package.
4
4
 
5
- The v0.1.0 distribution is being prepared; registry availability is not yet confirmed. Once published:
5
+ Registry installation (check availability with `npm view @simpludify/web@0.1.3 version`):
6
6
 
7
7
  ```sh
8
- npm install @simpludify/web@0.1.0
8
+ npm install @simpludify/web@0.1.3
9
9
  npm install --save-dev vite typescript
10
10
  ```
11
11
 
@@ -18,6 +18,7 @@ export declare class CanvasVisual implements CanvasPresentation {
18
18
  private dirty;
19
19
  private dead;
20
20
  private active;
21
+ private cacheResolution;
21
22
  private mirroredFocus;
22
23
  private readonly subscriptions;
23
24
  private syncs;
@@ -25,14 +26,17 @@ export declare class CanvasVisual implements CanvasPresentation {
25
26
  graphics(graphics: Graphics, context: number | undefined): void;
26
27
  text(text: Text, key: string | undefined): void;
27
28
  changedText(key: string): void;
29
+ cacheResolution(): number | undefined;
28
30
  });
29
31
  get diagnostics(): {
30
32
  nodes: number;
33
+ visibleNodes: number;
31
34
  buttons: number;
32
35
  syncs: number;
33
36
  textMetrics: number;
34
37
  };
35
38
  private measure;
39
+ private clearCache;
36
40
  beforeResolutionChange(): void;
37
41
  focus(node: CanvasNode): void;
38
42
  private native;
package/dist/game.d.ts CHANGED
@@ -44,6 +44,8 @@ export declare class Game<SceneId extends string = string> {
44
44
  private canvas;
45
45
  private canvasPresentation;
46
46
  private defaultCanvas;
47
+ private readonly ownedCanvas;
48
+ private readonly viewportBindings;
47
49
  get pointer(): ScenePointer;
48
50
  get pointerDiagnostics(): {
49
51
  captures: number;
@@ -119,6 +121,11 @@ export declare class Game<SceneId extends string = string> {
119
121
  private present;
120
122
  get viewport(): Viewport;
121
123
  get view(): ViewTransform;
124
+ /** Bind the mounted UI's resolved region to the world viewport. UI units become CSS pixels.
125
+ * One binding per UI; replacement/unbind restores the manual viewport. UI/node disposal unbinds.
126
+ * A collapsed/hidden region clips all world rendering and pointer input. No extra frame/observer.
127
+ */
128
+ bindViewport(ui: CanvasUI, node: CanvasNode): () => void;
122
129
  setViewport(viewport?: Viewport): void;
123
130
  worldToScreen(point: WorldPoint): ScreenPoint;
124
131
  screenToWorld(point: ScreenPoint): WorldPoint;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./style.css";
2
2
  import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, FixedStepRunner, GeometryTilemapLayer, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, cleanupAll, containsScreenPoint, createViewTransform, imageSize, intersectUI, isKeyCode, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
3
- import { Application, CanvasTextMetrics, Container, Graphics, Rectangle, Sprite, Text, TextStyle, Texture } from "pixi.js";
3
+ import { Application, CanvasTextMetrics, Container, Graphics, Rectangle, RendererType, Sprite, Text, TextStyle, Texture } from "pixi.js";
4
4
  export * from "@simpludify/core";
5
5
  // packages/web/src/canvas-node.ts
6
6
  /** Bridge owner cancellation to browser APIs such as fetch. */
@@ -372,6 +372,11 @@ var CanvasNativeText = class extends NativeControl {
372
372
  this.source = "pointer";
373
373
  this.begin();
374
374
  }), options);
375
+ el.addEventListener("blur", () => {
376
+ this.composing = false;
377
+ this.session = void 0;
378
+ this.source = "keyboard";
379
+ }, options);
375
380
  el.addEventListener("focus", () => this.run(() => this.begin()), options);
376
381
  el.addEventListener("compositionstart", () => this.run(() => {
377
382
  this.composing = true;
@@ -522,6 +527,7 @@ var CanvasVisual = class {
522
527
  dirty = true;
523
528
  dead = false;
524
529
  active = false;
530
+ cacheResolution;
525
531
  mirroredFocus;
526
532
  subscriptions = [];
527
533
  syncs = 0;
@@ -576,6 +582,7 @@ var CanvasVisual = class {
576
582
  get diagnostics() {
577
583
  return {
578
584
  nodes: this.entries.size,
585
+ visibleNodes: [...this.entries.values()].filter((e) => e.display.visible).length,
579
586
  buttons: [...this.entries.values()].filter((e) => e.button).length,
580
587
  syncs: this.syncs,
581
588
  textMetrics: this.metrics.size
@@ -619,7 +626,14 @@ var CanvasVisual = class {
619
626
  cache.values.set(width, result);
620
627
  return result;
621
628
  };
629
+ clearCache() {
630
+ if (this.cacheResolution === void 0) return;
631
+ this.display.cacheAsTexture(false);
632
+ this.cacheResolution = void 0;
633
+ }
622
634
  beforeResolutionChange() {
635
+ this.clearCache();
636
+ this.dirty = true;
623
637
  for (const entry of this.entries.values()) if (entry.text && !entry.text.destroyed) this.resources.changedText(entry.text.styleKey);
624
638
  }
625
639
  focus(node) {
@@ -748,6 +762,7 @@ var CanvasVisual = class {
748
762
  const visited = /* @__PURE__ */ new Set(), buttons = [];
749
763
  const paint = [];
750
764
  let order = 0;
765
+ let scrollVisible = false;
751
766
  let restoreNativeFocus;
752
767
  this.ui.walk((node, parentClip) => {
753
768
  const measuredText = node.kind === "panel" ? void 0 : this.measure(node, Math.max(0, node.bounds.width - node.style.padding * 2));
@@ -772,6 +787,19 @@ var CanvasVisual = class {
772
787
  entry.button = entry.native.element;
773
788
  } else entry.button = this.button(node);
774
789
  this.element.append(entry.button);
790
+ } else if (node.kind === "label") {
791
+ entry.label = document.createElement("span");
792
+ entry.label.className = "s-canvas-label";
793
+ const semantics = node.labelSemantics;
794
+ if (semantics?.role) {
795
+ entry.label.setAttribute("role", semantics.role);
796
+ if (semantics.role === "heading") entry.label.setAttribute("aria-level", String(semantics.level));
797
+ else {
798
+ entry.label.setAttribute("aria-live", semantics.role === "alert" ? "assertive" : "polite");
799
+ entry.label.setAttribute("aria-atomic", "true");
800
+ }
801
+ }
802
+ this.element.append(entry.label);
775
803
  }
776
804
  if (node.kind !== "panel") {
777
805
  entry.text = new Text({ text: node.text });
@@ -797,7 +825,14 @@ var CanvasVisual = class {
797
825
  } : {}
798
826
  });
799
827
  if (entry.button) buttons.push(entry.button);
828
+ if (entry.label) {
829
+ buttons.push(entry.label);
830
+ if (entry.label.textContent !== node.text) entry.label.textContent = node.text;
831
+ entry.label.setAttribute("aria-hidden", String(canvasOpacity(node) === 0));
832
+ }
800
833
  const b = node.bounds, s = node.style;
834
+ const scrollBox = intersectUI(b, clip);
835
+ if (node.style.scroll && scrollBox.width > 0 && scrollBox.height > 0 && canvasOpacity(node) > 0) scrollVisible = true;
801
836
  let enabled = !node.busy;
802
837
  for (let p = node; p; p = p.parent) enabled &&= p.enabled;
803
838
  const signature = JSON.stringify([
@@ -818,9 +853,12 @@ var CanvasVisual = class {
818
853
  if (entry.signature === signature) return;
819
854
  entry.signature = signature;
820
855
  entry.display.position.set(b.x, b.y);
856
+ const paintBox = intersectUI(b, clip);
857
+ const visible = paintBox.width > 0 && paintBox.height > 0 && canvasOpacity(node) > 0;
858
+ entry.display.visible = visible;
821
859
  const clipped = clip.x !== 0 || clip.y !== 0 || clip.width !== width / this.ui.scale || clip.height !== height / this.ui.scale;
822
860
  entry.display.mask = clipped ? entry.mask : null;
823
- entry.mask.renderable = clipped;
861
+ entry.mask.renderable = clipped && visible;
824
862
  const maskSignature = JSON.stringify([
825
863
  clipped,
826
864
  clip.x,
@@ -922,7 +960,8 @@ var CanvasVisual = class {
922
960
  const display = entry.scrollbar;
923
961
  if (!display) return;
924
962
  this.display.setChildIndex(display, order++);
925
- display.visible = bars.length > 0;
963
+ const viewport = intersectUI(node.bounds, clip);
964
+ display.visible = bars.length > 0 && viewport.width > 0 && viewport.height > 0 && canvasOpacity(node) > 0;
926
965
  display.mask = entry.display.mask;
927
966
  const signature = JSON.stringify([
928
967
  node.style.color,
@@ -977,6 +1016,14 @@ var CanvasVisual = class {
977
1016
  const button = focused ? this.entries.get(focused)?.button : void 0;
978
1017
  if ((focused !== this.mirroredFocus || focused === restoreNativeFocus) && button && !button.disabled && document.activeElement !== button) button.focus({ preventScroll: true });
979
1018
  this.mirroredFocus = this.ui.focusedNode;
1019
+ const ratio = this.resources.cacheResolution();
1020
+ const cache = scrollVisible && ratio !== void 0 && Math.max(width, height) * ratio <= 4096 ? ratio * this.ui.scale : void 0;
1021
+ if (cache !== this.cacheResolution) {
1022
+ this.clearCache();
1023
+ this.cacheResolution = cache;
1024
+ if (cache !== void 0) this.display.cacheAsTexture({ resolution: cache });
1025
+ }
1026
+ if (cache !== void 0) this.display.updateCacheTexture();
980
1027
  }
981
1028
  releaseEntry(entry) {
982
1029
  const backgroundContext = entry.background.context?.uid, maskContext = entry.mask.context?.uid;
@@ -986,6 +1033,7 @@ var CanvasVisual = class {
986
1033
  () => entry.abort.abort(),
987
1034
  () => entry.native?.dispose(),
988
1035
  () => entry.button?.remove(),
1036
+ () => entry.label?.remove(),
989
1037
  () => entry.background.destroy({ context: true }),
990
1038
  () => entry.scrollbar?.destroy({ context: true }),
991
1039
  () => entry.text?.destroy({ style: true }),
@@ -1010,6 +1058,7 @@ var CanvasVisual = class {
1010
1058
  cleanupAll([
1011
1059
  ...this.subscriptions.splice(0),
1012
1060
  ...entries.map((e) => () => this.releaseEntry(e)),
1061
+ () => this.clearCache(),
1013
1062
  () => this.display.destroy({ children: true }),
1014
1063
  () => this.element.remove()
1015
1064
  ], "Canvas visual cleanup failed");
@@ -1457,10 +1506,11 @@ var PixiRenderer = class {
1457
1506
  pendingUITextKeys = /* @__PURE__ */ new Set();
1458
1507
  canvasPresentations = /* @__PURE__ */ new Set();
1459
1508
  get canvasDiagnostics() {
1460
- let nodes = 0, buttons = 0, syncs = 0, textMetrics = 0;
1509
+ let nodes = 0, visibleNodes = 0, buttons = 0, syncs = 0, textMetrics = 0;
1461
1510
  for (const p of this.canvasPresentations) {
1462
1511
  const d = p.diagnostics;
1463
1512
  nodes += d.nodes;
1513
+ visibleNodes += d.visibleNodes;
1464
1514
  buttons += d.buttons;
1465
1515
  syncs += d.syncs;
1466
1516
  textMetrics += d.textMetrics;
@@ -1468,6 +1518,7 @@ var PixiRenderer = class {
1468
1518
  return {
1469
1519
  presentations: this.canvasPresentations.size,
1470
1520
  nodes,
1521
+ visibleNodes,
1471
1522
  buttons,
1472
1523
  syncs,
1473
1524
  textMetrics
@@ -1478,7 +1529,8 @@ var PixiRenderer = class {
1478
1529
  const visual = new CanvasVisual(ui, this.host, routing, {
1479
1530
  graphics: (graphics, context) => releaseUIGraphics(this.app.renderer, graphics, context),
1480
1531
  text: (text, key) => releaseUIText(this.app.renderer, text, key),
1481
- changedText: (key) => this.pendingUITextKeys.add(key)
1532
+ changedText: (key) => this.pendingUITextKeys.add(key),
1533
+ cacheResolution: () => this.app.renderer.type === RendererType.WEBGL ? this.app.renderer.resolution : void 0
1482
1534
  });
1483
1535
  this.canvasPresentations.add(visual);
1484
1536
  let disposed = false;
@@ -2297,6 +2349,8 @@ var Game = class {
2297
2349
  canvas;
2298
2350
  canvasPresentation;
2299
2351
  defaultCanvas;
2352
+ ownedCanvas = /* @__PURE__ */ new WeakSet();
2353
+ viewportBindings = /* @__PURE__ */ new WeakMap();
2300
2354
  get pointer() {
2301
2355
  this.assertAlive();
2302
2356
  return this.channelFor(this.engine.scene);
@@ -2498,6 +2552,7 @@ var Game = class {
2498
2552
  this.assertAlive();
2499
2553
  if (this.startPromise || this.defaultCanvas) throw new EngineError("UI_ALREADY_STARTED", "Define one default Canvas UI before run");
2500
2554
  this.defaultCanvas = new CanvasUI(this.engine.scene, root, options);
2555
+ this.ownedCanvas.add(this.defaultCanvas);
2501
2556
  return this.defaultCanvas;
2502
2557
  }
2503
2558
  uiOverlay(...nodes) {
@@ -2581,6 +2636,7 @@ var Game = class {
2581
2636
  canvasUI: (root, options) => {
2582
2637
  if (canvas) throw new EngineError("UI_OWNERSHIP", "A Scene has one Canvas UI root");
2583
2638
  canvas = new CanvasUI(scene, root, options);
2639
+ this.ownedCanvas.add(canvas);
2584
2640
  return canvas;
2585
2641
  },
2586
2642
  actions: (actions, bindings) => new ActionController(actions, {
@@ -2690,6 +2746,31 @@ var Game = class {
2690
2746
  this.updateProjection();
2691
2747
  return this.projection;
2692
2748
  }
2749
+ /** Bind the mounted UI's resolved region to the world viewport. UI units become CSS pixels.
2750
+ * One binding per UI; replacement/unbind restores the manual viewport. UI/node disposal unbinds.
2751
+ * A collapsed/hidden region clips all world rendering and pointer input. No extra frame/observer.
2752
+ */
2753
+ bindViewport(ui, node) {
2754
+ this.assertAlive();
2755
+ if (!this.ownedCanvas.has(ui) || ui.disposed || node.disposed || node.ui !== ui) throw new EngineError("UI_OWNERSHIP", "Viewport region must belong to this Game's Canvas UI");
2756
+ this.viewportBindings.get(ui)?.dispose();
2757
+ let removed = false;
2758
+ const off = [];
2759
+ const binding = {
2760
+ node,
2761
+ dispose: () => {
2762
+ if (removed) return;
2763
+ removed = true;
2764
+ if (this.viewportBindings.get(ui) === binding) this.viewportBindings.delete(ui);
2765
+ for (const unregister of off) unregister();
2766
+ this.projectionDirty = true;
2767
+ }
2768
+ };
2769
+ this.viewportBindings.set(ui, binding);
2770
+ off.push(ui.onDispose(binding.dispose), node.onDispose(binding.dispose));
2771
+ this.projectionDirty = true;
2772
+ return binding.dispose;
2773
+ }
2693
2774
  setViewport(viewport) {
2694
2775
  this.assertAlive();
2695
2776
  const next = viewport ? { ...viewport } : void 0;
@@ -2732,23 +2813,59 @@ var Game = class {
2732
2813
  this.render();
2733
2814
  }
2734
2815
  updateProjection() {
2735
- const v = this.requestedViewport, c = this.camera;
2736
- if (!this.projectionDirty && this.cameraX === c.x && this.cameraY === c.y && this.cameraZoom === c.zoom) return;
2816
+ this.canvasPresentation?.update(this.width, this.height);
2817
+ const ui = this.canvas;
2818
+ const region = ui ? this.viewportBindings.get(ui)?.node : void 0;
2819
+ let v = this.requestedViewport;
2820
+ let empty = false;
2821
+ if (ui && region) {
2822
+ const b = region.bounds, scale = ui.scale;
2823
+ const x = Math.max(0, Math.min(this.width, b.x * scale));
2824
+ const y = Math.max(0, Math.min(this.height, b.y * scale));
2825
+ const right = Math.max(x, Math.min(this.width, (b.x + b.width) * scale));
2826
+ const bottom = Math.max(y, Math.min(this.height, (b.y + b.height) * scale));
2827
+ let visible = region.ui === ui;
2828
+ for (let node = region; node; node = node.parent) if (node.disposed || !node.visible || !node.enabled) visible = false;
2829
+ empty = !visible || right <= x || bottom <= y;
2830
+ v = {
2831
+ x,
2832
+ y,
2833
+ width: empty ? 0 : right - x,
2834
+ height: empty ? 0 : bottom - y
2835
+ };
2836
+ }
2837
+ const viewport = v ?? {
2838
+ x: 0,
2839
+ y: 0,
2840
+ width: this.width,
2841
+ height: this.height
2842
+ };
2843
+ const previous = this.projection.viewport;
2844
+ const c = this.camera;
2845
+ if (!this.projectionDirty && previous.x === viewport.x && previous.y === viewport.y && previous.width === viewport.width && previous.height === viewport.height && this.cameraX === c.x && this.cameraY === c.y && this.cameraZoom === c.zoom) return;
2737
2846
  this.projectionDirty = false;
2738
2847
  this.cameraX = c.x;
2739
2848
  this.cameraY = c.y;
2740
2849
  this.cameraZoom = c.zoom;
2741
2850
  this.projection = createViewTransform({
2742
- viewport: v ?? {
2743
- x: 0,
2744
- y: 0,
2745
- width: this.width,
2746
- height: this.height
2747
- },
2851
+ viewport: empty ? {
2852
+ ...viewport,
2853
+ width: 1,
2854
+ height: 1
2855
+ } : viewport,
2748
2856
  resolution: this.resolution,
2749
2857
  aspect: this.aspect,
2750
2858
  camera: c
2751
2859
  });
2860
+ if (empty) this.projection = {
2861
+ ...this.projection,
2862
+ viewport,
2863
+ clip: {
2864
+ ...viewport,
2865
+ width: 0,
2866
+ height: 0
2867
+ }
2868
+ };
2752
2869
  }
2753
2870
  resize() {
2754
2871
  if (this.dead || !this.rendererReady) return;
@@ -2767,7 +2884,6 @@ var Game = class {
2767
2884
  }
2768
2885
  render() {
2769
2886
  this.updateProjection();
2770
- this.canvasPresentation?.update(this.width, this.height);
2771
2887
  this.prepared?.render(this.projection);
2772
2888
  }
2773
2889
  tick = (time) => {