类型:3D · 分类:UI Elements · 收录:2026-09-16 21:09:01 · 源码:会员可见
# 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]