@bigmistqke/view.gl 0.2.0 → 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/README.md +82 -27
- package/dev/dist/404.html +19 -0
- package/dev/dist/assets/bezier-DrA4cU_H.js +292 -0
- package/dev/dist/assets/game-of-life-BYlzWhEL.js +186 -0
- package/dev/dist/assets/glsl-composition-D_BaFFij.js +97 -0
- package/dev/dist/assets/grid-DfXHNLBm.js +181 -0
- package/dev/dist/assets/index-C2N3u8wj.css +12 -0
- package/dev/dist/assets/index-D-N0FDYs.js +110 -0
- package/dev/dist/assets/main-CSE2fki3.js +32 -0
- package/dev/dist/assets/overview-IeUhUflE.js +80 -0
- package/dev/dist/assets/pix-sim-DMJJA_WO.js +500 -0
- package/dev/dist/assets/ray-casting-DTQetDPd.js +411 -0
- package/dev/dist/assets/ray-casting-modules-Dkfw4tdc.js +765 -0
- package/dev/dist/assets/sdf-CS0uBonx.js +66 -0
- package/dev/dist/assets/symbol-BL_7GJ0M.js +67 -0
- package/dev/dist/assets/tag-C0Vskpp8.js +833 -0
- package/dev/dist/assets/tetris-D8mokodr.js +344 -0
- package/dev/dist/assets/utils-2dzuv_bW.js +62 -0
- package/dev/dist/index.html +51 -0
- package/dist/chunk-BZDGEIOT.mjs +278 -0
- package/dist/chunk-BZDGEIOT.mjs.map +1 -0
- package/dist/{chunk-EYXOSM7B.mjs → chunk-X4M3LV4R.mjs} +60 -133
- package/dist/chunk-X4M3LV4R.mjs.map +1 -0
- package/dist/gl.d.mts +2 -2
- package/dist/gl.mjs +1 -1
- package/dist/index.d.mts +13 -4
- package/dist/index.mjs +7 -2
- package/dist/tag.d.mts +6 -1
- package/dist/tag.mjs +13 -5
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-CyGyGi18.d.mts → types-CNdqh7tB.d.mts} +34 -4
- package/package.json +1 -1
- package/src/gl.ts +13 -2
- package/src/index.ts +55 -39
- package/src/tag.ts +18 -2
- package/src/types.ts +33 -3
- package/src/utils.ts +45 -10
- package/test/index.test.ts +111 -29
- package/test/mocks/webgl.ts +14 -0
- package/test/tag.test.ts +41 -0
- package/dist/chunk-CJTT7WPV.mjs +0 -75
- package/dist/chunk-CJTT7WPV.mjs.map +0 -1
- package/dist/chunk-EYXOSM7B.mjs.map +0 -1
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
import { c as compile, u as uniform, g as glsl } from './tag-C0Vskpp8.js';
|
|
2
|
+
import { d as dom } from './utils-2dzuv_bW.js';
|
|
3
|
+
|
|
4
|
+
const NUM_SPHERES = 8;
|
|
5
|
+
const NUM_BOXES = 3;
|
|
6
|
+
const NUM_TORI = 4;
|
|
7
|
+
const RAY_MARCH_ITERATIONS = 129;
|
|
8
|
+
const RAY_HIT_THRESHOLD = 1e-3;
|
|
9
|
+
const NORMAL_EPSILON = 1e-3;
|
|
10
|
+
const SPHERE_FLOAT_ARRAYS = {
|
|
11
|
+
positions: new Float32Array(
|
|
12
|
+
[
|
|
13
|
+
[0, 0, 0, 1.2],
|
|
14
|
+
[1.5, 0.5, -1, 1],
|
|
15
|
+
[-1.5, -0.5, 0.5, 1],
|
|
16
|
+
[0.5, 1.5, -0.5, 0.9],
|
|
17
|
+
[-1, -1.5, 1, 0.8],
|
|
18
|
+
[2, -1, -1.5, 0.7],
|
|
19
|
+
[-2, 1, 1.5, 0.9],
|
|
20
|
+
[0, -0.5, -2, 1]
|
|
21
|
+
].flat()
|
|
22
|
+
),
|
|
23
|
+
colors: new Float32Array(
|
|
24
|
+
[
|
|
25
|
+
[1, 0.3, 0.3],
|
|
26
|
+
// Red
|
|
27
|
+
[0.3, 1, 0.3],
|
|
28
|
+
// Green
|
|
29
|
+
[0.3, 0.3, 1],
|
|
30
|
+
// Blue
|
|
31
|
+
[1, 1, 0.3],
|
|
32
|
+
// Yellow
|
|
33
|
+
[1, 0.3, 1],
|
|
34
|
+
// Magenta
|
|
35
|
+
[0.3, 1, 1],
|
|
36
|
+
// Cyan
|
|
37
|
+
[1, 0.7, 0.3],
|
|
38
|
+
// Orange
|
|
39
|
+
[0.7, 0.3, 1]
|
|
40
|
+
// Purple
|
|
41
|
+
].flat()
|
|
42
|
+
)
|
|
43
|
+
};
|
|
44
|
+
const BOX_FLOAT_ARRAYS = {
|
|
45
|
+
positions: new Float32Array(
|
|
46
|
+
[
|
|
47
|
+
[1, 0, -0.5, 0.9],
|
|
48
|
+
[-1, 0, 0.5, 0.8],
|
|
49
|
+
[0, -1, -1, 0.9]
|
|
50
|
+
].flat()
|
|
51
|
+
),
|
|
52
|
+
colors: new Float32Array(
|
|
53
|
+
[
|
|
54
|
+
[0.8, 0.8, 0.2],
|
|
55
|
+
// Yellow-green
|
|
56
|
+
[0.2, 0.8, 0.8],
|
|
57
|
+
// Cyan
|
|
58
|
+
[0.8, 0.2, 0.8]
|
|
59
|
+
// Magenta
|
|
60
|
+
].flat()
|
|
61
|
+
)
|
|
62
|
+
};
|
|
63
|
+
const TORUS_FLOAT_ARRAYS = {
|
|
64
|
+
positions: new Float32Array(
|
|
65
|
+
[
|
|
66
|
+
[0, 1.5, 0],
|
|
67
|
+
[-1.5, 0, 0],
|
|
68
|
+
[1.5, -0.5, -0.5],
|
|
69
|
+
[0, -1.5, 1]
|
|
70
|
+
].flat()
|
|
71
|
+
),
|
|
72
|
+
radii: new Float32Array(
|
|
73
|
+
[
|
|
74
|
+
[0.8, 0.3],
|
|
75
|
+
[0.6, 0.2],
|
|
76
|
+
[0.7, 0.25],
|
|
77
|
+
[0.5, 0.2]
|
|
78
|
+
].flat()
|
|
79
|
+
),
|
|
80
|
+
colors: new Float32Array(
|
|
81
|
+
[
|
|
82
|
+
[0.9, 0.5, 0.2],
|
|
83
|
+
// Orange
|
|
84
|
+
[0.2, 0.9, 0.5],
|
|
85
|
+
// Green
|
|
86
|
+
[0.5, 0.2, 0.9],
|
|
87
|
+
// Purple
|
|
88
|
+
[0.9, 0.9, 0.2]
|
|
89
|
+
// Yellow
|
|
90
|
+
].flat()
|
|
91
|
+
)
|
|
92
|
+
};
|
|
93
|
+
let SHAPE_COUNTER = 0;
|
|
94
|
+
class Camera {
|
|
95
|
+
theta = 0;
|
|
96
|
+
// Horizontal angle
|
|
97
|
+
phi = Math.PI / 4;
|
|
98
|
+
// Vertical angle
|
|
99
|
+
targetTheta = 0;
|
|
100
|
+
targetPhi = Math.PI / 4;
|
|
101
|
+
radius = 6;
|
|
102
|
+
isDragging = false;
|
|
103
|
+
lastX = 0;
|
|
104
|
+
lastY = 0;
|
|
105
|
+
position = [0, 0, 0];
|
|
106
|
+
direction = [0, 0, -1];
|
|
107
|
+
up = [0, 1, 0];
|
|
108
|
+
right = [1, 0, 0];
|
|
109
|
+
template = glsl`
|
|
110
|
+
${uniform.vec3("u_cameraPos")}
|
|
111
|
+
${uniform.vec3("u_cameraDir")}
|
|
112
|
+
${uniform.vec3("u_cameraUp")}
|
|
113
|
+
${uniform.vec3("u_cameraRight")}`;
|
|
114
|
+
view;
|
|
115
|
+
startDrag(x, y) {
|
|
116
|
+
this.isDragging = true;
|
|
117
|
+
this.lastX = x;
|
|
118
|
+
this.lastY = y;
|
|
119
|
+
}
|
|
120
|
+
drag(x, y) {
|
|
121
|
+
if (!this.isDragging) return;
|
|
122
|
+
const deltaX = x - this.lastX;
|
|
123
|
+
const deltaY = y - this.lastY;
|
|
124
|
+
this.targetTheta += deltaX * 0.01;
|
|
125
|
+
this.targetPhi = Math.max(0.1, Math.min(Math.PI - 0.1, this.targetPhi - deltaY * 0.01));
|
|
126
|
+
this.lastX = x;
|
|
127
|
+
this.lastY = y;
|
|
128
|
+
}
|
|
129
|
+
endDrag() {
|
|
130
|
+
this.isDragging = false;
|
|
131
|
+
}
|
|
132
|
+
zoom(delta) {
|
|
133
|
+
this.radius = Math.max(2, Math.min(15, this.radius + delta * 0.01));
|
|
134
|
+
}
|
|
135
|
+
update() {
|
|
136
|
+
this.theta += (this.targetTheta - this.theta) * 0.1;
|
|
137
|
+
this.phi += (this.targetPhi - this.phi) * 0.1;
|
|
138
|
+
const x = this.radius * Math.sin(this.phi) * Math.cos(this.theta);
|
|
139
|
+
const y = this.radius * Math.cos(this.phi);
|
|
140
|
+
const z = this.radius * Math.sin(this.phi) * Math.sin(this.theta);
|
|
141
|
+
this.position = [x, y, z];
|
|
142
|
+
const target = [0, 0, 0];
|
|
143
|
+
const dir = [
|
|
144
|
+
target[0] - this.position[0],
|
|
145
|
+
target[1] - this.position[1],
|
|
146
|
+
target[2] - this.position[2]
|
|
147
|
+
];
|
|
148
|
+
const dirLength = Math.sqrt(dir[0] ** 2 + dir[1] ** 2 + dir[2] ** 2);
|
|
149
|
+
this.direction = [dir[0] / dirLength, dir[1] / dirLength, dir[2] / dirLength];
|
|
150
|
+
const up = [0, 1, 0];
|
|
151
|
+
this.right = [
|
|
152
|
+
this.direction[1] * up[2] - this.direction[2] * up[1],
|
|
153
|
+
this.direction[2] * up[0] - this.direction[0] * up[2],
|
|
154
|
+
this.direction[0] * up[1] - this.direction[1] * up[0]
|
|
155
|
+
];
|
|
156
|
+
this.up = [
|
|
157
|
+
this.right[1] * this.direction[2] - this.right[2] * this.direction[1],
|
|
158
|
+
this.right[2] * this.direction[0] - this.right[0] * this.direction[2],
|
|
159
|
+
this.right[0] * this.direction[1] - this.right[1] * this.direction[0]
|
|
160
|
+
];
|
|
161
|
+
if (this.view) {
|
|
162
|
+
this.view.uniforms.u_cameraPos.set(this.position[0], this.position[1], this.position[2]);
|
|
163
|
+
this.view.uniforms.u_cameraDir.set(this.direction[0], this.direction[1], this.direction[2]);
|
|
164
|
+
this.view.uniforms.u_cameraUp.set(this.up[0], this.up[1], this.up[2]);
|
|
165
|
+
this.view.uniforms.u_cameraRight.set(this.right[0], this.right[1], this.right[2]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
const canvas = dom("canvas", { parentElement: document.body });
|
|
171
|
+
const gl = canvas.getContext("webgl2");
|
|
172
|
+
let blendFactor = 0.8;
|
|
173
|
+
class Shape {
|
|
174
|
+
id = SHAPE_COUNTER++;
|
|
175
|
+
view;
|
|
176
|
+
calculateIntersection = Symbol();
|
|
177
|
+
calculateColor = Symbol();
|
|
178
|
+
calculateSDF = Symbol();
|
|
179
|
+
u_data = Symbol();
|
|
180
|
+
u_colors = Symbol();
|
|
181
|
+
u_positions = Symbol();
|
|
182
|
+
u_radii = Symbol();
|
|
183
|
+
setData(data) {
|
|
184
|
+
this.view?.uniforms[this.u_data]?.set(data);
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
setColors(data) {
|
|
188
|
+
this.view?.uniforms[this.u_colors]?.set(data);
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
class Sphere extends Shape {
|
|
193
|
+
sphereIntersection = Symbol();
|
|
194
|
+
sphereNormal = Symbol();
|
|
195
|
+
sphereSDF = Symbol();
|
|
196
|
+
template = glsl`
|
|
197
|
+
struct Sphere {
|
|
198
|
+
vec3 position;
|
|
199
|
+
float radius;
|
|
200
|
+
vec3 color;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
${uniform.vec4(this.u_data, { size: NUM_SPHERES })}
|
|
204
|
+
${uniform.vec3(this.u_colors, { size: NUM_SPHERES })}
|
|
205
|
+
|
|
206
|
+
float ${this.sphereIntersection}(in vec3 rayOrigin, in vec3 rayDirection, in Sphere sphere)
|
|
207
|
+
{
|
|
208
|
+
// We're shooting a ray from the camera and checking if it hits a sphere
|
|
209
|
+
// Think of it like shining a laser pointer at a ball
|
|
210
|
+
|
|
211
|
+
vec3 rayToSphere = rayOrigin - sphere.position;
|
|
212
|
+
|
|
213
|
+
// Step 1: Find the closest point on the ray to the sphere's position
|
|
214
|
+
// This is like finding where the laser beam passes nearest to the ball's position
|
|
215
|
+
float closestApproach = dot(rayToSphere, rayDirection);
|
|
216
|
+
|
|
217
|
+
// Step 2: Check if we're already inside the sphere
|
|
218
|
+
// If negative, we're outside and pointing away from sphere position
|
|
219
|
+
float rayStartDistanceSquared = dot(rayToSphere, rayToSphere) - sphere.radius * sphere.radius;
|
|
220
|
+
|
|
221
|
+
// Step 3: Use geometry to check if the ray can reach the sphere
|
|
222
|
+
// If this value is negative, the ray misses the sphere entirely
|
|
223
|
+
float canReachSphere = closestApproach * closestApproach - rayStartDistanceSquared;
|
|
224
|
+
|
|
225
|
+
if(canReachSphere < 0.0) {
|
|
226
|
+
return -1.0; // Ray misses the sphere
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Step 4: Calculate how far along the ray we hit the sphere
|
|
230
|
+
// We want the nearest intersection point (there can be two when ray goes through)
|
|
231
|
+
float hitDistance = -closestApproach - sqrt(canReachSphere);
|
|
232
|
+
|
|
233
|
+
return hitDistance;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Calculate normal vector at hit point on sphere surface
|
|
237
|
+
vec3 ${this.sphereNormal}(in vec3 hitPosition, in Sphere sphere)
|
|
238
|
+
{
|
|
239
|
+
return (hitPosition - sphere.position) / sphere.radius;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Find closest sphere intersection, returns distance and sets hitIndex and hitType
|
|
243
|
+
float ${this.calculateIntersection}(in vec3 rayOrigin, in vec3 rayDirection, in float maxDistance, out int hitIndex, out int hitType)
|
|
244
|
+
{
|
|
245
|
+
float closestDistance = maxDistance;
|
|
246
|
+
hitIndex = -1;
|
|
247
|
+
hitType = -1;
|
|
248
|
+
|
|
249
|
+
for(int i = 0; i < ${NUM_SPHERES}; i++) {
|
|
250
|
+
Sphere sphere = Sphere(${this.u_data}[i].xyz, ${this.u_data}[i].w, ${this.u_colors}[i]);
|
|
251
|
+
float distance = ${this.sphereIntersection}(rayOrigin, rayDirection, sphere);
|
|
252
|
+
|
|
253
|
+
if(distance > 0.0 && distance < closestDistance) {
|
|
254
|
+
closestDistance = distance;
|
|
255
|
+
hitIndex = i;
|
|
256
|
+
hitType = ${this.id};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return hitIndex >= 0 ? closestDistance : -1.0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Calculate sphere normal and color at hit point
|
|
264
|
+
void ${this.calculateColor}(in vec3 hitPosition, in int hitIndex, out vec3 normal, out vec3 color)
|
|
265
|
+
{
|
|
266
|
+
Sphere hitSphere = Sphere(${this.u_data}[hitIndex].xyz, ${this.u_data}[hitIndex].w, ${this.u_colors}[hitIndex]);
|
|
267
|
+
normal = ${this.sphereNormal}(hitPosition, hitSphere);
|
|
268
|
+
color = ${this.u_colors}[hitIndex];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Signed distance function for sphere
|
|
272
|
+
float ${this.sphereSDF}(in vec3 point, in vec3 center, in float radius)
|
|
273
|
+
{
|
|
274
|
+
return length(point - center) - radius;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Calculate SDF for all spheres
|
|
278
|
+
float ${this.calculateSDF}(in vec3 point, out int nearestIndex, out float minDistance)
|
|
279
|
+
{
|
|
280
|
+
minDistance = 1000.0;
|
|
281
|
+
nearestIndex = -1;
|
|
282
|
+
|
|
283
|
+
for(int i = 0; i < ${NUM_SPHERES}; i++) {
|
|
284
|
+
float distance = ${this.sphereSDF}(point, ${this.u_data}[i].xyz, ${this.u_data}[i].w);
|
|
285
|
+
|
|
286
|
+
if(distance < minDistance) {
|
|
287
|
+
minDistance = distance;
|
|
288
|
+
nearestIndex = i;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return minDistance;
|
|
293
|
+
}`;
|
|
294
|
+
}
|
|
295
|
+
class Box extends Shape {
|
|
296
|
+
boxIntersection = Symbol();
|
|
297
|
+
boxNormal = Symbol();
|
|
298
|
+
boxSDF = Symbol();
|
|
299
|
+
template = glsl`
|
|
300
|
+
|
|
301
|
+
#define NUM_BOXES ${NUM_BOXES}
|
|
302
|
+
|
|
303
|
+
struct Box {
|
|
304
|
+
vec3 position;
|
|
305
|
+
float size;
|
|
306
|
+
vec3 color;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
${uniform.vec4(this.u_data, { size: NUM_BOXES })}
|
|
310
|
+
${uniform.vec3(this.u_colors, { size: NUM_BOXES })}
|
|
311
|
+
|
|
312
|
+
float ${this.boxIntersection}(in vec3 rayOrigin, in vec3 rayDirection, in Box box)
|
|
313
|
+
{
|
|
314
|
+
// Ray-box intersection using slab method
|
|
315
|
+
// A box is defined by 6 planes, we find where the ray enters and exits each pair of parallel planes
|
|
316
|
+
|
|
317
|
+
vec3 boxMin = box.position - vec3(box.size);
|
|
318
|
+
vec3 boxMax = box.position + vec3(box.size);
|
|
319
|
+
|
|
320
|
+
// Calculate intersection distances for each axis
|
|
321
|
+
vec3 invDir = 1.0 / rayDirection;
|
|
322
|
+
vec3 t1 = (boxMin - rayOrigin) * invDir;
|
|
323
|
+
vec3 t2 = (boxMax - rayOrigin) * invDir;
|
|
324
|
+
|
|
325
|
+
// Find the min and max for each axis (handles negative direction)
|
|
326
|
+
vec3 tMin = min(t1, t2);
|
|
327
|
+
vec3 tMax = max(t1, t2);
|
|
328
|
+
|
|
329
|
+
// The ray enters the box at the maximum of all tMin values
|
|
330
|
+
float tNear = max(max(tMin.x, tMin.y), tMin.z);
|
|
331
|
+
|
|
332
|
+
// The ray exits the box at the minimum of all tMax values
|
|
333
|
+
float tFar = min(min(tMax.x, tMax.y), tMax.z);
|
|
334
|
+
|
|
335
|
+
// If tNear > tFar, ray misses the box
|
|
336
|
+
// If tFar < 0, box is behind the ray
|
|
337
|
+
if(tNear > tFar || tFar < 0.0) {
|
|
338
|
+
return -1.0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Return the nearest intersection (tNear, unless we're inside the box)
|
|
342
|
+
return tNear > 0.0 ? tNear : tFar;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Calculate normal vector at hit point on box surface
|
|
346
|
+
vec3 ${this.boxNormal}(in vec3 hitPosition, in Box box)
|
|
347
|
+
{
|
|
348
|
+
// Find which face of the box we hit by checking which coordinate is closest to the box boundary
|
|
349
|
+
vec3 localPos = hitPosition - box.position;
|
|
350
|
+
vec3 absLocal = abs(localPos);
|
|
351
|
+
|
|
352
|
+
// Return normal for the face that's closest to the box size
|
|
353
|
+
if(absLocal.x > absLocal.y && absLocal.x > absLocal.z) {
|
|
354
|
+
return vec3(sign(localPos.x), 0.0, 0.0);
|
|
355
|
+
} else if(absLocal.y > absLocal.z) {
|
|
356
|
+
return vec3(0.0, sign(localPos.y), 0.0);
|
|
357
|
+
} else {
|
|
358
|
+
return vec3(0.0, 0.0, sign(localPos.z));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Find closest box intersection, returns distance and sets hitIndex and hitType
|
|
363
|
+
float ${this.calculateIntersection}(in vec3 rayOrigin, in vec3 rayDirection, in float maxDistance, out int hitIndex, out int hitType)
|
|
364
|
+
{
|
|
365
|
+
float closestDistance = maxDistance;
|
|
366
|
+
hitIndex = -1;
|
|
367
|
+
hitType = -1;
|
|
368
|
+
|
|
369
|
+
for(int i = 0; i < ${NUM_BOXES}; i++) {
|
|
370
|
+
Box box = Box(${this.u_data}[i].xyz, ${this.u_data}[i].w, ${this.u_colors}[i]);
|
|
371
|
+
float distance = ${this.boxIntersection}(rayOrigin, rayDirection, box);
|
|
372
|
+
|
|
373
|
+
if(distance > 0.0 && distance < closestDistance) {
|
|
374
|
+
closestDistance = distance;
|
|
375
|
+
hitIndex = i;
|
|
376
|
+
hitType = ${this.id};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return hitIndex >= 0 ? closestDistance : -1.0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Calculate box normal and color at hit point
|
|
384
|
+
void ${this.calculateColor}(in vec3 hitPosition, in int hitIndex, out vec3 normal, out vec3 color)
|
|
385
|
+
{
|
|
386
|
+
Box hitBox = Box(${this.u_data}[hitIndex].xyz, ${this.u_data}[hitIndex].w, ${this.u_colors}[hitIndex]);
|
|
387
|
+
normal = ${this.boxNormal}(hitPosition, hitBox);
|
|
388
|
+
color = ${this.u_colors}[hitIndex];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Signed distance function for box
|
|
392
|
+
float ${this.boxSDF}(in vec3 point, in vec3 center, in float size)
|
|
393
|
+
{
|
|
394
|
+
vec3 d = abs(point - center) - vec3(size);
|
|
395
|
+
return length(max(d, vec3(0.0))) + min(max(d.x, max(d.y, d.z)), 0.0);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Calculate SDF for all boxes
|
|
399
|
+
float ${this.calculateSDF}(in vec3 point, out int nearestIndex, out float minDistance)
|
|
400
|
+
{
|
|
401
|
+
minDistance = 1000.0;
|
|
402
|
+
nearestIndex = -1;
|
|
403
|
+
|
|
404
|
+
for(int i = 0; i < ${NUM_BOXES}; i++) {
|
|
405
|
+
float distance = ${this.boxSDF}(point, ${this.u_data}[i].xyz, ${this.u_data}[i].w);
|
|
406
|
+
|
|
407
|
+
if(distance < minDistance) {
|
|
408
|
+
minDistance = distance;
|
|
409
|
+
nearestIndex = i;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return minDistance;
|
|
414
|
+
}
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
class Torus extends Shape {
|
|
418
|
+
torusIntersection = Symbol();
|
|
419
|
+
torusNormal = Symbol();
|
|
420
|
+
torusSDF = Symbol();
|
|
421
|
+
setData(positions, radii) {
|
|
422
|
+
this.view?.uniforms[this.u_positions]?.set(positions);
|
|
423
|
+
this.view?.uniforms[this.u_radii]?.set(radii);
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
template = glsl`
|
|
427
|
+
|
|
428
|
+
#define NUM_TORI ${NUM_TORI}
|
|
429
|
+
|
|
430
|
+
struct Torus {
|
|
431
|
+
vec3 position;
|
|
432
|
+
float majorRadius;
|
|
433
|
+
float minorRadius;
|
|
434
|
+
vec3 color;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
${uniform.vec3(this.u_positions, { size: NUM_TORI })}
|
|
438
|
+
${uniform.vec2(this.u_radii, { size: NUM_TORI })}
|
|
439
|
+
${uniform.vec3(this.u_colors, { size: NUM_TORI })}
|
|
440
|
+
|
|
441
|
+
// Signed distance function for torus
|
|
442
|
+
float ${this.torusSDF}(in vec3 point, in vec3 center, in float majorRadius, in float minorRadius)
|
|
443
|
+
{
|
|
444
|
+
vec3 p = point - center;
|
|
445
|
+
vec2 q = vec2(length(p.xz) - majorRadius, p.y);
|
|
446
|
+
return length(q) - minorRadius;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Calculate SDF for all tori
|
|
450
|
+
float ${this.calculateSDF}(in vec3 point, out int nearestIndex, out float minDistance)
|
|
451
|
+
{
|
|
452
|
+
minDistance = 1000.0;
|
|
453
|
+
nearestIndex = -1;
|
|
454
|
+
|
|
455
|
+
for(int i = 0; i < ${NUM_TORI}; i++) {
|
|
456
|
+
float distance = ${this.torusSDF}(point, ${this.u_positions}[i], ${this.u_radii}[i].x, ${this.u_radii}[i].y);
|
|
457
|
+
|
|
458
|
+
if(distance < minDistance) {
|
|
459
|
+
minDistance = distance;
|
|
460
|
+
nearestIndex = i;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return minDistance;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Torus intersection for fallback rendering
|
|
468
|
+
float ${this.torusIntersection}(in vec3 rayOrigin, in vec3 rayDirection, in Torus torus)
|
|
469
|
+
{
|
|
470
|
+
// Simplified intersection - not exact but good enough for fallback
|
|
471
|
+
return ${this.torusSDF}(rayOrigin, torus.position, torus.majorRadius, torus.minorRadius);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Calculate torus normal
|
|
475
|
+
vec3 ${this.torusNormal}(in vec3 hitPosition, in Torus torus)
|
|
476
|
+
{
|
|
477
|
+
vec3 p = hitPosition - torus.position;
|
|
478
|
+
float len = length(vec2(p.x, p.z));
|
|
479
|
+
vec3 d = vec3(p.x, 0.0, p.z) / len;
|
|
480
|
+
return normalize(p - d * torus.majorRadius);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Find closest torus intersection
|
|
484
|
+
float ${this.calculateIntersection}(in vec3 rayOrigin, in vec3 rayDirection, in float maxDistance, out int hitIndex, out int hitType)
|
|
485
|
+
{
|
|
486
|
+
float closestDistance = maxDistance;
|
|
487
|
+
hitIndex = -1;
|
|
488
|
+
hitType = -1;
|
|
489
|
+
|
|
490
|
+
for(int i = 0; i < ${NUM_TORI}; i++) {
|
|
491
|
+
Torus torus = Torus(${this.u_positions}[i], ${this.u_radii}[i].x, ${this.u_radii}[i].y, ${this.u_colors}[i]);
|
|
492
|
+
float distance = ${this.torusIntersection}(rayOrigin, rayDirection, torus);
|
|
493
|
+
|
|
494
|
+
if(distance > 0.0 && distance < closestDistance) {
|
|
495
|
+
closestDistance = distance;
|
|
496
|
+
hitIndex = i;
|
|
497
|
+
hitType = ${this.id};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return hitIndex >= 0 ? closestDistance : -1.0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Calculate torus normal and color at hit point
|
|
505
|
+
void ${this.calculateColor}(in vec3 hitPosition, in int hitIndex, out vec3 normal, out vec3 color)
|
|
506
|
+
{
|
|
507
|
+
Torus hitTorus = Torus(${this.u_positions}[hitIndex], ${this.u_radii}[hitIndex].x, ${this.u_radii}[hitIndex].y, ${this.u_colors}[hitIndex]);
|
|
508
|
+
normal = ${this.torusNormal}(hitPosition, hitTorus);
|
|
509
|
+
color = ${this.u_colors}[hitIndex];
|
|
510
|
+
}
|
|
511
|
+
`;
|
|
512
|
+
}
|
|
513
|
+
const box = new Box();
|
|
514
|
+
const sphere = new Sphere();
|
|
515
|
+
const torus = new Torus();
|
|
516
|
+
const camera = new Camera();
|
|
517
|
+
const shapes = [box, sphere, torus];
|
|
518
|
+
const { program, view } = compile.toQuad(
|
|
519
|
+
gl,
|
|
520
|
+
glsl`#version 300 es
|
|
521
|
+
precision mediump float;
|
|
522
|
+
|
|
523
|
+
struct HitInfo {
|
|
524
|
+
bool hit;
|
|
525
|
+
float distance;
|
|
526
|
+
vec3 position;
|
|
527
|
+
vec3 normal;
|
|
528
|
+
vec3 color;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
${shapes.map((shape) => shape.template)}
|
|
532
|
+
|
|
533
|
+
${camera.template}
|
|
534
|
+
${uniform.float("aspectRatio")}
|
|
535
|
+
${uniform.float("u_blendFactor")}
|
|
536
|
+
|
|
537
|
+
in vec2 v_uv;
|
|
538
|
+
out vec4 fragColor;
|
|
539
|
+
|
|
540
|
+
vec3 lightPos = vec3(5.0, 5.0, 5.0);
|
|
541
|
+
|
|
542
|
+
// Smooth minimum function for blending SDFs
|
|
543
|
+
float smoothMin(float a, float b, float k) {
|
|
544
|
+
float h = max(k - abs(a - b), 0.0);
|
|
545
|
+
return min(a, b) - h * h * 0.25 / k;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Structure to hold shape contribution info
|
|
549
|
+
struct ShapeContribution {
|
|
550
|
+
float distance;
|
|
551
|
+
int shapeType;
|
|
552
|
+
int index;
|
|
553
|
+
float weight;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// Combined SDF function that blends all shapes and calculates color weights
|
|
557
|
+
float combinedSDF(in vec3 point, out vec3 blendedColor)
|
|
558
|
+
{
|
|
559
|
+
ShapeContribution shapes[${shapes.length}];
|
|
560
|
+
float totalWeight = 0.0;
|
|
561
|
+
blendedColor = vec3(0.0);
|
|
562
|
+
|
|
563
|
+
${shapes.map((shape, i) => {
|
|
564
|
+
const index = Symbol();
|
|
565
|
+
const minDist = Symbol();
|
|
566
|
+
return glsl`
|
|
567
|
+
int ${index};
|
|
568
|
+
float ${minDist};
|
|
569
|
+
shapes[${i}].distance = ${shape.calculateSDF}(point, ${index}, ${minDist});
|
|
570
|
+
shapes[${i}].shapeType = ${shape.id};
|
|
571
|
+
shapes[${i}].index = ${index};
|
|
572
|
+
`;
|
|
573
|
+
})}
|
|
574
|
+
|
|
575
|
+
// Calculate blended distance using smooth minimum
|
|
576
|
+
float blendedDist = shapes[0].distance;
|
|
577
|
+
for(int i = 1; i < ${shapes.length}; i++) {
|
|
578
|
+
blendedDist = smoothMin(blendedDist, shapes[i].distance, u_blendFactor);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Calculate weights based on how close each shape is to the blended surface
|
|
582
|
+
for(int i = 0; i < ${shapes.length}; i++) {
|
|
583
|
+
float diff = abs(shapes[i].distance - blendedDist);
|
|
584
|
+
// Use exponential falloff with sharper cutoff for more vibrant colors
|
|
585
|
+
shapes[i].weight = exp(-diff * 20.0 / u_blendFactor);
|
|
586
|
+
totalWeight += shapes[i].weight;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Normalize weights and blend colors
|
|
590
|
+
if(totalWeight > 0.0) {
|
|
591
|
+
${shapes.map(
|
|
592
|
+
(shape, i) => glsl`
|
|
593
|
+
if(shapes[${i}].index >= 0) {
|
|
594
|
+
vec3 shapeColor = ${shape.u_colors}[shapes[${i}].index];
|
|
595
|
+
blendedColor += shapeColor * (shapes[${i}].weight / totalWeight);
|
|
596
|
+
}`
|
|
597
|
+
)}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return blendedDist;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Calculate normal using gradient of SDF
|
|
604
|
+
vec3 calculateSDFNormal(in vec3 point) {
|
|
605
|
+
const float eps = ${NORMAL_EPSILON};
|
|
606
|
+
vec3 dummyColor;
|
|
607
|
+
|
|
608
|
+
float center = combinedSDF(point, dummyColor);
|
|
609
|
+
float dx = combinedSDF(point + vec3(eps, 0.0, 0.0), dummyColor) - center;
|
|
610
|
+
float dy = combinedSDF(point + vec3(0.0, eps, 0.0), dummyColor) - center;
|
|
611
|
+
float dz = combinedSDF(point + vec3(0.0, 0.0, eps), dummyColor) - center;
|
|
612
|
+
|
|
613
|
+
return normalize(vec3(dx, dy, dz));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
vec3 calculateLighting(in vec3 position, in vec3 normal, in vec3 color)
|
|
617
|
+
{
|
|
618
|
+
vec3 lightDirection = normalize(lightPos - position);
|
|
619
|
+
float diffuse = max(0.0, dot(normal, lightDirection));
|
|
620
|
+
return color * (diffuse * 0.8 + 0.2); // 80% diffuse + 20% ambient
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Raymarching with SDF
|
|
624
|
+
bool raymarch(in vec3 rayOrigin, in vec3 rayDirection, out vec3 hitPosition, out vec3 hitColor)
|
|
625
|
+
{
|
|
626
|
+
float t = 0.0;
|
|
627
|
+
for(int i = 0; i < ${RAY_MARCH_ITERATIONS}; i++) {
|
|
628
|
+
hitPosition = rayOrigin + t * rayDirection;
|
|
629
|
+
float dist = combinedSDF(hitPosition, hitColor);
|
|
630
|
+
|
|
631
|
+
if(dist < ${RAY_HIT_THRESHOLD}) {
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
t += dist;
|
|
636
|
+
|
|
637
|
+
if(t > 100.0) {
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
void main()
|
|
645
|
+
{
|
|
646
|
+
// Ray from camera through pixel using camera vectors
|
|
647
|
+
vec3 rayOrigin = u_cameraPos;
|
|
648
|
+
vec3 rayDirection = normalize(u_cameraDir + v_uv.x * u_cameraRight * aspectRatio + v_uv.y * u_cameraUp);
|
|
649
|
+
|
|
650
|
+
vec3 color = vec3(0.1, 0.1, 0.2); // Background color
|
|
651
|
+
|
|
652
|
+
vec3 hitPosition;
|
|
653
|
+
vec3 hitColor;
|
|
654
|
+
|
|
655
|
+
if(raymarch(rayOrigin, rayDirection, hitPosition, hitColor)) {
|
|
656
|
+
// Calculate normal using SDF gradient
|
|
657
|
+
vec3 normal = calculateSDFNormal(hitPosition);
|
|
658
|
+
|
|
659
|
+
// Use the blended color from raymarch
|
|
660
|
+
color = calculateLighting(hitPosition, normal, hitColor);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Gamma correction
|
|
664
|
+
// color = pow(color, vec3(1.0 / 2.2));
|
|
665
|
+
|
|
666
|
+
fragColor = vec4(color, 1.0);
|
|
667
|
+
}`
|
|
668
|
+
);
|
|
669
|
+
gl.useProgram(program);
|
|
670
|
+
shapes.forEach((shape) => shape.view = view);
|
|
671
|
+
view.attributes.a_quad.bind();
|
|
672
|
+
sphere.setData(SPHERE_FLOAT_ARRAYS.positions).setColors(SPHERE_FLOAT_ARRAYS.colors);
|
|
673
|
+
box.setData(BOX_FLOAT_ARRAYS.positions).setColors(BOX_FLOAT_ARRAYS.colors);
|
|
674
|
+
torus.setData(TORUS_FLOAT_ARRAYS.positions, TORUS_FLOAT_ARRAYS.radii).setColors(TORUS_FLOAT_ARRAYS.colors);
|
|
675
|
+
view.uniforms.aspectRatio.set(canvas.width / canvas.height);
|
|
676
|
+
view.uniforms.u_blendFactor.set(blendFactor);
|
|
677
|
+
camera.view = view;
|
|
678
|
+
camera.update();
|
|
679
|
+
function animate(timestamp) {
|
|
680
|
+
const time = timestamp / 1e3;
|
|
681
|
+
const pulseFactor = 1 + 0.3 * Math.sin(time * 2);
|
|
682
|
+
SPHERE_FLOAT_ARRAYS.positions[3] = pulseFactor;
|
|
683
|
+
const orbitRadius = 1.5;
|
|
684
|
+
SPHERE_FLOAT_ARRAYS.positions[4] = 1.5 + Math.cos(time * 0.4) * orbitRadius * 0.5;
|
|
685
|
+
SPHERE_FLOAT_ARRAYS.positions[5] = 0.5 + Math.sin(time * 0.6) * 0.8;
|
|
686
|
+
SPHERE_FLOAT_ARRAYS.positions[6] = -1 + Math.sin(time * 0.4) * orbitRadius * 0.5;
|
|
687
|
+
SPHERE_FLOAT_ARRAYS.positions[8] = -1.5 + Math.cos(time * 0.3 + Math.PI) * orbitRadius * 0.5;
|
|
688
|
+
SPHERE_FLOAT_ARRAYS.positions[9] = -0.5 + Math.cos(time * 0.5) * 0.6;
|
|
689
|
+
SPHERE_FLOAT_ARRAYS.positions[10] = 0.5 + Math.sin(time * 0.3 + Math.PI) * orbitRadius * 0.5;
|
|
690
|
+
SPHERE_FLOAT_ARRAYS.positions[13] = 1.5 + Math.sin(time * 0.8) * 0.5;
|
|
691
|
+
SPHERE_FLOAT_ARRAYS.positions[16] = -1 + Math.sin(time * 0.4) * 0.8;
|
|
692
|
+
SPHERE_FLOAT_ARRAYS.positions[17] = -1.5 + Math.sin(time * 0.6) * 0.5;
|
|
693
|
+
SPHERE_FLOAT_ARRAYS.positions[18] = 1 + Math.cos(time * 0.4) * 0.3;
|
|
694
|
+
SPHERE_FLOAT_ARRAYS.positions[20] = 2 + Math.cos(time * 0.3) * 0.7;
|
|
695
|
+
SPHERE_FLOAT_ARRAYS.positions[22] = -1.5 + Math.sin(time * 0.3) * 0.5;
|
|
696
|
+
SPHERE_FLOAT_ARRAYS.positions[23] = 0.7 + 0.2 * Math.sin(time * 2);
|
|
697
|
+
SPHERE_FLOAT_ARRAYS.positions[24] = -2 + Math.sin(time * 0.4) * 0.5;
|
|
698
|
+
SPHERE_FLOAT_ARRAYS.positions[25] = 1 + Math.sin(time * 0.6) * 0.4;
|
|
699
|
+
SPHERE_FLOAT_ARRAYS.positions[28] = Math.cos(time * 0.3) * 1.2;
|
|
700
|
+
SPHERE_FLOAT_ARRAYS.positions[29] = -0.5 + Math.sin(time * 0.3) * 0.3;
|
|
701
|
+
SPHERE_FLOAT_ARRAYS.colors[0] = 0.5 + 0.5 * Math.sin(time * 3);
|
|
702
|
+
SPHERE_FLOAT_ARRAYS.colors[4] = 0.5 + 0.5 * Math.cos(time * 2);
|
|
703
|
+
SPHERE_FLOAT_ARRAYS.colors[9] = 0.5 + 0.5 * Math.sin(time * 2.5);
|
|
704
|
+
SPHERE_FLOAT_ARRAYS.colors[15] = 0.3 + 0.7 * Math.abs(Math.sin(time * 1.8));
|
|
705
|
+
SPHERE_FLOAT_ARRAYS.colors[16] = 0.3 + 0.7 * Math.abs(Math.sin(time * 1.8));
|
|
706
|
+
SPHERE_FLOAT_ARRAYS.colors[17] = 0.3 + 0.7 * Math.abs(Math.sin(time * 1.8));
|
|
707
|
+
BOX_FLOAT_ARRAYS.positions[0] = 1 + Math.cos(time * 0.35) * 0.3;
|
|
708
|
+
BOX_FLOAT_ARRAYS.positions[2] = -0.5 + Math.sin(time * 0.35) * 0.3;
|
|
709
|
+
BOX_FLOAT_ARRAYS.positions[7] = 0.8 + 0.2 * Math.sin(time * 1.5);
|
|
710
|
+
BOX_FLOAT_ARRAYS.positions[9] = -1 + Math.sin(time * 0.4) * 0.3;
|
|
711
|
+
TORUS_FLOAT_ARRAYS.radii[0] = 0.8 + 0.2 * Math.sin(time * 1.2);
|
|
712
|
+
TORUS_FLOAT_ARRAYS.positions[3] = -1.5 + Math.cos(time * 0.5) * 0.5;
|
|
713
|
+
TORUS_FLOAT_ARRAYS.positions[5] = Math.sin(time * 0.5) * 0.5;
|
|
714
|
+
TORUS_FLOAT_ARRAYS.positions[7] = -0.5 + Math.sin(time * 0.7) * 0.3;
|
|
715
|
+
TORUS_FLOAT_ARRAYS.radii[7] = 0.2 + 0.1 * Math.sin(time * 2);
|
|
716
|
+
sphere.setData(SPHERE_FLOAT_ARRAYS.positions).setColors(SPHERE_FLOAT_ARRAYS.colors);
|
|
717
|
+
box.setData(BOX_FLOAT_ARRAYS.positions).setColors(BOX_FLOAT_ARRAYS.colors);
|
|
718
|
+
torus.setData(TORUS_FLOAT_ARRAYS.positions, TORUS_FLOAT_ARRAYS.radii).setColors(TORUS_FLOAT_ARRAYS.colors);
|
|
719
|
+
blendFactor = 0.5 + 0.4 * Math.sin(time * 0.3);
|
|
720
|
+
view.uniforms.u_blendFactor.set(blendFactor);
|
|
721
|
+
}
|
|
722
|
+
canvas.addEventListener("mousedown", (e) => {
|
|
723
|
+
camera.startDrag(e.clientX, e.clientY);
|
|
724
|
+
});
|
|
725
|
+
canvas.addEventListener("mousemove", (e) => {
|
|
726
|
+
camera.drag(e.clientX, e.clientY);
|
|
727
|
+
});
|
|
728
|
+
canvas.addEventListener("mouseup", () => {
|
|
729
|
+
camera.endDrag();
|
|
730
|
+
});
|
|
731
|
+
canvas.addEventListener("mouseleave", () => {
|
|
732
|
+
camera.endDrag();
|
|
733
|
+
});
|
|
734
|
+
canvas.addEventListener("wheel", (e) => {
|
|
735
|
+
e.preventDefault();
|
|
736
|
+
camera.zoom(e.deltaY);
|
|
737
|
+
});
|
|
738
|
+
canvas.addEventListener("touchstart", (e) => {
|
|
739
|
+
if (e.touches.length === 1) {
|
|
740
|
+
camera.startDrag(e.touches[0].clientX, e.touches[0].clientY);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
canvas.addEventListener("touchmove", (e) => {
|
|
744
|
+
if (e.touches.length === 1) {
|
|
745
|
+
camera.drag(e.touches[0].clientX, e.touches[0].clientY);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
canvas.addEventListener("touchend", () => {
|
|
749
|
+
camera.endDrag();
|
|
750
|
+
});
|
|
751
|
+
function draw(timestamp) {
|
|
752
|
+
if (controller.signal.aborted) return;
|
|
753
|
+
camera.update();
|
|
754
|
+
animate(timestamp);
|
|
755
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
756
|
+
requestAnimationFrame(draw);
|
|
757
|
+
}
|
|
758
|
+
requestAnimationFrame(draw);
|
|
759
|
+
new ResizeObserver(() => {
|
|
760
|
+
canvas.width = window.innerWidth;
|
|
761
|
+
canvas.height = window.innerHeight;
|
|
762
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
763
|
+
view.uniforms.aspectRatio.set(canvas.width / canvas.height);
|
|
764
|
+
draw(performance.now());
|
|
765
|
+
}).observe(document.body);
|