@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,929 @@
1
+ // 转场库:每个函数 (c, A, B, p, o) —— A=旧画面 canvas,B=新画面 canvas,p∈[0,1],
2
+ // o: {W,H,lt,t,tmp,IMG, ...该转场在 eras.js 里的参数}。确定性:随机数一律 U.rng(种子)。
3
+ // 每个转场都取「新时代」最有辨识度的形式元素(见 经验日志「转场」)。
4
+ (() => {
5
+ let W = 1920, H = 1080; U.onStage((w, h) => { W = w; H = h; }); // 画布尺寸跟 U.setStage 走:竖屏整片(render.py --width 1080 --height 1920)的转场按当前画布算
6
+ const sx = x => x * W / 1920, sy = y => y * H / 1080; // 按 1920×1080 写的默认位置换算到当前画布(横屏时原样)
7
+ const { clamp, lerp, ease, rng } = U, TAU = Math.PI * 2;
8
+ const seg = (p, a, b) => clamp((p - a) / (b - a)); // 把 p 的 [a,b] 段拉成 0..1
9
+
10
+ // 缓存:同一转场里不随时间变化的预计算(瓦片表、离屏画布…)。
11
+ // 只缓存「和画面内容无关」的东西(几何表、scratch 画布)。依赖 A/B 画面内容的东西不要缓存:
12
+ // 缓存的是第一次被调用那一帧,同名转场在片中用第二次会画出第一次的内容,乱序渲染时还依赖调用顺序(迁移测试 B ②-10)。
13
+ // 确实要按段缓存时传 o:once(k, f, o) 的键带上进入段 id。
14
+ const cache = {};
15
+ const once = (k, f, o) => { const kk = o ? `${o.id}|${k}` : k; return cache[kk] || (cache[kk] = f()); };
16
+
17
+ const mkc = (w = W, h = H) => { const c = document.createElement('canvas'); c.width = w; c.height = h; return c; };
18
+
19
+ const T = window.TRANSITIONS = {
20
+ crossfade(c, A, B, p) { c.drawImage(A, 0, 0); c.globalAlpha = p; c.drawImage(B, 0, 0); c.globalAlpha = 1; },
21
+ cut(c, A, B, p) { c.drawImage(p < 0.5 ? A : B, 0, 0); },
22
+
23
+ // ① 洞穴→埃及:新画面拆成大小不一的石块,从左向右(带随机)拼进来,落位时从略大略偏处「砌」进去
24
+ tiles(c, A, B, p, o) {
25
+ const tiles = once('tiles', () => {
26
+ const r = rng(7), out = [];
27
+ // 三种块尺寸混排:先铺大块网格,再随机把一部分拆成小块
28
+ const bw = 240, bh = 180;
29
+ for (let y = 0; y < H; y += bh) for (let x = 0; x < W; x += bw) {
30
+ if (r() < 0.45) { for (let yy = 0; yy < 2; yy++) for (let xx = 0; xx < 2; xx++) out.push({ x: x + xx * bw / 2, y: y + yy * bh / 2, w: bw / 2, h: bh / 2 }); }
31
+ else out.push({ x, y, w: bw, h: bh });
32
+ }
33
+ out.forEach(t => { t.t = clamp((t.x + t.w / 2) / W * 0.62 + r() * 0.3); t.dx = (r() - 0.5) * 60; t.dy = -30 - r() * 50; t.rot = (r() - 0.5) * 0.12; });
34
+ return out;
35
+ });
36
+ c.drawImage(A, 0, 0);
37
+ for (const t of tiles) {
38
+ const q = clamp((p - t.t * 0.7) / 0.28); if (q <= 0) continue;
39
+ const e = ease.outBack(q);
40
+ c.save();
41
+ c.translate(t.x + t.w / 2 + t.dx * (1 - e), t.y + t.h / 2 + t.dy * (1 - e));
42
+ c.rotate(t.rot * (1 - e));
43
+ const s = 1.08 - 0.08 * e; c.scale(s, s);
44
+ if (q < 1) { c.shadowColor = 'rgba(30,15,5,.55)'; c.shadowBlur = 18 * (1 - q); c.shadowOffsetY = 10 * (1 - q); }
45
+ c.drawImage(B, t.x, t.y, t.w, t.h, -t.w / 2, -t.h / 2, t.w, t.h);
46
+ c.restore();
47
+ }
48
+ },
49
+
50
+ // ② 埃及→希腊:立方体绕竖轴转面(旧面向左转走,新面从右转入),面上有明暗
51
+ cube(c, A, B, p) {
52
+ const e = ease.inOut(p), th = e * Math.PI / 2;
53
+ c.fillStyle = '#0d0a08'; c.fillRect(0, 0, W, H);
54
+ const face = (img, ang, side) => {
55
+ // 正交近似透视:面宽 = W*cos(ang),远端边缩短 k
56
+ const w = W * Math.cos(ang); if (w < 2) return;
57
+ const N = 48, k = 0.18 * Math.sin(ang);
58
+ for (let i = 0; i < N; i++) {
59
+ const u0 = i / N, u1 = (i + 1) / N;
60
+ const far = side === 'left' ? 1 - u0 : u0; // 远端在转出去的那一侧
61
+ const sh = 1 - k * far, hh = H * sh;
62
+ const x = side === 'left' ? u0 * w : W - w + u0 * w;
63
+ c.drawImage(img, u0 * W, 0, W / N + 1, H, x, (H - hh) / 2, w / N + 1, hh);
64
+ }
65
+ c.fillStyle = `rgba(0,0,0,${0.55 * Math.sin(ang)})`;
66
+ c.fillRect(side === 'left' ? 0 : W - w, 0, w, H);
67
+ };
68
+ face(A, th, 'left');
69
+ face(B, Math.PI / 2 - th, 'right');
70
+ },
71
+
72
+ // ③ 希腊→罗马:马赛克小砖翻面——从猫的位置向外扩散,翻面瞬间露出深色砖背
73
+ mosaic(c, A, B, p, o) {
74
+ const tw = 18, th = 34, ox = W * 0.27, oy = H * 0.72;
75
+ const grid = once('mosaic', () => {
76
+ const r = rng(11), g = [], maxd = Math.hypot(W, H) * 0.8;
77
+ for (let y = 0; y < H; y += th) for (let x = 0; x < W; x += tw) {
78
+ const d = Math.hypot(x - ox, (y - oy) * 1.3) / maxd;
79
+ g.push({ x, y, t: clamp(d * 0.75 + r() * 0.25) });
80
+ }
81
+ return g;
82
+ });
83
+ c.drawImage(A, 0, 0);
84
+ for (const g of grid) {
85
+ const q = (p - g.t * 0.78) / 0.16;
86
+ if (q <= 0) continue;
87
+ if (q >= 1) { c.drawImage(B, g.x, g.y, tw, th, g.x, g.y, tw, th); continue; }
88
+ // 翻面:宽度按 |cos| 缩放,前半显示深色砖背,后半显示新砖
89
+ const sx = Math.abs(Math.cos(q * Math.PI));
90
+ c.fillStyle = '#17110d'; c.fillRect(g.x, g.y, tw, th);
91
+ const w = tw * sx;
92
+ if (q > 0.5) c.drawImage(B, g.x, g.y, tw, th, g.x + (tw - w) / 2, g.y, w, th);
93
+ else { c.fillStyle = '#2a1c14'; c.fillRect(g.x + (tw - w) / 2, g.y, w, th); }
94
+ }
95
+ },
96
+
97
+ // ④ 罗马→哥特:羊皮纸翻页,卷边从右向左扫过,背面是带淡淡旧画透印的羊皮纸
98
+ pageTurn(c, A, B, p) {
99
+ const e = ease.inOut(p);
100
+ const band = 260;
101
+ const x = lerp(W + band, -band * 1.4, e); // 卷边位置
102
+ c.drawImage(B, 0, 0);
103
+ // 旧页:卷边左侧
104
+ c.save(); c.beginPath(); c.rect(0, 0, Math.max(0, x - band * 0.15), H); c.clip(); c.drawImage(A, 0, 0); c.restore();
105
+ // 卷起的页背:羊皮纸色+旧画镜像透印+圆柱明暗
106
+ c.save();
107
+ c.beginPath(); c.rect(x - band * 0.15, 0, band, H); c.clip();
108
+ c.fillStyle = '#e9dcc0'; c.fillRect(x - band * 0.15, 0, band, H);
109
+ c.globalAlpha = 0.18; c.save(); c.translate(2 * x + band * 0.7, 0); c.scale(-1, 1); c.drawImage(A, 0, 0); c.restore(); c.globalAlpha = 1;
110
+ const g = c.createLinearGradient(x - band * 0.15, 0, x + band * 0.85, 0);
111
+ g.addColorStop(0, 'rgba(60,40,20,.45)'); g.addColorStop(0.18, 'rgba(255,248,230,.25)'); g.addColorStop(0.55, 'rgba(120,90,50,.12)'); g.addColorStop(1, 'rgba(40,25,10,.55)');
112
+ c.fillStyle = g; c.fillRect(x - band * 0.15, 0, band, H);
113
+ c.restore();
114
+ // 卷边投在新页上的影子
115
+ const sg = c.createLinearGradient(x + band * 0.85, 0, x + band * 0.85 + 90, 0);
116
+ sg.addColorStop(0, 'rgba(30,20,10,.35)'); sg.addColorStop(1, 'rgba(30,20,10,0)');
117
+ c.fillStyle = sg; c.fillRect(x + band * 0.85, 0, 90, H);
118
+ },
119
+
120
+ // ⑤ 哥特→文艺复兴:窗口迸出金色神光,光芒里旧画渐隐、新画浮现
121
+ godRays(c, A, B, p, o) {
122
+ const cx = o.cx || sx(440), cy = o.cy || sy(300);
123
+ c.drawImage(A, 0, 0);
124
+ c.globalAlpha = ease.out(seg(p, 0.05, 0.5)); c.drawImage(B, 0, 0); c.globalAlpha = 1;
125
+ // 光晕
126
+ const glow = Math.sin(Math.PI * Math.pow(seg(p, 0, 1), 0.6));
127
+ const rg = c.createRadialGradient(cx, cy, 0, cx, cy, 520);
128
+ rg.addColorStop(0, `rgba(255,246,210,${0.85 * glow})`); rg.addColorStop(0.35, `rgba(255,220,140,${0.35 * glow})`); rg.addColorStop(1, 'rgba(255,220,140,0)');
129
+ c.globalCompositeOperation = 'lighter'; c.fillStyle = rg; c.fillRect(0, 0, W, H);
130
+ // 光芒
131
+ const r = rng(5), n = 34, rot = p * 0.35;
132
+ for (let i = 0; i < n; i++) {
133
+ const a = i / n * Math.PI * 2 + rot + r() * 0.08, len = 2400, wd = 0.006 + r() * 0.01;
134
+ const al = glow * (0.25 + r() * 0.45);
135
+ c.fillStyle = `rgba(255,214,120,${al})`;
136
+ c.beginPath(); c.moveTo(cx, cy);
137
+ c.lineTo(cx + Math.cos(a - wd) * len, cy + Math.sin(a - wd) * len);
138
+ c.lineTo(cx + Math.cos(a + wd) * len, cy + Math.sin(a + wd) * len); c.fill();
139
+ }
140
+ c.globalCompositeOperation = 'source-over';
141
+ },
142
+
143
+ // ⑥ 文艺复兴→浮世绘:神奈川冲浪里的巨浪从左向右横扫,浪头白沫爪+飞溅浪花,浪后是新画面
144
+ wave(c, A, B, p) {
145
+ const e = ease.inOut(p);
146
+ const bw = 300; // 浪带宽
147
+ const x0 = lerp(-bw - 120, W + 260, e); // 浪带左缘
148
+ const edge = y => x0 + Math.sin(y / H * Math.PI * 1.6 + 0.6) * 70; // S 形
149
+ c.drawImage(A, 0, 0);
150
+ // 新画面:浪带左侧
151
+ c.save(); c.beginPath(); c.moveTo(-10, 0);
152
+ for (let y = 0; y <= H; y += 20) c.lineTo(edge(y) + 10, y);
153
+ c.lineTo(-10, H); c.closePath(); c.clip(); c.drawImage(B, 0, 0); c.restore();
154
+ // 浪带本体
155
+ c.save(); c.beginPath();
156
+ for (let y = 0; y <= H; y += 20) c.lineTo(edge(y), y);
157
+ for (let y = H; y >= 0; y -= 20) c.lineTo(edge(y) + bw, y);
158
+ c.closePath(); c.fillStyle = '#1f3f8a'; c.fill(); c.clip();
159
+ // 浪纹:几条平行的浅蓝曲线
160
+ c.strokeStyle = 'rgba(170,200,240,.75)'; c.lineWidth = 6;
161
+ for (let k = 1; k < 7; k++) {
162
+ c.beginPath();
163
+ for (let y = 0; y <= H; y += 20) c.lineTo(edge(y) + k * bw / 7 + Math.sin(y / 60 + k) * 6, y);
164
+ c.stroke();
165
+ }
166
+ c.restore();
167
+ // 浪头白沫爪(右缘锯齿)
168
+ c.fillStyle = '#fbf7ec'; c.strokeStyle = '#1b2a55'; c.lineWidth = 2;
169
+ for (let y = -20; y < H + 40; y += 54) {
170
+ const ex = edge(y) + bw;
171
+ c.beginPath(); c.moveTo(ex - 6, y);
172
+ c.quadraticCurveTo(ex + 40, y + 6, ex + 58, y + 22);
173
+ c.quadraticCurveTo(ex + 30, y + 26, ex + 22, y + 40);
174
+ c.quadraticCurveTo(ex + 10, y + 46, ex - 6, y + 54); c.closePath(); c.fill(); c.stroke();
175
+ }
176
+ // 飞沫
177
+ const r = rng(19);
178
+ for (let i = 0; i < 70; i++) {
179
+ const y = r() * H, ex = edge(y) + bw + 30 + r() * 170, rr = 3 + r() * 7;
180
+ c.globalAlpha = 0.9 * (1 - r() * 0.5);
181
+ c.beginPath(); c.arc(ex, y, rr, 0, Math.PI * 2); c.fill();
182
+ }
183
+ c.globalAlpha = 1;
184
+ },
185
+
186
+ // ⑦ 浮世绘→印象派:一笔笔短促的彩色笔触把新画面「画」上去
187
+ dabs(c, A, B, p) {
188
+ const D = once('dabs', () => {
189
+ const r = rng(23), out = [];
190
+ for (let i = 0; i < 2600; i++) out.push({ x: r() * W, y: r() * H, a: -0.9 + r() * 0.5, l: 34 + r() * 46, w: 10 + r() * 10, t: r() });
191
+ return out;
192
+ });
193
+ c.drawImage(A, 0, 0);
194
+ const q = ease.out(seg(p, 0, 0.7));
195
+ c.save(); c.beginPath();
196
+ let any = false;
197
+ for (const d of D) {
198
+ if (d.t > q) continue; any = true;
199
+ const s = clamp((q - d.t) / 0.12);
200
+ c.moveTo(d.x + Math.cos(d.a) * d.l * s / 2, d.y + Math.sin(d.a) * d.l * s / 2);
201
+ c.ellipse(d.x, d.y, d.l * s / 2, d.w / 2, d.a, 0, Math.PI * 2);
202
+ }
203
+ if (any) { c.clip(); c.drawImage(B, 0, 0); }
204
+ c.restore();
205
+ // 浮在上面的零星亮色笔触(印象派光斑)
206
+ const r = rng(29), al = Math.sin(Math.PI * p) * 0.8;
207
+ const cols = ['#f6e27a', '#f2a7c3', '#b9d9f2', '#c6e8a8', '#ffffff'];
208
+ for (let i = 0; i < 260; i++) {
209
+ const x = r() * W, y = r() * H, a = -0.8 + r() * 0.4, l = 20 + r() * 30, col = cols[(r() * cols.length) | 0];
210
+ if (r() > al) continue;
211
+ c.fillStyle = col; c.globalAlpha = 0.75; c.beginPath(); c.ellipse(x, y, l / 2, 4 + r() * 3, a, 0, Math.PI * 2); c.fill();
212
+ }
213
+ c.globalAlpha = 1;
214
+ if (p > 0.7) { c.globalAlpha = seg(p, 0.7, 1); c.drawImage(B, 0, 0); c.globalAlpha = 1; }
215
+ },
216
+
217
+ // ⑧ 印象派→梵高:星空漩涡——以窗户为中心的旋涡扭曲,新画面带着旋转卷进来、最后摆正
218
+ swirl(c, A, B, p, o) {
219
+ const cx = o.cx || sx(420), cy = o.cy || sy(300);
220
+ const S = 2; // 半分辨率做像素扭曲
221
+ const w = W / S, h = H / S;
222
+ const src = once('swirlSrc', () => ({ a: mkc(w, h), b: mkc(w, h), out: mkc(w, h) }));
223
+ const sa = src.a.getContext('2d'), sb = src.b.getContext('2d'), so = src.out.getContext('2d');
224
+ sa.drawImage(A, 0, 0, w, h); sb.drawImage(B, 0, 0, w, h);
225
+ const da = sa.getImageData(0, 0, w, h).data, db = sb.getImageData(0, 0, w, h).data;
226
+ const out = so.createImageData(w, h), d = out.data;
227
+ const e = ease.out(p);
228
+ const R = 1100 / S, ccx = cx / S, ccy = cy / S;
229
+ const twA = e * 5.0, twB = (1 - e) * 4.2, rotB = -(1 - e) * 0.9;
230
+ const mixR = e * 2.4; // 新画面从中心向外占领的半径(相对 R)
231
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
232
+ const dx = x - ccx, dy = y - ccy, r = Math.hypot(dx, dy), ang = Math.atan2(dy, dx), f = Math.max(0, 1 - r / R);
233
+ const useB = r / R < mixR - 0.15 + Math.sin(ang * 3 + r / 40) * 0.08;
234
+ let a2, sd;
235
+ if (useB) { a2 = ang + twB * f * f + rotB * Math.min(1, f * 1.6); sd = db; } else { a2 = ang - twA * f * f; sd = da; }
236
+ let sx = (ccx + Math.cos(a2) * r) | 0, sy = (ccy + Math.sin(a2) * r) | 0;
237
+ sx = sx < 0 ? 0 : sx >= w ? w - 1 : sx; sy = sy < 0 ? 0 : sy >= h ? h - 1 : sy;
238
+ const i = (y * w + x) * 4, j = (sy * w + sx) * 4;
239
+ d[i] = sd[j]; d[i + 1] = sd[j + 1]; d[i + 2] = sd[j + 2]; d[i + 3] = 255;
240
+ }
241
+ so.putImageData(out, 0, 0);
242
+ c.imageSmoothingQuality = 'high'; c.drawImage(src.out, 0, 0, W, H);
243
+ if (p > 0.85) { c.globalAlpha = seg(p, 0.85, 1); c.drawImage(B, 0, 0); c.globalAlpha = 1; }
244
+ },
245
+
246
+ // ⑨ 梵高→新艺术:从人物头部长出一块带金边的有机形状(慕夏式曲线轮廓),向外生长吞没画面
247
+ organic(c, A, B, p, o) {
248
+ const cx = o.cx || sx(1500), cy = o.cy || sy(330);
249
+ const e = ease.out(p) * 0.7 + p * 0.3;
250
+ const R = e * 2300;
251
+ const path = () => {
252
+ c.beginPath();
253
+ for (let i = 0; i <= 120; i++) {
254
+ const a = i / 120 * Math.PI * 2;
255
+ const rr = R * (1 + 0.22 * Math.sin(a * 3 + 0.7) + 0.12 * Math.sin(a * 5 + 2.1) + 0.06 * Math.sin(a * 9 + p * 3));
256
+ const x = cx + Math.cos(a) * rr, y = cy + Math.sin(a) * rr * 0.92;
257
+ i ? c.lineTo(x, y) : c.moveTo(x, y);
258
+ }
259
+ c.closePath();
260
+ };
261
+ c.drawImage(A, 0, 0);
262
+ if (R < 1) return;
263
+ c.save(); path(); c.clip(); c.drawImage(B, 0, 0); c.restore();
264
+ path(); c.lineJoin = 'round';
265
+ c.strokeStyle = '#6b4a24'; c.lineWidth = 14; c.stroke();
266
+ c.strokeStyle = '#d9b25a'; c.lineWidth = 8; c.stroke();
267
+ },
268
+
269
+ // ⑩ 新艺术→立体主义:画面碎成几何碎片,碎片错位旋转,新画面的碎片拼合落位
270
+ shards(c, A, B, p) {
271
+ const tris = once('shards', () => {
272
+ const r = rng(31), nx = 9, ny = 6, pts = [];
273
+ for (let j = 0; j <= ny; j++) for (let i = 0; i <= nx; i++) {
274
+ const edge = i === 0 || j === 0 || i === nx || j === ny;
275
+ pts.push([i / nx * W + (edge ? 0 : (r() - 0.5) * W / nx * 0.8), j / ny * H + (edge ? 0 : (r() - 0.5) * H / ny * 0.8)]);
276
+ }
277
+ const out = [];
278
+ for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
279
+ const a = pts[j * (nx + 1) + i], b = pts[j * (nx + 1) + i + 1], cc = pts[(j + 1) * (nx + 1) + i], d = pts[(j + 1) * (nx + 1) + i + 1];
280
+ const flip = r() < 0.5;
281
+ const t1 = flip ? [a, b, d] : [a, b, cc], t2 = flip ? [a, d, cc] : [b, d, cc];
282
+ for (const t of [t1, t2]) out.push({ p: t, cx: (t[0][0] + t[1][0] + t[2][0]) / 3, cy: (t[0][1] + t[1][1] + t[2][1]) / 3, t: r(), dx: (r() - 0.5) * 160, dy: (r() - 0.5) * 120, rot: (r() - 0.5) * 0.5, sh: 0.75 + r() * 0.5 });
283
+ }
284
+ return out;
285
+ });
286
+ const draw = (img, tr, q, out) => {
287
+ c.save(); c.beginPath(); tr.p.forEach((v, i) => i ? c.lineTo(v[0], v[1]) : c.moveTo(v[0], v[1])); c.closePath();
288
+ c.translate(tr.cx, tr.cy);
289
+ const k = out ? q : 1 - q; // 旧片:越来越偏;新片:越来越正
290
+ c.translate(tr.dx * k, tr.dy * k); c.rotate(tr.rot * k); c.translate(-tr.cx, -tr.cy);
291
+ c.clip(); c.drawImage(img, 0, 0);
292
+ c.fillStyle = `rgba(${out ? '60,45,30' : '255,250,235'},${0.12 * tr.sh * Math.sin(Math.PI * q)})`; c.fill();
293
+ c.strokeStyle = 'rgba(40,30,20,.35)'; c.lineWidth = 2; c.stroke();
294
+ c.restore();
295
+ };
296
+ c.fillStyle = '#cbbfa8'; c.fillRect(0, 0, W, H);
297
+ for (const tr of tris) {
298
+ const qo = ease.inOut(clamp((p - tr.t * 0.4) / 0.45)); // 旧片碎裂进度
299
+ const qi = ease.out(clamp((p - 0.25 - tr.t * 0.45) / 0.3)); // 新片落位进度
300
+ if (qi > 0) draw(B, tr, qi, false); else draw(A, tr, qo, true);
301
+ }
302
+ },
303
+
304
+ // ⑪ 立体→包豪斯:蓝方块+红圆+黄三角的构成旋转放大,中心的米白圆里是新画面,最后构成扫出画面
305
+ bauhaus(c, A, B, p, o) {
306
+ const cx = o.cx || sx(1010), cy = o.cy || sy(560);
307
+ c.drawImage(A, 0, 0);
308
+ const s = Math.pow(30, ease.out(seg(p, 0, 0.7))) * 0.35; // 指数放大
309
+ const rot = -0.5 + p * 0.9;
310
+ const shift = ease.in(seg(p, 0.4, 1)) * -1600; // 构成整体扫向左边
311
+ c.save(); c.translate(cx + shift, cy); c.rotate(rot); c.scale(s, s);
312
+ c.fillStyle = '#1d4fa3'; c.fillRect(-170, -170, 340, 340);
313
+ c.rotate(-rot * 0.6);
314
+ c.fillStyle = '#d0262b'; c.beginPath(); c.arc(0, 0, 150, 0, Math.PI * 2); c.fill();
315
+ c.fillStyle = '#f2c81c'; c.beginPath(); c.moveTo(0, -150); c.lineTo(130, 75); c.lineTo(-130, 75); c.closePath(); c.fill();
316
+ c.restore();
317
+ // 米白圆里是新画面(不跟随整体扫出,持续放大直到铺满)
318
+ const rr = Math.pow(40, ease.inOut(seg(p, 0.15, 0.85))) * 30;
319
+ c.save(); c.beginPath(); c.arc(cx, cy, rr, 0, Math.PI * 2); c.clip(); c.drawImage(B, 0, 0); c.restore();
320
+ },
321
+
322
+ // ⑫ 包豪斯→波普:黄色闪电劈开+本戴网点化,旧画面变网点、新画面从网点里显影
323
+ zigzag(c, A, B, p) {
324
+ const halftone = (img, amt, key) => {
325
+ const sc = 16;
326
+ const sm = once('ht_' + key, () => mkc(W / sc, H / sc));
327
+ const sg = sm.getContext('2d'); sg.drawImage(img, 0, 0, W / sc, H / sc);
328
+ const d = sg.getImageData(0, 0, W / sc, H / sc).data;
329
+ c.globalAlpha = 1; c.drawImage(img, 0, 0); c.fillStyle = 'rgba(244,236,216,.55)'; c.fillRect(0, 0, W, H);
330
+ for (let y = 0; y < H / sc; y++) for (let x = 0; x < W / sc; x++) {
331
+ const i = (y * W / sc + x) * 4, L = (d[i] * .3 + d[i + 1] * .59 + d[i + 2] * .11) / 255;
332
+ const r = sc * 0.62 * (0.35 + (1 - L) * 0.85) * amt + 1;
333
+ c.fillStyle = `rgb(${d[i]},${d[i + 1]},${d[i + 2]})`;
334
+ c.beginPath(); c.arc(x * sc + sc / 2 + (y % 2) * sc / 2, y * sc + sc / 2, r, 0, Math.PI * 2); c.fill();
335
+ }
336
+ };
337
+ if (p < 0.35) c.drawImage(A, 0, 0);
338
+ else if (p < 0.55) halftone(A, 1, 'a');
339
+ else if (p < 0.75) halftone(B, 1, 'b');
340
+ else { c.drawImage(B, 0, 0); c.globalAlpha = 1 - seg(p, 0.75, 1); halftone(B, 1, 'b'); c.globalAlpha = 1; }
341
+ // 闪电:黑边黄色锯齿带,从左上劈到右下
342
+ const q = seg(p, 0.05, 0.6); if (q <= 0 || q >= 1) return;
343
+ const pts = [], n = 9;
344
+ for (let i = 0; i <= n; i++) pts.push([lerp(780, 1450, i / n) + (i % 2 ? 90 : -90), lerp(-40, H + 40, i / n)]);
345
+ const k = Math.min(n, Math.floor(q * 2 * n) + 1);
346
+ c.save(); c.lineJoin = 'miter';
347
+ c.beginPath(); pts.slice(0, k + 1).forEach((v, i) => i ? c.lineTo(v[0], v[1]) : c.moveTo(v[0], v[1]));
348
+ c.strokeStyle = '#111'; c.lineWidth = 34; c.stroke(); c.strokeStyle = '#ffe02a'; c.lineWidth = 22; c.stroke();
349
+ c.restore();
350
+ // 起点星爆
351
+ if (q < 0.5) {
352
+ c.save(); c.translate(70, 990); c.fillStyle = '#ffe02a'; c.strokeStyle = '#111'; c.lineWidth = 4; c.beginPath();
353
+ for (let i = 0; i < 28; i++) { const a = i / 28 * Math.PI * 2, r = i % 2 ? 70 : 105; c.lineTo(Math.cos(a) * r, Math.sin(a) * r); }
354
+ c.closePath(); c.fill(); c.stroke(); c.restore();
355
+ }
356
+ },
357
+
358
+ // ⑬ 波普→8-bit:旧画面像素块越来越大,切到新画面后像素块由大变小直至清晰
359
+ pixelate(c, A, B, p) {
360
+ const img = p < 0.5 ? A : B;
361
+ const q = p < 0.5 ? p / 0.5 : 1 - (p - 0.5) / 0.5;
362
+ const bs = Math.max(1, Math.round(lerp(1, 72, ease.in(q))));
363
+ if (bs <= 1) { c.drawImage(img, 0, 0); return; }
364
+ const sm = once('px', () => mkc(W, H)), sg = sm.getContext('2d');
365
+ const w = Math.ceil(W / bs), h = Math.ceil(H / bs);
366
+ sg.imageSmoothingEnabled = true; sg.clearRect(0, 0, w, h); sg.drawImage(img, 0, 0, w, h);
367
+ c.imageSmoothingEnabled = false; c.drawImage(sm, 0, 0, w, h, 0, 0, w * bs, h * bs); c.imageSmoothingEnabled = true;
368
+ },
369
+
370
+ // ⑭ 8-bit→光追:黑底绿色霓虹线框(新画面的边缘+透视网格地面),扫描线自上而下把渲染结果「算」出来
371
+ wireframe(c, A, B, p) {
372
+ const edges = (() => { // 每帧从当前 B 重算(半分辨率 Sobel,约 10ms):缓存会串段、依赖渲染顺序
373
+ const s = 2, w = W / s, h = H / s, sm = once('edgesCanvas', () => mkc(w, h)), g = sm.getContext('2d', { willReadFrequently: true });
374
+ g.clearRect(0, 0, w, h); g.drawImage(B, 0, 0, w, h);
375
+ const d = g.getImageData(0, 0, w, h).data, out = g.createImageData(w, h), o = out.data;
376
+ const L = (x, y) => { const i = (y * w + x) * 4; return d[i] * .3 + d[i + 1] * .59 + d[i + 2] * .11; };
377
+ for (let y = 1; y < h - 1; y++) for (let x = 1; x < w - 1; x++) {
378
+ const gx = L(x + 1, y) - L(x - 1, y), gy = L(x, y + 1) - L(x, y - 1), m = Math.hypot(gx, gy);
379
+ const i = (y * w + x) * 4, v = m > 38 ? 255 : 0;
380
+ o[i] = 60; o[i + 1] = 255; o[i + 2] = 150; o[i + 3] = v;
381
+ }
382
+ g.putImageData(out, 0, 0); return sm;
383
+ })();
384
+ const wire = () => {
385
+ c.fillStyle = '#02060a'; c.fillRect(0, 0, W, H);
386
+ // 透视网格地面
387
+ c.strokeStyle = 'rgba(60,255,150,.8)'; c.lineWidth = 2;
388
+ const hy = 640, vx = W / 2;
389
+ for (let i = -24; i <= 24; i++) { c.beginPath(); c.moveTo(vx + i * 30, hy); c.lineTo(vx + i * 260, H); c.stroke(); }
390
+ for (let k = 0; k < 12; k++) { const y = hy + Math.pow(k / 12, 2) * (H - hy); c.beginPath(); c.moveTo(0, y); c.lineTo(W, y); c.stroke(); }
391
+ c.shadowColor = '#3cff96'; c.shadowBlur = 8; c.drawImage(edges, 0, 0, W, H); c.shadowBlur = 0;
392
+ };
393
+ if (p < 0.12) { c.drawImage(A, 0, 0); c.globalAlpha = p / 0.12; wire(); c.globalAlpha = 1; return; }
394
+ wire();
395
+ const q = ease.inOut(seg(p, 0.3, 1)), yy = q * H;
396
+ if (yy > 0) { c.save(); c.beginPath(); c.rect(0, 0, W, yy); c.clip(); c.drawImage(B, 0, 0); c.restore(); }
397
+ if (q > 0 && q < 1) { c.fillStyle = 'rgba(200,255,230,.9)'; c.fillRect(0, yy - 3, W, 6); }
398
+ },
399
+
400
+ // ⑮ 光追→2026:三色同心环从杯子(992,601)线性扩张,外径约 143px/帧,每环厚 137px,最内环里是新画面(分镜表 T15)
401
+ iris(c, A, B, p, o) {
402
+ const cx = o.cx || sx(992), cy = o.cy || sy(601);
403
+ const R = 19 + p * (1159 - 19 + 3 * 137 + 900); // 线性;到 p=1 时最内环也超出画面对角
404
+ c.drawImage(A, 0, 0);
405
+ const ring = (r, col) => { if (r <= 0) return; c.fillStyle = col; c.beginPath(); c.arc(cx, cy, r, 0, Math.PI * 2); c.fill(); };
406
+ ring(R, 'rgb(33,42,81)'); ring(R - 137, 'rgb(234,124,107)'); ring(R - 274, 'rgb(250,246,235)');
407
+ const ri = R - 411;
408
+ if (ri > 0) { c.save(); c.beginPath(); c.arc(cx, cy, ri, 0, Math.PI * 2); c.clip(); c.drawImage(B, 0, 0); c.restore(); }
409
+ },
410
+
411
+ // ===================== 迁移测试 A–D 收进来的转场(名字同 references/风格配方 里的配方卡) =====================
412
+ // 每个都长在「进入段」风格的签名语言上:先找新风格里会「出现/点亮/扫过」的物理过程,旧画先被它污染,新画从它的形状里露出来。
413
+ // ---- A:水墨 / 克里姆特 / 蒙克 / 敦煌 / 草间 ----
414
+ // 水墨 → 克里姆特:金箔一片片贴上来。从少女头部按螺旋次序落下方形金箔(带螺旋/眼形/同心圆压纹),
415
+ // 贴实后金色褪去露出新画面。q<0.5 贴金箔(从 1.3 倍、带旋转落位),0.5..1 金箔变透明露出 B。
416
+ goldLeaf(c, A, B, p, o) {
417
+ const ts = 72, cx = o.cx || sx(1300), cy = o.cy || sy(380);
418
+ const tiles = once('goldLeaf', () => { const r = rng(21), out = [], maxd = Math.hypot(W, H) * 0.8;
419
+ for (let y = 0; y < H; y += ts) for (let x = 0; x < W; x += ts) { const mx = x + ts / 2, my = y + ts / 2; const d = Math.hypot(mx - cx, my - cy) / maxd, a = (Math.atan2(my - cy, mx - cx) / (Math.PI * 2) + 1) % 1;
420
+ out.push({ x, y, key: d * 0.78 + a * 0.12 + r() * 0.08, rot: (r() - .5) * 0.8, m: (r() * 3) | 0 }); }
421
+ return out; });
422
+ c.drawImage(A, 0, 0);
423
+ for (const t of tiles) {
424
+ const q = clamp((p * 1.32 - t.key) / 0.32); if (q <= 0) continue;
425
+ if (q >= 1) { c.drawImage(B, t.x, t.y, ts, ts, t.x, t.y, ts, ts); continue; }
426
+ const mx = t.x + ts / 2, my = t.y + ts / 2;
427
+ if (q > 0.5) c.drawImage(B, t.x, t.y, ts, ts, t.x, t.y, ts, ts);
428
+ const u = q < 0.5 ? q / 0.5 : 1, fadeOut = q < 0.5 ? 1 : 1 - (q - 0.5) / 0.5;
429
+ c.save(); c.translate(mx, my); c.rotate(t.rot * (1 - ease.out(u))); const s = 1.3 - 0.3 * ease.out(u); c.scale(s, s);
430
+ c.globalAlpha = Math.min(1, u * 2.5) * fadeOut;
431
+ const g = c.createLinearGradient(-ts / 2, -ts / 2, ts / 2, ts / 2); const sh = clamp(u);
432
+ g.addColorStop(0, '#a8741e'); g.addColorStop(clamp(0.2 + sh * 0.5), '#fbe6a0'); g.addColorStop(1, '#c8962e');
433
+ c.fillStyle = g; c.fillRect(-ts / 2, -ts / 2, ts, ts);
434
+ c.strokeStyle = 'rgba(70,40,10,.75)'; c.lineWidth = 2.2; c.beginPath();
435
+ if (t.m === 0) { for (let a = 0; a < Math.PI * 5; a += 0.25) { const rr = 26 * a / (Math.PI * 5); a ? c.lineTo(Math.cos(a) * rr, Math.sin(a) * rr) : c.moveTo(0, 0); } }
436
+ else if (t.m === 1) { c.moveTo(-24, 0); c.quadraticCurveTo(0, -20, 24, 0); c.quadraticCurveTo(0, 20, -24, 0); c.moveTo(7, 0); c.arc(0, 0, 7, 0, Math.PI * 2); }
437
+ else { [8, 16, 24].forEach(rr => { c.moveTo(rr, 0); c.arc(0, 0, rr, 0, Math.PI * 2); }); }
438
+ c.stroke(); c.restore();
439
+ }
440
+ },
441
+ // 克里姆特 → 蒙克:《呐喊》的血色天空从上往下「流」过画面。旧画按行正弦扭曲、幅度越来越大;
442
+ // 波浪形前沿以上是新画面(扭曲幅度由大变小),前沿上压 4 道黄橙红色带。
443
+ screamWarp(c, A, B, p, o) {
444
+ const e = ease.inOut(p), ph = o.lt * 9;
445
+ const yF = x => lerp(-180, H + 220, e) + 80 * Math.sin(x * 0.0055 + p * 7);
446
+ c.fillStyle = '#3a1410'; c.fillRect(0, 0, W, H);
447
+ KIT.warpRows(c, A, y => 70 * e * Math.sin(y * 0.014 + ph) + 20 * e * Math.sin(y * 0.05 - ph), 6, 92 * e);
448
+ const tg = o.tmp.getContext('2d'); tg.reset(); tg.fillStyle = '#3a1410'; tg.fillRect(0, 0, W, H);
449
+ KIT.warpRows(tg, B, y => 60 * (1 - e) * Math.sin(y * 0.013 - ph), 6, 62 * (1 - e));
450
+ c.save(); c.beginPath(); c.moveTo(0, -10); for (let x = 0; x <= W; x += 24) c.lineTo(x, yF(x)); c.lineTo(W, -10); c.closePath(); c.clip(); c.drawImage(o.tmp, 0, 0); c.restore();
451
+ c.save(); c.lineCap = 'round';
452
+ ['#b82a1e', '#e2541c', '#f08a24', '#f4c03a'].forEach((col, k) => { c.strokeStyle = col; c.lineWidth = 26; c.beginPath(); for (let x = -20; x <= W + 20; x += 16) { const y = yF(x) - k * 22 + 10 * Math.sin(x * 0.02 + ph + k); x > -20 ? c.lineTo(x, y) : c.moveTo(x, y); } c.stroke(); });
453
+ c.restore();
454
+ },
455
+ // 蒙克 → 敦煌:飞天的飘带从右往左扫过,带后是壁画;前沿处旧画剥落成碎片掉下去。
456
+ apsara(c, A, B, p, o) {
457
+ const e = ease.inOut(p);
458
+ const xF = y => lerp(W + 260, -300, e) + 110 * Math.sin(y * 0.006 + p * 5);
459
+ c.drawImage(A, 0, 0);
460
+ c.save(); c.beginPath(); c.moveTo(W + 10, -10); for (let y = -10; y <= H + 10; y += 20) c.lineTo(xF(y), y); c.lineTo(W + 10, H + 10); c.closePath(); c.clip(); c.drawImage(B, 0, 0); c.restore();
461
+ // 剥落:前沿左侧旧画碎片(取 A 的像素)旋转落下
462
+ const fl = once('apsaraFlakes', () => { const r = rng(33), o2 = []; for (let i = 0; i < 70; i++) o2.push([r() * H, 20 + r() * 34, 10 + r() * 26, r() * 6.28, r()]); return o2; });
463
+ fl.forEach(([y0, s, dx, rot, k]) => { const born = clamp(1 - (xF(y0) + 40) / (W + 300)); const age = e - born * 0.9; if (age < 0) return;
464
+ const x = xF(y0) - dx + age * 120, y = y0 + age * age * 900; if (y > H + 40) return;
465
+ c.save(); c.translate(x, y); c.rotate(rot + age * 9); c.scale(1, Math.cos(age * 12 + k * 6)); c.drawImage(A, clamp(x, 0, W - s), clamp(y0, 0, H - s), s, s * 0.7, -s / 2, -s * 0.35, s, s * 0.7); c.restore(); });
466
+ // 三条飘带沿前沿翻卷(石绿、土红、石青),带白色描花
467
+ [['#6ab08e', 0, 44], ['#a5452f', 70, 38], ['#5a86b0', 140, 32]].forEach(([col, off, w], k) => {
468
+ const pts = []; for (let y = -60; y <= H + 60; y += 18) pts.push([xF(y) + off + 46 * Math.sin(y * 0.011 - p * 14 + k * 1.7), y]);
469
+ const path = KIT.ribbon(pts, q => w * (0.6 + 0.4 * Math.sin(q * 14 + p * 10 + k)));
470
+ c.fillStyle = col; c.fill(path); c.strokeStyle = '#5a2414'; c.lineWidth = 2.4; c.stroke(path);
471
+ c.strokeStyle = '#ecdfc4'; c.lineWidth = 2; c.setLineDash([4, 10]); c.beginPath(); pts.forEach((q, i) => i ? c.lineTo(q[0], q[1]) : c.moveTo(q[0], q[1])); c.stroke(); c.setLineDash([]);
472
+ });
473
+ },
474
+ // 敦煌 → 草间:波点从猫的位置一圈圈长出来,点里是新画面;点胀到彼此相接就整幅换完。
475
+ dotBloom(c, A, B, p, o) {
476
+ const sp = 58, cx = o.cx || sx(540), cy = o.cy || sy(760), maxd = Math.hypot(W, H);
477
+ c.drawImage(A, 0, 0);
478
+ const pts = once('dotBloom', () => { const r = rng(44), out = []; for (let y = 0, j = 0; y < H + sp; y += sp * 0.87, j++) for (let x = (j % 2) * sp / 2; x < W + sp; x += sp) out.push([x + (r() - .5) * 8, y + (r() - .5) * 8, Math.hypot(x - cx, y - cy) / maxd, r()]); return out; });
479
+ const path = new Path2D(), rims = [];
480
+ for (const [x, y, d, k] of pts) { const q = clamp((p * 1.55 - d * 1.1 - k * 0.08) / 0.42); if (q <= 0) continue; const r = ease.outBack(q) * sp * 0.74; path.moveTo(x + r, y); path.arc(x, y, r, 0, Math.PI * 2); if (q < 1) rims.push([x, y, r, q]); }
481
+ c.save(); c.clip(path); c.drawImage(B, 0, 0); c.restore();
482
+ c.lineWidth = 5; rims.forEach(([x, y, r, q]) => { c.strokeStyle = `rgba(251,246,238,${1 - q})`; c.beginPath(); c.arc(x, y, r, 0, Math.PI * 2); c.stroke(); });
483
+ },
484
+ // ---- C:Kirby / 莫奈 / 修拉 / 马蒂斯 / 哈林 ----
485
+ // ① Kirby → 莫奈:旧画面「落进水里」——逐行横向正弦错位(水面倒影的抖动)越来越强;
486
+ // 新画面从杯子位置以「横向短笔触」一圈圈荡开(涟漪由笔触组成),亮色水光短横线闪烁。
487
+ ripple(c, A, B, p, o) {
488
+ const cx = o.cx || sx(1000), cy = o.cy || sy(600), e = ease.inOut(p);
489
+ const amp = 46 * Math.sin(Math.PI * Math.min(1, p * 1.2)), rowH = 6;
490
+ for (let y = 0; y < H; y += rowH) { const d = Math.abs(y - cy) / H, dx = Math.sin(y * 0.045 - p * 26) * amp * (0.4 + d); c.drawImage(A, 0, y, W, rowH, dx, y, W, rowH); }
491
+ const S = once('rip', () => { const r = rng(41), out = []; for (let i = 0; i < 3600; i++) { const x = r() * W, y = r() * H; out.push({ x, y, l: 40 + r() * 70, h: 9 + r() * 8, d: Math.hypot(x - cx, (y - cy) * 1.9) / 1700 + r() * 0.06 }); } return out; });
492
+ const front = e * 1.15 - 0.05;
493
+ c.save(); c.beginPath(); let any = false;
494
+ for (const s of S) { if (s.d > front) continue; any = true; const k = clamp((front - s.d) / 0.06); c.ellipse(s.x, s.y, s.l * k / 2 + 1, s.h / 2, 0, 0, TAU); }
495
+ if (any) { c.clip(); c.drawImage(B, 0, 0); }
496
+ c.restore();
497
+ // 涟漪前沿:几圈浅色虚线椭圆+水光短横线
498
+ c.save(); c.lineCap = 'round';
499
+ for (let k = 0; k < 3; k++) { const R = front * 2300 - k * 70; if (R <= 0) continue; c.strokeStyle = `rgba(240,246,255,${0.7 - k * 0.2})`; c.lineWidth = 10 - k * 2; c.setLineDash([46, 22]); c.lineDashOffset = -p * 300; c.beginPath(); c.ellipse(cx, cy, R, R / 1.9, 0, 0, TAU); c.stroke(); }
500
+ c.setLineDash([]);
501
+ const r = rng(7 + Math.floor(p * 30)), al = Math.sin(Math.PI * p);
502
+ for (let i = 0; i < 140; i++) { const x = r() * W, y = r() * H; c.strokeStyle = ['#fffbe8', '#f8e0ec', '#e8f4ff'][i % 3]; c.globalAlpha = al * 0.8; c.lineWidth = 5; c.beginPath(); c.moveTo(x, y); c.lineTo(x + 18 + r() * 30, y); c.stroke(); }
503
+ c.restore();
504
+ if (p > 0.7) { c.globalAlpha = seg(p, 0.7, 0.95); c.drawImage(B, 0, 0); c.globalAlpha = 1; }
505
+ },
506
+
507
+ // ② 莫奈 → 修拉:画面「结晶」成一样大的圆点——先旧画面长出点(点长大、底下淡成纸色),
508
+ // 点按对角线波次逐个换成新画面的颜色,最后新画面在点下面显影、点收回去。
509
+ pointill(c, A, B, p, o) {
510
+ const pitch = 14, rowh = pitch * 0.866;
511
+ const sa = once('ptA', () => mkc(W, H)), sb = once('ptB', () => mkc(W, H));
512
+ const ga = sa.getContext('2d', { willReadFrequently: true }), gb = sb.getContext('2d', { willReadFrequently: true });
513
+ ga.drawImage(A, 0, 0); gb.drawImage(B, 0, 0);
514
+ const da = ga.getImageData(0, 0, W, H).data, db = gb.getImageData(0, 0, W, H).data;
515
+ // 底:旧画面 → 纸色 → 新画面
516
+ c.drawImage(A, 0, 0);
517
+ c.fillStyle = `rgba(246,238,220,${seg(p, 0.05, 0.4)})`; c.fillRect(0, 0, W, H);
518
+ if (p > 0.6) { c.globalAlpha = seg(p, 0.6, 0.95); c.drawImage(B, 0, 0); c.globalAlpha = 1; }
519
+ const grow = ease.out(seg(p, 0, 0.3)), shrink = 1 - ease.in(seg(p, 0.7, 1)), R = pitch * 0.46 * grow * shrink;
520
+ if (R < 0.4) return;
521
+ for (let j = 0, y = pitch / 2; y < H + pitch; j++, y += rowh) for (let x = (j % 2) * pitch / 2; x < W + pitch; x += pitch) {
522
+ const sw = clamp(((x / W) * 0.6 + (y / H) * 0.4) * 0.5 + U.hash(x | 0, y | 0) * 0.25 + 0.15); // 换色时刻:左上先、带随机
523
+ const useB = p > sw;
524
+ const i = (clamp(y | 0, 0, H - 1) * W + clamp(x | 0, 0, W - 1)) * 4, d = useB ? db : da;
525
+ // 换色瞬间点会「跳」一下(变大)
526
+ const pop = Math.abs(p - sw) < 0.04 ? 1.35 : 1;
527
+ c.fillStyle = `rgb(${d[i]},${d[i + 1]},${d[i + 2]})`; c.beginPath(); c.arc(x, y, R * pop, 0, TAU); c.fill();
528
+ }
529
+ },
530
+
531
+ // ③ 修拉 → 马蒂斯:一大张刷过水粉的钴蓝纸(剪刀边)从右往左扫过,纸上钉着白海藻、黄星、粉纸片(带投影);
532
+ // 纸经过的地方,后面换成了新画面。
533
+ cutouts(c, A, B, p, o) {
534
+ const e = ease.inOut(p), w = 1100; // 纸宽
535
+ const x0 = lerp(W + 40, -w - 360, e); // 纸左缘
536
+ const edge = (y, side) => { const r = Math.sin(y * 0.021 + side * 3) * 26 + Math.sin(y * 0.067 + side) * 12; return r; };
537
+ // 新画面在纸的右缘之后
538
+ c.drawImage(A, 0, 0);
539
+ c.save(); c.beginPath(); c.moveTo(W + 10, 0); for (let y = 0; y <= H; y += 18) c.lineTo(x0 + w + edge(y, 1), y); c.lineTo(W + 10, H); c.closePath(); c.clip(); c.drawImage(B, 0, 0); c.restore();
540
+ // 纸(带投影)
541
+ const sheet = new Path2D(); for (let y = 0; y <= H; y += 18) sheet.lineTo(x0 + edge(y, 0), y); for (let y = H; y >= 0; y -= 18) sheet.lineTo(x0 + w + edge(y, 1), y); sheet.closePath();
542
+ c.save(); c.shadowColor = 'rgba(10,20,40,.45)'; c.shadowOffsetX = 14; c.shadowOffsetY = 10; c.shadowBlur = 12; c.fillStyle = '#1d4fb8'; c.fill(sheet); c.restore();
543
+ c.save(); c.clip(sheet);
544
+ const r0 = rng(83); c.lineCap = 'round';
545
+ for (let i = 0; i < 260; i++) { const x = x0 + r0() * w, y = r0() * H; c.strokeStyle = r0() < 0.5 ? 'rgba(255,255,255,.08)' : 'rgba(0,0,0,.08)'; c.lineWidth = 8 + r0() * 14; c.beginPath(); c.moveTo(x, y); c.lineTo(x + 160, y - 40); c.stroke(); }
546
+ // 纸上的剪纸:海藻(茎+手指)、星、圆片;随纸移动并各自晃
547
+ const pin = (fn, col, x, y, rot) => { c.save(); c.translate(x, y); c.rotate(rot); c.shadowColor = 'rgba(10,20,40,.35)'; c.shadowOffsetX = 6; c.shadowOffsetY = 7; c.shadowBlur = 4; c.fillStyle = col; c.fill(fn); c.restore(); };
548
+ const algae = once('ctAlg', () => { const p2 = new Path2D(); p2.addPath(RIG.limb([0, 0], [0, -420], 50, 26));
549
+ for (let k = 0; k < 7; k++) { const u = 0.12 + k * 0.12, s = k % 2 ? 1 : -1, A0 = [0, -u * 420], th = s * (1.0 - 0.5 * u), L = 190 * (1 - 0.45 * u), d = [Math.sin(th), -Math.cos(th)];
550
+ p2.addPath(RIG.limb(A0, [A0[0] + d[0] * L, A0[1] + d[1] * L], 64 * (1 - 0.3 * u), 56 * (1 - 0.3 * u))); } return p2; });
551
+ const star = once('ctStar', () => KIT.star(0, 0, 120, 52, 6));
552
+ pin(algae, '#f7f3e8', x0 + 260, 900, Math.sin(p * 9) * 0.1);
553
+ pin(algae, '#f7d21e', x0 + 760, 1060, -0.2 + Math.sin(p * 11) * 0.1);
554
+ pin(star, '#f7d21e', x0 + 540, 260, p * 3);
555
+ pin(star, '#ef6f9a', x0 + 900, 520, -p * 2.5);
556
+ const blob = once('ctBlob', () => { const q = new Path2D(); q.ellipse(0, 0, 130, 90, 0, 0, TAU); return q; }); pin(blob, '#e2322a', x0 + 180, 300, 0.3);
557
+ c.restore();
558
+ },
559
+
560
+ // ④ 马蒂斯 → 哈林:一个粗黑轮廓的跳舞小人(双臂 V 字)从杯子处放大,身体里面是新画面,
561
+ // 周围一圈哈林式放射动作线;小人越长越大直到身体铺满整个画面。
562
+ radiant(c, A, B, p, o) {
563
+ const cx = o.cx || sx(1000), cy = o.cy || sy(600), s = 0.15 * Math.pow(320, ease.inOut(p));
564
+ const pose = { head: [0, -224], neck: [0, -182], hip: [0, -100], hl: [-84, -256], hr: [84, -256], kl: [-40, -54], kr: [40, -54], fl: [-60, -14], fr: [60, -14] };
565
+ const T = q => [cx + q[0] * s, cy + (q[1] + 130) * s];
566
+ const parts = [RIG.limb(T(pose.neck), T(pose.hip), 70 * s, 60 * s), RIG.limb(T(pose.neck), T(pose.hl), 36 * s, 32 * s), RIG.limb(T(pose.neck), T(pose.hr), 36 * s, 32 * s),
567
+ RIG.limb(T(pose.hip), T(pose.kl), 38 * s, 34 * s), RIG.limb(T(pose.kl), T(pose.fl), 34 * s, 32 * s), RIG.limb(T(pose.hip), T(pose.kr), 38 * s, 34 * s), RIG.limb(T(pose.kr), T(pose.fr), 34 * s, 32 * s)];
568
+ const hd = new Path2D(), hc = T(pose.head); hd.arc(hc[0], hc[1], 42 * s, 0, TAU); parts.push(hd);
569
+ c.drawImage(A, 0, 0);
570
+ // 黑色外轮廓(描粗+填黑)
571
+ c.save(); c.lineJoin = 'round'; c.strokeStyle = '#111'; c.lineWidth = 30; c.fillStyle = '#111'; parts.forEach(q => { c.stroke(q); c.fill(q); }); c.restore();
572
+ // 身体里是新画面:离屏做遮罩
573
+ const m = once('rdM', () => mkc()), mg = m.getContext('2d'); mg.globalCompositeOperation = 'source-over'; mg.clearRect(0, 0, W, H);
574
+ mg.fillStyle = '#fff'; parts.forEach(q => mg.fill(q)); mg.globalCompositeOperation = 'source-in'; mg.drawImage(B, 0, 0);
575
+ c.drawImage(m, 0, 0);
576
+ // 放射动作线(8fps 跳)
577
+ const st = Math.floor(o.t * 8), r = rng(300 + st); c.save(); c.strokeStyle = '#111'; c.lineCap = 'round'; c.lineWidth = 14;
578
+ const R0 = 300 * s + 30;
579
+ for (let i = 0; i < 16; i++) { const a = i / 16 * TAU + r() * 0.2, L = 50 + r() * 60; c.beginPath(); c.moveTo(cx + Math.cos(a) * R0, cy + Math.sin(a) * R0 * 0.9); c.lineTo(cx + Math.cos(a) * (R0 + L), cy + Math.sin(a) * (R0 + L) * 0.9); c.stroke(); }
580
+ c.restore();
581
+ if (p > 0.82) { c.globalAlpha = seg(p, 0.82, 1); c.drawImage(B, 0, 0); c.globalAlpha = 1; }
582
+ },
583
+ // ---- D:伦勃朗 / 橡皮管 / 皮影 / 新海诚 / 蓝色时期 ----
584
+ // 伦勃朗 → 橡皮管卡通:旧画先褪成棕褐老胶片(闪烁+划痕),黑色光圈收到少女杯口,再从同一点「弹」开(outBack 过冲)
585
+ irisFilm(c, A, B, p, o) {
586
+ const cx = o.cx || W / 2, cy = o.cy || H / 2, R0 = Math.hypot(Math.max(cx, W - cx), Math.max(cy, H - cy)) + 20;
587
+ const close = p < 0.5, q = close ? seg(p, 0, 0.5) : seg(p, 0.5, 1);
588
+ if (close) { c.save(); c.filter = `sepia(${q}) grayscale(${q * 0.7}) contrast(${1 + q * 0.3})`; c.drawImage(A, 0, 0); c.restore(); }
589
+ else c.drawImage(B, 0, 0);
590
+ const R = close ? R0 * (1 - ease.in(q)) : R0 * ease.outBack(q);
591
+ c.save(); c.fillStyle = '#0b0906'; c.beginPath(); c.rect(0, 0, W, H);
592
+ // 光圈边缘微微手抖(12fps 换形)
593
+ const wr = rng(Math.floor(o.t * 12) + 9); c.moveTo(cx + Math.max(0, R), cy);
594
+ for (let i = 1; i <= 48; i++) { const a = i / 48 * Math.PI * 2, rr = Math.max(0, R * (1 + (wr() - 0.5) * 0.015)); c.lineTo(cx + Math.cos(a) * rr, cy + Math.sin(a) * rr); }
595
+ c.fill('evenodd'); c.restore();
596
+ filmDirt(c, o.t, close ? q : 1 - q * 0.5);
597
+ },
598
+ // 橡皮管 → 皮影:旧片像灯灭一样压成暗褐剪影;幕后的油灯从中心点亮,暖光圈(边缘带灯焰抖动)向外推开,新画面在光里出现
599
+ lampGlow(c, A, B, p, o) {
600
+ const cx = o.cx || W / 2, cy = o.cy || sy(470);
601
+ c.drawImage(A, 0, 0);
602
+ c.save(); c.globalCompositeOperation = 'multiply'; c.fillStyle = `rgba(70,34,12,${Math.min(1, p * 2.2)})`; c.fillRect(0, 0, W, H); c.restore();
603
+ const fl = Math.sin(o.t * 37) * 0.04 + Math.sin(o.t * 23) * 0.03;
604
+ const R = 1500 * ease.inOut(seg(p, 0.12, 1)) * (1 + fl);
605
+ if (R > 1) {
606
+ const T = o.tmp, g = T.getContext('2d'); g.reset(); g.drawImage(B, 0, 0);
607
+ g.globalCompositeOperation = 'destination-in'; const m = g.createRadialGradient(cx, cy, R * 0.55, cx, cy, R); m.addColorStop(0, '#000'); m.addColorStop(1, 'rgba(0,0,0,0)'); g.fillStyle = m; g.fillRect(0, 0, W, H);
608
+ c.drawImage(T, 0, 0);
609
+ }
610
+ c.save(); c.globalCompositeOperation = 'lighter'; const gl = Math.sin(Math.PI * seg(p, 0.05, 1)), rg = c.createRadialGradient(cx, cy, 0, cx, cy, 300 + R * 0.4);
611
+ rg.addColorStop(0, `rgba(255,200,110,${0.7 * gl})`); rg.addColorStop(1, 'rgba(255,170,60,0)'); c.fillStyle = rg; c.fillRect(0, 0, W, H);
612
+ // 灯焰本身:一朵小火苗在光心跳
613
+ c.fillStyle = `rgba(255,240,190,${gl})`; c.beginPath(); c.ellipse(cx, cy, 14 * (1 + fl * 4), 34 * (1 + fl * 3), 0, 0, 7); c.fill(); c.restore();
614
+ },
615
+ // 皮影 → 新海诚:一道斜向的强光从左扫到右(漏光),光带身后是新画面;带横向拉丝、彩色光斑和飘起的光粒
616
+ flareSweep(c, A, B, p, o) {
617
+ const e = ease.inOut(p), X = lerp(-500, W + 700, e), k = 0.35;
618
+ c.drawImage(A, 0, 0);
619
+ c.save(); c.beginPath(); c.moveTo(-10, 0); c.lineTo(X, 0); c.lineTo(X - H * k, H); c.lineTo(-10, H); c.closePath(); c.clip(); c.drawImage(B, 0, 0); c.restore();
620
+ c.save(); c.globalCompositeOperation = 'lighter';
621
+ const band = (w, a, col) => { c.save(); c.translate(X - H * k / 2, H / 2); c.transform(1, 0, -k, 1, 0, 0); const g = c.createLinearGradient(-w, 0, w, 0); g.addColorStop(0, `rgba(${col},0)`); g.addColorStop(0.5, `rgba(${col},${a})`); g.addColorStop(1, `rgba(${col},0)`); c.fillStyle = g; c.fillRect(-w, -H, 2 * w, 2 * H); c.restore(); };
622
+ band(520, 0.55, '255,236,200'); band(160, 0.85, '255,255,250'); band(60, 0.6, '200,240,255');
623
+ const hz = c.createLinearGradient(0, 0, W, 0); hz.addColorStop(0, 'rgba(150,210,255,0)'); hz.addColorStop(clamp(X / W), `rgba(220,240,255,${0.7 * Math.sin(Math.PI * p)})`); hz.addColorStop(1, 'rgba(150,210,255,0)'); c.fillStyle = hz; c.fillRect(Math.max(0, X - 700), 300, 1400, 5);
624
+ const r = rng(77);
625
+ for (let i = 0; i < 40; i++) { const x = X - H * k * r() + (r() - 0.5) * 500, y = r() * H - p * 120 * r(), rr = 3 + r() * 14, a = (1 - Math.abs(x - X) / 700) * 0.6; if (a <= 0) continue;
626
+ const g = c.createRadialGradient(x, y, 0, x, y, rr); g.addColorStop(0, `rgba(255,252,235,${a})`); g.addColorStop(1, 'rgba(255,252,235,0)'); c.fillStyle = g; c.beginPath(); c.arc(x, y, rr, 0, 7); c.fill(); }
627
+ [[0.3, 40, '120,255,200'], [0.6, 24, '255,170,120'], [0.9, 60, '140,170,255']].forEach(([f, rr, col]) => { const x = lerp(X, W - X, f), y = lerp(300, 780, f); c.fillStyle = `rgba(${col},${0.18 * Math.sin(Math.PI * p)})`; c.beginPath(); for (let i = 0; i < 6; i++) { const a = i / 6 * Math.PI * 2; i ? c.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr) : c.moveTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr); } c.fill(); });
628
+ c.restore();
629
+ },
630
+ // 新海诚 → 蓝色时期:先把旧画的颜色「抽走」只剩蓝(color 混合),再由 6 道宽大的干笔从左到右刷过,笔下就是新画;笔头带鬃毛拖痕
631
+ blueBrush(c, A, B, p, o) {
632
+ c.drawImage(A, 0, 0);
633
+ c.save(); c.globalCompositeOperation = 'color'; c.fillStyle = `rgba(45,90,148,${Math.min(1, p * 2.2)})`; c.fillRect(0, 0, W, H); c.restore();
634
+ c.save(); c.globalCompositeOperation = 'multiply'; c.fillStyle = `rgba(90,120,170,${Math.min(0.6, p * 1.5)})`; c.fillRect(0, 0, W, H); c.restore();
635
+ const N = 6, bh = H / N + 90, r = rng(21);
636
+ for (let k = 0; k < N; k++) {
637
+ const s = ease.inOut(seg(p, 0.12 + k * 0.07, 0.62 + k * 0.07)), x1 = lerp(-120, W + 260, s); if (s <= 0) continue;
638
+ const y0 = k * (H / N) - 20 + (k % 2 ? 10 : -10), tilt = (k % 2 ? 1 : -1) * 18;
639
+ const path = new Path2D(); path.moveTo(-20, y0 + tilt); const n = 24;
640
+ for (let i = 0; i <= n; i++) { const x = lerp(-20, x1, i / n); path.lineTo(x, y0 + tilt * (1 - i / n) + Math.sin(i * 1.7 + k) * 6); }
641
+ path.quadraticCurveTo(x1 + 70, y0 + bh / 2, x1, y0 + bh);
642
+ for (let i = n; i >= 0; i--) { const x = lerp(-20, x1, i / n); path.lineTo(x, y0 + bh + tilt * (1 - i / n) + Math.cos(i * 1.3 + k) * 7); }
643
+ path.closePath();
644
+ c.save(); c.clip(path); c.drawImage(B, 0, 0);
645
+ // 鬃毛拖痕:笔头后 260px 内的深浅细线(颜料还没刷匀)
646
+ for (let i = 0; i < 26; i++) { const y = y0 + 6 + r() * (bh - 12), L = 80 + r() * 200; const g = c.createLinearGradient(x1 - L, 0, x1, 0); g.addColorStop(0, 'rgba(15,30,60,0)'); g.addColorStop(1, `rgba(${r() < .5 ? '15,30,60' : '170,195,220'},.5)`);
647
+ c.strokeStyle = g; c.lineWidth = 1 + r() * 3; c.beginPath(); c.moveTo(x1 - L, y); c.lineTo(x1 + 10, y + (r() - 0.5) * 4); c.stroke(); }
648
+ c.restore();
649
+ }
650
+ },
651
+ };
652
+
653
+ // D 组转场共用:老胶片的闪烁、竖划痕、灰尘(24fps 换一次)
654
+ const filmDirt = (c, t, amt = 1) => { // 24fps 换一次:闪烁、竖划痕、灰尘
655
+ const r = rng(Math.floor(t * 24) * 7919 + 3);
656
+ c.fillStyle = `rgba(255,248,230,${(0.03 + r() * 0.1) * amt})`; c.fillRect(0, 0, W, H);
657
+ for (let k = 0; k < 3; k++) { const x = r() * W; c.strokeStyle = `rgba(${r() < .5 ? '255,250,235' : '20,16,10'},${0.5 * amt})`; c.lineWidth = 1 + r() * 2; c.beginPath(); c.moveTo(x, 0); c.lineTo(x + (r() - .5) * 10, H); c.stroke(); }
658
+ for (let k = 0; k < 12; k++) { c.fillStyle = `rgba(20,16,10,${0.6 * amt})`; c.beginPath(); c.arc(r() * W, r() * H, 1 + r() * 3, 0, 7); c.fill(); }
659
+ };
660
+
661
+ // ---- B:构成主义 / 达利 / 霍珀 / 吉卜力 / 蒸汽波 ----
662
+ // ① 构成主义 → 达利:新画面像颜料一样从上往下「流」下来——下缘 edge(x) 是整体下落线 + 一排圆头液滴
663
+ // (剖面 (1-(dx/w)²)^0.3:长条+圆头,往下挂);旧画面被压着逐列下垂(融化);液面一道高光+一道暗边。
664
+ T.melt = function (c, A, B, p) {
665
+ const r = rng(41), drips = [];
666
+ for (let i = 0; i < 26; i++) drips.push([r() * W, 10 + r() * 22, 60 + r() * 220, r() * 0.25]);
667
+ const e = ease.inOut(p), fall = lerp(-160, H + 40, e);
668
+ const edge = x => { let d = 0; for (const [dx, w, h, t0] of drips) { const u = (x - dx) / w; if (u > -1 && u < 1) d = Math.max(d, h * seg(p, t0, t0 + 0.5) * Math.pow(1 - u * u, 0.3)); } return fall + d + Math.sin(x * 0.005 + 1) * 24; };
669
+ // 旧画面逐列下垂(越靠近液面压得越多)
670
+ const sw = 4;
671
+ for (let x = 0; x < W; x += sw) { const ed = edge(x), sag = Math.max(0, ed) * 0.22 * e; c.drawImage(A, x, 0, sw, H, x, sag, sw, H + sag * 0.3); }
672
+ // 新画面:液面以上
673
+ c.save(); c.beginPath(); c.moveTo(0, -10); for (let x = 0; x <= W; x += 4) c.lineTo(x, edge(x)); c.lineTo(W, -10); c.closePath(); c.clip(); c.drawImage(B, 0, 0); c.restore();
674
+ // 液面:内侧暗边(厚度感)+高光
675
+ c.save(); c.beginPath(); for (let x = 0; x <= W; x += 4) { const y = edge(x); x ? c.lineTo(x, y) : c.moveTo(x, y); }
676
+ c.strokeStyle = 'rgba(40,20,10,.35)'; c.lineWidth = 9; c.filter = 'blur(3px)'; c.stroke(); c.filter = 'none';
677
+ c.translate(0, -5); c.strokeStyle = 'rgba(255,250,232,.7)'; c.lineWidth = 3; c.stroke(); c.restore();
678
+ };
679
+
680
+ // ② 达利 → 霍珀:一道硬边斜光块从左侧切进来——光块里是新画面(带暖光),光块外的旧画面逐渐沉进冷绿阴影;
681
+ // 光块里有一根窗棂竖影跟着走。最后暖光褪去,只剩新画面。
682
+ T.sunShaft = function (c, A, B, p) {
683
+ const e = ease.out(p) * 0.4 + ease.inOut(p) * 0.6, slant = 260;
684
+ const xR = lerp(-slant, W + slant + 60, e), xL = lerp(-slant - 20, -slant - 900, e) ; // 右缘扫过全屏,左缘留在左外
685
+ const band = () => { c.beginPath(); c.moveTo(xL, 0); c.lineTo(xR, 0); c.lineTo(xR - slant, H); c.lineTo(xL - slant, H); c.closePath(); };
686
+ c.drawImage(A, 0, 0);
687
+ c.save(); c.globalCompositeOperation = 'multiply'; c.fillStyle = `rgba(70,120,110,${0.7 * Math.min(1, p * 2)})`; c.fillRect(0, 0, W, H); c.restore();
688
+ c.save(); band(); c.clip(); c.drawImage(B, 0, 0);
689
+ const warm = 0.45 * (1 - seg(p, 0.6, 1));
690
+ c.globalCompositeOperation = 'screen'; c.fillStyle = `rgba(255,214,140,${warm})`; c.fillRect(0, 0, W, H);
691
+ c.globalCompositeOperation = 'multiply'; c.fillStyle = `rgba(80,120,110,${0.8 * (1 - seg(p, 0.7, 1))})`;
692
+ const mx = xR - 420; c.beginPath(); c.moveTo(mx, 0); c.lineTo(mx + 34, 0); c.lineTo(mx + 34 - slant, H); c.lineTo(mx - slant, H); c.closePath(); c.fill();
693
+ c.restore();
694
+ // 光块前缘一道亮边
695
+ c.save(); c.strokeStyle = `rgba(255,236,190,${0.8 * (1 - seg(p, 0.8, 1))})`; c.lineWidth = 6; c.beginPath(); c.moveTo(xR, 0); c.lineTo(xR - slant, H); c.stroke(); c.restore();
696
+ };
697
+
698
+ // ③ 霍珀 → 吉卜力:水彩在湿纸上晕开。几团色晕从窗口先后绽开、边界是噪声扰动的圆,
699
+ // 边缘一圈颜料沉积的深色水痕+外侧一圈被水冲淡的亮晕;色晕里是新画面。
700
+ // 水痕/亮晕的 α 跟着「这一团的扩张进度」淡出,否则长大后的水痕线会横穿新画面(第一版踩到)。
701
+ T.bleed = function (c, A, B, p, o) {
702
+ const cx = o.cx || sx(580), cy = o.cy || sy(330);
703
+ const blobs = [[cx, cy, 0, 1.0], [1300, 420, 0.14, 0.75], [520, 880, 0.24, 0.6], [1650, 860, 0.3, 0.6], [1050, 100, 0.34, 0.55]];
704
+ const st = blobs.map(([x, y, t0, k]) => ({ x, y, q: ease.inOut(seg(p, t0, t0 + 0.66)), k }));
705
+ const one = (i) => { const b = st[i], R = b.q * 1400 * b.k; if (R <= 0) return;
706
+ for (let j = 0; j <= 90; j++) { const a = j / 90 * Math.PI * 2, n = PAINT.fbm(Math.cos(a) * 1.6 + i * 7, Math.sin(a) * 1.6 + p * 0.8, 3);
707
+ const rr = R * (1 + 0.28 * n + 0.05 * Math.sin(a * 9 + i)); const X = b.x + Math.cos(a) * rr, Y = b.y + Math.sin(a) * rr; j ? c.lineTo(X, Y) : c.moveTo(X, Y); }
708
+ c.closePath(); };
709
+ c.drawImage(A, 0, 0);
710
+ // 水痕只在刚绽开时有(q<0.55 内淡到 0)——审片抓到长大后的残圈横穿画面
711
+ st.forEach((b, i) => { if (b.q <= 0 || b.q >= 1) return; c.save(); c.beginPath(); one(i); c.strokeStyle = `rgba(255,250,236,${0.6 * (1 - b.q)})`; c.lineWidth = 70; c.filter = 'blur(18px)'; c.stroke(); c.restore(); });
712
+ c.save(); c.beginPath(); st.forEach((b, i) => one(i)); c.clip(); c.drawImage(B, 0, 0); c.restore();
713
+ st.forEach((b, i) => { if (b.q <= 0 || b.q >= 0.55) return; c.save(); c.beginPath(); one(i); c.globalCompositeOperation = 'multiply'; c.strokeStyle = `rgba(150,110,70,${0.6 * Math.pow(1 - b.q / 0.55, 2)})`; c.lineWidth = 5; c.filter = 'blur(1.2px)'; c.stroke(); c.restore(); });
714
+ };
715
+
716
+ // ④ 吉卜力 → 蒸汽波:VHS 磁带故障。前半旧画面被横向撕裂+RGB 分离,中段一下「场同步丢失」整幅上滚+黑条,
717
+ // 后半新画面带着逐渐收敛的撕裂稳定下来。
718
+ T.vhs = function (c, A, B, p) {
719
+ const f = Math.floor(p * 18), r = rng(900 + f), amp = Math.sin(Math.PI * p);
720
+ const src = p < 0.5 ? A : B;
721
+ const roll = p > 0.38 && p < 0.62 ? (seg(p, 0.38, 0.62) * H * 0.8) | 0 : 0;
722
+ c.fillStyle = '#000'; c.fillRect(0, 0, W, H);
723
+ if (roll) { c.drawImage(src, 0, roll); c.drawImage(src, 0, roll - H - 40); c.fillStyle = '#05010a'; c.fillRect(0, roll - 40, W, 40); }
724
+ else c.drawImage(src, 0, 0);
725
+ // 撕裂带(从 A 或 B 里取,横向错位)
726
+ let y = 0; while (y < H) { const h = 6 + r() * 48, use = r() < p ? B : A;
727
+ if (r() < 0.55 * amp + 0.08) { const dx = (r() - 0.5) * 260 * amp; c.drawImage(use, 0, y, W, h, dx, y, W, h); }
728
+ y += h; }
729
+ // RGB 分离:洋红/青两份半透明错位
730
+ c.save(); c.globalCompositeOperation = 'screen'; c.globalAlpha = 0.45 * amp;
731
+ const T2 = PAINT.scratch('vhsT'), tg = T2.getContext('2d');
732
+ [['#ff00ff', 14 * amp], ['#00ffff', -14 * amp]].forEach(([col, dx]) => { tg.globalCompositeOperation = 'source-over'; tg.clearRect(0, 0, W, H); tg.drawImage(src, 0, 0); tg.globalCompositeOperation = 'multiply'; tg.fillStyle = col; tg.fillRect(0, 0, W, H); c.drawImage(T2, dx, 0); });
733
+ c.restore();
734
+ // 雪花线
735
+ c.save(); c.globalAlpha = 0.7 * amp; for (let i = 0; i < 160; i++) { c.fillStyle = r() < 0.5 ? '#fff' : '#9a9a9a'; c.fillRect(r() * W, r() * H, 30 + r() * 200, 2); } c.restore();
736
+ };
737
+
738
+
739
+ // ---- 样片补的 4 个:A–D 每组第一段在片头没有进入转场,这里按「进入段的签名语言」补上 ----
740
+ Object.assign(T, {
741
+ // 进入水墨:旧画先被「洗」成宣纸(去色、提亮、减对比),几团墨点从落笔处先后洇开,边缘带一圈墨晕,新画在墨里出现
742
+ inkBloom(c, A, B, p, o) {
743
+ const cx = o.cx || sx(560), cy = o.cy || sy(740);
744
+ c.save(); c.filter = `grayscale(${seg(p, 0, 0.5)}) brightness(${1 + 0.35 * seg(p, 0, 0.6)}) contrast(${1 - 0.3 * seg(p, 0, 0.6)})`; c.drawImage(A, 0, 0); c.restore();
745
+ const blobs = once('inkBloom', () => { const r = rng(17), out = [[cx, cy, 0, 1.0]]; for (let i = 0; i < 6; i++) out.push([200 + r() * 1520, 120 + r() * 840, 0.08 + r() * 0.3, 0.45 + r() * 0.4]); return out; });
746
+ const outline = (g, x, y, R) => { g.beginPath(); for (let j = 0; j <= 72; j++) { const a = j / 72 * TAU, n = PAINT.fbm(Math.cos(a) * 1.8 + x * 0.01, Math.sin(a) * 1.8 + y * 0.01, 3), rr = R * (1 + 0.35 * n); j ? g.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr) : g.moveTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr); } g.closePath(); };
747
+ const st = blobs.map(([x, y, t0, k]) => { const q = ease.out(seg(p, t0, t0 + 0.7)); return [x, y, q, q * 1500 * k]; });
748
+ // 遮罩与墨边都在 1/4 分辨率上画再放大(边缘本来就是软的;全分辨率 blur 要 ~170ms)
749
+ const sm = once('inkBloomSm', () => [mkc(W / 4, H / 4), mkc(W / 4, H / 4)]), [mk, rim] = sm, mg = mk.getContext('2d'), rg = rim.getContext('2d');
750
+ mg.reset(); mg.scale(0.25, 0.25); mg.filter = 'blur(3px)'; mg.fillStyle = '#000';
751
+ st.forEach(([x, y, q, R]) => { if (q > 0) { outline(mg, x, y, R); mg.fill(); } });
752
+ rg.reset(); rg.scale(0.25, 0.25); rg.filter = 'blur(2.5px)'; rg.lineWidth = 34; // 墨晕边:刚洇开时浓,长大后淡出(否则残圈横穿新画面)
753
+ st.forEach(([x, y, q, R]) => { if (q <= 0 || q >= 1) return; rg.strokeStyle = `rgba(30,26,22,${0.55 * Math.pow(1 - q, 1.5)})`; outline(rg, x, y, R); rg.stroke(); });
754
+ c.save(); c.globalCompositeOperation = 'multiply'; c.drawImage(rim, 0, 0, W, H); c.restore();
755
+ const T2 = o.tmp, g = T2.getContext('2d'); g.reset(); g.drawImage(B, 0, 0); g.globalCompositeOperation = 'destination-in'; g.drawImage(mk, 0, 0, W, H);
756
+ c.drawImage(T2, 0, 0);
757
+ if (p > 0.8) { c.globalAlpha = seg(p, 0.8, 1); c.drawImage(B, 0, 0); c.globalAlpha = 1; }
758
+ },
759
+ // 进入构成主义:李西茨基「红楔打白圈」——一枚红楔从左下斜刺过画面,楔尖后面是新画面;三根黑排版条跟着楔子冲进来
760
+ redWedge(c, A, B, p, o) {
761
+ const e = ease.inOut(p), ang = -0.32, ca = Math.cos(ang), sa = Math.sin(ang);
762
+ const X = lerp(-700, W + 900, e); // 楔尖沿斜轴的位置
763
+ const mx = W / 2, my = H / 2, tf = (u, v) => [mx + (u - mx) * ca - (v - my) * sa, my + (u - mx) * sa + (v - my) * ca];
764
+ c.drawImage(A, 0, 0);
765
+ c.save(); c.beginPath(); [tf(-3000, -3000), tf(X - 420, -3000), tf(X - 420, 4000), tf(-3000, 4000)].forEach((q, i) => i ? c.lineTo(...q) : c.moveTo(...q)); c.closePath(); c.clip(); c.drawImage(B, 0, 0); c.restore();
766
+ c.save(); c.fillStyle = '#c8231c'; c.beginPath(); [tf(X, my), tf(X - 520, my - 330), tf(X - 520, my + 330)].forEach((q, i) => i ? c.lineTo(...q) : c.moveTo(...q)); c.closePath(); c.fill();
767
+ c.fillStyle = '#151311'; [[-150, 26, 0], [120, 18, 0.12], [260, 34, 0.2]].forEach(([dv, h, lag]) => { const x1 = lerp(-700, W + 900, ease.inOut(clamp(p - lag))) - 560;
768
+ c.beginPath(); [tf(-2000, my + dv), tf(x1, my + dv), tf(x1, my + dv + h), tf(-2000, my + dv + h)].forEach((q, i) => i ? c.lineTo(...q) : c.moveTo(...q)); c.closePath(); c.fill(); });
769
+ c.restore();
770
+ },
771
+ // 进入 Kirby 漫画:新画面按漫画分格一格格「砸」进来(从 1.18 倍落位、黑格线),没换到的旧画面先被盖上一层本戴网点
772
+ comicPanels(c, A, B, p, o) {
773
+ const cells = once('comicPanels', () => [[0, 0, 820, 520, 0], [820, 0, 1100, 520, 0.12], [0, 520, 640, 560, 0.24], [640, 520, 700, 560, 0.34], [1340, 520, 580, 560, 0.44]].map(([x, y, w, h, t0]) => [sx(x), sy(y), sx(w), sy(h), t0]));
774
+ c.drawImage(A, 0, 0);
775
+ c.save(); c.globalAlpha = Math.min(1, p * 2.5) * 0.85; c.fillStyle = PAINT.dotPattern(c, 'comicTr', 12, 3.4, '#e0287a', null, 75); c.fillRect(0, 0, W, H); c.restore();
776
+ for (const [x, y, w, h, t0] of cells) {
777
+ const q = seg(p, t0, t0 + 0.32); if (q <= 0) continue; const s = 1.18 - 0.18 * ease.outBack(q);
778
+ c.save(); c.beginPath(); c.rect(x, y, w, h); c.clip();
779
+ c.translate(x + w / 2, y + h / 2); c.scale(s, s); c.translate(-x - w / 2, -y - h / 2); c.drawImage(B, 0, 0); c.restore();
780
+ c.save(); c.strokeStyle = '#141212'; c.lineWidth = 14 * (1 - seg(p, 0.8, 1)); if (c.lineWidth > 0.5) c.strokeRect(x, y, w, h); c.restore();
781
+ }
782
+ },
783
+ // 进入伦勃朗:旧画沉进暗褐(暗角从四周合拢),然后一道从窗口斜下来的光柱把新画面照出来,最后暗部也慢慢显影
784
+ chiaroscuro(c, A, B, p, o) {
785
+ const lx = o.cx || sx(580), ly = o.cy || sy(300);
786
+ c.drawImage(A, 0, 0);
787
+ c.save(); c.globalCompositeOperation = 'multiply'; const d = seg(p, 0, 0.45);
788
+ const vg = c.createRadialGradient(W / 2, H / 2, lerp(1100, 80, d), W / 2, H / 2, lerp(1400, 700, d)); vg.addColorStop(0, 'rgba(255,255,255,1)'); vg.addColorStop(1, `rgba(28,18,10,${0.9 * d + 0.1})`);
789
+ c.fillStyle = vg; c.fillRect(0, 0, W, H); c.fillStyle = `rgba(40,26,14,${0.85 * d})`; c.fillRect(0, 0, W, H); c.restore();
790
+ const q = ease.inOut(seg(p, 0.25, 0.9)); if (q <= 0) return;
791
+ const T2 = o.tmp, g = T2.getContext('2d'); g.reset(); g.drawImage(B, 0, 0); g.globalCompositeOperation = 'destination-in'; g.filter = 'blur(40px)'; g.fillStyle = '#000';
792
+ const spread = lerp(60, 1400, q); g.beginPath(); g.moveTo(lx - 180, ly - 200); g.lineTo(lx + 180, ly - 200); g.lineTo(lx + 500 + spread, ly + 900); g.lineTo(lx - 200 - spread * 0.6, ly + 900); g.closePath(); g.fill();
793
+ g.beginPath(); g.arc(lx + 420, ly + 260, lerp(40, 1500, q), 0, TAU); g.fill(); g.filter = 'none';
794
+ c.drawImage(T2, 0, 0);
795
+ c.save(); c.globalCompositeOperation = 'lighter'; c.globalAlpha = 0.35 * Math.sin(Math.PI * q); c.fillStyle = 'rgba(255,214,150,1)'; c.beginPath(); c.moveTo(lx - 140, ly - 160); c.lineTo(lx + 140, ly - 160); c.lineTo(lx + 420 + spread * 0.5, ly + 900); c.lineTo(lx - 120 - spread * 0.3, ly + 900); c.closePath(); c.filter = 'blur(30px)'; c.fill(); c.restore();
796
+ if (p > 0.8) { c.globalAlpha = seg(p, 0.8, 1); c.drawImage(B, 0, 0); c.globalAlpha = 1; }
797
+ },
798
+ });
799
+
800
+ // ================= YouTube 解说语法的转场(references/09-视频动画语法.md) =================
801
+ // 解说片的转场原则和艺术速通相反:不抢戏、保持空间连续(观众知道「镜头去了哪」)——多数是相机运动和共享元素。
802
+ // 相机运动在场景里按时间算好(CAM),转场只负责「A、B 怎么合」。这些段一般写 punch: 0 或片级 window.PUNCH = 0(关掉拍点冲击)。
803
+ Object.assign(T, {
804
+ // 【同一世界】same:A、B 本来是同一幅画(同一世界画布、相机连续运动),直接用 B。Vox 桌面快摇、白板平移都走它。
805
+ same(c, A, B) { c.drawImage(B, 0, 0); },
806
+ // 【Kurzgesagt】lensReveal 钻进镜头:A 里相机正指数推进某个圆(眼睛/镜头),B 只露在这个圆里,圆随推进长大到盖满屏,圆边一圈光环。
807
+ // o.lens(t) → {x, y, r} 屏幕上的圆(由上一镜的相机算,必须和 A 的推进同步);o.ring 光环色。圆要盖满屏:推进倍数 ≥ CAM.coverZoom(r)。
808
+ lensReveal(c, A, B, p, o) {
809
+ const L = o.lens(o.t);
810
+ c.drawImage(A, 0, 0);
811
+ if (L.r < 1) return;
812
+ c.save(); c.beginPath(); c.arc(L.x, L.y, L.r, 0, TAU); c.clip(); c.drawImage(B, 0, 0); c.restore();
813
+ const ring = Math.max(3, L.r * 0.06), col = o.ring || [120, 240, 255], col2 = o.ring2 || [70, 230, 255];
814
+ c.save(); c.strokeStyle = `rgba(${col},.85)`; c.lineWidth = ring; c.beginPath(); c.arc(L.x, L.y, L.r + ring / 2, 0, TAU); c.stroke();
815
+ c.strokeStyle = `rgba(${col2},.25)`; c.lineWidth = ring * 3; c.beginPath(); c.arc(L.x, L.y, L.r + ring * 2, 0, TAU); c.stroke(); c.restore();
816
+ },
817
+ // 【Kurzgesagt】matchCut 填满再拉出:A 推进到某个色块满屏,B 从同色的东西里拉出;只在 o.at(默认中点)±o.width/2 内交叉溶解(±3 帧),两边颜色本来一样,剪点看不见。
818
+ matchCut(c, A, B, p, o) {
819
+ const w = o.width || 0.06, q = clamp((p - (o.at ?? 0.5) + w / 2) / w);
820
+ if (q <= 0) { c.drawImage(A, 0, 0); return; }
821
+ if (q >= 1) { c.drawImage(B, 0, 0); return; }
822
+ c.drawImage(A, 0, 0); c.globalAlpha = q; c.drawImage(B, 0, 0); c.globalAlpha = 1;
823
+ },
824
+ // 【storytime】whip 甩镜:旧画面加速甩出、新画面减速甩入,中段整屏横向运动模糊(采样 ≤8px 一张,少了读成残影)。o.dir=1 往左甩
825
+ whip(c, A, B, p, o) {
826
+ const e = MO.expoInOut(p), dir = o.dir || 1, off = e * W * dir;
827
+ const v = Math.max(0, 1 - Math.abs(p - 0.5) * 2.2);
828
+ const n = 1 + Math.round(v * 40), spread = v * 320;
829
+ for (let k = 0; k < n; k++) {
830
+ const d = n === 1 ? 0 : (k / (n - 1) - 0.5) * spread * dir;
831
+ c.globalAlpha = 1 / (k + 1) > 0.999 ? 1 : 1 / (k + 1);
832
+ c.drawImage(A, -off + d, 0); c.drawImage(B, W * dir - off + d, 0);
833
+ }
834
+ c.globalAlpha = 1;
835
+ },
836
+ // 【storytime】smash 砸镜:第一帧就是新画面,带 1.22 倍放大从猛到停(弹簧,夹在 ≥1 防黑边),首两帧白闪。反应特写用
837
+ smash(c, A, B, p, o) {
838
+ const lt = p * o.dur, s = Math.max(1, 1 + 0.22 * (1 - MO.springHz(lt, o.freq || 3, o.decay || 11)));
839
+ c.save(); c.translate(W / 2, H / 2); c.scale(s, s); c.translate(-W / 2, -H / 2); c.drawImage(B, 0, 0); c.restore();
840
+ if (lt < 2 / 60) { c.fillStyle = 'rgba(255,255,255,0.55)'; c.fillRect(0, 0, W, H); }
841
+ },
842
+ // 【动态文字】fillZoom 冲进色块:镜头冲进旧画面里某个形(o.cx,o.cy;形的颜色 o.fill = 新画面底色),冲满屏后新画面从 1.25 落回 1。
843
+ // 揭开点 = o.k(默认 .55):卡拍时段起点 = 小节线 − k×dur(MO.onBeat)。o.maxZoom 冲进倍数(指数缩放本身就是加速,别再套 expoIn)
844
+ fillZoom(c, A, B, p, o) {
845
+ const k = o.k || 0.55;
846
+ if (p < k) { const q = Math.pow(p / k, 2.2), s = Math.pow(o.maxZoom || 40, q);
847
+ c.save(); c.translate(o.cx, o.cy); c.scale(s, s); c.translate(-o.cx, -o.cy); c.drawImage(A, 0, 0); c.restore(); }
848
+ else { const q = MO.expoOut((p - k) / (1 - k)), s = lerp(1.25, 1, q);
849
+ c.fillStyle = o.fill || '#000'; c.fillRect(0, 0, W, H);
850
+ c.save(); c.globalAlpha = clamp(q * 3); c.translate(W / 2, H / 2); c.scale(s, s); c.translate(-W / 2, -H / 2); c.drawImage(B, 0, 0); c.restore(); }
851
+ },
852
+ // 【动态文字】bands 色带擦除:o.colors 几条斜切色带接力扫过(间隔 o.gap),最后一条后面是新画面。揭开点 ≈ .6
853
+ bands(c, A, B, p, o) {
854
+ const cols = o.colors || ['#FFD23F', '#FF5A36', '#14213D'], n = cols.length, sk = H * 0.35, gap = o.gap || 0.13;
855
+ const edge = q => lerp(-sk - 40, W + sk + 40, MO.expoInOut(clamp(q)));
856
+ c.drawImage(A, 0, 0);
857
+ const clipTo = x => { c.beginPath(); c.moveTo(-sk - 60, 0); c.lineTo(x + sk, 0); c.lineTo(x, H); c.lineTo(-sk - 60, H); c.closePath(); };
858
+ for (let i = 0; i < n; i++) { const x = edge((p - i * gap) / (1 - (n - 1) * gap)); c.save(); clipTo(x); c.clip(); c.fillStyle = cols[i]; c.fillRect(0, 0, W, H); c.restore(); }
859
+ const xb = edge((p - n * gap) / (1 - (n - 1) * gap));
860
+ c.save(); clipTo(xb); c.clip(); c.drawImage(B, 0, 0); c.restore();
861
+ },
862
+ // 【动态文字】push 推页:新旧一起平移(expoInOut),像镜头在长画布上滑到下一块。o.dir: 'left'(默认)| 'up'。到 90% 位移在 p≈.58
863
+ push(c, A, B, p, o) {
864
+ const e = MO.expoInOut(p);
865
+ if (o.dir === 'up') { c.drawImage(A, 0, -e * H); c.drawImage(B, 0, H - e * H); }
866
+ else { c.drawImage(A, -e * W, 0); c.drawImage(B, W - e * W, 0); }
867
+ },
868
+ // 【3b1b / 财经】zoomThrough 镜头穿越:相机推向 (cx,cy),新画面从该物体「里面」长出来。
869
+ // o: {cx, cy, zoom, r0=物体半径(圆形开口;0=不裁), live, bg, focus:[x,y] 新画面主体位置, bScale0, ease}
870
+ // live:true = 旧镜头自己在转场里继续推相机(矢量重画,线条不糊;推荐);否则把 A 当位图放大(10× 以上会糊)。
871
+ // 有 r0 时 zoom 要 ≥ CAM.coverZoom(r0, cx, cy)(≈1100/r0),否则开口到结束都盖不满屏,最后一帧会跳。
872
+ zoomThrough(c, A, B, p, o) {
873
+ const cx = o.cx, cy = o.cy, Z = o.zoom || 7, e = (o.ease || MO.smooth)(p);
874
+ const sA = Math.pow(Z, e);
875
+ c.fillStyle = o.bg || '#000'; c.fillRect(0, 0, W, H);
876
+ c.save(); if (!o.live) { c.translate(cx, cy); c.scale(sA, sA); c.translate(-cx, -cy); }
877
+ c.globalAlpha = 1 - (o.bScale0 ? seg(p, 0.5, 0.8) : o.r0 ? seg(p, 0.45, 0.8) : seg(p, 0.6, 0.95)); // 开口外的旧画面早点退,新镜头标题出来时不挂残影
878
+ c.drawImage(A, 0, 0); c.restore();
879
+ c.save();
880
+ if (o.r0) {
881
+ const R = o.r0 * sA; c.beginPath(); c.arc(cx, cy, R, 0, TAU); c.clip();
882
+ c.fillStyle = o.bg || '#000'; c.globalAlpha = seg(p, 0.18, 0.45); c.fillRect(0, 0, W, H); // 穿过物体表面:亮面渐暗成内部
883
+ }
884
+ // 新画面从「1/Z 大小、主体 focus 对准 (cx,cy)」走到原位全屏;没有圆形开口时用 bScale0(如 0.6)从较大尺寸起、晚一点溶进来,免得像浮着的小卡片
885
+ const [fx, fy] = o.focus || [W / 2, H / 2], sB = o.bScale0 ? Math.pow(o.bScale0, 1 - e) : Math.pow(Z, e - 1);
886
+ c.globalAlpha = o.r0 ? seg(p, 0.22, 0.55) : o.bScale0 ? seg(p, 0.45, 0.85) : seg(p, 0.25, 0.7);
887
+ const m = MO.smooth(seg(e, 0.55, 1)); // 开口没盖满屏前,主体一直待在开口正中;最后才归位
888
+ c.translate(lerp(cx, fx, m), lerp(cy, fy, m)); c.scale(sB, sB); c.translate(-fx, -fy); c.drawImage(B, 0, 0);
889
+ c.restore();
890
+ },
891
+ // 【发布会】blurPush 模糊推进:旧画面后退(0.92)+模糊+变暗,新画面从 1.06 带模糊落到 1.0 清晰;底下垫一张放大重模糊的旧画面防黑边
892
+ blurPush(c, A, B, p) {
893
+ const e = MO.appleOut(p);
894
+ c.save(); c.filter = 'blur(30px)'; c.translate(W / 2, H / 2); c.scale(1.08, 1.08); c.translate(-W / 2, -H / 2); c.drawImage(A, 0, 0); c.restore();
895
+ const draw = (img, s, blur, a) => { if (a <= 0) return; c.save(); c.globalAlpha = a; if (blur > 0.3) c.filter = `blur(${blur.toFixed(1)}px)`;
896
+ c.translate(W / 2, H / 2); c.scale(s, s); c.translate(-W / 2, -H / 2); c.drawImage(img, 0, 0); c.restore(); };
897
+ draw(A, lerp(1, 0.92, e), 24 * e, 1 - seg(p, 0.2, 0.75));
898
+ draw(B, lerp(1.06, 1, e), 24 * (1 - e), seg(p, 0.15, 0.7));
899
+ },
900
+ // 【发布会】expandRect 卡片展开成全屏(共享元素):o.rect={x,y,w,h} 是卡片在上一镜末帧的屏幕矩形、o.r 圆角;弹簧展开,里面装新画面,旧画面后退变暗。
901
+ // 新画面第一帧就不透明地装进卡片(卡内硬切被展开运动盖住):卡片半透明时任何溶接都会看到新旧两层字叠在一起。
902
+ expandRect(c, A, B, p, o) {
903
+ const e = MO.spring(p * (o.dur || 0.6), { duration: 0.55, bounce: 0 });
904
+ const R = o.rect, x = lerp(R.x, 0, e), y = lerp(R.y, 0, e), w = lerp(R.w, W, e), h = lerp(R.h, H, e), r = lerp(o.r || 28, 0, e);
905
+ c.save(); c.filter = 'blur(30px)'; c.translate(W / 2, H / 2); c.scale(1.08, 1.08); c.translate(-W / 2, -H / 2); c.drawImage(A, 0, 0); c.restore();
906
+ c.save(); c.globalAlpha = 1 - 0.6 * e;
907
+ const s = lerp(1, 0.94, e); c.translate(W / 2, H / 2); c.scale(s, s); c.translate(-W / 2, -H / 2); c.drawImage(A, 0, 0); c.restore();
908
+ c.save(); c.beginPath(); c.roundRect(x, y, w, h, r); c.clip();
909
+ const k = Math.max(w / W, h / H); c.translate(x + w / 2, y + h / 2); c.scale(k, k); c.translate(-W / 2, -H / 2);
910
+ c.drawImage(B, 0, 0); c.restore();
911
+ },
912
+ // 【财经】slidePush 面板横推:新面板从右推入、旧面板向左推出(bezier .7,0,.2,1),一道品牌色竖条 o.bar 领路
913
+ slidePush(c, A, B, p, o) {
914
+ const e = MO.bezier(0.7, 0, 0.2, 1)(p), dx = -W * e;
915
+ c.drawImage(A, dx, 0); c.drawImage(B, dx + W, 0);
916
+ if (o.bar) { c.fillStyle = o.bar; c.fillRect(dx + W - 14, 0, 14, H); }
917
+ },
918
+ // 【3b1b】fadeShift 淡出上移 / 淡入上移(manim FadeOut(shift=UP) + FadeIn(shift=UP)),o.shift 位移像素
919
+ fadeShift(c, A, B, p, o) {
920
+ const d = o.shift || 60, a = MO.smooth(seg(p, 0, 0.6)), b = MO.smooth(seg(p, 0.4, 1));
921
+ c.fillStyle = o.bg || '#000'; c.fillRect(0, 0, W, H);
922
+ c.save(); c.globalAlpha = 1 - a; c.drawImage(A, 0, -d * a); c.restore();
923
+ c.save(); c.globalAlpha = b; c.drawImage(B, 0, d * (1 - b)); c.restore();
924
+ },
925
+ });
926
+
927
+ // 项目自己的转场(transitions/<名>.js)可以复用这几个工具
928
+ window.TR_UTIL = { once, seg, mkc };
929
+ })();