arcane-core 0.26.1

Core library for Arcane - agent-native 2D game engine (TypeScript runtime, renderer, platform layer)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
// 2D Signed Distance Function primitives for the Arcane game engine.
//
// Ported from Inigo Quilez's 2D distance functions:
//   https://iquilezles.org/articles/distfunctions2d/
//
// All functions take a point p in local space and return the signed distance
// to the shape boundary. Negative values are inside, positive are outside.
//
// Usage: These are utility functions intended to be imported (#include or
// copy-paste) into fragment shaders that need procedural SDF shapes.

const PI: f32 = 3.14159265;
const TAU: f32 = 6.28318530;

// ============================================================================
// Helper functions
// ============================================================================

/// Dot product of a vector with itself (squared length).
fn dot2v(v: vec2<f32>) -> f32 {
    return dot(v, v);
}

/// Cross product of two 2D vectors (returns scalar z-component).
fn cross2d(a: vec2<f32>, b: vec2<f32>) -> f32 {
    return a.x * b.y - a.y * b.x;
}

/// Diagonal dot product: a.x*b.x - a.y*b.y. Used by vesica/rhombus shapes.
fn ndot(a: vec2<f32>, b: vec2<f32>) -> f32 {
    return a.x * b.x - a.y * b.y;
}

// ============================================================================
// Basic shapes
// ============================================================================

/// Circle centered at origin with radius r.
fn sd_circle(p: vec2<f32>, r: f32) -> f32 {
    return length(p) - r;
}

/// Axis-aligned box centered at origin with half-extents b.
fn sd_box(p: vec2<f32>, b: vec2<f32>) -> f32 {
    let d = abs(p) - b;
    return length(max(d, vec2<f32>(0.0))) + min(max(d.x, d.y), 0.0);
}

/// Rounded box with per-corner radii. b = half-extents.
/// r = (top-right, bottom-right, top-left, bottom-left) corner radii.
fn sd_rounded_box(p: vec2<f32>, b: vec2<f32>, r: vec4<f32>) -> f32 {
    // Select corner radius based on quadrant.
    // select(false_val, true_val, condition) in WGSL.
    var radii = select(r.xy, r.zw, p.x < 0.0);
    let radius = select(radii.x, radii.y, p.y < 0.0);

    let q = abs(p) - b + vec2<f32>(radius);
    return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - radius;
}

/// Ellipse with semi-axes ab = (a, b). Exact SDF using IQ's closed-form solution.
/// Reference: https://iquilezles.org/articles/ellipsedist/
fn sd_ellipse(p_in: vec2<f32>, ab_in: vec2<f32>) -> f32 {
    // Work in first quadrant (symmetry)
    var p = abs(p_in);

    // Ensure a >= b by swapping axes if needed
    var ab = ab_in;
    if ab.x < ab.y {
        p = p.yx;
        ab = ab.yx;
    }

    let a = ab.x;
    let b = ab.y;

    let l = a * a - b * b;

    // Nearly circular: fall back to circle SDF
    if l < 1e-7 {
        return length(p) - a;
    }

    // On-axis shortcut: if p.x ~ 0, closest point is pole (0, b)
    if p.x < 1e-6 {
        return p.y - b;
    }
    // If p.y ~ 0, closest point is (a, 0)
    if p.y < 1e-6 {
        return p.x - a;
    }

    let m = a * p.x / l;
    let m2 = m * m;
    let n = b * p.y / l;
    let n2 = n * n;
    let c = (m2 + n2 - 1.0) / 3.0;
    let c3 = c * c * c;
    let d = c3 + m2 * n2 * 2.0;

    var co: vec2<f32>;

    if d < 0.0 {
        // Three real roots: use trigonometric solution
        let h = acos(clamp(d / c3, -1.0, 1.0)) / 3.0;
        let s = cos(h);
        let t = sin(h) * sqrt(3.0);
        let rx = sqrt(max(-c * (s + t + 2.0) + m2, 0.0));
        let ry = sqrt(max(-c * (s - t + 2.0) + m2, 0.0));
        let g = (ry + sign(l) * rx + abs(n) / max(rx * ry, 1e-10)) * 0.5;
        co = vec2<f32>(
            a * g / m,
            b * sqrt(max(1.0 - g * g, 0.0))
        );
    } else {
        // One real root: use Cardano's formula
        let h = 2.0 * m * n * sqrt(d);
        let s = pow(max(d + h, 0.0), 1.0 / 3.0) * sign(d + h);
        let t_val = pow(max(abs(d - h), 0.0), 1.0 / 3.0) * sign(d - h);
        let rx = -(s + t_val) - c * 4.0 + 2.0 * m2;
        let ry = (s - t_val) * sqrt(3.0);
        let rm = sqrt(max(rx * rx + ry * ry, 0.0));
        co.x = (ry / sqrt(max(rm - rx, 1e-10)) + 2.0 * m * n / max(rm, 1e-10)) * 0.5;
        co.x = a * co.x;
        co.y = b * sqrt(max(1.0 - co.x * co.x / (a * a), 0.0));
    }

    // Clamp co.x to valid range
    co.x = clamp(co.x, 0.0, a);
    co.y = b * sqrt(max(1.0 - (co.x / a) * (co.x / a), 0.0));

    return length(p - co) * sign(p.y - co.y);
}

/// Line segment from a to b. Returns unsigned distance.
fn sd_segment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
    let pa = p - a;
    let ba = b - a;
    let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h);
}

/// Triangle defined by three vertices.
fn sd_triangle(p: vec2<f32>, p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>) -> f32 {
    let e0 = p1 - p0;
    let e1 = p2 - p1;
    let e2 = p0 - p2;

    let v0 = p - p0;
    let v1 = p - p1;
    let v2 = p - p2;

    let pq0 = v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0);
    let pq1 = v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0);
    let pq2 = v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0);

    let s = sign(e0.x * e2.y - e0.y * e2.x);
    let d = min(min(
        vec2<f32>(dot(pq0, pq0), s * (v0.x * e0.y - v0.y * e0.x)),
        vec2<f32>(dot(pq1, pq1), s * (v1.x * e1.y - v1.y * e1.x))),
        vec2<f32>(dot(pq2, pq2), s * (v2.x * e2.y - v2.y * e2.x)));

    return -sqrt(d.x) * sign(d.y);
}

// ============================================================================
// Organic shapes
// ============================================================================

/// Egg shape (asymmetric vertically). ra = large radius, rb = small radius.
/// The egg is oriented along the y-axis with the wider end at the bottom.
fn sd_egg(p: vec2<f32>, ra: f32, rb: f32) -> f32 {
    let k = sqrt(3.0);
    var q = vec2<f32>(abs(p.x), p.y);

    let r = ra - rb;

    // Bottom half-plane: circle of radius r centered at origin
    if q.y < 0.0 {
        return length(q) - r;
    }

    // Top region: small circle tangent at top
    if k * (q.x + r) < q.y {
        return length(q - vec2<f32>(0.0, k * r + rb)) - rb;
    }

    // Side region: large arc
    return length(q + vec2<f32>(r, 0.0)) - 2.0 * r - rb;
}

/// Heart shape. The heart is oriented with the point downward.
/// r controls the overall size.
fn sd_heart(p: vec2<f32>, r: f32) -> f32 {
    var q = p / r;
    q = vec2<f32>(abs(q.x), q.y + 0.75);

    if q.x + q.y > 1.0 {
        return r * (sqrt(dot2v(q - vec2<f32>(0.25, 0.75))) - sqrt(2.0) * 0.25);
    }

    let half_diag = vec2<f32>(0.5 * max(q.x + q.y, 0.0));
    return r * (sqrt(min(dot2v(q - vec2<f32>(0.0, 1.0)),
                         dot2v(q - half_diag))) *
                sign(q.x - q.y));
}

/// Moon shape. d = displacement of inner circle, ra = outer radius, rb = inner radius.
/// Creates a crescent by subtracting one circle from another.
fn sd_moon(p: vec2<f32>, d: f32, ra: f32, rb: f32) -> f32 {
    var q = vec2<f32>(p.x, abs(p.y));

    let a = (ra * ra - rb * rb + d * d) / (2.0 * d);
    let b = sqrt(max(ra * ra - a * a, 0.0));

    if d * (q.x * b - q.y * a) > d * d * max(b - q.y, 0.0) {
        return length(q - vec2<f32>(a, b));
    }

    return max(length(q) - ra,
              -(length(q - vec2<f32>(d, 0.0)) - rb));
}

/// Vesica piscis (lens/almond shape). r = circle radius, d = half-distance between centers.
/// The two circles are centered at (-d, 0) and (d, 0).
fn sd_vesica(p: vec2<f32>, r: f32, d: f32) -> f32 {
    let q = abs(p);

    let b = sqrt(max(r * r - d * d, 0.0));

    if (q.y - b) * d > q.x * b {
        return length(q - vec2<f32>(0.0, b));
    }

    return length(q - vec2<f32>(-d, 0.0)) - r;
}

/// Circular arc. sc = vec2(sin, cos) of the arc half-angle opening, ra = radius, rb = thickness.
/// The arc opens upward centered on the y-axis.
fn sd_arc(p: vec2<f32>, sc: vec2<f32>, ra: f32, rb: f32) -> f32 {
    let q = vec2<f32>(abs(p.x), p.y);

    // Test if the point is within the arc's angular span
    if sc.y * q.x > sc.x * q.y {
        // Within span: distance to the arc ring
        return abs(length(q) - ra) - rb;
    }

    // Outside span: distance to the arc endpoints
    return length(q - sc * ra) - rb;
}

// ============================================================================
// Geometric shapes (regular polygons and stars)
// ============================================================================

/// Regular hexagon with inradius r (flat-to-flat distance / 2).
fn sd_hexagon(p: vec2<f32>, r: f32) -> f32 {
    // k = (-sqrt(3)/2, 0.5, 1/sqrt(3))
    let k = vec3<f32>(-0.866025404, 0.5, 0.577350269);
    var q = abs(p);

    // Reflect across the two mirror axes
    q = q - 2.0 * min(dot(k.xy, q), 0.0) * k.xy;

    // Distance to the flat edge
    q = q - vec2<f32>(clamp(q.x, -k.z * r, k.z * r), r);

    return length(q) * sign(q.y);
}

/// Regular pentagon with inradius r.
fn sd_pentagon(p: vec2<f32>, r: f32) -> f32 {
    // k = (cos(pi/5), sin(pi/5), tan(pi/5))
    let k = vec3<f32>(0.809016994, 0.587785252, 0.726542528);
    var q = vec2<f32>(abs(p.x), -p.y);

    // Two reflections to fold into one sector
    q = q - 2.0 * min(dot(vec2<f32>(-k.x, k.y), q), 0.0) * vec2<f32>(-k.x, k.y);
    q = q - 2.0 * min(dot(vec2<f32>(k.x, k.y), q), 0.0) * vec2<f32>(k.x, k.y);

    // Distance to the flat edge
    q = q - vec2<f32>(clamp(q.x, -r * k.z, r * k.z), r);

    return length(q) * sign(q.y);
}

/// Regular octagon with inradius r.
fn sd_octogon(p: vec2<f32>, r: f32) -> f32 {
    // k = (-cos(pi/8), sin(pi/8), tan(pi/8))
    let k = vec3<f32>(-0.9238795325, 0.3826834324, 0.4142135624);
    var q = abs(p);

    // Reflect across two mirror axes (45-degree-spaced)
    q = q - 2.0 * min(dot(vec2<f32>(k.x, k.y), q), 0.0) * vec2<f32>(k.x, k.y);
    q = q - 2.0 * min(dot(vec2<f32>(-k.x, k.y), q), 0.0) * vec2<f32>(-k.x, k.y);

    // Distance to the flat edge
    q = q - vec2<f32>(clamp(q.x, -k.z * r, k.z * r), r);

    return length(q) * sign(q.y);
}

/// Five-pointed star. r = outer radius, rf = inner radius factor.
/// rf controls how "pointy" the star is. rf ~ 0.382 gives a regular star.
fn sd_star5(p: vec2<f32>, r: f32, rf: f32) -> f32 {
    // k1 = (cos(pi/5), -sin(pi/5)), k2 = (-cos(pi/5), -sin(pi/5))
    let k1 = vec2<f32>(0.809016994, -0.587785252);
    let k2 = vec2<f32>(-k1.x, k1.y);

    var q = vec2<f32>(abs(p.x), -p.y);

    // Fold into one star arm via two reflections
    q = q - 2.0 * max(dot(k1, q), 0.0) * k1;
    q = q - 2.0 * max(dot(k2, q), 0.0) * k2;

    // Distance to the inner edge of the arm and the central axis to the star tip
    let e0 = rf * vec2<f32>(-k1.y, k1.x); // inner edge direction
    let e1 = vec2<f32>(0.0, -1.0);        // axis to tip (downward in folded space)

    // Project q onto the inner edge line (from origin along e0 direction)
    let pq0 = q - e0 * clamp(dot(q, e0) / dot(e0, e0), 0.0, r);

    // Project q onto the central axis (origin to tip at (0, -r))
    let pq1 = q - e1 * clamp(dot(q, e1), 0.0, r);

    return sqrt(min(dot(pq0, pq0), dot(pq1, pq1))) *
           sign(q.y * e0.x - q.x * e0.y);
}

/// General n-pointed star. r = outer radius, n = point count, m = angular sharpness.
/// m controls the inner angle: m = 2 gives minimal points, higher m = sharper points.
/// Valid range: 2 <= m <= n.
fn sd_star(p: vec2<f32>, r: f32, n: u32, m: f32) -> f32 {
    let an = PI / f32(n);
    let en = PI / m;
    let acs = vec2<f32>(cos(an), sin(an));
    let ecs = vec2<f32>(cos(en), sin(en));

    // Fold the angle into the first sector using modulo
    let bn = (atan2(abs(p.x), p.y) % (2.0 * an)) - an;
    var q = length(p) * vec2<f32>(cos(bn), abs(sin(bn)));

    // Project onto the star edge
    q = q - r * acs;
    q = q + ecs * clamp(-dot(q, ecs), 0.0, r * acs.y / ecs.y);

    return length(q) * sign(q.x);
}

/// Cross (plus sign) shape. b = half-extents of one arm (b.x = length, b.y = thickness).
/// r = corner rounding radius. The cross is two overlapping rectangles.
fn sd_cross(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
    var q = abs(p);

    // Exploit 45-degree symmetry: fold so that x >= y
    if q.y > q.x {
        q = q.yx;
    }

    // Now q is in the sector where x >= y >= 0.
    // The cross boundary in this sector is an L-shape with corners at:
    //   (b.y, b.y) inner corner, (b.x, b.y) arm end, (b.y, b.x) arm end
    let t = q - b;
    let k = max(t.x, t.y);

    if k > 0.0 {
        // Outside the bounding box of the arm
        return length(max(t, vec2<f32>(0.0))) - r;
    }

    // Inside: distance to the inner L-corner boundary
    // The inner corner is at (b.y, b.y). The signed distance to the
    // two inner edges: q.x - b.y (right of inner edge) and q.y - b.y
    // But after the diagonal fold, we are in the x>=y sector, so only
    // the boundary at y = b.y matters when q.x < b.x.
    // Combined: max of (distance to outer edge, distance to inner corner)
    return max(q.y - b.y, k) - r;
}

// ============================================================================
// Utility shapes
// ============================================================================

/// Ring (annulus). r = center radius, w = ring thickness.
fn sd_ring(p: vec2<f32>, r: f32, w: f32) -> f32 {
    return abs(length(p) - r) - w * 0.5;
}

/// Pie (circular sector). sc = vec2(sin, cos) of the half-angle, r = radius.
/// The sector opens along the +y axis.
fn sd_pie(p: vec2<f32>, sc: vec2<f32>, r: f32) -> f32 {
    let q = vec2<f32>(abs(p.x), p.y);
    let l = length(q) - r;
    let m = length(q - sc * clamp(dot(q, sc), 0.0, r));
    return max(l, m * sign(sc.y * q.x - sc.x * q.y));
}

/// Rounded X (multiplication sign). w = arm half-width, r = tip rounding radius.
fn sd_rounded_x(p: vec2<f32>, w: f32, r: f32) -> f32 {
    let q = abs(p);
    // The X diagonals are the lines y=x and y=-x. After abs(), we only
    // need distance to y=x in the first quadrant.
    // Closest point on y=x to (qx, qy) is ((qx+qy)/2, (qx+qy)/2).
    let d = min(q.x + q.y, max(q.x, q.y));
    return d * 0.5 - w * 0.5 - r;
}