@biunow/agent-biuart 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/bin/server.mjs +575 -0
  2. package/data/styles.json +533 -0
  3. package/engine/audit_clip.py +46 -0
  4. package/engine/clip.html +24 -0
  5. package/engine/clip.js +104 -0
  6. package/engine/clip_schema.json +19 -0
  7. package/engine/clips/t1_3b1b.js +203 -0
  8. package/engine/clips/t2_keynote_ui.js +347 -0
  9. package/engine/clips/t3_finance_chart.js +206 -0
  10. package/engine/clips/y1_kurzgesagt.js +183 -0
  11. package/engine/clips/y2_vox.js +150 -0
  12. package/engine/clips/y3_whiteboard.js +209 -0
  13. package/engine/clips/y4_storytime.js +194 -0
  14. package/engine/clips/y5_kinetic_type.js +134 -0
  15. package/engine/compare.py +19 -0
  16. package/engine/demos/_shared/hero/c1_0.png +0 -0
  17. package/engine/demos/_shared/hero/c1_1.png +0 -0
  18. package/engine/demos/_shared/hero/c1_2.png +0 -0
  19. package/engine/demos/_shared/hero/c3_line.png +0 -0
  20. package/engine/demos/_shared/hero.js +96 -0
  21. package/engine/demos/long_scroll/eras.js +23 -0
  22. package/engine/demos/long_scroll/frames/8bit_act/01.png +0 -0
  23. package/engine/demos/long_scroll/frames/8bit_act/02.png +0 -0
  24. package/engine/demos/long_scroll/frames/8bit_act/03.png +0 -0
  25. package/engine/demos/long_scroll/frames/8bit_act/04.png +0 -0
  26. package/engine/demos/long_scroll/frames/8bit_act/05.png +0 -0
  27. package/engine/demos/long_scroll/frames/8bit_act/06.png +0 -0
  28. package/engine/demos/long_scroll/frames/8bit_act/07.png +0 -0
  29. package/engine/demos/long_scroll/frames/8bit_act/08.png +0 -0
  30. package/engine/demos/long_scroll/frames/8bit_act/meta.json +69 -0
  31. package/engine/demos/long_scroll/frames/8bit_walk/01.png +0 -0
  32. package/engine/demos/long_scroll/frames/8bit_walk/02.png +0 -0
  33. package/engine/demos/long_scroll/frames/8bit_walk/03.png +0 -0
  34. package/engine/demos/long_scroll/frames/8bit_walk/04.png +0 -0
  35. package/engine/demos/long_scroll/frames/8bit_walk/05.png +0 -0
  36. package/engine/demos/long_scroll/frames/8bit_walk/06.png +0 -0
  37. package/engine/demos/long_scroll/frames/8bit_walk/07.png +0 -0
  38. package/engine/demos/long_scroll/frames/8bit_walk/08.png +0 -0
  39. package/engine/demos/long_scroll/frames/8bit_walk/meta.json +69 -0
  40. package/engine/demos/long_scroll/frames/egypt_dodge/01.png +0 -0
  41. package/engine/demos/long_scroll/frames/egypt_dodge/02.png +0 -0
  42. package/engine/demos/long_scroll/frames/egypt_dodge/03.png +0 -0
  43. package/engine/demos/long_scroll/frames/egypt_dodge/04.png +0 -0
  44. package/engine/demos/long_scroll/frames/egypt_dodge/05.png +0 -0
  45. package/engine/demos/long_scroll/frames/egypt_dodge/06.png +0 -0
  46. package/engine/demos/long_scroll/frames/egypt_dodge/07.png +0 -0
  47. package/engine/demos/long_scroll/frames/egypt_dodge/08.png +0 -0
  48. package/engine/demos/long_scroll/frames/egypt_dodge/meta.json +69 -0
  49. package/engine/demos/long_scroll/frames/egypt_walk8/01.png +0 -0
  50. package/engine/demos/long_scroll/frames/egypt_walk8/02.png +0 -0
  51. package/engine/demos/long_scroll/frames/egypt_walk8/03.png +0 -0
  52. package/engine/demos/long_scroll/frames/egypt_walk8/04.png +0 -0
  53. package/engine/demos/long_scroll/frames/egypt_walk8/05.png +0 -0
  54. package/engine/demos/long_scroll/frames/egypt_walk8/06.png +0 -0
  55. package/engine/demos/long_scroll/frames/egypt_walk8/07.png +0 -0
  56. package/engine/demos/long_scroll/frames/egypt_walk8/08.png +0 -0
  57. package/engine/demos/long_scroll/frames/egypt_walk8/meta.json +69 -0
  58. package/engine/demos/long_scroll/frames/monet_throw/01.png +0 -0
  59. package/engine/demos/long_scroll/frames/monet_throw/02.png +0 -0
  60. package/engine/demos/long_scroll/frames/monet_throw/03.png +0 -0
  61. package/engine/demos/long_scroll/frames/monet_throw/04.png +0 -0
  62. package/engine/demos/long_scroll/frames/monet_throw/05.png +0 -0
  63. package/engine/demos/long_scroll/frames/monet_throw/06.png +0 -0
  64. package/engine/demos/long_scroll/frames/monet_throw/07.png +0 -0
  65. package/engine/demos/long_scroll/frames/monet_throw/08.png +0 -0
  66. package/engine/demos/long_scroll/frames/monet_throw/meta.json +71 -0
  67. package/engine/demos/long_scroll/frames/monet_walk/01.png +0 -0
  68. package/engine/demos/long_scroll/frames/monet_walk/02.png +0 -0
  69. package/engine/demos/long_scroll/frames/monet_walk/03.png +0 -0
  70. package/engine/demos/long_scroll/frames/monet_walk/04.png +0 -0
  71. package/engine/demos/long_scroll/frames/monet_walk/05.png +0 -0
  72. package/engine/demos/long_scroll/frames/monet_walk/06.png +0 -0
  73. package/engine/demos/long_scroll/frames/monet_walk/07.png +0 -0
  74. package/engine/demos/long_scroll/frames/monet_walk/08.png +0 -0
  75. package/engine/demos/long_scroll/frames/monet_walk/meta.json +71 -0
  76. package/engine/demos/long_scroll/lib/acts.js +87 -0
  77. package/engine/demos/long_scroll/lib/crossing.js +463 -0
  78. package/engine/demos/long_scroll/lib/world.js +143 -0
  79. package/engine/demos/long_scroll/lib/xing2_ding.js +42 -0
  80. package/engine/demos/long_scroll/lib/xing2_jia.js +114 -0
  81. package/engine/demos/long_scroll/lib/xing2_yi.js +133 -0
  82. package/engine/demos/long_scroll/segments/02_egypt.js +234 -0
  83. package/engine/demos/long_scroll/segments/14_8bit.js +118 -0
  84. package/engine/demos/long_scroll/segments/28_monet.js +86 -0
  85. package/engine/demos/t1_3b1b/eras.js +11 -0
  86. package/engine/demos/t1_3b1b/t1_s1.js +101 -0
  87. package/engine/demos/t1_3b1b/t1_s2.js +74 -0
  88. package/engine/demos/t1_3b1b/t1_s3.js +65 -0
  89. package/engine/demos/t2_keynote_ui/eras.js +10 -0
  90. package/engine/demos/t2_keynote_ui/t2_s1.js +57 -0
  91. package/engine/demos/t2_keynote_ui/t2_s2.js +70 -0
  92. package/engine/demos/t2_keynote_ui/t2_s3.js +61 -0
  93. package/engine/demos/t3_finance_chart/eras.js +11 -0
  94. package/engine/demos/t3_finance_chart/t3_s1.js +48 -0
  95. package/engine/demos/t3_finance_chart/t3_s2.js +42 -0
  96. package/engine/demos/t3_finance_chart/t3_s3.js +51 -0
  97. package/engine/demos/y1_kurzgesagt/eras.js +10 -0
  98. package/engine/demos/y1_kurzgesagt/kurzgesagt.js +347 -0
  99. package/engine/demos/y2_vox/eras.js +10 -0
  100. package/engine/demos/y2_vox/vox.js +240 -0
  101. package/engine/demos/y3_whiteboard/eras.js +10 -0
  102. package/engine/demos/y3_whiteboard/whiteboard.js +162 -0
  103. package/engine/demos/y4_storytime/eras.js +14 -0
  104. package/engine/demos/y4_storytime/s1_desk.js +18 -0
  105. package/engine/demos/y4_storytime/s2_screen.js +82 -0
  106. package/engine/demos/y4_storytime/s3_react.js +39 -0
  107. package/engine/demos/y4_storytime/s4_wide.js +16 -0
  108. package/engine/demos/y4_storytime/s5_close.js +18 -0
  109. package/engine/demos/y4_storytime/story.js +25 -0
  110. package/engine/demos/y4_storytime/vo_env.js +2 -0
  111. package/engine/demos/y4_storytime/world.js +73 -0
  112. package/engine/demos/y5_kinetic_type/eras.js +16 -0
  113. package/engine/demos/y5_kinetic_type/k1_title.js +38 -0
  114. package/engine/demos/y5_kinetic_type/k2_complete.js +55 -0
  115. package/engine/demos/y5_kinetic_type/k3_chat.js +47 -0
  116. package/engine/demos/y5_kinetic_type/k4_agent.js +49 -0
  117. package/engine/demos/y5_kinetic_type/kinetic.js +52 -0
  118. package/engine/engine.js +277 -0
  119. package/engine/eras.js +60 -0
  120. package/engine/eras_gallery.js +115 -0
  121. package/engine/eras_style_studies.js +20 -0
  122. package/engine/examples/assets/K/347/272/277/345/233/276_/346/250/252/345/261/217.png +0 -0
  123. package/engine/examples/assets//347/273/210/347/253/257_for/345/276/252/347/216/257/346/261/202/345/222/214.png +0 -0
  124. package/engine/examples/assets//347/273/210/347/253/257_sum/346/261/202/345/222/214.png +0 -0
  125. package/engine/examples/assets//350/220/245/346/224/266/345/233/276_/347/253/226/345/261/217.png +0 -0
  126. package/engine/examples/assets//350/264/242/347/273/217/345/233/276/350/241/250_/346/250/252/345/261/217.png +0 -0
  127. package/engine/examples/t1_3b1b.json +19 -0
  128. package/engine/examples/t2_fullbleed_pulse.json +15 -0
  129. package/engine/examples/t2_keynote_ui.json +15 -0
  130. package/engine/examples/t3_finance_chart.json +29 -0
  131. package/engine/examples/y1_kurzgesagt.json +16 -0
  132. package/engine/examples/y2_vox.json +14 -0
  133. package/engine/examples/y3_whiteboard.json +17 -0
  134. package/engine/examples/y4_storytime.json +52 -0
  135. package/engine/examples/y5_kinetic_type.json +14 -0
  136. package/engine/index.html +42 -0
  137. package/engine/lib/THREE_LICENSE.txt +21 -0
  138. package/engine/lib/brush.js +149 -0
  139. package/engine/lib/camera.js +99 -0
  140. package/engine/lib/chart.js +152 -0
  141. package/engine/lib/collage.js +97 -0
  142. package/engine/lib/diagram.js +264 -0
  143. package/engine/lib/finish.js +70 -0
  144. package/engine/lib/fonts/AlfaSlabOne-400.woff +0 -0
  145. package/engine/lib/fonts/Anton-400.woff +0 -0
  146. package/engine/lib/fonts/ArchivoBlack-400.woff +0 -0
  147. package/engine/lib/fonts/Bangers-400.woff +0 -0
  148. package/engine/lib/fonts/Caveat-600.woff +0 -0
  149. package/engine/lib/fonts/Cinzel-400.woff +0 -0
  150. package/engine/lib/fonts/Cinzel-700.woff +0 -0
  151. package/engine/lib/fonts/CormorantGaramond-500.woff +0 -0
  152. package/engine/lib/fonts/CormorantGaramond-500i.woff +0 -0
  153. package/engine/lib/fonts/CormorantGaramond-700.woff +0 -0
  154. package/engine/lib/fonts/EBGaramond-400.woff +0 -0
  155. package/engine/lib/fonts/EBGaramond-400i.woff +0 -0
  156. package/engine/lib/fonts/EBGaramond-600.woff +0 -0
  157. package/engine/lib/fonts/Exo2-800i.woff +0 -0
  158. package/engine/lib/fonts/GochiHand-400.woff +0 -0
  159. package/engine/lib/fonts/IMFellEnglish-400.woff +0 -0
  160. package/engine/lib/fonts/IMFellEnglish-400i.woff +0 -0
  161. package/engine/lib/fonts/Inter-var.woff +0 -0
  162. package/engine/lib/fonts/JosefinSans-700.woff +0 -0
  163. package/engine/lib/fonts/Kalam-700.woff +0 -0
  164. package/engine/lib/fonts/LICENSES.md +40 -0
  165. package/engine/lib/fonts/LXGWWenKai-500.woff +0 -0
  166. package/engine/lib/fonts/LibreBaskerville-400.woff +0 -0
  167. package/engine/lib/fonts/LibreBaskerville-700.woff +0 -0
  168. package/engine/lib/fonts/LilitaOne-400.woff +0 -0
  169. package/engine/lib/fonts/Marcellus-400.woff +0 -0
  170. package/engine/lib/fonts/NotoSansSC-500.woff +0 -0
  171. package/engine/lib/fonts/NotoSansSC-700.woff +0 -0
  172. package/engine/lib/fonts/NotoSansSC-800.woff +0 -0
  173. package/engine/lib/fonts/NotoSansSC-900.woff +0 -0
  174. package/engine/lib/fonts/NotoSansSC-sub.woff +0 -0
  175. package/engine/lib/fonts/NotoSerifJP-600.woff +0 -0
  176. package/engine/lib/fonts/NotoSerifSC-sub.woff +0 -0
  177. package/engine/lib/fonts/OFL.txt +94 -0
  178. package/engine/lib/fonts/PlayfairDisplay-400i.woff +0 -0
  179. package/engine/lib/fonts/PlayfairDisplay-700.woff +0 -0
  180. package/engine/lib/fonts/PlayfairDisplay-700i.woff +0 -0
  181. package/engine/lib/fonts/Poppins-600.woff +0 -0
  182. package/engine/lib/fonts/Poppins-700.woff +0 -0
  183. package/engine/lib/fonts/Poppins-800.woff +0 -0
  184. package/engine/lib/fonts/PressStart2P-400.woff +0 -0
  185. package/engine/lib/fonts/Righteous-400.woff +0 -0
  186. package/engine/lib/fonts/RobotoCondensed-var.woff +0 -0
  187. package/engine/lib/fonts/Rye-400.woff +0 -0
  188. package/engine/lib/fonts/UnifrakturMaguntia-400.woff +0 -0
  189. package/engine/lib/fonts/cmunrm.woff +0 -0
  190. package/engine/lib/fonts/cmunti.woff +0 -0
  191. package/engine/lib/fonts/index.json +1 -0
  192. package/engine/lib/fonts.js +56 -0
  193. package/engine/lib/kit.js +91 -0
  194. package/engine/lib/media.js +118 -0
  195. package/engine/lib/medium.js +258 -0
  196. package/engine/lib/motion.js +111 -0
  197. package/engine/lib/paint.js +200 -0
  198. package/engine/lib/palettes.js +57 -0
  199. package/engine/lib/post.js +108 -0
  200. package/engine/lib/render.js +189 -0
  201. package/engine/lib/rig.js +390 -0
  202. package/engine/lib/rig_huashu.js +171 -0
  203. package/engine/lib/three.min.js +7 -0
  204. package/engine/lib/toon.js +186 -0
  205. package/engine/lib/typo.js +136 -0
  206. package/engine/lib/ui.js +141 -0
  207. package/engine/lib/util.js +92 -0
  208. package/engine/render.py +157 -0
  209. package/engine/scenes/01_cave.js +364 -0
  210. package/engine/scenes/02_egypt.js +524 -0
  211. package/engine/scenes/03_greek.js +448 -0
  212. package/engine/scenes/04_roman.js +555 -0
  213. package/engine/scenes/05_gothic.js +522 -0
  214. package/engine/scenes/06_renaissance.js +338 -0
  215. package/engine/scenes/08_impressionism.js +206 -0
  216. package/engine/scenes/09_postimp.js +183 -0
  217. package/engine/scenes/10_nouveau.js +334 -0
  218. package/engine/scenes/11_cubism.js +265 -0
  219. package/engine/scenes/12_bauhaus.js +214 -0
  220. package/engine/scenes/13_pop.js +269 -0
  221. package/engine/scenes/14_8bit.js +238 -0
  222. package/engine/scenes/15_raytrace.js +300 -0
  223. package/engine/scenes/16_2026.js +565 -0
  224. package/engine/scenes/17_ink.js +273 -0
  225. package/engine/scenes/18_klimt.js +227 -0
  226. package/engine/scenes/19_munch.js +153 -0
  227. package/engine/scenes/20_dunhuang.js +215 -0
  228. package/engine/scenes/21_kusama.js +124 -0
  229. package/engine/scenes/22_constructivism.js +205 -0
  230. package/engine/scenes/23_dali.js +221 -0
  231. package/engine/scenes/24_hopper.js +150 -0
  232. package/engine/scenes/25_ghibli.js +251 -0
  233. package/engine/scenes/26_vaporwave.js +191 -0
  234. package/engine/scenes/27_kirby.js +255 -0
  235. package/engine/scenes/28_monet.js +176 -0
  236. package/engine/scenes/29_seurat.js +104 -0
  237. package/engine/scenes/30_matisse.js +151 -0
  238. package/engine/scenes/31_haring.js +109 -0
  239. package/engine/scenes/32_rembrandt.js +217 -0
  240. package/engine/scenes/33_rubberhose.js +219 -0
  241. package/engine/scenes/34_shadowpuppet.js +195 -0
  242. package/engine/scenes/35_shinkai.js +197 -0
  243. package/engine/scenes/36_picasso_blue.js +123 -0
  244. package/engine/scenes/38_giotto.js +133 -0
  245. package/engine/scenes/39_piero.js +53 -0
  246. package/engine/scenes/40_raphael.js +96 -0
  247. package/engine/scenes/41_michelangelo.js +112 -0
  248. package/engine/scenes/42_guardi.js +70 -0
  249. package/engine/scenes/43_lorenzetti.js +145 -0
  250. package/engine/scenes/44_cortona.js +86 -0
  251. package/engine/scenes/45_renoir.js +86 -0
  252. package/engine/scenes/46_pissarro.js +82 -0
  253. package/engine/scenes/47_sisley.js +94 -0
  254. package/engine/scenes/48_gauguin.js +107 -0
  255. package/engine/scenes/49_chagall.js +60 -0
  256. package/engine/scenes/50_ma_yuan.js +131 -0
  257. package/engine/scenes/51_persian_miniature.js +149 -0
  258. package/engine/scenes/52_south_park.js +105 -0
  259. package/engine/scenes/53_pixar.js +60 -0
  260. package/engine/scenes/54_ghibli_lab.js +68 -0
  261. package/engine/scenes/55_dunhuang.js +161 -0
  262. package/engine/scenes/index.js +28 -0
  263. package/engine/spec_assets.py +76 -0
  264. package/engine/transitions.js +929 -0
  265. package/engine//346/227/266/344/273/243/344/275/234/350/200/205/350/247/204/350/214/203.md +49 -0
  266. package/lib/host.mjs +754 -0
  267. package/lib/render-bridge.mjs +543 -0
  268. package/lib/render-cli.mjs +98 -0
  269. package/lib/storyboard-lint.mjs +54 -0
  270. package/manifest.json +52 -0
  271. package/package.json +8 -4
  272. package/runtime/package.json +8 -0
  273. package/skills/biuart/SKILL.md +154 -0
  274. package/skills/biuart/references/01-/346/213/206/350/247/243.md +55 -0
  275. package/skills/biuart/references/02-/346/234/272/345/210/266.md +41 -0
  276. package/skills/biuart/references/03-/344/270/200/345/270/247/345/205/210/350/241/214/345/233/233/346/235/241/350/267/257/347/272/277.md +39 -0
  277. package/skills/biuart/references/04-/347/272/257/344/273/243/347/240/201/347/273/230/345/210/266.md +92 -0
  278. package/skills/biuart/references/05-/350/212/202/345/245/217/344/270/216/351/205/215/344/271/220.md +19 -0
  279. package/skills/biuart/references/06-/345/217/243/346/222/255/351/251/261/345/212/250/347/232/204/350/211/272/346/234/257/347/237/255/347/211/207.md +31 -0
  280. package/skills/biuart/references/07-/346/255/243/351/235/242/347/273/217/351/252/214.md +196 -0
  281. package/skills/biuart/references/08-/351/243/216/346/240/274/344/275/234/350/200/205/350/247/204/350/214/203.md +61 -0
  282. package/skills/biuart/references/09-/350/247/206/351/242/221/345/212/250/347/224/273/350/257/255/346/263/225.md +382 -0
  283. package/skills/biuart/references/10-/350/247/222/350/211/262.md +34 -0
  284. package/skills/biuart/references/11-/351/225/277/345/215/267/347/251/277/350/266/212/347/211/207.md +43 -0
  285. package/skills/biuart/references/12-/345/217/243/346/222/255/346/225/264/347/211/207/344/270/216/347/273/217/351/252/214/345/233/236/346/265/201.md +25 -0
  286. package/skills/biuart/references/13-/345/217/243/346/222/255/344/270/216/350/257/255/351/237/263/345/244/215/345/210/273.md +7 -0
  287. package/skills/biuart/references/15-/350/267/250/345/234/272/346/231/257/351/243/216/346/240/274/346/217/220/347/202/274.md +48 -0
  288. package/skills/biuart/references/16-/351/243/216/346/240/274/346/274/224/347/244/27295/345/210/206/345/215/217/350/256/256.md +103 -0
  289. package/skills/biuart/references/about-author.md +99 -0
  290. package/skills/biuart/references/capabilities.md +91 -0
  291. package/skills/biuart/references/compatibility.md +34 -0
  292. package/skills/biuart/references/images.md +97 -0
  293. package/skills/biuart/references/voice-model-review.md +18 -0
  294. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/t1_3b1b.md +122 -0
  295. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/t2_keynote_ui.md +117 -0
  296. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/t3_finance_chart.md +116 -0
  297. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y1_kurzgesagt.md +193 -0
  298. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y2_vox.md +195 -0
  299. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y3_whiteboard.md +190 -0
  300. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y4_storytime.md +166 -0
  301. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y5_kinetic_type.md +158 -0
  302. package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y6_presenter_explainer.md +24 -0
  303. package/skills/biuart/references//345/217/243/346/222/255API.md +125 -0
  304. package/skills/biuart/references//350/211/262/346/235/277.md +38 -0
  305. package/skills/biuart/references//350/247/222/350/211/262/347/262/276/347/273/206/345/272/246.md +61 -0
  306. package/skills/biuart/references//351/225/234/345/244/264/350/241/250/346/250/241/346/235/277.md +80 -0
  307. package/skills/biuart/references//351/225/234/345/244/264/350/257/255/350/250/200.md +73 -0
  308. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/01_cave.md +46 -0
  309. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/02_egypt.md +64 -0
  310. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/03_greek.md +50 -0
  311. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/04_roman.md +99 -0
  312. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/05_gothic.md +32 -0
  313. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/06_renaissance.md +41 -0
  314. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/08_impressionism.md +34 -0
  315. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/09_postimp.md +17 -0
  316. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/10_nouveau.md +31 -0
  317. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/11_cubism.md +30 -0
  318. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/12_bauhaus.md +30 -0
  319. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/13_pop.md +29 -0
  320. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/14_8bit.md +27 -0
  321. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/15_raytrace.md +32 -0
  322. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/16_2026.md +55 -0
  323. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/17_ink.md +50 -0
  324. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/18_klimt.md +37 -0
  325. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/19_munch.md +42 -0
  326. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/20_dunhuang.md +49 -0
  327. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/21_kusama.md +32 -0
  328. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/22_constructivism.md +35 -0
  329. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/23_dali.md +29 -0
  330. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/24_hopper.md +30 -0
  331. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/25_ghibli.md +55 -0
  332. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/26_vaporwave.md +27 -0
  333. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/27_kirby.md +30 -0
  334. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/28_monet.md +39 -0
  335. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/29_seurat.md +31 -0
  336. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/30_matisse.md +26 -0
  337. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/31_haring.md +22 -0
  338. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/32_rembrandt.md +29 -0
  339. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/33_rubberhose.md +31 -0
  340. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/34_shadowpuppet.md +40 -0
  341. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/35_shinkai.md +34 -0
  342. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/36_picasso_blue.md +25 -0
  343. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/37_rick_morty.md +48 -0
  344. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/38_giotto.md +67 -0
  345. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/39_piero.md +59 -0
  346. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/40_raphael.md +65 -0
  347. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/41_michelangelo.md +65 -0
  348. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/42_guardi.md +65 -0
  349. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/43_lorenzetti.md +69 -0
  350. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/44_cortona.md +66 -0
  351. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/45_renoir.md +66 -0
  352. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/46_pissarro.md +66 -0
  353. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/47_sisley.md +67 -0
  354. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/48_gauguin.md +66 -0
  355. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/49_chagall.md +59 -0
  356. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/50_ma_yuan.md +68 -0
  357. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/51_persian_miniature.md +66 -0
  358. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/52_south_park.md +65 -0
  359. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/53_pixar.md +61 -0
  360. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/55_dunhuang.md +41 -0
  361. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/INDEX.md +89 -0
  362. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/_/350/275/254/345/234/272_/350/277/201/347/247/273/346/265/213/350/257/225.md +53 -0
  363. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/_/351/207/215/345/201/232/350/277/233/345/272/246.md +37 -0
  364. package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/_/351/237/263/350/275/250_/350/211/272/346/234/257/345/217/262/351/200/237/351/200/232.md +135 -0
  365. package/studio/app.js +380 -0
  366. package/studio/assert-panel-permit.mjs +67 -0
  367. package/studio/composer.css +465 -0
  368. package/studio/composer.js +480 -0
  369. package/studio/index.html +97 -0
  370. package/studio/optimize-prompt.mjs +84 -0
  371. package/studio/optimize-prompt.test.mjs +47 -0
  372. package/studio/recipes.mjs +87 -0
  373. package/studio/select-menu.js +450 -0
  374. package/studio/styles.css +71 -0
  375. package/studio/to-english-prompt.mjs +149 -0
  376. package/studio/to-english-prompt.test.mjs +72 -0
  377. package/README.md +0 -3
@@ -0,0 +1,347 @@
1
+ // 片段 · 发布会式界面(t2),做教程、功能、参数最顺手:一屏一件事。有截图就让截图当主画面:满幅铺开,口播点到要看的那块就推过去。
2
+ // 镜头:spec 级 data.camera 'smooth'(默认,缓推 0.9s、换块 0.7s、拉回 0.6s,sineInOut)| 'snap'(重音:快推 0.28s、横移 0.30s、进屏轻砸)。
3
+ // 一支片里 snap 只给需要重音的那几段,见 references/镜头语言.md。
4
+ // data: { eyebrow?(标题上方小字), title?, subtitle?, accent?:'#3d6bff', frame?: 'full'(默认)| 'glass' }
5
+ // cues:title(at 这一帧标题开始从模糊里浮出;text/sub 覆盖 data)
6
+ // card | step | image(一张卡:text 卡标题,sub 说明,image 截图路径(或 frames 录屏帧序列);at 这一帧出现;步骤号按出现顺序 01、02…,data.step 可指定)
7
+ // · 有 image,默认 frame 'full':截图满幅(等比放进内容框、不裁;data.fit:'cover' 才按焦点 data.focus=[fx,fy] 裁满),
8
+ // 四周垫一层同图的压暗模糊放大版,不留死背景;text / sub 是左下角的黑底撕边标签(12fps 打字)。at 这一帧硬切进来(0.17s 轻砸),
9
+ // 上一屏也是满幅截图时改成同倍率横移换过去。highlight 这一刻推到那块(推到框占画面约 42%,至少推近 18%、最多 2.6 倍),连着的 highlight 移过去;时长按 data.camera。
10
+ // · data.frame:'glass'(或 spec 级 data.frame):旧版毛玻璃卡——截图等比放进卡里,横图横卡;竖图在横屏里是「左字右图」。光斑、玻璃是可选包装,慎用
11
+ // · 没有 image:功能卡。连着的几张(最多 3 张)并排站在一屏里,新卡进来时旧卡让位(像示范片的三卡并列);
12
+ // 卡上有图标(data.icon:wave memory agent clock phone layers chart code check play bolt lock,不写就画步骤号)、
13
+ // 大标题、说明,可选一个大数字(data.value / prefix / suffix / decimals + data.label,卡片落定后计数)
14
+ // highlight(框出当前截图里的一块:data.rect = [x, y, w, h],相对截图的 0..1;text 可选标签;满幅模式下镜头同时推过去,data.fill 改推近后框占画面的比例)
15
+ // number(大数字单独一屏:data {value, prefix, suffix, decimals, label?(数字上方小字)};at = 数字落定时刻,提前 0.9s 开始计数,
16
+ // 上一屏这时开始退场,不叠在卡片上;说明写 text 或 sub 都认)
17
+ // · data.over: true 且上一屏是满幅截图:不另起一屏,留在截图的世界里——镜头拉回整张截图,截图压暗,数字砸在它上面计数,
18
+ // 说明是黑底撕边标签。数字讲的就是这张截图里的事(两次计时之比、这一页的结论)时用它;和截图无关的数字照旧单独一屏
19
+ // 竖屏:第一张卡进来时标题缩成页眉留在顶上(不让上下大片空着);卡片按剩余高度放大。safe:版面只排在 top..H−bottom 之间。
20
+ // alpha:不画光斑底;卡片垫一层深色、字带投影,压在亮画面上也看得清。
21
+ // 不写 theme:近黑底上琥珀/青绿/浅青/珊瑚四团慢飘的光斑(不再有蓝紫光斑)。
22
+ // spec.theme(全片色板 ctx.pal):平涂 bg 底(只有极轻的暗角,不画光斑、不画毛玻璃和扫光),卡片 surface 实色,标题与卡标题 ink、说明 sub,
23
+ // 强调色(步骤号、框、副标签)accent(data.accent 写了照用),框不发光;满幅截图四周的模糊底压一层 bg,撕边标签 ink 底 bg 字。
24
+ CLIPS.t2_keynote_ui = (() => {
25
+ const { clamp, lerp } = U;
26
+ const SANS = '"Inter", "PuHui-Medium"', SANSB = '"Inter", "PuHui-Bold"', ZH = '"PuHui-Medium"', ZHB = '"PuHui-Bold"';
27
+ let BL, slides, scr = {}, G, T;
28
+ // 文字与小件的颜色:默认(深色光斑底上的白字)一套,全片色板时从 6 个键派生一套
29
+ const T0 = { title: '#f5f3ff', eyebrow: '#a7a7b4', subtitle: '#c9c9d4', head: '#f5f5f7', body: 'rgba(235,235,245,0.66)', body2: 'rgba(235,235,245,0.62)', tile: 'rgba(255,255,255,0.10)', icon: '#f2f2f7',
30
+ num: '#fff', numLabel: 'rgba(235,235,245,0.6)', rule: 'rgba(255,255,255,0.12)', frame: 'rgba(255,255,255,0.22)', bigLabel: '#a7a7b4', bigCap: 'rgba(235,235,245,0.78)', fullBg: '#0b0b0f', accent: '#3d6bff' };
31
+ const fromPal = p => ({ pal: p, title: p.ink, eyebrow: p.sub, subtitle: p.sub, head: p.ink, body: p.sub, body2: p.sub, tile: PAL.alpha(p.ink, 0.07), icon: p.ink,
32
+ num: p.ink, numLabel: p.sub, rule: PAL.alpha(p.ink, 0.14), frame: PAL.alpha(p.sub, 0.35), bigLabel: p.sub, bigCap: PAL.alpha(p.ink, 0.78), fullBg: p.bg, accent: p.accent,
33
+ labBg: p.ink, labFg: p.bg, vig: PAL.dark(p) ? '0,0,0' : PAL.rgb(p.ink).join(',') });
34
+ const onAcc = acc => T.pal ? PAL.on(acc, [T.pal.surface, T.pal.ink, T.pal.bg]) : '#fff';
35
+ const track = (c, size, em = -0.02) => { c.letterSpacing = (size * em).toFixed(2) + 'px'; };
36
+ // 本语法自带的几个线性图标(UI.icon 只有 wave/memory/agent):单位框 ±0.5,圆头等线宽
37
+ const ICON = {
38
+ clock: c => { c.beginPath(); c.arc(0, 0, 0.42, 0, 7); c.stroke(); c.beginPath(); c.moveTo(0, -0.24); c.lineTo(0, 0); c.lineTo(0.18, 0.1); c.stroke(); },
39
+ phone: c => { c.beginPath(); c.roundRect(-0.25, -0.45, 0.5, 0.9, 0.1); c.stroke(); c.beginPath(); c.moveTo(-0.08, 0.32); c.lineTo(0.08, 0.32); c.stroke(); },
40
+ layers: c => { for (let k = 0; k < 3; k++) { const y = -0.22 + k * 0.22; c.globalAlpha = k ? 0.6 : 1; c.beginPath(); c.moveTo(-0.45, y); c.lineTo(0, y - 0.18); c.lineTo(0.45, y); c.lineTo(0, y + 0.18); c.closePath(); c.stroke(); } c.globalAlpha = 1; },
41
+ chart: c => { c.beginPath(); c.moveTo(-0.42, -0.42); c.lineTo(-0.42, 0.4); c.lineTo(0.44, 0.4); c.stroke(); c.beginPath(); c.moveTo(-0.28, 0.18); c.lineTo(-0.06, -0.04); c.lineTo(0.1, 0.06); c.lineTo(0.36, -0.26); c.stroke(); },
42
+ code: c => { c.beginPath(); c.moveTo(-0.18, -0.28); c.lineTo(-0.42, 0); c.lineTo(-0.18, 0.28); c.moveTo(0.18, -0.28); c.lineTo(0.42, 0); c.lineTo(0.18, 0.28); c.moveTo(0.08, -0.38); c.lineTo(-0.08, 0.38); c.stroke(); },
43
+ check: c => { c.beginPath(); c.arc(0, 0, 0.42, 0, 7); c.stroke(); c.beginPath(); c.moveTo(-0.2, 0); c.lineTo(-0.05, 0.16); c.lineTo(0.22, -0.16); c.stroke(); },
44
+ play: c => { c.beginPath(); c.moveTo(-0.22, -0.34); c.lineTo(0.34, 0); c.lineTo(-0.22, 0.34); c.closePath(); c.fill(); },
45
+ bolt: c => { c.beginPath(); c.moveTo(0.08, -0.46); c.lineTo(-0.28, 0.06); c.lineTo(0, 0.06); c.lineTo(-0.08, 0.46); c.lineTo(0.28, -0.06); c.lineTo(0, -0.06); c.closePath(); c.fill(); },
46
+ lock: c => { c.beginPath(); c.roundRect(-0.32, -0.04, 0.64, 0.46, 0.08); c.stroke(); c.beginPath(); c.arc(0, -0.04, 0.2, Math.PI, 0); c.stroke(); },
47
+ };
48
+ const icon = (g, kind, x, y, s, col, t) => {
49
+ if (['wave', 'memory', 'agent'].includes(kind)) return UI.icon(g, kind, x, y, s, col, t);
50
+ const f = ICON[kind]; if (!f) return false;
51
+ g.save(); g.translate(x, y); g.scale(s, s); g.strokeStyle = col; g.fillStyle = col; g.lineWidth = 0.075; g.lineCap = 'round'; g.lineJoin = 'round'; f(g); g.restore();
52
+ };
53
+ // 放得下才算:TY.fit 量每行宽度,多行折成等长(说明文字不留一个字挂在下一行)
54
+ const fitB = (g, text, maxW, size, fam, o = {}) => TY.fit(g, text, maxW, size, fam, { ...o, balance: true });
55
+ const fmtNum = (d, k) => (d.prefix || '') + TY.fmt((+d.value || 0) * k, d.decimals ?? 0) + (d.suffix || '');
56
+ return {
57
+ fonts: ['PuHui-Medium', 'PuHui-Bold'],
58
+ safe: true,
59
+ init(ctx) {
60
+ const { W, H, u, safe } = ctx;
61
+ T = ctx.pal ? fromPal(ctx.pal) : T0;
62
+ BL = [{ x: .22 * W, y: .28 * H, r: .4 * Math.max(W, H), col: [214, 140, 52], a: .7, ax: .11 * W, ay: .11 * H, period: 11, ph: 0 },
63
+ { x: .79 * W, y: .24 * H, r: .37 * Math.max(W, H), col: [38, 150, 136], a: .7, ax: .1 * W, ay: .15 * H, period: 13, ph: 2.1 },
64
+ { x: .69 * W, y: .86 * H, r: .3 * Math.max(W, H), col: [120, 214, 255], a: .5, ax: .13 * W, ay: .09 * H, period: 9.5, ph: 4 },
65
+ { x: .27 * W, y: .89 * H, r: .27 * Math.max(W, H), col: [255, 96, 84], a: .42, ax: .09 * W, ay: .08 * H, period: 12, ph: 1.3 }];
66
+ const hasTitle = ctx.of('title').length > 0, hdr = ctx.portrait && hasTitle;
67
+ G = { y0: safe.top + (hdr ? 250 * u : 40 * u), y1: H - safe.bottom - 40 * u, hdrY: safe.top + 150 * u, hdr };
68
+ G.cy = (G.y0 + G.y1) / 2; G.bh = G.y1 - G.y0;
69
+ // 分屏:截图卡一张一屏;连着的无图卡最多 3 张一屏;number 单独一屏
70
+ slides = []; let k = 0;
71
+ for (const q of ctx.cues) {
72
+ if (['card', 'step', 'image'].includes(q.kind)) {
73
+ const n = q.data && q.data.step != null ? q.data.step : ++k, im = ctx.still(q), cd = { q, n, im };
74
+ const last = slides[slides.length - 1], full = im && ((q.data && q.data.frame) || ctx.data.frame || 'full') !== 'glass';
75
+ if (!im && last && last.type === 'cards' && last.cards.length < 3) last.cards.push(cd);
76
+ else slides.push({ type: im ? 'img' : 'cards', cards: [cd], start: q.at, full });
77
+ } else if (q.kind === 'number') {
78
+ const last = slides[slides.length - 1];
79
+ if (q.data && q.data.over && last && last.full) (last.nums = last.nums || []).push(q); // 砸在当前截图上,不另起一屏
80
+ else slides.push({ type: 'num', q, start: q.at - 0.9 });
81
+ }
82
+ }
83
+ slides.forEach((s, i) => { s.next = slides[i + 1] ? slides[i + 1].start : 1e9; s.panIn = s.full && i > 0 && slides[i - 1].full; s.panOut = s.full && !!slides[i + 1] && slides[i + 1].full; });
84
+ // 满幅截图:内容框(让开 safe,四边留一点)、截图的几何、这一屏的镜头(highlight 推近 / 换块;snap 用重音时长)
85
+ const snap = (ctx.data && ctx.data.camera) === 'snap', MV = snap ? { in: MO.PULSE.punch, next: MO.PULSE.pan, back: MO.PULSE.punch, ease: MO.cubicInOut } : { in: MO.PULSE.push, next: MO.PULSE.glide, back: 0.6, ease: MO.sineInOut };
86
+ slides.forEach(s => { s.snap = snap; s.panDur = snap ? MO.PULSE.pan : MO.PULSE.glide; });
87
+ for (const s of slides) if (s.full) {
88
+ const cd = s.cards[0], q = cd.q, m = 24 * u, bx = { x: ctx.box.x + m, y: ctx.box.y + m, w: ctx.box.w - 2 * m, h: ctx.box.h - 2 * m };
89
+ const fit = (q.data && q.data.fit) === 'cover' ? 'cover' : 'contain', fo = (q.data && q.data.focus) || [0.5, 0.5];
90
+ s.geo = { box: bx, fit, fx: fo[0], fy: fo[1] };
91
+ const [iw, ih] = MD.size(cd.im), s0 = (fit === 'cover' ? Math.max : Math.min)(bx.w / iw, bx.h / ih), dw = iw * s0, dh = ih * s0;
92
+ const ix = fit === 'cover' ? clamp(bx.x + bx.w * fo[0] - dw * fo[0], bx.x + bx.w - dw, bx.x) : bx.x + (bx.w - dw) / 2, iy = fit === 'cover' ? clamp(bx.y + bx.h * fo[1] - dh * fo[1], bx.y + bx.h - dh, bx.y) : bx.y + (bx.h - dh) / 2;
93
+ s.f = { x: ix, y: iy, w: dw, h: dh };
94
+ const bcx = bx.x + bx.w / 2, bcy = bx.y + bx.h / 2; s.base = { x: W / 2, y: H / 2, z: 1 }; s.events = []; let zoomed = false;
95
+ for (const qh of ctx.of('highlight')) {
96
+ if (qh.at < s.start || qh.at >= s.next || !qh.data || !qh.data.rect) continue;
97
+ const R = CLIP.sub(qh.data.rect, s.f), fill = qh.data.fill || 0.42, z = clamp(Math.min(bx.w * fill / R.w, bx.h * fill / R.h), 1.18, 2.6); // 下限 18%;框太小时最多推到 2.6 倍
98
+ const tx = R.x + R.w / 2 - (bcx - W / 2) / z, ty = R.y + R.h / 2 - (bcy - H / 2) / z; // 框中心落到内容框中心(让开 safe)
99
+ s.events.push({ at: qh.at, kind: 'to', x: tx, y: ty, z, dur: Math.min(zoomed ? MV.next : MV.in, Math.max(MO.PULSE.punch, s.next - qh.at - 0.05)), ease: MV.ease }); // 离下一屏太近就推快一点,别被硬切砍在半路
100
+ zoomed = true;
101
+ }
102
+ for (const qn of s.nums || []) { // 推近过就先拉回整张截图,拉到位数字再砸下来
103
+ qn.t1 = qn.at - 0.9 + (zoomed ? MV.back : 0);
104
+ if (zoomed) s.events.push({ at: qn.at - 0.9, kind: 'to', ...s.base, dur: MV.back, ease: MV.ease });
105
+ }
106
+ }
107
+ // 截图卡的版式:标题行 + 截图区;截图区按截图比例定。竖图在横屏里改成「左字右图」
108
+ const maxW = W * (ctx.portrait ? 0.9 : 0.8), maxH = G.bh * (ctx.portrait ? 0.92 : 0.86), head = (ctx.portrait ? 230 : 130) * u, pad = 36 * u;
109
+ for (const s of slides) if (s.type === 'img' && !s.full) {
110
+ const cd = s.cards[0], [iw0, ih0] = MD.size(cd.im), ar = iw0 / ih0;
111
+ if (!ctx.portrait && ar < 0.9) { // 横屏放竖图:左字右图,图撑满卡高
112
+ const ih = maxH - 2 * pad, iw = ih * ar, tw = 560 * u;
113
+ cd.box = { w: tw + iw + 3 * pad, h: maxH, img: { x: tw + 2 * pad, y: pad, w: iw, h: ih }, side: tw };
114
+ } else {
115
+ let iw = maxW - 2 * pad, ih = iw / ar; if (ih > maxH - head - 2 * pad) { ih = maxH - head - 2 * pad; iw = ih * ar; }
116
+ const cw = Math.max(iw + 2 * pad, Math.min(maxW, 760 * u)), ch = ih + head + 2 * pad;
117
+ cd.box = { w: cw, h: ch, img: { x: (cw - iw) / 2, y: head + pad, w: iw, h: ih } };
118
+ }
119
+ }
120
+ },
121
+ draw(c, t, ctx) {
122
+ const { W, H, u, data: d } = ctx, acc = d.accent || T.accent;
123
+ // ---- 底:光斑(只依赖片段时间,连续) ----
124
+ let bd = null;
125
+ const covered = slides.some(s => s.full && t >= s.start + (s.panIn ? s.panDur || MO.PULSE.pan : 0) - 1e-6 && t < s.next - 1e-6); // 满幅截图整屏盖住时不画光斑底(省 2/3 的渲染时间)
126
+ if (!ctx.alpha && !covered && T.pal) { // 全片色板:平涂底+极轻暗角
127
+ c.drawImage(PAINT.cached('clip_t2flat_' + T.pal.bg + W + 'x' + H, W, H, g => { g.fillStyle = T.pal.bg; g.fillRect(0, 0, W, H);
128
+ const vg = g.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.45, W / 2, H / 2, Math.max(W, H) * 0.7); vg.addColorStop(0, `rgba(${T.vig},0)`); vg.addColorStop(1, `rgba(${T.vig},0.08)`); g.fillStyle = vg; g.fillRect(0, 0, W, H); }), 0, 0);
129
+ } else if (!ctx.alpha && !covered) {
130
+ const bgc = UI.scratch('clip_t2bg', W, H), g = bgc.getContext('2d'); g.clearRect(0, 0, W, H); // 先清空:不清的话上一帧的残留会透过半透明边缘,同一时刻先后渲出来差 1 个色阶(qa 确定性 ✗)
131
+ UI.mesh(g, t, { base: '#050508', blobs: BL, blur: 90, scale: 0.25, grain: 0.035, key: 'clip_t2mesh' });
132
+ const vg = g.createRadialGradient(W / 2, H / 2, 100 * u, W / 2, H / 2, Math.max(W, H) * 0.55); vg.addColorStop(0, 'rgba(0,0,0,0.38)'); vg.addColorStop(1, 'rgba(0,0,0,0.1)');
133
+ g.fillStyle = vg; g.fillRect(0, 0, W, H); c.drawImage(bgc, 0, 0); bd = UI.backdrop(bgc, { blur: 40, key: 'clip_t2bd' });
134
+ }
135
+ const shadowText = g => { if (ctx.alpha) { g.shadowColor = 'rgba(0,0,0,0.75)'; g.shadowBlur = 18 * u; g.shadowOffsetY = 3 * u; } };
136
+ // ---- 标题:眉题 → 主标题(模糊→清晰+上移)→ 副标题;第一屏进来时:横屏退到后面,竖屏缩成页眉留在顶上 ----
137
+ const qt = ctx.of('title')[0], first = slides[0] ? slides[0].start : 1e9;
138
+ if (qt) {
139
+ const out = MO.appleOut(MO.seg(t, first - 0.15, first + 0.5));
140
+ const keep = G.hdr, title = qt.text ?? d.title ?? '', eb = (qt.data && qt.data.eyebrow) ?? d.eyebrow, sub = qt.sub ?? d.subtitle;
141
+ if (out < 1 || keep) {
142
+ c.save(); c.textAlign = 'center'; c.textBaseline = 'alphabetic'; shadowText(c);
143
+ let sz = TY.fit(c, title, W * 0.86, (ctx.portrait ? 150 : 190) * u, 'Inter', { maxLines: 1, min: 0.55, weight: '600 ', soft: true }); // 先缩字号放一行,放不下再折两行
144
+ if (!sz.ok) sz = fitB(c, title, W * 0.86, (ctx.portrait ? 150 : 190) * u, 'Inter', { maxLines: 2, weight: '600 ' });
145
+ const cy = G.cy - (sz.lines.length - 1) * sz.size * 0.55;
146
+ // 竖屏:整块缩到页眉(主标题 → 约 64px),眉题和副标题淡出
147
+ const hs = keep ? Math.min(1, 68 * u / sz.size) : 1, sc = keep ? lerp(1, hs, out) : lerp(1, 0.92, out), ty = keep ? lerp(0, G.hdrY - cy, out) : 0;
148
+ c.translate(W / 2, cy + ty); c.scale(sc, sc); c.translate(-W / 2, -cy);
149
+ const aSide = 1 - out;
150
+ if (!keep) { c.globalAlpha = 1 - out; if (out > 0) c.filter = `blur(${(20 * out * u).toFixed(1)}px)`; }
151
+ if (eb && aSide > 0) { c.save(); c.globalAlpha *= keep ? aSide : 1; c.font = `500 ${34 * u}px ${ZH}`; c.fillStyle = T.eyebrow; c.letterSpacing = `${6 * u}px`; TY.blurIn(c, eb, W / 2, cy - sz.size * 0.95, MO.appleOut(ctx.p(t, qt.at, 0.7)), { blur: 10 * u, dy: 16 * u }); c.restore(); }
152
+ c.font = `600 ${sz.size}px ${SANSB}`; track(c, sz.size); c.fillStyle = T.title;
153
+ sz.lines.forEach((ln, i) => TY.blurIn(c, ln, W / 2, cy + i * sz.size * 1.1, MO.appleOut(ctx.p(t, qt.at + 0.15 + i * 0.12, 1.0)), { blur: 26 * u, dy: 40 * u }));
154
+ if (sub && aSide > 0) { c.save(); c.globalAlpha *= keep ? aSide : 1; c.letterSpacing = '0px'; c.font = `400 ${46 * u}px ${ZH}`; c.fillStyle = T.subtitle;
155
+ TY.wrap(c, sub, W * 0.84).forEach((ln, i) => TY.blurIn(c, ln, W / 2, cy + (sz.lines.length - 1) * sz.size * 1.1 + 110 * u + i * 62 * u, MO.appleOut(ctx.p(t, qt.at + 0.5, 0.9)), { blur: 12 * u, dy: 22 * u })); c.restore(); }
156
+ c.restore();
157
+ }
158
+ }
159
+ // ---- 每一屏:下一屏开始时往左后方退走(0.65s),不和下一屏的内容叠着 ----
160
+ slides.forEach((s, si) => {
161
+ if (t < s.start - 1e-6) return;
162
+ const nx = slides[si + 1], leave = MO.appleOut(MO.seg(t, s.next - 0.15, s.next + (nx && nx.type === 'num' ? 0.3 : Math.max(0.5, s.panOut ? s.panDur || 0 : 0)))); if (leave >= 1) return; // 满幅横移出场要等横移走完
163
+ if (s.full) return drawFull(c, s, t, ctx, acc);
164
+ if (s.type === 'num') return drawNumber(c, s, t, leave, ctx, shadowText);
165
+ if (s.type === 'img') return drawCard(c, s.cards[0], 0, t, leave, ctx, bd, cardGeoImg(s.cards[0], ctx), 'i' + si);
166
+ // 无图卡并排:n 张时的位置;新卡进来时旧卡按弹簧挪到新位置
167
+ const vis = s.cards.filter(cd => t >= cd.q.at - 1e-6), n = vis.length, last = vis[n - 1]; ctx._stat = s.cards.some(cd => cd.q.data && cd.q.data.value != null);
168
+ const rf = n > 1 ? MO.spring(t - last.q.at, { duration: 0.7, bounce: 0.12 }) : 1;
169
+ vis.forEach((cd, j) => { const A = rowGeo(j, n, ctx, s.cards.length), Bp = j < n - 1 ? rowGeo(j, n - 1, ctx, s.cards.length) : A;
170
+ const geo = { cx: lerp(Bp.cx, A.cx, rf), cy: lerp(Bp.cy, A.cy, rf), w: lerp(Bp.w, A.w, rf), h: lerp(Bp.h, A.h, rf), focus: j === n - 1 ? 1 : 1 - 0.25 * rf };
171
+ drawCard(c, cd, j, t, leave, ctx, bd, geo, 'c' + si + '_' + j); });
172
+ });
173
+ },
174
+ };
175
+ // 满幅截图一屏:底是同图的压暗模糊放大版(缓存一次),截图等比铺满内容框,镜头按 s.events 脉冲;左上角黑底撕边标签。
176
+ // 进场:上一屏也是满幅截图 → 同倍率横移(默认 0.7s,snap 0.30s;at 这一帧露出第一条边);否则硬切(snap 加 0.17s 轻砸)。离场:下一屏是满幅截图就让它横移盖过来,否则在下一屏开始时硬切掉
177
+ function drawFull(c, s, t, ctx, acc) {
178
+ const { W, H, u } = ctx, cd = s.cards[0], q = cd.q;
179
+ if (t < s.start - 1e-6) return;
180
+ const nx = slides[slides.indexOf(s) + 1], P = MO.PULSE;
181
+ if (nx && t >= s.next + (s.panOut ? s.panDur || P.pan : 0) - 1e-6) return;
182
+ const pd = s.panDur || P.pan, pe = s.snap ? MO.cubicInOut : MO.sineInOut;
183
+ const panIn = s.panIn ? MO.pulse(ctx.lt(t, s.start), 0, pd, pe) : 1, panOut = s.panOut ? MO.pulse(ctx.lt(t, s.next), 0, pd, pe) : 0;
184
+ const dx = (1 - panIn) * W - panOut * W;
185
+ const slam = s.panIn || !s.snap ? 1 : MO.slamK(ctx.lt(t, s.start) / P.slam, 1.06, 0.985); // 进屏轻砸只在 snap;默认硬切进来不缩放
186
+ const cam = CAM.track(s.base, s.events, t), im = ctx.frame(q, t);
187
+ c.save(); c.translate(dx, 0); c.beginPath(); c.rect(0, 0, W, H); c.clip(); // 只画在自己这一屏里:模糊底是 2 倍大的,不裁会在横移时压到邻屏
188
+ if (!ctx.alpha) { c.fillStyle = T.fullBg; c.fillRect(0, 0, W, H); }
189
+ CAM.with(c, { x: cam.x, y: cam.y, z: cam.z * slam }, g => {
190
+ if (!ctx.alpha) { const bg = PAINT.cached('t2full_bg_' + slides.indexOf(s) + '_' + W + 'x' + H + (T.pal ? T.pal.bg : ''), W, H, gg => { gg.filter = T.pal ? 'blur(48px) saturate(0.5)' : 'blur(48px) brightness(0.42) saturate(0.8)'; MD.cover(gg, cd.im, { x: -80, y: -80, w: W + 160, h: H + 160, clip: false }); gg.filter = 'none';
191
+ if (T.pal) { gg.fillStyle = PAL.alpha(T.pal.bg, 0.8); gg.fillRect(0, 0, W, H); } }); // 全片色板:模糊底上再压八成底色,四周和前后镜头同色、只透一点截图的影子
192
+ g.drawImage(bg, -W * 0.5, -H * 0.5, W * 2, H * 2); }
193
+ const f = s.f;
194
+ g.save(); g.shadowColor = 'rgba(0,0,0,.5)'; g.shadowBlur = 40 * u; g.shadowOffsetY = 14 * u; g.fillStyle = '#000'; g.fillRect(f.x, f.y, f.w, f.h); g.restore();
195
+ if (s.geo.fit === 'cover') MD.cover(g, im, { ...s.geo.box, fx: s.geo.fx, fy: s.geo.fy });
196
+ else g.drawImage(im, f.x, f.y, f.w, f.h);
197
+ hlOn(g, cd, f, t, ctx, acc, u, s.next, cam.z * slam);
198
+ });
199
+ // 标签:屏幕层,不跟镜头;压在内容框左下角(截图自己的标题多在上面,不挡它),at 这一帧出现、12fps 打字
200
+ const size = (ctx.portrait ? 54 : 46) * u, ss = size * 0.66, lx = ctx.box.x + 56 * u, yb = ctx.box.y + ctx.box.h - (ctx.portrait ? 110 : 60) * u;
201
+ const ySub = yb - ss * 0.75, yMain = q.sub ? ySub - ss * 0.75 - 12 * u - size * 0.75 : yb - size * 0.75;
202
+ if (q.text) MD.label(c, q.text, lx, yMain, { t, at: q.at, size, maxW: ctx.box.w - 112 * u, bg: T.labBg, fg: T.labFg });
203
+ if (q.sub) MD.label(c, q.sub, lx, q.text ? ySub : yb - ss * 0.75, { t, at: q.at + (q.text ? 0.25 : 0), size: ss, maxW: ctx.box.w - 112 * u, fam: 'PuHui-Bold', bg: acc, fg: onAcc(acc) });
204
+ for (const qn of s.nums || []) overNumber(c, qn, t, ctx);
205
+ c.restore();
206
+ }
207
+ // data.over 的大数字:(拉回整张截图后)截图压暗 → 数字 0.17s 砸入并计数到 at 落定 → 说明是黑底撕边标签。画在屏幕层,不跟镜头
208
+ function overNumber(c, q, t, ctx) {
209
+ const { W, u } = ctx, t0 = q.t1; if (t < t0 - 1e-6) return;
210
+ const nd = q.data || {}, k = MO.expoOut(clamp((t - t0) / Math.max(0.3, q.at - t0))), cap = q.text ?? q.sub, bx = ctx.box;
211
+ c.save(); c.fillStyle = `rgba(0,0,0,${(0.62 * clamp(ctx.lt(t, t0) / 0.17)).toFixed(3)})`; c.fillRect(0, 0, W, ctx.H); c.restore();
212
+ let size = (ctx.portrait ? 200 : 240) * u; c.font = `600 ${size}px ${SANSB}`; track(c, size, -0.03);
213
+ const fw = c.measureText(fmtNum(nd, 1)).width; if (fw > bx.w * 0.86) size *= bx.w * 0.86 / fw;
214
+ const cx = bx.x + bx.w / 2, cy = bx.y + bx.h / 2 + size * 0.3 - (cap ? 40 * u : 0), my = cy - size * 0.3, z = MO.slamK(ctx.lt(t, t0) / MO.PULSE.slam, 1.35, 0.96);
215
+ c.save(); c.translate(cx, my); c.scale(z, z); c.translate(-cx, -my);
216
+ c.font = `600 ${size}px ${SANSB}`; track(c, size, -0.03); c.fillStyle = T.pal ? (PAL.dark(T.pal) ? T.pal.ink : T.pal.surface) : '#ffffff'; c.shadowColor = 'rgba(0,0,0,0.6)'; c.shadowBlur = 30 * u;
217
+ TY.tabular(c, fmtNum(nd, k), cx, cy, { align: 'center' }); c.restore();
218
+ if (cap) MD.label(c, cap, cx, cy + 110 * u, { t, at: t0 + 0.2, size: (ctx.portrait ? 54 : 48) * u, maxW: bx.w * 0.92, align: 'center', bg: T.labBg, fg: T.labFg });
219
+ }
220
+ // 截图上的 highlight:at 这一帧框开始弹出,其余压暗,一道光扫过框内;只认落在这一屏在屏期间的 highlight
221
+ function hlOn(g, cd, f, t, ctx, acc, u, until, z = 1) { // z:这一刻相机的放大倍数(满幅模式画在被放大的世界里,标签要按它反算,不然长标签被放大出画)
222
+ const q = cd.q;
223
+ for (const qh of ctx.of('highlight')) {
224
+ if (qh.at < q.at || qh.at >= until || !qh.data || !qh.data.rect) continue;
225
+ const hl = ctx.lt(t, qh.at); if (hl <= 0) continue;
226
+ const R = CLIP.sub(qh.data.rect, f), s = MO.spring(hl, { duration: 0.45, bounce: 0.25 }), e = 8 * u * (1 - s);
227
+ g.save(); g.fillStyle = 'rgba(0,0,0,0.38)'; g.beginPath(); g.rect(f.x, f.y, f.w, f.h); g.roundRect(R.x - e, R.y - e, R.w + 2 * e, R.h + 2 * e, 10 * u); g.globalAlpha = clamp(hl / 0.3); g.fill('evenodd'); g.restore();
228
+ g.save(); g.strokeStyle = acc; g.lineWidth = 5 * u; if (!T.pal) { g.shadowColor = acc; g.shadowBlur = 18 * u * 0.85; } g.globalAlpha = clamp(hl / 0.15); // 全片色板:框不发光
229
+ g.beginPath(); g.roundRect(R.x - e, R.y - e, R.w + 2 * e, R.h + 2 * e, 10 * u); g.stroke(); g.restore();
230
+ UI.sheen(g, UI.rrPath(R.x, R.y, R.w, R.h, 10 * u), MO.seg(hl, 0.25, 1.0), { x0: R.x, x1: R.x + R.w, width: Math.max(60 * u, R.w * 0.2), alpha: 0.35 });
231
+ if (qh.text) {
232
+ const k = clamp(30 * u * z, 30 * u, 52 * u) / z, sz = k, lh = 48 * u * k / (30 * u), pd = 18 * u * k / (30 * u); // 屏幕上的字大小夹在 30–52u:推近后也不会越放越大
233
+ const fl = TY.fitFloor(g, qh.text, Math.min(f.w * 0.9, ctx.W * 0.84 / z) - 2 * pd, sz, 'PuHui-Bold', { maxLines: 1, floor: sz * 0.62 }), txt = fl.lines[0] || '';
234
+ g.font = `600 ${fl.size}px ${ZHB}`; const lw = g.measureText(txt).width + 2 * pd, ly = R.y + R.h + 16 * u / z + lh > f.y + f.h ? R.y - lh - 16 * u / z : R.y + R.h + 16 * u / z, lx = clamp(R.x, f.x, Math.max(f.x, f.x + f.w - lw));
235
+ g.save(); g.globalAlpha = clamp((hl - 0.15) / 0.25); g.fillStyle = acc; g.beginPath(); g.roundRect(lx, ly, lw, lh, lh / 2); g.fill(); g.fillStyle = onAcc(acc); g.textBaseline = 'middle'; g.fillText(txt, lx + pd, ly + lh / 2 + 1); g.restore(); }
236
+ }
237
+ }
238
+ // 截图卡的屏幕几何
239
+ function cardGeoImg(cd, ctx) { return { cx: ctx.W / 2, cy: G.cy, w: cd.box.w, h: cd.box.h, focus: 1 }; }
240
+ // 无图卡:横屏排一行(1 张时宽一点),竖屏叠成一列;total = 这一屏最终有几张(排版按当前张数,但尺寸上限按最终张数,挪位时不忽大忽小)
241
+ function rowGeo(j, n, ctx, total) {
242
+ const { W, u } = ctx, gap = 44 * u;
243
+ if (ctx.portrait) { const w = W * 0.86, h = Math.min(400 * u, (G.bh - (total - 1) * gap) / total), y0 = G.cy - (n * h + (n - 1) * gap) / 2; return { cx: W / 2, cy: y0 + j * (h + gap) + h / 2, w, h }; }
244
+ const w = total === 1 ? 760 * u : Math.min(540 * u, (W * 0.9 - (total - 1) * gap) / total), h = Math.min((ctx._stat ? 660 : 540) * u, G.bh * 0.9), x0 = W / 2 - (n * w + (n - 1) * gap) / 2;
245
+ return { cx: x0 + j * (w + gap) + w / 2, cy: G.cy, w, h };
246
+ }
247
+ // 一张卡:弹簧从下方升起+绕 Y 轴转正;内容画进离屏纹理(毛玻璃按卡片的屏幕位置取样),再伪 3D 贴回
248
+ function drawCard(c, cd, i, t, leave, ctx, bd, geo, key) {
249
+ const { u, data: d } = ctx, q = cd.q, lt = ctx.lt(t, q.at); if (lt <= 0) return;
250
+ const sp = MO.spring(lt, { duration: 0.7, bounce: 0.15 }), acc = d.accent || T.accent;
251
+ const Bw = geo.w, Bh = geo.h, cx = geo.cx - leave * ctx.W * 0.35, cy = geo.cy + (1 - sp) * 170 * u; // 落定后停住:不漂浮、不摇
252
+ const sc = (0.9 + 0.1 * sp) * (1 - 0.12 * leave) * lerp(0.97, 1, geo.focus), ry = (1 - sp) * -0.45 - 0.35 * leave, a = clamp(lt / 0.2) * (1 - leave) * lerp(0.6, 1, geo.focus);
253
+ const pad = 60, tex = scr[key] || (scr[key] = document.createElement('canvas')); tex.width = Math.ceil(Bw + 2 * pad); tex.height = Math.ceil(Bh + 2 * pad);
254
+ const g = tex.getContext('2d'); g.reset();
255
+ const sw = Bw * sc, sh = Bh * sc;
256
+ if (ctx.alpha) { g.fillStyle = 'rgba(12,12,22,0.62)'; g.beginPath(); g.roundRect(pad, pad, Bw, Bh, 34 * u); g.fill(); }
257
+ if (T.pal && !ctx.alpha) { g.fillStyle = T.pal.surface; g.beginPath(); g.roundRect(pad, pad, Bw, Bh, 34 * u); g.fill(); g.strokeStyle = PAL.alpha(T.pal.sub, 0.3); g.lineWidth = 1.5; g.stroke(); } // 全片色板:实色卡片,不是毛玻璃
258
+ else UI.glass(g, { x: pad, y: pad, w: Bw, h: Bh, r: 34 * u, bd, sample: { x: cx - sw / 2, y: cy - sh / 2, w: sw, h: sh }, tint: ctx.alpha ? 0.08 : 0.075, stroke: 0.22, light: 0.14, shadow: false });
259
+ g.save(); g.translate(pad, pad);
260
+ if (cd.im) imgContent(g, cd, Bw, Bh, t, ctx, acc); else featContent(g, cd, Bw, Bh, lt, t, ctx, acc);
261
+ if (!T.pal) UI.sheen(g, UI.rrPath(0, 0, Bw, Bh, 34 * u), MO.seg(lt, 0.4, 1.2), { x0: 0, x1: Bw, width: 120 * u, angle: -0.35, alpha: 0.16 }); // 光扫过整张卡(入场后 0.4s)
262
+ g.restore();
263
+ c.save(); c.globalAlpha = a;
264
+ UI.shadow(c, cx - sw / 2 + 10 * u, cy - sh / 2 + 20 * u, sw - 20 * u, sh - 20 * u, 34 * u, { blur: 80 * u, oy: 40 * u, alpha: T.pal && !PAL.dark(T.pal) ? 0.16 : 0.55 });
265
+ UI.persp(c, tex, { cx, cy, w: tex.width * sc, h: tex.height * sc, ry, persp: 1800 * u, strip: 3, shade: 0.35 });
266
+ c.restore();
267
+ }
268
+ // 步骤号徽章
269
+ function badge(g, x, y, s, n, acc, u) { g.fillStyle = acc; g.beginPath(); g.roundRect(x, y, s, s, s * 0.28); g.fill();
270
+ g.fillStyle = onAcc(acc); g.font = `700 ${s * 0.5}px ${SANSB}`; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText(String(n).padStart(2, '0'), x + s / 2, y + s / 2 + 2 * u); g.textAlign = 'left'; g.textBaseline = 'alphabetic'; }
271
+ // 截图卡的内容:步骤号+标题+说明,截图 contain;highlight 框出一块
272
+ function imgContent(g, cd, Bw, Bh, t, ctx, acc) {
273
+ const { u } = ctx, q = cd.q, B = cd.box, I = B.img, hp = 40 * u, bs = 70 * u;
274
+ badge(g, hp, hp, bs, cd.n, acc, u);
275
+ if (B.side) { // 左字右图:标题、说明在左栏往下排
276
+ const tw = B.side - hp, tl = fitB(g, q.text || '', tw, 58 * u, 'PuHui-Bold', { maxLines: 3, weight: '600 ' });
277
+ g.font = `600 ${tl.size}px ${ZHB}`; g.fillStyle = T.head; tl.lines.forEach((ln, k) => g.fillText(ln, hp, hp + bs + 70 * u + k * tl.size * 1.25));
278
+ if (q.sub) { const sl = fitB(g, q.sub, tw, 34 * u, 'PuHui-Medium', { maxLines: 5 }); g.font = `400 ${sl.size}px ${ZH}`; g.fillStyle = T.body;
279
+ sl.lines.forEach((ln, k) => g.fillText(ln, hp, hp + bs + 70 * u + tl.lines.length * tl.size * 1.25 + 30 * u + k * sl.size * 1.4)); }
280
+ } else {
281
+ const tx = hp + bs + 26 * u, tw = Bw - tx - hp;
282
+ if (ctx.portrait) { // 竖屏:卡头高一些,标题、说明字大一号
283
+ const tl = fitB(g, q.text || '', tw, 56 * u, 'PuHui-Bold', { maxLines: 2, weight: '600 ', min: 0.7 }); g.font = `600 ${tl.size}px ${ZHB}`; g.fillStyle = T.head;
284
+ tl.lines.forEach((ln, k) => g.fillText(ln, tx, hp + tl.size * 0.85 + k * tl.size * 1.2));
285
+ // 标题占两行时说明只留一行,卡头放得下
286
+ if (q.sub) { const sl = fitB(g, q.sub, tw, 34 * u, 'PuHui-Medium', { maxLines: tl.lines.length > 1 ? 1 : 2, min: 0.75 }); g.font = `400 ${sl.size}px ${ZH}`; g.fillStyle = T.body;
287
+ sl.lines.forEach((ln, k) => g.fillText(ln, tx, hp + tl.size * 0.85 + (tl.lines.length - 1) * tl.size * 1.2 + 24 * u + sl.size + k * sl.size * 1.35)); }
288
+ } else {
289
+ const tl = TY.fitFloor(g, q.text || '', tw, 46 * u, 'PuHui-Bold', { maxLines: 1, weight: '600 ', floor: 30 * u });
290
+ g.font = `600 ${tl.size}px ${ZHB}`; g.fillStyle = T.head; g.fillText(tl.lines[0] || '', tx, hp + (q.sub ? 32 * u : 48 * u));
291
+ if (q.sub) { const sl = TY.fitFloor(g, q.sub, tw, 28 * u, 'PuHui-Medium', { maxLines: 1, floor: 20 * u }); g.font = `400 ${sl.size}px ${ZH}`; g.fillStyle = T.body2; g.fillText(sl.lines[0] || '', tx, hp + 74 * u); }
292
+ }
293
+ }
294
+ const [iw, ih] = MD.size(cd.im), f = CLIP.fit(iw, ih, I.x, I.y, I.w, I.h);
295
+ g.save(); g.shadowColor = 'rgba(0,0,0,.45)'; g.shadowBlur = 30 * u; g.shadowOffsetY = 12 * u; g.fillStyle = '#000'; g.beginPath(); g.roundRect(f.x, f.y, f.w, f.h, 10 * u); g.fill(); g.restore();
296
+ g.save(); g.beginPath(); g.roundRect(f.x, f.y, f.w, f.h, 10 * u); g.clip(); g.drawImage(ctx.frame(q, t), f.x, f.y, f.w, f.h); g.restore();
297
+ g.strokeStyle = T.frame; g.lineWidth = 1.5; g.beginPath(); g.roundRect(f.x + .75, f.y + .75, f.w - 1.5, f.h - 1.5, 10 * u); g.stroke();
298
+ // 框出重点(和满幅模式同一套):只认落在这张卡在屏期间的 highlight
299
+ const nxAt = (() => { const i = ctx.cues.indexOf(q); const n = ctx.cues.slice(i + 1).find(z => ['card', 'step', 'image', 'number'].includes(z.kind)); return n ? (n.kind === 'number' ? n.at - 0.9 : n.at) : 1e9; })();
300
+ hlOn(g, cd, f, t, ctx, acc, u, nxAt);
301
+ }
302
+ // 无图功能卡的内容:图标(或步骤号)→ 大标题 → 说明 →(可选)大数字计数。横屏竖卡上下排,竖屏横卡左右排
303
+ function featContent(g, cd, Bw, Bh, lt, t, ctx, acc) {
304
+ const { u } = ctx, q = cd.q, dd = q.data || {}, hasNum = dd.value != null, pad = 48 * u, tile = (ctx.portrait ? 110 : 120) * u;
305
+ const drawTile = (x, y) => { g.fillStyle = T.tile; g.beginPath(); g.roundRect(x, y, tile, tile, 26 * u); g.fill();
306
+ if (!dd.icon || icon(g, dd.icon, x + tile / 2, y + tile / 2, tile * 0.6, T.icon, t) === false) {
307
+ g.fillStyle = acc; g.beginPath(); g.roundRect(x, y, tile, tile, 26 * u); g.fill(); g.fillStyle = onAcc(acc); g.font = `700 ${tile * 0.46}px ${SANSB}`; g.textAlign = 'center'; g.textBaseline = 'middle';
308
+ g.fillText(String(cd.n).padStart(2, '0'), x + tile / 2, y + tile / 2 + 3 * u); g.textAlign = 'left'; g.textBaseline = 'alphabetic'; } };
309
+ const num = (x, y, size, align) => { const v = MO.expoOut(clamp((lt - 0.35) / 0.9)); g.save(); g.font = `600 ${size}px ${SANSB}`; track(g, size, -0.03); g.fillStyle = T.num;
310
+ TY.tabular(g, fmtNum(dd, v), x, y, { align }); g.restore(); if (dd.label) { g.font = `400 ${28 * u}px ${ZH}`; g.fillStyle = T.numLabel; g.textAlign = align; g.fillText(dd.label, x, y + 46 * u); g.textAlign = 'left'; } };
311
+ g.textAlign = 'left'; g.textBaseline = 'alphabetic';
312
+ if (!ctx.portrait) {
313
+ drawTile(pad, pad);
314
+ const tl = fitB(g, q.text || '', Bw - 2 * pad, 64 * u, 'PuHui-Bold', { maxLines: 2, weight: '600 ', min: 0.6 });
315
+ let y = pad + tile + 40 * u + tl.size; g.font = `600 ${tl.size}px ${ZHB}`; g.fillStyle = T.head; tl.lines.forEach((ln, k) => g.fillText(ln, pad, y + k * tl.size * 1.2)); y += (tl.lines.length - 1) * tl.size * 1.2;
316
+ if (q.sub) { const sl = fitB(g, q.sub, Bw - 2 * pad, (hasNum ? 32 : 38) * u, 'PuHui-Medium', { maxLines: hasNum ? 2 : 4, min: 0.75 }); g.font = `400 ${sl.size}px ${ZH}`; g.fillStyle = T.body;
317
+ sl.lines.forEach((ln, k) => g.fillText(ln, pad, y + 26 * u + sl.size + k * sl.size * 1.45)); }
318
+ if (hasNum) { g.fillStyle = T.rule; g.fillRect(pad, Bh - pad - 200 * u, Bw - 2 * pad, 1.5); num(pad, Bh - pad - 66 * u, Math.min(128 * u, (Bw - 2 * pad) / Math.max(3, fmtNum(dd, 1).length) * 1.6), 'left'); }
319
+ } else {
320
+ drawTile(pad, (Bh - tile) / 2);
321
+ const tx = pad + tile + 34 * u, nw = hasNum ? Math.min(340 * u, Bw * 0.36) : 0, tw = Bw - tx - pad - nw;
322
+ const tl = fitB(g, q.text || '', tw, 62 * u, 'PuHui-Bold', { maxLines: 2, weight: '600 ', min: 0.6 }); // 标题放不下就折两行,不再截掉后半句
323
+ const sl = q.sub ? fitB(g, q.sub, tw, 38 * u, 'PuHui-Medium', { maxLines: tl.lines.length > 1 ? 1 : 2, min: 0.75 }) : { lines: [], size: 0 };
324
+ const tH = tl.size + (tl.lines.length - 1) * tl.size * 1.15, blk = tH + (sl.lines.length ? 22 * u + sl.lines.length * sl.size * 1.4 : 0), y0 = (Bh - blk) / 2 + tl.size * 0.85;
325
+ g.font = `600 ${tl.size}px ${ZHB}`; g.fillStyle = T.head; tl.lines.forEach((ln, k) => g.fillText(ln, tx, y0 + k * tl.size * 1.15));
326
+ const ys = y0 + (tl.lines.length - 1) * tl.size * 1.15;
327
+ g.font = `400 ${sl.size}px ${ZH}`; g.fillStyle = T.body; sl.lines.forEach((ln, k) => g.fillText(ln, tx, ys + 22 * u + sl.size + k * sl.size * 1.4));
328
+ if (hasNum) num(Bw - pad, Bh / 2 + 20 * u, Math.min(96 * u, nw / Math.max(3, fmtNum(dd, 1).length) * 1.7), 'right');
329
+ }
330
+ }
331
+ // 大数字一屏:at 落定(提前 0.9s 开始滚);数字上方可有小字 label,下方说明(text 或 sub)
332
+ function drawNumber(c, s, t, leave, ctx, shadowText) {
333
+ const { W, u } = ctx, q = s.q, nd = q.data || {}, k = MO.expoOut(clamp((t - s.start) / 0.9)), a = MO.appleOut(clamp((t - s.start - 0.12) / 0.3)) * (1 - leave);
334
+ const cap = q.text ?? q.sub;
335
+ c.save(); c.globalAlpha = a; shadowText(c); c.textAlign = 'center'; c.textBaseline = 'alphabetic';
336
+ if (leave > 0) { c.filter = `blur(${(16 * leave * u).toFixed(1)}px)`; }
337
+ const str = fmtNum(nd, k), full = fmtNum(nd, 1);
338
+ c.font = `600 ${220 * u}px ${SANSB}`; track(c, 220 * u, -0.03); let size = 220 * u; const fw = c.measureText(full).width; if (fw > W * 0.88) size *= W * 0.88 / fw;
339
+ c.font = `600 ${size}px ${SANSB}`; track(c, size, -0.03); c.fillStyle = T.num;
340
+ const y = G.cy + size * 0.32 - (cap ? 30 * u : 0);
341
+ TY.tabular(c, str, W / 2, y, { align: 'center' });
342
+ c.letterSpacing = '0px';
343
+ if (nd.label) { c.font = `500 ${34 * u}px ${ZH}`; c.fillStyle = T.bigLabel; c.fillText(nd.label, W / 2, y - size * 0.85); }
344
+ if (cap) { c.font = `400 ${(ctx.portrait ? 46 : 44) * u}px ${ZH}`; c.fillStyle = T.bigCap; TY.wrap(c, cap, W * 0.84).forEach((ln, i) => c.fillText(ln, W / 2, y + 100 * u + i * 60 * u)); }
345
+ c.restore();
346
+ }
347
+ })();
@@ -0,0 +1,206 @@
1
+ // 片段 · 财经图表(t3):版式先出 → 坐标系 → 数据长出 → 只标一件事。真实数据驱动,单位/来源/角标全从 data 读。
2
+ // data: { title, unit(副标题:指标与单位), source(来源,左下), badge?(只有写了才画右下角标,如 "示意数据"),
3
+ // chart: 'bar'|'line'|'candle', series: [{label, value}] 或 K 线 [{label, o, h, l, c}], decimals=1, prefix='', suffix='',
4
+ // highlight?: {index, text, sub}, colors?: {main, accent, dim}, upDown?: 'cn'(红涨绿跌,默认)|'us', theme? }
5
+ // 主题(图表镜头要留在片子的世界里,底和前后镜头同色系):spec.theme 或 data.theme,
6
+ // 全片色板名(paper / poster / ink / navy / bauhaus / snow / wood / chalk,见 lib/palettes.js)→ 按 ctx.pal 上色:底 bg、字 ink、次要字和刻度 sub、
7
+ // 折线/标注/大数字 accent、柱子 accent2;K 线涨跌仍按 upDown 用红绿(语义色)。
8
+ // 旧写法照旧:'light'(白底,默认)| 'dark'(深底浅字)| 对象 {bg, ink, sub, src, tick, xlab, grid, red, bar, dim}(写了 bg 按它的明暗选浅/深一套,再逐项覆盖)。
9
+ // 'paper' 现在是全片色板「纸本」(以前是 FT 三文鱼粉)。
10
+ // 没写主题但 spec.safe.fill 写了片子的底色:图表底就用它,深色底自动换深底浅字——不再在深色片里冒出一张白卡。
11
+ // cues(at 秒):title(版式)· bar|line|candle(数据从 at 开始长出,dur 默认按根数;柱状图可写多个 bar cue,各带 data.index(数或数组)=这几根在这一刻才长,让每根柱子踩自己的那个词)
12
+ // series 每项可写 color(柱子自己的颜色,和片里别处同一个量的配色对上)· highlight(at 这一帧标注圆点弹出;text/sub 覆盖 data.highlight,data.index 指定第几根)
13
+ // number(大数字:data {value, prefix, suffix, decimals},at = 数字落定的时刻,提前 0.9s 开始滚)
14
+ // 规矩:柱一律从 0 起;折线/K 线纵轴不从 0 起时自动在副标题后加「纵轴未从 0 开始」。
15
+ CLIPS.t3_finance_chart = (() => {
16
+ const { clamp, lerp } = U;
17
+ const THEMES = {
18
+ light: { bg: '#FFFFFF', ink: '#0C0C0C', sub: '#4F5B61', src: '#7c868b', tick: '#5b666b', xlab: '#3a4246', grid: 'rgba(12,12,12,0.13)', red: '#E3120B', bar: '#006BA2', dim: '#C6D2D8' },
19
+ dark: { bg: '#0E1116', ink: '#F2F4F5', sub: '#A7B2B8', src: '#848F95', tick: '#9AA5AB', xlab: '#C3CBD0', grid: 'rgba(255,255,255,0.14)', red: '#FF4B3E', bar: '#3EBCD2', dim: '#3A4650' },
20
+ };
21
+ const lum = col => { const m = String(col).trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); if (!m) return 1;
22
+ const h = m[1].length === 3 ? [...m[1]].map(x => x + x).join('') : m[1], [r, g, b] = [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16) / 255);
23
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
24
+ // 旧主题写法('light' / 'dark' / 不带色板键的对象)不当全片色板解析(clip.js 认这个钩子)
25
+ const legacy = th => th === 'light' || th === 'dark' || (!!th && typeof th === 'object' && !Array.isArray(th) && th.name == null && !['surface', 'accent', 'accent2'].some(k => k in th));
26
+ // 全片色板 → 图表的一整套色:辅助色都从 6 个键派生
27
+ const fromPal = p => ({ bg: p.bg, ink: p.ink, sub: p.sub, src: PAL.mix(p.sub, p.bg, 0.2), tick: p.sub, xlab: PAL.mix(p.ink, p.sub, 0.45), grid: PAL.alpha(p.ink, 0.13),
28
+ red: p.accent, bar: p.accent2, dim: PAL.mix(p.bg, p.sub, 0.3), dark: PAL.dark(p) });
29
+ function palette(ctx) {
30
+ if (ctx.pal) return fromPal(ctx.pal);
31
+ const T = (typeof ctx.theme === 'string' || (ctx.theme && Object.keys(ctx.theme).length)) ? ctx.theme : ctx.data.theme;
32
+ if (typeof T === 'string' && !legacy(T) && window.PALETTES[T]) return fromPal(PAL.resolve(T)); // data.theme 写了色板名
33
+ if (typeof T === 'string') return { ...(THEMES[T] || THEMES.light) };
34
+ const bg = (T && T.bg) || (ctx.safe && ctx.safe.fill) || null;
35
+ const base = bg ? (lum(bg) < 0.45 ? THEMES.dark : THEMES.light) : THEMES.light;
36
+ return { ...base, ...(bg ? { bg } : {}), ...(T || {}) };
37
+ }
38
+ let RED, INK, SUB, GRID, P;
39
+ const CN = '"PuHui-Medium"', CNB = '"PuHui-Bold"', NUM = '"RobotoCondensed", "PuHui-Medium"';
40
+ let L, BUF;
41
+ const fmtv = (d, v) => (d.prefix || '') + TY.fmt(v, d.decimals ?? 1) + (d.suffix || '');
42
+ return {
43
+ fonts: ['PuHui-Medium', 'PuHui-Bold'],
44
+ safe: true,
45
+ legacyTheme: legacy,
46
+ init(ctx) {
47
+ P = palette(ctx); RED = P.red; INK = P.ink; SUB = P.sub; GRID = P.grid;
48
+ const { W, H, u, data: d } = ctx, m = (ctx.portrait ? 70 : 90) * u, c = document.createElement('canvas').getContext('2d');
49
+ const kind = (ctx.of('bar', 'line', 'candle')[0] || {}).kind || d.chart || 'bar';
50
+ const S = d.series || [];
51
+ const vals = kind === 'candle' ? S.flatMap(r => [r.h, r.l]) : S.map(r => r.value);
52
+ let lo = Math.min(...vals), hi = Math.max(...vals);
53
+ let zeroBased = kind === 'bar' || lo <= 0 || (hi - lo) / hi > 0.6;
54
+ if (zeroBased) lo = Math.min(0, lo);
55
+ const pad = (hi - lo) * 0.08, ticks = CH.ticks(zeroBased ? lo : lo - pad, hi + pad, ctx.portrait ? 5 : 4);
56
+ const step = ticks[1] - ticks[0]; // 刻度要把数据整个包住(CH.ticks 只给区间内的整刻度)
57
+ while (ticks[ticks.length - 1] < hi + pad * 0.5) ticks.push(+(ticks[ticks.length - 1] + step).toFixed(10));
58
+ while (ticks[0] > (zeroBased ? lo : lo - pad * 0.5)) ticks.unshift(+(ticks[0] - step).toFixed(10));
59
+ const titleSz = (ctx.portrait ? 64 : 58) * u, t = TY.fit(c, d.title || '', W - 2 * m, titleSz, 'PuHui-Bold', { maxLines: ctx.portrait ? 3 : 2, weight: '700 ' });
60
+ const sub = (d.unit || '') + (!zeroBased ? (d.unit ? ' ' : '') + '纵轴未从 0 开始' : '');
61
+ // 副标题、来源行:放不下先缩一点,再折两行(竖屏长口径会出画);来源行要让开右下角标
62
+ const fsz = ctx.portrait ? 1.2 : 1, subF = TY.fit(c, sub, W - 2 * m, 32 * u * fsz, 'PuHui-Medium', { maxLines: 2, min: 0.8 });
63
+ c.font = `700 ${28 * u}px "PuHui-Medium"`; const badgeW = d.badge ? c.measureText(d.badge).width + 28 + 30 * u : 0;
64
+ const srcF = TY.fit(c, d.source || '', W - 2 * m - badgeW, 26 * u * fsz, 'PuHui-Medium', { maxLines: 2, min: 0.8 });
65
+ const top = ctx.safe.top + 80 * u + t.lines.length * t.size * 1.25 + 60 * u + (subF.lines.length - 1) * subF.size * 1.3;
66
+ const band = ctx.of('number').length ? 150 * u : 0; // 有 number:给大数字单独留一条,不压在图区里(折线一上涨,右上角正好是线头)
67
+ const F = { x: m + 20 * u, y: top + 40 * u + band, w: W - 2 * m - 40 * u, h: H - top - 40 * u - band - (ctx.portrait ? 260 : 190) * u - ctx.safe.bottom - (srcF.lines.length - 1) * srcF.size * 1.3 }; // safe.bottom:字幕带整条让出来,来源行落在它上面
68
+ const n = S.length, slot = (F.w - 80 * u) / n, xs = S.map((_, i) => F.x + slot * (i + 0.5)); // 右边留 80px 给刻度值,别压在最后一根柱上
69
+ // x 轴:时间序列(折线/K 线,或 data.xAxis:'time')按间隔抽稀;类目(柱状图默认)每根都要有名字,放不下就缩字、再放不下错成两行
70
+ const category = (d.xAxis || (kind === 'bar' ? 'category' : 'time')) === 'category';
71
+ const fs0 = ctx.portrait ? 1.2 : 1, xf = 30 * u * fs0;
72
+ c.font = `400 ${xf}px "RobotoCondensed", "PuHui-Medium"`;
73
+ const maxW = Math.max(1, ...S.map(r => c.measureText(String(r.label ?? '')).width));
74
+ const xScale = category ? clamp(slot * 0.94 / maxW, 0.7, 1) : 1, stagger = category && slot * 0.94 < maxW * 0.7;
75
+ // 刻度标签按步长定小数位:步长 2.5 就要 1 位,不然 −2.5/2.5/7.5 会被四舍五入印成 −3/3/8(刻度撒谎)
76
+ let tdec = 0; while (tdec < 4 && Math.abs(step * 10 ** tdec - Math.round(step * 10 ** tdec)) > 1e-6) tdec++;
77
+ if (kind === 'bar' && Math.min(...vals) < 0) { // 负数柱的数值写在柱底下方:最低那根下面留出一行,不然压到类目名
78
+ const vmin = Math.min(...vals), need = 56 * u * fs0;
79
+ while ((vmin - ticks[0]) / (ticks[ticks.length - 1] - ticks[0]) * F.h < need) ticks.unshift(+(ticks[0] - step).toFixed(10));
80
+ }
81
+ const qh1 = ctx.of('highlight')[0], H1 = d.highlight || {}, hiI = qh1 ? (qh1.data && qh1.data.index != null ? qh1.data.index : (H1.index != null ? H1.index : S.length - 1)) : -1;
82
+ if (kind === 'bar' && hiI >= 0 && S[hiI] && S[hiI].value >= 0 && S.some((r, j) => j !== hiI && r.value >= S[hiI].value)) { // 标注的不是最高那根:文字块要抬到邻柱之上,顶上留够一块(不够就加一档刻度)
83
+ const nL = [C(qh1.text, H1.text), C(qh1.sub, H1.sub)].filter(Boolean).length, need = (52 * nL + 44 + 30) * u * fs0, vmax = Math.max(...vals);
84
+ while ((ticks[ticks.length - 1] - vmax) / (ticks[ticks.length - 1] - ticks[0]) * F.h < need) ticks.push(+(ticks[ticks.length - 1] + step).toFixed(10));
85
+ }
86
+ L = { subF, srcF, m, kind, S, zeroBased, ticks, tdec, yS: CH.lin(ticks[0], ticks[ticks.length - 1], F.y + F.h, F.y), F, xs, slot, t, sub, band, category, xFont: xf * xScale, stagger,
87
+ every: category ? 1 : Math.max(1, Math.ceil(n / (ctx.portrait ? 6 : 10))) };
88
+ const main = (d.colors && d.colors.main) || (kind === 'bar' ? P.bar : RED);
89
+ L.col = { main, accent: (d.colors && d.colors.accent) || main, dim: (d.colors && d.colors.dim) || P.dim };
90
+ const up = P.dark != null ? (P.dark ? '#FF4B3E' : '#E3120B') : RED; // 涨跌是语义色:用了全片色板(accent 未必是红)也照旧红绿
91
+ L.updown = d.upDown === 'us' ? ['#1B9E5A', up] : [up, '#1B9E5A'];
92
+ },
93
+ draw(c0, t, ctx) {
94
+ // alpha:字和线叠在别的画面上可能看不见。先画进缓冲,再带一圈主题底色的光晕合成(和 t1 同一招;默认白纸黑字=白光晕)
95
+ const c = ctx.alpha ? (BUF = BUF || document.createElement('canvas'), BUF.width = ctx.W, BUF.height = ctx.H, BUF.getContext('2d')) : c0;
96
+ const { W, H, u, data: d } = ctx, { m, F, yS, xs, S, kind } = L, fs = ctx.portrait ? 1.2 : 1; // 竖屏在手机上看:小字放大两成
97
+ const qt = ctx.of('title')[0] || { at: 0 }, lt = ctx.lt(t, qt.at);
98
+ if (!ctx.alpha) { c.fillStyle = P.bg; c.fillRect(0, 0, W, H); }
99
+ // ---- 版式:顶红线 → 小旗 → 标题 → 副标题(单位/口径)→ 来源 → 角标 ----
100
+ if (lt > 0) {
101
+ const p = MO.quintOut(MO.seg(lt, 0, 0.45));
102
+ c.fillStyle = RED; c.fillRect(0, 0, W * p, 8 * u); CH.tag(c, m, ctx.safe.top + 52 * u, 96 * p * u, 22 * u, RED);
103
+ c.save(); c.textBaseline = 'alphabetic'; c.textAlign = 'left';
104
+ const tp = MO.quintOut(MO.seg(lt, 0.05, 0.5)); c.globalAlpha = tp; c.fillStyle = INK; c.font = `700 ${L.t.size}px ${CNB}`;
105
+ L.t.lines.forEach((ln, i) => c.fillText(ln, m, ctx.safe.top + 80 * u + (i + 1) * L.t.size * 1.2 + (1 - tp) * 18 * u));
106
+ const sp = MO.quintOut(MO.seg(lt, 0.15, 0.6)), sy = ctx.safe.top + 80 * u + L.t.lines.length * L.t.size * 1.2 + 54 * u;
107
+ c.globalAlpha = sp; c.font = `400 ${L.subF.size}px ${CN}`; c.fillStyle = SUB; L.subF.lines.forEach((ln, k) => c.fillText(ln, m, sy + k * L.subF.size * 1.3 + (1 - sp) * 14 * u));
108
+ c.globalAlpha = MO.seg(lt, 0.2, 0.6); c.font = `400 ${L.srcF.size}px ${CN}`; c.fillStyle = P.src;
109
+ L.srcF.lines.forEach((ln, k, a) => c.fillText(ln, m, H - 40 * u - ctx.safe.bottom - (a.length - 1 - k) * L.srcF.size * 1.3));
110
+ c.restore();
111
+ if (d.badge) { c.save(); c.globalAlpha = MO.seg(lt, 0.2, 0.6); CH.demoBadge(c, d.badge, { x: W - m, y: H - 50 * u - ctx.safe.bottom, font: `700 ${28 * u}px ${CN}`, col: RED, box: tint(RED, 0.06) }); c.restore(); }
112
+ }
113
+ // ---- 坐标系(版式之后 0.25s) ----
114
+ const qd = ctx.of('bar', 'line', 'candle')[0] || { at: qt.at + 0.75 }, gStart = Math.min(qt.at + 0.25, qd.at - 0.4);
115
+ const gp = MO.seg(ctx.lt(t, gStart), 0, 0.6);
116
+ const fmtTick = v => TY.fmt(v, L.tdec);
117
+ CH.grid(c, F, L.ticks, yS, gp, { col: GRID, zeroCol: L.zeroBased ? INK : GRID, font: `400 ${28 * u * fs}px ${NUM}`, labelCol: P.tick, side: 'right', lw: 1.5 * u, zeroLw: 3 * u, fmt: fmtTick, labelDy: -10 * u });
118
+ const qh0 = ctx.of('highlight')[0], hi0 = qh0 ? (qh0.data && qh0.data.index != null ? qh0.data.index : (d.highlight && d.highlight.index != null ? d.highlight.index : S.length - 1)) : -1;
119
+ const keep = i => i % L.every === 0 || i === hi0 || (i === S.length - 1 && (S.length - 1) % L.every >= L.every / 2); // 被标注的那根永远有名字;末尾标签离上一个太近就不硬塞
120
+ const xp = MO.seg(ctx.lt(t, gStart + 0.15), 0, 0.5), xy = F.y + F.h + 46 * u, xo = { font: `400 ${L.xFont}px ${NUM}`, col: P.xlab };
121
+ if (!L.stagger) CH.xLabels(c, S.map((r, i) => ({ x: xs[i], label: keep(i) ? String(r.label ?? '') : '' })), xy, xp, xo);
122
+ else { CH.xLabels(c, S.map((r, i) => ({ x: xs[i], label: i % 2 ? '' : String(r.label ?? '') })), xy, xp, xo); CH.xLabels(c, S.map((r, i) => ({ x: xs[i], label: i % 2 ? String(r.label ?? '') : '' })), xy + L.xFont * 1.15, xp, xo); }
123
+ // ---- 数据 ----
124
+ const n = S.length, ddur = qd.dur || Math.min(2.2, Math.max(0.9, n * (kind === 'bar' ? 0.22 : 0.06)));
125
+ const dp = ctx.p(t, qd.at, ddur);
126
+ const qh = ctx.of('highlight')[0], hi = qh ? (qh.data && qh.data.index != null ? qh.data.index : (d.highlight && d.highlight.index != null ? d.highlight.index : n - 1)) : -1;
127
+ const hiP = qh ? MO.smooth(ctx.p(t, qh.at, 0.4)) : 0;
128
+ let anchor = null;
129
+ if (kind === 'bar') {
130
+ const bw = Math.min(150 * u, L.slot * 0.62);
131
+ const on = qh && ctx.lt(t, qh.at) > 0, opt = { col: L.col.main, hiP, hiCol: L.col.accent, dimCol: L.col.dim,
132
+ fmt: v => fmtv(d, v), font: `600 ${Math.max(24 * u * fs, Math.min(34 * u * fs, L.slot * 0.3))}px ${NUM}`, labelCol: INK, label: n <= 14 };
133
+ const own = {}; // 带 data.index 的 bar cue:这几根柱子从它的 at 起单独长
134
+ for (const q of ctx.of('bar')) if (q.data && q.data.index != null) [].concat(q.data.index).forEach(i => { own[i] = q; });
135
+ let tops;
136
+ if (!Object.keys(own).length && !S.some(r => r.color)) tops = CH.bars(c, S.map((r, i) => ({ x: xs[i], v: r.value })), yS, bw, dp, { ...opt, lag: Math.min(0.22, 2 / n), hi: on ? hi : -1 });
137
+ else tops = S.map((r, i) => {
138
+ const q = own[i], p = q ? ctx.p(t, q.at, q.dur || 0.7) : MO.lagged(i, n, dp, Math.min(0.22, 2 / n));
139
+ const col = r.color || L.col.main;
140
+ return CH.bars(c, [{ x: xs[i], v: r.value }], yS, bw, p, { ...opt, lag: 0, col, hiCol: col, hi: on ? (i === hi ? 0 : 1) : -1 })[0];
141
+ });
142
+ if (hi >= 0) anchor = [xs[hi], tops[hi][1]];
143
+ L.neg = hi >= 0 && S[hi].value < 0; L.tops = tops;
144
+ L.barAbove = hi >= 0 && S[hi].value >= 0 && S.some((r, j) => j !== hi && r.value > S[hi].value * 1.5); // 有更高的柱子挡在旁边:标注放在这根柱子正上方,别压进别的柱子
145
+ } else if (kind === 'line') {
146
+ const pts = S.map((r, i) => [xs[i], yS(r.value)]);
147
+ const lp = MO.smooth(dp);
148
+ const hp = CH.line(c, pts, lp, { col: L.col.main, lw: 5 * u, headR: 9 * u, area: { top: F.y, base: F.y + F.h, c0: tint(RED, 0.12), c1: tint(RED, 0) } });
149
+ if (hp && lp < 1) { const v = yS.inv(hp[1]); c.save(); c.font = `700 ${36 * u}px ${NUM}`; c.fillStyle = L.col.main; c.textAlign = 'left'; c.textBaseline = 'middle'; TY.tabular(c, fmtv(d, v), hp[0] + 20 * u, hp[1] - 4 * u); c.restore(); }
150
+ else if (lp >= 1) { const v = S[n - 1].value; c.save(); c.font = `700 ${36 * u}px ${NUM}`; c.fillStyle = L.col.main; c.textAlign = 'right'; c.textBaseline = 'alphabetic'; TY.tabular(c, fmtv(d, v), pts[n - 1][0], pts[n - 1][1] - 24 * u, { align: 'right' }); c.restore(); }
151
+ if (hi >= 0) anchor = pts[hi];
152
+ } else {
153
+ const cw = Math.min(30 * u, L.slot * 0.6);
154
+ CH.candles(c, S, xs, yS, cw, dp, { up: L.updown[0], down: L.updown[1], lag: Math.min(0.06, 1.5 / n) });
155
+ if (hi >= 0) anchor = [xs[hi], yS(S[hi].c)];
156
+ }
157
+ // ---- 标注:一镜只标一件事 ----
158
+ let above = L.barAbove;
159
+ if (qh && anchor && kind === 'bar' && !above && !L.neg) { // 预演斜向标注的文字框:压到别的柱子或它的数值,就改放这根柱子正上方
160
+ const H0 = d.highlight || {}, ls = [C(qh.text, H0.text), C(qh.sub, H0.sub)].filter(Boolean);
161
+ const ax = anchor[0] - Math.min(150 * u, L.slot * 0.62) / 2, right = ax > F.x + F.w * 0.55, tx = right ? ax - 60 * u : ax + 60 * u, ty = Math.max(F.y + 60 * u, anchor[1] - 140 * u);
162
+ c.save(); c.font = `700 ${50 * u}px ${NUM}`; let tw = c.measureText(ls[0] || '').width; c.font = `400 ${28 * u * fs}px ${CN}`; if (ls[1]) tw = Math.max(tw, c.measureText(ls[1]).width); c.restore();
163
+ const bx0 = right ? tx - 12 - tw : tx + 12, bx1 = bx0 + tw, by1 = ty + (ls.length > 1 ? 56 : 16) * u;
164
+ above = L.tops.some(([bx, by], j) => j !== hi && bx + L.slot * 0.45 > bx0 && bx - L.slot * 0.45 < bx1 && by - 44 * u * fs < by1);
165
+ }
166
+ if (qh && anchor && kind === 'bar' && above) {
167
+ const H0 = d.highlight || {}, lines = [C(qh.text, H0.text), C(qh.sub, H0.sub)].filter(Boolean), k = ctx.lt(t, qh.at);
168
+ if (k > 0) {
169
+ const s = MO.spring(k, { duration: 0.45, bounce: 0.25 }), lp = MO.quintOut(MO.seg(k, 0.12, 0.45)), fp = MO.quintOut(MO.seg(k, 0.3, 0.7));
170
+ // 文字块放在这根柱子正上方:先量宽,横向夹在图区里;再抬到它横跨的所有柱子(含数值标签)之上,引线从本柱数值标签上沿连上去
171
+ const [x, y] = anchor, lh = 52 * u * fs, vlab = 44 * u * fs;
172
+ c.save(); c.font = `700 ${50 * u * fs}px ${NUM}`; let tw = c.measureText(lines[0]).width; c.font = `400 ${28 * u * fs}px ${CN}`; if (lines[1]) tw = Math.max(tw, c.measureText(lines[1]).width);
173
+ const cx = clamp(x, F.x + tw / 2, F.x + F.w - 80 * u - tw / 2);
174
+ const cover = L.tops.filter(([bx]) => bx > cx - tw / 2 - L.slot / 2 && bx < cx + tw / 2 + L.slot / 2).map(([, by]) => by);
175
+ const bottom = Math.max(F.y + lh * lines.length, Math.min(y, ...cover) - vlab - 18 * u); // 文字块底边
176
+ c.fillStyle = RED; c.beginPath(); c.arc(x, y - 4 * u, 9 * u * Math.max(0, s), 0, 7); c.fill();
177
+ const y0 = y - vlab, y1 = bottom + 8 * u;
178
+ if (y0 - y1 > 6 * u) { c.strokeStyle = INK; c.lineWidth = 2 * u; c.beginPath(); c.moveTo(x, y0); c.lineTo(lerp(x, cx, lp), lerp(y0, y1, lp)); c.stroke(); }
179
+ c.globalAlpha = fp; c.fillStyle = INK; c.textAlign = 'center'; c.textBaseline = 'alphabetic';
180
+ lines.forEach((ln, i) => { c.font = i === 0 ? `700 ${50 * u * fs}px ${NUM}` : `400 ${28 * u * fs}px ${CN}`; c.fillText(ln, cx, bottom - (lines.length - 1 - i) * lh * (i === 0 ? 1 : 0.62) + (1 - fp) * 12 * u); });
181
+ c.restore();
182
+ }
183
+ } else if (qh && anchor) {
184
+ if (kind === 'bar') anchor = [anchor[0] - Math.min(150 * u, L.slot * 0.62) / 2, anchor[1]];
185
+ const H0 = d.highlight || {}, lines = [C(qh.text, H0.text), C(qh.sub, H0.sub)].filter(Boolean);
186
+ const right = anchor[0] > F.x + F.w * 0.55, tx = right ? anchor[0] - 60 * u : anchor[0] + 60 * u;
187
+ const ty = L.neg ? Math.min(F.y + F.h - 90 * u, anchor[1] + 70 * u) : Math.max(F.y + 60 * u, anchor[1] - 140 * u); // 负数柱:标注往下走,别压回零线上方的柱子
188
+ CH.callout(c, ctx.lt(t, qh.at), 0, { x: anchor[0], y: anchor[1], tx, ty, lines, col: INK, dotCol: RED, font: `700 ${50 * u}px ${NUM}`, font2: `400 ${28 * u * fs}px ${CN}`, align: right ? 'right' : 'left', r: 9 * u });
189
+ }
190
+ // ---- 大数字(number cue:at = 落定时刻) ----
191
+ for (const q of ctx.of('number')) {
192
+ const nd = q.data || {}, k = MO.expoOut(clamp((t - (q.at - 0.9)) / 0.9)); if (t < q.at - 0.9) continue;
193
+ c.save(); c.globalAlpha = clamp((t - (q.at - 0.9)) / 0.2); c.font = `700 ${120 * u}px ${NUM}`; c.fillStyle = RED; c.textAlign = 'right'; c.textBaseline = 'alphabetic';
194
+ const numStr = (nd.prefix || '') + TY.fmt((nd.value || 0) * k, nd.decimals ?? 0) + (nd.suffix || ''), by = F.y - 62 * u; // 落在图区上方专留的那一条(让开顶格刻度值)
195
+ const nw = c.measureText((nd.prefix || '') + TY.fmt(nd.value || 0, nd.decimals ?? 0) + (nd.suffix || '')).width;
196
+ TY.tabular(c, numStr, W - m, by, { align: 'right' });
197
+ const cap = q.text ?? q.sub; if (cap) { c.font = `400 ${30 * u * fs}px ${CN}`; c.fillStyle = SUB; c.textAlign = 'right'; c.fillText(cap, W - m - nw - 24 * u, by - 10 * u); }
198
+ c.restore();
199
+ }
200
+ if (ctx.alpha) { c0.save(); c0.shadowColor = P.bg; for (const b of [8, 3, 1.5]) { c0.shadowBlur = b * ctx.u; c0.drawImage(BUF, 0, 0); } c0.restore(); } // 三遍由宽到窄:外圈柔光+贴字的实边
201
+ },
202
+ };
203
+ function C(...a) { return a.find(v => v != null && v !== ''); }
204
+ function tint(col, a) { const m = String(col).trim().match(/^#([0-9a-f]{6})$/i); if (!m) return a ? 'rgba(227,18,11,' + a + ')' : 'rgba(227,18,11,0)';
205
+ const n = parseInt(m[1], 16); return `rgba(${n >> 16},${(n >> 8) & 255},${n & 255},${a})`; }
206
+ })();