Files
drawlogo/_design-test/build-page.js
T

990 lines
57 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 生成「甄房」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');