@weasel-js/renderer 0.0.0-stage → 1.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/chunk-2VOBNWBN.js +11 -0
- package/dist/chunk-2VOBNWBN.js.map +1 -0
- package/dist/chunk-ORBXHIRB.js +178 -0
- package/dist/chunk-ORBXHIRB.js.map +1 -0
- package/dist/chunk-VHQJ56XQ.js +1437 -0
- package/dist/chunk-VHQJ56XQ.js.map +1 -0
- package/dist/imageCache-oJVgxhkr.d.ts +372 -0
- package/dist/index.d.ts +1070 -0
- package/dist/index.js +4086 -0
- package/dist/index.js.map +1 -0
- package/dist/mesh.d.ts +274 -0
- package/dist/mesh.js +603 -0
- package/dist/mesh.js.map +1 -0
- package/dist/paintKinds-B2vEy9A4.d.ts +330 -0
- package/dist/patterns-builtin.d.ts +118 -0
- package/dist/patterns-builtin.js +28 -0
- package/dist/patterns-builtin.js.map +1 -0
- package/dist/test-seams.d.ts +120 -0
- package/dist/test-seams.js +4 -0
- package/dist/test-seams.js.map +1 -0
- package/dist/testing/glRecorder.d.ts +36 -0
- package/dist/testing/glRecorder.js +155 -0
- package/dist/testing/glRecorder.js.map +1 -0
- package/dist/testing/vertexUploads.d.ts +30 -0
- package/dist/testing/vertexUploads.js +48 -0
- package/dist/testing/vertexUploads.js.map +1 -0
- package/package.json +71 -4
package/dist/mesh.js
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import { resolveColor, lerpColorArray, srgbFloatToOklab, oklabToOklch, oklchToOklab, oklabToSrgbU8, rgbaToHex } from '@weasel-js/paint';
|
|
2
|
+
import { mat3 } from '@weasel-js/geom';
|
|
3
|
+
|
|
4
|
+
// src/meshPaint/meshPaint.ts
|
|
5
|
+
|
|
6
|
+
// src/meshPaint/surface.ts
|
|
7
|
+
function patchCorner(patch, i) {
|
|
8
|
+
return patch.points[(i & 3) * 3];
|
|
9
|
+
}
|
|
10
|
+
function isTensorPatch(patch) {
|
|
11
|
+
return patch.points.length >= 16;
|
|
12
|
+
}
|
|
13
|
+
function isValidPatch(patch) {
|
|
14
|
+
return patch.points.length === 12 || patch.points.length === 16;
|
|
15
|
+
}
|
|
16
|
+
function bezier(p0, p1, p2, p3, t) {
|
|
17
|
+
const s = 1 - t;
|
|
18
|
+
const a = s * s * s;
|
|
19
|
+
const b = 3 * s * s * t;
|
|
20
|
+
const c = 3 * s * t * t;
|
|
21
|
+
const d = t * t * t;
|
|
22
|
+
return {
|
|
23
|
+
x: a * p0.x + b * p1.x + c * p2.x + d * p3.x,
|
|
24
|
+
y: a * p0.y + b * p1.y + c * p2.y + d * p3.y
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function edge(patch, i) {
|
|
28
|
+
const p = patch.points;
|
|
29
|
+
const a = (i & 3) * 3;
|
|
30
|
+
return [p[a], p[a + 1], p[a + 2], p[(i + 1 & 3) * 3]];
|
|
31
|
+
}
|
|
32
|
+
function evalPatch(patch, u, v) {
|
|
33
|
+
return isTensorPatch(patch) ? evalTensor(patch, u, v) : evalCoons(patch, u, v);
|
|
34
|
+
}
|
|
35
|
+
function evalCoons(patch, u, v) {
|
|
36
|
+
const [b0, b1, b2, b3] = edge(patch, 0);
|
|
37
|
+
const [r0, r1, r2, r3] = edge(patch, 1);
|
|
38
|
+
const [t0, t1, t2, t3] = edge(patch, 2);
|
|
39
|
+
const [l0, l1, l2, l3] = edge(patch, 3);
|
|
40
|
+
const bottom = bezier(b0, b1, b2, b3, u);
|
|
41
|
+
const top = bezier(t0, t1, t2, t3, 1 - u);
|
|
42
|
+
const right = bezier(r0, r1, r2, r3, v);
|
|
43
|
+
const left = bezier(l0, l1, l2, l3, 1 - v);
|
|
44
|
+
const c0 = patchCorner(patch, 0);
|
|
45
|
+
const c1 = patchCorner(patch, 1);
|
|
46
|
+
const c2 = patchCorner(patch, 2);
|
|
47
|
+
const c3 = patchCorner(patch, 3);
|
|
48
|
+
const ruledU = (a, b) => (1 - v) * a + v * b;
|
|
49
|
+
const ruledV = (a, b) => (1 - u) * a + u * b;
|
|
50
|
+
const corner = (k0, k1, k2, k3) => (1 - u) * (1 - v) * k0 + u * (1 - v) * k1 + u * v * k2 + (1 - u) * v * k3;
|
|
51
|
+
return {
|
|
52
|
+
x: ruledU(bottom.x, top.x) + ruledV(left.x, right.x) - corner(c0.x, c1.x, c2.x, c3.x),
|
|
53
|
+
y: ruledU(bottom.y, top.y) + ruledV(left.y, right.y) - corner(c0.y, c1.y, c2.y, c3.y)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function tensorNet(patch) {
|
|
57
|
+
const p = patch.points;
|
|
58
|
+
return [
|
|
59
|
+
[p[0], p[11], p[10], p[9]],
|
|
60
|
+
[p[1], p[12], p[13], p[8]],
|
|
61
|
+
[p[2], p[15], p[14], p[7]],
|
|
62
|
+
[p[3], p[4], p[5], p[6]]
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
var BERNSTEIN = (t) => {
|
|
66
|
+
const s = 1 - t;
|
|
67
|
+
return [s * s * s, 3 * s * s * t, 3 * s * t * t, t * t * t];
|
|
68
|
+
};
|
|
69
|
+
function evalTensor(patch, u, v) {
|
|
70
|
+
const net = tensorNet(patch);
|
|
71
|
+
const bu = BERNSTEIN(u);
|
|
72
|
+
const bv = BERNSTEIN(v);
|
|
73
|
+
let x = 0;
|
|
74
|
+
let y = 0;
|
|
75
|
+
for (let i = 0; i < 4; i++) {
|
|
76
|
+
for (let j = 0; j < 4; j++) {
|
|
77
|
+
const w = bu[i] * bv[j];
|
|
78
|
+
x += w * net[i][j].x;
|
|
79
|
+
y += w * net[i][j].y;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { x, y };
|
|
83
|
+
}
|
|
84
|
+
function cornerWeights(u, v) {
|
|
85
|
+
return [(1 - u) * (1 - v), u * (1 - v), u * v, (1 - u) * v];
|
|
86
|
+
}
|
|
87
|
+
function patchBounds(patch, steps = 8) {
|
|
88
|
+
let minX = Infinity;
|
|
89
|
+
let minY = Infinity;
|
|
90
|
+
let maxX = -Infinity;
|
|
91
|
+
let maxY = -Infinity;
|
|
92
|
+
for (let i = 0; i <= steps; i++) {
|
|
93
|
+
for (let j = 0; j <= steps; j++) {
|
|
94
|
+
const p = evalPatch(patch, i / steps, j / steps);
|
|
95
|
+
if (p.x < minX) minX = p.x;
|
|
96
|
+
if (p.y < minY) minY = p.y;
|
|
97
|
+
if (p.x > maxX) maxX = p.x;
|
|
98
|
+
if (p.y > maxY) maxY = p.y;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/meshPaint/bake.ts
|
|
105
|
+
var MESH_BAKE_SIZE = 256;
|
|
106
|
+
var TEXELS_PER_CELL = 10;
|
|
107
|
+
function gridFor(size) {
|
|
108
|
+
return Math.max(24, Math.ceil(size / TEXELS_PER_CELL));
|
|
109
|
+
}
|
|
110
|
+
function meshBounds(patches) {
|
|
111
|
+
let minX = Infinity;
|
|
112
|
+
let minY = Infinity;
|
|
113
|
+
let maxX = -Infinity;
|
|
114
|
+
let maxY = -Infinity;
|
|
115
|
+
for (const patch of patches) {
|
|
116
|
+
if (!isValidPatch(patch)) continue;
|
|
117
|
+
const b = patchBounds(patch);
|
|
118
|
+
minX = Math.min(minX, b.x);
|
|
119
|
+
minY = Math.min(minY, b.y);
|
|
120
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
121
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
122
|
+
}
|
|
123
|
+
if (!Number.isFinite(minX) || maxX <= minX || maxY <= minY) return null;
|
|
124
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
125
|
+
}
|
|
126
|
+
function colorAt(corners, u, v, space) {
|
|
127
|
+
const bottom = lerpColorArray(corners[0], corners[1], u, space);
|
|
128
|
+
const top = lerpColorArray(corners[3], corners[2], u, space);
|
|
129
|
+
return lerpColorArray(bottom, top, v, space);
|
|
130
|
+
}
|
|
131
|
+
function bakeMesh(patches, space = "rgb", size = MESH_BAKE_SIZE) {
|
|
132
|
+
const box = meshBounds(patches);
|
|
133
|
+
if (!box) return null;
|
|
134
|
+
const pixels = new Uint8ClampedArray(size * size * 4);
|
|
135
|
+
const grid = gridFor(size);
|
|
136
|
+
const toTexel = (p) => ({
|
|
137
|
+
x: (p.x - box.x) / box.width * (size - 1) + 0.5,
|
|
138
|
+
y: (p.y - box.y) / box.height * (size - 1) + 0.5
|
|
139
|
+
});
|
|
140
|
+
for (const patch of patches) {
|
|
141
|
+
if (!isValidPatch(patch)) continue;
|
|
142
|
+
const corners = patch.colors.map((c) => resolveColor(c));
|
|
143
|
+
for (let i = 0; i < grid; i++) {
|
|
144
|
+
for (let j = 0; j < grid; j++) {
|
|
145
|
+
const u0 = i / grid;
|
|
146
|
+
const u1 = (i + 1) / grid;
|
|
147
|
+
const v0 = j / grid;
|
|
148
|
+
const v1 = (j + 1) / grid;
|
|
149
|
+
const quad = [
|
|
150
|
+
{ at: toTexel(evalPatch(patch, u0, v0)), color: colorAt(corners, u0, v0, space) },
|
|
151
|
+
{ at: toTexel(evalPatch(patch, u1, v0)), color: colorAt(corners, u1, v0, space) },
|
|
152
|
+
{ at: toTexel(evalPatch(patch, u1, v1)), color: colorAt(corners, u1, v1, space) },
|
|
153
|
+
{ at: toTexel(evalPatch(patch, u0, v1)), color: colorAt(corners, u0, v1, space) }
|
|
154
|
+
];
|
|
155
|
+
fillTriangle(pixels, size, quad[0], quad[1], quad[2]);
|
|
156
|
+
fillTriangle(pixels, size, quad[0], quad[2], quad[3]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { pixels, size, box };
|
|
161
|
+
}
|
|
162
|
+
function fillTriangle(pixels, size, a, b, c) {
|
|
163
|
+
const minX = Math.max(0, Math.floor(Math.min(a.at.x, b.at.x, c.at.x)));
|
|
164
|
+
const maxX = Math.min(size - 1, Math.ceil(Math.max(a.at.x, b.at.x, c.at.x)));
|
|
165
|
+
const minY = Math.max(0, Math.floor(Math.min(a.at.y, b.at.y, c.at.y)));
|
|
166
|
+
const maxY = Math.min(size - 1, Math.ceil(Math.max(a.at.y, b.at.y, c.at.y)));
|
|
167
|
+
if (maxX < minX || maxY < minY) return;
|
|
168
|
+
const area = (b.at.x - a.at.x) * (c.at.y - a.at.y) - (c.at.x - a.at.x) * (b.at.y - a.at.y);
|
|
169
|
+
if (Math.abs(area) < 1e-9) return;
|
|
170
|
+
for (let y = minY; y <= maxY; y++) {
|
|
171
|
+
for (let x = minX; x <= maxX; x++) {
|
|
172
|
+
const px = x + 0.5;
|
|
173
|
+
const py = y + 0.5;
|
|
174
|
+
const w0 = ((b.at.x - px) * (c.at.y - py) - (c.at.x - px) * (b.at.y - py)) / area;
|
|
175
|
+
const w1 = ((c.at.x - px) * (a.at.y - py) - (a.at.x - px) * (c.at.y - py)) / area;
|
|
176
|
+
const w2 = 1 - w0 - w1;
|
|
177
|
+
if (w0 < -1e-6 || w1 < -1e-6 || w2 < -1e-6) continue;
|
|
178
|
+
const k = (y * size + x) * 4;
|
|
179
|
+
for (let ch = 0; ch < 4; ch++) {
|
|
180
|
+
pixels[k + ch] = (w0 * a.color[ch] + w1 * b.color[ch] + w2 * c.color[ch]) * 255;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/meshPaint/bakeCache.ts
|
|
187
|
+
var MESH_BAKE_MIN = 32;
|
|
188
|
+
var MESH_BAKE_MAX = 2048;
|
|
189
|
+
var MESH_BAKE_CACHE_BYTES = 64 * 1024 * 1024;
|
|
190
|
+
var MESH_BAKE_CACHE_ENTRIES = 256;
|
|
191
|
+
function meshBakeSize(devicePx, maxTextureSize) {
|
|
192
|
+
const ceiling = Math.min(MESH_BAKE_MAX, floorPow2(maxTextureSize));
|
|
193
|
+
if (Number.isNaN(devicePx)) return Math.min(MESH_BAKE_SIZE, ceiling);
|
|
194
|
+
if (devicePx <= MESH_BAKE_MIN) return Math.min(MESH_BAKE_MIN, ceiling);
|
|
195
|
+
if (devicePx >= ceiling) return ceiling;
|
|
196
|
+
return Math.min(ceiling, 2 ** Math.ceil(Math.log2(devicePx)));
|
|
197
|
+
}
|
|
198
|
+
function floorPow2(n) {
|
|
199
|
+
if (!(n >= 1)) return MESH_BAKE_MIN;
|
|
200
|
+
if (!Number.isFinite(n)) return MESH_BAKE_MAX;
|
|
201
|
+
return 2 ** Math.floor(Math.log2(n));
|
|
202
|
+
}
|
|
203
|
+
var MeshBakeCache = class {
|
|
204
|
+
constructor(free, budgetBytes = MESH_BAKE_CACHE_BYTES, maxEntries = MESH_BAKE_CACHE_ENTRIES) {
|
|
205
|
+
this.free = free;
|
|
206
|
+
this.budgetBytes = budgetBytes;
|
|
207
|
+
this.maxEntries = maxEntries;
|
|
208
|
+
}
|
|
209
|
+
free;
|
|
210
|
+
budgetBytes;
|
|
211
|
+
maxEntries;
|
|
212
|
+
byPaint = /* @__PURE__ */ new WeakMap();
|
|
213
|
+
/** Insertion order is recency: a hit is deleted and re-added. */
|
|
214
|
+
order = /* @__PURE__ */ new Set();
|
|
215
|
+
held = 0;
|
|
216
|
+
/** Bytes the held bakes occupy, as RGBA8. */
|
|
217
|
+
get bytes() {
|
|
218
|
+
return this.held;
|
|
219
|
+
}
|
|
220
|
+
/** Bakes held. */
|
|
221
|
+
get size() {
|
|
222
|
+
return this.order.size;
|
|
223
|
+
}
|
|
224
|
+
get(paint, size) {
|
|
225
|
+
const entry = this.byPaint.get(paint)?.get(size);
|
|
226
|
+
if (!entry) return void 0;
|
|
227
|
+
this.order.delete(entry);
|
|
228
|
+
this.order.add(entry);
|
|
229
|
+
return entry.value;
|
|
230
|
+
}
|
|
231
|
+
set(paint, size, value) {
|
|
232
|
+
let bySize = this.byPaint.get(paint);
|
|
233
|
+
if (!bySize) {
|
|
234
|
+
bySize = /* @__PURE__ */ new Map();
|
|
235
|
+
this.byPaint.set(paint, bySize);
|
|
236
|
+
}
|
|
237
|
+
const old = bySize.get(size);
|
|
238
|
+
if (old) this.drop(old);
|
|
239
|
+
const entry = { value, size, bySize };
|
|
240
|
+
bySize.set(size, entry);
|
|
241
|
+
this.order.add(entry);
|
|
242
|
+
this.held += size * size * 4;
|
|
243
|
+
this.evict(entry);
|
|
244
|
+
}
|
|
245
|
+
/** Free every bake. The cache stays usable. */
|
|
246
|
+
clear() {
|
|
247
|
+
for (const entry of [...this.order]) this.drop(entry);
|
|
248
|
+
}
|
|
249
|
+
/** Oldest first until both bounds hold, never evicting `keep` — the bake
|
|
250
|
+
* the caller is about to draw. */
|
|
251
|
+
evict(keep) {
|
|
252
|
+
for (const entry of this.order) {
|
|
253
|
+
if (this.held <= this.budgetBytes && this.order.size <= this.maxEntries) return;
|
|
254
|
+
if (entry !== keep) this.drop(entry);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
drop(entry) {
|
|
258
|
+
if (!this.order.delete(entry)) return;
|
|
259
|
+
entry.bySize.delete(entry.size);
|
|
260
|
+
this.held -= entry.size * entry.size * 4;
|
|
261
|
+
this.free(entry.value);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/meshPaint/meshShader.ts
|
|
266
|
+
var MESH_PROGRAM_ID = "kit:mesh-gradient";
|
|
267
|
+
var MESH_VERT_SRC = (
|
|
268
|
+
/* glsl */
|
|
269
|
+
`#version 300 es
|
|
270
|
+
in vec2 a_position;
|
|
271
|
+
uniform mat3 u_proj;
|
|
272
|
+
uniform mat3 u_model;
|
|
273
|
+
uniform mat3 u_worldInv;
|
|
274
|
+
out vec2 v_world;
|
|
275
|
+
void main() {
|
|
276
|
+
vec3 screen = u_model * vec3(a_position, 1.0);
|
|
277
|
+
vec3 clip = u_proj * vec3(screen.xy, 1.0);
|
|
278
|
+
gl_Position = vec4(clip.xy, 0.0, 1.0);
|
|
279
|
+
vec3 world = u_worldInv * vec3(screen.xy, 1.0);
|
|
280
|
+
v_world = world.xy;
|
|
281
|
+
}
|
|
282
|
+
`
|
|
283
|
+
);
|
|
284
|
+
var MESH_FRAG_SRC = (
|
|
285
|
+
/* glsl */
|
|
286
|
+
`#version 300 es
|
|
287
|
+
precision highp float;
|
|
288
|
+
in vec2 v_world;
|
|
289
|
+
uniform sampler2D u_sampler;
|
|
290
|
+
uniform vec2 u_meshOrigin;
|
|
291
|
+
uniform vec2 u_meshSize;
|
|
292
|
+
uniform float u_opacity;
|
|
293
|
+
uniform float u_alpha;
|
|
294
|
+
out vec4 outColor;
|
|
295
|
+
void main() {
|
|
296
|
+
vec2 t =(v_world - u_meshOrigin) / max(u_meshSize, vec2(0.0001));
|
|
297
|
+
if (t.x < 0.0 || t.x > 1.0 || t.y < 0.0 || t.y > 1.0) {
|
|
298
|
+
outColor = vec4(0.0);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
vec2 texels = vec2(textureSize(u_sampler, 0));
|
|
302
|
+
vec2 uv = (t * (texels - 1.0) + 0.5) / texels;
|
|
303
|
+
vec4 texel = texture(u_sampler, uv);
|
|
304
|
+
float a = texel.a * u_opacity * u_alpha;
|
|
305
|
+
outColor = vec4(texel.rgb * a, a);
|
|
306
|
+
}
|
|
307
|
+
`
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// src/meshPaint/meshPaint.ts
|
|
311
|
+
var MESH_GRADIENT_KIND = "mesh-gradient";
|
|
312
|
+
function isMeshGradientFill(fill) {
|
|
313
|
+
return fill != null && fill.fill === MESH_GRADIENT_KIND;
|
|
314
|
+
}
|
|
315
|
+
function asMesh(fill) {
|
|
316
|
+
return fill;
|
|
317
|
+
}
|
|
318
|
+
function asFill(mesh) {
|
|
319
|
+
return mesh;
|
|
320
|
+
}
|
|
321
|
+
function shifted(color, dL, cScale, dHueDeg) {
|
|
322
|
+
const [r, g, b, a] = resolveColor(color);
|
|
323
|
+
const [L, A, B] = srgbFloatToOklab(r, g, b);
|
|
324
|
+
const [, C, h] = oklabToOklch(L, A, B);
|
|
325
|
+
const [nL, nA, nB] = oklchToOklab(
|
|
326
|
+
Math.min(1, Math.max(0, L + dL)),
|
|
327
|
+
Math.max(0, C * cScale),
|
|
328
|
+
h + dHueDeg * Math.PI / 180
|
|
329
|
+
);
|
|
330
|
+
const [nr, ng, nb] = oklabToSrgbU8(nL, nA, nB);
|
|
331
|
+
return rgbaToHex([nr / 255, ng / 255, nb / 255, a]);
|
|
332
|
+
}
|
|
333
|
+
function seedMeshPatch(color) {
|
|
334
|
+
const corners = [
|
|
335
|
+
{ x: 0, y: 0 },
|
|
336
|
+
{ x: 1, y: 0 },
|
|
337
|
+
{ x: 1, y: 1 },
|
|
338
|
+
{ x: 0, y: 1 }
|
|
339
|
+
];
|
|
340
|
+
const points = [];
|
|
341
|
+
for (let i = 0; i < 4; i++) {
|
|
342
|
+
const a = corners[i];
|
|
343
|
+
const b = corners[(i + 1) % 4];
|
|
344
|
+
const nx = (b.y - a.y) * 0.125;
|
|
345
|
+
const ny = -(b.x - a.x) * 0.125;
|
|
346
|
+
points.push(
|
|
347
|
+
a,
|
|
348
|
+
{ x: a.x + (b.x - a.x) / 3 + nx, y: a.y + (b.y - a.y) / 3 + ny },
|
|
349
|
+
{ x: a.x + 2 * (b.x - a.x) / 3 + nx, y: a.y + 2 * (b.y - a.y) / 3 + ny }
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
fill: MESH_GRADIENT_KIND,
|
|
354
|
+
patches: [{
|
|
355
|
+
points,
|
|
356
|
+
colors: [
|
|
357
|
+
color,
|
|
358
|
+
shifted(color, 0.14, 0.85, 0),
|
|
359
|
+
shifted(color, -0.06, 1.1, 38),
|
|
360
|
+
shifted(color, -0.16, 1, -22)
|
|
361
|
+
]
|
|
362
|
+
}],
|
|
363
|
+
units: "bounds"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function bandPatch(x0, x1, left, right) {
|
|
367
|
+
const corners = [{ x: x0, y: 0 }, { x: x1, y: 0 }, { x: x1, y: 1 }, { x: x0, y: 1 }];
|
|
368
|
+
const points = [];
|
|
369
|
+
for (let i = 0; i < 4; i++) {
|
|
370
|
+
const a = corners[i];
|
|
371
|
+
const b = corners[(i + 1) % 4];
|
|
372
|
+
points.push(a, { x: a.x + (b.x - a.x) / 3, y: a.y + (b.y - a.y) / 3 }, { x: a.x + 2 * (b.x - a.x) / 3, y: a.y + 2 * (b.y - a.y) / 3 });
|
|
373
|
+
}
|
|
374
|
+
return { points, colors: [left, right, right, left] };
|
|
375
|
+
}
|
|
376
|
+
function meshFromStops(stops) {
|
|
377
|
+
const sorted = [...stops].sort((a, b) => a.offset - b.offset);
|
|
378
|
+
if (sorted.length === 1) sorted.push({ offset: 1, color: sorted[0].color });
|
|
379
|
+
if (sorted.length === 0) return seedMeshPatch("#000000ff");
|
|
380
|
+
const patches = [];
|
|
381
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
382
|
+
const a = sorted[i];
|
|
383
|
+
const b = sorted[i + 1];
|
|
384
|
+
if (b.offset > a.offset) patches.push(bandPatch(a.offset, b.offset, a.color, b.color));
|
|
385
|
+
}
|
|
386
|
+
return { fill: MESH_GRADIENT_KIND, patches, units: "bounds" };
|
|
387
|
+
}
|
|
388
|
+
function meshStops(mesh) {
|
|
389
|
+
const valid = mesh.patches.filter(isValidPatch);
|
|
390
|
+
if (valid.length === 0) return [];
|
|
391
|
+
const xs = valid.flatMap((p) => [p.points[0].x, p.points[3].x]);
|
|
392
|
+
const lo = Math.min(...xs);
|
|
393
|
+
const span = Math.max(...xs) - lo;
|
|
394
|
+
const offsetOf = (x) => span > 0 ? (x - lo) / span : 0;
|
|
395
|
+
const stops = [];
|
|
396
|
+
for (const patch of valid) {
|
|
397
|
+
for (const [point, color] of [[patch.points[0], patch.colors[0]], [patch.points[3], patch.colors[1]]]) {
|
|
398
|
+
const offset = offsetOf(point.x);
|
|
399
|
+
if (!stops.some((s) => s.offset === offset && s.color === color)) stops.push({ offset, color });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return stops.sort((a, b) => a.offset - b.offset);
|
|
403
|
+
}
|
|
404
|
+
function mapPatches(mesh, move) {
|
|
405
|
+
return mesh.patches.map((patch) => ({ ...patch, points: patch.points.map(move) }));
|
|
406
|
+
}
|
|
407
|
+
var BAKE_SOURCE = /* @__PURE__ */ new WeakMap();
|
|
408
|
+
var CACHES = /* @__PURE__ */ new WeakMap();
|
|
409
|
+
function cacheFor(ctx) {
|
|
410
|
+
let cache = CACHES.get(ctx.resources);
|
|
411
|
+
if (!cache) {
|
|
412
|
+
const gl = ctx.gl;
|
|
413
|
+
const made = new MeshBakeCache((texture) => gl.deleteTexture(texture));
|
|
414
|
+
ctx.resources.onRelease(() => {
|
|
415
|
+
made.clear();
|
|
416
|
+
CACHES.delete(ctx.resources);
|
|
417
|
+
});
|
|
418
|
+
CACHES.set(ctx.resources, made);
|
|
419
|
+
cache = made;
|
|
420
|
+
}
|
|
421
|
+
return cache;
|
|
422
|
+
}
|
|
423
|
+
function devicePixelsOf(ctx, mesh, box) {
|
|
424
|
+
const m = ctx.spaceToDevice(mesh.units);
|
|
425
|
+
if (!m) return Number.NaN;
|
|
426
|
+
return Math.max(box.width * Math.hypot(m[0], m[1]), box.height * Math.hypot(m[3], m[4]));
|
|
427
|
+
}
|
|
428
|
+
function bakeFor(ctx, mesh) {
|
|
429
|
+
const box = meshBounds(mesh.patches);
|
|
430
|
+
if (!box) return null;
|
|
431
|
+
const size = meshBakeSize(devicePixelsOf(ctx, mesh, box), ctx.maxTextureSize);
|
|
432
|
+
const cache = cacheFor(ctx);
|
|
433
|
+
const key = BAKE_SOURCE.get(mesh) ?? mesh;
|
|
434
|
+
const hit = cache.get(key, size);
|
|
435
|
+
if (hit) return { texture: hit, box };
|
|
436
|
+
const baked = bakeMesh(mesh.patches, mesh.interpolate ?? "rgb", size);
|
|
437
|
+
if (!baked) return null;
|
|
438
|
+
const gl = ctx.gl;
|
|
439
|
+
const texture = gl.createTexture();
|
|
440
|
+
if (!texture) return null;
|
|
441
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
442
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
443
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
444
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
445
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
446
|
+
gl.texImage2D(
|
|
447
|
+
gl.TEXTURE_2D,
|
|
448
|
+
0,
|
|
449
|
+
gl.RGBA,
|
|
450
|
+
baked.size,
|
|
451
|
+
baked.size,
|
|
452
|
+
0,
|
|
453
|
+
gl.RGBA,
|
|
454
|
+
gl.UNSIGNED_BYTE,
|
|
455
|
+
baked.pixels
|
|
456
|
+
);
|
|
457
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
458
|
+
cache.set(key, size, texture);
|
|
459
|
+
return { texture, box };
|
|
460
|
+
}
|
|
461
|
+
function bindMesh(ctx, fill) {
|
|
462
|
+
const mesh = asMesh(fill);
|
|
463
|
+
if (!mesh.patches.some(isValidPatch)) return null;
|
|
464
|
+
const inverse = ctx.spaceInverse(mesh.units);
|
|
465
|
+
if (!inverse) return null;
|
|
466
|
+
const program = ctx.program(MESH_PROGRAM_ID);
|
|
467
|
+
if (!program) return null;
|
|
468
|
+
const entry = bakeFor(ctx, mesh);
|
|
469
|
+
if (!entry) return null;
|
|
470
|
+
const gl = ctx.gl;
|
|
471
|
+
gl.useProgram(program.handle);
|
|
472
|
+
ctx.setProjAndModel(program);
|
|
473
|
+
gl.uniformMatrix3fv(program.uniform("u_worldInv"), false, mat3.toUniform(inverse));
|
|
474
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
475
|
+
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
|
476
|
+
gl.uniform1i(program.uniform("u_sampler"), 0);
|
|
477
|
+
const { box } = entry;
|
|
478
|
+
gl.uniform2f(program.uniform("u_meshOrigin"), box.x, box.y);
|
|
479
|
+
gl.uniform2f(program.uniform("u_meshSize"), box.width, box.height);
|
|
480
|
+
gl.uniform1f(program.uniform("u_opacity"), mesh.opacity ?? 1);
|
|
481
|
+
gl.uniform1f(program.uniform("u_alpha"), ctx.alpha);
|
|
482
|
+
return program;
|
|
483
|
+
}
|
|
484
|
+
var meshDrawKind = {
|
|
485
|
+
id: MESH_GRADIENT_KIND,
|
|
486
|
+
programs: { [MESH_PROGRAM_ID]: { vert: MESH_VERT_SRC, frag: MESH_FRAG_SRC } },
|
|
487
|
+
bind: bindMesh,
|
|
488
|
+
inPoseFrame: (fill, box) => {
|
|
489
|
+
const mesh = asMesh(fill);
|
|
490
|
+
if (mesh.units !== "bounds") return fill;
|
|
491
|
+
const posed = asFill({
|
|
492
|
+
...mesh,
|
|
493
|
+
patches: mapPatches(mesh, (p) => ({
|
|
494
|
+
x: box.x + p.x * box.width,
|
|
495
|
+
y: box.y + p.y * box.height
|
|
496
|
+
})),
|
|
497
|
+
units: "local"
|
|
498
|
+
});
|
|
499
|
+
BAKE_SOURCE.set(posed, fill);
|
|
500
|
+
return posed;
|
|
501
|
+
},
|
|
502
|
+
toBoundsFrame: (fill, box) => {
|
|
503
|
+
const mesh = asMesh(fill);
|
|
504
|
+
if (mesh.units === "bounds" || box.width === 0 || box.height === 0) return fill;
|
|
505
|
+
return asFill({
|
|
506
|
+
...mesh,
|
|
507
|
+
patches: mapPatches(mesh, (p) => ({
|
|
508
|
+
x: (p.x - box.x) / box.width,
|
|
509
|
+
y: (p.y - box.y) / box.height
|
|
510
|
+
})),
|
|
511
|
+
units: "bounds"
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
function meshGradientXml(id, mesh) {
|
|
516
|
+
const P = "wzl";
|
|
517
|
+
const units = mesh.units === "bounds" ? "objectBoundingBox" : "userSpaceOnUse";
|
|
518
|
+
const space = mesh.interpolate && mesh.interpolate !== "rgb" ? ` interpolate="${mesh.interpolate}"` : "";
|
|
519
|
+
const patches = mesh.patches.map((patch) => {
|
|
520
|
+
const points = patch.points.map((p) => `${trim(p.x)},${trim(p.y)}`).join(" ");
|
|
521
|
+
const colors = patch.colors.join(" ");
|
|
522
|
+
return `<${P}:patch points="${points}" colors="${colors}"/>`;
|
|
523
|
+
}).join("");
|
|
524
|
+
return `<${P}:meshGradient id="${id}" gradientUnits="${units}"${space}>${patches}</${P}:meshGradient>`;
|
|
525
|
+
}
|
|
526
|
+
function trim(n) {
|
|
527
|
+
return Number.isInteger(n) ? String(n) : String(Number(n.toFixed(4)));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/meshPaint/handles.ts
|
|
531
|
+
var INTERIOR_OF_CORNER = [12, 15, 14, 13];
|
|
532
|
+
function kindOf(index) {
|
|
533
|
+
if (index >= 12) return "interior";
|
|
534
|
+
return index % 3 === 0 ? "corner" : "control";
|
|
535
|
+
}
|
|
536
|
+
function near(a, b) {
|
|
537
|
+
return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
|
|
538
|
+
}
|
|
539
|
+
function same(a, b) {
|
|
540
|
+
return near(a.x, b.x) && near(a.y, b.y);
|
|
541
|
+
}
|
|
542
|
+
function meshHandles(mesh) {
|
|
543
|
+
const handles = [];
|
|
544
|
+
mesh.patches.forEach((patch, p) => {
|
|
545
|
+
if (!isValidPatch(patch)) return;
|
|
546
|
+
patch.points.forEach((at, index) => {
|
|
547
|
+
const kind = kindOf(index);
|
|
548
|
+
const twin = handles.find((h) => h.kind === kind && same(h.at, at));
|
|
549
|
+
if (twin) twin.refs.push({ patch: p, index });
|
|
550
|
+
else handles.push({ id: `${p}:${index}`, kind, at, refs: [{ patch: p, index }] });
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
return handles;
|
|
554
|
+
}
|
|
555
|
+
function carriedBy(patch, corner) {
|
|
556
|
+
const k = corner / 3;
|
|
557
|
+
const carried = [corner + 1, (corner + 11) % 12];
|
|
558
|
+
if (isTensorPatch(patch)) carried.push(INTERIOR_OF_CORNER[k]);
|
|
559
|
+
return carried;
|
|
560
|
+
}
|
|
561
|
+
function moveMeshHandle(mesh, handle, to) {
|
|
562
|
+
const dx = to.x - handle.at.x;
|
|
563
|
+
const dy = to.y - handle.at.y;
|
|
564
|
+
const placed = /* @__PURE__ */ new Map();
|
|
565
|
+
for (const { patch, index } of handle.refs) {
|
|
566
|
+
placed.set(`${patch}:${index}`, to);
|
|
567
|
+
}
|
|
568
|
+
if (handle.kind === "corner") {
|
|
569
|
+
for (const { patch, index } of handle.refs) {
|
|
570
|
+
for (const carried of carriedBy(mesh.patches[patch], index)) {
|
|
571
|
+
const key = `${patch}:${carried}`;
|
|
572
|
+
if (placed.has(key)) continue;
|
|
573
|
+
const from = mesh.patches[patch].points[carried];
|
|
574
|
+
placed.set(key, { x: from.x + dx, y: from.y + dy });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
...mesh,
|
|
580
|
+
patches: mesh.patches.map((patch, p) => ({
|
|
581
|
+
...patch,
|
|
582
|
+
points: patch.points.map((point, i) => placed.get(`${p}:${i}`) ?? point)
|
|
583
|
+
}))
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function meshGuides(mesh) {
|
|
587
|
+
const edges = [];
|
|
588
|
+
const arms = [];
|
|
589
|
+
for (const patch of mesh.patches) {
|
|
590
|
+
if (!isValidPatch(patch)) continue;
|
|
591
|
+
const p = patch.points;
|
|
592
|
+
for (let k = 0; k < 4; k++) {
|
|
593
|
+
const corner = k * 3;
|
|
594
|
+
edges.push([p[corner], p[corner + 1], p[corner + 2], p[(corner + 3) % 12]]);
|
|
595
|
+
for (const carried of carriedBy(patch, corner)) arms.push([p[corner], p[carried]]);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return { edges, arms };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export { MESH_BAKE_MAX, MESH_BAKE_MIN, MESH_BAKE_SIZE, MESH_GRADIENT_KIND, bakeMesh, cornerWeights, evalPatch, isMeshGradientFill, isTensorPatch, isValidPatch, meshBakeSize, meshBounds, meshDrawKind, meshFromStops, meshGradientXml, meshGuides, meshHandles, meshStops, moveMeshHandle, patchBounds, patchCorner, seedMeshPatch };
|
|
602
|
+
//# sourceMappingURL=mesh.js.map
|
|
603
|
+
//# sourceMappingURL=mesh.js.map
|