| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- /**
- * wuxing-sandbox-helpers/flow-animation.js
- * 五行能量流动粒子动画
- *
- * 相生循环:沿外圆路径,能量依次转化(土→金→水→木→火)
- * 身(土)→智(金)→富(水)→行(木)→心(火)→身(土)
- * 路径:outer[i] → outer[(i+1)%5] (弧线)
- * 颜色随路径从源维度过渡到目标维度
- *
- * 相克循环:沿五角星边路径,能量抑制
- * 身(土)→富(水), 富(水)→心(火), 心(火)→智(金), 智(金)→行(木), 行(木)→身(土)
- * 路径:outer[i] → inner[(i+1)%5] → outer[(i+2)%5] (星边直线)
- * 粒子保持源维度颜色
- */
- var WUXING_COLORS = [
- { r: 141, g: 110, b: 99 }, // 0 土·身 (body)
- { r: 255, g: 107, b: 53 }, // 1 火·心 (mind)
- { r: 255, g: 215, b: 0 }, // 2 金·智 (wisdom)
- { r: 76, g: 175, b: 80 }, // 3 木·行 (action)
- { r: 66, g: 165, b: 245 } // 4 水·富 (wealth)
- ]
- var DIM_KEYS = ['body', 'mind', 'wisdom', 'action', 'wealth']
- /**
- * 维度代码 → 颜色
- */
- function colorForDimCode(code) {
- var idx = DIM_KEYS.indexOf(code)
- if (idx < 0) return { r: 200, g: 200, b: 200 }
- return WUXING_COLORS[idx]
- }
- /**
- * 计算两点间的弧线路径(圆心(cx,cy),顺时针方向)
- * @param {{x:number,y:number}} src
- * @param {{x:number,y:number}} dst
- * @param {number} cx
- * @param {number} cy
- * @param {number} numSegments - 分段数
- * @returns {Array<{x:number,y:number}>}
- */
- function arcWaypoints(src, dst, cx, cy, numSegments) {
- numSegments = numSegments || 8
- var angleSrc = Math.atan2(src.y - cy, src.x - cx)
- var angleDst = Math.atan2(dst.y - cy, dst.x - cx)
- var r = Math.sqrt(Math.pow(src.x - cx, 2) + Math.pow(src.y - cy, 2))
- // 顺时针方向角度差
- var diff = angleDst - angleSrc
- if (diff < 0) diff += Math.PI * 2
- var points = []
- for (var i = 0; i <= numSegments; i++) {
- var t = i / numSegments
- var angle = angleSrc + diff * t
- points.push({
- x: cx + r * Math.cos(angle),
- y: cy + r * Math.sin(angle)
- })
- }
- return points
- }
- /**
- * 粒子对象
- */
- function Particle(path, srcColor, dstColor, speed, baseSize) {
- this.path = path // [{x,y},...] 路径控制点
- this.srcColor = srcColor // {r,g,b}
- this.dstColor = dstColor // {r,g,b}(相克 与 srcColor 相同)
- this.progress = 0 // 0→1
- this.speed = speed
- this.baseSize = baseSize || 3
- this.x = path[0].x
- this.y = path[0].y
- this.currentColor = { r: srcColor.r, g: srcColor.g, b: srcColor.b }
- this.size = this.baseSize
- this.alive = true
- }
- Particle.prototype.update = function () {
- this.progress += this.speed
- if (this.progress >= 1) {
- this.alive = false
- return
- }
- var t = this.progress
- var totalSegs = this.path.length - 1
- var segIdx = Math.min(Math.floor(t * totalSegs), totalSegs - 1)
- var segFrac = (t * totalSegs) - segIdx
- var p0 = this.path[segIdx]
- var p1 = this.path[segIdx + 1]
- this.x = p0.x + (p1.x - p0.x) * segFrac
- this.y = p0.y + (p1.y - p0.y) * segFrac
- // 颜色插值
- this.currentColor = {
- r: Math.round(this.srcColor.r + (this.dstColor.r - this.srcColor.r) * t),
- g: Math.round(this.srcColor.g + (this.dstColor.g - this.srcColor.g) * t),
- b: Math.round(this.srcColor.b + (this.dstColor.b - this.srcColor.b) * t)
- }
- // 粒子衰减
- this.size = this.baseSize * (1 - t * 0.3)
- }
- /**
- * FlowAnimation - 五行能量粒子流动画控制器
- * 使用独立 Canvas 覆盖层绘制粒子
- *
- * @param {CanvasRenderingContext2D} ctx
- * @param {number} w - Canvas 宽度(逻辑像素)
- * @param {number} h - Canvas 高度(逻辑像素)
- * @param {Object} opts
- * @param {Array<{x:number,y:number}>} opts.outer - calcStarVertices 外顶点数组
- * @param {Array<{x:number,y:number}>} opts.inner - calcStarVertices 内顶点数组
- * @param {Array<string>} opts.dimOrder - sortedDimensions 的 code 数组
- */
- export function FlowAnimation(ctx, w, h, opts) {
- this._ctx = ctx
- this._w = w
- this._h = h
- this._cx = w / 2
- this._cy = h / 2
- this._outer = opts.outer
- this._inner = opts.inner
- this._dimOrder = opts.dimOrder
- this._particles = []
- this._timer = null
- this._running = false
- this._frameCount = 0
- this._buildShengPaths()
- this._buildKePaths()
- }
- /**
- * 构建相生路径(外圆弧线)
- * 顶点顺序 0→1→2→3→4→0
- */
- FlowAnimation.prototype._buildShengPaths = function () {
- this._shengPaths = []
- for (var i = 0; i < 5; i++) {
- var src = this._outer[i]
- var dst = this._outer[(i + 1) % 5]
- this._shengPaths.push({
- waypoints: arcWaypoints(src, dst, this._cx, this._cy, 6),
- srcColor: colorForDimCode(this._dimOrder[i]),
- dstColor: colorForDimCode(this._dimOrder[(i + 1) % 5])
- })
- }
- }
- /**
- * 构建相克路径(五角星边)
- * 相克索引对:0→2, 2→4, 4→1, 1→3, 3→0
- * 路径:outer[s] → inner[(s+1)%5] → outer[e]
- */
- FlowAnimation.prototype._buildKePaths = function () {
- this._kePaths = []
- var segments = [[0, 2], [2, 4], [4, 1], [1, 3], [3, 0]]
- for (var i = 0; i < segments.length; i++) {
- var s = segments[i][0]
- var e = segments[i][1]
- var srcColor = colorForDimCode(this._dimOrder[e])
- this._kePaths.push({
- waypoints: [
- this._outer[e],
- this._inner[(s + 1) % 5],
- this._outer[s]
- ],
- srcColor: srcColor,
- dstColor: srcColor // 相克粒子保持被克方颜色(能量被吸收)
- })
- }
- }
- /**
- * 启动连续动画循环
- */
- FlowAnimation.prototype.start = function () {
- if (this._running) return
- this._running = true
- this._loop()
- }
- /**
- * 停止动画循环
- */
- FlowAnimation.prototype.stop = function () {
- this._running = false
- if (this._timer) {
- clearTimeout(this._timer)
- this._timer = null
- }
- }
- /**
- * 刷新路径(当 Canvas 尺寸或维度排序发生变化时调用)
- */
- FlowAnimation.prototype.refreshPaths = function (opts) {
- if (opts) {
- if (opts.outer) this._outer = opts.outer
- if (opts.inner) this._inner = opts.inner
- if (opts.dimOrder) this._dimOrder = opts.dimOrder
- if (opts.w) {
- this._w = opts.w
- this._cx = opts.w / 2
- }
- if (opts.h) {
- this._h = opts.h
- this._cy = opts.h / 2
- }
- }
- this._buildShengPaths()
- this._buildKePaths()
- }
- FlowAnimation.prototype._loop = function () {
- if (!this._running) return
- var self = this
- this._frameCount++
- // 周期性发射粒子
- if (this._frameCount % 3 === 0) {
- this._emitSheng()
- }
- if (this._frameCount % 5 === 0) {
- this._emitKe()
- }
- // 更新所有粒子
- for (var i = this._particles.length - 1; i >= 0; i--) {
- this._particles[i].update()
- if (!this._particles[i].alive) {
- this._particles.splice(i, 1)
- }
- }
- // 绘制
- this._render()
- this._timer = setTimeout(function () {
- self._loop()
- }, 33) // ~30fps
- }
- /**
- * 发射一个相生粒子(随机选择段)
- */
- FlowAnimation.prototype._emitSheng = function () {
- if (this._shengPaths.length === 0) return
- var idx = Math.floor(Math.random() * this._shengPaths.length)
- var seg = this._shengPaths[idx]
- this._particles.push(new Particle(
- seg.waypoints,
- seg.srcColor,
- seg.dstColor,
- 0.012 + Math.random() * 0.01,
- 2.5 + Math.random() * 1.5
- ))
- }
- /**
- * 发射一个相克粒子(随机选择段)
- */
- FlowAnimation.prototype._emitKe = function () {
- if (this._kePaths.length === 0) return
- var idx = Math.floor(Math.random() * this._kePaths.length)
- var seg = this._kePaths[idx]
- this._particles.push(new Particle(
- seg.waypoints,
- seg.srcColor,
- seg.srcColor,
- 0.01 + Math.random() * 0.008,
- 3 + Math.random() * 1.5
- ))
- }
- /**
- * 渲染所有粒子到 Canvas
- */
- FlowAnimation.prototype._render = function () {
- var ctx = this._ctx
- ctx.clearRect(0, 0, this._w, this._h)
- for (var i = 0; i < this._particles.length; i++) {
- var p = this._particles[i]
- var c = p.currentColor
- var alpha = 1 - p.progress * 0.25
- var r = Math.max(p.size, 1.5)
- // 外层辉光
- ctx.beginPath()
- ctx.arc(p.x, p.y, r * 3.5, 0, Math.PI * 2)
- ctx.closePath()
- ctx.fillStyle = 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + (alpha * 0.1) + ')'
- ctx.fill()
- // 内层辉光
- ctx.beginPath()
- ctx.arc(p.x, p.y, r * 1.8, 0, Math.PI * 2)
- ctx.closePath()
- ctx.fillStyle = 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + (alpha * 0.35) + ')'
- ctx.fill()
- // 核心圆点
- ctx.beginPath()
- ctx.arc(p.x, p.y, r, 0, Math.PI * 2)
- ctx.closePath()
- ctx.fillStyle = 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + alpha + ')'
- ctx.fill()
- }
- }
|