Files
ChronicleOfTheImmortalClan/scripts/gen-art.mjs
T
thzxx c52ecffcd9 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 通过
2026-08-23 12:50:18 +08:00

125 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)