Files
perler-lab/docs/prototype-chibi.html

1068 lines
58 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PERLER LAB · AI 拼豆专区(交互原型)</title>
<style>
/* ═══════════════════════════════════════════════════════════════
PERLER LAB · AI 拼豆专区 (/chibi) 交互原型
设计令牌来自 DESIGN.md §1/§4 · 文案来自 copy-library.md §8
═══════════════════════════════════════════════════════════════ */
:root{
--bg-page:#f4f3ef; --bg-card:#ffffff; --bg-subtle:#f0ede4; --bg-hover:#faf9f4;
--text-primary:#171713; --text-secondary:#66645d; --text-tertiary:#8a877d;
--text-disabled:#b0aca0; --text-on-dark:#ffffff;
--accent:#edb045; --success:#3daf80; --danger:#e04f4f; --info:#3677d2;
--border:#e6e3d9; --border-strong:#d2cec4; --border-dashed:#e0dccd;
--font-sans:system-ui,-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
--font-mono:ui-monospace,"Cascadia Code",Consolas,monospace;
--text-xs:11px; --text-sm:12px; --text-md:13px; --text-lg:17px; --text-xl:20px; --text-hero:32px;
--space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px; --space-5:24px; --space-6:32px;
--radius-sm:8px; --radius-md:10px; --radius-lg:12px; --radius-full:999px;
--shadow-card:0 1px 2px rgba(0,0,0,.04);
--shadow-modal:0 8px 32px rgba(23,23,19,.14);
--shadow-float:0 2px 12px rgba(23,23,19,.08);
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font-sans);background:var(--bg-page);color:var(--text-primary);font-size:var(--text-md);line-height:1.6;-webkit-font-smoothing:antialiased}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
input,textarea{font-family:inherit}
.page{max-width:880px;margin:0 auto;padding:0 24px}
/* ── NAV (chibi 版) ── */
.nav{position:sticky;top:0;z-index:50;background:var(--bg-card);border-bottom:1px solid var(--border)}
.nav-inner{max-width:960px;margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;gap:var(--space-4)}
.nav-back{display:inline-flex;align-items:center;gap:6px;font-size:var(--text-sm);color:var(--text-secondary);text-decoration:none;font-weight:var(--weight-medium);padding:6px 10px;border-radius:var(--radius-sm)}
.nav-back:hover{background:var(--bg-subtle);color:var(--text-primary)}
.brand{display:flex;align-items:center;gap:var(--space-2);text-decoration:none;color:var(--text-primary);font-weight:700;font-size:var(--text-lg)}
.brand-mark{width:26px;height:26px;border-radius:6px;background:var(--accent);color:var(--text-primary);display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:14px}
.nav-spacer{flex:1}
.nav-badge{display:inline-flex;align-items:center;gap:6px;font-size:var(--text-xs);color:var(--text-tertiary);background:var(--bg-subtle);padding:4px 10px;border-radius:var(--radius-full)}
.nav-badge-dot{width:7px;height:7px;border-radius:50%;background:var(--success);display:inline-block}
/* ── HERO ── */
.hero{padding:48px 0 28px}
.eyebrow{font-size:var(--text-xs);letter-spacing:.22em;text-transform:uppercase;color:var(--accent);font-weight:700}
.hero h1{font-size:var(--text-hero);font-weight:700;line-height:1.25;margin:12px 0 14px;letter-spacing:-.01em}
.hero h1 em{font-style:italic;color:var(--accent)}
.hero-sub{font-size:14px;color:var(--text-secondary);max-width:640px}
/* ── 卡片 ── */
.card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-card);padding:var(--space-5);margin-top:var(--space-4)}
.section-head{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-4)}
.section-num{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--accent);font-weight:700;background:var(--bg-subtle);padding:2px 7px;border-radius:var(--radius-sm)}
.section-title{font-size:var(--text-lg);font-weight:700}
.section-note{margin-left:auto;font-size:var(--text-xs);color:var(--text-tertiary)}
/* ── 分类 Tabs ── */
.tabs{display:grid;grid-template-columns:repeat(5,1fr);gap:var(--space-2);position:relative;padding-bottom:3px}
.tabs-indicator{position:absolute;bottom:0;left:0;height:3px;border-radius:3px;background:var(--accent);transition:left .32s cubic-bezier(.16,1,.3,1),width .32s cubic-bezier(.16,1,.3,1)}
.tab{border:1px solid var(--border);border-radius:var(--radius-md);padding:10px 6px;text-align:center;background:var(--bg-card);transition:border-color .15s,background .15s}
.tab:hover{border-color:var(--border-strong)}
.tab.is-selected{background:var(--accent);border-color:var(--accent)}
.tab-name{display:block;font-size:var(--text-md);font-weight:700}
.tab-note{display:block;font-size:10px;color:var(--text-tertiary);margin-top:2px;line-height:1.4}
.tab.is-selected .tab-note{color:rgba(23,23,19,.65)}
/* 对比图 */
.compare{display:flex;align-items:center;gap:var(--space-4);margin:var(--space-4) 0}
.compare-box{flex:1;border:1px dashed var(--border-strong);border-radius:var(--radius-md);padding:var(--space-3);text-align:center;background:var(--bg-hover)}
.compare-label{display:block;font-size:var(--text-xs);color:var(--text-tertiary);margin-bottom:8px}
.compare-arrow{font-size:20px;color:var(--text-disabled)}
.pixel{display:grid;grid-template-columns:repeat(8,1fr);gap:2px;width:96px;margin:0 auto}
.pixel span{border-radius:50%;aspect-ratio:1;display:block}
.compare-source{width:96px;height:96px;margin:0 auto;border-radius:var(--radius-sm);background:
radial-gradient(circle at 30% 30%, #d2cec4 0%, transparent 60%),
radial-gradient(circle at 70% 65%, #b0aca0 0%, transparent 55%),
linear-gradient(145deg,#f5dfae 0%,#e8e4d8 100%)}
/* 细分 */
.subtype-title{font-size:var(--text-xs);color:var(--text-tertiary);margin:var(--space-4) 0 var(--space-2)}
.subtypes{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-2)}
.subtype{border:1px solid var(--border);border-radius:var(--radius-md);padding:10px 12px;text-align:left;background:var(--bg-card)}
.subtype:hover{border-color:var(--border-strong)}
.subtype.is-selected{border-color:var(--accent);background:var(--bg-hover)}
.subtype-name{display:block;font-size:var(--text-md);font-weight:700}
.subtype-note{display:block;font-size:10px;color:var(--text-tertiary);margin-top:2px}
/* 参数选项 */
.options{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--space-2)}
.options.is-3col{grid-template-columns:repeat(3,1fr)}
.option{position:relative;border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 6px;text-align:center;background:var(--bg-card)}
.option:hover{border-color:var(--border-strong)}
.option.is-selected{background:var(--accent);border-color:var(--accent)}
.option-value{display:block;font-size:var(--text-md);font-weight:700}
.option-hint{display:block;font-size:10px;color:var(--text-tertiary)}
.option.is-selected .option-hint{color:rgba(23,23,19,.65)}
.recommend{position:absolute;top:-8px;right:8px;background:var(--danger);color:#fff;font-size:9px;font-weight:700;padding:1px 6px;border-radius:var(--radius-full)}
/* 提示词标签 */
.tags{display:flex;flex-wrap:wrap;gap:var(--space-2)}
.tag{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--border);border-radius:var(--radius-full);padding:6px 13px;font-size:var(--text-sm);color:var(--text-secondary);background:var(--bg-card)}
.tag:hover{border-color:var(--border-strong);color:var(--text-primary)}
.tag.is-selected{background:var(--bg-subtle);border-color:var(--accent);color:var(--text-primary);font-weight:500}
.tag-marker{font-size:var(--text-xs);font-weight:700}
.tag.is-selected .tag-marker{color:var(--accent)}
.tag.is-disabled{opacity:.45;cursor:not-allowed}
.tag-hint{font-size:var(--text-xs);color:var(--danger);margin-top:8px}
/* 细节行:文本域 + 参考图 */
.detail-row{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);margin-top:var(--space-4)}
.field-label{display:block;font-size:var(--text-xs);font-weight:600;color:var(--text-secondary);margin-bottom:6px}
.textarea{width:100%;min-height:96px;border:1px solid var(--border-strong);border-radius:var(--radius-md);padding:10px 12px;font-size:var(--text-md);resize:vertical;outline:none;background:var(--bg-card);color:var(--text-primary)}
.textarea:focus{border-color:var(--accent)}
.char-counter{font-size:var(--text-xs);color:var(--text-tertiary);text-align:right;margin-top:4px;font-family:var(--font-mono)}
/* 上传框 */
.upload-zone{border:1.5px dashed var(--border-strong);border-radius:var(--radius-md);padding:var(--space-4);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}
.upload-zone:hover{border-color:var(--accent);background:var(--bg-hover)}
.upload-zone .plus{font-size:22px;color:var(--text-disabled);line-height:1;display:block}
.upload-zone .ut{font-size:var(--text-sm);font-weight:600;margin-top:4px;display:block}
.upload-zone .uh{font-size:var(--text-xs);color:var(--text-tertiary);margin-top:2px;display:block}
.upload-zone.is-error{border-color:var(--danger);background:#fdf3f0}
.upload-zone.is-error .ut{color:var(--danger)}
.upload-file{display:flex;align-items:center;gap:var(--space-3);background:var(--bg-subtle);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3)}
.upload-thumb{width:40px;height:40px;border-radius:var(--radius-sm);object-fit:cover;border:1px solid var(--border)}
.upload-meta{flex:1;min-width:0}
.upload-name{font-size:var(--text-sm);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.upload-size{font-size:var(--text-xs);color:var(--text-tertiary)}
.upload-reset{font-size:var(--text-xs);color:var(--info);padding:4px 8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-card)}
.upload-error{font-size:var(--text-xs);color:var(--danger);margin-top:6px}
/* ── 生成前确认 ── */
.confirm{display:grid;grid-template-columns:200px 1fr;gap:var(--space-5)}
.sample{text-align:center}
.sample-label{font-size:var(--text-xs);color:var(--text-tertiary);display:block;margin-bottom:8px}
.sample-desc{font-size:var(--text-xs);color:var(--text-tertiary);display:block;margin-top:8px}
.spec{border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden}
.spec-row{display:flex;justify-content:space-between;padding:9px 14px;font-size:var(--text-sm);border-bottom:1px dashed var(--border-dashed)}
.spec-row:last-child{border-bottom:none}
.spec-label{color:var(--text-tertiary)}
.spec-value{font-weight:600}
.spec-value b{font-family:var(--font-mono)}
.confirm-upload-row{margin-top:var(--space-4)}
.confirm-note{font-size:var(--text-xs);color:var(--text-secondary);margin:var(--space-3) 0;background:var(--bg-subtle);border-radius:var(--radius-md);padding:10px 12px}
.confirm-note b{color:var(--text-primary)}
.confirm-actions{display:flex;align-items:center;gap:var(--space-3);margin-top:var(--space-2)}
.btn-primary{flex:1;padding:12px 0;background:var(--accent);color:var(--text-primary);font-size:var(--text-md);font-weight:700;border-radius:var(--radius-md);display:inline-flex;align-items:center;justify-content:center;gap:8px}
.btn-primary:hover{background:#e0a63c}
.btn-primary:disabled{background:var(--border);color:var(--text-disabled);cursor:not-allowed}
.btn-link{font-size:var(--text-sm);color:var(--info);padding:8px 12px}
.btn-link:hover{text-decoration:underline}
.spinner{width:18px;height:18px;border:2px solid var(--border-strong);border-top-color:var(--accent);border-radius:50%;display:inline-block;animation:spin .8s linear infinite}
.btn-primary:disabled .spinner{border-top-color:var(--text-disabled)}
@keyframes spin{to{transform:rotate(360deg)}}
.remain-note{font-size:var(--text-xs);color:var(--success);margin-top:10px;display:flex;align-items:center;gap:6px}
.remain-note .dot{width:7px;height:7px;border-radius:50%;background:var(--success);display:inline-block}
/* 生成结果 */
.result{margin-top:var(--space-5);border:1px solid var(--accent);border-radius:var(--radius-lg);padding:var(--space-4);background:var(--bg-hover)}
.result-head{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)}
.result-title{font-size:var(--text-md);font-weight:700;display:flex;align-items:center;gap:8px}
.result-badge{background:var(--success);color:#fff;font-size:10px;font-weight:700;padding:2px 8px;border-radius:var(--radius-full)}
.result-meta{font-size:var(--text-xs);color:var(--text-tertiary)}
.result-canvas-wrap{display:flex;justify-content:center;background:#fff;border-radius:var(--radius-md);padding:var(--space-3);overflow:auto}
.result-info{display:flex;justify-content:space-between;align-items:center;margin-top:var(--space-3);font-size:var(--text-sm)}
.result-info b{font-weight:600}
.result-actions{margin-top:var(--space-3);display:flex;gap:var(--space-3)}
.result-btn{flex:1;padding:10px 0;border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:600}
.result-btn.save{background:var(--text-primary);color:var(--text-on-dark)}
.result-btn.save:hover{background:#2b2b26}
.result-btn.plan{background:var(--bg-card);border:1px solid var(--border-strong)}
.result-btn.plan:hover{background:var(--bg-subtle)}
/* ── PROMISE ── */
.promises{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-4);margin:var(--space-6) 0}
.promise{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:var(--space-5);box-shadow:var(--shadow-card)}
.promise-icon{width:38px;height:38px;border-radius:var(--radius-sm);background:var(--bg-subtle);display:flex;align-items:center;justify-content:center;color:var(--accent)}
.promise-title{font-size:var(--text-md);font-weight:700;margin:12px 0 6px}
.promise-desc{font-size:var(--text-xs);color:var(--text-tertiary);line-height:1.7}
/* ── FOOTER ── */
.footer{border-top:1px solid var(--border);padding:var(--space-5) 0 var(--space-6)}
.footer-inner{max-width:880px;margin:0 auto;padding:0 24px;display:flex;justify-content:space-between;align-items:center;font-size:var(--text-sm)}
.footer-text{color:var(--text-secondary);font-weight:500}
.footer-link{color:var(--info);text-decoration:none;font-size:var(--text-sm)}
.footer-link:hover{text-decoration:underline}
/* ── 移动端底部 CTA ── */
.mobile-bar{display:none;position:fixed;bottom:0;left:0;right:0;background:var(--bg-card);border-top:1px solid var(--border);padding:10px 16px calc(10px + env(safe-area-inset-bottom));z-index:40;gap:var(--space-3);align-items:center}
/* ── MODAL (scale + fade 过渡) ── */
.modal-mask{position:fixed;inset:0;background:rgba(23,23,19,.4);display:flex;align-items:center;justify-content:center;z-index:100;padding:16px;opacity:0;visibility:hidden;transition:opacity .24s ease,visibility .24s}
.modal-mask.is-visible{opacity:1;visibility:visible}
.modal-mask .modal{transform:scale(.96) translateY(10px);opacity:0;transition:transform .28s cubic-bezier(.16,1,.3,1),opacity .28s ease}
.modal-mask.is-visible .modal{transform:scale(1) translateY(0);opacity:1}
.modal{background:var(--bg-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-modal);width:100%;max-width:380px;padding:var(--space-5);position:relative}
.modal-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;font-size:18px;color:var(--text-tertiary);line-height:1}
.modal-close:hover{background:var(--bg-subtle);color:var(--text-primary)}
.modal-title{font-size:var(--text-lg);font-weight:700;margin-bottom:var(--space-4)}
.modal-input{width:100%;padding:11px 12px;border:1px solid var(--border-strong);border-radius:var(--radius-md);font-size:var(--text-md);outline:none;background:var(--bg-card)}
.modal-input:focus{border-color:var(--accent)}
.redeem-error{font-size:var(--text-sm);color:var(--danger);margin:8px 0 0}
.redeem-ok{font-size:var(--text-sm);color:var(--success);margin:8px 0 0;display:flex;align-items:center;gap:6px}
.redeem-btn{width:100%;margin-top:var(--space-3);padding:11px 0;background:var(--accent);color:var(--text-primary);font-size:var(--text-md);font-weight:700;border-radius:var(--radius-md)}
.redeem-btn:hover{background:#e0a63c}
/* ── TOAST ── */
.toast{position:fixed;left:50%;bottom:80px;transform:translateX(-50%) translateY(20px);background:var(--text-primary);color:#fff;font-size:var(--text-sm);padding:10px 18px;border-radius:var(--radius-full);opacity:0;transition:all .25s;z-index:200;pointer-events:none;max-width:90vw;text-align:center}
.toast.is-show{opacity:1;transform:translateX(-50%) translateY(0)}
/* ── 微交互 · 动画系统(纯 CSS,零依赖) ── */
button{position:relative;overflow:hidden}
.btn-primary,.redeem-btn,.result-btn,.upload-reset,.btn-link{transition:transform .12s ease,background .22s,border-color .22s,box-shadow .22s}
.btn-primary:active,.redeem-btn:active,.result-btn:active{transform:translateY(1px)}
.promise{transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
.promise:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(23,23,19,.09)}
/* NAV 返回链接:下滑线动效 */
.nav-back{position:relative}
.nav-back::after{content:"";position:absolute;left:10px;right:10px;bottom:2px;height:2px;border-radius:2px;background:var(--accent);transform:scaleX(0);transform-origin:left;transition:transform .25s cubic-bezier(.16,1,.3,1)}
.nav-back:hover::after{transform:scaleX(1)}
/* 分类切换:内容淡入上滑 */
.content-in{animation:content-in .36s cubic-bezier(.16,1,.3,1)}
@keyframes content-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
/* 豆子放置:逐个落下 */
.pixel span{animation:bead-drop .4s cubic-bezier(.34,1.56,.64,1) both}
@keyframes bead-drop{from{transform:scale(.8);opacity:0}to{transform:scale(1);opacity:1}}
/* 参数选中:回弹 pop */
.option.is-selected,.subtype.is-selected,.tab.is-selected{animation:pop .42s cubic-bezier(.34,1.56,.64,1)}
@keyframes pop{0%{transform:scale(1)}35%{transform:scale(1.08)}100%{transform:scale(1)}}
/* 生成中:动态圆点 */
.dots{display:inline-flex;gap:3px;margin-left:2px;vertical-align:2px}
.dots i{width:4px;height:4px;border-radius:50%;background:currentColor;animation:dot-pulse 1.1s infinite both}
.dots i:nth-child(2){animation-delay:.18s}
.dots i:nth-child(3){animation-delay:.36s}
@keyframes dot-pulse{0%,80%,100%{transform:scale(.55);opacity:.35}40%{transform:scale(1);opacity:1}}
/* 生成中:示例图 shimmer 扫光 */
.sample{position:relative;overflow:hidden}
.sample.is-generating::after{content:"";position:absolute;inset:0;z-index:2;border-radius:var(--radius-md);background:linear-gradient(100deg,transparent 25%,rgba(237,176,69,.25) 50%,transparent 75%);background-size:220% 100%;animation:shimmer 1.1s linear infinite}
@keyframes shimmer{0%{background-position:220% 0}100%{background-position:-220% 0}}
/* 图纸生成进度扫描 */
.gen-sweep{position:absolute;inset:0;z-index:3;pointer-events:none;overflow:hidden;border-radius:var(--radius-md);display:none}
.gen-sweep.is-active{display:block}
.gen-sweep::before{content:"";position:absolute;left:50%;top:50%;width:160%;height:160%;transform:translate(-50%,-50%);background:conic-gradient(from 0deg,rgba(237,176,69,0) 0deg,rgba(237,176,69,.5) 80deg,rgba(255,255,255,.92) 115deg,rgba(237,176,69,0) 150deg);animation:sweep .95s linear infinite}
@keyframes sweep{to{transform:translate(-50%,-50%) rotate(360deg)}}
.result-canvas-wrap{position:relative}
/* 按钮涟漪 */
.ripple{position:absolute;border-radius:50%;background:rgba(255,255,255,.45);transform:scale(0);animation:ripple .62s ease-out forwards;pointer-events:none;z-index:1}
@keyframes ripple{to{transform:scale(1);opacity:0}}
/* 下载成功勾勾 */
.check-svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:-2px;margin-right:6px}
.check-svg polyline{stroke-dasharray:24;stroke-dashoffset:24;animation:check-draw .5s ease forwards .12s}
@keyframes check-draw{to{stroke-dashoffset:0}}
.result-btn.is-downloading{background:var(--success);border-color:var(--success);color:var(--text-on-dark)}
/* ── FAQ 手风琴 ── */
.faq{margin-top:var(--space-2)}
.faq-item{border:1px solid var(--border);border-radius:var(--radius-md);margin-bottom:var(--space-2);overflow:hidden;background:var(--bg-card);transition:border-color .25s,box-shadow .25s}
.faq-item.is-open{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent),var(--shadow-card)}
.faq-q{width:100%;display:flex;align-items:center;gap:var(--space-3);padding:13px 16px;font-size:var(--text-md);font-weight:600;text-align:left;background:var(--bg-card);color:var(--text-primary)}
.faq-q:hover{background:var(--bg-hover)}
.faq-num{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--accent);font-weight:700;background:var(--bg-subtle);padding:1px 6px;border-radius:var(--radius-sm)}
.faq-arrow{margin-left:auto;font-size:var(--text-md);color:var(--text-tertiary);transition:transform .32s cubic-bezier(.16,1,.3,1),color .25s}
.faq-item.is-open .faq-arrow{transform:rotate(45deg);color:var(--accent)}
.faq-body{display:grid;grid-template-rows:0fr;transition:grid-template-rows .34s cubic-bezier(.16,1,.3,1)}
.faq-item.is-open .faq-body{grid-template-rows:1fr}
.faq-inner{overflow:hidden;min-height:0}
.faq-a{font-size:var(--text-sm);color:var(--text-secondary);line-height:1.85;padding:0 16px 14px 16px}
/* ── FOOTER 品牌故事 ── */
.footer-story{max-width:880px;margin:0 auto;padding:var(--space-6) 24px var(--space-5);display:grid;grid-template-columns:2fr 1fr 1fr;gap:var(--space-6)}
.footer-col{display:flex;flex-direction:column;gap:8px}
.footer-h{font-size:var(--text-sm);font-weight:700;margin-bottom:2px;color:var(--text-primary)}
.footer-col a{font-size:var(--text-sm);color:var(--text-secondary);text-decoration:none;width:fit-content}
.footer-col a:hover{color:var(--text-primary);text-decoration:underline}
.footer-desc{font-size:var(--text-sm);color:var(--text-secondary);line-height:1.9;margin-top:var(--space-3);max-width:400px}
.footer-slogan{font-size:var(--text-sm);color:var(--accent);font-weight:600;margin-top:var(--space-2)}
.footer-note{color:var(--text-tertiary);font-size:var(--text-xs)}
/* 动效减弱偏好 */
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:.001s!important;animation-iteration-count:1!important;transition-duration:.001s!important;scroll-behavior:auto!important}
.gen-sweep,.ripple{display:none!important}
.pixel span{animation-delay:0ms!important}
}
/* ── RESPONSIVE ── */
@media (max-width:767px){
.page{padding:0 16px}
.nav-inner{padding:0 16px;gap:var(--space-3)}
.nav-back span.txt{display:none}
.nav-badge span.txt{display:none}
.hero h1{font-size:26px}
.tabs{grid-template-columns:repeat(2,1fr)}
.compare{gap:var(--space-2)}
.pixel{width:72px}
.compare-source{width:72px;height:72px}
.subtypes,.options,.options.is-3col{grid-template-columns:1fr}
.detail-row{grid-template-columns:1fr}
.confirm{grid-template-columns:1fr}
.sample{display:flex;align-items:center;gap:var(--space-4);justify-content:center}
.sample-label{margin-bottom:0}
.promises{grid-template-columns:1fr}
.mobile-bar{display:flex}
.footer-inner{flex-direction:column;gap:8px;text-align:center}
.footer-story{grid-template-columns:1fr;gap:var(--space-4);padding:var(--space-5) 16px var(--space-4)}
}
</style>
</head>
<body>
<!-- ═══ NAV (chibi 版) ═══ -->
<nav class="nav">
<div class="nav-inner">
<a class="nav-back" href="prototype-home.html"><span>←</span><span class="txt">普通拼豆</span></a>
<a class="brand" href="prototype-chibi.html"><span class="brand-mark">P</span><span>PERLER <b>LAB</b></span></a>
<div class="nav-spacer"></div>
<span class="nav-badge"><span class="nav-badge-dot"></span><span class="txt">每次生成消耗 1 次权益</span></span>
</div>
</nav>
<main class="page">
<!-- ═══ BEAD HERO ═══ -->
<header class="hero">
<span class="eyebrow">IMAGE TO PERLER</span>
<h1>先选大类,再定细节,<em>生成结果先保存。</em></h1>
<p class="hero-sub">每次 AI 成功生成扣 1 次;该作品转成正式拼豆图纸并首次下载免费。只有重新生成新作品才再次扣费。</p>
</header>
<!-- ═══ BEAD BUILDER ═══ -->
<div class="card">
<!-- 01 选择拼豆大类 -->
<div class="section-head">
<span class="section-num">01</span>
<h2 class="section-title">选择拼豆大类</h2>
</div>
<div class="tabs" id="tabs"></div>
<div class="compare">
<div class="compare-box">
<span class="compare-label">原图</span>
<div class="compare-source"></div>
</div>
<span class="compare-arrow">→</span>
<div class="compare-box">
<span class="compare-label">像素拼豆效果</span>
<div class="pixel" id="comparePixel"></div>
</div>
</div>
<p class="subtype-title">细分选择 · 同一种类下不同画风</p>
<div class="subtypes" id="subtypes"></div>
</div>
<div class="card">
<!-- 02 图纸格数 -->
<div class="section-head">
<span class="section-num">02</span>
<h2 class="section-title">图纸格数</h2>
</div>
<div class="options" id="gridOptions"></div>
</div>
<div class="card">
<!-- 03 最大颜色数 -->
<div class="section-head">
<span class="section-num">03</span>
<h2 class="section-title">最大颜色数</h2>
<span class="section-note">颜色越多还原度越高</span>
</div>
<div class="options" id="colorOptions"></div>
</div>
<div class="card">
<!-- 04 主体构图 -->
<div class="section-head">
<span class="section-num">04</span>
<h2 class="section-title">主体构图</h2>
</div>
<div class="options is-3col" id="compOptions"></div>
</div>
<div class="card">
<!-- 05 细节提示词 -->
<div class="section-head">
<span class="section-num">05</span>
<h2 class="section-title">细节提示词</h2>
<span class="section-note">最多选 5 项</span>
</div>
<div class="tags" id="tags"></div>
<p class="tag-hint" id="tagHint" style="display:none"></p>
<div class="detail-row">
<div>
<label class="field-label" for="promptText">补充描述(选填)</label>
<textarea class="textarea" id="promptText" maxlength="160" placeholder="想保留的细节、不想要的元素…"></textarea>
<div class="char-counter"><span id="charCount">0</span>/160</div>
</div>
<div>
<span class="field-label">参考图上传(选填)</span>
<div id="refUploadZone"></div>
<input type="file" id="refFileInput" accept="image/jpeg,image/png,image/webp" style="display:none">
</div>
</div>
</div>
<!-- ═══ 生成前确认 ═══ -->
<div class="card">
<div class="section-head">
<span class="section-num">✓</span>
<h2 class="section-title">生成前确认</h2>
</div>
<div class="confirm">
<div class="sample">
<span class="sample-label">示例</span>
<div class="pixel" id="confirmPixel"></div>
<span class="sample-desc">默认像素效果</span>
</div>
<div>
<div class="spec" id="spec">
<div class="spec-row"><span class="spec-label">类别</span><span class="spec-value" id="specCategory">宠物</span></div>
<div class="spec-row"><span class="spec-label">细分</span><span class="spec-value" id="specSubtype">猫咪头像</span></div>
<div class="spec-row"><span class="spec-label">图纸</span><span class="spec-value"><b id="specGrid">64</b>×<b id="specGrid2">64</b></span></div>
<div class="spec-row"><span class="spec-label">最大颜色</span><span class="spec-value"><b id="specColors">24</b> 色</span></div>
<div class="spec-row"><span class="spec-label">主体构图</span><span class="spec-value" id="specComp">主体特写</span></div>
<div class="spec-row"><span class="spec-label">提示词</span><span class="spec-value" id="specTags">0 项</span></div>
<div class="spec-row"><span class="spec-label">参考图</span><span class="spec-value" id="specRef">未上传</span></div>
<div class="spec-row"><span class="spec-label">预计耗时</span><span class="spec-value">约 10 – 20 秒</span></div>
<div class="spec-row"><span class="spec-label">预计颗数</span><span class="spec-value">约 <b id="specBeads">0</b> 颗</span></div>
</div>
<div class="confirm-upload-row">
<span class="field-label">上传需要转换的原图</span>
<div id="originUploadZone"></div>
<input type="file" id="originFileInput" accept="image/jpeg,image/png,image/webp" style="display:none">
</div>
<p class="confirm-note"><b>AI 生成属于付费功能。</b>成功生成扣 1 次;本作品转图纸和首次下载免费,失败不扣。</p>
<div class="confirm-actions">
<button type="button" class="btn-primary" id="genBtn" disabled>兑换后生成 →</button>
<button type="button" class="btn-link" id="codeLinkBtn">输入兑换码</button>
</div>
<p class="remain-note" id="remainNote" style="display:none"><span class="dot"></span><span id="remainText">剩余 10 次生成权益</span></p>
</div>
</div>
<!-- 生成结果 -->
<div class="result" id="resultArea" style="display:none">
<div class="result-head">
<span class="result-title">生成结果 <span class="result-badge">已保存</span></span>
<span class="result-meta" id="resultMeta"></span>
</div>
<div class="result-canvas-wrap">
<div class="gen-sweep" id="resultSweep"></div>
<canvas id="resultCanvas"></canvas>
</div>
<div class="result-info">
<span><b id="resultName">默认像素效果</b> · <span id="resultSpec"></span></span>
<span id="resultBeads"></span>
</div>
<div class="result-actions">
<button type="button" class="result-btn save" id="saveBtn">保存图片</button>
<button type="button" class="result-btn plan" id="toPlanBtn">下载拼豆图纸</button>
</div>
</div>
</div>
<!-- ═══ 常见问题 FAQ ═══ -->
<section class="card" id="faq">
<div class="section-head">
<span class="section-num">?</span>
<h2 class="section-title">常见问题</h2>
<span class="section-note">FAQ · 常见疑问快速解答</span>
</div>
<div class="faq">
<div class="faq-item is-open">
<button type="button" class="faq-q"><span class="faq-num">01</span>生成一次消耗多少次权益?<span class="faq-arrow">+</span></button>
<div class="faq-body"><div class="faq-inner"><p class="faq-a">每次 AI 成功生成扣 1 次权益;该作品转拼豆图纸与首次高清下载免费,重新生成新作品才再次扣费。</p></div></div>
</div>
<div class="faq-item">
<button type="button" class="faq-q"><span class="faq-num">02</span>生成的图纸可以免费下载吗?<span class="faq-arrow">+</span></button>
<div class="faq-body"><div class="faq-inner"><p class="faq-a">可以。成功生成的作品转正式图纸和首次下载均免费;再次下载或导出其他尺寸会消耗 1 次权益。</p></div></div>
</div>
<div class="faq-item">
<button type="button" class="faq-q"><span class="faq-num">03</span>生成失败会扣权益吗?<span class="faq-arrow">+</span></button>
<div class="faq-body"><div class="faq-inner"><p class="faq-a">不会。只有 AI 成功生成才扣 1 次,失败或超时自动返还权益,可以放心重试。</p></div></div>
</div>
<div class="faq-item">
<button type="button" class="faq-q"><span class="faq-num">04</span>图片上传有什么限制?<span class="faq-arrow">+</span></button>
<div class="faq-body"><div class="faq-inner"><p class="faq-a">支持 JPG、PNG、WEBP 格式,单张不超过 12MB。主体清楚、光线自然的照片生成效果更好。</p></div></div>
</div>
<div class="faq-item">
<button type="button" class="faq-q"><span class="faq-num">05</span>提示词标签最多选几个?<span class="faq-arrow">+</span></button>
<div class="faq-body"><div class="faq-inner"><p class="faq-a">最多选 5 项。超出后其余标签会置灰并提示「最多选 5 项」,取消已选标签即可继续添加。</p></div></div>
</div>
<div class="faq-item">
<button type="button" class="faq-q"><span class="faq-num">06</span>图纸尺寸和颜色可以调整吗?<span class="faq-arrow">+</span></button>
<div class="faq-body"><div class="faq-inner"><p class="faq-a">可以。图纸格数提供 48 / 64 / 80 / 96 四档,最大颜色数提供 16 / 24 / 32 / 40 四档,主体构图也有三种可选。</p></div></div>
</div>
</div>
</section>
<!-- ═══ 三大承诺 ═══ -->
<section class="promises">
<div class="promise">
<span class="promise-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8" rx="2"/><rect x="13" y="3" width="8" height="8" rx="2"/><rect x="3" y="13" width="8" height="8" rx="2"/><rect x="13" y="13" width="8" height="8" rx="2"/></svg>
</span>
<h3 class="promise-title">分类更准确</h3>
<p class="promise-desc">不同主体使用不同提示词,风景、宠物和食物不再套用人物规则。</p>
</div>
<div class="promise">
<span class="promise-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="4.5"/><circle cx="12" cy="12" r="1" fill="currentColor"/></svg>
</span>
<h3 class="promise-title">生成按次计费</h3>
<p class="promise-desc">每次成功生成扣 1 次权益,失败不扣,避免无限消耗模型费用。</p>
</div>
<div class="promise">
<span class="promise-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7 3v5c0 4.5-3 8.2-7 10-4-1.8-7-5.5-7-10V6l7-3z"/><path d="M9 12l2 2 4-4"/></svg>
</span>
<h3 class="promise-title">结果防丢失</h3>
<p class="promise-desc">未保存时锁住所有切换操作,并在关闭页面前再次提醒。</p>
</div>
</section>
</main>
<!-- ═══ FOOTER(品牌故事) ═══ -->
<footer class="footer">
<div class="footer-story">
<div class="footer-col footer-about">
<a class="brand" href="prototype-chibi.html"><span class="brand-mark">P</span><span>PERLER <b>LAB</b></span></a>
<p class="footer-desc">PERLER LAB 专注于把数字图片变成可以亲手拼制的拼豆图纸。从一张照片到一盒拼豆,我们相信每一颗颜色都有位置——让像素回到指尖,让创作回归耐心与温度。</p>
<p class="footer-slogan">让每一颗颜色都有位置。</p>
</div>
<div class="footer-col">
<h4 class="footer-h">产品</h4>
<a href="prototype-home.html">图片转拼豆</a>
<a href="prototype-chibi.html">AI 拼豆专区</a>
<a href="prototype-home.html">下载权益</a>
</div>
<div class="footer-col">
<h4 class="footer-h">帮助</h4>
<a href="#faq">常见问题</a>
<a href="#">代理合作</a>
<a href="#">联系客服</a>
</div>
</div>
<div class="footer-inner">
<span class="footer-text">PERLER LAB · AI 拼豆专区</span>
<span class="footer-note">© 2026 PERLER LAB · 图纸仅供个人手作参考</span>
<a class="footer-link" href="prototype-home.html">返回图片转拼豆 →</a>
</div>
</footer>
<!-- ═══ 移动端底部 CTA ═══ -->
<div class="mobile-bar">
<button type="button" class="btn-primary" id="mobileGenBtn" disabled style="flex:1">兑换后生成 →</button>
<button type="button" class="btn-link" id="mobileCodeBtn">输入兑换码</button>
</div>
<!-- ═══ 兑换码弹窗 ═══ -->
<div class="modal-mask" id="redeemMask" style="display:none">
<div class="modal" role="dialog" aria-modal="true" aria-label="输入兑换码">
<button type="button" class="modal-close" id="redeemClose" aria-label="关闭">×</button>
<h3 class="modal-title">输入兑换码</h3>
<input class="modal-input" id="redeemInput" placeholder="请输入兑换码" autocomplete="off">
<p class="redeem-error" id="redeemError" style="display:none"></p>
<p class="redeem-ok" id="redeemOk" style="display:none">✓ 已确认到账,兑换码已自动解锁</p>
<button type="button" class="redeem-btn" id="redeemBtn">兑换</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
/* ═══════════════════════════════════════════════════════════════
* PERLER LAB AI 专区原型 · 交互逻辑(原生 JS)
* ═══════════════════════════════════════════════════════════════ */
(function(){
'use strict';
/* ── 数据:五大类 + 细分 + 标签 (copy-library.md §8.2) ── */
var CATEGORIES = [
{ id:'landscape', name:'风景', note:'山川、城市和旅行纪念',
pal:['#2e8a5e','#8fbf6f','#3677d2','#f0c06a','#5a4632','#f4f3ef'],
subtypes:[{name:'自然山川'},{name:'城市夜景'},{name:'地标建筑'}],
tags:['突出主体建筑','减少零碎纹理','保留光影氛围','纯色边框','高对比轮廓','云端与远山'] },
{ id:'people', name:'人物', note:'五官、发型和穿搭清楚',
pal:['#edb045','#c8a06b','#e8b4a0','#8c3a2e','#171713','#f4f3ef'],
subtypes:[{name:'萌系大眼',note:'大眼、圆脸、粗轮廓'},{name:'清爽少年',note:'自然五官、利落穿搭'},{name:'复古像素',note:'怀旧配色、游戏感'}],
tags:['五官更清晰','保留衣服细节','保留发型','保留配饰','包含宠物','纯色背景'] },
{ id:'pet', name:'宠物', note:'毛色、表情和品种特征',
pal:['#edb045','#c8a06b','#8a6d4f','#f0c06a','#171713','#f4f3ef'],
subtypes:[{name:'猫咪头像',note:'圆润脸型与花纹'},{name:'狗狗全身',note:'耳朵、四肢和毛色'},{name:'双宠互动',note:'两只宠物同框'}],
tags:['眼睛更有神','保留毛色花纹','突出耳朵轮廓','保留项圈配饰','完整四肢','纯色背景'] },
{ id:'item', name:'物品', note:'手办、礼物和纪念物',
pal:['#3677d2','#e05b5b','#7a5cc2','#f0c06a','#66645d','#f4f3ef'],
subtypes:[{name:'手办玩具',note:'造型轮廓优先'},{name:'生活用品',note:'保留结构与材质'},{name:'礼物纪念',note:'突出包装和装饰'}],
tags:['保留品牌外形','突出结构轮廓','减少反光渐变','保留小配件','居中构图','纯色背景'] },
{ id:'food', name:'食物', note:'甜品、饮品和特色美食',
pal:['#e8b4d0','#d87a9e','#edb045','#3daf80','#c8a06b','#f4f3ef'],
subtypes:[{name:'甜品蛋糕',note:'奶油与装饰清晰'},{name:'咖啡饮品',note:'杯型、分层和配料'},{name:'家常美食',note:'主食材清晰可辨'}],
tags:['突出食物主体','保留食材层次','减少细碎装饰','增强色彩对比','俯拍构图','纯色背景'] }
];
var GRID_OPTIONS = [
{ value:48, hint:'简单' }, { value:64, hint:'推荐', rec:true },
{ value:80, hint:'精细' }, { value:96, hint:'高清' }
];
var COLOR_OPTIONS = [
{ value:16, hint:'省豆' }, { value:24, hint:'均衡' },
{ value:32, hint:'细腻' }, { value:40, hint:'高还原' }
];
var COMPOSITIONS = [
{ id:'特写', label:'主体特写', density:0.7 },
{ id:'半幅', label:'半幅构图', density:0.6 },
{ id:'完整', label:'完整构图', density:0.48 }
];
var MAX_TAGS = 5, MAX_PROMPT = 160, MAX_BYTES = 12*1024*1024;
/* ── 状态 ── */
var categoryId = 'pet';
var subtypeName = '猫咪头像';
var gridSize = 64, maxColors = 24, composition = '特写';
var selectedTags = [];
var promptText = '';
var hasCode = false;
var remainCount = 10;
var generating = false;
var refImage = null, originImage = null;
var $ = function(id){ return document.getElementById(id); };
function fmt(n){ return n.toLocaleString('zh-CN'); }
function toast(msg){
var t = $('toast'); t.textContent = msg; t.classList.add('is-show');
clearTimeout(toast._t); toast._t = setTimeout(function(){ t.classList.remove('is-show'); }, 2600);
}
function cat(){ for (var i=0;i<CATEGORIES.length;i++) if (CATEGORIES[i].id===categoryId) return CATEGORIES[i]; return CATEGORIES[0]; }
/* ── 8×8 像素示例(按分类配色生成) ── */
function buildPixel(c){
var pal = c.pal;
var cells = [];
// 简化:中心主体 + 边框
for (var y=0;y<8;y++) for (var x=0;x<8;x++){
var ci = 0;
var dx = x-3.5, dy = y-3.5, d = Math.sqrt(dx*dx+dy*dy);
if (d < 2.2) ci = 2; // 主体
else if (d < 3.3) ci = 1; // 阴影
else if (Math.random()<0.25) ci = 3; // 噪点
else ci = 5; // 纸色
cells.push(pal[ci]);
}
return cells;
}
function renderPixel(container, colors){
var html = '';
for (var i=0;i<colors.length;i++) html += '<span style="background:'+colors[i]+'"></span>';
container.innerHTML = html;
var spans = container.querySelectorAll('span');
for (var j=0;j<spans.length;j++) spans[j].style.animationDelay = (j*18)+'ms';
}
var pixelCache = {};
function getPixelFor(cid){
if (!pixelCache[cid]) pixelCache[cid] = buildPixel(catFromId(cid));
return pixelCache[cid];
}
function catFromId(id){ for (var i=0;i<CATEGORIES.length;i++) if (CATEGORIES[i].id===id) return CATEGORIES[i]; return CATEGORIES[0]; }
/* ── 01 分类 Tabs(滑动指示条 + 内容切换动画) ── */
function moveTabIndicator(){
var tabs = $('tabs');
var sel = tabs.querySelector('.tab.is-selected');
if (!sel) return;
var ind = tabs.querySelector('.tabs-indicator');
if (!ind){ ind = document.createElement('span'); ind.className = 'tabs-indicator'; tabs.appendChild(ind); }
var tRect = tabs.getBoundingClientRect();
var sRect = sel.getBoundingClientRect();
ind.style.left = (sRect.left - tRect.left) + 'px';
ind.style.width = sRect.width + 'px';
}
function animateEnter(el){
if (!el) return;
el.classList.remove('content-in');
void el.offsetWidth;
el.classList.add('content-in');
}
function renderTabs(){
var html = '';
for (var i=0;i<CATEGORIES.length;i++){
var c = CATEGORIES[i];
html += '<button type="button" class="tab'+(c.id===categoryId?' is-selected':'')+'" data-cat="'+c.id+'" role="tab" aria-selected="'+(c.id===categoryId)+'">' +
'<span class="tab-name">'+c.name+'</span><span class="tab-note">'+c.note+'</span></button>';
}
$('tabs').innerHTML = html;
Array.prototype.forEach.call($('tabs').querySelectorAll('.tab'), function(t){
t.addEventListener('click', function(){ selectCategory(t.dataset.cat); });
});
moveTabIndicator();
}
function renderSubtypes(){
var c = cat();
var html = '';
for (var i=0;i<c.subtypes.length;i++){
var s = c.subtypes[i];
html += '<button type="button" class="subtype'+(s.name===subtypeName?' is-selected':'')+'" data-sub="'+s.name+'">' +
'<span class="subtype-name">'+s.name+'</span>'+(s.note?'<span class="subtype-note">'+s.note+'</span>':'')+'</button>';
}
$('subtypes').innerHTML = html;
Array.prototype.forEach.call($('subtypes').querySelectorAll('.subtype'), function(b){
b.addEventListener('click', function(){ subtypeName = this.dataset.sub; renderSubtypes(); renderSpec(); });
});
}
function selectCategory(id){
if (id === categoryId) return;
categoryId = id;
var c = catFromId(id);
subtypeName = c.subtypes[0].name;
selectedTags = [];
renderTabs(); renderSubtypes(); renderTags();
renderPixel($('comparePixel'), getPixelFor(id));
renderPixel($('confirmPixel'), getPixelFor(id));
renderSpec();
animateEnter(document.querySelector('.compare'));
animateEnter($('subtypes'));
}
/* ── 02/03/04 参数选项 ── */
function renderOptionsInto(containerId, options, getKey, isSelected, onSelect){
var html = '';
for (var i=0;i<options.length;i++){
var o = options[i];
html += '<button type="button" class="option'+(isSelected(o)?' is-selected':'')+'" data-key="'+getKey(o)+'">' +
(o.rec?'<span class="recommend">推荐</span>':'') +
'<span class="option-value">'+o.value+'</span>'+(o.hint?'<span class="option-hint">'+o.hint+'</span>':'')+'</button>';
}
$(containerId).innerHTML = html;
Array.prototype.forEach.call($(containerId).querySelectorAll('.option'), function(b){
b.addEventListener('click', function(){ onSelect(getKey(options[Array.prototype.indexOf.call(b.parentNode.children,b)])); });
});
}
function renderGridOptions(){
renderOptionsInto('gridOptions', GRID_OPTIONS,
function(o){ return o.value; },
function(o){ return o.value === gridSize; },
function(v){ gridSize = Number(v); renderGridOptions(); renderSpec(); });
}
function renderColorOptions(){
renderOptionsInto('colorOptions', COLOR_OPTIONS,
function(o){ return o.value; },
function(o){ return o.value === maxColors; },
function(v){ maxColors = Number(v); renderColorOptions(); renderSpec(); });
}
function renderCompOptions(){
renderOptionsInto('compOptions', COMPOSITIONS,
function(o){ return o.id; },
function(o){ return o.id === composition; },
function(v){ composition = v; renderCompOptions(); renderSpec(); });
}
/* ── 05 提示词标签 ── */
function renderTags(){
var c = cat();
var html = '';
for (var i=0;i<c.tags.length;i++){
var tag = c.tags[i];
var sel = selectedTags.indexOf(tag) !== -1;
var dis = !sel && selectedTags.length >= MAX_TAGS;
html += '<button type="button" class="tag'+(sel?' is-selected':'')+(dis?' is-disabled':'')+'" data-tag="'+tag+'">' +
'<span class="tag-marker">'+(sel?'✓':'+')+'</span>'+tag+'</button>';
}
$('tags').innerHTML = html;
Array.prototype.forEach.call($('tags').querySelectorAll('.tag'), function(b){
b.addEventListener('click', function(){
if (this.classList.contains('is-disabled')) return;
var tag = this.dataset.tag;
var idx = selectedTags.indexOf(tag);
if (idx !== -1){ selectedTags.splice(idx,1); }
else {
if (selectedTags.length >= MAX_TAGS){
$('tagHint').textContent = '最多选 ' + MAX_TAGS + ' 项';
$('tagHint').style.display = 'block';
clearTimeout(renderTags._t); renderTags._t = setTimeout(function(){ $('tagHint').style.display='none'; }, 2000);
return;
}
selectedTags.push(tag);
}
$('tagHint').style.display = 'none';
renderTags();
renderSpec();
});
});
}
/* ── 生成前确认(扩展规格摘要) ── */
function density(){ for (var i=0;i<COMPOSITIONS.length;i++) if (COMPOSITIONS[i].id===composition) return COMPOSITIONS[i].density; return 0.7; }
function renderSpec(){
var c = cat();
var compLabel = composition;
for (var i=0;i<COMPOSITIONS.length;i++) if (COMPOSITIONS[i].id === composition) compLabel = COMPOSITIONS[i].label;
$('specCategory').textContent = c.name;
$('specSubtype').textContent = subtypeName;
$('specGrid').textContent = gridSize; $('specGrid2').textContent = gridSize;
$('specColors').textContent = maxColors;
$('specComp').textContent = compLabel;
$('specTags').textContent = selectedTags.length + ' 项';
$('specRef').textContent = refState.value ? '已上传' : '未上传';
$('specBeads').textContent = fmt(Math.round(gridSize*gridSize*density()));
}
function updateCta(){
var label = generating ? 'AI 生成中<span class="dots"><i></i><i></i><i></i></span>' : (hasCode ? '生成 →' : '兑换后生成 →');
$('genBtn').innerHTML = generating ? '<span class="spinner"></span>'+label : label;
$('mobileGenBtn').innerHTML = generating ? '<span class="spinner"></span>'+label : label;
$('genBtn').disabled = !hasCode || generating;
$('mobileGenBtn').disabled = !hasCode || generating;
$('codeLinkBtn').style.display = hasCode ? 'none' : '';
$('mobileCodeBtn').style.display = hasCode ? 'none' : '';
$('remainNote').style.display = hasCode ? '' : 'none';
if (hasCode) $('remainText').textContent = '剩余 ' + remainCount + ' 次生成权益';
}
/* ── 上传组件(参考图 / 原图 共用) ── */
function makeUpload(zoneId, inputId, state){
var zone = $(zoneId), input = $(inputId);
zone.className = 'upload-zone';
zone.innerHTML = '<span class="plus">+</span><span class="ut">'+state.title+'</span><span class="uh">'+state.hint+'</span>';
zone.addEventListener('click', function(){ input.click(); });
input.addEventListener('change', function(){
var file = this.files[0];
this.value = '';
if (!file) return;
if (!/^image\//.test(file.type)){ showUploadError(zone, '图片读取失败,请换一张重试', state); return; }
if (file.size > MAX_BYTES){ showUploadError(zone, '图片不能超过 12MB', state); return; }
var reader = new FileReader();
reader.onload = function(){
state.value = { name: file.name, url: reader.result, size: file.size };
renderUpload(zone, input, state);
};
reader.onerror = function(){ showUploadError(zone, '图片读取失败,请换一张重试', state); };
reader.readAsDataURL(file);
});
}
function showUploadError(zone, msg, state){
zone.className = 'upload-zone is-error';
zone.innerHTML = '<span class="ut">'+msg+'</span>';
setTimeout(function(){
zone.className = 'upload-zone';
zone.innerHTML = '<span class="plus">+</span><span class="ut">'+state.title+'</span><span class="uh">'+state.hint+'</span>';
}, 2200);
}
function renderUpload(zone, input, state){
if (state.value){
var kb = state.value.size < 1024 ? state.value.size + ' B' : (state.value.size/1024).toFixed(1) + ' KB';
zone.className = '';
zone.innerHTML = '';
zone.style.padding = '0';
zone.style.borderStyle = 'solid';
zone.style.cursor = 'default';
var wrap = document.createElement('div');
wrap.className = 'upload-file';
wrap.innerHTML = '<img class="upload-thumb" src="'+state.value.url+'" alt="'+state.value.name+'">' +
'<div class="upload-meta"><div class="upload-name">'+state.value.name+'</div><div class="upload-size">'+kb+' · 已就绪</div></div>' +
'<button type="button" class="upload-reset">重置</button>';
zone.appendChild(wrap);
if (typeof renderSpec === 'function') renderSpec();
zone.querySelector('.upload-reset').addEventListener('click', function(e){
e.stopPropagation();
state.value = null;
zone.style.padding = ''; zone.style.borderStyle = 'dashed'; zone.style.cursor = 'pointer';
zone.innerHTML = '<span class="plus">+</span><span class="ut">'+state.title+'</span><span class="uh">'+state.hint+'</span>';
if (typeof renderSpec === 'function') renderSpec();
});
}
}
var refState = { title:'上传参考图', hint:'支持 JPG、PNG、WEBP · 不超过 12MB', value:null };
var originState = { title:'上传需要转换的原图', hint:'主体清楚、光线自然效果更好 · 不超过 12MB', value:null };
makeUpload('refUploadZone', 'refFileInput', refState);
makeUpload('originUploadZone', 'originFileInput', originState);
/* ── 文本域计数 ── */
$('promptText').addEventListener('input', function(){
$('charCount').textContent = this.value.length;
});
/* ── 兑换码(scale + fade 过渡) ── */
function openRedeem(){
var m = $('redeemMask');
m.classList.add('is-open');
m.style.display = 'flex';
m.classList.add('is-visible');
document.body.style.overflow = 'hidden';
setTimeout(function(){ $('redeemInput').focus(); }, 60);
}
function closeRedeem(){
var m = $('redeemMask');
m.classList.remove('is-visible');
setTimeout(function(){
if (!m.classList.contains('is-visible')){ m.style.display = 'none'; m.classList.remove('is-open'); document.body.style.overflow = ''; }
}, 260);
}
$('codeLinkBtn').addEventListener('click', openRedeem);
$('mobileCodeBtn').addEventListener('click', openRedeem);
$('redeemClose').addEventListener('click', closeRedeem);
$('redeemMask').addEventListener('mousedown', function(e){ if (e.target === this) closeRedeem(); });
document.addEventListener('keydown', function(e){ if (e.key==='Escape' && $('redeemMask').style.display!=='none') closeRedeem(); });
$('redeemInput').addEventListener('input', function(){ $('redeemError').style.display='none'; });
$('redeemBtn').addEventListener('click', doRedeem);
$('redeemInput').addEventListener('keydown', function(e){ if (e.key==='Enter') doRedeem(); });
function doRedeem(){
var code = $('redeemInput').value.trim();
if (!code){
$('redeemOk').style.display = 'none';
$('redeemError').textContent = '兑换失败 / 兑换码不存在,请检查后重试';
$('redeemError').style.display = 'block';
return;
}
hasCode = true;
$('redeemOk').style.display = 'block';
$('redeemError').style.display = 'none';
setTimeout(function(){
closeRedeem();
$('redeemInput').value = '';
$('redeemOk').style.display = 'none';
updateCta();
toast('已确认到账,兑换码已自动解锁');
}, 900);
}
/* ── 生成(AI 生成中:动态圆点 + 示例图 shimmer) ── */
function doGenerate(){
if (!hasCode || generating) return;
generating = true;
updateCta();
var sample = document.querySelector('.confirm .sample');
if (sample) sample.classList.add('is-generating');
setTimeout(function(){
generating = false;
remainCount = Math.max(0, remainCount - 1);
updateCta();
renderResult();
$('resultArea').style.display = 'block';
if (sample) sample.classList.remove('is-generating');
toast('AI 生成成功,消耗 1 次权益 · 已保存');
$('resultArea').scrollIntoView({ behavior:'smooth', block:'nearest' });
}, 1800);
}
$('genBtn').addEventListener('click', doGenerate);
$('mobileGenBtn').addEventListener('click', doGenerate);
/* ── 生成结果(豆子逐个落下 + 进度扫描,占位图) ── */
function renderResult(){
var c = cat();
var canvas = $('resultCanvas');
var g = gridSize;
var cell = Math.max(3, Math.floor(480 / g));
var size = g * cell;
var dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.width = size*dpr; canvas.height = size*dpr;
canvas.style.width = size+'px'; canvas.style.height = size+'px';
var ctx = canvas.getContext('2d');
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.fillStyle = '#f4f3ef'; ctx.fillRect(0,0,size,size);
// 分类配色渐变 + blob
var pal = c.pal.slice();
var cells = [];
for (var y=0;y<g;y++) for (var x=0;x<g;x++){
var px = x/(g-1||1), py = y/(g-1||1);
var d = Math.sqrt(Math.pow(px-0.5,2)+Math.pow(py-0.5,2));
var ci;
if (d < 0.28) ci = 1;
else if (d < 0.42) ci = 0;
else if (Math.random()<0.12) ci = 2;
else ci = 5;
cells.push(ci);
}
var sweep = $('resultSweep');
if (sweep) sweep.classList.add('is-active');
var start = null, drawn = 0, total = g * g;
function frame(t){
if (start === null) start = t;
var p = Math.min(1, (t - start) / 520);
var target = Math.floor(total * p);
for (var i = drawn; i < target && i < total; i++){
var cx = i % g, cy = Math.floor(i / g);
ctx.beginPath();
ctx.arc(cx*cell+cell/2, cy*cell+cell/2, cell*0.42, 0, Math.PI*2);
ctx.fillStyle = pal[cells[i]]; ctx.fill();
}
drawn = target;
if (p < 1) requestAnimationFrame(frame);
else if (sweep) sweep.classList.remove('is-active');
}
requestAnimationFrame(frame);
$('resultMeta').textContent = c.name + ' · ' + subtypeName + ' · 图纸 ' + g + '×' + g;
$('resultName').textContent = subtypeName + ' · 默认像素效果';
$('resultSpec').textContent = g + ' × ' + g;
$('resultBeads').textContent = '实际使用 ' + fmt(g*g) + ' 颗 · 已扣除 1 次';
}
function buttonSuccess(btn, label){
if (btn.dataset.busy) return;
btn.dataset.busy = '1';
var orig = btn.innerHTML;
btn.classList.add('is-downloading');
btn.innerHTML = '<svg class="check-svg" viewBox="0 0 24 24" aria-hidden="true"><polyline points="4 12 10 18 20 6"/></svg><span>' + label + '</span>';
setTimeout(function(){ btn.innerHTML = orig; btn.classList.remove('is-downloading'); delete btn.dataset.busy; }, 1900);
}
$('saveBtn').addEventListener('click', function(){
buttonSuccess(this, '已保存');
toast('图片已保存(原型演示:长按下方图片保存到手机)');
});
$('toPlanBtn').addEventListener('click', function(){
buttonSuccess(this, '图纸已生成');
toast('转拼豆图纸已开始生成(原型演示)');
});
/* ── 全局涟漪反馈 ── */
document.addEventListener('click', function(e){
var b = e.target && e.target.closest ? e.target.closest('button') : null;
if (!b || b.disabled) return;
var rect = b.getBoundingClientRect();
var d = Math.max(rect.width, rect.height) * 1.15;
var r = document.createElement('span');
r.className = 'ripple';
r.style.width = r.style.height = d + 'px';
r.style.left = (e.clientX - rect.left - d / 2) + 'px';
r.style.top = (e.clientY - rect.top - d / 2) + 'px';
b.appendChild(r);
setTimeout(function(){ r.remove(); }, 700);
});
/* ── FAQ 手风琴 ── */
document.querySelectorAll('.faq-q').forEach(function(q){
q.addEventListener('click', function(){
var item = q.parentElement;
var willOpen = !item.classList.contains('is-open');
document.querySelectorAll('.faq-item.is-open').forEach(function(o){ if (o !== item) o.classList.remove('is-open'); });
item.classList.toggle('is-open', willOpen);
});
});
/* ── 分类指示条随窗口自适应 ── */
window.addEventListener('resize', function(){ moveTabIndicator(); });
/* ── 初始化 ── */
renderTabs();
renderSubtypes();
renderTags();
renderGridOptions();
renderColorOptions();
renderCompOptions();
renderPixel($('comparePixel'), getPixelFor(categoryId));
renderPixel($('confirmPixel'), getPixelFor(categoryId));
renderSpec();
updateCta();
})();
</script>
</body>
</html>