@cruxgarden/plasma-ui 0.2.2 → 0.2.4

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ - **Fixed:** the field stopped rendering after a window resize and never came
6
+ back. Two causes, both now addressed. A drag-resize reallocated the eight
7
+ render targets once per distinct size — 88 allocations across a 60-step drag
8
+ in the repro — and the driver dropped the WebGL context under it. And there
9
+ was no `webglcontextlost` / `webglcontextrestored` handling at all, so once
10
+ the context went, the frame loop kept running against a dead context
11
+ forever. The resize now settles before reallocating (88 allocations became
12
+ 8, and the context survives), and a lost context is caught, restored and
13
+ fully rebuilt. `preventDefault` on the loss event is what asks the browser
14
+ to attempt the restore in the first place.
15
+ - **Fixed:** the field never rendered at all under React StrictMode. `destroy()`
16
+ called `WEBGL_lose_context.loseContext()`, but the `<canvas>` belongs to the
17
+ host component and outlives the renderer, so the remount StrictMode performs
18
+ in development got a context that could never draw again. Cleanup now
19
+ deletes its own textures, framebuffers, programs and buffer and leaves the
20
+ canvas usable. A renderer replacing another on the same canvas also zeroes
21
+ the canvas size, so the next allocation is never skipped as a no-op — that
22
+ skip left the multi-target framebuffer with no attachments and drew
23
+ "Framebuffer is incomplete".
24
+
3
25
  ## 0.2.1
4
26
 
5
27
  Fixes for the reports that came in after 0.2.0, and one new option.
package/dist/index.js CHANGED
@@ -419,13 +419,18 @@ var _PlasmaRenderer = class _PlasmaRenderer {
419
419
  this.imgSrc = null;
420
420
  this.bgColor = null;
421
421
  this.srcEl = null;
422
+ // re-uploaded each frame
423
+ this.floatOK = false;
424
+ /** True between webglcontextlost and webglcontextrestored: draw nothing. */
425
+ this.lost = false;
426
+ /** Everything initGL() created, so destroy() can free it by hand. */
427
+ this.owned = { tex: [], fb: [], prog: [], buf: [] };
422
428
  this.recs = /* @__PURE__ */ new Map();
423
429
  this.nextId = 1;
424
430
  this.raf = 0;
425
431
  this.last = 0;
426
432
  this.time = 0;
427
433
  this.dpr = 1;
428
- this.resizeQueued = false;
429
434
  /** Coarse pointer = touch. Only there does a fling run on the compositor with rAF deferred. */
430
435
  this.coarse = typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
431
436
  this.frozen = false;
@@ -450,6 +455,26 @@ var _PlasmaRenderer = class _PlasmaRenderer {
450
455
  this.lightQuery = typeof matchMedia !== "undefined" ? matchMedia("(prefers-color-scheme: light)") : null;
451
456
  this.tintCache = /* @__PURE__ */ new Map();
452
457
  this.RP = new Float32Array(MAX_PULSES * 4);
458
+ /**
459
+ * A drag-resize reallocates eight render targets per distinct size, and the
460
+ * driver can drop the context under that. Without these two handlers the
461
+ * field simply stopped: the frame loop kept running against a dead context
462
+ * and nothing ever brought it back. preventDefault is what asks the browser
463
+ * to attempt a restore at all.
464
+ */
465
+ this.onContextLost = (e) => {
466
+ e.preventDefault();
467
+ this.lost = true;
468
+ cancelAnimationFrame(this.raf);
469
+ this.raf = 0;
470
+ };
471
+ this.onContextRestored = () => {
472
+ this.lost = false;
473
+ this.initGL();
474
+ this.applyResize();
475
+ this.last = 0;
476
+ if (!document.hidden && !this.raf) this.raf = requestAnimationFrame(this.frame);
477
+ };
453
478
  /**
454
479
  * A hidden tab draws nothing anyone can see. Browsers throttle rAF there
455
480
  * but do not all stop it, so the loop stops itself and picks up where it
@@ -511,12 +536,8 @@ var _PlasmaRenderer = class _PlasmaRenderer {
511
536
  * otherwise reallocated every texture repeatedly mid-scroll.
512
537
  */
513
538
  this.resize = () => {
514
- if (this.resizeQueued) return;
515
- this.resizeQueued = true;
516
- requestAnimationFrame(() => {
517
- this.resizeQueued = false;
518
- this.applyResize();
519
- });
539
+ clearTimeout(this.resizeSettle);
540
+ this.resizeSettle = setTimeout(this.applyResize, 120);
520
541
  };
521
542
  this.applyResize = () => {
522
543
  if (this.frozen) {
@@ -559,6 +580,10 @@ var _PlasmaRenderer = class _PlasmaRenderer {
559
580
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
560
581
  };
561
582
  this.frame = (now2) => {
583
+ if (this.lost) {
584
+ this.raf = 0;
585
+ return;
586
+ }
562
587
  if (!this.oneShot) this.raf = requestAnimationFrame(this.frame);
563
588
  const dt = this.last ? Math.min((now2 - this.last) / 1e3, 0.05) : 0.016;
564
589
  this.last = now2;
@@ -580,7 +605,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
580
605
  this.recs.forEach((r) => {
581
606
  if (!r.el.isConnected) return;
582
607
  if (!s.reducedMotion) {
583
- r.formV += (170 * (1 - r.form) - 12 * r.formV) * dt;
608
+ r.formV += (170 * (1 - r.form) - 26 * r.formV) * dt;
584
609
  r.form += r.formV * dt;
585
610
  } else r.form = 1;
586
611
  const b = elementBox(r.el);
@@ -696,26 +721,11 @@ var _PlasmaRenderer = class _PlasmaRenderer {
696
721
  this.FR = new Float32Array(this.max);
697
722
  this.EL = new Float32Array(this.max);
698
723
  this.SOLO = new Float32Array(this.max);
699
- this.floatOK = !!gl.getExtension("EXT_color_buffer_float");
700
- const sh = makeShaders(this.max);
701
- this.progs = { bg: this.program(sh.bgFrag), mask: this.program(sh.maskFrag), tint: this.program(sh.tintFrag), blur: this.program(sh.blurFrag), comp: this.program(sh.compFrag) };
702
- const buf = gl.createBuffer();
703
- gl.bindBuffer(gl.ARRAY_BUFFER, buf);
704
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
705
- gl.enableVertexAttribArray(0);
706
- gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
707
- this.rtA = this.target();
708
- this.rtB = this.target();
709
- this.rtC = this.target();
710
- this.rtT = this.target();
711
- this.rtFr = this.target();
712
- this.rtBg = this.target();
713
- this.rtBgM = this.target();
714
- this.rtBgH = this.target();
715
- this.mrtFb = gl.createFramebuffer();
716
- this.configure(settings, true);
724
+ this.initGL();
717
725
  this.mouse.x = this.mouse.tx = innerWidth / 2;
718
726
  this.mouse.y = this.mouse.ty = innerHeight / 2;
727
+ canvas.addEventListener("webglcontextlost", this.onContextLost);
728
+ canvas.addEventListener("webglcontextrestored", this.onContextRestored);
719
729
  addEventListener("resize", this.resize);
720
730
  document.addEventListener("visibilitychange", this.onVisibility);
721
731
  if (this.coarse) addEventListener("scroll", this.onScroll, { passive: true });
@@ -893,8 +903,41 @@ var _PlasmaRenderer = class _PlasmaRenderer {
893
903
  bump(e) {
894
904
  this.energy = Math.max(this.energy, Math.min(e, 1));
895
905
  }
906
+ /**
907
+ * Every GL object this renderer owns. A lost context invalidates all of
908
+ * them, so creation lives here rather than in the constructor: the restore
909
+ * handler runs exactly the same path.
910
+ */
911
+ initGL() {
912
+ const gl = this.gl;
913
+ this.owned = { tex: [], fb: [], prog: [], buf: [] };
914
+ this.floatOK = !!gl.getExtension("EXT_color_buffer_float");
915
+ const sh = makeShaders(this.max);
916
+ this.progs = { bg: this.program(sh.bgFrag), mask: this.program(sh.maskFrag), tint: this.program(sh.tintFrag), blur: this.program(sh.blurFrag), comp: this.program(sh.compFrag) };
917
+ const buf = gl.createBuffer();
918
+ if (buf) this.owned.buf.push(buf);
919
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf);
920
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
921
+ gl.enableVertexAttribArray(0);
922
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
923
+ this.rtA = this.target();
924
+ this.rtB = this.target();
925
+ this.rtC = this.target();
926
+ this.rtT = this.target();
927
+ this.rtFr = this.target();
928
+ this.rtBg = this.target();
929
+ this.rtBgM = this.target();
930
+ this.rtBgH = this.target();
931
+ this.mrtFb = gl.createFramebuffer();
932
+ this.owned.fb.push(this.mrtFb);
933
+ this.configure(this.settings, true);
934
+ this.canvas.width = this.canvas.height = 0;
935
+ }
896
936
  destroy() {
897
937
  cancelAnimationFrame(this.raf);
938
+ clearTimeout(this.resizeSettle);
939
+ this.canvas.removeEventListener("webglcontextlost", this.onContextLost);
940
+ this.canvas.removeEventListener("webglcontextrestored", this.onContextRestored);
898
941
  removeEventListener("resize", this.resize);
899
942
  document.removeEventListener("visibilitychange", this.onVisibility);
900
943
  removeEventListener("scroll", this.onScroll);
@@ -906,7 +949,14 @@ var _PlasmaRenderer = class _PlasmaRenderer {
906
949
  r.el.style.scale = "";
907
950
  });
908
951
  this.recs.clear();
909
- this.gl.getExtension("WEBGL_lose_context")?.loseContext();
952
+ const gl = this.gl;
953
+ if (!gl.isContextLost()) {
954
+ this.owned.tex.forEach((t) => gl.deleteTexture(t));
955
+ this.owned.fb.forEach((f) => gl.deleteFramebuffer(f));
956
+ this.owned.prog.forEach((pr) => gl.deleteProgram(pr));
957
+ this.owned.buf.forEach((b) => gl.deleteBuffer(b));
958
+ }
959
+ this.owned = { tex: [], fb: [], prog: [], buf: [] };
910
960
  }
911
961
  freeze() {
912
962
  const c = this.canvas;
@@ -1149,6 +1199,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
1149
1199
  UNIFORMS.forEach((n) => {
1150
1200
  u[n] = gl.getUniformLocation(pr, n);
1151
1201
  });
1202
+ this.owned.prog.push(pr);
1152
1203
  return { pr, u };
1153
1204
  }
1154
1205
  target() {
@@ -1159,7 +1210,10 @@ var _PlasmaRenderer = class _PlasmaRenderer {
1159
1210
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1160
1211
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1161
1212
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1162
- return { tex, fb: gl.createFramebuffer(), w: 0, h: 0 };
1213
+ const fb = gl.createFramebuffer();
1214
+ this.owned.tex.push(tex);
1215
+ this.owned.fb.push(fb);
1216
+ return { tex, fb, w: 0, h: 0 };
1163
1217
  }
1164
1218
  };
1165
1219
  _PlasmaRenderer.RUNWAY = 1;
@@ -94,13 +94,17 @@ export declare class PlasmaRenderer {
94
94
  private bgColor;
95
95
  private srcEl;
96
96
  private floatOK;
97
+ /** True between webglcontextlost and webglcontextrestored: draw nothing. */
98
+ private lost;
99
+ private resizeSettle;
100
+ /** Everything initGL() created, so destroy() can free it by hand. */
101
+ private owned;
97
102
  private recs;
98
103
  private nextId;
99
104
  private raf;
100
105
  private last;
101
106
  private time;
102
107
  private dpr;
103
- private resizeQueued;
104
108
  /** Coarse pointer = touch. Only there does a fling run on the compositor with rAF deferred. */
105
109
  private coarse;
106
110
  private frozen;
@@ -148,6 +152,21 @@ export declare class PlasmaRenderer {
148
152
  pulse(x: number, y: number, strength?: number): void;
149
153
  /** Raise the material's energy (brightens contours and color); it decays on its own. */
150
154
  bump(e: number): void;
155
+ /**
156
+ * Every GL object this renderer owns. A lost context invalidates all of
157
+ * them, so creation lives here rather than in the constructor: the restore
158
+ * handler runs exactly the same path.
159
+ */
160
+ private initGL;
161
+ /**
162
+ * A drag-resize reallocates eight render targets per distinct size, and the
163
+ * driver can drop the context under that. Without these two handlers the
164
+ * field simply stopped: the frame loop kept running against a dead context
165
+ * and nothing ever brought it back. preventDefault is what asks the browser
166
+ * to attempt a restore at all.
167
+ */
168
+ private onContextLost;
169
+ private onContextRestored;
151
170
  destroy(): void;
152
171
  /**
153
172
  * A hidden tab draws nothing anyone can see. Browsers throttle rAF there
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxgarden/plasma-ui",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Liquid panels for React, rendered in WebGL on canvas: every panel is one shared plasma - they fuse on contact, refract, and snap to a grid.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {