@clipkit/runtime 1.0.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/LICENSE +54 -0
- package/README.md +98 -0
- package/dist/animation/easings.d.ts +9 -0
- package/dist/animation/easings.d.ts.map +1 -0
- package/dist/animation/easings.js +230 -0
- package/dist/animation/easings.js.map +1 -0
- package/dist/animation/expr.d.ts +44 -0
- package/dist/animation/expr.d.ts.map +1 -0
- package/dist/animation/expr.js +236 -0
- package/dist/animation/expr.js.map +1 -0
- package/dist/animation/keyframes.d.ts +23 -0
- package/dist/animation/keyframes.d.ts.map +1 -0
- package/dist/animation/keyframes.js +117 -0
- package/dist/animation/keyframes.js.map +1 -0
- package/dist/animation/motion-path.d.ts +18 -0
- package/dist/animation/motion-path.d.ts.map +1 -0
- package/dist/animation/motion-path.js +269 -0
- package/dist/animation/motion-path.js.map +1 -0
- package/dist/animation/noise1d.d.ts +5 -0
- package/dist/animation/noise1d.d.ts.map +1 -0
- package/dist/animation/noise1d.js +27 -0
- package/dist/animation/noise1d.js.map +1 -0
- package/dist/animation/presets.d.ts +60 -0
- package/dist/animation/presets.d.ts.map +1 -0
- package/dist/animation/presets.js +221 -0
- package/dist/animation/presets.js.map +1 -0
- package/dist/assets/cache.d.ts +18 -0
- package/dist/assets/cache.d.ts.map +1 -0
- package/dist/assets/cache.js +56 -0
- package/dist/assets/cache.js.map +1 -0
- package/dist/assets/fonts.d.ts +20 -0
- package/dist/assets/fonts.d.ts.map +1 -0
- package/dist/assets/fonts.js +127 -0
- package/dist/assets/fonts.js.map +1 -0
- package/dist/assets/loader.d.ts +18 -0
- package/dist/assets/loader.d.ts.map +1 -0
- package/dist/assets/loader.js +87 -0
- package/dist/assets/loader.js.map +1 -0
- package/dist/assets/lut.d.ts +5 -0
- package/dist/assets/lut.d.ts.map +1 -0
- package/dist/assets/lut.js +77 -0
- package/dist/assets/lut.js.map +1 -0
- package/dist/assets/media-time.d.ts +31 -0
- package/dist/assets/media-time.d.ts.map +1 -0
- package/dist/assets/media-time.js +65 -0
- package/dist/assets/media-time.js.map +1 -0
- package/dist/assets/mp4-frame-source.d.ts +44 -0
- package/dist/assets/mp4-frame-source.d.ts.map +1 -0
- package/dist/assets/mp4-frame-source.js +387 -0
- package/dist/assets/mp4-frame-source.js.map +1 -0
- package/dist/audio/encoder.d.ts +31 -0
- package/dist/audio/encoder.d.ts.map +1 -0
- package/dist/audio/encoder.js +96 -0
- package/dist/audio/encoder.js.map +1 -0
- package/dist/audio/fades.d.ts +16 -0
- package/dist/audio/fades.d.ts.map +1 -0
- package/dist/audio/fades.js +43 -0
- package/dist/audio/fades.js.map +1 -0
- package/dist/audio/limiter.d.ts +8 -0
- package/dist/audio/limiter.d.ts.map +1 -0
- package/dist/audio/limiter.js +39 -0
- package/dist/audio/limiter.js.map +1 -0
- package/dist/audio/loader.d.ts +6 -0
- package/dist/audio/loader.d.ts.map +1 -0
- package/dist/audio/loader.js +42 -0
- package/dist/audio/loader.js.map +1 -0
- package/dist/audio/mixer.d.ts +17 -0
- package/dist/audio/mixer.d.ts.map +1 -0
- package/dist/audio/mixer.js +204 -0
- package/dist/audio/mixer.js.map +1 -0
- package/dist/audio/varispeed.d.ts +24 -0
- package/dist/audio/varispeed.d.ts.map +1 -0
- package/dist/audio/varispeed.js +114 -0
- package/dist/audio/varispeed.js.map +1 -0
- package/dist/audio/wav.d.ts +6 -0
- package/dist/audio/wav.d.ts.map +1 -0
- package/dist/audio/wav.js +62 -0
- package/dist/audio/wav.js.map +1 -0
- package/dist/backend/backend.d.ts +579 -0
- package/dist/backend/backend.d.ts.map +1 -0
- package/dist/backend/backend.js +17 -0
- package/dist/backend/backend.js.map +1 -0
- package/dist/backend/webgl-backend.d.ts +97 -0
- package/dist/backend/webgl-backend.d.ts.map +1 -0
- package/dist/backend/webgl-backend.js +2142 -0
- package/dist/backend/webgl-backend.js.map +1 -0
- package/dist/backend/webgpu-backend.d.ts +121 -0
- package/dist/backend/webgpu-backend.d.ts.map +1 -0
- package/dist/backend/webgpu-backend.js +2481 -0
- package/dist/backend/webgpu-backend.js.map +1 -0
- package/dist/compositor/bitfont.d.ts +8 -0
- package/dist/compositor/bitfont.d.ts.map +1 -0
- package/dist/compositor/bitfont.js +52 -0
- package/dist/compositor/bitfont.js.map +1 -0
- package/dist/compositor/camera.d.ts +5 -0
- package/dist/compositor/camera.d.ts.map +1 -0
- package/dist/compositor/camera.js +114 -0
- package/dist/compositor/camera.js.map +1 -0
- package/dist/compositor/color.d.ts +26 -0
- package/dist/compositor/color.d.ts.map +1 -0
- package/dist/compositor/color.js +189 -0
- package/dist/compositor/color.js.map +1 -0
- package/dist/compositor/element-renderers/caption.d.ts +4 -0
- package/dist/compositor/element-renderers/caption.d.ts.map +1 -0
- package/dist/compositor/element-renderers/caption.js +376 -0
- package/dist/compositor/element-renderers/caption.js.map +1 -0
- package/dist/compositor/element-renderers/group.d.ts +12 -0
- package/dist/compositor/element-renderers/group.d.ts.map +1 -0
- package/dist/compositor/element-renderers/group.js +259 -0
- package/dist/compositor/element-renderers/group.js.map +1 -0
- package/dist/compositor/element-renderers/image.d.ts +4 -0
- package/dist/compositor/element-renderers/image.d.ts.map +1 -0
- package/dist/compositor/element-renderers/image.js +97 -0
- package/dist/compositor/element-renderers/image.js.map +1 -0
- package/dist/compositor/element-renderers/lit.d.ts +6 -0
- package/dist/compositor/element-renderers/lit.d.ts.map +1 -0
- package/dist/compositor/element-renderers/lit.js +82 -0
- package/dist/compositor/element-renderers/lit.js.map +1 -0
- package/dist/compositor/element-renderers/particles.d.ts +4 -0
- package/dist/compositor/element-renderers/particles.d.ts.map +1 -0
- package/dist/compositor/element-renderers/particles.js +212 -0
- package/dist/compositor/element-renderers/particles.js.map +1 -0
- package/dist/compositor/element-renderers/shape.d.ts +4 -0
- package/dist/compositor/element-renderers/shape.d.ts.map +1 -0
- package/dist/compositor/element-renderers/shape.js +171 -0
- package/dist/compositor/element-renderers/shape.js.map +1 -0
- package/dist/compositor/element-renderers/svg.d.ts +4 -0
- package/dist/compositor/element-renderers/svg.d.ts.map +1 -0
- package/dist/compositor/element-renderers/svg.js +210 -0
- package/dist/compositor/element-renderers/svg.js.map +1 -0
- package/dist/compositor/element-renderers/text.d.ts +25 -0
- package/dist/compositor/element-renderers/text.d.ts.map +1 -0
- package/dist/compositor/element-renderers/text.js +1358 -0
- package/dist/compositor/element-renderers/text.js.map +1 -0
- package/dist/compositor/element-renderers/video.d.ts +12 -0
- package/dist/compositor/element-renderers/video.d.ts.map +1 -0
- package/dist/compositor/element-renderers/video.js +109 -0
- package/dist/compositor/element-renderers/video.js.map +1 -0
- package/dist/compositor/fit.d.ts +18 -0
- package/dist/compositor/fit.d.ts.map +1 -0
- package/dist/compositor/fit.js +106 -0
- package/dist/compositor/fit.js.map +1 -0
- package/dist/compositor/lighting.d.ts +63 -0
- package/dist/compositor/lighting.d.ts.map +1 -0
- package/dist/compositor/lighting.js +141 -0
- package/dist/compositor/lighting.js.map +1 -0
- package/dist/compositor/mat4.d.ts +88 -0
- package/dist/compositor/mat4.d.ts.map +1 -0
- package/dist/compositor/mat4.js +245 -0
- package/dist/compositor/mat4.js.map +1 -0
- package/dist/compositor/project.d.ts +24 -0
- package/dist/compositor/project.d.ts.map +1 -0
- package/dist/compositor/project.js +105 -0
- package/dist/compositor/project.js.map +1 -0
- package/dist/compositor/render-context.d.ts +194 -0
- package/dist/compositor/render-context.d.ts.map +1 -0
- package/dist/compositor/render-context.js +10 -0
- package/dist/compositor/render-context.js.map +1 -0
- package/dist/compositor/resolve.d.ts +80 -0
- package/dist/compositor/resolve.d.ts.map +1 -0
- package/dist/compositor/resolve.js +276 -0
- package/dist/compositor/resolve.js.map +1 -0
- package/dist/compositor/scene.d.ts +10 -0
- package/dist/compositor/scene.d.ts.map +1 -0
- package/dist/compositor/scene.js +658 -0
- package/dist/compositor/scene.js.map +1 -0
- package/dist/compositor/transform.d.ts +73 -0
- package/dist/compositor/transform.d.ts.map +1 -0
- package/dist/compositor/transform.js +229 -0
- package/dist/compositor/transform.js.map +1 -0
- package/dist/compositor/unit.d.ts +27 -0
- package/dist/compositor/unit.d.ts.map +1 -0
- package/dist/compositor/unit.js +74 -0
- package/dist/compositor/unit.js.map +1 -0
- package/dist/encoder/exporter.d.ts +95 -0
- package/dist/encoder/exporter.d.ts.map +1 -0
- package/dist/encoder/exporter.js +341 -0
- package/dist/encoder/exporter.js.map +1 -0
- package/dist/encoder/index.d.ts +3 -0
- package/dist/encoder/index.d.ts.map +1 -0
- package/dist/encoder/index.js +2 -0
- package/dist/encoder/index.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +32 -0
- package/dist/logger.js.map +1 -0
- package/dist/runtime.d.ts +216 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1012 -0
- package/dist/runtime.js.map +1 -0
- package/dist/svg/morph.d.ts +6 -0
- package/dist/svg/morph.d.ts.map +1 -0
- package/dist/svg/morph.js +62 -0
- package/dist/svg/morph.js.map +1 -0
- package/dist/svg/svg-renderer.d.ts +18 -0
- package/dist/svg/svg-renderer.d.ts.map +1 -0
- package/dist/svg/svg-renderer.js +142 -0
- package/dist/svg/svg-renderer.js.map +1 -0
- package/dist/text/caption-chunk.d.ts +17 -0
- package/dist/text/caption-chunk.d.ts.map +1 -0
- package/dist/text/caption-chunk.js +76 -0
- package/dist/text/caption-chunk.js.map +1 -0
- package/dist/text/font-atlas.d.ts +63 -0
- package/dist/text/font-atlas.d.ts.map +1 -0
- package/dist/text/font-atlas.js +225 -0
- package/dist/text/font-atlas.js.map +1 -0
- package/dist/text/measure.d.ts +38 -0
- package/dist/text/measure.d.ts.map +1 -0
- package/dist/text/measure.js +164 -0
- package/dist/text/measure.js.map +1 -0
- package/dist/text/text-animation.d.ts +52 -0
- package/dist/text/text-animation.d.ts.map +1 -0
- package/dist/text/text-animation.js +133 -0
- package/dist/text/text-animation.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,2142 @@
|
|
|
1
|
+
// WebGL2 implementation of the Backend interface.
|
|
2
|
+
//
|
|
3
|
+
// Same shape as the WebGPU backend (deliberately) so the runtime can swap
|
|
4
|
+
// between them transparently. WebGL2 + GLSL ES 3.0 shaders, premultiplied
|
|
5
|
+
// alpha throughout, shared unit-quad VBO, individual uniforms (no UBOs —
|
|
6
|
+
// simpler and our uniform set is small).
|
|
7
|
+
//
|
|
8
|
+
// Premultiplied alpha discipline:
|
|
9
|
+
// - Source is configured with `premultipliedAlpha: true`
|
|
10
|
+
// - Blend func: ONE / ONE_MINUS_SRC_ALPHA (premultiplied)
|
|
11
|
+
// - Texture upload uses UNPACK_PREMULTIPLY_ALPHA_WEBGL
|
|
12
|
+
// - Shaders assume premultiplied input
|
|
13
|
+
import { composeQuadTransform, homographyToPhysical, invertHomography, projectPixelMatrix } from '../compositor/transform.js';
|
|
14
|
+
import { getLogger } from '../logger.js';
|
|
15
|
+
import { STYLIZE_MODE_INDEX } from './backend.js';
|
|
16
|
+
// ─── Shaders ────────────────────────────────────────────────────────────────
|
|
17
|
+
const SHAPE_VS = `#version 300 es
|
|
18
|
+
in vec2 a_pos;
|
|
19
|
+
in vec2 a_uv;
|
|
20
|
+
out vec2 v_uv;
|
|
21
|
+
uniform mat4 u_transform;
|
|
22
|
+
void main() {
|
|
23
|
+
gl_Position = u_transform * vec4(a_pos, 0.0, 1.0);
|
|
24
|
+
v_uv = a_uv;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
// Shadow pipeline: draw a quad sized to the shape PLUS blur padding,
|
|
28
|
+
// then in the fragment shader compute the signed distance from each
|
|
29
|
+
// pixel to the un-padded shape and fade alpha to 0 over `blur` pixels.
|
|
30
|
+
// Pixels inside the shape's SDF (negative distance) get full shadow
|
|
31
|
+
// alpha — the companion shape draw paints over them after.
|
|
32
|
+
const SHADOW_FS = `#version 300 es
|
|
33
|
+
precision highp float;
|
|
34
|
+
in vec2 v_uv;
|
|
35
|
+
out vec4 fragColor;
|
|
36
|
+
uniform vec4 u_color; // shadow color, premultiplied
|
|
37
|
+
uniform float u_blur; // PIXELS; falloff distance past edge
|
|
38
|
+
uniform float u_cornerRadius; // PIXELS, of the SHAPE (not quad)
|
|
39
|
+
uniform float u_shapeType; // 0.0 rect, 1.0 ellipse
|
|
40
|
+
uniform vec2 u_size; // pixel (width, height) of the SHAPE
|
|
41
|
+
uniform vec2 u_quadSize; // pixel (width, height) of the rendered quad
|
|
42
|
+
void main() {
|
|
43
|
+
// Pixel position in quad-local space. The shape sits centered, with
|
|
44
|
+
// blur-sized margins on every side, so subtracting half the quad's
|
|
45
|
+
// size and adding half the shape's size positions us in shape-local
|
|
46
|
+
// coords for the SDF calculation.
|
|
47
|
+
vec2 p = v_uv * u_quadSize;
|
|
48
|
+
vec2 shapeHalf = u_size * 0.5;
|
|
49
|
+
vec2 quadHalf = u_quadSize * 0.5;
|
|
50
|
+
vec2 ps = p - quadHalf + shapeHalf; // pixel position in SHAPE's local frame
|
|
51
|
+
float dist;
|
|
52
|
+
if (u_shapeType > 0.5) {
|
|
53
|
+
vec2 d = (ps - shapeHalf) / shapeHalf;
|
|
54
|
+
dist = (sqrt(dot(d, d)) - 1.0) * min(shapeHalf.x, shapeHalf.y);
|
|
55
|
+
} else {
|
|
56
|
+
float r = u_cornerRadius;
|
|
57
|
+
vec2 q = abs(ps - shapeHalf) - shapeHalf + vec2(r);
|
|
58
|
+
dist = min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r;
|
|
59
|
+
}
|
|
60
|
+
// CSS box-shadow is a Gaussian BLUR of an offset shape, not a
|
|
61
|
+
// hard-edge falloff: alpha is ~1.0 deep inside the shape, ~0.5
|
|
62
|
+
// right at the edge, and tails to 0 about u_blur pixels past the
|
|
63
|
+
// edge. Symmetric smoothstep approximates the erfc shape closely
|
|
64
|
+
// enough — without it the shape-edge alpha is 1.0 instead of 0.5,
|
|
65
|
+
// making shadows look much darker and extend further than CSS.
|
|
66
|
+
if (dist > u_blur) discard;
|
|
67
|
+
float alpha = 1.0 - smoothstep(-u_blur, u_blur, dist);
|
|
68
|
+
if (alpha < 0.001) discard;
|
|
69
|
+
fragColor = u_color * alpha;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
const SHAPE_FS = `#version 300 es
|
|
73
|
+
precision highp float;
|
|
74
|
+
in vec2 v_uv;
|
|
75
|
+
out vec4 fragColor;
|
|
76
|
+
uniform vec4 u_color; // fill, premultiplied
|
|
77
|
+
uniform vec4 u_strokeColor; // stroke, premultiplied
|
|
78
|
+
uniform float u_strokeWidth; // PIXELS; 0 disables stroke
|
|
79
|
+
uniform float u_cornerRadius; // PIXELS
|
|
80
|
+
uniform float u_shapeType; // 0.0 rect, 1.0 ellipse
|
|
81
|
+
uniform vec2 u_size; // pixel (width, height)
|
|
82
|
+
void main() {
|
|
83
|
+
// Signed pixel distance from the shape boundary: negative inside,
|
|
84
|
+
// positive outside. Used both to discard outside pixels and to
|
|
85
|
+
// decide whether a pixel falls in the stroke band (boundary-side
|
|
86
|
+
// strokeWidth pixels deep).
|
|
87
|
+
vec2 p = v_uv * u_size;
|
|
88
|
+
vec2 half_ = u_size * 0.5;
|
|
89
|
+
float dist;
|
|
90
|
+
if (u_shapeType > 0.5) {
|
|
91
|
+
// Ellipse — exact pixel SDF is hard; use a normalized-space
|
|
92
|
+
// approximation scaled by the smaller half-axis. Exact for
|
|
93
|
+
// circles; underestimates the boundary distance for elongated
|
|
94
|
+
// ellipses, which means the stroke band reads slightly thin near
|
|
95
|
+
// the long-axis ends. Good enough for icon-shaped uses.
|
|
96
|
+
vec2 d = (p - half_) / half_;
|
|
97
|
+
dist = (sqrt(dot(d, d)) - 1.0) * min(half_.x, half_.y);
|
|
98
|
+
} else {
|
|
99
|
+
// Rectangle / rounded rectangle. r = 0 collapses to a sharp rect.
|
|
100
|
+
float r = u_cornerRadius;
|
|
101
|
+
vec2 q = abs(p - half_) - half_ + vec2(r);
|
|
102
|
+
dist = min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r;
|
|
103
|
+
}
|
|
104
|
+
// Anti-aliased boundary: use screen-space derivative to find the
|
|
105
|
+
// AA band around dist = 0, and blend out as we exit the shape.
|
|
106
|
+
// Without this, rotated rectangles show jagged stairstep edges
|
|
107
|
+
// (the rotated geometry no longer aligns with pixel rows). Band
|
|
108
|
+
// width = 2 × fwidth(dist) — wider than the standard 1 px so edges
|
|
109
|
+
// stay visibly smooth even when the canvas is downsampled to a
|
|
110
|
+
// smaller preview or the display has higher pixel density than the
|
|
111
|
+
// canvas backing store.
|
|
112
|
+
float aa = fwidth(dist);
|
|
113
|
+
float outerAlpha = 1.0 - smoothstep(-aa, aa, dist);
|
|
114
|
+
if (outerAlpha < 0.001) discard;
|
|
115
|
+
|
|
116
|
+
vec4 base;
|
|
117
|
+
if (u_strokeWidth > 0.0) {
|
|
118
|
+
// Stroke band is the [-strokeWidth, 0] interval of dist; the fill
|
|
119
|
+
// interior is dist <= -strokeWidth. strokeAlpha rises from 0 (fill)
|
|
120
|
+
// to 1 (stroke) as dist crosses -strokeWidth, with the same ~1px
|
|
121
|
+
// AA softness applied at the inner boundary so the stroke doesn't
|
|
122
|
+
// stair-step against the fill.
|
|
123
|
+
float strokeAlpha = smoothstep(-u_strokeWidth - aa, -u_strokeWidth + aa, dist);
|
|
124
|
+
base = mix(u_color, u_strokeColor, strokeAlpha);
|
|
125
|
+
} else {
|
|
126
|
+
base = u_color;
|
|
127
|
+
}
|
|
128
|
+
// Premultiplied output: scaling all channels by outerAlpha is correct.
|
|
129
|
+
fragColor = base * outerAlpha;
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
// ── Lit shape path (CKP/1.0 §4.8 PBR) ───────────────────────────────────────
|
|
133
|
+
// Same SDF as SHAPE_FS, but the fill is shaded: Lambert diffuse + GGX
|
|
134
|
+
// specular + Schlick Fresnel from directional lights, in WORLD space, so
|
|
135
|
+
// the highlight is view-dependent and sweeps as the camera moves. Albedo
|
|
136
|
+
// is the shape's straight-alpha fill. u_worldMatrix maps the unit quad to
|
|
137
|
+
// world (pre-camera) for the per-fragment position; u_transform still maps
|
|
138
|
+
// to clip.
|
|
139
|
+
const LIT_SHAPE_VS = `#version 300 es
|
|
140
|
+
in vec2 a_pos;
|
|
141
|
+
in vec2 a_uv;
|
|
142
|
+
out vec2 v_uv;
|
|
143
|
+
out vec3 v_worldPos;
|
|
144
|
+
uniform mat4 u_transform;
|
|
145
|
+
uniform mat4 u_worldMatrix;
|
|
146
|
+
void main() {
|
|
147
|
+
gl_Position = u_transform * vec4(a_pos, 0.0, 1.0);
|
|
148
|
+
v_uv = a_uv;
|
|
149
|
+
vec4 wp = u_worldMatrix * vec4(a_pos, 0.0, 1.0);
|
|
150
|
+
v_worldPos = wp.xyz;
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
// Shared PBR fragment library (§4.8): common uniforms + helpers +
|
|
154
|
+
// shadePBR(albedo, N, V). Concatenated into BOTH the lit-shape and
|
|
155
|
+
// lit-textured fragment shaders so the lighting math can never diverge
|
|
156
|
+
// between vector and textured surfaces. Must stay math-identical to the
|
|
157
|
+
// WGSL pbrLibWGSL() in the WebGPU backend.
|
|
158
|
+
const PBR_FS_LIB = `
|
|
159
|
+
uniform vec3 u_normal;
|
|
160
|
+
uniform vec3 u_eye;
|
|
161
|
+
uniform float u_rough;
|
|
162
|
+
uniform float u_metal;
|
|
163
|
+
uniform float u_reflect;
|
|
164
|
+
uniform float u_emissive;
|
|
165
|
+
uniform vec3 u_ambient;
|
|
166
|
+
uniform int u_numLights;
|
|
167
|
+
uniform vec3 u_lightDir[4];
|
|
168
|
+
uniform vec3 u_lightColor[4];
|
|
169
|
+
uniform int u_envCount; // 0 ⇒ no environment reflection
|
|
170
|
+
uniform vec3 u_envColor[4]; // straight RGB, sorted by offset
|
|
171
|
+
uniform float u_envOffset[4];
|
|
172
|
+
uniform vec3 u_envAvg; // mean env color (irradiance / rough fallback)
|
|
173
|
+
uniform vec3 u_tangent; // world +U (normal mapping)
|
|
174
|
+
uniform vec3 u_bitangent; // world +V
|
|
175
|
+
uniform float u_normalScale;
|
|
176
|
+
uniform int u_hasNormalMap; // 0 ⇒ flat face normal
|
|
177
|
+
uniform sampler2D u_normalMap;
|
|
178
|
+
uniform int u_envIsImage; // 1 ⇒ sample u_envMap as equirect
|
|
179
|
+
uniform sampler2D u_envMap;
|
|
180
|
+
const float PI = 3.14159265;
|
|
181
|
+
float ggxD(float NdotH, float a) { float a2 = a * a; float d = NdotH * NdotH * (a2 - 1.0) + 1.0; return a2 / (PI * d * d); }
|
|
182
|
+
float gSchlick(float x, float k) { return x / (x * (1.0 - k) + k); }
|
|
183
|
+
// Sample the gradient environment at parameter t∈[0,1] (const-indexed for
|
|
184
|
+
// portability — matches the WGSL path). Stops are sorted by offset.
|
|
185
|
+
vec3 sampleEnv(float t) {
|
|
186
|
+
vec3 c = u_envColor[0];
|
|
187
|
+
if (u_envCount > 1) {
|
|
188
|
+
vec3 last = u_envColor[1];
|
|
189
|
+
if (u_envCount > 2) last = u_envColor[2];
|
|
190
|
+
if (u_envCount > 3) last = u_envColor[3];
|
|
191
|
+
if (t <= u_envOffset[1]) {
|
|
192
|
+
c = mix(u_envColor[0], u_envColor[1], clamp((t - u_envOffset[0]) / max(u_envOffset[1] - u_envOffset[0], 1e-4), 0.0, 1.0));
|
|
193
|
+
} else if (u_envCount > 2 && t <= u_envOffset[2]) {
|
|
194
|
+
c = mix(u_envColor[1], u_envColor[2], clamp((t - u_envOffset[1]) / max(u_envOffset[2] - u_envOffset[1], 1e-4), 0.0, 1.0));
|
|
195
|
+
} else if (u_envCount > 3 && t <= u_envOffset[3]) {
|
|
196
|
+
c = mix(u_envColor[2], u_envColor[3], clamp((t - u_envOffset[2]) / max(u_envOffset[3] - u_envOffset[2], 1e-4), 0.0, 1.0));
|
|
197
|
+
} else {
|
|
198
|
+
c = last;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return c;
|
|
202
|
+
}
|
|
203
|
+
// Perturb the face normal by a tangent-space normal map (flat = #8080ff).
|
|
204
|
+
vec3 perturbNormal(vec3 N, vec2 uv) {
|
|
205
|
+
if (u_hasNormalMap == 0) return N;
|
|
206
|
+
vec3 s = texture(u_normalMap, uv).rgb * 2.0 - 1.0;
|
|
207
|
+
s.xy *= u_normalScale;
|
|
208
|
+
return normalize(s.x * normalize(u_tangent) + s.y * normalize(u_bitangent) + s.z * N);
|
|
209
|
+
}
|
|
210
|
+
// Shade a fragment given its straight-alpha albedo, world normal, view
|
|
211
|
+
// vector. Returns clamped straight RGB (ambient + direct + env + emissive).
|
|
212
|
+
vec3 shadePBR(vec3 albedo, vec3 Nin, vec3 V) {
|
|
213
|
+
vec3 N = Nin;
|
|
214
|
+
if (dot(N, V) < 0.0) N = -N; // two-sided
|
|
215
|
+
float NdotV = max(dot(N, V), 1e-4);
|
|
216
|
+
vec3 F0 = mix(vec3(0.04), albedo, u_metal);
|
|
217
|
+
float a = u_rough * u_rough;
|
|
218
|
+
float k = (u_rough + 1.0) * (u_rough + 1.0) / 8.0;
|
|
219
|
+
|
|
220
|
+
vec3 color = albedo * u_ambient; // ambient (flat fill) term
|
|
221
|
+
for (int i = 0; i < 4; i++) {
|
|
222
|
+
if (i >= u_numLights) break;
|
|
223
|
+
vec3 L = normalize(u_lightDir[i]);
|
|
224
|
+
vec3 H = normalize(V + L);
|
|
225
|
+
float NdotL = max(dot(N, L), 0.0);
|
|
226
|
+
float NdotH = max(dot(N, H), 0.0);
|
|
227
|
+
float VdotH = max(dot(V, H), 0.0);
|
|
228
|
+
vec3 F = F0 + (1.0 - F0) * pow(1.0 - VdotH, 5.0);
|
|
229
|
+
float D = ggxD(NdotH, a);
|
|
230
|
+
float G = gSchlick(NdotL, k) * gSchlick(NdotV, k);
|
|
231
|
+
vec3 spec = (D * G) * F / max(4.0 * NdotL * NdotV, 1e-3);
|
|
232
|
+
vec3 kd = (1.0 - F) * (1.0 - u_metal);
|
|
233
|
+
color += (kd * albedo + spec) * u_lightColor[i] * NdotL;
|
|
234
|
+
}
|
|
235
|
+
// Environment reflection: mirror the gradient sky along R, roughness-
|
|
236
|
+
// blurred toward the average; IBL split (diffuse + Fresnel specular).
|
|
237
|
+
if (u_envCount > 0 || u_envIsImage == 1) {
|
|
238
|
+
vec3 R = reflect(-V, N);
|
|
239
|
+
vec3 sharp;
|
|
240
|
+
if (u_envIsImage == 1) {
|
|
241
|
+
// Equirect (lat-long) sample along the reflection ray. Up = −y.
|
|
242
|
+
vec3 Rn = normalize(R);
|
|
243
|
+
vec2 euv = vec2(atan(Rn.x, Rn.z) / (2.0 * PI) + 0.5, acos(clamp(-Rn.y, -1.0, 1.0)) / PI);
|
|
244
|
+
sharp = texture(u_envMap, euv).rgb;
|
|
245
|
+
} else {
|
|
246
|
+
float t = clamp(0.5 - 0.5 * (R.y / max(length(R), 1e-4)), 0.0, 1.0); // up→1, down→0
|
|
247
|
+
sharp = sampleEnv(t);
|
|
248
|
+
}
|
|
249
|
+
vec3 envc = mix(sharp, u_envAvg, u_rough);
|
|
250
|
+
vec3 Fr = F0 + (max(vec3(1.0 - u_rough), F0) - F0) * pow(1.0 - NdotV, 5.0);
|
|
251
|
+
vec3 kdEnv = (1.0 - Fr) * (1.0 - u_metal);
|
|
252
|
+
color += (kdEnv * albedo * u_envAvg + envc * Fr) * u_reflect;
|
|
253
|
+
}
|
|
254
|
+
color = mix(color, albedo, clamp(u_emissive, 0.0, 1.0));
|
|
255
|
+
return clamp(color, 0.0, 1.0);
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
const LIT_SHAPE_FS = `#version 300 es
|
|
259
|
+
precision highp float;
|
|
260
|
+
in vec2 v_uv;
|
|
261
|
+
in vec3 v_worldPos;
|
|
262
|
+
out vec4 fragColor;
|
|
263
|
+
uniform vec4 u_albedo; // straight (non-premultiplied)
|
|
264
|
+
uniform vec4 u_strokeAlbedo; // straight
|
|
265
|
+
uniform float u_strokeWidth;
|
|
266
|
+
uniform float u_cornerRadius;
|
|
267
|
+
uniform float u_shapeType;
|
|
268
|
+
uniform vec2 u_size;
|
|
269
|
+
${PBR_FS_LIB}
|
|
270
|
+
void main() {
|
|
271
|
+
vec2 p = v_uv * u_size;
|
|
272
|
+
vec2 half_ = u_size * 0.5;
|
|
273
|
+
float dist;
|
|
274
|
+
if (u_shapeType > 0.5) {
|
|
275
|
+
vec2 d = (p - half_) / half_;
|
|
276
|
+
dist = (sqrt(dot(d, d)) - 1.0) * min(half_.x, half_.y);
|
|
277
|
+
} else {
|
|
278
|
+
float r = u_cornerRadius;
|
|
279
|
+
vec2 q = abs(p - half_) - half_ + vec2(r);
|
|
280
|
+
dist = min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r;
|
|
281
|
+
}
|
|
282
|
+
float aa = fwidth(dist);
|
|
283
|
+
float outerAlpha = 1.0 - smoothstep(-aa, aa, dist);
|
|
284
|
+
if (outerAlpha < 0.001) discard;
|
|
285
|
+
|
|
286
|
+
vec4 alb = u_albedo;
|
|
287
|
+
if (u_strokeWidth > 0.0) {
|
|
288
|
+
float sa = smoothstep(-u_strokeWidth - aa, -u_strokeWidth + aa, dist);
|
|
289
|
+
alb = mix(u_albedo, u_strokeAlbedo, sa);
|
|
290
|
+
}
|
|
291
|
+
vec3 N = perturbNormal(normalize(u_normal), v_uv);
|
|
292
|
+
vec3 color = shadePBR(alb.rgb, N, normalize(u_eye - v_worldPos));
|
|
293
|
+
float outA = alb.a * outerAlpha;
|
|
294
|
+
fragColor = vec4(color * outA, outA); // premultiplied
|
|
295
|
+
}`;
|
|
296
|
+
// Lit textured quad (§4.8): images, video, and flattened group-card
|
|
297
|
+
// layers shaded as one surface. Albedo = the texture's own (straight)
|
|
298
|
+
// pixels; same shadePBR as shapes.
|
|
299
|
+
const LIT_TEXTURED_VS = `#version 300 es
|
|
300
|
+
in vec2 a_pos;
|
|
301
|
+
in vec2 a_uv;
|
|
302
|
+
out vec2 v_uv;
|
|
303
|
+
out vec2 v_quadPos;
|
|
304
|
+
out vec3 v_worldPos;
|
|
305
|
+
uniform mat4 u_transform;
|
|
306
|
+
uniform mat4 u_worldMatrix;
|
|
307
|
+
uniform vec4 u_uvRect; // (u0, v0, u1, v1)
|
|
308
|
+
void main() {
|
|
309
|
+
gl_Position = u_transform * vec4(a_pos, 0.0, 1.0);
|
|
310
|
+
v_uv = mix(u_uvRect.xy, u_uvRect.zw, a_uv);
|
|
311
|
+
v_quadPos = a_uv;
|
|
312
|
+
vec4 wp = u_worldMatrix * vec4(a_pos, 0.0, 1.0);
|
|
313
|
+
v_worldPos = wp.xyz;
|
|
314
|
+
}`;
|
|
315
|
+
const LIT_TEXTURED_FS = `#version 300 es
|
|
316
|
+
precision highp float;
|
|
317
|
+
in vec2 v_uv;
|
|
318
|
+
in vec2 v_quadPos;
|
|
319
|
+
in vec3 v_worldPos;
|
|
320
|
+
out vec4 fragColor;
|
|
321
|
+
uniform sampler2D u_tex;
|
|
322
|
+
uniform vec4 u_tint; // premultiplied
|
|
323
|
+
uniform float u_cornerRadius; // PIXELS; 0 disables masking
|
|
324
|
+
uniform vec2 u_size; // pixel (width, height) of the quad
|
|
325
|
+
${PBR_FS_LIB}
|
|
326
|
+
void main() {
|
|
327
|
+
vec4 s = texture(u_tex, v_uv); // premultiplied
|
|
328
|
+
float cov = s.a;
|
|
329
|
+
vec3 albedo = cov > 0.0 ? s.rgb / cov : s.rgb; // straight albedo
|
|
330
|
+
// tint as straight color × opacity (group layers pass (o,o,o,o)).
|
|
331
|
+
vec3 tintRgb = u_tint.a > 0.0 ? u_tint.rgb / u_tint.a : vec3(1.0);
|
|
332
|
+
albedo *= tintRgb;
|
|
333
|
+
|
|
334
|
+
float maskAlpha = 1.0;
|
|
335
|
+
if (u_cornerRadius > 0.0) {
|
|
336
|
+
vec2 p = v_quadPos * u_size;
|
|
337
|
+
vec2 half_ = u_size * 0.5;
|
|
338
|
+
float r = u_cornerRadius;
|
|
339
|
+
vec2 q = abs(p - half_) - half_ + vec2(r);
|
|
340
|
+
float dist = min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r;
|
|
341
|
+
float aa = fwidth(dist);
|
|
342
|
+
maskAlpha = 1.0 - smoothstep(-aa, aa, dist);
|
|
343
|
+
if (maskAlpha < 0.001) discard;
|
|
344
|
+
}
|
|
345
|
+
vec3 N = perturbNormal(normalize(u_normal), v_quadPos);
|
|
346
|
+
vec3 color = shadePBR(albedo, N, normalize(u_eye - v_worldPos));
|
|
347
|
+
float outA = cov * u_tint.a * maskAlpha;
|
|
348
|
+
fragColor = vec4(color * outA, outA); // premultiplied
|
|
349
|
+
}`;
|
|
350
|
+
// Gradient pipeline: shape filled with a linear or radial gradient.
|
|
351
|
+
const GRADIENT_VS = `#version 300 es
|
|
352
|
+
in vec2 a_pos;
|
|
353
|
+
in vec2 a_uv;
|
|
354
|
+
out vec2 v_uv;
|
|
355
|
+
uniform mat4 u_transform;
|
|
356
|
+
void main() {
|
|
357
|
+
gl_Position = u_transform * vec4(a_pos, 0.0, 1.0);
|
|
358
|
+
v_uv = a_uv;
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
const GRADIENT_FS = `#version 300 es
|
|
362
|
+
precision highp float;
|
|
363
|
+
in vec2 v_uv;
|
|
364
|
+
out vec4 fragColor;
|
|
365
|
+
uniform vec4 u_meta; // cornerRadius (PIXELS), shapeType, fillType, numStops
|
|
366
|
+
uniform vec4 u_params; // linear:(cos,sin,_,_) | radial:(cx,cy,radius,_)
|
|
367
|
+
uniform vec2 u_size; // pixel (width, height)
|
|
368
|
+
uniform vec4 u_stops[4]; // 4 stop colors (premultiplied)
|
|
369
|
+
uniform vec4 u_stopOffsets; // 4 stop offsets
|
|
370
|
+
|
|
371
|
+
void main() {
|
|
372
|
+
vec2 uv = v_uv;
|
|
373
|
+
float cornerRadius = u_meta.x;
|
|
374
|
+
float shapeType = u_meta.y;
|
|
375
|
+
float fillType = u_meta.z;
|
|
376
|
+
int nStops = int(u_meta.w);
|
|
377
|
+
|
|
378
|
+
// Shape masking — SDF in pixel space (corners stay circular).
|
|
379
|
+
vec2 p_px = uv * u_size;
|
|
380
|
+
vec2 half_ = u_size * 0.5;
|
|
381
|
+
if (shapeType > 0.5) {
|
|
382
|
+
vec2 d = (p_px - half_) / half_;
|
|
383
|
+
if (dot(d, d) > 1.0) discard;
|
|
384
|
+
} else if (cornerRadius > 0.0) {
|
|
385
|
+
float r = cornerRadius;
|
|
386
|
+
vec2 q = abs(p_px - half_) - (half_ - vec2(r));
|
|
387
|
+
vec2 outside = max(q, vec2(0.0));
|
|
388
|
+
if (length(outside) > r) discard;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Gradient parameter t — runs in UV space (gradient directions are
|
|
392
|
+
// relative to the shape's normalized bounding box).
|
|
393
|
+
float t;
|
|
394
|
+
if (fillType > 0.5) {
|
|
395
|
+
float radius = max(u_params.z, 0.0001);
|
|
396
|
+
t = clamp(distance(uv, u_params.xy) / radius, 0.0, 1.0);
|
|
397
|
+
} else {
|
|
398
|
+
vec2 dir = u_params.xy;
|
|
399
|
+
vec2 centered = uv - vec2(0.5);
|
|
400
|
+
t = clamp(dot(centered, dir) + 0.5, 0.0, 1.0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
vec4 color = u_stops[0];
|
|
404
|
+
for (int i = 0; i < 3; i++) {
|
|
405
|
+
if (i >= nStops - 1) break;
|
|
406
|
+
float off0 = u_stopOffsets[i];
|
|
407
|
+
float off1 = u_stopOffsets[i + 1];
|
|
408
|
+
if (t >= off0 && t <= off1) {
|
|
409
|
+
float segT = (t - off0) / max(off1 - off0, 0.0001);
|
|
410
|
+
color = mix(u_stops[i], u_stops[i + 1], segT);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (t >= u_stopOffsets[nStops - 1]) {
|
|
415
|
+
color = u_stops[nStops - 1];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fragColor = color;
|
|
419
|
+
}
|
|
420
|
+
`;
|
|
421
|
+
const TEXTURED_VS = `#version 300 es
|
|
422
|
+
in vec2 a_pos;
|
|
423
|
+
in vec2 a_uv;
|
|
424
|
+
out vec2 v_uv;
|
|
425
|
+
out vec2 v_quadPos;
|
|
426
|
+
uniform mat4 u_transform;
|
|
427
|
+
uniform vec4 u_uvRect; // (u0, v0, u1, v1)
|
|
428
|
+
void main() {
|
|
429
|
+
gl_Position = u_transform * vec4(a_pos, 0.0, 1.0);
|
|
430
|
+
v_uv = mix(u_uvRect.xy, u_uvRect.zw, a_uv);
|
|
431
|
+
v_quadPos = a_uv;
|
|
432
|
+
}
|
|
433
|
+
`;
|
|
434
|
+
const TEXTURED_FS = `#version 300 es
|
|
435
|
+
precision highp float;
|
|
436
|
+
in vec2 v_uv;
|
|
437
|
+
in vec2 v_quadPos;
|
|
438
|
+
out vec4 fragColor;
|
|
439
|
+
uniform sampler2D u_tex;
|
|
440
|
+
uniform vec4 u_tint; // premultiplied
|
|
441
|
+
uniform float u_cornerRadius; // PIXELS; 0 disables masking
|
|
442
|
+
uniform vec2 u_size; // pixel (width, height) of the quad
|
|
443
|
+
uniform float u_alphaGamma; // coverage exponent; 1 = no-op (see backend.ts)
|
|
444
|
+
void main() {
|
|
445
|
+
vec4 s = texture(u_tex, v_uv); // already premultiplied (UNPACK_PREMULTIPLY_ALPHA_WEBGL)
|
|
446
|
+
if (u_alphaGamma != 1.0) {
|
|
447
|
+
// Reshape coverage: a' = a^g. Premultiplied, so scale the whole
|
|
448
|
+
// sample by a^(g-1); the max() guard keeps g<1 finite at a=0.
|
|
449
|
+
s *= pow(max(s.a, 1e-5), u_alphaGamma - 1.0);
|
|
450
|
+
}
|
|
451
|
+
float maskAlpha = 1.0;
|
|
452
|
+
if (u_cornerRadius > 0.0) {
|
|
453
|
+
// Same rounded-rect SDF as SHAPE_FS, evaluated in the quad's
|
|
454
|
+
// local pixel space (v_quadPos is the un-remapped 0..1 vertex
|
|
455
|
+
// attribute, not the sampling UV).
|
|
456
|
+
vec2 p = v_quadPos * u_size;
|
|
457
|
+
vec2 half_ = u_size * 0.5;
|
|
458
|
+
float r = u_cornerRadius;
|
|
459
|
+
vec2 q = abs(p - half_) - half_ + vec2(r);
|
|
460
|
+
float dist = min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r;
|
|
461
|
+
float aa = fwidth(dist);
|
|
462
|
+
maskAlpha = 1.0 - smoothstep(-aa, aa, dist);
|
|
463
|
+
if (maskAlpha < 0.001) discard;
|
|
464
|
+
}
|
|
465
|
+
fragColor = s * u_tint * maskAlpha;
|
|
466
|
+
}
|
|
467
|
+
`;
|
|
468
|
+
// Masked composite: content sampled through a second texture's alpha or
|
|
469
|
+
// luminance. Shares TEXTURED_VS. Both textures are premultiplied; the
|
|
470
|
+
// whole premultiplied content color scales by the mask factor, which is
|
|
471
|
+
// the correct premultiplied masking operation.
|
|
472
|
+
const MASKED_FS = `#version 300 es
|
|
473
|
+
precision highp float;
|
|
474
|
+
in vec2 v_uv;
|
|
475
|
+
in vec2 v_quadPos;
|
|
476
|
+
out vec4 fragColor;
|
|
477
|
+
uniform sampler2D u_tex; // content (premultiplied)
|
|
478
|
+
uniform sampler2D u_mask; // mask (premultiplied)
|
|
479
|
+
uniform vec4 u_tint; // premultiplied
|
|
480
|
+
uniform float u_mode; // 0 alpha, 1 alpha-inverted, 2 luma, 3 luma-inverted
|
|
481
|
+
void main() {
|
|
482
|
+
vec4 c = texture(u_tex, v_uv) * u_tint;
|
|
483
|
+
vec4 m = texture(u_mask, v_uv);
|
|
484
|
+
float f;
|
|
485
|
+
if (u_mode < 0.5) f = m.a;
|
|
486
|
+
else if (u_mode < 1.5) f = 1.0 - m.a;
|
|
487
|
+
else {
|
|
488
|
+
float luma = dot(m.rgb, vec3(0.2126, 0.7152, 0.0722));
|
|
489
|
+
f = (u_mode < 2.5) ? luma : (1.0 - luma);
|
|
490
|
+
}
|
|
491
|
+
fragColor = c * f;
|
|
492
|
+
}
|
|
493
|
+
`;
|
|
494
|
+
// Backdrop-blend composite (§4.5): piecewise blend modes that can't be
|
|
495
|
+
// fixed-function. Reads the isolated element layer (u_src) and a
|
|
496
|
+
// backdrop snapshot (u_backdrop), both premultiplied + surface-sized,
|
|
497
|
+
// runs the W3C separable composite, and REPLACES the target (caller
|
|
498
|
+
// sets blendFunc to ONE,ZERO). Where the element is transparent the
|
|
499
|
+
// output equals the backdrop, so replacing is a no-op there.
|
|
500
|
+
const BACKDROP_BLEND_FS = `#version 300 es
|
|
501
|
+
precision highp float;
|
|
502
|
+
in vec2 v_uv;
|
|
503
|
+
out vec4 fragColor;
|
|
504
|
+
uniform sampler2D u_src; // element layer, premultiplied
|
|
505
|
+
uniform sampler2D u_backdrop; // backdrop snapshot, premultiplied
|
|
506
|
+
uniform int u_mode; // 0 overlay, 1 hard-light, 2 soft-light
|
|
507
|
+
uniform float u_backdropFlipY; // 1.0 flips backdrop v
|
|
508
|
+
float blendCh(int mode, float cb, float cs) {
|
|
509
|
+
if (mode == 0) { // overlay
|
|
510
|
+
return cb <= 0.5 ? (2.0*cb*cs) : (1.0 - 2.0*(1.0-cb)*(1.0-cs));
|
|
511
|
+
} else if (mode == 1) { // hard-light = overlay(src,backdrop)
|
|
512
|
+
return cs <= 0.5 ? (2.0*cs*cb) : (1.0 - 2.0*(1.0-cs)*(1.0-cb));
|
|
513
|
+
} else { // soft-light (W3C)
|
|
514
|
+
if (cs <= 0.5) return cb - (1.0 - 2.0*cs) * cb * (1.0 - cb);
|
|
515
|
+
float d = cb <= 0.25 ? (((16.0*cb - 12.0)*cb + 4.0)*cb) : sqrt(cb);
|
|
516
|
+
return cb + (2.0*cs - 1.0) * (d - cb);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
void main() {
|
|
520
|
+
vec4 s = texture(u_src, v_uv);
|
|
521
|
+
vec2 buv = vec2(v_uv.x, mix(v_uv.y, 1.0 - v_uv.y, u_backdropFlipY));
|
|
522
|
+
vec4 b = texture(u_backdrop, buv);
|
|
523
|
+
float as = s.a, ab = b.a;
|
|
524
|
+
vec3 Cs = as > 0.0 ? s.rgb / as : vec3(0.0);
|
|
525
|
+
vec3 Cb = ab > 0.0 ? b.rgb / ab : vec3(0.0);
|
|
526
|
+
vec3 Bc = vec3(blendCh(u_mode, Cb.r, Cs.r), blendCh(u_mode, Cb.g, Cs.g), blendCh(u_mode, Cb.b, Cs.b));
|
|
527
|
+
vec3 co = as*(1.0-ab)*Cs + as*ab*Bc + (1.0-as)*ab*Cb; // premultiplied
|
|
528
|
+
float ao = as + ab*(1.0-as);
|
|
529
|
+
fragColor = vec4(co, ao);
|
|
530
|
+
}
|
|
531
|
+
`;
|
|
532
|
+
// Filter composite: a layer texture drawn 1:1 with an optional separable
|
|
533
|
+
// Gaussian blur pass plus color ops. Shares TEXTURED_VS. 25 taps spread
|
|
534
|
+
// over ±3σ; weights computed in-shader and normalized by their sum so
|
|
535
|
+
// edge-clamped taps don't darken. Color ops run on STRAIGHT alpha
|
|
536
|
+
// (unpremultiply → brightness → contrast → saturation → re-premultiply);
|
|
537
|
+
// premultiplied math would drag translucent pixels toward black on the
|
|
538
|
+
// contrast midpoint. Must match the WebGPU FILTERED_SHADER exactly.
|
|
539
|
+
const FILTERED_FS = `#version 300 es
|
|
540
|
+
precision highp float;
|
|
541
|
+
in vec2 v_uv;
|
|
542
|
+
out vec4 fragColor;
|
|
543
|
+
uniform sampler2D u_tex; // layer (premultiplied)
|
|
544
|
+
uniform vec2 u_texel; // blur direction ÷ texture PHYSICAL dims
|
|
545
|
+
uniform float u_sigma; // Gaussian σ in PHYSICAL pixels; 0 = no blur
|
|
546
|
+
uniform vec4 u_colorOps; // (brightness, contrast, saturation, hue radians)
|
|
547
|
+
uniform vec4 u_tint; // premultiplied
|
|
548
|
+
void main() {
|
|
549
|
+
vec4 acc;
|
|
550
|
+
if (u_sigma > 0.0) {
|
|
551
|
+
acc = vec4(0.0);
|
|
552
|
+
float wsum = 0.0;
|
|
553
|
+
for (int i = -12; i <= 12; i++) {
|
|
554
|
+
float d = float(i) * u_sigma * 0.25; // taps cover ±3σ
|
|
555
|
+
float w = exp(-0.5 * d * d / (u_sigma * u_sigma));
|
|
556
|
+
acc += texture(u_tex, v_uv + u_texel * d) * w;
|
|
557
|
+
wsum += w;
|
|
558
|
+
}
|
|
559
|
+
acc /= wsum;
|
|
560
|
+
} else {
|
|
561
|
+
acc = texture(u_tex, v_uv);
|
|
562
|
+
}
|
|
563
|
+
float a = acc.a;
|
|
564
|
+
vec3 c = a > 0.0 ? acc.rgb / a : vec3(0.0);
|
|
565
|
+
c *= u_colorOps.x; // brightness
|
|
566
|
+
c = (c - 0.5) * u_colorOps.y + 0.5; // contrast
|
|
567
|
+
float l = dot(c, vec3(0.2126, 0.7152, 0.0722)); // Rec. 709 luma
|
|
568
|
+
c = mix(vec3(l), c, u_colorOps.z); // saturation
|
|
569
|
+
if (u_colorOps.w != 0.0) { // hue rotate (SVG matrix)
|
|
570
|
+
float hc = cos(u_colorOps.w);
|
|
571
|
+
float hs = sin(u_colorOps.w);
|
|
572
|
+
c = mat3(
|
|
573
|
+
0.213 + 0.787*hc - 0.213*hs, 0.213 - 0.213*hc + 0.143*hs, 0.213 - 0.213*hc - 0.787*hs,
|
|
574
|
+
0.715 - 0.715*hc - 0.715*hs, 0.715 + 0.285*hc + 0.140*hs, 0.715 - 0.715*hc + 0.715*hs,
|
|
575
|
+
0.072 - 0.072*hc + 0.928*hs, 0.072 - 0.072*hc - 0.283*hs, 0.072 + 0.928*hc + 0.072*hs
|
|
576
|
+
) * c;
|
|
577
|
+
}
|
|
578
|
+
c = clamp(c, 0.0, 1.0);
|
|
579
|
+
fragColor = vec4(c * a, a) * u_tint;
|
|
580
|
+
}
|
|
581
|
+
`;
|
|
582
|
+
// Stylize composite: one effects-array pass (§4.7) — pixelate, dither,
|
|
583
|
+
// halftone, or ascii — drawn 1:1 like the filter composite. Shares
|
|
584
|
+
// TEXTURED_VS. The mode is a uniform, so all branches are uniform
|
|
585
|
+
// control flow. Color math runs on STRAIGHT alpha; dot/glyph "ink"
|
|
586
|
+
// scales BOTH color and alpha (premultiplied output). Must match the
|
|
587
|
+
// WebGPU STYLIZED_SHADER exactly.
|
|
588
|
+
const STYLIZED_FS = `#version 300 es
|
|
589
|
+
precision highp float;
|
|
590
|
+
in vec2 v_uv;
|
|
591
|
+
out vec4 fragColor;
|
|
592
|
+
uniform sampler2D u_tex; // layer (premultiplied)
|
|
593
|
+
uniform sampler2D u_aux; // ascii glyph atlas (80×8); layer tex when unused
|
|
594
|
+
uniform vec2 u_texSize; // layer PHYSICAL dims
|
|
595
|
+
uniform vec4 u_params; // (mode, p0, p1, 0) — px params pre-scaled to PHYSICAL
|
|
596
|
+
uniform vec4 u_tint; // premultiplied
|
|
597
|
+
|
|
598
|
+
const float BAYER[16] = float[16](
|
|
599
|
+
0., 8., 2., 10.,
|
|
600
|
+
12., 4., 14., 6.,
|
|
601
|
+
3., 11., 1., 9.,
|
|
602
|
+
15., 7., 13., 5.);
|
|
603
|
+
|
|
604
|
+
// ── Normative noise (§4.7 fractal_noise / turbulent_displace) ──
|
|
605
|
+
// PCG integer hash → value noise (quintic fade) → fBM (lacunarity 2,
|
|
606
|
+
// gain 0.5, per-octave seed+o). Integer ops are bit-exact everywhere.
|
|
607
|
+
uint pcg(uint v) {
|
|
608
|
+
uint s = v * 747796405u + 2891336453u;
|
|
609
|
+
uint w = ((s >> ((s >> 28) + 4u)) ^ s) * 277803737u;
|
|
610
|
+
return (w >> 22) ^ w;
|
|
611
|
+
}
|
|
612
|
+
float h01(ivec3 c, uint seed) {
|
|
613
|
+
return float(pcg(uint(c.x) ^ pcg(uint(c.y) ^ pcg(uint(c.z) ^ pcg(seed))))) / 4294967295.0;
|
|
614
|
+
}
|
|
615
|
+
float vnoise(vec3 p, uint seed) {
|
|
616
|
+
vec3 i = floor(p);
|
|
617
|
+
vec3 f = p - i;
|
|
618
|
+
vec3 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
|
619
|
+
ivec3 c = ivec3(i);
|
|
620
|
+
float n000 = h01(c, seed);
|
|
621
|
+
float n100 = h01(c + ivec3(1, 0, 0), seed);
|
|
622
|
+
float n010 = h01(c + ivec3(0, 1, 0), seed);
|
|
623
|
+
float n110 = h01(c + ivec3(1, 1, 0), seed);
|
|
624
|
+
float n001 = h01(c + ivec3(0, 0, 1), seed);
|
|
625
|
+
float n101 = h01(c + ivec3(1, 0, 1), seed);
|
|
626
|
+
float n011 = h01(c + ivec3(0, 1, 1), seed);
|
|
627
|
+
float n111 = h01(c + ivec3(1, 1, 1), seed);
|
|
628
|
+
return mix(
|
|
629
|
+
mix(mix(n000, n100, u.x), mix(n010, n110, u.x), u.y),
|
|
630
|
+
mix(mix(n001, n101, u.x), mix(n011, n111, u.x), u.y), u.z);
|
|
631
|
+
}
|
|
632
|
+
float fbm(vec3 p, int octaves, uint seed) {
|
|
633
|
+
float v = 0.0;
|
|
634
|
+
float amp = 1.0;
|
|
635
|
+
float wsum = 0.0;
|
|
636
|
+
for (int o = 0; o < 8; o++) {
|
|
637
|
+
if (o >= octaves) break;
|
|
638
|
+
v += amp * vnoise(p, seed + uint(o));
|
|
639
|
+
wsum += amp;
|
|
640
|
+
p *= 2.0;
|
|
641
|
+
amp *= 0.5;
|
|
642
|
+
}
|
|
643
|
+
return v / wsum;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
void main() {
|
|
647
|
+
float mode = u_params.x;
|
|
648
|
+
vec2 px = v_uv * u_texSize;
|
|
649
|
+
if (mode < 0.5) {
|
|
650
|
+
// pixelate — every pixel takes its cell's center sample.
|
|
651
|
+
float cell = max(u_params.y, 1.0);
|
|
652
|
+
vec2 center = (floor(px / cell) + 0.5) * cell;
|
|
653
|
+
fragColor = texture(u_tex, center / u_texSize) * u_tint;
|
|
654
|
+
} else if (mode < 1.5) {
|
|
655
|
+
// dither — per-channel quantize to N levels, 4×4 Bayer threshold
|
|
656
|
+
// indexed by output pixel coords. Alpha (coverage) is untouched.
|
|
657
|
+
vec4 s = texture(u_tex, v_uv);
|
|
658
|
+
float a = s.a;
|
|
659
|
+
vec3 c = a > 0.0 ? s.rgb / a : vec3(0.0);
|
|
660
|
+
// Bayer cells of u_params.z (pixel_size) LOGICAL px: divide device px
|
|
661
|
+
// by (pixelRatio · pixel_size). Resolution-independent — the dot size
|
|
662
|
+
// is stable across preview DPI / export and survives the editor's
|
|
663
|
+
// fit-to-stage downscale instead of smearing into mush.
|
|
664
|
+
ivec2 ip = ivec2(px / max(u_params.w * u_params.z, 1.0));
|
|
665
|
+
float t = (BAYER[(ip.y % 4) * 4 + (ip.x % 4)] + 0.5) / 16.0;
|
|
666
|
+
float n = max(u_params.y, 2.0) - 1.0;
|
|
667
|
+
c = clamp(floor(c * n + t) / n, 0.0, 1.0);
|
|
668
|
+
fragColor = vec4(c * a, a) * u_tint;
|
|
669
|
+
} else if (mode < 2.5) {
|
|
670
|
+
// halftone — rotated dot grid; dot radius ∝ sqrt(luma) so ink AREA
|
|
671
|
+
// tracks luminance; dots tinted with the cell's color. The
|
|
672
|
+
// clamp(r,0,1) factor fades sub-pixel dots instead of popping.
|
|
673
|
+
float cell = max(u_params.y, 2.0);
|
|
674
|
+
float ang = radians(u_params.z);
|
|
675
|
+
mat2 rot = mat2(cos(ang), -sin(ang), sin(ang), cos(ang));
|
|
676
|
+
mat2 inv = mat2(cos(ang), sin(ang), -sin(ang), cos(ang));
|
|
677
|
+
vec2 rp = rot * px;
|
|
678
|
+
vec2 centerR = (floor(rp / cell) + 0.5) * cell;
|
|
679
|
+
vec4 s = texture(u_tex, (inv * centerR) / u_texSize);
|
|
680
|
+
float a = s.a;
|
|
681
|
+
vec3 c = a > 0.0 ? s.rgb / a : vec3(0.0);
|
|
682
|
+
float luma = dot(c, vec3(0.2126, 0.7152, 0.0722)) * a;
|
|
683
|
+
float r = 0.5 * cell * sqrt(luma);
|
|
684
|
+
float d = length(rp - centerR);
|
|
685
|
+
float ink = (1.0 - smoothstep(r - 1.0, r + 1.0, d)) * clamp(r, 0.0, 1.0);
|
|
686
|
+
fragColor = vec4(c, 1.0) * (a * ink) * u_tint;
|
|
687
|
+
} else if (mode < 3.5) {
|
|
688
|
+
// ascii — cells map to the 10-glyph density ramp in the atlas,
|
|
689
|
+
// tinted with the cell's sampled color.
|
|
690
|
+
float cell = max(u_params.y, 4.0);
|
|
691
|
+
vec2 cellOrigin = floor(px / cell) * cell;
|
|
692
|
+
vec4 s = texture(u_tex, (cellOrigin + 0.5 * cell) / u_texSize);
|
|
693
|
+
float a = s.a;
|
|
694
|
+
vec3 c = a > 0.0 ? s.rgb / a : vec3(0.0);
|
|
695
|
+
float luma = dot(c, vec3(0.2126, 0.7152, 0.0722)) * a;
|
|
696
|
+
float idx = clamp(floor(luma * 10.0), 0.0, 9.0);
|
|
697
|
+
vec2 g = clamp(floor((px - cellOrigin) / cell * 8.0), 0.0, 7.0);
|
|
698
|
+
vec2 auxUv = vec2((idx * 8.0 + g.x + 0.5) / 80.0, (g.y + 0.5) / 8.0);
|
|
699
|
+
float ink = texture(u_aux, auxUv).a;
|
|
700
|
+
fragColor = vec4(c, 1.0) * (a * ink) * u_tint;
|
|
701
|
+
} else if (mode < 4.5) {
|
|
702
|
+
// drop_shadow — aux is the ladder-blurred layer; its alpha,
|
|
703
|
+
// offset and tinted, composites UNDER the content (premultiplied
|
|
704
|
+
// under-operator: dst × (1 − src.a)).
|
|
705
|
+
vec4 c = texture(u_tex, v_uv);
|
|
706
|
+
vec2 texel = 1.0 / u_texSize;
|
|
707
|
+
vec2 ouv = clamp(v_uv - vec2(u_params.y, u_params.z) * texel, vec2(0.0), vec2(1.0));
|
|
708
|
+
float sa = texture(u_aux, ouv).a;
|
|
709
|
+
fragColor = c + u_tint * (sa * (1.0 - c.a));
|
|
710
|
+
} else if (mode < 5.5) {
|
|
711
|
+
// glow — blurred silhouette × intensity × color, under the content.
|
|
712
|
+
vec4 c = texture(u_tex, v_uv);
|
|
713
|
+
float ga = clamp(texture(u_aux, v_uv).a * u_params.y, 0.0, 1.0);
|
|
714
|
+
fragColor = c + u_tint * (ga * (1.0 - c.a));
|
|
715
|
+
} else if (mode < 6.5) {
|
|
716
|
+
// stroke — outline band outside the silhouette: max alpha over a
|
|
717
|
+
// 16-tap ring at the stroke width, under the content.
|
|
718
|
+
vec4 c = texture(u_tex, v_uv);
|
|
719
|
+
vec2 texel = 1.0 / u_texSize;
|
|
720
|
+
float w = max(u_params.y, 1.0);
|
|
721
|
+
float s = 0.0;
|
|
722
|
+
for (int i = 0; i < 16; i++) {
|
|
723
|
+
float ang = 6.2831853 * float(i) / 16.0;
|
|
724
|
+
s = max(s, texture(u_tex, clamp(v_uv + vec2(cos(ang), sin(ang)) * w * texel, vec2(0.0), vec2(1.0))).a);
|
|
725
|
+
}
|
|
726
|
+
fragColor = c + u_tint * (s * (1.0 - c.a));
|
|
727
|
+
} else if (mode < 7.5) {
|
|
728
|
+
// chroma_key — BT.709 CbCr distance ramp (§4.7). u_tint.rgb = key
|
|
729
|
+
// color (STRAIGHT), u_tint.a = spill; p0 tolerance, p1 softness.
|
|
730
|
+
vec4 s = texture(u_tex, v_uv);
|
|
731
|
+
vec3 c = s.a > 0.0 ? s.rgb / s.a : vec3(0.0);
|
|
732
|
+
vec3 k = u_tint.rgb;
|
|
733
|
+
const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);
|
|
734
|
+
float cy = dot(c, LUMA);
|
|
735
|
+
float ky = dot(k, LUMA);
|
|
736
|
+
vec2 cc = vec2((c.b - cy) / 1.8556, (c.r - cy) / 1.5748);
|
|
737
|
+
vec2 kc = vec2((k.b - ky) / 1.8556, (k.r - ky) / 1.5748);
|
|
738
|
+
float d = distance(cc, kc);
|
|
739
|
+
float a = u_params.z > 0.0
|
|
740
|
+
? clamp((d - u_params.y) / u_params.z, 0.0, 1.0)
|
|
741
|
+
: (d > u_params.y ? 1.0 : 0.0);
|
|
742
|
+
// Spill suppression: cap the key's dominant channel (ties g→r→b)
|
|
743
|
+
// at the max of the other two, scaled by spill.
|
|
744
|
+
if (k.g >= k.r && k.g >= k.b) c.g -= u_tint.a * max(0.0, c.g - max(c.r, c.b));
|
|
745
|
+
else if (k.r >= k.b) c.r -= u_tint.a * max(0.0, c.r - max(c.g, c.b));
|
|
746
|
+
else c.b -= u_tint.a * max(0.0, c.b - max(c.r, c.g));
|
|
747
|
+
float ao = s.a * a;
|
|
748
|
+
fragColor = vec4(c * ao, ao);
|
|
749
|
+
} else if (mode < 8.5) {
|
|
750
|
+
// luma_key — p0 threshold, p1 softness, u_tint.r = invert flag.
|
|
751
|
+
vec4 s = texture(u_tex, v_uv);
|
|
752
|
+
vec3 c = s.a > 0.0 ? s.rgb / s.a : vec3(0.0);
|
|
753
|
+
float y = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
|
754
|
+
float a = u_params.z > 0.0
|
|
755
|
+
? clamp((y - u_params.y) / u_params.z, 0.0, 1.0)
|
|
756
|
+
: (y > u_params.y ? 1.0 : 0.0);
|
|
757
|
+
if (u_tint.x > 0.5) a = 1.0 - a;
|
|
758
|
+
float ao = s.a * a;
|
|
759
|
+
fragColor = vec4(c * ao, ao);
|
|
760
|
+
} else if (mode < 9.5) {
|
|
761
|
+
// levels — per-channel remap (§4.7): u_tint = (in_black, in_white,
|
|
762
|
+
// out_black, out_white), p0 = gamma; y = x^(1/gamma).
|
|
763
|
+
vec4 s = texture(u_tex, v_uv);
|
|
764
|
+
vec3 c = s.a > 0.0 ? s.rgb / s.a : vec3(0.0);
|
|
765
|
+
vec3 x = clamp((c - u_tint.x) / max(u_tint.y - u_tint.x, 1e-5), 0.0, 1.0);
|
|
766
|
+
x = pow(x, vec3(1.0 / max(u_params.y, 1e-5)));
|
|
767
|
+
c = clamp(u_tint.z + x * (u_tint.w - u_tint.z), 0.0, 1.0);
|
|
768
|
+
fragColor = vec4(c * s.a, s.a);
|
|
769
|
+
} else if (mode < 10.5) {
|
|
770
|
+
// lut — 3D lattice packed as N slices along x in a 2D atlas (aux,
|
|
771
|
+
// N²×N, slice index = blue). Manual trilinear: two bilinear taps
|
|
772
|
+
// mixed across the blue axis. p0 = N, p1 = intensity.
|
|
773
|
+
vec4 s = texture(u_tex, v_uv);
|
|
774
|
+
vec3 c = s.a > 0.0 ? s.rgb / s.a : vec3(0.0);
|
|
775
|
+
float n = max(u_params.y, 2.0);
|
|
776
|
+
float b = clamp(c.b, 0.0, 1.0) * (n - 1.0);
|
|
777
|
+
float b0 = floor(b);
|
|
778
|
+
float b1 = min(b0 + 1.0, n - 1.0);
|
|
779
|
+
vec2 cellUv = vec2(
|
|
780
|
+
(clamp(c.r, 0.0, 1.0) * (n - 1.0) + 0.5) / (n * n),
|
|
781
|
+
(clamp(c.g, 0.0, 1.0) * (n - 1.0) + 0.5) / n);
|
|
782
|
+
vec3 lo = texture(u_aux, cellUv + vec2(b0 / n, 0.0)).rgb;
|
|
783
|
+
vec3 hi = texture(u_aux, cellUv + vec2(b1 / n, 0.0)).rgb;
|
|
784
|
+
c = mix(c, mix(lo, hi, b - b0), clamp(u_params.z, 0.0, 1.0));
|
|
785
|
+
fragColor = vec4(clamp(c, 0.0, 1.0) * s.a, s.a);
|
|
786
|
+
} else if (mode < 11.5) {
|
|
787
|
+
// fractal_noise — grayscale fBM over the element's footprint.
|
|
788
|
+
// p0 = scale px, p1 = evolution,
|
|
789
|
+
// u_tint = (offset_x/scale, offset_y/scale, octaves, seed).
|
|
790
|
+
vec4 s = texture(u_tex, v_uv);
|
|
791
|
+
float v = fbm(
|
|
792
|
+
vec3(px / max(u_params.y, 1e-3) + u_tint.xy, u_params.z),
|
|
793
|
+
int(u_tint.z + 0.5), uint(u_tint.w + 0.5));
|
|
794
|
+
fragColor = vec4(vec3(v) * s.a, s.a);
|
|
795
|
+
} else if (mode < 12.5) {
|
|
796
|
+
// turbulent_displace — sample the layer at p + noise vector.
|
|
797
|
+
// p0 = amount px, p1 = scale px, u_tint = (evolution, octaves, seed, 0).
|
|
798
|
+
float sc = max(u_params.z, 1e-3);
|
|
799
|
+
int oct = int(u_tint.y + 0.5);
|
|
800
|
+
uint sd = uint(u_tint.z + 0.5);
|
|
801
|
+
float dx = fbm(vec3(px / sc, u_tint.x), oct, sd) - 0.5;
|
|
802
|
+
float dy = fbm(vec3(px / sc, u_tint.x), oct, sd + 7919u) - 0.5;
|
|
803
|
+
vec2 duv = vec2(dx, dy) * 2.0 * u_params.y / u_texSize;
|
|
804
|
+
fragColor = texture(u_tex, clamp(v_uv + duv, vec2(0.0), vec2(1.0)));
|
|
805
|
+
} else {
|
|
806
|
+
// bloom_bright — extract pixels brighter than a soft threshold for a
|
|
807
|
+
// whole-frame bloom pass. p0 = threshold, p1 = knee. Output is the
|
|
808
|
+
// straight bright color with alpha 1 so the subsequent blur spreads
|
|
809
|
+
// it cleanly; the composite adds it back × intensity.
|
|
810
|
+
vec4 s = texture(u_tex, v_uv);
|
|
811
|
+
vec3 c = s.a > 0.0 ? s.rgb / s.a : vec3(0.0);
|
|
812
|
+
float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
|
813
|
+
float f = clamp((l - u_params.y) / max(u_params.z, 1e-3), 0.0, 1.0);
|
|
814
|
+
fragColor = vec4(c * f, 1.0);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
`;
|
|
818
|
+
// Glass composite (§4.7 'glass') — faithful port of the
|
|
819
|
+
// ybouane/liquidglass FS_GLASS shader onto our conventions (full-
|
|
820
|
+
// surface quad via TEXTURED_VS, premultiplied snapshot textures,
|
|
821
|
+
// premultiplied output). The pane geometry is ANALYTIC: rounded-rect
|
|
822
|
+
// SDF + half-circle bevel height field, dual-surface (biconvex)
|
|
823
|
+
// refraction or dome magnification, Fresnel + Blinn-Phong lighting,
|
|
824
|
+
// inner stroke, and an outside-only drop shadow. Must match the WebGPU
|
|
825
|
+
// GLASS_SHADER exactly.
|
|
826
|
+
//
|
|
827
|
+
// Two variants from one template (CKP/1.0 glass under 3D, §4.7): the
|
|
828
|
+
// PROJECTIVE variant maps surface px → pane-local through the inverse
|
|
829
|
+
// of the pane's plane homography (exact ray/plane intersection in
|
|
830
|
+
// projective form) and forward-maps refracted sample points back —
|
|
831
|
+
// everything between (SDF, bevel, refraction, light rig) runs in the
|
|
832
|
+
// pane's local frame and tilts with it. The non-projective source is
|
|
833
|
+
// byte-identical to the CKP/1.0 shader (the equivalence gate).
|
|
834
|
+
const glassFsSource = (projective) => `#version 300 es
|
|
835
|
+
precision highp float;
|
|
836
|
+
in vec2 v_uv;
|
|
837
|
+
out vec4 fragColor;
|
|
838
|
+
uniform sampler2D u_backdrop; // FROSTED snapshot (premultiplied)
|
|
839
|
+
uniform sampler2D u_sharp; // UNBLURRED snapshot (premultiplied)
|
|
840
|
+
uniform vec2 u_texSize; // surface PHYSICAL dims
|
|
841
|
+
uniform vec2 u_paneCenter; // pane centre, PHYSICAL px
|
|
842
|
+
uniform vec2 u_paneHalf; // pane half-size, PHYSICAL px
|
|
843
|
+
uniform vec2 u_rot; // (cos θ, sin θ) of pane rotation
|
|
844
|
+
uniform vec4 u_geo; // (cornerRadius, zRadius, bevelMode, bdFlip) PHYSICAL
|
|
845
|
+
uniform vec4 u_optics; // (refract, chroma, edgeHL, fresnel)
|
|
846
|
+
uniform vec4 u_look; // (specular, saturation −1..1, alpha, 0)
|
|
847
|
+
uniform vec4 u_shadow; // (alpha, spread, offY, 0) PHYSICAL
|
|
848
|
+
uniform vec4 u_tint; // STRAIGHT rgba — alpha = strength${projective ? `
|
|
849
|
+
uniform mat3 u_h; // pane-local → surface px (projective)
|
|
850
|
+
uniform mat3 u_hinv; // surface px → pane-local` : ''}
|
|
851
|
+
|
|
852
|
+
float rrSDF(vec2 p, vec2 b, float r) {
|
|
853
|
+
vec2 q = abs(p) - b + vec2(r);
|
|
854
|
+
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Half-circle bevel height field (reference bevelHeight).
|
|
858
|
+
float bevelHeight(float d, float zR) {
|
|
859
|
+
if (d <= 0.0) return 0.0;
|
|
860
|
+
if (d >= zR) return zR;
|
|
861
|
+
return sqrt(d * (2.0 * zR - d));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
vec3 straight3(vec4 s) {
|
|
865
|
+
return s.a > 0.0 ? s.rgb / s.a : vec3(0.0);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
void main() {${projective ? `
|
|
869
|
+
// Pane-local coordinates: invert the pane→surface homography. A
|
|
870
|
+
// non-positive w means the fragment looks past the plane's horizon
|
|
871
|
+
// (behind the camera) — nothing there.
|
|
872
|
+
vec2 px = v_uv * u_texSize;
|
|
873
|
+
vec3 lh = u_hinv * vec3(px, 1.0);
|
|
874
|
+
if (lh.z <= 0.0) { fragColor = vec4(0.0); return; }
|
|
875
|
+
vec2 p = lh.xy / lh.z;` : `
|
|
876
|
+
// Pane-local coordinates (rotate surface px by −θ around the centre).
|
|
877
|
+
vec2 px = v_uv * u_texSize;
|
|
878
|
+
vec2 rel = px - u_paneCenter;
|
|
879
|
+
vec2 p = vec2(rel.x * u_rot.x + rel.y * u_rot.y,
|
|
880
|
+
-rel.x * u_rot.y + rel.y * u_rot.x);`}
|
|
881
|
+
vec2 half_ = u_paneHalf;
|
|
882
|
+
float r = min(u_geo.x, min(half_.x, half_.y));
|
|
883
|
+
float sdf = rrSDF(p, half_, r);
|
|
884
|
+
|
|
885
|
+
// ── Drop shadow — OUTSIDE the panel only ──
|
|
886
|
+
if (sdf > 0.0) {
|
|
887
|
+
float a = 0.0;
|
|
888
|
+
if (u_shadow.x > 0.0) {
|
|
889
|
+
float sdfShadow = rrSDF(p - vec2(0.0, u_shadow.z), half_, r);
|
|
890
|
+
float d = max(sdfShadow - 1.0, 0.0);
|
|
891
|
+
float spread = max(u_shadow.y, 1.0);
|
|
892
|
+
float falloff = 1.0 / (spread * spread);
|
|
893
|
+
float outerShadow = exp(-d * d * falloff) * 0.65;
|
|
894
|
+
float contactShadow = exp(-d * 0.08 / max(spread * 0.04, 0.01)) * 0.35;
|
|
895
|
+
a = (outerShadow + contactShadow) * u_shadow.x;
|
|
896
|
+
}
|
|
897
|
+
fragColor = vec4(0.0, 0.0, 0.0, a);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
float mask = 1.0 - smoothstep(-1.5, 0.5, sdf);
|
|
902
|
+
|
|
903
|
+
float maxD = min(half_.x, half_.y);
|
|
904
|
+
float inside = -sdf;
|
|
905
|
+
float edge = smoothstep(maxD * 0.35, 0.0, inside);
|
|
906
|
+
|
|
907
|
+
// ── Surface normal via the bevel height field (e = 2px, analytic SDF
|
|
908
|
+
// — no blurred-field facets, no measured-gradient lip) ──
|
|
909
|
+
float zR = u_geo.y;
|
|
910
|
+
float e = 2.0;
|
|
911
|
+
float hC = bevelHeight(inside, zR);
|
|
912
|
+
vec2 hGrad = vec2(
|
|
913
|
+
bevelHeight(-rrSDF(p + vec2(e, 0.0), half_, r), zR) -
|
|
914
|
+
bevelHeight(-rrSDF(p - vec2(e, 0.0), half_, r), zR),
|
|
915
|
+
bevelHeight(-rrSDF(p + vec2(0.0, e), half_, r), zR) -
|
|
916
|
+
bevelHeight(-rrSDF(p - vec2(0.0, e), half_, r), zR)) / (2.0 * e);
|
|
917
|
+
vec3 N = normalize(vec3(-hGrad, 1.0));
|
|
918
|
+
|
|
919
|
+
float depth = smoothstep(0.0, zR, inside);
|
|
920
|
+
|
|
921
|
+
// ── Refraction ──
|
|
922
|
+
float refrPow = 1.0 - 1.0 / 1.5;
|
|
923
|
+
float thickNorm = (hC * 2.0) / max(zR * 2.0, 1.0);
|
|
924
|
+
vec2 refrPx;
|
|
925
|
+
if (u_geo.z < 0.5) {
|
|
926
|
+
// Biconvex pill: entry + exit + through-thickness refraction,
|
|
927
|
+
// plus a depth-scaled magnification pull toward the centre.
|
|
928
|
+
vec2 surfRefr = hGrad * refrPow;
|
|
929
|
+
refrPx = (surfRefr * 2.0 + surfRefr * thickNorm * 0.5) * u_optics.x * 30.0;
|
|
930
|
+
vec2 centerDir = -p / max(half_, vec2(1.0));
|
|
931
|
+
refrPx += centerDir * u_optics.x * 4.0 * depth;
|
|
932
|
+
} else {
|
|
933
|
+
// Dome: uniform magnification — contract sampling toward centre.
|
|
934
|
+
refrPx = -p * u_optics.x * depth * 0.35;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ── Chromatic aberration ──
|
|
938
|
+
float caS = u_optics.y * 18.0 * (edge * 0.7 + 0.3) * 2.0;
|
|
939
|
+
vec2 caD = N.xy * caS;
|
|
940
|
+
|
|
941
|
+
${projective ? ` // Pane-local sample points → surface px via the FORWARD homography
|
|
942
|
+
// (refraction and aberration computed in the pane's frame).
|
|
943
|
+
vec3 fR = u_h * vec3(p + refrPx + caD, 1.0);
|
|
944
|
+
vec3 fG = u_h * vec3(p + refrPx, 1.0);
|
|
945
|
+
vec3 fB = u_h * vec3(p + refrPx - caD, 1.0);
|
|
946
|
+
vec2 uvR = clamp(fR.xy / (max(fR.z, 1e-4) * u_texSize), vec2(0.0), vec2(1.0));
|
|
947
|
+
vec2 uvG = clamp(fG.xy / (max(fG.z, 1e-4) * u_texSize), vec2(0.0), vec2(1.0));
|
|
948
|
+
vec2 uvB = clamp(fB.xy / (max(fB.z, 1e-4) * u_texSize), vec2(0.0), vec2(1.0));` : ` // Pane-local offsets → surface space (rotate by +θ) → uv.
|
|
949
|
+
vec2 refrW = vec2(refrPx.x * u_rot.x - refrPx.y * u_rot.y,
|
|
950
|
+
refrPx.x * u_rot.y + refrPx.y * u_rot.x);
|
|
951
|
+
vec2 caW = vec2(caD.x * u_rot.x - caD.y * u_rot.y,
|
|
952
|
+
caD.x * u_rot.y + caD.y * u_rot.x);
|
|
953
|
+
vec2 base = v_uv + refrW / u_texSize;
|
|
954
|
+
vec2 oCA = caW / u_texSize;
|
|
955
|
+
vec2 uvR = clamp(base + oCA, vec2(0.0), vec2(1.0));
|
|
956
|
+
vec2 uvG = clamp(base, vec2(0.0), vec2(1.0));
|
|
957
|
+
vec2 uvB = clamp(base - oCA, vec2(0.0), vec2(1.0));`}
|
|
958
|
+
if (u_geo.w > 0.5) { // GL-canvas snapshots are bottom-up
|
|
959
|
+
uvR.y = 1.0 - uvR.y; uvG.y = 1.0 - uvG.y; uvB.y = 1.0 - uvB.y;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
vec3 sharp = vec3(
|
|
963
|
+
straight3(texture(u_sharp, uvR)).r,
|
|
964
|
+
straight3(texture(u_sharp, uvG)).g,
|
|
965
|
+
straight3(texture(u_sharp, uvB)).b);
|
|
966
|
+
vec3 blur = vec3(
|
|
967
|
+
straight3(texture(u_backdrop, uvR)).r,
|
|
968
|
+
straight3(texture(u_backdrop, uvG)).g,
|
|
969
|
+
straight3(texture(u_backdrop, uvB)).b);
|
|
970
|
+
// Edge-weighted blur mix: centre fully frosted, rim 15% sharp.
|
|
971
|
+
float edgeMix = 1.0 - edge * 0.15;
|
|
972
|
+
vec3 col = mix(sharp, blur, edgeMix);
|
|
973
|
+
|
|
974
|
+
// ── Saturation (reference: 0 = unchanged) ──
|
|
975
|
+
float lum = dot(col, vec3(0.299, 0.587, 0.114));
|
|
976
|
+
col = mix(vec3(lum), col, 1.0 + u_look.y);
|
|
977
|
+
|
|
978
|
+
// ── Tint (our color param in place of the reference's fixed cool tint) ──
|
|
979
|
+
col = mix(col, u_tint.rgb, u_tint.a);
|
|
980
|
+
col *= 1.0 + 0.06 * depth;
|
|
981
|
+
|
|
982
|
+
// ── Fresnel ──
|
|
983
|
+
float fres = pow(1.0 - abs(N.z), 4.0) * u_optics.w;
|
|
984
|
+
|
|
985
|
+
// ── Specular highlights (multi-light Blinn-Phong, reference lights) ──
|
|
986
|
+
vec3 V = vec3(0.0, 0.0, 1.0);
|
|
987
|
+
vec3 L1 = normalize(vec3(0.4, 0.7, 1.0));
|
|
988
|
+
float sp = pow(max(dot(N, normalize(L1 + V)), 0.0), 90.0);
|
|
989
|
+
vec3 L2 = normalize(vec3(-0.3, -0.5, 1.0));
|
|
990
|
+
sp += pow(max(dot(N, normalize(L2 + V)), 0.0), 50.0) * 0.3;
|
|
991
|
+
vec3 L3 = normalize(vec3(0.1, 0.3, 1.0));
|
|
992
|
+
sp += pow(max(dot(N, L3), 0.0), 6.0) * 0.1;
|
|
993
|
+
vec3 L4 = normalize(vec3(0.0, 0.9, 0.4));
|
|
994
|
+
sp += pow(max(dot(N, normalize(L4 + V)), 0.0), 120.0) * 0.6;
|
|
995
|
+
float totalSpec = sp * u_look.x;
|
|
996
|
+
|
|
997
|
+
// ── Inner border / stroke highlight ──
|
|
998
|
+
float borderWidth = 1.5;
|
|
999
|
+
float innerStroke = smoothstep(-borderWidth - 1.0, -borderWidth, sdf)
|
|
1000
|
+
* (1.0 - smoothstep(-1.0, 0.0, sdf));
|
|
1001
|
+
float topBias = 0.5 + 0.5 * (-p.y / half_.y);
|
|
1002
|
+
innerStroke *= (0.4 + 0.6 * topBias);
|
|
1003
|
+
|
|
1004
|
+
// ── Edge highlight & inner glow ──
|
|
1005
|
+
float rim = edge * u_optics.z * 0.22;
|
|
1006
|
+
float innerGlow = smoothstep(5.0, 0.0, -sdf) * u_optics.z * 0.15;
|
|
1007
|
+
|
|
1008
|
+
// ── Environment-like reflection (fake) ──
|
|
1009
|
+
float envRefl = (N.y * 0.5 + 0.5) * fres * 0.08;
|
|
1010
|
+
|
|
1011
|
+
// ── Composite ──
|
|
1012
|
+
vec3 fin = col;
|
|
1013
|
+
fin += vec3(totalSpec);
|
|
1014
|
+
fin += vec3(rim + innerGlow);
|
|
1015
|
+
fin += vec3(innerStroke * u_optics.z * 0.55);
|
|
1016
|
+
fin += vec3(envRefl);
|
|
1017
|
+
fin = mix(fin, vec3(1.0), fres * 0.2);
|
|
1018
|
+
|
|
1019
|
+
float outA = mask * u_look.z;
|
|
1020
|
+
fragColor = vec4(clamp(fin, 0.0, 1.0), 1.0) * outA;
|
|
1021
|
+
}
|
|
1022
|
+
`;
|
|
1023
|
+
// ─── Unit quad — same convention as the WebGPU backend ─────────────────────
|
|
1024
|
+
// prettier-ignore
|
|
1025
|
+
const UNIT_QUAD_VERTICES = new Float32Array([
|
|
1026
|
+
-1, -1, 0, 1,
|
|
1027
|
+
1, -1, 1, 1,
|
|
1028
|
+
-1, 1, 0, 0,
|
|
1029
|
+
-1, 1, 0, 0,
|
|
1030
|
+
1, -1, 1, 1,
|
|
1031
|
+
1, 1, 1, 0,
|
|
1032
|
+
]);
|
|
1033
|
+
export class WebGL2Backend {
|
|
1034
|
+
canvas;
|
|
1035
|
+
width;
|
|
1036
|
+
height;
|
|
1037
|
+
capabilities;
|
|
1038
|
+
gl;
|
|
1039
|
+
vbo;
|
|
1040
|
+
vao;
|
|
1041
|
+
shapeProgram;
|
|
1042
|
+
shapeLocs;
|
|
1043
|
+
shadowProgram;
|
|
1044
|
+
shadowLocs;
|
|
1045
|
+
gradientProgram;
|
|
1046
|
+
gradientLocs;
|
|
1047
|
+
texturedProgram;
|
|
1048
|
+
texturedLocs;
|
|
1049
|
+
maskedProgram;
|
|
1050
|
+
maskedLocs;
|
|
1051
|
+
backdropBlendProgram;
|
|
1052
|
+
backdropBlendLocs;
|
|
1053
|
+
filteredProgram;
|
|
1054
|
+
filteredLocs;
|
|
1055
|
+
stylizedProgram;
|
|
1056
|
+
stylizedLocs;
|
|
1057
|
+
glassProgram;
|
|
1058
|
+
glassLocs;
|
|
1059
|
+
// Lazy projective variant (CKP/1.0 glass under 3D) — compiled on
|
|
1060
|
+
// first use so 2D documents never pay for it.
|
|
1061
|
+
glass3dProgram = null;
|
|
1062
|
+
glass3dLocs = null;
|
|
1063
|
+
// Lazy PBR lit-shape program (§4.8) — only compiled when a lit shape
|
|
1064
|
+
// is first drawn; unlit documents never pay for it.
|
|
1065
|
+
litShapeProgram = null;
|
|
1066
|
+
litShapeLocs = null;
|
|
1067
|
+
// Lazy PBR lit-textured program (§4.8) — lit images / video / group
|
|
1068
|
+
// cards. Compiled on first lit textured draw.
|
|
1069
|
+
litTexturedProgram = null;
|
|
1070
|
+
litTexturedLocs = null;
|
|
1071
|
+
nextTextureId = 1;
|
|
1072
|
+
liveTextures = new Set();
|
|
1073
|
+
framingActive = false;
|
|
1074
|
+
disposed = false;
|
|
1075
|
+
/** Physical backing-store dims ÷ logical dims (renderResolution). */
|
|
1076
|
+
pixelRatio = 1;
|
|
1077
|
+
/**
|
|
1078
|
+
* Offscreen-surface stack. Empty = drawing to the canvas. Each entry
|
|
1079
|
+
* redirects draws into a framebuffer; flipY compensates for GL's
|
|
1080
|
+
* bottom-up framebuffer textures so layers sample top-down like
|
|
1081
|
+
* uploaded images.
|
|
1082
|
+
*/
|
|
1083
|
+
surfaceStack = [];
|
|
1084
|
+
renderTargetFbos = new Map();
|
|
1085
|
+
/**
|
|
1086
|
+
* Set the blend function for the next draw. All draw methods call
|
|
1087
|
+
* this with their params' blend (or undefined → premultiplied over),
|
|
1088
|
+
* so state never leaks between draws. Alpha always composites with
|
|
1089
|
+
* source-over so blended elements still build coverage normally.
|
|
1090
|
+
*/
|
|
1091
|
+
applyBlend(mode) {
|
|
1092
|
+
const gl = this.gl;
|
|
1093
|
+
switch (mode) {
|
|
1094
|
+
case 'add':
|
|
1095
|
+
gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1096
|
+
break;
|
|
1097
|
+
case 'multiply':
|
|
1098
|
+
gl.blendFuncSeparate(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1099
|
+
break;
|
|
1100
|
+
case 'screen':
|
|
1101
|
+
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_COLOR, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1102
|
+
break;
|
|
1103
|
+
default:
|
|
1104
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
/** Dims + flip of whatever we're currently drawing into. */
|
|
1108
|
+
currentSurface() {
|
|
1109
|
+
const top = this.surfaceStack[this.surfaceStack.length - 1];
|
|
1110
|
+
if (top)
|
|
1111
|
+
return top;
|
|
1112
|
+
return {
|
|
1113
|
+
fbo: null,
|
|
1114
|
+
width: this.width,
|
|
1115
|
+
height: this.height,
|
|
1116
|
+
physWidth: this.canvas.width,
|
|
1117
|
+
physHeight: this.canvas.height,
|
|
1118
|
+
flipY: false,
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
constructor(canvas) {
|
|
1122
|
+
this.canvas = canvas;
|
|
1123
|
+
this.width = canvas.width;
|
|
1124
|
+
this.height = canvas.height;
|
|
1125
|
+
}
|
|
1126
|
+
async init() {
|
|
1127
|
+
const log = getLogger();
|
|
1128
|
+
const gl = this.canvas.getContext('webgl2', {
|
|
1129
|
+
alpha: true,
|
|
1130
|
+
premultipliedAlpha: true,
|
|
1131
|
+
preserveDrawingBuffer: false,
|
|
1132
|
+
antialias: true,
|
|
1133
|
+
});
|
|
1134
|
+
if (!gl) {
|
|
1135
|
+
log.warn('WebGL2 not available in this environment');
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
this.gl = gl;
|
|
1139
|
+
// Build pipelines.
|
|
1140
|
+
try {
|
|
1141
|
+
this.shapeProgram = this.buildProgram(SHAPE_VS, SHAPE_FS, 'shape');
|
|
1142
|
+
this.shapeLocs = {
|
|
1143
|
+
aPos: gl.getAttribLocation(this.shapeProgram, 'a_pos'),
|
|
1144
|
+
aUv: gl.getAttribLocation(this.shapeProgram, 'a_uv'),
|
|
1145
|
+
uTransform: gl.getUniformLocation(this.shapeProgram, 'u_transform'),
|
|
1146
|
+
uColor: gl.getUniformLocation(this.shapeProgram, 'u_color'),
|
|
1147
|
+
uStrokeColor: gl.getUniformLocation(this.shapeProgram, 'u_strokeColor'),
|
|
1148
|
+
uStrokeWidth: gl.getUniformLocation(this.shapeProgram, 'u_strokeWidth'),
|
|
1149
|
+
uCornerRadius: gl.getUniformLocation(this.shapeProgram, 'u_cornerRadius'),
|
|
1150
|
+
uShapeType: gl.getUniformLocation(this.shapeProgram, 'u_shapeType'),
|
|
1151
|
+
uSize: gl.getUniformLocation(this.shapeProgram, 'u_size'),
|
|
1152
|
+
};
|
|
1153
|
+
this.shadowProgram = this.buildProgram(SHAPE_VS, SHADOW_FS, 'shadow');
|
|
1154
|
+
this.shadowLocs = {
|
|
1155
|
+
aPos: gl.getAttribLocation(this.shadowProgram, 'a_pos'),
|
|
1156
|
+
aUv: gl.getAttribLocation(this.shadowProgram, 'a_uv'),
|
|
1157
|
+
uTransform: gl.getUniformLocation(this.shadowProgram, 'u_transform'),
|
|
1158
|
+
uColor: gl.getUniformLocation(this.shadowProgram, 'u_color'),
|
|
1159
|
+
uBlur: gl.getUniformLocation(this.shadowProgram, 'u_blur'),
|
|
1160
|
+
uCornerRadius: gl.getUniformLocation(this.shadowProgram, 'u_cornerRadius'),
|
|
1161
|
+
uShapeType: gl.getUniformLocation(this.shadowProgram, 'u_shapeType'),
|
|
1162
|
+
uSize: gl.getUniformLocation(this.shadowProgram, 'u_size'),
|
|
1163
|
+
uQuadSize: gl.getUniformLocation(this.shadowProgram, 'u_quadSize'),
|
|
1164
|
+
};
|
|
1165
|
+
this.gradientProgram = this.buildProgram(GRADIENT_VS, GRADIENT_FS, 'gradient');
|
|
1166
|
+
this.gradientLocs = {
|
|
1167
|
+
aPos: gl.getAttribLocation(this.gradientProgram, 'a_pos'),
|
|
1168
|
+
aUv: gl.getAttribLocation(this.gradientProgram, 'a_uv'),
|
|
1169
|
+
uTransform: gl.getUniformLocation(this.gradientProgram, 'u_transform'),
|
|
1170
|
+
uMeta: gl.getUniformLocation(this.gradientProgram, 'u_meta'),
|
|
1171
|
+
uParams: gl.getUniformLocation(this.gradientProgram, 'u_params'),
|
|
1172
|
+
uSize: gl.getUniformLocation(this.gradientProgram, 'u_size'),
|
|
1173
|
+
uStops: gl.getUniformLocation(this.gradientProgram, 'u_stops'),
|
|
1174
|
+
uStopOffsets: gl.getUniformLocation(this.gradientProgram, 'u_stopOffsets'),
|
|
1175
|
+
};
|
|
1176
|
+
this.texturedProgram = this.buildProgram(TEXTURED_VS, TEXTURED_FS, 'textured');
|
|
1177
|
+
this.texturedLocs = {
|
|
1178
|
+
aPos: gl.getAttribLocation(this.texturedProgram, 'a_pos'),
|
|
1179
|
+
aUv: gl.getAttribLocation(this.texturedProgram, 'a_uv'),
|
|
1180
|
+
uTransform: gl.getUniformLocation(this.texturedProgram, 'u_transform'),
|
|
1181
|
+
uUvRect: gl.getUniformLocation(this.texturedProgram, 'u_uvRect'),
|
|
1182
|
+
uTint: gl.getUniformLocation(this.texturedProgram, 'u_tint'),
|
|
1183
|
+
uTex: gl.getUniformLocation(this.texturedProgram, 'u_tex'),
|
|
1184
|
+
uCornerRadius: gl.getUniformLocation(this.texturedProgram, 'u_cornerRadius'),
|
|
1185
|
+
uSize: gl.getUniformLocation(this.texturedProgram, 'u_size'),
|
|
1186
|
+
uAlphaGamma: gl.getUniformLocation(this.texturedProgram, 'u_alphaGamma'),
|
|
1187
|
+
};
|
|
1188
|
+
this.maskedProgram = this.buildProgram(TEXTURED_VS, MASKED_FS, 'masked');
|
|
1189
|
+
this.maskedLocs = {
|
|
1190
|
+
aPos: gl.getAttribLocation(this.maskedProgram, 'a_pos'),
|
|
1191
|
+
aUv: gl.getAttribLocation(this.maskedProgram, 'a_uv'),
|
|
1192
|
+
uTransform: gl.getUniformLocation(this.maskedProgram, 'u_transform'),
|
|
1193
|
+
uUvRect: gl.getUniformLocation(this.maskedProgram, 'u_uvRect'),
|
|
1194
|
+
uTex: gl.getUniformLocation(this.maskedProgram, 'u_tex'),
|
|
1195
|
+
uMask: gl.getUniformLocation(this.maskedProgram, 'u_mask'),
|
|
1196
|
+
uTint: gl.getUniformLocation(this.maskedProgram, 'u_tint'),
|
|
1197
|
+
uMode: gl.getUniformLocation(this.maskedProgram, 'u_mode'),
|
|
1198
|
+
};
|
|
1199
|
+
this.filteredProgram = this.buildProgram(TEXTURED_VS, FILTERED_FS, 'filtered');
|
|
1200
|
+
this.filteredLocs = {
|
|
1201
|
+
aPos: gl.getAttribLocation(this.filteredProgram, 'a_pos'),
|
|
1202
|
+
aUv: gl.getAttribLocation(this.filteredProgram, 'a_uv'),
|
|
1203
|
+
uTransform: gl.getUniformLocation(this.filteredProgram, 'u_transform'),
|
|
1204
|
+
uUvRect: gl.getUniformLocation(this.filteredProgram, 'u_uvRect'),
|
|
1205
|
+
uTex: gl.getUniformLocation(this.filteredProgram, 'u_tex'),
|
|
1206
|
+
uTexel: gl.getUniformLocation(this.filteredProgram, 'u_texel'),
|
|
1207
|
+
uSigma: gl.getUniformLocation(this.filteredProgram, 'u_sigma'),
|
|
1208
|
+
uColorOps: gl.getUniformLocation(this.filteredProgram, 'u_colorOps'),
|
|
1209
|
+
uTint: gl.getUniformLocation(this.filteredProgram, 'u_tint'),
|
|
1210
|
+
};
|
|
1211
|
+
this.stylizedProgram = this.buildProgram(TEXTURED_VS, STYLIZED_FS, 'stylized');
|
|
1212
|
+
this.stylizedLocs = {
|
|
1213
|
+
aPos: gl.getAttribLocation(this.stylizedProgram, 'a_pos'),
|
|
1214
|
+
aUv: gl.getAttribLocation(this.stylizedProgram, 'a_uv'),
|
|
1215
|
+
uTransform: gl.getUniformLocation(this.stylizedProgram, 'u_transform'),
|
|
1216
|
+
uUvRect: gl.getUniformLocation(this.stylizedProgram, 'u_uvRect'),
|
|
1217
|
+
uTex: gl.getUniformLocation(this.stylizedProgram, 'u_tex'),
|
|
1218
|
+
uAux: gl.getUniformLocation(this.stylizedProgram, 'u_aux'),
|
|
1219
|
+
uTexSize: gl.getUniformLocation(this.stylizedProgram, 'u_texSize'),
|
|
1220
|
+
uParams: gl.getUniformLocation(this.stylizedProgram, 'u_params'),
|
|
1221
|
+
uTint: gl.getUniformLocation(this.stylizedProgram, 'u_tint'),
|
|
1222
|
+
};
|
|
1223
|
+
this.glassProgram = this.buildProgram(TEXTURED_VS, glassFsSource(false), 'glass');
|
|
1224
|
+
this.glassLocs = this.glassLocsOf(this.glassProgram);
|
|
1225
|
+
this.backdropBlendProgram = this.buildProgram(TEXTURED_VS, BACKDROP_BLEND_FS, 'backdropBlend');
|
|
1226
|
+
this.backdropBlendLocs = {
|
|
1227
|
+
aPos: gl.getAttribLocation(this.backdropBlendProgram, 'a_pos'),
|
|
1228
|
+
aUv: gl.getAttribLocation(this.backdropBlendProgram, 'a_uv'),
|
|
1229
|
+
uTransform: gl.getUniformLocation(this.backdropBlendProgram, 'u_transform'),
|
|
1230
|
+
uUvRect: gl.getUniformLocation(this.backdropBlendProgram, 'u_uvRect'),
|
|
1231
|
+
uSrc: gl.getUniformLocation(this.backdropBlendProgram, 'u_src'),
|
|
1232
|
+
uBackdrop: gl.getUniformLocation(this.backdropBlendProgram, 'u_backdrop'),
|
|
1233
|
+
uMode: gl.getUniformLocation(this.backdropBlendProgram, 'u_mode'),
|
|
1234
|
+
uBackdropFlipY: gl.getUniformLocation(this.backdropBlendProgram, 'u_backdropFlipY'),
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
catch (err) {
|
|
1238
|
+
log.error('WebGL2 shader compile failed:', err instanceof Error ? err.message : String(err));
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
// Vertex buffer + VAO.
|
|
1242
|
+
this.vbo = gl.createBuffer();
|
|
1243
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
|
|
1244
|
+
gl.bufferData(gl.ARRAY_BUFFER, UNIT_QUAD_VERTICES, gl.STATIC_DRAW);
|
|
1245
|
+
this.vao = gl.createVertexArray();
|
|
1246
|
+
gl.bindVertexArray(this.vao);
|
|
1247
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
|
|
1248
|
+
// Two attributes share the same VBO; we re-enable them per program because
|
|
1249
|
+
// the attribute indices may differ between shape and textured programs.
|
|
1250
|
+
// We bind them here for the shape program; in drawTexturedQuad we re-bind
|
|
1251
|
+
// for the textured program if locations differ.
|
|
1252
|
+
this.setupVertexAttribs(this.shapeLocs.aPos, this.shapeLocs.aUv);
|
|
1253
|
+
gl.bindVertexArray(null);
|
|
1254
|
+
// Premultiplied alpha blending.
|
|
1255
|
+
gl.enable(gl.BLEND);
|
|
1256
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1257
|
+
// Texture upload defaults.
|
|
1258
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
1259
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
1260
|
+
this.capabilities = {
|
|
1261
|
+
api: 'webgl2',
|
|
1262
|
+
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
|
|
1263
|
+
};
|
|
1264
|
+
log.info(`WebGL2 backend ready (maxTextureSize=${this.capabilities.maxTextureSize})`);
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
buildProgram(vsSrc, fsSrc, label) {
|
|
1268
|
+
const gl = this.gl;
|
|
1269
|
+
const vs = this.compileShader(gl.VERTEX_SHADER, vsSrc, `${label}.vs`);
|
|
1270
|
+
const fs = this.compileShader(gl.FRAGMENT_SHADER, fsSrc, `${label}.fs`);
|
|
1271
|
+
const program = gl.createProgram();
|
|
1272
|
+
gl.attachShader(program, vs);
|
|
1273
|
+
gl.attachShader(program, fs);
|
|
1274
|
+
gl.linkProgram(program);
|
|
1275
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1276
|
+
const info = gl.getProgramInfoLog(program);
|
|
1277
|
+
throw new Error(`Link error (${label}): ${info}`);
|
|
1278
|
+
}
|
|
1279
|
+
gl.deleteShader(vs);
|
|
1280
|
+
gl.deleteShader(fs);
|
|
1281
|
+
return program;
|
|
1282
|
+
}
|
|
1283
|
+
compileShader(type, src, label) {
|
|
1284
|
+
const gl = this.gl;
|
|
1285
|
+
const shader = gl.createShader(type);
|
|
1286
|
+
gl.shaderSource(shader, src);
|
|
1287
|
+
gl.compileShader(shader);
|
|
1288
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1289
|
+
const info = gl.getShaderInfoLog(shader);
|
|
1290
|
+
throw new Error(`Compile error (${label}): ${info}`);
|
|
1291
|
+
}
|
|
1292
|
+
return shader;
|
|
1293
|
+
}
|
|
1294
|
+
setupVertexAttribs(aPos, aUv) {
|
|
1295
|
+
const gl = this.gl;
|
|
1296
|
+
gl.enableVertexAttribArray(aPos);
|
|
1297
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0);
|
|
1298
|
+
gl.enableVertexAttribArray(aUv);
|
|
1299
|
+
gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 16, 8);
|
|
1300
|
+
}
|
|
1301
|
+
resize(width, height, pixelRatio = 1) {
|
|
1302
|
+
if (this.disposed)
|
|
1303
|
+
return;
|
|
1304
|
+
const physW = Math.max(1, Math.round(width * pixelRatio));
|
|
1305
|
+
const physH = Math.max(1, Math.round(height * pixelRatio));
|
|
1306
|
+
if (width === this.width &&
|
|
1307
|
+
height === this.height &&
|
|
1308
|
+
this.canvas.width === physW &&
|
|
1309
|
+
this.canvas.height === physH)
|
|
1310
|
+
return;
|
|
1311
|
+
this.width = width;
|
|
1312
|
+
this.height = height;
|
|
1313
|
+
this.pixelRatio = pixelRatio;
|
|
1314
|
+
this.canvas.width = physW;
|
|
1315
|
+
this.canvas.height = physH;
|
|
1316
|
+
this.gl.viewport(0, 0, physW, physH);
|
|
1317
|
+
}
|
|
1318
|
+
// ─── Textures ─────────────────────────────────────────────────────────────
|
|
1319
|
+
createTexture(source) {
|
|
1320
|
+
const { width, height } = sourceDimensions(source);
|
|
1321
|
+
const gl = this.gl;
|
|
1322
|
+
const handle = gl.createTexture();
|
|
1323
|
+
gl.bindTexture(gl.TEXTURE_2D, handle);
|
|
1324
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1325
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1326
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1327
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1328
|
+
this.uploadToTexture(handle, source);
|
|
1329
|
+
const texture = { id: this.nextTextureId++, width, height, handle };
|
|
1330
|
+
this.liveTextures.add(texture);
|
|
1331
|
+
return texture;
|
|
1332
|
+
}
|
|
1333
|
+
updateTexture(texture, source) {
|
|
1334
|
+
const t = texture;
|
|
1335
|
+
this.uploadToTexture(t.handle, source);
|
|
1336
|
+
}
|
|
1337
|
+
uploadToTexture(handle, source) {
|
|
1338
|
+
const gl = this.gl;
|
|
1339
|
+
gl.bindTexture(gl.TEXTURE_2D, handle);
|
|
1340
|
+
// texImage2D accepts each of our source types directly.
|
|
1341
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE,
|
|
1342
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1343
|
+
source);
|
|
1344
|
+
}
|
|
1345
|
+
destroyTexture(texture) {
|
|
1346
|
+
const t = texture;
|
|
1347
|
+
if (this.liveTextures.delete(t)) {
|
|
1348
|
+
this.gl.deleteTexture(t.handle);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// ─── Frame lifecycle ──────────────────────────────────────────────────────
|
|
1352
|
+
beginFrame(clearColor = [0, 0, 0, 1]) {
|
|
1353
|
+
if (this.framingActive) {
|
|
1354
|
+
getLogger().warn('beginFrame called while another frame is in progress');
|
|
1355
|
+
this.endFrame();
|
|
1356
|
+
}
|
|
1357
|
+
this.framingActive = true;
|
|
1358
|
+
// Defensive: a frame never starts mid-target.
|
|
1359
|
+
this.surfaceStack.length = 0;
|
|
1360
|
+
const gl = this.gl;
|
|
1361
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1362
|
+
// Viewport covers the PHYSICAL backing store — using logical dims
|
|
1363
|
+
// here broke hi-res (pixelRatio > 1) rendering by drawing into the
|
|
1364
|
+
// bottom-left fraction of the canvas.
|
|
1365
|
+
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
1366
|
+
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
|
|
1367
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1368
|
+
gl.bindVertexArray(this.vao);
|
|
1369
|
+
}
|
|
1370
|
+
endFrame() {
|
|
1371
|
+
if (!this.framingActive)
|
|
1372
|
+
return;
|
|
1373
|
+
this.framingActive = false;
|
|
1374
|
+
if (this.surfaceStack.length > 0) {
|
|
1375
|
+
getLogger().warn('endFrame with unbalanced pushTarget — restoring canvas');
|
|
1376
|
+
this.surfaceStack.length = 0;
|
|
1377
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
1378
|
+
}
|
|
1379
|
+
// WebGL has no explicit submit; the browser presents after the current
|
|
1380
|
+
// RAF callback returns.
|
|
1381
|
+
this.gl.bindVertexArray(null);
|
|
1382
|
+
}
|
|
1383
|
+
// ─── Offscreen render targets ─────────────────────────────────────────────
|
|
1384
|
+
createRenderTarget(width, height) {
|
|
1385
|
+
const gl = this.gl;
|
|
1386
|
+
const physW = Math.max(1, Math.round(width * this.pixelRatio));
|
|
1387
|
+
const physH = Math.max(1, Math.round(height * this.pixelRatio));
|
|
1388
|
+
const handle = gl.createTexture();
|
|
1389
|
+
gl.bindTexture(gl.TEXTURE_2D, handle);
|
|
1390
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1391
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1392
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1393
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1394
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, physW, physH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
1395
|
+
const texture = { id: this.nextTextureId++, width: physW, height: physH, handle };
|
|
1396
|
+
this.liveTextures.add(texture);
|
|
1397
|
+
const fbo = gl.createFramebuffer();
|
|
1398
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
1399
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, handle, 0);
|
|
1400
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
1401
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.currentSurface().fbo);
|
|
1402
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
1403
|
+
throw new Error(`render target framebuffer incomplete (0x${status.toString(16)})`);
|
|
1404
|
+
}
|
|
1405
|
+
const target = { texture, width, height };
|
|
1406
|
+
this.renderTargetFbos.set(target, fbo);
|
|
1407
|
+
return target;
|
|
1408
|
+
}
|
|
1409
|
+
destroyRenderTarget(target) {
|
|
1410
|
+
const fbo = this.renderTargetFbos.get(target);
|
|
1411
|
+
if (fbo) {
|
|
1412
|
+
this.gl.deleteFramebuffer(fbo);
|
|
1413
|
+
this.renderTargetFbos.delete(target);
|
|
1414
|
+
}
|
|
1415
|
+
this.destroyTexture(target.texture);
|
|
1416
|
+
}
|
|
1417
|
+
pushTarget(target, clearColor = [0, 0, 0, 0]) {
|
|
1418
|
+
const fbo = this.renderTargetFbos.get(target);
|
|
1419
|
+
if (!fbo) {
|
|
1420
|
+
getLogger().warn('pushTarget with unknown / destroyed target — ignored');
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const gl = this.gl;
|
|
1424
|
+
const tex = target.texture;
|
|
1425
|
+
this.surfaceStack.push({
|
|
1426
|
+
fbo,
|
|
1427
|
+
width: target.width,
|
|
1428
|
+
height: target.height,
|
|
1429
|
+
physWidth: tex.width,
|
|
1430
|
+
physHeight: tex.height,
|
|
1431
|
+
flipY: true,
|
|
1432
|
+
});
|
|
1433
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
1434
|
+
gl.viewport(0, 0, tex.width, tex.height);
|
|
1435
|
+
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
|
|
1436
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1437
|
+
}
|
|
1438
|
+
popTarget() {
|
|
1439
|
+
if (this.surfaceStack.length === 0) {
|
|
1440
|
+
getLogger().warn('popTarget without matching pushTarget — ignored');
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
this.surfaceStack.pop();
|
|
1444
|
+
const s = this.currentSurface();
|
|
1445
|
+
const gl = this.gl;
|
|
1446
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, s.fbo);
|
|
1447
|
+
gl.viewport(0, 0, s.physWidth, s.physHeight);
|
|
1448
|
+
}
|
|
1449
|
+
// ─── Drawing ──────────────────────────────────────────────────────────────
|
|
1450
|
+
drawShapeShadow(params) {
|
|
1451
|
+
if (!this.framingActive)
|
|
1452
|
+
return;
|
|
1453
|
+
this.applyBlend(undefined);
|
|
1454
|
+
if (params.blur <= 0 && params.offsetX === 0 && params.offsetY === 0)
|
|
1455
|
+
return;
|
|
1456
|
+
const gl = this.gl;
|
|
1457
|
+
const blur = Math.max(0, params.blur);
|
|
1458
|
+
// The shadow quad spans the shape PLUS `blur` pixels on every
|
|
1459
|
+
// side. Center it on the shape, then displace by the user's offset.
|
|
1460
|
+
const quadW = params.width + blur * 2;
|
|
1461
|
+
const quadH = params.height + blur * 2;
|
|
1462
|
+
const surface = this.currentSurface();
|
|
1463
|
+
const transform = params.transform
|
|
1464
|
+
? projectPixelMatrix(params.transform, surface.width, surface.height, surface.flipY)
|
|
1465
|
+
: composeQuadTransform(params.cx + params.offsetX, params.cy + params.offsetY, quadW, quadH, params.rotation, surface.width, surface.height, params.skewX ?? 0, params.skewY ?? 0, surface.flipY);
|
|
1466
|
+
const cornerRadius = Math.max(0, Math.min(params.cornerRadius ?? 0, Math.min(params.width, params.height) * 0.5));
|
|
1467
|
+
const shapeType = params.shape === 'ellipse' ? 1 : 0;
|
|
1468
|
+
gl.useProgram(this.shadowProgram);
|
|
1469
|
+
this.setupVertexAttribs(this.shadowLocs.aPos, this.shadowLocs.aUv);
|
|
1470
|
+
if (this.shadowLocs.uTransform)
|
|
1471
|
+
gl.uniformMatrix4fv(this.shadowLocs.uTransform, false, transform);
|
|
1472
|
+
if (this.shadowLocs.uColor)
|
|
1473
|
+
gl.uniform4f(this.shadowLocs.uColor, params.color[0], params.color[1], params.color[2], params.color[3]);
|
|
1474
|
+
if (this.shadowLocs.uBlur)
|
|
1475
|
+
gl.uniform1f(this.shadowLocs.uBlur, blur);
|
|
1476
|
+
if (this.shadowLocs.uCornerRadius)
|
|
1477
|
+
gl.uniform1f(this.shadowLocs.uCornerRadius, cornerRadius);
|
|
1478
|
+
if (this.shadowLocs.uShapeType)
|
|
1479
|
+
gl.uniform1f(this.shadowLocs.uShapeType, shapeType);
|
|
1480
|
+
if (this.shadowLocs.uSize)
|
|
1481
|
+
gl.uniform2f(this.shadowLocs.uSize, params.width, params.height);
|
|
1482
|
+
if (this.shadowLocs.uQuadSize)
|
|
1483
|
+
gl.uniform2f(this.shadowLocs.uQuadSize, quadW, quadH);
|
|
1484
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1485
|
+
}
|
|
1486
|
+
drawShape(params) {
|
|
1487
|
+
if (!this.framingActive)
|
|
1488
|
+
return;
|
|
1489
|
+
if (params.gradient) {
|
|
1490
|
+
this.drawGradientShape(params);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (params.lit) {
|
|
1494
|
+
this.drawLitShape(params);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const gl = this.gl;
|
|
1498
|
+
this.applyBlend(params.blend);
|
|
1499
|
+
const surface = this.currentSurface();
|
|
1500
|
+
const transform = params.transform
|
|
1501
|
+
? projectPixelMatrix(params.transform, surface.width, surface.height, surface.flipY)
|
|
1502
|
+
: composeQuadTransform(params.cx, params.cy, params.width, params.height, params.rotation, surface.width, surface.height, params.skewX ?? 0, params.skewY ?? 0, surface.flipY);
|
|
1503
|
+
// cornerRadius is PIXELS — clamp to half the smaller side so overflowing
|
|
1504
|
+
// values still produce a sensible shape (full-corner pill or circle).
|
|
1505
|
+
const cornerRadius = Math.max(0, Math.min(params.cornerRadius ?? 0, Math.min(params.width, params.height) * 0.5));
|
|
1506
|
+
const shapeType = params.shape === 'ellipse' ? 1 : 0;
|
|
1507
|
+
gl.useProgram(this.shapeProgram);
|
|
1508
|
+
this.setupVertexAttribs(this.shapeLocs.aPos, this.shapeLocs.aUv);
|
|
1509
|
+
if (this.shapeLocs.uTransform)
|
|
1510
|
+
gl.uniformMatrix4fv(this.shapeLocs.uTransform, false, transform);
|
|
1511
|
+
if (this.shapeLocs.uColor)
|
|
1512
|
+
gl.uniform4f(this.shapeLocs.uColor, params.color[0], params.color[1], params.color[2], params.color[3]);
|
|
1513
|
+
const sw = params.strokeWidth ?? 0;
|
|
1514
|
+
const sc = params.strokeColor ?? params.color;
|
|
1515
|
+
if (this.shapeLocs.uStrokeColor)
|
|
1516
|
+
gl.uniform4f(this.shapeLocs.uStrokeColor, sc[0], sc[1], sc[2], sc[3]);
|
|
1517
|
+
if (this.shapeLocs.uStrokeWidth)
|
|
1518
|
+
gl.uniform1f(this.shapeLocs.uStrokeWidth, sw);
|
|
1519
|
+
if (this.shapeLocs.uCornerRadius)
|
|
1520
|
+
gl.uniform1f(this.shapeLocs.uCornerRadius, cornerRadius);
|
|
1521
|
+
if (this.shapeLocs.uShapeType)
|
|
1522
|
+
gl.uniform1f(this.shapeLocs.uShapeType, shapeType);
|
|
1523
|
+
if (this.shapeLocs.uSize)
|
|
1524
|
+
gl.uniform2f(this.shapeLocs.uSize, params.width, params.height);
|
|
1525
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1526
|
+
}
|
|
1527
|
+
getLitShapeProgram() {
|
|
1528
|
+
if (!this.litShapeProgram) {
|
|
1529
|
+
const gl = this.gl;
|
|
1530
|
+
const prog = this.buildProgram(LIT_SHAPE_VS, LIT_SHAPE_FS, 'litShape');
|
|
1531
|
+
const u = (n) => gl.getUniformLocation(prog, n);
|
|
1532
|
+
this.litShapeProgram = prog;
|
|
1533
|
+
this.litShapeLocs = {
|
|
1534
|
+
aPos: gl.getAttribLocation(prog, 'a_pos'),
|
|
1535
|
+
aUv: gl.getAttribLocation(prog, 'a_uv'),
|
|
1536
|
+
u: {
|
|
1537
|
+
transform: u('u_transform'), worldMatrix: u('u_worldMatrix'),
|
|
1538
|
+
albedo: u('u_albedo'), strokeAlbedo: u('u_strokeAlbedo'),
|
|
1539
|
+
strokeWidth: u('u_strokeWidth'), cornerRadius: u('u_cornerRadius'),
|
|
1540
|
+
shapeType: u('u_shapeType'), size: u('u_size'),
|
|
1541
|
+
normal: u('u_normal'), eye: u('u_eye'),
|
|
1542
|
+
rough: u('u_rough'), metal: u('u_metal'), reflect: u('u_reflect'),
|
|
1543
|
+
emissive: u('u_emissive'), ambient: u('u_ambient'),
|
|
1544
|
+
numLights: u('u_numLights'), lightDir: u('u_lightDir'), lightColor: u('u_lightColor'),
|
|
1545
|
+
envCount: u('u_envCount'), envColor: u('u_envColor'), envOffset: u('u_envOffset'), envAvg: u('u_envAvg'),
|
|
1546
|
+
tangent: u('u_tangent'), bitangent: u('u_bitangent'), normalScale: u('u_normalScale'),
|
|
1547
|
+
hasNormalMap: u('u_hasNormalMap'), normalMap: u('u_normalMap'),
|
|
1548
|
+
envIsImage: u('u_envIsImage'), envMap: u('u_envMap'),
|
|
1549
|
+
},
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
return this.litShapeProgram;
|
|
1553
|
+
}
|
|
1554
|
+
drawLitShape(params) {
|
|
1555
|
+
const lit = params.lit;
|
|
1556
|
+
const gl = this.gl;
|
|
1557
|
+
this.applyBlend(params.blend);
|
|
1558
|
+
const surface = this.currentSurface();
|
|
1559
|
+
const transform = params.transform
|
|
1560
|
+
? projectPixelMatrix(params.transform, surface.width, surface.height, surface.flipY)
|
|
1561
|
+
: composeQuadTransform(params.cx, params.cy, params.width, params.height, params.rotation, surface.width, surface.height, params.skewX ?? 0, params.skewY ?? 0, surface.flipY);
|
|
1562
|
+
const cornerRadius = Math.max(0, Math.min(params.cornerRadius ?? 0, Math.min(params.width, params.height) * 0.5));
|
|
1563
|
+
const shapeType = params.shape === 'ellipse' ? 1 : 0;
|
|
1564
|
+
this.getLitShapeProgram();
|
|
1565
|
+
const { aPos, aUv, u } = this.litShapeLocs;
|
|
1566
|
+
gl.useProgram(this.litShapeProgram);
|
|
1567
|
+
this.setupVertexAttribs(aPos, aUv);
|
|
1568
|
+
if (u.transform)
|
|
1569
|
+
gl.uniformMatrix4fv(u.transform, false, transform);
|
|
1570
|
+
if (u.worldMatrix)
|
|
1571
|
+
gl.uniformMatrix4fv(u.worldMatrix, false, Array.from(lit.worldMatrix));
|
|
1572
|
+
const alb = lit.albedo;
|
|
1573
|
+
if (u.albedo)
|
|
1574
|
+
gl.uniform4f(u.albedo, alb[0], alb[1], alb[2], alb[3]);
|
|
1575
|
+
const salb = lit.strokeAlbedo ?? alb;
|
|
1576
|
+
if (u.strokeAlbedo)
|
|
1577
|
+
gl.uniform4f(u.strokeAlbedo, salb[0], salb[1], salb[2], salb[3]);
|
|
1578
|
+
if (u.strokeWidth)
|
|
1579
|
+
gl.uniform1f(u.strokeWidth, params.strokeWidth ?? 0);
|
|
1580
|
+
if (u.cornerRadius)
|
|
1581
|
+
gl.uniform1f(u.cornerRadius, cornerRadius);
|
|
1582
|
+
if (u.shapeType)
|
|
1583
|
+
gl.uniform1f(u.shapeType, shapeType);
|
|
1584
|
+
if (u.size)
|
|
1585
|
+
gl.uniform2f(u.size, params.width, params.height);
|
|
1586
|
+
this.setLitPbrUniforms(u, lit);
|
|
1587
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1588
|
+
}
|
|
1589
|
+
// Set the PBR uniforms shared by the lit-shape and lit-textured
|
|
1590
|
+
// programs (material, normal/eye, lights, environment). The env count
|
|
1591
|
+
// is set every draw — uniforms persist per-program, so a prior
|
|
1592
|
+
// env-bearing draw must not leak into one without an environment.
|
|
1593
|
+
setLitPbrUniforms(u, lit) {
|
|
1594
|
+
const gl = this.gl;
|
|
1595
|
+
if (u.normal)
|
|
1596
|
+
gl.uniform3f(u.normal, lit.normal[0], lit.normal[1], lit.normal[2]);
|
|
1597
|
+
if (u.eye)
|
|
1598
|
+
gl.uniform3f(u.eye, lit.eye[0], lit.eye[1], lit.eye[2]);
|
|
1599
|
+
if (u.rough)
|
|
1600
|
+
gl.uniform1f(u.rough, lit.roughness);
|
|
1601
|
+
if (u.metal)
|
|
1602
|
+
gl.uniform1f(u.metal, lit.metalness);
|
|
1603
|
+
if (u.reflect)
|
|
1604
|
+
gl.uniform1f(u.reflect, lit.reflectivity);
|
|
1605
|
+
if (u.emissive)
|
|
1606
|
+
gl.uniform1f(u.emissive, lit.emissive);
|
|
1607
|
+
if (u.ambient)
|
|
1608
|
+
gl.uniform3f(u.ambient, lit.ambient[0], lit.ambient[1], lit.ambient[2]);
|
|
1609
|
+
const n = Math.min(4, lit.lightDirs.length);
|
|
1610
|
+
if (u.numLights)
|
|
1611
|
+
gl.uniform1i(u.numLights, n);
|
|
1612
|
+
if (n > 0) {
|
|
1613
|
+
const dirs = new Float32Array(12);
|
|
1614
|
+
const cols = new Float32Array(12);
|
|
1615
|
+
for (let i = 0; i < n; i++) {
|
|
1616
|
+
dirs[i * 3] = lit.lightDirs[i][0];
|
|
1617
|
+
dirs[i * 3 + 1] = lit.lightDirs[i][1];
|
|
1618
|
+
dirs[i * 3 + 2] = lit.lightDirs[i][2];
|
|
1619
|
+
cols[i * 3] = lit.lightColors[i][0];
|
|
1620
|
+
cols[i * 3 + 1] = lit.lightColors[i][1];
|
|
1621
|
+
cols[i * 3 + 2] = lit.lightColors[i][2];
|
|
1622
|
+
}
|
|
1623
|
+
if (u.lightDir)
|
|
1624
|
+
gl.uniform3fv(u.lightDir, dirs.subarray(0, n * 3));
|
|
1625
|
+
if (u.lightColor)
|
|
1626
|
+
gl.uniform3fv(u.lightColor, cols.subarray(0, n * 3));
|
|
1627
|
+
}
|
|
1628
|
+
const env = lit.env;
|
|
1629
|
+
const ec = env ? Math.min(4, env.stopColors.length) : 0;
|
|
1630
|
+
if (u.envCount)
|
|
1631
|
+
gl.uniform1i(u.envCount, ec);
|
|
1632
|
+
if (ec > 0 && env) {
|
|
1633
|
+
const ecol = new Float32Array(12);
|
|
1634
|
+
const eoff = new Float32Array(4);
|
|
1635
|
+
for (let i = 0; i < ec; i++) {
|
|
1636
|
+
ecol[i * 3] = env.stopColors[i][0];
|
|
1637
|
+
ecol[i * 3 + 1] = env.stopColors[i][1];
|
|
1638
|
+
ecol[i * 3 + 2] = env.stopColors[i][2];
|
|
1639
|
+
eoff[i] = env.stopOffsets[i];
|
|
1640
|
+
}
|
|
1641
|
+
if (u.envColor)
|
|
1642
|
+
gl.uniform3fv(u.envColor, ecol.subarray(0, ec * 3));
|
|
1643
|
+
if (u.envOffset)
|
|
1644
|
+
gl.uniform1fv(u.envOffset, eoff.subarray(0, ec));
|
|
1645
|
+
}
|
|
1646
|
+
// avg is the roughness-blur fallback for BOTH gradient and image envs.
|
|
1647
|
+
if (env && u.envAvg)
|
|
1648
|
+
gl.uniform3f(u.envAvg, env.avg[0], env.avg[1], env.avg[2]);
|
|
1649
|
+
// §4.8 Phase 2 normal map — bound to texture unit 1 (default flat
|
|
1650
|
+
// when absent so the sampler is always valid). Restore unit 0 after.
|
|
1651
|
+
const nm = lit.normalMap;
|
|
1652
|
+
if (u.hasNormalMap)
|
|
1653
|
+
gl.uniform1i(u.hasNormalMap, nm ? 1 : 0);
|
|
1654
|
+
if (u.normalScale)
|
|
1655
|
+
gl.uniform1f(u.normalScale, nm ? nm.scale : 1);
|
|
1656
|
+
if (nm) {
|
|
1657
|
+
if (u.tangent)
|
|
1658
|
+
gl.uniform3f(u.tangent, nm.tangent[0], nm.tangent[1], nm.tangent[2]);
|
|
1659
|
+
if (u.bitangent)
|
|
1660
|
+
gl.uniform3f(u.bitangent, nm.bitangent[0], nm.bitangent[1], nm.bitangent[2]);
|
|
1661
|
+
}
|
|
1662
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1663
|
+
gl.bindTexture(gl.TEXTURE_2D, nm ? nm.texture.handle : this.getFlatNormalTexture());
|
|
1664
|
+
if (u.normalMap)
|
|
1665
|
+
gl.uniform1i(u.normalMap, 1);
|
|
1666
|
+
// §4.8 Phase 3 image environment — bound to unit 2 (default flat when
|
|
1667
|
+
// absent; only sampled when u_envIsImage = 1).
|
|
1668
|
+
const envImg = env?.image;
|
|
1669
|
+
if (u.envIsImage)
|
|
1670
|
+
gl.uniform1i(u.envIsImage, envImg ? 1 : 0);
|
|
1671
|
+
gl.activeTexture(gl.TEXTURE2);
|
|
1672
|
+
gl.bindTexture(gl.TEXTURE_2D, envImg ? envImg.handle : this.getFlatNormalTexture());
|
|
1673
|
+
if (u.envMap)
|
|
1674
|
+
gl.uniform1i(u.envMap, 2);
|
|
1675
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1676
|
+
}
|
|
1677
|
+
// 1×1 flat tangent-space normal (#8080ff = (0,0,1)) bound when a lit
|
|
1678
|
+
// draw has no normal map, so the sampler always references a texture.
|
|
1679
|
+
flatNormalTex = null;
|
|
1680
|
+
getFlatNormalTexture() {
|
|
1681
|
+
if (!this.flatNormalTex) {
|
|
1682
|
+
const gl = this.gl;
|
|
1683
|
+
const tex = gl.createTexture();
|
|
1684
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1685
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([128, 128, 255, 255]));
|
|
1686
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
1687
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
1688
|
+
this.flatNormalTex = tex;
|
|
1689
|
+
}
|
|
1690
|
+
return this.flatNormalTex;
|
|
1691
|
+
}
|
|
1692
|
+
getLitTexturedProgram() {
|
|
1693
|
+
if (!this.litTexturedProgram) {
|
|
1694
|
+
const gl = this.gl;
|
|
1695
|
+
const prog = this.buildProgram(LIT_TEXTURED_VS, LIT_TEXTURED_FS, 'litTextured');
|
|
1696
|
+
const u = (n) => gl.getUniformLocation(prog, n);
|
|
1697
|
+
this.litTexturedProgram = prog;
|
|
1698
|
+
this.litTexturedLocs = {
|
|
1699
|
+
aPos: gl.getAttribLocation(prog, 'a_pos'),
|
|
1700
|
+
aUv: gl.getAttribLocation(prog, 'a_uv'),
|
|
1701
|
+
u: {
|
|
1702
|
+
transform: u('u_transform'), worldMatrix: u('u_worldMatrix'), uvRect: u('u_uvRect'),
|
|
1703
|
+
tex: u('u_tex'), tint: u('u_tint'), cornerRadius: u('u_cornerRadius'), size: u('u_size'),
|
|
1704
|
+
normal: u('u_normal'), eye: u('u_eye'),
|
|
1705
|
+
rough: u('u_rough'), metal: u('u_metal'), reflect: u('u_reflect'),
|
|
1706
|
+
emissive: u('u_emissive'), ambient: u('u_ambient'),
|
|
1707
|
+
numLights: u('u_numLights'), lightDir: u('u_lightDir'), lightColor: u('u_lightColor'),
|
|
1708
|
+
envCount: u('u_envCount'), envColor: u('u_envColor'), envOffset: u('u_envOffset'), envAvg: u('u_envAvg'),
|
|
1709
|
+
tangent: u('u_tangent'), bitangent: u('u_bitangent'), normalScale: u('u_normalScale'),
|
|
1710
|
+
hasNormalMap: u('u_hasNormalMap'), normalMap: u('u_normalMap'),
|
|
1711
|
+
envIsImage: u('u_envIsImage'), envMap: u('u_envMap'),
|
|
1712
|
+
},
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
return this.litTexturedProgram;
|
|
1716
|
+
}
|
|
1717
|
+
drawLitTexturedQuad(params) {
|
|
1718
|
+
const lit = params.lit;
|
|
1719
|
+
const gl = this.gl;
|
|
1720
|
+
this.applyBlend(params.blend);
|
|
1721
|
+
const surface = this.currentSurface();
|
|
1722
|
+
const transform = params.transform
|
|
1723
|
+
? projectPixelMatrix(params.transform, surface.width, surface.height, surface.flipY)
|
|
1724
|
+
: composeQuadTransform(params.cx, params.cy, params.width, params.height, params.rotation, surface.width, surface.height, params.skewX ?? 0, params.skewY ?? 0, surface.flipY);
|
|
1725
|
+
const cornerRadius = Math.max(0, Math.min(params.cornerRadius ?? 0, Math.min(params.width, params.height) * 0.5));
|
|
1726
|
+
const uvRect = params.uvRect ?? [0, 0, 1, 1];
|
|
1727
|
+
const tint = params.tint ?? [1, 1, 1, 1];
|
|
1728
|
+
this.getLitTexturedProgram();
|
|
1729
|
+
const { aPos, aUv, u } = this.litTexturedLocs;
|
|
1730
|
+
gl.useProgram(this.litTexturedProgram);
|
|
1731
|
+
this.setupVertexAttribs(aPos, aUv);
|
|
1732
|
+
if (u.transform)
|
|
1733
|
+
gl.uniformMatrix4fv(u.transform, false, transform);
|
|
1734
|
+
if (u.worldMatrix)
|
|
1735
|
+
gl.uniformMatrix4fv(u.worldMatrix, false, Array.from(lit.worldMatrix));
|
|
1736
|
+
if (u.uvRect)
|
|
1737
|
+
gl.uniform4f(u.uvRect, uvRect[0], uvRect[1], uvRect[2], uvRect[3]);
|
|
1738
|
+
if (u.tint)
|
|
1739
|
+
gl.uniform4f(u.tint, tint[0], tint[1], tint[2], tint[3]);
|
|
1740
|
+
if (u.cornerRadius)
|
|
1741
|
+
gl.uniform1f(u.cornerRadius, cornerRadius);
|
|
1742
|
+
if (u.size)
|
|
1743
|
+
gl.uniform2f(u.size, params.width, params.height);
|
|
1744
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1745
|
+
gl.bindTexture(gl.TEXTURE_2D, params.texture.handle);
|
|
1746
|
+
if (u.tex)
|
|
1747
|
+
gl.uniform1i(u.tex, 0);
|
|
1748
|
+
this.setLitPbrUniforms(u, lit);
|
|
1749
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1750
|
+
}
|
|
1751
|
+
drawGradientShape(params) {
|
|
1752
|
+
if (!this.framingActive || !params.gradient)
|
|
1753
|
+
return;
|
|
1754
|
+
const gl = this.gl;
|
|
1755
|
+
this.applyBlend(params.blend);
|
|
1756
|
+
const surface = this.currentSurface();
|
|
1757
|
+
const transform = params.transform
|
|
1758
|
+
? projectPixelMatrix(params.transform, surface.width, surface.height, surface.flipY)
|
|
1759
|
+
: composeQuadTransform(params.cx, params.cy, params.width, params.height, params.rotation, surface.width, surface.height, params.skewX ?? 0, params.skewY ?? 0, surface.flipY);
|
|
1760
|
+
const cornerRadius = Math.max(0, Math.min(params.cornerRadius ?? 0, Math.min(params.width, params.height) * 0.5));
|
|
1761
|
+
const shapeType = params.shape === 'ellipse' ? 1 : 0;
|
|
1762
|
+
const g = params.gradient;
|
|
1763
|
+
const fillType = g.type === 'radial' ? 1 : 0;
|
|
1764
|
+
const stops = g.stops.slice(0, 4);
|
|
1765
|
+
const nStops = Math.max(2, stops.length);
|
|
1766
|
+
// Flatten stop colors into a 16-float array (4 stops × 4 floats); pad
|
|
1767
|
+
// missing slots with the last stop's color.
|
|
1768
|
+
const stopColors = new Float32Array(16);
|
|
1769
|
+
for (let i = 0; i < 4; i++) {
|
|
1770
|
+
const stop = stops[i] ?? stops[stops.length - 1];
|
|
1771
|
+
stopColors[i * 4] = stop.color[0];
|
|
1772
|
+
stopColors[i * 4 + 1] = stop.color[1];
|
|
1773
|
+
stopColors[i * 4 + 2] = stop.color[2];
|
|
1774
|
+
stopColors[i * 4 + 3] = stop.color[3];
|
|
1775
|
+
}
|
|
1776
|
+
const stopOffsets = new Float32Array(4);
|
|
1777
|
+
for (let i = 0; i < 4; i++)
|
|
1778
|
+
stopOffsets[i] = stops[i] ? stops[i].offset : 1;
|
|
1779
|
+
gl.useProgram(this.gradientProgram);
|
|
1780
|
+
this.setupVertexAttribs(this.gradientLocs.aPos, this.gradientLocs.aUv);
|
|
1781
|
+
if (this.gradientLocs.uTransform)
|
|
1782
|
+
gl.uniformMatrix4fv(this.gradientLocs.uTransform, false, transform);
|
|
1783
|
+
if (this.gradientLocs.uMeta)
|
|
1784
|
+
gl.uniform4f(this.gradientLocs.uMeta, cornerRadius, shapeType, fillType, nStops);
|
|
1785
|
+
if (this.gradientLocs.uSize)
|
|
1786
|
+
gl.uniform2f(this.gradientLocs.uSize, params.width, params.height);
|
|
1787
|
+
if (this.gradientLocs.uParams) {
|
|
1788
|
+
if (g.type === 'linear') {
|
|
1789
|
+
gl.uniform4f(this.gradientLocs.uParams, Math.cos(g.angle), Math.sin(g.angle), 0, 0);
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
gl.uniform4f(this.gradientLocs.uParams, g.cx, g.cy, g.radius, 0);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
if (this.gradientLocs.uStops)
|
|
1796
|
+
gl.uniform4fv(this.gradientLocs.uStops, stopColors);
|
|
1797
|
+
if (this.gradientLocs.uStopOffsets)
|
|
1798
|
+
gl.uniform4fv(this.gradientLocs.uStopOffsets, stopOffsets);
|
|
1799
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1800
|
+
}
|
|
1801
|
+
drawTexturedQuad(params) {
|
|
1802
|
+
if (!this.framingActive)
|
|
1803
|
+
return;
|
|
1804
|
+
if (params.lit) {
|
|
1805
|
+
this.drawLitTexturedQuad(params);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const gl = this.gl;
|
|
1809
|
+
this.applyBlend(params.blend);
|
|
1810
|
+
const surface = this.currentSurface();
|
|
1811
|
+
const transform = params.transform
|
|
1812
|
+
? projectPixelMatrix(params.transform, surface.width, surface.height, surface.flipY)
|
|
1813
|
+
: composeQuadTransform(params.cx, params.cy, params.width, params.height, params.rotation, surface.width, surface.height, params.skewX ?? 0, params.skewY ?? 0, surface.flipY);
|
|
1814
|
+
const uvRect = params.uvRect ?? [0, 0, 1, 1];
|
|
1815
|
+
const tint = params.tint ?? [1, 1, 1, 1];
|
|
1816
|
+
gl.useProgram(this.texturedProgram);
|
|
1817
|
+
this.setupVertexAttribs(this.texturedLocs.aPos, this.texturedLocs.aUv);
|
|
1818
|
+
if (this.texturedLocs.uTransform)
|
|
1819
|
+
gl.uniformMatrix4fv(this.texturedLocs.uTransform, false, transform);
|
|
1820
|
+
if (this.texturedLocs.uUvRect)
|
|
1821
|
+
gl.uniform4f(this.texturedLocs.uUvRect, uvRect[0], uvRect[1], uvRect[2], uvRect[3]);
|
|
1822
|
+
if (this.texturedLocs.uTint)
|
|
1823
|
+
gl.uniform4f(this.texturedLocs.uTint, tint[0], tint[1], tint[2], tint[3]);
|
|
1824
|
+
const cornerRadius = Math.max(0, Math.min(params.cornerRadius ?? 0, Math.min(params.width, params.height) * 0.5));
|
|
1825
|
+
if (this.texturedLocs.uCornerRadius)
|
|
1826
|
+
gl.uniform1f(this.texturedLocs.uCornerRadius, cornerRadius);
|
|
1827
|
+
if (this.texturedLocs.uSize)
|
|
1828
|
+
gl.uniform2f(this.texturedLocs.uSize, params.width, params.height);
|
|
1829
|
+
if (this.texturedLocs.uAlphaGamma)
|
|
1830
|
+
gl.uniform1f(this.texturedLocs.uAlphaGamma, params.alphaGamma ?? 1);
|
|
1831
|
+
const t = params.texture;
|
|
1832
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1833
|
+
gl.bindTexture(gl.TEXTURE_2D, t.handle);
|
|
1834
|
+
if (this.texturedLocs.uTex)
|
|
1835
|
+
gl.uniform1i(this.texturedLocs.uTex, 0);
|
|
1836
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1837
|
+
}
|
|
1838
|
+
drawMaskedQuad(params) {
|
|
1839
|
+
if (!this.framingActive)
|
|
1840
|
+
return;
|
|
1841
|
+
const gl = this.gl;
|
|
1842
|
+
this.applyBlend(params.blend);
|
|
1843
|
+
const surface = this.currentSurface();
|
|
1844
|
+
const transform = params.transform
|
|
1845
|
+
? projectPixelMatrix(params.transform, surface.width, surface.height, surface.flipY)
|
|
1846
|
+
: composeQuadTransform(params.cx, params.cy, params.width, params.height, params.rotation, surface.width, surface.height, 0, 0, surface.flipY);
|
|
1847
|
+
const tint = params.tint ?? [1, 1, 1, 1];
|
|
1848
|
+
const mode = params.mode === 'alpha' ? 0 :
|
|
1849
|
+
params.mode === 'alpha-inverted' ? 1 :
|
|
1850
|
+
params.mode === 'luma' ? 2 : 3;
|
|
1851
|
+
gl.useProgram(this.maskedProgram);
|
|
1852
|
+
this.setupVertexAttribs(this.maskedLocs.aPos, this.maskedLocs.aUv);
|
|
1853
|
+
if (this.maskedLocs.uTransform)
|
|
1854
|
+
gl.uniformMatrix4fv(this.maskedLocs.uTransform, false, transform);
|
|
1855
|
+
if (this.maskedLocs.uUvRect)
|
|
1856
|
+
gl.uniform4f(this.maskedLocs.uUvRect, 0, 0, 1, 1);
|
|
1857
|
+
if (this.maskedLocs.uTint)
|
|
1858
|
+
gl.uniform4f(this.maskedLocs.uTint, tint[0], tint[1], tint[2], tint[3]);
|
|
1859
|
+
if (this.maskedLocs.uMode)
|
|
1860
|
+
gl.uniform1f(this.maskedLocs.uMode, mode);
|
|
1861
|
+
const content = params.content;
|
|
1862
|
+
const mask = params.mask;
|
|
1863
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1864
|
+
gl.bindTexture(gl.TEXTURE_2D, content.handle);
|
|
1865
|
+
if (this.maskedLocs.uTex)
|
|
1866
|
+
gl.uniform1i(this.maskedLocs.uTex, 0);
|
|
1867
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1868
|
+
gl.bindTexture(gl.TEXTURE_2D, mask.handle);
|
|
1869
|
+
if (this.maskedLocs.uMask)
|
|
1870
|
+
gl.uniform1i(this.maskedLocs.uMask, 1);
|
|
1871
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1872
|
+
// Restore the conventional active unit so later single-texture
|
|
1873
|
+
// draws bind where they expect.
|
|
1874
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1875
|
+
}
|
|
1876
|
+
drawFilteredQuad(params) {
|
|
1877
|
+
if (!this.framingActive)
|
|
1878
|
+
return;
|
|
1879
|
+
const gl = this.gl;
|
|
1880
|
+
this.applyBlend(params.blend);
|
|
1881
|
+
const surface = this.currentSurface();
|
|
1882
|
+
const transform = composeQuadTransform(params.cx, params.cy, params.width, params.height, 0, surface.width, surface.height, 0, 0, surface.flipY);
|
|
1883
|
+
const tint = params.tint ?? [1, 1, 1, 1];
|
|
1884
|
+
const t = params.texture;
|
|
1885
|
+
// blurRadius is logical px; texture dims are physical, so σ scales
|
|
1886
|
+
// by the pixel ratio and texel offsets divide by physical dims.
|
|
1887
|
+
const sigma = params.blurRadius * this.pixelRatio;
|
|
1888
|
+
gl.useProgram(this.filteredProgram);
|
|
1889
|
+
this.setupVertexAttribs(this.filteredLocs.aPos, this.filteredLocs.aUv);
|
|
1890
|
+
if (this.filteredLocs.uTransform)
|
|
1891
|
+
gl.uniformMatrix4fv(this.filteredLocs.uTransform, false, transform);
|
|
1892
|
+
if (this.filteredLocs.uUvRect)
|
|
1893
|
+
gl.uniform4f(this.filteredLocs.uUvRect, 0, 0, 1, 1);
|
|
1894
|
+
if (this.filteredLocs.uTexel)
|
|
1895
|
+
gl.uniform2f(this.filteredLocs.uTexel, params.blurDir[0] / t.width, params.blurDir[1] / t.height);
|
|
1896
|
+
if (this.filteredLocs.uSigma)
|
|
1897
|
+
gl.uniform1f(this.filteredLocs.uSigma, sigma);
|
|
1898
|
+
if (this.filteredLocs.uColorOps)
|
|
1899
|
+
gl.uniform4f(this.filteredLocs.uColorOps, params.brightness, params.contrast, params.saturation, ((params.hueRotate ?? 0) * Math.PI) / 180);
|
|
1900
|
+
if (this.filteredLocs.uTint)
|
|
1901
|
+
gl.uniform4f(this.filteredLocs.uTint, tint[0], tint[1], tint[2], tint[3]);
|
|
1902
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1903
|
+
gl.bindTexture(gl.TEXTURE_2D, t.handle);
|
|
1904
|
+
if (this.filteredLocs.uTex)
|
|
1905
|
+
gl.uniform1i(this.filteredLocs.uTex, 0);
|
|
1906
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1907
|
+
}
|
|
1908
|
+
drawBackdropBlend(params) {
|
|
1909
|
+
if (!this.framingActive)
|
|
1910
|
+
return;
|
|
1911
|
+
const gl = this.gl;
|
|
1912
|
+
const surface = this.currentSurface();
|
|
1913
|
+
const transform = composeQuadTransform(params.width / 2, params.height / 2, params.width, params.height, 0, surface.width, surface.height, 0, 0, surface.flipY);
|
|
1914
|
+
const mode = params.mode === 'overlay' ? 0 : params.mode === 'hard-light' ? 1 : 2;
|
|
1915
|
+
gl.useProgram(this.backdropBlendProgram);
|
|
1916
|
+
this.setupVertexAttribs(this.backdropBlendLocs.aPos, this.backdropBlendLocs.aUv);
|
|
1917
|
+
// Output already carries the composited backdrop where src is
|
|
1918
|
+
// transparent, so REPLACE the target rather than blend over it.
|
|
1919
|
+
gl.blendFunc(gl.ONE, gl.ZERO);
|
|
1920
|
+
if (this.backdropBlendLocs.uTransform)
|
|
1921
|
+
gl.uniformMatrix4fv(this.backdropBlendLocs.uTransform, false, transform);
|
|
1922
|
+
if (this.backdropBlendLocs.uUvRect)
|
|
1923
|
+
gl.uniform4f(this.backdropBlendLocs.uUvRect, 0, 0, 1, 1);
|
|
1924
|
+
if (this.backdropBlendLocs.uMode)
|
|
1925
|
+
gl.uniform1i(this.backdropBlendLocs.uMode, mode);
|
|
1926
|
+
if (this.backdropBlendLocs.uBackdropFlipY)
|
|
1927
|
+
gl.uniform1f(this.backdropBlendLocs.uBackdropFlipY, params.backdropFlipY ? 1 : 0);
|
|
1928
|
+
const src = params.src;
|
|
1929
|
+
const bd = params.backdrop;
|
|
1930
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1931
|
+
gl.bindTexture(gl.TEXTURE_2D, src.handle);
|
|
1932
|
+
if (this.backdropBlendLocs.uSrc)
|
|
1933
|
+
gl.uniform1i(this.backdropBlendLocs.uSrc, 0);
|
|
1934
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1935
|
+
gl.bindTexture(gl.TEXTURE_2D, bd.handle);
|
|
1936
|
+
if (this.backdropBlendLocs.uBackdrop)
|
|
1937
|
+
gl.uniform1i(this.backdropBlendLocs.uBackdrop, 1);
|
|
1938
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1939
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1940
|
+
}
|
|
1941
|
+
drawStylizedQuad(params) {
|
|
1942
|
+
if (!this.framingActive)
|
|
1943
|
+
return;
|
|
1944
|
+
const gl = this.gl;
|
|
1945
|
+
this.applyBlend(params.blend);
|
|
1946
|
+
const surface = this.currentSurface();
|
|
1947
|
+
const transform = composeQuadTransform(params.cx, params.cy, params.width, params.height, 0, surface.width, surface.height, 0, 0, surface.flipY);
|
|
1948
|
+
const tint = params.tint ?? [1, 1, 1, 1];
|
|
1949
|
+
const t = params.texture;
|
|
1950
|
+
const aux = (params.aux ?? params.texture);
|
|
1951
|
+
// px-dimensioned params scale to PHYSICAL pixels; counts/angles/
|
|
1952
|
+
// intensities don't.
|
|
1953
|
+
const p0Px = params.mode !== 'dither' && params.mode !== 'glow'
|
|
1954
|
+
&& params.mode !== 'chroma_key' && params.mode !== 'luma_key'
|
|
1955
|
+
&& params.mode !== 'levels' && params.mode !== 'lut';
|
|
1956
|
+
const p1Px = params.mode === 'drop_shadow' || params.mode === 'turbulent_displace';
|
|
1957
|
+
const p0 = p0Px ? params.p0 * this.pixelRatio : params.p0;
|
|
1958
|
+
const p1 = p1Px ? (params.p1 ?? 0) * this.pixelRatio : (params.p1 ?? 0);
|
|
1959
|
+
const modeIdx = STYLIZE_MODE_INDEX[params.mode];
|
|
1960
|
+
gl.useProgram(this.stylizedProgram);
|
|
1961
|
+
this.setupVertexAttribs(this.stylizedLocs.aPos, this.stylizedLocs.aUv);
|
|
1962
|
+
if (this.stylizedLocs.uTransform)
|
|
1963
|
+
gl.uniformMatrix4fv(this.stylizedLocs.uTransform, false, transform);
|
|
1964
|
+
if (this.stylizedLocs.uUvRect)
|
|
1965
|
+
gl.uniform4f(this.stylizedLocs.uUvRect, 0, 0, 1, 1);
|
|
1966
|
+
if (this.stylizedLocs.uTexSize)
|
|
1967
|
+
gl.uniform2f(this.stylizedLocs.uTexSize, t.width, t.height);
|
|
1968
|
+
// u_params.w carries the pixel ratio so device-pixel-indexed effects
|
|
1969
|
+
// (dither's Bayer cell) can stay a stable LOGICAL-pixel size — i.e.
|
|
1970
|
+
// preview (hi-DPI) matches export (1×) instead of going sub-pixel.
|
|
1971
|
+
if (this.stylizedLocs.uParams)
|
|
1972
|
+
gl.uniform4f(this.stylizedLocs.uParams, modeIdx, p0, p1, this.pixelRatio);
|
|
1973
|
+
if (this.stylizedLocs.uTint)
|
|
1974
|
+
gl.uniform4f(this.stylizedLocs.uTint, tint[0], tint[1], tint[2], tint[3]);
|
|
1975
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1976
|
+
gl.bindTexture(gl.TEXTURE_2D, t.handle);
|
|
1977
|
+
if (this.stylizedLocs.uTex)
|
|
1978
|
+
gl.uniform1i(this.stylizedLocs.uTex, 0);
|
|
1979
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1980
|
+
gl.bindTexture(gl.TEXTURE_2D, aux.handle);
|
|
1981
|
+
if (this.stylizedLocs.uAux)
|
|
1982
|
+
gl.uniform1i(this.stylizedLocs.uAux, 1);
|
|
1983
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1984
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1985
|
+
}
|
|
1986
|
+
drawGlassQuad(params) {
|
|
1987
|
+
if (!this.framingActive)
|
|
1988
|
+
return;
|
|
1989
|
+
const gl = this.gl;
|
|
1990
|
+
this.applyBlend(params.blend);
|
|
1991
|
+
const surface = this.currentSurface();
|
|
1992
|
+
const transform = composeQuadTransform(params.cx, params.cy, params.width, params.height, 0, surface.width, surface.height, 0, 0, surface.flipY);
|
|
1993
|
+
const backdrop = params.backdrop;
|
|
1994
|
+
const sharp = params.backdropSharp;
|
|
1995
|
+
const pr = this.pixelRatio;
|
|
1996
|
+
const rad = (params.rotation * Math.PI) / 180;
|
|
1997
|
+
// CKP/1.0 glass under 3D (§4.7): a pane homography selects the
|
|
1998
|
+
// lazily-compiled projective variant. A singular homography is the
|
|
1999
|
+
// edge-on degenerate case — the pane is invisible, draw nothing.
|
|
2000
|
+
let h = null;
|
|
2001
|
+
let hinv = null;
|
|
2002
|
+
if (params.paneHomography) {
|
|
2003
|
+
h = homographyToPhysical(params.paneHomography, pr);
|
|
2004
|
+
hinv = invertHomography(h);
|
|
2005
|
+
if (!hinv)
|
|
2006
|
+
return;
|
|
2007
|
+
if (!this.glass3dProgram) {
|
|
2008
|
+
this.glass3dProgram = this.buildProgram(TEXTURED_VS, glassFsSource(true), 'glass3d');
|
|
2009
|
+
this.glass3dLocs = this.glassLocsOf(this.glass3dProgram);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
const program = h ? this.glass3dProgram : this.glassProgram;
|
|
2013
|
+
const locs = h ? this.glass3dLocs : this.glassLocs;
|
|
2014
|
+
gl.useProgram(program);
|
|
2015
|
+
this.setupVertexAttribs(locs.aPos, locs.aUv);
|
|
2016
|
+
if (locs.uTransform)
|
|
2017
|
+
gl.uniformMatrix4fv(locs.uTransform, false, transform);
|
|
2018
|
+
if (locs.uUvRect)
|
|
2019
|
+
gl.uniform4f(locs.uUvRect, 0, 0, 1, 1);
|
|
2020
|
+
// Surface dims, NOT the frosted texture's — the blur ladder
|
|
2021
|
+
// downsamples it; normalized UVs sample it fine either way.
|
|
2022
|
+
if (locs.uTexSize)
|
|
2023
|
+
gl.uniform2f(locs.uTexSize, surface.physWidth, surface.physHeight);
|
|
2024
|
+
if (locs.uPaneCenter)
|
|
2025
|
+
gl.uniform2f(locs.uPaneCenter, params.paneCx * pr, params.paneCy * pr);
|
|
2026
|
+
if (locs.uPaneHalf)
|
|
2027
|
+
gl.uniform2f(locs.uPaneHalf, params.paneHalfW * pr, params.paneHalfH * pr);
|
|
2028
|
+
if (locs.uRot)
|
|
2029
|
+
gl.uniform2f(locs.uRot, Math.cos(rad), Math.sin(rad));
|
|
2030
|
+
if (locs.uGeo) {
|
|
2031
|
+
gl.uniform4f(locs.uGeo, params.cornerRadius * pr, params.zRadius * pr, params.bevelMode, params.backdropFlipY ? 1 : 0);
|
|
2032
|
+
}
|
|
2033
|
+
if (locs.uOptics)
|
|
2034
|
+
gl.uniform4f(locs.uOptics, params.refract, params.chroma, params.edgeHighlight, params.fresnel);
|
|
2035
|
+
if (locs.uLook)
|
|
2036
|
+
gl.uniform4f(locs.uLook, params.specular, params.saturation, params.alpha, 0);
|
|
2037
|
+
if (locs.uShadow)
|
|
2038
|
+
gl.uniform4f(locs.uShadow, params.shadowAlpha, params.shadowSpread * pr, params.shadowOffY * pr, 0);
|
|
2039
|
+
if (locs.uTint)
|
|
2040
|
+
gl.uniform4f(locs.uTint, params.tint[0], params.tint[1], params.tint[2], params.tint[3]);
|
|
2041
|
+
if (h && locs.uH)
|
|
2042
|
+
gl.uniformMatrix3fv(locs.uH, false, h);
|
|
2043
|
+
if (hinv && locs.uHinv)
|
|
2044
|
+
gl.uniformMatrix3fv(locs.uHinv, false, hinv);
|
|
2045
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2046
|
+
gl.bindTexture(gl.TEXTURE_2D, backdrop.handle);
|
|
2047
|
+
if (locs.uBackdrop)
|
|
2048
|
+
gl.uniform1i(locs.uBackdrop, 0);
|
|
2049
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
2050
|
+
gl.bindTexture(gl.TEXTURE_2D, sharp.handle);
|
|
2051
|
+
if (locs.uSharp)
|
|
2052
|
+
gl.uniform1i(locs.uSharp, 1);
|
|
2053
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
2054
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2055
|
+
}
|
|
2056
|
+
glassLocsOf(program) {
|
|
2057
|
+
const gl = this.gl;
|
|
2058
|
+
return {
|
|
2059
|
+
aPos: gl.getAttribLocation(program, 'a_pos'),
|
|
2060
|
+
aUv: gl.getAttribLocation(program, 'a_uv'),
|
|
2061
|
+
uTransform: gl.getUniformLocation(program, 'u_transform'),
|
|
2062
|
+
uUvRect: gl.getUniformLocation(program, 'u_uvRect'),
|
|
2063
|
+
uBackdrop: gl.getUniformLocation(program, 'u_backdrop'),
|
|
2064
|
+
uSharp: gl.getUniformLocation(program, 'u_sharp'),
|
|
2065
|
+
uTexSize: gl.getUniformLocation(program, 'u_texSize'),
|
|
2066
|
+
uPaneCenter: gl.getUniformLocation(program, 'u_paneCenter'),
|
|
2067
|
+
uPaneHalf: gl.getUniformLocation(program, 'u_paneHalf'),
|
|
2068
|
+
uRot: gl.getUniformLocation(program, 'u_rot'),
|
|
2069
|
+
uGeo: gl.getUniformLocation(program, 'u_geo'),
|
|
2070
|
+
uOptics: gl.getUniformLocation(program, 'u_optics'),
|
|
2071
|
+
uLook: gl.getUniformLocation(program, 'u_look'),
|
|
2072
|
+
uShadow: gl.getUniformLocation(program, 'u_shadow'),
|
|
2073
|
+
uTint: gl.getUniformLocation(program, 'u_tint'),
|
|
2074
|
+
uH: gl.getUniformLocation(program, 'u_h'),
|
|
2075
|
+
uHinv: gl.getUniformLocation(program, 'u_hinv'),
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
copySurfaceTo(target) {
|
|
2079
|
+
const gl = this.gl;
|
|
2080
|
+
const fbo = this.renderTargetFbos.get(target);
|
|
2081
|
+
if (!fbo) {
|
|
2082
|
+
getLogger().warn('copySurfaceTo with unknown / destroyed target — ignored');
|
|
2083
|
+
return { flippedY: false };
|
|
2084
|
+
}
|
|
2085
|
+
const surface = this.currentSurface();
|
|
2086
|
+
const tex = target.texture;
|
|
2087
|
+
// Same-rect blit (required when the canvas read buffer is
|
|
2088
|
+
// multisampled — the blit doubles as the MSAA resolve). The canvas
|
|
2089
|
+
// default framebuffer stores rows bottom-up; render-target FBOs are
|
|
2090
|
+
// drawn with a flipped projection so their rows are top-down. The
|
|
2091
|
+
// sampler compensates via the returned flag instead of flipping
|
|
2092
|
+
// here, which a multisample resolve blit would not allow.
|
|
2093
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, surface.fbo);
|
|
2094
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo);
|
|
2095
|
+
gl.blitFramebuffer(0, 0, surface.physWidth, surface.physHeight, 0, 0, tex.width, tex.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
2096
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, surface.fbo);
|
|
2097
|
+
return { flippedY: surface.fbo === null };
|
|
2098
|
+
}
|
|
2099
|
+
// ─── Cleanup ──────────────────────────────────────────────────────────────
|
|
2100
|
+
async finish() {
|
|
2101
|
+
// gl.finish blocks until the pipeline drains.
|
|
2102
|
+
this.gl.finish();
|
|
2103
|
+
}
|
|
2104
|
+
dispose() {
|
|
2105
|
+
if (this.disposed)
|
|
2106
|
+
return;
|
|
2107
|
+
this.disposed = true;
|
|
2108
|
+
const gl = this.gl;
|
|
2109
|
+
if (!gl)
|
|
2110
|
+
return;
|
|
2111
|
+
for (const t of this.liveTextures)
|
|
2112
|
+
gl.deleteTexture(t.handle);
|
|
2113
|
+
this.liveTextures.clear();
|
|
2114
|
+
if (this.vbo)
|
|
2115
|
+
gl.deleteBuffer(this.vbo);
|
|
2116
|
+
if (this.vao)
|
|
2117
|
+
gl.deleteVertexArray(this.vao);
|
|
2118
|
+
if (this.shapeProgram)
|
|
2119
|
+
gl.deleteProgram(this.shapeProgram);
|
|
2120
|
+
if (this.gradientProgram)
|
|
2121
|
+
gl.deleteProgram(this.gradientProgram);
|
|
2122
|
+
if (this.texturedProgram)
|
|
2123
|
+
gl.deleteProgram(this.texturedProgram);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
2127
|
+
function sourceDimensions(source) {
|
|
2128
|
+
if ('codedWidth' in source && 'codedHeight' in source) {
|
|
2129
|
+
return { width: source.codedWidth, height: source.codedHeight };
|
|
2130
|
+
}
|
|
2131
|
+
if ('videoWidth' in source && 'videoHeight' in source) {
|
|
2132
|
+
return { width: source.videoWidth, height: source.videoHeight };
|
|
2133
|
+
}
|
|
2134
|
+
if ('naturalWidth' in source && 'naturalHeight' in source) {
|
|
2135
|
+
return { width: source.naturalWidth, height: source.naturalHeight };
|
|
2136
|
+
}
|
|
2137
|
+
return { width: source.width, height: source.height };
|
|
2138
|
+
}
|
|
2139
|
+
function clamp(n, lo, hi) {
|
|
2140
|
+
return Math.max(lo, Math.min(hi, n));
|
|
2141
|
+
}
|
|
2142
|
+
//# sourceMappingURL=webgl-backend.js.map
|