motionsites中文站 / Engraved Certificate
Engraved Certificate
aadmin2026-09-16 发布3D3 阅读

Engraved Certificate

类型:3D · 分类:UI Elements · 收录:2026-09-16 21:09:01 · 源码:会员可见

[hidecontent type="vip1"]
# Engraved Certificate
A responsive engraved certificate: plate field, dual guilloche rosettes, and a drifting harmonic pass that auto-cycles through four cam states.

> 来源:ThreeUI Community · https://threeui.com · MIT License(Meng To / Design+Code)
> 组件页:https://threeui.com/component/engraved-certificate · 预览动画:https://threeui.com/previews/engraved-certificate.webm
> 分类:UI Elements

/* ===== src/shaders/neuform-isolated/sources/kinetic-lathe-certificate.html (html) ===== */
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  <title>Kinetic Lathe</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
  <style>
    :root {
      --ground: #ded6c2;
      --ground2: #d3cab3;
      --sheet: #f5efdf;
      --sheet2: #efe7d3;
      --ink: #1f3a30;
      --ink-6: rgba(31, 58, 48, 0.62);
      --ink-4: rgba(31, 58, 48, 0.44);
      --rule: rgba(31, 58, 48, 0.22);
    }

    * { box-sizing: border-box; }

    html, body {
      width: 100%;
      height: 100%;
      margin: 0;
      overflow: hidden;
      background:
        radial-gradient(118% 92% at 50% 0%, #e6dfcd 0%, var(--ground) 52%, var(--ground2) 100%);
      color: var(--ink);
      font-family: "Cormorant Garamond", Didot, "Times New Roman", serif;
      -webkit-font-smoothing: antialiased;
    }

    main {
      width: 100%;
      height: 100%;
      display: grid;
      place-items: center;
      padding: clamp(12px, 3vmin, 28px);
    }

    #cert {
      position: relative;
      width: min(100%, 720px);
      aspect-ratio: 1.08 / 1;
      max-height: min(100%, 720px);
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: clamp(10px, 2.4vmin, 18px);
      padding: clamp(16px, 4vmin, 36px);
      border-radius: 3px;
      background:
        repeating-linear-gradient(0deg, rgba(31, 58, 48, 0.045) 0 0.5px, transparent 0.5px 6px),
        linear-gradient(168deg, #f8f3e6 0%, var(--sheet) 40%, var(--sheet2) 100%);
      box-shadow:
        0 0 0 1px rgba(31, 58, 48, 0.13),
        0 1px 0 rgba(255, 255, 255, 0.55) inset,
        0 30px 60px -34px rgba(22, 38, 28, 0.62),
        0 3px 8px rgba(22, 38, 28, 0.09);
    }

    #plate {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      display: block;
      border-radius: inherit;
      pointer-events: none;
    }

    .face {
      position: relative;
      z-index: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: clamp(10px, 2.2vmin, 16px);
      width: min(100%, 420px);
      text-align: center;
    }

    .mark {
      font-family: "IBM Plex Mono", ui-monospace, monospace;
      font-size: clamp(9px, 1.7vmin, 11px);
      letter-spacing: 0.24em;
      text-transform: uppercase;
      color: var(--ink-4);
    }

    h1 {
      margin: 0;
      font-size: clamp(22px, 4.8vmin, 34px);
      font-weight: 500;
      letter-spacing: 0.02em;
      line-height: 1;
    }

    h1 em {
      font-style: normal;
      color: var(--ink-6);
    }

    #medal {
      position: relative;
      width: min(58vmin, 280px);
      height: min(58vmin, 280px);
      border-radius: 50%;
      flex: none;
    }

    #rosette,
    #drift {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      display: block;
      pointer-events: none;
    }

    .caption {
      font-family: "IBM Plex Mono", ui-monospace, monospace;
      font-size: clamp(9px, 1.7vmin, 11px);
      letter-spacing: 0.22em;
      text-transform: uppercase;
      color: var(--ink-4);
    }

    @media (max-width: 520px), (max-height: 560px) {
      #cert {
        aspect-ratio: auto;
        height: 100%;
        max-height: none;
      }

      #medal {
        width: min(62vmin, 240px);
        height: min(62vmin, 240px);
      }
    }
  </style>
</head>
<body>
  <main>
    <article id="cert" aria-label="Kinetic Lathe certificate">
      <canvas id="plate" aria-hidden="true"></canvas>
      <div class="face">
        <div class="mark">Kinetic Lathe</div>
        <figure id="medal">
          <canvas id="rosette" aria-hidden="true"></canvas>
          <canvas id="drift" aria-hidden="true"></canvas>
        </figure>
        <h1>Matrix 92 · Cam <em id="barName">C</em></h1>
        <div class="caption">Primary cam · <span id="capBar">C</span></div>
      </div>
    </article>
  </main>
<script>
(() => {
  "use strict";
  const reduce = matchMedia("(prefers-reduced-motion:reduce)").matches;
  const TAU = Math.PI * 2;
  const INK = "rgba(31,58,48,";
  const ink = (a) => INK + a + ")";
  const SUB = "#f5efdf";

  const cert = document.getElementById("cert");
  const medal = document.getElementById("medal");
  const cPlate = document.getElementById("plate");
  const cRose = document.getElementById("rosette");
  const cDrift = document.getElementById("drift");
  const ctxP = cPlate.getContext("2d");
  const ctxR = cRose.getContext("2d");
  const ctxD = cDrift.getContext("2d");
  if (!ctxP || !ctxR || !ctxD) return;

  const BARS = [
    { id: "A", m: 7, m2: 13, m3: 9 },
    { id: "B", m: 9, m2: 15, m3: 12 },
    { id: "C", m: 11, m2: 19, m3: 14 },
    { id: "D", m: 15, m2: 23, m3: 18 },
  ];
  let sel = 2;

  function fit(canvas, ctx, w, h) {
    const S = Math.min(window.devicePixelRatio || 1, 2);
    const W = Math.max(2, Math.round(w * S));
    const H = Math.max(2, Math.round(h * S));
    if (canvas.width !== W || canvas.height !== H) {
      canvas.width = W;
      canvas.height = H;
    }
    ctx.setTransform(S, 0, 0, S, 0, 0);
    ctx.clearRect(0, 0, w, h);
  }

  function circleOutline(cx, cy, r, samples) {
    const pts = new Array(samples);
    for (let i = 0; i < samples; i++) {
      const th = (i / samples) * TAU;
      pts[i] = [cx + Math.cos(th) * r, cy + Math.sin(th) * r];
    }
    return pts;
  }

  function rectOutline(x, y, w, h, r) {
    const pts = [];
    const push = (px, py) => {
      const l = pts[pts.length - 1];
      if (l && Math.abs(l[0] - px) < 1e-4 && Math.abs(l[1] - py) < 1e-4) return;
      pts.push([px, py]);
    };
    const seg = (x0, y0, xe, ye) => {
      const n = Math.max(2, Math.round(Math.hypot(xe - x0, ye - y0) / 4));
      for (let i = 0; i <= n; i++) push(x0 + ((xe - x0) * i) / n, y0 + ((ye - y0) * i) / n);
    };
    const arc = (cx, cy, a0, a1) => {
      const n = Math.max(7, Math.round((Math.abs(a1 - a0) * r) / 1.6));
      for (let i = 0; i <= n; i++) {
        const a = a0 + ((a1 - a0) * i) / n;
        push(cx + Math.cos(a) * r, cy + Math.sin(a) * r);
      }
    };
    const x1 = x + w;
    const y1 = y + h;
    const HP = Math.PI / 2;
    seg(x + r, y, x1 - r, y);
    arc(x1 - r, y + r, -HP, 0);
    seg(x1, y + r, x1, y1 - r);
    arc(x1 - r, y1 - r, 0, HP);
    seg(x1 - r, y1, x + r, y1);
    arc(x + r, y1 - r, HP, Math.PI);
    seg(x, y1 - r, x, y + r);
    arc(x + r, y + r, Math.PI, Math.PI * 1.5);
    return pts;
  }

  function rail(pts, M) {
    const nP = pts.length;
    const cum = new Float64Array(nP + 1);
    for (let i = 1; i <= nP; i++) {
      const p = pts[i % nP];
      const q = pts[i - 1];
      cum[i] = cum[i - 1] + Math.hypot(p[0] - q[0], p[1] - q[1]);
    }
    const L = cum[nP];
    const out = new Array(M);
    let j = 0;
    for (let i = 0; i < M; i++) {
      const target = (i / M) * L;
      while (j < nP - 1 && cum[j + 1] < target) j++;
      const seg = cum[j + 1] - cum[j] || 1;
      const u = (target - cum[j]) / seg;
      const p = pts[j];
      const q = pts[(j + 1) % nP];
      out[i] = [
        p[0] + (q[0] - p[0]) * u,
        p[1] + (q[1] - p[1]) * u,
        (q[1] - p[1]) / seg,
        -(q[0] - p[0]) / seg,
      ];
    }
    return out;
  }

  function band(ctx, r, o) {
    const M = r.length;
    ctx.lineWidth = o.lw;
    ctx.strokeStyle = ink(o.alpha);
    ctx.beginPath();
    for (let k = 0; k < o.K; k++) {
      const ph = (o.phase || 0) + (k / o.K) * TAU;
      for (let i = 0; i <= M; i++) {
        const p = r[i % M];
        const u = (i % M) / M;
        const off = o.amp * Math.sin(o.waves * u * TAU + ph);
        const x = p[0] + p[2] * off;
        const y = p[1] + p[3] * off;
        if (i) ctx.lineTo(x, y);
        else ctx.moveTo(x, y);
      }
    }
    ctx.stroke();
  }

  function railLine(ctx, r, off, alpha, lw) {
    const M = r.length;
    ctx.lineWidth = lw;
    ctx.strokeStyle = ink(alpha);
    ctx.beginPath();
    for (let i = 0; i <= M; i++) {
      const p = r[i % M];
      const x = p[0] + p[2] * off;
      const y = p[1] + p[3] * off;
      if (i) ctx.lineTo(x, y);
      else ctx.moveTo(x, y);
    }
    ctx.stroke();
  }

  function rosette(ctx, cx, cy, o) {
    const n = o.n;
    const pts = o.pts;
    const m = o.m;
    const d = o.d;
    const dir = o.dir || 1;
    const shrink = o.shrink === undefined ? 0.06 : o.shrink;
    ctx.lineWidth = o.lw;
    ctx.strokeStyle = ink(o.alpha);
    ctx.lineJoin = "round";
    ctx.beginPath();
    for (let k = 0; k < n; k++) {
      const f = k / n;
      const phi = (o.phase || 0) + dir * f * TAU;
      const a = o.A * (1 - shrink * f);
      for (let i = 0; i <= pts; i++) {
        const t = (i / pts) * TAU;
        const x = cx + a * Math.cos(t) + d * Math.cos(m * t + phi);
        const y = cy + a * Math.sin(t) - d * Math.sin(m * t + phi);
        if (i) ctx.lineTo(x, y);
        else ctx.moveTo(x, y);
      }
    }
    ctx.stroke();
  }

  function waveField(ctx, w, h) {
    ctx.lineWidth = 0.5;
    ctx.strokeStyle = ink(0.075);
    const pitch = 6.2;
    const N = Math.ceil(h / pitch) + 2;
    const step = 7;
    ctx.beginPath();
    for (let i = 0; i < N; i++) {
      const y0 = i * pitch - 3;
      const ph = i * 0.44;
      for (let x = 0; x <= w; x += step) {
        const y =
          y0 +
          3.3 * Math.sin(x * 0.0139 + ph) +
          1.9 * Math.sin(x * 0.0327 - ph * 1.7) +
          0.9 * Math.sin(x * 0.0715 + ph * 0.6);
        if (x) ctx.lineTo(x, y);
        else ctx.moveTo(x, y);
      }
    }
    ctx.stroke();
  }

  function rand32(a) {
    return function () {
      a |= 0;
      a = (a + 0x6d2b79f5) | 0;
      let t = Math.imul(a ^ (a >>> 15), 1 | a);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
  }

  function drawPlate() {
    const w = cert.clientWidth;
    const h = cert.clientHeight;
    if (!w || !h) return;
    fit(cPlate, ctxP, w, h);
    ctxP.fillStyle = SUB;
    ctxP.fillRect(0, 0, w, h);

    let g = ctxP.createRadialGradient(w * 0.28, h * 0.16, 8, w * 0.28, h * 0.16, Math.max(w, h) * 0.95);
    g.addColorStop(0, "rgba(255,252,240,.85)");
    g.addColorStop(1, "rgba(255,252,240,0)");
    ctxP.fillStyle = g;
    ctxP.fillRect(0, 0, w, h);
    g = ctxP.createRadialGradient(w * 0.88, h * 0.94, 8, w * 0.88, h * 0.94, Math.max(w, h) * 0.85);
    g.addColorStop(0, ink(0.06));
    g.addColorStop(1, ink(0));
    ctxP.fillStyle = g;
    ctxP.fillRect(0, 0, w, h);

    const rnd = rand32(4409);
    for (let i = 0; i < 1100; i++) {
      ctxP.fillStyle = ink(0.018 + rnd() * 0.05);
      ctxP.fillRect(rnd() * w, rnd() * h, 1, rnd() < 0.12 ? 2 : 1);
    }

    waveField(ctxP, w, h);

    const narrow = w < 520;
    const inset = narrow ? 7 : 10;
    const half = narrow ? 3.1 : 3.8;
    const br = rail(
      rectOutline(inset + half, inset + half, w - 2 * (inset + half), h - 2 * (inset + half), narrow ? 5 : 7),
      narrow ? 760 : 1240,
    );
    band(ctxP, br, { amp: half, waves: narrow ? 54 : 116, K: 9, alpha: 0.3, lw: 0.5 });
    railLine(ctxP, br, -half - 1.9, 0.34, 0.6);
    railLine(ctxP, br, half + 1.9, 0.3, 0.5);
    railLine(ctxP, br, half + 4.4, 0.13, 0.5);
  }

  function drawRosettes() {
    const w = medal.clientWidth;
    const h = medal.clientHeight;
    if (!w || !h) return;
    fit(cRose, ctxR, w, h);
    const cx = w / 2;
    const cy = h / 2;
    const R = Math.min(w, h) / 2;
    const bar = BARS[sel];
    const small = R < 130;

    const g = ctxR.createRadialGradient(cx, cy, R * 0.06, cx, cy, R);
    g.addColorStop(0, "rgba(249,245,234,.92)");
    g.addColorStop(0.76, "rgba(249,245,234,.84)");
    g.addColorStop(1, "rgba(249,245,234,0)");
    ctxR.fillStyle = g;
    ctxR.beginPath();
    ctxR.arc(cx, cy, R, 0, TAU);
    ctxR.fill();

    const ring = rail(circleOutline(cx, cy, R * 0.93, 640), small ? 460 : 620);
    band(ctxR, ring, { amp: R * 0.036, waves: (bar.m + 1) * 5, K: 7, alpha: 0.3, lw: 0.5 });
    railLine(ctxR, ring, -R * 0.036 - 1.8, 0.34, 0.6);
    railLine(ctxR, ring, R * 0.036 + 1.8, 0.28, 0.5);

    rosette(ctxR, cx, cy, {
      A: R * 0.655,
      d: R * 0.215,
      m: bar.m,
      n: small ? 44 : 58,
      pts: small ? 340 : 470,
      alpha: 0.42,
      lw: 0.5,
      shrink: 0.035,
      phase: 0.2,
    });
    rosette(ctxR, cx, cy, {
      A: R * 0.335,
      d: R * 0.115,
      m: bar.m2,
      n: small ? 34 : 44,
      pts: small ? 280 : 380,
      alpha: 0.38,
      lw: 0.5,
      dir: -1,
      shrink: 0.05,
      phase: 0.9,
    });

    ctxR.beginPath();
    ctxR.arc(cx, cy, R * 0.07, 0, TAU);
    ctxR.fillStyle = "rgba(249,245,234,.86)";
    ctxR.fill();
    ctxR.lineWidth = 0.6;
    ctxR.strokeStyle = ink(0.32);
    ctxR.stroke();
    ctxR.beginPath();
    ctxR.arc(cx, cy, R * 0.04, 0, TAU);
    ctxR.strokeStyle = ink(0.24);
    ctxR.lineWidth = 0.5;
    ctxR.stroke();
  }

  let phase = 0;
  function drawDrift() {
    const w = medal.clientWidth;
    const h = medal.clientHeight;
    if (!w || !h) return;
    fit(cDrift, ctxD, w, h);
    const cx = w / 2;
    const cy = h / 2;
    const R = Math.min(w, h) / 2;
    const bar = BARS[sel];
    const small = R < 130;
    rosette(ctxD, cx, cy, {
      A: R * 0.515,
      d: R * 0.165,
      m: bar.m3,
      n: small ? 26 : 34,
      pts: small ? 240 : 320,
      alpha: 0.3,
      lw: 0.5,
      shrink: 0.03,
      phase,
    });
  }

  const elBarName = document.getElementById("barName");
  const elCapBar = document.getElementById("capBar");

  function paintLabels() {
    const b = BARS[sel];
    elBarName.textContent = b.id;
    elCapBar.textContent = b.id;
  }

  function drawAll() {
    drawPlate();
    drawRosettes();
    drawDrift();
  }

  paintLabels();
  drawAll();

  let timer = 0;
  let onScreen = true;
  let camTimer = 0;

  function tick() {
    timer = 0;
    phase += 0.019;
    if (phase > TAU) phase -= TAU;
    drawDrift();
    schedule();
  }

  function schedule() {
    if (timer || reduce || !onScreen || document.hidden) return;
    timer = setTimeout(tick, 140);
  }

  function halt() {
    if (timer) {
      clearTimeout(timer);
      timer = 0;
    }
  }

  function cycleCam() {
    if (reduce || document.hidden || !onScreen) return;
    sel = (sel + 1) % BARS.length;
    paintLabels();
    drawRosettes();
    drawDrift();
  }

  if ("IntersectionObserver" in window) {
    new IntersectionObserver(
      (entries) => {
        onScreen = entries[0].isIntersecting;
        if (onScreen) schedule();
        else halt();
      },
      { threshold: 0.02 },
    ).observe(cert);
  } else {
    schedule();
  }

  document.addEventListener("visibilitychange", () => {
    if (document.hidden) halt();
    else schedule();
  });
  window.addEventListener("pagehide", halt);

  let rt = 0;
  window.addEventListener("resize", () => {
    clearTimeout(rt);
    rt = setTimeout(drawAll, 120);
  });

  if (!reduce) camTimer = setInterval(cycleCam, 4200);
  schedule();

  if (document.fonts && document.fonts.ready) {
    document.fonts.ready.then(drawAll).catch(() => {});
  }
})();
</script>
</body>
</html>
[/hidecontent]
← 返回首页