This commit is contained in:
암냥 2026-09-02 00:14:56 +09:00
commit ca4d5af818
4 changed files with 96 additions and 124 deletions

View file

@ -43,11 +43,11 @@ canvas {
gap: 10px; gap: 10px;
padding: 11px 16px; padding: 11px 16px;
border: 1px solid #ffffff2b; border: 1px solid #ffffff2b;
border-radius: 999px; border-radius: 5px;
color: #d7d7d7; color: #d7d7d7;
background: #0b0b0be6; background: #0b0b0be6;
box-shadow: 0 10px 30px #0008; box-shadow: none;
font: 500 11px monospace; font: 500 15px monospace;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;

View file

@ -45,32 +45,26 @@ function createBadgeTexture(pass) {
ctx.fillStyle = "#050505"; ctx.fillStyle = "#050505";
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
const sheen = ctx.createLinearGradient(0, 0, 912, 1288); ctx.strokeStyle = "#ffffff1f";
sheen.addColorStop(0, "#ffffff09");
sheen.addColorStop(0.45, "transparent");
sheen.addColorStop(1, "#ffffff05");
ctx.fillStyle = sheen;
ctx.fillRect(0, 0, 912, 1288);
ctx.strokeStyle = "#ffffff24";
ctx.lineWidth = 1.5; ctx.lineWidth = 1.5;
ctx.strokeRect(30, 30, 852, 1228); ctx.strokeRect(30, 30, 852, 1228);
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
ctx.font = "700 29px Arial, sans-serif"; ctx.font = "700 34px Arial, sans-serif";
ctx.fillText("PASS", 58, 79); ctx.fillText("PASS", 58, 79);
ctx.fillStyle = "#8f8f8f"; ctx.fillStyle = "#8f8f8f";
ctx.fillText("PORT", 140, 79); ctx.fillText("PORT", 154, 79);
ctx.font = "500 13px monospace"; ctx.font = "500 19px monospace";
ctx.fillText("DIGITAL ACCESS SYSTEM", 58, 105); ctx.fillText("DIGITAL ACCESS SYSTEM", 58, 110);
ctx.textAlign = "right"; ctx.textAlign = "right";
ctx.fillStyle = accent; ctx.fillStyle = accent;
ctx.font = "600 34px monospace";
const statusWidth = ctx.measureText(pass.status).width;
ctx.beginPath(); ctx.beginPath();
ctx.arc(700, 77, 6, 0, Math.PI * 2); ctx.arc(852 - statusWidth - 24, 74, 9, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
ctx.font = "600 16px monospace"; ctx.fillText(pass.status, 852, 86);
ctx.fillText(pass.status, 852, 83);
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.strokeStyle = "#ffffff2d"; ctx.strokeStyle = "#ffffff2d";
@ -79,67 +73,51 @@ function createBadgeTexture(pass) {
ctx.lineTo(854, 145); ctx.lineTo(854, 145);
ctx.stroke(); ctx.stroke();
ctx.save();
ctx.globalAlpha = 0.075;
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 8;
ctx.font = "800 350px Arial, sans-serif";
ctx.strokeText("P", 590, 520);
for (let row = 0; row < 9; row += 1) {
for (let column = 0; column < 9; column += 1) {
const radius = Math.max(1.5, 5 - (row + column) * 0.18);
ctx.beginPath();
ctx.arc(620 + column * 25, 250 + row * 25, radius, 0, Math.PI * 2);
ctx.fillStyle = "#ffffff";
ctx.fill();
}
}
ctx.restore();
ctx.fillStyle = "#929292"; ctx.fillStyle = "#929292";
ctx.font = "500 14px monospace"; ctx.font = "500 22px monospace";
ctx.fillText("TEMPORARY / GUEST", 58, 220); ctx.fillText("TEMPORARY ACCESS CREDENTIAL", 58, 220);
ctx.textAlign = "right"; ctx.textAlign = "right";
ctx.fillText(`NO. ${pass.credentialId}`, 854, 220); ctx.fillText(`NO. ${pass.credentialId}`, 854, 220);
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
ctx.font = "700 86px Arial, sans-serif"; ctx.font = "650 54px Arial, sans-serif";
ctx.fillText("ACCESS", 52, 350); ctx.fillText("GUEST ACCESS", 55, 350);
ctx.font = "300 86px Arial, sans-serif"; ctx.fillStyle = "#8e8e8e";
ctx.fillText("PASS", 52, 430); ctx.font = "400 25px Arial, sans-serif";
ctx.fillText("Temporary entry credential", 58, 398);
ctx.fillStyle = "#858585"; ctx.fillStyle = "#858585";
ctx.font = "500 14px monospace"; ctx.font = "500 22px monospace";
ctx.fillText("PASS HOLDER", 58, 545); ctx.fillText("PASS HOLDER", 58, 510);
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
const holderSize = fitText(ctx, pass.holder, 796, 66); const holderSize = fitText(ctx, pass.holder, 796, 66);
ctx.font = `600 ${holderSize}px Arial, sans-serif`; ctx.font = `600 ${holderSize}px Arial, sans-serif`;
ctx.fillText(pass.holder, 55, 625); ctx.fillText(pass.holder, 55, 590);
ctx.strokeStyle = "#ffffff2d"; ctx.strokeStyle = "#ffffff2d";
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(58, 685); ctx.moveTo(58, 660);
ctx.lineTo(854, 685); ctx.lineTo(854, 660);
ctx.stroke(); ctx.stroke();
ctx.fillStyle = "#858585"; ctx.fillStyle = "#858585";
ctx.font = "500 14px monospace"; ctx.font = "500 22px monospace";
ctx.fillText("AUTHORIZED LOCATION", 58, 738); ctx.fillText("AUTHORIZED LOCATION", 58, 718);
ctx.fillText("VALID FROM", 520, 738); ctx.fillText("VALID FROM", 520, 718);
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
const locationSize = fitText(ctx, pass.location, 400, 32); const locationSize = fitText(ctx, pass.location, 400, 32);
ctx.font = `500 ${locationSize}px Arial, sans-serif`; ctx.font = `500 ${locationSize}px Arial, sans-serif`;
ctx.fillText(pass.location, 58, 782); ctx.fillText(pass.location, 58, 764);
ctx.font = "500 20px monospace"; ctx.font = "500 30px monospace";
ctx.fillText(pass.validFrom, 520, 780); ctx.fillText(pass.validFrom, 520, 762);
ctx.fillStyle = "#858585"; ctx.fillStyle = "#858585";
ctx.font = "500 14px monospace"; ctx.font = "500 22px monospace";
ctx.fillText("VALID UNTIL", 520, 842); ctx.fillText("VALID UNTIL", 520, 832);
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
ctx.font = "500 20px monospace"; ctx.font = "500 30px monospace";
ctx.fillText(pass.validUntil, 520, 884); ctx.fillText(pass.validUntil, 520, 876);
ctx.strokeStyle = "#ffffff2d"; ctx.strokeStyle = "#ffffff2d";
ctx.beginPath(); ctx.beginPath();
@ -155,19 +133,14 @@ function createBadgeTexture(pass) {
ctx.font = "700 14px Arial, sans-serif"; ctx.font = "700 14px Arial, sans-serif";
ctx.fillText("✓", 65, 1029); ctx.fillText("✓", 65, 1029);
ctx.fillStyle = "#9a9a9a"; ctx.fillStyle = "#9a9a9a";
ctx.font = "500 13px monospace"; ctx.font = "500 20px monospace";
ctx.fillText("DIGITALLY SIGNED ACCESS CREDENTIAL", 100, 1020); ctx.fillText("DIGITALLY SIGNED ACCESS CREDENTIAL", 100, 1020);
ctx.fillText("ISSUER / PASSPORT", 100, 1045); ctx.fillText("ISSUER / PASSPORT", 100, 1045);
ctx.textAlign = "right"; ctx.textAlign = "right";
ctx.fillStyle = "#ffffff";
ctx.fillRect(803, 1003, 51, 51);
ctx.fillStyle = "#050505";
ctx.font = "700 26px Arial, sans-serif";
ctx.fillText("P", 838, 1037);
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.fillStyle = "#616161"; ctx.fillStyle = "#616161";
ctx.font = "500 13px monospace"; ctx.font = "500 19px monospace";
ctx.fillText("TAP CARD TO VIEW VERIFICATION DETAILS", 58, 1205); ctx.fillText("TAP CARD TO VIEW VERIFICATION DETAILS", 58, 1205);
const texture = new THREE.CanvasTexture(canvas); const texture = new THREE.CanvasTexture(canvas);
@ -190,28 +163,27 @@ function createBackTexture(pass) {
ctx.strokeRect(30, 30, 852, 1228); ctx.strokeRect(30, 30, 852, 1228);
ctx.fillStyle = "#fff"; ctx.fillStyle = "#fff";
ctx.font = "700 27px Arial, sans-serif"; ctx.font = "700 32px Arial, sans-serif";
ctx.fillText("PASS PORT", 58, 82); ctx.fillText("PASS PORT", 58, 82);
ctx.textAlign = "right"; ctx.textAlign = "right";
ctx.fillStyle = "#777"; ctx.fillStyle = "#777";
ctx.font = "500 14px monospace"; ctx.font = "500 19px monospace";
ctx.fillText("CREDENTIAL / REVERSE", 854, 82); ctx.fillText("CREDENTIAL / REVERSE", 854, 82);
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.save(); ctx.fillStyle = "#8c8c8c";
ctx.globalAlpha = 0.09; ctx.font = "500 21px monospace";
ctx.strokeStyle = "#fff"; ctx.fillText("VERIFICATION", 58, 210);
ctx.lineWidth = 8; ctx.fillStyle = "#ffffff";
ctx.font = "800 190px Arial, sans-serif"; ctx.font = "650 48px Arial, sans-serif";
ctx.strokeText("SIGNED", 42, 315); ctx.fillText("Credential details", 55, 275);
ctx.restore();
ctx.fillStyle = "#8c8c8c"; ctx.fillStyle = "#8c8c8c";
ctx.font = "500 14px monospace"; ctx.font = "500 21px monospace";
ctx.fillText("VERIFICATION STATUS", 58, 385); ctx.fillText("CURRENT STATUS", 58, 390);
ctx.fillStyle = accent; ctx.fillStyle = accent;
ctx.font = "600 54px Arial, sans-serif"; ctx.font = "600 56px Arial, sans-serif";
ctx.fillText(pass.status, 55, 453); ctx.fillText(pass.status, 55, 450);
ctx.strokeStyle = "#ffffff2d"; ctx.strokeStyle = "#ffffff2d";
ctx.beginPath(); ctx.beginPath();
@ -229,11 +201,11 @@ function createBackTexture(pass) {
rows.forEach(([label, value], index) => { rows.forEach(([label, value], index) => {
const y = 570 + index * 92; const y = 570 + index * 92;
ctx.fillStyle = "#777"; ctx.fillStyle = "#777";
ctx.font = "500 14px monospace"; ctx.font = "500 20px monospace";
ctx.fillText(label, 58, y); ctx.fillText(label, 58, y);
ctx.textAlign = "right"; ctx.textAlign = "right";
ctx.fillStyle = "#fff"; ctx.fillStyle = "#fff";
ctx.font = "500 20px Arial, sans-serif"; ctx.font = "500 27px Arial, sans-serif";
ctx.fillText(value, 854, y); ctx.fillText(value, 854, y);
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.strokeStyle = "#ffffff17"; ctx.strokeStyle = "#ffffff17";
@ -244,15 +216,15 @@ function createBackTexture(pass) {
}); });
ctx.fillStyle = "#777"; ctx.fillStyle = "#777";
ctx.font = "500 14px monospace"; ctx.font = "500 20px monospace";
ctx.fillText("USAGE", 58, 1060); ctx.fillText("Credit", 58, 1060);
ctx.fillStyle = "#d5d5d5"; ctx.fillStyle = "#d5d5d5";
ctx.font = "500 19px Arial, sans-serif"; ctx.font = "500 23px Arial, sans-serif";
ctx.fillText("Present this credential at the authorized access point.", 58, 1100); ctx.fillText("https://github.com/hjopel/vercel-3d-card", 58, 1100);
ctx.fillText("Do not share the credential URL with another person.", 58, 1135); ctx.fillText("@imnyang", 58, 1135);
ctx.fillStyle = "#666"; ctx.fillStyle = "#666";
ctx.font = "500 13px monospace"; ctx.font = "500 18px monospace";
ctx.fillText("TAP CARD TO RETURN TO FRONT", 58, 1210); ctx.fillText("TAP CARD TO RETURN TO FRONT", 58, 1210);
const texture = new THREE.CanvasTexture(canvas); const texture = new THREE.CanvasTexture(canvas);
@ -435,11 +407,11 @@ function Band({ pass, flipped, onFlip, maxSpeed = 50, minSpeed = 10 }) {
color="#080808" color="#080808"
clearcoat={1} clearcoat={1}
clearcoatRoughness={0.15} clearcoatRoughness={0.15}
iridescence={1} iridescence={0.1}
iridescenceIOR={1} iridescenceIOR={1.1}
iridescenceThicknessRange={[0, 2400]} iridescenceThicknessRange={[0, 300]}
metalness={0.5} metalness={0.25}
roughness={0.3} roughness={0.38}
/> />
</mesh> </mesh>
<mesh position={[0, 0.523, 0.0062]}> <mesh position={[0, 0.523, 0.0062]}>
@ -448,11 +420,11 @@ function Band({ pass, flipped, onFlip, maxSpeed = 50, minSpeed = 10 }) {
map={badgeTexture} map={badgeTexture}
clearcoat={1} clearcoat={1}
clearcoatRoughness={0.12} clearcoatRoughness={0.12}
iridescence={0.55} iridescence={0.08}
iridescenceIOR={1.2} iridescenceIOR={1.2}
iridescenceThicknessRange={[0, 900]} iridescenceThicknessRange={[0, 240]}
metalness={0.2} metalness={0.1}
roughness={0.28} roughness={0.34}
/> />
</mesh> </mesh>
<mesh position={[0, 0.523, 0.0004]} rotation={[0, Math.PI, 0]}> <mesh position={[0, 0.523, 0.0004]} rotation={[0, Math.PI, 0]}>

View file

@ -1 +1 @@
*{box-sizing:border-box}html,body,#pass-root{width:100%;height:100%;margin:0;overflow:hidden;background:#000}body{min-width:320px}canvas{display:block;touch-action:none}.loading{display:grid;width:100%;height:100%;color:#fff;background:#000;font:12px monospace;letter-spacing:.12em;text-transform:uppercase;place-items:center}.flip-control{position:fixed;z-index:5;right:50%;bottom:28px;display:flex;align-items:center;gap:10px;padding:11px 16px;border:1px solid #ffffff2b;border-radius:999px;color:#d7d7d7;background:#0b0b0be6;box-shadow:0 10px 30px #0008;font:500 11px monospace;letter-spacing:.08em;text-transform:uppercase;cursor:pointer;transform:translate(50%);transition:color .16s ease,border-color .16s ease,background .16s ease}.flip-control:hover,.flip-control:focus-visible{border-color:#ffffff70;color:#fff;background:#151515f2;outline:none}.flip-control span{font:17px Arial,sans-serif} *{box-sizing:border-box}html,body,#pass-root{width:100%;height:100%;margin:0;overflow:hidden;background:#000}body{min-width:320px}canvas{display:block;touch-action:none}.loading{display:grid;width:100%;height:100%;color:#fff;background:#000;font:12px monospace;letter-spacing:.12em;text-transform:uppercase;place-items:center}.flip-control{position:fixed;z-index:5;right:50%;bottom:28px;display:flex;align-items:center;gap:10px;padding:11px 16px;border:1px solid #ffffff2b;border-radius:5px;color:#d7d7d7;background:#0b0b0be6;box-shadow:none;font:500 15px monospace;letter-spacing:.08em;text-transform:uppercase;cursor:pointer;transform:translate(50%);transition:color .16s ease,border-color .16s ease,background .16s ease}.flip-control:hover,.flip-control:focus-visible{border-color:#ffffff70;color:#fff;background:#151515f2;outline:none}.flip-control span{font:17px Arial,sans-serif}

File diff suppressed because one or more lines are too long