Skip to content

Instantly share code, notes, and snippets.

@carddass2018-svg
Created May 3, 2026 07:59
Show Gist options
  • Select an option

  • Save carddass2018-svg/bd267e488976e503146862565a662ab5 to your computer and use it in GitHub Desktop.

Select an option

Save carddass2018-svg/bd267e488976e503146862565a662ab5 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>龍珠閃卡 - 頂級燙金壓紋版</title>
<style>
body { background: #000; margin: 0; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; font-family: sans-serif; transition: transform 0.1s ease-out; transform-style: preserve-3d; }
#auth-overlay { position: fixed; inset: 0; background: #000; z-index: 9999; display: flex; flex-direction: column; align-items: center; justify-content: center; }
#auth-btn { background: #f1c40f; border: none; padding: 20px 50px; border-radius: 12px; font-size: 20px; font-weight: bold; cursor: pointer; }
.panel { position: fixed; top: 0; left: 0; width: 100%; background: #111; padding: 15px; border-bottom: 2px solid #333; z-index: 999; color: #fff; transition: 0.3s; max-height: 80vh; overflow-y: auto; }
.panel.hidden { transform: translateY(-110%); }
#set-btn { position: fixed; top: 15px; left: 15px; z-index: 1000; background: rgba(0,0,0,0.8); color: #fff; border: 1px solid #f1c40f; border-radius: 50%; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.row { margin-bottom: 8px; font-size: 11px; }
.status { font-size: 10px; margin-left: 5px; color: #555; }
.ok { color: #0f0 !important; }
#perspective-container { perspective: 1500px; padding: 50px; }
#card {
width: 85vw; max-width: 320px; aspect-ratio: 1/1.46; position: relative;
border-radius: 20px; background: #000;
box-shadow: 0 0 50px rgba(0,0,0,0.9);
overflow: hidden; transform-style: preserve-3d; transform: translateZ(0);
-webkit-mask-image: -webkit-radial-gradient(white, black);
}
.layer { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; object-fit: fill; border-radius: inherit; }
#base-img { z-index: 1; }
#foil-img { z-index: 5; mix-blend-mode: hard-light; opacity: 0.8; display: none; }
/* 🚀 強化版燙金層 */
#gold-sign-container {
position: absolute; inset: 0; z-index: 15;
display: none;
/* 修正位置與縮放 */
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
/* 壓印立體感濾鏡 */
filter: drop-shadow(1px 1px 0px rgba(0,0,0,0.7)) drop-shadow(-0.5px -0.5px 0.5px rgba(255,255,255,0.3));
}
#gold-reflect {
position: absolute; inset: -150%;
background: linear-gradient(135deg,
#443300 0%, #b8860b 20%, #ffdf00 45%,
#ffffff 50%, #ffdf00 55%, #b8860b 80%, #443300 100%);
background-size: 250% 250%;
/* 🚀 注入 SVG 噪點濾鏡 */
filter: url(#gold-noise);
}
#shine-layer {
z-index: 20; position: absolute; inset: -100%;
background: radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0.2) 30%, transparent 60%);
background-size: 150% 150%; background-repeat: no-repeat;
mix-blend-mode: overlay; filter: blur(35px); pointer-events: none; opacity: 0;
}
canvas { display: none; }
</style>
</head>
<body>
<!-- 🚀 SVG 噪點濾鏡定義 -->
<svg style="display: none;">
<filter id="gold-noise">
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch" />
<feComposite operator="in" in2="SourceGraphic" />
<feBlend mode="overlay" in2="SourceGraphic" />
</filter>
</svg>
<div id="auth-overlay"><button id="auth-btn" onclick="init()">啟動壓紋燙金版</button></div>
<div id="set-btn" onclick="document.getElementById('p').classList.toggle('hidden')">⚙️</div>
<div class="panel" id="p">
<div class="row">1. 底圖: <span id="s-b" class="status">未選</span><input type="file" accept="image/*" onchange="ld(this,'b')"></div>
<div class="row">2. 遮罩: <span id="s-m" class="status">未選</span><input type="file" accept="image/*" onchange="ld(this,'m')"></div>
<div class="row">3. 貼圖: <span id="s-f" class="status">未選</span><input type="file" accept="image/*" onchange="ld(this,'f')"></div>
<div class="row">4. 簽名 (透明PNG): <span id="s-g" class="status">選填</span><input type="file" accept="image/*" onchange="ld(this,'g')"></div>
<button id="gen-btn" onclick="render()" style="width:100%; padding:12px; background:#444; color:#888; border:none; border-radius:8px; font-weight:bold; cursor:not-allowed;">生成閃卡</button>
</div>
<div id="perspective-container">
<div id="card">
<img id="base-img" class="layer">
<img id="foil-img" class="layer">
<div id="gold-sign-container" class="layer">
<div id="gold-reflect"></div>
</div>
<div id="shine-layer"></div>
</div>
</div>
<canvas id="cvs"></canvas>
<script>
let cache = { b:null, m:null, f:null, g:null };
const foilE = document.getElementById('foil-img'),
baseE = document.getElementById('base-img'),
goldContainer = document.getElementById('gold-sign-container'),
goldReflect = document.getElementById('gold-reflect'),
shineE = document.getElementById('shine-layer'),
cardE = document.getElementById('card'),
cvs = document.getElementById('cvs');
function ld(input, type) {
if (!input.files || !input.files[0]) return;
const status = document.getElementById('s-' + type);
status.innerText = "載入中...";
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const max = 900;
let scale = Math.min(1, max / Math.max(img.width, img.height));
const tmpC = document.createElement('canvas');
tmpC.width = img.width * scale; tmpC.height = img.height * scale;
tmpC.getContext('2d').drawImage(img, 0, 0, tmpC.width, tmpC.height);
const smallImg = new Image();
smallImg.onload = () => {
cache[type] = smallImg;
if (type === 'b') baseE.src = smallImg.src;
if (type === 'g') {
goldContainer.style.webkitMaskImage = `url(${smallImg.src})`;
goldContainer.style.maskImage = `url(${smallImg.src})`;
}
status.innerText = "OK ✓"; status.className = "status ok";
if (cache.b && cache.m && cache.f) {
const btn = document.getElementById('gen-btn');
btn.style.background = "#f1c40f"; btn.style.color = "#000"; btn.style.cursor = "pointer";
}
};
smallImg.src = tmpC.toDataURL('image/png');
};
img.src = e.target.result;
};
reader.readAsDataURL(input.files[0]);
}
function render() {
if (!cache.b || !cache.m || !cache.f) return;
const w = cache.m.width, h = cache.m.height;
cvs.width = w; cvs.height = h;
const ctx = cvs.getContext('2d');
ctx.drawImage(cache.m, 0, 0, w, h);
const mData = ctx.getImageData(0,0,w,h).data;
ctx.clearRect(0,0,w,h);
ctx.drawImage(cache.f, 0, 0, w, h);
const fImgData = ctx.getImageData(0,0,w,h);
const fData = fImgData.data;
for (let i = 0; i < fData.length; i += 4) {
const br = (mData[i] + mData[i+1] + mData[i+2]) / 3;
if (br < 80) fData[i+3] = 0;
else if (br < 120) fData[i+3] = ((br - 80) / 40) * 255;
}
ctx.putImageData(fImgData, 0, 0);
foilE.src = cvs.toDataURL();
foilE.style.display = 'block';
if (cache.g) goldContainer.style.display = 'block';
document.getElementById('p').classList.add('hidden');
}
function init() {
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission().then(res => { if(res === 'granted') start(); });
} else { start(); }
}
function start() {
document.getElementById('auth-overlay').style.display = 'none';
let lastG = 0, lastB = 0;
const LERP = 0.3, OFFSET_B = 60;
window.addEventListener('deviceorientation', e => {
window.requestAnimationFrame(() => {
let curG = e.gamma || 0, curB = (e.beta || OFFSET_B) - OFFSET_B;
lastG += (curG - lastG) * LERP; lastB += (curB - lastB) * LERP;
cardE.style.transform = `rotateX(${-lastB * 0.8}deg) rotateY(${lastG * 0.8}deg) translateZ(0)`;
const hue = (lastG + lastB) * 30;
foilE.style.filter = `hue-rotate(${hue}deg) saturate(0.5) brightness(1.2)`;
if (cache.g) {
// ✨ 燙金反射位移:倍率微調讓反光更靈活
const goldX = 50 + (lastG * 4);
const goldY = 50 + (lastB * 4);
goldReflect.style.backgroundPosition = `${goldX}% ${goldY}%`;
goldContainer.style.transform = `translate(${lastG * 0.3}px, ${lastB * 0.3}px)`;
}
const posX = 50 + (lastG * 12), posY = 50 + (lastB * 12);
shineE.style.backgroundPosition = `${posX}% ${posY}%`;
shineE.style.transform = `translate(${lastG * 10}px, ${lastB * 10}px)`;
shineE.style.opacity = 0.35 + (Math.abs(lastG) + Math.abs(lastB)) / 50;
});
});
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment