/** * wuxing-sandbox-helpers/render.js * 五角星 Canvas 渲染(纯函数,无状态) */ /** * 计算五角星顶点坐标 * 外顶点:5 个,72° 等分,起始角度 -90°(正上方) * 内顶点:5 个,偏移 36°,半径 = 外半径 × 0.382 * @param {number} cx - 中心 x * @param {number} cy - 中心 y * @param {number} outerR - 外半径 * @returns {{outer:Array<{x:number,y:number}>,inner:Array<{x:number,y:number}>}} */ export function calcStarVertices(cx, cy, outerR) { var innerR = outerR * 0.382 var outer = [] var inner = [] for (var i = 0; i < 5; i++) { var angle = -Math.PI / 2 + i * 2 * Math.PI / 5 outer.push({ x: cx + outerR * Math.cos(angle), y: cy + outerR * Math.sin(angle) }) var innerAngle = angle + Math.PI / 5 inner.push({ x: cx + innerR * Math.cos(innerAngle), y: cy + innerR * Math.sin(innerAngle) }) } return { outer: outer, inner: inner } } /** 五色配置(与 CSS 一致,ts 顺序 = [body, mind, wisdom, action, wealth]) */ var COLORS = [ { r: 141, g: 110, b: 99 }, // 土·身 (body) { r: 255, g: 107, b: 53 }, // 火·心 (mind) { r: 255, g: 215, b: 0 }, // 金·智 (wisdom) { r: 76, g: 175, b: 80 }, // 木·行 (action) { r: 66, g: 165, b: 245 } // 水·富 (wealth) ] /** * 绘制完整五角星图(含填充、描边、太极) * @param {CanvasRenderingContext2D} ctx * @param {number} w - canvas 宽度 * @param {number} h - canvas 高度 * @param {Array} ts - 5 维归一化值 (0~1),顺序 [body,mind,wisdom,action,wealth] * @param {Object} [options] * @param {boolean} [options.showTaiji=true] - 是否绘制太极 * @param {number} [options.familyEnergy] - 家庭能量值(显示在阳面) * @param {number} [options.personalEnergy] - 个人能量值(显示在阴面) */ export function renderStar(ctx, w, h, ts, options) { if (!ctx || !w || !h) return var opts = options || {} var showTaiji = opts.showTaiji !== false var familyEnergy = opts.familyEnergy var personalEnergy = opts.personalEnergy // 清空 ctx.clearRect(0, 0, w, h) // 星型几何 var cx = w / 2 var cy = h / 2 var outerR = Math.min(w, h) * 0.46 var vertices = calcStarVertices(cx, cy, outerR) var outer = vertices.outer var inner = vertices.inner // 绘制每个星臂 for (var i = 0; i < 5; i++) { var outerIdx = (i + 4) % 5 var iBefore = (i + 3) % 5 var iAfter = (i + 4) % 5 var t = ts[i] var c = COLORS[i] // 星臂 polygon: center, inner[iBefore], outer[outerIdx], inner[iAfter] var polygon = [ { x: cx, y: cy }, inner[iBefore], outer[outerIdx], inner[iAfter] ] // --- 背景填充(低透明度) --- ctx.beginPath() ctx.moveTo(polygon[0].x, polygon[0].y) for (var p = 1; p < polygon.length; p++) { ctx.lineTo(polygon[p].x, polygon[p].y) } ctx.closePath() ctx.fillStyle = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', 0.08)' ctx.fill() // --- 渐变填充(t 控制色彩饱和度,覆盖整个星臂) --- if (t > 0.01) { ctx.save() ctx.beginPath() ctx.moveTo(polygon[0].x, polygon[0].y) for (var fp = 1; fp < polygon.length; fp++) { ctx.lineTo(polygon[fp].x, polygon[fp].y) } ctx.closePath() ctx.clip() var grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(outerR, 1)) grad.addColorStop(0, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.85 * t) + ')') grad.addColorStop(0.5, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.35 * t) + ')') grad.addColorStop(1, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.02 * t) + ')') ctx.fillStyle = grad ctx.fillRect(0, 0, w, h) ctx.restore() } // --- 星臂描边 --- ctx.beginPath() ctx.moveTo(polygon[0].x, polygon[0].y) for (var sp = 1; sp < polygon.length; sp++) { ctx.lineTo(polygon[sp].x, polygon[sp].y) } ctx.closePath() ctx.strokeStyle = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', 0.4)' ctx.lineWidth = 1.5 ctx.stroke() } // --- 五角星外轮廓(10 个顶点依次连接) --- ctx.beginPath() ctx.moveTo(outer[0].x, outer[0].y) ctx.lineTo(inner[0].x, inner[0].y) ctx.lineTo(outer[1].x, outer[1].y) ctx.lineTo(inner[1].x, inner[1].y) ctx.lineTo(outer[2].x, outer[2].y) ctx.lineTo(inner[2].x, inner[2].y) ctx.lineTo(outer[3].x, outer[3].y) ctx.lineTo(inner[3].x, inner[3].y) ctx.lineTo(outer[4].x, outer[4].y) ctx.lineTo(inner[4].x, inner[4].y) ctx.closePath() ctx.strokeStyle = 'rgba(143, 197, 232, 0.50)' ctx.lineWidth = 2 ctx.stroke() // --- 内部五边形(5 个内顶点) --- ctx.beginPath() ctx.moveTo(inner[0].x, inner[0].y) ctx.lineTo(inner[1].x, inner[1].y) ctx.lineTo(inner[2].x, inner[2].y) ctx.lineTo(inner[3].x, inner[3].y) ctx.lineTo(inner[4].x, inner[4].y) ctx.closePath() ctx.strokeStyle = 'rgba(143, 197, 232, 0.35)' ctx.lineWidth = 1.5 ctx.stroke() // --- 中心太极阴阳鱼(在 Canvas 最上层绘制) --- if (showTaiji) { var taijiR = outerR * 0.30 drawTaiji(ctx, cx, cy, taijiR, familyEnergy, personalEnergy) } } /** * 绘制太极阴阳鱼 * 阳(亮面·白)= 家庭能量,阴(暗面·黑)= 个人能量 * 数值显示在两个鱼眼上 * @param {CanvasRenderingContext2D} ctx * @param {number} cx - 中心 x * @param {number} cy - 中心 y * @param {number} r - 半径 * @param {number} [familyEnergy] - 家庭能量值 * @param {number} [personalEnergy] - 个人能量值 */ export function drawTaiji(ctx, cx, cy, r, familyEnergy, personalEnergy) { ctx.save() // 1. 外圆背景(白色底) ctx.beginPath() ctx.arc(cx, cy, r, 0, Math.PI * 2) ctx.closePath() ctx.fillStyle = '#fff' ctx.fill() ctx.strokeStyle = 'rgba(0,0,0,0.15)' ctx.lineWidth = 1 ctx.stroke() // 2. 左半边(阴,黑色) ctx.beginPath() ctx.moveTo(cx, cy) ctx.arc(cx, cy, r, Math.PI / 2, -Math.PI / 2) ctx.closePath() ctx.fillStyle = '#000' ctx.fill() // 3. 上半小圆左半弧(白色,在黑色半圆中) ctx.beginPath() ctx.moveTo(cx, cy - r) ctx.arc(cx, cy - r / 2, r / 2, Math.PI / 2, -Math.PI / 2) ctx.closePath() ctx.fillStyle = '#fff' ctx.fill() // 4. 下半小圆右半弧(黑色,在白色半圆中) ctx.beginPath() ctx.moveTo(cx, cy + r) ctx.arc(cx, cy + r / 2, r / 2, -Math.PI / 2, Math.PI / 2) ctx.closePath() ctx.fillStyle = '#000' ctx.fill() // 鱼眼半径(够大,容纳数值文字) var eyeR = r * 0.16 // 5. 阳鱼眼(白色区域中的黑点 → 写白色文字) ctx.beginPath() ctx.arc(cx, cy + r / 2, eyeR, 0, Math.PI * 2) ctx.closePath() ctx.fillStyle = '#000' ctx.fill() // 6. 阴鱼眼(黑色区域中的白点 → 写黑色文字) ctx.beginPath() ctx.arc(cx, cy - r / 2, eyeR, 0, Math.PI * 2) ctx.closePath() ctx.fillStyle = '#fff' ctx.fill() // 7. 阳眼文字(黑底上写白色)— 家庭能量 var familyVal = (familyEnergy != null && !isNaN(familyEnergy)) ? Math.round(familyEnergy) : '--' var eyeFontSize = Math.round(eyeR * 1.2) ctx.font = 'bold ' + eyeFontSize + 'px sans-serif' ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillStyle = '#fff' ctx.fillText(familyVal + '', cx, cy + r / 2) // 8. 阴眼文字(白底上写黑色)— 个人能量 var personalVal = (personalEnergy != null && !isNaN(personalEnergy)) ? Math.round(personalEnergy) : '--' ctx.fillStyle = '#000' ctx.fillText(personalVal + '', cx, cy - r / 2) ctx.restore() }