badgecraft 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +406 -0
- package/cli/index.mjs +143 -0
- package/cli/render.d.ts +20 -0
- package/cli/render.mjs +87 -0
- package/cli/studio.mjs +98 -0
- package/dist/badge-B-g_9WFj.d.ts +79 -0
- package/dist/badge-CCjUxNIG.d.cts +79 -0
- package/dist/chunk-3OMN635U.js +44 -0
- package/dist/chunk-3OMN635U.js.map +1 -0
- package/dist/chunk-BRT5S5PF.cjs +3418 -0
- package/dist/chunk-BRT5S5PF.cjs.map +1 -0
- package/dist/chunk-EBDSIGZR.cjs +48 -0
- package/dist/chunk-EBDSIGZR.cjs.map +1 -0
- package/dist/chunk-RRYBMB25.js +3410 -0
- package/dist/chunk-RRYBMB25.js.map +1 -0
- package/dist/cli-browser.js +526 -0
- package/dist/element.cjs +176 -0
- package/dist/element.cjs.map +1 -0
- package/dist/element.d.cts +31 -0
- package/dist/element.d.ts +31 -0
- package/dist/element.js +174 -0
- package/dist/element.js.map +1 -0
- package/dist/index.cjs +47 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +60 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +29 -0
- package/dist/react.d.ts +29 -0
- package/dist/react.js +55 -0
- package/dist/react.js.map +1 -0
- package/dist/studio/assets/badge-9Zq6e9Ht.js +524 -0
- package/dist/studio/assets/base-ItCYnbux.css +1 -0
- package/dist/studio/assets/base-LFa9cTPp.js +1 -0
- package/dist/studio/assets/element-CCTxyf61.js +1 -0
- package/dist/studio/assets/index-BVuSTaz-.css +1 -0
- package/dist/studio/assets/index-CTTcx2pV.js +28 -0
- package/dist/studio/assets/option-keys-CKi71y0n.js +1 -0
- package/dist/studio/assets/react-Cq5Erey2.js +9 -0
- package/dist/studio/assets/react-DyJFYpWJ.js +1 -0
- package/dist/studio/assets/studio-C6plMNOe.css +1 -0
- package/dist/studio/assets/studio-DBcSY6tV.js +1 -0
- package/dist/studio/assets/style-C2sJ2Xy3.css +1 -0
- package/dist/studio/assets/vue-BpGwr3iF.js +1 -0
- package/dist/studio/element.html +31 -0
- package/dist/studio/env/studio.hdr +5 -0
- package/dist/studio/index.html +24 -0
- package/dist/studio/logos/apple.svg +1 -0
- package/dist/studio/logos/atlassian.svg +1 -0
- package/dist/studio/logos/google.svg +1 -0
- package/dist/studio/logos/meta.svg +1 -0
- package/dist/studio/react.html +16 -0
- package/dist/studio/studio.html +27 -0
- package/dist/studio/vue.html +15 -0
- package/dist/types-CHM3-ZZV.d.cts +204 -0
- package/dist/types-CHM3-ZZV.d.ts +204 -0
- package/dist/vue.cjs +93 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.cts +115 -0
- package/dist/vue.d.ts +115 -0
- package/dist/vue.js +87 -0
- package/dist/vue.js.map +1 -0
- package/docs/cli.md +146 -0
- package/docs/gallery.jpg +0 -0
- package/docs/performance.md +151 -0
- package/docs/render-quality.md +220 -0
- package/docs/studio.jpg +0 -0
- package/package.json +163 -0
|
@@ -0,0 +1,3410 @@
|
|
|
1
|
+
// src/util.ts
|
|
2
|
+
function stableStringify(value) {
|
|
3
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
4
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
5
|
+
const obj = value;
|
|
6
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0 && typeof obj[k] !== "function").sort();
|
|
7
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
8
|
+
}
|
|
9
|
+
function hashString(str, seed = 0) {
|
|
10
|
+
let h1 = 3735928559 ^ seed;
|
|
11
|
+
let h2 = 1103547991 ^ seed;
|
|
12
|
+
for (let i = 0; i < str.length; i++) {
|
|
13
|
+
const ch = str.charCodeAt(i);
|
|
14
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
15
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
16
|
+
}
|
|
17
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
18
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
19
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);
|
|
20
|
+
}
|
|
21
|
+
var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
|
|
22
|
+
|
|
23
|
+
// src/environment.ts
|
|
24
|
+
var environments = {
|
|
25
|
+
studio: {
|
|
26
|
+
top: "#727984",
|
|
27
|
+
horizon: "#20232a",
|
|
28
|
+
bottom: "#101116",
|
|
29
|
+
intensity: 1,
|
|
30
|
+
// Product-photo softboxes separated by dark space: the gaps give polished
|
|
31
|
+
// metal its contrast, while the wide key still lights coloured enamel.
|
|
32
|
+
lights: [
|
|
33
|
+
{ azimuth: -12, elevation: 8, size: [90, 65], intensity: 0.22, color: "#ffffff", softness: 1 },
|
|
34
|
+
{ azimuth: -36, elevation: 38, size: [58, 65], intensity: 4.5, color: "#fff8ef", softness: 0.45 },
|
|
35
|
+
{ azimuth: 48, elevation: 12, size: [16, 85], intensity: 3.2, color: "#e9f1ff", softness: 0.35 },
|
|
36
|
+
{ azimuth: 0, elevation: 72, size: [135, 24], intensity: 3.8, color: "#ffffff", softness: 0.5 },
|
|
37
|
+
{ azimuth: -120, elevation: 20, size: [24, 65], intensity: 1.8, color: "#ffffff", softness: 0.4 },
|
|
38
|
+
{ azimuth: 135, elevation: 30, size: 34, shape: "circle", intensity: 2, color: "#fff2dd", softness: 0.6 }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
soft: {
|
|
42
|
+
top: "#c9ccd2",
|
|
43
|
+
horizon: "#8a8c91",
|
|
44
|
+
bottom: "#3a3a3d",
|
|
45
|
+
intensity: 1,
|
|
46
|
+
lights: [
|
|
47
|
+
{ azimuth: 0, elevation: 25, size: [160, 70], intensity: 1.2, color: "#ffffff", softness: 1 },
|
|
48
|
+
{ azimuth: -60, elevation: 45, size: [80, 60], intensity: 2, color: "#fffaf0", softness: 1 }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
sunset: {
|
|
52
|
+
top: "#3b3f7a",
|
|
53
|
+
horizon: "#e0806a",
|
|
54
|
+
bottom: "#1a1012",
|
|
55
|
+
intensity: 1,
|
|
56
|
+
lights: [
|
|
57
|
+
{ azimuth: -55, elevation: 12, size: 26, shape: "circle", intensity: 14, color: "#ffb46b", softness: 0.6 },
|
|
58
|
+
{ azimuth: 0, elevation: 15, size: [120, 40], intensity: 0.9, color: "#ffc7a8", softness: 1 },
|
|
59
|
+
{ azimuth: 70, elevation: 40, size: [60, 40], intensity: 1.6, color: "#8fa6ff", softness: 0.8 }
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
night: {
|
|
63
|
+
top: "#101218",
|
|
64
|
+
horizon: "#07080b",
|
|
65
|
+
bottom: "#020203",
|
|
66
|
+
intensity: 1,
|
|
67
|
+
lights: [
|
|
68
|
+
{ azimuth: -30, elevation: 40, size: [30, 22], intensity: 9, color: "#ffffff", softness: 0.35 },
|
|
69
|
+
{ azimuth: 115, elevation: 25, size: [16, 60], intensity: 5, color: "#dfe8ff", softness: 0.3 },
|
|
70
|
+
{ azimuth: -135, elevation: 15, size: [14, 50], intensity: 4, color: "#ffffff", softness: 0.3 },
|
|
71
|
+
{ azimuth: 0, elevation: 10, size: [90, 30], intensity: 0.45, color: "#ffffff", softness: 1 }
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
neon: {
|
|
75
|
+
top: "#1a1030",
|
|
76
|
+
horizon: "#101018",
|
|
77
|
+
bottom: "#050508",
|
|
78
|
+
intensity: 1,
|
|
79
|
+
lights: [
|
|
80
|
+
{ azimuth: -45, elevation: 25, size: [20, 80], intensity: 6, color: "#ff3fa4", softness: 0.4 },
|
|
81
|
+
{ azimuth: 45, elevation: 25, size: [20, 80], intensity: 6, color: "#2fd8ff", softness: 0.4 },
|
|
82
|
+
{ azimuth: 0, elevation: 60, size: [120, 18], intensity: 3, color: "#b98cff", softness: 0.6 },
|
|
83
|
+
{ azimuth: 0, elevation: 10, size: [100, 40], intensity: 0.8, color: "#ffffff", softness: 1 }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function resolveEnvironment(input) {
|
|
88
|
+
if (!input) input = "studio";
|
|
89
|
+
if (typeof input === "string") {
|
|
90
|
+
const spec2 = Object.prototype.hasOwnProperty.call(environments, input) ? environments[input] : void 0;
|
|
91
|
+
if (spec2) return { kind: "procedural", spec: spec2, key: `preset:${input}` };
|
|
92
|
+
return { kind: "image", spec: { url: input }, key: `url:${input}|1` };
|
|
93
|
+
}
|
|
94
|
+
const finite = (value, fallback, lo, hi) => typeof value === "number" && Number.isFinite(value) ? clamp(value, lo, hi) : fallback;
|
|
95
|
+
const intensity = finite(input.intensity, 1, 0, 1e4);
|
|
96
|
+
if ("url" in input) {
|
|
97
|
+
const spec2 = { url: input.url, intensity };
|
|
98
|
+
return { kind: "image", spec: spec2, key: `url:${spec2.url}|${intensity}` };
|
|
99
|
+
}
|
|
100
|
+
const spec = {
|
|
101
|
+
...input,
|
|
102
|
+
intensity,
|
|
103
|
+
lights: input.lights?.map((light) => {
|
|
104
|
+
const size = light.size ?? 30;
|
|
105
|
+
const dimensions = Array.isArray(size) ? size : [size, size];
|
|
106
|
+
return {
|
|
107
|
+
...light,
|
|
108
|
+
azimuth: finite(light.azimuth, 0, -Infinity, Infinity) % 360,
|
|
109
|
+
elevation: finite(light.elevation, 0, -90, 90),
|
|
110
|
+
intensity: finite(light.intensity, 1, 0, 1e4),
|
|
111
|
+
softness: finite(light.softness, 0.5, 0, 1),
|
|
112
|
+
size: dimensions.map((value) => finite(value, 30, 1e-3, 170))
|
|
113
|
+
};
|
|
114
|
+
})
|
|
115
|
+
};
|
|
116
|
+
return { kind: "procedural", spec, key: `proc:${stableStringify(spec)}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/materials.ts
|
|
120
|
+
var BASE = {
|
|
121
|
+
color: "#ffffff",
|
|
122
|
+
useSvgColor: false,
|
|
123
|
+
metalness: 0,
|
|
124
|
+
roughness: 0.4,
|
|
125
|
+
clearcoat: 0,
|
|
126
|
+
clearcoatRoughness: 0.05,
|
|
127
|
+
ior: 1.5,
|
|
128
|
+
iridescence: 0,
|
|
129
|
+
iridescenceThickness: 450,
|
|
130
|
+
flakes: 0,
|
|
131
|
+
flakeScale: 220,
|
|
132
|
+
brushed: 0,
|
|
133
|
+
brushDirection: "circular",
|
|
134
|
+
emissive: 0
|
|
135
|
+
};
|
|
136
|
+
var metal = (color, roughness, extra = {}) => ({
|
|
137
|
+
color,
|
|
138
|
+
metalness: 1,
|
|
139
|
+
roughness,
|
|
140
|
+
...extra
|
|
141
|
+
});
|
|
142
|
+
var materials = {
|
|
143
|
+
gold: metal("#f7dc8f", 0.24),
|
|
144
|
+
"rose-gold": metal("#f6b99c", 0.22),
|
|
145
|
+
"white-gold": metal("#efe7d2", 0.2),
|
|
146
|
+
"matte-gold": metal("#f0d284", 0.48),
|
|
147
|
+
"brushed-gold": metal("#f7dc8f", 0.34, { brushed: 0.7, brushDirection: "circular" }),
|
|
148
|
+
silver: metal("#f1f0ee", 0.24),
|
|
149
|
+
"brushed-silver": metal("#e9e9ea", 0.3, { brushed: 0.7, brushDirection: "circular" }),
|
|
150
|
+
platinum: metal("#d8d3cb", 0.22),
|
|
151
|
+
chrome: metal("#e6e9ec", 0.04),
|
|
152
|
+
copper: metal("#f2a27c", 0.24),
|
|
153
|
+
bronze: metal("#d69a5c", 0.32),
|
|
154
|
+
brass: metal("#e9c46f", 0.26),
|
|
155
|
+
titanium: metal("#b9b2a9", 0.3),
|
|
156
|
+
gunmetal: metal("#6b6d73", 0.28),
|
|
157
|
+
"black-chrome": metal("#3c3d42", 0.1),
|
|
158
|
+
enamel: { useSvgColor: true, metalness: 0, roughness: 0.28, clearcoat: 1, clearcoatRoughness: 0.09 },
|
|
159
|
+
"matte-enamel": { useSvgColor: true, metalness: 0, roughness: 0.62, clearcoat: 0.15, clearcoatRoughness: 0.4 },
|
|
160
|
+
candy: { useSvgColor: true, metalness: 0.85, roughness: 0.3, clearcoat: 1, clearcoatRoughness: 0.04 },
|
|
161
|
+
glitter: {
|
|
162
|
+
useSvgColor: true,
|
|
163
|
+
metalness: 0.2,
|
|
164
|
+
roughness: 0.35,
|
|
165
|
+
clearcoat: 1,
|
|
166
|
+
clearcoatRoughness: 0.04,
|
|
167
|
+
flakes: 0.9,
|
|
168
|
+
flakeScale: 260
|
|
169
|
+
},
|
|
170
|
+
pearl: {
|
|
171
|
+
color: "#f4eee6",
|
|
172
|
+
metalness: 0.15,
|
|
173
|
+
roughness: 0.28,
|
|
174
|
+
clearcoat: 1,
|
|
175
|
+
clearcoatRoughness: 0.05,
|
|
176
|
+
iridescence: 0.7,
|
|
177
|
+
iridescenceThickness: 560
|
|
178
|
+
},
|
|
179
|
+
holographic: metal("#f2f2f4", 0.18, { iridescence: 1, iridescenceThickness: 420, flakes: 0.4 }),
|
|
180
|
+
onyx: { color: "#191b20", metalness: 0, roughness: 0.28, clearcoat: 1, clearcoatRoughness: 0.1 },
|
|
181
|
+
obsidian: { color: "#0e0e10", metalness: 0, roughness: 0.2, clearcoat: 1, clearcoatRoughness: 0.03 }
|
|
182
|
+
};
|
|
183
|
+
var defaultSlotMaterials = {
|
|
184
|
+
metal: "gold",
|
|
185
|
+
enamel: "enamel",
|
|
186
|
+
base: "onyx",
|
|
187
|
+
accent: "silver"
|
|
188
|
+
};
|
|
189
|
+
function resolveMaterial(input, slot) {
|
|
190
|
+
const value = input ?? defaultSlotMaterials[slot];
|
|
191
|
+
let spec;
|
|
192
|
+
if (typeof value === "string") {
|
|
193
|
+
spec = materials[value] ?? materials[defaultSlotMaterials[slot]];
|
|
194
|
+
} else {
|
|
195
|
+
const { preset, ...rest } = value;
|
|
196
|
+
spec = { ...preset ? materials[preset] : {}, ...stripUndefined(rest) };
|
|
197
|
+
}
|
|
198
|
+
const out = { ...BASE, ...stripUndefined(spec) };
|
|
199
|
+
const ranges = {
|
|
200
|
+
metalness: [0, 1],
|
|
201
|
+
roughness: [0, 1],
|
|
202
|
+
clearcoat: [0, 1],
|
|
203
|
+
clearcoatRoughness: [0, 1],
|
|
204
|
+
ior: [1, 3],
|
|
205
|
+
iridescence: [0, 1],
|
|
206
|
+
iridescenceThickness: [100, 1200],
|
|
207
|
+
flakes: [0, 1],
|
|
208
|
+
flakeScale: [1, 1e4],
|
|
209
|
+
brushed: [0, 1],
|
|
210
|
+
emissive: [0, 1]
|
|
211
|
+
};
|
|
212
|
+
for (const key of Object.keys(ranges)) {
|
|
213
|
+
const [lo, hi] = ranges[key];
|
|
214
|
+
out[key] = Number.isFinite(out[key]) ? clamp(out[key], lo, hi) : BASE[key];
|
|
215
|
+
}
|
|
216
|
+
if (spec.useSvgColor === void 0 && slot === "enamel" && !spec.color) out.useSvgColor = true;
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
function stripUndefined(obj) {
|
|
220
|
+
const out = {};
|
|
221
|
+
for (const key of Object.keys(obj)) if (obj[key] !== void 0) out[key] = obj[key];
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/options.ts
|
|
226
|
+
var defaults = {
|
|
227
|
+
mode: "auto",
|
|
228
|
+
color: "#ffffff",
|
|
229
|
+
rim: 0.03,
|
|
230
|
+
outline: "auto",
|
|
231
|
+
autoOutline: 0.012,
|
|
232
|
+
fillHoles: true,
|
|
233
|
+
relief: {
|
|
234
|
+
depth: 0.05,
|
|
235
|
+
thickness: 0.03,
|
|
236
|
+
floor: 0.3,
|
|
237
|
+
profile: "round",
|
|
238
|
+
bevel: 0.02,
|
|
239
|
+
edge: 0.014,
|
|
240
|
+
enamelLevel: 0.16,
|
|
241
|
+
enamelDome: 0.12,
|
|
242
|
+
domeWidth: 0.05,
|
|
243
|
+
smoothing: 0.9,
|
|
244
|
+
ao: 0.45
|
|
245
|
+
},
|
|
246
|
+
exposure: 1,
|
|
247
|
+
toneMapping: "neutral",
|
|
248
|
+
interaction: "tilt",
|
|
249
|
+
tiltMax: 18,
|
|
250
|
+
pose: { pitch: -5, yaw: -8, roll: 0 },
|
|
251
|
+
idle: "none",
|
|
252
|
+
lockedColor: "#3a3a3c",
|
|
253
|
+
shadow: 0.35,
|
|
254
|
+
padding: 0.06
|
|
255
|
+
};
|
|
256
|
+
var PROFILE = { round: 0, soft: 1, chamfer: 2 };
|
|
257
|
+
var TONE = { neutral: 0, aces: 1, agx: 2, none: 3 };
|
|
258
|
+
var num = (v, fallback, lo = -Infinity, hi = Infinity) => typeof v === "number" && Number.isFinite(v) ? clamp(v, lo, hi) : fallback;
|
|
259
|
+
function resolveOptions(o) {
|
|
260
|
+
const r = { ...defaults.relief, ...stripUndefined2(o.relief ?? {}) };
|
|
261
|
+
const frame = o.frame == null ? null : typeof o.frame === "string" ? { shape: o.frame } : { shape: "circle", ...o.frame };
|
|
262
|
+
if (frame) {
|
|
263
|
+
frame.padding = num(frame.padding, 0.22, 0, 0.45);
|
|
264
|
+
frame.innerRing = num(frame.innerRing, 0, 0, 0.3);
|
|
265
|
+
}
|
|
266
|
+
const bake2 = {
|
|
267
|
+
depth: num(r.depth, defaults.relief.depth, 1e-3, 0.5),
|
|
268
|
+
mode: o.mode ?? defaults.mode,
|
|
269
|
+
color: o.color ?? defaults.color,
|
|
270
|
+
frame,
|
|
271
|
+
rim: num(o.rim, defaults.rim, 0, 0.3),
|
|
272
|
+
outline: o.outline === void 0 || o.outline === "auto" ? "auto" : num(o.outline, 0, 0, 0.3),
|
|
273
|
+
autoOutline: defaults.autoOutline,
|
|
274
|
+
fillHoles: o.fillHoles ?? defaults.fillHoles,
|
|
275
|
+
floor: num(r.floor, defaults.relief.floor, 0, 0.95),
|
|
276
|
+
profile: PROFILE[r.profile] ?? 0,
|
|
277
|
+
bevel: num(r.bevel, defaults.relief.bevel, 1e-3, 0.5),
|
|
278
|
+
edge: num(r.edge, defaults.relief.edge, 1e-3, 0.2),
|
|
279
|
+
enamelLevel: num(r.enamelLevel, defaults.relief.enamelLevel, 0, 1),
|
|
280
|
+
enamelDome: num(r.enamelDome, defaults.relief.enamelDome, -1, 1),
|
|
281
|
+
domeWidth: num(r.domeWidth, defaults.relief.domeWidth, 1e-3, 0.5),
|
|
282
|
+
smoothing: num(r.smoothing, defaults.relief.smoothing, 0, 8)
|
|
283
|
+
};
|
|
284
|
+
const render = {
|
|
285
|
+
materials: [
|
|
286
|
+
resolveMaterial(o.metal, "metal"),
|
|
287
|
+
resolveMaterial(o.enamel, "enamel"),
|
|
288
|
+
resolveMaterial(o.base, "base"),
|
|
289
|
+
resolveMaterial(o.accent, "accent")
|
|
290
|
+
],
|
|
291
|
+
environment: resolveEnvironment(o.environment),
|
|
292
|
+
envRotation: num(o.environmentRotation, 0) % 360 * Math.PI / 180,
|
|
293
|
+
thickness: num(r.thickness, defaults.relief.thickness, 0, 0.5),
|
|
294
|
+
edge: bake2.edge,
|
|
295
|
+
ao: num(r.ao, defaults.relief.ao, 0, 1),
|
|
296
|
+
exposure: num(o.exposure, defaults.exposure, 0, 16),
|
|
297
|
+
toneMapping: TONE[o.toneMapping ?? defaults.toneMapping] ?? 0,
|
|
298
|
+
interaction: o.interaction ?? defaults.interaction,
|
|
299
|
+
tiltMax: num(o.tiltMax, defaults.tiltMax, 0, 80),
|
|
300
|
+
pose: {
|
|
301
|
+
pitch: num(o.pose?.pitch, defaults.pose.pitch, -89, 89),
|
|
302
|
+
yaw: num(o.pose?.yaw, defaults.pose.yaw, -180, 180),
|
|
303
|
+
roll: num(o.pose?.roll, defaults.pose.roll, -180, 180)
|
|
304
|
+
},
|
|
305
|
+
idle: o.idle ?? defaults.idle,
|
|
306
|
+
locked: !!o.locked,
|
|
307
|
+
lockedColor: o.lockedColor ?? defaults.lockedColor,
|
|
308
|
+
shadow: num(o.shadow, defaults.shadow, 0, 1),
|
|
309
|
+
padding: num(o.padding, defaults.padding, 0, 0.4)
|
|
310
|
+
};
|
|
311
|
+
const quality = typeof o.quality !== "number" || !Number.isFinite(o.quality) ? "auto" : clamp(Math.round(o.quality), 64, 2048);
|
|
312
|
+
return {
|
|
313
|
+
bake: bake2,
|
|
314
|
+
bakeKey: stableStringify(bake2),
|
|
315
|
+
render,
|
|
316
|
+
renderKey: stableStringify(render),
|
|
317
|
+
quality
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function stripUndefined2(obj) {
|
|
321
|
+
const out = {};
|
|
322
|
+
for (const k of Object.keys(obj)) if (obj[k] !== void 0) out[k] = obj[k];
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/svg/frames.ts
|
|
327
|
+
var f = (n) => Math.round(n * 1e3) / 1e3;
|
|
328
|
+
function roundedPolygon(points, radius) {
|
|
329
|
+
const n = points.length;
|
|
330
|
+
let d = "";
|
|
331
|
+
for (let i = 0; i < n; i++) {
|
|
332
|
+
const p = points[i];
|
|
333
|
+
const prev = points[(i - 1 + n) % n];
|
|
334
|
+
const next = points[(i + 1) % n];
|
|
335
|
+
const lenPrev = Math.hypot(prev[0] - p[0], prev[1] - p[1]);
|
|
336
|
+
const lenNext = Math.hypot(next[0] - p[0], next[1] - p[1]);
|
|
337
|
+
const r = Math.min(radius, lenPrev / 2, lenNext / 2);
|
|
338
|
+
const a = [p[0] + (prev[0] - p[0]) / lenPrev * r, p[1] + (prev[1] - p[1]) / lenPrev * r];
|
|
339
|
+
const b = [p[0] + (next[0] - p[0]) / lenNext * r, p[1] + (next[1] - p[1]) / lenNext * r];
|
|
340
|
+
d += `${i === 0 ? "M" : "L"}${f(a[0])} ${f(a[1])}Q${f(p[0])} ${f(p[1])} ${f(b[0])} ${f(b[1])}`;
|
|
341
|
+
}
|
|
342
|
+
return `${d}Z`;
|
|
343
|
+
}
|
|
344
|
+
function regular(count, radius, startDeg, corner, cx = 50, cy = 50) {
|
|
345
|
+
const pts = [];
|
|
346
|
+
for (let i = 0; i < count; i++) {
|
|
347
|
+
const a = (startDeg + 360 / count * i) * Math.PI / 180;
|
|
348
|
+
pts.push([cx + Math.cos(a) * radius, cy + Math.sin(a) * radius]);
|
|
349
|
+
}
|
|
350
|
+
return roundedPolygon(pts, corner);
|
|
351
|
+
}
|
|
352
|
+
function star(points, outer, inner, corner) {
|
|
353
|
+
const pts = [];
|
|
354
|
+
for (let i = 0; i < points * 2; i++) {
|
|
355
|
+
const r = i % 2 === 0 ? outer : inner;
|
|
356
|
+
const a = (-90 + 180 / points * i) * Math.PI / 180;
|
|
357
|
+
pts.push([50 + Math.cos(a) * r, 50 + Math.sin(a) * r]);
|
|
358
|
+
}
|
|
359
|
+
return roundedPolygon(pts, corner);
|
|
360
|
+
}
|
|
361
|
+
var framePaths = {
|
|
362
|
+
circle: "M50 3A47 47 0 1 1 50 97A47 47 0 1 1 50 3Z",
|
|
363
|
+
hexagon: regular(6, 48, -90, 9),
|
|
364
|
+
octagon: regular(8, 48, -67.5, 6),
|
|
365
|
+
shield: "M14 13Q50 1 86 13L86 47C86 72 70 88 50 97C30 88 14 72 14 47Z",
|
|
366
|
+
square: roundedPolygon(
|
|
367
|
+
[
|
|
368
|
+
[5, 5],
|
|
369
|
+
[95, 5],
|
|
370
|
+
[95, 95],
|
|
371
|
+
[5, 95]
|
|
372
|
+
],
|
|
373
|
+
16
|
|
374
|
+
),
|
|
375
|
+
diamond: regular(4, 48, -90, 8),
|
|
376
|
+
star: star(5, 49, 27, 5)
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// src/color.ts
|
|
380
|
+
var probe;
|
|
381
|
+
function normalizeWithCanvas(color) {
|
|
382
|
+
if (probe === void 0) {
|
|
383
|
+
probe = typeof document !== "undefined" ? document.createElement("canvas").getContext("2d") : null;
|
|
384
|
+
}
|
|
385
|
+
if (!probe) return null;
|
|
386
|
+
probe.fillStyle = "#000000";
|
|
387
|
+
probe.fillStyle = color;
|
|
388
|
+
const a = String(probe.fillStyle);
|
|
389
|
+
probe.fillStyle = "#ffffff";
|
|
390
|
+
probe.fillStyle = color;
|
|
391
|
+
const b = String(probe.fillStyle);
|
|
392
|
+
if (a !== b) return null;
|
|
393
|
+
if (!/^#|^rgba?\(/i.test(a)) {
|
|
394
|
+
probe.clearRect(0, 0, 1, 1);
|
|
395
|
+
probe.fillRect(0, 0, 1, 1);
|
|
396
|
+
const p = probe.getImageData(0, 0, 1, 1).data;
|
|
397
|
+
return `rgb(${p[0]},${p[1]},${p[2]})`;
|
|
398
|
+
}
|
|
399
|
+
return a;
|
|
400
|
+
}
|
|
401
|
+
function parseColor(color, fallback = [1, 1, 1]) {
|
|
402
|
+
if (!color) return fallback;
|
|
403
|
+
let c = color.trim();
|
|
404
|
+
const normalized = /^#|^rgba?\(/i.test(c) ? c : normalizeWithCanvas(c);
|
|
405
|
+
if (!normalized) return fallback;
|
|
406
|
+
c = normalized;
|
|
407
|
+
if (c[0] === "#") {
|
|
408
|
+
let hex = c.slice(1);
|
|
409
|
+
if (!/^(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.test(hex)) return fallback;
|
|
410
|
+
if (hex.length === 3 || hex.length === 4) hex = [...hex.slice(0, 3)].map((h) => h + h).join("");
|
|
411
|
+
if (hex.length >= 6) {
|
|
412
|
+
const v = parseInt(hex.slice(0, 6), 16);
|
|
413
|
+
if (!Number.isNaN(v)) return [(v >> 16 & 255) / 255, (v >> 8 & 255) / 255, (v & 255) / 255];
|
|
414
|
+
}
|
|
415
|
+
return fallback;
|
|
416
|
+
}
|
|
417
|
+
const m = c.match(/rgba?\(([^)]+)\)/i);
|
|
418
|
+
if (m) {
|
|
419
|
+
const parts = m[1].split(/[\s,/]+/).filter(Boolean);
|
|
420
|
+
const ch = parts.slice(0, 3).map((p) => p.endsWith("%") ? parseFloat(p) / 100 : parseFloat(p) / 255);
|
|
421
|
+
if (ch.length === 3 && ch.every((v) => Number.isFinite(v))) return ch.map((v) => Math.max(0, Math.min(1, v)));
|
|
422
|
+
}
|
|
423
|
+
return fallback;
|
|
424
|
+
}
|
|
425
|
+
function srgbToLinear(c) {
|
|
426
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
427
|
+
}
|
|
428
|
+
function linearRgb(color, fallback) {
|
|
429
|
+
const [r, g, b] = parseColor(color, fallback);
|
|
430
|
+
return [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/bake/edt.ts
|
|
434
|
+
var INF = 1e20;
|
|
435
|
+
function scratch(n) {
|
|
436
|
+
return { f: new Float64Array(n), z: new Float64Array(n + 1), v: new Int32Array(n) };
|
|
437
|
+
}
|
|
438
|
+
function edt1d(grid, offset, stride, length, s, arg) {
|
|
439
|
+
const { f: f2, z, v } = s;
|
|
440
|
+
v[0] = 0;
|
|
441
|
+
z[0] = -INF;
|
|
442
|
+
z[1] = INF;
|
|
443
|
+
f2[0] = grid[offset];
|
|
444
|
+
for (let q = 1, k = 0; q < length; q++) {
|
|
445
|
+
f2[q] = grid[offset + q * stride];
|
|
446
|
+
const q2 = q * q;
|
|
447
|
+
let sq;
|
|
448
|
+
do {
|
|
449
|
+
const r = v[k];
|
|
450
|
+
sq = (f2[q] - f2[r] + q2 - r * r) / (q - r) / 2;
|
|
451
|
+
} while (sq <= z[k] && --k > -1);
|
|
452
|
+
k++;
|
|
453
|
+
v[k] = q;
|
|
454
|
+
z[k] = sq;
|
|
455
|
+
z[k + 1] = INF;
|
|
456
|
+
}
|
|
457
|
+
for (let q = 0, k = 0; q < length; q++) {
|
|
458
|
+
while (z[k + 1] < q) k++;
|
|
459
|
+
const r = v[k];
|
|
460
|
+
const qr = q - r;
|
|
461
|
+
grid[offset + q * stride] = f2[r] + qr * qr;
|
|
462
|
+
if (arg) arg[offset + q * stride] = r;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function edt2dNearest(grid, width, height) {
|
|
466
|
+
const s = scratch(Math.max(width, height));
|
|
467
|
+
const rowOf = new Int32Array(width * height);
|
|
468
|
+
const colOf = new Int32Array(width * height);
|
|
469
|
+
for (let x = 0; x < width; x++) edt1d(grid, x, width, height, s, rowOf);
|
|
470
|
+
for (let y = 0; y < height; y++) edt1d(grid, y * width, 1, width, s, colOf);
|
|
471
|
+
const nearest = new Int32Array(width * height);
|
|
472
|
+
for (let y = 0; y < height; y++) {
|
|
473
|
+
const row = y * width;
|
|
474
|
+
for (let x = 0; x < width; x++) {
|
|
475
|
+
const sx = colOf[row + x];
|
|
476
|
+
const sy = rowOf[row + sx];
|
|
477
|
+
nearest[row + x] = sy * width + sx;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return nearest;
|
|
481
|
+
}
|
|
482
|
+
function edgeOffset(nx, ny, a) {
|
|
483
|
+
let gx = Math.abs(nx);
|
|
484
|
+
let gy = Math.abs(ny);
|
|
485
|
+
if (gx < gy) {
|
|
486
|
+
const t = gx;
|
|
487
|
+
gx = gy;
|
|
488
|
+
gy = t;
|
|
489
|
+
}
|
|
490
|
+
if (gy < 1e-6) return 0.5 - a;
|
|
491
|
+
const a1 = 0.5 * gy / gx;
|
|
492
|
+
if (a < a1) return 0.5 * (gx + gy) - Math.sqrt(2 * gx * gy * a);
|
|
493
|
+
if (a < 1 - a1) return (0.5 - a) * gx;
|
|
494
|
+
return -0.5 * (gx + gy) + Math.sqrt(2 * gx * gy * (1 - a));
|
|
495
|
+
}
|
|
496
|
+
function signedDistance(coverage, width, height, options = {}) {
|
|
497
|
+
const insideOnly = options.insideOnly ?? false;
|
|
498
|
+
const refineSq = (options.refineWithin ?? Infinity) ** 2;
|
|
499
|
+
const wideSq = Math.min(refineSq, 2304);
|
|
500
|
+
const n = width * height;
|
|
501
|
+
const grid = new Float32Array(n);
|
|
502
|
+
const ex = new Float32Array(n);
|
|
503
|
+
const ey = new Float32Array(n);
|
|
504
|
+
const nx = new Float32Array(n);
|
|
505
|
+
const ny = new Float32Array(n);
|
|
506
|
+
let seeds = 0;
|
|
507
|
+
const at = (x, y) => coverage[(y < 0 ? 0 : y >= height ? height - 1 : y) * width + (x < 0 ? 0 : x >= width ? width - 1 : x)];
|
|
508
|
+
for (let y = 0; y < height; y++) {
|
|
509
|
+
const interiorRow = y > 0 && y < height - 1;
|
|
510
|
+
for (let x = 0; x < width; x++) {
|
|
511
|
+
const i = y * width + x;
|
|
512
|
+
const a = coverage[i];
|
|
513
|
+
let boundary;
|
|
514
|
+
if (a > 0 && a < 1) boundary = true;
|
|
515
|
+
else if (interiorRow && x > 0 && x < width - 1) {
|
|
516
|
+
const l = coverage[i - 1];
|
|
517
|
+
const r = coverage[i + 1];
|
|
518
|
+
const u = coverage[i - width];
|
|
519
|
+
const d = coverage[i + width];
|
|
520
|
+
boundary = a <= 0 ? l >= 1 || r >= 1 || u >= 1 || d >= 1 : l <= 0 || r <= 0 || u <= 0 || d <= 0;
|
|
521
|
+
} else {
|
|
522
|
+
boundary = a <= 0 ? Math.max(at(x - 1, y), at(x + 1, y), at(x, y - 1), at(x, y + 1)) >= 1 : Math.min(at(x - 1, y), at(x + 1, y), at(x, y - 1), at(x, y + 1)) <= 0;
|
|
523
|
+
}
|
|
524
|
+
if (!boundary) {
|
|
525
|
+
grid[i] = INF;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
grid[i] = 0;
|
|
529
|
+
seeds++;
|
|
530
|
+
const gx = at(x + 1, y - 1) + 2 * at(x + 1, y) + at(x + 1, y + 1) - at(x - 1, y - 1) - 2 * at(x - 1, y) - at(x - 1, y + 1);
|
|
531
|
+
const gy = at(x - 1, y + 1) + 2 * at(x, y + 1) + at(x + 1, y + 1) - at(x - 1, y - 1) - 2 * at(x, y - 1) - at(x + 1, y - 1);
|
|
532
|
+
const gl = Math.hypot(gx, gy);
|
|
533
|
+
if (gl > 1e-4) {
|
|
534
|
+
const ux = gx / gl;
|
|
535
|
+
const uy = gy / gl;
|
|
536
|
+
nx[i] = ux;
|
|
537
|
+
ny[i] = uy;
|
|
538
|
+
const off = edgeOffset(ux, uy, a);
|
|
539
|
+
ex[i] = x + ux * off;
|
|
540
|
+
ey[i] = y + uy * off;
|
|
541
|
+
} else {
|
|
542
|
+
ex[i] = x;
|
|
543
|
+
ey[i] = y;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const sd = new Float32Array(n);
|
|
548
|
+
if (seeds === 0) {
|
|
549
|
+
const v = coverage[0] >= 0.5 ? width + height : -(width + height);
|
|
550
|
+
sd.fill(v);
|
|
551
|
+
return sd;
|
|
552
|
+
}
|
|
553
|
+
const isSeed = new Uint8Array(n);
|
|
554
|
+
for (let i = 0; i < n; i++) isSeed[i] = grid[i] === 0 ? 1 : 0;
|
|
555
|
+
const nearest = edt2dNearest(grid, width, height);
|
|
556
|
+
for (let y = 0; y < height; y++) {
|
|
557
|
+
for (let x = 0; x < width; x++) {
|
|
558
|
+
const i = y * width + x;
|
|
559
|
+
if (insideOnly && coverage[i] <= 0) continue;
|
|
560
|
+
const c = nearest[i];
|
|
561
|
+
if (grid[i] > refineSq) {
|
|
562
|
+
const d2 = Math.sqrt(grid[i]);
|
|
563
|
+
sd[i] = coverage[i] >= 0.5 ? d2 : -d2;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const R = grid[i] < wideSq ? 2 : 1;
|
|
567
|
+
const cx = c % width;
|
|
568
|
+
const cy = (c - cx) / width;
|
|
569
|
+
let s = c;
|
|
570
|
+
let best = Infinity;
|
|
571
|
+
for (let oy = -R; oy <= R; oy++) {
|
|
572
|
+
const yy = cy + oy;
|
|
573
|
+
if (yy < 0 || yy >= height) continue;
|
|
574
|
+
for (let ox = -R; ox <= R; ox++) {
|
|
575
|
+
const xx = cx + ox;
|
|
576
|
+
if (xx < 0 || xx >= width) continue;
|
|
577
|
+
const q = yy * width + xx;
|
|
578
|
+
if (!isSeed[q]) continue;
|
|
579
|
+
const qx = x - ex[q];
|
|
580
|
+
const qy = y - ey[q];
|
|
581
|
+
const d2 = qx * qx + qy * qy;
|
|
582
|
+
if (d2 < best) {
|
|
583
|
+
best = d2;
|
|
584
|
+
s = q;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const dx = x - ex[s];
|
|
589
|
+
const dy = y - ey[s];
|
|
590
|
+
const d = Math.sqrt(best);
|
|
591
|
+
const along = dx * nx[s] + dy * ny[s];
|
|
592
|
+
const inside = d < 1.5 && (nx[s] !== 0 || ny[s] !== 0) ? along >= 0 : coverage[i] >= 0.5;
|
|
593
|
+
sd[i] = inside ? d : -d;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return sd;
|
|
597
|
+
}
|
|
598
|
+
function insideDistance(coverage, width, height, refineWithin) {
|
|
599
|
+
const sd = signedDistance(coverage, width, height, { insideOnly: true, refineWithin });
|
|
600
|
+
for (let i = 0; i < sd.length; i++) sd[i] = coverage[i] > 0 ? Math.max(0, sd[i]) : 0;
|
|
601
|
+
return sd;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/bake/filters.ts
|
|
605
|
+
function gaussianBlur(src, width, height, sigma) {
|
|
606
|
+
if (sigma <= 0.01) return src.slice();
|
|
607
|
+
const radius = Math.max(1, Math.ceil(sigma * 3));
|
|
608
|
+
const kernel = new Float32Array(radius * 2 + 1);
|
|
609
|
+
let sum = 0;
|
|
610
|
+
for (let i = -radius; i <= radius; i++) {
|
|
611
|
+
const w = Math.exp(-i * i / (2 * sigma * sigma));
|
|
612
|
+
kernel[i + radius] = w;
|
|
613
|
+
sum += w;
|
|
614
|
+
}
|
|
615
|
+
for (let i = 0; i < kernel.length; i++) kernel[i] /= sum;
|
|
616
|
+
const tmp = new Float32Array(width * height);
|
|
617
|
+
const out = new Float32Array(width * height);
|
|
618
|
+
const taps = kernel.length;
|
|
619
|
+
for (let y = 0; y < height; y++) {
|
|
620
|
+
const row = y * width;
|
|
621
|
+
for (let x = 0; x < width; x++) {
|
|
622
|
+
let acc = 0;
|
|
623
|
+
if (x >= radius && x < width - radius) {
|
|
624
|
+
const start = row + x - radius;
|
|
625
|
+
for (let k = 0; k < taps; k++) acc += src[start + k] * kernel[k];
|
|
626
|
+
} else {
|
|
627
|
+
for (let k = -radius; k <= radius; k++) {
|
|
628
|
+
const xx = x + k < 0 ? 0 : x + k >= width ? width - 1 : x + k;
|
|
629
|
+
acc += src[row + xx] * kernel[k + radius];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
tmp[row + x] = acc;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (let k = -radius; k <= radius; k++) {
|
|
636
|
+
const w = kernel[k + radius];
|
|
637
|
+
for (let y = 0; y < height; y++) {
|
|
638
|
+
const yy = y + k < 0 ? 0 : y + k >= height ? height - 1 : y + k;
|
|
639
|
+
const srcRow = yy * width;
|
|
640
|
+
const dstRow = y * width;
|
|
641
|
+
for (let x = 0; x < width; x++) out[dstRow + x] += tmp[srcRow + x] * w;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return out;
|
|
645
|
+
}
|
|
646
|
+
function boxBlurPass(src, dst, width, height, r) {
|
|
647
|
+
const tmp = new Float32Array(width * height);
|
|
648
|
+
const inv = 1 / (2 * r + 1);
|
|
649
|
+
for (let y = 0; y < height; y++) {
|
|
650
|
+
const row = y * width;
|
|
651
|
+
let acc = src[row] * (r + 1);
|
|
652
|
+
for (let k = 1; k <= r; k++) acc += src[row + Math.min(k, width - 1)];
|
|
653
|
+
for (let x = 0; x < width; x++) {
|
|
654
|
+
tmp[row + x] = acc * inv;
|
|
655
|
+
const add = src[row + Math.min(x + r + 1, width - 1)];
|
|
656
|
+
const sub = src[row + Math.max(x - r, 0)];
|
|
657
|
+
acc += add - sub;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (let x = 0; x < width; x++) {
|
|
661
|
+
let acc = tmp[x] * (r + 1);
|
|
662
|
+
for (let k = 1; k <= r; k++) acc += tmp[Math.min(k, height - 1) * width + x];
|
|
663
|
+
for (let y = 0; y < height; y++) {
|
|
664
|
+
dst[y * width + x] = acc * inv;
|
|
665
|
+
const add = tmp[Math.min(y + r + 1, height - 1) * width + x];
|
|
666
|
+
const sub = tmp[Math.max(y - r, 0) * width + x];
|
|
667
|
+
acc += add - sub;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function fastBlur(src, width, height, sigma) {
|
|
672
|
+
if (sigma < 2) return gaussianBlur(src, width, height, sigma);
|
|
673
|
+
const n = 3;
|
|
674
|
+
const wIdeal = Math.sqrt(12 * sigma * sigma / n + 1);
|
|
675
|
+
let wl = Math.floor(wIdeal);
|
|
676
|
+
if (wl % 2 === 0) wl--;
|
|
677
|
+
const wu = wl + 2;
|
|
678
|
+
const m = Math.round((12 * sigma * sigma - n * wl * wl - 4 * n * wl - 3 * n) / (-4 * wl - 4));
|
|
679
|
+
let a = src.slice();
|
|
680
|
+
let b = new Float32Array(width * height);
|
|
681
|
+
for (let i = 0; i < n; i++) {
|
|
682
|
+
const size = i < m ? wl : wu;
|
|
683
|
+
boxBlurPass(a, b, width, height, size - 1 >> 1);
|
|
684
|
+
const t = a;
|
|
685
|
+
a = b;
|
|
686
|
+
b = t;
|
|
687
|
+
}
|
|
688
|
+
return a;
|
|
689
|
+
}
|
|
690
|
+
function maskedBlur(value, mask, width, height, sigma) {
|
|
691
|
+
const n = width * height;
|
|
692
|
+
const weighted = new Float32Array(n);
|
|
693
|
+
for (let i = 0; i < n; i++) weighted[i] = value[i] * mask[i];
|
|
694
|
+
const num2 = fastBlur(weighted, width, height, sigma);
|
|
695
|
+
const den = fastBlur(mask, width, height, sigma);
|
|
696
|
+
const out = new Float32Array(n);
|
|
697
|
+
for (let i = 0; i < n; i++) out[i] = den[i] > 1e-4 ? num2[i] / den[i] : value[i];
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
function sampleBilinear(src, width, height, x, y) {
|
|
701
|
+
x = x < 0 ? 0 : x > width - 1 ? width - 1 : x;
|
|
702
|
+
y = y < 0 ? 0 : y > height - 1 ? height - 1 : y;
|
|
703
|
+
const x0 = Math.floor(x);
|
|
704
|
+
const y0 = Math.floor(y);
|
|
705
|
+
const x1 = x0 + 1 < width ? x0 + 1 : x0;
|
|
706
|
+
const y1 = y0 + 1 < height ? y0 + 1 : y0;
|
|
707
|
+
const fx = x - x0;
|
|
708
|
+
const fy = y - y0;
|
|
709
|
+
const a = src[y0 * width + x0];
|
|
710
|
+
const b = src[y0 * width + x1];
|
|
711
|
+
const c = src[y1 * width + x0];
|
|
712
|
+
const d = src[y1 * width + x1];
|
|
713
|
+
return (a + (b - a) * fx) * (1 - fy) + (c + (d - c) * fx) * fy;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/bake/bake.ts
|
|
717
|
+
var TUBE_ASPECT = 0.6;
|
|
718
|
+
function profileCurve(t, kind) {
|
|
719
|
+
if (t <= 0) return 0;
|
|
720
|
+
if (t >= 1) return 1;
|
|
721
|
+
if (kind === 0) {
|
|
722
|
+
const u = 1 - t;
|
|
723
|
+
return Math.sqrt(1 - u * u);
|
|
724
|
+
}
|
|
725
|
+
if (kind === 1) return Math.sin(t * Math.PI / 2);
|
|
726
|
+
return t;
|
|
727
|
+
}
|
|
728
|
+
function channel(src, c, n) {
|
|
729
|
+
const out = new Float32Array(n);
|
|
730
|
+
for (let i = 0; i < n; i++) out[i] = src[i * 4 + c] / 255;
|
|
731
|
+
return out;
|
|
732
|
+
}
|
|
733
|
+
function fillHoles(union, size) {
|
|
734
|
+
const n = size * size;
|
|
735
|
+
const outside = new Uint8Array(n);
|
|
736
|
+
const stack = new Int32Array(n);
|
|
737
|
+
let sp = 0;
|
|
738
|
+
const seed = (i) => {
|
|
739
|
+
if (!outside[i] && union[i] < 0.5) {
|
|
740
|
+
outside[i] = 1;
|
|
741
|
+
stack[sp++] = i;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
for (let x = 0; x < size; x++) {
|
|
745
|
+
seed(x);
|
|
746
|
+
seed((size - 1) * size + x);
|
|
747
|
+
}
|
|
748
|
+
for (let y = 0; y < size; y++) {
|
|
749
|
+
seed(y * size);
|
|
750
|
+
seed(y * size + size - 1);
|
|
751
|
+
}
|
|
752
|
+
while (sp > 0) {
|
|
753
|
+
const i = stack[--sp];
|
|
754
|
+
const x = i % size;
|
|
755
|
+
if (x > 0) seed(i - 1);
|
|
756
|
+
if (x < size - 1) seed(i + 1);
|
|
757
|
+
if (i >= size) seed(i - size);
|
|
758
|
+
if (i < n - size) seed(i + size);
|
|
759
|
+
}
|
|
760
|
+
const out = new Float32Array(n);
|
|
761
|
+
for (let i = 0; i < n; i++) {
|
|
762
|
+
if (outside[i]) {
|
|
763
|
+
out[i] = union[i];
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const x = i % size;
|
|
767
|
+
const touchesOutside = x > 0 && outside[i - 1] || x < size - 1 && outside[i + 1] || i >= size && outside[i - size] || i < n - size && outside[i + size];
|
|
768
|
+
out[i] = touchesOutside ? union[i] : 1;
|
|
769
|
+
}
|
|
770
|
+
return out;
|
|
771
|
+
}
|
|
772
|
+
function localHalfWidth(dist, mask, wire, size, bevelPx) {
|
|
773
|
+
const n = size * size;
|
|
774
|
+
const out = new Float32Array(n);
|
|
775
|
+
const wireCap = size * 0.15;
|
|
776
|
+
const step = 0.75;
|
|
777
|
+
for (let y = 0; y < size; y++) {
|
|
778
|
+
for (let x = 0; x < size; x++) {
|
|
779
|
+
const i = y * size + x;
|
|
780
|
+
if (mask[i] <= 0.01) continue;
|
|
781
|
+
const d0 = dist[i];
|
|
782
|
+
const cap = wire[i] >= mask[i] * 0.5 ? wireCap : bevelPx * 1.05;
|
|
783
|
+
const gx = (dist[x < size - 1 ? i + 1 : i] - dist[x > 0 ? i - 1 : i]) * 0.5;
|
|
784
|
+
const gy = (dist[y < size - 1 ? i + size : i] - dist[y > 0 ? i - size : i]) * 0.5;
|
|
785
|
+
const gl = Math.hypot(gx, gy);
|
|
786
|
+
if (gl < 0.2) {
|
|
787
|
+
out[i] = d0;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
const ux = gx / gl * step;
|
|
791
|
+
const uy = gy / gl * step;
|
|
792
|
+
let px = x;
|
|
793
|
+
let py = y;
|
|
794
|
+
let prev = d0;
|
|
795
|
+
let best = d0;
|
|
796
|
+
const maxSteps = Math.ceil(cap / step) + 2;
|
|
797
|
+
for (let s = 0; s < maxSteps && best < cap; s++) {
|
|
798
|
+
px += ux;
|
|
799
|
+
py += uy;
|
|
800
|
+
const v = sampleBilinear(dist, size, size, px, py);
|
|
801
|
+
if (v <= prev + 1e-3) break;
|
|
802
|
+
prev = v;
|
|
803
|
+
best = v;
|
|
804
|
+
}
|
|
805
|
+
out[i] = best;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return out;
|
|
809
|
+
}
|
|
810
|
+
function bake(input) {
|
|
811
|
+
const { size, params } = input;
|
|
812
|
+
const n = size * size;
|
|
813
|
+
const kind = params.profile;
|
|
814
|
+
const rimPx = params.rim * size;
|
|
815
|
+
const bevelPx = Math.max(1, params.bevel * size);
|
|
816
|
+
const edgePx = Math.max(1, params.edge * size);
|
|
817
|
+
const domePx = Math.max(1, params.domeWidth * size);
|
|
818
|
+
const floor = params.floor;
|
|
819
|
+
const wire = channel(input.roles, 0, n);
|
|
820
|
+
const raisedF = channel(input.roles, 1, n);
|
|
821
|
+
const inset = channel(input.roles, 2, n);
|
|
822
|
+
const flat = channel(input.slotsA, 0, n);
|
|
823
|
+
const accent = channel(input.slotsA, 1, n);
|
|
824
|
+
const base = channel(input.slotsA, 2, n);
|
|
825
|
+
const metal2 = channel(input.slotsB, 0, n);
|
|
826
|
+
const enamel = channel(input.slotsB, 1, n);
|
|
827
|
+
const union = new Float32Array(n);
|
|
828
|
+
for (let i = 0; i < n; i++) union[i] = Math.min(1, wire[i] + raisedF[i] + inset[i] + flat[i]);
|
|
829
|
+
const silhouette = params.fillHoles ? fillHoles(union, size) : union;
|
|
830
|
+
const backplate = new Float32Array(n);
|
|
831
|
+
for (let i = 0; i < n; i++) backplate[i] = Math.max(0, silhouette[i] - union[i]);
|
|
832
|
+
const sd = signedDistance(silhouette, size, size, { refineWithin: Math.max(edgePx, rimPx) * 2 + 8 });
|
|
833
|
+
if (rimPx > 0.5) {
|
|
834
|
+
for (let i = 0; i < n; i++) {
|
|
835
|
+
if (silhouette[i] <= 0) continue;
|
|
836
|
+
const band = Math.min(silhouette[i], Math.max(0, Math.min(1, rimPx + 0.5 - sd[i])));
|
|
837
|
+
if (band <= 0) continue;
|
|
838
|
+
const keep = 1 - band;
|
|
839
|
+
wire[i] = Math.max(wire[i], band);
|
|
840
|
+
raisedF[i] *= keep;
|
|
841
|
+
inset[i] *= keep;
|
|
842
|
+
flat[i] *= keep;
|
|
843
|
+
backplate[i] *= keep;
|
|
844
|
+
metal2[i] = Math.max(metal2[i], band);
|
|
845
|
+
enamel[i] *= keep;
|
|
846
|
+
base[i] *= keep;
|
|
847
|
+
accent[i] *= keep;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const up = new Float32Array(n);
|
|
851
|
+
for (let i = 0; i < n; i++) up[i] = Math.min(1, wire[i] + raisedF[i]);
|
|
852
|
+
const dUp = insideDistance(up, size, size, size * 0.16);
|
|
853
|
+
const dIn = maskedBlur(insideDistance(inset, size, size, domePx * 1.5), inset, size, size, Math.max(1, domePx * 0.3));
|
|
854
|
+
const wLocal = maskedBlur(localHalfWidth(dUp, up, wire, size, bevelPx), up, size, size, 3 * size / 512);
|
|
855
|
+
const ridge = [];
|
|
856
|
+
for (let i = 0; i < n; i++) {
|
|
857
|
+
if (wire[i] > 0.5 && dUp[i] > 0.75 * wLocal[i] && wLocal[i] > 0.5) ridge.push(wLocal[i]);
|
|
858
|
+
}
|
|
859
|
+
ridge.sort((a, b) => a - b);
|
|
860
|
+
const wMedian = ridge.length > 8 ? ridge[ridge.length >> 1] : 0;
|
|
861
|
+
const tubeScale = wMedian > 0 ? TUBE_ASPECT * wMedian / size / Math.max(0.05, 1 - floor) : Infinity;
|
|
862
|
+
const heightScale = Math.max(params.depth * 0.2, Math.min(params.depth, tubeScale));
|
|
863
|
+
const tubeRef = wMedian > 0 ? heightScale * size * Math.max(0.05, 1 - floor) / TUBE_ASPECT : Infinity;
|
|
864
|
+
let h = new Float32Array(n);
|
|
865
|
+
let covered = 0;
|
|
866
|
+
for (let i = 0; i < n; i++) {
|
|
867
|
+
if (silhouette[i] <= 0) continue;
|
|
868
|
+
covered += silhouette[i];
|
|
869
|
+
const e = sd[i] <= 0 ? 0 : profileCurve(sd[i] / edgePx, 0);
|
|
870
|
+
const body = floor * e;
|
|
871
|
+
const cu = up[i];
|
|
872
|
+
const ci = inset[i];
|
|
873
|
+
const cf = flat[i] + backplate[i];
|
|
874
|
+
const total = cu + ci + cf;
|
|
875
|
+
if (total < 1e-4) {
|
|
876
|
+
h[i] = body;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
let acc = cf * body;
|
|
880
|
+
if (cu > 0) {
|
|
881
|
+
const wf = Math.min(1, wire[i] / Math.max(cu, 1e-4));
|
|
882
|
+
const wl = Math.max(0.75, wLocal[i]);
|
|
883
|
+
const wEff = Math.max(0.75, wf * wl + (1 - wf) * Math.min(bevelPx, wl));
|
|
884
|
+
const t = dUp[i] / wEff;
|
|
885
|
+
const rise = wf * Math.min(1, wl / tubeRef) + (1 - wf);
|
|
886
|
+
acc += cu * (body + (1 - body) * rise * profileCurve(t, kind));
|
|
887
|
+
}
|
|
888
|
+
if (ci > 0) {
|
|
889
|
+
const t = Math.min(1, dIn[i] / domePx);
|
|
890
|
+
const dome = 1 - (1 - t) * (1 - t);
|
|
891
|
+
acc += ci * (body + e * (params.enamelLevel + params.enamelDome * dome));
|
|
892
|
+
}
|
|
893
|
+
h[i] = acc / total;
|
|
894
|
+
}
|
|
895
|
+
const scale = size / 512;
|
|
896
|
+
h = gaussianBlur(h, size, size, params.smoothing * scale);
|
|
897
|
+
const near = fastBlur(h, size, size, Math.max(1, size * 8e-3));
|
|
898
|
+
const far = fastBlur(h, size, size, Math.max(2, size * 0.03));
|
|
899
|
+
const surface = new Float32Array(n * 4);
|
|
900
|
+
const cavity = new Float32Array(n);
|
|
901
|
+
for (let y = 0; y < size; y++) {
|
|
902
|
+
for (let x = 0; x < size; x++) {
|
|
903
|
+
const i = y * size + x;
|
|
904
|
+
const gx = (h[x < size - 1 ? i + 1 : i] - h[x > 0 ? i - 1 : i]) * 0.5;
|
|
905
|
+
const gy = (h[y < size - 1 ? i + size : i] - h[y > 0 ? i - size : i]) * 0.5;
|
|
906
|
+
surface[i * 4] = h[i];
|
|
907
|
+
surface[i * 4 + 1] = gx;
|
|
908
|
+
surface[i * 4 + 2] = gy;
|
|
909
|
+
surface[i * 4 + 3] = sd[i];
|
|
910
|
+
cavity[i] = Math.min(1, Math.max(0, near[i] - h[i]) * 2.2 + Math.max(0, far[i] - h[i]) * 1.1);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const albedo = new Uint8Array(n * 4);
|
|
914
|
+
const seeds = new Float32Array(n);
|
|
915
|
+
let seedCount = 0;
|
|
916
|
+
for (let i = 0; i < n; i++) {
|
|
917
|
+
const opaque = input.albedo[i * 4 + 3] >= 128;
|
|
918
|
+
seeds[i] = opaque ? 0 : INF;
|
|
919
|
+
if (opaque) seedCount++;
|
|
920
|
+
}
|
|
921
|
+
const nearest = seedCount > 0 ? edt2dNearest(seeds, size, size) : null;
|
|
922
|
+
for (let i = 0; i < n; i++) {
|
|
923
|
+
const src = input.albedo[i * 4 + 3] >= 128 || !nearest ? i : nearest[i];
|
|
924
|
+
if (nearest || input.albedo[i * 4 + 3] > 0) {
|
|
925
|
+
albedo[i * 4] = input.albedo[src * 4];
|
|
926
|
+
albedo[i * 4 + 1] = input.albedo[src * 4 + 1];
|
|
927
|
+
albedo[i * 4 + 2] = input.albedo[src * 4 + 2];
|
|
928
|
+
} else {
|
|
929
|
+
albedo[i * 4] = albedo[i * 4 + 1] = albedo[i * 4 + 2] = 255;
|
|
930
|
+
}
|
|
931
|
+
albedo[i * 4 + 3] = Math.round(cavity[i] * 255);
|
|
932
|
+
}
|
|
933
|
+
const weights = new Uint8Array(n * 4);
|
|
934
|
+
for (let i = 0; i < n; i++) {
|
|
935
|
+
const m = metal2[i];
|
|
936
|
+
const en = enamel[i];
|
|
937
|
+
const b = base[i] + backplate[i];
|
|
938
|
+
const a = accent[i];
|
|
939
|
+
const sum = m + en + b + a;
|
|
940
|
+
if (sum > 1e-3) {
|
|
941
|
+
const inv = 255 / sum;
|
|
942
|
+
weights[i * 4] = Math.round(m * inv);
|
|
943
|
+
weights[i * 4 + 1] = Math.round(en * inv);
|
|
944
|
+
weights[i * 4 + 2] = Math.round(b * inv);
|
|
945
|
+
weights[i * 4 + 3] = Math.round(a * inv);
|
|
946
|
+
} else if (silhouette[i] > 0) {
|
|
947
|
+
weights[i * 4 + 2] = 255;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return { size, surface, albedo, weights, heightScale, coverage: covered / n };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/bake/worker-code.generated.ts
|
|
954
|
+
var worker_code_generated_default = '"use strict";(()=>{function An(a){return{f:new Float64Array(a),z:new Float64Array(a+1),v:new Int32Array(a)}}function cn(a,n,e,t,c,u){let{f:m,z:f,v:s}=c;s[0]=0,f[0]=-1e20,f[1]=1e20,m[0]=a[n];for(let l=1,r=0;l<t;l++){m[l]=a[n+l*e];let b=l*l,x;do{let d=s[r];x=(m[l]-m[d]+b-d*d)/(l-d)/2}while(x<=f[r]&&--r>-1);r++,s[r]=l,f[r]=x,f[r+1]=1e20}for(let l=0,r=0;l<t;l++){for(;f[r+1]<l;)r++;let b=s[r],x=l-b;a[n+l*e]=m[b]+x*x,u&&(u[n+l*e]=b)}}function nn(a,n,e){let t=An(Math.max(n,e)),c=new Int32Array(n*e),u=new Int32Array(n*e);for(let f=0;f<n;f++)cn(a,f,n,e,t,c);for(let f=0;f<e;f++)cn(a,f*n,1,n,t,u);let m=new Int32Array(n*e);for(let f=0;f<e;f++){let s=f*n;for(let l=0;l<n;l++){let r=u[s+l],b=c[s+r];m[s+l]=b*n+r}}return m}function Mn(a,n,e){let t=Math.abs(a),c=Math.abs(n);if(t<c){let m=t;t=c,c=m}if(c<1e-6)return .5-e;let u=.5*c/t;return e<u?.5*(t+c)-Math.sqrt(2*t*c*e):e<1-u?(.5-e)*t:-.5*(t+c)+Math.sqrt(2*t*c*(1-e))}function tn(a,n,e,t={}){let c=t.insideOnly??!1,u=(t.refineWithin??1/0)**2,m=Math.min(u,2304),f=n*e,s=new Float32Array(f),l=new Float32Array(f),r=new Float32Array(f),b=new Float32Array(f),x=new Float32Array(f),d=0,A=(y,I)=>a[(I<0?0:I>=e?e-1:I)*n+(y<0?0:y>=n?n-1:y)];for(let y=0;y<e;y++){let I=y>0&&y<e-1;for(let i=0;i<n;i++){let F=y*n+i,B=a[F],O;if(B>0&&B<1)O=!0;else if(I&&i>0&&i<n-1){let U=a[F-1],W=a[F+1],D=a[F-n],j=a[F+n];O=B<=0?U>=1||W>=1||D>=1||j>=1:U<=0||W<=0||D<=0||j<=0}else O=B<=0?Math.max(A(i-1,y),A(i+1,y),A(i,y-1),A(i,y+1))>=1:Math.min(A(i-1,y),A(i+1,y),A(i,y-1),A(i,y+1))<=0;if(!O){s[F]=1e20;continue}s[F]=0,d++;let E=A(i+1,y-1)+2*A(i+1,y)+A(i+1,y+1)-A(i-1,y-1)-2*A(i-1,y)-A(i-1,y+1),q=A(i-1,y+1)+2*A(i,y+1)+A(i+1,y+1)-A(i-1,y-1)-2*A(i,y-1)-A(i+1,y-1),v=Math.hypot(E,q);if(v>1e-4){let U=E/v,W=q/v;b[F]=U,x[F]=W;let D=Mn(U,W,B);l[F]=i+U*D,r[F]=y+W*D}else l[F]=i,r[F]=y}}let k=new Float32Array(f);if(d===0){let y=a[0]>=.5?n+e:-(n+e);return k.fill(y),k}let P=new Uint8Array(f);for(let y=0;y<f;y++)P[y]=s[y]===0?1:0;let _=nn(s,n,e);for(let y=0;y<e;y++)for(let I=0;I<n;I++){let i=y*n+I;if(c&&a[i]<=0)continue;let F=_[i];if(s[i]>u){let R=Math.sqrt(s[i]);k[i]=a[i]>=.5?R:-R;continue}let B=s[i]<m?2:1,O=F%n,E=(F-O)/n,q=F,v=1/0;for(let R=-B;R<=B;R++){let K=E+R;if(!(K<0||K>=e))for(let Q=-B;Q<=B;Q++){let V=O+Q;if(V<0||V>=n)continue;let C=K*n+V;if(!P[C])continue;let X=I-l[C],N=y-r[C],Y=X*X+N*N;Y<v&&(v=Y,q=C)}}let U=I-l[q],W=y-r[q],D=Math.sqrt(v),j=U*b[q]+W*x[q],S=D<1.5&&(b[q]!==0||x[q]!==0)?j>=0:a[i]>=.5;k[i]=S?D:-D}return k}function on(a,n,e,t){let c=tn(a,n,e,{insideOnly:!0,refineWithin:t});for(let u=0;u<c.length;u++)c[u]=a[u]>0?Math.max(0,c[u]):0;return c}function en(a,n,e,t){if(t<=.01)return a.slice();let c=Math.max(1,Math.ceil(t*3)),u=new Float32Array(c*2+1),m=0;for(let r=-c;r<=c;r++){let b=Math.exp(-r*r/(2*t*t));u[r+c]=b,m+=b}for(let r=0;r<u.length;r++)u[r]/=m;let f=new Float32Array(n*e),s=new Float32Array(n*e),l=u.length;for(let r=0;r<e;r++){let b=r*n;for(let x=0;x<n;x++){let d=0;if(x>=c&&x<n-c){let A=b+x-c;for(let k=0;k<l;k++)d+=a[A+k]*u[k]}else for(let A=-c;A<=c;A++){let k=x+A<0?0:x+A>=n?n-1:x+A;d+=a[b+k]*u[A+c]}f[b+x]=d}}for(let r=-c;r<=c;r++){let b=u[r+c];for(let x=0;x<e;x++){let A=(x+r<0?0:x+r>=e?e-1:x+r)*n,k=x*n;for(let P=0;P<n;P++)s[k+P]+=f[A+P]*b}}return s}function Fn(a,n,e,t,c){let u=new Float32Array(e*t),m=1/(2*c+1);for(let f=0;f<t;f++){let s=f*e,l=a[s]*(c+1);for(let r=1;r<=c;r++)l+=a[s+Math.min(r,e-1)];for(let r=0;r<e;r++){u[s+r]=l*m;let b=a[s+Math.min(r+c+1,e-1)],x=a[s+Math.max(r-c,0)];l+=b-x}}for(let f=0;f<e;f++){let s=u[f]*(c+1);for(let l=1;l<=c;l++)s+=u[Math.min(l,t-1)*e+f];for(let l=0;l<t;l++){n[l*e+f]=s*m;let r=u[Math.min(l+c+1,t-1)*e+f],b=u[Math.max(l-c,0)*e+f];s+=r-b}}}function g(a,n,e,t){if(t<2)return en(a,n,e,t);let c=3,u=Math.sqrt(12*t*t/c+1),m=Math.floor(u);m%2===0&&m--;let f=m+2,s=Math.round((12*t*t-c*m*m-4*c*m-3*c)/(-4*m-4)),l=a.slice(),r=new Float32Array(n*e);for(let b=0;b<c;b++){let x=b<s?m:f;Fn(l,r,n,e,x-1>>1);let d=l;l=r,r=d}return l}function rn(a,n,e,t,c){let u=e*t,m=new Float32Array(u);for(let r=0;r<u;r++)m[r]=a[r]*n[r];let f=g(m,e,t,c),s=g(n,e,t,c),l=new Float32Array(u);for(let r=0;r<u;r++)l[r]=s[r]>1e-4?f[r]/s[r]:a[r];return l}function ln(a,n,e,t,c){t=t<0?0:t>n-1?n-1:t,c=c<0?0:c>e-1?e-1:c;let u=Math.floor(t),m=Math.floor(c),f=u+1<n?u+1:u,s=m+1<e?m+1:m,l=t-u,r=c-m,b=a[m*n+u],x=a[m*n+f],d=a[s*n+u],A=a[s*n+f];return(b+(x-b)*l)*(1-r)+(d+(A-d)*l)*r}var fn=.6;function un(a,n){if(a<=0)return 0;if(a>=1)return 1;if(n===0){let e=1-a;return Math.sqrt(1-e*e)}return n===1?Math.sin(a*Math.PI/2):a}function L(a,n,e){let t=new Float32Array(e);for(let c=0;c<e;c++)t[c]=a[c*4+n]/255;return t}function dn(a,n){let e=n*n,t=new Uint8Array(e),c=new Int32Array(e),u=0,m=s=>{!t[s]&&a[s]<.5&&(t[s]=1,c[u++]=s)};for(let s=0;s<n;s++)m(s),m((n-1)*n+s);for(let s=0;s<n;s++)m(s*n),m(s*n+n-1);for(;u>0;){let s=c[--u],l=s%n;l>0&&m(s-1),l<n-1&&m(s+1),s>=n&&m(s-n),s<e-n&&m(s+n)}let f=new Float32Array(e);for(let s=0;s<e;s++){if(t[s]){f[s]=a[s];continue}let l=s%n,r=l>0&&t[s-1]||l<n-1&&t[s+1]||s>=n&&t[s-n]||s<e-n&&t[s+n];f[s]=r?a[s]:1}return f}function In(a,n,e,t,c){let u=t*t,m=new Float32Array(u),f=t*.15,s=.75;for(let l=0;l<t;l++)for(let r=0;r<t;r++){let b=l*t+r;if(n[b]<=.01)continue;let x=a[b],d=e[b]>=n[b]*.5?f:c*1.05,A=(a[r<t-1?b+1:b]-a[r>0?b-1:b])*.5,k=(a[l<t-1?b+t:b]-a[l>0?b-t:b])*.5,P=Math.hypot(A,k);if(P<.2){m[b]=x;continue}let _=A/P*s,y=k/P*s,I=r,i=l,F=x,B=x,O=Math.ceil(d/s)+2;for(let E=0;E<O&&B<d;E++){I+=_,i+=y;let q=ln(a,t,t,I,i);if(q<=F+.001)break;F=q,B=q}m[b]=B}return m}function mn(a){let{size:n,params:e}=a,t=n*n,c=e.profile,u=e.rim*n,m=Math.max(1,e.bevel*n),f=Math.max(1,e.edge*n),s=Math.max(1,e.domeWidth*n),l=e.floor,r=L(a.roles,0,t),b=L(a.roles,1,t),x=L(a.roles,2,t),d=L(a.slotsA,0,t),A=L(a.slotsA,1,t),k=L(a.slotsA,2,t),P=L(a.slotsB,0,t),_=L(a.slotsB,1,t),y=new Float32Array(t);for(let o=0;o<t;o++)y[o]=Math.min(1,r[o]+b[o]+x[o]+d[o]);let I=e.fillHoles?dn(y,n):y,i=new Float32Array(t);for(let o=0;o<t;o++)i[o]=Math.max(0,I[o]-y[o]);let F=tn(I,n,n,{refineWithin:Math.max(f,u)*2+8});if(u>.5)for(let o=0;o<t;o++){if(I[o]<=0)continue;let p=Math.min(I[o],Math.max(0,Math.min(1,u+.5-F[o])));if(p<=0)continue;let M=1-p;r[o]=Math.max(r[o],p),b[o]*=M,x[o]*=M,d[o]*=M,i[o]*=M,P[o]=Math.max(P[o],p),_[o]*=M,k[o]*=M,A[o]*=M}let B=new Float32Array(t);for(let o=0;o<t;o++)B[o]=Math.min(1,r[o]+b[o]);let O=on(B,n,n,n*.16),E=rn(on(x,n,n,s*1.5),x,n,n,Math.max(1,s*.3)),q=rn(In(O,B,r,n,m),B,n,n,3*n/512),v=[];for(let o=0;o<t;o++)r[o]>.5&&O[o]>.75*q[o]&&q[o]>.5&&v.push(q[o]);v.sort((o,p)=>o-p);let U=v.length>8?v[v.length>>1]:0,W=U>0?fn*U/n/Math.max(.05,1-l):1/0,D=Math.max(e.depth*.2,Math.min(e.depth,W)),j=U>0?D*n*Math.max(.05,1-l)/fn:1/0,S=new Float32Array(t),R=0;for(let o=0;o<t;o++){if(I[o]<=0)continue;R+=I[o];let p=F[o]<=0?0:un(F[o]/f,0),M=l*p,H=B[o],w=x[o],Z=d[o]+i[o],T=H+w+Z;if(T<1e-4){S[o]=M;continue}let z=Z*M;if(H>0){let G=Math.min(1,r[o]/Math.max(H,1e-4)),$=Math.max(.75,q[o]),yn=Math.max(.75,G*$+(1-G)*Math.min(m,$)),bn=O[o]/yn,xn=G*Math.min(1,$/j)+(1-G);z+=H*(M+(1-M)*xn*un(bn,c))}if(w>0){let G=Math.min(1,E[o]/s),$=1-(1-G)*(1-G);z+=w*(M+p*(e.enamelLevel+e.enamelDome*$))}S[o]=z/T}let K=n/512;S=en(S,n,n,e.smoothing*K);let Q=g(S,n,n,Math.max(1,n*.008)),V=g(S,n,n,Math.max(2,n*.03)),C=new Float32Array(t*4),X=new Float32Array(t);for(let o=0;o<n;o++)for(let p=0;p<n;p++){let M=o*n+p,H=(S[p<n-1?M+1:M]-S[p>0?M-1:M])*.5,w=(S[o<n-1?M+n:M]-S[o>0?M-n:M])*.5;C[M*4]=S[M],C[M*4+1]=H,C[M*4+2]=w,C[M*4+3]=F[M],X[M]=Math.min(1,Math.max(0,Q[M]-S[M])*2.2+Math.max(0,V[M]-S[M])*1.1)}let N=new Uint8Array(t*4),Y=new Float32Array(t),sn=0;for(let o=0;o<t;o++){let p=a.albedo[o*4+3]>=128;Y[o]=p?0:1e20,p&&sn++}let h=sn>0?nn(Y,n,n):null;for(let o=0;o<t;o++){let p=a.albedo[o*4+3]>=128||!h?o:h[o];h||a.albedo[o*4+3]>0?(N[o*4]=a.albedo[p*4],N[o*4+1]=a.albedo[p*4+1],N[o*4+2]=a.albedo[p*4+2]):N[o*4]=N[o*4+1]=N[o*4+2]=255,N[o*4+3]=Math.round(X[o]*255)}let J=new Uint8Array(t*4);for(let o=0;o<t;o++){let p=P[o],M=_[o],H=k[o]+i[o],w=A[o],Z=p+M+H+w;if(Z>.001){let T=255/Z;J[o*4]=Math.round(p*T),J[o*4+1]=Math.round(M*T),J[o*4+2]=Math.round(H*T),J[o*4+3]=Math.round(w*T)}else I[o]>0&&(J[o*4+2]=255)}return{size:n,surface:C,albedo:N,weights:J,heightScale:D,coverage:R/t}}var an=self;an.onmessage=a=>{let{id:n,input:e}=a.data;try{let t=mn(e);an.postMessage({id:n,out:t},[t.surface.buffer,t.albedo.buffer,t.weights.buffer])}catch(t){an.postMessage({id:n,error:t instanceof Error?t.message:String(t)})}};})();\n';
|
|
955
|
+
|
|
956
|
+
// src/bake/client.ts
|
|
957
|
+
var MAX_WORKERS = 3;
|
|
958
|
+
var workers = [];
|
|
959
|
+
var workerUrl = null;
|
|
960
|
+
var workersUnavailable = false;
|
|
961
|
+
var nextId = 1;
|
|
962
|
+
function runOnMainThread(job) {
|
|
963
|
+
try {
|
|
964
|
+
job.resolve(bake(job.input));
|
|
965
|
+
} catch (error) {
|
|
966
|
+
job.reject(error instanceof Error ? error : new Error(String(error)));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function disablePool() {
|
|
970
|
+
workersUnavailable = true;
|
|
971
|
+
const orphaned = workers.flatMap((w) => [...w.jobs.values()]);
|
|
972
|
+
for (const w of workers) w.worker.terminate();
|
|
973
|
+
workers = [];
|
|
974
|
+
if (workerUrl) URL.revokeObjectURL(workerUrl);
|
|
975
|
+
workerUrl = null;
|
|
976
|
+
orphaned.forEach(runOnMainThread);
|
|
977
|
+
}
|
|
978
|
+
function spawn() {
|
|
979
|
+
try {
|
|
980
|
+
if (!workerUrl) workerUrl = URL.createObjectURL(new Blob([worker_code_generated_default], { type: "text/javascript" }));
|
|
981
|
+
const entry = { worker: new Worker(workerUrl), jobs: /* @__PURE__ */ new Map() };
|
|
982
|
+
entry.worker.onmessage = (event) => {
|
|
983
|
+
const { id, out, error } = event.data;
|
|
984
|
+
const job = entry.jobs.get(id);
|
|
985
|
+
if (!job) return;
|
|
986
|
+
entry.jobs.delete(id);
|
|
987
|
+
if (out) job.resolve(out);
|
|
988
|
+
else job.reject(new Error(error ?? "bake failed"));
|
|
989
|
+
};
|
|
990
|
+
entry.worker.onerror = (event) => {
|
|
991
|
+
event.preventDefault();
|
|
992
|
+
disablePool();
|
|
993
|
+
};
|
|
994
|
+
return entry;
|
|
995
|
+
} catch {
|
|
996
|
+
disablePool();
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function pickWorker() {
|
|
1001
|
+
if (workersUnavailable || typeof Worker === "undefined" || typeof Blob === "undefined") return null;
|
|
1002
|
+
const limit = Math.min(MAX_WORKERS, Math.max(1, (navigator.hardwareConcurrency || 2) - 1));
|
|
1003
|
+
const idle = workers.find((w) => w.jobs.size === 0);
|
|
1004
|
+
if (idle) return idle;
|
|
1005
|
+
if (workers.length < limit) {
|
|
1006
|
+
const fresh = spawn();
|
|
1007
|
+
if (fresh) {
|
|
1008
|
+
workers.push(fresh);
|
|
1009
|
+
return fresh;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return workers.reduce((a, w) => !a || w.jobs.size < a.jobs.size ? w : a, null);
|
|
1013
|
+
}
|
|
1014
|
+
function bakeAsync(input) {
|
|
1015
|
+
return new Promise((resolve, reject) => {
|
|
1016
|
+
const job = { input, resolve, reject };
|
|
1017
|
+
const entry = pickWorker();
|
|
1018
|
+
if (!entry) {
|
|
1019
|
+
setTimeout(() => runOnMainThread(job), 0);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const id = nextId++;
|
|
1023
|
+
entry.jobs.set(id, job);
|
|
1024
|
+
const copy = {
|
|
1025
|
+
...input,
|
|
1026
|
+
roles: input.roles.slice(),
|
|
1027
|
+
slotsA: input.slotsA.slice(),
|
|
1028
|
+
slotsB: input.slotsB.slice(),
|
|
1029
|
+
albedo: input.albedo.slice()
|
|
1030
|
+
};
|
|
1031
|
+
try {
|
|
1032
|
+
entry.worker.postMessage({ id, input: copy }, [
|
|
1033
|
+
copy.roles.buffer,
|
|
1034
|
+
copy.slotsA.buffer,
|
|
1035
|
+
copy.slotsB.buffer,
|
|
1036
|
+
copy.albedo.buffer
|
|
1037
|
+
]);
|
|
1038
|
+
} catch {
|
|
1039
|
+
disablePool();
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/svg/prepare.ts
|
|
1045
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
1046
|
+
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
1047
|
+
var SHAPES = /* @__PURE__ */ new Set(["path", "rect", "circle", "ellipse", "line", "polyline", "polygon", "text", "tspan", "textPath", "image"]);
|
|
1048
|
+
var NON_RENDERED = "defs,clipPath,mask,pattern,marker,symbol,linearGradient,radialGradient,filter,foreignObject,style,title,desc,metadata,script";
|
|
1049
|
+
function ensureNamespaces(source) {
|
|
1050
|
+
return source.replace(/<svg\b[^>]*>/i, (tag) => {
|
|
1051
|
+
let out = tag;
|
|
1052
|
+
if (!/\sxmlns\s*=/.test(out)) out = out.replace(/<svg/i, `<svg xmlns="${SVG_NS}"`);
|
|
1053
|
+
if (/xlink:/.test(source) && !/xmlns:xlink\s*=/.test(out)) out = out.replace(/<svg/i, `<svg xmlns:xlink="${XLINK_NS}"`);
|
|
1054
|
+
return out;
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
function parseSvg(source) {
|
|
1058
|
+
const doc = new DOMParser().parseFromString(ensureNamespaces(source), "image/svg+xml");
|
|
1059
|
+
const root = doc.documentElement;
|
|
1060
|
+
if (!root || root.localName !== "svg" || root.namespaceURI !== SVG_NS || doc.getElementsByTagName("parsererror").length > 0) {
|
|
1061
|
+
throw new Error("badgecraft: the input is not a valid SVG document");
|
|
1062
|
+
}
|
|
1063
|
+
for (const el of [root, ...root.querySelectorAll("*")]) {
|
|
1064
|
+
if (el.namespaceURI !== SVG_NS || ["script", "foreignObject", "animate", "animateMotion", "animateTransform", "set", "discard"].includes(el.localName)) {
|
|
1065
|
+
el.remove();
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
for (const attr of [...el.attributes]) {
|
|
1069
|
+
if (/^on/i.test(attr.localName)) el.removeAttributeNode(attr);
|
|
1070
|
+
else if (attr.localName === "href") {
|
|
1071
|
+
const value = attr.value.trim();
|
|
1072
|
+
const embeddedImage = el.localName === "image" && /^data:image\//i.test(value);
|
|
1073
|
+
if (!value.startsWith("#") && !embeddedImage) el.removeAttributeNode(attr);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return root;
|
|
1078
|
+
}
|
|
1079
|
+
function readViewBox(svg) {
|
|
1080
|
+
const vb = svg.getAttribute("viewBox");
|
|
1081
|
+
if (vb) {
|
|
1082
|
+
const p = vb.trim().split(/[\s,]+/).map(Number);
|
|
1083
|
+
if (p.length === 4 && p.every(Number.isFinite) && p[2] > 0 && p[3] > 0) return p;
|
|
1084
|
+
}
|
|
1085
|
+
const w = svg.getAttribute("width");
|
|
1086
|
+
const h = svg.getAttribute("height");
|
|
1087
|
+
if (w && h && !w.includes("%") && !h.includes("%")) {
|
|
1088
|
+
const pw = parseFloat(w);
|
|
1089
|
+
const ph = parseFloat(h);
|
|
1090
|
+
if (pw > 0 && ph > 0) return [0, 0, pw, ph];
|
|
1091
|
+
}
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
function hrefOf(el) {
|
|
1095
|
+
return el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS, "href");
|
|
1096
|
+
}
|
|
1097
|
+
function inlineUses(root) {
|
|
1098
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
1099
|
+
const uses = [...root.getElementsByTagNameNS(SVG_NS, "use")].filter((u) => !u.closest(NON_RENDERED));
|
|
1100
|
+
if (uses.length === 0) return;
|
|
1101
|
+
for (const use of uses) {
|
|
1102
|
+
const href = hrefOf(use);
|
|
1103
|
+
const target = href?.startsWith("#") ? [...root.querySelectorAll("[id]")].find((el) => el.id === href.slice(1)) : null;
|
|
1104
|
+
if (!target || target.contains(use)) {
|
|
1105
|
+
use.remove();
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const doc = use.ownerDocument;
|
|
1109
|
+
const x = use.getAttribute("x") ?? "0";
|
|
1110
|
+
const y = use.getAttribute("y") ?? "0";
|
|
1111
|
+
const group = doc.createElementNS(SVG_NS, "g");
|
|
1112
|
+
for (const attr of [...use.attributes]) {
|
|
1113
|
+
if (["href", "xlink:href", "x", "y", "width", "height", "transform"].includes(attr.name)) continue;
|
|
1114
|
+
group.setAttribute(attr.name, attr.value);
|
|
1115
|
+
}
|
|
1116
|
+
const transform = `${use.getAttribute("transform") ?? ""} translate(${x} ${y})`.trim();
|
|
1117
|
+
group.setAttribute("transform", transform);
|
|
1118
|
+
if (target.localName === "symbol") {
|
|
1119
|
+
const inner = doc.createElementNS(SVG_NS, "svg");
|
|
1120
|
+
inner.setAttribute("width", use.getAttribute("width") ?? target.getAttribute("width") ?? "100%");
|
|
1121
|
+
inner.setAttribute("height", use.getAttribute("height") ?? target.getAttribute("height") ?? "100%");
|
|
1122
|
+
for (const name of ["viewBox", "preserveAspectRatio"]) {
|
|
1123
|
+
const v = target.getAttribute(name);
|
|
1124
|
+
if (v) inner.setAttribute(name, v);
|
|
1125
|
+
}
|
|
1126
|
+
inner.setAttribute("overflow", "visible");
|
|
1127
|
+
for (const child of [...target.childNodes]) inner.appendChild(child.cloneNode(true));
|
|
1128
|
+
group.appendChild(inner);
|
|
1129
|
+
} else {
|
|
1130
|
+
const copy = target.cloneNode(true);
|
|
1131
|
+
copy.removeAttribute("id");
|
|
1132
|
+
group.appendChild(copy);
|
|
1133
|
+
}
|
|
1134
|
+
use.replaceWith(group);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function composeFrame(user, userBox, frame) {
|
|
1139
|
+
const doc = user.ownerDocument;
|
|
1140
|
+
const root = doc.createElementNS(SVG_NS, "svg");
|
|
1141
|
+
root.setAttribute("viewBox", "0 0 100 100");
|
|
1142
|
+
const d = frame.path ?? framePaths[frame.shape ?? "circle"] ?? framePaths.circle;
|
|
1143
|
+
const plate = doc.createElementNS(SVG_NS, "path");
|
|
1144
|
+
plate.setAttribute("d", d);
|
|
1145
|
+
plate.setAttribute("fill", "#000");
|
|
1146
|
+
plate.setAttribute("data-relief", "flat");
|
|
1147
|
+
plate.setAttribute("data-material", frame.material ?? "base");
|
|
1148
|
+
plate.setAttribute("data-mb-frame", "");
|
|
1149
|
+
root.appendChild(plate);
|
|
1150
|
+
if (frame.innerRing && frame.innerRing > 0) {
|
|
1151
|
+
const width = frame.innerRing * 100;
|
|
1152
|
+
const s = 0.84;
|
|
1153
|
+
const ring = doc.createElementNS(SVG_NS, "path");
|
|
1154
|
+
ring.setAttribute("d", d);
|
|
1155
|
+
ring.setAttribute("fill", "none");
|
|
1156
|
+
ring.setAttribute("stroke", "#fff");
|
|
1157
|
+
ring.setAttribute("stroke-width", String(width / s));
|
|
1158
|
+
ring.setAttribute("stroke-linejoin", "round");
|
|
1159
|
+
ring.setAttribute("transform", `translate(50 50) scale(${s}) translate(-50 -50)`);
|
|
1160
|
+
ring.setAttribute("data-material", "metal");
|
|
1161
|
+
ring.setAttribute("data-mb-frame", "");
|
|
1162
|
+
root.appendChild(ring);
|
|
1163
|
+
}
|
|
1164
|
+
const pad = Math.min(0.45, Math.max(0, frame.padding ?? 0.22)) * 100;
|
|
1165
|
+
const inner = doc.createElementNS(SVG_NS, "svg");
|
|
1166
|
+
for (const attr of [...user.attributes]) {
|
|
1167
|
+
if (/^(xmlns(:.*)?|width|height|x|y|viewBox|preserveAspectRatio|version|baseProfile)$/.test(attr.name)) continue;
|
|
1168
|
+
inner.setAttribute(attr.name, attr.value);
|
|
1169
|
+
}
|
|
1170
|
+
inner.setAttribute("x", String(pad));
|
|
1171
|
+
inner.setAttribute("y", String(pad));
|
|
1172
|
+
inner.setAttribute("width", String(100 - 2 * pad));
|
|
1173
|
+
inner.setAttribute("height", String(100 - 2 * pad));
|
|
1174
|
+
inner.setAttribute("viewBox", userBox.join(" "));
|
|
1175
|
+
inner.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1176
|
+
inner.setAttribute("overflow", "visible");
|
|
1177
|
+
while (user.firstChild) inner.appendChild(user.firstChild);
|
|
1178
|
+
root.appendChild(inner);
|
|
1179
|
+
return root;
|
|
1180
|
+
}
|
|
1181
|
+
function attrUp(el, name) {
|
|
1182
|
+
return el.closest(`[${name}]`)?.getAttribute(name) ?? null;
|
|
1183
|
+
}
|
|
1184
|
+
var defaultSlot = { wire: "metal", raised: "metal", inset: "enamel", flat: "base" };
|
|
1185
|
+
var SLOTS = /* @__PURE__ */ new Set(["metal", "enamel", "base", "accent"]);
|
|
1186
|
+
function slotFor(el, specific, role) {
|
|
1187
|
+
const v = el.getAttribute(specific) ?? attrUp(el, specific) ?? attrUp(el, "data-material");
|
|
1188
|
+
return v && SLOTS.has(v) ? v : defaultSlot[role];
|
|
1189
|
+
}
|
|
1190
|
+
function paintVisible(paint, opacity) {
|
|
1191
|
+
if (!paint || paint === "none") return false;
|
|
1192
|
+
if (/^rgba\(.*,\s*0\)$/.test(paint) || paint === "transparent") return false;
|
|
1193
|
+
return parseFloat(opacity || "1") > 4e-3;
|
|
1194
|
+
}
|
|
1195
|
+
function passColor(kind, g) {
|
|
1196
|
+
const on = (b) => b ? "ff" : "00";
|
|
1197
|
+
if (kind === "roles") return `#${on(g.role === "wire")}${on(g.role === "raised")}${on(g.role === "inset")}`;
|
|
1198
|
+
if (kind === "slotsA") return `#${on(g.role === "flat")}${on(g.slot === "accent")}${on(g.slot === "base")}`;
|
|
1199
|
+
return `#${on(g.slot === "metal")}${on(g.slot === "enamel")}00`;
|
|
1200
|
+
}
|
|
1201
|
+
function buildPass(root, infos, kind, outlinePx) {
|
|
1202
|
+
const clone = root.cloneNode(true);
|
|
1203
|
+
const doc = clone.ownerDocument;
|
|
1204
|
+
const tagged = clone.querySelectorAll("[data-mb-i]");
|
|
1205
|
+
const solidFilters = /* @__PURE__ */ new Map();
|
|
1206
|
+
const solidFilter = (color) => {
|
|
1207
|
+
let id = solidFilters.get(color);
|
|
1208
|
+
if (!id) {
|
|
1209
|
+
id = `mb-solid-${color.slice(1)}`;
|
|
1210
|
+
solidFilters.set(color, id);
|
|
1211
|
+
}
|
|
1212
|
+
return `url(#${id})`;
|
|
1213
|
+
};
|
|
1214
|
+
for (const el of tagged) {
|
|
1215
|
+
const info = infos[Number(el.getAttribute("data-mb-i"))];
|
|
1216
|
+
el.removeAttribute("data-mb-i");
|
|
1217
|
+
let css = el.getAttribute("style") ?? "";
|
|
1218
|
+
if (css && !css.trim().endsWith(";")) css += ";";
|
|
1219
|
+
if (info.hidden) {
|
|
1220
|
+
el.setAttribute("style", `${css}display:none !important;`);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (kind === "albedo") {
|
|
1224
|
+
css += "filter:none !important;mix-blend-mode:normal !important;";
|
|
1225
|
+
if (info.shape) {
|
|
1226
|
+
if (!info.fill && !info.image) css += "fill:none !important;";
|
|
1227
|
+
if (!info.stroke || info.stroke.slot === "metal") css += "stroke:none !important;";
|
|
1228
|
+
if (info.image && !info.fill) css += "display:none !important;";
|
|
1229
|
+
}
|
|
1230
|
+
el.setAttribute("style", css);
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
css += "opacity:1 !important;filter:none !important;mix-blend-mode:normal !important;";
|
|
1234
|
+
if (info.shape) {
|
|
1235
|
+
css += "fill-opacity:1 !important;stroke-opacity:1 !important;marker:none !important;";
|
|
1236
|
+
if (info.image) {
|
|
1237
|
+
css += info.fill ? `filter:${solidFilter(passColor(kind, info.fill))} !important;` : "display:none !important;";
|
|
1238
|
+
} else {
|
|
1239
|
+
css += info.fill ? `fill:${passColor(kind, info.fill)} !important;` : "fill:none !important;";
|
|
1240
|
+
if (info.stroke) css += `stroke:${passColor(kind, info.stroke)} !important;`;
|
|
1241
|
+
else if (info.outline) {
|
|
1242
|
+
css += `stroke:${passColor(kind, info.outline)} !important;stroke-width:${outlinePx}px !important;vector-effect:non-scaling-stroke !important;stroke-linejoin:round !important;stroke-linecap:round !important;stroke-dasharray:none !important;`;
|
|
1243
|
+
} else css += "stroke:none !important;";
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
el.setAttribute("style", css);
|
|
1247
|
+
}
|
|
1248
|
+
const rootCss = clone.getAttribute("style") ?? "";
|
|
1249
|
+
clone.setAttribute("style", `${rootCss}${rootCss && !rootCss.trim().endsWith(";") ? ";" : ""}background:none !important;`);
|
|
1250
|
+
if (solidFilters.size > 0) {
|
|
1251
|
+
const defs = doc.createElementNS(SVG_NS, "defs");
|
|
1252
|
+
for (const [color, id] of solidFilters) {
|
|
1253
|
+
const filter = doc.createElementNS(SVG_NS, "filter");
|
|
1254
|
+
filter.setAttribute("id", id);
|
|
1255
|
+
filter.setAttribute("color-interpolation-filters", "sRGB");
|
|
1256
|
+
const flood = doc.createElementNS(SVG_NS, "feFlood");
|
|
1257
|
+
flood.setAttribute("flood-color", color);
|
|
1258
|
+
const comp = doc.createElementNS(SVG_NS, "feComposite");
|
|
1259
|
+
comp.setAttribute("in2", "SourceAlpha");
|
|
1260
|
+
comp.setAttribute("operator", "in");
|
|
1261
|
+
filter.append(flood, comp);
|
|
1262
|
+
defs.appendChild(filter);
|
|
1263
|
+
}
|
|
1264
|
+
clone.insertBefore(defs, clone.firstChild);
|
|
1265
|
+
}
|
|
1266
|
+
return new XMLSerializer().serializeToString(clone);
|
|
1267
|
+
}
|
|
1268
|
+
function prepareSvg(source, options, size) {
|
|
1269
|
+
if (typeof document === "undefined") throw new Error("badgecraft: SVG preparation needs a browser DOM");
|
|
1270
|
+
let root = document.importNode(parseSvg(source), true);
|
|
1271
|
+
const host = document.createElement("div");
|
|
1272
|
+
host.setAttribute("aria-hidden", "true");
|
|
1273
|
+
host.style.cssText = "position:fixed;left:-10000px;top:-10000px;width:64px;height:64px;overflow:hidden;opacity:0;pointer-events:none;contain:strict;";
|
|
1274
|
+
host.style.color = options.color;
|
|
1275
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
1276
|
+
(document.body ?? document.documentElement).appendChild(host);
|
|
1277
|
+
try {
|
|
1278
|
+
inlineUses(root);
|
|
1279
|
+
shadow.appendChild(root);
|
|
1280
|
+
let box = readViewBox(root);
|
|
1281
|
+
if (!box) {
|
|
1282
|
+
const bb = root.getBBox?.();
|
|
1283
|
+
box = bb && bb.width > 0 && bb.height > 0 ? [bb.x, bb.y, bb.width, bb.height] : [0, 0, 100, 100];
|
|
1284
|
+
}
|
|
1285
|
+
if (options.frame) {
|
|
1286
|
+
const framed = composeFrame(root, box, options.frame);
|
|
1287
|
+
root.replaceWith(framed);
|
|
1288
|
+
root = framed;
|
|
1289
|
+
box = [0, 0, 100, 100];
|
|
1290
|
+
}
|
|
1291
|
+
const all = [...root.querySelectorAll("*")].filter((el) => !el.closest(NON_RENDERED));
|
|
1292
|
+
const hiddenCache = /* @__PURE__ */ new Map();
|
|
1293
|
+
const isHidden = (el) => {
|
|
1294
|
+
if (!el || el === root.parentNode) return false;
|
|
1295
|
+
const cached = hiddenCache.get(el);
|
|
1296
|
+
if (cached !== void 0) return cached;
|
|
1297
|
+
const cs = getComputedStyle(el);
|
|
1298
|
+
const hidden = cs.display === "none" || parseFloat(cs.opacity || "1") <= 4e-3 || el !== root && isHidden(el.parentElement);
|
|
1299
|
+
hiddenCache.set(el, hidden);
|
|
1300
|
+
return hidden;
|
|
1301
|
+
};
|
|
1302
|
+
const raws = all.map((el) => {
|
|
1303
|
+
const shape = SHAPES.has(el.localName);
|
|
1304
|
+
const image = el.localName === "image";
|
|
1305
|
+
const hidden = isHidden(el) || shape && getComputedStyle(el).visibility === "hidden";
|
|
1306
|
+
let hasFill = false;
|
|
1307
|
+
let hasStroke = false;
|
|
1308
|
+
let fillPaint = "";
|
|
1309
|
+
if (shape && !hidden) {
|
|
1310
|
+
const cs = getComputedStyle(el);
|
|
1311
|
+
hasFill = image || el.localName !== "line" && paintVisible(cs.fill, cs.fillOpacity);
|
|
1312
|
+
hasStroke = !image && paintVisible(cs.stroke, cs.strokeOpacity) && parseFloat(cs.strokeWidth) > 0;
|
|
1313
|
+
fillPaint = cs.fill;
|
|
1314
|
+
}
|
|
1315
|
+
return { el, hidden, shape, image, hasFill, hasStroke, fillPaint, frame: !!el.closest("[data-mb-frame]") };
|
|
1316
|
+
});
|
|
1317
|
+
const art = raws.filter((r) => r.shape && !r.hidden && !r.frame);
|
|
1318
|
+
const hasStrokes = art.some((r) => r.hasStroke && attrUp(r.el, "data-stroke-relief") !== "none");
|
|
1319
|
+
const fillColors = new Set(art.filter((r) => r.hasFill && !r.image).map((r) => r.fillPaint));
|
|
1320
|
+
const hasImages = art.some((r) => r.image);
|
|
1321
|
+
const mode = options.mode !== "auto" ? options.mode : hasStrokes || hasImages || fillColors.size > 1 || [...fillColors].some((paint) => paint.startsWith("url(")) ? "cloisonne" : "embossed";
|
|
1322
|
+
const outline = options.outline === "auto" ? mode === "cloisonne" && !hasStrokes && fillColors.size > 1 ? options.autoOutline : 0 : Math.max(0, options.outline);
|
|
1323
|
+
const infos = raws.map((r) => {
|
|
1324
|
+
const info = { hidden: r.hidden, shape: r.shape, image: r.image, fill: null, stroke: null, outline: null };
|
|
1325
|
+
if (r.hidden || !r.shape) return info;
|
|
1326
|
+
if (r.hasFill) {
|
|
1327
|
+
const relief = attrUp(r.el, "data-relief") ?? (mode === "embossed" ? "raised" : "inset");
|
|
1328
|
+
if (relief === "raised" || relief === "inset" || relief === "flat") {
|
|
1329
|
+
info.fill = { role: relief, slot: slotFor(r.el, "data-fill-material", relief) };
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (r.hasStroke) {
|
|
1333
|
+
const relief = attrUp(r.el, "data-stroke-relief") ?? "raised";
|
|
1334
|
+
const role = relief === "raised" ? "wire" : relief === "inset" || relief === "flat" ? relief : null;
|
|
1335
|
+
if (role) info.stroke = { role, slot: slotFor(r.el, "data-stroke-material", role) };
|
|
1336
|
+
} else if (outline > 0 && info.fill && info.fill.role === "inset" && !r.image && !r.frame) {
|
|
1337
|
+
info.outline = { role: "wire", slot: slotFor(r.el, "data-stroke-material", "wire") };
|
|
1338
|
+
}
|
|
1339
|
+
return info;
|
|
1340
|
+
});
|
|
1341
|
+
all.forEach((el, i) => el.setAttribute("data-mb-i", String(i)));
|
|
1342
|
+
const margin = 0.03 + outline * 0.6;
|
|
1343
|
+
const [bx, by, bw, bh] = box;
|
|
1344
|
+
const side = Math.max(bw, bh) * (1 + 2 * margin);
|
|
1345
|
+
root.setAttribute("viewBox", `${bx + bw / 2 - side / 2} ${by + bh / 2 - side / 2} ${side} ${side}`);
|
|
1346
|
+
root.setAttribute("width", String(size));
|
|
1347
|
+
root.setAttribute("height", String(size));
|
|
1348
|
+
root.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1349
|
+
root.removeAttribute("x");
|
|
1350
|
+
root.removeAttribute("y");
|
|
1351
|
+
if (!root.getAttribute("color")) root.setAttribute("color", options.color);
|
|
1352
|
+
const outlinePx = outline * size;
|
|
1353
|
+
return {
|
|
1354
|
+
roles: buildPass(root, infos, "roles", outlinePx),
|
|
1355
|
+
slotsA: buildPass(root, infos, "slotsA", outlinePx),
|
|
1356
|
+
slotsB: buildPass(root, infos, "slotsB", outlinePx),
|
|
1357
|
+
albedo: buildPass(root, infos, "albedo", outlinePx),
|
|
1358
|
+
mode,
|
|
1359
|
+
outline
|
|
1360
|
+
};
|
|
1361
|
+
} finally {
|
|
1362
|
+
host.remove();
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/svg/rasterize.ts
|
|
1367
|
+
function loadImage(url) {
|
|
1368
|
+
return new Promise((resolve, reject) => {
|
|
1369
|
+
const img = new Image();
|
|
1370
|
+
img.decoding = "async";
|
|
1371
|
+
img.onload = () => resolve(img);
|
|
1372
|
+
img.onerror = () => reject(new Error("badgecraft: the browser could not render the SVG"));
|
|
1373
|
+
img.src = url;
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
async function rasterizeSvg(svg, size, background) {
|
|
1377
|
+
const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
|
|
1378
|
+
try {
|
|
1379
|
+
const img = await loadImage(url);
|
|
1380
|
+
const canvas = document.createElement("canvas");
|
|
1381
|
+
canvas.width = size;
|
|
1382
|
+
canvas.height = size;
|
|
1383
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
1384
|
+
if (!ctx) throw new Error("badgecraft: 2D canvas unavailable");
|
|
1385
|
+
if (background) {
|
|
1386
|
+
ctx.fillStyle = background;
|
|
1387
|
+
ctx.fillRect(0, 0, size, size);
|
|
1388
|
+
}
|
|
1389
|
+
ctx.drawImage(img, 0, 0, size, size);
|
|
1390
|
+
return ctx.getImageData(0, 0, size, size).data;
|
|
1391
|
+
} finally {
|
|
1392
|
+
URL.revokeObjectURL(url);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/pipeline.ts
|
|
1397
|
+
function mapsKey(source, bakeKey, size) {
|
|
1398
|
+
return `${hashString(source)}:${hashString(bakeKey)}:${size}`;
|
|
1399
|
+
}
|
|
1400
|
+
async function bakeSvg(source, settings, size) {
|
|
1401
|
+
const prepared = prepareSvg(
|
|
1402
|
+
source,
|
|
1403
|
+
{
|
|
1404
|
+
mode: settings.mode,
|
|
1405
|
+
color: settings.color,
|
|
1406
|
+
frame: settings.frame,
|
|
1407
|
+
outline: settings.outline,
|
|
1408
|
+
autoOutline: settings.autoOutline
|
|
1409
|
+
},
|
|
1410
|
+
size
|
|
1411
|
+
);
|
|
1412
|
+
const [roles, slotsA, slotsB, albedo] = await Promise.all([
|
|
1413
|
+
rasterizeSvg(prepared.roles, size, "#000"),
|
|
1414
|
+
rasterizeSvg(prepared.slotsA, size, "#000"),
|
|
1415
|
+
rasterizeSvg(prepared.slotsB, size, "#000"),
|
|
1416
|
+
rasterizeSvg(prepared.albedo, size)
|
|
1417
|
+
]);
|
|
1418
|
+
return bakeAsync({
|
|
1419
|
+
size,
|
|
1420
|
+
roles,
|
|
1421
|
+
slotsA,
|
|
1422
|
+
slotsB,
|
|
1423
|
+
albedo,
|
|
1424
|
+
params: {
|
|
1425
|
+
depth: settings.depth,
|
|
1426
|
+
rim: settings.rim,
|
|
1427
|
+
fillHoles: settings.fillHoles,
|
|
1428
|
+
floor: settings.floor,
|
|
1429
|
+
profile: settings.profile,
|
|
1430
|
+
bevel: settings.bevel,
|
|
1431
|
+
edge: settings.edge,
|
|
1432
|
+
enamelLevel: settings.enamelLevel,
|
|
1433
|
+
enamelDome: settings.enamelDome,
|
|
1434
|
+
domeWidth: settings.domeWidth,
|
|
1435
|
+
smoothing: settings.smoothing
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
var cpuCache = /* @__PURE__ */ new Map();
|
|
1440
|
+
var cpuBytes = /* @__PURE__ */ new Map();
|
|
1441
|
+
var CPU_CACHE_ENTRIES = 8;
|
|
1442
|
+
var CPU_CACHE_BYTES = 48 * 1024 * 1024;
|
|
1443
|
+
function cachedBake(key, produce) {
|
|
1444
|
+
let pending = cpuCache.get(key);
|
|
1445
|
+
if (pending) {
|
|
1446
|
+
cpuCache.delete(key);
|
|
1447
|
+
cpuCache.set(key, pending);
|
|
1448
|
+
return pending;
|
|
1449
|
+
}
|
|
1450
|
+
const job = produce();
|
|
1451
|
+
job.then(
|
|
1452
|
+
(out) => {
|
|
1453
|
+
if (cpuCache.get(key) !== job) return;
|
|
1454
|
+
cpuBytes.set(key, out.surface.byteLength + out.albedo.byteLength + out.weights.byteLength);
|
|
1455
|
+
trimCpuCache();
|
|
1456
|
+
},
|
|
1457
|
+
() => cpuCache.get(key) === job && cpuCache.delete(key)
|
|
1458
|
+
);
|
|
1459
|
+
pending = job;
|
|
1460
|
+
cpuCache.set(key, pending);
|
|
1461
|
+
trimCpuCache();
|
|
1462
|
+
return pending;
|
|
1463
|
+
}
|
|
1464
|
+
function trimCpuCache() {
|
|
1465
|
+
let total = 0;
|
|
1466
|
+
for (const bytes of cpuBytes.values()) total += bytes;
|
|
1467
|
+
while (cpuCache.size > 1 && (cpuCache.size > CPU_CACHE_ENTRIES || total > CPU_CACHE_BYTES)) {
|
|
1468
|
+
const oldest = cpuCache.keys().next().value;
|
|
1469
|
+
total -= cpuBytes.get(oldest) ?? 0;
|
|
1470
|
+
cpuCache.delete(oldest);
|
|
1471
|
+
cpuBytes.delete(oldest);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
var gpuCache = /* @__PURE__ */ new Map();
|
|
1475
|
+
async function acquireMaps(renderer, key, produce) {
|
|
1476
|
+
let entry = gpuCache.get(key);
|
|
1477
|
+
if (entry?.textures && entry.generation !== renderer.generation) entry = void 0;
|
|
1478
|
+
if (!entry) {
|
|
1479
|
+
const created = {
|
|
1480
|
+
refs: 0,
|
|
1481
|
+
generation: renderer.generation,
|
|
1482
|
+
promise: cachedBake(key, produce).then((out) => {
|
|
1483
|
+
created.textures = renderer.createBadgeTextures(out);
|
|
1484
|
+
created.generation = renderer.generation;
|
|
1485
|
+
return created.textures;
|
|
1486
|
+
})
|
|
1487
|
+
};
|
|
1488
|
+
created.promise.catch(() => gpuCache.get(key) === created && gpuCache.delete(key));
|
|
1489
|
+
gpuCache.set(key, created);
|
|
1490
|
+
entry = created;
|
|
1491
|
+
}
|
|
1492
|
+
entry.refs++;
|
|
1493
|
+
if (entry.disposeTimer) {
|
|
1494
|
+
clearTimeout(entry.disposeTimer);
|
|
1495
|
+
entry.disposeTimer = void 0;
|
|
1496
|
+
}
|
|
1497
|
+
try {
|
|
1498
|
+
return { key, textures: await entry.promise, entry };
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
entry.refs--;
|
|
1501
|
+
throw error;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
function releaseMaps(renderer, handle) {
|
|
1505
|
+
const entry = handle.entry;
|
|
1506
|
+
entry.refs--;
|
|
1507
|
+
if (entry.refs > 0) return;
|
|
1508
|
+
entry.disposeTimer = setTimeout(() => {
|
|
1509
|
+
if (entry.refs > 0) return;
|
|
1510
|
+
if (gpuCache.get(handle.key) === entry) gpuCache.delete(handle.key);
|
|
1511
|
+
if (entry.textures && entry.generation === renderer.generation) renderer.deleteBadgeTextures(entry.textures);
|
|
1512
|
+
}, 4e3);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// src/render/hdr.ts
|
|
1516
|
+
function decodeHdr(buffer) {
|
|
1517
|
+
const bytes = new Uint8Array(buffer);
|
|
1518
|
+
let pos = 0;
|
|
1519
|
+
const readLine = () => {
|
|
1520
|
+
let s = "";
|
|
1521
|
+
while (pos < bytes.length && bytes[pos] !== 10) s += String.fromCharCode(bytes[pos++]);
|
|
1522
|
+
if (pos >= bytes.length) throw new Error("badgecraft: truncated HDR header");
|
|
1523
|
+
pos++;
|
|
1524
|
+
return s.replace(/\r$/, "");
|
|
1525
|
+
};
|
|
1526
|
+
const magic = readLine();
|
|
1527
|
+
if (!magic.startsWith("#?")) throw new Error("badgecraft: not a Radiance HDR file");
|
|
1528
|
+
let line = readLine();
|
|
1529
|
+
while (line.length > 0) line = readLine();
|
|
1530
|
+
const size = readLine().match(/-Y\s+(\d+)\s+\+X\s+(\d+)/);
|
|
1531
|
+
if (!size) throw new Error("badgecraft: unsupported HDR orientation");
|
|
1532
|
+
const height = Number(size[1]);
|
|
1533
|
+
const width = Number(size[2]);
|
|
1534
|
+
if (width < 1 || height < 1 || width * height > 16777216) {
|
|
1535
|
+
throw new Error("badgecraft: invalid or oversized HDR dimensions");
|
|
1536
|
+
}
|
|
1537
|
+
const requireBytes = (count) => {
|
|
1538
|
+
if (pos + count > bytes.length) throw new Error("badgecraft: truncated HDR pixels");
|
|
1539
|
+
};
|
|
1540
|
+
const rgbe = new Uint8Array(width * height * 4);
|
|
1541
|
+
const scan = new Uint8Array(width * 4);
|
|
1542
|
+
for (let y = 0; y < height; y++) {
|
|
1543
|
+
requireBytes(4);
|
|
1544
|
+
const rle = width >= 8 && width < 32768 && bytes[pos] === 2 && bytes[pos + 1] === 2 && !(bytes[pos + 2] & 128);
|
|
1545
|
+
if (!rle) {
|
|
1546
|
+
requireBytes(width * 4);
|
|
1547
|
+
rgbe.set(bytes.subarray(pos, pos + width * 4), y * width * 4);
|
|
1548
|
+
pos += width * 4;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if ((bytes[pos + 2] << 8 | bytes[pos + 3]) !== width) {
|
|
1552
|
+
throw new Error("badgecraft: HDR scanline width mismatch");
|
|
1553
|
+
}
|
|
1554
|
+
pos += 4;
|
|
1555
|
+
for (let c = 0; c < 4; c++) {
|
|
1556
|
+
let x = 0;
|
|
1557
|
+
while (x < width) {
|
|
1558
|
+
requireBytes(1);
|
|
1559
|
+
let count = bytes[pos++];
|
|
1560
|
+
const length = count > 128 ? count - 128 : count;
|
|
1561
|
+
if (length === 0 || x + length > width) throw new Error("badgecraft: invalid HDR run length");
|
|
1562
|
+
requireBytes(count > 128 ? 1 : length);
|
|
1563
|
+
if (count > 128) {
|
|
1564
|
+
count -= 128;
|
|
1565
|
+
const v = bytes[pos++];
|
|
1566
|
+
for (let k = 0; k < count; k++) scan[x++ * 4 + c] = v;
|
|
1567
|
+
} else {
|
|
1568
|
+
for (let k = 0; k < count; k++) scan[x++ * 4 + c] = bytes[pos++];
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
rgbe.set(scan, y * width * 4);
|
|
1573
|
+
}
|
|
1574
|
+
const data = new Float32Array(width * height * 4);
|
|
1575
|
+
for (let i = 0; i < width * height; i++) {
|
|
1576
|
+
const e = rgbe[i * 4 + 3];
|
|
1577
|
+
const f2 = e ? Math.pow(2, e - 136) : 0;
|
|
1578
|
+
data[i * 4] = rgbe[i * 4] * f2;
|
|
1579
|
+
data[i * 4 + 1] = rgbe[i * 4 + 1] * f2;
|
|
1580
|
+
data[i * 4 + 2] = rgbe[i * 4 + 2] * f2;
|
|
1581
|
+
data[i * 4 + 3] = 1;
|
|
1582
|
+
}
|
|
1583
|
+
return { width, height, data };
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/render/mesh.ts
|
|
1587
|
+
function reliefMesh(surface, size) {
|
|
1588
|
+
const steps = Math.min(size, 384);
|
|
1589
|
+
const vertices = [];
|
|
1590
|
+
const indices = [];
|
|
1591
|
+
const distances = [];
|
|
1592
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1593
|
+
const boundary = [];
|
|
1594
|
+
const sample = (u, v) => {
|
|
1595
|
+
const x = Math.max(0, Math.min(size - 1, u * size - 0.5));
|
|
1596
|
+
const y = Math.max(0, Math.min(size - 1, v * size - 0.5));
|
|
1597
|
+
const ix = Math.floor(x), iy = Math.floor(y);
|
|
1598
|
+
const jx = Math.min(ix + 1, size - 1), jy = Math.min(iy + 1, size - 1);
|
|
1599
|
+
const a = surface[(iy * size + ix) * 4 + 3] * (1 - x + ix) + surface[(iy * size + jx) * 4 + 3] * (x - ix);
|
|
1600
|
+
const b = surface[(jy * size + ix) * 4 + 3] * (1 - x + ix) + surface[(jy * size + jx) * 4 + 3] * (x - ix);
|
|
1601
|
+
return a * (1 - y + iy) + b * (y - iy);
|
|
1602
|
+
};
|
|
1603
|
+
for (let y = 0; y <= steps; y++) for (let x = 0; x <= steps; x++) {
|
|
1604
|
+
vertices.push(x / steps, y / steps, 0);
|
|
1605
|
+
distances.push(sample(x / steps, y / steps));
|
|
1606
|
+
}
|
|
1607
|
+
const crossing = (a, b) => {
|
|
1608
|
+
if (distances[a] === 0) return a;
|
|
1609
|
+
if (distances[b] === 0) return b;
|
|
1610
|
+
const key = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
1611
|
+
const found = edges.get(key);
|
|
1612
|
+
if (found !== void 0) return found;
|
|
1613
|
+
let lo = 0, hi = 1;
|
|
1614
|
+
for (let i = 0; i < 12; i++) {
|
|
1615
|
+
const t2 = (lo + hi) / 2;
|
|
1616
|
+
const d = sample(vertices[a * 3] * (1 - t2) + vertices[b * 3] * t2, vertices[a * 3 + 1] * (1 - t2) + vertices[b * 3 + 1] * t2);
|
|
1617
|
+
if (d >= 0 === distances[a] >= 0) lo = t2;
|
|
1618
|
+
else hi = t2;
|
|
1619
|
+
}
|
|
1620
|
+
const t = (lo + hi) / 2;
|
|
1621
|
+
const id = vertices.length / 3;
|
|
1622
|
+
vertices.push(vertices[a * 3] * (1 - t) + vertices[b * 3] * t, vertices[a * 3 + 1] * (1 - t) + vertices[b * 3 + 1] * t, 0);
|
|
1623
|
+
distances.push(0);
|
|
1624
|
+
edges.set(key, id);
|
|
1625
|
+
return id;
|
|
1626
|
+
};
|
|
1627
|
+
const triangle = (a, b, c) => {
|
|
1628
|
+
const input = [a, b, c], poly = [];
|
|
1629
|
+
for (let i = 0; i < 3; i++) {
|
|
1630
|
+
const p = input[i], q = input[(i + 1) % 3];
|
|
1631
|
+
if (distances[p] >= 0) poly.push(p);
|
|
1632
|
+
if (distances[p] >= 0 !== distances[q] >= 0) poly.push(crossing(p, q));
|
|
1633
|
+
}
|
|
1634
|
+
for (let i = poly.length - 1; i >= 0; i--) {
|
|
1635
|
+
if (poly[i] === poly[(i + 1) % poly.length]) poly.splice(i, 1);
|
|
1636
|
+
}
|
|
1637
|
+
if (poly.length < 3) return;
|
|
1638
|
+
for (let i = 1; i + 1 < poly.length; i++) indices.push(poly[0], poly[i], poly[i + 1]);
|
|
1639
|
+
for (let i = 0; i < poly.length; i++) {
|
|
1640
|
+
const p = poly[i], q = poly[(i + 1) % poly.length];
|
|
1641
|
+
const onMapEdge = [0, 1].some((axis) => vertices[p * 3 + axis] === vertices[q * 3 + axis] && (vertices[p * 3 + axis] === 0 || vertices[p * 3 + axis] === 1));
|
|
1642
|
+
if (distances[p] === 0 && distances[q] === 0 || onMapEdge) boundary.push([p, q]);
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
for (let y = 0; y < steps; y++) for (let x = 0; x < steps; x++) {
|
|
1646
|
+
const a = y * (steps + 1) + x, b = a + 1, c = a + steps + 1, d = c + 1;
|
|
1647
|
+
const da = distances[a], db = distances[b], dc = distances[c], dd = distances[d];
|
|
1648
|
+
if (da < 0 && db < 0 && dc < 0 && dd < 0) continue;
|
|
1649
|
+
if (da > 0 && db > 0 && dc > 0 && dd > 0 && x > 0 && y > 0 && x < steps - 1 && y < steps - 1) {
|
|
1650
|
+
indices.push(a, c, b, b, c, d);
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
triangle(a, c, b);
|
|
1654
|
+
triangle(b, c, d);
|
|
1655
|
+
}
|
|
1656
|
+
const faceCount = indices.length;
|
|
1657
|
+
for (const [a, b] of boundary) {
|
|
1658
|
+
const i = vertices.length / 3;
|
|
1659
|
+
for (const [id, side] of [[a, 1], [a, -1], [b, 1], [b, -1]]) vertices.push(vertices[id * 3], vertices[id * 3 + 1], side);
|
|
1660
|
+
indices.push(i, i + 1, i + 2, i + 2, i + 1, i + 3);
|
|
1661
|
+
}
|
|
1662
|
+
return { vertices: new Float32Array(vertices), indices: new Uint32Array(indices), faceCount };
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/render/math.ts
|
|
1666
|
+
function perspective(fovY, aspect, near, far) {
|
|
1667
|
+
const f2 = 1 / Math.tan(fovY / 2);
|
|
1668
|
+
const nf = 1 / (near - far);
|
|
1669
|
+
const m = new Float32Array(16);
|
|
1670
|
+
m[0] = f2 / aspect;
|
|
1671
|
+
m[5] = f2;
|
|
1672
|
+
m[10] = (far + near) * nf;
|
|
1673
|
+
m[11] = -1;
|
|
1674
|
+
m[14] = 2 * far * near * nf;
|
|
1675
|
+
return m;
|
|
1676
|
+
}
|
|
1677
|
+
function multiply(a, b) {
|
|
1678
|
+
const out = new Float32Array(16);
|
|
1679
|
+
for (let c = 0; c < 4; c++) {
|
|
1680
|
+
for (let r = 0; r < 4; r++) {
|
|
1681
|
+
out[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return out;
|
|
1685
|
+
}
|
|
1686
|
+
function translation(x, y, z) {
|
|
1687
|
+
const m = identity();
|
|
1688
|
+
m[12] = x;
|
|
1689
|
+
m[13] = y;
|
|
1690
|
+
m[14] = z;
|
|
1691
|
+
return m;
|
|
1692
|
+
}
|
|
1693
|
+
function identity() {
|
|
1694
|
+
const m = new Float32Array(16);
|
|
1695
|
+
m[0] = m[5] = m[10] = m[15] = 1;
|
|
1696
|
+
return m;
|
|
1697
|
+
}
|
|
1698
|
+
function rotation(pitch, yaw, roll = 0) {
|
|
1699
|
+
const cx = Math.cos(pitch);
|
|
1700
|
+
const sx = Math.sin(pitch);
|
|
1701
|
+
const cy = Math.cos(yaw);
|
|
1702
|
+
const sy = Math.sin(yaw);
|
|
1703
|
+
const rx = identity();
|
|
1704
|
+
rx[5] = cx;
|
|
1705
|
+
rx[6] = sx;
|
|
1706
|
+
rx[9] = -sx;
|
|
1707
|
+
rx[10] = cx;
|
|
1708
|
+
const ry = identity();
|
|
1709
|
+
ry[0] = cy;
|
|
1710
|
+
ry[2] = -sy;
|
|
1711
|
+
ry[8] = sy;
|
|
1712
|
+
ry[10] = cy;
|
|
1713
|
+
let m = multiply(ry, rx);
|
|
1714
|
+
if (roll) {
|
|
1715
|
+
const cz = Math.cos(roll);
|
|
1716
|
+
const sz = Math.sin(roll);
|
|
1717
|
+
const rz = identity();
|
|
1718
|
+
rz[0] = cz;
|
|
1719
|
+
rz[1] = sz;
|
|
1720
|
+
rz[4] = -sz;
|
|
1721
|
+
rz[5] = cz;
|
|
1722
|
+
m = multiply(m, rz);
|
|
1723
|
+
}
|
|
1724
|
+
return m;
|
|
1725
|
+
}
|
|
1726
|
+
function normalMatrix(m) {
|
|
1727
|
+
return new Float32Array([m[0], m[1], m[2], m[4], m[5], m[6], m[8], m[9], m[10]]);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/render/shaders.ts
|
|
1731
|
+
var common = (
|
|
1732
|
+
/* glsl */
|
|
1733
|
+
`
|
|
1734
|
+
const float PI = 3.14159265359;
|
|
1735
|
+
|
|
1736
|
+
// Equirectangular mapping. u = 0.5 looks along +z (towards the viewer), v = 0 is straight up.
|
|
1737
|
+
vec2 dirToUv(vec3 d, float rotation) {
|
|
1738
|
+
float u = atan(d.x, d.z) * (0.5 / PI) + 0.5 + rotation;
|
|
1739
|
+
float v = acos(clamp(d.y, -1.0, 1.0)) / PI;
|
|
1740
|
+
return vec2(u, v);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
vec3 uvToDir(vec2 uv) {
|
|
1744
|
+
float phi = (uv.x - 0.5) * 2.0 * PI;
|
|
1745
|
+
float theta = uv.y * PI;
|
|
1746
|
+
return vec3(sin(theta) * sin(phi), cos(theta), sin(theta) * cos(phi));
|
|
1747
|
+
}
|
|
1748
|
+
`
|
|
1749
|
+
);
|
|
1750
|
+
var fullscreenVertex = (
|
|
1751
|
+
/* glsl */
|
|
1752
|
+
`#version 300 es
|
|
1753
|
+
void main() {
|
|
1754
|
+
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
1755
|
+
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
|
1756
|
+
}
|
|
1757
|
+
`
|
|
1758
|
+
);
|
|
1759
|
+
var MAX_LIGHTS = 8;
|
|
1760
|
+
var envGenerateFragment = (
|
|
1761
|
+
/* glsl */
|
|
1762
|
+
`#version 300 es
|
|
1763
|
+
precision highp float;
|
|
1764
|
+
${common}
|
|
1765
|
+
uniform vec2 uSize;
|
|
1766
|
+
uniform vec3 uTop;
|
|
1767
|
+
uniform vec3 uHorizon;
|
|
1768
|
+
uniform vec3 uBottom;
|
|
1769
|
+
uniform int uLightCount;
|
|
1770
|
+
uniform vec3 uLightDir[${MAX_LIGHTS}];
|
|
1771
|
+
uniform vec3 uLightColor[${MAX_LIGHTS}];
|
|
1772
|
+
uniform vec4 uLightShape[${MAX_LIGHTS}]; // tan(half width), tan(half height), softness, circle?
|
|
1773
|
+
uniform float uScale;
|
|
1774
|
+
out vec4 outColor;
|
|
1775
|
+
|
|
1776
|
+
void main() {
|
|
1777
|
+
vec2 uv = gl_FragCoord.xy / uSize;
|
|
1778
|
+
vec3 d = uvToDir(uv);
|
|
1779
|
+
vec3 col = d.y > 0.0
|
|
1780
|
+
? mix(uHorizon, uTop, pow(d.y, 0.55))
|
|
1781
|
+
: mix(uHorizon, uBottom, pow(-d.y, 0.4));
|
|
1782
|
+
for (int i = 0; i < ${MAX_LIGHTS}; i++) {
|
|
1783
|
+
if (i >= uLightCount) break;
|
|
1784
|
+
vec3 c = uLightDir[i];
|
|
1785
|
+
float cd = dot(d, c);
|
|
1786
|
+
if (cd <= 0.02) continue;
|
|
1787
|
+
vec3 up = abs(c.y) > 0.99 ? vec3(0.0, 0.0, 1.0) : vec3(0.0, 1.0, 0.0);
|
|
1788
|
+
vec3 right = normalize(cross(up, c));
|
|
1789
|
+
vec3 upv = cross(c, right);
|
|
1790
|
+
vec2 p = vec2(dot(d, right), dot(d, upv)) / cd;
|
|
1791
|
+
vec4 s = uLightShape[i];
|
|
1792
|
+
float soft = clamp(s.z, 0.02, 1.0);
|
|
1793
|
+
float e;
|
|
1794
|
+
if (s.w > 0.5) {
|
|
1795
|
+
e = length(p) / s.x;
|
|
1796
|
+
} else {
|
|
1797
|
+
vec2 q = abs(p) / s.xy;
|
|
1798
|
+
e = pow(pow(q.x, 8.0) + pow(q.y, 8.0), 0.125);
|
|
1799
|
+
}
|
|
1800
|
+
float m = 1.0 - smoothstep(1.0 - soft, 1.0, e);
|
|
1801
|
+
col += uLightColor[i] * m;
|
|
1802
|
+
}
|
|
1803
|
+
outColor = vec4(col * uScale, 1.0);
|
|
1804
|
+
}
|
|
1805
|
+
`
|
|
1806
|
+
);
|
|
1807
|
+
var sampling = (
|
|
1808
|
+
/* glsl */
|
|
1809
|
+
`
|
|
1810
|
+
vec2 hammersley(int i, int n) {
|
|
1811
|
+
uint b = uint(i);
|
|
1812
|
+
b = (b << 16u) | (b >> 16u);
|
|
1813
|
+
b = ((b & 0x55555555u) << 1u) | ((b & 0xAAAAAAAAu) >> 1u);
|
|
1814
|
+
b = ((b & 0x33333333u) << 2u) | ((b & 0xCCCCCCCCu) >> 2u);
|
|
1815
|
+
b = ((b & 0x0F0F0F0Fu) << 4u) | ((b & 0xF0F0F0F0u) >> 4u);
|
|
1816
|
+
b = ((b & 0x00FF00FFu) << 8u) | ((b & 0xFF00FF00u) >> 8u);
|
|
1817
|
+
return vec2(float(i) / float(n), float(b) * 2.3283064365386963e-10);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
mat3 tangentFrame(vec3 n) {
|
|
1821
|
+
vec3 up = abs(n.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
|
1822
|
+
vec3 t = normalize(cross(up, n));
|
|
1823
|
+
return mat3(t, cross(n, t), n);
|
|
1824
|
+
}
|
|
1825
|
+
`
|
|
1826
|
+
);
|
|
1827
|
+
var prefilterFragment = (
|
|
1828
|
+
/* glsl */
|
|
1829
|
+
`#version 300 es
|
|
1830
|
+
precision highp float;
|
|
1831
|
+
${common}
|
|
1832
|
+
${sampling}
|
|
1833
|
+
uniform sampler2D uSrc;
|
|
1834
|
+
uniform vec2 uSize;
|
|
1835
|
+
uniform float uRoughness;
|
|
1836
|
+
uniform float uTexelSolidAngle;
|
|
1837
|
+
out vec4 outColor;
|
|
1838
|
+
const int SAMPLES = 256;
|
|
1839
|
+
|
|
1840
|
+
void main() {
|
|
1841
|
+
vec2 uv = gl_FragCoord.xy / uSize;
|
|
1842
|
+
// The mirror level is a resample, not a convolution. Preserve crisp studio
|
|
1843
|
+
// cards and skip 256 identical GGX samples over the largest mip level.
|
|
1844
|
+
if (uRoughness <= 0.001) {
|
|
1845
|
+
outColor = vec4(textureLod(uSrc, uv, 0.0).rgb, 1.0);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
vec3 N = uvToDir(uv);
|
|
1849
|
+
float a = max(uRoughness * uRoughness, 1e-4);
|
|
1850
|
+
float a2 = a * a;
|
|
1851
|
+
mat3 tbn = tangentFrame(N);
|
|
1852
|
+
vec3 sum = vec3(0.0);
|
|
1853
|
+
float wsum = 0.0;
|
|
1854
|
+
for (int i = 0; i < SAMPLES; i++) {
|
|
1855
|
+
vec2 xi = hammersley(i, SAMPLES);
|
|
1856
|
+
float phi = 2.0 * PI * xi.x;
|
|
1857
|
+
float cosT = sqrt((1.0 - xi.y) / (1.0 + (a2 - 1.0) * xi.y));
|
|
1858
|
+
float sinT = sqrt(1.0 - cosT * cosT);
|
|
1859
|
+
vec3 H = tbn * vec3(cos(phi) * sinT, sin(phi) * sinT, cosT);
|
|
1860
|
+
float NdH = dot(N, H);
|
|
1861
|
+
vec3 L = 2.0 * NdH * H - N;
|
|
1862
|
+
float NdL = dot(N, L);
|
|
1863
|
+
if (NdL > 0.0) {
|
|
1864
|
+
float d = NdH * NdH * (a2 - 1.0) + 1.0;
|
|
1865
|
+
float D = a2 / (PI * d * d);
|
|
1866
|
+
float pdf = D * 0.25;
|
|
1867
|
+
float saSample = 1.0 / (float(SAMPLES) * pdf + 1e-5);
|
|
1868
|
+
float lod = max(0.5 * log2(saSample / uTexelSolidAngle) + 1.0, 0.0);
|
|
1869
|
+
sum += textureLod(uSrc, dirToUv(L, 0.0), lod).rgb * NdL;
|
|
1870
|
+
wsum += NdL;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
outColor = vec4(sum / max(wsum, 1e-4), 1.0);
|
|
1874
|
+
}
|
|
1875
|
+
`
|
|
1876
|
+
);
|
|
1877
|
+
var irradianceFragment = (
|
|
1878
|
+
/* glsl */
|
|
1879
|
+
`#version 300 es
|
|
1880
|
+
precision highp float;
|
|
1881
|
+
${common}
|
|
1882
|
+
${sampling}
|
|
1883
|
+
uniform sampler2D uSrc;
|
|
1884
|
+
uniform vec2 uSize;
|
|
1885
|
+
uniform float uTexelSolidAngle;
|
|
1886
|
+
out vec4 outColor;
|
|
1887
|
+
const int SAMPLES = 512;
|
|
1888
|
+
|
|
1889
|
+
void main() {
|
|
1890
|
+
vec2 uv = gl_FragCoord.xy / uSize;
|
|
1891
|
+
vec3 N = uvToDir(uv);
|
|
1892
|
+
mat3 tbn = tangentFrame(N);
|
|
1893
|
+
vec3 sum = vec3(0.0);
|
|
1894
|
+
for (int i = 0; i < SAMPLES; i++) {
|
|
1895
|
+
vec2 xi = hammersley(i, SAMPLES);
|
|
1896
|
+
float phi = 2.0 * PI * xi.x;
|
|
1897
|
+
float cosT = sqrt(1.0 - xi.y);
|
|
1898
|
+
float sinT = sqrt(xi.y);
|
|
1899
|
+
vec3 L = tbn * vec3(cos(phi) * sinT, sin(phi) * sinT, cosT);
|
|
1900
|
+
float pdf = cosT / PI;
|
|
1901
|
+
float saSample = 1.0 / (float(SAMPLES) * pdf + 1e-5);
|
|
1902
|
+
float lod = max(0.5 * log2(saSample / uTexelSolidAngle) + 1.0, 0.0);
|
|
1903
|
+
sum += textureLod(uSrc, dirToUv(L, 0.0), lod).rgb;
|
|
1904
|
+
}
|
|
1905
|
+
outColor = vec4(sum / float(SAMPLES), 1.0);
|
|
1906
|
+
}
|
|
1907
|
+
`
|
|
1908
|
+
);
|
|
1909
|
+
var badgeVertex = (
|
|
1910
|
+
/* glsl */
|
|
1911
|
+
`#version 300 es
|
|
1912
|
+
precision highp float;
|
|
1913
|
+
layout(location = 0) in vec2 aUv;
|
|
1914
|
+
layout(location = 1) in float aLip;
|
|
1915
|
+
uniform sampler2D uSurface;
|
|
1916
|
+
uniform mat4 uModel;
|
|
1917
|
+
uniform mat4 uViewProj;
|
|
1918
|
+
uniform float uDepth;
|
|
1919
|
+
uniform float uThickness;
|
|
1920
|
+
uniform float uEdgeTexels;
|
|
1921
|
+
uniform float uSide;
|
|
1922
|
+
out vec2 vUv;
|
|
1923
|
+
out vec3 vWorld;
|
|
1924
|
+
out vec3 vLocal;
|
|
1925
|
+
|
|
1926
|
+
float roundProfile(float t) {
|
|
1927
|
+
float u = 1.0 - clamp(t, 0.0, 1.0);
|
|
1928
|
+
return sqrt(max(0.0, 1.0 - u * u));
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
void main() {
|
|
1932
|
+
vec4 s = textureLod(uSurface, aUv, 0.0);
|
|
1933
|
+
float side = uSide == 0.0 ? aLip : uSide;
|
|
1934
|
+
float z = side > 0.0 ? s.r * uDepth : -uThickness * roundProfile(max(s.a, 0.0) / uEdgeTexels);
|
|
1935
|
+
vec3 p = vec3(aUv.x - 0.5, 0.5 - aUv.y, z);
|
|
1936
|
+
vec4 w = uModel * vec4(p, 1.0);
|
|
1937
|
+
vUv = aUv;
|
|
1938
|
+
vWorld = w.xyz;
|
|
1939
|
+
vLocal = p;
|
|
1940
|
+
gl_Position = uViewProj * w;
|
|
1941
|
+
}
|
|
1942
|
+
`
|
|
1943
|
+
);
|
|
1944
|
+
var badgeFragment = (
|
|
1945
|
+
/* glsl */
|
|
1946
|
+
`#version 300 es
|
|
1947
|
+
precision highp float;
|
|
1948
|
+
${common}
|
|
1949
|
+
in vec2 vUv;
|
|
1950
|
+
in vec3 vWorld;
|
|
1951
|
+
in vec3 vLocal;
|
|
1952
|
+
|
|
1953
|
+
uniform sampler2D uSurface;
|
|
1954
|
+
uniform sampler2D uAlbedo;
|
|
1955
|
+
uniform sampler2D uWeights;
|
|
1956
|
+
uniform sampler2D uEnvSpec;
|
|
1957
|
+
uniform sampler2D uEnvIrr;
|
|
1958
|
+
uniform float uEnvMaxLod;
|
|
1959
|
+
uniform float uEnvRotation;
|
|
1960
|
+
uniform float uEnvScale;
|
|
1961
|
+
|
|
1962
|
+
uniform vec4 uMatColor[4]; // linear rgb, useSvgColor
|
|
1963
|
+
uniform vec4 uMatA[4]; // metalness, roughness, clearcoat, clearcoat roughness
|
|
1964
|
+
uniform vec4 uMatB[4]; // dielectric f0, iridescence, film thickness (nm), flakes
|
|
1965
|
+
uniform vec4 uMatC[4]; // brushed, brush direction, flake scale, emissive
|
|
1966
|
+
|
|
1967
|
+
uniform mat3 uNormalMatrix;
|
|
1968
|
+
uniform vec3 uCamPos;
|
|
1969
|
+
uniform float uMapSize;
|
|
1970
|
+
uniform float uPixelScale;
|
|
1971
|
+
uniform float uDepth;
|
|
1972
|
+
uniform float uThickness;
|
|
1973
|
+
uniform float uEdgeTexels;
|
|
1974
|
+
uniform float uAo;
|
|
1975
|
+
uniform float uExposure;
|
|
1976
|
+
uniform float uSide;
|
|
1977
|
+
uniform int uToneMap;
|
|
1978
|
+
uniform float uLocked;
|
|
1979
|
+
uniform vec3 uLockedColor;
|
|
1980
|
+
uniform float uShine;
|
|
1981
|
+
|
|
1982
|
+
out vec4 outColor;
|
|
1983
|
+
|
|
1984
|
+
vec3 envSpec(vec3 dir, float roughness) {
|
|
1985
|
+
return textureLod(uEnvSpec, dirToUv(dir, uEnvRotation), roughness * uEnvMaxLod).rgb * uEnvScale;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
vec3 envIrr(vec3 dir) {
|
|
1989
|
+
return textureLod(uEnvIrr, dirToUv(dir, uEnvRotation), 0.0).rgb * uEnvScale;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// Karis' analytic approximation of the split-sum BRDF integral.
|
|
1993
|
+
vec2 envBRDF(float roughness, float NdV) {
|
|
1994
|
+
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
|
|
1995
|
+
const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
|
|
1996
|
+
vec4 r = roughness * c0 + c1;
|
|
1997
|
+
float a004 = min(r.x * r.x, exp2(-9.28 * NdV)) * r.x + r.y;
|
|
1998
|
+
return vec2(-1.04, 1.04) * a004 + r.zw;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
float hash12(vec2 p) {
|
|
2002
|
+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
2003
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
2004
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
vec3 hash32(vec2 p) {
|
|
2008
|
+
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
|
|
2009
|
+
p3 += dot(p3, p3.yxz + 33.33);
|
|
2010
|
+
return fract((p3.xxy + p3.yzz) * p3.zyx);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
float noise1(float x) {
|
|
2014
|
+
float i = floor(x);
|
|
2015
|
+
float f = fract(x);
|
|
2016
|
+
float a = hash12(vec2(i, 17.0));
|
|
2017
|
+
float b = hash12(vec2(i + 1.0, 17.0));
|
|
2018
|
+
return mix(a, b, f * f * (3.0 - 2.0 * f));
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// Thin-film interference tint (cheap spectral approximation).
|
|
2022
|
+
vec3 thinFilm(float cosTheta, float thicknessNm) {
|
|
2023
|
+
float n = 1.4;
|
|
2024
|
+
float cosT = sqrt(max(0.0, 1.0 - (1.0 - cosTheta * cosTheta) / (n * n)));
|
|
2025
|
+
float opd = 2.0 * n * thicknessNm * cosT;
|
|
2026
|
+
vec3 phase = 2.0 * PI * opd / vec3(650.0, 532.0, 450.0);
|
|
2027
|
+
return 0.5 + 0.5 * cos(phase);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
vec3 srgbToLinear(vec3 c) {
|
|
2031
|
+
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
vec3 linearToSrgb(vec3 c) {
|
|
2035
|
+
c = clamp(c, 0.0, 1.0);
|
|
2036
|
+
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// Khronos PBR Neutral: keeps base colours faithful, rolls off highlights.
|
|
2040
|
+
vec3 toneNeutral(vec3 color) {
|
|
2041
|
+
const float startCompression = 0.8 - 0.04;
|
|
2042
|
+
const float desaturation = 0.15;
|
|
2043
|
+
float x = min(color.r, min(color.g, color.b));
|
|
2044
|
+
float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
|
|
2045
|
+
color -= offset;
|
|
2046
|
+
float peak = max(color.r, max(color.g, color.b));
|
|
2047
|
+
if (peak < startCompression) return color;
|
|
2048
|
+
const float d = 1.0 - startCompression;
|
|
2049
|
+
float newPeak = 1.0 - d * d / (peak + d - startCompression);
|
|
2050
|
+
color *= newPeak / peak;
|
|
2051
|
+
float g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);
|
|
2052
|
+
return mix(color, vec3(newPeak), g);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
vec3 toneAces(vec3 x) {
|
|
2056
|
+
x *= 0.6;
|
|
2057
|
+
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
vec3 toneAgx(vec3 v) {
|
|
2061
|
+
const mat3 m = mat3(0.842479062253094, 0.0423282422610123, 0.0423756549057051,
|
|
2062
|
+
0.0784335999999992, 0.878468636469772, 0.0784336,
|
|
2063
|
+
0.0792237451477643, 0.0791661274605434, 0.879142973793104);
|
|
2064
|
+
const mat3 mi = mat3(1.19687900512017, -0.0528968517574562, -0.0529716355144438,
|
|
2065
|
+
-0.0980208811401368, 1.15190312990417, -0.0980434501171241,
|
|
2066
|
+
-0.0990297440797205, -0.0989611768448433, 1.15107367264116);
|
|
2067
|
+
const float minEv = -12.47393;
|
|
2068
|
+
const float maxEv = 4.026069;
|
|
2069
|
+
v = m * v;
|
|
2070
|
+
v = clamp(log2(max(v, 1e-10)), minEv, maxEv);
|
|
2071
|
+
v = (v - minEv) / (maxEv - minEv);
|
|
2072
|
+
vec3 v2 = v * v;
|
|
2073
|
+
vec3 v4 = v2 * v2;
|
|
2074
|
+
v = 15.5 * v4 * v2 - 40.14 * v4 * v + 31.96 * v4 - 6.868 * v2 * v + 0.4298 * v2 + 0.1191 * v - 0.00232;
|
|
2075
|
+
v = mi * v;
|
|
2076
|
+
return pow(max(v, 0.0), vec3(2.2));
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
vec3 toneMap(vec3 c) {
|
|
2080
|
+
if (uToneMap == 0) return toneNeutral(c);
|
|
2081
|
+
if (uToneMap == 1) return toneAces(c);
|
|
2082
|
+
if (uToneMap == 2) return toneAgx(c);
|
|
2083
|
+
return c;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
void main() {
|
|
2087
|
+
vec4 s = texture(uSurface, vUv);
|
|
2088
|
+
float sd = s.a;
|
|
2089
|
+
// The contour mesh supplies silhouette coverage through MSAA. A second
|
|
2090
|
+
// distance-field alpha cut would expose cracks at the front/back seam.
|
|
2091
|
+
float alpha = 1.0;
|
|
2092
|
+
|
|
2093
|
+
vec4 w = texture(uWeights, vUv);
|
|
2094
|
+
w /= max(w.r + w.g + w.b + w.a, 1e-4);
|
|
2095
|
+
|
|
2096
|
+
if (uLocked > 0.5) {
|
|
2097
|
+
float line = alpha * clamp(w.r + w.a, 0.0, 1.0);
|
|
2098
|
+
if (line <= 0.001) discard;
|
|
2099
|
+
outColor = vec4(linearToSrgb(uLockedColor) * line, line);
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
vec3 nLocal;
|
|
2104
|
+
if (uSide > 0.0) {
|
|
2105
|
+
float k = uDepth * uMapSize;
|
|
2106
|
+
nLocal = normalize(vec3(-s.g * k, s.b * k, 1.0));
|
|
2107
|
+
} else {
|
|
2108
|
+
// Flat back with the same rounded edge as the front.
|
|
2109
|
+
float texel = 1.0 / uMapSize;
|
|
2110
|
+
float sx = texture(uSurface, vUv + vec2(texel, 0.0)).a - texture(uSurface, vUv - vec2(texel, 0.0)).a;
|
|
2111
|
+
float sy = texture(uSurface, vUv + vec2(0.0, texel)).a - texture(uSurface, vUv - vec2(0.0, texel)).a;
|
|
2112
|
+
float t = clamp(sd / uEdgeTexels, 0.0, 1.0);
|
|
2113
|
+
float u = 1.0 - t;
|
|
2114
|
+
float dp = t < 1.0 ? u / max(sqrt(max(1.0 - u * u, 1e-4)), 0.05) : 0.0;
|
|
2115
|
+
float k = uThickness * dp / uEdgeTexels * uMapSize * 0.5;
|
|
2116
|
+
nLocal = uSide == 0.0 ? normalize(vec3(-sx, sy, 0.0)) : normalize(vec3(-k * sx, k * sy, -1.0));
|
|
2117
|
+
w = vec4(1.0, 0.0, 0.0, 0.0);
|
|
2118
|
+
}
|
|
2119
|
+
vec3 N = normalize(uNormalMatrix * nLocal);
|
|
2120
|
+
vec3 V = normalize(uCamPos - vWorld);
|
|
2121
|
+
|
|
2122
|
+
vec4 albedo = texture(uAlbedo, vUv);
|
|
2123
|
+
vec3 svg = albedo.rgb; // sRGB texture: already linear
|
|
2124
|
+
float cavity = uSide > 0.0 ? albedo.a : 0.0;
|
|
2125
|
+
|
|
2126
|
+
vec3 baseColor = vec3(0.0);
|
|
2127
|
+
vec4 A = vec4(0.0);
|
|
2128
|
+
vec4 B = vec4(0.0);
|
|
2129
|
+
vec4 C = vec4(0.0);
|
|
2130
|
+
float brushDir = 0.0;
|
|
2131
|
+
float brushBest = -1.0;
|
|
2132
|
+
for (int i = 0; i < 4; i++) {
|
|
2133
|
+
float wi = w[i];
|
|
2134
|
+
baseColor += wi * mix(uMatColor[i].rgb, svg, uMatColor[i].a);
|
|
2135
|
+
A += wi * uMatA[i];
|
|
2136
|
+
B += wi * uMatB[i];
|
|
2137
|
+
C += wi * uMatC[i];
|
|
2138
|
+
float bw = wi * uMatC[i].x;
|
|
2139
|
+
if (bw > brushBest) {
|
|
2140
|
+
brushBest = bw;
|
|
2141
|
+
brushDir = uMatC[i].y;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
float metalness = clamp(A.x, 0.0, 1.0);
|
|
2145
|
+
float roughness = clamp(A.y, 0.02, 1.0);
|
|
2146
|
+
float clearcoat = clamp(A.z, 0.0, 1.0);
|
|
2147
|
+
float ccRoughness = clamp(A.w, 0.02, 1.0);
|
|
2148
|
+
float f0d = B.x;
|
|
2149
|
+
float irid = B.y;
|
|
2150
|
+
float filmNm = B.z;
|
|
2151
|
+
float flakes = B.w;
|
|
2152
|
+
float brushed = C.x;
|
|
2153
|
+
if (uSide < 0.0) {
|
|
2154
|
+
// The back plate gets a lathe-turned finish.
|
|
2155
|
+
brushed = max(brushed, 0.55);
|
|
2156
|
+
brushDir = 0.0;
|
|
2157
|
+
}
|
|
2158
|
+
float flakeScale = max(C.z, 1.0);
|
|
2159
|
+
float emissive = C.w;
|
|
2160
|
+
|
|
2161
|
+
// Brushed metal: streaky normal perturbation along the brushing direction.
|
|
2162
|
+
vec3 Nb = N;
|
|
2163
|
+
if (brushed > 0.001) {
|
|
2164
|
+
vec2 p = vLocal.xy;
|
|
2165
|
+
float coord;
|
|
2166
|
+
vec2 across;
|
|
2167
|
+
if (brushDir < 0.5) { coord = length(p) * 700.0; across = normalize(p + 1e-5); }
|
|
2168
|
+
else if (brushDir < 1.5) { coord = atan(p.y, p.x) * 160.0; across = normalize(vec2(-p.y, p.x) + 1e-5); }
|
|
2169
|
+
else if (brushDir < 2.5) { coord = p.y * 700.0; across = vec2(0.0, 1.0); }
|
|
2170
|
+
else { coord = p.x * 700.0; across = vec2(1.0, 0.0); }
|
|
2171
|
+
// Streaks finer than a pixel would alias into moir\xE9; fade them and let roughness carry the look.
|
|
2172
|
+
float footprint = fwidth(coord) * uPixelScale;
|
|
2173
|
+
float n = (noise1(coord) - 0.5) * clamp(1.4 - footprint * 0.9, 0.0, 1.0)
|
|
2174
|
+
+ 0.5 * (noise1(coord * 3.7 + 11.0) - 0.5) * clamp(1.4 - footprint * 3.3, 0.0, 1.0);
|
|
2175
|
+
Nb = normalize(N + uNormalMatrix * vec3(across * n * 0.22 * brushed, 0.0));
|
|
2176
|
+
roughness = mix(roughness, min(1.0, roughness + 0.12), brushed);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
float NdV = clamp(dot(Nb, V), 1e-4, 1.0);
|
|
2180
|
+
// Resolve normal detail below a pixel into roughness instead of sparkling
|
|
2181
|
+
// pinprick highlights. This uses derivatives, with no additional map reads.
|
|
2182
|
+
vec3 dx = dFdx(N) * uPixelScale;
|
|
2183
|
+
vec3 dy = dFdy(N) * uPixelScale;
|
|
2184
|
+
float variance = min(0.15 * (dot(dx, dx) + dot(dy, dy)), 0.025);
|
|
2185
|
+
roughness = sqrt(clamp(roughness * roughness + variance, 0.0004, 1.0));
|
|
2186
|
+
ccRoughness = sqrt(clamp(ccRoughness * ccRoughness + variance, 0.0004, 1.0));
|
|
2187
|
+
|
|
2188
|
+
vec3 F0 = mix(vec3(f0d), baseColor, metalness);
|
|
2189
|
+
if (irid > 0.001) {
|
|
2190
|
+
vec3 film = thinFilm(NdV, filmNm);
|
|
2191
|
+
F0 = mix(F0, mix(film, film * baseColor * 1.6, metalness * 0.5), irid);
|
|
2192
|
+
}
|
|
2193
|
+
vec2 ab = envBRDF(roughness, NdV);
|
|
2194
|
+
vec3 FssEss = F0 * ab.x + ab.y;
|
|
2195
|
+
float Ems = 1.0 - (ab.x + ab.y);
|
|
2196
|
+
vec3 Favg = F0 + (1.0 - F0) / 21.0;
|
|
2197
|
+
vec3 Fms = FssEss * Favg / (1.0 - Ems * Favg);
|
|
2198
|
+
vec3 specWeight = FssEss + Fms * Ems;
|
|
2199
|
+
|
|
2200
|
+
float ao = 1.0 - uAo * cavity;
|
|
2201
|
+
float specAo = mix(1.0, ao, 0.65);
|
|
2202
|
+
vec3 R = reflect(-V, Nb);
|
|
2203
|
+
vec3 irradiance = envIrr(Nb);
|
|
2204
|
+
vec3 diffuse = baseColor * (1.0 - metalness) * (1.0 - specWeight) * irradiance * ao;
|
|
2205
|
+
// Multiple scattering is broad, so it follows irradiance rather than the
|
|
2206
|
+
// sharp reflected softbox. Reuse the diffuse lookup already in this pass.
|
|
2207
|
+
vec3 color = diffuse + envSpec(R, roughness) * FssEss * specAo
|
|
2208
|
+
+ irradiance * Fms * Ems * ao;
|
|
2209
|
+
|
|
2210
|
+
// Metallic flakes: each cell reflects the environment at its own random angle.
|
|
2211
|
+
if (flakes > 0.001) {
|
|
2212
|
+
vec2 cuv = vUv * flakeScale;
|
|
2213
|
+
vec2 cell = floor(cuv);
|
|
2214
|
+
vec3 rnd = hash32(cell);
|
|
2215
|
+
vec2 f = fract(cuv) - 0.5 - (rnd.xy - 0.5) * 0.4;
|
|
2216
|
+
float footprint = max(fwidth(cuv.x), fwidth(cuv.y)) * uPixelScale;
|
|
2217
|
+
float edge = max(0.025, footprint * 0.35);
|
|
2218
|
+
float mask = (1.0 - smoothstep(0.23 - edge, 0.23 + edge, length(f))) * step(0.3, rnd.z);
|
|
2219
|
+
// Unresolved flakes become the average coverage, avoiding crawling pixels.
|
|
2220
|
+
float resolved = 1.0 - smoothstep(0.6, 1.8, footprint);
|
|
2221
|
+
mask = mix(0.116, mask, resolved);
|
|
2222
|
+
vec3 fn = normalize(Nb + uNormalMatrix * vec3((rnd.xy - 0.5) * 1.3 * resolved, 0.0));
|
|
2223
|
+
vec3 fr = reflect(-V, fn);
|
|
2224
|
+
vec3 flakeColor = envSpec(fr, mix(max(roughness, 0.25), 0.06, resolved)) * mix(vec3(0.85), baseColor, 0.55) * specAo;
|
|
2225
|
+
color = mix(color, flakeColor, mask * clamp(flakes, 0.0, 1.0));
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// Clear-coat layer (uses the unperturbed normal).
|
|
2229
|
+
if (clearcoat > 0.001) {
|
|
2230
|
+
float NdVc = clamp(dot(N, V), 1e-4, 1.0);
|
|
2231
|
+
float Fc = 0.04 + 0.96 * pow(1.0 - NdVc, 5.0);
|
|
2232
|
+
vec2 abc = envBRDF(ccRoughness, NdVc);
|
|
2233
|
+
vec3 cc = envSpec(reflect(-V, N), ccRoughness) * (0.04 * abc.x + abc.y);
|
|
2234
|
+
float transmission = 1.0 - clearcoat * Fc;
|
|
2235
|
+
color = color * transmission * transmission + clearcoat * cc * specAo;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
color += baseColor * emissive;
|
|
2239
|
+
|
|
2240
|
+
// Optional travelling glint across the face.
|
|
2241
|
+
if (uShine > 0.0) {
|
|
2242
|
+
float band = dot(vLocal.xy, normalize(vec2(1.0, 0.6))) - (uShine * 2.6 - 1.3);
|
|
2243
|
+
color += exp(-band * band * 90.0) * 0.9 * specWeight;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
color = toneMap(color * uExposure);
|
|
2247
|
+
color = linearToSrgb(color);
|
|
2248
|
+
color += (hash12(gl_FragCoord.xy) - 0.5) / 255.0;
|
|
2249
|
+
outColor = vec4(color * alpha, alpha);
|
|
2250
|
+
}
|
|
2251
|
+
`
|
|
2252
|
+
);
|
|
2253
|
+
var shadowVertex = (
|
|
2254
|
+
/* glsl */
|
|
2255
|
+
`#version 300 es
|
|
2256
|
+
precision highp float;
|
|
2257
|
+
layout(location = 0) in vec2 aUv;
|
|
2258
|
+
uniform mat4 uModel;
|
|
2259
|
+
uniform mat4 uViewProj;
|
|
2260
|
+
uniform float uZ;
|
|
2261
|
+
uniform float uOffset;
|
|
2262
|
+
uniform float uSpread;
|
|
2263
|
+
out vec2 vUv;
|
|
2264
|
+
|
|
2265
|
+
void main() {
|
|
2266
|
+
vec2 uv = (aUv - 0.5) * uSpread + 0.5;
|
|
2267
|
+
vec3 p = vec3(uv.x - 0.5, 0.5 - uv.y, uZ);
|
|
2268
|
+
vec4 w = uModel * vec4(p, 1.0);
|
|
2269
|
+
w.y -= uOffset;
|
|
2270
|
+
vUv = uv;
|
|
2271
|
+
gl_Position = uViewProj * w;
|
|
2272
|
+
}
|
|
2273
|
+
`
|
|
2274
|
+
);
|
|
2275
|
+
var shadowFragment = (
|
|
2276
|
+
/* glsl */
|
|
2277
|
+
`#version 300 es
|
|
2278
|
+
precision highp float;
|
|
2279
|
+
in vec2 vUv;
|
|
2280
|
+
uniform sampler2D uSurface;
|
|
2281
|
+
uniform float uOpacity;
|
|
2282
|
+
uniform float uBlur;
|
|
2283
|
+
out vec4 outColor;
|
|
2284
|
+
|
|
2285
|
+
void main() {
|
|
2286
|
+
float sd = texture(uSurface, clamp(vUv, 0.0, 1.0)).a;
|
|
2287
|
+
float a = smoothstep(-uBlur, uBlur * 0.5, sd);
|
|
2288
|
+
a = a * a * uOpacity;
|
|
2289
|
+
if (a <= 0.002) discard;
|
|
2290
|
+
outColor = vec4(0.0, 0.0, 0.0, a);
|
|
2291
|
+
}
|
|
2292
|
+
`
|
|
2293
|
+
);
|
|
2294
|
+
|
|
2295
|
+
// src/render/renderer.ts
|
|
2296
|
+
var ENV_WIDTH = 512;
|
|
2297
|
+
var ENV_LEVELS = 6;
|
|
2298
|
+
var IRR_WIDTH = 64;
|
|
2299
|
+
var BRUSH_DIRS = { circular: 0, radial: 1, horizontal: 2, vertical: 3 };
|
|
2300
|
+
function compile(gl, type, source) {
|
|
2301
|
+
const shader = gl.createShader(type);
|
|
2302
|
+
gl.shaderSource(shader, source);
|
|
2303
|
+
gl.compileShader(shader);
|
|
2304
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) && !gl.isContextLost()) {
|
|
2305
|
+
const log = gl.getShaderInfoLog(shader);
|
|
2306
|
+
gl.deleteShader(shader);
|
|
2307
|
+
throw new Error(`badgecraft: shader compile failed
|
|
2308
|
+
${log}`);
|
|
2309
|
+
}
|
|
2310
|
+
return shader;
|
|
2311
|
+
}
|
|
2312
|
+
function link(gl, vs, fs) {
|
|
2313
|
+
const program = gl.createProgram();
|
|
2314
|
+
const v = compile(gl, gl.VERTEX_SHADER, vs);
|
|
2315
|
+
const f2 = compile(gl, gl.FRAGMENT_SHADER, fs);
|
|
2316
|
+
gl.attachShader(program, v);
|
|
2317
|
+
gl.attachShader(program, f2);
|
|
2318
|
+
gl.linkProgram(program);
|
|
2319
|
+
gl.deleteShader(v);
|
|
2320
|
+
gl.deleteShader(f2);
|
|
2321
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS) && !gl.isContextLost()) {
|
|
2322
|
+
throw new Error(`badgecraft: program link failed
|
|
2323
|
+
${gl.getProgramInfoLog(program)}`);
|
|
2324
|
+
}
|
|
2325
|
+
const u = {};
|
|
2326
|
+
const count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
|
|
2327
|
+
for (let i = 0; i < count; i++) {
|
|
2328
|
+
const info = gl.getActiveUniform(program, i);
|
|
2329
|
+
if (!info) continue;
|
|
2330
|
+
const name = info.name.replace(/\[0\]$/, "");
|
|
2331
|
+
u[name] = gl.getUniformLocation(program, info.name);
|
|
2332
|
+
}
|
|
2333
|
+
return { program, u };
|
|
2334
|
+
}
|
|
2335
|
+
var shared;
|
|
2336
|
+
var Renderer = class _Renderer {
|
|
2337
|
+
constructor() {
|
|
2338
|
+
/** Incremented whenever the context is restored; GPU resources must be re-created. */
|
|
2339
|
+
this.generation = 0;
|
|
2340
|
+
this.floatTargets = false;
|
|
2341
|
+
this.anisotropy = null;
|
|
2342
|
+
this.maxAnisotropy = 1;
|
|
2343
|
+
this.renderLimit = 2048;
|
|
2344
|
+
this.envCache = /* @__PURE__ */ new Map();
|
|
2345
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
2346
|
+
this.canvas = document.createElement("canvas");
|
|
2347
|
+
this.canvas.width = 256;
|
|
2348
|
+
this.canvas.height = 256;
|
|
2349
|
+
const gl = this.canvas.getContext("webgl2", {
|
|
2350
|
+
alpha: true,
|
|
2351
|
+
premultipliedAlpha: true,
|
|
2352
|
+
antialias: true,
|
|
2353
|
+
depth: true,
|
|
2354
|
+
stencil: false,
|
|
2355
|
+
preserveDrawingBuffer: false
|
|
2356
|
+
});
|
|
2357
|
+
if (!gl) throw new Error("badgecraft: WebGL2 is not available");
|
|
2358
|
+
this.gl = gl;
|
|
2359
|
+
this.canvas.addEventListener("webglcontextlost", (e) => e.preventDefault());
|
|
2360
|
+
this.canvas.addEventListener("webglcontextrestored", () => {
|
|
2361
|
+
this.envCache.clear();
|
|
2362
|
+
this.init();
|
|
2363
|
+
this.generation++;
|
|
2364
|
+
this.listeners.forEach((fn) => fn());
|
|
2365
|
+
});
|
|
2366
|
+
this.init();
|
|
2367
|
+
}
|
|
2368
|
+
/** The page-wide renderer, or null when WebGL2 is unavailable. */
|
|
2369
|
+
static shared() {
|
|
2370
|
+
if (shared === void 0) {
|
|
2371
|
+
try {
|
|
2372
|
+
shared = new _Renderer();
|
|
2373
|
+
} catch (error) {
|
|
2374
|
+
console.warn(error);
|
|
2375
|
+
shared = null;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return shared;
|
|
2379
|
+
}
|
|
2380
|
+
/** Called after a lost context has been restored. */
|
|
2381
|
+
onRestore(fn) {
|
|
2382
|
+
this.listeners.add(fn);
|
|
2383
|
+
return () => this.listeners.delete(fn);
|
|
2384
|
+
}
|
|
2385
|
+
init() {
|
|
2386
|
+
const gl = this.gl;
|
|
2387
|
+
this.renderLimit = Math.min(2048, gl.getParameter(gl.MAX_RENDERBUFFER_SIZE));
|
|
2388
|
+
this.floatTargets = !!gl.getExtension("EXT_color_buffer_float");
|
|
2389
|
+
this.anisotropy = gl.getExtension("EXT_texture_filter_anisotropic");
|
|
2390
|
+
this.maxAnisotropy = this.anisotropy ? Math.min(2, gl.getParameter(this.anisotropy.MAX_TEXTURE_MAX_ANISOTROPY_EXT)) : 1;
|
|
2391
|
+
gl.getExtension("OES_texture_float_linear");
|
|
2392
|
+
this.badgeProgram = link(gl, badgeVertex, badgeFragment);
|
|
2393
|
+
this.shadowProgram = link(gl, shadowVertex, shadowFragment);
|
|
2394
|
+
this.envProgram = link(gl, fullscreenVertex, envGenerateFragment);
|
|
2395
|
+
this.prefilterProgram = link(gl, fullscreenVertex, prefilterFragment);
|
|
2396
|
+
this.irrProgram = link(gl, fullscreenVertex, irradianceFragment);
|
|
2397
|
+
this.fbo = gl.createFramebuffer();
|
|
2398
|
+
this.emptyVao = gl.createVertexArray();
|
|
2399
|
+
const verts = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
|
2400
|
+
const indices = new Uint16Array([0, 2, 1, 1, 2, 3]);
|
|
2401
|
+
this.shadowQuad = gl.createVertexArray();
|
|
2402
|
+
gl.bindVertexArray(this.shadowQuad);
|
|
2403
|
+
const vb = gl.createBuffer();
|
|
2404
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vb);
|
|
2405
|
+
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
|
|
2406
|
+
gl.enableVertexAttribArray(0);
|
|
2407
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
2408
|
+
const ib = gl.createBuffer();
|
|
2409
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib);
|
|
2410
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
|
|
2411
|
+
gl.bindVertexArray(null);
|
|
2412
|
+
}
|
|
2413
|
+
get contextLost() {
|
|
2414
|
+
return this.gl.isContextLost();
|
|
2415
|
+
}
|
|
2416
|
+
// ---------------------------------------------------------------- textures
|
|
2417
|
+
texture(internal, format, type, size, data, mipmaps) {
|
|
2418
|
+
const gl = this.gl;
|
|
2419
|
+
const tex = gl.createTexture();
|
|
2420
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
2421
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
2422
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internal, size, size, 0, format, type, data);
|
|
2423
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2424
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2425
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
2426
|
+
if (mipmaps) {
|
|
2427
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
2428
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
2429
|
+
if (this.anisotropy) {
|
|
2430
|
+
gl.texParameterf(gl.TEXTURE_2D, this.anisotropy.TEXTURE_MAX_ANISOTROPY_EXT, this.maxAnisotropy);
|
|
2431
|
+
}
|
|
2432
|
+
} else {
|
|
2433
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
2434
|
+
}
|
|
2435
|
+
return tex;
|
|
2436
|
+
}
|
|
2437
|
+
createBadgeTextures(out) {
|
|
2438
|
+
const gl = this.gl;
|
|
2439
|
+
const data = reliefMesh(out.surface, out.size);
|
|
2440
|
+
const mesh = gl.createVertexArray();
|
|
2441
|
+
const vertexBuffer = gl.createBuffer();
|
|
2442
|
+
const indexBuffer = gl.createBuffer();
|
|
2443
|
+
gl.bindVertexArray(mesh);
|
|
2444
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
2445
|
+
gl.bufferData(gl.ARRAY_BUFFER, data.vertices, gl.STATIC_DRAW);
|
|
2446
|
+
gl.enableVertexAttribArray(0);
|
|
2447
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 12, 0);
|
|
2448
|
+
gl.enableVertexAttribArray(1);
|
|
2449
|
+
gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 12, 8);
|
|
2450
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
2451
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data.indices, gl.STATIC_DRAW);
|
|
2452
|
+
gl.bindVertexArray(null);
|
|
2453
|
+
return {
|
|
2454
|
+
mesh,
|
|
2455
|
+
vertexBuffer,
|
|
2456
|
+
indexBuffer,
|
|
2457
|
+
faceCount: data.faceCount,
|
|
2458
|
+
sideCount: data.indices.length - data.faceCount,
|
|
2459
|
+
surface: this.texture(gl.RGBA16F, gl.RGBA, gl.FLOAT, out.size, out.surface, this.floatTargets),
|
|
2460
|
+
albedo: this.texture(gl.SRGB8_ALPHA8, gl.RGBA, gl.UNSIGNED_BYTE, out.size, out.albedo, true),
|
|
2461
|
+
weights: this.texture(gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, out.size, out.weights, true),
|
|
2462
|
+
size: out.size,
|
|
2463
|
+
heightScale: out.heightScale
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
deleteBadgeTextures(t) {
|
|
2467
|
+
const gl = this.gl;
|
|
2468
|
+
gl.deleteVertexArray(t.mesh);
|
|
2469
|
+
gl.deleteBuffer(t.vertexBuffer);
|
|
2470
|
+
gl.deleteBuffer(t.indexBuffer);
|
|
2471
|
+
gl.deleteTexture(t.surface);
|
|
2472
|
+
gl.deleteTexture(t.albedo);
|
|
2473
|
+
gl.deleteTexture(t.weights);
|
|
2474
|
+
}
|
|
2475
|
+
// ------------------------------------------------------------- environment
|
|
2476
|
+
/** Pre-filtered environment textures, shared and reference-counted across badges. */
|
|
2477
|
+
async acquireEnvironment(env) {
|
|
2478
|
+
const key = env.key;
|
|
2479
|
+
let entry = this.envCache.get(key);
|
|
2480
|
+
if (!entry) {
|
|
2481
|
+
const created = { refs: 0, promise: this.buildEnvironment(env) };
|
|
2482
|
+
created.promise.then(
|
|
2483
|
+
(t) => created.textures = t,
|
|
2484
|
+
() => this.envCache.get(key) === created && this.envCache.delete(key)
|
|
2485
|
+
);
|
|
2486
|
+
this.envCache.set(key, created);
|
|
2487
|
+
entry = created;
|
|
2488
|
+
}
|
|
2489
|
+
entry.refs++;
|
|
2490
|
+
if (entry.disposeTimer) {
|
|
2491
|
+
clearTimeout(entry.disposeTimer);
|
|
2492
|
+
entry.disposeTimer = void 0;
|
|
2493
|
+
}
|
|
2494
|
+
try {
|
|
2495
|
+
return { key, textures: await entry.promise };
|
|
2496
|
+
} catch (error) {
|
|
2497
|
+
entry.refs--;
|
|
2498
|
+
throw error;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
releaseEnvironment(handle) {
|
|
2502
|
+
const entry = this.envCache.get(handle.key);
|
|
2503
|
+
if (!entry || entry.textures !== handle.textures) return;
|
|
2504
|
+
entry.refs--;
|
|
2505
|
+
if (entry.refs > 0) return;
|
|
2506
|
+
entry.disposeTimer = setTimeout(() => {
|
|
2507
|
+
if (entry.refs > 0 || this.envCache.get(handle.key) !== entry) return;
|
|
2508
|
+
this.envCache.delete(handle.key);
|
|
2509
|
+
if (entry.textures && !this.gl.isContextLost()) {
|
|
2510
|
+
this.gl.deleteTexture(entry.textures.spec);
|
|
2511
|
+
this.gl.deleteTexture(entry.textures.irr);
|
|
2512
|
+
}
|
|
2513
|
+
}, 5e3);
|
|
2514
|
+
}
|
|
2515
|
+
envFormat() {
|
|
2516
|
+
const gl = this.gl;
|
|
2517
|
+
return this.floatTargets ? { internal: gl.RGBA16F, type: gl.HALF_FLOAT, scale: 1 } : { internal: gl.RGBA8, type: gl.UNSIGNED_BYTE, scale: 8 };
|
|
2518
|
+
}
|
|
2519
|
+
allocEnvTexture(width, height, levels) {
|
|
2520
|
+
const gl = this.gl;
|
|
2521
|
+
const { internal } = this.envFormat();
|
|
2522
|
+
const tex = gl.createTexture();
|
|
2523
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
2524
|
+
gl.texStorage2D(gl.TEXTURE_2D, levels, internal, width, height);
|
|
2525
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
2526
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2527
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
2528
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, levels > 1 ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
|
|
2529
|
+
return tex;
|
|
2530
|
+
}
|
|
2531
|
+
async buildEnvironment(env) {
|
|
2532
|
+
const gl = this.gl;
|
|
2533
|
+
const { scale } = this.envFormat();
|
|
2534
|
+
const generation = this.generation;
|
|
2535
|
+
const image = env.kind === "image" ? await this.loadImageEnvironment(env.spec.url, (env.spec.intensity ?? 1) / scale) : null;
|
|
2536
|
+
if (gl.isContextLost() || generation !== this.generation) {
|
|
2537
|
+
throw new Error("badgecraft: environment invalidated by context loss");
|
|
2538
|
+
}
|
|
2539
|
+
const srcW = ENV_WIDTH * 2;
|
|
2540
|
+
const srcH = ENV_WIDTH;
|
|
2541
|
+
const srcLevels = Math.floor(Math.log2(srcW)) + 1;
|
|
2542
|
+
const src = this.allocEnvTexture(srcW, srcH, srcLevels);
|
|
2543
|
+
try {
|
|
2544
|
+
if (env.kind === "procedural") {
|
|
2545
|
+
this.renderProcedural(src, srcW, srcH, env.spec, scale);
|
|
2546
|
+
} else if (image) {
|
|
2547
|
+
this.uploadImageEnvironment(src, srcW, srcH, image);
|
|
2548
|
+
}
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
gl.deleteTexture(src);
|
|
2551
|
+
throw error;
|
|
2552
|
+
}
|
|
2553
|
+
gl.bindTexture(gl.TEXTURE_2D, src);
|
|
2554
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
2555
|
+
const spec = this.allocEnvTexture(ENV_WIDTH, ENV_WIDTH / 2, ENV_LEVELS);
|
|
2556
|
+
const texelSA = 4 * Math.PI / (srcW * srcH);
|
|
2557
|
+
const pre = this.prefilterProgram;
|
|
2558
|
+
gl.useProgram(pre.program);
|
|
2559
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2560
|
+
gl.bindTexture(gl.TEXTURE_2D, src);
|
|
2561
|
+
gl.uniform1i(pre.u.uSrc, 0);
|
|
2562
|
+
gl.uniform1f(pre.u.uTexelSolidAngle, texelSA);
|
|
2563
|
+
for (let level = 0; level < ENV_LEVELS; level++) {
|
|
2564
|
+
const w = ENV_WIDTH >> level;
|
|
2565
|
+
const h = Math.max(1, ENV_WIDTH / 2 >> level);
|
|
2566
|
+
this.bindTarget(spec, level, w, h);
|
|
2567
|
+
gl.uniform2f(pre.u.uSize, w, h);
|
|
2568
|
+
gl.uniform1f(pre.u.uRoughness, level / (ENV_LEVELS - 1));
|
|
2569
|
+
this.fullscreen();
|
|
2570
|
+
}
|
|
2571
|
+
const irr = this.allocEnvTexture(IRR_WIDTH, IRR_WIDTH / 2, 1);
|
|
2572
|
+
const ip = this.irrProgram;
|
|
2573
|
+
gl.useProgram(ip.program);
|
|
2574
|
+
gl.bindTexture(gl.TEXTURE_2D, src);
|
|
2575
|
+
gl.uniform1i(ip.u.uSrc, 0);
|
|
2576
|
+
gl.uniform1f(ip.u.uTexelSolidAngle, texelSA);
|
|
2577
|
+
this.bindTarget(irr, 0, IRR_WIDTH, IRR_WIDTH / 2);
|
|
2578
|
+
gl.uniform2f(ip.u.uSize, IRR_WIDTH, IRR_WIDTH / 2);
|
|
2579
|
+
this.fullscreen();
|
|
2580
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2581
|
+
gl.deleteTexture(src);
|
|
2582
|
+
return { spec, irr, maxLod: ENV_LEVELS - 1, scale };
|
|
2583
|
+
}
|
|
2584
|
+
bindTarget(tex, level, w, h) {
|
|
2585
|
+
const gl = this.gl;
|
|
2586
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
|
|
2587
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, level);
|
|
2588
|
+
gl.viewport(0, 0, w, h);
|
|
2589
|
+
gl.disable(gl.BLEND);
|
|
2590
|
+
gl.disable(gl.DEPTH_TEST);
|
|
2591
|
+
gl.disable(gl.CULL_FACE);
|
|
2592
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
2593
|
+
}
|
|
2594
|
+
fullscreen() {
|
|
2595
|
+
const gl = this.gl;
|
|
2596
|
+
gl.bindVertexArray(this.emptyVao);
|
|
2597
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2598
|
+
gl.bindVertexArray(null);
|
|
2599
|
+
}
|
|
2600
|
+
renderProcedural(target, w, h, spec, scale) {
|
|
2601
|
+
const gl = this.gl;
|
|
2602
|
+
const p = this.envProgram;
|
|
2603
|
+
const intensity = spec.intensity ?? 1;
|
|
2604
|
+
const lights = (spec.lights ?? []).slice(0, MAX_LIGHTS);
|
|
2605
|
+
const dirs = new Float32Array(MAX_LIGHTS * 3);
|
|
2606
|
+
const colors = new Float32Array(MAX_LIGHTS * 3);
|
|
2607
|
+
const shapes = new Float32Array(MAX_LIGHTS * 4);
|
|
2608
|
+
lights.forEach((l, i) => {
|
|
2609
|
+
const az = l.azimuth * Math.PI / 180;
|
|
2610
|
+
const el = l.elevation * Math.PI / 180;
|
|
2611
|
+
dirs.set([Math.sin(az) * Math.cos(el), Math.sin(el), Math.cos(az) * Math.cos(el)], i * 3);
|
|
2612
|
+
const c = linearRgb(l.color ?? "#ffffff");
|
|
2613
|
+
const k = (l.intensity ?? 1) * intensity;
|
|
2614
|
+
colors.set([c[0] * k, c[1] * k, c[2] * k], i * 3);
|
|
2615
|
+
const size = l.size ?? 30;
|
|
2616
|
+
const [sw, sh] = Array.isArray(size) ? size : [size, size];
|
|
2617
|
+
const t = (deg) => Math.tan(Math.min(deg, 170) * Math.PI / 360);
|
|
2618
|
+
shapes.set([t(sw), t(sh), l.softness ?? 0.5, l.shape === "circle" ? 1 : 0], i * 4);
|
|
2619
|
+
});
|
|
2620
|
+
const top = linearRgb(spec.top ?? "#555555").map((v) => v * intensity);
|
|
2621
|
+
const horizon = linearRgb(spec.horizon ?? "#222222").map((v) => v * intensity);
|
|
2622
|
+
const bottom = linearRgb(spec.bottom ?? "#080808").map((v) => v * intensity);
|
|
2623
|
+
gl.useProgram(p.program);
|
|
2624
|
+
this.bindTarget(target, 0, w, h);
|
|
2625
|
+
gl.uniform2f(p.u.uSize, w, h);
|
|
2626
|
+
gl.uniform3fv(p.u.uTop, top);
|
|
2627
|
+
gl.uniform3fv(p.u.uHorizon, horizon);
|
|
2628
|
+
gl.uniform3fv(p.u.uBottom, bottom);
|
|
2629
|
+
gl.uniform1i(p.u.uLightCount, lights.length);
|
|
2630
|
+
gl.uniform3fv(p.u.uLightDir, dirs);
|
|
2631
|
+
gl.uniform3fv(p.u.uLightColor, colors);
|
|
2632
|
+
gl.uniform4fv(p.u.uLightShape, shapes);
|
|
2633
|
+
gl.uniform1f(p.u.uScale, 1 / scale);
|
|
2634
|
+
this.fullscreen();
|
|
2635
|
+
}
|
|
2636
|
+
async loadImageEnvironment(url, intensity) {
|
|
2637
|
+
let width;
|
|
2638
|
+
let height;
|
|
2639
|
+
let data;
|
|
2640
|
+
if (/\.hdr(\?|#|$)/i.test(url)) {
|
|
2641
|
+
const res = await fetch(url);
|
|
2642
|
+
if (!res.ok) throw new Error(`badgecraft: failed to load environment ${url}`);
|
|
2643
|
+
({ width, height, data } = decodeHdr(await res.arrayBuffer()));
|
|
2644
|
+
} else {
|
|
2645
|
+
const img = new Image();
|
|
2646
|
+
img.crossOrigin = "anonymous";
|
|
2647
|
+
img.src = url;
|
|
2648
|
+
await img.decode();
|
|
2649
|
+
const scale = Math.min(1, 2048 / Math.max(img.naturalWidth, img.naturalHeight));
|
|
2650
|
+
width = Math.max(1, Math.round(img.naturalWidth * scale));
|
|
2651
|
+
height = Math.max(1, Math.round(img.naturalHeight * scale));
|
|
2652
|
+
const c = document.createElement("canvas");
|
|
2653
|
+
c.width = width;
|
|
2654
|
+
c.height = height;
|
|
2655
|
+
const ctx = c.getContext("2d");
|
|
2656
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
2657
|
+
const px = ctx.getImageData(0, 0, width, height).data;
|
|
2658
|
+
data = new Float32Array(width * height * 4);
|
|
2659
|
+
const lut = new Float32Array(256);
|
|
2660
|
+
for (let i = 0; i < 256; i++) {
|
|
2661
|
+
const v = i / 255;
|
|
2662
|
+
lut[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
2663
|
+
}
|
|
2664
|
+
for (let i = 0; i < width * height; i++) {
|
|
2665
|
+
data[i * 4] = lut[px[i * 4]];
|
|
2666
|
+
data[i * 4 + 1] = lut[px[i * 4 + 1]];
|
|
2667
|
+
data[i * 4 + 2] = lut[px[i * 4 + 2]];
|
|
2668
|
+
data[i * 4 + 3] = 1;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
for (let i = 0; i < data.length; i++) if ((i & 3) !== 3) data[i] *= intensity;
|
|
2672
|
+
return { width, height, data };
|
|
2673
|
+
}
|
|
2674
|
+
uploadImageEnvironment(target, w, h, image) {
|
|
2675
|
+
const gl = this.gl;
|
|
2676
|
+
const { width, height, data } = image;
|
|
2677
|
+
const limit = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
2678
|
+
if (width > limit || height > limit) {
|
|
2679
|
+
throw new Error(`badgecraft: environment dimensions exceed the GPU texture limit (${limit}px)`);
|
|
2680
|
+
}
|
|
2681
|
+
const tmp = gl.createTexture();
|
|
2682
|
+
gl.bindTexture(gl.TEXTURE_2D, tmp);
|
|
2683
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
2684
|
+
const floatLinear = !!gl.getExtension("OES_texture_float_linear");
|
|
2685
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, floatLinear ? gl.RGBA32F : gl.RGBA16F, width, height, 0, gl.RGBA, gl.FLOAT, data);
|
|
2686
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
2687
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
2688
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
2689
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2690
|
+
const pre = this.prefilterProgram;
|
|
2691
|
+
gl.useProgram(pre.program);
|
|
2692
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2693
|
+
gl.uniform1i(pre.u.uSrc, 0);
|
|
2694
|
+
gl.uniform1f(pre.u.uRoughness, 0);
|
|
2695
|
+
gl.uniform1f(pre.u.uTexelSolidAngle, 4 * Math.PI / (width * height));
|
|
2696
|
+
this.bindTarget(target, 0, w, h);
|
|
2697
|
+
gl.bindTexture(gl.TEXTURE_2D, tmp);
|
|
2698
|
+
gl.uniform2f(pre.u.uSize, w, h);
|
|
2699
|
+
this.fullscreen();
|
|
2700
|
+
gl.deleteTexture(tmp);
|
|
2701
|
+
}
|
|
2702
|
+
// ------------------------------------------------------------------ drawing
|
|
2703
|
+
ensureSize(w, h) {
|
|
2704
|
+
const c = this.canvas;
|
|
2705
|
+
if (c.width >= w && c.height >= h) return;
|
|
2706
|
+
c.width = Math.max(c.width, Math.ceil(w / 128) * 128);
|
|
2707
|
+
c.height = Math.max(c.height, Math.ceil(h / 128) * 128);
|
|
2708
|
+
}
|
|
2709
|
+
/** Draw a badge and copy the result into `target`. */
|
|
2710
|
+
draw(p, target) {
|
|
2711
|
+
const gl = this.gl;
|
|
2712
|
+
if (gl.isContextLost()) return;
|
|
2713
|
+
const sampleScale = Math.max(1, Math.min(2, this.renderLimit / Math.max(p.width, p.height)));
|
|
2714
|
+
const w = Math.round(p.width * sampleScale);
|
|
2715
|
+
const h = Math.round(p.height * sampleScale);
|
|
2716
|
+
this.ensureSize(w, h);
|
|
2717
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2718
|
+
gl.viewport(0, 0, w, h);
|
|
2719
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
2720
|
+
gl.scissor(0, 0, w, h);
|
|
2721
|
+
gl.clearColor(0, 0, 0, 0);
|
|
2722
|
+
gl.clearDepth(1);
|
|
2723
|
+
gl.depthMask(true);
|
|
2724
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
2725
|
+
gl.enable(gl.BLEND);
|
|
2726
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
2727
|
+
gl.enable(gl.DEPTH_TEST);
|
|
2728
|
+
gl.depthFunc(gl.LEQUAL);
|
|
2729
|
+
const fov = 22 * Math.PI / 180;
|
|
2730
|
+
const aspect = w / h;
|
|
2731
|
+
const extent = 0.5 / (1 - 2 * Math.min(0.4, Math.max(0, p.padding)));
|
|
2732
|
+
const fit = aspect >= 1 ? extent : extent / aspect;
|
|
2733
|
+
const dist = fit / Math.tan(fov / 2);
|
|
2734
|
+
const proj = perspective(fov, aspect, Math.max(0.05, dist - 2), dist + 2);
|
|
2735
|
+
const viewProj = multiply(proj, translation(0, 0, -dist));
|
|
2736
|
+
const model = rotation(p.pitch, p.yaw, p.roll);
|
|
2737
|
+
const nMat = normalMatrix(model);
|
|
2738
|
+
const depth = p.locked ? 0 : p.maps.heightScale;
|
|
2739
|
+
const thickness = p.locked ? 0 : p.thickness;
|
|
2740
|
+
const edgeTexels = Math.max(1, p.edge * p.maps.size);
|
|
2741
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2742
|
+
gl.bindTexture(gl.TEXTURE_2D, p.maps.surface);
|
|
2743
|
+
if (p.shadow > 0 && !p.locked) {
|
|
2744
|
+
const s = this.shadowProgram;
|
|
2745
|
+
gl.useProgram(s.program);
|
|
2746
|
+
gl.depthMask(false);
|
|
2747
|
+
gl.disable(gl.CULL_FACE);
|
|
2748
|
+
gl.uniformMatrix4fv(s.u.uModel, false, model);
|
|
2749
|
+
gl.uniformMatrix4fv(s.u.uViewProj, false, viewProj);
|
|
2750
|
+
gl.uniform1i(s.u.uSurface, 0);
|
|
2751
|
+
gl.uniform1f(s.u.uZ, -thickness - 0.01);
|
|
2752
|
+
gl.uniform1f(s.u.uOffset, 0.018);
|
|
2753
|
+
gl.uniform1f(s.u.uSpread, 1);
|
|
2754
|
+
gl.uniform1f(s.u.uOpacity, p.shadow);
|
|
2755
|
+
gl.uniform1f(s.u.uBlur, p.shadowBlur * p.maps.size);
|
|
2756
|
+
gl.bindVertexArray(this.shadowQuad);
|
|
2757
|
+
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
|
2758
|
+
gl.depthMask(true);
|
|
2759
|
+
}
|
|
2760
|
+
const b = this.badgeProgram;
|
|
2761
|
+
gl.useProgram(b.program);
|
|
2762
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
2763
|
+
gl.bindTexture(gl.TEXTURE_2D, p.maps.albedo);
|
|
2764
|
+
gl.activeTexture(gl.TEXTURE2);
|
|
2765
|
+
gl.bindTexture(gl.TEXTURE_2D, p.maps.weights);
|
|
2766
|
+
gl.activeTexture(gl.TEXTURE3);
|
|
2767
|
+
gl.bindTexture(gl.TEXTURE_2D, p.env.spec);
|
|
2768
|
+
gl.activeTexture(gl.TEXTURE4);
|
|
2769
|
+
gl.bindTexture(gl.TEXTURE_2D, p.env.irr);
|
|
2770
|
+
gl.uniform1i(b.u.uSurface, 0);
|
|
2771
|
+
gl.uniform1i(b.u.uAlbedo, 1);
|
|
2772
|
+
gl.uniform1i(b.u.uWeights, 2);
|
|
2773
|
+
gl.uniform1i(b.u.uEnvSpec, 3);
|
|
2774
|
+
gl.uniform1i(b.u.uEnvIrr, 4);
|
|
2775
|
+
gl.uniform1f(b.u.uEnvMaxLod, p.env.maxLod);
|
|
2776
|
+
gl.uniform1f(b.u.uEnvScale, p.env.scale);
|
|
2777
|
+
gl.uniform1f(b.u.uEnvRotation, p.envRotation / (2 * Math.PI));
|
|
2778
|
+
gl.uniformMatrix4fv(b.u.uModel, false, model);
|
|
2779
|
+
gl.uniformMatrix4fv(b.u.uViewProj, false, viewProj);
|
|
2780
|
+
gl.uniformMatrix3fv(b.u.uNormalMatrix, false, nMat);
|
|
2781
|
+
gl.uniform3f(b.u.uCamPos, 0, 0, dist);
|
|
2782
|
+
gl.uniform1f(b.u.uMapSize, p.maps.size);
|
|
2783
|
+
gl.uniform1f(b.u.uPixelScale, sampleScale);
|
|
2784
|
+
gl.uniform1f(b.u.uDepth, depth);
|
|
2785
|
+
gl.uniform1f(b.u.uThickness, thickness);
|
|
2786
|
+
gl.uniform1f(b.u.uEdgeTexels, edgeTexels);
|
|
2787
|
+
gl.uniform1f(b.u.uAo, p.ao);
|
|
2788
|
+
gl.uniform1f(b.u.uExposure, p.exposure);
|
|
2789
|
+
gl.uniform1i(b.u.uToneMap, p.toneMapping);
|
|
2790
|
+
gl.uniform1f(b.u.uLocked, p.locked ? 1 : 0);
|
|
2791
|
+
gl.uniform3fv(b.u.uLockedColor, p.lockedColor);
|
|
2792
|
+
gl.uniform1f(b.u.uShine, p.shine);
|
|
2793
|
+
this.setMaterials(b.u, p.materials);
|
|
2794
|
+
gl.enable(gl.CULL_FACE);
|
|
2795
|
+
gl.cullFace(gl.BACK);
|
|
2796
|
+
gl.bindVertexArray(p.maps.mesh);
|
|
2797
|
+
if (!p.locked) {
|
|
2798
|
+
gl.frontFace(gl.CW);
|
|
2799
|
+
gl.uniform1f(b.u.uSide, -1);
|
|
2800
|
+
gl.drawElements(gl.TRIANGLES, p.maps.faceCount, gl.UNSIGNED_INT, 0);
|
|
2801
|
+
}
|
|
2802
|
+
gl.frontFace(gl.CCW);
|
|
2803
|
+
gl.uniform1f(b.u.uSide, 1);
|
|
2804
|
+
gl.drawElements(gl.TRIANGLES, p.maps.faceCount, gl.UNSIGNED_INT, 0);
|
|
2805
|
+
if (!p.locked && p.maps.sideCount > 0) {
|
|
2806
|
+
gl.uniform1f(b.u.uSide, 0);
|
|
2807
|
+
gl.drawElements(gl.TRIANGLES, p.maps.sideCount, gl.UNSIGNED_INT, p.maps.faceCount * 4);
|
|
2808
|
+
}
|
|
2809
|
+
gl.bindVertexArray(null);
|
|
2810
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
2811
|
+
target.clearRect(0, 0, target.canvas.width, target.canvas.height);
|
|
2812
|
+
target.imageSmoothingEnabled = true;
|
|
2813
|
+
target.imageSmoothingQuality = "high";
|
|
2814
|
+
target.drawImage(this.canvas, 0, this.canvas.height - h, w, h, 0, 0, target.canvas.width, target.canvas.height);
|
|
2815
|
+
}
|
|
2816
|
+
setMaterials(u, mats) {
|
|
2817
|
+
const gl = this.gl;
|
|
2818
|
+
const color = new Float32Array(16);
|
|
2819
|
+
const a = new Float32Array(16);
|
|
2820
|
+
const b = new Float32Array(16);
|
|
2821
|
+
const c = new Float32Array(16);
|
|
2822
|
+
mats.forEach((m, i) => {
|
|
2823
|
+
const lin = linearRgb(m.color);
|
|
2824
|
+
color.set([lin[0], lin[1], lin[2], m.useSvgColor ? 1 : 0], i * 4);
|
|
2825
|
+
a.set([m.metalness, m.roughness, m.clearcoat, m.clearcoatRoughness], i * 4);
|
|
2826
|
+
const f0 = ((m.ior - 1) / (m.ior + 1)) ** 2;
|
|
2827
|
+
const coatedF0 = ((m.ior - 1.5) / (m.ior + 1.5)) ** 2;
|
|
2828
|
+
const coat = Math.min(1, Math.max(0, m.clearcoat));
|
|
2829
|
+
b.set([f0 + (coatedF0 - f0) * coat, m.iridescence, m.iridescenceThickness, m.flakes], i * 4);
|
|
2830
|
+
c.set([m.brushed, BRUSH_DIRS[m.brushDirection] ?? 0, m.flakeScale, m.emissive], i * 4);
|
|
2831
|
+
});
|
|
2832
|
+
gl.uniform4fv(u.uMatColor, color);
|
|
2833
|
+
gl.uniform4fv(u.uMatA, a);
|
|
2834
|
+
gl.uniform4fv(u.uMatB, b);
|
|
2835
|
+
gl.uniform4fv(u.uMatC, c);
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
|
|
2839
|
+
// src/scheduler.ts
|
|
2840
|
+
var active = /* @__PURE__ */ new Set();
|
|
2841
|
+
var rescheduled = /* @__PURE__ */ new Set();
|
|
2842
|
+
var raf = 0;
|
|
2843
|
+
var last = 0;
|
|
2844
|
+
var ticking = false;
|
|
2845
|
+
function tick(now) {
|
|
2846
|
+
raf = 0;
|
|
2847
|
+
ticking = true;
|
|
2848
|
+
const dt = last ? Math.min(0.05, (now - last) / 1e3) : 1 / 60;
|
|
2849
|
+
last = now;
|
|
2850
|
+
rescheduled.clear();
|
|
2851
|
+
for (const task of [...active]) {
|
|
2852
|
+
let keep = false;
|
|
2853
|
+
try {
|
|
2854
|
+
keep = task.frame(dt, now);
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
console.error(error);
|
|
2857
|
+
}
|
|
2858
|
+
if (!keep && !rescheduled.has(task)) active.delete(task);
|
|
2859
|
+
}
|
|
2860
|
+
rescheduled.clear();
|
|
2861
|
+
ticking = false;
|
|
2862
|
+
if (active.size > 0) raf = requestAnimationFrame(tick);
|
|
2863
|
+
else last = 0;
|
|
2864
|
+
}
|
|
2865
|
+
function schedule(task) {
|
|
2866
|
+
active.add(task);
|
|
2867
|
+
if (ticking) {
|
|
2868
|
+
rescheduled.add(task);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
if (!raf && typeof requestAnimationFrame !== "undefined") raf = requestAnimationFrame(tick);
|
|
2872
|
+
}
|
|
2873
|
+
function unschedule(task) {
|
|
2874
|
+
active.delete(task);
|
|
2875
|
+
rescheduled.delete(task);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// src/svg/load.ts
|
|
2879
|
+
var fetched = /* @__PURE__ */ new Map();
|
|
2880
|
+
async function loadSvgSource(input) {
|
|
2881
|
+
if (typeof input !== "string") return Promise.resolve(new XMLSerializer().serializeToString(input));
|
|
2882
|
+
const s = input.trim();
|
|
2883
|
+
if (!s) throw new Error("badgecraft: SVG source is empty");
|
|
2884
|
+
if (s.startsWith("<")) return Promise.resolve(s);
|
|
2885
|
+
if (s.startsWith("data:")) {
|
|
2886
|
+
const comma = s.indexOf(",");
|
|
2887
|
+
if (comma < 0) throw new Error("badgecraft: invalid SVG data URL");
|
|
2888
|
+
const meta = s.slice(0, comma);
|
|
2889
|
+
const body = s.slice(comma + 1);
|
|
2890
|
+
return Promise.resolve(meta.includes(";base64") ? decodeURIComponent(escape(atob(body))) : decodeURIComponent(body));
|
|
2891
|
+
}
|
|
2892
|
+
const key = new URL(s, document.baseURI).href;
|
|
2893
|
+
let pending = fetched.get(key);
|
|
2894
|
+
if (pending) {
|
|
2895
|
+
fetched.delete(key);
|
|
2896
|
+
fetched.set(key, pending);
|
|
2897
|
+
return pending;
|
|
2898
|
+
}
|
|
2899
|
+
const job = fetch(key).then((res) => {
|
|
2900
|
+
if (!res.ok) throw new Error(`badgecraft: failed to load SVG from ${s} (${res.status})`);
|
|
2901
|
+
return res.text();
|
|
2902
|
+
});
|
|
2903
|
+
job.catch(() => fetched.get(key) === job && fetched.delete(key));
|
|
2904
|
+
fetched.set(key, job);
|
|
2905
|
+
while (fetched.size > 32) fetched.delete(fetched.keys().next().value);
|
|
2906
|
+
return job;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
// src/badge.ts
|
|
2910
|
+
var MAX_PIXELS = 2048;
|
|
2911
|
+
function stepSpring(s, dt, stiffness, damping) {
|
|
2912
|
+
const steps = Math.ceil(dt / (1 / 240));
|
|
2913
|
+
const h = dt / steps;
|
|
2914
|
+
for (let i = 0; i < steps; i++) {
|
|
2915
|
+
const a = stiffness * (s.target - s.x) - damping * s.v;
|
|
2916
|
+
s.v += a * h;
|
|
2917
|
+
s.x += s.v * h;
|
|
2918
|
+
}
|
|
2919
|
+
if (Math.abs(s.target - s.x) < 0.02 && Math.abs(s.v) < 0.2) {
|
|
2920
|
+
s.x = s.target;
|
|
2921
|
+
s.v = 0;
|
|
2922
|
+
return false;
|
|
2923
|
+
}
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2926
|
+
var prefersReducedMotion = () => typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2927
|
+
var Badgecraft = class {
|
|
2928
|
+
constructor(host, options) {
|
|
2929
|
+
this.maps = null;
|
|
2930
|
+
this.mapSize = 0;
|
|
2931
|
+
this.pendingSize = 0;
|
|
2932
|
+
this.env = null;
|
|
2933
|
+
this.fallbackImage = null;
|
|
2934
|
+
this.bakeToken = 0;
|
|
2935
|
+
this.envToken = 0;
|
|
2936
|
+
this.visible = true;
|
|
2937
|
+
this.destroyed = false;
|
|
2938
|
+
this.dirty = true;
|
|
2939
|
+
this.isReady = false;
|
|
2940
|
+
this.cleanups = [];
|
|
2941
|
+
// Motion state, in degrees.
|
|
2942
|
+
this.pitch = { x: 0, v: 0, target: 0 };
|
|
2943
|
+
this.yaw = { x: 0, v: 0, target: 0 };
|
|
2944
|
+
this.hover = null;
|
|
2945
|
+
this.drag = null;
|
|
2946
|
+
this.coasting = false;
|
|
2947
|
+
this.spinning = false;
|
|
2948
|
+
this.time = 0;
|
|
2949
|
+
this.shine = 0;
|
|
2950
|
+
this.shineStart = -1;
|
|
2951
|
+
this.host = host;
|
|
2952
|
+
this.options = { ...options };
|
|
2953
|
+
this.resolved = resolveOptions(this.options);
|
|
2954
|
+
this.ready = new Promise((resolve, reject) => {
|
|
2955
|
+
this.resolveReady = resolve;
|
|
2956
|
+
this.rejectReady = reject;
|
|
2957
|
+
});
|
|
2958
|
+
this.ready.catch(() => {
|
|
2959
|
+
});
|
|
2960
|
+
this.canvas = document.createElement("canvas");
|
|
2961
|
+
this.canvas.setAttribute("aria-hidden", "true");
|
|
2962
|
+
this.canvas.style.cssText = "display:block;width:100%;height:100%;";
|
|
2963
|
+
this.canvas.width = 0;
|
|
2964
|
+
this.canvas.height = 0;
|
|
2965
|
+
host.appendChild(this.canvas);
|
|
2966
|
+
if (!host.hasAttribute("role")) host.setAttribute("role", "img");
|
|
2967
|
+
this.ensureHostSize();
|
|
2968
|
+
const ctx = this.canvas.getContext("2d");
|
|
2969
|
+
if (!ctx) {
|
|
2970
|
+
this.canvas.remove();
|
|
2971
|
+
throw new Error("badgecraft: 2D canvas unavailable");
|
|
2972
|
+
}
|
|
2973
|
+
this.ctx = ctx;
|
|
2974
|
+
this.renderer = Renderer.shared();
|
|
2975
|
+
if (this.renderer) {
|
|
2976
|
+
this.cleanups.push(this.renderer.onRestore(() => this.handleContextRestore()));
|
|
2977
|
+
}
|
|
2978
|
+
this.observe();
|
|
2979
|
+
this.bindPointer();
|
|
2980
|
+
this.applyInteractionStyle();
|
|
2981
|
+
this.loadEnvironment();
|
|
2982
|
+
}
|
|
2983
|
+
ensureHostSize() {
|
|
2984
|
+
const host = this.host;
|
|
2985
|
+
if (host.getClientRects().length > 0) {
|
|
2986
|
+
const style = getComputedStyle(host);
|
|
2987
|
+
if (host.clientHeight === 0 && !style.aspectRatio.includes("/")) host.style.aspectRatio = "1 / 1";
|
|
2988
|
+
if (host.clientWidth === 0 && style.display === "inline") host.style.display = "inline-block";
|
|
2989
|
+
if (host.clientWidth === 0 && !host.style.width) host.style.width = "160px";
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
// ------------------------------------------------------------------ public
|
|
2993
|
+
/** Merge new options. Only what changed is recomputed. */
|
|
2994
|
+
update(partial) {
|
|
2995
|
+
this.setOptions({ ...this.options, ...partial });
|
|
2996
|
+
}
|
|
2997
|
+
/** Replace all options. */
|
|
2998
|
+
setOptions(options) {
|
|
2999
|
+
if (this.destroyed) return;
|
|
3000
|
+
const prev = this.resolved;
|
|
3001
|
+
const prevSvg = this.options.svg;
|
|
3002
|
+
this.options = { ...options };
|
|
3003
|
+
this.resolved = resolveOptions(this.options);
|
|
3004
|
+
const next = this.resolved;
|
|
3005
|
+
const needsLargerMaps = next.quality === "auto" && next.render.padding !== prev.render.padding && this.targetMapSize() > this.mapSize * 1.25;
|
|
3006
|
+
if (next.bakeKey !== prev.bakeKey || this.options.svg !== prevSvg || next.quality !== prev.quality || needsLargerMaps) {
|
|
3007
|
+
this.rebuild();
|
|
3008
|
+
}
|
|
3009
|
+
if (next.render.environment.key !== prev.render.environment.key) this.loadEnvironment();
|
|
3010
|
+
if (next.render.interaction !== prev.render.interaction || next.render.locked !== prev.render.locked) {
|
|
3011
|
+
this.stopMotion();
|
|
3012
|
+
this.applyInteractionStyle();
|
|
3013
|
+
}
|
|
3014
|
+
if (next.renderKey !== prev.renderKey) this.invalidate();
|
|
3015
|
+
}
|
|
3016
|
+
spin(turns = 1) {
|
|
3017
|
+
if (this.destroyed || this.resolved.render.locked || !Number.isFinite(turns * 360) || prefersReducedMotion()) return;
|
|
3018
|
+
this.spinning = true;
|
|
3019
|
+
this.coasting = false;
|
|
3020
|
+
this.yaw.target = Math.round(this.yaw.x / 360) * 360 + 360 * turns;
|
|
3021
|
+
this.pitch.target = 0;
|
|
3022
|
+
this.invalidate();
|
|
3023
|
+
}
|
|
3024
|
+
/** Sweep a highlight across the badge once. */
|
|
3025
|
+
glint() {
|
|
3026
|
+
if (this.destroyed || this.resolved.render.locked || prefersReducedMotion()) return;
|
|
3027
|
+
this.shineStart = this.time;
|
|
3028
|
+
this.invalidate();
|
|
3029
|
+
}
|
|
3030
|
+
toDataURL(type = "image/png", quality) {
|
|
3031
|
+
this.renderNow();
|
|
3032
|
+
return this.canvas.toDataURL(type, quality);
|
|
3033
|
+
}
|
|
3034
|
+
toBlob(type = "image/png", quality) {
|
|
3035
|
+
this.renderNow();
|
|
3036
|
+
return new Promise((resolve) => this.canvas.toBlob(resolve, type, quality));
|
|
3037
|
+
}
|
|
3038
|
+
destroy() {
|
|
3039
|
+
if (this.destroyed) return;
|
|
3040
|
+
this.destroyed = true;
|
|
3041
|
+
this.bakeToken++;
|
|
3042
|
+
this.envToken++;
|
|
3043
|
+
if (!this.isReady) this.rejectReady(new DOMException("badgecraft: destroyed before ready", "AbortError"));
|
|
3044
|
+
unschedule(this);
|
|
3045
|
+
this.cleanups.forEach((fn) => fn());
|
|
3046
|
+
if (this.maps && this.renderer) releaseMaps(this.renderer, this.maps);
|
|
3047
|
+
if (this.env && this.renderer) this.renderer.releaseEnvironment(this.env);
|
|
3048
|
+
this.maps = null;
|
|
3049
|
+
this.env = null;
|
|
3050
|
+
this.fallbackImage = null;
|
|
3051
|
+
this.canvas.remove();
|
|
3052
|
+
}
|
|
3053
|
+
// --------------------------------------------------------------- lifecycle
|
|
3054
|
+
observe() {
|
|
3055
|
+
const ro = new ResizeObserver((entries) => {
|
|
3056
|
+
const entry = entries[entries.length - 1];
|
|
3057
|
+
const box = entry.devicePixelContentBoxSize?.[0];
|
|
3058
|
+
const dpr = window.devicePixelRatio || 1;
|
|
3059
|
+
const w = box ? box.inlineSize : Math.round(entry.contentRect.width * dpr);
|
|
3060
|
+
const h = box ? box.blockSize : Math.round(entry.contentRect.height * dpr);
|
|
3061
|
+
this.resize(w, h);
|
|
3062
|
+
});
|
|
3063
|
+
try {
|
|
3064
|
+
ro.observe(this.canvas, { box: "device-pixel-content-box" });
|
|
3065
|
+
} catch {
|
|
3066
|
+
ro.observe(this.canvas);
|
|
3067
|
+
}
|
|
3068
|
+
this.cleanups.push(() => ro.disconnect());
|
|
3069
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
3070
|
+
const io = new IntersectionObserver(
|
|
3071
|
+
(entries) => {
|
|
3072
|
+
this.visible = entries[entries.length - 1].isIntersecting;
|
|
3073
|
+
if (this.visible) this.invalidate();
|
|
3074
|
+
},
|
|
3075
|
+
{ rootMargin: "200px" }
|
|
3076
|
+
);
|
|
3077
|
+
io.observe(this.canvas);
|
|
3078
|
+
this.cleanups.push(() => io.disconnect());
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
resize(w, h) {
|
|
3082
|
+
if (w <= 0 || h <= 0) {
|
|
3083
|
+
if (this.host.getClientRects().length > 0) {
|
|
3084
|
+
requestAnimationFrame(() => {
|
|
3085
|
+
if (!this.destroyed) this.ensureHostSize();
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
const scale = Math.min(1, MAX_PIXELS / Math.max(w, h));
|
|
3091
|
+
w = Math.max(1, Math.round(w * scale));
|
|
3092
|
+
h = Math.max(1, Math.round(h * scale));
|
|
3093
|
+
if (this.canvas.width !== w || this.canvas.height !== h) {
|
|
3094
|
+
this.canvas.width = w;
|
|
3095
|
+
this.canvas.height = h;
|
|
3096
|
+
this.dirty = true;
|
|
3097
|
+
}
|
|
3098
|
+
const wanted = this.targetMapSize();
|
|
3099
|
+
const grow = !this.maps || wanted > this.mapSize * 1.25;
|
|
3100
|
+
if (grow && wanted !== this.pendingSize && this.renderer) this.rebuild();
|
|
3101
|
+
else if (!this.renderer && !this.fallbackImage) this.rebuild();
|
|
3102
|
+
if (this.renderer) this.renderNow();
|
|
3103
|
+
else this.paintFallback();
|
|
3104
|
+
}
|
|
3105
|
+
targetMapSize() {
|
|
3106
|
+
const q = this.resolved.quality;
|
|
3107
|
+
if (q !== "auto") return q;
|
|
3108
|
+
const px = Math.max(this.canvas.width, this.canvas.height) * (1 - 2 * this.resolved.render.padding);
|
|
3109
|
+
return clamp(Math.ceil(px * 1.2 / 64) * 64, 256, 1024);
|
|
3110
|
+
}
|
|
3111
|
+
async rebuild() {
|
|
3112
|
+
const token = ++this.bakeToken;
|
|
3113
|
+
if (this.canvas.width <= 0 && this.resolved.quality === "auto") return;
|
|
3114
|
+
const size = this.targetMapSize();
|
|
3115
|
+
this.pendingSize = size;
|
|
3116
|
+
try {
|
|
3117
|
+
const source = await loadSvgSource(this.options.svg);
|
|
3118
|
+
if (token !== this.bakeToken || this.destroyed) return;
|
|
3119
|
+
if (!this.renderer) {
|
|
3120
|
+
const image = await this.loadFallback(source);
|
|
3121
|
+
if (token !== this.bakeToken || this.destroyed) return;
|
|
3122
|
+
this.fallbackImage = image;
|
|
3123
|
+
this.paintFallback();
|
|
3124
|
+
this.markReady();
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
const { bake: bake2, bakeKey } = this.resolved;
|
|
3128
|
+
const key = mapsKey(source, bakeKey, size);
|
|
3129
|
+
if (this.maps?.key === key) return;
|
|
3130
|
+
const handle = await acquireMaps(this.renderer, key, () => bakeSvg(source, bake2, size));
|
|
3131
|
+
if (token !== this.bakeToken || this.destroyed) {
|
|
3132
|
+
releaseMaps(this.renderer, handle);
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
if (this.maps) releaseMaps(this.renderer, this.maps);
|
|
3136
|
+
this.maps = handle;
|
|
3137
|
+
this.mapSize = size;
|
|
3138
|
+
this.invalidate();
|
|
3139
|
+
} catch (error) {
|
|
3140
|
+
if (token === this.bakeToken) this.fail(error);
|
|
3141
|
+
} finally {
|
|
3142
|
+
if (token === this.bakeToken) this.pendingSize = 0;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
async loadEnvironment() {
|
|
3146
|
+
if (!this.renderer) return;
|
|
3147
|
+
const token = ++this.envToken;
|
|
3148
|
+
const env = this.resolved.render.environment;
|
|
3149
|
+
try {
|
|
3150
|
+
const handle = await this.renderer.acquireEnvironment(env);
|
|
3151
|
+
if (token !== this.envToken || this.destroyed) {
|
|
3152
|
+
this.renderer.releaseEnvironment(handle);
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (this.env) this.renderer.releaseEnvironment(this.env);
|
|
3156
|
+
this.env = handle;
|
|
3157
|
+
this.invalidate();
|
|
3158
|
+
} catch (error) {
|
|
3159
|
+
if (token === this.envToken && !this.renderer.contextLost) this.fail(error);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
handleContextRestore() {
|
|
3163
|
+
if (this.destroyed) return;
|
|
3164
|
+
this.maps = null;
|
|
3165
|
+
this.env = null;
|
|
3166
|
+
this.mapSize = 0;
|
|
3167
|
+
this.loadEnvironment();
|
|
3168
|
+
this.rebuild();
|
|
3169
|
+
}
|
|
3170
|
+
fail(error) {
|
|
3171
|
+
if (this.destroyed) return;
|
|
3172
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3173
|
+
if (!this.isReady) this.rejectReady(err);
|
|
3174
|
+
if (this.options.onError) this.options.onError(err);
|
|
3175
|
+
else console.error(err);
|
|
3176
|
+
}
|
|
3177
|
+
markReady() {
|
|
3178
|
+
if (this.isReady) return;
|
|
3179
|
+
this.isReady = true;
|
|
3180
|
+
this.resolveReady();
|
|
3181
|
+
this.options.onReady?.();
|
|
3182
|
+
}
|
|
3183
|
+
/** Without WebGL2, show the flat artwork. */
|
|
3184
|
+
async loadFallback(source) {
|
|
3185
|
+
const root = parseSvg(source);
|
|
3186
|
+
if (!root.hasAttribute("color")) root.setAttribute("color", this.resolved.bake.color);
|
|
3187
|
+
source = new XMLSerializer().serializeToString(root);
|
|
3188
|
+
const url = URL.createObjectURL(new Blob([source], { type: "image/svg+xml" }));
|
|
3189
|
+
try {
|
|
3190
|
+
const img = new Image();
|
|
3191
|
+
img.src = url;
|
|
3192
|
+
await img.decode();
|
|
3193
|
+
return img;
|
|
3194
|
+
} finally {
|
|
3195
|
+
URL.revokeObjectURL(url);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
paintFallback() {
|
|
3199
|
+
const img = this.fallbackImage;
|
|
3200
|
+
if (!img) return;
|
|
3201
|
+
const { width, height } = this.canvas;
|
|
3202
|
+
this.ctx.clearRect(0, 0, width, height);
|
|
3203
|
+
const s = Math.min(width, height) * (1 - 2 * this.resolved.render.padding);
|
|
3204
|
+
const scale = s / Math.max(img.naturalWidth, img.naturalHeight);
|
|
3205
|
+
const w = img.naturalWidth * scale;
|
|
3206
|
+
const h = img.naturalHeight * scale;
|
|
3207
|
+
this.ctx.drawImage(img, (width - w) / 2, (height - h) / 2, w, h);
|
|
3208
|
+
}
|
|
3209
|
+
// ---------------------------------------------------------------- drawing
|
|
3210
|
+
invalidate() {
|
|
3211
|
+
this.dirty = true;
|
|
3212
|
+
if (!this.destroyed) schedule(this);
|
|
3213
|
+
}
|
|
3214
|
+
renderNow() {
|
|
3215
|
+
const r = this.renderer;
|
|
3216
|
+
if (!r || !this.maps || !this.env || this.destroyed || r.contextLost) return;
|
|
3217
|
+
if (this.canvas.width <= 0) return;
|
|
3218
|
+
const s = this.resolved.render;
|
|
3219
|
+
const rad = Math.PI / 180;
|
|
3220
|
+
r.draw(
|
|
3221
|
+
{
|
|
3222
|
+
width: this.canvas.width,
|
|
3223
|
+
height: this.canvas.height,
|
|
3224
|
+
maps: this.maps.textures,
|
|
3225
|
+
env: this.env.textures,
|
|
3226
|
+
materials: s.materials,
|
|
3227
|
+
pitch: (this.pitch.x + (s.locked ? 0 : s.pose.pitch)) * rad,
|
|
3228
|
+
yaw: (this.yaw.x + (s.locked ? 0 : s.pose.yaw)) * rad,
|
|
3229
|
+
roll: (s.locked ? 0 : s.pose.roll) * rad,
|
|
3230
|
+
thickness: s.thickness,
|
|
3231
|
+
edge: s.edge,
|
|
3232
|
+
ao: s.ao,
|
|
3233
|
+
exposure: s.exposure,
|
|
3234
|
+
toneMapping: s.toneMapping,
|
|
3235
|
+
envRotation: s.envRotation,
|
|
3236
|
+
locked: s.locked,
|
|
3237
|
+
lockedColor: linearRgb(s.lockedColor),
|
|
3238
|
+
shadow: s.shadow,
|
|
3239
|
+
shadowBlur: 0.022,
|
|
3240
|
+
padding: s.padding,
|
|
3241
|
+
shine: this.shine
|
|
3242
|
+
},
|
|
3243
|
+
this.ctx
|
|
3244
|
+
);
|
|
3245
|
+
this.dirty = false;
|
|
3246
|
+
this.markReady();
|
|
3247
|
+
}
|
|
3248
|
+
/** Scheduler callback: advance motion, redraw if needed. */
|
|
3249
|
+
frame(dt) {
|
|
3250
|
+
if (this.destroyed) return false;
|
|
3251
|
+
this.time += dt;
|
|
3252
|
+
const s = this.resolved.render;
|
|
3253
|
+
const reduced = prefersReducedMotion();
|
|
3254
|
+
let moving = false;
|
|
3255
|
+
const before = this.pitch.x + this.yaw.x * 1e3 + this.shine * 1e7;
|
|
3256
|
+
if (!this.drag && !this.spinning && !this.coasting) {
|
|
3257
|
+
let tp = 0;
|
|
3258
|
+
let ty = 0;
|
|
3259
|
+
if (this.hover && s.interaction === "tilt" && !s.locked) {
|
|
3260
|
+
const max = reduced ? s.tiltMax * 0.4 : s.tiltMax;
|
|
3261
|
+
tp = this.hover.y * max;
|
|
3262
|
+
ty = this.hover.x * max;
|
|
3263
|
+
} else if (s.idle === "float" && !reduced && !s.locked) {
|
|
3264
|
+
tp = Math.sin(this.time * 0.9) * 4;
|
|
3265
|
+
ty = Math.sin(this.time * 0.6 + 1.3) * 7;
|
|
3266
|
+
moving = true;
|
|
3267
|
+
}
|
|
3268
|
+
this.pitch.target = tp;
|
|
3269
|
+
this.yaw.target = Math.round(this.yaw.x / 360) * 360 + ty;
|
|
3270
|
+
}
|
|
3271
|
+
if (this.drag) {
|
|
3272
|
+
moving = true;
|
|
3273
|
+
} else if (this.coasting) {
|
|
3274
|
+
this.yaw.x += this.yaw.v * dt;
|
|
3275
|
+
this.yaw.v *= Math.exp(-dt * 1.6);
|
|
3276
|
+
stepSpring(this.pitch, dt, 120, 18);
|
|
3277
|
+
if (Math.abs(this.yaw.v) < 120) {
|
|
3278
|
+
this.coasting = false;
|
|
3279
|
+
this.spinning = true;
|
|
3280
|
+
this.yaw.target = Math.round(this.yaw.x / 360) * 360;
|
|
3281
|
+
}
|
|
3282
|
+
moving = true;
|
|
3283
|
+
} else {
|
|
3284
|
+
const k = this.spinning ? 26 : 140;
|
|
3285
|
+
const c = this.spinning ? 2 * Math.sqrt(k) * 0.82 : 2 * Math.sqrt(k) * 0.72;
|
|
3286
|
+
const a = stepSpring(this.pitch, dt, k, c);
|
|
3287
|
+
const b = stepSpring(this.yaw, dt, k, c);
|
|
3288
|
+
if (!a && !b && this.spinning) {
|
|
3289
|
+
this.spinning = false;
|
|
3290
|
+
this.yaw.x = this.yaw.target = (this.yaw.x % 360 + 360) % 360;
|
|
3291
|
+
}
|
|
3292
|
+
moving = moving || a || b;
|
|
3293
|
+
}
|
|
3294
|
+
if (s.idle === "shine" && !reduced && !s.locked && this.shineStart < 0) {
|
|
3295
|
+
if (this.time % 5 < dt) this.shineStart = this.time;
|
|
3296
|
+
moving = true;
|
|
3297
|
+
}
|
|
3298
|
+
if (this.shineStart >= 0) {
|
|
3299
|
+
const t = (this.time - this.shineStart) / 1.3;
|
|
3300
|
+
this.shine = t >= 1 ? 0 : Math.max(1e-4, t);
|
|
3301
|
+
if (t >= 1) this.shineStart = -1;
|
|
3302
|
+
moving = true;
|
|
3303
|
+
}
|
|
3304
|
+
if (this.pitch.x + this.yaw.x * 1e3 + this.shine * 1e7 !== before) this.dirty = true;
|
|
3305
|
+
if (this.dirty) {
|
|
3306
|
+
if (this.renderer) this.renderNow();
|
|
3307
|
+
else {
|
|
3308
|
+
this.paintFallback();
|
|
3309
|
+
this.dirty = false;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
return moving && this.visible;
|
|
3313
|
+
}
|
|
3314
|
+
// ------------------------------------------------------------ interaction
|
|
3315
|
+
stopMotion() {
|
|
3316
|
+
const id = this.drag?.id;
|
|
3317
|
+
this.drag = null;
|
|
3318
|
+
if (id !== void 0 && this.canvas.hasPointerCapture(id)) this.canvas.releasePointerCapture(id);
|
|
3319
|
+
this.hover = null;
|
|
3320
|
+
this.coasting = false;
|
|
3321
|
+
this.spinning = false;
|
|
3322
|
+
this.pitch.v = this.yaw.v = 0;
|
|
3323
|
+
this.pitch.target = 0;
|
|
3324
|
+
this.yaw.target = Math.round(this.yaw.x / 360) * 360;
|
|
3325
|
+
}
|
|
3326
|
+
applyInteractionStyle() {
|
|
3327
|
+
const mode = this.resolved.render.locked ? "none" : this.resolved.render.interaction;
|
|
3328
|
+
this.canvas.style.touchAction = mode === "drag" ? "none" : "pan-y";
|
|
3329
|
+
this.canvas.style.cursor = mode === "drag" ? "grab" : "";
|
|
3330
|
+
}
|
|
3331
|
+
bindPointer() {
|
|
3332
|
+
const el = this.canvas;
|
|
3333
|
+
const local = (e) => {
|
|
3334
|
+
const rect = el.getBoundingClientRect();
|
|
3335
|
+
return {
|
|
3336
|
+
x: clamp((e.clientX - rect.left) / rect.width * 2 - 1, -1, 1),
|
|
3337
|
+
y: clamp((e.clientY - rect.top) / rect.height * 2 - 1, -1, 1)
|
|
3338
|
+
};
|
|
3339
|
+
};
|
|
3340
|
+
const onMove = (e) => {
|
|
3341
|
+
const mode = this.resolved.render.interaction;
|
|
3342
|
+
if (mode === "tilt" && e.pointerType !== "touch") {
|
|
3343
|
+
this.hover = local(e);
|
|
3344
|
+
this.invalidate();
|
|
3345
|
+
}
|
|
3346
|
+
if (this.drag && e.pointerId === this.drag.id) {
|
|
3347
|
+
const dx = e.clientX - this.drag.x;
|
|
3348
|
+
const dy = e.clientY - this.drag.y;
|
|
3349
|
+
const now = performance.now();
|
|
3350
|
+
const prevYaw = this.yaw.x;
|
|
3351
|
+
this.yaw.x = this.drag.yaw + dx * 0.7;
|
|
3352
|
+
this.pitch.x = clamp(this.drag.pitch + dy * 0.35, -30, 30);
|
|
3353
|
+
const dt = Math.max(1, now - this.drag.t) / 1e3;
|
|
3354
|
+
this.drag.vx = this.drag.vx * 0.6 + (this.yaw.x - prevYaw) / dt * 0.4;
|
|
3355
|
+
this.drag.t = now;
|
|
3356
|
+
this.yaw.v = 0;
|
|
3357
|
+
this.pitch.v = 0;
|
|
3358
|
+
this.invalidate();
|
|
3359
|
+
}
|
|
3360
|
+
};
|
|
3361
|
+
const onLeave = () => {
|
|
3362
|
+
this.hover = null;
|
|
3363
|
+
this.invalidate();
|
|
3364
|
+
};
|
|
3365
|
+
const onDown = (e) => {
|
|
3366
|
+
if (this.drag || e.button !== 0 || this.resolved.render.interaction !== "drag" || this.resolved.render.locked) return;
|
|
3367
|
+
el.setPointerCapture(e.pointerId);
|
|
3368
|
+
this.spinning = false;
|
|
3369
|
+
this.coasting = false;
|
|
3370
|
+
this.drag = { id: e.pointerId, x: e.clientX, y: e.clientY, yaw: this.yaw.x, pitch: this.pitch.x, t: performance.now(), vx: 0 };
|
|
3371
|
+
el.style.cursor = "grabbing";
|
|
3372
|
+
this.invalidate();
|
|
3373
|
+
};
|
|
3374
|
+
const onUp = (e) => {
|
|
3375
|
+
if (!this.drag || e.pointerId !== this.drag.id) return;
|
|
3376
|
+
const idle = performance.now() - this.drag.t > 80;
|
|
3377
|
+
this.yaw.v = idle || e.type !== "pointerup" || prefersReducedMotion() ? 0 : clamp(this.drag.vx, -2400, 2400);
|
|
3378
|
+
this.drag = null;
|
|
3379
|
+
el.style.cursor = "grab";
|
|
3380
|
+
this.pitch.target = 0;
|
|
3381
|
+
if (Math.abs(this.yaw.v) > 120) this.coasting = true;
|
|
3382
|
+
else {
|
|
3383
|
+
this.spinning = true;
|
|
3384
|
+
this.yaw.target = Math.round(this.yaw.x / 360) * 360;
|
|
3385
|
+
}
|
|
3386
|
+
this.invalidate();
|
|
3387
|
+
};
|
|
3388
|
+
el.addEventListener("pointermove", onMove);
|
|
3389
|
+
el.addEventListener("pointerleave", onLeave);
|
|
3390
|
+
el.addEventListener("pointerdown", onDown);
|
|
3391
|
+
el.addEventListener("pointerup", onUp);
|
|
3392
|
+
el.addEventListener("pointercancel", onUp);
|
|
3393
|
+
el.addEventListener("lostpointercapture", onUp);
|
|
3394
|
+
this.cleanups.push(() => {
|
|
3395
|
+
el.removeEventListener("pointermove", onMove);
|
|
3396
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
3397
|
+
el.removeEventListener("pointerdown", onDown);
|
|
3398
|
+
el.removeEventListener("pointerup", onUp);
|
|
3399
|
+
el.removeEventListener("pointercancel", onUp);
|
|
3400
|
+
el.removeEventListener("lostpointercapture", onUp);
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3404
|
+
function createBadge(host, options) {
|
|
3405
|
+
return new Badgecraft(host, options);
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
export { Badgecraft, createBadge, defaults, environments, framePaths, materials, resolveMaterial };
|
|
3409
|
+
//# sourceMappingURL=chunk-RRYBMB25.js.map
|
|
3410
|
+
//# sourceMappingURL=chunk-RRYBMB25.js.map
|