q5 2.26.0 → 2.27.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.
Files changed (5) hide show
  1. package/deno.json +1 -1
  2. package/package.json +1 -1
  3. package/q5.d.ts +227 -36
  4. package/q5.js +154 -113
  5. package/q5.min.js +1 -1
package/q5.js CHANGED
@@ -160,7 +160,7 @@ function Q5(scope, parent, renderer) {
160
160
  };
161
161
 
162
162
  $.frameRate = (hz) => {
163
- if (hz != $._targetFrameRate) {
163
+ if (hz && hz != $._targetFrameRate) {
164
164
  $._targetFrameRate = hz;
165
165
  $._targetFrameDuration = 1000 / hz;
166
166
 
@@ -256,18 +256,8 @@ function Q5(scope, parent, renderer) {
256
256
  };
257
257
 
258
258
  let t = globalScope || $;
259
- $._isTouchAware = t.touchStarted || t.touchMoved || t.touchEnded;
260
259
 
261
- if ($._isGlobal) {
262
- $.preload = t.preload;
263
- $.setup = t.setup;
264
- $.draw = t.draw;
265
- $.postProcess = t.postProcess;
266
- }
267
- $.preload ??= () => {};
268
- $.setup ??= () => {};
269
- $.draw ??= () => {};
270
- $.postProcess ??= () => {};
260
+ $._isTouchAware = t.touchStarted || t.touchMoved || t.touchEnded;
271
261
 
272
262
  let userFns = [
273
263
  'setup',
@@ -277,6 +267,7 @@ function Q5(scope, parent, renderer) {
277
267
  'mouseReleased',
278
268
  'mouseDragged',
279
269
  'mouseClicked',
270
+ 'doubleClicked',
280
271
  'mouseWheel',
281
272
  'keyPressed',
282
273
  'keyReleased',
@@ -286,12 +277,15 @@ function Q5(scope, parent, renderer) {
286
277
  'touchEnded',
287
278
  'windowResized'
288
279
  ];
289
- for (let k of userFns) {
290
- if (!t[k]) $[k] = () => {};
280
+ // shim if undefined
281
+ for (let name of userFns) $[name] ??= () => {};
282
+
283
+ function wrapWithFES(fn) {
284
+ if (!t[fn]) $[fn] = () => {};
291
285
  else if ($._isGlobal) {
292
- $[k] = (event) => {
286
+ $[fn] = (event) => {
293
287
  try {
294
- return t[k](event);
288
+ return t[fn](event);
295
289
  } catch (e) {
296
290
  if ($._fes) $._fes(e);
297
291
  throw e;
@@ -300,28 +294,24 @@ function Q5(scope, parent, renderer) {
300
294
  }
301
295
  }
302
296
 
303
- async function _setup() {
304
- $._startDone = true;
297
+ async function _start() {
298
+ wrapWithFES('preload');
299
+ $.preload();
305
300
  await Promise.all($._preloadPromises);
306
301
  if ($._g) await Promise.all($._g._preloadPromises);
302
+
303
+ for (let name of userFns) wrapWithFES(name);
304
+
305
+ $.draw = t.draw || (() => {});
306
+
307
307
  millisStart = performance.now();
308
308
  await $.setup();
309
309
  $._setupDone = true;
310
- if ($.frameCount) return;
311
310
  if ($.ctx === null) $.createCanvas(200, 200);
311
+ if ($.frameCount) return;
312
312
  raf($._draw);
313
313
  }
314
314
 
315
- function _start() {
316
- try {
317
- $.preload();
318
- if (!$._startDone) _setup();
319
- } catch (e) {
320
- if ($._fes) $._fes(e);
321
- throw e;
322
- }
323
- }
324
-
325
315
  if (autoLoaded) _start();
326
316
  else setTimeout(_start, 32);
327
317
  }
@@ -430,21 +420,44 @@ Q5.modules.canvas = ($, q) => {
430
420
 
431
421
  if ($._scope != 'image') {
432
422
  if ($._scope == 'graphics') $._pixelDensity = this._pixelDensity;
433
- else if (window.IntersectionObserver) {
434
- let wasObserved = false;
435
- new IntersectionObserver((e) => {
436
- c.visible = e[0].isIntersecting;
437
- if (!wasObserved) {
438
- $._wasLooping = $._loop;
439
- wasObserved = true;
440
- }
441
- if (c.visible) {
442
- if ($._wasLooping && !$._loop) $.loop();
443
- } else {
444
- $._wasLooping = $._loop;
445
- $.noLoop();
446
- }
447
- }).observe(c);
423
+ else if (!Q5._server) {
424
+ // the canvas can become detached from the DOM
425
+ // if the innerHTML of one of its parents is edited
426
+ // check if canvas is still attached to the DOM
427
+ let el = c;
428
+ while (el && el.parentElement != document.body) {
429
+ el = el.parentElement;
430
+ }
431
+ if (!el) {
432
+ // reattach canvas to the DOM
433
+ document.getElementById(c.id)?.remove();
434
+ addCanvas();
435
+ }
436
+
437
+ if (window.IntersectionObserver) {
438
+ let wasObserved = false;
439
+ new IntersectionObserver((e) => {
440
+ let isIntersecting = e[0].isIntersecting;
441
+
442
+ if (!isIntersecting) {
443
+ // the canvas might still be onscreen, just behind other elements
444
+ let r = c.getBoundingClientRect();
445
+ c.visible = r.top < window.innerHeight && r.bottom > 0 && r.left < window.innerWidth && r.right > 0;
446
+ } else c.visible = true;
447
+
448
+ if (!wasObserved) {
449
+ $._wasLooping = $._loop;
450
+ wasObserved = true;
451
+ }
452
+
453
+ if (c.visible) {
454
+ if ($._wasLooping && !$._loop) $.loop();
455
+ } else {
456
+ $._wasLooping = $._loop;
457
+ $.noLoop();
458
+ }
459
+ }).observe(c);
460
+ }
448
461
  }
449
462
  }
450
463
 
@@ -609,7 +622,9 @@ Q5.modules.canvas = ($, q) => {
609
622
  $.popStyles = () => {
610
623
  let styles = $._styles.pop();
611
624
  for (let s of $._styleNames) $[s] = styles[s];
612
- q.Color = styles.Color;
625
+
626
+ if ($._webgpu) $.colorMode($._colorMode, $._colorFormat);
627
+ else q.Color = styles.Color;
613
628
  };
614
629
 
615
630
  if (window && $._scope != 'graphics') {
@@ -3091,7 +3106,12 @@ Q5.modules.input = ($, q) => {
3091
3106
 
3092
3107
  $._updateMouse = (e) => {
3093
3108
  if (e.changedTouches) return;
3094
- if (c) {
3109
+
3110
+ if (document.pointerLockElement) {
3111
+ // In pointer lock mode, update position based on movement
3112
+ q.mouseX += e.movementX;
3113
+ q.mouseY += e.movementY;
3114
+ } else if (c) {
3095
3115
  let rect = c.getBoundingClientRect();
3096
3116
  let sx = c.scrollWidth / $.width || 1;
3097
3117
  let sy = c.scrollHeight / $.height || 1;
@@ -3109,10 +3129,8 @@ Q5.modules.input = ($, q) => {
3109
3129
  q.moveY = e.movementY;
3110
3130
  };
3111
3131
 
3112
- let pressedInCanvas = 0;
3113
-
3114
3132
  $._onmousedown = (e) => {
3115
- pressedInCanvas++;
3133
+ if (!c?.visible) return;
3116
3134
  $._startAudio();
3117
3135
  $._updateMouse(e);
3118
3136
  q.mouseIsPressed = true;
@@ -3127,19 +3145,30 @@ Q5.modules.input = ($, q) => {
3127
3145
  };
3128
3146
 
3129
3147
  $._onmouseup = (e) => {
3148
+ if (!c?.visible) return;
3130
3149
  $._updateMouse(e);
3131
3150
  q.mouseIsPressed = false;
3132
3151
  $.mouseReleased(e);
3133
3152
  };
3134
3153
 
3135
3154
  $._onclick = (e) => {
3155
+ if (!c?.visible) return;
3136
3156
  $._updateMouse(e);
3137
3157
  q.mouseIsPressed = true;
3138
3158
  $.mouseClicked(e);
3139
3159
  q.mouseIsPressed = false;
3140
3160
  };
3141
3161
 
3162
+ $._ondblclick = (e) => {
3163
+ if (!c?.visible) return;
3164
+ $._updateMouse(e);
3165
+ q.mouseIsPressed = true;
3166
+ $.doubleClicked(e);
3167
+ q.mouseIsPressed = false;
3168
+ };
3169
+
3142
3170
  $._onwheel = (e) => {
3171
+ if (!c?.visible) return;
3143
3172
  $._updateMouse(e);
3144
3173
  e.delta = e.deltaY;
3145
3174
  if ($.mouseWheel(e) == false || $._noScroll) e.preventDefault();
@@ -3160,10 +3189,10 @@ Q5.modules.input = ($, q) => {
3160
3189
  $.noCursor = () => ($.canvas.style.cursor = 'none');
3161
3190
  $.noScroll = () => ($._noScroll = true);
3162
3191
 
3163
- if (window) {
3164
- $.lockMouse = document.body?.requestPointerLock;
3165
- $.unlockMouse = document.exitPointerLock;
3166
- }
3192
+ $.requestPointerLock = (unadjustedMovement = false) => {
3193
+ return document.body?.requestPointerLock({ unadjustedMovement });
3194
+ };
3195
+ $.exitPointerLock = () => document.exitPointerLock();
3167
3196
 
3168
3197
  $._onkeydown = (e) => {
3169
3198
  if (e.repeat) return;
@@ -3204,6 +3233,7 @@ Q5.modules.input = ($, q) => {
3204
3233
  }
3205
3234
 
3206
3235
  $._ontouchstart = (e) => {
3236
+ if (!c?.visible) return;
3207
3237
  $._startAudio();
3208
3238
  q.touches = [...e.touches].map(getTouchInfo);
3209
3239
  if (!$._isTouchAware) {
@@ -3217,6 +3247,7 @@ Q5.modules.input = ($, q) => {
3217
3247
  };
3218
3248
 
3219
3249
  $._ontouchmove = (e) => {
3250
+ if (!c?.visible) return;
3220
3251
  q.touches = [...e.touches].map(getTouchInfo);
3221
3252
  if (!$._isTouchAware) {
3222
3253
  q.mouseX = $.touches[0].x;
@@ -3227,6 +3258,7 @@ Q5.modules.input = ($, q) => {
3227
3258
  };
3228
3259
 
3229
3260
  $._ontouchend = (e) => {
3261
+ if (!c?.visible) return;
3230
3262
  q.touches = [...e.touches].map(getTouchInfo);
3231
3263
  if (!$._isTouchAware && !$.touches.length) {
3232
3264
  q.mouseIsPressed = false;
@@ -3235,11 +3267,18 @@ Q5.modules.input = ($, q) => {
3235
3267
  if (!$.touchEnded(e)) e.preventDefault();
3236
3268
  };
3237
3269
 
3238
- if (c) {
3239
- let l = c.addEventListener.bind(c);
3270
+ if (window) {
3271
+ let l = window.addEventListener;
3272
+ l('keydown', (e) => $._onkeydown(e), false);
3273
+ l('keyup', (e) => $._onkeyup(e), false);
3274
+
3240
3275
  l('mousedown', (e) => $._onmousedown(e));
3241
- l('wheel', (e) => $._onwheel(e));
3276
+ l('mousemove', (e) => $._onmousemove(e), false);
3277
+ l('mouseup', (e) => $._onmouseup(e));
3242
3278
  l('click', (e) => $._onclick(e));
3279
+ l('dblclick', (e) => $._ondblclick(e));
3280
+
3281
+ if (!c) l('wheel', (e) => $._onwheel(e));
3243
3282
 
3244
3283
  l('touchstart', (e) => $._ontouchstart(e));
3245
3284
  l('touchmove', (e) => $._ontouchmove(e));
@@ -3247,25 +3286,10 @@ Q5.modules.input = ($, q) => {
3247
3286
  l('touchcancel', (e) => $._ontouchend(e));
3248
3287
  }
3249
3288
 
3250
- if (window) {
3251
- let l = window.addEventListener;
3252
- l('keydown', (e) => $._onkeydown(e), false);
3253
- l('keyup', (e) => $._onkeyup(e), false);
3254
-
3255
- if (!c) {
3256
- l('mousedown', (e) => $._onmousedown(e));
3257
- l('wheel', (e) => $._onwheel(e));
3258
- l('click', (e) => $._onclick(e));
3259
- }
3260
-
3261
- l('mousemove', (e) => $._onmousemove(e), false);
3262
- l('mouseup', (e) => {
3263
- if (pressedInCanvas > 0) {
3264
- pressedInCanvas--;
3265
- $._onmouseup(e);
3266
- }
3267
- });
3268
- }
3289
+ // making the window level event listener for wheel events
3290
+ // not passive would be necessary to be able to use `e.preventDefault`
3291
+ // but browsers warn that it's bad for performance
3292
+ if (c) c.addEventListener('wheel', (e) => $._onwheel(e));
3269
3293
  };
3270
3294
  Q5.modules.math = ($, q) => {
3271
3295
  $.RADIANS = 0;
@@ -4134,6 +4158,10 @@ Q5.modules.sound = ($, q) => {
4134
4158
  return Q5.aud.resume();
4135
4159
  }
4136
4160
  };
4161
+
4162
+ $.outputVolume = (level) => {
4163
+ if (Q5.soundOut) Q5.soundOut.gain.value = level;
4164
+ };
4137
4165
  };
4138
4166
 
4139
4167
  if (window.OfflineAudioContext) {
@@ -4183,6 +4211,7 @@ Q5.Sound = class {
4183
4211
  source.onended = () => {
4184
4212
  if (!this.paused) {
4185
4213
  this.ended = true;
4214
+ if (this._onended) this._onended();
4186
4215
  this.sources.delete(source);
4187
4216
  }
4188
4217
  };
@@ -4278,6 +4307,9 @@ Q5.Sound = class {
4278
4307
  isLooping() {
4279
4308
  return this._loop;
4280
4309
  }
4310
+ onended(cb) {
4311
+ this._onended = cb;
4312
+ }
4281
4313
  };
4282
4314
  Q5.modules.util = ($, q) => {
4283
4315
  $._loadFile = (url, cb, type) => {
@@ -4346,7 +4378,7 @@ Q5.modules.util = ($, q) => {
4346
4378
  name = name || 'untitled';
4347
4379
  ext = ext || 'png';
4348
4380
  if (imgRegex.test(ext)) {
4349
- if ($.canvas?.renderer == 'webgpu' && data.canvas.renderer == 'c2d') {
4381
+ if ($.canvas?.renderer == 'webgpu' && data.canvas?.renderer == 'c2d') {
4350
4382
  data = await $._g._saveCanvas(data, ext);
4351
4383
  } else {
4352
4384
  data = await $._saveCanvas(data, ext);
@@ -4371,8 +4403,9 @@ Q5.modules.util = ($, q) => {
4371
4403
  if (!a || (typeof a == 'string' && (!b || (!c && b.length < 5)))) {
4372
4404
  c = b;
4373
4405
  b = a;
4374
- a = $.canvas;
4406
+ a = $;
4375
4407
  }
4408
+ if (a == $.canvas) a = $;
4376
4409
  if (c) saveFile(a, b, c);
4377
4410
  else if (b) {
4378
4411
  let lastDot = b.lastIndexOf('.');
@@ -4949,6 +4982,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
4949
4982
  createMainView();
4950
4983
  };
4951
4984
 
4985
+ // since these values are checked so often in `addColor`,
4986
+ // they're stored in local variables for better performance
4952
4987
  let usingRGB = true,
4953
4988
  colorFormat = 1;
4954
4989
 
@@ -5473,7 +5508,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5473
5508
  }
5474
5509
  };
5475
5510
 
5476
- $._finishRender = async () => {
5511
+ $._finishRender = () => {
5477
5512
  pass.end();
5478
5513
 
5479
5514
  pass = encoder.beginRenderPass({
@@ -5505,12 +5540,6 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5505
5540
  Q5.device.queue.submit([encoder.finish()]);
5506
5541
  $._pass = pass = encoder = null;
5507
5542
 
5508
- // destroy buffers
5509
- Q5.device.queue.onSubmittedWorkDone().then(() => {
5510
- for (let b of $._buffers) b.destroy();
5511
- $._buffers = [];
5512
- });
5513
-
5514
5543
  // clear the stacks for the next frame
5515
5544
  drawStack.splice(0, drawStack.length);
5516
5545
  colorIndex = 1;
@@ -5518,9 +5547,15 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5518
5547
  matrices = [matrices[0]];
5519
5548
  matricesIndexStack = [];
5520
5549
 
5521
- $.texture = frameA;
5550
+ $._texture = frameA;
5522
5551
 
5523
5552
  for (let m of $._hooks.postRender) m();
5553
+
5554
+ // destroy buffers
5555
+ Q5.device.queue.onSubmittedWorkDone().then(() => {
5556
+ for (let b of $._buffers) b.destroy();
5557
+ $._buffers = [];
5558
+ });
5524
5559
  };
5525
5560
  };
5526
5561
 
@@ -6020,7 +6055,10 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6020
6055
  };
6021
6056
 
6022
6057
  let curveSegments = 20;
6023
- $.curveDetail = (x) => (curveSegments = x);
6058
+ $.curveDetail = (v) => (curveSegments = v);
6059
+
6060
+ let bezierSegments = 20;
6061
+ $.bezierDetail = (v) => (bezierSegments = v);
6024
6062
 
6025
6063
  let shapeVertCount;
6026
6064
  let sv = []; // shape vertices
@@ -6052,7 +6090,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6052
6090
  let startX = sv[prevIndex];
6053
6091
  let startY = sv[prevIndex + 1];
6054
6092
 
6055
- let step = 1 / curveSegments;
6093
+ let step = 1 / bezierSegments;
6056
6094
 
6057
6095
  let vx, vy;
6058
6096
  let quadratic = arguments.length == 4;
@@ -6100,6 +6138,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6100
6138
  }
6101
6139
  }
6102
6140
 
6141
+ // Use curveSegments to determine step size
6142
+ let step = 1 / curveSegments;
6143
+
6103
6144
  // calculate catmull-rom spline curve points
6104
6145
  for (let i = 0; i < points.length - 3; i++) {
6105
6146
  let p0 = points[i];
@@ -6107,7 +6148,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6107
6148
  let p2 = points[i + 2];
6108
6149
  let p3 = points[i + 3];
6109
6150
 
6110
- for (let t = 0; t <= 1; t += 0.1) {
6151
+ for (let t = 0; t <= 1; t += step) {
6111
6152
  let t2 = t * t;
6112
6153
  let t3 = t2 * t;
6113
6154
 
@@ -6429,13 +6470,16 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6429
6470
 
6430
6471
  $._textureBindGroups = [];
6431
6472
 
6432
- $._saveCanvas = async (img, ext) => {
6433
- let graphicsFrame = img._graphics && img._drawStack?.length;
6434
- if (graphicsFrame) img.finishFrame();
6473
+ $._saveCanvas = async (g, ext) => {
6474
+ let makeFrame = g._drawStack?.length;
6475
+ if (makeFrame) {
6476
+ g._render();
6477
+ g._finishRender();
6478
+ }
6435
6479
 
6436
- let texture = img.texture;
6480
+ let texture = g._texture;
6437
6481
 
6438
- if (graphicsFrame) img.beginFrame();
6482
+ if (makeFrame) g._beginRender();
6439
6483
 
6440
6484
  let w = texture.width,
6441
6485
  h = texture.height,
@@ -6446,8 +6490,6 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6446
6490
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
6447
6491
  });
6448
6492
 
6449
- $._buffers.push(buffer);
6450
-
6451
6493
  let en = Q5.device.createCommandEncoder();
6452
6494
 
6453
6495
  en.copyTextureToBuffer({ texture }, { buffer, bytesPerRow, rowsPerImage: h }, { width: w, height: h });
@@ -6482,6 +6524,8 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6482
6524
  let ctx = cnv.getContext('2d', { colorSpace });
6483
6525
  ctx.putImageData(data, 0, 0);
6484
6526
 
6527
+ $._buffers.push(buffer);
6528
+
6485
6529
  // Convert to blob then data URL
6486
6530
  let blob = await cnv.convertToBlob({ type: 'image/' + ext });
6487
6531
  return await new Promise((resolve) => {
@@ -6533,10 +6577,10 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6533
6577
  }
6534
6578
 
6535
6579
  texture.index = tIdx + vidFrames;
6536
- img.texture = texture;
6580
+ img._texture = texture;
6537
6581
 
6538
6582
  $._textureBindGroups[texture.index] = Q5.device.createBindGroup({
6539
- label: img.src || 'canvas',
6583
+ label: img.src || texture.label || 'canvas',
6540
6584
  layout: textureLayout,
6541
6585
  entries: [
6542
6586
  { binding: 0, resource: $._imageSampler },
@@ -6587,16 +6631,6 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6587
6631
  $._addTexture(g, g._frameA);
6588
6632
  $._addTexture(g, g._frameB);
6589
6633
  g._beginRender();
6590
-
6591
- g.finishFrame = function () {
6592
- this._render();
6593
- this._finishRender();
6594
- };
6595
- g.beginFrame = function () {
6596
- this.resetMatrix();
6597
- this._beginRender();
6598
- this.frameCount++;
6599
- };
6600
6634
  } else $._extendImage(g);
6601
6635
  return g;
6602
6636
  };
@@ -6618,7 +6652,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6618
6652
 
6619
6653
  $.image = (img, dx = 0, dy = 0, dw, dh, sx = 0, sy = 0, sw, sh) => {
6620
6654
  let isVideo;
6621
- if (img.texture == undefined) {
6655
+ if (img._texture == undefined) {
6622
6656
  isVideo = img.tagName == 'VIDEO';
6623
6657
  if (!isVideo || !img.width || !img.currentTime) return;
6624
6658
  if (img.flipped) $.scale(-1, 1);
@@ -6630,14 +6664,17 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6630
6664
  w = cnv.width,
6631
6665
  h = cnv.height,
6632
6666
  pd = img._pixelDensity || 1,
6633
- graphicsFrame = img._graphics && img._drawStack?.length;
6667
+ makeFrame = img._graphics && img._drawStack?.length;
6634
6668
 
6635
- if (graphicsFrame) img.finishFrame();
6669
+ if (makeFrame) {
6670
+ img._render();
6671
+ img._finishRender();
6672
+ }
6636
6673
 
6637
6674
  if (img.modified) {
6638
6675
  Q5.device.queue.copyExternalImageToTexture(
6639
6676
  { source: cnv },
6640
- { texture: img.texture, colorSpace: $.canvas.colorSpace },
6677
+ { texture: img._texture, colorSpace: $.canvas.colorSpace },
6641
6678
  [w, h, 1]
6642
6679
  );
6643
6680
  img.frameCount++;
@@ -6667,9 +6704,13 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6667
6704
  addVert(r, b, u1, v1, ci, ti, ia);
6668
6705
 
6669
6706
  if (!isVideo) {
6670
- $._drawStack.push($._imagePL, img.texture.index);
6707
+ $._drawStack.push($._imagePL, img._texture.index);
6671
6708
 
6672
- if (graphicsFrame) img.beginFrame();
6709
+ if (makeFrame) {
6710
+ img.resetMatrix();
6711
+ img._beginRender();
6712
+ img.frameCount++;
6713
+ }
6673
6714
  } else {
6674
6715
  // render video
6675
6716
  let externalTexture = Q5.device.importExternalTexture({ source: img });