Created
May 3, 2026 07:59
-
-
Save carddass2018-svg/bd267e488976e503146862565a662ab5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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