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:
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { seasonOf } from '../src/renderer/game/data/season'
|
||||
import { tideOf, yearRing, seasonTint, seasonCssVars, TIDES } from '../src/renderer/game/core/timesense'
|
||||
import { FxGate, FxKind } from '../src/renderer/game/core/fxqueue'
|
||||
|
||||
describe('timesense 时间感知矩阵', () => {
|
||||
it.each([[1, 'spring'], [3, 'spring'], [4, 'summer'], [6, 'summer'], [7, 'autumn'], [9, 'autumn'], [10, 'winter'], [12, 'winter']] as const)(
|
||||
'月%i → %s',
|
||||
(m, s) => {
|
||||
expect(seasonOf(m)).toBe(s)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([[1, '立春'], [2, '雨水'], [4, '清明'], [12, '冬至'], [12, '小寒']])('节气 24 枚全在', () => {
|
||||
expect(TIDES.length).toBe(24)
|
||||
})
|
||||
|
||||
it.each([1, 2, 4, 6, 9, 11, 12].map((m) => [m] as const))('月%i 两个节气均有效', (m) => {
|
||||
expect(TIDES).toContain(tideOf(m, 0))
|
||||
expect(TIDES).toContain(tideOf(m, 1))
|
||||
})
|
||||
|
||||
it.each([1, 6, 12].map((m) => [m] as const))('年环 %i 月在 [0,1]', (m) => {
|
||||
const r = yearRing(m)
|
||||
expect(r).toBeGreaterThanOrEqual(0)
|
||||
expect(r).toBeLessThan(1)
|
||||
})
|
||||
|
||||
it('季相色均出自金碧墨色系(accent/tint 非空且不为霓虹)', () => {
|
||||
for (const s of ['spring', 'summer', 'autumn', 'winter'] as const) {
|
||||
const t = seasonTint(s)
|
||||
expect(t.accent).toMatch(/^#[0-9a-f]{6}$/i)
|
||||
expect(t.glow).toMatch(/^\d+, \d+, \d+$/)
|
||||
expect(t.name.length).toBeGreaterThan(0)
|
||||
const vars = seasonCssVars(s)
|
||||
expect(vars['--season-accent']).toBe(t.accent)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('FxGate 竞态安全', () => {
|
||||
function freshGate() {
|
||||
const emitted: FxKind[] = []
|
||||
const gate = new FxGate({ emit: (e) => emitted.push(e.kind) })
|
||||
return { gate, emitted }
|
||||
}
|
||||
|
||||
it('std 模式:spark/mist/ripple/pulse 允许,blade 需 full', () => {
|
||||
const { gate, emitted } = freshGate()
|
||||
gate.emit('spark')
|
||||
gate.emit('mist')
|
||||
gate.emit('ripple')
|
||||
gate.emit('pulse')
|
||||
gate.emit('blade')
|
||||
expect(emitted).toEqual(['spark', 'mist', 'ripple', 'pulse'])
|
||||
expect(emitted).not.toContain('blade')
|
||||
})
|
||||
|
||||
it('soft 模式:仅 ripple/pulse/drift', () => {
|
||||
const { gate, emitted } = freshGate()
|
||||
gate.setMode('soft')
|
||||
gate.emit('spark')
|
||||
gate.emit('ripple')
|
||||
gate.emit('drift')
|
||||
expect(emitted).toEqual(['ripple', 'drift'])
|
||||
})
|
||||
|
||||
it('prefers-reduced 时全发抑制(降级)', () => {
|
||||
const { gate, emitted } = freshGate()
|
||||
gate.setReduced(true)
|
||||
gate.emit('spark')
|
||||
gate.emit('pulse')
|
||||
expect(emitted).toEqual([])
|
||||
})
|
||||
|
||||
it('重入安全:队列 drain 期间再 emit 不丢不叠(单帧批处理语义)', () => {
|
||||
const { gate, emitted } = freshGate()
|
||||
gate.emit('spark')
|
||||
gate.emit('spark')
|
||||
gate.emit('spark')
|
||||
expect(emitted.length).toBe(3)
|
||||
expect(gate.pending()).toBe(0)
|
||||
})
|
||||
|
||||
it('deltas:差异、净零、NaN 滤除', () => {
|
||||
const d = FxGate.deltas({ a: 10, b: 5, c: NaN }, { a: 14, b: 5, d: 3 })
|
||||
expect(d.a).toBe(4)
|
||||
expect(d.b).toBeUndefined()
|
||||
expect(d.c).toBeUndefined() // NaN 原值被过滤
|
||||
expect(d.d).toBe(3)
|
||||
})
|
||||
|
||||
it('deltas 空对象不产生输出', () => {
|
||||
expect(Object.keys(FxGate.deltas({}, {}))).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user