v0.1.13: 金碧山河(SVG/Canvas 素材 + 动效系统 + 舞美重装)

【素材生成(零外部资源)】
- scripts/gen-art.mjs 程序化产出 5 枚 SVG 资产:金碧山水全景(山脊/金乌/雁阵/水纹)、
  祥云纹带、回纹角饰、仙鹤、星辰层 → src/renderer/assets/gen/,npm run gen:art 可重跑

【Canvas 动效系统(单 RAF)】
- ui/fx-canvas.ts:灵雾粒子流(常驻随季色)/金砂喷发(spark)/刀光(blade)/涟漪(ripple),
  单实例防重入、防膨胀 cap、GPU 友好、visibility 自停
- ui/fx.ts + core/fxqueue.ts:FxGate 统一发射闸——档位过滤(柔/标/全)、
  reduced-motion 降级、单帧批处理防竞态、deltas 纯函数(漂字差分)

【舞美】
- 金碧背景层 bg-scene(全页面,随季节 hue-rotate 色温渐变)
- 顶栏时辰司:季节印(春·熏风/夏·蝉鸣/秋·霜叶/冬·雪静)+年月进度环(SVG 弧段)+节气名
- 成员卡:闭关 aura 三点浮动 + 修为流光 shimmer
- 消息流:good 事件(突破/获宝/落槌)→金砂喷发,战事→刀光扫过战报
- Boot 卷轴展开过渡;drift 漂字层(独立节点队列不覆盖,8 条裁剪)
- 设置页动效三档(柔和/标准/全开)

【竞态规避】
- RAF 单循环 guard、particles cap 裁剪、FxGate draining 互斥、漂字节点附加不覆盖、
  visibility 自动降级、advance in-flight 闸沿用

【测试】937→967(timesense 矩阵 15 + FxGate 竞态/档位/deltas 15)
全量 35 套件通过;金钟罩零漂移(纯 UI/素材层);typecheck/build 通过
This commit is contained in:
2026-08-23 12:50:18 +08:00
parent 607624695d
commit c52ecffcd9
20 changed files with 904 additions and 6 deletions
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env node
/**
* gen-art.mjs — 程序化生成 SVG 素材(金碧山水/纹样/饰件),零外部资源。
* 用法:node scripts/gen-art.mjs (产物写 src/renderer/assets/gen/*.svg
*/
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const OUT = join(dirname(fileURLToPath(import.meta.url)), '../src/renderer/assets/gen')
mkdirSync(OUT, { recursive: true })
// ============ 1. 金碧山水全景(主背景) ============
function goldenLandscape() {
const ridge = (y, amp, seed, opacity) => {
const pts = []
let x = -40
let phase = seed
while (x <= 1280) {
const yy = y + Math.sin((x / 170) + phase) * amp + Math.sin((x / 71) + phase * 2.2) * amp * 0.3
pts.push(`${x.toFixed(0)},${yy.toFixed(0)}`)
phase += 0.35
x += 80
}
return `<polygon points="${pts.join(' ')} 1300,760 -40,760" fill="#8a6d2f" opacity="${opacity}" />`
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 760" preserveAspectRatio="xMidYMax slice">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#181208"/>
<stop offset="0.65" stop-color="#241a10"/>
<stop offset="1" stop-color="#38271a"/>
</linearGradient>
<radialGradient id="sun" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#f2cf7e" stop-opacity="0.9"/>
<stop offset="0.5" stop-color="#c99a3d" stop-opacity="0.35"/>
<stop offset="1" stop-color="#c99a3d" stop-opacity="0"/>
</radialGradient>
<filter id="blur2"><feGaussianBlur stdDeviation="2"/></filter>
<filter id="blur6"><feGaussianBlur stdDeviation="6"/></filter>
</defs>
<rect width="1200" height="760" fill="url(#sky)"/>
<circle cx="905" cy="150" r="130" fill="url(#sun)"/>
<circle cx="905" cy="150" r="30" fill="#f6dd9d" opacity="0.75" filter="url(#blur2)"/>
${ridge(420, 46, 1.4, 0.10)}
${ridge(500, 52, 2.1, 0.15)}
${ridge(590, 46, 3.2, 0.22)}
<g stroke="#cbb35f" stroke-width="1.6" opacity="0.35" fill="none" filter="url(#blur2)">
<path d="M300 262 q16 -11 32 0 q16 -11 32 0"/>
<path d="M250 288 q14 -10 28 0 q14 -10 28 0"/>
<path d="M980 205 q13 -9 26 0 q13 -9 26 0"/>
</g>
<g stroke="#cbb35f" stroke-width="0.9" opacity="0.22">
<path d="M60 620 h300 M60 640 h220 M60 660 h150" />
</g>
<rect x="0" y="700" width="1200" height="60" fill="#1c1409" opacity="0.4"/>
</svg>`
}
// ============ 2. 祥云纹带(分隔/装饰) ============
function cloudBand() {
const cloud = (x, y, s, o) =>
`<g transform="translate(${x},${y}) scale(${s})" fill="#cbb35f" opacity="${o}">
<path d="M0 0 q14 -20 34 -18 q26 2 30 14 q18 -4 26 8 q4 12 -8 16 H8 q-14 -2 -8 -20z"/>
</g>`
let out = ''
for (let i = 0; i < 6; i++) out += cloud(i * 200 - 20, ((i % 2) * 10) - 4, 0.9 + (i % 3) * 0.15, 0.5 - (i % 2) * 0.15)
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 60">${out}</svg>`
}
// ============ 3. 回纹角饰(卡片角框) ============
function meanderCorner() {
const sq = (x, y, s) => `<path d="M${x} ${y + s} h${s} v${s} h${-s * 0} z" fill="none" stroke="#b3913e" stroke-width="2"/>`
let p = ''
for (let i = 0; i < 4; i++) {
p += `<path d="M${i * 8 + 0} 0 v${i * 8 + 0} M0 ${i * 8 + 0} h${i * 8 + 0}" />`
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<g fill="none" stroke="#b3913e" stroke-width="1.6" opacity="0.7">
<path d="M4 36 L4 4 L36 4" />
<path d="M10 30 L10 10 L34 10" />
<path d="M16 24 L16 16 L30 16" />
</g>
</svg>`
}
// ============ 4. 仙鹤(点缀) ============
function crane() {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">
<g stroke="#e0cf9b" stroke-width="2.2" fill="none" opacity="0.7" stroke-linecap="round">
<path d="M8 52 q22 -18 42 -16"/>
<path d="M50 36 q14 12 30 14"/>
<path d="M44 34 L22 22 M44 34 L70 20 M44 34 L52 14"/>
</g>
<circle cx="42" cy="33" r="2.4" fill="#e6b84c" opacity="0.9"/>
</svg>`
}
// ============ 5. 星辰点缀层 ============
function starfield() {
let p = ''
for (let i = 0; i < 42; i++) {
const x = ((i * 137) % 1200)
const y = ((i * 83) % 420)
const r = 0.6 + (i % 3) * 0.5
const o = 0.12 + ((i * 7) % 5) * 0.09
p += `<circle cx="${x}" cy="${y}" r="${r}" fill="#f4dc9a" opacity="${o}"/>`
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 460">${p}</svg>`
}
const out = {
'landscape-gold.svg': goldenLandscape(),
'cloud-band.svg': cloudBand(),
'meander-corner.svg': meanderCorner(),
'crane.svg': crane(),
'starfield.svg': starfield()
}
for (const [name, svg] of Object.entries(out)) {
writeFileSync(join(OUT, name), svg, 'utf-8')
console.log('✓', name, `${svg.length} B`)
}
console.log('gen-art done →', OUT)