@frapx/core 0.2.0 → 0.4.0

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/dist/index.js CHANGED
@@ -25,6 +25,134 @@ var TextureLoadError = class extends ShaderError {
25
25
  var DestroyedError = class extends ShaderError {
26
26
  };
27
27
 
28
+ // src/internal/webgl.ts
29
+ var defaultVertexShader = `
30
+ attribute vec2 a_position;
31
+ varying vec2 v_uv;
32
+
33
+ void main() {
34
+ v_uv = a_position * 0.5 + 0.5;
35
+ gl_Position = vec4(a_position, 0.0, 1.0);
36
+ }
37
+ `;
38
+ var defaultVertexShader300 = `#version 300 es
39
+ in vec2 a_position;
40
+ out vec2 v_uv;
41
+
42
+ void main() {
43
+ v_uv = a_position * 0.5 + 0.5;
44
+ gl_Position = vec4(a_position, 0.0, 1.0);
45
+ }
46
+ `;
47
+ var isGLSL300 = (src) => /^\s*#version\s+300\s+es\b/.test(src);
48
+ var isWebGL2Context = (gl) => typeof gl.createVertexArray === "function";
49
+ var createProgram = (gl, vertexSource, fragmentSource) => {
50
+ const vertex = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
51
+ const fragment = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
52
+ const program = gl.createProgram();
53
+ if (!program) {
54
+ throw new ShaderCompileError("Failed to create WebGL program.");
55
+ }
56
+ gl.attachShader(program, vertex);
57
+ gl.attachShader(program, fragment);
58
+ gl.linkProgram(program);
59
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
60
+ const log = gl.getProgramInfoLog(program) ?? "Unknown program link error.";
61
+ gl.deleteProgram(program);
62
+ gl.deleteShader(vertex);
63
+ gl.deleteShader(fragment);
64
+ throw new ShaderCompileError(log);
65
+ }
66
+ gl.deleteShader(vertex);
67
+ gl.deleteShader(fragment);
68
+ return program;
69
+ };
70
+ var createFullscreenBuffer = (gl, program) => {
71
+ const buffer = gl.createBuffer();
72
+ if (!buffer) {
73
+ throw new ShaderCompileError("Failed to create fullscreen buffer.");
74
+ }
75
+ const location = gl.getAttribLocation(program, "a_position");
76
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
77
+ gl.bufferData(
78
+ gl.ARRAY_BUFFER,
79
+ new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
80
+ gl.STATIC_DRAW
81
+ );
82
+ if (location >= 0) {
83
+ gl.enableVertexAttribArray(location);
84
+ gl.vertexAttribPointer(location, 2, gl.FLOAT, false, 0, 0);
85
+ }
86
+ return buffer;
87
+ };
88
+ var compileShader = (gl, type, source) => {
89
+ const shader = gl.createShader(type);
90
+ if (!shader) {
91
+ throw new ShaderCompileError("Failed to create WebGL shader.");
92
+ }
93
+ gl.shaderSource(shader, source);
94
+ gl.compileShader(shader);
95
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
96
+ const log = gl.getShaderInfoLog(shader) ?? "Unknown shader compile error.";
97
+ gl.deleteShader(shader);
98
+ throw new ShaderCompileError(log);
99
+ }
100
+ return shader;
101
+ };
102
+
103
+ // src/internal/capture.ts
104
+ var waitForGpu = (gl) => {
105
+ if (!isWebGL2Context(gl)) return Promise.resolve();
106
+ const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
107
+ if (!sync) return Promise.resolve();
108
+ gl.flush();
109
+ return new Promise((resolve) => {
110
+ const poll = () => {
111
+ const status = gl.clientWaitSync(sync, 0, 0);
112
+ if (status === gl.TIMEOUT_EXPIRED) {
113
+ setTimeout(poll, 4);
114
+ return;
115
+ }
116
+ gl.deleteSync(sync);
117
+ resolve();
118
+ };
119
+ poll();
120
+ });
121
+ };
122
+ var readPixels = (gl, framebuffer, width, height, format) => {
123
+ const length = width * height * 4;
124
+ const data = format === "rgba16f" ? new Float32Array(length) : new Uint8Array(length);
125
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
126
+ gl.readPixels(
127
+ 0,
128
+ 0,
129
+ width,
130
+ height,
131
+ gl.RGBA,
132
+ format === "rgba16f" ? gl.FLOAT : gl.UNSIGNED_BYTE,
133
+ data
134
+ );
135
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
136
+ flipRows(data, width * 4, height);
137
+ return data;
138
+ };
139
+ var canvasToPng = (canvas) => new Promise((resolve, reject) => {
140
+ canvas.toBlob((blob) => {
141
+ if (blob) resolve(blob);
142
+ else reject(new Error("Failed to encode the canvas as PNG."));
143
+ }, "image/png");
144
+ });
145
+ var flipRows = (data, rowLength, rows) => {
146
+ const temp = data.slice(0, rowLength);
147
+ for (let top = 0, bottom = rows - 1; top < bottom; top++, bottom--) {
148
+ const topStart = top * rowLength;
149
+ const bottomStart = bottom * rowLength;
150
+ temp.set(data.subarray(topStart, topStart + rowLength));
151
+ data.copyWithin(topStart, bottomStart, bottomStart + rowLength);
152
+ data.set(temp, bottomStart);
153
+ }
154
+ };
155
+
28
156
  // src/internal/dom.ts
29
157
  var resolveElement = (target) => {
30
158
  if (!target) return null;
@@ -35,7 +163,17 @@ var setupCanvas = (options) => {
35
163
  const target = resolveElement(options.target);
36
164
  const canvas = options.canvas ?? document.createElement("canvas");
37
165
  const measurementTarget = target ?? options.canvas;
38
- if (!measurementTarget) return null;
166
+ if (!measurementTarget) {
167
+ if (!options.detached) return null;
168
+ canvas.dataset.frapxCanvas = "";
169
+ return {
170
+ target: canvas,
171
+ canvas,
172
+ createdCanvas: true,
173
+ restoredPosition: null,
174
+ detached: true
175
+ };
176
+ }
39
177
  const createdCanvas = !options.canvas;
40
178
  const restoredPosition = target && getComputedStyle(target).position === "static" ? target.style.position : null;
41
179
  if (target && restoredPosition !== null) {
@@ -76,6 +214,19 @@ var resizeCanvasToTarget = (canvas, target, dpr) => {
76
214
  if (canvas.height !== height) canvas.height = height;
77
215
  return { width, height, viewportWidth, viewportHeight };
78
216
  };
217
+ var resizeCanvasToResolution = (canvas, target, resolution) => {
218
+ const [width, height] = resolution;
219
+ let viewportWidth = width;
220
+ let viewportHeight = height;
221
+ if (target) {
222
+ const rect = target.getBoundingClientRect();
223
+ viewportWidth = Math.max(1, rect.width || canvas.clientWidth || 1);
224
+ viewportHeight = Math.max(1, rect.height || canvas.clientHeight || 1);
225
+ }
226
+ if (canvas.width !== width) canvas.width = width;
227
+ if (canvas.height !== height) canvas.height = height;
228
+ return { width, height, viewportWidth, viewportHeight };
229
+ };
79
230
  var applyCanvasStyle = (canvas, options, shouldPosition) => {
80
231
  canvas.dataset.frapxCanvas = "";
81
232
  if (!shouldPosition) return;
@@ -93,6 +244,7 @@ var applyCanvasStyle = (canvas, options, shouldPosition) => {
93
244
 
94
245
  // src/host.ts
95
246
  var MAX_DELTA_SECONDS = 0.1;
247
+ var IDLE_POINTER = { x: 0, y: 0, uvX: 0, uvY: 0, active: 0 };
96
248
  var createRuntimeHost = (options) => {
97
249
  return new RuntimeHost(options);
98
250
  };
@@ -107,6 +259,8 @@ var RuntimeHost = class {
107
259
  this.rafId = 0;
108
260
  this.running = false;
109
261
  this.inRender = false;
262
+ this.captureDrawing = false;
263
+ this.resizePending = false;
110
264
  this.destroyed = false;
111
265
  this.gates = /* @__PURE__ */ new Set();
112
266
  this.reducedMotionActive = false;
@@ -122,7 +276,15 @@ var RuntimeHost = class {
122
276
  this.removeListeners = [];
123
277
  this.currentStatus = "idle";
124
278
  this.contextRestoredShouldRun = false;
279
+ this.manualDelta = 0;
280
+ this.lastManualTime = null;
281
+ this.objectFitRestore = null;
282
+ // Letterbox offset (CSS px) of the contained canvas content inside the target.
283
+ this.contentOffsetX = 0;
284
+ this.contentOffsetY = 0;
125
285
  this.options = options;
286
+ this.clockMode = options.clock ?? "realtime";
287
+ this.fixedResolution = options.resolution ? normalizeResolution(options.resolution) : null;
126
288
  this.ready = new Promise((resolve, reject) => {
127
289
  this.readyResolve = resolve;
128
290
  this.readyReject = reject;
@@ -143,6 +305,9 @@ var RuntimeHost = class {
143
305
  get supported() {
144
306
  return !["unsupported", "destroyed"].includes(this.currentStatus);
145
307
  }
308
+ get clock() {
309
+ return this.clockMode;
310
+ }
146
311
  start() {
147
312
  if (this.destroyed || this.currentStatus === "unsupported") return;
148
313
  this.running = true;
@@ -156,12 +321,94 @@ var RuntimeHost = class {
156
321
  }
157
322
  }
158
323
  render() {
159
- if (this.destroyed || !this.dom || !this.context || this.currentStatus === "unsupported" || this.hasVisibilityBlock || this.inRender)
324
+ if (this.destroyed || !this.dom || !this.context || this.currentStatus === "unsupported" || this.inRender)
325
+ return;
326
+ if (this.isManual) {
327
+ this.renderInternal(this.elapsed, this.manualDelta);
160
328
  return;
329
+ }
330
+ if (this.hasVisibilityBlock) return;
161
331
  this.renderInternal(this.elapsed, 0);
162
332
  }
333
+ setTime(seconds) {
334
+ if (!Number.isFinite(seconds)) {
335
+ throw new RangeError(`setTime() expects a finite number, got ${seconds}`);
336
+ }
337
+ if (this.destroyed) return;
338
+ if (this.isManual) {
339
+ this.manualDelta = this.lastManualTime === null ? 0 : seconds - this.lastManualTime;
340
+ this.lastManualTime = seconds;
341
+ }
342
+ this.elapsed = seconds;
343
+ }
344
+ setClock(clock) {
345
+ if (this.destroyed || clock === this.clockMode) return;
346
+ this.clockMode = clock;
347
+ this.manualDelta = 0;
348
+ this.lastManualTime = null;
349
+ this.reconcile();
350
+ }
351
+ setResolution(resolution) {
352
+ if (this.destroyed) return;
353
+ if (resolution === null) {
354
+ if (this.isDetached) {
355
+ throw new TypeError(
356
+ "A canvas without a target needs a fixed resolution."
357
+ );
358
+ }
359
+ this.fixedResolution = null;
360
+ } else {
361
+ const next = normalizeResolution(resolution);
362
+ if (this.context) assertWithinLimits(this.context, next);
363
+ this.fixedResolution = next;
364
+ }
365
+ this.applyObjectFit();
366
+ this.resize();
367
+ }
368
+ async captureFrame(options = {}) {
369
+ const format = options.format ?? "png";
370
+ if (format !== "png" && format !== "rgba8" && format !== "rgba16f") {
371
+ throw new RangeError(`Unknown capture format: ${String(format)}`);
372
+ }
373
+ await this.ready;
374
+ if (this.destroyed) {
375
+ throw new DestroyedError("Runtime host was destroyed.");
376
+ }
377
+ if (!this.dom || !this.context) {
378
+ throw new UnsupportedError("Cannot capture without a WebGL context.");
379
+ }
380
+ if (this.inRender) {
381
+ throw new Error("captureFrame() cannot be called while a frame is drawing.");
382
+ }
383
+ const sourceFormat = this.options.captureSource?.()?.format;
384
+ if (format === "rgba16f" && sourceFormat !== "rgba16f" && sourceFormat !== "rgba32f") {
385
+ throw new UnsupportedError(
386
+ "rgba16f capture needs a float render target, such as feedback with format rgba16f."
387
+ );
388
+ }
389
+ options.signal?.throwIfAborted();
390
+ const delta = this.isManual ? this.manualDelta : 0;
391
+ const drawContext = this.context;
392
+ if (!this.options.onCaptureDraw) {
393
+ this.renderInternal(this.elapsed, delta);
394
+ return this.readCapture(format, drawContext);
395
+ }
396
+ try {
397
+ await this.renderForCapture(this.options.onCaptureDraw, delta, options.signal);
398
+ return this.readCapture(format, drawContext);
399
+ } finally {
400
+ if (this.resizePending) {
401
+ this.resizePending = false;
402
+ this.resize();
403
+ }
404
+ }
405
+ }
163
406
  resize() {
164
407
  if (this.destroyed || !this.dom) return;
408
+ if (this.captureDrawing) {
409
+ this.resizePending = true;
410
+ return;
411
+ }
165
412
  const prevWidth = this.width;
166
413
  const prevHeight = this.height;
167
414
  const prevViewportWidth = this.viewportWidth;
@@ -169,22 +416,9 @@ var RuntimeHost = class {
169
416
  this.measureSize();
170
417
  const sizeChanged = this.width !== prevWidth || this.height !== prevHeight || this.viewportWidth !== prevViewportWidth || this.viewportHeight !== prevViewportHeight;
171
418
  if (sizeChanged && this.context) {
172
- this.options.onResize?.({
173
- gl: this.context,
174
- canvas: this.dom.canvas,
175
- time: this.elapsed,
176
- delta: 0,
177
- frame: this.frame,
178
- width: this.width,
179
- height: this.height,
180
- viewportWidth: this.viewportWidth,
181
- viewportHeight: this.viewportHeight,
182
- pixelRatio: this.pixelRatio,
183
- reducedMotion: this.reducedMotionActive,
184
- pointer: { ...this.pointer }
185
- });
419
+ this.options.onResize?.(this.createFrameState(this.elapsed, 0));
186
420
  }
187
- if (this.options.renderMode === "demand" && !this.inRender) {
421
+ if (this.shouldRenderOnDemand && !this.inRender) {
188
422
  this.render();
189
423
  }
190
424
  }
@@ -200,6 +434,8 @@ var RuntimeHost = class {
200
434
  this.context = null;
201
435
  if (this.dom?.createdCanvas) {
202
436
  this.dom.canvas.remove();
437
+ } else if (this.dom && this.objectFitRestore !== null) {
438
+ this.dom.canvas.style.objectFit = this.objectFitRestore;
203
439
  }
204
440
  const dom = this.dom;
205
441
  if (dom?.restoredPosition !== null && dom && this.options.target) {
@@ -208,6 +444,18 @@ var RuntimeHost = class {
208
444
  this.currentStatus = "destroyed";
209
445
  this.readyReject?.(new DestroyedError("Runtime host was destroyed."));
210
446
  }
447
+ get isManual() {
448
+ return this.clockMode === "manual";
449
+ }
450
+ get isDetached() {
451
+ if (this.dom) return this.dom.detached === true;
452
+ return this.options.target === void 0 && this.options.canvas === void 0;
453
+ }
454
+ // Demand mode re-renders on input changes, except under a manual clock where
455
+ // only explicit render() calls draw.
456
+ get shouldRenderOnDemand() {
457
+ return this.options.renderMode === "demand" && !this.isManual;
458
+ }
211
459
  get hasVisibilityBlock() {
212
460
  return this.gates.has("offscreen") || this.gates.has("hidden");
213
461
  }
@@ -224,6 +472,11 @@ var RuntimeHost = class {
224
472
  if (this.destroyed || this.currentStatus === "unsupported" || this.currentStatus === "error" || this.currentStatus === "context-lost") {
225
473
  return;
226
474
  }
475
+ if (this.isManual) {
476
+ this.cancelFrame();
477
+ this.currentStatus = "paused";
478
+ return;
479
+ }
227
480
  if (this.running) {
228
481
  if (this.hasVisibilityBlock) {
229
482
  this.cancelFrame();
@@ -248,11 +501,60 @@ var RuntimeHost = class {
248
501
  this.renderInternal(this.elapsed, 0);
249
502
  }
250
503
  renderInternal(time, delta) {
251
- if (!this.dom || !this.context) return;
504
+ if (!this.dom || !this.context || this.inRender) return;
252
505
  const gl = this.context;
253
506
  this.inRender = true;
254
- this.measureSize();
255
- const state = {
507
+ try {
508
+ this.measureSize();
509
+ this.options.onDraw(this.createFrameState(time, delta, gl));
510
+ this.frame++;
511
+ } finally {
512
+ this.inRender = false;
513
+ }
514
+ }
515
+ // Holds the render lock across an asynchronous capture draw so no other
516
+ // frame interleaves with it.
517
+ async renderForCapture(draw, delta, signal) {
518
+ if (!this.dom || !this.context) return;
519
+ this.inRender = true;
520
+ this.captureDrawing = true;
521
+ try {
522
+ this.measureSize();
523
+ await draw(this.createFrameState(this.elapsed, delta, this.context), signal);
524
+ this.frame++;
525
+ } finally {
526
+ this.inRender = false;
527
+ this.captureDrawing = false;
528
+ }
529
+ }
530
+ // Reads the frame just drawn, in the task that drew it: the drawing buffer
531
+ // is not preserved. Fails if the context drawn into is gone, including when
532
+ // it was lost and restored while an asynchronous draw was pending.
533
+ readCapture(format, drawContext) {
534
+ if (this.destroyed) {
535
+ throw new DestroyedError("Runtime host was destroyed.");
536
+ }
537
+ const gl = this.context;
538
+ if (!gl || gl !== drawContext || !this.dom) {
539
+ throw new UnsupportedError("The WebGL context was lost while capturing.");
540
+ }
541
+ if (format === "png") {
542
+ return canvasToPng(this.dom.canvas);
543
+ }
544
+ if (format === "rgba8") {
545
+ return readPixels(gl, null, this.width, this.height, "rgba8");
546
+ }
547
+ const source = this.options.captureSource();
548
+ return readPixels(
549
+ gl,
550
+ source.framebuffer,
551
+ source.width,
552
+ source.height,
553
+ "rgba16f"
554
+ );
555
+ }
556
+ createFrameState(time, delta, gl = this.context) {
557
+ return {
256
558
  gl,
257
559
  canvas: this.dom.canvas,
258
560
  time,
@@ -263,15 +565,32 @@ var RuntimeHost = class {
263
565
  viewportWidth: this.viewportWidth,
264
566
  viewportHeight: this.viewportHeight,
265
567
  pixelRatio: this.pixelRatio,
266
- reducedMotion: this.reducedMotionActive,
267
- pointer: { ...this.pointer }
568
+ reducedMotion: this.isManual ? false : this.reducedMotionActive,
569
+ pointer: this.isManual ? { ...IDLE_POINTER } : { ...this.pointer }
268
570
  };
269
- this.options.onDraw(state);
270
- this.frame++;
271
- this.inRender = false;
272
571
  }
273
572
  measureSize() {
274
573
  if (!this.dom) return;
574
+ if (this.fixedResolution) {
575
+ const size2 = resizeCanvasToResolution(
576
+ this.dom.canvas,
577
+ this.dom.detached ? null : this.dom.target,
578
+ this.fixedResolution
579
+ );
580
+ this.width = size2.width;
581
+ this.height = size2.height;
582
+ this.viewportWidth = size2.viewportWidth;
583
+ this.viewportHeight = size2.viewportHeight;
584
+ this.pixelRatio = Math.max(
585
+ size2.width / size2.viewportWidth,
586
+ size2.height / size2.viewportHeight
587
+ );
588
+ this.contentOffsetX = (size2.viewportWidth - size2.width / this.pixelRatio) / 2;
589
+ this.contentOffsetY = (size2.viewportHeight - size2.height / this.pixelRatio) / 2;
590
+ return;
591
+ }
592
+ this.contentOffsetX = 0;
593
+ this.contentOffsetY = 0;
275
594
  this.pixelRatio = resolveDpr(this.options.dpr, this.options.maxDpr);
276
595
  const size = resizeCanvasToTarget(
277
596
  this.dom.canvas,
@@ -283,8 +602,21 @@ var RuntimeHost = class {
283
602
  this.viewportWidth = size.viewportWidth;
284
603
  this.viewportHeight = size.viewportHeight;
285
604
  }
605
+ // Letterbox the fixed-resolution canvas instead of stretching it.
606
+ applyObjectFit() {
607
+ if (!this.dom) return;
608
+ const style = this.dom.canvas.style;
609
+ if (this.fixedResolution && this.objectFitRestore === null) {
610
+ this.objectFitRestore = style.objectFit ?? "";
611
+ style.objectFit = "contain";
612
+ } else if (!this.fixedResolution && this.objectFitRestore !== null) {
613
+ style.objectFit = this.objectFitRestore;
614
+ this.objectFitRestore = null;
615
+ }
616
+ }
286
617
  scheduleFrame() {
287
- if (this.rafId || this.options.renderMode === "demand") return;
618
+ if (this.rafId || this.options.renderMode === "demand" || this.isManual)
619
+ return;
288
620
  this.rafId = requestAnimationFrame((timestamp) => {
289
621
  this.rafId = 0;
290
622
  if (!this.running || this.destroyed) return;
@@ -314,10 +646,17 @@ var RuntimeHost = class {
314
646
  ...this.options.canvas !== void 0 && { canvas: this.options.canvas },
315
647
  ...this.options.layer !== void 0 && { layer: this.options.layer },
316
648
  ...this.options.canvasClass !== void 0 && { canvasClass: this.options.canvasClass },
317
- ...this.options.canvasStyle !== void 0 && { canvasStyle: this.options.canvasStyle }
649
+ ...this.options.canvasStyle !== void 0 && { canvasStyle: this.options.canvasStyle },
650
+ // Only a missing target/canvas may fall back to a detached canvas; a
651
+ // target that fails to resolve must still report TargetNotFoundError.
652
+ detached: this.fixedResolution !== null && this.options.target === void 0 && this.options.canvas === void 0
318
653
  });
319
654
  if (!this.dom) {
320
- this.failUnsupported(new TargetNotFoundError("Target or canvas not found."));
655
+ this.failUnsupported(
656
+ new TargetNotFoundError(
657
+ "Target or canvas not found. Pass `resolution` to render without a target."
658
+ )
659
+ );
321
660
  return;
322
661
  }
323
662
  let gl;
@@ -333,9 +672,23 @@ var RuntimeHost = class {
333
672
  this.failUnsupported(new UnsupportedError("WebGL is not supported."));
334
673
  return;
335
674
  }
675
+ if (this.fixedResolution) {
676
+ try {
677
+ assertWithinLimits(gl, this.fixedResolution);
678
+ } catch (error) {
679
+ this.failUnsupported(
680
+ error instanceof Error ? error : new UnsupportedError(String(error))
681
+ );
682
+ return;
683
+ }
684
+ if (this.options.dpr !== void 0 || this.options.maxDpr !== void 0) {
685
+ this.debugWarn("`dpr` and `maxDpr` are ignored while `resolution` is set.");
686
+ }
687
+ }
336
688
  this.context = gl;
337
689
  this.currentStatus = "loading";
338
690
  try {
691
+ this.applyObjectFit();
339
692
  this.setupDomObservers();
340
693
  this.setupContextEvents();
341
694
  this.measureSize();
@@ -346,22 +699,24 @@ var RuntimeHost = class {
346
699
  this.options.onReady?.();
347
700
  if (this.running || (this.options.autoStart ?? true)) {
348
701
  this.start();
349
- } else if (this.options.renderMode === "demand") {
702
+ } else if (this.shouldRenderOnDemand) {
350
703
  this.render();
351
704
  }
352
705
  } catch (error) {
353
- this.fail(error instanceof Error ? error : new Error(String(error)));
706
+ if (error instanceof UnsupportedError) this.failUnsupported(error);
707
+ else this.fail(error instanceof Error ? error : new Error(String(error)));
354
708
  }
355
709
  }
356
710
  setupDomObservers() {
357
711
  if (!this.dom) return;
358
- if (this.options.autoResize ?? true) {
712
+ const detached = this.dom.detached === true;
713
+ if (!detached && (this.options.autoResize ?? true)) {
359
714
  this.resizeObserver = new ResizeObserver(() => {
360
715
  this.resize();
361
716
  });
362
717
  this.resizeObserver.observe(this.dom.target);
363
718
  }
364
- if (this.options.pauseWhenOffscreen ?? true) {
719
+ if (!detached && (this.options.pauseWhenOffscreen ?? true)) {
365
720
  this.intersectionObserver = new IntersectionObserver((entries) => {
366
721
  const entry = entries[0];
367
722
  this.setGate("offscreen", !entry?.isIntersecting);
@@ -379,22 +734,25 @@ var RuntimeHost = class {
379
734
  });
380
735
  }
381
736
  this.setupMotionObserver();
737
+ if (detached) return;
382
738
  const onPointerMove = (event) => {
383
739
  if (!this.dom) return;
384
740
  const pointerEvent = event;
385
741
  const rect = this.dom.target.getBoundingClientRect();
386
- const x = pointerEvent.clientX - rect.left;
387
- const y = pointerEvent.clientY - rect.top;
742
+ const x = pointerEvent.clientX - rect.left - this.contentOffsetX;
743
+ const y = pointerEvent.clientY - rect.top - this.contentOffsetY;
744
+ const contentWidth = this.fixedResolution ? this.width / this.pixelRatio : rect.width;
745
+ const contentHeight = this.fixedResolution ? this.height / this.pixelRatio : rect.height;
388
746
  this.pointer.x = x * this.pixelRatio;
389
- this.pointer.y = (rect.height - y) * this.pixelRatio;
390
- this.pointer.uvX = rect.width > 0 ? x / rect.width : 0;
391
- this.pointer.uvY = rect.height > 0 ? 1 - y / rect.height : 0;
747
+ this.pointer.y = (contentHeight - y) * this.pixelRatio;
748
+ this.pointer.uvX = contentWidth > 0 ? x / contentWidth : 0;
749
+ this.pointer.uvY = contentHeight > 0 ? 1 - y / contentHeight : 0;
392
750
  this.pointer.active = 1;
393
- if (this.options.renderMode === "demand") this.render();
751
+ if (this.shouldRenderOnDemand) this.render();
394
752
  };
395
753
  const onPointerLeave = () => {
396
754
  this.pointer.active = 0;
397
- if (this.options.renderMode === "demand") this.render();
755
+ if (this.shouldRenderOnDemand) this.render();
398
756
  };
399
757
  this.dom.target.addEventListener("pointermove", onPointerMove);
400
758
  this.dom.target.addEventListener("pointerenter", onPointerMove);
@@ -416,7 +774,7 @@ var RuntimeHost = class {
416
774
  this.reducedMotionActive = event.matches;
417
775
  if (this.options.respectReducedMotion) {
418
776
  this.setGate("reducedMotion", event.matches);
419
- } else if (this.options.renderMode === "demand") {
777
+ } else if (this.shouldRenderOnDemand) {
420
778
  this.render();
421
779
  }
422
780
  };
@@ -445,7 +803,7 @@ var RuntimeHost = class {
445
803
  try {
446
804
  await this.options.onContextRestored?.(gl, this.width, this.height);
447
805
  if (this.contextRestoredShouldRun) this.start();
448
- else this.render();
806
+ else if (!this.isManual) this.render();
449
807
  } catch (error) {
450
808
  this.fail(error instanceof Error ? error : new Error(String(error)));
451
809
  }
@@ -477,6 +835,27 @@ var RuntimeHost = class {
477
835
  }
478
836
  }
479
837
  };
838
+ var normalizeResolution = (resolution) => {
839
+ const [width, height] = resolution;
840
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width < 1 || height < 1) {
841
+ throw new RangeError(`Invalid resolution: ${width}x${height}`);
842
+ }
843
+ return [Math.round(width), Math.round(height)];
844
+ };
845
+ var assertWithinLimits = (gl, resolution) => {
846
+ const [width, height] = resolution;
847
+ const viewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
848
+ const renderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);
849
+ const limit = (axis) => Math.min(
850
+ viewportDims?.[axis] ?? Number.POSITIVE_INFINITY,
851
+ typeof renderbufferSize === "number" ? renderbufferSize : Number.POSITIVE_INFINITY
852
+ );
853
+ if (width > limit(0) || height > limit(1)) {
854
+ throw new UnsupportedError(
855
+ `Resolution ${width}x${height} exceeds this GPU's limit of ${limit(0)}x${limit(1)}.`
856
+ );
857
+ }
858
+ };
480
859
 
481
860
  // src/internal/names.ts
482
861
  var toUniformName = (name) => name.startsWith("u_") ? name : `u_${name}`;
@@ -539,78 +918,209 @@ var isTextureSource = (input) => typeof HTMLImageElement !== "undefined" && (inp
539
918
  var getSourceWidth = (source) => "naturalWidth" in source ? source.naturalWidth : source.width;
540
919
  var getSourceHeight = (source) => "naturalHeight" in source ? source.naturalHeight : source.height;
541
920
 
542
- // src/internal/webgl.ts
543
- var defaultVertexShader = `
544
- attribute vec2 a_position;
545
- varying vec2 v_uv;
546
-
547
- void main() {
548
- v_uv = a_position * 0.5 + 0.5;
549
- gl_Position = vec4(a_position, 0.0, 1.0);
550
- }
551
- `;
552
- var defaultVertexShader300 = `#version 300 es
553
- in vec2 a_position;
554
- out vec2 v_uv;
921
+ // src/internal/textureSet.ts
922
+ var createTextureSet = (inputs = {}) => {
923
+ const textureInputs = new Map(Object.entries(inputs));
924
+ let textures = /* @__PURE__ */ new Map();
925
+ const units = /* @__PURE__ */ new Map();
926
+ const versions = /* @__PURE__ */ new Map();
927
+ const reserveUnit = (gl, name) => {
928
+ const existing = units.get(name);
929
+ if (existing !== void 0) return existing;
930
+ const maxUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
931
+ const used = new Set(units.values());
932
+ for (let unit = 0; unit < maxUnits; unit += 1) {
933
+ if (!used.has(unit)) {
934
+ units.set(name, unit);
935
+ return unit;
936
+ }
937
+ }
938
+ throw new TextureLoadError(
939
+ `Texture unit limit exceeded while loading texture: ${name}`
940
+ );
941
+ };
942
+ return {
943
+ get textures() {
944
+ return textures;
945
+ },
946
+ hasInput(name) {
947
+ return textureInputs.has(name);
948
+ },
949
+ reserveUnit,
950
+ async loadAll(gl, isCancelled) {
951
+ const loaded = /* @__PURE__ */ new Map();
952
+ for (const [name, input] of textureInputs) {
953
+ if (isCancelled()) break;
954
+ const unit = reserveUnit(gl, name);
955
+ loaded.set(name, await loadTexture(gl, name, input, unit));
956
+ }
957
+ for (const texture of textures.values()) {
958
+ gl.deleteTexture(texture.texture);
959
+ }
960
+ textures = loaded;
961
+ },
962
+ async set(gl, name, input, isCancelled) {
963
+ const version = (versions.get(name) ?? 0) + 1;
964
+ versions.set(name, version);
965
+ try {
966
+ const unit = reserveUnit(gl, name);
967
+ const loaded = await loadTexture(gl, name, input, unit);
968
+ if (isCancelled() || versions.get(name) !== version) {
969
+ gl.deleteTexture(loaded.texture);
970
+ return null;
971
+ }
972
+ const previous = textures.get(name);
973
+ textureInputs.set(name, input);
974
+ textures.set(name, loaded);
975
+ if (previous) gl.deleteTexture(previous.texture);
976
+ return loaded;
977
+ } catch (error) {
978
+ if (!textures.has(name) && !textureInputs.has(name)) {
979
+ units.delete(name);
980
+ }
981
+ throw error;
982
+ }
983
+ },
984
+ bind(gl, locations, onMissing) {
985
+ for (const texture of textures.values()) {
986
+ const unit = units.get(texture.name);
987
+ if (unit === void 0) continue;
988
+ const location = locations.get(texture.uniformName);
989
+ if (!location) {
990
+ onMissing?.(texture.uniformName);
991
+ continue;
992
+ }
993
+ gl.activeTexture(gl.TEXTURE0 + unit);
994
+ gl.bindTexture(gl.TEXTURE_2D, texture.texture);
995
+ gl.uniform1i(location, unit);
996
+ }
997
+ },
998
+ dispose(gl) {
999
+ for (const texture of textures.values()) {
1000
+ gl.deleteTexture(texture.texture);
1001
+ }
1002
+ }
1003
+ };
1004
+ };
555
1005
 
556
- void main() {
557
- v_uv = a_position * 0.5 + 0.5;
558
- gl_Position = vec4(a_position, 0.0, 1.0);
559
- }
560
- `;
561
- var isGLSL300 = (src) => /^\s*#version\s+300\s+es\b/.test(src);
562
- var createProgram = (gl, vertexSource, fragmentSource) => {
563
- const vertex = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
564
- const fragment = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
565
- const program = gl.createProgram();
566
- if (!program) {
567
- throw new ShaderCompileError("Failed to create WebGL program.");
1006
+ // src/internal/uniforms.ts
1007
+ var normalizeUniform = (input) => {
1008
+ if (typeof input === "number") {
1009
+ return { type: "float", value: [input] };
568
1010
  }
569
- gl.attachShader(program, vertex);
570
- gl.attachShader(program, fragment);
571
- gl.linkProgram(program);
572
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
573
- const log = gl.getProgramInfoLog(program) ?? "Unknown program link error.";
574
- gl.deleteProgram(program);
575
- gl.deleteShader(vertex);
576
- gl.deleteShader(fragment);
577
- throw new ShaderCompileError(log);
1011
+ if (input instanceof Float32Array) {
1012
+ if (input.length === 9) return { type: "mat3", value: input };
1013
+ if (input.length === 16) return { type: "mat4", value: input };
1014
+ if (input.length === 2) return { type: "vec2", value: input };
1015
+ if (input.length === 3) return { type: "vec3", value: input };
1016
+ if (input.length === 4) return { type: "vec4", value: input };
578
1017
  }
579
- gl.deleteShader(vertex);
580
- gl.deleteShader(fragment);
581
- return program;
1018
+ if (isExplicitUniform(input)) {
1019
+ return { type: input.type, value: input.value };
1020
+ }
1021
+ if (Array.isArray(input)) {
1022
+ if (input.length === 2) return { type: "vec2", value: input };
1023
+ if (input.length === 3) return { type: "vec3", value: input };
1024
+ if (input.length === 4) return { type: "vec4", value: input };
1025
+ }
1026
+ return { type: "float", value: [0] };
582
1027
  };
583
- var createFullscreenBuffer = (gl, program) => {
584
- const buffer = gl.createBuffer();
585
- if (!buffer) {
586
- throw new ShaderCompileError("Failed to create fullscreen buffer.");
1028
+ var applyUniform = (gl, location, uniform) => {
1029
+ const value = uniform.value;
1030
+ switch (uniform.type) {
1031
+ case "float":
1032
+ gl.uniform1f(location, value[0] ?? 0);
1033
+ break;
1034
+ case "vec2":
1035
+ gl.uniform2f(location, value[0] ?? 0, value[1] ?? 0);
1036
+ break;
1037
+ case "vec3":
1038
+ gl.uniform3f(location, value[0] ?? 0, value[1] ?? 0, value[2] ?? 0);
1039
+ break;
1040
+ case "vec4":
1041
+ gl.uniform4f(
1042
+ location,
1043
+ value[0] ?? 0,
1044
+ value[1] ?? 0,
1045
+ value[2] ?? 0,
1046
+ value[3] ?? 0
1047
+ );
1048
+ break;
1049
+ case "mat3":
1050
+ gl.uniformMatrix3fv(location, false, value);
1051
+ break;
1052
+ case "mat4":
1053
+ gl.uniformMatrix4fv(location, false, value);
1054
+ break;
587
1055
  }
588
- const location = gl.getAttribLocation(program, "a_position");
589
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
590
- gl.bufferData(
591
- gl.ARRAY_BUFFER,
592
- new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
593
- gl.STATIC_DRAW
594
- );
595
- if (location >= 0) {
596
- gl.enableVertexAttribArray(location);
597
- gl.vertexAttribPointer(location, 2, gl.FLOAT, false, 0, 0);
1056
+ };
1057
+ var applyProgramUniform = (gl, program, location, uniform) => {
1058
+ gl.useProgram(program);
1059
+ applyUniform(gl, location, uniform);
1060
+ };
1061
+ var applyUniforms = (gl, locations, cache, onMissing) => {
1062
+ for (const [name, uniform] of cache) {
1063
+ const location = locations.get(name);
1064
+ if (!location) {
1065
+ onMissing?.(name);
1066
+ continue;
1067
+ }
1068
+ applyUniform(gl, location, uniform);
598
1069
  }
599
- return buffer;
600
1070
  };
601
- var compileShader = (gl, type, source) => {
602
- const shader = gl.createShader(type);
603
- if (!shader) {
604
- throw new ShaderCompileError("Failed to create WebGL shader.");
1071
+ var collectUniformLocations = (gl, program) => {
1072
+ const locations = /* @__PURE__ */ new Map();
1073
+ const count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
1074
+ for (let index = 0; index < count; index += 1) {
1075
+ const active = gl.getActiveUniform(program, index);
1076
+ if (!active) continue;
1077
+ const name = active.name.replace(/\[0\]$/, "");
1078
+ const location = gl.getUniformLocation(program, name);
1079
+ if (location) {
1080
+ locations.set(name, location);
1081
+ }
605
1082
  }
606
- gl.shaderSource(shader, source);
607
- gl.compileShader(shader);
608
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
609
- const log = gl.getShaderInfoLog(shader) ?? "Unknown shader compile error.";
610
- gl.deleteShader(shader);
611
- throw new ShaderCompileError(log);
1083
+ return locations;
1084
+ };
1085
+ var createUniformCache = (uniforms) => {
1086
+ const cache = /* @__PURE__ */ new Map();
1087
+ for (const [name, value] of Object.entries(uniforms ?? {})) {
1088
+ cache.set(toUniformName(name), normalizeUniform(value));
612
1089
  }
613
- return shader;
1090
+ return cache;
1091
+ };
1092
+ var isExplicitUniform = (input) => {
1093
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
1094
+ return "type" in input && "value" in input;
1095
+ };
1096
+
1097
+ // src/internal/fullscreenProgram.ts
1098
+ var createFullscreenProgram = (gl, fragment, vertex) => {
1099
+ const defaultVertex = isGLSL300(fragment) ? defaultVertexShader300 : defaultVertexShader;
1100
+ const program = createProgram(gl, vertex ?? defaultVertex, fragment);
1101
+ gl.useProgram(program);
1102
+ const buffer = createFullscreenBuffer(gl, program);
1103
+ const attribLocation = gl.getAttribLocation(program, "a_position");
1104
+ const locations = collectUniformLocations(gl, program);
1105
+ return {
1106
+ program,
1107
+ locations,
1108
+ use() {
1109
+ gl.useProgram(program);
1110
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1111
+ if (attribLocation >= 0) {
1112
+ gl.enableVertexAttribArray(attribLocation);
1113
+ gl.vertexAttribPointer(attribLocation, 2, gl.FLOAT, false, 0, 0);
1114
+ }
1115
+ },
1116
+ draw() {
1117
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
1118
+ },
1119
+ dispose() {
1120
+ gl.deleteBuffer(buffer);
1121
+ gl.deleteProgram(program);
1122
+ }
1123
+ };
614
1124
  };
615
1125
 
616
1126
  // src/internal/blit.ts
@@ -633,7 +1143,6 @@ void main() {
633
1143
  fragColor = texture(u_texture, v_uv);
634
1144
  }
635
1145
  `;
636
- var isWebGL2Context = (gl) => typeof gl.createVertexArray === "function";
637
1146
  var createBlitter = (gl) => {
638
1147
  const program = createProgram(
639
1148
  gl,
@@ -666,6 +1175,173 @@ var createBlitter = (gl) => {
666
1175
  }
667
1176
  };
668
1177
  };
1178
+
1179
+ // src/internal/renderTarget.ts
1180
+ var DEFAULT_CLEAR_COLOR = [0, 0, 0, 0];
1181
+ var supportsRenderTargetFormat = (gl, format) => {
1182
+ if (format === "rgba8") return true;
1183
+ return isWebGL2Context(gl) && gl.getExtension("EXT_color_buffer_float") !== null;
1184
+ };
1185
+ var resolveRenderTargetFormat = (gl, format = "rgba8", formatFallback = true) => {
1186
+ if (supportsRenderTargetFormat(gl, format)) return format;
1187
+ const reason = `${format} render targets need WebGL2 with EXT_color_buffer_float.`;
1188
+ if (!formatFallback) throw new UnsupportedError(reason);
1189
+ console.warn(`[frapx] ${reason} Falling back to rgba8.`);
1190
+ return "rgba8";
1191
+ };
1192
+ var createRenderTarget = (gl, width, height, options = {}) => {
1193
+ const format = resolveRenderTargetFormat(
1194
+ gl,
1195
+ options.format,
1196
+ options.formatFallback
1197
+ );
1198
+ return new Target(gl, width, height, format, options);
1199
+ };
1200
+ var createPingPong = (gl, width, height, options = {}) => {
1201
+ const format = resolveRenderTargetFormat(
1202
+ gl,
1203
+ options.format,
1204
+ options.formatFallback
1205
+ );
1206
+ let read = new Target(gl, width, height, format, options);
1207
+ let write;
1208
+ try {
1209
+ write = new Target(gl, width, height, format, options);
1210
+ } catch (error) {
1211
+ read.dispose();
1212
+ throw error;
1213
+ }
1214
+ return {
1215
+ get read() {
1216
+ return read;
1217
+ },
1218
+ get write() {
1219
+ return write;
1220
+ },
1221
+ format,
1222
+ swap() {
1223
+ const previous = read;
1224
+ read = write;
1225
+ write = previous;
1226
+ },
1227
+ resize(nextWidth, nextHeight) {
1228
+ read.resize(nextWidth, nextHeight);
1229
+ write.resize(nextWidth, nextHeight);
1230
+ },
1231
+ clear() {
1232
+ read.clear();
1233
+ write.clear();
1234
+ },
1235
+ dispose() {
1236
+ read.dispose();
1237
+ write.dispose();
1238
+ }
1239
+ };
1240
+ };
1241
+ var Target = class {
1242
+ constructor(gl, width, height, format, options) {
1243
+ this.gl = gl;
1244
+ this.format = format;
1245
+ this.options = options;
1246
+ this.width = width;
1247
+ this.height = height;
1248
+ this.allocate();
1249
+ }
1250
+ resize(width, height) {
1251
+ if (width === this.width && height === this.height) return;
1252
+ this.release();
1253
+ this.width = width;
1254
+ this.height = height;
1255
+ this.allocate();
1256
+ }
1257
+ clear() {
1258
+ const gl = this.gl;
1259
+ const [r, g, b, a] = this.options.clearColor ?? DEFAULT_CLEAR_COLOR;
1260
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
1261
+ gl.viewport(0, 0, this.width, this.height);
1262
+ gl.clearColor(r, g, b, a);
1263
+ gl.clear(gl.COLOR_BUFFER_BIT);
1264
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1265
+ }
1266
+ dispose() {
1267
+ this.release();
1268
+ }
1269
+ allocate() {
1270
+ const gl = this.gl;
1271
+ const texture = gl.createTexture();
1272
+ if (!texture) {
1273
+ throw new ShaderCompileError("Failed to create render target texture.");
1274
+ }
1275
+ const framebuffer = gl.createFramebuffer();
1276
+ if (!framebuffer) {
1277
+ gl.deleteTexture(texture);
1278
+ throw new ShaderCompileError("Failed to create render target framebuffer.");
1279
+ }
1280
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1281
+ if (this.format !== "rgba8" && isWebGL2Context(gl)) {
1282
+ const half = this.format === "rgba16f";
1283
+ gl.texImage2D(
1284
+ gl.TEXTURE_2D,
1285
+ 0,
1286
+ half ? gl.RGBA16F : gl.RGBA32F,
1287
+ this.width,
1288
+ this.height,
1289
+ 0,
1290
+ gl.RGBA,
1291
+ half ? gl.HALF_FLOAT : gl.FLOAT,
1292
+ null
1293
+ );
1294
+ } else {
1295
+ gl.texImage2D(
1296
+ gl.TEXTURE_2D,
1297
+ 0,
1298
+ gl.RGBA,
1299
+ this.width,
1300
+ this.height,
1301
+ 0,
1302
+ gl.RGBA,
1303
+ gl.UNSIGNED_BYTE,
1304
+ null
1305
+ );
1306
+ }
1307
+ const wrap = getWrap2(gl, this.options.wrap ?? "clamp");
1308
+ const linear = this.options.filter !== "nearest" && (this.format !== "rgba32f" || gl.getExtension("OES_texture_float_linear") !== null);
1309
+ const filter = linear ? gl.LINEAR : gl.NEAREST;
1310
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
1311
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
1312
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
1313
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
1314
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
1315
+ gl.framebufferTexture2D(
1316
+ gl.FRAMEBUFFER,
1317
+ gl.COLOR_ATTACHMENT0,
1318
+ gl.TEXTURE_2D,
1319
+ texture,
1320
+ 0
1321
+ );
1322
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
1323
+ if (status !== gl.FRAMEBUFFER_COMPLETE) {
1324
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1325
+ gl.deleteFramebuffer(framebuffer);
1326
+ gl.deleteTexture(texture);
1327
+ throw new ShaderCompileError(
1328
+ `Render target framebuffer (${this.format}) is incomplete: ${status}`
1329
+ );
1330
+ }
1331
+ this.texture = texture;
1332
+ this.framebuffer = framebuffer;
1333
+ this.clear();
1334
+ }
1335
+ release() {
1336
+ this.gl.deleteFramebuffer(this.framebuffer);
1337
+ this.gl.deleteTexture(this.texture);
1338
+ }
1339
+ };
1340
+ var getWrap2 = (gl, wrap) => {
1341
+ if (wrap === "repeat") return gl.REPEAT;
1342
+ if (wrap === "mirror") return gl.MIRRORED_REPEAT;
1343
+ return gl.CLAMP_TO_EDGE;
1344
+ };
669
1345
  export {
670
1346
  DestroyedError,
671
1347
  ShaderCompileError,
@@ -673,10 +1349,19 @@ export {
673
1349
  TargetNotFoundError,
674
1350
  TextureLoadError,
675
1351
  UnsupportedError,
1352
+ applyProgramUniform,
1353
+ applyUniform,
1354
+ applyUniforms,
1355
+ collectUniformLocations,
676
1356
  createBlitter,
677
1357
  createFullscreenBuffer,
1358
+ createFullscreenProgram,
1359
+ createPingPong,
678
1360
  createProgram,
1361
+ createRenderTarget,
679
1362
  createRuntimeHost,
1363
+ createTextureSet,
1364
+ createUniformCache,
680
1365
  defaultVertexShader,
681
1366
  defaultVertexShader300,
682
1367
  fromUniformName,
@@ -685,11 +1370,16 @@ export {
685
1370
  hexToRgb,
686
1371
  hexToRgba,
687
1372
  isGLSL300,
1373
+ isWebGL2Context,
688
1374
  loadTexture,
1375
+ normalizeUniform,
689
1376
  resizeCanvasToTarget,
690
1377
  resolveDpr,
691
1378
  resolveElement,
1379
+ resolveRenderTargetFormat,
692
1380
  setupCanvas,
693
- toUniformName
1381
+ supportsRenderTargetFormat,
1382
+ toUniformName,
1383
+ waitForGpu
694
1384
  };
695
1385
  //# sourceMappingURL=index.js.map