@viji-dev/core 0.3.23 → 0.3.24

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.
@@ -201,6 +201,27 @@ class ParameterSystem {
201
201
  this.parameterObjects.set(paramName, imageObject);
202
202
  return imageObject;
203
203
  }
204
+ createButtonParameter(config) {
205
+ const paramName = config.label;
206
+ const buttonObject = {
207
+ value: false,
208
+ label: config.label,
209
+ description: config.description ?? "",
210
+ group: config.group ?? "general",
211
+ category: config.category ?? "general"
212
+ };
213
+ const definition = {
214
+ type: "button",
215
+ defaultValue: false,
216
+ label: buttonObject.label,
217
+ description: buttonObject.description,
218
+ group: buttonObject.group,
219
+ category: buttonObject.category
220
+ };
221
+ this.storeParameterDefinition(paramName, definition);
222
+ this.parameterObjects.set(paramName, buttonObject);
223
+ return buttonObject;
224
+ }
204
225
  storeParameterDefinition(name, definition) {
205
226
  this.parameterDefinitions.set(name, definition);
206
227
  this.parameterValues.set(name, definition.defaultValue);
@@ -277,8 +298,14 @@ class ParameterSystem {
277
298
  }
278
299
  break;
279
300
  case "image":
280
- if (value !== null && !(value instanceof ImageBitmap) && !(value instanceof OffscreenCanvas)) {
281
- console.error(`Parameter '${name}' must be null, ImageBitmap, or OffscreenCanvas, got: ${value}`);
301
+ if (value !== null && !(value instanceof ImageBitmap)) {
302
+ console.error(`Parameter '${name}' must be null or ImageBitmap, got: ${value}`);
303
+ return false;
304
+ }
305
+ break;
306
+ case "button":
307
+ if (typeof value !== "boolean") {
308
+ console.error(`Parameter '${name}' must be a boolean, got: ${value}`);
282
309
  return false;
283
310
  }
284
311
  break;
@@ -338,10 +365,29 @@ class ParameterSystem {
338
365
  getAllParameterObjects() {
339
366
  return this.parameterObjects;
340
367
  }
368
+ /**
369
+ * Reset all button parameters to false after each frame.
370
+ * Buttons are momentary: true for exactly 1 frame when pressed, then auto-reset.
371
+ */
372
+ resetButtonParameters() {
373
+ for (const [name, definition] of this.parameterDefinitions) {
374
+ if (definition.type === "button") {
375
+ const paramObj = this.parameterObjects.get(name);
376
+ if (paramObj && paramObj.value === true) {
377
+ paramObj.value = false;
378
+ this.parameterValues.set(name, false);
379
+ }
380
+ }
381
+ }
382
+ }
341
383
  }
342
384
  class InteractionSystem {
343
385
  // Interaction enabled state
344
386
  isEnabled = true;
387
+ // Previous button bitmask for detecting press/release transitions
388
+ previousButtons = 0;
389
+ // Previous touch positions for computing deltas and velocity
390
+ previousTouchPositions = /* @__PURE__ */ new Map();
345
391
  // Mouse interaction state
346
392
  mouseState = {
347
393
  x: 0,
@@ -351,7 +397,6 @@ class InteractionSystem {
351
397
  leftButton: false,
352
398
  rightButton: false,
353
399
  middleButton: false,
354
- velocity: { x: 0, y: 0 },
355
400
  deltaX: 0,
356
401
  deltaY: 0,
357
402
  wheelDelta: 0,
@@ -374,7 +419,9 @@ class InteractionSystem {
374
419
  shift: false,
375
420
  ctrl: false,
376
421
  alt: false,
377
- meta: false
422
+ meta: false,
423
+ textureData: null
424
+ // Assigned in constructor body
378
425
  };
379
426
  // Touch interaction state
380
427
  touchState = {
@@ -383,23 +430,26 @@ class InteractionSystem {
383
430
  started: [],
384
431
  moved: [],
385
432
  ended: [],
386
- primary: null,
387
- gestures: {
388
- isPinching: false,
389
- isRotating: false,
390
- isPanning: false,
391
- isTapping: false,
392
- pinchScale: 1,
393
- pinchDelta: 0,
394
- rotationAngle: 0,
395
- rotationDelta: 0,
396
- panDelta: { x: 0, y: 0 },
397
- tapCount: 0,
398
- lastTapTime: 0,
399
- tapPosition: null
400
- }
433
+ primary: null
434
+ };
435
+ // Keyboard texture buffer (256x3, Shadertoy-compatible)
436
+ // Row 0: held state, Row 1: pressed this frame, Row 2: toggle
437
+ keyboardTextureData = new Uint8Array(256 * 3);
438
+ // Unified pointer state (derived from mouse + touch)
439
+ pointerState = {
440
+ x: 0,
441
+ y: 0,
442
+ deltaX: 0,
443
+ deltaY: 0,
444
+ isDown: false,
445
+ wasPressed: false,
446
+ wasReleased: false,
447
+ isInCanvas: false,
448
+ type: "none"
401
449
  };
450
+ previousPointerDown = false;
402
451
  constructor() {
452
+ this.keyboardState.textureData = this.keyboardTextureData;
403
453
  this.handleMouseUpdate = this.handleMouseUpdate.bind(this);
404
454
  this.handleKeyboardUpdate = this.handleKeyboardUpdate.bind(this);
405
455
  this.handleTouchUpdate = this.handleTouchUpdate.bind(this);
@@ -412,7 +462,8 @@ class InteractionSystem {
412
462
  return {
413
463
  mouse: this.mouseState,
414
464
  keyboard: this.keyboardState,
415
- touches: this.touchState
465
+ touches: this.touchState,
466
+ pointer: this.pointerState
416
467
  };
417
468
  }
418
469
  /**
@@ -422,17 +473,17 @@ class InteractionSystem {
422
473
  this.mouseState.wasPressed = false;
423
474
  this.mouseState.wasReleased = false;
424
475
  this.mouseState.wasMoved = false;
476
+ this.mouseState.deltaX = 0;
477
+ this.mouseState.deltaY = 0;
425
478
  this.mouseState.wheelDelta = 0;
426
479
  this.mouseState.wheelX = 0;
427
480
  this.mouseState.wheelY = 0;
428
481
  this.keyboardState.pressedThisFrame.clear();
429
482
  this.keyboardState.releasedThisFrame.clear();
483
+ this.keyboardTextureData.fill(0, 256, 512);
430
484
  this.touchState.started = [];
431
485
  this.touchState.moved = [];
432
486
  this.touchState.ended = [];
433
- this.touchState.gestures.isTapping = false;
434
- this.touchState.gestures.pinchDelta = 0;
435
- this.touchState.gestures.rotationDelta = 0;
436
487
  }
437
488
  /**
438
489
  * Handle mouse update messages from the host
@@ -446,16 +497,17 @@ class InteractionSystem {
446
497
  this.mouseState.rightButton = (data.buttons & 2) !== 0;
447
498
  this.mouseState.middleButton = (data.buttons & 4) !== 0;
448
499
  this.mouseState.isPressed = data.buttons > 0;
449
- this.mouseState.deltaX = data.deltaX || 0;
450
- this.mouseState.deltaY = data.deltaY || 0;
500
+ this.mouseState.deltaX += data.deltaX || 0;
501
+ this.mouseState.deltaY += data.deltaY || 0;
451
502
  this.mouseState.wheelDelta += data.wheelDeltaY || 0;
452
503
  this.mouseState.wheelX += data.wheelDeltaX || 0;
453
504
  this.mouseState.wheelY += data.wheelDeltaY || 0;
454
- this.mouseState.velocity.x = data.deltaX || 0;
455
- this.mouseState.velocity.y = data.deltaY || 0;
456
- this.mouseState.wasPressed = this.mouseState.wasPressed || (data.wasPressed || false);
457
- this.mouseState.wasReleased = this.mouseState.wasReleased || (data.wasReleased || false);
505
+ const justPressed = data.buttons & ~this.previousButtons;
506
+ const justReleased = this.previousButtons & ~data.buttons;
507
+ this.mouseState.wasPressed = this.mouseState.wasPressed || justPressed !== 0;
508
+ this.mouseState.wasReleased = this.mouseState.wasReleased || justReleased !== 0;
458
509
  this.mouseState.wasMoved = this.mouseState.wasMoved || (data.deltaX !== 0 || data.deltaY !== 0);
510
+ this.previousButtons = data.buttons;
459
511
  }
460
512
  /**
461
513
  * Handle keyboard update messages from the host
@@ -468,11 +520,21 @@ class InteractionSystem {
468
520
  this.keyboardState.activeKeys.add(key);
469
521
  this.keyboardState.pressedThisFrame.add(key);
470
522
  this.keyboardState.lastKeyPressed = data.key;
523
+ const kc = data.keyCode;
524
+ if (kc !== void 0 && kc >= 0 && kc <= 255) {
525
+ this.keyboardTextureData[kc] = 255;
526
+ this.keyboardTextureData[256 + kc] = 255;
527
+ this.keyboardTextureData[512 + kc] ^= 255;
528
+ }
471
529
  }
472
530
  } else if (data.type === "keyup") {
473
531
  this.keyboardState.activeKeys.delete(key);
474
532
  this.keyboardState.releasedThisFrame.add(key);
475
533
  this.keyboardState.lastKeyReleased = data.key;
534
+ const kc = data.keyCode;
535
+ if (kc !== void 0 && kc >= 0 && kc <= 255) {
536
+ this.keyboardTextureData[kc] = 0;
537
+ }
476
538
  }
477
539
  this.keyboardState.shift = data.shiftKey;
478
540
  this.keyboardState.ctrl = data.ctrlKey;
@@ -484,52 +546,92 @@ class InteractionSystem {
484
546
  */
485
547
  handleTouchUpdate(data) {
486
548
  if (!this.isEnabled) return;
487
- this.touchState.started = [];
488
- this.touchState.moved = [];
489
- this.touchState.ended = [];
490
- const touches = data.touches.map((touch) => ({
491
- id: touch.identifier,
492
- x: touch.clientX,
493
- y: touch.clientY,
494
- pressure: touch.pressure || 0,
495
- radius: Math.max(touch.radiusX || 0, touch.radiusY || 0),
496
- radiusX: touch.radiusX || 0,
497
- radiusY: touch.radiusY || 0,
498
- rotationAngle: touch.rotationAngle || 0,
499
- force: touch.force || touch.pressure || 0,
500
- deltaX: 0,
501
- // Could be calculated if we track previous positions
502
- deltaY: 0,
503
- velocity: { x: 0, y: 0 },
504
- // Could be calculated if we track movement
505
- isNew: data.type === "touchstart",
506
- isActive: true,
507
- isEnding: data.type === "touchend" || data.type === "touchcancel"
508
- }));
509
- this.touchState.points = touches;
510
- this.touchState.count = touches.length;
511
- this.touchState.primary = touches[0] || null;
549
+ const touches = data.touches.map((touch) => {
550
+ const id = touch.identifier;
551
+ const ended = !!touch.ended;
552
+ let deltaX = 0;
553
+ let deltaY = 0;
554
+ let vx = 0;
555
+ let vy = 0;
556
+ const prev = this.previousTouchPositions.get(id);
557
+ if (prev) {
558
+ deltaX = touch.clientX - prev.x;
559
+ deltaY = touch.clientY - prev.y;
560
+ const dt = (data.timestamp - prev.timestamp) / 1e3;
561
+ if (dt > 0) {
562
+ vx = deltaX / dt;
563
+ vy = deltaY / dt;
564
+ }
565
+ }
566
+ if (ended) {
567
+ this.previousTouchPositions.delete(id);
568
+ } else {
569
+ this.previousTouchPositions.set(id, {
570
+ x: touch.clientX,
571
+ y: touch.clientY,
572
+ timestamp: data.timestamp
573
+ });
574
+ }
575
+ return {
576
+ id,
577
+ x: touch.clientX,
578
+ y: touch.clientY,
579
+ pressure: touch.force,
580
+ radius: Math.max(touch.radiusX, touch.radiusY),
581
+ radiusX: touch.radiusX,
582
+ radiusY: touch.radiusY,
583
+ rotationAngle: touch.rotationAngle,
584
+ force: touch.force,
585
+ isInCanvas: touch.isInCanvas,
586
+ deltaX,
587
+ deltaY,
588
+ velocity: { x: vx, y: vy },
589
+ isNew: data.type === "touchstart" && !prev,
590
+ isActive: !ended,
591
+ isEnding: ended
592
+ };
593
+ });
594
+ const activePoints = touches.filter((t) => t.isActive);
595
+ const endedPoints = touches.filter((t) => t.isEnding);
596
+ this.touchState.points = activePoints;
597
+ this.touchState.count = activePoints.length;
598
+ this.touchState.primary = activePoints[0] || null;
512
599
  if (data.type === "touchstart") {
513
- this.touchState.started = touches;
600
+ this.touchState.started.push(...touches.filter((t) => t.isNew));
514
601
  } else if (data.type === "touchmove") {
515
- this.touchState.moved = touches;
602
+ this.touchState.moved = activePoints;
516
603
  } else if (data.type === "touchend" || data.type === "touchcancel") {
517
- this.touchState.ended = touches;
518
- }
519
- this.touchState.gestures = {
520
- isPinching: false,
521
- isRotating: false,
522
- isPanning: false,
523
- isTapping: false,
524
- pinchScale: 1,
525
- pinchDelta: 0,
526
- rotationAngle: 0,
527
- rotationDelta: 0,
528
- panDelta: { x: 0, y: 0 },
529
- tapCount: 0,
530
- lastTapTime: 0,
531
- tapPosition: null
532
- };
604
+ this.touchState.ended.push(...endedPoints);
605
+ }
606
+ }
607
+ /**
608
+ * Compute unified pointer state from current mouse and touch state.
609
+ * Call after all interaction events are processed, before render.
610
+ */
611
+ updatePointerState() {
612
+ const touch = this.touchState;
613
+ const mouse = this.mouseState;
614
+ if (touch.count > 0 && touch.primary) {
615
+ const p = touch.primary;
616
+ this.pointerState.x = p.x;
617
+ this.pointerState.y = p.y;
618
+ this.pointerState.deltaX = p.deltaX;
619
+ this.pointerState.deltaY = p.deltaY;
620
+ this.pointerState.isDown = true;
621
+ this.pointerState.isInCanvas = p.isInCanvas;
622
+ this.pointerState.type = "touch";
623
+ } else {
624
+ this.pointerState.x = mouse.x;
625
+ this.pointerState.y = mouse.y;
626
+ this.pointerState.deltaX = mouse.deltaX;
627
+ this.pointerState.deltaY = mouse.deltaY;
628
+ this.pointerState.isDown = mouse.leftButton;
629
+ this.pointerState.isInCanvas = mouse.isInCanvas;
630
+ this.pointerState.type = mouse.isInCanvas ? "mouse" : "none";
631
+ }
632
+ this.pointerState.wasPressed = this.pointerState.isDown && !this.previousPointerDown;
633
+ this.pointerState.wasReleased = !this.pointerState.isDown && this.previousPointerDown;
634
+ this.previousPointerDown = this.pointerState.isDown;
533
635
  }
534
636
  /**
535
637
  * Reset all interaction state (called when loading new scene)
@@ -543,7 +645,6 @@ class InteractionSystem {
543
645
  leftButton: false,
544
646
  rightButton: false,
545
647
  middleButton: false,
546
- velocity: { x: 0, y: 0 },
547
648
  deltaX: 0,
548
649
  deltaY: 0,
549
650
  wheelDelta: 0,
@@ -562,26 +663,26 @@ class InteractionSystem {
562
663
  this.keyboardState.ctrl = false;
563
664
  this.keyboardState.alt = false;
564
665
  this.keyboardState.meta = false;
666
+ this.keyboardTextureData.fill(0);
565
667
  this.touchState.points = [];
566
668
  this.touchState.count = 0;
567
669
  this.touchState.started = [];
568
670
  this.touchState.moved = [];
569
671
  this.touchState.ended = [];
570
672
  this.touchState.primary = null;
571
- Object.assign(this.touchState.gestures, {
572
- isPinching: false,
573
- isRotating: false,
574
- isPanning: false,
575
- isTapping: false,
576
- pinchScale: 1,
577
- pinchDelta: 0,
578
- rotationAngle: 0,
579
- rotationDelta: 0,
580
- panDelta: { x: 0, y: 0 },
581
- tapCount: 0,
582
- lastTapTime: 0,
583
- tapPosition: null
673
+ this.previousTouchPositions.clear();
674
+ Object.assign(this.pointerState, {
675
+ x: 0,
676
+ y: 0,
677
+ deltaX: 0,
678
+ deltaY: 0,
679
+ isDown: false,
680
+ wasPressed: false,
681
+ wasReleased: false,
682
+ isInCanvas: false,
683
+ type: "none"
584
684
  });
685
+ this.previousPointerDown = false;
585
686
  }
586
687
  /**
587
688
  * Enable or disable interaction processing
@@ -609,8 +710,6 @@ class InteractionSystem {
609
710
  this.mouseState.leftButton = false;
610
711
  this.mouseState.rightButton = false;
611
712
  this.mouseState.middleButton = false;
612
- this.mouseState.velocity.x = 0;
613
- this.mouseState.velocity.y = 0;
614
713
  this.mouseState.deltaX = 0;
615
714
  this.mouseState.deltaY = 0;
616
715
  this.mouseState.wheelDelta = 0;
@@ -628,24 +727,24 @@ class InteractionSystem {
628
727
  this.keyboardState.ctrl = false;
629
728
  this.keyboardState.alt = false;
630
729
  this.keyboardState.meta = false;
730
+ this.keyboardTextureData.fill(0);
631
731
  this.touchState.points = [];
632
732
  this.touchState.count = 0;
633
733
  this.touchState.started = [];
634
734
  this.touchState.moved = [];
635
735
  this.touchState.ended = [];
636
736
  this.touchState.primary = null;
637
- this.touchState.gestures.isPinching = false;
638
- this.touchState.gestures.isRotating = false;
639
- this.touchState.gestures.isPanning = false;
640
- this.touchState.gestures.isTapping = false;
641
- this.touchState.gestures.pinchScale = 1;
642
- this.touchState.gestures.pinchDelta = 0;
643
- this.touchState.gestures.rotationAngle = 0;
644
- this.touchState.gestures.rotationDelta = 0;
645
- this.touchState.gestures.panDelta = { x: 0, y: 0 };
646
- this.touchState.gestures.tapCount = 0;
647
- this.touchState.gestures.lastTapTime = 0;
648
- this.touchState.gestures.tapPosition = null;
737
+ this.previousTouchPositions.clear();
738
+ this.pointerState.x = 0;
739
+ this.pointerState.y = 0;
740
+ this.pointerState.deltaX = 0;
741
+ this.pointerState.deltaY = 0;
742
+ this.pointerState.isDown = false;
743
+ this.pointerState.wasPressed = false;
744
+ this.pointerState.wasReleased = false;
745
+ this.pointerState.isInCanvas = false;
746
+ this.pointerState.type = "none";
747
+ this.previousPointerDown = false;
649
748
  }
650
749
  }
651
750
  class CVSystem {
@@ -1982,7 +2081,7 @@ class P5WorkerAdapter {
1982
2081
  addP5PropertyToImageParameters(parameterObjects) {
1983
2082
  if (!this.p5Instance) return;
1984
2083
  const isImageLike = (value) => {
1985
- return value instanceof ImageBitmap || value instanceof OffscreenCanvas || value && typeof value === "object" && "width" in value && "height" in value;
2084
+ return value instanceof ImageBitmap || value && typeof value === "object" && "width" in value && "height" in value;
1986
2085
  };
1987
2086
  for (const [name, param] of parameterObjects) {
1988
2087
  try {
@@ -2191,6 +2290,11 @@ class ShaderParameterParser {
2191
2290
  break;
2192
2291
  case "image":
2193
2292
  break;
2293
+ case "button":
2294
+ if (param.config.default !== void 0) {
2295
+ console.warn(`Parameter ${param.uniformName} of type 'button' ignores 'default' — buttons always start as false`);
2296
+ }
2297
+ break;
2194
2298
  case "accumulator":
2195
2299
  if (param.config.rate === void 0) {
2196
2300
  throw new Error(`Accumulator '${param.uniformName}' requires a 'rate' config key (parameter name or numeric constant)`);
@@ -2198,9 +2302,6 @@ class ShaderParameterParser {
2198
2302
  if (typeof param.config.rate !== "string" && typeof param.config.rate !== "number") {
2199
2303
  throw new Error(`Accumulator '${param.uniformName}' rate must be a parameter name or numeric constant`);
2200
2304
  }
2201
- if (typeof param.config.rate === "string" && param.config.rate.startsWith("u_")) {
2202
- throw new Error(`Accumulator '${param.uniformName}' cannot reference built-in uniform '${param.config.rate}' — use a parameter name`);
2203
- }
2204
2305
  break;
2205
2306
  default:
2206
2307
  console.warn(`Unknown parameter type: ${param.type}`);
@@ -2225,6 +2326,8 @@ class ShaderParameterParser {
2225
2326
  return `uniform int ${param.uniformName};`;
2226
2327
  case "image":
2227
2328
  return `uniform sampler2D ${param.uniformName};`;
2329
+ case "button":
2330
+ return `uniform bool ${param.uniformName};`;
2228
2331
  case "accumulator":
2229
2332
  return `uniform float ${param.uniformName}; // accumulator (rate: ${param.config.rate})`;
2230
2333
  default:
@@ -2244,11 +2347,15 @@ class ShaderParameterParser {
2244
2347
  if (param.type !== "accumulator") continue;
2245
2348
  const rate = param.config.rate;
2246
2349
  if (typeof rate === "string" && !paramNames.has(rate)) {
2247
- const available = [...paramNames].join(", ");
2248
- console.warn(
2249
- `Accumulator '${param.uniformName}' references parameter '${rate}' which does not exist.` + (available ? ` Available parameters: ${available}` : " No parameters declared.")
2250
- );
2251
- param.config._rateInvalid = true;
2350
+ if (rate.startsWith("u_")) {
2351
+ param.config._rateIsBuiltIn = true;
2352
+ } else {
2353
+ const available = [...paramNames].join(", ");
2354
+ console.warn(
2355
+ `Accumulator '${param.uniformName}' references parameter '${rate}' which does not exist.` + (available ? ` Available parameters: ${available}` : " No parameters declared.")
2356
+ );
2357
+ param.config._rateInvalid = true;
2358
+ }
2252
2359
  }
2253
2360
  }
2254
2361
  }
@@ -2295,6 +2402,7 @@ class ShaderWorkerAdapter {
2295
2402
  audioWaveformTexture = null;
2296
2403
  videoTexture = null;
2297
2404
  segmentationTexture = null;
2405
+ keyboardTexture = null;
2298
2406
  // Multi-stream textures
2299
2407
  streamTextures = [];
2300
2408
  // Device video textures
@@ -2302,6 +2410,8 @@ class ShaderWorkerAdapter {
2302
2410
  // Accumulator state (CPU-side phase accumulators for smooth parameter-driven animation)
2303
2411
  accumulatorValues = /* @__PURE__ */ new Map();
2304
2412
  accumulatorWarned = /* @__PURE__ */ new Set();
2413
+ // Cache of scalar uniform values for accumulator built-in rate lookups
2414
+ scalarUniformCache = /* @__PURE__ */ new Map();
2305
2415
  // Backbuffer support (ping-pong framebuffers)
2306
2416
  backbufferFramebuffer = null;
2307
2417
  backbufferTexture = null;
@@ -2478,7 +2588,6 @@ uniform vec2 u_resolution; // Canvas width and height in pixels
2478
2588
  uniform float u_time; // Elapsed time in seconds since scene start
2479
2589
  uniform float u_deltaTime; // Time elapsed since last frame in seconds
2480
2590
  uniform int u_frame; // Current frame number
2481
- uniform float u_pixelRatio; // Device pixel ratio for high-DPI displays
2482
2591
  uniform float u_fps; // Current frames per second
2483
2592
 
2484
2593
  // Mouse API
@@ -2488,7 +2597,10 @@ uniform bool u_mousePressed; // True if any mouse button is pressed
2488
2597
  uniform bool u_mouseLeft; // True if left mouse button is pressed
2489
2598
  uniform bool u_mouseRight; // True if right mouse button is pressed
2490
2599
  uniform bool u_mouseMiddle; // True if middle mouse button is pressed
2491
- uniform vec2 u_mouseVelocity; // Mouse movement velocity in pixels per second
2600
+ uniform vec2 u_mouseDelta; // Mouse movement delta in pixels (per frame)
2601
+ uniform float u_mouseWheel; // Mouse wheel scroll delta (per frame)
2602
+ uniform bool u_mouseWasPressed; // True on the frame a mouse button was pressed
2603
+ uniform bool u_mouseWasReleased; // True on the frame a mouse button was released
2492
2604
 
2493
2605
  // Keyboard API - Common keys
2494
2606
  uniform bool u_keySpace; // True if spacebar is pressed
@@ -2504,6 +2616,10 @@ uniform bool u_keyDown; // True if Down arrow key is pressed
2504
2616
  uniform bool u_keyLeft; // True if Left arrow key is pressed
2505
2617
  uniform bool u_keyRight; // True if Right arrow key is pressed
2506
2618
 
2619
+ // Keyboard Texture (Shadertoy-compatible 256x3)
2620
+ // Row 0: held state, Row 1: pressed this frame, Row 2: toggle
2621
+ uniform sampler2D u_keyboard; // Keyboard state texture (256x3, LUMINANCE)
2622
+
2507
2623
  // Touch API
2508
2624
  uniform int u_touchCount; // Number of active touch points (0-5)
2509
2625
  uniform vec2 u_touch0; // First touch point position in pixels
@@ -2512,6 +2628,14 @@ uniform vec2 u_touch2; // Third touch point position in pixels
2512
2628
  uniform vec2 u_touch3; // Fourth touch point position in pixels
2513
2629
  uniform vec2 u_touch4; // Fifth touch point position in pixels
2514
2630
 
2631
+ // Pointer API (unified mouse/touch)
2632
+ uniform vec2 u_pointer; // Primary input position in pixels (WebGL coords)
2633
+ uniform vec2 u_pointerDelta; // Primary input movement delta (per frame)
2634
+ uniform bool u_pointerDown; // True if primary input is active (left-click or touch)
2635
+ uniform bool u_pointerWasPressed; // True on the frame primary input became active
2636
+ uniform bool u_pointerWasReleased; // True on the frame primary input was released
2637
+ uniform bool u_pointerInCanvas; // True if primary input is inside canvas bounds
2638
+
2515
2639
  // Audio - Volume
2516
2640
  uniform float u_audioVolume; // RMS volume level (0-1)
2517
2641
  uniform float u_audioPeak; // Peak amplitude (0-1)
@@ -2845,6 +2969,7 @@ ${error}`);
2845
2969
  this.textureUnits.set("u_audioWaveform", this.nextTextureUnit++);
2846
2970
  this.textureUnits.set("u_video", this.nextTextureUnit++);
2847
2971
  this.textureUnits.set("u_segmentationMask", this.nextTextureUnit++);
2972
+ this.textureUnits.set("u_keyboard", this.nextTextureUnit++);
2848
2973
  for (let i = 0; i < ShaderWorkerAdapter.MAX_STREAMS; i++) {
2849
2974
  this.textureUnits.set(`u_stream${i}`, this.nextTextureUnit++);
2850
2975
  }
@@ -2929,7 +3054,6 @@ ${error}`);
2929
3054
  this.setUniform("u_time", "float", viji.time);
2930
3055
  this.setUniform("u_deltaTime", "float", viji.deltaTime);
2931
3056
  this.setUniform("u_frame", "int", viji.frameCount);
2932
- this.setUniform("u_pixelRatio", "float", viji.pixelRatio);
2933
3057
  this.setUniform("u_fps", "float", viji.fps);
2934
3058
  this.setUniform("u_mouse", "vec2", [viji.mouse.x, viji.height - viji.mouse.y]);
2935
3059
  this.setUniform("u_mouseInCanvas", "bool", viji.mouse.isInCanvas);
@@ -2937,7 +3061,10 @@ ${error}`);
2937
3061
  this.setUniform("u_mouseLeft", "bool", viji.mouse.leftButton);
2938
3062
  this.setUniform("u_mouseRight", "bool", viji.mouse.rightButton);
2939
3063
  this.setUniform("u_mouseMiddle", "bool", viji.mouse.middleButton);
2940
- this.setUniform("u_mouseVelocity", "vec2", [viji.mouse.velocity.x, -viji.mouse.velocity.y]);
3064
+ this.setUniform("u_mouseDelta", "vec2", [viji.mouse.deltaX, -viji.mouse.deltaY]);
3065
+ this.setUniform("u_mouseWheel", "float", viji.mouse.wheelDelta);
3066
+ this.setUniform("u_mouseWasPressed", "bool", viji.mouse.wasPressed);
3067
+ this.setUniform("u_mouseWasReleased", "bool", viji.mouse.wasReleased);
2941
3068
  this.setUniform("u_keySpace", "bool", viji.keyboard.isPressed(" ") || viji.keyboard.isPressed("space"));
2942
3069
  this.setUniform("u_keyShift", "bool", viji.keyboard.shift);
2943
3070
  this.setUniform("u_keyCtrl", "bool", viji.keyboard.ctrl);
@@ -2950,6 +3077,9 @@ ${error}`);
2950
3077
  this.setUniform("u_keyDown", "bool", viji.keyboard.isPressed("ArrowDown"));
2951
3078
  this.setUniform("u_keyLeft", "bool", viji.keyboard.isPressed("ArrowLeft"));
2952
3079
  this.setUniform("u_keyRight", "bool", viji.keyboard.isPressed("ArrowRight"));
3080
+ if (viji.keyboard.textureData) {
3081
+ this.updateKeyboardTexture(viji.keyboard.textureData);
3082
+ }
2953
3083
  this.setUniform("u_touchCount", "int", viji.touches.count);
2954
3084
  for (let i = 0; i < 5; i++) {
2955
3085
  const touch = viji.touches.points[i];
@@ -2959,6 +3089,12 @@ ${error}`);
2959
3089
  this.setUniform(`u_touch${i}`, "vec2", [0, 0]);
2960
3090
  }
2961
3091
  }
3092
+ this.setUniform("u_pointer", "vec2", [viji.pointer.x, viji.height - viji.pointer.y]);
3093
+ this.setUniform("u_pointerDelta", "vec2", [viji.pointer.deltaX, -viji.pointer.deltaY]);
3094
+ this.setUniform("u_pointerDown", "bool", viji.pointer.isDown);
3095
+ this.setUniform("u_pointerWasPressed", "bool", viji.pointer.wasPressed);
3096
+ this.setUniform("u_pointerWasReleased", "bool", viji.pointer.wasReleased);
3097
+ this.setUniform("u_pointerInCanvas", "bool", viji.pointer.isInCanvas);
2962
3098
  const audio = viji.audio;
2963
3099
  this.setUniform("u_audioVolume", "float", audio.volume?.current || 0);
2964
3100
  this.setUniform("u_audioPeak", "float", audio.volume?.peak || 0);
@@ -3294,6 +3430,8 @@ ${error}`);
3294
3430
  const sourceParam = parameterObjects.get(rateConfig);
3295
3431
  if (sourceParam !== void 0) {
3296
3432
  rate = sourceParam.value ?? 0;
3433
+ } else if (this.scalarUniformCache.has(rateConfig)) {
3434
+ rate = this.scalarUniformCache.get(rateConfig);
3297
3435
  } else {
3298
3436
  if (!this.accumulatorWarned.has(param.uniformName)) {
3299
3437
  console.warn(`Accumulator '${param.uniformName}': rate source '${rateConfig}' not found, using 0`);
@@ -3343,6 +3481,9 @@ ${error}`);
3343
3481
  this.updateImageTexture(param.uniformName, value);
3344
3482
  }
3345
3483
  break;
3484
+ case "button":
3485
+ this.setUniform(param.uniformName, "bool", value);
3486
+ break;
3346
3487
  }
3347
3488
  }
3348
3489
  }
@@ -3350,13 +3491,34 @@ ${error}`);
3350
3491
  * Set uniform value
3351
3492
  */
3352
3493
  setUniform(name, type, value) {
3353
- const location = this.uniformLocations.get(name);
3354
- if (location === null || location === void 0) {
3355
- if (name.includes("[") && Math.random() < 0.01) {
3356
- console.log(`[ShaderAdapter] Uniform '${name}' not found (location is ${location})`);
3357
- }
3358
- return;
3494
+ switch (type) {
3495
+ case "float":
3496
+ this.scalarUniformCache.set(name, value);
3497
+ break;
3498
+ case "int":
3499
+ this.scalarUniformCache.set(name, value);
3500
+ break;
3501
+ case "bool":
3502
+ this.scalarUniformCache.set(name, value ? 1 : 0);
3503
+ break;
3504
+ case "vec2":
3505
+ this.scalarUniformCache.set(name + ".x", value[0]);
3506
+ this.scalarUniformCache.set(name + ".y", value[1]);
3507
+ break;
3508
+ case "vec3":
3509
+ this.scalarUniformCache.set(name + ".x", value[0]);
3510
+ this.scalarUniformCache.set(name + ".y", value[1]);
3511
+ this.scalarUniformCache.set(name + ".z", value[2]);
3512
+ break;
3513
+ case "vec4":
3514
+ this.scalarUniformCache.set(name + ".x", value[0]);
3515
+ this.scalarUniformCache.set(name + ".y", value[1]);
3516
+ this.scalarUniformCache.set(name + ".z", value[2]);
3517
+ this.scalarUniformCache.set(name + ".w", value[3]);
3518
+ break;
3359
3519
  }
3520
+ const location = this.uniformLocations.get(name);
3521
+ if (location === null || location === void 0) return;
3360
3522
  const gl = this.gl;
3361
3523
  switch (type) {
3362
3524
  case "float":
@@ -3605,6 +3767,37 @@ ${error}`);
3605
3767
  gl.uniform1i(location, unit);
3606
3768
  }
3607
3769
  }
3770
+ /**
3771
+ * Update keyboard state texture (Shadertoy-compatible 256x3)
3772
+ */
3773
+ updateKeyboardTexture(data) {
3774
+ const gl = this.gl;
3775
+ const unit = this.textureUnits.get("u_keyboard");
3776
+ if (!this.keyboardTexture) {
3777
+ this.keyboardTexture = gl.createTexture();
3778
+ }
3779
+ gl.activeTexture(gl.TEXTURE0 + unit);
3780
+ gl.bindTexture(gl.TEXTURE_2D, this.keyboardTexture);
3781
+ gl.texImage2D(
3782
+ gl.TEXTURE_2D,
3783
+ 0,
3784
+ gl.LUMINANCE,
3785
+ 256,
3786
+ 3,
3787
+ 0,
3788
+ gl.LUMINANCE,
3789
+ gl.UNSIGNED_BYTE,
3790
+ data
3791
+ );
3792
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
3793
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
3794
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3795
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3796
+ const location = this.uniformLocations.get("u_keyboard");
3797
+ if (location) {
3798
+ gl.uniform1i(location, unit);
3799
+ }
3800
+ }
3608
3801
  /**
3609
3802
  * Update image parameter texture
3610
3803
  */
@@ -3806,7 +3999,6 @@ class VijiWorkerRuntime {
3806
3999
  gl: null,
3807
4000
  width: 0,
3808
4001
  height: 0,
3809
- pixelRatio: 1,
3810
4002
  // Timing
3811
4003
  time: 0,
3812
4004
  deltaTime: 0,
@@ -3895,6 +4087,9 @@ class VijiWorkerRuntime {
3895
4087
  image: (defaultValue, config) => {
3896
4088
  return this.parameterSystem.createImageParameter(defaultValue, config);
3897
4089
  },
4090
+ button: (config) => {
4091
+ return this.parameterSystem.createButtonParameter(config);
4092
+ },
3898
4093
  // Context selection
3899
4094
  useContext: (type) => {
3900
4095
  if (type === "2d") {
@@ -4020,6 +4215,9 @@ class VijiWorkerRuntime {
4020
4215
  case "image":
4021
4216
  this.viji.image(null, paramConfig);
4022
4217
  break;
4218
+ case "button":
4219
+ this.viji.button(paramConfig);
4220
+ break;
4023
4221
  }
4024
4222
  }
4025
4223
  // Reset parameter state (called when loading new scene)
@@ -4572,6 +4770,7 @@ class VijiWorkerRuntime {
4572
4770
  this.viji.frameCount = ++this.frameCount;
4573
4771
  this.trackEffectiveFrameTime(currentTime);
4574
4772
  this.lastTime = currentTime;
4773
+ this.interactionSystem.updatePointerState();
4575
4774
  try {
4576
4775
  if (this.shaderAdapter && this.rendererType === "shader") {
4577
4776
  const parameterObjects = this.parameterSystem.getAllParameterObjects();
@@ -4593,6 +4792,7 @@ class VijiWorkerRuntime {
4593
4792
  stack: error.stack
4594
4793
  });
4595
4794
  }
4795
+ this.parameterSystem.resetButtonParameters();
4596
4796
  if (this.pendingCaptures.length > 0) {
4597
4797
  const captures = [...this.pendingCaptures];
4598
4798
  this.pendingCaptures = [];
@@ -26049,4 +26249,4 @@ async function setSceneCode(sceneCode) {
26049
26249
  }
26050
26250
  }
26051
26251
  self.setSceneCode = setSceneCode;
26052
- //# sourceMappingURL=viji.worker-DTQvTudb.js.map
26252
+ //# sourceMappingURL=viji.worker-Zg128woJ.js.map