// 程序化"素描"生成器:用带手绘抖动的线条绘制各条目的速写。 // 同一词条每次渲染结果一致(基于名称的种子),供 3D 页背与 HTML 面板复用。 const INK = '#3a2d22'; const INK_SOFT = 'rgba(58,45,34,0.55)'; const INK_FAINT = 'rgba(58,45,34,0.28)'; const WASH = 'rgba(150,120,80,0.10)'; // 确定性伪随机数(mulberry32) function prng(seedStr) { let a = 0; for (let i = 0; i < seedStr.length; i++) { a = (a * 31 + seedStr.charCodeAt(i)) >>> 0; } a = a || 123456789; return function () { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } export function renderSketch(entry, size = 760) { const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); const rand = prng(entry.name + entry.sketch); ctx.clearRect(0, 0, size, size); const cx = size / 2; const cy = size / 2; const r = size * 0.48; const P = (x, y) => [cx + x * r, cy + y * r]; // 手绘折线 function line(pts, { color = INK, lw = 2.4, passes = 2, wobble = 0.015, close = false, dash = null } = {}) { ctx.strokeStyle = color; ctx.lineWidth = lw * (size / 760); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; if (dash) ctx.setLineDash(dash); else ctx.setLineDash([]); for (let p = 0; p < passes; p++) { ctx.beginPath(); pts.forEach((pt, i) => { const [x, y] = P(pt[0], pt[1]); const jx = wobble * r * (rand() - 0.5) * 2; const jy = wobble * r * (rand() - 0.5) * 2; if (i === 0) ctx.moveTo(x + jx, y + jy); else ctx.lineTo(x + jx, y + jy); }); if (close) ctx.closePath(); ctx.stroke(); } ctx.setLineDash([]); } // 形状填充(可选淡淡水彩底) function fill(pts, color = WASH) { ctx.fillStyle = color; ctx.beginPath(); pts.forEach((pt, i) => { const [x, y] = P(pt[0], pt[1]); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.closePath(); ctx.fill(); } // 阴影排线(在裁剪区域内) function hatch(pts, { spacing = 0.035, angle = -45, color = INK_SOFT, lw = 1.1, box = 2.4 } = {}) { ctx.save(); ctx.beginPath(); pts.forEach((pt, i) => { const [x, y] = P(pt[0], pt[1]); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.closePath(); ctx.clip(); ctx.save(); ctx.translate(cx, cy); ctx.rotate((angle * Math.PI) / 180); ctx.strokeStyle = color; ctx.lineWidth = lw * (size / 760); ctx.beginPath(); const L = box * r; const s = spacing * r; for (let i = -60; i < 60; i++) { ctx.moveTo(i * s, -L); ctx.lineTo(i * s, L); } ctx.stroke(); ctx.restore(); ctx.restore(); } function circle(cx0, cy0, rad, opts = {}) { const { color = INK, lw = 2.2, passes = 1, fill: f = null } = opts; const [x, y] = P(cx0, cy0); const rr = rad * r; if (f) { ctx.fillStyle = f; ctx.beginPath(); ctx.arc(x, y, rr, 0, Math.PI * 2); ctx.fill(); } ctx.strokeStyle = color; ctx.lineWidth = lw * (size / 760); for (let p = 0; p < passes; p++) { ctx.beginPath(); const j = (rand() - 0.5) * r * 0.02; ctx.arc(x + j, y + j, rr, 0, Math.PI * 2); ctx.stroke(); } } function dot(x, y, rad, color = INK) { const [px, py] = P(x, y); ctx.fillStyle = color; ctx.beginPath(); ctx.arc(px, py, rad * r, 0, Math.PI * 2); ctx.fill(); } // 各类型速写 const types = { fossil() { // 头骨 fill([[0, 0.35], [-0.55, 0.05], [-0.5, -0.5], [-0.2, -0.78], [0.3, -0.75], [0.58, -0.4], [0.5, 0.0], [0.3, 0.1]], 'rgba(150,120,80,0.12)'); line([[0, 0.32], [-0.52, 0.05], [-0.5, -0.42], [-0.3, -0.66], [0.0, -0.74], [0.32, -0.68], [0.55, -0.4], [0.5, 0.0], [0.32, 0.12]], { lw: 2.8 }); // 眼窝 circle(-0.22, -0.18, 0.16, { lw: 2 }); circle(0.28, -0.18, 0.15, { lw: 2 }); // 鼻腔 line([[-0.02, -0.05], [0.0, 0.14], [-0.16, 0.14]], { lw: 2 }); line([[-0.02, -0.05], [0.0, 0.14], [0.14, 0.14]], { lw: 1.4 }); // 上颌与牙齿 line([[0.05, 0.12], [0.22, 0.1], [0.32, 0.12]], { lw: 2.2 }); for (let i = 0; i < 5; i++) { line([[-0.42 + i * 0.16, 0.02], [-0.42 + i * 0.16, 0.16]], { lw: 1.2, passes: 1, wobble: 0.004 }); } // 骨裂纹理 line([[-0.1, -0.7], [-0.16, -0.55], [-0.05, -0.45]], { color: INK_FAINT, lw: 1 }); line([[0.2, -0.66], [0.1, -0.55], [0.18, -0.42]], { color: INK_FAINT, lw: 1 }); // 下方地平线与年代波纹 line([[-0.6, 0.5], [0.6, 0.5]], { color: INK_FAINT, lw: 1.2, passes: 1 }); line([[-0.45, 0.62], [0.45, 0.62]], { color: INK_FAINT, lw: 1, passes: 1 }); }, tablet() { fill([[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]], 'rgba(170,140,95,0.16)'); line([[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5], [-0.5, -0.5]], { lw: 3, wobble: 0.01 }); // 楔形文字符号:小三角 for (let row = 0; row < 5; row++) { for (let col = 0; col < 6; col++) { const x = -0.36 + col * 0.15; const y = -0.34 + row * 0.18; const s = 0.04 + rand() * 0.02; line([[x, y], [x - s, y - s * 1.4]], { lw: 1.6, passes: 1, wobble: 0.005 }); line([[x, y], [x + s, y - s * 1.4]], { lw: 1.6, passes: 1, wobble: 0.005 }); line([[x, y], [x, y - s * 1.8]], { lw: 1.6, passes: 1, wobble: 0.005 }); } } // 裂缝 line([[-0.1, 0.44], [0.02, 0.2], [-0.08, -0.05], [0.05, -0.3]], { color: INK_SOFT, lw: 1.4, passes: 1 }); line([[-0.44, -0.1], [-0.25, -0.15]], { color: INK_SOFT, lw: 1.2, passes: 1 }); }, pyramid() { // 地平线 line([[-0.72, 0.42], [0.72, 0.42]], { lw: 2 }); // 主金字塔 fill([[-0.52, 0.42], [0.0, -0.62], [0.55, 0.42]], 'rgba(160,120,70,0.12)'); line([[-0.52, 0.42], [0.0, -0.62], [0.55, 0.42], [-0.52, 0.42]], { lw: 3 }); // 石砌纹理 for (let i = 1; i <= 5; i++) { const t = i / 6; const xl = -0.52 + (0.52) * t; const xr = 0.55 - (0.55) * t; const y = 0.42 - (1.04) * t; line([[xl, y], [xr, y]], { color: INK_SOFT, lw: 1.1, passes: 1, wobble: 0.004 }); } // 小金字塔 line([[-0.72, 0.42], [-0.5, 0.0], [-0.28, 0.42]], { lw: 2 }); line([[-0.72, 0.42], [-0.5, 0.0], [-0.28, 0.42], [-0.72, 0.42]], { lw: 2, color: INK_SOFT }); // 太阳 circle(0.55, -0.55, 0.16, { lw: 2.4 }); for (let i = 0; i < 10; i++) { const a = (i / 10) * Math.PI * 2 + 0.3; line([[0.55 + Math.cos(a) * 0.2, -0.55 + Math.sin(a) * 0.2], [0.55 + Math.cos(a) * 0.28, -0.55 + Math.sin(a) * 0.28]], { lw: 1.6, passes: 1 }); } // 沙丘波纹 line([[-0.7, 0.62], [-0.2, 0.62]], { color: INK_FAINT, lw: 1.2, passes: 1 }); line([[0.1, 0.66], [0.65, 0.66]], { color: INK_FAINT, lw: 1.2, passes: 1 }); }, figure() { // 圆形徽章背景 circle(0, 0, 0.85, { color: INK_FAINT, lw: 1.4, passes: 1 }); circle(0, 0, 0.72, { color: INK_FAINT, lw: 1, passes: 1 }); // 头部轮廓(侧影) fill([[0.18, -0.28], [0.18, -0.1], [0.24, 0.04], [0.3, 0.14], [0.26, 0.24], [0.2, 0.3], [0.24, 0.5], [0.05, 0.55], [-0.05, 0.5], [-0.02, 0.34], [0.0, 0.24], [-0.05, 0.12], [-0.04, -0.1], [-0.05, -0.4], [0.02, -0.6], [0.12, -0.66], [0.18, -0.28]], 'rgba(120,95,70,0.12)'); line([[0.05, -0.62], [0.16, -0.56], [0.18, -0.3], [0.2, -0.12], [0.25, 0.02], [0.3, 0.12], [0.27, 0.22], [0.21, 0.3], [0.25, 0.46], [0.14, 0.55], [0.02, 0.56], [-0.04, 0.5], [-0.02, 0.34], [0.0, 0.24], [-0.05, 0.14], [-0.05, -0.08], [-0.04, -0.3], [0.0, -0.52], [0.05, -0.62]], { lw: 2.8 }); // 发髻 / 头发 line([[0.05, -0.62], [-0.02, -0.5], [0.02, -0.38], [-0.03, -0.26], [0.0, -0.14]], { lw: 2, color: INK_SOFT }); hatch([[0.02, -0.6], [0.16, -0.56], [0.18, -0.3], [0.0, -0.52], [0.02, -0.6]], { spacing: 0.03, angle: 60, color: INK_SOFT, lw: 1 }); // 眼睛、眉、耳 line([[0.2, -0.02], [0.28, -0.02], [0.25, 0.04], [0.2, -0.02]], { lw: 1.8, passes: 1 }); line([[0.19, -0.1], [0.3, -0.07]], { lw: 1.8, passes: 1 }); line([[-0.05, -0.08], [-0.02, -0.2]], { lw: 1.6, passes: 1 }); // 肩部与衣领 line([[-0.28, 0.55], [0.0, 0.55]], { lw: 2.6 }); line([[-0.55, 0.85], [0.0, 0.55], [0.6, 0.85]], { lw: 2.4 }); line([[-0.42, 0.7], [0.05, 0.58]], { lw: 1.6, color: INK_SOFT, passes: 1 }); }, temple() { // 台基 line([[-0.7, 0.6], [0.7, 0.6]], { lw: 2.6 }); line([[-0.64, 0.66], [0.64, 0.66]], { lw: 2 }); // 柱子 for (let i = 0; i < 6; i++) { const x = -0.5 + i * 0.2; line([[x, 0.58], [x, -0.1]], { lw: 2 }); line([[x - 0.02, 0.58], [x - 0.02, -0.1]], { lw: 1, color: INK_SOFT, passes: 1 }); } // 柱顶横梁 line([[-0.6, -0.1], [0.6, -0.1]], { lw: 2.4 }); line([[-0.58, -0.18], [0.58, -0.18]], { lw: 2 }); // 三角楣 fill([[-0.58, -0.18], [0.0, -0.66], [0.58, -0.18]], 'rgba(140,110,70,0.10)'); line([[-0.58, -0.18], [0.0, -0.66], [0.58, -0.18], [-0.58, -0.18]], { lw: 2.6 }); // 楣内浮雕圆 circle(0, -0.4, 0.1, { lw: 1.8, passes: 1 }); // 山门台阶 for (let i = 1; i <= 3; i++) { line([[-0.3 - i * 0.08, 0.66 + i * 0.05], [0.3 + i * 0.08, 0.66 + i * 0.05]], { lw: 1.6, color: INK_SOFT, passes: 1 }); } }, crown() { // 冠身 fill([[-0.55, 0.5], [-0.55, -0.1], [0.55, -0.1], [0.55, 0.5]], 'rgba(160,120,50,0.14)'); line([[-0.55, 0.5], [-0.55, -0.1], [0.55, -0.1], [0.55, 0.5]], { lw: 3 }); // 齿尖与宝珠 const peaks = [-0.55, -0.28, 0.0, 0.28, 0.55]; peaks.forEach((px, i) => { const h = 0.5 + (i % 2 === 0 ? 0.26 : 0.4); line([[px, 0.5], [px + (i % 2 === 0 ? 0.02 : 0), h]], { lw: 2.4 }); dot(px, h + 0.07, 0.05, '#a03a2a'); }); // 冠带 line([[-0.58, 0.28], [0.58, 0.28]], { lw: 2, color: INK_SOFT }); // 中央纹饰 circle(0, 0.12, 0.14, { lw: 2, passes: 1 }); dot(0, 0.12, 0.05, '#a03a2a'); // 底部绒边 for (let i = 0; i < 14; i++) { const x = -0.55 + i * 0.085; dot(x, -0.12, 0.02, INK_SOFT); } hatch([[-0.55, 0.5], [-0.55, -0.1], [0.55, -0.1], [0.55, 0.5]], { spacing: 0.035, angle: -60, color: INK_FAINT, lw: 1 }); }, colosseum() { // 底层地面 line([[-0.75, 0.6], [0.75, 0.6]], { lw: 2 }); // 三层椭圆拱券 const tiers = [ { y: 0.12, rw: 0.68, rh: 0.16, n: 7 }, { y: -0.1, rw: 0.74, rh: 0.15, n: 8 }, { y: -0.32, rw: 0.8, rh: 0.14, n: 9 }, ]; tiers.forEach((t) => { // 顶部线(略残缺) line([[-t.rw, t.y - t.rh], [t.rw, t.y - t.rh]], { lw: 2.2 }); }); tiers.forEach((t) => { for (let i = 0; i <= t.n; i++) { const a = -0.9 + (i / t.n) * 1.8; // 参数 -0.9..0.9 const x = a * t.rw; const y = t.y; // 拱券(简化小竖线 + 弧) line([[x, y], [x, y + t.rh * 0.7]], { lw: 1.6, color: INK_SOFT, passes: 1 }); if (i < t.n) { const x2 = (a + 0.9 / t.n) * t.rw; const mx = (x + x2) / 2; line([[x, y + t.rh * 0.7], [mx, y + t.rh * 1.15], [x2, y + t.rh * 0.7]], { lw: 1.4, color: INK_SOFT, passes: 1 }); } } }); // 残破上缘 line([[-0.82, -0.48], [-0.6, -0.42], [-0.4, -0.55], [-0.1, -0.4], [0.2, -0.52], [0.5, -0.4], [0.82, -0.46]], { lw: 2.2, passes: 1 }); line([[-0.82, 0.56], [-0.85, -0.3]], { lw: 2, passes: 1 }); line([[0.82, 0.56], [0.85, -0.3]], { lw: 2, passes: 1 }); }, scroll() { // 卷轴两端 circle(-0.52, 0, 0.09, { lw: 2.2 }); circle(0.52, 0, 0.09, { lw: 2.2 }); // 展开的纸 fill([[-0.52, -0.28], [0.52, -0.28], [0.52, 0.28], [-0.52, 0.28]], 'rgba(210,190,150,0.20)'); line([[-0.52, -0.28], [0.52, -0.28]], { lw: 2.4 }); line([[-0.52, 0.28], [0.52, 0.28]], { lw: 2.4 }); // 竖排文字行 for (let i = 0; i < 7; i++) { const x = -0.38 + i * 0.13; line([[x, -0.18], [x, 0.18]], { lw: 1.3, color: INK_SOFT, passes: 1, wobble: 0.005 }); } // 毛笔 line([[0.05, -0.6], [0.02, -0.9]], { lw: 2.2 }); line([[0.0, -0.92], [0.14, -0.98], [0.05, -0.72]], { lw: 2, color: INK_SOFT, passes: 1 }); // 墨滴 dot(0.3, 0.42, 0.03, INK_SOFT); dot(0.36, 0.46, 0.02, INK_FAINT); }, horseman() { // 地平线 line([[-0.75, 0.55], [0.75, 0.55]], { lw: 1.8, color: INK_FAINT }); // 马(剪影线条) fill([[0.2, 0.5], [0.32, 0.05], [0.55, 0.0], [0.6, 0.3], [0.42, 0.42], [0.2, 0.5]], 'rgba(90,65,45,0.10)'); line([[0.25, 0.5], [0.35, 0.15], [0.42, 0.05], [0.58, 0.05], [0.6, 0.28], [0.45, 0.4], [0.25, 0.5]], { lw: 2.6 }); // 马腿 line([[0.3, 0.45], [0.26, 0.55]], { lw: 2 }); line([[0.44, 0.42], [0.42, 0.55]], { lw: 2 }); line([[0.55, 0.3], [0.6, 0.55]], { lw: 2 }); // 马尾 line([[0.25, 0.5], [0.08, 0.35], [0.02, 0.5]], { lw: 2, color: INK_SOFT }); // 骑手 line([[-0.05, 0.4], [-0.02, 0.15], [0.05, 0.05], [0.12, 0.15], [0.1, 0.4]], { lw: 2.4 }); circle(-0.02, -0.02, 0.09, { lw: 1.8, passes: 1 }); // 弓 line([[-0.05, 0.18], [-0.3, 0.28]], { lw: 1.6, passes: 1 }); line([[-0.3, 0.28], [-0.32, 0.1]], { lw: 1.6, color: INK_SOFT, passes: 1 }); // 箭 line([[-0.28, 0.24], [-0.55, 0.14]], { lw: 1.4, passes: 1 }); // 旗帜 line([[0.1, 0.15], [0.4, -0.3]], { lw: 1.8 }); line([[0.4, -0.3], [0.32, -0.32], [0.38, -0.4], [0.42, -0.28]], { lw: 1.6, passes: 1 }); }, ship() { // 波浪 line([[-0.75, 0.5], [-0.4, 0.5], [-0.2, 0.42], [0.0, 0.5], [0.3, 0.42], [0.55, 0.5], [0.75, 0.5]], { lw: 1.6, color: INK_SOFT, passes: 1 }); line([[-0.7, 0.66], [0.7, 0.66]], { lw: 1.4, color: INK_FAINT, passes: 1 }); // 船体 fill([[-0.5, 0.12], [0.5, 0.12], [0.38, 0.4], [-0.4, 0.4]], 'rgba(80,60,40,0.14)'); line([[-0.5, 0.12], [0.5, 0.12], [0.4, 0.38], [-0.42, 0.38], [-0.5, 0.12]], { lw: 2.6 }); // 桅杆 line([[0.0, 0.1], [0.0, -0.7]], { lw: 2.2 }); line([[-0.35, 0.14], [-0.35, -0.45]], { lw: 1.8 }); line([[0.35, 0.14], [0.35, -0.45]], { lw: 1.8 }); // 帆(鼓风) fill([[0.0, -0.62], [0.42, -0.5], [0.3, -0.2], [0.0, -0.28]], 'rgba(200,180,140,0.22)'); line([[0.0, -0.62], [0.4, -0.5], [0.3, -0.2], [0.0, -0.28], [0.0, -0.62]], { lw: 1.8 }); fill([[-0.35, -0.4], [-0.05, -0.45], [-0.08, -0.2], [-0.35, -0.15]], 'rgba(200,180,140,0.18)'); line([[-0.35, -0.4], [-0.06, -0.44], [-0.1, -0.18], [-0.35, -0.14], [-0.35, -0.4]], { lw: 1.6 }); fill([[0.35, -0.4], [0.65, -0.48], [0.62, -0.2], [0.35, -0.15]], 'rgba(200,180,140,0.18)'); line([[0.35, -0.4], [0.62, -0.48], [0.6, -0.18], [0.35, -0.14], [0.35, -0.4]], { lw: 1.6 }); // 旗帜 line([[0.0, -0.7], [0.0, -0.78]], { lw: 1.6 }); line([[0.0, -0.76], [0.2, -0.74], [0.0, -0.7]], { lw: 1.4, passes: 1 }); // 海鸟 line([[-0.6, -0.4], [-0.5, -0.46], [-0.4, -0.4]], { lw: 1.2, color: INK_SOFT, passes: 1 }); line([[-0.3, -0.55], [-0.2, -0.61], [-0.1, -0.55]], { lw: 1.2, color: INK_SOFT, passes: 1 }); }, globe() { circle(0, 0, 0.66, { lw: 2.6 }); // 经纬线 circle(0, 0, 0.66, { lw: 1, color: INK_FAINT, passes: 1 }); for (const k of [0.3, 0.6]) { ctx.save(); ctx.translate(cx, cy); ctx.strokeStyle = INK_FAINT; ctx.lineWidth = 1 * (size / 760); ctx.beginPath(); ctx.ellipse(0, 0, 0.66 * r * Math.sqrt(1 - k * k), 0.66 * r, 0, 0, Math.PI * 2); ctx.stroke(); ctx.beginPath(); ctx.ellipse(0, 0, 0.66 * r, 0.66 * r * Math.sqrt(1 - k * k), 0, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } // 大陆点阵 for (let i = 0; i < 60; i++) { const a = rand() * Math.PI * 2; const rr = Math.sqrt(rand()) * 0.6; dot(Math.cos(a) * rr, Math.sin(a) * rr, 0.012, INK_SOFT); } // 航线(虚线)与箭头 line([[0.3, -0.3], [0.55, -0.15], [0.6, 0.1], [0.45, 0.35], [0.15, 0.45]], { color: '#a03a2a', lw: 2, passes: 1, dash: [14, 9] }); line([[0.15, 0.45], [0.02, 0.52]], { color: '#a03a2a', lw: 2, passes: 1 }); line([[0.15, 0.45], [0.22, 0.55]], { color: '#a03a2a', lw: 2, passes: 1 }); // 起止点 dot(0.3, -0.3, 0.04, '#a03a2a'); dot(0.15, 0.45, 0.04, '#a03a2a'); }, apple() { // 苹果 fill([[0, -0.55], [0.35, -0.42], [0.42, -0.1], [0.28, 0.22], [0.0, 0.42], [-0.3, 0.2], [-0.4, -0.15], [-0.3, -0.44]], 'rgba(150,60,40,0.14)'); line([[0.02, -0.5], [0.36, -0.4], [0.44, -0.08], [0.28, 0.24], [0.0, 0.44], [-0.3, 0.2], [-0.42, -0.16], [-0.32, -0.42]], { lw: 2.6 }); line([[0.02, -0.5], [0.36, -0.4]], { lw: 2.6 }); // 咬痕 circle(-0.52, -0.05, 0.18, { lw: 2, passes: 1, color: INK_SOFT }); // 果柄与叶 line([[0.0, -0.5], [0.02, -0.72]], { lw: 2.2 }); fill([[0.02, -0.72], [0.28, -0.78], [0.2, -0.58]], 'rgba(60,90,50,0.16)'); line([[0.02, -0.72], [0.28, -0.76], [0.2, -0.6], [0.02, -0.72]], { lw: 1.8 }); // 下落的轨迹(引力) line([[-0.1, 0.55], [-0.25, 0.72]], { color: INK_SOFT, lw: 1.6, passes: 1, dash: [10, 8] }); dot(-0.3, 0.78, 0.03, INK_SOFT); line([[0.55, 0.5], [0.62, 0.62]], { color: INK_FAINT, lw: 1.2, passes: 1 }); }, machine() { // 底座 line([[-0.7, 0.55], [0.7, 0.55]], { lw: 2.4 }); // 锅炉 fill([[-0.45, -0.2], [0.1, -0.2], [0.1, 0.1], [-0.45, 0.1]], 'rgba(90,70,50,0.14)'); line([[-0.45, -0.2], [0.1, -0.2], [0.1, 0.1], [-0.45, 0.1], [-0.45, -0.2]], { lw: 2.4 }); // 铆钉 for (let i = 0; i < 5; i++) dot(-0.4 + i * 0.12, -0.2, 0.02, INK_SOFT); for (let i = 0; i < 5; i++) dot(-0.4 + i * 0.12, 0.1, 0.02, INK_SOFT); // 活塞杆 line([[0.1, -0.05], [0.42, -0.05]], { lw: 2.2 }); line([[0.42, -0.1], [0.42, 0.0]], { lw: 1.6 }); // 飞轮 circle(0.5, -0.05, 0.3, { lw: 2.4 }); circle(0.5, -0.05, 0.08, { lw: 1.8, passes: 1 }); for (let i = 0; i < 6; i++) { const a = (i / 6) * Math.PI * 2; line([[0.5 + Math.cos(a) * 0.08, -0.05 + Math.sin(a) * 0.08], [0.5 + Math.cos(a) * 0.3, -0.05 + Math.sin(a) * 0.3]], { lw: 1.4, passes: 1 }); } // 蒸汽 line([[-0.3, -0.32], [-0.2, -0.45], [-0.32, -0.6]], { color: INK_SOFT, lw: 1.8, passes: 1 }); line([[-0.1, -0.4], [0.0, -0.55], [-0.08, -0.72]], { color: INK_SOFT, lw: 1.8, passes: 1 }); line([[0.1, -0.34], [0.22, -0.48], [0.14, -0.62]], { color: INK_FAINT, lw: 1.6, passes: 1 }); // 传动带 line([[0.1, -0.05], [0.2, -0.1], [0.5, -0.32]], { lw: 1.2, color: INK_SOFT, passes: 1, dash: [9, 7] }); }, bell() { // 钟轭 line([[-0.5, -0.55], [0.5, -0.55]], { lw: 2.6 }); line([[-0.5, -0.55], [-0.42, -0.28]], { lw: 2 }); line([[0.5, -0.55], [0.42, -0.28]], { lw: 2 }); // 钟身 fill([[-0.42, -0.28], [0.42, -0.28], [0.55, 0.45], [0.3, 0.5], [-0.3, 0.5], [-0.55, 0.45]], 'rgba(120,95,70,0.12)'); line([[-0.42, -0.28], [0.42, -0.28], [0.55, 0.45], [0.3, 0.5], [-0.3, 0.5], [-0.55, 0.45], [-0.42, -0.28]], { lw: 2.6 }); // 底口线 line([[-0.55, 0.45], [0.55, 0.45]], { lw: 2 }); // 裂纹(自由钟标志) line([[-0.05, -0.1], [0.02, 0.05], [-0.08, 0.22], [0.0, 0.42]], { lw: 2, color: '#a03a2a', passes: 1 }); // 铭文带 line([[-0.36, 0.05], [0.36, 0.05]], { lw: 1.4, color: INK_SOFT, passes: 1 }); // 钟舌 line([[0.0, -0.1], [0.0, 0.3]], { lw: 1.8 }); circle(0, 0.32, 0.07, { lw: 1.6, passes: 1 }); }, tree() { // 树干 line([[-0.0, 0.55], [0.0, 0.2], [0.02, -0.1]], { lw: 2.6 }); // 树根 line([[-0.15, 0.55], [0.0, 0.35]], { lw: 2 }); line([[0.15, 0.55], [0.0, 0.35]], { lw: 2 }); // 递归枝干 const branch = (x, y, ang, len, depth) => { const ex = x + Math.cos(ang) * len; const ey = y + Math.sin(ang) * len; line([[x, y], [ex, ey]], { lw: Math.max(0.8, 2.4 * (len / 0.4)), passes: 1, wobble: 0.006 }); if (depth > 0) { branch(ex, ey, ang + 0.5, len * 0.68, depth - 1); branch(ex, ey, ang - 0.5, len * 0.68, depth - 1); branch(ex, ey, ang + 0.2, len * 0.55, depth - 1); } }; branch(0.02, -0.1, -Math.PI / 2, 0.42, 3); // 叶点 for (let i = 0; i < 90; i++) { const a = rand() * Math.PI * 2; const rr = Math.sqrt(rand()) * 0.5; dot(Math.cos(a) * rr * 1.2, -0.5 + Math.sin(a) * rr * 0.8, 0.012 + rand() * 0.012, INK_SOFT); } // 地面 line([[-0.6, 0.6], [0.6, 0.6]], { lw: 1.8, color: INK_FAINT }); }, atom() { // 原子核 circle(0, 0, 0.1, { lw: 2 }); dot(0.03, 0.03, 0.03, '#a03a2a'); dot(-0.04, -0.02, 0.02, '#a03a2a'); // 轨道 const orbit = (rx, ry, rot) => { ctx.save(); ctx.translate(cx, cy); ctx.rotate(rot); ctx.strokeStyle = INK_SOFT; ctx.lineWidth = 1.6 * (size / 760); ctx.beginPath(); ctx.ellipse(0, 0, rx * r, ry * r, 0, 0, Math.PI * 2); ctx.stroke(); // 电子 const ea = rand() * Math.PI * 2; const ex = Math.cos(ea) * rx * r; const ey = Math.sin(ea) * ry * r; ctx.fillStyle = '#a03a2a'; ctx.beginPath(); ctx.arc(ex, ey, 0.045 * r, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }; orbit(0.66, 0.3, 0.2); orbit(0.66, 0.3, 2.2); orbit(0.5, 0.66, 1.2); // 公式 ctx.save(); ctx.translate(cx, cy + 0.86 * r); ctx.fillStyle = INK; ctx.font = `${Math.round(0.16 * r)}px Georgia, serif`; ctx.textAlign = 'center'; ctx.fillText('E = mc²', 0, 0); ctx.restore(); }, dove() { // 鸽身(飞翔剪影) fill([[-0.4, 0.1], [-0.2, -0.1], [0.0, -0.12], [0.2, -0.1], [0.35, 0.05], [0.1, 0.18], [-0.2, 0.2]], 'rgba(200,190,170,0.22)'); line([[-0.42, 0.08], [-0.2, -0.1], [0.0, -0.14], [0.2, -0.1], [0.34, 0.05], [0.1, 0.18], [-0.2, 0.2], [-0.42, 0.08]], { lw: 2.4 }); // 翅膀 line([[-0.1, -0.06], [-0.3, -0.5], [-0.05, -0.62]], { lw: 2.2 }); line([[-0.3, -0.5], [-0.02, -0.28]], { lw: 1.4, color: INK_SOFT }); // 眼与喙 dot(-0.34, 0.02, 0.03, INK); line([[-0.42, 0.06], [-0.52, 0.12], [-0.42, 0.12]], { lw: 1.6, passes: 1 }); // 橄榄枝 line([[-0.52, 0.12], [-0.62, 0.3]], { lw: 1.8 }); for (let i = 0; i < 5; i++) { const a = -0.4 + i * 0.5; const bx = -0.62 + Math.cos(a) * 0.1; const by = 0.3 + Math.sin(a) * 0.05 + i * 0.01; line([[-0.62 + (i * 0.02), 0.28 + i * 0.005], [bx, by]], { lw: 1.2, passes: 1 }); } // 光芒 for (let i = 0; i < 8; i++) { const a = (i / 8) * Math.PI * 2; line([[Math.cos(a) * 0.72, Math.sin(a) * 0.72], [Math.cos(a) * 0.82, Math.sin(a) * 0.82]], { lw: 1.4, color: INK_FAINT, passes: 1 }); } }, rocket() { // 星空 for (let i = 0; i < 30; i++) { dot(-0.85 + rand() * 1.7, -0.85 + rand() * 1.4, 0.008 + rand() * 0.01, INK_FAINT); } // 火箭主体 fill([[-0.14, -0.7], [0.14, -0.7], [0.14, 0.5], [-0.14, 0.5]], 'rgba(200,200,205,0.20)'); line([[-0.14, -0.7], [0.14, -0.7], [0.14, 0.5], [-0.14, 0.5], [-0.14, -0.7]], { lw: 2.4 }); // 头锥 line([[-0.14, -0.7], [0.0, -0.92], [0.14, -0.7]], { lw: 2.4 }); // 舷窗 circle(0, -0.3, 0.07, { lw: 1.6, passes: 1 }); // 尾翼 line([[-0.14, 0.2], [-0.3, 0.5]], { lw: 2 }); line([[-0.14, 0.2], [-0.16, 0.5]], { lw: 1.2, color: INK_SOFT }); line([[0.14, 0.2], [0.3, 0.5]], { lw: 2 }); line([[0.14, 0.2], [0.16, 0.5]], { lw: 1.2, color: INK_SOFT }); // 喷焰 line([[-0.08, 0.5], [0.0, 0.72], [0.08, 0.5]], { lw: 2, color: '#a03a2a' }); line([[-0.05, 0.5], [0.0, 0.62], [0.05, 0.5]], { lw: 1.4, color: '#c9a227' }); // 月球 circle(0.62, -0.6, 0.22, { lw: 2.2, fill: 'rgba(210,200,180,0.16)' }); circle(0.55, -0.66, 0.05, { lw: 1.2, passes: 1, color: INK_FAINT }); circle(0.68, -0.55, 0.035, { lw: 1.2, passes: 1, color: INK_FAINT }); // 旗帜 line([[0.4, 0.5], [0.4, 0.3]], { lw: 1.4 }); line([[0.4, 0.3], [0.56, 0.34], [0.4, 0.4]], { lw: 1.4, passes: 1 }); }, book() { // 摊开的书 fill([[-0.5, -0.1], [0.0, -0.2], [0.5, -0.1], [0.5, 0.4], [0.0, 0.5], [-0.5, 0.4]], 'rgba(210,190,150,0.18)'); line([[-0.5, -0.1], [0.0, -0.2], [0.5, -0.1], [0.5, 0.4], [0.0, 0.5], [-0.5, 0.4], [-0.5, -0.1]], { lw: 2.4 }); line([[0.0, -0.2], [0.0, 0.5]], { lw: 2 }); for (let i = 0; i < 4; i++) { const y = -0.05 + i * 0.14; line([[-0.4, y], [-0.08, y]], { lw: 1.2, color: INK_SOFT, passes: 1, wobble: 0.004 }); line([[0.08, y], [0.4, y]], { lw: 1.2, color: INK_SOFT, passes: 1, wobble: 0.004 }); } hatch([[-0.5, -0.1], [0.0, -0.2], [0.5, -0.1], [0.5, 0.4], [0.0, 0.5], [-0.5, 0.4]], { spacing: 0.04, angle: -45, color: INK_FAINT, lw: 1 }); }, bridge() { line([[-0.7, 0.45], [0.7, 0.45]], { lw: 1.6, color: INK_SOFT, passes: 1 }); line([[-0.6, 0.6], [0.6, 0.6]], { lw: 1.2, color: INK_FAINT, passes: 1 }); fill([[-0.5, 0.45], [-0.5, -0.05], [0.5, -0.05], [0.5, 0.45]], 'rgba(150,140,130,0.10)'); line([[-0.5, 0.45], [-0.5, 0.05], [-0.35, -0.2], [0.0, -0.35], [0.35, -0.2], [0.5, 0.05], [0.5, 0.45]], { lw: 2.6 }); line([[-0.55, 0.45], [-0.55, 0.28], [0.55, 0.28], [0.55, 0.45]], { lw: 1.6, color: INK_SOFT }); for (let i = 0; i < 7; i++) { const x = -0.45 + i * 0.15; line([[x, 0.28], [x, 0.4]], { lw: 1.2, passes: 1, color: INK_SOFT }); } line([[-0.3, 0.52], [0.0, 0.56], [0.3, 0.52]], { lw: 1, color: INK_FAINT, passes: 1 }); }, flag() { line([[-0.4, 0.6], [-0.4, -0.7]], { lw: 2.4 }); fill([[-0.4, -0.65], [-0.1, -0.6], [0.05, -0.45], [-0.25, -0.5]], 'rgba(160,58,42,0.16)'); line([[-0.4, -0.65], [-0.1, -0.6], [0.05, -0.45], [-0.25, -0.5], [-0.4, -0.65]], { lw: 2.2 }); line([[-0.32, -0.55], [0.0, -0.48]], { lw: 1.4, color: INK_SOFT, passes: 1 }); line([[-0.25, -0.5], [-0.15, -0.32]], { lw: 1.4, color: INK_SOFT, passes: 1 }); line([[-0.6, 0.62], [0.6, 0.62]], { lw: 1.6, color: INK_FAINT }); for (let i = 0; i < 7; i++) { const a = -0.5 + i * 0.3; line([[0.05 + Math.cos(a) * 0.3, -0.3 + Math.sin(a) * 0.2], [0.05 + Math.cos(a) * 0.42, -0.3 + Math.sin(a) * 0.3]], { lw: 1.2, color: INK_FAINT, passes: 1 }); } }, scale() { line([[0.0, 0.6], [0.0, -0.15]], { lw: 2.4 }); line([[-0.55, -0.05], [0.55, -0.05]], { lw: 2.2 }); circle(0.0, -0.1, 0.06, { lw: 1.8, passes: 1 }); line([[-0.55, -0.05], [-0.55, 0.2]], { lw: 1.6, passes: 1 }); line([[0.55, -0.05], [0.55, 0.2]], { lw: 1.6, passes: 1 }); line([[-0.72, 0.2], [-0.38, 0.2]], { lw: 2 }); line([[0.38, 0.2], [0.72, 0.2]], { lw: 2 }); line([[-0.7, 0.2], [-0.72, 0.34], [-0.38, 0.34], [-0.4, 0.2]], { lw: 1.6, color: INK_SOFT, passes: 1 }); line([[0.38, 0.2], [0.4, 0.34], [0.7, 0.34], [0.72, 0.2]], { lw: 1.6, color: INK_SOFT, passes: 1 }); line([[-0.3, 0.6], [0.3, 0.6]], { lw: 2.6 }); line([[-0.2, 0.68], [0.2, 0.68]], { lw: 2 }); }, sword() { line([[0.1, -0.6], [0.0, -0.05], [-0.02, 0.15]], { lw: 2.6 }); line([[0.1, -0.6], [0.05, -0.05], [0.02, 0.15]], { lw: 1.4, color: INK_SOFT, passes: 1 }); fill([[0.1, -0.6], [0.0, -0.05], [0.05, -0.05]], 'rgba(150,150,160,0.10)'); line([[-0.25, 0.05], [0.22, -0.02]], { lw: 3 }); line([[-0.02, 0.15], [0.05, 0.42]], { lw: 2.4 }); circle(0.02, 0.45, 0.05, { lw: 1.6, passes: 1 }); line([[-0.5, 0.6], [0.5, 0.6]], { lw: 1.4, color: INK_FAINT, passes: 1 }); line([[0.15, -0.5], [0.28, -0.42]], { lw: 1.2, color: INK_FAINT, passes: 1 }); line([[0.18, -0.3], [0.32, -0.22]], { lw: 1, color: INK_FAINT, passes: 1 }); }, }; const draw = types[entry.sketch] || types.fossil; draw(); return canvas; }