生成式设计在个人主页的落地实践
在个人主页中引入「生成式设计系统」,让每一次访问能产出一套独特且自洽的视觉语言——配色、字体、排版、装饰层、阴影、圆角、间距,皆由单一伪随机种子确定性推导而来。
缘起:从「固定页面」到「生成规则」
传统静态主页的视觉语言在部署那一刻即被固化:设计师在 Figma 中定稿配色、字重、间距、圆角,工程师将其翻译为 CSS 变量与类名,部署后便不再变化。这种「设计即交付物」的模式虽稳妥,却也意味着访客每次看到的都是同一张面孔。
生成式设计(Generative Design)提供了另一条路径:不再交付固定的视觉结果,而是交付一套「生成规则」与「参数空间」。访问者每次打开页面、每次点击「重掷」、每次在种子栏输入任意字符,系统都依据同一套确定性算法、从一颗种子出发,即时演算出一套完整、自洽、可用的设计系统——内容固定,样式即时生长。
本文记录该生成式设计系统在个人主页中的落地要点、核心算法与工程落地时的关键决策,供同类实践参考。
核心架构:种子 → 伪随机流 → 设计令牌 → CSS 变量
整个生成链路可拆解为四个阶段,数据单向流动,无循环依赖:
Seed (string) ──► Hash (FNV-1a) ──► PRNG (Mulberry32) ──► Design Tokens ──► CSS Custom Properties │ │ │ │ │ 用户输入/随机 统一成 32-bit 确定性伪随机流 调色板/字体/间距/ 注入 :root, 种子字符串 无符号整数 (可复现、可遍历) 阴影/圆角/装饰层 即时生效1. 种子规范化与哈希:FNV-1a 32-bit
种子来源不受控:可能是用户手输的中文、Emoji、空字符串、极长字符串,也可能是 Math.random() 产出的数字字符串。首要任务是将任意字符串稳定映射为 32-bit 无符号整数,作为 PRNG 的初始状态。
// FNV-1a 32-bit:极简、分布均匀、无依赖function hashStr(str) { var s = String(str == null ? "" : str); var h = 2166136261 >>> 0; // FNV offset basis for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); // FNV prime } // 末轮混淆:让空串、单字符、数字字符串等边界情况碰撞概率进一步降低 h ^= s.length + 1; h = Math.imul(h ^ (h >>> 16), 2246822507) >>> 0; h = Math.imul(h ^ (h >>> 13), 3266489909) >>> 0; return (h ^ (h >>> 16)) >>> 0;}工程要点:
- 统一按字符串处理:
"012" ≠ "12" ≠ 12,避免隐式类型转换导致的语义歧义; - 空字符串合法:作为「无种子/随机种子」的显式表示,哈希后仍得到确定值;
Math.imul保证 32-bit 乘法溢出语义与 C/C++ 一致,跨引擎行为一致;- 末轮混淆(两轮
xorshift + multiply)大幅降低短串、近似串的碰撞概率。
2. 确定性伪随机数生成器:Mulberry32
哈希值仅作为初始状态,后续需源源不断的均匀分布随机数。选用 Mulberry32——单状态、周期 2³²、零分配、极简实现:
function mulberry32(seed) { return function () { var t = (seed += 0x6D2B79F5) >>> 0; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };}为何不选 Math.random()?
| 维度 | Math.random() | Mulberry32 |
|---|---|---|
| 可复现性 | ❌ 无法指定种子 | ✅ 同种子必产出同序列 |
| 状态隔离 | ❌ 全局共享 | ✅ 实例级隔离,互不干扰 |
| 规范一致性 | ❌ 引擎实现不一 | ✅ 算法固定,跨环境一致 |
| 体积 | 内置 | ~10 行 ES5 代码 |
工程要点:
- 仅在
applySeed()时实例化一次rand = mulberry32(seedToU32(seed)),随后所有随机决策均调用rand(); - 提供
pick(arr)、range(min, max)等语义化封装,业务代码零直接依赖数值分布细节。
3. 调色板生成:HSL 空间中的约束求解
配色是视觉系统的骨架。生成器在 HSL 空间构建调色板,再通过 对比度约束求解 保证可读性:
function genPalette() { var baseHue = range(0, 360); // 基准色相 var offsets = pick([ // 配色策略:单色/类似/互补/三角色/分裂互补 [0, 0, 0], [0, 30, -30], [0, 180, 180], [0, 120, 240], [0, 150, 210] ]); var hues = offsets.map(function (o) { return (baseHue + o + 360) % 360; }); var sat = range(42, 88); // 饱和度带 var dark = rand() < 0.52; // 暗/亮模式 52% 概率暗色
// 背景/表层/前景三层基础色 if (dark) { bg = { h: hues[0], s: sat * 0.32, l: range(6, 12) }; surface = { h: hues[0], s: sat * 0.3, l: bg.l + range(5, 9) }; fg = { h: hues[0], s: sat * 0.12, l: range(90, 96) }; } else { bg = { h: hues[0], s: sat * 0.4, l: range(93, 98) }; surface = rand() < 0.55 ? { h: hues[0], s: sat * 0.25, l: 99 } : { h: bg.h, s: bg.s, l: bg.l }; fg = { h: hues[0], s: sat * 0.45, l: range(10, 18) }; }
// 强调色与第二强调色 accent = { h: hues[1], s: sat, l: range(44, 58) }; accent2 = { h: hues[2], s: sat * 0.85, l: range(40, 60) };
// 对比度守卫:逐步调整亮度直到满足 WCAG AA ensureContrast(fg, bg, 4.5); ensureContrast(accent, surface, 3.5); ensureContrast(accent, bg, 2.8);
// 衍生色:muted / dim muted = { h: fg.h, s: fg.s * 0.45, l: dark ? range(58, 68) : range(38, 48) }; dim = { h: fg.h, s: fg.s * 0.3, l: dark ? range(40, 50) : range(55, 65) };
return { bg, surface, fg, accent, accent2, muted, dim, dark };}关键决策:
- HSL 而非 sRGB/Lab:色相旋转直观、饱和度/亮度线性插值符合设计直觉、CSS
hsl()原生支持、无需色彩管理库; - 策略化配色而非纯随机:五种经典配色策略(单色、类似、互补、三角色、分裂互补)离散采样,避免「全随机」产生的不协调感;
- 对比度迭代修正:
ensureContrast()在保持色相、饱和度不变的前提下,仅调整亮度,以最小视觉偏移换取可读性达标(WCAG AA 4.5:1 / 3:1 分级); - 暗色模式概率偏向:
rand() < 0.52让暗色略占优势,符合开发者/设计师主页的审美惯性。
4. 设计令牌全谱生成
调色板仅是色彩维度。完整设计系统还需字体、排版、布局、装饰、阴影、圆角、边框等正交维度的令牌,全部在 genDesign() 中一次性生成并写入 :root:
function genDesign() { var p = genPalette(); var root = document.documentElement.style; var set = function (k, v) { root.setProperty(k, v); };
// 色彩令牌 set("--bg", css(p.bg)); set("--surface", css(p.surface)); set("--fg", css(p.fg)); set("--muted", css(p.muted)); set("--dim", css(p.dim)); set("--accent", css(p.accent)); set("--accent2", css(p.accent2)); set("--accent-a", "0.14");
// 字体族:Serif/Display、Sans/Body、Mono 三轴各备选 7~5 组 set("--font-display", pick(DISPLAY_FONTS)); set("--font-body", pick(BODY_FONTS)); // font-mono 固定为系统等宽栈,保证代码片段可读性
// 排版比例与微调 set("--scale", pick([1.2, 1.25, 1.333, 1.414, 1.5])); // 模块化比例 set("--name-factor", range(0.88, 1.45).toFixed(2)); // 英雄区标题缩放 set("--tracking", pick(["-0.04em", "-0.02em", "0em", "0.03em", "0.08em"])); set("--tag-weight", pick([500, 600, 700, 800]));
// 布局对齐与容器宽度 var align = pick(["left", "left", "center"]); // 左对齐权重 2/3 set("--align", align); set("--links-justify", align === "center" ? "center" : "flex-start"); document.body.setAttribute("data-align", align); // CSS 选择器联动 set("--container", pick([ "min(88vw, 560px)", "min(90vw, 680px)", "min(92vw, 820px)", "min(94vw, 920px)" ]));
// 间距、圆角、边框、阴影 set("--space", range(1.05, 1.85).toFixed(2) + "rem"); set("--radius", pick([0, 0, 6, 10, 14, 20, 999]) + "px"); // 0 与 999 各 1/7 概率 set("--border-w", pick([0, 1, 1, 2, 2, 3]) + "px");
var shadowType = pick(["none", "soft", "soft", "hard"]); if (shadowType === "soft") { set("--shadow", "0 " + (range(10, 26) | 0) + "px " + (range(28, 56) | 0) + "px hsl(0 0% 0% / " + (p.dark ? 0.35 : 0.14) + ")"); } else if (shadowType === "hard") { set("--shadow", (range(4, 9) | 0) + "px " + (range(4, 9) | 0) + "px 0 " + css(p.fg)); } else { set("--shadow", "none"); }
// 装饰层:纯色/渐变/点阵/网格/斜纹/几何体/大字/光晕 genDecoration(p);}正交性保证:每个令牌独立采样,互不制约。例如 --radius 与 --shadow 无耦合,--font-display 与 --scale 无耦合。这种「参数空间正交采样」虽可能产出极端组合(如 radius=999px + shadow=hard),但正是生成式设计的张力所在——极端即个性,常态即舒适。
5. 装饰层程序化生成
body::before 仅提供基础噪点纹理。genDecoration(palette) 在运行时动态挂载 .deco 元素,构建八种风格迥异的背景层:
| 类型 | 实现要点 | 视觉特征 |
|---|---|---|
plain | 纯色背景 | 极简、聚焦内容 |
gradient | 双色线性渐变,随机角度 | 方向性、层次感 |
glow | 双径向渐变叠加,位置/半径/不透明度随机 | 深度、氛围感 |
dots | radial-gradient 点阵,background-size 控制间距 | 复古印刷/半调感 |
grid | 正交线性渐变十字叠加 | 瑞士风/技术感 |
diagonal | repeating-linear-gradient(45deg...) | 动态、节奏 |
shapes | 4~8 个随机几何体(圆/环/方/三角),随机位置/旋转/透明度 | 抽象、生成艺术感 |
bigword | 超大号显示字体,极低不透明度,旋转偏移 | 编辑性、品牌感 |
实现细节:
- 所有
.deco统一position: fixed; pointer-events: none; z-index: 0; will-change: transform, opacity,不干扰内容层交互与滚动; - 切换种子前
body.querySelectorAll(".deco").forEach(el => el.remove())清理旧层,避免 DOM 堆积; bigword文案从预设词表抽样("HI","HELLO","你好","WELCOME","★","CODE","YUER"),中英混排体现双语语境。
交互层:种子栏与骰子动画
生成式系统若无交互入口,便退化为「随机主题切换器」。种子栏 让访客成为共创者:
<div class="seedbar" id="seedbar" data-stage="seed"> <span class="seedbar-prefix" id="seed-prefix">seed</span> <input class="seedbar-input" id="seed-input" type="text" spellcheck="false" autocomplete="off" placeholder="任意字符…" /> <button type="button" class="seedbar-copy" id="seed-copy">copy</button> <button type="button" class="seedbar-btn" id="reroll" aria-label="reroll"> <span class="die" id="die-face">🎲</span> </button></div>交互逻辑
| 触发 | 行为 |
|---|---|
| 页面首次加载 | applySeed(randomSeed()),随机种子落地 |
| 用户输入种子并回车/失焦 | applySeed(input.value, { animate: true }),平滑过渡 |
| 点击 🎲 重掷 | playDiceRoll(nextSeed, () => applySeed(nextSeed, { animate: true })) |
| 点击 Copy | navigator.clipboard.writeText(currentSeed),按钮文案临时变为 COPIED |
骰子面映射:种子最后一位数字
var DIE_FACES = ["⚀", "⚁", "⚂", "⚃", "⚄", "⚅"];
function faceForSeed(seed) { var s = String(seed == null ? "" : seed); var d; if (!s.length) { d = seedToU32(s) % 10; } else { var ch = s.charAt(s.length - 1); d = (ch >= "0" && ch <= "9") ? +ch : (seedToU32(s) % 10); } var n = d % 6; if (n === 0) n = 6; return DIE_FACES[n - 1];}设计意图:种子最后一位数字直观映射为骰子点数(非数字字符回退哈希),让「种子即骰子」的隐喻在视觉上闭环。
种子切换过渡动画(GSAP)
if (opts.animate && typeof gsap !== "undefined" && gsap.to && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) { var targets = document.querySelectorAll(".card, .block"); gsap.fromTo(targets, { autoAlpha: 0.55, y: 10, scale: 0.99 }, { autoAlpha: 1, y: 0, scale: 1, duration: 0.45, ease: "power3.out", stagger: 0.04 } ); var decos = document.querySelectorAll(".deco"); if (decos.length) { gsap.fromTo(decos, { autoAlpha: 0, scale: 0.85 }, { autoAlpha: 1, scale: 1, duration: 0.7, ease: "power2.out", stagger: 0.05 } ); }}- 不重排、不重绘布局:仅
opacity、transform,GPU 合成层完成; - 交错延迟 传递「重生」而非「闪烁」的质感;
- 尊重
prefers-reduced-motion:动画降级为瞬时切换。
工程落地的关键决策与避坑指南
1. 单文件零构建约束下的模块化
项目坚持 单 HTML 文件、无构建、无打包。生成引擎、色彩工具、交互逻辑、GSAP 动画、RSS 拉取、时钟、代码流式启动动画——全部内联在一个 <script> IIFE 中。
组织策略:
(function () { 'use strict';
/* ===== 1. PRNG & Hash ===== */ var mulberry32 = ...; var hashStr = ...; var seedToU32 = ...;
/* ===== 2. Color Science ===== */ var hslToRgb = ...; var luminance = ...; var contrastRatio = ...; var ensureContrast = ...; var css = ...; // HSL 对象 → "hsl(h s% l% / a)" var genPalette = ...;
/* ===== 3. Design Token Generation ===== */ var DISPLAY_FONTS = [...]; var BODY_FONTS = [...]; var genDecoration = ...; var genDesign = ...;
/* ===== 4. Seed Application & UI ===== */ var applySeed = ...; var randomSeed = ...; var faceForSeed = ...; var setDieFace = ...; var playDiceRoll = ...;
/* ===== 5. Boot / Streaming / GSAP Intro ===== */ var runCodeStream = ...; var startIntro = ...;
/* ===== 6. Runtime: Clock, RSS, Magnetic ===== */ var padTime = ...; var fetchPosts = ...; var bindMagnetic = ...;
/* ===== Entry ===== */ applySeed(randomSeed()); runCodeStream(function (meta) { startIntro(meta); });})();- 命名空间隔离:IIFE 形成闭包,全局仅暴露
currentSeed、rand等极少必要引用; - 阶段注释分块:
/* ===== N. 模块名 ===== */替代文件分割,便于在单文件中定位; - 零依赖核心:PRNG、哈希、色彩数学、令牌生成均为原生 JS,仅 GSAP 与
fetch为外部运行时依赖。
2. CSS 变量注入时机与 FOUC 抑制
genDesign() 直接操作 document.documentElement.style.setProperty(),浏览器同步重算样式,无闪烁(FOUC)风险。但需注意:
- 首屏渲染前完成:在
applySeed(randomSeed())调用时,HTML 已解析完毕、CSSOM 就绪,变量注入即时生效; transition仅用于交互态::root变量变更本身不触发过渡,.card, .block等组件通过transition: background-color .35s, box-shadow .35s...响应色彩/阴影变化,种子切换动画由 GSAP 显式驱动,而非依赖 CSS Transition;prefers-reduced-motion全链路贯通:CSS@media、GSAPmatchMedia、骰子动画分支、种子切换过渡分支,四处同步判断,确保动效减弱模式下零动画、零延迟。
3. 字体回退与中英文混排
DISPLAY_FONTS 与 BODY_FONTS 均采用 字体栈 而非单一字体:
var DISPLAY_FONTS = [ 'Georgia, "Noto Serif SC", "Songti SC", serif', '"Palatino Linotype", "Book Antiqua", "Noto Serif SC", serif', 'Impact, "Arial Black", "PingFang SC", sans-serif', ...];原则:
- 西文字体在前(Georgia, Palatino, Impact…),中文回退在后(Noto Serif SC, PingFang SC, Songti SC);
- 系统字体优先,避免
@font-face加载延迟与 CLS; - 等宽字体栈固定为
ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace,保证代码片段、种子栏、时钟的字符宽度稳定。
4. 色彩可访问性:对比度守卫的实现细节
function ensureContrast(color, bg, min) { if (min == null) min = 4.5; var bgRgb = hslToRgb(bg.h, bg.s, bg.l); var dir = luminance(bgRgb) > 0.4 ? -1 : 1; // 背景亮则降低前景亮度,反之升高 var guard = 0; while (contrastRatio(hslToRgb(color.h, color.s, color.l), bgRgb) < min && guard++ < 60) { color.l = Math.min(100, Math.max(0, color.l + dir * 1.5)); }}要点:
- 单向亮度调整:锁定色相、饱和度,仅移动明度,保持色彩身份;
- 方向自适应:基于背景相对亮度判断前景应变亮还是变暗;
- 迭代上限 60 步:极端情况下防止死循环,步长 1.5% 足以在 0~100 区间收敛;
- 分级阈值:正文 4.5:1(WCAG AA),强调色对表层 3.5:1,强调色对背景 2.8:1(大号文本/装饰性元素放宽)。
5. 种子持久化策略:不持久化
/* every load/refresh gets a fresh seed — no URL / storage restore */applySeed(randomSeed());决策依据:
- 生成式设计的核心体验是「偶遇」:每次打开皆不同,刷新即重生,书签/分享链接固化种子反而削弱惊喜感;
- 种子栏即分享入口:用户若钟情某版面,可手工 Copy Seed,后续在种子栏粘贴复现,分享权归属用户;
- 避免 URL 污染:
#seed=...或?seed=...会干扰语义化 URL、SEO 与分享预览; - 隐私最小化:不写
localStorage、sessionStorage、Cookie,无指纹追踪风险。
6. 响应式与容器查询的配合
生成式令牌中包含 --container(容器最大宽度)、--space(基础间距)、--radius(圆角)、--border-w(边框宽度)等布局级变量。这些变量在 @media (max-width: 720px) 与 @media (max-width: 420px) 中被媒体查询覆盖而非 JS 重算:
@media (max-width: 720px) { .card, .block { padding: calc(var(--space) * 1.25) calc(var(--space) * 1.1); } .item { grid-template-columns: 1fr; gap: 0.4rem; } .item-go { display: none; } .seedbar { bottom: 0.85rem; gap: 0.35rem; padding: 0.35rem 0.4rem 0.35rem 0.6rem; } .seedbar-input { width: min(34vw, 8.5rem); min-width: 4.5rem; }}分工原则:
- JS 负责「设计语言」维度:配色、字体、比例、装饰层、阴影风格;
- CSS 媒体查询负责「布局适配」维度:断点下的内边距、网格结构、交互元素显隐;
- 两者正交,避免「JS 监听 resize 重算布局令牌」的性能陷阱与闪烁。
7. 装饰层 DOM 生命周期管理
function genDecoration(p) { var body = document.body; body.querySelectorAll(".deco").forEach(function (el) { el.remove(); }); body.style.background = ""; body.style.backgroundImage = ""; body.style.backgroundSize = ""; // ... 依据 type 生成新背景或挂载新 .deco 元素}- 每次
genDesign()先清理旧.deco,防止重掷种子时 DOM 节点累积; - 背景样式显式重置,避免
gradient与dots/grid叠加残留; will-change: transform, opacity仅在.deco上声明,内容层不受影响。
参考与延伸阅读
- Mulberry32 原作者:Tommy Ettinger, 「Mulberry32: A fast, high-quality PRNG」
- FNV-1a Hash:Fowler–Noll–Vo, 「FNV Hash Functions」
- WCAG 2.1 Contrast Requirements:W3C, 「Understanding Success Criterion 1.4.3」
- Modular Scale:Tim Brown, 「More Meaningful Typography」
- Generative Design Patterns:Hartmut Bohnacker 等, 「Generative Design: Visualize, Program, and Create with JavaScript in p5.js」
- CSS Custom Properties for Design Systems:Lea Verou, 「CSS Variables: The Good, the Bad, and the Ugly」
- prefers-reduced-motion:Eric Bailey, 「Revisiting prefers-reduced-motion」