@simpludify/web 0.1.4 → 0.1.5

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.4 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.5 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
- Registry installation (check availability with `npm view @simpludify/web@0.1.4 version`):
5
+ Registry installation (check availability with `npm view @simpludify/web@0.1.5 version`):
6
6
 
7
7
  ```sh
8
- npm install @simpludify/web@0.1.4
8
+ npm install @simpludify/web@0.1.5
9
9
  npm install --save-dev vite typescript
10
10
  ```
11
11
 
package/dist/game.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CanvasNode } from "@simpludify/core";
2
- import { type ActionBindings, ActionController, type ActionSet, type Actor, Aspect, Camera, CanvasUI, type CanvasUIOptions, Engine, type FixedStepOptions, type FrameDriver, type JsonValue, type LogicalResolution, type RendererPort, type ReplayControllers, ReplayPlayer, Scene, ScenePointer, type ScreenPoint, type TweenScheduler, type Viewport, type ViewTransform, type WorldPoint } from "@simpludify/core";
2
+ import { type ActionBindings, ActionController, type ActionSet, type Actor, Aspect, Camera, CanvasUI, type CanvasUIOptions, Engine, type FixedStepOptions, type FrameDriver, type JsonValue, type LogicalResolution, type RendererPort, type ReplayControllers, ReplayPlayer, Scene, ScenePointer, type ScreenPoint, type TweenScheduler, type Viewport, type ViewTransform, VirtualInput, type WorldPoint } from "@simpludify/core";
3
3
  import { GamepadInput } from "./gamepads.js";
4
4
  import { Theme, type ThemeTokens, type UINode } from "./ui.js";
5
5
  export interface SceneContext {
@@ -11,6 +11,7 @@ export interface SceneContext {
11
11
  ui(...nodes: UINode[]): void;
12
12
  canvasUI(root: CanvasNode, options?: CanvasUIOptions): CanvasUI;
13
13
  readonly pointer: ScenePointer;
14
+ readonly virtualInput: VirtualInput;
14
15
  actions<A extends ActionSet>(actions: A, bindings?: ActionBindings<A>): ActionController<A>;
15
16
  onUpdate(callback: (seconds: number) => void): () => void;
16
17
  onActivate(callback: () => void): () => void;
@@ -43,6 +44,11 @@ export declare class Game<SceneId extends string = string> {
43
44
  private readonly keyboard;
44
45
  private readonly pointers;
45
46
  private readonly pointerChannels;
47
+ private readonly virtualInputs;
48
+ private virtualPresentation;
49
+ get virtualInput(): VirtualInput;
50
+ private virtualFor;
51
+ private prepareVirtual;
46
52
  private canvas;
47
53
  private canvasPresentation;
48
54
  private defaultCanvas;
@@ -58,6 +64,7 @@ export declare class Game<SceneId extends string = string> {
58
64
  deliveries: number;
59
65
  scrollDrags: number;
60
66
  controlDrags: number;
67
+ virtualDrags: number;
61
68
  };
62
69
  private channelFor;
63
70
  private readonly observer;
package/dist/index.d.ts CHANGED
@@ -12,3 +12,4 @@ export { GamepadInput, GamepadSource } from "./gamepads.js";
12
12
  export { BrowserInput } from "./input.js";
13
13
  export { PixiRenderer } from "./renderer.js";
14
14
  export * from "./ui.js";
15
+ export type { VirtualInputPresentation } from "./virtual-input-visual.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./style.css";
2
- import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, EventBus, FixedStepRunner, GeometryTilemapLayer, Lifetime, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, cleanupAll, containsScreenPoint, createViewTransform, defineEvent, imageSize, intersectUI, isKeyCode, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
2
+ import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, EventBus, FixedStepRunner, GeometryTilemapLayer, Lifetime, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, VirtualInput, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, cleanupAll, containsScreenPoint, createViewTransform, defineEvent, imageSize, intersectUI, isKeyCode, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
3
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
@@ -589,6 +589,9 @@ var BrowserPointer = class {
589
589
  window.addEventListener("blur", (event) => {
590
590
  if (event.target === window) this.release();
591
591
  }, options);
592
+ host.addEventListener("focusout", (event) => this.guard(() => {
593
+ if (!(event.relatedTarget instanceof Node) || !host.contains(event.relatedTarget) || blocksGameKeyboard(event.relatedTarget)) this.routing.releaseVirtual();
594
+ }), options);
592
595
  document.addEventListener("visibilitychange", () => {
593
596
  if (document.hidden) this.release();
594
597
  }, options);
@@ -1907,6 +1910,108 @@ var TilemapVisual = class {
1907
1910
  ], "Tilemap presentation cleanup failed");
1908
1911
  }
1909
1912
  };
1913
+ // packages/web/src/virtual-input-visual.ts
1914
+ /** Screen-space presentation only. The existing BrowserPointer is the sole event adapter. */
1915
+ var VirtualInputVisual = class {
1916
+ input;
1917
+ release;
1918
+ display = new Container();
1919
+ entries = /* @__PURE__ */ new Map();
1920
+ dead = false;
1921
+ off;
1922
+ constructor(input, release) {
1923
+ this.input = input;
1924
+ this.release = release;
1925
+ this.display.eventMode = "none";
1926
+ this.display.sortableChildren = true;
1927
+ this.display.zIndex = .5;
1928
+ this.off = input.onDispose(() => this.dispose());
1929
+ }
1930
+ get diagnostics() {
1931
+ return {
1932
+ controls: this.entries.size,
1933
+ visible: [...this.entries.values()].filter((e) => e.graphics.visible).length
1934
+ };
1935
+ }
1936
+ update() {
1937
+ if (this.dead) return;
1938
+ const live = new Set(this.input.controls);
1939
+ for (const [control, entry] of this.entries) if (!live.has(control)) {
1940
+ this.entries.delete(control);
1941
+ this.release(entry.graphics);
1942
+ }
1943
+ this.display.visible = this.input.visible;
1944
+ for (const control of live) {
1945
+ let entry = this.entries.get(control);
1946
+ if (!entry) {
1947
+ entry = {
1948
+ graphics: new Graphics(),
1949
+ signature: ""
1950
+ };
1951
+ entry.graphics.zIndex = control.kind === "button" ? 1 : 0;
1952
+ this.entries.set(control, entry);
1953
+ this.display.addChild(entry.graphics);
1954
+ }
1955
+ entry.graphics.visible = control.visible;
1956
+ if (!control.visible || !this.input.visible) continue;
1957
+ const center = control.center, knob = control.knob, radius = control.radius;
1958
+ const signature = JSON.stringify([
1959
+ center,
1960
+ knob,
1961
+ radius,
1962
+ control.color,
1963
+ control.enabled,
1964
+ this.input.enabled,
1965
+ control.pressed,
1966
+ control.kind
1967
+ ]);
1968
+ if (signature === entry.signature) continue;
1969
+ entry.signature = signature;
1970
+ const g = entry.graphics;
1971
+ g.clear();
1972
+ g.alpha = control.enabled && this.input.enabled ? 1 : .35;
1973
+ g.circle(center.x, center.y, radius).fill({
1974
+ color: control.color,
1975
+ alpha: .12
1976
+ }).stroke({
1977
+ color: control.color,
1978
+ alpha: .75,
1979
+ width: 2
1980
+ });
1981
+ if (control.kind === "stick") {
1982
+ g.moveTo(center.x, center.y).lineTo(knob.x, knob.y).stroke({
1983
+ color: control.color,
1984
+ alpha: .5,
1985
+ width: 3
1986
+ });
1987
+ g.circle(knob.x, knob.y, radius * .36).fill({
1988
+ color: control.color,
1989
+ alpha: control.pressed ? .85 : .45
1990
+ });
1991
+ } else {
1992
+ if (control.pressed) g.circle(center.x, center.y, radius * .7).fill({
1993
+ color: control.color,
1994
+ alpha: .65
1995
+ });
1996
+ if (control.directional && control.pressed) g.moveTo(center.x, center.y).lineTo(knob.x, knob.y).stroke({
1997
+ color: control.color,
1998
+ width: 4
1999
+ });
2000
+ }
2001
+ }
2002
+ }
2003
+ dispose() {
2004
+ if (this.dead) return;
2005
+ this.dead = true;
2006
+ const entries = [...this.entries.values()];
2007
+ this.entries.clear();
2008
+ cleanupAll([
2009
+ this.off,
2010
+ ...entries.map((e) => () => this.release(e.graphics)),
2011
+ () => this.display.destroy({ children: true })
2012
+ ], "Virtual input presentation cleanup failed");
2013
+ }
2014
+ };
1910
2015
  // packages/web/src/renderer.ts
1911
2016
  var PixiRenderer = class {
1912
2017
  host;
@@ -1915,6 +2020,46 @@ var PixiRenderer = class {
1915
2020
  app = new Application();
1916
2021
  pendingUITextKeys = /* @__PURE__ */ new Set();
1917
2022
  canvasPresentations = /* @__PURE__ */ new Set();
2023
+ virtualPresentations = /* @__PURE__ */ new Set();
2024
+ get virtualInputDiagnostics() {
2025
+ let controls = 0, visible = 0;
2026
+ for (const visual of this.virtualPresentations) {
2027
+ controls += visual.diagnostics.controls;
2028
+ visible += visual.diagnostics.visible;
2029
+ }
2030
+ return {
2031
+ presentations: this.virtualPresentations.size,
2032
+ controls,
2033
+ visible
2034
+ };
2035
+ }
2036
+ prepareVirtualInput(input) {
2037
+ if (!this.ready || this.dead) throw new EngineError("RENDERER_NOT_READY", "Renderer is not available");
2038
+ const visual = new VirtualInputVisual(input, (graphics) => {
2039
+ const context = graphics.context.uid;
2040
+ graphics.destroy({ context: true });
2041
+ releaseUIGraphics(this.app.renderer, graphics, context);
2042
+ });
2043
+ this.virtualPresentations.add(visual);
2044
+ const off = input.onDispose(() => {
2045
+ this.virtualPresentations.delete(visual);
2046
+ });
2047
+ let disposed = false;
2048
+ return {
2049
+ update: () => {
2050
+ if (disposed || input.disposed) return;
2051
+ if (!visual.display.parent) this.app.stage.addChild(visual.display);
2052
+ visual.update();
2053
+ },
2054
+ dispose: () => {
2055
+ if (disposed) return;
2056
+ disposed = true;
2057
+ off();
2058
+ this.virtualPresentations.delete(visual);
2059
+ visual.dispose();
2060
+ }
2061
+ };
2062
+ }
1918
2063
  get canvasDiagnostics() {
1919
2064
  let nodes = 0, visibleNodes = 0, buttons = 0, syncs = 0, textMetrics = 0;
1920
2065
  for (const p of this.canvasPresentations) {
@@ -2508,6 +2653,10 @@ var PixiRenderer = class {
2508
2653
  this.scenes.clear();
2509
2654
  cleanupAll([
2510
2655
  ...scenes.map((scene) => scene.dispose),
2656
+ ...[...this.virtualPresentations].map((p) => () => {
2657
+ this.virtualPresentations.delete(p);
2658
+ p.dispose();
2659
+ }),
2511
2660
  ...[...this.canvasPresentations].map((p) => () => {
2512
2661
  this.canvasPresentations.delete(p);
2513
2662
  p.dispose();
@@ -2765,6 +2914,37 @@ var Game = class {
2765
2914
  keyboard;
2766
2915
  pointers;
2767
2916
  pointerChannels = /* @__PURE__ */ new WeakMap();
2917
+ virtualInputs = /* @__PURE__ */ new WeakMap();
2918
+ virtualPresentation;
2919
+ get virtualInput() {
2920
+ this.assertAlive();
2921
+ return this.virtualFor(this.engine.scene);
2922
+ }
2923
+ virtualFor(scene) {
2924
+ let input = this.virtualInputs.get(scene);
2925
+ if (!input) {
2926
+ input = new VirtualInput(this.channelFor(scene), this.engine.input);
2927
+ this.virtualInputs.set(scene, input);
2928
+ const owned = input;
2929
+ input.onDispose(() => {
2930
+ if (this.virtualInputs.get(scene) === owned) this.virtualInputs.delete(scene);
2931
+ if (this.engine.scene === scene) {
2932
+ this.virtualPresentation?.dispose();
2933
+ this.virtualPresentation = void 0;
2934
+ }
2935
+ });
2936
+ }
2937
+ return input;
2938
+ }
2939
+ prepareVirtual(scene) {
2940
+ const input = this.virtualInputs.get(scene);
2941
+ if (!input || input.controls.length === 0) return;
2942
+ const renderer = this.renderer;
2943
+ if (!renderer.prepareVirtualInput) throw new EngineError("RENDERER_CAPABILITY", "Renderer does not support virtual input presentation");
2944
+ const presentation = renderer.prepareVirtualInput(input);
2945
+ input.onDispose(() => presentation.dispose());
2946
+ return presentation;
2947
+ }
2768
2948
  canvas;
2769
2949
  canvasPresentation;
2770
2950
  defaultCanvas;
@@ -2885,7 +3065,7 @@ var Game = class {
2885
3065
  world: this.screenToWorld(screen),
2886
3066
  view: this.view
2887
3067
  };
2888
- }, this.report, () => this.canvasPresentation?.update(this.width, this.height));
3068
+ }, this.report, () => this.updateProjection());
2889
3069
  document.addEventListener("visibilitychange", () => {
2890
3070
  this.previousTime = void 0;
2891
3071
  this.fixed?.reset();
@@ -3040,6 +3220,7 @@ var Game = class {
3040
3220
  const scene = new Scene(id), nodes = [], controller = new AbortController();
3041
3221
  let canvas;
3042
3222
  const pointer = this.channelFor(scene);
3223
+ const virtualFor = () => this.virtualFor(scene);
3043
3224
  this.staging = scene;
3044
3225
  scene.onDispose(() => controller.abort());
3045
3226
  try {
@@ -3055,6 +3236,9 @@ var Game = class {
3055
3236
  add: (actor) => scene.add(actor),
3056
3237
  ui: (...items) => nodes.push(...items),
3057
3238
  pointer,
3239
+ get virtualInput() {
3240
+ return virtualFor();
3241
+ },
3058
3242
  canvasUI: (root, options) => {
3059
3243
  if (canvas) throw new EngineError("UI_OWNERSHIP", "A Scene has one Canvas UI root");
3060
3244
  canvas = new CanvasUI(scene, root, options);
@@ -3093,6 +3277,7 @@ var Game = class {
3093
3277
  async present(scene, nodes, canvas) {
3094
3278
  let prepared, ui, committed = false;
3095
3279
  let canvasPresentation;
3280
+ let virtualPresentation;
3096
3281
  try {
3097
3282
  const preparing = this.renderer.prepare(scene);
3098
3283
  preparing.then((value) => {
@@ -3113,6 +3298,7 @@ var Game = class {
3113
3298
  canvasPresentation = renderer.prepareCanvasUI(canvas, this.pointers.routing);
3114
3299
  scene.onDispose(() => canvasPresentation?.dispose());
3115
3300
  }
3301
+ virtualPresentation = this.prepareVirtual(scene);
3116
3302
  scene.markReady();
3117
3303
  this.errorBox.hidden = true;
3118
3304
  try {
@@ -3123,18 +3309,20 @@ var Game = class {
3123
3309
  this.report(error);
3124
3310
  }
3125
3311
  this.assertAlive();
3126
- const oldPrepared = this.prepared, oldUI = this.ui, oldCanvas = this.canvasPresentation;
3312
+ const oldPrepared = this.prepared, oldUI = this.ui, oldCanvas = this.canvasPresentation, oldVirtual = this.virtualPresentation;
3127
3313
  this.prepared = prepared;
3128
3314
  this.ui = ui;
3129
3315
  this.canvas = canvas;
3130
3316
  this.canvasPresentation = canvasPresentation;
3317
+ this.virtualPresentation = virtualPresentation;
3131
3318
  this.pointers.routing.setScene(this.channelFor(scene), canvas);
3132
3319
  committed = true;
3133
3320
  this.element.append(ui.element);
3134
3321
  for (const cleanup of [
3135
3322
  () => oldPrepared?.dispose(),
3136
3323
  () => oldUI?.dispose(),
3137
- () => oldCanvas?.dispose()
3324
+ () => oldCanvas?.dispose(),
3325
+ () => oldVirtual?.dispose()
3138
3326
  ]) try {
3139
3327
  cleanup();
3140
3328
  } catch (error) {
@@ -3150,7 +3338,8 @@ var Game = class {
3150
3338
  if (!committed) for (const cleanup of [
3151
3339
  () => prepared?.dispose(),
3152
3340
  () => ui?.dispose(),
3153
- () => canvasPresentation?.dispose()
3341
+ () => canvasPresentation?.dispose(),
3342
+ () => virtualPresentation?.dispose()
3154
3343
  ]) try {
3155
3344
  cleanup();
3156
3345
  } catch (cleanupError) {
@@ -3262,6 +3451,7 @@ var Game = class {
3262
3451
  width: this.width,
3263
3452
  height: this.height
3264
3453
  };
3454
+ this.virtualInputs.get(this.engine.scene)?.refresh(viewport);
3265
3455
  const previous = this.projection.viewport;
3266
3456
  const c = this.camera;
3267
3457
  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;
@@ -3306,6 +3496,8 @@ var Game = class {
3306
3496
  }
3307
3497
  render() {
3308
3498
  this.updateProjection();
3499
+ this.virtualPresentation ??= this.prepareVirtual(this.engine.scene);
3500
+ this.virtualPresentation?.update();
3309
3501
  this.prepared?.render(this.projection);
3310
3502
  }
3311
3503
  tick = (time) => {
@@ -3319,12 +3511,14 @@ var Game = class {
3319
3511
  } else {
3320
3512
  const elapsed = this.previousTime === void 0 ? 0 : Math.max(0, (time - this.previousTime) / 1e3);
3321
3513
  this.previousTime = time;
3514
+ if (this.virtualInputs.has(this.engine.scene)) this.updateProjection();
3322
3515
  if (this.playback) this.playback.advance(elapsed, this.options.fixedStep?.maxSteps ?? 8);
3323
3516
  else if (this.fixed) this.fixed.advance(elapsed);
3324
3517
  else this.engine.step(Math.min(this.maxDelta, elapsed));
3325
3518
  if (!this.dead && !this.engine.scene.disposed) this.engine.scene.advancePresentation(Math.min(this.maxDelta, elapsed));
3326
3519
  if (this.engine.paused) this.canvas?.stopMotion();
3327
3520
  else this.canvas?.advance(elapsed);
3521
+ this.virtualInputs.get(this.engine.scene)?.advance(Math.min(this.maxDelta, elapsed));
3328
3522
  if (this.ratio !== (this.options.pixelRatio ?? Math.min(window.devicePixelRatio || 1, 2))) this.resize();
3329
3523
  this.render();
3330
3524
  }
@@ -3354,6 +3548,7 @@ var Game = class {
3354
3548
  () => this.gamepads.dispose(),
3355
3549
  () => this.keyboard.dispose(),
3356
3550
  () => this.canvasPresentation?.dispose(),
3551
+ () => this.virtualPresentation?.dispose(),
3357
3552
  () => this.canvas?.dispose(),
3358
3553
  () => this.observer.disconnect(),
3359
3554
  () => this.ui?.dispose(),
@@ -3371,6 +3566,7 @@ var Game = class {
3371
3566
  this.staging = void 0;
3372
3567
  this.canvas = void 0;
3373
3568
  this.canvasPresentation = void 0;
3569
+ this.virtualPresentation = void 0;
3374
3570
  this.defaultCanvas = void 0;
3375
3571
  this.defaultUI.length = 0;
3376
3572
  this.scenes.clear();