Spaces:
Running
Running
Commit ·
55303d0
1
Parent(s): 6ea47d5
Step 3: 手绘动画引擎 + 15 个场景 + 横竖屏布局
Browse files- README.md +1 -1
- shouhouzi/fonts/kuaile.woff2 +0 -0
- shouhouzi/js/draw.js +629 -0
- shouhouzi/js/engine.js +304 -0
- shouhouzi/js/scenes.js +897 -0
- shouhouzi/tools/frame-test.html +9 -0
- shouhouzi/tools/subset_font.py +15 -0
README.md
CHANGED
|
@@ -24,7 +24,7 @@ pinned: false
|
|
| 24 |
|---|------|------|
|
| 25 |
| 1 | 写计划 / 大纲 / 步骤(本文件) | ✅ 完成 |
|
| 26 |
| 2 | 旁白文案定稿 + TTS 生成配音 + 时长表(15 场景 / 77 句 / 约 6 分钟,旁白+商人+助手三种声线) | ✅ 完成 |
|
| 27 |
-
| 3 | 动画引擎 + 全部场景(横屏 16:9 / 竖屏 9:16 双布局) |
|
| 28 |
| 4 | 程序化音乐 + 音效 + 旁白混音(WebAudio 离线渲染) | ⏳ |
|
| 29 |
| 5 | 播放器 UI + MP4 本地导出(24/30/60fps,横/竖屏) | ⏳ |
|
| 30 |
| 6 | 抖音 / B站 发布文案(可一键复制) | ⏳ |
|
|
|
|
| 24 |
|---|------|------|
|
| 25 |
| 1 | 写计划 / 大纲 / 步骤(本文件) | ✅ 完成 |
|
| 26 |
| 2 | 旁白文案定稿 + TTS 生成配音 + 时长表(15 场景 / 77 句 / 约 6 分钟,旁白+商人+助手三种声线) | ✅ 完成 |
|
| 27 |
+
| 3 | 动画引擎 + 全部 15 个场景(横屏 16:9 / 竖屏 9:16 双布局,已用无头浏览器逐句截图验证) | ✅ 完成 |
|
| 28 |
| 4 | 程序化音乐 + 音效 + 旁白混音(WebAudio 离线渲染) | ⏳ |
|
| 29 |
| 5 | 播放器 UI + MP4 本地导出(24/30/60fps,横/竖屏) | ⏳ |
|
| 30 |
| 6 | 抖音 / B站 发布文案(可一键复制) | ⏳ |
|
shouhouzi/fonts/kuaile.woff2
ADDED
|
Binary file (69.8 kB). View file
|
|
|
shouhouzi/js/draw.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =========================================================
|
| 2 |
+
* draw.js —— 手绘涂鸦风绘图库(角色 + 道具),全部程序化绘制
|
| 3 |
+
* 约定:所有函数都是纯函数,给定参数和 D.boil 就得到确定的画面
|
| 4 |
+
* ========================================================= */
|
| 5 |
+
(function () {
|
| 6 |
+
const TAU = Math.PI * 2;
|
| 7 |
+
const INK = '#2b2321';
|
| 8 |
+
const FONT = '"KuaiLe","ZCOOL KuaiLe","PingFang SC","Microsoft YaHei","Noto Sans SC",sans-serif';
|
| 9 |
+
const C = {
|
| 10 |
+
ink: INK, paper: '#fbf3e1', sky: '#bfe7ff', sky2: '#eaf7ff', grass: '#9fd468', grass2: '#7fbf4f',
|
| 11 |
+
forest: '#4f9e55', forest2: '#3b8346', fur: '#a8693b', fur2: '#8a5229', face: '#f5d6a8',
|
| 12 |
+
gold: '#ffcd3c', gold2: '#f2a51a', red: '#ff5b4f', green: '#38b86a', blue: '#4a8fe2',
|
| 13 |
+
purple: '#7a5cc7', pink: '#ff8fb1', skin: '#ffd9b8', white: '#fffdf8', grey: '#9aa0a6',
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
function hash(n) { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453123; return s - Math.floor(s); }
|
| 17 |
+
const D = { TAU, INK, C, FONT, boil: 0, hash };
|
| 18 |
+
// 线条"抖动":随 boil 每秒变化 8 次,营造手绘动画感
|
| 19 |
+
function jit(seed, amt) { return (hash(seed * 1.37 + D.boil * 17.13) - 0.5) * 2 * amt; }
|
| 20 |
+
D.jit = jit;
|
| 21 |
+
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
| 22 |
+
D.clamp01 = clamp01;
|
| 23 |
+
D.lerp = (a, b, k) => a + (b - a) * k;
|
| 24 |
+
|
| 25 |
+
/* ---------- 缓动 ---------- */
|
| 26 |
+
const E = {
|
| 27 |
+
lin: clamp01,
|
| 28 |
+
out: (x) => 1 - Math.pow(1 - clamp01(x), 3),
|
| 29 |
+
in: (x) => Math.pow(clamp01(x), 3),
|
| 30 |
+
inout: (x) => { x = clamp01(x); return x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; },
|
| 31 |
+
back: (x) => { x = clamp01(x); const c = 1.9; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); },
|
| 32 |
+
elastic: (x) => { x = clamp01(x); if (x === 0 || x === 1) return x; return Math.pow(2, -10 * x) * Math.sin((x * 10 - .75) * (TAU / 3)) + 1; },
|
| 33 |
+
bounce: (x) => { x = clamp01(x); const n = 7.5625, d = 2.75; if (x < 1 / d) return n * x * x; if (x < 2 / d) return n * (x -= 1.5 / d) * x + .75; if (x < 2.5 / d) return n * (x -= 2.25 / d) * x + .9375; return n * (x -= 2.625 / d) * x + .984375; },
|
| 34 |
+
};
|
| 35 |
+
D.E = E;
|
| 36 |
+
|
| 37 |
+
/* ---------- 基础路径 ---------- */
|
| 38 |
+
D.blob = function (ctx, cx, cy, rx, ry, seed = 1, amt = 0.035, n = 16) {
|
| 39 |
+
const pts = [];
|
| 40 |
+
for (let i = 0; i < n; i++) {
|
| 41 |
+
const a = i / n * TAU, r = 1 + jit(seed + i * 3.1, amt);
|
| 42 |
+
pts.push([cx + Math.cos(a) * rx * r, cy + Math.sin(a) * ry * r]);
|
| 43 |
+
}
|
| 44 |
+
ctx.beginPath();
|
| 45 |
+
let p0 = pts[n - 1], p1 = pts[0];
|
| 46 |
+
ctx.moveTo((p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
|
| 47 |
+
for (let i = 0; i < n; i++) {
|
| 48 |
+
const p = pts[i], q = pts[(i + 1) % n];
|
| 49 |
+
ctx.quadraticCurveTo(p[0], p[1], (p[0] + q[0]) / 2, (p[1] + q[1]) / 2);
|
| 50 |
+
}
|
| 51 |
+
ctx.closePath();
|
| 52 |
+
};
|
| 53 |
+
D.rrect = function (ctx, x, y, w, h, r = 12, seed = 1, amt = 1.6) {
|
| 54 |
+
const j = (k) => jit(seed + k, amt);
|
| 55 |
+
r = Math.min(r, w / 2, h / 2);
|
| 56 |
+
ctx.beginPath();
|
| 57 |
+
ctx.moveTo(x + r + j(1), y + j(2));
|
| 58 |
+
ctx.quadraticCurveTo(x + w / 2, y + j(3), x + w - r + j(4), y + j(5));
|
| 59 |
+
ctx.quadraticCurveTo(x + w, y, x + w + j(6), y + r + j(7));
|
| 60 |
+
ctx.quadraticCurveTo(x + w + j(8), y + h / 2, x + w + j(9), y + h - r + j(10));
|
| 61 |
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r + j(11), y + h + j(12));
|
| 62 |
+
ctx.quadraticCurveTo(x + w / 2, y + h + j(13), x + r + j(14), y + h + j(15));
|
| 63 |
+
ctx.quadraticCurveTo(x, y + h, x + j(16), y + h - r + j(17));
|
| 64 |
+
ctx.quadraticCurveTo(x + j(18), y + h / 2, x + j(19), y + r + j(20));
|
| 65 |
+
ctx.quadraticCurveTo(x, y, x + r + j(1), y + j(2));
|
| 66 |
+
ctx.closePath();
|
| 67 |
+
};
|
| 68 |
+
D.line = function (ctx, x1, y1, x2, y2, seed = 1, amt = 2) {
|
| 69 |
+
ctx.beginPath();
|
| 70 |
+
ctx.moveTo(x1 + jit(seed, amt * .5), y1 + jit(seed + 1, amt * .5));
|
| 71 |
+
ctx.quadraticCurveTo((x1 + x2) / 2 + jit(seed + 2, amt), (y1 + y2) / 2 + jit(seed + 3, amt), x2 + jit(seed + 4, amt * .5), y2 + jit(seed + 5, amt * .5));
|
| 72 |
+
};
|
| 73 |
+
D.poly = function (ctx, pts, seed = 1, amt = 1.5) {
|
| 74 |
+
ctx.beginPath();
|
| 75 |
+
pts.forEach((p, i) => { const x = p[0] + jit(seed + i * 2, amt), y = p[1] + jit(seed + i * 2 + 1, amt); i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); });
|
| 76 |
+
ctx.closePath();
|
| 77 |
+
};
|
| 78 |
+
D.fs = function (ctx, fill, lw = 5, stroke = INK) {
|
| 79 |
+
if (fill) { ctx.fillStyle = fill; ctx.fill(); }
|
| 80 |
+
if (lw) { ctx.lineWidth = lw; ctx.strokeStyle = stroke; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.stroke(); }
|
| 81 |
+
};
|
| 82 |
+
D.stroke = function (ctx, lw = 5, color = INK) { ctx.lineWidth = lw; ctx.strokeStyle = color; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.stroke(); };
|
| 83 |
+
D.thick = function (ctx, x1, y1, x2, y2, color, w, seed = 1) { // 带描边的粗线(手臂/腿)
|
| 84 |
+
D.line(ctx, x1, y1, x2, y2, seed, 1.5); D.stroke(ctx, w + 8, INK);
|
| 85 |
+
D.line(ctx, x1, y1, x2, y2, seed, 1.5); D.stroke(ctx, w, color);
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
/* ---------- 文字 ---------- */
|
| 89 |
+
D.font = (size) => `${size}px ${FONT}`;
|
| 90 |
+
D.text = function (ctx, str, x, y, size, color = INK, o = {}) {
|
| 91 |
+
ctx.save();
|
| 92 |
+
ctx.translate(x, y);
|
| 93 |
+
if (o.rot) ctx.rotate(o.rot);
|
| 94 |
+
if (o.scale) ctx.scale(o.scale, o.scale);
|
| 95 |
+
ctx.font = D.font(size);
|
| 96 |
+
ctx.textAlign = o.align || 'center';
|
| 97 |
+
ctx.textBaseline = o.base || 'middle';
|
| 98 |
+
if (o.alpha != null) ctx.globalAlpha *= o.alpha;
|
| 99 |
+
if (o.stroke) { ctx.lineWidth = o.stroke; ctx.strokeStyle = o.strokeColor || INK; ctx.lineJoin = 'round'; ctx.strokeText(str, 0, 0); }
|
| 100 |
+
if (o.shadow) { ctx.fillStyle = o.shadow; ctx.fillText(str, o.sx || 4, o.sy || 6); }
|
| 101 |
+
ctx.fillStyle = color; ctx.fillText(str, 0, 0);
|
| 102 |
+
ctx.restore();
|
| 103 |
+
};
|
| 104 |
+
// 粗描边大字(海报字)
|
| 105 |
+
D.bigText = function (ctx, str, x, y, size, color, o = {}) {
|
| 106 |
+
D.text(ctx, str, x, y, size, color, Object.assign({ stroke: size * 0.16, shadow: 'rgba(43,35,33,.9)', sx: size * .05, sy: size * .08 }, o));
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
/* ---------- 通用变换 ---------- */
|
| 110 |
+
D.at = function (ctx, x, y, s, fn, rot = 0, alpha = 1) {
|
| 111 |
+
if (s <= 0.001 || alpha <= 0.001) return;
|
| 112 |
+
ctx.save(); ctx.translate(x, y); if (rot) ctx.rotate(rot); ctx.scale(s, s);
|
| 113 |
+
if (alpha < 1) ctx.globalAlpha *= alpha;
|
| 114 |
+
fn(); ctx.restore();
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
/* =======================================================
|
| 118 |
+
* 角色:猴子
|
| 119 |
+
* 原点在脚底中心,身高约 165
|
| 120 |
+
* o: {t, seed, flip, mood:'happy'|'shock'|'sad'|'smug'|'love', aL, aR, fur, blink, hold}
|
| 121 |
+
* ======================================================= */
|
| 122 |
+
D.monkey = function (ctx, x, y, s = 1, o = {}) {
|
| 123 |
+
const t = o.t || 0, sd = o.seed || 1;
|
| 124 |
+
const fur = o.fur || C.fur, face = C.face;
|
| 125 |
+
ctx.save(); ctx.translate(x, y); ctx.scale(o.flip ? -s : s, s); if (o.rot) ctx.rotate(o.rot);
|
| 126 |
+
// 尾巴
|
| 127 |
+
ctx.beginPath(); ctx.moveTo(22, -38);
|
| 128 |
+
ctx.bezierCurveTo(78, -26, 96, -98, 62 + Math.sin(t * 3 + sd) * 6, -112);
|
| 129 |
+
ctx.quadraticCurveTo(44, -122, 50, -100);
|
| 130 |
+
D.stroke(ctx, 15, INK);
|
| 131 |
+
ctx.beginPath(); ctx.moveTo(22, -38);
|
| 132 |
+
ctx.bezierCurveTo(78, -26, 96, -98, 62 + Math.sin(t * 3 + sd) * 6, -112);
|
| 133 |
+
ctx.quadraticCurveTo(44, -122, 50, -100);
|
| 134 |
+
D.stroke(ctx, 8, fur);
|
| 135 |
+
// 腿
|
| 136 |
+
D.blob(ctx, -18, -10, 17, 12, sd + 1); D.fs(ctx, fur, 4);
|
| 137 |
+
D.blob(ctx, 18, -10, 17, 12, sd + 2); D.fs(ctx, fur, 4);
|
| 138 |
+
// 身体
|
| 139 |
+
D.blob(ctx, 0, -52, 36, 42, sd + 3); D.fs(ctx, fur, 5);
|
| 140 |
+
D.blob(ctx, 0, -46, 22, 27, sd + 4); D.fs(ctx, face, 0);
|
| 141 |
+
// 手臂
|
| 142 |
+
const arm = (side, a) => {
|
| 143 |
+
const sx = side * 28, sy = -74, L = 46;
|
| 144 |
+
const ex = sx + side * Math.sin(a) * L, ey = sy + Math.cos(a) * L;
|
| 145 |
+
D.thick(ctx, sx, sy, ex, ey, fur, 9, sd + side * 7);
|
| 146 |
+
D.blob(ctx, ex, ey, 9, 9, sd + side * 9); D.fs(ctx, face, 4);
|
| 147 |
+
return [ex, ey];
|
| 148 |
+
};
|
| 149 |
+
const hL = arm(-1, o.aL != null ? o.aL : 0.35);
|
| 150 |
+
const hR = arm(1, o.aR != null ? o.aR : 0.35);
|
| 151 |
+
// 头
|
| 152 |
+
const hy = -122;
|
| 153 |
+
D.blob(ctx, -44, hy - 2, 17, 17, sd + 5); D.fs(ctx, fur, 5);
|
| 154 |
+
D.blob(ctx, 44, hy - 2, 17, 17, sd + 6); D.fs(ctx, fur, 5);
|
| 155 |
+
D.blob(ctx, -44, hy - 2, 9, 9, sd + 7); D.fs(ctx, C.pink, 0);
|
| 156 |
+
D.blob(ctx, 44, hy - 2, 9, 9, sd + 8); D.fs(ctx, C.pink, 0);
|
| 157 |
+
D.blob(ctx, 0, hy, 42, 39, sd + 9); D.fs(ctx, fur, 5);
|
| 158 |
+
D.blob(ctx, -14, hy - 2, 17, 16, sd + 10); D.fs(ctx, face, 0);
|
| 159 |
+
D.blob(ctx, 14, hy - 2, 17, 16, sd + 11); D.fs(ctx, face, 0);
|
| 160 |
+
D.blob(ctx, 0, hy + 17, 27, 17, sd + 12); D.fs(ctx, face, 3);
|
| 161 |
+
// 表情
|
| 162 |
+
const mood = o.mood || 'happy';
|
| 163 |
+
const blink = o.blink != null ? o.blink : ((t * 0.7 + sd * 0.37) % 3.2 < 0.12);
|
| 164 |
+
ctx.fillStyle = INK;
|
| 165 |
+
if (mood === 'shock') {
|
| 166 |
+
for (const ex of [-14, 14]) { D.blob(ctx, ex, hy - 3, 9, 10, sd + ex); D.fs(ctx, '#fff', 3); ctx.beginPath(); ctx.arc(ex, hy - 2, 4, 0, TAU); ctx.fillStyle = INK; ctx.fill(); }
|
| 167 |
+
D.blob(ctx, 0, hy + 20, 7, 9, sd + 30); D.fs(ctx, '#7a2b22', 3);
|
| 168 |
+
} else if (mood === 'love') {
|
| 169 |
+
for (const ex of [-14, 14]) D.text(ctx, '$', ex, hy - 2, 26, C.green, { stroke: 4 });
|
| 170 |
+
ctx.beginPath(); ctx.arc(0, hy + 14, 10, 0.1, Math.PI - 0.1); D.stroke(ctx, 4);
|
| 171 |
+
} else {
|
| 172 |
+
for (const ex of [-14, 14]) {
|
| 173 |
+
if (blink || mood === 'smug') { ctx.beginPath(); ctx.moveTo(ex - 6, hy - 2); ctx.lineTo(ex + 6, hy - 2); D.stroke(ctx, 4); }
|
| 174 |
+
else { ctx.beginPath(); ctx.arc(ex, hy - 3, 5.5, 0, TAU); ctx.fillStyle = INK; ctx.fill(); ctx.beginPath(); ctx.arc(ex + 2, hy - 5, 2, 0, TAU); ctx.fillStyle = '#fff'; ctx.fill(); }
|
| 175 |
+
}
|
| 176 |
+
ctx.beginPath(); ctx.arc(-5, hy + 10, 2, 0, TAU); ctx.arc(5, hy + 10, 2, 0, TAU); ctx.fillStyle = INK; ctx.fill();
|
| 177 |
+
ctx.beginPath();
|
| 178 |
+
if (mood === 'sad') { ctx.arc(0, hy + 26, 9, Math.PI + .3, -.3); D.stroke(ctx, 4); ctx.beginPath(); ctx.moveTo(-22, hy - 16); ctx.lineTo(-8, hy - 20); ctx.moveTo(22, hy - 16); ctx.lineTo(8, hy - 20); D.stroke(ctx, 3); }
|
| 179 |
+
else if (mood === 'smug') { ctx.moveTo(-10, hy + 18); ctx.quadraticCurveTo(4, hy + 26, 13, hy + 13); D.stroke(ctx, 4); }
|
| 180 |
+
else { ctx.arc(0, hy + 13, 11, 0.15, Math.PI - 0.15); D.stroke(ctx, 4); }
|
| 181 |
+
ctx.beginPath(); ctx.ellipse(-25, hy + 10, 6, 3.5, 0, 0, TAU); ctx.ellipse(25, hy + 10, 6, 3.5, 0, 0, TAU); ctx.fillStyle = 'rgba(255,120,120,.55)'; ctx.fill();
|
| 182 |
+
}
|
| 183 |
+
if (o.hold) o.hold(ctx, hR[0], hR[1], hL);
|
| 184 |
+
ctx.restore();
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
/* =======================================================
|
| 188 |
+
* 角色:人物(村民 / 商人 / 助手)
|
| 189 |
+
* 原点在脚底中心,身高约 280
|
| 190 |
+
* o: {t, walk(相位,null 为站立), shirt, pants, skin, hat:'straw'|'top'|'cap'|null, hair:'short'|'bun'|'slick'|'bald'|'spiky',
|
| 191 |
+
* hairColor, mustache, glasses:'sun'|'round'|null, tie, belly, thin, mood, aL, aR, holdR, holdL, flip, jump}
|
| 192 |
+
* ======================================================= */
|
| 193 |
+
D.person = function (ctx, x, y, s = 1, o = {}) {
|
| 194 |
+
const t = o.t || 0, sd = o.seed || 3;
|
| 195 |
+
const skin = o.skin || C.skin, shirt = o.shirt || '#5aa9e6', pants = o.pants || '#5b4a3a';
|
| 196 |
+
ctx.save(); ctx.translate(x, y - (o.jump || 0)); ctx.scale(o.flip ? -s : s, s);
|
| 197 |
+
const w = o.walk;
|
| 198 |
+
const sw = w == null ? 0 : Math.sin(w) * 22;
|
| 199 |
+
const bob = w == null ? Math.sin(t * 2 + sd) * 1.5 : Math.abs(Math.cos(w)) * -5;
|
| 200 |
+
ctx.translate(0, bob);
|
| 201 |
+
const bw = o.thin ? 30 : 38;
|
| 202 |
+
// 腿
|
| 203 |
+
D.thick(ctx, -13, -98, -13 + sw, -6, pants, 15, sd + 1);
|
| 204 |
+
D.thick(ctx, 13, -98, 13 - sw, -6, pants, 15, sd + 2);
|
| 205 |
+
D.blob(ctx, -16 + sw, -4, 15, 8, sd + 3); D.fs(ctx, '#3a2f2a', 4);
|
| 206 |
+
D.blob(ctx, 16 - sw, -4, 15, 8, sd + 4); D.fs(ctx, '#3a2f2a', 4);
|
| 207 |
+
// 躯干
|
| 208 |
+
if (o.belly) { D.blob(ctx, 0, -138, bw + 14, 56, sd + 5); D.fs(ctx, shirt, 5); }
|
| 209 |
+
else { D.rrect(ctx, -bw, -192, bw * 2, 102, 22, sd + 5); D.fs(ctx, shirt, 5); }
|
| 210 |
+
if (o.tie) {
|
| 211 |
+
ctx.beginPath(); ctx.moveTo(-9, -188); ctx.lineTo(9, -188); ctx.lineTo(5, -176); ctx.lineTo(10, -128); ctx.lineTo(0, -116); ctx.lineTo(-10, -128); ctx.lineTo(-5, -176); ctx.closePath(); D.fs(ctx, o.tie, 3.5);
|
| 212 |
+
}
|
| 213 |
+
if (o.suit) { // 西装翻领
|
| 214 |
+
ctx.beginPath(); ctx.moveTo(-bw + 6, -190); ctx.lineTo(-12, -150); ctx.lineTo(-20, -190); ctx.closePath(); D.fs(ctx, '#ffffff', 3);
|
| 215 |
+
ctx.beginPath(); ctx.moveTo(bw - 6, -190); ctx.lineTo(12, -150); ctx.lineTo(20, -190); ctx.closePath(); D.fs(ctx, '#ffffff', 3);
|
| 216 |
+
}
|
| 217 |
+
// 手臂
|
| 218 |
+
const arm = (side, a, hold) => {
|
| 219 |
+
const sx = side * (bw - 2), sy = -180, L = 72;
|
| 220 |
+
const swing = w == null ? 0 : side * Math.sin(w) * 0.45;
|
| 221 |
+
const aa = a + swing;
|
| 222 |
+
const ex = sx + side * Math.sin(aa) * L, ey = sy + Math.cos(aa) * L;
|
| 223 |
+
D.thick(ctx, sx, sy, ex, ey, shirt, 13, sd + side * 5);
|
| 224 |
+
D.blob(ctx, ex, ey, 10, 10, sd + side * 6); D.fs(ctx, skin, 4);
|
| 225 |
+
if (hold) { ctx.save(); ctx.translate(ex, ey); hold(ctx); ctx.restore(); }
|
| 226 |
+
};
|
| 227 |
+
// 头
|
| 228 |
+
const hy = -236;
|
| 229 |
+
const drawHead = () => {
|
| 230 |
+
D.blob(ctx, 0, hy, 42, 44, sd + 7); D.fs(ctx, skin, 5);
|
| 231 |
+
const hc = o.hairColor || '#3b2b22';
|
| 232 |
+
const hair = o.hair || 'short';
|
| 233 |
+
if (hair === 'short') { ctx.beginPath(); ctx.moveTo(-42, hy - 4); ctx.quadraticCurveTo(-40, hy - 50, 0, hy - 46); ctx.quadraticCurveTo(40, hy - 50, 42, hy - 6); ctx.quadraticCurveTo(20, hy - 26, -10, hy - 24); ctx.quadraticCurveTo(-30, hy - 20, -42, hy - 4); D.fs(ctx, hc, 4); }
|
| 234 |
+
else if (hair === 'spiky') { D.poly(ctx, [[-42, hy - 6], [-38, hy - 46], [-22, hy - 36], [-12, hy - 58], [2, hy - 38], [16, hy - 58], [24, hy - 36], [40, hy - 46], [42, hy - 6], [0, hy - 24]], sd); D.fs(ctx, hc, 4); }
|
| 235 |
+
else if (hair === 'bun') { D.blob(ctx, 0, hy - 52, 18, 16, sd + 8); D.fs(ctx, hc, 4); ctx.beginPath(); ctx.moveTo(-43, hy + 6); ctx.quadraticCurveTo(-46, hy - 48, 0, hy - 44); ctx.quadraticCurveTo(46, hy - 48, 43, hy + 6); ctx.quadraticCurveTo(30, hy - 24, 0, hy - 26); ctx.quadraticCurveTo(-30, hy - 24, -43, hy + 6); D.fs(ctx, hc, 4); }
|
| 236 |
+
else if (hair === 'slick') { ctx.beginPath(); ctx.moveTo(-43, hy - 2); ctx.quadraticCurveTo(-44, hy - 50, 8, hy - 47); ctx.quadraticCurveTo(46, hy - 42, 43, hy - 4); ctx.quadraticCurveTo(30, hy - 30, -20, hy - 30); ctx.quadraticCurveTo(-34, hy - 24, -43, hy - 2); D.fs(ctx, hc, 4); ctx.beginPath(); ctx.moveTo(-20, hy - 40); ctx.quadraticCurveTo(10, hy - 46, 30, hy - 34); D.stroke(ctx, 2.5, '#777'); }
|
| 237 |
+
// 脸
|
| 238 |
+
const mood = o.mood || 'happy';
|
| 239 |
+
const blink = ((t * 0.6 + sd * 0.29) % 3.6) < 0.12;
|
| 240 |
+
if (o.glasses === 'sun') {
|
| 241 |
+
D.rrect(ctx, -34, hy - 12, 30, 18, 6, sd + 9, .8); D.fs(ctx, '#1b1b1b', 3);
|
| 242 |
+
D.rrect(ctx, 4, hy - 12, 30, 18, 6, sd + 10, .8); D.fs(ctx, '#1b1b1b', 3);
|
| 243 |
+
ctx.beginPath(); ctx.moveTo(-4, hy - 6); ctx.lineTo(4, hy - 6); D.stroke(ctx, 3);
|
| 244 |
+
ctx.beginPath(); ctx.moveTo(-28, hy - 8); ctx.lineTo(-20, hy - 8); D.stroke(ctx, 2.5, 'rgba(255,255,255,.7)');
|
| 245 |
+
} else if (mood === 'shock') {
|
| 246 |
+
for (const ex of [-15, 15]) { D.blob(ctx, ex, hy - 4, 10, 12, sd + ex); D.fs(ctx, '#fff', 3); ctx.beginPath(); ctx.arc(ex, hy - 3, 4, 0, TAU); ctx.fillStyle = INK; ctx.fill(); }
|
| 247 |
+
} else if (mood === 'money') {
|
| 248 |
+
for (const ex of [-15, 15]) D.text(ctx, '¥', ex, hy - 4, 30, C.green, { stroke: 5, strokeColor: INK });
|
| 249 |
+
} else {
|
| 250 |
+
for (const ex of [-15, 15]) {
|
| 251 |
+
if (blink || mood === 'sly') { ctx.beginPath(); ctx.moveTo(ex - 7, hy - 4); ctx.quadraticCurveTo(ex, hy - (mood === 'sly' ? 0 : 6), ex + 7, hy - 4); D.stroke(ctx, 4); }
|
| 252 |
+
else { ctx.beginPath(); ctx.arc(ex, hy - 4, 5.5, 0, TAU); ctx.fillStyle = INK; ctx.fill(); ctx.beginPath(); ctx.arc(ex + 2, hy - 6, 1.8, 0, TAU); ctx.fillStyle = '#fff'; ctx.fill(); }
|
| 253 |
+
}
|
| 254 |
+
if (mood === 'angry') { ctx.beginPath(); ctx.moveTo(-26, hy - 22); ctx.lineTo(-8, hy - 14); ctx.moveTo(26, hy - 22); ctx.lineTo(8, hy - 14); D.stroke(ctx, 4); }
|
| 255 |
+
if (mood === 'sad') { ctx.beginPath(); ctx.moveTo(-24, hy - 16); ctx.lineTo(-8, hy - 22); ctx.moveTo(24, hy - 16); ctx.lineTo(8, hy - 22); D.stroke(ctx, 4); }
|
| 256 |
+
}
|
| 257 |
+
if (o.glasses === 'round') { ctx.beginPath(); ctx.arc(-15, hy - 4, 12, 0, TAU); ctx.moveTo(27, hy - 4); ctx.arc(15, hy - 4, 12, 0, TAU); ctx.moveTo(-3, hy - 4); ctx.lineTo(3, hy - 4); D.stroke(ctx, 3); }
|
| 258 |
+
// 鼻子腮红
|
| 259 |
+
ctx.beginPath(); ctx.arc(0, hy + 8, 4, 0.3, Math.PI - 0.3); D.stroke(ctx, 3);
|
| 260 |
+
ctx.beginPath(); ctx.ellipse(-27, hy + 12, 7, 4, 0, 0, TAU); ctx.ellipse(27, hy + 12, 7, 4, 0, 0, TAU); ctx.fillStyle = 'rgba(255,120,120,.45)'; ctx.fill();
|
| 261 |
+
// 嘴
|
| 262 |
+
ctx.beginPath();
|
| 263 |
+
if (mood === 'shock') { D.blob(ctx, 0, hy + 26, 9, 11, sd + 20); D.fs(ctx, '#7a2b22', 3.5); }
|
| 264 |
+
else if (mood === 'sad' || mood === 'angry') { ctx.arc(0, hy + 34, 11, Math.PI + .35, -.35); D.stroke(ctx, 4); }
|
| 265 |
+
else if (mood === 'sly') { ctx.moveTo(-14, hy + 22); ctx.quadraticCurveTo(4, hy + 32, 18, hy + 16); D.stroke(ctx, 4); }
|
| 266 |
+
else if (mood === 'talk') { const k = Math.abs(Math.sin(t * 14)); D.blob(ctx, 0, hy + 24, 11, 3 + k * 8, sd + 21); D.fs(ctx, '#7a2b22', 3.5); }
|
| 267 |
+
else if (mood === 'laugh' || mood === 'money') { ctx.moveTo(-14, hy + 18); ctx.quadraticCurveTo(0, hy + 40, 14, hy + 18); ctx.closePath(); D.fs(ctx, '#7a2b22', 3.5); }
|
| 268 |
+
else { ctx.arc(0, hy + 16, 12, 0.2, Math.PI - 0.2); D.stroke(ctx, 4); }
|
| 269 |
+
if (o.mustache) { ctx.beginPath(); ctx.moveTo(0, hy + 13); ctx.quadraticCurveTo(-14, hy + 6, -26, hy + 16); ctx.quadraticCurveTo(-34, hy + 8, -30, hy + 2); ctx.moveTo(0, hy + 13); ctx.quadraticCurveTo(14, hy + 6, 26, hy + 16); ctx.quadraticCurveTo(34, hy + 8, 30, hy + 2); D.stroke(ctx, 6, '#3b2b22'); }
|
| 270 |
+
// 帽子
|
| 271 |
+
if (o.hat === 'straw') {
|
| 272 |
+
D.blob(ctx, 0, hy - 30, 66, 13, sd + 30); D.fs(ctx, '#f0c96a', 4);
|
| 273 |
+
D.blob(ctx, 0, hy - 46, 32, 20, sd + 31); D.fs(ctx, '#f0c96a', 4);
|
| 274 |
+
ctx.beginPath(); ctx.moveTo(-30, hy - 36); ctx.quadraticCurveTo(0, hy - 30, 30, hy - 36); D.stroke(ctx, 6, C.red);
|
| 275 |
+
} else if (o.hat === 'top') {
|
| 276 |
+
D.blob(ctx, 0, hy - 36, 58, 10, sd + 32); D.fs(ctx, '#222', 4);
|
| 277 |
+
D.rrect(ctx, -32, hy - 108, 64, 72, 6, sd + 33); D.fs(ctx, '#222', 4);
|
| 278 |
+
ctx.fillStyle = C.red; ctx.fillRect(-32, hy - 54, 64, 11);
|
| 279 |
+
} else if (o.hat === 'cap') {
|
| 280 |
+
ctx.beginPath(); ctx.moveTo(-42, hy - 14); ctx.quadraticCurveTo(-40, hy - 56, 0, hy - 54); ctx.quadraticCurveTo(40, hy - 56, 42, hy - 14); ctx.closePath(); D.fs(ctx, o.capColor || C.red, 4);
|
| 281 |
+
D.blob(ctx, 34, hy - 16, 30, 8, sd + 34); D.fs(ctx, o.capColor || C.red, 4);
|
| 282 |
+
}
|
| 283 |
+
};
|
| 284 |
+
// 先画背后手,再头,再前手
|
| 285 |
+
arm(-1, o.aL != null ? o.aL : 0.25, o.holdL);
|
| 286 |
+
drawHead();
|
| 287 |
+
arm(1, o.aR != null ? o.aR : 0.25, o.holdR);
|
| 288 |
+
ctx.restore();
|
| 289 |
+
};
|
| 290 |
+
|
| 291 |
+
// 预设角色
|
| 292 |
+
D.merchant = (ctx, x, y, s, o = {}) => D.person(ctx, x, y, s, Object.assign({ shirt: '#2f4a7a', pants: '#25365a', hat: 'top', mustache: true, tie: C.red, belly: true, suit: true, hair: 'bald', seed: 11 }, o));
|
| 293 |
+
D.assistant = (ctx, x, y, s, o = {}) => D.person(ctx, x, y, s, Object.assign({ shirt: '#6b6f7a', pants: '#3e4048', hair: 'slick', hairColor: '#1f1f1f', glasses: 'sun', tie: C.green, thin: true, suit: true, mood: 'sly', seed: 21 }, o));
|
| 294 |
+
const VILLAGERS = [
|
| 295 |
+
{ shirt: '#e8743b', pants: '#5b4a3a', hat: 'straw', hair: 'short', seed: 31 },
|
| 296 |
+
{ shirt: '#5aa9e6', pants: '#3d5a80', hair: 'bun', hairColor: '#2a1d16', seed: 32 },
|
| 297 |
+
{ shirt: '#7bc96f', pants: '#6b4f3a', hat: 'cap', capColor: '#ffcd3c', hair: 'short', seed: 33 },
|
| 298 |
+
{ shirt: '#c77dff', pants: '#40334f', hair: 'spiky', hairColor: '#4a2f22', seed: 34, glasses: 'round' },
|
| 299 |
+
{ shirt: '#ff6b81', pants: '#4b3b47', hair: 'bun', hairColor: '#5a3a2a', seed: 35 },
|
| 300 |
+
{ shirt: '#f4d35e', pants: '#5c5346', hat: 'straw', hair: 'short', seed: 36, skin: '#f1c6a0' },
|
| 301 |
+
{ shirt: '#4ecdc4', pants: '#2f4858', hair: 'short', hairColor: '#222', seed: 37, mustache: true },
|
| 302 |
+
];
|
| 303 |
+
D.villager = (ctx, i, x, y, s, o = {}) => D.person(ctx, x, y, s, Object.assign({}, VILLAGERS[((i % VILLAGERS.length) + VILLAGERS.length) % VILLAGERS.length], { seed: 31 + i * 3 }, o));
|
| 304 |
+
|
| 305 |
+
/* =======================================================
|
| 306 |
+
* 道具
|
| 307 |
+
* ======================================================= */
|
| 308 |
+
D.coin = function (ctx, x, y, r = 22, spin = 0) {
|
| 309 |
+
const sx = Math.max(0.12, Math.abs(Math.cos(spin)));
|
| 310 |
+
ctx.save(); ctx.translate(x, y); ctx.scale(sx, 1);
|
| 311 |
+
D.blob(ctx, 0, 0, r, r, 77, 0.02, 12); D.fs(ctx, C.gold, 4);
|
| 312 |
+
ctx.beginPath(); ctx.arc(0, 0, r * 0.7, 0, TAU); D.stroke(ctx, 2.5, C.gold2);
|
| 313 |
+
if (sx > .4) D.text(ctx, '¥', 0, 1, r * 1.05, '#b9770e');
|
| 314 |
+
ctx.restore();
|
| 315 |
+
};
|
| 316 |
+
D.bill = function (ctx, x, y, s = 1, rot = 0, val = '10') {
|
| 317 |
+
D.at(ctx, x, y, s, () => {
|
| 318 |
+
D.rrect(ctx, -60, -30, 120, 60, 6, 88); D.fs(ctx, '#9fdc8c', 4);
|
| 319 |
+
ctx.beginPath(); ctx.arc(0, 0, 18, 0, TAU); D.stroke(ctx, 3, '#3c8c3a');
|
| 320 |
+
D.text(ctx, '¥' + val, 0, 1, 22, '#2f6f2c');
|
| 321 |
+
}, rot);
|
| 322 |
+
};
|
| 323 |
+
D.tree = function (ctx, x, y, s = 1, seed = 1, col = C.forest) {
|
| 324 |
+
D.at(ctx, x, y, s, () => {
|
| 325 |
+
D.rrect(ctx, -12, -70, 24, 72, 6, seed); D.fs(ctx, '#8a5a35', 4);
|
| 326 |
+
D.blob(ctx, 0, -120, 66, 58, seed + 2, .07); D.fs(ctx, col, 5);
|
| 327 |
+
D.blob(ctx, -30, -95, 40, 32, seed + 3, .07); D.fs(ctx, col, 5);
|
| 328 |
+
D.blob(ctx, 32, -98, 40, 32, seed + 4, .07); D.fs(ctx, col, 5);
|
| 329 |
+
ctx.beginPath(); ctx.arc(-18, -140, 12, Math.PI * 1.1, Math.PI * 1.7); D.stroke(ctx, 4, 'rgba(255,255,255,.35)');
|
| 330 |
+
});
|
| 331 |
+
};
|
| 332 |
+
D.pine = function (ctx, x, y, s = 1, seed = 1, col = C.forest2) {
|
| 333 |
+
D.at(ctx, x, y, s, () => {
|
| 334 |
+
ctx.fillStyle = '#7a4f2e'; ctx.fillRect(-7, -20, 14, 22);
|
| 335 |
+
D.poly(ctx, [[0, -170], [44, -90], [-44, -90]], seed); D.fs(ctx, col, 4);
|
| 336 |
+
D.poly(ctx, [[0, -130], [56, -40], [-56, -40]], seed + 9); D.fs(ctx, col, 4);
|
| 337 |
+
D.poly(ctx, [[0, -90], [64, -14], [-64, -14]], seed + 19); D.fs(ctx, col, 4);
|
| 338 |
+
});
|
| 339 |
+
};
|
| 340 |
+
D.house = function (ctx, x, y, s = 1, seed = 1, wall = '#f6e3c3', roof = '#d9534f', smokeT = null) {
|
| 341 |
+
D.at(ctx, x, y, s, () => {
|
| 342 |
+
if (smokeT != null) for (let i = 0; i < 4; i++) { const k = ((smokeT * 0.35 + i / 4) % 1); D.blob(ctx, 46 + Math.sin(k * 6 + i) * 10, -190 - k * 120, 12 + k * 18, 10 + k * 14, seed + i); ctx.globalAlpha = 1 - k; D.fs(ctx, '#ffffff', 3, 'rgba(43,35,33,.5)'); ctx.globalAlpha = 1; }
|
| 343 |
+
D.rrect(ctx, 30, -190, 26, 50, 3, seed + 5); D.fs(ctx, '#a0522d', 4);
|
| 344 |
+
D.rrect(ctx, -80, -100, 160, 100, 6, seed); D.fs(ctx, wall, 5);
|
| 345 |
+
D.poly(ctx, [[-100, -96], [0, -176], [100, -96]], seed + 1); D.fs(ctx, roof, 5);
|
| 346 |
+
D.rrect(ctx, -18, -60, 36, 60, 4, seed + 2); D.fs(ctx, '#9b6a43', 4);
|
| 347 |
+
D.rrect(ctx, 34, -74, 30, 28, 3, seed + 3); D.fs(ctx, '#bfe7ff', 4);
|
| 348 |
+
D.rrect(ctx, -64, -74, 30, 28, 3, seed + 4); D.fs(ctx, '#bfe7ff', 4);
|
| 349 |
+
});
|
| 350 |
+
};
|
| 351 |
+
D.cloud = function (ctx, x, y, s = 1, seed = 1) {
|
| 352 |
+
D.at(ctx, x, y, s, () => {
|
| 353 |
+
D.blob(ctx, -40, 4, 44, 28, seed); D.fs(ctx, '#fff', 4);
|
| 354 |
+
D.blob(ctx, 34, 6, 40, 26, seed + 1); D.fs(ctx, '#fff', 4);
|
| 355 |
+
D.blob(ctx, 0, -14, 50, 36, seed + 2); D.fs(ctx, '#fff', 4);
|
| 356 |
+
ctx.fillStyle = '#fff'; ctx.fillRect(-60, -2, 110, 22);
|
| 357 |
+
});
|
| 358 |
+
};
|
| 359 |
+
D.sun = function (ctx, x, y, r, t, face = true) {
|
| 360 |
+
ctx.save(); ctx.translate(x, y); ctx.rotate(t * 0.2);
|
| 361 |
+
for (let i = 0; i < 12; i++) { const a = i / 12 * TAU; D.line(ctx, Math.cos(a) * (r + 14), Math.sin(a) * (r + 14), Math.cos(a) * (r + 38), Math.sin(a) * (r + 38), i); D.stroke(ctx, 6, '#ffb52e'); }
|
| 362 |
+
ctx.rotate(-t * 0.2);
|
| 363 |
+
D.blob(ctx, 0, 0, r, r, 5); D.fs(ctx, '#ffd84a', 5);
|
| 364 |
+
if (face) { ctx.beginPath(); ctx.arc(-r * .3, -r * .1, 4, 0, TAU); ctx.arc(r * .3, -r * .1, 4, 0, TAU); ctx.fillStyle = INK; ctx.fill(); ctx.beginPath(); ctx.arc(0, r * .1, r * .3, .2, Math.PI - .2); D.stroke(ctx, 4); }
|
| 365 |
+
ctx.restore();
|
| 366 |
+
};
|
| 367 |
+
// 笼子:count 只猴子(显示上限 cap 个头像)
|
| 368 |
+
D.cage = function (ctx, x, y, w, h, fill, t, o = {}) {
|
| 369 |
+
ctx.save(); ctx.translate(x, y);
|
| 370 |
+
D.rrect(ctx, -w / 2, -h, w, h, 10, 51); D.fs(ctx, 'rgba(255,240,210,.55)', 0);
|
| 371 |
+
// 猴子按网格排布,fill 0..1
|
| 372 |
+
const cols = o.cols || 5, rows = o.rows || 3;
|
| 373 |
+
const n = Math.round(cols * rows * D.clamp01(fill));
|
| 374 |
+
const cw = w / cols, rh = (h - 20) / rows;
|
| 375 |
+
for (let i = 0; i < n; i++) {
|
| 376 |
+
const r = rows - 1 - Math.floor(i / cols), c = i % cols;
|
| 377 |
+
const mx = -w / 2 + cw * (c + .5) + (r % 2 ? 8 : -8), my = -h + 20 + rh * (r + 1) - 4;
|
| 378 |
+
D.monkey(ctx, mx, my + Math.sin(t * 5 + i) * 2, Math.min(cw, rh) / 150, { t, seed: i + 1, mood: o.mood || (i % 3 ? 'happy' : 'shock'), aL: 2.6 + Math.sin(t * 4 + i) * .3, aR: 0.4 });
|
| 379 |
+
}
|
| 380 |
+
// 栏杆
|
| 381 |
+
for (let i = 0; i <= 10; i++) { const bx = -w / 2 + w * i / 10; D.line(ctx, bx, -h, bx, 0, 60 + i, 1.5); D.stroke(ctx, 6, '#5b5b66'); }
|
| 382 |
+
D.rrect(ctx, -w / 2 - 10, -h - 16, w + 20, 22, 6, 52); D.fs(ctx, '#7b6f66', 5);
|
| 383 |
+
D.rrect(ctx, -w / 2 - 10, -8, w + 20, 18, 6, 53); D.fs(ctx, '#7b6f66', 5);
|
| 384 |
+
if (o.label) { D.rrect(ctx, -70, -h - 64, 140, 44, 8, 54); D.fs(ctx, '#fff4d6', 4); D.text(ctx, o.label, 0, -h - 42, 26, INK); }
|
| 385 |
+
ctx.restore();
|
| 386 |
+
};
|
| 387 |
+
// 价格牌(木牌) flip 0..1 用于翻牌动画
|
| 388 |
+
D.priceSign = function (ctx, x, y, s, price, o = {}) {
|
| 389 |
+
D.at(ctx, x, y, s, () => {
|
| 390 |
+
if (!o.noPost) { D.rrect(ctx, -10, -40, 20, 200, 4, 71); D.fs(ctx, '#8a5a35', 4); }
|
| 391 |
+
ctx.save(); ctx.translate(0, -110); ctx.rotate(Math.sin((o.t || 0) * 2) * 0.03);
|
| 392 |
+
ctx.scale(1, o.flip != null ? Math.max(0.02, Math.abs(Math.cos(o.flip * Math.PI))) : 1);
|
| 393 |
+
D.rrect(ctx, -150, -95, 300, 190, 16, 72); D.fs(ctx, o.bg || '#ffe9a8', 6);
|
| 394 |
+
D.text(ctx, o.head || '高价收猴', 0, -52, 40, '#7a4b1c');
|
| 395 |
+
D.bigText(ctx, '¥' + price, -14, 18, 96, o.color || C.red);
|
| 396 |
+
D.text(ctx, '/只', 108, 42, 30, '#7a4b1c');
|
| 397 |
+
ctx.restore();
|
| 398 |
+
});
|
| 399 |
+
};
|
| 400 |
+
D.bubble = function (ctx, x, y, w, h, tx, ty, o = {}) { // 对话气泡,tail 指向 (tx,ty)
|
| 401 |
+
ctx.save();
|
| 402 |
+
const fill = o.fill || '#fff';
|
| 403 |
+
ctx.beginPath();
|
| 404 |
+
const bx = x - w / 2, by = y - h / 2;
|
| 405 |
+
D.rrect(ctx, bx, by, w, h, Math.min(30, h / 2), o.seed || 91, 1.2);
|
| 406 |
+
D.fs(ctx, fill, 5);
|
| 407 |
+
ctx.beginPath(); const mx = Math.max(bx + 30, Math.min(bx + w - 30, tx));
|
| 408 |
+
ctx.moveTo(mx - 16, y + (ty > y ? h / 2 - 3 : -h / 2 + 3)); ctx.lineTo(tx, ty); ctx.lineTo(mx + 16, y + (ty > y ? h / 2 - 3 : -h / 2 + 3));
|
| 409 |
+
D.fs(ctx, fill, 0); ctx.beginPath(); ctx.moveTo(mx - 16, y + (ty > y ? h / 2 : -h / 2)); ctx.lineTo(tx, ty); ctx.lineTo(mx + 16, y + (ty > y ? h / 2 : -h / 2)); D.stroke(ctx, 5);
|
| 410 |
+
if (o.text) {
|
| 411 |
+
const lines = Array.isArray(o.text) ? o.text : [o.text];
|
| 412 |
+
const sz = o.size || 40; lines.forEach((ln, i) => D.text(ctx, ln, x, y + (i - (lines.length - 1) / 2) * sz * 1.25, sz, o.color || INK));
|
| 413 |
+
}
|
| 414 |
+
ctx.restore();
|
| 415 |
+
};
|
| 416 |
+
D.thought = function (ctx, x, y, w, h, tx, ty, o = {}) {
|
| 417 |
+
D.blob(ctx, x, y, w / 2, h / 2, 93, .06, 22); D.fs(ctx, '#fff', 5);
|
| 418 |
+
D.blob(ctx, D.lerp(x, tx, .62), D.lerp(y, ty, .62), 14, 12, 94); D.fs(ctx, '#fff', 4);
|
| 419 |
+
D.blob(ctx, D.lerp(x, tx, .85), D.lerp(y, ty, .85), 8, 7, 95); D.fs(ctx, '#fff', 4);
|
| 420 |
+
if (o.text) { const lines = Array.isArray(o.text) ? o.text : [o.text]; const sz = o.size || 34; lines.forEach((ln, i) => D.text(ctx, ln, x, y + (i - (lines.length - 1) / 2) * sz * 1.25, sz, o.color || INK)); }
|
| 421 |
+
};
|
| 422 |
+
D.megaphone = function (ctx) {
|
| 423 |
+
ctx.save(); ctx.rotate(-0.35);
|
| 424 |
+
D.poly(ctx, [[0, -10], [70, -34], [70, 34], [0, 10]], 101); D.fs(ctx, C.red, 4);
|
| 425 |
+
D.blob(ctx, 72, 0, 9, 34, 102); D.fs(ctx, '#ffe9a8', 4);
|
| 426 |
+
ctx.restore();
|
| 427 |
+
};
|
| 428 |
+
D.net = function (ctx) {
|
| 429 |
+
ctx.save(); ctx.rotate(-0.5);
|
| 430 |
+
D.line(ctx, 0, 30, 0, -110, 111); D.stroke(ctx, 7, '#8a5a35');
|
| 431 |
+
D.blob(ctx, 0, -140, 34, 30, 112); D.fs(ctx, 'rgba(255,255,255,.4)', 4);
|
| 432 |
+
for (let i = -2; i <= 2; i++) { ctx.beginPath(); ctx.moveTo(i * 13, -168); ctx.lineTo(i * 13, -112); ctx.moveTo(-32, -140 + i * 11); ctx.lineTo(32, -140 + i * 11); D.stroke(ctx, 1.5, 'rgba(43,35,33,.6)'); }
|
| 433 |
+
ctx.restore();
|
| 434 |
+
};
|
| 435 |
+
D.car = function (ctx, x, y, s, t, o = {}) {
|
| 436 |
+
D.at(ctx, x, y, s, () => {
|
| 437 |
+
D.rrect(ctx, -130, -90, 260, 64, 20, 121); D.fs(ctx, o.color || C.red, 5);
|
| 438 |
+
D.rrect(ctx, -70, -142, 140, 60, 20, 122); D.fs(ctx, o.color || C.red, 5);
|
| 439 |
+
D.rrect(ctx, -56, -130, 52, 40, 8, 123); D.fs(ctx, '#bfe7ff', 4);
|
| 440 |
+
D.rrect(ctx, 6, -130, 52, 40, 8, 124); D.fs(ctx, '#bfe7ff', 4);
|
| 441 |
+
for (const wx of [-78, 78]) { ctx.save(); ctx.translate(wx, -24); ctx.rotate(t * 12); D.blob(ctx, 0, 0, 28, 28, 125); D.fs(ctx, '#333', 5); ctx.beginPath(); ctx.moveTo(-12, 0); ctx.lineTo(12, 0); ctx.moveTo(0, -12); ctx.lineTo(0, 12); D.stroke(ctx, 4, '#bbb'); ctx.restore(); }
|
| 442 |
+
if (o.driver) o.driver();
|
| 443 |
+
});
|
| 444 |
+
};
|
| 445 |
+
D.cow = function (ctx, x, y, s, t, o = {}) {
|
| 446 |
+
D.at(ctx, x, y, s, () => {
|
| 447 |
+
for (const lx of [-50, -20, 30, 58]) { D.rrect(ctx, lx - 9, -60, 18, 60, 5, 130 + lx); D.fs(ctx, '#fff', 4); }
|
| 448 |
+
D.blob(ctx, 0, -86, 96, 50, 131); D.fs(ctx, '#fff', 5);
|
| 449 |
+
D.blob(ctx, -30, -96, 26, 20, 132); D.fs(ctx, '#333', 0); D.blob(ctx, 34, -70, 20, 16, 133); D.fs(ctx, '#333', 0);
|
| 450 |
+
D.blob(ctx, 100, -120, 42, 36, 134); D.fs(ctx, '#fff', 5);
|
| 451 |
+
D.blob(ctx, 116, -104, 26, 18, 135); D.fs(ctx, '#ffb3c1', 4);
|
| 452 |
+
ctx.beginPath(); ctx.arc(92, -128, 5, 0, TAU); ctx.fillStyle = INK; ctx.fill();
|
| 453 |
+
D.poly(ctx, [[80, -150], [70, -176], [90, -156]], 136); D.fs(ctx, '#f6e3c3', 3);
|
| 454 |
+
D.poly(ctx, [[112, -152], [124, -178], [124, -150]], 137); D.fs(ctx, '#f6e3c3', 3);
|
| 455 |
+
if (o.tag) { D.rrect(ctx, -60, -190, 120, 46, 8, 138); D.fs(ctx, '#fff4d6', 4); D.text(ctx, o.tag, 0, -167, 28, C.red); }
|
| 456 |
+
});
|
| 457 |
+
};
|
| 458 |
+
D.stamp = function (ctx, x, y, s, text, color = C.red, rot = -0.2) {
|
| 459 |
+
D.at(ctx, x, y, s, () => {
|
| 460 |
+
ctx.globalAlpha *= 0.92;
|
| 461 |
+
ctx.font = D.font(64); const w = ctx.measureText(text).width + 60;
|
| 462 |
+
D.rrect(ctx, -w / 2, -50, w, 100, 14, 141, 2.5); D.stroke(ctx, 9, color);
|
| 463 |
+
D.text(ctx, text, 0, 3, 64, color);
|
| 464 |
+
}, rot);
|
| 465 |
+
};
|
| 466 |
+
D.cross = function (ctx, x, y, r, color = C.red) {
|
| 467 |
+
D.line(ctx, x - r, y - r, x + r, y + r, 151, 3); D.stroke(ctx, 12, color);
|
| 468 |
+
D.line(ctx, x + r, y - r, x - r, y + r, 152, 3); D.stroke(ctx, 12, color);
|
| 469 |
+
};
|
| 470 |
+
D.check = function (ctx, x, y, r, k = 1, color = C.green) {
|
| 471 |
+
ctx.beginPath(); ctx.moveTo(x - r, y);
|
| 472 |
+
const p1 = [x - r * .3, y + r * .7], p2 = [x + r, y - r * .8];
|
| 473 |
+
if (k < .4) { const q = k / .4; ctx.lineTo(D.lerp(x - r, p1[0], q), D.lerp(y, p1[1], q)); }
|
| 474 |
+
else { ctx.lineTo(p1[0], p1[1]); const q = (k - .4) / .6; ctx.lineTo(D.lerp(p1[0], p2[0], q), D.lerp(p1[1], p2[1], q)); }
|
| 475 |
+
D.stroke(ctx, 12, color);
|
| 476 |
+
};
|
| 477 |
+
D.arrow = function (ctx, x1, y1, x2, y2, color = INK, w = 8, k = 1) {
|
| 478 |
+
const ex = D.lerp(x1, x2, k), ey = D.lerp(y1, y2, k);
|
| 479 |
+
D.line(ctx, x1, y1, ex, ey, 161, 2); D.stroke(ctx, w, color);
|
| 480 |
+
const a = Math.atan2(ey - y1, ex - x1), h = w * 3;
|
| 481 |
+
ctx.beginPath(); ctx.moveTo(ex + Math.cos(a) * 4, ey + Math.sin(a) * 4);
|
| 482 |
+
ctx.lineTo(ex - Math.cos(a - .5) * h, ey - Math.sin(a - .5) * h);
|
| 483 |
+
ctx.lineTo(ex - Math.cos(a + .5) * h, ey - Math.sin(a + .5) * h); ctx.closePath();
|
| 484 |
+
ctx.fillStyle = color; ctx.fill(); D.stroke(ctx, 3, color);
|
| 485 |
+
};
|
| 486 |
+
D.qmark = function (ctx, x, y, s, t = 0, color = C.blue) {
|
| 487 |
+
D.at(ctx, x, y, s, () => D.bigText(ctx, '?', 0, 0, 120, color), Math.sin(t * 3) * .12);
|
| 488 |
+
};
|
| 489 |
+
D.exclaim = function (ctx, x, y, s, t = 0, color = C.red) {
|
| 490 |
+
D.at(ctx, x, y, s, () => D.bigText(ctx, '!', 0, 0, 120, color), Math.sin(t * 5) * .1);
|
| 491 |
+
};
|
| 492 |
+
D.sparkle = function (ctx, x, y, r, t, color = '#fff6a8') {
|
| 493 |
+
const k = 0.6 + 0.4 * Math.sin(t * 8);
|
| 494 |
+
ctx.save(); ctx.translate(x, y); ctx.scale(k, k); ctx.rotate(t);
|
| 495 |
+
ctx.beginPath(); for (let i = 0; i < 8; i++) { const a = i / 8 * TAU, rr = i % 2 ? r * .35 : r; ctx.lineTo(Math.cos(a) * rr, Math.sin(a) * rr); } ctx.closePath();
|
| 496 |
+
D.fs(ctx, color, 3); ctx.restore();
|
| 497 |
+
};
|
| 498 |
+
D.piggy = function (ctx, x, y, s, broken = 0) {
|
| 499 |
+
D.at(ctx, x, y, s, () => {
|
| 500 |
+
if (broken > 0) {
|
| 501 |
+
for (let i = 0; i < 5; i++) { const a = i / 5 * TAU + .3, d = broken * 80; D.at(ctx, Math.cos(a) * d, Math.sin(a) * d * .6 - 40 + broken * broken * 60, 1, () => { D.poly(ctx, [[-30, -20], [30, -26], [20, 24], [-26, 18]], 170 + i); D.fs(ctx, C.pink, 4); }, a + broken * 3); }
|
| 502 |
+
return;
|
| 503 |
+
}
|
| 504 |
+
D.blob(ctx, 0, -60, 80, 60, 171); D.fs(ctx, C.pink, 5);
|
| 505 |
+
D.blob(ctx, 78, -66, 20, 24, 172); D.fs(ctx, '#ff6f9a', 4);
|
| 506 |
+
ctx.beginPath(); ctx.arc(30, -84, 5, 0, TAU); ctx.fillStyle = INK; ctx.fill();
|
| 507 |
+
D.poly(ctx, [[10, -116], [26, -142], [36, -112]], 173); D.fs(ctx, C.pink, 4);
|
| 508 |
+
ctx.fillStyle = INK; ctx.fillRect(-20, -122, 34, 7);
|
| 509 |
+
for (const lx of [-46, 40]) { D.rrect(ctx, lx - 10, -14, 20, 18, 4, 174 + lx); D.fs(ctx, C.pink, 4); }
|
| 510 |
+
});
|
| 511 |
+
};
|
| 512 |
+
D.calendar = function (ctx, x, y, s, label, sub, flip = 0) {
|
| 513 |
+
D.at(ctx, x, y, s, () => {
|
| 514 |
+
D.rrect(ctx, -110, -130, 220, 250, 14, 181); D.fs(ctx, '#fff', 6);
|
| 515 |
+
D.rrect(ctx, -110, -130, 220, 64, 14, 182); D.fs(ctx, C.red, 6);
|
| 516 |
+
for (const rx of [-60, 60]) { D.rrect(ctx, rx - 7, -150, 14, 40, 6, 183 + rx); D.fs(ctx, '#888', 4); }
|
| 517 |
+
D.text(ctx, sub || '', 0, -98, 30, '#fff');
|
| 518 |
+
ctx.save(); ctx.scale(1, Math.max(0.05, 1 - flip)); D.bigText(ctx, label, 0, 20, 84, INK, { shadow: null }); ctx.restore();
|
| 519 |
+
});
|
| 520 |
+
};
|
| 521 |
+
D.tulip = function (ctx, x, y, s, t = 0) {
|
| 522 |
+
D.at(ctx, x, y, s, () => {
|
| 523 |
+
D.line(ctx, 0, 0, Math.sin(t) * 6, -140, 191); D.stroke(ctx, 8, C.green);
|
| 524 |
+
D.blob(ctx, -26, -50, 26, 10, 192); D.fs(ctx, '#58c06a', 4);
|
| 525 |
+
ctx.save(); ctx.translate(Math.sin(t) * 6, -150);
|
| 526 |
+
D.poly(ctx, [[-40, 0], [-44, -60], [-20, -34], [0, -70], [20, -34], [44, -60], [40, 0], [0, 26]], 193); D.fs(ctx, '#ff4f6d', 5);
|
| 527 |
+
ctx.restore();
|
| 528 |
+
});
|
| 529 |
+
};
|
| 530 |
+
D.laptop = function (ctx, x, y, s, t = 0) {
|
| 531 |
+
D.at(ctx, x, y, s, () => {
|
| 532 |
+
D.rrect(ctx, -110, -150, 220, 140, 12, 201); D.fs(ctx, '#3b3f4a', 5);
|
| 533 |
+
D.rrect(ctx, -96, -138, 192, 116, 6, 202); D.fs(ctx, '#e8f6ff', 3);
|
| 534 |
+
D.text(ctx, '.com', 0, -80, 52, C.blue);
|
| 535 |
+
D.poly(ctx, [[-140, -10], [140, -10], [120, 14], [-120, 14]], 203); D.fs(ctx, '#9aa0aa', 5);
|
| 536 |
+
});
|
| 537 |
+
};
|
| 538 |
+
D.sneaker = function (ctx, x, y, s) {
|
| 539 |
+
D.at(ctx, x, y, s, () => {
|
| 540 |
+
ctx.beginPath(); ctx.moveTo(-90, 0); ctx.lineTo(-90, -60); ctx.quadraticCurveTo(-40, -86, -20, -60); ctx.quadraticCurveTo(30, -40, 90, -26); ctx.quadraticCurveTo(100, 0, 90, 0); ctx.closePath(); D.fs(ctx, '#fff', 5);
|
| 541 |
+
D.rrect(ctx, -96, -10, 196, 22, 8, 211); D.fs(ctx, C.red, 5);
|
| 542 |
+
ctx.beginPath(); ctx.moveTo(-60, -40); ctx.quadraticCurveTo(0, -10, 60, -24); D.stroke(ctx, 7, C.blue);
|
| 543 |
+
});
|
| 544 |
+
};
|
| 545 |
+
D.card = function (ctx, x, y, s) {
|
| 546 |
+
D.at(ctx, x, y, s, () => {
|
| 547 |
+
D.rrect(ctx, -60, -84, 120, 168, 12, 221); D.fs(ctx, '#ffe17a', 5);
|
| 548 |
+
D.rrect(ctx, -46, -70, 92, 92, 8, 222); D.fs(ctx, '#bfe7ff', 4);
|
| 549 |
+
D.text(ctx, 'SSR', 0, 50, 34, C.purple);
|
| 550 |
+
D.sparkle(ctx, 30, -60, 16, 1);
|
| 551 |
+
});
|
| 552 |
+
};
|
| 553 |
+
D.cryptoCoin = function (ctx, x, y, s, t = 0) {
|
| 554 |
+
D.at(ctx, x, y, s, () => {
|
| 555 |
+
D.blob(ctx, 0, 0, 70, 70, 231, .02); D.fs(ctx, '#ffb547', 6);
|
| 556 |
+
ctx.beginPath(); ctx.arc(0, 0, 50, 0, TAU); D.stroke(ctx, 4, '#e08a12');
|
| 557 |
+
D.text(ctx, '空气', 0, 2, 36, '#8a4b00');
|
| 558 |
+
}, Math.sin(t * 2) * .1);
|
| 559 |
+
};
|
| 560 |
+
D.cricket = function (ctx, x, y, s, t) {
|
| 561 |
+
D.at(ctx, x, y, s, () => {
|
| 562 |
+
D.blob(ctx, 0, -20, 34, 16, 241); D.fs(ctx, '#5a7d3a', 4);
|
| 563 |
+
D.blob(ctx, 34, -26, 14, 12, 242); D.fs(ctx, '#5a7d3a', 4);
|
| 564 |
+
ctx.beginPath(); ctx.moveTo(42, -36); ctx.quadraticCurveTo(60, -80 + Math.sin(t * 20) * 6, 80, -70); ctx.moveTo(38, -36); ctx.quadraticCurveTo(46, -86, 64, -88 + Math.sin(t * 22) * 6); D.stroke(ctx, 3);
|
| 565 |
+
ctx.beginPath(); ctx.moveTo(-10, -10); ctx.lineTo(-30, 10); ctx.moveTo(10, -10); ctx.lineTo(24, 10); D.stroke(ctx, 4);
|
| 566 |
+
});
|
| 567 |
+
};
|
| 568 |
+
D.tumbleweed = function (ctx, x, y, s, t) {
|
| 569 |
+
D.at(ctx, x, y, s, () => {
|
| 570 |
+
for (let i = 0; i < 6; i++) { ctx.beginPath(); ctx.ellipse(0, 0, 40, 40 - i * 6, i, 0, TAU); D.stroke(ctx, 3, '#a07a4a'); }
|
| 571 |
+
}, t * 4);
|
| 572 |
+
};
|
| 573 |
+
D.ledgerBook = function (ctx, x, y, w, h) {
|
| 574 |
+
D.rrect(ctx, x, y, w, h, 16, 251); D.fs(ctx, '#fffaf0', 6);
|
| 575 |
+
ctx.save(); ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip();
|
| 576 |
+
for (let yy = y + 70; yy < y + h; yy += 56) { ctx.beginPath(); ctx.moveTo(x + 10, yy); ctx.lineTo(x + w - 10, yy); D.stroke(ctx, 2, 'rgba(74,143,226,.35)'); }
|
| 577 |
+
ctx.beginPath(); ctx.moveTo(x + 70, y); ctx.lineTo(x + 70, y + h); D.stroke(ctx, 2.5, 'rgba(255,91,79,.5)');
|
| 578 |
+
ctx.restore();
|
| 579 |
+
};
|
| 580 |
+
D.heart = function (ctx, x, y, r, color = C.red) {
|
| 581 |
+
ctx.beginPath(); ctx.moveTo(x, y + r * .9);
|
| 582 |
+
ctx.bezierCurveTo(x - r * 1.6, y - r * .2, x - r * .7, y - r * 1.4, x, y - r * .5);
|
| 583 |
+
ctx.bezierCurveTo(x + r * .7, y - r * 1.4, x + r * 1.6, y - r * .2, x, y + r * .9);
|
| 584 |
+
D.fs(ctx, color, 4);
|
| 585 |
+
};
|
| 586 |
+
// 确定性粒子:彩带
|
| 587 |
+
D.confetti = function (ctx, x, y, dt, n = 40, spread = 600, seed = 1) {
|
| 588 |
+
if (dt < 0 || dt > 4) return;
|
| 589 |
+
const cols = [C.red, C.gold, C.blue, C.green, C.pink, C.purple];
|
| 590 |
+
for (let i = 0; i < n; i++) {
|
| 591 |
+
const h1 = hash(seed + i), h2 = hash(seed + i * 7.3), h3 = hash(seed + i * 3.7);
|
| 592 |
+
const vx = (h1 - .5) * spread * 1.6, vy = -300 - h2 * 600;
|
| 593 |
+
const px = x + vx * dt, py = y + vy * dt + 700 * dt * dt;
|
| 594 |
+
ctx.save(); ctx.translate(px, py); ctx.rotate(dt * (4 + h3 * 8)); ctx.globalAlpha *= D.clamp01(1.4 - dt / 3);
|
| 595 |
+
ctx.fillStyle = cols[i % cols.length]; ctx.fillRect(-9, -5, 18, 10 * Math.abs(Math.cos(dt * 6 + i)) + 2);
|
| 596 |
+
ctx.restore();
|
| 597 |
+
}
|
| 598 |
+
};
|
| 599 |
+
D.coinBurst = function (ctx, x, y, dt, n = 14, seed = 3, power = 1) {
|
| 600 |
+
if (dt < 0 || dt > 2.4) return;
|
| 601 |
+
for (let i = 0; i < n; i++) {
|
| 602 |
+
const h1 = hash(seed + i), h2 = hash(seed + i * 5.1);
|
| 603 |
+
const vx = (h1 - .5) * 700 * power, vy = -(400 + h2 * 500) * power;
|
| 604 |
+
const px = x + vx * dt, py = y + vy * dt + 1100 * dt * dt;
|
| 605 |
+
ctx.save(); ctx.globalAlpha *= D.clamp01(2.4 - dt); D.coin(ctx, px, py, 20, dt * 10 + i); ctx.restore();
|
| 606 |
+
}
|
| 607 |
+
};
|
| 608 |
+
D.speedLines = function (ctx, cx, cy, t, color = 'rgba(43,35,33,.12)', n = 36, r0 = 380) {
|
| 609 |
+
ctx.save();
|
| 610 |
+
for (let i = 0; i < n; i++) {
|
| 611 |
+
const a = i / n * TAU + hash(i + Math.floor(t * 10)) * .1;
|
| 612 |
+
const r1 = r0 + hash(i * 3 + Math.floor(t * 10)) * 120;
|
| 613 |
+
ctx.beginPath(); ctx.moveTo(cx + Math.cos(a) * r1, cy + Math.sin(a) * r1); ctx.lineTo(cx + Math.cos(a) * 2400, cy + Math.sin(a) * 2400);
|
| 614 |
+
ctx.lineWidth = 6 + hash(i) * 14; ctx.strokeStyle = color; ctx.stroke();
|
| 615 |
+
}
|
| 616 |
+
ctx.restore();
|
| 617 |
+
};
|
| 618 |
+
D.dust = function (ctx, x, y, t, n = 5, seed = 1) {
|
| 619 |
+
for (let i = 0; i < n; i++) {
|
| 620 |
+
const k = ((t * 1.6 + i / n) % 1);
|
| 621 |
+
ctx.globalAlpha = (1 - k) * .8;
|
| 622 |
+
D.blob(ctx, x - k * 90 - i * 6, y - k * 40 - hash(seed + i) * 20, 10 + k * 24, 8 + k * 18, seed + i);
|
| 623 |
+
D.fs(ctx, '#efe2c8', 3, 'rgba(43,35,33,.5)');
|
| 624 |
+
}
|
| 625 |
+
ctx.globalAlpha = 1;
|
| 626 |
+
};
|
| 627 |
+
|
| 628 |
+
window.D = D;
|
| 629 |
+
})();
|
shouhouzi/js/engine.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =========================================================
|
| 2 |
+
* engine.js —— 时间轴、画幅布局、逐帧渲染(预览与导出共用)
|
| 3 |
+
* ========================================================= */
|
| 4 |
+
(function () {
|
| 5 |
+
const PRE = 0.5, POST = 0.4;
|
| 6 |
+
const STAGE_W = 1280, STAGE_H = 960;
|
| 7 |
+
|
| 8 |
+
/* ---------- 时间轴 ---------- */
|
| 9 |
+
const TL = { scenes: [], total: 0, lines: [] };
|
| 10 |
+
let acc = 0;
|
| 11 |
+
window.SCRIPT.forEach((sc, si) => {
|
| 12 |
+
const s = { id: sc.id, title: sc.title, index: si, start: acc, L: [], lines: sc.lines };
|
| 13 |
+
let c = PRE;
|
| 14 |
+
sc.lines.forEach((ln, li) => {
|
| 15 |
+
s.L.push(c);
|
| 16 |
+
TL.lines.push({ scene: si, idx: li, start: acc + c, end: acc + c + ln.dur, until: acc + c + ln.dur + ln.gap, text: ln.s, v: ln.v, file: ln.file, dur: ln.dur });
|
| 17 |
+
c += ln.dur + ln.gap;
|
| 18 |
+
});
|
| 19 |
+
s.dur = c + POST;
|
| 20 |
+
acc += s.dur;
|
| 21 |
+
TL.scenes.push(s);
|
| 22 |
+
});
|
| 23 |
+
TL.total = acc;
|
| 24 |
+
TL.byId = {}; TL.scenes.forEach((s) => (TL.byId[s.id] = s));
|
| 25 |
+
TL.at = function (t) {
|
| 26 |
+
t = Math.max(0, Math.min(TL.total - 1e-4, t));
|
| 27 |
+
let lo = 0, hi = TL.scenes.length - 1;
|
| 28 |
+
while (lo < hi) { const m = (lo + hi + 1) >> 1; if (TL.scenes[m].start <= t) lo = m; else hi = m - 1; }
|
| 29 |
+
return { sc: TL.scenes[lo], lt: t - TL.scenes[lo].start };
|
| 30 |
+
};
|
| 31 |
+
// 全局时间:场景 id 第 i 句开始后 off 秒;i 为 -1 表示场景开始,'end' 表示场景结束
|
| 32 |
+
TL.cue = function (id, i = 0, off = 0) {
|
| 33 |
+
const s = TL.byId[id];
|
| 34 |
+
if (i === -1) return s.start + off;
|
| 35 |
+
if (i === 'end') return s.start + s.dur + off;
|
| 36 |
+
return s.start + s.L[i] + off;
|
| 37 |
+
};
|
| 38 |
+
TL.lineAt = function (t) {
|
| 39 |
+
for (let i = TL.lines.length - 1; i >= 0; i--) { const l = TL.lines[i]; if (t >= l.start - 0.05) return t <= l.until + 0.05 ? l : null; }
|
| 40 |
+
return null;
|
| 41 |
+
};
|
| 42 |
+
window.TL = TL;
|
| 43 |
+
|
| 44 |
+
/* ---------- 画幅布局 ---------- */
|
| 45 |
+
// landscape:B站 1920×1080;portrait:抖音 1080×1920(顶部刘海/状态栏/顶栏、底部文案与进度条、右侧互动按钮均避开)
|
| 46 |
+
const LAYOUTS = {
|
| 47 |
+
landscape: { name: 'landscape', W: 1920, H: 1080, k: 0.9, sx: (1920 - STAGE_W * 0.9) / 2, sy: 22, sub: { cx: 960, y: 952, size: 46, maxW: 1600 }, safe: null },
|
| 48 |
+
portrait: { name: 'portrait', W: 1080, H: 1920, k: 0.72, sx: 24, sy: 476, sub: { cx: 485, y: 1225, size: 50, maxW: 880 }, safe: { top: 210, bottom: 1480, right: 940 } },
|
| 49 |
+
};
|
| 50 |
+
window.LAYOUTS = LAYOUTS;
|
| 51 |
+
|
| 52 |
+
/* ---------- HUD 关键帧(收购价 / 笼中猴子) ---------- */
|
| 53 |
+
function buildHud() {
|
| 54 |
+
const P = [
|
| 55 |
+
[TL.cue('arrive', 2, 0.6), '10', '收购价'],
|
| 56 |
+
[TL.cue('twenty', 3, 0.3), '20', '收购价'],
|
| 57 |
+
[TL.cue('scarce', 1, 0.6), '25', '收购价'],
|
| 58 |
+
[TL.cue('fifty', 2, 0.8), '50', '收购价'],
|
| 59 |
+
[TL.cue('deal', 2, 1.6), '35', '助手卖价'],
|
| 60 |
+
[TL.cue('gone', 0, 0), '???', '收购价'],
|
| 61 |
+
[TL.cue('gone', 'end', -0.4), null, ''],
|
| 62 |
+
];
|
| 63 |
+
const CAGE = [
|
| 64 |
+
[TL.cue('ten', 3, 0), 0], [TL.cue('ten', 4, 4.5), 600],
|
| 65 |
+
[TL.cue('twenty', 4, 0), 600], [TL.cue('twenty', 4, 4.5), 900],
|
| 66 |
+
[TL.cue('scarce', 3, 0), 900], [TL.cue('scarce', 3, 3.5), 1000],
|
| 67 |
+
[TL.cue('frenzy', 4, 0), 1000], [TL.cue('frenzy', 4, 3.6), 0],
|
| 68 |
+
];
|
| 69 |
+
return { P, CAGE };
|
| 70 |
+
}
|
| 71 |
+
const HUD = buildHud();
|
| 72 |
+
function hudAt(t) {
|
| 73 |
+
let price = null, label = '', since = 99;
|
| 74 |
+
for (const [tt, p, lab] of HUD.P) if (t >= tt) { price = p; label = lab; since = t - tt; }
|
| 75 |
+
let cage = null;
|
| 76 |
+
if (t >= HUD.CAGE[0][0] && t < TL.cue('gone', -1)) {
|
| 77 |
+
cage = 0;
|
| 78 |
+
for (let i = 0; i < HUD.CAGE.length - 1; i++) {
|
| 79 |
+
const [a, va] = HUD.CAGE[i], [b, vb] = HUD.CAGE[i + 1];
|
| 80 |
+
if (t >= a && t < b) { cage = Math.round(D.lerp(va, vb, D.E.inout((t - a) / (b - a)))); break; }
|
| 81 |
+
if (t >= b) cage = vb;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
return { price, label, since, cage };
|
| 85 |
+
}
|
| 86 |
+
window.hudAt = hudAt;
|
| 87 |
+
|
| 88 |
+
/* ---------- 纸张颗粒 ---------- */
|
| 89 |
+
let grain = null;
|
| 90 |
+
function getGrain() {
|
| 91 |
+
if (grain) return grain;
|
| 92 |
+
const c = document.createElement('canvas'); c.width = c.height = 256;
|
| 93 |
+
const g = c.getContext('2d'); const id = g.createImageData(256, 256);
|
| 94 |
+
for (let i = 0; i < 256 * 256; i++) { const v = 128 + (D.hash(i * 0.731) - .5) * 90; id.data[i * 4] = id.data[i * 4 + 1] = id.data[i * 4 + 2] = v; id.data[i * 4 + 3] = 255; }
|
| 95 |
+
g.putImageData(id, 0, 0); grain = c; return c;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* ---------- 字幕折行 ---------- */
|
| 99 |
+
function wrap(ctx, text, maxW) {
|
| 100 |
+
const out = []; let cur = '';
|
| 101 |
+
for (const ch of text) {
|
| 102 |
+
if (ctx.measureText(cur + ch).width > maxW && cur) {
|
| 103 |
+
// 避免标点落在行首
|
| 104 |
+
if (/[,。!?、:;”)》…,.!?]/.test(ch)) { cur += ch; continue; }
|
| 105 |
+
out.push(cur); cur = ch;
|
| 106 |
+
} else cur += ch;
|
| 107 |
+
}
|
| 108 |
+
if (cur) out.push(cur);
|
| 109 |
+
// 均衡两行长度
|
| 110 |
+
if (out.length === 2) {
|
| 111 |
+
const all = out.join(''); const half = Math.ceil(all.length / 2);
|
| 112 |
+
let cut = half;
|
| 113 |
+
for (let d = 0; d < 5; d++) { if (/[,。!?:;]/.test(all[half + d - 1])) { cut = half + d; break; } if (/[,。!?:;]/.test(all[half - d - 1])) { cut = half - d; break; } }
|
| 114 |
+
const a = all.slice(0, cut), b = all.slice(cut);
|
| 115 |
+
if (ctx.measureText(a).width <= maxW && ctx.measureText(b).width <= maxW) return [a, b];
|
| 116 |
+
}
|
| 117 |
+
return out;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* ---------- 叠加层 ---------- */
|
| 121 |
+
function drawSubtitle(ctx, t, Lo) {
|
| 122 |
+
const ln = TL.lineAt(t);
|
| 123 |
+
if (!ln) return;
|
| 124 |
+
const a = D.clamp01((t - ln.start + 0.05) / 0.15) * D.clamp01((ln.until + 0.05 - t) / 0.12);
|
| 125 |
+
if (a <= 0) return;
|
| 126 |
+
const S = Lo.sub;
|
| 127 |
+
ctx.save(); ctx.globalAlpha = a;
|
| 128 |
+
ctx.font = D.font(S.size);
|
| 129 |
+
const lines = wrap(ctx, ln.text, S.maxW);
|
| 130 |
+
const lh = S.size * 1.32;
|
| 131 |
+
const y0 = Lo.name === 'portrait' ? S.y : S.y - (lines.length - 1) * lh / 2;
|
| 132 |
+
const col = ln.v === 'm' ? '#ffd54a' : ln.v === 'a' ? '#8ff5b5' : '#ffffff';
|
| 133 |
+
if (ln.v !== 'n') {
|
| 134 |
+
const tag = ln.v === 'm' ? '商人' : '助手';
|
| 135 |
+
const ty = y0 - lh * 0.5 - 30;
|
| 136 |
+
ctx.font = D.font(S.size * 0.62); const tw = ctx.measureText(tag).width + 34;
|
| 137 |
+
D.rrect(ctx, S.cx - tw / 2, ty - 22, tw, 44, 20, 7, 0.6); D.fs(ctx, ln.v === 'm' ? '#d9a400' : '#2c9c5c', 4);
|
| 138 |
+
D.text(ctx, tag, S.cx, ty + 1, S.size * 0.62, '#fff');
|
| 139 |
+
ctx.font = D.font(S.size);
|
| 140 |
+
}
|
| 141 |
+
lines.forEach((s, i) => {
|
| 142 |
+
const y = y0 + i * lh;
|
| 143 |
+
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 144 |
+
ctx.lineJoin = 'round';
|
| 145 |
+
ctx.lineWidth = S.size * 0.26; ctx.strokeStyle = 'rgba(30,22,20,.95)'; ctx.strokeText(s, S.cx, y);
|
| 146 |
+
ctx.fillStyle = col; ctx.fillText(s, S.cx, y);
|
| 147 |
+
});
|
| 148 |
+
ctx.restore();
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
function drawHud(ctx, t, Lo, sc, lt) {
|
| 152 |
+
const h = hudAt(t);
|
| 153 |
+
const P = Lo.name === 'portrait';
|
| 154 |
+
// 章节标签
|
| 155 |
+
const chap = sc.index === 0 ? '开场' : `第${sc.index}幕`;
|
| 156 |
+
const ttl = sc.index === 0 ? '先问你一个问题' : sc.title;
|
| 157 |
+
const ink = D.clamp01(lt / 0.5) * D.clamp01((sc.dur - lt) / 0.3);
|
| 158 |
+
if (P) {
|
| 159 |
+
// 竖屏:顶部安全区下方放系列标题 + 章节
|
| 160 |
+
D.bigText(ctx, '收猴子的故事', 485, 282, 74, '#ffcd3c', { shadow: 'rgba(43,35,33,.9)' });
|
| 161 |
+
ctx.save(); ctx.globalAlpha = ink;
|
| 162 |
+
ctx.font = D.font(38); const txt = `${chap}|${ttl}`; const w = ctx.measureText(txt).width + 50;
|
| 163 |
+
D.rrect(ctx, 485 - w / 2, 340, w, 58, 28, 3, 1); D.fs(ctx, '#2b2321', 0);
|
| 164 |
+
D.text(ctx, txt, 485, 370, 38, '#fff');
|
| 165 |
+
ctx.restore();
|
| 166 |
+
if (h.price || h.cage != null) drawPriceChip(ctx, t, h, 485, 438, 0.92, true);
|
| 167 |
+
} else {
|
| 168 |
+
ctx.save(); ctx.globalAlpha = ink;
|
| 169 |
+
ctx.font = D.font(34); const txt = `${chap}|${ttl}`; const w = ctx.measureText(txt).width + 46;
|
| 170 |
+
D.rrect(ctx, 34, 34, w, 56, 28, 3, 1); D.fs(ctx, 'rgba(43,35,33,.88)', 0);
|
| 171 |
+
D.text(ctx, txt, 34 + w / 2, 63, 34, '#fff');
|
| 172 |
+
ctx.restore();
|
| 173 |
+
D.text(ctx, '经济学寓言 · 收猴子的故事', 34, 120, 26, 'rgba(43,35,33,.55)', { align: 'left' });
|
| 174 |
+
if (h.price || h.cage != null) drawPriceChip(ctx, t, h, 1920 - 190, 120, 1, false);
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
function drawPriceChip(ctx, t, h, cx, cy, s, row) {
|
| 178 |
+
const pulse = h.since < 0.6 ? 1 + 0.25 * Math.sin(h.since / 0.6 * Math.PI) : 1;
|
| 179 |
+
ctx.save(); ctx.translate(cx, cy); ctx.scale(s, s);
|
| 180 |
+
if (row) {
|
| 181 |
+
// 横排:收购价 | 笼中猴子
|
| 182 |
+
let x = 0;
|
| 183 |
+
const parts = [];
|
| 184 |
+
if (h.price) parts.push(['p', h.label, '¥' + h.price]);
|
| 185 |
+
if (h.cage != null) parts.push(['c', '笼中猴子', h.cage + '只']);
|
| 186 |
+
const pw = 300, gap = 20, tot = parts.length * pw + (parts.length - 1) * gap;
|
| 187 |
+
x = -tot / 2;
|
| 188 |
+
parts.forEach(([k, lab, val]) => {
|
| 189 |
+
D.rrect(ctx, x, -34, pw, 68, 20, k === 'p' ? 5 : 6, 1); D.fs(ctx, k === 'p' ? '#fff4d6' : '#e7f6ff', 4);
|
| 190 |
+
D.text(ctx, lab, x + 20, 1, 28, '#6b5a4a', { align: 'left' });
|
| 191 |
+
ctx.save(); ctx.translate(x + pw - 22, 0); if (k === 'p') ctx.scale(pulse, pulse);
|
| 192 |
+
D.text(ctx, val, 0, 2, 44, k === 'p' ? (h.price === '???' ? '#888' : D.C.red) : D.C.blue, { align: 'right', stroke: 0 });
|
| 193 |
+
ctx.restore();
|
| 194 |
+
x += pw + gap;
|
| 195 |
+
});
|
| 196 |
+
} else {
|
| 197 |
+
let y = 0;
|
| 198 |
+
if (h.price) {
|
| 199 |
+
D.rrect(ctx, -150, -60, 300, 140, 22, 5, 1.2); D.fs(ctx, '#fff4d6', 5);
|
| 200 |
+
D.text(ctx, h.label, 0, -30, 30, '#6b5a4a');
|
| 201 |
+
ctx.save(); ctx.translate(0, 26); ctx.scale(pulse, pulse);
|
| 202 |
+
D.bigText(ctx, '¥' + h.price, 0, 0, 64, h.price === '???' ? '#aaa' : D.C.red, { shadow: null });
|
| 203 |
+
ctx.restore();
|
| 204 |
+
y = 100;
|
| 205 |
+
}
|
| 206 |
+
if (h.cage != null) {
|
| 207 |
+
D.rrect(ctx, -150, y, 300, 90, 22, 6, 1.2); D.fs(ctx, '#e7f6ff', 5);
|
| 208 |
+
D.text(ctx, '笼中猴子', 0, y + 26, 26, '#4b5d6b');
|
| 209 |
+
D.text(ctx, h.cage + ' 只', 0, y + 62, 38, D.C.blue);
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
ctx.restore();
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
function drawIris(ctx, Lo, sc, lt, first, last) {
|
| 216 |
+
let r = null;
|
| 217 |
+
const cx = Lo.sx + STAGE_W * Lo.k / 2, cy = Lo.sy + STAGE_H * Lo.k / 2;
|
| 218 |
+
const R = Math.hypot(Lo.W, Lo.H);
|
| 219 |
+
if (!first && lt < 0.45) r = D.E.inout(lt / 0.45) * R;
|
| 220 |
+
const endD = last ? 1.2 : 0.3;
|
| 221 |
+
if (sc.dur - lt < endD) r = D.E.inout((sc.dur - lt) / endD) * R;
|
| 222 |
+
if (first && lt < 0.6) { ctx.fillStyle = `rgba(43,35,33,${1 - lt / 0.6})`; ctx.fillRect(0, 0, Lo.W, Lo.H); }
|
| 223 |
+
if (r == null) return;
|
| 224 |
+
ctx.save(); ctx.beginPath(); ctx.rect(0, 0, Lo.W, Lo.H); ctx.arc(cx, cy, Math.max(0, r), 0, Math.PI * 2, true);
|
| 225 |
+
ctx.fillStyle = '#2b2321'; ctx.fill('evenodd'); ctx.restore();
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function drawSafeGuides(ctx, Lo) {
|
| 229 |
+
if (!Lo.safe) {
|
| 230 |
+
ctx.save(); ctx.setLineDash([16, 12]); ctx.strokeStyle = 'rgba(0,200,255,.8)'; ctx.lineWidth = 3;
|
| 231 |
+
ctx.strokeRect(Lo.W * 0.05, Lo.H * 0.05, Lo.W * 0.9, Lo.H * 0.9); ctx.restore(); return;
|
| 232 |
+
}
|
| 233 |
+
const s = Lo.safe;
|
| 234 |
+
ctx.save();
|
| 235 |
+
ctx.fillStyle = 'rgba(255,0,80,.18)';
|
| 236 |
+
ctx.fillRect(0, 0, Lo.W, s.top); ctx.fillRect(0, s.bottom, Lo.W, Lo.H - s.bottom); ctx.fillRect(s.right, Lo.H * 0.45, Lo.W - s.right, s.bottom - Lo.H * 0.45);
|
| 237 |
+
ctx.font = '30px sans-serif'; ctx.fillStyle = 'rgba(255,255,255,.95)'; ctx.textAlign = 'center';
|
| 238 |
+
ctx.fillText('刘海 / 状态栏 / 顶部导航 区域', Lo.W / 2, s.top / 2);
|
| 239 |
+
ctx.fillText('抖音 账号名 / 文案 / 音乐 / 进度条 区域', Lo.W / 2, s.bottom + 120);
|
| 240 |
+
ctx.save(); ctx.translate(s.right + 70, Lo.H * 0.62); ctx.rotate(Math.PI / 2); ctx.fillText('点赞 / 评论 / 分享 按钮', 0, 0); ctx.restore();
|
| 241 |
+
ctx.restore();
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/* ---------- 主渲染 ---------- */
|
| 245 |
+
function render(ctx, t, Lo, opts = {}) {
|
| 246 |
+
D.boil = Math.floor(t * 8);
|
| 247 |
+
const { sc, lt } = TL.at(t);
|
| 248 |
+
const scene = window.SCENES[sc.id];
|
| 249 |
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
| 250 |
+
ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over';
|
| 251 |
+
ctx.fillStyle = '#fbf3e1'; ctx.fillRect(0, 0, Lo.W, Lo.H);
|
| 252 |
+
const S = {
|
| 253 |
+
t, lt, dur: sc.dur, L: sc.L, id: sc.id, portrait: Lo.name === 'portrait',
|
| 254 |
+
ls: (i) => lt - sc.L[i],
|
| 255 |
+
};
|
| 256 |
+
ctx.save();
|
| 257 |
+
ctx.setTransform(Lo.k, 0, 0, Lo.k, Lo.sx, Lo.sy);
|
| 258 |
+
try { scene && scene(ctx, lt, S); } catch (e) { console.error(sc.id, e); }
|
| 259 |
+
ctx.restore();
|
| 260 |
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
| 261 |
+
// 颗粒与暗角
|
| 262 |
+
ctx.save(); ctx.globalAlpha = 0.07; ctx.globalCompositeOperation = 'multiply';
|
| 263 |
+
ctx.fillStyle = ctx.createPattern(getGrain(), 'repeat'); ctx.fillRect(0, 0, Lo.W, Lo.H); ctx.restore();
|
| 264 |
+
const vg = ctx.createRadialGradient(Lo.W / 2, Lo.H / 2, Math.min(Lo.W, Lo.H) * 0.45, Lo.W / 2, Lo.H / 2, Math.hypot(Lo.W, Lo.H) * 0.62);
|
| 265 |
+
vg.addColorStop(0, 'rgba(43,35,33,0)'); vg.addColorStop(1, 'rgba(43,35,33,.28)');
|
| 266 |
+
ctx.fillStyle = vg; ctx.fillRect(0, 0, Lo.W, Lo.H);
|
| 267 |
+
drawIris(ctx, Lo, sc, lt, sc.index === 0, sc.index === TL.scenes.length - 1);
|
| 268 |
+
const irisDark = (sc.index > 0 && lt < 0.12) || (sc.dur - lt < 0.08);
|
| 269 |
+
if (!irisDark) drawHud(ctx, t, Lo, sc, lt);
|
| 270 |
+
drawSubtitle(ctx, t, Lo);
|
| 271 |
+
if (opts.guides) drawSafeGuides(ctx, Lo);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
/* ---------- 封面 ---------- */
|
| 275 |
+
function renderCover(ctx, Lo) {
|
| 276 |
+
D.boil = 3;
|
| 277 |
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
| 278 |
+
ctx.fillStyle = '#fbf3e1'; ctx.fillRect(0, 0, Lo.W, Lo.H);
|
| 279 |
+
ctx.save(); ctx.setTransform(Lo.k, 0, 0, Lo.k, Lo.sx, Lo.sy);
|
| 280 |
+
window.SCENES.__cover(ctx, Lo.name === 'portrait');
|
| 281 |
+
ctx.restore();
|
| 282 |
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
| 283 |
+
ctx.save(); ctx.globalAlpha = 0.07; ctx.globalCompositeOperation = 'multiply';
|
| 284 |
+
ctx.fillStyle = ctx.createPattern(getGrain(), 'repeat'); ctx.fillRect(0, 0, Lo.W, Lo.H); ctx.restore();
|
| 285 |
+
const P = Lo.name === 'portrait';
|
| 286 |
+
if (P) {
|
| 287 |
+
D.bigText(ctx, '收猴子的故事', 540, 330, 132, '#ffcd3c', { rot: -0.03 });
|
| 288 |
+
ctx.save(); D.rrect(ctx, 150, 420, 780, 92, 40, 2, 1); D.fs(ctx, '#ff5b4f', 6); ctx.restore();
|
| 289 |
+
D.text(ctx, '一个寓言,看懂所有泡沫', 540, 468, 54, '#fff');
|
| 290 |
+
D.bigText(ctx, '你会花35块', 470, 1320, 92, '#fff');
|
| 291 |
+
D.bigText(ctx, '买一只猴子吗?', 470, 1440, 92, '#ffcd3c');
|
| 292 |
+
} else {
|
| 293 |
+
D.bigText(ctx, '收猴子的故事', 960, 150, 150, '#ffcd3c', { rot: -0.03 });
|
| 294 |
+
D.rrect(ctx, 600, 238, 720, 84, 40, 2, 1); D.fs(ctx, '#ff5b4f', 6);
|
| 295 |
+
D.text(ctx, '一个寓言,看懂所有泡沫', 960, 281, 52, '#fff');
|
| 296 |
+
D.bigText(ctx, '35块买只猴子', 330, 560, 72, '#fff', { rot: -0.08 });
|
| 297 |
+
D.bigText(ctx, '你干不干?', 330, 660, 72, '#ffcd3c', { rot: -0.08 });
|
| 298 |
+
D.bigText(ctx, '博傻理论', 1640, 600, 70, '#fff', { rot: 0.08 });
|
| 299 |
+
D.bigText(ctx, '泡沫真相', 1640, 700, 70, '#8ff5b5', { rot: 0.08 });
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
window.ENGINE = { render, renderCover, TL, LAYOUTS, STAGE_W, STAGE_H };
|
| 304 |
+
})();
|
shouhouzi/js/scenes.js
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =========================================================
|
| 2 |
+
* scenes.js —— 15 个场景动画。舞台坐标 1280×960,地平线 GY。
|
| 3 |
+
* 每个场景函数签名 (ctx, lt, S):lt 场景内时间,S.ls(i) = 距第 i 句开始的秒数
|
| 4 |
+
* 背景会画到舞台之外,以覆盖横屏两侧与竖屏上下的额外区域。
|
| 5 |
+
* ========================================================= */
|
| 6 |
+
(function () {
|
| 7 |
+
const { C, E } = D;
|
| 8 |
+
const GY = 780;
|
| 9 |
+
const X0 = -900, X1 = 2200, Y0 = -1000, Y1 = 2300;
|
| 10 |
+
const SC = {};
|
| 11 |
+
|
| 12 |
+
/* ---------- 小工具 ---------- */
|
| 13 |
+
// 元素可见度:a 时刻弹入,b 时刻后收起;返回缩放系数
|
| 14 |
+
function vis(lt, a, b = null, inD = 0.45, outD = 0.3) {
|
| 15 |
+
if (lt < a) return 0;
|
| 16 |
+
let k = E.back((lt - a) / inD);
|
| 17 |
+
if (b != null && lt > b) k *= 1 - E.in((lt - b) / outD);
|
| 18 |
+
return Math.max(0, k);
|
| 19 |
+
}
|
| 20 |
+
function fade(lt, a, b = null, inD = 0.35, outD = 0.3) {
|
| 21 |
+
if (lt < a) return 0;
|
| 22 |
+
let k = D.clamp01((lt - a) / inD);
|
| 23 |
+
if (b != null && lt > b) k *= 1 - D.clamp01((lt - b) / outD);
|
| 24 |
+
return k;
|
| 25 |
+
}
|
| 26 |
+
const at = D.at;
|
| 27 |
+
function fillAll(ctx, col) { ctx.fillStyle = col; ctx.fillRect(X0, Y0, X1 - X0, Y1 - Y0); }
|
| 28 |
+
function sky(ctx, top = '#8fd3ff', bot = '#e9f8ff') {
|
| 29 |
+
const g = ctx.createLinearGradient(0, -700, 0, GY); g.addColorStop(0, top); g.addColorStop(1, bot);
|
| 30 |
+
ctx.fillStyle = g; ctx.fillRect(X0, Y0, X1 - X0, GY - Y0 + 10);
|
| 31 |
+
}
|
| 32 |
+
function ground(ctx, col = C.grass, col2 = C.grass2, t = 0) {
|
| 33 |
+
ctx.beginPath(); ctx.moveTo(X0, GY + 10);
|
| 34 |
+
for (let x = X0; x <= X1; x += 80) ctx.lineTo(x, GY + Math.sin(x * 0.01) * 10 + D.jit(x, 1.5));
|
| 35 |
+
ctx.lineTo(X1, Y1); ctx.lineTo(X0, Y1); ctx.closePath(); D.fs(ctx, col, 5);
|
| 36 |
+
// 草丛 / 小花
|
| 37 |
+
for (let i = 0; i < 60; i++) {
|
| 38 |
+
const x = X0 + D.hash(i * 3.3) * (X1 - X0), y = GY + 40 + D.hash(i * 7.7) * 1300;
|
| 39 |
+
ctx.beginPath(); ctx.moveTo(x - 12, y); ctx.lineTo(x - 4, y - 18); ctx.lineTo(x, y - 4); ctx.lineTo(x + 6, y - 20); ctx.lineTo(x + 12, y);
|
| 40 |
+
D.stroke(ctx, 4, col2);
|
| 41 |
+
if (i % 5 === 0) { ctx.beginPath(); ctx.arc(x + 20, y - 8, 7, 0, Math.PI * 2); D.fs(ctx, i % 2 ? '#fff' : '#ffd84a', 3); }
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
function mountains(ctx, col = '#bfe0c8', col2 = '#a6d1b4') {
|
| 45 |
+
D.poly(ctx, [[X0, GY], [-500, 380], [-200, 560], [100, 300], [420, 520], [700, 330], [1000, 540], [1300, 280], [1650, 500], [1900, 360], [X1, 520], [X1, GY]], 401, 2); D.fs(ctx, col, 4);
|
| 46 |
+
D.poly(ctx, [[X0, GY], [-350, 560], [0, 640], [300, 500], [640, 620], [900, 480], [1200, 640], [1500, 520], [X1, 640], [X1, GY]], 402, 2); D.fs(ctx, col2, 4);
|
| 47 |
+
}
|
| 48 |
+
function clouds(ctx, t, y = -40) {
|
| 49 |
+
for (let i = 0; i < 6; i++) {
|
| 50 |
+
const span = X1 - X0 + 400;
|
| 51 |
+
const x = X0 - 200 + ((D.hash(i * 9.1) * span + t * (14 + i * 4)) % span);
|
| 52 |
+
D.cloud(ctx, x, y + D.hash(i * 4.2) * 260 - (i % 2) * 380, 0.8 + D.hash(i) * 0.6, 410 + i);
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
// 森林带:density 0..1 控制树上猴子数量
|
| 56 |
+
const FOREST = [];
|
| 57 |
+
for (let i = 0; i < 20; i++) FOREST.push({ x: X0 + 60 + i * 160 + D.hash(i * 2.1) * 50, s: 0.95 + D.hash(i * 5.3) * 0.5, pine: i % 3 === 1 });
|
| 58 |
+
function forest(ctx, t, y, density = 1, o = {}) {
|
| 59 |
+
const sc = o.scale || 1;
|
| 60 |
+
FOREST.forEach((f, i) => {
|
| 61 |
+
if (f.pine) D.pine(ctx, f.x, y, f.s * sc * 1.05, 420 + i, o.col2 || C.forest2);
|
| 62 |
+
else D.tree(ctx, f.x, y, f.s * sc, 420 + i, o.col || C.forest);
|
| 63 |
+
});
|
| 64 |
+
const n = Math.round(FOREST.length * 1.5 * D.clamp01(density));
|
| 65 |
+
for (let i = 0; i < n; i++) {
|
| 66 |
+
const f = FOREST[(i * 7) % FOREST.length];
|
| 67 |
+
const hang = i % 3 === 0;
|
| 68 |
+
const sw = Math.sin(t * 2.2 + i) * 0.35;
|
| 69 |
+
const mx = f.x + (D.hash(i * 1.7) - .5) * 70, my = y - 150 * f.s * sc + (i % 2) * 30;
|
| 70 |
+
if (hang) { // 吊在树枝上荡秋千
|
| 71 |
+
ctx.save(); ctx.translate(mx, my - 40); ctx.rotate(sw);
|
| 72 |
+
D.line(ctx, 0, 0, 0, 40, 430 + i); D.stroke(ctx, 6, '#6b8e23');
|
| 73 |
+
D.monkey(ctx, 0, 190, 0.62 * sc, { t, seed: i + 3, aL: 3.0, aR: 2.9, mood: o.mood || 'happy' });
|
| 74 |
+
ctx.restore();
|
| 75 |
+
} else {
|
| 76 |
+
D.monkey(ctx, mx, my + 40 + Math.abs(Math.sin(t * 3 + i)) * -14, 0.55 * sc, { t, seed: i + 3, flip: i % 2, aL: 2.4 + Math.sin(t * 4 + i) * .4, mood: o.mood || 'happy' });
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
function houses(ctx, t, smoke = true) {
|
| 81 |
+
D.house(ctx, -330, GY + 20, 0.9, 501, '#f6e3c3', '#d9534f', smoke ? t : null);
|
| 82 |
+
D.house(ctx, 170, GY + 30, 1.05, 502, '#fdebd0', '#4a8fe2', smoke ? t + 1 : null);
|
| 83 |
+
D.house(ctx, 1120, GY + 30, 1.0, 503, '#f6e3c3', '#e8a33d', smoke ? t + 2 : null);
|
| 84 |
+
D.house(ctx, 1560, GY + 20, 0.9, 504, '#fdebd0', '#7bc96f', smoke ? t + 3 : null);
|
| 85 |
+
}
|
| 86 |
+
function paper(ctx, col = '#fbf3e1', grid = 'rgba(74,143,226,.13)') {
|
| 87 |
+
fillAll(ctx, col);
|
| 88 |
+
ctx.beginPath();
|
| 89 |
+
for (let x = -896; x < X1; x += 64) { ctx.moveTo(x, Y0); ctx.lineTo(x, Y1); }
|
| 90 |
+
for (let y = -960; y < Y1; y += 64) { ctx.moveTo(X0, y); ctx.lineTo(X1, y); }
|
| 91 |
+
ctx.lineWidth = 2; ctx.strokeStyle = grid; ctx.stroke();
|
| 92 |
+
}
|
| 93 |
+
function rays(ctx, cx, cy, t, col1 = '#ffe08a', col2 = '#ffd04d', n = 18) {
|
| 94 |
+
fillAll(ctx, col1);
|
| 95 |
+
ctx.save(); ctx.translate(cx, cy); ctx.rotate(t * 0.15);
|
| 96 |
+
for (let i = 0; i < n; i++) { ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, 3200, i / n * Math.PI * 2, (i + .5) / n * Math.PI * 2); ctx.closePath(); ctx.fillStyle = col2; ctx.fill(); }
|
| 97 |
+
ctx.restore();
|
| 98 |
+
}
|
| 99 |
+
function cam(ctx, cx, cy, z, fn) { ctx.save(); ctx.translate(cx, cy); ctx.scale(z, z); ctx.translate(-cx, -cy); fn(); ctx.restore(); }
|
| 100 |
+
function shake(ctx, amt, t) { if (amt > 0) ctx.translate(Math.sin(t * 61) * amt, Math.cos(t * 47) * amt); }
|
| 101 |
+
function tint(ctx, col, a) { if (a <= 0) return; ctx.save(); ctx.globalAlpha = a; fillAll(ctx, col); ctx.restore(); }
|
| 102 |
+
function desat(ctx, a) { if (a <= 0) return; ctx.save(); ctx.globalCompositeOperation = 'saturation'; ctx.globalAlpha = a; fillAll(ctx, '#808080'); ctx.restore(); }
|
| 103 |
+
function card(ctx, x, y, w, h, col = '#fffaf0', seed = 600) { D.rrect(ctx, x - w / 2 + 10, y - h / 2 + 14, w, h, 28, seed + 1); D.fs(ctx, 'rgba(43,35,33,.25)', 0); D.rrect(ctx, x - w / 2, y - h / 2, w, h, 28, seed); D.fs(ctx, col, 6); }
|
| 104 |
+
function label(ctx, txt, x, y, size, bg = C.red, fg = '#fff', rot = 0) {
|
| 105 |
+
ctx.save(); ctx.translate(x, y); ctx.rotate(rot); ctx.font = D.font(size); const w = ctx.measureText(txt).width + size;
|
| 106 |
+
D.rrect(ctx, -w / 2, -size * .8, w, size * 1.6, size * .6, 611); D.fs(ctx, bg, 5);
|
| 107 |
+
D.text(ctx, txt, 0, 2, size, fg); ctx.restore();
|
| 108 |
+
}
|
| 109 |
+
function rock(ctx, x, y, s) { at(ctx, x, y, s, () => { D.blob(ctx, 0, -50, 120, 60, 620); D.fs(ctx, '#b8b2a7', 6); ctx.beginPath(); ctx.moveTo(-60, -70); ctx.lineTo(-30, -60); D.stroke(ctx, 4, '#8f897f'); }); }
|
| 110 |
+
function counterText(v) { return Math.round(v).toLocaleString('en-US'); }
|
| 111 |
+
|
| 112 |
+
/* =======================================================
|
| 113 |
+
* 0 开场 hook
|
| 114 |
+
* ======================================================= */
|
| 115 |
+
SC.hook = function (ctx, lt, S) {
|
| 116 |
+
const t = S.t;
|
| 117 |
+
const shout = fade(lt, S.L[2], S.L[3]);
|
| 118 |
+
rays(ctx, 640, 480, t, shout > 0 ? '#ffd27a' : '#ffe9a6', shout > 0 ? '#ffc04a' : '#ffdf8a');
|
| 119 |
+
if (shout > 0) D.speedLines(ctx, 640, 420, t, `rgba(255,255,255,${0.5 * shout})`);
|
| 120 |
+
ctx.save(); shake(ctx, shout * (S.ls(2) < 1.2 ? 6 : 0), t);
|
| 121 |
+
// L0 大问号
|
| 122 |
+
const q = vis(lt, S.L[0], S.L[1] + 0.2);
|
| 123 |
+
at(ctx, 640, 420, q * 2.2, () => D.qmark(ctx, 0, 0, 1, t, C.blue));
|
| 124 |
+
if (q > 0) at(ctx, 640, 640, q, () => D.text(ctx, '先问你一个问题', 0, 0, 64, C.ink));
|
| 125 |
+
// L1~L2 商人登场
|
| 126 |
+
const m = vis(lt, S.L[1], S.L[3] - 0.1, 0.6);
|
| 127 |
+
if (m > 0) {
|
| 128 |
+
const my = D.lerp(1300, GY + 120, E.back(D.clamp01(S.ls(1) / 0.6)));
|
| 129 |
+
D.merchant(ctx, 900, my, 1.5 * Math.min(1, m + .2), { t, mood: shout > 0 ? 'talk' : 'happy', aR: 2.3, holdR: (c) => D.megaphone(c), aL: 0.5 });
|
| 130 |
+
if (shout > 0) {
|
| 131 |
+
const b = vis(lt, S.L[2], S.L[3] - 0.1);
|
| 132 |
+
at(ctx, 420, 330, b, () => {
|
| 133 |
+
D.bubble(ctx, 0, 0, 640, 300, 360, 160, { seed: 3 });
|
| 134 |
+
D.bigText(ctx, '收猴子!', 0, -70, 78, C.ink, { shadow: null, stroke: 0 });
|
| 135 |
+
D.bigText(ctx, '¥10/只', 0, 40, 120, C.red);
|
| 136 |
+
}, -0.05);
|
| 137 |
+
D.coinBurst(ctx, 640, 500, S.ls(2) - 0.4, 18, 7, 1.1);
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
// L3 干不干
|
| 141 |
+
const b3 = vis(lt, S.L[3], S.L[4] + 1.5);
|
| 142 |
+
if (b3 > 0) {
|
| 143 |
+
const click = S.ls(3) > 1.4;
|
| 144 |
+
at(ctx, 400, 480, b3 * (click && S.ls(3) < 1.6 ? 0.9 : 1), () => { card(ctx, 0, 0, 380, 200, click ? '#6fe08f' : '#b9f0c9', 701); D.bigText(ctx, '干!', 0, 0, 100, '#fff'); });
|
| 145 |
+
at(ctx, 880, 480, b3, () => { card(ctx, 0, 0, 380, 200, '#e1e1e1', 702); D.text(ctx, '不干', 0, 0, 80, '#999'); });
|
| 146 |
+
// 手形光标
|
| 147 |
+
const k = E.inout(D.clamp01((S.ls(3) - 0.4) / 0.9));
|
| 148 |
+
const hx = D.lerp(1100, 440, k), hy = D.lerp(820, 520, k);
|
| 149 |
+
at(ctx, hx, hy, b3, () => { D.blob(ctx, 0, 0, 30, 36, 703); D.fs(ctx, '#fff', 5); D.rrect(ctx, -10, -70, 20, 60, 10, 704); D.fs(ctx, '#fff', 5); });
|
| 150 |
+
if (click) D.confetti(ctx, 400, 440, S.ls(3) - 1.4, 30, 500, 11);
|
| 151 |
+
}
|
| 152 |
+
// L4 白来的钱
|
| 153 |
+
const b4 = vis(lt, S.L[4] + 0.2, S.L[5] - 0.1);
|
| 154 |
+
if (b4 > 0) {
|
| 155 |
+
at(ctx, 640, 260, b4, () => D.bigText(ctx, '白来的钱?', 0, 0, 96, C.green));
|
| 156 |
+
D.villager(ctx, 0, 460, GY + 60, 1.3 * b4, { t, mood: 'money', aL: 2.6, aR: 2.6 });
|
| 157 |
+
D.monkey(ctx, 820, GY + 40, 1.6 * b4, { t, mood: 'smug', aR: 2.5 });
|
| 158 |
+
D.coinBurst(ctx, 460, 500, S.ls(4) - 0.6, 14, 21, 0.9);
|
| 159 |
+
}
|
| 160 |
+
// L5 最贵的钱
|
| 161 |
+
const b5 = vis(lt, S.L[5], S.L[6] - 0.1);
|
| 162 |
+
if (b5 > 0) {
|
| 163 |
+
tint(ctx, '#2b2321', 0.55 * D.clamp01(S.ls(5) / 1.5));
|
| 164 |
+
const k = D.clamp01(S.ls(5) / 2);
|
| 165 |
+
D.bill(ctx, 640, 440, 3.2 * b5, Math.sin(t * 2) * 0.06, '10');
|
| 166 |
+
at(ctx, 900, 300, vis(lt, S.L[5] + 2.2), () => { D.stamp(ctx, 0, 0, 1.2, '最贵的钱?', C.red, 0.15); });
|
| 167 |
+
at(ctx, 640, 700, b5, () => D.text(ctx, '……真的是白来的吗?', 0, 0, 52, '#fff', { alpha: k }));
|
| 168 |
+
}
|
| 169 |
+
// L6 标题卡
|
| 170 |
+
const b6 = vis(lt, S.L[6] - 0.05, null, 0.7);
|
| 171 |
+
if (b6 > 0) {
|
| 172 |
+
rays(ctx, 640, 420, t, '#ffcd3c', '#ffb52e', 22);
|
| 173 |
+
D.confetti(ctx, 640, 200, S.ls(6) - 0.2, 60, 800, 31);
|
| 174 |
+
const title = '收猴子的故事';
|
| 175 |
+
[...title].forEach((ch, i) => {
|
| 176 |
+
const d = S.ls(6) - 0.15 - i * 0.12;
|
| 177 |
+
const k = E.elastic(D.clamp01(d / 0.8));
|
| 178 |
+
at(ctx, 640 + (i - 2.5) * 150, 360 + Math.sin(t * 3 + i) * 8, k, () => D.bigText(ctx, ch, 0, 0, 150, i % 2 ? '#fff' : C.red));
|
| 179 |
+
});
|
| 180 |
+
at(ctx, 640, 520, vis(lt, S.L[6] + 1.2), () => label(ctx, '经济学寓言', 0, 0, 56, C.ink, '#ffcd3c', -0.03));
|
| 181 |
+
D.monkey(ctx, 240, GY + 120, 1.5 * vis(lt, S.L[6] + 0.8), { t, aL: 2.9, aR: 2.9, mood: 'happy' });
|
| 182 |
+
D.monkey(ctx, 1040, GY + 120, 1.5 * vis(lt, S.L[6] + 1.0), { t, flip: 1, aL: 2.9, aR: 2.9, mood: 'smug' });
|
| 183 |
+
}
|
| 184 |
+
ctx.restore();
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
/* =======================================================
|
| 188 |
+
* 1 小山村 village
|
| 189 |
+
* ======================================================= */
|
| 190 |
+
SC.village = function (ctx, lt, S) {
|
| 191 |
+
const t = S.t;
|
| 192 |
+
const z = D.lerp(1.25, 1, E.inout(lt / 6));
|
| 193 |
+
cam(ctx, 640, 520, z, () => {
|
| 194 |
+
sky(ctx); D.sun(ctx, 1050, 60, 70, t); clouds(ctx, t);
|
| 195 |
+
mountains(ctx);
|
| 196 |
+
forest(ctx, t, GY - 70, vis(lt, S.L[1], null, 1.2), { scale: 0.85 });
|
| 197 |
+
ground(ctx);
|
| 198 |
+
houses(ctx, t);
|
| 199 |
+
// 小路
|
| 200 |
+
ctx.beginPath(); ctx.moveTo(560, Y1); ctx.quadraticCurveTo(600, 1000, 660, GY + 20); ctx.lineTo(720, GY + 20); ctx.quadraticCurveTo(760, 1000, 900, Y1); D.fs(ctx, '#e9d3a6', 4);
|
| 201 |
+
const fy = fade(lt, S.L[0], S.L[1] + 1);
|
| 202 |
+
if (fy > 0) at(ctx, 640, 200, 1, () => D.bigText(ctx, '从前……', 0, 0, 90, '#fff', { alpha: fy }));
|
| 203 |
+
if (lt > S.L[1] && lt < S.L[2] + 0.2) at(ctx, 640, 210, vis(lt, S.L[1] + 0.8, S.L[2]), () => label(ctx, '成百上千只猴子', 0, 0, 56, C.fur, '#fff'));
|
| 204 |
+
// L2 三个不能
|
| 205 |
+
const icons = [['不能吃', '🍚'], ['不能用', '🔧'], ['卖不了钱', '¥']];
|
| 206 |
+
icons.forEach(([txt], i) => {
|
| 207 |
+
const k = vis(lt, S.L[2] + 0.4 + i * 1.3, S.L[3] - 0.1);
|
| 208 |
+
if (k <= 0) return;
|
| 209 |
+
at(ctx, 300 + i * 340, 250, k, () => {
|
| 210 |
+
card(ctx, 0, 0, 290, 170, '#fff', 720 + i);
|
| 211 |
+
D.monkey(ctx, -80, 60, 0.6, { t, seed: i + 9, mood: 'smug' });
|
| 212 |
+
D.text(ctx, txt, 40, 0, 46, C.ink);
|
| 213 |
+
if (lt > S.L[2] + 0.9 + i * 1.3) D.cross(ctx, 40, 0, 50);
|
| 214 |
+
});
|
| 215 |
+
});
|
| 216 |
+
// L3 偷玉米
|
| 217 |
+
if (lt > S.L[3]) {
|
| 218 |
+
const d = S.ls(3);
|
| 219 |
+
const mx = D.lerp(-200, 1500, d / 4.4);
|
| 220 |
+
D.monkey(ctx, mx, GY + 110 - Math.abs(Math.sin(d * 9)) * 40, 1.1, { t, seed: 5, flip: 0, aR: 2.2, mood: 'smug', hold: (c, x, y) => { D.blob(c, x + 6, y - 20, 14, 34, 730); D.fs(c, '#ffd84a', 4); D.blob(c, x + 6, y + 14, 18, 10, 731); D.fs(c, '#7bc96f', 3); } });
|
| 221 |
+
const vx = D.lerp(-500, 1150, (d - 0.3) / 4.4);
|
| 222 |
+
D.villager(ctx, 0, vx, GY + 140, 1.0, { t, walk: d * 14, mood: 'angry', aR: 2.8 });
|
| 223 |
+
if (d > 0.6) at(ctx, vx + 60, GY - 210, 1, () => D.text(ctx, '站住!', 0, 0, 44, C.red, { stroke: 8, strokeColor: '#fff' }));
|
| 224 |
+
}
|
| 225 |
+
});
|
| 226 |
+
};
|
| 227 |
+
|
| 228 |
+
/* =======================================================
|
| 229 |
+
* 2 商人进村 arrive
|
| 230 |
+
* ======================================================= */
|
| 231 |
+
SC.arrive = function (ctx, lt, S) {
|
| 232 |
+
const t = S.t;
|
| 233 |
+
sky(ctx); D.sun(ctx, 1120, 40, 60, t); clouds(ctx, t);
|
| 234 |
+
mountains(ctx); forest(ctx, t, GY - 70, 0.8, { scale: 0.8, mood: S.ls(2) > 0 ? 'shock' : 'happy' }); ground(ctx); houses(ctx, t);
|
| 235 |
+
rock(ctx, 640, GY + 150, 1.6);
|
| 236 |
+
// 村民探头
|
| 237 |
+
const peek = vis(lt, S.L[0] + 1.5);
|
| 238 |
+
[[150, 0], [300, 1], [1000, 2], [1150, 3]].forEach(([x, i]) => D.villager(ctx, i, x, GY + 220, 0.9 * peek, { t, mood: S.ls(2) > 0.4 ? 'shock' : 'happy', flip: x > 640 }));
|
| 239 |
+
// 商人走入 -> 跳上石头
|
| 240 |
+
const d0 = S.ls(0), d1 = S.ls(1);
|
| 241 |
+
let mx, my, walk = null, aR = 0.3, mood = 'happy';
|
| 242 |
+
if (d1 < 0) { mx = D.lerp(-300, 470, E.out(d0 / 4)); my = GY + 200; walk = d0 < 4 ? d0 * 10 : null; }
|
| 243 |
+
else { const k = D.clamp01(d1 / 0.7); mx = D.lerp(470, 640, k); my = D.lerp(GY + 200, GY + 50, k) - Math.sin(k * Math.PI) * 120; aR = D.lerp(0.3, 2.4, k); }
|
| 244 |
+
if (S.ls(2) > 0) { mood = 'talk'; aR = 2.4; }
|
| 245 |
+
if (walk != null) D.dust(ctx, mx - 30, my, t, 5, 3);
|
| 246 |
+
D.merchant(ctx, mx, my, 1.2, { t, walk, mood, aR, holdR: d1 > 0.2 ? (c) => D.megaphone(c) : null, aL: 0.4 });
|
| 247 |
+
if (d0 > 0.3 && d1 < 0) at(ctx, mx, my - 380, vis(lt, S.L[0] + 0.5, S.L[1]), () => label(ctx, '神秘商人', 0, 0, 42, C.ink, '#ffcd3c'));
|
| 248 |
+
// L2 喊话
|
| 249 |
+
if (S.ls(2) > 0) {
|
| 250 |
+
const k = vis(lt, S.L[2]);
|
| 251 |
+
D.speedLines(ctx, 760, 300, t, 'rgba(255,255,255,.35)', 28, 300);
|
| 252 |
+
at(ctx, 980, 230, k, () => {
|
| 253 |
+
D.bubble(ctx, 0, 0, 520, 280, -220, 160, { seed: 7 });
|
| 254 |
+
D.text(ctx, '高价收猴子!', 0, -70, 60, C.ink);
|
| 255 |
+
D.bigText(ctx, '¥10/只', 0, 30, 100, C.red);
|
| 256 |
+
D.text(ctx, '现金结算', 0, 105, 36, C.green);
|
| 257 |
+
}, Math.sin(t * 8) * 0.02);
|
| 258 |
+
[[150, 0], [300, 1], [1000, 2], [1150, 3]].forEach(([x], i) => D.exclaim(ctx, x, GY - 120, 0.5 * vis(lt, S.L[2] + 0.4 + i * 0.15), t));
|
| 259 |
+
}
|
| 260 |
+
};
|
| 261 |
+
|
| 262 |
+
/* =======================================================
|
| 263 |
+
* 3 十元一只 ten
|
| 264 |
+
* ======================================================= */
|
| 265 |
+
SC.ten = function (ctx, lt, S) {
|
| 266 |
+
const t = S.t;
|
| 267 |
+
sky(ctx); clouds(ctx, t); D.sun(ctx, 1100, 0, 60, t);
|
| 268 |
+
const dens = 1 - 0.35 * D.clamp01((lt - S.L[2]) / 14);
|
| 269 |
+
forest(ctx, t, GY - 10, dens, { scale: 1.15 });
|
| 270 |
+
ground(ctx);
|
| 271 |
+
const conv = S.ls(3) > 0; // 交易阶段
|
| 272 |
+
// 商人 + 笼子(左侧)
|
| 273 |
+
const cageFill = conv ? D.clamp01(S.ls(3) / 9) * 0.6 : 0;
|
| 274 |
+
if (lt > S.L[1]) {
|
| 275 |
+
D.cage(ctx, 260, GY + 200, 360, 250, cageFill, t, { cols: 5, rows: 3, label: '收购笼' });
|
| 276 |
+
D.merchant(ctx, 520, GY + 210, 1.0, { t, mood: 'laugh', aR: conv ? 1.6 + Math.sin(t * 6) * .3 : 0.4 });
|
| 277 |
+
}
|
| 278 |
+
// L0 村民怀疑
|
| 279 |
+
if (lt < S.L[2] + 0.3) {
|
| 280 |
+
const k = vis(lt, S.L[0], S.L[2]);
|
| 281 |
+
[[700, 0], [860, 1], [1020, 2]].forEach(([x, i]) => D.villager(ctx, i, x, GY + 200, 1.0, { t, mood: S.ls(1) > 1.5 ? 'shock' : 'sly', aR: 2.6 }));
|
| 282 |
+
at(ctx, 860, 250, k * (S.ls(1) > 0 ? 1 - D.clamp01(S.ls(1) / .3) : 1), () => D.thought(ctx, 0, 0, 520, 200, 0, 180, { text: ['这人……', '脑子有问题吧?'], size: 46 }));
|
| 283 |
+
// L1 第一个人拿到钱
|
| 284 |
+
if (S.ls(1) > 0) {
|
| 285 |
+
const d = S.ls(1);
|
| 286 |
+
const mx = D.lerp(700, 520, E.inout(d / 1)), my = GY + 40 - Math.sin(D.clamp01(d) * Math.PI) * 80;
|
| 287 |
+
if (d < 1.2) D.monkey(ctx, mx, my, 0.7, { t, mood: 'shock' });
|
| 288 |
+
D.coinBurst(ctx, 640, GY - 100, d - 1.1, 10, 41, 0.8);
|
| 289 |
+
at(ctx, 700, GY - 260, vis(lt, S.L[1] + 1.3), () => D.bigText(ctx, '+¥10', 0, -Math.min(60, (d - 1.3) * 40), 80, C.green));
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
// L2 冲进山
|
| 293 |
+
if (S.ls(2) > 0 && !conv) {
|
| 294 |
+
const d = S.ls(2);
|
| 295 |
+
for (let i = 0; i < 7; i++) {
|
| 296 |
+
const x = D.lerp(-300 - i * 170, 1700, (d - i * 0.08) / 4.2);
|
| 297 |
+
D.villager(ctx, i, x, GY + 170 + (i % 2) * 70, 0.95, { t: t + i, walk: d * 12 + i, mood: 'laugh', aR: 2.7, holdR: (c) => D.net(c) });
|
| 298 |
+
D.dust(ctx, x - 40, GY + 170 + (i % 2) * 70, t + i * .3, 3, i);
|
| 299 |
+
}
|
| 300 |
+
at(ctx, 640, 220, vis(lt, S.L[2] + 0.3, S.L[3] - 0.2), () => D.bigText(ctx, '冲啊!!', 0, 0, 110, C.red, { rot: -0.08 }));
|
| 301 |
+
}
|
| 302 |
+
// L3~L4 一手交猴一手交钱:猴子流水线
|
| 303 |
+
if (conv) {
|
| 304 |
+
const d = S.ls(3);
|
| 305 |
+
for (let i = 0; i < 6; i++) {
|
| 306 |
+
const ph = ((d * 0.55 + i / 6) % 1);
|
| 307 |
+
const x = D.lerp(1250, 330, ph), y = GY + 60 - Math.abs(Math.sin(ph * Math.PI * 3)) * 140;
|
| 308 |
+
D.monkey(ctx, x, y, 0.55, { t, seed: i + 20, mood: 'shock', aL: 2.8, aR: 2.8 });
|
| 309 |
+
const cx = D.lerp(560, 1150, ph), cy = GY - 260 - Math.sin(ph * Math.PI) * 160;
|
| 310 |
+
D.coin(ctx, cx, cy, 22, t * 8 + i);
|
| 311 |
+
}
|
| 312 |
+
[[900, 3], [1050, 4], [1200, 5]].forEach(([x, i], j) => D.villager(ctx, i, x, GY + 210, 0.95, { t, mood: 'money', aL: 2.5 + Math.sin(t * 6 + j) * .3, aR: 2.5, flip: true, jump: Math.abs(Math.sin(t * 5 + j)) * 20 }));
|
| 313 |
+
at(ctx, 640, 170, vis(lt, S.L[3] + 0.2, S.L[4]), () => label(ctx, '一手交猴 · 一手交钱', 0, 0, 52, C.ink, '#ffcd3c'));
|
| 314 |
+
if (S.ls(4) > 0) at(ctx, 950, 200, vis(lt, S.L[4] + 0.5), () => { card(ctx, 0, 0, 420, 140, '#fff', 740); D.text(ctx, '村民口袋:鼓起来了!', 0, 0, 40, C.green); });
|
| 315 |
+
}
|
| 316 |
+
};
|
| 317 |
+
|
| 318 |
+
/* =======================================================
|
| 319 |
+
* 4 二十元 twenty
|
| 320 |
+
* ======================================================= */
|
| 321 |
+
SC.twenty = function (ctx, lt, S) {
|
| 322 |
+
const t = S.t;
|
| 323 |
+
// L1 太阳快速划过(一整天)
|
| 324 |
+
const day = D.clamp01(S.ls(1) / 4.5);
|
| 325 |
+
const dusk = S.ls(1) > 0 && S.ls(2) < 0 ? Math.sin(day * Math.PI) * 0.0 + day * 0.5 : 0;
|
| 326 |
+
sky(ctx, dusk > 0 ? '#f6a96b' : '#8fd3ff', '#fff1d6');
|
| 327 |
+
D.sun(ctx, D.lerp(-100, 1400, S.ls(1) > 0 && S.ls(2) < 0 ? day : 0.75), D.lerp(300, -60, Math.sin((S.ls(1) > 0 && S.ls(2) < 0 ? day : .75) * Math.PI)), 60, t);
|
| 328 |
+
clouds(ctx, t);
|
| 329 |
+
const dens = D.lerp(0.6, 0.3, D.clamp01(lt / 8));
|
| 330 |
+
forest(ctx, t, GY - 10, S.ls(4) > 0 ? 0.3 : dens, { scale: 1.15 });
|
| 331 |
+
ground(ctx);
|
| 332 |
+
if (S.ls(2) < 0) {
|
| 333 |
+
// 抓猴子越来越难
|
| 334 |
+
const d = S.ls(1);
|
| 335 |
+
const x = 640 + Math.sin(t * 1.4) * 260;
|
| 336 |
+
D.villager(ctx, 0, x, GY + 200, 1.1, { t, walk: t * 8, flip: Math.cos(t * 1.4) < 0, mood: d > 0 ? 'sad' : 'happy', aR: 2.6, holdR: (c) => D.net(c) });
|
| 337 |
+
if (d > 0) {
|
| 338 |
+
for (let i = 0; i < 3; i++) { const k = (t * 1.5 + i / 3) % 1; ctx.globalAlpha = 1 - k; D.blob(ctx, x + 50 + i * 10, GY - 130 + k * 60, 8, 12, 750 + i); D.fs(ctx, '#7fd0ff', 3); ctx.globalAlpha = 1; }
|
| 339 |
+
at(ctx, 640, 200, vis(lt, S.L[1] + 0.6, S.L[2]), () => { card(ctx, 0, 0, 560, 150, '#fff', 751); D.text(ctx, '跑一整天……才 1~2 只', 0, 0, 48, C.ink); });
|
| 340 |
+
} else {
|
| 341 |
+
at(ctx, 640, 200, vis(lt, S.L[0] + 0.8, S.L[1]), () => label(ctx, '林子里的猴子:越来越少 ↓', 0, 0, 48, C.forest2));
|
| 342 |
+
}
|
| 343 |
+
} else {
|
| 344 |
+
// L2~L3 商人涨价
|
| 345 |
+
const d3 = S.ls(3);
|
| 346 |
+
D.merchant(ctx, 330, GY + 210, 1.15, { t, mood: S.ls(3) > 0 && S.ls(4) < 0 ? 'talk' : 'laugh', aR: 2.4, holdR: (c) => D.megaphone(c) });
|
| 347 |
+
const flip = D.clamp01((d3 + 0.1) / 0.6);
|
| 348 |
+
const price = flip > 0.5 ? '20' : '10';
|
| 349 |
+
D.priceSign(ctx, 760, GY + 160, 1.1, price, { t, flip });
|
| 350 |
+
if (d3 > 0.5) { at(ctx, 1030, 220, vis(lt, S.L[3] + 0.5), () => D.stamp(ctx, 0, 0, 1, '涨价!翻倍!', C.red, 0.18)); }
|
| 351 |
+
if (d3 > 0.3 && S.ls(4) < 0) D.speedLines(ctx, 760, 400, t, 'rgba(255,255,255,.4)', 24, 320);
|
| 352 |
+
// L4 全村沸腾 + 隔壁村
|
| 353 |
+
if (S.ls(4) > 0) {
|
| 354 |
+
const d = S.ls(4);
|
| 355 |
+
for (let i = 0; i < 6; i++) D.villager(ctx, i, 640 + i * 120 - 90, GY + 330, 0.8, { t: t + i, mood: 'laugh', aL: 2.8, aR: 2.8, jump: Math.abs(Math.sin(t * 6 + i)) * 30 });
|
| 356 |
+
D.confetti(ctx, 900, 300, d, 40, 700, 51);
|
| 357 |
+
at(ctx, 1150, 470, vis(lt, S.L[4] + 1.6), () => { D.rrect(ctx, -8, 0, 16, 180, 4, 760); D.fs(ctx, '#8a5a35', 4); D.poly(ctx, [[-120, -40], [110, -40], [150, 0], [110, 40], [-120, 40]], 761); D.fs(ctx, '#fff4d6', 5); D.text(ctx, '隔壁村 →', 0, 2, 40, C.ink); });
|
| 358 |
+
for (let i = 0; i < 4; i++) { const x = D.lerp(1900, 1100, (d - 2 - i * 0.3) / 2.5); if (d > 2 + i * .3) D.villager(ctx, i + 2, x, GY + 420, 0.75, { t, walk: t * 12 + i, flip: true, mood: 'laugh', aR: 2.6, holdR: (c) => D.net(c) }); }
|
| 359 |
+
}
|
| 360 |
+
}
|
| 361 |
+
};
|
| 362 |
+
|
| 363 |
+
/* =======================================================
|
| 364 |
+
* 5 二十五元 scarce
|
| 365 |
+
* ======================================================= */
|
| 366 |
+
SC.scarce = function (ctx, lt, S) {
|
| 367 |
+
const t = S.t;
|
| 368 |
+
sky(ctx, '#a9cbe0', '#eef4f2'); clouds(ctx, t);
|
| 369 |
+
forest(ctx, t, GY - 10, D.lerp(0.25, 0.06, D.clamp01(lt / 6)), { scale: 1.15, col: '#6aa36a', col2: '#4f8a55' });
|
| 370 |
+
ground(ctx, '#b5d488', '#93b866');
|
| 371 |
+
// L0-L1 价格牌 20 -> 25
|
| 372 |
+
if (S.ls(2) < 0) {
|
| 373 |
+
const flip = D.clamp01((S.ls(1) - 0.6) / 0.6);
|
| 374 |
+
D.priceSign(ctx, 900, GY + 170, 1.15, flip > 0.5 ? '25' : '20', { t, flip });
|
| 375 |
+
D.merchant(ctx, 470, GY + 210, 1.1, { t, mood: S.ls(1) > 0 ? 'talk' : 'happy', aR: 2.2, holdR: (c) => D.megaphone(c) });
|
| 376 |
+
at(ctx, 640, 180, vis(lt, S.L[0] + 0.4, S.L[1]), () => label(ctx, '猴子更少了……', 0, 0, 52, C.ink, '#fff'));
|
| 377 |
+
if (S.ls(1) > 1.2) at(ctx, 1100, 280, vis(lt, S.L[1] + 1.2), () => D.stamp(ctx, 0, 0, 0.9, '再涨!', C.red, 0.2));
|
| 378 |
+
} else if (S.ls(3) < 0) {
|
| 379 |
+
// L2 望远镜找猴子
|
| 380 |
+
const d = S.ls(2);
|
| 381 |
+
D.villager(ctx, 2, 420, GY + 200, 1.2, { t, mood: 'sad', aR: 2.0, holdR: (c) => { c.save(); c.rotate(-1.2); D.rrect(c, -10, -80, 26, 90, 6, 770); D.fs(c, '#c08a3e', 4); D.rrect(c, -14, -100, 34, 30, 6, 771); D.fs(c, '#8a5a35', 4); c.restore(); } });
|
| 382 |
+
// 望远镜视野
|
| 383 |
+
const vx = 860 + Math.sin(d * 1.3) * 200, vy = 330 + Math.cos(d * 1.7) * 60;
|
| 384 |
+
ctx.save(); ctx.beginPath(); ctx.arc(vx, vy, 150, 0, Math.PI * 2); ctx.lineWidth = 14; ctx.strokeStyle = C.ink; ctx.stroke(); ctx.fillStyle = 'rgba(255,255,255,.12)'; ctx.fill(); ctx.restore();
|
| 385 |
+
at(ctx, vx, vy, 1, () => D.text(ctx, '空', 0, 0, 90, 'rgba(43,35,33,.35)'));
|
| 386 |
+
[0, 1, 2].forEach((i) => D.qmark(ctx, 300 + i * 340, 150, 0.6 * vis(lt, S.L[2] + 0.6 + i * .4), t + i, C.blue));
|
| 387 |
+
} else {
|
| 388 |
+
// L3 抓到一只的人在炫耀
|
| 389 |
+
const d = S.ls(3);
|
| 390 |
+
for (let i = 0; i < 6; i++) { const x = 640 + (i - 2.5) * 210; if (Math.abs(x - 640) > 100) D.villager(ctx, i + 1, x, GY + 240 + (i % 2) * 40, 0.9, { t: t + i, mood: 'shock', aL: 2.5 + Math.sin(t * 8 + i) * .4, aR: 2.5 - Math.sin(t * 8 + i) * .4, flip: x > 640 }); }
|
| 391 |
+
D.villager(ctx, 0, 640, GY + 180, 1.25, { t, mood: 'laugh', aR: 2.9, aL: 0.6, jump: Math.abs(Math.sin(t * 4)) * 16, holdR: (c) => D.monkey(c, 0, 0, 0.75, { t, mood: 'smug', aL: 2.8, aR: 2.8 }) });
|
| 392 |
+
for (let i = 0; i < 6; i++) D.sparkle(ctx, 640 + Math.cos(i + t) * 200, 160 + Math.sin(i * 2 + t) * 80, 26, t + i);
|
| 393 |
+
at(ctx, 640, 120, vis(lt, S.L[3] + 0.4), () => label(ctx, '全村炫耀三天!', 0, 0, 54, C.purple));
|
| 394 |
+
}
|
| 395 |
+
};
|
| 396 |
+
|
| 397 |
+
/* =======================================================
|
| 398 |
+
* 6 五十元 fifty
|
| 399 |
+
* ======================================================= */
|
| 400 |
+
SC.fifty = function (ctx, lt, S) {
|
| 401 |
+
const t = S.t;
|
| 402 |
+
const spot = S.ls(2) < 0;
|
| 403 |
+
if (spot) {
|
| 404 |
+
fillAll(ctx, '#2a2340');
|
| 405 |
+
ctx.save(); const g = ctx.createRadialGradient(640, GY, 50, 640, GY - 200, 600); g.addColorStop(0, 'rgba(255,240,180,.9)'); g.addColorStop(1, 'rgba(255,240,180,0)');
|
| 406 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(560, -900); ctx.lineTo(720, -900); ctx.lineTo(1000, GY + 250); ctx.lineTo(280, GY + 250); ctx.closePath(); ctx.fill(); ctx.restore();
|
| 407 |
+
ctx.fillStyle = '#3a3157'; ctx.fillRect(X0, GY + 200, X1 - X0, Y1);
|
| 408 |
+
D.merchant(ctx, 640, GY + 230, 1.4, { t, mood: S.ls(1) > 0 ? 'sly' : 'happy', aR: S.ls(1) > 0 ? 2.8 : 0.4, aL: S.ls(1) > 0 ? 2.8 : 0.4 });
|
| 409 |
+
at(ctx, 640, 120, vis(lt, S.L[1] + 0.2), () => D.bigText(ctx, '大 招', 0, 0, 120, '#ffcd3c'));
|
| 410 |
+
return;
|
| 411 |
+
}
|
| 412 |
+
rays(ctx, 640, 380, t, '#ffd27a', '#ffb547', 20);
|
| 413 |
+
const d2 = S.ls(2);
|
| 414 |
+
// 巨型 ¥50
|
| 415 |
+
if (S.ls(4) < 0) {
|
| 416 |
+
const drop = E.bounce(D.clamp01(d2 / 0.9));
|
| 417 |
+
D.priceSign(ctx, 640, D.lerp(-600, 600, drop), 1.6, '50', { t, noPost: true, bg: '#fff2b8', head: '最新收购价' });
|
| 418 |
+
D.confetti(ctx, 640, 100, d2 - 0.6, 70, 900, 61);
|
| 419 |
+
if (S.ls(3) > 0) {
|
| 420 |
+
// 村民瞪大眼
|
| 421 |
+
for (let i = 0; i < 5; i++) {
|
| 422 |
+
const x = 140 + i * 250, k = vis(lt, S.L[3] + i * 0.12);
|
| 423 |
+
D.villager(ctx, i, x, GY + 380, 1.05 * k, { t, mood: i % 2 ? 'money' : 'shock', aL: 2.7, aR: 2.7, jump: Math.abs(Math.sin(t * 7 + i)) * 20 });
|
| 424 |
+
}
|
| 425 |
+
at(ctx, 640, 120, vis(lt, S.L[3] + 0.3), () => D.bigText(ctx, '五十块!!', 0, 0, 96, '#fff'));
|
| 426 |
+
}
|
| 427 |
+
} else {
|
| 428 |
+
// L4 交接给助手 / L5 坐车离开
|
| 429 |
+
sky(ctx, '#ffd9a0', '#fff6e0'); clouds(ctx, t); ground(ctx);
|
| 430 |
+
const d5 = S.ls(5);
|
| 431 |
+
// 道路
|
| 432 |
+
ctx.fillStyle = '#c9b48f'; ctx.fillRect(X0, GY + 260, X1 - X0, 120);
|
| 433 |
+
const carX = d5 > 0 ? D.lerp(380, 2100, E.in(D.clamp01((d5 - 0.8) / 2.2))) : 380;
|
| 434 |
+
D.car(ctx, carX, GY + 360, 1.1, d5 > 0.8 ? t : 0, { color: C.red, driver: () => { if (d5 > 0.5) D.merchant(ctx, -10, -60, 0.45, { t, mood: 'laugh', hat: 'top', aR: 2.8 }); } });
|
| 435 |
+
if (d5 > 0.8) D.dust(ctx, carX - 140, GY + 350, t, 6, 9);
|
| 436 |
+
if (d5 < 0.6) D.merchant(ctx, 600, GY + 300, 1.1, { t, mood: 'talk', aR: 1.6 });
|
| 437 |
+
const ak = vis(lt, S.L[4] + 1.6);
|
| 438 |
+
D.assistant(ctx, 900, GY + 300, 1.1 * ak, { t, mood: 'sly', aR: d5 > 1 ? 2.6 + Math.sin(t * 10) * .3 : 0.3 });
|
| 439 |
+
if (ak > 0) at(ctx, 900, GY - 120, vis(lt, S.L[4] + 2.0, S.L[5] + 0.5), () => label(ctx, '助手', 0, 0, 44, C.green));
|
| 440 |
+
if (d5 < 0) at(ctx, 420, 220, vis(lt, S.L[4] + 0.3), () => D.bubble(ctx, 0, 0, 480, 160, 160, 330, { text: ['我进城办点事', '这里交给助手'], size: 40 }));
|
| 441 |
+
if (d5 > 0.9) at(ctx, Math.min(carX, 1150), GY - 40, vis(lt, S.L[5] + 0.9), () => D.bigText(ctx, '拜拜~', 0, 0, 70, '#fff'));
|
| 442 |
+
}
|
| 443 |
+
};
|
| 444 |
+
|
| 445 |
+
/* =======================================================
|
| 446 |
+
* 7 助手的提议 deal
|
| 447 |
+
* ======================================================= */
|
| 448 |
+
SC.deal = function (ctx, lt, S) {
|
| 449 |
+
const t = S.t;
|
| 450 |
+
const g = ctx.createLinearGradient(0, -800, 0, GY); g.addColorStop(0, '#3a2f6b'); g.addColorStop(1, '#c47aa0');
|
| 451 |
+
ctx.fillStyle = g; ctx.fillRect(X0, Y0, X1 - X0, GY - Y0 + 10);
|
| 452 |
+
ctx.beginPath(); ctx.arc(1080, 60, 60, 0, Math.PI * 2); D.fs(ctx, '#fff4c2', 5);
|
| 453 |
+
for (let i = 0; i < 30; i++) { ctx.fillStyle = 'rgba(255,255,255,.7)'; ctx.fillRect(X0 + D.hash(i) * (X1 - X0), -700 + D.hash(i * 3) * 900, 4, 4); }
|
| 454 |
+
ground(ctx, '#6f8f5a', '#5a7748');
|
| 455 |
+
D.cage(ctx, 330, GY + 230, 470, 330, 1, t, { cols: 6, rows: 3, label: '1000 只' });
|
| 456 |
+
// 价格标签
|
| 457 |
+
const tagK = vis(lt, S.L[2] + 1.4);
|
| 458 |
+
at(ctx, 330, GY - 210, tagK, () => { D.rrect(ctx, -110, -50, 220, 100, 18, 801); D.fs(ctx, '#8ff5b5', 6); D.bigText(ctx, '¥35', 0, 2, 78, C.ink, { shadow: null, stroke: 0 }); }, Math.sin(t * 3) * 0.08);
|
| 459 |
+
const lean = S.ls(1) > 0 ? 0.3 : 0;
|
| 460 |
+
D.assistant(ctx, 700, GY + 240, 1.25, { t, mood: S.ls(2) > 0 ? 'talk' : 'sly', aL: 1.2 + lean, aR: S.ls(2) > 0 ? 2.0 + Math.sin(t * 3) * .2 : 0.4 });
|
| 461 |
+
// 村民
|
| 462 |
+
for (let i = 0; i < 4; i++) {
|
| 463 |
+
const mood = S.ls(4) > 1.5 ? 'money' : S.ls(2) > 0 ? 'shock' : 'happy';
|
| 464 |
+
D.villager(ctx, i, 920 + i * 125, GY + 250 + (i % 2) * 40, 0.95, { t: t + i, mood, flip: true, aR: 0.4, jump: mood === 'money' ? Math.abs(Math.sin(t * 6 + i)) * 18 : 0 });
|
| 465 |
+
}
|
| 466 |
+
if (S.ls(1) > 0 && S.ls(2) < 0) at(ctx, 880, 300, vis(lt, S.L[1]), () => D.bubble(ctx, 0, 0, 240, 120, -120, 200, { text: '嘘……', size: 54 }));
|
| 467 |
+
// L3 等老板回来卖 50
|
| 468 |
+
if (S.ls(3) > 0) {
|
| 469 |
+
const k = vis(lt, S.L[3]);
|
| 470 |
+
at(ctx, 1000, 150, k, () => {
|
| 471 |
+
D.thought(ctx, 0, 0, 470, 260, -200, 180);
|
| 472 |
+
D.merchant(ctx, -110, 110, 0.5, { t, mood: 'laugh' });
|
| 473 |
+
D.arrow(ctx, -40, 0, 50, 0, C.ink, 7);
|
| 474 |
+
D.bigText(ctx, '¥50', 140, 0, 74, C.red, { shadow: null });
|
| 475 |
+
});
|
| 476 |
+
}
|
| 477 |
+
// L4 净赚 15
|
| 478 |
+
if (S.ls(4) > 0) {
|
| 479 |
+
const k = vis(lt, S.L[4] + 0.2);
|
| 480 |
+
at(ctx, 640, 470, k, () => { card(ctx, 0, 0, 640, 170, '#fff8d6', 810); D.bigText(ctx, '50 − 35 = +15', 0, 0, 84, C.green, { shadow: null }); });
|
| 481 |
+
for (let i = 0; i < 7; i++) D.sparkle(ctx, 360 + i * 95, 360 + Math.sin(t * 3 + i) * 20, 18, t + i);
|
| 482 |
+
if (S.ls(4) > 3) at(ctx, 700, GY - 200, vis(lt, S.L[4] + 3), () => D.bubble(ctx, 0, 0, 380, 110, -10, 140, { text: '只告诉你们哦', size: 42, fill: '#efffef' }));
|
| 483 |
+
}
|
| 484 |
+
};
|
| 485 |
+
|
| 486 |
+
/* =======================================================
|
| 487 |
+
* 8 全村抢购 frenzy
|
| 488 |
+
* ======================================================= */
|
| 489 |
+
SC.frenzy = function (ctx, lt, S) {
|
| 490 |
+
const t = S.t;
|
| 491 |
+
if (S.ls(1) < 0) {
|
| 492 |
+
rays(ctx, 640, 420, t, '#c9f5d3', '#a7ecb9', 20);
|
| 493 |
+
for (let i = 0; i < 26; i++) { const k = ((t * 0.5 + D.hash(i)) % 1); D.coin(ctx, X0 + D.hash(i * 3) * (X1 - X0), -800 + k * 2700, 24, t * 5 + i); }
|
| 494 |
+
for (let i = 0; i < 3; i++) D.villager(ctx, i, 360 + i * 280, GY + 160, 1.25, { t: t + i, mood: 'money', aL: 2.8, aR: 2.8, jump: Math.abs(Math.sin(t * 5 + i)) * 26 });
|
| 495 |
+
at(ctx, 640, 230, vis(lt, S.L[0] + 1.5), () => D.bigText(ctx, '这简直是捡钱!', 0, 0, 104, C.green, { rot: -0.05 }));
|
| 496 |
+
return;
|
| 497 |
+
}
|
| 498 |
+
sky(ctx, '#ffd29a', '#fff3dc'); clouds(ctx, t); ground(ctx, '#c9d98a', '#a9bf6a');
|
| 499 |
+
if (S.ls(3) < 0) {
|
| 500 |
+
// L1 砸存钱罐 / L2 借钱卖牛
|
| 501 |
+
const pb = S.ls(1);
|
| 502 |
+
const broken = D.clamp01((pb - 1.2) / 0.9);
|
| 503 |
+
at(ctx, 330, GY + 120, 1, () => D.piggy(ctx, 0, 0, 1.3, broken));
|
| 504 |
+
if (pb > 0.4 && pb < 1.4) at(ctx, 330, GY - 100, 1, () => { ctx.rotate(-0.6 + Math.sin(D.clamp01((pb - 0.4) / 0.8) * Math.PI) * 0.9); D.rrect(ctx, -12, -120, 24, 140, 6, 820); D.fs(ctx, '#8a5a35', 4); D.rrect(ctx, -50, -150, 100, 50, 8, 821); D.fs(ctx, '#888', 4); });
|
| 505 |
+
D.coinBurst(ctx, 330, GY, pb - 1.3, 16, 71, 1);
|
| 506 |
+
at(ctx, 330, 180, vis(lt, S.L[1] + 0.3), () => label(ctx, '掏空积蓄', 0, 0, 52, C.pink));
|
| 507 |
+
if (S.ls(2) > 0) {
|
| 508 |
+
at(ctx, 760, 330, vis(lt, S.L[2] + 0.1), () => { card(ctx, 0, 0, 260, 300, '#fff', 830); D.text(ctx, '借 条', 0, -100, 46, C.ink); for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.moveTo(-90, -40 + i * 40); ctx.lineTo(90, -40 + i * 40); D.stroke(ctx, 3, '#bbb'); } D.text(ctx, '¥¥¥', 0, 110, 40, C.red); }, -0.08);
|
| 509 |
+
D.cow(ctx, 1040, GY + 200, 1.1 * vis(lt, S.L[2] + 1.7), t, { tag: '卖牛' });
|
| 510 |
+
at(ctx, 950, 180, vis(lt, S.L[2] + 1.9), () => label(ctx, '借钱 · 卖牛', 0, 0, 50, C.red));
|
| 511 |
+
}
|
| 512 |
+
} else {
|
| 513 |
+
// L3 长队 / L4 抢空
|
| 514 |
+
const d = S.ls(3);
|
| 515 |
+
const fillK = S.ls(4) > 0 ? 1 - D.clamp01(S.ls(4) / 3.6) : 1;
|
| 516 |
+
D.cage(ctx, 220, GY + 220, 400, 300, fillK, t, { cols: 5, rows: 3, label: S.ls(4) > 3.6 ? '空了' : '¥35 一只' });
|
| 517 |
+
D.assistant(ctx, 500, GY + 230, 1.0, { t, mood: 'laugh', aR: 1.4 + Math.sin(t * 8) * .4 });
|
| 518 |
+
const qn = 9, off = (d * 60) % 150;
|
| 519 |
+
for (let i = 0; i < qn; i++) {
|
| 520 |
+
const x = 640 + i * 150 - off;
|
| 521 |
+
D.villager(ctx, i, x, GY + 250, 0.85, { t: t + i, walk: t * 10 + i, flip: true, mood: i % 3 ? 'money' : 'shock', aR: 2.6, holdR: S.ls(4) > 0 && i % 2 ? (c) => D.monkey(c, 0, 0, .35, { t, mood: 'smug' }) : null });
|
| 522 |
+
}
|
| 523 |
+
at(ctx, 960, 180, vis(lt, S.L[3] + 0.3, S.L[4]), () => label(ctx, '排队抢购中……', 0, 0, 50, C.ink, '#ffcd3c'));
|
| 524 |
+
if (S.ls(4) > 0) at(ctx, 640, 230, vis(lt, S.L[4] + 3.4), () => D.stamp(ctx, 0, 0, 1.4, '一抢而空', C.red, -0.15));
|
| 525 |
+
}
|
| 526 |
+
};
|
| 527 |
+
|
| 528 |
+
/* =======================================================
|
| 529 |
+
* 9 人去楼空 gone
|
| 530 |
+
* ======================================================= */
|
| 531 |
+
SC.gone = function (ctx, lt, S) {
|
| 532 |
+
const t = S.t;
|
| 533 |
+
sky(ctx, '#7c8ea3', '#c9d2da'); clouds(ctx, t * .5); mountains(ctx, '#a5b3ad', '#93a39b'); ground(ctx, '#8fa37a', '#7a8f66');
|
| 534 |
+
houses(ctx, t, false);
|
| 535 |
+
if (S.ls(2) < 0) {
|
| 536 |
+
// 空荡荡的收购点
|
| 537 |
+
ctx.fillStyle = '#b6a585'; ctx.fillRect(X0, GY + 250, X1 - X0, 110);
|
| 538 |
+
at(ctx, 360, GY + 230, 1, () => { D.rrect(ctx, -110, -110, 220, 20, 4, 840); D.fs(ctx, '#8a5a35', 4); D.rrect(ctx, -100, -90, 14, 90, 4, 841); D.fs(ctx, '#8a5a35', 4); D.rrect(ctx, 86, -90, 14, 90, 4, 842); D.fs(ctx, '#8a5a35', 4); });
|
| 539 |
+
at(ctx, 360, GY - 30, 1, () => { D.rrect(ctx, -6, 0, 12, 160, 3, 843); D.fs(ctx, '#8a5a35', 4); D.rrect(ctx, -130, -70, 260, 80, 10, 844); D.fs(ctx, '#e8dcc0', 4); D.text(ctx, '收猴处', 0, -30, 44, '#777'); }, 0.25);
|
| 540 |
+
const p = S.ls(0);
|
| 541 |
+
if (p < 1.5) for (let i = 0; i < 6; i++) { const k = D.clamp01(p / 1.2); ctx.globalAlpha = 1 - k; D.blob(ctx, 360 + Math.cos(i) * k * 120, GY + 40 + Math.sin(i * 2) * k * 80, 40 + k * 30, 30 + k * 20, 850 + i); D.fs(ctx, '#fff', 4); ctx.globalAlpha = 1; }
|
| 542 |
+
if (p < 0.4) D.assistant(ctx, 360, GY + 230, 1.0, { t, mood: 'sly' });
|
| 543 |
+
at(ctx, 360, 230, vis(lt, S.L[0] + 0.8), () => D.bigText(ctx, '人呢?', 0, 0, 80, '#fff'));
|
| 544 |
+
// 村民张望
|
| 545 |
+
for (let i = 0; i < 3; i++) D.villager(ctx, i, 820 + i * 150, GY + 260, 0.95, { t, mood: S.ls(1) > 0 ? 'sad' : 'shock', aR: S.ls(1) > 0 ? 2.2 : 0.3, holdL: (c) => D.monkey(c, 0, 20, .35, { t, mood: 'sad' }) });
|
| 546 |
+
if (S.ls(1) > 0) {
|
| 547 |
+
D.tumbleweed(ctx, D.lerp(-200, 1500, S.ls(1) / 3.4), GY + 290 - Math.abs(Math.sin(S.ls(1) * 5)) * 40, 1, S.ls(1));
|
| 548 |
+
at(ctx, 1000, 230, vis(lt, S.L[1] + 0.6), () => D.thought(ctx, 0, 0, 380, 160, -40, 140, { text: '老板呢……?', size: 44 }));
|
| 549 |
+
}
|
| 550 |
+
} else {
|
| 551 |
+
// 日历翻页 → 满村猴子
|
| 552 |
+
if (S.ls(3) < 0) {
|
| 553 |
+
const d = S.ls(2);
|
| 554 |
+
const labels = ['1周', '1个月', '1年'];
|
| 555 |
+
const idx = Math.min(2, Math.floor(d / 1.15));
|
| 556 |
+
const fl = (d % 1.15) / 1.15;
|
| 557 |
+
D.calendar(ctx, 640, 420, 1.5, labels[idx], '过去了', idx < 2 && fl > 0.75 ? (fl - 0.75) * 4 : 0);
|
| 558 |
+
for (let i = 0; i < 4; i++) D.monkey(ctx, 200 + i * 300, GY + 240, 0.7, { t, seed: i, mood: 'smug', flip: i % 2 });
|
| 559 |
+
} else {
|
| 560 |
+
// 雨 + 蟋蟀 + 满山猴子
|
| 561 |
+
for (let i = 0; i < 18; i++) { const sp = i % 6; const x = -500 + i * 140, y = GY - 120 + (sp % 3) * 80; D.monkey(ctx, x, y + Math.sin(t * 3 + i) * 6, 0.5, { t, seed: i + 30, mood: 'smug', flip: i % 2, aL: 2.5 + Math.sin(t * 3 + i) * .4 }); }
|
| 562 |
+
for (let i = 0; i < 4; i++) D.villager(ctx, i, 230 + i * 270, GY + 300, 1.0, { t, mood: 'sad', aR: 0.9, holdR: (c) => D.monkey(c, 0, 30, .4, { t, mood: 'smug' }) });
|
| 563 |
+
D.cricket(ctx, 1120, GY + 420, 0.8, t);
|
| 564 |
+
at(ctx, 640, 120, vis(lt, S.L[3] + 1.5), () => label(ctx, '只剩下:满山的猴子', 0, 0, 52, '#55606b'));
|
| 565 |
+
}
|
| 566 |
+
// 雨
|
| 567 |
+
ctx.save(); ctx.strokeStyle = 'rgba(220,235,255,.55)'; ctx.lineWidth = 3;
|
| 568 |
+
for (let i = 0; i < 90; i++) { const x = X0 + D.hash(i * 1.3) * (X1 - X0), y = Y0 + ((D.hash(i * 7.1) * 3000 + t * 1300) % 3000); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - 12, y + 42); ctx.stroke(); }
|
| 569 |
+
ctx.restore();
|
| 570 |
+
}
|
| 571 |
+
desat(ctx, 0.55);
|
| 572 |
+
};
|
| 573 |
+
|
| 574 |
+
/* =======================================================
|
| 575 |
+
* 10 算账 ledger
|
| 576 |
+
* ======================================================= */
|
| 577 |
+
SC.ledger = function (ctx, lt, S) {
|
| 578 |
+
const t = S.t;
|
| 579 |
+
paper(ctx, '#f7efdc');
|
| 580 |
+
const bookOut = S.ls(6) > 0 ? E.inout(S.ls(6) / 0.6) : 0;
|
| 581 |
+
if (bookOut < 1) {
|
| 582 |
+
ctx.save(); ctx.translate(0, bookOut * 1400);
|
| 583 |
+
D.ledgerBook(ctx, 90, 20, 1100, 920);
|
| 584 |
+
D.text(ctx, '收猴子 · 账本', 640, 72, 50, C.ink);
|
| 585 |
+
const rows = [
|
| 586 |
+
[S.L[1] + 1.4, '收购', '10元 × 600只', '6,000', C.ink],
|
| 587 |
+
[S.L[1] + 3.2, '收购', '20元 × 300只', '6,000', C.ink],
|
| 588 |
+
[S.L[1] + 5.0, '收购', '25元 × 100只', '2,500', C.ink],
|
| 589 |
+
];
|
| 590 |
+
rows.forEach(([a, k, f, v, c], i) => {
|
| 591 |
+
const y = 170 + i * 72, p = fade(lt, a, null, 0.25);
|
| 592 |
+
if (p <= 0) return;
|
| 593 |
+
ctx.globalAlpha = p;
|
| 594 |
+
D.text(ctx, k, 180, y, 40, C.blue);
|
| 595 |
+
const typed = Math.floor(D.clamp01((lt - a) / 0.7) * f.length);
|
| 596 |
+
D.text(ctx, f.slice(0, typed), 300, y, 44, c, { align: 'left' });
|
| 597 |
+
D.text(ctx, v, 1110, y, 46, c, { align: 'right' });
|
| 598 |
+
ctx.globalAlpha = 1;
|
| 599 |
+
});
|
| 600 |
+
if (lt > S.L[2]) {
|
| 601 |
+
const p = D.clamp01(S.ls(2) / 0.5);
|
| 602 |
+
ctx.beginPath(); ctx.moveTo(160, 395); ctx.lineTo(160 + 960 * p, 395); D.stroke(ctx, 4);
|
| 603 |
+
D.text(ctx, '商人收购总成本', 180, 450, 44, C.ink, { align: 'left', alpha: p });
|
| 604 |
+
at(ctx, 1110, 450, vis(lt, S.L[2] + 0.4), () => D.text(ctx, '¥14,500', 0, 0, 56, C.blue, { align: 'right' }));
|
| 605 |
+
}
|
| 606 |
+
if (lt > S.L[3]) {
|
| 607 |
+
const p = fade(lt, S.L[3] + 0.6);
|
| 608 |
+
D.text(ctx, '卖出', 180, 540, 40, C.purple, { alpha: p });
|
| 609 |
+
const f = '35元 × 1000只'; D.text(ctx, f.slice(0, Math.floor(D.clamp01((S.ls(3) - 0.6) / 0.8) * f.length)), 300, 540, 44, C.ink, { align: 'left' });
|
| 610 |
+
at(ctx, 1110, 540, vis(lt, S.L[3] + 4.6), () => D.text(ctx, '¥35,000', 0, 0, 56, C.purple, { align: 'right' }));
|
| 611 |
+
}
|
| 612 |
+
if (lt > S.L[4]) {
|
| 613 |
+
const k = vis(lt, S.L[4] + 0.3);
|
| 614 |
+
at(ctx, 640, 660, k, () => { D.rrect(ctx, -460, -58, 920, 116, 24, 901); D.fs(ctx, '#e3f9e9', 5); D.text(ctx, '商人净赚', -280, 2, 50, C.ink); D.bigText(ctx, '+¥20,500', 140, 2, 84, C.green, { shadow: null }); });
|
| 615 |
+
D.coinBurst(ctx, 900, 640, S.ls(4) - 0.6, 14, 91, 0.8);
|
| 616 |
+
}
|
| 617 |
+
if (lt > S.L[5]) {
|
| 618 |
+
const k = vis(lt, S.L[5] + 0.4);
|
| 619 |
+
at(ctx, 640, 820, k, () => { D.rrect(ctx, -460, -58, 920, 116, 24, 902); D.fs(ctx, '#ffe4e1', 5); D.text(ctx, '村民净亏', -280, 2, 50, C.ink); D.bigText(ctx, '−¥20,500', 140, 2, 84, C.red, { shadow: null }); });
|
| 620 |
+
if (S.ls(5) > 4) at(ctx, 1000, 560, vis(lt, S.L[5] + 4), () => D.stamp(ctx, 0, 0, 0.75, '换回1000只猴子', C.fur, -0.12));
|
| 621 |
+
}
|
| 622 |
+
ctx.restore();
|
| 623 |
+
// L0 标题
|
| 624 |
+
if (S.ls(1) < 0) at(ctx, 640, 480, vis(lt, S.L[0], S.L[1] - .1), () => D.bigText(ctx, '算一笔账', 0, 0, 130, C.gold));
|
| 625 |
+
}
|
| 626 |
+
if (bookOut > 0) {
|
| 627 |
+
// 钱从村民口袋搬进商人口袋
|
| 628 |
+
const d = S.ls(6);
|
| 629 |
+
at(ctx, 0, (1 - bookOut) * -1200, 1, () => {
|
| 630 |
+
D.villager(ctx, 0, 240, GY + 120, 1.3, { t, mood: 'sad', aR: 0.4 });
|
| 631 |
+
D.merchant(ctx, 1040, GY + 120, 1.3, { t, mood: 'laugh', aL: 2.6 });
|
| 632 |
+
for (let i = 0; i < 9; i++) { const ph = (d * 0.6 + i / 9) % 1; const x = D.lerp(300, 980, ph), y = GY - 260 - Math.sin(ph * Math.PI) * 300; D.coin(ctx, x, y, 26, t * 6 + i); }
|
| 633 |
+
D.cage(ctx, 640, GY + 160, 330, 200, 1, t, { cols: 5, rows: 2, mood: 'smug' });
|
| 634 |
+
at(ctx, 640, 150, vis(lt, S.L[6] + 0.6), () => label(ctx, '猴子还是那些猴子', 0, 0, 54, C.ink, '#ffcd3c'));
|
| 635 |
+
D.text(ctx, '村民口袋', 240, GY + 190, 40, C.ink); D.text(ctx, '商人口袋', 1040, GY + 190, 40, C.ink);
|
| 636 |
+
});
|
| 637 |
+
}
|
| 638 |
+
};
|
| 639 |
+
|
| 640 |
+
/* =======================================================
|
| 641 |
+
* 11 为什么会上当 why
|
| 642 |
+
* ======================================================= */
|
| 643 |
+
SC.why = function (ctx, lt, S) {
|
| 644 |
+
const t = S.t;
|
| 645 |
+
paper(ctx, '#eef3fb', 'rgba(74,143,226,.12)');
|
| 646 |
+
if (S.ls(1) < 0) {
|
| 647 |
+
D.villager(ctx, 3, 640, GY + 160, 1.5, { t, mood: 'sad', aR: 2.9 });
|
| 648 |
+
[0, 1, 2].forEach((i) => D.qmark(ctx, 420 + i * 220, 200 + (i % 2) * 40, 0.8 * vis(lt, S.L[0] + 0.3 + i * .3), t + i, [C.red, C.blue, C.purple][i]));
|
| 649 |
+
at(ctx, 640, 420, vis(lt, S.L[0] + 1.6), () => D.bigText(ctx, '为什么会上当?', 0, 0, 90, C.ink, { shadow: null, stroke: 0 }));
|
| 650 |
+
return;
|
| 651 |
+
}
|
| 652 |
+
// 进度圆点
|
| 653 |
+
const step = S.ls(4) > 0 ? 3 : S.ls(3) > 0 ? 3 : S.ls(2) > 0 ? 2 : 1;
|
| 654 |
+
[1, 2, 3].forEach((n, i) => { D.blob(ctx, 520 + i * 120, 50, 34, 34, 910 + i); D.fs(ctx, n <= step ? C.red : '#ddd', 5); D.text(ctx, String(n), 520 + i * 120, 52, 40, '#fff'); });
|
| 655 |
+
if (S.ls(4) < 0) {
|
| 656 |
+
const cards = [
|
| 657 |
+
{ i: 1, title: '价格是"造"出来的', sub: '只有一个买家', a: S.L[1], b: S.L[2] },
|
| 658 |
+
{ i: 2, title: '真实的赚钱效应', sub: '前面的人真赚到了', a: S.L[2], b: S.L[3] },
|
| 659 |
+
{ i: 3, title: '相信有人接盘', sub: '不是因为猴子有用', a: S.L[3], b: S.L[4] },
|
| 660 |
+
];
|
| 661 |
+
cards.forEach((c) => {
|
| 662 |
+
if (lt < c.a - 0.1 || lt > c.b + 0.5) return;
|
| 663 |
+
const kin = E.out((lt - c.a) / 0.5), kout = E.in((lt - c.b) / 0.45);
|
| 664 |
+
const x = 640 + (1 - kin) * 1500 - kout * 1600;
|
| 665 |
+
ctx.save(); ctx.translate(x - 640, 0);
|
| 666 |
+
card(ctx, 640, 500, 1060, 760, '#fff', 920 + c.i);
|
| 667 |
+
D.blob(ctx, 230, 190, 50, 50, 930 + c.i); D.fs(ctx, C.red, 5); D.text(ctx, String(c.i), 230, 193, 60, '#fff');
|
| 668 |
+
D.text(ctx, c.title, 300, 180, 62, C.ink, { align: 'left' });
|
| 669 |
+
D.text(ctx, c.sub, 300, 250, 40, '#7a6a5a', { align: 'left' });
|
| 670 |
+
const d = lt - c.a;
|
| 671 |
+
if (c.i === 1) {
|
| 672 |
+
// 需求曲线:唯一买家离开 → 归零
|
| 673 |
+
ctx.save(); ctx.translate(200, 820);
|
| 674 |
+
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(880, 0); ctx.moveTo(0, 0); ctx.lineTo(0, -480); D.stroke(ctx, 5);
|
| 675 |
+
D.text(ctx, '价格', -10, -510, 32, C.ink); D.text(ctx, '时间', 880, 36, 32, C.ink);
|
| 676 |
+
const pts = [[0, 10], [150, 20], [300, 25], [450, 50], [600, 35], [700, 0], [860, 0]];
|
| 677 |
+
const prog = D.clamp01(d / 5);
|
| 678 |
+
ctx.beginPath();
|
| 679 |
+
const maxX = 860 * prog;
|
| 680 |
+
pts.forEach((p, j) => { if (p[0] <= maxX) { const y = -p[1] * 8; j ? ctx.lineTo(p[0], y) : ctx.moveTo(p[0], y); } });
|
| 681 |
+
D.stroke(ctx, 9, C.red);
|
| 682 |
+
pts.slice(0, 5).forEach((p) => { if (p[0] <= maxX) D.text(ctx, '¥' + p[1], p[0], -p[1] * 8 - 34, 32, C.ink); });
|
| 683 |
+
if (prog > 0.82) { D.text(ctx, '商人走了 → 需求 = 0', 640, -120, 40, C.red); }
|
| 684 |
+
D.merchant(ctx, 520, -40, 0.45 * (prog > 0.75 ? Math.max(0, 1 - (prog - .75) * 5) : 1), { t, mood: 'laugh' });
|
| 685 |
+
ctx.restore();
|
| 686 |
+
} else if (c.i === 2) {
|
| 687 |
+
D.villager(ctx, 1, 420, 820, 1.2, { t, mood: 'money', aL: 2.7, aR: 2.7, jump: Math.abs(Math.sin(t * 5)) * 20 });
|
| 688 |
+
D.coinBurst(ctx, 420, 520, (d % 1.6) - 0.1, 10, 101 + Math.floor(d / 1.6), 0.8);
|
| 689 |
+
D.villager(ctx, 4, 860, 820, 1.1, { t, mood: 'shock', flip: true });
|
| 690 |
+
at(ctx, 880, 400, vis(lt, c.a + 1.5), () => D.thought(ctx, 0, 0, 340, 150, -40, 120, { text: ['他都赚了', '我也能!'], size: 38 }));
|
| 691 |
+
} else {
|
| 692 |
+
D.villager(ctx, 5, 420, 820, 1.1, { t, mood: 'money', aR: 2.4, holdR: (cx) => D.monkey(cx, 0, 0, .5, { t, mood: 'smug' }) });
|
| 693 |
+
D.arrow(ctx, 560, 560, 800, 560, C.ink, 8, D.clamp01((d - 1) / 0.8));
|
| 694 |
+
at(ctx, 900, 560, vis(lt, c.a + 1.8), () => { D.text(ctx, '更高价', 0, -60, 40, C.red); D.qmark(ctx, 0, 40, 0.8, t); });
|
| 695 |
+
at(ctx, 640, 380, vis(lt, c.a + 3), () => D.text(ctx, '猴子有没有用?不重要', 0, 0, 44, '#7a6a5a'));
|
| 696 |
+
}
|
| 697 |
+
ctx.restore();
|
| 698 |
+
});
|
| 699 |
+
} else {
|
| 700 |
+
// L4~L5 博傻链条
|
| 701 |
+
const d = S.ls(4);
|
| 702 |
+
D.text(ctx, '博 傻 理 论', 640, 160, 90, C.ink);
|
| 703 |
+
D.text(ctx, 'Greater Fool Theory', 640, 236, 36, '#7a6a5a');
|
| 704 |
+
const people = [[200, '¥35'], [480, '¥50'], [760, '¥80?'], [1040, '你']];
|
| 705 |
+
const last = S.ls(5) > 0;
|
| 706 |
+
people.forEach(([x, tag], i) => {
|
| 707 |
+
const k = vis(lt, S.L[4] + 0.4 + i * 0.7);
|
| 708 |
+
const isYou = i === 3;
|
| 709 |
+
if (last && !isYou) ctx.globalAlpha = Math.max(0.25, 1 - S.ls(5));
|
| 710 |
+
if (isYou && last) { ctx.save(); const g = ctx.createRadialGradient(x, 600, 20, x, 600, 360); g.addColorStop(0, 'rgba(255,230,120,.7)'); g.addColorStop(1, 'rgba(255,230,120,0)'); ctx.fillStyle = g; ctx.fillRect(x - 400, 200, 800, 800); ctx.restore(); }
|
| 711 |
+
D.villager(ctx, i + 1, x, GY + 40, (0.8 + i * 0.12) * k, { t: t + i, mood: isYou && last ? 'shock' : 'money', aR: 1.6 });
|
| 712 |
+
at(ctx, x, GY + 90, k, () => label(ctx, tag, 0, 0, 38, isYou ? C.red : C.ink, '#fff'));
|
| 713 |
+
if (i < 3) D.arrow(ctx, x + 80, 420, x + 200, 420, C.ink, 6, D.clamp01((lt - S.L[4] - 0.9 - i * 0.7) / 0.4));
|
| 714 |
+
ctx.globalAlpha = 1;
|
| 715 |
+
});
|
| 716 |
+
// 猴子沿链条传递
|
| 717 |
+
const pass = D.clamp01((d - 0.6) / 3.2) * 3;
|
| 718 |
+
const pi = Math.min(3, pass);
|
| 719 |
+
const mx = 200 + pi * 280, my = 400 - Math.abs(Math.sin(pass * Math.PI)) * 80;
|
| 720 |
+
D.monkey(ctx, mx, my, 0.6, { t, mood: 'smug' });
|
| 721 |
+
if (last) at(ctx, 640, 320, vis(lt, S.L[5] + 0.6), () => { D.rrect(ctx, -420, -50, 840, 100, 26, 940); D.fs(ctx, C.red, 5); D.text(ctx, '最后一个傻子 = 你自己', 0, 2, 52, '#fff'); });
|
| 722 |
+
}
|
| 723 |
+
};
|
| 724 |
+
|
| 725 |
+
/* =======================================================
|
| 726 |
+
* 12 现实中的猴子 real
|
| 727 |
+
* ======================================================= */
|
| 728 |
+
SC.real = function (ctx, lt, S) {
|
| 729 |
+
const t = S.t;
|
| 730 |
+
paper(ctx, '#fff6e6');
|
| 731 |
+
if (S.ls(2) < 0) {
|
| 732 |
+
const book = vis(lt, S.L[0]);
|
| 733 |
+
at(ctx, 640, 470, book, () => { card(ctx, 0, 0, 620, 420, '#ffe9b0', 950); D.text(ctx, '寓言', 0, -100, 90, C.fur); D.monkey(ctx, 0, 160, 1.0, { t, mood: 'smug', aL: 2.2, aR: 2.2 }); });
|
| 734 |
+
if (S.ls(1) > 0) {
|
| 735 |
+
at(ctx, 640, 150, vis(lt, S.L[1] + 0.2), () => D.bigText(ctx, '真实上演了几百年', 0, 0, 72, C.red, { rot: -0.03 }));
|
| 736 |
+
at(ctx, 1000, 640, vis(lt, S.L[1] + 1.4), () => D.stamp(ctx, 0, 0, 1, '真事!', C.red, 0.2));
|
| 737 |
+
}
|
| 738 |
+
return;
|
| 739 |
+
}
|
| 740 |
+
// 时间线
|
| 741 |
+
ctx.beginPath(); ctx.moveTo(60, 860); ctx.lineTo(1220, 860); D.stroke(ctx, 6);
|
| 742 |
+
[[200, '1637', S.L[2]], [640, '2000', S.L[3]], [1080, '今天', S.L[4]]].forEach(([x, y, a]) => {
|
| 743 |
+
const k = vis(lt, a);
|
| 744 |
+
at(ctx, x, 860, k, () => { D.blob(ctx, 0, 0, 20, 20, 960); D.fs(ctx, C.red, 4); D.text(ctx, y, 0, 50, 40, C.ink); });
|
| 745 |
+
});
|
| 746 |
+
if (S.ls(5) < 0) {
|
| 747 |
+
// 当前展示的卡片
|
| 748 |
+
const which = S.ls(4) > 0 ? 2 : S.ls(3) > 0 ? 1 : 0;
|
| 749 |
+
const a = [S.L[2], S.L[3], S.L[4]][which];
|
| 750 |
+
const k = vis(lt, a, null, 0.5);
|
| 751 |
+
at(ctx, 640, 430, k, () => {
|
| 752 |
+
card(ctx, 0, 0, 1080, 640, '#fff', 970 + which);
|
| 753 |
+
if (which === 0) {
|
| 754 |
+
D.text(ctx, '荷兰 · 郁金香狂热', 0, -250, 54, C.ink);
|
| 755 |
+
D.tulip(ctx, -280, 220, 1.6, t);
|
| 756 |
+
D.bigText(ctx, '=', 0, 30, 110, C.ink, { shadow: null, stroke: 0 });
|
| 757 |
+
D.house(ctx, 280, 200, 1.2, 971, '#f6e3c3', '#d9534f');
|
| 758 |
+
D.text(ctx, '一颗球茎 ≈ 一栋房子(据说)', 0, 270, 38, '#7a6a5a');
|
| 759 |
+
} else if (which === 1) {
|
| 760 |
+
D.text(ctx, '互联网泡沫', 0, -250, 54, C.ink);
|
| 761 |
+
D.laptop(ctx, -260, 120, 1.4, t);
|
| 762 |
+
// 走势:暴涨后暴跌
|
| 763 |
+
ctx.save(); ctx.translate(60, 170);
|
| 764 |
+
const pts = [[0, 0], [60, -40], [120, -60], [180, -150], [240, -290], [290, -330], [330, -120], [380, -40], [420, -20]];
|
| 765 |
+
const prog = D.clamp01((lt - a - 0.4) / 3.5);
|
| 766 |
+
ctx.beginPath(); pts.forEach((p, j) => { if (j / (pts.length - 1) <= prog) j ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]); }); D.stroke(ctx, 10, prog > 0.7 ? C.red : C.green);
|
| 767 |
+
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(440, 0); ctx.moveTo(0, 0); ctx.lineTo(0, -360); D.stroke(ctx, 4);
|
| 768 |
+
ctx.restore();
|
| 769 |
+
D.text(ctx, '名字加个“.com”,股价就大涨', 0, 270, 38, '#7a6a5a');
|
| 770 |
+
} else {
|
| 771 |
+
D.text(ctx, '空气币 · 炒鞋 · 炒卡', 0, -250, 54, C.ink);
|
| 772 |
+
const items = [(c) => D.cryptoCoin(c, 0, 0, 1.2, t), (c) => D.sneaker(c, 0, 20, 1.2), (c) => D.card(c, 0, 0, 1.1)];
|
| 773 |
+
items.forEach((fn, i) => {
|
| 774 |
+
const kk = vis(lt, a + 0.3 + i * 0.6);
|
| 775 |
+
at(ctx, -320 + i * 320, 40, kk, () => {
|
| 776 |
+
// 每样东西后面都探出一条猴子尾巴
|
| 777 |
+
ctx.beginPath(); ctx.moveTo(60, 40); ctx.bezierCurveTo(140, 60, 150, -60, 110, -80 + Math.sin(t * 4 + i) * 8); D.stroke(ctx, 14); ctx.beginPath(); ctx.moveTo(60, 40); ctx.bezierCurveTo(140, 60, 150, -60, 110, -80 + Math.sin(t * 4 + i) * 8); D.stroke(ctx, 7, C.fur);
|
| 778 |
+
fn(ctx);
|
| 779 |
+
});
|
| 780 |
+
});
|
| 781 |
+
D.text(ctx, '被包装成“稀缺资产”', 0, 270, 38, '#7a6a5a');
|
| 782 |
+
}
|
| 783 |
+
});
|
| 784 |
+
} else {
|
| 785 |
+
// L5 收猴子剧本循环
|
| 786 |
+
const steps = ['制造需求', '抬高价格', '让你接盘', '庄家离场'];
|
| 787 |
+
const cx = 640, cy = 440, R = 300;
|
| 788 |
+
ctx.save(); ctx.globalAlpha = 0.9; ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.setLineDash([18, 16]); D.stroke(ctx, 6, '#c9b9a5'); ctx.restore();
|
| 789 |
+
steps.forEach((s, i) => {
|
| 790 |
+
const a = -Math.PI / 2 + i * Math.PI / 2;
|
| 791 |
+
const k = vis(lt, S.L[5] + 0.6 + i * 0.9);
|
| 792 |
+
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
|
| 793 |
+
at(ctx, x, y, k, () => { D.rrect(ctx, -150, -46, 300, 92, 30, 980 + i); D.fs(ctx, [C.blue, C.gold, C.red, C.ink][i], 5); D.text(ctx, s, 0, 2, 48, '#fff'); });
|
| 794 |
+
});
|
| 795 |
+
const ang = -Math.PI / 2 + (lt - S.L[5]) * 1.2;
|
| 796 |
+
D.monkey(ctx, cx + Math.cos(ang) * (R - 110), cy + Math.sin(ang) * (R - 110) + 60, 0.5, { t, mood: 'smug' });
|
| 797 |
+
D.text(ctx, '剧本', cx, cy - 10, 60, C.ink);
|
| 798 |
+
D.text(ctx, '从没变过', cx, cy + 56, 40, '#7a6a5a');
|
| 799 |
+
}
|
| 800 |
+
};
|
| 801 |
+
|
| 802 |
+
/* =======================================================
|
| 803 |
+
* 13 识别猴子三问 check
|
| 804 |
+
* ======================================================= */
|
| 805 |
+
SC.check = function (ctx, lt, S) {
|
| 806 |
+
const t = S.t;
|
| 807 |
+
rays(ctx, 640, 480, t * .5, '#e3f4ff', '#d3ecff', 16);
|
| 808 |
+
if (S.ls(1) < 0) {
|
| 809 |
+
D.monkey(ctx, 640, GY + 80, 2.2 * vis(lt, S.L[0]), { t, mood: 'shock', aL: 2.4, aR: 2.4 });
|
| 810 |
+
D.at(ctx, 640, 260, vis(lt, S.L[0] + 0.6), () => { ctx.beginPath(); ctx.arc(0, 0, 130, 0, Math.PI * 2); D.stroke(ctx, 18); ctx.beginPath(); ctx.moveTo(90, 90); ctx.lineTo(190, 190); D.stroke(ctx, 26, '#8a5a35'); D.text(ctx, '?', 0, 0, 120, C.blue); }, -0.2);
|
| 811 |
+
return;
|
| 812 |
+
}
|
| 813 |
+
// 写字板
|
| 814 |
+
at(ctx, 640, 500, vis(lt, S.L[1] - 0.1), () => {
|
| 815 |
+
D.rrect(ctx, -500, -420, 1000, 860, 30, 1001); D.fs(ctx, '#c08a3e', 7);
|
| 816 |
+
D.rrect(ctx, -460, -370, 920, 790, 16, 1002); D.fs(ctx, '#fffdf6', 5);
|
| 817 |
+
D.rrect(ctx, -120, -450, 240, 70, 16, 1003); D.fs(ctx, '#999', 5);
|
| 818 |
+
D.text(ctx, '识别"猴子"的三个问题', 0, -300, 54, C.ink);
|
| 819 |
+
const qs = [['它本身到底有什么用?', '(抛开价格)'], ['现在是谁在买?为什么买?', ''], ['再也没人接盘,', '我还愿意拿着吗?']];
|
| 820 |
+
qs.forEach(([a, b], i) => {
|
| 821 |
+
const st = S.L[2 + i];
|
| 822 |
+
if (lt < st) return;
|
| 823 |
+
const y = -170 + i * 190;
|
| 824 |
+
ctx.save(); ctx.globalAlpha = D.clamp01((lt - st) / .3);
|
| 825 |
+
D.rrect(ctx, -400, y - 40, 80, 80, 14, 1010 + i); D.fs(ctx, '#fff', 5);
|
| 826 |
+
const typedA = a.slice(0, Math.floor(D.clamp01((lt - st) / 1.0) * a.length));
|
| 827 |
+
D.text(ctx, typedA, -290, b ? y - 20 : y, 48, C.ink, { align: 'left' });
|
| 828 |
+
if (b) D.text(ctx, b, -290, y + 36, b.startsWith('(') ? 34 : 48, b.startsWith('(') ? '#998' : C.ink, { align: 'left' });
|
| 829 |
+
D.check(ctx, -360, y, 28, D.clamp01((lt - st - 2.2) / 0.4));
|
| 830 |
+
ctx.restore();
|
| 831 |
+
});
|
| 832 |
+
});
|
| 833 |
+
if (S.ls(5) > 0) {
|
| 834 |
+
const k = vis(lt, S.L[5] + 0.3, null, 0.6);
|
| 835 |
+
tint(ctx, '#2b2321', 0.4 * D.clamp01(S.ls(5) / .5));
|
| 836 |
+
D.monkey(ctx, 640, GY + 40, 2.6 * k, { t, mood: 'smug', aL: 2.9, aR: 2.9 });
|
| 837 |
+
at(ctx, 640, 180, vis(lt, S.L[5] + 1.6), () => D.stamp(ctx, 0, 0, 1.5, '大概率是猴子', C.red, -0.1));
|
| 838 |
+
}
|
| 839 |
+
};
|
| 840 |
+
|
| 841 |
+
/* =======================================================
|
| 842 |
+
* 14 结尾 outro
|
| 843 |
+
* ======================================================= */
|
| 844 |
+
SC.outro = function (ctx, lt, S) {
|
| 845 |
+
const t = S.t;
|
| 846 |
+
sky(ctx, '#ff9e6b', '#ffe0b3'); D.sun(ctx, 640, 520, 110, t, false); mountains(ctx, '#e2a07c', '#c98a6a');
|
| 847 |
+
forest(ctx, t, GY - 60, 0.6, { scale: 0.8, col: '#5f8f4a', col2: '#4c7a3e' }); ground(ctx, '#a8b86a', '#8fa055'); houses(ctx, t);
|
| 848 |
+
if (S.ls(1) < 0) {
|
| 849 |
+
[['稳赚不赔', 360, 260, C.green], ['等老板回来就翻倍', 900, 380, C.purple]].forEach(([s, x, y, c], i) => {
|
| 850 |
+
const k = vis(lt, S.L[0] + 0.3 + i * 1.6);
|
| 851 |
+
at(ctx, x, y, k, () => { D.bubble(ctx, 0, 0, s.length * 56 + 80, 130, 0, 150, { text: s, size: 52, color: c }); if (lt > S.L[0] + 1.3 + i * 1.6) D.cross(ctx, 0, 0, 70); });
|
| 852 |
+
});
|
| 853 |
+
D.assistant(ctx, 640, GY + 240, 1.1, { t, mood: 'sly', aR: 2.2 });
|
| 854 |
+
} else if (S.ls(2) < 0) {
|
| 855 |
+
// 放大镜看笼子
|
| 856 |
+
D.cage(ctx, 640, GY + 220, 520, 340, 1, t, { cols: 5, rows: 2, mood: 'smug' });
|
| 857 |
+
const mx = 640 + Math.sin(t * 1.5) * 120, my = GY - 30;
|
| 858 |
+
at(ctx, mx, my, vis(lt, S.L[1] + 0.3), () => { ctx.beginPath(); ctx.arc(0, 0, 150, 0, Math.PI * 2); ctx.fillStyle = 'rgba(220,240,255,.35)'; ctx.fill(); D.stroke(ctx, 18); ctx.beginPath(); ctx.moveTo(105, 105); ctx.lineTo(230, 230); D.stroke(ctx, 30, '#8a5a35'); });
|
| 859 |
+
at(ctx, 640, 170, vis(lt, S.L[1] + 1.2), () => label(ctx, '先看看:是不是猴子?', 0, 0, 54, C.ink, '#ffcd3c'));
|
| 860 |
+
} else {
|
| 861 |
+
// 关注 + 再见
|
| 862 |
+
const d2 = S.ls(2);
|
| 863 |
+
const followK = vis(lt, S.L[2] + 0.2);
|
| 864 |
+
const pressed = d2 > 2.6;
|
| 865 |
+
at(ctx, 640, 380, followK * (pressed && d2 < 2.8 ? 0.9 : 1), () => { D.rrect(ctx, -230, -70, 460, 140, 70, 1101); D.fs(ctx, pressed ? '#bbbbbb' : '#ff4d6d', 6); D.text(ctx, pressed ? '已关注' : '+ 关注', 0, 4, 64, '#fff'); });
|
| 866 |
+
const icons = [['👍', '点赞'], ['🪙', '投币'], ['⭐', '收藏']];
|
| 867 |
+
icons.forEach(([, s], i) => at(ctx, 400 + i * 240, 600, vis(lt, S.L[2] + 0.8 + i * 0.3), () => {
|
| 868 |
+
D.blob(ctx, 0, 0, 70, 70, 1110 + i); D.fs(ctx, [C.blue, C.gold, C.pink][i], 5);
|
| 869 |
+
if (i === 0) D.heart(ctx, 0, 0, 30, '#fff'); else if (i === 1) D.coin(ctx, 0, 0, 30, 0); else D.sparkle(ctx, 0, 0, 40, 0.5, '#fff');
|
| 870 |
+
D.text(ctx, s, 0, 105, 36, C.ink);
|
| 871 |
+
}));
|
| 872 |
+
const k = E.inout(D.clamp01((d2 - 1.6) / 1));
|
| 873 |
+
at(ctx, D.lerp(1200, 690, k), D.lerp(820, 410, k), followK, () => { D.blob(ctx, 0, 0, 30, 36, 1120); D.fs(ctx, '#fff', 5); D.rrect(ctx, -10, -70, 20, 60, 10, 1121); D.fs(ctx, '#fff', 5); });
|
| 874 |
+
if (pressed) D.confetti(ctx, 640, 380, d2 - 2.6, 50, 700, 81);
|
| 875 |
+
if (S.ls(3) > 0) {
|
| 876 |
+
D.monkey(ctx, 640, GY + 200, 1.8 * vis(lt, S.L[3]), { t, mood: 'happy', aL: 0.4, aR: 2.6 + Math.sin(t * 10) * 0.4 });
|
| 877 |
+
at(ctx, 640, 170, vis(lt, S.L[3] + 0.2), () => D.bigText(ctx, '下期再见!', 0, 0, 110, '#fff'));
|
| 878 |
+
}
|
| 879 |
+
}
|
| 880 |
+
};
|
| 881 |
+
|
| 882 |
+
/* ---------- 封面 ---------- */
|
| 883 |
+
SC.__cover = function (ctx, portrait) {
|
| 884 |
+
const t = 1.3;
|
| 885 |
+
rays(ctx, 640, 520, 0, '#ffd27a', '#ffbf4a', 22);
|
| 886 |
+
D.speedLines(ctx, 640, 520, 0, 'rgba(255,255,255,.35)', 30, 420);
|
| 887 |
+
ctx.fillStyle = '#9fd468'; ctx.fillRect(X0, GY + 200, X1 - X0, Y1);
|
| 888 |
+
D.cage(ctx, 330, GY + 260, 470, 330, 1, t, { cols: 5, rows: 3, label: '1000只' });
|
| 889 |
+
D.merchant(ctx, 900, GY + 290, 1.35, { t, mood: 'laugh', aR: 2.4, holdR: (c) => D.megaphone(c), aL: 0.5 });
|
| 890 |
+
D.priceSign(ctx, 1090, GY + 10, 0.95, '50', { t: 0, noPost: true, bg: '#fff2b8' });
|
| 891 |
+
D.monkey(ctx, 640, GY + 330, 1.4, { t, mood: 'smug', aL: 2.8, aR: 2.4 });
|
| 892 |
+
for (let i = 0; i < 10; i++) D.coin(ctx, 120 + D.hash(i * 3) * 1040, 80 + D.hash(i * 7) * 380, 30, i);
|
| 893 |
+
D.exclaim(ctx, 640, 220, 0.9, 0.3);
|
| 894 |
+
};
|
| 895 |
+
|
| 896 |
+
window.SCENES = SC;
|
| 897 |
+
})();
|
shouhouzi/tools/frame-test.html
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><meta charset="utf-8">
|
| 2 |
+
<style>@font-face{font-family:"KuaiLe";src:url(../fonts/kuaile.woff2) format("woff2");}</style>
|
| 3 |
+
<canvas id="c"></canvas>
|
| 4 |
+
<script src="../js/script-data.js"></script><script src="../js/draw.js"></script><script src="../js/scenes.js"></script><script src="../js/engine.js"></script>
|
| 5 |
+
<script>
|
| 6 |
+
window.snap=async(t,mode,guides)=>{await document.fonts.load('40px KuaiLe');const L=LAYOUTS[mode];const c=document.getElementById('c');c.width=L.W;c.height=L.H;const x=c.getContext('2d');ENGINE.render(x,t,L,{guides});return c.toDataURL('image/jpeg',0.8)};
|
| 7 |
+
window.cover=async(mode)=>{await document.fonts.load('40px KuaiLe');const L=LAYOUTS[mode];const c=document.getElementById('c');c.width=L.W;c.height=L.H;const x=c.getContext('2d');ENGINE.renderCover(x,L);return c.toDataURL('image/png')};
|
| 8 |
+
window.bench=async(n,mode)=>{const L=LAYOUTS[mode];const c=document.getElementById('c');c.width=L.W;c.height=L.H;const x=c.getContext('2d');const t0=performance.now();for(let i=0;i<n;i++)ENGINE.render(x,i*TL.total/n,L);return (performance.now()-t0)/n};
|
| 9 |
+
</script>
|
shouhouzi/tools/subset_font.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 收集项目中所有用到的字符,子集化站酷快乐体为 woff2
|
| 2 |
+
import os,glob,sys
|
| 3 |
+
from fontTools import subset
|
| 4 |
+
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 5 |
+
src=sys.argv[1] if len(sys.argv)>1 else '/tmp/ZCOOLKuaiLe-Regular.ttf'
|
| 6 |
+
chars=set()
|
| 7 |
+
for f in glob.glob(ROOT+'/js/*.js')+glob.glob(ROOT+'/*.html'):
|
| 8 |
+
chars|=set(open(f,encoding='utf-8').read())
|
| 9 |
+
chars|=set('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ¥$%+-−×=.,:;!?()[]/|·…“”‘’,。!?、:;()《》【】~~')
|
| 10 |
+
text=''.join(sorted(c for c in chars if ord(c)>=32))
|
| 11 |
+
opts=subset.Options(); opts.flavor='woff2'; opts.layout_features=['*']
|
| 12 |
+
f=subset.load_font(src,opts); s=subset.Subsetter(opts); s.populate(text=text); s.subset(f)
|
| 13 |
+
os.makedirs(ROOT+'/fonts',exist_ok=True)
|
| 14 |
+
subset.save_font(f,ROOT+'/fonts/kuaile.woff2',opts)
|
| 15 |
+
print(len(text),'chars',os.path.getsize(ROOT+'/fonts/kuaile.woff2'),'bytes')
|