@multiplekex/shallot 0.1.9 → 0.1.10
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/package.json +1 -1
- package/src/core/state.ts +2 -0
- package/src/extras/arrows/index.ts +13 -8
- package/src/extras/gradient/index.ts +1050 -0
- package/src/extras/index.ts +1 -0
- package/src/extras/lines/index.ts +13 -8
- package/src/extras/text/index.ts +11 -7
- package/src/standard/compute/graph.ts +10 -12
- package/src/standard/compute/index.ts +1 -0
- package/src/standard/compute/inspect.ts +34 -52
- package/src/standard/compute/pass.ts +23 -0
- package/src/standard/render/camera.ts +7 -2
- package/src/standard/render/forward.ts +99 -89
- package/src/standard/render/index.ts +68 -44
- package/src/standard/render/mesh/index.ts +190 -19
- package/src/standard/render/pass.ts +63 -0
- package/src/standard/render/postprocess.ts +241 -57
- package/src/standard/render/scene.ts +87 -2
- package/src/standard/render/surface/compile.ts +74 -0
- package/src/standard/render/surface/index.ts +116 -0
- package/src/standard/render/surface/structs.ts +50 -0
- package/src/standard/render/transparent.ts +62 -65
- package/src/standard/render/material/index.ts +0 -92
- package/src/standard/render/opaque.ts +0 -44
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
import { MAX_ENTITIES, resource } from "../../core";
|
|
2
|
+
import { setTraits, type FieldAccessor } from "../../core/component";
|
|
3
|
+
import type { Plugin, State, System } from "../../core";
|
|
4
|
+
import { Compute, type ComputeNode, type ExecutionContext } from "../../standard/compute";
|
|
5
|
+
import { Pass } from "../../standard/render/pass";
|
|
6
|
+
|
|
7
|
+
export type PhysicsConfig = {
|
|
8
|
+
springConstant: number;
|
|
9
|
+
damping: number;
|
|
10
|
+
bounds: { min: number; max: number };
|
|
11
|
+
targetThreshold: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_PHYSICS_CONFIG: PhysicsConfig = {
|
|
15
|
+
springConstant: 0.0002,
|
|
16
|
+
damping: 0.95,
|
|
17
|
+
bounds: { min: -20, max: 120 },
|
|
18
|
+
targetThreshold: 5,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Bubble = {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
vx: number;
|
|
25
|
+
vy: number;
|
|
26
|
+
targetX: number;
|
|
27
|
+
targetY: number;
|
|
28
|
+
color: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type GradientConfig = {
|
|
32
|
+
color1: string;
|
|
33
|
+
color2: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type BubbleConfig = {
|
|
37
|
+
size: number;
|
|
38
|
+
blur: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type TextureConfig = {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
fineNoise: number;
|
|
44
|
+
mediumNoise: number;
|
|
45
|
+
coarseNoise: number;
|
|
46
|
+
largeScale: number;
|
|
47
|
+
fiberIntensity: number;
|
|
48
|
+
opacity: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type OverlayConfig = {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
color: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_GRADIENT_CONFIG: GradientConfig = {
|
|
57
|
+
color1: "#0bff06",
|
|
58
|
+
color2: "#00a4ae",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const DEFAULT_BUBBLE_CONFIG: BubbleConfig = {
|
|
62
|
+
size: 75,
|
|
63
|
+
blur: 10,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const DEFAULT_TEXTURE_CONFIG: TextureConfig = {
|
|
67
|
+
enabled: true,
|
|
68
|
+
fineNoise: 0.2,
|
|
69
|
+
mediumNoise: 0.2,
|
|
70
|
+
coarseNoise: 0.1,
|
|
71
|
+
largeScale: 0.35,
|
|
72
|
+
fiberIntensity: 0.15,
|
|
73
|
+
opacity: 0.4,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const DEFAULT_OVERLAY_CONFIG: OverlayConfig = {
|
|
77
|
+
enabled: true,
|
|
78
|
+
color: "#65FD00",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type RGBA = [number, number, number, number];
|
|
82
|
+
type RGB = [number, number, number];
|
|
83
|
+
|
|
84
|
+
function hashString(str: string): number {
|
|
85
|
+
let hash = 0;
|
|
86
|
+
for (let i = 0; i < str.length; i++) {
|
|
87
|
+
const char = str.charCodeAt(i);
|
|
88
|
+
hash = (hash << 5) - hash + char;
|
|
89
|
+
hash = hash & hash;
|
|
90
|
+
}
|
|
91
|
+
return Math.abs(hash);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hashStringToU32(str: string): number {
|
|
95
|
+
let hash = 0;
|
|
96
|
+
for (let i = 0; i < str.length; i++) {
|
|
97
|
+
const char = str.charCodeAt(i);
|
|
98
|
+
hash = (hash << 5) - hash + char;
|
|
99
|
+
hash = hash >>> 0;
|
|
100
|
+
}
|
|
101
|
+
return hash;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createSeededRandom(seed: number): () => number {
|
|
105
|
+
let value = seed;
|
|
106
|
+
return () => {
|
|
107
|
+
value = (value * 9301 + 49297) % 233280;
|
|
108
|
+
return value / 233280;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function generateHslColor(random: () => number): string {
|
|
113
|
+
const h = Math.floor(random() * 360);
|
|
114
|
+
const s = 70 + Math.floor(random() * 30);
|
|
115
|
+
const l = 50 + Math.floor(random() * 20);
|
|
116
|
+
return `hsl(${h}, ${s}%, ${l}%)`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function generateAngle(key: string): number {
|
|
120
|
+
const hash = hashString(key + "_gradient");
|
|
121
|
+
const random = createSeededRandom(hash);
|
|
122
|
+
return Math.floor(random() * 360);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function generateBubbles(
|
|
126
|
+
key: string,
|
|
127
|
+
count: number = 4,
|
|
128
|
+
physics: PhysicsConfig = DEFAULT_PHYSICS_CONFIG
|
|
129
|
+
): Bubble[] {
|
|
130
|
+
const seed = hashString(key);
|
|
131
|
+
const random = createSeededRandom(seed);
|
|
132
|
+
const range = physics.bounds.max - physics.bounds.min;
|
|
133
|
+
|
|
134
|
+
const bubbles: Bubble[] = [];
|
|
135
|
+
for (let i = 0; i < count; i++) {
|
|
136
|
+
const x = physics.bounds.min + random() * range;
|
|
137
|
+
const y = physics.bounds.min + random() * range;
|
|
138
|
+
const color = generateHslColor(random);
|
|
139
|
+
const vx = (random() - 0.5) * 0.3;
|
|
140
|
+
const vy = (random() - 0.5) * 0.3;
|
|
141
|
+
const targetX = physics.bounds.min + random() * range;
|
|
142
|
+
const targetY = physics.bounds.min + random() * range;
|
|
143
|
+
bubbles.push({ x, y, color, vx, vy, targetX, targetY });
|
|
144
|
+
}
|
|
145
|
+
return bubbles;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clamp(value: number, min: number, max: number): number {
|
|
149
|
+
return Math.min(Math.max(value, min), max);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function distance(x1: number, y1: number, x2: number, y2: number): number {
|
|
153
|
+
const dx = x2 - x1;
|
|
154
|
+
const dy = y2 - y1;
|
|
155
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stepBubble(bubble: Bubble, physics: PhysicsConfig = DEFAULT_PHYSICS_CONFIG): Bubble {
|
|
159
|
+
let { x, y, vx, vy, targetX, targetY, color } = bubble;
|
|
160
|
+
|
|
161
|
+
vx += (targetX - x) * physics.springConstant;
|
|
162
|
+
vy += (targetY - y) * physics.springConstant;
|
|
163
|
+
|
|
164
|
+
vx *= physics.damping;
|
|
165
|
+
vy *= physics.damping;
|
|
166
|
+
|
|
167
|
+
x += vx;
|
|
168
|
+
y += vy;
|
|
169
|
+
|
|
170
|
+
x = clamp(x, physics.bounds.min, physics.bounds.max);
|
|
171
|
+
y = clamp(y, physics.bounds.min, physics.bounds.max);
|
|
172
|
+
|
|
173
|
+
return { x, y, vx, vy, targetX, targetY, color };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function updateBubbleTarget(
|
|
177
|
+
bubble: Bubble,
|
|
178
|
+
random: () => number,
|
|
179
|
+
physics: PhysicsConfig = DEFAULT_PHYSICS_CONFIG
|
|
180
|
+
): Bubble {
|
|
181
|
+
if (distance(bubble.x, bubble.y, bubble.targetX, bubble.targetY) < physics.targetThreshold) {
|
|
182
|
+
const range = physics.bounds.max - physics.bounds.min;
|
|
183
|
+
return {
|
|
184
|
+
...bubble,
|
|
185
|
+
targetX: physics.bounds.min + random() * range,
|
|
186
|
+
targetY: physics.bounds.min + random() * range,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return bubble;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function bubblesMoved(bubbles: Bubble[], threshold: number = 0.001): boolean {
|
|
193
|
+
return bubbles.some((b) => Math.abs(b.vx) > threshold || Math.abs(b.vy) > threshold);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function setBubbleTarget(bubble: Bubble, targetX: number, targetY: number): Bubble {
|
|
197
|
+
return { ...bubble, targetX, targetY };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function applyBubbleImpulse(bubble: Bubble, impulseX: number, impulseY: number): Bubble {
|
|
201
|
+
return { ...bubble, vx: bubble.vx + impulseX, vy: bubble.vy + impulseY };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function setBubblePosition(bubble: Bubble, x: number, y: number): Bubble {
|
|
205
|
+
return { ...bubble, x, y };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function findNearestBubble(bubbles: Bubble[], x: number, y: number): number {
|
|
209
|
+
if (bubbles.length === 0) return -1;
|
|
210
|
+
|
|
211
|
+
let nearestIdx = 0;
|
|
212
|
+
let nearestDist = distance(bubbles[0].x, bubbles[0].y, x, y);
|
|
213
|
+
|
|
214
|
+
for (let i = 1; i < bubbles.length; i++) {
|
|
215
|
+
const dist = distance(bubbles[i].x, bubbles[i].y, x, y);
|
|
216
|
+
if (dist < nearestDist) {
|
|
217
|
+
nearestDist = dist;
|
|
218
|
+
nearestIdx = i;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return nearestIdx;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function screenToGradientSpace(
|
|
226
|
+
screenX: number,
|
|
227
|
+
screenY: number,
|
|
228
|
+
canvasWidth: number,
|
|
229
|
+
canvasHeight: number
|
|
230
|
+
): { x: number; y: number } {
|
|
231
|
+
return {
|
|
232
|
+
x: (screenX / canvasWidth) * 100,
|
|
233
|
+
y: (screenY / canvasHeight) * 100,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function parseColor(color: string): RGBA {
|
|
238
|
+
color = color.trim();
|
|
239
|
+
|
|
240
|
+
if (color.startsWith("#")) {
|
|
241
|
+
return parseHexColor(color);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (color.startsWith("rgb")) {
|
|
245
|
+
return parseRgbColor(color);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (color.startsWith("hsl")) {
|
|
249
|
+
return parseHslColor(color);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (color === "transparent") {
|
|
253
|
+
return [0, 0, 0, 0];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return [0, 0, 0, 255];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function parseHexColor(hex: string): RGBA {
|
|
260
|
+
hex = hex.replace("#", "");
|
|
261
|
+
|
|
262
|
+
if (hex.length === 3) {
|
|
263
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (hex.length === 4) {
|
|
267
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
271
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
272
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
273
|
+
const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) : 255;
|
|
274
|
+
|
|
275
|
+
return [r, g, b, a];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseRgbColor(rgb: string): RGBA {
|
|
279
|
+
const match = rgb.match(/rgba?\(([^)]+)\)/);
|
|
280
|
+
if (!match) return [0, 0, 0, 255];
|
|
281
|
+
|
|
282
|
+
const parts = match[1].split(",").map((s) => s.trim());
|
|
283
|
+
const r = parseInt(parts[0], 10);
|
|
284
|
+
const g = parseInt(parts[1], 10);
|
|
285
|
+
const b = parseInt(parts[2], 10);
|
|
286
|
+
const a = parts[3] !== undefined ? Math.round(parseFloat(parts[3]) * 255) : 255;
|
|
287
|
+
|
|
288
|
+
return [r, g, b, a];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function hslToRgb(h: number, s: number, l: number): RGB {
|
|
292
|
+
h = ((h % 360) + 360) % 360;
|
|
293
|
+
h /= 360;
|
|
294
|
+
|
|
295
|
+
if (s === 0) {
|
|
296
|
+
const gray = Math.round(l * 255);
|
|
297
|
+
return [gray, gray, gray];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const hue2rgb = (p: number, q: number, t: number): number => {
|
|
301
|
+
if (t < 0) t += 1;
|
|
302
|
+
if (t > 1) t -= 1;
|
|
303
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
304
|
+
if (t < 1 / 2) return q;
|
|
305
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
306
|
+
return p;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
310
|
+
const p = 2 * l - q;
|
|
311
|
+
|
|
312
|
+
return [
|
|
313
|
+
Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
|
|
314
|
+
Math.round(hue2rgb(p, q, h) * 255),
|
|
315
|
+
Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseHslColor(hsl: string): RGBA {
|
|
320
|
+
const match = hsl.match(/hsla?\(([^)]+)\)/);
|
|
321
|
+
if (!match) return [0, 0, 0, 255];
|
|
322
|
+
|
|
323
|
+
const parts = match[1].split(",").map((s) => s.trim());
|
|
324
|
+
const h = parseInt(parts[0], 10);
|
|
325
|
+
const s = parseInt(parts[1], 10) / 100;
|
|
326
|
+
const l = parseInt(parts[2], 10) / 100;
|
|
327
|
+
const a = parts[3] !== undefined ? Math.round(parseFloat(parts[3]) * 255) : 255;
|
|
328
|
+
|
|
329
|
+
const [r, g, b] = hslToRgb(h, s, l);
|
|
330
|
+
return [r, g, b, a];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const STRIDE = 16;
|
|
334
|
+
const GradientData = {
|
|
335
|
+
data: new Float32Array(MAX_ENTITIES * STRIDE),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const seeds = new Map<number, string>();
|
|
339
|
+
const color1s = new Map<number, string>();
|
|
340
|
+
const color2s = new Map<number, string>();
|
|
341
|
+
const overlayColors = new Map<number, string>();
|
|
342
|
+
|
|
343
|
+
interface GradientProxy extends Array<number>, FieldAccessor {}
|
|
344
|
+
|
|
345
|
+
function floatProxy(offset: number): GradientProxy {
|
|
346
|
+
const data = GradientData.data;
|
|
347
|
+
|
|
348
|
+
function getValue(eid: number): number {
|
|
349
|
+
return data[eid * STRIDE + offset];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function setValue(eid: number, value: number): void {
|
|
353
|
+
data[eid * STRIDE + offset] = value;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return new Proxy([] as unknown as GradientProxy, {
|
|
357
|
+
get(_, prop) {
|
|
358
|
+
if (prop === "get") return getValue;
|
|
359
|
+
if (prop === "set") return setValue;
|
|
360
|
+
const eid = Number(prop);
|
|
361
|
+
if (Number.isNaN(eid)) return undefined;
|
|
362
|
+
return getValue(eid);
|
|
363
|
+
},
|
|
364
|
+
set(_, prop, value) {
|
|
365
|
+
const eid = Number(prop);
|
|
366
|
+
if (Number.isNaN(eid)) return false;
|
|
367
|
+
setValue(eid, value);
|
|
368
|
+
return true;
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface StringProxy {
|
|
374
|
+
[eid: number]: string | undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function stringProxy(map: Map<number, string>): StringProxy {
|
|
378
|
+
return new Proxy({} as StringProxy, {
|
|
379
|
+
get(_, prop) {
|
|
380
|
+
const eid = Number(prop);
|
|
381
|
+
if (Number.isNaN(eid)) return undefined;
|
|
382
|
+
return map.get(eid);
|
|
383
|
+
},
|
|
384
|
+
set(_, prop, value) {
|
|
385
|
+
const eid = Number(prop);
|
|
386
|
+
if (Number.isNaN(eid)) return false;
|
|
387
|
+
if (value === undefined || value === null) {
|
|
388
|
+
map.delete(eid);
|
|
389
|
+
} else {
|
|
390
|
+
map.set(eid, value);
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export const Gradient = {
|
|
398
|
+
seed: stringProxy(seeds),
|
|
399
|
+
color1: stringProxy(color1s),
|
|
400
|
+
color2: stringProxy(color2s),
|
|
401
|
+
overlayColor: stringProxy(overlayColors),
|
|
402
|
+
angle: floatProxy(0),
|
|
403
|
+
enabled: floatProxy(1),
|
|
404
|
+
bubbleSize: floatProxy(2),
|
|
405
|
+
bubbleBlur: floatProxy(3),
|
|
406
|
+
textureEnabled: floatProxy(4),
|
|
407
|
+
textureOpacity: floatProxy(5),
|
|
408
|
+
overlayEnabled: floatProxy(6),
|
|
409
|
+
bubbleCount: floatProxy(7),
|
|
410
|
+
springConstant: floatProxy(8),
|
|
411
|
+
damping: floatProxy(9),
|
|
412
|
+
boundsMin: floatProxy(10),
|
|
413
|
+
boundsMax: floatProxy(11),
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
setTraits(Gradient, {
|
|
417
|
+
defaults: () => ({
|
|
418
|
+
enabled: 1,
|
|
419
|
+
bubbleSize: DEFAULT_BUBBLE_CONFIG.size,
|
|
420
|
+
bubbleBlur: DEFAULT_BUBBLE_CONFIG.blur,
|
|
421
|
+
textureEnabled: 1,
|
|
422
|
+
textureOpacity: DEFAULT_TEXTURE_CONFIG.opacity,
|
|
423
|
+
overlayEnabled: 1,
|
|
424
|
+
bubbleCount: 4,
|
|
425
|
+
springConstant: DEFAULT_PHYSICS_CONFIG.springConstant,
|
|
426
|
+
damping: DEFAULT_PHYSICS_CONFIG.damping,
|
|
427
|
+
boundsMin: DEFAULT_PHYSICS_CONFIG.bounds.min,
|
|
428
|
+
boundsMax: DEFAULT_PHYSICS_CONFIG.bounds.max,
|
|
429
|
+
}),
|
|
430
|
+
accessors: {
|
|
431
|
+
angle: Gradient.angle,
|
|
432
|
+
enabled: Gradient.enabled,
|
|
433
|
+
bubbleSize: Gradient.bubbleSize,
|
|
434
|
+
bubbleBlur: Gradient.bubbleBlur,
|
|
435
|
+
textureEnabled: Gradient.textureEnabled,
|
|
436
|
+
textureOpacity: Gradient.textureOpacity,
|
|
437
|
+
overlayEnabled: Gradient.overlayEnabled,
|
|
438
|
+
bubbleCount: Gradient.bubbleCount,
|
|
439
|
+
springConstant: Gradient.springConstant,
|
|
440
|
+
damping: Gradient.damping,
|
|
441
|
+
boundsMin: Gradient.boundsMin,
|
|
442
|
+
boundsMax: Gradient.boundsMax,
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
export interface GradientState {
|
|
447
|
+
bubbles: Map<number, Bubble[]>;
|
|
448
|
+
randoms: Map<number, () => number>;
|
|
449
|
+
width: number;
|
|
450
|
+
height: number;
|
|
451
|
+
compositePipeline: GPURenderPipeline | null;
|
|
452
|
+
compositeBindGroup: GPUBindGroup | null;
|
|
453
|
+
compositeBindGroupLayout: GPUBindGroupLayout | null;
|
|
454
|
+
compositeUniformBuffer: GPUBuffer | null;
|
|
455
|
+
gradientAngle: number;
|
|
456
|
+
gradientColor1: [number, number, number, number];
|
|
457
|
+
gradientColor2: [number, number, number, number];
|
|
458
|
+
textureSeed: number;
|
|
459
|
+
textureEnabled: boolean;
|
|
460
|
+
fineNoise: number;
|
|
461
|
+
mediumNoise: number;
|
|
462
|
+
coarseNoise: number;
|
|
463
|
+
largeScale: number;
|
|
464
|
+
fiberIntensity: number;
|
|
465
|
+
textureOpacity: number;
|
|
466
|
+
overlayColor: [number, number, number, number];
|
|
467
|
+
bubbleUniformData: Float32Array | null;
|
|
468
|
+
bubbleRadius: number;
|
|
469
|
+
blurRadius: number;
|
|
470
|
+
bubbleCount: number;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export const GradientResource = resource<GradientState>("gradient");
|
|
474
|
+
|
|
475
|
+
export function clearGradientState(state: State): void {
|
|
476
|
+
const res = GradientResource.from(state);
|
|
477
|
+
if (!res) return;
|
|
478
|
+
res.bubbles.clear();
|
|
479
|
+
res.randoms.clear();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function getBubbles(state: State, eid: number): Bubble[] | undefined {
|
|
483
|
+
const res = GradientResource.from(state);
|
|
484
|
+
if (!res) return undefined;
|
|
485
|
+
return res.bubbles.get(eid);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function setBubbles(state: State, eid: number, bubbles: Bubble[]): void {
|
|
489
|
+
const res = GradientResource.from(state);
|
|
490
|
+
if (!res) return;
|
|
491
|
+
res.bubbles.set(eid, bubbles);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function updateBubble(
|
|
495
|
+
state: State,
|
|
496
|
+
eid: number,
|
|
497
|
+
index: number,
|
|
498
|
+
updater: (bubble: Bubble) => Bubble
|
|
499
|
+
): void {
|
|
500
|
+
const res = GradientResource.from(state);
|
|
501
|
+
if (!res) return;
|
|
502
|
+
const bubbles = res.bubbles.get(eid);
|
|
503
|
+
if (!bubbles || index < 0 || index >= bubbles.length) return;
|
|
504
|
+
bubbles[index] = updater(bubbles[index]);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const COMPOSITE_SHADER = /* wgsl */ `
|
|
508
|
+
struct CompositeUniforms {
|
|
509
|
+
overlayColor: vec4f,
|
|
510
|
+
color1: vec4f,
|
|
511
|
+
color2: vec4f,
|
|
512
|
+
b0_pos: vec2f,
|
|
513
|
+
_b0_pad: vec2f,
|
|
514
|
+
b0_color: vec4f,
|
|
515
|
+
b1_pos: vec2f,
|
|
516
|
+
_b1_pad: vec2f,
|
|
517
|
+
b1_color: vec4f,
|
|
518
|
+
b2_pos: vec2f,
|
|
519
|
+
_b2_pad: vec2f,
|
|
520
|
+
b2_color: vec4f,
|
|
521
|
+
b3_pos: vec2f,
|
|
522
|
+
_b3_pad: vec2f,
|
|
523
|
+
b3_color: vec4f,
|
|
524
|
+
textureSeed: u32,
|
|
525
|
+
textureEnabled: u32,
|
|
526
|
+
fineNoise: f32,
|
|
527
|
+
mediumNoise: f32,
|
|
528
|
+
coarseNoise: f32,
|
|
529
|
+
largeScale: f32,
|
|
530
|
+
fiberIntensity: f32,
|
|
531
|
+
textureOpacity: f32,
|
|
532
|
+
size: vec2f,
|
|
533
|
+
angle: f32,
|
|
534
|
+
bubbleRadius: f32,
|
|
535
|
+
blurRadius: f32,
|
|
536
|
+
bubbleCount: u32,
|
|
537
|
+
_pad: vec2f,
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
struct VertexOutput {
|
|
541
|
+
@builtin(position) position: vec4f,
|
|
542
|
+
@location(0) uv: vec2f,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@vertex
|
|
546
|
+
fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
547
|
+
var positions = array<vec2f, 6>(
|
|
548
|
+
vec2f(-1.0, -1.0),
|
|
549
|
+
vec2f( 1.0, -1.0),
|
|
550
|
+
vec2f(-1.0, 1.0),
|
|
551
|
+
vec2f(-1.0, 1.0),
|
|
552
|
+
vec2f( 1.0, -1.0),
|
|
553
|
+
vec2f( 1.0, 1.0),
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
var uvs = array<vec2f, 6>(
|
|
557
|
+
vec2f(0.0, 1.0),
|
|
558
|
+
vec2f(1.0, 1.0),
|
|
559
|
+
vec2f(0.0, 0.0),
|
|
560
|
+
vec2f(0.0, 0.0),
|
|
561
|
+
vec2f(1.0, 1.0),
|
|
562
|
+
vec2f(1.0, 0.0),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
var output: VertexOutput;
|
|
566
|
+
output.position = vec4f(positions[vertexIndex], 0.0, 1.0);
|
|
567
|
+
output.uv = uvs[vertexIndex];
|
|
568
|
+
return output;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
@group(0) @binding(0) var<uniform> uniforms: CompositeUniforms;
|
|
572
|
+
|
|
573
|
+
fn pcgHash(input: u32) -> u32 {
|
|
574
|
+
var state = input * 747796405u + 2891336453u;
|
|
575
|
+
var word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
|
|
576
|
+
return (word >> 22u) ^ word;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
fn noise3(seed: u32) -> vec3f {
|
|
580
|
+
let h = pcgHash(seed);
|
|
581
|
+
return vec3f(
|
|
582
|
+
f32(h & 0x3FFu) / 1023.0,
|
|
583
|
+
f32((h >> 10u) & 0x3FFu) / 1023.0,
|
|
584
|
+
f32((h >> 20u) & 0x3FFu) / 1023.0
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
fn generateTexture(uv: vec2f) -> f32 {
|
|
589
|
+
if (uniforms.textureEnabled == 0u) { return 0.0; }
|
|
590
|
+
|
|
591
|
+
let x = uv.x * uniforms.size.x;
|
|
592
|
+
let y = uv.y * uniforms.size.y;
|
|
593
|
+
let pixelSeed = pcgHash(uniforms.textureSeed ^ (u32(x) + u32(y) * 65536u));
|
|
594
|
+
let noise = noise3(pixelSeed);
|
|
595
|
+
|
|
596
|
+
let fineNoise = noise.x * uniforms.fineNoise;
|
|
597
|
+
let mediumNoise = noise.y * uniforms.mediumNoise;
|
|
598
|
+
let coarseNoise = noise.z * uniforms.coarseNoise;
|
|
599
|
+
|
|
600
|
+
let largeScaleX = sin(x * 0.005) * cos(y * 0.003);
|
|
601
|
+
let largeScaleY = cos(x * 0.003) * sin(y * 0.007);
|
|
602
|
+
let largeScale = (largeScaleX + largeScaleY) * uniforms.largeScale;
|
|
603
|
+
|
|
604
|
+
let fiberX = sin(x * 0.02) * uniforms.fiberIntensity;
|
|
605
|
+
let fiberY = sin(y * 0.015) * uniforms.fiberIntensity;
|
|
606
|
+
let fiber = (fiberX + fiberY) * 0.1;
|
|
607
|
+
|
|
608
|
+
return clamp(fineNoise + mediumNoise + coarseNoise + largeScale + fiber, 0.0, 1.0) * uniforms.textureOpacity;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fn lum(c: vec3f) -> f32 {
|
|
612
|
+
return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
fn clipColor(c: vec3f) -> vec3f {
|
|
616
|
+
let l = lum(c);
|
|
617
|
+
let n = min(min(c.r, c.g), c.b);
|
|
618
|
+
let x = max(max(c.r, c.g), c.b);
|
|
619
|
+
|
|
620
|
+
var result = c;
|
|
621
|
+
if (n < 0.0) {
|
|
622
|
+
result = vec3f(l) + ((c - vec3f(l)) * l) / (l - n);
|
|
623
|
+
}
|
|
624
|
+
if (x > 1.0) {
|
|
625
|
+
result = vec3f(l) + ((c - vec3f(l)) * (1.0 - l)) / (x - l);
|
|
626
|
+
}
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
fn setLum(c: vec3f, l: f32) -> vec3f {
|
|
631
|
+
let d = l - lum(c);
|
|
632
|
+
return clipColor(c + vec3f(d));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
fn sat(c: vec3f) -> f32 {
|
|
636
|
+
return max(max(c.r, c.g), c.b) - min(min(c.r, c.g), c.b);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
fn setSat(c: vec3f, s: f32) -> vec3f {
|
|
640
|
+
let cmin = min(min(c.r, c.g), c.b);
|
|
641
|
+
let cmax = max(max(c.r, c.g), c.b);
|
|
642
|
+
|
|
643
|
+
if (cmax == cmin) {
|
|
644
|
+
return vec3f(0.0);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Sort channels and apply saturation
|
|
648
|
+
var result = vec3f(0.0);
|
|
649
|
+
let range = cmax - cmin;
|
|
650
|
+
|
|
651
|
+
// Apply proportional saturation to each channel
|
|
652
|
+
result.r = (c.r - cmin) * s / range;
|
|
653
|
+
result.g = (c.g - cmin) * s / range;
|
|
654
|
+
result.b = (c.b - cmin) * s / range;
|
|
655
|
+
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
fn hueBlend(base: vec3f, blend: vec3f) -> vec3f {
|
|
660
|
+
// W3C CSS Compositing spec: Hue = SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb))
|
|
661
|
+
let withSat = setSat(blend, sat(base));
|
|
662
|
+
return setLum(withSat, lum(base));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
fn overlayBlend(base: f32, blend: f32) -> f32 {
|
|
666
|
+
if (base < 0.5) {
|
|
667
|
+
return 2.0 * base * blend;
|
|
668
|
+
}
|
|
669
|
+
return 1.0 - 2.0 * (1.0 - base) * (1.0 - blend);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
fn differenceBlend(base: vec3f, blend: vec3f) -> vec3f {
|
|
673
|
+
return abs(base - blend);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fn alphaOver(dst: vec4f, src: vec4f) -> vec4f {
|
|
677
|
+
if (src.a <= 0.001) { return dst; }
|
|
678
|
+
let outAlpha = src.a + dst.a * (1.0 - src.a);
|
|
679
|
+
if (outAlpha <= 0.0) { return dst; }
|
|
680
|
+
return vec4f(
|
|
681
|
+
(src.rgb * src.a + dst.rgb * dst.a * (1.0 - src.a)) / outAlpha,
|
|
682
|
+
outAlpha
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
fn processBubble(pixelPos: vec2f, pos: vec2f, color: vec4f, twoSigmaSq: f32) -> vec4f {
|
|
687
|
+
let bubbleCenter = pos * uniforms.size;
|
|
688
|
+
let dist = distance(pixelPos, bubbleCenter);
|
|
689
|
+
let alpha = color.a * exp(-(dist * dist) / twoSigmaSq);
|
|
690
|
+
return vec4f(color.rgb, alpha);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
fn generateBubbles(uv: vec2f) -> vec4f {
|
|
694
|
+
let pixelPos = uv * uniforms.size;
|
|
695
|
+
let effectiveRadius = uniforms.bubbleRadius + uniforms.blurRadius;
|
|
696
|
+
let sigma = effectiveRadius / 3.0;
|
|
697
|
+
let twoSigmaSq = 2.0 * sigma * sigma;
|
|
698
|
+
|
|
699
|
+
var result = vec4f(0.0);
|
|
700
|
+
|
|
701
|
+
if (uniforms.bubbleCount > 0u) {
|
|
702
|
+
let b0 = processBubble(pixelPos, uniforms.b0_pos, uniforms.b0_color, twoSigmaSq);
|
|
703
|
+
result = alphaOver(result, b0);
|
|
704
|
+
}
|
|
705
|
+
if (uniforms.bubbleCount > 1u) {
|
|
706
|
+
let b1 = processBubble(pixelPos, uniforms.b1_pos, uniforms.b1_color, twoSigmaSq);
|
|
707
|
+
result = alphaOver(result, b1);
|
|
708
|
+
}
|
|
709
|
+
if (uniforms.bubbleCount > 2u) {
|
|
710
|
+
let b2 = processBubble(pixelPos, uniforms.b2_pos, uniforms.b2_color, twoSigmaSq);
|
|
711
|
+
result = alphaOver(result, b2);
|
|
712
|
+
}
|
|
713
|
+
if (uniforms.bubbleCount > 3u) {
|
|
714
|
+
let b3 = processBubble(pixelPos, uniforms.b3_pos, uniforms.b3_color, twoSigmaSq);
|
|
715
|
+
result = alphaOver(result, b3);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
fn generateGradient(uv: vec2f) -> vec3f {
|
|
722
|
+
let rad = uniforms.angle * 3.14159265 / 180.0;
|
|
723
|
+
let cos_a = cos(rad);
|
|
724
|
+
let sin_a = sin(rad);
|
|
725
|
+
|
|
726
|
+
let cx = uniforms.size.x / 2.0;
|
|
727
|
+
let cy = uniforms.size.y / 2.0;
|
|
728
|
+
let size = min(uniforms.size.x, uniforms.size.y);
|
|
729
|
+
|
|
730
|
+
let px = uv.x * uniforms.size.x;
|
|
731
|
+
let py = uv.y * uniforms.size.y;
|
|
732
|
+
let dx = px - cx;
|
|
733
|
+
let dy = py - cy;
|
|
734
|
+
|
|
735
|
+
let projected = (dx * cos_a + dy * sin_a) / size + 0.5;
|
|
736
|
+
let t = clamp(projected, 0.0, 1.0);
|
|
737
|
+
|
|
738
|
+
return mix(uniforms.color1.rgb, uniforms.color2.rgb, t);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
@fragment
|
|
742
|
+
fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
743
|
+
let gradient = generateGradient(input.uv);
|
|
744
|
+
let bubble = generateBubbles(input.uv);
|
|
745
|
+
|
|
746
|
+
var result = gradient;
|
|
747
|
+
|
|
748
|
+
let bubbleAlpha = bubble.a;
|
|
749
|
+
if (bubbleAlpha > 0.0) {
|
|
750
|
+
let blended = hueBlend(result, bubble.rgb);
|
|
751
|
+
result = mix(result, blended, bubbleAlpha);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let textureAlpha = generateTexture(input.uv);
|
|
755
|
+
if (textureAlpha > 0.0) {
|
|
756
|
+
let blended = vec3f(
|
|
757
|
+
overlayBlend(result.r, 1.0),
|
|
758
|
+
overlayBlend(result.g, 1.0),
|
|
759
|
+
overlayBlend(result.b, 1.0)
|
|
760
|
+
);
|
|
761
|
+
result = mix(result, blended, textureAlpha);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
let overlayAlpha = uniforms.overlayColor.a;
|
|
765
|
+
if (overlayAlpha > 0.0) {
|
|
766
|
+
let blended = differenceBlend(result, uniforms.overlayColor.rgb);
|
|
767
|
+
result = mix(result, blended, overlayAlpha);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return vec4f(result, 1.0);
|
|
771
|
+
}
|
|
772
|
+
`;
|
|
773
|
+
|
|
774
|
+
function createCompositeNode(res: GradientState): ComputeNode {
|
|
775
|
+
return {
|
|
776
|
+
id: "gradient",
|
|
777
|
+
pass: Pass.Opaque,
|
|
778
|
+
inputs: [],
|
|
779
|
+
outputs: [{ id: "scene", access: "write" }],
|
|
780
|
+
|
|
781
|
+
execute(ctx: ExecutionContext) {
|
|
782
|
+
if (res.width === 0 || res.height === 0) return;
|
|
783
|
+
if (!res.bubbleUniformData) return;
|
|
784
|
+
|
|
785
|
+
if (!res.compositeUniformBuffer) {
|
|
786
|
+
res.compositeUniformBuffer = ctx.device.createBuffer({
|
|
787
|
+
label: "composite-uniforms",
|
|
788
|
+
size: 240,
|
|
789
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (!res.compositeBindGroupLayout) {
|
|
794
|
+
res.compositeBindGroupLayout = ctx.device.createBindGroupLayout({
|
|
795
|
+
entries: [
|
|
796
|
+
{
|
|
797
|
+
binding: 0,
|
|
798
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
799
|
+
buffer: { type: "uniform" },
|
|
800
|
+
},
|
|
801
|
+
],
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (!res.compositePipeline) {
|
|
806
|
+
const module = ctx.device.createShaderModule({ code: COMPOSITE_SHADER });
|
|
807
|
+
res.compositePipeline = ctx.device.createRenderPipeline({
|
|
808
|
+
layout: ctx.device.createPipelineLayout({
|
|
809
|
+
bindGroupLayouts: [res.compositeBindGroupLayout],
|
|
810
|
+
}),
|
|
811
|
+
vertex: { module, entryPoint: "vs" },
|
|
812
|
+
fragment: {
|
|
813
|
+
module,
|
|
814
|
+
entryPoint: "fs",
|
|
815
|
+
targets: [{ format: ctx.format }],
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (!res.compositeBindGroup) {
|
|
821
|
+
res.compositeBindGroup = ctx.device.createBindGroup({
|
|
822
|
+
layout: res.compositeBindGroupLayout,
|
|
823
|
+
entries: [{ binding: 0, resource: { buffer: res.compositeUniformBuffer } }],
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const uniformData = new Float32Array(60);
|
|
828
|
+
|
|
829
|
+
uniformData[0] = res.overlayColor[0];
|
|
830
|
+
uniformData[1] = res.overlayColor[1];
|
|
831
|
+
uniformData[2] = res.overlayColor[2];
|
|
832
|
+
uniformData[3] = res.overlayColor[3];
|
|
833
|
+
uniformData[4] = res.gradientColor1[0];
|
|
834
|
+
uniformData[5] = res.gradientColor1[1];
|
|
835
|
+
uniformData[6] = res.gradientColor1[2];
|
|
836
|
+
uniformData[7] = res.gradientColor1[3];
|
|
837
|
+
uniformData[8] = res.gradientColor2[0];
|
|
838
|
+
uniformData[9] = res.gradientColor2[1];
|
|
839
|
+
uniformData[10] = res.gradientColor2[2];
|
|
840
|
+
uniformData[11] = res.gradientColor2[3];
|
|
841
|
+
|
|
842
|
+
for (let i = 0; i < 4; i++) {
|
|
843
|
+
const srcOffset = i * 8;
|
|
844
|
+
const dstOffset = 12 + i * 8;
|
|
845
|
+
uniformData[dstOffset + 0] = res.bubbleUniformData[srcOffset + 0];
|
|
846
|
+
uniformData[dstOffset + 1] = res.bubbleUniformData[srcOffset + 1];
|
|
847
|
+
uniformData[dstOffset + 4] = res.bubbleUniformData[srcOffset + 4];
|
|
848
|
+
uniformData[dstOffset + 5] = res.bubbleUniformData[srcOffset + 5];
|
|
849
|
+
uniformData[dstOffset + 6] = res.bubbleUniformData[srcOffset + 6];
|
|
850
|
+
uniformData[dstOffset + 7] = res.bubbleUniformData[srcOffset + 7];
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const u32View = new Uint32Array(uniformData.buffer);
|
|
854
|
+
u32View[44] = res.textureSeed;
|
|
855
|
+
u32View[45] = res.textureEnabled ? 1 : 0;
|
|
856
|
+
uniformData[46] = res.fineNoise;
|
|
857
|
+
uniformData[47] = res.mediumNoise;
|
|
858
|
+
uniformData[48] = res.coarseNoise;
|
|
859
|
+
uniformData[49] = res.largeScale;
|
|
860
|
+
uniformData[50] = res.fiberIntensity;
|
|
861
|
+
uniformData[51] = res.textureOpacity;
|
|
862
|
+
uniformData[52] = res.width;
|
|
863
|
+
uniformData[53] = res.height;
|
|
864
|
+
uniformData[54] = res.gradientAngle;
|
|
865
|
+
uniformData[55] = res.bubbleRadius;
|
|
866
|
+
uniformData[56] = res.blurRadius;
|
|
867
|
+
u32View[57] = res.bubbleCount;
|
|
868
|
+
|
|
869
|
+
ctx.queue.writeBuffer(res.compositeUniformBuffer, 0, uniformData);
|
|
870
|
+
|
|
871
|
+
const pass = ctx.encoder.beginRenderPass({
|
|
872
|
+
colorAttachments: [
|
|
873
|
+
{
|
|
874
|
+
view: ctx.canvasView,
|
|
875
|
+
loadOp: "clear",
|
|
876
|
+
storeOp: "store",
|
|
877
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
878
|
+
},
|
|
879
|
+
],
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
pass.setPipeline(res.compositePipeline);
|
|
883
|
+
pass.setBindGroup(0, res.compositeBindGroup);
|
|
884
|
+
pass.draw(6);
|
|
885
|
+
pass.end();
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export const GradientSystem: System = {
|
|
891
|
+
group: "simulation",
|
|
892
|
+
|
|
893
|
+
update(state: State) {
|
|
894
|
+
const compute = Compute.from(state);
|
|
895
|
+
const res = GradientResource.from(state);
|
|
896
|
+
if (!compute || !res) return;
|
|
897
|
+
|
|
898
|
+
const canvas = state.canvas;
|
|
899
|
+
if (!canvas) return;
|
|
900
|
+
|
|
901
|
+
if (res.width !== canvas.width || res.height !== canvas.height) {
|
|
902
|
+
res.width = canvas.width;
|
|
903
|
+
res.height = canvas.height;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
for (const eid of state.query([Gradient])) {
|
|
907
|
+
if (!Gradient.enabled[eid]) continue;
|
|
908
|
+
|
|
909
|
+
const bubbleCount = Math.min(
|
|
910
|
+
4,
|
|
911
|
+
Math.max(1, Math.floor(Gradient.bubbleCount[eid] || 4))
|
|
912
|
+
);
|
|
913
|
+
const physics: PhysicsConfig = {
|
|
914
|
+
springConstant:
|
|
915
|
+
Gradient.springConstant[eid] || DEFAULT_PHYSICS_CONFIG.springConstant,
|
|
916
|
+
damping: Gradient.damping[eid] || DEFAULT_PHYSICS_CONFIG.damping,
|
|
917
|
+
bounds: {
|
|
918
|
+
min: Gradient.boundsMin[eid] ?? DEFAULT_PHYSICS_CONFIG.bounds.min,
|
|
919
|
+
max: Gradient.boundsMax[eid] ?? DEFAULT_PHYSICS_CONFIG.bounds.max,
|
|
920
|
+
},
|
|
921
|
+
targetThreshold: DEFAULT_PHYSICS_CONFIG.targetThreshold,
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
let b = res.bubbles.get(eid);
|
|
925
|
+
const seed = Gradient.seed[eid] || String(eid);
|
|
926
|
+
|
|
927
|
+
if (!b || b.length !== bubbleCount) {
|
|
928
|
+
b = generateBubbles(seed, bubbleCount, physics);
|
|
929
|
+
res.bubbles.set(eid, b);
|
|
930
|
+
res.randoms.set(eid, createSeededRandom(hashString(seed + "-runtime")));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const random = res.randoms.get(eid)!;
|
|
934
|
+
for (let i = 0; i < b.length; i++) {
|
|
935
|
+
b[i] = updateBubbleTarget(stepBubble(b[i], physics), random, physics);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (!bubblesMoved(b)) continue;
|
|
939
|
+
|
|
940
|
+
res.bubbleCount = bubbleCount;
|
|
941
|
+
const angle = Gradient.angle[eid] || generateAngle(seed);
|
|
942
|
+
const gradientColor1 = Gradient.color1[eid] || DEFAULT_GRADIENT_CONFIG.color1;
|
|
943
|
+
const gradientColor2 = Gradient.color2[eid] || DEFAULT_GRADIENT_CONFIG.color2;
|
|
944
|
+
|
|
945
|
+
const color1 = parseColor(gradientColor1);
|
|
946
|
+
const color2 = parseColor(gradientColor2);
|
|
947
|
+
res.gradientAngle = angle;
|
|
948
|
+
res.gradientColor1 = [
|
|
949
|
+
color1[0] / 255,
|
|
950
|
+
color1[1] / 255,
|
|
951
|
+
color1[2] / 255,
|
|
952
|
+
color1[3] / 255,
|
|
953
|
+
];
|
|
954
|
+
res.gradientColor2 = [
|
|
955
|
+
color2[0] / 255,
|
|
956
|
+
color2[1] / 255,
|
|
957
|
+
color2[2] / 255,
|
|
958
|
+
color2[3] / 255,
|
|
959
|
+
];
|
|
960
|
+
|
|
961
|
+
const bubbleSize = Gradient.bubbleSize[eid] ?? DEFAULT_BUBBLE_CONFIG.size;
|
|
962
|
+
const bubbleBlur = Gradient.bubbleBlur[eid] ?? DEFAULT_BUBBLE_CONFIG.blur;
|
|
963
|
+
|
|
964
|
+
const bubbleUniformData = new Float32Array(4 * 8);
|
|
965
|
+
for (let i = 0; i < b.length; i++) {
|
|
966
|
+
const offset = i * 8;
|
|
967
|
+
bubbleUniformData[offset + 0] = b[i].x / 100;
|
|
968
|
+
bubbleUniformData[offset + 1] = b[i].y / 100;
|
|
969
|
+
const color = parseColor(b[i].color);
|
|
970
|
+
bubbleUniformData[offset + 4] = color[0] / 255;
|
|
971
|
+
bubbleUniformData[offset + 5] = color[1] / 255;
|
|
972
|
+
bubbleUniformData[offset + 6] = color[2] / 255;
|
|
973
|
+
bubbleUniformData[offset + 7] = color[3] / 255;
|
|
974
|
+
}
|
|
975
|
+
res.bubbleUniformData = bubbleUniformData;
|
|
976
|
+
res.bubbleRadius = (bubbleSize / 100) * Math.min(res.width, res.height);
|
|
977
|
+
res.blurRadius = (bubbleBlur / 100) * Math.min(res.width, res.height);
|
|
978
|
+
|
|
979
|
+
const textureEnabled = Gradient.textureEnabled[eid] !== 0;
|
|
980
|
+
res.textureEnabled = textureEnabled;
|
|
981
|
+
if (textureEnabled) {
|
|
982
|
+
res.textureSeed = hashStringToU32(seed + "_texture");
|
|
983
|
+
res.textureOpacity = Gradient.textureOpacity[eid] || DEFAULT_TEXTURE_CONFIG.opacity;
|
|
984
|
+
res.fineNoise = DEFAULT_TEXTURE_CONFIG.fineNoise;
|
|
985
|
+
res.mediumNoise = DEFAULT_TEXTURE_CONFIG.mediumNoise;
|
|
986
|
+
res.coarseNoise = DEFAULT_TEXTURE_CONFIG.coarseNoise;
|
|
987
|
+
res.largeScale = DEFAULT_TEXTURE_CONFIG.largeScale;
|
|
988
|
+
res.fiberIntensity = DEFAULT_TEXTURE_CONFIG.fiberIntensity;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const overlayEnabled = Gradient.overlayEnabled[eid] !== 0;
|
|
992
|
+
if (overlayEnabled) {
|
|
993
|
+
const overlayColor = parseColor(
|
|
994
|
+
Gradient.overlayColor[eid] || DEFAULT_OVERLAY_CONFIG.color
|
|
995
|
+
);
|
|
996
|
+
res.overlayColor = [
|
|
997
|
+
overlayColor[0] / 255,
|
|
998
|
+
overlayColor[1] / 255,
|
|
999
|
+
overlayColor[2] / 255,
|
|
1000
|
+
overlayColor[3] / 255,
|
|
1001
|
+
];
|
|
1002
|
+
} else {
|
|
1003
|
+
res.overlayColor = [0, 0, 0, 0];
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
export { hashString, createSeededRandom, generateBubbles, stepBubble, parseColor };
|
|
1010
|
+
|
|
1011
|
+
export const GradientPlugin: Plugin = {
|
|
1012
|
+
systems: [GradientSystem],
|
|
1013
|
+
components: { Gradient },
|
|
1014
|
+
|
|
1015
|
+
initialize(state: State) {
|
|
1016
|
+
const gradientState: GradientState = {
|
|
1017
|
+
bubbles: new Map(),
|
|
1018
|
+
randoms: new Map(),
|
|
1019
|
+
width: 0,
|
|
1020
|
+
height: 0,
|
|
1021
|
+
compositePipeline: null,
|
|
1022
|
+
compositeBindGroup: null,
|
|
1023
|
+
compositeBindGroupLayout: null,
|
|
1024
|
+
compositeUniformBuffer: null,
|
|
1025
|
+
gradientAngle: 0,
|
|
1026
|
+
gradientColor1: [0, 0, 0, 1],
|
|
1027
|
+
gradientColor2: [0, 0, 0, 1],
|
|
1028
|
+
textureSeed: 0,
|
|
1029
|
+
textureEnabled: true,
|
|
1030
|
+
fineNoise: DEFAULT_TEXTURE_CONFIG.fineNoise,
|
|
1031
|
+
mediumNoise: DEFAULT_TEXTURE_CONFIG.mediumNoise,
|
|
1032
|
+
coarseNoise: DEFAULT_TEXTURE_CONFIG.coarseNoise,
|
|
1033
|
+
largeScale: DEFAULT_TEXTURE_CONFIG.largeScale,
|
|
1034
|
+
fiberIntensity: DEFAULT_TEXTURE_CONFIG.fiberIntensity,
|
|
1035
|
+
textureOpacity: DEFAULT_TEXTURE_CONFIG.opacity,
|
|
1036
|
+
overlayColor: [0, 0, 0, 0],
|
|
1037
|
+
bubbleUniformData: null,
|
|
1038
|
+
bubbleRadius: 0,
|
|
1039
|
+
blurRadius: 0,
|
|
1040
|
+
bubbleCount: 4,
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
state.setResource(GradientResource, gradientState);
|
|
1044
|
+
|
|
1045
|
+
const compute = Compute.from(state);
|
|
1046
|
+
if (compute) {
|
|
1047
|
+
compute.graph.add(createCompositeNode(gradientState));
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
};
|