<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>丝韵壁纸 SILKRIBBON STUDIO · 手机壁纸生成器</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=Noto+Sans+SC:wght@300;400;500;700&family=Unbounded:wght@500;700;900&family=ZCOOL+QingKe+HuangYou&display=swap" rel="stylesheet">
<style>
:root{
--bg0:#0c1118; --bg1:#111a26; --panel:#151f2e; --panel2:#121a27;
--line:#243349; --line2:#2c3d57;
--ink:#e9eef6; --muted:#8fa0b6; --dim:#5a6b82;
--amber:#f6b352; --teal:#3fd6c5; --coral:#ff6a55; --blue:#6fa0ff;
--disp:'ZCOOL QingKe HuangYou','Noto Sans SC',sans-serif;
--lat:'Unbounded','Noto Sans SC',sans-serif;
--body:'Noto Sans SC',sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{
font-family:var(--body); color:var(--ink); background:var(--bg0);
background-image:
radial-gradient(1100px 700px at 88% -8%, rgba(63,214,197,.09), transparent 60%),
radial-gradient(900px 640px at -8% 34%, rgba(246,179,82,.08), transparent 60%),
radial-gradient(800px 600px at 60% 110%, rgba(255,106,85,.06), transparent 60%),
linear-gradient(180deg,var(--bg0),var(--bg1));
background-attachment:fixed; min-height:100vh; overflow-x:hidden;
}
body::before{ /* 工整的网格底纹 */
content:''; position:fixed; inset:0; z-index:0; pointer-events:none;
background-image:
linear-gradient(rgba(148,170,200,.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(148,170,200,.045) 1px, transparent 1px);
background-size:56px 56px;
mask-image:radial-gradient(120% 90% at 50% 20%, #000 40%, transparent 100%);
}
.bg-silk{position:fixed; inset:0; width:100%; height:100%; z-index:0; opacity:.4;
pointer-events:none; animation:silkdrift 26s ease-in-out infinite alternate}
@keyframes silkdrift{from{transform:translateY(-14px) scale(1.02)}to{transform:translateY(16px) scale(1.06)}}
.bg-noise{position:fixed; inset:0; z-index:99; pointer-events:none; opacity:.05; mix-blend-mode:overlay;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='160' height='160' filter='url(%23n)' opacity='0.7'/></svg>")}
::selection{background:rgba(246,179,82,.35)}
button{font-family:inherit; cursor:pointer}
:focus-visible{outline:2px solid var(--amber); outline-offset:2px; border-radius:6px}
.mono{font-family:var(--lat)}
.wrap{max-width:1280px; margin:0 auto; padding:0 28px; position:relative; z-index:1}
/* ── 顶栏 ─────────────────────────── */
header.top{display:flex; align-items:center; justify-content:space-between; padding:22px 0 6px}
.logo{display:flex; align-items:center; gap:12px}
.logo svg{width:30px; height:30px}
.logo b{font-family:var(--disp); font-size:22px; letter-spacing:3px; font-weight:400}
.logo .en{font-family:var(--lat); font-size:9px; letter-spacing:4px; color:var(--dim); border:1px solid var(--line); padding:4px 8px; border-radius:5px; transform:translateY(1px)}
.top-meta{display:flex; gap:10px}
.pill{font-family:var(--lat); font-size:10px; letter-spacing:2px; color:var(--muted); border:1px solid var(--line); padding:6px 12px; border-radius:99px; background:rgba(21,31,46,.6)}
.pill.hot{color:var(--amber); border-color:rgba(246,179,82,.4)}
/* ── 标题带 ───────────────────────── */
.headline{display:flex; align-items:flex-end; justify-content:space-between; gap:24px; margin:30px 0 26px; flex-wrap:wrap}
.headline h1{font-family:var(--disp); font-weight:400; font-size:clamp(34px,4.6vw,56px); letter-spacing:3px; line-height:1.15}
.headline h1 em{font-style:normal; color:var(--amber); position:relative; white-space:nowrap}
.headline h1 em::after{content:''; position:absolute; left:-2%; right:-2%; bottom:6%; height:26%;
background:linear-gradient(90deg, rgba(246,179,82,.28), rgba(255,106,85,.22)); transform:skewX(-14deg); z-index:-1; border-radius:3px}
.headline .sub{margin-top:12px; color:var(--muted); font-size:14px; line-height:1.9; max-width:56ch; font-weight:300}
.headline .sub b{color:var(--teal); font-weight:500}
.stats{display:flex; gap:30px}
.stat{text-align:right}
.stat b{display:block; font-family:var(--lat); font-weight:900; font-size:30px; line-height:1}
.stat:nth-child(1) b{color:var(--amber)} .stat:nth-child(2) b{color:var(--teal)} .stat:nth-child(3) b{color:var(--coral)}
.stat i{display:block; font-style:normal; font-size:11px; letter-spacing:4px; color:var(--dim); margin-top:7px}
/* ── 工作台 ───────────────────────── */
.bench{display:grid; grid-template-columns:minmax(340px,440px) 1fr; gap:30px; align-items:start}
.stage-col{position:sticky; top:20px}
.stage{position:relative; display:flex; flex-direction:column; align-items:center; perspective:1100px; padding:18px 0 4px}
.stage-glow{position:absolute; width:130%; height:56%; bottom:2%; left:-15%; z-index:0; pointer-events:none;
background:radial-gradient(closest-side, rgba(246,179,82,.20), rgba(63,214,197,.10) 48%, transparent 72%); filter:blur(28px)}
.phone-bob{position:relative; z-index:1; animation:bob 7s ease-in-out infinite alternate}
@keyframes bob{from{transform:translateY(-6px)}to{transform:translateY(7px)}}
.phone{width:min(298px,72vw); aspect-ratio:9/19.5; border-radius:46px; padding:10px; position:relative;
background:linear-gradient(160deg,#2c3a50,#131b28);
box-shadow:0 34px 80px rgba(0,0,0,.55), inset 0 0 0 1px #35435e, inset 0 2px 0 rgba(255,255,255,.07);
transition:transform .35s ease; transform-style:preserve-3d}
.phone::before,.phone::after{content:''; position:absolute; right:-3px; width:3px; border-radius:2px; background:#3a4a66}
.phone::before{top:22%; height:56px} .phone::after{top:38%; height:34px}
.screen{position:relative; width:100%; height:100%; border-radius:37px; overflow:hidden; background:#000; box-shadow:inset 0 0 0 1px rgba(255,255,255,.05)}
.screen canvas{width:100%; height:100%; display:block; object-fit:cover}
.notch{position:absolute; top:11px; left:50%; transform:translateX(-50%); width:32%; height:17px; background:#000; border-radius:10px; z-index:2; box-shadow:0 0 0 1px rgba(255,255,255,.05)}
.stage.weaving .screen::after{content:''; position:absolute; inset:0; z-index:3;
background:linear-gradient(180deg, transparent 20%, rgba(255,255,255,.09) 50%, transparent 80%);
animation:scan 1.05s linear infinite}
@keyframes scan{from{transform:translateY(-100%)}to{transform:translateY(100%)}}
.stage-tag{margin-top:16px; font-size:10px; letter-spacing:3px; color:var(--dim); text-align:center}
.stage.weaving .stage-tag{color:var(--teal)}
.stage.weaving .stage-tag::after{content:'…'; animation:dots 1s steps(3) infinite}
@keyframes dots{0%{content:''}33%{content:'·'}66%{content:'··'}100%{content:'···'}}
.actions{display:flex; flex-wrap:wrap; gap:10px; margin-top:20px}
.btn-gen{flex:1 1 100%; display:flex; align-items:center; justify-content:center; gap:12px;
padding:16px 22px; border:none; border-radius:14px; position:relative; overflow:hidden;
background:linear-gradient(135deg,var(--coral),var(--amber)); color:#221104;
font-family:var(--disp); font-size:19px; letter-spacing:4px;
box-shadow:0 10px 34px rgba(255,122,80,.32), inset 0 1px 0 rgba(255,255,255,.4);
transition:transform .2s ease, box-shadow .2s ease}
.btn-gen:hover{transform:translateY(-2px); box-shadow:0 16px 42px rgba(255,122,80,.42), inset 0 1px 0 rgba(255,255,255,.4)}
.btn-gen:active{transform:translateY(1px) scale(.99)}
.btn-gen::after{content:''; position:absolute; top:0; bottom:0; width:46%; left:-60%;
background:linear-gradient(100deg, transparent, rgba(255,255,255,.5), transparent); transform:skewX(-20deg)}
.btn-gen:hover::after{animation:shine .8s ease}
@keyframes shine{to{left:130%}}
.btn-gen kbd{font-family:var(--lat); font-size:10px; letter-spacing:1px; background:rgba(0,0,0,.22); padding:3px 8px; border-radius:6px}
.btn-ghost{flex:1; padding:12px 14px; border-radius:12px; border:1px solid var(--line2); background:var(--panel);
color:var(--muted); font-size:13px; letter-spacing:2px; transition:all .2s ease}
.btn-ghost:hover{border-color:var(--amber); color:var(--amber); transform:translateY(-2px); box-shadow:0 8px 22px rgba(0,0,0,.3)}
.seedline{margin-top:12px; font-size:10px; letter-spacing:2px; color:var(--dim); display:flex; align-items:center; gap:10px; justify-content:center}
.seedline b{color:var(--amber); font-weight:700}
.seedline button{background:none; border:none; color:var(--teal); font-family:var(--lat); font-size:10px; letter-spacing:2px; border-bottom:1px dashed var(--teal); padding-bottom:1px}
.hintline{margin-top:8px; text-align:center; font-size:11px; color:var(--dim); letter-spacing:1px}
/* 灵感库 */
.history{margin-top:26px}
.thumbs{display:grid; grid-template-columns:repeat(auto-fill,minmax(74px,1fr)); gap:10px; margin-top:14px}
.thumbs figure{position:relative; aspect-ratio:9/19.5; border-radius:10px; overflow:hidden; border:1px solid var(--line);
cursor:pointer; transition:transform .2s ease, border-color .2s ease; background:#0a0f16}
.thumbs figure:hover{transform:translateY(-4px); border-color:var(--amber)}
.thumbs img{width:100%; height:100%; object-fit:cover; display:block}
.thumbs figcaption{position:absolute; left:0; right:0; bottom:0; padding:4px 0; text-align:center; font-size:7px; letter-spacing:1px; color:#cfd9e6; background:linear-gradient(transparent, rgba(0,0,0,.75))}
.thumbs .ov{position:absolute; inset:0; background:rgba(6,10,16,.62); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:7px; opacity:0; transition:opacity .2s ease}
.thumbs figure:hover .ov{opacity:1}
.mini{font-size:11px; letter-spacing:2px; padding:5px 12px; border-radius:7px; border:1px solid rgba(255,255,255,.35); background:rgba(255,255,255,.08); color:#fff; transition:all .15s ease}
.mini:hover{background:var(--amber); color:#221104; border-color:var(--amber)}
.thumbs .empty{grid-column:1/-1; border:1px dashed var(--line2); border-radius:10px; padding:22px; text-align:center; color:var(--dim); font-size:12px; letter-spacing:2px}
/* ── 控制台 ───────────────────────── */
.console{display:flex; flex-direction:column; gap:18px}
.panel{background:linear-gradient(180deg,var(--panel),var(--panel2)); border:1px solid var(--line); border-radius:18px;
padding:20px 22px 22px; position:relative; overflow:hidden}
.panel::before{content:''; position:absolute; top:0; left:22px; right:22px; height:1px;
background:linear-gradient(90deg, transparent, rgba(246,179,82,.35), transparent)}
.panel h3{display:flex; align-items:center; gap:12px; margin-bottom:16px}
.panel h3 .no{font-size:10px; font-weight:700; letter-spacing:1px; color:var(--amber); border:1px solid rgba(246,179,82,.45); padding:3px 7px; border-radius:6px}
.panel h3 .tt{font-family:var(--disp); font-weight:400; font-size:19px; letter-spacing:3px}
.panel h3 .rule{flex:1; height:1px; background:linear-gradient(90deg,var(--line2),transparent)}
.panel h3 .hint{font-size:11px; color:var(--dim); letter-spacing:1px}
.chips{display:grid; grid-template-columns:repeat(auto-fill,minmax(104px,1fr)); gap:10px}
.chip{display:flex; flex-direction:column; align-items:flex-start; gap:7px; padding:12px 12px 10px; border-radius:12px;
border:1px solid var(--line); background:rgba(10,15,23,.5); text-align:left; transition:all .2s ease; position:relative}
.chip:hover{transform:translateY(-3px); border-color:var(--line2); box-shadow:0 10px 24px rgba(0,0,0,.3)}
.chip .dots{display:flex; gap:5px}
.chip .dots i{width:11px; height:11px; border-radius:50%; box-shadow:0 0 8px rgba(255,255,255,.15)}
.chip b{font-size:14px; font-weight:500; letter-spacing:2px}
.chip em{font-style:normal; font-size:10px; color:var(--dim); letter-spacing:1px}
.chip.sel{border-color:var(--amber); background:rgba(246,179,82,.09); box-shadow:0 0 0 1px rgba(246,179,82,.35), 0 8px 24px rgba(0,0,0,.3)}
.chip.sel::after{content:'✓'; position:absolute; top:8px; right:10px; font-size:11px; color:var(--amber)}
.themes{display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); gap:10px}
.tcard{display:flex; flex-direction:column; gap:8px; padding:9px; border-radius:12px; border:1px solid var(--line);
background:rgba(10,15,23,.5); transition:all .2s ease; position:relative; text-align:left}
.tcard:hover{transform:translateY(-3px); border-color:var(--line2)}
.tcard .sw{height:46px; border-radius:8px; box-shadow:inset 0 0 0 1px rgba(255,255,255,.08)}
.tcard b{font-size:13px; font-weight:500; letter-spacing:2px}
.tcard i{position:absolute; top:13px; right:13px; font-style:normal; font-size:9px; letter-spacing:1px; color:rgba(255,255,255,.85); background:rgba(0,0,0,.4); padding:2px 6px; border-radius:5px}
.tcard.sel{border-color:var(--teal); box-shadow:0 0 0 1px rgba(63,214,197,.4); background:rgba(63,214,197,.07)}
.ctl{display:grid; grid-template-columns:76px 1fr 52px; align-items:center; gap:14px; padding:9px 0}
.ctl + .ctl{border-top:1px dashed rgba(44,61,87,.5)}
.ctl-name{font-size:13px; letter-spacing:2px; color:var(--muted)}
.ctl output{font-size:11px; color:var(--amber); text-align:right; letter-spacing:1px}
input[type=range]{-webkit-appearance:none; appearance:none; width:100%; height:22px; background:transparent}
input[type=range]::-webkit-slider-runnable-track{height:4px; border-radius:4px; background:linear-gradient(90deg,var(--line2),var(--line))}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none; width:16px; height:16px; border-radius:50%; margin-top:-6px;
background:var(--amber); border:3px solid #1a2333; box-shadow:0 0 10px rgba(246,179,82,.5); transition:transform .15s ease; cursor:grab}
input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.25)}
input[type=range]::-moz-range-track{height:4px; border-radius:4px; background:var(--line2)}
input[type=range]::-moz-range-thumb{width:12px; height:12px; border-radius:50%; background:var(--amber); border:3px solid #1a2333; cursor:grab}
.switch-row{display:flex; gap:22px; margin-top:14px; flex-wrap:wrap}
.switch{display:flex; align-items:center; gap:9px; font-size:13px; letter-spacing:2px; color:var(--muted); cursor:pointer; user-select:none}
.switch input{display:none}
.switch i{width:36px; height:20px; border-radius:99px; background:var(--line); position:relative; transition:background .2s ease; flex:none}
.switch i::after{content:''; position:absolute; top:3px; left:3px; width:14px; height:14px; border-radius:50%; background:#8fa0b6; transition:all .2s ease}
.switch input:checked + i{background:rgba(63,214,197,.35)}
.switch input:checked + i::after{left:19px; background:var(--teal); box-shadow:0 0 8px rgba(63,214,197,.6)}
.switch:hover i{box-shadow:0 0 0 3px rgba(63,214,197,.12)}
.seg{display:flex; gap:8px; flex-wrap:wrap}
.seg button{flex:1; min-width:110px; padding:11px 10px; border-radius:10px; border:1px solid var(--line); background:rgba(10,15,23,.5);
color:var(--muted); font-family:var(--lat); font-size:11px; letter-spacing:1px; transition:all .2s ease}
.seg button small{display:block; font-family:var(--body); font-size:10px; letter-spacing:2px; margin-top:4px; color:var(--dim)}
.seg button:hover{border-color:var(--line2); transform:translateY(-2px)}
.seg button.sel{border-color:var(--blue); color:var(--blue); background:rgba(111,160,255,.08); box-shadow:0 0 0 1px rgba(111,160,255,.3)}
.note{margin-top:14px; font-size:10px; letter-spacing:2px; color:var(--dim); line-height:1.9}
.note span{color:var(--teal)}
/* ── 页脚 ─────────────────────────── */
.ticker{overflow:hidden; border-top:1px solid #1c2a3e; border-bottom:1px solid #1c2a3e; padding:15px 0; margin-top:52px}
.ticker-in{display:flex; width:max-content; animation:tick 32s linear infinite}
.ticker:hover .ticker-in{animation-play-state:paused}
.tk{display:flex; gap:46px; padding-right:46px}
.tk span{font-family:var(--lat); font-size:11px; font-weight:600; letter-spacing:5px; color:#48596f; white-space:nowrap}
.tk span.sep{color:var(--amber); opacity:.6}
@keyframes tick{to{transform:translateX(-50%)}}
.foot-note{padding:22px 0 34px; text-align:center; font-size:11px; letter-spacing:2px; color:var(--dim)}
.foot-note b{color:var(--muted); font-weight:400}
/* 出场动画 & 提示 */
.reveal{opacity:0; transform:translateY(16px); transition:opacity .6s ease, transform .6s ease}
.reveal.in{opacity:1; transform:none}
.toast{position:fixed; left:50%; bottom:34px; transform:translate(-50%,90px); z-index:200;
background:#1c2a3d; border:1px solid rgba(246,179,82,.45); color:var(--ink); font-size:13px; letter-spacing:2px;
padding:12px 24px; border-radius:12px; box-shadow:0 16px 44px rgba(0,0,0,.5); transition:transform .35s cubic-bezier(.2,.9,.3,1.2)}
.toast.show{transform:translate(-50%,0)}
@media (max-width:980px){
.bench{grid-template-columns:1fr}
.stage-col{position:static}
.headline{flex-direction:column; align-items:flex-start}
.stats{width:100%; justify-content:flex-start} .stat{text-align:left}
.top-meta .pill:first-child{display:none}
}
</style>
</head>
<body>
<svg class="bg-silk" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="sg1" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#f6b352" stop-opacity="0"/><stop offset=".5" stop-color="#f6b352" stop-opacity=".55"/><stop offset="1" stop-color="#ff6a55" stop-opacity="0"/>
</linearGradient>
<linearGradient id="sg2" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#3fd6c5" stop-opacity="0"/><stop offset=".5" stop-color="#3fd6c5" stop-opacity=".5"/><stop offset="1" stop-color="#6fa0ff" stop-opacity="0"/>
</linearGradient>
</defs>
<path d="M-100,640 C300,420 620,840 920,560 S1420,300 1620,540" stroke="url(#sg1)" stroke-width="2" fill="none"/>
<path d="M-100,230 C360,450 720,110 1060,340 S1520,640 1660,380" stroke="url(#sg2)" stroke-width="1.6" fill="none"/>
<path d="M-100,780 C420,660 820,920 1240,700" stroke="url(#sg2)" stroke-width="1" fill="none" opacity=".6"/>
</svg>
<div class="bg-noise" aria-hidden="true"></div>
<div class="wrap">
<header class="top">
<div class="logo">
<svg viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M3 22 C10 8, 22 26, 29 10" stroke="#f6b352" stroke-width="2.4" stroke-linecap="round"/>
<path d="M3 12 C11 26, 21 6, 29 20" stroke="#3fd6c5" stroke-width="2.4" stroke-linecap="round" opacity=".85"/>
</svg>
<b>丝韵壁纸</b><span class="en">SILKRIBBON STUDIO</span>
</div>
<div class="top-meta">
<span class="pill">CANVAS 实时渲染</span>
<span class="pill hot">V2.6 · 2026.07</span>
</div>
</header>
<section class="headline">
<div>
<h1>一键织出<em>旗舰级</em>丝带壁纸</h1>
<p class="sub">致敬各大手机厂商经典壁纸语言 —— 选一个<b>品牌风格</b>、定一种<b>主题氛围</b>、微调<b>织造参数</b>,剩下的交给随机数。每一次点击,都是一条全新的丝绸光带。</p>
</div>
<div class="stats">
<div class="stat"><b>08</b><i>品牌风格</i></div>
<div class="stat"><b>08</b><i>主题氛围</i></div>
<div class="stat"><b>∞</b><i>随机组合</i></div>
</div>
</section>
<main class="bench">
<!-- 左:舞台 -->
<section class="stage-col">
<div class="stage" id="stage">
<div class="stage-glow"></div>
<div class="phone-bob">
<div class="phone" id="phone">
<div class="screen"><canvas id="cv" width="1080" height="2340"></canvas><div class="notch"></div></div>
</div>
</div>
<div class="stage-tag mono" id="stageTag">INITIALIZING…</div>
</div>
<div class="actions">
<button class="btn-gen" id="btnGen">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15l6 6"/><path d="M4 4l5 5"/></svg>
随机生成 <kbd>SPACE</kbd>
</button>
<button class="btn-ghost" id="btnDice">🎲 换风格</button>
<button class="btn-ghost" id="btnDl">⬇ 下载 PNG</button>
<button class="btn-ghost" id="btnBatch">✦ 连出四张</button>
</div>
<div class="seedline mono">SEED <b id="seedVal">—</b><button id="seedCopy">COPY</button></div>
<p class="hintline">切换风格 / 主题 / 参数时保留当前种子,便于横向对比</p>
<section class="history reveal">
<h3 style="display:flex;align-items:center;gap:12px">
<span class="no mono" style="font-size:10px;font-weight:700;color:var(--amber);border:1px solid rgba(246,179,82,.45);padding:3px 7px;border-radius:6px">05</span>
<span style="font-family:var(--disp);font-size:19px;letter-spacing:3px">灵感库</span>
<i style="flex:1;height:1px;background:linear-gradient(90deg,var(--line2),transparent)"></i>
<span style="font-size:11px;color:var(--dim);letter-spacing:1px;font-style:normal">点击载入 · 悬停下载</span>
</h3>
<div class="thumbs" id="thumbs"><div class="empty">生成的壁纸会收藏在这里</div></div>
</section>
</section>
<!-- 右:控制台 -->
<aside class="console">
<section class="panel reveal">
<h3><span class="no mono">01</span><span class="tt">品牌风格</span><i class="rule"></i><span class="hint">经典丝带语言</span></h3>
<div class="chips" id="brandChips"></div>
</section>
<section class="panel reveal">
<h3><span class="no mono">02</span><span class="tt">主题氛围</span><i class="rule"></i><span class="hint">深浅底色</span></h3>
<div class="themes" id="themeCards"></div>
</section>
<section class="panel reveal">
<h3><span class="no mono">03</span><span class="tt">织造参数</span><i class="rule"></i><span class="hint">拖动即时生效</span></h3>
<div id="ctlBox"></div>
<div class="switch-row">
<label class="switch"><input type="checkbox" id="tWatermark" checked><i></i>品牌水印</label>
<label class="switch"><input type="checkbox" id="tThreads" checked><i></i>丝芯高光</label>
</div>
</section>
<section class="panel reveal">
<h3><span class="no mono">04</span><span class="tt">画布与导出</span><i class="rule"></i><span class="hint">无损 PNG</span></h3>
<div class="seg" id="resSeg"></div>
<p class="note mono">OUTPUT · <span id="resInfo">1080 × 2340 · 9 : 19.5</span><br>适配主流机型锁屏与桌面,导出即原分辨率。</p>
</section>
</aside>
</main>
</div>
<footer class="wrap" style="padding:0 0">
<div class="ticker" aria-hidden="true">
<div class="ticker-in">
<div class="tk">
<span>HUAWEI</span><span class="sep">✦</span><span>XIAOMI</span><span class="sep">✦</span><span>OPPO</span><span class="sep">✦</span><span>VIVO</span><span class="sep">✦</span><span>HONOR</span><span class="sep">✦</span><span>SAMSUNG</span><span class="sep">✦</span><span>ONEPLUS</span><span class="sep">✦</span><span>MEIZU</span><span class="sep">✦</span>
</div>
<div class="tk">
<span>HUAWEI</span><span class="sep">✦</span><span>XIAOMI</span><span class="sep">✦</span><span>OPPO</span><span class="sep">✦</span><span>VIVO</span><span class="sep">✦</span><span>HONOR</span><span class="sep">✦</span><span>SAMSUNG</span><span class="sep">✦</span><span>ONEPLUS</span><span class="sep">✦</span><span>MEIZU</span><span class="sep">✦</span>
</div>
</div>
</div>
<p class="foot-note">纯前端 Canvas 生成 · <b>风格致敬,非官方素材</b> · 空格键随时随机织造</p>
</footer>
<div class="toast" id="toast"></div>
<script>
/* ═══════════ 数据:品牌 × 主题 ═══════════ */
const BRANDS=[
{id:'huawei',cn:'华为',en:'HUAWEI',tag:'星河绸缎',dots:['#ff4d3d','#ffb457'],
dark:{blend:'lighter',glow:.8,angle:.35,threads:true,palettes:[
['#ff3b2f','#ff8a3d','#ffd9a0'],['#e01f2d','#ff6a3d','#ffc46b'],['#fff1d6','#ffb457','#ff5a3c'],['#c8102e','#ff5f45','#ffe3b3']]},
light:{blend:'multiply',glow:.1,angle:.3,palettes:[
['#c8102e','#e0501e','#f2b25a'],['#a3122a','#d84a2a','#e8a33d'],['#8f1126','#c23a20','#b3541e']]}},
{id:'xiaomi',cn:'小米',en:'XIAOMI',tag:'缤纷活力',dots:['#ff6900','#3ddc84'],
dark:{blend:'lighter',glow:.55,angle:.5,palettes:[
['#ff6900','#ffb648','#fff3d6'],['#3ddc84','#7ce8b4','#eafff3'],['#4d8dff','#7fb4ff','#d6e6ff'],['#ff5f7a','#ff9a62','#ffd9a0']]},
light:{blend:'multiply',glow:.08,angle:.45,palettes:[
['#ff6900','#ff9a3d','#ffc46b'],['#0fae62','#3ddc84','#9be8c0'],['#2f6fe0','#4d8dff','#a8c8ff'],['#e0447a','#ff7a9a','#ffc2d1']]}},
{id:'oppo',cn:'OPPO',en:'OPPO',tag:'流光溢彩',dots:['#19b5a5','#7fd4c1'],
dark:{blend:'lighter',glow:.5,angle:.28,palettes:[
['#19b5a5','#5fd8c4','#d8fff4'],['#4da3ff','#8fd0ff','#e0f4ff'],['#ffd166','#ffe3a3','#fff8e6'],['#7fd4c1','#b8ead9','#f0fff9']]},
light:{blend:'multiply',glow:.05,angle:.25,palettes:[
['#0e9488','#3ec9b4','#a5e8d9'],['#2f7fd6','#6db4f2','#c4e2ff'],['#d9a02f','#f2c46b','#ffe9b8'],['#3aa08f','#7fd4c1','#d1f2e8']]}},
{id:'vivo',cn:'vivo',en:'VIVO',tag:'本原之蓝',dots:['#4169ff','#41d0ff'],
dark:{blend:'lighter',glow:.9,angle:.3,palettes:[
['#4169ff','#41d0ff','#e6fbff'],['#2f4fd8','#6fa0ff','#cfe0ff'],['#00c2ff','#7fe3ff','#eaffff'],['#a8c6ff','#e0ecff','#ffffff']]},
light:{blend:'multiply',glow:.08,angle:.3,palettes:[
['#2f55d8','#2fa8e0','#9adcf2'],['#1e3fae','#4d7ae0','#b3ccf2'],['#0090c8','#4dc4e8','#bfeefc']]}},
{id:'honor',cn:'荣耀',en:'HONOR',tag:'极光幻夜',dots:['#5a7bff','#4de3c0'],
dark:{blend:'lighter',glow:.85,angle:.4,palettes:[
['#5a7bff','#4de3c0','#d8fff4'],['#7b5cff','#5aa8ff','#cfe8ff'],['#37e0a8','#6fd4ff','#e0f8ff'],['#9a6bff','#5ad0e8','#e6fffa']]},
light:{blend:'multiply',glow:.08,angle:.35,palettes:[
['#3a5bd8','#2fb89a','#a8e8d6'],['#5a3fd8','#3a8ae0','#b3d8f2'],['#1fae82','#4da8d8','#c2ecdf']]}},
{id:'samsung',cn:'三星',en:'SAMSUNG',tag:'暗夜张力',dots:['#2f6bff','#ff4d6d'],
dark:{blend:'lighter',glow:.6,angle:.65,palettes:[
['#2f6bff','#7fb4ff','#e0ecff'],['#ff4d6d','#ff8fa3','#ffe0e6'],['#8a5cff','#b39aff','#ece0ff'],['#00c2a8','#6fe8d4','#e0fff8']]},
light:{blend:'multiply',glow:.06,angle:.6,palettes:[
['#1e4fd8','#5a8ae0','#b3ccf2'],['#d83a5c','#f27a92','#ffc9d4'],['#6a3fd8','#9a7ae0','#d6c9f2']]}},
{id:'oneplus',cn:'一加',en:'ONEPLUS',tag:'红黑极简',dots:['#eb0028','#d8d8d8'],
dark:{blend:'lighter',glow:.45,angle:.2,countBias:.55,widthMul:1.5,palettes:[
['#eb0028','#ff4d5e','#ffd9de'],['#f2f2f2','#c8c8c8','#8a8a8a'],['#eb0028','#8a0f1f','#ff8a95']]},
light:{blend:'multiply',glow:.05,angle:.2,countBias:.55,widthMul:1.5,palettes:[
['#c4001f','#eb0028','#ff8a95'],['#3a3a3a','#7a7a7a','#c2c2c2'],['#a30018','#e04452','#f2b3ba']]}},
{id:'meizu',cn:'魅族',en:'MEIZU',tag:'留白呼吸',dots:['#2f80ed','#eaf4ff'],
dark:{blend:'lighter',glow:.5,angle:.25,countBias:.8,palettes:[
['#2f80ed','#7ab4f2','#e0f0ff'],['#e8f4ff','#b3d8f2','#ffffff'],['#4da3ff','#a3d1ff','#f0f8ff']]},
light:{blend:'multiply',glow:.05,angle:.25,countBias:.8,palettes:[
['#1e5fc4','#4d8ae0','#b3d4f2'],['#2f80ed','#7ab4f2','#d6e9fa'],['#174a9e','#3a72c4','#a8ccec']]}}
];
const THEMES=[
{id:'obsidian',cn:'曜石黑金',light:false,bg:['#0b0d12','#161a24','#0a0c10']},
{id:'dawn',cn:'晨曦微光',light:true,bg:['#fff7ec','#ffe9d6','#f5d6bf']},
{id:'abyss',cn:'深海幽蓝',light:false,bg:['#04121f','#082a3d','#02101a']},
{id:'aurora',cn:'极光之夜',light:false,bg:['#061024','#0a2438','#041018']},
{id:'sakura',cn:'樱花初雪',light:true,bg:['#fff5f7','#ffe4ec','#f6cfdd']},
{id:'desert',cn:'大漠暮色',light:false,bg:['#1c1208','#3a2410','#120c06']},
{id:'neon',cn:'霓虹都市',light:false,bg:['#130a1e','#2a1238','#0c0614']},
{id:'ink',cn:'水墨留白',light:true,bg:['#f7f7f4','#ececea','#deded9']}
];
const RES=[
{label:'1080 × 2340',ratio:'9 : 19.5',size:[1080,2340]},
{label:'1170 × 2532',ratio:'9 : 19.5',size:[1170,2532]},
{label:'1440 × 3120',ratio:'2K 旗舰',size:[1440,3120]}
];
const SLIDERS=[
['count','丝带数量',2,12,1,''],
['width','线条粗细',40,180,5,'%'],
['flow','流动幅度',30,180,5,'%'],
['glow','光晕强度',0,100,5,'%'],
['grain','颗粒噪点',0,100,5,'%']
];
/* ═══════════ 状态 ═══════════ */
const state={brand:'huawei',theme:'obsidian',res:0,seed:0,
p:{count:6,width:100,flow:100,glow:65,grain:35,watermark:true,threads:true}};
const history=[];
const $=s=>document.querySelector(s);
const cv=$('#cv'), ctx=cv.getContext('2d');
const stage=$('#stage'), phone=$('#phone'), stageTag=$('#stageTag');
let current=null, animId=0;
/* ═══════════ 工具 ═══════════ */
function mulberry32(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}}
const clamp01=t=>Math.max(0,Math.min(1,t));
const easeOut=t=>1-Math.pow(1-t,3);
const pick=(r,arr)=>arr[(r()*arr.length)|0];
function hexA(hex,a){const n=parseInt(hex.slice(1),16);return `rgba(${n>>16&255},${n>>8&255},${n&255},${a})`}
const brandById=id=>BRANDS.find(b=>b.id===id);
const themeById=id=>THEMES.find(t=>t.id===id);
let toastT;function toast(m){const t=$('#toast');t.textContent=m;t.classList.add('show');clearTimeout(toastT);toastT=setTimeout(()=>t.classList.remove('show'),1900)}
/* ═══════════ 场景构建 ═══════════ */
function snapCfg(){return{seed:state.seed,brand:state.brand,theme:state.theme,res:state.res,p:{...state.p}}}
function makeRibbon(rng,W,H,flowMul,wMul,S,threadsOn){
const N=84,pts=[];
const yAmp=H*(0.035+0.13*rng())*flowMul;
const f1=0.6+rng()*1.5, f2=1.6+rng()*2.6, p1=rng()*6.283, p2=rng()*6.283;
const wBase=H*(0.03+0.07*rng())*wMul;
const drift=(rng()-0.5)*H*0.22*flowMul;
const len=W*1.4;
for(let i=0;i<=N;i++){
const t=i/N, x=(t-0.5)*len;
const y=Math.sin(t*6.283*f1+p1)*yAmp + Math.sin(t*6.283*f2+p2)*yAmp*0.38 + t*drift;
const taper=Math.pow(Math.sin(Math.PI*t),0.75);
const w=Math.max(2, wBase*(0.28+0.72*taper)*(0.78+0.22*Math.sin(t*9.4+p2)));
pts.push({x,y,w});
}
return {pts,wBase,colors:pick(rng,S.palettes),alpha:0.5+0.45*rng(),
angle:(rng()*2-1)*(S.angle||.3),cy:H*(0.16+0.68*rng()),cx:W/2,
core:rng()<0.8,threads:threadsOn&&(S.threads!==false)&&rng()<0.6,
blend:S.blend,glowMul:0.6+0.8*rng()};
}
function buildScene(cfg){
const rng=mulberry32(cfg.seed);
const B=brandById(cfg.brand), T=themeById(cfg.theme);
const [W,H]=RES[cfg.res].size;
const mode=T.light?'light':'dark';
const S=B[mode];
const count=Math.max(2,Math.round(cfg.p.count*(S.countBias||1)));
const flowMul=(cfg.p.flow/100)*(S.flowMul||1);
const wMul=(cfg.p.width/100)*(S.widthMul||1);
const ribbons=[];
for(let i=0;i<count;i++)ribbons.push(makeRibbon(rng,W,H,flowMul,wMul,S,cfg.p.threads));
ribbons.sort((a,b)=>b.wBase-a.wBase);
return {W,H,mode,T,B,S,ribbons,
glow:(cfg.p.glow/100)*(S.glow||.5),
bgRand:{gx1:rng(),gy1:rng(),gx2:rng(),gy2:rng()},
pal:ribbons[0]?ribbons[0].colors:S.palettes[0]};
}
/* ═══════════ 绘制 ═══════════ */
function drawBG(c,sc){
const{W,H,T}=sc;
const g=c.createLinearGradient(0,0,W*0.25,H);
g.addColorStop(0,T.bg[0]);g.addColorStop(0.55,T.bg[1]);g.addColorStop(1,T.bg[2]);
c.fillStyle=g;c.fillRect(0,0,W,H);
const r=sc.bgRand, a=sc.mode==='dark'?0.30:0.16;
blob(c,W*(0.12+r.gx1*0.3),H*(0.08+r.gy1*0.25),H*0.55,sc.pal[0],a,sc);
blob(c,W*(0.58+r.gx2*0.35),H*(0.58+r.gy2*0.3),H*0.6,sc.pal[2],a*0.8,sc);
}
function blob(c,x,y,rad,color,alpha,sc){
c.save();c.globalCompositeOperation=sc.mode==='dark'?'lighter':'source-over';
const g=c.createRadialGradient(x,y,0,x,y,rad);
g.addColorStop(0,hexA(color,alpha));g.addColorStop(1,hexA(color,0));
c.fillStyle=g;c.fillRect(x-rad,y-rad,rad*2,rad*2);c.restore();
}
function drawRibbon(c,r,rv,sc){
if(rv<=0)return;
const n=Math.max(4,Math.ceil(r.pts.length*rv)),P=r.pts;
c.save();
c.translate(r.cx,r.cy);c.rotate(r.angle);
c.globalCompositeOperation=r.blend;c.globalAlpha=r.alpha;
const g=c.createLinearGradient(P[0].x,0,P[P.length-1].x,0);
const[c0,c1,c2]=r.colors;
g.addColorStop(0,hexA(c0,0));g.addColorStop(0.16,hexA(c0,0.85));
g.addColorStop(0.5,hexA(c1,0.95));g.addColorStop(0.84,hexA(c2,0.85));
g.addColorStop(1,hexA(c2,0));
const glow=sc.glow*r.glowMul;
if(glow>0.04&&sc.mode==='dark'){c.shadowColor=hexA(c1,0.9);c.shadowBlur=glow*sc.H*0.05}
c.beginPath();c.moveTo(P[0].x,P[0].y-P[0].w/2);
for(let i=1;i<n;i++)c.lineTo(P[i].x,P[i].y-P[i].w/2);
for(let i=n-1;i>=0;i--)c.lineTo(P[i].x,P[i].y+P[i].w/2);
c.closePath();c.fillStyle=g;c.fill();
c.shadowBlur=0;
if(r.core&&sc.mode==='dark'){
c.globalAlpha=r.alpha*0.7;c.strokeStyle='rgba(255,255,255,0.5)';
c.lineWidth=Math.max(1,sc.H*0.0011);c.beginPath();c.moveTo(P[0].x,P[0].y);
for(let i=1;i<n;i++)c.lineTo(P[i].x,P[i].y);c.stroke();
}
if(r.threads){
c.globalAlpha=r.alpha*0.5;c.strokeStyle=hexA(c2,0.8);
c.lineWidth=Math.max(1,sc.H*0.0009);
for(const off of[-0.32,0.34]){c.beginPath();
for(let i=0;i<n;i++){const yy=P[i].y+P[i].w*off;i?c.lineTo(P[i].x,yy):c.moveTo(P[i].x,yy)}c.stroke()}
}
c.restore();
}
function vignette(c,sc){
const{W,H,mode}=sc;
const g=c.createRadialGradient(W/2,H*0.45,H*0.2,W/2,H*0.5,H*0.78);
g.addColorStop(0,'rgba(0,0,0,0)');g.addColorStop(1,`rgba(0,0,0,${mode==='dark'?0.4:0.10})`);
c.fillStyle=g;c.fillRect(0,0,W,H);
}
let grainCv=null;
function grain(c,sc,amount){
if(amount<=0)return;
if(!grainCv){grainCv=document.createElement('canvas');grainCv.width=grainCv.height=180;
const gc=grainCv.getContext('2d'),id=gc.createImageData(180,180);
for(let i=0;i<id.data.length;i+=4){const v=Math.random()*255|0;id.data[i]=id.data[i+1]=id.data[i+2]=v;id.data[i+3]=255}
gc.putImageData(id,0,0)}
c.save();c.globalAlpha=amount*(sc.mode==='dark'?0.10:0.07);
c.globalCompositeOperation='overlay';c.fillStyle=c.createPattern(grainCv,'repeat');
c.fillRect(0,0,sc.W,sc.H);c.restore();
}
function watermark(c,sc,cfg){
if(!cfg.p.watermark)return;
const{W,H,mode,B}=sc;
c.save();
c.globalAlpha=mode==='dark'?0.5:0.45;
c.fillStyle=mode==='dark'?'#ffffff':'#1c1c1c';
const fs=Math.round(H*0.0155);
c.font=`500 ${fs}px Unbounded,'Noto Sans SC',sans-serif`;
c.textAlign='center';c.textBaseline='alphabetic';
let ls=false;try{if('letterSpacing' in c){c.letterSpacing=Math.round(fs*0.42)+'px';ls=true}}catch(e){}
c.fillText(`${B.en} · SILK RIBBON`,W/2,H*0.962);
if(ls)c.letterSpacing='0px';
c.globalAlpha*=0.7;c.fillRect(W/2-H*0.02,H*0.947,H*0.04,Math.max(1,H*0.0009));
c.restore();
}
function renderScene(c,sc,cfg,reveals,opts={}){
drawBG(c,sc);
sc.ribbons.forEach((r,i)=>drawRibbon(c,r,reveals?reveals[i]:1,sc));
vignette(c,sc);
if(opts.grain)grain(c,sc,cfg.p.grain/100);
watermark(c,sc,cfg);
}
/* ═══════════ 生成 & 动画 ═══════════ */
function setWeaving(b){stage.classList.toggle('weaving',b)}
function syncMeta(){
const B=brandById(state.brand),T=themeById(state.theme),[W,H]=RES[state.res].size;
stageTag.textContent=`${B.en} · ${T.cn} · ${W}×${H}`;
$('#seedVal').textContent=state.seed;
$('#resInfo').textContent=`${RES[state.res].label} · ${RES[state.res].ratio}`;
}
function generate(o={}){
const seed=o.seed??((Math.random()*0x7fffffff)|0);
state.seed=seed;
const cfg=snapCfg();
const[W,H]=RES[cfg.res].size;
if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H}
const sc=buildScene(cfg);current={sc,cfg};
syncMeta();
cancelAnimationFrame(animId);
const fast=!!o.fast, n=sc.ribbons.length;
const dur=fast?380:760, stag=fast?26:66;
const t0=performance.now(), reveals=new Array(n).fill(0);
setWeaving(true);
const step=now=>{
const el=now-t0;let all=true;
for(let i=0;i<n;i++){const rv=easeOut(clamp01((el-i*stag)/dur));reveals[i]=rv;if(rv<1)all=false}
renderScene(ctx,sc,cfg,reveals,{grain:all});
if(!all&&el<dur+stag*n+900){animId=requestAnimationFrame(step)}
else{renderScene(ctx,sc,cfg,null,{grain:true});setWeaving(false);if(!o.noHistory)pushHistory(cfg)}
};
animId=requestAnimationFrame(step);
}
/* ═══════════ 灵感库 & 导出 ═══════════ */
function pushHistory(cfg){
const[W,H]=RES[cfg.res].size,t=document.createElement('canvas');
const th=200;t.width=Math.round(th*W/H);t.height=th;
t.getContext('2d').drawImage(cv,0,0,t.width,t.height);
history.unshift({cfg,thumb:t.toDataURL('image/jpeg',0.72)});
if(history.length>8)history.pop();
renderHistory();
}
function renderHistory(){
const box=$('#thumbs');
if(!history.length){box.innerHTML='<div class="empty">生成的壁纸会收藏在这里</div>';return}
box.innerHTML=history.map((h,i)=>`<figure data-i="${i}">
<img src="${h.thumb}" alt="seed ${h.cfg.seed}">
<figcaption class="mono">${h.cfg.seed}</figcaption>
<div class="ov"><button class="mini" data-act="load">载入</button><button class="mini" data-act="dl">下载</button></div>
</figure>`).join('');
}
$('#thumbs').addEventListener('click',e=>{
const btn=e.target.closest('button');if(!btn)return;
const h=history[+e.target.closest('figure').dataset.i];
if(btn.dataset.act==='load'){applyCfg(h.cfg);generate({seed:h.cfg.seed});toast('已载入灵感 ✦')}
else downloadCfg(h.cfg);
});
function downloadCfg(cfg){
const[W,H]=RES[cfg.res].size,c=document.createElement('canvas');c.width=W;c.height=H;
renderScene(c.getContext('2d'),buildScene(cfg),cfg,null,{grain:true});
const a=document.createElement('a');
a.download=`silkribbon_${cfg.brand}_${cfg.seed}_${W}x${H}.png`;
a.href=c.toDataURL('image/png');a.click();toast('已开始下载 PNG ⬇');
}
/* ═══════════ 控制台构建 ═══════════ */
function buildUI(){
$('#brandChips').innerHTML=BRANDS.map(b=>`<button class="chip ${b.id===state.brand?'sel':''}" data-id="${b.id}">
<span class="dots"><i style="background:${b.dots[0]}"></i><i style="background:${b.dots[1]}"></i></span>
<b>${b.cn}</b><em>${b.tag}</em></button>`).join('');
$('#themeCards').innerHTML=THEMES.map(t=>`<button class="tcard ${t.id===state.theme?'sel':''}" data-id="${t.id}">
<span class="sw" style="background:linear-gradient(160deg,${t.bg[0]},${t.bg[1]} 55%,${t.bg[2]})"></span>
<b>${t.cn}</b><i>${t.light?'浅':'深'}</i></button>`).join('');
$('#resSeg').innerHTML=RES.map((r,i)=>`<button data-i="${i}" class="${i===state.res?'sel':''}">${r.label}<small>${r.ratio}</small></button>`).join('');
$('#ctlBox').innerHTML=SLIDERS.map(([k,name,min,max,step,unit])=>`<label class="ctl">
<span class="ctl-name">${name}</span>
<input type="range" id="s_${k}" min="${min}" max="${max}" step="${step}" value="${state.p[k]}">
<output class="mono" id="o_${k}">${state.p[k]}${unit}</output></label>`).join('');
}
function refreshUI(){
document.querySelectorAll('#brandChips .chip').forEach(el=>el.classList.toggle('sel',el.dataset.id===state.brand));
document.querySelectorAll('#themeCards .tcard').forEach(el=>el.classList.toggle('sel',el.dataset.id===state.theme));
document.querySelectorAll('#resSeg button').forEach(el=>el.classList.toggle('sel',+el.dataset.i===state.res));
SLIDERS.forEach(([k,,, , ,unit])=>{$('#s_'+k).value=state.p[k];$('#o_'+k).textContent=state.p[k]+unit});
$('#tWatermark').checked=state.p.watermark;$('#tThreads').checked=state.p.threads;
}
function applyCfg(cfg){
state.brand=cfg.brand;state.theme=cfg.theme;state.res=cfg.res;state.p={...cfg.p};refreshUI();
}
/* 事件 */
$('#brandChips').addEventListener('click',e=>{
const c=e.target.closest('.chip');if(!c)return;
state.brand=c.dataset.id;refreshUI();generate({seed:state.seed,fast:true});
});
$('#themeCards').addEventListener('click',e=>{
const c=e.target.closest('.tcard');if(!c)return;
state.theme=c.dataset.id;refreshUI();generate({seed:state.seed,fast:true});
});
$('#resSeg').addEventListener('click',e=>{
const b=e.target.closest('button');if(!b)return;
state.res=+b.dataset.i;refreshUI();generate({seed:state.seed,fast:true});
});
let deb=null;
$('#ctlBox').addEventListener('input',e=>{
const inp=e.target;if(inp.type!=='range')return;
const k=inp.id.slice(2),unit=SLIDERS.find(s=>s[0]===k)[5];
state.p[k]=+inp.value;$('#o_'+k).textContent=inp.value+unit;
clearTimeout(deb);deb=setTimeout(()=>generate({seed:state.seed,fast:true}),220);
});
$('#tWatermark').addEventListener('change',e=>{state.p.watermark=e.target.checked;generate({seed:state.seed,fast:true})});
$('#tThreads').addEventListener('change',e=>{state.p.threads=e.target.checked;generate({seed:state.seed,fast:true})});
$('#btnGen').addEventListener('click',()=>generate());
$('#btnDice').addEventListener('click',()=>{
state.brand=pick(Math.random,BRANDS).id;state.theme=pick(Math.random,THEMES).id;
refreshUI();generate();toast(`🎲 ${brandById(state.brand).cn} · ${themeById(state.theme).cn}`);
});
$('#btnDl').addEventListener('click',()=>{if(!current)return;
const a=document.createElement('a'),[W,H]=RES[current.cfg.res].size;
a.download=`silkribbon_${current.cfg.brand}_${current.cfg.seed}_${W}x${H}.png`;
a.href=cv.toDataURL('image/png');a.click();toast('已开始下载 PNG ⬇');
});
$('#btnBatch').addEventListener('click',()=>{
for(let k=0;k<4;k++){
const cfg={...snapCfg(),seed:(Math.random()*0x7fffffff)|0};
const[W,H]=RES[cfg.res].size,c=document.createElement('canvas');c.width=W;c.height=H;
renderScene(c.getContext('2d'),buildScene(cfg),cfg,null,{grain:true});
const t=document.createElement('canvas');const th=200;t.width=Math.round(th*W/H);t.height=th;
t.getContext('2d').drawImage(c,0,0,t.width,t.height);
history.unshift({cfg,thumb:t.toDataURL('image/jpeg',0.72)});
}
while(history.length>8)history.pop();
renderHistory();toast('✦ 已连出 4 张,收入灵感库');
});
$('#seedCopy').addEventListener('click',()=>{
(navigator.clipboard?navigator.clipboard.writeText(String(state.seed)):Promise.reject())
.then(()=>toast('种子已复制,可复现同款')).catch(()=>toast('复制失败'));
});
document.addEventListener('keydown',e=>{
if(e.code==='Space'&&!/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)){
e.preventDefault();$('#btnGen').click();
}
});
/* 手机视差倾斜 */
stage.addEventListener('pointermove',e=>{
const r=stage.getBoundingClientRect();
const dx=(e.clientX-r.left)/r.width-0.5, dy=(e.clientY-r.top)/r.height-0.5;
phone.style.transform=`rotateY(${dx*10}deg) rotateX(${-dy*8}deg)`;
});
stage.addEventListener('pointerleave',()=>phone.style.transform='');
/* 滚动出场 */
const io=new IntersectionObserver(es=>es.forEach(x=>x.isIntersecting&&x.target.classList.add('in')),{threshold:.12});
document.querySelectorAll('.reveal').forEach(el=>io.observe(el));
/* 字体就绪后重绘水印 */
if(document.fonts&&document.fonts.ready)document.fonts.ready.then(()=>{
if(current)renderScene(ctx,current.sc,current.cfg,null,{grain:true});
});
/* 启动 */
buildUI();
generate();
</script>
</body>
</html>
index.html
index.html