inti
This commit is contained in:
1 parent
a49b006205
commit
0ca23e82a0
3 files changed
+415
No files matched your search
@@ -0,0 +1,86 @@
|
||||
const https = require("https");
|
||||
|
||||
// ─── Font & Color Data ───────────────────────────────────────────
|
||||
const FONTS = {
|
||||
elegant: { name:"Elegant Cursive", family:"Dancing Script", weight:"700", style:"normal", size:62, skewX:0, ls:1, sw:0, yo:0, url:"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700&display=swap" },
|
||||
classic: { name:"Classic Flourish", family:"Great Vibes", weight:"normal", style:"normal", size:62, skewX:0, ls:2, sw:0, yo:0, url:"https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap" },
|
||||
modern: { name:"French Elegance", family:"Parisienne", weight:"normal", style:"normal", size:62, skewX:0, ls:2, sw:0, yo:0, url:"https://fonts.googleapis.com/css2?family=Parisienne&display=swap" },
|
||||
bold: { name:"Smooth Flow", family:"Sacramento", weight:"normal", style:"normal", size:68, skewX:0, ls:1, sw:0, yo:0, url:"https://fonts.googleapis.com/css2?family=Sacramento&display=swap" },
|
||||
refined: { name:"Formal Script", family:"Allura", weight:"normal", style:"normal", size:64, skewX:0, ls:2, sw:0, yo:0, url:"https://fonts.googleapis.com/css2?family=Allura&display=swap" },
|
||||
light: { name:"Brush Stroke", family:"Alex Brush", weight:"normal", style:"normal", size:64, skewX:0, ls:1, sw:0, yo:0, url:"https://fonts.googleapis.com/css2?family=Alex+Brush&display=swap" },
|
||||
serif: { name:"Victorian", family:"Mrs Saint Delafield", weight:"normal", style:"normal", size:64, skewX:0, ls:2, sw:0, yo:0, url:"https://fonts.googleapis.com/css2?family=Mrs+Saint+Delafield&display=swap" },
|
||||
baskerville: { name:"Handwritten", family:"Zeyada", weight:"normal", style:"normal", size:64, skewX:0, ls:1, sw:0, yo:0, url:"https://fonts.googleapis.com/css2?family=Zeyada&display=swap" },
|
||||
};
|
||||
const COLORS = { navy:"#1a1a4e", black:"#111111", blue:"#1e3a8a", gold:"#8B6914", crimson:"#8B0000", forest:"#1a4a2e" };
|
||||
|
||||
// ─── Font Cache (survives across warm invocations) ───────────────
|
||||
const cache = {};
|
||||
|
||||
function get(u){return new Promise((ok,no)=>{const go=h=>{https.get(h,{headers:{"User-Agent":"Mozilla/5.0"}},r=>{if(r.statusCode>=300&&r.statusCode<400&&r.headers.location){go(r.headers.location);return}const c=[];r.on("data",d=>c.push(d));r.on("end",()=>ok({ok:r.statusCode===200,buf:Buffer.concat(c)}))}).on("error",no)};go(u)})}
|
||||
|
||||
async function loadFont(k){
|
||||
const f=FONTS[k]; if(!f||cache[k]) return;
|
||||
cache[k]={css:`@import url('${f.url.replace(/&/g,"&")}');`,m:"fb"};
|
||||
try{
|
||||
const css=await get(f.url); if(!css.ok) throw 0;
|
||||
const m=css.buf.toString().match(/url\((https:\/\/fonts\.gstatic\.com\/[^\)]+\.woff2)\)/);
|
||||
if(!m) throw 0;
|
||||
const w=await get(m[1]); if(!w.ok) throw 0;
|
||||
const ur=css.buf.toString().match(/unicode-range:\s*([^;}\n]+)/);
|
||||
cache[k]={css:`@font-face{font-family:'${f.family}';font-style:${f.style};font-weight:${f.weight};src:url(data:font/woff2;base64,${w.buf.toString("base64")}) format('woff2');${ur?"unicode-range:"+ur[1].trim()+";":""}}`,m:"ok"};
|
||||
}catch(e){/* keep fallback */}
|
||||
}
|
||||
|
||||
function fc(k){return(cache[k]&&cache[k].css)||`@import url('${(FONTS[k]||FONTS.elegant).url.replace(/&/g,"&")}');`}
|
||||
|
||||
// ─── SVG Helpers ─────────────────────────────────────────────────
|
||||
function esc(s){return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}
|
||||
function bi(c){const bg=c||"#faf8f3",t=bg==="transparent",r=t?250:(parseInt(bg.slice(1,3),16)||250),g=t?248:(parseInt(bg.slice(3,5),16)||248),b=t?243:(parseInt(bg.slice(5,7),16)||243),l=(r*299+g*587+b*114)/1000;return{bg,t,l,gr:l>128?"rgba(0,0,0,.035)":"rgba(255,255,255,.06)",pn:l>128?"#3a3a3a":"#aaa"}}
|
||||
function dots(W,H,f){let d="";for(let i=0;i<50;i++)d+=`<circle cx="${(i*137+29)%W}" cy="${(i*89+17)%H}" r=".7" fill="${f}"/>`;return d}
|
||||
function fl(f,t,W,H){const e=f.size*.48*t.length,s=W/2-e/2,y=H/2+f.size*.38;let d=`M ${s} ${y}`;for(let x=0;x<=e;x+=4){const p=x/e;d+=` L ${(s+x).toFixed(1)} ${(y+Math.sin(p*Math.PI*2.5)*6*(1-p*.7)).toFixed(1)}`}return{d,l:(e*1.05).toFixed(0)}}
|
||||
|
||||
function buildSVG(text,font,fk,color,speed,bgC,animated){
|
||||
const W=600,H=200,b=bi(bgC),dur=(2.4/speed).toFixed(2),fDl=(dur*.78).toFixed(2),fDr=(dur*.28).toFixed(2);
|
||||
const sk=font.skewX?`skewX(${font.skewX})`:"",f=fl(font,text,W,H),dt=b.t?"":dots(W,H,b.gr);
|
||||
const txtEl=`<text x="${W/2}" y="${H/2+font.size*.08+font.yo}" font-family="'${font.family}',cursive,serif" font-size="${font.size}" font-weight="${font.weight}" font-style="${font.style}" fill="${color}" text-anchor="middle" dominant-baseline="middle" letter-spacing="${font.ls}" transform="translate(0,0) ${sk}" transform-origin="${W/2} ${H/2}" ${font.sw?`stroke="${color}" stroke-width="${font.sw}"`:""}>${esc(text)}</text>`;
|
||||
if(!animated) return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">
|
||||
<style>${fc(fk)}</style>
|
||||
${b.t?"":`<rect width="${W}" height="${H}" rx="4" fill="${b.bg}"/>`}${dt}
|
||||
${txtEl}
|
||||
<path d="${f.d}" fill="none" stroke="${color}" stroke-width="1.2" stroke-linecap="round" opacity=".45"/>
|
||||
</svg>`;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">
|
||||
<style>${fc(fk)}</style>
|
||||
<defs><clipPath id="r"><rect x="0" y="0" width="0" height="${H}"><animate attributeName="width" from="0" to="${W}" dur="${dur}s" fill="freeze" calcMode="spline" keySplines="0.25 0.1 0.25 1" keyTimes="0;1" repeatCount="indefinite"/></rect></clipPath></defs>
|
||||
${b.t?"":`<rect width="${W}" height="${H}" rx="4" fill="${b.bg}"/>`}${dt}
|
||||
<g clip-path="url(#r)">${txtEl}</g>
|
||||
<g opacity="1"><animateTransform attributeName="transform" type="translate" from="0 0" to="${W} 0" dur="${dur}s" fill="freeze" calcMode="spline" keySplines="0.25 0.1 0.25 1" keyTimes="0;1" repeatCount="indefinite"/><g transform="translate(0,${H/2-8}) rotate(22)"><rect x="-1.5" y="-28" width="3" height="26" rx="1" fill="${b.pn}"/><polygon points="0,1 -1.8,-5 1.8,-5" fill="${color}"/></g><animate attributeName="opacity" values="1;1;0" keyTimes="0;0.92;1" dur="${dur}s" fill="freeze" repeatCount="indefinite"/></g>
|
||||
<path d="${f.d}" fill="none" stroke="${color}" stroke-width="1.2" stroke-linecap="round" opacity=".45" stroke-dasharray="${f.l}" stroke-dashoffset="${f.l}"><animate attributeName="stroke-dashoffset" from="${f.l}" to="0" begin="${fDl}s" dur="${fDr}s" fill="freeze" calcMode="spline" keySplines="0.4 0 0.2 1" keyTimes="0;1" repeatCount="indefinite"/></path>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
// ─── Vercel Handler ──────────────────────────────────────────────
|
||||
module.exports = async (req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
if (req.method === "OPTIONS") { res.status(204).end(); return; }
|
||||
|
||||
const q = req.query || {};
|
||||
if (!q.signature) { res.redirect("/"); return; }
|
||||
|
||||
const text = String(q.signature).slice(0,50);
|
||||
const fk = (q.font||"elegant").toLowerCase();
|
||||
const ck = (q.color||"navy").toLowerCase();
|
||||
const spd = Math.max(.25,Math.min(5,parseFloat(q.speed)||1));
|
||||
const fmt = (q.format||"svg").toLowerCase();
|
||||
const bgR = q.bg||"";
|
||||
const bgC = bgR==="transparent"?"transparent":/^#?[0-9a-fA-F]{3,6}$/.test(bgR)?(bgR[0]==="#"?bgR:"#"+bgR):"#faf8f3";
|
||||
|
||||
const font = FONTS[fk]||FONTS.elegant;
|
||||
const color= COLORS[ck]||COLORS.navy;
|
||||
if(!cache[fk]) await loadFont(fk);
|
||||
|
||||
const svg = buildSVG(text,font,fk,color,spd,bgC,fmt!=="static");
|
||||
res.setHeader("Content-Type","image/svg+xml;charset=utf-8");
|
||||
res.setHeader("Cache-Control","public,s-maxage=3600,stale-while-revalidate=86400");
|
||||
res.status(200).send(svg);
|
||||
};
|
||||
@@ -0,0 +1,314 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Signature Atelier</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=Dancing+Script:wght@700&family=Great+Vibes&family=Parisienne&family=Sacramento&family=Allura&family=Alex+Brush&family=Mrs+Saint+Delafield&family=Zeyada&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--gold:#c8a24e;--bg:#1a1a2e;--card:rgba(255,255,255,.04);--bdr:rgba(200,162,78,.2);--txt:#e8e4df;--dim:#8a8578;--dark:#4a4540}
|
||||
body{font-family:Georgia,'Times New Roman',serif;background:linear-gradient(145deg,#1a1a2e,#16213e 40%,#0f3460);color:var(--txt);min-height:100vh}
|
||||
.wrap{max-width:880px;margin:0 auto;padding:40px 24px;position:relative;z-index:1}
|
||||
.bg-deco{position:fixed;inset:0;background:radial-gradient(ellipse at 20% 50%,rgba(255,215,0,.03),transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(255,255,255,.02),transparent 40%);pointer-events:none}
|
||||
.lb{display:block;font-size:11px;letter-spacing:3px;text-transform:uppercase;color:var(--gold);margin-bottom:14px}
|
||||
.lb-sm{font-size:10px;margin-bottom:8px;color:var(--dim)}
|
||||
h1{font-size:42px;font-weight:300;margin:0 0 8px;background:linear-gradient(135deg,#f0e6d3,#c8a24e,#f0e6d3);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:2px;text-align:center}
|
||||
.sub{color:var(--dim);font-size:14px;text-align:center;font-style:italic;margin-bottom:48px}
|
||||
.section{margin-bottom:32px}
|
||||
input[type=text]{width:100%;padding:16px 20px;background:rgba(255,255,255,.05);border:1px solid var(--bdr);border-radius:8px;color:#f0e6d3;font-size:22px;font-family:Georgia,serif;outline:none;box-sizing:border-box;transition:border .3s}
|
||||
input[type=text]:focus{border-color:rgba(200,162,78,.6)}
|
||||
.font-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
|
||||
.font-btn{padding:14px 12px;text-align:center;cursor:pointer;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:8px;transition:all .25s}
|
||||
.font-btn.on{background:rgba(200,162,78,.15);border-color:rgba(200,162,78,.5)}
|
||||
.font-btn .preview{font-size:22px;color:#a09888;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.font-btn.on .preview{color:#f0e6d3}
|
||||
.font-btn .tag{font-size:9px;letter-spacing:2px;text-transform:uppercase;color:#6a6560}
|
||||
.font-btn.on .tag{color:var(--gold)}
|
||||
.font-btn .api{font-size:8px;letter-spacing:1px;color:var(--dark);margin-top:3px}
|
||||
.color-row{display:flex;gap:12px;flex-wrap:wrap}
|
||||
.color-btn{display:flex;align-items:center;gap:8px;padding:10px 16px;cursor:pointer;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:6px;transition:all .25s}
|
||||
.color-btn.on{background:rgba(200,162,78,.12);border-color:rgba(200,162,78,.4)}
|
||||
.color-dot{width:18px;height:18px;border-radius:50%;border:2px solid rgba(255,255,255,.15)}
|
||||
.color-btn.on .color-dot{box-shadow:0 0 8px rgba(0,0,0,.3)}
|
||||
.color-name{font-size:12px;color:#7a7570;letter-spacing:1px}
|
||||
.color-btn.on .color-name{color:#e8e4df}
|
||||
.bg-section{display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap}
|
||||
.bg-hex{display:flex;align-items:center;gap:10px}
|
||||
.bg-picker{position:relative;width:40px;height:40px}
|
||||
.bg-picker input[type=color]{position:absolute;inset:0;width:100%;height:100%;border:none;padding:0;cursor:pointer;border-radius:8px;background:none}
|
||||
.bg-picker .ring{position:absolute;inset:0;border-radius:8px;pointer-events:none;border:2px solid rgba(200,162,78,.3)}
|
||||
.bg-hex-input{width:80px;padding:8px 10px;background:rgba(255,255,255,.06);border:1px solid var(--bdr);border-radius:6px;color:#f0e6d3;font-size:14px;font-family:'Courier New',monospace;outline:none;letter-spacing:1px}
|
||||
.bg-swatches{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.bg-sw{width:28px;height:28px;border-radius:6px;cursor:pointer;padding:0;border:1px solid rgba(255,255,255,.15);transition:all .2s}
|
||||
.bg-sw.on{border:2px solid var(--gold);box-shadow:0 0 8px rgba(200,162,78,.4)}
|
||||
.bg-sw.checker{background:linear-gradient(45deg,#ccc 25%,transparent 25%,transparent 75%,#ccc 75%),linear-gradient(45deg,#ccc 25%,transparent 25%,transparent 75%,#ccc 75%);background-size:8px 8px;background-position:0 0,4px 4px}
|
||||
.speed-row{display:flex;align-items:center;gap:16px}
|
||||
.speed-btns{display:flex;gap:8px}
|
||||
.speed-btn{padding:10px 14px;cursor:pointer;min-width:54px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:6px;display:flex;flex-direction:column;align-items:center;gap:3px;transition:all .25s}
|
||||
.speed-btn.on{background:rgba(200,162,78,.18);border-color:rgba(200,162,78,.5)}
|
||||
.speed-btn .val{font-size:15px;font-weight:600;color:#8a8578}
|
||||
.speed-btn.on .val{color:#f0e6d3}
|
||||
.speed-btn .desc{font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:#5a5550}
|
||||
.speed-btn.on .desc{color:var(--gold)}
|
||||
.speed-bar{flex:1;height:6px;background:rgba(255,255,255,.06);border-radius:3px;overflow:hidden}
|
||||
.speed-fill{height:100%;background:linear-gradient(90deg,#c8a24e,#e8c84e);border-radius:3px;transition:width .35s cubic-bezier(.4,0,.2,1)}
|
||||
.preview-box{border-radius:12px;padding:6px;box-shadow:0 8px 32px rgba(0,0,0,.3);border:1px solid var(--bdr);overflow:hidden;transition:background .3s}
|
||||
.preview-box svg{width:100%;height:auto;display:block;border-radius:8px}
|
||||
.replay-btn{padding:6px 14px;background:rgba(200,162,78,.1);border:1px solid rgba(200,162,78,.3);border-radius:4px;color:var(--gold);font-size:11px;cursor:pointer;letter-spacing:2px;text-transform:uppercase}
|
||||
.tabs{display:flex;gap:0}
|
||||
.tab-btn{padding:13px 28px;cursor:pointer;background:transparent;border:1px solid var(--bdr);border-bottom-color:var(--bdr);border-radius:8px 8px 0 0;color:#6a6560;font-size:12px;letter-spacing:2px;text-transform:uppercase;font-family:Georgia,serif;transition:all .2s}
|
||||
.tab-btn.on{background:var(--card);border-bottom-color:transparent;color:var(--gold);font-weight:700}
|
||||
.tab-fill{flex:1;border-bottom:1px solid var(--bdr)}
|
||||
.tab-body{background:var(--card);border-radius:0 12px 12px 12px;padding:28px;border:1px solid var(--bdr);border-top:none}
|
||||
.gold-btn{padding:16px 48px;background:linear-gradient(135deg,#c8a24e,#a07830);border:none;border-radius:8px;color:#1a1a2e;font-size:13px;font-weight:700;letter-spacing:3px;text-transform:uppercase;cursor:pointer;box-shadow:0 4px 20px rgba(200,162,78,.3);font-family:Georgia,serif;transition:all .3s}
|
||||
.gold-btn:disabled{background:rgba(200,162,78,.2);color:#8a7a60;box-shadow:none;cursor:default}
|
||||
.outline-btn{padding:10px 24px;background:transparent;border:1px solid rgba(200,162,78,.3);border-radius:6px;color:var(--gold);font-size:11px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;font-family:Georgia,serif;transition:all .25s}
|
||||
.outline-btn.ok{color:#6bcc6b;border-color:rgba(80,180,80,.4)}
|
||||
.url-row{margin-bottom:16px}
|
||||
.url-box{display:flex;gap:8px;background:rgba(0,0,0,.2);border:1px solid rgba(200,162,78,.12);border-radius:8px;padding:4px;align-items:center}
|
||||
.url-code{flex:1;padding:10px 12px;font-size:12px;color:#d4c9b0;font-family:'Courier New',monospace;word-break:break-all;line-height:1.5}
|
||||
.copy-btn{padding:8px 16px;flex-shrink:0;background:rgba(200,162,78,.12);border:1px solid rgba(200,162,78,.3);border-radius:6px;color:var(--gold);font-size:11px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;font-family:Georgia,serif;font-weight:600;transition:all .25s;white-space:nowrap}
|
||||
.copy-btn.ok{background:rgba(80,180,80,.15);border-color:rgba(80,180,80,.4);color:#6bcc6b}
|
||||
.srv-input{flex:1;padding:10px 14px;background:rgba(255,255,255,.06);border:1px solid var(--bdr);border-radius:6px;color:#f0e6d3;font-size:14px;font-family:'Courier New',monospace;outline:none;box-sizing:border-box}
|
||||
.srv-reset{padding:10px 16px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:6px;color:var(--dim);font-size:11px;cursor:pointer;letter-spacing:1px;white-space:nowrap}
|
||||
.footer{text-align:center;margin-top:48px;padding-top:24px;border-top:1px solid rgba(255,255,255,.05)}
|
||||
.footer p{color:var(--dark);font-size:11px;letter-spacing:2px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-deco"></div>
|
||||
<div class="wrap">
|
||||
<div style="text-align:center;font-size:11px;letter-spacing:6px;color:var(--gold);margin-bottom:12px">✦ Signature Atelier ✦</div>
|
||||
<h1>Signature Designer</h1>
|
||||
<p class="sub">Craft your personal mark · SVG vector animation</p>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="section"><div class="lb">Your Name</div>
|
||||
<input type="text" id="nameInput" value="John Smith" oninput="update()">
|
||||
</div>
|
||||
|
||||
<!-- Fonts -->
|
||||
<div class="section"><div class="lb">Script Style</div><div class="font-grid" id="fontGrid"></div></div>
|
||||
|
||||
<!-- Ink Color -->
|
||||
<div class="section"><div class="lb">Ink Color</div><div class="color-row" id="colorRow"></div></div>
|
||||
|
||||
<!-- Background -->
|
||||
<div class="section"><div class="lb">Background Color</div>
|
||||
<div class="bg-section">
|
||||
<div class="bg-hex">
|
||||
<div class="bg-picker"><input type="color" id="bgPicker" value="#faf8f3" oninput="setBg(this.value)"><div class="ring"></div></div>
|
||||
<div style="display:flex;align-items:center;gap:2px">
|
||||
<span style="color:#6a6560;font-size:14px;font-family:monospace">#</span>
|
||||
<input type="text" id="bgHex" class="bg-hex-input" value="faf8f3" maxlength="6" oninput="onBgHex(this)" onblur="onBgHexBlur(this)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-swatches" id="bgSwatches"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Speed -->
|
||||
<div class="section"><div class="lb">Animation Speed</div>
|
||||
<div class="speed-row"><div class="speed-btns" id="speedBtns"></div><div class="speed-bar"><div class="speed-fill" id="speedFill"></div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<div class="lb" style="margin-bottom:0">Live Preview <span style="font-size:9px;color:#5a5550;letter-spacing:1px;margin-left:8px;font-weight:400">SVG VECTOR</span></div>
|
||||
<button class="replay-btn" onclick="update()">↻ Replay</button>
|
||||
</div>
|
||||
<div class="preview-box" id="previewBox"><div id="previewSvg" style="width:100%;line-height:0;border-radius:8px;overflow:hidden"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="section">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn on" data-tab="local" onclick="switchTab('local')">⬇ Download SVG</button>
|
||||
<button class="tab-btn" data-tab="api" onclick="switchTab('api')">🔗 API & URLs</button>
|
||||
<div class="tab-fill"></div>
|
||||
</div>
|
||||
<div class="tab-body">
|
||||
<!-- Local tab -->
|
||||
<div id="tab-local">
|
||||
<div style="text-align:center">
|
||||
<p style="color:var(--dim);font-size:13px;margin-bottom:8px">Download an animated SVG — scalable, lightweight, and crisp at any size.</p>
|
||||
<div style="display:inline-flex;gap:12px;align-items:center;background:rgba(200,162,78,.06);border:1px solid rgba(200,162,78,.15);border-radius:8px;padding:10px 20px;margin-bottom:24px">
|
||||
<span style="font-size:11px;color:var(--dim)">Size ≈</span>
|
||||
<span id="svgSize" style="font-size:14px;color:var(--gold);font-weight:600">—</span>
|
||||
</div>
|
||||
<div><button class="gold-btn" onclick="downloadSvg()">↓ Download Animated SVG</button></div>
|
||||
<div style="margin-top:24px"><button class="outline-btn" id="copySrcBtn" onclick="copySvgSrc()">Copy SVG Source Code</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- API tab -->
|
||||
<div id="tab-api" style="display:none">
|
||||
<div style="margin-bottom:24px"><div class="lb lb-sm">Server Address</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input type="text" id="apiBase" class="srv-input" value="" oninput="updateUrls()">
|
||||
<button class="srv-reset" onclick="document.getElementById('apiBase').value=defaultBase();updateUrls()">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="urlRows"></div>
|
||||
<div style="border-top:1px solid rgba(200,162,78,.1);margin:8px 0 24px"></div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<div class="lb lb-sm" style="margin-bottom:0">Server Preview</div>
|
||||
<button class="replay-btn" style="font-size:10px;padding:5px 12px" onclick="refreshPreview()">↻ Refresh</button>
|
||||
</div>
|
||||
<div style="background:#faf8f3;border-radius:8px;padding:8px;min-height:80px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px rgba(0,0,0,.2);overflow:hidden">
|
||||
<img id="srvImg" style="max-width:100%;border-radius:4px;display:block" alt="Server preview">
|
||||
<div id="srvErr" style="display:none;text-align:center;padding:24px 20px;color:#999">
|
||||
<div style="font-size:28px;margin-bottom:8px;opacity:.4">⚠</div>
|
||||
<div style="font-size:13px;font-style:italic;margin-bottom:10px">Cannot connect to server</div>
|
||||
<div style="font-size:11px;color:#aaa;margin-top:10px">Deploy to Vercel or start local server</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:var(--dark);font-size:11px;margin-top:10px;font-style:italic;line-height:1.5">Loads directly from your API. URLs work anywhere.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer"><p>CRAFTED WITH PRECISION · EVERY STROKE MATTERS</p></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ─── Data ────────────────────────────────────────────────────────
|
||||
const FONTS=[
|
||||
{id:"dancing",family:"'Dancing Script', cursive",style:"Elegant Cursive",apiKey:"elegant",url:"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700&display=swap"},
|
||||
{id:"greatvibes",family:"'Great Vibes', cursive",style:"Classic Flourish",apiKey:"classic",url:"https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap"},
|
||||
{id:"parisienne",family:"'Parisienne', cursive",style:"French Elegance",apiKey:"modern",url:"https://fonts.googleapis.com/css2?family=Parisienne&display=swap"},
|
||||
{id:"sacramento",family:"'Sacramento', cursive",style:"Smooth Flow",apiKey:"bold",url:"https://fonts.googleapis.com/css2?family=Sacramento&display=swap"},
|
||||
{id:"allura",family:"'Allura', cursive",style:"Formal Script",apiKey:"refined",url:"https://fonts.googleapis.com/css2?family=Allura&display=swap"},
|
||||
{id:"alexbrush",family:"'Alex Brush', cursive",style:"Brush Stroke",apiKey:"light",url:"https://fonts.googleapis.com/css2?family=Alex+Brush&display=swap"},
|
||||
{id:"mrssaint",family:"'Mrs Saint Delafield', cursive",style:"Victorian",apiKey:"serif",url:"https://fonts.googleapis.com/css2?family=Mrs+Saint+Delafield&display=swap"},
|
||||
{id:"zeyada",family:"'Zeyada', cursive",style:"Handwritten",apiKey:"baskerville",url:"https://fonts.googleapis.com/css2?family=Zeyada&display=swap"},
|
||||
];
|
||||
const COLORS=[
|
||||
{id:"navy",name:"Navy Ink",color:"#1a1a4e"},{id:"black",name:"Midnight",color:"#111111"},
|
||||
{id:"blue",name:"Royal Blue",color:"#1e3a8a"},{id:"gold",name:"Gold",color:"#8B6914"},
|
||||
{id:"crimson",name:"Crimson",color:"#8B0000"},{id:"forest",name:"Forest",color:"#1a4a2e"},
|
||||
];
|
||||
const SPEEDS=[{l:"0.5×",v:.5,d:"Slow"},{l:"1×",v:1,d:"Normal"},{l:"1.5×",v:1.5,d:"Fast"},{l:"2×",v:2,d:"Faster"},{l:"3×",v:3,d:"Rapid"}];
|
||||
const BG_SWATCHES=[
|
||||
{c:"#faf8f3",t:"Parchment"},{c:"#ffffff",t:"White"},{c:"#f5f0e8",t:"Cream"},{c:"#e8e0d0",t:"Warm Sand"},
|
||||
{c:"#f0f4f8",t:"Ice Blue"},{c:"#f8f0f4",t:"Blush"},{c:"#1a1a2e",t:"Dark Navy"},{c:"#111111",t:"Black"},
|
||||
{c:"#2d2d2d",t:"Charcoal"},{c:"#0d1b2a",t:"Deep Ocean"},{c:"transparent",t:"None"},
|
||||
];
|
||||
|
||||
let state={font:FONTS[0],ink:COLORS[0],bg:"#faf8f3",speed:1,svgStr:""};
|
||||
|
||||
function defaultBase(){return location.origin+"/api"}
|
||||
|
||||
// ─── SVG Builder ─────────────────────────────────────────────────
|
||||
function escXml(s){return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}
|
||||
function buildSVG(text,fontFamily,inkColor,bgColor,speed,fontUrl){
|
||||
const W=600,H=180,bg=bgColor||"#faf8f3",isT=bg==="transparent";
|
||||
const dur=(2.4/speed).toFixed(2),fDel=(dur*.78).toFixed(2),fDr=(dur*.28).toFixed(2);
|
||||
const estW=text.length*28,fsx=W/2-estW/2,fBy=H/2+36;
|
||||
let fD=`M ${fsx} ${fBy}`;
|
||||
for(let x=0;x<=estW;x+=4){const t=x/estW;fD+=` L ${(fsx+x).toFixed(1)} ${(fBy+Math.sin(t*Math.PI*2.5)*6*(1-t*.7)).toFixed(1)}`}
|
||||
const fLen=(estW*1.05).toFixed(0);
|
||||
const bR=isT?250:(parseInt(bg.slice(1,3),16)||250),bG=isT?248:(parseInt(bg.slice(3,5),16)||248),bB=isT?243:(parseInt(bg.slice(5,7),16)||243);
|
||||
const lum=(bR*299+bG*587+bB*114)/1000;
|
||||
const grain=lum>128?"rgba(0,0,0,.035)":"rgba(255,255,255,.06)",pen=lum>128?"#3a3a3a":"#aaa";
|
||||
let dots="";if(!isT)for(let i=0;i<50;i++)dots+=`<circle cx="${(i*137+29)%W}" cy="${(i*89+17)%H}" r=".7" fill="${grain}"/>`;
|
||||
const fi=fontUrl?`@import url('${fontUrl.replace(/&/g,"&")}');`:"";
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">
|
||||
<style>${fi}</style>
|
||||
<defs><clipPath id="reveal"><rect x="0" y="0" width="0" height="${H}"><animate attributeName="width" from="0" to="${W}" dur="${dur}s" fill="freeze" calcMode="spline" keySplines="0.25 0.1 0.25 1" keyTimes="0;1" repeatCount="indefinite"/></rect></clipPath></defs>
|
||||
${isT?"":`<rect width="${W}" height="${H}" rx="6" fill="${bg}"/>`}${dots}
|
||||
<g clip-path="url(#reveal)"><text x="${W/2}" y="${H/2+4}" font-family="${fontFamily}" font-size="60" fill="${inkColor}" text-anchor="middle" dominant-baseline="middle" letter-spacing="1">${escXml(text)}</text></g>
|
||||
<g opacity="1"><animateTransform attributeName="transform" type="translate" from="0 0" to="${W} 0" dur="${dur}s" fill="freeze" calcMode="spline" keySplines="0.25 0.1 0.25 1" keyTimes="0;1" repeatCount="indefinite"/><g transform="translate(0,${H/2-22}) rotate(22)"><rect x="-1.8" y="-28" width="3.6" height="26" rx="1.2" fill="${pen}"/><polygon points="0,1 -2,-5 2,-5" fill="${inkColor}"/></g><animate attributeName="opacity" values="1;1;0" keyTimes="0;0.92;1" dur="${dur}s" fill="freeze" repeatCount="indefinite"/></g>
|
||||
<path d="${fD}" fill="none" stroke="${inkColor}" stroke-width="1.4" stroke-linecap="round" opacity=".5" stroke-dasharray="${fLen}" stroke-dashoffset="${fLen}"><animate attributeName="stroke-dashoffset" from="${fLen}" to="0" begin="${fDel}s" dur="${fDr}s" fill="freeze" calcMode="spline" keySplines="0.4 0 0.2 1" keyTimes="0;1" repeatCount="indefinite"/></path>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
// ─── State & Render ──────────────────────────────────────────────
|
||||
function update(){
|
||||
const name=document.getElementById("nameInput").value;
|
||||
if(!name.trim()){document.getElementById("previewSvg").innerHTML='<div style="padding:40px;text-align:center;color:#bbb;font-style:italic">Type a name to preview</div>';state.svgStr="";return}
|
||||
state.svgStr=buildSVG(name,state.font.family,state.ink.color,state.bg,state.speed,state.font.url);
|
||||
document.getElementById("previewSvg").innerHTML=state.svgStr;
|
||||
const box=document.getElementById("previewBox");
|
||||
if(state.bg==="transparent"){box.style.background="linear-gradient(45deg,#ddd 25%,transparent 25%,transparent 75%,#ddd 75%),linear-gradient(45deg,#ddd 25%,transparent 25%,transparent 75%,#ddd 75%),#eee";box.style.backgroundSize="16px 16px,16px 16px";box.style.backgroundPosition="0 0,8px 8px"}
|
||||
else{box.style.background=state.bg;box.style.backgroundSize="";box.style.backgroundPosition=""}
|
||||
document.getElementById("svgSize").textContent=state.svgStr?`${(new Blob([state.svgStr]).size/1024).toFixed(1)} KB`:"—";
|
||||
updateUrls();
|
||||
renderFontGrid();
|
||||
}
|
||||
|
||||
function setBg(v){state.bg=v;document.getElementById("bgPicker").value=v==="transparent"?"#ffffff":v;document.getElementById("bgHex").value=v==="transparent"?"transparent":v.replace("#","");renderBgSwatches();update()}
|
||||
function onBgHex(el){const v=el.value.replace(/[^0-9a-fA-F]/g,"").slice(0,6);el.value=v;if(v.length===6)setBg("#"+v)}
|
||||
function onBgHexBlur(el){const v=el.value;if(v.length===6)setBg("#"+v);else if(v.length===3)setBg("#"+v[0]+v[0]+v[1]+v[1]+v[2]+v[2])}
|
||||
|
||||
function switchTab(t){
|
||||
document.querySelectorAll(".tab-btn").forEach(b=>b.classList.toggle("on",b.dataset.tab===t));
|
||||
document.getElementById("tab-local").style.display=t==="local"?"":"none";
|
||||
document.getElementById("tab-api").style.display=t==="api"?"":"none";
|
||||
}
|
||||
|
||||
function downloadSvg(){
|
||||
if(!state.svgStr)return;
|
||||
const a=document.createElement("a");a.href=URL.createObjectURL(new Blob([state.svgStr],{type:"image/svg+xml"}));
|
||||
a.download=`signature_${document.getElementById("nameInput").value.replace(/\s+/g,"_")}.svg`;a.click();URL.revokeObjectURL(a.href);
|
||||
}
|
||||
function copySvgSrc(){
|
||||
navigator.clipboard.writeText(state.svgStr).catch(()=>{const t=document.createElement("textarea");t.value=state.svgStr;document.body.appendChild(t);t.select();document.execCommand("copy");document.body.removeChild(t)});
|
||||
const b=document.getElementById("copySrcBtn");b.classList.add("ok");b.textContent="✓ Copied!";setTimeout(()=>{b.classList.remove("ok");b.textContent="Copy SVG Source Code"},2000);
|
||||
}
|
||||
function copyUrl(btn,text){
|
||||
navigator.clipboard.writeText(text).catch(()=>{const t=document.createElement("textarea");t.value=text;document.body.appendChild(t);t.select();document.execCommand("copy");document.body.removeChild(t)});
|
||||
btn.classList.add("ok");btn.textContent="✓ Copied!";setTimeout(()=>{btn.classList.remove("ok");btn.textContent="Copy"},2000);
|
||||
}
|
||||
|
||||
function updateUrls(){
|
||||
const name=document.getElementById("nameInput").value,base=document.getElementById("apiBase").value;
|
||||
const bgP=state.bg!=="#faf8f3"?`&bg=${encodeURIComponent(state.bg)}`:"";
|
||||
const svgU=`${base}?signature=${encodeURIComponent(name)}&font=${state.font.apiKey}&color=${state.ink.id}&speed=${state.speed}${bgP}`;
|
||||
const staticU=svgU+"&format=static";
|
||||
const rows=[
|
||||
{l:"Animated SVG URL",u:svgU,f:"svg"},
|
||||
{l:"Static SVG URL",u:staticU,f:"static"},
|
||||
{l:"HTML Embed",u:`<img src="${svgU}" alt="Signature" />`,f:"html"},
|
||||
{l:"Markdown Embed",u:``,f:"md"},
|
||||
];
|
||||
document.getElementById("urlRows").innerHTML=rows.map(r=>`<div class="url-row"><div class="lb lb-sm">${r.l}</div><div class="url-box"><code class="url-code">${r.u.replace(/</g,"<").replace(/>/g,">")}</code><button class="copy-btn" onclick="copyUrl(this,\`${r.u.replace(/`/g,"\\`").replace(/\\/g,"\\\\")}\`)">Copy</button></div></div>`).join("");
|
||||
// Server preview
|
||||
const img=document.getElementById("srvImg"),err=document.getElementById("srvErr");
|
||||
img.src=svgU+"&_t="+Date.now();
|
||||
img.onload=()=>{img.style.display="block";err.style.display="none"};
|
||||
img.onerror=()=>{img.style.display="none";err.style.display="block"};
|
||||
}
|
||||
function refreshPreview(){updateUrls()}
|
||||
|
||||
// ─── Init UI ─────────────────────────────────────────────────────
|
||||
function renderFontGrid(){
|
||||
const name=document.getElementById("nameInput").value||"Preview";
|
||||
document.getElementById("fontGrid").innerHTML=FONTS.map(f=>`<button class="font-btn${state.font.id===f.id?" on":""}" onclick="state.font=FONTS[${FONTS.indexOf(f)}];update()"><div class="preview" style="font-family:${f.family}">${name}</div><div class="tag">${f.style}</div><div class="api">api: ${f.apiKey}</div></button>`).join("");
|
||||
}
|
||||
function renderColorRow(){
|
||||
document.getElementById("colorRow").innerHTML=COLORS.map((c,i)=>`<button class="color-btn${state.ink.id===c.id?" on":""}" onclick="state.ink=COLORS[${i}];update()"><div class="color-dot" style="background:${c.color}${state.ink.id===c.id?";box-shadow:0 0 8px "+c.color+"60":""}"></div><span class="color-name">${c.name}</span></button>`).join("");
|
||||
}
|
||||
function renderBgSwatches(){
|
||||
document.getElementById("bgSwatches").innerHTML=BG_SWATCHES.map(s=>{
|
||||
const isT=s.c==="transparent",on=isT?(state.bg==="transparent"):(state.bg.toLowerCase()===s.c.toLowerCase());
|
||||
return `<button class="bg-sw${on?" on":""}${isT?" checker":""}" title="${s.t}" style="${isT?"":"background:"+s.c}" onclick="setBg('${s.c}')"></button>`;
|
||||
}).join("");
|
||||
}
|
||||
function renderSpeedBtns(){
|
||||
document.getElementById("speedBtns").innerHTML=SPEEDS.map(s=>`<button class="speed-btn${state.speed===s.v?" on":""}" onclick="state.speed=${s.v};renderSpeedBtns();update()"><span class="val">${s.l}</span><span class="desc">${s.d}</span></button>`).join("");
|
||||
document.getElementById("speedFill").style.width=`${(state.speed/3)*100}%`;
|
||||
}
|
||||
|
||||
// Boot
|
||||
document.getElementById("apiBase").value=defaultBase();
|
||||
renderFontGrid();renderColorRow();renderBgSwatches();renderSpeedBtns();
|
||||
setTimeout(update,300);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/api/:path*", "destination": "/api" },
|
||||
{ "source": "/", "destination": "/index.html" }
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/api/(.*)",
|
||||
"headers": [
|
||||
{ "key": "Access-Control-Allow-Origin", "value": "*" },
|
||||
{ "key": "Cache-Control", "value": "public, s-maxage=3600, stale-while-revalidate=86400" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user