Skills に戻る
heygen-com/hyperframes実行前に内容を確認

SKILL DETAIL

frost-sequence-camera-orbit

heygen-com/hyperframes/frost-sequence-camera-orbit

An orbiting camera follows an ice logo as it breaks apart, reforms into two text moments, and fades. HyperFrames block, 1920×1080, 22.5s, 216 variables.

インストール · 132出典を見る

Installation

npx skills add https://github.com/heygen-com/hyperframes --skill frost-sequence-camera-orbit

スキルファイル

SKILL.md

最終同期 · 2026/09/20

assets/Clipper-LICENSE.txt
/*******************************************************************************
 *                                                                              *
 * Author    :  Angus Johnson                                                   *
 * Version   :  6.4.2                                                           *
 * Date      :  27 February 2017                                                *
 * Website   :  http://www.angusj.com                                           *
 * Copyright :  Angus Johnson 2010-2017                                         *
 *                                                                              *
 * License:                                                                     *
 * Use, modification & distribution is subject to Boost Software License Ver 1. *
 * http://www.boost.org/LICENSE_1_0.txt                                         *
 *                                                                              *
 * Attributions:                                                                *
 * The code in this library is an extension of Bala Vatti's clipping algorithm: *
 * "A generic solution to polygon clipping"                                     *
 * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63.             *
 * http://portal.acm.org/citation.cfm?id=129906                                 *
 *                                                                              *
 * Computer graphics and geometric modeling: implementation and algorithms      *
 * By Max K. Agoston                                                            *
 * Springer; 1 edition (January 4, 2005)                                        *
 * http://books.google.com/books?q=vatti+clipping+agoston                       *
 *                                                                              *
 * See also:                                                                    *
 * "Polygon Offsetting by Computing Winding Numbers"                            *
 * Paper no. DETC2005-85513 pp. 565-575                                         *
 * ASME 2005 International Design Engineering Technical Conferences             *
 * and Computers and Information in Engineering Conference (IDETC/CIE2005)      *
 * September 24-28, 2005 , Long Beach, California, USA                          *
 * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf              *
 *                                                                              *
 *******************************************************************************/
/*******************************************************************************
 *                                                                              *
 * Author    :  Timo                                                            *
 * Version   :  6.4.2.2                                                         *
 * Date      :  8 September 2017                                                 *
 *                                                                              *
 * This is a translation of the C# Clipper library to Javascript.               *
 * Int128 struct of C# is implemented using JSBN of Tom Wu.                     *

Full text: http://www.boost.org/LICENSE_1_0.txt
assets/example-logo.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M50 5 L95 50 L50 95 L5 50 Z"/></svg>
assets/fonts/Geist-OFL.txt
Copyright (c) 2023 Vercel, in collaboration with basement.studio

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL

-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
assets/gsap-3.14.2.min.js
/*!
 * GSAP 3.14.2
 * https://gsap.com
 *
 * @license Copyright 2025, GreenSock. All rights reserved.
 * Subject to the terms at https://gsap.com/standard-license.
 * @author: Jack Doyle, [email protected]
 */

!(function (t, e) {
  "object" == typeof exports && "undefined" != typeof module
    ? e(exports)
    : "function" == typeof define && define.amd
      ? define(["exports"], e)
      : e(((t = t || self).window = t.window || {}));
})(this, function (e) {
  "use strict";
  function _inheritsLoose(t, e) {
    ((t.prototype = Object.create(e.prototype)), ((t.prototype.constructor = t).__proto__ = e));
  }
  function _assertThisInitialized(t) {
    if (void 0 === t)
      throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    return t;
  }
  function r(t) {
    return "string" == typeof t;
  }
  function s(t) {
    return "function" == typeof t;
  }
  function t(t) {
    return "number" == typeof t;
  }
  function u(t) {
    return void 0 === t;
  }
  function v(t) {
    return "object" == typeof t;
  }
  function w(t) {
    return !1 !== t;
  }
  function x() {
    return "undefined" != typeof window;
  }
  function y(t) {
    return s(t) || r(t);
  }
  function R(t) {
    return (i = bt(t, ht)) && Fe;
  }
  function S(t, e) {
    return console.warn(
      "Invalid property",
      t,
      "set to",
      e,
      "Missing plugin? gsap.registerPlugin()",
    );
  }
  function T(t, e) {
    return !e && console.warn(t);
  }
  function U(t, e) {
    return (t && (ht[t] = e) && i && (i[t] = e)) || ht;
  }
  function V() {
    return 0;
  }
  function ga(t) {
    var e,
      r,
      i = t[0];
    if ((v(i) || s(i) || (t = [t]), !(e = (i._gsap || {}).harness))) {
      for (r = yt.length; r-- && !yt[r].targetTest(i); );
      e = yt[r];
    }
    for (r = t.length; r--; )
      (t[r] && (t[r]._gsap || (t[r]._gsap = new Xt(t[r], e)))) || t.splice(r, 1);
    return t;
  }
  function ha(t) {
    return t._gsap || ga(Pt(t))[0]._gsap;
  }
  function ia(t, e, r) {
    return (r = t[e]) && s(r) ? t[e]() : (u(r) && t.getAttribute && t.getAttribute(e)) || r;
  }
  function ja(t, e) {
    return (t = t.split(",")).forEach(e) || t;
  }
  function ka(t) {
    return Math.round(1e5 * t) / 1e5 || 0;
  }
  function la(t) {
    return Math.round(1e7 * t) / 1e7 || 0;
  }
  function ma(t, e) {
    var r = e.charAt(0),
      i = parseFloat(e.substr(2));
    return ((t = parseFloat(t)), "+" === r ? t + i : "-" === r ? t - i : "*" === r ? t * i : t / i);
  }
  function na(t, e) {
    for (var r = e.length, i = 0; t.indexOf(e[i]) < 0 && ++i < r; );
    return i < r;
  }
  function oa() {
    var t,
      e,
      r = pt.length,
      i = pt.slice(0);
    for (_t = {}, t = pt.length = 0; t < r; t++)
      (e = i[t]) && e._lazy && (e.render(e._lazy[0], e._lazy[1], !0)._lazy = 0);
  }
  function pa(t) {
    return !!(t._initted || t._startAt || t.add);
  }
  function qa(t, e, r, i) {
    (pt.length && !I && oa(),
      t.render(e, r, i || !!(I && e < 0 && pa(t))),
      pt.length && !I && oa());
  }
  function ra(t) {
    var e = parseFloat(t);
    return (e || 0 === e) && (t + "").match(ot).length < 2 ? e : r(t) ? t.trim() : t;
  }
  function sa(t) {
    return t;
  }
  function ta(t, e) {
    for (var r in e) r in t || (t[r] = e[r]);
    return t;
  }
  function wa(t, e) {
    for (var r in e)
      "__proto__" !== r &&
        "constructor" !== r &&
        "prototype" !== r &&
        (t[r] = v(e[r]) ? wa(t[r] || (t[r] = {}), e[r]) : e[r]);
    return t;
  }
  function xa(t, e) {
    var r,
      i = {};
    for (r in t) r in e || (i[r] = t[r]);
    return i;
  }
  function ya(t) {
    var e = t.parent || L,
      r = t.keyframes
        ? (function _setKeyframeDefaults(i) {
            return function (t, e) {
              for (var r in e) r in t || ("duration" === r && i) || "ease" === r || (t[r] = e[r]);
            };
          })($(t.keyframes))
        : ta;
    if (w(t.inherit)) for (; e; ) (r(t, e.vars.defaults), (e = e.parent || e._dp));
    return t;
  }
  function Aa(t, e, r, i, n) {
    (void 0 === r && (r = "_first"), void 0 === i && (i = "_last"));
    var a,
      s = t[i];
    if (n) for (a = e[n]; s && s[n] > a; ) s = s._prev;
    return (
      s ? ((e._next = s._next), (s._next = e)) : ((e._next = t[r]), (t[r] = e)),
      e._next ? (e._next._prev = e) : (t[i] = e),
      (e._prev = s),
      (e.parent = e._dp = t),
      e
    );
  }
  function Ba(t, e, r, i) {
    (void 0 === r && (r = "_first"), void 0 === i && (i = "_last"));
    var n = e._prev,
      a = e._next;
    (n ? (n._next = a) : t[r] === e && (t[r] = a),
      a ? (a._prev = n) : t[i] === e && (t[i] = n),
      (e._next = e._prev = e.parent = null));
  }
  function Ca(t, e) {
    (t.parent && (!e || t.parent.autoRemoveChildren) && t.parent.remove && t.parent.remove(t),
      (t._act = 0));
  }
  function Da(t, e) {
    if (t && (!e || e._end > t._dur || e._start < 0))
      for (var r = t; r; ) ((r._dirty = 1), (r = r.parent));
    return t;
  }
  function Fa(t, e, r, i) {
    return (
      t._startAt &&
      (I
        ? t._startAt.revert(ft)
        : (t.vars.immediateRender && !t.vars.autoRevert) || t._startAt.render(e, !0, i))
    );
  }
  function Ha(t) {
    return t._repeat ? wt(t._tTime, (t = t.duration() + t._rDelay)) * t : 0;
  }
  function Ja(t, e) {
    return (t - e._start) * e._ts + (0 <= e._ts ? 0 : e._dirty ? e.totalDuration() : e._tDur);
  }
  function Ka(t) {
    return (t._end = la(t._start + (t._tDur / Math.abs(t._ts || t._rts || q) || 0)));
  }
  function La(t, e) {
    var r = t._dp;
    return (
      r &&
        r.smoothChildTiming &&
        t._ts &&
        ((t._start = la(
          r._time -
            (0 < t._ts ? e / t._ts : ((t._dirty ? t.totalDuration() : t._tDur) - e) / -t._ts),
        )),
        Ka(t),
        r._dirty || Da(r, t)),
      t
    );
  }
  function Ma(t, e) {
    var r;
    if (
      ((e._time || (!e._dur && e._initted) || (e._start < t._time && (e._dur || !e.add))) &&
        ((r = Ja(t.rawTime(), e)),
        (!e._dur || Mt(0, e.totalDuration(), r) - e._tTime > q) && e.render(r, !0)),
      Da(t, e)._dp && t._initted && t._time >= t._dur && t._ts)
    ) {
      if (t._dur < t.duration())
        for (r = t; r._dp; ) (0 <= r.rawTime() && r.totalTime(r._tTime), (r = r._dp));
      t._zTime = -q;
    }
  }
  function Na(e, r, i, n) {
    return (
      r.parent && Ca(r),
      (r._start = la((t(i) ? i : i || e !== L ? Ot(e, i, r) : e._time) + r._delay)),
      (r._end = la(r._start + (r.totalDuration() / Math.abs(r.timeScale()) || 0))),
      Aa(e, r, "_first", "_last", e._sort ? "_start" : 0),
      xt(r) || (e._recent = r),
      n || Ma(e, r),
      e._ts < 0 && La(e, e._tTime),
      e
    );
  }
  function Oa(t, e) {
    return (ht.ScrollTrigger || S("scrollTrigger", e)) && ht.ScrollTrigger.create(e, t);
  }
  function Pa(t, e, r, i, n) {
    return (
      Qt(t, e, n),
      t._initted
        ? !r &&
          t._pt &&
          !I &&
          ((t._dur && !1 !== t.vars.lazy) || (!t._dur && t.vars.lazy)) &&
          f !== It.frame
          ? (pt.push(t), (t._lazy = [n, i]), 1)
          : void 0
        : 1
    );
  }
  function Ua(t, e, r, i) {
    var n = t._repeat,
      a = la(e) || 0,
      s = t._tTime / t._tDur;
    return (
      s && !i && (t._time *= a / t._dur),
      (t._dur = a),
      (t._tDur = n ? (n < 0 ? 1e10 : la(a * (n + 1) + t._rDelay * n)) : a),
      0 < s && !i && La(t, (t._tTime = t._tDur * s)),
      t.parent && Ka(t),
      r || Da(t.parent, t),
      t
    );
  }
  function Va(t) {
    return t instanceof Zt ? Da(t) : Ua(t, t._dur);
  }
  function Ya(e, r, i) {
    var n,
      a,
      s = t(r[1]),
      o = (s ? 2 : 1) + (e < 2 ? 0 : 1),
      u = r[o];
    if ((s && (u.duration = r[1]), (u.parent = i), e)) {
      for (n = u, a = i; a && !("immediateRender" in n); )
        ((n = a.vars.defaults || {}), (a = w(a.vars.inherit) && a.parent));
      ((u.immediateRender = w(n.immediateRender)),
        e < 2 ? (u.runBackwards = 1) : (u.startAt = r[o - 1]));
    }
    return new te(r[0], u, r[1 + o]);
  }
  function Za(t, e) {
    return t || 0 === t ? e(t) : e;
  }
  function _a(t, e) {
    return r(t) && (e = ut.exec(t)) ? e[1] : "";
  }
  function cb(t, e) {
    return (
      t &&
      v(t) &&
      "length" in t &&
      ((!e && !t.length) || (t.length - 1 in t && v(t[0]))) &&
      !t.nodeType &&
      t !== h
    );
  }
  function fb(r) {
    return (
      (r = Pt(r)[0] || T("Invalid scope") || {}),
      function (t) {
        var e = r.current || r.nativeElement || r;
        return Pt(
          t,
          e.querySelectorAll ? e : e === r ? T("Invalid scope") || a.createElement("div") : r,
        );
      }
    );
  }
  function gb(t) {
    return t.sort(function () {
      return 0.5 - Math.random();
    });
  }
  function hb(t) {
    if (s(t)) return t;
    var p = v(t) ? t : { each: t },
      _ = Vt(p.ease),
      m = p.from || 0,
      g = parseFloat(p.base) || 0,
      y = {},
      e = 0 < m && m < 1,
      T = isNaN(m) || e,
      b = p.axis,
      w = m,
      x = m;
    return (
      r(m)
        ? (w = x = { center: 0.5, edges: 0.5, end: 1 }[m] || 0)
        : !e && T && ((w = m[0]), (x = m[1])),
      function (t, e, r) {
        var i,
          n,
          a,
          s,
          o,
          u,
          h,
          l,
          f,
          d = (r || p).length,
          c = y[d];
        if (!c) {
          if (!(f = "auto" === p.grid ? 0 : (p.grid || [1, X])[1])) {
            for (h = -X; h < (h = r[f++].getBoundingClientRect().left) && f < d; );
            f < d && f--;
          }
          for (
            c = y[d] = [],
              i = T ? Math.min(f, d) * w - 0.5 : m % f,
              n = f === X ? 0 : T ? (d * x) / f - 0.5 : (m / f) | 0,
              l = X,
              u = h = 0;
            u < d;
            u++
          )
            ((a = (u % f) - i),
              (s = n - ((u / f) | 0)),
              (c[u] = o = b ? Math.abs("y" === b ? s : a) : J(a * a + s * s)),
              h < o && (h = o),
              o < l && (l = o));
          ("random" === m && gb(c),
            (c.max = h - l),
            (c.min = l),
            (c.v = d =
              (parseFloat(p.amount) ||
                parseFloat(p.each) *
                  (d < f ? d - 1 : b ? ("y" === b ? d / f : f) : Math.max(f, d / f)) ||
                0) * ("edges" === m ? -1 : 1)),
            (c.b = d < 0 ? g - d : g),
            (c.u = _a(p.amount || p.each) || 0),
            (_ = _ && d < 0 ? jt(_) : _));
        }
        return ((d = (c[t] - c.min) / c.max || 0), la(c.b + (_ ? _(d) : d) * c.v) + c.u);
      }
    );
  }
  function ib(i) {
    var n = Math.pow(10, ((i + "").split(".")[1] || "").length);
    return function (e) {
      var r = la(Math.round(parseFloat(e) / i) * i * n);
      return (r - (r % 1)) / n + (t(e) ? 0 : _a(e));
    };
  }
  function jb(h, e) {
    var l,
      f,
      r = $(h);
    return (
      !r &&
        v(h) &&
        ((l = r = h.radius || X),
        h.values ? ((h = Pt(h.values)), (f = !t(h[0])) && (l *= l)) : (h = ib(h.increment))),
      Za(
        e,
        r
          ? s(h)
            ? function (t) {
                return ((f = h(t)), Math.abs(f - t) <= l ? f : t);
              }
            : function (e) {
                for (
                  var r,
                    i,
                    n = parseFloat(f ? e.x : e),
                    a = parseFloat(f ? e.y : 0),
                    s = X,
                    o = 0,
                    u = h.length;
                  u--;
                )
                  (r = f ? (r = h[u].x - n) * r + (i = h[u].y - a) * i : Math.abs(h[u] - n)) < s &&
                    ((s = r), (o = u));
                return ((o = !l || s <= l ? h[o] : e), f || o === e || t(e) ? o : o + _a(e));
              }
          : ib(h),
      )
    );
  }
  function kb(t, e, r, i) {
    return Za($(t) ? !e : !0 === r ? !!(r = 0) : !i, function () {
      return $(t)
        ? t[~~(Math.random() * t.length)]
        : (r = r || 1e-5) &&
            (i = r < 1 ? Math.pow(10, (r + "").length - 2) : 1) &&
            Math.floor(Math.round((t - r / 2 + Math.random() * (e - t + 0.99 * r)) / r) * r * i) /
              i;
    });
  }
  function ob(e, r, t) {
    return Za(t, function (t) {
      return e[~~r(t)];
    });
  }
  function rb(t) {
    return t.replace(tt, function (t) {
      var e = t.indexOf("[") + 1,
        r = t.substring(e || 7, e ? t.indexOf("]") : t.length - 1).split(et);
      return kb(e ? r : +r[0], e ? 0 : +r[1], +r[2] || 1e-5);
    });
  }
  function ub(t, e, r) {
    var i,
      n,
      a,
      s = t.labels,
      o = X;
    for (i in s) (n = s[i] - e) < 0 == !!r && n && o > (n = Math.abs(n)) && ((a = i), (o = n));
    return a;
  }
  function wb(t) {
    return (
      Ca(t),
      t.scrollTrigger && t.scrollTrigger.kill(!!I),
      t.progress() < 1 && Dt(t, "onInterrupt"),
      t
    );
  }
  function zb(t) {
    if (t)
      if (((t = (!t.name && t.default) || t), x() || t.headless)) {
        var e = t.name,
          r = s(t),
          i =
            e && !r && t.init
              ? function () {
                  this._props = [];
                }
              : t,
          n = { init: V, render: ve, add: Jt, kill: Te, modifier: ye, rawVars: 0 },
          a = { targetTest: 0, get: 0, getSetter: le, aliases: {}, register: 0 };
        if ((Lt(), t !== i)) {
          if (mt[e]) return;
          (ta(i, ta(xa(t, n), a)),
            bt(i.prototype, bt(n, xa(t, a))),
            (mt[(i.prop = e)] = i),
            t.targetTest && (yt.push(i), (ct[e] = 1)),
            (e = ("css" === e ? "CSS" : e.charAt(0).toUpperCase() + e.substr(1)) + "Plugin"));
        }
        (U(e, i), t.register && t.register(Fe, i, we));
      } else St.push(t);
  }
  function Cb(t, e, r) {
    return (
      ((6 * (t += t < 0 ? 1 : 1 < t ? -1 : 0) < 1
        ? e + (r - e) * t * 6
        : t < 0.5
          ? r
          : 3 * t < 2
            ? e + (r - e) * (2 / 3 - t) * 6
            : e) *
        zt +
        0.5) |
      0
    );
  }
  function Db(e, r, i) {
    var n,
      a,
      s,
      o,
      u,
      h,
      l,
      f,
      d,
      c,
      p = e ? (t(e) ? [e >> 16, (e >> 8) & zt, e & zt] : 0) : Et.black;
    if (!p) {
      if (("," === e.substr(-1) && (e = e.substr(0, e.length - 1)), Et[e])) p = Et[e];
      else if ("#" === e.charAt(0)) {
        if (
          (e.length < 6 &&
            (e =
              "#" +
              (n = e.charAt(1)) +
              n +
              (a = e.charAt(2)) +
              a +
              (s = e.charAt(3)) +
              s +
              (5 === e.length ? e.charAt(4) + e.charAt(4) : "")),
          9 === e.length)
        )
          return [
            (p = parseInt(e.substr(1, 6), 16)) >> 16,
            (p >> 8) & zt,
            p & zt,
            parseInt(e.substr(7), 16) / 255,
          ];
        p = [(e = parseInt(e.substr(1), 16)) >> 16, (e >> 8) & zt, e & zt];
      } else if ("hsl" === e.substr(0, 3))
        if (((p = c = e.match(rt)), r)) {
          if (~e.indexOf("=")) return ((p = e.match(it)), i && p.length < 4 && (p[3] = 1), p);
        } else
          ((o = (+p[0] % 360) / 360),
            (u = p[1] / 100),
            (n = 2 * (h = p[2] / 100) - (a = h <= 0.5 ? h * (u + 1) : h + u - h * u)),
            3 < p.length && (p[3] *= 1),
            (p[0] = Cb(o + 1 / 3, n, a)),
            (p[1] = Cb(o, n, a)),
            (p[2] = Cb(o - 1 / 3, n, a)));
      else p = e.match(rt) || Et.transparent;
      p = p.map(Number);
    }
    return (
      r &&
        !c &&
        ((n = p[0] / zt),
        (a = p[1] / zt),
        (s = p[2] / zt),
        (h = ((l = Math.max(n, a, s)) + (f = Math.min(n, a, s))) / 2),
        l === f
          ? (o = u = 0)
          : ((d = l - f),
            (u = 0.5 < h ? d / (2 - l - f) : d / (l + f)),
            (o =
              l === n
                ? (a - s) / d + (a < s ? 6 : 0)
                : l === a
                  ? (s - n) / d + 2
                  : (n - a) / d + 4),
            (o *= 60)),
        (p[0] = ~~(o + 0.5)),
        (p[1] = ~~(100 * u + 0.5)),
        (p[2] = ~~(100 * h + 0.5))),
      i && p.length < 4 && (p[3] = 1),
      p
    );
  }
  function Eb(t) {
    var r = [],
      i = [],
      n = -1;
    return (
      t.split(Rt).forEach(function (t) {
        var e = t.match(nt) || [];
        (r.push.apply(r, e), i.push((n += e.length + 1)));
      }),
      (r.c = i),
      r
    );
  }
  function Fb(t, e, r) {
    var i,
      n,
      a,
      s,
      o = "",
      u = (t + o).match(Rt),
      h = e ? "hsla(" : "rgba(",
      l = 0;
    if (!u) return t;
    if (
      ((u = u.map(function (t) {
        return (
          (t = Db(t, e, 1)) &&
          h + (e ? t[0] + "," + t[1] + "%," + t[2] + "%," + t[3] : t.join(",")) + ")"
        );
      })),
      r && ((a = Eb(t)), (i = r.c).join(o) !== a.c.join(o)))
    )
      for (s = (n = t.replace(Rt, "1").split(nt)).length - 1; l < s; l++)
        o +=
          n[l] +
          (~i.indexOf(l) ? u.shift() || h + "0,0,0,0)" : (a.length ? a : u.length ? u : r).shift());
    if (!n) for (s = (n = t.split(Rt)).length - 1; l < s; l++) o += n[l] + u[l];
    return o + n[s];
  }
  function Ib(t) {
    var e,
      r = t.join(" ");
    if (((Rt.lastIndex = 0), Rt.test(r)))
      return ((e = Ft.test(r)), (t[1] = Fb(t[1], e)), (t[0] = Fb(t[0], e, Eb(t[1]))), !0);
  }
  function Rb(t) {
    var e = (t + "").split("("),
      r = Bt[e[0]];
    return r && 1 < e.length && r.config
      ? r.config.apply(
          null,
          ~t.indexOf("{")
            ? [
                (function _parseObjectInString(t) {
                  for (
                    var e,
                      r,
                      i,
                      n = {},
                      a = t.substr(1, t.length - 3).split(":"),
                      s = a[0],
                      o = 1,
                      u = a.length;
                    o < u;
                    o++
                  )
                    ((r = a[o]),
                      (e = o !== u - 1 ? r.lastIndexOf(",") : r.length),
                      (i = r.substr(0, e)),
                      (n[s] = isNaN(i) ? i.replace(Nt, "").trim() : +i),
                      (s = r.substr(e + 1).trim()));
                  return n;
                })(e[1]),
              ]
            : (function _valueInParentheses(t) {
                var e = t.indexOf("(") + 1,
                  r = t.indexOf(")"),
                  i = t.indexOf("(", e);
                return t.substring(e, ~i && i < r ? t.indexOf(")", r + 1) : r);
              })(t)
                .split(",")
                .map(ra),
        )
      : Bt._CE && Yt.test(t)
        ? Bt._CE("", t)
        : r;
  }
  function Tb(t, e) {
    for (var r, i = t._first; i; )
      (i instanceof Zt
        ? Tb(i, e)
        : !i.vars.yoyoEase ||
          (i._yoyo && i._repeat) ||
          i._yoyo === e ||
          (i.timeline
            ? Tb(i.timeline, e)
            : ((r = i._ease), (i._ease = i._yEase), (i._yEase = r), (i._yoyo = e))),
        (i = i._next));
  }
  function Vb(t, e, r, i) {
    (void 0 === r &&
      (r = function easeOut(t) {
        return 1 - e(1 - t);
      }),
      void 0 === i &&
        (i = function easeInOut(t) {
          return t < 0.5 ? e(2 * t) / 2 : 1 - e(2 * (1 - t)) / 2;
        }));
    var n,
      a = { easeIn: e, easeOut: r, easeInOut: i };
    return (
      ja(t, function (t) {
        for (var e in ((Bt[t] = ht[t] = a), (Bt[(n = t.toLowerCase())] = r), a))
          Bt[n + ("easeIn" === e ? ".in" : "easeOut" === e ? ".out" : ".inOut")] = Bt[t + "." + e] =
            a[e];
      }),
      a
    );
  }
  function Wb(e) {
    return function (t) {
      return t < 0.5 ? (1 - e(1 - 2 * t)) / 2 : 0.5 + e(2 * (t - 0.5)) / 2;
    };
  }
  function Xb(r, t, e) {
    function Lm(t) {
      return 1 === t ? 1 : i * Math.pow(2, -10 * t) * G((t - a) * n) + 1;
    }
    var i = 1 <= t ? t : 1,
      n = (e || (r ? 0.3 : 0.45)) / (t < 1 ? t : 1),
      a = (n / Z) * (Math.asin(1 / i) || 0),
      s =
        "out" === r
          ? Lm
          : "in" === r
            ? function (t) {
                return 1 - Lm(1 - t);
              }
            : Wb(Lm);
    return (
      (n = Z / n),
      (s.config = function (t, e) {
        return Xb(r, t, e);
      }),
      s
    );
  }
  function Yb(e, r) {
    function Tm(t) {
      return t ? --t * t * ((r + 1) * t + r) + 1 : 0;
    }
    void 0 === r && (r = 1.70158);
    var t =
      "out" === e
        ? Tm
        : "in" === e
          ? function (t) {
              return 1 - Tm(1 - t);
            }
          : Wb(Tm);
    return (
      (t.config = function (t) {
        return Yb(e, t);
      }),
      t
    );
  }
  var F,
    I,
    l,
    L,
    h,
    n,
    a,
    i,
    o,
    f,
    d,
    c,
    p,
    _,
    m,
    g,
    b,
    k,
    O,
    M,
    C,
    P,
    A,
    D,
    z,
    E,
    B,
    Y,
    N = { autoSleep: 120, force3D: "auto", nullTargetWarn: 1, units: { lineHeight: "" } },
    j = { duration: 0.5, overwrite: !1, delay: 0 },
    X = 1e8,
    q = 1 / X,
    Z = 2 * Math.PI,
    W = Z / 4,
    H = 0,
    J = Math.sqrt,
    Q = Math.cos,
    G = Math.sin,
    K = ("function" == typeof ArrayBuffer && ArrayBuffer.isView) || function () {},
    $ = Array.isArray,
    tt = /random\([^)]+\)/g,
    et = /,\s*/g,
    rt = /(?:-?\.?\d|\.)+/gi,
    it = /[-+=.]*\d+[.e\-+]*\d*[e\-+]*\d*/g,
    nt = /[-+=.]*\d+[.e-]*\d*[a-z%]*/g,
    at = /[-+=.]*\d+\.?\d*(?:e-|e\+)?\d*/gi,
    st = /[+-]=-?[.\d]+/,
    ot = /[^,'"\[\]\s]+/gi,
    ut = /^[+\-=e\s\d]*\d+[.\d]*([a-z]*|%)\s*$/i,
    ht = {},
    lt = { suppressEvents: !0, isStart: !0, kill: !1 },
    ft = { suppressEvents: !0, kill: !1 },
    dt = { suppressEvents: !0 },
    ct = {},
    pt = [],
    _t = {},
    mt = {},
    gt = {},
    vt = 30,
    yt = [],
    Tt = "",
    bt = function _merge(t, e) {
      for (var r in e) t[r] = e[r];
      return t;
    },
    wt = function _animationCycle(t, e) {
      var r = Math.floor((t = la(t / e)));
      return t && r === t ? r - 1 : r;
    },
    xt = function _isFromOrFromStart(t) {
      var e = t.data;
      return "isFromStart" === e || "isStart" === e;
    },
    kt = { _start: 0, endTime: V, totalDuration: V },
    Ot = function _parsePosition(t, e, i) {
      var n,
        a,
        s,
        o = t.labels,
        u = t._recent || kt,
        h = t.duration() >= X ? u.endTime(!1) : t._dur;
      return r(e) && (isNaN(e) || e in o)
        ? ((a = e.charAt(0)),
          (s = "%" === e.substr(-1)),
          (n = e.indexOf("=")),
          "<" === a || ">" === a
            ? (0 <= n && (e = e.replace(/=/, "")),
              ("<" === a ? u._start : u.endTime(0 <= u._repeat)) +
                (parseFloat(e.substr(1)) || 0) * (s ? (n < 0 ? u : i).totalDuration() / 100 : 1))
            : n < 0
              ? (e in o || (o[e] = h), o[e])
              : ((a = parseFloat(e.charAt(n - 1) + e.substr(n + 1))),
                s && i && (a = (a / 100) * ($(i) ? i[0] : i).totalDuration()),
                1 < n ? _parsePosition(t, e.substr(0, n - 1), i) + a : h + a))
        : null == e
          ? h
          : +e;
    },
    Mt = function _clamp(t, e, r) {
      return r < t ? t : e < r ? e : r;
    },
    Ct = [].slice,
    Pt = function toArray(t, e, i) {
      return l && !e && l.selector
        ? l.selector(t)
        : !r(t) || i || (!n && Lt())
          ? $(t)
            ? (function _flatten(t, e, i) {
                return (
                  void 0 === i && (i = []),
                  t.forEach(function (t) {
                    return (r(t) && !e) || cb(t, 1) ? i.push.apply(i, Pt(t)) : i.push(t);
                  }) || i
                );
              })(t, i)
            : cb(t)
              ? Ct.call(t, 0)
              : t
                ? [t]
                : []
          : Ct.call((e || a).querySelectorAll(t), 0);
    },
    At = function mapRange(e, t, r, i, n) {
      var a = t - e,
        s = i - r;
      return Za(n, function (t) {
        return r + (((t - e) / a) * s || 0);
      });
    },
    Dt = function _callback(t, e, r) {
      var i,
        n,
        a,
        s = t.vars,
        o = s[e],
        u = l,
        h = t._ctx;
      if (o)
        return (
          (i = s[e + "Params"]),
          (n = s.callbackScope || t),
          r && pt.length && oa(),
          h && (l = h),
          (a = i ? o.apply(n, i) : o.call(n)),
          (l = u),
          a
        );
    },
    St = [],
    zt = 255,
    Et = {
      aqua: [0, zt, zt],
      lime: [0, zt, 0],
      silver: [192, 192, 192],
      black: [0, 0, 0],
      maroon: [128, 0, 0],
      teal: [0, 128, 128],
      blue: [0, 0, zt],
      navy: [0, 0, 128],
      white: [zt, zt, zt],
      olive: [128, 128, 0],
      yellow: [zt, zt, 0],
      orange: [zt, 165, 0],
      gray: [128, 128, 128],
      purple: [128, 0, 128],
      green: [0, 128, 0],
      red: [zt, 0, 0],
      pink: [zt, 192, 203],
      cyan: [0, zt, zt],
      transparent: [zt, zt, zt, 0],
    },
    Rt = (function () {
      var t,
        e = "(?:\\b(?:(?:rgb|rgba|hsl|hsla)\\(.+?\\))|\\B#(?:[0-9a-f]{3,4}){1,2}\\b";
      for (t in Et) e += "|" + t + "\\b";
      return new RegExp(e + ")", "gi");
    })(),
    Ft = /hsl[a]?\(/,
    It =
      ((O = Date.now),
      (M = 500),
      (C = 33),
      (P = O()),
      (A = P),
      (z = D = 1e3 / 240),
      (g = {
        time: 0,
        frame: 0,
        tick: function tick() {
          Al(!0);
        },
        deltaRatio: function deltaRatio(t) {
          return b / (1e3 / (t || 60));
        },
        wake: function wake() {
          o &&
            (!n &&
              x() &&
              ((h = n = window),
              (a = h.document || {}),
              (ht.gsap = Fe),
              (h.gsapVersions || (h.gsapVersions = [])).push(Fe.version),
              R(i || h.GreenSockGlobals || (!h.gsap && h) || {}),
              St.forEach(zb)),
            (m = "undefined" != typeof requestAnimationFrame && requestAnimationFrame),
            p && g.sleep(),
            (_ =
              m ||
              function (t) {
                return setTimeout(t, (z - 1e3 * g.time + 1) | 0);
              }),
            (c = 1),
            Al(2));
        },
        sleep: function sleep() {
          ((m ? cancelAnimationFrame : clearTimeout)(p), (c = 0), (_ = V));
        },
        lagSmoothing: function lagSmoothing(t, e) {
          ((M = t || 1 / 0), (C = Math.min(e || 33, M)));
        },
        fps: function fps(t) {
          ((D = 1e3 / (t || 240)), (z = 1e3 * g.time + D));
        },
        add: function add(n, t, e) {
          var a = t
            ? function (t, e, r, i) {
                (n(t, e, r, i), g.remove(a));
              }
            : n;
          return (g.remove(n), E[e ? "unshift" : "push"](a), Lt(), a);
        },
        remove: function remove(t, e) {
          ~(e = E.indexOf(t)) && E.splice(e, 1) && e <= k && k--;
        },
        _listeners: (E = []),
      })),
    Lt = function _wake() {
      return !c && It.wake();
    },
    Bt = {},
    Yt = /^[\d.\-M][\d.\-,\s]/,
    Nt = /["']/g,
    jt = function _invertEase(e) {
      return function (t) {
        return 1 - e(1 - t);
      };
    },
    Vt = function _parseEase(t, e) {
      return (t && (s(t) ? t : Bt[t] || Rb(t))) || e;
    };
  function Al(t) {
    var e,
      r,
      i,
      n,
      a = O() - A,
      s = !0 === t;
    if (
      ((M < a || a < 0) && (P += a - C),
      (0 < (e = (i = (A += a) - P) - z) || s) &&
        ((n = ++g.frame),
        (b = i - 1e3 * g.time),
        (g.time = i /= 1e3),
        (z += e + (D <= e ? 4 : D - e)),
        (r = 1)),
      s || (p = _(Al)),
      r)
    )
      for (k = 0; k < E.length; k++) E[k](i, b, n, t);
  }
  function jn(t) {
    return t < Y
      ? B * t * t
      : t < 0.7272727272727273
        ? B * Math.pow(t - 1.5 / 2.75, 2) + 0.75
        : t < 0.9090909090909092
          ? B * (t -= 2.25 / 2.75) * t + 0.9375
          : B * Math.pow(t - 2.625 / 2.75, 2) + 0.984375;
  }
  (ja("Linear,Quad,Cubic,Quart,Quint,Strong", function (t, e) {
    var r = e < 5 ? e + 1 : e;
    Vb(
      t + ",Power" + (r - 1),
      e
        ? function (t) {
            return Math.pow(t, r);
          }
        : function (t) {
            return t;
          },
      function (t) {
        return 1 - Math.pow(1 - t, r);
      },
      function (t) {
        return t < 0.5 ? Math.pow(2 * t, r) / 2 : 1 - Math.pow(2 * (1 - t), r) / 2;
      },
    );
  }),
    (Bt.Linear.easeNone = Bt.none = Bt.Linear.easeIn),
    Vb("Elastic", Xb("in"), Xb("out"), Xb()),
    (B = 7.5625),
    (Y = 1 / 2.75),
    Vb(
      "Bounce",
      function (t) {
        return 1 - jn(1 - t);
      },
      jn,
    ),
    Vb("Expo", function (t) {
      return Math.pow(2, 10 * (t - 1)) * t + t * t * t * t * t * t * (1 - t);
    }),
    Vb("Circ", function (t) {
      return -(J(1 - t * t) - 1);
    }),
    Vb("Sine", function (t) {
      return 1 === t ? 1 : 1 - Q(t * W);
    }),
    Vb("Back", Yb("in"), Yb("out"), Yb()),
    (Bt.SteppedEase =
      Bt.steps =
      ht.SteppedEase =
        {
          config: function config(t, e) {
            void 0 === t && (t = 1);
            var r = 1 / t,
              i = t + (e ? 0 : 1),
              n = e ? 1 : 0;
            return function (t) {
              return (((i * Mt(0, 0.99999999, t)) | 0) + n) * r;
            };
          },
        }),
    (j.ease = Bt["quad.out"]),
    ja("onComplete,onUpdate,onStart,onRepeat,onReverseComplete,onInterrupt", function (t) {
      return (Tt += t + "," + t + "Params,");
    }));
  var Ut,
    Xt = function GSCache(t, e) {
      ((this.id = H++),
        ((t._gsap = this).target = t),
        (this.harness = e),
        (this.get = e ? e.get : ia),
        (this.set = e ? e.getSetter : le));
    },
    qt =
      (((Ut = Animation.prototype).delay = function delay(t) {
        return t || 0 === t
          ? (this.parent &&
              this.parent.smoothChildTiming &&
              this.startTime(this._start + t - this._delay),
            (this._delay = t),
            this)
          : this._delay;
      }),
      (Ut.duration = function duration(t) {
        return arguments.length
          ? this.totalDuration(0 < this._repeat ? t + (t + this._rDelay) * this._repeat : t)
          : this.totalDuration() && this._dur;
      }),
      (Ut.totalDuration = function totalDuration(t) {
        return arguments.length
          ? ((this._dirty = 0),
            Ua(this, this._repeat < 0 ? t : (t - this._repeat * this._rDelay) / (this._repeat + 1)))
          : this._tDur;
      }),
      (Ut.totalTime = function totalTime(t, e) {
        if ((Lt(), !arguments.length)) return this._tTime;
        var r = this._dp;
        if (r && r.smoothChildTiming && this._ts) {
          for (La(this, t), !r._dp || r.parent || Ma(r, this); r && r.parent; )
            (r.parent._time !==
              r._start +
                (0 <= r._ts ? r._tTime / r._ts : (r.totalDuration() - r._tTime) / -r._ts) &&
              r.totalTime(r._tTime, !0),
              (r = r.parent));
          !this.parent &&
            this._dp.autoRemoveChildren &&
            ((0 < this._ts && t < this._tDur) || (this._ts < 0 && 0 < t) || (!this._tDur && !t)) &&
            Na(this._dp, this, this._start - this._delay);
        }
        return (
          (this._tTime !== t ||
            (!this._dur && !e) ||
            (this._initted && Math.abs(this._zTime) === q) ||
            (!this._initted && this._dur && t) ||
            (!t && !this._initted && (this.add || this._ptLookup))) &&
            (this._ts || (this._pTime = t), qa(this, t, e)),
          this
        );
      }),
      (Ut.time = function time(t, e) {
        return arguments.length
          ? this.totalTime(
              Math.min(this.totalDuration(), t + Ha(this)) % (this._dur + this._rDelay) ||
                (t ? this._dur : 0),
              e,
            )
          : this._time;
      }),
      (Ut.totalProgress = function totalProgress(t, e) {
        return arguments.length
          ? this.totalTime(this.totalDuration() * t, e)
          : this.totalDuration()
            ? Math.min(1, this._tTime / this._tDur)
            : 0 <= this.rawTime() && this._initted
              ? 1
              : 0;
      }),
      (Ut.progress = function progress(t, e) {
        return arguments.length
          ? this.totalTime(
              this.duration() * (!this._yoyo || 1 & this.iteration() ? t : 1 - t) + Ha(this),
              e,
            )
          : this.duration()
            ? Math.min(1, this._time / this._dur)
            : 0 < this.rawTime()
              ? 1
              : 0;
      }),
      (Ut.iteration = function iteration(t, e) {
        var r = this.duration() + this._rDelay;
        return arguments.length
          ? this.totalTime(this._time + (t - 1) * r, e)
          : this._repeat
            ? wt(this._tTime, r) + 1
            : 1;
      }),
      (Ut.timeScale = function timeScale(t, e) {
        if (!arguments.length) return this._rts === -q ? 0 : this._rts;
        if (this._rts === t) return this;
        var r = this.parent && this._ts ? Ja(this.parent._time, this) : this._tTime;
        return (
          (this._rts = +t || 0),
          (this._ts = this._ps || t === -q ? 0 : this._rts),
          this.totalTime(Mt(-Math.abs(this._delay), this.totalDuration(), r), !1 !== e),
          Ka(this),
          (function _recacheAncestors(t) {
            for (var e = t.parent; e && e.parent; )
              ((e._dirty = 1), e.totalDuration(), (e = e.parent));
            return t;
          })(this)
        );
      }),
      (Ut.paused = function paused(t) {
        return arguments.length
          ? (this._ps !== t &&
              ((this._ps = t)
                ? ((this._pTime = this._tTime || Math.max(-this._delay, this.rawTime())),
                  (this._ts = this._act = 0))
                : (Lt(),
                  (this._ts = this._rts),
                  this.totalTime(
                    this.parent && !this.parent.smoothChildTiming
                      ? this.rawTime()
                      : this._tTime || this._pTime,
                    1 === this.progress() && Math.abs(this._zTime) !== q && (this._tTime -= q),
                  ))),
            this)
          : this._ps;
      }),
      (Ut.startTime = function startTime(t) {
        if (arguments.length) {
          this._start = la(t);
          var e = this.parent || this._dp;
          return (!e || (!e._sort && this.parent) || Na(e, this, this._start - this._delay), this);
        }
        return this._start;
      }),
      (Ut.endTime = function endTime(t) {
        return (
          this._start + (w(t) ? this.totalDuration() : this.duration()) / Math.abs(this._ts || 1)
        );
      }),
      (Ut.rawTime = function rawTime(t) {
        var e = this.parent || this._dp;
        return e
          ? t && (!this._ts || (this._repeat && this._time && this.totalProgress() < 1))
            ? this._tTime % (this._dur + this._rDelay)
            : this._ts
              ? Ja(e.rawTime(t), this)
              : this._tTime
          : this._tTime;
      }),
      (Ut.revert = function revert(t) {
        void 0 === t && (t = dt);
        var e = I;
        return (
          (I = t),
          pa(this) &&
            (this.timeline && this.timeline.revert(t), this.totalTime(-0.01, t.suppressEvents)),
          "nested" !== this.data && !1 !== t.kill && this.kill(),
          (I = e),
          this
        );
      }),
      (Ut.globalTime = function globalTime(t) {
        for (var e = this, r = arguments.length ? t : e.rawTime(); e; )
          ((r = e._start + r / (Math.abs(e._ts) || 1)), (e = e._dp));
        return !this.parent && this._sat ? this._sat.globalTime(t) : r;
      }),
      (Ut.repeat = function repeat(t) {
        return arguments.length
          ? ((this._repeat = t === 1 / 0 ? -2 : t), Va(this))
          : -2 === this._repeat
            ? 1 / 0
            : this._repeat;
      }),
      (Ut.repeatDelay = function repeatDelay(t) {
        if (arguments.length) {
          var e = this._time;
          return ((this._rDelay = t), Va(this), e ? this.time(e) : this);
        }
        return this._rDelay;
      }),
      (Ut.yoyo = function yoyo(t) {
        return arguments.length ? ((this._yoyo = t), this) : this._yoyo;
      }),
      (Ut.seek = function seek(t, e) {
        return this.totalTime(Ot(this, t), w(e));
      }),
      (Ut.restart = function restart(t, e) {
        return (
          this.play().totalTime(t ? -this._delay : 0, w(e)), this._dur || (this._zTime = -q), this
        );
      }),
      (Ut.play = function play(t, e) {
        return (null != t && this.seek(t, e), this.reversed(!1).paused(!1));
      }),
      (Ut.reverse = function reverse(t, e) {
        return (null != t && this.seek(t || this.totalDuration(), e), this.reversed(!0).paused(!1));
      }),
      (Ut.pause = function pause(t, e) {
        return (null != t && this.seek(t, e), this.paused(!0));
      }),
      (Ut.resume = function resume() {
        return this.paused(!1);
      }),
      (Ut.reversed = function reversed(t) {
        return arguments.length
          ? (!!t !== this.reversed() && this.timeScale(-this._rts || (t ? -q : 0)), this)
          : this._rts < 0;
      }),
      (Ut.invalidate = function invalidate() {
        return ((this._initted = this._act = 0), (this._zTime = -q), this);
      }),
      (Ut.isActive = function isActive() {
        var t,
          e = this.parent || this._dp,
          r = this._start;
        return !(
          e &&
          !(
            this._ts &&
            this._initted &&
            e.isActive() &&
            (t = e.rawTime(!0)) >= r &&
            t < this.endTime(!0) - q
          )
        );
      }),
      (Ut.eventCallback = function eventCallback(t, e, r) {
        var i = this.vars;
        return 1 < arguments.length
          ? (e
              ? ((i[t] = e), r && (i[t + "Params"] = r), "onUpdate" === t && (this._onUpdate = e))
              : delete i[t],
            this)
          : i[t];
      }),
      (Ut.then = function then(t) {
        var i = this,
          n = i._prom;
        return new Promise(function (e) {
          function Fo() {
            var t = i.then;
            ((i.then = null),
              n && n(),
              s(r) && (r = r(i)) && (r.then || r === i) && (i.then = t),
              e(r),
              (i.then = t));
          }
          var r = s(t) ? t : sa;
          (i._initted && 1 === i.totalProgress() && 0 <= i._ts) || (!i._tTime && i._ts < 0)
            ? Fo()
            : (i._prom = Fo);
        });
      }),
      (Ut.kill = function kill() {
        wb(this);
      }),
      Animation);
  function Animation(t) {
    ((this.vars = t),
      (this._delay = +t.delay || 0),
      (this._repeat = t.repeat === 1 / 0 ? -2 : t.repeat || 0) &&
        ((this._rDelay = t.repeatDelay || 0), (this._yoyo = !!t.yoyo || !!t.yoyoEase)),
      (this._ts = 1),
      Ua(this, +t.duration, 1, 1),
      (this.data = t.data),
      l && (this._ctx = l).data.push(this),
      c || It.wake());
  }
  ta(qt.prototype, {
    _time: 0,
    _start: 0,
    _end: 0,
    _tTime: 0,
    _tDur: 0,
    _dirty: 0,
    _repeat: 0,
    _yoyo: !1,
    parent: null,
    _initted: !1,
    _rDelay: 0,
    _ts: 1,
    _dp: 0,
    ratio: 0,
    _zTime: -q,
    _prom: 0,
    _ps: !1,
    _rts: 1,
  });
  var Zt = (function (i) {
    function Timeline(t, e) {
      var r;
      return (
        void 0 === t && (t = {}),
        ((r = i.call(this, t) || this).labels = {}),
        (r.smoothChildTiming = !!t.smoothChildTiming),
        (r.autoRemoveChildren = !!t.autoRemoveChildren),
        (r._sort = w(t.sortChildren)),
        L && Na(t.parent || L, _assertThisInitialized(r), e),
        t.reversed && r.reverse(),
        t.paused && r.paused(!0),
        t.scrollTrigger && Oa(_assertThisInitialized(r), t.scrollTrigger),
        r
      );
    }
    _inheritsLoose(Timeline, i);
    var e = Timeline.prototype;
    return (
      (e.to = function to(t, e, r) {
        return (Ya(0, arguments, this), this);
      }),
      (e.from = function from(t, e, r) {
        return (Ya(1, arguments, this), this);
      }),
      (e.fromTo = function fromTo(t, e, r, i) {
        return (Ya(2, arguments, this), this);
      }),
      (e.set = function set(t, e, r) {
        return (
          (e.duration = 0),
          (e.parent = this),
          ya(e).repeatDelay || (e.repeat = 0),
          (e.immediateRender = !!e.immediateRender),
          new te(t, e, Ot(this, r), 1),
          this
        );
      }),
      (e.call = function call(t, e, r) {
        return Na(this, te.delayedCall(0, t, e), r);
      }),
      (e.staggerTo = function staggerTo(t, e, r, i, n, a, s) {
        return (
          (r.duration = e),
          (r.stagger = r.stagger || i),
          (r.onComplete = a),
          (r.onCompleteParams = s),
          (r.parent = this),
          new te(t, r, Ot(this, n)),
          this
        );
      }),
      (e.staggerFrom = function staggerFrom(t, e, r, i, n, a, s) {
        return (
          (r.runBackwards = 1),
          (ya(r).immediateRender = w(r.immediateRender)),
          this.staggerTo(t, e, r, i, n, a, s)
        );
      }),
      (e.staggerFromTo = function staggerFromTo(t, e, r, i, n, a, s, o) {
        return (
          (i.startAt = r),
          (ya(i).immediateRender = w(i.immediateRender)),
          this.staggerTo(t, e, i, n, a, s, o)
        );
      }),
      (e.render = function render(t, e, r) {
        var i,
          n,
          a,
          s,
          o,
          u,
          h,
          l,
          f,
          d,
          c,
          p,
          _ = this._time,
          m = this._dirty ? this.totalDuration() : this._tDur,
          g = this._dur,
          v = t <= 0 ? 0 : la(t),
          y = this._zTime < 0 != t < 0 && (this._initted || !g);
        if ((this !== L && m < v && 0 <= t && (v = m), v !== this._tTime || r || y)) {
          if (
            (_ !== this._time && g && ((v += this._time - _), (t += this._time - _)),
            (i = v),
            (f = this._start),
            (u = !(l = this._ts)),
            y && (g || (_ = this._zTime), (!t && e) || (this._zTime = t)),
            this._repeat)
          ) {
            if (((c = this._yoyo), (o = g + this._rDelay), this._repeat < -1 && t < 0))
              return this.totalTime(100 * o + t, e, r);
            if (
              ((i = la(v % o)),
              v === m
                ? ((s = this._repeat), (i = g))
                : ((s = ~~(d = la(v / o))) && s === d && ((i = g), s--), g < i && (i = g)),
              (d = wt(this._tTime, o)),
              !_ && this._tTime && d !== s && this._tTime - d * o - this._dur <= 0 && (d = s),
              c && 1 & s && ((i = g - i), (p = 1)),
              s !== d && !this._lock)
            ) {
              var T = c && 1 & d,
                b = T === (c && 1 & s);
              if (
                (s < d && (T = !T),
                (_ = T ? 0 : v % g ? g : v),
                (this._lock = 1),
                (this.render(_ || (p ? 0 : la(s * o)), e, !g)._lock = 0),
                (this._tTime = v),
                !e && this.parent && Dt(this, "onRepeat"),
                this.vars.repeatRefresh && !p && ((this.invalidate()._lock = 1), (d = s)),
                (_ && _ !== this._time) ||
                  u != !this._ts ||
                  (this.vars.onRepeat && !this.parent && !this._act))
              )
                return this;
              if (
                ((g = this._dur),
                (m = this._tDur),
                b &&
                  ((this._lock = 2),
                  (_ = T ? g : -1e-4),
                  this.render(_, !0),
                  this.vars.repeatRefresh && !p && this.invalidate()),
                (this._lock = 0),
                !this._ts && !u)
              )
                return this;
              Tb(this, p);
            }
          }
          if (
            (this._hasPause &&
              !this._forcing &&
              this._lock < 2 &&
              (h = (function _findNextPauseTween(t, e, r) {
                var i;
                if (e < r)
                  for (i = t._first; i && i._start <= r; ) {
                    if ("isPause" === i.data && i._start > e) return i;
                    i = i._next;
                  }
                else
                  for (i = t._last; i && i._start >= r; ) {
                    if ("isPause" === i.data && i._start < e) return i;
                    i = i._prev;
                  }
              })(this, la(_), la(i))) &&
              (v -= i - (i = h._start)),
            (this._tTime = v),
            (this._time = i),
            (this._act = !l),
            this._initted ||
              ((this._onUpdate = this.vars.onUpdate),
              (this._initted = 1),
              (this._zTime = t),
              (_ = 0)),
            !_ && v && g && !e && !d && (Dt(this, "onStart"), this._tTime !== v))
          )
            return this;
          if (_ <= i && 0 <= t)
            for (n = this._first; n; ) {
              if (((a = n._next), (n._act || i >= n._start) && n._ts && h !== n)) {
                if (n.parent !== this) return this.render(t, e, r);
                if (
                  (n.render(
                    0 < n._ts
                      ? (i - n._start) * n._ts
                      : (n._dirty ? n.totalDuration() : n._tDur) + (i - n._start) * n._ts,
                    e,
                    r,
                  ),
                  i !== this._time || (!this._ts && !u))
                ) {
                  ((h = 0), a && (v += this._zTime = -q));
                  break;
                }
              }
              n = a;
            }
          else {
            n = this._last;
            for (var w = t < 0 ? t : i; n; ) {
              if (((a = n._prev), (n._act || w <= n._end) && n._ts && h !== n)) {
                if (n.parent !== this) return this.render(t, e, r);
                if (
                  (n.render(
                    0 < n._ts
                      ? (w - n._start) * n._ts
                      : (n._dirty ? n.totalDuration() : n._tDur) + (w - n._start) * n._ts,
                    e,
                    r || (I && pa(n)),
                  ),
                  i !== this._time || (!this._ts && !u))
                ) {
                  ((h = 0), a && (v += this._zTime = w ? -q : q));
                  break;
                }
              }
              n = a;
            }
          }
          if (
            h &&
            !e &&
            (this.pause(), (h.render(_ <= i ? 0 : -q)._zTime = _ <= i ? 1 : -1), this._ts)
          )
            return ((this._start = f), Ka(this), this.render(t, e, r));
          (this._onUpdate && !e && Dt(this, "onUpdate", !0),
            ((v === m && this._tTime >= this.totalDuration()) || (!v && _)) &&
              ((f !== this._start && Math.abs(l) === Math.abs(this._ts)) ||
                this._lock ||
                ((!t && g) || !((v === m && 0 < this._ts) || (!v && this._ts < 0)) || Ca(this, 1),
                e ||
                  (t < 0 && !_) ||
                  (!v && !_ && m) ||
                  (Dt(this, v === m && 0 <= t ? "onComplete" : "onReverseComplete", !0),
                  !this._prom || (v < m && 0 < this.timeScale()) || this._prom()))));
        }
        return this;
      }),
      (e.add = function add(e, i) {
        var n = this;
        if ((t(i) || (i = Ot(this, i, e)), !(e instanceof qt))) {
          if ($(e))
            return (
              e.forEach(function (t) {
                return n.add(t, i);
              }),
              this
            );
          if (r(e)) return this.addLabel(e, i);
          if (!s(e)) return this;
          e = te.delayedCall(0, e);
        }
        return this !== e ? Na(this, e, i) : this;
      }),
      (e.getChildren = function getChildren(t, e, r, i) {
        (void 0 === t && (t = !0),
          void 0 === e && (e = !0),
          void 0 === r && (r = !0),
          void 0 === i && (i = -X));
        for (var n = [], a = this._first; a; )
          (a._start >= i &&
            (a instanceof te
              ? e && n.push(a)
              : (r && n.push(a), t && n.push.apply(n, a.getChildren(!0, e, r)))),
            (a = a._next));
        return n;
      }),
      (e.getById = function getById(t) {
        for (var e = this.getChildren(1, 1, 1), r = e.length; r--; )
          if (e[r].vars.id === t) return e[r];
      }),
      (e.remove = function remove(t) {
        return r(t)
          ? this.removeLabel(t)
          : s(t)
            ? this.killTweensOf(t)
            : (t.parent === this && Ba(this, t),
              t === this._recent && (this._recent = this._last),
              Da(this));
      }),
      (e.totalTime = function totalTime(t, e) {
        return arguments.length
          ? ((this._forcing = 1),
            !this._dp &&
              this._ts &&
              (this._start = la(
                It.time - (0 < this._ts ? t / this._ts : (this.totalDuration() - t) / -this._ts),
              )),
            i.prototype.totalTime.call(this, t, e),
            (this._forcing = 0),
            this)
          : this._tTime;
      }),
      (e.addLabel = function addLabel(t, e) {
        return ((this.labels[t] = Ot(this, e)), this);
      }),
      (e.removeLabel = function removeLabel(t) {
        return (delete this.labels[t], this);
      }),
      (e.addPause = function addPause(t, e, r) {
        var i = te.delayedCall(0, e || V, r);
        return ((i.data = "isPause"), (this._hasPause = 1), Na(this, i, Ot(this, t)));
      }),
      (e.removePause = function removePause(t) {
        var e = this._first;
        for (t = Ot(this, t); e; ) (e._start === t && "isPause" === e.data && Ca(e), (e = e._next));
      }),
      (e.killTweensOf = function killTweensOf(t, e, r) {
        for (var i = this.getTweensOf(t, r), n = i.length; n--; ) Wt !== i[n] && i[n].kill(t, e);
        return this;
      }),
      (e.getTweensOf = function getTweensOf(e, r) {
        for (var i, n = [], a = Pt(e), s = this._first, o = t(r); s; )
          (s instanceof te
            ? na(s._targets, a) &&
              (o
                ? (!Wt || (s._initted && s._ts)) &&
                  s.globalTime(0) <= r &&
                  s.globalTime(s.totalDuration()) > r
                : !r || s.isActive()) &&
              n.push(s)
            : (i = s.getTweensOf(a, r)).length && n.push.apply(n, i),
            (s = s._next));
        return n;
      }),
      (e.tweenTo = function tweenTo(t, e) {
        e = e || {};
        var r,
          i = this,
          n = Ot(i, t),
          a = e.startAt,
          s = e.onStart,
          o = e.onStartParams,
          u = e.immediateRender,
          h = te.to(
            i,
            ta(
              {
                ease: e.ease || "none",
                lazy: !1,
                immediateRender: !1,
                time: n,
                overwrite: "auto",
                duration:
                  e.duration ||
                  Math.abs((n - (a && "time" in a ? a.time : i._time)) / i.timeScale()) ||
                  q,
                onStart: function onStart() {
                  if ((i.pause(), !r)) {
                    var t =
                      e.duration ||
                      Math.abs((n - (a && "time" in a ? a.time : i._time)) / i.timeScale());
                    (h._dur !== t && Ua(h, t, 0, 1).render(h._time, !0, !0), (r = 1));
                  }
                  s && s.apply(h, o || []);
                },
              },
              e,
            ),
          );
        return u ? h.render(0) : h;
      }),
      (e.tweenFromTo = function tweenFromTo(t, e, r) {
        return this.tweenTo(e, ta({ startAt: { time: Ot(this, t) } }, r));
      }),
      (e.recent = function recent() {
        return this._recent;
      }),
      (e.nextLabel = function nextLabel(t) {
        return (void 0 === t && (t = this._time), ub(this, Ot(this, t)));
      }),
      (e.previousLabel = function previousLabel(t) {
        return (void 0 === t && (t = this._time), ub(this, Ot(this, t), 1));
      }),
      (e.currentLabel = function currentLabel(t) {
        return arguments.length ? this.seek(t, !0) : this.previousLabel(this._time + q);
      }),
      (e.shiftChildren = function shiftChildren(t, e, r) {
        void 0 === r && (r = 0);
        var i,
          n = this._first,
          a = this.labels;
        for (t = la(t); n; ) (n._start >= r && ((n._start += t), (n._end += t)), (n = n._next));
        if (e) for (i in a) a[i] >= r && (a[i] += t);
        return Da(this);
      }),
      (e.invalidate = function invalidate(t) {
        var e = this._first;
        for (this._lock = 0; e; ) (e.invalidate(t), (e = e._next));
        return i.prototype.invalidate.call(this, t);
      }),
      (e.clear = function clear(t) {
        void 0 === t && (t = !0);
        for (var e, r = this._first; r; ) ((e = r._next), this.remove(r), (r = e));
        return (
          this._dp && (this._time = this._tTime = this._pTime = 0),
          t && (this.labels = {}),
          Da(this)
        );
      }),
      (e.totalDuration = function totalDuration(t) {
        var e,
          r,
          i,
          n = 0,
          a = this,
          s = a._last,
          o = X;
        if (arguments.length)
          return a.timeScale(
            (a._repeat < 0 ? a.duration() : a.totalDuration()) / (a.reversed() ? -t : t),
          );
        if (a._dirty) {
          for (i = a.parent; s; )
            ((e = s._prev),
              s._dirty && s.totalDuration(),
              o < (r = s._start) && a._sort && s._ts && !a._lock
                ? ((a._lock = 1), (Na(a, s, r - s._delay, 1)._lock = 0))
                : (o = r),
              r < 0 &&
                s._ts &&
                ((n -= r),
                ((!i && !a._dp) || (i && i.smoothChildTiming)) &&
                  ((a._start += la(r / a._ts)), (a._time -= r), (a._tTime -= r)),
                a.shiftChildren(-r, !1, -Infinity),
                (o = 0)),
              s._end > n && s._ts && (n = s._end),
              (s = e));
          (Ua(a, a === L && a._time > n ? a._time : n, 1, 1), (a._dirty = 0));
        }
        return a._tDur;
      }),
      (Timeline.updateRoot = function updateRoot(t) {
        if ((L._ts && (qa(L, Ja(t, L)), (f = It.frame)), It.frame >= vt)) {
          vt += N.autoSleep || 120;
          var e = L._first;
          if ((!e || !e._ts) && N.autoSleep && It._listeners.length < 2) {
            for (; e && !e._ts; ) e = e._next;
            e || It.sleep();
          }
        }
      }),
      Timeline
    );
  })(qt);
  ta(Zt.prototype, { _lock: 0, _hasPause: 0, _forcing: 0 });
  function dc(t, e, i, n, a, o) {
    var u, h, l, f;
    if (
      mt[t] &&
      !1 !==
        (u = new mt[t]()).init(
          a,
          u.rawVars
            ? e[t]
            : (function _processVars(t, e, i, n, a) {
                if (
                  (s(t) && (t = Gt(t, a, e, i, n)),
                  !v(t) || (t.style && t.nodeType) || $(t) || K(t))
                )
                  return r(t) ? Gt(t, a, e, i, n) : t;
                var o,
                  u = {};
                for (o in t) u[o] = Gt(t[o], a, e, i, n);
                return u;
              })(e[t], n, a, o, i),
          i,
          n,
          o,
        ) &&
      ((i._pt = h = new we(i._pt, a, t, 0, 1, u.render, u, 0, u.priority)), i !== d)
    )
      for (l = i._ptLookup[i._targets.indexOf(a)], f = u._props.length; f--; ) l[u._props[f]] = h;
    return u;
  }
  function jc(t, r, e, i) {
    var n,
      a,
      s = r.ease || i || "power1.inOut";
    if ($(r))
      ((a = e[t] || (e[t] = [])),
        r.forEach(function (t, e) {
          return a.push({ t: (e / (r.length - 1)) * 100, v: t, e: s });
        }));
    else
      for (n in r)
        ((a = e[n] || (e[n] = [])), "ease" === n || a.push({ t: parseFloat(t), v: r[n], e: s }));
  }
  var Wt,
    Ht,
    Jt = function _addPropTween(t, e, i, n, a, o, u, h, l, f) {
      s(n) && (n = n(a || 0, t, o));
      var d,
        c = t[e],
        p =
          "get" !== i
            ? i
            : s(c)
              ? l
                ? t[e.indexOf("set") || !s(t["get" + e.substr(3)]) ? e : "get" + e.substr(3)](l)
                : t[e]()
              : c,
        _ = s(c) ? (l ? ue : re) : ee;
      if (
        (r(n) &&
          (~n.indexOf("random(") && (n = rb(n)),
          "=" === n.charAt(1) && ((!(d = ma(p, n) + (_a(p) || 0)) && 0 !== d) || (n = d))),
        !f || p !== n || Ht)
      )
        return isNaN(p * n) || "" === n
          ? (c || e in t || S(e, n),
            function _addComplexStringPropTween(t, e, r, i, n, a, s) {
              var o,
                u,
                h,
                l,
                f,
                d,
                c,
                p,
                _ = new we(this._pt, t, e, 0, 1, ge, null, n),
                m = 0,
                g = 0;
              for (
                _.b = r,
                  _.e = i,
                  r += "",
                  (c = ~(i += "").indexOf("random(")) && (i = rb(i)),
                  a && (a((p = [r, i]), t, e), (r = p[0]), (i = p[1])),
                  u = r.match(at) || [];
                (o = at.exec(i));
              )
                ((l = o[0]),
                  (f = i.substring(m, o.index)),
                  h ? (h = (h + 1) % 5) : "rgba(" === f.substr(-5) && (h = 1),
                  l !== u[g++] &&
                    ((d = parseFloat(u[g - 1]) || 0),
                    (_._pt = {
                      _next: _._pt,
                      p: f || 1 === g ? f : ",",
                      s: d,
                      c: "=" === l.charAt(1) ? ma(d, l) - d : parseFloat(l) - d,
                      m: h && h < 4 ? Math.round : 0,
                    }),
                    (m = at.lastIndex)));
              return (
                (_.c = m < i.length ? i.substring(m, i.length) : ""),
                (_.fp = s),
                (st.test(i) || c) && (_.e = 0),
                (this._pt = _)
              );
            }.call(this, t, e, p, n, _, h || N.stringFilter, l))
          : ((d = new we(
              this._pt,
              t,
              e,
              +p || 0,
              n - (p || 0),
              "boolean" == typeof c ? _e : ce,
              0,
              _,
            )),
            l && (d.fp = l),
            u && d.modifier(u, this, t),
            (this._pt = d));
    },
    Qt = function _initTween(t, e, r) {
      var i,
        n,
        a,
        s,
        o,
        u,
        h,
        l,
        f,
        d,
        c,
        p,
        _,
        m = t.vars,
        g = m.ease,
        v = m.startAt,
        y = m.immediateRender,
        T = m.lazy,
        b = m.onUpdate,
        x = m.runBackwards,
        k = m.yoyoEase,
        O = m.keyframes,
        M = m.autoRevert,
        C = t._dur,
        P = t._startAt,
        A = t._targets,
        D = t.parent,
        S = D && "nested" === D.data ? D.vars.targets : A,
        z = "auto" === t._overwrite && !F,
        E = t.timeline;
      if (
        (!E || (O && g) || (g = "none"),
        (t._ease = Vt(g, j.ease)),
        (t._yEase = k ? jt(Vt(!0 === k ? g : k, j.ease)) : 0),
        k && t._yoyo && !t._repeat && ((k = t._yEase), (t._yEase = t._ease), (t._ease = k)),
        (t._from = !E && !!m.runBackwards),
        !E || (O && !m.stagger))
      ) {
        if (
          ((p = (l = A[0] ? ha(A[0]).harness : 0) && m[l.prop]),
          (i = xa(m, ct)),
          P &&
            (P._zTime < 0 && P.progress(1),
            e < 0 && x && y && !M ? P.render(-1, !0) : P.revert(x && C ? ft : lt),
            (P._lazy = 0)),
          v)
        ) {
          if (
            (Ca(
              (t._startAt = te.set(
                A,
                ta(
                  {
                    data: "isStart",
                    overwrite: !1,
                    parent: D,
                    immediateRender: !0,
                    lazy: !P && w(T),
                    startAt: null,
                    delay: 0,
                    onUpdate:
                      b &&
                      function () {
                        return Dt(t, "onUpdate");
                      },
                    stagger: 0,
                  },
                  v,
                ),
              )),
            ),
            (t._startAt._dp = 0),
            (t._startAt._sat = t),
            e < 0 && (I || (!y && !M)) && t._startAt.revert(ft),
            y && C && e <= 0 && r <= 0)
          )
            return void (e && (t._zTime = e));
        } else if (x && C && !P)
          if (
            (e && (y = !1),
            (a = ta(
              {
                overwrite: !1,
                data: "isFromStart",
                lazy: y && !P && w(T),
                immediateRender: y,
                stagger: 0,
                parent: D,
              },
              i,
            )),
            p && (a[l.prop] = p),
            Ca((t._startAt = te.set(A, a))),
            (t._startAt._dp = 0),
            (t._startAt._sat = t),
            e < 0 && (I ? t._startAt.revert(ft) : t._startAt.render(-1, !0)),
            (t._zTime = e),
            y)
          ) {
            if (!e) return;
          } else _initTween(t._startAt, q, q);
        for (t._pt = t._ptCache = 0, T = (C && w(T)) || (T && !C), n = 0; n < A.length; n++) {
          if (
            ((h = (o = A[n])._gsap || ga(A)[n]._gsap),
            (t._ptLookup[n] = d = {}),
            _t[h.id] && pt.length && oa(),
            (c = S === A ? n : S.indexOf(o)),
            l &&
              !1 !== (f = new l()).init(o, p || i, t, c, S) &&
              ((t._pt = s = new we(t._pt, o, f.name, 0, 1, f.render, f, 0, f.priority)),
              f._props.forEach(function (t) {
                d[t] = s;
              }),
              f.priority && (u = 1)),
            !l || p)
          )
            for (a in i)
              mt[a] && (f = dc(a, i, t, c, o, S))
                ? f.priority && (u = 1)
                : (d[a] = s = Jt.call(t, o, a, "get", i[a], c, S, 0, m.stringFilter));
          (t._op && t._op[n] && t.kill(o, t._op[n]),
            z &&
              t._pt &&
              ((Wt = t), L.killTweensOf(o, d, t.globalTime(e)), (_ = !t.parent), (Wt = 0)),
            t._pt && T && (_t[h.id] = 1));
        }
        (u && be(t), t._onInit && t._onInit(t));
      }
      ((t._onUpdate = b),
        (t._initted = (!t._op || t._pt) && !_),
        O && e <= 0 && E.render(X, !0, !0));
    },
    Gt = function _parseFuncOrString(t, e, i, n, a) {
      return s(t) ? t.call(e, i, n, a) : r(t) && ~t.indexOf("random(") ? rb(t) : t;
    },
    Kt = Tt + "repeat,repeatDelay,yoyo,repeatRefresh,yoyoEase,autoRevert",
    $t = {};
  ja(Kt + ",id,stagger,delay,duration,paused,scrollTrigger", function (t) {
    return ($t[t] = 1);
  });
  var te = (function (R) {
    function Tween(e, r, i, n) {
      var a;
      "number" == typeof r && ((i.duration = r), (r = i), (i = null));
      var s,
        o,
        u,
        h,
        l,
        f,
        d,
        c,
        p = (a = R.call(this, n ? r : ya(r)) || this).vars,
        _ = p.duration,
        m = p.delay,
        g = p.immediateRender,
        b = p.stagger,
        x = p.overwrite,
        k = p.keyframes,
        O = p.defaults,
        M = p.scrollTrigger,
        C = p.yoyoEase,
        P = r.parent || L,
        A = ($(e) || K(e) ? t(e[0]) : "length" in r) ? [e] : Pt(e);
      if (
        ((a._targets = A.length
          ? ga(A)
          : T("GSAP target " + e + " not found. https://gsap.com", !N.nullTargetWarn) || []),
        (a._ptLookup = []),
        (a._overwrite = x),
        k || b || y(_) || y(m))
      ) {
        if (
          ((r = a.vars),
          (s = a.timeline =
            new Zt({
              data: "nested",
              defaults: O || {},
              targets: P && "nested" === P.data ? P.vars.targets : A,
            })).kill(),
          (s.parent = s._dp = _assertThisInitialized(a)),
          (s._start = 0),
          b || y(_) || y(m))
        ) {
          if (((h = A.length), (d = b && hb(b)), v(b)))
            for (l in b) ~Kt.indexOf(l) && ((c = c || {})[l] = b[l]);
          for (o = 0; o < h; o++)
            (((u = xa(r, $t)).stagger = 0),
              C && (u.yoyoEase = C),
              c && bt(u, c),
              (f = A[o]),
              (u.duration = +Gt(_, _assertThisInitialized(a), o, f, A)),
              (u.delay = (+Gt(m, _assertThisInitialized(a), o, f, A) || 0) - a._delay),
              !b &&
                1 === h &&
                u.delay &&
                ((a._delay = m = u.delay), (a._start += m), (u.delay = 0)),
              s.to(f, u, d ? d(o, f, A) : 0),
              (s._ease = Bt.none));
          s.duration() ? (_ = m = 0) : (a.timeline = 0);
        } else if (k) {
          (ya(ta(s.vars.defaults, { ease: "none" })), (s._ease = Vt(k.ease || r.ease || "none")));
          var D,
            S,
            z,
            E = 0;
          if ($(k))
            (k.forEach(function (t) {
              return s.to(A, t, ">");
            }),
              s.duration());
          else {
            for (l in ((u = {}), k)) "ease" === l || "easeEach" === l || jc(l, k[l], u, k.easeEach);
            for (l in u)
              for (
                D = u[l].sort(function (t, e) {
                  return t.t - e.t;
                }),
                  o = E = 0;
                o < D.length;
                o++
              )
                (((z = { ease: (S = D[o]).e, duration: ((S.t - (o ? D[o - 1].t : 0)) / 100) * _ })[
                  l
                ] = S.v),
                  s.to(A, z, E),
                  (E += z.duration));
            s.duration() < _ && s.to({}, { duration: _ - s.duration() });
          }
        }
        _ || a.duration((_ = s.duration()));
      } else a.timeline = 0;
      return (
        !0 !== x || F || ((Wt = _assertThisInitialized(a)), L.killTweensOf(A), (Wt = 0)),
        Na(P, _assertThisInitialized(a), i),
        r.reversed && a.reverse(),
        r.paused && a.paused(!0),
        (g ||
          (!_ &&
            !k &&
            a._start === la(P._time) &&
            w(g) &&
            (function _hasNoPausedAncestors(t) {
              return !t || (t._ts && _hasNoPausedAncestors(t.parent));
            })(_assertThisInitialized(a)) &&
            "nested" !== P.data)) &&
          ((a._tTime = -q), a.render(Math.max(0, -m) || 0)),
        M && Oa(_assertThisInitialized(a), M),
        a
      );
    }
    _inheritsLoose(Tween, R);
    var e = Tween.prototype;
    return (
      (e.render = function render(t, e, r) {
        var i,
          n,
          a,
          s,
          o,
          u,
          h,
          l,
          f,
          d = this._time,
          c = this._tDur,
          p = this._dur,
          _ = t < 0,
          m = c - q < t && !_ ? c : t < q ? 0 : t;
        if (p) {
          if (
            m !== this._tTime ||
            !t ||
            r ||
            (!this._initted && this._tTime) ||
            (this._startAt && this._zTime < 0 != _) ||
            this._lazy
          ) {
            if (((i = m), (l = this.timeline), this._repeat)) {
              if (((s = p + this._rDelay), this._repeat < -1 && _))
                return this.totalTime(100 * s + t, e, r);
              if (
                ((i = la(m % s)),
                m === c
                  ? ((a = this._repeat), (i = p))
                  : (a = ~~(o = la(m / s))) && a === o
                    ? ((i = p), a--)
                    : p < i && (i = p),
                (u = this._yoyo && 1 & a) && ((f = this._yEase), (i = p - i)),
                (o = wt(this._tTime, s)),
                i === d && !r && this._initted && a === o)
              )
                return ((this._tTime = m), this);
              a !== o &&
                (l && this._yEase && Tb(l, u),
                this.vars.repeatRefresh &&
                  !u &&
                  !this._lock &&
                  i !== s &&
                  this._initted &&
                  ((this._lock = r = 1), (this.render(la(s * a), !0).invalidate()._lock = 0)));
            }
            if (!this._initted) {
              if (Pa(this, _ ? t : i, r, e, m)) return ((this._tTime = 0), this);
              if (!(d === this._time || (r && this.vars.repeatRefresh && a !== o))) return this;
              if (p !== this._dur) return this.render(t, e, r);
            }
            if (
              ((this._tTime = m),
              (this._time = i),
              !this._act && this._ts && ((this._act = 1), (this._lazy = 0)),
              (this.ratio = h = (f || this._ease)(i / p)),
              this._from && (this.ratio = h = 1 - h),
              !d && m && !e && !o && (Dt(this, "onStart"), this._tTime !== m))
            )
              return this;
            for (n = this._pt; n; ) (n.r(h, n.d), (n = n._next));
            ((l && l.render(t < 0 ? t : l._dur * l._ease(i / this._dur), e, r)) ||
              (this._startAt && (this._zTime = t)),
              this._onUpdate && !e && (_ && Fa(this, t, 0, r), Dt(this, "onUpdate")),
              this._repeat &&
                a !== o &&
                this.vars.onRepeat &&
                !e &&
                this.parent &&
                Dt(this, "onRepeat"),
              (m !== this._tDur && m) ||
                this._tTime !== m ||
                (_ && !this._onUpdate && Fa(this, t, 0, !0),
                (!t && p) ||
                  !((m === this._tDur && 0 < this._ts) || (!m && this._ts < 0)) ||
                  Ca(this, 1),
                e ||
                  (_ && !d) ||
                  !(m || d || u) ||
                  (Dt(this, m === c ? "onComplete" : "onReverseComplete", !0),
                  !this._prom || (m < c && 0 < this.timeScale()) || this._prom())));
          }
        } else
          !(function _renderZeroDurationTween(t, e, r, i) {
            var n,
              a,
              s,
              o = t.ratio,
              u =
                e < 0 ||
                (!e &&
                  ((!t._start &&
                    (function _parentPlayheadIsBeforeStart(t) {
                      var e = t.parent;
                      return (
                        e &&
                        e._ts &&
                        e._initted &&
                        !e._lock &&
                        (e.rawTime() < 0 || _parentPlayheadIsBeforeStart(e))
                      );
                    })(t) &&
                    (t._initted || !xt(t))) ||
                    ((t._ts < 0 || t._dp._ts < 0) && !xt(t))))
                  ? 0
                  : 1,
              h = t._rDelay,
              l = 0;
            if (
              (h &&
                t._repeat &&
                ((l = Mt(0, t._tDur, e)),
                (a = wt(l, h)),
                t._yoyo && 1 & a && (u = 1 - u),
                a !== wt(t._tTime, h) &&
                  ((o = 1 - u), t.vars.repeatRefresh && t._initted && t.invalidate())),
              u !== o || I || i || t._zTime === q || (!e && t._zTime))
            ) {
              if (!t._initted && Pa(t, e, i, r, l)) return;
              for (
                s = t._zTime,
                  t._zTime = e || (r ? q : 0),
                  r = r || (e && !s),
                  t.ratio = u,
                  t._from && (u = 1 - u),
                  t._time = 0,
                  t._tTime = l,
                  n = t._pt;
                n;
              )
                (n.r(u, n.d), (n = n._next));
              (e < 0 && Fa(t, e, 0, !0),
                t._onUpdate && !r && Dt(t, "onUpdate"),
                l && t._repeat && !r && t.parent && Dt(t, "onRepeat"),
                (e >= t._tDur || e < 0) &&
                  t.ratio === u &&
                  (u && Ca(t, 1),
                  r ||
                    I ||
                    (Dt(t, u ? "onComplete" : "onReverseComplete", !0), t._prom && t._prom())));
            } else t._zTime || (t._zTime = e);
          })(this, t, e, r);
        return this;
      }),
      (e.targets = function targets() {
        return this._targets;
      }),
      (e.invalidate = function invalidate(t) {
        return (
          (t && this.vars.runBackwards) || (this._startAt = 0),
          (this._pt = this._op = this._onUpdate = this._lazy = this.ratio = 0),
          (this._ptLookup = []),
          this.timeline && this.timeline.invalidate(t),
          R.prototype.invalidate.call(this, t)
        );
      }),
      (e.resetTo = function resetTo(t, e, r, i, n) {
        (c || It.wake(), this._ts || this.play());
        var a,
          s = Math.min(this._dur, (this._dp._time - this._start) * this._ts);
        return (
          this._initted || Qt(this, s),
          (a = this._ease(s / this._dur)),
          (function _updatePropTweens(t, e, r, i, n, a, s, o) {
            var u,
              h,
              l,
              f,
              d = ((t._pt && t._ptCache) || (t._ptCache = {}))[e];
            if (!d)
              for (d = t._ptCache[e] = [], l = t._ptLookup, f = t._targets.length; f--; ) {
                if ((u = l[f][e]) && u.d && u.d._pt)
                  for (u = u.d._pt; u && u.p !== e && u.fp !== e; ) u = u._next;
                if (!u)
                  return (
                    (Ht = 1),
                    (t.vars[e] = "+=0"),
                    Qt(t, s),
                    (Ht = 0),
                    o ? T(e + " not eligible for reset") : 1
                  );
                d.push(u);
              }
            for (f = d.length; f--; )
              (((u = (h = d[f])._pt || h).s = (!i && 0 !== i) || n ? u.s + (i || 0) + a * u.c : i),
                (u.c = r - u.s),
                h.e && (h.e = ka(r) + _a(h.e)),
                h.b && (h.b = u.s + _a(h.b)));
          })(this, t, e, r, i, a, s, n)
            ? this.resetTo(t, e, r, i, 1)
            : (La(this, 0),
              this.parent || Aa(this._dp, this, "_first", "_last", this._dp._sort ? "_start" : 0),
              this.render(0))
        );
      }),
      (e.kill = function kill(t, e) {
        if ((void 0 === e && (e = "all"), !(t || (e && "all" !== e))))
          return (
            (this._lazy = this._pt = 0),
            this.parent ? wb(this) : this.scrollTrigger && this.scrollTrigger.kill(!!I),
            this
          );
        if (this.timeline) {
          var i = this.timeline.totalDuration();
          return (
            this.timeline.killTweensOf(t, e, Wt && !0 !== Wt.vars.overwrite)._first || wb(this),
            this.parent &&
              i !== this.timeline.totalDuration() &&
              Ua(this, (this._dur * this.timeline._tDur) / i, 0, 1),
            this
          );
        }
        var n,
          a,
          s,
          o,
          u,
          h,
          l,
          f = this._targets,
          d = t ? Pt(t) : f,
          c = this._ptLookup,
          p = this._pt;
        if (
          (!e || "all" === e) &&
          (function _arraysMatch(t, e) {
            for (var r = t.length, i = r === e.length; i && r-- && t[r] === e[r]; );
            return r < 0;
          })(f, d)
        )
          return ("all" === e && (this._pt = 0), wb(this));
        for (
          n = this._op = this._op || [],
            "all" !== e &&
              (r(e) &&
                ((u = {}),
                ja(e, function (t) {
                  return (u[t] = 1);
                }),
                (e = u)),
              (e = (function _addAliasesToVars(t, e) {
                var r,
                  i,
                  n,
                  a,
                  s = t[0] ? ha(t[0]).harness : 0,
                  o = s && s.aliases;
                if (!o) return e;
                for (i in ((r = bt({}, e)), o))
                  if (i in r) for (n = (a = o[i].split(",")).length; n--; ) r[a[n]] = r[i];
                return r;
              })(f, e))),
            l = f.length;
          l--;
        )
          if (~d.indexOf(f[l]))
            for (u in ((a = c[l]),
            "all" === e ? ((n[l] = e), (o = a), (s = {})) : ((s = n[l] = n[l] || {}), (o = e)),
            o))
              ((h = a && a[u]) &&
                (("kill" in h.d && !0 !== h.d.kill(u)) || Ba(this, h, "_pt"), delete a[u]),
                "all" !== s && (s[u] = 1));
        return (this._initted && !this._pt && p && wb(this), this);
      }),
      (Tween.to = function to(t, e, r) {
        return new Tween(t, e, r);
      }),
      (Tween.from = function from(t, e) {
        return Ya(1, arguments);
      }),
      (Tween.delayedCall = function delayedCall(t, e, r, i) {
        return new Tween(e, 0, {
          immediateRender: !1,
          lazy: !1,
          overwrite: !1,
          delay: t,
          onComplete: e,
          onReverseComplete: e,
          onCompleteParams: r,
          onReverseCompleteParams: r,
          callbackScope: i,
        });
      }),
      (Tween.fromTo = function fromTo(t, e, r) {
        return Ya(2, arguments);
      }),
      (Tween.set = function set(t, e) {
        return ((e.duration = 0), e.repeatDelay || (e.repeat = 0), new Tween(t, e));
      }),
      (Tween.killTweensOf = function killTweensOf(t, e, r) {
        return L.killTweensOf(t, e, r);
      }),
      Tween
    );
  })(qt);
  (ta(te.prototype, { _targets: [], _lazy: 0, _startAt: 0, _op: 0, _onInit: 0 }),
    ja("staggerTo,staggerFrom,staggerFromTo", function (r) {
      te[r] = function () {
        var t = new Zt(),
          e = Ct.call(arguments, 0);
        return (e.splice("staggerFromTo" === r ? 5 : 4, 0, 0), t[r].apply(t, e));
      };
    }));
  function rc(t, e, r) {
    return t.setAttribute(e, r);
  }
  function zc(t, e, r, i) {
    i.mSet(t, e, i.m.call(i.tween, r, i.mt), i);
  }
  var ee = function _setterPlain(t, e, r) {
      return (t[e] = r);
    },
    re = function _setterFunc(t, e, r) {
      return t[e](r);
    },
    ue = function _setterFuncWithParam(t, e, r, i) {
      return t[e](i.fp, r);
    },
    le = function _getSetter(t, e) {
      return s(t[e]) ? re : u(t[e]) && t.setAttribute ? rc : ee;
    },
    ce = function _renderPlain(t, e) {
      return e.set(e.t, e.p, Math.round(1e6 * (e.s + e.c * t)) / 1e6, e);
    },
    _e = function _renderBoolean(t, e) {
      return e.set(e.t, e.p, !!(e.s + e.c * t), e);
    },
    ge = function _renderComplexString(t, e) {
      var r = e._pt,
        i = "";
      if (!t && e.b) i = e.b;
      else if (1 === t && e.e) i = e.e;
      else {
        for (; r; )
          ((i = r.p + (r.m ? r.m(r.s + r.c * t) : Math.round(1e4 * (r.s + r.c * t)) / 1e4) + i),
            (r = r._next));
        i += e.c;
      }
      e.set(e.t, e.p, i, e);
    },
    ve = function _renderPropTweens(t, e) {
      for (var r = e._pt; r; ) (r.r(t, r.d), (r = r._next));
    },
    ye = function _addPluginModifier(t, e, r, i) {
      for (var n, a = this._pt; a; ) ((n = a._next), a.p === i && a.modifier(t, e, r), (a = n));
    },
    Te = function _killPropTweensOf(t) {
      for (var e, r, i = this._pt; i; )
        ((r = i._next),
          (i.p === t && !i.op) || i.op === t ? Ba(this, i, "_pt") : i.dep || (e = 1),
          (i = r));
      return !e;
    },
    be = function _sortPropTweensByPriority(t) {
      for (var e, r, i, n, a = t._pt; a; ) {
        for (e = a._next, r = i; r && r.pr > a.pr; ) r = r._next;
        ((a._prev = r ? r._prev : n) ? (a._prev._next = a) : (i = a),
          (a._next = r) ? (r._prev = a) : (n = a),
          (a = e));
      }
      t._pt = i;
    },
    we =
      ((PropTween.prototype.modifier = function modifier(t, e, r) {
        ((this.mSet = this.mSet || this.set),
          (this.set = zc),
          (this.m = t),
          (this.mt = r),
          (this.tween = e));
      }),
      PropTween);
  function PropTween(t, e, r, i, n, a, s, o, u) {
    ((this.t = e),
      (this.s = i),
      (this.c = n),
      (this.p = r),
      (this.r = a || ce),
      (this.d = s || this),
      (this.set = o || ee),
      (this.pr = u || 0),
      (this._next = t) && (t._prev = this));
  }
  (ja(
    Tt +
      "parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",
    function (t) {
      return (ct[t] = 1);
    },
  ),
    (ht.TweenMax = ht.TweenLite = te),
    (ht.TimelineLite = ht.TimelineMax = Zt),
    (L = new Zt({
      sortChildren: !1,
      defaults: j,
      autoRemoveChildren: !0,
      id: "root",
      smoothChildTiming: !0,
    })),
    (N.stringFilter = Ib));
  function Hc(t) {
    return (Oe[t] || Me).map(function (t) {
      return t();
    });
  }
  function Ic() {
    var t = Date.now(),
      o = [];
    2 < t - Ce &&
      (Hc("matchMediaInit"),
      ke.forEach(function (t) {
        var e,
          r,
          i,
          n,
          a = t.queries,
          s = t.conditions;
        for (r in a)
          ((e = h.matchMedia(a[r]).matches) && (i = 1), e !== s[r] && ((s[r] = e), (n = 1)));
        n && (t.revert(), i && o.push(t));
      }),
      Hc("matchMediaRevert"),
      o.forEach(function (e) {
        return e.onMatch(e, function (t) {
          return e.add(null, t);
        });
      }),
      (Ce = t),
      Hc("matchMedia"));
  }
  var xe,
    ke = [],
    Oe = {},
    Me = [],
    Ce = 0,
    Pe = 0,
    De =
      (((xe = Context.prototype).add = function add(t, i, n) {
        function Jw() {
          var t,
            e = l,
            r = a.selector;
          return (
            e && e !== a && e.data.push(a),
            n && (a.selector = fb(n)),
            (l = a),
            (t = i.apply(a, arguments)),
            s(t) && a._r.push(t),
            (l = e),
            (a.selector = r),
            (a.isReverted = !1),
            t
          );
        }
        s(t) && ((n = i), (i = t), (t = s));
        var a = this;
        return (
          (a.last = Jw),
          t === s
            ? Jw(a, function (t) {
                return a.add(null, t);
              })
            : t
              ? (a[t] = Jw)
              : Jw
        );
      }),
      (xe.ignore = function ignore(t) {
        var e = l;
        ((l = null), t(this), (l = e));
      }),
      (xe.getTweens = function getTweens() {
        var e = [];
        return (
          this.data.forEach(function (t) {
            return t instanceof Context
              ? e.push.apply(e, t.getTweens())
              : t instanceof te && !(t.parent && "nested" === t.parent.data) && e.push(t);
          }),
          e
        );
      }),
      (xe.clear = function clear() {
        this._r.length = this.data.length = 0;
      }),
      (xe.kill = function kill(i, t) {
        var n = this;
        if (
          (i
            ? (function () {
                for (var t, e = n.getTweens(), r = n.data.length; r--; )
                  "isFlip" === (t = n.data[r]).data &&
                    (t.revert(),
                    t.getChildren(!0, !0, !1).forEach(function (t) {
                      return e.splice(e.indexOf(t), 1);
                    }));
                for (
                  e
                    .map(function (t) {
                      return {
                        g:
                          t._dur || t._delay || (t._sat && !t._sat.vars.immediateRender)
                            ? t.globalTime(0)
                            : -1 / 0,
                        t: t,
                      };
                    })
                    .sort(function (t, e) {
                      return e.g - t.g || -1 / 0;
                    })
                    .forEach(function (t) {
                      return t.t.revert(i);
                    }),
                    r = n.data.length;
                  r--;
                )
                  (t = n.data[r]) instanceof Zt
                    ? "nested" !== t.data && (t.scrollTrigger && t.scrollTrigger.revert(), t.kill())
                    : t instanceof te || !t.revert || t.revert(i);
                (n._r.forEach(function (t) {
                  return t(i, n);
                }),
                  (n.isReverted = !0));
              })()
            : this.data.forEach(function (t) {
                return t.kill && t.kill();
              }),
          this.clear(),
          t)
        )
          for (var e = ke.length; e--; ) ke[e].id === this.id && ke.splice(e, 1);
      }),
      (xe.revert = function revert(t) {
        this.kill(t || {});
      }),
      Context);
  function Context(t, e) {
    ((this.selector = e && fb(e)),
      (this.data = []),
      (this._r = []),
      (this.isReverted = !1),
      (this.id = Pe++),
      t && this.add(t));
  }
  var Se,
    Ee =
      (((Se = MatchMedia.prototype).add = function add(t, e, r) {
        v(t) || (t = { matches: t });
        var i,
          n,
          a,
          s = new De(0, r || this.scope),
          o = (s.conditions = {});
        for (n in (l && !s.selector && (s.selector = l.selector),
        this.contexts.push(s),
        (e = s.add("onMatch", e)),
        (s.queries = t)))
          "all" === n
            ? (a = 1)
            : (i = h.matchMedia(t[n])) &&
              (ke.indexOf(s) < 0 && ke.push(s),
              (o[n] = i.matches) && (a = 1),
              i.addListener ? i.addListener(Ic) : i.addEventListener("change", Ic));
        return (
          a &&
            e(s, function (t) {
              return s.add(null, t);
            }),
          this
        );
      }),
      (Se.revert = function revert(t) {
        this.kill(t || {});
      }),
      (Se.kill = function kill(e) {
        this.contexts.forEach(function (t) {
          return t.kill(e, !0);
        });
      }),
      MatchMedia);
  function MatchMedia(t) {
    ((this.contexts = []), (this.scope = t), l && l.data.push(this));
  }
  var Re = {
    registerPlugin: function registerPlugin() {
      for (var t = arguments.length, e = new Array(t), r = 0; r < t; r++) e[r] = arguments[r];
      e.forEach(function (t) {
        return zb(t);
      });
    },
    timeline: function timeline(t) {
      return new Zt(t);
    },
    getTweensOf: function getTweensOf(t, e) {
      return L.getTweensOf(t, e);
    },
    getProperty: function getProperty(i, t, e, n) {
      r(i) && (i = Pt(i)[0]);
      var a = ha(i || {}).get,
        s = e ? sa : ra;
      return (
        "native" === e && (e = ""),
        i
          ? t
            ? s(((mt[t] && mt[t].get) || a)(i, t, e, n))
            : function (t, e, r) {
                return s(((mt[t] && mt[t].get) || a)(i, t, e, r));
              }
          : i
      );
    },
    quickSetter: function quickSetter(r, e, i) {
      if (1 < (r = Pt(r)).length) {
        var n = r.map(function (t) {
            return Fe.quickSetter(t, e, i);
          }),
          a = n.length;
        return function (t) {
          for (var e = a; e--; ) n[e](t);
        };
      }
      r = r[0] || {};
      var s = mt[e],
        o = ha(r),
        u = (o.harness && (o.harness.aliases || {})[e]) || e,
        h = s
          ? function (t) {
              var e = new s();
              ((d._pt = 0), e.init(r, i ? t + i : t, d, 0, [r]), e.render(1, e), d._pt && ve(1, d));
            }
          : o.set(r, u);
      return s
        ? h
        : function (t) {
            return h(r, u, i ? t + i : t, o, 1);
          };
    },
    quickTo: function quickTo(t, i, e) {
      function by(t, e, r) {
        return n.resetTo(i, t, e, r);
      }
      var r,
        n = Fe.to(t, ta((((r = {})[i] = "+=0.1"), (r.paused = !0), (r.stagger = 0), r), e || {}));
      return ((by.tween = n), by);
    },
    isTweening: function isTweening(t) {
      return 0 < L.getTweensOf(t, !0).length;
    },
    defaults: function defaults(t) {
      return (t && t.ease && (t.ease = Vt(t.ease, j.ease)), wa(j, t || {}));
    },
    config: function config(t) {
      return wa(N, t || {});
    },
    registerEffect: function registerEffect(t) {
      var i = t.name,
        n = t.effect,
        e = t.plugins,
        a = t.defaults,
        r = t.extendTimeline;
      ((e || "").split(",").forEach(function (t) {
        return t && !mt[t] && !ht[t] && T(i + " effect requires " + t + " plugin.");
      }),
        (gt[i] = function (t, e, r) {
          return n(Pt(t), ta(e || {}, a), r);
        }),
        r &&
          (Zt.prototype[i] = function (t, e, r) {
            return this.add(gt[i](t, v(e) ? e : (r = e) && {}, this), r);
          }));
    },
    registerEase: function registerEase(t, e) {
      Bt[t] = Vt(e);
    },
    parseEase: function parseEase(t, e) {
      return arguments.length ? Vt(t, e) : Bt;
    },
    getById: function getById(t) {
      return L.getById(t);
    },
    exportRoot: function exportRoot(t, e) {
      void 0 === t && (t = {});
      var r,
        i,
        n = new Zt(t);
      for (
        n.smoothChildTiming = w(t.smoothChildTiming),
          L.remove(n),
          n._dp = 0,
          n._time = n._tTime = L._time,
          r = L._first;
        r;
      )
        ((i = r._next),
          (!e && !r._dur && r instanceof te && r.vars.onComplete === r._targets[0]) ||
            Na(n, r, r._start - r._delay),
          (r = i));
      return (Na(L, n, 0), n);
    },
    context: function context(t, e) {
      return t ? new De(t, e) : l;
    },
    matchMedia: function matchMedia(t) {
      return new Ee(t);
    },
    matchMediaRefresh: function matchMediaRefresh() {
      return (
        ke.forEach(function (t) {
          var e,
            r,
            i = t.conditions;
          for (r in i) i[r] && ((i[r] = !1), (e = 1));
          e && t.revert();
        }) || Ic()
      );
    },
    addEventListener: function addEventListener(t, e) {
      var r = Oe[t] || (Oe[t] = []);
      ~r.indexOf(e) || r.push(e);
    },
    removeEventListener: function removeEventListener(t, e) {
      var r = Oe[t],
        i = r && r.indexOf(e);
      0 <= i && r.splice(i, 1);
    },
    utils: {
      wrap: function wrap(e, t, r) {
        var i = t - e;
        return $(e)
          ? ob(e, wrap(0, e.length), t)
          : Za(r, function (t) {
              return ((i + ((t - e) % i)) % i) + e;
            });
      },
      wrapYoyo: function wrapYoyo(e, t, r) {
        var i = t - e,
          n = 2 * i;
        return $(e)
          ? ob(e, wrapYoyo(0, e.length - 1), t)
          : Za(r, function (t) {
              return e + (i < (t = (n + ((t - e) % n)) % n || 0) ? n - t : t);
            });
      },
      distribute: hb,
      random: kb,
      snap: jb,
      normalize: function normalize(t, e, r) {
        return At(t, e, 0, 1, r);
      },
      getUnit: _a,
      clamp: function clamp(e, r, t) {
        return Za(t, function (t) {
          return Mt(e, r, t);
        });
      },
      splitColor: Db,
      toArray: Pt,
      selector: fb,
      mapRange: At,
      pipe: function pipe() {
        for (var t = arguments.length, e = new Array(t), r = 0; r < t; r++) e[r] = arguments[r];
        return function (t) {
          return e.reduce(function (t, e) {
            return e(t);
          }, t);
        };
      },
      unitize: function unitize(e, r) {
        return function (t) {
          return e(parseFloat(t)) + (r || _a(t));
        };
      },
      interpolate: function interpolate(e, i, t, n) {
        var a = isNaN(e + i)
          ? 0
          : function (t) {
              return (1 - t) * e + t * i;
            };
        if (!a) {
          var s,
            o,
            u,
            h,
            l,
            f = r(e),
            d = {};
          if ((!0 === t && (n = 1) && (t = null), f)) ((e = { p: e }), (i = { p: i }));
          else if ($(e) && !$(i)) {
            for (u = [], h = e.length, l = h - 2, o = 1; o < h; o++)
              u.push(interpolate(e[o - 1], e[o]));
            (h--,
              (a = function func(t) {
                t *= h;
                var e = Math.min(l, ~~t);
                return u[e](t - e);
              }),
              (t = i));
          } else n || (e = bt($(e) ? [] : {}, e));
          if (!u) {
            for (s in i) Jt.call(d, e, s, "get", i[s]);
            a = function func(t) {
              return ve(t, d) || (f ? e.p : e);
            };
          }
        }
        return Za(t, a);
      },
      shuffle: gb,
    },
    install: R,
    effects: gt,
    ticker: It,
    updateRoot: Zt.updateRoot,
    plugins: mt,
    globalTimeline: L,
    core: {
      PropTween: we,
      globals: U,
      Tween: te,
      Timeline: Zt,
      Animation: qt,
      getCache: ha,
      _removeLinkedListItem: Ba,
      reverting: function reverting() {
        return I;
      },
      context: function context(t) {
        return (t && l && (l.data.push(t), (t._ctx = l)), l);
      },
      suppressOverwrites: function suppressOverwrites(t) {
        return (F = t);
      },
    },
  };
  (ja("to,from,fromTo,delayedCall,set,killTweensOf", function (t) {
    return (Re[t] = te[t]);
  }),
    It.add(Zt.updateRoot),
    (d = Re.to({}, { duration: 0 })));
  function Mc(t, e) {
    for (var r = t._pt; r && r.p !== e && r.op !== e && r.fp !== e; ) r = r._next;
    return r;
  }
  function Oc(t, a) {
    return {
      name: t,
      headless: 1,
      rawVars: 1,
      init: function init(t, n, e) {
        e._onInit = function (t) {
          var e, i;
          if (
            (r(n) &&
              ((e = {}),
              ja(n, function (t) {
                return (e[t] = 1);
              }),
              (n = e)),
            a)
          ) {
            for (i in ((e = {}), n)) e[i] = a(n[i]);
            n = e;
          }
          !(function _addModifiers(t, e) {
            var r,
              i,
              n,
              a = t._targets;
            for (r in e)
              for (i = a.length; i--; )
                (n = (n = t._ptLookup[i][r]) && n.d) &&
                  (n._pt && (n = Mc(n, r)), n && n.modifier && n.modifier(e[r], t, a[i], r));
          })(t, n);
        };
      },
    };
  }
  var Fe =
    Re.registerPlugin(
      {
        name: "attr",
        init: function init(t, e, r, i, n) {
          var a, s, o;
          for (a in ((this.tween = r), e))
            ((o = t.getAttribute(a) || ""),
              ((s = this.add(t, "setAttribute", (o || 0) + "", e[a], i, n, 0, 0, a)).op = a),
              (s.b = o),
              this._props.push(a));
        },
        render: function render(t, e) {
          for (var r = e._pt; r; ) (I ? r.set(r.t, r.p, r.b, r) : r.r(t, r.d), (r = r._next));
        },
      },
      {
        name: "endArray",
        headless: 1,
        init: function init(t, e) {
          for (var r = e.length; r--; ) this.add(t, r, t[r] || 0, e[r], 0, 0, 0, 0, 0, 1);
        },
      },
      Oc("roundProps", ib),
      Oc("modifiers"),
      Oc("snap", jb),
    ) || Re;
  ((te.version = Zt.version = Fe.version = "3.14.2"), (o = 1), x() && Lt());
  function yd(t, e) {
    return e.set(e.t, e.p, Math.round(1e4 * (e.s + e.c * t)) / 1e4 + e.u, e);
  }
  function zd(t, e) {
    return e.set(e.t, e.p, 1 === t ? e.e : Math.round(1e4 * (e.s + e.c * t)) / 1e4 + e.u, e);
  }
  function Ad(t, e) {
    return e.set(e.t, e.p, t ? Math.round(1e4 * (e.s + e.c * t)) / 1e4 + e.u : e.b, e);
  }
  function Bd(t, e) {
    return e.set(
      e.t,
      e.p,
      1 === t ? e.e : t ? Math.round(1e4 * (e.s + e.c * t)) / 1e4 + e.u : e.b,
      e,
    );
  }
  function Cd(t, e) {
    var r = e.s + e.c * t;
    e.set(e.t, e.p, ~~(r + (r < 0 ? -0.5 : 0.5)) + e.u, e);
  }
  function Dd(t, e) {
    return e.set(e.t, e.p, t ? e.e : e.b, e);
  }
  function Ed(t, e) {
    return e.set(e.t, e.p, 1 !== t ? e.b : e.e, e);
  }
  function Fd(t, e, r) {
    return (t.style[e] = r);
  }
  function Gd(t, e, r) {
    return t.style.setProperty(e, r);
  }
  function Hd(t, e, r) {
    return (t._gsap[e] = r);
  }
  function Id(t, e, r) {
    return (t._gsap.scaleX = t._gsap.scaleY = r);
  }
  function Jd(t, e, r, i, n) {
    var a = t._gsap;
    ((a.scaleX = a.scaleY = r), a.renderTransform(n, a));
  }
  function Kd(t, e, r, i, n) {
    var a = t._gsap;
    ((a[e] = r), a.renderTransform(n, a));
  }
  function Nd(t, e) {
    var r = this,
      i = this.target,
      n = i.style,
      a = i._gsap;
    if (t in hr && n) {
      if (((this.tfm = this.tfm || {}), "transform" === t))
        return mr.transform.split(",").forEach(function (t) {
          return Nd.call(r, t, e);
        });
      if (
        (~(t = mr[t] || t).indexOf(",")
          ? t.split(",").forEach(function (t) {
              return (r.tfm[t] = xr(i, t));
            })
          : (this.tfm[t] = a.x ? a[t] : xr(i, t)),
        t === vr && (this.tfm.zOrigin = a.zOrigin),
        0 <= this.props.indexOf(gr))
      )
        return;
      (a.svg && ((this.svgo = i.getAttribute("data-svg-origin")), this.props.push(vr, e, "")),
        (t = gr));
    }
    (n || e) && this.props.push(t, e, n[t]);
  }
  function Od(t) {
    t.translate &&
      (t.removeProperty("translate"), t.removeProperty("scale"), t.removeProperty("rotate"));
  }
  function Pd() {
    var t,
      e,
      r = this.props,
      i = this.target,
      n = i.style,
      a = i._gsap;
    for (t = 0; t < r.length; t += 3)
      r[t + 1]
        ? 2 === r[t + 1]
          ? i[r[t]](r[t + 2])
          : (i[r[t]] = r[t + 2])
        : r[t + 2]
          ? (n[r[t]] = r[t + 2])
          : n.removeProperty(
              "--" === r[t].substr(0, 2) ? r[t] : r[t].replace(cr, "-$1").toLowerCase(),
            );
    if (this.tfm) {
      for (e in this.tfm) a[e] = this.tfm[e];
      (a.svg && (a.renderTransform(), i.setAttribute("data-svg-origin", this.svgo || "")),
        ((t = Ue()) && t.isStart) ||
          n[gr] ||
          (Od(n),
          a.zOrigin &&
            n[vr] &&
            ((n[vr] += " " + a.zOrigin + "px"), (a.zOrigin = 0), a.renderTransform()),
          (a.uncache = 1)));
    }
  }
  function Qd(t, e) {
    var r = { target: t, props: [], revert: Pd, save: Nd };
    return (
      t._gsap || Fe.core.getCache(t),
      e &&
        t.style &&
        t.nodeType &&
        e.split(",").forEach(function (t) {
          return r.save(t);
        }),
      r
    );
  }
  function Sd(t, e) {
    var r = Le.createElementNS
      ? Le.createElementNS((e || "http://www.w3.org/1999/xhtml").replace(/^https/, "http"), t)
      : Le.createElement(t);
    return r && r.style ? r : Le.createElement(t);
  }
  function Td(t, e, r) {
    var i = getComputedStyle(t);
    return (
      i[e] ||
      i.getPropertyValue(e.replace(cr, "-$1").toLowerCase()) ||
      i.getPropertyValue(e) ||
      (!r && Td(t, Tr(e) || e, 1)) ||
      ""
    );
  }
  function Wd() {
    (function _windowExists() {
      return "undefined" != typeof window;
    })() &&
      window.document &&
      ((Ie = window),
      (Le = Ie.document),
      (Ye = Le.documentElement),
      (je = Sd("div") || { style: {} }),
      Sd("div"),
      (gr = Tr(gr)),
      (vr = gr + "Origin"),
      (je.style.cssText = "border-width:0;line-height:0;position:absolute;padding:0"),
      (Xe = !!Tr("perspective")),
      (Ue = Fe.core.reverting),
      (Ne = 1));
  }
  function Xd(t) {
    var e,
      r = t.ownerSVGElement,
      i = Sd("svg", (r && r.getAttribute("xmlns")) || "http://www.w3.org/2000/svg"),
      n = t.cloneNode(!0);
    ((n.style.display = "block"), i.appendChild(n), Ye.appendChild(i));
    try {
      e = n.getBBox();
    } catch (t) {}
    return (i.removeChild(n), Ye.removeChild(i), e);
  }
  function Yd(t, e) {
    for (var r = e.length; r--; ) if (t.hasAttribute(e[r])) return t.getAttribute(e[r]);
  }
  function Zd(e) {
    var r, i;
    try {
      r = e.getBBox();
    } catch (t) {
      ((r = Xd(e)), (i = 1));
    }
    return (
      (r && (r.width || r.height)) || i || (r = Xd(e)),
      !r || r.width || r.x || r.y
        ? r
        : {
            x: +Yd(e, ["x", "cx", "x1"]) || 0,
            y: +Yd(e, ["y", "cy", "y1"]) || 0,
            width: 0,
            height: 0,
          }
    );
  }
  function $d(t) {
    return !(!t.getCTM || (t.parentNode && !t.ownerSVGElement) || !Zd(t));
  }
  function _d(t, e) {
    if (e) {
      var r,
        i = t.style;
      (e in hr && e !== vr && (e = gr),
        i.removeProperty
          ? (("ms" !== (r = e.substr(0, 2)) && "webkit" !== e.substr(0, 6)) || (e = "-" + e),
            i.removeProperty("--" === r ? e : e.replace(cr, "-$1").toLowerCase()))
          : i.removeAttribute(e));
    }
  }
  function ae(t, e, r, i, n, a) {
    var s = new we(t._pt, e, r, 0, 1, a ? Ed : Dd);
    return (((t._pt = s).b = i), (s.e = n), t._props.push(r), s);
  }
  function de(t, e, r, i) {
    var n,
      a,
      s,
      o,
      u = parseFloat(r) || 0,
      h = (r + "").trim().substr((u + "").length) || "px",
      l = je.style,
      f = pr.test(e),
      d = "svg" === t.tagName.toLowerCase(),
      c = (d ? "client" : "offset") + (f ? "Width" : "Height"),
      p = "px" === i,
      _ = "%" === i;
    if (i === h || !u || br[i] || br[h]) return u;
    if (
      ("px" === h || p || (u = de(t, e, r, "px")),
      (o = t.getCTM && $d(t)),
      (_ || "%" === h) && (hr[e] || ~e.indexOf("adius")))
    )
      return (
        (n = o ? t.getBBox()[f ? "width" : "height"] : t[c]), ka(_ ? (u / n) * 100 : (u / 100) * n)
      );
    if (
      ((l[f ? "width" : "height"] = 100 + (p ? h : i)),
      (a =
        ("rem" !== i && ~e.indexOf("adius")) || ("em" === i && t.appendChild && !d)
          ? t
          : t.parentNode),
      o && (a = (t.ownerSVGElement || {}).parentNode),
      (a && a !== Le && a.appendChild) || (a = Le.body),
      (s = a._gsap) && _ && s.width && f && s.time === It.time && !s.uncache)
    )
      return ka((u / s.width) * 100);
    if (!_ || ("height" !== e && "width" !== e))
      ((!_ && "%" !== h) || wr[Td(a, "display")] || (l.position = Td(t, "position")),
        a === t && (l.position = "static"),
        a.appendChild(je),
        (n = je[c]),
        a.removeChild(je),
        (l.position = "absolute"));
    else {
      var m = t.style[e];
      ((t.style[e] = 100 + i), (n = t[c]), m ? (t.style[e] = m) : _d(t, e));
    }
    return (
      f && _ && (((s = ha(a)).time = It.time), (s.width = a[c])),
      ka(p ? (n * u) / 100 : n && u ? (100 / n) * u : 0)
    );
  }
  function fe(t, e, r, i) {
    if (!r || "none" === r) {
      var n = Tr(e, t, 1),
        a = n && Td(t, n, 1);
      a && a !== r ? ((e = n), (r = a)) : "borderColor" === e && (r = Td(t, "borderTopColor"));
    }
    var s,
      o,
      u,
      h,
      l,
      f,
      d,
      c,
      p,
      _,
      m,
      g = new we(this._pt, t.style, e, 0, 1, ge),
      v = 0,
      y = 0;
    if (
      ((g.b = r),
      (g.e = i),
      (r += ""),
      "var(--" === (i += "").substring(0, 6) && (i = Td(t, i.substring(4, i.indexOf(")")))),
      "auto" === i &&
        ((f = t.style[e]), (t.style[e] = i), (i = Td(t, e) || i), f ? (t.style[e] = f) : _d(t, e)),
      Ib((s = [r, i])),
      (i = s[1]),
      (u = (r = s[0]).match(nt) || []),
      (i.match(nt) || []).length)
    ) {
      for (; (o = nt.exec(i)); )
        ((d = o[0]),
          (p = i.substring(v, o.index)),
          l ? (l = (l + 1) % 5) : ("rgba(" !== p.substr(-5) && "hsla(" !== p.substr(-5)) || (l = 1),
          d !== (f = u[y++] || "") &&
            ((h = parseFloat(f) || 0),
            (m = f.substr((h + "").length)),
            "=" === d.charAt(1) && (d = ma(h, d) + m),
            (c = parseFloat(d)),
            (_ = d.substr((c + "").length)),
            (v = nt.lastIndex - _.length),
            _ || ((_ = _ || N.units[e] || m), v === i.length && ((i += _), (g.e += _))),
            m !== _ && (h = de(t, e, f, _) || 0),
            (g._pt = {
              _next: g._pt,
              p: p || 1 === y ? p : ",",
              s: h,
              c: c - h,
              m: (l && l < 4) || "zIndex" === e ? Math.round : 0,
            })));
      g.c = v < i.length ? i.substring(v, i.length) : "";
    } else g.r = "display" === e && "none" === i ? Ed : Dd;
    return (st.test(i) && (g.e = 0), (this._pt = g));
  }
  function he(t) {
    var e = t.split(" "),
      r = e[0],
      i = e[1] || "50%";
    return (
      ("top" !== r && "bottom" !== r && "left" !== i && "right" !== i) ||
        ((t = r), (r = i), (i = t)),
      (e[0] = kr[r] || r),
      (e[1] = kr[i] || i),
      e.join(" ")
    );
  }
  function ie(t, e) {
    if (e.tween && e.tween._time === e.tween._dur) {
      var r,
        i,
        n,
        a = e.t,
        s = a.style,
        o = e.u,
        u = a._gsap;
      if ("all" === o || !0 === o) ((s.cssText = ""), (i = 1));
      else
        for (n = (o = o.split(",")).length; -1 < --n; )
          ((r = o[n]), hr[r] && ((i = 1), (r = "transformOrigin" === r ? vr : gr)), _d(a, r));
      i &&
        (_d(a, gr),
        u &&
          (u.svg && a.removeAttribute("transform"),
          (s.scale = s.rotate = s.translate = "none"),
          Pr(a, 1),
          (u.uncache = 1),
          Od(s)));
    }
  }
  function me(t) {
    return "matrix(1, 0, 0, 1, 0, 0)" === t || "none" === t || !t;
  }
  function ne(t) {
    var e = Td(t, gr);
    return me(e) ? Mr : e.substr(7).match(it).map(ka);
  }
  function oe(t, e) {
    var r,
      i,
      n,
      a,
      s = t._gsap || ha(t),
      o = t.style,
      u = ne(t);
    return s.svg && t.getAttribute("transform")
      ? "1,0,0,1,0,0" ===
        (u = [(n = t.transform.baseVal.consolidate().matrix).a, n.b, n.c, n.d, n.e, n.f]).join(",")
        ? Mr
        : u
      : (u !== Mr ||
          t.offsetParent ||
          t === Ye ||
          s.svg ||
          ((n = o.display),
          (o.display = "block"),
          ((r = t.parentNode) && (t.offsetParent || t.getBoundingClientRect().width)) ||
            ((a = 1), (i = t.nextElementSibling), Ye.appendChild(t)),
          (u = ne(t)),
          n ? (o.display = n) : _d(t, "display"),
          a && (i ? r.insertBefore(t, i) : r ? r.appendChild(t) : Ye.removeChild(t))),
        e && 6 < u.length ? [u[0], u[1], u[4], u[5], u[12], u[13]] : u);
  }
  function pe(t, e, r, i, n, a) {
    var s,
      o,
      u,
      h = t._gsap,
      l = n || oe(t, !0),
      f = h.xOrigin || 0,
      d = h.yOrigin || 0,
      c = h.xOffset || 0,
      p = h.yOffset || 0,
      _ = l[0],
      m = l[1],
      g = l[2],
      v = l[3],
      y = l[4],
      T = l[5],
      b = e.split(" "),
      w = parseFloat(b[0]) || 0,
      x = parseFloat(b[1]) || 0;
    (r
      ? l !== Mr &&
        (o = _ * v - m * g) &&
        ((u = w * (-m / o) + x * (_ / o) - (_ * T - m * y) / o),
        (w = w * (v / o) + x * (-g / o) + (g * T - v * y) / o),
        (x = u))
      : ((w = (s = Zd(t)).x + (~b[0].indexOf("%") ? (w / 100) * s.width : w)),
        (x = s.y + (~(b[1] || b[0]).indexOf("%") ? (x / 100) * s.height : x))),
      i || (!1 !== i && h.smooth)
        ? ((y = w - f),
          (T = x - d),
          (h.xOffset = c + (y * _ + T * g) - y),
          (h.yOffset = p + (y * m + T * v) - T))
        : (h.xOffset = h.yOffset = 0),
      (h.xOrigin = w),
      (h.yOrigin = x),
      (h.smooth = !!i),
      (h.origin = e),
      (h.originIsAbsolute = !!r),
      (t.style[vr] = "0px 0px"),
      a &&
        (ae(a, h, "xOrigin", f, w),
        ae(a, h, "yOrigin", d, x),
        ae(a, h, "xOffset", c, h.xOffset),
        ae(a, h, "yOffset", p, h.yOffset)),
      t.setAttribute("data-svg-origin", w + " " + x));
  }
  function se(t, e, r) {
    var i = _a(e);
    return ka(parseFloat(e) + parseFloat(de(t, "x", r + "px", i))) + i;
  }
  function ze(t, e, i, n, a) {
    var s,
      o,
      u = 360,
      h = r(a),
      l = parseFloat(a) * (h && ~a.indexOf("rad") ? lr : 1) - n,
      f = n + l + "deg";
    return (
      h &&
        ("short" === (s = a.split("_")[1]) && (l %= u) !== l % 180 && (l += l < 0 ? u : -u),
        "cw" === s && l < 0
          ? (l = ((l + 36e9) % u) - ~~(l / u) * u)
          : "ccw" === s && 0 < l && (l = ((l - 36e9) % u) - ~~(l / u) * u)),
      (t._pt = o = new we(t._pt, e, i, n, l, zd)),
      (o.e = f),
      (o.u = "deg"),
      t._props.push(i),
      o
    );
  }
  function Ae(t, e) {
    for (var r in e) t[r] = e[r];
    return t;
  }
  function Be(t, e, r) {
    var i,
      n,
      a,
      s,
      o,
      u,
      h,
      l = Ae({}, r._gsap),
      f = r.style;
    for (n in (l.svg
      ? ((a = r.getAttribute("transform")),
        r.setAttribute("transform", ""),
        (f[gr] = e),
        (i = Pr(r, 1)),
        _d(r, gr),
        r.setAttribute("transform", a))
      : ((a = getComputedStyle(r)[gr]), (f[gr] = e), (i = Pr(r, 1)), (f[gr] = a)),
    hr))
      (a = l[n]) !== (s = i[n]) &&
        "perspective,force3D,transformOrigin,svgOrigin".indexOf(n) < 0 &&
        ((o = _a(a) !== (h = _a(s)) ? de(r, n, a, h) : parseFloat(a)),
        (u = parseFloat(s)),
        (t._pt = new we(t._pt, i, n, o, u - o, yd)),
        (t._pt.u = h || 0),
        t._props.push(n));
    Ae(i, l);
  }
  var Ie,
    Le,
    Ye,
    Ne,
    je,
    Ve,
    Ue,
    Xe,
    qe = Bt.Power0,
    Ze = Bt.Power1,
    We = Bt.Power2,
    He = Bt.Power3,
    Je = Bt.Power4,
    Qe = Bt.Linear,
    Ge = Bt.Quad,
    Ke = Bt.Cubic,
    $e = Bt.Quart,
    tr = Bt.Quint,
    er = Bt.Strong,
    rr = Bt.Elastic,
    ir = Bt.Back,
    nr = Bt.SteppedEase,
    ar = Bt.Bounce,
    sr = Bt.Sine,
    or = Bt.Expo,
    ur = Bt.Circ,
    hr = {},
    lr = 180 / Math.PI,
    fr = Math.PI / 180,
    dr = Math.atan2,
    cr = /([A-Z])/g,
    pr = /(left|right|width|margin|padding|x)/i,
    _r = /[\s,\(]\S/,
    mr = { autoAlpha: "opacity,visibility", scale: "scaleX,scaleY", alpha: "opacity" },
    gr = "transform",
    vr = gr + "Origin",
    yr = "O,Moz,ms,Ms,Webkit".split(","),
    Tr = function _checkPropPrefix(t, e, r) {
      var i = (e || je).style,
        n = 5;
      if (t in i && !r) return t;
      for (t = t.charAt(0).toUpperCase() + t.substr(1); n-- && !(yr[n] + t in i); );
      return n < 0 ? null : (3 === n ? "ms" : 0 <= n ? yr[n] : "") + t;
    },
    br = { deg: 1, rad: 1, turn: 1 },
    wr = { grid: 1, flex: 1 },
    xr = function _get(t, e, r, i) {
      var n;
      return (
        Ne || Wd(),
        e in mr && "transform" !== e && ~(e = mr[e]).indexOf(",") && (e = e.split(",")[0]),
        hr[e] && "transform" !== e
          ? ((n = Pr(t, i)),
            (n =
              "transformOrigin" !== e
                ? n[e]
                : n.svg
                  ? n.origin
                  : Ar(Td(t, vr)) + " " + n.zOrigin + "px"))
          : ((n = t.style[e]) && "auto" !== n && !i && !~(n + "").indexOf("calc(")) ||
            (n = (Or[e] && Or[e](t, e, r)) || Td(t, e) || ia(t, e) || ("opacity" === e ? 1 : 0)),
        r && !~(n + "").trim().indexOf(" ") ? de(t, e, n, r) + r : n
      );
    },
    kr = { top: "0%", bottom: "100%", left: "0%", right: "100%", center: "50%" },
    Or = {
      clearProps: function clearProps(t, e, r, i, n) {
        if ("isFromStart" !== n.data) {
          var a = (t._pt = new we(t._pt, e, r, 0, 0, ie));
          return ((a.u = i), (a.pr = -10), (a.tween = n), t._props.push(r), 1);
        }
      },
    },
    Mr = [1, 0, 0, 1, 0, 0],
    Cr = {},
    Pr = function _parseTransform(t, e) {
      var r = t._gsap || new Xt(t);
      if ("x" in r && !e && !r.uncache) return r;
      var i,
        n,
        a,
        s,
        o,
        u,
        h,
        l,
        f,
        d,
        c,
        p,
        _,
        m,
        g,
        v,
        y,
        T,
        b,
        w,
        x,
        k,
        O,
        M,
        C,
        P,
        A,
        D,
        S,
        z,
        E,
        R,
        F = t.style,
        I = r.scaleX < 0,
        L = "deg",
        B = getComputedStyle(t),
        Y = Td(t, vr) || "0";
      return (
        (i = n = a = u = h = l = f = d = c = 0),
        (s = o = 1),
        (r.svg = !(!t.getCTM || !$d(t))),
        B.translate &&
          (("none" === B.translate && "none" === B.scale && "none" === B.rotate) ||
            (F[gr] =
              ("none" !== B.translate
                ? "translate3d(" + (B.translate + " 0 0").split(" ").slice(0, 3).join(", ") + ") "
                : "") +
              ("none" !== B.rotate ? "rotate(" + B.rotate + ") " : "") +
              ("none" !== B.scale ? "scale(" + B.scale.split(" ").join(",") + ") " : "") +
              ("none" !== B[gr] ? B[gr] : "")),
          (F.scale = F.rotate = F.translate = "none")),
        (m = oe(t, r.svg)),
        r.svg &&
          ((M = r.uncache
            ? ((C = t.getBBox()), (Y = r.xOrigin - C.x + "px " + (r.yOrigin - C.y) + "px"), "")
            : !e && t.getAttribute("data-svg-origin")),
          pe(t, M || Y, !!M || r.originIsAbsolute, !1 !== r.smooth, m)),
        (p = r.xOrigin || 0),
        (_ = r.yOrigin || 0),
        m !== Mr &&
          ((T = m[0]),
          (b = m[1]),
          (w = m[2]),
          (x = m[3]),
          (i = k = m[4]),
          (n = O = m[5]),
          6 === m.length
            ? ((s = Math.sqrt(T * T + b * b)),
              (o = Math.sqrt(x * x + w * w)),
              (u = T || b ? dr(b, T) * lr : 0),
              (f = w || x ? dr(w, x) * lr + u : 0) && (o *= Math.abs(Math.cos(f * fr))),
              r.svg && ((i -= p - (p * T + _ * w)), (n -= _ - (p * b + _ * x))))
            : ((R = m[6]),
              (z = m[7]),
              (A = m[8]),
              (D = m[9]),
              (S = m[10]),
              (E = m[11]),
              (i = m[12]),
              (n = m[13]),
              (a = m[14]),
              (h = (g = dr(R, S)) * lr),
              g &&
                ((M = k * (v = Math.cos(-g)) + A * (y = Math.sin(-g))),
                (C = O * v + D * y),
                (P = R * v + S * y),
                (A = k * -y + A * v),
                (D = O * -y + D * v),
                (S = R * -y + S * v),
                (E = z * -y + E * v),
                (k = M),
                (O = C),
                (R = P)),
              (l = (g = dr(-w, S)) * lr),
              g &&
                ((v = Math.cos(-g)),
                (E = x * (y = Math.sin(-g)) + E * v),
                (T = M = T * v - A * y),
                (b = C = b * v - D * y),
                (w = P = w * v - S * y)),
              (u = (g = dr(b, T)) * lr),
              g &&
                ((M = T * (v = Math.cos(g)) + b * (y = Math.sin(g))),
                (C = k * v + O * y),
                (b = b * v - T * y),
                (O = O * v - k * y),
                (T = M),
                (k = C)),
              h && 359.9 < Math.abs(h) + Math.abs(u) && ((h = u = 0), (l = 180 - l)),
              (s = ka(Math.sqrt(T * T + b * b + w * w))),
              (o = ka(Math.sqrt(O * O + R * R))),
              (g = dr(k, O)),
              (f = 2e-4 < Math.abs(g) ? g * lr : 0),
              (c = E ? 1 / (E < 0 ? -E : E) : 0)),
          r.svg &&
            ((M = t.getAttribute("transform")),
            (r.forceCSS = t.setAttribute("transform", "") || !me(Td(t, gr))),
            M && t.setAttribute("transform", M))),
        90 < Math.abs(f) &&
          Math.abs(f) < 270 &&
          (I
            ? ((s *= -1), (f += u <= 0 ? 180 : -180), (u += u <= 0 ? 180 : -180))
            : ((o *= -1), (f += f <= 0 ? 180 : -180))),
        (e = e || r.uncache),
        (r.x =
          i -
          ((r.xPercent =
            i &&
            ((!e && r.xPercent) || (Math.round(t.offsetWidth / 2) === Math.round(-i) ? -50 : 0)))
            ? (t.offsetWidth * r.xPercent) / 100
            : 0) +
          "px"),
        (r.y =
          n -
          ((r.yPercent =
            n &&
            ((!e && r.yPercent) || (Math.round(t.offsetHeight / 2) === Math.round(-n) ? -50 : 0)))
            ? (t.offsetHeight * r.yPercent) / 100
            : 0) +
          "px"),
        (r.z = a + "px"),
        (r.scaleX = ka(s)),
        (r.scaleY = ka(o)),
        (r.rotation = ka(u) + L),
        (r.rotationX = ka(h) + L),
        (r.rotationY = ka(l) + L),
        (r.skewX = f + L),
        (r.skewY = d + L),
        (r.transformPerspective = c + "px"),
        (r.zOrigin = parseFloat(Y.split(" ")[2]) || (!e && r.zOrigin) || 0) && (F[vr] = Ar(Y)),
        (r.xOffset = r.yOffset = 0),
        (r.force3D = N.force3D),
        (r.renderTransform = r.svg ? Fr : Xe ? Rr : Dr),
        (r.uncache = 0),
        r
      );
    },
    Ar = function _firstTwoOnly(t) {
      return (t = t.split(" "))[0] + " " + t[1];
    },
    Dr = function _renderNon3DTransforms(t, e) {
      ((e.z = "0px"), (e.rotationY = e.rotationX = "0deg"), (e.force3D = 0), Rr(t, e));
    },
    Sr = "0deg",
    zr = "0px",
    Er = ") ",
    Rr = function _renderCSSTransforms(t, e) {
      var r = e || this,
        i = r.xPercent,
        n = r.yPercent,
        a = r.x,
        s = r.y,
        o = r.z,
        u = r.rotation,
        h = r.rotationY,
        l = r.rotationX,
        f = r.skewX,
        d = r.skewY,
        c = r.scaleX,
        p = r.scaleY,
        _ = r.transformPerspective,
        m = r.force3D,
        g = r.target,
        v = r.zOrigin,
        y = "",
        T = ("auto" === m && t && 1 !== t) || !0 === m;
      if (v && (l !== Sr || h !== Sr)) {
        var b,
          w = parseFloat(h) * fr,
          x = Math.sin(w),
          k = Math.cos(w);
        ((w = parseFloat(l) * fr),
          (b = Math.cos(w)),
          (a = se(g, a, x * b * -v)),
          (s = se(g, s, -Math.sin(w) * -v)),
          (o = se(g, o, k * b * -v + v)));
      }
      (_ !== zr && (y += "perspective(" + _ + Er),
        (i || n) && (y += "translate(" + i + "%, " + n + "%) "),
        (!T && a === zr && s === zr && o === zr) ||
          (y +=
            o !== zr || T
              ? "translate3d(" + a + ", " + s + ", " + o + ") "
              : "translate(" + a + ", " + s + Er),
        u !== Sr && (y += "rotate(" + u + Er),
        h !== Sr && (y += "rotateY(" + h + Er),
        l !== Sr && (y += "rotateX(" + l + Er),
        (f === Sr && d === Sr) || (y += "skew(" + f + ", " + d + Er),
        (1 === c && 1 === p) || (y += "scale(" + c + ", " + p + Er),
        (g.style[gr] = y || "translate(0, 0)"));
    },
    Fr = function _renderSVGTransforms(t, e) {
      var r,
        i,
        n,
        a,
        s,
        o = e || this,
        u = o.xPercent,
        h = o.yPercent,
        l = o.x,
        f = o.y,
        d = o.rotation,
        c = o.skewX,
        p = o.skewY,
        _ = o.scaleX,
        m = o.scaleY,
        g = o.target,
        v = o.xOrigin,
        y = o.yOrigin,
        T = o.xOffset,
        b = o.yOffset,
        w = o.forceCSS,
        x = parseFloat(l),
        k = parseFloat(f);
      ((d = parseFloat(d)),
        (c = parseFloat(c)),
        (p = parseFloat(p)) && ((c += p = parseFloat(p)), (d += p)),
        d || c
          ? ((d *= fr),
            (c *= fr),
            (r = Math.cos(d) * _),
            (i = Math.sin(d) * _),
            (n = Math.sin(d - c) * -m),
            (a = Math.cos(d - c) * m),
            c &&
              ((p *= fr),
              (s = Math.tan(c - p)),
              (n *= s = Math.sqrt(1 + s * s)),
              (a *= s),
              p && ((s = Math.tan(p)), (r *= s = Math.sqrt(1 + s * s)), (i *= s))),
            (r = ka(r)),
            (i = ka(i)),
            (n = ka(n)),
            (a = ka(a)))
          : ((r = _), (a = m), (i = n = 0)),
        ((x && !~(l + "").indexOf("px")) || (k && !~(f + "").indexOf("px"))) &&
          ((x = de(g, "x", l, "px")), (k = de(g, "y", f, "px"))),
        (v || y || T || b) &&
          ((x = ka(x + v - (v * r + y * n) + T)), (k = ka(k + y - (v * i + y * a) + b))),
        (u || h) &&
          ((s = g.getBBox()),
          (x = ka(x + (u / 100) * s.width)),
          (k = ka(k + (h / 100) * s.height))),
        (s = "matrix(" + r + "," + i + "," + n + "," + a + "," + x + "," + k + ")"),
        g.setAttribute("transform", s),
        w && (g.style[gr] = s));
    };
  ja("padding,margin,Width,Radius", function (e, r) {
    var t = "Right",
      i = "Bottom",
      n = "Left",
      o = (r < 3 ? ["Top", t, i, n] : ["Top" + n, "Top" + t, i + t, i + n]).map(function (t) {
        return r < 2 ? e + t : "border" + t + e;
      });
    Or[1 < r ? "border" + e : e] = function (e, t, r, i, n) {
      var a, s;
      if (arguments.length < 4)
        return (
          (a = o.map(function (t) {
            return xr(e, t, r);
          })),
          5 === (s = a.join(" ")).split(a[0]).length ? a[0] : s
        );
      ((a = (i + "").split(" ")),
        (s = {}),
        o.forEach(function (t, e) {
          return (s[t] = a[e] = a[e] || a[((e - 1) / 2) | 0]);
        }),
        e.init(t, s, n));
    };
  });
  var Ir,
    Lr,
    Br,
    Yr = {
      name: "css",
      register: Wd,
      targetTest: function targetTest(t) {
        return t.style && t.nodeType;
      },
      init: function init(t, e, i, n, a) {
        var s,
          o,
          u,
          h,
          l,
          f,
          d,
          c,
          p,
          _,
          m,
          g,
          v,
          y,
          T,
          b,
          w,
          x = this._props,
          k = t.style,
          O = i.vars.startAt;
        for (d in (Ne || Wd(),
        (this.styles = this.styles || Qd(t)),
        (b = this.styles.props),
        (this.tween = i),
        e))
          if ("autoRound" !== d && ((o = e[d]), !mt[d] || !dc(d, e, i, n, t, a)))
            if (
              ((l = typeof o),
              (f = Or[d]),
              "function" === l && (l = typeof (o = o.call(i, n, t, a))),
              "string" === l && ~o.indexOf("random(") && (o = rb(o)),
              f)
            )
              f(this, t, d, o, i) && (T = 1);
            else if ("--" === d.substr(0, 2))
              ((s = (getComputedStyle(t).getPropertyValue(d) + "").trim()),
                (o += ""),
                (Rt.lastIndex = 0),
                Rt.test(s) ||
                  ((c = _a(s)), (p = _a(o)) ? c !== p && (s = de(t, d, s, p) + p) : c && (o += c)),
                this.add(k, "setProperty", s, o, n, a, 0, 0, d),
                x.push(d),
                b.push(d, 0, k[d]));
            else if ("undefined" !== l) {
              if (
                (O && d in O
                  ? ((s = "function" == typeof O[d] ? O[d].call(i, n, t, a) : O[d]),
                    r(s) && ~s.indexOf("random(") && (s = rb(s)),
                    _a(s + "") || "auto" === s || (s += N.units[d] || _a(xr(t, d)) || ""),
                    "=" === (s + "").charAt(1) && (s = xr(t, d)))
                  : (s = xr(t, d)),
                (h = parseFloat(s)),
                (_ = "string" === l && "=" === o.charAt(1) && o.substr(0, 2)) && (o = o.substr(2)),
                (u = parseFloat(o)),
                d in mr &&
                  ("autoAlpha" === d &&
                    (1 === h && "hidden" === xr(t, "visibility") && u && (h = 0),
                    b.push("visibility", 0, k.visibility),
                    ae(
                      this,
                      k,
                      "visibility",
                      h ? "inherit" : "hidden",
                      u ? "inherit" : "hidden",
                      !u,
                    )),
                  "scale" !== d &&
                    "transform" !== d &&
                    ~(d = mr[d]).indexOf(",") &&
                    (d = d.split(",")[0])),
                (m = d in hr))
              ) {
                if (
                  (this.styles.save(d), (w = o), "string" === l && "var(--" === o.substring(0, 6))
                ) {
                  if ("calc(" === (o = Td(t, o.substring(4, o.indexOf(")")))).substring(0, 5)) {
                    var M = t.style.perspective;
                    ((t.style.perspective = o),
                      (o = Td(t, "perspective")),
                      M ? (t.style.perspective = M) : _d(t, "perspective"));
                  }
                  u = parseFloat(o);
                }
                if (
                  (g ||
                    (((v = t._gsap).renderTransform && !e.parseTransform) ||
                      Pr(t, e.parseTransform),
                    (y = !1 !== e.smoothOrigin && v.smooth),
                    ((g = this._pt =
                      new we(this._pt, k, gr, 0, 1, v.renderTransform, v, 0, -1)).dep = 1)),
                  "scale" === d)
                )
                  ((this._pt = new we(
                    this._pt,
                    v,
                    "scaleY",
                    v.scaleY,
                    (_ ? ma(v.scaleY, _ + u) : u) - v.scaleY || 0,
                    yd,
                  )),
                    (this._pt.u = 0),
                    x.push("scaleY", d),
                    (d += "X"));
                else {
                  if ("transformOrigin" === d) {
                    (b.push(vr, 0, k[vr]),
                      (o = he(o)),
                      v.svg
                        ? pe(t, o, 0, y, 0, this)
                        : ((p = parseFloat(o.split(" ")[2]) || 0) !== v.zOrigin &&
                            ae(this, v, "zOrigin", v.zOrigin, p),
                          ae(this, k, d, Ar(s), Ar(o))));
                    continue;
                  }
                  if ("svgOrigin" === d) {
                    pe(t, o, 1, y, 0, this);
                    continue;
                  }
                  if (d in Cr) {
                    ze(this, v, d, h, _ ? ma(h, _ + o) : o);
                    continue;
                  }
                  if ("smoothOrigin" === d) {
                    ae(this, v, "smooth", v.smooth, o);
                    continue;
                  }
                  if ("force3D" === d) {
                    v[d] = o;
                    continue;
                  }
                  if ("transform" === d) {
                    Be(this, o, t);
                    continue;
                  }
                }
              } else d in k || (d = Tr(d) || d);
              if (m || ((u || 0 === u) && (h || 0 === h) && !_r.test(o) && d in k))
                ((u = u || 0),
                  (c = (s + "").substr((h + "").length)) !==
                    (p = _a(o) || (d in N.units ? N.units[d] : c)) && (h = de(t, d, s, p)),
                  (this._pt = new we(
                    this._pt,
                    m ? v : k,
                    d,
                    h,
                    (_ ? ma(h, _ + u) : u) - h,
                    m || ("px" !== p && "zIndex" !== d) || !1 === e.autoRound ? yd : Cd,
                  )),
                  (this._pt.u = p || 0),
                  m && w !== o
                    ? ((this._pt.b = s), (this._pt.e = w), (this._pt.r = Bd))
                    : c !== p && "%" !== p && ((this._pt.b = s), (this._pt.r = Ad)));
              else if (d in k) fe.call(this, t, d, s, _ ? _ + o : o);
              else if (d in t) this.add(t, d, s || t[d], _ ? _ + o : o, n, a);
              else if ("parseTransform" !== d) {
                S(d, o);
                continue;
              }
              (m ||
                (d in k
                  ? b.push(d, 0, k[d])
                  : "function" == typeof t[d]
                    ? b.push(d, 2, t[d]())
                    : b.push(d, 1, s || t[d])),
                x.push(d));
            }
        T && be(this);
      },
      render: function render(t, e) {
        if (e.tween._time || !Ue()) for (var r = e._pt; r; ) (r.r(t, r.d), (r = r._next));
        else e.styles.revert();
      },
      get: xr,
      aliases: mr,
      getSetter: function getSetter(t, e, r) {
        var i = mr[e];
        return (
          i && i.indexOf(",") < 0 && (e = i),
          e in hr && e !== vr && (t._gsap.x || xr(t, "x"))
            ? r && Ve === r
              ? "scale" === e
                ? Id
                : Hd
              : (Ve = r || {}) && ("scale" === e ? Jd : Kd)
            : t.style && !u(t.style[e])
              ? Fd
              : ~e.indexOf("-")
                ? Gd
                : le(t, e)
        );
      },
      core: { _removeProperty: _d, _getMatrix: oe },
    };
  ((Fe.utils.checkPrefix = Tr),
    (Fe.core.getStyleSaver = Qd),
    (Br = ja(
      (Ir = "x,y,z,scale,scaleX,scaleY,xPercent,yPercent") +
        "," +
        (Lr = "rotation,rotationX,rotationY,skewX,skewY") +
        ",transform,transformOrigin,svgOrigin,force3D,smoothOrigin,transformPerspective",
      function (t) {
        hr[t] = 1;
      },
    )),
    ja(Lr, function (t) {
      ((N.units[t] = "deg"), (Cr[t] = 1));
    }),
    (mr[Br[13]] = Ir + "," + Lr),
    ja(
      "0:translateX,1:translateY,2:translateZ,8:rotate,8:rotationZ,8:rotateZ,9:rotateX,10:rotateY",
      function (t) {
        var e = t.split(":");
        mr[e[1]] = Br[e[0]];
      },
    ),
    ja(
      "x,y,z,top,right,bottom,left,width,height,fontSize,padding,margin,perspective",
      function (t) {
        N.units[t] = "px";
      },
    ),
    Fe.registerPlugin(Yr));
  var Nr = Fe.registerPlugin(Yr) || Fe,
    jr = Nr.core.Tween;
  ((e.Back = ir),
    (e.Bounce = ar),
    (e.CSSPlugin = Yr),
    (e.Circ = ur),
    (e.Cubic = Ke),
    (e.Elastic = rr),
    (e.Expo = or),
    (e.Linear = Qe),
    (e.Power0 = qe),
    (e.Power1 = Ze),
    (e.Power2 = We),
    (e.Power3 = He),
    (e.Power4 = Je),
    (e.Quad = Ge),
    (e.Quart = $e),
    (e.Quint = tr),
    (e.Sine = sr),
    (e.SteppedEase = nr),
    (e.Strong = er),
    (e.TimelineLite = Zt),
    (e.TimelineMax = Zt),
    (e.TweenLite = te),
    (e.TweenMax = jr),
    (e.default = Nr),
    (e.gsap = Nr));
  if (typeof window === "undefined" || window !== e) {
    Object.defineProperty(e, "__esModule", { value: !0 });
  } else {
    delete e.default;
  }
});
assets/GSAP-NOTICE.txt
GSAP 3.14.2
https://gsap.com

Copyright GreenSock. All rights reserved.
Used under the GSAP Standard License: https://gsap.com/standard-license.
assets/logo.svg
<svg width="231" height="223" viewBox="0 0 231 223" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M44.118 0H151.528C195.082 0 230.39 35.307 230.39 78.861V181.297H186.273V78.861C186.273 77.812 186.231 76.77 186.151 75.737L78.74 181.28C79.102 181.291 79.466 181.297 79.83 181.297H186.272V222.964H79.83C36.276 222.964 0 187.311 0 143.757V41.593H44.118V143.756C44.118 145.723 44.27 147.667 44.566 149.574L154.339 41.709C153.413 41.632 152.476 41.593 151.527 41.593H44.117V0H44.118Z" fill="black"/>
</svg>
assets/OpentypeJS-LICENSE.txt
The MIT License (MIT)

Copyright (c) 2020 Frederik De Bleser

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
assets/test-mark.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240"><path fill="#fff" fill-rule="evenodd" d="M120 10 146 80 220 82 162 128 183 207 120 162 57 207 78 128 20 82 94 80Z M120 89 139 123 120 140 101 123Z"/></svg>
assets/Three-LICENSE.txt
The MIT License

Copyright © 2010-2026 three.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
assets/ThreeMeshBVH-LICENSE.txt
The MIT License

Copyright (c) 2018 Garrett Johnson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
frost-sequence-camera-orbit.html
<!doctype html>
<html
  lang="en"
  data-composition-variables='[{"id":"headline1","type":"string","label":"First text (| = line break)","default":"Hard to|break.","maxLength":60},{"id":"headline2","type":"string","label":"Second text (| = line break)","default":"Easy to|remember.","maxLength":60},{"id":"logoUrl","type":"string","label":"Logo SVG asset path (upload in Assets, then paste path)","default":"assets/logo.svg","maxLength":200},{"id":"assemblyMode","type":"enum","label":"Assembly response","default":"hybrid","options":[{"value":"physical","label":"Physical attraction (100 = full strength)"},{"value":"directed","label":"Direct progress (100 = assembled)"},{"value":"hybrid","label":"Hybrid progress + physical flight"}]},{"id":"materialBaseColor","type":"color","label":"Base color","default":"#ffffff","materialSection":"Base","materialScope":"object"},{"id":"baseRoughness","type":"number","label":"Base roughness","default":0.31,"min":0,"max":0.5,"step":0.005,"materialSection":"Base","materialScope":"object"},{"id":"materialTransmission","type":"boolean","label":"Transmission","default":true,"materialSection":"Transmission","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"materialBacklight","type":"number","label":"Backlight through ice","default":0.27,"min":0,"max":2,"step":0.01,"materialSection":"Transmission","materialScope":"object"},{"id":"ior","type":"number","label":"Index of refraction","default":1.675,"min":1,"max":2,"step":0.005,"materialSection":"Transmission","materialScope":"object"},{"id":"thicknessScale","type":"number","label":"Thickness scale","default":2.04,"min":0.1,"max":3,"step":0.01,"materialSection":"Transmission","materialScope":"object"},{"id":"materialAbsorption","type":"boolean","label":"Absorption / tint","default":true,"materialSection":"Absorption / tint","materialScope":"object","materialToggle":true,"materialRequires":["materialTransmission"],"description":"Switch off to remove this feature without losing its settings."},{"id":"attenuationColor","type":"color","label":"Attenuation colour (white = no absorption, clear glass)","default":"#d5f4ff","materialSection":"Absorption / tint","materialScope":"object"},{"id":"attenuationDistance","type":"number","label":"Attenuation distance","default":6.54,"min":0.05,"max":12,"step":0.01,"materialSection":"Absorption / tint","materialScope":"object"},{"id":"materialDispersion","type":"boolean","label":"Dispersion","default":false,"materialSection":"Dispersion","materialScope":"object","materialToggle":true,"materialRequires":["materialTransmission"],"description":"Switch off to remove this feature without losing its settings."},{"id":"dispersion","type":"number","label":"Dispersion (0 = off; on/off reloads)","default":0.12,"min":0,"max":0.3,"step":0.005,"materialSection":"Dispersion","materialScope":"object"},{"id":"materialReflections","type":"boolean","label":"Reflections","default":true,"materialSection":"Reflections","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"envIntensity","type":"number","label":"Environment intensity","default":2.04,"min":0,"max":3,"step":0.01,"materialSection":"Reflections","materialScope":"object"},{"id":"specularIntensity","type":"number","label":"Specular intensity","default":1.44,"min":0,"max":2,"step":0.01,"materialSection":"Reflections","materialScope":"object"},{"id":"materialFrost","type":"boolean","label":"Frost","default":true,"materialSection":"Frost","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"materialInteriorFrost","type":"number","label":"Uniform frost","default":0,"min":0,"max":1,"step":0.01,"materialSection":"Frost","materialScope":"object"},{"id":"frostScale","type":"number","label":"Scale","default":0.7,"min":0.2,"max":6,"step":0.05,"materialSection":"Frost","materialScope":"object"},{"id":"frostThreshold","type":"number","label":"Threshold (1 = no frost, clear glass)","default":0.62,"min":0,"max":1,"step":0.01,"materialSection":"Frost","materialScope":"object"},{"id":"frostSoftness","type":"number","label":"Softness","default":0.24,"min":0.01,"max":1,"step":0.01,"materialSection":"Frost","materialScope":"object"},{"id":"frostRoughness","type":"number","label":"Roughness","default":0.73,"min":0,"max":1,"step":0.01,"materialSection":"Frost","materialScope":"object"},{"id":"frostDiffuse","type":"number","label":"Diffuse (how much light frosted areas catch)","default":0.11,"min":0,"max":1,"step":0.01,"materialSection":"Frost","materialScope":"object"},{"id":"materialSurfaceBumps","type":"boolean","label":"Object surface bumps / crack notches","default":false,"materialSection":"Surface bumps","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Disable crystal, grain, micro, ripple and crack normal perturbations and crack notches on the solid. Preserves their settings and the shard finish."},{"id":"materialCrystals","type":"boolean","label":"Crystal bumps","default":true,"materialSection":"Crystal bumps","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"crystalBump","type":"number","label":"Crystal bump","default":0.01,"min":0,"max":1,"step":0.01,"materialSection":"Crystal bumps","materialScope":"object"},{"id":"crystalScale","type":"number","label":"Crystal scale","default":4,"min":4,"max":80,"step":1,"materialSection":"Crystal bumps","materialScope":"object"},{"id":"materialGrain","type":"boolean","label":"Grain bumps","default":true,"materialSection":"Grain bumps","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"materialGrainAmount","type":"number","label":"Grain strength","default":0.47,"min":0,"max":2,"step":0.01,"materialSection":"Grain bumps","materialScope":"object"},{"id":"materialGrainScale","type":"number","label":"Grain scale","default":150,"min":1,"max":150,"step":1,"materialSection":"Grain bumps","materialScope":"object"},{"id":"materialCutNormals","type":"boolean","label":"Fracture normals","default":true,"materialSection":"Fracture normals","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"materialMicro","type":"boolean","label":"Micro bumps","default":true,"materialSection":"Micro bumps","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"microBump","type":"number","label":"Micro bump","default":0.445,"min":0,"max":1,"step":0.005,"materialSection":"Micro bumps","materialScope":"object"},{"id":"microScale","type":"number","label":"Micro scale","default":10,"min":10,"max":200,"step":1,"materialSection":"Micro bumps","materialScope":"object"},{"id":"microCoverage","type":"number","label":"Micro coverage","default":0.25,"min":0,"max":1,"step":0.01,"materialSection":"Micro bumps","materialScope":"object"},{"id":"bumpMaskScale","type":"number","label":"Mask scale","default":1.45,"min":0.1,"max":6,"step":0.05,"materialSection":"Micro bumps","materialScope":"object"},{"id":"materialRipples","type":"boolean","label":"Ripples","default":true,"materialSection":"Ripples","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"rippleBump","type":"number","label":"Ripple bump","default":0.22,"min":0,"max":1,"step":0.01,"materialSection":"Ripples","materialScope":"object"},{"id":"rippleScale","type":"number","label":"Ripple scale","default":23,"min":2,"max":30,"step":0.5,"materialSection":"Ripples","materialScope":"object"},{"id":"materialSmudges","type":"boolean","label":"Smudges","default":true,"materialSection":"Smudges","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"smudgeAmount","type":"number","label":"Amount","default":0.78,"min":0,"max":1,"step":0.01,"materialSection":"Smudges","materialScope":"object"},{"id":"smudgeCoverage","type":"number","label":"Coverage","default":0.58,"min":0,"max":1,"step":0.01,"materialSection":"Smudges","materialScope":"object"},{"id":"smudgeMaskScale","type":"number","label":"Mask scale","default":1.85,"min":0.1,"max":6,"step":0.05,"materialSection":"Smudges","materialScope":"object"},{"id":"smudgeAnisotropy","type":"number","label":"Anisotropy","default":16.5,"min":1,"max":20,"step":0.5,"materialSection":"Smudges","materialScope":"object"},{"id":"smudgeRoughness","type":"number","label":"Roughness","default":0.75,"min":0,"max":1,"step":0.01,"materialSection":"Smudges","materialScope":"object"},{"id":"smudgeWhiteness","type":"number","label":"Whiteness","default":0.035,"min":0,"max":0.5,"step":0.005,"materialSection":"Smudges","materialScope":"object"},{"id":"smudgeScale","type":"number","label":"Scale","default":1.3,"min":0.5,"max":10,"step":0.1,"materialSection":"Smudges","materialScope":"object"},{"id":"materialCracks","type":"boolean","label":"Cracks","default":true,"materialSection":"Cracks","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"crackLargeScale","type":"number","label":"Large scale","default":2.45,"min":0.3,"max":8,"step":0.05,"materialSection":"Cracks","materialScope":"object"},{"id":"crackWarp","type":"number","label":"Warp","default":0.22,"min":0,"max":1.5,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"crackCoverage","type":"number","label":"Coverage","default":0.19,"min":0,"max":1,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"crackRegionScale","type":"number","label":"Region scale","default":1.8,"min":0.1,"max":4,"step":0.05,"materialSection":"Cracks","materialScope":"object"},{"id":"crackRegionCoverage","type":"number","label":"Region coverage","default":0.6,"min":0,"max":1,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"veinScale","type":"number","label":"Vein scale","default":8,"min":1,"max":20,"step":0.25,"materialSection":"Cracks","materialScope":"object"},{"id":"veinContrast","type":"number","label":"Vein contrast","default":0.55,"min":0,"max":1,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"crackWidth","type":"number","label":"Width","default":0.0025,"min":0.0005,"max":0.02,"step":0.0005,"materialSection":"Cracks","materialScope":"object"},{"id":"crackBrightness","type":"number","label":"Brightness","default":0.65,"min":0,"max":3,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"crackDarkness","type":"number","label":"Darkness","default":0.69,"min":0,"max":1,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"crackRefraction","type":"number","label":"Refraction","default":0.076,"min":0,"max":0.1,"step":0.001,"materialSection":"Cracks","materialScope":"object"},{"id":"crackSurfaceStrength","type":"number","label":"Surface strength","default":0.16,"min":0,"max":1,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"fineScale","type":"number","label":"Fine scale","default":18.4,"min":2,"max":20,"step":0.1,"materialSection":"Cracks","materialScope":"object"},{"id":"fineAmount","type":"number","label":"Fine amount","default":0.52,"min":0,"max":1,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"fineCoverage","type":"number","label":"Fine coverage","default":1,"min":0,"max":1,"step":0.01,"materialSection":"Cracks","materialScope":"object"},{"id":"materialScatter","type":"boolean","label":"Internal scattering","default":true,"materialSection":"Internal scattering","materialScope":"object","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"materialInclusionScale","type":"number","label":"Inclusion scale","default":0.05,"min":0.05,"max":3,"step":0.01,"materialSection":"Internal scattering","materialScope":"object"},{"id":"materialInclusionAmount","type":"number","label":"Photographic inclusions","default":2,"min":0,"max":2,"step":0.01,"materialSection":"Internal scattering","materialScope":"object"},{"id":"interiorScatter","type":"number","label":"Interior scatter","default":0.09,"min":0,"max":1,"step":0.01,"materialSection":"Internal scattering","materialScope":"object"},{"id":"materialClearcoat","type":"boolean","label":"Clearcoat","default":true,"materialSection":"Clearcoat","materialScope":"object","materialToggle":true,"materialRequires":["materialReflections"],"description":"Switch off to remove this feature without losing its settings."},{"id":"clearcoat","type":"number","label":"Clearcoat","default":0,"min":0,"max":1,"step":0.01,"materialSection":"Clearcoat","materialScope":"object"},{"id":"clearcoatRoughness","type":"number","label":"Clearcoat roughness","default":0,"min":0,"max":1,"step":0.005,"materialSection":"Clearcoat","materialScope":"object"},{"id":"materialShardNormals","type":"boolean","label":"Shard normals","default":true,"materialSection":"Shard normals","materialScope":"shards","materialToggle":true,"materialRequires":[],"description":"Switch off to remove this feature without losing its settings."},{"id":"spriteNormal","type":"number","label":"Sprite normal strength","default":0.15,"min":0,"max":2.5,"step":0.05,"materialSection":"Shard normals","materialScope":"shards"},{"id":"materialShardFrost","type":"boolean","label":"Shard frost","default":true,"materialSection":"Shard frost","materialScope":"shards","materialToggle":true,"materialRequires":[],"description":"Uses the shared material settings from Logo & text. This switch enables them on shards."},{"id":"materialShardTransmission","type":"boolean","label":"Shard transparency","default":true,"materialSection":"Shard transparency","materialScope":"shards","materialToggle":true,"materialRequires":[],"description":"Uses the shared material settings from Logo & text. This switch enables them on shards."},{"id":"spriteSeeThrough","type":"number","label":"See-through (0 = off; on/off reloads)","default":1,"min":0,"max":1,"step":0.01,"materialSection":"Shard transparency","materialScope":"shards"},{"id":"materialShardReflections","type":"boolean","label":"Shard reflections","default":true,"materialSection":"Shard reflections","materialScope":"shards","materialToggle":true,"materialRequires":[],"description":"Uses the shared material settings from Logo & text. This switch enables them on shards."},{"id":"minPixelSize","type":"number","label":"Minimum pixel size","default":0.25,"min":0,"max":4,"step":0.05,"materialSection":"Shard shape","materialScope":"shards"},{"id":"grainSizeMultiplier","type":"number","label":"Grain size multiplier","default":1.05,"min":0.2,"max":4,"step":0.05,"materialSection":"Shard shape","materialScope":"shards"},{"id":"spriteSize","type":"number","label":"Sprite size","default":1.9,"min":0.3,"max":4,"step":0.05,"materialSection":"Shard shape","materialScope":"shards"},{"id":"spriteTilt","type":"number","label":"Sprite tilt","default":12,"min":0,"max":70,"step":1,"unit":"deg","materialSection":"Shard shape","materialScope":"shards"},{"id":"spriteAlphaCut","type":"number","label":"Alpha cut","default":0.6,"min":0.05,"max":0.6,"step":0.01,"materialSection":"Shard shape","materialScope":"shards"},{"id":"fontWeight","type":"enum","label":"Type - Weight (Geist)","default":"600","options":[{"value":"400","label":"Regular 400"},{"value":"600","label":"SemiBold 600"},{"value":"700","label":"Bold 700"}]},{"id":"letterSpacing","type":"number","label":"Type - Letter spacing","default":0.01,"min":-0.1,"max":0.4,"step":0.005,"unit":"em"},{"id":"shardAmount","type":"number","label":"Shards - Visible fraction of the broken volume (Powder amount)","default":0.44,"min":0.02,"max":1,"step":0.01},{"id":"strayDust","type":"number","label":"Shards - Ambient dust motes around the object (the experiment had 40)","default":24,"min":0,"max":200,"step":1},{"id":"sliceRadius","type":"number","label":"Break - First (diagonal) slice radius","default":0.6,"min":0.05,"max":1.5,"step":0.01},{"id":"sliceStrength","type":"number","label":"Break - First slice strength","default":21.5,"min":1,"max":40,"step":0.5},{"id":"finalEjectBoost","type":"number","label":"Break - Last break: eject speed and speed cap multiplier","default":2.5,"min":1,"max":6,"step":0.1},{"id":"shatterRadius","type":"number","label":"Break - Follow-up slice radius (sets their spacing too)","default":0.45,"min":0.1,"max":1.5,"step":0.01},{"id":"shatterStrength","type":"number","label":"Break - Follow-up slice strength","default":32.5,"min":1,"max":40,"step":0.5},{"id":"cutThreshold","type":"number","label":"Tune break - Cut threshold (surface gone above this erosion)","default":0.3,"min":0.3,"max":0.98,"step":0.01},{"id":"cutSoftness","type":"number","label":"Tune break - Cut softness","default":0.19,"min":0.005,"max":0.3,"step":0.005},{"id":"edgeWidth","type":"number","label":"Tune break - Crumbly edge band width","default":0.19,"min":0.05,"max":0.8,"step":0.01},{"id":"edgeInset","type":"number","label":"Tune break - Edge inset","default":0,"min":0,"max":0.3,"step":0.005},{"id":"brushSoftness","type":"number","label":"Tune break - Brush softness (edge falloff of a slice)","default":0.15,"min":0.02,"max":1,"step":0.01},{"id":"brushNoise","type":"number","label":"Tune break - Brush noise (ragged boundary)","default":0.4,"min":0,"max":1,"step":0.01},{"id":"crumbleRate","type":"number","label":"Tune break - Crumble rate along cracks (high = the whole shape goes at once)","default":4.3,"min":0,"max":6,"step":0.05},{"id":"crumbleCrackBias","type":"number","label":"Tune break - Crumble crack bias","default":5.1,"min":0,"max":6,"step":0.05},{"id":"crumbleDuration","type":"number","label":"Tune break - Crumble duration after a stroke","default":0.35,"min":0,"max":2,"step":0.01},{"id":"ejectSpeed","type":"number","label":"Flight - Eject speed","default":0.91,"min":0,"max":4,"step":0.01},{"id":"ejectSpread","type":"number","label":"Flight - Eject spread along the normal","default":0.48,"min":0,"max":3,"step":0.01},{"id":"ejectTurbulence","type":"number","label":"Flight - Eject turbulence","default":4,"min":0,"max":4,"step":0.01},{"id":"drag","type":"number","label":"Flight - Drag (speed decays by this per second)","default":0,"min":0,"max":8,"step":0.01},{"id":"gravity","type":"number","label":"Flight - Gravity (0 = shards never fall)","default":0,"min":0,"max":2,"step":0.005},{"id":"turbulence","type":"number","label":"Flight - Turbulence strength (curl noise)","default":4,"min":0,"max":4,"step":0.01},{"id":"turbulenceScale","type":"number","label":"Flight - Turbulence scale","default":2.65,"min":0.2,"max":8,"step":0.05},{"id":"turbulenceDecay","type":"number","label":"Flight - Turbulence decay with age (low = keeps swirling)","default":3.65,"min":0.05,"max":4,"step":0.01},{"id":"clumpCohesion","type":"number","label":"Flight - Clump cohesion (shards orbit a leader; 0 = none)","default":0.9,"min":0,"max":10,"step":0.05},{"id":"followObject","type":"number","label":"Flight - Shards follow the object motion for (s; 30 = whole flight)","default":30,"min":0,"max":30,"step":0.5,"unit":"s"},{"id":"settleTime","type":"number","label":"Flight - Settle time (velocity is killed after this; 12 = never)","default":2.2,"min":0.3,"max":12,"step":0.05},{"id":"settledDrift","type":"number","label":"Flight - Organic drift once settled (curl noise)","default":1,"min":0,"max":1,"step":0.005},{"id":"maxSpeed","type":"number","label":"Flight - Speed cap","default":26.3,"min":1,"max":40,"step":0.1},{"id":"repelStrength","type":"number","label":"Flight - Push out of the solid shape while it breaks","default":30,"min":0,"max":30,"step":0.1},{"id":"repelRange","type":"number","label":"Flight - Push range outside the surface","default":0.97,"min":0.02,"max":2,"step":0.01},{"id":"repelRadial","type":"number","label":"Flight - Push away from the shape centre (clears pockets and the hole)","default":17.5,"min":0,"max":60,"step":0.5},{"id":"repelRadialRange","type":"number","label":"Flight - Radial push range (object radii)","default":3.3,"min":1,"max":4,"step":0.05},{"id":"tumble","type":"number","label":"Flight - Tumble rate","default":1.05,"min":0,"max":12,"step":0.05},{"id":"returnGroupStagger","type":"number","label":"Assembly - Regional delay (seconds)","default":1.35,"min":0,"max":1.5,"step":0.05,"unit":"s"},{"id":"returnGroupScale","type":"number","label":"Assembly - Region / noise size","default":2.55,"min":0.1,"max":3,"step":0.05,"description":"Larger values form broader connected patches. Noise is evaluated only on retarget."},{"id":"returnGroupSeed","type":"number","label":"Return - Group timing seed","default":60765,"min":0,"max":65535,"step":1},{"id":"formSpread","type":"number","label":"Return - Wave spread (nearest shards leave first, seconds)","default":0,"min":0,"max":4,"step":0.05,"unit":"s"},{"id":"waveReach","type":"number","label":"Return - Distance over which the wave spreads","default":10,"min":0.2,"max":10,"step":0.1},{"id":"formJitter","type":"number","label":"Return - Per-shard stagger (random delay up to this)","default":2.8,"min":0,"max":3,"step":0.05,"unit":"s"},{"id":"formFill","type":"number","label":"Return - Fill-in rate for voxels no shard returns to","default":3,"min":0.05,"max":3,"step":0.05},{"id":"returnSpring","type":"number","label":"Return - Spring stiffness","default":29.9,"min":0.5,"max":40,"step":0.1},{"id":"returnDamping","type":"number","label":"Return - Spring damping","default":1.34,"min":0.2,"max":2,"step":0.01},{"id":"returnRamp","type":"number","label":"Return - Spring ramp-in (seconds until it pulls at full strength)","default":0.45,"min":0,"max":3,"step":0.05},{"id":"returnMaxSpeed","type":"number","label":"Return - Speed cap on the way home","default":60,"min":1,"max":60,"step":0.5},{"id":"alignToSurface","type":"number","label":"Return - Shards turn to lie on the surface (0 = keep tumbling)","default":1,"min":0,"max":1,"step":0.01},{"id":"alignCurve","type":"number","label":"Return - Alignment curve over the flight home (1 linear, higher = later)","default":3.75,"min":0.2,"max":4,"step":0.05},{"id":"healRate","type":"number","label":"Return - Neighbour heal rate","default":3,"min":0.02,"max":3,"step":0.01},{"id":"cellRestore","type":"number","label":"Return - Cell restore rate","default":60,"min":0,"max":60,"step":0.5},{"id":"landedFade","type":"number","label":"Return - Landed shard fade","default":1.65,"min":0,"max":2,"step":0.01},{"id":"refrostTime","type":"number","label":"Return - Refrost time","default":10.6,"min":0.2,"max":12,"step":0.1},{"id":"keyColor","type":"color","label":"Light - Key colour","default":"#f0f7ff"},{"id":"keyIntensity","type":"number","label":"Light - Key intensity","default":5.05,"min":0,"max":12,"step":0.05},{"id":"keyElevation","type":"number","label":"Light - Key elevation","default":42,"min":10,"max":89,"step":0.5,"unit":"deg"},{"id":"keyAzimuth","type":"number","label":"Light - Key azimuth","default":-38,"min":-90,"max":90,"step":0.5,"unit":"deg"},{"id":"keySize","type":"number","label":"Light - Key size (softbox)","default":1.25,"min":0.2,"max":3,"step":0.01},{"id":"fill","type":"number","label":"Light - Fill intensity (hemisphere, diffuse only: barely shows on clear ice)","default":0.18,"min":0,"max":1.5,"step":0.005},{"id":"fillColor","type":"color","label":"Light - Fill sky colour","default":"#cadde9"},{"id":"fillGroundColor","type":"color","label":"Light - Fill ground colour","default":"#172635"},{"id":"rim","type":"number","label":"Light - Rim spot intensity","default":2.5,"min":0,"max":5,"step":0.01},{"id":"rimColor","type":"color","label":"Light - Rim colour","default":"#d9f1ff"},{"id":"rimElevation","type":"number","label":"Light - Rim elevation (0 = straight behind)","default":20,"min":0,"max":89,"step":0.5,"unit":"deg"},{"id":"swayAmplitude","type":"number","label":"Light - Key sway amplitude","default":0,"min":0,"max":0.15,"step":0.001},{"id":"swayPeriod","type":"number","label":"Light - Key sway period","default":35,"min":2,"max":40,"step":0.5,"unit":"s"},{"id":"envSoftbox","type":"number","label":"Light - Environment softbox (the main light on glass)","default":1.1,"min":0,"max":3,"step":0.01},{"id":"envRim","type":"number","label":"Light - Environment rim strip","default":1.4,"min":0,"max":3,"step":0.01},{"id":"envFill","type":"number","label":"Light - Environment front fill","default":0.22,"min":0,"max":1,"step":0.005},{"id":"backdropTop","type":"color","label":"Backdrop - Top colour","default":"#050505"},{"id":"backdropMid","type":"color","label":"Backdrop - Mid colour","default":"#050505"},{"id":"backdropBottom","type":"color","label":"Backdrop - Bottom colour","default":"#050505"},{"id":"backdropCenterX","type":"number","label":"Backdrop - Bloom centre X","default":0.73,"min":0,"max":1,"step":0.005},{"id":"backdropCenterY","type":"number","label":"Backdrop - Bloom centre Y","default":0.22,"min":0,"max":2,"step":0.005},{"id":"backdropRadius","type":"number","label":"Backdrop - Bloom radius","default":0.8,"min":0.1,"max":2,"step":0.005},{"id":"backdropFalloff","type":"number","label":"Backdrop - Bloom falloff","default":0.85,"min":0.5,"max":6,"step":0.01},{"id":"backdropNoise","type":"number","label":"Backdrop - Dither noise","default":0,"min":0,"max":3,"step":0.05},{"id":"bloomThreshold","type":"number","label":"Post - Bloom threshold","default":3,"min":0,"max":3,"step":0.01},{"id":"bloomIntensity","type":"number","label":"Post - Bloom intensity","default":0.04,"min":0,"max":1.5,"step":0.005},{"id":"bloomRadius","type":"number","label":"Post - Bloom radius","default":1,"min":0,"max":1,"step":0.005},{"id":"monochrome","type":"number","label":"Post - Monochrome","default":0.04,"min":0,"max":1,"step":0.01},{"id":"tonemap","type":"enum","label":"Post - Tonemap","default":"aces","options":[{"value":"agx","label":"AgX"},{"value":"aces","label":"ACES"},{"value":"neutral","label":"Neutral"},{"value":"linear","label":"Linear"}]},{"id":"exposure","type":"number","label":"Post - Exposure","default":1,"min":0.1,"max":4,"step":0.01},{"id":"contrast","type":"number","label":"Post - Contrast","default":1.03,"min":0.6,"max":1.6,"step":0.005},{"id":"blackLift","type":"number","label":"Post - Black lift","default":0,"min":0,"max":0.1,"step":0.001},{"id":"vignetteStrength","type":"number","label":"Post - Vignette strength","default":0.14,"min":0,"max":1,"step":0.005},{"id":"vignetteSoftness","type":"number","label":"Post - Vignette softness","default":1.2,"min":0.1,"max":1.5,"step":0.005},{"id":"vignetteRadius","type":"number","label":"Post - Vignette radius","default":1.2,"min":0.2,"max":1.6,"step":0.005},{"id":"grainStrength","type":"number","label":"Post - Film grain","default":0,"min":0,"max":0.15,"step":0.001},{"id":"quality","type":"enum","label":"Performance - Quality profile","default":"full","options":[{"value":"full","label":"Full (baked look; needs a real GPU)"},{"value":"lite","label":"Lite (the source ?lite profile for software WebGPU)"}]},{"id":"upscaler","type":"enum","label":"Performance - Upscaler","default":"fsr1","options":[{"value":"fsr1","label":"FSR 1 (TRAA + EASU/RCAS)"},{"value":"taau","label":"TAAU"},{"value":"bilinear","label":"Bilinear"},{"value":"native","label":"Native (no upscale)"}]},{"id":"renderScale","type":"number","label":"Performance - Scene resolution scale (upscaled to 1080p)","default":1,"min":0.35,"max":1,"step":0.05},{"id":"shapeResolution","type":"enum","label":"Shape voxel resolution","default":"256","options":[{"value":"128","label":"128 — draft"},{"value":"256","label":"256 — high"},{"value":"384","label":"384 — ultra"}],"description":"Resolution of the logo and text distance fields. Rebuilds shapes; higher values increase startup time and memory. Independent of breakup resolution."},{"id":"erosionResolution","type":"enum","label":"Performance - Erosion field resolution (voxels per axis)","default":"96","options":[{"value":"64","label":"64"},{"value":"96","label":"96"},{"value":"128","label":"128"},{"value":"192","label":"192 (baked)"}]},{"id":"particleCount","type":"enum","label":"Performance - Powder particles","default":"100k","options":[{"value":"100k","label":"100k (baked)"},{"value":"250k","label":"250k"},{"value":"500k","label":"500k"},{"value":"1M","label":"1M"}]},{"id":"logoMeshDetail","type":"number","label":"Shape - Logo mesh detail (1-4; higher = finer surface)","default":2,"min":1,"max":4,"step":1},{"id":"deformStrength","type":"number","label":"Geometry - Ice deformation strength (0 = original)","default":0,"min":0,"max":0.08,"step":0.001},{"id":"deformScale","type":"number","label":"Geometry - Noise feature size (larger = broader)","default":0.41,"min":0.001,"max":0.5,"step":0.001},{"id":"deformSeed","type":"number","label":"Geometry - Deformation seed","default":7,"min":0,"max":65535,"step":1},{"id":"rimAzimuth","type":"number","label":"Light - Rim azimuth","default":-146,"min":-180,"max":180,"step":1},{"id":"rimAngle","type":"number","label":"Light - Rim beam angle","default":26,"min":10,"max":89,"step":1},{"id":"rimSize","type":"number","label":"Light - Rim reflection size","default":0.45,"min":0.1,"max":3,"step":0.05},{"id":"fillReflectionStrength","type":"number","label":"Light - Fill reflection strength","default":0.3,"min":0,"max":2,"step":0.01},{"id":"accentCoolIntensity","type":"number","label":"Studio Cool accent - intensity","default":3.71,"min":0,"max":4,"step":0.01},{"id":"accentCoolReflection","type":"number","label":"Studio Cool accent - reflection","default":2.31,"min":0,"max":3,"step":0.01},{"id":"accentCoolElevation","type":"number","label":"Studio Cool accent - elevation","default":63,"min":-85,"max":85,"step":1},{"id":"accentCoolAzimuth","type":"number","label":"Studio Cool accent - azimuth","default":22,"min":-180,"max":180,"step":1},{"id":"accentCoolSize","type":"number","label":"Studio Cool accent - size","default":2.65,"min":0.1,"max":3,"step":0.05},{"id":"accentCoolColor","type":"color","label":"Studio Cool accent - color","default":"#ff5900"},{"id":"accentWarmIntensity","type":"number","label":"Studio Warm accent - intensity","default":2.99,"min":0,"max":4,"step":0.01},{"id":"accentWarmReflection","type":"number","label":"Studio Warm accent - reflection","default":0.34,"min":0,"max":3,"step":0.01},{"id":"accentWarmElevation","type":"number","label":"Studio Warm accent - elevation","default":30,"min":-85,"max":85,"step":1},{"id":"accentWarmAzimuth","type":"number","label":"Studio Warm accent - azimuth","default":-30,"min":-180,"max":180,"step":1},{"id":"accentWarmSize","type":"number","label":"Studio Warm accent - size","default":1.2,"min":0.1,"max":3,"step":0.05},{"id":"accentWarmColor","type":"color","label":"Studio Warm accent - color","default":"#75aaff"},{"id":"returnNoiseAmount","type":"enum","label":"Assembly - Regional pattern","default":"2","options":[{"value":"2","label":"Travelling seam + organic spread"},{"value":"1","label":"Smooth noise regions"},{"value":"0","label":"Original cell groups"}]},{"id":"assemblyFrontDuration","type":"number","label":"Assembly - Growth duration (seconds)","default":3.2,"min":0,"max":6,"step":0.05},{"id":"assemblyOriginX","type":"number","label":"Assembly - Growth start X","default":-0.65,"min":-1,"max":1,"step":0.01},{"id":"assemblyOriginY","type":"number","label":"Assembly - Growth start Y","default":0.55,"min":-1,"max":1,"step":0.01},{"id":"assemblyAngle","type":"number","label":"Assembly - Seam direction (degrees)","default":-35,"min":-180,"max":180,"step":1},{"id":"assemblySpread","type":"number","label":"Assembly - Outward spread vs seam travel","default":0.65,"min":0,"max":1,"step":0.01},{"id":"assemblyFrontNoise","type":"number","label":"Assembly - Growth edge irregularity","default":0.35,"min":0,"max":1,"step":0.01},{"id":"assemblySpeedVariation","type":"number","label":"Assembly - Return speed variation","default":0.65,"min":0,"max":1,"step":0.01},{"id":"assemblyBend","type":"number","label":"Assembly - Approach path bend","default":1.2,"min":0,"max":3,"step":0.05},{"id":"assemblySwirl","type":"number","label":"Assembly - Approach twist","default":1.1,"min":0,"max":3,"step":0.05},{"id":"assemblyLandingVariation","type":"number","label":"Assembly - Landing transition variation","default":0.8,"min":0,"max":1,"step":0.01},{"id":"rendererProfile","type":"enum","label":"Renderer experiment (reload required)","default":"studio","options":[{"value":"original","label":"Original glass + GPU startup"},{"value":"lookup","label":"Original glass + material lookup"},{"value":"mesh","label":"Mesh surface + original glass"},{"value":"studio","label":"Studio glass approximation"},{"value":"matcap","label":"Matcap glass approximation"}]},{"id":"textWidth","type":"number","label":"Type - Headline block width (the mark is 2.6 wide)","default":6.5,"min":1.2,"max":9,"step":0.05},{"id":"textLineHeight","type":"number","label":"Type - Line height","default":0.95,"min":0.7,"max":1.4,"step":0.01,"unit":"em"},{"id":"textDepth","type":"number","label":"Type - Extrusion depth (fraction of the font size)","default":0.16,"min":0.05,"max":0.8,"step":0.01},{"id":"textBevel","type":"number","label":"Type - Bevel (fraction of the font size)","default":0.04,"min":0,"max":0.12,"step":0.005},{"id":"textCorner","type":"number","label":"Type - Corner rounding (fraction of the font size)","default":0.008,"min":0,"max":0.06,"step":0.002},{"id":"textMeshDetail","type":"number","label":"Type - Text mesh detail (1-4; higher = smoother deformation)","default":4,"min":1,"max":4,"step":1}]'
>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=1920, height=1080" />
    <title>Frost Sequence Rig</title>
    <script src="assets/gsap-3.14.2.min.js"></script>
    <script src="assets/frost.js"></script>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }
      html,
      body {
        background: #030303;
      }
      #frost-root {
        position: relative;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background: #030303;
      }
      #frost-stage {
        position: absolute;
        inset: 0;
        background: #030303;
      }
      #frost-stage canvas {
        position: absolute;
        inset: 0;
        width: 1920px;
        height: 1080px;
        display: block;
      }
      #frost-build-status {
        position: absolute;
        top: 16px;
        right: 16px;
        z-index: 5;
        padding: 10px 14px;
        background: #000b;
        color: #eee;
        font: 16px system-ui;
      }
      #frost-stage.nogpu {
        background: radial-gradient(ellipse 78% 78% at 50% 150%, #2a2a2a, #050505 70%, #030303);
      }
      /* the motion checker needs a moving layout box on every frame: a background-coloured 1.5 px plate */
      #frost-plate {
        position: absolute;
        left: 0;
        top: 1078px;
        width: 1.5px;
        height: 1.5px;
        background: #030303;
        pointer-events: none;
      }
      #frost-root {
        pointer-events: none;
      }
      #frost-root > .clip {
        position: absolute;
        left: 0;
        top: 0;
        width: 1920px;
        height: 1080px;
        background: transparent;
        pointer-events: none;
      }
    </style>
  </head>
  <body>
    <div
      data-hf-id="hf-b4of"
      id="frost-root"
      data-composition-id="frost-sequence-rig"
      data-start="0"
      data-duration="22.5"
      data-width="1920"
      data-height="1080"
      data-requires-webgpu=""
    >
      <div
        data-hf-id="hf-5a64"
        id="logo-position"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="1"
        data-name="Logo · Position XYZ"
        aria-label="Logo · Position XYZ"
      ></div>
      <div
        data-hf-id="hf-fjc6"
        id="logo-rotation"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="2"
        data-name="Logo · Rotation XYZ"
        aria-label="Logo · Rotation XYZ"
      ></div>
      <div
        data-hf-id="hf-speh"
        id="logo-breakup"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="3"
        data-name="Logo · Breakup %"
        aria-label="Logo · Breakup %"
      ></div>
      <div
        data-hf-id="hf-k8ny"
        id="logo-assembly"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="4"
        data-name="Logo · Assembly %"
        aria-label="Logo · Assembly %"
      ></div>
      <div
        data-hf-id="hf-uxt2"
        id="headline1-position"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="5"
        data-name="First text · Position XYZ"
        aria-label="First text · Position XYZ"
      ></div>
      <div
        data-hf-id="hf-y4rq"
        id="headline1-rotation"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="6"
        data-name="First text · Rotation XYZ"
        aria-label="First text · Rotation XYZ"
      ></div>
      <div
        data-hf-id="hf-35xb"
        id="headline1-breakup"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="7"
        data-name="First text · Breakup %"
        aria-label="First text · Breakup %"
      ></div>
      <div
        data-hf-id="hf-4hde"
        id="headline1-assembly"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="8"
        data-name="First text · Assembly %"
        aria-label="First text · Assembly %"
      ></div>
      <div
        data-hf-id="hf-tyaz"
        id="headline2-position"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="9"
        data-name="Second text · Position XYZ"
        aria-label="Second text · Position XYZ"
      ></div>
      <div
        data-hf-id="hf-2lbw"
        id="headline2-rotation"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="10"
        data-name="Second text · Rotation XYZ"
        aria-label="Second text · Rotation XYZ"
      ></div>
      <div
        data-hf-id="hf-xk1d"
        id="headline2-breakup"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="11"
        data-name="Second text · Breakup %"
        aria-label="Second text · Breakup %"
      ></div>
      <div
        data-hf-id="hf-9dhs"
        id="headline2-assembly"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="12"
        data-name="Second text · Assembly %"
        aria-label="Second text · Assembly %"
      ></div>
      <div
        data-hf-id="hf-b04i"
        id="camera-orbit"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="14"
        data-name="Camera · Orbit Y"
        aria-label="Camera · Orbit Y"
      ></div>
      <div
        data-hf-id="hf-flyz"
        id="scene-fade"
        class="clip"
        data-start="0"
        data-duration="22.5"
        data-track-index="13"
        data-name="Scene · Fade out %"
        aria-label="Scene · Fade out %"
      ></div>
    </div>
    <script>
      (() => {
        "use strict";
        // Studio reruns composition scripts in the same document after timeline edits.
        window.__frostInstance?.dispose();
        const W = 1920,
          H = 1080;
        const schema = [
          {
            id: "materialBaseColor",
            type: "color",
            label: "Base color",
            default: "#ffffff",
            materialSection: "Base",
            materialScope: "object",
          },
          {
            id: "baseRoughness",
            type: "number",
            label: "Base roughness",
            default: 0.31,
            min: 0,
            max: 0.5,
            step: 0.005,
            materialSection: "Base",
            materialScope: "object",
          },
          {
            id: "materialTransmission",
            type: "boolean",
            label: "Transmission",
            default: true,
            materialSection: "Transmission",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "materialBacklight",
            type: "number",
            label: "Backlight through ice",
            default: 0.27,
            min: 0,
            max: 2,
            step: 0.01,
            materialSection: "Transmission",
            materialScope: "object",
          },
          {
            id: "ior",
            type: "number",
            label: "Index of refraction",
            default: 1.675,
            min: 1,
            max: 2,
            step: 0.005,
            materialSection: "Transmission",
            materialScope: "object",
          },
          {
            id: "thicknessScale",
            type: "number",
            label: "Thickness scale",
            default: 2.04,
            min: 0.1,
            max: 3,
            step: 0.01,
            materialSection: "Transmission",
            materialScope: "object",
          },
          {
            id: "materialAbsorption",
            type: "boolean",
            label: "Absorption / tint",
            default: true,
            materialSection: "Absorption / tint",
            materialScope: "object",
            materialToggle: true,
            materialRequires: ["materialTransmission"],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "attenuationColor",
            type: "color",
            label: "Attenuation colour (white = no absorption, clear glass)",
            default: "#d5f4ff",
            materialSection: "Absorption / tint",
            materialScope: "object",
          },
          {
            id: "attenuationDistance",
            type: "number",
            label: "Attenuation distance",
            default: 6.54,
            min: 0.05,
            max: 12,
            step: 0.01,
            materialSection: "Absorption / tint",
            materialScope: "object",
          },
          {
            id: "materialDispersion",
            type: "boolean",
            label: "Dispersion",
            default: false,
            materialSection: "Dispersion",
            materialScope: "object",
            materialToggle: true,
            materialRequires: ["materialTransmission"],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "dispersion",
            type: "number",
            label: "Dispersion (0 = off; on/off reloads)",
            default: 0.12,
            min: 0,
            max: 0.3,
            step: 0.005,
            materialSection: "Dispersion",
            materialScope: "object",
          },
          {
            id: "materialReflections",
            type: "boolean",
            label: "Reflections",
            default: true,
            materialSection: "Reflections",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "envIntensity",
            type: "number",
            label: "Environment intensity",
            default: 2.04,
            min: 0,
            max: 3,
            step: 0.01,
            materialSection: "Reflections",
            materialScope: "object",
          },
          {
            id: "specularIntensity",
            type: "number",
            label: "Specular intensity",
            default: 1.44,
            min: 0,
            max: 2,
            step: 0.01,
            materialSection: "Reflections",
            materialScope: "object",
          },
          {
            id: "materialFrost",
            type: "boolean",
            label: "Frost",
            default: true,
            materialSection: "Frost",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "materialInteriorFrost",
            type: "number",
            label: "Uniform frost",
            default: 0,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Frost",
            materialScope: "object",
          },
          {
            id: "frostScale",
            type: "number",
            label: "Scale",
            default: 0.7,
            min: 0.2,
            max: 6,
            step: 0.05,
            materialSection: "Frost",
            materialScope: "object",
          },
          {
            id: "frostThreshold",
            type: "number",
            label: "Threshold (1 = no frost, clear glass)",
            default: 0.62,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Frost",
            materialScope: "object",
          },
          {
            id: "frostSoftness",
            type: "number",
            label: "Softness",
            default: 0.24,
            min: 0.01,
            max: 1,
            step: 0.01,
            materialSection: "Frost",
            materialScope: "object",
          },
          {
            id: "frostRoughness",
            type: "number",
            label: "Roughness",
            default: 0.73,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Frost",
            materialScope: "object",
          },
          {
            id: "frostDiffuse",
            type: "number",
            label: "Diffuse (how much light frosted areas catch)",
            default: 0.11,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Frost",
            materialScope: "object",
          },
          {
            id: "materialSurfaceBumps",
            type: "boolean",
            label: "Object surface bumps / crack notches",
            default: false,
            materialSection: "Surface bumps",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description:
              "Disable crystal, grain, micro, ripple and crack normal perturbations and crack notches on the solid. Preserves their settings and the shard finish.",
          },
          {
            id: "materialCrystals",
            type: "boolean",
            label: "Crystal bumps",
            default: true,
            materialSection: "Crystal bumps",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "crystalBump",
            type: "number",
            label: "Crystal bump",
            default: 0.01,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Crystal bumps",
            materialScope: "object",
          },
          {
            id: "crystalScale",
            type: "number",
            label: "Crystal scale",
            default: 4,
            min: 4,
            max: 80,
            step: 1,
            materialSection: "Crystal bumps",
            materialScope: "object",
          },
          {
            id: "materialGrain",
            type: "boolean",
            label: "Grain bumps",
            default: true,
            materialSection: "Grain bumps",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "materialGrainAmount",
            type: "number",
            label: "Grain strength",
            default: 0.47,
            min: 0,
            max: 2,
            step: 0.01,
            materialSection: "Grain bumps",
            materialScope: "object",
          },
          {
            id: "materialGrainScale",
            type: "number",
            label: "Grain scale",
            default: 150,
            min: 1,
            max: 150,
            step: 1,
            materialSection: "Grain bumps",
            materialScope: "object",
          },
          {
            id: "materialCutNormals",
            type: "boolean",
            label: "Fracture normals",
            default: true,
            materialSection: "Fracture normals",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "materialMicro",
            type: "boolean",
            label: "Micro bumps",
            default: true,
            materialSection: "Micro bumps",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "microBump",
            type: "number",
            label: "Micro bump",
            default: 0.445,
            min: 0,
            max: 1,
            step: 0.005,
            materialSection: "Micro bumps",
            materialScope: "object",
          },
          {
            id: "microScale",
            type: "number",
            label: "Micro scale",
            default: 10,
            min: 10,
            max: 200,
            step: 1,
            materialSection: "Micro bumps",
            materialScope: "object",
          },
          {
            id: "microCoverage",
            type: "number",
            label: "Micro coverage",
            default: 0.25,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Micro bumps",
            materialScope: "object",
          },
          {
            id: "bumpMaskScale",
            type: "number",
            label: "Mask scale",
            default: 1.45,
            min: 0.1,
            max: 6,
            step: 0.05,
            materialSection: "Micro bumps",
            materialScope: "object",
          },
          {
            id: "materialRipples",
            type: "boolean",
            label: "Ripples",
            default: true,
            materialSection: "Ripples",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "rippleBump",
            type: "number",
            label: "Ripple bump",
            default: 0.22,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Ripples",
            materialScope: "object",
          },
          {
            id: "rippleScale",
            type: "number",
            label: "Ripple scale",
            default: 23,
            min: 2,
            max: 30,
            step: 0.5,
            materialSection: "Ripples",
            materialScope: "object",
          },
          {
            id: "materialSmudges",
            type: "boolean",
            label: "Smudges",
            default: true,
            materialSection: "Smudges",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "smudgeAmount",
            type: "number",
            label: "Amount",
            default: 0.78,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Smudges",
            materialScope: "object",
          },
          {
            id: "smudgeCoverage",
            type: "number",
            label: "Coverage",
            default: 0.58,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Smudges",
            materialScope: "object",
          },
          {
            id: "smudgeMaskScale",
            type: "number",
            label: "Mask scale",
            default: 1.85,
            min: 0.1,
            max: 6,
            step: 0.05,
            materialSection: "Smudges",
            materialScope: "object",
          },
          {
            id: "smudgeAnisotropy",
            type: "number",
            label: "Anisotropy",
            default: 16.5,
            min: 1,
            max: 20,
            step: 0.5,
            materialSection: "Smudges",
            materialScope: "object",
          },
          {
            id: "smudgeRoughness",
            type: "number",
            label: "Roughness",
            default: 0.75,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Smudges",
            materialScope: "object",
          },
          {
            id: "smudgeWhiteness",
            type: "number",
            label: "Whiteness",
            default: 0.035,
            min: 0,
            max: 0.5,
            step: 0.005,
            materialSection: "Smudges",
            materialScope: "object",
          },
          {
            id: "smudgeScale",
            type: "number",
            label: "Scale",
            default: 1.3,
            min: 0.5,
            max: 10,
            step: 0.1,
            materialSection: "Smudges",
            materialScope: "object",
          },
          {
            id: "materialCracks",
            type: "boolean",
            label: "Cracks",
            default: true,
            materialSection: "Cracks",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "crackLargeScale",
            type: "number",
            label: "Large scale",
            default: 2.45,
            min: 0.3,
            max: 8,
            step: 0.05,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackWarp",
            type: "number",
            label: "Warp",
            default: 0.22,
            min: 0,
            max: 1.5,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackCoverage",
            type: "number",
            label: "Coverage",
            default: 0.19,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackRegionScale",
            type: "number",
            label: "Region scale",
            default: 1.8,
            min: 0.1,
            max: 4,
            step: 0.05,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackRegionCoverage",
            type: "number",
            label: "Region coverage",
            default: 0.6,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "veinScale",
            type: "number",
            label: "Vein scale",
            default: 8,
            min: 1,
            max: 20,
            step: 0.25,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "veinContrast",
            type: "number",
            label: "Vein contrast",
            default: 0.55,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackWidth",
            type: "number",
            label: "Width",
            default: 0.0025,
            min: 0.0005,
            max: 0.02,
            step: 0.0005,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackBrightness",
            type: "number",
            label: "Brightness",
            default: 0.65,
            min: 0,
            max: 3,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackDarkness",
            type: "number",
            label: "Darkness",
            default: 0.69,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackRefraction",
            type: "number",
            label: "Refraction",
            default: 0.076,
            min: 0,
            max: 0.1,
            step: 0.001,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "crackSurfaceStrength",
            type: "number",
            label: "Surface strength",
            default: 0.16,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "fineScale",
            type: "number",
            label: "Fine scale",
            default: 18.4,
            min: 2,
            max: 20,
            step: 0.1,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "fineAmount",
            type: "number",
            label: "Fine amount",
            default: 0.52,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "fineCoverage",
            type: "number",
            label: "Fine coverage",
            default: 1,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Cracks",
            materialScope: "object",
          },
          {
            id: "materialScatter",
            type: "boolean",
            label: "Internal scattering",
            default: true,
            materialSection: "Internal scattering",
            materialScope: "object",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "materialInclusionScale",
            type: "number",
            label: "Inclusion scale",
            default: 0.05,
            min: 0.05,
            max: 3,
            step: 0.01,
            materialSection: "Internal scattering",
            materialScope: "object",
          },
          {
            id: "materialInclusionAmount",
            type: "number",
            label: "Photographic inclusions",
            default: 2,
            min: 0,
            max: 2,
            step: 0.01,
            materialSection: "Internal scattering",
            materialScope: "object",
          },
          {
            id: "interiorScatter",
            type: "number",
            label: "Interior scatter",
            default: 0.09,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Internal scattering",
            materialScope: "object",
          },
          {
            id: "materialClearcoat",
            type: "boolean",
            label: "Clearcoat",
            default: true,
            materialSection: "Clearcoat",
            materialScope: "object",
            materialToggle: true,
            materialRequires: ["materialReflections"],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "clearcoat",
            type: "number",
            label: "Clearcoat",
            default: 0,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Clearcoat",
            materialScope: "object",
          },
          {
            id: "clearcoatRoughness",
            type: "number",
            label: "Clearcoat roughness",
            default: 0,
            min: 0,
            max: 1,
            step: 0.005,
            materialSection: "Clearcoat",
            materialScope: "object",
          },
          {
            id: "materialShardNormals",
            type: "boolean",
            label: "Shard normals",
            default: true,
            materialSection: "Shard normals",
            materialScope: "shards",
            materialToggle: true,
            materialRequires: [],
            description: "Switch off to remove this feature without losing its settings.",
          },
          {
            id: "spriteNormal",
            type: "number",
            label: "Sprite normal strength",
            default: 0.15,
            min: 0,
            max: 2.5,
            step: 0.05,
            materialSection: "Shard normals",
            materialScope: "shards",
          },
          {
            id: "materialShardFrost",
            type: "boolean",
            label: "Shard frost",
            default: true,
            materialSection: "Shard frost",
            materialScope: "shards",
            materialToggle: true,
            materialRequires: [],
            description:
              "Uses the shared material settings from Logo & text. This switch enables them on shards.",
          },
          {
            id: "materialShardTransmission",
            type: "boolean",
            label: "Shard transparency",
            default: true,
            materialSection: "Shard transparency",
            materialScope: "shards",
            materialToggle: true,
            materialRequires: [],
            description:
              "Uses the shared material settings from Logo & text. This switch enables them on shards.",
          },
          {
            id: "spriteSeeThrough",
            type: "number",
            label: "See-through (0 = off; on/off reloads)",
            default: 1,
            min: 0,
            max: 1,
            step: 0.01,
            materialSection: "Shard transparency",
            materialScope: "shards",
          },
          {
            id: "materialShardReflections",
            type: "boolean",
            label: "Shard reflections",
            default: true,
            materialSection: "Shard reflections",
            materialScope: "shards",
            materialToggle: true,
            materialRequires: [],
            description:
              "Uses the shared material settings from Logo & text. This switch enables them on shards.",
          },
          {
            id: "minPixelSize",
            type: "number",
            label: "Minimum pixel size",
            default: 0.25,
            min: 0,
            max: 4,
            step: 0.05,
            materialSection: "Shard shape",
            materialScope: "shards",
          },
          {
            id: "grainSizeMultiplier",
            type: "number",
            label: "Grain size multiplier",
            default: 1.05,
            min: 0.2,
            max: 4,
            step: 0.05,
            materialSection: "Shard shape",
            materialScope: "shards",
          },
          {
            id: "spriteSize",
            type: "number",
            label: "Sprite size",
            default: 1.9,
            min: 0.3,
            max: 4,
            step: 0.05,
            materialSection: "Shard shape",
            materialScope: "shards",
          },
          {
            id: "spriteTilt",
            type: "number",
            label: "Sprite tilt",
            default: 12,
            min: 0,
            max: 70,
            step: 1,
            unit: "deg",
            materialSection: "Shard shape",
            materialScope: "shards",
          },
          {
            id: "spriteAlphaCut",
            type: "number",
            label: "Alpha cut",
            default: 0.6,
            min: 0.05,
            max: 0.6,
            step: 0.01,
            materialSection: "Shard shape",
            materialScope: "shards",
          },
          {
            id: "headline1",
            type: "string",
            label: "Copy - Headline 1 (| = line break)",
            default: "Hard to|break.",
            maxLength: 60,
          },
          {
            id: "headline2",
            type: "string",
            label: "Copy - Headline 2 (| = line break)",
            default: "Easy to|remember.",
            maxLength: 60,
          },
          {
            id: "textWidth",
            type: "number",
            label: "Type - Headline block width (the mark is 2.6 wide)",
            default: 6.5,
            min: 1.2,
            max: 9,
            step: 0.05,
          },
          {
            id: "fontWeight",
            type: "enum",
            label: "Type - Weight (Geist)",
            default: "600",
            options: [
              { value: "400", label: "Regular 400" },
              { value: "600", label: "SemiBold 600" },
              { value: "700", label: "Bold 700" },
            ],
          },
          {
            id: "letterSpacing",
            type: "number",
            label: "Type - Letter spacing",
            default: 0.01,
            min: -0.1,
            max: 0.4,
            step: 0.005,
            unit: "em",
          },
          {
            id: "textLineHeight",
            type: "number",
            label: "Type - Line height",
            default: 0.95,
            min: 0.7,
            max: 1.4,
            step: 0.01,
            unit: "em",
          },
          {
            id: "textDepth",
            type: "number",
            label: "Type - Extrusion depth (fraction of the font size)",
            default: 0.16,
            min: 0.05,
            max: 0.8,
            step: 0.01,
          },
          {
            id: "textBevel",
            type: "number",
            label: "Type - Bevel (fraction of the font size)",
            default: 0.04,
            min: 0,
            max: 0.12,
            step: 0.005,
          },
          {
            id: "textCorner",
            type: "number",
            label: "Type - Corner rounding (fraction of the font size)",
            default: 0.008,
            min: 0,
            max: 0.06,
            step: 0.002,
          },
          {
            id: "textMeshDetail",
            type: "number",
            label: "Type - Text mesh detail (1-4; higher = smoother deformation)",
            default: 4,
            min: 1,
            max: 4,
            step: 1,
          },
          {
            id: "logoBreakAt",
            type: "number",
            label: "Timing - Mark breaks",
            default: 1.5,
            min: 0.3,
            max: 4,
            step: 0.1,
            unit: "s",
          },
          {
            id: "formHeadline1At",
            type: "number",
            label: "Timing - Shards form headline 1",
            default: 3.8,
            min: 2,
            max: 10,
            step: 0.1,
            unit: "s",
          },
          {
            id: "headline1BreakAt",
            type: "number",
            label: "Timing - Headline 1 breaks",
            default: 10.375,
            min: 6,
            max: 18,
            step: 0.025,
            unit: "s",
          },
          {
            id: "formHeadline2At",
            type: "number",
            label: "Timing - Shards form headline 2",
            default: 12.675,
            min: 9,
            max: 22,
            step: 0.025,
            unit: "s",
          },
          {
            id: "headline2BreakAt",
            type: "number",
            label: "Timing - Headline 2 breaks",
            default: 18.5,
            min: 15,
            max: 27,
            step: 0.025,
            unit: "s",
          },
          {
            id: "fadeOutAt",
            type: "number",
            label: "Timing - Shards fade to empty",
            default: 22,
            min: 17,
            max: 29.5,
            step: 0.025,
            unit: "s",
          },
          {
            id: "turnYaw",
            type: "number",
            label: "Motion - Mark turn (yaw)",
            default: 180,
            min: -360,
            max: 360,
            step: 5,
            unit: "deg",
          },
          {
            id: "turnPitch",
            type: "number",
            label: "Motion - Mark tilt toward the camera (pitch)",
            default: -41,
            min: -90,
            max: 90,
            step: 1,
            unit: "deg",
          },
          {
            id: "approach",
            type: "number",
            label: "Motion - How close the mark comes",
            default: 7,
            min: 0,
            max: 7,
            step: 0.1,
          },
          {
            id: "turnYaw2",
            type: "number",
            label: "Motion - Headline 1 turn (yaw; same sign as the mark turn keeps one direction)",
            default: 160,
            min: -360,
            max: 360,
            step: 5,
            unit: "deg",
          },
          {
            id: "turnPitch2",
            type: "number",
            label: "Motion - Headline 1 tilt (pitch)",
            default: 35,
            min: -90,
            max: 90,
            step: 1,
            unit: "deg",
          },
          {
            id: "retreat",
            type: "number",
            label: "Motion - How far headline 1 backs off",
            default: 3.5,
            min: 0,
            max: 8,
            step: 0.1,
          },
          {
            id: "zoomIn",
            type: "number",
            label: "Motion - Extra approach for the last break (shards fly past the camera)",
            default: 5,
            min: 0,
            max: 8,
            step: 0.1,
          },
          {
            id: "finalYaw",
            type: "number",
            label: "Motion - Last break turn (yaw)",
            default: 40,
            min: -180,
            max: 180,
            step: 5,
            unit: "deg",
          },
          {
            id: "finalPitch",
            type: "number",
            label: "Motion - Last break tilt (pitch)",
            default: -20,
            min: -90,
            max: 90,
            step: 1,
            unit: "deg",
          },
          {
            id: "driftYaw",
            type: "number",
            label: "Motion - Constant slow turn (never stands still)",
            default: 2,
            min: -30,
            max: 30,
            step: 0.5,
            unit: "deg/s",
          },
          {
            id: "driftSway",
            type: "number",
            label: "Motion - Slow tilt sway and depth bob amplitude",
            default: 4,
            min: 0,
            max: 20,
            step: 0.5,
            unit: "deg",
          },
          {
            id: "idleYaw",
            type: "number",
            label: "Motion - Idle turn back and forth while a headline holds (amplitude)",
            default: 6,
            min: 0,
            max: 40,
            step: 0.5,
            unit: "deg",
          },
          {
            id: "shardAmount",
            type: "number",
            label: "Shards - Visible fraction of the broken volume (Powder amount)",
            default: 0.44,
            min: 0.02,
            max: 1,
            step: 0.01,
          },
          {
            id: "strayDust",
            type: "number",
            label: "Shards - Ambient dust motes around the object (the experiment had 40)",
            default: 24,
            min: 0,
            max: 200,
            step: 1,
          },
          {
            id: "sliceRadius",
            type: "number",
            label: "Break - First (diagonal) slice radius",
            default: 0.6,
            min: 0.05,
            max: 1.5,
            step: 0.01,
          },
          {
            id: "sliceStrength",
            type: "number",
            label: "Break - First slice strength",
            default: 21.5,
            min: 1,
            max: 40,
            step: 0.5,
          },
          {
            id: "breakDuration",
            type: "number",
            label: "Break - Time for the follow-up slices to cross the shape",
            default: 2,
            min: 0.4,
            max: 4,
            step: 0.1,
            unit: "s",
          },
          {
            id: "finalEjectBoost",
            type: "number",
            label: "Break - Last break: eject speed and speed cap multiplier",
            default: 2.5,
            min: 1,
            max: 6,
            step: 0.1,
          },
          {
            id: "shatterRadius",
            type: "number",
            label: "Break - Follow-up slice radius (sets their spacing too)",
            default: 0.45,
            min: 0.1,
            max: 1.5,
            step: 0.01,
          },
          {
            id: "shatterStrength",
            type: "number",
            label: "Break - Follow-up slice strength",
            default: 32.5,
            min: 1,
            max: 40,
            step: 0.5,
          },
          {
            id: "cutThreshold",
            type: "number",
            label: "Tune break - Cut threshold (surface gone above this erosion)",
            default: 0.3,
            min: 0.3,
            max: 0.98,
            step: 0.01,
          },
          {
            id: "cutSoftness",
            type: "number",
            label: "Tune break - Cut softness",
            default: 0.19,
            min: 0.005,
            max: 0.3,
            step: 0.005,
          },
          {
            id: "edgeWidth",
            type: "number",
            label: "Tune break - Crumbly edge band width",
            default: 0.19,
            min: 0.05,
            max: 0.8,
            step: 0.01,
          },
          {
            id: "edgeInset",
            type: "number",
            label: "Tune break - Edge inset",
            default: 0,
            min: 0,
            max: 0.3,
            step: 0.005,
          },
          {
            id: "brushSoftness",
            type: "number",
            label: "Tune break - Brush softness (edge falloff of a slice)",
            default: 0.15,
            min: 0.02,
            max: 1,
            step: 0.01,
          },
          {
            id: "brushNoise",
            type: "number",
            label: "Tune break - Brush noise (ragged boundary)",
            default: 0.4,
            min: 0,
            max: 1,
            step: 0.01,
          },
          {
            id: "crumbleRate",
            type: "number",
            label: "Tune break - Crumble rate along cracks (high = the whole shape goes at once)",
            default: 4.3,
            min: 0,
            max: 6,
            step: 0.05,
          },
          {
            id: "crumbleCrackBias",
            type: "number",
            label: "Tune break - Crumble crack bias",
            default: 5.1,
            min: 0,
            max: 6,
            step: 0.05,
          },
          {
            id: "crumbleDuration",
            type: "number",
            label: "Tune break - Crumble duration after a stroke",
            default: 0.35,
            min: 0,
            max: 2,
            step: 0.01,
          },
          {
            id: "ejectSpeed",
            type: "number",
            label: "Flight - Eject speed",
            default: 0.91,
            min: 0,
            max: 4,
            step: 0.01,
          },
          {
            id: "ejectSpread",
            type: "number",
            label: "Flight - Eject spread along the normal",
            default: 0.48,
            min: 0,
            max: 3,
            step: 0.01,
          },
          {
            id: "ejectTurbulence",
            type: "number",
            label: "Flight - Eject turbulence",
            default: 4,
            min: 0,
            max: 4,
            step: 0.01,
          },
          {
            id: "drag",
            type: "number",
            label: "Flight - Drag (speed decays by this per second)",
            default: 0,
            min: 0,
            max: 8,
            step: 0.01,
          },
          {
            id: "gravity",
            type: "number",
            label: "Flight - Gravity (0 = shards never fall)",
            default: 0,
            min: 0,
            max: 2,
            step: 0.005,
          },
          {
            id: "turbulence",
            type: "number",
            label: "Flight - Turbulence strength (curl noise)",
            default: 4,
            min: 0,
            max: 4,
            step: 0.01,
          },
          {
            id: "turbulenceScale",
            type: "number",
            label: "Flight - Turbulence scale",
            default: 2.65,
            min: 0.2,
            max: 8,
            step: 0.05,
          },
          {
            id: "turbulenceDecay",
            type: "number",
            label: "Flight - Turbulence decay with age (low = keeps swirling)",
            default: 3.65,
            min: 0.05,
            max: 4,
            step: 0.01,
          },
          {
            id: "clumpCohesion",
            type: "number",
            label: "Flight - Clump cohesion (shards orbit a leader; 0 = none)",
            default: 0.9,
            min: 0,
            max: 10,
            step: 0.05,
          },
          {
            id: "followObject",
            type: "number",
            label: "Flight - Shards follow the object motion for (s; 30 = whole flight)",
            default: 30,
            min: 0,
            max: 30,
            step: 0.5,
            unit: "s",
          },
          {
            id: "settleTime",
            type: "number",
            label: "Flight - Settle time (velocity is killed after this; 12 = never)",
            default: 2.2,
            min: 0.3,
            max: 12,
            step: 0.05,
          },
          {
            id: "settledDrift",
            type: "number",
            label: "Flight - Organic drift once settled (curl noise)",
            default: 1,
            min: 0,
            max: 1,
            step: 0.005,
          },
          {
            id: "maxSpeed",
            type: "number",
            label: "Flight - Speed cap",
            default: 26.3,
            min: 1,
            max: 40,
            step: 0.1,
          },
          {
            id: "repelStrength",
            type: "number",
            label: "Flight - Push out of the solid shape while it breaks",
            default: 30,
            min: 0,
            max: 30,
            step: 0.1,
          },
          {
            id: "repelRange",
            type: "number",
            label: "Flight - Push range outside the surface",
            default: 0.97,
            min: 0.02,
            max: 2,
            step: 0.01,
          },
          {
            id: "repelRadial",
            type: "number",
            label: "Flight - Push away from the shape centre (clears pockets and the hole)",
            default: 17.5,
            min: 0,
            max: 60,
            step: 0.5,
          },
          {
            id: "repelRadialRange",
            type: "number",
            label: "Flight - Radial push range (object radii)",
            default: 3.3,
            min: 1,
            max: 4,
            step: 0.05,
          },
          {
            id: "tumble",
            type: "number",
            label: "Flight - Tumble rate",
            default: 1.05,
            min: 0,
            max: 12,
            step: 0.05,
          },
          {
            id: "returnGroupStagger",
            type: "number",
            label: "Assembly - Regional delay (seconds)",
            default: 1.35,
            min: 0,
            max: 1.5,
            step: 0.05,
            unit: "s",
          },
          {
            id: "returnGroupScale",
            type: "number",
            label: "Assembly - Region / noise size",
            default: 2.55,
            min: 0.1,
            max: 3,
            step: 0.05,
            description:
              "Larger values form broader connected patches. Noise is evaluated only on retarget.",
          },
          {
            id: "returnGroupSeed",
            type: "number",
            label: "Return - Group timing seed",
            default: 60765,
            min: 0,
            max: 65535,
            step: 1,
          },
          {
            id: "formSpread",
            type: "number",
            label: "Return - Wave spread (nearest shards leave first, seconds)",
            default: 0,
            min: 0,
            max: 4,
            step: 0.05,
            unit: "s",
          },
          {
            id: "waveReach",
            type: "number",
            label: "Return - Distance over which the wave spreads",
            default: 10,
            min: 0.2,
            max: 10,
            step: 0.1,
          },
          {
            id: "formJitter",
            type: "number",
            label: "Return - Per-shard stagger (random delay up to this)",
            default: 2.8,
            min: 0,
            max: 3,
            step: 0.05,
            unit: "s",
          },
          {
            id: "formFill",
            type: "number",
            label: "Return - Fill-in rate for voxels no shard returns to",
            default: 3,
            min: 0.05,
            max: 3,
            step: 0.05,
          },
          {
            id: "returnSpring",
            type: "number",
            label: "Return - Spring stiffness",
            default: 29.9,
            min: 0.5,
            max: 40,
            step: 0.1,
          },
          {
            id: "returnDamping",
            type: "number",
            label: "Return - Spring damping",
            default: 1.34,
            min: 0.2,
            max: 2,
            step: 0.01,
          },
          {
            id: "returnRamp",
            type: "number",
            label: "Return - Spring ramp-in (seconds until it pulls at full strength)",
            default: 0.45,
            min: 0,
            max: 3,
            step: 0.05,
          },
          {
            id: "returnMaxSpeed",
            type: "number",
            label: "Return - Speed cap on the way home",
            default: 60,
            min: 1,
            max: 60,
            step: 0.5,
          },
          {
            id: "alignToSurface",
            type: "number",
            label: "Return - Shards turn to lie on the surface (0 = keep tumbling)",
            default: 1,
            min: 0,
            max: 1,
            step: 0.01,
          },
          {
            id: "alignCurve",
            type: "number",
            label: "Return - Alignment curve over the flight home (1 linear, higher = later)",
            default: 3.75,
            min: 0.2,
            max: 4,
            step: 0.05,
          },
          {
            id: "healRate",
            type: "number",
            label: "Return - Neighbour heal rate",
            default: 3,
            min: 0.02,
            max: 3,
            step: 0.01,
          },
          {
            id: "cellRestore",
            type: "number",
            label: "Return - Cell restore rate",
            default: 60,
            min: 0,
            max: 60,
            step: 0.5,
          },
          {
            id: "landedFade",
            type: "number",
            label: "Return - Landed shard fade",
            default: 1.65,
            min: 0,
            max: 2,
            step: 0.01,
          },
          {
            id: "refrostTime",
            type: "number",
            label: "Return - Refrost time",
            default: 10.6,
            min: 0.2,
            max: 12,
            step: 0.1,
          },
          { id: "keyColor", type: "color", label: "Light - Key colour", default: "#f0f7ff" },
          {
            id: "keyIntensity",
            type: "number",
            label: "Light - Key intensity",
            default: 5.05,
            min: 0,
            max: 12,
            step: 0.05,
          },
          {
            id: "keyElevation",
            type: "number",
            label: "Light - Key elevation",
            default: 42,
            min: 10,
            max: 89,
            step: 0.5,
            unit: "deg",
          },
          {
            id: "keyAzimuth",
            type: "number",
            label: "Light - Key azimuth",
            default: -38,
            min: -90,
            max: 90,
            step: 0.5,
            unit: "deg",
          },
          {
            id: "keySize",
            type: "number",
            label: "Light - Key size (softbox)",
            default: 1.25,
            min: 0.2,
            max: 3,
            step: 0.01,
          },
          {
            id: "fill",
            type: "number",
            label: "Light - Fill intensity (hemisphere, diffuse only: barely shows on clear ice)",
            default: 0.18,
            min: 0,
            max: 1.5,
            step: 0.005,
          },
          { id: "fillColor", type: "color", label: "Light - Fill sky colour", default: "#cadde9" },
          {
            id: "fillGroundColor",
            type: "color",
            label: "Light - Fill ground colour",
            default: "#172635",
          },
          {
            id: "rim",
            type: "number",
            label: "Light - Rim spot intensity",
            default: 2.5,
            min: 0,
            max: 5,
            step: 0.01,
          },
          { id: "rimColor", type: "color", label: "Light - Rim colour", default: "#d9f1ff" },
          {
            id: "rimElevation",
            type: "number",
            label: "Light - Rim elevation (0 = straight behind)",
            default: 20,
            min: 0,
            max: 89,
            step: 0.5,
            unit: "deg",
          },
          {
            id: "swayAmplitude",
            type: "number",
            label: "Light - Key sway amplitude",
            default: 0,
            min: 0,
            max: 0.15,
            step: 0.001,
          },
          {
            id: "swayPeriod",
            type: "number",
            label: "Light - Key sway period",
            default: 35,
            min: 2,
            max: 40,
            step: 0.5,
            unit: "s",
          },
          {
            id: "envSoftbox",
            type: "number",
            label: "Light - Environment softbox (the main light on glass)",
            default: 1.1,
            min: 0,
            max: 3,
            step: 0.01,
          },
          {
            id: "envRim",
            type: "number",
            label: "Light - Environment rim strip",
            default: 1.4,
            min: 0,
            max: 3,
            step: 0.01,
          },
          {
            id: "envFill",
            type: "number",
            label: "Light - Environment front fill",
            default: 0.22,
            min: 0,
            max: 1,
            step: 0.005,
          },
          { id: "backdropTop", type: "color", label: "Backdrop - Top colour", default: "#050505" },
          { id: "backdropMid", type: "color", label: "Backdrop - Mid colour", default: "#050505" },
          {
            id: "backdropBottom",
            type: "color",
            label: "Backdrop - Bottom colour",
            default: "#050505",
          },
          {
            id: "backdropCenterX",
            type: "number",
            label: "Backdrop - Bloom centre X",
            default: 0.73,
            min: 0,
            max: 1,
            step: 0.005,
          },
          {
            id: "backdropCenterY",
            type: "number",
            label: "Backdrop - Bloom centre Y",
            default: 0.22,
            min: 0,
            max: 2,
            step: 0.005,
          },
          {
            id: "backdropRadius",
            type: "number",
            label: "Backdrop - Bloom radius",
            default: 0.8,
            min: 0.1,
            max: 2,
            step: 0.005,
          },
          {
            id: "backdropFalloff",
            type: "number",
            label: "Backdrop - Bloom falloff",
            default: 0.85,
            min: 0.5,
            max: 6,
            step: 0.01,
          },
          {
            id: "backdropNoise",
            type: "number",
            label: "Backdrop - Dither noise",
            default: 0,
            min: 0,
            max: 3,
            step: 0.05,
          },
          {
            id: "bloomThreshold",
            type: "number",
            label: "Post - Bloom threshold",
            default: 3,
            min: 0,
            max: 3,
            step: 0.01,
          },
          {
            id: "bloomIntensity",
            type: "number",
            label: "Post - Bloom intensity",
            default: 0.04,
            min: 0,
            max: 1.5,
            step: 0.005,
          },
          {
            id: "bloomRadius",
            type: "number",
            label: "Post - Bloom radius",
            default: 1,
            min: 0,
            max: 1,
            step: 0.005,
          },
          {
            id: "monochrome",
            type: "number",
            label: "Post - Monochrome",
            default: 0.04,
            min: 0,
            max: 1,
            step: 0.01,
          },
          {
            id: "tonemap",
            type: "enum",
            label: "Post - Tonemap",
            default: "aces",
            options: [
              { value: "agx", label: "AgX" },
              { value: "aces", label: "ACES" },
              { value: "neutral", label: "Neutral" },
              { value: "linear", label: "Linear" },
            ],
          },
          {
            id: "exposure",
            type: "number",
            label: "Post - Exposure",
            default: 1,
            min: 0.1,
            max: 4,
            step: 0.01,
          },
          {
            id: "contrast",
            type: "number",
            label: "Post - Contrast",
            default: 1.03,
            min: 0.6,
            max: 1.6,
            step: 0.005,
          },
          {
            id: "blackLift",
            type: "number",
            label: "Post - Black lift",
            default: 0,
            min: 0,
            max: 0.1,
            step: 0.001,
          },
          {
            id: "vignetteStrength",
            type: "number",
            label: "Post - Vignette strength",
            default: 0.14,
            min: 0,
            max: 1,
            step: 0.005,
          },
          {
            id: "vignetteSoftness",
            type: "number",
            label: "Post - Vignette softness",
            default: 1.2,
            min: 0.1,
            max: 1.5,
            step: 0.005,
          },
          {
            id: "vignetteRadius",
            type: "number",
            label: "Post - Vignette radius",
            default: 1.2,
            min: 0.2,
            max: 1.6,
            step: 0.005,
          },
          {
            id: "grainStrength",
            type: "number",
            label: "Post - Film grain",
            default: 0,
            min: 0,
            max: 0.15,
            step: 0.001,
          },
          {
            id: "quality",
            type: "enum",
            label: "Performance - Quality profile",
            default: "full",
            options: [
              { value: "full", label: "Full (baked look; needs a real GPU)" },
              { value: "lite", label: "Lite (the source ?lite profile for software WebGPU)" },
            ],
          },
          {
            id: "upscaler",
            type: "enum",
            label: "Performance - Upscaler",
            default: "fsr1",
            options: [
              { value: "fsr1", label: "FSR 1 (TRAA + EASU/RCAS)" },
              { value: "taau", label: "TAAU" },
              { value: "bilinear", label: "Bilinear" },
              { value: "native", label: "Native (no upscale)" },
            ],
          },
          {
            id: "renderScale",
            type: "number",
            label: "Performance - Scene resolution scale (upscaled to 1080p)",
            default: 1,
            min: 0.35,
            max: 1,
            step: 0.05,
          },
          {
            id: "shapeResolution",
            type: "enum",
            label: "Shape voxel resolution",
            default: "256",
            options: [
              { value: "128", label: "128 \u2014 draft" },
              { value: "256", label: "256 \u2014 high" },
              { value: "384", label: "384 \u2014 ultra" },
            ],
            description:
              "Resolution of the logo and text distance fields. Rebuilds shapes; higher values increase startup time and memory. Independent of breakup resolution.",
          },
          {
            id: "erosionResolution",
            type: "enum",
            label: "Performance - Erosion field resolution (voxels per axis)",
            default: "96",
            options: [
              { value: "64", label: "64" },
              { value: "96", label: "96" },
              { value: "128", label: "128" },
              { value: "192", label: "192 (baked)" },
            ],
          },
          {
            id: "particleCount",
            type: "enum",
            label: "Performance - Powder particles",
            default: "100k",
            options: [
              { value: "100k", label: "100k (baked)" },
              { value: "250k", label: "250k" },
              { value: "500k", label: "500k" },
              { value: "1M", label: "1M" },
            ],
          },
          {
            id: "logoMeshDetail",
            type: "number",
            label: "Shape - Logo mesh detail (1-4; higher = finer surface)",
            default: 2,
            min: 1,
            max: 4,
            step: 1,
          },
          {
            id: "logoUrl",
            type: "string",
            label: "Shape - SVG to extrude as the ice mark",
            default: "assets/logo.svg",
            maxLength: 200,
          },
          {
            id: "deformStrength",
            type: "number",
            label: "Geometry - Ice deformation strength (0 = original)",
            default: 0,
            min: 0,
            max: 0.08,
            step: 0.001,
          },
          {
            id: "deformScale",
            type: "number",
            label: "Geometry - Noise feature size (larger = broader)",
            default: 0.41,
            min: 0.001,
            max: 0.5,
            step: 0.001,
          },
          {
            id: "deformSeed",
            type: "number",
            label: "Geometry - Deformation seed",
            default: 7,
            min: 0,
            max: 65535,
            step: 1,
          },
          {
            id: "cameraMode",
            type: "enum",
            label: "Camera - Camera source",
            default: "original",
            options: [
              { value: "original", label: "Original approved camera" },
              { value: "authored", label: "Motion workspace track" },
            ],
          },
          {
            id: "cameraTrack",
            type: "string",
            label: "Camera - Motion track JSON (version 1)",
            default: "",
            maxLength: 48000,
          },
          {
            id: "rimAzimuth",
            type: "number",
            label: "Light - Rim azimuth",
            default: -146,
            min: -180,
            max: 180,
            step: 1,
          },
          {
            id: "rimAngle",
            type: "number",
            label: "Light - Rim beam angle",
            default: 26,
            min: 10,
            max: 89,
            step: 1,
          },
          {
            id: "rimSize",
            type: "number",
            label: "Light - Rim reflection size",
            default: 0.45,
            min: 0.1,
            max: 3,
            step: 0.05,
          },
          {
            id: "fillReflectionStrength",
            type: "number",
            label: "Light - Fill reflection strength",
            default: 0.3,
            min: 0,
            max: 2,
            step: 0.01,
          },
          {
            id: "accentCoolIntensity",
            type: "number",
            label: "Studio Cool accent - intensity",
            default: 3.71,
            min: 0,
            max: 4,
            step: 0.01,
          },
          {
            id: "accentCoolReflection",
            type: "number",
            label: "Studio Cool accent - reflection",
            default: 2.31,
            min: 0,
            max: 3,
            step: 0.01,
          },
          {
            id: "accentCoolElevation",
            type: "number",
            label: "Studio Cool accent - elevation",
            default: 63,
            min: -85,
            max: 85,
            step: 1,
          },
          {
            id: "accentCoolAzimuth",
            type: "number",
            label: "Studio Cool accent - azimuth",
            default: 22,
            min: -180,
            max: 180,
            step: 1,
          },
          {
            id: "accentCoolSize",
            type: "number",
            label: "Studio Cool accent - size",
            default: 2.65,
            min: 0.1,
            max: 3,
            step: 0.05,
          },
          {
            id: "accentCoolColor",
            type: "color",
            label: "Studio Cool accent - color",
            default: "#ff5900",
          },
          {
            id: "accentWarmIntensity",
            type: "number",
            label: "Studio Warm accent - intensity",
            default: 2.99,
            min: 0,
            max: 4,
            step: 0.01,
          },
          {
            id: "accentWarmReflection",
            type: "number",
            label: "Studio Warm accent - reflection",
            default: 0.34,
            min: 0,
            max: 3,
            step: 0.01,
          },
          {
            id: "accentWarmElevation",
            type: "number",
            label: "Studio Warm accent - elevation",
            default: 30,
            min: -85,
            max: 85,
            step: 1,
          },
          {
            id: "accentWarmAzimuth",
            type: "number",
            label: "Studio Warm accent - azimuth",
            default: -30,
            min: -180,
            max: 180,
            step: 1,
          },
          {
            id: "accentWarmSize",
            type: "number",
            label: "Studio Warm accent - size",
            default: 1.2,
            min: 0.1,
            max: 3,
            step: 0.05,
          },
          {
            id: "accentWarmColor",
            type: "color",
            label: "Studio Warm accent - color",
            default: "#75aaff",
          },
          {
            id: "returnNoiseAmount",
            type: "enum",
            label: "Assembly - Regional pattern",
            default: "2",
            options: [
              { value: "2", label: "Travelling seam + organic spread" },
              { value: "1", label: "Smooth noise regions" },
              { value: "0", label: "Original cell groups" },
            ],
          },
          {
            id: "assemblyFrontDuration",
            type: "number",
            label: "Assembly - Growth duration (seconds)",
            default: 3.2,
            min: 0,
            max: 6,
            step: 0.05,
          },
          {
            id: "assemblyOriginX",
            type: "number",
            label: "Assembly - Growth start X",
            default: -0.65,
            min: -1,
            max: 1,
            step: 0.01,
          },
          {
            id: "assemblyOriginY",
            type: "number",
            label: "Assembly - Growth start Y",
            default: 0.55,
            min: -1,
            max: 1,
            step: 0.01,
          },
          {
            id: "assemblyAngle",
            type: "number",
            label: "Assembly - Seam direction (degrees)",
            default: -35,
            min: -180,
            max: 180,
            step: 1,
          },
          {
            id: "assemblySpread",
            type: "number",
            label: "Assembly - Outward spread vs seam travel",
            default: 0.65,
            min: 0,
            max: 1,
            step: 0.01,
          },
          {
            id: "assemblyFrontNoise",
            type: "number",
            label: "Assembly - Growth edge irregularity",
            default: 0.35,
            min: 0,
            max: 1,
            step: 0.01,
          },
          {
            id: "assemblySpeedVariation",
            type: "number",
            label: "Assembly - Return speed variation",
            default: 0.65,
            min: 0,
            max: 1,
            step: 0.01,
          },
          {
            id: "assemblyBend",
            type: "number",
            label: "Assembly - Approach path bend",
            default: 1.2,
            min: 0,
            max: 3,
            step: 0.05,
          },
          {
            id: "assemblySwirl",
            type: "number",
            label: "Assembly - Approach twist",
            default: 1.1,
            min: 0,
            max: 3,
            step: 0.05,
          },
          {
            id: "assemblyLandingVariation",
            type: "number",
            label: "Assembly - Landing transition variation",
            default: 0.8,
            min: 0,
            max: 1,
            step: 0.01,
          },
          {
            id: "rendererProfile",
            type: "enum",
            label: "Renderer experiment (reload required)",
            default: "studio",
            options: [
              { value: "original", label: "Original glass + GPU startup" },
              { value: "lookup", label: "Original glass + material lookup" },
              { value: "mesh", label: "Mesh surface + original glass" },
              { value: "studio", label: "Studio glass approximation" },
              { value: "matcap", label: "Matcap glass approximation" },
            ],
          },
        ];
        const rigSchema = JSON.parse(
          document.documentElement.getAttribute("data-composition-variables"),
        );
        const defaults = Object.fromEntries(
          [...schema, ...rigSchema].map((v) => [v.id, v.default]),
        );
        const values = {
          ...defaults,
          ...(window.__hyperframes && window.__hyperframes.getVariables
            ? window.__hyperframes.getVariables()
            : {}),
        };
        window.Frost.setRendererProfile(values.rendererProfile);
        console.info("[Frost renderer]", JSON.stringify(window.__rewrite));
        const matcapKeys = [
          "ior",
          "thicknessScale",
          "attenuationColor",
          "attenuationDistance",
          "materialBacklight",
          "keyColor",
          "keyElevation",
          "keyAzimuth",
          "keySize",
          "envSoftbox",
          "envRim",
          "envFill",
          "rimColor",
          "rimElevation",
          "rimAzimuth",
          "rimSize",
          "accentCoolColor",
          "accentCoolReflection",
          "accentCoolElevation",
          "accentCoolAzimuth",
          "accentCoolSize",
          "accentWarmColor",
          "accentWarmReflection",
          "accentWarmElevation",
          "accentWarmAzimuth",
          "accentWarmSize",
        ];
        const matcapSignature = (v) => JSON.stringify(matcapKeys.map((k) => v[k]));
        const clamp = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v));
        // The GPU surface is an implementation detail; only the logo's authored controls belong on the timeline.
        const surfaceHost = document.getElementById("frost-root");
        const surfaceRoot = surfaceHost.shadowRoot || surfaceHost.attachShadow({ mode: "open" });
        surfaceRoot.replaceChildren();
        const surfaceStyle = document.createElement("style");
        surfaceStyle.textContent =
          "#frost-stage{position:absolute;inset:0;background:#030303}canvas{position:absolute;inset:0;width:1920px;height:1080px;display:block}#frost-build-status{position:absolute;top:16px;right:16px;z-index:5;color:#eee;font:16px system-ui}";
        surfaceRoot.appendChild(surfaceStyle);
        const stage = document.createElement("div");
        stage.id = "frost-stage";
        surfaceRoot.appendChild(stage);
        surfaceRoot.appendChild(document.createElement("slot"));
        stage.dataset.build = window.Frost.REVIEW_BUILD;
        const buildStatus = document.createElement("span");
        buildStatus.id = "frost-build-status";
        buildStatus.setAttribute("data-hf-ignore", "");
        stage.appendChild(buildStatus);
        const TUNE_IDS = Object.keys(window.Frost.TUNABLES);
        function optionsFrom(values) {
          const num = (key, a, b) =>
            Number.isFinite(Number(values[key])) ? clamp(Number(values[key]), a, b) : defaults[key];
          const str = (key, max) =>
            String(values[key] ?? defaults[key])
              .replace(/\s+/g, " ")
              .trim()
              .slice(0, max);
          const pick = (key, options) =>
            options.includes(String(values[key])) ? String(values[key]) : defaults[key];
          const lines = (key) =>
            str(key, 60)
              .split("|")
              .map((s) => s.trim())
              .filter(Boolean);
          const breakDuration = num("breakDuration", 0.4, 4);
          // beats: each far enough after the previous one for a break to finish before the next form
          const raw = {
            logoBreak: num("logoBreakAt", 0.3, 4),
            form1: num("formHeadline1At", 2, 10),
            break1: num("headline1BreakAt", 6, 18),
            form2: num("formHeadline2At", 9, 22),
            break2: num("headline2BreakAt", 15, 27),
            fadeOut: num("fadeOutAt", 17, 29.5),
          };
          const schedule = window.Frost.resolveSchedule(raw, breakDuration);
          const hex = (key) => {
            const v = String(values[key] ?? "")
              .trim()
              .toLowerCase();
            return /^#[0-9a-f]{3}$/.test(v)
              ? "#" +
                  v
                    .slice(1)
                    .split("")
                    .map((ch) => ch + ch)
                    .join("")
              : /^#[0-9a-f]{6}$/.test(v)
                ? v
                : defaults[key];
          };
          const tune = {};
          for (const id of TUNE_IDS) {
            const e = schema.find((v) => v.id === id);
            if (!e) continue;
            tune[id] =
              e.type === "boolean"
                ? values[id] === true || values[id] === 1 || values[id] === "true"
                : e.type === "number"
                  ? num(id, e.min, e.max)
                  : e.type === "color"
                    ? hex(id)
                    : e.type === "enum"
                      ? pick(
                          id,
                          e.options.map((x) => x.value),
                        )
                      : str(id, 64);
          }
          return {
            cameraMode: pick("cameraMode", ["original", "authored"]),
            cameraTrack: window.Frost.parseTrack(values.cameraTrack || ""),
            headlines: [lines("headline1"), lines("headline2")],
            text: {
              width: num("textWidth", 1.2, 9),
              lineHeight: num("textLineHeight", 0.7, 1.4),
              depth: num("textDepth", 0.05, 0.8),
              bevel: num("textBevel", 0, 0.12),
              corner: num("textCorner", 0, 0.06),
              meshDetail: Math.round(num("textMeshDetail", 1, 4)),
              weight: Number(pick("fontWeight", ["400", "600", "700"])),
              letterSpacing: num("letterSpacing", -0.1, 0.4),
            },
            schedule,
            deformation: {
              strength: num("deformStrength", 0, 0.08),
              scale: window.Frost.validateDeformationScale(
                Number(values.deformScale ?? defaults.deformScale),
              ),
              seed: Math.round(num("deformSeed", 0, 65535)),
            },
            pose: {
              turnYaw: num("turnYaw", -360, 360),
              turnPitch: num("turnPitch", -90, 90),
              approach: num("approach", 0, 7),
              turnYaw2: num("turnYaw2", -360, 360),
              turnPitch2: num("turnPitch2", -90, 90),
              retreat: num("retreat", 0, 8),
              zoomIn: num("zoomIn", 0, 8),
              finalYaw: num("finalYaw", -180, 180),
              finalPitch: num("finalPitch", -90, 90),
              driftYaw: num("driftYaw", -30, 30),
              driftSway: num("driftSway", 0, 20),
              idleYaw: num("idleYaw", 0, 40),
            },
            shards: {
              amount: num("shardAmount", 0.02, 1),
              formSpread: num("formSpread", 0, 4),
              formFill: num("formFill", 0.05, 3),
              sliceRadius: num("sliceRadius", 0.05, 1.5),
              sliceStrength: num("sliceStrength", 1, 40),
              shatterRadius: num("shatterRadius", 0.1, 1.5),
              shatterStrength: num("shatterStrength", 1, 40),
              breakDuration,
              strayDust: num("strayDust", 0, 200),
              followObject: num("followObject", 0, 30),
              finalEjectBoost: num("finalEjectBoost", 1, 6),
            },
            tune,
            quality: values.quality === "lite" ? "lite" : "full",
            upscaler: pick("upscaler", ["fsr1", "taau", "bilinear", "native"]),
            renderScale: num("renderScale", 0.35, 1),
            shapeResolution: pick("shapeResolution", ["128", "256", "384"]),
            erosionResolution: pick("erosionResolution", ["64", "96", "128", "192"]),
            particleCount: pick("particleCount", ["100k", "250k", "500k", "1M"]),
            logoUrl: str("logoUrl", 200) || defaults.logoUrl,
            logoMeshDetail: Math.round(num("logoMeshDetail", 1, 4)),
          };
        }
        let opts;
        try {
          opts = optionsFrom(values);
        } catch (error) {
          buildStatus.textContent = error.message;
          stage.dataset.state = "invalid-settings";
          window.__frostConfigError = error.message;
          return;
        }
        const schedule = opts.schedule;
        const DURATION = 22.5;
        const canvas = document.createElement("canvas");
        canvas.width = W;
        canvas.height = H;
        stage.appendChild(canvas);
        // Native 3D Depth is an editable scalar adapter on the two progress controllers.
        // Their Depth values mean 0–100% progress, never rendered object depth.
        // Explicit keyframes make Studio's 3D inspector record edits at the playhead.
        const timeline = gsap.timeline({ paused: true });
        timeline.to(
          "#logo-position",
          {
            keyframes: {
              "0%": { x: 0, y: 0, z: 0 },
              "100%": { x: 0, y: 0, z: 0, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#logo-rotation",
          {
            keyframes: {
              "0%": { rotationX: 0, rotationY: 0, rotationZ: 0 },
              "100%": { rotationX: 0, rotationY: 0, rotationZ: 0, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#headline1-position",
          {
            keyframes: {
              "0%": { x: 0, y: 0, z: 0 },
              "100%": { x: 0, y: 0, z: 0, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#headline1-rotation",
          {
            keyframes: {
              "0%": { rotationX: 0, rotationY: 0, rotationZ: 0 },
              "100%": { rotationX: 0, rotationY: 0, rotationZ: 0, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#headline2-position",
          {
            keyframes: {
              "0%": { x: 0, y: 0, z: 0 },
              "100%": { x: 0, y: 0, z: 0, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#headline2-rotation",
          {
            keyframes: {
              "0%": { rotationX: 0, rotationY: 0, rotationZ: 0 },
              "100%": { rotationX: 0, rotationY: 0, rotationZ: 0, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#logo-breakup",
          {
            keyframes: {
              "0%": { z: 0, ease: "none" },
              "6.222222222222222%": { z: 0, ease: "none" },
              "10.666666666666666%": { z: 100, ease: "power1.in" },
              "100%": { z: 100, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#logo-assembly",
          {
            keyframes: {
              "0%": { z: 0, ease: "none" },
              "100%": { z: 0, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#headline1-breakup",
          {
            keyframes: {
              "0%": { z: 0, ease: "none" },
              "45.33333333333333%": { z: 0, ease: "none" },
              "49.77777777777778%": { z: 100, ease: "power1.in" },
              "100%": { z: 100, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#headline1-assembly",
          {
            keyframes: {
              "0%": { z: 0, ease: "none" },
              "19.555555555555557%": { z: 0, ease: "none" },
              "33.33333333333333%": { z: 100, ease: "sine.inOut" },
              "100%": { z: 100, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#headline2-breakup",
          {
            keyframes: {
              "0%": { z: 0, ease: "none" },
              "86.66666666666667%": { z: 0, ease: "none" },
              "91.55555555555556%": { z: 100, ease: "power1.in" },
              "100%": { z: 100, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#headline2-assembly",
          {
            keyframes: {
              "0%": { z: 0, ease: "none" },
              "58.666666666666664%": { z: 0, ease: "none" },
              "74.66666666666667%": { z: 100, ease: "sine.inOut" },
              "100%": { z: 100, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#scene-fade",
          {
            keyframes: {
              "0%": { z: 0, ease: "none" },
              "93.33333333333333%": { z: 0, ease: "none" },
              "100%": { z: 100, ease: "sine.inOut" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        timeline.to(
          "#camera-orbit",
          {
            keyframes: {
              "0%": { rotationX: 0, rotationY: 0, rotationZ: 0, ease: "none" },
              "5.555555555555555%": { rotationX: 0, rotationY: -20, rotationZ: 0, ease: "none" },
              "7.6923076923076925%": {
                rotationX: 0,
                rotationY: -31.39736,
                rotationZ: 0,
                ease: "none",
              },
              "9.82905982905983%": {
                rotationX: 0,
                rotationY: -49.403732,
                rotationZ: 0,
                ease: "none",
              },
              "11.965811965811966%": {
                rotationX: 0,
                rotationY: -72.817478,
                rotationZ: 0,
                ease: "none",
              },
              "14.102564102564102%": {
                rotationX: 0,
                rotationY: -100.436959,
                rotationZ: 0,
                ease: "none",
              },
              "16.23931623931624%": {
                rotationX: 0,
                rotationY: -131.060537,
                rotationZ: 0,
                ease: "none",
              },
              "18.37606837606838%": {
                rotationX: 0,
                rotationY: -163.486573,
                rotationZ: 0,
                ease: "none",
              },
              "20.51282051282051%": {
                rotationX: 0,
                rotationY: -196.513427,
                rotationZ: 0,
                ease: "none",
              },
              "22.64957264957265%": {
                rotationX: 0,
                rotationY: -228.939463,
                rotationZ: 0,
                ease: "none",
              },
              "24.786324786324784%": {
                rotationX: 0,
                rotationY: -259.563041,
                rotationZ: 0,
                ease: "none",
              },
              "26.923076923076927%": {
                rotationX: 0,
                rotationY: -287.182522,
                rotationZ: 0,
                ease: "none",
              },
              "29.059829059829056%": {
                rotationX: 0,
                rotationY: -310.596268,
                rotationZ: 0,
                ease: "none",
              },
              "31.196581196581196%": {
                rotationX: 0,
                rotationY: -328.60264,
                rotationZ: 0,
                ease: "none",
              },
              "33.33333333333333%": { rotationX: 0, rotationY: -340, rotationZ: 0, ease: "none" },
              "44.44444444444444%": { rotationX: 0, rotationY: -380, rotationZ: 0, ease: "none" },
              "46.6031746031746%": {
                rotationX: 0,
                rotationY: -390.850146,
                rotationZ: 0,
                ease: "none",
              },
              "48.76190476190476%": {
                rotationX: 0,
                rotationY: -407.241983,
                rotationZ: 0,
                ease: "none",
              },
              "50.920634920634924%": {
                rotationX: 0,
                rotationY: -428.251895,
                rotationZ: 0,
                ease: "none",
              },
              "53.07936507936508%": {
                rotationX: 0,
                rotationY: -452.956268,
                rotationZ: 0,
                ease: "none",
              },
              "55.23809523809524%": {
                rotationX: 0,
                rotationY: -480.431487,
                rotationZ: 0,
                ease: "none",
              },
              "57.3968253968254%": {
                rotationX: 0,
                rotationY: -509.753936,
                rotationZ: 0,
                ease: "none",
              },
              "59.555555555555564%": { rotationX: 0, rotationY: -540, rotationZ: 0, ease: "none" },
              "61.71428571428572%": {
                rotationX: 0,
                rotationY: -570.246064,
                rotationZ: 0,
                ease: "none",
              },
              "63.87301587301587%": {
                rotationX: 0,
                rotationY: -599.568513,
                rotationZ: 0,
                ease: "none",
              },
              "66.03174603174602%": {
                rotationX: 0,
                rotationY: -627.043732,
                rotationZ: 0,
                ease: "none",
              },
              "68.19047619047619%": {
                rotationX: 0,
                rotationY: -651.748105,
                rotationZ: 0,
                ease: "none",
              },
              "70.34920634920636%": {
                rotationX: 0,
                rotationY: -672.758017,
                rotationZ: 0,
                ease: "none",
              },
              "72.50793650793652%": {
                rotationX: 0,
                rotationY: -689.149854,
                rotationZ: 0,
                ease: "none",
              },
              "74.66666666666667%": { rotationX: 0, rotationY: -700, rotationZ: 0, ease: "none" },
              "85.77777777777779%": { rotationX: 0, rotationY: -740, rotationZ: 0, ease: "none" },
              "87.80952380952381%": {
                rotationX: 0,
                rotationY: -754.178426,
                rotationZ: 0,
                ease: "none",
              },
              "89.84126984126985%": {
                rotationX: 0,
                rotationY: -780.349854,
                rotationZ: 0,
                ease: "none",
              },
              "91.87301587301587%": {
                rotationX: 0,
                rotationY: -815.91137,
                rotationZ: 0,
                ease: "none",
              },
              "93.90476190476191%": {
                rotationX: 0,
                rotationY: -858.260058,
                rotationZ: 0,
                ease: "none",
              },
              "95.93650793650794%": {
                rotationX: 0,
                rotationY: -904.793003,
                rotationZ: 0,
                ease: "none",
              },
              "97.96825396825398%": {
                rotationX: 0,
                rotationY: -952.907289,
                rotationZ: 0,
                ease: "none",
              },
              "100%": { rotationX: 0, rotationY: -1000, rotationZ: 0, ease: "none" },
            },
            duration: 22.5,
            ease: "none",
          },
          0,
        );
        window.__timelines = window.__timelines || {};
        window.__timelines["frost-sequence-rig"] = timeline;
        const activeTimeline = () => window.__timelines["frost-sequence-rig"] || timeline;
        function rigRevision() {
          const clean = (v) =>
            Array.isArray(v)
              ? v.map(clean)
              : v && typeof v === "object"
                ? Object.fromEntries(
                    Object.entries(v)
                      .filter(
                        ([k, v]) =>
                          !k.startsWith("_") &&
                          typeof v !== "function" &&
                          ![
                            "parent",
                            "callbackScope",
                            "scrollTrigger",
                            "onUpdateParams",
                            "onStartParams",
                            "onCompleteParams",
                          ].includes(k),
                      )
                      .map(([k, v]) => [k, clean(v)]),
                  )
                : v;
          return JSON.stringify(
            activeTimeline()
              .getChildren(false, true, false)
              .map((t) => [t.startTime(), t.duration(), clean(t.vars)]),
          );
        }
        function sampleRig(t) {
          const active = activeTimeline(),
            saved = active.totalTime();
          active.totalTime(t, true);
          const get = (id, k) => Number.parseFloat(gsap.getProperty("#" + id, k)) || 0;
          const objects = ["logo", "headline1", "headline2"].map((id) => ({
            x: get(id + "-position", "x") / 100,
            y: -get(id + "-position", "y") / 100,
            z: get(id + "-position", "z") / 100,
            rx: get(id + "-rotation", "rotationX"),
            ry: get(id + "-rotation", "rotationY"),
            rz: get(id + "-rotation", "rotationZ"),
            breakup: get(id + "-breakup", "z"),
            assembly: get(id + "-assembly", "z"),
          }));
          // Do not replace a visible solid. Incoming assembly takes over the existing
          // cloud once the outgoing object has fully broken. No particle reset.
          let target = 0;
          for (let k = 1; k < objects.length; k++)
            if (objects[k].assembly > 0 && objects[k - 1].breakup >= 99.999 && target === k - 1)
              target = k;
          const result = {
            ...objects[target],
            target,
            fade: get("scene-fade", "z"),
            cameraYaw: get("camera-orbit", "rotationY"),
            objects,
          };
          active.totalTime(saved, true);
          return result;
        }
        // Opt-in Studio-only preview quality. Standalone/capture always keeps full quality.
        let previewPixelRatio;
        try {
          if (
            window.parent !== window &&
            new URLSearchParams(location.search).get("__hf_shader_loading") === "player"
          ) {
            const quality = new URLSearchParams(window.parent.location.search).get("frostPreview");
            if (quality === "half") previewPixelRatio = 1;
            if (quality === "quarter") previewPixelRatio = 0.5;
          }
        } catch {} // Cross-origin embedding retains full quality.
        stage.dataset.previewQuality =
          previewPixelRatio === undefined ? "full" : String(previewPixelRatio / 2);
        const frost = window.Frost.create({
          canvas,
          width: W,
          height: H,
          ...opts,
          previewPixelRatio,
          rig: { duration: DURATION, mode: values.assemblyMode, sequence: true, sample: sampleRig },
          fontUrl: "assets/fonts/Geist-Bold.ttf",
          onProgress: (msg) => {
            stage.dataset.state = msg;
            buildStatus.textContent = "Building Frost · " + msg;
            (window.__frostLog = window.__frostLog || []).push([msg]);
          },
        });

        window.__hf = window.__hf || {};
        window.__hf.buildReady = window.__hf.buildReady || {};
        window.__hf.buildReady.frost = frost.ready;

        let lastTime = 0,
          disposed = false,
          renderFailed = false,
          revision = rigRevision();
        function describeRenderError(err) {
          if (err instanceof Error) return err.message;
          // A loader's rejected promise is often the raw DOM Event from an
          // onerror, not an Error, so the failed target is the real reason.
          if (err && typeof err === "object" && "type" in err) {
            const source = err.target && (err.target.src || err.target.href);
            return source ? `failed to load ${source}` : `${err.type} event`;
          }
          return String(err);
        }
        function showRenderError(err) {
          if (disposed || renderFailed) return;
          renderFailed = true;
          const message = describeRenderError(err);
          console.error("Frost initialization/render failed:", err);
          window.__frostRuntimeError = message;
          stage.dataset.state = "render-error";
          stage.dataset.busy = "false";
          buildStatus.hidden = false;
          buildStatus.textContent = "Frost failed: " + message;
        }
        function renderAt(time) {
          if (disposed || renderFailed) return;
          const nextRevision = rigRevision();
          if (nextRevision !== revision) {
            revision = nextRevision;
            stage.dataset.revisions = String(Number(stage.dataset.revisions || 0) + 1);
            frost.invalidate();
          }
          lastTime = clamp(time, 0, DURATION);
          canvas.style.opacity = String(1 - clamp(sampleRig(lastTime).fade / 100));
          frost.renderAt(lastTime);
          stage.dataset.requestedTime = lastTime.toFixed(4);
          stage.dataset.busy = "true";
          const requested = lastTime;
          frost
            .waitForGpu()
            .then(() => {
              if (disposed || requested !== lastTime) return;
              stage.dataset.time = requested.toFixed(4);
              stage.dataset.rigSample = JSON.stringify(sampleRig(requested));
              stage.dataset.busy = "false";
            })
            .catch(showRenderError);
        }
        frost.ready
          .then(() => {
            buildStatus.hidden = true;
            if (!disposed) renderAt(lastTime);
          })
          .catch((err) => {
            showRenderError(err);
          });

        timeline.eventCallback("onUpdate", () => renderAt(timeline.time()));
        window.__logoRig = { sample: sampleRig, mode: values.assemblyMode };

        function seek(event) {
          const d = event && event.detail;
          if (!d || typeof d.time !== "number") return;
          const time = clamp(d.time, 0, DURATION);
          activeTimeline().totalTime(time, true);
          renderAt(time);
          // async renderer: hold the capture until the GPU queue has drained (and until the scene has loaded)
          if (typeof d.waitUntil === "function") d.waitUntil(frost.waitForGpu());
        }
        window.addEventListener("hf-seek", seek);
        window.__frostRendererProfile = values.rendererProfile;
        window.__frostReady = frost.ready.then(async () => {
          if (disposed) return;
          const world = frost.world,
            camera = world.camera;
          // Capture the approved camera framing, then orbit that view around the fixed object.
          world.rig.update(camera, world.objectGroup, 0, 0);
          const base = camera.position.clone(),
            aim = world.rig.lookAt.clone();
          const pivot = world.motionGroup.position.clone();
          const rotate = (out, source, angle) => {
            const x = source.x - pivot.x,
              z = source.z - pivot.z,
              c = Math.cos(angle),
              s = Math.sin(angle);
            out.set(pivot.x + c * x + s * z, source.y, pivot.z - s * x + c * z);
          };
          world.onCameraTransform = (t) => {
            const angle = (sampleRig(t).cameraYaw * Math.PI) / 180;
            rotate(camera.position, base, angle);
            rotate(world.rig.lookAt, aim, angle);
            camera.lookAt(world.rig.lookAt);
            camera.updateMatrixWorld(true);
          };
          frost.invalidate();
          renderAt(lastTime);
          await frost.waitForGpu();
        });
        window.__frost = {
          renderAt,
          schedule,
          duration: DURATION,
          poseAt: (t) => frost.poseAt(t),
          get world() {
            return frost.world;
          },
          get shape() {
            return frost.shape;
          },
          waitForGpu: () => frost.waitForGpu(),
        };
        window.__frostInstance = {
          dispose() {
            if (disposed) return;
            disposed = true;
            canvasResize.disconnect();
            timeline.kill();
            window.removeEventListener("hf-seek", seek);
            frost.dispose();
            canvas.remove();
          },
          applyVariables() {
            return "rebuild";
          },
        };
        // Studio can resize the drawing buffer after initialization; resizing clears its pixels.
        // Redraw even when the paused playhead has not changed.
        let canvasSize = canvas.width + "x" + canvas.height;
        const canvasResize = new MutationObserver(() => {
          const size = canvas.width + "x" + canvas.height;
          if (size === canvasSize) return;
          canvasSize = size;
          if (!disposed) frost.redraw();
        });
        canvasResize.observe(canvas, { attributes: true, attributeFilter: ["width", "height"] });
        renderAt(0);
      })();
    </script>
  </body>
</html>
registry-item.json
{
  "$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
  "name": "frost-sequence-camera-orbit",
  "type": "hyperframes:block",
  "title": "Frost Sequence Camera Orbit",
  "description": "An orbiting camera follows an ice logo as it breaks apart, reforms into two text moments, and fades.",
  "tags": [
    "3d-motion",
    "ice",
    "frost",
    "camera-orbit",
    "logo",
    "particles",
    "webgpu",
    "two-headlines"
  ],
  "dimensions": {
    "width": 1920,
    "height": 1080
  },
  "duration": 22.5,
  "files": [
    {
      "path": "frost-sequence-camera-orbit.html",
      "target": "compositions/frost-sequence-camera-orbit/frost-sequence-camera-orbit.html",
      "type": "hyperframes:composition"
    },
    {
      "path": "assets/example-logo.svg",
      "target": "compositions/frost-sequence-camera-orbit/assets/example-logo.svg",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/fonts/Geist-Bold.ttf",
      "target": "compositions/frost-sequence-camera-orbit/assets/fonts/Geist-Bold.ttf",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/fonts/Geist-OFL.txt",
      "target": "compositions/frost-sequence-camera-orbit/assets/fonts/Geist-OFL.txt",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/fonts/Geist-Regular.ttf",
      "target": "compositions/frost-sequence-camera-orbit/assets/fonts/Geist-Regular.ttf",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/fonts/Geist-SemiBold.ttf",
      "target": "compositions/frost-sequence-camera-orbit/assets/fonts/Geist-SemiBold.ttf",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/frost.js",
      "target": "compositions/frost-sequence-camera-orbit/assets/frost.js",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/Three-LICENSE.txt",
      "target": "compositions/frost-sequence-camera-orbit/assets/Three-LICENSE.txt",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/ThreeMeshBVH-LICENSE.txt",
      "target": "compositions/frost-sequence-camera-orbit/assets/ThreeMeshBVH-LICENSE.txt",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/OpentypeJS-LICENSE.txt",
      "target": "compositions/frost-sequence-camera-orbit/assets/OpentypeJS-LICENSE.txt",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/Clipper-LICENSE.txt",
      "target": "compositions/frost-sequence-camera-orbit/assets/Clipper-LICENSE.txt",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/gsap-3.14.2.min.js",
      "target": "compositions/frost-sequence-camera-orbit/assets/gsap-3.14.2.min.js",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/GSAP-NOTICE.txt",
      "target": "compositions/frost-sequence-camera-orbit/assets/GSAP-NOTICE.txt",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/logo.svg",
      "target": "compositions/frost-sequence-camera-orbit/assets/logo.svg",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/shards-atlas.png",
      "target": "compositions/frost-sequence-camera-orbit/assets/shards-atlas.png",
      "type": "hyperframes:asset",
      "url": "https://static.heygen.ai/hyperframes-oss/registry-assets/122961c82d240e47.png"
    },
    {
      "path": "assets/test-mark.svg",
      "target": "compositions/frost-sequence-camera-orbit/assets/test-mark.svg",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/textures/bluenoise64.png",
      "target": "compositions/frost-sequence-camera-orbit/assets/textures/bluenoise64.png",
      "type": "hyperframes:asset"
    },
    {
      "path": "assets/textures/ice-inclusions-generated.png",
      "target": "compositions/frost-sequence-camera-orbit/assets/textures/ice-inclusions-generated.png",
      "type": "hyperframes:asset",
      "url": "https://static.heygen.ai/hyperframes-oss/registry-assets/4bea3a8ab90c9527.png"
    },
    {
      "path": "SKILL.md",
      "target": "compositions/frost-sequence-camera-orbit/SKILL.md",
      "type": "hyperframes:asset"
    }
  ],
  "preview": {
    "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/frost-sequence-camera-orbit.mp4",
    "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/frost-sequence-camera-orbit.png"
  }
}
SKILL.md
---
name: frost-sequence-camera-orbit
description: An orbiting camera follows an ice logo as it breaks apart, reforms into two text moments, and fades. HyperFrames block, 1920×1080, 22.5s, 216 variables.
---

# Frost Sequence Camera Orbit

A complete 22.5-second ice sequence: logo, first text, second text, breakup, and fade. The camera orbits the fixed objects with slower front-facing passes for reading. The same particle field carries each transition. Customize the two text moments and SVG logo; the source includes editable camera and assembly timing.

Composition id: `frost-sequence-rig`. Duration 22.5 s at 30 fps, 1920×1080.

## Files

- `frost-sequence-camera-orbit.html` (131 KB)
- `assets/example-logo.svg` (1 KB)
- `assets/fonts/Geist-Bold.ttf` (65 KB)
- `assets/fonts/Geist-OFL.txt` (4 KB)
- `assets/fonts/Geist-Regular.ttf` (65 KB)
- `assets/fonts/Geist-SemiBold.ttf` (65 KB)
- `assets/frost.js` (1.5 MB)
- `assets/Three-LICENSE.txt` (1 KB)
- `assets/ThreeMeshBVH-LICENSE.txt` (1 KB)
- `assets/OpentypeJS-LICENSE.txt` (1 KB)
- `assets/Clipper-LICENSE.txt` (3 KB)
- `assets/gsap-3.14.2.min.js` (128 KB)
- `assets/GSAP-NOTICE.txt` (1 KB)
- `assets/logo.svg` (1 KB)
- `assets/shards-atlas.png` (fetched from the CDN at install)
- `assets/test-mark.svg` (1 KB)
- `assets/textures/bluenoise64.png` (12 KB)
- `assets/textures/ice-inclusions-generated.png` (fetched from the CDN at install)

## Install

Install with `npx hyperframes add frost-sequence-camera-orbit`; by default the files above land under `compositions/frost-sequence-camera-orbit/`. Then mount the block from the host `index.html`:

```html
<div
  data-composition-id="frost-sequence-rig"
  data-composition-src="compositions/frost-sequence-camera-orbit/frost-sequence-camera-orbit.html"
  data-start="0"
  data-duration="22.5"
  data-track-index="1"
  data-width="1920"
  data-height="1080"
></div>
```

Render with custom values by targeting the composition file directly:

```sh
npx --yes [email protected] render 'compositions/frost-sequence-camera-orbit/frost-sequence-camera-orbit.html' --variables '{"headline1":"Hard to|break.","headline2":"Easy to|remember."}'
```

## Variables

Read at runtime via `window.__hyperframes.getVariables()`; declared on the composition root as `data-composition-variables` (single-quoted attribute, plain JSON).

| id                          | type    | default             | label / range                                                                                 |
| --------------------------- | ------- | ------------------- | --------------------------------------------------------------------------------------------- | ------------- | -------------------------- | ------- | ------- |
| `headline1`                 | string  | `"Hard to           | break."`                                                                                      | First text (  | = line break) max 60 chars |
| `headline2`                 | string  | `"Easy to           | remember."`                                                                                   | Second text ( | = line break) max 60 chars |
| `logoUrl`                   | string  | `"assets/logo.svg"` | Logo SVG asset path (upload in Assets, then paste path) max 200 chars                         |
| `assemblyMode`              | enum    | `"hybrid"`          | Assembly response (physical                                                                   | directed      | hybrid)                    |
| `materialBaseColor`         | color   | `"#ffffff"`         | Base color                                                                                    |
| `baseRoughness`             | number  | `0.31`              | Base roughness 0–0.5 step 0.005                                                               |
| `materialTransmission`      | boolean | `true`              | Transmission                                                                                  |
| `materialBacklight`         | number  | `0.27`              | Backlight through ice 0–2 step 0.01                                                           |
| `ior`                       | number  | `1.675`             | Index of refraction 1–2 step 0.005                                                            |
| `thicknessScale`            | number  | `2.04`              | Thickness scale 0.1–3 step 0.01                                                               |
| `materialAbsorption`        | boolean | `true`              | Absorption / tint                                                                             |
| `attenuationColor`          | color   | `"#d5f4ff"`         | Attenuation colour (white = no absorption, clear glass)                                       |
| `attenuationDistance`       | number  | `6.54`              | Attenuation distance 0.05–12 step 0.01                                                        |
| `materialDispersion`        | boolean | `false`             | Dispersion                                                                                    |
| `dispersion`                | number  | `0.12`              | Dispersion (0 = off; on/off reloads) 0–0.3 step 0.005                                         |
| `materialReflections`       | boolean | `true`              | Reflections                                                                                   |
| `envIntensity`              | number  | `2.04`              | Environment intensity 0–3 step 0.01                                                           |
| `specularIntensity`         | number  | `1.44`              | Specular intensity 0–2 step 0.01                                                              |
| `materialFrost`             | boolean | `true`              | Frost                                                                                         |
| `materialInteriorFrost`     | number  | `0`                 | Uniform frost 0–1 step 0.01                                                                   |
| `frostScale`                | number  | `0.7`               | Scale 0.2–6 step 0.05                                                                         |
| `frostThreshold`            | number  | `0.62`              | Threshold (1 = no frost, clear glass) 0–1 step 0.01                                           |
| `frostSoftness`             | number  | `0.24`              | Softness 0.01–1 step 0.01                                                                     |
| `frostRoughness`            | number  | `0.73`              | Roughness 0–1 step 0.01                                                                       |
| `frostDiffuse`              | number  | `0.11`              | Diffuse (how much light frosted areas catch) 0–1 step 0.01                                    |
| `materialSurfaceBumps`      | boolean | `false`             | Object surface bumps / crack notches                                                          |
| `materialCrystals`          | boolean | `true`              | Crystal bumps                                                                                 |
| `crystalBump`               | number  | `0.01`              | Crystal bump 0–1 step 0.01                                                                    |
| `crystalScale`              | number  | `4`                 | Crystal scale 4–80 step 1                                                                     |
| `materialGrain`             | boolean | `true`              | Grain bumps                                                                                   |
| `materialGrainAmount`       | number  | `0.47`              | Grain strength 0–2 step 0.01                                                                  |
| `materialGrainScale`        | number  | `150`               | Grain scale 1–150 step 1                                                                      |
| `materialCutNormals`        | boolean | `true`              | Fracture normals                                                                              |
| `materialMicro`             | boolean | `true`              | Micro bumps                                                                                   |
| `microBump`                 | number  | `0.445`             | Micro bump 0–1 step 0.005                                                                     |
| `microScale`                | number  | `10`                | Micro scale 10–200 step 1                                                                     |
| `microCoverage`             | number  | `0.25`              | Micro coverage 0–1 step 0.01                                                                  |
| `bumpMaskScale`             | number  | `1.45`              | Mask scale 0.1–6 step 0.05                                                                    |
| `materialRipples`           | boolean | `true`              | Ripples                                                                                       |
| `rippleBump`                | number  | `0.22`              | Ripple bump 0–1 step 0.01                                                                     |
| `rippleScale`               | number  | `23`                | Ripple scale 2–30 step 0.5                                                                    |
| `materialSmudges`           | boolean | `true`              | Smudges                                                                                       |
| `smudgeAmount`              | number  | `0.78`              | Amount 0–1 step 0.01                                                                          |
| `smudgeCoverage`            | number  | `0.58`              | Coverage 0–1 step 0.01                                                                        |
| `smudgeMaskScale`           | number  | `1.85`              | Mask scale 0.1–6 step 0.05                                                                    |
| `smudgeAnisotropy`          | number  | `16.5`              | Anisotropy 1–20 step 0.5                                                                      |
| `smudgeRoughness`           | number  | `0.75`              | Roughness 0–1 step 0.01                                                                       |
| `smudgeWhiteness`           | number  | `0.035`             | Whiteness 0–0.5 step 0.005                                                                    |
| `smudgeScale`               | number  | `1.3`               | Scale 0.5–10 step 0.1                                                                         |
| `materialCracks`            | boolean | `true`              | Cracks                                                                                        |
| `crackLargeScale`           | number  | `2.45`              | Large scale 0.3–8 step 0.05                                                                   |
| `crackWarp`                 | number  | `0.22`              | Warp 0–1.5 step 0.01                                                                          |
| `crackCoverage`             | number  | `0.19`              | Coverage 0–1 step 0.01                                                                        |
| `crackRegionScale`          | number  | `1.8`               | Region scale 0.1–4 step 0.05                                                                  |
| `crackRegionCoverage`       | number  | `0.6`               | Region coverage 0–1 step 0.01                                                                 |
| `veinScale`                 | number  | `8`                 | Vein scale 1–20 step 0.25                                                                     |
| `veinContrast`              | number  | `0.55`              | Vein contrast 0–1 step 0.01                                                                   |
| `crackWidth`                | number  | `0.0025`            | Width 0.0005–0.02 step 0.0005                                                                 |
| `crackBrightness`           | number  | `0.65`              | Brightness 0–3 step 0.01                                                                      |
| `crackDarkness`             | number  | `0.69`              | Darkness 0–1 step 0.01                                                                        |
| `crackRefraction`           | number  | `0.076`             | Refraction 0–0.1 step 0.001                                                                   |
| `crackSurfaceStrength`      | number  | `0.16`              | Surface strength 0–1 step 0.01                                                                |
| `fineScale`                 | number  | `18.4`              | Fine scale 2–20 step 0.1                                                                      |
| `fineAmount`                | number  | `0.52`              | Fine amount 0–1 step 0.01                                                                     |
| `fineCoverage`              | number  | `1`                 | Fine coverage 0–1 step 0.01                                                                   |
| `materialScatter`           | boolean | `true`              | Internal scattering                                                                           |
| `materialInclusionScale`    | number  | `0.05`              | Inclusion scale 0.05–3 step 0.01                                                              |
| `materialInclusionAmount`   | number  | `2`                 | Photographic inclusions 0–2 step 0.01                                                         |
| `interiorScatter`           | number  | `0.09`              | Interior scatter 0–1 step 0.01                                                                |
| `materialClearcoat`         | boolean | `true`              | Clearcoat                                                                                     |
| `clearcoat`                 | number  | `0`                 | Clearcoat 0–1 step 0.01                                                                       |
| `clearcoatRoughness`        | number  | `0`                 | Clearcoat roughness 0–1 step 0.005                                                            |
| `materialShardNormals`      | boolean | `true`              | Shard normals                                                                                 |
| `spriteNormal`              | number  | `0.15`              | Sprite normal strength 0–2.5 step 0.05                                                        |
| `materialShardFrost`        | boolean | `true`              | Shard frost                                                                                   |
| `materialShardTransmission` | boolean | `true`              | Shard transparency                                                                            |
| `spriteSeeThrough`          | number  | `1`                 | See-through (0 = off; on/off reloads) 0–1 step 0.01                                           |
| `materialShardReflections`  | boolean | `true`              | Shard reflections                                                                             |
| `minPixelSize`              | number  | `0.25`              | Minimum pixel size 0–4 step 0.05                                                              |
| `grainSizeMultiplier`       | number  | `1.05`              | Grain size multiplier 0.2–4 step 0.05                                                         |
| `spriteSize`                | number  | `1.9`               | Sprite size 0.3–4 step 0.05                                                                   |
| `spriteTilt`                | number  | `12`                | Sprite tilt 0–70 step 1                                                                       |
| `spriteAlphaCut`            | number  | `0.6`               | Alpha cut 0.05–0.6 step 0.01                                                                  |
| `fontWeight`                | enum    | `"600"`             | Type · Weight (Geist) (400                                                                    | 600           | 700)                       |
| `letterSpacing`             | number  | `0.01`              | Type · Letter spacing -0.1–0.4 step 0.005                                                     |
| `shardAmount`               | number  | `0.44`              | Shards · Visible fraction of the broken volume (Powder amount) 0.02–1 step 0.01               |
| `strayDust`                 | number  | `24`                | Shards · Ambient dust motes around the object (the experiment had 40) 0–200 step 1            |
| `sliceRadius`               | number  | `0.6`               | Break · First (diagonal) slice radius 0.05–1.5 step 0.01                                      |
| `sliceStrength`             | number  | `21.5`              | Break · First slice strength 1–40 step 0.5                                                    |
| `finalEjectBoost`           | number  | `2.5`               | Break · Last break: eject speed and speed cap multiplier 1–6 step 0.1                         |
| `shatterRadius`             | number  | `0.45`              | Break · Follow-up slice radius (sets their spacing too) 0.1–1.5 step 0.01                     |
| `shatterStrength`           | number  | `32.5`              | Break · Follow-up slice strength 1–40 step 0.5                                                |
| `cutThreshold`              | number  | `0.3`               | Tune break · Cut threshold (surface gone above this erosion) 0.3–0.98 step 0.01               |
| `cutSoftness`               | number  | `0.19`              | Tune break · Cut softness 0.005–0.3 step 0.005                                                |
| `edgeWidth`                 | number  | `0.19`              | Tune break · Crumbly edge band width 0.05–0.8 step 0.01                                       |
| `edgeInset`                 | number  | `0`                 | Tune break · Edge inset 0–0.3 step 0.005                                                      |
| `brushSoftness`             | number  | `0.15`              | Tune break · Brush softness (edge falloff of a slice) 0.02–1 step 0.01                        |
| `brushNoise`                | number  | `0.4`               | Tune break · Brush noise (ragged boundary) 0–1 step 0.01                                      |
| `crumbleRate`               | number  | `4.3`               | Tune break · Crumble rate along cracks (high = the whole shape goes at once) 0–6 step 0.05    |
| `crumbleCrackBias`          | number  | `5.1`               | Tune break · Crumble crack bias 0–6 step 0.05                                                 |
| `crumbleDuration`           | number  | `0.35`              | Tune break · Crumble duration after a stroke 0–2 step 0.01                                    |
| `ejectSpeed`                | number  | `0.91`              | Flight · Eject speed 0–4 step 0.01                                                            |
| `ejectSpread`               | number  | `0.48`              | Flight · Eject spread along the normal 0–3 step 0.01                                          |
| `ejectTurbulence`           | number  | `4`                 | Flight · Eject turbulence 0–4 step 0.01                                                       |
| `drag`                      | number  | `0`                 | Flight · Drag (speed decays by this per second) 0–8 step 0.01                                 |
| `gravity`                   | number  | `0`                 | Flight · Gravity (0 = shards never fall) 0–2 step 0.005                                       |
| `turbulence`                | number  | `4`                 | Flight · Turbulence strength (curl noise) 0–4 step 0.01                                       |
| `turbulenceScale`           | number  | `2.65`              | Flight · Turbulence scale 0.2–8 step 0.05                                                     |
| `turbulenceDecay`           | number  | `3.65`              | Flight · Turbulence decay with age (low = keeps swirling) 0.05–4 step 0.01                    |
| `clumpCohesion`             | number  | `0.9`               | Flight · Clump cohesion (shards orbit a leader; 0 = none) 0–10 step 0.05                      |
| `followObject`              | number  | `30`                | Flight · Shards follow the object motion for (s; 30 = whole flight) 0–30 step 0.5             |
| `settleTime`                | number  | `2.2`               | Flight · Settle time (velocity is killed after this; 12 = never) 0.3–12 step 0.05             |
| `settledDrift`              | number  | `1`                 | Flight · Organic drift once settled (curl noise) 0–1 step 0.005                               |
| `maxSpeed`                  | number  | `26.3`              | Flight · Speed cap 1–40 step 0.1                                                              |
| `repelStrength`             | number  | `30`                | Flight · Push out of the solid shape while it breaks 0–30 step 0.1                            |
| `repelRange`                | number  | `0.97`              | Flight · Push range outside the surface 0.02–2 step 0.01                                      |
| `repelRadial`               | number  | `17.5`              | Flight · Push away from the shape centre (clears pockets and the hole) 0–60 step 0.5          |
| `repelRadialRange`          | number  | `3.3`               | Flight · Radial push range (object radii) 1–4 step 0.05                                       |
| `tumble`                    | number  | `1.05`              | Flight · Tumble rate 0–12 step 0.05                                                           |
| `returnGroupStagger`        | number  | `1.35`              | Assembly · Regional delay (seconds) 0–1.5 step 0.05                                           |
| `returnGroupScale`          | number  | `2.55`              | Assembly · Region / noise size 0.1–3 step 0.05                                                |
| `returnGroupSeed`           | number  | `60765`             | Return · Group timing seed 0–65535 step 1                                                     |
| `formSpread`                | number  | `0`                 | Return · Wave spread (nearest shards leave first, seconds) 0–4 step 0.05                      |
| `waveReach`                 | number  | `10`                | Return · Distance over which the wave spreads 0.2–10 step 0.1                                 |
| `formJitter`                | number  | `2.8`               | Return · Per-shard stagger (random delay up to this) 0–3 step 0.05                            |
| `formFill`                  | number  | `3`                 | Return · Fill-in rate for voxels no shard returns to 0.05–3 step 0.05                         |
| `returnSpring`              | number  | `29.9`              | Return · Spring stiffness 0.5–40 step 0.1                                                     |
| `returnDamping`             | number  | `1.34`              | Return · Spring damping 0.2–2 step 0.01                                                       |
| `returnRamp`                | number  | `0.45`              | Return · Spring ramp-in (seconds until it pulls at full strength) 0–3 step 0.05               |
| `returnMaxSpeed`            | number  | `60`                | Return · Speed cap on the way home 1–60 step 0.5                                              |
| `alignToSurface`            | number  | `1`                 | Return · Shards turn to lie on the surface (0 = keep tumbling) 0–1 step 0.01                  |
| `alignCurve`                | number  | `3.75`              | Return · Alignment curve over the flight home (1 linear, higher = later) 0.2–4 step 0.05      |
| `healRate`                  | number  | `3`                 | Return · Neighbour heal rate 0.02–3 step 0.01                                                 |
| `cellRestore`               | number  | `60`                | Return · Cell restore rate 0–60 step 0.5                                                      |
| `landedFade`                | number  | `1.65`              | Return · Landed shard fade 0–2 step 0.01                                                      |
| `refrostTime`               | number  | `10.6`              | Return · Refrost time 0.2–12 step 0.1                                                         |
| `keyColor`                  | color   | `"#f0f7ff"`         | Light · Key colour                                                                            |
| `keyIntensity`              | number  | `5.05`              | Light · Key intensity 0–12 step 0.05                                                          |
| `keyElevation`              | number  | `42`                | Light · Key elevation 10–89 step 0.5                                                          |
| `keyAzimuth`                | number  | `-38`               | Light · Key azimuth -90–90 step 0.5                                                           |
| `keySize`                   | number  | `1.25`              | Light · Key size (softbox) 0.2–3 step 0.01                                                    |
| `fill`                      | number  | `0.18`              | Light · Fill intensity (hemisphere, diffuse only: barely shows on clear ice) 0–1.5 step 0.005 |
| `fillColor`                 | color   | `"#cadde9"`         | Light · Fill sky colour                                                                       |
| `fillGroundColor`           | color   | `"#172635"`         | Light · Fill ground colour                                                                    |
| `rim`                       | number  | `2.5`               | Light · Rim spot intensity 0–5 step 0.01                                                      |
| `rimColor`                  | color   | `"#d9f1ff"`         | Light · Rim colour                                                                            |
| `rimElevation`              | number  | `20`                | Light · Rim elevation (0 = straight behind) 0–89 step 0.5                                     |
| `swayAmplitude`             | number  | `0`                 | Light · Key sway amplitude 0–0.15 step 0.001                                                  |
| `swayPeriod`                | number  | `35`                | Light · Key sway period 2–40 step 0.5                                                         |
| `envSoftbox`                | number  | `1.1`               | Light · Environment softbox (the main light on glass) 0–3 step 0.01                           |
| `envRim`                    | number  | `1.4`               | Light · Environment rim strip 0–3 step 0.01                                                   |
| `envFill`                   | number  | `0.22`              | Light · Environment front fill 0–1 step 0.005                                                 |
| `backdropTop`               | color   | `"#050505"`         | Backdrop · Top colour                                                                         |
| `backdropMid`               | color   | `"#050505"`         | Backdrop · Mid colour                                                                         |
| `backdropBottom`            | color   | `"#050505"`         | Backdrop · Bottom colour                                                                      |
| `backdropCenterX`           | number  | `0.73`              | Backdrop · Bloom centre X 0–1 step 0.005                                                      |
| `backdropCenterY`           | number  | `0.22`              | Backdrop · Bloom centre Y 0–2 step 0.005                                                      |
| `backdropRadius`            | number  | `0.8`               | Backdrop · Bloom radius 0.1–2 step 0.005                                                      |
| `backdropFalloff`           | number  | `0.85`              | Backdrop · Bloom falloff 0.5–6 step 0.01                                                      |
| `backdropNoise`             | number  | `0`                 | Backdrop · Dither noise 0–3 step 0.05                                                         |
| `bloomThreshold`            | number  | `3`                 | Post · Bloom threshold 0–3 step 0.01                                                          |
| `bloomIntensity`            | number  | `0.04`              | Post · Bloom intensity 0–1.5 step 0.005                                                       |
| `bloomRadius`               | number  | `1`                 | Post · Bloom radius 0–1 step 0.005                                                            |
| `monochrome`                | number  | `0.04`              | Post · Monochrome 0–1 step 0.01                                                               |
| `tonemap`                   | enum    | `"aces"`            | Post · Tonemap (agx                                                                           | aces          | neutral                    | linear) |
| `exposure`                  | number  | `1`                 | Post · Exposure 0.1–4 step 0.01                                                               |
| `contrast`                  | number  | `1.03`              | Post · Contrast 0.6–1.6 step 0.005                                                            |
| `blackLift`                 | number  | `0`                 | Post · Black lift 0–0.1 step 0.001                                                            |
| `vignetteStrength`          | number  | `0.14`              | Post · Vignette strength 0–1 step 0.005                                                       |
| `vignetteSoftness`          | number  | `1.2`               | Post · Vignette softness 0.1–1.5 step 0.005                                                   |
| `vignetteRadius`            | number  | `1.2`               | Post · Vignette radius 0.2–1.6 step 0.005                                                     |
| `grainStrength`             | number  | `0`                 | Post · Film grain 0–0.15 step 0.001                                                           |
| `quality`                   | enum    | `"full"`            | Performance · Quality profile (full                                                           | lite)         |
| `upscaler`                  | enum    | `"fsr1"`            | Performance · Upscaler (fsr1                                                                  | taau          | bilinear                   | native) |
| `renderScale`               | number  | `1`                 | Performance · Scene resolution scale (upscaled to 1080p) 0.35–1 step 0.05                     |
| `shapeResolution`           | enum    | `"256"`             | Shape voxel resolution (128                                                                   | 256           | 384)                       |
| `erosionResolution`         | enum    | `"96"`              | Performance · Erosion field resolution (voxels per axis) (64                                  | 96            | 128                        | 192)    |
| `particleCount`             | enum    | `"100k"`            | Performance · Powder particles (100k                                                          | 250k          | 500k                       | 1M)     |
| `logoMeshDetail`            | number  | `2`                 | Shape · Logo mesh detail (1-4; higher = finer surface) 1–4 step 1                             |
| `deformStrength`            | number  | `0`                 | Geometry · Ice deformation strength (0 = original) 0–0.08 step 0.001                          |
| `deformScale`               | number  | `0.41`              | Geometry · Noise feature size (larger = broader) 0.001–0.5 step 0.001                         |
| `deformSeed`                | number  | `7`                 | Geometry · Deformation seed 0–65535 step 1                                                    |
| `rimAzimuth`                | number  | `-146`              | Light · Rim azimuth -180–180 step 1                                                           |
| `rimAngle`                  | number  | `26`                | Light · Rim beam angle 10–89 step 1                                                           |
| `rimSize`                   | number  | `0.45`              | Light · Rim reflection size 0.1–3 step 0.05                                                   |
| `fillReflectionStrength`    | number  | `0.3`               | Light · Fill reflection strength 0–2 step 0.01                                                |
| `accentCoolIntensity`       | number  | `3.71`              | Studio Cool accent · intensity 0–4 step 0.01                                                  |
| `accentCoolReflection`      | number  | `2.31`              | Studio Cool accent · reflection 0–3 step 0.01                                                 |
| `accentCoolElevation`       | number  | `63`                | Studio Cool accent · elevation -85–85 step 1                                                  |
| `accentCoolAzimuth`         | number  | `22`                | Studio Cool accent · azimuth -180–180 step 1                                                  |
| `accentCoolSize`            | number  | `2.65`              | Studio Cool accent · size 0.1–3 step 0.05                                                     |
| `accentCoolColor`           | color   | `"#ff5900"`         | Studio Cool accent · color                                                                    |
| `accentWarmIntensity`       | number  | `2.99`              | Studio Warm accent · intensity 0–4 step 0.01                                                  |
| `accentWarmReflection`      | number  | `0.34`              | Studio Warm accent · reflection 0–3 step 0.01                                                 |
| `accentWarmElevation`       | number  | `30`                | Studio Warm accent · elevation -85–85 step 1                                                  |
| `accentWarmAzimuth`         | number  | `-30`               | Studio Warm accent · azimuth -180–180 step 1                                                  |
| `accentWarmSize`            | number  | `1.2`               | Studio Warm accent · size 0.1–3 step 0.05                                                     |
| `accentWarmColor`           | color   | `"#75aaff"`         | Studio Warm accent · color                                                                    |
| `returnNoiseAmount`         | enum    | `"2"`               | Assembly · Regional pattern (2                                                                | 1             | 0)                         |
| `assemblyFrontDuration`     | number  | `3.2`               | Assembly · Growth duration (seconds) 0–6 step 0.05                                            |
| `assemblyOriginX`           | number  | `-0.65`             | Assembly · Growth start X -1–1 step 0.01                                                      |
| `assemblyOriginY`           | number  | `0.55`              | Assembly · Growth start Y -1–1 step 0.01                                                      |
| `assemblyAngle`             | number  | `-35`               | Assembly · Seam direction (degrees) -180–180 step 1                                           |
| `assemblySpread`            | number  | `0.65`              | Assembly · Outward spread vs seam travel 0–1 step 0.01                                        |
| `assemblyFrontNoise`        | number  | `0.35`              | Assembly · Growth edge irregularity 0–1 step 0.01                                             |
| `assemblySpeedVariation`    | number  | `0.65`              | Assembly · Return speed variation 0–1 step 0.01                                               |
| `assemblyBend`              | number  | `1.2`               | Assembly · Approach path bend 0–3 step 0.05                                                   |
| `assemblySwirl`             | number  | `1.1`               | Assembly · Approach twist 0–3 step 0.05                                                       |
| `assemblyLandingVariation`  | number  | `0.8`               | Assembly · Landing transition variation 0–1 step 0.01                                         |
| `rendererProfile`           | enum    | `"studio"`          | Renderer experiment (reload required) (original                                               | lookup        | mesh                       | studio  | matcap) |
| `textWidth`                 | number  | `6.5`               | Type · Headline block width (the mark is 2.6 wide) 1.2–9 step 0.05                            |
| `textLineHeight`            | number  | `0.95`              | Type · Line height 0.7–1.4 step 0.01                                                          |
| `textDepth`                 | number  | `0.16`              | Type · Extrusion depth (fraction of the font size) 0.05–0.8 step 0.01                         |
| `textBevel`                 | number  | `0.04`              | Type · Bevel (fraction of the font size) 0–0.12 step 0.005                                    |
| `textCorner`                | number  | `0.008`             | Type · Corner rounding (fraction of the font size) 0–0.06 step 0.002                          |
| `textMeshDetail`            | number  | `4`                 | Type · Text mesh detail (1-4; higher = smoother deformation) 1–4 step 1                       |

## Runtime contract

- One paused GSAP timeline registered as `window.__timelines["frost-sequence-rig"]`.
- Re-syncs on the `hf-seek` CustomEvent; every frame is a closed-form function of time (seeded PRNG only, no rAF loops, no Date.now).
- Renderer: WebGPU, GSAP, Matcap.
- External runtime dependencies: none (served locally).

## Editing rules (from the source project)

1. Keep `data-composition-variables` a single-quoted attribute with plain `"` JSON. Never save it through Studio's Design panel.
2. Do not put `<canvas>` in static markup; create it at runtime.
3. Keep every visual state a function of t; seek-safety is what makes the block renderable.
source/build.mjs
// Bundle src/frost.ts (+ [email protected] WebGPU/TSL, three-mesh-bvh) into one
// classic IIFE at assets/frost.js exposing `window.Frost`. Run `npm install && npm run build` after editing src/.
import "./tools/compile-preflight.mjs";
if (process.exitCode) process.exit(process.exitCode);
// CPU-only shader generation checks the portable interface budget, including motion vectors.
await import("./tools/run-shard-compile.mjs");
if (process.exitCode) process.exit(process.exitCode);
import { build } from "esbuild";
import { createRequire } from "node:module";
import { statSync } from "node:fs";

const require = createRequire(import.meta.url);
// The addons (SVGLoader, BufferGeometryUtils, GLTFLoader, three-mesh-bvh) import the classic `three` build; the
// app uses `three/webgpu`, which re-exports the core. Point the bare specifier at the WebGPU build so exactly one
// Three instance exists in the bundle (same alias as the experiment's vite.config.ts).
const threeWebgpu = require.resolve("three/webgpu");
const threeAlias = {
  name: "three-webgpu-alias",
  setup(b) {
    b.onResolve({ filter: /^three$/ }, () => ({ path: threeWebgpu }));
  },
};

await build({
  entryPoints: ["src/frost.ts"],
  bundle: true,
  format: "iife",
  globalName: "Frost",
  minify: true,
  sourcemap: false,
  target: ["es2022"],
  outfile: "assets/frost.js",
  plugins: [threeAlias],
  legalComments: "linked",
  logLevel: "info",
  banner: {
    js: "/* Frost: [email protected] (MIT) + [email protected] (MIT). Generated by build.mjs; edit src/ and rebuild. */",
  },
});
console.log(`assets/frost.js ${(statSync("assets/frost.js").size / 1024).toFixed(0)} KB`);

// Lightweight workspace uses classic WebGL only; math/geometry helpers share their source
// with the full WebGPU block without importing its renderer or simulation.
await build({
  entryPoints: ["src/motion/editor.ts"],
  bundle: true,
  format: "iife",
  minify: true,
  target: ["es2022"],
  outfile: "assets/motion.js",
  legalComments: "linked",
  logLevel: "info",
  plugins: [
    {
      name: "motion-three",
      setup(b) {
        b.onResolve({ filter: /^three\/webgpu$/ }, () => ({
          path: require.resolve("three").replace("three.cjs", "three.module.js"),
        }));
      },
    },
  ],
});
source/package.json
{
  "name": "frost-sequence-camera-orbit-source",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "node build.mjs"
  },
  "devDependencies": {
    "esbuild": "^0.24.0",
    "opentype.js": "^2.0.0",
    "three": "0.185.1",
    "three-mesh-bvh": "0.9.14",
    "typescript": "5.7"
  }
}
source/presets/approved-material.json
{
  "headline1": "Hard to|break.",
  "headline2": "Easy to|remember.",
  "textWidth": 6.5,
  "fontWeight": "600",
  "letterSpacing": 0.01,
  "textLineHeight": 0.95,
  "textDepth": 0.16,
  "textBevel": 0.04,
  "textCorner": 0.008,
  "textMeshDetail": 4,
  "logoBreakAt": 1.5,
  "formHeadline1At": 3.8,
  "headline1BreakAt": 10.375,
  "formHeadline2At": 12.675,
  "headline2BreakAt": 18.5,
  "fadeOutAt": 22,
  "turnYaw": 180,
  "turnPitch": -41,
  "approach": 7,
  "turnYaw2": 160,
  "turnPitch2": 35,
  "retreat": 3.5,
  "zoomIn": 5,
  "finalYaw": 40,
  "finalPitch": -20,
  "driftYaw": 2,
  "driftSway": 4,
  "idleYaw": 6,
  "shardAmount": 0.44,
  "strayDust": 24,
  "sliceRadius": 0.6,
  "sliceStrength": 21.5,
  "breakDuration": 2,
  "finalEjectBoost": 2.5,
  "shatterRadius": 0.45,
  "shatterStrength": 32.5,
  "cutThreshold": 0.3,
  "cutSoftness": 0.19,
  "edgeWidth": 0.19,
  "edgeInset": 0,
  "brushSoftness": 0.15,
  "brushNoise": 0.4,
  "crumbleRate": 4.3,
  "crumbleCrackBias": 5.1,
  "crumbleDuration": 0.35,
  "ejectSpeed": 0.91,
  "ejectSpread": 0.48,
  "ejectTurbulence": 4,
  "drag": 0,
  "gravity": 0,
  "turbulence": 4,
  "turbulenceScale": 2.65,
  "turbulenceDecay": 3.65,
  "clumpCohesion": 0.9,
  "followObject": 30,
  "settleTime": 2.2,
  "settledDrift": 1,
  "maxSpeed": 26.3,
  "repelStrength": 30,
  "repelRange": 0.97,
  "repelRadial": 17.5,
  "repelRadialRange": 3.3,
  "tumble": 1.05,
  "minPixelSize": 0.25,
  "grainSizeMultiplier": 1.05,
  "spriteSize": 1.9,
  "returnGroupStagger": 1.35,
  "returnGroupScale": 2.55,
  "returnGroupSeed": 60765,
  "formSpread": 0,
  "waveReach": 10,
  "formJitter": 2.8,
  "formFill": 3,
  "returnSpring": 29.9,
  "returnDamping": 1.34,
  "returnRamp": 0.45,
  "returnMaxSpeed": 60,
  "alignToSurface": 1,
  "alignCurve": 3.75,
  "healRate": 3,
  "cellRestore": 60,
  "landedFade": 1.65,
  "refrostTime": 10.6,
  "keyColor": "#f0f7ff",
  "keyIntensity": 5.05,
  "keyElevation": 42,
  "keyAzimuth": -38,
  "keySize": 1.25,
  "fill": 0.18,
  "fillColor": "#cadde9",
  "fillGroundColor": "#172635",
  "rim": 2.5,
  "rimColor": "#d9f1ff",
  "rimElevation": 20,
  "swayAmplitude": 0,
  "swayPeriod": 35,
  "envSoftbox": 1.1,
  "envRim": 1.4,
  "envFill": 0.22,
  "backdropTop": "#050505",
  "backdropMid": "#050505",
  "backdropBottom": "#050505",
  "backdropCenterX": 0.73,
  "backdropCenterY": 0.22,
  "backdropRadius": 0.8,
  "backdropFalloff": 0.85,
  "backdropNoise": 0,
  "ior": 1.675,
  "dispersion": 0.12,
  "thicknessScale": 2.04,
  "attenuationDistance": 6.54,
  "attenuationColor": "#d5f4ff",
  "baseRoughness": 0.31,
  "clearcoat": 0,
  "clearcoatRoughness": 0,
  "envIntensity": 2.04,
  "specularIntensity": 1.44,
  "interiorScatter": 0.09,
  "crumbleGlow": 0.16,
  "edgeWhiteness": 0.09,
  "frostScale": 0.7,
  "frostThreshold": 0.62,
  "frostSoftness": 0.24,
  "frostRoughness": 0.73,
  "frostDiffuse": 0.11,
  "crystalBump": 0.01,
  "crystalScale": 4,
  "crackLargeScale": 2.45,
  "crackWarp": 0.22,
  "crackWarpScale": 2.2,
  "crackCoverage": 0.19,
  "crackRegionScale": 1.8,
  "crackRegionCoverage": 0.6,
  "veinScale": 8,
  "veinContrast": 0.55,
  "crackWidth": 0.0025,
  "crackBrightness": 0.65,
  "crackDarkness": 0.69,
  "crackRefraction": 0.076,
  "crackSurfaceStrength": 0.16,
  "fineScale": 18.4,
  "fineAmount": 0.52,
  "fineCoverage": 1,
  "smudgeAmount": 0.78,
  "smudgeCoverage": 0.58,
  "smudgeMaskScale": 1.85,
  "smudgeAnisotropy": 16.5,
  "smudgeRoughness": 0.75,
  "smudgeWhiteness": 0.035,
  "smudgeScale": 1.3,
  "microBump": 0.445,
  "microScale": 10,
  "microCoverage": 0.25,
  "rippleBump": 0.22,
  "rippleScale": 23,
  "bumpMaskScale": 1.45,
  "baseTone": "#f3fbff",
  "wrap": 0.47,
  "fragTranslucency": 0.96,
  "fragThroughTint": "#dff4ff",
  "fragFresnelPower": 5,
  "fragRoughness": 0.09,
  "fragClearcoat": 0,
  "fragSpecular": 1,
  "sparkle": 0.4,
  "sparkleFraction": 0.18,
  "spriteTilt": 12,
  "spriteNormal": 0.15,
  "spriteFrost": 0.23,
  "spriteFrostBoost": 0.15,
  "spriteFrostRoughness": 0.42,
  "spriteEdgeLight": 0.85,
  "spriteAlphaCut": 0.6,
  "spriteSeeThrough": 1,
  "bloomThreshold": 3,
  "bloomIntensity": 0.04,
  "bloomRadius": 1,
  "monochrome": 0.04,
  "tonemap": "aces",
  "exposure": 1,
  "contrast": 1.03,
  "blackLift": 0,
  "vignetteStrength": 0.14,
  "vignetteSoftness": 1.2,
  "vignetteRadius": 1.2,
  "grainStrength": 0,
  "quality": "full",
  "upscaler": "fsr1",
  "renderScale": 1,
  "erosionResolution": "96",
  "particleCount": "100k",
  "logoUrl": "assets/logo.svg",
  "deformStrength": 0,
  "deformScale": 0.41,
  "deformSeed": 7,
  "logoMeshDetail": 2,
  "rimAzimuth": -146,
  "rimAngle": 26,
  "rimSize": 0.45,
  "fillReflectionStrength": 0.3,
  "accentCoolIntensity": 3.71,
  "accentCoolReflection": 2.31,
  "accentCoolElevation": 63,
  "accentCoolAzimuth": 22,
  "accentCoolSize": 2.65,
  "accentCoolColor": "#ff5900",
  "accentWarmIntensity": 2.99,
  "accentWarmReflection": 0.34,
  "accentWarmElevation": 30,
  "accentWarmAzimuth": -30,
  "accentWarmSize": 1.2,
  "accentWarmColor": "#75aaff",
  "cameraMode": "original",
  "cameraTrack": "",
  "materialBaseColor": "#ffffff",
  "materialTransmission": true,
  "materialAbsorption": true,
  "materialDispersion": false,
  "materialReflections": true,
  "materialFrost": true,
  "materialInteriorFrost": 0,
  "materialCrystals": true,
  "materialGrain": true,
  "materialGrainAmount": 0.47,
  "materialGrainScale": 150,
  "materialCutNormals": true,
  "materialMicro": true,
  "materialRipples": true,
  "materialSmudges": true,
  "materialCracks": true,
  "materialScatter": true,
  "materialClearcoat": true,
  "materialShardNormals": true,
  "materialShardFrost": true,
  "materialShardTransmission": true,
  "materialShardReflections": true,
  "materialShardSparkle": true,
  "materialShardEdges": true,
  "materialBacklight": 0.27,
  "materialInclusionAmount": 2,
  "materialInclusionScale": 0.05,
  "returnNoiseAmount": "2",
  "assemblyFrontDuration": 3.2,
  "assemblyOriginX": -0.65,
  "assemblyOriginY": 0.55,
  "assemblyAngle": -35,
  "assemblySpread": 0.65,
  "assemblyFrontNoise": 0.35,
  "assemblySpeedVariation": 0.65,
  "assemblyBend": 1.2,
  "assemblySwirl": 1.1,
  "assemblyLandingVariation": 0.8,
  "materialSurfaceBumps": false,
  "shapeResolution": "256",
  "rendererProfile": "studio"
}
source/presets/source-hero4-material.json
{
  "ice": {
    "ior": 1.445,
    "dispersion": 0.155,
    "thicknessScale": 2.41,
    "attenuationDistance": 4.47,
    "attenuationColor": "#524c4c",
    "baseRoughness": 0.5,
    "frost": {
      "scale": 0.2,
      "threshold": 1,
      "softness": 0.36,
      "roughness": 0,
      "diffuse": 0.56,
      "crystalBump": 0.37,
      "crystalScale": 67
    },
    "cracks": {
      "largeScale": 8,
      "warp": 0,
      "warpScale": 2.2,
      "coverage": 0.01,
      "regionScale": 1.35,
      "regionCoverage": 0,
      "veinScale": 20,
      "veinContrast": 0.57,
      "width": 0.0175,
      "brightness": 0.25,
      "darkness": 1,
      "refraction": 0.008,
      "surfaceStrength": 0,
      "steps": 21,
      "fineCracks": true,
      "fineScale": 10.4,
      "fineAmount": 0,
      "fineCoverage": 0.45,
      "fineNearLarge": 1
    },
    "smudges": {
      "amount": 0,
      "coverage": 0.29,
      "maskScale": 3.1,
      "anisotropy": 5.5,
      "roughness": 0.02,
      "whiteness": 0.035,
      "scale": 0.5
    },
    "bumps": {
      "microBump": 0.32,
      "microScale": 121,
      "microCoverage": 0.76,
      "rippleBump": 0.45,
      "rippleScale": 2.5,
      "maskScale": 3.6
    },
    "crumbleGlow": 0.16,
    "edgeWhiteness": 0.09,
    "clearcoat": 0,
    "clearcoatRoughness": 0.085,
    "envIntensity": 0.28,
    "specularIntensity": 0.65,
    "interiorScatter": 0.05
  },
  "performance": {
    "adaptiveResolution": true,
    "targetFps": 60,
    "minPixelRatio": 0.75,
    "sceneResolutionScale": 1,
    "dynamicSceneResolution": true,
    "minSceneResolutionScale": 0.35,
    "upscaler": "taau",
    "upscaleSharpness": 0.2,
    "nativePostEffects": true,
    "idleSkip": true,
    "adaptiveSteps": true,
    "voronoiCells": "27",
    "hazeResolution": "quarter",
    "turbulence": "fast",
    "gpuTimers": true
  },
  "lighting": {
    "key": {
      "color": "#ffffff",
      "intensity": 12,
      "elevation": 62,
      "azimuth": -10.5,
      "size": 3,
      "hoverBoost": 0.3
    },
    "fill": 1.5,
    "fillColor": "#ffffff",
    "fillGroundColor": "#07a4a6",
    "rim": 5,
    "rimColor": "#0b82ea",
    "rimElevation": 9.5,
    "shadow": {
      "intensity": 0,
      "softness": 6,
      "samples": 1,
      "mapSize": "1024",
      "bias": -0.0006
    },
    "sway": {
      "amplitude": 0.118,
      "period": 35
    },
    "backdrop": {
      "top": "#212121",
      "mid": "#171717",
      "bottom": "#030303",
      "centerX": 0.19,
      "centerY": 1.5,
      "radius": 0.78,
      "falloff": 4,
      "noise": 0
    },
    "envSoftbox": 1.22,
    "envRim": 2.43,
    "envFill": 1,
    "rimAzimuth": -180,
    "rimAngle": 81,
    "rimSize": 2.05,
    "fillReflectionStrength": 2,
    "accentCool": {
      "intensity": 4,
      "reflection": 3,
      "elevation": 25,
      "azimuth": -65,
      "size": 2.85,
      "color": "#c7ddff"
    },
    "accentWarm": {
      "intensity": 3.64,
      "reflection": 3,
      "elevation": 34,
      "azimuth": 127,
      "size": 3,
      "color": "#ff0000"
    }
  },
  "post": {
    "dof": {
      "enabled": false,
      "focusBias": -0.43,
      "aperture": 4,
      "range": 12
    },
    "bloom": {
      "threshold": 0.28,
      "intensity": 0,
      "radius": 0.11
    },
    "monochrome": 0,
    "tonemap": "aces",
    "exposure": 1.3,
    "contrast": 1.15,
    "blackLift": 0,
    "vignette": {
      "strength": 0.325,
      "softness": 1.175,
      "radius": 1.11
    },
    "grain": {
      "strength": 0,
      "size": 1,
      "speed": 0,
      "shadowWeight": 0.28,
      "backgroundStrength": 0.035,
      "backgroundSize": 2,
      "backgroundSpeed": 0
    },
    "dither": true,
    "aaMode": "traa",
    "pixelRatioCap": 2
  }
}
source/README.md
# Source for assets/frost.js and assets/motion.js

This directory holds the TypeScript source and esbuild script that produce
the two bundled scripts the composition loads: `../assets/frost.js` (the
WebGPU/TSL ice rig, via `src/frost.ts`) and `../assets/motion.js` (the
lightweight WebGL editor helper, via `src/motion/editor.ts`).

## Build

```sh
npm install
npm run build
```

This regenerates `../assets/frost.js` and `../assets/motion.js`, plus a
`.LEGAL.txt` file next to each listing the bundled third-party notices
esbuild was able to extract automatically. `assets/Three-LICENSE.txt`,
`assets/ThreeMeshBVH-LICENSE.txt`, `assets/OpentypeJS-LICENSE.txt`, and
`assets/Clipper-LICENSE.txt` cover the same dependencies explicitly, since
automatic extraction doesn't catch every one of them.

`build.mjs` runs two checks before bundling: a source-only TypeScript pass
(`tools/compile-preflight.mjs`) and a headless shader-compile probe
(`tools/run-shard-compile.mjs`) that generates the WGSL for one shard
without a GPU, to catch a class of shader bug that only shows up at
compile time.

## Scope

This is the buildable subset: the TypeScript source, `build.mjs`, and the
two tool scripts it imports. The original project's test suite and a
standalone visual tuning UI (`workbench/`) are not included here.
source/src/assets.ts
// FROST: the experiment served its assets from the site root (/textures, /fonts, /logo.svg ...). As a
// HyperFrames block they live in ./assets next to index.html, resolved relative to the composition document.
export const ASSET_BASE: string =
  (typeof window !== "undefined" && (window as any).__frostAssetBase) || "assets/";
export const assetUrl = (name: string) => ASSET_BASE + name;
source/src/cache.ts
// FROST: an IndexedDB cache for the expensive, deterministic build products (extruded geometries, distance
// fields, grain homes), keyed by everything that shapes them. A reload with the same headlines and type
// settings skips the voxelisation; the entry is dropped automatically when the key changes.
import * as THREE from "three/webgpu";

const DB = "frost-build-cache",
  STORE = "builds",
  VERSION = 1;
/** bump when the build pipeline changes in a way that makes old entries wrong */
export const BUILD_VERSION = "frost-build-10-planar-cap-normals";

export interface CachedGeometry {
  position: Float32Array;
  normal: Float32Array;
  uv: Float32Array | null;
  index: Uint32Array | null;
}
export interface CachedBuild {
  geometries: CachedGeometry[];
  sdfs: {
    data: Float32Array;
    res: number;
    bound: number;
    thickness: number;
    sampleBound: number;
  }[];
  rests: Float32Array[];
}

function open(): Promise<IDBDatabase | null> {
  return new Promise((resolve) => {
    try {
      if (typeof indexedDB === "undefined") return resolve(null);
      const req = indexedDB.open(DB, VERSION);
      req.onupgradeneeded = () => {
        req.result.createObjectStore(STORE);
      };
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => resolve(null);
    } catch {
      resolve(null);
    }
  });
}

export async function readBuild(key: string): Promise<CachedBuild | null> {
  const db = await open();
  if (!db) return null;
  return new Promise((resolve) => {
    try {
      const tx = db.transaction(STORE, "readonly");
      const req = tx.objectStore(STORE).get(key);
      req.onsuccess = () => resolve((req.result as CachedBuild) ?? null);
      req.onerror = () => resolve(null);
    } catch {
      resolve(null);
    }
  });
}

export async function writeBuild(key: string, build: CachedBuild): Promise<void> {
  const db = await open();
  if (!db) return;
  await new Promise<void>((resolve) => {
    try {
      const tx = db.transaction(STORE, "readwrite");
      const store = tx.objectStore(STORE);
      store.clear(); // one entry: the current build
      store.put(build, key);
      tx.oncomplete = () => resolve();
      tx.onerror = () => resolve();
    } catch {
      resolve();
    }
  });
}

export function packGeometry(g: THREE.BufferGeometry): CachedGeometry {
  const a = (name: string) =>
    g.attributes[name] ? (g.attributes[name].array as Float32Array) : null;
  return {
    position: a("position")!,
    normal: a("normal")!,
    uv: a("uv"),
    index: g.index ? new Uint32Array(g.index.array as ArrayLike<number>) : null,
  };
}

export function unpackGeometry(c: CachedGeometry): THREE.BufferGeometry {
  const g = new THREE.BufferGeometry();
  g.setAttribute("position", new THREE.Float32BufferAttribute(c.position, 3));
  g.setAttribute("normal", new THREE.Float32BufferAttribute(c.normal, 3));
  if (c.uv) g.setAttribute("uv", new THREE.Float32BufferAttribute(c.uv, 2));
  if (c.index) g.setIndex(new THREE.BufferAttribute(c.index, 1));
  g.computeBoundingBox();
  return g;
}
source/src/core/AdaptiveResolution.ts
/**
 * Keeps the render workload inside a frame budget by changing only pixel density (effects, simulation
 * rates and content untouched). Adjustments are quantised and hysteretic so targets are not resized constantly.
 */
export class AdaptiveResolution {
  private ratio = 0;
  private frameMs = 0;
  private frames = 0;
  private lastAdjust = 0;

  update(
    now: number,
    elapsedMs: number,
    deviceRatio: number,
    cap: number,
    enabled: boolean,
    targetFps: number,
    minimum: number,
  ) {
    const maximum = Math.max(0.5, Math.min(deviceRatio || 1, cap));
    const minRatio = Math.min(maximum, Math.max(0.5, minimum));
    if (!this.ratio || !enabled) this.ratio = maximum;
    this.ratio = Math.min(maximum, Math.max(minRatio, this.ratio));

    // Clip one-off shader compilation/rebuild stalls before feeding the controller.
    const sample = Math.min(50, Math.max(1, elapsedMs));
    this.frameMs = this.frames === 0 ? sample : this.frameMs + (sample - this.frameMs) * 0.08;
    this.frames++;
    if (!enabled || this.frames < 45 || now - this.lastAdjust < 0.5) return this.ratio;

    const budget = 1000 / Math.max(30, targetFps);
    let next = this.ratio;
    if (this.frameMs > budget * 1.08 && this.ratio > minRatio) {
      const ideal = this.ratio * Math.sqrt(budget / this.frameMs) * 0.98;
      next = Math.min(this.ratio - 0.05, ideal);
      next = Math.floor(next * 20) / 20;
    } else if (this.frameMs < budget * 0.82 && this.ratio < maximum) {
      next = Math.ceil((this.ratio + 0.05) * 20) / 20;
    }
    next = Math.min(maximum, Math.max(minRatio, next));
    if (next !== this.ratio) {
      this.ratio = next;
      // Give the resized render targets time to establish their new steady-state cost.
      this.frameMs = budget;
      this.lastAdjust = now;
    }
    return this.ratio;
  }

  /**
   * Plans a split-resolution frame. `sourceRatio` is the DPR paid by the scene pass; `outputRatio` is the
   * canvas/final-post DPR. With split output disabled they are identical (whole-frame scaling fallback).
   */
  updateScene(
    now: number,
    elapsedMs: number,
    deviceRatio: number,
    outputCap: number,
    enabled: boolean,
    targetFps: number,
    minPixelRatio: number,
    maxSceneScale: number,
    minSceneScale: number,
    splitOutput: boolean,
  ) {
    const nativeRatio = Math.max(0.5, Math.min(deviceRatio || 1, outputCap));
    const maxScale = Math.max(0.25, Math.min(1, maxSceneScale));
    const sourceMaximum = Math.max(0.5, nativeRatio * maxScale);
    const relativeMinimum = nativeRatio * Math.max(0.25, Math.min(maxScale, minSceneScale));
    const sourceMinimum = Math.min(sourceMaximum, Math.max(0.5, minPixelRatio, relativeMinimum));
    const sourceRatio = this.update(
      now,
      elapsedMs,
      sourceMaximum,
      sourceMaximum,
      enabled,
      targetFps,
      sourceMinimum,
    );
    const outputRatio = splitOutput ? nativeRatio : sourceRatio;
    return {
      sourceRatio,
      outputRatio,
      sceneScale: splitOutput ? sourceRatio / nativeRatio : 1,
    };
  }
}
source/src/core/clock.ts
/** Wall-clock with pause/step support. All systems read `clock.t` and `clock.dt`. */
export const clock = {
  t: 0,
  dt: 1 / 60,
  frame: 0,
  paused: false,
  stepRequested: false,
  _last: performance.now() / 1000,
  tick(now = performance.now() / 1000) {
    const raw = Math.min(0.05, Math.max(0, now - this._last));
    this._last = now;
    if (this.paused && !this.stepRequested) {
      this.dt = 0;
      return;
    }
    this.stepRequested = false;
    this.dt = this.paused ? 1 / 60 : raw;
    this.t += this.dt;
    this.frame++;
  },
};
source/src/core/ease.ts
export const expoInOut = (x: number) =>
  x <= 0
    ? 0
    : x >= 1
      ? 1
      : x < 0.5
        ? Math.pow(2, 20 * x - 10) / 2
        : (2 - Math.pow(2, -20 * x + 10)) / 2;
export const expoOut = (x: number) => (x >= 1 ? 1 : 1 - Math.pow(2, -10 * x));
export const smooth = (x: number) => x * x * (3 - 2 * x);
export const clamp01 = (x: number) => Math.min(1, Math.max(0, x));
/** Frame-rate independent exponential smoothing toward `target` with time constant `tau` (seconds). */
export const damp = (cur: number, target: number, tau: number, dt: number) =>
  tau <= 0 ? target : cur + (target - cur) * (1 - Math.exp(-dt / tau));
source/src/core/interaction.ts
// Cursor -> object-space SDF raycast -> stroke capsules (through the volume) + stroke velocity for the powder.
import * as THREE from "three/webgpu";
import type { ShapeSpec } from "../shape/sdf";
import { raycastSDF } from "../shape/sdf";
import { input, sim, heroFrame } from "./state";
import { clock } from "./clock";
import { D } from "../dials/store";
import { damp } from "./ease";
import type { ErosionField } from "../erosion/ErosionField";
import { MAX_SEGMENTS } from "../erosion/ErosionField";
import type { Powder } from "../powder/Powder";

const DEPTH_STEPS = 4;
/** ?stroke=1 drives a synthetic sweep (headless checks). */
const FORCE_STROKE =
  typeof location !== "undefined" && new URLSearchParams(location.search).has("stroke");
/** ?strokeFrames=N stops the synthetic sweep after N frames (to watch what the field does afterwards). */
const STROKE_FRAMES =
  typeof location !== "undefined"
    ? parseInt(new URLSearchParams(location.search).get("strokeFrames") || "1000000", 10)
    : 1e6;
const STROKE_START =
  typeof location !== "undefined"
    ? parseInt(new URLSearchParams(location.search).get("strokeStart") || "0", 10)
    : 0;

export class Interaction {
  readonly strokeSegments: THREE.Vector4[] = Array.from(
    { length: MAX_SEGMENTS * 2 },
    () => new THREE.Vector4(),
  );
  strokeCount = 0;
  /** Smoothed stroke velocity in world units / s and its direction (kept after the stroke ends, speed decays). */
  readonly strokeVel = new THREE.Vector3();
  readonly strokeDir = new THREE.Vector3(1, 0.4, 0).normalize();
  strokeSpeed = 0;
  /** Debug: last entry/exit hit points in world space. */
  readonly hitEntry = new THREE.Vector3();
  readonly hitExit = new THREE.Vector3();
  hasHit = false;

  private prevEntry = new THREE.Vector3();
  private prevExit = new THREE.Vector3();
  private prevEntryWorld = new THREE.Vector3();
  private prevHit = false;
  private prevX = 0;
  private prevY = 0;
  /** Scripted strokes (hero triggers): a sweep along an object-space path, or a burst at a point. */
  /** FROST: a queue instead of one sweep, so a shatter can run several parallel slices with staggered starts. */
  private scripted: {
    kind: "sweep" | "front";
    from: THREE.Vector3;
    to: THREE.Vector3;
    normal?: THREE.Vector3;
    reach?: number;
    t0: number;
    delay: number;
    duration: number;
    radius: number;
    strength: number;
    through: boolean;
    prevT: number;
  }[] = [];
  private scrollP = -1;
  private scrollPath = { from: new THREE.Vector3(-1, -0.6, 0), to: new THREE.Vector3(1, 0.6, 0) };
  private ray = new THREE.Ray();
  private inv = new THREE.Matrix4();
  private tmp = new THREE.Vector3();
  private objectRayOrigin = new THREE.Vector3();
  private objectRayDirection = new THREE.Vector3();
  private workA = new THREE.Vector3();
  private workB = new THREE.Vector3();
  private workDirection = new THREE.Vector3();
  private velocity = new THREE.Vector3();
  private rayHit = { entry: new THREE.Vector3(), exit: new THREE.Vector3(), tEntry: 0, tExit: 0 };
  private resetDone = true;

  constructor(
    readonly shape: ShapeSpec,
    readonly objectGroup: THREE.Object3D,
    readonly camera: THREE.PerspectiveCamera,
    readonly erosion: ErosionField | null,
    readonly powder: Powder | null,
    /** V2: called when a reset completes (chunks snap home) */
    readonly onReset?: () => void,
  ) {}

  /** Play a sweep across the object (object-space path, fractions of the bound). */
  sweep(
    opts: {
      from?: [number, number, number];
      to?: [number, number, number];
      duration?: number;
      radius?: number;
      strength?: number;
      through?: boolean;
      delay?: number;
    } = {},
  ) {
    const B = this.shape.bound;
    const f = opts.from ?? [-1, -0.55, 0],
      to = opts.to ?? [1, 0.55, 0];
    this.scripted.push({
      kind: "sweep",
      from: new THREE.Vector3(f[0] * B, f[1] * B, f[2] * B),
      to: new THREE.Vector3(to[0] * B, to[1] * B, to[2] * B),
      t0: -1,
      delay: opts.delay ?? 0,
      duration: opts.duration ?? 0.7,
      radius: opts.radius ?? D.erosion.brushRadius,
      strength: opts.strength ?? D.erosion.brushStrength * 1.5,
      through: opts.through ?? true,
      prevT: 0,
    });
  }

  /**
   * Break front: a band the full slice-path length (plus half a bound each way) travels outward on both sides,
   * eroding all it passes until the shape is gone. `reach` in bounds; eject direction stays the slice's.
   */
  front(opts: {
    from: [number, number, number];
    to: [number, number, number];
    delay?: number;
    duration?: number;
    reach?: number;
    radius?: number;
    strength?: number;
  }) {
    const B = this.shape.bound;
    const [fx, fy] = opts.from,
      [tx, ty] = opts.to;
    const dx = tx - fx,
      dy = ty - fy,
      len = Math.hypot(dx, dy) || 1;
    const ex = (dx / len) * 0.5,
      ey = (dy / len) * 0.5;
    this.scripted.push({
      kind: "front",
      from: new THREE.Vector3((fx - ex) * B, (fy - ey) * B, 0),
      to: new THREE.Vector3((tx + ex) * B, (ty + ey) * B, 0),
      normal: new THREE.Vector3(-dy / len, dx / len, 0),
      reach: (opts.reach ?? 1.3) * B,
      t0: -1,
      delay: opts.delay ?? 0,
      duration: opts.duration ?? 1.5,
      radius: opts.radius ?? D.erosion.brushRadius,
      strength: opts.strength ?? D.erosion.brushStrength * 1.5,
      through: true,
      prevT: 0,
    });
  }

  /** FROST: drop every queued sweep (a reset). */
  clearScripted() {
    this.scripted.length = 0;
  }

  /** Burst at a world-space point (e.g. a click hit), eroding a sphere and throwing grains outward. */
  burst(worldPoint: THREE.Vector3, radius: number, strength: number, duration = 0.18) {
    const inv = new THREE.Matrix4().copy(this.objectGroup.matrixWorld).invert();
    const p = worldPoint.clone().applyMatrix4(inv);
    this.scripted.push({
      from: p,
      to: p.clone(),
      t0: -1,
      delay: 0,
      duration,
      radius,
      strength,
      through: false,
      prevT: 0,
    });
    const centre = new THREE.Vector3().setFromMatrixPosition(this.objectGroup.matrixWorld);
    const dir = worldPoint.clone().sub(centre);
    if (dir.lengthSq() < 1e-6) dir.set(0, 1, 0);
    this.strokeDir.copy(dir.normalize());
    this.strokeSpeed = D.erosion.brushSpeedRef * 1.5;
  }

  /** Scroll-driven erosion: erodes along a diagonal as `p` (0..1) advances; call every frame. */
  sweepTo(p: number) {
    this.scrollP = Math.max(0, Math.min(1, p));
  }

  private pushScripted(t: number, dt: number) {
    if (this.scripted.length === 0) return false;
    let pushed = false;
    for (const sc of this.scripted) {
      if (sc.t0 < 0) sc.t0 = t + sc.delay;
      if (t < sc.t0) continue;
      const u = Math.min(1, (t - sc.t0) / Math.max(0.01, sc.duration));
      const B = this.shape.bound;
      if (sc.kind === "front") {
        // the band starts fast and slows down; its radius never drops below the distance it moved this frame
        const ease = (x: number) => 1 - (1 - x) * (1 - x);
        const dPrev = sc.reach! * ease(sc.prevT),
          dNow = sc.reach! * ease(u);
        const r = Math.max(sc.radius, (dNow - dPrev) * 1.5);
        // one band per depth slice: the shapes are thin, so one at z = 0 usually covers them
        const th = this.shape.thickness;
        const zs = th <= r * 0.9 ? [0] : [-th, 0, th];
        if (this.strokeCount + zs.length * 2 > MAX_SEGMENTS) break;
        for (const s of [1, -1]) {
          const a = this.workA.copy(sc.from).addScaledVector(sc.normal!, s * dNow),
            b = this.workB.copy(sc.to).addScaledVector(sc.normal!, s * dNow);
          for (const z of zs) {
            this.strokeSegments[this.strokeCount * 2].set(a.x, a.y, z, r);
            this.strokeSegments[this.strokeCount * 2 + 1].set(b.x, b.y, z, sc.strength);
            this.strokeCount++;
          }
        }
        this.strokeSpeed = Math.max(this.strokeSpeed, (sc.reach! / sc.duration) * 0.6);
        sc.prevT = u;
        pushed = true;
        continue;
      }
      const a = this.workA.copy(sc.from).lerp(sc.to, sc.prevT),
        b = this.workB.copy(sc.from).lerp(sc.to, u);
      const n = sc.through ? 3 : 1;
      if (this.strokeCount + n > MAX_SEGMENTS) break;
      for (let k = 0; k < n; k++) {
        const z = sc.through ? -B + (2 * B * (k + 0.5)) / n : 0;
        this.strokeSegments[this.strokeCount * 2].set(
          a.x,
          a.y,
          sc.through ? z - B / n : a.z,
          sc.radius,
        );
        this.strokeSegments[this.strokeCount * 2 + 1].set(
          b.x,
          b.y,
          sc.through ? z + B / n : b.z,
          sc.strength,
        );
        this.strokeCount++;
      }
      if (sc.from.distanceToSquared(sc.to) > 1e-6) {
        const dirW = this.workDirection
          .copy(sc.to)
          .sub(sc.from)
          .transformDirection(this.objectGroup.matrixWorld);
        this.strokeDir.copy(dirW.normalize());
        this.strokeSpeed = Math.max(
          this.strokeSpeed,
          (sc.from.distanceTo(sc.to) / sc.duration) * 0.6,
        );
      }
      sc.prevT = u;
      pushed = true;
    }
    this.scripted = this.scripted.filter((sc) => sc.prevT < 1);
    void dt;
    return pushed;
  }

  private pushScroll(dt: number) {
    if (this.scrollP < 0) return false;
    const p = this.scrollP;
    if (this.prevScrollP < 0) {
      this.prevScrollP = p;
      return false;
    }
    const dp = p - this.prevScrollP;
    if (Math.abs(dp) < 0.0005) return false;
    const B = this.shape.bound;
    const a = this.workA
      .copy(this.scrollPath.from)
      .lerp(this.scrollPath.to, this.prevScrollP)
      .multiplyScalar(B);
    const b = this.workB.copy(this.scrollPath.from).lerp(this.scrollPath.to, p).multiplyScalar(B);
    if (dp > 0) {
      for (let k = 0; k < 3 && this.strokeCount < MAX_SEGMENTS; k++) {
        const z = -B + (2 * B * (k + 0.5)) / 3;
        this.strokeSegments[this.strokeCount * 2].set(a.x, a.y, z - B / 3, D.erosion.brushRadius);
        this.strokeSegments[this.strokeCount * 2 + 1].set(
          b.x,
          b.y,
          z + B / 3,
          D.erosion.brushStrength * 2.5,
        );
        this.strokeCount++;
      }
      const dirW = this.workDirection
        .copy(b)
        .sub(a)
        .transformDirection(this.objectGroup.matrixWorld);
      if (dirW.lengthSq() > 1e-8) this.strokeDir.copy(dirW.normalize());
      this.strokeSpeed = Math.max(
        this.strokeSpeed,
        Math.min(6, (Math.abs(dp) * 2 * B) / Math.max(dt, 1 / 120)),
      );
    }
    this.prevScrollP = p;
    return dp > 0;
  }
  private prevScrollP = -1;

  update(t: number, dt: number) {
    this.strokeCount = 0;
    const E = D.erosion;
    // --- reset (click / R): fade the powder over `resetFade` ms, then clear everything
    if (sim.resetRequestedAt >= 0) {
      const u = (t - sim.resetRequestedAt) / Math.max(0.05, D.healing.resetFade / 1000);
      sim.fade = Math.max(0, 1 - u);
      if (u >= 1) {
        if (this.erosion) this.erosion.clear(this.erosion.renderer);
        if (this.powder && this.erosion) this.powder.reset(this.erosion.renderer);
        this.onReset?.();
        sim.resetRequestedAt = -1;
        sim.lastStrokeT = -1e9;
        sim.fade = 1;
      }
    } else sim.fade = damp(sim.fade, 1, 0.15, dt);

    const moved = input.x !== this.prevX || input.y !== this.prevY;
    let hit: ReturnType<typeof raycastSDF> = null;
    if (input.inside) {
      // world ray from the cursor, into object space
      this.tmp.set(input.x, input.y, 0.5).unproject(this.camera);
      this.ray.origin.copy(this.camera.position);
      this.ray.direction.copy(this.tmp).sub(this.camera.position).normalize();
      this.inv.copy(this.objectGroup.matrixWorld).invert();
      const o = this.objectRayOrigin.copy(this.ray.origin).applyMatrix4(this.inv);
      const d = this.objectRayDirection.copy(this.ray.direction).transformDirection(this.inv);
      hit = raycastSDF(this.shape, o, d, 60, this.rayHit);
    }
    sim.overObject = !!hit;
    this.hasHit = !!hit;
    if (hit) {
      this.hitEntry.copy(hit.entry).applyMatrix4(this.objectGroup.matrixWorld);
      this.hitExit.copy(hit.exit).applyMatrix4(this.objectGroup.matrixWorld);
    }

    let strokeThisFrame = false;
    if (dt > 0 && this.pushScripted(t, dt)) strokeThisFrame = true;
    if (dt > 0 && this.pushScroll(dt)) strokeThisFrame = true;
    const cursorErodes = !heroFrame.enabled || heroFrame.cursorErodes;
    if (
      cursorErodes &&
      hit &&
      this.prevHit &&
      moved &&
      dt > 0 &&
      this.strokeCount + DEPTH_STEPS <= MAX_SEGMENTS
    ) {
      const entryWorld = this.hitEntry;
      const vel = this.velocity.copy(entryWorld).sub(this.prevEntryWorld).divideScalar(dt);
      const speed = vel.length();
      if (speed > 0.02) {
        strokeThisFrame = true;
        this.strokeVel.lerp(vel, 0.5);
        this.strokeSpeed = damp(this.strokeSpeed, Math.min(speed, E.brushSpeedRef * 3), 0.08, dt);
        if (this.strokeVel.lengthSq() > 1e-6) this.strokeDir.copy(this.strokeVel).normalize();
        const k = THREE.MathUtils.clamp(speed / E.brushSpeedRef, 0.12, 2.5);
        const strength = E.brushStrength * k;
        const radius = E.brushRadius * (0.72 + 0.28 * Math.min(1, k));
        for (let s = 0; s < DEPTH_STEPS; s++) {
          const f = (s / (DEPTH_STEPS - 1)) * E.throughDepth;
          const idx = this.strokeCount + s;
          this.strokeSegments[idx * 2].set(
            THREE.MathUtils.lerp(this.prevEntry.x, this.prevExit.x, f),
            THREE.MathUtils.lerp(this.prevEntry.y, this.prevExit.y, f),
            THREE.MathUtils.lerp(this.prevEntry.z, this.prevExit.z, f),
            radius,
          );
          this.strokeSegments[idx * 2 + 1].set(
            THREE.MathUtils.lerp(hit.entry.x, hit.exit.x, f),
            THREE.MathUtils.lerp(hit.entry.y, hit.exit.y, f),
            THREE.MathUtils.lerp(hit.entry.z, hit.exit.z, f),
            strength,
          );
        }
        this.strokeCount += DEPTH_STEPS;
      }
    }
    if (
      (D.debug.forceStroke ||
        (FORCE_STROKE && clock.frame >= STROKE_START && clock.frame < STROKE_FRAMES)) &&
      dt > 0
    ) {
      // synthetic diagonal sweep (lower-left -> upper-right) for testing without a cursor
      const ph = (t * 0.25) % 1;
      const B = this.shape.bound * 0.9;
      const ax = -B + ph * 2 * B,
        ay = -B * 0.7 + ph * 1.4 * B;
      const bx = ax + 0.15,
        by = ay + 0.1;
      this.strokeSegments[0].set(ax, ay, -B, E.brushRadius);
      this.strokeSegments[1].set(bx, by, B, E.brushStrength);
      this.strokeCount = 1;
      this.strokeDir.set(1, 0.7, 0).normalize();
      this.strokeSpeed = E.brushSpeedRef;
      strokeThisFrame = true;
    }
    if (!strokeThisFrame) this.strokeSpeed = damp(this.strokeSpeed, 0, 1.2, dt);
    sim.strokeActive = strokeThisFrame;
    // scroll-driven pages hold their erosion: healing is suppressed while suspended
    if (heroFrame.enabled && heroFrame.healSuspended) sim.lastStrokeT = t;

    if (hit) {
      this.prevEntry.copy(hit.entry);
      this.prevExit.copy(hit.exit);
      this.prevEntryWorld.copy(this.hitEntry);
    }
    this.prevHit = !!hit;
    this.prevX = input.x;
    this.prevY = input.y;
    void this.resetDone;
  }
}
source/src/core/motion.ts
import type { Schedule, Pose } from "../frost";

export const SETTLE = 2.8;
export const ATTACK = 0.65; // anticipation before each break
const clamp01 = (x: number) => Math.max(0, Math.min(1, x));

/** C2 turn envelopes: continuous velocity/acceleration over an ongoing drift. */
export function makePoseAt(S: Schedule, P: Pose) {
  const flowEase = (u: number) => {
    u = clamp01(u);
    return u * u * u * (u * (u * 6 - 15) + 10);
  };
  /** Quintic rise and fall meet with zero velocity and acceleration at the crest. */
  const bump = (u: number, a: number) =>
    u <= a ? flowEase(u / Math.max(a, 1e-6)) : 1 - flowEase((u - a) / Math.max(1 - a, 1e-6));
  type Turn = {
    t0: number;
    tBreakEnd: number;
    t1: number;
    yaw: number;
    pitch: number;
    z: number;
    keep: boolean;
  };
  const turns: Turn[] = [
    {
      t0: S.logoBreak - ATTACK,
      tBreakEnd: S.form1,
      t1: S.form1 + SETTLE,
      yaw: P.turnYaw,
      pitch: P.turnPitch,
      z: P.approach,
      keep: false,
    },
    {
      t0: S.break1 - ATTACK,
      tBreakEnd: S.form2,
      t1: S.form2 + SETTLE,
      yaw: P.turnYaw2,
      pitch: P.turnPitch2,
      z: -P.retreat,
      keep: false,
    },
    {
      t0: S.break2 - ATTACK,
      tBreakEnd: S.fadeOut + 1.5,
      t1: S.fadeOut + 1.5,
      yaw: P.finalYaw,
      pitch: P.finalPitch,
      z: P.approach + P.zoomIn,
      keep: true,
    },
  ];
  /** the keyed part of the path (without drift / idle), in degrees and units */
  function keyPoseAt(t: number) {
    let yaw = 0,
      pitch = 0,
      z = 0;
    for (const k of turns) {
      const u = (t - k.t0) / Math.max(k.t1 - k.t0, 1e-6);
      if (u <= 0) continue;
      yaw += k.yaw * flowEase(u);
      const a = (k.tBreakEnd - k.t0) / Math.max(k.t1 - k.t0, 1e-6);
      const b = k.keep ? flowEase(u) : bump(u, a);
      pitch += k.pitch * b;
      z += k.z * b;
    }
    return { yaw, pitch, z };
  }
  const driftYawAt = (t: number) => P.driftYaw * t;
  const idleYawAt = (t: number) => P.idleYaw * Math.sin((2 * Math.PI * t) / 9);
  return function poseAt(t: number) {
    const k = keyPoseAt(t);
    const sway = P.driftSway * Math.sin((2 * Math.PI * t) / 11);
    return {
      yaw: k.yaw + driftYawAt(t) + idleYawAt(t),
      pitch: k.pitch + sway,
      z: k.z + 0.0875 * P.driftSway * Math.sin((2 * Math.PI * t) / 11),
    };
  };
}
source/src/core/schedule.ts
import type { Schedule } from "../frost";
import { ATTACK, SETTLE } from "./motion";

export const FADE_DURATION = 1.5;
export const DEFAULT_SCHEDULE: Readonly<Schedule> = Object.freeze({
  logoBreak: 1.5,
  form1: 3.8,
  break1: 10.375,
  form2: 12.675,
  break2: 18.5,
  fadeOut: 22,
});
export const durationOf = (s: Schedule) => s.fadeOut + FADE_DURATION;

/** Preserve entered beat times, moving only overlaps forward; never truncate the final break. */
export function resolveSchedule(raw: Schedule, breakDuration: number): Schedule {
  const breakSpan = 0.3 + breakDuration + 0.6;
  const logoBreak = raw.logoBreak;
  const form1 = Math.max(raw.form1, logoBreak + breakSpan);
  const break1 = Math.max(raw.break1, form1 + SETTLE + ATTACK);
  const form2 = Math.max(raw.form2, break1 + breakSpan);
  const break2 = Math.max(raw.break2, form2 + SETTLE + ATTACK);
  const fadeOut = Math.max(raw.fadeOut, break2 + breakSpan);
  return { logoBreak, form1, break1, form2, break2, fadeOut };
}
source/src/core/seed.ts
/** Deterministic PRNG (mulberry32) seeded from the DialKit `seed`. */
export function rng(seed: number) {
  let a = (seed * 1664525 + 1013904223) >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
source/src/core/state.ts
/** Shared interaction state written by the DOM layer and read by the render loop. */
export const input = {
  x: 0,
  y: 0, // normalized -1..1 (y up)
  px: 0,
  py: 0, // pixels
  inside: false, // pointer inside the viewport
  lastMoveT: 0, // clock time of last movement
  moved: false,
  clicked: false,
  uiHidden: false,
  overUi: false,
};

export const sim = {
  strokeActive: false, // cursor currently over the object with movement
  overObject: false,
  lastStrokeT: -1e9,
  healing: false,
  resetRequestedAt: -1, // clock time when a reset fade started, -1 when none
  fade: 1, // powder fade multiplier (reset)
  sceneFade: 1, // scene exposure multiplier (shape crossfade)
  hoverAmount: 0, // smoothed 0..1 hover feel
  fps: 0,
  frameMs: 0,
  pixelRatio: 1, // effective DPR of the heavy scene pass
  outputPixelRatio: 1, // DPR of the final post-processing output
  sceneResolutionScale: 1,
  gpuFrames: 0,
  gpuRenderMs: 0,
  gpuComputeMs: 0,
  fieldActive: true,
  powderActive: true,
  computeMs: 0,
  counts: { active: 0, healing: 0, dormant: 0, total: 0 },
  erosion: { max: 0, over50: 0, over90: 0, refrost: 0 },
};

/**
 * Per-page override layer used by the hero variants. Nothing here is set on `/`, which keeps the
 * experiment page exactly as tuned. Hero pages write into it every frame from their own DialKit panel.
 */
export const heroFrame = {
  enabled: false,
  clickResets: true,
  /** false = the hero keeps the shared Camera folder + Shape offsets (the page looks exactly like `/`) */
  framing: true,
  /** camera framing (replaces the Camera folder values while enabled) */
  distance: 13.5,
  height: 1.2,
  lookAtX: 0,
  lookAtY: -0.15,
  fov: 27,
  baseYaw: 0,
  basePitch: 0,
  rotateYaw: 35,
  rotatePitch: 10,
  parallaxRange: 3,
  /** world-space offset of the object (so it can sit right / left / cropped) */
  objectX: 0,
  objectY: 0,
  objectZ: 0,
  /** optional backdrop override (light variants) */
  backdrop: null as null | {
    top: string;
    mid: string;
    bottom: string;
    centerX: number;
    centerY: number;
    radius: number;
    falloff: number;
  },
  /** optional shape override (logo variant) */
  shape: null as null | string,
  /** while true the field never heals (scroll-driven erosion holds its state) */
  healSuspended: false,
  /** cursor erosion on/off (some variants only erode via scripted triggers) */
  cursorErodes: true,
  /** show the stats HUD on hero pages (off by default, the page is the point) */
  hud: false,
  /** Optional final camera transform used by scroll-driven camera exports after the normal object rig. */
  cameraOverride: null as null | ((camera: import("three/webgpu").PerspectiveCamera) => void),
  cameraProgress: 0,
  /** extrusion parameters for the logo shape (hero 4); a change reloads the mark */
  logoParams: null as null | Record<string, number>,
};
source/src/dials/baked.json
{
  "shape": {
    "shape": "logo",
    "size": 1,
    "tubeRatio": 0.28,
    "deformAmplitude": 0,
    "deformFrequency": 0.3,
    "segments": 512,
    "seed": 7,
    "offsetX": 0,
    "offsetY": 0.48,
    "offsetZ": 0,
    "logo": {
      "width": 2.6,
      "depth": 0.22,
      "bevelThickness": 0.06,
      "bevelSize": 0.05,
      "bevelOffset": 0,
      "bevelSegments": 13,
      "cornerRadius": 0.03,
      "curveSegments": 58,
      "creaseAngle": 40,
      "sdfRes": 128
    }
  },
  "ice": {
    "ior": 1.61,
    "dispersion": 0.17,
    "thicknessScale": 0.1,
    "attenuationDistance": 3.64,
    "attenuationColor": "#525252",
    "baseRoughness": 0.095,
    "frost": {
      "scale": 1.05,
      "threshold": 0.61,
      "softness": 0.36,
      "roughness": 0.73,
      "diffuse": 0.57,
      "crystalBump": 0.62,
      "crystalScale": 44
    },
    "cracks": {
      "largeScale": 1.15,
      "warp": 0.37,
      "warpScale": 2.2,
      "coverage": 0.84,
      "regionScale": 1.35,
      "regionCoverage": 0.52,
      "veinScale": 20,
      "veinContrast": 1,
      "width": 0.0175,
      "brightness": 0.41,
      "darkness": 1,
      "refraction": 0.077,
      "surfaceStrength": 0.67,
      "steps": 21,
      "fineCracks": true,
      "fineScale": 20,
      "fineAmount": 0.96,
      "fineCoverage": 0.23,
      "fineNearLarge": 1
    },
    "smudges": {
      "amount": 1,
      "coverage": 0.29,
      "maskScale": 3.1,
      "anisotropy": 5.5,
      "roughness": 0.91,
      "whiteness": 0.5,
      "scale": 1.6
    },
    "bumps": {
      "microBump": 1,
      "microScale": 70,
      "microCoverage": 0.4,
      "rippleBump": 0.45,
      "rippleScale": 2.5,
      "maskScale": 3.6
    },
    "crumbleGlow": 0.16,
    "edgeWhiteness": 0.5,
    "clearcoat": 0.15,
    "clearcoatRoughness": 0.415,
    "envIntensity": 1.45,
    "specularIntensity": 1.28,
    "interiorScatter": 0.32
  },
  "erosion": {
    "resolution": "192",
    "brushRadius": 0.465,
    "brushSoftness": 0.23,
    "brushStrength": 5.6,
    "brushSpeedRef": 3.7,
    "brushNoise": 0.4,
    "brushNoiseScale": 16,
    "throughDepth": 0.84,
    "cellSnap": 1,
    "breakCellScale": 2.2,
    "crumbleRate": 5.35,
    "crumbleCrackBias": 5.7,
    "crumbleDuration": 0.35,
    "cutThreshold": 0.46,
    "cutSoftness": 0.045,
    "edgeWidth": 0.37,
    "interiorSteps": 64,
    "interiorFrost": 0.1,
    "edgeInset": 0.175,
    "edgeBump": 0.48,
    "edgeBumpScale": 66
  },
  "powder": {
    "particleCount": "100k",
    "amount": 0.09,
    "grainSizeMultiplier": 1.5,
    "nearSurfaceFraction": 1,
    "nearSurfaceDepth": 0.5,
    "grainSizes": {
      "tinyDustRatio": 0.2,
      "smallGrainRatio": 0.22,
      "mediumClumpRatio": 0.07,
      "tinyDustSize": 0.0055,
      "smallGrainSize": 0.011,
      "mediumClumpSize": 0.022,
      "largeFragmentSize": 0.075,
      "sizeJitter": 1
    },
    "grainVariants": 6,
    "facetedGrains": false,
    "minPixelSize": 0.65,
    "ejectSpeed": 0.8,
    "minEjectSpeed": 0.66,
    "ejectSpread": 0,
    "ejectTurbulence": 0,
    "backwardRatio": 0.25,
    "clumpSpeedJitter": 1,
    "drag": 0.8,
    "gravity": 0.895,
    "turbulence": 2.4,
    "turbulenceScale": 5.85,
    "turbulenceDecay": 2.58,
    "clumpCohesion": 10,
    "clumpCount": 500,
    "settleTime": 2.65,
    "settledDrift": 0.01,
    "tumble": 2.5,
    "baseTone": "#999999",
    "wrap": 0,
    "fragments": {
      "translucency": 1,
      "throughTint": "#212121",
      "fresnelPower": 5.2,
      "roughness": 0,
      "clearcoat": 0,
      "specular": 0.3,
      "sparkle": 0.2,
      "sparkleFraction": 0.6,
      "sparkleSpread": 1.08
    },
    "sprites": {
      "enabled": true,
      "sizeScale": 1.4,
      "tilt": 30,
      "normalStrength": 0.1,
      "frostFromAtlas": 1,
      "frostBoost": 0.15,
      "frostRoughness": 0.1,
      "edgeLight": 3,
      "alphaCut": 0.6,
      "seeThrough": 1
    },
    "repelFromObject": true,
    "repelStrength": 24,
    "repelRange": 1.49,
    "repelRadial": 0,
    "repelRadialRange": 1.6,
    "maxSpeed": 8,
    "lostRadius": 30,
    "densityShadowStrength": 0.6,
    "densityAOStrength": 0,
    "densityGrid": "64",
    "densityExtent": 10,
    "hazeIntensity": 0,
    "hazeSteps": 8,
    "inheritRotation": true,
    "inheritTime": 400,
    "strayCount": 40,
    "straySpeed": 0.06,
    "fragmentShadowMap": false
  },
  "healing": {
    "healDelay": 0,
    "returnAfter": 3,
    "waveTime": 0,
    "waveReach": 1,
    "waveJitter": 0.3,
    "returnMode": "spring",
    "returnSpring": 40,
    "returnDamping": 0.9,
    "returnRamp": 3,
    "returnDrag": 0.85,
    "returnMaxSpeed": 13,
    "landRadius": 0.2,
    "returnDuration": 1,
    "returnCurve": 0.27,
    "cellRestore": 60,
    "cellStragglers": 1,
    "growFromEdges": true,
    "breakup": 1,
    "breakupScale": 40,
    "alignToSurface": 1,
    "alignDistance": 0.52,
    "landedFade": 2,
    "landShrink": 0.3,
    "depositRadius": 3,
    "ghostReturnDistance": 0,
    "ghostFlightFraction": 0.45,
    "healRate": 3,
    "healGap": 0.5,
    "fallbackHeal": 0.275,
    "refrostTime": 4.9,
    "refrostStrength": 0.17,
    "resetFade": 50
  },
  "fracture": {
    "chunks": 600,
    "seed": 3,
    "hitPadding": 0.5,
    "eject": {
      "speed": 2.6,
      "spread": 0.7,
      "spin": 5,
      "jitter": 0.45,
      "backwardRatio": 0.2
    },
    "motion": {
      "drag": 1.3,
      "angularDrag": 1.6,
      "gravity": 0.3,
      "turbulence": 1.2,
      "turbulenceScale": 1.4,
      "maxSpeed": 10
    },
    "return": {
      "returnAfter": 1.4,
      "returnJitter": 0.9,
      "idleDelay": 0.6,
      "spring": 14,
      "damping": 1,
      "ramp": 0.5,
      "rotationRate": 6,
      "snapDistance": 0.015
    },
    "caps": {
      "frost": 0.85,
      "roughness": 0.8,
      "grain": 0.5,
      "darken": 0.15
    }
  },
  "lighting": {
    "key": {
      "color": "#ffffff",
      "intensity": 4.6,
      "elevation": 80.5,
      "azimuth": 72,
      "size": 0.91,
      "hoverBoost": 0.3
    },
    "fill": 0.19,
    "fillColor": "#ff0000",
    "fillGroundColor": "#d11f1f",
    "rim": 3.35,
    "rimColor": "#f0f8ff",
    "rimElevation": 89,
    "shadow": {
      "intensity": 0,
      "softness": 6,
      "samples": 1,
      "mapSize": "1024",
      "bias": -0.0006
    },
    "sway": {
      "amplitude": 0.15,
      "period": 9.5
    },
    "backdrop": {
      "top": "#2a2a2a",
      "mid": "#050505",
      "bottom": "#030303",
      "centerX": 0.5,
      "centerY": 1.5,
      "radius": 0.78,
      "falloff": 4,
      "noise": 3
    },
    "envSoftbox": 0.8,
    "envRim": 1.3,
    "envFill": 0.12
  },
  "camera": {
    "fov": 41,
    "distance": 9.05,
    "height": 1.2,
    "lookAtY": -0.15,
    "parallaxRange": 1.9,
    "parallaxSmoothing": 3,
    "rotateYaw": 16.5,
    "rotatePitch": 10,
    "rotationLag": 0.63,
    "baseYaw": 0,
    "basePitch": 0,
    "idleDelay": 4,
    "idleAmplitude": 4,
    "idlePeriod": 24
  },
  "post": {
    "dof": {
      "enabled": false,
      "focusBias": -0.43,
      "aperture": 4,
      "range": 12
    },
    "bloom": {
      "threshold": 3,
      "intensity": 0.045,
      "radius": 0.235
    },
    "monochrome": 0.52,
    "tonemap": "agx",
    "exposure": 1.08,
    "contrast": 1.285,
    "blackLift": 0.01,
    "vignette": {
      "strength": 0.325,
      "softness": 1.175,
      "radius": 1.245
    },
    "grain": {
      "strength": 0,
      "size": 1,
      "speed": 0,
      "shadowWeight": 0.28
    },
    "dither": true,
    "aaMode": "traa",
    "pixelRatioCap": 2
  },
  "performance": {
    "idleSkip": true,
    "adaptiveSteps": true,
    "voronoiCells": "27",
    "hazeResolution": "quarter",
    "turbulence": "fast",
    "gpuTimers": true
  },
  "debug": {
    "showErosionSlice": false,
    "erosionSliceZ": 0.5,
    "showDensityGrid": false,
    "showHitPoints": false,
    "colorByState": false,
    "colorBySize": false,
    "colorByAge": false,
    "wireframe": false,
    "freeze": false,
    "stats": true,
    "forceStroke": false
  },
  "hero4": {
    "frame": {
      "sameAsMain": false,
      "distance": 13.45,
      "height": 1.2,
      "lookAtX": 0,
      "lookAtY": 0.05,
      "fov": 29.5,
      "objectX": 0,
      "objectY": 1.26,
      "baseYaw": 0,
      "basePitch": 0,
      "rotateYaw": 40,
      "rotatePitch": 45,
      "parallaxRange": 15
    },
    "type": {
      "line1": "Hard to break.",
      "line2": "Easy to remember.",
      "weight": 500,
      "letterSpacing": -0.04,
      "lineHeight": 0.92,
      "lineFrac": 0.16,
      "sx": 0,
      "sy": -0.125,
      "depth": 1.3,
      "align": "center",
      "uppercase": false,
      "color": "#f2f2f2"
    },
    "copy": {
      "brand": "Frost",
      "nav": "Manifesto, Work",
      "cta": "Say hello",
      "wordmark": "Frost",
      "tagline": "A symbol that breaks and returns.",
      "subtitle": "An identity system built to take pressure. Break it, watch it come back, and know exactly what holds.",
      "cta1": "Start a project",
      "cta2": "See the work",
      "cue": "Scroll"
    },
    "wordmark": {
      "show": false,
      "wordmarkVw": 2.4,
      "weight": 600,
      "letterSpacing": 0.42,
      "sy": -0.34
    },
    "subtitle": {
      "show": true,
      "sy": -0.56,
      "size": 15,
      "maxWidth": 42
    },
    "ctas": {
      "show": true,
      "sy": -0.72,
      "gap": 14
    },
    "trigger": {
      "autoSweep": false,
      "first": 2,
      "interval": 10,
      "duration": 0.7,
      "radius": 0.5,
      "strength": 14,
      "cursorErodes": true,
      "clickResets": true
    },
    "style": {
      "textColor": "#f2f2f2",
      "mutedAlpha": 0.5,
      "showStats": false
    }
  }
}
source/src/dials/defaults.ts
// Every tunable in this piece lives here as a DialKit config.
// Baking chosen values = editing the first element of each tuple.
import type { DialConfig } from "dialkit";
const sel = <T extends string>(def: T, options: readonly T[]) => ({
  type: "select" as const,
  options: options as unknown as string[],
  default: def,
});
/** `?lite=1` lowers every heavy default (used for headless technical checks on software WebGPU). */
export const LITE =
  typeof location !== "undefined" && new URLSearchParams(location.search).has("lite");

export const presetsDefaults = {
  copyAllJson: { type: "action" as const, label: "Copy all params as JSON" },
} satisfies DialConfig;

export const SHAPES = ["torus", "sphere", "roundedBox", "pyramid", "icosahedron", "logo"] as const;
export type ShapeName = (typeof SHAPES)[number];

export const shapeDefaults = {
  shape: sel("torus", SHAPES),
  size: [1.0, 0.5, 1.6, 0.01],
  tubeRatio: [0.45, 0.2, 0.7, 0.005],
  deformAmplitude: [0.045, 0, 0.2, 0.001],
  deformFrequency: [1.1, 0.3, 4, 0.05],
  segments: [256, 64, 512, 8],
  seed: [7, 0, 999, 1],
  // world-space offset of the object (hero pages override this from their own panel)
  offsetX: [0, -5, 5, 0.01],
  offsetY: [0, -4, 4, 0.01],
  offsetZ: [0, -4, 4, 0.01],
  // extrusion of /public/logo.svg when shape = logo (fractions are relative to `logo.width`)
  logo: {
    _collapsed: true,
    width: [2.6, 0.8, 5, 0.05],
    depth: [0.22, 0.02, 1, 0.005],
    bevelThickness: [0.06, 0, 0.3, 0.002],
    bevelSize: [0.05, 0, 0.3, 0.002],
    bevelOffset: [0, -0.1, 0.1, 0.002],
    bevelSegments: [5, 1, 16, 1],
    cornerRadius: [0.03, 0, 0.15, 0.001],
    curveSegments: [24, 2, 64, 1],
    creaseAngle: [40, 0, 180, 1],
    sdfRes: [64, 32, 128, 8],
  },
} satisfies DialConfig;

export const iceDefaults = {
  ior: [1.36, 1.0, 2.0, 0.005],
  dispersion: [0, 0, 0.3, 0.005],
  thicknessScale: [1.0, 0.1, 3, 0.01],
  attenuationDistance: [0.9, 0.05, 5, 0.01],
  attenuationColor: "#cfd2d4",
  baseRoughness: [0.05, 0, 0.5, 0.005],
  frost: {
    scale: [1.6, 0.2, 6, 0.05],
    threshold: [0.5, 0, 1, 0.01],
    softness: [0.28, 0.01, 1, 0.01],
    roughness: [0.65, 0, 1, 0.01],
    diffuse: [0.55, 0, 1, 0.01],
    crystalBump: [0.12, 0, 1, 0.01],
    crystalScale: [28, 4, 80, 1],
  },
  cracks: {
    largeScale: [1.6, 0.3, 8, 0.05],
    warp: [0.45, 0, 1.5, 0.01],
    warpScale: [1.4, 0.2, 6, 0.05],
    coverage: [0.55, 0, 1, 0.01],
    regionScale: [0.8, 0.1, 4, 0.05],
    regionCoverage: [0.7, 0, 1, 0.01],
    veinScale: [5, 1, 20, 0.25],
    veinContrast: [0.7, 0, 1, 0.01],
    width: [0.004, 0.0005, 0.02, 0.0005],
    brightness: [0.4, 0, 3, 0.01],
    darkness: [0.5, 0, 1, 0.01],
    refraction: [0.02, 0, 0.1, 0.001],
    surfaceStrength: [0.35, 0, 1, 0.01],
    steps: [14, 4, 32, 1],
    fineCracks: true,
    fineScale: [5.5, 2, 20, 0.1],
    fineAmount: [0.45, 0, 1, 0.01],
    fineCoverage: [0.35, 0, 1, 0.01],
    fineNearLarge: [0.75, 0, 1, 0.01],
  },
  smudges: {
    amount: [0.6, 0, 1, 0.01],
    coverage: [0.55, 0, 1, 0.01],
    maskScale: [1.2, 0.1, 6, 0.05],
    anisotropy: [6, 1, 20, 0.5],
    roughness: [0.35, 0, 1, 0.01],
    whiteness: [0.08, 0, 0.5, 0.005],
    scale: [3.2, 0.5, 10, 0.1],
  },
  bumps: {
    microBump: [0.25, 0, 1, 0.005],
    microScale: [60, 10, 200, 1],
    microCoverage: [0.6, 0, 1, 0.01],
    rippleBump: [0.3, 0, 1, 0.01],
    rippleScale: [9, 2, 30, 0.5],
    maskScale: [1.5, 0.1, 6, 0.05],
  },
  // emissive glow of partially eroded (crumbling) ice along the view ray; the old value was 1 and blew out
  crumbleGlow: [0.15, 0, 2, 0.01],
  // how much the crumbly edge band around a cut whitens and opacifies the ice (0 = stays glassy)
  edgeWhiteness: [0.5, 0, 1, 0.01],
  clearcoat: [0.3, 0, 1, 0.01],
  clearcoatRoughness: [0.08, 0, 1, 0.005],
  envIntensity: [0.55, 0, 3, 0.01],
  specularIntensity: [1.0, 0, 2, 0.01],
  interiorScatter: [0.25, 0, 1, 0.01],
} satisfies DialConfig;

export const erosionDefaults = {
  resolution: sel("128", ["64", "96", "128", "192"] as const),
  brushRadius: [0.28, 0.05, 0.8, 0.005],
  brushSoftness: [0.5, 0, 1, 0.01],
  brushStrength: [2.2, 0.1, 8, 0.05],
  brushSpeedRef: [3.0, 0.2, 12, 0.1],
  brushNoise: [0.55, 0, 1, 0.01],
  brushNoiseScale: [9, 2, 30, 0.5],
  throughDepth: [1.0, 0, 1, 0.01],
  // ice breaks along cells: the brush is evaluated per break cell (flat facets) and mixed with the smooth capsule
  cellSnap: [0.85, 0, 1, 0.01],
  breakCellScale: [3.0, 0.5, 8, 0.1],
  crumbleRate: [1.6, 0, 6, 0.05],
  crumbleCrackBias: [2.5, 0, 6, 0.05],
  crumbleDuration: [0.5, 0, 2, 0.01],
  // the surface is gone where erosion exceeds cutThreshold (narrow stochastic transition, resolved by TRAA);
  // the crumbly edge band sits just below it, edgeWidth wide
  cutThreshold: [0.7, 0.3, 0.98, 0.01],
  cutSoftness: [0.04, 0.005, 0.3, 0.005],
  edgeWidth: [0.35, 0.05, 0.8, 0.01],
  // the cut surface seen through a hole: ray-march steps through the field, and extra frost on the fresh break
  interiorSteps: [24, 8, 64, 1],
  interiorFrost: [0, 0, 1, 0.01],
  edgeInset: [0.06, 0, 0.3, 0.005],
  edgeBump: [0.6, 0, 2, 0.01],
  edgeBumpScale: [55, 10, 160, 1],
} satisfies DialConfig;

export const powderDefaults = {
  particleCount: sel("1M", ["100k", "250k", "500k", "1M", "2M"] as const),
  // fraction of the eroded volume that becomes visible powder (the rest simply vanishes)
  amount: [0.3, 0.02, 1, 0.01],
  grainSizeMultiplier: [1.0, 0.2, 4, 0.05],
  // where the interior sample points sit: fraction placed within `nearSurfaceDepth` of the surface
  nearSurfaceFraction: [0.6, 0, 1, 0.01],
  nearSurfaceDepth: [0.2, 0.05, 0.5, 0.01],
  grainSizes: {
    // four classes, tiny -> large; the ratios are normalised, the leftover ~1% is large fragments
    tinyDustRatio: [0.7, 0, 1, 0.01],
    smallGrainRatio: [0.22, 0, 1, 0.01],
    mediumClumpRatio: [0.07, 0, 1, 0.01],
    tinyDustSize: [0.0055, 0.001, 0.03, 0.0005],
    smallGrainSize: [0.011, 0.002, 0.05, 0.0005],
    mediumClumpSize: [0.022, 0.005, 0.1, 0.001],
    largeFragmentSize: [0.05, 0.01, 0.2, 0.001],
    sizeJitter: [0.45, 0, 1, 0.01],
  },
  grainVariants: [5, 1, 6, 1],
  // flat-shaded low-poly splinters instead of smooth blobs (each face catches the key light separately)
  facetedGrains: false,
  // grains never render below this many pixels (sub-pixel dust flickers under TRAA)
  minPixelSize: [1.5, 0, 4, 0.05],
  ejectSpeed: [1.1, 0, 4, 0.01],
  // grains released after the cursor stopped (crumble) still get at least this kick, units/s
  minEjectSpeed: [1.0, 0, 4, 0.01],
  ejectSpread: [0.5, 0, 3, 0.01],
  ejectTurbulence: [0.8, 0, 4, 0.01],
  backwardRatio: [0.25, 0, 1, 0.01],
  clumpSpeedJitter: [0.5, 0, 1, 0.01],
  drag: [1.4, 0, 8, 0.01],
  gravity: [0.12, 0, 2, 0.005],
  turbulence: [0.9, 0, 4, 0.01],
  turbulenceScale: [1.8, 0.2, 8, 0.05],
  turbulenceDecay: [0.6, 0.05, 4, 0.01],
  clumpCohesion: [2.4, 0, 10, 0.05],
  clumpCount: [24000, 500, 100000, 500],
  settleTime: [3.2, 0.3, 12, 0.05],
  settledDrift: [0.012, 0, 0.1, 0.001],
  tumble: [2.5, 0, 12, 0.05],
  baseTone: "#dcdcdc",
  wrap: [0.45, 0, 1, 0.01],
  // ice-shard look of the grains: the backdrop shows through their centres, rims stay bright (Fresnel),
  // real specular / clearcoat from the lights, and per-grain sparkle
  fragments: {
    translucency: [0.6, 0, 1, 0.01],
    throughTint: "#aeb6bb",
    fresnelPower: [3, 0.5, 8, 0.1],
    roughness: [0.32, 0, 1, 0.01],
    clearcoat: [0.6, 0, 1, 0.01],
    specular: [1.0, 0, 3, 0.01],
    sparkle: [0.6, 0, 3, 0.01],
    sparkleFraction: [0.35, 0, 1, 0.01],
    sparkleSpread: [0.5, 0, 1.5, 0.01],
  },
  // repulsion from the object: an acceleration away from the surface (full strength inside, smooth
  // falloff over repelRange outside), added to the other forces so grains drift out organically
  // shard sprites: every grain is a camera-facing quad textured from the shard atlas (16 photographed-looking
  // ice shards: silhouette, bevel normal map, frost structure, thickness). Off = the low-poly mesh grains.
  sprites: {
    enabled: true,
    sizeScale: [1.4, 0.3, 4, 0.05],
    // random tilt off the camera plane (degrees): 0 = flat billboards, more = thinner, more varied shards
    tilt: [30, 0, 70, 1],
    normalStrength: [1, 0, 2.5, 0.05],
    // how much the atlas' frost structure drives the look (0 = every shard clear glass)
    frostFromAtlas: [1, 0, 1, 0.01],
    frostBoost: [0.35, 0, 1.5, 0.01],
    frostRoughness: [0.7, 0, 1, 0.01],
    // key light caught by the thin bevelled rim
    edgeLight: [0.6, 0, 3, 0.01],
    alphaCut: [0.2, 0.05, 0.6, 0.01],
    // real transparency: how much the clear parts of a shard show what is behind them (rebuilds the powder)
    seeThrough: [0, 0, 1, 0.01],
  },
  repelFromObject: false,
  repelStrength: [6, 0, 30, 0.1],
  repelRange: [0.35, 0.02, 2, 0.01],
  // plus a push straight away from the object's centre, so grains never settle in concave pockets of
  // the shape (the surface-normal push alone balances out inside a pocket); range in object radii
  repelRadial: [12, 0, 60, 0.5],
  repelRadialRange: [1.6, 1, 4, 0.05],
  // hard cap on grain speed (units/s): repulsion and ejection can never fling a grain off-screen
  maxSpeed: [8, 1, 40, 0.1],
  // a grain farther than this from the object (world units, far off screen) is brought back to its rest
  lostRadius: [30, 5, 100, 1],
  densityShadowStrength: [0.75, 0, 2, 0.01],
  densityAOStrength: [0.55, 0, 2, 0.01],
  densityGrid: sel("64", ["32", "48", "64"] as const),
  densityExtent: [4.2, 2, 10, 0.1],
  hazeIntensity: [0.22, 0, 1.5, 0.005],
  hazeSteps: [28, 8, 64, 1],
  inheritRotation: true,
  inheritTime: [220, 0, 1500, 10],
  strayCount: [40, 0, 200, 1],
  straySpeed: [0.06, 0, 0.5, 0.005],
  fragmentShadowMap: true,
} satisfies DialConfig;

export const healingDefaults = {
  healDelay: [3.5, 0, 12, 0.1],
  // a grain also turns back on its own this long after it left (0 = only when the cursor is idle),
  // so pieces keep cycling while you keep breaking
  returnAfter: [3.0, 0, 20, 0.1],
  // grains fly home in a wave: the ones closest to the object first, the far plume last, spread over
  // waveTime (a grain waveReach units away waits the full waveTime)
  waveTime: [2.0, 0, 8, 0.05],
  waveReach: [3.0, 0.2, 10, 0.1],
  waveJitter: [0.6, 0, 3, 0.05],
  // return motion: 'spring' = a damped spring toward the rest that ramps in while the grain is still
  // flying out (continuous decelerate / turn / accelerate back); 'path' = the eased curved path
  returnMode: sel("spring", ["spring", "path"] as const),
  returnSpring: [6, 0.5, 40, 0.1],
  returnDamping: [0.9, 0.2, 2, 0.01],
  returnRamp: [0.6, 0, 3, 0.05],
  // share of the powder drag that still acts during the spring return (absorbs ballistic speed)
  returnDrag: [0.25, 0, 1, 0.01],
  // speed cap on the way home (world units / s): keeps far grains visible instead of crossing the screen in a frame
  returnMaxSpeed: [12, 1, 60, 0.5],
  landRadius: [0.03, 0.005, 0.2, 0.005],
  returnDuration: [2.4, 0.2, 8, 0.05],
  returnCurve: [0.35, 0, 1.5, 0.01],
  // a landing grain restores its whole break cell (the facet it broke out of) at this rate (1/s; 30 = instant)
  cellRestore: [30, 0, 60, 0.5],
  // a break cell refills only once all its grains are home; allow this many still in flight
  cellStragglers: [0, 0, 20, 1],
  // the refilling cell grows back from what is already solid (hole floor, walls, landed shards) at `cellRestore`,
  // one voxel layer at a time, instead of the whole facet popping in
  growFromEdges: true,
  // per-voxel timing variation of every refill, cleanup and refrost timer (0 = a break cell changes look all
  // at once, 1 = it dissolves in small patches); breakupScale sets the patch size (higher = smaller)
  breakup: [0.7, 0, 1, 0.01],
  breakupScale: [10, 1, 40, 0.5],
  // returning shards turn to lie flat on the surface they land on (0 = keep facing the camera)
  alignToSurface: [1, 0, 1, 0.01],
  // distance from home over which a shard turns (world units)
  alignDistance: [0.6, 0.05, 3, 0.01],
  // FROST: shape of the alignment over the flight home (1 = linear, < 1 early, > 1 late)
  alignCurve: [1, 0.2, 4, 0.05],
  // a landed shard lies on the surface until its voxel is solid again, then fades over this many seconds
  landedFade: [0.25, 0, 2, 0.01],
  // shrink a shard over the last part of its flight (0 = it arrives at full size)
  landShrink: [0, 0, 0.3, 0.005],
  // plus a small sphere of voxels around its rest position (in voxels); 0 = cells only
  depositRadius: [0.5, 0, 3, 0.25],
  // optional: hidden material comes back as extra dust materialising this far from the surface (0 = off;
  // it never rebuilds anything by itself, only the grains that flew out do)
  ghostReturnDistance: [0, 0, 1.5, 0.01],
  ghostFlightFraction: [0.45, 0.1, 1, 0.01],
  // voxels without a grain follow their healed neighbours at this rate, staying `healGap` behind
  healRate: [0.6, 0.02, 3, 0.01],
  healGap: [0.04, 0, 0.5, 0.01],
  // cleanup rate for voxels no grain came back to; starts only after the wave has fully landed
  fallbackHeal: [0.15, 0, 0.5, 0.005],
  refrostTime: [4.0, 0.2, 12, 0.1],
  refrostStrength: [0.3, 0, 1, 0.01],
  resetFade: [400, 50, 2000, 10],
} satisfies DialConfig;

export const lightingDefaults = {
  key: {
    color: "#ffffff",
    intensity: [3.2, 0, 12, 0.05],
    elevation: [62, 10, 89, 0.5],
    azimuth: [18, -90, 90, 0.5],
    size: [1.0, 0.2, 3, 0.01],
    hoverBoost: [0.06, 0, 0.3, 0.005],
  },
  fill: [0.18, 0, 1.5, 0.005],
  fillColor: "#ffffff",
  fillGroundColor: "#262626",
  rim: [0.9, 0, 5, 0.01],
  rimColor: "#ffffff",
  rimElevation: [55, 0, 89, 0.5],
  shadow: {
    intensity: [1.0, 0, 1, 0.01],
    softness: [6, 0, 24, 0.25],
    samples: [12, 1, 32, 1],
    mapSize: sel("2048", ["1024", "2048", "4096"] as const),
    bias: [-0.0006, -0.005, 0.005, 0.0001],
  },
  sway: { amplitude: [0.03, 0, 0.15, 0.001], period: [12, 2, 40, 0.5] },
  backdrop: {
    top: "#2a2a2a",
    mid: "#050505",
    bottom: "#030303",
    centerX: [0.5, 0, 1, 0.005],
    centerY: [1.02, 0.5, 1.5, 0.005],
    radius: [0.55, 0.1, 1.5, 0.005],
    falloff: [1.6, 0.5, 4, 0.01],
    noise: [1.0, 0, 3, 0.05],
  },
  envSoftbox: [1.0, 0, 3, 0.01],
  envRim: [0.5, 0, 3, 0.01],
  envFill: [0.12, 0, 1, 0.005],
} satisfies DialConfig;

export const cameraDefaults = {
  fov: [30, 15, 60, 0.5],
  distance: [13.5, 4, 24, 0.05],
  height: [1.2, -3, 6, 0.01],
  lookAtY: [-0.15, -2, 2, 0.01],
  parallaxRange: [3, 0, 10, 0.1],
  parallaxSmoothing: [0.8, 0.05, 3, 0.01],
  rotateYaw: [35, 0, 90, 0.5],
  rotatePitch: [10, 0, 45, 0.5],
  rotationLag: [1.2, 0.05, 4, 0.01],
  baseYaw: [0, -180, 180, 0.5],
  basePitch: [0, -90, 90, 0.5],
  idleDelay: [4, 0, 15, 0.1],
  idleAmplitude: [4, 0, 15, 0.1],
  idlePeriod: [24, 4, 80, 0.5],
} satisfies DialConfig;

export const postDefaults = {
  dof: {
    enabled: true,
    focusBias: [0, -3, 3, 0.01],
    aperture: [0.9, 0, 4, 0.01],
    range: [3.0, 0.2, 12, 0.05],
  },
  bloom: {
    threshold: [1.2, 0, 3, 0.01],
    intensity: [0.15, 0, 1.5, 0.005],
    radius: [0.15, 0, 1, 0.005],
  },
  monochrome: [1.0, 0, 1, 0.01],
  tonemap: sel("agx", ["agx", "aces", "neutral", "linear"] as const),
  exposure: [1.0, 0.1, 4, 0.01],
  contrast: [1.08, 0.6, 1.6, 0.005],
  blackLift: [0.02, 0, 0.1, 0.001],
  vignette: {
    strength: [0.2, 0, 1, 0.005],
    softness: [0.7, 0.1, 1.5, 0.005],
    radius: [0.85, 0.2, 1.6, 0.005],
  },
  grain: {
    strength: [0.03, 0, 0.15, 0.001],
    size: [1, 1, 4, 1],
    speed: [1, 0, 3, 0.05],
    shadowWeight: [0.8, 0, 1, 0.01],
    // background-only grain against banding in recorded video: amplitude (0.04 = about 10 grey levels), block
    // size in device pixels (use 3+ when the recording is downscaled), 0 speed = static (survives encoding best)
    backgroundStrength: [0, 0, 0.25, 0.001],
    backgroundSize: [2, 1, 8, 1],
    backgroundSpeed: [0, 0, 3, 0.05],
  },
  dither: true,
  aaMode: sel("traa", ["traa", "none"] as const),
  pixelRatioCap: [2, 0.5, 2, 0.25],
} satisfies DialConfig;

/** V2: real fragmentation. The object is pre-fractured into rigid chunks that fly out and come back. */
export const fractureDefaults = {
  chunks: [600, 40, 3000, 10],
  seed: [3, 0, 999, 1],
  // a chunk breaks off when the brush passes within brushRadius (+ this share of the chunk's size)
  hitPadding: [0.5, 0, 2, 0.05],
  eject: {
    speed: [2.6, 0, 10, 0.05],
    spread: [0.7, 0, 4, 0.05],
    spin: [5, 0, 20, 0.1],
    jitter: [0.45, 0, 1, 0.01],
    backwardRatio: [0.2, 0, 1, 0.01],
  },
  motion: {
    drag: [1.3, 0, 8, 0.01],
    angularDrag: [1.6, 0, 8, 0.01],
    gravity: [0.3, 0, 3, 0.01],
    turbulence: [1.2, 0, 6, 0.05],
    turbulenceScale: [1.4, 0.2, 6, 0.05],
    maxSpeed: [10, 1, 40, 0.1],
  },
  return: {
    // a chunk turns back this long after it broke off (jittered), or sooner when the cursor is idle
    returnAfter: [1.4, 0, 20, 0.05],
    returnJitter: [0.9, 0, 5, 0.05],
    idleDelay: [0.6, 0, 10, 0.05],
    spring: [14, 0.5, 60, 0.1],
    damping: [1.0, 0.2, 2, 0.01],
    ramp: [0.5, 0, 3, 0.05],
    rotationRate: [6, 0.5, 30, 0.1],
    snapDistance: [0.015, 0.002, 0.2, 0.001],
  },
  caps: {
    // inner (fracture) faces: fresh break = frosted, crumbly
    frost: [0.85, 0, 1, 0.01],
    roughness: [0.8, 0, 1, 0.01],
    grain: [0.5, 0, 2, 0.01],
    darken: [0.15, 0, 1, 0.01],
  },
} satisfies DialConfig;

export const performanceDefaults = {
  // Dynamic resolution now scales the expensive scene/MRT pass. When nativePostEffects is on the
  // final grade, vignette, grain and dither stay at the output DPR and only the 3D work is upscaled.
  adaptiveResolution: true,
  targetFps: [60, 30, 120, 1],
  minPixelRatio: [0.75, 0.5, 2, 0.05],
  sceneResolutionScale: [1, 0.5, 1, 0.05],
  dynamicSceneResolution: true,
  minSceneResolutionScale: [0.35, 0.25, 1, 0.05],
  upscaler: sel("taau", ["taau", "fsr1", "bilinear", "native"] as const),
  upscaleSharpness: [0.2, 0, 1, 0.05],
  nativePostEffects: true,
  idleSkip: false, // skip field / particle / haze / density work while nothing is happening (lossless)
  adaptiveSteps: false, // crack raymarch: fewer steps where the ice is thin (same step length as the thickest chord)
  voronoiCells: sel("27", ["27", "8"] as const), // 8 = jittered-lattice Voronoi, ~3x cheaper cracks, occasional phantom sheet
  hazeResolution: sel("full", ["full", "half", "quarter"] as const),
  turbulence: sel("full", ["full", "fast"] as const), // fast = swirl from one noise sample instead of a 6-sample curl
  gpuTimers: true,
} satisfies DialConfig;

export const debugDefaults = {
  _collapsed: true,
  showErosionSlice: false,
  erosionSliceZ: [0.5, 0, 1, 0.01],
  showDensityGrid: false,
  showHitPoints: false,
  colorByState: false,
  colorBySize: false,
  colorByAge: false,
  wireframe: false,
  freeze: false,
  stepOnce: { type: "action" as const, label: "Step one frame" },
  stats: true,
  forceStroke: false,
} satisfies DialConfig;

// ---------------------------------------------------------------------------------------------
// Baked overrides: paste a "Copy all params as JSON" export into ./baked.json and every default
// above is overridden at boot (tuple[0] / select default / plain value). Nothing persists otherwise.
import baked from "./baked.json";
function applyBaked(cfg: any, values: any) {
  if (!values || typeof values !== "object") return;
  for (const [k, v] of Object.entries(values)) {
    if (!Object.hasOwn(cfg, k) || k.startsWith("_")) continue;
    const c = cfg[k];
    if (Array.isArray(c)) {
      if (typeof v === "number") c[0] = v;
    } else if (c && typeof c === "object" && "type" in c) {
      if ((c as any).type !== "action") (c as any).default = v;
    } else if (c && typeof c === "object") applyBaked(c, v);
    else cfg[k] = v;
  }
}
/** Apply the baked values stored under `key` (e.g. a hero panel) to a DialKit config, in place. */
export function withBaked<T>(key: string, cfg: T): T {
  applyBaked(cfg, (baked as any)[key]);
  return cfg;
}
export const BAKED_FOLDERS: Record<string, any> = {
  shape: shapeDefaults,
  ice: iceDefaults,
  erosion: erosionDefaults,
  powder: powderDefaults,
  healing: healingDefaults,
  fracture: fractureDefaults,
  lighting: lightingDefaults,
  camera: cameraDefaults,
  post: postDefaults,
  performance: performanceDefaults,
  debug: debugDefaults,
};
for (const [folder, cfg] of Object.entries(BAKED_FOLDERS)) applyBaked(cfg, (baked as any)[folder]);
if (typeof location !== "undefined" && new URLSearchParams(location.search).has("idleskip"))
  (performanceDefaults as any).idleSkip = true;
if (LITE) fractureDefaults.chunks[0] = 120;
if (LITE) {
  // headless / software-GPU checks: shrink everything heavy after the baked values are applied
  shapeDefaults.segments[0] = 96;
  iceDefaults.cracks.steps[0] = 6;
  (iceDefaults.cracks as any).fineCracks = false;
  iceDefaults.dispersion[0] = 0;
  erosionDefaults.resolution.default = "64";
  powderDefaults.particleCount.default = "100k";
  powderDefaults.densityGrid.default = "32";
  powderDefaults.hazeSteps[0] = 8;
}
source/src/dials/store.ts
// FROST: the DialKit store without React / DialKit. `D` holds every folder's defaults with baked.json applied,
// resolved exactly as the experiment's resolveDefaults() does (tuple[0] / select default / plain value).
// The composition writes its hero-07 overrides into `D` once at boot and bumps `D.version`; there is no live panel.
import { MATERIAL_FEATURES } from "../ice/features";
import * as defs from "./defaults";

export type ShapeD = any;
export type IceD = any;
export type ErosionD = any;
export type PowderD = any;
export type HealingD = any;
export type LightingD = any;
export type CameraD = any;
export type PostD = any;
export type PerformanceD = any;
export type FractureD = any;
export type DebugD = any;

function resolveDefaults(cfg: any): any {
  const out: any = {};
  for (const [k, v] of Object.entries(cfg)) {
    if (k.startsWith("_")) continue;
    if (Array.isArray(v)) out[k] = v[0];
    else if (typeof v === "object" && v !== null && "type" in v) {
      const t = (v as any).type;
      if (t === "action") continue;
      out[k] = (v as any).default ?? ((v as any).options ? (v as any).options[0] : undefined);
    } else if (typeof v === "object" && v !== null) out[k] = resolveDefaults(v);
    else out[k] = v;
  }
  return out;
}

/** The store. Systems read `D.camera.fov` etc. every frame. */
export const D = {
  shape: resolveDefaults(defs.shapeDefaults),
  ice: { ...resolveDefaults(defs.iceDefaults), features: { ...MATERIAL_FEATURES } },
  erosion: resolveDefaults(defs.erosionDefaults),
  powder: resolveDefaults(defs.powderDefaults),
  healing: resolveDefaults(defs.healingDefaults),
  lighting: resolveDefaults(defs.lightingDefaults),
  camera: resolveDefaults(defs.cameraDefaults),
  post: resolveDefaults(defs.postDefaults),
  performance: resolveDefaults(defs.performanceDefaults),
  fracture: resolveDefaults(defs.fractureDefaults),
  debug: resolveDefaults(defs.debugDefaults),
  version: 0,
};

const actionListeners = new Set<(path: string) => void>();
export function onDialAction(fn: (path: string) => void) {
  actionListeners.add(fn);
  return () => {
    actionListeners.delete(fn);
  };
}
export const extraPanels: Record<string, () => any> = {};
export const toast = new EventTarget();
source/src/erosion/AssemblyField.ts
/** Arrival-driven headline coverage. Each region belongs to an actual returning shard, so
 * disconnected glyphs and sparse powder never need a timed neighbour/global reveal. */
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
import type { ErosionField } from "./ErosionField";
const {
  Fn,
  instancedArray,
  instanceIndex,
  uint,
  int,
  float,
  ivec3,
  vec3,
  vec4,
  If,
  Loop,
  atomicStore,
  atomicLoad,
  atomicMin,
  clamp,
  floor,
  length,
  dot,
  select,
  texture3D,
  textureStore,
} = tsl;

export const ASSEMBLY_GRID = 32;
export const assemblyReach = (bound: number) => (4 * bound) / ASSEMBLY_GRID;
const NONE = 0xffffffff;

export class AssemblyField {
  private readonly seeds = instancedArray(ASSEMBLY_GRID ** 3, "uint").toAtomic();
  private readonly owners = [
    instancedArray(ASSEMBLY_GRID ** 3, "uint"),
    instancedArray(ASSEMBLY_GRID ** 3, "uint"),
  ];
  private readonly prepareNodes: any[];
  private readonly updateNode: any;

  constructor(
    private readonly erosion: ErosionField,
    count: number,
    buffers: Record<string, any>,
    u: any,
  ) {
    const G = ASSEMBLY_GRID,
      n = G ** 3,
      i = instanceIndex,
      bound = erosion.bound;
    const coord = (idx: any, res: number) =>
      ivec3(idx.mod(uint(res)), idx.div(uint(res)).mod(uint(res)), idx.div(uint(res * res)));
    const index = (c: any) =>
      uint(c.x)
        .add(uint(c.y).mul(uint(G)))
        .add(uint(c.z).mul(uint(G * G)));
    const grid = (p: any) =>
      clamp(
        ivec3(
          floor(
            p
              .div(2 * bound)
              .add(0.5)
              .mul(G),
          ),
        ),
        ivec3(0),
        ivec3(G - 1),
      );
    const at = (c: any) =>
      vec3(c)
        .add(0.5)
        .div(G)
        .sub(0.5)
        .mul(2 * bound);
    const clear = Fn(() => {
      atomicStore(this.seeds.element(i), uint(NONE));
    })().compute(n, [64]);
    const seed = Fn(() => {
      const state = buffers.meta.element(i).x;
      // Only shards genuinely in flight may own target volume. No invisible ghost arrivals.
      If(state.greaterThan(0.5).and(state.lessThan(2.5)), () => {
        atomicMin(this.seeds.element(index(grid(buffers.rest.element(i).xyz))), uint(i));
      });
    })().compute(count, [64]);
    const copy = Fn(() => {
      this.owners[0].element(i).assign(atomicLoad(this.seeds.element(i)));
    })().compute(n, [64]);
    this.prepareNodes = [clear, seed, copy];
    let source = 0;
    // Fixed, small ownership grid: six ping-pong passes on retarget only, independent of
    // erosion resolution and particle count. Extra unit pass closes jump-flood edge cases.
    for (const jump of [16, 8, 4, 2, 1, 1]) {
      const read = this.owners[source],
        write = this.owners[1 - source];
      this.prepareNodes.push(
        Fn(() => {
          const c = coord(i, G).toVar(),
            p = at(c).toVar();
          const best = uint(NONE).toVar(),
            distance = float(1e30).toVar();
          const range = { start: int(-1), end: int(1), condition: "<=" };
          Loop(range, range, range, ({ i: x, j: y, k: z }: any) => {
            const cc = clamp(c.add(ivec3(x, y, z).mul(jump)), ivec3(0), ivec3(G - 1));
            const candidate = read.element(index(cc)).toVar();
            If(candidate.notEqual(uint(NONE)), () => {
              const delta = buffers.rest.element(candidate).xyz.sub(p);
              const d = dot(delta, delta).toVar();
              If(d.lessThan(distance).or(d.equal(distance).and(candidate.lessThan(best))), () => {
                best.assign(candidate);
                distance.assign(d);
              });
            });
          });
          write.element(i).assign(best);
        })().compute(n, [64]),
      );
      source = 1 - source;
    }
    const owner = this.owners[source],
      R = erosion.res;
    const previous = texture3D(erosion.tex);
    this.updateNode = Fn(() => {
      const c = coord(i, R).toVar();
      const p = vec3(c)
        .add(0.5)
        .div(R)
        .sub(0.5)
        .mul(2 * bound)
        .toVar();
      const id = owner.element(index(grid(p))).toVar();
      const old = previous.load(c).level(0).toVar();
      const e = old.r.toVar();
      If(id.notEqual(uint(NONE)), () => {
        const rest = buffers.rest.element(id).xyz.toVar();
        const target = u.model.mul(vec4(rest, 1)).xyz;
        const distance = length(buffers.pos.element(id).xyz.sub(target));
        const state = buffers.meta.element(id).x;
        // A spatial contact front expands around this shard's own home as it approaches.
        // No elapsed-time fade, cell-wide wait, stochastic refill, or neighbour propagation.
        const localDistance = length(p.sub(rest));
        const contact = u.landRadius.add(
          float(assemblyReach(bound)).div(float(1).add(localDistance.div((2 * bound) / G))),
        );
        const returning = state
          .greaterThan(1.5)
          .and(state.lessThan(2.5))
          .or(state.greaterThan(4.5))
          .or(state.lessThan(0.5));
        If(returning.and(distance.lessThanEqual(contact)), () => {
          e.assign(0);
        });
      });
      textureStore(
        erosion.scratch,
        c,
        vec4(e, 0, select(e.lessThan(0.5), float(1), float(0)), 0),
      ).toWriteOnly();
    })().compute(R ** 3, [64]);
  }

  prepare(renderer: THREE.WebGPURenderer) {
    for (const node of this.prepareNodes) renderer.compute(node);
  }
  update(renderer: THREE.WebGPURenderer) {
    renderer.compute(this.updateNode);
    this.erosion.commitAssembly(renderer);
  }
  dispose() {
    for (const b of [this.seeds, ...this.owners]) b.value.dispose?.();
  }
}
source/src/erosion/ErosionField.ts
// The erosion field: a 3D storage texture in object space (rgba16float: r = erosion 0..1, g = refrost).
// Cursor strokes splat capsules into it (compute), erosion crumbles along the crack network for a
// short while after a stroke, and after `healDelay` the field decays back to 0 with a frost-first refill.
// One canonical texture `tex` is read by every consumer; `scratch` receives the step, then is copied back.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
const {
  Fn,
  vec3,
  vec4,
  float,
  uniform,
  uniformArray,
  instanceIndex,
  texture3D,
  textureStore,
  uvec3,
  ivec3,
  int,
  uint,
  If,
  Loop,
  max,
  min,
  length,
  dot,
  clamp,
  smoothstep,
  mix,
  abs,
  select,
  instancedArray,
  atomicAdd,
  atomicMax,
  atomicStore,
  atomicLoad,
} = tsl;
import type { ShapeSpec } from "../shape/sdf";
import { hash31, voronoiEdge, voronoiCell, gnoise, saturate } from "../tsl/noise";
import { D } from "../dials/store";
import { sim } from "../core/state";

type N = any;
export const MAX_SEGMENTS = 12;

export class ErosionField {
  /** Floor (object units) for the erodable shell just outside the distance-field surface; set before construction. */
  static extraShell = 0;
  readonly res: number;
  readonly bound: number;
  readonly tex: THREE.Storage3DTexture;
  readonly scratch: THREE.Storage3DTexture;
  readonly crackTex: THREE.Storage3DTexture;
  /** Baked break cells: xyz = cell centre (object space), w = cell hash. The brush snaps to these. */
  readonly cellTex: THREE.Storage3DTexture;
  readonly u = {
    reconstruct: uniform(0),
    dt: uniform(0),
    time: uniform(0),
    heal: uniform(0),
    healRate: uniform(0.6),
    healGap: uniform(0.12),
    fallbackHeal: uniform(0.04),
    fallbackOn: uniform(0),
    refrostTime: uniform(4),
    cellRestore: uniform(30),
    growEdges: uniform(1),
    breakup: uniform(0.7),
    breakupScale: uniform(10),
    crumbleRate: uniform(1.6),
    crumbleCrackBias: uniform(2.5),
    crumbleUntil: uniform(-1),
    brushNoise: uniform(0.55),
    brushNoiseScale: uniform(9),
    brushSoftness: uniform(0.5),
    segCount: uniform(0),
    seed: uniform(0),
    crackScale: uniform(2.2),
    crackWarp: uniform(0.45),
    crackWarpScale: uniform(1.4),
    crackCoverage: uniform(0.55),
    breakScale: uniform(3),
    cellSnap: uniform(0.85),
  };
  readonly segments = uniformArray(
    Array.from({ length: MAX_SEGMENTS * 2 }, () => new THREE.Vector4()),
  );
  private stepNode: any;
  private copyNode: any;
  private cellClearNode: any;
  private flightClearNode: any;
  private clearNode: any;
  private fillNode: any;
  private healClearNode: any;
  private bakeNode: any;
  private statsNode: any;
  private statsClearNode: any;
  private statsBuf: any;
  private statsPending = false;
  /** Last readback: max erosion, voxels > 0.5, voxels > 0.9 (debug HUD). */
  readonly stats = { max: 0, over50: 0, over90: 0, refrost: 0 };
  /** clock time of the last stats readback that completed (for the idle-skip logic). */
  statsReadT = -1;
  private statsIssuedT = -1;
  private baked = false;
  skipBake = false;
  private texNode: any;
  private lastCrackSig = "";
  private settingsVersion = -1;

  /**
   * @param atomics shared atomic uint buffer (also holds the powder density grid + counters);
   *   the heal grid (one counter per field voxel) starts at `healOffset`.
   */
  readonly atomics: any;
  readonly healOffset: number;

  readonly cellOffset: number;

  readonly flightOffset: number;
  constructor(
    readonly renderer: THREE.WebGPURenderer,
    readonly shape: ShapeSpec,
    res: number,
    seed: number,
    atomics: any,
    healOffset: number,
    cellOffset: number,
    flightOffset: number,
  ) {
    this.cellOffset = cellOffset;
    this.flightOffset = flightOffset;
    // assigned explicitly (not as parameter properties): buildComputes() below reads them, and the
    // TS->JS transform may order parameter-property assignment after field initialisation
    this.atomics = atomics;
    this.healOffset = healOffset;
    if (!Number.isInteger(healOffset))
      throw new Error("ErosionField: healOffset must be an integer");
    this.res = res;
    this.bound = shape.bound;
    this.u.seed.value = seed;
    const make = () => {
      const t = new THREE.Storage3DTexture(res, res, res);
      t.type = THREE.HalfFloatType;
      t.format = THREE.RGBAFormat;
      t.minFilter = THREE.LinearFilter;
      t.magFilter = THREE.LinearFilter;
      t.wrapS = t.wrapT = t.wrapR = THREE.ClampToEdgeWrapping;
      t.generateMipmaps = false;
      return t;
    };
    this.tex = make();
    this.scratch = make();
    this.crackTex = make();
    this.cellTex = make();
    this.texNode = texture3D(this.tex);
    this.buildComputes();
  }

  /** Object-space position -> field uvw. */
  uvw(p: N) {
    return vec3(p)
      .div(this.bound * 2)
      .add(0.5);
  }
  /** Sample the field (trilinear) at an object-space position. Returns vec4 (r erosion, g refrost). */
  sample(pObject: N) {
    return this.texNode.sample(this.uvw(pObject)).level(0);
  }
  /** Baked crack helper texture: xyz = domain-warp vector (unit-less, -1..1), w = crack proximity 0..1. */
  sampleCrack(pObject: N) {
    return texture3D(this.crackTex).sample(this.uvw(pObject)).level(0);
  }
  /** Warped crack-domain coordinate for an object-space point (shared by bake + material). */
  static warpDomain(p: N, warp: N, scale: N, warpAmount: N) {
    return vec3(p).mul(scale).add(vec3(warp).mul(warpAmount));
  }

  private buildComputes() {
    const res = this.res,
      total = res * res * res,
      bound = this.bound;
    const u = this.u;
    const voxel = (bound * 2) / res;
    const shell = Math.max(voxel * 1.5, ErosionField.extraShell);
    const idx = instanceIndex;
    const coord = () => {
      const x = idx.mod(uint(res));
      const y = idx.div(uint(res)).mod(uint(res));
      const z = idx.div(uint(res * res));
      return uvec3(x, y, z);
    };
    const toObject = (c: N) =>
      vec3(c)
        .add(0.5)
        .div(res)
        .sub(0.5)
        .mul(bound * 2);

    const readTex = texture3D(this.tex);
    const readScratch = texture3D(this.scratch);
    const crack = texture3D(this.crackTex);
    const cells = texture3D(this.cellTex);

    // --- step: crumble propagation + heal + stroke splats -> scratch
    this.stepNode = Fn(() => {
      const c = coord();
      // NOTE: values used inside loops/branches AND afterwards must be pinned with toVar() up front,
      // otherwise TSL assigns them at first use (inside the branch) and later reads are uninitialised.
      const p = toObject(c).toVar();
      const sd = this.shape.sdfNode(p).toVar();
      const cur = readTex.load(ivec3(c)).level(0).toVar();
      const e = cur.r.toVar();
      const refrost = cur.g.toVar();
      const mark = cur.b.toVar(); // 1 = healed by a landed grain (propagation may spread from here only)
      const hit = max(0.0, cur.a.sub(u.dt.div(0.2))).toVar(); // brush-hit age: 1 the frame the brush touches this voxel, 0 after 0.2 s
      const crackP = crack.load(ivec3(c)).level(0).w;
      // breakup: every refill / cleanup / refrost timer runs at a locally varied rate, so a break cell (whose
      // voxels all carry the same erosion value) no longer crosses the look thresholds all at once
      const bn = gnoise(p.mul(u.breakupScale).add(u.seed.mul(1.7).add(11.0)))
        .mul(0.5)
        .add(0.5);
      const rateMul = mix(float(1), float(0.15).add(saturate(bn).mul(1.7)), u.breakup).toVar();

      // crumble propagation: erosion spreads toward the max of the 6 neighbours, biased by crack proximity
      If(u.time.lessThan(u.crumbleUntil), () => {
        const nb = float(0).toVar();
        const offs = [
          ivec3(1, 0, 0),
          ivec3(-1, 0, 0),
          ivec3(0, 1, 0),
          ivec3(0, -1, 0),
          ivec3(0, 0, 1),
          ivec3(0, 0, -1),
        ];
        for (const o of offs) {
          const cc = clamp(ivec3(c).add(o), ivec3(0), ivec3(res - 1));
          nb.assign(max(nb, readTex.load(cc).level(0).r));
        }
        const inside = smoothstep(0.02, -0.02, sd);
        const rate = u.crumbleRate.mul(float(1).add(crackP.mul(u.crumbleCrackBias))).mul(inside);
        const target = nb.mul(0.92).sub(0.06);
        If(target.greaterThan(e), () => {
          e.assign(min(target, e.add(target.sub(e).mul(saturate(rate.mul(u.dt))))));
        });
      });

      // stroke splats: soft capsules with a ragged noise boundary
      // break cell of this voxel: ice breaks along cell faces, so the brush is evaluated at the cell centre
      // (every voxel of a cell gets the same erosion -> flat facets) and mixed with the smooth capsule
      const cell = cells.load(ivec3(c)).level(0).toVar();
      Loop({ start: int(0), end: int(u.segCount), type: "int", condition: "<" }, ({ i }: any) => {
        const a4 = this.segments.element(i.mul(2));
        const b4 = this.segments.element(i.mul(2).add(1));
        const a = a4.xyz,
          b = b4.xyz,
          radius = a4.w,
          strength = b4.w;
        const ab = b.sub(a);
        const capsuleDist = (q: N) => {
          const tt = clamp(dot(q.sub(a), ab).div(max(dot(ab, ab), 1e-6)), 0.0, 1.0);
          return length(q.sub(a.add(ab.mul(tt))));
        };
        const n = gnoise(p.mul(u.brushNoiseScale).add(u.seed))
          .mul(0.5)
          .add(gnoise(p.mul(u.brushNoiseScale.mul(2.7)).add(u.seed.add(3.1))).mul(0.25));
        const r = radius.mul(float(1).add(n.mul(u.brushNoise)));
        const inner = r.mul(float(1).sub(u.brushSoftness));
        const wSmooth = smoothstep(r, inner, capsuleDist(p));
        const rCell = radius.mul(float(1).add(cell.w.sub(0.5).mul(u.brushNoise).mul(0.6)));
        const wCell = smoothstep(
          rCell,
          rCell.mul(float(1).sub(u.brushSoftness)),
          capsuleDist(cell.xyz),
        );
        const w = mix(wSmooth, wCell, u.cellSnap);
        e.assign(min(1.0, e.add(w.mul(strength).mul(u.dt))));
        If(w.greaterThan(0.02), () => {
          hit.assign(1.0);
        });
      });

      // healing is driven by the returning grains: a grain that lands deposits into the heal grid and
      // the voxel refills (as frost first); voxels without a grain follow their healed neighbours
      const healIdx = uint(this.healOffset).add(idx);
      const deposits = float(atomicLoad(this.atomics.element(healIdx))).toVar();
      // Ordinary healing waits for its cell; reconstruction deposits restore locally on arrival.
      const cellId0 = uint(clamp(cell.w.mul(65535.0), 0.0, 65535.0));
      const cellReady0 = atomicLoad(
        this.atomics.element(uint(this.flightOffset).add(cellId0)),
      ).equal(uint(0));
      If(deposits.greaterThan(0.5).and(cellReady0.or(u.reconstruct.greaterThan(0.5))), () => {
        atomicStore(this.atomics.element(healIdx), uint(0));
        refrost.assign(min(1.0, refrost.add(e.mul(2.5))));
        e.assign(0.0);
        mark.assign(1.0);
      });
      // break-cell restore: the last grain of the cell to land triggers it (cellHits, one frame), then the
      // whole cell refills at `cellRestore` (mark = 2 marks a restoring voxel). Nothing else may refill a
      // voxel whose cell still has grains in flight (cellReady), so the object never rebuilds ahead of them.
      const cellId = uint(clamp(cell.w.mul(65535.0), 0.0, 65535.0));
      const cellHits = float(atomicLoad(this.atomics.element(uint(this.cellOffset).add(cellId))));
      const inFlight = atomicLoad(this.atomics.element(uint(this.flightOffset).add(cellId)));
      const cellReady = inFlight.equal(uint(0));
      If(cellHits.greaterThan(0.5).and(e.greaterThan(0.0)), () => {
        mark.assign(2.0);
      });
      If(
        mark
          .greaterThan(1.5)
          .and(e.greaterThan(0.0))
          .and(cellReady)
          .and(u.reconstruct.lessThan(0.5)),
        () => {
          // grow back from what is already solid (hole floor, walls, landed grains) instead of popping in at once
          const canGrow = float(1).toVar();
          If(u.growEdges.greaterThan(0.5), () => {
            canGrow.assign(0.0);
            const offs2 = [
              ivec3(1, 0, 0),
              ivec3(-1, 0, 0),
              ivec3(0, 1, 0),
              ivec3(0, -1, 0),
              ivec3(0, 0, 1),
              ivec3(0, 0, -1),
            ];
            for (const o of offs2) {
              const cc = clamp(ivec3(c).add(o), ivec3(0), ivec3(res - 1));
              If(readTex.load(cc).level(0).r.lessThan(0.5), () => {
                canGrow.assign(1.0);
              });
            }
          });
          If(canGrow.greaterThan(0.5), () => {
            const before = e;
            const after = max(0.0, e.sub(u.cellRestore.mul(rateMul).mul(u.dt)));
            refrost.assign(min(1.0, refrost.add(before.sub(after).mul(2.5))));
            e.assign(after);
          });
        },
      );
      If(u.heal.greaterThan(0.5).and(cellReady.or(u.reconstruct.greaterThan(0.5))), () => {
        // voxels without a grain of their own follow neighbours that a grain has already rebuilt
        const nbMin = float(8).toVar();
        const nbMark = float(0).toVar();
        const offs = [
          ivec3(1, 0, 0),
          ivec3(-1, 0, 0),
          ivec3(0, 1, 0),
          ivec3(0, -1, 0),
          ivec3(0, 0, 1),
          ivec3(0, 0, -1),
        ];
        for (const o of offs) {
          const cc = clamp(ivec3(c).add(o), ivec3(0), ivec3(res - 1));
          const nv = readTex.load(cc).level(0);
          If(nv.b.greaterThan(0.5), () => {
            nbMin.assign(min(nbMin, nv.r));
            nbMark.assign(1.0);
          });
        }
        // Spatial front from deposited material; an additive gap per voxel left permanent holes.
        const reconstruct = u.reconstruct.greaterThan(0.5);
        const target = select(
          reconstruct,
          select(nbMin.lessThan(0.05), float(0), float(1)),
          nbMin.add(u.healGap),
        );
        If(nbMark.greaterThan(0.5).and(target.lessThan(e)), () => {
          const rate = select(
            reconstruct,
            max(u.cellRestore.mul(u.fallbackHeal), u.healRate),
            u.healRate,
          );
          const after = max(target, e.sub(rate.mul(rateMul).mul(u.dt)));
          refrost.assign(min(1.0, refrost.add(e.sub(after).mul(3.0))));
          e.assign(after);
          If(after.lessThanEqual(target.add(0.001)), () => {
            mark.assign(1.0);
          });
        });
        // fallback cleanup only after the whole wave has landed (never ahead of the grains)
        If(u.fallbackOn.greaterThan(0.5).and(reconstruct.not()), () => {
          const after2 = max(0.0, e.sub(u.fallbackHeal.mul(rateMul).mul(u.dt)));
          refrost.assign(min(1.0, refrost.add(e.sub(after2).mul(3.0))));
          e.assign(after2);
        });
      });
      // anything that eroded this frame is no longer "rebuilt"
      If(e.greaterThan(cur.r.add(0.0005)), () => {
        mark.assign(0.0);
      });
      refrost.assign(max(0.0, refrost.sub(u.dt.mul(rateMul).div(max(u.refrostTime, 0.01)))));
      // keep the field zero outside the shape so the surface sampling never reads garbage
      e.assign(select(sd.greaterThan(shell), 0.0, e));
      textureStore(this.scratch, c, vec4(e, refrost, mark, hit)).toWriteOnly();
    })().compute(total, [64]);

    // --- clear the break-cell restore triggers (after the step consumed them); the in-flight counters only
    // clear on reset
    this.cellClearNode = Fn(() => {
      atomicStore(this.atomics.element(uint(this.cellOffset).add(instanceIndex)), uint(0));
    })().compute(65536, [64]);
    this.flightClearNode = Fn(() => {
      atomicStore(this.atomics.element(uint(this.flightOffset).add(instanceIndex)), uint(0));
    })().compute(65536, [64]);

    // --- copy scratch -> tex
    this.copyNode = Fn(() => {
      const c = coord();
      const v = readScratch.load(ivec3(c)).level(0);
      textureStore(this.tex, c, v).toWriteOnly();
    })().compute(total, [64]);

    // --- stats reduction (debug HUD): max erosion + voxel counts above thresholds
    this.statsBuf = instancedArray(4, "uint").toAtomic();
    this.statsClearNode = Fn(() => {
      Loop(4, ({ i: k }: any) => {
        atomicStore(this.statsBuf.element(k), uint(0));
      });
    })().compute(1, [1]);
    this.statsNode = Fn(() => {
      const c = coord();
      const v = readTex.load(ivec3(c)).level(0);
      const e = v.r;
      atomicMax(this.statsBuf.element(0), uint(e.mul(65535.0)));
      atomicMax(this.statsBuf.element(3), uint(v.g.mul(65535.0)));
      If(e.greaterThan(0.5), () => {
        atomicAdd(this.statsBuf.element(1), uint(1));
      });
      If(e.greaterThan(0.9), () => {
        atomicAdd(this.statsBuf.element(2), uint(1));
      });
    })().compute(total, [64]);

    // --- clear both
    this.clearNode = Fn(() => {
      const c = coord();
      textureStore(this.tex, c, vec4(0.0)).toWriteOnly();
    })().compute(total, [64]);

    // --- FROST: the whole shape fully eroded (a shape that is not there yet: the grains heal it in), and the
    // heal-grid counters cleared (both used when the distance field is retargeted to another shape)
    this.fillNode = Fn(() => {
      const c = coord();
      const sd = this.shape.sdfNode(toObject(c));
      textureStore(
        this.tex,
        c,
        vec4(select(sd.greaterThan(shell), 0.0, 1.0), 0.0, 0.0, 0.0),
      ).toWriteOnly();
    })().compute(total, [64]);
    this.healClearNode = Fn(() => {
      atomicStore(this.atomics.element(uint(this.healOffset).add(instanceIndex)), uint(0));
    })().compute(total, [64]);

    // --- bake the crack helper: a low-frequency domain-warp vector (so cell boundaries curve instead of
    // reading as flat polygons) and the proximity to the nearest (warped, partially covered) boundary
    this.bakeNode = Fn(() => {
      const c = coord();
      const p = toObject(c).toVar();
      const wq = p.mul(u.crackWarpScale).add(u.seed.mul(0.11));
      const warp = vec3(
        gnoise(wq)
          .mul(0.6)
          .add(gnoise(wq.mul(2.1).add(vec3(7.3, 1.9, 4.4))).mul(0.4)),
        gnoise(wq.add(vec3(13.7, 5.1, 9.9)))
          .mul(0.6)
          .add(gnoise(wq.mul(2.1).add(vec3(2.2, 8.8, 6.1))).mul(0.4)),
        gnoise(wq.add(vec3(3.3, 17.1, 12.5)))
          .mul(0.6)
          .add(gnoise(wq.mul(2.1).add(vec3(9.7, 4.4, 1.1))).mul(0.4)),
      );
      const q = ErosionField.warpDomain(p, warp, u.crackScale, u.crackWarp);
      const ve = voronoiEdge(q, u.seed, u.crackCoverage);
      const dEdge = ve.w.div(u.crackScale); // approx object-space distance
      const prox = smoothstep(voxel * 2.5, 0.0, dEdge);
      const h = hash31(vec3(c)).mul(0.15);
      textureStore(this.crackTex, c, vec4(warp, saturate(prox.add(h.mul(prox))))).toWriteOnly();
      // break cells (a separate, finer Voronoi in the same warped domain): centre back in object space
      const bq = ErosionField.warpDomain(p, warp, u.breakScale, u.crackWarp);
      const bc = voronoiCell(bq, u.seed.add(3.0));
      const centreObj = bc.xyz.sub(warp.mul(u.crackWarp)).div(u.breakScale);
      textureStore(this.cellTex, c, vec4(centreObj, hash31(bc.xyz.add(u.seed)))).toWriteOnly();
    })().compute(total, [64]);
    void abs;
    void mix;
  }

  clear(renderer: THREE.WebGPURenderer) {
    renderer.compute(this.clearNode);
    renderer.compute(this.cellClearNode);
    renderer.compute(this.flightClearNode);
  }

  /**
   * The shape behind `this.shape.sdfNode` changed: re-bake the crack / break-cell helpers, reset the heal grid
   * and cell counters, start solid (`fill` false) or fully eroded (`fill` true, healed in by returning grains).
   */
  rebake(renderer: THREE.WebGPURenderer, fill: boolean) {
    const u = this.u,
      E = D.erosion,
      C = D.ice.cracks;
    u.crackScale.value = C.largeScale;
    u.crackWarp.value = C.warp;
    u.crackWarpScale.value = C.warpScale;
    u.crackCoverage.value = C.coverage;
    u.breakScale.value = E.breakCellScale;
    // Helpers depend on the shared bound and crack parameters, never the shape.
    const sig = `${C.largeScale}|${C.warp}|${C.warpScale}|${C.coverage}|${E.breakCellScale}`;
    if ((!this.baked || sig !== this.lastCrackSig) && !this.skipBake)
      renderer.compute(this.bakeNode);
    this.lastCrackSig = sig;
    this.baked = true;
    u.reconstruct.value = fill ? 1 : 0;
    renderer.compute(fill ? this.fillNode : this.clearNode);
    renderer.compute(this.cellClearNode);
    renderer.compute(this.flightClearNode);
    renderer.compute(this.healClearNode);
    this.crumbleUntil = -1;
    u.crumbleUntil.value = -1;
  }

  /** Debug: reduce the field and read the result back asynchronously (throttled by the caller). */
  readStats(renderer: THREE.WebGPURenderer, t = 0) {
    if (this.statsPending) return;
    this.statsPending = true;
    this.statsIssuedT = t;
    renderer.compute(this.statsClearNode);
    renderer.compute(this.statsNode);
    renderer
      .getArrayBufferAsync(this.statsBuf.value)
      .then((buf: ArrayBuffer) => {
        const a = new Uint32Array(buf);
        this.stats.max = a[0] / 65535;
        this.stats.over50 = a[1];
        this.stats.over90 = a[2];
        this.stats.refrost = a[3] / 65535;
        this.statsReadT = this.statsIssuedT;
        this.statsPending = false;
      })
      .catch(() => {
        this.statsPending = false;
      });
  }

  /** True while a stroke, the crumble window, or (refrost) healing can still change the field. */
  crumbleUntil = -1;

  /**
   * Advance the field. `segments` holds MAX_SEGMENTS capsules as (ax,ay,az,radius),(bx,by,bz,strength).
   * With `run` false only the bookkeeping happens (the GPU passes are skipped: the field is static).
   */
  step(
    renderer: THREE.WebGPURenderer,
    t: number,
    dt: number,
    segments: THREE.Vector4[],
    segCount: number,
    run = true,
  ) {
    const E = D.erosion,
      H = D.healing;
    const u = this.u;
    u.dt.value = Math.min(dt, 1 / 30);
    u.time.value = t;
    if (D.version !== this.settingsVersion) {
      this.settingsVersion = D.version;
      const C = D.ice.cracks;
      const crackSig = `${C.largeScale}|${C.warp}|${C.warpScale}|${C.coverage}|${E.breakCellScale}`;
      if (!this.baked || crackSig !== this.lastCrackSig) {
        u.crackScale.value = C.largeScale;
        u.crackWarp.value = C.warp;
        u.crackWarpScale.value = C.warpScale;
        u.crackCoverage.value = C.coverage;
        u.breakScale.value = E.breakCellScale;
        if (!this.skipBake) renderer.compute(this.bakeNode);
        if (!this.baked) renderer.compute(this.clearNode);
        this.baked = true;
        this.lastCrackSig = crackSig;
      }
      u.healRate.value = H.healRate;
      u.healGap.value = H.healGap;
      u.fallbackHeal.value = H.fallbackHeal;
      u.refrostTime.value = H.refrostTime;
      u.cellRestore.value = H.cellRestore;
      u.growEdges.value = H.growFromEdges ? 1 : 0;
      u.breakup.value = H.breakup;
      u.breakupScale.value = H.breakupScale;
      u.crumbleRate.value = E.crumbleRate;
      u.crumbleCrackBias.value = E.crumbleCrackBias;
      u.brushNoise.value = E.brushNoise;
      u.brushNoiseScale.value = E.brushNoiseScale;
      u.cellSnap.value = E.cellSnap;
      // Softness 0 makes the capsule falloff smoothstep(r, r, d): undefined in WGSL, 1 everywhere here (whole
      // shape eroded in one frame); keep a sliver of softness.
      u.brushSoftness.value = Math.max(E.brushSoftness, 0.02);
    }
    if (segCount > 0) {
      sim.lastStrokeT = t;
      u.crumbleUntil.value = t + E.crumbleDuration;
      this.crumbleUntil = t + E.crumbleDuration;
    }
    const healing = t - sim.lastStrokeT > H.healDelay;
    sim.healing = healing;
    u.heal.value = healing ? 1 : 0;
    // the timer-based cleanup may only start once every grain of the wave has had time to land
    u.fallbackOn.value =
      t - sim.lastStrokeT >
      H.healDelay + H.waveTime + H.waveJitter + Math.max(H.returnDuration, H.returnAfter) + 1.0
        ? 1
        : 0;
    u.segCount.value = segCount;
    if (segCount > 0) {
      const arr = this.segments.array as THREE.Vector4[];
      for (let i = 0; i < segCount * 2; i++) arr[i].copy(segments[i]);
    }
    if (!run || u.reconstruct.value > 0.5) return; // AssemblyField owns arrival-driven coverage.
    renderer.compute(this.stepNode);
    renderer.compute(this.cellClearNode);
    renderer.compute(this.copyNode);
  }

  /** Commit the assembly pass before both the ice and powder read the same field. */
  commitAssembly(renderer: THREE.WebGPURenderer) {
    renderer.compute(this.copyNode);
  }

  dispose() {
    this.tex.dispose();
    this.scratch.dispose();
    this.crackTex.dispose();
    this.cellTex.dispose();
  }
}
source/src/frost.ts
import { LogoRig, type LogoRigOptions } from "./rig/LogoRig";
export { setRendererProfile } from "./rewrite";
import { RW, rwMetrics } from "./rewrite";
import { voxelizeGPU } from "./shape/gpuSdf";
// Frost: the ice-logo effect as a HyperFrames block driver. All motion is on the object group; the camera never moves.
// Mark -> break front -> headline 1 -> break -> headline 2 -> break -> empty frame; each headline is a second shape
// whose distance field is swapped into the live field texture, starts fully eroded and heals in from the shards.
// Deterministic: frame t = round(t * 60) fixed 1/60 s steps; backward seeks reset and replay. Edits marked `FROST:`.
import * as THREE from "three/webgpu";
import { World } from "./World";
import { MATERIAL_FEATURES, featureId } from "./ice/features";
import approvedPreset from "../presets/approved-material.json";
import sourceMaterial from "../presets/source-hero4-material.json";
import { parseTrack, applyCameraAt, type CameraTrack } from "./motion/track";
import { objectFrameAt } from "./motion/objectFrame";
export { parseTrack, cameraAt } from "./motion/track";
import { deformGeometry, resolveDeformation, type Deformation } from "./shape/deform";
import { resolveTextMeshDetail } from "./shape/textRefine";
import { resolveLogoMeshDetail } from "./shape/logoRefine";
export { validateDeformationScale } from "./shape/deform";
import { RETURN_GROUP_DEFAULTS } from "./powder/returnGroups";
import { makePoseAt, SETTLE } from "./core/motion";
import { DEFAULT_SCHEDULE, durationOf, FADE_DURATION } from "./core/schedule";
export { resolveSchedule, durationOf } from "./core/schedule";
import { D } from "./dials/store";
import { clock } from "./core/clock";
import { sim, heroFrame, input } from "./core/state";
import { rng } from "./core/seed";
import { makeShape, sampleInterior, type LogoSDF } from "./shape/sdf";
import {
  loadLogoShapes,
  extrudeShapes,
  voxelize,
  halfExtents,
  makeLogoSDF,
  type LogoParams,
} from "./shape/logo";
import { loadTypeface, textShapes, lineWidth } from "./shape/text";
import { setIceFrameIndex } from "./ice/IceMaterial";
import { ErosionField } from "./erosion/ErosionField";
import { assetUrl } from "./assets";
import { BUILD_VERSION, readBuild, writeBuild, packGeometry, unpackGeometry } from "./cache";

export const SIM_STEP = 1 / 60;
export const DURATION = durationOf(DEFAULT_SCHEDULE);
export const REVIEW_BUILD = "frost-ice-textured-r1-reference-optics";
/** Frames re-rendered (not just simulated) before a jump target so TRAA history is converged there. */
const WARMUP_RENDERS = 12;

export interface Schedule {
  logoBreak: number;
  form1: number;
  break1: number;
  form2: number;
  break2: number;
  fadeOut: number;
}
export interface Pose {
  turnYaw: number;
  turnPitch: number;
  approach: number;
  turnYaw2: number;
  turnPitch2: number;
  retreat: number;
  zoomIn: number;
  finalYaw: number;
  finalPitch: number;
  driftYaw: number;
  driftSway: number;
  idleYaw: number;
}
export interface Shards {
  amount: number;
  formSpread: number;
  formFill: number;
  sliceRadius: number;
  sliceStrength: number;
  shatterRadius: number;
  shatterStrength: number;
  breakDuration: number;
  strayDust: number;
  followObject: number;
  finalEjectBoost: number;
}

export interface FrostOptions {
  rig?: LogoRigOptions;
  cameraMode?: "original" | "authored";
  cameraTrack?: CameraTrack;
  canvas: HTMLCanvasElement;
  width: number;
  height: number;
  /** two headlines, each as lines */
  headlines: [string[], string[]];
  /** headline extrusion: block width (world units), line height (em), depth / bevel / corner as font-size fractions */
  text: {
    width: number;
    lineHeight: number;
    depth: number;
    bevel: number;
    corner: number;
    weight: 400 | 600 | 700;
    letterSpacing: number;
    meshDetail?: number;
  };
  schedule: Schedule;
  deformation?: Deformation;
  pose: Pose;
  shards: Shards;
  /** the experiment's own tunables (Erosion / Ice / Powder / Healing / Lighting / Post folders), id -> value */
  tune: Record<string, number | string | boolean>;
  quality: "full" | "lite";
  upscaler: "fsr1" | "taau" | "bilinear" | "native";
  renderScale: number;
  previewPixelRatio?: number;
  shapeResolution?: "128" | "256" | "384";
  erosionResolution: "64" | "96" | "128" | "192";
  particleCount: "100k" | "250k" | "500k" | "1M";
  logoUrl: string;
  logoMeshDetail?: number;
  fontUrl: string;
  onProgress?: (msg: string) => void;
}

/** Exposed tunables: the DialKit path in the store and whether a change alters the simulation's history. */
export const TUNABLES: Record<string, { path: string[]; replay: boolean }> = {
  // shards in flight
  turbulenceScale: { path: ["powder", "turbulenceScale"], replay: true },
  clumpCohesion: { path: ["powder", "clumpCohesion"], replay: true },
  repelStrength: { path: ["powder", "repelStrength"], replay: true },
  repelRange: { path: ["powder", "repelRange"], replay: true },
  repelRadial: { path: ["powder", "repelRadial"], replay: true },
  repelRadialRange: { path: ["powder", "repelRadialRange"], replay: true },
  wrap: { path: ["powder", "wrap"], replay: false },
  turbulenceDecay: { path: ["powder", "turbulenceDecay"], replay: true },
  settledDrift: { path: ["powder", "settledDrift"], replay: true },
  tumble: { path: ["powder", "tumble"], replay: false },
  // return
  assemblyFrontDuration: { path: ["healing", "assemblyFrontDuration"], replay: true },
  assemblyOriginX: { path: ["healing", "assemblyOriginX"], replay: true },
  assemblyOriginY: { path: ["healing", "assemblyOriginY"], replay: true },
  assemblyAngle: { path: ["healing", "assemblyAngle"], replay: true },
  assemblySpread: { path: ["healing", "assemblySpread"], replay: true },
  assemblyFrontNoise: { path: ["healing", "assemblyFrontNoise"], replay: true },
  assemblySpeedVariation: { path: ["healing", "assemblySpeedVariation"], replay: true },
  assemblyBend: { path: ["healing", "assemblyBend"], replay: true },
  assemblySwirl: { path: ["healing", "assemblySwirl"], replay: true },
  assemblyLandingVariation: { path: ["healing", "assemblyLandingVariation"], replay: true },
  returnNoiseAmount: { path: ["healing", "returnNoiseAmount"], replay: true },
  returnGroupStagger: { path: ["healing", "returnGroupStagger"], replay: true },
  returnGroupScale: { path: ["healing", "returnGroupScale"], replay: true },
  returnGroupSeed: { path: ["healing", "returnGroupSeed"], replay: true },
  formJitter: { path: ["healing", "waveJitter"], replay: true },
  waveReach: { path: ["healing", "waveReach"], replay: true },
  alignToSurface: { path: ["healing", "alignToSurface"], replay: false },
  alignCurve: { path: ["healing", "alignCurve"], replay: false },
  // lighting
  rimAzimuth: { path: ["lighting", "rimAzimuth"], replay: false },
  rimAngle: { path: ["lighting", "rimAngle"], replay: false },
  rimSize: { path: ["lighting", "rimSize"], replay: false },
  fillReflectionStrength: { path: ["lighting", "fillReflectionStrength"], replay: false },
  accentCoolIntensity: { path: ["lighting", "accentCool", "intensity"], replay: false },
  accentCoolReflection: { path: ["lighting", "accentCool", "reflection"], replay: false },
  accentCoolElevation: { path: ["lighting", "accentCool", "elevation"], replay: false },
  accentCoolAzimuth: { path: ["lighting", "accentCool", "azimuth"], replay: false },
  accentCoolSize: { path: ["lighting", "accentCool", "size"], replay: false },
  accentCoolColor: { path: ["lighting", "accentCool", "color"], replay: false },
  accentWarmIntensity: { path: ["lighting", "accentWarm", "intensity"], replay: false },
  accentWarmReflection: { path: ["lighting", "accentWarm", "reflection"], replay: false },
  accentWarmElevation: { path: ["lighting", "accentWarm", "elevation"], replay: false },
  accentWarmAzimuth: { path: ["lighting", "accentWarm", "azimuth"], replay: false },
  accentWarmSize: { path: ["lighting", "accentWarm", "size"], replay: false },
  accentWarmColor: { path: ["lighting", "accentWarm", "color"], replay: false },
  keyColor: { path: ["lighting", "key", "color"], replay: false },
  keyIntensity: { path: ["lighting", "key", "intensity"], replay: false },
  keyElevation: { path: ["lighting", "key", "elevation"], replay: false },
  keyAzimuth: { path: ["lighting", "key", "azimuth"], replay: false },
  keySize: { path: ["lighting", "key", "size"], replay: false },
  fill: { path: ["lighting", "fill"], replay: false },
  fillColor: { path: ["lighting", "fillColor"], replay: false },
  fillGroundColor: { path: ["lighting", "fillGroundColor"], replay: false },
  rim: { path: ["lighting", "rim"], replay: false },
  rimColor: { path: ["lighting", "rimColor"], replay: false },
  rimElevation: { path: ["lighting", "rimElevation"], replay: false },
  swayAmplitude: { path: ["lighting", "sway", "amplitude"], replay: false },
  swayPeriod: { path: ["lighting", "sway", "period"], replay: false },
  envSoftbox: { path: ["lighting", "envSoftbox"], replay: false },
  envRim: { path: ["lighting", "envRim"], replay: false },
  envFill: { path: ["lighting", "envFill"], replay: false },
  backdropTop: { path: ["lighting", "backdrop", "top"], replay: false },
  backdropMid: { path: ["lighting", "backdrop", "mid"], replay: false },
  backdropBottom: { path: ["lighting", "backdrop", "bottom"], replay: false },
  backdropCenterX: { path: ["lighting", "backdrop", "centerX"], replay: false },
  backdropCenterY: { path: ["lighting", "backdrop", "centerY"], replay: false },
  backdropRadius: { path: ["lighting", "backdrop", "radius"], replay: false },
  backdropFalloff: { path: ["lighting", "backdrop", "falloff"], replay: false },
  backdropNoise: { path: ["lighting", "backdrop", "noise"], replay: false },
  // ice material
  ...Object.fromEntries(
    Object.keys(MATERIAL_FEATURES).map((name) => [
      featureId(name),
      { path: ["ice", "features", name], replay: false },
    ]),
  ),
  materialBacklight: { path: ["ice", "backlight"], replay: false },
  materialInclusionAmount: { path: ["ice", "inclusionAmount"], replay: false },
  materialInclusionScale: { path: ["ice", "inclusionScale"], replay: false },
  materialBaseColor: { path: ["ice", "baseColor"], replay: false },
  materialInteriorFrost: { path: ["erosion", "interiorFrost"], replay: false },
  materialGrainAmount: { path: ["erosion", "edgeBump"], replay: false },
  materialGrainScale: { path: ["erosion", "edgeBumpScale"], replay: false },
  ior: { path: ["ice", "ior"], replay: false },
  dispersion: { path: ["ice", "dispersion"], replay: false },
  thicknessScale: { path: ["ice", "thicknessScale"], replay: false },
  attenuationDistance: { path: ["ice", "attenuationDistance"], replay: false },
  attenuationColor: { path: ["ice", "attenuationColor"], replay: false },
  baseRoughness: { path: ["ice", "baseRoughness"], replay: false },
  crumbleGlow: { path: ["ice", "crumbleGlow"], replay: false },
  edgeWhiteness: { path: ["ice", "edgeWhiteness"], replay: false },
  clearcoat: { path: ["ice", "clearcoat"], replay: false },
  clearcoatRoughness: { path: ["ice", "clearcoatRoughness"], replay: false },
  envIntensity: { path: ["ice", "envIntensity"], replay: false },
  specularIntensity: { path: ["ice", "specularIntensity"], replay: false },
  interiorScatter: { path: ["ice", "interiorScatter"], replay: false },
  frostScale: { path: ["ice", "frost", "scale"], replay: false },
  frostThreshold: { path: ["ice", "frost", "threshold"], replay: false },
  frostSoftness: { path: ["ice", "frost", "softness"], replay: false },
  frostRoughness: { path: ["ice", "frost", "roughness"], replay: false },
  frostDiffuse: { path: ["ice", "frost", "diffuse"], replay: false },
  crystalBump: { path: ["ice", "frost", "crystalBump"], replay: false },
  crystalScale: { path: ["ice", "frost", "crystalScale"], replay: false },
  crackLargeScale: { path: ["ice", "cracks", "largeScale"], replay: false },
  crackWarp: { path: ["ice", "cracks", "warp"], replay: false },
  crackWarpScale: { path: ["ice", "cracks", "warpScale"], replay: false },
  crackCoverage: { path: ["ice", "cracks", "coverage"], replay: false },
  crackRegionScale: { path: ["ice", "cracks", "regionScale"], replay: false },
  crackRegionCoverage: { path: ["ice", "cracks", "regionCoverage"], replay: false },
  veinScale: { path: ["ice", "cracks", "veinScale"], replay: false },
  veinContrast: { path: ["ice", "cracks", "veinContrast"], replay: false },
  crackWidth: { path: ["ice", "cracks", "width"], replay: false },
  crackBrightness: { path: ["ice", "cracks", "brightness"], replay: false },
  crackDarkness: { path: ["ice", "cracks", "darkness"], replay: false },
  crackRefraction: { path: ["ice", "cracks", "refraction"], replay: false },
  crackSurfaceStrength: { path: ["ice", "cracks", "surfaceStrength"], replay: false },
  fineScale: { path: ["ice", "cracks", "fineScale"], replay: false },
  fineAmount: { path: ["ice", "cracks", "fineAmount"], replay: false },
  fineCoverage: { path: ["ice", "cracks", "fineCoverage"], replay: false },
  smudgeAmount: { path: ["ice", "smudges", "amount"], replay: false },
  smudgeCoverage: { path: ["ice", "smudges", "coverage"], replay: false },
  smudgeMaskScale: { path: ["ice", "smudges", "maskScale"], replay: false },
  smudgeAnisotropy: { path: ["ice", "smudges", "anisotropy"], replay: false },
  smudgeRoughness: { path: ["ice", "smudges", "roughness"], replay: false },
  smudgeWhiteness: { path: ["ice", "smudges", "whiteness"], replay: false },
  smudgeScale: { path: ["ice", "smudges", "scale"], replay: false },
  microBump: { path: ["ice", "bumps", "microBump"], replay: false },
  microScale: { path: ["ice", "bumps", "microScale"], replay: false },
  microCoverage: { path: ["ice", "bumps", "microCoverage"], replay: false },
  rippleBump: { path: ["ice", "bumps", "rippleBump"], replay: false },
  rippleScale: { path: ["ice", "bumps", "rippleScale"], replay: false },
  bumpMaskScale: { path: ["ice", "bumps", "maskScale"], replay: false },
  // shard look
  baseTone: { path: ["powder", "baseTone"], replay: false },
  fragTranslucency: { path: ["powder", "fragments", "translucency"], replay: false },
  fragThroughTint: { path: ["powder", "fragments", "throughTint"], replay: false },
  fragFresnelPower: { path: ["powder", "fragments", "fresnelPower"], replay: false },
  fragRoughness: { path: ["powder", "fragments", "roughness"], replay: false },
  fragClearcoat: { path: ["powder", "fragments", "clearcoat"], replay: false },
  fragSpecular: { path: ["powder", "fragments", "specular"], replay: false },
  sparkle: { path: ["powder", "fragments", "sparkle"], replay: false },
  sparkleFraction: { path: ["powder", "fragments", "sparkleFraction"], replay: false },
  spriteTilt: { path: ["powder", "sprites", "tilt"], replay: false },
  spriteNormal: { path: ["powder", "sprites", "normalStrength"], replay: false },
  spriteFrost: { path: ["powder", "sprites", "frostFromAtlas"], replay: false },
  spriteFrostBoost: { path: ["powder", "sprites", "frostBoost"], replay: false },
  spriteFrostRoughness: { path: ["powder", "sprites", "frostRoughness"], replay: false },
  spriteEdgeLight: { path: ["powder", "sprites", "edgeLight"], replay: false },
  spriteAlphaCut: { path: ["powder", "sprites", "alphaCut"], replay: false },
  spriteSeeThrough: { path: ["powder", "sprites", "seeThrough"], replay: false },
  // post
  bloomThreshold: { path: ["post", "bloom", "threshold"], replay: false },
  bloomIntensity: { path: ["post", "bloom", "intensity"], replay: false },
  bloomRadius: { path: ["post", "bloom", "radius"], replay: false },
  monochrome: { path: ["post", "monochrome"], replay: false },
  tonemap: { path: ["post", "tonemap"], replay: false },
  exposure: { path: ["post", "exposure"], replay: false },
  contrast: { path: ["post", "contrast"], replay: false },
  blackLift: { path: ["post", "blackLift"], replay: false },
  vignetteStrength: { path: ["post", "vignette", "strength"], replay: false },
  vignetteSoftness: { path: ["post", "vignette", "softness"], replay: false },
  vignetteRadius: { path: ["post", "vignette", "radius"], replay: false },
  grainStrength: { path: ["post", "grain", "strength"], replay: false },
  cutThreshold: { path: ["erosion", "cutThreshold"], replay: false },
  cutSoftness: { path: ["erosion", "cutSoftness"], replay: false },
  edgeWidth: { path: ["erosion", "edgeWidth"], replay: false },
  edgeInset: { path: ["erosion", "edgeInset"], replay: false },
  brushSoftness: { path: ["erosion", "brushSoftness"], replay: true },
  brushNoise: { path: ["erosion", "brushNoise"], replay: true },
  crumbleRate: { path: ["erosion", "crumbleRate"], replay: true },
  crumbleCrackBias: { path: ["erosion", "crumbleCrackBias"], replay: true },
  crumbleDuration: { path: ["erosion", "crumbleDuration"], replay: true },
  ejectSpeed: { path: ["powder", "ejectSpeed"], replay: true },
  ejectSpread: { path: ["powder", "ejectSpread"], replay: true },
  ejectTurbulence: { path: ["powder", "ejectTurbulence"], replay: true },
  drag: { path: ["powder", "drag"], replay: true },
  gravity: { path: ["powder", "gravity"], replay: true },
  turbulence: { path: ["powder", "turbulence"], replay: true },
  settleTime: { path: ["powder", "settleTime"], replay: true },
  maxSpeed: { path: ["powder", "maxSpeed"], replay: true },
  minPixelSize: { path: ["powder", "minPixelSize"], replay: false },
  grainSizeMultiplier: { path: ["powder", "grainSizeMultiplier"], replay: false },
  spriteSize: { path: ["powder", "sprites", "sizeScale"], replay: false },
  returnSpring: { path: ["healing", "returnSpring"], replay: true },
  returnDamping: { path: ["healing", "returnDamping"], replay: true },
  returnRamp: { path: ["healing", "returnRamp"], replay: true },
  returnMaxSpeed: { path: ["healing", "returnMaxSpeed"], replay: true },
  healRate: { path: ["healing", "healRate"], replay: true },
  cellRestore: { path: ["healing", "cellRestore"], replay: true },
  landedFade: { path: ["healing", "landedFade"], replay: true },
  refrostTime: { path: ["healing", "refrostTime"], replay: true },
};

export interface FrostInstance {
  ready: Promise<void>;
  renderAt(t: number): void;
  redraw(): void;
  invalidate(): void;
  waitForGpu(): Promise<void>;
  poseAt(t: number): { yaw: number; pitch: number; z: number };
  /**
   * Apply new option values in place. 'live': the current frame was updated (re-simulated from 0 when the
   * change alters history); 'rebuild': the change needs a full rebuild (shapes, quality, resolution).
   */
  applyOptions(
    next: Pick<
      FrostOptions,
      "schedule" | "pose" | "shards" | "tune" | "cameraMode" | "cameraTrack"
    >,
    structuralKey: string,
  ): "live" | "rebuild";
  readonly structuralKey: string;
  readonly world: World | null;
  /** index of the shape currently in the live field: 0 mark, 1 headline 1, 2 headline 2 */
  readonly shape: number;
  dispose(): void;
}

/** Approved authored appearance, also used when a direct runtime caller supplies partial tunables. */
export const APPEARANCE_DEFAULTS: Readonly<Record<string, number | string | boolean>> =
  Object.freeze(
    Object.fromEntries(
      Object.entries(TUNABLES)
        .filter(
          ([id, spec]) =>
            id.startsWith("material") ||
            ["ice", "lighting", "post"].includes(spec.path[0]) ||
            ["baseTone", "wrap", "sparkle", "sparkleFraction"].includes(id) ||
            id.startsWith("frag") ||
            id.startsWith("sprite"),
        )
        .map(([id]) => [id, (approvedPreset as Record<string, number | string | boolean>)[id]]),
    ),
  );

/** hero 07 auto-sweep path (object space, fractions of the bound) */
const SLICE_FROM: [number, number, number] = [-1, 0.3, 0],
  SLICE_TO: [number, number, number] = [1, -0.3, 0];

/** everything that changes the built shapes, the world, or the render pipeline */
export function structuralKeyOf(
  o: Pick<
    FrostOptions,
    | "headlines"
    | "text"
    | "quality"
    | "upscaler"
    | "renderScale"
    | "erosionResolution"
    | "shapeResolution"
    | "particleCount"
    | "logoUrl"
    | "deformation"
    | "logoMeshDetail"
  > & {
    shards: { strayDust: number; amount: number };
    tune: Record<string, number | string | boolean>;
  },
) {
  // dispersion and sprite see-through are compiled into the materials when non-zero (World key)
  return JSON.stringify([
    o.headlines,
    { ...o.text, meshDetail: resolveTextMeshDetail(o.text.meshDetail) },
    resolveDeformation(o.deformation),
    o.quality,
    o.upscaler,
    o.renderScale,
    o.shapeResolution ?? "256",
    o.erosionResolution,
    o.particleCount,
    o.logoUrl,
    resolveLogoMeshDetail(o.logoMeshDetail),
    o.shards.strayDust,
    Number(o.tune.spriteSeeThrough ?? APPEARANCE_DEFAULTS.spriteSeeThrough) > 0,
  ]);
}

export function create(o: FrostOptions): FrostInstance {
  o = {
    ...o,
    logoMeshDetail: resolveLogoMeshDetail(o.logoMeshDetail),
    text: { ...o.text, meshDetail: resolveTextMeshDetail(o.text.meshDetail) },
  };
  let world: World | null = null;
  let renderer: THREE.WebGPURenderer | null = null;
  let disposed = false;
  let simIndex = -1;
  let renderedIndex = -1;
  let lastTime = 0;
  let pendingGpu: Promise<void> | null = null;
  let requestedIndex = 0;
  let seekError: unknown = null;
  const log = (m: string) => {
    rwMetrics.stages.push([m, performance.now() - rwMetrics.start]);
    o.onProgress?.(m);
  };
  let S = o.schedule,
    P = o.pose,
    SH = o.shards;
  let cameraTrack = parseTrack(o.cameraTrack);
  const authored = () => o.cameraMode === "authored" && cameraTrack.keys.length > 0;
  const stationary = () => authored() && cameraTrack.objectMotion === "stationary";
  function syncCamera() {
    if (!world) return;
    world.onCameraTransform = authored()
      ? (t) => {
          if (world)
            applyCameraAt(
              world.camera,
              cameraTrack,
              t,
              world.objectGroup.matrixWorld,
              world.rig.lookAt,
            );
        }
      : null;
    if (!authored()) {
      world.camera.zoom = 1;
      world.camera.updateProjectionMatrix();
    }
  }
  const structuralKey = structuralKeyOf(o);

  // shapes: [logo, headline 1, headline 2] — geometries for the mesh, distance fields sharing one bound, and
  // the live field texture the GPU reads (its data is overwritten on every retarget)
  const geos: THREE.BufferGeometry[] = [];
  const sdfs: LogoSDF[] = [];
  /** every grain's home inside each shape (N x vec4: xyz + the grain's own threshold), computed once */
  const restSets: Float32Array[] = [];
  let live: LogoSDF | null = null;
  let current = -1;
  let logoRig: LogoRig | null = null;

  // ---------------------------------------------------------------------------------------------
  const setPath = (path: string[], value: number | string | boolean) => {
    let t: any = D;
    for (let i = 0; i < path.length - 1; i++) t = t[path[i]];
    t[path[path.length - 1]] = value;
  };
  function applyTune(tune: Record<string, number | string | boolean>) {
    for (const [id, v] of Object.entries({
      ...APPEARANCE_DEFAULTS,
      ...RETURN_GROUP_DEFAULTS,
      ...tune,
    })) {
      const spec = TUNABLES[id];
      if (spec && (typeof v === "string" || typeof v === "boolean" || Number.isFinite(v)))
        setPath(spec.path, v);
    }
  }

  // hero 07 / baked hero4 override layer (variants.tsx CameraJourney + baked.json hero4), minus the camera export
  function configure() {
    heroFrame.enabled = true;
    heroFrame.clickResets = true;
    heroFrame.shape = "logo";
    heroFrame.cursorErodes = true;
    heroFrame.hud = false;
    heroFrame.framing = true;
    // the object sits on the look-at point (hero4 raised it above a DOM headline; there is no DOM here)
    Object.assign(heroFrame, {
      distance: 13.45,
      height: 1.2,
      lookAtX: 0,
      lookAtY: 0.05,
      fov: 29.5,
      objectX: 0,
      objectY: 0.05,
      objectZ: 0,
      baseYaw: 0,
      basePitch: 0,
      rotateYaw: 40,
      rotatePitch: 45,
      parallaxRange: 15,
    });
    heroFrame.cameraOverride = null;
    D.shape.deformAmplitude = 0; // geometric deformation is baked before SDF creation, never applied twice
    D.camera.idleAmplitude = 0; // the composition owns the object's rotation
    const Pf = D.performance;
    Pf.adaptiveResolution = false;
    Pf.dynamicSceneResolution = false;
    Pf.sceneResolutionScale = Math.max(0.25, Math.min(1, o.renderScale));
    Pf.minSceneResolutionScale = Pf.sceneResolutionScale;
    Pf.upscaler = o.upscaler;
    Pf.nativePostEffects = true;
    Pf.upscaleSharpness = 1;
    Pf.minPixelRatio = o.previewPixelRatio ?? 2;
    D.post.pixelRatioCap = o.previewPixelRatio ?? 2;
    Pf.idleSkip = false;
    Pf.gpuTimers = false;
    D.debug.stats = false;
    D.debug.freeze = false;
    D.debug.forceStroke = false;
    D.shape.logo.sdfRes = [128, 256, 384].includes(Number(o.shapeResolution))
      ? Number(o.shapeResolution)
      : 256;
    D.erosion.resolution = o.erosionResolution;
    D.powder.particleCount = o.particleCount;
    D.powder.amount = SH.amount;
    D.powder.lostRadius = 80; // a shard 30 units out was snapped home mid-flight; the break throws them further than that
    D.powder.strayCount = Math.round(SH.strayDust); // the experiment's ambient dust ring stays where the object was; off by default
    // shards keep following the object group's motion (rotation and approach) for this long after they leave: the
    // experiment used 0.4 s, which made the cloud stop turning while the mark kept turning
    D.powder.inheritRotation = true;
    D.powder.inheritTime = SH.followObject * 1000;
    D.ice = { ...structuredClone(sourceMaterial.ice), features: { ...MATERIAL_FEATURES } };
    D.lighting = structuredClone(sourceMaterial.lighting);
    D.post = structuredClone(sourceMaterial.post);
    D.post.pixelRatioCap = o.previewPixelRatio ?? 2;
    D.post.grain.backgroundStrength = 0;
    D.powder.hazeIntensity = 0;
    D.ice.cracks.steps = 18;
    D.erosion.interiorSteps = 48;
    Pf.adaptiveSteps = sourceMaterial.performance.adaptiveSteps;
    applyTune(o.tune);
    if (o.quality === "lite") {
      // Mirrors defaults.ts `if (LITE)`: headless / software-GPU checks shrink everything heavy after the tuned values
      D.shape.segments = 96;
      D.ice.cracks.steps = 6;
      D.ice.cracks.fineCracks = false; // Lite reduces cost without overriding the explicit dispersion control.
      D.erosion.resolution = "64";
      D.powder.particleCount = "100k";
      D.powder.densityGrid = "32";
      D.powder.hazeSteps = 8;
    }
    healOff();
  }
  /** shards stay out: nothing schedules a return (baked healDelay 0 / returnAfter 3 would pull them back at once) */
  function healOff() {
    D.healing.healDelay = 1e9;
    D.healing.returnAfter = 0;
    D.healing.waveTime = 0;
    D.version++;
  }
  /** shards fly home in a wave (nearest first), the field fills in behind them */
  function healOn() {
    D.healing.healDelay = 0;
    D.healing.returnAfter = 0;
    D.healing.waveTime = SH.formSpread;
    D.healing.fallbackHeal = SH.formFill;
    D.version++;
  }

  // ---------------------------------------------------------------------------------------------
  const ready = (async () => {
    await new Promise((resolve) => setTimeout(resolve, 250));
    if (disposed) return;
    const nav = navigator as Navigator & { gpu?: { requestAdapter(): Promise<unknown> } };
    if (!nav.gpu) throw new Error("Frost: navigator.gpu is unavailable (WebGPU required)");
    const adapter = await nav.gpu.requestAdapter().catch(() => null);
    if (!adapter) throw new Error("Frost: no WebGPU adapter");
    log("webgpu adapter");
    configure();

    const r = new THREE.WebGPURenderer({
      canvas: o.canvas,
      antialias: false,
      forceWebGL: false,
      powerPreference: "high-performance",
      trackTimestamp: false,
    } as any);
    await r.init();
    if ((r as any).backend?.isWebGLBackend)
      throw new Error("Frost: WebGPU backend unavailable, WebGL fallback is not supported");
    r.toneMapping = THREE.NoToneMapping;
    r.outputColorSpace = THREE.SRGBColorSpace;
    r.shadowMap.enabled = D.lighting.shadow.intensity > 0; // baked 0: no shadow pass, no visible difference
    r.shadowMap.type = THREE.VSMShadowMap;
    r.setPixelRatio(1);
    r.setSize(o.width, o.height, false);
    renderer = r;
    log("renderer");

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(30, o.width / o.height, 0.5, 80);
    camera.position.set(0, 1.2, 13.5);

    const logoParams = D.shape.logo as unknown as LogoParams;
    const Pd = D.powder;
    const count =
      { "100k": 100_000, "250k": 250_000, "500k": 500_000, "1M": 1_000_000 }[
        Pd.particleCount as string
      ] ?? 100_000;
    const deformation = resolveDeformation(o.deformation);
    const logoOnly = !!o.rig && !o.rig.sequence;
    const cacheKey = JSON.stringify([
      BUILD_VERSION,
      logoOnly,
      deformation,
      o.headlines,
      o.text,
      o.logoUrl,
      o.logoMeshDetail,
      logoParams,
      count,
      Pd.nearSurfaceFraction,
      Pd.nearSurfaceDepth,
      D.shape.seed,
      D.shape.size,
    ]);
    const blueNoise = await new THREE.TextureLoader()
      .loadAsync(assetUrl("textures/bluenoise64.png"))
      .then((tex) => {
        tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
        tex.minFilter = tex.magFilter = THREE.NearestFilter;
        tex.generateMipmaps = false;
        tex.colorSpace = THREE.NoColorSpace;
        return tex;
      });
    const fractureDetail = await new THREE.TextureLoader()
      .loadAsync(assetUrl("textures/ice-inclusions-generated.png"))
      .then((tex) => {
        tex.wrapS = tex.wrapT = THREE.MirroredRepeatWrapping;
        tex.minFilter = THREE.LinearMipmapLinearFilter;
        tex.magFilter = THREE.LinearFilter;
        tex.generateMipmaps = true;
        tex.colorSpace = THREE.NoColorSpace;
        return tex;
      });
    let bound = 0;
    const cached = new URLSearchParams(location.search).has("fresh")
      ? null
      : await readBuild(cacheKey).catch(() => null);
    if (
      cached &&
      cached.geometries.length === (logoOnly ? 1 : 3) &&
      cached.sdfs.length === (logoOnly ? 1 : 3) &&
      cached.rests.length === (logoOnly ? 0 : 2)
    ) {
      for (const g of cached.geometries) geos.push(unpackGeometry(g));
      for (const s of cached.sdfs)
        sdfs.push(makeLogoSDF(s.data, s.res, s.bound, s.thickness, s.sampleBound));
      bound = cached.sdfs[0].bound;
      log("shapes (cached)");
    } else {
      const fontUrl = o.fontUrl.replace(
        /Geist-[A-Za-z]+\.ttf$/,
        `Geist-${o.text.weight === 400 ? "Regular" : o.text.weight === 600 ? "SemiBold" : "Bold"}.ttf`,
      );
      const [logoShapes, font] = await Promise.all([
        loadLogoShapes(o.logoUrl, logoParams),
        logoOnly ? Promise.resolve(null) : loadTypeface(fontUrl),
      ]);
      if (disposed) return;
      if (!logoShapes.length) throw new Error("Frost: the logo SVG produced no shapes");
      log("assets");
      // geometries: the mark exactly as the experiment builds it; the headlines through the same extrusion with
      // depth / bevel / corner relative to the font size and far fewer curve samples (glyph outlines are curves)
      geos.push(extrudeShapes(logoShapes, logoParams, logoParams.width));
      const textParams: LogoParams = {
        ...logoParams,
        depth: o.text.depth,
        bevelThickness: o.text.bevel,
        bevelSize: o.text.bevel * 0.8,
        bevelOffset: 0,
        cornerRadius: o.text.corner,
        curveSegments: 10,
        bevelSegments: 4,
      };
      for (const lines of logoOnly ? [] : o.headlines) {
        const clean = lines.map((l) => l.trim()).filter(Boolean);
        if (!clean.length) clean.push(" ");
        const widest = Math.max(
          0.001,
          ...clean.map((l) => lineWidth(font, l, o.text.letterSpacing)),
        );
        const size = o.text.width / widest;
        geos.push(
          extrudeShapes(
            textShapes(font, clean, size, o.text.lineHeight, o.text.letterSpacing),
            textParams,
            size,
          ),
        );
      }
      // Bake once before voxelization: render mesh, erosion SDF and all shard homes share this surface.
      for (let i = 0; i < geos.length; i++) {
        const source = geos[i];
        log(i === 0 ? "logo geometry" : `text mesh ${i}/2 · detail ${o.text.meshDetail}`);
        await new Promise((resolve) => setTimeout(resolve, 0));
        if (disposed) return;
        geos[i] = deformGeometry(
          source,
          deformation,
          logoParams.creaseAngle,
          i === 0 ? undefined : o.text.meshDetail,
          i === 0 ? o.logoMeshDetail : undefined,
        );
        if (i > 0)
          log(`text mesh ${i}/2 · ${geos[i].getAttribute("position").count / 3} triangles`);
        await new Promise((resolve) => setTimeout(resolve, 0));
        if (geos[i] !== source) source.dispose();
      }
      for (const g of geos) {
        const h = halfExtents(g);
        bound = Math.max(bound, h.x, h.y, h.z);
      }
      bound *= 1.15;
      for (const g of geos) {
        const h = halfExtents(g);
        const sampleBound = Math.max(h.x, h.y, h.z) * 1.15;
        sdfs.push(
          RW.gpu
            ? await voxelizeGPU(r, g, bound, logoParams.sdfRes, sampleBound)
            : voxelize(g, bound, logoParams.sdfRes, sampleBound),
        );
        await new Promise((res) => setTimeout(res, 0));
      }
      if (disposed) return;
      log("shapes");
    }
    // the live field is its own copy: retargeting overwrites it, never the per-shape sources
    live = makeLogoSDF(
      sdfs[0].data.slice(),
      sdfs[0].res,
      bound,
      sdfs[0].thickness,
      sdfs[0].sampleDomain.value,
    );
    // the erodable shell just outside the surface must cover the distance field's own error at any field resolution
    ErosionField.extraShell = ((2 * bound) / logoParams.sdfRes) * 2.0;

    // mesh geometries the way World.build makes them (a scaled clone)
    const size = D.shape.size;
    const rawGeos = geos.slice();
    for (let i = 0; i < geos.length; i++) {
      const g = geos[i].clone();
      g.scale(size, size, size);
      geos[i] = g;
    }

    world = new World(
      r,
      scene,
      camera,
      blueNoise,
      { sdf: live, geometry: geos[0] },
      fractureDetail,
    );
    world.onObjectTransform = (t) => {
      if (!world) return;
      if (logoRig) {
        logoRig.pose(t);
        return;
      }
      const frame = objectFrameAt(t, S, P, current, stationary());
      world.motionGroup.quaternion.copy(frame.rotation);
      world.motionGroup.position.copy(frame.position);
      world.objectGroup.quaternion.copy(frame.facing);
    };
    current = 0;
    syncCamera();
    // grain homes per shape: the mark keeps the ones the powder was built with; a headline is sampled inside its
    // own box with the same class layout (thresholds and strays untouched)
    const pw = world.powder;
    restSets.push(pw.restInit.slice());
    if (cached && cached.rests.length === 2) {
      for (const cr of cached.rests) {
        const rest = pw.restInit.slice();
        rest.set(cr.subarray(0, pw.count * 4));
        restSets.push(rest);
      }
    } else {
      for (let k = 1; k < geos.length; k++) {
        const spec = makeShape("logo", D.shape.size, D.shape.tubeRatio, sdfs[k]);
        const h = halfExtents(rawGeos[k]);
        const samples = sampleInterior(
          spec,
          pw.count,
          Pd.nearSurfaceFraction,
          Pd.nearSurfaceDepth,
          rng(D.shape.seed + 101 + k),
          [(h.x + 0.05) * size, (h.y + 0.05) * size, (h.z + 0.05) * size],
        );
        const rest = pw.restInit.slice();
        for (let i = 0; i < pw.count; i++) {
          rest[i * 4] = samples.positions[i * 3];
          rest[i * 4 + 1] = samples.positions[i * 3 + 1];
          rest[i * 4 + 2] = samples.positions[i * 3 + 2];
        }
        restSets.push(rest);
        await new Promise((res) => setTimeout(res, 0));
      }
      if (!new URLSearchParams(location.search).has("fresh"))
        writeBuild(cacheKey, {
          geometries: rawGeos.map(packGeometry),
          sdfs: sdfs.map((s) => ({
            data: s.data,
            res: s.res,
            bound: s.bound,
            thickness: s.thickness,
            sampleBound: s.sampleDomain.value,
          })),
          rests: restSets.slice(1).map((r) => r.slice(0, pw.count * 4)),
        }).catch(() => {});
    }
    if (o.rig) logoRig = new LogoRig(world, { ...o.rig, retarget: (k) => retarget(k, true) });
    (window as any).__fb = { world, renderer: r, D, sim, clock };
    // draw frame 0 now so every pipeline compiles inside the readiness gate, not at the first visible seek
    for (const f of [0.25, 0.5, 0.75, 0]) {
      renderAt(f * durationOf(S));
      while (pendingGpu) await pendingGpu;
    }
    log("world");
  })();

  // object motion: one continuous path. yaw(t) = constant drift + idle sway + a smooth turn step per break
  // (velocity/acceleration matched at boundaries); pitch and depth rise through a break, ease back during formation.
  // A headline faces the camera by construction: its object frame carries the inverse of this rotation when its
  // formation settles (see retarget), so the group keeps turning and the text still lands square.
  let poseAt = makePoseAt(S, P);
  function rebuildKeys() {
    poseAt = makePoseAt(S, P);
  }
  /** rotation of the motion group at time t */
  const rotationAt = (t: number) => {
    const p = poseAt(t);
    return new THREE.Quaternion().setFromEuler(
      new THREE.Euler(THREE.MathUtils.degToRad(p.pitch), THREE.MathUtils.degToRad(p.yaw), 0, "YXZ"),
    );
  };
  /** facing offset for shape k: the inverse of the group rotation when its formation has settled */
  const facingOffset = new THREE.Quaternion();
  function offsetFor(k: number) {
    if (k === 0 || stationary()) return new THREE.Quaternion();
    const t = k === 1 ? S.form1 + SETTLE : S.form2 + SETTLE;
    return rotationAt(t).invert();
  }

  // ---------------------------------------------------------------------------------------------
  // shape retargeting: the live field texture takes shape k's distances, the mesh takes its geometry, the field
  // is re-baked and starts fully eroded (a headline) or solid (the mark), and every grain gets a home inside it
  function retarget(k: number, fill: boolean) {
    if (!world || !renderer || !live) return;
    const src = sdfs[k];
    if (k !== current) {
      live.data.set(src.data);
      live.sampleDomain.value = src.sampleDomain.value;
      if (live.sampleBoundNode) live.sampleBoundNode.value = src.sampleDomain.value;
      live.texture.needsUpdate = true;
      live.thickness = src.thickness;
      world.shape = makeShape("logo", D.shape.size, D.shape.tubeRatio, live);
      world.mesh.geometry = geos[k];
      current = k;
    }
    // the new shape faces the camera once its formation settles, whatever the group is doing: apply the inverse
    // rotation now (nothing is visible), and make sure the jump is not read as one frame of object motion
    facingOffset.copy(o.rig ? new THREE.Quaternion() : offsetFor(k));
    world.objectGroup.quaternion.copy(facingOffset);
    world.motionGroup.updateMatrixWorld(true);
    world.powder.syncModel();
    world.erosion.rebake(renderer, fill);
    world.powder.retarget(renderer, restSets[k]);
  }
  /** the diagonal slice through the shape (hero 07 auto sweep) */
  function slice(final = false) {
    if (!world || !renderer) return;
    healOff();
    world.erosion.u.reconstruct.value = 0;
    D.powder.repelFromObject = true; // the shape is solid: shards are pushed out of it while it breaks
    // the last break clears the frame: shards leave faster and the speed cap goes up with them
    const boost = final ? SH.finalEjectBoost : 1;
    D.powder.ejectSpeed = Number(o.tune.ejectSpeed) * boost;
    D.powder.maxSpeed = Number(o.tune.maxSpeed) * boost;
    D.powder.minEjectSpeed = 0.66 * boost;
    D.version++;
    world.powder.restoreThresholds(renderer);
    world.interaction.sweep({
      from: SLICE_FROM,
      to: SLICE_TO,
      duration: 0.7,
      radius: SH.sliceRadius,
      strength: SH.sliceStrength,
    });
  }
  /** the break continues from the slice: parallel slices march outward on alternating sides, one after another,
   *  spaced so they overlap, spread over `breakDuration` */
  function shatter() {
    if (!world) return;
    const B = world.shape.bound;
    const dx = SLICE_TO[0] - SLICE_FROM[0],
      dy = SLICE_TO[1] - SLICE_FROM[1],
      len = Math.hypot(dx, dy);
    const nx = -dy / len,
      ny = dx / len;
    const spacing = Math.max(0.15, SH.shatterRadius * 1.7) / B; // in bound fractions
    const perSide = Math.max(1, Math.ceil(1.25 / spacing));
    const interval = SH.breakDuration / (perSide * 2);
    let i = 0;
    for (let k = 1; k <= perSide; k++) {
      for (const side of [1, -1]) {
        const off = side * k * spacing;
        world.interaction.sweep({
          from: [SLICE_FROM[0] + nx * off, SLICE_FROM[1] + ny * off, 0],
          to: [SLICE_TO[0] + nx * off, SLICE_TO[1] + ny * off, 0],
          duration: 0.5,
          delay: i * interval,
          radius: SH.shatterRadius,
          strength: SH.shatterStrength,
        });
        i++;
      }
    }
  }
  /** whatever the front missed: the field is set fully eroded and the last dormant grains leave too */
  function finishShatter() {
    if (!world || !renderer) return;
    renderer.compute((world.erosion as any).fillNode);
    // nothing is left to repel from: with it on, the invisible shape's pockets and hole trap shards
    D.powder.repelFromObject = false;
    D.version++;
  }
  function form(k: number) {
    if (!world) return;
    world.powder.u.assemblyEnd.value = (k === 1 ? S.break1 : S.break2) - 0.65;
    retarget(k, true);
    sim.lastStrokeT = clock.t; // the fallback fill-in waits for the wave to land (ErosionField.step)
    healOn();
  }
  function startFade() {
    if (!world) return;
    healOff();
    world.mesh.visible = false;
    D.healing.resetFade = FADE_DURATION * 1000;
    sim.resetRequestedAt = clock.t;
  }

  function reset() {
    if (!world) return;
    renderedIndex = -1;
    D.powder.repelFromObject = true;
    world.resetSim();
    world.interaction.clearScripted();
    facingOffset.identity();
    retarget(0, false);
    // A sequence may have left another shape's rest positions in the buffer.
    // Reset positions after restoring the logo homes, including invisible grains.
    if (o.rig?.sequence && renderer) world.powder.reset(renderer);
    healOff();
    D.healing.resetFade = 50;
    input.inside = false;
    input.x = 0;
    input.y = 0;
    input.lastMoveT = 0;
    input.moved = false;
    logoRig?.reset();
    simIndex = 0;
  }

  /** One fixed step ending at `st`, firing every event scheduled inside (prev, st]. */
  function step(index: number, render: boolean) {
    if (!world) return;
    const st = index * SIM_STEP;
    if (logoRig) {
      setIceFrameIndex(index);
      world.frame(st, SIM_STEP, render);
      return;
    }
    clock.t = st;
    // integer-exact: an event fires in the step whose index its time rounds to (no float-boundary misses)
    const at = (time: number) => Math.round(time / SIM_STEP) === index;
    for (const b of [S.logoBreak, S.break1, S.break2]) {
      if (at(b)) slice(b === S.break2);
      if (at(b + 0.3)) shatter();
      if (at(b + 0.3 + SH.breakDuration + 0.5)) finishShatter();
    }
    if (at(S.form1)) form(1);
    if (at(S.form2)) form(2);
    if (at(S.fadeOut)) startFade();
    setIceFrameIndex(index);
    world.frame(st, SIM_STEP, render);
  }

  async function drainSeeks() {
    // Small batches bound main-thread work and GPU queue depth. New slider requests
    // replace obsolete targets at a batch boundary; fixed simulation steps stay exact.
    let activeTarget = -1,
      firstRender = 0;
    while (!disposed && world && renderer) {
      const target = requestedIndex;
      if (simIndex < 0 || target < simIndex) reset();
      if (target !== activeTarget) {
        firstRender = target - simIndex > 60 ? target - WARMUP_RENDERS : target;
        activeTarget = target;
      }
      const end = Math.min(target, simIndex + 8);
      if (target === simIndex && renderedIndex !== target) {
        setIceFrameIndex(target);
        // Warm the post-processing history for a cold, paused first frame too.
        // This keeps direct seeks to zero consistent with playback and capture.
        const passes = o.rig && target === 0 && renderedIndex < 0 ? WARMUP_RENDERS : 1;
        for (let pass = 0; pass < passes; pass++) world.frame(target * SIM_STEP, 0, true);
        renderedIndex = target;
      }
      while (simIndex < end) {
        simIndex++;
        const render = simIndex === target || simIndex > firstRender;
        step(simIndex, render);
        if (render) renderedIndex = simIndex;
      }
      const queue = (renderer as any).backend?.device?.queue;
      if (queue?.onSubmittedWorkDone) await queue.onSubmittedWorkDone();
      if (disposed) return;
      if (simIndex === requestedIndex && renderedIndex === requestedIndex) return;
      await new Promise<void>((resolve) => setTimeout(resolve, 0));
    }
  }

  function renderAt(t: number) {
    if (disposed) return;
    lastTime = Number.isFinite(t) ? Math.max(0, Math.min(o.rig?.duration ?? durationOf(S), t)) : 0;
    requestedIndex = Math.round(lastTime / SIM_STEP);
    if (!world || !renderer || pendingGpu) return;
    seekError = null;
    pendingGpu = Promise.resolve()
      .then(drainSeeks)
      .catch((error) => {
        seekError = error;
        console.error("Frost seek failed", error);
      })
      .finally(() => {
        pendingGpu = null;
      });
  }

  /** Material-only changes redraw without advancing the simulation. */
  function redraw() {
    renderedIndex = -1;
    renderAt(lastTime);
  }

  function applyOptions(
    next: Pick<
      FrostOptions,
      "schedule" | "pose" | "shards" | "tune" | "cameraMode" | "cameraTrack"
    >,
    key: string,
  ): "live" | "rebuild" {
    if (key !== structuralKey) return "rebuild";
    let replay = false;
    const same = (a: any, b: any) => JSON.stringify(a) === JSON.stringify(b);
    if (!same(next.schedule, S) || !same(next.pose, P)) replay = true;
    const wasStationary = stationary();
    cameraTrack = parseTrack(next.cameraTrack);
    o.cameraTrack = cameraTrack;
    o.cameraMode = next.cameraMode;
    if (wasStationary !== stationary()) replay = true;
    syncCamera();
    if (!same(next.shards, SH)) replay = true;
    for (const [id, v] of Object.entries(next.tune)) {
      if (o.tune[id] !== v) {
        const spec = TUNABLES[id];
        if (spec) {
          setPath(spec.path, v);
          if (spec.replay) replay = true;
        }
      }
    }
    o.schedule = S = next.schedule;
    o.pose = P = next.pose;
    o.shards = SH = next.shards;
    o.tune = { ...next.tune };
    D.powder.amount = SH.amount;
    D.powder.inheritTime = SH.followObject * 1000;
    D.version++;
    rebuildKeys();
    if (!world) return "live";
    if (replay) {
      simIndex = -1;
      renderedIndex = -1;
      renderAt(lastTime);
    } else redraw();
    return "live";
  }

  async function waitForGpu() {
    await ready;
    while (pendingGpu) await pendingGpu;
    if (seekError) throw seekError;
  }

  function dispose() {
    disposed = true;
    world?.dispose();
    world = null;
    logoRig?.dispose();
    for (const s of sdfs) s.texture.dispose();
    live?.texture.dispose();
    live = null;
    for (const g of geos) g.dispose();
    if (renderer) {
      renderer.dispose();
      renderer = null;
    }
    heroFrame.enabled = false;
    heroFrame.shape = null;
    heroFrame.cameraOverride = null;
    delete (window as any).__fb;
  }

  return {
    ready,
    renderAt,
    redraw,
    invalidate: () => {
      simIndex = -1;
      renderedIndex = -1;
    },
    waitForGpu,
    poseAt: (t: number) => poseAt(t),
    applyOptions,
    structuralKey,
    get world() {
      return world;
    },
    get shape() {
      return current;
    },
    dispose,
  };
}
source/src/ice/CrackPlanes.ts
// Runtime material lookup, independent of SVG geometry. Store a boundary PLANE,
// then evaluate its distance at the real pixel, so fine lines stay sharp.
// Nearest-plane selection is approximate around cell junctions; opt-in experiment.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
import { voronoiEdge } from "../tsl/noise";
import { D } from "../dials/store";
import { rwMetrics } from "../rewrite";
const {
  Fn,
  vec3,
  vec4,
  float,
  uint,
  uniform,
  instanceIndex,
  uvec3,
  ivec3,
  texture3D,
  textureStore,
  dot,
  abs,
} = tsl;
export class CrackPlanes {
  private signature = "";
  private entries: any[] = [];
  constructor(
    private renderer: THREE.WebGPURenderer,
    private bound: number,
    private seed: number,
    private res = 128,
  ) {
    for (let j = 0; j < 2; j++) {
      const tex = new THREE.Storage3DTexture(res, res, res);
      tex.type = THREE.FloatType;
      tex.format = THREE.RGBAFormat;
      tex.minFilter = tex.magFilter = THREE.NearestFilter;
      const center = uniform(new THREE.Vector3()),
        extent = uniform(1),
        coverage = uniform(0.5),
        s = uniform(seed + j * 7);
      const kernel = Fn(() => {
        const i = instanceIndex,
          c = uvec3(i.mod(uint(res)), i.div(uint(res)).mod(uint(res)), i.div(uint(res * res)));
        const p = vec3(c).add(0.5).div(res).sub(0.5).mul(extent.mul(2)).add(center).toVar();
        const e = voronoiEdge(p, s, coverage).toVar();
        textureStore(tex, c, vec4(e.xyz, dot(e.xyz, p).add(e.w))).toWriteOnly();
      })().compute(res ** 3, [64]);
      this.entries.push({ tex, center, extent, coverage, s, kernel });
    }
    this.update();
  }
  update() {
    const C = D.ice.cracks,
      sig = JSON.stringify([
        C.largeScale,
        C.fineScale,
        C.warp,
        C.coverage,
        C.fineCoverage,
        this.seed,
      ]);
    if (sig === this.signature) return;
    this.signature = sig;
    const seedV = new THREE.Vector3(this.seed * 0.731, this.seed * 0.137, this.seed * 0.529),
      ratio = C.fineScale / C.largeScale;
    const e0 = this.entries[0],
      e1 = this.entries[1];
    e0.center.value.copy(seedV).multiplyScalar(0.37);
    e0.extent.value = this.bound * C.largeScale + Math.abs(C.warp) * 2 + 0.1;
    e0.coverage.value = C.coverage;
    e1.center.value.copy(e0.center.value).multiplyScalar(ratio).add(seedV);
    e1.extent.value = e0.extent.value * ratio;
    e1.coverage.value = C.fineCoverage;
    for (const e of this.entries) this.renderer.compute(e.kernel);
    rwMetrics.materialPlanes = {
      res: this.res,
      bytes: 2 * this.res ** 3 * 16,
      builds: (rwMetrics.materialPlanes?.builds || 0) + 1,
    };
  }
  sample(p: any, fine = false) {
    const e = this.entries[+fine],
      uv = p.sub(e.center).div(e.extent.mul(2)).add(0.5);
    const plane = texture3D(e.tex).sample(uv).level(0).toVar();
    return vec4(plane.xyz, abs(plane.w.sub(dot(plane.xyz, p))));
  }
  dispose() {
    for (const e of this.entries) {
      e.tex.dispose();
      e.kernel.dispose();
    }
  }
}
source/src/ice/EnvironmentSampling.ts
import { RW } from "../rewrite";
import { tsl } from "../tsl/t";
const { normalize, cross, select, abs, vec3, texture, equirectUV, pmremTexture } = tsl;
type N = any;
export function iceEnvironment(environment: any, strength: N, direction: N, rough: N) {
  if (RW.environment) return pmremTexture(environment, direction, rough).rgb.mul(strength);
  const d = normalize(direction);
  const tangent = normalize(
    cross(d, select(abs(d.y).lessThan(0.95), vec3(0, 1, 0), vec3(1, 0, 0))),
  );
  const bitangent = cross(d, tangent);
  const spread = rough.mul(rough).mul(0.32);
  const sample = (q: N) => texture(environment, equirectUV(normalize(q))).level(0).rgb;
  return sample(d)
    .mul(0.4)
    .add(sample(d.add(tangent.mul(spread))).mul(0.15))
    .add(sample(d.sub(tangent.mul(spread))).mul(0.15))
    .add(sample(d.add(bitangent.mul(spread))).mul(0.15))
    .add(sample(d.sub(bitangent.mul(spread))).mul(0.15))
    .mul(strength);
}
source/src/ice/FastOptics.ts
import { tsl } from "../tsl/t";
import { iceEnvironment } from "./EnvironmentSampling";
const { normalize, mat3, vec3, float, dot, select, pow, clamp, reflect, refract, exp, log, max } =
  tsl;
// Artistic glass against dark backgrounds: approximate transmitted studio light.
export function fastOptics({ ray, normal, environment, u, f, roughness, path }: any) {
  const n = select(dot(normal, ray).greaterThan(0), normal.negate(), normal),
    ior = max(u.ior, 1.001);
  const world = (v: any) => normalize(mat3(u.model).mul(v));
  const env = (v: any) => iceEnvironment(environment, u.envStrength, world(v), roughness);
  const f0 = pow(ior.sub(1).div(ior.add(1)), 2),
    F = f0.add(
      float(1)
        .sub(f0)
        .mul(pow(float(1).sub(clamp(dot(n, ray.negate()), 0, 1)), 5)),
    );
  const attenuation = exp(
    log(max(u.attColor, vec3(0.001)))
      .mul(path.mul(u.thicknessScale).div(max(u.attDist, 0.01)))
      .mul(f.absorption),
  );
  return {
    reflected: env(reflect(ray, n)).mul(F).mul(f.reflections),
    transmitted: env(normalize(refract(ray, n, float(1).div(ior))))
      .mul(float(1).sub(F))
      .mul(u.backlight)
      .mul(attenuation),
  };
}
source/src/ice/features.ts
/** Feature switches preserve their individual amounts when disabled. */
export const MATERIAL_FEATURES: Readonly<Record<string, boolean>> = Object.freeze({
  transmission: true,
  absorption: true,
  dispersion: true,
  reflections: true,
  frost: true,
  crystals: true,
  grain: true,
  cutNormals: true,
  surfaceBumps: true,
  smudges: false,
  micro: false,
  ripples: false,
  cracks: false,
  scatter: true,
  clearcoat: true,
  shardNormals: true,
  shardFrost: true,
  shardTransmission: true,
  shardReflections: true,
  shardSparkle: true,
  shardEdges: true,
});
export const featureId = (name: string) => `material${name[0].toUpperCase()}${name.slice(1)}`;
source/src/ice/GlassMatcap.ts
// Shape-independent studio-glass lookup: lighting is camera-locked.
import * as THREE from "three/webgpu";
import { D } from "../dials/store";
export function createGlassMatcap(environment: THREE.DataTexture, n = 1024) {
  const out = new Float32Array(n * n * 4),
    src = environment.image.data as Float32Array,
    w = environment.image.width,
    h = environment.image.height;
  const I = D.ice,
    ior = I.ior,
    f0 = ((ior - 1) / (ior + 1)) ** 2,
    color = new THREE.Color(I.attenuationColor),
    colors = [color.r, color.g, color.b];
  const sample = (x: number, y: number, z: number, c: number) => {
    const u = (((Math.atan2(z, x) / (2 * Math.PI) + 0.5) % 1) + 1) % 1,
      v = Math.asin(Math.max(-1, Math.min(1, y))) / Math.PI + 0.5;
    const px = u * w - 0.5,
      py = v * h - 0.5,
      ix = Math.floor(px),
      iy = Math.floor(py),
      tx = px - ix,
      ty = py - iy;
    const a = (xx: number, yy: number) =>
      src[(Math.max(0, Math.min(h - 1, yy)) * w + ((xx + w) % w)) * 4 + c];
    return (
      (a(ix, iy) * (1 - tx) + a(ix + 1, iy) * tx) * (1 - ty) +
      (a(ix, iy + 1) * (1 - tx) + a(ix + 1, iy + 1) * tx) * ty
    );
  };
  for (let y = 0; y < n; y++)
    for (let x = 0; x < n; x++) {
      let nx = ((x + 0.5) / n) * 2 - 1,
        ny = ((y + 0.5) / n) * 2 - 1;
      const rr = nx * nx + ny * ny;
      if (rr >= 1) {
        const m = 0.99999 / Math.sqrt(rr);
        nx *= m;
        ny *= m;
      }
      const nz = Math.sqrt(Math.max(1e-8, 1 - nx * nx - ny * ny));
      const eta = 1 / ior,
        k = eta * nz - Math.sqrt(1 - eta * eta * (1 - nz * nz));
      const dx = k * nx,
        dy = k * ny,
        dz = -eta + k * nz,
        chord = -2 * (nx * dx + ny * dy + nz * dz);
      const ex = nx + dx * chord,
        ey = ny + dy * chord,
        ez = nz + dz * chord;
      const dn = dx * ex + dy * ey + dz * ez,
        k2 = 1 - ior * ior * (1 - dn * dn),
        f = f0 + (1 - f0) * (1 - nz) ** 5;
      const b = ior * dn - Math.sqrt(Math.max(0, k2)),
        ox = ior * dx - b * ex,
        oy = ior * dy - b * ey,
        oz = ior * dz - b * ez;
      for (let c = 0; c < 3; c++) {
        const att = Math.pow(
          colors[c],
          (chord * I.thicknessScale) / Math.max(I.attenuationDistance, 0.01),
        );
        out[(x + y * n) * 4 + c] =
          sample(2 * nz * nx, 2 * nz * ny, 2 * nz * nz - 1, c) * f +
          sample(ox, oy, oz, c) * (1 - f) * (I.backlight ?? 0.4) * att;
      }
      out[(x + y * n) * 4 + 3] = 1;
    }
  const tex = new THREE.DataTexture(out, n, n, THREE.RGBAFormat, THREE.FloatType);
  tex.colorSpace = THREE.LinearSRGBColorSpace;
  tex.minFilter = tex.magFilter = THREE.LinearFilter;
  tex.needsUpdate = true;
  return tex;
}
source/src/ice/IceMaterial.ts
import { RW } from "../rewrite";
import { fastOptics } from "./FastOptics";
// One ice material evaluated at the bounded volume hit. Optical features are live,
// independently switchable uniforms; erosion only changes the visible geometry.
// Exposes { material, uniforms, update } — every uniform is driven from DialKit each frame.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
const {
  Fn,
  vec3,
  vec4,
  float,
  uniform,
  varying,
  texture,
  vec2,
  positionLocal,
  normalLocal,
  cameraPosition,
  transformNormalToView,
  normalize,
  dot,
  length,
  max,
  min,
  abs,
  pow,
  exp,
  mix,
  smoothstep,
  clamp,
  reflect,
  select,
  If,
  Loop,
  Break,
  int,
  output,
  screenUV,
} = tsl;
import type { ShapeSpec } from "../shape/sdf";
import { ErosionField } from "../erosion/ErosionField";
import {
  fbm,
  gnoise,
  vnoise3,
  hash31,
  voronoiCell,
  voronoiCell8,
  voronoiEdge,
  softThreshold,
  saturate,
} from "../tsl/noise";
import { D } from "../dials/store";
import { rng } from "../core/seed";
import { ICE_VARIANT } from "./variant";
import { opticalTransport } from "./OpticalTransport";
import { iceSurface, iceSmudgeDirections } from "./SharedSurface";
import { iceDetail, iceFrost, iceInclusions } from "./SurfaceDetail";
import { MATERIAL_FEATURES } from "./features";

type N = any;

export interface IceMaterialOptions {
  shape: ShapeSpec;
  erosion: ErosionField;
  seed: number;
  blueNoise: THREE.Texture;
  dispersion?: boolean;
  voronoiCells?: "27" | "8";
  environment?: THREE.Texture;
  fractureDetail?: THREE.Texture;
  backdrop?: any;
  meshEntry?: any;
  glassMatcap?: any;
}
export interface IceMaterialBundle {
  material: THREE.MeshPhysicalNodeMaterial;
  uniforms: Record<string, any>;
  features: Record<string, any>;
  update(
    t: number,
    dt: number,
    keyLight: THREE.DirectionalLight,
    objectGroup: THREE.Object3D,
  ): void;
}

let frameCounter = 0;
// HyperFrames adapter: keep the original deterministic frame clock.
let frameIndexOverride = -1;
export function setIceFrameIndex(index: number) {
  frameIndexOverride = index;
}

export function createIceMaterial({
  shape,
  erosion,
  seed,
  environment,
  fractureDetail,
  backdrop,
  meshEntry,
  glassMatcap,
  voronoiCells = "27",
}: IceMaterialOptions): IceMaterialBundle {
  const voro = voronoiCells === "8" ? voronoiCell8 : voronoiCell;
  const rand = rng(seed + 11);
  const u = {
    model: uniform(new THREE.Matrix4()),
    envStrength: uniform(1),
    backlight: uniform(0.4),
    inclusionAmount: uniform(0.3),
    inclusionScale: uniform(0.4),
    baseColor: uniform(new THREE.Color("#f8f8f8")),
    ior: uniform(1.36),
    dispersion: uniform(0.02),
    thicknessScale: uniform(1),
    attDist: uniform(0.9),
    attColor: uniform(new THREE.Color("#cfd2d4")),
    baseRough: uniform(0.05),
    frostScale: uniform(1.6),
    frostThreshold: uniform(0.5),
    frostSoftness: uniform(0.28),
    frostRough: uniform(0.65),
    frostDiffuse: uniform(0.55),
    crystalBump: uniform(0.12),
    crystalScale: uniform(28),
    crackLarge: uniform(1.6),
    crackWarp: uniform(0.45),
    crackCoverage: uniform(0.55),
    regionScale: uniform(0.8),
    regionCoverage: uniform(0.7),
    veinScale: uniform(5),
    veinContrast: uniform(0.7),
    crackFine: uniform(5.5),
    crackFineAmount: uniform(0.45),
    fineCoverage: uniform(0.35),
    fineNearLarge: uniform(0.75),
    crackWidth: uniform(0.004),
    crackBright: uniform(0.4),
    crackDark: uniform(0.5),
    crackRefr: uniform(0.02),
    surfaceCrack: uniform(0.35),
    crackSteps: uniform(14),
    smudgeAmount: uniform(0.6),
    smudgeCoverage: uniform(0.55),
    smudgeMaskScale: uniform(1.2),
    smudgeAniso: uniform(6),
    smudgeRough: uniform(0.35),
    smudgeWhite: uniform(0.08),
    smudgeScale: uniform(3.2),
    microBump: uniform(0.25),
    microScale: uniform(60),
    microCoverage: uniform(0.6),
    rippleBump: uniform(0.3),
    rippleScale: uniform(9),
    bumpMaskScale: uniform(1.5),
    clearcoat: uniform(0.3),
    clearcoatRough: uniform(0.08),
    specularIntensity: uniform(1),
    interiorScatter: uniform(0.25),
    deformAmp: uniform(0.045),
    deformFreq: uniform(1.1),
    seed: uniform(seed),
    edgeWidth: uniform(0.35),
    edgeInset: uniform(0.06),
    edgeBump: uniform(0.6),
    edgeBumpScale: uniform(55),
    refrostStrength: uniform(1),
    cutThreshold: uniform(0.7),
    cutSoft: uniform(0.04),
    interiorSteps: uniform(24),
    interiorFrost: uniform(0),
    crumbleGlow: uniform(0.15),
    edgeWhite: uniform(0.5),
    lightDir: uniform(new THREE.Vector3(0, 1, 0)),
    modelInv: uniform(new THREE.Matrix4()),
    jitter: uniform(new THREE.Vector2()),
    adaptiveSteps: uniform(0),
    powderTone: uniform(new THREE.Color("#dcdcdc")),
    keyIntensity: uniform(3.2),
    keyColor: uniform(new THREE.Color("#ffffff")),
    hover: uniform(0),
  };
  const f = Object.fromEntries(
    Object.keys(MATERIAL_FEATURES).map((name) => [name, uniform(Number(MATERIAL_FEATURES[name]))]),
  ) as Record<string, any>;
  const seedV = vec3(seed * 0.731, seed * 0.137, seed * 0.529);
  const smudgeDirs = iceSmudgeDirections(seed);

  const material: any = RW.matcap
    ? new THREE.MeshBasicNodeMaterial()
    : new THREE.MeshPhysicalNodeMaterial();
  const lightDir = new THREE.Vector3();
  const objectRotation = new THREE.Quaternion();
  let settingsVersion = -1;
  // Render the raymarched solid-ice path directly: BackSide keeps the original interior shading, visible from the
  // intact first frame (DoubleSide would draw this proxy first, then cover it with the front-shell finish).
  // cutRes finds the first remaining solid along the ray; cutHit discards the proxy when the ray holds no ice.
  material.side = THREE.BackSide;
  material.transparent = false;
  material.metalness = 0;
  material.transmission = 1; // enables the transmission pass; the node below drives the value

  // ---------- vertex: deformation + erosion inset + crack notches
  const deformAt = (p: N) => fbm(p.mul(u.deformFreq).add(seedV), 3).mul(u.deformAmp);
  // crumbly edge band just below the cut threshold
  const edgeBandOf = (er: N) =>
    smoothstep(u.cutThreshold.sub(u.edgeWidth), u.cutThreshold.sub(u.edgeWidth.mul(0.15)), er);
  const displaced = Fn(() => {
    const p = positionLocal,
      n = normalLocal;
    const d = deformAt(p);
    const er = erosion.sample(p).r;
    const edge = edgeBandOf(er);
    const notch = erosion
      .sampleCrack(p)
      .w.mul(u.surfaceCrack)
      .mul(f.cracks)
      .mul(f.surfaceBumps)
      .mul(0.012);
    return p.add(n.mul(d.sub(edge.mul(u.edgeInset)).sub(notch)));
  })();
  material.positionNode = displaced;
  const vPos = varying(displaced);

  // ---------- one material, evaluated at the first remaining solid on the ray
  const camObj = u.modelInv.mul(vec4(cameraPosition, 1.0)).xyz;
  const V = normalize(vPos.sub(camObj));
  const L = u.lightDir;
  const meshPosition = meshEntry?.position.sample(screenUV);
  const meshNormal = meshEntry?.normal.sample(screenUV).xyz;
  const meshValid = meshEntry
    ? meshEntry.depth
        .sample(screenUV)
        .r.lessThan(0.999999)
        .and(erosion.sample(meshPosition.xyz).r.lessThan(u.cutThreshold))
    : null;
  const cutRes = Fn(() => {
    const ro = camObj;
    // The voxel SDF clamps to its border outside [-bound, bound]. Those values
    // are not distances from the camera: marching from t=0 can jump across the
    // entire thin logo/text and leave cutHit=-1. Enter the texture's valid box
    // analytically before sampling, and never search beyond the proxy surface.
    const reciprocal = (d: N) =>
      select(d.greaterThanEqual(0), float(1), float(-1)).div(max(abs(d), 1e-6));
    const invRay = vec3(reciprocal(V.x), reciprocal(V.y), reciprocal(V.z));
    const a = vec3(-shape.bound).sub(ro).mul(invRay);
    const b = vec3(shape.bound).sub(ro).mul(invRay);
    const near = min(a, b),
      far = max(a, b);
    const tStart = max(max(near.x, max(near.y, near.z)), 0);
    const tMax = min(min(far.x, min(far.y, far.z)), length(vPos.sub(camObj)));
    const result = vec4(vPos, -1).toVar();
    if (meshEntry)
      If(meshValid, () => {
        result.assign(vec4(meshPosition.xyz, length(meshPosition.xyz.sub(camObj))));
      });
    If(tMax.greaterThan(tStart).and(result.w.lessThan(0)), () => {
      // Raster coverage already establishes the exact mesh silhouette. The
      // interpolated voxel field has a finite surface error, especially in thin
      // rotating glyphs. A strict negative-distance test clipped their far edge.
      const voxel = float((2 * erosion.bound) / erosion.res);
      const surfaceVoxel = shape.voxelSize ?? voxel;
      const shell = surfaceVoxel.mul(0.4);
      const t = tStart.toVar();
      Loop(192, () => {
        If(t.greaterThan(tMax), () => {
          Break();
        });
        const q = ro.add(V.mul(t));
        const d = shape.sdfNode(q);
        If(d.lessThanEqual(0.001).and(erosion.sample(q).r.lessThan(u.cutThreshold)), () => {
          result.assign(vec4(q, t));
          Break();
        });
        // Conservative steps near the surface and through cut-away ice; no
        // whole-chord subsampling that can step across a thin surviving glyph.
        t.addAssign(max(d.sub(shell).mul(0.7), surfaceVoxel.mul(0.35)));
      });
      // The rasterized far surface is an exact endpoint, even when the voxel
      // SDF misses a bevel. Only accept it when the same erosion field is solid.
      If(result.w.lessThan(0).and(erosion.sample(vPos).r.lessThan(u.cutThreshold)), () => {
        result.assign(vec4(vPos, tMax));
      });
    });
    return result;
  })();
  const cutHit = cutRes.w;
  const cutPos = cutRes.xyz;
  // outward normal of the solid at the cut: erosion gradient (points into the void), else the shell normal
  const ge = 0.02;
  const cutGrad = vec3(
    erosion.sample(cutPos.add(vec3(ge, 0, 0))).r.sub(erosion.sample(cutPos.sub(vec3(ge, 0, 0))).r),
    erosion.sample(cutPos.add(vec3(0, ge, 0))).r.sub(erosion.sample(cutPos.sub(vec3(0, ge, 0))).r),
    erosion.sample(cutPos.add(vec3(0, 0, ge))).r.sub(erosion.sample(cutPos.sub(vec3(0, 0, ge))).r),
  );
  const cutGradLen = length(cutGrad);
  // Shade the actual hit surface, not the proxy's far wall.
  const shapeEpsilon = shape.voxelSize ? shape.voxelSize.mul(0.75) : float(ge);
  const shapeGrad = vec3(
    shape
      .sdfNode(cutPos.add(vec3(shapeEpsilon, 0, 0)))
      .sub(shape.sdfNode(cutPos.sub(vec3(shapeEpsilon, 0, 0)))),
    shape
      .sdfNode(cutPos.add(vec3(0, shapeEpsilon, 0)))
      .sub(shape.sdfNode(cutPos.sub(vec3(0, shapeEpsilon, 0)))),
    shape
      .sdfNode(cutPos.add(vec3(0, 0, shapeEpsilon)))
      .sub(shape.sdfNode(cutPos.sub(vec3(0, 0, shapeEpsilon)))),
  );
  const sdfNormal = () =>
    select(
      length(shapeGrad).greaterThan(1e-5),
      shapeGrad.div(max(length(shapeGrad), 1e-5)),
      normalLocal.negate(),
    );
  const shapeNormal = meshEntry
    ? Fn(() => {
        const n = vec3(0).toVar();
        If(meshValid, () => n.assign(normalize(meshNormal))).Else(() => n.assign(sdfNormal()));
        return n;
      })()
    : sdfNormal();
  const nBase = normalize(
    mix(
      shapeNormal,
      cutGrad.div(max(cutGradLen, 1e-5)),
      smoothstep(0.02, 0.15, cutGradLen).mul(f.cutNormals),
    ),
  );
  const p = cutPos;
  const { frost, smudge, surfCrack, nSurface, regionMask, roughness } = iceSurface(
    p,
    nBase,
    seedV,
    smudgeDirs,
    erosion,
    u,
    f,
    RW.aa,
  );
  // The back-face proxy is flipped by Three; supply the opposite normal.
  material.normalNode = transformNormalToView(nSurface.negate());
  const tExit = length(vPos.sub(cutPos));

  // Sparse hairline fissures at several depths. Cell transitions used to shade
  // entire planes, covering the transparent windows with white polygon tiles.
  // Evaluate actual distance to each fissure instead; energy is bounded and
  // normalized so extra sampling cannot turn the whole volume opaque.
  const march = Fn(() => {
    const glint = float(0).toVar();
    const steps = clamp(u.crackSteps.div(4).floor(), 2, 6).toVar();
    Loop({ start: int(0), end: int(steps), type: "int", condition: "<" }, ({ i }: any) => {
      const depth = float(i).add(0.5).div(steps);
      const q = cutPos.add(V.mul(tExit).mul(depth)).toVar();
      const helper = erosion.sampleCrack(q);
      const qw = ErosionField.warpDomain(q, helper.xyz, u.crackLarge, u.crackWarp).add(
        seedV.mul(0.37),
      );
      const edge = (erosion as any).materialPlanes
        ? (erosion as any).materialPlanes.sample(qw)
        : voronoiEdge(qw, u.seed, u.crackCoverage);
      const width = max(u.crackWidth.mul(u.crackLarge), 0.001);
      const fissure = float(1).sub(smoothstep(width, width.mul(2.5), edge.w));
      const vein = float(1).sub(
        u.veinContrast.mul(
          fbm(q.mul(u.veinScale).add(seedV.mul(41)), 2)
            .mul(0.5)
            .add(0.5),
        ),
      );
      const fineEdge = (erosion as any).materialPlanes
        ? (erosion as any).materialPlanes.sample(
            qw.mul(u.crackFine.div(u.crackLarge)).add(seedV),
            true,
          )
        : voronoiEdge(
            qw.mul(u.crackFine.div(u.crackLarge)).add(seedV),
            u.seed.add(7),
            u.fineCoverage,
          );
      const fineWidth = max(u.crackWidth.mul(u.crackFine).mul(0.35), 0.001);
      const branch = float(1).sub(smoothstep(fineWidth, fineWidth.mul(2.5), fineEdge.w));
      const nearLarge = mix(
        float(1),
        float(1).sub(smoothstep(0.03, 0.25, edge.w)),
        u.fineNearLarge,
      );
      const feather = smoothstep(0.24, 0.68, fbm(q.mul(38).add(seedV), 2).mul(0.5).add(0.5));
      const filaments = fissure.mul(feather).add(branch.mul(u.crackFineAmount).mul(nearLarge));
      const solid = smoothstep(0.85, 0.35, erosion.sample(q).r);
      const illumination = abs(dot(edge.xyz, L)).mul(0.6).add(0.4);
      glint.addAssign(
        filaments
          .mul(vein)
          .mul(regionMask)
          .mul(solid)
          .mul(illumination)
          .mul(exp(depth.mul(tExit).div(max(u.attDist, 0.01)).negate())),
      );
    });
    const density = clamp(glint.div(steps), 0, 1);
    return vec4(density.mul(u.crackBright), float(1).sub(density.mul(u.crackDark).mul(0.15)), 0, 0);
  })();
  const m = Fn(() => {
    const result = vec4(0, 1, 0, 0).toVar();
    If(f.cracks.greaterThan(0.5), () => {
      result.assign(march);
    });
    return result;
  })();

  // Clear dielectric first. The seven surface layers remain independent and sparse.
  // Surface frost mostly broadens reflection; it must not turn the whole volume white.
  const opticalPath = max(tExit.mul(u.thicknessScale), 0.001);
  const densityAt = (q: N, depth: N) =>
    iceDetail(fractureDetail, q, nBase, u.inclusionScale, depth);
  const inclusions =
    ICE_VARIANT === "photographic"
      ? iceInclusions(fractureDetail, p, nBase, V, tExit, u.inclusionScale, u.inclusionAmount).mul(
          f.scatter,
        )
      : float(0);
  const frostCover = clamp(
    frost.mul(u.frostDiffuse).add(smudge.mul(u.smudgeWhite)).add(inclusions.mul(0.65)),
    0,
    0.65,
  );
  const surfaceDetail =
    ICE_VARIANT === "photographic" ? densityAt(p, float(0)).mul(f.frost) : float(0);
  material.roughnessNode = clamp(roughness.add(surfaceDetail.mul(0.22)), 0.015, 0.8);
  material.iorNode = u.ior;
  material.dispersionNode = u.dispersion.mul(f.dispersion);
  material.thicknessNode = opticalPath;
  material.attenuationDistanceNode = max(u.attDist, 0.01);
  material.attenuationColorNode = mix(vec3(1), u.attColor, f.absorption);
  material.colorNode = u.baseColor;
  material.clearcoatNode = u.clearcoat.mul(f.clearcoat);
  material.clearcoatRoughnessNode = u.clearcoatRough;
  material.specularIntensityNode = u.specularIntensity.mul(f.reflections);
  const volumeGlint = m.x.add(surfCrack.mul(0.3)).mul(u.keyIntensity).mul(0.3);
  const scatter = frostCover.mul(u.interiorScatter).mul(f.scatter);
  material.emissiveNode = u.keyColor.mul(volumeGlint.add(scatter.mul(0.3)));
  if (RW.matcap) {
    const nv = normalize(transformNormalToView(nSurface)),
      uv = nv.xy.mul(0.495).add(0.5);
    const spread = roughness.mul(roughness).mul(0.06),
      capSample = (v: N) => texture(glassMatcap, clamp(v, 0.002, 0.998)).rgb;
    const cap = capSample(uv)
      .mul(0.4)
      .add(capSample(uv.add(vec2(spread, 0))).mul(0.15))
      .add(capSample(uv.sub(vec2(spread, 0))).mul(0.15))
      .add(capSample(uv.add(vec2(0, spread))).mul(0.15))
      .add(capSample(uv.sub(vec2(0, spread))).mul(0.15))
      .mul(u.envStrength);
    material.outputNode = Fn(() => {
      cutHit.lessThan(0).discard();
      const cover = max(frostCover, float(1).sub(f.transmission));
      const frosted = u.baseColor.mul(cap.mul(0.5).add(vec3(0.2)).mul(u.keyIntensity));
      return vec4(
        mix(cap.mul(m.y), frosted, cover)
          .add(u.keyColor.mul(volumeGlint))
          .add(u.keyColor.mul(inclusions.mul(u.backlight).mul(0.8))),
        1,
      );
    })();
  } else if (ICE_VARIANT === "physical") {
    material.transmissionNode = f.transmission.mul(float(1).sub(frostCover));
    material.outputNode = Fn(() => {
      cutHit.lessThan(0).discard();
      return vec4(output.rgb.mul(m.y), output.a);
    })();
  } else {
    const optical = RW.fast
      ? fastOptics({ ray: V, normal: nSurface, environment, u, f, roughness, path: tExit })
      : opticalTransport({
          shape,
          erosion,
          p,
          ray: V,
          normal: nSurface,
          environment,
          backdrop,
          u,
          f,
          roughness,
        });
    // Keep physical lighting for the frosted fraction; custom transport owns the clear fraction.
    material.transmission = 0;
    material.transmissionNode = null;
    material.outputNode = Fn(() => {
      cutHit.lessThan(0).discard();
      const clear = optical.reflected.add(optical.transmitted.mul(m.y));
      const cover = max(frostCover, float(1).sub(f.transmission));
      const lit = clear.mul(float(1).sub(cover)).add(output.rgb.mul(cover));
      const frozenLight = u.keyColor.mul(inclusions.mul(u.backlight).mul(0.8));
      return vec4(lit.add(frozenLight).add(u.keyColor.mul(volumeGlint)), 1);
    })();
  }

  function update(
    _t: number,
    _dt: number,
    keyLight: THREE.DirectionalLight,
    objectGroup: THREE.Object3D,
  ) {
    if (D.version !== settingsVersion) {
      settingsVersion = D.version;
      const I = D.ice,
        E = D.erosion,
        S = D.shape,
        H = D.healing;
      for (const name of Object.keys(f))
        f[name].value = Number(I.features?.[name] ?? MATERIAL_FEATURES[name]);
      u.baseColor.value.set(I.baseColor ?? "#ffffff");
      u.envStrength.value = I.envIntensity;
      u.backlight.value = I.backlight ?? 0.4;
      u.inclusionAmount.value = I.inclusionAmount ?? 0.3;
      u.inclusionScale.value = I.inclusionScale ?? 0.4;
      u.ior.value = I.ior;
      u.dispersion.value = I.dispersion;
      u.thicknessScale.value = I.thicknessScale;
      u.attDist.value = I.attenuationDistance;
      u.attColor.value.set(I.attenuationColor);
      u.baseRough.value = I.baseRoughness;
      u.frostScale.value = I.frost.scale;
      u.frostThreshold.value = I.frost.threshold;
      u.frostSoftness.value = I.frost.softness;
      u.frostRough.value = I.frost.roughness;
      u.frostDiffuse.value = I.frost.diffuse;
      u.crystalBump.value = I.frost.crystalBump;
      u.crystalScale.value = I.frost.crystalScale;
      const C = I.cracks;
      u.crackLarge.value = C.largeScale;
      u.crackWarp.value = C.warp;
      u.crackCoverage.value = C.coverage;
      u.regionScale.value = C.regionScale;
      u.regionCoverage.value = C.regionCoverage;
      u.veinScale.value = C.veinScale;
      u.veinContrast.value = C.veinContrast;
      u.crackFine.value = C.fineScale;
      u.crackFineAmount.value = C.fineAmount;
      u.fineCoverage.value = C.fineCoverage;
      u.fineNearLarge.value = C.fineNearLarge;
      u.crackWidth.value = C.width;
      u.crackBright.value = C.brightness;
      u.crackDark.value = C.darkness;
      u.crackRefr.value = C.refraction;
      u.surfaceCrack.value = C.surfaceStrength;
      u.crackSteps.value = C.steps;
      u.adaptiveSteps.value = D.performance.adaptiveSteps ? 1 : 0;
      u.smudgeAmount.value = I.smudges.amount;
      u.smudgeCoverage.value = I.smudges.coverage;
      u.smudgeMaskScale.value = I.smudges.maskScale;
      u.smudgeAniso.value = I.smudges.anisotropy;
      u.smudgeRough.value = I.smudges.roughness;
      u.smudgeWhite.value = I.smudges.whiteness;
      u.smudgeScale.value = I.smudges.scale;
      u.microBump.value = I.bumps.microBump;
      u.microScale.value = I.bumps.microScale;
      u.microCoverage.value = I.bumps.microCoverage;
      u.rippleBump.value = I.bumps.rippleBump;
      u.rippleScale.value = I.bumps.rippleScale;
      u.bumpMaskScale.value = I.bumps.maskScale;
      u.clearcoat.value = I.clearcoat;
      u.clearcoatRough.value = I.clearcoatRoughness;
      u.specularIntensity.value = I.specularIntensity;
      u.interiorScatter.value = I.interiorScatter;
      u.deformAmp.value = S.deformAmplitude;
      u.deformFreq.value = S.deformFrequency;
      u.edgeWidth.value = E.edgeWidth;
      u.cutThreshold.value = E.cutThreshold;
      u.cutSoft.value = E.cutSoftness;
      u.edgeInset.value = E.edgeInset;
      u.interiorSteps.value = E.interiorSteps;
      u.interiorFrost.value = E.interiorFrost;
      u.crumbleGlow.value = I.crumbleGlow;
      u.edgeWhite.value = I.edgeWhiteness;
      u.edgeBump.value = E.edgeBump;
      u.edgeBumpScale.value = E.edgeBumpScale;
      u.refrostStrength.value = H.refrostStrength;
      u.powderTone.value.set(D.powder.baseTone);
      u.keyIntensity.value = D.lighting.key.intensity;
      u.keyColor.value.set(D.lighting.key.color);
    }
    // key direction in object space
    lightDir.copy(keyLight.position).sub(keyLight.target.position).normalize();
    objectGroup.getWorldQuaternion(objectRotation).invert();
    u.lightDir.value.copy(lightDir.applyQuaternion(objectRotation));
    u.model.value.copy(objectGroup.matrixWorld);
    u.modelInv.value.copy(objectGroup.matrixWorld).invert();
    // deterministic per-frame blue-noise offset (golden-ratio sequence) so TRAA converges instead of blinking
    const frameIndex = frameIndexOverride >= 0 ? frameIndexOverride : ++frameCounter;
    u.jitter.value.set((frameIndex * 0.7548776662) % 1, (frameIndex * 0.5698402909) % 1);
  }

  return { material, uniforms: u, features: f, update };
}
source/src/ice/MeshEntry.ts
// MVP: exact raster entry position and smooth mesh normal. The erosion/SDF path remains
// the fallback at every pixel whose original outer surface has been removed.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
const { pass, mrt, vec4, positionLocal, normalLocal } = tsl;
export function createMeshEntry(geometry: THREE.BufferGeometry, camera: THREE.Camera) {
  const scene = new THREE.Scene();
  const material = new THREE.MeshBasicNodeMaterial();
  material.side = THREE.FrontSide;
  material.toneMapped = false;
  const mesh = new THREE.Mesh(geometry, material);
  mesh.frustumCulled = false;
  mesh.matrixAutoUpdate = false;
  scene.add(mesh);
  const surfacePass = pass(scene, camera, { type: THREE.FloatType, samples: 0 });
  surfacePass.setMRT(mrt({ output: vec4(positionLocal, 1), normal: vec4(normalLocal, 0) }));
  const position = surfacePass.getTextureNode("output"),
    normal = surfacePass.getTextureNode("normal"),
    depth = surfacePass.getTextureNode("depth");
  for (const name of ["output", "normal"]) {
    const t = surfacePass.getTexture(name);
    t.minFilter = t.magFilter = THREE.NearestFilter;
    t.generateMipmaps = false;
  }
  return {
    surfacePass,
    position,
    normal,
    depth,
    mesh,
    material,
    update(source: THREE.Mesh, scale: number) {
      mesh.geometry = source.geometry;
      mesh.matrix.copy(source.matrixWorld);
      mesh.matrixWorld.copy(source.matrixWorld);
      mesh.visible = source.visible;
      surfacePass.setResolutionScale(scale);
    },
    dispose() {
      surfacePass.dispose();
      material.dispose();
    },
  };
}
source/src/ice/OpticalTransport.ts
// Single-boundary entry/exit transport with a bounded SDF exit trace. Reflection
// samples HDR studio cards; transmission projects through the background plane.
// This is a real-time optical approximation, not a multi-bounce path tracer.
import { iceEnvironment } from "./EnvironmentSampling";
import { tsl } from "../tsl/t";
import { backdropColorAt } from "../scene/Backdrop";
const {
  Fn,
  vec2,
  vec3,
  vec4,
  float,
  int,
  Loop,
  If,
  Break,
  normalize,
  length,
  max,
  min,
  abs,
  dot,
  pow,
  exp,
  log,
  mix,
  clamp,
  refract,
  reflect,
  select,
  mat3,
  texture,
  equirectUV,
  cameraViewMatrix,
  cameraProjectionMatrix,
  cross,
} = tsl;
type N = any;
export function opticalTransport(o: any) {
  const { shape, erosion, p, ray, normal, environment, backdrop, u, f, roughness } = o;
  const B = shape.bound,
    epsilon = Math.max(0.001, B / 1500);
  const worldDir = (d: N) => normalize(mat3(u.model).mul(d));
  const env = (direction: N, rough: N) =>
    iceEnvironment(environment, u.envStrength, direction, rough);
  const gradient = (q: N) => {
    const e = shape.voxelSize ? shape.voxelSize.mul(0.75) : float(B / 240);
    const g = vec3(
      shape.sdfNode(q.add(vec3(e, 0, 0))).sub(shape.sdfNode(q.sub(vec3(e, 0, 0)))),
      shape.sdfNode(q.add(vec3(0, e, 0))).sub(shape.sdfNode(q.sub(vec3(0, e, 0)))),
      shape.sdfNode(q.add(vec3(0, 0, e))).sub(shape.sdfNode(q.sub(vec3(0, 0, e)))),
    );
    return g.div(max(length(g), 1e-5));
  };
  const ior = max(u.ior, 1.001);
  const n = select(dot(normal, ray).greaterThan(0), normal.negate(), normal);
  const inside = normalize(refract(ray, n, float(1).div(ior)));
  const exit = Fn(() => {
    const distance = float(epsilon * 2).toVar();
    const last = float(0).toVar();
    Loop(64, () => {
      const q = p.add(inside.mul(distance));
      const sdf = shape.sdfNode(q);
      const er = erosion.sample(q).r;
      If(
        distance
          .greaterThan(epsilon * 4)
          .and(sdf.greaterThan(0).or(er.greaterThan(u.cutThreshold))),
        () => {
          Break();
        },
      );
      last.assign(distance);
      distance.addAssign(clamp(abs(sdf).mul(0.7), epsilon, B / 12));
      If(distance.greaterThan(B * 2), () => {
        Break();
      });
    });
    // Refine the boundary so thickness does not jump in visible steps.
    Loop(5, () => {
      const mid = last.add(distance).mul(0.5);
      const q = p.add(inside.mul(mid));
      If(shape.sdfNode(q).lessThan(0).and(erosion.sample(q).r.lessThan(u.cutThreshold)), () => {
        last.assign(mid);
      }).Else(() => {
        distance.assign(mid);
      });
    });
    return vec4(p.add(inside.mul(distance)), distance);
  })();
  const exitNormal = gradient(exit.xyz);
  const exitN = select(dot(exitNormal, inside).lessThan(0), exitNormal.negate(), exitNormal);
  const outward = refract(inside, exitN.negate(), ior);
  const tir = length(outward).lessThan(0.0001);
  const outgoing = select(tir, reflect(inside, exitN), outward);
  const exitWorld = u.model.mul(vec4(exit.xyz, 1)).xyz;
  const transmittedPlate = (direction: N) => {
    const dirW = worldDir(direction);
    const posV = cameraViewMatrix.mul(vec4(exitWorld, 1)).xyz;
    const dirV = mat3(cameraViewMatrix).mul(dirW);
    const t = float(-35).sub(posV.z).div(min(dirV.z, -0.001));
    const q = posV.add(dirV.mul(clamp(t, 0, 150)));
    const clip = cameraProjectionMatrix.mul(vec4(q, 1));
    const uv = vec2(
      clip.x.div(max(clip.w, 0.001)).mul(0.5).add(0.5),
      clip.y.div(max(clip.w, 0.001)).mul(-0.5).add(0.5),
    );
    const background = backdropColorAt(backdrop, clamp(uv, 0, 1));
    // Backlighting is an authored studio source behind the translucent volume.
    return background.add(env(dirW, roughness).mul(u.backlight));
  };
  const dispersion = u.dispersion.mul(f.dispersion).mul(0.025);
  const spreadDir = normalize(cross(inside, exitN).add(vec3(0.00001, 0, 0)));
  const transmitted = vec3(
    transmittedPlate(normalize(outgoing.add(spreadDir.mul(dispersion)))).r,
    transmittedPlate(outgoing).g,
    transmittedPlate(normalize(outgoing.sub(spreadDir.mul(dispersion)))).b,
  );
  const f0 = pow(ior.sub(1).div(ior.add(1)), 2);
  const fresnel = f0
    .add(
      float(1)
        .sub(f0)
        .mul(pow(float(1).sub(clamp(dot(n, ray.negate()), 0, 1)), 5)),
    )
    .mul(f.reflections);
  const exitFresnel = select(
    tir,
    float(1),
    f0.add(
      float(1)
        .sub(f0)
        .mul(pow(float(1).sub(abs(dot(exitN, inside))), 5)),
    ),
  );
  const distance = exit.w.mul(u.thicknessScale);
  const attenuation = exp(
    log(max(u.attColor, vec3(0.001)))
      .mul(distance.div(max(u.attDist, 0.01)))
      .mul(f.absorption),
  );
  const reflected = env(worldDir(reflect(ray, n)), roughness);
  const innerReflection = env(worldDir(reflect(inside, exitN)), roughness);
  const through = transmitted
    .mul(float(1).sub(exitFresnel))
    .add(innerReflection.mul(exitFresnel))
    .mul(attenuation);
  return {
    reflected: reflected.mul(fresnel),
    transmitted: through.mul(float(1).sub(fresnel)),
    distance,
  };
}
source/src/ice/optics.ts
/** Authored thin-film response, independent of saved workbench dial values.
 * The dials retain their values/ranges; these gains keep surface effects subordinate
 * to transmission instead of adding several opaque, white layers to the same ice.
 */
export const ICE_OPTICS = Object.freeze({
  frostVeil: 0.045,
  frostDiffuse: 0.18,
  frostRoughness: 0.22,
  smudgeDiffuse: 0.18,
  smudgeRoughness: 0.16,
  normalDetail: 0.18,
  clearcoat: 0.16,
  clearcoatRoughness: 0.22,
  specular: 0.55,
  scatterRadiance: 0.025,
});
source/src/ice/SharedSurface.ts
// One surface evaluator for intact ice and detached shards. No independent finish.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
import { rng } from "../core/seed";
import { ErosionField } from "../erosion/ErosionField";
import { iceFrost } from "./SurfaceDetail";
import { fbm, gnoise, vnoise3, softThreshold, saturate, voronoiEdge } from "../tsl/noise";
const { Fn, If, vec3, vec4, float, dot, smoothstep, normalize, clamp, dFdx, dFdy, length, max } =
  tsl;
type N = any;
export function iceSmudgeDirections(seed: number) {
  const rand = rng(seed + 11);
  return [0, 1, 2].map(() => {
    const v = new THREE.Vector3(rand() * 2 - 1, rand() * 2 - 1, rand() * 2 - 1).normalize();
    return vec3(v.x, v.y, v.z);
  });
}
export function iceSurface(
  p: N,
  nBase: N,
  seedV: N,
  smudgeDirs: N[],
  erosion: any,
  u: any,
  f: any,
  filter = false,
) {
  const band = (scale: N) =>
    filter
      ? float(1).sub(
          smoothstep(0.35, 1.25, max(length(dFdx(p.mul(scale))), length(dFdy(p.mul(scale))))),
        )
      : float(1);
  // frost
  // Sample at the visible solid hit, consistently before and during breakup.
  const frost0 = iceFrost(p, seedV, u.frostScale, u.frostThreshold, u.frostSoftness);
  const frost = saturate(frost0.add(u.interiorFrost)).mul(f.frost);

  // smudges: anisotropic streaks in 3 directions + blotchy prints (evaluated at the solid hit)
  const stretch = (q: N, d: N) =>
    q.sub(d.mul(dot(q, d)).mul(float(1).sub(float(1).div(u.smudgeAniso))));
  const smudgeNoise = vec4(
    gnoise(stretch(p.mul(u.smudgeScale), smudgeDirs[0]).add(seedV.mul(3))),
    gnoise(stretch(p.mul(u.smudgeScale), smudgeDirs[1]).add(seedV.mul(4))),
    gnoise(stretch(p.mul(u.smudgeScale), smudgeDirs[2]).add(seedV.mul(5))),
    gnoise(p.mul(u.smudgeScale.mul(1.7)).add(seedV.mul(7.0))),
  );
  const streaks = smoothstep(0.45, 0.85, smudgeNoise.x.mul(0.5).add(0.5))
    .add(smoothstep(0.45, 0.85, smudgeNoise.y.mul(0.5).add(0.5)))
    .add(smoothstep(0.45, 0.85, smudgeNoise.z.mul(0.5).add(0.5)));
  const blotch = smoothstep(0.6, 0.72, smudgeNoise.w.mul(0.5).add(0.5));
  // noise masks: where smudges / micro bumps / cracks apply (low frequency)
  const maskNoise = vec3(
    fbm(p.mul(u.smudgeMaskScale).add(seedV.mul(21.0)), 2)
      .mul(0.5)
      .add(0.5),
    fbm(p.mul(u.bumpMaskScale).add(seedV.mul(27.0)), 2)
      .mul(0.5)
      .add(0.5),
    fbm(p.mul(u.regionScale).add(seedV.mul(33.0)), 2)
      .mul(0.5)
      .add(0.5),
  );
  const smudgeMask = softThreshold(maskNoise.x, float(1).sub(u.smudgeCoverage), 0.35);
  const microMask = softThreshold(maskNoise.y, float(1).sub(u.microCoverage), 0.35);
  const regionMask = softThreshold(maskNoise.z, float(1).sub(u.regionCoverage), 0.45);
  const smudge = saturate(
    streaks.div(3).add(blotch.mul(0.6)).mul(u.smudgeAmount).mul(smudgeMask),
  ).mul(f.smudges);

  // surface cracks (distance to the nearest Voronoi boundary at the surface)
  const crackHelper = erosion.sampleCrack(p);
  const warpedP = ErosionField.warpDomain(p, crackHelper.xyz, u.crackLarge, u.crackWarp).add(
    seedV.mul(0.37),
  );
  const ve = Fn(() => {
    const edge = vec4(0, 0, 1, 1).toVar();
    If(f.cracks.greaterThan(0.5), () => {
      edge.assign(
        erosion.materialPlanes
          ? erosion.materialPlanes.sample(warpedP)
          : voronoiEdge(warpedP, u.seed, u.crackCoverage),
      );
    });
    return edge;
  })();
  const dEdge = ve.w.div(u.crackLarge);
  const veinSurf = float(1).sub(
    u.veinContrast.mul(
      fbm(p.mul(u.veinScale).add(seedV.mul(41.0)), 2)
        .mul(0.5)
        .add(0.5),
    ),
  );
  const surfCrack = smoothstep(u.crackWidth.mul(5.0), 0.0, dEdge)
    .mul(u.surfaceCrack)
    .mul(regionMask)
    .mul(veinSurf)
    .mul(f.cracks);

  // Independent normal layers; none is activated by the breakup field.
  const topness = saturate(nBase.y.mul(0.5).add(0.5));
  const crystals = vnoise3(p.mul(u.crystalScale).add(seedV))
    .mul(u.crystalBump)
    .mul(0.6)
    .mul(f.crystals)
    .mul(band(u.crystalScale));
  const grain = vnoise3(p.mul(u.edgeBumpScale).add(seedV.mul(13.0)))
    .mul(u.edgeBump)
    .mul(0.5)
    .mul(f.grain)
    .mul(band(u.edgeBumpScale));
  const micro = vnoise3(p.mul(u.microScale).add(seedV.mul(5.0)))
    .mul(u.microBump)
    .mul(microMask)
    .mul(f.micro)
    .mul(band(u.microScale));
  const ripple = vnoise3(p.mul(u.rippleScale).add(seedV.mul(9.0)))
    .mul(u.rippleBump)
    .mul(topness)
    .mul(f.ripples)
    .mul(band(u.rippleScale));
  // The master switch removes normal perturbations without discarding their saved
  // strengths, crack colour, frost coverage or genuine fracture-face normals.
  const detailNormal = crystals
    .add(grain)
    .sub(micro)
    .sub(ripple)
    .sub(ve.xyz.mul(surfCrack).mul(0.6));
  const nSurface = normalize(nBase.add(detailNormal.mul(f.surfaceBumps)));
  return {
    frost,
    smudge,
    surfCrack,
    nSurface,
    regionMask,
    roughness: clamp(
      u.baseRough
        .add(frost.mul(u.frostRough))
        .add(smudge.mul(u.smudgeRough))
        .add(surfCrack.mul(0.12)),
      0.015,
      0.8,
    ),
  };
}
source/src/ice/SurfaceDetail.ts
// Shared object-space surface detail for the solid and detached pieces.
// Shards sample their home coordinate, so the texture travels with the ice.
import { tsl } from "../tsl/t";
import { fbm, softThreshold } from "../tsl/noise";
const { Fn, Loop, int, exp, clamp, vec2, vec3, float, texture, abs, pow, max, smoothstep } = tsl;
type N = any;
export function iceDetail(map: any, p: N, n: N, scale: N, depth: N = float(0)) {
  if (!map) return float(0);
  const w = pow(abs(n), vec3(6));
  const weight = w.div(max(w.x.add(w.y).add(w.z), 0.001));
  const uv = (v: N, o: N) =>
    vec2(v.x.mul(0.932).sub(v.y.mul(0.362)), v.x.mul(0.362).add(v.y.mul(0.932)))
      .mul(scale)
      .add(o)
      .add(vec2(depth.mul(0.217), depth.mul(0.371)));
  return texture(map, uv(p.yz, vec2(0.317, 0.173)))
    .r.mul(weight.x)
    .add(texture(map, uv(p.zx, vec2(0.619, 0.431))).r.mul(weight.y))
    .add(texture(map, uv(p.xy, vec2(0))).r.mul(weight.z));
}
export function iceFrost(p: N, seed: N, scale: N, threshold: N, softness: N) {
  const region = softThreshold(
    fbm(p.mul(scale).add(seed.mul(2)), 3)
      .mul(0.5)
      .add(0.5),
    threshold,
    softness,
  );
  const crystals = fbm(p.mul(scale.mul(32)).add(seed.mul(3.1)), 3)
    .mul(0.5)
    .add(0.5);
  // Granular deposits with broken edges, not smooth white fog painted over glass.
  return pow(region, 1.5).mul(smoothstep(0.3, 0.69, crystals));
}

// The same three-depth inclusion density for intact ice and individual fragments.
export function iceInclusions(map: any, p: N, n: N, ray: N, path: N, scale: N, amount: N) {
  return Fn(() => {
    const sum = float(0).toVar();
    Loop({ start: int(0), end: int(3), type: "int", condition: "<" }, ({ i }: any) => {
      const depth = float(i).add(0.5).div(3);
      const q = p.add(ray.mul(path).mul(depth));
      sum.addAssign(pow(iceDetail(map, q, n, scale, depth), 1.6).mul(exp(depth.mul(-0.7))));
    });
    return clamp(sum.div(3).mul(amount), 0, 0.45);
  })();
}
source/src/ice/variant.ts
export const ICE_VARIANT: string = "photographic";
source/src/motion/editor.ts
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import preset from "../../presets/approved-material.json";
import {
  parseTrack,
  emptyTrack,
  ShotHistory,
  baselineCamera,
  applyCameraAt,
  cameraAt,
  type CameraKey,
  type CameraTrack,
  type V3,
  type Q4,
} from "./track";
import { objectFrameAt, shapeAt } from "./objectFrame";
import { skeletonMeshes } from "./meshes";
import { resolveSchedule, durationOf } from "../core/schedule";
const $ = <T extends HTMLElement = HTMLElement>(id: string) => document.getElementById(id) as T;
const field = (id: string) => $<HTMLInputElement>(id),
  select = (id: string) => $<HTMLSelectElement>(id);
const status = (s: string) => {
  $("status").textContent = s;
};
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v));
const store = {
  get: (key: string) => {
    try {
      return localStorage.getItem(key);
    } catch {
      return null;
    }
  },
  set: (key: string, value: string) => {
    try {
      localStorage.setItem(key, value);
    } catch {
      status("Browser storage unavailable. Export JSON to keep this shot.");
    }
  },
};
let values: any = { ...preset },
  time = 0,
  playing = false,
  draft = false,
  selected = "",
  roll = 0,
  syncing = false,
  dirty = true,
  ready = false;
let fullWindow: Window | null = null;
const history = new ShotHistory();
try {
  const raw = new URLSearchParams(location.hash.slice(1)).get("vars");
  if (raw) values = { ...values, ...JSON.parse(raw) };
  const saved = store.get("frost-motion-draft-v1");
  if (saved) history.track = parseTrack(saved);
  if (values.cameraMode === "authored") history.track = parseTrack(values.cameraTrack);
} catch (e) {
  status(String(e));
}
const schedule = () =>
  resolveSchedule(
    {
      logoBreak: +values.logoBreakAt,
      form1: +values.formHeadline1At,
      break1: +values.headline1BreakAt,
      form2: +values.formHeadline2At,
      break2: +values.headline2BreakAt,
      fadeOut: +values.fadeOutAt,
    },
    +values.breakDuration,
  );
let S = schedule(),
  duration = durationOf(S);
const camera = baselineCamera(),
  overviewCamera = new THREE.PerspectiveCamera(45, 320 / 220, 0.1, 150);
overviewCamera.position.set(19, 14, 22);
overviewCamera.lookAt(0, 0, 4);
const renderer = new THREE.WebGLRenderer({
    canvas: $<HTMLCanvasElement>("preview"),
    antialias: true,
  }),
  overviewRenderer = new THREE.WebGLRenderer({
    canvas: $<HTMLCanvasElement>("overview-canvas"),
    antialias: true,
  });
for (const r of [renderer, overviewRenderer]) {
  r.setPixelRatio(Math.min(devicePixelRatio, 1.5));
  r.setClearColor("#161b21");
}
const scene = new THREE.Scene(),
  motion = new THREE.Group(),
  object = new THREE.Group();
scene.add(motion);
motion.add(object);
scene.add(new THREE.HemisphereLight("#d9edff", "#485260", 2));
const light = new THREE.DirectionalLight("#ffffff", 2.5);
light.position.set(3, 7, 9);
scene.add(light);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = false;
controls.target.set(0, 0.05, 0);
controls.minDistance = 0.3;
controls.maxDistance = 100;
controls.update();
const overviewControls = new OrbitControls(overviewCamera, overviewRenderer.domElement);
overviewControls.target.set(0, 0, 3);
overviewControls.enableDamping = false;
overviewControls.update();
overviewCamera.layers.enable(1);
const grid = new THREE.GridHelper(40, 20, 0x425c6e, 0x2b3945);
grid.position.y = -3;
grid.layers.set(1);
scene.add(grid);
const pathGroup = new THREE.Group();
pathGroup.layers.set(1);
scene.add(pathGroup);
const helper = new THREE.CameraHelper(camera);
helper.layers.set(1);
scene.add(helper);
let meshes: THREE.Mesh[] = [],
  markers: THREE.Mesh[] = [];
const track = () => history.track;
const frame = () =>
  objectFrameAt(time, S, values, shapeAt(time, S), track().objectMotion === "stationary");
function objectsAt() {
  const f = frame();
  motion.position.copy(f.position);
  motion.quaternion.copy(f.rotation);
  object.quaternion.copy(f.facing);
  motion.updateMatrixWorld(true);
  const k = shapeAt(time, S),
    breaking = time >= [S.logoBreak, S.break1, S.break2][k];
  meshes.forEach((m, i) => {
    m.visible = i === k;
    (m.material as THREE.MeshLambertMaterial).wireframe = breaking;
  });
  $("phase").textContent =
    ["Logo", "Headline 1", "Headline 2"][k] + (breaking ? " · breakup ghost" : " · solid geometry");
  return f;
}
function cameraReplay() {
  syncing = true;
  const f = objectsAt();
  applyCameraAt(camera, track(), time, f.matrix, controls.target);
  const state = cameraAt(track(), time, f.matrix);
  if (track().orientation === "free")
    controls.target
      .copy(camera.position)
      .add(
        new THREE.Vector3(0, 0, -1)
          .applyQuaternion(camera.quaternion)
          .multiplyScalar(Math.max(1, camera.position.distanceTo(state.target))),
      );
  controls.update();
  applyCameraAt(camera, track(), time, f.matrix, new THREE.Vector3());
  roll = state.roll;
  field("view-fov").value = camera.fov.toFixed(2);
  field("view-zoom").value = camera.zoom.toFixed(3);
  field("view-roll").value = roll.toFixed(2);
  syncing = false;
}
function seek(t: number) {
  time = THREE.MathUtils.clamp(t, 0, duration);
  draft = false;
  cameraReplay();
  field("time").value = String(time);
  $("time-label").textContent = `${time.toFixed(3)} / ${duration.toFixed(3)} s`;
  $("view-state").textContent = "CAMERA · Track replay";
  dirty = true;
}
controls.addEventListener("change", () => {
  if (syncing) return;
  playing = false;
  draft = true;
  if (roll) camera.rotateZ(THREE.MathUtils.degToRad(roll));
  $("view-state").textContent = "CAMERA · Uncaptured view";
  dirty = true;
});
overviewControls.addEventListener("change", () => (dirty = true));
function freshId() {
  let i = 1;
  while (track().keys.some((k) => k.id === "key-" + i)) i++;
  return "key-" + i;
}
function commit(next: CameraTrack, replay = true) {
  if (next.keys.some((k) => k.time > duration))
    throw Error(`Keep camera keys within ${duration} seconds.`);
  history.commit(next);
  store.set("frost-motion-draft-v1", JSON.stringify(track()));
  refresh();
  if (replay) seek(time);
  dirty = true;
}
function capture(update = false) {
  if (!ready) return;
  const next = clone(track()),
    previous = next.keys.find((k) => k.id === selected),
    target = controls.target.clone();
  if (next.anchorSpace === "local") target.applyMatrix4(frame().matrix.clone().invert());
  const q = camera.quaternion
    .clone()
    .multiply(
      new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), (-roll * Math.PI) / 180),
    );
  const key: CameraKey = {
    id: update && previous ? previous.id : freshId(),
    time,
    position: camera.position.toArray() as V3,
    target: target.toArray() as V3,
    quaternion: q.toArray() as Q4,
    fov: camera.fov,
    zoom: camera.zoom,
    roll,
    ease: previous?.ease || "flow",
  };
  if (update && !previous) throw Error("Select a key to update.");
  if (update) next.keys[next.keys.findIndex((k) => k.id === selected)] = key;
  else next.keys.push(key);
  selected = key.id;
  commit(next);
  status(`Captured ${key.id} at ${time.toFixed(3)} s. Scrub or play to replay.`);
}
function editKey(change: (k: CameraKey) => void) {
  const next = clone(track()),
    key = next.keys.find((k) => k.id === selected);
  if (!key) return;
  change(key);
  commit(next);
}
function path() {
  for (const child of [...pathGroup.children]) {
    pathGroup.remove(child);
    (child as THREE.Mesh).geometry?.dispose();
    const mat = (child as THREE.Mesh).material;
    if (mat && !Array.isArray(mat)) mat.dispose();
  }
  markers = [];
  if (!track().keys.length) return;
  const points = [];
  for (let i = 0; i <= 200; i++) points.push(cameraAt(track(), (duration * i) / 200).position);
  const line = new THREE.Line(
    new THREE.BufferGeometry().setFromPoints(points),
    new THREE.LineBasicMaterial({ color: 0x7ba7c5 }),
  );
  line.layers.set(1);
  pathGroup.add(line);
  for (const key of track().keys) {
    const m = new THREE.Mesh(
      new THREE.SphereGeometry(0.17, 10, 8),
      new THREE.MeshBasicMaterial({ color: key.id === selected ? 0xffd27b : 0x9adefc }),
    );
    m.position.fromArray(key.position);
    m.userData.key = key.id;
    m.layers.set(1);
    pathGroup.add(m);
    markers.push(m);
  }
}
function refresh() {
  field("name").value = track().name;
  select("orientation").value = track().orientation;
  select("anchor").value = track().anchorSpace;
  select("objects").value = track().objectMotion;
  const list = select("key-list");
  list.replaceChildren();
  const lane = $("keys");
  lane.replaceChildren();
  for (const k of track().keys) {
    const o = new Option(`${k.time.toFixed(3)} s · ${k.id}`, k.id);
    list.add(o);
    const b = document.createElement("button");
    b.className = "key" + (k.id === selected ? " selected" : "");
    b.style.left = `${(k.time / duration) * 100}%`;
    b.textContent = String(track().keys.indexOf(k) + 1);
    b.setAttribute("aria-label", `${k.id} at ${k.time.toFixed(3)} seconds; drag to retime`);
    b.onpointerdown = (e) => {
      e.stopPropagation();
      selected = k.id;
      b.setPointerCapture(e.pointerId);
      const before = clone(track());
      let moved = false;
      const move = (event: PointerEvent) => {
        moved = true;
        const rect = lane.getBoundingClientRect();
        const t = THREE.MathUtils.clamp(
          ((event.clientX - rect.left) / rect.width) * duration,
          0,
          duration,
        );
        b.style.left = `${(t / duration) * 100}%`;
        field("key-time").value = t.toFixed(3);
      };
      b.onpointermove = move;
      b.onpointerup = () => {
        b.onpointermove = null;
        b.onpointerup = null;
        if (moved) {
          const next = clone(before);
          next.keys.find((x) => x.id === k.id)!.time = +field("key-time").value;
          safe(() => commit(next));
        } else {
          refresh();
          seek(k.time);
        }
      };
    };
    lane.append(b);
  }
  if (!track().keys.length) list.add(new Option("No keys captured", ""));
  if (!track().keys.some((k) => k.id === selected)) selected = track().keys[0]?.id || "";
  list.value = selected;
  const k = track().keys.find((k) => k.id === selected);
  if (k) {
    field("key-time").value = String(k.time);
    select("ease").value = k.ease;
    ["px", "py", "pz"].forEach((id, j) => (field(id).value = k.position[j].toFixed(4)));
    ["tx", "ty", "tz"].forEach((id, j) => (field(id).value = k.target[j].toFixed(4)));
  }
  path();
}
function safe(fn: () => void) {
  try {
    fn();
  } catch (e) {
    status(String(e));
  }
}
function button(id: string, fn: () => void) {
  $(id).onclick = () => safe(fn);
}
button("capture", () => capture());
button("update", () => capture(true));
button("play", () => {
  playing = !playing;
  if (playing) {
    draft = false;
    if (time >= duration) seek(0);
  }
});
button("undo", () => {
  history.undo();
  refresh();
  seek(time);
  store.set("frost-motion-draft-v1", JSON.stringify(track()));
});
button("redo", () => {
  history.redo();
  refresh();
  seek(time);
  store.set("frost-motion-draft-v1", JSON.stringify(track()));
});
field("time").oninput = () => {
  playing = false;
  seek(+field("time").value);
};
$("keys").onclick = (e) => {
  if (e.target !== $("keys")) return;
  const rect = $("keys").getBoundingClientRect();
  seek(((e.clientX - rect.left) / rect.width) * duration);
};
select("key-list").onchange = () => {
  selected = select("key-list").value;
  refresh();
  seek(track().keys.find((k) => k.id === selected)!.time);
};
field("key-time").onchange = () => safe(() => editKey((k) => (k.time = +field("key-time").value)));
select("ease").onchange = () => safe(() => editKey((k) => (k.ease = select("ease").value as any)));
for (const [ids, attr] of [
  [["px", "py", "pz"], "position"],
  [["tx", "ty", "tz"], "target"],
] as const)
  for (let j = 0; j < 3; j++)
    field(ids[j]).onchange = () => safe(() => editKey((k) => (k[attr][j] = +field(ids[j]).value)));
for (const [id, attr] of [
  ["name", "name"],
  ["orientation", "orientation"],
  ["anchor", "anchorSpace"],
  ["objects", "objectMotion"],
])
  field(id).onchange = () =>
    safe(() => {
      const next = clone(track());
      if (attr === "anchorSpace" && next.anchorSpace !== field(id).value) {
        for (const k of next.keys) {
          const m = objectFrameAt(
            k.time,
            S,
            values,
            shapeAt(k.time, S),
            next.objectMotion === "stationary",
          ).matrix;
          if (field(id).value === "local") m.invert();
          k.target = new THREE.Vector3(...k.target).applyMatrix4(m).toArray() as V3;
        }
      }
      (next as any)[attr] = field(id).value;
      commit(next);
    });
for (const id of ["view-fov", "view-zoom", "view-roll"])
  field(id).onchange = () =>
    safe(() => {
      const f = +field("view-fov").value,
        z = +field("view-zoom").value,
        r = +field("view-roll").value;
      if (f < 5 || f > 120 || z < 0.1 || z > 10 || !Number.isFinite(r) || Math.abs(r) > 720)
        throw Error("FOV 5–120, zoom 0.1–10, roll −720–720.");
      camera.fov = f;
      camera.zoom = z;
      camera.rotateZ(((r - roll) * Math.PI) / 180);
      roll = r;
      camera.updateProjectionMatrix();
      draft = true;
      playing = false;
      dirty = true;
      $("view-state").textContent = "CAMERA · Uncaptured view";
    });
button("duplicate", () => {
  const next = clone(track()),
    k = next.keys.find((k) => k.id === selected);
  if (!k) return;
  const copy = clone(k);
  copy.id = freshId();
  copy.time = Math.min(duration, k.time + 0.5);
  while (next.keys.some((x) => Math.abs(x.time - copy.time) < 1 / 240) && copy.time > 0)
    copy.time -= 1 / 30;
  next.keys.push(copy);
  selected = copy.id;
  commit(next);
  seek(copy.time);
});
button("delete", () => {
  commit({ ...clone(track()), keys: track().keys.filter((k) => k.id !== selected) });
});
function library(): Map<string, CameraTrack> {
  try {
    return new Map(Object.entries(JSON.parse(store.get("frost-motion-shots-v1") || "{}")));
  } catch {
    return new Map();
  }
}
function refreshLibrary() {
  select("saved").replaceChildren(...[...library().keys()].sort().map((n) => new Option(n, n)));
}
button("save", () => {
  const shots = library();
  shots.set(track().name, clone(track()));
  store.set("frost-motion-shots-v1", JSON.stringify(Object.fromEntries(shots)));
  refreshLibrary();
  select("saved").value = track().name;
  status("Saved shot: " + track().name);
});
button("load", () => {
  const shot = library().get(select("saved").value);
  if (shot) commit(parseTrack(shot));
});
const appliedValues = (original = false) => ({
  ...values,
  cameraMode: original ? "original" : "authored",
  cameraTrack: original ? "" : JSON.stringify(parseTrack(track())),
});
function download(name: string, data: any) {
  const a = document.createElement("a"),
    url = URL.createObjectURL(
      new Blob([JSON.stringify(data, null, 2) + "\n"], { type: "application/json" }),
    );
  a.href = url;
  a.download = name;
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
button("export", () => download("frost-shot.json", parseTrack(track())));
button("export-preset", () => download("frost-motion-preset.json", appliedValues()));
button("import", () => field("file").click());
field("file").onchange = async () => {
  try {
    const file = field("file").files?.[0];
    if (!file) return;
    const data = JSON.parse(await file.text());
    if (data.version === 1) {
      commit(parseTrack(data));
    } else {
      const parsed = parseTrack(data.cameraTrack || "");
      values = { ...preset, ...data };
      S = schedule();
      duration = durationOf(S);
      field("time").max = String(duration);
      history.commit(parsed);
      await loadMeshes();
      refresh();
      seek(Math.min(time, duration));
    }
    status("JSON imported.");
  } catch (e) {
    status(String(e));
  } finally {
    field("file").value = "";
  }
};
function apply(original = false) {
  if (!original && !track().keys.length) throw Error("Capture at least one view before applying.");
  const vars = appliedValues(original),
    url = "/workbench?block=frost#vars=" + encodeURIComponent(JSON.stringify(vars));
  store.set("frost-motion-applied-v1", JSON.stringify(vars));
  if (fullWindow && !fullWindow.closed) fullWindow.location.href = url;
  else fullWindow = window.open(url, "frost-motion-full");
  $("applied").textContent = original
    ? "Original camera restored in full Frost."
    : "Applied " + track().name + " to full Frost. Export preset for Studio/capture.";
  status(
    "Full Frost opened with explicit camera variables. Approved repository defaults remain unchanged.",
  );
}
button("apply", () => apply());
button("reset", () => {
  commit(emptyTrack());
  roll = 0;
  seek(time);
  apply(true);
});
// Direct path manipulation in the overview's view plane. One undo step per drag.
const ray = new THREE.Raycaster();
ray.layers.set(1);
let drag: { id: string; before: CameraTrack; plane: THREE.Plane; offset: THREE.Vector3 } | null =
  null;
overviewRenderer.domElement.addEventListener(
  "pointerdown",
  (e) => {
    const rect = overviewRenderer.domElement.getBoundingClientRect();
    ray.setFromCamera(
      new THREE.Vector2(
        ((e.clientX - rect.left) / rect.width) * 2 - 1,
        (-(e.clientY - rect.top) / rect.height) * 2 + 1,
      ),
      overviewCamera,
    );
    const hit = ray.intersectObjects(markers)[0];
    if (!hit) return;
    e.stopImmediatePropagation();
    overviewControls.enabled = false;
    selected = hit.object.userData.key;
    const point = hit.object.position.clone(),
      plane = new THREE.Plane().setFromNormalAndCoplanarPoint(
        overviewCamera.getWorldDirection(new THREE.Vector3()),
        point,
      ),
      intersection = new THREE.Vector3();
    ray.ray.intersectPlane(plane, intersection);
    drag = { id: selected, before: clone(track()), plane, offset: point.sub(intersection) };
    overviewRenderer.domElement.setPointerCapture(e.pointerId);
  },
  true,
);
overviewRenderer.domElement.addEventListener("pointermove", (e) => {
  if (!drag) return;
  const rect = overviewRenderer.domElement.getBoundingClientRect();
  ray.setFromCamera(
    new THREE.Vector2(
      ((e.clientX - rect.left) / rect.width) * 2 - 1,
      (-(e.clientY - rect.top) / rect.height) * 2 + 1,
    ),
    overviewCamera,
  );
  const point = new THREE.Vector3();
  if (ray.ray.intersectPlane(drag.plane, point)) {
    track().keys.find((k) => k.id === drag!.id)!.position = point.add(drag.offset).toArray() as V3;
    path();
    seek(time);
  }
});
overviewRenderer.domElement.addEventListener("pointerup", () => {
  if (!drag) return;
  const next = clone(track());
  history.track = drag.before;
  drag = null;
  overviewControls.enabled = true;
  safe(() => commit(next));
});
window.addEventListener("keydown", (e) => {
  if ((e.target as HTMLElement).matches("input,select,textarea")) return;
  if (e.code === "Space") {
    e.preventDefault();
    playing = !playing;
  }
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
    e.preventDefault();
    $(e.shiftKey ? "redo" : "undo").click();
  }
});
async function loadMeshes() {
  ready = false;
  status("Preparing cached structural meshes…");
  const geos = await skeletonMeshes(values, status);
  for (const m of meshes) {
    object.remove(m);
    (m.material as THREE.Material).dispose();
  }
  meshes = geos.map((g) => {
    const mesh = new THREE.Mesh(g, new THREE.MeshLambertMaterial({ color: 0xa8c2cd }));
    object.add(mesh);
    return mesh;
  });
  ready = true;
  status("Ready · lightweight meshes only · full Frost loads only when you Apply.");
}
const resize = () => {
  for (const [r, el] of [
    [renderer, $("camera-view")],
    [overviewRenderer, $("overview")],
  ] as const) {
    const rect = el.getBoundingClientRect();
    r.setSize(rect.width, rect.height, false);
  }
  overviewCamera.aspect = $("overview").clientWidth / $("overview").clientHeight;
  overviewCamera.updateProjectionMatrix();
  dirty = true;
};
new ResizeObserver(resize).observe($("camera-view"));
resize();
let last = 0;
function animate(now: number) {
  requestAnimationFrame(animate);
  if (playing && ready) {
    seek(time + Math.min(0.1, (now - last) / 1000));
    if (time >= duration) playing = false;
  }
  last = now;
  $("play").textContent = playing ? "Pause" : "Play";
  if (dirty) {
    objectsAt();
    camera.updateMatrixWorld(true);
    helper.update();
    renderer.render(scene, camera);
    overviewRenderer.render(scene, overviewCamera);
    dirty = false;
  }
}
requestAnimationFrame(animate);
refresh();
refreshLibrary();
loadMeshes()
  .then(() => seek(0))
  .catch((e) => status("Mesh load failed: " + e));
// Small, inspectable automation surface for capture/edit/apply integration checks.
(window as any).__frostMotion = {
  build: "frost-motion-20260905-r13",
  get ready() {
    return ready;
  },
  get track() {
    return clone(track());
  },
  get values() {
    return clone(values);
  },
  get time() {
    return time;
  },
  camera,
  overviewCamera,
  seek,
  capture,
  commit,
  history,
  appliedValues,
  get matrices() {
    return { camera: camera.matrixWorld.toArray(), object: frame().matrix.toArray() };
  },
  get renderer() {
    return "WebGL skeleton; no World/SDF/particles/post";
  },
};
source/src/motion/meshes.ts
/** Skeleton meshes only: identical extrusion/deformation, cached independently of all simulation data. */
import * as THREE from "three/webgpu";
import { extrudeShapes, loadLogoShapes } from "../shape/logo";
import { loadTypeface, textShapes, lineWidth } from "../shape/text";
import { deformGeometry } from "../shape/deform";
import { D } from "../dials/store";
const memory = new Map<string, Promise<THREE.BufferGeometry[]>>();
async function database() {
  return new Promise<IDBDatabase>((resolve, reject) => {
    const q = indexedDB.open("frost-motion-meshes", 1);
    q.onupgradeneeded = () => q.result.createObjectStore("meshes");
    q.onsuccess = () => resolve(q.result);
    q.onerror = () => reject(q.error);
  });
}
export function skeletonMeshes(v: any, progress: (s: string) => void) {
  const key = JSON.stringify([
    "r13",
    v.logoUrl,
    v.headline1,
    v.headline2,
    v.fontWeight,
    v.textWidth,
    v.letterSpacing,
    v.textLineHeight,
    v.textDepth,
    v.textBevel,
    v.textCorner,
    v.deformStrength,
    v.deformScale,
    v.deformSeed,
    v.textMeshDetail,
    v.logoMeshDetail,
    D.shape.logo,
    D.shape.size,
  ]);
  if (memory.has(key)) return memory.get(key)!;
  const promise = (async () => {
    let db: IDBDatabase | undefined;
    try {
      db = await database();
      const packed: any = await new Promise((resolve, reject) => {
        const q = db!.transaction("meshes").objectStore("meshes").get(key);
        q.onsuccess = () => resolve(q.result);
        q.onerror = () => reject(q.error);
      });
      if (packed) {
        db.close();
        return packed.map((p: any) => {
          const g = new THREE.BufferGeometry();
          g.setAttribute("position", new THREE.BufferAttribute(p.position, 3));
          g.setAttribute("normal", new THREE.BufferAttribute(p.normal, 3));
          g.computeBoundingBox();
          return g;
        });
      }
    } catch {}
    const P = D.shape.logo,
      fontName =
        String(v.fontWeight) === "400"
          ? "Regular"
          : String(v.fontWeight) === "700"
            ? "Bold"
            : "SemiBold";
    const [shapes, font] = await Promise.all([
      loadLogoShapes(v.logoUrl, P),
      loadTypeface(`assets/fonts/Geist-${fontName}.ttf`),
    ]);
    const geos = [extrudeShapes(shapes, P, P.width)],
      tp = {
        ...P,
        depth: v.textDepth,
        bevelThickness: v.textBevel,
        bevelSize: v.textBevel * 0.8,
        bevelOffset: 0,
        cornerRadius: v.textCorner,
        curveSegments: 10,
        bevelSegments: 4,
      };
    for (const text of [v.headline1, v.headline2]) {
      const lines = String(text)
        .split("|")
        .map((s) => s.trim())
        .filter(Boolean);
      if (!lines.length) lines.push(" ");
      const size =
        v.textWidth / Math.max(0.001, ...lines.map((l) => lineWidth(font, l, v.letterSpacing)));
      geos.push(
        extrudeShapes(textShapes(font, lines, size, v.textLineHeight, v.letterSpacing), tp, size),
      );
    }
    for (let i = 0; i < 3; i++) {
      progress(`Preparing ${["logo", "first headline", "second headline"][i]} mesh…`);
      await new Promise((r) => setTimeout(r, 0));
      const old = geos[i];
      geos[i] = deformGeometry(
        old,
        { strength: v.deformStrength, scale: v.deformScale, seed: v.deformSeed },
        P.creaseAngle,
        i ? Number(v.textMeshDetail) : undefined,
        i ? undefined : Number(v.logoMeshDetail),
      );
      if (old !== geos[i]) old.dispose();
      geos[i].scale(D.shape.size, D.shape.size, D.shape.size);
    }
    if (db) {
      try {
        const tx = db.transaction("meshes", "readwrite");
        tx.objectStore("meshes").put(
          geos.map((g) => ({
            position: g.attributes.position.array,
            normal: g.attributes.normal.array,
          })),
          key,
        );
        tx.oncomplete = () => db!.close();
      } catch {
        db.close();
      }
    }
    return geos;
  })();
  memory.set(key, promise);
  promise.catch(() => memory.delete(key));
  return promise;
}
source/src/motion/objectFrame.ts
import * as THREE from "three/webgpu";
import { makePoseAt, SETTLE } from "../core/motion";
import type { Schedule, Pose } from "../frost";
export const shapeAt = (t: number, s: Schedule) => (t < s.form1 ? 0 : t < s.form2 ? 1 : 2);
/** Same parent YXZ motion and settled inverse-facing child as the full Frost block. */
export function objectFrameAt(
  t: number,
  s: Schedule,
  p: Pose,
  k = shapeAt(t, s),
  stationary = false,
) {
  const pose = makePoseAt(s, p),
    at = pose(t),
    q = (time: number) => {
      const v = pose(time);
      return new THREE.Quaternion().setFromEuler(
        new THREE.Euler((v.pitch * Math.PI) / 180, (v.yaw * Math.PI) / 180, 0, "YXZ"),
      );
    };
  const rotation = stationary ? new THREE.Quaternion() : q(t),
    facing =
      stationary || k === 0
        ? new THREE.Quaternion()
        : q((k === 1 ? s.form1 : s.form2) + SETTLE).invert();
  const position = new THREE.Vector3(0, 0.05, stationary ? 0 : at.z);
  return {
    rotation,
    facing,
    position,
    matrix: new THREE.Matrix4().compose(
      position,
      rotation.clone().multiply(facing),
      new THREE.Vector3(1, 1, 1),
    ),
  };
}
source/src/motion/track.ts
/** Versioned, absolute-time camera tracks. No renderer, DOM, clock or simulation state. */
import * as THREE from "three/webgpu";
export type V3 = [number, number, number];
export type Q4 = [number, number, number, number];
export type CameraKey = {
  id: string;
  time: number;
  position: V3;
  target: V3;
  quaternion: Q4;
  fov: number;
  zoom: number;
  roll: number;
  ease: "flow" | "linear" | "ease-in-out";
};
export type CameraTrack = {
  version: 1;
  name: string;
  orientation: "target" | "free";
  anchorSpace: "world" | "local";
  objectMotion: "existing" | "stationary";
  keys: CameraKey[];
};
export const emptyTrack = (): CameraTrack => ({
  version: 1,
  name: "Untitled shot",
  orientation: "target",
  anchorSpace: "local",
  objectMotion: "existing",
  keys: [],
});
export function parseTrack(input: unknown): CameraTrack {
  if (input == null || input === "") return emptyTrack();
  const v: any = typeof input === "string" ? JSON.parse(input) : input;
  if (v?.version !== 1 || !Array.isArray(v.keys) || v.keys.length > 64)
    throw Error("Camera track must be version 1 with at most 64 keys.");
  const pick = (x: any, choices: string[], name: string) => {
    if (!choices.includes(x)) throw Error("Invalid " + name);
    return x;
  };
  const number = (x: any, a: number, b: number, name: string) => {
    if (typeof x !== "number" || !Number.isFinite(x) || x < a || x > b)
      throw Error("Invalid " + name);
    return x;
  };
  const vector = (x: any, n: number, name: string) => {
    if (!Array.isArray(x) || x.length !== n) throw Error("Invalid " + name);
    return x.map((y) => number(y, -10000, 10000, name));
  };
  const track: CameraTrack = {
    version: 1,
    name: String(v.name || "Untitled shot").slice(0, 100),
    orientation: pick(v.orientation, ["target", "free"], "orientation") as any,
    anchorSpace: pick(v.anchorSpace, ["world", "local"], "anchor space") as any,
    objectMotion: pick(v.objectMotion, ["existing", "stationary"], "object motion") as any,
    keys: v.keys
      .map((k: any) => {
        const q = vector(k.quaternion, 4, "quaternion") as Q4;
        if (Math.abs(Math.hypot(...q) - 1) > 0.001)
          throw Error("Camera quaternion must be normalized.");
        return {
          id: String(k.id).slice(0, 100),
          time: number(k.time, 0, 120, "key time"),
          position: vector(k.position, 3, "position") as V3,
          target: vector(k.target, 3, "target") as V3,
          quaternion: q,
          fov: number(k.fov, 5, 120, "FOV"),
          zoom: number(k.zoom, 0.1, 10, "zoom"),
          roll: number(k.roll, -720, 720, "roll"),
          ease: pick(k.ease, ["flow", "linear", "ease-in-out"], "ease") as any,
        };
      })
      .sort((a: CameraKey, b: CameraKey) => a.time - b.time),
  };
  const ids = new Set();
  for (let i = 0; i < track.keys.length; i++) {
    const k = track.keys[i];
    if (!k.id || ids.has(k.id)) throw Error("Camera key IDs must be unique.");
    ids.add(k.id);
    if (i && k.time - track.keys[i - 1].time < 1 / 240)
      throw Error("Camera keys need distinct times (at least 1/240 second apart).");
  }
  return track;
}
export function baselineCamera() {
  const c = new THREE.PerspectiveCamera(29.5, 16 / 9, 0.5, 80);
  c.position.set(0, 1.2, 13.45);
  c.lookAt(0, 0.05, 0);
  c.updateMatrixWorld();
  return c;
}
export function cameraAt(track: CameraTrack, t: number, targetMatrix = new THREE.Matrix4()) {
  const keys = track.keys;
  if (!keys.length) {
    const c = baselineCamera();
    return {
      position: c.position,
      quaternion: c.quaternion,
      target: new THREE.Vector3(0, 0.05, 0),
      fov: 29.5,
      zoom: 1,
      roll: 0,
    };
  }
  t = Math.max(keys[0].time, Math.min(keys.at(-1)!.time, t));
  let i = 0;
  while (i < keys.length - 2 && keys[i + 1].time < t) i++;
  const a = keys[i],
    b = keys[Math.min(i + 1, keys.length - 1)],
    span = b.time - a.time;
  const raw = span > 0 ? (t - a.time) / span : 0,
    u = a.ease === "ease-in-out" ? raw * raw * (3 - 2 * raw) : raw;
  const interp = (get: (k: CameraKey) => number) => {
    if (!span) return get(a);
    if (a.ease !== "flow") return THREE.MathUtils.lerp(get(a), get(b), u);
    const prev = keys[Math.max(0, i - 1)],
      next = keys[Math.min(keys.length - 1, i + 2)];
    const m0 = (get(b) - get(prev)) / Math.max(b.time - prev.time, 1e-6),
      m1 = (get(next) - get(a)) / Math.max(next.time - a.time, 1e-6);
    return (
      (2 * u ** 3 - 3 * u * u + 1) * get(a) +
      (u ** 3 - 2 * u * u + u) * span * m0 +
      (-2 * u ** 3 + 3 * u * u) * get(b) +
      (u ** 3 - u * u) * span * m1
    );
  };
  const position = new THREE.Vector3(...([0, 1, 2].map((j) => interp((k) => k.position[j])) as V3));
  const target = new THREE.Vector3(...([0, 1, 2].map((j) => interp((k) => k.target[j])) as V3));
  if (track.anchorSpace === "local") target.applyMatrix4(targetMatrix);
  const quaternion =
    track.orientation === "target"
      ? new THREE.Quaternion().setFromRotationMatrix(
          new THREE.Matrix4().lookAt(position, target, new THREE.Vector3(0, 1, 0)),
        )
      : new THREE.Quaternion(...a.quaternion).slerp(new THREE.Quaternion(...b.quaternion), u);
  quaternion.multiply(
    new THREE.Quaternion().setFromAxisAngle(
      new THREE.Vector3(0, 0, 1),
      THREE.MathUtils.degToRad(interp((k) => k.roll)),
    ),
  );
  return {
    position,
    quaternion,
    target,
    roll: interp((k) => k.roll),
    fov: THREE.MathUtils.clamp(
      interp((k) => k.fov),
      5,
      120,
    ),
    zoom: THREE.MathUtils.clamp(
      interp((k) => k.zoom),
      0.1,
      10,
    ),
  };
}
export function applyCameraAt(
  camera: THREE.PerspectiveCamera,
  track: CameraTrack,
  t: number,
  targetMatrix: THREE.Matrix4,
  focus: THREE.Vector3,
) {
  const state = cameraAt(track, t, targetMatrix);
  camera.position.copy(state.position);
  camera.quaternion.copy(state.quaternion);
  camera.fov = state.fov;
  camera.zoom = state.zoom;
  camera.updateProjectionMatrix();
  camera.updateMatrixWorld(true);
  focus.copy(state.target);
  return state;
}
export class ShotHistory {
  private past: string[] = [];
  private future: string[] = [];
  constructor(public track: CameraTrack = emptyTrack()) {}
  commit(next: CameraTrack) {
    next = parseTrack(next);
    if (JSON.stringify(next) === JSON.stringify(this.track)) return;
    this.past.push(JSON.stringify(this.track));
    if (this.past.length > 100) this.past.shift();
    this.future = [];
    this.track = next;
  }
  undo() {
    if (this.past.length) {
      this.future.push(JSON.stringify(this.track));
      this.track = parseTrack(this.past.pop()!);
    }
  }
  redo() {
    if (this.future.length) {
      this.past.push(JSON.stringify(this.track));
      this.track = parseTrack(this.future.pop()!);
    }
  }
}
source/src/post/Post.ts
// Post-processing graph (TSL PostProcessing + pass()). Order:
//   scene (HDR float16, MRT output+velocity) -> TRAA -> haze (additive volumetric) -> DOF -> bloom
//   -> monochrome + grade (tonemap, black lift, contrast, exposure) -> vignette -> blue-noise grain
//   -> sRGB -> triangular dither. TRAA runs first so the grain is never temporally averaged away.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
const {
  rtt,
  Fn,
  pass,
  mrt,
  output,
  velocity,
  vec2,
  vec3,
  vec4,
  float,
  uniform,
  texture,
  screenUV,
  screenCoordinate,
  screenSize,
  mix,
  luminance,
  smoothstep,
  length,
  max,
  clamp,
  agxToneMapping,
  acesFilmicToneMapping,
  neutralToneMapping,
  linearToneMapping,
  floor,
  step,
} = tsl;
import { traa } from "three/addons/tsl/display/TRAANode.js";
import { taau } from "three/addons/tsl/display/TAAUNode.js";
import { fsr1 } from "three/addons/tsl/display/FSR1Node.js";
import { sharpen } from "three/addons/tsl/display/SharpenNode.js";
import { dof } from "three/addons/tsl/display/DepthOfFieldNode.js";
import { bloom } from "three/addons/tsl/display/BloomNode.js";
import { sRGBTransferOETF } from "three/src/nodes/display/ColorSpaceFunctions.js";
import type { PerformanceD, PostD } from "../dials/store";
import { hash31 } from "../tsl/noise";

type N = any;

export interface PostGraphOptions {
  scene: THREE.Scene;
  camera: THREE.PerspectiveCamera;
  renderer: THREE.WebGPURenderer;
  blueNoise: THREE.Texture;
  /** Optional additive haze node factory (gets the scene depth texture node and the viewZ node). */
  haze?: (depthNode: N, viewZNode: N) => N;
  /** Build the first graph with the live settings, avoiding a throwaway startup pipeline. */
  initial?: PostD;
  initialPerformance?: PerformanceD;
  initialHaze?: boolean;
}

export function createPost(o: PostGraphOptions) {
  const u = {
    exposure: uniform(1.0),
    contrast: uniform(1.08),
    blackLift: uniform(0.02),
    monochrome: uniform(1.0),
    vignette: uniform(0.2),
    vignetteSoft: uniform(0.7),
    vignetteRadius: uniform(0.85),
    grain: uniform(0.03),
    grainSize: uniform(1),
    grainSeed: uniform(new THREE.Vector2()),
    grainShadow: uniform(0.8),
    bgGrain: uniform(0),
    bgGrainSize: uniform(2),
    bgGrainSeed: uniform(new THREE.Vector2()),
    dither: uniform(1.0),
    focus: uniform(12.4),
    focusRange: uniform(3.0),
    aperture: uniform(0.9),
    bloomStrength: uniform(0.15),
    bloomRadius: uniform(0.15),
    bloomThreshold: uniform(1.2),
    sceneFade: uniform(1.0),
    hazeIntensity: uniform(0.22),
    upscaleSharpness: uniform(0.2),
  };

  const post = new ((THREE as any).RenderPipeline ?? THREE.PostProcessing)(o.renderer);
  post.outputColorTransform = false;

  let currentMode = { aa: "", tonemap: "", dof: true, haze: false, upscaler: "", sharpen: false };
  let scenePass: any = null;
  let temporal: any = null;
  let lastRenderFrame = -1;
  let hazeRTT: any = null;
  let hazeScale = 1;
  let settingsVersion = -1;
  let disposableNodes: any[] = [];

  function build(mode: {
    aa: string;
    tonemap: string;
    dof: boolean;
    haze: boolean;
    upscaler: string;
    sharpen: boolean;
  }) {
    currentMode = { ...mode };
    const previousNodes = disposableNodes;
    const nextNodes: any[] = [];
    const own = (node: any) => {
      if (node && typeof node.dispose === "function") nextNodes.push(node);
      return node;
    };
    scenePass = own(pass(o.scene, o.camera, { type: THREE.HalfFloatType, samples: 0 } as any));
    scenePass.setMRT(mrt({ output, velocity }));
    const beauty = scenePass.getTextureNode("output");
    const depthTex = scenePass.getTextureNode("depth");
    const velTex = scenePass.getTextureNode("velocity");
    const viewZ = scenePass.getViewZNode();

    // TAAU reconstructs and anti-aliases the low-resolution MRT directly. FSR1 expects an
    // anti-aliased input, so retain TRAA ahead of it. An RTT makes the bilinear path an explicit
    // output-resolution pass; native is the original same-resolution pipeline.
    let color: N;
    temporal = null;
    if (mode.upscaler === "taau") color = temporal = own(taau(beauty, depthTex, velTex, o.camera));
    else {
      color =
        mode.aa === "traa" ? (temporal = own(traa(beauty, depthTex, velTex, o.camera))) : beauty;
      if (mode.upscaler === "fsr1")
        color = own(fsr1(color, float(2).sub(u.upscaleSharpness.mul(2)), true));
      else if (mode.upscaler === "bilinear") color = own(rtt(color));
    }
    if (mode.sharpen && (mode.upscaler === "taau" || mode.upscaler === "bilinear")) {
      color = own(sharpen(color, float(2).sub(u.upscaleSharpness.mul(2)), true));
    }

    hazeRTT = null;
    if (o.haze && mode.haze) {
      // the haze is a smooth volumetric term: it can be rendered at reduced resolution and upsampled
      hazeRTT = own(rtt(o.haze(depthTex, viewZ)));
      hazeRTT.setResolutionScale(hazeScale);
      color = color.add(hazeRTT.mul(u.hazeIntensity));
    }

    if (mode.dof) color = own(dof(color, viewZ, u.focus, u.focusRange, u.aperture));

    const bl = own(bloom(color, u.bloomStrength, u.bloomRadius, u.bloomThreshold));
    (bl as any).smoothWidth.value = 0.35;
    color = color.add(bl);

    const graded = Fn(() => {
      const hdr = vec3(color).mul(u.sceneFade).toVar();
      const luma = luminance(hdr);
      hdr.assign(mix(hdr, vec3(luma), u.monochrome));
      const tm =
        mode.tonemap === "aces"
          ? acesFilmicToneMapping(hdr, u.exposure)
          : mode.tonemap === "neutral"
            ? neutralToneMapping(hdr, u.exposure)
            : mode.tonemap === "linear"
              ? linearToneMapping(hdr, u.exposure)
              : agxToneMapping(hdr, u.exposure);
      const t = clamp(vec3(tm), 0.0, 1.0).toVar();
      const mid = float(0.18);
      t.assign(clamp(mid.add(t.sub(mid).mul(u.contrast)), 0.0, 1.0));
      const sCurve = t.mul(t).mul(float(3).sub(t.mul(2)));
      t.assign(mix(t, sCurve, clamp(u.contrast.sub(1.0).mul(1.5), 0.0, 0.6)));
      t.assign(t.mul(u.blackLift.oneMinus()).add(u.blackLift));
      const aspect = screenSize.x.div(screenSize.y);
      const dv = vec2(screenUV.x.sub(0.5).mul(aspect), screenUV.y.sub(0.5));
      const r = length(dv).div(u.vignetteRadius);
      const vig = smoothstep(float(1.0).sub(u.vignetteSoft), 1.0001, r).mul(u.vignette);
      t.assign(t.mul(vig.oneMinus()));
      const srgb = vec3(sRGBTransferOETF(t)).toVar();
      const px = floor(screenCoordinate.xy.div(u.grainSize));
      const bnUV = px.div(64.0).add(u.grainSeed);
      const n = texture(o.blueNoise, bnUV).r.sub(0.5);
      const n2 = texture(o.blueNoise, bnUV.add(vec2(0.37, 0.61))).r.sub(0.5);
      const tri = n.add(n2);
      const lum = luminance(srgb);
      const shadowW = mix(float(1.0), smoothstep(1.0, 0.05, lum), u.grainShadow);
      const grainAmt = u.grain.mul(shadowW).mul(smoothstep(0.0, 0.08, lum).mul(0.85).add(0.15));
      srgb.addAssign(vec3(tri.mul(grainAmt)));
      // background-only grain for recorded video: coarse blocks of white noise, several grey levels deep, so
      // the codec keeps a texture where the plain gradient would band (object pixels are left alone)
      const isBg = step(0.9999995, depthTex.r);
      const pb = floor(screenCoordinate.xy.div(max(u.bgGrainSize, 1.0)));
      const b1 = hash31(vec3(pb, u.bgGrainSeed.x.mul(97.0)));
      const b2 = hash31(vec3(pb.add(vec2(17.3, 5.1)), u.bgGrainSeed.y.mul(53.0)));
      srgb.addAssign(vec3(b1.add(b2).sub(1.0).mul(u.bgGrain).mul(isBg)));
      const dn = texture(o.blueNoise, screenCoordinate.xy.div(64.0).add(u.grainSeed.yx)).r.sub(0.5);
      const dn2 = texture(
        o.blueNoise,
        screenCoordinate.xy.div(64.0).add(vec2(0.5, 0.25)).add(u.grainSeed),
      ).r.sub(0.5);
      srgb.addAssign(vec3(dn.add(dn2).mul(u.dither).div(255.0)));
      return vec4(max(srgb, 0.0), 1.0);
    })();

    post.outputNode = graded;
    post.needsUpdate = true;
    disposableNodes = nextNodes;
    for (const node of previousNodes) node.dispose();
  }

  build({
    aa: o.initial?.aaMode ?? "traa",
    tonemap: o.initial?.tonemap ?? "agx",
    dof: o.initial?.dof.enabled ?? true,
    haze: !!o.haze && (o.initialHaze ?? true),
    upscaler: o.initialPerformance?.nativePostEffects ? o.initialPerformance.upscaler : "native",
    sharpen:
      !!o.initialPerformance?.nativePostEffects &&
      !!o.initialPerformance.upscaleSharpness &&
      (o.initialPerformance.upscaler === "taau" || o.initialPerformance.upscaler === "bilinear"),
  });

  function update(
    p: PostD,
    perf: PerformanceD,
    focusDistance: number,
    sceneFade: number,
    hazeIntensity: number,
    t: number,
    version: number,
  ) {
    const haze = !!o.haze && hazeIntensity !== 0;
    const upscaler = perf.nativePostEffects ? perf.upscaler : "native";
    const useSharpen =
      perf.nativePostEffects &&
      perf.upscaleSharpness > 0 &&
      (upscaler === "taau" || upscaler === "bilinear");
    if (version !== settingsVersion || haze !== currentMode.haze) {
      settingsVersion = version;
      if (
        p.aaMode !== currentMode.aa ||
        p.tonemap !== currentMode.tonemap ||
        p.dof.enabled !== currentMode.dof ||
        haze !== currentMode.haze ||
        upscaler !== currentMode.upscaler ||
        useSharpen !== currentMode.sharpen
      ) {
        build({
          aa: p.aaMode,
          tonemap: p.tonemap,
          dof: p.dof.enabled,
          haze,
          upscaler,
          sharpen: useSharpen,
        });
      }
      u.exposure.value = p.exposure;
      u.contrast.value = p.contrast;
      u.blackLift.value = p.blackLift;
      u.monochrome.value = p.monochrome;
      u.vignette.value = p.vignette.strength;
      u.vignetteSoft.value = p.vignette.softness;
      u.vignetteRadius.value = p.vignette.radius;
      u.grain.value = p.grain.strength;
      u.grainSize.value = p.grain.size;
      u.grainShadow.value = p.grain.shadowWeight;
      u.bgGrain.value = p.grain.backgroundStrength;
      u.bgGrainSize.value = p.grain.backgroundSize;
      u.dither.value = p.dither ? 1 : 0;
      u.focusRange.value = p.dof.range;
      u.aperture.value = p.dof.aperture;
      u.bloomStrength.value = p.bloom.intensity;
      u.bloomRadius.value = p.bloom.radius;
      u.bloomThreshold.value = p.bloom.threshold;
      u.upscaleSharpness.value = perf.upscaleSharpness;
    }
    const bs = p.grain.backgroundSpeed > 0 ? Math.floor(t * 24 * p.grain.backgroundSpeed) : 0;
    u.bgGrainSeed.value.set(((bs * 0.7548776662) % 1) + 0.1, ((bs * 0.5698402909) % 1) + 0.1);
    const gs = p.grain.speed > 0 ? Math.floor(t * 24 * p.grain.speed) : 0;
    u.grainSeed.value.set((gs * 0.7548776662) % 1, (gs * 0.5698402909) % 1);
    u.focus.value = focusDistance + p.dof.focusBias;
    u.sceneFade.value = sceneFade;
    u.hazeIntensity.value = hazeIntensity;
  }

  function setHazeResolution(scale: number) {
    if (scale === hazeScale) return;
    hazeScale = scale;
    if (hazeRTT) hazeRTT.setResolutionScale(scale);
  }

  function setSceneResolution(scale: number) {
    if (scenePass) scenePass.setResolutionScale(scale);
  }

  function setFrame(index: number) {
    if (temporal) {
      // Pinned three 0.185.1 temporal AA uses a 31-entry jitter cycle and seeds history on resize.
      // Tie jitter to composition time, discard stale seek history.
      temporal._jitterIndex = ((index % 31) + 31) % 31;
      if (index !== lastRenderFrame + 1) temporal._historyRenderTarget.setSize(1, 1);
    }
    lastRenderFrame = index;
  }

  function dispose() {
    for (const node of disposableNodes) node.dispose();
    disposableNodes = [];
    post.dispose();
  }

  return {
    post,
    uniforms: u,
    update,
    setHazeResolution,
    setSceneResolution,
    setFrame,
    dispose,
    get scenePass() {
      return scenePass;
    },
  };
}
source/src/powder/Powder.ts
// Interior powder: ~1M particles filling the object's volume, simulated in WebGPU compute, rendered as
// directly instanced grains (dormant/ghost vertices collapse), shaded by a density grid
// (self-shadow + AO) and topped with a volumetric haze pass over the same grid.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
const {
  refract,
  log,
  output,
  attribute,
  positionView,
  reflect,
  vec2,
  Fn,
  vec3,
  vec4,
  float,
  uniform,
  instancedArray,
  storage,
  instanceIndex,
  If,
  Loop,
  int,
  uint,
  uvec3,
  ivec3,
  atomicAdd,
  atomicSub,
  atomicLoad,
  atomicStore,
  max,
  min,
  abs,
  dot,
  length,
  normalize,
  exp,
  mix,
  smoothstep,
  clamp,
  select,
  floor,
  fract,
  sin,
  cos,
  sqrt,
  texture3D,
  textureStore,
  positionLocal,
  normalLocal,
  transformNormalToView,
  varying,
  cameraPosition,
  screenUV,
  mat3,
  mat4,
  pow,
  Break,
  Continue,
  cross,
  step,
  negate,
  uv,
  texture,
  uniformArray,
  cameraViewMatrix,
} = tsl;
import type { ShapeSpec } from "../shape/sdf";
import { sampleInterior, sdfNormalNode } from "../shape/sdf";
import { buildGrainVariants } from "../shape/geometry";
import { SHARD_ATLAS_URL, SHARD_GRID, SHARD_CELLS } from "./shardAtlas";
import type { ErosionField } from "../erosion/ErosionField";
import { AssemblyField, assemblyReach } from "../erosion/AssemblyField";
import {
  assemblyFrontPhase,
  returnGroupPhase,
  returnReserve,
  RETURN_GROUP_DEFAULTS,
} from "./returnGroups";
import type { Interaction } from "../core/interaction";
import { curlNoise, vnoise3, hash11, hash31, hashSeed, rotateAxis, saturate } from "../tsl/noise";
import { D } from "../dials/store";
import { backdropColorAt, type BackdropUniforms } from "../scene/Backdrop";
import { iceSurface, iceSmudgeDirections } from "../ice/SharedSurface";
import { iceEnvironment } from "../ice/EnvironmentSampling";
import { iceDetail, iceFrost, iceInclusions } from "../ice/SurfaceDetail";
import { ICE_VARIANT } from "../ice/variant";
import { sim } from "../core/state";

type N = any;

/** Shard sprite atlas (generated from the Higgsfield shard sheet): loaded once, shared by every rebuild. */
let shardAtlas: THREE.Texture | null = null;
function getShardAtlas(): THREE.Texture {
  if (!shardAtlas) {
    const t = new THREE.TextureLoader().load(SHARD_ATLAS_URL);
    t.flipY = false;
    t.colorSpace = THREE.NoColorSpace;
    t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
    t.minFilter = THREE.LinearMipmapLinearFilter;
    t.magFilter = THREE.LinearFilter;
    t.generateMipmaps = true;
    t.anisotropy = 4;
    shardAtlas = t;
  }
  return shardAtlas;
}
// GHOST = eroded but gated out by `amount`: never rendered, still heals its voxel when the wave arrives
// WAITING = landed, but its break cell still has grains in flight: it sits at rest (invisible) until the
// cell refills, and only leaves again if the brush cuts its voxel
const DORMANT = 0,
  ACTIVE = 1,
  HEALING = 2,
  STRAY = 3,
  GHOST = 4,
  WAITING = 5;

export interface PowderOptions {
  renderer: THREE.WebGPURenderer;
  scene: THREE.Scene;
  shape: ShapeSpec;
  erosion: ErosionField;
  seed: number;
  rand: () => number;
  count: number;
  variants: number;
  strays: number;
  densityRes: number;
  clumpCount: number;
  objectGroup: THREE.Object3D;
  enabled?: boolean;
  /** 'fast' swaps the 6-sample finite-difference curl for a one-sample swirl. */
  turbulence?: "full" | "fast";
  /** shared atomic uint buffer: [density G^3 | 16 counters | heal grid]. */
  atomics: any;
  countersOffset: number;
  healOffset: number;
  cellOffset: number;
  flightOffset: number;
  fieldRes: number;
  /** backdrop gradient uniforms (the translucent grains show the backdrop through their centres). */
  backdrop: BackdropUniforms;
  fractureDetail?: THREE.Texture;
  iceUniforms: Record<string, any>;
  iceFeatures: Record<string, any>;
  environment: THREE.Texture;
}

export class Powder {
  readonly total: number;
  /** FROST: grains that belong to the shape (the rest are strays) and the initial rest buffer (thresholds, strays). */
  readonly count: number;
  readonly restInit: Float32Array;
  readonly meshes: THREE.Mesh[] = [];
  readonly group = new THREE.Group();
  readonly densityTex: THREE.Storage3DTexture;
  private assembly?: AssemblyField;
  assemblyEnabled = true;
  beforeIntegrate?: () => void;
  beforeAssembly?: () => void;
  private buffers: Record<string, any> = {};
  private nodes: Record<string, any> = {};
  private material!: THREE.MeshStandardNodeMaterial;
  private prevModel = new THREE.Matrix4();
  private readonly inversePrevModel = new THREE.Matrix4();
  private statsTimer = 0;
  private densityDirty = true;
  private fragmentShadowMap = true;
  private settingsVersion = -1;
  readonly u = {
    rigEnabled: uniform(0),
    rigStrength: uniform(1),
    surfaceFrost: uniform(1),
    surfaceDetail: uniform(1),
    dt: uniform(0),
    time: uniform(0),
    model: uniform(new THREE.Matrix4()),
    previousModel: uniform(new THREE.Matrix4()),
    modelDelta: uniform(new THREE.Matrix4()),
    normalMat: uniform(new THREE.Matrix3()),
    strokeDir: uniform(new THREE.Vector3(1, 0, 0)),
    strokeSpeed: uniform(0),
    heal: uniform(0),
    ejectSpeed: uniform(1.1),
    ejectSpread: uniform(0.5),
    ejectTurb: uniform(0.8),
    backwardRatio: uniform(0.25),
    clumpJitter: uniform(0.5),
    drag: uniform(1.4),
    gravity: uniform(0.12),
    turbulence: uniform(0.9),
    turbScale: uniform(1.8),
    turbDecay: uniform(0.6),
    cohesion: uniform(2.4),
    settleTime: uniform(3.2),
    settledDrift: uniform(0.012),
    tumble: uniform(2.5),
    returnDuration: uniform(2.4),
    returnCurve: uniform(0.35),
    inherit: uniform(1),
    inheritTime: uniform(0.22),
    straySpeed: uniform(0.06),
    fade: uniform(1),
    sizeMul: uniform(1),
    amount: uniform(0.3),
    dustScale: uniform(0.0055),
    grainScale: uniform(0.011),
    clumpScale: uniform(0.022),
    fragmentScale: uniform(0.05),
    sizeJitter: uniform(0.45),
    densityExtent: uniform(4.2),
    densityScale: uniform(0.03),
    shadowStrength: uniform(0.75),
    aoStrength: uniform(0.55),
    lightDir: uniform(new THREE.Vector3(0, 1, 0)),
    tone: uniform(new THREE.Color("#dcdcdc")),
    wrap: uniform(0.45),
    keyIntensity: uniform(3.2),
    keyColor: uniform(new THREE.Color("#ffffff")),
    colorByState: uniform(0),
    colorBySize: uniform(0),
    colorByAge: uniform(0),
    camPos: uniform(new THREE.Vector3()),
    invProj: uniform(new THREE.Matrix4()),
    camWorld: uniform(new THREE.Matrix4()),
    hazeSteps: uniform(28),
    strayBase: uniform(1),
    holeRadius: uniform(0.55),
    bound: uniform(1.5),
    fieldActive: uniform(1),
    hazeOn: uniform(1),
    waveTime: uniform(2),
    waveJitter: uniform(0.6),
    minEject: uniform(1),
    minPixel: uniform(1.5),
    pixelWorld: uniform(0.001),
    ghostDist: uniform(0),
    ghostFrac: uniform(0.45),
    waveReach: uniform(3),
    depositRadius: uniform(2),
    returnMode: uniform(1),
    springK: uniform(6),
    springDamp: uniform(0.9),
    springRamp: uniform(0.6),
    landRadius: uniform(0.03),
    returnAfter: uniform(3),
    maxSpeed: uniform(8),
    returnDrag: uniform(0.25),
    returnMaxSpeed: uniform(12),
    lostRadius: uniform(30),
    stragglers: uniform(0),
    spriteSee: uniform(0),
    frontDuration: uniform(3.2),
    frontX: uniform(-0.65),
    frontY: uniform(0.55),
    frontAngle: uniform(-35),
    frontSpread: uniform(0.65),
    frontNoise: uniform(0.35),
    speedVariation: uniform(0.65),
    pathBend: uniform(1.2),
    pathSwirl: uniform(1.1),
    landingVariation: uniform(0.8),
    groupNoise: uniform(2),
    assemblyEnd: uniform(0),
    groupStagger: uniform(0),
    groupScale: uniform(0.65),
    groupSeed: uniform(7),
    alignAmt: uniform(1),
    alignDist: uniform(0.6),
    alignCurve: uniform(1),
    landedFade: uniform(0.25),
    landShrink: uniform(0),
    translucency: uniform(0.6),
    throughTint: uniform(new THREE.Color("#aeb6bb")),
    fresnelPower: uniform(3),
    fragRough: uniform(0.32),
    fragClearcoat: uniform(0.6),
    fragSpecular: uniform(1),
    sparkle: uniform(0.6),
    sparkleFraction: uniform(0.35),
    sparkleSpread: uniform(0.5),
    lightDirView: uniform(new THREE.Vector3(0, 1, 0)),
    spriteSize: uniform(1.4),
    spriteTilt: uniform(0.5),
    spriteNormal: uniform(1),
    spriteFrost: uniform(1),
    spriteFrostBoost: uniform(0.35),
    spriteFrostRough: uniform(0.7),
    spriteEdge: uniform(0.6),
    spriteCut: uniform(0.2),
    repel: uniform(0),
    repelStrength: uniform(6),
    repelRange: uniform(0.35),
    repelRadial: uniform(12),
    repelRadialRange: uniform(1.6),
    modelInv: uniform(new THREE.Matrix4()),
  };

  constructor(readonly o: PowderOptions) {
    const { count, strays, rand, shape } = o;
    this.total = count + strays;
    const N = this.total;
    const P = D.powder;
    // ---- CPU init
    const samples = sampleInterior(shape, count, P.nearSurfaceFraction, P.nearSurfaceDepth, rand);
    const pos = new Float32Array(N * 4),
      vel = new Float32Array(N * 4),
      rest = new Float32Array(N * 4),
      meta = new Float32Array(N * 4),
      heal = new Float32Array(N * 4);
    const ratios = [
      P.grainSizes.tinyDustRatio,
      P.grainSizes.smallGrainRatio,
      P.grainSizes.mediumClumpRatio,
      0.01,
    ];
    const rs = ratios.reduce((a, b) => a + b, 0);
    const cum = [
      ratios[0] / rs,
      (ratios[0] + ratios[1]) / rs,
      (ratios[0] + ratios[1] + ratios[2]) / rs,
    ];
    // clump leaders via spatial cells
    const cellSize = Math.cbrt((8 * shape.bound ** 3) / Math.max(100, o.clumpCount));
    const halfCells = Math.ceil(shape.bound / cellSize) + 1;
    const gridSide = halfCells * 2 + 1;
    const leaders = new Map<number, number>();
    for (let i = 0; i < count; i++) {
      const i3 = i * 3,
        i4 = i * 4;
      const x = samples.positions[i3],
        y = samples.positions[i3 + 1],
        z = samples.positions[i3 + 2];
      const depth = samples.depths[i];
      const r = rand();
      let cls = r < cum[0] ? 0 : r < cum[1] ? 1 : r < cum[2] ? 2 : 3;
      if (cls === 3 && depth > 0.2) cls = 2;
      const cx = Math.floor(x / cellSize) + halfCells,
        cy = Math.floor(y / cellSize) + halfCells,
        cz = Math.floor(z / cellSize) + halfCells;
      const key = cx + cy * gridSide + cz * gridSide * gridSide;
      let leader = leaders.get(key);
      if (leader === undefined) {
        leader = i;
        leaders.set(key, i);
      }
      rest[i4] = x;
      rest[i4 + 1] = y;
      rest[i4 + 2] = z;
      rest[i4 + 3] = 0.35 + rand() * 0.4;
      pos[i4] = x;
      pos[i4 + 1] = y;
      pos[i4 + 2] = z;
      meta[i4] = DORMANT;
      meta[i4 + 1] = rand();
      meta[i4 + 2] = cls;
      meta[i4 + 3] = leader;
    }
    // strays: at rest in / around the hole
    for (let s = 0; s < strays; s++) {
      const i = count + s;
      const a = rand() * Math.PI * 2;
      const rr =
        shape.name === "torus"
          ? (shape.size - shape.size * shape.tubeRatio) * (0.15 + rand() * 0.75)
          : shape.bound * (1.05 + rand() * 0.35);
      const x = Math.cos(a) * rr,
        y = Math.sin(a) * rr,
        z = (rand() - 0.5) * shape.thickness * 1.2;
      const i4 = i * 4;
      rest[i4] = x;
      rest[i4 + 1] = y;
      rest[i4 + 2] = z;
      rest[i4 + 3] = 2;
      pos[i4] = x;
      pos[i4 + 1] = y;
      pos[i4 + 2] = z;
      pos[i4 + 3] = 0.01;
      vel[i4 + 3] = rand() * 10;
      meta[i4] = STRAY;
      meta[i4 + 1] = rand();
      meta[i4 + 2] = rand() < 0.8 ? 0 : 1;
      meta[i4 + 3] = i;
    }
    this.count = count;
    this.restInit = rest.slice();
    const B = this.buffers;
    B.pos = instancedArray(pos, "vec4");
    B.vel = instancedArray(vel, "vec4");
    B.rest = instancedArray(rest, "vec4");
    B.meta = instancedArray(meta, "vec4");
    B.heal = instancedArray(heal, "vec4");
    B.leaders = instancedArray(N, "vec4");
    const V = o.variants;
    const G = o.densityRes;
    B.atomics = o.atomics; // density voxels at 0, counters at countersOffset, heal grid at healOffset
    this.densityTex = new THREE.Storage3DTexture(G, G, G);
    this.densityTex.type = THREE.HalfFloatType;
    this.densityTex.format = THREE.RGBAFormat;
    this.densityTex.minFilter = this.densityTex.magFilter = THREE.LinearFilter;
    this.densityTex.wrapS =
      this.densityTex.wrapT =
      this.densityTex.wrapR =
        THREE.ClampToEdgeWrapping;
    this.u.bound.value = shape.bound;
    this.u.holeRadius.value =
      shape.name === "torus" ? shape.size - shape.size * shape.tubeRatio : shape.bound;
    if (o.enabled !== false) {
      this.buildCompute();
      this.assembly = new AssemblyField(o.erosion, count, B, this.u);
      this.buildMeshes();
      o.scene.add(this.group);
    }
    this.prevModel.copy(o.objectGroup.matrixWorld);
  }

  // --------------------------------------------------------------------------------------------
  private buildCompute() {
    const { count, shape, erosion, variants: V } = this.o;
    const N = this.total,
      u = this.u,
      B = this.buffers;
    const G = this.o.densityRes;
    const i = instanceIndex;
    void count;

    const CO = this.o.countersOffset,
      HO = this.o.healOffset,
      CLO = this.o.cellOffset,
      FLO = this.o.flightOffset,
      FR = this.o.fieldRes; // FR: erosion field resolution (R is the rest buffer element below)
    const cellTexNode = texture3D(erosion.cellTex);
    const counter = (v: N) => B.atomics.element(uint(CO).add(uint(v)));
    const densityAt = (idx: N) => B.atomics.element(idx);
    const healAt = (idx: N) => B.atomics.element(uint(HO).add(idx));
    const classScale = (cls: N) =>
      select(
        cls.lessThan(0.5),
        u.dustScale,
        select(
          cls.lessThan(1.5),
          u.grainScale,
          select(cls.lessThan(2.5), u.clumpScale, u.fragmentScale),
        ),
      );
    const classWeight = (cls: N) =>
      select(
        cls.lessThan(0.5),
        uint(1),
        select(cls.lessThan(1.5), uint(2), select(cls.lessThan(2.5), uint(5), uint(12))),
      );
    // Read a frozen step: live neighbor reads race other workgroups writing their state.
    this.nodes.snapshotLeaders = Fn(() => {
      B.leaders.element(i).assign(vec4(B.pos.element(i).xyz, B.meta.element(i).x));
    })().compute(N, [64]);
    const expoInOut = (x: N) => {
      const xc = clamp(x, 0.0, 1.0);
      return select(
        xc.lessThan(0.5),
        pow(2.0, xc.mul(20.0).sub(10.0)).div(2.0),
        float(2)
          .sub(pow(2.0, xc.mul(-20.0).add(10.0)))
          .div(2.0),
      );
    };

    this.nodes.resetCounters = Fn(() => {
      Loop(8, ({ i: k }: any) => {
        atomicStore(counter(k), uint(0));
      });
    })().compute(1, [1]);

    const curl =
      this.o.turbulence === "fast"
        ? (q: N) => cross(vnoise3(q), vnoise3(q.add(vec3(31.7, 11.3, 57.9)))).mul(1.6)
        : (q: N) => curlNoise(q);

    // normalize() of a zero vector is NaN on the GPU and a NaN grain is lost for good (the logo SDF is flat
    // outside its grid, so its gradient is zero there): every direction is normalised safely
    const safeNormalize = (v: N) => v.div(max(length(v), 1e-6));
    // one update kernel, instantiated twice: all particles, and (idle) the strays only
    const buildUpdate = (indexNode: N, dispatchCount: number) =>
      Fn(() => {
        const i = indexNode;
        const P = B.pos.element(i),
          Vv = B.vel.element(i),
          R = B.rest.element(i),
          M = B.meta.element(i),
          H = B.heal.element(i);
        // every value shared between the state branches is pinned to a variable here (TSL assigns
        // unpinned nodes at first use, which may be inside another branch -> uninitialised reads)
        const state = M.x.toVar();
        const seed = M.y.toVar(),
          cls = M.z.toVar(),
          leader = M.w.toVar();
        const pos = P.xyz.toVar();
        const size = P.w.toVar();
        const vel = Vv.xyz.toVar();
        const age = Vv.w.toVar();
        const rest = R.xyz.toVar();
        const threshold = R.w.toVar();
        const h1 = hashSeed(seed, 1).toVar(),
          h2 = hashSeed(seed, 2).toVar(),
          h3 = hashSeed(seed, 3).toVar();
        const baseSize = classScale(cls)
          .mul(float(1).sub(u.sizeJitter.mul(0.5)).add(u.sizeJitter.mul(h1)))
          .mul(u.sizeMul)
          .toVar();
        const restWorld = u.model.mul(vec4(rest, 1.0)).xyz.toVar();
        const restNormalWorld = safeNormalize(
          u.normalMat.mul(sdfNormalNode(shape, rest, 0.004)),
        ).toVar();
        const centreWorld = u.model.mul(vec4(0.0, 0.0, 0.0, 1.0)).xyz.toVar();

        const erSample = erosion.sample(rest).toVar();
        const erRest = erSample.r.toVar();
        const hitRecent = erSample.a.greaterThan(0.01); // the brush is cutting this grain's rest voxel right now
        const gate = hash11(seed.mul(211.7).add(5.0)).lessThan(u.amount);
        const healScheduled = H.w.greaterThan(0.5);
        // deposit into the heal grid around the rest voxel (the field refills there next frame); the radius
        // lets one returning grain stand in for the material that vanished on breakup (powder.amount < 1)
        // break cell of the rest voxel (nearest voxel, like the erosion step reads it)
        const cellIdOf = () => {
          const gc = ivec3(floor(erosion.uvw(rest).mul(float(FR))));
          const cellHash = cellTexNode.load(clamp(gc, ivec3(0), ivec3(FR - 1))).level(0).w;
          return uint(clamp(cellHash.mul(65535.0), 0.0, 65535.0));
        };
        // leaving: one more grain of this cell in flight
        const leave = () => {
          atomicAdd(B.atomics.element(uint(FLO).add(cellIdOf())), uint(1));
        };
        // arriving: one fewer; the last one home (allowing `cellStragglers`) triggers the cell restore
        const arrive = (trigger: N) => {
          const cellId = cellIdOf().toVar();
          const before = atomicSub(B.atomics.element(uint(FLO).add(cellId)), uint(1)).toVar();
          If(trigger.and(before.lessThanEqual(uint(1).add(uint(u.stragglers)))), () => {
            atomicAdd(B.atomics.element(uint(CLO).add(cellId)), uint(1));
          });
        };
        const deposit = () => {
          // the small voxel sphere around the rest (the cell itself refills once all its grains are home)
          const gp = rest
            .add(this.o.erosion.bound)
            .div(this.o.erosion.bound * 2)
            .mul(float(FR));
          const gi = ivec3(floor(gp)).toVar();
          const rf = u.depositRadius.toVar();
          const r = int(rf.ceil()).toVar();
          const rng = { start: r.negate(), end: r, condition: "<=" };
          Loop(rng, rng, rng, ({ i: dx, j: dy, k: dz }: any) => {
            const cc = gi.add(ivec3(dx, dy, dz));
            const inSphere = float(dx.mul(dx).add(dy.mul(dy)).add(dz.mul(dz))).lessThanEqual(
              rf.mul(rf).add(0.01),
            );
            If(
              inSphere
                .and(cc.x.greaterThanEqual(0))
                .and(cc.y.greaterThanEqual(0))
                .and(cc.z.greaterThanEqual(0))
                .and(cc.x.lessThan(FR))
                .and(cc.y.lessThan(FR))
                .and(cc.z.lessThan(FR)),
              () => {
                const hidx = uint(cc.x)
                  .add(uint(cc.y).mul(uint(FR)))
                  .add(uint(cc.z).mul(uint(FR * FR)));
                atomicAdd(healAt(hidx), uint(1));
              },
            );
          });
        };
        // return order: grains nearest the object first, the far plume last (ghost dust spreads evenly)
        const scheduleTime = () => {
          const distance = length(pos.sub(restWorld));
          const wave = clamp(distance.div(u.waveReach), 0.0, 1.0).mul(u.waveTime);
          const regional = erosion.u.reconstruct
            .greaterThan(0.5)
            .and(u.groupNoise.greaterThan(0.5));
          const jitter = select(regional, h3.mul(u.waveJitter).mul(0.04), h3.mul(u.waveJitter));
          const base = wave.add(jitter);
          const growing = u.groupNoise.greaterThan(1.5);
          const delay = float(0).toVar();
          If(
            erosion.u.reconstruct.greaterThan(0.5).and(u.groupStagger.greaterThan(0).or(growing)),
            () => {
              const reserve = select(
                u.returnMode.greaterThan(0.5),
                returnReserve(distance, length(vel), u),
                u.returnDuration.add(0.2),
              );
              const available = max(0, u.assemblyEnd.sub(u.time).sub(base).sub(reserve));
              // Scale the whole front to the available window, rather than clipping
              // all late regions to the same departure time.
              delay.assign(
                min(select(growing, u.frontDuration, u.groupStagger), available).mul(
                  select(
                    u.rigEnabled.greaterThan(0.5),
                    assemblyFrontPhase(rest, u),
                    max(0, threshold.sub(2)),
                  ),
                ),
              );
            },
          );
          return u.time.add(wave).add(jitter).add(delay);
        };
        const ghostScheduleTime = () => u.time.add(h1.mul(u.waveTime)).add(h3.mul(u.waveJitter));

        // leave the surface: velocity along the stroke (a share backwards), spread along the normal, turbulence
        const eject = () => {
          const pw = restWorld;
          const nw = restNormalWorld;
          const leaderSeed = B.meta.element(uint(leader)).y;
          const clumpRand = float(1)
            .sub(u.clumpJitter.mul(0.5))
            .add(u.clumpJitter.mul(hash11(leaderSeed.mul(77.7))));
          const backward = select(
            hash11(leaderSeed.mul(31.3).add(1.0)).lessThan(u.backwardRatio),
            float(-1),
            float(1),
          );
          const spd = max(u.strokeSpeed, u.minEject);
          const dir = u.strokeDir.mul(backward);
          const turb = curl(pw.mul(u.turbScale).add(u.time.mul(0.2)));
          const v0 = dir
            .mul(spd.mul(u.ejectSpeed).mul(clumpRand))
            .add(nw.mul(u.ejectSpread).mul(float(0.5).add(h2)).mul(spd.mul(0.4)))
            .add(turb.mul(u.ejectTurb).mul(spd.mul(0.25)).mul(h3.add(0.5)));
          const fragScale = select(cls.greaterThan(2.5), float(0.35), float(1.0));
          vel.assign(v0.mul(fragScale));
          pos.assign(pw.add(nw.mul(0.002)));
          age.assign(0.0);
          size.assign(baseSize);
          state.assign(ACTIVE);
          H.w.assign(0.0);
          leave();
        };
        If(state.lessThan(0.5), () => {
          // DORMANT: sample the erosion field at rest (skipped while the field is static). The state
          // chain below must stay intact, so the idle gate is nested rather than part of the condition.
          If(u.fieldActive.greaterThan(0.5), () => {
            If(erRest.greaterThan(threshold), () => {
              If(gate, () => {
                eject();
              }).Else(() => {
                state.assign(GHOST);
                H.w.assign(0.0);
              });
            });
          });
        })
          .ElseIf(state.lessThan(1.5), () => {
            // ACTIVE
            age.addAssign(u.dt);
            const settle = smoothstep(u.settleTime.mul(0.45), u.settleTime, age);
            const turb = curl(pos.mul(u.turbScale).add(u.time.mul(0.15)))
              .mul(u.turbulence)
              .mul(exp(age.mul(u.turbDecay).negate()));
            const leaderP = B.leaders.element(uint(leader));
            const leaderState = leaderP.w;
            const coh = select(
              leaderState.greaterThan(0.5).and(leaderState.lessThan(1.5)),
              leaderP.xyz.sub(pos).mul(u.cohesion).mul(float(1).sub(settle)),
              vec3(0),
            );
            // optional repulsion from the object: full strength inside the surface, smooth falloff outside
            const repelF = vec3(0).toVar();
            If(u.repel.greaterThan(0.5), () => {
              const pObj = u.modelInv.mul(vec4(pos, 1.0)).xyz.toVar();
              const dObj = shape.sdfNode(pObj).toVar();
              If(dObj.lessThan(u.repelRange), () => {
                const wgt = saturate(float(1).sub(dObj.div(u.repelRange)));
                const nW = safeNormalize(u.normalMat.mul(sdfNormalNode(shape, pObj, 0.004)));
                repelF.assign(nW.mul(u.repelStrength).mul(wgt.mul(wgt)));
              });
              // radial push from the object's centre: the only direction that always leads out of a pocket
              const toOut = pos.sub(centreWorld);
              const rn = length(toOut).div(u.bound.mul(u.repelRadialRange));
              If(rn.lessThan(1.0), () => {
                const w2 = float(1).sub(rn.mul(rn));
                const dirOut = safeNormalize(
                  toOut.add(vec3(h1.sub(0.5), h2.sub(0.5), h3.sub(0.5)).mul(0.05)),
                );
                repelF.addAssign(dirOut.mul(u.repelRadial).mul(w2.mul(w2)));
              });
            });
            vel.addAssign(
              turb
                .add(coh)
                .add(repelF)
                .add(vec3(0, u.gravity.negate(), 0))
                .mul(u.dt),
            );
            vel.mulAssign(exp(u.drag.mul(u.dt).negate()));
            vel.mulAssign(float(1).sub(settle.mul(min(1.0, u.dt.mul(3.0)))));
            pos.addAssign(vel.mul(u.dt));
            pos.addAssign(
              curl(pos.mul(0.9).add(u.time.mul(0.05)))
                .mul(u.settledDrift)
                .mul(settle)
                .mul(u.dt),
            );
            If(u.inherit.greaterThan(0.5).and(age.lessThan(u.inheritTime)), () => {
              const follow = float(1).sub(
                smoothstep(u.inheritTime.mul(0.75), max(u.inheritTime, 0.001), age),
              );
              pos.assign(mix(pos, u.modelDelta.mul(vec4(pos, 1.0)).xyz, follow));
              vel.assign(mix(vel, mat3(u.modelDelta).mul(vel), follow));
            });
            // hard speed cap: nothing ever flings a grain off-screen
            const spdNow = length(vel);
            If(spdNow.greaterThan(u.maxSpeed), () => {
              vel.mulAssign(u.maxSpeed.div(spdNow));
            });
            // healing: scheduled when the cursor is idle, or on the grain's own timer (returnAfter)
            const ownTimer = u.returnAfter.greaterThan(0.001).and(age.greaterThan(u.returnAfter));
            If(u.heal.greaterThan(0.5).or(ownTimer), () => {
              If(healScheduled.not(), () => {
                H.w.assign(scheduleTime());
              });
              If(H.w.greaterThan(0.5).and(u.time.greaterThanEqual(H.w)), () => {
                state.assign(HEALING);
                // FROST: the flight start lives in H.w; `age` keeps counting so the tumble phase never jumps
                H.xyz.assign(pos);
                H.w.assign(u.time);
              });
            }).Else(() => {
              H.w.assign(0.0);
            });
          })
          .ElseIf(state.lessThan(2.5), () => {
            // HEALING
            const isGhost = H.w.lessThan(-0.5);
            // FROST: flight start from H.w (ghosts keep the experiment's age-based start); age keeps counting
            const t0 = select(isGhost, age, H.w).toVar();
            If(isGhost.not(), () => {
              age.addAssign(u.dt);
            });
            const start = H.xyz;
            const target = restWorld;
            const nw = restNormalWorld;
            If(u.returnMode.greaterThan(0.5), () => {
              // spring return: a damped spring toward the rest that ramps in over `springRamp`, so the grain
              // decelerates, turns and comes back in one continuous motion; lands when it reaches the rest
              const tt = u.time.sub(t0);
              const ramp = smoothstep(0.0, max(u.springRamp, 0.001), tt);
              // Ease inherited frame motion into the spring instead of dropping it at return start.
              const follow = u.inherit
                .mul(float(1).sub(ramp))
                .mul(
                  float(1).sub(smoothstep(u.inheritTime.mul(0.75), max(u.inheritTime, 0.001), age)),
                );
              pos.assign(mix(pos, u.modelDelta.mul(vec4(pos, 1.0)).xyz, follow));
              vel.assign(mix(vel, mat3(u.modelDelta).mul(vel), follow));
              const d = target.sub(pos);
              const dist = length(d).toVar();
              const organic = select(
                erosion.u.reconstruct.greaterThan(0.5).and(u.groupNoise.greaterThan(1.5)),
                float(1),
                float(0),
              );
              const speedFactor = mix(
                float(1),
                mix(float(0.55), float(1.8), h1),
                u.speedVariation.mul(organic),
              );
              const k = u.springK.mul(u.rigStrength).mul(speedFactor.mul(speedFactor)).mul(ramp);
              const c = sqrt(u.springK.mul(u.rigStrength))
                .mul(speedFactor)
                .mul(2.0)
                .mul(u.springDamp)
                .mul(ramp);
              // A moving guide creates different approach angles. It collapses onto
              // the true home near contact and after a bounded time, so arcs settle.
              const flightDistance = length(target.sub(start));
              const homeFraction = clamp(dist.div(max(flightDistance, 0.05)), 0, 1);
              const guideLife = float(1).sub(smoothstep(0.5, 2.4, tt.mul(speedFactor)));
              const side = safeNormalize(cross(nw, vec3(h2, h3, h1).sub(0.5)).add(0.001));
              const other = cross(nw, side);
              const twist = tt.mul(u.pathSwirl).mul(h2.mul(2).sub(1)).add(h3.mul(6.283185));
              const arc = side
                .mul(cos(twist))
                .add(other.mul(sin(twist)))
                .add(nw.mul(h2.sub(0.5)));
              const guide = arc
                .mul(min(flightDistance, 3))
                .mul(u.pathBend)
                .mul(0.5)
                .mul(homeFraction.mul(0.3).add(sin(homeFraction.mul(Math.PI)).mul(0.7)))
                .mul(guideLife)
                .mul(organic);
              const previousTarget = u.previousModel.mul(vec4(rest, 1.0)).xyz;
              const targetVelocity = target.sub(previousTarget).div(max(u.dt, 1e-5));
              const acc = d
                .add(guide)
                .mul(k)
                .sub(vel.sub(targetVelocity).mul(c))
                .add(vec3(0, u.gravity.negate().mul(float(1).sub(ramp)), 0));
              vel.addAssign(acc.mul(u.dt));
              vel.mulAssign(exp(u.drag.mul(u.returnDrag).mul(u.dt).negate()));
              const spdBack = length(vel);
              const speedLimit = u.returnMaxSpeed.mul(speedFactor);
              If(spdBack.greaterThan(speedLimit), () => {
                vel.mulAssign(speedLimit.div(spdBack));
              });
              // never overshoot the rest in one step
              const step = vel.mul(u.dt);
              const stepLen = length(step);
              pos.addAssign(select(stepLen.greaterThan(dist), d, step));
              size.assign(
                baseSize
                  .mul(
                    select(
                      u.landShrink.greaterThan(0.001),
                      saturate(dist.div(max(u.landShrink, 0.001))),
                      float(1),
                    ),
                  )
                  .mul(select(isGhost, smoothstep(0.0, 0.25, tt), float(1))),
              );
              const landed = dist.lessThan(u.landRadius);
              If(landed, () => {
                If(isGhost.not(), () => {
                  arrive(hitRecent.not().and(erosion.u.reconstruct.lessThan(0.5)));
                  If(hitRecent.not().and(erosion.u.reconstruct.lessThan(0.5)), () => {
                    deposit();
                  });
                });
                state.assign(select(isGhost, float(DORMANT), float(WAITING)));
                pos.assign(target);
                vel.assign(vec3(0));
                H.w.assign(0.0);
                size.assign(
                  select(isGhost.or(erosion.u.reconstruct.greaterThan(0.5)), float(0), baseSize),
                );
              });
            }).Else(() => {
              // path return: eased curved flight, shrinking into the surface; deposits at 85% of the flight
              const uu = clamp(u.time.sub(t0).div(max(u.returnDuration, 0.05)), 0.0, 1.0).toVar();
              const uuPrev = clamp(
                u.time.sub(u.dt).sub(t0).div(max(u.returnDuration, 0.05)),
                0.0,
                1.0,
              );
              const e = expoInOut(uu);
              const mid = mix(start, target, 0.5)
                .add(
                  nw
                    .mul(u.returnCurve)
                    .mul(length(target.sub(start)))
                    .mul(0.5),
                )
                .add(vec3(h2, h3, h1).sub(0.5).mul(u.returnCurve).mul(0.4));
              const a = mix(start, mid, e),
                b = mix(mid, target, e);
              pos.assign(mix(a, b, e));
              const fadeIn = select(
                isGhost,
                smoothstep(float(1).sub(u.ghostFrac), float(1).sub(u.ghostFrac).add(0.08), uu),
                float(1),
              );
              size.assign(baseSize.mul(float(1).sub(smoothstep(0.82, 1.0, uu))).mul(fadeIn));
              If(uuPrev.lessThan(0.85).and(uu.greaterThanEqual(0.85)).and(isGhost.not()), () => {
                arrive(hitRecent.not().and(erosion.u.reconstruct.lessThan(0.5)));
                If(hitRecent.not().and(erosion.u.reconstruct.lessThan(0.5)), () => {
                  deposit();
                });
              });
              If(uu.greaterThanEqual(1.0), () => {
                state.assign(select(isGhost, float(DORMANT), float(WAITING)));
                pos.assign(target);
                H.w.assign(0.0);
              });
            });
            // The same spatial contact controls both return modes. Once home, disarm this
            // shard until the next breakup even if a neighbouring owner is still approaching.
            If(erosion.u.reconstruct.greaterThan(0.5).and(isGhost.not()), () => {
              const variation = select(u.groupNoise.greaterThan(1.5), u.landingVariation, float(0));
              const reach = float(assemblyReach(erosion.bound)).mul(
                mix(float(1), mix(float(0.35), float(1.65), h2), variation),
              );
              const contactFade = smoothstep(
                u.landRadius,
                max(u.landRadius.add(0.001), reach),
                length(target.sub(pos)),
              );
              size.assign(
                baseSize.mul(
                  pow(contactFade, mix(float(1), mix(float(0.45), float(2.2), h3), variation)),
                ),
              );
              If(state.greaterThan(4.5), () => {
                R.w.assign(2.0);
              });
            });
            // a stroke elsewhere never touches a grain in flight: only the brush passing over its own rest voxel
            // matters, and that is handled on landing (no deposit, and it leaves the surface again next frame)
          })
          .ElseIf(state.lessThan(3.5), () => {
            // STRAY: slow orbit in / around the hole, object space -> world
            const ang = u.time.mul(u.straySpeed).mul(float(0.5).add(h1)).add(seed.mul(6.2831));
            const r = length(rest.xy);
            const zz = rest.z.add(sin(u.time.mul(0.3).add(seed.mul(12.0))).mul(0.05));
            const local = vec3(cos(ang).mul(r), sin(ang).mul(r), zz);
            pos.assign(u.model.mul(vec4(local, 1.0)).xyz);
            size.assign(classScale(cls).mul(0.9).mul(u.sizeMul));
            age.assign(age.add(u.dt));
          })
          .ElseIf(state.lessThan(4.5), () => {
            // GHOST: vanished on breakup. It never rebuilds anything (only grains that flew out do); on the
            // wave it just becomes dormant again, or optionally shows up as extra dust flying in.
            If(u.heal.greaterThan(0.5), () => {
              If(healScheduled.not(), () => {
                H.w.assign(
                  ghostScheduleTime().add(u.returnDuration.mul(float(1).sub(u.ghostFrac))),
                );
              });
              If(H.w.greaterThan(0.5).and(u.time.greaterThanEqual(H.w)), () => {
                If(u.ghostDist.greaterThan(0.001), () => {
                  const nw = restNormalWorld;
                  const tangent = safeNormalize(
                    cross(nw, vec3(h1.sub(0.5), h2.sub(0.5), h3.sub(0.5)).add(0.001)),
                  );
                  const startPos = restWorld
                    .add(nw.mul(u.ghostDist).mul(float(0.4).add(h1.mul(0.6))))
                    .add(tangent.mul(u.ghostDist).mul(h2.sub(0.5)).mul(0.8));
                  H.xyz.assign(startPos);
                  H.w.assign(-1.0); // -1: ghost-origin return (fades in, no deposit)
                  pos.assign(startPos);
                  age.assign(u.time.sub(u.returnDuration.mul(float(1).sub(u.ghostFrac))));
                  size.assign(0.0);
                  state.assign(HEALING);
                }).Else(() => {
                  state.assign(DORMANT);
                  H.w.assign(0.0);
                });
              });
            }).Else(() => {
              H.w.assign(0.0);
            });
          })
          .Else(() => {
            // WAITING: home, lying on the surface until its voxel is solid again (the cell waits for its other
            // grains), then it fades over `landedFade`. Cut again by the brush -> leaves again.
            pos.assign(restWorld);
            If(erosion.u.reconstruct.greaterThan(0.5), () => {
              state.assign(DORMANT);
              H.w.assign(0);
              size.assign(0);
            })
              .ElseIf(erRest.lessThan(threshold), () => {
                If(H.w.lessThan(0.5), () => {
                  H.w.assign(u.time);
                });
                const f = saturate(u.time.sub(H.w).div(max(u.landedFade, 0.01)));
                size.assign(baseSize.mul(float(1).sub(f)));
                If(f.greaterThanEqual(1.0), () => {
                  state.assign(DORMANT);
                  H.w.assign(0.0);
                  size.assign(0.0);
                });
              })
              .Else(() => {
                size.assign(baseSize);
                H.w.assign(0.0);
                If(hitRecent.and(u.fieldActive.greaterThan(0.5)), () => {
                  eject();
                });
              });
          });

        // lost-grain guard: a grain that is not a finite number any more, or has drifted far off screen, is put
        // back near its rest so the state machine can land it (nothing is ever lost for good)
        If(state.greaterThan(0.5).and(state.lessThan(2.5)), () => {
          const finite = abs(pos.x)
            .lessThan(1e30)
            .and(abs(pos.y).lessThan(1e30))
            .and(abs(pos.z).lessThan(1e30))
            .and(abs(vel.x).lessThan(1e30))
            .and(abs(vel.y).lessThan(1e30))
            .and(abs(vel.z).lessThan(1e30));
          const far = length(pos.sub(centreWorld)).greaterThan(u.lostRadius);
          If(finite.not().or(far), () => {
            pos.assign(restWorld.add(restNormalWorld.mul(0.05)));
            vel.assign(vec3(0.0));
          });
        });
        P.xyz.assign(pos);
        P.w.assign(size);
        Vv.xyz.assign(vel);
        Vv.w.assign(age);
        M.x.assign(state);

        // Active-particle statistics and density accumulation (ghosts are never drawn)
        If(state.greaterThan(0.5).and(state.lessThan(3.5)).or(state.greaterThan(4.5)), () => {
          const variant = uint(floor(seed.mul(1000.0))).mod(uint(V));
          atomicAdd(counter(variant), uint(1));
          const gp = pos.add(u.densityExtent).div(u.densityExtent.mul(2.0)).mul(float(G));
          const gi = ivec3(floor(gp));
          If(
            gi.x
              .greaterThanEqual(0)
              .and(gi.y.greaterThanEqual(0))
              .and(gi.z.greaterThanEqual(0))
              .and(gi.x.lessThan(G))
              .and(gi.y.lessThan(G))
              .and(gi.z.lessThan(G)),
            () => {
              const idx = uint(gi.x)
                .add(uint(gi.y).mul(uint(G)))
                .add(uint(gi.z).mul(uint(G * G)));
              atomicAdd(densityAt(idx), classWeight(cls));
            },
          );
        });
      })().compute(dispatchCount, [64]);
    this.nodes.update = buildUpdate(instanceIndex, N);
    this.nodes.updateStrays =
      this.o.strays > 0 ? buildUpdate(instanceIndex.add(uint(count)), this.o.strays) : null;

    // density resolve: density, light transmittance, ao -> 3D texture; then clear the atomic grid
    const gCoord = () => uvec3(i.mod(uint(G)), i.div(uint(G)).mod(uint(G)), i.div(uint(G * G)));
    const loadD = (c: N) => {
      const cc = clamp(ivec3(c), ivec3(0), ivec3(G - 1));
      const idx = uint(cc.x)
        .add(uint(cc.y).mul(uint(G)))
        .add(uint(cc.z).mul(uint(G * G)));
      return float(atomicLoad(densityAt(idx))).mul(u.densityScale);
    };
    this.nodes.resolve = Fn(() => {
      const c = gCoord();
      const d = loadD(ivec3(c)).toVar();
      const dens = float(1).sub(exp(d.negate()));
      // transmittance toward the key light (6 steps in grid space)
      const Lg = u.lightDir.mul(float(G)).div(u.densityExtent.mul(2.0)).toVar();
      const acc = float(0).toVar();
      const q = vec3(c).add(0.5).toVar();
      Loop(6, () => {
        q.addAssign(Lg.mul(0.9));
        acc.addAssign(loadD(ivec3(floor(q))));
      });
      const T = exp(acc.mul(0.9).negate());
      // local density for ao
      const nb = loadD(ivec3(c).add(ivec3(1, 0, 0)))
        .add(loadD(ivec3(c).add(ivec3(-1, 0, 0))))
        .add(loadD(ivec3(c).add(ivec3(0, 1, 0))))
        .add(loadD(ivec3(c).add(ivec3(0, -1, 0))))
        .add(loadD(ivec3(c).add(ivec3(0, 0, 1))))
        .add(loadD(ivec3(c).add(ivec3(0, 0, -1))));
      const ao = float(1).sub(exp(nb.div(6.0).add(d).mul(1.2).negate()));
      textureStore(this.densityTex, c, vec4(dens, T, ao, 1.0)).toWriteOnly();
    })().compute(G * G * G, [64]);
    this.nodes.clearDensity = Fn(() => {
      atomicStore(densityAt(i), uint(0));
    })().compute(G * G * G, [64]);

    // FROST: after a retarget, in-flight grains (active/healing) count toward their new cell so the last one home
    // restores it; landed (waiting) grains go dormant at their new home; dormant/ghost grains sit in the eroded
    // shape, threshold raised to keep the filled field from ejecting them (restoreThresholds() re-arms).
    this.nodes.retargetCount = Fn(() => {
      const M = B.meta.element(i),
        R = B.rest.element(i),
        P = B.pos.element(i);
      const state = M.x.toVar();
      If(state.greaterThan(4.5), () => {
        M.x.assign(DORMANT);
        P.w.assign(0.0);
        B.heal.element(i).assign(vec4(0.0));
        state.assign(DORMANT);
      });
      If(state.greaterThan(0.5).and(state.lessThan(2.5)), () => {
        // While returning, the ejection threshold is unused. Pack the stable group
        // phase above 2 here, avoiding a ninth storage binding on default WebGPU.
        // restoreThresholds() reinstates the original value before the next breakup.
        If(
          erosion.u.reconstruct
            .greaterThan(0.5)
            .and(u.groupStagger.greaterThan(0).or(u.groupNoise.greaterThan(1.5))),
          () => {
            R.w.assign(
              float(2).add(
                select(
                  u.groupNoise.greaterThan(1.5),
                  assemblyFrontPhase(R.xyz, u),
                  returnGroupPhase(R.xyz, u.groupScale, u.groupSeed, u.groupNoise),
                ),
              ),
            );
          },
        );
        const gc = ivec3(floor(erosion.uvw(R.xyz).mul(float(FR))));
        const cellHash = cellTexNode.load(clamp(gc, ivec3(0), ivec3(FR - 1))).level(0).w;
        atomicAdd(
          B.atomics.element(uint(FLO).add(uint(clamp(cellHash.mul(65535.0), 0.0, 65535.0)))),
          uint(1),
        );
      }).ElseIf(state.lessThan(0.5).or(state.greaterThan(3.5).and(state.lessThan(4.5))), () => {
        R.w.assign(2.0);
      });
    })().compute(N, [64]);

    this.nodes.reset = Fn(() => {
      const M = B.meta.element(i),
        Vv = B.vel.element(i),
        P = B.pos.element(i),
        R = B.rest.element(i);
      If(M.x.lessThan(2.5).or(M.x.greaterThan(3.5)), () => {
        M.x.assign(DORMANT);
        Vv.assign(vec4(0.0));
        P.assign(vec4(R.xyz, 0.0));
        B.heal.element(i).assign(vec4(0.0));
      });
    })().compute(N, [64]);
    void count;
    void shape;
  }

  // --------------------------------------------------------------------------------------------
  private buildMeshes() {
    const { variants: V, rand } = this.o;
    const N = this.total,
      u = this.u,
      B = this.buffers;
    const SP = D.powder.sprites;
    const sprite = !!SP.enabled;
    // sprite grains: one camera-facing quad per grain, textured from the shard atlas (silhouette, normal,
    // frost structure, thickness); mesh grains: the low-poly variants
    const geos = sprite
      ? Array.from({ length: V }, () => new THREE.PlaneGeometry(2, 2))
      : buildGrainVariants(V, rand, D.powder.facetedGrains);
    const atlasTex = sprite ? getShardAtlas() : null;
    const cellsArr = sprite
      ? uniformArray(SHARD_CELLS.map((c) => new THREE.Vector2(c[0], c[1])))
      : null;
    const posRO = storage(B.pos.value, "vec4", N).toReadOnly();
    const velRO = storage(B.vel.value, "vec4", N).toReadOnly();
    const metaRO = storage(B.meta.value, "vec4", N).toReadOnly();
    const restRO = storage(B.rest.value, "vec4", N).toReadOnly();
    const healRO = storage(B.heal.value, "vec4", N).toReadOnly();
    const dens = texture3D(this.densityTex);

    for (let v = 0; v < V; v++) {
      const geo = new THREE.InstancedBufferGeometry().copy(
        geos[v] as any,
      ) as THREE.InstancedBufferGeometry;
      // Draw a stable partition of the particle buffer. Dormant/ghost grains
      // collapse in the vertex shader, so visibility never depends on the
      // compute-generated compact list or indirect argument buffer.
      geo.instanceCount = Math.max(0, Math.ceil((N - v) / V));
      const mat = new THREE.MeshPhysicalNodeMaterial();
      mat.metalness = 0;
      mat.side = THREE.DoubleSide;
      mat.forceSinglePass = true;
      const pIdx = instanceIndex.mul(uint(V)).add(uint(v));
      const P = posRO.element(pIdx),
        Vv = velRO.element(pIdx),
        M = metaRO.element(pIdx);
      const seed = M.y,
        cls = M.z,
        state = M.x,
        age = Vv.w;
      const axis = normalize(
        vec3(hashSeed(seed, 11), hashSeed(seed, 12), hashSeed(seed, 13)).sub(0.5),
      );
      const spin = hashSeed(seed, 14)
        .mul(6.2831)
        .add(age.mul(u.tumble).mul(float(0.4).add(hashSeed(seed, 15))));
      const camDist = length(P.xyz.sub(cameraPosition));
      const drawable = state.greaterThan(0.5).and(state.lessThan(3.5)).or(state.greaterThan(4.5));
      const size = select(
        drawable.and(P.w.greaterThan(1e-5)),
        max(P.w, camDist.mul(u.pixelWorld).mul(u.minPixel)),
        float(0),
      ).mul(u.fade);
      // density shading (varying: sample at the grain centre)
      const guv = P.xyz.add(u.densityExtent).div(u.densityExtent.mul(2.0));
      const dSample = varying(dens.sample(guv).level(0));
      const shadow = mix(float(1), dSample.g, u.shadowStrength);
      const ao = float(1).sub(dSample.b.mul(u.aoStrength));
      let vSeed: N;
      const I = this.o.iceUniforms,
        features = this.o.iceFeatures;
      const surfaceFeatures = {
        ...features,
        // The solid's diagnostic bump switch preserves the existing shard finish.
        surfaceBumps: float(1),
        frost: features.frost.mul(features.shardFrost),
        crystals: features.crystals.mul(features.shardNormals),
        grain: features.grain.mul(features.shardNormals),
        micro: features.micro.mul(features.shardNormals),
        ripples: features.ripples.mul(features.shardNormals),
      };
      const homeGradient = sdfNormalNode(this.o.shape, restRO.element(pIdx).xyz, 0.02);
      const homeInfo = varying(
        vec4(select(length(homeGradient).greaterThan(0.001), homeGradient, vec3(0, 0, 1)), size),
      );
      const homeNormal = homeInfo.xyz;
      const patch = varying(restRO.element(pIdx).xyz.add(attribute("position", "vec3").mul(size)));
      const objectSeed = vec3(this.o.seed * 0.731, this.o.seed * 0.137, this.o.seed * 0.529);
      const surface = iceSurface(
        patch,
        homeNormal,
        objectSeed,
        iceSmudgeDirections(this.o.seed),
        this.o.erosion,
        I,
        surfaceFeatures,
      );
      const inheritedFrost = surface.frost;
      const inheritedDetail =
        ICE_VARIANT === "photographic"
          ? iceInclusions(
              this.o.fractureDetail,
              patch,
              homeNormal,
              homeNormal.negate(),
              homeInfo.w,
              I.inclusionScale,
              I.inclusionAmount,
            ).mul(u.surfaceDetail)
          : float(0);
      const viewDir = normalize(positionView.negate());
      let nView: N,
        wrapL: N,
        frost: N = float(0),
        thick: N = float(1);
      if (sprite) {
        // --- quad in view space: in-plane spin (tumble) + a random tilt off the camera plane, mirrored at random
        const cellIdx = int(floor(hashSeed(seed, 31).mul(SHARD_GRID * SHARD_GRID)));
        const ext = cellsArr.element(cellIdx);
        const flipX = select(hashSeed(seed, 32).lessThan(0.5), float(-1), float(1));
        const flipY = select(hashSeed(seed, 33).lessThan(0.5), float(-1), float(1));
        const tiltAng = hashSeed(seed, 34).mul(u.spriteTilt);
        const phi = hashSeed(seed, 35).mul(6.2831);
        const tiltAxis = vec3(cos(phi), sin(phi), 0.0);
        const rot = (q: N) => rotateAxis(rotateAxis(q, vec3(0, 0, 1), spin), tiltAxis, tiltAng);
        // camera-facing basis (view space); the mirror is folded into the signs
        const bR0 = rot(vec3(flipX, 0.0, 0.0)),
          bF0 = rot(vec3(0.0, 0.0, 1.0));
        // on the way home the shard turns to lie on the surface it lands on: the basis blends toward the
        // tangent frame of the SDF normal at its rest point (a landed, WAITING shard is fully aligned)
        const R4 = restRO.element(pIdx);
        const restW = u.model.mul(vec4(R4.xyz, 1.0)).xyz;
        const nRestW0 = u.normalMat.mul(sdfNormalNode(this.o.shape, R4.xyz, 0.004));
        const nRestW = nRestW0.div(max(length(nRestW0), 1e-6));
        const nRestV0 = normalize(
          mat3(cameraViewMatrix)
            .mul(nRestW)
            .add(vec3(0.0, 0.0, 1e-4)),
        );
        const nRestV = select(nRestV0.z.lessThan(0.0), nRestV0.negate(), nRestV0); // the side the camera sees
        const healingNow = state.greaterThan(1.5).and(state.lessThan(2.5));
        const waitingNow = state.greaterThan(4.5);
        const distHome = length(restW.sub(P.xyz));
        // FROST: a returning shard turns to lie on the surface over its whole flight home (fraction of the start
        // distance covered, shaped by alignCurve), instead of snapping within alignDist of the surface
        const H4 = healRO.element(pIdx);
        const startDist = max(length(restW.sub(H4.xyz)), 0.05);
        const homeFrac = saturate(float(1).sub(distHome.div(startDist)));
        const align = select(
          waitingNow,
          float(1),
          select(healingNow, pow(homeFrac, u.alignCurve), float(0)),
        ).mul(u.alignAmt);
        const bRt = normalize(bR0.sub(nRestV.mul(dot(bR0, nRestV))).add(vec3(1e-4, 0.0, 0.0)));
        const bF1 = normalize(mix(bF0, nRestV, align));
        const bR1a = normalize(mix(bR0, bRt, align));
        const bR1 = normalize(bR1a.sub(bF1.mul(dot(bR1a, bF1))));
        const bU1 = cross(bF1, bR1).mul(flipX.mul(flipY));
        const offV = bR1
          .mul(positionLocal.x.mul(ext.x).mul(flipX))
          .add(bU1.mul(positionLocal.y.mul(ext.y).mul(flipY)))
          .mul(size)
          .mul(u.spriteSize);
        mat.positionNode = P.xyz.add(mat3(u.camWorld).mul(offV));
        const bR = varying(bR1);
        const bU = varying(bU1);
        const bF = varying(bF1);
        // One location instead of four scalar varyings. Leave room for the
        // physical lighting and motion-vector outputs on 16-location GPUs.
        const atlasInfo = varying(vec4(float(cellIdx), flipX, flipY, seed));
        const vCell = atlasInfo.x,
          vFx = atlasInfo.y,
          vFy = atlasInfo.z;
        vSeed = atlasInfo.w;
        // --- atlas fetch
        const uvL = uv();
        const uS = mix(uvL.x, float(1).sub(uvL.x), step(vFx, 0.0));
        const vS = mix(uvL.y, float(1).sub(uvL.y), step(vFy, 0.0));
        const ci = int(vCell.add(0.5));
        const cx = float(ci.mod(int(SHARD_GRID))),
          cy = float(ci.div(int(SHARD_GRID)));
        const extF = cellsArr.element(ci);
        const auv = vec2(
          cx.add(0.5).add(uS.sub(0.5).mul(extF.x)),
          cy.add(0.5).add(vS.sub(0.5).mul(extF.y)),
        ).div(SHARD_GRID);
        const smp = texture(atlasTex, auv);
        const nxy = smp.rg.mul(2.0).sub(1.0).mul(u.spriteNormal);
        const nz = sqrt(saturate(float(1).sub(dot(nxy, nxy))));
        nView = normalize(bR.mul(nxy.x).add(bU.mul(nxy.y)).add(bF.mul(nz)));
        // Rotate the inherited surface perturbation into the shard's facet frame.
        const detailNormal = surface.nSurface.sub(homeNormal);
        nView = normalize(
          nView.add(bR.mul(detailNormal.x)).add(bU.mul(detailNormal.y)).add(bF.mul(detailNormal.z)),
        );
        mat.normalNode = nView;
        mat.maskNode = smp.a.greaterThan(u.spriteCut);
        frost = clamp(
          inheritedFrost
            .mul(I.frostDiffuse)
            .add(surface.smudge.mul(I.smudgeWhite))
            .add(inheritedDetail.mul(0.65)),
          0,
          0.65,
        );
        thick = saturate(smp.a.sub(0.35).div(0.65));
        if (SP.seeThrough > 0) {
          // real see-through: the clear parts of a shard blend over whatever is behind them (ice included)
          mat.transparent = true;
          mat.depthWrite = false;
          const fresS = pow(saturate(float(1).sub(saturate(dot(nView, viewDir)))), u.fresnelPower);
          // Thin-sheet optical coverage, not opaque white atlas paint.
          const opticalCover = mix(float(0.42), float(0.85), frost)
            .add(fresS.mul(0.45))
            .add(float(1).sub(thick).mul(0.18));
          mat.opacityNode = mix(
            float(1),
            saturate(opticalCover),
            u.spriteSee.mul(features.transmission).mul(features.shardTransmission),
          );
        }
        wrapL = saturate(dot(nView, u.lightDirView).add(u.wrap).div(float(1).add(u.wrap)));
      } else {
        vSeed = varying(seed);
        const local = rotateAxis(positionLocal.mul(size), axis, spin);
        mat.positionNode = P.xyz.add(local);
        const nLocal = rotateAxis(normalLocal, axis, spin);
        mat.normalNode = transformNormalToView(nLocal);
        nView = normalize(transformNormalToView(nLocal));
        wrapL = saturate(dot(normalize(nLocal), u.lightDir).add(u.wrap).div(float(1).add(u.wrap)));
      }
      if (!sprite)
        frost = clamp(
          inheritedFrost
            .mul(I.frostDiffuse)
            .add(surface.smudge.mul(I.smudgeWhite))
            .add(inheritedDetail.mul(0.65)),
          0,
          0.65,
        );
      const f0 = pow(I.ior.sub(1).div(I.ior.add(1)), 2);
      const fres = f0.add(
        float(1)
          .sub(f0)
          .mul(pow(saturate(float(1).sub(saturate(dot(nView, viewDir)))), 5)),
      );
      const surfaceDensity =
        ICE_VARIANT === "photographic"
          ? iceDetail(this.o.fractureDetail, patch, homeNormal, I.inclusionScale).mul(
              surfaceFeatures.frost,
            )
          : float(0);
      const roughness = clamp(surface.roughness.add(surfaceDensity.mul(0.22)), 0.015, 0.8);
      mat.roughnessNode = roughness;
      mat.iorNode = I.ior;
      mat.clearcoatNode = I.clearcoat.mul(features.clearcoat).mul(features.shardReflections);
      mat.clearcoatRoughnessNode = I.clearcoatRough;
      mat.specularIntensityNode = I.specularIntensity
        .mul(features.reflections)
        .mul(features.shardReflections);
      const reflectionOn = features.reflections.mul(features.shardReflections);
      const transmissionOn = features.transmission.mul(features.shardTransmission);
      const viewToWorld = (q: N) => normalize(mat3(u.camWorld).mul(q));
      const env = (q: N) => iceEnvironment(this.o.environment, I.envStrength, q, roughness);
      const ray = viewDir.negate();
      const transmittedRay = refract(ray, nView, float(1).div(max(I.ior, 1.001)));
      // Detached pieces are thin volumes: use atlas thickness and world size,
      // rather than tracing the intact object's SDF after they have left it.
      const opticalDepth = max(homeInfo.w.mul(thick).mul(I.thicknessScale), 0.001);
      const attenuation = exp(
        log(max(I.attColor, vec3(0.001)))
          .mul(opticalDepth.div(max(I.attDist, 0.01)))
          .mul(features.absorption),
      );
      const refractedUV = clamp(
        screenUV.add(transmittedRay.xy.sub(ray.xy).mul(opticalDepth).mul(0.08)),
        0,
        1,
      );
      const plate = backdropColorAt(this.o.backdrop, refractedUV)
        .add(env(viewToWorld(transmittedRay)).mul(I.backlight))
        .mul(attenuation);
      const reflected = env(viewToWorld(reflect(ray, nView)))
        .mul(fres)
        .mul(reflectionOn)
        .mul(I.specularIntensity);
      const cover = max(frost, float(1).sub(transmissionOn));
      const scatter = I.keyColor
        .mul(
          surface.surfCrack
            .mul(I.crackBright)
            .mul(0.3)
            .add(inheritedDetail.mul(I.backlight).mul(0.8)),
        )
        .mul(features.scatter);
      mat.colorNode = I.baseColor;
      mat.emissiveNode = I.keyColor
        .mul(cover.mul(I.interiorScatter).mul(0.3))
        .mul(features.scatter);
      mat.outputNode = Fn(() => {
        const clear = reflected.add(plate.mul(float(1).sub(fres.mul(reflectionOn))));
        const radiance = clear.mul(float(1).sub(cover)).add(output.rgb.mul(cover)).add(scatter);
        // Preserve physical radiance through the thin-sheet blend.
        const a = max(output.a, 0.001);
        const background = backdropColorAt(this.o.backdrop, screenUV);
        return vec4(max(radiance.sub(background.mul(float(1).sub(a))), vec3(0)).div(a), output.a);
      })();
      const mesh = new THREE.Mesh(geo, mat);
      mesh.frustumCulled = false;
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      this.meshes.push(mesh);
      this.group.add(mesh);
      if (v === 0) this.material = mat;
    }
  }

  /** Additive volumetric haze over the density grid (evaluated in the post graph). */
  hazeNode(depthNode: N, viewZ: N, _camera: THREE.PerspectiveCamera): N {
    const u = this.u;
    const dens = texture3D(this.densityTex);
    return Fn(() => {
      // screenUV is y-down on the WebGPU post quad: NDC y must be negated or the march mirrors vertically
      const ndc0 = screenUV.mul(2.0).sub(1.0);
      const ndc = vec2(ndc0.x, ndc0.y.negate());
      const clip = u.invProj.mul(vec4(ndc.x, ndc.y, 1.0, 1.0));
      const vView = clip.xyz.div(clip.w);
      const dirView = normalize(vView);
      const dir = normalize(mat3(u.camWorld).mul(dirView));
      const ro = u.camPos;
      // ray-box
      const ext = u.densityExtent;
      const inv = vec3(1).div(dir);
      const t0 = ext.negate().sub(ro).mul(inv),
        t1 = ext.sub(ro).mul(inv);
      const tmin = max(max(min(t0.x, t1.x), min(t0.y, t1.y)), min(t0.z, t1.z));
      const tmax = min(min(max(t0.x, t1.x), max(t0.y, t1.y)), max(t0.z, t1.z));
      // scene depth along the ray
      const zRatio = float(1).div(max(dirView.z.negate(), 1e-4));
      const tScene = viewZ.negate().mul(zRatio);
      const tEnd = min(tmax, tScene);
      const acc = float(0).toVar();
      If(tEnd.greaterThan(max(tmin, 0.0)).and(u.hazeOn.greaterThan(0.5)), () => {
        const tStart = max(tmin, 0.0);
        const steps = max(u.hazeSteps, 4.0);
        const dtt = tEnd.sub(tStart).div(steps);
        const jitter = hash31(vec3(screenUV.mul(1234.0), u.time)).mul(dtt);
        const tt = tStart.add(jitter).toVar();
        Loop({ start: int(0), end: int(steps), type: "int", condition: "<" }, () => {
          const pw = ro.add(dir.mul(tt));
          const guv = pw.add(ext).div(ext.mul(2.0));
          const s = dens.sample(guv).level(0);
          acc.addAssign(s.r.mul(s.g).mul(dtt));
          tt.addAssign(dtt);
        });
      });
      const col = u.tone.mul(u.keyColor).mul(u.keyIntensity).mul(acc).mul(0.35);
      return vec4(col, 0.0);
    })();
    void depthNode;
  }

  // --------------------------------------------------------------------------------------------
  step(
    renderer: THREE.WebGPURenderer,
    t: number,
    dt: number,
    inter: Interaction,
    key: THREE.DirectionalLight,
    idle: { fieldActive: boolean; powderActive: boolean },
  ) {
    const P = D.powder,
      H = D.healing,
      u = this.u;
    u.fieldActive.value = idle.fieldActive ? 1 : 0;
    u.hazeOn.value = idle.powderActive ? 1 : 0;
    u.dt.value = Math.min(dt, 1 / 30);
    u.time.value = t;
    const model = this.o.objectGroup.matrixWorld;
    u.model.value.copy(model);
    u.previousModel.value.copy(this.prevModel);
    u.normalMat.value.getNormalMatrix(model);
    u.modelDelta.value.copy(model).multiply(this.inversePrevModel.copy(this.prevModel).invert());
    this.prevModel.copy(model);
    u.strokeDir.value.copy(inter.strokeDir);
    u.strokeSpeed.value = inter.strokeSpeed;
    u.heal.value = sim.healing ? 1 : 0;
    u.fade.value = sim.fade;
    u.lightDir.value.copy(key.position).sub(key.target.position).normalize();
    u.modelInv.value.copy(model).invert();
    if (D.version !== this.settingsVersion) {
      this.settingsVersion = D.version;
      u.ejectSpeed.value = P.ejectSpeed;
      u.ejectSpread.value = P.ejectSpread;
      u.ejectTurb.value = P.ejectTurbulence;
      u.backwardRatio.value = P.backwardRatio;
      u.clumpJitter.value = P.clumpSpeedJitter;
      u.drag.value = P.drag;
      u.gravity.value = P.gravity;
      u.turbulence.value = P.turbulence;
      u.turbScale.value = P.turbulenceScale;
      u.turbDecay.value = P.turbulenceDecay;
      u.cohesion.value = P.clumpCohesion;
      u.settleTime.value = P.settleTime;
      u.settledDrift.value = P.settledDrift;
      u.tumble.value = P.tumble;
      u.returnDuration.value = H.returnDuration;
      u.returnCurve.value = H.returnCurve;
      u.inherit.value = P.inheritRotation ? 1 : 0;
      u.inheritTime.value = P.inheritTime / 1000;
      u.straySpeed.value = P.straySpeed;
      u.dustScale.value = P.grainSizes.tinyDustSize;
      u.grainScale.value = P.grainSizes.smallGrainSize;
      u.clumpScale.value = P.grainSizes.mediumClumpSize;
      u.fragmentScale.value = P.grainSizes.largeFragmentSize;
      u.sizeJitter.value = P.grainSizes.sizeJitter;
      u.sizeMul.value = P.grainSizeMultiplier;
      u.amount.value = P.amount;
      u.minEject.value = P.minEjectSpeed;
      u.minPixel.value = P.minPixelSize;
      u.waveTime.value = H.waveTime;
      u.waveJitter.value = H.waveJitter;
      u.ghostDist.value = H.ghostReturnDistance;
      u.ghostFrac.value = H.ghostFlightFraction;
      u.waveReach.value = H.waveReach;
      u.depositRadius.value = H.depositRadius;
      u.returnMode.value = H.returnMode === "spring" ? 1 : 0;
      u.springK.value = H.returnSpring;
      u.springDamp.value = H.returnDamping;
      u.springRamp.value = H.returnRamp;
      u.landRadius.value = H.landRadius;
      u.returnAfter.value = H.returnAfter;
      u.maxSpeed.value = P.maxSpeed;
      u.returnDrag.value = H.returnDrag;
      u.returnMaxSpeed.value = H.returnMaxSpeed;
      u.lostRadius.value = P.lostRadius;
      u.stragglers.value = H.cellStragglers;
      u.spriteSee.value = P.sprites.seeThrough;
      u.alignAmt.value = H.alignToSurface;
      u.alignDist.value = H.alignDistance;
      u.alignCurve.value = (H as any).alignCurve ?? 1;
      u.landedFade.value = H.landedFade;
      u.landShrink.value = H.landShrink;
      u.densityExtent.value = P.densityExtent;
      u.shadowStrength.value = P.densityShadowStrength;
      u.aoStrength.value = P.densityAOStrength;
      u.densityScale.value = 0.02 * (this.o.densityRes / 64) ** 3 * (1_000_000 / this.total) ** 0.5;
      u.tone.value.set(P.baseTone);
      u.wrap.value = P.wrap;
      u.keyIntensity.value = D.lighting.key.intensity;
      u.keyColor.value.set(D.lighting.key.color);
      const F = P.fragments;
      u.translucency.value = F.translucency;
      u.throughTint.value.set(F.throughTint);
      u.fresnelPower.value = F.fresnelPower;
      u.fragRough.value = F.roughness;
      u.fragClearcoat.value = F.clearcoat;
      u.fragSpecular.value = F.specular;
      u.sparkle.value = F.sparkle;
      u.sparkleFraction.value = F.sparkleFraction;
      u.sparkleSpread.value = F.sparkleSpread;
      const SPu = P.sprites;
      u.spriteSize.value = SPu.sizeScale;
      u.spriteTilt.value = THREE.MathUtils.degToRad(SPu.tilt);
      u.spriteNormal.value = SPu.normalStrength;
      u.spriteFrost.value = SPu.frostFromAtlas;
      u.spriteFrostBoost.value = SPu.frostBoost;
      u.spriteFrostRough.value = SPu.frostRoughness;
      u.spriteEdge.value = SPu.edgeLight;
      u.spriteCut.value = SPu.alphaCut;
      // Material-panel switches change shading only; saved strengths and simulation survive.
      const features = D.ice.features;
      u.surfaceFrost.value = Number(features.shardFrost && features.frost);
      u.surfaceDetail.value = Number(features.shardFrost && features.scatter);
      if (!features.shardNormals) u.spriteNormal.value = 0;
      if (!features.shardFrost) {
        u.spriteFrost.value = 0;
        u.spriteFrostBoost.value = 0;
      }
      if (!features.shardTransmission) {
        u.translucency.value = 0;
        u.spriteSee.value = 0;
      }
      if (!features.shardReflections) {
        u.fragSpecular.value = 0;
        u.fragClearcoat.value = 0;
      }
      if (!features.shardSparkle) u.sparkle.value = 0;
      if (!features.shardEdges) u.spriteEdge.value = 0;
      u.repel.value = P.repelFromObject ? 1 : 0;
      u.repelStrength.value = P.repelStrength;
      u.repelRange.value = P.repelRange;
      u.repelRadial.value = P.repelRadial;
      u.repelRadialRange.value = P.repelRadialRange;
      u.colorByState.value = D.debug.colorByState ? 1 : 0;
      u.colorBySize.value = D.debug.colorBySize ? 1 : 0;
      u.colorByAge.value = D.debug.colorByAge ? 1 : 0;
      u.hazeSteps.value = P.hazeSteps;
      if (P.fragmentShadowMap !== this.fragmentShadowMap) {
        this.fragmentShadowMap = P.fragmentShadowMap;
        for (const m of this.meshes) m.castShadow = P.fragmentShadowMap;
      }
    }

    this.beforeIntegrate?.();
    if (dt <= 0) return;
    renderer.compute(this.nodes.resetCounters);
    if (idle.powderActive || idle.fieldActive || !this.nodes.updateStrays) {
      renderer.compute(this.nodes.snapshotLeaders);
      renderer.compute(this.nodes.update);
      renderer.compute(this.nodes.resolve);
      renderer.compute(this.nodes.clearDensity);
      this.densityDirty = true;
    } else {
      // Nothing can move except the strays: update their positions and density counters only.
      renderer.compute(this.nodes.updateStrays);
      if (this.densityDirty) {
        renderer.compute(this.nodes.resolve);
        renderer.compute(this.nodes.clearDensity);
        this.densityDirty = false;
      }
    }

    this.beforeAssembly?.();
    if (this.assemblyEnabled && this.o.erosion.u.reconstruct.value > 0.5 && idle.fieldActive)
      this.assembly?.update(renderer);

    // stats (async readback, throttled)
    this.statsTimer += dt;
    if (this.statsTimer > 0.5 && (D.debug.stats || D.performance.idleSkip)) {
      this.statsTimer = 0;
      const CO = this.o.countersOffset;
      renderer
        .getArrayBufferAsync(this.buffers.atomics.value, null, CO * 4, 16 * 4)
        .then((buf: ArrayBuffer) => {
          const c = new Uint32Array(buf);
          let active = 0;
          for (let v = 0; v < this.o.variants; v++) active += c[v];
          sim.counts.total = this.total;
          sim.counts.active = Math.max(0, active - this.o.strays);
          sim.counts.dormant = Math.max(0, this.total - active);
        })
        .catch(() => {});
    }
  }

  /** Update camera uniforms used by the haze raymarch (call before post render). */
  updateCamera(camera: THREE.PerspectiveCamera, viewportHeightPx: number) {
    this.u.pixelWorld.value =
      (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2))) / Math.max(1, viewportHeightPx);
    this.u.lightDirView.value
      .copy(this.u.lightDir.value)
      .transformDirection(camera.matrixWorldInverse);
    this.u.camPos.value.copy(camera.position);
    this.u.invProj.value.copy(camera.projectionMatrixInverse);
    this.u.camWorld.value.copy(camera.matrixWorld);
  }

  reset(renderer: THREE.WebGPURenderer) {
    renderer.compute(this.nodes.reset);
  }

  /** FROST: the object frame jumped on purpose (facing offset at a retarget): no inherited motion for that frame. */
  syncModel() {
    this.prevModel.copy(this.o.objectGroup.matrixWorld);
  }

  /**
   * FROST: give every grain a new home (object space, N x vec4, thresholds and strays preserved by the caller).
   * Call after the erosion field was re-baked for the new shape: the in-flight counters are rebuilt from it.
   */
  retarget(renderer: THREE.WebGPURenderer, rest: Float32Array) {
    const attr = this.buffers.rest.value;
    (attr.array as Float32Array).set(rest);
    this.upload(renderer, attr);
    this.u.groupNoise.value = Number(D.healing.returnNoiseAmount ?? 2);
    this.u.frontDuration.value = Number(
      D.healing.assemblyFrontDuration ?? RETURN_GROUP_DEFAULTS.assemblyFrontDuration,
    );
    this.u.frontX.value = Number(
      D.healing.assemblyOriginX ?? RETURN_GROUP_DEFAULTS.assemblyOriginX,
    );
    this.u.frontY.value = Number(
      D.healing.assemblyOriginY ?? RETURN_GROUP_DEFAULTS.assemblyOriginY,
    );
    this.u.frontAngle.value = Number(
      D.healing.assemblyAngle ?? RETURN_GROUP_DEFAULTS.assemblyAngle,
    );
    this.u.frontSpread.value = Number(
      D.healing.assemblySpread ?? RETURN_GROUP_DEFAULTS.assemblySpread,
    );
    this.u.frontNoise.value = Number(
      D.healing.assemblyFrontNoise ?? RETURN_GROUP_DEFAULTS.assemblyFrontNoise,
    );
    this.u.speedVariation.value = Number(
      D.healing.assemblySpeedVariation ?? RETURN_GROUP_DEFAULTS.assemblySpeedVariation,
    );
    this.u.pathBend.value = Number(D.healing.assemblyBend ?? RETURN_GROUP_DEFAULTS.assemblyBend);
    this.u.pathSwirl.value = Number(D.healing.assemblySwirl ?? RETURN_GROUP_DEFAULTS.assemblySwirl);
    this.u.landingVariation.value = Number(
      D.healing.assemblyLandingVariation ?? RETURN_GROUP_DEFAULTS.assemblyLandingVariation,
    );

    this.u.groupStagger.value =
      D.healing.returnGroupStagger ?? RETURN_GROUP_DEFAULTS.returnGroupStagger;
    this.u.groupScale.value = D.healing.returnGroupScale ?? RETURN_GROUP_DEFAULTS.returnGroupScale;
    this.u.groupSeed.value = D.healing.returnGroupSeed ?? RETURN_GROUP_DEFAULTS.returnGroupSeed;
    renderer.compute(this.nodes.retargetCount);
    if (this.o.erosion.u.reconstruct.value > 0.5) this.assembly?.prepare(renderer);
  }

  prepareAssembly(renderer: THREE.WebGPURenderer) {
    this.assembly?.prepare(renderer);
  }

  /** FROST: every grain may leave again (the thresholds retarget() raised come back from the initial buffer). */
  restoreThresholds(renderer: THREE.WebGPURenderer) {
    const attr = this.buffers.rest.value,
      a = attr.array as Float32Array;
    for (let i = 3; i < a.length; i += 4) a[i] = this.restInit[i];
    this.upload(renderer, attr);
  }

  private upload(renderer: THREE.WebGPURenderer, attr: any) {
    attr.needsUpdate = true;
    // the storage binding is updated lazily by three; the compute below must already see the new homes
    const backend: any = (renderer as any).backend;
    const gpu = backend?.get?.(attr);
    if (gpu?.buffer && backend.device)
      backend.device.queue.writeBuffer(
        gpu.buffer,
        0,
        attr.array.buffer,
        attr.array.byteOffset,
        attr.array.byteLength,
      );
  }

  dispose() {
    this.o.scene.remove(this.group);
    for (const m of this.meshes) {
      m.geometry.dispose();
      (m.material as THREE.Material).dispose();
    }
    this.densityTex.dispose();
    this.assembly?.dispose();
    for (const k of Object.keys(this.buffers)) {
      const b = this.buffers[k];
      if (b?.value?.dispose) b.value.dispose?.();
    }
  }
}
void abs;
void sqrt;
void fract;
void Break;
void Continue;
void cross;
void step;
void negate;
void mat4;
void cameraPosition;
void int;
source/src/powder/returnGroups.ts
/** Spatial return groups, evaluated once per retarget, never in the flight integrator. */
import { tsl } from "../tsl/t";
import { hash31, voronoiCell, gnoise } from "../tsl/noise";
import preset from "../../presets/approved-material.json";
const { max, float, length, sqrt, log, select, smoothstep, vec3, dot, abs, cos, sin, clamp, mix } =
  tsl;
export const RETURN_GROUP_DEFAULTS = {
  returnGroupStagger: preset.returnGroupStagger,
  returnGroupScale: preset.returnGroupScale,
  returnGroupSeed: preset.returnGroupSeed,
  returnNoiseAmount: 2,
  assemblyFrontDuration: 3.2,
  assemblyOriginX: -0.65,
  assemblyOriginY: 0.55,
  assemblyAngle: -35,
  assemblySpread: 0.65,
  assemblyFrontNoise: 0.35,
  assemblySpeedVariation: 0.65,
  assemblyBend: 1.2,
  assemblySwirl: 1.1,
  assemblyLandingVariation: 0.8,
};
export function returnGroupPhase(p: any, scale: any, seed: any, noiseAmount: any = float(0)) {
  const cell = voronoiCell(p.div(max(scale, 0.1)), seed);
  const q = p.div(max(scale, 0.1)).add(vec3(seed.mul(0.173), seed.mul(0.317), seed.mul(0.571)));
  const noise = smoothstep(0.18, 0.82, gnoise(q).mul(0.5).add(0.5));
  return select(noiseAmount.greaterThan(0.5), noise, hash31(cell.xyz.add(11.7)));
}
/** Conservative travel + settling allowance, used only to limit *additional* delay.
 * Flight parameters and existing wave timing are never changed by the group control. */
export function returnReserve(distance: any, speed: any, u: any) {
  const frequency = sqrt(max(u.springK, 0.5));
  const envelope = distance.add(speed.div(frequency)).add(0.03);
  return max(
    float(2.8),
    u.springRamp
      .add(distance.div(max(u.returnMaxSpeed, 1)))
      .add(
        log(max(envelope.div(max(u.landRadius, 0.001)), 1)).div(
          max(frequency.mul(u.springDamp), 0.5),
        ),
      )
      .add(0.35),
  );
}

/** A travelling seam spreads sideways, like the breakup brush. Object-space field,
 * evaluated only at retarget; packed into the existing threshold slot. */
export function assemblyFrontPhase(p: any, u: any) {
  const q = p.div(max(u.bound, 0.001)).sub(vec3(u.frontX, u.frontY, 0));
  const angle = u.frontAngle.mul(Math.PI / 180);
  const along = dot(q.xy, vec3(cos(angle), sin(angle), 0).xy);
  const across = abs(dot(q.xy, vec3(sin(angle).negate(), cos(angle), 0).xy));
  // Behind the start grows too, at a slower rate; no disconnected glyph is excluded.
  const travel = max(along, 0).add(max(along.negate(), 0).mul(1.4)).div(1.5);
  const spread = across.div(1.5);
  const noise = gnoise(p.div(max(u.groupScale, 0.1)).add(u.groupSeed.mul(0.173)))
    .mul(0.5)
    .add(0.5);
  return clamp(mix(travel, spread, u.frontSpread).add(noise.mul(u.frontNoise).mul(0.35)), 0, 1);
}
source/src/powder/shardAtlas.ts
// generated by scratchpad/atlas/atlas.mjs from the Higgsfield shard sheet (16 shards, 4x4)
// R,G = tangent-space normal xy, B = frost/structure, A = silhouette * (0.35 + 0.65 * thickness)
import { assetUrl } from "../assets";
export const SHARD_ATLAS_URL = assetUrl("shards-atlas.png"); // FROST: was '/shards-atlas.png'
export const SHARD_GRID = 4;
/** used extent of each cell (fraction of the cell), row-major from the top-left */
export const SHARD_CELLS: [number, number][] = [
  [0.8035, 0.94],
  [0.94, 0.8055],
  [0.7254, 0.94],
  [0.94, 0.8764],
  [0.94, 0.9233],
  [0.7478, 0.94],
  [0.94, 0.8079],
  [0.6781, 0.94],
  [0.94, 0.8965],
  [0.94, 0.8612],
  [0.7025, 0.94],
  [0.8094, 0.94],
  [0.94, 0.903],
  [0.8238, 0.94],
  [0.94, 0.7249],
  [0.8374, 0.94],
];
source/src/rewrite.ts
const profiles: Record<string, string> = {
  original: "gpu",
  lookup: "gpu,materials",
  mesh: "gpu,mesh,materials",
  studio: "gpu,fast,environment,materials,aa",
  matcap: "gpu,matcap,materials,aa",
};
const params =
  typeof location === "undefined" ? new URLSearchParams() : new URLSearchParams(location.search);
function resolveFlags(profile: string) {
  const flags = new Set(
    (params.has("rewrite") ? params.get("rewrite")! : profiles[profile] || profiles.studio).split(
      ",",
    ),
  );
  return {
    gpu: flags.has("gpu"),
    environment: flags.has("environment"),
    materials: flags.has("materials"),
    matcap: flags.has("matcap"),
    fast: flags.has("fast"),
    aa: flags.has("aa"),
    mesh: flags.has("mesh") || flags.has("matcap") || flags.has("fast"),
  };
}
export const RW = resolveFlags(typeof window === "undefined" ? "original" : "studio");
export const rwMetrics: any = { flags: RW, stages: [], sdf: [], start: performance.now() };
if (typeof window !== "undefined") (window as any).__rewrite = rwMetrics;
/** Host variables may be injected after the external bundle executes. Resolve
 * the profile from the same merged values as the composition, before mounting. */
export function setRendererProfile(profile = "studio") {
  Object.assign(RW, resolveFlags(profile));
  rwMetrics.profile = profile;
}
source/src/rig/LogoRig.ts
/** Timeline controls sampled at fixed simulation time, independent of render FPS. */
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
import type { World } from "../World";
import { D } from "../dials/store";
import { assemblyFrontPhase } from "../powder/returnGroups";
import { sim } from "../core/state";
const {
  Fn,
  uniform,
  instancedArray,
  instanceIndex,
  uint,
  ivec3,
  vec3,
  vec4,
  float,
  If,
  texture3D,
  textureStore,
  clamp,
  abs,
  max,
  min,
  length,
  mix,
  smoothstep,
  sin,
} = tsl;
export interface RigSample {
  x: number;
  y: number;
  z: number;
  rx: number;
  ry: number;
  rz: number;
  breakup: number;
  assembly: number;
  target?: number;
  fade?: number;
}
export interface LogoRigOptions {
  duration: number;
  mode: "physical" | "directed" | "hybrid";
  sequence?: boolean;
  sample: (t: number) => RigSample;
  retarget?: (target: number) => void;
}
export class LogoRig {
  private previousBreak = 0;
  private previousAssembly = 0;
  private assembling = false;
  private capturePending = false;
  private readonly from = uniform(0);
  private readonly to = uniform(0);
  private readonly progress = uniform(0);
  private readonly mode = uniform(0);
  private readonly start;
  private readonly captureProgress;
  private readonly returnRadius;
  private newCut = false;
  private readonly fracture;
  private readonly capture;
  private readonly guide;
  private sample!: RigSample;
  private target = 0;
  private breakLatch = false;
  constructor(
    private world: World,
    private options: LogoRigOptions,
  ) {
    const e = world.erosion,
      pw = world.powder,
      B = pw.buffers,
      u = pw.u,
      R = e.res,
      i = instanceIndex;
    this.start = instancedArray(pw.count, "vec4");
    this.captureProgress = instancedArray(pw.count, "float");
    this.returnRadius = instancedArray(pw.count, "vec2");
    const previous = texture3D(e.tex),
      cells = texture3D(e.cellTex);
    this.fracture = Fn(() => {
      const c = ivec3(i.mod(uint(R)), i.div(uint(R)).mod(uint(R)), i.div(uint(R * R)));
      const cell = cells.load(c).level(0).toVar(),
        old = previous.load(c).level(0).toVar();
      // A diagonal front through the existing fracture cells: same shard geometry, no mesh displacement.
      const rank = clamp(
        abs(cell.y.add(cell.x.mul(0.3)))
          .div(e.bound * 1.3)
          .add(cell.w.mul(0.07)),
        0.00001,
        0.99999,
      );
      const cut = rank.greaterThan(this.from).and(rank.lessThanEqual(this.to));
      If(cut, () => {
        old.assign(vec4(1, 0, 0, 1));
      }).Else(() => {
        old.a.assign(old.a.mul(0.5));
      });
      textureStore(e.scratch, c, old).toWriteOnly();
    })().compute(R ** 3, [64]);
    this.capture = Fn(() => {
      this.start.element(i).assign(B.pos.element(i));
      this.captureProgress.element(i).assign(this.progress);
      this.returnRadius
        .element(i)
        .assign(length(B.pos.element(i).xyz.sub(u.model.mul(vec4(B.rest.element(i).xyz, 1)).xyz)));
    })().compute(pw.count, [64]);
    this.guide = Fn(() => {
      const P = B.pos.element(i),
        V = B.vel.element(i),
        M = B.meta.element(i),
        home = u.model.mul(vec4(B.rest.element(i).xyz, 1)).xyz;
      const state = M.x;
      // A landed shard must stay landed; never revive its captured size or old path.
      If(state.greaterThan(4.5), () => {
        P.xyz.assign(home);
        P.w.assign(0);
        V.xyz.assign(vec3(0));
      });
      If(state.greaterThan(0.5).and(state.lessThan(2.5)), () => {
        // Shards released during an overlapping breakup begin their own continuous approach here.
        If(state.lessThan(1.5), () => {
          this.start.element(i).assign(P);
          this.captureProgress.element(i).assign(this.progress);
          B.heal.element(i).assign(vec4(P.xyz, u.time));
          this.returnRadius.element(i).assign(length(P.xyz.sub(home)));
        });
        const phase = max(
          this.captureProgress.element(i),
          assemblyFrontPhase(B.rest.element(i).xyz, u).mul(0.22),
        );
        const p = clamp(
          this.progress.sub(phase).div(max(0.000001, float(1).sub(phase))),
          0,
          1,
        ).toVar();
        If(this.progress.greaterThanEqual(0.99999), () => {
          p.assign(1);
        });
        const ease = p.mul(p).mul(float(3).sub(p.mul(2)));
        const start = this.start.element(i).xyz;
        const bend = sin(p.mul(Math.PI)).mul(0.35);
        const path = mix(start, home, ease).add(
          vec3(bend.mul(sin(M.y.mul(12))), bend.mul(0.5), bend),
        );
        If(this.mode.greaterThan(0.5), () => {
          P.xyz.assign(path);
        }).Else(() => {
          // Keep physical tangential motion, bounded by a shrinking distance to the
          // current home. Guidance can pull inward, but cannot undo spring progress.
          const radius = this.returnRadius.element(i);
          const offset = P.xyz.sub(home).toVar();
          const distance = length(offset);
          const allowed = min(radius.y, radius.x.mul(float(1).sub(ease)));
          const nextDistance = min(distance, allowed);
          P.xyz.assign(home.add(offset.mul(nextDistance.div(max(distance, 0.000001)))));
          radius.y.assign(nextDistance);
        });
        // AssemblyField may restore a region only when its visible shard actually reaches it.
        M.x.assign(2);
        P.w.assign(max(P.w, this.start.element(i).w.mul(float(1).sub(smoothstep(0.9, 1, p)))));
        If(p.greaterThanEqual(0.99999), () => {
          P.xyz.assign(home);
          P.w.assign(0);
          V.xyz.assign(vec3(0));
          M.x.assign(5);
        });
      });
    })().compute(pw.count, [64]);
    pw.beforeIntegrate = () => {
      u.strokeDir.value.set(1, -0.3, 0).normalize();
      u.strokeSpeed.value = 3;
      u.rigStrength.value =
        this.options.mode === "physical" ? this.progress.value : this.progress.value > 0 ? 1 : 0;
      u.returnAfter.value = 0;
      u.heal.value = this.progress.value > 0 ? 1 : 0;
      u.repel.value = this.progress.value > 0 ? 0 : 1;
    };
    pw.beforeAssembly = () => {
      if (this.capturePending) {
        world.renderer.compute(this.capture);
        this.capturePending = false;
      }
      if (this.assembling && this.options.mode !== "physical") world.renderer.compute(this.guide);
      if (this.assembling && this.newCut) pw.prepareAssembly(world.renderer);
    };
    pw.assemblyEnabled = false;
    u.rigEnabled.value = 1;
    world.onBeforeSimulation = (t, dt) => this.update(t, dt);
  }
  pose(t: number) {
    const s = this.options.sample(t);
    this.sample = s;
    this.world.motionGroup.position.set(s.x, s.y + 0.05, s.z);
    this.world.motionGroup.rotation.set(
      (s.rx * Math.PI) / 180,
      (s.ry * Math.PI) / 180,
      (s.rz * Math.PI) / 180,
      "YXZ",
    );
    this.world.objectGroup.quaternion.identity();
    const target = s.target ?? 0;
    if (target !== this.target) {
      this.target = target;
      this.options.retarget?.(target);
      this.previousBreak = 0;
      this.previousAssembly = 0;
      this.breakLatch = false;
      this.assembling = false;
      this.capturePending = false;
      this.progress.value = 0;
    }
  }
  reset() {
    this.target = 0;
    this.breakLatch = false;
    this.previousBreak = 0;
    this.previousAssembly = 0;
    this.assembling = false;
    this.capturePending = false;
    this.world.powder.assemblyEnabled = false;
    this.progress.value = 0;
    this.world.powder.restoreThresholds(this.world.renderer);
  }
  private update(t: number, dt: number) {
    if (dt <= 0) return;
    const s = this.sample || this.options.sample(t),
      w = this.world,
      pw = w.powder;
    const b = Math.max(0, Math.min(1, s.breakup / 100)),
      a = Math.max(0, Math.min(1, s.assembly / 100));
    D.performance.idleSkip = false;
    w.erosion.u.reconstruct.value = 1;
    this.newCut = b > this.previousBreak;
    if (this.options.sequence && this.newCut) {
      if (!this.breakLatch) pw.restoreThresholds(w.renderer);
      this.breakLatch = true;
    }
    if (this.options.sequence && b === 0 && a === 0) this.breakLatch = false;
    this.from.value = this.previousBreak;
    this.to.value = b;
    w.renderer.compute(this.fracture);
    w.erosion.commitAssembly(w.renderer);
    if (a > 0 && this.previousAssembly <= 0 && !this.breakLatch) {
      if (!this.options.sequence) {
        pw.retarget(w.renderer, pw.restInit);
        pw.restoreThresholds(w.renderer);
      }
      pw.assemblyEnabled = true;
      this.assembling = true;
      this.capturePending = true;
      pw.u.assemblyEnd.value = 1e6;
      sim.lastStrokeT = t;
    }
    if (a === 0 || this.breakLatch) {
      this.assembling = false;
      pw.assemblyEnabled = false;
    }
    const returning = a > 0 && !this.breakLatch;
    sim.healing = returning;
    this.progress.value = returning ? a : 0;
    this.mode.value = this.options.mode === "directed" ? 1 : 0;
    // Repulsion fights return; use the original eject physics only while no return is requested.
    pw.u.repel.value = returning ? 0 : 1;
    this.previousBreak = b;
    this.previousAssembly = a;
  }
  dispose() {
    this.start.value.dispose?.();
    this.captureProgress.value.dispose?.();
    this.returnRadius.value.dispose?.();
  }
}
source/src/scene/Backdrop.ts
// Fullscreen studio backdrop: a single soft radial bloom from the top-centre falling to near-black,
// rendered directly as `scene.backgroundNode` with dithering so it never bands.
import { tsl } from "../tsl/t";
const {
  Fn,
  vec2,
  vec3,
  vec4,
  float,
  uniform,
  screenUV,
  screenSize,
  mix,
  pow,
  smoothstep,
  fract,
  sin,
  dot,
  length,
  max,
} = tsl;
import * as THREE from "three/webgpu";
import { hash31 } from "../tsl/noise";

export type BackdropUniforms = {
  uTop: any;
  uMid: any;
  uBottom: any;
  uCenter: any;
  uRadius: any;
  uFalloff: any;
  uNoise: any;
  uTime: any;
};

/** The studio gradient colour at a screen uv (shared by the background and the translucent grains). */
export function backdropColorAt(u: BackdropUniforms, uv: any) {
  const aspect = screenSize.x.div(screenSize.y);
  const d = vec2(uv.x.sub(u.uCenter.x).mul(aspect), uv.y.sub(u.uCenter.y));
  const r = length(d).div(u.uRadius);
  const t = pow(smoothstep(0.0, 1.0, r), u.uFalloff);
  const vert = smoothstep(0.0, 1.0, uv.y.oneMinus().mul(1.15));
  const corner = smoothstep(0.35, 1.3, length(vec2(uv.x.sub(0.5).mul(aspect), uv.y.sub(0.5))));
  const col = mix(u.uTop, u.uMid, t);
  return mix(col, u.uBottom, vert.mul(corner).mul(0.85));
}

export function createBackdrop() {
  const uTop = uniform(new THREE.Color("#2a2a2a"));
  const uMid = uniform(new THREE.Color("#050505"));
  const uBottom = uniform(new THREE.Color("#030303"));
  const uCenter = uniform(new THREE.Vector2(0.5, 1.02));
  const uRadius = uniform(0.55);
  const uFalloff = uniform(1.6);
  const uNoise = uniform(1.0);
  const uTime = uniform(0);

  const uniforms = { uTop, uMid, uBottom, uCenter, uRadius, uFalloff, uNoise, uTime };
  const node = Fn(() => {
    const col = backdropColorAt(uniforms, screenUV).toVar();
    // barely-visible large-scale noise (±1 grey level) plus a hash dither to break banding
    const uv = screenUV;
    const n = hash31(vec3(uv.mul(vec2(37.0, 21.0)), uTime.mul(0.37)));
    const big = sin(dot(uv, vec2(5.3, 7.1)).add(uTime.mul(0.05)))
      .mul(0.5)
      .add(0.5);
    const grey = big
      .sub(0.5)
      .mul(1.0 / 255.0)
      .mul(uNoise)
      .add(
        n
          .sub(0.5)
          .mul(1.0 / 255.0)
          .mul(uNoise),
      );
    return vec4(max(col.add(vec3(grey)), 0.0), 1.0);
  })();

  return { node, uniforms };
}
void fract;
void float;
source/src/scene/DebugViews.ts
// Debug overlays: erosion field slice, density grid slice, SDF hit points.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
const { vec3, vec4, uniform, uv, texture3D, float } = tsl;
import { D } from "../dials/store";
import type { ErosionField } from "../erosion/ErosionField";
import type { Powder } from "../powder/Powder";
import type { Interaction } from "../core/interaction";

export class DebugViews {
  readonly group = new THREE.Group();
  private erosionQuad: THREE.Mesh;
  private densityQuad: THREE.Mesh;
  private hitEntry: THREE.Mesh;
  private hitExit: THREE.Mesh;
  private uSlice = uniform(0.5);

  constructor(
    readonly camera: THREE.Camera,
    erosion: ErosionField,
    powder: Powder,
  ) {
    const mkQuad = (tex: THREE.Texture, mode: "erosion" | "density") => {
      const m = new THREE.MeshBasicNodeMaterial();
      const s = texture3D(tex).sample(vec3(uv(), this.uSlice)).level(0);
      m.colorNode =
        mode === "erosion"
          ? vec4(s.r, s.r.mul(0.6).add(s.g.mul(0.8)), s.r.mul(0.6), 1.0)
          : vec4(s.r.mul(2.0), s.g, s.b, 1.0);
      m.depthTest = false;
      m.depthWrite = false;
      m.transparent = false;
      const q = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), m);
      q.renderOrder = 1000;
      q.frustumCulled = false;
      return q;
    };
    this.erosionQuad = mkQuad(erosion.tex, "erosion");
    this.densityQuad = mkQuad(powder.densityTex, "density");
    const dot = new THREE.SphereGeometry(0.03, 12, 8);
    this.hitEntry = new THREE.Mesh(dot, new THREE.MeshBasicNodeMaterial({ color: "#ff4040" }));
    this.hitExit = new THREE.Mesh(dot, new THREE.MeshBasicNodeMaterial({ color: "#40a0ff" }));
    this.group.add(this.hitEntry, this.hitExit);
    // slices live in camera space (lower-left corner)
    camera.add(this.erosionQuad, this.densityQuad);
    void float;
  }

  update(interaction: Interaction) {
    const dbg = D.debug;
    this.uSlice.value = dbg.erosionSliceZ;
    const cam = this.camera as THREE.PerspectiveCamera;
    const dist = 3;
    const h = 2 * dist * Math.tan(THREE.MathUtils.degToRad(cam.fov / 2)),
      w = h * cam.aspect;
    const size = h * 0.28;
    this.erosionQuad.visible = dbg.showErosionSlice;
    this.erosionQuad.scale.set(size, size, 1);
    this.erosionQuad.position.set(-w / 2 + size * 0.6, -h / 2 + size * 0.6, -dist);
    this.densityQuad.visible = dbg.showDensityGrid;
    this.densityQuad.scale.set(size, size, 1);
    this.densityQuad.position.set(
      -w / 2 + size * (dbg.showErosionSlice ? 1.75 : 0.6),
      -h / 2 + size * 0.6,
      -dist,
    );
    const show = dbg.showHitPoints && interaction.hasHit;
    this.hitEntry.visible = show;
    this.hitExit.visible = show;
    if (show) {
      this.hitEntry.position.copy(interaction.hitEntry);
      this.hitExit.position.copy(interaction.hitExit);
    }
  }

  dispose() {
    this.camera.remove(this.erosionQuad, this.densityQuad);
    this.erosionQuad.geometry.dispose();
    this.densityQuad.geometry.dispose();
    (this.erosionQuad.material as THREE.Material).dispose();
    (this.densityQuad.material as THREE.Material).dispose();
  }
}
source/src/scene/Environment.ts
// HDR studio: rectangular softboxes and narrow edge strips, with dark flags
// between them. Float radiance is retained for reflections and PMREM filtering.
import * as THREE from "three/webgpu";
export function createEnvironment(width = 1024, height = 512) {
  const data = new Float32Array(width * height * 4);
  const tex = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.FloatType);
  tex.mapping = THREE.EquirectangularReflectionMapping;
  tex.colorSpace = THREE.LinearSRGBColorSpace;
  tex.wrapS = THREE.RepeatWrapping;
  tex.wrapT = THREE.ClampToEdgeWrapping;
  tex.minFilter = tex.magFilter = THREE.LinearFilter;
  const smooth = (a: number, b: number, x: number) => {
    const t = Math.max(0, Math.min(1, (x - a) / (b - a)));
    return t * t * (3 - 2 * t);
  };
  const directions = Array.from({ length: width * height }, (_, i) => {
    const u = ((i % width) + 0.5) / width,
      v = (Math.floor(i / width) + 0.5) / height;
    const az = (u - 0.5) * Math.PI * 2,
      el = (v - 0.5) * Math.PI;
    return new THREE.Vector3(
      Math.cos(el) * Math.cos(az),
      Math.sin(el),
      Math.cos(el) * Math.sin(az),
    );
  });
  function update(L: any) {
    const card = (color: string, power: number, el: number, az: number, w: number, h: number) => {
      const e = THREE.MathUtils.degToRad(el),
        a = THREE.MathUtils.degToRad(az);
      const normal = new THREE.Vector3(
        Math.cos(e) * Math.sin(a),
        Math.sin(e),
        Math.cos(e) * Math.cos(a),
      );
      const right = new THREE.Vector3()
        .crossVectors(new THREE.Vector3(0, 1, 0), normal)
        .normalize();
      const up = new THREE.Vector3().crossVectors(normal, right);
      return { color: new THREE.Color(color), power, normal, right, up, w, h };
    };
    const cards = [
      card(
        L.key.color,
        L.envSoftbox * 5,
        L.key.elevation,
        L.key.azimuth,
        0.42 * L.key.size,
        0.65 * L.key.size,
      ),
      card(
        L.rimColor,
        L.envRim * 6,
        L.rimElevation,
        L.rimAzimuth,
        0.1 * L.rimSize,
        0.9 * L.rimSize,
      ),
      card(
        L.accentCool.color,
        L.accentCool.reflection * 4,
        L.accentCool.elevation,
        L.accentCool.azimuth,
        0.13 * L.accentCool.size,
        0.75 * L.accentCool.size,
      ),
      card(
        L.accentWarm.color,
        L.accentWarm.reflection * 4,
        L.accentWarm.elevation,
        L.accentWarm.azimuth,
        0.2 * L.accentWarm.size,
        0.7 * L.accentWarm.size,
      ),
    ];
    const sky = new THREE.Color(L.fillColor),
      ground = new THREE.Color(L.fillGroundColor);
    directions.forEach((d, i) => {
      const hemisphere = smooth(-0.3, 0.8, d.y);
      const fill = L.envFill * 0.055;
      let r = (ground.r * (1 - hemisphere) + sky.r * hemisphere) * fill;
      let g = (ground.g * (1 - hemisphere) + sky.g * hemisphere) * fill;
      let b = (ground.b * (1 - hemisphere) + sky.b * hemisphere) * fill;
      for (const c of cards) {
        const facing = d.dot(c.normal);
        if (facing <= 0) continue;
        const x = Math.abs(d.dot(c.right) / facing),
          y = Math.abs(d.dot(c.up) / facing);
        const coverage = (1 - smooth(c.w * 0.45, c.w, x)) * (1 - smooth(c.h * 0.45, c.h, y));
        const glow = c.power * coverage;
        r += c.color.r * glow;
        g += c.color.g * glow;
        b += c.color.b * glow;
      }
      data.set([r, g, b, 1], i * 4);
    });
    tex.needsUpdate = true;
    (tex as any).needsPMREMUpdate = true;
  }
  return { texture: tex, update };
}
source/src/scene/Lights.ts
// One soft key from above-front (VSM shadows), a weak hemisphere fill, a wide rim spot from above-behind.
import * as THREE from "three/webgpu";
import { D } from "../dials/store";

export class Lights {
  key = new THREE.DirectionalLight(0xffffff, 3);
  fill = new THREE.HemisphereLight(0xffffff, 0x000000, 0.2);
  rim = new THREE.SpotLight(0xffffff, 1, 0, Math.PI / 3, 1, 0);
  accentCool = new THREE.DirectionalLight(0xc7ddff, 0.35);
  accentWarm = new THREE.DirectionalLight(0xffdfc4, 0.22);
  group = new THREE.Group();
  private mapSize = 0;
  private settingsVersion = -1;

  constructor() {
    this.key.castShadow = true;
    this.key.shadow.camera.left = -5.5;
    this.key.shadow.camera.right = 5.5;
    this.key.shadow.camera.top = 5.5;
    this.key.shadow.camera.bottom = -5.5;
    this.key.shadow.camera.near = 2;
    this.key.shadow.camera.far = 30;
    this.key.target.position.set(0, 0, 0);
    this.rim.target.position.set(0, 0, 0);
    this.group.add(
      this.key,
      this.key.target,
      this.fill,
      this.rim,
      this.rim.target,
      this.accentCool,
      this.accentCool.target,
      this.accentWarm,
      this.accentWarm.target,
    );
  }

  update(t: number, hoverAmount: number) {
    const L = D.lighting;
    const swayA = Math.sin((t * 2 * Math.PI) / Math.max(1, L.sway.period)) * L.sway.amplitude;
    const swayB =
      Math.sin(((t * 2 * Math.PI) / Math.max(1, L.sway.period)) * 0.61 + 0.8) * L.sway.amplitude;
    const el = THREE.MathUtils.degToRad(L.key.elevation + swayB * 40);
    const az = THREE.MathUtils.degToRad(L.key.azimuth + swayA * 60);
    const dist = 12;
    this.key.position.set(
      Math.cos(el) * Math.sin(az) * dist,
      Math.sin(el) * dist,
      Math.cos(el) * Math.cos(az) * dist,
    );
    this.key.intensity = L.key.intensity * (1 + L.key.hoverBoost * hoverAmount);
    const rel = THREE.MathUtils.degToRad(L.rimElevation);
    const raz = THREE.MathUtils.degToRad(L.rimAzimuth ?? 180);
    this.rim.position.set(
      Math.cos(rel) * Math.sin(raz) * 10,
      Math.sin(rel) * 10,
      Math.cos(rel) * Math.cos(raz) * 10,
    );
    for (const [light, cfg] of [
      [this.accentCool, L.accentCool],
      [this.accentWarm, L.accentWarm],
    ] as const) {
      if (!cfg) continue;
      const e = THREE.MathUtils.degToRad(cfg.elevation),
        a = THREE.MathUtils.degToRad(cfg.azimuth);
      light.position.set(
        Math.cos(e) * Math.sin(a) * 12,
        Math.sin(e) * 12,
        Math.cos(e) * Math.cos(a) * 12,
      );
      light.color.set(cfg.color);
      light.intensity = cfg.intensity;
    }
    if (D.version !== this.settingsVersion) {
      this.settingsVersion = D.version;
      this.key.color.set(L.key.color);
      this.fill.intensity = L.fill;
      this.fill.color.set(L.fillColor);
      this.fill.groundColor.set(L.fillGroundColor);
      this.rim.color.set(L.rimColor);
      this.rim.intensity = L.rim * 40;
      this.rim.angle = THREE.MathUtils.degToRad(L.rimAngle ?? 65);
      this.rim.penumbra = 1;
      this.rim.decay = 1.2;
      this.rim.distance = 60;
      const sh = this.key.shadow;
      sh.radius = L.shadow.softness;
      sh.blurSamples = Math.round(L.shadow.samples);
      sh.bias = L.shadow.bias;
      sh.intensity = L.shadow.intensity;
      const ms = parseInt(L.shadow.mapSize, 10);
      if (ms !== this.mapSize) {
        this.mapSize = ms;
        sh.mapSize.set(ms, ms);
        if (sh.map) {
          sh.map.dispose();
          (sh as any).map = null;
        }
        sh.needsUpdate = true;
      }
    }
  }
}
source/src/scene/Rig.ts
// Camera parallax + object rotation rig, driven by the cursor with lag, idle Lissajous drift.
import * as THREE from "three/webgpu";
import { damp } from "../core/ease";
import { input, heroFrame } from "../core/state";
import { D } from "../dials/store";

export class Rig {
  yaw = 0;
  pitch = 0; // smoothed object rotation (radians)
  parYaw = 0;
  parPitch = 0; // smoothed camera parallax (radians)
  idleBlend = 0; // 0 = cursor control, 1 = idle drift
  readonly lookAt = new THREE.Vector3();
  private readonly base = new THREE.Vector3();
  private readonly spherical = new THREE.Spherical();

  update(camera: THREE.PerspectiveCamera, object: THREE.Object3D, t: number, dt: number) {
    const framed = heroFrame.enabled && heroFrame.framing;
    const c = D.camera;
    const frame = framed ? heroFrame : c;
    const idle = t - input.lastMoveT > c.idleDelay;
    this.idleBlend = damp(this.idleBlend, idle ? 1 : 0, idle ? 1.5 : 0.6, dt);
    const cx = input.inside ? input.x : 0,
      cy = input.inside ? input.y : 0;
    const w = (2 * Math.PI) / Math.max(1, c.idlePeriod);
    const driftYaw = Math.sin(t * w) * c.idleAmplitude,
      driftPitch = Math.sin(t * w * 1.37 + 1.1) * c.idleAmplitude * 0.5;
    const targetYaw = THREE.MathUtils.degToRad(
      frame.baseYaw + THREE.MathUtils.lerp(cx * frame.rotateYaw, driftYaw, this.idleBlend),
    );
    const targetPitch = THREE.MathUtils.degToRad(
      frame.basePitch + THREE.MathUtils.lerp(-cy * frame.rotatePitch, driftPitch, this.idleBlend),
    );
    this.yaw = damp(this.yaw, targetYaw, c.rotationLag * 0.35, dt);
    this.pitch = damp(this.pitch, targetPitch, c.rotationLag * 0.35, dt);
    object.rotation.set(this.pitch, this.yaw, 0, "YXZ");

    const pr = THREE.MathUtils.degToRad(frame.parallaxRange);
    this.parYaw = damp(this.parYaw, -cx * pr, c.parallaxSmoothing * 0.35, dt);
    this.parPitch = damp(this.parPitch, cy * pr * 0.6, c.parallaxSmoothing * 0.35, dt);
    this.lookAt.set(framed ? heroFrame.lookAtX : 0, frame.lookAtY, 0);
    if (heroFrame.cameraOverride) {
      // Exported camera motion owns the view, while the existing rig above continues to animate the object.
      heroFrame.cameraOverride(camera);
      return;
    }
    const base = this.base.set(0, frame.height, frame.distance).sub(this.lookAt);
    const sph = this.spherical.setFromVector3(base);
    sph.theta += this.parYaw;
    sph.phi = THREE.MathUtils.clamp(sph.phi + this.parPitch, 0.1, Math.PI - 0.1);
    camera.position.setFromSpherical(sph).add(this.lookAt);
    camera.lookAt(this.lookAt);
    if (camera.fov !== frame.fov) {
      camera.fov = frame.fov;
      camera.updateProjectionMatrix();
    }
  }
}
source/src/shape/deform.ts
import * as THREE from "three/webgpu";
import { ImprovedNoise } from "three/addons/math/ImprovedNoise.js";
import { mergeVertices, toCreasedNormals } from "three/addons/utils/BufferGeometryUtils.js";
import { rng } from "../core/seed";
import approvedPreset from "../../presets/approved-material.json";
import { refineText, textRefinementSettings, repairTextSeams } from "./textRefine";
import { logoRefinementSettings } from "./logoRefine";

export interface Deformation {
  strength: number;
  scale: number;
  seed: number;
}
export const DEFORMATION_DEFAULTS: Readonly<Deformation> = Object.freeze({
  strength: approvedPreset.deformStrength,
  scale: approvedPreset.deformScale,
  seed: approvedPreset.deformSeed,
});
export function validateDeformationScale(value: number) {
  if (!Number.isFinite(value) || value < 0.001 || value > 0.5)
    throw new RangeError(
      `Noise feature size ${value} is outside 0.001–0.5. Edit this value to load the preset; other settings have not been changed.`,
    );
  return value;
}
export function resolveDeformation(input?: Partial<Deformation>): Deformation {
  const number = (key: keyof Deformation, min: number, max: number) => {
    const v = input?.[key];
    return typeof v === "number" && Number.isFinite(v)
      ? Math.max(min, Math.min(max, v))
      : DEFORMATION_DEFAULTS[key];
  };
  return {
    strength: number("strength", 0, 0.08),
    scale: validateDeformationScale(input?.scale ?? DEFORMATION_DEFAULTS.scale),
    seed: Math.round(number("seed", 0, 65535)),
  };
}

/** Three octave Perlin noise, like the original shader FBM, baked in object space.
 * Sequential noise shears keep a one-to-one continuous map (each shear has determinant 1),
 * so opposite sides of thin strokes/counters move together instead of inflating into each other.
 */
export function makeDeformation(input: Deformation) {
  const { strength, scale, seed } = resolveDeformation(input);
  const noise = new ImprovedNoise(),
    rand = rng(seed);
  const offsets = Array.from({ length: 3 }, () => [rand() * 256, rand() * 256, rand() * 256]);
  function fbm(a: number, b: number, channel: number) {
    const o = offsets[channel];
    let x = a / scale + o[0],
      y = b / scale + o[1],
      z = o[2],
      sum = 0,
      amp = 1;
    for (let i = 0; i < 3; i++) {
      sum += amp * noise.noise(x, y, z);
      x = x * 2 + 17.3;
      y = y * 2 + 9.1;
      z = z * 2 + 31.7;
      amp *= 0.5;
    }
    return sum / 1.75;
  }
  return (x: number, y: number, z: number): [number, number, number] => {
    if (strength === 0) return [x, y, z];
    x += strength * fbm(y, z, 0);
    y += strength * fbm(z, x, 1);
    z += strength * fbm(x, y, 2);
    return [x, y, z];
  };
}

/** Conforming shared-edge subdivision. Stop before exceeding a fixed extra-triangle budget.
 * Every marked edge is split in ALL adjacent triangles, including cap/side seams.
 */
function refine(positions: number[], initial: number[], maxEdge: number) {
  let triangles = initial;
  const limit = initial.length / 3 + 6000,
    maxLength2 = maxEdge * maxEdge;
  const key = (a: number, b: number) => (a < b ? `${a}:${b}` : `${b}:${a}`);
  for (let pass = 0; pass < 8; pass++) {
    const marked = new Map<string, number>();
    let extra = 0;
    for (let i = 0; i < triangles.length; i += 3) {
      const a = triangles[i],
        b = triangles[i + 1],
        c = triangles[i + 2];
      for (const [u, v] of [
        [a, b],
        [b, c],
        [c, a],
      ]) {
        const dx = positions[u * 3] - positions[v * 3],
          dy = positions[u * 3 + 1] - positions[v * 3 + 1],
          dz = positions[u * 3 + 2] - positions[v * 3 + 2];
        if (dx * dx + dy * dy + dz * dz > maxLength2) {
          marked.set(key(u, v), -1);
          extra++;
        }
      }
    }
    if (!marked.size || triangles.length / 3 + extra > limit) break;
    for (const [edge] of marked) {
      const [a, b] = edge.split(":").map(Number),
        m = positions.length / 3;
      positions.push(
        (positions[a * 3] + positions[b * 3]) / 2,
        (positions[a * 3 + 1] + positions[b * 3 + 1]) / 2,
        (positions[a * 3 + 2] + positions[b * 3 + 2]) / 2,
      );
      marked.set(edge, m);
    }
    const out: number[] = [];
    for (let i = 0; i < triangles.length; i += 3) {
      const a = triangles[i],
        b = triangles[i + 1],
        c = triangles[i + 2];
      const ab = marked.get(key(a, b)),
        bc = marked.get(key(b, c)),
        ca = marked.get(key(c, a));
      if (ab !== undefined && bc !== undefined && ca !== undefined)
        out.push(a, ab, ca, ab, b, bc, ca, bc, c, ab, bc, ca);
      else if (ab !== undefined && bc !== undefined) out.push(b, bc, ab, a, ab, c, ab, bc, c);
      else if (bc !== undefined && ca !== undefined) out.push(c, ca, bc, b, bc, a, bc, ca, a);
      else if (ca !== undefined && ab !== undefined) out.push(a, ab, ca, c, ca, b, ca, ab, b);
      else if (ab !== undefined) out.push(a, ab, c, ab, b, c);
      else if (bc !== undefined) out.push(b, bc, a, bc, c, a);
      else if (ca !== undefined) out.push(c, ca, b, ca, a, b);
      else out.push(a, b, c);
    }
    triangles = out;
  }
  return triangles;
}

/** Zero is an exact identity, including original buffers/normals. Caller owns the returned geometry. */
export function deformGeometry(
  source: THREE.BufferGeometry,
  input: Deformation,
  creaseAngle = 40,
  textMeshDetail?: number,
  logoMeshDetail?: number,
): THREE.BufferGeometry {
  const options = resolveDeformation(input);
  if (options.strength === 0) return source;
  const copy = source.clone();
  // The material uses object-space fields, not UVs. Weld geometric seams before refinement.
  for (const name of Object.keys(copy.attributes))
    if (name !== "position") copy.deleteAttribute(name);
  copy.clearGroups();
  const welded = mergeVertices(copy, 1e-7);
  copy.dispose();
  const positions = Array.from(welded.getAttribute("position").array);
  const original = Array.from(welded.index!.array);
  welded.dispose();
  // Independent text/logo budgets share conforming surface refinement. Legacy callers
  // without a mesh-detail argument retain the original refinement path.
  const settings =
    textMeshDetail !== undefined
      ? textRefinementSettings(options.scale, textMeshDetail)
      : logoMeshDetail !== undefined
        ? logoRefinementSettings(source, logoMeshDetail)
        : null;
  const triangles = settings
    ? refineText(
        positions,
        repairTextSeams(positions, original),
        settings.maxEdge,
        settings.extraTriangles,
      )
    : refine(positions, original, options.scale * 0.3);
  const warp = makeDeformation(options);
  for (let i = 0; i < positions.length; i += 3) {
    const q = warp(positions[i], positions[i + 1], positions[i + 2]);
    positions[i] = q[0];
    positions[i + 1] = q[1];
    positions[i + 2] = q[2];
  }
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geometry.setIndex(triangles);
  const result = toCreasedNormals(geometry, THREE.MathUtils.degToRad(creaseAngle));
  if (result !== geometry) geometry.dispose();
  // Keep the centered-geometry contract used by bounds and shard-home sampling.
  result.center();
  result.computeBoundingBox();
  result.computeBoundingSphere();
  return result;
}
source/src/shape/geometry.ts
// Mesh geometry per shape. Every shape ends up as a closed, densely tessellated surface whose
// vertices lie on the SDF zero set (so the raymarch / erosion coupling stays consistent).
import * as THREE from "three/webgpu";
import { ShapeSpec, sdfNormal } from "./sdf";

/** Push each vertex onto the SDF zero set along the gradient (few Newton steps) and take SDF normals. */
function projectOntoSDF(geo: THREE.BufferGeometry, s: ShapeSpec, steps = 4) {
  const pos = geo.attributes.position as THREE.BufferAttribute;
  const nrm = new Float32Array(pos.count * 3);
  for (let i = 0; i < pos.count; i++) {
    let x = pos.getX(i),
      y = pos.getY(i),
      z = pos.getZ(i);
    for (let k = 0; k < steps; k++) {
      const d = s.sdf(x, y, z);
      const n = sdfNormal(s, x, y, z);
      x -= n[0] * d;
      y -= n[1] * d;
      z -= n[2] * d;
    }
    const n = sdfNormal(s, x, y, z, 0.004);
    pos.setXYZ(i, x, y, z);
    nrm[i * 3] = n[0];
    nrm[i * 3 + 1] = n[1];
    nrm[i * 3 + 2] = n[2];
  }
  pos.needsUpdate = true;
  geo.setAttribute("normal", new THREE.BufferAttribute(nrm, 3));
  return geo;
}

export function buildGeometry(
  s: ShapeSpec,
  segments: number,
  logoGeometry?: THREE.BufferGeometry,
): THREE.BufferGeometry {
  const R = s.size,
    r = s.size * s.tubeRatio;
  switch (s.name) {
    case "torus": {
      const g = new THREE.TorusGeometry(R, r, Math.max(24, Math.round(segments * 0.4)), segments);
      // TorusGeometry lies in XY with the hole along Z — matches the sdf.
      return g;
    }
    case "sphere":
      return projectOntoSDF(
        new THREE.SphereGeometry(1, segments, Math.round(segments * 0.6)),
        s,
        2,
      );
    case "roundedBox": {
      const seg = Math.max(8, Math.round(segments / 4));
      const h = s.size * 0.92;
      const g = new THREE.BoxGeometry(
        2 * h,
        2 * h,
        2 * h * 0.55,
        seg,
        seg,
        Math.max(4, Math.round(seg * 0.55)),
      );
      // pre-round: blend vertices toward a sphere before projecting so the corners get tessellation
      const pos = g.attributes.position as THREE.BufferAttribute;
      for (let i = 0; i < pos.count; i++) {
        const x = pos.getX(i),
          y = pos.getY(i),
          z = pos.getZ(i);
        const l = Math.hypot(x, y, z) || 1;
        const t = 0.35;
        pos.setXYZ(
          i,
          x * (1 - t) + (x / l) * h * t,
          y * (1 - t) + (y / l) * h * t,
          z * (1 - t) + (z / l) * h * t,
        );
      }
      return projectOntoSDF(g, s, 6);
    }
    case "pyramid": {
      const g = new THREE.IcosahedronGeometry(s.size * 1.4, 6);
      return projectOntoSDF(g, s, 8);
    }
    case "icosahedron": {
      const g = new THREE.IcosahedronGeometry(s.size * 1.2, 6);
      return projectOntoSDF(g, s, 6);
    }
    case "logo": {
      const g = (logoGeometry ?? new THREE.TorusGeometry(R, r, 64, segments)).clone();
      if (logoGeometry)
        g.scale(s.size, s.size, s.size); // normals are unaffected by a uniform scale
      else g.computeVertexNormals();
      return g;
    }
  }
}

/** Small irregular grain meshes (lumpy icospheres and shards). */
export function buildGrainVariants(
  count: number,
  rand: () => number,
  faceted = false,
): THREE.BufferGeometry[] {
  const out: THREE.BufferGeometry[] = [];
  for (let v = 0; v < count; v++) {
    if (faceted) {
      out.push(buildFacetedShard(v, rand));
      continue;
    }
    const shard = v % 3 === 2;
    const g = shard
      ? new THREE.TetrahedronGeometry(1, 1) // 16 tris
      : new THREE.IcosahedronGeometry(1, v % 2); // 20 / 80 tris
    const pos = g.attributes.position as THREE.BufferAttribute;
    const sx = 0.7 + rand() * 0.6,
      sy = 0.7 + rand() * 0.6,
      sz = shard ? 0.35 + rand() * 0.3 : 0.7 + rand() * 0.6;
    const seed = rand() * 100;
    for (let i = 0; i < pos.count; i++) {
      const x = pos.getX(i),
        y = pos.getY(i),
        z = pos.getZ(i);
      const l =
        1 + 0.28 * Math.sin(seed + x * 5.1 + y * 3.7) * Math.cos(seed * 0.7 + z * 4.3 + x * 2.2);
      pos.setXYZ(i, x * l * sx, y * l * sy, z * l * sz);
    }
    g.computeVertexNormals();
    out.push(g);
  }
  return out;
}

/** Flat-shaded shard: a low-poly convex solid with per-vertex jitter and hard edges, so every face catches
 *  the key light separately (glass-splinter look). 8-20 tris, no more than the smooth variants. */
function buildFacetedShard(v: number, rand: () => number): THREE.BufferGeometry {
  const kind = v % 3;
  const g =
    kind === 0
      ? new THREE.IcosahedronGeometry(1, 0)
      : kind === 1
        ? new THREE.OctahedronGeometry(1, 0)
        : new THREE.TetrahedronGeometry(1, 0);
  const pos = g.attributes.position as THREE.BufferAttribute;
  // elongated splinter proportions; one axis thin
  const sx = 0.55 + rand() * 0.9,
    sy = 0.55 + rand() * 0.9,
    sz = 0.25 + rand() * 0.4;
  // jitter shared per unique vertex position so the faces stay planar and closed
  const jit = new Map<string, [number, number, number]>();
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i),
      y = pos.getY(i),
      z = pos.getZ(i);
    const k = `${x.toFixed(4)},${y.toFixed(4)},${z.toFixed(4)}`;
    let j = jit.get(k);
    if (!j) {
      j = [(rand() - 0.5) * 0.5, (rand() - 0.5) * 0.5, (rand() - 0.5) * 0.5];
      jit.set(k, j);
    }
    pos.setXYZ(i, (x + j[0]) * sx, (y + j[1]) * sy, (z + j[2]) * sz);
  }
  const flat = g.index ? g.toNonIndexed() : g;
  flat.computeVertexNormals();
  return flat;
}
source/src/shape/gpuSdf.ts
import * as THREE from "three/webgpu";
import { MeshBVH } from "three-mesh-bvh";
import { BVHComputeData, pointQueryResultStruct, wgslTagFn } from "three-mesh-bvh/webgpu";
import { tsl } from "../tsl/t";
import { makeLogoSDF, halfExtents } from "./logo";
import { rwMetrics } from "../rewrite";
const { Fn, instanceIndex, uint, vec3, float, If, dot, max, instancedArray, uniform } = tsl;

/** Runtime GPU closest-point queries. SVG/text geometry remains fully editable.
 * CPU winding uses the exact existing column algorithm; the simulation API is unchanged. */
export async function voxelizeGPU(
  renderer: THREE.WebGPURenderer,
  geo: THREE.BufferGeometry,
  bound: number,
  res: number,
  sampleBound: number,
) {
  const start = performance.now(),
    half = halfExtents(geo),
    thickness = Math.min(half.x, half.y, half.z),
    n = Math.max(16, Math.round(res));
  const range = bound,
    voxel = (2 * sampleBound) / n,
    maxDist = Math.min(range, Math.max(thickness * 2.5, voxel * 14));
  const bvh = new MeshBVH(geo);
  (geo as any).boundsTree = bvh;
  const data = new BVHComputeData(geo);
  data.update();
  const packed = performance.now();
  const query = wgslTagFn`
 fn frostClosest(p:vec3f,cap:f32)->f32 {
  var hit:${pointQueryResultStruct};
  hit.found=true; hit.distanceSq=cap*cap;
  let found=${data.fns.closestPointToPoint}(p,&hit);
  return select(-1.0,sqrt(hit.distanceSq),found);
 }`;
  const output = instancedArray(n * n * n, "float"),
    offset = uniform(0, "uint");
  const box = geo.boundingBox!,
    lo = vec3(box.min.x, box.min.y, box.min.z),
    hi = vec3(box.max.x, box.max.y, box.max.z);
  const slabs = 8,
    slabCount = n * n * slabs;
  const kernel = Fn(() => {
    const i = instanceIndex.add(offset),
      x = i.mod(uint(n)),
      y = i.div(uint(n)).mod(uint(n)),
      z = i.div(uint(n * n));
    const p = vec3(x, y, z)
      .add(0.5)
      .div(n)
      .sub(0.5)
      .mul(2 * sampleBound)
      .toVar();
    const delta = max(max(lo.sub(p), p.sub(hi)), vec3(0));
    const d = float(-1).toVar();
    If(dot(delta, delta).lessThanEqual(maxDist * maxDist), () => {
      d.assign(query(p, float(maxDist)));
    });
    output.element(i).assign(d);
  })().compute(slabCount, [64]);
  for (let base = 0; base < n * n * n; base += slabCount) {
    offset.value = base;
    renderer.compute(kernel);
    await renderer.backend.device.queue.onSubmittedWorkDone();
  }
  const distances = new Float32Array(await renderer.getArrayBufferAsync(output.value));
  const gpuDone = performance.now();
  // Audit sampled GPU distances against the original CPU mesh query.
  let auditMax = 0,
    auditSum = 0,
    auditCount = 0,
    auditMissing = 0,
    random = 173;
  const auditP = new THREE.Vector3();
  for (let a = 0; a < 2048; a++) {
    random = (Math.imul(random, 1664525) + 1013904223) >>> 0;
    const i = random % (n * n * n),
      x = i % n,
      y = Math.floor(i / n) % n,
      z = Math.floor(i / (n * n));
    auditP.set(
      ((x + 0.5) / n - 0.5) * 2 * sampleBound,
      ((y + 0.5) / n - 0.5) * 2 * sampleBound,
      ((z + 0.5) / n - 0.5) * 2 * sampleBound,
    );
    const hit = bvh.closestPointToPoint(auditP, {}, 0, maxDist);
    if (hit && hit.distance <= maxDist && distances[i] >= 0) {
      const e = Math.abs(hit.distance - distances[i]);
      auditMax = Math.max(auditMax, e);
      auditSum += e * e;
      auditCount++;
    } else if (!!(hit && hit.distance <= maxDist) !== distances[i] >= 0) auditMissing++;
  }
  // Match the original nonzero winding rule, including deterministic column nudges.
  const ray = new THREE.Ray(new THREE.Vector3(), new THREE.Vector3(0, 0, 1));
  for (let y = 0; y < n; y++)
    for (let x = 0; x < n; x++) {
      const cx = ((x + 0.5) / n - 0.5) * 2 * sampleBound + voxel * 0.013,
        cy = ((y + 0.5) / n - 0.5) * 2 * sampleBound + voxel * 0.017;
      ray.origin.set(cx, cy, -sampleBound - 1);
      const crossings = (bvh.raycast(ray, THREE.DoubleSide) as any[])
        .map((h) => ({ z: h.distance - sampleBound - 1, delta: h.face.normal.z < 0 ? 1 : -1 }))
        .sort((a, b) => a.z - b.z);
      let k = 0,
        winding = 0;
      for (let z = 0; z < n; z++) {
        const pz = ((z + 0.5) / n - 0.5) * 2 * sampleBound;
        while (k < crossings.length && crossings[k].z < pz) winding += crossings[k++].delta;
        const index = x + y * n + z * n * n;
        let d = distances[index] >= 0 ? distances[index] : range;
        if (winding !== 0) d = -d;
        distances[index] = Math.min(1, Math.max(0, (d + range) / (2 * range)));
      }
    }
  const finished = performance.now();
  rwMetrics.sdf.push({
    n,
    triangles: geo.index!.count / 3,
    packMs: packed - start,
    gpuMs: gpuDone - packed,
    signMs: finished - gpuDone,
    totalMs: finished - start,
    audit: {
      samples: 2048,
      compared: auditCount,
      maxDistanceError: auditMax,
      rmsDistanceError: Math.sqrt(auditSum / Math.max(1, auditCount)),
      missing: auditMissing,
    },
  });
  data.dispose();
  output.value.dispose();
  kernel.dispose();
  return makeLogoSDF(distances, n, bound, thickness, sampleBound);
}
source/src/shape/logo.ts
// `logo` shape: load an SVG, round corners, extrude with a bevel, crease normals, voxelise a signed distance grid
// (three-mesh-bvh) so raycast, sampling and raymarch match the primitives. FROST: loadLogo() is split into
// loadLogoShapes() + extrudeShapes() + voxelize() so headlines share the pipeline and several shapes share one `bound`.
import { unionOutlines } from "./unionOutlines";
import * as THREE from "three/webgpu";
import { SVGLoader } from "three/addons/loaders/SVGLoader.js";
import { mergeVertices, toCreasedNormals } from "three/addons/utils/BufferGeometryUtils.js";
import { MeshBVH } from "three-mesh-bvh";
import type { LogoSDF } from "./sdf";

export interface LogoParams {
  /** overall width of the mark in world units */
  width: number;
  /** extrusion depth as a fraction of the width */
  depth: number;
  bevelThickness: number;
  bevelSize: number;
  bevelOffset: number;
  bevelSegments: number;
  curveSegments: number;
  /** rounding radius applied to the 2D outline corners (fraction of the width, 0 = keep hard corners) */
  cornerRadius: number;
  /** normals are hard above this angle (degrees) and smooth below it */
  creaseAngle: number;
  /** SDF grid resolution */
  sdfRes: number;
}
export const DEFAULT_LOGO_PARAMS: LogoParams = {
  width: 2.6,
  depth: 0.22,
  bevelThickness: 0.06,
  bevelSize: 0.05,
  bevelOffset: 0,
  bevelSegments: 5,
  curveSegments: 24,
  cornerRadius: 0.03,
  creaseAngle: 40,
  sdfRes: 64,
};

/** Round the corners of a closed polygon: cut each sharp corner back by `r` and bridge it with a quadratic curve. */
function roundPolygon(pts: THREE.Vector2[], r: number): THREE.Path {
  // Closed paths repeat their first point. Keeping that duplicate makes both end
  // segments zero-length and drops the first real corner (e.g. the H's left foot).
  pts = pts.filter((p, i) => i === 0 || p.distanceToSquared(pts[i - 1]) > 1e-18);
  if (pts.length > 1 && pts[0].distanceToSquared(pts[pts.length - 1]) < 1e-18)
    pts = pts.slice(0, -1);
  const n = pts.length;
  const path = new THREE.Path();
  if (r <= 0 || n < 3) {
    path.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < n; i++) path.lineTo(pts[i].x, pts[i].y);
    path.closePath();
    return path;
  }
  const segs: { a: THREE.Vector2; c: THREE.Vector2; b: THREE.Vector2; sharp: boolean }[] = [];
  for (let i = 0; i < n; i++) {
    const p = pts[i],
      prev = pts[(i - 1 + n) % n],
      next = pts[(i + 1) % n];
    const d1 = prev.clone().sub(p),
      d2 = next.clone().sub(p);
    const l1 = d1.length(),
      l2 = d2.length();
    if (l1 < 1e-9 || l2 < 1e-9) continue;
    const ang = Math.acos(THREE.MathUtils.clamp(d1.dot(d2) / (l1 * l2), -1, 1));
    const sharp = ang < THREE.MathUtils.degToRad(168);
    const rr = Math.min(r, l1 * 0.45, l2 * 0.45);
    segs.push({
      a: p.clone().addScaledVector(d1.normalize(), sharp ? rr : 0),
      c: p,
      b: p.clone().addScaledVector(d2.normalize(), sharp ? rr : 0),
      sharp,
    });
  }
  segs.forEach((s, i) => {
    if (i === 0) path.moveTo(s.a.x, s.a.y);
    else path.lineTo(s.a.x, s.a.y);
    if (s.sharp) path.quadraticCurveTo(s.c.x, s.c.y, s.b.x, s.b.y);
  });
  path.closePath();
  return path;
}

/** A relative or root-relative path, or an inline SVG data URI: never a scheme or protocol-relative host. */
const isProjectAssetUrl = (url: string) =>
  /^data:image\/svg\+xml[,;]/i.test(url) || !/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i.test(url.trim());

/** The SVG's shapes fitted to `P.width`, centred, y-up (world units). */
export async function loadLogoShapes(
  url: string,
  params: Partial<LogoParams> = {},
): Promise<THREE.Shape[]> {
  const P = { ...DEFAULT_LOGO_PARAMS, ...params };
  if (!isProjectAssetUrl(url)) throw new Error("The logo must be a project asset path, not " + url);
  const text = await fetch(url).then((r) =>
    r.ok ? r.text() : Promise.reject(new Error("no logo")),
  );
  const data = new SVGLoader().parse(text);
  const raw: THREE.Shape[] = [];
  for (const p of data.paths) raw.push(...(p as any).toShapes(true));
  if (!raw.length) return [];
  // svg units -> world units (fit the width), y-up
  const box = new THREE.Box2();
  for (const s of raw) for (const pt of s.getPoints(8)) box.expandByPoint(pt);
  const size = box.getSize(new THREE.Vector2()),
    centre = box.getCenter(new THREE.Vector2());
  const k = P.width / Math.max(size.x, size.y);
  const tx = (v: THREE.Vector2) => new THREE.Vector2((v.x - centre.x) * k, -(v.y - centre.y) * k);
  return raw.map((s) => {
    const shape = new THREE.Shape(s.getPoints(P.curveSegments).map(tx));
    shape.holes = s.holes.map((h) => new THREE.Path(h.getPoints(P.curveSegments).map(tx)));
    return shape;
  });
}

/**
 * Round outline corners (radius `P.cornerRadius * ref`), extrude with a bevel (depth and bevel as fractions of
 * `ref`), merge, crease normals and centre. `ref` is the mark's width for the logo, the font size for a headline.
 */
export function extrudeShapes(
  raw: THREE.Shape[],
  P: LogoParams,
  ref: number,
): THREE.BufferGeometry {
  const r = P.cornerRadius * ref;
  const rounded: THREE.Shape[] = unionOutlines(raw, P.curveSegments).map((s) => {
    const outer = roundPolygon(s.getPoints(P.curveSegments), r);
    const shape = new THREE.Shape(outer.getPoints(P.curveSegments));
    shape.holes = s.holes.map((h) => roundPolygon(h.getPoints(P.curveSegments), r));
    return shape;
  });
  const shapes = unionOutlines(rounded, P.curveSegments);
  const bevel = P.bevelThickness > 0 || P.bevelSize > 0;
  let geo: THREE.BufferGeometry = new THREE.ExtrudeGeometry(shapes, {
    depth: P.depth * ref,
    bevelEnabled: bevel,
    bevelThickness: P.bevelThickness * ref,
    bevelSize: P.bevelSize * ref,
    bevelOffset: P.bevelOffset * ref,
    bevelSegments: Math.max(1, Math.round(P.bevelSegments)),
    curveSegments: Math.max(2, Math.round(P.curveSegments)),
  });
  geo.center();
  geo = mergeVertices(geo, 1e-5);
  geo = toCreasedNormals(geo, THREE.MathUtils.degToRad(P.creaseAngle));
  geo.computeBoundingBox();
  // The broad caps are planes. Averaging adjacent bevel faces into their
  // normals makes Earcut's long triangles show through reflective materials.
  // Keep the end rings tangent to the caps, so the rounded bevel joins smoothly
  // without tilting the entire cap. Positions/topology and the SDF stay identical.
  if (P.bevelThickness > 0 && P.bevelSize > 0) {
    const position = geo.getAttribute("position"),
      normal = geo.getAttribute("normal");
    const { min, max } = geo.boundingBox!;
    const epsilon = Math.max((max.z - min.z) * 1e-6, 1e-8);
    for (let i = 0; i < position.count; i++) {
      const z = position.getZ(i);
      if (Math.abs(z - min.z) <= epsilon) normal.setXYZ(i, 0, 0, -1);
      else if (Math.abs(z - max.z) <= epsilon) normal.setXYZ(i, 0, 0, 1);
    }
    normal.needsUpdate = true;
  }
  return geo;
}

/** Half extents of a centred geometry. */
export function halfExtents(geo: THREE.BufferGeometry) {
  if (!geo.boundingBox) geo.computeBoundingBox();
  const half = new THREE.Vector3();
  geo.boundingBox!.getSize(half).multiplyScalar(0.5);
  return half;
}

/** The texture samples [-sampleBound, sampleBound]^3; bound keeps the shared simulation domain and distance range. */
export function makeLogoSDF(
  data3: Float32Array,
  n: number,
  bound: number,
  thickness: number,
  sampleBound = bound,
): LogoSDF {
  const range = bound;
  // Sampling can be tight per shape while erosion/particles keep a common world domain.
  const sampleDomain = { value: sampleBound };
  const tex = new THREE.Data3DTexture(data3, n, n, n);
  tex.format = THREE.RedFormat;
  tex.type = THREE.FloatType;
  tex.minFilter = tex.magFilter = THREE.LinearFilter;
  tex.wrapS = tex.wrapT = tex.wrapR = THREE.ClampToEdgeWrapping;
  tex.needsUpdate = true;
  const sample = (x: number, y: number, z: number) => {
    const bound = sampleDomain.value;
    const fx = (x / (2 * bound) + 0.5) * n - 0.5,
      fy = (y / (2 * bound) + 0.5) * n - 0.5,
      fz = (z / (2 * bound) + 0.5) * n - 0.5;
    const ix = Math.max(0, Math.min(n - 2, Math.floor(fx))),
      iy = Math.max(0, Math.min(n - 2, Math.floor(fy))),
      iz = Math.max(0, Math.min(n - 2, Math.floor(fz)));
    const tx2 = Math.max(0, Math.min(1, fx - ix)),
      ty = Math.max(0, Math.min(1, fy - iy)),
      tz = Math.max(0, Math.min(1, fz - iz));
    const v = (a: number, b: number, c: number) => data3[a + b * n + c * n * n];
    const c00 = v(ix, iy, iz) * (1 - tx2) + v(ix + 1, iy, iz) * tx2,
      c10 = v(ix, iy + 1, iz) * (1 - tx2) + v(ix + 1, iy + 1, iz) * tx2;
    const c01 = v(ix, iy, iz + 1) * (1 - tx2) + v(ix + 1, iy, iz + 1) * tx2,
      c11 = v(ix, iy + 1, iz + 1) * (1 - tx2) + v(ix + 1, iy + 1, iz + 1) * tx2;
    const c0 = c00 * (1 - ty) + c10 * ty,
      c1 = c01 * (1 - ty) + c11 * ty;
    const outside = Math.abs(x) > bound || Math.abs(y) > bound || Math.abs(z) > bound;
    const distance = (c0 * (1 - tz) + c1 * tz) * 2 * range - range;
    return outside
      ? Math.max(
          distance,
          Math.hypot(
            Math.max(Math.abs(x) - bound, 0),
            Math.max(Math.abs(y) - bound, 0),
            Math.max(Math.abs(z) - bound, 0),
          ),
        )
      : distance;
  };
  return { texture: tex, res: n, bound, range, thickness, data: data3, sample, sampleDomain };
}

/**
 * Voxel SDF over [-bound, bound]^3 at `res` per axis: BVH closest-point distance, signed by z-ray parity.
 * Face normals give wrong signs near edges and bevels, so those voxels read solid and never erode.
 */
export function voxelize(
  geo: THREE.BufferGeometry,
  bound: number,
  res: number,
  sampleBound = bound,
): LogoSDF {
  const half = halfExtents(geo);
  const thickness = Math.min(half.x, half.y, half.z);
  const bvh = new MeshBVH(geo);
  const n = Math.max(16, Math.round(res)),
    data3 = new Float32Array(n * n * n);
  const p = new THREE.Vector3();
  const range = bound;
  const voxel = (2 * sampleBound) / n;
  const maxDist = Math.min(range, Math.max(thickness * 2.5, voxel * 14));
  const box = geo.boundingBox!;
  const maxDistSquared = maxDist * maxDist;
  const hit: any = {};
  const ray = new THREE.Ray(new THREE.Vector3(), new THREE.Vector3(0, 0, 1));
  const inside = new Uint8Array(n);
  for (let y = 0; y < n; y++)
    for (let x = 0; x < n; x++) {
      // one ray per column, nudged off the grid so it never grazes an edge exactly
      const cx = ((x + 0.5) / n - 0.5) * 2 * sampleBound + voxel * 0.013,
        cy = ((y + 0.5) / n - 0.5) * 2 * sampleBound + voxel * 0.017;
      ray.origin.set(cx, cy, -sampleBound - 1);
      const hits = bvh.raycast(ray, THREE.DoubleSide) as { distance: number }[];
      const crossings = (hits as any[])
        .map((h) => ({ z: h.distance - sampleBound - 1, delta: h.face.normal.z < 0 ? 1 : -1 }))
        .sort((a, b) => a.z - b.z);
      let k = 0,
        winding = 0;
      for (let z = 0; z < n; z++) {
        const pz = ((z + 0.5) / n - 0.5) * 2 * sampleBound;
        while (k < crossings.length && crossings[k].z < pz) winding += crossings[k++].delta;
        inside[z] = Number(winding !== 0);
      }
      for (let z = 0; z < n; z++) {
        p.set(
          ((x + 0.5) / n - 0.5) * 2 * sampleBound,
          ((y + 0.5) / n - 0.5) * 2 * sampleBound,
          ((z + 0.5) / n - 0.5) * 2 * sampleBound,
        );
        // A thin extrusion occupies little of the cube. The box distance is a
        // lower bound: skip BVH work only when the capped query cannot find a hit.
        const dx = Math.max(box.min.x - p.x, 0, p.x - box.max.x);
        const dy = Math.max(box.min.y - p.y, 0, p.y - box.max.y);
        const dz = Math.max(box.min.z - p.z, 0, p.z - box.max.z);
        const res2 =
          dx * dx + dy * dy + dz * dz > maxDistSquared
            ? null
            : bvh.closestPointToPoint(p, hit, 0, maxDist);
        let d = res2 ? res2.distance : range;
        if (inside[z]) d = -d;
        data3[x + y * n + z * n * n] = Math.min(1, Math.max(0, (d + range) / (2 * range)));
      }
    }
  return makeLogoSDF(data3, n, bound, thickness, sampleBound);
}

/** The experiment's original entry point: SVG -> extruded geometry + its own SDF. */
export async function loadLogo(
  url: string,
  params: Partial<LogoParams> = {},
): Promise<{ sdf: LogoSDF; geometry: THREE.BufferGeometry } | undefined> {
  const P = { ...DEFAULT_LOGO_PARAMS, ...params };
  const shapes = await loadLogoShapes(url, P);
  if (!shapes.length) return undefined;
  const geo = extrudeShapes(shapes, P, P.width);
  const half = halfExtents(geo);
  const bound = Math.max(half.x, half.y, half.z) * 1.15;
  return { sdf: voxelize(geo, bound, P.sdfRes), geometry: geo };
}
source/src/shape/logoRefine.ts
import type * as THREE from "three/webgpu";
import preset from "../../presets/approved-material.json";
export const LOGO_MESH_DETAIL_DEFAULT = preset.logoMeshDetail;
export function resolveLogoMeshDetail(value?: number) {
  return typeof value === "number" && Number.isFinite(value)
    ? Math.max(1, Math.min(4, Math.round(value)))
    : LOGO_MESH_DETAIL_DEFAULT;
}
/** Use a physical surface target, independent of contour sampling and tiny noise sizes.
 * Long caps and side faces share the same threshold; already short bevel edges stay put. */
export function logoRefinementSettings(source: THREE.BufferGeometry, detail: number) {
  source.computeBoundingBox();
  const b = source.boundingBox!;
  const width = Math.max(b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z, 0.001),
    level = resolveLogoMeshDetail(detail);
  return { maxEdge: width / (32 * level), extraTriangles: 160_000 * level };
}
source/src/shape/sdf.ts
// Object-space signed distance fields, on the CPU (raycast + interior sampling) and in TSL (shaders).
// The shape sits centered at the origin; `size` = 1 gives a torus of major radius 1.
import * as THREE from "three/webgpu";
import { tsl } from "../tsl/t";
const { Fn, vec2, vec3, float, length, max, min, abs, dot, sqrt, clamp, sign, select, texture3D } =
  tsl;
import type { ShapeName } from "../dials/defaults";

type N = any;

export interface ShapeSpec {
  name: ShapeName;
  size: number;
  tubeRatio: number;
  /** Half extent of the bounding cube used for the erosion field (object space). */
  bound: number;
  /** CPU signed distance. */
  sdf: (x: number, y: number, z: number) => number;
  /** TSL signed distance for a vec3 node. */
  sdfNode: (p: N) => N;
  /** Approximate tube half-thickness (used for the surface-bias sampling shell). */
  thickness: number;
  /** Object-space voxel spacing; follows the active shape during retargeting. */
  voxelSize?: N;
}

const ICOSA_NORMALS: [number, number, number][] = (() => {
  const phi = (1 + Math.sqrt(5)) / 2;
  const n: [number, number, number][] = [];
  const push = (x: number, y: number, z: number) => {
    const l = Math.hypot(x, y, z);
    n.push([x / l, y / l, z / l]);
  };
  // 20 face normals of a regular icosahedron = vertices of a dodecahedron
  for (const sx of [-1, 1]) for (const sy of [-1, 1]) for (const sz of [-1, 1]) push(sx, sy, sz);
  for (const s1 of [-1, 1])
    for (const s2 of [-1, 1]) {
      push(0, s1 / phi, s2 * phi);
      push(s1 / phi, s2 * phi, 0);
      push(s1 * phi, 0, s2 / phi);
    }
  return n;
})();

const CUBE_POS: [number, number, number][] = [];
const RECT_POS: [number, number, number][] = [];

export function makeShape(
  name: ShapeName,
  size: number,
  tubeRatio: number,
  logoSDF?: LogoSDF,
): ShapeSpec {
  const R = size,
    r = size * tubeRatio;
  switch (name) {
    case "torus": {
      return {
        name,
        size,
        tubeRatio,
        bound: (R + r) * 1.12,
        thickness: r,
        sdf: (x, y, z) => {
          const qx = Math.hypot(x, y) - R;
          return Math.hypot(qx, z) - r;
        },
        sdfNode: (p) => length(vec2(length(p.xy).sub(R), p.z)).sub(r),
      };
    }
    case "sphere": {
      const rad = size * 1.15;
      return {
        name,
        size,
        tubeRatio,
        bound: rad * 1.15,
        thickness: rad,
        sdf: (x, y, z) => Math.hypot(x, y, z) - rad,
        sdfNode: (p) => length(p).sub(rad),
      };
    }
    case "roundedBox": {
      const h = size * 0.92,
        rr = size * 0.22;
      return {
        name,
        size,
        tubeRatio,
        bound: (h + rr) * 1.12,
        thickness: h,
        sdf: (x, y, z) => {
          const qx = Math.abs(x) - h + rr,
            qy = Math.abs(y) - h + rr,
            qz = Math.abs(z) - h * 0.55 + rr;
          const ox = Math.max(qx, 0),
            oy = Math.max(qy, 0),
            oz = Math.max(qz, 0);
          return Math.hypot(ox, oy, oz) + Math.min(Math.max(qx, qy, qz), 0) - rr;
        },
        sdfNode: (p) => {
          const q = abs(p).sub(vec3(h - rr, h - rr, h * 0.55 - rr));
          return length(max(q, 0))
            .add(min(max(q.x, max(q.y, q.z)), 0))
            .sub(rr);
        },
      };
    }
    case "pyramid": {
      // square pyramid, apex up (+y), base half-width b, height hgt, with a rounding radius
      const b = size * 1.05,
        hgt = size * 1.5,
        rr = size * 0.06;
      const ny = b / Math.hypot(b, hgt),
        nx = hgt / Math.hypot(b, hgt);
      const yOff = -hgt * 0.4;
      return {
        name,
        size,
        tubeRatio,
        bound: Math.max(b, hgt) * 1.25,
        thickness: b * 0.6,
        sdf: (x, y, z) => {
          y -= yOff;
          const ax = Math.abs(x),
            az = Math.abs(z);
          const dSide = Math.max(ax * nx + y * ny - hgt * ny, az * nx + y * ny - hgt * ny);
          const dBase = -y;
          return Math.max(dSide, dBase) - rr;
        },
        sdfNode: (p) => {
          const pp = vec3(p.x, p.y.sub(yOff), p.z);
          const ax = abs(pp.x),
            az = abs(pp.z);
          const dSide = max(
            ax
              .mul(nx)
              .add(pp.y.mul(ny))
              .sub(hgt * ny),
            az
              .mul(nx)
              .add(pp.y.mul(ny))
              .sub(hgt * ny),
          );
          return max(dSide, pp.y.negate()).sub(rr);
        },
      };
    }
    case "icosahedron": {
      const h = size * 1.05,
        rr = size * 0.05;
      return {
        name,
        size,
        tubeRatio,
        bound: h * 1.3,
        thickness: h * 0.8,
        sdf: (x, y, z) => {
          let d = -Infinity;
          for (const n of ICOSA_NORMALS) d = Math.max(d, x * n[0] + y * n[1] + z * n[2]);
          return d - h - rr;
        },
        sdfNode: (p) => {
          let d: N = float(-100);
          for (const n of ICOSA_NORMALS) d = max(d, dot(p, vec3(n[0], n[1], n[2])));
          return d.sub(h + rr);
        },
      };
    }
    case "logo": {
      // `size` scales the loaded mark uniformly (geometry, bound and distance field alike)
      const ls = logoSDF!,
        k = size;
      const sampleBoundNode = (ls.sampleBoundNode ??= tsl.uniform(ls.sampleDomain.value));
      return {
        name,
        size,
        tubeRatio,
        bound: ls.bound * k,
        thickness: ls.thickness * k,
        voxelSize: sampleBoundNode.mul((2 * k) / ls.res),
        sdf: (x, y, z) => ls.sample(x / k, y / k, z / k) * k,
        sdfNode: (p) => {
          const sampleBound = sampleBoundNode.mul(k);
          const uvw = p.div(sampleBound.mul(2)).add(0.5);
          const distance = texture3D(ls.texture, uvw)
            .r.mul(ls.range * 2)
            .sub(ls.range)
            .mul(k);
          const outside = max(abs(p).sub(sampleBound), 0);
          return select(
            max(outside.x, max(outside.y, outside.z)).greaterThan(0),
            max(distance, length(outside)),
            distance,
          );
        },
      };
    }
  }
}

/** Voxelised SDF for arbitrary meshes (logo). Values stored normalised: (d + range) / (2 range). */
export interface LogoSDF {
  texture: THREE.Data3DTexture;
  res: number;
  bound: number;
  range: number;
  thickness: number;
  data: Float32Array;
  sample: (x: number, y: number, z: number) => number;
  sampleDomain: { value: number };
  sampleBoundNode?: N;
}

/** Central-difference normal from the CPU sdf. */
export function sdfNormal(
  s: ShapeSpec,
  x: number,
  y: number,
  z: number,
  e = 1e-3,
): [number, number, number] {
  const nx = s.sdf(x + e, y, z) - s.sdf(x - e, y, z);
  const ny = s.sdf(x, y + e, z) - s.sdf(x, y - e, z);
  const nz = s.sdf(x, y, z + e) - s.sdf(x, y, z - e);
  const l = Math.hypot(nx, ny, nz) || 1;
  return [nx / l, ny / l, nz / l];
}

/** TSL gradient normal from the shape sdf. */
export const sdfNormalNode = (s: ShapeSpec, p: N, e = 0.002) => {
  const ex = vec3(e, 0, 0),
    ey = vec3(0, e, 0),
    ez = vec3(0, 0, e);
  const n = vec3(
    s.sdfNode(p.add(ex)).sub(s.sdfNode(p.sub(ex))),
    s.sdfNode(p.add(ey)).sub(s.sdfNode(p.sub(ey))),
    s.sdfNode(p.add(ez)).sub(s.sdfNode(p.sub(ez))),
  );
  return n.div(max(length(n), 1e-6));
};

export interface RayHit {
  entry: THREE.Vector3;
  exit: THREE.Vector3;
  tEntry: number;
  tExit: number;
}

/**
 * Sphere-trace the object-space sdf. Returns entry and exit points along the ray, or null.
 * `origin`/`dir` in object space; dir normalised.
 */
export function raycastSDF(
  s: ShapeSpec,
  origin: THREE.Vector3,
  dir: THREE.Vector3,
  maxDist = 60,
  target?: RayHit,
): RayHit | null {
  let t = 0;
  let px = origin.x,
    py = origin.y,
    pz = origin.z;
  // enter
  let entered = false;
  for (let i = 0; i < 160; i++) {
    px = dir.x * t + origin.x;
    py = dir.y * t + origin.y;
    pz = dir.z * t + origin.z;
    const d = s.sdf(px, py, pz);
    if (d < 0.0015) {
      entered = true;
      break;
    }
    t += Math.max(d, 0.002);
    if (t > maxDist) break;
  }
  if (!entered) return null;
  const tEntry = t;
  const hit = target ?? {
    entry: new THREE.Vector3(),
    exit: new THREE.Vector3(),
    tEntry: 0,
    tExit: 0,
  };
  hit.entry.set(px, py, pz);
  // exit: march inside using the interior distance
  let tt = t + 0.004;
  for (let i = 0; i < 200; i++) {
    px = dir.x * tt + origin.x;
    py = dir.y * tt + origin.y;
    pz = dir.z * tt + origin.z;
    const d = s.sdf(px, py, pz);
    if (d > 0.0015) break;
    tt += Math.max(-d, 0.004);
    if (tt - t > s.bound * 4) break;
  }
  hit.exit.set(dir.x * tt + origin.x, dir.y * tt + origin.y, dir.z * tt + origin.z);
  hit.tEntry = tEntry;
  hit.tExit = tt;
  return hit;
}

export interface InteriorSamples {
  positions: Float32Array; // xyz per point
  depths: Float32Array; // 0 at the surface .. 1 deep inside (relative to thickness)
  count: number;
}

/** Rejection-sample the shape interior; `surfaceBias` of the points land in the outer `shell` fraction of thickness. */
export function sampleInterior(
  s: ShapeSpec,
  count: number,
  surfaceBias: number,
  shell: number,
  rand: () => number,
  box?: [number, number, number],
): InteriorSamples {
  const positions = new Float32Array(count * 3);
  const depths = new Float32Array(count);
  // FROST: optional half extents of the shape itself (a thin headline fills ~1% of the bound cube, so sampling
  // the cube starves the guard below and leaves most points at the origin)
  const BX = box ? box[0] : s.bound,
    BY = box ? box[1] : s.bound,
    BZ = box ? box[2] : s.bound;
  const B = s.bound;
  const shellDepth = s.thickness * shell;
  const nSurf = Math.round(count * surfaceBias);
  let i = 0;
  let guard = 0;
  while (i < count && guard < count * 400) {
    guard++;
    const x = (rand() * 2 - 1) * BX,
      y = (rand() * 2 - 1) * BY,
      z = (rand() * 2 - 1) * BZ;
    const d = s.sdf(x, y, z);
    if (d >= 0) continue;
    const depth = -d;
    const wantSurface = i < nSurf;
    // Thin glyphs may have no deep core. Relax the bias after the bounded first
    // pass; never silently return zero-filled homes at the origin.
    if (guard < count * 60) {
      if (wantSurface && depth > shellDepth) continue;
      if (!wantSurface && depth <= shellDepth) continue;
    }
    positions[i * 3] = x;
    positions[i * 3 + 1] = y;
    positions[i * 3 + 2] = z;
    depths[i] = Math.min(1, depth / s.thickness);
    i++;
  }
  if (i !== count)
    throw new Error(`Frost: sampled only ${i}/${count} interior homes; check shape bounds and SDF`);
  return { positions, depths, count: i };
}

void CUBE_POS;
void RECT_POS;
void sqrt;
void clamp;
void sign;
void select;
void Fn;
source/src/shape/text.ts
// Headline outlines as THREE.Shape[] in world units, from a TTF via opentype.js (kerning applied); same
// extrusion / bevel / rounding / SDF voxelisation as the SVG mark (shape/logo.ts extrudeShapes + voxelize).
import * as THREE from "three/webgpu";
import { SVGLoader } from "three/addons/loaders/SVGLoader.js";
import opentype from "opentype.js";

export type Typeface = opentype.Font;

const fonts = new Map<string, Promise<Typeface>>();
export function loadTypeface(url: string): Promise<Typeface> {
  let p = fonts.get(url);
  if (!p) {
    p = fetch(url)
      .then((r) => {
        if (!r.ok) throw new Error(`typeface: ${r.status}`);
        return r.arrayBuffer();
      })
      .then((buf) => opentype.parse(buf));
    fonts.set(url, p);
  }
  return p;
}

/** Advance width of one line in world units for font size 1 (one em = 1). */
export function lineWidth(font: Typeface, line: string, letterSpacing = 0) {
  return font.getAdvanceWidth(line, 1, { kerning: true, letterSpacing } as any);
}

/**
 * `lines` set at `size` (em in world units), every line centred, stacked `lineHeight` ems apart, the block
 * centred on the origin (cap height centred on y = 0 for a single line). Returns shapes with holes resolved.
 */
export function textShapes(
  font: Typeface,
  lines: string[],
  size: number,
  lineHeight: number,
  letterSpacing = 0,
): THREE.Shape[] {
  const scale = size / font.unitsPerEm;
  const capHeight = ((font.tables as any)?.os2?.sCapHeight ?? font.ascender * 0.72) * scale;
  const step = lineHeight * size;
  const blockH = step * (lines.length - 1) + capHeight;
  const shapes: THREE.Shape[] = [];
  lines.forEach((line, i) => {
    const w = font.getAdvanceWidth(line, size, { kerning: true, letterSpacing } as any);
    const baseline = blockH / 2 - capHeight - i * step;
    const path = font.getPath(line, -w / 2, 0, size, { kerning: true, letterSpacing } as any);
    const sp = new THREE.ShapePath();
    // opentype paths are y-down: flip into the y-up object space
    const Y = (y: number) => baseline - y;
    for (const c of path.commands) {
      if (c.type === "M") sp.moveTo(c.x, Y(c.y));
      else if (c.type === "L") sp.lineTo(c.x, Y(c.y));
      else if (c.type === "Q") sp.quadraticCurveTo(c.x1, Y(c.y1), c.x, Y(c.y));
      else if (c.type === "C") sp.bezierCurveTo(c.x1, Y(c.y1), c.x2, Y(c.y2), c.x, Y(c.y));
      else if (c.type === "Z") sp.currentPath?.closePath();
    }
    shapes.push(...SVGLoader.createShapes(sp as any));
  });
  return shapes;
}
source/src/shape/textRefine.ts
/** Text-only mesh density. Priority edge bisection spends a strict budget on the longest
 * remaining edges; every incident face splits together, including cap/bevel/side seams. */
import preset from "../../presets/approved-material.json";
export const TEXT_MESH_DETAIL_DEFAULT = preset.textMeshDetail;
export function resolveTextMeshDetail(value?: number) {
  return typeof value === "number" && Number.isFinite(value)
    ? Math.max(1, Math.min(4, Math.round(value)))
    : TEXT_MESH_DETAIL_DEFAULT;
}
export function textRefinementSettings(scale: number, detail: number) {
  const level = resolveTextMeshDetail(detail);
  return { maxEdge: scale / (2 * level), extraTriangles: 160_000 * level };
}
type Edge = { a: number; b: number; length2: number; faces: Set<number> };
/** Exported for measurement before displacement; positions gain shared midpoint vertices. */
export function refineText(
  positions: number[],
  initial: number[],
  maxEdge: number,
  extraTriangles: number,
) {
  const triangles: (number[] | null)[] = [],
    edges = new Map<string, Edge>(),
    heap: Edge[] = [];
  const max2 = maxEdge * maxEdge,
    limit = initial.length / 3 + extraTriangles;
  let count = initial.length / 3;
  const key = (a: number, b: number) => (a < b ? `${a}:${b}` : `${b}:${a}`);
  const better = (a: Edge, b: Edge) =>
    a.length2 > b.length2 || (a.length2 === b.length2 && (a.a < b.a || (a.a === b.a && a.b < b.b)));
  function push(edge: Edge) {
    let i = heap.length;
    heap.push(edge);
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (!better(edge, heap[parent])) break;
      heap[i] = heap[parent];
      i = parent;
    }
    heap[i] = edge;
  }
  function pop() {
    const best = heap[0],
      last = heap.pop()!;
    if (heap.length) {
      let i = 0;
      while (i * 2 + 1 < heap.length) {
        let child = i * 2 + 1;
        if (child + 1 < heap.length && better(heap[child + 1], heap[child])) child++;
        if (!better(heap[child], last)) break;
        heap[i] = heap[child];
        i = child;
      }
      heap[i] = last;
    }
    return best;
  }
  function add(a: number, b: number, c: number) {
    const id = triangles.length;
    triangles.push([a, b, c]);
    for (const [u, v] of [
      [a, b],
      [b, c],
      [c, a],
    ]) {
      const k = key(u, v);
      let edge = edges.get(k);
      if (!edge) {
        const dx = positions[u * 3] - positions[v * 3],
          dy = positions[u * 3 + 1] - positions[v * 3 + 1],
          dz = positions[u * 3 + 2] - positions[v * 3 + 2];
        edge = {
          a: Math.min(u, v),
          b: Math.max(u, v),
          length2: dx * dx + dy * dy + dz * dz,
          faces: new Set(),
        };
        edges.set(k, edge);
        if (edge.length2 > max2) push(edge);
      }
      edge.faces.add(id);
    }
  }
  function remove(id: number) {
    const t = triangles[id]!;
    triangles[id] = null;
    for (let j = 0; j < 3; j++) {
      const k = key(t[j], t[(j + 1) % 3]),
        e = edges.get(k)!;
      e.faces.delete(id);
      if (!e.faces.size) edges.delete(k);
    }
  }
  for (let i = 0; i < initial.length; i += 3) add(initial[i], initial[i + 1], initial[i + 2]);
  while (heap.length && count < limit) {
    const edge = pop();
    if (edges.get(key(edge.a, edge.b)) !== edge) continue;
    const ids = [...edge.faces];
    if (count + ids.length > limit) continue;
    const m = positions.length / 3;
    positions.push(
      (positions[edge.a * 3] + positions[edge.b * 3]) / 2,
      (positions[edge.a * 3 + 1] + positions[edge.b * 3 + 1]) / 2,
      (positions[edge.a * 3 + 2] + positions[edge.b * 3 + 2]) / 2,
    );
    const faces = ids.map((id) => triangles[id]!);
    for (const id of ids) remove(id);
    for (const t of faces) {
      // Retain original winding, regardless of which side sees a->b versus b->a.
      const j = t.findIndex((v, j) => key(v, t[(j + 1) % 3]) === key(edge.a, edge.b));
      const a = t[j],
        b = t[(j + 1) % 3],
        c = t[(j + 2) % 3];
      add(a, m, c);
      add(m, b, c);
    }
    count += ids.length;
  }
  const out: number[] = [];
  for (const t of triangles) if (t) out.push(t[0], t[1], t[2]);
  return out;
}

/** Repair subpixel extrusion seam duplicates before subdividing them. Only boundary
 * vertices are eligible; real glyph contours/counters and the SVG path are untouched. */
export function repairTextSeams(positions: number[], indices: number[]) {
  const clean: number[] = [];
  for (let i = 0; i < indices.length; i += 3) {
    const [a, b, c] = indices.slice(i, i + 3);
    if (a !== b && b !== c && c !== a) clean.push(a, b, c);
  }
  const edges = new Map<string, { a: number; b: number; count: number }>();
  for (let i = 0; i < clean.length; i += 3)
    for (const [a, b] of [
      [clean[i], clean[i + 1]],
      [clean[i + 1], clean[i + 2]],
      [clean[i + 2], clean[i]],
    ]) {
      const key = a < b ? `${a}:${b}` : `${b}:${a}`,
        edge = edges.get(key);
      if (edge) edge.count++;
      else edges.set(key, { a, b, count: 1 });
    }
  const boundary = new Set<number>();
  for (const e of edges.values())
    if (e.count === 1) {
      boundary.add(e.a);
      boundary.add(e.b);
    }
  const eps = 1e-5,
    grid = new Map<string, number[]>(),
    remap = new Map<number, number>();
  for (const v of [...boundary].sort((a, b) => a - b)) {
    const x = positions[v * 3],
      y = positions[v * 3 + 1],
      z = positions[v * 3 + 2],
      cx = Math.floor(x / eps),
      cy = Math.floor(y / eps),
      cz = Math.floor(z / eps);
    let representative = v;
    for (let dx = -1; dx <= 1; dx++)
      for (let dy = -1; dy <= 1; dy++)
        for (let dz = -1; dz <= 1; dz++)
          for (const other of grid.get(`${cx + dx}:${cy + dy}:${cz + dz}`) || []) {
            if (
              Math.hypot(
                x - positions[other * 3],
                y - positions[other * 3 + 1],
                z - positions[other * 3 + 2],
              ) <= eps
            )
              representative = Math.min(representative, other);
          }
    remap.set(v, representative);
    if (representative === v) {
      const key = `${cx}:${cy}:${cz}`,
        bucket = grid.get(key) || [];
      bucket.push(v);
      grid.set(key, bucket);
    }
  }
  const out: number[] = [];
  for (let i = 0; i < clean.length; i += 3) {
    const [a, b, c] = clean.slice(i, i + 3).map((v) => remap.get(v) ?? v);
    if (a !== b && b !== c && c !== a) out.push(a, b, c);
  }
  return out;
}
source/src/shape/unionOutlines.ts
// Resolve all overlapping filled regions before extrusion: one boundary per solid.
import * as THREE from "three/webgpu";
import Clipper from "../vendor/clipper.cjs";
const C: any = Clipper,
  SCALE = 1e6;
export function unionOutlines(shapes: THREE.Shape[], segments = 24): THREE.Shape[] {
  const paths: any[] = [];
  for (const shape of shapes) {
    for (const [i, ring] of [shape, ...shape.holes].entries()) {
      const points = ring
        .getPoints(segments)
        .map((p) => ({ X: Math.round(p.x * SCALE), Y: Math.round(p.y * SCALE) }));
      if (points.length < 3) continue;
      // Nonzero filling adds solids and subtracts holes; overlapping solids do not XOR.
      if (C.Clipper.Orientation(points) !== (i === 0)) points.reverse();
      paths.push(points);
    }
  }
  const clipper = new C.Clipper();
  clipper.StrictlySimple = true;
  clipper.AddPaths(paths, C.PolyType.ptSubject, true);
  const tree = new C.PolyTree();
  clipper.Execute(C.ClipType.ctUnion, tree, C.PolyFillType.pftNonZero, C.PolyFillType.pftNonZero);
  const result: THREE.Shape[] = [];
  const pts = (node: any) =>
    node.Contour().map((p: any) => new THREE.Vector2(p.X / SCALE, p.Y / SCALE));
  const walk = (node: any) => {
    for (const child of node.Childs()) {
      if (!child.IsHole()) {
        const shape = new THREE.Shape(pts(child));
        shape.holes = child
          .Childs()
          .filter((h: any) => h.IsHole())
          .map((h: any) => new THREE.Path(pts(h)));
        result.push(shape);
      }
      walk(child);
    }
  };
  walk(tree);
  return result;
}
source/src/tsl/noise.ts
// Procedural noise library in TSL. Everything is deterministic from the `seed` node passed in.
import { tsl } from "./t";
const {
  Fn,
  vec3,
  vec4,
  float,
  floor,
  fract,
  dot,
  mix,
  abs,
  max,
  min,
  sqrt,
  normalize,
  cross,
  sin,
  cos,
  int,
  If,
  Loop,
  mx_noise_float,
  mx_noise_vec3,
  smoothstep,
  select,
} = tsl;

type N = any;

/** vec3 -> float hash in [0,1). */
export const hash31 = Fn(([p]: [N]) => {
  const q = fract(vec3(p).mul(vec3(0.1031, 0.103, 0.0973))).toVar();
  q.addAssign(dot(q, q.yxz.add(33.33)));
  return fract(q.x.add(q.y).mul(q.z));
});

/** vec3 -> vec3 hash in [0,1)^3. */
export const hash33 = Fn(([p]: [N]) => {
  const q = fract(vec3(p).mul(vec3(0.1031, 0.103, 0.0973))).toVar();
  q.addAssign(dot(q, q.yxz.add(33.33)));
  return fract(q.xxy.add(q.yxx).mul(q.zyx));
});

/** float -> float hash. */
export const hash11 = Fn(([x]: [N]) => {
  const q = fract(float(x).mul(0.1031)).toVar();
  q.mulAssign(q.add(33.33));
  q.mulAssign(q.add(q));
  return fract(q);
});

/**
 * Decorrelated per-particle random in [0,1) from a seed in [0,1) and a small integer channel `k`
 * (hash11 on small inputs is smooth in its argument, which correlated grain rotations with the seed).
 */
export const hashSeed = (seed: N, k: number) =>
  hash31(
    vec3(
      float(seed)
        .mul(1024.7)
        .add(k * 3.1),
      float(seed)
        .mul(2047.3)
        .add(k * 7.7 + 1.3),
      float(seed)
        .mul(511.1)
        .add(k * 13.9 + 2.9),
    ),
  );

/** Perlin gradient noise, ~[-1,1]. */
export const gnoise = (p: N) => mx_noise_float(p);

/**
 * vec3 gradient noise built from three float Perlin evaluations. (three's mx_noise_vec3 is avoided:
 * its WGSL stalls SwiftShader's compiler in fragment shaders, which is our only headless check.)
 */
export const vnoise3 = (p: N) =>
  vec3(
    mx_noise_float(p),
    mx_noise_float(vec3(p).yzx.add(vec3(17.1, 9.7, 3.3))),
    mx_noise_float(vec3(p).zxy.add(vec3(31.7, 5.9, 21.3))),
  );

/** Fractal Brownian motion, unrolled `octaves` times. Returns roughly [-1,1]. */
export function fbm(p: N, octaves: number, lacunarity = 2.0, gain = 0.5): N {
  let sum: N = float(0);
  let amp = 1;
  let norm = 0;
  let q: N = vec3(p);
  for (let i = 0; i < octaves; i++) {
    sum = sum.add(mx_noise_float(q).mul(amp));
    norm += amp;
    amp *= gain;
    q = q.mul(lacunarity).add(vec3(17.3, 9.1, 31.7));
  }
  return sum.div(norm);
}

/** Ridged fbm in [0,1] (sharp creases) — used for frost crystal texture. */
export function ridged(p: N, octaves: number): N {
  let sum: N = float(0);
  let amp = 1;
  let norm = 0;
  let q: N = vec3(p);
  for (let i = 0; i < octaves; i++) {
    const n = abs(mx_noise_float(q)).oneMinus();
    sum = sum.add(n.mul(n).mul(amp));
    norm += amp;
    amp *= 0.5;
    q = q.mul(2.1).add(vec3(5.2, 1.3, 8.7));
  }
  return sum.div(norm);
}

/** Divergence-free curl noise from a vec3 potential (finite differences). */
export const curlNoise = Fn(([p]: [N]) => {
  const e = float(0.02);
  const dx = vec3(e, 0, 0),
    dy = vec3(0, e, 0),
    dz = vec3(0, 0, e);
  const px0 = vnoise3(p.sub(dx)),
    px1 = vnoise3(p.add(dx));
  const py0 = vnoise3(p.sub(dy)),
    py1 = vnoise3(p.add(dy));
  const pz0 = vnoise3(p.sub(dz)),
    pz1 = vnoise3(p.add(dz));
  const x = py1.z.sub(py0.z).sub(pz1.y.sub(pz0.y));
  const y = pz1.x.sub(pz0.x).sub(px1.z.sub(px0.z));
  const z = px1.y.sub(px0.y).sub(py1.x.sub(py0.x));
  return vec3(x, y, z).div(e.mul(2));
});

/**
 * 3D Voronoi over the 27-cell neighbourhood.
 * Returns vec4( nearest feature point (3), sqrt F1 ) — feature point acts as the cell id.
 */
export const voronoiCell = Fn(([p, seed]: [N, N]) => {
  const ip = floor(p).toVar();
  const fp = fract(p).toVar();
  const f1 = float(8).toVar();
  const center = vec3(0).toVar();
  const rng = { start: int(-1), end: int(1), condition: "<=" };
  Loop(rng, rng, rng, ({ i, j, k }: any) => {
    const g = vec3(float(i), float(j), float(k));
    const cell = ip.add(g);
    const o = hash33(cell.add(seed));
    const r = g.add(o).sub(fp);
    const d = dot(r, r);
    If(d.lessThan(f1), () => {
      f1.assign(d);
      center.assign(cell.add(o));
    });
  });
  return vec4(center, sqrt(f1));
});

/**
 * Cheaper Voronoi: feature points on a jittered lattice, only the 2x2x2 cells around p are searched.
 * ~3x cheaper than the 27-cell version; the nearest point is occasionally missed near cell corners.
 */
export const voronoiCell8 = Fn(([p, seed]: [N, N]) => {
  const ip = floor(vec3(p).sub(0.5)).toVar();
  const f1 = float(8).toVar();
  const center = vec3(0).toVar();
  const rng = { start: int(0), end: int(1), condition: "<=" };
  Loop(rng, rng, rng, ({ i, j, k }: any) => {
    const cell = ip.add(vec3(float(i), float(j), float(k)));
    const fp = cell.add(hash33(cell.add(seed)));
    const r = fp.sub(p);
    const d = dot(r, r);
    If(d.lessThan(f1), () => {
      f1.assign(d);
      center.assign(fp);
    });
  });
  return vec4(center, sqrt(f1));
});

/**
 * Distance to the nearest Voronoi cell boundary (true plane distance, IQ-style second pass); boundaries whose
 * pair-hash exceeds `coverage` are culled so cells are not fully enclosed. Returns vec4(boundary normal (3), distance).
 */
export const voronoiEdge = Fn(([p, seed, coverage]: [N, N, N]) => {
  const ip = floor(p).toVar();
  const fp = fract(p).toVar();
  const md = float(8).toVar();
  const mr = vec3(0).toVar();
  const mc = vec3(0).toVar();
  const rng1 = { start: int(-1), end: int(1), condition: "<=" };
  Loop(rng1, rng1, rng1, ({ i, j, k }: any) => {
    const g = vec3(float(i), float(j), float(k));
    const cell = ip.add(g);
    const o = hash33(cell.add(seed));
    const r = g.add(o).sub(fp);
    const d = dot(r, r);
    If(d.lessThan(md), () => {
      md.assign(d);
      mr.assign(r);
      mc.assign(cell);
    });
  });
  md.assign(8);
  const mn = vec3(0, 0, 1).toVar();
  const rng2 = { start: int(-2), end: int(2), condition: "<=" };
  Loop(rng2, rng2, rng2, ({ i, j, k }: any) => {
    const g = vec3(float(i), float(j), float(k));
    const cell = ip.add(g);
    const o = hash33(cell.add(seed));
    const r = g.add(o).sub(fp);
    const diff = r.sub(mr);
    const keep = hash31(cell.add(mc).mul(0.731).add(seed)).lessThan(coverage);
    If(dot(diff, diff).greaterThan(0.00001).and(keep), () => {
      const n = normalize(diff);
      const d = dot(mr.add(r).mul(0.5), n);
      If(d.lessThan(md), () => {
        md.assign(d);
        mn.assign(n);
      });
    });
  });
  return vec4(mn, md);
});

/** Smooth 0..1 threshold helper. */
export const softThreshold = (x: N, threshold: N, softness: N) =>
  smoothstep(
    float(threshold).sub(float(softness).mul(0.5)),
    float(threshold).add(float(softness).mul(0.5)),
    x,
  );

/** Orthonormal basis (tangent, bitangent) for a normal. */
export const basisFor = (n: N) => {
  const nn = normalize(n);
  const up = select(abs(nn.y).lessThan(0.99), vec3(0, 1, 0), vec3(1, 0, 0));
  const t = normalize(cross(up, nn));
  const b = cross(nn, t);
  return { t, b };
};

/** Rotation of vector v around axis a by angle ang (Rodrigues). */
export const rotateAxis = (v: N, a: N, ang: N) => {
  const c = cos(ang),
    s = sin(ang);
  return v
    .mul(c)
    .add(cross(a, v).mul(s))
    .add(a.mul(dot(a, v)).mul(float(1).sub(c)));
};

export const saturate = (x: N) => min(max(x, 0), 1);
source/src/tsl/t.ts
// three/tsl re-exported untyped: the TSL typings are far stricter than the runtime and would need
// a cast on nearly every call. Node graphs stay readable; correctness is checked at shader build time.
import * as tslNS from "three/tsl";
export const tsl: any = tslNS;
source/src/vendor/clipper-README.md
# ClipperLib

> forked from [Javascript Clipper](http://sourceforge.net/projects/jsclipper/)

## Description

The Javascript Clipper library performs clipping and offsetting for both lines and polygons. All four boolean clipping operations are supported - intersection, union, difference and exclusive-or. Polygons can be of any shape including self-intersecting polygons.

Javascript Clipper is a port of Angus Johnson's Clipper library: <https://sourceforge.net/projects/polyclipping/>

LIVE DEMO: <http://jsclipper.sourceforge.net/6.2.1.0/main_demo.html>

Information and examples:
<http://jsclipper.sourceforge.net/6.2.1.0/>

Donate Javascript Clipper Project: <https://sourceforge.net/p/jsclipper/wiki/Donations/>

Use cases:

- Over 1500 schools in the UK uses Javascript Clipper in Digimap for Schools service. Digimap for Schools is an online mapping service for use by teachers and pupils. Read more:
  <https://mobilegeo.wordpress.com/> and
  <http://digimapforschools.edina.ac.uk/cosmo-free/osmapper>

[Javascript Clipper Web Site](https://sourceforge.net/p/jsclipper/wiki/)

## Features

- Line and polygon clipping - intersection, union, difference & xor
- Line and polygon offsetting with 3 types of joining - miter, square and round
- Polygons can be of any shape, including self-intersecting polygons
- Minkowski Addition and Minkowski Difference functions included
- The library is written in Javascript
- Comprehensive documentation
- Demos use inline SVG and Canvas libraries
- The library is significantly faster than commercial alternatives
- Uses Tom Wu's fast big integer library
- UMD support

## Categories

Algorithms, Graphics

## Links

- [Documentation](./Documentation.md)
- [ChangeLog](./ChangeLog.txt)

## License

[Boost Software License (BSL1.0)](http://www.boost.org/LICENSE_1_0.txt)
source/src/vendor/clipper.cjs
/*******************************************************************************
 *                                                                              *
 * Author    :  Angus Johnson                                                   *
 * Version   :  6.4.2                                                           *
 * Date      :  27 February 2017                                                *
 * Website   :  http://www.angusj.com                                           *
 * Copyright :  Angus Johnson 2010-2017                                         *
 *                                                                              *
 * License:                                                                     *
 * Use, modification & distribution is subject to Boost Software License Ver 1. *
 * http://www.boost.org/LICENSE_1_0.txt                                         *
 *                                                                              *
 * Attributions:                                                                *
 * The code in this library is an extension of Bala Vatti's clipping algorithm: *
 * "A generic solution to polygon clipping"                                     *
 * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63.             *
 * http://portal.acm.org/citation.cfm?id=129906                                 *
 *                                                                              *
 * Computer graphics and geometric modeling: implementation and algorithms      *
 * By Max K. Agoston                                                            *
 * Springer; 1 edition (January 4, 2005)                                        *
 * http://books.google.com/books?q=vatti+clipping+agoston                       *
 *                                                                              *
 * See also:                                                                    *
 * "Polygon Offsetting by Computing Winding Numbers"                            *
 * Paper no. DETC2005-85513 pp. 565-575                                         *
 * ASME 2005 International Design Engineering Technical Conferences             *
 * and Computers and Information in Engineering Conference (IDETC/CIE2005)      *
 * September 24-28, 2005 , Long Beach, California, USA                          *
 * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf              *
 *                                                                              *
 *******************************************************************************/
/*******************************************************************************
 *                                                                              *
 * Author    :  Timo                                                            *
 * Version   :  6.4.2.2                                                         *
 * Date      :  8 September 2017                                                 *
 *                                                                              *
 * This is a translation of the C# Clipper library to Javascript.               *
 * Int128 struct of C# is implemented using JSBN of Tom Wu.                     *
 * Because Javascript lacks support for 64-bit integers, the space              *
 * is a little more restricted than in C# version.                              *
 *                                                                              *
 * C# version has support for coordinate space:                                 *
 * +-4611686018427387903 ( sqrt(2^127 -1)/2 )                                   *
 * while Javascript version has support for space:                              *
 * +-4503599627370495 ( sqrt(2^106 -1)/2 )                                      *
 *                                                                              *
 * Tom Wu's JSBN proved to be the fastest big integer library:                  *
 * http://jsperf.com/big-integer-library-test                                   *
 *                                                                              *
 * This class can be made simpler when (if ever) 64-bit integer support comes   *
 * or floating point Clipper is released.                                       *
 *                                                                              *
 *******************************************************************************/
/*******************************************************************************
 *                                                                              *
 * Basic JavaScript BN library - subset useful for RSA encryption.              *
 * http://www-cs-students.stanford.edu/~tjw/jsbn/                               *
 * Copyright (c) 2005  Tom Wu                                                   *
 * All Rights Reserved.                                                         *
 * See "LICENSE" for details:                                                   *
 * http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE                        *
 *                                                                              *
 *******************************************************************************/
(function () {
  "use strict";
  var ClipperLib = {};
  ClipperLib.version = "6.4.2.2";

  //UseLines: Enables open path clipping. Adds a very minor cost to performance.
  ClipperLib.use_lines = true;

  //ClipperLib.use_xyz: adds a Z member to IntPoint. Adds a minor cost to performance.
  ClipperLib.use_xyz = false;

  var isNode = false;
  if (typeof module !== "undefined" && module.exports) {
    module.exports = ClipperLib;
    isNode = true;
  } else {
    if (typeof define === "function" && define.amd) {
      define(ClipperLib);
    }
    if (typeof document !== "undefined") window.ClipperLib = ClipperLib;
    else self["ClipperLib"] = ClipperLib;
  }
  var navigator_appName;
  if (!isNode) {
    var nav = navigator.userAgent.toString().toLowerCase();
    navigator_appName = navigator.appName;
  } else {
    var nav = "chrome"; // Node.js uses Chrome's V8 engine
    navigator_appName = "Netscape"; // Firefox, Chrome and Safari returns "Netscape", so Node.js should also
  }
  // Browser test to speedup performance critical functions
  var browser = {};

  if (nav.indexOf("chrome") != -1 && nav.indexOf("chromium") == -1) browser.chrome = 1;
  else browser.chrome = 0;
  if (nav.indexOf("chromium") != -1) browser.chromium = 1;
  else browser.chromium = 0;
  if (nav.indexOf("safari") != -1 && nav.indexOf("chrome") == -1 && nav.indexOf("chromium") == -1)
    browser.safari = 1;
  else browser.safari = 0;
  if (nav.indexOf("firefox") != -1) browser.firefox = 1;
  else browser.firefox = 0;
  if (nav.indexOf("firefox/17") != -1) browser.firefox17 = 1;
  else browser.firefox17 = 0;
  if (nav.indexOf("firefox/15") != -1) browser.firefox15 = 1;
  else browser.firefox15 = 0;
  if (nav.indexOf("firefox/3") != -1) browser.firefox3 = 1;
  else browser.firefox3 = 0;
  if (nav.indexOf("opera") != -1) browser.opera = 1;
  else browser.opera = 0;
  if (nav.indexOf("msie 10") != -1) browser.msie10 = 1;
  else browser.msie10 = 0;
  if (nav.indexOf("msie 9") != -1) browser.msie9 = 1;
  else browser.msie9 = 0;
  if (nav.indexOf("msie 8") != -1) browser.msie8 = 1;
  else browser.msie8 = 0;
  if (nav.indexOf("msie 7") != -1) browser.msie7 = 1;
  else browser.msie7 = 0;
  if (nav.indexOf("msie ") != -1) browser.msie = 1;
  else browser.msie = 0;
  ClipperLib.biginteger_used = null;

  // Copyright (c) 2005  Tom Wu
  // All Rights Reserved.
  // See "LICENSE" for details.
  // Basic JavaScript BN library - subset useful for RSA encryption.
  // Bits per digit
  var dbits;
  // JavaScript engine analysis
  var canary = 0xdeadbeefcafe;
  var j_lm = (canary & 0xffffff) == 0xefcafe;
  // (public) Constructor
  /**
   * @constructor
   */
  function BigInteger(a, b, c) {
    // This test variable can be removed,
    // but at least for performance tests it is useful piece of knowledge
    // This is the only ClipperLib related variable in BigInteger library
    ClipperLib.biginteger_used = 1;
    if (a != null)
      if ("number" == typeof a && "undefined" == typeof b)
        this.fromInt(a); // faster conversion
      else if ("number" == typeof a) this.fromNumber(a, b, c);
      else if (b == null && "string" != typeof a) this.fromString(a, 256);
      else this.fromString(a, b);
  }
  // return new, unset BigInteger
  function nbi() {
    return new BigInteger(null, undefined, undefined);
  }
  // am: Compute w_j += (x*this_i), propagate carries,
  // c is initial carry, returns final carry.
  // c < 3*dvalue, x < 2*dvalue, this_i < dvalue
  // We need to select the fastest one that works in this environment.
  // am1: use a single mult and divide to get the high bits,
  // max digit bits should be 26 because
  // max internal value = 2*dvalue^2-2*dvalue (< 2^53)
  function am1(i, x, w, j, c, n) {
    while (--n >= 0) {
      var v = x * this[i++] + w[j] + c;
      c = Math.floor(v / 0x4000000);
      w[j++] = v & 0x3ffffff;
    }
    return c;
  }
  // am2 avoids a big mult-and-extract completely.
  // Max digit bits should be <= 30 because we do bitwise ops
  // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31)
  function am2(i, x, w, j, c, n) {
    var xl = x & 0x7fff,
      xh = x >> 15;
    while (--n >= 0) {
      var l = this[i] & 0x7fff;
      var h = this[i++] >> 15;
      var m = xh * l + h * xl;
      l = xl * l + ((m & 0x7fff) << 15) + w[j] + (c & 0x3fffffff);
      c = (l >>> 30) + (m >>> 15) + xh * h + (c >>> 30);
      w[j++] = l & 0x3fffffff;
    }
    return c;
  }
  // Alternately, set max digit bits to 28 since some
  // browsers slow down when dealing with 32-bit numbers.
  function am3(i, x, w, j, c, n) {
    var xl = x & 0x3fff,
      xh = x >> 14;
    while (--n >= 0) {
      var l = this[i] & 0x3fff;
      var h = this[i++] >> 14;
      var m = xh * l + h * xl;
      l = xl * l + ((m & 0x3fff) << 14) + w[j] + c;
      c = (l >> 28) + (m >> 14) + xh * h;
      w[j++] = l & 0xfffffff;
    }
    return c;
  }
  if (j_lm && navigator_appName == "Microsoft Internet Explorer") {
    BigInteger.prototype.am = am2;
    dbits = 30;
  } else if (j_lm && navigator_appName != "Netscape") {
    BigInteger.prototype.am = am1;
    dbits = 26;
  } else {
    // Mozilla/Netscape seems to prefer am3
    BigInteger.prototype.am = am3;
    dbits = 28;
  }
  BigInteger.prototype.DB = dbits;
  BigInteger.prototype.DM = (1 << dbits) - 1;
  BigInteger.prototype.DV = 1 << dbits;
  var BI_FP = 52;
  BigInteger.prototype.FV = Math.pow(2, BI_FP);
  BigInteger.prototype.F1 = BI_FP - dbits;
  BigInteger.prototype.F2 = 2 * dbits - BI_FP;
  // Digit conversions
  var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz";
  var BI_RC = new Array();
  var rr, vv;
  rr = "0".charCodeAt(0);
  for (vv = 0; vv <= 9; ++vv) BI_RC[rr++] = vv;
  rr = "a".charCodeAt(0);
  for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv;
  rr = "A".charCodeAt(0);
  for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv;

  function int2char(n) {
    return BI_RM.charAt(n);
  }

  function intAt(s, i) {
    var c = BI_RC[s.charCodeAt(i)];
    return c == null ? -1 : c;
  }
  // (protected) copy this to r
  function bnpCopyTo(r) {
    for (var i = this.t - 1; i >= 0; --i) r[i] = this[i];
    r.t = this.t;
    r.s = this.s;
  }
  // (protected) set from integer value x, -DV <= x < DV
  function bnpFromInt(x) {
    this.t = 1;
    this.s = x < 0 ? -1 : 0;
    if (x > 0) this[0] = x;
    else if (x < -1) this[0] = x + this.DV;
    else this.t = 0;
  }
  // return bigint initialized to value
  function nbv(i) {
    var r = nbi();
    r.fromInt(i);
    return r;
  }
  // (protected) set from string and radix
  function bnpFromString(s, b) {
    var k;
    if (b == 16) k = 4;
    else if (b == 8) k = 3;
    else if (b == 256)
      k = 8; // byte array
    else if (b == 2) k = 1;
    else if (b == 32) k = 5;
    else if (b == 4) k = 2;
    else {
      this.fromRadix(s, b);
      return;
    }
    this.t = 0;
    this.s = 0;
    var i = s.length,
      mi = false,
      sh = 0;
    while (--i >= 0) {
      var x = k == 8 ? s[i] & 0xff : intAt(s, i);
      if (x < 0) {
        if (s.charAt(i) == "-") mi = true;
        continue;
      }
      mi = false;
      if (sh == 0) this[this.t++] = x;
      else if (sh + k > this.DB) {
        this[this.t - 1] |= (x & ((1 << (this.DB - sh)) - 1)) << sh;
        this[this.t++] = x >> (this.DB - sh);
      } else this[this.t - 1] |= x << sh;
      sh += k;
      if (sh >= this.DB) sh -= this.DB;
    }
    if (k == 8 && (s[0] & 0x80) != 0) {
      this.s = -1;
      if (sh > 0) this[this.t - 1] |= ((1 << (this.DB - sh)) - 1) << sh;
    }
    this.clamp();
    if (mi) BigInteger.ZERO.subTo(this, this);
  }
  // (protected) clamp off excess high words
  function bnpClamp() {
    var c = this.s & this.DM;
    while (this.t > 0 && this[this.t - 1] == c) --this.t;
  }
  // (public) return string representation in given radix
  function bnToString(b) {
    if (this.s < 0) return "-" + this.negate().toString(b);
    var k;
    if (b == 16) k = 4;
    else if (b == 8) k = 3;
    else if (b == 2) k = 1;
    else if (b == 32) k = 5;
    else if (b == 4) k = 2;
    else return this.toRadix(b);
    var km = (1 << k) - 1,
      d,
      m = false,
      r = "",
      i = this.t;
    var p = this.DB - ((i * this.DB) % k);
    if (i-- > 0) {
      if (p < this.DB && (d = this[i] >> p) > 0) {
        m = true;
        r = int2char(d);
      }
      while (i >= 0) {
        if (p < k) {
          d = (this[i] & ((1 << p) - 1)) << (k - p);
          d |= this[--i] >> (p += this.DB - k);
        } else {
          d = (this[i] >> (p -= k)) & km;
          if (p <= 0) {
            p += this.DB;
            --i;
          }
        }
        if (d > 0) m = true;
        if (m) r += int2char(d);
      }
    }
    return m ? r : "0";
  }
  // (public) -this
  function bnNegate() {
    var r = nbi();
    BigInteger.ZERO.subTo(this, r);
    return r;
  }
  // (public) |this|
  function bnAbs() {
    return this.s < 0 ? this.negate() : this;
  }
  // (public) return + if this > a, - if this < a, 0 if equal
  function bnCompareTo(a) {
    var r = this.s - a.s;
    if (r != 0) return r;
    var i = this.t;
    r = i - a.t;
    if (r != 0) return this.s < 0 ? -r : r;
    while (--i >= 0) if ((r = this[i] - a[i]) != 0) return r;
    return 0;
  }
  // returns bit length of the integer x
  function nbits(x) {
    var r = 1,
      t;
    if ((t = x >>> 16) != 0) {
      x = t;
      r += 16;
    }
    if ((t = x >> 8) != 0) {
      x = t;
      r += 8;
    }
    if ((t = x >> 4) != 0) {
      x = t;
      r += 4;
    }
    if ((t = x >> 2) != 0) {
      x = t;
      r += 2;
    }
    if ((t = x >> 1) != 0) {
      x = t;
      r += 1;
    }
    return r;
  }
  // (public) return the number of bits in "this"
  function bnBitLength() {
    if (this.t <= 0) return 0;
    return this.DB * (this.t - 1) + nbits(this[this.t - 1] ^ (this.s & this.DM));
  }
  // (protected) r = this << n*DB
  function bnpDLShiftTo(n, r) {
    var i;
    for (i = this.t - 1; i >= 0; --i) r[i + n] = this[i];
    for (i = n - 1; i >= 0; --i) r[i] = 0;
    r.t = this.t + n;
    r.s = this.s;
  }
  // (protected) r = this >> n*DB
  function bnpDRShiftTo(n, r) {
    for (var i = n; i < this.t; ++i) r[i - n] = this[i];
    r.t = Math.max(this.t - n, 0);
    r.s = this.s;
  }
  // (protected) r = this << n
  function bnpLShiftTo(n, r) {
    var bs = n % this.DB;
    var cbs = this.DB - bs;
    var bm = (1 << cbs) - 1;
    var ds = Math.floor(n / this.DB),
      c = (this.s << bs) & this.DM,
      i;
    for (i = this.t - 1; i >= 0; --i) {
      r[i + ds + 1] = (this[i] >> cbs) | c;
      c = (this[i] & bm) << bs;
    }
    for (i = ds - 1; i >= 0; --i) r[i] = 0;
    r[ds] = c;
    r.t = this.t + ds + 1;
    r.s = this.s;
    r.clamp();
  }
  // (protected) r = this >> n
  function bnpRShiftTo(n, r) {
    r.s = this.s;
    var ds = Math.floor(n / this.DB);
    if (ds >= this.t) {
      r.t = 0;
      return;
    }
    var bs = n % this.DB;
    var cbs = this.DB - bs;
    var bm = (1 << bs) - 1;
    r[0] = this[ds] >> bs;
    for (var i = ds + 1; i < this.t; ++i) {
      r[i - ds - 1] |= (this[i] & bm) << cbs;
      r[i - ds] = this[i] >> bs;
    }
    if (bs > 0) r[this.t - ds - 1] |= (this.s & bm) << cbs;
    r.t = this.t - ds;
    r.clamp();
  }
  // (protected) r = this - a
  function bnpSubTo(a, r) {
    var i = 0,
      c = 0,
      m = Math.min(a.t, this.t);
    while (i < m) {
      c += this[i] - a[i];
      r[i++] = c & this.DM;
      c >>= this.DB;
    }
    if (a.t < this.t) {
      c -= a.s;
      while (i < this.t) {
        c += this[i];
        r[i++] = c & this.DM;
        c >>= this.DB;
      }
      c += this.s;
    } else {
      c += this.s;
      while (i < a.t) {
        c -= a[i];
        r[i++] = c & this.DM;
        c >>= this.DB;
      }
      c -= a.s;
    }
    r.s = c < 0 ? -1 : 0;
    if (c < -1) r[i++] = this.DV + c;
    else if (c > 0) r[i++] = c;
    r.t = i;
    r.clamp();
  }
  // (protected) r = this * a, r != this,a (HAC 14.12)
  // "this" should be the larger one if appropriate.
  function bnpMultiplyTo(a, r) {
    var x = this.abs(),
      y = a.abs();
    var i = x.t;
    r.t = i + y.t;
    while (--i >= 0) r[i] = 0;
    for (i = 0; i < y.t; ++i) r[i + x.t] = x.am(0, y[i], r, i, 0, x.t);
    r.s = 0;
    r.clamp();
    if (this.s != a.s) BigInteger.ZERO.subTo(r, r);
  }
  // (protected) r = this^2, r != this (HAC 14.16)
  function bnpSquareTo(r) {
    var x = this.abs();
    var i = (r.t = 2 * x.t);
    while (--i >= 0) r[i] = 0;
    for (i = 0; i < x.t - 1; ++i) {
      var c = x.am(i, x[i], r, 2 * i, 0, 1);
      if ((r[i + x.t] += x.am(i + 1, 2 * x[i], r, 2 * i + 1, c, x.t - i - 1)) >= x.DV) {
        r[i + x.t] -= x.DV;
        r[i + x.t + 1] = 1;
      }
    }
    if (r.t > 0) r[r.t - 1] += x.am(i, x[i], r, 2 * i, 0, 1);
    r.s = 0;
    r.clamp();
  }
  // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20)
  // r != q, this != m.  q or r may be null.
  function bnpDivRemTo(m, q, r) {
    var pm = m.abs();
    if (pm.t <= 0) return;
    var pt = this.abs();
    if (pt.t < pm.t) {
      if (q != null) q.fromInt(0);
      if (r != null) this.copyTo(r);
      return;
    }
    if (r == null) r = nbi();
    var y = nbi(),
      ts = this.s,
      ms = m.s;
    var nsh = this.DB - nbits(pm[pm.t - 1]); // normalize modulus
    if (nsh > 0) {
      pm.lShiftTo(nsh, y);
      pt.lShiftTo(nsh, r);
    } else {
      pm.copyTo(y);
      pt.copyTo(r);
    }
    var ys = y.t;
    var y0 = y[ys - 1];
    if (y0 == 0) return;
    var yt = y0 * (1 << this.F1) + (ys > 1 ? y[ys - 2] >> this.F2 : 0);
    var d1 = this.FV / yt,
      d2 = (1 << this.F1) / yt,
      e = 1 << this.F2;
    var i = r.t,
      j = i - ys,
      t = q == null ? nbi() : q;
    y.dlShiftTo(j, t);
    if (r.compareTo(t) >= 0) {
      r[r.t++] = 1;
      r.subTo(t, r);
    }
    BigInteger.ONE.dlShiftTo(ys, t);
    t.subTo(y, y); // "negative" y so we can replace sub with am later
    while (y.t < ys) y[y.t++] = 0;
    while (--j >= 0) {
      // Estimate quotient digit
      var qd = r[--i] == y0 ? this.DM : Math.floor(r[i] * d1 + (r[i - 1] + e) * d2);
      if ((r[i] += y.am(0, qd, r, j, 0, ys)) < qd) {
        // Try it out
        y.dlShiftTo(j, t);
        r.subTo(t, r);
        while (r[i] < --qd) r.subTo(t, r);
      }
    }
    if (q != null) {
      r.drShiftTo(ys, q);
      if (ts != ms) BigInteger.ZERO.subTo(q, q);
    }
    r.t = ys;
    r.clamp();
    if (nsh > 0) r.rShiftTo(nsh, r); // Denormalize remainder
    if (ts < 0) BigInteger.ZERO.subTo(r, r);
  }
  // (public) this mod a
  function bnMod(a) {
    var r = nbi();
    this.abs().divRemTo(a, null, r);
    if (this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r, r);
    return r;
  }
  // Modular reduction using "classic" algorithm
  /**
   * @constructor
   */
  function Classic(m) {
    this.m = m;
  }

  function cConvert(x) {
    if (x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m);
    else return x;
  }

  function cRevert(x) {
    return x;
  }

  function cReduce(x) {
    x.divRemTo(this.m, null, x);
  }

  function cMulTo(x, y, r) {
    x.multiplyTo(y, r);
    this.reduce(r);
  }

  function cSqrTo(x, r) {
    x.squareTo(r);
    this.reduce(r);
  }
  Classic.prototype.convert = cConvert;
  Classic.prototype.revert = cRevert;
  Classic.prototype.reduce = cReduce;
  Classic.prototype.mulTo = cMulTo;
  Classic.prototype.sqrTo = cSqrTo;
  // (protected) return "-1/this % 2^DB"; useful for Mont. reduction
  // justification:
  //         xy == 1 (mod m)
  //         xy =  1+km
  //   xy(2-xy) = (1+km)(1-km)
  // x[y(2-xy)] = 1-k^2m^2
  // x[y(2-xy)] == 1 (mod m^2)
  // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2
  // should reduce x and y(2-xy) by m^2 at each step to keep size bounded.
  // JS multiply "overflows" differently from C/C++, so care is needed here.
  function bnpInvDigit() {
    if (this.t < 1) return 0;
    var x = this[0];
    if ((x & 1) == 0) return 0;
    var y = x & 3; // y == 1/x mod 2^2
    y = (y * (2 - (x & 0xf) * y)) & 0xf; // y == 1/x mod 2^4
    y = (y * (2 - (x & 0xff) * y)) & 0xff; // y == 1/x mod 2^8
    y = (y * (2 - (((x & 0xffff) * y) & 0xffff))) & 0xffff; // y == 1/x mod 2^16
    // last step - calculate inverse mod DV directly;
    // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints
    y = (y * (2 - ((x * y) % this.DV))) % this.DV; // y == 1/x mod 2^dbits
    // we really want the negative inverse, and -DV < y < DV
    return y > 0 ? this.DV - y : -y;
  }
  // Montgomery reduction
  /**
   * @constructor
   */
  function Montgomery(m) {
    this.m = m;
    this.mp = m.invDigit();
    this.mpl = this.mp & 0x7fff;
    this.mph = this.mp >> 15;
    this.um = (1 << (m.DB - 15)) - 1;
    this.mt2 = 2 * m.t;
  }
  // xR mod m
  function montConvert(x) {
    var r = nbi();
    x.abs().dlShiftTo(this.m.t, r);
    r.divRemTo(this.m, null, r);
    if (x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r, r);
    return r;
  }
  // x/R mod m
  function montRevert(x) {
    var r = nbi();
    x.copyTo(r);
    this.reduce(r);
    return r;
  }
  // x = x/R mod m (HAC 14.32)
  function montReduce(x) {
    while (x.t <= this.mt2)
      // pad x so am has enough room later
      x[x.t++] = 0;
    for (var i = 0; i < this.m.t; ++i) {
      // faster way of calculating u0 = x[i]*mp mod DV
      var j = x[i] & 0x7fff;
      var u0 = (j * this.mpl + (((j * this.mph + (x[i] >> 15) * this.mpl) & this.um) << 15)) & x.DM;
      // use am to combine the multiply-shift-add into one call
      j = i + this.m.t;
      x[j] += this.m.am(0, u0, x, i, 0, this.m.t);
      // propagate carry
      while (x[j] >= x.DV) {
        x[j] -= x.DV;
        x[++j]++;
      }
    }
    x.clamp();
    x.drShiftTo(this.m.t, x);
    if (x.compareTo(this.m) >= 0) x.subTo(this.m, x);
  }
  // r = "x^2/R mod m"; x != r
  function montSqrTo(x, r) {
    x.squareTo(r);
    this.reduce(r);
  }
  // r = "xy/R mod m"; x,y != r
  function montMulTo(x, y, r) {
    x.multiplyTo(y, r);
    this.reduce(r);
  }
  Montgomery.prototype.convert = montConvert;
  Montgomery.prototype.revert = montRevert;
  Montgomery.prototype.reduce = montReduce;
  Montgomery.prototype.mulTo = montMulTo;
  Montgomery.prototype.sqrTo = montSqrTo;
  // (protected) true iff this is even
  function bnpIsEven() {
    return (this.t > 0 ? this[0] & 1 : this.s) == 0;
  }
  // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79)
  function bnpExp(e, z) {
    if (e > 0xffffffff || e < 1) return BigInteger.ONE;
    var r = nbi(),
      r2 = nbi(),
      g = z.convert(this),
      i = nbits(e) - 1;
    g.copyTo(r);
    while (--i >= 0) {
      z.sqrTo(r, r2);
      if ((e & (1 << i)) > 0) z.mulTo(r2, g, r);
      else {
        var t = r;
        r = r2;
        r2 = t;
      }
    }
    return z.revert(r);
  }
  // (public) this^e % m, 0 <= e < 2^32
  function bnModPowInt(e, m) {
    var z;
    if (e < 256 || m.isEven()) z = new Classic(m);
    else z = new Montgomery(m);
    return this.exp(e, z);
  }
  // protected
  BigInteger.prototype.copyTo = bnpCopyTo;
  BigInteger.prototype.fromInt = bnpFromInt;
  BigInteger.prototype.fromString = bnpFromString;
  BigInteger.prototype.clamp = bnpClamp;
  BigInteger.prototype.dlShiftTo = bnpDLShiftTo;
  BigInteger.prototype.drShiftTo = bnpDRShiftTo;
  BigInteger.prototype.lShiftTo = bnpLShiftTo;
  BigInteger.prototype.rShiftTo = bnpRShiftTo;
  BigInteger.prototype.subTo = bnpSubTo;
  BigInteger.prototype.multiplyTo = bnpMultiplyTo;
  BigInteger.prototype.squareTo = bnpSquareTo;
  BigInteger.prototype.divRemTo = bnpDivRemTo;
  BigInteger.prototype.invDigit = bnpInvDigit;
  BigInteger.prototype.isEven = bnpIsEven;
  BigInteger.prototype.exp = bnpExp;
  // public
  BigInteger.prototype.toString = bnToString;
  BigInteger.prototype.negate = bnNegate;
  BigInteger.prototype.abs = bnAbs;
  BigInteger.prototype.compareTo = bnCompareTo;
  BigInteger.prototype.bitLength = bnBitLength;
  BigInteger.prototype.mod = bnMod;
  BigInteger.prototype.modPowInt = bnModPowInt;
  // "constants"
  BigInteger.ZERO = nbv(0);
  BigInteger.ONE = nbv(1);
  // Copyright (c) 2005-2009  Tom Wu
  // All Rights Reserved.
  // See "LICENSE" for details.
  // Extended JavaScript BN functions, required for RSA private ops.
  // Version 1.1: new BigInteger("0", 10) returns "proper" zero
  // Version 1.2: square() API, isProbablePrime fix
  // (public)
  function bnClone() {
    var r = nbi();
    this.copyTo(r);
    return r;
  }
  // (public) return value as integer
  function bnIntValue() {
    if (this.s < 0) {
      if (this.t == 1) return this[0] - this.DV;
      else if (this.t == 0) return -1;
    } else if (this.t == 1) return this[0];
    else if (this.t == 0) return 0;
    // assumes 16 < DB < 32
    return ((this[1] & ((1 << (32 - this.DB)) - 1)) << this.DB) | this[0];
  }
  // (public) return value as byte
  function bnByteValue() {
    return this.t == 0 ? this.s : (this[0] << 24) >> 24;
  }
  // (public) return value as short (assumes DB>=16)
  function bnShortValue() {
    return this.t == 0 ? this.s : (this[0] << 16) >> 16;
  }
  // (protected) return x s.t. r^x < DV
  function bnpChunkSize(r) {
    return Math.floor((Math.LN2 * this.DB) / Math.log(r));
  }
  // (public) 0 if this == 0, 1 if this > 0
  function bnSigNum() {
    if (this.s < 0) return -1;
    else if (this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0;
    else return 1;
  }
  // (protected) convert to radix string
  function bnpToRadix(b) {
    if (b == null) b = 10;
    if (this.signum() == 0 || b < 2 || b > 36) return "0";
    var cs = this.chunkSize(b);
    var a = Math.pow(b, cs);
    var d = nbv(a),
      y = nbi(),
      z = nbi(),
      r = "";
    this.divRemTo(d, y, z);
    while (y.signum() > 0) {
      r = (a + z.intValue()).toString(b).substr(1) + r;
      y.divRemTo(d, y, z);
    }
    return z.intValue().toString(b) + r;
  }
  // (protected) convert from radix string
  function bnpFromRadix(s, b) {
    this.fromInt(0);
    if (b == null) b = 10;
    var cs = this.chunkSize(b);
    var d = Math.pow(b, cs),
      mi = false,
      j = 0,
      w = 0;
    for (var i = 0; i < s.length; ++i) {
      var x = intAt(s, i);
      if (x < 0) {
        if (s.charAt(i) == "-" && this.signum() == 0) mi = true;
        continue;
      }
      w = b * w + x;
      if (++j >= cs) {
        this.dMultiply(d);
        this.dAddOffset(w, 0);
        j = 0;
        w = 0;
      }
    }
    if (j > 0) {
      this.dMultiply(Math.pow(b, j));
      this.dAddOffset(w, 0);
    }
    if (mi) BigInteger.ZERO.subTo(this, this);
  }
  // (protected) alternate constructor
  function bnpFromNumber(a, b, c) {
    if ("number" == typeof b) {
      // new BigInteger(int,int,RNG)
      if (a < 2) this.fromInt(1);
      else {
        this.fromNumber(a, c);
        if (!this.testBit(a - 1))
          // force MSB set
          this.bitwiseTo(BigInteger.ONE.shiftLeft(a - 1), op_or, this);
        if (this.isEven()) this.dAddOffset(1, 0); // force odd
        while (!this.isProbablePrime(b)) {
          this.dAddOffset(2, 0);
          if (this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a - 1), this);
        }
      }
    } else {
      // new BigInteger(int,RNG)
      var x = new Array(),
        t = a & 7;
      x.length = (a >> 3) + 1;
      b.nextBytes(x);
      if (t > 0) x[0] &= (1 << t) - 1;
      else x[0] = 0;
      this.fromString(x, 256);
    }
  }
  // (public) convert to bigendian byte array
  function bnToByteArray() {
    var i = this.t,
      r = new Array();
    r[0] = this.s;
    var p = this.DB - ((i * this.DB) % 8),
      d,
      k = 0;
    if (i-- > 0) {
      if (p < this.DB && (d = this[i] >> p) != (this.s & this.DM) >> p)
        r[k++] = d | (this.s << (this.DB - p));
      while (i >= 0) {
        if (p < 8) {
          d = (this[i] & ((1 << p) - 1)) << (8 - p);
          d |= this[--i] >> (p += this.DB - 8);
        } else {
          d = (this[i] >> (p -= 8)) & 0xff;
          if (p <= 0) {
            p += this.DB;
            --i;
          }
        }
        if ((d & 0x80) != 0) d |= -256;
        if (k == 0 && (this.s & 0x80) != (d & 0x80)) ++k;
        if (k > 0 || d != this.s) r[k++] = d;
      }
    }
    return r;
  }

  function bnEquals(a) {
    return this.compareTo(a) == 0;
  }

  function bnMin(a) {
    return this.compareTo(a) < 0 ? this : a;
  }

  function bnMax(a) {
    return this.compareTo(a) > 0 ? this : a;
  }
  // (protected) r = this op a (bitwise)
  function bnpBitwiseTo(a, op, r) {
    var i,
      f,
      m = Math.min(a.t, this.t);
    for (i = 0; i < m; ++i) r[i] = op(this[i], a[i]);
    if (a.t < this.t) {
      f = a.s & this.DM;
      for (i = m; i < this.t; ++i) r[i] = op(this[i], f);
      r.t = this.t;
    } else {
      f = this.s & this.DM;
      for (i = m; i < a.t; ++i) r[i] = op(f, a[i]);
      r.t = a.t;
    }
    r.s = op(this.s, a.s);
    r.clamp();
  }
  // (public) this & a
  function op_and(x, y) {
    return x & y;
  }

  function bnAnd(a) {
    var r = nbi();
    this.bitwiseTo(a, op_and, r);
    return r;
  }
  // (public) this | a
  function op_or(x, y) {
    return x | y;
  }

  function bnOr(a) {
    var r = nbi();
    this.bitwiseTo(a, op_or, r);
    return r;
  }
  // (public) this ^ a
  function op_xor(x, y) {
    return x ^ y;
  }

  function bnXor(a) {
    var r = nbi();
    this.bitwiseTo(a, op_xor, r);
    return r;
  }
  // (public) this & ~a
  function op_andnot(x, y) {
    return x & ~y;
  }

  function bnAndNot(a) {
    var r = nbi();
    this.bitwiseTo(a, op_andnot, r);
    return r;
  }
  // (public) ~this
  function bnNot() {
    var r = nbi();
    for (var i = 0; i < this.t; ++i) r[i] = this.DM & ~this[i];
    r.t = this.t;
    r.s = ~this.s;
    return r;
  }
  // (public) this << n
  function bnShiftLeft(n) {
    var r = nbi();
    if (n < 0) this.rShiftTo(-n, r);
    else this.lShiftTo(n, r);
    return r;
  }
  // (public) this >> n
  function bnShiftRight(n) {
    var r = nbi();
    if (n < 0) this.lShiftTo(-n, r);
    else this.rShiftTo(n, r);
    return r;
  }
  // return index of lowest 1-bit in x, x < 2^31
  function lbit(x) {
    if (x == 0) return -1;
    var r = 0;
    if ((x & 0xffff) == 0) {
      x >>= 16;
      r += 16;
    }
    if ((x & 0xff) == 0) {
      x >>= 8;
      r += 8;
    }
    if ((x & 0xf) == 0) {
      x >>= 4;
      r += 4;
    }
    if ((x & 3) == 0) {
      x >>= 2;
      r += 2;
    }
    if ((x & 1) == 0) ++r;
    return r;
  }
  // (public) returns index of lowest 1-bit (or -1 if none)
  function bnGetLowestSetBit() {
    for (var i = 0; i < this.t; ++i) if (this[i] != 0) return i * this.DB + lbit(this[i]);
    if (this.s < 0) return this.t * this.DB;
    return -1;
  }
  // return number of 1 bits in x
  function cbit(x) {
    var r = 0;
    while (x != 0) {
      x &= x - 1;
      ++r;
    }
    return r;
  }
  // (public) return number of set bits
  function bnBitCount() {
    var r = 0,
      x = this.s & this.DM;
    for (var i = 0; i < this.t; ++i) r += cbit(this[i] ^ x);
    return r;
  }
  // (public) true iff nth bit is set
  function bnTestBit(n) {
    var j = Math.floor(n / this.DB);
    if (j >= this.t) return this.s != 0;
    return (this[j] & (1 << (n % this.DB))) != 0;
  }
  // (protected) this op (1<<n)
  function bnpChangeBit(n, op) {
    var r = BigInteger.ONE.shiftLeft(n);
    this.bitwiseTo(r, op, r);
    return r;
  }
  // (public) this | (1<<n)
  function bnSetBit(n) {
    return this.changeBit(n, op_or);
  }
  // (public) this & ~(1<<n)
  function bnClearBit(n) {
    return this.changeBit(n, op_andnot);
  }
  // (public) this ^ (1<<n)
  function bnFlipBit(n) {
    return this.changeBit(n, op_xor);
  }
  // (protected) r = this + a
  function bnpAddTo(a, r) {
    var i = 0,
      c = 0,
      m = Math.min(a.t, this.t);
    while (i < m) {
      c += this[i] + a[i];
      r[i++] = c & this.DM;
      c >>= this.DB;
    }
    if (a.t < this.t) {
      c += a.s;
      while (i < this.t) {
        c += this[i];
        r[i++] = c & this.DM;
        c >>= this.DB;
      }
      c += this.s;
    } else {
      c += this.s;
      while (i < a.t) {
        c += a[i];
        r[i++] = c & this.DM;
        c >>= this.DB;
      }
      c += a.s;
    }
    r.s = c < 0 ? -1 : 0;
    if (c > 0) r[i++] = c;
    else if (c < -1) r[i++] = this.DV + c;
    r.t = i;
    r.clamp();
  }
  // (public) this + a
  function bnAdd(a) {
    var r = nbi();
    this.addTo(a, r);
    return r;
  }
  // (public) this - a
  function bnSubtract(a) {
    var r = nbi();
    this.subTo(a, r);
    return r;
  }
  // (public) this * a
  function bnMultiply(a) {
    var r = nbi();
    this.multiplyTo(a, r);
    return r;
  }
  // (public) this^2
  function bnSquare() {
    var r = nbi();
    this.squareTo(r);
    return r;
  }
  // (public) this / a
  function bnDivide(a) {
    var r = nbi();
    this.divRemTo(a, r, null);
    return r;
  }
  // (public) this % a
  function bnRemainder(a) {
    var r = nbi();
    this.divRemTo(a, null, r);
    return r;
  }
  // (public) [this/a,this%a]
  function bnDivideAndRemainder(a) {
    var q = nbi(),
      r = nbi();
    this.divRemTo(a, q, r);
    return new Array(q, r);
  }
  // (protected) this *= n, this >= 0, 1 < n < DV
  function bnpDMultiply(n) {
    this[this.t] = this.am(0, n - 1, this, 0, 0, this.t);
    ++this.t;
    this.clamp();
  }
  // (protected) this += n << w words, this >= 0
  function bnpDAddOffset(n, w) {
    if (n == 0) return;
    while (this.t <= w) this[this.t++] = 0;
    this[w] += n;
    while (this[w] >= this.DV) {
      this[w] -= this.DV;
      if (++w >= this.t) this[this.t++] = 0;
      ++this[w];
    }
  }
  // A "null" reducer
  /**
   * @constructor
   */
  function NullExp() {}

  function nNop(x) {
    return x;
  }

  function nMulTo(x, y, r) {
    x.multiplyTo(y, r);
  }

  function nSqrTo(x, r) {
    x.squareTo(r);
  }
  NullExp.prototype.convert = nNop;
  NullExp.prototype.revert = nNop;
  NullExp.prototype.mulTo = nMulTo;
  NullExp.prototype.sqrTo = nSqrTo;
  // (public) this^e
  function bnPow(e) {
    return this.exp(e, new NullExp());
  }
  // (protected) r = lower n words of "this * a", a.t <= n
  // "this" should be the larger one if appropriate.
  function bnpMultiplyLowerTo(a, n, r) {
    var i = Math.min(this.t + a.t, n);
    r.s = 0; // assumes a,this >= 0
    r.t = i;
    while (i > 0) r[--i] = 0;
    var j;
    for (j = r.t - this.t; i < j; ++i) r[i + this.t] = this.am(0, a[i], r, i, 0, this.t);
    for (j = Math.min(a.t, n); i < j; ++i) this.am(0, a[i], r, i, 0, n - i);
    r.clamp();
  }
  // (protected) r = "this * a" without lower n words, n > 0
  // "this" should be the larger one if appropriate.
  function bnpMultiplyUpperTo(a, n, r) {
    --n;
    var i = (r.t = this.t + a.t - n);
    r.s = 0; // assumes a,this >= 0
    while (--i >= 0) r[i] = 0;
    for (i = Math.max(n - this.t, 0); i < a.t; ++i)
      r[this.t + i - n] = this.am(n - i, a[i], r, 0, 0, this.t + i - n);
    r.clamp();
    r.drShiftTo(1, r);
  }
  // Barrett modular reduction
  /**
   * @constructor
   */
  function Barrett(m) {
    // setup Barrett
    this.r2 = nbi();
    this.q3 = nbi();
    BigInteger.ONE.dlShiftTo(2 * m.t, this.r2);
    this.mu = this.r2.divide(m);
    this.m = m;
  }

  function barrettConvert(x) {
    if (x.s < 0 || x.t > 2 * this.m.t) return x.mod(this.m);
    else if (x.compareTo(this.m) < 0) return x;
    else {
      var r = nbi();
      x.copyTo(r);
      this.reduce(r);
      return r;
    }
  }

  function barrettRevert(x) {
    return x;
  }
  // x = x mod m (HAC 14.42)
  function barrettReduce(x) {
    x.drShiftTo(this.m.t - 1, this.r2);
    if (x.t > this.m.t + 1) {
      x.t = this.m.t + 1;
      x.clamp();
    }
    this.mu.multiplyUpperTo(this.r2, this.m.t + 1, this.q3);
    this.m.multiplyLowerTo(this.q3, this.m.t + 1, this.r2);
    while (x.compareTo(this.r2) < 0) x.dAddOffset(1, this.m.t + 1);
    x.subTo(this.r2, x);
    while (x.compareTo(this.m) >= 0) x.subTo(this.m, x);
  }
  // r = x^2 mod m; x != r
  function barrettSqrTo(x, r) {
    x.squareTo(r);
    this.reduce(r);
  }
  // r = x*y mod m; x,y != r
  function barrettMulTo(x, y, r) {
    x.multiplyTo(y, r);
    this.reduce(r);
  }
  Barrett.prototype.convert = barrettConvert;
  Barrett.prototype.revert = barrettRevert;
  Barrett.prototype.reduce = barrettReduce;
  Barrett.prototype.mulTo = barrettMulTo;
  Barrett.prototype.sqrTo = barrettSqrTo;
  // (public) this^e % m (HAC 14.85)
  function bnModPow(e, m) {
    var i = e.bitLength(),
      k,
      r = nbv(1),
      z;
    if (i <= 0) return r;
    else if (i < 18) k = 1;
    else if (i < 48) k = 3;
    else if (i < 144) k = 4;
    else if (i < 768) k = 5;
    else k = 6;
    if (i < 8) z = new Classic(m);
    else if (m.isEven()) z = new Barrett(m);
    else z = new Montgomery(m);
    // precomputation
    var g = new Array(),
      n = 3,
      k1 = k - 1,
      km = (1 << k) - 1;
    g[1] = z.convert(this);
    if (k > 1) {
      var g2 = nbi();
      z.sqrTo(g[1], g2);
      while (n <= km) {
        g[n] = nbi();
        z.mulTo(g2, g[n - 2], g[n]);
        n += 2;
      }
    }
    var j = e.t - 1,
      w,
      is1 = true,
      r2 = nbi(),
      t;
    i = nbits(e[j]) - 1;
    while (j >= 0) {
      if (i >= k1) w = (e[j] >> (i - k1)) & km;
      else {
        w = (e[j] & ((1 << (i + 1)) - 1)) << (k1 - i);
        if (j > 0) w |= e[j - 1] >> (this.DB + i - k1);
      }
      n = k;
      while ((w & 1) == 0) {
        w >>= 1;
        --n;
      }
      if ((i -= n) < 0) {
        i += this.DB;
        --j;
      }
      if (is1) {
        // ret == 1, don't bother squaring or multiplying it
        g[w].copyTo(r);
        is1 = false;
      } else {
        while (n > 1) {
          z.sqrTo(r, r2);
          z.sqrTo(r2, r);
          n -= 2;
        }
        if (n > 0) z.sqrTo(r, r2);
        else {
          t = r;
          r = r2;
          r2 = t;
        }
        z.mulTo(r2, g[w], r);
      }
      while (j >= 0 && (e[j] & (1 << i)) == 0) {
        z.sqrTo(r, r2);
        t = r;
        r = r2;
        r2 = t;
        if (--i < 0) {
          i = this.DB - 1;
          --j;
        }
      }
    }
    return z.revert(r);
  }
  // (public) gcd(this,a) (HAC 14.54)
  function bnGCD(a) {
    var x = this.s < 0 ? this.negate() : this.clone();
    var y = a.s < 0 ? a.negate() : a.clone();
    if (x.compareTo(y) < 0) {
      var t = x;
      x = y;
      y = t;
    }
    var i = x.getLowestSetBit(),
      g = y.getLowestSetBit();
    if (g < 0) return x;
    if (i < g) g = i;
    if (g > 0) {
      x.rShiftTo(g, x);
      y.rShiftTo(g, y);
    }
    while (x.signum() > 0) {
      if ((i = x.getLowestSetBit()) > 0) x.rShiftTo(i, x);
      if ((i = y.getLowestSetBit()) > 0) y.rShiftTo(i, y);
      if (x.compareTo(y) >= 0) {
        x.subTo(y, x);
        x.rShiftTo(1, x);
      } else {
        y.subTo(x, y);
        y.rShiftTo(1, y);
      }
    }
    if (g > 0) y.lShiftTo(g, y);
    return y;
  }
  // (protected) this % n, n < 2^26
  function bnpModInt(n) {
    if (n <= 0) return 0;
    var d = this.DV % n,
      r = this.s < 0 ? n - 1 : 0;
    if (this.t > 0)
      if (d == 0) r = this[0] % n;
      else for (var i = this.t - 1; i >= 0; --i) r = (d * r + this[i]) % n;
    return r;
  }
  // (public) 1/this % m (HAC 14.61)
  function bnModInverse(m) {
    var ac = m.isEven();
    if ((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO;
    var u = m.clone(),
      v = this.clone();
    var a = nbv(1),
      b = nbv(0),
      c = nbv(0),
      d = nbv(1);
    while (u.signum() != 0) {
      while (u.isEven()) {
        u.rShiftTo(1, u);
        if (ac) {
          if (!a.isEven() || !b.isEven()) {
            a.addTo(this, a);
            b.subTo(m, b);
          }
          a.rShiftTo(1, a);
        } else if (!b.isEven()) b.subTo(m, b);
        b.rShiftTo(1, b);
      }
      while (v.isEven()) {
        v.rShiftTo(1, v);
        if (ac) {
          if (!c.isEven() || !d.isEven()) {
            c.addTo(this, c);
            d.subTo(m, d);
          }
          c.rShiftTo(1, c);
        } else if (!d.isEven()) d.subTo(m, d);
        d.rShiftTo(1, d);
      }
      if (u.compareTo(v) >= 0) {
        u.subTo(v, u);
        if (ac) a.subTo(c, a);
        b.subTo(d, b);
      } else {
        v.subTo(u, v);
        if (ac) c.subTo(a, c);
        d.subTo(b, d);
      }
    }
    if (v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO;
    if (d.compareTo(m) >= 0) return d.subtract(m);
    if (d.signum() < 0) d.addTo(m, d);
    else return d;
    if (d.signum() < 0) return d.add(m);
    else return d;
  }
  var lowprimes = [
    2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
    101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193,
    197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307,
    311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421,
    431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547,
    557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659,
    661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797,
    809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929,
    937, 941, 947, 953, 967, 971, 977, 983, 991, 997,
  ];
  var lplim = (1 << 26) / lowprimes[lowprimes.length - 1];
  // (public) test primality with certainty >= 1-.5^t
  function bnIsProbablePrime(t) {
    var i,
      x = this.abs();
    if (x.t == 1 && x[0] <= lowprimes[lowprimes.length - 1]) {
      for (i = 0; i < lowprimes.length; ++i) if (x[0] == lowprimes[i]) return true;
      return false;
    }
    if (x.isEven()) return false;
    i = 1;
    while (i < lowprimes.length) {
      var m = lowprimes[i],
        j = i + 1;
      while (j < lowprimes.length && m < lplim) m *= lowprimes[j++];
      m = x.modInt(m);
      while (i < j) if (m % lowprimes[i++] == 0) return false;
    }
    return x.millerRabin(t);
  }
  // (protected) true if probably prime (HAC 4.24, Miller-Rabin)
  function bnpMillerRabin(t) {
    var n1 = this.subtract(BigInteger.ONE);
    var k = n1.getLowestSetBit();
    if (k <= 0) return false;
    var r = n1.shiftRight(k);
    t = (t + 1) >> 1;
    if (t > lowprimes.length) t = lowprimes.length;
    var a = nbi();
    for (var i = 0; i < t; ++i) {
      //Pick bases at random, instead of starting at 2
      a.fromInt(lowprimes[Math.floor(Math.random() * lowprimes.length)]);
      var y = a.modPow(r, this);
      if (y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) {
        var j = 1;
        while (j++ < k && y.compareTo(n1) != 0) {
          y = y.modPowInt(2, this);
          if (y.compareTo(BigInteger.ONE) == 0) return false;
        }
        if (y.compareTo(n1) != 0) return false;
      }
    }
    return true;
  }
  // protected
  BigInteger.prototype.chunkSize = bnpChunkSize;
  BigInteger.prototype.toRadix = bnpToRadix;
  BigInteger.prototype.fromRadix = bnpFromRadix;
  BigInteger.prototype.fromNumber = bnpFromNumber;
  BigInteger.prototype.bitwiseTo = bnpBitwiseTo;
  BigInteger.prototype.changeBit = bnpChangeBit;
  BigInteger.prototype.addTo = bnpAddTo;
  BigInteger.prototype.dMultiply = bnpDMultiply;
  BigInteger.prototype.dAddOffset = bnpDAddOffset;
  BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo;
  BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo;
  BigInteger.prototype.modInt = bnpModInt;
  BigInteger.prototype.millerRabin = bnpMillerRabin;
  // public
  BigInteger.prototype.clone = bnClone;
  BigInteger.prototype.intValue = bnIntValue;
  BigInteger.prototype.byteValue = bnByteValue;
  BigInteger.prototype.shortValue = bnShortValue;
  BigInteger.prototype.signum = bnSigNum;
  BigInteger.prototype.toByteArray = bnToByteArray;
  BigInteger.prototype.equals = bnEquals;
  BigInteger.prototype.min = bnMin;
  BigInteger.prototype.max = bnMax;
  BigInteger.prototype.and = bnAnd;
  BigInteger.prototype.or = bnOr;
  BigInteger.prototype.xor = bnXor;
  BigInteger.prototype.andNot = bnAndNot;
  BigInteger.prototype.not = bnNot;
  BigInteger.prototype.shiftLeft = bnShiftLeft;
  BigInteger.prototype.shiftRight = bnShiftRight;
  BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit;
  BigInteger.prototype.bitCount = bnBitCount;
  BigInteger.prototype.testBit = bnTestBit;
  BigInteger.prototype.setBit = bnSetBit;
  BigInteger.prototype.clearBit = bnClearBit;
  BigInteger.prototype.flipBit = bnFlipBit;
  BigInteger.prototype.add = bnAdd;
  BigInteger.prototype.subtract = bnSubtract;
  BigInteger.prototype.multiply = bnMultiply;
  BigInteger.prototype.divide = bnDivide;
  BigInteger.prototype.remainder = bnRemainder;
  BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder;
  BigInteger.prototype.modPow = bnModPow;
  BigInteger.prototype.modInverse = bnModInverse;
  BigInteger.prototype.pow = bnPow;
  BigInteger.prototype.gcd = bnGCD;
  BigInteger.prototype.isProbablePrime = bnIsProbablePrime;
  // JSBN-specific extension
  BigInteger.prototype.square = bnSquare;
  var Int128 = BigInteger;
  // BigInteger interfaces not implemented in jsbn:
  // BigInteger(int signum, byte[] magnitude)
  // double doubleValue()
  // float floatValue()
  // int hashCode()
  // long longValue()
  // static BigInteger valueOf(long val)
  // Helper functions to make BigInteger functions callable with two parameters
  // as in original C# Clipper
  Int128.prototype.IsNegative = function () {
    if (this.compareTo(Int128.ZERO) == -1) return true;
    else return false;
  };

  Int128.op_Equality = function (val1, val2) {
    if (val1.compareTo(val2) == 0) return true;
    else return false;
  };

  Int128.op_Inequality = function (val1, val2) {
    if (val1.compareTo(val2) != 0) return true;
    else return false;
  };

  Int128.op_GreaterThan = function (val1, val2) {
    if (val1.compareTo(val2) > 0) return true;
    else return false;
  };

  Int128.op_LessThan = function (val1, val2) {
    if (val1.compareTo(val2) < 0) return true;
    else return false;
  };

  Int128.op_Addition = function (lhs, rhs) {
    return new Int128(lhs, undefined, undefined).add(new Int128(rhs, undefined, undefined));
  };

  Int128.op_Subtraction = function (lhs, rhs) {
    return new Int128(lhs, undefined, undefined).subtract(new Int128(rhs, undefined, undefined));
  };

  Int128.Int128Mul = function (lhs, rhs) {
    return new Int128(lhs, undefined, undefined).multiply(new Int128(rhs, undefined, undefined));
  };

  Int128.op_Division = function (lhs, rhs) {
    return lhs.divide(rhs);
  };

  Int128.prototype.ToDouble = function () {
    return parseFloat(this.toString()); // This could be something faster
  };

  // end of Int128 section
  /*
	// Uncomment the following two lines if you want to use Int128 outside ClipperLib
	if (typeof(document) !== "undefined") window.Int128 = Int128;
	else self.Int128 = Int128;
	*/

  // ---------------------------------------------

  // Here starts the actual Clipper library:
  // Helper function to support Inheritance in Javascript
  var Inherit = function (ce, ce2) {
    var p;
    if (typeof Object.getOwnPropertyNames === "undefined") {
      for (p in ce2.prototype)
        if (typeof ce.prototype[p] === "undefined" || ce.prototype[p] === Object.prototype[p])
          ce.prototype[p] = ce2.prototype[p];
      for (p in ce2) if (typeof ce[p] === "undefined") ce[p] = ce2[p];
      ce.$baseCtor = ce2;
    } else {
      var props = Object.getOwnPropertyNames(ce2.prototype);
      for (var i = 0; i < props.length; i++)
        if (typeof Object.getOwnPropertyDescriptor(ce.prototype, props[i]) === "undefined")
          Object.defineProperty(
            ce.prototype,
            props[i],
            Object.getOwnPropertyDescriptor(ce2.prototype, props[i]),
          );
      for (p in ce2) if (typeof ce[p] === "undefined") ce[p] = ce2[p];
      ce.$baseCtor = ce2;
    }
  };

  /**
   * @constructor
   */
  ClipperLib.Path = function () {
    return [];
  };

  ClipperLib.Path.prototype.push = Array.prototype.push;

  /**
   * @constructor
   */
  ClipperLib.Paths = function () {
    return []; // Was previously [[]], but caused problems when pushed
  };

  ClipperLib.Paths.prototype.push = Array.prototype.push;

  // Preserves the calling way of original C# Clipper
  // Is essential due to compatibility, because DoublePoint is public class in original C# version
  /**
   * @constructor
   */
  ClipperLib.DoublePoint = function () {
    var a = arguments;
    this.X = 0;
    this.Y = 0;
    // public DoublePoint(DoublePoint dp)
    // public DoublePoint(IntPoint ip)
    if (a.length === 1) {
      this.X = a[0].X;
      this.Y = a[0].Y;
    } else if (a.length === 2) {
      this.X = a[0];
      this.Y = a[1];
    }
  }; // This is internal faster function when called without arguments
  /**
   * @constructor
   */
  ClipperLib.DoublePoint0 = function () {
    this.X = 0;
    this.Y = 0;
  };

  ClipperLib.DoublePoint0.prototype = ClipperLib.DoublePoint.prototype;

  // This is internal faster function when called with 1 argument (dp or ip)
  /**
   * @constructor
   */
  ClipperLib.DoublePoint1 = function (dp) {
    this.X = dp.X;
    this.Y = dp.Y;
  };

  ClipperLib.DoublePoint1.prototype = ClipperLib.DoublePoint.prototype;

  // This is internal faster function when called with 2 arguments (x and y)
  /**
   * @constructor
   */
  ClipperLib.DoublePoint2 = function (x, y) {
    this.X = x;
    this.Y = y;
  };

  ClipperLib.DoublePoint2.prototype = ClipperLib.DoublePoint.prototype;

  // PolyTree & PolyNode start
  /**
   * @suppress {missingProperties}
   */
  ClipperLib.PolyNode = function () {
    this.m_Parent = null;
    this.m_polygon = new ClipperLib.Path();
    this.m_Index = 0;
    this.m_jointype = 0;
    this.m_endtype = 0;
    this.m_Childs = [];
    this.IsOpen = false;
  };

  ClipperLib.PolyNode.prototype.IsHoleNode = function () {
    var result = true;
    var node = this.m_Parent;
    while (node !== null) {
      result = !result;
      node = node.m_Parent;
    }
    return result;
  };

  ClipperLib.PolyNode.prototype.ChildCount = function () {
    return this.m_Childs.length;
  };

  ClipperLib.PolyNode.prototype.Contour = function () {
    return this.m_polygon;
  };

  ClipperLib.PolyNode.prototype.AddChild = function (Child) {
    var cnt = this.m_Childs.length;
    this.m_Childs.push(Child);
    Child.m_Parent = this;
    Child.m_Index = cnt;
  };

  ClipperLib.PolyNode.prototype.GetNext = function () {
    if (this.m_Childs.length > 0) return this.m_Childs[0];
    else return this.GetNextSiblingUp();
  };

  ClipperLib.PolyNode.prototype.GetNextSiblingUp = function () {
    if (this.m_Parent === null) return null;
    else if (this.m_Index === this.m_Parent.m_Childs.length - 1)
      return this.m_Parent.GetNextSiblingUp();
    else return this.m_Parent.m_Childs[this.m_Index + 1];
  };

  ClipperLib.PolyNode.prototype.Childs = function () {
    return this.m_Childs;
  };

  ClipperLib.PolyNode.prototype.Parent = function () {
    return this.m_Parent;
  };

  ClipperLib.PolyNode.prototype.IsHole = function () {
    return this.IsHoleNode();
  };

  // PolyTree : PolyNode
  /**
   * @suppress {missingProperties}
   * @constructor
   */
  ClipperLib.PolyTree = function () {
    this.m_AllPolys = [];
    ClipperLib.PolyNode.call(this);
  };

  ClipperLib.PolyTree.prototype.Clear = function () {
    for (var i = 0, ilen = this.m_AllPolys.length; i < ilen; i++) this.m_AllPolys[i] = null;
    this.m_AllPolys.length = 0;
    this.m_Childs.length = 0;
  };

  ClipperLib.PolyTree.prototype.GetFirst = function () {
    if (this.m_Childs.length > 0) return this.m_Childs[0];
    else return null;
  };

  ClipperLib.PolyTree.prototype.Total = function () {
    var result = this.m_AllPolys.length;
    //with negative offsets, ignore the hidden outer polygon ...
    if (result > 0 && this.m_Childs[0] !== this.m_AllPolys[0]) result--;
    return result;
  };

  Inherit(ClipperLib.PolyTree, ClipperLib.PolyNode);

  // PolyTree & PolyNode end

  ClipperLib.Math_Abs_Int64 =
    ClipperLib.Math_Abs_Int32 =
    ClipperLib.Math_Abs_Double =
      function (a) {
        return Math.abs(a);
      };

  ClipperLib.Math_Max_Int32_Int32 = function (a, b) {
    return Math.max(a, b);
  };

  /*
	-----------------------------------
	cast_32 speedtest: http://jsperf.com/truncate-float-to-integer/2
	-----------------------------------
	*/
  if (browser.msie || browser.opera || browser.safari)
    ClipperLib.Cast_Int32 = function (a) {
      return a | 0;
    };
  else
    ClipperLib.Cast_Int32 = function (a) {
      // eg. browser.chrome || browser.chromium || browser.firefox
      return ~~a;
    };

  /*
	--------------------------
	cast_64 speedtests: http://jsperf.com/truncate-float-to-integer
	Chrome: bitwise_not_floor
	Firefox17: toInteger (typeof test)
	IE9: bitwise_or_floor
	IE7 and IE8: to_parseint
	Chromium: to_floor_or_ceil
	Firefox3: to_floor_or_ceil
	Firefox15: to_floor_or_ceil
	Opera: to_floor_or_ceil
	Safari: to_floor_or_ceil
	--------------------------
	*/
  if (typeof Number.toInteger === "undefined") Number.toInteger = null;

  if (browser.chrome)
    ClipperLib.Cast_Int64 = function (a) {
      if (a < -2147483648 || a > 2147483647) return a < 0 ? Math.ceil(a) : Math.floor(a);
      else return ~~a;
    };
  else if (browser.firefox && typeof Number.toInteger === "function")
    ClipperLib.Cast_Int64 = function (a) {
      return Number.toInteger(a);
    };
  else if (browser.msie7 || browser.msie8)
    ClipperLib.Cast_Int64 = function (a) {
      return parseInt(a, 10);
    };
  else if (browser.msie)
    ClipperLib.Cast_Int64 = function (a) {
      if (a < -2147483648 || a > 2147483647) return a < 0 ? Math.ceil(a) : Math.floor(a);
      return a | 0;
    };

  // eg. browser.chromium || browser.firefox || browser.opera || browser.safari
  else
    ClipperLib.Cast_Int64 = function (a) {
      return a < 0 ? Math.ceil(a) : Math.floor(a);
    };

  ClipperLib.Clear = function (a) {
    a.length = 0;
  };

  //ClipperLib.MaxSteps = 64; // How many steps at maximum in arc in BuildArc() function
  ClipperLib.PI = 3.141592653589793;
  ClipperLib.PI2 = 2 * 3.141592653589793;
  /**
   * @constructor
   */
  ClipperLib.IntPoint = function () {
    var a = arguments,
      alen = a.length;
    this.X = 0;
    this.Y = 0;
    if (ClipperLib.use_xyz) {
      this.Z = 0;
      if (alen === 3) // public IntPoint(cInt x, cInt y, cInt z = 0)
      {
        this.X = a[0];
        this.Y = a[1];
        this.Z = a[2];
      } else if (alen === 2) // public IntPoint(cInt x, cInt y)
      {
        this.X = a[0];
        this.Y = a[1];
        this.Z = 0;
      } else if (alen === 1) {
        if (a[0] instanceof ClipperLib.DoublePoint) // public IntPoint(DoublePoint dp)
        {
          var dp = a[0];
          this.X = ClipperLib.Clipper.Round(dp.X);
          this.Y = ClipperLib.Clipper.Round(dp.Y);
          this.Z = 0;
        } else // public IntPoint(IntPoint pt)
        {
          var pt = a[0];
          if (typeof pt.Z === "undefined") pt.Z = 0;
          this.X = pt.X;
          this.Y = pt.Y;
          this.Z = pt.Z;
        }
      } else // public IntPoint()
      {
        this.X = 0;
        this.Y = 0;
        this.Z = 0;
      }
    } else // if (!ClipperLib.use_xyz)
    {
      if (alen === 2) // public IntPoint(cInt X, cInt Y)
      {
        this.X = a[0];
        this.Y = a[1];
      } else if (alen === 1) {
        if (a[0] instanceof ClipperLib.DoublePoint) // public IntPoint(DoublePoint dp)
        {
          var dp = a[0];
          this.X = ClipperLib.Clipper.Round(dp.X);
          this.Y = ClipperLib.Clipper.Round(dp.Y);
        } else // public IntPoint(IntPoint pt)
        {
          var pt = a[0];
          this.X = pt.X;
          this.Y = pt.Y;
        }
      } else // public IntPoint(IntPoint pt)
      {
        this.X = 0;
        this.Y = 0;
      }
    }
  };

  ClipperLib.IntPoint.op_Equality = function (a, b) {
    //return a == b;
    return a.X === b.X && a.Y === b.Y;
  };

  ClipperLib.IntPoint.op_Inequality = function (a, b) {
    //return a !== b;
    return a.X !== b.X || a.Y !== b.Y;
  };

  /*
  ClipperLib.IntPoint.prototype.Equals = function (obj)
  {
	if (obj === null)
		return false;
	if (obj instanceof ClipperLib.IntPoint)
	{
		var a = Cast(obj, ClipperLib.IntPoint);
		return (this.X == a.X) && (this.Y == a.Y);
	}
	else
		return false;
  };

	*/

  /**
   * @constructor
   */
  ClipperLib.IntPoint0 = function () {
    this.X = 0;
    this.Y = 0;
    if (ClipperLib.use_xyz) this.Z = 0;
  };

  ClipperLib.IntPoint0.prototype = ClipperLib.IntPoint.prototype;

  /**
   * @constructor
   */
  ClipperLib.IntPoint1 = function (pt) {
    this.X = pt.X;
    this.Y = pt.Y;
    if (ClipperLib.use_xyz) {
      if (typeof pt.Z === "undefined") this.Z = 0;
      else this.Z = pt.Z;
    }
  };

  ClipperLib.IntPoint1.prototype = ClipperLib.IntPoint.prototype;

  /**
   * @constructor
   */
  ClipperLib.IntPoint1dp = function (dp) {
    this.X = ClipperLib.Clipper.Round(dp.X);
    this.Y = ClipperLib.Clipper.Round(dp.Y);
    if (ClipperLib.use_xyz) this.Z = 0;
  };

  ClipperLib.IntPoint1dp.prototype = ClipperLib.IntPoint.prototype;

  /**
   * @constructor
   */
  ClipperLib.IntPoint2 = function (x, y, z) {
    this.X = x;
    this.Y = y;
    if (ClipperLib.use_xyz) {
      if (typeof z === "undefined") this.Z = 0;
      else this.Z = z;
    }
  };

  ClipperLib.IntPoint2.prototype = ClipperLib.IntPoint.prototype;

  /**
   * @constructor
   */
  ClipperLib.IntRect = function () {
    var a = arguments,
      alen = a.length;
    if (alen === 4) // function (l, t, r, b)
    {
      this.left = a[0];
      this.top = a[1];
      this.right = a[2];
      this.bottom = a[3];
    } else if (alen === 1) // function (ir)
    {
      var ir = a[0];
      this.left = ir.left;
      this.top = ir.top;
      this.right = ir.right;
      this.bottom = ir.bottom;
    } else // function ()
    {
      this.left = 0;
      this.top = 0;
      this.right = 0;
      this.bottom = 0;
    }
  };

  /**
   * @constructor
   */
  ClipperLib.IntRect0 = function () {
    this.left = 0;
    this.top = 0;
    this.right = 0;
    this.bottom = 0;
  };

  ClipperLib.IntRect0.prototype = ClipperLib.IntRect.prototype;

  /**
   * @constructor
   */
  ClipperLib.IntRect1 = function (ir) {
    this.left = ir.left;
    this.top = ir.top;
    this.right = ir.right;
    this.bottom = ir.bottom;
  };

  ClipperLib.IntRect1.prototype = ClipperLib.IntRect.prototype;

  /**
   * @constructor
   */
  ClipperLib.IntRect4 = function (l, t, r, b) {
    this.left = l;
    this.top = t;
    this.right = r;
    this.bottom = b;
  };

  ClipperLib.IntRect4.prototype = ClipperLib.IntRect.prototype;

  ClipperLib.ClipType = {
    ctIntersection: 0,
    ctUnion: 1,
    ctDifference: 2,
    ctXor: 3,
  };

  ClipperLib.PolyType = {
    ptSubject: 0,
    ptClip: 1,
  };

  ClipperLib.PolyFillType = {
    pftEvenOdd: 0,
    pftNonZero: 1,
    pftPositive: 2,
    pftNegative: 3,
  };

  ClipperLib.JoinType = {
    jtSquare: 0,
    jtRound: 1,
    jtMiter: 2,
  };

  ClipperLib.EndType = {
    etOpenSquare: 0,
    etOpenRound: 1,
    etOpenButt: 2,
    etClosedLine: 3,
    etClosedPolygon: 4,
  };

  ClipperLib.EdgeSide = {
    esLeft: 0,
    esRight: 1,
  };

  ClipperLib.Direction = {
    dRightToLeft: 0,
    dLeftToRight: 1,
  };

  /**
   * @constructor
   */
  ClipperLib.TEdge = function () {
    this.Bot = new ClipperLib.IntPoint0();
    this.Curr = new ClipperLib.IntPoint0(); //current (updated for every new scanbeam)
    this.Top = new ClipperLib.IntPoint0();
    this.Delta = new ClipperLib.IntPoint0();
    this.Dx = 0;
    this.PolyTyp = ClipperLib.PolyType.ptSubject;
    this.Side = ClipperLib.EdgeSide.esLeft; //side only refers to current side of solution poly
    this.WindDelta = 0; //1 or -1 depending on winding direction
    this.WindCnt = 0;
    this.WindCnt2 = 0; //winding count of the opposite polytype
    this.OutIdx = 0;
    this.Next = null;
    this.Prev = null;
    this.NextInLML = null;
    this.NextInAEL = null;
    this.PrevInAEL = null;
    this.NextInSEL = null;
    this.PrevInSEL = null;
  };

  /**
   * @constructor
   */
  ClipperLib.IntersectNode = function () {
    this.Edge1 = null;
    this.Edge2 = null;
    this.Pt = new ClipperLib.IntPoint0();
  };

  ClipperLib.MyIntersectNodeSort = function () {};

  ClipperLib.MyIntersectNodeSort.Compare = function (node1, node2) {
    var i = node2.Pt.Y - node1.Pt.Y;
    if (i > 0) return 1;
    else if (i < 0) return -1;
    else return 0;
  };

  /**
   * @constructor
   */
  ClipperLib.LocalMinima = function () {
    this.Y = 0;
    this.LeftBound = null;
    this.RightBound = null;
    this.Next = null;
  };

  /**
   * @constructor
   */
  ClipperLib.Scanbeam = function () {
    this.Y = 0;
    this.Next = null;
  };

  /**
   * @constructor
   */
  ClipperLib.Maxima = function () {
    this.X = 0;
    this.Next = null;
    this.Prev = null;
  };

  //OutRec: contains a path in the clipping solution. Edges in the AEL will
  //carry a pointer to an OutRec when they are part of the clipping solution.
  /**
   * @constructor
   */
  ClipperLib.OutRec = function () {
    this.Idx = 0;
    this.IsHole = false;
    this.IsOpen = false;
    this.FirstLeft = null; //see comments in clipper.pas
    this.Pts = null;
    this.BottomPt = null;
    this.PolyNode = null;
  };

  /**
   * @constructor
   */
  ClipperLib.OutPt = function () {
    this.Idx = 0;
    this.Pt = new ClipperLib.IntPoint0();
    this.Next = null;
    this.Prev = null;
  };

  /**
   * @constructor
   */
  ClipperLib.Join = function () {
    this.OutPt1 = null;
    this.OutPt2 = null;
    this.OffPt = new ClipperLib.IntPoint0();
  };

  ClipperLib.ClipperBase = function () {
    this.m_MinimaList = null;
    this.m_CurrentLM = null;
    this.m_edges = new Array();
    this.m_UseFullRange = false;
    this.m_HasOpenPaths = false;
    this.PreserveCollinear = false;
    this.m_Scanbeam = null;
    this.m_PolyOuts = null;
    this.m_ActiveEdges = null;
  };

  // Ranges are in original C# too high for Javascript (in current state 2013 september):
  // protected const double horizontal = -3.4E+38;
  // internal const cInt loRange = 0x3FFFFFFF; // = 1073741823 = sqrt(2^63 -1)/2
  // internal const cInt hiRange = 0x3FFFFFFFFFFFFFFFL; // = 4611686018427387903 = sqrt(2^127 -1)/2
  // So had to adjust them to more suitable for Javascript.
  // If JS some day supports truly 64-bit integers, then these ranges can be as in C#
  // and biginteger library can be more simpler (as then 128bit can be represented as two 64bit numbers)
  ClipperLib.ClipperBase.horizontal = -9007199254740992; //-2^53
  ClipperLib.ClipperBase.Skip = -2;
  ClipperLib.ClipperBase.Unassigned = -1;
  ClipperLib.ClipperBase.tolerance = 1e-20;
  ClipperLib.ClipperBase.loRange = 47453132; // sqrt(2^53 -1)/2
  ClipperLib.ClipperBase.hiRange = 4503599627370495; // sqrt(2^106 -1)/2

  ClipperLib.ClipperBase.near_zero = function (val) {
    return val > -ClipperLib.ClipperBase.tolerance && val < ClipperLib.ClipperBase.tolerance;
  };

  ClipperLib.ClipperBase.IsHorizontal = function (e) {
    return e.Delta.Y === 0;
  };

  ClipperLib.ClipperBase.prototype.PointIsVertex = function (pt, pp) {
    var pp2 = pp;
    do {
      if (ClipperLib.IntPoint.op_Equality(pp2.Pt, pt)) return true;
      pp2 = pp2.Next;
    } while (pp2 !== pp);
    return false;
  };

  ClipperLib.ClipperBase.prototype.PointOnLineSegment = function (
    pt,
    linePt1,
    linePt2,
    UseFullRange,
  ) {
    if (UseFullRange)
      return (
        (pt.X === linePt1.X && pt.Y === linePt1.Y) ||
        (pt.X === linePt2.X && pt.Y === linePt2.Y) ||
        (pt.X > linePt1.X === pt.X < linePt2.X &&
          pt.Y > linePt1.Y === pt.Y < linePt2.Y &&
          Int128.op_Equality(
            Int128.Int128Mul(pt.X - linePt1.X, linePt2.Y - linePt1.Y),
            Int128.Int128Mul(linePt2.X - linePt1.X, pt.Y - linePt1.Y),
          ))
      );
    else
      return (
        (pt.X === linePt1.X && pt.Y === linePt1.Y) ||
        (pt.X === linePt2.X && pt.Y === linePt2.Y) ||
        (pt.X > linePt1.X === pt.X < linePt2.X &&
          pt.Y > linePt1.Y === pt.Y < linePt2.Y &&
          (pt.X - linePt1.X) * (linePt2.Y - linePt1.Y) ===
            (linePt2.X - linePt1.X) * (pt.Y - linePt1.Y))
      );
  };

  ClipperLib.ClipperBase.prototype.PointOnPolygon = function (pt, pp, UseFullRange) {
    var pp2 = pp;
    while (true) {
      if (this.PointOnLineSegment(pt, pp2.Pt, pp2.Next.Pt, UseFullRange)) return true;
      pp2 = pp2.Next;
      if (pp2 === pp) break;
    }
    return false;
  };

  ClipperLib.ClipperBase.prototype.SlopesEqual = ClipperLib.ClipperBase.SlopesEqual = function () {
    var a = arguments,
      alen = a.length;
    var e1, e2, pt1, pt2, pt3, pt4, UseFullRange;
    if (alen === 3) // function (e1, e2, UseFullRange)
    {
      e1 = a[0];
      e2 = a[1];
      UseFullRange = a[2];
      if (UseFullRange)
        return Int128.op_Equality(
          Int128.Int128Mul(e1.Delta.Y, e2.Delta.X),
          Int128.Int128Mul(e1.Delta.X, e2.Delta.Y),
        );
      else
        return (
          ClipperLib.Cast_Int64(e1.Delta.Y * e2.Delta.X) ===
          ClipperLib.Cast_Int64(e1.Delta.X * e2.Delta.Y)
        );
    } else if (alen === 4) // function (pt1, pt2, pt3, UseFullRange)
    {
      pt1 = a[0];
      pt2 = a[1];
      pt3 = a[2];
      UseFullRange = a[3];
      if (UseFullRange)
        return Int128.op_Equality(
          Int128.Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X),
          Int128.Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y),
        );
      else
        return (
          ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt2.X - pt3.X)) -
            ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt2.Y - pt3.Y)) ===
          0
        );
    } else // function (pt1, pt2, pt3, pt4, UseFullRange)
    {
      pt1 = a[0];
      pt2 = a[1];
      pt3 = a[2];
      pt4 = a[3];
      UseFullRange = a[4];
      if (UseFullRange)
        return Int128.op_Equality(
          Int128.Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X),
          Int128.Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y),
        );
      else
        return (
          ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt3.X - pt4.X)) -
            ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt3.Y - pt4.Y)) ===
          0
        );
    }
  };

  ClipperLib.ClipperBase.SlopesEqual3 = function (e1, e2, UseFullRange) {
    if (UseFullRange)
      return Int128.op_Equality(
        Int128.Int128Mul(e1.Delta.Y, e2.Delta.X),
        Int128.Int128Mul(e1.Delta.X, e2.Delta.Y),
      );
    else
      return (
        ClipperLib.Cast_Int64(e1.Delta.Y * e2.Delta.X) ===
        ClipperLib.Cast_Int64(e1.Delta.X * e2.Delta.Y)
      );
  };

  ClipperLib.ClipperBase.SlopesEqual4 = function (pt1, pt2, pt3, UseFullRange) {
    if (UseFullRange)
      return Int128.op_Equality(
        Int128.Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X),
        Int128.Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y),
      );
    else
      return (
        ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt2.X - pt3.X)) -
          ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt2.Y - pt3.Y)) ===
        0
      );
  };

  ClipperLib.ClipperBase.SlopesEqual5 = function (pt1, pt2, pt3, pt4, UseFullRange) {
    if (UseFullRange)
      return Int128.op_Equality(
        Int128.Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X),
        Int128.Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y),
      );
    else
      return (
        ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt3.X - pt4.X)) -
          ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt3.Y - pt4.Y)) ===
        0
      );
  };

  ClipperLib.ClipperBase.prototype.Clear = function () {
    this.DisposeLocalMinimaList();
    for (var i = 0, ilen = this.m_edges.length; i < ilen; ++i) {
      for (var j = 0, jlen = this.m_edges[i].length; j < jlen; ++j) this.m_edges[i][j] = null;
      ClipperLib.Clear(this.m_edges[i]);
    }
    ClipperLib.Clear(this.m_edges);
    this.m_UseFullRange = false;
    this.m_HasOpenPaths = false;
  };

  ClipperLib.ClipperBase.prototype.DisposeLocalMinimaList = function () {
    while (this.m_MinimaList !== null) {
      var tmpLm = this.m_MinimaList.Next;
      this.m_MinimaList = null;
      this.m_MinimaList = tmpLm;
    }
    this.m_CurrentLM = null;
  };

  ClipperLib.ClipperBase.prototype.RangeTest = function (Pt, useFullRange) {
    if (useFullRange.Value) {
      if (
        Pt.X > ClipperLib.ClipperBase.hiRange ||
        Pt.Y > ClipperLib.ClipperBase.hiRange ||
        -Pt.X > ClipperLib.ClipperBase.hiRange ||
        -Pt.Y > ClipperLib.ClipperBase.hiRange
      )
        ClipperLib.Error("Coordinate outside allowed range in RangeTest().");
    } else if (
      Pt.X > ClipperLib.ClipperBase.loRange ||
      Pt.Y > ClipperLib.ClipperBase.loRange ||
      -Pt.X > ClipperLib.ClipperBase.loRange ||
      -Pt.Y > ClipperLib.ClipperBase.loRange
    ) {
      useFullRange.Value = true;
      this.RangeTest(Pt, useFullRange);
    }
  };

  ClipperLib.ClipperBase.prototype.InitEdge = function (e, eNext, ePrev, pt) {
    e.Next = eNext;
    e.Prev = ePrev;
    //e.Curr = pt;
    e.Curr.X = pt.X;
    e.Curr.Y = pt.Y;
    if (ClipperLib.use_xyz) e.Curr.Z = pt.Z;
    e.OutIdx = -1;
  };

  ClipperLib.ClipperBase.prototype.InitEdge2 = function (e, polyType) {
    if (e.Curr.Y >= e.Next.Curr.Y) {
      //e.Bot = e.Curr;
      e.Bot.X = e.Curr.X;
      e.Bot.Y = e.Curr.Y;
      if (ClipperLib.use_xyz) e.Bot.Z = e.Curr.Z;
      //e.Top = e.Next.Curr;
      e.Top.X = e.Next.Curr.X;
      e.Top.Y = e.Next.Curr.Y;
      if (ClipperLib.use_xyz) e.Top.Z = e.Next.Curr.Z;
    } else {
      //e.Top = e.Curr;
      e.Top.X = e.Curr.X;
      e.Top.Y = e.Curr.Y;
      if (ClipperLib.use_xyz) e.Top.Z = e.Curr.Z;
      //e.Bot = e.Next.Curr;
      e.Bot.X = e.Next.Curr.X;
      e.Bot.Y = e.Next.Curr.Y;
      if (ClipperLib.use_xyz) e.Bot.Z = e.Next.Curr.Z;
    }
    this.SetDx(e);
    e.PolyTyp = polyType;
  };

  ClipperLib.ClipperBase.prototype.FindNextLocMin = function (E) {
    var E2;
    for (;;) {
      while (
        ClipperLib.IntPoint.op_Inequality(E.Bot, E.Prev.Bot) ||
        ClipperLib.IntPoint.op_Equality(E.Curr, E.Top)
      )
        E = E.Next;
      if (
        E.Dx !== ClipperLib.ClipperBase.horizontal &&
        E.Prev.Dx !== ClipperLib.ClipperBase.horizontal
      )
        break;
      while (E.Prev.Dx === ClipperLib.ClipperBase.horizontal) E = E.Prev;
      E2 = E;
      while (E.Dx === ClipperLib.ClipperBase.horizontal) E = E.Next;
      if (E.Top.Y === E.Prev.Bot.Y) continue;
      //ie just an intermediate horz.
      if (E2.Prev.Bot.X < E.Bot.X) E = E2;
      break;
    }
    return E;
  };

  ClipperLib.ClipperBase.prototype.ProcessBound = function (E, LeftBoundIsForward) {
    var EStart;
    var Result = E;
    var Horz;

    if (Result.OutIdx === ClipperLib.ClipperBase.Skip) {
      //check if there are edges beyond the skip edge in the bound and if so
      //create another LocMin and calling ProcessBound once more ...
      E = Result;
      if (LeftBoundIsForward) {
        while (E.Top.Y === E.Next.Bot.Y) E = E.Next;
        while (E !== Result && E.Dx === ClipperLib.ClipperBase.horizontal) E = E.Prev;
      } else {
        while (E.Top.Y === E.Prev.Bot.Y) E = E.Prev;
        while (E !== Result && E.Dx === ClipperLib.ClipperBase.horizontal) E = E.Next;
      }
      if (E === Result) {
        if (LeftBoundIsForward) Result = E.Next;
        else Result = E.Prev;
      } else {
        //there are more edges in the bound beyond result starting with E
        if (LeftBoundIsForward) E = Result.Next;
        else E = Result.Prev;
        var locMin = new ClipperLib.LocalMinima();
        locMin.Next = null;
        locMin.Y = E.Bot.Y;
        locMin.LeftBound = null;
        locMin.RightBound = E;
        E.WindDelta = 0;
        Result = this.ProcessBound(E, LeftBoundIsForward);
        this.InsertLocalMinima(locMin);
      }
      return Result;
    }

    if (E.Dx === ClipperLib.ClipperBase.horizontal) {
      //We need to be careful with open paths because this may not be a
      //true local minima (ie E may be following a skip edge).
      //Also, consecutive horz. edges may start heading left before going right.
      if (LeftBoundIsForward) EStart = E.Prev;
      else EStart = E.Next;

      if (EStart.Dx === ClipperLib.ClipperBase.horizontal) //ie an adjoining horizontal skip edge
      {
        if (EStart.Bot.X !== E.Bot.X && EStart.Top.X !== E.Bot.X) this.ReverseHorizontal(E);
      } else if (EStart.Bot.X !== E.Bot.X) this.ReverseHorizontal(E);
    }

    EStart = E;
    if (LeftBoundIsForward) {
      while (
        Result.Top.Y === Result.Next.Bot.Y &&
        Result.Next.OutIdx !== ClipperLib.ClipperBase.Skip
      )
        Result = Result.Next;
      if (
        Result.Dx === ClipperLib.ClipperBase.horizontal &&
        Result.Next.OutIdx !== ClipperLib.ClipperBase.Skip
      ) {
        //nb: at the top of a bound, horizontals are added to the bound
        //only when the preceding edge attaches to the horizontal's left vertex
        //unless a Skip edge is encountered when that becomes the top divide
        Horz = Result;
        while (Horz.Prev.Dx === ClipperLib.ClipperBase.horizontal) Horz = Horz.Prev;
        if (Horz.Prev.Top.X > Result.Next.Top.X) Result = Horz.Prev;
      }
      while (E !== Result) {
        E.NextInLML = E.Next;
        if (E.Dx === ClipperLib.ClipperBase.horizontal && E !== EStart && E.Bot.X !== E.Prev.Top.X)
          this.ReverseHorizontal(E);
        E = E.Next;
      }
      if (E.Dx === ClipperLib.ClipperBase.horizontal && E !== EStart && E.Bot.X !== E.Prev.Top.X)
        this.ReverseHorizontal(E);
      Result = Result.Next;
      //move to the edge just beyond current bound
    } else {
      while (
        Result.Top.Y === Result.Prev.Bot.Y &&
        Result.Prev.OutIdx !== ClipperLib.ClipperBase.Skip
      )
        Result = Result.Prev;
      if (
        Result.Dx === ClipperLib.ClipperBase.horizontal &&
        Result.Prev.OutIdx !== ClipperLib.ClipperBase.Skip
      ) {
        Horz = Result;
        while (Horz.Next.Dx === ClipperLib.ClipperBase.horizontal) Horz = Horz.Next;
        if (Horz.Next.Top.X === Result.Prev.Top.X || Horz.Next.Top.X > Result.Prev.Top.X) {
          Result = Horz.Next;
        }
      }
      while (E !== Result) {
        E.NextInLML = E.Prev;
        if (E.Dx === ClipperLib.ClipperBase.horizontal && E !== EStart && E.Bot.X !== E.Next.Top.X)
          this.ReverseHorizontal(E);
        E = E.Prev;
      }
      if (E.Dx === ClipperLib.ClipperBase.horizontal && E !== EStart && E.Bot.X !== E.Next.Top.X)
        this.ReverseHorizontal(E);
      Result = Result.Prev;
      //move to the edge just beyond current bound
    }

    return Result;
  };

  ClipperLib.ClipperBase.prototype.AddPath = function (pg, polyType, Closed) {
    if (ClipperLib.use_lines) {
      if (!Closed && polyType === ClipperLib.PolyType.ptClip)
        ClipperLib.Error("AddPath: Open paths must be subject.");
    } else {
      if (!Closed) ClipperLib.Error("AddPath: Open paths have been disabled.");
    }
    var highI = pg.length - 1;
    if (Closed) while (highI > 0 && ClipperLib.IntPoint.op_Equality(pg[highI], pg[0])) --highI;
    while (highI > 0 && ClipperLib.IntPoint.op_Equality(pg[highI], pg[highI - 1])) --highI;
    if ((Closed && highI < 2) || (!Closed && highI < 1)) return false;
    //create a new edge array ...
    var edges = new Array();
    for (var i = 0; i <= highI; i++) edges.push(new ClipperLib.TEdge());
    var IsFlat = true;
    //1. Basic (first) edge initialization ...

    //edges[1].Curr = pg[1];
    edges[1].Curr.X = pg[1].X;
    edges[1].Curr.Y = pg[1].Y;
    if (ClipperLib.use_xyz) edges[1].Curr.Z = pg[1].Z;

    var $1 = {
      Value: this.m_UseFullRange,
    };

    this.RangeTest(pg[0], $1);
    this.m_UseFullRange = $1.Value;

    $1.Value = this.m_UseFullRange;
    this.RangeTest(pg[highI], $1);
    this.m_UseFullRange = $1.Value;

    this.InitEdge(edges[0], edges[1], edges[highI], pg[0]);
    this.InitEdge(edges[highI], edges[0], edges[highI - 1], pg[highI]);
    for (var i = highI - 1; i >= 1; --i) {
      $1.Value = this.m_UseFullRange;
      this.RangeTest(pg[i], $1);
      this.m_UseFullRange = $1.Value;

      this.InitEdge(edges[i], edges[i + 1], edges[i - 1], pg[i]);
    }

    var eStart = edges[0];
    //2. Remove duplicate vertices, and (when closed) collinear edges ...
    var E = eStart,
      eLoopStop = eStart;
    for (;;) {
      //console.log(E.Next, eStart);
      //nb: allows matching start and end points when not Closed ...
      if (E.Curr === E.Next.Curr && (Closed || E.Next !== eStart)) {
        if (E === E.Next) break;
        if (E === eStart) eStart = E.Next;
        E = this.RemoveEdge(E);
        eLoopStop = E;
        continue;
      }
      if (E.Prev === E.Next) break;
      else if (
        Closed &&
        ClipperLib.ClipperBase.SlopesEqual4(
          E.Prev.Curr,
          E.Curr,
          E.Next.Curr,
          this.m_UseFullRange,
        ) &&
        (!this.PreserveCollinear || !this.Pt2IsBetweenPt1AndPt3(E.Prev.Curr, E.Curr, E.Next.Curr))
      ) {
        //Collinear edges are allowed for open paths but in closed paths
        //the default is to merge adjacent collinear edges into a single edge.
        //However, if the PreserveCollinear property is enabled, only overlapping
        //collinear edges (ie spikes) will be removed from closed paths.
        if (E === eStart) eStart = E.Next;
        E = this.RemoveEdge(E);
        E = E.Prev;
        eLoopStop = E;
        continue;
      }
      E = E.Next;
      if (E === eLoopStop || (!Closed && E.Next === eStart)) break;
    }
    if ((!Closed && E === E.Next) || (Closed && E.Prev === E.Next)) return false;
    if (!Closed) {
      this.m_HasOpenPaths = true;
      eStart.Prev.OutIdx = ClipperLib.ClipperBase.Skip;
    }
    //3. Do second stage of edge initialization ...
    E = eStart;
    do {
      this.InitEdge2(E, polyType);
      E = E.Next;
      if (IsFlat && E.Curr.Y !== eStart.Curr.Y) IsFlat = false;
    } while (E !== eStart);
    //4. Finally, add edge bounds to LocalMinima list ...
    //Totally flat paths must be handled differently when adding them
    //to LocalMinima list to avoid endless loops etc ...
    if (IsFlat) {
      if (Closed) return false;

      E.Prev.OutIdx = ClipperLib.ClipperBase.Skip;

      var locMin = new ClipperLib.LocalMinima();
      locMin.Next = null;
      locMin.Y = E.Bot.Y;
      locMin.LeftBound = null;
      locMin.RightBound = E;
      locMin.RightBound.Side = ClipperLib.EdgeSide.esRight;
      locMin.RightBound.WindDelta = 0;

      for (;;) {
        if (E.Bot.X !== E.Prev.Top.X) this.ReverseHorizontal(E);
        if (E.Next.OutIdx === ClipperLib.ClipperBase.Skip) break;
        E.NextInLML = E.Next;
        E = E.Next;
      }
      this.InsertLocalMinima(locMin);
      this.m_edges.push(edges);
      return true;
    }
    this.m_edges.push(edges);
    var leftBoundIsForward;
    var EMin = null;

    //workaround to avoid an endless loop in the while loop below when
    //open paths have matching start and end points ...
    if (ClipperLib.IntPoint.op_Equality(E.Prev.Bot, E.Prev.Top)) E = E.Next;

    for (;;) {
      E = this.FindNextLocMin(E);
      if (E === EMin) break;
      else if (EMin === null) EMin = E;
      //E and E.Prev now share a local minima (left aligned if horizontal).
      //Compare their slopes to find which starts which bound ...
      var locMin = new ClipperLib.LocalMinima();
      locMin.Next = null;
      locMin.Y = E.Bot.Y;
      if (E.Dx < E.Prev.Dx) {
        locMin.LeftBound = E.Prev;
        locMin.RightBound = E;
        leftBoundIsForward = false;
        //Q.nextInLML = Q.prev
      } else {
        locMin.LeftBound = E;
        locMin.RightBound = E.Prev;
        leftBoundIsForward = true;
        //Q.nextInLML = Q.next
      }
      locMin.LeftBound.Side = ClipperLib.EdgeSide.esLeft;
      locMin.RightBound.Side = ClipperLib.EdgeSide.esRight;
      if (!Closed) locMin.LeftBound.WindDelta = 0;
      else if (locMin.LeftBound.Next === locMin.RightBound) locMin.LeftBound.WindDelta = -1;
      else locMin.LeftBound.WindDelta = 1;
      locMin.RightBound.WindDelta = -locMin.LeftBound.WindDelta;
      E = this.ProcessBound(locMin.LeftBound, leftBoundIsForward);
      if (E.OutIdx === ClipperLib.ClipperBase.Skip) E = this.ProcessBound(E, leftBoundIsForward);
      var E2 = this.ProcessBound(locMin.RightBound, !leftBoundIsForward);
      if (E2.OutIdx === ClipperLib.ClipperBase.Skip)
        E2 = this.ProcessBound(E2, !leftBoundIsForward);
      if (locMin.LeftBound.OutIdx === ClipperLib.ClipperBase.Skip) locMin.LeftBound = null;
      else if (locMin.RightBound.OutIdx === ClipperLib.ClipperBase.Skip) locMin.RightBound = null;
      this.InsertLocalMinima(locMin);
      if (!leftBoundIsForward) E = E2;
    }
    return true;
  };

  ClipperLib.ClipperBase.prototype.AddPaths = function (ppg, polyType, closed) {
    //  console.log("-------------------------------------------");
    //  console.log(JSON.stringify(ppg));
    var result = false;
    for (var i = 0, ilen = ppg.length; i < ilen; ++i)
      if (this.AddPath(ppg[i], polyType, closed)) result = true;
    return result;
  };

  ClipperLib.ClipperBase.prototype.Pt2IsBetweenPt1AndPt3 = function (pt1, pt2, pt3) {
    if (
      ClipperLib.IntPoint.op_Equality(pt1, pt3) ||
      ClipperLib.IntPoint.op_Equality(pt1, pt2) ||
      ClipperLib.IntPoint.op_Equality(pt3, pt2)
    )
      //if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2))
      return false;
    else if (pt1.X !== pt3.X) return pt2.X > pt1.X === pt2.X < pt3.X;
    else return pt2.Y > pt1.Y === pt2.Y < pt3.Y;
  };

  ClipperLib.ClipperBase.prototype.RemoveEdge = function (e) {
    //removes e from double_linked_list (but without removing from memory)
    e.Prev.Next = e.Next;
    e.Next.Prev = e.Prev;
    var result = e.Next;
    e.Prev = null; //flag as removed (see ClipperBase.Clear)
    return result;
  };

  ClipperLib.ClipperBase.prototype.SetDx = function (e) {
    e.Delta.X = e.Top.X - e.Bot.X;
    e.Delta.Y = e.Top.Y - e.Bot.Y;
    if (e.Delta.Y === 0) e.Dx = ClipperLib.ClipperBase.horizontal;
    else e.Dx = e.Delta.X / e.Delta.Y;
  };

  ClipperLib.ClipperBase.prototype.InsertLocalMinima = function (newLm) {
    if (this.m_MinimaList === null) {
      this.m_MinimaList = newLm;
    } else if (newLm.Y >= this.m_MinimaList.Y) {
      newLm.Next = this.m_MinimaList;
      this.m_MinimaList = newLm;
    } else {
      var tmpLm = this.m_MinimaList;
      while (tmpLm.Next !== null && newLm.Y < tmpLm.Next.Y) tmpLm = tmpLm.Next;
      newLm.Next = tmpLm.Next;
      tmpLm.Next = newLm;
    }
  };

  ClipperLib.ClipperBase.prototype.PopLocalMinima = function (Y, current) {
    current.v = this.m_CurrentLM;
    if (this.m_CurrentLM !== null && this.m_CurrentLM.Y === Y) {
      this.m_CurrentLM = this.m_CurrentLM.Next;
      return true;
    }
    return false;
  };

  ClipperLib.ClipperBase.prototype.ReverseHorizontal = function (e) {
    //swap horizontal edges' top and bottom x's so they follow the natural
    //progression of the bounds - ie so their xbots will align with the
    //adjoining lower edge. [Helpful in the ProcessHorizontal() method.]
    var tmp = e.Top.X;
    e.Top.X = e.Bot.X;
    e.Bot.X = tmp;
    if (ClipperLib.use_xyz) {
      tmp = e.Top.Z;
      e.Top.Z = e.Bot.Z;
      e.Bot.Z = tmp;
    }
  };

  ClipperLib.ClipperBase.prototype.Reset = function () {
    this.m_CurrentLM = this.m_MinimaList;
    if (this.m_CurrentLM === null)
      //ie nothing to process
      return;
    //reset all edges ...
    this.m_Scanbeam = null;
    var lm = this.m_MinimaList;
    while (lm !== null) {
      this.InsertScanbeam(lm.Y);
      var e = lm.LeftBound;
      if (e !== null) {
        //e.Curr = e.Bot;
        e.Curr.X = e.Bot.X;
        e.Curr.Y = e.Bot.Y;
        if (ClipperLib.use_xyz) e.Curr.Z = e.Bot.Z;
        e.OutIdx = ClipperLib.ClipperBase.Unassigned;
      }
      e = lm.RightBound;
      if (e !== null) {
        //e.Curr = e.Bot;
        e.Curr.X = e.Bot.X;
        e.Curr.Y = e.Bot.Y;
        if (ClipperLib.use_xyz) e.Curr.Z = e.Bot.Z;
        e.OutIdx = ClipperLib.ClipperBase.Unassigned;
      }
      lm = lm.Next;
    }
    this.m_ActiveEdges = null;
  };

  ClipperLib.ClipperBase.prototype.InsertScanbeam = function (Y) {
    //single-linked list: sorted descending, ignoring dups.
    if (this.m_Scanbeam === null) {
      this.m_Scanbeam = new ClipperLib.Scanbeam();
      this.m_Scanbeam.Next = null;
      this.m_Scanbeam.Y = Y;
    } else if (Y > this.m_Scanbeam.Y) {
      var newSb = new ClipperLib.Scanbeam();
      newSb.Y = Y;
      newSb.Next = this.m_Scanbeam;
      this.m_Scanbeam = newSb;
    } else {
      var sb2 = this.m_Scanbeam;
      while (sb2.Next !== null && Y <= sb2.Next.Y) {
        sb2 = sb2.Next;
      }
      if (Y === sb2.Y) {
        return;
      } //ie ignores duplicates
      var newSb1 = new ClipperLib.Scanbeam();
      newSb1.Y = Y;
      newSb1.Next = sb2.Next;
      sb2.Next = newSb1;
    }
  };

  ClipperLib.ClipperBase.prototype.PopScanbeam = function (Y) {
    if (this.m_Scanbeam === null) {
      Y.v = 0;
      return false;
    }
    Y.v = this.m_Scanbeam.Y;
    this.m_Scanbeam = this.m_Scanbeam.Next;
    return true;
  };

  ClipperLib.ClipperBase.prototype.LocalMinimaPending = function () {
    return this.m_CurrentLM !== null;
  };

  ClipperLib.ClipperBase.prototype.CreateOutRec = function () {
    var result = new ClipperLib.OutRec();
    result.Idx = ClipperLib.ClipperBase.Unassigned;
    result.IsHole = false;
    result.IsOpen = false;
    result.FirstLeft = null;
    result.Pts = null;
    result.BottomPt = null;
    result.PolyNode = null;
    this.m_PolyOuts.push(result);
    result.Idx = this.m_PolyOuts.length - 1;
    return result;
  };

  ClipperLib.ClipperBase.prototype.DisposeOutRec = function (index) {
    var outRec = this.m_PolyOuts[index];
    outRec.Pts = null;
    outRec = null;
    this.m_PolyOuts[index] = null;
  };

  ClipperLib.ClipperBase.prototype.UpdateEdgeIntoAEL = function (e) {
    if (e.NextInLML === null) {
      ClipperLib.Error("UpdateEdgeIntoAEL: invalid call");
    }
    var AelPrev = e.PrevInAEL;
    var AelNext = e.NextInAEL;
    e.NextInLML.OutIdx = e.OutIdx;
    if (AelPrev !== null) {
      AelPrev.NextInAEL = e.NextInLML;
    } else {
      this.m_ActiveEdges = e.NextInLML;
    }
    if (AelNext !== null) {
      AelNext.PrevInAEL = e.NextInLML;
    }
    e.NextInLML.Side = e.Side;
    e.NextInLML.WindDelta = e.WindDelta;
    e.NextInLML.WindCnt = e.WindCnt;
    e.NextInLML.WindCnt2 = e.WindCnt2;
    e = e.NextInLML;
    e.Curr.X = e.Bot.X;
    e.Curr.Y = e.Bot.Y;
    e.PrevInAEL = AelPrev;
    e.NextInAEL = AelNext;
    if (!ClipperLib.ClipperBase.IsHorizontal(e)) {
      this.InsertScanbeam(e.Top.Y);
    }
    return e;
  };

  ClipperLib.ClipperBase.prototype.SwapPositionsInAEL = function (edge1, edge2) {
    //check that one or other edge hasn't already been removed from AEL ...
    if (edge1.NextInAEL === edge1.PrevInAEL || edge2.NextInAEL === edge2.PrevInAEL) {
      return;
    }

    if (edge1.NextInAEL === edge2) {
      var next = edge2.NextInAEL;
      if (next !== null) {
        next.PrevInAEL = edge1;
      }
      var prev = edge1.PrevInAEL;
      if (prev !== null) {
        prev.NextInAEL = edge2;
      }
      edge2.PrevInAEL = prev;
      edge2.NextInAEL = edge1;
      edge1.PrevInAEL = edge2;
      edge1.NextInAEL = next;
    } else if (edge2.NextInAEL === edge1) {
      var next1 = edge1.NextInAEL;
      if (next1 !== null) {
        next1.PrevInAEL = edge2;
      }
      var prev1 = edge2.PrevInAEL;
      if (prev1 !== null) {
        prev1.NextInAEL = edge1;
      }
      edge1.PrevInAEL = prev1;
      edge1.NextInAEL = edge2;
      edge2.PrevInAEL = edge1;
      edge2.NextInAEL = next1;
    } else {
      var next2 = edge1.NextInAEL;
      var prev2 = edge1.PrevInAEL;
      edge1.NextInAEL = edge2.NextInAEL;
      if (edge1.NextInAEL !== null) {
        edge1.NextInAEL.PrevInAEL = edge1;
      }
      edge1.PrevInAEL = edge2.PrevInAEL;
      if (edge1.PrevInAEL !== null) {
        edge1.PrevInAEL.NextInAEL = edge1;
      }
      edge2.NextInAEL = next2;
      if (edge2.NextInAEL !== null) {
        edge2.NextInAEL.PrevInAEL = edge2;
      }
      edge2.PrevInAEL = prev2;
      if (edge2.PrevInAEL !== null) {
        edge2.PrevInAEL.NextInAEL = edge2;
      }
    }

    if (edge1.PrevInAEL === null) {
      this.m_ActiveEdges = edge1;
    } else {
      if (edge2.PrevInAEL === null) {
        this.m_ActiveEdges = edge2;
      }
    }
  };

  ClipperLib.ClipperBase.prototype.DeleteFromAEL = function (e) {
    var AelPrev = e.PrevInAEL;
    var AelNext = e.NextInAEL;
    if (AelPrev === null && AelNext === null && e !== this.m_ActiveEdges) {
      return;
    } //already deleted
    if (AelPrev !== null) {
      AelPrev.NextInAEL = AelNext;
    } else {
      this.m_ActiveEdges = AelNext;
    }
    if (AelNext !== null) {
      AelNext.PrevInAEL = AelPrev;
    }
    e.NextInAEL = null;
    e.PrevInAEL = null;
  };

  // public Clipper(int InitOptions = 0)
  /**
   * @suppress {missingProperties}
   */
  ClipperLib.Clipper = function (InitOptions) {
    if (typeof InitOptions === "undefined") InitOptions = 0;
    this.m_PolyOuts = null;
    this.m_ClipType = ClipperLib.ClipType.ctIntersection;
    this.m_Scanbeam = null;
    this.m_Maxima = null;
    this.m_ActiveEdges = null;
    this.m_SortedEdges = null;
    this.m_IntersectList = null;
    this.m_IntersectNodeComparer = null;
    this.m_ExecuteLocked = false;
    this.m_ClipFillType = ClipperLib.PolyFillType.pftEvenOdd;
    this.m_SubjFillType = ClipperLib.PolyFillType.pftEvenOdd;
    this.m_Joins = null;
    this.m_GhostJoins = null;
    this.m_UsingPolyTree = false;
    this.ReverseSolution = false;
    this.StrictlySimple = false;

    ClipperLib.ClipperBase.call(this);

    this.m_Scanbeam = null;
    this.m_Maxima = null;
    this.m_ActiveEdges = null;
    this.m_SortedEdges = null;
    this.m_IntersectList = new Array();
    this.m_IntersectNodeComparer = ClipperLib.MyIntersectNodeSort.Compare;
    this.m_ExecuteLocked = false;
    this.m_UsingPolyTree = false;
    this.m_PolyOuts = new Array();
    this.m_Joins = new Array();
    this.m_GhostJoins = new Array();
    this.ReverseSolution = (1 & InitOptions) !== 0;
    this.StrictlySimple = (2 & InitOptions) !== 0;
    this.PreserveCollinear = (4 & InitOptions) !== 0;
    if (ClipperLib.use_xyz) {
      this.ZFillFunction = null; // function (IntPoint vert1, IntPoint vert2, ref IntPoint intersectPt);
    }
  };

  ClipperLib.Clipper.ioReverseSolution = 1;
  ClipperLib.Clipper.ioStrictlySimple = 2;
  ClipperLib.Clipper.ioPreserveCollinear = 4;

  ClipperLib.Clipper.prototype.Clear = function () {
    if (this.m_edges.length === 0) return;
    //avoids problems with ClipperBase destructor
    this.DisposeAllPolyPts();
    ClipperLib.ClipperBase.prototype.Clear.call(this);
  };

  ClipperLib.Clipper.prototype.InsertMaxima = function (X) {
    //double-linked list: sorted ascending, ignoring dups.
    var newMax = new ClipperLib.Maxima();
    newMax.X = X;
    if (this.m_Maxima === null) {
      this.m_Maxima = newMax;
      this.m_Maxima.Next = null;
      this.m_Maxima.Prev = null;
    } else if (X < this.m_Maxima.X) {
      newMax.Next = this.m_Maxima;
      newMax.Prev = null;
      this.m_Maxima = newMax;
    } else {
      var m = this.m_Maxima;
      while (m.Next !== null && X >= m.Next.X) {
        m = m.Next;
      }
      if (X === m.X) {
        return;
      } //ie ignores duplicates (& CG to clean up newMax)
      //insert newMax between m and m.Next ...
      newMax.Next = m.Next;
      newMax.Prev = m;
      if (m.Next !== null) {
        m.Next.Prev = newMax;
      }
      m.Next = newMax;
    }
  };

  // ************************************
  ClipperLib.Clipper.prototype.Execute = function () {
    var a = arguments,
      alen = a.length,
      ispolytree = a[1] instanceof ClipperLib.PolyTree;
    if (alen === 4 && !ispolytree) // function (clipType, solution, subjFillType, clipFillType)
    {
      var clipType = a[0],
        solution = a[1],
        subjFillType = a[2],
        clipFillType = a[3];
      if (this.m_ExecuteLocked) return false;
      if (this.m_HasOpenPaths)
        ClipperLib.Error("Error: PolyTree struct is needed for open path clipping.");
      this.m_ExecuteLocked = true;
      ClipperLib.Clear(solution);
      this.m_SubjFillType = subjFillType;
      this.m_ClipFillType = clipFillType;
      this.m_ClipType = clipType;
      this.m_UsingPolyTree = false;
      try {
        var succeeded = this.ExecuteInternal();
        //build the return polygons ...
        if (succeeded) this.BuildResult(solution);
      } finally {
        this.DisposeAllPolyPts();
        this.m_ExecuteLocked = false;
      }
      return succeeded;
    } else if (
      alen === 4 &&
      ispolytree
    ) // function (clipType, polytree, subjFillType, clipFillType)
    {
      var clipType = a[0],
        polytree = a[1],
        subjFillType = a[2],
        clipFillType = a[3];
      if (this.m_ExecuteLocked) return false;
      this.m_ExecuteLocked = true;
      this.m_SubjFillType = subjFillType;
      this.m_ClipFillType = clipFillType;
      this.m_ClipType = clipType;
      this.m_UsingPolyTree = true;
      try {
        var succeeded = this.ExecuteInternal();
        //build the return polygons ...
        if (succeeded) this.BuildResult2(polytree);
      } finally {
        this.DisposeAllPolyPts();
        this.m_ExecuteLocked = false;
      }
      return succeeded;
    } else if (alen === 2 && !ispolytree) // function (clipType, solution)
    {
      var clipType = a[0],
        solution = a[1];
      return this.Execute(
        clipType,
        solution,
        ClipperLib.PolyFillType.pftEvenOdd,
        ClipperLib.PolyFillType.pftEvenOdd,
      );
    } else if (alen === 2 && ispolytree) // function (clipType, polytree)
    {
      var clipType = a[0],
        polytree = a[1];
      return this.Execute(
        clipType,
        polytree,
        ClipperLib.PolyFillType.pftEvenOdd,
        ClipperLib.PolyFillType.pftEvenOdd,
      );
    }
  };

  ClipperLib.Clipper.prototype.FixHoleLinkage = function (outRec) {
    //skip if an outermost polygon or
    //already already points to the correct FirstLeft ...
    if (
      outRec.FirstLeft === null ||
      (outRec.IsHole !== outRec.FirstLeft.IsHole && outRec.FirstLeft.Pts !== null)
    )
      return;
    var orfl = outRec.FirstLeft;
    while (orfl !== null && (orfl.IsHole === outRec.IsHole || orfl.Pts === null))
      orfl = orfl.FirstLeft;
    outRec.FirstLeft = orfl;
  };

  ClipperLib.Clipper.prototype.ExecuteInternal = function () {
    try {
      this.Reset();
      this.m_SortedEdges = null;
      this.m_Maxima = null;

      var botY = {},
        topY = {};

      if (!this.PopScanbeam(botY)) {
        return false;
      }
      this.InsertLocalMinimaIntoAEL(botY.v);
      while (this.PopScanbeam(topY) || this.LocalMinimaPending()) {
        this.ProcessHorizontals();
        this.m_GhostJoins.length = 0;
        if (!this.ProcessIntersections(topY.v)) {
          return false;
        }
        this.ProcessEdgesAtTopOfScanbeam(topY.v);
        botY.v = topY.v;
        this.InsertLocalMinimaIntoAEL(botY.v);
      }

      //fix orientations ...
      var outRec, i, ilen;
      //fix orientations ...
      for (i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) {
        outRec = this.m_PolyOuts[i];
        if (outRec.Pts === null || outRec.IsOpen) continue;
        if ((outRec.IsHole ^ this.ReverseSolution) == this.Area$1(outRec) > 0)
          this.ReversePolyPtLinks(outRec.Pts);
      }

      this.JoinCommonEdges();

      for (i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) {
        outRec = this.m_PolyOuts[i];
        if (outRec.Pts === null) continue;
        else if (outRec.IsOpen) this.FixupOutPolyline(outRec);
        else this.FixupOutPolygon(outRec);
      }

      if (this.StrictlySimple) this.DoSimplePolygons();
      return true;
    } finally {
      //catch { return false; }
      this.m_Joins.length = 0;
      this.m_GhostJoins.length = 0;
    }
  };

  ClipperLib.Clipper.prototype.DisposeAllPolyPts = function () {
    for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; ++i) this.DisposeOutRec(i);
    ClipperLib.Clear(this.m_PolyOuts);
  };

  ClipperLib.Clipper.prototype.AddJoin = function (Op1, Op2, OffPt) {
    var j = new ClipperLib.Join();
    j.OutPt1 = Op1;
    j.OutPt2 = Op2;
    //j.OffPt = OffPt;
    j.OffPt.X = OffPt.X;
    j.OffPt.Y = OffPt.Y;
    if (ClipperLib.use_xyz) j.OffPt.Z = OffPt.Z;
    this.m_Joins.push(j);
  };

  ClipperLib.Clipper.prototype.AddGhostJoin = function (Op, OffPt) {
    var j = new ClipperLib.Join();
    j.OutPt1 = Op;
    //j.OffPt = OffPt;
    j.OffPt.X = OffPt.X;
    j.OffPt.Y = OffPt.Y;
    if (ClipperLib.use_xyz) j.OffPt.Z = OffPt.Z;
    this.m_GhostJoins.push(j);
  };

  //if (ClipperLib.use_xyz)
  //{
  ClipperLib.Clipper.prototype.SetZ = function (pt, e1, e2) {
    if (this.ZFillFunction !== null) {
      if (pt.Z !== 0 || this.ZFillFunction === null) return;
      else if (ClipperLib.IntPoint.op_Equality(pt, e1.Bot)) pt.Z = e1.Bot.Z;
      else if (ClipperLib.IntPoint.op_Equality(pt, e1.Top)) pt.Z = e1.Top.Z;
      else if (ClipperLib.IntPoint.op_Equality(pt, e2.Bot)) pt.Z = e2.Bot.Z;
      else if (ClipperLib.IntPoint.op_Equality(pt, e2.Top)) pt.Z = e2.Top.Z;
      else this.ZFillFunction(e1.Bot, e1.Top, e2.Bot, e2.Top, pt);
    }
  };
  //}

  ClipperLib.Clipper.prototype.InsertLocalMinimaIntoAEL = function (botY) {
    var lm = {};

    var lb;
    var rb;
    while (this.PopLocalMinima(botY, lm)) {
      lb = lm.v.LeftBound;
      rb = lm.v.RightBound;

      var Op1 = null;
      if (lb === null) {
        this.InsertEdgeIntoAEL(rb, null);
        this.SetWindingCount(rb);
        if (this.IsContributing(rb)) Op1 = this.AddOutPt(rb, rb.Bot);
      } else if (rb === null) {
        this.InsertEdgeIntoAEL(lb, null);
        this.SetWindingCount(lb);
        if (this.IsContributing(lb)) Op1 = this.AddOutPt(lb, lb.Bot);
        this.InsertScanbeam(lb.Top.Y);
      } else {
        this.InsertEdgeIntoAEL(lb, null);
        this.InsertEdgeIntoAEL(rb, lb);
        this.SetWindingCount(lb);
        rb.WindCnt = lb.WindCnt;
        rb.WindCnt2 = lb.WindCnt2;
        if (this.IsContributing(lb)) Op1 = this.AddLocalMinPoly(lb, rb, lb.Bot);
        this.InsertScanbeam(lb.Top.Y);
      }
      if (rb !== null) {
        if (ClipperLib.ClipperBase.IsHorizontal(rb)) {
          if (rb.NextInLML !== null) {
            this.InsertScanbeam(rb.NextInLML.Top.Y);
          }
          this.AddEdgeToSEL(rb);
        } else {
          this.InsertScanbeam(rb.Top.Y);
        }
      }
      if (lb === null || rb === null) continue;
      //if output polygons share an Edge with a horizontal rb, they'll need joining later ...
      if (
        Op1 !== null &&
        ClipperLib.ClipperBase.IsHorizontal(rb) &&
        this.m_GhostJoins.length > 0 &&
        rb.WindDelta !== 0
      ) {
        for (var i = 0, ilen = this.m_GhostJoins.length; i < ilen; i++) {
          //if the horizontal Rb and a 'ghost' horizontal overlap, then convert
          //the 'ghost' join to a real join ready for later ...
          var j = this.m_GhostJoins[i];

          if (this.HorzSegmentsOverlap(j.OutPt1.Pt.X, j.OffPt.X, rb.Bot.X, rb.Top.X))
            this.AddJoin(j.OutPt1, Op1, j.OffPt);
        }
      }

      if (
        lb.OutIdx >= 0 &&
        lb.PrevInAEL !== null &&
        lb.PrevInAEL.Curr.X === lb.Bot.X &&
        lb.PrevInAEL.OutIdx >= 0 &&
        ClipperLib.ClipperBase.SlopesEqual5(
          lb.PrevInAEL.Curr,
          lb.PrevInAEL.Top,
          lb.Curr,
          lb.Top,
          this.m_UseFullRange,
        ) &&
        lb.WindDelta !== 0 &&
        lb.PrevInAEL.WindDelta !== 0
      ) {
        var Op2 = this.AddOutPt(lb.PrevInAEL, lb.Bot);
        this.AddJoin(Op1, Op2, lb.Top);
      }
      if (lb.NextInAEL !== rb) {
        if (
          rb.OutIdx >= 0 &&
          rb.PrevInAEL.OutIdx >= 0 &&
          ClipperLib.ClipperBase.SlopesEqual5(
            rb.PrevInAEL.Curr,
            rb.PrevInAEL.Top,
            rb.Curr,
            rb.Top,
            this.m_UseFullRange,
          ) &&
          rb.WindDelta !== 0 &&
          rb.PrevInAEL.WindDelta !== 0
        ) {
          var Op2 = this.AddOutPt(rb.PrevInAEL, rb.Bot);
          this.AddJoin(Op1, Op2, rb.Top);
        }
        var e = lb.NextInAEL;
        if (e !== null)
          while (e !== rb) {
            //nb: For calculating winding counts etc, IntersectEdges() assumes
            //that param1 will be to the right of param2 ABOVE the intersection ...
            this.IntersectEdges(rb, e, lb.Curr);
            //order important here
            e = e.NextInAEL;
          }
      }
    }
  };

  ClipperLib.Clipper.prototype.InsertEdgeIntoAEL = function (edge, startEdge) {
    if (this.m_ActiveEdges === null) {
      edge.PrevInAEL = null;
      edge.NextInAEL = null;
      this.m_ActiveEdges = edge;
    } else if (startEdge === null && this.E2InsertsBeforeE1(this.m_ActiveEdges, edge)) {
      edge.PrevInAEL = null;
      edge.NextInAEL = this.m_ActiveEdges;
      this.m_ActiveEdges.PrevInAEL = edge;
      this.m_ActiveEdges = edge;
    } else {
      if (startEdge === null) startEdge = this.m_ActiveEdges;
      while (startEdge.NextInAEL !== null && !this.E2InsertsBeforeE1(startEdge.NextInAEL, edge))
        startEdge = startEdge.NextInAEL;
      edge.NextInAEL = startEdge.NextInAEL;
      if (startEdge.NextInAEL !== null) startEdge.NextInAEL.PrevInAEL = edge;
      edge.PrevInAEL = startEdge;
      startEdge.NextInAEL = edge;
    }
  };

  ClipperLib.Clipper.prototype.E2InsertsBeforeE1 = function (e1, e2) {
    if (e2.Curr.X === e1.Curr.X) {
      if (e2.Top.Y > e1.Top.Y) return e2.Top.X < ClipperLib.Clipper.TopX(e1, e2.Top.Y);
      else return e1.Top.X > ClipperLib.Clipper.TopX(e2, e1.Top.Y);
    } else return e2.Curr.X < e1.Curr.X;
  };

  ClipperLib.Clipper.prototype.IsEvenOddFillType = function (edge) {
    if (edge.PolyTyp === ClipperLib.PolyType.ptSubject)
      return this.m_SubjFillType === ClipperLib.PolyFillType.pftEvenOdd;
    else return this.m_ClipFillType === ClipperLib.PolyFillType.pftEvenOdd;
  };

  ClipperLib.Clipper.prototype.IsEvenOddAltFillType = function (edge) {
    if (edge.PolyTyp === ClipperLib.PolyType.ptSubject)
      return this.m_ClipFillType === ClipperLib.PolyFillType.pftEvenOdd;
    else return this.m_SubjFillType === ClipperLib.PolyFillType.pftEvenOdd;
  };

  ClipperLib.Clipper.prototype.IsContributing = function (edge) {
    var pft, pft2;
    if (edge.PolyTyp === ClipperLib.PolyType.ptSubject) {
      pft = this.m_SubjFillType;
      pft2 = this.m_ClipFillType;
    } else {
      pft = this.m_ClipFillType;
      pft2 = this.m_SubjFillType;
    }
    switch (pft) {
      case ClipperLib.PolyFillType.pftEvenOdd:
        if (edge.WindDelta === 0 && edge.WindCnt !== 1) return false;
        break;
      case ClipperLib.PolyFillType.pftNonZero:
        if (Math.abs(edge.WindCnt) !== 1) return false;
        break;
      case ClipperLib.PolyFillType.pftPositive:
        if (edge.WindCnt !== 1) return false;
        break;
      default:
        if (edge.WindCnt !== -1) return false;
        break;
    }
    switch (this.m_ClipType) {
      case ClipperLib.ClipType.ctIntersection:
        switch (pft2) {
          case ClipperLib.PolyFillType.pftEvenOdd:
          case ClipperLib.PolyFillType.pftNonZero:
            return edge.WindCnt2 !== 0;
          case ClipperLib.PolyFillType.pftPositive:
            return edge.WindCnt2 > 0;
          default:
            return edge.WindCnt2 < 0;
        }
      case ClipperLib.ClipType.ctUnion:
        switch (pft2) {
          case ClipperLib.PolyFillType.pftEvenOdd:
          case ClipperLib.PolyFillType.pftNonZero:
            return edge.WindCnt2 === 0;
          case ClipperLib.PolyFillType.pftPositive:
            return edge.WindCnt2 <= 0;
          default:
            return edge.WindCnt2 >= 0;
        }
      case ClipperLib.ClipType.ctDifference:
        if (edge.PolyTyp === ClipperLib.PolyType.ptSubject)
          switch (pft2) {
            case ClipperLib.PolyFillType.pftEvenOdd:
            case ClipperLib.PolyFillType.pftNonZero:
              return edge.WindCnt2 === 0;
            case ClipperLib.PolyFillType.pftPositive:
              return edge.WindCnt2 <= 0;
            default:
              return edge.WindCnt2 >= 0;
          }
        else
          switch (pft2) {
            case ClipperLib.PolyFillType.pftEvenOdd:
            case ClipperLib.PolyFillType.pftNonZero:
              return edge.WindCnt2 !== 0;
            case ClipperLib.PolyFillType.pftPositive:
              return edge.WindCnt2 > 0;
            default:
              return edge.WindCnt2 < 0;
          }
      case ClipperLib.ClipType.ctXor:
        if (edge.WindDelta === 0)
          switch (pft2) {
            case ClipperLib.PolyFillType.pftEvenOdd:
            case ClipperLib.PolyFillType.pftNonZero:
              return edge.WindCnt2 === 0;
            case ClipperLib.PolyFillType.pftPositive:
              return edge.WindCnt2 <= 0;
            default:
              return edge.WindCnt2 >= 0;
          }
        else return true;
    }
    return true;
  };

  ClipperLib.Clipper.prototype.SetWindingCount = function (edge) {
    var e = edge.PrevInAEL;
    //find the edge of the same polytype that immediately preceeds 'edge' in AEL
    while (e !== null && (e.PolyTyp !== edge.PolyTyp || e.WindDelta === 0)) e = e.PrevInAEL;
    if (e === null) {
      var pft =
        edge.PolyTyp === ClipperLib.PolyType.ptSubject ? this.m_SubjFillType : this.m_ClipFillType;
      if (edge.WindDelta === 0) {
        edge.WindCnt = pft === ClipperLib.PolyFillType.pftNegative ? -1 : 1;
      } else {
        edge.WindCnt = edge.WindDelta;
      }
      edge.WindCnt2 = 0;
      e = this.m_ActiveEdges;
      //ie get ready to calc WindCnt2
    } else if (edge.WindDelta === 0 && this.m_ClipType !== ClipperLib.ClipType.ctUnion) {
      edge.WindCnt = 1;
      edge.WindCnt2 = e.WindCnt2;
      e = e.NextInAEL;
      //ie get ready to calc WindCnt2
    } else if (this.IsEvenOddFillType(edge)) {
      //EvenOdd filling ...
      if (edge.WindDelta === 0) {
        //are we inside a subj polygon ...
        var Inside = true;
        var e2 = e.PrevInAEL;
        while (e2 !== null) {
          if (e2.PolyTyp === e.PolyTyp && e2.WindDelta !== 0) Inside = !Inside;
          e2 = e2.PrevInAEL;
        }
        edge.WindCnt = Inside ? 0 : 1;
      } else {
        edge.WindCnt = edge.WindDelta;
      }
      edge.WindCnt2 = e.WindCnt2;
      e = e.NextInAEL;
      //ie get ready to calc WindCnt2
    } else {
      //nonZero, Positive or Negative filling ...
      if (e.WindCnt * e.WindDelta < 0) {
        //prev edge is 'decreasing' WindCount (WC) toward zero
        //so we're outside the previous polygon ...
        if (Math.abs(e.WindCnt) > 1) {
          //outside prev poly but still inside another.
          //when reversing direction of prev poly use the same WC
          if (e.WindDelta * edge.WindDelta < 0) edge.WindCnt = e.WindCnt;
          else edge.WindCnt = e.WindCnt + edge.WindDelta;
        } else edge.WindCnt = edge.WindDelta === 0 ? 1 : edge.WindDelta;
      } else {
        //prev edge is 'increasing' WindCount (WC) away from zero
        //so we're inside the previous polygon ...
        if (edge.WindDelta === 0) edge.WindCnt = e.WindCnt < 0 ? e.WindCnt - 1 : e.WindCnt + 1;
        else if (e.WindDelta * edge.WindDelta < 0) edge.WindCnt = e.WindCnt;
        else edge.WindCnt = e.WindCnt + edge.WindDelta;
      }
      edge.WindCnt2 = e.WindCnt2;
      e = e.NextInAEL;
      //ie get ready to calc WindCnt2
    }
    //update WindCnt2 ...
    if (this.IsEvenOddAltFillType(edge)) {
      //EvenOdd filling ...
      while (e !== edge) {
        if (e.WindDelta !== 0) edge.WindCnt2 = edge.WindCnt2 === 0 ? 1 : 0;
        e = e.NextInAEL;
      }
    } else {
      //nonZero, Positive or Negative filling ...
      while (e !== edge) {
        edge.WindCnt2 += e.WindDelta;
        e = e.NextInAEL;
      }
    }
  };

  ClipperLib.Clipper.prototype.AddEdgeToSEL = function (edge) {
    //SEL pointers in PEdge are use to build transient lists of horizontal edges.
    //However, since we don't need to worry about processing order, all additions
    //are made to the front of the list ...
    if (this.m_SortedEdges === null) {
      this.m_SortedEdges = edge;
      edge.PrevInSEL = null;
      edge.NextInSEL = null;
    } else {
      edge.NextInSEL = this.m_SortedEdges;
      edge.PrevInSEL = null;
      this.m_SortedEdges.PrevInSEL = edge;
      this.m_SortedEdges = edge;
    }
  };

  ClipperLib.Clipper.prototype.PopEdgeFromSEL = function (e) {
    //Pop edge from front of SEL (ie SEL is a FILO list)
    e.v = this.m_SortedEdges;
    if (e.v === null) {
      return false;
    }
    var oldE = e.v;
    this.m_SortedEdges = e.v.NextInSEL;
    if (this.m_SortedEdges !== null) {
      this.m_SortedEdges.PrevInSEL = null;
    }
    oldE.NextInSEL = null;
    oldE.PrevInSEL = null;
    return true;
  };

  ClipperLib.Clipper.prototype.CopyAELToSEL = function () {
    var e = this.m_ActiveEdges;
    this.m_SortedEdges = e;
    while (e !== null) {
      e.PrevInSEL = e.PrevInAEL;
      e.NextInSEL = e.NextInAEL;
      e = e.NextInAEL;
    }
  };

  ClipperLib.Clipper.prototype.SwapPositionsInSEL = function (edge1, edge2) {
    if (edge1.NextInSEL === null && edge1.PrevInSEL === null) return;
    if (edge2.NextInSEL === null && edge2.PrevInSEL === null) return;
    if (edge1.NextInSEL === edge2) {
      var next = edge2.NextInSEL;
      if (next !== null) next.PrevInSEL = edge1;
      var prev = edge1.PrevInSEL;
      if (prev !== null) prev.NextInSEL = edge2;
      edge2.PrevInSEL = prev;
      edge2.NextInSEL = edge1;
      edge1.PrevInSEL = edge2;
      edge1.NextInSEL = next;
    } else if (edge2.NextInSEL === edge1) {
      var next = edge1.NextInSEL;
      if (next !== null) next.PrevInSEL = edge2;
      var prev = edge2.PrevInSEL;
      if (prev !== null) prev.NextInSEL = edge1;
      edge1.PrevInSEL = prev;
      edge1.NextInSEL = edge2;
      edge2.PrevInSEL = edge1;
      edge2.NextInSEL = next;
    } else {
      var next = edge1.NextInSEL;
      var prev = edge1.PrevInSEL;
      edge1.NextInSEL = edge2.NextInSEL;
      if (edge1.NextInSEL !== null) edge1.NextInSEL.PrevInSEL = edge1;
      edge1.PrevInSEL = edge2.PrevInSEL;
      if (edge1.PrevInSEL !== null) edge1.PrevInSEL.NextInSEL = edge1;
      edge2.NextInSEL = next;
      if (edge2.NextInSEL !== null) edge2.NextInSEL.PrevInSEL = edge2;
      edge2.PrevInSEL = prev;
      if (edge2.PrevInSEL !== null) edge2.PrevInSEL.NextInSEL = edge2;
    }
    if (edge1.PrevInSEL === null) this.m_SortedEdges = edge1;
    else if (edge2.PrevInSEL === null) this.m_SortedEdges = edge2;
  };

  ClipperLib.Clipper.prototype.AddLocalMaxPoly = function (e1, e2, pt) {
    this.AddOutPt(e1, pt);
    if (e2.WindDelta === 0) this.AddOutPt(e2, pt);
    if (e1.OutIdx === e2.OutIdx) {
      e1.OutIdx = -1;
      e2.OutIdx = -1;
    } else if (e1.OutIdx < e2.OutIdx) this.AppendPolygon(e1, e2);
    else this.AppendPolygon(e2, e1);
  };

  ClipperLib.Clipper.prototype.AddLocalMinPoly = function (e1, e2, pt) {
    var result;
    var e, prevE;
    if (ClipperLib.ClipperBase.IsHorizontal(e2) || e1.Dx > e2.Dx) {
      result = this.AddOutPt(e1, pt);
      e2.OutIdx = e1.OutIdx;
      e1.Side = ClipperLib.EdgeSide.esLeft;
      e2.Side = ClipperLib.EdgeSide.esRight;
      e = e1;
      if (e.PrevInAEL === e2) prevE = e2.PrevInAEL;
      else prevE = e.PrevInAEL;
    } else {
      result = this.AddOutPt(e2, pt);
      e1.OutIdx = e2.OutIdx;
      e1.Side = ClipperLib.EdgeSide.esRight;
      e2.Side = ClipperLib.EdgeSide.esLeft;
      e = e2;
      if (e.PrevInAEL === e1) prevE = e1.PrevInAEL;
      else prevE = e.PrevInAEL;
    }

    if (prevE !== null && prevE.OutIdx >= 0 && prevE.Top.Y < pt.Y && e.Top.Y < pt.Y) {
      var xPrev = ClipperLib.Clipper.TopX(prevE, pt.Y);
      var xE = ClipperLib.Clipper.TopX(e, pt.Y);
      if (
        xPrev === xE &&
        e.WindDelta !== 0 &&
        prevE.WindDelta !== 0 &&
        ClipperLib.ClipperBase.SlopesEqual5(
          new ClipperLib.IntPoint2(xPrev, pt.Y),
          prevE.Top,
          new ClipperLib.IntPoint2(xE, pt.Y),
          e.Top,
          this.m_UseFullRange,
        )
      ) {
        var outPt = this.AddOutPt(prevE, pt);
        this.AddJoin(result, outPt, e.Top);
      }
    }
    return result;
  };

  ClipperLib.Clipper.prototype.AddOutPt = function (e, pt) {
    if (e.OutIdx < 0) {
      var outRec = this.CreateOutRec();
      outRec.IsOpen = e.WindDelta === 0;
      var newOp = new ClipperLib.OutPt();
      outRec.Pts = newOp;
      newOp.Idx = outRec.Idx;
      //newOp.Pt = pt;
      newOp.Pt.X = pt.X;
      newOp.Pt.Y = pt.Y;
      if (ClipperLib.use_xyz) newOp.Pt.Z = pt.Z;
      newOp.Next = newOp;
      newOp.Prev = newOp;
      if (!outRec.IsOpen) this.SetHoleState(e, outRec);
      e.OutIdx = outRec.Idx;
      //nb: do this after SetZ !
      return newOp;
    } else {
      var outRec = this.m_PolyOuts[e.OutIdx];
      //OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most'
      var op = outRec.Pts;
      var ToFront = e.Side === ClipperLib.EdgeSide.esLeft;
      if (ToFront && ClipperLib.IntPoint.op_Equality(pt, op.Pt)) return op;
      else if (!ToFront && ClipperLib.IntPoint.op_Equality(pt, op.Prev.Pt)) return op.Prev;
      var newOp = new ClipperLib.OutPt();
      newOp.Idx = outRec.Idx;
      //newOp.Pt = pt;
      newOp.Pt.X = pt.X;
      newOp.Pt.Y = pt.Y;
      if (ClipperLib.use_xyz) newOp.Pt.Z = pt.Z;
      newOp.Next = op;
      newOp.Prev = op.Prev;
      newOp.Prev.Next = newOp;
      op.Prev = newOp;
      if (ToFront) outRec.Pts = newOp;
      return newOp;
    }
  };

  ClipperLib.Clipper.prototype.GetLastOutPt = function (e) {
    var outRec = this.m_PolyOuts[e.OutIdx];
    if (e.Side === ClipperLib.EdgeSide.esLeft) {
      return outRec.Pts;
    } else {
      return outRec.Pts.Prev;
    }
  };

  ClipperLib.Clipper.prototype.SwapPoints = function (pt1, pt2) {
    var tmp = new ClipperLib.IntPoint1(pt1.Value);
    //pt1.Value = pt2.Value;
    pt1.Value.X = pt2.Value.X;
    pt1.Value.Y = pt2.Value.Y;
    if (ClipperLib.use_xyz) pt1.Value.Z = pt2.Value.Z;
    //pt2.Value = tmp;
    pt2.Value.X = tmp.X;
    pt2.Value.Y = tmp.Y;
    if (ClipperLib.use_xyz) pt2.Value.Z = tmp.Z;
  };

  ClipperLib.Clipper.prototype.HorzSegmentsOverlap = function (seg1a, seg1b, seg2a, seg2b) {
    var tmp;
    if (seg1a > seg1b) {
      tmp = seg1a;
      seg1a = seg1b;
      seg1b = tmp;
    }
    if (seg2a > seg2b) {
      tmp = seg2a;
      seg2a = seg2b;
      seg2b = tmp;
    }
    return seg1a < seg2b && seg2a < seg1b;
  };

  ClipperLib.Clipper.prototype.SetHoleState = function (e, outRec) {
    var e2 = e.PrevInAEL;
    var eTmp = null;
    while (e2 !== null) {
      if (e2.OutIdx >= 0 && e2.WindDelta !== 0) {
        if (eTmp === null) eTmp = e2;
        else if (eTmp.OutIdx === e2.OutIdx) eTmp = null; //paired
      }
      e2 = e2.PrevInAEL;
    }

    if (eTmp === null) {
      outRec.FirstLeft = null;
      outRec.IsHole = false;
    } else {
      outRec.FirstLeft = this.m_PolyOuts[eTmp.OutIdx];
      outRec.IsHole = !outRec.FirstLeft.IsHole;
    }
  };

  ClipperLib.Clipper.prototype.GetDx = function (pt1, pt2) {
    if (pt1.Y === pt2.Y) return ClipperLib.ClipperBase.horizontal;
    else return (pt2.X - pt1.X) / (pt2.Y - pt1.Y);
  };

  ClipperLib.Clipper.prototype.FirstIsBottomPt = function (btmPt1, btmPt2) {
    var p = btmPt1.Prev;
    while (ClipperLib.IntPoint.op_Equality(p.Pt, btmPt1.Pt) && p !== btmPt1) p = p.Prev;
    var dx1p = Math.abs(this.GetDx(btmPt1.Pt, p.Pt));
    p = btmPt1.Next;
    while (ClipperLib.IntPoint.op_Equality(p.Pt, btmPt1.Pt) && p !== btmPt1) p = p.Next;
    var dx1n = Math.abs(this.GetDx(btmPt1.Pt, p.Pt));
    p = btmPt2.Prev;
    while (ClipperLib.IntPoint.op_Equality(p.Pt, btmPt2.Pt) && p !== btmPt2) p = p.Prev;
    var dx2p = Math.abs(this.GetDx(btmPt2.Pt, p.Pt));
    p = btmPt2.Next;
    while (ClipperLib.IntPoint.op_Equality(p.Pt, btmPt2.Pt) && p !== btmPt2) p = p.Next;
    var dx2n = Math.abs(this.GetDx(btmPt2.Pt, p.Pt));

    if (
      Math.max(dx1p, dx1n) === Math.max(dx2p, dx2n) &&
      Math.min(dx1p, dx1n) === Math.min(dx2p, dx2n)
    ) {
      return this.Area(btmPt1) > 0; //if otherwise identical use orientation
    } else {
      return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n);
    }
  };

  ClipperLib.Clipper.prototype.GetBottomPt = function (pp) {
    var dups = null;
    var p = pp.Next;
    while (p !== pp) {
      if (p.Pt.Y > pp.Pt.Y) {
        pp = p;
        dups = null;
      } else if (p.Pt.Y === pp.Pt.Y && p.Pt.X <= pp.Pt.X) {
        if (p.Pt.X < pp.Pt.X) {
          dups = null;
          pp = p;
        } else {
          if (p.Next !== pp && p.Prev !== pp) dups = p;
        }
      }
      p = p.Next;
    }
    if (dups !== null) {
      //there appears to be at least 2 vertices at bottomPt so ...
      while (dups !== p) {
        if (!this.FirstIsBottomPt(p, dups)) pp = dups;
        dups = dups.Next;
        while (ClipperLib.IntPoint.op_Inequality(dups.Pt, pp.Pt)) dups = dups.Next;
      }
    }
    return pp;
  };

  ClipperLib.Clipper.prototype.GetLowermostRec = function (outRec1, outRec2) {
    //work out which polygon fragment has the correct hole state ...
    if (outRec1.BottomPt === null) outRec1.BottomPt = this.GetBottomPt(outRec1.Pts);
    if (outRec2.BottomPt === null) outRec2.BottomPt = this.GetBottomPt(outRec2.Pts);
    var bPt1 = outRec1.BottomPt;
    var bPt2 = outRec2.BottomPt;
    if (bPt1.Pt.Y > bPt2.Pt.Y) return outRec1;
    else if (bPt1.Pt.Y < bPt2.Pt.Y) return outRec2;
    else if (bPt1.Pt.X < bPt2.Pt.X) return outRec1;
    else if (bPt1.Pt.X > bPt2.Pt.X) return outRec2;
    else if (bPt1.Next === bPt1) return outRec2;
    else if (bPt2.Next === bPt2) return outRec1;
    else if (this.FirstIsBottomPt(bPt1, bPt2)) return outRec1;
    else return outRec2;
  };

  ClipperLib.Clipper.prototype.OutRec1RightOfOutRec2 = function (outRec1, outRec2) {
    do {
      outRec1 = outRec1.FirstLeft;
      if (outRec1 === outRec2) return true;
    } while (outRec1 !== null);
    return false;
  };

  ClipperLib.Clipper.prototype.GetOutRec = function (idx) {
    var outrec = this.m_PolyOuts[idx];
    while (outrec !== this.m_PolyOuts[outrec.Idx]) outrec = this.m_PolyOuts[outrec.Idx];
    return outrec;
  };

  ClipperLib.Clipper.prototype.AppendPolygon = function (e1, e2) {
    //get the start and ends of both output polygons ...
    var outRec1 = this.m_PolyOuts[e1.OutIdx];
    var outRec2 = this.m_PolyOuts[e2.OutIdx];
    var holeStateRec;
    if (this.OutRec1RightOfOutRec2(outRec1, outRec2)) holeStateRec = outRec2;
    else if (this.OutRec1RightOfOutRec2(outRec2, outRec1)) holeStateRec = outRec1;
    else holeStateRec = this.GetLowermostRec(outRec1, outRec2);

    //get the start and ends of both output polygons and
    //join E2 poly onto E1 poly and delete pointers to E2 ...

    var p1_lft = outRec1.Pts;
    var p1_rt = p1_lft.Prev;
    var p2_lft = outRec2.Pts;
    var p2_rt = p2_lft.Prev;
    //join e2 poly onto e1 poly and delete pointers to e2 ...
    if (e1.Side === ClipperLib.EdgeSide.esLeft) {
      if (e2.Side === ClipperLib.EdgeSide.esLeft) {
        //z y x a b c
        this.ReversePolyPtLinks(p2_lft);
        p2_lft.Next = p1_lft;
        p1_lft.Prev = p2_lft;
        p1_rt.Next = p2_rt;
        p2_rt.Prev = p1_rt;
        outRec1.Pts = p2_rt;
      } else {
        //x y z a b c
        p2_rt.Next = p1_lft;
        p1_lft.Prev = p2_rt;
        p2_lft.Prev = p1_rt;
        p1_rt.Next = p2_lft;
        outRec1.Pts = p2_lft;
      }
    } else {
      if (e2.Side === ClipperLib.EdgeSide.esRight) {
        //a b c z y x
        this.ReversePolyPtLinks(p2_lft);
        p1_rt.Next = p2_rt;
        p2_rt.Prev = p1_rt;
        p2_lft.Next = p1_lft;
        p1_lft.Prev = p2_lft;
      } else {
        //a b c x y z
        p1_rt.Next = p2_lft;
        p2_lft.Prev = p1_rt;
        p1_lft.Prev = p2_rt;
        p2_rt.Next = p1_lft;
      }
    }
    outRec1.BottomPt = null;
    if (holeStateRec === outRec2) {
      if (outRec2.FirstLeft !== outRec1) outRec1.FirstLeft = outRec2.FirstLeft;
      outRec1.IsHole = outRec2.IsHole;
    }
    outRec2.Pts = null;
    outRec2.BottomPt = null;
    outRec2.FirstLeft = outRec1;
    var OKIdx = e1.OutIdx;
    var ObsoleteIdx = e2.OutIdx;
    e1.OutIdx = -1;
    //nb: safe because we only get here via AddLocalMaxPoly
    e2.OutIdx = -1;
    var e = this.m_ActiveEdges;
    while (e !== null) {
      if (e.OutIdx === ObsoleteIdx) {
        e.OutIdx = OKIdx;
        e.Side = e1.Side;
        break;
      }
      e = e.NextInAEL;
    }
    outRec2.Idx = outRec1.Idx;
  };

  ClipperLib.Clipper.prototype.ReversePolyPtLinks = function (pp) {
    if (pp === null) return;
    var pp1;
    var pp2;
    pp1 = pp;
    do {
      pp2 = pp1.Next;
      pp1.Next = pp1.Prev;
      pp1.Prev = pp2;
      pp1 = pp2;
    } while (pp1 !== pp);
  };

  ClipperLib.Clipper.SwapSides = function (edge1, edge2) {
    var side = edge1.Side;
    edge1.Side = edge2.Side;
    edge2.Side = side;
  };

  ClipperLib.Clipper.SwapPolyIndexes = function (edge1, edge2) {
    var outIdx = edge1.OutIdx;
    edge1.OutIdx = edge2.OutIdx;
    edge2.OutIdx = outIdx;
  };

  ClipperLib.Clipper.prototype.IntersectEdges = function (e1, e2, pt) {
    //e1 will be to the left of e2 BELOW the intersection. Therefore e1 is before
    //e2 in AEL except when e1 is being inserted at the intersection point ...
    var e1Contributing = e1.OutIdx >= 0;
    var e2Contributing = e2.OutIdx >= 0;

    if (ClipperLib.use_xyz) this.SetZ(pt, e1, e2);

    if (ClipperLib.use_lines) {
      //if either edge is on an OPEN path ...
      if (e1.WindDelta === 0 || e2.WindDelta === 0) {
        //ignore subject-subject open path intersections UNLESS they
        //are both open paths, AND they are both 'contributing maximas' ...
        if (e1.WindDelta === 0 && e2.WindDelta === 0) return;
        //if intersecting a subj line with a subj poly ...
        else if (
          e1.PolyTyp === e2.PolyTyp &&
          e1.WindDelta !== e2.WindDelta &&
          this.m_ClipType === ClipperLib.ClipType.ctUnion
        ) {
          if (e1.WindDelta === 0) {
            if (e2Contributing) {
              this.AddOutPt(e1, pt);
              if (e1Contributing) e1.OutIdx = -1;
            }
          } else {
            if (e1Contributing) {
              this.AddOutPt(e2, pt);
              if (e2Contributing) e2.OutIdx = -1;
            }
          }
        } else if (e1.PolyTyp !== e2.PolyTyp) {
          if (
            e1.WindDelta === 0 &&
            Math.abs(e2.WindCnt) === 1 &&
            (this.m_ClipType !== ClipperLib.ClipType.ctUnion || e2.WindCnt2 === 0)
          ) {
            this.AddOutPt(e1, pt);
            if (e1Contributing) e1.OutIdx = -1;
          } else if (
            e2.WindDelta === 0 &&
            Math.abs(e1.WindCnt) === 1 &&
            (this.m_ClipType !== ClipperLib.ClipType.ctUnion || e1.WindCnt2 === 0)
          ) {
            this.AddOutPt(e2, pt);
            if (e2Contributing) e2.OutIdx = -1;
          }
        }
        return;
      }
    }
    //update winding counts...
    //assumes that e1 will be to the Right of e2 ABOVE the intersection
    if (e1.PolyTyp === e2.PolyTyp) {
      if (this.IsEvenOddFillType(e1)) {
        var oldE1WindCnt = e1.WindCnt;
        e1.WindCnt = e2.WindCnt;
        e2.WindCnt = oldE1WindCnt;
      } else {
        if (e1.WindCnt + e2.WindDelta === 0) e1.WindCnt = -e1.WindCnt;
        else e1.WindCnt += e2.WindDelta;
        if (e2.WindCnt - e1.WindDelta === 0) e2.WindCnt = -e2.WindCnt;
        else e2.WindCnt -= e1.WindDelta;
      }
    } else {
      if (!this.IsEvenOddFillType(e2)) e1.WindCnt2 += e2.WindDelta;
      else e1.WindCnt2 = e1.WindCnt2 === 0 ? 1 : 0;
      if (!this.IsEvenOddFillType(e1)) e2.WindCnt2 -= e1.WindDelta;
      else e2.WindCnt2 = e2.WindCnt2 === 0 ? 1 : 0;
    }
    var e1FillType, e2FillType, e1FillType2, e2FillType2;
    if (e1.PolyTyp === ClipperLib.PolyType.ptSubject) {
      e1FillType = this.m_SubjFillType;
      e1FillType2 = this.m_ClipFillType;
    } else {
      e1FillType = this.m_ClipFillType;
      e1FillType2 = this.m_SubjFillType;
    }
    if (e2.PolyTyp === ClipperLib.PolyType.ptSubject) {
      e2FillType = this.m_SubjFillType;
      e2FillType2 = this.m_ClipFillType;
    } else {
      e2FillType = this.m_ClipFillType;
      e2FillType2 = this.m_SubjFillType;
    }
    var e1Wc, e2Wc;
    switch (e1FillType) {
      case ClipperLib.PolyFillType.pftPositive:
        e1Wc = e1.WindCnt;
        break;
      case ClipperLib.PolyFillType.pftNegative:
        e1Wc = -e1.WindCnt;
        break;
      default:
        e1Wc = Math.abs(e1.WindCnt);
        break;
    }
    switch (e2FillType) {
      case ClipperLib.PolyFillType.pftPositive:
        e2Wc = e2.WindCnt;
        break;
      case ClipperLib.PolyFillType.pftNegative:
        e2Wc = -e2.WindCnt;
        break;
      default:
        e2Wc = Math.abs(e2.WindCnt);
        break;
    }
    if (e1Contributing && e2Contributing) {
      if (
        (e1Wc !== 0 && e1Wc !== 1) ||
        (e2Wc !== 0 && e2Wc !== 1) ||
        (e1.PolyTyp !== e2.PolyTyp && this.m_ClipType !== ClipperLib.ClipType.ctXor)
      ) {
        this.AddLocalMaxPoly(e1, e2, pt);
      } else {
        this.AddOutPt(e1, pt);
        this.AddOutPt(e2, pt);
        ClipperLib.Clipper.SwapSides(e1, e2);
        ClipperLib.Clipper.SwapPolyIndexes(e1, e2);
      }
    } else if (e1Contributing) {
      if (e2Wc === 0 || e2Wc === 1) {
        this.AddOutPt(e1, pt);
        ClipperLib.Clipper.SwapSides(e1, e2);
        ClipperLib.Clipper.SwapPolyIndexes(e1, e2);
      }
    } else if (e2Contributing) {
      if (e1Wc === 0 || e1Wc === 1) {
        this.AddOutPt(e2, pt);
        ClipperLib.Clipper.SwapSides(e1, e2);
        ClipperLib.Clipper.SwapPolyIndexes(e1, e2);
      }
    } else if ((e1Wc === 0 || e1Wc === 1) && (e2Wc === 0 || e2Wc === 1)) {
      //neither edge is currently contributing ...
      var e1Wc2, e2Wc2;
      switch (e1FillType2) {
        case ClipperLib.PolyFillType.pftPositive:
          e1Wc2 = e1.WindCnt2;
          break;
        case ClipperLib.PolyFillType.pftNegative:
          e1Wc2 = -e1.WindCnt2;
          break;
        default:
          e1Wc2 = Math.abs(e1.WindCnt2);
          break;
      }
      switch (e2FillType2) {
        case ClipperLib.PolyFillType.pftPositive:
          e2Wc2 = e2.WindCnt2;
          break;
        case ClipperLib.PolyFillType.pftNegative:
          e2Wc2 = -e2.WindCnt2;
          break;
        default:
          e2Wc2 = Math.abs(e2.WindCnt2);
          break;
      }
      if (e1.PolyTyp !== e2.PolyTyp) {
        this.AddLocalMinPoly(e1, e2, pt);
      } else if (e1Wc === 1 && e2Wc === 1)
        switch (this.m_ClipType) {
          case ClipperLib.ClipType.ctIntersection:
            if (e1Wc2 > 0 && e2Wc2 > 0) this.AddLocalMinPoly(e1, e2, pt);
            break;
          case ClipperLib.ClipType.ctUnion:
            if (e1Wc2 <= 0 && e2Wc2 <= 0) this.AddLocalMinPoly(e1, e2, pt);
            break;
          case ClipperLib.ClipType.ctDifference:
            if (
              (e1.PolyTyp === ClipperLib.PolyType.ptClip && e1Wc2 > 0 && e2Wc2 > 0) ||
              (e1.PolyTyp === ClipperLib.PolyType.ptSubject && e1Wc2 <= 0 && e2Wc2 <= 0)
            )
              this.AddLocalMinPoly(e1, e2, pt);
            break;
          case ClipperLib.ClipType.ctXor:
            this.AddLocalMinPoly(e1, e2, pt);
            break;
        }
      else ClipperLib.Clipper.SwapSides(e1, e2);
    }
  };

  ClipperLib.Clipper.prototype.DeleteFromSEL = function (e) {
    var SelPrev = e.PrevInSEL;
    var SelNext = e.NextInSEL;
    if (SelPrev === null && SelNext === null && e !== this.m_SortedEdges) return;
    //already deleted
    if (SelPrev !== null) SelPrev.NextInSEL = SelNext;
    else this.m_SortedEdges = SelNext;
    if (SelNext !== null) SelNext.PrevInSEL = SelPrev;
    e.NextInSEL = null;
    e.PrevInSEL = null;
  };

  ClipperLib.Clipper.prototype.ProcessHorizontals = function () {
    var horzEdge = {}; //m_SortedEdges;
    while (this.PopEdgeFromSEL(horzEdge)) {
      this.ProcessHorizontal(horzEdge.v);
    }
  };

  ClipperLib.Clipper.prototype.GetHorzDirection = function (HorzEdge, $var) {
    if (HorzEdge.Bot.X < HorzEdge.Top.X) {
      $var.Left = HorzEdge.Bot.X;
      $var.Right = HorzEdge.Top.X;
      $var.Dir = ClipperLib.Direction.dLeftToRight;
    } else {
      $var.Left = HorzEdge.Top.X;
      $var.Right = HorzEdge.Bot.X;
      $var.Dir = ClipperLib.Direction.dRightToLeft;
    }
  };

  ClipperLib.Clipper.prototype.ProcessHorizontal = function (horzEdge) {
    var $var = {
      Dir: null,
      Left: null,
      Right: null,
    };

    this.GetHorzDirection(horzEdge, $var);
    var dir = $var.Dir;
    var horzLeft = $var.Left;
    var horzRight = $var.Right;

    var IsOpen = horzEdge.WindDelta === 0;

    var eLastHorz = horzEdge,
      eMaxPair = null;
    while (eLastHorz.NextInLML !== null && ClipperLib.ClipperBase.IsHorizontal(eLastHorz.NextInLML))
      eLastHorz = eLastHorz.NextInLML;
    if (eLastHorz.NextInLML === null) eMaxPair = this.GetMaximaPair(eLastHorz);

    var currMax = this.m_Maxima;
    if (currMax !== null) {
      //get the first maxima in range (X) ...
      if (dir === ClipperLib.Direction.dLeftToRight) {
        while (currMax !== null && currMax.X <= horzEdge.Bot.X) {
          currMax = currMax.Next;
        }
        if (currMax !== null && currMax.X >= eLastHorz.Top.X) {
          currMax = null;
        }
      } else {
        while (currMax.Next !== null && currMax.Next.X < horzEdge.Bot.X) {
          currMax = currMax.Next;
        }
        if (currMax.X <= eLastHorz.Top.X) {
          currMax = null;
        }
      }
    }
    var op1 = null;
    for (;;) {
      //loop through consec. horizontal edges
      var IsLastHorz = horzEdge === eLastHorz;
      var e = this.GetNextInAEL(horzEdge, dir);
      while (e !== null) {
        //this code block inserts extra coords into horizontal edges (in output
        //polygons) whereever maxima touch these horizontal edges. This helps
        //'simplifying' polygons (ie if the Simplify property is set).
        if (currMax !== null) {
          if (dir === ClipperLib.Direction.dLeftToRight) {
            while (currMax !== null && currMax.X < e.Curr.X) {
              if (horzEdge.OutIdx >= 0 && !IsOpen) {
                this.AddOutPt(horzEdge, new ClipperLib.IntPoint2(currMax.X, horzEdge.Bot.Y));
              }
              currMax = currMax.Next;
            }
          } else {
            while (currMax !== null && currMax.X > e.Curr.X) {
              if (horzEdge.OutIdx >= 0 && !IsOpen) {
                this.AddOutPt(horzEdge, new ClipperLib.IntPoint2(currMax.X, horzEdge.Bot.Y));
              }
              currMax = currMax.Prev;
            }
          }
        }

        if (
          (dir === ClipperLib.Direction.dLeftToRight && e.Curr.X > horzRight) ||
          (dir === ClipperLib.Direction.dRightToLeft && e.Curr.X < horzLeft)
        ) {
          break;
        }

        //Also break if we've got to the end of an intermediate horizontal edge ...
        //nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal.
        if (
          e.Curr.X === horzEdge.Top.X &&
          horzEdge.NextInLML !== null &&
          e.Dx < horzEdge.NextInLML.Dx
        )
          break;

        if (horzEdge.OutIdx >= 0 && !IsOpen) //note: may be done multiple times
        {
          if (ClipperLib.use_xyz) {
            if (dir === ClipperLib.Direction.dLeftToRight) this.SetZ(e.Curr, horzEdge, e);
            else this.SetZ(e.Curr, e, horzEdge);
          }

          op1 = this.AddOutPt(horzEdge, e.Curr);
          var eNextHorz = this.m_SortedEdges;
          while (eNextHorz !== null) {
            if (
              eNextHorz.OutIdx >= 0 &&
              this.HorzSegmentsOverlap(
                horzEdge.Bot.X,
                horzEdge.Top.X,
                eNextHorz.Bot.X,
                eNextHorz.Top.X,
              )
            ) {
              var op2 = this.GetLastOutPt(eNextHorz);
              this.AddJoin(op2, op1, eNextHorz.Top);
            }
            eNextHorz = eNextHorz.NextInSEL;
          }
          this.AddGhostJoin(op1, horzEdge.Bot);
        }

        //OK, so far we're still in range of the horizontal Edge  but make sure
        //we're at the last of consec. horizontals when matching with eMaxPair
        if (e === eMaxPair && IsLastHorz) {
          if (horzEdge.OutIdx >= 0) {
            this.AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge.Top);
          }
          this.DeleteFromAEL(horzEdge);
          this.DeleteFromAEL(eMaxPair);
          return;
        }

        if (dir === ClipperLib.Direction.dLeftToRight) {
          var Pt = new ClipperLib.IntPoint2(e.Curr.X, horzEdge.Curr.Y);
          this.IntersectEdges(horzEdge, e, Pt);
        } else {
          var Pt = new ClipperLib.IntPoint2(e.Curr.X, horzEdge.Curr.Y);
          this.IntersectEdges(e, horzEdge, Pt);
        }
        var eNext = this.GetNextInAEL(e, dir);
        this.SwapPositionsInAEL(horzEdge, e);
        e = eNext;
      } //end while(e !== null)

      //Break out of loop if HorzEdge.NextInLML is not also horizontal ...
      if (horzEdge.NextInLML === null || !ClipperLib.ClipperBase.IsHorizontal(horzEdge.NextInLML)) {
        break;
      }

      horzEdge = this.UpdateEdgeIntoAEL(horzEdge);
      if (horzEdge.OutIdx >= 0) {
        this.AddOutPt(horzEdge, horzEdge.Bot);
      }

      $var = {
        Dir: dir,
        Left: horzLeft,
        Right: horzRight,
      };

      this.GetHorzDirection(horzEdge, $var);
      dir = $var.Dir;
      horzLeft = $var.Left;
      horzRight = $var.Right;
    } //end for (;;)

    if (horzEdge.OutIdx >= 0 && op1 === null) {
      op1 = this.GetLastOutPt(horzEdge);
      var eNextHorz = this.m_SortedEdges;
      while (eNextHorz !== null) {
        if (
          eNextHorz.OutIdx >= 0 &&
          this.HorzSegmentsOverlap(horzEdge.Bot.X, horzEdge.Top.X, eNextHorz.Bot.X, eNextHorz.Top.X)
        ) {
          var op2 = this.GetLastOutPt(eNextHorz);
          this.AddJoin(op2, op1, eNextHorz.Top);
        }
        eNextHorz = eNextHorz.NextInSEL;
      }
      this.AddGhostJoin(op1, horzEdge.Top);
    }

    if (horzEdge.NextInLML !== null) {
      if (horzEdge.OutIdx >= 0) {
        op1 = this.AddOutPt(horzEdge, horzEdge.Top);

        horzEdge = this.UpdateEdgeIntoAEL(horzEdge);
        if (horzEdge.WindDelta === 0) {
          return;
        }
        //nb: HorzEdge is no longer horizontal here
        var ePrev = horzEdge.PrevInAEL;
        var eNext = horzEdge.NextInAEL;
        if (
          ePrev !== null &&
          ePrev.Curr.X === horzEdge.Bot.X &&
          ePrev.Curr.Y === horzEdge.Bot.Y &&
          ePrev.WindDelta === 0 &&
          ePrev.OutIdx >= 0 &&
          ePrev.Curr.Y > ePrev.Top.Y &&
          ClipperLib.ClipperBase.SlopesEqual3(horzEdge, ePrev, this.m_UseFullRange)
        ) {
          var op2 = this.AddOutPt(ePrev, horzEdge.Bot);
          this.AddJoin(op1, op2, horzEdge.Top);
        } else if (
          eNext !== null &&
          eNext.Curr.X === horzEdge.Bot.X &&
          eNext.Curr.Y === horzEdge.Bot.Y &&
          eNext.WindDelta !== 0 &&
          eNext.OutIdx >= 0 &&
          eNext.Curr.Y > eNext.Top.Y &&
          ClipperLib.ClipperBase.SlopesEqual3(horzEdge, eNext, this.m_UseFullRange)
        ) {
          var op2 = this.AddOutPt(eNext, horzEdge.Bot);
          this.AddJoin(op1, op2, horzEdge.Top);
        }
      } else {
        horzEdge = this.UpdateEdgeIntoAEL(horzEdge);
      }
    } else {
      if (horzEdge.OutIdx >= 0) {
        this.AddOutPt(horzEdge, horzEdge.Top);
      }
      this.DeleteFromAEL(horzEdge);
    }
  };

  ClipperLib.Clipper.prototype.GetNextInAEL = function (e, Direction) {
    return Direction === ClipperLib.Direction.dLeftToRight ? e.NextInAEL : e.PrevInAEL;
  };

  ClipperLib.Clipper.prototype.IsMinima = function (e) {
    return e !== null && e.Prev.NextInLML !== e && e.Next.NextInLML !== e;
  };

  ClipperLib.Clipper.prototype.IsMaxima = function (e, Y) {
    return e !== null && e.Top.Y === Y && e.NextInLML === null;
  };

  ClipperLib.Clipper.prototype.IsIntermediate = function (e, Y) {
    return e.Top.Y === Y && e.NextInLML !== null;
  };

  ClipperLib.Clipper.prototype.GetMaximaPair = function (e) {
    if (ClipperLib.IntPoint.op_Equality(e.Next.Top, e.Top) && e.Next.NextInLML === null) {
      return e.Next;
    } else {
      if (ClipperLib.IntPoint.op_Equality(e.Prev.Top, e.Top) && e.Prev.NextInLML === null) {
        return e.Prev;
      } else {
        return null;
      }
    }
  };

  ClipperLib.Clipper.prototype.GetMaximaPairEx = function (e) {
    //as above but returns null if MaxPair isn't in AEL (unless it's horizontal)
    var result = this.GetMaximaPair(e);
    if (
      result === null ||
      result.OutIdx === ClipperLib.ClipperBase.Skip ||
      (result.NextInAEL === result.PrevInAEL && !ClipperLib.ClipperBase.IsHorizontal(result))
    ) {
      return null;
    }
    return result;
  };

  ClipperLib.Clipper.prototype.ProcessIntersections = function (topY) {
    if (this.m_ActiveEdges === null) return true;
    try {
      this.BuildIntersectList(topY);
      if (this.m_IntersectList.length === 0) return true;
      if (this.m_IntersectList.length === 1 || this.FixupIntersectionOrder())
        this.ProcessIntersectList();
      else return false;
    } catch ($$e2) {
      this.m_SortedEdges = null;
      this.m_IntersectList.length = 0;
      ClipperLib.Error("ProcessIntersections error");
    }
    this.m_SortedEdges = null;
    return true;
  };

  ClipperLib.Clipper.prototype.BuildIntersectList = function (topY) {
    if (this.m_ActiveEdges === null) return;
    //prepare for sorting ...
    var e = this.m_ActiveEdges;
    //console.log(JSON.stringify(JSON.decycle( e )));
    this.m_SortedEdges = e;
    while (e !== null) {
      e.PrevInSEL = e.PrevInAEL;
      e.NextInSEL = e.NextInAEL;
      e.Curr.X = ClipperLib.Clipper.TopX(e, topY);
      e = e.NextInAEL;
    }
    //bubblesort ...
    var isModified = true;
    while (isModified && this.m_SortedEdges !== null) {
      isModified = false;
      e = this.m_SortedEdges;
      while (e.NextInSEL !== null) {
        var eNext = e.NextInSEL;
        var pt = new ClipperLib.IntPoint0();
        //console.log("e.Curr.X: " + e.Curr.X + " eNext.Curr.X" + eNext.Curr.X);
        if (e.Curr.X > eNext.Curr.X) {
          this.IntersectPoint(e, eNext, pt);
          if (pt.Y < topY) {
            pt = new ClipperLib.IntPoint2(ClipperLib.Clipper.TopX(e, topY), topY);
          }
          var newNode = new ClipperLib.IntersectNode();
          newNode.Edge1 = e;
          newNode.Edge2 = eNext;
          //newNode.Pt = pt;
          newNode.Pt.X = pt.X;
          newNode.Pt.Y = pt.Y;
          if (ClipperLib.use_xyz) newNode.Pt.Z = pt.Z;
          this.m_IntersectList.push(newNode);
          this.SwapPositionsInSEL(e, eNext);
          isModified = true;
        } else e = eNext;
      }
      if (e.PrevInSEL !== null) e.PrevInSEL.NextInSEL = null;
      else break;
    }
    this.m_SortedEdges = null;
  };

  ClipperLib.Clipper.prototype.EdgesAdjacent = function (inode) {
    return inode.Edge1.NextInSEL === inode.Edge2 || inode.Edge1.PrevInSEL === inode.Edge2;
  };

  ClipperLib.Clipper.IntersectNodeSort = function (node1, node2) {
    //the following typecast is safe because the differences in Pt.Y will
    //be limited to the height of the scanbeam.
    return node2.Pt.Y - node1.Pt.Y;
  };

  ClipperLib.Clipper.prototype.FixupIntersectionOrder = function () {
    //pre-condition: intersections are sorted bottom-most first.
    //Now it's crucial that intersections are made only between adjacent edges,
    //so to ensure this the order of intersections may need adjusting ...
    this.m_IntersectList.sort(this.m_IntersectNodeComparer);
    this.CopyAELToSEL();
    var cnt = this.m_IntersectList.length;
    for (var i = 0; i < cnt; i++) {
      if (!this.EdgesAdjacent(this.m_IntersectList[i])) {
        var j = i + 1;
        while (j < cnt && !this.EdgesAdjacent(this.m_IntersectList[j])) j++;
        if (j === cnt) return false;
        var tmp = this.m_IntersectList[i];
        this.m_IntersectList[i] = this.m_IntersectList[j];
        this.m_IntersectList[j] = tmp;
      }
      this.SwapPositionsInSEL(this.m_IntersectList[i].Edge1, this.m_IntersectList[i].Edge2);
    }
    return true;
  };

  ClipperLib.Clipper.prototype.ProcessIntersectList = function () {
    for (var i = 0, ilen = this.m_IntersectList.length; i < ilen; i++) {
      var iNode = this.m_IntersectList[i];
      this.IntersectEdges(iNode.Edge1, iNode.Edge2, iNode.Pt);
      this.SwapPositionsInAEL(iNode.Edge1, iNode.Edge2);
    }
    this.m_IntersectList.length = 0;
  };

  /*
	--------------------------------
	Round speedtest: http://jsperf.com/fastest-round
	--------------------------------
	*/
  var R1 = function (a) {
    return a < 0 ? Math.ceil(a - 0.5) : Math.round(a);
  };

  var R2 = function (a) {
    return a < 0 ? Math.ceil(a - 0.5) : Math.floor(a + 0.5);
  };

  var R3 = function (a) {
    return a < 0 ? -Math.round(Math.abs(a)) : Math.round(a);
  };

  var R4 = function (a) {
    if (a < 0) {
      a -= 0.5;
      return a < -2147483648 ? Math.ceil(a) : a | 0;
    } else {
      a += 0.5;
      return a > 2147483647 ? Math.floor(a) : a | 0;
    }
  };

  if (browser.msie) ClipperLib.Clipper.Round = R1;
  else if (browser.chromium) ClipperLib.Clipper.Round = R3;
  else if (browser.safari) ClipperLib.Clipper.Round = R4;
  else ClipperLib.Clipper.Round = R2; // eg. browser.chrome || browser.firefox || browser.opera
  ClipperLib.Clipper.TopX = function (edge, currentY) {
    //if (edge.Bot == edge.Curr) alert ("edge.Bot = edge.Curr");
    //if (edge.Bot == edge.Top) alert ("edge.Bot = edge.Top");
    if (currentY === edge.Top.Y) return edge.Top.X;
    return edge.Bot.X + ClipperLib.Clipper.Round(edge.Dx * (currentY - edge.Bot.Y));
  };

  ClipperLib.Clipper.prototype.IntersectPoint = function (edge1, edge2, ip) {
    ip.X = 0;
    ip.Y = 0;
    var b1, b2;
    //nb: with very large coordinate values, it's possible for SlopesEqual() to
    //return false but for the edge.Dx value be equal due to double precision rounding.
    if (edge1.Dx === edge2.Dx) {
      ip.Y = edge1.Curr.Y;
      ip.X = ClipperLib.Clipper.TopX(edge1, ip.Y);
      return;
    }
    if (edge1.Delta.X === 0) {
      ip.X = edge1.Bot.X;
      if (ClipperLib.ClipperBase.IsHorizontal(edge2)) {
        ip.Y = edge2.Bot.Y;
      } else {
        b2 = edge2.Bot.Y - edge2.Bot.X / edge2.Dx;
        ip.Y = ClipperLib.Clipper.Round(ip.X / edge2.Dx + b2);
      }
    } else if (edge2.Delta.X === 0) {
      ip.X = edge2.Bot.X;
      if (ClipperLib.ClipperBase.IsHorizontal(edge1)) {
        ip.Y = edge1.Bot.Y;
      } else {
        b1 = edge1.Bot.Y - edge1.Bot.X / edge1.Dx;
        ip.Y = ClipperLib.Clipper.Round(ip.X / edge1.Dx + b1);
      }
    } else {
      b1 = edge1.Bot.X - edge1.Bot.Y * edge1.Dx;
      b2 = edge2.Bot.X - edge2.Bot.Y * edge2.Dx;
      var q = (b2 - b1) / (edge1.Dx - edge2.Dx);
      ip.Y = ClipperLib.Clipper.Round(q);
      if (Math.abs(edge1.Dx) < Math.abs(edge2.Dx))
        ip.X = ClipperLib.Clipper.Round(edge1.Dx * q + b1);
      else ip.X = ClipperLib.Clipper.Round(edge2.Dx * q + b2);
    }
    if (ip.Y < edge1.Top.Y || ip.Y < edge2.Top.Y) {
      if (edge1.Top.Y > edge2.Top.Y) {
        ip.Y = edge1.Top.Y;
        ip.X = ClipperLib.Clipper.TopX(edge2, edge1.Top.Y);
        return ip.X < edge1.Top.X;
      } else ip.Y = edge2.Top.Y;
      if (Math.abs(edge1.Dx) < Math.abs(edge2.Dx)) ip.X = ClipperLib.Clipper.TopX(edge1, ip.Y);
      else ip.X = ClipperLib.Clipper.TopX(edge2, ip.Y);
    }
    //finally, don't allow 'ip' to be BELOW curr.Y (ie bottom of scanbeam) ...
    if (ip.Y > edge1.Curr.Y) {
      ip.Y = edge1.Curr.Y;
      //better to use the more vertical edge to derive X ...
      if (Math.abs(edge1.Dx) > Math.abs(edge2.Dx)) ip.X = ClipperLib.Clipper.TopX(edge2, ip.Y);
      else ip.X = ClipperLib.Clipper.TopX(edge1, ip.Y);
    }
  };

  ClipperLib.Clipper.prototype.ProcessEdgesAtTopOfScanbeam = function (topY) {
    var e = this.m_ActiveEdges;

    while (e !== null) {
      //1. process maxima, treating them as if they're 'bent' horizontal edges,
      //   but exclude maxima with horizontal edges. nb: e can't be a horizontal.
      var IsMaximaEdge = this.IsMaxima(e, topY);
      if (IsMaximaEdge) {
        var eMaxPair = this.GetMaximaPairEx(e);
        IsMaximaEdge = eMaxPair === null || !ClipperLib.ClipperBase.IsHorizontal(eMaxPair);
      }
      if (IsMaximaEdge) {
        if (this.StrictlySimple) {
          this.InsertMaxima(e.Top.X);
        }
        var ePrev = e.PrevInAEL;
        this.DoMaxima(e);
        if (ePrev === null) e = this.m_ActiveEdges;
        else e = ePrev.NextInAEL;
      } else {
        //2. promote horizontal edges, otherwise update Curr.X and Curr.Y ...
        if (this.IsIntermediate(e, topY) && ClipperLib.ClipperBase.IsHorizontal(e.NextInLML)) {
          e = this.UpdateEdgeIntoAEL(e);
          if (e.OutIdx >= 0) this.AddOutPt(e, e.Bot);
          this.AddEdgeToSEL(e);
        } else {
          e.Curr.X = ClipperLib.Clipper.TopX(e, topY);
          e.Curr.Y = topY;
        }

        if (ClipperLib.use_xyz) {
          if (e.Top.Y === topY) e.Curr.Z = e.Top.Z;
          else if (e.Bot.Y === topY) e.Curr.Z = e.Bot.Z;
          else e.Curr.Z = 0;
        }

        //When StrictlySimple and 'e' is being touched by another edge, then
        //make sure both edges have a vertex here ...
        if (this.StrictlySimple) {
          var ePrev = e.PrevInAEL;
          if (
            e.OutIdx >= 0 &&
            e.WindDelta !== 0 &&
            ePrev !== null &&
            ePrev.OutIdx >= 0 &&
            ePrev.Curr.X === e.Curr.X &&
            ePrev.WindDelta !== 0
          ) {
            var ip = new ClipperLib.IntPoint1(e.Curr);

            if (ClipperLib.use_xyz) {
              this.SetZ(ip, ePrev, e);
            }

            var op = this.AddOutPt(ePrev, ip);
            var op2 = this.AddOutPt(e, ip);
            this.AddJoin(op, op2, ip); //StrictlySimple (type-3) join
          }
        }
        e = e.NextInAEL;
      }
    }
    //3. Process horizontals at the Top of the scanbeam ...
    this.ProcessHorizontals();
    this.m_Maxima = null;
    //4. Promote intermediate vertices ...
    e = this.m_ActiveEdges;
    while (e !== null) {
      if (this.IsIntermediate(e, topY)) {
        var op = null;
        if (e.OutIdx >= 0) op = this.AddOutPt(e, e.Top);
        e = this.UpdateEdgeIntoAEL(e);
        //if output polygons share an edge, they'll need joining later ...
        var ePrev = e.PrevInAEL;
        var eNext = e.NextInAEL;

        if (
          ePrev !== null &&
          ePrev.Curr.X === e.Bot.X &&
          ePrev.Curr.Y === e.Bot.Y &&
          op !== null &&
          ePrev.OutIdx >= 0 &&
          ePrev.Curr.Y === ePrev.Top.Y &&
          ClipperLib.ClipperBase.SlopesEqual5(
            e.Curr,
            e.Top,
            ePrev.Curr,
            ePrev.Top,
            this.m_UseFullRange,
          ) &&
          e.WindDelta !== 0 &&
          ePrev.WindDelta !== 0
        ) {
          var op2 = this.AddOutPt(ePrev2, e.Bot);
          this.AddJoin(op, op2, e.Top);
        } else if (
          eNext !== null &&
          eNext.Curr.X === e.Bot.X &&
          eNext.Curr.Y === e.Bot.Y &&
          op !== null &&
          eNext.OutIdx >= 0 &&
          eNext.Curr.Y === eNext.Top.Y &&
          ClipperLib.ClipperBase.SlopesEqual5(
            e.Curr,
            e.Top,
            eNext.Curr,
            eNext.Top,
            this.m_UseFullRange,
          ) &&
          e.WindDelta !== 0 &&
          eNext.WindDelta !== 0
        ) {
          var op2 = this.AddOutPt(eNext, e.Bot);
          this.AddJoin(op, op2, e.Top);
        }
      }
      e = e.NextInAEL;
    }
  };

  ClipperLib.Clipper.prototype.DoMaxima = function (e) {
    var eMaxPair = this.GetMaximaPairEx(e);
    if (eMaxPair === null) {
      if (e.OutIdx >= 0) this.AddOutPt(e, e.Top);
      this.DeleteFromAEL(e);
      return;
    }
    var eNext = e.NextInAEL;
    while (eNext !== null && eNext !== eMaxPair) {
      this.IntersectEdges(e, eNext, e.Top);
      this.SwapPositionsInAEL(e, eNext);
      eNext = e.NextInAEL;
    }
    if (e.OutIdx === -1 && eMaxPair.OutIdx === -1) {
      this.DeleteFromAEL(e);
      this.DeleteFromAEL(eMaxPair);
    } else if (e.OutIdx >= 0 && eMaxPair.OutIdx >= 0) {
      if (e.OutIdx >= 0) this.AddLocalMaxPoly(e, eMaxPair, e.Top);
      this.DeleteFromAEL(e);
      this.DeleteFromAEL(eMaxPair);
    } else if (ClipperLib.use_lines && e.WindDelta === 0) {
      if (e.OutIdx >= 0) {
        this.AddOutPt(e, e.Top);
        e.OutIdx = ClipperLib.ClipperBase.Unassigned;
      }
      this.DeleteFromAEL(e);
      if (eMaxPair.OutIdx >= 0) {
        this.AddOutPt(eMaxPair, e.Top);
        eMaxPair.OutIdx = ClipperLib.ClipperBase.Unassigned;
      }
      this.DeleteFromAEL(eMaxPair);
    } else ClipperLib.Error("DoMaxima error");
  };

  ClipperLib.Clipper.ReversePaths = function (polys) {
    for (var i = 0, len = polys.length; i < len; i++) polys[i].reverse();
  };

  ClipperLib.Clipper.Orientation = function (poly) {
    return ClipperLib.Clipper.Area(poly) >= 0;
  };

  ClipperLib.Clipper.prototype.PointCount = function (pts) {
    if (pts === null) return 0;
    var result = 0;
    var p = pts;
    do {
      result++;
      p = p.Next;
    } while (p !== pts);
    return result;
  };

  ClipperLib.Clipper.prototype.BuildResult = function (polyg) {
    ClipperLib.Clear(polyg);
    for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) {
      var outRec = this.m_PolyOuts[i];
      if (outRec.Pts === null) continue;
      var p = outRec.Pts.Prev;
      var cnt = this.PointCount(p);
      if (cnt < 2) continue;
      var pg = new Array(cnt);
      for (var j = 0; j < cnt; j++) {
        pg[j] = p.Pt;
        p = p.Prev;
      }
      polyg.push(pg);
    }
  };

  ClipperLib.Clipper.prototype.BuildResult2 = function (polytree) {
    polytree.Clear();
    //add each output polygon/contour to polytree ...
    //polytree.m_AllPolys.set_Capacity(this.m_PolyOuts.length);
    for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) {
      var outRec = this.m_PolyOuts[i];
      var cnt = this.PointCount(outRec.Pts);
      if ((outRec.IsOpen && cnt < 2) || (!outRec.IsOpen && cnt < 3)) continue;
      this.FixHoleLinkage(outRec);
      var pn = new ClipperLib.PolyNode();
      polytree.m_AllPolys.push(pn);
      outRec.PolyNode = pn;
      pn.m_polygon.length = cnt;
      var op = outRec.Pts.Prev;
      for (var j = 0; j < cnt; j++) {
        pn.m_polygon[j] = op.Pt;
        op = op.Prev;
      }
    }
    //fixup PolyNode links etc ...
    //polytree.m_Childs.set_Capacity(this.m_PolyOuts.length);
    for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) {
      var outRec = this.m_PolyOuts[i];
      if (outRec.PolyNode === null) continue;
      else if (outRec.IsOpen) {
        outRec.PolyNode.IsOpen = true;
        polytree.AddChild(outRec.PolyNode);
      } else if (outRec.FirstLeft !== null && outRec.FirstLeft.PolyNode !== null)
        outRec.FirstLeft.PolyNode.AddChild(outRec.PolyNode);
      else polytree.AddChild(outRec.PolyNode);
    }
  };

  ClipperLib.Clipper.prototype.FixupOutPolyline = function (outRec) {
    var pp = outRec.Pts;
    var lastPP = pp.Prev;
    while (pp !== lastPP) {
      pp = pp.Next;
      if (ClipperLib.IntPoint.op_Equality(pp.Pt, pp.Prev.Pt)) {
        if (pp === lastPP) {
          lastPP = pp.Prev;
        }
        var tmpPP = pp.Prev;
        tmpPP.Next = pp.Next;
        pp.Next.Prev = tmpPP;
        pp = tmpPP;
      }
    }
    if (pp === pp.Prev) {
      outRec.Pts = null;
    }
  };

  ClipperLib.Clipper.prototype.FixupOutPolygon = function (outRec) {
    //FixupOutPolygon() - removes duplicate points and simplifies consecutive
    //parallel edges by removing the middle vertex.
    var lastOK = null;
    outRec.BottomPt = null;
    var pp = outRec.Pts;
    var preserveCol = this.PreserveCollinear || this.StrictlySimple;
    for (;;) {
      if (pp.Prev === pp || pp.Prev === pp.Next) {
        outRec.Pts = null;
        return;
      }

      //test for duplicate points and collinear edges ...
      if (
        ClipperLib.IntPoint.op_Equality(pp.Pt, pp.Next.Pt) ||
        ClipperLib.IntPoint.op_Equality(pp.Pt, pp.Prev.Pt) ||
        (ClipperLib.ClipperBase.SlopesEqual4(pp.Prev.Pt, pp.Pt, pp.Next.Pt, this.m_UseFullRange) &&
          (!preserveCol || !this.Pt2IsBetweenPt1AndPt3(pp.Prev.Pt, pp.Pt, pp.Next.Pt)))
      ) {
        lastOK = null;
        pp.Prev.Next = pp.Next;
        pp.Next.Prev = pp.Prev;
        pp = pp.Prev;
      } else if (pp === lastOK) break;
      else {
        if (lastOK === null) lastOK = pp;
        pp = pp.Next;
      }
    }
    outRec.Pts = pp;
  };

  ClipperLib.Clipper.prototype.DupOutPt = function (outPt, InsertAfter) {
    var result = new ClipperLib.OutPt();
    //result.Pt = outPt.Pt;
    result.Pt.X = outPt.Pt.X;
    result.Pt.Y = outPt.Pt.Y;
    if (ClipperLib.use_xyz) result.Pt.Z = outPt.Pt.Z;
    result.Idx = outPt.Idx;
    if (InsertAfter) {
      result.Next = outPt.Next;
      result.Prev = outPt;
      outPt.Next.Prev = result;
      outPt.Next = result;
    } else {
      result.Prev = outPt.Prev;
      result.Next = outPt;
      outPt.Prev.Next = result;
      outPt.Prev = result;
    }
    return result;
  };

  ClipperLib.Clipper.prototype.GetOverlap = function (a1, a2, b1, b2, $val) {
    if (a1 < a2) {
      if (b1 < b2) {
        $val.Left = Math.max(a1, b1);
        $val.Right = Math.min(a2, b2);
      } else {
        $val.Left = Math.max(a1, b2);
        $val.Right = Math.min(a2, b1);
      }
    } else {
      if (b1 < b2) {
        $val.Left = Math.max(a2, b1);
        $val.Right = Math.min(a1, b2);
      } else {
        $val.Left = Math.max(a2, b2);
        $val.Right = Math.min(a1, b1);
      }
    }
    return $val.Left < $val.Right;
  };

  ClipperLib.Clipper.prototype.JoinHorz = function (op1, op1b, op2, op2b, Pt, DiscardLeft) {
    var Dir1 =
      op1.Pt.X > op1b.Pt.X ? ClipperLib.Direction.dRightToLeft : ClipperLib.Direction.dLeftToRight;
    var Dir2 =
      op2.Pt.X > op2b.Pt.X ? ClipperLib.Direction.dRightToLeft : ClipperLib.Direction.dLeftToRight;
    if (Dir1 === Dir2) return false;
    //When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we
    //want Op1b to be on the Right. (And likewise with Op2 and Op2b.)
    //So, to facilitate this while inserting Op1b and Op2b ...
    //when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b,
    //otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.)
    if (Dir1 === ClipperLib.Direction.dLeftToRight) {
      while (op1.Next.Pt.X <= Pt.X && op1.Next.Pt.X >= op1.Pt.X && op1.Next.Pt.Y === Pt.Y)
        op1 = op1.Next;
      if (DiscardLeft && op1.Pt.X !== Pt.X) op1 = op1.Next;
      op1b = this.DupOutPt(op1, !DiscardLeft);
      if (ClipperLib.IntPoint.op_Inequality(op1b.Pt, Pt)) {
        op1 = op1b;
        //op1.Pt = Pt;
        op1.Pt.X = Pt.X;
        op1.Pt.Y = Pt.Y;
        if (ClipperLib.use_xyz) op1.Pt.Z = Pt.Z;
        op1b = this.DupOutPt(op1, !DiscardLeft);
      }
    } else {
      while (op1.Next.Pt.X >= Pt.X && op1.Next.Pt.X <= op1.Pt.X && op1.Next.Pt.Y === Pt.Y)
        op1 = op1.Next;
      if (!DiscardLeft && op1.Pt.X !== Pt.X) op1 = op1.Next;
      op1b = this.DupOutPt(op1, DiscardLeft);
      if (ClipperLib.IntPoint.op_Inequality(op1b.Pt, Pt)) {
        op1 = op1b;
        //op1.Pt = Pt;
        op1.Pt.X = Pt.X;
        op1.Pt.Y = Pt.Y;
        if (ClipperLib.use_xyz) op1.Pt.Z = Pt.Z;
        op1b = this.DupOutPt(op1, DiscardLeft);
      }
    }
    if (Dir2 === ClipperLib.Direction.dLeftToRight) {
      while (op2.Next.Pt.X <= Pt.X && op2.Next.Pt.X >= op2.Pt.X && op2.Next.Pt.Y === Pt.Y)
        op2 = op2.Next;
      if (DiscardLeft && op2.Pt.X !== Pt.X) op2 = op2.Next;
      op2b = this.DupOutPt(op2, !DiscardLeft);
      if (ClipperLib.IntPoint.op_Inequality(op2b.Pt, Pt)) {
        op2 = op2b;
        //op2.Pt = Pt;
        op2.Pt.X = Pt.X;
        op2.Pt.Y = Pt.Y;
        if (ClipperLib.use_xyz) op2.Pt.Z = Pt.Z;
        op2b = this.DupOutPt(op2, !DiscardLeft);
      }
    } else {
      while (op2.Next.Pt.X >= Pt.X && op2.Next.Pt.X <= op2.Pt.X && op2.Next.Pt.Y === Pt.Y)
        op2 = op2.Next;
      if (!DiscardLeft && op2.Pt.X !== Pt.X) op2 = op2.Next;
      op2b = this.DupOutPt(op2, DiscardLeft);
      if (ClipperLib.IntPoint.op_Inequality(op2b.Pt, Pt)) {
        op2 = op2b;
        //op2.Pt = Pt;
        op2.Pt.X = Pt.X;
        op2.Pt.Y = Pt.Y;
        if (ClipperLib.use_xyz) op2.Pt.Z = Pt.Z;
        op2b = this.DupOutPt(op2, DiscardLeft);
      }
    }
    if ((Dir1 === ClipperLib.Direction.dLeftToRight) === DiscardLeft) {
      op1.Prev = op2;
      op2.Next = op1;
      op1b.Next = op2b;
      op2b.Prev = op1b;
    } else {
      op1.Next = op2;
      op2.Prev = op1;
      op1b.Prev = op2b;
      op2b.Next = op1b;
    }
    return true;
  };

  ClipperLib.Clipper.prototype.JoinPoints = function (j, outRec1, outRec2) {
    var op1 = j.OutPt1,
      op1b = new ClipperLib.OutPt();
    var op2 = j.OutPt2,
      op2b = new ClipperLib.OutPt();
    //There are 3 kinds of joins for output polygons ...
    //1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere
    //along (horizontal) collinear edges (& Join.OffPt is on the same horizontal).
    //2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same
    //location at the Bottom of the overlapping segment (& Join.OffPt is above).
    //3. StrictlySimple joins where edges touch but are not collinear and where
    //Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point.
    var isHorizontal = j.OutPt1.Pt.Y === j.OffPt.Y;
    if (
      isHorizontal &&
      ClipperLib.IntPoint.op_Equality(j.OffPt, j.OutPt1.Pt) &&
      ClipperLib.IntPoint.op_Equality(j.OffPt, j.OutPt2.Pt)
    ) {
      //Strictly Simple join ...
      if (outRec1 !== outRec2) return false;

      op1b = j.OutPt1.Next;
      while (op1b !== op1 && ClipperLib.IntPoint.op_Equality(op1b.Pt, j.OffPt)) op1b = op1b.Next;
      var reverse1 = op1b.Pt.Y > j.OffPt.Y;
      op2b = j.OutPt2.Next;
      while (op2b !== op2 && ClipperLib.IntPoint.op_Equality(op2b.Pt, j.OffPt)) op2b = op2b.Next;
      var reverse2 = op2b.Pt.Y > j.OffPt.Y;
      if (reverse1 === reverse2) return false;
      if (reverse1) {
        op1b = this.DupOutPt(op1, false);
        op2b = this.DupOutPt(op2, true);
        op1.Prev = op2;
        op2.Next = op1;
        op1b.Next = op2b;
        op2b.Prev = op1b;
        j.OutPt1 = op1;
        j.OutPt2 = op1b;
        return true;
      } else {
        op1b = this.DupOutPt(op1, true);
        op2b = this.DupOutPt(op2, false);
        op1.Next = op2;
        op2.Prev = op1;
        op1b.Prev = op2b;
        op2b.Next = op1b;
        j.OutPt1 = op1;
        j.OutPt2 = op1b;
        return true;
      }
    } else if (isHorizontal) {
      //treat horizontal joins differently to non-horizontal joins since with
      //them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt
      //may be anywhere along the horizontal edge.
      op1b = op1;
      while (op1.Prev.Pt.Y === op1.Pt.Y && op1.Prev !== op1b && op1.Prev !== op2) op1 = op1.Prev;
      while (op1b.Next.Pt.Y === op1b.Pt.Y && op1b.Next !== op1 && op1b.Next !== op2)
        op1b = op1b.Next;
      if (op1b.Next === op1 || op1b.Next === op2) return false;
      //a flat 'polygon'
      op2b = op2;
      while (op2.Prev.Pt.Y === op2.Pt.Y && op2.Prev !== op2b && op2.Prev !== op1b) op2 = op2.Prev;
      while (op2b.Next.Pt.Y === op2b.Pt.Y && op2b.Next !== op2 && op2b.Next !== op1)
        op2b = op2b.Next;
      if (op2b.Next === op2 || op2b.Next === op1) return false;
      //a flat 'polygon'
      //Op1 -. Op1b & Op2 -. Op2b are the extremites of the horizontal edges

      var $val = {
        Left: null,
        Right: null,
      };

      if (!this.GetOverlap(op1.Pt.X, op1b.Pt.X, op2.Pt.X, op2b.Pt.X, $val)) return false;
      var Left = $val.Left;
      var Right = $val.Right;

      //DiscardLeftSide: when overlapping edges are joined, a spike will created
      //which needs to be cleaned up. However, we don't want Op1 or Op2 caught up
      //on the discard Side as either may still be needed for other joins ...
      var Pt = new ClipperLib.IntPoint0();
      var DiscardLeftSide;
      if (op1.Pt.X >= Left && op1.Pt.X <= Right) {
        //Pt = op1.Pt;
        Pt.X = op1.Pt.X;
        Pt.Y = op1.Pt.Y;
        if (ClipperLib.use_xyz) Pt.Z = op1.Pt.Z;
        DiscardLeftSide = op1.Pt.X > op1b.Pt.X;
      } else if (op2.Pt.X >= Left && op2.Pt.X <= Right) {
        //Pt = op2.Pt;
        Pt.X = op2.Pt.X;
        Pt.Y = op2.Pt.Y;
        if (ClipperLib.use_xyz) Pt.Z = op2.Pt.Z;
        DiscardLeftSide = op2.Pt.X > op2b.Pt.X;
      } else if (op1b.Pt.X >= Left && op1b.Pt.X <= Right) {
        //Pt = op1b.Pt;
        Pt.X = op1b.Pt.X;
        Pt.Y = op1b.Pt.Y;
        if (ClipperLib.use_xyz) Pt.Z = op1b.Pt.Z;
        DiscardLeftSide = op1b.Pt.X > op1.Pt.X;
      } else {
        //Pt = op2b.Pt;
        Pt.X = op2b.Pt.X;
        Pt.Y = op2b.Pt.Y;
        if (ClipperLib.use_xyz) Pt.Z = op2b.Pt.Z;
        DiscardLeftSide = op2b.Pt.X > op2.Pt.X;
      }
      j.OutPt1 = op1;
      j.OutPt2 = op2;
      return this.JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide);
    } else {
      //nb: For non-horizontal joins ...
      //    1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y
      //    2. Jr.OutPt1.Pt > Jr.OffPt.Y
      //make sure the polygons are correctly oriented ...
      op1b = op1.Next;
      while (ClipperLib.IntPoint.op_Equality(op1b.Pt, op1.Pt) && op1b !== op1) op1b = op1b.Next;
      var Reverse1 =
        op1b.Pt.Y > op1.Pt.Y ||
        !ClipperLib.ClipperBase.SlopesEqual4(op1.Pt, op1b.Pt, j.OffPt, this.m_UseFullRange);
      if (Reverse1) {
        op1b = op1.Prev;
        while (ClipperLib.IntPoint.op_Equality(op1b.Pt, op1.Pt) && op1b !== op1) op1b = op1b.Prev;

        if (
          op1b.Pt.Y > op1.Pt.Y ||
          !ClipperLib.ClipperBase.SlopesEqual4(op1.Pt, op1b.Pt, j.OffPt, this.m_UseFullRange)
        )
          return false;
      }
      op2b = op2.Next;
      while (ClipperLib.IntPoint.op_Equality(op2b.Pt, op2.Pt) && op2b !== op2) op2b = op2b.Next;

      var Reverse2 =
        op2b.Pt.Y > op2.Pt.Y ||
        !ClipperLib.ClipperBase.SlopesEqual4(op2.Pt, op2b.Pt, j.OffPt, this.m_UseFullRange);
      if (Reverse2) {
        op2b = op2.Prev;
        while (ClipperLib.IntPoint.op_Equality(op2b.Pt, op2.Pt) && op2b !== op2) op2b = op2b.Prev;

        if (
          op2b.Pt.Y > op2.Pt.Y ||
          !ClipperLib.ClipperBase.SlopesEqual4(op2.Pt, op2b.Pt, j.OffPt, this.m_UseFullRange)
        )
          return false;
      }
      if (
        op1b === op1 ||
        op2b === op2 ||
        op1b === op2b ||
        (outRec1 === outRec2 && Reverse1 === Reverse2)
      )
        return false;
      if (Reverse1) {
        op1b = this.DupOutPt(op1, false);
        op2b = this.DupOutPt(op2, true);
        op1.Prev = op2;
        op2.Next = op1;
        op1b.Next = op2b;
        op2b.Prev = op1b;
        j.OutPt1 = op1;
        j.OutPt2 = op1b;
        return true;
      } else {
        op1b = this.DupOutPt(op1, true);
        op2b = this.DupOutPt(op2, false);
        op1.Next = op2;
        op2.Prev = op1;
        op1b.Prev = op2b;
        op2b.Next = op1b;
        j.OutPt1 = op1;
        j.OutPt2 = op1b;
        return true;
      }
    }
  };

  ClipperLib.Clipper.GetBounds = function (paths) {
    var i = 0,
      cnt = paths.length;
    while (i < cnt && paths[i].length === 0) i++;
    if (i === cnt) return new ClipperLib.IntRect(0, 0, 0, 0);
    var result = new ClipperLib.IntRect();
    result.left = paths[i][0].X;
    result.right = result.left;
    result.top = paths[i][0].Y;
    result.bottom = result.top;
    for (; i < cnt; i++)
      for (var j = 0, jlen = paths[i].length; j < jlen; j++) {
        if (paths[i][j].X < result.left) result.left = paths[i][j].X;
        else if (paths[i][j].X > result.right) result.right = paths[i][j].X;
        if (paths[i][j].Y < result.top) result.top = paths[i][j].Y;
        else if (paths[i][j].Y > result.bottom) result.bottom = paths[i][j].Y;
      }
    return result;
  };
  ClipperLib.Clipper.prototype.GetBounds2 = function (ops) {
    var opStart = ops;
    var result = new ClipperLib.IntRect();
    result.left = ops.Pt.X;
    result.right = ops.Pt.X;
    result.top = ops.Pt.Y;
    result.bottom = ops.Pt.Y;
    ops = ops.Next;
    while (ops !== opStart) {
      if (ops.Pt.X < result.left) result.left = ops.Pt.X;
      if (ops.Pt.X > result.right) result.right = ops.Pt.X;
      if (ops.Pt.Y < result.top) result.top = ops.Pt.Y;
      if (ops.Pt.Y > result.bottom) result.bottom = ops.Pt.Y;
      ops = ops.Next;
    }
    return result;
  };

  ClipperLib.Clipper.PointInPolygon = function (pt, path) {
    //returns 0 if false, +1 if true, -1 if pt ON polygon boundary
    //See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & Agathos
    //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf
    var result = 0,
      cnt = path.length;
    if (cnt < 3) return 0;
    var ip = path[0];
    for (var i = 1; i <= cnt; ++i) {
      var ipNext = i === cnt ? path[0] : path[i];
      if (ipNext.Y === pt.Y) {
        if (ipNext.X === pt.X || (ip.Y === pt.Y && ipNext.X > pt.X === ip.X < pt.X)) return -1;
      }
      if (ip.Y < pt.Y !== ipNext.Y < pt.Y) {
        if (ip.X >= pt.X) {
          if (ipNext.X > pt.X) result = 1 - result;
          else {
            var d = (ip.X - pt.X) * (ipNext.Y - pt.Y) - (ipNext.X - pt.X) * (ip.Y - pt.Y);
            if (d === 0) return -1;
            else if (d > 0 === ipNext.Y > ip.Y) result = 1 - result;
          }
        } else {
          if (ipNext.X > pt.X) {
            var d = (ip.X - pt.X) * (ipNext.Y - pt.Y) - (ipNext.X - pt.X) * (ip.Y - pt.Y);
            if (d === 0) return -1;
            else if (d > 0 === ipNext.Y > ip.Y) result = 1 - result;
          }
        }
      }
      ip = ipNext;
    }
    return result;
  };

  ClipperLib.Clipper.prototype.PointInPolygon = function (pt, op) {
    //returns 0 if false, +1 if true, -1 if pt ON polygon boundary
    var result = 0;
    var startOp = op;
    var ptx = pt.X,
      pty = pt.Y;
    var poly0x = op.Pt.X,
      poly0y = op.Pt.Y;
    do {
      op = op.Next;
      var poly1x = op.Pt.X,
        poly1y = op.Pt.Y;
      if (poly1y === pty) {
        if (poly1x === ptx || (poly0y === pty && poly1x > ptx === poly0x < ptx)) return -1;
      }
      if (poly0y < pty !== poly1y < pty) {
        if (poly0x >= ptx) {
          if (poly1x > ptx) result = 1 - result;
          else {
            var d = (poly0x - ptx) * (poly1y - pty) - (poly1x - ptx) * (poly0y - pty);
            if (d === 0) return -1;
            if (d > 0 === poly1y > poly0y) result = 1 - result;
          }
        } else {
          if (poly1x > ptx) {
            var d = (poly0x - ptx) * (poly1y - pty) - (poly1x - ptx) * (poly0y - pty);
            if (d === 0) return -1;
            if (d > 0 === poly1y > poly0y) result = 1 - result;
          }
        }
      }
      poly0x = poly1x;
      poly0y = poly1y;
    } while (startOp !== op);

    return result;
  };

  ClipperLib.Clipper.prototype.Poly2ContainsPoly1 = function (outPt1, outPt2) {
    var op = outPt1;
    do {
      //nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon
      var res = this.PointInPolygon(op.Pt, outPt2);
      if (res >= 0) return res > 0;
      op = op.Next;
    } while (op !== outPt1);
    return true;
  };

  ClipperLib.Clipper.prototype.FixupFirstLefts1 = function (OldOutRec, NewOutRec) {
    var outRec, firstLeft;
    for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) {
      outRec = this.m_PolyOuts[i];
      firstLeft = ClipperLib.Clipper.ParseFirstLeft(outRec.FirstLeft);
      if (outRec.Pts !== null && firstLeft === OldOutRec) {
        if (this.Poly2ContainsPoly1(outRec.Pts, NewOutRec.Pts)) outRec.FirstLeft = NewOutRec;
      }
    }
  };

  ClipperLib.Clipper.prototype.FixupFirstLefts2 = function (innerOutRec, outerOutRec) {
    //A polygon has split into two such that one is now the inner of the other.
    //It's possible that these polygons now wrap around other polygons, so check
    //every polygon that's also contained by OuterOutRec's FirstLeft container
    //(including nil) to see if they've become inner to the new inner polygon ...
    var orfl = outerOutRec.FirstLeft;
    var outRec, firstLeft;
    for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) {
      outRec = this.m_PolyOuts[i];
      if (outRec.Pts === null || outRec === outerOutRec || outRec === innerOutRec) continue;
      firstLeft = ClipperLib.Clipper.ParseFirstLeft(outRec.FirstLeft);
      if (firstLeft !== orfl && firstLeft !== innerOutRec && firstLeft !== outerOutRec) continue;
      if (this.Poly2ContainsPoly1(outRec.Pts, innerOutRec.Pts)) outRec.FirstLeft = innerOutRec;
      else if (this.Poly2ContainsPoly1(outRec.Pts, outerOutRec.Pts)) outRec.FirstLeft = outerOutRec;
      else if (outRec.FirstLeft === innerOutRec || outRec.FirstLeft === outerOutRec)
        outRec.FirstLeft = orfl;
    }
  };

  ClipperLib.Clipper.prototype.FixupFirstLefts3 = function (OldOutRec, NewOutRec) {
    //same as FixupFirstLefts1 but doesn't call Poly2ContainsPoly1()
    var outRec;
    var firstLeft;
    for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) {
      outRec = this.m_PolyOuts[i];
      firstLeft = ClipperLib.Clipper.ParseFirstLeft(outRec.FirstLeft);
      if (outRec.Pts !== null && firstLeft === OldOutRec) outRec.FirstLeft = NewOutRec;
    }
  };

  ClipperLib.Clipper.ParseFirstLeft = function (FirstLeft) {
    while (FirstLeft !== null && FirstLeft.Pts === null) FirstLeft = FirstLeft.FirstLeft;
    return FirstLeft;
  };

  ClipperLib.Clipper.prototype.JoinCommonEdges = function () {
    for (var i = 0, ilen = this.m_Joins.length; i < ilen; i++) {
      var join = this.m_Joins[i];
      var outRec1 = this.GetOutRec(join.OutPt1.Idx);
      var outRec2 = this.GetOutRec(join.OutPt2.Idx);
      if (outRec1.Pts === null || outRec2.Pts === null) continue;

      if (outRec1.IsOpen || outRec2.IsOpen) {
        continue;
      }

      //get the polygon fragment with the correct hole state (FirstLeft)
      //before calling JoinPoints() ...
      var holeStateRec;
      if (outRec1 === outRec2) holeStateRec = outRec1;
      else if (this.OutRec1RightOfOutRec2(outRec1, outRec2)) holeStateRec = outRec2;
      else if (this.OutRec1RightOfOutRec2(outRec2, outRec1)) holeStateRec = outRec1;
      else holeStateRec = this.GetLowermostRec(outRec1, outRec2);

      if (!this.JoinPoints(join, outRec1, outRec2)) continue;

      if (outRec1 === outRec2) {
        //instead of joining two polygons, we've just created a new one by
        //splitting one polygon into two.
        outRec1.Pts = join.OutPt1;
        outRec1.BottomPt = null;
        outRec2 = this.CreateOutRec();
        outRec2.Pts = join.OutPt2;
        //update all OutRec2.Pts Idx's ...
        this.UpdateOutPtIdxs(outRec2);

        if (this.Poly2ContainsPoly1(outRec2.Pts, outRec1.Pts)) {
          //outRec1 contains outRec2 ...
          outRec2.IsHole = !outRec1.IsHole;
          outRec2.FirstLeft = outRec1;
          if (this.m_UsingPolyTree) this.FixupFirstLefts2(outRec2, outRec1);
          if ((outRec2.IsHole ^ this.ReverseSolution) == this.Area$1(outRec2) > 0)
            this.ReversePolyPtLinks(outRec2.Pts);
        } else if (this.Poly2ContainsPoly1(outRec1.Pts, outRec2.Pts)) {
          //outRec2 contains outRec1 ...
          outRec2.IsHole = outRec1.IsHole;
          outRec1.IsHole = !outRec2.IsHole;
          outRec2.FirstLeft = outRec1.FirstLeft;
          outRec1.FirstLeft = outRec2;
          if (this.m_UsingPolyTree) this.FixupFirstLefts2(outRec1, outRec2);

          if ((outRec1.IsHole ^ this.ReverseSolution) == this.Area$1(outRec1) > 0)
            this.ReversePolyPtLinks(outRec1.Pts);
        } else {
          //the 2 polygons are completely separate ...
          outRec2.IsHole = outRec1.IsHole;
          outRec2.FirstLeft = outRec1.FirstLeft;
          //fixup FirstLeft pointers that may need reassigning to OutRec2
          if (this.m_UsingPolyTree) this.FixupFirstLefts1(outRec1, outRec2);
        }
      } else {
        //joined 2 polygons together ...
        outRec2.Pts = null;
        outRec2.BottomPt = null;
        outRec2.Idx = outRec1.Idx;
        outRec1.IsHole = holeStateRec.IsHole;
        if (holeStateRec === outRec2) outRec1.FirstLeft = outRec2.FirstLeft;
        outRec2.FirstLeft = outRec1;
        //fixup FirstLeft pointers that may need reassigning to OutRec1
        if (this.m_UsingPolyTree) this.FixupFirstLefts3(outRec2, outRec1);
      }
    }
  };

  ClipperLib.Clipper.prototype.UpdateOutPtIdxs = function (outrec) {
    var op = outrec.Pts;
    do {
      op.Idx = outrec.Idx;
      op = op.Prev;
    } while (op !== outrec.Pts);
  };

  ClipperLib.Clipper.prototype.DoSimplePolygons = function () {
    var i = 0;
    while (i < this.m_PolyOuts.length) {
      var outrec = this.m_PolyOuts[i++];
      var op = outrec.Pts;
      if (op === null || outrec.IsOpen) continue;
      do {
        //for each Pt in Polygon until duplicate found do ...
        var op2 = op.Next;
        while (op2 !== outrec.Pts) {
          if (
            ClipperLib.IntPoint.op_Equality(op.Pt, op2.Pt) &&
            op2.Next !== op &&
            op2.Prev !== op
          ) {
            //split the polygon into two ...
            var op3 = op.Prev;
            var op4 = op2.Prev;
            op.Prev = op4;
            op4.Next = op;
            op2.Prev = op3;
            op3.Next = op2;
            outrec.Pts = op;
            var outrec2 = this.CreateOutRec();
            outrec2.Pts = op2;
            this.UpdateOutPtIdxs(outrec2);
            if (this.Poly2ContainsPoly1(outrec2.Pts, outrec.Pts)) {
              //OutRec2 is contained by OutRec1 ...
              outrec2.IsHole = !outrec.IsHole;
              outrec2.FirstLeft = outrec;
              if (this.m_UsingPolyTree) this.FixupFirstLefts2(outrec2, outrec);
            } else if (this.Poly2ContainsPoly1(outrec.Pts, outrec2.Pts)) {
              //OutRec1 is contained by OutRec2 ...
              outrec2.IsHole = outrec.IsHole;
              outrec.IsHole = !outrec2.IsHole;
              outrec2.FirstLeft = outrec.FirstLeft;
              outrec.FirstLeft = outrec2;
              if (this.m_UsingPolyTree) this.FixupFirstLefts2(outrec, outrec2);
            } else {
              //the 2 polygons are separate ...
              outrec2.IsHole = outrec.IsHole;
              outrec2.FirstLeft = outrec.FirstLeft;
              if (this.m_UsingPolyTree) this.FixupFirstLefts1(outrec, outrec2);
            }
            op2 = op;
            //ie get ready for the next iteration
          }
          op2 = op2.Next;
        }
        op = op.Next;
      } while (op !== outrec.Pts);
    }
  };

  ClipperLib.Clipper.Area = function (poly) {
    if (!Array.isArray(poly)) return 0;
    var cnt = poly.length;
    if (cnt < 3) return 0;
    var a = 0;
    for (var i = 0, j = cnt - 1; i < cnt; ++i) {
      a += (poly[j].X + poly[i].X) * (poly[j].Y - poly[i].Y);
      j = i;
    }
    return -a * 0.5;
  };

  ClipperLib.Clipper.prototype.Area = function (op) {
    var opFirst = op;
    if (op === null) return 0;
    var a = 0;
    do {
      a = a + (op.Prev.Pt.X + op.Pt.X) * (op.Prev.Pt.Y - op.Pt.Y);
      op = op.Next;
    } while (op !== opFirst); // && typeof op !== 'undefined');
    return a * 0.5;
  };

  ClipperLib.Clipper.prototype.Area$1 = function (outRec) {
    return this.Area(outRec.Pts);
  };

  ClipperLib.Clipper.SimplifyPolygon = function (poly, fillType) {
    var result = new Array();
    var c = new ClipperLib.Clipper(0);
    c.StrictlySimple = true;
    c.AddPath(poly, ClipperLib.PolyType.ptSubject, true);
    c.Execute(ClipperLib.ClipType.ctUnion, result, fillType, fillType);
    return result;
  };

  ClipperLib.Clipper.SimplifyPolygons = function (polys, fillType) {
    if (typeof fillType === "undefined") fillType = ClipperLib.PolyFillType.pftEvenOdd;
    var result = new Array();
    var c = new ClipperLib.Clipper(0);
    c.StrictlySimple = true;
    c.AddPaths(polys, ClipperLib.PolyType.ptSubject, true);
    c.Execute(ClipperLib.ClipType.ctUnion, result, fillType, fillType);
    return result;
  };

  ClipperLib.Clipper.DistanceSqrd = function (pt1, pt2) {
    var dx = pt1.X - pt2.X;
    var dy = pt1.Y - pt2.Y;
    return dx * dx + dy * dy;
  };

  ClipperLib.Clipper.DistanceFromLineSqrd = function (pt, ln1, ln2) {
    //The equation of a line in general form (Ax + By + C = 0)
    //given 2 points (x¹,y¹) & (x²,y²) is ...
    //(y¹ - y²)x + (x² - x¹)y + (y² - y¹)x¹ - (x² - x¹)y¹ = 0
    //A = (y¹ - y²); B = (x² - x¹); C = (y² - y¹)x¹ - (x² - x¹)y¹
    //perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²)
    //see http://en.wikipedia.org/wiki/Perpendicular_distance
    var A = ln1.Y - ln2.Y;
    var B = ln2.X - ln1.X;
    var C = A * ln1.X + B * ln1.Y;
    C = A * pt.X + B * pt.Y - C;
    return (C * C) / (A * A + B * B);
  };

  ClipperLib.Clipper.SlopesNearCollinear = function (pt1, pt2, pt3, distSqrd) {
    //this function is more accurate when the point that's GEOMETRICALLY
    //between the other 2 points is the one that's tested for distance.
    //nb: with 'spikes', either pt1 or pt3 is geometrically between the other pts
    if (Math.abs(pt1.X - pt2.X) > Math.abs(pt1.Y - pt2.Y)) {
      if (pt1.X > pt2.X === pt1.X < pt3.X)
        return ClipperLib.Clipper.DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd;
      else if (pt2.X > pt1.X === pt2.X < pt3.X)
        return ClipperLib.Clipper.DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd;
      else return ClipperLib.Clipper.DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd;
    } else {
      if (pt1.Y > pt2.Y === pt1.Y < pt3.Y)
        return ClipperLib.Clipper.DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd;
      else if (pt2.Y > pt1.Y === pt2.Y < pt3.Y)
        return ClipperLib.Clipper.DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd;
      else return ClipperLib.Clipper.DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd;
    }
  };

  ClipperLib.Clipper.PointsAreClose = function (pt1, pt2, distSqrd) {
    var dx = pt1.X - pt2.X;
    var dy = pt1.Y - pt2.Y;
    return dx * dx + dy * dy <= distSqrd;
  };

  ClipperLib.Clipper.ExcludeOp = function (op) {
    var result = op.Prev;
    result.Next = op.Next;
    op.Next.Prev = result;
    result.Idx = 0;
    return result;
  };

  ClipperLib.Clipper.CleanPolygon = function (path, distance) {
    if (typeof distance === "undefined") distance = 1.415;
    //distance = proximity in units/pixels below which vertices will be stripped.
    //Default ~= sqrt(2) so when adjacent vertices or semi-adjacent vertices have
    //both x & y coords within 1 unit, then the second vertex will be stripped.
    var cnt = path.length;
    if (cnt === 0) return new Array();
    var outPts = new Array(cnt);
    for (var i = 0; i < cnt; ++i) outPts[i] = new ClipperLib.OutPt();
    for (var i = 0; i < cnt; ++i) {
      outPts[i].Pt = path[i];
      outPts[i].Next = outPts[(i + 1) % cnt];
      outPts[i].Next.Prev = outPts[i];
      outPts[i].Idx = 0;
    }
    var distSqrd = distance * distance;
    var op = outPts[0];
    while (op.Idx === 0 && op.Next !== op.Prev) {
      if (ClipperLib.Clipper.PointsAreClose(op.Pt, op.Prev.Pt, distSqrd)) {
        op = ClipperLib.Clipper.ExcludeOp(op);
        cnt--;
      } else if (ClipperLib.Clipper.PointsAreClose(op.Prev.Pt, op.Next.Pt, distSqrd)) {
        ClipperLib.Clipper.ExcludeOp(op.Next);
        op = ClipperLib.Clipper.ExcludeOp(op);
        cnt -= 2;
      } else if (ClipperLib.Clipper.SlopesNearCollinear(op.Prev.Pt, op.Pt, op.Next.Pt, distSqrd)) {
        op = ClipperLib.Clipper.ExcludeOp(op);
        cnt--;
      } else {
        op.Idx = 1;
        op = op.Next;
      }
    }
    if (cnt < 3) cnt = 0;
    var result = new Array(cnt);
    for (var i = 0; i < cnt; ++i) {
      result[i] = new ClipperLib.IntPoint1(op.Pt);
      op = op.Next;
    }
    outPts = null;
    return result;
  };

  ClipperLib.Clipper.CleanPolygons = function (polys, distance) {
    var result = new Array(polys.length);
    for (var i = 0, ilen = polys.length; i < ilen; i++)
      result[i] = ClipperLib.Clipper.CleanPolygon(polys[i], distance);
    return result;
  };

  ClipperLib.Clipper.Minkowski = function (pattern, path, IsSum, IsClosed) {
    var delta = IsClosed ? 1 : 0;
    var polyCnt = pattern.length;
    var pathCnt = path.length;
    var result = new Array();
    if (IsSum)
      for (var i = 0; i < pathCnt; i++) {
        var p = new Array(polyCnt);
        for (var j = 0, jlen = pattern.length, ip = pattern[j]; j < jlen; j++, ip = pattern[j])
          p[j] = new ClipperLib.IntPoint2(path[i].X + ip.X, path[i].Y + ip.Y);
        result.push(p);
      }
    else
      for (var i = 0; i < pathCnt; i++) {
        var p = new Array(polyCnt);
        for (var j = 0, jlen = pattern.length, ip = pattern[j]; j < jlen; j++, ip = pattern[j])
          p[j] = new ClipperLib.IntPoint2(path[i].X - ip.X, path[i].Y - ip.Y);
        result.push(p);
      }
    var quads = new Array();
    for (var i = 0; i < pathCnt - 1 + delta; i++)
      for (var j = 0; j < polyCnt; j++) {
        var quad = new Array();
        quad.push(result[i % pathCnt][j % polyCnt]);
        quad.push(result[(i + 1) % pathCnt][j % polyCnt]);
        quad.push(result[(i + 1) % pathCnt][(j + 1) % polyCnt]);
        quad.push(result[i % pathCnt][(j + 1) % polyCnt]);
        if (!ClipperLib.Clipper.Orientation(quad)) quad.reverse();
        quads.push(quad);
      }
    return quads;
  };

  ClipperLib.Clipper.MinkowskiSum = function (pattern, path_or_paths, pathIsClosed) {
    if (!(path_or_paths[0] instanceof Array)) {
      var path = path_or_paths;
      var paths = ClipperLib.Clipper.Minkowski(pattern, path, true, pathIsClosed);
      var c = new ClipperLib.Clipper();
      c.AddPaths(paths, ClipperLib.PolyType.ptSubject, true);
      c.Execute(
        ClipperLib.ClipType.ctUnion,
        paths,
        ClipperLib.PolyFillType.pftNonZero,
        ClipperLib.PolyFillType.pftNonZero,
      );
      return paths;
    } else {
      var paths = path_or_paths;
      var solution = new ClipperLib.Paths();
      var c = new ClipperLib.Clipper();
      for (var i = 0; i < paths.length; ++i) {
        var tmp = ClipperLib.Clipper.Minkowski(pattern, paths[i], true, pathIsClosed);
        c.AddPaths(tmp, ClipperLib.PolyType.ptSubject, true);
        if (pathIsClosed) {
          var path = ClipperLib.Clipper.TranslatePath(paths[i], pattern[0]);
          c.AddPath(path, ClipperLib.PolyType.ptClip, true);
        }
      }
      c.Execute(
        ClipperLib.ClipType.ctUnion,
        solution,
        ClipperLib.PolyFillType.pftNonZero,
        ClipperLib.PolyFillType.pftNonZero,
      );
      return solution;
    }
  };

  ClipperLib.Clipper.TranslatePath = function (path, delta) {
    var outPath = new ClipperLib.Path();
    for (var i = 0; i < path.length; i++)
      outPath.push(new ClipperLib.IntPoint2(path[i].X + delta.X, path[i].Y + delta.Y));
    return outPath;
  };

  ClipperLib.Clipper.MinkowskiDiff = function (poly1, poly2) {
    var paths = ClipperLib.Clipper.Minkowski(poly1, poly2, false, true);
    var c = new ClipperLib.Clipper();
    c.AddPaths(paths, ClipperLib.PolyType.ptSubject, true);
    c.Execute(
      ClipperLib.ClipType.ctUnion,
      paths,
      ClipperLib.PolyFillType.pftNonZero,
      ClipperLib.PolyFillType.pftNonZero,
    );
    return paths;
  };

  ClipperLib.Clipper.PolyTreeToPaths = function (polytree) {
    var result = new Array();
    //result.set_Capacity(polytree.get_Total());
    ClipperLib.Clipper.AddPolyNodeToPaths(polytree, ClipperLib.Clipper.NodeType.ntAny, result);
    return result;
  };

  ClipperLib.Clipper.AddPolyNodeToPaths = function (polynode, nt, paths) {
    var match = true;
    switch (nt) {
      case ClipperLib.Clipper.NodeType.ntOpen:
        return;
      case ClipperLib.Clipper.NodeType.ntClosed:
        match = !polynode.IsOpen;
        break;
      default:
        break;
    }
    if (polynode.m_polygon.length > 0 && match) paths.push(polynode.m_polygon);
    for (
      var $i3 = 0, $t3 = polynode.Childs(), $l3 = $t3.length, pn = $t3[$i3];
      $i3 < $l3;
      $i3++, pn = $t3[$i3]
    )
      ClipperLib.Clipper.AddPolyNodeToPaths(pn, nt, paths);
  };

  ClipperLib.Clipper.OpenPathsFromPolyTree = function (polytree) {
    var result = new ClipperLib.Paths();
    //result.set_Capacity(polytree.ChildCount());
    for (var i = 0, ilen = polytree.ChildCount(); i < ilen; i++)
      if (polytree.Childs()[i].IsOpen) result.push(polytree.Childs()[i].m_polygon);
    return result;
  };

  ClipperLib.Clipper.ClosedPathsFromPolyTree = function (polytree) {
    var result = new ClipperLib.Paths();
    //result.set_Capacity(polytree.Total());
    ClipperLib.Clipper.AddPolyNodeToPaths(polytree, ClipperLib.Clipper.NodeType.ntClosed, result);
    return result;
  };

  Inherit(ClipperLib.Clipper, ClipperLib.ClipperBase);
  ClipperLib.Clipper.NodeType = {
    ntAny: 0,
    ntOpen: 1,
    ntClosed: 2,
  };

  /**
   * @constructor
   */
  ClipperLib.ClipperOffset = function (miterLimit, arcTolerance) {
    if (typeof miterLimit === "undefined") miterLimit = 2;
    if (typeof arcTolerance === "undefined")
      arcTolerance = ClipperLib.ClipperOffset.def_arc_tolerance;
    this.m_destPolys = new ClipperLib.Paths();
    this.m_srcPoly = new ClipperLib.Path();
    this.m_destPoly = new ClipperLib.Path();
    this.m_normals = new Array();
    this.m_delta = 0;
    this.m_sinA = 0;
    this.m_sin = 0;
    this.m_cos = 0;
    this.m_miterLim = 0;
    this.m_StepsPerRad = 0;
    this.m_lowest = new ClipperLib.IntPoint0();
    this.m_polyNodes = new ClipperLib.PolyNode();
    this.MiterLimit = miterLimit;
    this.ArcTolerance = arcTolerance;
    this.m_lowest.X = -1;
  };

  ClipperLib.ClipperOffset.two_pi = 6.28318530717959;
  ClipperLib.ClipperOffset.def_arc_tolerance = 0.25;
  ClipperLib.ClipperOffset.prototype.Clear = function () {
    ClipperLib.Clear(this.m_polyNodes.Childs());
    this.m_lowest.X = -1;
  };

  ClipperLib.ClipperOffset.Round = ClipperLib.Clipper.Round;
  ClipperLib.ClipperOffset.prototype.AddPath = function (path, joinType, endType) {
    var highI = path.length - 1;
    if (highI < 0) return;
    var newNode = new ClipperLib.PolyNode();
    newNode.m_jointype = joinType;
    newNode.m_endtype = endType;
    //strip duplicate points from path and also get index to the lowest point ...
    if (
      endType === ClipperLib.EndType.etClosedLine ||
      endType === ClipperLib.EndType.etClosedPolygon
    )
      while (highI > 0 && ClipperLib.IntPoint.op_Equality(path[0], path[highI])) highI--;
    //newNode.m_polygon.set_Capacity(highI + 1);
    newNode.m_polygon.push(path[0]);
    var j = 0,
      k = 0;
    for (var i = 1; i <= highI; i++)
      if (ClipperLib.IntPoint.op_Inequality(newNode.m_polygon[j], path[i])) {
        j++;
        newNode.m_polygon.push(path[i]);
        if (
          path[i].Y > newNode.m_polygon[k].Y ||
          (path[i].Y === newNode.m_polygon[k].Y && path[i].X < newNode.m_polygon[k].X)
        )
          k = j;
      }
    if (endType === ClipperLib.EndType.etClosedPolygon && j < 2) return;

    this.m_polyNodes.AddChild(newNode);
    //if this path's lowest pt is lower than all the others then update m_lowest
    if (endType !== ClipperLib.EndType.etClosedPolygon) return;
    if (this.m_lowest.X < 0)
      this.m_lowest = new ClipperLib.IntPoint2(this.m_polyNodes.ChildCount() - 1, k);
    else {
      var ip = this.m_polyNodes.Childs()[this.m_lowest.X].m_polygon[this.m_lowest.Y];
      if (
        newNode.m_polygon[k].Y > ip.Y ||
        (newNode.m_polygon[k].Y === ip.Y && newNode.m_polygon[k].X < ip.X)
      )
        this.m_lowest = new ClipperLib.IntPoint2(this.m_polyNodes.ChildCount() - 1, k);
    }
  };

  ClipperLib.ClipperOffset.prototype.AddPaths = function (paths, joinType, endType) {
    for (var i = 0, ilen = paths.length; i < ilen; i++) this.AddPath(paths[i], joinType, endType);
  };

  ClipperLib.ClipperOffset.prototype.FixOrientations = function () {
    //fixup orientations of all closed paths if the orientation of the
    //closed path with the lowermost vertex is wrong ...
    if (
      this.m_lowest.X >= 0 &&
      !ClipperLib.Clipper.Orientation(this.m_polyNodes.Childs()[this.m_lowest.X].m_polygon)
    ) {
      for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) {
        var node = this.m_polyNodes.Childs()[i];
        if (
          node.m_endtype === ClipperLib.EndType.etClosedPolygon ||
          (node.m_endtype === ClipperLib.EndType.etClosedLine &&
            ClipperLib.Clipper.Orientation(node.m_polygon))
        )
          node.m_polygon.reverse();
      }
    } else {
      for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) {
        var node = this.m_polyNodes.Childs()[i];
        if (
          node.m_endtype === ClipperLib.EndType.etClosedLine &&
          !ClipperLib.Clipper.Orientation(node.m_polygon)
        )
          node.m_polygon.reverse();
      }
    }
  };

  ClipperLib.ClipperOffset.GetUnitNormal = function (pt1, pt2) {
    var dx = pt2.X - pt1.X;
    var dy = pt2.Y - pt1.Y;
    if (dx === 0 && dy === 0) return new ClipperLib.DoublePoint2(0, 0);
    var f = 1 / Math.sqrt(dx * dx + dy * dy);
    dx *= f;
    dy *= f;
    return new ClipperLib.DoublePoint2(dy, -dx);
  };

  ClipperLib.ClipperOffset.prototype.DoOffset = function (delta) {
    this.m_destPolys = new Array();
    this.m_delta = delta;
    //if Zero offset, just copy any CLOSED polygons to m_p and return ...
    if (ClipperLib.ClipperBase.near_zero(delta)) {
      //this.m_destPolys.set_Capacity(this.m_polyNodes.ChildCount);
      for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) {
        var node = this.m_polyNodes.Childs()[i];
        if (node.m_endtype === ClipperLib.EndType.etClosedPolygon)
          this.m_destPolys.push(node.m_polygon);
      }
      return;
    }
    //see offset_triginometry3.svg in the documentation folder ...
    if (this.MiterLimit > 2) this.m_miterLim = 2 / (this.MiterLimit * this.MiterLimit);
    else this.m_miterLim = 0.5;
    var y;
    if (this.ArcTolerance <= 0) y = ClipperLib.ClipperOffset.def_arc_tolerance;
    else if (this.ArcTolerance > Math.abs(delta) * ClipperLib.ClipperOffset.def_arc_tolerance)
      y = Math.abs(delta) * ClipperLib.ClipperOffset.def_arc_tolerance;
    else y = this.ArcTolerance;
    //see offset_triginometry2.svg in the documentation folder ...
    var steps = 3.14159265358979 / Math.acos(1 - y / Math.abs(delta));
    this.m_sin = Math.sin(ClipperLib.ClipperOffset.two_pi / steps);
    this.m_cos = Math.cos(ClipperLib.ClipperOffset.two_pi / steps);
    this.m_StepsPerRad = steps / ClipperLib.ClipperOffset.two_pi;
    if (delta < 0) this.m_sin = -this.m_sin;
    //this.m_destPolys.set_Capacity(this.m_polyNodes.ChildCount * 2);
    for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) {
      var node = this.m_polyNodes.Childs()[i];
      this.m_srcPoly = node.m_polygon;
      var len = this.m_srcPoly.length;
      if (
        len === 0 ||
        (delta <= 0 && (len < 3 || node.m_endtype !== ClipperLib.EndType.etClosedPolygon))
      )
        continue;
      this.m_destPoly = new Array();
      if (len === 1) {
        if (node.m_jointype === ClipperLib.JoinType.jtRound) {
          var X = 1,
            Y = 0;
          for (var j = 1; j <= steps; j++) {
            this.m_destPoly.push(
              new ClipperLib.IntPoint2(
                ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + X * delta),
                ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + Y * delta),
              ),
            );
            var X2 = X;
            X = X * this.m_cos - this.m_sin * Y;
            Y = X2 * this.m_sin + Y * this.m_cos;
          }
        } else {
          var X = -1,
            Y = -1;
          for (var j = 0; j < 4; ++j) {
            this.m_destPoly.push(
              new ClipperLib.IntPoint2(
                ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + X * delta),
                ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + Y * delta),
              ),
            );
            if (X < 0) X = 1;
            else if (Y < 0) Y = 1;
            else X = -1;
          }
        }
        this.m_destPolys.push(this.m_destPoly);
        continue;
      }
      //build m_normals ...
      this.m_normals.length = 0;
      //this.m_normals.set_Capacity(len);
      for (var j = 0; j < len - 1; j++)
        this.m_normals.push(
          ClipperLib.ClipperOffset.GetUnitNormal(this.m_srcPoly[j], this.m_srcPoly[j + 1]),
        );
      if (
        node.m_endtype === ClipperLib.EndType.etClosedLine ||
        node.m_endtype === ClipperLib.EndType.etClosedPolygon
      )
        this.m_normals.push(
          ClipperLib.ClipperOffset.GetUnitNormal(this.m_srcPoly[len - 1], this.m_srcPoly[0]),
        );
      else this.m_normals.push(new ClipperLib.DoublePoint1(this.m_normals[len - 2]));
      if (node.m_endtype === ClipperLib.EndType.etClosedPolygon) {
        var k = len - 1;
        for (var j = 0; j < len; j++) k = this.OffsetPoint(j, k, node.m_jointype);
        this.m_destPolys.push(this.m_destPoly);
      } else if (node.m_endtype === ClipperLib.EndType.etClosedLine) {
        var k = len - 1;
        for (var j = 0; j < len; j++) k = this.OffsetPoint(j, k, node.m_jointype);
        this.m_destPolys.push(this.m_destPoly);
        this.m_destPoly = new Array();
        //re-build m_normals ...
        var n = this.m_normals[len - 1];
        for (var j = len - 1; j > 0; j--)
          this.m_normals[j] = new ClipperLib.DoublePoint2(
            -this.m_normals[j - 1].X,
            -this.m_normals[j - 1].Y,
          );
        this.m_normals[0] = new ClipperLib.DoublePoint2(-n.X, -n.Y);
        k = 0;
        for (var j = len - 1; j >= 0; j--) k = this.OffsetPoint(j, k, node.m_jointype);
        this.m_destPolys.push(this.m_destPoly);
      } else {
        var k = 0;
        for (var j = 1; j < len - 1; ++j) k = this.OffsetPoint(j, k, node.m_jointype);
        var pt1;
        if (node.m_endtype === ClipperLib.EndType.etOpenButt) {
          var j = len - 1;
          pt1 = new ClipperLib.IntPoint2(
            ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * delta),
            ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * delta),
          );
          this.m_destPoly.push(pt1);
          pt1 = new ClipperLib.IntPoint2(
            ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X - this.m_normals[j].X * delta),
            ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y - this.m_normals[j].Y * delta),
          );
          this.m_destPoly.push(pt1);
        } else {
          var j = len - 1;
          k = len - 2;
          this.m_sinA = 0;
          this.m_normals[j] = new ClipperLib.DoublePoint2(
            -this.m_normals[j].X,
            -this.m_normals[j].Y,
          );
          if (node.m_endtype === ClipperLib.EndType.etOpenSquare) this.DoSquare(j, k);
          else this.DoRound(j, k);
        }
        //re-build m_normals ...
        for (var j = len - 1; j > 0; j--)
          this.m_normals[j] = new ClipperLib.DoublePoint2(
            -this.m_normals[j - 1].X,
            -this.m_normals[j - 1].Y,
          );
        this.m_normals[0] = new ClipperLib.DoublePoint2(-this.m_normals[1].X, -this.m_normals[1].Y);
        k = len - 1;
        for (var j = k - 1; j > 0; --j) k = this.OffsetPoint(j, k, node.m_jointype);
        if (node.m_endtype === ClipperLib.EndType.etOpenButt) {
          pt1 = new ClipperLib.IntPoint2(
            ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X - this.m_normals[0].X * delta),
            ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y - this.m_normals[0].Y * delta),
          );
          this.m_destPoly.push(pt1);
          pt1 = new ClipperLib.IntPoint2(
            ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + this.m_normals[0].X * delta),
            ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + this.m_normals[0].Y * delta),
          );
          this.m_destPoly.push(pt1);
        } else {
          k = 1;
          this.m_sinA = 0;
          if (node.m_endtype === ClipperLib.EndType.etOpenSquare) this.DoSquare(0, 1);
          else this.DoRound(0, 1);
        }
        this.m_destPolys.push(this.m_destPoly);
      }
    }
  };

  ClipperLib.ClipperOffset.prototype.Execute = function () {
    var a = arguments,
      ispolytree = a[0] instanceof ClipperLib.PolyTree;
    if (!ispolytree) // function (solution, delta)
    {
      var solution = a[0],
        delta = a[1];
      ClipperLib.Clear(solution);
      this.FixOrientations();
      this.DoOffset(delta);
      //now clean up 'corners' ...
      var clpr = new ClipperLib.Clipper(0);
      clpr.AddPaths(this.m_destPolys, ClipperLib.PolyType.ptSubject, true);
      if (delta > 0) {
        clpr.Execute(
          ClipperLib.ClipType.ctUnion,
          solution,
          ClipperLib.PolyFillType.pftPositive,
          ClipperLib.PolyFillType.pftPositive,
        );
      } else {
        var r = ClipperLib.Clipper.GetBounds(this.m_destPolys);
        var outer = new ClipperLib.Path();
        outer.push(new ClipperLib.IntPoint2(r.left - 10, r.bottom + 10));
        outer.push(new ClipperLib.IntPoint2(r.right + 10, r.bottom + 10));
        outer.push(new ClipperLib.IntPoint2(r.right + 10, r.top - 10));
        outer.push(new ClipperLib.IntPoint2(r.left - 10, r.top - 10));
        clpr.AddPath(outer, ClipperLib.PolyType.ptSubject, true);
        clpr.ReverseSolution = true;
        clpr.Execute(
          ClipperLib.ClipType.ctUnion,
          solution,
          ClipperLib.PolyFillType.pftNegative,
          ClipperLib.PolyFillType.pftNegative,
        );
        if (solution.length > 0) solution.splice(0, 1);
      }
      //console.log(JSON.stringify(solution));
    } else // function (polytree, delta)
    {
      var solution = a[0],
        delta = a[1];
      solution.Clear();
      this.FixOrientations();
      this.DoOffset(delta);
      //now clean up 'corners' ...
      var clpr = new ClipperLib.Clipper(0);
      clpr.AddPaths(this.m_destPolys, ClipperLib.PolyType.ptSubject, true);
      if (delta > 0) {
        clpr.Execute(
          ClipperLib.ClipType.ctUnion,
          solution,
          ClipperLib.PolyFillType.pftPositive,
          ClipperLib.PolyFillType.pftPositive,
        );
      } else {
        var r = ClipperLib.Clipper.GetBounds(this.m_destPolys);
        var outer = new ClipperLib.Path();
        outer.push(new ClipperLib.IntPoint2(r.left - 10, r.bottom + 10));
        outer.push(new ClipperLib.IntPoint2(r.right + 10, r.bottom + 10));
        outer.push(new ClipperLib.IntPoint2(r.right + 10, r.top - 10));
        outer.push(new ClipperLib.IntPoint2(r.left - 10, r.top - 10));
        clpr.AddPath(outer, ClipperLib.PolyType.ptSubject, true);
        clpr.ReverseSolution = true;
        clpr.Execute(
          ClipperLib.ClipType.ctUnion,
          solution,
          ClipperLib.PolyFillType.pftNegative,
          ClipperLib.PolyFillType.pftNegative,
        );
        //remove the outer PolyNode rectangle ...
        if (solution.ChildCount() === 1 && solution.Childs()[0].ChildCount() > 0) {
          var outerNode = solution.Childs()[0];
          //solution.Childs.set_Capacity(outerNode.ChildCount);
          solution.Childs()[0] = outerNode.Childs()[0];
          solution.Childs()[0].m_Parent = solution;
          for (var i = 1; i < outerNode.ChildCount(); i++) solution.AddChild(outerNode.Childs()[i]);
        } else solution.Clear();
      }
    }
  };

  ClipperLib.ClipperOffset.prototype.OffsetPoint = function (j, k, jointype) {
    //cross product ...
    this.m_sinA =
      this.m_normals[k].X * this.m_normals[j].Y - this.m_normals[j].X * this.m_normals[k].Y;

    if (Math.abs(this.m_sinA * this.m_delta) < 1.0) {
      //dot product ...
      var cosA =
        this.m_normals[k].X * this.m_normals[j].X + this.m_normals[j].Y * this.m_normals[k].Y;
      if (cosA > 0) // angle ==> 0 degrees
      {
        this.m_destPoly.push(
          new ClipperLib.IntPoint2(
            ClipperLib.ClipperOffset.Round(
              this.m_srcPoly[j].X + this.m_normals[k].X * this.m_delta,
            ),
            ClipperLib.ClipperOffset.Round(
              this.m_srcPoly[j].Y + this.m_normals[k].Y * this.m_delta,
            ),
          ),
        );
        return k;
      }
      //else angle ==> 180 degrees
    } else if (this.m_sinA > 1) this.m_sinA = 1.0;
    else if (this.m_sinA < -1) this.m_sinA = -1.0;
    if (this.m_sinA * this.m_delta < 0) {
      this.m_destPoly.push(
        new ClipperLib.IntPoint2(
          ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[k].X * this.m_delta),
          ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[k].Y * this.m_delta),
        ),
      );
      this.m_destPoly.push(new ClipperLib.IntPoint1(this.m_srcPoly[j]));
      this.m_destPoly.push(
        new ClipperLib.IntPoint2(
          ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * this.m_delta),
          ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * this.m_delta),
        ),
      );
    } else
      switch (jointype) {
        case ClipperLib.JoinType.jtMiter: {
          var r =
            1 +
            (this.m_normals[j].X * this.m_normals[k].X + this.m_normals[j].Y * this.m_normals[k].Y);
          if (r >= this.m_miterLim) this.DoMiter(j, k, r);
          else this.DoSquare(j, k);
          break;
        }
        case ClipperLib.JoinType.jtSquare:
          this.DoSquare(j, k);
          break;
        case ClipperLib.JoinType.jtRound:
          this.DoRound(j, k);
          break;
      }
    k = j;
    return k;
  };

  ClipperLib.ClipperOffset.prototype.DoSquare = function (j, k) {
    var dx = Math.tan(
      Math.atan2(
        this.m_sinA,
        this.m_normals[k].X * this.m_normals[j].X + this.m_normals[k].Y * this.m_normals[j].Y,
      ) / 4,
    );
    this.m_destPoly.push(
      new ClipperLib.IntPoint2(
        ClipperLib.ClipperOffset.Round(
          this.m_srcPoly[j].X + this.m_delta * (this.m_normals[k].X - this.m_normals[k].Y * dx),
        ),
        ClipperLib.ClipperOffset.Round(
          this.m_srcPoly[j].Y + this.m_delta * (this.m_normals[k].Y + this.m_normals[k].X * dx),
        ),
      ),
    );
    this.m_destPoly.push(
      new ClipperLib.IntPoint2(
        ClipperLib.ClipperOffset.Round(
          this.m_srcPoly[j].X + this.m_delta * (this.m_normals[j].X + this.m_normals[j].Y * dx),
        ),
        ClipperLib.ClipperOffset.Round(
          this.m_srcPoly[j].Y + this.m_delta * (this.m_normals[j].Y - this.m_normals[j].X * dx),
        ),
      ),
    );
  };

  ClipperLib.ClipperOffset.prototype.DoMiter = function (j, k, r) {
    var q = this.m_delta / r;
    this.m_destPoly.push(
      new ClipperLib.IntPoint2(
        ClipperLib.ClipperOffset.Round(
          this.m_srcPoly[j].X + (this.m_normals[k].X + this.m_normals[j].X) * q,
        ),
        ClipperLib.ClipperOffset.Round(
          this.m_srcPoly[j].Y + (this.m_normals[k].Y + this.m_normals[j].Y) * q,
        ),
      ),
    );
  };

  ClipperLib.ClipperOffset.prototype.DoRound = function (j, k) {
    var a = Math.atan2(
      this.m_sinA,
      this.m_normals[k].X * this.m_normals[j].X + this.m_normals[k].Y * this.m_normals[j].Y,
    );

    var steps = Math.max(
      ClipperLib.Cast_Int32(ClipperLib.ClipperOffset.Round(this.m_StepsPerRad * Math.abs(a))),
      1,
    );

    var X = this.m_normals[k].X,
      Y = this.m_normals[k].Y,
      X2;
    for (var i = 0; i < steps; ++i) {
      this.m_destPoly.push(
        new ClipperLib.IntPoint2(
          ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + X * this.m_delta),
          ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + Y * this.m_delta),
        ),
      );
      X2 = X;
      X = X * this.m_cos - this.m_sin * Y;
      Y = X2 * this.m_sin + Y * this.m_cos;
    }
    this.m_destPoly.push(
      new ClipperLib.IntPoint2(
        ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * this.m_delta),
        ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * this.m_delta),
      ),
    );
  };

  ClipperLib.Error = function (message) {
    try {
      throw new Error(message);
    } catch (err) {
      alert(err.message);
    }
  };

  // ---------------------------------------------

  // JS extension by Timo 2013
  ClipperLib.JS = {};

  ClipperLib.JS.AreaOfPolygon = function (poly, scale) {
    if (!scale) scale = 1;
    return ClipperLib.Clipper.Area(poly) / (scale * scale);
  };

  ClipperLib.JS.AreaOfPolygons = function (poly, scale) {
    if (!scale) scale = 1;
    var area = 0;
    for (var i = 0; i < poly.length; i++) {
      area += ClipperLib.Clipper.Area(poly[i]);
    }
    return area / (scale * scale);
  };

  ClipperLib.JS.BoundsOfPath = function (path, scale) {
    return ClipperLib.JS.BoundsOfPaths([path], scale);
  };

  ClipperLib.JS.BoundsOfPaths = function (paths, scale) {
    if (!scale) scale = 1;
    var bounds = ClipperLib.Clipper.GetBounds(paths);
    bounds.left /= scale;
    bounds.bottom /= scale;
    bounds.right /= scale;
    bounds.top /= scale;
    return bounds;
  };

  // Clean() joins vertices that are too near each other
  // and causes distortion to offsetted polygons without cleaning
  ClipperLib.JS.Clean = function (polygon, delta) {
    if (!(polygon instanceof Array)) return [];
    var isPolygons = polygon[0] instanceof Array;
    var polygon = ClipperLib.JS.Clone(polygon);
    if (typeof delta !== "number" || delta === null) {
      ClipperLib.Error("Delta is not a number in Clean().");
      return polygon;
    }
    if (polygon.length === 0 || (polygon.length === 1 && polygon[0].length === 0) || delta < 0)
      return polygon;
    if (!isPolygons) polygon = [polygon];
    var k_length = polygon.length;
    var len, poly, result, d, p, j, i;
    var results = [];
    for (var k = 0; k < k_length; k++) {
      poly = polygon[k];
      len = poly.length;
      if (len === 0) continue;
      else if (len < 3) {
        result = poly;
        results.push(result);
        continue;
      }
      result = poly;
      d = delta * delta;
      //d = Math.floor(c_delta * c_delta);
      p = poly[0];
      j = 1;
      for (i = 1; i < len; i++) {
        if ((poly[i].X - p.X) * (poly[i].X - p.X) + (poly[i].Y - p.Y) * (poly[i].Y - p.Y) <= d)
          continue;
        result[j] = poly[i];
        p = poly[i];
        j++;
      }
      p = poly[j - 1];
      if ((poly[0].X - p.X) * (poly[0].X - p.X) + (poly[0].Y - p.Y) * (poly[0].Y - p.Y) <= d) j--;
      if (j < len) result.splice(j, len - j);
      if (result.length) results.push(result);
    }
    if (!isPolygons && results.length) results = results[0];
    else if (!isPolygons && results.length === 0) results = [];
    else if (isPolygons && results.length === 0) results = [[]];
    return results;
  };
  // Make deep copy of Polygons or Polygon
  // so that also IntPoint objects are cloned and not only referenced
  // This should be the fastest way
  ClipperLib.JS.Clone = function (polygon) {
    if (!(polygon instanceof Array)) return [];
    if (polygon.length === 0) return [];
    else if (polygon.length === 1 && polygon[0].length === 0) return [[]];
    var isPolygons = polygon[0] instanceof Array;
    if (!isPolygons) polygon = [polygon];
    var len = polygon.length,
      plen,
      i,
      j,
      result;
    var results = new Array(len);
    for (i = 0; i < len; i++) {
      plen = polygon[i].length;
      result = new Array(plen);
      for (j = 0; j < plen; j++) {
        result[j] = {
          X: polygon[i][j].X,
          Y: polygon[i][j].Y,
        };
      }
      results[i] = result;
    }
    if (!isPolygons) results = results[0];
    return results;
  };

  // Removes points that doesn't affect much to the visual appearance.
  // If middle point is at or under certain distance (tolerance) of the line segment between
  // start and end point, the middle point is removed.
  ClipperLib.JS.Lighten = function (polygon, tolerance) {
    if (!(polygon instanceof Array)) return [];
    if (typeof tolerance !== "number" || tolerance === null) {
      ClipperLib.Error("Tolerance is not a number in Lighten().");
      return ClipperLib.JS.Clone(polygon);
    }
    if (
      polygon.length === 0 ||
      (polygon.length === 1 && polygon[0].length === 0) ||
      tolerance < 0
    ) {
      return ClipperLib.JS.Clone(polygon);
    }
    var isPolygons = polygon[0] instanceof Array;
    if (!isPolygons) polygon = [polygon];
    var i, j, poly, k, poly2, plen, A, B, P, d, rem, addlast;
    var bxax, byay, l, ax, ay;
    var len = polygon.length;
    var toleranceSq = tolerance * tolerance;
    var results = [];
    for (i = 0; i < len; i++) {
      poly = polygon[i];
      plen = poly.length;
      if (plen === 0) continue;
      for (
        k = 0;
        k < 1000000;
        k++ // could be forever loop, but wiser to restrict max repeat count
      ) {
        poly2 = [];
        plen = poly.length;
        // the first have to added to the end, if first and last are not the same
        // this way we ensure that also the actual last point can be removed if needed
        if (poly[plen - 1].X !== poly[0].X || poly[plen - 1].Y !== poly[0].Y) {
          addlast = 1;
          poly.push({
            X: poly[0].X,
            Y: poly[0].Y,
          });
          plen = poly.length;
        } else addlast = 0;
        rem = []; // Indexes of removed points
        for (j = 0; j < plen - 2; j++) {
          A = poly[j]; // Start point of line segment
          P = poly[j + 1]; // Middle point. This is the one to be removed.
          B = poly[j + 2]; // End point of line segment
          ax = A.X;
          ay = A.Y;
          bxax = B.X - ax;
          byay = B.Y - ay;
          if (
            bxax !== 0 ||
            byay !== 0
          ) // To avoid Nan, when A==P && P==B. And to avoid peaks (A==B && A!=P), which have lenght, but not area.
          {
            l = ((P.X - ax) * bxax + (P.Y - ay) * byay) / (bxax * bxax + byay * byay);
            if (l > 1) {
              ax = B.X;
              ay = B.Y;
            } else if (l > 0) {
              ax += bxax * l;
              ay += byay * l;
            }
          }
          bxax = P.X - ax;
          byay = P.Y - ay;
          d = bxax * bxax + byay * byay;
          if (d <= toleranceSq) {
            rem[j + 1] = 1;
            j++; // when removed, transfer the pointer to the next one
          }
        }
        // add all unremoved points to poly2
        poly2.push({
          X: poly[0].X,
          Y: poly[0].Y,
        });
        for (j = 1; j < plen - 1; j++)
          if (!rem[j])
            poly2.push({
              X: poly[j].X,
              Y: poly[j].Y,
            });
        poly2.push({
          X: poly[plen - 1].X,
          Y: poly[plen - 1].Y,
        });
        // if the first point was added to the end, remove it
        if (addlast) poly.pop();
        // break, if there was not anymore removed points
        if (!rem.length) break;
        // else continue looping using poly2, to check if there are points to remove
        else poly = poly2;
      }
      plen = poly2.length;
      // remove duplicate from end, if needed
      if (poly2[plen - 1].X === poly2[0].X && poly2[plen - 1].Y === poly2[0].Y) {
        poly2.pop();
      }
      if (poly2.length > 2)
        // to avoid two-point-polygons
        results.push(poly2);
    }
    if (!isPolygons) {
      results = results[0];
    }
    if (typeof results === "undefined") {
      results = [];
    }
    return results;
  };

  ClipperLib.JS.PerimeterOfPath = function (path, closed, scale) {
    if (typeof path === "undefined") return 0;
    var sqrt = Math.sqrt;
    var perimeter = 0.0;
    var p1,
      p2,
      p1x = 0.0,
      p1y = 0.0,
      p2x = 0.0,
      p2y = 0.0;
    var j = path.length;
    if (j < 2) return 0;
    if (closed) {
      path[j] = path[0];
      j++;
    }
    while (--j) {
      p1 = path[j];
      p1x = p1.X;
      p1y = p1.Y;
      p2 = path[j - 1];
      p2x = p2.X;
      p2y = p2.Y;
      perimeter += sqrt((p1x - p2x) * (p1x - p2x) + (p1y - p2y) * (p1y - p2y));
    }
    if (closed) path.pop();
    return perimeter / scale;
  };

  ClipperLib.JS.PerimeterOfPaths = function (paths, closed, scale) {
    if (!scale) scale = 1;
    var perimeter = 0;
    for (var i = 0; i < paths.length; i++) {
      perimeter += ClipperLib.JS.PerimeterOfPath(paths[i], closed, scale);
    }
    return perimeter;
  };

  ClipperLib.JS.ScaleDownPath = function (path, scale) {
    var i, p;
    if (!scale) scale = 1;
    i = path.length;
    while (i--) {
      p = path[i];
      p.X = p.X / scale;
      p.Y = p.Y / scale;
    }
  };

  ClipperLib.JS.ScaleDownPaths = function (paths, scale) {
    var i, j, p;
    if (!scale) scale = 1;
    i = paths.length;
    while (i--) {
      j = paths[i].length;
      while (j--) {
        p = paths[i][j];
        p.X = p.X / scale;
        p.Y = p.Y / scale;
      }
    }
  };

  ClipperLib.JS.ScaleUpPath = function (path, scale) {
    var i,
      p,
      round = Math.round;
    if (!scale) scale = 1;
    i = path.length;
    while (i--) {
      p = path[i];
      p.X = round(p.X * scale);
      p.Y = round(p.Y * scale);
    }
  };

  ClipperLib.JS.ScaleUpPaths = function (paths, scale) {
    var i,
      j,
      p,
      round = Math.round;
    if (!scale) scale = 1;
    i = paths.length;
    while (i--) {
      j = paths[i].length;
      while (j--) {
        p = paths[i][j];
        p.X = round(p.X * scale);
        p.Y = round(p.Y * scale);
      }
    }
  };

  /**
   * @constructor
   */
  ClipperLib.ExPolygons = function () {
    return [];
  };
  /**
   * @constructor
   */
  ClipperLib.ExPolygon = function () {
    this.outer = null;
    this.holes = null;
  };

  ClipperLib.JS.AddOuterPolyNodeToExPolygons = function (polynode, expolygons) {
    var ep = new ClipperLib.ExPolygon();
    ep.outer = polynode.Contour();
    var childs = polynode.Childs();
    var ilen = childs.length;
    ep.holes = new Array(ilen);
    var node, n, i, j, childs2, jlen;
    for (i = 0; i < ilen; i++) {
      node = childs[i];
      ep.holes[i] = node.Contour();
      //Add outer polygons contained by (nested within) holes ...
      for (j = 0, childs2 = node.Childs(), jlen = childs2.length; j < jlen; j++) {
        n = childs2[j];
        ClipperLib.JS.AddOuterPolyNodeToExPolygons(n, expolygons);
      }
    }
    expolygons.push(ep);
  };

  ClipperLib.JS.ExPolygonsToPaths = function (expolygons) {
    var a, i, alen, ilen;
    var paths = new ClipperLib.Paths();
    for (a = 0, alen = expolygons.length; a < alen; a++) {
      paths.push(expolygons[a].outer);
      for (i = 0, ilen = expolygons[a].holes.length; i < ilen; i++) {
        paths.push(expolygons[a].holes[i]);
      }
    }
    return paths;
  };
  ClipperLib.JS.PolyTreeToExPolygons = function (polytree) {
    var expolygons = new ClipperLib.ExPolygons();
    var node, i, childs, ilen;
    for (i = 0, childs = polytree.Childs(), ilen = childs.length; i < ilen; i++) {
      node = childs[i];
      ClipperLib.JS.AddOuterPolyNodeToExPolygons(node, expolygons);
    }
    return expolygons;
  };
})();
source/src/World.ts
import { CrackPlanes } from "./ice/CrackPlanes";
import { RW } from "./rewrite";
import { createMeshEntry } from "./ice/MeshEntry";
import { createGlassMatcap } from "./ice/GlassMatcap";
// World: owns the scene graph, the object, the simulation modules and the render loop.
// A thin host component mounts it elsewhere. Everything tunable reads from the DialKit store `D`.
import * as THREE from "three/webgpu";
import { D } from "./dials/store";
import type { ShapeName } from "./dials/defaults";
import { clock } from "./core/clock";
import { input, sim, heroFrame } from "./core/state";
import { damp } from "./core/ease";
import { rng } from "./core/seed";
import { createBackdrop } from "./scene/Backdrop";
import { createEnvironment } from "./scene/Environment";
import { Lights } from "./scene/Lights";
import { Rig } from "./scene/Rig";
import { makeShape, type ShapeSpec, type LogoSDF } from "./shape/sdf";
import { buildGeometry } from "./shape/geometry";
import { createPost } from "./post/Post";
import { createIceMaterial, type IceMaterialBundle } from "./ice/IceMaterial";
import { ErosionField } from "./erosion/ErosionField";
import { Powder } from "./powder/Powder";
import { Interaction } from "./core/interaction";
import { DebugViews } from "./scene/DebugViews";
import { tsl } from "./tsl/t";
import { AdaptiveResolution } from "./core/AdaptiveResolution";

export interface WorldKey {
  logoId: number;
  shape: string;
  seed: number;
  particleCount: string;
  resolution: string;
  densityGrid: string;
  size: number;
  tubeRatio: number;
  segments: number;
  variants: number;
  faceted: boolean;
  sprites: boolean;
  spriteBlend: boolean;
  strays: number;
  clumps: number;
  dispersion: boolean;
  voronoiCells: string;
  turbulence: string;
}
export const worldKeyOf = (): WorldKey => ({
  logoId: currentLogoId,
  shape: heroFrame.enabled && heroFrame.shape ? heroFrame.shape : D.shape.shape,
  seed: D.shape.seed,
  particleCount: D.powder.particleCount,
  resolution: D.erosion.resolution,
  densityGrid: D.powder.densityGrid,
  size: D.shape.size,
  tubeRatio: D.shape.tubeRatio,
  segments: D.shape.segments,
  variants: D.powder.grainVariants,
  faceted: D.powder.facetedGrains,
  sprites: D.powder.sprites.enabled,
  spriteBlend: D.powder.sprites.seeThrough > 0,
  strays: D.powder.strayCount,
  clumps: D.powder.clumpCount,
  dispersion: true,
  voronoiCells: D.performance.voronoiCells,
  turbulence: D.performance.turbulence,
});
export const keyString = (k: WorldKey) => JSON.stringify(k);
export let currentLogoId = 0;
export const bumpLogoId = () => {
  currentLogoId++;
};

/** Undocumented URL flags for bisecting problems: ?nobake ?nopowder ?noerosion ?noice ?nopost */
export const FLAGS = new Set(
  typeof location !== "undefined" ? [...new URLSearchParams(location.search).keys()] : [],
);
/** hash buckets for break-cell restore counters (cells are identified by their baked hash) */
export const CELL_BUCKETS = 65536;
const PARTICLE_COUNTS: Record<string, number> = {
  "100k": 100_000,
  "250k": 250_000,
  "500k": 500_000,
  "1M": 1_000_000,
  "2M": 2_000_000,
};

export class World {
  /** FROST: the composition's continuous motion lives on this outer group; objectGroup (the experiment's) is its child
   *  and only carries the facing offset that makes a freshly formed shape face the camera. */
  readonly motionGroup = new THREE.Group();
  readonly objectGroup = new THREE.Group();
  readonly backdrop = createBackdrop();
  readonly env = createEnvironment();
  readonly lights = new Lights();
  rig = new Rig();
  readonly post;
  shape!: ShapeSpec;
  mesh!: THREE.Mesh;
  meshEntry?: ReturnType<typeof createMeshEntry>;
  glassMatcap?: THREE.Texture;
  materialPlanes?: CrackPlanes;
  ice!: IceMaterialBundle;
  erosion!: ErosionField;
  powder!: Powder;
  interaction!: Interaction;
  debugViews!: DebugViews;
  private pixelRatio = 0;
  private frozenByDial = false;
  private statsTimer = 0;
  buildCount = 0;
  private timersPending = false;
  private fieldMaybeNonZero = true;
  private lastActivityT = 0;

  /**
   * Idle-skip bookkeeping. The field only changes during a stroke, the crumble window, or while
   * healing/refrost still has something to decay; the powder only needs work while grains are out.
   */
  private idleState(t: number) {
    const stroke = this.interaction.strokeCount > 0;
    if (stroke) {
      this.fieldMaybeNonZero = true;
      this.lastActivityT = t;
    }
    const st = this.erosion.stats;
    // a readback taken after the last stroke that shows an empty field proves it is static again
    if (
      this.fieldMaybeNonZero &&
      this.erosion.statsReadT > sim.lastStrokeT + 0.6 &&
      st.max < 0.002 &&
      st.refrost < 0.002
    )
      this.fieldMaybeNonZero = false;
    const healingNow = sim.healing && this.fieldMaybeNonZero;
    const refrostNow = this.fieldMaybeNonZero && st.refrost > 0.002;
    const fieldActive =
      !D.performance.idleSkip ||
      stroke ||
      t < this.erosion.crumbleUntil ||
      healingNow ||
      refrostNow;
    const powderActive =
      !D.performance.idleSkip ||
      fieldActive ||
      sim.counts.active > 0 ||
      t - this.lastActivityT < 1.5 ||
      sim.resetRequestedAt >= 0;
    sim.fieldActive = fieldActive;
    sim.powderActive = powderActive;
    return { fieldActive, powderActive };
  }
  private envSig = "";
  private backdropSig = "";
  private frameTimes = new Float64Array(30);
  private frameTimeSum = 0;
  private frameTimeCount = 0;
  private frameTimeIndex = 0;
  private lastFrameStart = 0;
  private gpuPending = false;
  private gpuTimerLast = -Infinity;
  private readonly renderSize = new THREE.Vector2();
  private readonly adaptiveResolution = new AdaptiveResolution();
  /** ?offscreen=1 renders into a render target, not the canvas (presenting kills headless SwiftShader devices). */
  private offscreenRT: THREE.RenderTarget | null = FLAGS.has("offscreen")
    ? new THREE.RenderTarget(16, 16, {
        depthBuffer: false,
        type: THREE.UnsignedByteType,
        format: THREE.RGBAFormat,
      })
    : null;

  constructor(
    readonly renderer: THREE.WebGPURenderer,
    readonly scene: THREE.Scene,
    readonly camera: THREE.PerspectiveCamera,
    readonly blueNoise: THREE.Texture,
    public logo?: { sdf: LogoSDF; geometry: THREE.BufferGeometry },
    readonly fractureDetail?: THREE.Texture,
  ) {
    this.motionGroup.add(this.objectGroup);
    scene.add(this.motionGroup, this.lights.group, camera);
    scene.backgroundNode = this.backdrop.node;
    scene.environment = this.env.texture;
    this.build();
    this.post = createPost({
      scene,
      camera,
      renderer,
      blueNoise,
      haze: (depthNode, viewZ) => this.powder.hazeNode(depthNode, viewZ, camera),
      initial: D.post,
      initialPerformance: D.performance,
      initialHaze: D.powder.hazeIntensity !== 0,
    });
  }

  /** (Re)build the shape-dependent parts: geometry, ice material, erosion field, powder. */
  build() {
    this.buildCount++;
    const key = worldKeyOf();
    const rand = rng(key.seed + 1);
    const wanted = heroFrame.enabled && heroFrame.shape ? heroFrame.shape : D.shape.shape;
    const shapeName = (wanted === "logo" && !this.logo ? "torus" : wanted) as ShapeName;
    this.shape = makeShape(shapeName, key.size, key.tubeRatio, this.logo?.sdf);
    const geometry = buildGeometry(this.shape, key.segments, this.logo?.geometry);
    // one shared atomic uint buffer: [powder density G^3 | 16 counters | erosion heal grid R^3]
    const G = parseInt(key.densityGrid, 10),
      R = parseInt(key.resolution, 10);
    const countersOffset = G * G * G,
      healOffset = countersOffset + 16,
      cellOffset = healOffset + R * R * R,
      flightOffset = cellOffset + CELL_BUCKETS;
    const atomics = tsl.instancedArray(flightOffset + CELL_BUCKETS, "uint").toAtomic();
    this.erosion = new ErosionField(
      this.renderer,
      this.shape,
      R,
      key.seed,
      atomics,
      healOffset,
      cellOffset,
      flightOffset,
    );
    if (RW.materials) {
      this.materialPlanes = new CrackPlanes(this.renderer, this.shape.bound, key.seed);
      (this.erosion as any).materialPlanes = this.materialPlanes;
    }
    this.erosion.skipBake = FLAGS.has("nobake");
    if (RW.mesh) this.meshEntry = createMeshEntry(geometry, this.camera);
    if (RW.matcap) {
      this.env.update(D.lighting);
      this.glassMatcap = createGlassMatcap(this.env.texture);
    }
    this.ice = createIceMaterial({
      meshEntry: this.meshEntry,
      glassMatcap: this.glassMatcap,
      shape: this.shape,
      erosion: this.erosion,
      seed: key.seed,
      blueNoise: this.blueNoise,
      environment: this.env.texture,
      fractureDetail: this.fractureDetail,
      backdrop: this.backdrop.uniforms,
      dispersion: key.dispersion,
      voronoiCells: key.voronoiCells as "27" | "8",
    });
    if (FLAGS.has("noice")) {
      const m = new THREE.MeshStandardNodeMaterial();
      m.roughness = 0.3;
      m.color.set("#bbbbbb");
      (this.ice as any).material = m;
    }
    if (this.meshEntry) this.meshEntry.material.positionNode = this.ice.material.positionNode;
    this.mesh = new THREE.Mesh(geometry, this.ice.material);
    this.mesh.castShadow = true;
    this.mesh.receiveShadow = true;
    this.mesh.frustumCulled = false;
    this.objectGroup.add(this.mesh);
    this.powder = new Powder({
      enabled: !FLAGS.has("nopowder"),
      renderer: this.renderer,
      scene: this.scene,
      shape: this.shape,
      erosion: this.erosion,
      seed: key.seed,
      rand,
      count: PARTICLE_COUNTS[key.particleCount] ?? 1_000_000,
      variants: key.variants,
      strays: key.strays,
      densityRes: G,
      clumpCount: key.clumps,
      objectGroup: this.objectGroup,
      turbulence: key.turbulence as "full" | "fast",
      atomics,
      countersOffset,
      healOffset,
      cellOffset,
      flightOffset,
      fieldRes: R,
      backdrop: this.backdrop.uniforms,
      fractureDetail: this.fractureDetail,
      iceUniforms: this.ice.uniforms,
      iceFeatures: this.ice.features,
      environment: this.env.texture,
    });
    this.interaction = new Interaction(
      this.shape,
      this.objectGroup,
      this.camera,
      this.erosion,
      this.powder,
    );
    this.debugViews = new DebugViews(this.camera, this.erosion, this.powder);
    this.scene.add(this.debugViews.group);
  }

  disposeBuilt() {
    this.materialPlanes?.dispose();
    this.materialPlanes = undefined;
    this.meshEntry?.dispose();
    this.meshEntry = undefined;
    this.glassMatcap?.dispose();
    this.glassMatcap = undefined;
    this.debugViews.dispose();
    this.scene.remove(this.debugViews.group);
    this.objectGroup.remove(this.mesh);
    this.mesh.geometry.dispose();
    this.ice.material.dispose();
    this.erosion.dispose();
    this.powder.dispose();
  }

  rebuild() {
    this.disposeBuilt();
    this.build();
    this.post.post.needsUpdate = true;
  }

  private updateEnvironment() {
    const L = D.lighting;
    const sig = JSON.stringify([
      L.envSoftbox,
      L.envRim,
      L.envFill,
      L.key,
      L.fill,
      L.fillColor,
      L.fillGroundColor,
      L.fillReflectionStrength,
      L.rimColor,
      L.rimElevation,
      L.rimAzimuth,
      L.rimSize,
      L.accentCool,
      L.accentWarm,
    ]);
    if (sig !== this.envSig) {
      this.envSig = sig;
      this.env.update(L);
    }
    this.scene.environmentIntensity = D.ice.envIntensity;
  }

  private updateBackdrop(t: number) {
    const b = D.lighting.backdrop;
    const h = heroFrame.enabled ? heroFrame.backdrop : null;
    const u = this.backdrop.uniforms;
    const top = h?.top ?? b.top,
      mid = h?.mid ?? b.mid,
      bottom = h?.bottom ?? b.bottom;
    const centerX = h?.centerX ?? b.centerX,
      centerY = h?.centerY ?? b.centerY,
      radius = h?.radius ?? b.radius,
      falloff = h?.falloff ?? b.falloff;
    const sig = `${top}|${mid}|${bottom}|${centerX}|${centerY}|${radius}|${falloff}|${b.noise}`;
    if (sig !== this.backdropSig) {
      this.backdropSig = sig;
      u.uTop.value.set(top);
      u.uMid.value.set(mid);
      u.uBottom.value.set(bottom);
      u.uCenter.value.set(centerX, centerY);
      u.uRadius.value = radius;
      u.uFalloff.value = falloff;
      u.uNoise.value = b.noise;
    }
    u.uTime.value = t;
  }

  /** FROST: called after the camera is final for this frame and before the post graph renders. */
  onBeforeRender: ((t: number) => void) | null = null;
  /** Called after the rig places the object group, before its world matrix is used (the object motion). */
  onBeforeSimulation: ((t: number, dt: number) => void) | null = null;
  onObjectTransform: ((t: number) => void) | null = null;
  /** Authored camera evaluates after the current target matrix, before interaction and render. */
  onCameraTransform: ((t: number) => void) | null = null;

  /**
   * One frame: input -> rig -> simulation compute -> post render. The composition supplies the clock (t, dt),
   * not performance.now(), so each step is pure; `render` false advances without drawing (fast-forward after a seek).
   */
  frame(t: number, dt: number, render = true) {
    clock.t = t;
    clock.dt = dt;
    clock.frame++;
    const start = t * 1000;
    const elapsed = 1000 / D.performance.targetFps;
    const splitOutput =
      !FLAGS.has("nopost") &&
      D.performance.nativePostEffects &&
      D.performance.upscaler !== "native";
    const resolution = this.adaptiveResolution.updateScene(
      t,
      elapsed,
      window.devicePixelRatio || 1,
      D.post.pixelRatioCap,
      D.performance.adaptiveResolution && D.performance.dynamicSceneResolution,
      D.performance.targetFps,
      D.performance.minPixelRatio,
      D.performance.sceneResolutionScale,
      D.performance.minSceneResolutionScale,
      splitOutput,
    );
    sim.pixelRatio = resolution.sourceRatio;
    sim.outputPixelRatio = resolution.outputRatio;
    sim.sceneResolutionScale = resolution.sceneScale;
    if (
      resolution.outputRatio !== this.pixelRatio ||
      this.renderer.getPixelRatio() !== resolution.outputRatio
    ) {
      this.pixelRatio = resolution.outputRatio;
      this.renderer.setPixelRatio(resolution.outputRatio);
      const sz = this.renderer.getSize(this.renderSize);
      this.renderer.setSize(sz.x, sz.y, false);
    }

    if (!this.onCameraTransform) this.rig.update(this.camera, this.objectGroup, t, dt);
    if (heroFrame.enabled && heroFrame.framing)
      this.motionGroup.position.set(heroFrame.objectX, heroFrame.objectY, heroFrame.objectZ);
    else this.motionGroup.position.set(D.shape.offsetX, D.shape.offsetY, D.shape.offsetZ);
    this.objectGroup.position.set(0, 0, 0);
    if (this.onObjectTransform) this.onObjectTransform(t);
    this.motionGroup.updateMatrixWorld(true);
    this.onCameraTransform?.(t);
    this.updateEnvironment();
    this.updateBackdrop(t);
    sim.hoverAmount = damp(sim.hoverAmount, sim.overObject ? 1 : 0, 0.4 / 3, dt);
    this.lights.update(t, sim.hoverAmount);

    this.interaction.update(t, dt);
    this.onBeforeSimulation?.(t, dt);
    this.debugViews.update(this.interaction);
    if (!FLAGS.has("noice")) {
      this.ice.update(t, dt, this.lights.key, this.objectGroup);
      this.ice.uniforms.hover.value = sim.hoverAmount;
    }

    const c0 = performance.now();
    const idle = this.idleState(t);
    if (dt > 0) {
      if (!FLAGS.has("noerosion"))
        this.erosion.step(
          this.renderer,
          t,
          dt,
          this.interaction.strokeSegments,
          this.interaction.strokeCount,
          idle.fieldActive,
        );
    }
    // Uniform-only updates at dt=0 keep paused workbench edits live.
    if (!FLAGS.has("nopowder"))
      this.powder.step(this.renderer, t, dt, this.interaction, this.lights.key, idle);
    sim.computeMs = performance.now() - c0;
    const wantStats = D.debug.stats || D.performance.idleSkip;
    if (wantStats) {
      this.statsTimer += Math.max(dt, 1 / 120);
      const interval = this.fieldMaybeNonZero ? 0.25 : 0.4;
      if (this.statsTimer > interval || this.offscreenRT) {
        this.statsTimer = 0;
        this.erosion.readStats(this.renderer, t);
        sim.erosion = this.erosion.stats;
      }
    }

    this.camera.updateMatrixWorld();
    if (this.onBeforeRender) this.onBeforeRender(t);
    // Sprite size belongs to the low-resolution scene pass, not the native-resolution final output.
    this.powder.updateCamera(
      this.camera,
      this.renderer.getSize(this.renderSize).y * resolution.sourceRatio,
    );
    const focus = this.camera.position.distanceTo(this.rig.lookAt);
    this.post.update(
      D.post,
      D.performance,
      focus,
      sim.sceneFade,
      D.powder.hazeIntensity,
      t,
      D.version,
    );
    this.materialPlanes?.update();
    this.post.setSceneResolution(resolution.sceneScale);
    this.meshEntry?.update(this.mesh, resolution.sceneScale);
    (this.mesh.material as any).wireframe = D.debug.wireframe;
    if (this.offscreenRT) {
      const sz = this.renderer.getSize(this.renderSize);
      const pr = this.renderer.getPixelRatio();
      const w = Math.max(1, Math.floor(sz.x * pr)),
        h = Math.max(1, Math.floor(sz.y * pr));
      if (this.offscreenRT.width !== w || this.offscreenRT.height !== h)
        this.offscreenRT.setSize(w, h);
      this.renderer.setRenderTarget(this.offscreenRT);
    }
    this.post.setHazeResolution(
      D.performance.hazeResolution === "quarter"
        ? 0.25
        : D.performance.hazeResolution === "half"
          ? 0.5
          : 1,
    );
    // HyperFrames drives explicit frames outside Three's animation loop. Advance
    // the node frame so FRAME-scoped passes cannot reuse the previous image.
    if (render) {
      (this.renderer as any)._nodes.nodeFrame.update();
      this.post.setFrame(Math.round(t * 60));
      if (FLAGS.has("nopost")) this.renderer.render(this.scene, this.camera);
      else this.post.post.render();
    }
    if (this.offscreenRT) this.renderer.setRenderTarget(null);
    if (D.performance.gpuTimers && !this.timersPending && t - this.gpuTimerLast >= 0.25) {
      const r: any = this.renderer;
      if (r.backend?.trackTimestamp) {
        this.timersPending = true;
        this.gpuTimerLast = t;
        Promise.all([r.resolveTimestampsAsync("render"), r.resolveTimestampsAsync("compute")])
          .then(() => {
            sim.gpuRenderMs = r.info.render.timestamp || 0;
            sim.gpuComputeMs = r.info.compute.timestamp || 0;
            this.timersPending = false;
          })
          .catch(() => {
            this.timersPending = false;
          });
      }
    }
    if (this.offscreenRT && !this.gpuPending) {
      const q = (this.renderer as any).backend?.device?.queue;
      if (q?.onSubmittedWorkDone) {
        this.gpuPending = true;
        q.onSubmittedWorkDone()
          .then(() => {
            sim.gpuFrames++;
            this.gpuPending = false;
          })
          .catch(() => {
            this.gpuPending = false;
          });
      }
    }
    void input;
  }

  /**
   * FROST: return every stateful part of the simulation to its t = 0 state so a backward seek can replay
   * the timeline deterministically (field cleared, grains dormant, rig and interaction fresh).
   */
  resetSim() {
    this.erosion.clear(this.renderer);
    this.erosion.crumbleUntil = -1;
    this.erosion.u.crumbleUntil.value = -1;
    this.powder.reset(this.renderer);
    this.interaction = new Interaction(
      this.shape,
      this.objectGroup,
      this.camera,
      this.erosion,
      this.powder,
    );
    this.rig = new Rig();
    this.mesh.visible = true;
    sim.strokeActive = false;
    sim.overObject = false;
    sim.lastStrokeT = -1e9;
    sim.healing = false;
    sim.resetRequestedAt = -1;
    sim.fade = 1;
    sim.sceneFade = 1;
    sim.hoverAmount = 0;
    sim.fieldActive = true;
    sim.powderActive = true;
    this.fieldMaybeNonZero = true;
    this.lastActivityT = 0;
    this.statsTimer = 0;
    clock.t = 0;
    clock.dt = 1 / 60;
    clock.frame = 0;
    clock.paused = false;
  }

  /** Debug helper: generated WGSL for the object's material. */
  async dumpShader() {
    const r: any = this.renderer;
    return r.debug.getShaderAsync(this.scene, this.camera, this.mesh);
  }

  /** Headless check helper: read the offscreen frame back, return luma stats + a PNG data URL. */
  async readback() {
    if (!this.offscreenRT) return null;
    const rt = this.offscreenRT,
      w = rt.width,
      h = rt.height;
    const data = (await this.renderer.readRenderTargetPixelsAsync(rt, 0, 0, w, h)) as Uint8Array;
    const stride =
      data.length >= Math.ceil((w * 4) / 256) * 256 * h ? Math.ceil((w * 4) / 256) * 256 : w * 4;
    let sum = 0,
      mx = 0;
    const cv = document.createElement("canvas");
    cv.width = w;
    cv.height = h;
    const ctx = cv.getContext("2d")!;
    const img = ctx.createImageData(w, h);
    for (let y = 0; y < h; y++)
      for (let x = 0; x < w; x++) {
        const si = y * stride + x * 4,
          di = (y * w + x) * 4; // WebGPU readback rows are top-down
        img.data[di] = data[si];
        img.data[di + 1] = data[si + 1];
        img.data[di + 2] = data[si + 2];
        img.data[di + 3] = 255;
        const l = 0.2126 * data[si] + 0.7152 * data[si + 1] + 0.0722 * data[si + 2];
        sum += l;
        if (l > mx) mx = l;
      }
    ctx.putImageData(img, 0, 0);
    return { mean: sum / (w * h), max: mx, png: cv.toDataURL("image/png"), w, h };
  }

  dispose() {
    this.disposeBuilt();
    this.post.dispose();
    this.scene.remove(this.motionGroup, this.lights.group);
    this.scene.backgroundNode = null;
    this.scene.environment = null;
    this.env.texture.dispose();
    this.fractureDetail?.dispose();
  }
}
source/tools/compile-preflight.mjs
// Source-only compiler pass. No renderer, browser, GPU, or simulation is started.
import ts from "typescript";
import { resolve } from "node:path";
const root = resolve("src");
const files = ts.sys.readDirectory(root, [".ts"], ["**/node_modules/**"]);
const program = ts.createProgram(files, {
  noEmit: true,
  skipLibCheck: true,
  target: ts.ScriptTarget.ES2022,
  module: ts.ModuleKind.ESNext,
  moduleResolution: ts.ModuleResolutionKind.Bundler,
  resolveJsonModule: true,
  allowSyntheticDefaultImports: true,
});
const relevant = new Set([2304, 2552, 2448, 2454]);
const diagnostics = [
  ...program.getSyntacticDiagnostics(),
  ...program.getSemanticDiagnostics().filter((d) => relevant.has(d.code)),
].filter((d) => d.file?.fileName.startsWith(root));
if (diagnostics.length) {
  console.error(
    ts.formatDiagnosticsWithColorAndContext(diagnostics, {
      getCanonicalFileName: (f) => f,
      getCurrentDirectory: () => process.cwd(),
      getNewLine: () => "\n",
    }),
  );
  process.exitCode = 1;
} else
  console.log(
    "Source compilation: syntax, unresolved names and use-before-initialization passed. No GPU execution.",
  );
source/tools/run-shard-compile.mjs
import { build } from "esbuild";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
const here = dirname(fileURLToPath(import.meta.url));
const three = resolve(here, "../node_modules/three/src");
const dir = mkdtempSync(resolve(tmpdir(), "frost-shard-compile-"));
try {
  const outfile = resolve(dir, "probe.mjs");
  await build({
    entryPoints: [resolve(here, "shard-compile-probe.ts")],
    outfile,
    bundle: true,
    platform: "node",
    format: "esm",
    alias: {
      "three/webgpu": resolve(three, "Three.WebGPU.js"),
      "three/tsl": resolve(three, "Three.TSL.js"),
    },
  });
  const result = spawnSync(process.execPath, [outfile], { stdio: "inherit" });
  process.exitCode = result.status ?? 1;
} finally {
  rmSync(dir, { recursive: true, force: true });
}
source/tools/shard-compile-probe.ts
// CPU-only TSL -> WGSL probe. No renderer initialization, browser or GPU calls.
import * as THREE from "three/webgpu";
import { uniform, vec3, instancedArray, lights, mrt, output, velocity } from "three/tsl";
import WGSLNodeBuilder from "three/src/renderers/webgpu/nodes/WGSLNodeBuilder.js";
import { Powder } from "../src/powder/Powder";
import { ErosionField } from "../src/erosion/ErosionField";
import { makeShape } from "../src/shape/sdf";
import { createIceMaterial } from "../src/ice/IceMaterial";
import { D } from "../src/dials/store";
import assert from "node:assert/strict";
THREE.TextureLoader.prototype.load = function () {
  return new THREE.DataTexture(new Uint8Array([128, 128, 128, 255]), 1, 1);
} as any;
const renderer: any = new THREE.WebGPURenderer({
  canvas: { width: 1, height: 1, style: {}, addEventListener() {}, setAttribute() {} } as any,
});
renderer.hasFeature = () => true;
renderer.hasCompatibility = () => false;
renderer.backend.capabilities.getUniformBufferLimit = () => 65536;
const scene = new THREE.Scene(),
  camera = new THREE.PerspectiveCamera();
const shape = makeShape("sphere", 1, 0.3),
  atomics = instancedArray(140000, "uint").toAtomic();
const erosion = new ErosionField(renderer, shape, 8, 7, atomics, 528, 1040, 66576);
const backdrop: any = {
  uTop: uniform(new THREE.Color()),
  uMid: uniform(new THREE.Color()),
  uBottom: uniform(new THREE.Color()),
  uCenter: uniform(new THREE.Vector2()),
  uRadius: uniform(1),
  uFalloff: uniform(1),
  uNoise: uniform(0),
  uTime: uniform(0),
  uAspect: uniform(1),
};
const tex = new THREE.DataTexture(new Uint8Array([128, 128, 128, 255]), 1, 1);
const ice = createIceMaterial({
  shape,
  erosion,
  seed: 7,
  blueNoise: tex,
  environment: tex,
  fractureDetail: tex,
  backdrop,
});
D.powder.sprites.enabled = true;
D.powder.sprites.seeThrough = 0.78;
const powder = new Powder({
  renderer,
  scene,
  shape,
  erosion,
  seed: 7,
  rand: Math.random,
  count: 32,
  strays: 0,
  variants: 3,
  densityRes: 8,
  clumpCount: 4,
  objectGroup: new THREE.Group(),
  enabled: false,
  atomics,
  countersOffset: 512,
  healOffset: 528,
  cellOffset: 1040,
  flightOffset: 66576,
  fieldRes: 8,
  backdrop,
  fractureDetail: tex,
  iceUniforms: ice.uniforms,
  iceFeatures: ice.features,
  environment: tex,
});
(powder as any).buildMeshes();
const ids: number[] = [];
for (let v = 0; v < powder.meshes.length; v++) {
  const g: any = powder.meshes[v].geometry;
  assert.equal(g.indirect, null, "Shard visibility must not depend on indirect draw arguments");
  for (let i = 0; i < g.instanceCount; i++) ids.push(i * powder.meshes.length + v);
}
assert.deepEqual(
  ids.sort((a, b) => a - b),
  Array.from({ length: powder.total }, (_, i) => i),
  "Each particle is drawn exactly once",
);
const mesh = powder.meshes[0];
// Include the production motion-vector MRT and physical lighting. A bare
// unlit shader omitted the additional varyings used by the workbench pipeline.
const target = new THREE.RenderTarget(1, 1, { count: 2 });
target.textures[0].name = "output";
target.textures[1].name = "velocity";
renderer.getRenderTarget = () => target;
renderer.getMRT = () => mrt({ output, velocity });
const key = new THREE.DirectionalLight();
const rim = new THREE.SpotLight();
const builder: any = new WGSLNodeBuilder(mesh, renderer);
builder.scene = scene;
builder.camera = camera;
builder.lightsNode = lights([key, rim, new THREE.HemisphereLight()]);
builder.build();
const varyings = builder.vertexShader.match(/struct VaryingsStruct \{([\s\S]*?)\}/)?.[1];
assert.ok(varyings, "WGSL vertex output structure exists");
const locations = [...varyings.matchAll(/@location\(\s*(\d+)\s*\)/g)].map((m) => Number(m[1]));
assert.ok(
  locations.length <= 14 && Math.max(...locations) < 14,
  `Vertex output budget exceeded: ${locations.length} locations; reserve two of the hardware 16 for environment/shadow variants`,
);
const builtins = Number(/@builtin\(\s*front_facing\s*\)/.test(builder.fragmentShader));
assert.ok(
  locations.length + builtins <= 15,
  "Inter-stage outputs plus front_facing must fit the compatibility-mode budget",
);
console.log(
  "Physical shard shader with motion vectors:",
  locations.length,
  "vertex output locations (hardware limit 16).",
);
assert.match(builder.vertexShader, /instanceIndex \* 3u/);
assert.match(builder.vertexShader, /vec4<f32>\( (?:varyings\.)?positionLocal, 1.0 \)/);
console.log(
  "Shard WGSL generated without a GPU. Vertex bytes:",
  builder.vertexShader.length,
  "fragment bytes:",
  builder.fragmentShader.length,
);
// Compile simulation and retarget too: material-only checks cannot catch errors
// in assembly motion. This generates WGSL without submitting any GPU commands.
(powder as any).buildCompute();
for (const name of ["update", "retargetCount"]) {
  const compute: any = new WGSLNodeBuilder((powder as any).nodes[name], renderer);
  compute.scene = scene;
  compute.camera = camera;
  compute.build();
  assert.ok(compute.computeShader.includes("@compute"));
  console.log("Assembly " + name + " WGSL generated:", compute.computeShader.length, "bytes.");
}