feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能

This commit is contained in:
sgp
2026-10-03 21:44:18 +08:00
commit 9e222877ea
497 changed files with 43611 additions and 0 deletions
+989
View File
@@ -0,0 +1,989 @@
// 生成「甄房」LOGO 素材挑选页(自包含单文件 HTML,可直接双击打开)
// 用法: node _design-test/build-page.js
const fs = require('fs');
const path = require('path');
const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'house-icons.json'), 'utf8'));
const TIERS = data.tiers;
// ── 字体清单(本地 OFL + 本机系统字体)────────────────────────────
const FONTS = JSON.parse(fs.readFileSync(path.join(__dirname, 'font-specimens.json'), 'utf8'));
const FONT_CATS = Object.keys(FONTS.cats);
const fontFaces = FONTS.fonts.filter(f => f.kind === 'ofl').map(f =>
"@font-face{font-family:'" + f.family + "';src:url('" + f.src + "') format('" +
(f.fmt === 'ttf' ? 'truetype' : (f.fmt || 'woff2')) + "');font-display:swap}"
).join('\n');
const LIC_BAD = /BY-SA|BY-NC|BY-ND|GPL|-NC-|NonCommercial|非商业/i;
// ── 「甄房」LOGO 概念方案(本次设计)──────────────────────────────
const CONCEPTS = [
{
id: 'zhen-01', name: '甄选之屋', en: 'Selected House', tag: '对勾 · 最直白',
desc: '屋顶 + 墙体 + 对勾。一套图形讲完「甄选好房」,识别度最高,缩小到 favicon 仍清晰。适合做主标。',
mode: 'stroke',
svg: '<path d="M6 30.5 L32 8 L58 30.5"/><path d="M13.5 29.5 V56 H50.5 V29.5"/><path d="M23.5 43 l7 7 L44 31.5"/>'
},
{
id: 'zhen-02', name: '檐瓦徽章', en: 'Eave Tile Badge', tag: '中式 · 圆章',
desc: '圆形徽章内一条上翘屋檐 + 两重瓦纹。「甄」本义为陶瓦甄别,此稿把字义直接画进图形,最有文化底子。',
mode: 'stroke',
svg: '<circle cx="32" cy="32" r="26"/><path d="M11 25.5 C19 15, 45 15, 53 25.5"/><path d="M19.5 34 q12.5 -7 25 0"/><path d="M19.5 43 q12.5 -7 25 0"/>'
},
{
id: 'zhen-03', name: '窗格之家', en: 'Window House', tag: '几何 · 秩序',
desc: '屋顶 + 田字窗格。窗格本身有「筛选、框选」的隐喻,结构方正,延展做栅格、水印、加载态都很顺。',
mode: 'stroke',
svg: '<path d="M7 27 L32 7.5 L57 27"/><rect x="14.5" y="27" width="35" height="29.5" rx="3.5"/><path d="M32 27 V56.5"/><path d="M14.5 41.5 H49.5"/>'
},
{
id: 'zhen-04', name: '印章 · 甄', en: 'Seal Zhen', tag: '汉字 · 印鉴',
desc: '圆角方印 + 甄字。最贴近「甄房」二字的字面表达,气质正统稳重,适合政务、金融类房产业务线。',
mode: 'mixed',
svg: '<rect x="5.5" y="5.5" width="53" height="53" rx="11"/>' +
'<text x="32" y="43.5" text-anchor="middle" font-size="31" font-weight="600" font-family="Songti SC,STSong,SimSun,serif" fill="currentColor" stroke="none">甄</text>'
},
{
id: 'zhen-05', name: '钥匙之家', en: 'Key House', tag: '门锁 · 交付',
desc: '实心屋形挖出钥匙孔。指向「交付钥匙 = 成交」,适合交易、租售、代办过户等业务。',
mode: 'stroke',
svg: '<path d="M7 28.5 L32 7.5 L57 28.5 V56 H7 Z"/><circle cx="32" cy="34" r="6.5"/><path d="M32 40.5 V51"/>'
},
{
id: 'zhen-06', name: '找房定位', en: 'Find A Home', tag: '定位 · 找房',
desc: '地图定位针内嵌小屋。天然表达「在你想要的位置找到房」,适合 C 端找房 App、地图找房入口。',
mode: 'stroke',
svg: '<path d="M32 5 C22.5 5 15 12.5 15 22 c0 11.5 17 29 17 29 s17 -17.5 17 -29 C49 12.5 41.5 5 32 5 z"/><path d="M23.5 22.5 L32 15.5 L40.5 22.5"/><path d="M26.5 22.5 V30 H37.5 V22.5"/>'
},
{
id: 'zhen-07', name: '联排双屋', en: 'Twin Houses', tag: '多房源 · 平台',
desc: '两栋联排小屋并列。强调「房源多、成规模」,适合房产平台、小区/楼盘库这类主标。',
mode: 'stroke',
svg: '<path d="M4 34 L17 23 L30 34"/><path d="M8 33 V56 H26 V33"/>' +
'<path d="M34 34 L47 23 L60 34"/><path d="M38 33 V56 H56 V33"/>'
},
{
id: 'zhen-08', name: '层叠甄选', en: 'Layered Pick', tag: '抽象 · 现代',
desc: '屋顶下三层递减横线,像层层筛选的房源列表,也呼应「甄」字的多横结构。最现代、最像互联网品牌。',
mode: 'stroke',
svg: '<path d="M6 25 L32 7 L58 25"/><path d="M13 32.5 H51"/><path d="M18 40.5 H46"/><path d="M23 48.5 H41"/>'
}
];
// ── 第二批:home-04 外框 + 字母 Z(直接用 home-04 原始路径,外框几何固定)────
// 外框 = 素材库 hugeicons:home-04(MIT)的原始 path,按 24→64 网格等比放大(stroke 同步放大,粗细比例与原图一致)
// 唯一改动:去掉原图底部那条内部横线(M16 17H8)——它正好落在 Z 的位置,且不属于「外框」
const H04 = 'M3 11.99v2.51c0 3.3 0 4.95 1.025 5.975S6.7 21.5 10 21.5h4c3.3 0 4.95 0 5.975-1.025S21 17.8 21 14.5v-2.51c0-1.682 0-2.522-.356-3.25s-1.02-1.244-2.346-2.276l-2-1.555C14.233 3.303 13.2 2.5 12 2.5s-2.233.803-4.298 2.409l-2 1.555C4.375 7.496 3.712 8.012 3.356 8.74S3 10.308 3 11.99';
const FG = 'translate(5.3304 5.3294) scale(2.22222)'; // 24 网格 → 64 网格,外框落位 12~52 / 10.9~53.1,中心 (32,32)
const INK = '#0C2340', GOLD = '#C6A15B', WH = '#FFFFFF'; // #0C2340 = 定稿色板「甄房蓝 900」
// 定稿字形:Forte 的 Z(已由 fontTools 抽成真实曲线,见 logo/glyphs.json)
const GLYPH_FILE = path.join(__dirname, 'logo', 'glyphs.json');
const GLYPHS = fs.existsSync(GLYPH_FILE) ? JSON.parse(fs.readFileSync(GLYPH_FILE, 'utf8')) : {};
const FORTEZ = GLYPHS.forteZ ? GLYPHS.forteZ.d : '';
const ZCX = GLYPHS.forteZ ? GLYPHS.forteZ.placed[0] + GLYPHS.forteZ.placed[2] / 2 : 32; // Z 着墨中心 x
// 水平拉伸(只拉宽、不动高):绕 Z 自身中心缩放,宽度倍数 sx
const wideZ = (sx) => '<g transform="translate(' + ZCX + ' 0) scale(' + sx + ' 1) translate(-' + ZCX + ' 0)"><path d="' +
FORTEZ + '" fill="' + INK + '"/></g>';
const frame = (sw, color) => '<g transform="' + FG + '"><path d="' + H04 + '" fill="none" stroke="' + color +
'" stroke-width="' + sw + '" stroke-linecap="round" stroke-linejoin="round"/></g>';
const ZSTD = 'M21.5 30 H42.5 V35.4 L30 42.1 H42.5 V47.5 H21.5 V42.1 L34 35.4 H21.5 Z'; // 实心方块 Z(深底反白用)
const ZSKEL = 'M23 31.5 H41 L23 46 H41'; // 圆角 Z 骨架
// Z 统一字面框:x 21.5~42.5、y 30~47.5(各款按笔画宽自动内缩,保证字面大小一致)
const ZB = { x0: 21.5, x1: 42.5, y0: 30, y1: 47.5 };
// 骨架式 Z:给定笔画宽度与端点,自动算出中心线端点,使描边后的外沿正好落进字面框
function zline(w, o) {
o = o || {};
var caps = o.caps || 'butt', pad = caps === 'round' ? w / 2 : 0;
var x0 = ZB.x0 + w / 2 + pad, x1 = ZB.x1 - w / 2 - pad;
var y0 = ZB.y0 + w / 2 + pad, y1 = ZB.y1 - w / 2 - pad;
return '<path d="M' + x0 + ' ' + y0 + ' H' + x1 + ' L' + x0 + ' ' + y1 + ' H' + x1 +
'" fill="none" stroke="' + (o.color || INK) + '" stroke-width="' + w +
'" stroke-linecap="' + caps + '" stroke-linejoin="' + (o.join || 'miter') + '"/>';
}
const CONCEPTS_FRAME = [
// ── 已定稿 ───────────────────────────────────────────
...(FORTEZ ? [{
id: 'zf-final', star: 2, name: 'Forte Z', en: 'Final', tag: '已定稿 ✓',
desc: '你选定的最终字形:Forte 的大写 Z。已用 fontTools 从字体里抽出真实曲线(不是位图描摹),所以不依赖字体环境即可使用;完整资产(SVG + 36 个位图 + 色板)见顶部「定稿交付」链接。',
mode: 'solid',
svg: frame(1.5, INK) + '<path d="' + FORTEZ + '" fill="' + INK + '"/>'
},
{
id: 'zf-w115', name: 'Forte Z · 拉宽 15%', en: 'Wide 115', tag: '水平拉伸 ×1.15',
desc: '外框与字高完全不动,只把 Z 水平拉宽 15%。最克制的一档,比原版略舒展,几乎看不出被拉过——想要「稳」就选它。',
mode: 'solid',
svg: frame(1.5, INK) + wideZ(1.15)
},
{
id: 'zf-w130', name: 'Forte Z · 拉宽 30%', en: 'Wide 130', tag: '水平拉伸 ×1.30',
desc: '明显变宽的一档:横画被拉长、字腔更开阔,整体更「阔气」。放在方框里左右留白更窄,视觉重心更稳,是我建议优先考虑的一档。',
mode: 'solid',
svg: frame(1.5, INK) + wideZ(1.30)
},
{
id: 'zf-w145', name: 'Forte Z · 拉宽 45%', en: 'Wide 145', tag: '水平拉伸 ×1.45',
desc: '宽体感最强的一档,接近专业字体的「extended」字重观感。注意:水平拉伸会让竖画显厚、斜画变缓,再往上就会开始显胖。',
mode: 'solid',
svg: frame(1.5, INK) + wideZ(1.45)
}] : []),
// ── 保留三款 ─────────────────────────────────────────
{
id: 'zf-k1', star: 1, name: '深底反白', en: 'Dark Reverse', tag: '已保留 · 深色场景',
desc: '深墨蓝圆角底 + 白框白 Z。深色界面、App 图标、深色名片用这一版。',
mode: 'solid',
svg: '<rect x="5" y="5" width="54" height="54" rx="13" fill="' + INK + '"/>' +
frame(1.5, WH) + '<path d="' + ZSTD + '" fill="' + WH + '"/>'
},
{
id: 'zf-k2', star: 1, name: '圆角 Z', en: 'Round Z', tag: '已保留 · 亲和',
desc: 'Z 用 5.2 圆头描边,转角全圆,跟圆润的框最统一。',
mode: 'solid',
svg: frame(1.5, INK) +
'<path d="' + ZSKEL + '" fill="none" stroke="' + INK + '" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>'
},
{
id: 'zf-k3', star: 1, name: '线框 Z', en: 'Line Z', tag: '已保留 · 轻',
desc: 'Z 用 3.6 描边空心字,整体透气,设计工作室调性。',
mode: 'solid',
svg: frame(1.5, INK) +
'<path d="' + ZSKEL + '" fill="none" stroke="' + INK + '" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>'
},
// ── 新增 Z 字形(外框不动,只换 Z)──────────────────────
{
id: 'zf-01', name: '锐折 Z', en: 'Sharp Z', tag: '尖角 · 硬朗',
desc: '3.6 等宽细线,但转角是尖的(miter 直角),端点平切。比圆角版硬朗、比实心版轻,最有「精密工具」气质。',
mode: 'solid',
svg: frame(1.5, INK) + zline(3.6, { caps: 'butt', join: 'miter' })
},
{
id: 'zf-02', name: '方头粗 Z', en: 'Square Bold', tag: '几何 · 压得住',
desc: '5.4 粗线、方头方角,像 Helvetica 的骨架直接画出来。远近都清楚,做门头招牌没问题。',
mode: 'solid',
svg: frame(1.5, INK) + zline(5.4, { caps: 'butt', join: 'miter' })
},
{
id: 'zf-03', name: '细线 Z', en: 'Hairline', tag: '极细 · 高级',
desc: '笔画只有 2.4,跟外框的细线等重。整个标看起来像一张线描稿,适合高端、克制的物料。',
mode: 'solid',
svg: frame(1.5, INK) + zline(2.4, { caps: 'butt', join: 'miter' })
},
{
id: 'zf-04', name: '超粗 Z', en: 'Heavy', tag: '最重 · 冲击力',
desc: '笔画 6.6,几乎把框内填满。远看是一个厚重的方块符号,适合做车贴、导视、单色印章。',
mode: 'solid',
svg: frame(1.5, INK) + zline(6.6, { caps: 'butt', join: 'miter' })
},
{
id: 'zf-05', name: '宽体 Z', en: 'Extended', tag: '宽扁 · 稳',
desc: '字面拉宽到 27(比标准宽 28%),笔画 4.4。宽扁的字给人「稳、可信」的心理暗示,适合金融/政务向。',
mode: 'solid',
svg: frame(1.5, INK) + '<path d="M20.7 32.2 H43.3 L20.7 45.3 H43.3" fill="none" stroke="' + INK +
'" stroke-width="4.4" stroke-linecap="butt" stroke-linejoin="miter"/>'
},
{
id: 'zf-06', name: '窄体 Z', en: 'Condensed', tag: '瘦高 · 利落',
desc: '字面收到 13(标准的一半多点),笔画 5.4。瘦高的字显精致、有设计感,也留出更多呼吸。',
mode: 'solid',
svg: frame(1.5, INK) + '<path d="M28.2 32.7 H35.8 L28.2 44.8 H35.8" fill="none" stroke="' + INK +
'" stroke-width="5.4" stroke-linecap="butt" stroke-linejoin="miter"/>'
},
{
id: 'zf-07', name: '曲线斜画', en: 'Curved Z', tag: '柔 · 流动',
desc: '把 Z 的斜画改成一条 S 弯。硬邦邦的字母一下有了流动感,和外框的圆弧线条也更呼应。',
mode: 'solid',
svg: frame(1.5, INK) + '<path d="M23.3 31.8 H40.7 C33.5 36, 30.5 41.5, 23.3 45.7 H40.7" fill="none" stroke="' + INK +
'" stroke-width="3.6" stroke-linecap="butt" stroke-linejoin="round"/>'
},
{
id: 'zf-08', name: '断笔 Z', en: 'Stencil', tag: '断开 · 有节奏',
desc: '三笔全断开,像模板喷字。笔画的间隙让这个 Z 有了节奏感,也更容易在低精度场景下不糊成一团。',
mode: 'solid',
svg: frame(1.5, INK) +
'<path d="M23.3 32.2 H40.7" fill="none" stroke="' + INK + '" stroke-width="3.4" stroke-linecap="butt"/>' +
'<path d="M38.6 36.2 L25.4 42.6" fill="none" stroke="' + INK + '" stroke-width="3.4" stroke-linecap="butt"/>' +
'<path d="M23.3 46.6 H40.7" fill="none" stroke="' + INK + '" stroke-width="3.4" stroke-linecap="butt"/>'
},
{
id: 'zf-09', name: '空心字 Z', en: 'Hollow', tag: '真·空心',
desc: '不是用线画的,是把一个实心 Z 内部挖空——所以横竖有真实的粗细与字腔,放大看是很正经的字母设计。',
mode: 'solid',
svg: frame(1.5, INK) +
'<path fill-rule="evenodd" fill="' + INK + '" d="M21.5 30 H42.5 V35.4 L30 42.1 H42.5 V47.5 H21.5 V42.1 L34 35.4 H21.5 Z ' +
'M25.5 33 H38.5 V35.6 L30.5 41.9 H38.5 V44.5 H25.5 V41.9 L33.5 35.6 H25.5 Z"/>'
},
{
id: 'zf-10', name: '勾尾 Z', en: 'Check Tail', tag: '甄选 · 语义',
desc: 'Z 的底横末端收笔上扬成一个勾,等于把「甄选/通过」这层意思藏进了字母里。形义双关,是这批里最有故事的一稿。',
mode: 'solid',
svg: frame(1.5, INK) +
'<path d="M23.3 31.8 H40.7 L23.3 45.7 H32 L39.5 38.2" fill="none" stroke="' + INK +
'" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round"/>'
},
{
id: 'zf-11', name: '粗细对比 Z', en: 'Contrast', tag: '衬线感 · 优雅',
desc: '横画加粗到 6、斜画压细到 2.3,做出传统衬线字的粗细对比。优雅、有书卷气,适合线下高端品牌。',
mode: 'solid',
svg: frame(1.5, INK) +
'<path fill="' + INK + '" d="M21.5 30 H42.5 V36 L29 41.5 H42.5 V47.5 H21.5 V41.5 H23 L36.5 36 H21.5 Z"/>'
},
{
id: 'zf-12', name: '金线 Z', en: 'Gold Line', tag: '墨蓝 + 金',
desc: '线框 Z 的配色版:框留墨蓝、Z 换金色 #C6A15B。比实心金 Z 更含蓄,烫金、压凹工艺都能做。',
mode: 'solid',
svg: frame(1.5, INK) + zline(3.6, { caps: 'butt', join: 'miter', color: GOLD })
}
];
// ── 数据瘦身 + 预置检索字段 ──────────────────────────────────────
const items = data.list.map((it, i) => ({
i: i,
id: it.id, n: it.name, t: it.tier, s: it.source, sn: it.sourceName,
ty: it.type, lic: it.license, bad: LIC_BAD.test(it.license || '') ? 1 : 0,
svg: it.svg
}));
const sources = {};
items.forEach(x => { sources[x.s] = (sources[x.s] || 0) + 1; });
const sourceList = Object.keys(sources).sort((a, b) => sources[b] - sources[a]);
const payload = {
builtAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
total: items.length,
sourceCount: sourceList.length,
tiers: TIERS.map(t => ({ key: t.key, label: t.label, desc: t.desc })),
sources: sourceList.map(id => ({ id: id, count: sources[id], name: (items.find(x => x.s === id) || {}).sn || id })),
items: items
};
const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>甄房 ZhenFang · LOGO 素材挑选台</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0a0d13; --panel:#111621; --panel2:#161c29; --line:rgba(255,255,255,.08);
--tx:#e9edf5; --mut:#8b94a7; --dim:#5d6577;
--gold:#c9a961; --gold2:#e6cd93; --ok:#4ade80;
--serif:"Songti SC","STSong","SimSun","Noto Serif SC",serif;
}
body{font-family:"PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;background:var(--bg);color:var(--tx);
-webkit-font-smoothing:antialiased;padding:0 0 80px}
a{color:var(--gold)}
.wrap{max-width:1560px;margin:0 auto;padding:0 28px}
/* ── Hero ── */
.hero{padding:52px 0 34px;border-bottom:1px solid var(--line);
background:radial-gradient(900px 320px at 18% -10%,rgba(201,169,97,.16),transparent 60%)}
.hero h1{font-size:34px;letter-spacing:1px}
.hero h1 b{font-family:var(--serif);font-weight:600;letter-spacing:6px;
background:linear-gradient(100deg,var(--gold2),var(--gold));-webkit-background-clip:text;background-clip:text;color:transparent}
.hero .en{color:var(--dim);letter-spacing:7px;font-size:11px;margin-top:8px}
.hero p.lead{color:var(--mut);font-size:13.5px;margin-top:16px;line-height:1.85;max-width:900px}
.kpis{display:flex;gap:34px;margin-top:24px;flex-wrap:wrap}
.kpi b{display:block;font-size:24px;color:var(--gold2);font-weight:600}
.kpi span{font-size:11.5px;color:var(--mut)}
h2.sec{font-size:19px;margin:46px 0 6px;display:flex;align-items:center;gap:10px}
h2.sec:before{content:"";width:4px;height:18px;border-radius:2px;background:linear-gradient(180deg,var(--gold2),var(--gold))}
.secsub{color:var(--mut);font-size:12.5px;margin-bottom:20px;line-height:1.7}
/* ── 方案 ── */
.concepts{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:18px}
.cc{background:linear-gradient(180deg,var(--panel2),var(--panel));border:1px solid var(--line);border-radius:16px;
padding:20px;display:flex;gap:18px;transition:.2s;position:relative;overflow:hidden}
.cc:hover{border-color:rgba(201,169,97,.42);transform:translateY(-2px)}
.cc .mark{width:88px;height:88px;flex:none;border-radius:20px;display:flex;align-items:center;justify-content:center;
background:linear-gradient(150deg,#f7f3e9,#efe7d5);box-shadow:0 8px 22px rgba(0,0,0,.35)}
.cc .mark svg{width:58px;height:58px;color:#1d2431}
.cc.solid .mark{background:#fff;box-shadow:0 6px 18px rgba(0,0,0,.38),0 0 0 1px rgba(0,0,0,.06) inset}
.cc.solid .mark svg{width:62px;height:62px}
code{background:#0d1220;border:1px solid var(--line);border-radius:5px;padding:1px 5px;font-size:11px;color:var(--gold2)}
.star{color:var(--gold)}
.cc .body{flex:1;min-width:0}
.cc .nm{font-size:15.5px;font-weight:600;display:flex;align-items:center;gap:8px}
.cc .nm .star{color:var(--gold);font-size:12px;line-height:1}
.cc .nm em{font-style:normal;font-size:10px;color:var(--gold);border:1px solid rgba(201,169,97,.4);
padding:1px 7px;border-radius:20px;font-weight:400}
.cc .lock{display:flex;align-items:baseline;gap:9px;margin:9px 0 8px}
.cc .lock .zh{font-family:var(--serif);font-size:27px;letter-spacing:5px;font-weight:600}
.cc .lock .en2{font-size:9.5px;letter-spacing:3.5px;color:var(--dim)}
.cc .ds{font-size:12px;color:var(--mut);line-height:1.75}
.cc .acts{margin-top:11px;display:flex;gap:8px}
.cc .acts button{font-size:11px;padding:4px 10px;border-radius:7px;border:1px solid var(--line);
background:#0d1220;color:var(--mut);cursor:pointer}
.cc .acts button:hover{color:var(--gold2);border-color:rgba(201,169,97,.45)}
/* ── 挑选台 ── */
.picker{display:grid;grid-template-columns:1fr 348px;gap:22px;align-items:start;margin-top:8px}
.filters{position:sticky;top:0;z-index:9;background:linear-gradient(180deg,var(--bg) 72%,rgba(10,13,19,0));
padding:12px 0 14px;display:flex;flex-direction:column;gap:11px}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
input[type=search],select{background:#0d1220;border:1px solid var(--line);color:var(--tx);
border-radius:9px;padding:8px 12px;font-size:12.5px;outline:none;font-family:inherit}
input[type=search]{flex:1;min-width:220px}
input[type=search]:focus,select:focus{border-color:rgba(201,169,97,.5)}
.tabs{display:flex;gap:7px;flex-wrap:wrap}
.tab{padding:6px 13px;border-radius:20px;border:1px solid var(--line);background:#0d1220;color:var(--mut);
font-size:12px;cursor:pointer;white-space:nowrap;transition:.15s}
.tab:hover{color:var(--tx)}
.tab.on{background:linear-gradient(120deg,var(--gold),#a8873f);color:#17130a;border-color:transparent;font-weight:600}
.tab i{font-style:normal;opacity:.62;margin-left:5px;font-size:10.5px}
.chk{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--mut);cursor:pointer;user-select:none}
.chk input{accent-color:var(--gold)}
.cnt{font-size:11.5px;color:var(--dim);margin-left:auto}
input[type=color]{width:34px;height:30px;padding:0;border:1px solid var(--line);border-radius:8px;background:#0d1220;cursor:pointer}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:10px}
.ic{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:11px 7px 8px;cursor:pointer;
display:flex;flex-direction:column;align-items:center;gap:7px;transition:.14s;position:relative}
.ic:hover{border-color:rgba(201,169,97,.5);background:var(--panel2)}
.ic.on{border-color:var(--gold);box-shadow:0 0 0 1px var(--gold) inset}
.ic .bx{width:52px;height:52px;display:flex;align-items:center;justify-content:center;color:#c3cbd9}
.ic .bx.multi{background:#f5f2ea;border-radius:12px}
.ic.on .bx{color:var(--gold2)}
.ic .lb{font-size:9.5px;color:var(--mut);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ic .lic{position:absolute;top:5px;right:6px;font-size:8px;color:#f0a55c;display:none}
.ic.bad .lic{display:block}
.ic .fav{position:absolute;top:3px;left:4px;font-size:11px;line-height:1;opacity:0;transition:.15s}
.ic:hover .fav,.ic.faved .fav{opacity:1}
.more{grid-column:1/-1;text-align:center;padding:22px 0}
.more button{padding:10px 26px;border-radius:10px;border:1px solid var(--line);background:var(--panel);
color:var(--mut);cursor:pointer;font-size:12.5px;font-family:inherit}
.more button:hover{color:var(--gold2);border-color:rgba(201,169,97,.45)}
.empty{grid-column:1/-1;text-align:center;color:var(--dim);padding:60px 0;font-size:13px}
/* ── 详情面板 ── */
.detail{position:sticky;top:14px;background:linear-gradient(180deg,var(--panel2),var(--panel));
border:1px solid var(--line);border-radius:16px;padding:18px}
.detail h3{font-size:13px;color:var(--mut);font-weight:500;margin-bottom:13px;display:flex;justify-content:space-between}
.prev{display:flex;gap:10px;justify-content:center;margin-bottom:14px}
.prev .p{width:82px;height:82px;border-radius:19px;display:flex;align-items:center;justify-content:center}
.prev .p.round{border-radius:50%}
.prev .p.square{border-radius:9px}
.lockup{display:flex;align-items:center;gap:11px;padding:14px;border-radius:12px;background:#0d1220;
border:1px solid var(--line);margin-bottom:13px}
.lockup .bd{width:50px;height:50px;border-radius:13px;display:flex;align-items:center;justify-content:center;flex:none}
.lockup .tw .zh{font-family:var(--serif);font-size:23px;letter-spacing:4px;font-weight:600;line-height:1.1}
.lockup .tw .en3{font-size:8.5px;letter-spacing:3px;color:var(--dim);margin-top:3px}
.meta{font-size:11px;color:var(--mut);line-height:1.95;word-break:break-all}
.meta b{color:var(--dim);font-weight:400}
.meta .warn{color:#f0a55c}
.dacts{display:flex;gap:7px;flex-wrap:wrap;margin-top:13px}
.dacts button{flex:1;min-width:82px;padding:8px 4px;border-radius:9px;border:1px solid var(--line);
background:#0d1220;color:var(--mut);font-size:11.5px;cursor:pointer;font-family:inherit}
.dacts button:hover{color:var(--gold2);border-color:rgba(201,169,97,.45)}
.dacts button.pri{background:linear-gradient(120deg,var(--gold),#a8873f);color:#17130a;border-color:transparent;font-weight:600}
.ph{color:var(--dim);font-size:12px;text-align:center;padding:34px 0;line-height:1.9}
.faved-list{max-height:150px;overflow:auto;margin-top:11px;border-top:1px solid var(--line);padding-top:11px}
.faved-list .fi{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:11px;color:var(--mut)}
.faved-list .fi svg{width:18px;height:18px;flex:none;color:#c3cbd9}
.faved-list .fi span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.faved-list .fi em{cursor:pointer;color:var(--dim);font-style:normal}
.toast{position:fixed;left:50%;bottom:34px;transform:translateX(-50%) translateY(16px);opacity:0;
background:#1b2130;border:1px solid rgba(201,169,97,.4);color:var(--gold2);padding:10px 20px;
border-radius:11px;font-size:12.5px;pointer-events:none;transition:.22s;z-index:99}
.toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
footer{margin-top:52px;border-top:1px solid var(--line);padding-top:22px;color:var(--dim);font-size:11.5px;line-height:2}
@media(max-width:1080px){.picker{grid-template-columns:1fr}.detail{position:static}}
/* ── 字体样本 ── */
.fsgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(198px,1fr));gap:12px}
.fs{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:12px 10px 11px;text-align:center;
cursor:pointer;transition:.14s}
.fs:hover{border-color:rgba(201,169,97,.45);background:var(--panel2);transform:translateY(-2px)}
.fs .tile{background:#fff;border-radius:12px;height:126px;display:flex;align-items:center;justify-content:center;
margin-bottom:10px;box-shadow:0 4px 12px rgba(0,0,0,.28)}
.fs .fn{font-size:11.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fs .fc{font-size:9.5px;color:var(--mut);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fs .fl{display:inline-block;font-size:9px;padding:1px 7px;border-radius:20px;margin-top:6px}
.fs .fl.ofl{color:#5ee08a;border:1px solid rgba(94,224,138,.35)}
.fs .fl.sys{color:#f0a55c;border:1px solid rgba(240,165,92,.3)}
${fontFaces}
</style>
</head>
<body>
<div class="hero"><div class="wrap">
<h1>「甄房」LOGO · <b>素材挑选台</b></h1>
<div class="en">Z H E N F A N G &nbsp;·&nbsp; L O G O &nbsp; P I C K E R</div>
<p class="lead">
第一步:从你的设计项目素材库(画个标 · 共享图标库 222 源 / 37 万图标)里,把跟「房子」相关的素材全部捞了出来,
去重、分层、按风格精选成 <b style="color:var(--gold2)">${items.length}</b> 款可挑选图形;<br>
第二步:基于这批素材的造型语言,我另外设计了两批「甄房」LOGO 概念方案(下方 ${8 + CONCEPTS_FRAME.length} 稿,均为原创矢量,可直接用)——
第一批 8 稿走<b style="color:var(--gold2)">线条 / 中式 / 印章</b>路线;第二批 ${CONCEPTS_FRAME.length} 稿是<b style="color:var(--gold2)">home-04 房子外框 + Z 字形</b>(外框一路不动,只换 Z 的字形与笔画);
第三步:Z 的字形不再手画,直接扫你机器上已装的<b style="color:var(--gold2)">${FONTS.total} 款真字体</b>(素材库 OFL 12 款 + 系统字体 ${FONTS.total - 12} 款)渲染对比(第三节)。
</p>
<div class="kpis">
<div class="kpi"><b>${items.length}</b><span>精选房子类素材</span></div>
<div class="kpi"><b>${sourceList.length}</b><span>覆盖图标库源</span></div>
<div class="kpi"><b>${8 + CONCEPTS_FRAME.length}</b><span>甄房 LOGO 方案</span></div>
<div class="kpi"><b>${FONTS.total}</b><span>可试 Z 的字体</span></div>
<div class="kpi"><b>${payload.builtAt}</b><span>素材生成时间</span></div>
</div>
<div style="margin-top:26px;background:linear-gradient(120deg,rgba(198,161,91,.13),rgba(12,35,64,.55));
border:1px solid rgba(198,161,91,.42);border-radius:14px;padding:16px 18px;
display:flex;align-items:center;gap:16px;flex-wrap:wrap">
<span style="font-size:13.5px;color:#E3CB99;font-weight:600">✓ 定稿交付已完成</span>
<span style="font-size:12.5px;color:var(--mut)">Forte Z + 甄房蓝(主色 #0C2340)|10 个矢量资产 + 36 个位图 + 完整色板规范</span>
<a href="logo/index.html" target="_blank" style="margin-left:auto;font-size:12.5px;color:#0C2340;
background:linear-gradient(120deg,#E3CB99,#C6A15B);padding:8px 18px;border-radius:9px;
text-decoration:none;font-weight:600">打开定稿交付页 →</a>
<a href="logo/brush-z.html" target="_blank" style="font-size:12.5px;color:#2a1f06;
background:linear-gradient(120deg,#F3D98F,#E0B95C);padding:8px 18px;border-radius:9px;
text-decoration:none;font-weight:700">★ 原创笔刷 Z(版权归你)→</a>
<a href="logo/ofl-brush.html" target="_blank" style="font-size:12.5px;color:#0a2c19;
background:linear-gradient(120deg,#8fe8ad,#4fd07f);padding:8px 18px;border-radius:9px;
text-decoration:none;font-weight:600">OFL 可商用字体试排 →</a>
</div>
</div></div>
<div class="wrap">
<h2 class="sec">一、甄房 LOGO 概念方案 · 第一批(线条 / 中式 / 印章 · 8 稿)</h2>
<div class="secsub">
取名「甄房」:甄,本义为制陶甄别、去伪存真,引申为<b style="color:var(--gold2)">甄选</b>;房即房源。
因此所有方案都围绕「房子 + 甄选」两条线索做叠加。每稿都附合体字标(甄房 / ZHENFANG),可直接定为终稿方向。
</div>
<div class="concepts" id="concepts"></div>
<h2 class="sec">二、home-04 外框 + Z 字形探索(12 款新字形 + 3 款已留)</h2>
<div class="secsub">
外框固定不动 = <code>hugeicons:home-04</code>(MIT)原图路径等比放大,形状/比例/线宽全部不动。<b style="color:var(--gold2)">这一节只换里面的 Z</b>:字面统一锁在 21×17.5 的字面框内,各款按笔画宽度自动内缩,所以 12 款的字面大小一致、只有字形不同。<br>
<span class="star">★</span> 标记的三款是你已选定保留的(深底反白 / 圆角 Z / 线框 Z),放在最前面方便对照。
</div>
<div class="concepts" id="concepts-frame"></div>
<h2 class="sec">三、Z 字母 · 字体精选(${FONTS.total} 款)</h2>
<div class="secsub">
不再手画字形——直接用<b style="color:var(--gold2)">真字体</b>渲染 Z。外框依旧是 home-04 原图(不动),Z 用各款字体的大写 Z,并<b style="color:var(--gold2)">等比放大到填满同一个字面框(21.5 × 20)</b>——与你保留的三款(Z 约 21 宽 × 19.7 高、竖直中心 38.75)取同一比例,所以不存在「大小不一」的干扰,只看字形本身的宽窄与笔画性格。<br>
<span style="color:#5ee08a">●</span> 本地素材库字体(OFL-1.1,<b>可直接商用</b>)&nbsp;&nbsp;
<span style="color:#f0a55c">●</span> 你本机已安装的系统字体(<b>仅作预览,商用需自行取得授权</b>)<br>
点任意一张卡片即可复制它的 SVG。注意:SVG 里的 Z 是<b>活文字</b>(依赖字体),定稿后我可以把你选中的那一款转成曲线路径。
</div>
<div class="row" style="margin-bottom:14px">
<div class="tabs" id="fontcats"></div>
<label class="chk"><input type="checkbox" id="onlyOfl"> 只看可商用(OFL)</label>
<span class="cnt" id="fontcnt"></span>
</div>
<div class="fsgrid" id="fsgrid"></div>
<h2 class="sec">四、房子类素材挑选(${items.length} 款 · 点击即预览成标)</h2>
<div class="secsub">
分五层:<b>房屋本体</b>(做主图形首选)、<b>门户构件</b>(屋顶/门窗/钥匙)、<b>楼宇建筑</b>(楼宇/公共建筑/城市)、
<b>房产交易</b>(成交/租售/服务)、<b>品牌标志</b>(知名品牌 Logo,仅作参考,有商标风险)。
点任意一款 → 右侧实时看它套上「甄房」字标的效果,可换主色、换形状、复制/下载 SVG。
</div>
<div class="picker">
<div>
<div class="filters">
<div class="row">
<input type="search" id="q" placeholder="搜索名称 / 来源,如 house、key、villa、lucide…">
<label class="chk"><input type="checkbox" id="onlyFav"> 只看已收藏</label>
<span class="cnt" id="cnt"></span>
</div>
<div class="tabs" id="tabs"></div>
<div class="row">
<select id="src"></select>
<select id="ty">
<option value="">全部样式</option>
<option value="icon">单色(可换主色)</option>
<option value="multi">彩色(保留原色)</option>
</select>
<select id="shape">
<option value="squircle">形状:圆角方</option>
<option value="round">形状:圆形</option>
<option value="square">形状:方形</option>
</select>
<label class="chk">主色 <input type="color" id="brand" value="#1f6feb"></label>
<select id="mode">
<option value="light">底:浅色</option>
<option value="dark" selected>底:深色</option>
</select>
</div>
</div>
<div class="grid" id="grid"></div>
</div>
<div class="detail" id="detail"></div>
</div>
<footer>
素材来源:画个标(DrawLogo)共享图标库 · 按 manifest/icons.json 的 source 定向读取后本地提取,未改动原库任何文件。<br>
协议提示:标记 <span style="color:#f0a55c">⚠</span> 的素材为 BY-SA / BY-NC / BY-ND / GPL 等限制性协议,商用前请替换或单独确认;常用 MIT / Apache-2.0 / ISC / CC BY 4.0 可商用(建议署名)。<br>
「品牌标志」层为第三方注册商标,仅作造型参考,<b style="color:#f0a55c">不得直接用作甄房 LOGO</b>。<br>
第二节外框造型取自 <code>hugeicons:home-04</code>(MIT)原图路径,未改动其形状;框内 Z 字全部为原创几何,可放心用于甄房。<br>
生成时间 ${payload.builtAt} · 由 _design-test/extract-house.js + build-page.js 生成
</footer>
</div>
<div class="toast" id="toast"></div>
<script>
window.HOUSE = ${JSON.stringify(payload).replace(/</g, '\\u003c')};
</script>
<script>
(function(){
var D = window.HOUSE;
var ITEMS = D.items;
/* ── 图标渲染(复刻画个标判定 + 按真实颜色纠偏,保证深底可见)── */
var STROKE_SRC = {'feather':1,'lucide':1,'tabler':1};
// 是否真彩色素材(含明显彩度/亮色)→ 保留原色;否则按单色参与换色
// 注意:<defs>(mask/渐变定义)里的颜色不代表成品配色,先剥离再判定
function isVivid(svg){
var s = svg.replace(/<defs[\\s\\S]*?<\\/defs>/g, '');
var hex = s.match(/#[0-9a-fA-F]{3,8}/g) || [], i, v, r, g, b, mx, mn;
for(i = 0; i < hex.length; i++){
v = hex[i].slice(1);
if(v.length === 3) v = v[0]+v[0]+v[1]+v[1]+v[2]+v[2];
if(v.length >= 6 && /^[0-9a-f]+$/i.test(v.substr(0,6))){
r = parseInt(v.substr(0,2),16); g = parseInt(v.substr(2,2),16); b = parseInt(v.substr(4,2),16);
mx = Math.max(r,g,b); mn = Math.min(r,g,b);
if((mx - mn) > 24 || mx > 96) return true;
}
}
return false;
}
function isMono(it){ return !isVivid(it.svg); }
// 素材内色值平均亮度(<defs> 不计)
function avgLum(svg){
var s = svg.replace(/<defs[\\s\\S]*?<\\/defs>/g, '');
var hex = s.match(/#[0-9a-fA-F]{3,8}/g) || [], tot = 0, n = 0, i, v, r, g, b;
for(i = 0; i < hex.length; i++){
v = hex[i].slice(1);
if(v.length === 3) v = v[0]+v[0]+v[1]+v[1]+v[2]+v[2];
if(v.length < 6 || !/^[0-9a-f]+$/i.test(v.substr(0,6))) continue;
r = parseInt(v.substr(0,2),16); g = parseInt(v.substr(2,2),16); b = parseInt(v.substr(4,2),16);
tot += (0.2126*r + 0.7152*g + 0.0722*b) / 255; n++;
}
return n ? tot / n : 0.5;
}
// 彩色素材若主打深色(深色品牌色)→ 需浅色衬底;主打亮色/白色 → 深色卡片底即可
function needsChip(it){ return !isMono(it) && avgLum(it.svg) < 0.45; }
function isStroke(it){
if(!isMono(it)) return false;
if(STROKE_SRC[it.s]) return true;
var b = it.svg;
if(/stroke-width="0"/.test(b)) return false;
// 极小 scale 包装(512 系坐标缩到 24,如 selfhst / antd):描边会被同比缩成发丝 → 强制走填充
var sc = b.match(/scale\\(([\\d.]+)\\)/);
if(sc && parseFloat(sc[1]) < 0.12) return false;
// 填充型信号:evenodd 填充规则且元素级无描边(semi-icons 等,误判描边会整图不可见)
if(/fill-rule\\s*=\\s*"evenodd"/.test(b) && !/stroke\\s*=\\s*"(?!none")[^"]*"/.test(b)) return false;
if(/stroke\\s*=|stroke-linecap\\s*=|stroke-linejoin\\s*=/.test(b)) return true;
var g = b.match(/<(?:path|circle|rect|ellipse|line|polyline|polygon)\\b[^>]*>/g);
if(g && g.length && !/fill\\s*=\\s*"(?!none")[^"]*"/.test(b)) return true;
return false;
}
// 深色硬编码 + currentColor 统一换成主题色(按亮度判定,覆盖 #0c1221 / #231f20 这类非纯黑深色)
function themed(body, color){
return body.replace(/currentColor/gi, color).replace(/#[0-9a-fA-F]{3,8}/g, function(h){
var v = h.slice(1);
if(v.length === 3) v = v[0]+v[0]+v[1]+v[1]+v[2]+v[2];
if(v.length < 6 || !/^[0-9a-f]+$/i.test(v.substr(0,6))) return h;
var r = parseInt(v.substr(0,2),16), g = parseInt(v.substr(2,2),16), b = parseInt(v.substr(4,2),16);
return (0.299*r + 0.587*g + 0.114*b) < 96 ? color : h;
});
}
function iconSvg(it, color, size, w){
var mono = isMono(it);
if(!mono) return '<svg viewBox="0 0 24 24" width="'+size+'" height="'+size+'" xmlns="http://www.w3.org/2000/svg">'+it.svg+'</svg>';
var body = themed(it.svg, color);
var attrs = isStroke(it)
? 'fill="none" stroke="'+color+'" stroke-width="'+(w||1.9)+'" stroke-linecap="round" stroke-linejoin="round"'
: 'fill="'+color+'" stroke="none"';
return '<svg viewBox="0 0 24 24" width="'+size+'" height="'+size+'" xmlns="http://www.w3.org/2000/svg" '+attrs+'>'+body+'</svg>';
}
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
/* ── 配色 ── */
var S = {
tier:'all', q:'', src:'', ty:'', onlyFav:false,
brand:'#1f6feb', mode:'light', shape:'squircle',
active:null, fav:{}
};
function brandBg(){
return S.mode==='dark' ? '#111827' : S.brand;
}
function multiBg(){
return '#f5f2ea'; // 深色作品专用浅色衬底
}
/* ── 方案渲染(两批:线条原创 / 实心几何)── */
var G1 = ${JSON.stringify(CONCEPTS).replace(/</g, '\\u003c')};
var G2 = ${JSON.stringify(CONCEPTS_FRAME).replace(/</g, '\\u003c')};
function conceptSvg(c){
if(c.mode === 'solid') return '<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">'+c.svg+'</svg>';
var sw = c.mode === 'mixed' ? 3.4 : 3.6;
return '<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" '+
'stroke-width="'+sw+'" stroke-linecap="round" stroke-linejoin="round">'+c.svg+'</svg>';
}
function conceptStandalone(c){
if(c.mode === 'solid') return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="512" height="512">'+c.svg+'</svg>';
var sw = c.mode === 'mixed' ? 3.4 : 3.6;
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="512" height="512" fill="none" '+
'stroke="#1f6feb" stroke-width="'+sw+'" stroke-linecap="round" stroke-linejoin="round">'+c.svg+'</svg>';
}
function renderConcepts(hostId, list){
var host = document.getElementById(hostId);
host.innerHTML = list.map(function(c){
return '<div class="cc'+(c.mode === 'solid' ? ' solid' : '')+'" data-cid="'+c.id+'">'+
'<div class="mark">'+conceptSvg(c)+'</div>'+
'<div class="body">'+
'<div class="nm">'+(c.star ? '<span class="star">'+(c.star > 1 ? '★★' : '★')+'</span>' : '')+esc(c.name)+'<em>'+esc(c.tag)+'</em></div>'+
'<div class="lock"><span class="zh">甄房</span><span class="en2">ZHENFANG</span></div>'+
'<div class="ds">'+esc(c.desc)+'</div>'+
'<div class="acts"><button data-act="dl">下载 SVG</button><button data-act="copy">复制代码</button></div>'+
'</div></div>';
}).join('');
host.addEventListener('click', function(e){
var b = e.target.closest('button'); if(!b) return;
var c = list.filter(function(x){return x.id === b.closest('.cc').dataset.cid;})[0];
if(b.dataset.act === 'dl') download('zhenfang-'+c.id+'.svg', conceptStandalone(c));
else copy(conceptStandalone(c), '已复制 '+c.name+' 的 SVG 代码');
});
}
renderConcepts('concepts', G1);
renderConcepts('concepts-frame', G2);
/* ── Z 字母字体样本(真字体渲染 + 统一字高归一化)── */
var FG2 = '${FG}', H04P = '${H04}', INKC = '${INK}';
var FT = ${JSON.stringify(FONTS.fonts).replace(/</g, '\\u003c')};
var FCTS = ${JSON.stringify(FONT_CATS)};
var fCat = 'all', onlyOfl = false;
function famCss(f){ return "'" + String(f.family).replace(/'/g, '') + "', sans-serif"; }
function frameTag(){
return '<g transform="' + FG2 + '"><path d="' + H04P + '" fill="none" stroke="' + INKC +
'" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g>';
}
function specimenSvg(f, size, tf){
return '<svg viewBox="0 0 64 64" width="' + size + '" height="' + size + '" xmlns="http://www.w3.org/2000/svg">' +
frameTag() + '<text x="32" y="47.5" text-anchor="middle" font-family="' + famCss(f) + '" font-size="25" fill="' + INKC + '"' +
(tf ? ' transform="' + tf + '"' : '') + '>Z</text></svg>';
}
var fsgrid = document.getElementById('fsgrid');
function renderFonts(){
var list = FT.filter(function(f){
if(onlyOfl && f.kind !== 'ofl') return false;
if(fCat !== 'all' && f.cat !== fCat) return false;
return true;
});
fsgrid.innerHTML = list.map(function(f){
var badge = f.kind === 'ofl' ? '<span class="fl ofl">OFL · 可商用</span>' : '<span class="fl sys">系统字体 · 需授权</span>';
return '<div class="fs" data-s="' + f.slug + '" title="' + esc(f.family) + ' — ' + esc(f.cn) + '">' +
'<div class="tile">' + specimenSvg(f, 116) + '</div>' +
'<div class="fn">' + esc(f.family) + '</div>' +
'<div class="fc">' + esc(f.cn) + '</div>' + badge + '</div>';
}).join('');
document.getElementById('fontcnt').textContent = '共 ' + list.length + ' 款';
fitZ();
}
// 字形着墨度量:SVG <text> 的 getBBox / 命中测试拿到的都是「行盒」,不是字形,
// 只有 canvas measureText 的 actualBoundingBox* 才是真正的着墨范围(对系统字体与 @font-face 都有效)
var _mctx = document.createElement('canvas').getContext('2d');
function inkMetrics(family, px){
_mctx.font = px + 'px "' + String(family).replace(/"/g, '') + '", sans-serif';
var m = _mctx.measureText('Z');
if(!m || (!m.actualBoundingBoxAscent && !m.actualBoundingBoxDescent)) return null;
var asc = m.actualBoundingBoxAscent || 0, desc = m.actualBoundingBoxDescent || 0;
var w = (m.actualBoundingBoxLeft || 0) + (m.actualBoundingBoxRight || 0);
if(!w) w = m.width;
return { asc: asc, desc: desc, w: w, h: asc + desc }; // 25px 测量值 ⇔ viewBox 用户单位(text 的 font-size=25 即 25 用户单位)
}
var FMAP = {};
FT.forEach(function(f){ FMAP[f.slug] = f; });
function fitOne(card){
var f = FMAP[card.getAttribute('data-s')];
var t = card.querySelector('.tile svg text');
if(!f || !t) return;
var mk = inkMetrics(f.family, 25); // 以 25px 测量,比例与字号无关
if(!mk || mk.h < 2) return;
// 目标:等比填满 21.5 × 20 的字面框(与保留三款同比例),哪一边先贴死就以哪边为准
var k = Math.min(21.5 / mk.w, 20 / mk.h);
if(k > 8) k = 8;
if(k < 0.1) k = 0.1;
// 让字形竖直中心落在 38.75(与保留款一致):反推基线最终位置
var baseFinal = 38.75 + (mk.asc - mk.desc) * k / 2;
var dy = baseFinal - 47.5;
t.setAttribute('transform', 'translate(0 ' + dy + ') translate(32 47.5) scale(' + k + ') translate(-32 -47.5)');
card.setAttribute('data-fit', '1');
}
function fitZ(){
var cards = fsgrid.querySelectorAll('.fs');
for(var i = 0; i < cards.length; i++) fitOne(cards[i]);
}
fsgrid.addEventListener('click', function(e){
var c = e.target.closest('.fs'); if(!c) return;
var f = FT.filter(function(x){ return x.slug === c.dataset.s; })[0];
var tf = c.querySelector('text').getAttribute('transform') || '';
copy(specimenSvg(f, 512, tf), '已复制 ' + f.family + ' 的 Z(活文字,依赖该字体)');
});
var fcats = document.getElementById('fontcats');
fcats.innerHTML = ['all'].concat(FCTS).map(function(c){
return '<div class="tab' + (c === 'all' ? ' on' : '') + '" data-k="' + esc(c) + '">' +
(c === 'all' ? '全部' : esc(c)) + '</div>';
}).join('');
fcats.onclick = function(e){
var t = e.target.closest('.tab'); if(!t) return;
fCat = t.dataset.k;
[].forEach.call(fcats.children, function(x){ x.classList.toggle('on', x === t); });
renderFonts();
};
document.getElementById('onlyOfl').onchange = function(e){ onlyOfl = e.target.checked; renderFonts(); };
renderFonts();
function refitAll(){ // 字体异步加载完成后重测(首次可能量到回退字体)
var cs = fsgrid.querySelectorAll('.fs');
for(var i = 0; i < cs.length; i++) cs[i].removeAttribute('data-fit');
fitZ();
}
if(document.fonts && document.fonts.ready){ document.fonts.ready.then(refitAll); }
if(document.fonts && document.fonts.addEventListener){ document.fonts.addEventListener('loadingdone', refitAll); }
setTimeout(refitAll, 700); setTimeout(refitAll, 2000);
/* ── 过滤 ── */
function filtered(){
var q = S.q.trim().toLowerCase();
return ITEMS.filter(function(it){
if(S.tier !== 'all' && it.t !== S.tier) return false;
if(S.src && it.s !== S.src) return false;
if(S.ty === 'icon' && !isMono(it)) return false;
if(S.ty === 'multi' && isMono(it)) return false;
if(S.onlyFav && !S.fav[it.id]) return false;
if(q && (it.n+' '+it.id+' '+it.sn).toLowerCase().indexOf(q) < 0) return false;
return true;
});
}
/* ── 网格分批渲染 ── */
// 坐标自适应:少数素材(slds 等)原始坐标不是 24 系,直接套 24 viewBox 会缩成小点
function autofit(root){
var svgs = root.querySelectorAll('svg:not([data-fit])');
for(var i = 0; i < svgs.length; i++){
var svg = svgs[i], bb;
svg.setAttribute('data-fit', '1');
try{ bb = svg.getBBox(); }catch(e){ continue; }
if(!bb || !bb.width || !bb.height) continue;
var mx = Math.max(bb.width, bb.height);
if(mx >= 12 && mx <= 26) continue;
var k = 18 / mx;
if(k > 8) k = 8;
if(k < 0.4) k = 0.4;
var cx = bb.x + bb.width / 2, cy = bb.y + bb.height / 2;
var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('transform', 'translate(' + (12 - cx * k) + ' ' + (12 - cy * k) + ') scale(' + k + ')');
while(svg.firstChild) g.appendChild(svg.firstChild);
svg.appendChild(g);
}
}
var shown = 0, list = [], BATCH = 120;
var grid = document.getElementById('grid');
function renderGrid(reset){
if(reset){ list = filtered(); shown = 0; grid.innerHTML = ''; }
var frag = document.createDocumentFragment();
var end = Math.min(shown + BATCH, list.length);
for(var k = shown; k < end; k++){
var it = list[k];
var mono = isMono(it);
var d = document.createElement('div');
d.className = 'ic' + (it.bad ? ' bad' : '') + (S.fav[it.id] ? ' faved' : '') + (S.active && S.active.id === it.id ? ' on' : '');
d.dataset.id = it.id;
d.title = it.n + ' · ' + it.sn + (it.lic ? ' · ' + it.lic : '');
d.innerHTML = '<span class="lic">⚠</span><span class="fav">'+(S.fav[it.id]?'★':'☆')+'</span>'+
'<div class="bx'+(needsChip(it)?' multi':'')+'">'+iconSvg(it, '#c3cbd9', mono?40:34, 1.7)+'</div>'+
'<div class="lb">'+esc(it.n)+'</div>';
frag.appendChild(d);
}
shown = end;
grid.appendChild(frag);
autofit(grid);
var old = grid.querySelector('.more'); if(old) old.remove();
if(shown < list.length){
var m = document.createElement('div');
m.className = 'more';
m.innerHTML = '<button>加载更多(已显示 '+shown+' / '+list.length+')</button>';
m.querySelector('button').onclick = function(){ renderGrid(false); };
grid.appendChild(m);
}
if(!list.length){ grid.innerHTML = '<div class="empty">没有匹配的素材,换个关键词或清掉筛选试试。</div>'; }
document.getElementById('cnt').textContent = '筛选结果 ' + list.length + ' 款';
}
grid.addEventListener('click', function(e){
var card = e.target.closest('.ic'); if(!card) return;
var it = ITEMS.filter(function(x){return x.id === card.dataset.id;})[0];
if(e.target.classList.contains('fav')){
if(S.fav[it.id]) delete S.fav[it.id]; else S.fav[it.id] = it;
card.classList.toggle('faved');
card.querySelector('.fav').textContent = S.fav[it.id] ? '★' : '☆';
if(S.onlyFav) renderGrid(true);
renderDetail(); return;
}
S.active = it;
[].forEach.call(grid.querySelectorAll('.ic.on'), function(c){ c.classList.remove('on'); });
card.classList.add('on');
renderDetail();
if(window.innerWidth > 1080) document.getElementById('detail').scrollIntoView({block:'nearest'});
});
/* ── 详情面板 ── */
function badge(B, size){
var bg = needsChip(B) ? multiBg() : brandBg();
var ico = iconSvg(B, '#ffffff', Math.round(size * 0.6), 1.9);
return '<div class="p '+S.shape+'" style="width:'+size+'px;height:'+size+'px;background:'+bg+'">'+ico+'</div>';
}
function lockup(B){
var bg = needsChip(B) ? multiBg() : brandBg();
return '<div class="lockup">'+
'<div class="bd" style="background:'+bg+'">'+iconSvg(B, '#ffffff', 30, 1.9)+'</div>'+
'<div class="tw"><div class="zh">甄房</div><div class="en3">ZHENFANG</div></div></div>';
}
function renderDetail(){
var el = document.getElementById('detail');
var B = S.active;
var nFav = Object.keys(S.fav).length;
if(!B){
el.innerHTML = '<h3>实时预览<span>'+(nFav?('已收藏 '+nFav):'')+'</span></h3>'+
'<div class="ph">← 从左侧点选任意一款房子素材<br>这里会实时合成「甄房」标<br><br>'+
'<span style="color:var(--dim);font-size:11px">可换主色 / 形状 / 底色,一键复制或下载 SVG</span></div>'+
favBlock();
bindFav(el); autofit(el); return;
}
var multi = !isMono(B);
el.innerHTML = '<h3>实时预览<span>'+(nFav?('已收藏 '+nFav):'')+'</span></h3>'+
'<div class="prev">'+badge(B, 82)+'</div>'+
lockup(B)+
'<div class="meta">'+
'<div><b>名称</b> '+esc(B.n)+'</div>'+
'<div><b>标识</b> '+esc(B.id)+'</div>'+
'<div><b>来源</b> '+esc(B.sn)+'('+esc(B.s)+')</div>'+
'<div><b>协议</b> <span class="'+(B.bad?'warn':'')+'">'+esc(B.lic||'未标注')+(B.bad?' ⚠ 限制性协议,商用需确认':'')+'</span></div>'+
(multi?'<div class="warn">多色素材:保留原色,不随主色变化</div>':'')+
'</div>'+
'<div class="dacts">'+
'<button class="pri" data-a="fav">'+(S.fav[B.id]?'★ 已收藏':'☆ 收藏')+'</button>'+
'<button data-a="copy">复制 SVG</button>'+
'<button data-a="dl">下载图形</button>'+
'<button data-a="dl2">下载带底标</button>'+
'</div>'+
favBlock();
bindFav(el); autofit(el);
el.querySelector('[data-a=fav]').onclick = function(){
if(S.fav[B.id]) delete S.fav[B.id]; else S.fav[B.id] = B;
renderGrid(true); renderDetail();
};
el.querySelector('[data-a=copy]').onclick = function(){ copy(svgFlat(B), '已复制 '+B.n+' 的 SVG'); };
el.querySelector('[data-a=dl]').onclick = function(){ download('zhenfang-icon-'+B.s+'-'+B.n+'.svg', svgFlat(B)); };
el.querySelector('[data-a=dl2]').onclick = function(){ download('zhenfang-logo-'+B.s+'-'+B.n+'.svg', svgBadge(B)); };
}
// 纯图形 SVG(24 坐标,可换色)
function svgFlat(B){
var multi = !isMono(B);
if(multi) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512">'+B.svg+'</svg>';
var body = themed(B.svg, S.brand);
var attrs = isStroke(B)
? 'fill="none" stroke="'+S.brand+'" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"'
: 'fill="'+S.brand+'" stroke="none"';
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512" '+attrs+'>'+body+'</svg>';
}
// 带底标 SVG(512 画布,含底色形状 + 白色图形)
function svgBadge(B){
var multi = !isMono(B);
var bg = needsChip(B) ? multiBg() : brandBg();
var bgShape = S.shape === 'round'
? '<circle cx="256" cy="256" r="256" fill="'+bg+'"/>'
: '<rect x="0" y="0" width="512" height="512" rx="'+(S.shape === 'square' ? 40 : 110)+'" fill="'+bg+'"/>';
var ico;
if(multi){
ico = '<g transform="translate(128 128) scale(10.6667)">'+B.svg+'</g>';
} else if(isStroke(B)){
ico = '<g transform="translate(128 128) scale(10.6667)" fill="none" stroke="#fff" stroke-width="1.9" '+
'stroke-linecap="round" stroke-linejoin="round">'+themed(B.svg, '#fff')+'</g>';
} else {
ico = '<g transform="translate(128 128) scale(10.6667)" fill="#fff" stroke="none">'+themed(B.svg, '#fff')+'</g>';
}
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">'+bgShape+ico+'</svg>';
}
function favBlock(){
var ks = Object.keys(S.fav);
if(!ks.length) return '';
return '<div class="faved-list">'+ks.map(function(k){
var it = S.fav[k];
return '<div class="fi" data-id="'+esc(k)+'">'+iconSvg(it,'#c3cbd9',18,1.7)+'<span>'+esc(it.n)+'</span><em>×</em></div>';
}).join('')+'</div>';
}
function bindFav(el){
el.querySelectorAll('.faved-list .fi').forEach(function(row){
row.querySelector('em').onclick = function(){
delete S.fav[row.dataset.id];
if(S.active && S.active.id === row.dataset.id) {} // 保留预览
renderGrid(true); renderDetail();
};
});
}
/* ── 工具 ── */
function toast(m){ var t = document.getElementById('toast'); t.textContent = m; t.classList.add('on');
clearTimeout(t._t); t._t = setTimeout(function(){ t.classList.remove('on'); }, 1700); }
function copy(text, msg){
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(text).then(function(){ toast(msg); }, function(){ fallback(text, msg); });
} else fallback(text, msg);
}
function fallback(text, msg){
var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta);
ta.select(); try{ document.execCommand('copy'); toast(msg); }catch(e){ toast('复制失败,请手动选择'); }
document.body.removeChild(ta);
}
function download(name, text){
var b = new Blob([text], {type:'image/svg+xml'});
var a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = name;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(function(){ URL.revokeObjectURL(a.href); }, 1500);
toast('已下载 ' + name);
}
/* ── 筛选控件 ── */
var tabs = document.getElementById('tabs');
var countByTier = {all: ITEMS.length};
ITEMS.forEach(function(it){ countByTier[it.t] = (countByTier[it.t]||0) + 1; });
tabs.innerHTML = [{key:'all',label:'全部'}].concat(D.tiers).map(function(t){
var key = t.key || 'all';
return '<div class="tab'+(key==='all'?' on':'')+'" data-k="'+key+'">'+esc(t.label)+'<i>'+(countByTier[key]||0)+'</i></div>';
}).join('');
tabs.onclick = function(e){
var t = e.target.closest('.tab'); if(!t) return;
S.tier = t.dataset.k;
[].forEach.call(tabs.children, function(c){ c.classList.toggle('on', c === t); });
renderGrid(true);
};
document.getElementById('src').innerHTML = '<option value="">全部来源('+D.sourceCount+')</option>' +
D.sources.map(function(s){ return '<option value="'+esc(s.id)+'">'+esc(s.name)+' · '+s.count+'</option>'; }).join('');
document.getElementById('q').oninput = function(e){ S.q = e.target.value; renderGrid(true); };
document.getElementById('src').onchange = function(e){ S.src = e.target.value; renderGrid(true); };
document.getElementById('ty').onchange = function(e){ S.ty = e.target.value; renderGrid(true); };
document.getElementById('onlyFav').onchange = function(e){ S.onlyFav = e.target.checked; renderGrid(true); };
document.getElementById('brand').oninput = function(e){ S.brand = e.target.value; renderGrid(false); renderDetail(); };
document.getElementById('mode').onchange = function(e){ S.mode = e.target.value; renderDetail(); };
document.getElementById('shape').onchange = function(e){ S.shape = e.target.value; renderDetail(); };
renderGrid(true);
renderDetail();
})();
</script>
</body>
</html>`;
fs.writeFileSync(path.join(__dirname, 'zhenfang-logo-picker.html'), html);
console.log('已生成:', path.join(__dirname, 'zhenfang-logo-picker.html'), (Buffer.byteLength(html) / 1024).toFixed(0) + 'KB');