Add 3D loose-leaf chronicle of human civilisation
A Three.js site presenting 20 world history milestones as double-sided loose-leaf pages, with three views: a leather binder with page-flip animation, a golden timeline, and a dotted globe with great-circle arcs. All textures (paper, leather, registration cards, and 18 hand-drawn sketches) are generated procedurally via Canvas 2D, so no external image assets are needed beyond the Earth map. Includes dev helper scripts for screenshots, ASCII previews, and headless verification.
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
// 程序化"素描"生成器:用带手绘抖动的线条绘制各条目的速写。
|
||||
// 同一词条每次渲染结果一致(基于名称的种子),供 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.42;
|
||||
|
||||
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 });
|
||||
},
|
||||
};
|
||||
|
||||
const draw = types[entry.sketch] || types.fossil;
|
||||
draw();
|
||||
return canvas;
|
||||
}
|
||||
Reference in New Issue
Block a user