Repository navigation
Expand file tree
/
Copy pathrender.js
More file actions
436 lines (416 loc) · 15.6 KB
/
Copy pathrender.js
File metadata and controls
436 lines (416 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
/* brushtrokes render — 共用的笔触绘制器
*
* 网页实时预览、下载的单文件播放器,都从这里取绘制逻辑,
* 保证「页面里看到的」和「下载后播放的」完全一致。
*
* 笔触 packed 格式:[d, color, width, start, dur, detail, layer, len, alpha, kind]
*/
(function (root) {
'use strict';
const KIND = { NORMAL: 0, GLAZE: 1, SCUMBLE: 2, IMPASTO: 3, FILL: 4, DOT: 5, GLOW: 6, PIXEL: 7 };
const RE_TOK = /[MLZ]|-?\d+(?:\.\d+)?/g;
/* ---------------------------------------------------------------- 颜色工具 */
function h2r(h) {
return [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
}
function rgb(r, g, b) {
const c = v => Math.max(0, Math.min(255, Math.round(v)));
return 'rgb(' + c(r) + ',' + c(g) + ',' + c(b) + ')';
}
function hex(r, g, b) {
const c = v => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0');
return '#' + c(r) + c(g) + c(b);
}
/* 提亮/压暗:f 为倍率,add 为加数 */
function tone(h, f, add) {
const c = h2r(h);
return rgb(c[0] * f + add, c[1] * f + add, c[2] * f + add);
}
/* 亮脊色:底色越亮提亮越少,接近纯白时几乎不提亮 —— 避免浅色区域被顶成纯白线 */
function ridge(h) {
const c = h2r(h);
const lum = 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];
const k = Math.max(0, Math.min(1, (235 - lum) / 235));
const f = 1 + 0.22 * k;
const add = 22 * k * k;
return rgb(
Math.max(0, Math.min(255, Math.round(Math.min(c[0] * f + add, 244, c[0] + 34)))),
Math.max(0, Math.min(255, Math.round(Math.min(c[1] * f + add, 244, c[1] + 34)))),
Math.max(0, Math.min(255, Math.round(Math.min(c[2] * f + add, 244, c[2] + 34)))));
}
/* ---------------------------------------------------------------- 路径解析
* 同时支持线(M…L…)和闭合填充(M…L…Z)
*/
function parsePath(d) {
const tok = d.match(RE_TOK);
if (!tok) return { pts: new Float32Array(0), closed: false };
const closed = /Z/i.test(d);
const n = Math.floor(tok.length / 3);
const pts = new Float32Array(n * 2);
for (let k = 0, j = 0; k < n; k++, j += 2) {
pts[j] = +tok[k * 3 + 1];
pts[j + 1] = +tok[k * 3 + 2];
}
return { pts, closed };
}
/* ---------------------------------------------------------------- 底纹
* 油画布 / 线描黑底 / 素描纸,画一次即可,之后所有笔触叠在上面
*/
function paintGround(c, w, h, spec) {
spec = spec || {};
const type = spec.type || 'oil';
const color = spec.color || '#181613';
c.setTransform(1, 0, 0, 1, 0, 0);
c.globalAlpha = 1;
c.fillStyle = color;
c.fillRect(0, 0, w, h);
if (type === 'lineart') {
// 近黑底,四角压暗,中间略提 —— 让白线有「浮起来」的感觉
const g = c.createRadialGradient(w * 0.5, h * 0.46, Math.min(w, h) * 0.1,
w * 0.5, h * 0.5, Math.max(w, h) * 0.78);
g.addColorStop(0, 'rgba(255,255,255,0.030)');
g.addColorStop(0.55, 'rgba(255,255,255,0.008)');
g.addColorStop(1, 'rgba(0,0,0,0.34)');
c.fillStyle = g;
c.fillRect(0, 0, w, h);
return;
}
if (type === 'pencil') {
// 纸张纤维:细密划痕 + 轻微暖色斑
const img = c.getImageData(0, 0, w, h), d = img.data;
let seed = 20261109;
const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
for (let i = 0; i < d.length; i += 4) {
const v = (rnd() - 0.5) * 8;
d[i] += v; d[i + 1] += v * 0.97; d[i + 2] += v * 0.85;
}
c.putImageData(img, 0, 0);
// 纸张纤维纹理
c.save();
c.globalAlpha = 0.055;
c.strokeStyle = '#8a7a60';
c.lineWidth = 1;
for (let i = 0; i < Math.round(w * h / 7000); i++) {
const x = rnd() * w, y = rnd() * h;
const a = (rnd() - 0.5) * 0.5, L = 6 + rnd() * 24;
c.beginPath(); c.moveTo(x, y);
c.lineTo(x + Math.cos(a) * L, y + Math.sin(a) * L); c.stroke();
}
c.restore();
return;
}
if (type === 'posterize') {
// 极淡的中心渐亮,避免大面积平涂死板
const g = c.createRadialGradient(w * 0.5, h * 0.45, 0, w * 0.5, h * 0.5, Math.max(w, h) * 0.8);
g.addColorStop(0, 'rgba(255,255,255,0.045)');
g.addColorStop(1, 'rgba(0,0,0,0.10)');
c.fillStyle = g;
c.fillRect(0, 0, w, h);
return;
}
if (type === 'ink') {
// 宣纸:暖白底 + 纤维纹理 + 轻微墨晕,四角略压
const img = c.getImageData(0, 0, w, h), d = img.data;
let seed = 20261109;
const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
for (let i = 0; i < d.length; i += 4) {
const v = (rnd() - 0.5) * 7;
d[i] += v; d[i + 1] += v * 0.98; d[i + 2] += v * 0.9;
}
c.putImageData(img, 0, 0);
// 湿纸的纤维
c.save();
c.globalAlpha = 0.045;
c.strokeStyle = '#6b5a42';
c.lineWidth = 1;
for (let i = 0; i < Math.round(w * h / 6000); i++) {
const x = rnd() * w, y = rnd() * h;
const a = (rnd() - 0.5) * 0.6, L = 8 + rnd() * 30;
c.beginPath(); c.moveTo(x, y);
c.lineTo(x + Math.cos(a) * L, y + Math.sin(a) * L); c.stroke();
}
c.restore();
// 四角轻微压暗,像纸被墨洇过
const g = c.createRadialGradient(w * 0.5, h * 0.5, Math.min(w, h) * 0.25,
w * 0.5, h * 0.5, Math.max(w, h) * 0.8);
g.addColorStop(0, 'rgba(120,100,70,0)');
g.addColorStop(1, 'rgba(120,100,70,0.09)');
c.fillStyle = g;
c.fillRect(0, 0, w, h);
return;
}
if (type === 'dot') {
// 点彩:纯白画布(修拉是在白底上点色的)
const img = c.getImageData(0, 0, w, h), d = img.data;
let seed = 20261109;
const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
for (let i = 0; i < d.length; i += 4) {
const v = (rnd() - 0.5) * 5;
d[i] += v; d[i + 1] += v; d[i + 2] += v * 0.96;
}
c.putImageData(img, 0, 0);
return;
}
if (type === 'neon') {
// 近黑底 + 极弱的中场提亮,给发光留出对比空间
const g = c.createRadialGradient(w * 0.5, h * 0.45, 0, w * 0.5, h * 0.5, Math.max(w, h) * 0.85);
g.addColorStop(0, 'rgba(40,30,70,0.30)');
g.addColorStop(0.6, 'rgba(14,10,26,0.12)');
g.addColorStop(1, 'rgba(0,0,0,0.45)');
c.fillStyle = g;
c.fillRect(0, 0, w, h);
return;
}
if (type === 'pixel') {
// 像素画:深色底 + 极淡的扫描线,营造老显示器感
c.fillStyle = 'rgba(0,0,0,0.18)';
for (let y = 0; y < h; y += 4) c.fillRect(0, y, w, 1);
return;
}
// oil:画布颗粒
const img = c.getImageData(0, 0, w, h), d = img.data;
let seed = 20261109;
for (let i = 0; i < d.length; i += 4) {
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
const v = (((seed >> 16) & 255) / 255 - 0.5) * 4.5;
d[i] += v; d[i + 1] += v; d[i + 2] += v * 0.9;
}
c.putImageData(img, 0, 0);
}
/* ---------------------------------------------------------------- 笔画局部路径
* prog < 1 时只画到整条路径的一部分,实现「逐笔生长」
*/
function tracePartial(c, pts, prog, SC) {
const npt = pts.length >> 1;
const lim = Math.max(2, Math.ceil(npt * prog));
c.beginPath();
c.moveTo(pts[0] * SC, pts[1] * SC);
for (let j = 1; j < lim && j < npt; j++) c.lineTo(pts[j * 2] * SC, pts[j * 2 + 1] * SC);
return { npt, lim };
}
/* ---------------------------------------------------------------- 单笔绘制
* S: stroke packed 记录;i: 索引;SC: 画布/原图比例
* pre: { pts, color, lite, alpha, kind } —— 预处理缓存(可选,省去重复解析)
*/
function drawStroke(c, S, SC, i, prog, pre) {
const st = S[i];
const pts = pre ? pre.pts : null;
const color = pre ? pre.color : st[1];
const kind = pre ? pre.kind : (st[9] | 0);
const alpha = pre ? pre.alpha : (st[8] === undefined ? 1 : st[8]);
const w = st[2] * SC;
/* ---- 点彩圆点:修拉那种小色点,逐点落下 ---- */
if (kind === KIND.DOT) {
if (!pts) return;
const x = pts[0] * SC, y = pts[1] * SC;
c.save();
// 生长中用半径缩放来「点」出来
const r = Math.max(0.4, (w * 0.5) * (0.35 + 0.65 * Math.min(1, prog)));
c.globalAlpha = alpha;
c.fillStyle = color;
c.beginPath();
c.arc(x, y, r, 0, Math.PI * 2);
c.fill();
// 点的高光侧:让密集的点看起来有厚薄变化
if (prog > 0.6 && r > 1.6) {
c.globalAlpha = alpha * 0.28;
c.fillStyle = pre && pre.lite ? pre.lite : '#ffffff';
c.beginPath();
c.arc(x - r * 0.26, y - r * 0.26, r * 0.44, 0, Math.PI * 2);
c.fill();
}
c.restore();
return;
}
/* ---- 像素块:一格一色,硬边方块 ---- */
if (kind === KIND.PIXEL) {
if (!pts) return;
const x = pts[0] * SC, y = pts[1] * SC;
// 边长就是笔宽的 2 倍(笔宽存的是半格)
const sz = Math.max(1, w * 2);
c.save();
c.globalAlpha = alpha;
c.fillStyle = color;
if (prog >= 0.999) {
c.fillRect(x - sz / 2, y - sz / 2, sz, sz);
} else {
// 点亮过程:从中心放大
const k = 0.25 + 0.75 * Math.min(1, prog);
const s2 = sz * k;
c.fillRect(x - s2 / 2, y - s2 / 2, s2, s2);
}
c.restore();
return;
}
/* ---- 霓虹发光:宽而虚的外晕 + 亮芯 ---- */
if (kind === KIND.GLOW) {
if (!pts) return;
const r = tracePartial(c, pts, prog, SC);
c.save();
c.lineCap = 'round';
c.lineJoin = 'round';
/* 用 lighter 做加色叠加才有「发光」感,但密集区域会亮度累加到饱和成白。
* 所以外层辉光给得很淡,主要靠一层实心亮线撑住形体。 */
c.globalCompositeOperation = 'lighter';
c.strokeStyle = color;
const layers = [
[w * 3.2, alpha * 0.045],
[w * 1.7, alpha * 0.085],
];
for (const [lw, la] of layers) {
c.globalAlpha = la;
c.lineWidth = lw;
c.stroke();
}
c.restore();
// 主体:正常混合画实线,保证密集处不会叠成白
c.save();
c.lineCap = 'round';
c.lineJoin = 'round';
c.globalAlpha = alpha * 0.78;
c.strokeStyle = color;
c.lineWidth = Math.max(0.55, w * 0.72);
c.stroke();
// 芯:比主体再亮一点,但很细
c.globalAlpha = alpha * 0.42;
c.strokeStyle = pre && pre.lite ? pre.lite : '#ffffff';
c.lineWidth = Math.max(0.45, w * 0.26);
c.stroke();
c.restore();
return;
}
/* ---- 色块填充:一次铺满一块区域 ---- */
if (kind === KIND.FILL) {
if (!pts) return;
const n = pts.length >> 1;
// 用已画比例做「擦入」:从中心向外扩张,视觉上是色块被刷开
c.save();
if (prog >= 0.999) {
c.globalAlpha = alpha;
c.beginPath();
c.moveTo(pts[0] * SC, pts[1] * SC);
for (let j = 1; j < n; j++) c.lineTo(pts[j * 2] * SC, pts[j * 2 + 1] * SC);
c.closePath();
c.fillStyle = color;
c.fill('evenodd');
} else {
// 生长中:按质心缩放,像颜料从中心摊开
let cx = 0, cy = 0;
for (let j = 0; j < n; j++) { cx += pts[j * 2]; cy += pts[j * 2 + 1]; }
cx /= n; cy /= n;
const k = 0.28 + 0.72 * prog;
c.globalAlpha = alpha * Math.min(1, prog * 2.4);
c.beginPath();
c.moveTo((cx + (pts[0] - cx) * k) * SC, (cy + (pts[1] - cy) * k) * SC);
for (let j = 1; j < n; j++) {
c.lineTo((cx + (pts[j * 2] - cx) * k) * SC, (cy + (pts[j * 2 + 1] - cy) * k) * SC);
}
c.closePath();
c.fillStyle = color;
c.fill('evenodd');
}
c.restore();
return;
}
if (!pts) return;
const r = tracePartial(c, pts, prog, SC);
c.lineCap = 'round';
c.lineJoin = 'round';
/* ---- 干扫:三道平行细线 ---- */
if (kind === KIND.SCUMBLE) {
c.globalAlpha = alpha;
for (let b = 0; b < 3; b++) {
const off = (b - 1) * w * 0.34;
c.save(); c.translate(0, off);
c.strokeStyle = b === 1 ? color : (pre ? pre.lite : ridge(color));
c.lineWidth = w * (b === 1 ? 0.5 : 0.26);
c.stroke(); c.restore();
}
c.globalAlpha = 1;
return;
}
/* ---- 厚涂:暗边 + 主体 + 高光偏移 ---- */
if (kind === KIND.IMPASTO) {
c.globalAlpha = alpha * 0.55;
c.strokeStyle = tone(color, 0.55, 0);
c.lineWidth = w * 1.16; c.stroke();
c.globalAlpha = alpha;
c.strokeStyle = color; c.lineWidth = w; c.stroke();
c.save(); c.translate(-w * 0.14, -w * 0.14);
c.strokeStyle = pre ? pre.lite : ridge(color);
c.lineWidth = w * 0.36; c.stroke();
c.restore();
c.globalAlpha = 1;
return;
}
if (kind === KIND.GLAZE) {
c.globalAlpha = alpha * 0.62;
c.strokeStyle = color;
c.lineWidth = w * 1.34;
c.stroke();
c.globalAlpha = alpha;
c.lineWidth = w;
c.stroke();
c.globalAlpha = 1;
return;
}
/* ---- 普通笔触 ---- */
c.globalAlpha = alpha;
c.strokeStyle = color;
c.lineWidth = w;
c.stroke();
// 起锋收锋:首尾各补一小段更细的笔道
if (w > 4.5 && r.lim >= 4) {
c.strokeStyle = color;
c.beginPath(); c.moveTo(pts[0] * SC, pts[1] * SC);
for (let j = 1; j < Math.min(3, r.npt); j++) c.lineTo(pts[j * 2] * SC, pts[j * 2 + 1] * SC);
c.lineWidth = w * 0.45; c.stroke();
const e = Math.min(r.lim, r.npt) - 1;
c.beginPath(); c.moveTo(pts[e * 2] * SC, pts[e * 2 + 1] * SC);
for (let j = e - 1; j >= Math.max(0, e - 2); j--) c.lineTo(pts[j * 2] * SC, pts[j * 2 + 1] * SC);
c.lineWidth = w * 0.4; c.stroke();
}
// 颜料渐枯:只在深色粗笔上做,浅色笔上叠亮色会形成白拖尾
if (w > 7.5) {
const c0 = h2r(color);
const lum = 0.299 * c0[0] + 0.587 * c0[1] + 0.114 * c0[2];
const dryK = Math.max(0, Math.min(1, (180 - lum) / 180));
if (dryK > 0.08) {
c.globalAlpha = alpha * 0.30 * dryK;
c.strokeStyle = pre ? pre.lite : ridge(color);
const s0 = Math.floor(r.npt * 0.62), e0 = r.lim;
if (e0 > s0 + 1) {
c.beginPath(); c.moveTo(pts[s0 * 2] * SC, pts[s0 * 2 + 1] * SC);
for (let j = s0 + 1; j < e0 && j < r.npt; j++) c.lineTo(pts[j * 2] * SC, pts[j * 2 + 1] * SC);
c.lineWidth = w * 0.44; c.stroke();
}
}
}
c.globalAlpha = 1;
}
/* ---------------------------------------------------------------- 预处理
* 把 packed 笔触解析成绘制期要用的结构,避免每帧重复解析路径
*/
function preprocess(S) {
const out = new Array(S.length);
for (let i = 0; i < S.length; i++) {
const s = S[i];
const p = parsePath(s[0]);
const kind = s[9] | 0;
out[i] = {
pts: p.pts,
color: s[1],
lite: (kind === KIND.SCUMBLE || kind === KIND.IMPASTO || kind === KIND.DOT || kind === KIND.GLOW)
? ridge(s[1]) : null,
alpha: s[8] === undefined ? 1 : s[8],
kind,
closed: p.closed,
};
}
return out;
}
root.OILRender = {
KIND, h2r, rgb, hex, tone, ridge,
parsePath, paintGround, tracePartial, drawStroke, preprocess,
};
})(typeof self !== 'undefined' ? self : this);