1068 lines
58 KiB
HTML
1068 lines
58 KiB
HTML
<!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>
|