render.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /**
  2. * wuxing-sandbox-helpers/render.js
  3. * 五角星 Canvas 渲染(纯函数,无状态)
  4. */
  5. /**
  6. * 计算五角星顶点坐标
  7. * 外顶点:5 个,72° 等分,起始角度 -90°(正上方)
  8. * 内顶点:5 个,偏移 36°,半径 = 外半径 × 0.382
  9. * @param {number} cx - 中心 x
  10. * @param {number} cy - 中心 y
  11. * @param {number} outerR - 外半径
  12. * @returns {{outer:Array<{x:number,y:number}>,inner:Array<{x:number,y:number}>}}
  13. */
  14. export function calcStarVertices(cx, cy, outerR) {
  15. var innerR = outerR * 0.382
  16. var outer = []
  17. var inner = []
  18. for (var i = 0; i < 5; i++) {
  19. var angle = -Math.PI / 2 + i * 2 * Math.PI / 5
  20. outer.push({
  21. x: cx + outerR * Math.cos(angle),
  22. y: cy + outerR * Math.sin(angle)
  23. })
  24. var innerAngle = angle + Math.PI / 5
  25. inner.push({
  26. x: cx + innerR * Math.cos(innerAngle),
  27. y: cy + innerR * Math.sin(innerAngle)
  28. })
  29. }
  30. return { outer: outer, inner: inner }
  31. }
  32. /** 五色配置(与 CSS 一致,ts 顺序 = [body, mind, wisdom, action, wealth]) */
  33. var COLORS = [
  34. { r: 141, g: 110, b: 99 }, // 土·身 (body)
  35. { r: 255, g: 107, b: 53 }, // 火·心 (mind)
  36. { r: 255, g: 215, b: 0 }, // 金·智 (wisdom)
  37. { r: 76, g: 175, b: 80 }, // 木·行 (action)
  38. { r: 66, g: 165, b: 245 } // 水·富 (wealth)
  39. ]
  40. /**
  41. * 绘制完整五角星图(含填充、描边、太极)
  42. * @param {CanvasRenderingContext2D} ctx
  43. * @param {number} w - canvas 宽度
  44. * @param {number} h - canvas 高度
  45. * @param {Array<number>} ts - 5 维归一化值 (0~1),顺序 [body,mind,wisdom,action,wealth]
  46. * @param {Object} [options]
  47. * @param {boolean} [options.showTaiji=true] - 是否绘制太极
  48. * @param {number} [options.familyEnergy] - 家庭能量值(显示在阳面)
  49. * @param {number} [options.personalEnergy] - 个人能量值(显示在阴面)
  50. */
  51. export function renderStar(ctx, w, h, ts, options) {
  52. if (!ctx || !w || !h) return
  53. var opts = options || {}
  54. var showTaiji = opts.showTaiji !== false
  55. var familyEnergy = opts.familyEnergy
  56. var personalEnergy = opts.personalEnergy
  57. // 清空
  58. ctx.clearRect(0, 0, w, h)
  59. // 星型几何
  60. var cx = w / 2
  61. var cy = h / 2
  62. var outerR = Math.min(w, h) * 0.46
  63. var vertices = calcStarVertices(cx, cy, outerR)
  64. var outer = vertices.outer
  65. var inner = vertices.inner
  66. // 绘制每个星臂
  67. for (var i = 0; i < 5; i++) {
  68. var outerIdx = (i + 4) % 5
  69. var iBefore = (i + 3) % 5
  70. var iAfter = (i + 4) % 5
  71. var t = ts[i]
  72. var c = COLORS[i]
  73. // 星臂 polygon: center, inner[iBefore], outer[outerIdx], inner[iAfter]
  74. var polygon = [
  75. { x: cx, y: cy },
  76. inner[iBefore],
  77. outer[outerIdx],
  78. inner[iAfter]
  79. ]
  80. // --- 背景填充(低透明度) ---
  81. ctx.beginPath()
  82. ctx.moveTo(polygon[0].x, polygon[0].y)
  83. for (var p = 1; p < polygon.length; p++) {
  84. ctx.lineTo(polygon[p].x, polygon[p].y)
  85. }
  86. ctx.closePath()
  87. ctx.fillStyle = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', 0.08)'
  88. ctx.fill()
  89. // --- 渐变填充(t 控制色彩饱和度,覆盖整个星臂) ---
  90. if (t > 0.01) {
  91. ctx.save()
  92. ctx.beginPath()
  93. ctx.moveTo(polygon[0].x, polygon[0].y)
  94. for (var fp = 1; fp < polygon.length; fp++) {
  95. ctx.lineTo(polygon[fp].x, polygon[fp].y)
  96. }
  97. ctx.closePath()
  98. ctx.clip()
  99. var grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(outerR, 1))
  100. grad.addColorStop(0, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.85 * t) + ')')
  101. grad.addColorStop(0.5, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.35 * t) + ')')
  102. grad.addColorStop(1, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.02 * t) + ')')
  103. ctx.fillStyle = grad
  104. ctx.fillRect(0, 0, w, h)
  105. ctx.restore()
  106. }
  107. // --- 星臂描边 ---
  108. ctx.beginPath()
  109. ctx.moveTo(polygon[0].x, polygon[0].y)
  110. for (var sp = 1; sp < polygon.length; sp++) {
  111. ctx.lineTo(polygon[sp].x, polygon[sp].y)
  112. }
  113. ctx.closePath()
  114. ctx.strokeStyle = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', 0.4)'
  115. ctx.lineWidth = 1.5
  116. ctx.stroke()
  117. }
  118. // --- 五角星外轮廓(10 个顶点依次连接) ---
  119. ctx.beginPath()
  120. ctx.moveTo(outer[0].x, outer[0].y)
  121. ctx.lineTo(inner[0].x, inner[0].y)
  122. ctx.lineTo(outer[1].x, outer[1].y)
  123. ctx.lineTo(inner[1].x, inner[1].y)
  124. ctx.lineTo(outer[2].x, outer[2].y)
  125. ctx.lineTo(inner[2].x, inner[2].y)
  126. ctx.lineTo(outer[3].x, outer[3].y)
  127. ctx.lineTo(inner[3].x, inner[3].y)
  128. ctx.lineTo(outer[4].x, outer[4].y)
  129. ctx.lineTo(inner[4].x, inner[4].y)
  130. ctx.closePath()
  131. ctx.strokeStyle = 'rgba(143, 197, 232, 0.50)'
  132. ctx.lineWidth = 2
  133. ctx.stroke()
  134. // --- 内部五边形(5 个内顶点) ---
  135. ctx.beginPath()
  136. ctx.moveTo(inner[0].x, inner[0].y)
  137. ctx.lineTo(inner[1].x, inner[1].y)
  138. ctx.lineTo(inner[2].x, inner[2].y)
  139. ctx.lineTo(inner[3].x, inner[3].y)
  140. ctx.lineTo(inner[4].x, inner[4].y)
  141. ctx.closePath()
  142. ctx.strokeStyle = 'rgba(143, 197, 232, 0.35)'
  143. ctx.lineWidth = 1.5
  144. ctx.stroke()
  145. // --- 中心太极阴阳鱼(在 Canvas 最上层绘制) ---
  146. if (showTaiji) {
  147. var taijiR = outerR * 0.30
  148. drawTaiji(ctx, cx, cy, taijiR, familyEnergy, personalEnergy)
  149. }
  150. }
  151. /**
  152. * 绘制太极阴阳鱼
  153. * 阳(亮面·白)= 家庭能量,阴(暗面·黑)= 个人能量
  154. * 数值显示在两个鱼眼上
  155. * @param {CanvasRenderingContext2D} ctx
  156. * @param {number} cx - 中心 x
  157. * @param {number} cy - 中心 y
  158. * @param {number} r - 半径
  159. * @param {number} [familyEnergy] - 家庭能量值
  160. * @param {number} [personalEnergy] - 个人能量值
  161. */
  162. export function drawTaiji(ctx, cx, cy, r, familyEnergy, personalEnergy) {
  163. ctx.save()
  164. // 1. 外圆背景(白色底)
  165. ctx.beginPath()
  166. ctx.arc(cx, cy, r, 0, Math.PI * 2)
  167. ctx.closePath()
  168. ctx.fillStyle = '#fff'
  169. ctx.fill()
  170. ctx.strokeStyle = 'rgba(0,0,0,0.15)'
  171. ctx.lineWidth = 1
  172. ctx.stroke()
  173. // 2. 左半边(阴,黑色)
  174. ctx.beginPath()
  175. ctx.moveTo(cx, cy)
  176. ctx.arc(cx, cy, r, Math.PI / 2, -Math.PI / 2)
  177. ctx.closePath()
  178. ctx.fillStyle = '#000'
  179. ctx.fill()
  180. // 3. 上半小圆左半弧(白色,在黑色半圆中)
  181. ctx.beginPath()
  182. ctx.moveTo(cx, cy - r)
  183. ctx.arc(cx, cy - r / 2, r / 2, Math.PI / 2, -Math.PI / 2)
  184. ctx.closePath()
  185. ctx.fillStyle = '#fff'
  186. ctx.fill()
  187. // 4. 下半小圆右半弧(黑色,在白色半圆中)
  188. ctx.beginPath()
  189. ctx.moveTo(cx, cy + r)
  190. ctx.arc(cx, cy + r / 2, r / 2, -Math.PI / 2, Math.PI / 2)
  191. ctx.closePath()
  192. ctx.fillStyle = '#000'
  193. ctx.fill()
  194. // 鱼眼半径(够大,容纳数值文字)
  195. var eyeR = r * 0.16
  196. // 5. 阳鱼眼(白色区域中的黑点 → 写白色文字)
  197. ctx.beginPath()
  198. ctx.arc(cx, cy + r / 2, eyeR, 0, Math.PI * 2)
  199. ctx.closePath()
  200. ctx.fillStyle = '#000'
  201. ctx.fill()
  202. // 6. 阴鱼眼(黑色区域中的白点 → 写黑色文字)
  203. ctx.beginPath()
  204. ctx.arc(cx, cy - r / 2, eyeR, 0, Math.PI * 2)
  205. ctx.closePath()
  206. ctx.fillStyle = '#fff'
  207. ctx.fill()
  208. // 7. 阳眼文字(黑底上写白色)— 家庭能量
  209. var familyVal = (familyEnergy != null && !isNaN(familyEnergy)) ? Math.round(familyEnergy) : '--'
  210. var eyeFontSize = Math.round(eyeR * 1.2)
  211. ctx.font = 'bold ' + eyeFontSize + 'px sans-serif'
  212. ctx.textAlign = 'center'
  213. ctx.textBaseline = 'middle'
  214. ctx.fillStyle = '#fff'
  215. ctx.fillText(familyVal + '', cx, cy + r / 2)
  216. // 8. 阴眼文字(白底上写黑色)— 个人能量
  217. var personalVal = (personalEnergy != null && !isNaN(personalEnergy)) ? Math.round(personalEnergy) : '--'
  218. ctx.fillStyle = '#000'
  219. ctx.fillText(personalVal + '', cx, cy - r / 2)
  220. ctx.restore()
  221. }