/** * 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} 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() } }