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,349 @@
|
||||
import * as THREE from 'three';
|
||||
import { CATEGORY_INFO } from '../data/entries.js';
|
||||
import { renderSketch } from './sketch.js';
|
||||
|
||||
// —— 页面几何 / 纹理常量 ——
|
||||
export const PAGE_W = 1.6; // 世界单位:页宽
|
||||
export const PAGE_H = 2.24; // 世界单位:页高
|
||||
export const TEX_W = 768;
|
||||
export const TEX_H = 1075;
|
||||
export const HOLE_U = [0.085, 0.24, 0.4, 0.56, 0.72, 0.855]; // 活页孔(自顶部归一化)
|
||||
export const HOLE_X = 0.065; // 孔中心相对页内缘(环前交点 0.085 − 页缘空隙 0.02)
|
||||
export const HOLE_R = 0.052; // 孔半径(世界)
|
||||
|
||||
const SERIF = '"Noto Serif SC","Songti SC","STSong","SimSun",Georgia,serif';
|
||||
const KAI = '"Kaiti SC","STKaiti","KaiTi","Noto Serif SC",serif';
|
||||
const NUM = 'Georgia,"Times New Roman",serif';
|
||||
|
||||
// 版面边距(左边缘避开活页孔,右/上/下为常规边距)
|
||||
const MARGIN = { left: 86, right: 58, top: 50, bottom: 50 };
|
||||
const CONTENT_L = MARGIN.left + 18; // 内容左界
|
||||
const CONTENT_R = TEX_W - MARGIN.right - 18; // 内容右界
|
||||
const CONTENT_W = CONTENT_R - CONTENT_L; // 内容宽度
|
||||
|
||||
// —— 基础纸张 ——
|
||||
export function makePaper(w, h, { tone = 0, ruled = false, margin = false, seed = 0 } = {}) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = w; c.height = h;
|
||||
const ctx = c.getContext('2d');
|
||||
|
||||
// 底色(暖黄纸)
|
||||
const base = [232 - tone, 216 - tone, 182 - tone];
|
||||
const g = ctx.createLinearGradient(0, 0, 0, h);
|
||||
g.addColorStop(0, `rgb(${base[0] + 6},${base[1] + 5},${base[2] + 4})`);
|
||||
g.addColorStop(0.5, `rgb(${base[0]},${base[1]},${base[2]})`);
|
||||
g.addColorStop(1, `rgb(${base[0] - 8},${base[1] - 7},${base[2] - 6})`);
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// 随机纤维与斑点
|
||||
const rand = mulberry(seed + 1);
|
||||
for (let i = 0; i < w * h / 90; i++) {
|
||||
const x = rand() * w, y = rand() * h;
|
||||
const dark = rand() > 0.5;
|
||||
ctx.fillStyle = dark ? 'rgba(120,95,60,0.10)' : 'rgba(255,250,230,0.14)';
|
||||
ctx.fillRect(x, y, rand() * 2 + 0.4, rand() * 2 + 0.4);
|
||||
}
|
||||
// 纤维线条
|
||||
ctx.strokeStyle = 'rgba(140,110,70,0.07)';
|
||||
ctx.lineWidth = 0.7;
|
||||
for (let i = 0; i < w * h / 2600; i++) {
|
||||
const x = rand() * w, y = rand() * h, a = rand() * Math.PI, l = rand() * 22 + 6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l);
|
||||
ctx.stroke();
|
||||
}
|
||||
// 纸面晕染
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const x = rand() * w, y = rand() * h, r = rand() * 140 + 40;
|
||||
const gr = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||||
const warm = rand() > 0.5;
|
||||
gr.addColorStop(0, warm ? 'rgba(180,150,95,0.05)' : 'rgba(120,100,70,0.05)');
|
||||
gr.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = gr;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
|
||||
// 边缘做旧(暗角)
|
||||
const edge = ctx.createRadialGradient(w / 2, h / 2, Math.min(w, h) * 0.4, w / 2, h / 2, Math.max(w, h) * 0.72);
|
||||
edge.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
edge.addColorStop(1, 'rgba(90,60,30,0.16)');
|
||||
ctx.fillStyle = edge;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// 横格线
|
||||
if (ruled) {
|
||||
ctx.strokeStyle = 'rgba(120,95,60,0.28)';
|
||||
ctx.lineWidth = 0.8;
|
||||
for (let y = 90; y < h - 60; y += 34) {
|
||||
ctx.beginPath(); ctx.moveTo(margin ? 84 : 40, y); ctx.lineTo(w - 40, y); ctx.stroke();
|
||||
}
|
||||
}
|
||||
// 红色边线
|
||||
if (margin) {
|
||||
ctx.strokeStyle = 'rgba(160,60,40,0.45)';
|
||||
ctx.lineWidth = 1.1;
|
||||
ctx.beginPath(); ctx.moveTo(84, 40); ctx.lineTo(84, h - 40); ctx.stroke();
|
||||
}
|
||||
return { canvas: c, ctx };
|
||||
}
|
||||
|
||||
function mulberry(seed) {
|
||||
let a = seed || 1;
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
// 画活页孔
|
||||
function drawHoles(ctx, w, h) {
|
||||
const hx = (HOLE_X / PAGE_W) * w;
|
||||
const hr = (HOLE_R / PAGE_W) * w;
|
||||
for (const u of HOLE_U) {
|
||||
const y = u * h;
|
||||
// 孔内阴影(穿过去的感觉)
|
||||
const g = ctx.createRadialGradient(hx, y, 0, hx, y, hr);
|
||||
g.addColorStop(0, '#2b1f13');
|
||||
g.addColorStop(0.62, '#4a3826');
|
||||
g.addColorStop(0.82, '#6b5740');
|
||||
g.addColorStop(1, '#8a735a');
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath(); ctx.arc(hx, y, hr, 0, Math.PI * 2); ctx.fill();
|
||||
// 孔缘
|
||||
ctx.strokeStyle = 'rgba(70,50,32,0.85)';
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath(); ctx.arc(hx, y, hr, 0, Math.PI * 2); ctx.stroke();
|
||||
ctx.strokeStyle = 'rgba(255,244,214,0.35)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.arc(hx, y, hr - 2, Math.PI * 0.15, Math.PI * 0.85); ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// 描边 + 填充的通用文字
|
||||
function text(ctx, str, x, y, { font, size, color = '#3a2d22', align = 'left', baseline = 'alphabetic', weight = 'normal', ls = 0 } = {}) {
|
||||
ctx.font = `${weight} ${size}px ${font}`;
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = align;
|
||||
ctx.textBaseline = baseline;
|
||||
ctx.fillText(str, x, y);
|
||||
}
|
||||
|
||||
// 双线装饰框(左边缘避开活页孔)
|
||||
function drawBorder(ctx, w, h, color = 'rgba(90,61,43,0.55)') {
|
||||
const m = MARGIN;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.strokeRect(m.left, m.top, w - m.left - m.right, h - m.top - m.bottom);
|
||||
ctx.lineWidth = 0.7;
|
||||
ctx.strokeRect(m.left + 8, m.top + 8, w - m.left - m.right - 16, h - m.top - m.bottom - 16);
|
||||
// 角饰
|
||||
ctx.fillStyle = color;
|
||||
const corners = [[m.left, m.top], [w - m.right, m.top], [m.left, h - m.bottom], [w - m.right, h - m.bottom]];
|
||||
for (const [cx, cy] of corners) {
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
ctx.rotate(Math.PI / 4);
|
||||
ctx.fillRect(-3, -3, 6, 6);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// 类别印章
|
||||
function drawSeal(ctx, category, x, y, size = 52) {
|
||||
const info = CATEGORY_INFO[category] || CATEGORY_INFO['事'];
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate(-0.05);
|
||||
ctx.fillStyle = info.color;
|
||||
ctx.globalAlpha = 0.92;
|
||||
ctx.fillRect(-size / 2, -size / 2, size, size);
|
||||
ctx.strokeStyle = 'rgba(255,240,210,0.85)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(-size / 2 + 4, -size / 2 + 4, size - 8, size - 8);
|
||||
ctx.fillStyle = '#f5e8cf';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = `${size * 0.62}px ${KAI}`;
|
||||
ctx.fillText(category, 0, 2);
|
||||
ctx.restore();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// —— 正面:登记卡 ——
|
||||
export function renderFront(entry) {
|
||||
const w = TEX_W, h = TEX_H;
|
||||
const { canvas, ctx } = makePaper(w, h, { seed: hash(entry.id) });
|
||||
const info = CATEGORY_INFO[entry.category] || CATEGORY_INFO['事'];
|
||||
|
||||
drawBorder(ctx, w, h);
|
||||
drawHoles(ctx, w, h);
|
||||
|
||||
// 页眉:编号 + 印章
|
||||
text(ctx, '编 号 · ' + entry.id, CONTENT_L, 92, { font: NUM, size: 26, color: '#5a432c' });
|
||||
drawSeal(ctx, entry.category, CONTENT_R - 30, 86, 58);
|
||||
text(ctx, info.en, CONTENT_R - 30, 128, { font: NUM, size: 15, color: '#7a6147', align: 'center' });
|
||||
|
||||
// 顶部分隔线
|
||||
ctx.strokeStyle = 'rgba(90,61,43,0.4)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.moveTo(CONTENT_L, 160); ctx.lineTo(CONTENT_R, 160); ctx.stroke();
|
||||
|
||||
// 名称(大字,居中)
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = `700 ${fitFont(ctx, entry.name, CONTENT_W - 40, 36, 86)}px ${KAI}`;
|
||||
ctx.fillStyle = '#33271a';
|
||||
ctx.fillText(entry.name, w / 2, 248);
|
||||
// 名称下划线装饰
|
||||
ctx.strokeStyle = info.color;
|
||||
ctx.lineWidth = 2.4;
|
||||
ctx.beginPath(); ctx.moveTo(w / 2 - 220, 314); ctx.lineTo(w / 2 + 220, 314); ctx.stroke();
|
||||
ctx.fillStyle = info.color;
|
||||
ctx.beginPath(); ctx.arc(w / 2, 314, 4, 0, Math.PI * 2); ctx.fill();
|
||||
|
||||
// 登记字段
|
||||
const labelX = CONTENT_L + 56; // 标签右对齐
|
||||
const colonX = CONTENT_L + 64; // 冒号
|
||||
const valX = CONTENT_L + 82; // 值起始
|
||||
const fy = 372, rowH = 62;
|
||||
const fields = [
|
||||
['时间', entry.time],
|
||||
['地区', entry.region],
|
||||
['类别', `${entry.category} · ${info.en}`],
|
||||
];
|
||||
fields.forEach(([label, value], i) => {
|
||||
const y = fy + i * rowH;
|
||||
text(ctx, label, labelX, y, { font: SERIF, size: 26, color: '#7a5c38', align: 'right' });
|
||||
text(ctx, ':', colonX, y, { font: SERIF, size: 26, color: '#7a5c38' });
|
||||
text(ctx, value, valX, y, { font: SERIF, size: 26, color: '#33271a' });
|
||||
// 值后虚线延伸至右界
|
||||
const vw = ctx.measureText(value).width;
|
||||
ctx.strokeStyle = 'rgba(120,95,60,0.35)';
|
||||
ctx.lineWidth = 0.9;
|
||||
ctx.setLineDash([2, 5]);
|
||||
ctx.beginPath(); ctx.moveTo(valX + vw + 14, y + 12); ctx.lineTo(CONTENT_R, y + 12); ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
});
|
||||
|
||||
// 描述区
|
||||
const dy = 600;
|
||||
text(ctx, '描 述', CONTENT_L, dy, { font: SERIF, size: 26, color: '#7a5c38' });
|
||||
ctx.strokeStyle = 'rgba(120,95,60,0.3)';
|
||||
ctx.lineWidth = 0.9;
|
||||
ctx.beginPath(); ctx.moveTo(CONTENT_L, dy + 18); ctx.lineTo(CONTENT_R, dy + 18); ctx.stroke();
|
||||
|
||||
ctx.font = `400 24px ${SERIF}`;
|
||||
ctx.fillStyle = '#33271a';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
const maxW = CONTENT_W;
|
||||
let line = '', yy = dy + 40;
|
||||
for (const ch of entry.description) {
|
||||
const test = line + ch;
|
||||
if (ctx.measureText(test).width > maxW && line) {
|
||||
ctx.fillText(line, CONTENT_L, yy);
|
||||
line = ch; yy += 40;
|
||||
} else line = test;
|
||||
}
|
||||
if (line) ctx.fillText(line, CONTENT_L, yy);
|
||||
|
||||
// 页脚
|
||||
text(ctx, '人类文明里程碑 · 活页编年史', w / 2, h - 52, { font: SERIF, size: 19, color: '#7a6147', align: 'center' });
|
||||
text(ctx, entry.id, CONTENT_R, h - 52, { font: NUM, size: 18, color: '#8a735a', align: 'right' });
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// —— 背面:素描 ——
|
||||
export function renderBack(entry) {
|
||||
const w = TEX_W, h = TEX_H;
|
||||
const { canvas, ctx } = makePaper(w, h, { seed: hash(entry.id) + 7 });
|
||||
|
||||
drawBorder(ctx, w, h);
|
||||
drawHoles(ctx, w, h);
|
||||
|
||||
// 内容水平镜像(背面用 BackSide 渲染,镜像后正读)——关于竖直中线镜像
|
||||
ctx.save();
|
||||
ctx.translate(w / 2, 0);
|
||||
ctx.scale(-1, 1);
|
||||
ctx.translate(-w / 2, 0);
|
||||
|
||||
// 页眉(居中)
|
||||
text(ctx, '素 描 · SKETCH', w / 2, 92, { font: NUM, size: 24, color: '#6b5740', align: 'center' });
|
||||
text(ctx, entry.id, w / 2, 124, { font: NUM, size: 20, color: '#8a735a', align: 'center' });
|
||||
ctx.strokeStyle = 'rgba(90,61,43,0.35)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.moveTo(w / 2 - 90, 152); ctx.lineTo(w / 2 + 90, 152); ctx.stroke();
|
||||
|
||||
// 速写(居中)
|
||||
const sketch = renderSketch(entry, 520);
|
||||
const sx = (w - 520) / 2;
|
||||
const sy = 170;
|
||||
ctx.drawImage(sketch, sx, sy);
|
||||
ctx.strokeStyle = 'rgba(90,61,43,0.3)';
|
||||
ctx.lineWidth = 0.8;
|
||||
ctx.strokeRect(sx - 8, sy - 8, 520 + 16, 520 + 16);
|
||||
|
||||
// 题注(居中)
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = `700 ${fitFont(ctx, entry.name, CONTENT_W - 40, 32, 78)}px ${KAI}`;
|
||||
ctx.fillStyle = '#33271a';
|
||||
ctx.fillText(entry.name, w / 2, 772);
|
||||
|
||||
ctx.font = `400 25px ${SERIF}`;
|
||||
ctx.fillStyle = '#6b5740';
|
||||
ctx.fillText(`${entry.time} · ${entry.region}`, w / 2, 828);
|
||||
|
||||
// 波浪线(居中)
|
||||
ctx.strokeStyle = 'rgba(90,61,43,0.5)';
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.beginPath();
|
||||
for (let x = w / 2 - 130; x <= w / 2 + 130; x += 10) {
|
||||
const yy = 868 + Math.sin((x - w / 2) / 26) * 5;
|
||||
if (x === w / 2 - 130) ctx.moveTo(x, yy); else ctx.lineTo(x, yy);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// 名称自适应字号
|
||||
function fitFont(ctx, str, maxW, minSize, maxSize) {
|
||||
if (minSize > maxSize) [minSize, maxSize] = [maxSize, minSize]; // 容错:确保 min < max
|
||||
let size = maxSize;
|
||||
ctx.font = `700 ${size}px ${KAI}`;
|
||||
while (ctx.measureText(str).width > maxW && size > minSize) {
|
||||
size -= 2;
|
||||
ctx.font = `700 ${size}px ${KAI}`;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
function hash(str) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0;
|
||||
return h;
|
||||
}
|
||||
|
||||
// 生成页面的两个纹理
|
||||
const texCache = new Map();
|
||||
export function getPageTextures(entry) {
|
||||
if (texCache.has(entry.id)) return texCache.get(entry.id);
|
||||
const front = new THREE.CanvasTexture(renderFront(entry));
|
||||
front.colorSpace = THREE.SRGBColorSpace;
|
||||
front.anisotropy = 8;
|
||||
|
||||
const back = new THREE.CanvasTexture(renderBack(entry));
|
||||
back.colorSpace = THREE.SRGBColorSpace;
|
||||
back.anisotropy = 8;
|
||||
|
||||
const pair = { front, back };
|
||||
texCache.set(entry.id, pair);
|
||||
return pair;
|
||||
}
|
||||
@@ -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