/** * 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 一致) */ var COLORS = [ { r: 76, g: 175, b: 80 }, // 木·身 { r: 255, g: 107, b: 53 }, // 火·心 { r: 141, g: 110, b: 99 }, // 土·智 { r: 255, g: 215, b: 0 }, // 金·行 { r: 66, g: 165, b: 245 } // 水·富 ] /** * 绘制完整五角星图(含填充、描边、太极) * @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] - 是否绘制太极 */ export function renderStar(ctx, w, h, ts, options) { if (!ctx || !w || !h) return var opts = options || {} var showTaiji = opts.showTaiji !== false // 清空 ctx.clearRect(0, 0, w, h) // 星型几何 var cx = w / 2 var cy = h / 2 var outerR = Math.min(w, h) * 0.42 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.22 drawTaiji(ctx, cx, cy, taijiR) } } /** * 绘制太极阴阳鱼 * @param {CanvasRenderingContext2D} ctx * @param {number} cx - 中心 x * @param {number} cy - 中心 y * @param {number} r - 半径 */ export function drawTaiji(ctx, cx, cy, r) { var yangColor = 'rgba(91, 155, 213, 0.35)' var yinColor = 'rgba(26, 42, 74, 0.75)' var strokeColor = 'rgba(143, 197, 232, 0.2)' var yangEyeColor = 'rgba(26, 42, 74, 0.85)' var yinEyeColor = 'rgba(143, 197, 232, 0.75)' ctx.save() // 1. 外圆背景及边框 ctx.beginPath() ctx.arc(cx, cy, r, 0, Math.PI * 2) ctx.closePath() ctx.fillStyle = yangColor ctx.fill() ctx.strokeStyle = strokeColor 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 = yinColor 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 = yangColor 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 = yinColor ctx.fill() // 5. 阳眼(深色点,上方) ctx.beginPath() ctx.arc(cx, cy - r / 2, r * 0.12, 0, Math.PI * 2) ctx.closePath() ctx.fillStyle = yangEyeColor ctx.fill() // 6. 阴眼(浅色点,下方) ctx.beginPath() ctx.arc(cx, cy + r / 2, r * 0.12, 0, Math.PI * 2) ctx.closePath() ctx.fillStyle = yinEyeColor ctx.fill() ctx.restore() // 7. 文字标签 ctx.save() var fontSize = Math.round(r * 0.3) ctx.font = 'bold ' + fontSize + 'px sans-serif' ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillStyle = 'rgba(143, 197, 232, 0.6)' ctx.fillText('家', cx - r * 0.3, cy + r * 0.15) ctx.fillStyle = 'rgba(91, 155, 213, 0.8)' ctx.fillText('个', cx + r * 0.3, cy - r * 0.15) ctx.restore() }