@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/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/index.d.ts +238 -5
- package/dist/index.js +796 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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)
|
|
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.
|
|
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.
|
|
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
|
-
|
|
255
|
-
|
|
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")
|
|
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(
|
|
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.
|
|
702
|
+
} else if (this.shouldRenderOnDemand) {
|
|
350
703
|
this.render();
|
|
351
704
|
}
|
|
352
705
|
} catch (error) {
|
|
353
|
-
|
|
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
|
-
|
|
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 = (
|
|
390
|
-
this.pointer.uvX =
|
|
391
|
-
this.pointer.uvY =
|
|
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.
|
|
751
|
+
if (this.shouldRenderOnDemand) this.render();
|
|
394
752
|
};
|
|
395
753
|
const onPointerLeave = () => {
|
|
396
754
|
this.pointer.active = 0;
|
|
397
|
-
if (this.
|
|
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.
|
|
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/
|
|
543
|
-
var
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
gl.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
|
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
|
-
|
|
1381
|
+
supportsRenderTargetFormat,
|
|
1382
|
+
toUniformName,
|
|
1383
|
+
waitForGpu
|
|
694
1384
|
};
|
|
695
1385
|
//# sourceMappingURL=index.js.map
|