render.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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 一致) */
  33. var COLORS = [
  34. { r: 76, g: 175, b: 80 }, // 木·身
  35. { r: 255, g: 107, b: 53 }, // 火·心
  36. { r: 141, g: 110, b: 99 }, // 土·智
  37. { r: 255, g: 215, b: 0 }, // 金·行
  38. { r: 66, g: 165, b: 245 } // 水·富
  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. */
  49. export function renderStar(ctx, w, h, ts, options) {
  50. if (!ctx || !w || !h) return
  51. var opts = options || {}
  52. var showTaiji = opts.showTaiji !== false
  53. // 清空
  54. ctx.clearRect(0, 0, w, h)
  55. // 星型几何
  56. var cx = w / 2
  57. var cy = h / 2
  58. var outerR = Math.min(w, h) * 0.42
  59. var vertices = calcStarVertices(cx, cy, outerR)
  60. var outer = vertices.outer
  61. var inner = vertices.inner
  62. // 绘制每个星臂
  63. for (var i = 0; i < 5; i++) {
  64. var outerIdx = (i + 4) % 5
  65. var iBefore = (i + 3) % 5
  66. var iAfter = (i + 4) % 5
  67. var t = ts[i]
  68. var c = COLORS[i]
  69. // 星臂 polygon: center, inner[iBefore], outer[outerIdx], inner[iAfter]
  70. var polygon = [
  71. { x: cx, y: cy },
  72. inner[iBefore],
  73. outer[outerIdx],
  74. inner[iAfter]
  75. ]
  76. // --- 背景填充(低透明度) ---
  77. ctx.beginPath()
  78. ctx.moveTo(polygon[0].x, polygon[0].y)
  79. for (var p = 1; p < polygon.length; p++) {
  80. ctx.lineTo(polygon[p].x, polygon[p].y)
  81. }
  82. ctx.closePath()
  83. ctx.fillStyle = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', 0.08)'
  84. ctx.fill()
  85. // --- 渐变填充(t 控制色彩饱和度,覆盖整个星臂) ---
  86. if (t > 0.01) {
  87. ctx.save()
  88. ctx.beginPath()
  89. ctx.moveTo(polygon[0].x, polygon[0].y)
  90. for (var fp = 1; fp < polygon.length; fp++) {
  91. ctx.lineTo(polygon[fp].x, polygon[fp].y)
  92. }
  93. ctx.closePath()
  94. ctx.clip()
  95. var grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(outerR, 1))
  96. grad.addColorStop(0, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.85 * t) + ')')
  97. grad.addColorStop(0.5, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.35 * t) + ')')
  98. grad.addColorStop(1, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.02 * t) + ')')
  99. ctx.fillStyle = grad
  100. ctx.fillRect(0, 0, w, h)
  101. ctx.restore()
  102. }
  103. // --- 星臂描边 ---
  104. ctx.beginPath()
  105. ctx.moveTo(polygon[0].x, polygon[0].y)
  106. for (var sp = 1; sp < polygon.length; sp++) {
  107. ctx.lineTo(polygon[sp].x, polygon[sp].y)
  108. }
  109. ctx.closePath()
  110. ctx.strokeStyle = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', 0.4)'
  111. ctx.lineWidth = 1.5
  112. ctx.stroke()
  113. }
  114. // --- 五角星外轮廓(10 个顶点依次连接) ---
  115. ctx.beginPath()
  116. ctx.moveTo(outer[0].x, outer[0].y)
  117. ctx.lineTo(inner[0].x, inner[0].y)
  118. ctx.lineTo(outer[1].x, outer[1].y)
  119. ctx.lineTo(inner[1].x, inner[1].y)
  120. ctx.lineTo(outer[2].x, outer[2].y)
  121. ctx.lineTo(inner[2].x, inner[2].y)
  122. ctx.lineTo(outer[3].x, outer[3].y)
  123. ctx.lineTo(inner[3].x, inner[3].y)
  124. ctx.lineTo(outer[4].x, outer[4].y)
  125. ctx.lineTo(inner[4].x, inner[4].y)
  126. ctx.closePath()
  127. ctx.strokeStyle = 'rgba(143, 197, 232, 0.50)'
  128. ctx.lineWidth = 2
  129. ctx.stroke()
  130. // --- 内部五边形(5 个内顶点) ---
  131. ctx.beginPath()
  132. ctx.moveTo(inner[0].x, inner[0].y)
  133. ctx.lineTo(inner[1].x, inner[1].y)
  134. ctx.lineTo(inner[2].x, inner[2].y)
  135. ctx.lineTo(inner[3].x, inner[3].y)
  136. ctx.lineTo(inner[4].x, inner[4].y)
  137. ctx.closePath()
  138. ctx.strokeStyle = 'rgba(143, 197, 232, 0.35)'
  139. ctx.lineWidth = 1.5
  140. ctx.stroke()
  141. // --- 中心太极阴阳鱼(在 Canvas 最上层绘制) ---
  142. if (showTaiji) {
  143. var taijiR = outerR * 0.22
  144. drawTaiji(ctx, cx, cy, taijiR)
  145. }
  146. }
  147. /**
  148. * 绘制太极阴阳鱼
  149. * @param {CanvasRenderingContext2D} ctx
  150. * @param {number} cx - 中心 x
  151. * @param {number} cy - 中心 y
  152. * @param {number} r - 半径
  153. */
  154. export function drawTaiji(ctx, cx, cy, r) {
  155. var yangColor = 'rgba(91, 155, 213, 0.35)'
  156. var yinColor = 'rgba(26, 42, 74, 0.75)'
  157. var strokeColor = 'rgba(143, 197, 232, 0.2)'
  158. var yangEyeColor = 'rgba(26, 42, 74, 0.85)'
  159. var yinEyeColor = 'rgba(143, 197, 232, 0.75)'
  160. ctx.save()
  161. // 1. 外圆背景及边框
  162. ctx.beginPath()
  163. ctx.arc(cx, cy, r, 0, Math.PI * 2)
  164. ctx.closePath()
  165. ctx.fillStyle = yangColor
  166. ctx.fill()
  167. ctx.strokeStyle = strokeColor
  168. ctx.lineWidth = 1
  169. ctx.stroke()
  170. // 2. 左半边:阴(深色)
  171. ctx.beginPath()
  172. ctx.moveTo(cx, cy)
  173. ctx.arc(cx, cy, r, Math.PI / 2, -Math.PI / 2)
  174. ctx.closePath()
  175. ctx.fillStyle = yinColor
  176. ctx.fill()
  177. // 3. 上半小圆左半弧:阳色
  178. ctx.beginPath()
  179. ctx.moveTo(cx, cy - r)
  180. ctx.arc(cx, cy - r / 2, r / 2, Math.PI / 2, -Math.PI / 2)
  181. ctx.closePath()
  182. ctx.fillStyle = yangColor
  183. ctx.fill()
  184. // 4. 下半小圆右半弧:阴色
  185. ctx.beginPath()
  186. ctx.moveTo(cx, cy + r)
  187. ctx.arc(cx, cy + r / 2, r / 2, -Math.PI / 2, Math.PI / 2)
  188. ctx.closePath()
  189. ctx.fillStyle = yinColor
  190. ctx.fill()
  191. // 5. 阳眼(深色点,上方)
  192. ctx.beginPath()
  193. ctx.arc(cx, cy - r / 2, r * 0.12, 0, Math.PI * 2)
  194. ctx.closePath()
  195. ctx.fillStyle = yangEyeColor
  196. ctx.fill()
  197. // 6. 阴眼(浅色点,下方)
  198. ctx.beginPath()
  199. ctx.arc(cx, cy + r / 2, r * 0.12, 0, Math.PI * 2)
  200. ctx.closePath()
  201. ctx.fillStyle = yinEyeColor
  202. ctx.fill()
  203. ctx.restore()
  204. // 7. 文字标签
  205. ctx.save()
  206. var fontSize = Math.round(r * 0.3)
  207. ctx.font = 'bold ' + fontSize + 'px sans-serif'
  208. ctx.textAlign = 'center'
  209. ctx.textBaseline = 'middle'
  210. ctx.fillStyle = 'rgba(143, 197, 232, 0.6)'
  211. ctx.fillText('家', cx - r * 0.3, cy + r * 0.15)
  212. ctx.fillStyle = 'rgba(91, 155, 213, 0.8)'
  213. ctx.fillText('个', cx + r * 0.3, cy - r * 0.15)
  214. ctx.restore()
  215. }