@biunow/agent-biuart 0.0.0-stage → 0.1.0

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,186 @@
1
+ // 卡通与扁平插画 TOON:Kurzgesagt 式「无描边、双色分面」、柔光;故事型(storytime)极简豆子角色(默认中性造型,可换预设),
2
+ // pose-to-pose 换姿势(快切+身体挤压回弹)、眨眼时刻表、口型读口播包络(一拍二)、汗滴、漫画符号。
3
+ (() => {
4
+ let W = 1920, H = 1080; U.onStage((w, h) => { W = w; H = h; }); // 画布尺寸跟 U.setStage 走(默认 1920×1080)
5
+ const { clamp, lerp } = U;
6
+ const TOON = window.TOON = {};
7
+
8
+ // ---------- 扁平插画 ----------
9
+ // 双色分面(无描边、靠色块分面):clip(S) → 整块阴影色 → 往光源方向平移 v 填高光 → 再往回挪 w 填本色:右下留阴影带、左上留细高光边
10
+ TOON.flat = (c, path, base, sh, hi, v = [10, 12], w = [4, 5]) => {
11
+ c.save(); c.clip(path);
12
+ c.fillStyle = sh || base; c.fill(path);
13
+ c.save(); c.translate(-v[0], -v[1]); c.fillStyle = hi || base; c.fill(path); c.restore();
14
+ c.save(); c.translate(-v[0] + w[0], -v[1] + w[1]); c.fillStyle = base; c.fill(path); c.restore();
15
+ c.restore();
16
+ };
17
+ // 柔光:径向渐变方块(发光物、眼睛、信号脉冲)
18
+ TOON.glow = (c, x, y, r, col, a = 1) => { const g = c.createRadialGradient(x, y, 0, x, y, r); g.addColorStop(0, PAINT.rgb(PAINT.hex(col), a)); g.addColorStop(1, PAINT.rgb(PAINT.hex(col), 0)); c.fillStyle = g; c.fillRect(x - r, y - r, 2 * r, 2 * r); };
19
+ // 竖向渐变(天空)
20
+ TOON.vgrad = (c, y0, y1, cols) => { const g = c.createLinearGradient(0, y0, 0, y1); cols.forEach((k, i) => g.addColorStop(i / (cols.length - 1), k)); return g; };
21
+ TOON.circle = (x, y, r) => { const p = new Path2D(); p.arc(x, y, r, 0, Math.PI * 2); return p; };
22
+ TOON.ellipse = (x, y, rx, ry, a = 0) => { const p = new Path2D(); p.ellipse(x, y, rx, ry, a, 0, Math.PI * 2); return p; };
23
+
24
+ // ---------- 口型:读口播包络 ----------
25
+ // window.VO_ENV = {fps, a:[...]}(0..1 的 RMS 包络,由音频脚本从口播 wav 生成)。口型「一拍二」:每 1/12 秒才换一次形。
26
+ TOON.env = t => { const E = window.VO_ENV; if (!E) return 0; const i = Math.round(t * E.fps); return i < 0 || i >= E.a.length ? 0 : E.a[i]; };
27
+ TOON.mouth = (t, fps = 12) => { const v = TOON.env(MO.step(t, fps)); return v < 0.06 ? 0 : v < 0.22 ? 1 : 2; }; // 0 闭 1 半 2 张
28
+
29
+ // ---------- 豆子角色(故事型极简角色) ----------
30
+ // 正面、圆头、豆子身体、面条手臂。造型由 o.char 决定(预设名或对象),默认 'neutral':
31
+ // neutral 短发、蓝 T、深色裤子,不戴帽子和眼镜——给任何人的故事当主角
32
+ // bun 丸子头、芥黄 T、深色裤子
33
+ // author 本 skill 作者的卡通形象(纯白渔夫帽、圆框黑眼镜、白 T、卡其短裤、黑手表)。只在讲作者自己的故事时显式指定;
34
+ // 作者形象不随 MIT 授权(见 README)
35
+ // 对象:{ base: 预设名, skin, hair, shirt, pants, shoe, hairStyle: 'short'|'bun'|'none', hat: true/false, glasses: true/false, watch: true/false } 覆盖预设里的项
36
+ // o: { x, y(脚底), s(头半径 px), char, pose:{l,r}(手臂目标点,相对肩,头半径单位), expr, mouth(0/1/2), blink, look:[dx,dy](眼珠方向),
37
+ // squash(竖向缩放,以脚底为轴,体积守恒), tilt(头歪), bob(头下沉), browUp, lw }
38
+ // expr: 'talk' | 'happy' | 'blank'(死鱼眼) | 'worry' | 'shock'。返回 { hl, hr(两手位置), head:[x,y] }
39
+ const LINE = '#1B1B1F', BAND = '#E4E4E8', HATC = '#FFFFFF';
40
+ TOON.LOOKS = {
41
+ neutral: { skin: '#F3CDAE', hair: '#2A2420', hairStyle: 'short', shirt: '#6E9CCB', pants: '#3E4656', shoe: '#FAFAF6', hat: false, glasses: false, watch: false },
42
+ bun: { skin: '#E9B994', hair: '#3B2A22', hairStyle: 'bun', shirt: '#E2B33C', pants: '#3E4656', shoe: '#FAFAF6', hat: false, glasses: false, watch: false },
43
+ author: { skin: '#FFE2CC', hair: '#1B1B1F', hairStyle: 'none', shirt: '#FFFFFF', pants: '#CDBB97', shoe: '#FAFAF6', hat: true, glasses: true, watch: true },
44
+ };
45
+ // 预设名或对象 → 完整造型;不认识的预设名 console.error(qa / render 判失败),不悄悄换成别的
46
+ TOON.look = ch => {
47
+ if (ch == null) return TOON.LOOKS.neutral;
48
+ if (typeof ch === 'string') { if (TOON.LOOKS[ch]) return TOON.LOOKS[ch]; console.error(`TOON:没有这个角色预设「${ch}」(可选 ${Object.keys(TOON.LOOKS).join(' / ')},或传造型对象)`); return TOON.LOOKS.neutral; }
49
+ return { ...(TOON.LOOKS[ch.base] || TOON.LOOKS.neutral), ...ch };
50
+ };
51
+ TOON.POSES = {
52
+ rest: { l: [-0.95, 1.05], r: [0.95, 1.05] },
53
+ talk: { l: [-0.95, 1.05], r: [1.05, 0.55] }, // 一只手微抬,口播默认
54
+ point: { l: [-0.95, 1.05], r: [1.75, -0.15] }, // 指向画右
55
+ type: { l: [-0.55, 0.62], r: [0.55, 0.62] }, // 双手在胸前(打字)
56
+ shrug: { l: [-1.45, -0.05], r: [1.45, -0.05], palms: 1 },
57
+ stiff: { l: [-0.78, 1.25], r: [0.78, 1.25] }, // 僵住
58
+ };
59
+ TOON.bean = (c, o) => {
60
+ const R = o.s, x = o.x, y = o.y, lw = o.lw || Math.max(4, R * 0.05), sq = o.squash || 1, LK = TOON.look(o.char);
61
+ const SK = LK.skin, SHIRT = LK.shirt, SHORTS = LK.pants, HAIR = LK.hair;
62
+ const pose = o.pose || TOON.POSES.rest;
63
+ c.save(); c.translate(x, y); c.scale(1 / Math.sqrt(sq), sq);
64
+ c.lineJoin = 'round'; c.lineCap = 'round'; c.strokeStyle = LINE; c.lineWidth = lw;
65
+ const bodyTop = -R * 1.32;
66
+ const shoulderY = bodyTop + R * 0.32, shL = [-R * 0.42, shoulderY], shR = [R * 0.42, shoulderY];
67
+ for (const sx of [-1, 1]) { // 腿:两截短圆柱+白鞋
68
+ c.fillStyle = SK; c.beginPath(); c.roundRect(sx * R * 0.2 - R * 0.09, -R * 0.24, R * 0.18, R * 0.2, R * 0.05); c.fill(); c.stroke();
69
+ c.fillStyle = LK.shoe; c.beginPath(); c.ellipse(sx * R * 0.22, -R * 0.03, R * 0.17, R * 0.08, 0, 0, Math.PI * 2); c.fill(); c.stroke();
70
+ }
71
+ const body = new Path2D(); // 身体(豆子)
72
+ body.moveTo(-R * 0.5, -R * 0.2);
73
+ body.bezierCurveTo(-R * 0.62, -R * 0.75, -R * 0.55, bodyTop + R * 0.05, 0, bodyTop);
74
+ body.bezierCurveTo(R * 0.55, bodyTop + R * 0.05, R * 0.62, -R * 0.75, R * 0.5, -R * 0.2);
75
+ body.bezierCurveTo(R * 0.3, -R * 0.12, -R * 0.3, -R * 0.12, -R * 0.5, -R * 0.2); body.closePath();
76
+ c.fillStyle = SHIRT; c.fill(body);
77
+ c.save(); c.clip(body); c.fillStyle = SHORTS; c.fillRect(-R, -R * 0.42, R * 2, R * 0.5);
78
+ c.strokeStyle = LINE; c.lineWidth = lw * 0.6; c.beginPath(); c.moveTo(0, -R * 0.36); c.lineTo(0, -R * 0.12); c.stroke(); c.restore();
79
+ c.stroke(body);
80
+ c.beginPath(); c.moveTo(-R * 0.6, -R * 0.42); c.quadraticCurveTo(0, -R * 0.36, R * 0.6, -R * 0.42); c.lineWidth = lw * 0.8; c.stroke(); c.lineWidth = lw;
81
+ // 面条手臂:肩 → 手(二次曲线,肘自然下垂),双描边做出「管子」
82
+ const arm = (sh, tgt, side) => {
83
+ const hx = sh[0] + tgt[0] * R * 0.62, hy = sh[1] + tgt[1] * R * 0.62;
84
+ const mx = (sh[0] + hx) / 2 + side * R * 0.08, my = Math.max(sh[1], hy) + R * 0.12;
85
+ c.beginPath(); c.moveTo(sh[0], sh[1]); c.quadraticCurveTo(mx, my, hx, hy);
86
+ c.strokeStyle = LINE; c.lineWidth = R * 0.17 + lw; c.stroke(); c.strokeStyle = SK; c.lineWidth = R * 0.17; c.stroke();
87
+ c.beginPath(); c.moveTo(sh[0], sh[1]); const q = 0.28; c.lineTo(lerp(sh[0], mx, q * 2), lerp(sh[1], my, q * 2));
88
+ c.strokeStyle = LINE; c.lineWidth = R * 0.26 + lw; c.stroke(); c.strokeStyle = SHIRT; c.lineWidth = R * 0.26; c.stroke();
89
+ c.fillStyle = SK; c.strokeStyle = LINE; c.lineWidth = lw; c.beginPath(); c.arc(hx, hy, R * 0.13, 0, Math.PI * 2); c.fill(); c.stroke();
90
+ if (side > 0 && LK.watch) { // 近侧(画右)手腕黑手表
91
+ const ang = Math.atan2(hy - my, hx - mx), wx = hx - Math.cos(ang) * R * 0.17, wy = hy - Math.sin(ang) * R * 0.17;
92
+ c.save(); c.translate(wx, wy); c.rotate(ang + Math.PI / 2); c.fillStyle = '#1D1F22'; c.fillRect(-R * 0.1, -R * 0.04, R * 0.2, R * 0.08); c.restore();
93
+ }
94
+ return [hx, hy];
95
+ };
96
+ c.lineWidth = lw;
97
+ const hl = arm(shL, pose.l, -1), hr = arm(shR, pose.r, 1);
98
+ // 头:略宽的圆,压在身体上
99
+ c.restore(); c.save(); c.translate(x, y); c.scale(1 / Math.sqrt(sq), sq);
100
+ c.lineJoin = 'round'; c.lineCap = 'round'; c.strokeStyle = LINE; c.lineWidth = lw;
101
+ const hy = bodyTop - R * 0.78 + (o.bob || 0);
102
+ c.translate(0, hy); c.rotate(o.tilt || 0);
103
+ if (LK.hairStyle === 'bun') { c.fillStyle = HAIR; c.beginPath(); c.arc(0, -R * 0.98, R * 0.34, 0, Math.PI * 2); c.fill(); c.stroke(); } // 丸子(压在头后面)
104
+ if (LK.hat) { c.fillStyle = HAIR; // 鬓角(帽檐下露一点黑发)
105
+ for (const sx of [-1, 1]) { c.beginPath(); c.ellipse(sx * R * 0.86, -R * 0.12, R * 0.16, R * 0.3, sx * 0.2, 0, Math.PI * 2); c.fill(); } }
106
+ c.fillStyle = SK; c.beginPath(); c.ellipse(0, 0, R * 1.0, R * 0.93, 0, 0, Math.PI * 2); c.fill(); c.stroke();
107
+ if (LK.hairStyle === 'short' || LK.hairStyle === 'bun') { // 头发:比头略大的一顶,前沿是一道带分缝的刘海,两侧盖到耳朵上沿
108
+ const edge = new Path2D(); edge.moveTo(-R * 1.2, R * 0.05);
109
+ for (let i = 0; i <= 24; i++) { const k = i / 24, xx = -R * 1.05 + k * R * 2.1, ax = Math.abs(xx) / R, side = U.ss(0.55, 0.98, ax);
110
+ edge.lineTo(xx, -R * 0.42 + R * 0.06 * Math.cos(k * Math.PI * 3) * (1 - side) + side * R * (LK.hairStyle === 'bun' ? 0.32 : 0.45) + (xx > R * 0.15 && xx < R * 0.35 ? -R * 0.08 : 0)); }
111
+ edge.lineTo(R * 1.2, R * 0.05); edge.lineTo(R * 1.2, -R * 1.4); edge.lineTo(-R * 1.2, -R * 1.4); edge.closePath();
112
+ c.save(); c.clip(edge); c.fillStyle = HAIR; c.beginPath(); c.ellipse(0, -R * 0.05, R * 1.05, R * 0.99, 0, 0, Math.PI * 2); c.fill(); c.stroke(); c.restore();
113
+ }
114
+ if (LK.hat) {
115
+ // 帽子:帽冠+帽带+往下斜的帽檐
116
+ const crown = new Path2D(); crown.moveTo(-R * 0.78, -R * 0.42); crown.bezierCurveTo(-R * 0.8, -R * 1.25, R * 0.8, -R * 1.25, R * 0.78, -R * 0.42); crown.closePath();
117
+ c.fillStyle = HATC; c.fill(crown); c.stroke(crown);
118
+ c.save(); c.clip(crown); c.fillStyle = BAND; c.fillRect(-R, -R * 0.62, R * 2, R * 0.18); c.restore();
119
+ c.beginPath(); c.moveTo(-R * 0.79, -R * 0.62); c.quadraticCurveTo(0, -R * 0.7, R * 0.79, -R * 0.62); c.lineWidth = lw * 0.7; c.stroke(); c.lineWidth = lw;
120
+ const brim = new Path2D(); brim.moveTo(-R * 1.18, -R * 0.24); brim.quadraticCurveTo(0, -R * 0.66, R * 1.18, -R * 0.24);
121
+ brim.quadraticCurveTo(R * 1.22, -R * 0.12, R * 1.08, -R * 0.12); brim.quadraticCurveTo(0, -R * 0.44, -R * 1.08, -R * 0.12); brim.quadraticCurveTo(-R * 1.22, -R * 0.12, -R * 1.18, -R * 0.24); brim.closePath();
122
+ c.fillStyle = HATC; c.fill(brim); c.stroke(brim);
123
+ }
124
+ // 眉毛(表情主载体):[整体上下, 内端相对外端的高差](负 = 内端抬高 = 担心/委屈)
125
+ const ex = o.expr || 'talk', lk = o.look || [0, 0];
126
+ const brow = { talk: [0, 0], happy: [-0.05, 0.03], blank: [0.03, 0], worry: [-0.02, -0.11], shock: [-0.11, 0.02] }[ex] || [0, 0];
127
+ if (o.browUp) brow[0] -= o.browUp;
128
+ c.lineWidth = lw * 1.4;
129
+ for (const sx of [-1, 1]) { const by = -R * 0.13 + brow[0] * R; c.beginPath(); c.moveTo(sx * R * 0.52, by); c.lineTo(sx * R * 0.2, by + brow[1] * R); c.stroke(); }
130
+ c.lineWidth = lw;
131
+ // 眼睛是黑点;戴眼镜的造型(author)加两只圆框(线比轮廓粗 1.5 倍)+鼻梁
132
+ const gy = R * 0.2, gr = R * 0.27;
133
+ for (const sx of [-1, 1]) {
134
+ const gx = sx * R * 0.37;
135
+ if (LK.glasses) { c.fillStyle = 'rgba(255,255,255,0.35)'; c.beginPath(); c.arc(gx, gy, gr, 0, Math.PI * 2); c.fill(); }
136
+ const pr = (ex === 'blank' ? R * 0.035 : ex === 'shock' ? R * 0.05 : R * 0.075) * (LK.glasses ? 1 : 1.15);
137
+ c.fillStyle = LINE;
138
+ if (o.blink) { c.beginPath(); c.moveTo(gx - R * 0.08 + lk[0] * R * 0.08, gy + lk[1] * R * 0.06); c.lineTo(gx + R * 0.08 + lk[0] * R * 0.08, gy + lk[1] * R * 0.06); c.lineWidth = lw; c.stroke(); }
139
+ else { c.beginPath(); c.ellipse(gx + lk[0] * R * 0.09, gy + lk[1] * R * 0.07, pr, pr * (ex === 'blank' ? 1 : 1.25), 0, 0, Math.PI * 2); c.fill();
140
+ if (ex !== 'blank') { c.fillStyle = '#fff'; c.beginPath(); c.arc(gx + lk[0] * R * 0.09 - pr * 0.35, gy + lk[1] * R * 0.07 - pr * 0.45, pr * 0.32, 0, Math.PI * 2); c.fill(); } }
141
+ if (LK.glasses) { c.lineWidth = lw * 1.5; c.strokeStyle = LINE; c.beginPath(); c.arc(gx, gy, gr, 0, Math.PI * 2); c.stroke(); }
142
+ }
143
+ if (LK.glasses) { c.beginPath(); c.moveTo(-R * 0.1, gy - R * 0.02); c.quadraticCurveTo(0, gy - R * 0.08, R * 0.1, gy - R * 0.02); c.stroke(); }
144
+ c.lineWidth = lw;
145
+ c.fillStyle = 'rgba(255,140,120,0.35)'; for (const sx of [-1, 1]) { c.beginPath(); c.ellipse(sx * R * 0.68, R * 0.5, R * 0.11, R * 0.06, 0, 0, Math.PI * 2); c.fill(); }
146
+ // 嘴
147
+ const m = o.mouth || 0, my = R * 0.6;
148
+ c.strokeStyle = LINE; c.lineWidth = lw;
149
+ if (ex === 'blank' && !m) { c.beginPath(); c.moveTo(-R * 0.1, my); c.lineTo(R * 0.1, my); c.stroke(); }
150
+ else if (ex === 'worry' && !m) { c.beginPath(); c.moveTo(-R * 0.13, my + R * 0.03); c.quadraticCurveTo(-R * 0.05, my - R * 0.04, 0, my + R * 0.01); c.quadraticCurveTo(R * 0.06, my + R * 0.05, R * 0.13, my - R * 0.01); c.stroke(); }
151
+ else if (!m) { c.beginPath(); c.moveTo(-R * 0.13, my - R * 0.02); c.quadraticCurveTo(0, my + R * 0.1, R * 0.13, my - R * 0.02); c.stroke(); }
152
+ else {
153
+ const mh = m === 1 ? R * 0.1 : R * 0.2, mw = m === 1 ? R * 0.14 : R * 0.18;
154
+ c.fillStyle = '#6E2430'; c.beginPath(); c.moveTo(-mw, my - R * 0.03); c.quadraticCurveTo(0, my - R * 0.06, mw, my - R * 0.03); c.quadraticCurveTo(mw * 0.9, my + mh, 0, my + mh); c.quadraticCurveTo(-mw * 0.9, my + mh, -mw, my - R * 0.03); c.closePath(); c.fill();
155
+ c.save(); c.clip(); c.fillStyle = '#F07B7B'; c.beginPath(); c.ellipse(0, my + mh, mw * 0.6, mh * 0.45, 0, 0, Math.PI * 2); c.fill(); c.restore(); c.stroke();
156
+ }
157
+ c.restore();
158
+ return { hl, hr, head: [x, y + hy * sq] };
159
+ };
160
+
161
+ // ---------- pose-to-pose ----------
162
+ // keys = [[t, poseName, expr], ...](同一条时间轴的秒)。换姿势:手臂 0.1s expoOut 过去(24fps 下 3 帧的「快切」),
163
+ // 身体同时挤压 7% 后按弹簧回弹(3.5Hz / 衰减 9)。返回 { pose, expr, squash, since }
164
+ TOON.poseAt = (keys, t, poses = TOON.POSES) => {
165
+ let k = 0; for (let i = 0; i < keys.length; i++) if (t >= keys[i][0]) k = i;
166
+ const cur = poses[keys[k][1]], prv = poses[keys[Math.max(0, k - 1)][1]];
167
+ const dt = t - keys[k][0], q = k === 0 ? 1 : MO.expoOut(clamp(dt / 0.1));
168
+ const mix = (a, b) => [lerp(a[0], b[0], q), lerp(a[1], b[1], q)];
169
+ return { pose: { l: mix(prv.l, cur.l), r: mix(prv.r, cur.r), palms: cur.palms }, expr: keys[k][2], squash: k === 0 ? 1 : 1 + MO.settle(dt, -0.07, 3.5, 9), since: dt };
170
+ };
171
+ // 眨眼:固定时刻表(间隔 2.4–3.6s 的「随机」写死),闭眼 dur 秒(24fps 下 2 帧 ≈ 0.083s;「无语」的慢眨眼 3 帧)
172
+ TOON.blinkAt = (t, times, dur = 0.083) => times.some(b => t >= b && t < b + dur) ? 1 : 0;
173
+ // 活着:说话时头随口型轻点(每次张嘴下沉 0.015R)+呼吸(1.6s 一周期,身体 ±1.2%)
174
+ TOON.alive = (t, R) => ({ bob: TOON.mouth(t) * R * 0.015, breathe: 1 + 0.012 * Math.sin(2 * Math.PI * t / 1.6) });
175
+
176
+ // ---------- 漫画符号 ----------
177
+ // 汗滴:p 0..1 沿脸侧滑下
178
+ TOON.sweat = (c, x, y, r, p) => {
179
+ const yy = y + p * r * 3.2, a = p < 0.85 ? 1 : 1 - (p - 0.85) / 0.15;
180
+ c.save(); c.globalAlpha = a; c.fillStyle = '#9ED8F5'; c.strokeStyle = LINE; c.lineWidth = Math.max(3, r * 0.12);
181
+ c.beginPath(); c.moveTo(x, yy - r * 1.5); c.quadraticCurveTo(x + r * 1.05, yy + r * 0.1, x, yy + r); c.quadraticCurveTo(x - r * 1.05, yy + r * 0.1, x, yy - r * 1.5); c.fill(); c.stroke();
182
+ c.fillStyle = '#fff'; c.beginPath(); c.ellipse(x - r * 0.3, yy - r * 0.05, r * 0.18, r * 0.32, 0, 0, Math.PI * 2); c.fill(); c.restore();
183
+ };
184
+ // 「?」「!」弹出(白描边,过冲 30%)
185
+ TOON.mark = (c, ch, x, y, p, size = 160, color = LINE) => { if (p <= 0) return; TY.pop(c, ch, x, y, p, { size, fam: 'PuHui-Black', color, stroke: '#fff', strokeW: size * 0.12, over: 3 }); };
186
+ })();
@@ -0,0 +1,136 @@
1
+ // 文字动效 TY:逐字/逐词弹入、遮罩升起、弹入、打字、从模糊浮出、荧光笔、下划线、主词砸入、关键词强调、数字计数与等宽数字。
2
+ // 约定:p 是 0..1 进度;lt/t 是秒。字体用 CSS 字体串或 family 名(中文默认 PuHui-*,开源版是思源黑体,见 lib/fonts.js)。
3
+ // 可读性底线(y5/y1 卡片):主词 : 辅句 : 标签 ≈ 3.4 : 1 : 0.4;一句辅句在屏 ≥1.2s;入场慢(0.3–0.4s)、退场快(≈0.18s)。
4
+ (() => {
5
+ const { clamp, lerp } = U;
6
+ const TY = window.TY = {};
7
+
8
+ // ---------- 基础 ----------
9
+ TY.font = (size, fam = 'PuHui-Black') => `${size}px "${fam}"`;
10
+ // 画一段字:可带透明、缩放、旋转(绕 ox,oy)、字距 track、描边
11
+ TY.text = (c, s, x, y, { size = 120, fam = 'PuHui-Black', color = '#111', align = 'center', base = 'alphabetic', alpha = 1, scale = 1, rot = 0, track = 0, stroke, strokeW = 0, ox = x, oy = y } = {}) => {
12
+ if (alpha <= 0 || scale <= 0) return;
13
+ c.save(); c.globalAlpha *= alpha; c.translate(ox, oy); c.rotate(rot); c.scale(scale, scale); c.translate(-ox, -oy);
14
+ c.font = TY.font(size, fam); c.textAlign = align; c.textBaseline = base; if (track) c.letterSpacing = track + 'px';
15
+ if (stroke && strokeW) { c.lineJoin = 'round'; c.strokeStyle = stroke; c.lineWidth = strokeW; c.strokeText(s, x, y); }
16
+ c.fillStyle = color; c.fillText(s, x, y); c.restore();
17
+ };
18
+ TY.width = (c, s, size, fam = 'PuHui-Black', track = 0) => { c.save(); c.font = TY.font(size, fam); if (track) c.letterSpacing = track + 'px'; const w = c.measureText(s).width; c.restore(); return w; };
19
+ // 折行:按 maxW 把一段字拆成多行(中文逐字可断;英文/数字按词断;行首不放中文标点)。font 是 CSS 字体串
20
+ TY.wrap = (c, text, maxW, font) => {
21
+ c.save(); if (font) c.font = font;
22
+ const toks = String(text).match(/[A-Za-z0-9.,%+\-'’]+\s*|\s+|./gu) || [], lines = []; let cur = '';
23
+ for (const tk of toks) {
24
+ if (tk === '\n') { lines.push(cur); cur = ''; continue; }
25
+ if (cur && c.measureText(cur + tk.trimEnd()).width > maxW && !/^[,。、:;!?」』)》,.!?)]$/.test(tk)) { lines.push(cur.trimEnd()); cur = tk.trimStart(); }
26
+ else cur += tk;
27
+ }
28
+ if (cur.trim()) lines.push(cur.trimEnd());
29
+ c.restore(); return lines;
30
+ };
31
+ // 把字号从 size 往下缩,直到真放得下:折行后不超过 maxLines 行,而且量过每行宽 ≤ maxW(单个长英文词/长数字折不开,只数行数会超宽)。返回 {size, lines, ok}
32
+ // 先在 [min, 1] 倍里找;找不到时 soft:true 返回 ok:false 让调用方换方案(如改折两行),否则继续缩(最低 0.2 倍)。balance:true 时多行折成等长。
33
+ TY.fit = (c, text, maxW, size, fam, { maxLines = 2, min = 0.45, weight = '', soft = false, balance = false } = {}) => {
34
+ const font = s => `${weight}${s}px "${fam}"`, ok = (s, lines) => { c.save(); c.font = font(s); const r = lines.length <= maxLines && lines.every(l => c.measureText(l).width <= maxW + 0.5); c.restore(); return r; };
35
+ let s = size, lines = [String(text)], good = false;
36
+ for (; s >= size * (soft ? min : 0.2) - 1e-9; s *= 0.94) { lines = TY.wrap(c, text, maxW, font(s)); if (ok(s, lines)) { good = true; break; } if (s < size * min && soft) break; }
37
+ if (!good && !soft) s /= 0.94;
38
+ if (good && balance && lines.length > 1) { const n = lines.length; let lo = maxW * 0.4, hi = maxW; for (let k = 0; k < 14; k++) { const m = (lo + hi) / 2; if (TY.wrap(c, text, m, font(s)).length > n) lo = m; else hi = m; } lines = TY.wrap(c, text, hi, font(s)); }
39
+ return { size: s, lines, ok: good };
40
+ };
41
+
42
+ // 保底版 fit:字号不低于 floor(px)。放得下就和 TY.fit 一样;放不下就停在 floor,按 maxLines 截断并加「…」,单行过宽的长词也截。
43
+ // 用在「内容长度不可控」的地方(节点标签、卡片说明、图表标签):宁可截断,也不缩到看不清。返回 {size, lines, ok, cut}
44
+ TY.fitFloor = (c, text, maxW, size, fam, { maxLines = 2, floor = size * 0.5, weight = '', balance = false } = {}) => {
45
+ floor = Math.min(floor, size);
46
+ const r = TY.fit(c, text, maxW, size, fam, { maxLines, min: Math.max(0.2, floor / size), weight, soft: true, balance });
47
+ if (r.ok) return { ...r, cut: false };
48
+ const font = `${weight}${floor}px "${fam}"`; c.save(); c.font = font;
49
+ const w = s => c.measureText(s).width, ell = line => { if (w(line) <= maxW) return line; let l = line; while (l.length > 1 && w(l + '…') > maxW) l = l.slice(0, -1); return l + '…'; };
50
+ let lines = TY.wrap(c, text, maxW, font), cut = false;
51
+ if (lines.length > maxLines) { lines = lines.slice(0, maxLines); lines[maxLines - 1] = lines[maxLines - 1] + '…'; cut = true; }
52
+ lines = lines.map(l => { const e = ell(l); if (e !== l) cut = true; return e; });
53
+ c.restore(); return { size: floor, lines, ok: false, cut };
54
+ };
55
+
56
+ // ---------- 入场 ----------
57
+ // 逐字错开出现:每字 dur 秒,从 dy 往上、透明→不透明、可带缩放;Kurzgesagt 标题 0.5s/字、错开 45ms。返回总宽。
58
+ TY.charsIn = (c, str, x, y, lt, { font, color, dur = 0.35, stagger = 0.04, dy = 24, scale0 = 1, align = 'left', ease = MO.cubicOut, shadow, spacing = 0 } = {}) => {
59
+ c.save(); c.font = font; c.textBaseline = 'alphabetic'; c.fillStyle = color; const A0 = c.globalAlpha; // 乘上调用方的透明度(外面包一层淡出才生效)
60
+ const chars = [...str]; const ws = chars.map(ch => c.measureText(ch).width + spacing); const tw = ws.reduce((a, b) => a + b, 0) - spacing;
61
+ let cx = align === 'center' ? x - tw / 2 : align === 'right' ? x - tw : x;
62
+ chars.forEach((ch, i) => {
63
+ const q = clamp((lt - i * stagger) / dur); if (q > 0) {
64
+ const e = ease(q); c.save(); c.globalAlpha = A0 * clamp(q * 2.2);
65
+ c.translate(cx + ws[i] / 2, y + dy * (1 - e)); const s = lerp(scale0, 1, e); c.scale(s, s);
66
+ if (shadow) { c.shadowColor = shadow.color; c.shadowOffsetX = shadow.x || 0; c.shadowOffsetY = shadow.y || 0; c.shadowBlur = shadow.blur || 0; }
67
+ c.fillText(ch, -ws[i] / 2 + spacing / 2, 0); c.restore();
68
+ }
69
+ cx += ws[i];
70
+ });
71
+ c.restore(); return tw;
72
+ };
73
+ // 遮罩升起:字从基线下的「槽」里推上来(动态文字最常见的出字法)。p 0..1,o.dir=+1 往上出,-1 往下收。o 同 TY.text
74
+ TY.rise = (c, s, x, y, p, o = {}) => {
75
+ const size = o.size || 120, e = MO.expoOut(clamp(p)), w = TY.width(c, s, size, o.fam, o.track), a = o.align || 'left';
76
+ const x0 = a === 'left' ? x : a === 'center' ? x - w / 2 : x - w;
77
+ c.save(); c.beginPath(); c.rect(x0 - 20, y - size * 1.05, w + 40, size * 1.32); c.clip();
78
+ TY.text(c, s, x, y + size * 1.2 * (1 - e) * (o.dir || 1), { ...o, align: a }); c.restore();
79
+ };
80
+ // 弹入:从 0 放大到 1,backOut 过冲后停(以字中心为轴)。o.over = backOut 的 s
81
+ TY.pop = (c, s, x, y, p, o = {}) => {
82
+ if (p <= 0) return; const size = o.size || 120, sc = MO.backOut(clamp(p), o.over ?? 2.2) * (o.scale || 1);
83
+ TY.text(c, s, x, y, { ...o, scale: sc, ox: x, oy: y - size * 0.36 });
84
+ };
85
+ // 从模糊浮出(发布会标题):模糊→清晰+上移+透明→不透明。用调用方设好的 font/fillStyle/textAlign。
86
+ TY.blurIn = (c, text, x, y, p, { blur = 18, dy = 28, scale = 0 } = {}) => {
87
+ if (p <= 0) return;
88
+ c.save(); c.globalAlpha *= clamp(p * 1.4);
89
+ const b = (1 - p) * blur; if (b > 0.3) c.filter = `blur(${b.toFixed(2)}px)`;
90
+ c.translate(x, y + (1 - p) * dy); if (scale) { const s = 1 + (1 - p) * scale; c.scale(s, s); }
91
+ c.fillText(text, 0, 0); c.restore();
92
+ };
93
+ // 打字:按字符数截断(p 0..1)
94
+ TY.typed = (s, p) => [...s].slice(0, Math.round(clamp(p) * [...s].length)).join('');
95
+ // 打字机:t 时刻已打出的字数(rate 字/秒,可按 fps 步进;Vox 13 字/秒+12fps,白板正文 16 字/秒)
96
+ TY.typedCount = (t, t0, rate, fps = 0) => Math.max(0, Math.floor(((fps ? MO.step(t, fps) : t) - t0) * rate + 1e-6) + 1);
97
+
98
+ // ---------- 强调 ----------
99
+ // 荧光笔:从左往右刷出的斜切色块,放在字后面(动态文字)。拼贴风的毛边 multiply 荧光笔见 CL.highlight
100
+ TY.marker = (c, x, y, w, h, p, color = '#FFE14D', skew = 0.18) => {
101
+ if (p <= 0) return; const e = MO.expoOut(clamp(p)), ww = w * e;
102
+ c.save(); c.fillStyle = color; c.beginPath(); c.moveTo(x + h * skew, y); c.lineTo(x + ww + h * skew, y); c.lineTo(x + ww, y + h); c.lineTo(x, y + h); c.closePath(); c.fill(); c.restore();
103
+ };
104
+ TY.underline = (c, x, y, w, p, color, lw = 10) => { if (p <= 0) return; c.save(); c.strokeStyle = color; c.lineCap = 'round'; c.lineWidth = lw; c.beginPath(); c.moveTo(x, y); c.lineTo(x + w * MO.expoOut(clamp(p)), y); c.stroke(); c.restore(); };
105
+ // 主词「砸」进来:从 from 倍缩到 1(弹簧 2.2Hz/10,过冲 ≈8%),前 0.06s 透明→不透明。lt = 本词出现后的秒数
106
+ TY.slam = (c, s, x, y, lt, o = {}) => {
107
+ if (lt < 0) return; const size = o.size || 340, k = MO.springHz(lt, o.freq || 2.2, o.decay || 10), sc = lerp(o.from || 1.7, 1, k);
108
+ TY.text(c, s, x, y, { size, fam: 'PuHui-Black', align: 'left', ...o, scale: sc * (o.scale || 1), alpha: clamp(lt / 0.06), ox: o.ox ?? x, oy: o.oy ?? y - size * 0.36 });
109
+ };
110
+ // 关键词:tIn 起从槽里升起,到「强调拍」tHit 放大到 1.35 倍并钉住(弹簧 2.5Hz/9)。返回当前缩放(给荧光笔/下划线跟着放大)。
111
+ // 字和动作说同一件事:「下一行」放大的那一拍正好按下 Tab(y5)。
112
+ TY.keyword = (c, s, x, y, t, tIn, tHit, o) => {
113
+ if (t < tHit) { TY.rise(c, s, x, y, MO.at(t, tIn, 0.35), { ...o, align: 'left' }); return 1; }
114
+ const sc = lerp(1, 1.35, MO.springHz(t - tHit, 2.5, 9));
115
+ TY.text(c, s, x, y, { ...o, align: 'left', scale: sc, ox: x, oy: y });
116
+ return sc;
117
+ };
118
+ // 关键词后面的荧光笔:跟关键词同一拍出现、同一缩放
119
+ TY.keywordMarker = (c, x, y, w, size, t, tHit, sc, col) => { if (t < tHit) return; TY.marker(c, x - 8, y - size * 0.98 * sc, (w + 16) * sc, size * 1.18 * sc, MO.at(t, tHit, 0.3), col); };
120
+
121
+ // ---------- 数字 ----------
122
+ // 千分位格式;负号用真减号「−」
123
+ TY.fmt = (v, dec = 0, sep = true) => { const s = Math.abs(v).toFixed(dec); const [i, f] = s.split('.'); const ii = sep ? i.replace(/\B(?=(\d{3})+(?!\d))/g, ',') : i; return (v < 0 ? '−' : '') + ii + (f ? '.' + f : ''); };
124
+ // 计数:[start, start+dur] 内从 a 滚到 b;默认 expoOut(先快后慢,最后一位落定)。数字和动画只改「怎么出现」,不改口径。
125
+ TY.count = (lt, start, dur, a, b, ease = MO.expoOut) => lerp(a, b, ease(clamp((lt - start) / dur)));
126
+ // 等宽数字排布:canvas 开不了 tnum,滚动时数字宽度会抖,必须每个数字占同宽格子自己排。用调用方设好的 font/fillStyle。返回总宽
127
+ TY.tabular = (c, str, x, y, { align = 'left', digitW } = {}) => {
128
+ const dw = digitW || c.measureText('0').width;
129
+ const ws = [...str].map(ch => /[0-9]/.test(ch) ? dw : c.measureText(ch).width);
130
+ const total = ws.reduce((s, w) => s + w, 0);
131
+ let cx = align === 'right' ? x - total : align === 'center' ? x - total / 2 : x;
132
+ const ta = c.textAlign; c.textAlign = 'center';
133
+ [...str].forEach((ch, i) => { c.fillText(ch, cx + ws[i] / 2, y); cx += ws[i]; });
134
+ c.textAlign = ta; return total;
135
+ };
136
+ })();
@@ -0,0 +1,141 @@
1
+ // 发布会式界面 UI:渐变光斑底(mesh gradient 近似)、毛玻璃卡片、伪 3D 透视(切片)、光扫过(sheen)、只画影子的投影、线性图标。
2
+ // 约定:坐标是 1920×1080 屏幕坐标;所有动画参数由调用方用时间算好传进来,本库不读时钟。
3
+ // 卡片展开成全屏、模糊推进是转场(transitions.js 的 expandRect / blurPush)。
4
+ // 坑:blur 会从画布外拉进透明像素——画进不清空的离屏会逐帧累积(qa 确定性 ✗),所以先铺原图再叠模糊版;
5
+ // 切片透视的条带取整像素首尾相接,别用「重叠 0.75px 防缝」,半透明下重叠处出竖纹。
6
+ (() => {
7
+ let W = 1920, H = 1080; U.onStage((w, h) => { W = w; H = h; }); // 画布尺寸跟 U.setStage 走(默认 1920×1080)
8
+ const { clamp, lerp } = U;
9
+ const UI = window.UI = {};
10
+ const cvs = {};
11
+ const scratch = (k, w, h) => { let c = cvs[k]; if (!c || c.width !== w || c.height !== h) { c = cvs[k] = document.createElement('canvas'); c.width = w; c.height = h; } return c; };
12
+ UI.scratch = scratch;
13
+
14
+ UI.rr = (c, x, y, w, h, r) => { c.beginPath(); c.roundRect(x, y, w, h, r); };
15
+ UI.rrPath = (x, y, w, h, r) => { const p = new Path2D(); p.roundRect(x, y, w, h, r); return p; };
16
+
17
+ // ---------- 渐变光斑底(mesh gradient 的廉价近似) ----------
18
+ // 小画布上画几团径向渐变(screen 叠加)→ 小画布上模糊 → 放大铺满 → 叠静态颗粒防色带。
19
+ // blobs: [{x,y,r,col:[r,g,b],a, ax,ay,period,ph}] 位置单位是屏幕像素,ax/ay 漂移振幅,period 秒
20
+ UI.mesh = (c, t, { base = '#05060a', blobs = [], blur = 40, scale = 0.25, grain = 0.04, key = 'mesh' } = {}) => {
21
+ const sw = Math.round(W * scale), sh = Math.round(H * scale);
22
+ const a = scratch(key + 'a', sw, sh), g = a.getContext('2d');
23
+ g.globalCompositeOperation = 'source-over'; g.fillStyle = base; g.fillRect(0, 0, sw, sh);
24
+ g.globalCompositeOperation = 'screen';
25
+ for (const b of blobs) {
26
+ const w = 2 * Math.PI / (b.period || 10), ph = b.ph || 0;
27
+ const x = (b.x + (b.ax || 0) * Math.sin(w * t + ph)) * scale, y = (b.y + (b.ay || 0) * Math.cos(w * t * 0.83 + ph)) * scale, r = b.r * scale * (1 + 0.06 * Math.sin(w * t * 1.3 + ph));
28
+ const gr = g.createRadialGradient(x, y, 0, x, y, r);
29
+ const [R, G, B] = b.col, A = b.a == null ? 1 : b.a;
30
+ gr.addColorStop(0, `rgba(${R},${G},${B},${A})`); gr.addColorStop(0.45, `rgba(${R},${G},${B},${A * 0.45})`); gr.addColorStop(1, `rgba(${R},${G},${B},0)`);
31
+ g.fillStyle = gr; g.fillRect(0, 0, sw, sh);
32
+ }
33
+ const bb = scratch(key + 'b', sw, sh), gb = bb.getContext('2d');
34
+ // 先铺不模糊的原图再叠模糊版:blur 会从画布外拉进透明像素,边缘半透明——画进不清空的离屏会逐帧累积(qa 确定性 ✗ 就是这么来的)
35
+ gb.clearRect(0, 0, sw, sh); gb.drawImage(a, 0, 0); gb.filter = `blur(${blur * scale}px)`; gb.drawImage(a, 0, 0); gb.filter = 'none';
36
+ c.save(); c.imageSmoothingQuality = 'high'; c.drawImage(bb, 0, 0, W, H);
37
+ if (grain) { c.globalAlpha = grain; c.drawImage(UI.grainTex(), 0, 0); }
38
+ c.restore();
39
+ };
40
+ // 静态颗粒(种子固定):对抗 8bit 视频编码里大面积渐变的色带
41
+ UI.grainTex = () => PAINT.cached('gl_grain' + W + 'x' + H, W, H, (g) => {
42
+ const r = U.rng(77), im = g.createImageData(W, H), d = im.data;
43
+ for (let i = 0; i < d.length; i += 4) { const v = r() * 255; d[i] = d[i + 1] = d[i + 2] = v; d[i + 3] = 255; }
44
+ g.putImageData(im, 0, 0);
45
+ });
46
+
47
+ // ---------- 背景的模糊副本(毛玻璃取样用) ----------
48
+ UI.backdrop = (src, { scale = 0.25, blur = 28, key = 'bd' } = {}) => {
49
+ const sw = Math.round(W * scale), sh = Math.round(H * scale);
50
+ const a = scratch(key, sw, sh), g = a.getContext('2d');
51
+ g.clearRect(0, 0, sw, sh); g.drawImage(src, 0, 0, sw, sh); g.filter = `blur(${blur * scale}px)`; g.drawImage(src, 0, 0, sw, sh); g.filter = 'none'; // 同上:先垫原图防边缘透明
52
+ return a;
53
+ };
54
+
55
+ // ---------- 阴影(只画影子,不画形状本身:形状移到屏外,影子偏移回来) ----------
56
+ UI.shadow = (c, x, y, w, h, r, { blur = 60, oy = 28, alpha = 0.45 } = {}) => {
57
+ c.save(); c.shadowColor = `rgba(0,0,0,${alpha})`; c.shadowBlur = blur; c.shadowOffsetX = 4000; c.shadowOffsetY = oy;
58
+ c.fillStyle = '#000'; UI.rr(c, x - 4000, y, w, h, r); c.fill(); c.restore();
59
+ };
60
+
61
+ // ---------- 毛玻璃卡片 ----------
62
+ // bd = UI.backdrop(...) 的结果;sample = 取样的屏幕矩形(默认与卡片同位,卡片画进离屏纹理时要传屏幕位置)
63
+ UI.glass = (c, { x, y, w, h, r = 28, bd, sample, tint = 0.07, stroke = 0.22, light = 0.16, shadow = true }) => {
64
+ if (shadow) UI.shadow(c, x, y, w, h, r, { blur: 70, oy: 30, alpha: 0.5 });
65
+ c.save(); UI.rr(c, x, y, w, h, r); c.clip();
66
+ if (bd) { const s = bd.width / W, S = sample || { x, y, w, h }; c.drawImage(bd, S.x * s, S.y * s, S.w * s, S.h * s, x, y, w, h); }
67
+ c.fillStyle = `rgba(255,255,255,${tint})`; c.fillRect(x, y, w, h);
68
+ // 顶部受光:上缘一道柔和亮带(玻璃的厚度感)
69
+ const g = c.createLinearGradient(0, y, 0, y + h * 0.5); g.addColorStop(0, `rgba(255,255,255,${light})`); g.addColorStop(1, 'rgba(255,255,255,0)');
70
+ c.fillStyle = g; c.fillRect(x, y, w, h * 0.5);
71
+ c.restore();
72
+ // 1px 描边:左上亮、右下暗(光从左上来)
73
+ c.save(); const sg = c.createLinearGradient(x, y, x + w, y + h);
74
+ sg.addColorStop(0, `rgba(255,255,255,${stroke * 1.8})`); sg.addColorStop(0.5, `rgba(255,255,255,${stroke * 0.5})`); sg.addColorStop(1, `rgba(255,255,255,${stroke})`);
75
+ c.strokeStyle = sg; c.lineWidth = 1.5; UI.rr(c, x + 0.75, y + 0.75, w - 1.5, h - 1.5, r); c.stroke(); c.restore();
76
+ };
77
+
78
+ // ---------- 光扫过(sheen) ----------
79
+ // 在 clip 区域里让一道斜向亮带从左扫到右;p∈[0,1];band 宽 width;angle 弧度
80
+ // comp:默认 'lighter'(叠在画面上);在只含文字的离屏上用 'source-atop'(光只落在字上)
81
+ UI.sheen = (c, clipPath, p, { x0 = 0, x1 = W, width = 260, angle = -0.35, alpha = 0.32, col = '255,255,255', comp = 'lighter' } = {}) => {
82
+ if (p <= 0 || p >= 1) return;
83
+ const cx = lerp(x0 - width * 2, x1 + width * 2, p);
84
+ c.save(); if (clipPath) c.clip(clipPath);
85
+ c.globalCompositeOperation = comp;
86
+ c.translate(cx, H / 2); c.rotate(angle);
87
+ const g = c.createLinearGradient(-width, 0, width, 0);
88
+ g.addColorStop(0, `rgba(${col},0)`); g.addColorStop(0.5, `rgba(${col},${alpha})`); g.addColorStop(1, `rgba(${col},0)`);
89
+ c.fillStyle = g; c.fillRect(-width, -H * 1.5, width * 2, H * 3);
90
+ c.restore();
91
+ };
92
+
93
+ // ---------- 伪 3D:把一张纹理绕 Y 轴(竖条切片)或 X 轴(横条切片)转 ----------
94
+ // tex: 卡片纹理(任意分辨率),屏幕中心 cx,cy,屏幕尺寸 w×h(未旋转时),ry/rx 弧度,persp 透视距离(px)
95
+ // 远侧变暗 shade(0..1),模拟侧光。条宽 strip px。
96
+ // 切片边界取整像素、首尾相接(不重叠也不留缝):重叠 0.75px 防缝的老办法,在半透明纹理/半透明绘制下重叠处透明度叠加,出一条条竖纹横纹。
97
+ // 另外先画进离屏再整体按 alpha 贴回,整卡淡入淡出时也不出纹。
98
+ UI.persp = (c0, tex, { cx, cy, w, h, ry = 0, rx = 0, persp = 1600, strip = 3, shade = 0.35, alpha = 1 }) => {
99
+ const sc = scratch('persp', W, H), c = sc.getContext('2d'); c.reset();
100
+ c.save();
101
+ const tw = tex.width, th = tex.height;
102
+ if (Math.abs(ry) >= Math.abs(rx)) {
103
+ const n = Math.max(8, Math.ceil(w / strip)), cs = Math.cos(ry), sn = Math.sin(ry);
104
+ const P = u => { const X = (u - 0.5) * w, z = X * sn, s = persp / (persp + z); return [cx + X * cs * s, s]; };
105
+ for (let i = 0; i < n; i++) {
106
+ const u0 = i / n, u1 = (i + 1) / n, [x0, s0] = P(u0), [x1, s1] = P(u1), s = (s0 + s1) / 2;
107
+ const hh = h * s, xa = Math.round(Math.min(x0, x1)), xb = Math.round(Math.max(x0, x1)); if (xb > xa) c.drawImage(tex, u0 * tw, 0, (u1 - u0) * tw, th, xa, cy - hh / 2, xb - xa, hh);
108
+ }
109
+ if (shade && ry) { // 转离观众的一侧暗一点
110
+ const [xa] = P(0), [xb] = P(1), g = c.createLinearGradient(xa, 0, xb, 0), k = Math.min(1, Math.abs(Math.sin(ry)) * 2) * shade;
111
+ g.addColorStop(0, `rgba(0,0,0,${ry > 0 ? 0 : k})`); g.addColorStop(1, `rgba(0,0,0,${ry > 0 ? k : 0})`);
112
+ c.globalCompositeOperation = 'source-atop'; c.fillStyle = g; c.fillRect(Math.min(xa, xb), cy - h, Math.abs(xb - xa), h * 2);
113
+ }
114
+ } else {
115
+ const n = Math.max(8, Math.ceil(h / strip)), cs = Math.cos(rx), sn = Math.sin(rx);
116
+ const P = v => { const Y = (v - 0.5) * h, z = -Y * sn, s = persp / (persp + z); return [cy + Y * cs * s, s]; };
117
+ for (let i = 0; i < n; i++) {
118
+ const v0 = i / n, v1 = (i + 1) / n, [y0, s0] = P(v0), [y1, s1] = P(v1), s = (s0 + s1) / 2;
119
+ const ww = w * s, ya = Math.round(Math.min(y0, y1)), yb = Math.round(Math.max(y0, y1)); if (yb > ya) c.drawImage(tex, 0, v0 * th, tw, (v1 - v0) * th, cx - ww / 2, ya, ww, yb - ya);
120
+ }
121
+ }
122
+ c.restore();
123
+ c0.save(); c0.globalAlpha *= alpha; c0.drawImage(sc, 0, 0); c0.restore();
124
+ };
125
+
126
+ // ---------- 线性图标(SF Symbols 气质:圆头、等线宽) ----------
127
+ UI.icon = (c, kind, x, y, s, col, t = 0, lw = 0.075) => {
128
+ c.save(); c.translate(x, y); c.scale(s, s); c.strokeStyle = col; c.fillStyle = col; c.lineWidth = lw; c.lineCap = 'round'; c.lineJoin = 'round';
129
+ if (kind === 'wave') { // 语音:5 根会跳的竖条
130
+ const hs = [0.35, 0.7, 1, 0.6, 0.3];
131
+ hs.forEach((h0, i) => { const h = h0 * (0.75 + 0.25 * Math.sin(t * 7 + i * 1.3)); const xx = -0.4 + i * 0.2; c.beginPath(); c.moveTo(xx, -h * 0.45); c.lineTo(xx, h * 0.45); c.stroke(); });
132
+ } else if (kind === 'memory') { // 记忆:三层叠片
133
+ for (let k = 0; k < 3; k++) { const yy = -0.28 + k * 0.24 + 0.02 * Math.sin(t * 2 + k); c.beginPath(); c.moveTo(-0.45, yy); c.lineTo(0, yy - 0.18); c.lineTo(0.45, yy); c.lineTo(0, yy + 0.18); c.closePath(); c.globalAlpha = k === 0 ? 1 : 0.6; c.stroke(); }
134
+ } else if (kind === 'agent') { // 智能体:四角星 + 小星
135
+ const r = 0.42 * (1 + 0.05 * Math.sin(t * 3));
136
+ c.beginPath(); c.moveTo(0, -r); c.quadraticCurveTo(0, 0, r, 0); c.quadraticCurveTo(0, 0, 0, r); c.quadraticCurveTo(0, 0, -r, 0); c.quadraticCurveTo(0, 0, 0, -r); c.fill();
137
+ const r2 = 0.16; c.translate(0.36, -0.36); c.beginPath(); c.moveTo(0, -r2); c.quadraticCurveTo(0, 0, r2, 0); c.quadraticCurveTo(0, 0, 0, r2); c.quadraticCurveTo(0, 0, -r2, 0); c.quadraticCurveTo(0, 0, 0, -r2); c.globalAlpha = 0.7; c.fill();
138
+ }
139
+ c.restore();
140
+ };
141
+ })();
@@ -0,0 +1,92 @@
1
+ // 通用工具(最先加载,转场库与引擎共用)
2
+ (() => {
3
+ // ---------- 工具 ----------
4
+ window.U = {
5
+ TAU: Math.PI * 2,
6
+ clamp: (x, a = 0, b = 1) => Math.max(a, Math.min(b, x)),
7
+ lerp: (a, b, p) => a + (b - a) * p,
8
+ // smoothstep:x 在 [a,b] 内平滑地从 0 走到 1(开场写出、渐显、段内节奏都用它)
9
+ ss: (a, b, x) => { const t = Math.max(0, Math.min(1, (x - a) / (b - a))); return t * t * (3 - 2 * t); },
10
+ // 把 p 的 [a,b] 段线性拉成 0..1(转场里分阶段用)
11
+ seg: (p, a, b) => Math.max(0, Math.min(1, (p - a) / (b - a))),
12
+ // 步进帧率:把连续时间量化成 fps 步——「一顿一顿」是很多风格的味道本身:
13
+ // 橡皮管卡通 12fps(一拍二)、皮影杆操 10fps、哈林跳舞小人 8fps 硬切、手绘线条沸腾 8–12fps、胶片层 24fps。
14
+ stepTime: (t, fps) => Math.floor(t * fps) / fps,
15
+ ease: {
16
+ linear: p => p,
17
+ inOut: p => p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2,
18
+ out: p => 1 - Math.pow(1 - p, 3),
19
+ in: p => p * p * p,
20
+ outBack: p => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(p - 1, 3) + c1 * Math.pow(p - 1, 2); },
21
+ },
22
+ // 种子随机:mulberry32
23
+ rng(seed) { let a = seed >>> 0; return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; },
24
+ hash(i, j = 0) { let h = (i * 374761393 + j * 668265263) | 0; h = (h ^ (h >>> 13)) * 1274126177; return ((h ^ (h >>> 16)) >>> 0) / 4294967296; },
25
+ // 点列 → Path2D(折线)
26
+ poly(pts, closed = true) { const p = new Path2D(); pts.forEach((q, i) => i ? p.lineTo(q[0], q[1]) : p.moveTo(q[0], q[1])); if (closed) p.closePath(); return p; },
27
+ };
28
+
29
+ // ---------- 画布尺寸 ----------
30
+ // 默认 1920×1080。竖屏片段(clip.html,spec.width/height)和竖屏整片(index.html?w=1080&h=1920,或 eras.js 里写 window.FILM_SIZE = [1080, 1920])
31
+ // 启动时调一次 U.setStage(w, h)。用到屏幕尺寸的库(PAINT / BRUSH / RENDER / POST / KIT / TOON / CAM / UI / CH / DG / CL / MD、transitions.js、engine.js)
32
+ // 用 U.onStage 登记,尺寸一改全部跟着改;登记时立刻按当前尺寸回调一次,后加载的库也拿得到。
33
+ // 35 种艺术风格场景(scenes/)是按 1920×1080 画的,竖屏只保证解说语法、转场和引擎本身。
34
+ const stageHooks = [];
35
+ window.STAGE = { W: 1920, H: 1080 };
36
+ U.onStage = f => { stageHooks.push(f); f(window.STAGE.W, window.STAGE.H); };
37
+ U.setStage = (w, h) => { w = Math.round(+w); h = Math.round(+h); if (!(w > 0 && h > 0)) throw new Error(`U.setStage:尺寸不对 ${w}×${h}`); window.STAGE = { W: w, H: h }; stageHooks.forEach(f => f(w, h)); };
38
+
39
+ // ---------- 字形检查 ----------
40
+ // 字体文件缺某个字时浏览器会静默回退到系统字体:measureText 不报错、宽度也对,换台机器就变样(迁移测试 B、D:全角字母、▶、「戏」「蓝」都踩过)。
41
+ // 做法:启动时(engine boot,字体加载完)把 FONT_FACES 里每个字体文件的 cmap 表读出来(WOFF1 用 DecompressionStream 解 zlib),查表判断有没有这个字。
42
+ // 不用 measureText 对比回退字体:macOS 上 Chromium 的逐字回退以首选字体为基准挑系统字体,两种写法会落到不同字体上,缺字也测不出来(实测误判)。
43
+ // font 可以是 family 名('LXGWWenKai-500')或 CSS 字体串('80px "LXGWWenKai-500"')。family 不在 FONT_FACES 里 → 当作全缺。
44
+ const famOf = font => { const m = /"([^"]+)"/.exec(font) || /'([^']+)'/.exec(font); return m ? m[1] : String(font).replace(/^(italic\s+|bold\s+|\d+\s+)*[\d.]+px\s+/, '').trim(); };
45
+ const CMAPS = {};
46
+ const inflate = async (buf) => new Uint8Array(await new Response(new Blob([buf]).stream().pipeThrough(new DecompressionStream('deflate'))).arrayBuffer());
47
+ async function readCmap(url) {
48
+ const ab = await (await fetch(url)).arrayBuffer(), dv = new DataView(ab), sig = dv.getUint32(0);
49
+ let tab = null;
50
+ if (sig === 0x774F4646) { // 'wOFF'
51
+ const n = dv.getUint16(12);
52
+ for (let i = 0; i < n; i++) { const o = 44 + i * 20; if (dv.getUint32(o) !== 0x636D6170) continue; // 'cmap'
53
+ const off = dv.getUint32(o + 4), comp = dv.getUint32(o + 8), orig = dv.getUint32(o + 12), raw = new Uint8Array(ab, off, comp);
54
+ tab = comp < orig ? await inflate(raw) : raw.slice(); }
55
+ } else { // 裸 TTF/OTF
56
+ const n = dv.getUint16(4);
57
+ for (let i = 0; i < n; i++) { const o = 12 + i * 16; if (dv.getUint32(o) === 0x636D6170) tab = new Uint8Array(ab, dv.getUint32(o + 8), dv.getUint32(o + 12)).slice(); }
58
+ }
59
+ if (!tab) throw new Error('没有 cmap 表');
60
+ const t = new DataView(tab.buffer, tab.byteOffset, tab.byteLength), set = new Set(), nt = t.getUint16(2);
61
+ for (let i = 0; i < nt; i++) {
62
+ const so = t.getUint32(4 + i * 8 + 4), fmt = t.getUint16(so);
63
+ if (fmt === 4) { const segX2 = t.getUint16(so + 6), ends = so + 14, starts = ends + segX2 + 2, deltas = starts + segX2, ros = deltas + segX2;
64
+ for (let k = 0; k < segX2 / 2; k++) { const e = t.getUint16(ends + k * 2), s = t.getUint16(starts + k * 2), d = t.getInt16(deltas + k * 2), ro = t.getUint16(ros + k * 2);
65
+ for (let ch = s; ch <= e && ch !== 0xFFFF; ch++) { let g; if (!ro) g = (ch + d) & 0xFFFF; else { g = t.getUint16(ros + k * 2 + ro + (ch - s) * 2); if (g) g = (g + d) & 0xFFFF; } if (g) set.add(ch); } } }
66
+ else if (fmt === 12 || fmt === 13) { const ng = t.getUint32(so + 12); for (let k = 0; k < ng; k++) { const o = so + 16 + k * 12, s = t.getUint32(o), e = t.getUint32(o + 4), g = t.getUint32(o + 8);
67
+ for (let ch = s; ch <= e; ch++) if (fmt === 13 ? g : g + ch - s) set.add(ch); } }
68
+ else if (fmt === 0) { for (let ch = 0; ch < 256; ch++) if (t.getUint8(so + 6 + ch)) set.add(ch); }
69
+ else if (fmt === 6) { const first = t.getUint16(so + 6), cnt = t.getUint16(so + 8); for (let k = 0; k < cnt; k++) if (t.getUint16(so + 10 + k * 2)) set.add(first + k); }
70
+ }
71
+ return set;
72
+ }
73
+ // 引擎 boot 里调用一次(字体加载之后)。读失败的字体记 console.error,不阻断启动。
74
+ U.loadCmaps = async (faces = window.FONT_FACES || []) => {
75
+ await Promise.all(faces.map(async f => { try { CMAPS[f.family] = await readCmap(f.url); } catch (e) { console.error(`读不了字体 ${f.family} 的 cmap:${e}`); } }));
76
+ };
77
+ U.missingGlyphs = (font, text) => {
78
+ const fam = famOf(font), cm = CMAPS[fam];
79
+ const chars = [...new Set([...String(text)])].filter(ch => !/\s/.test(ch));
80
+ if (!cm) return chars;
81
+ return chars.filter(ch => !cm.has(ch.codePointAt(0)));
82
+ };
83
+ // 缺字就 console.error(qa.py / render.py 会把页面 console.error 当失败)。返回 true = 全都有。
84
+ U.assertGlyphs = (font, text, where = '') => {
85
+ const fam = famOf(font), miss = U.missingGlyphs(font, text);
86
+ if (miss.length) console.error(!CMAPS[fam] ? `字体「${fam}」没注册或没读到 cmap(lib/fonts.js 的 FONT_FACES)${where ? '(' + where + ')' : ''}`
87
+ : `缺字形:字体「${fam}」里没有「${miss.join('')}」${where ? '(' + where + ')' : ''}——会静默回退系统字体。用 scripts/font_subset.py 补进子集,或改用路径自画。`);
88
+ return miss.length === 0;
89
+ };
90
+ U.fontFamily = famOf;
91
+
92
+ })();