// RunhappyToken.tsx — Framer code component
// -----------------------------------------------------------------------------
// A self-contained 3D "Runhappy token" rendered in WebGL. Works like an image:
// drop it on the canvas, resize it, tweak the variables in the right-hand panel.
//
// ZERO external dependencies. The whole renderer is vanilla WebGL, inlined below.
// (React + the `framer` property-control helpers are built into Framer itself —
// they are not third-party packages you need to install.)
//
// HOW TO USE IN FRAMER
//   1. In your project: Assets ▸ Code ▸ New Code File ▸ paste this whole file.
//   2. Drag "RunhappyToken" onto the canvas. Resize the frame like an image.
//   3. Adjust Pose (rotate / zoom) and Camera (focal length / depth of field) in the
//      properties panel. Color / material / lighting are fixed to the brand look.
//
// The frame's size IS the token's size — make the frame bigger, token gets bigger.
// The background is transparent, so it sits on whatever is behind it.
// -----------------------------------------------------------------------------

import * as React from "react"
import { addPropertyControls, ControlType } from "framer"

// =============================================================================
// ====  VANILLA WEBGL CORE (no dependencies) — identical to token-core.js  =====
// =============================================================================

const DEFAULT_SVG = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="500" height="500" rx="64" fill="#FFD13A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M302.633 108C356.81 108 395 147.461 395 203.584C395 232.298 384.456 256.587 366.415 273.645C357.582 281.997 353.165 286.174 352.471 290.008C351.777 293.842 353.93 298.242 358.238 307.039L383.587 358.809C391.099 374.15 394.855 381.822 391.367 387.411C387.878 392.999 379.332 393 362.244 393H311.338C302.864 393 298.627 393 295.477 390.671C292.328 388.341 291.09 384.29 288.613 376.19L269.858 314.849C267.982 308.711 267.042 305.641 264.972 303.556C264.122 302.7 263.146 301.977 262.079 301.415C259.478 300.046 256.267 300.046 249.845 300.046C241.374 300.046 237.138 300.046 234.111 302.106C232.881 302.944 231.818 304.006 230.98 305.236C228.919 308.261 228.919 312.495 228.919 320.962V369.25C228.919 380.445 228.919 386.044 225.439 389.522C221.959 393 216.358 393 205.156 393H156.763C145.561 393 139.96 392.999 136.48 389.522C133 386.044 133 380.445 133 369.25V131.75C133 120.554 133 114.956 136.48 111.478C139.96 108 145.561 108 156.763 108H302.633ZM203.765 208.916C202.152 207.463 199.671 207.6 198.223 209.217L183.801 225.335C182.353 226.953 182.481 229.437 184.091 230.895C196.891 242.485 218.508 261.5 257.873 261.501C278.809 261.501 294.317 256.145 306.455 248.794C315.948 243.045 323.528 235.816 328.734 230.931C330.318 229.445 330.447 226.953 328.999 225.335L314.578 209.217C313.129 207.599 310.641 207.462 309.047 208.937C303.141 214.403 298.351 219.227 291.168 223.577C283.553 228.189 273.319 232.017 257.873 232.017C229.952 232.017 215.639 219.616 203.765 208.916Z" fill="#FFFFFF"/>
<path d="M397.6 0C433.443 0 451.365 2.94148e-06 465.056 6.97559C477.098 13.1115 486.889 22.9021 493.024 34.9443C500 48.6347 500 66.557 500 102.4V397.6C500 433.443 500 451.365 493.024 465.056C486.889 477.098 477.098 486.889 465.056 493.024C451.365 500 433.443 500 397.6 500H102.4C66.557 500 48.6347 500 34.9443 493.024C22.9021 486.889 13.1115 477.098 6.97559 465.056C2.94148e-06 451.365 0 433.443 0 397.6V102.4C0 66.557 2.94148e-06 48.6347 6.97559 34.9443C13.1115 22.9021 22.9021 13.1115 34.9443 6.97559C48.6347 2.94148e-06 66.557 0 102.4 0H397.6ZM114.8 38C87.9174 38 74.4757 37.9998 64.208 43.2314C55.1764 47.8333 47.8333 55.1764 43.2314 64.208C37.9998 74.4757 38 87.9174 38 114.8V386.2C38 413.083 37.9998 426.524 43.2314 436.792C47.8333 445.824 55.1764 453.167 64.208 457.769C74.4757 463 87.9174 463 114.8 463H386.2C413.083 463 426.524 463 436.792 457.769C445.824 453.167 453.167 445.824 457.769 436.792C463 426.524 463 413.083 463 386.2V114.8C463 87.9174 463 74.4757 457.769 64.208C453.167 55.1764 445.824 47.8333 436.792 43.2314C426.524 37.9998 413.083 38 386.2 38H114.8Z" fill="#FFFFFF"/>
</svg>`

const DEFAULT_OPTIONS: any = {
    svg: DEFAULT_SVG,
    color: "#f4a610",
    background: "transparent", // Framer: drop onto any frame (bg not exposed as a control)
    dpr: 0,
    tiltX: 0,
    tiltY: 10,
    tiltZ: 6,
    zoom: 1,
    focalLength: 100,
    dof: true,
    dofFocus: 0.5,
    dofAmount: 0.5,
    dofBokeh: 10,
    spin: false,
    spinSpeed: 0.5,
    spinPause: 0.3,
    spinAxisX: 0,
    spinAxisY: -0.6,
    spinAxisZ: 0,
    float: false,
    floatAmp: 0.2,
    floatSpeed: 1,
    thickness: 0.09,
    cornerRadius: 0.34,
    edgeBevel: 0.01,
    reliefDepth: 0.05,
    frameDepth: 0.05,
    reliefBlur: 0.01,
    roughness: 0.6,
    metalness: 0,
    gloss: 1.2,
    lightIntensity: 1.05,
    lightAngle: 31,
    lightElevation: 28,
    fillIntensity: 0.45,
    fillAngle: -120,
    fillElevation: 12,
    ambient: 0,
    envTint: "#ffdf9e",
}

const M = {
    ident: () => new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
    mul(a: any, b: any) {
        const o = new Float32Array(16)
        for (let c = 0; c < 4; c++)
            for (let r = 0; r < 4; r++)
                o[c * 4 + r] =
                    a[r] * b[c * 4] +
                    a[4 + r] * b[c * 4 + 1] +
                    a[8 + r] * b[c * 4 + 2] +
                    a[12 + r] * b[c * 4 + 3]
        return o
    },
    perspective(fovy: number, aspect: number, near: number, far: number) {
        const f = 1 / Math.tan(fovy / 2),
            nf = 1 / (near - far)
        return new Float32Array([f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, -1, 0, 0, 2 * far * near * nf, 0])
    },
    translate(x: number, y: number, z: number) {
        const m = M.ident()
        m[12] = x
        m[13] = y
        m[14] = z
        return m
    },
    scale(s: number) {
        const m = M.ident()
        m[0] = m[5] = m[10] = s
        return m
    },
    rotX(a: number) {
        const c = Math.cos(a), s = Math.sin(a), m = M.ident()
        m[5] = c; m[6] = s; m[9] = -s; m[10] = c
        return m
    },
    rotY(a: number) {
        const c = Math.cos(a), s = Math.sin(a), m = M.ident()
        m[0] = c; m[2] = -s; m[8] = s; m[10] = c
        return m
    },
    rotZ(a: number) {
        const c = Math.cos(a), s = Math.sin(a), m = M.ident()
        m[0] = c; m[1] = s; m[4] = -s; m[5] = c
        return m
    },
    rotAxis(a: number, x: number, y: number, z: number) {
        const len = Math.hypot(x, y, z) || 1
        x /= len; y /= len; z /= len
        const c = Math.cos(a), s = Math.sin(a), t = 1 - c, m = M.ident()
        m[0] = c + x * x * t; m[1] = y * x * t + z * s; m[2] = z * x * t - y * s
        m[4] = x * y * t - z * s; m[5] = c + y * y * t; m[6] = z * y * t + x * s
        m[8] = x * z * t + y * s; m[9] = y * z * t - x * s; m[10] = c + z * z * t
        return m
    },
    mat3from(m: any) {
        return new Float32Array([m[0], m[1], m[2], m[4], m[5], m[6], m[8], m[9], m[10]])
    },
}

function hexToRgb(hex: string) {
    const h = (hex || "#000").replace("#", "")
    const n = h.length === 3 ? h.split("").map((c) => c + c).join("") : h
    const i = parseInt(n, 16)
    return [((i >> 16) & 255) / 255, ((i >> 8) & 255) / 255, (i & 255) / 255]
}

function easeInOutBack(x: number) {
    const c2 = 1.70158 * 1.525
    return x < 0.5
        ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
        : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (2 * x - 2) + c2) + 2) / 2
}

function rasterizeSvg(svgText: string, size: number, blurPx: number) {
    const doc = new DOMParser().parseFromString(svgText, "image/svg+xml")
    const paths = [...doc.querySelectorAll("path")]
    const layers = ["glyph", "frame"]
    function renderOne(node: any) {
        const wrap = doc.documentElement.cloneNode(false) as any
        wrap.setAttribute("width", String(size))
        wrap.setAttribute("height", String(size))
        const clone = node.cloneNode(true)
        clone.setAttribute("fill", "#ffffff")
        clone.removeAttribute("fill-opacity")
        wrap.appendChild(clone)
        return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(new XMLSerializer().serializeToString(wrap))
    }
    return Promise.all(
        layers.map((_, idx) => {
            const node = paths[idx]
            if (!node) return Promise.resolve(new Float32Array(size * size))
            return new Promise<Float32Array>((resolve) => {
                const img = new Image()
                img.onload = () => {
                    const cv = document.createElement("canvas")
                    cv.width = cv.height = size
                    const ctx = cv.getContext("2d") as any
                    if (blurPx > 0) ctx.filter = `blur(${blurPx}px)`
                    ctx.drawImage(img, 0, 0, size, size)
                    const data = ctx.getImageData(0, 0, size, size).data
                    const out = new Float32Array(size * size)
                    for (let i = 0; i < out.length; i++) out[i] = data[i * 4 + 3] / 255
                    resolve(out)
                }
                img.onerror = () => resolve(new Float32Array(size * size))
                img.src = renderOne(node)
            })
        })
    ).then(([glyph, frame]: any) => ({ width: size, height: size, glyph, frame }))
}

function sampleBilinear(map: any, u: number, v: number) {
    const w = map.width, h = map.height, d = map
    const x = Math.min(w - 1, Math.max(0, u * (w - 1)))
    const y = Math.min(h - 1, Math.max(0, v * (h - 1)))
    const x0 = Math.floor(x), y0 = Math.floor(y), x1 = Math.min(w - 1, x0 + 1), y1 = Math.min(h - 1, y0 + 1)
    const fx = x - x0, fy = y - y0
    const a = d.data[y0 * w + x0], b = d.data[y0 * w + x1], c = d.data[y1 * w + x0], e = d.data[y1 * w + x1]
    return (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + e * fx) * fy
}

function sdRoundRect(px: number, py: number, half: number, r: number) {
    const qx = Math.abs(px) - (half - r)
    const qy = Math.abs(py) - (half - r)
    const ax = Math.max(qx, 0), ay = Math.max(qy, 0)
    return Math.sqrt(ax * ax + ay * ay) + Math.min(Math.max(qx, qy), 0) - r
}

function buildGeometry(opts: any, relief: any) {
    const SEG = 300
    const half = 1.0
    const corner = opts.cornerRadius
    const D = opts.thickness * 2
    const T = 0
    const BOTTOM = -D
    const er = Math.max(0.001, Math.min(opts.edgeBevel, D * 0.5 - 0.001, corner * 0.95))

    const B: number[][] = []
    const Nrm: number[][] = []
    for (let j = 0; j < SEG; j++) {
        const t = (j / SEG) * Math.PI * 2
        const dx = Math.cos(t), dy = Math.sin(t)
        let lo = 0, hi = 1.6
        for (let it = 0; it < 24; it++) {
            const mid = (lo + hi) / 2
            if (sdRoundRect(dx * mid, dy * mid, half, corner) < 0) lo = mid
            else hi = mid
        }
        const rr = (lo + hi) / 2
        const bx = dx * rr, by = dy * rr
        B.push([bx, by])
        const e = 1e-3
        const gx = sdRoundRect(bx + e, by, half, corner) - sdRoundRect(bx - e, by, half, corner)
        const gy = sdRoundRect(bx, by + e, half, corner) - sdRoundRect(bx, by - e, half, corner)
        const gl = Math.hypot(gx, gy) || 1
        Nrm.push([gx / gl, gy / gl])
    }
    const I = B.map((b, j) => [b[0] - Nrm[j][0] * er, b[1] - Nrm[j][1] * er])

    function reliefAt(x: number, y: number) {
        const u = (x + 1) / 2
        const v = 1 - (y + 1) / 2
        const g = sampleBilinear({ width: relief.width, height: relief.height, data: relief.glyph }, u, v)
        const f = sampleBilinear({ width: relief.width, height: relief.height, data: relief.frame }, u, v)
        return g * opts.reliefDepth * 2 + f * opts.frameDepth * 2
    }

    const rings: number[][][] = []

    const RT = 150
    for (let i = 1; i <= RT; i++) {
        const f = i / RT
        const row: number[][] = []
        for (let j = 0; j < SEG; j++) {
            const x = I[j][0] * f, y = I[j][1] * f
            row.push([x, y, T + reliefAt(x, y)])
        }
        rings.push(row)
    }

    // frame height at the boundary — the bullnose rolls off from this level so
    // the raised frame stays flush with the rounded edge (no inset margin).
    const zTop = I.map((p) => T + reliefAt(p[0], p[1]))

    const EQ = 16
    for (let k = 1; k <= EQ; k++) {
        const a = (k / EQ) * (Math.PI / 2)
        const h = er * Math.sin(a), dz = er * (1 - Math.cos(a))
        const row: number[][] = []
        for (let j = 0; j < SEG; j++) row.push([I[j][0] + Nrm[j][0] * h, I[j][1] + Nrm[j][1] * h, zTop[j] - dz])
        rings.push(row)
    }
    const SM = 6
    for (let k = 1; k < SM; k++) {
        const t = k / SM
        const row: number[][] = []
        for (let j = 0; j < SEG; j++) {
            const a = zTop[j] - er, b = BOTTOM + er
            row.push([I[j][0] + Nrm[j][0] * er, I[j][1] + Nrm[j][1] * er, a + (b - a) * t])
        }
        rings.push(row)
    }
    for (let k = 0; k <= EQ; k++) {
        const a = (k / EQ) * (Math.PI / 2)
        const h = er * Math.cos(a), z = BOTTOM + er * (1 - Math.sin(a))
        const row: number[][] = []
        for (let j = 0; j < SEG; j++) row.push([I[j][0] + Nrm[j][0] * h, I[j][1] + Nrm[j][1] * h, z])
        rings.push(row)
    }

    const RB = 24
    for (let i = RB - 1; i >= 1; i--) {
        const f = i / RB
        const row: number[][] = []
        for (let j = 0; j < SEG; j++) row.push([I[j][0] * f, I[j][1] * f, BOTTOM])
        rings.push(row)
    }

    const verts: number[][] = []
    const idx: number[] = []
    const id = (arr: number[]) => {
        verts.push(arr)
        return verts.length - 1
    }
    // pole z = average of the innermost ring so the apex can't spike into a
    // visible dot at the center (which lands inside the R's mouth).
    let cz = 0
    for (let j = 0; j < SEG; j++) cz += rings[0][j][2]
    cz /= SEG
    const topCenter = id([0, 0, cz])
    const ringIdx = rings.map((r) => r.map((p) => id(p)))
    const botCenter = id([0, 0, BOTTOM])

    // cap fans must wind the SAME way as the strips, or the face normals cancel at
    // the innermost ring and leave a mis-shaded disc at the pole (dot on the front,
    // square on the back). Vertex order below matches the strip winding.
    const first = ringIdx[0]
    for (let j = 0; j < SEG; j++) {
        const j2 = (j + 1) % SEG
        idx.push(topCenter, first[j2], first[j])
    }
    for (let i = 0; i < ringIdx.length - 1; i++) {
        const A = ringIdx[i], C = ringIdx[i + 1]
        for (let j = 0; j < SEG; j++) {
            const j2 = (j + 1) % SEG
            idx.push(A[j], A[j2], C[j], C[j], A[j2], C[j2])
        }
    }
    const last = ringIdx[ringIdx.length - 1]
    for (let j = 0; j < SEG; j++) {
        const j2 = (j + 1) % SEG
        idx.push(botCenter, last[j], last[j2])
    }

    const nrm = verts.map(() => [0, 0, 0])
    for (let t = 0; t < idx.length; t += 3) {
        const A = verts[idx[t]], B = verts[idx[t + 1]], C = verts[idx[t + 2]]
        const ux = B[0] - A[0], uy = B[1] - A[1], uz = B[2] - A[2]
        const vx = C[0] - A[0], vy = C[1] - A[1], vz = C[2] - A[2]
        const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx
        for (let k = 0; k < 3; k++) {
            const n = nrm[idx[t + k]]
            n[0] += nx; n[1] += ny; n[2] += nz
        }
    }
    for (const n of nrm) {
        const l = Math.hypot(n[0], n[1], n[2]) || 1
        n[0] /= l; n[1] /= l; n[2] /= l
    }

    // topology-aware normal smoothing — kills the faceted/"hairy" look on the
    // steep relief walls without moving any geometry.
    const R = ringIdx.length
    const avgPole = (src: any, ringArr: number[]) => {
        let ax = 0, ay = 0, az = 0
        for (let j = 0; j < SEG; j++) {
            const n = src[ringArr[j]]
            ax += n[0]; ay += n[1]; az += n[2]
        }
        const l = Math.hypot(ax, ay, az) || 1
        return [ax / l, ay / l, az / l]
    }
    let cur: number[][] = nrm
    for (let pass = 0; pass < 2; pass++) {
        const nxt = cur.map((n) => n.slice())
        for (let r = 0; r < R; r++) {
            for (let j = 0; j < SEG; j++) {
                const c = ringIdx[r][j]
                const nb = [
                    cur[c],
                    cur[ringIdx[r][(j + SEG - 1) % SEG]],
                    cur[ringIdx[r][(j + 1) % SEG]],
                    cur[r > 0 ? ringIdx[r - 1][j] : topCenter],
                    cur[r < R - 1 ? ringIdx[r + 1][j] : botCenter],
                ]
                let ax = 0, ay = 0, az = 0
                for (const n of nb) { ax += n[0]; ay += n[1]; az += n[2] }
                const l = Math.hypot(ax, ay, az) || 1
                nxt[c] = [ax / l, ay / l, az / l]
            }
        }
        nxt[topCenter] = avgPole(cur, ringIdx[0])
        nxt[botCenter] = avgPole(cur, ringIdx[R - 1])
        cur = nxt
    }

    // guard against zero-length normals (sharp relief can cancel face normals to
    // ~0 → NaN after normalize() in the shader → see-through specks).
    const mid = BOTTOM / 2
    for (let i = 0; i < verts.length; i++) {
        const n = cur[i]
        if (!(Math.hypot(n[0], n[1], n[2]) > 1e-5)) cur[i] = [0, 0, verts[i][2] > mid ? 1 : -1]
    }

    const positions = new Float32Array(idx.length * 3)
    const normals = new Float32Array(idx.length * 3)
    for (let t = 0; t < idx.length; t++) {
        const v = verts[idx[t]], n = cur[idx[t]]
        positions.set(v, t * 3)
        normals.set(n, t * 3)
    }
    return { positions, normals, count: idx.length }
}

const VERT = `
attribute vec3 aPos;
attribute vec3 aNrm;
uniform mat4 uMVP;
uniform mat4 uModelView;
uniform mat3 uNormal;
varying vec3 vN;
varying vec3 vView;
void main(){
  vN = normalize(uNormal * aNrm);
  vec4 mv = uModelView * vec4(aPos,1.0);
  vView = mv.xyz;
  gl_Position = uMVP * vec4(aPos,1.0);
}`

const FRAG = `
precision highp float;
varying vec3 vN;
varying vec3 vView;
uniform vec3 uColor;
uniform vec3 uEnv;
uniform vec3 uLightDir;
uniform float uLightI;
uniform vec3 uFillDir;
uniform float uFillI;
uniform float uAmbient;
uniform float uRough;
uniform float uMetal;
uniform float uGloss;
vec3 aces(vec3 x){ return clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14),0.0,1.0); }
void main(){
  float nl = length(vN);
  vec3 N = nl > 1e-4 ? vN / nl : vec3(0.0, 0.0, 1.0);
  if(!gl_FrontFacing) N = -N;
  vec3 V = normalize(-vView);
  vec3 L = normalize(uLightDir);
  vec3 Lf = normalize(uFillDir);
  vec3 H = normalize(L+V);
  float key = clamp(dot(N,L),0.0,1.0);
  float fill = clamp(dot(N,Lf),0.0,1.0);
  float up = N.y*0.5+0.5;
  // sRGB -> linear so a fully-lit face renders as the actual swatch color
  // (otherwise the final gamma encode washes the gold to pale cream)
  vec3 base = pow(uColor, vec3(2.2));
  vec3 env = pow(uEnv, vec3(2.2));
  // body-tinted ambient keeps the color saturated instead of washing to white
  vec3 amb = base * mix(0.5, 0.85, up) * uAmbient;
  float shin = mix(8.0, 220.0, 1.0-uRough);
  float spec = pow(clamp(dot(N,H),0.0,1.0), shin) * uGloss;
  float fres = pow(1.0-clamp(dot(N,V),0.0,1.0), 4.0)*0.25;
  vec3 diffuse = base * (key*uLightI + fill*uFillI);
  vec3 dielectric = amb + diffuse + vec3(spec) + env*fres;
  vec3 metalCol = amb + base*spec + diffuse*0.5 + env*fres;
  vec3 col = mix(dielectric, metalCol, uMetal);
  col = aces(col);
  col = pow(col, vec3(1.0/2.2));
  gl_FragColor = vec4(col, 1.0);
}`

const DOF_VERT = `
attribute vec2 aQuad;
varying vec2 vUv;
void main(){ vUv = aQuad * 0.5 + 0.5; gl_Position = vec4(aQuad, 0.0, 1.0); }`

const DOF_FRAG = `
precision highp float;
uniform sampler2D uColor;
uniform sampler2D uDepth;
uniform vec2 uTexel;
uniform float uFocus;
uniform float uAperture;
uniform float uMaxBlur;
uniform float uNear;
uniform float uFar;
varying vec2 vUv;
float eyeDepth(vec2 uv){
  float z = texture2D(uDepth, uv).r;
  float ndc = z * 2.0 - 1.0;
  return (2.0 * uNear * uFar) / (uFar + uNear - ndc * (uFar - uNear));
}
float coc(float eye){ return clamp(uAperture * abs(eye - uFocus) / max(eye, 0.001), 0.0, 1.0); }
const int N = 32;
void main(){
  vec4 c0 = texture2D(uColor, vUv);
  float r = coc(eyeDepth(vUv)) * uMaxBlur;
  if(r < 0.75){ gl_FragColor = c0; return; }
  vec4 sum = vec4(c0.rgb * c0.a, c0.a);
  float wsum = 1.0;
  float ga = 2.39996323;
  for(int i = 0; i < N; i++){
    float fi = float(i) + 1.0;
    float t = fi / float(N);
    float ang = fi * ga;
    vec2 off = vec2(cos(ang), sin(ang)) * sqrt(t) * r;
    vec2 uv = vUv + off * uTexel;
    vec4 c = texture2D(uColor, uv);
    float w = clamp(coc(eyeDepth(uv)) * uMaxBlur / max(r, 1.0), 0.0, 1.0);
    sum += vec4(c.rgb * c.a, c.a) * w;
    wsum += w;
  }
  vec4 o = sum / max(wsum, 1e-4);
  vec3 rgb = o.a > 1e-4 ? o.rgb / o.a : c0.rgb;
  gl_FragColor = vec4(rgb, o.a);
}`

function compile(gl: any, type: number, src: string) {
    const s = gl.createShader(type)
    gl.shaderSource(s, src)
    gl.compileShader(s)
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error("shader: " + gl.getShaderInfoLog(s))
    return s
}

function createTokenRenderer(canvas: HTMLCanvasElement, userOpts: any) {
    const opts = { ...DEFAULT_OPTIONS, ...(userOpts || {}) }
    const gl: any = canvas.getContext("webgl", { antialias: true, alpha: true, premultipliedAlpha: false })
    if (!gl) throw new Error("WebGL not available")

    const prog = gl.createProgram()
    gl.attachShader(prog, compile(gl, gl.VERTEX_SHADER, VERT))
    gl.attachShader(prog, compile(gl, gl.FRAGMENT_SHADER, FRAG))
    gl.linkProgram(prog)
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error("link: " + gl.getProgramInfoLog(prog))
    gl.useProgram(prog)

    const loc = (n: string) => gl.getUniformLocation(prog, n)
    const U: any = {
        mvp: loc("uMVP"), mv: loc("uModelView"), nrm: loc("uNormal"),
        color: loc("uColor"), env: loc("uEnv"), light: loc("uLightDir"),
        lightI: loc("uLightI"), fill: loc("uFillDir"), fillI: loc("uFillI"),
        ambient: loc("uAmbient"), rough: loc("uRough"),
        metal: loc("uMetal"), gloss: loc("uGloss"),
    }
    const aPos = gl.getAttribLocation(prog, "aPos")
    const aNrm = gl.getAttribLocation(prog, "aNrm")
    const posBuf = gl.createBuffer()
    const nrmBuf = gl.createBuffer()

    gl.enable(gl.DEPTH_TEST)
    gl.disable(gl.CULL_FACE) // double-sided: shader flips back-face normals

    let geo: any = null
    let raf = 0
    let startT = -1
    let disposed = false

    const NEAR = 0.1
    const FAR = 100

    // depth-of-field resources (lazy; needs WEBGL_depth_texture)
    const depthExt = gl.getExtension("WEBGL_depth_texture")
    let dofSupported = !!depthExt
    let dofProg: any = null,
        DU: any = null,
        aQuad = -1,
        quadBuf: any = null,
        fbo: any = null,
        colorTex: any = null,
        depthTex: any = null,
        fboW = 0,
        fboH = 0
    let lastFocusEye = 6

    function initDof() {
        if (dofProg || !dofSupported) return
        dofProg = gl.createProgram()
        gl.attachShader(dofProg, compile(gl, gl.VERTEX_SHADER, DOF_VERT))
        gl.attachShader(dofProg, compile(gl, gl.FRAGMENT_SHADER, DOF_FRAG))
        gl.linkProgram(dofProg)
        if (!gl.getProgramParameter(dofProg, gl.LINK_STATUS)) throw new Error("dof link")
        DU = {
            color: gl.getUniformLocation(dofProg, "uColor"),
            depth: gl.getUniformLocation(dofProg, "uDepth"),
            texel: gl.getUniformLocation(dofProg, "uTexel"),
            focus: gl.getUniformLocation(dofProg, "uFocus"),
            aperture: gl.getUniformLocation(dofProg, "uAperture"),
            maxBlur: gl.getUniformLocation(dofProg, "uMaxBlur"),
            near: gl.getUniformLocation(dofProg, "uNear"),
            far: gl.getUniformLocation(dofProg, "uFar"),
        }
        aQuad = gl.getAttribLocation(dofProg, "aQuad")
        quadBuf = gl.createBuffer()
        gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf)
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW)
        fbo = gl.createFramebuffer()
        colorTex = gl.createTexture()
        depthTex = gl.createTexture()
    }

    function ensureFbo(W: number, H: number) {
        if (fboW === W && fboH === H) return
        fboW = W
        fboH = H
        gl.bindTexture(gl.TEXTURE_2D, colorTex)
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, W, H, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
        gl.bindTexture(gl.TEXTURE_2D, depthTex)
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, W, H, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo)
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex, 0)
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTex, 0)
        if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) dofSupported = false
        gl.bindFramebuffer(gl.FRAMEBUFFER, null)
    }

    function rebuildGeometry() {
        return rasterizeSvg(opts.svg, 512, opts.reliefBlur).then((relief: any) => {
            geo = buildGeometry(opts, relief)
            gl.bindBuffer(gl.ARRAY_BUFFER, posBuf)
            gl.bufferData(gl.ARRAY_BUFFER, geo.positions, gl.STATIC_DRAW)
            gl.bindBuffer(gl.ARRAY_BUFFER, nrmBuf)
            gl.bufferData(gl.ARRAY_BUFFER, geo.normals, gl.STATIC_DRAW)
        })
    }

    function resize() {
        const dpr = opts.dpr > 0 ? opts.dpr : Math.min(2, window.devicePixelRatio || 1)
        const rect = canvas.getBoundingClientRect()
        const w = Math.max(1, Math.round((rect.width || 300) * dpr))
        const h = Math.max(1, Math.round((rect.height || 300) * dpr))
        if (canvas.width !== w || canvas.height !== h) {
            canvas.width = w
            canvas.height = h
        }
    }

    function drawScene(t: number, W: number, H: number) {
        gl.viewport(0, 0, W, H)
        if (opts.background === "transparent") gl.clearColor(0, 0, 0, 0)
        else {
            const bg = hexToRgb(opts.background)
            gl.clearColor(bg[0], bg[1], bg[2], 1)
        }
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

        if (geo) {
            const aspect = W / H
            // focal length -> vertical FOV via a 35mm-equivalent sensor
            const sensor = 36
            const fov = 2 * Math.atan(sensor / (2 * Math.max(8, opts.focalLength)))
            const proj = M.perspective(fov, aspect, NEAR, FAR)
            const f = 1 / Math.tan(fov / 2)

            const ry = (opts.tiltY * Math.PI) / 180
            const rx = (opts.tiltX * Math.PI) / 180
            const rz = (opts.tiltZ * Math.PI) / 180

            // spin = a turn about an arbitrary axis (default Y) in the token's local frame
            let spinAngle = 0
            if (opts.spin) {
                const spinDur = 1 / Math.max(0.05, opts.spinSpeed)
                const period = spinDur + Math.max(0, opts.spinPause)
                const tc = t % period
                if (tc < spinDur) spinAngle = easeInOutBack(tc / spinDur) * Math.PI * 2
            }
            let axx = opts.spinAxisX,
                axy = opts.spinAxisY,
                axz = opts.spinAxisZ
            if (Math.hypot(axx, axy, axz) < 1e-6) {
                axx = 0; axy = 1; axz = 0
            }

            // Auto-fit: smallest camera distance that keeps every corner of the tilted
            // slab inside the frame (accounts for perspective foreshortening of the
            // near corner). Fit scales with f, so focal length changes perspective
            // character, not framing. zoom>1 magnifies past the fit (intentional crop).
            const m = 0.93
            const zTop = opts.thickness + Math.max(opts.frameDepth, opts.reliefDepth, 0)
            const zBot = -opts.thickness
            const floatPad = opts.float ? Math.abs(opts.floatAmp) * 2 : 0
            const cxr = Math.cos(rx),
                sxr = Math.sin(rx)
            const czr = Math.cos(rz),
                szr = Math.sin(rz)
            const cyB = Math.cos(ry),
                syB = Math.sin(ry)
            const dReqForSpin = (spinAng: number) => {
                const ca = Math.cos(spinAng),
                    sa = Math.sin(spinAng),
                    t1 = 1 - ca
                let d = 0
                for (let xi = -1; xi <= 1; xi += 2)
                    for (let yi = -1; yi <= 1; yi += 2)
                        for (const z of [zBot, zTop]) {
                            // corner -> spin(axis) -> rotY -> rotX -> rotZ (pre-translate)
                            const dotk = axx * xi + axy * yi + axz * z
                            const sx0 = xi * ca + (axy * z - axz * yi) * sa + axx * dotk * t1
                            const sy0 = yi * ca + (axz * xi - axx * z) * sa + axy * dotk * t1
                            const sz0 = z * ca + (axx * yi - axy * xi) * sa + axz * dotk * t1
                            const x1 = cyB * sx0 + syB * sz0,
                                z1 = -syB * sx0 + cyB * sz0
                            const y2 = cxr * sy0 - sxr * z1,
                                z2 = sxr * sy0 + cxr * z1
                            const xf = czr * x1 - szr * y2,
                                yf = szr * x1 + czr * y2
                            const dx = z2 + ((f / aspect) * Math.abs(xf)) / m
                            const dy = z2 + (f * (Math.abs(yf) + floatPad)) / m
                            d = Math.max(d, dx, dy)
                        }
                return d
            }
            let fitDist
            if (opts.spin) {
                fitDist = 0
                for (let i = 0; i < 24; i++) fitDist = Math.max(fitDist, dReqForSpin((i / 24) * Math.PI * 2))
            } else {
                fitDist = dReqForSpin(0)
            }
            const dist = fitDist / opts.zoom
            const view = M.translate(0, 0, -dist)
            // focus plane sweeps front-to-back across the tilted token's depth
            lastFocusEye = dist - (opts.dofFocus * 2 - 1) * 1.0

            // roll (rotZ) outermost = in-frame roll; spin innermost = local-axis turn
            let model = M.mul(M.rotZ(rz), M.mul(M.rotX(rx), M.rotY(ry)))
            if (opts.spin) model = M.mul(model, M.rotAxis(spinAngle, axx, axy, axz))
            model = M.mul(model, M.scale(1.0))
            if (opts.float) {
                const bob = Math.sin(t * opts.floatSpeed * 2) * opts.floatAmp
                model = M.mul(M.translate(0, bob * 2, 0), model)
            }
            const mv = M.mul(view, model)
            const mvp = M.mul(proj, mv)
            const normalMat = M.mat3from(mv)

            gl.useProgram(prog)
            gl.uniformMatrix4fv(U.mvp, false, mvp)
            gl.uniformMatrix4fv(U.mv, false, mv)
            gl.uniformMatrix3fv(U.nrm, false, normalMat)
            gl.uniform3fv(U.color, hexToRgb(opts.color))
            gl.uniform3fv(U.env, hexToRgb(opts.envTint))
            const az = (opts.lightAngle * Math.PI) / 180
            const el = (opts.lightElevation * Math.PI) / 180
            gl.uniform3f(U.light, Math.cos(el) * Math.sin(az), Math.sin(el), Math.cos(el) * Math.cos(az))
            gl.uniform1f(U.lightI, opts.lightIntensity)
            const faz = (opts.fillAngle * Math.PI) / 180
            const fel = (opts.fillElevation * Math.PI) / 180
            gl.uniform3f(U.fill, Math.cos(fel) * Math.sin(faz), Math.sin(fel), Math.cos(fel) * Math.cos(faz))
            gl.uniform1f(U.fillI, opts.fillIntensity)
            gl.uniform1f(U.ambient, opts.ambient)
            gl.uniform1f(U.rough, opts.roughness)
            gl.uniform1f(U.metal, opts.metalness)
            gl.uniform1f(U.gloss, opts.gloss)

            gl.bindBuffer(gl.ARRAY_BUFFER, posBuf)
            gl.enableVertexAttribArray(aPos)
            gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)
            gl.bindBuffer(gl.ARRAY_BUFFER, nrmBuf)
            gl.enableVertexAttribArray(aNrm)
            gl.vertexAttribPointer(aNrm, 3, gl.FLOAT, false, 0, 0)
            gl.drawArrays(gl.TRIANGLES, 0, geo.count)
        }
    }

    function drawDof(W: number, H: number) {
        gl.bindFramebuffer(gl.FRAMEBUFFER, null)
        gl.viewport(0, 0, W, H)
        gl.disable(gl.DEPTH_TEST)
        gl.useProgram(dofProg)
        gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf)
        gl.enableVertexAttribArray(aQuad)
        gl.vertexAttribPointer(aQuad, 2, gl.FLOAT, false, 0, 0)
        gl.activeTexture(gl.TEXTURE0)
        gl.bindTexture(gl.TEXTURE_2D, colorTex)
        gl.uniform1i(DU.color, 0)
        gl.activeTexture(gl.TEXTURE1)
        gl.bindTexture(gl.TEXTURE_2D, depthTex)
        gl.uniform1i(DU.depth, 1)
        gl.uniform2f(DU.texel, 1 / W, 1 / H)
        gl.uniform1f(DU.focus, lastFocusEye)
        gl.uniform1f(DU.aperture, opts.dofAmount * 3.0)
        gl.uniform1f(DU.maxBlur, opts.dofBokeh * (H / 600))
        gl.uniform1f(DU.near, NEAR)
        gl.uniform1f(DU.far, FAR)
        gl.drawArrays(gl.TRIANGLES, 0, 3)
        gl.bindTexture(gl.TEXTURE_2D, null)
        gl.activeTexture(gl.TEXTURE0)
        gl.bindTexture(gl.TEXTURE_2D, null)
        gl.enable(gl.DEPTH_TEST)
    }

    function frame(now: number) {
        if (disposed) return
        if (startT < 0) startT = now
        const t = (now - startT) / 1000
        resize()
        const W = canvas.width, H = canvas.height
        if (opts.dof && dofSupported) {
            initDof()
            ensureFbo(W, H) // may clear dofSupported if the FBO is unsupported
        }
        if (opts.dof && dofSupported) {
            gl.bindFramebuffer(gl.FRAMEBUFFER, fbo)
            drawScene(t, W, H)
            drawDof(W, H)
        } else {
            gl.bindFramebuffer(gl.FRAMEBUFFER, null)
            drawScene(t, W, H)
        }
        raf = requestAnimationFrame(frame)
    }

    rebuildGeometry().then(() => {
        raf = requestAnimationFrame(frame)
    })

    return {
        setOptions(patch: any) {
            const geoKeys = ["svg", "thickness", "cornerRadius", "edgeBevel", "reliefDepth", "frameDepth", "reliefBlur"]
            const rebuild = geoKeys.some((k) => k in patch && patch[k] !== opts[k])
            Object.assign(opts, patch)
            if (rebuild) rebuildGeometry()
        },
        dispose() {
            disposed = true
            cancelAnimationFrame(raf)
            gl.deleteBuffer(posBuf)
            gl.deleteBuffer(nrmBuf)
            gl.deleteProgram(prog)
            if (dofProg) {
                gl.deleteProgram(dofProg)
                gl.deleteBuffer(quadBuf)
                gl.deleteFramebuffer(fbo)
                gl.deleteTexture(colorTex)
                gl.deleteTexture(depthTex)
            }
            const ext = gl.getExtension("WEBGL_lose_context")
            if (ext) ext.loseContext()
        },
    }
}

// =============================================================================
// ====  FRAMER REACT WRAPPER  =================================================
// =============================================================================

/**
 * @framerIntrinsicWidth 240
 * @framerIntrinsicHeight 240
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight any
 */
export default function RunhappyToken(props: any) {
    // Pose + Camera + Motion are exposed in Framer; color / material / lighting /
    // shape render from the baked DEFAULT_OPTIONS (kept off the panel to reduce
    // clutter). Background defaults to transparent so it drops onto any frame.
    const {
        tiltX, tiltY, tiltZ, zoom,
        focalLength, dof, dofFocus, dofAmount, dofBokeh,
        spin, spinSpeed, spinPause, spinAxisX, spinAxisY, spinAxisZ, float, floatAmp,
        style,
    } = props

    const canvasRef = React.useRef<HTMLCanvasElement | null>(null)
    const tokenRef = React.useRef<any>(null)

    const optionObj = {
        tiltX, tiltY, tiltZ, zoom,
        focalLength, dof, dofFocus, dofAmount, dofBokeh,
        spin, spinSpeed, spinPause, spinAxisX, spinAxisY, spinAxisZ, float, floatAmp,
    }

    // create once
    React.useEffect(() => {
        if (!canvasRef.current) return
        try {
            tokenRef.current = createTokenRenderer(canvasRef.current, optionObj)
        } catch (e) {
            // WebGL unavailable — fail quietly
        }
        return () => {
            tokenRef.current?.dispose()
            tokenRef.current = null
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [])

    // push prop changes
    React.useEffect(() => {
        tokenRef.current?.setOptions(optionObj)
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [
        tiltX, tiltY, tiltZ, zoom,
        focalLength, dof, dofFocus, dofAmount, dofBokeh,
        spin, spinSpeed, spinPause, spinAxisX, spinAxisY, spinAxisZ, float, floatAmp,
    ])

    return (
        <canvas
            ref={canvasRef}
            style={{ width: "100%", height: "100%", display: "block", ...style }}
        />
    )
}

RunhappyToken.defaultProps = {
    width: 240,
    height: 240,
}

// Pose + Camera + Motion are exposed. Color / material / lighting / shape are fixed
// to the baked DEFAULT_OPTIONS to keep the Framer panel focused.
addPropertyControls(RunhappyToken, {
    // ---- Pose (object rotation: pitch / yaw / roll) ----
    tiltX: { type: ControlType.Number, title: "Rotate X", min: -180, max: 180, step: 1, defaultValue: 0 },
    tiltY: { type: ControlType.Number, title: "Rotate Y", min: -180, max: 180, step: 1, defaultValue: 10 },
    tiltZ: { type: ControlType.Number, title: "Rotate Z", min: -180, max: 180, step: 1, defaultValue: 6 },
    zoom: { type: ControlType.Number, title: "Zoom", min: 0.5, max: 2, step: 0.01, defaultValue: 1 },

    // ---- Camera / lens ----
    focalLength: { type: ControlType.Number, title: "Focal len", min: 14, max: 200, step: 1, defaultValue: 100 },
    dof: { type: ControlType.Boolean, title: "Depth of field", defaultValue: true },
    dofFocus: {
        type: ControlType.Number, title: "Focus", min: 0, max: 1, step: 0.01, defaultValue: 0.5,
        hidden: (p: any) => !p.dof,
    },
    dofAmount: {
        type: ControlType.Number, title: "Blur", min: 0, max: 1, step: 0.01, defaultValue: 0.5,
        hidden: (p: any) => !p.dof,
    },
    dofBokeh: {
        type: ControlType.Number, title: "Bokeh size", min: 4, max: 48, step: 1, defaultValue: 10,
        hidden: (p: any) => !p.dof,
    },

    // ---- Motion ----
    spin: { type: ControlType.Boolean, title: "Spin", defaultValue: false },
    spinSpeed: {
        type: ControlType.Number, title: "Spin speed", min: 0.05, max: 3, step: 0.05, defaultValue: 0.5,
        hidden: (p: any) => !p.spin,
    },
    spinPause: {
        type: ControlType.Number, title: "Spin pause", min: 0, max: 4, step: 0.1, defaultValue: 0.3,
        hidden: (p: any) => !p.spin,
    },
    spinAxisX: {
        type: ControlType.Number, title: "Spin axis X", min: -1, max: 1, step: 0.05, defaultValue: 0,
        hidden: (p: any) => !p.spin,
    },
    spinAxisY: {
        type: ControlType.Number, title: "Spin axis Y", min: -1, max: 1, step: 0.05, defaultValue: -0.6,
        hidden: (p: any) => !p.spin,
    },
    spinAxisZ: {
        type: ControlType.Number, title: "Spin axis Z", min: -1, max: 1, step: 0.05, defaultValue: 0,
        hidden: (p: any) => !p.spin,
    },
    float: { type: ControlType.Boolean, title: "Float", defaultValue: false },
    floatAmp: {
        type: ControlType.Number, title: "Float amt", min: 0, max: 0.2, step: 0.005, defaultValue: 0.2,
        hidden: (p: any) => !p.float,
    },
})
