@biunow/agent-biuart 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/server.mjs +575 -0
- package/data/styles.json +533 -0
- package/engine/audit_clip.py +46 -0
- package/engine/clip.html +24 -0
- package/engine/clip.js +104 -0
- package/engine/clip_schema.json +19 -0
- package/engine/clips/t1_3b1b.js +203 -0
- package/engine/clips/t2_keynote_ui.js +347 -0
- package/engine/clips/t3_finance_chart.js +206 -0
- package/engine/clips/y1_kurzgesagt.js +183 -0
- package/engine/clips/y2_vox.js +150 -0
- package/engine/clips/y3_whiteboard.js +209 -0
- package/engine/clips/y4_storytime.js +194 -0
- package/engine/clips/y5_kinetic_type.js +134 -0
- package/engine/compare.py +19 -0
- package/engine/demos/_shared/hero/c1_0.png +0 -0
- package/engine/demos/_shared/hero/c1_1.png +0 -0
- package/engine/demos/_shared/hero/c1_2.png +0 -0
- package/engine/demos/_shared/hero/c3_line.png +0 -0
- package/engine/demos/_shared/hero.js +96 -0
- package/engine/demos/long_scroll/eras.js +23 -0
- package/engine/demos/long_scroll/frames/8bit_act/01.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_act/02.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_act/03.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_act/04.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_act/05.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_act/06.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_act/07.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_act/08.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_act/meta.json +69 -0
- package/engine/demos/long_scroll/frames/8bit_walk/01.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_walk/02.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_walk/03.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_walk/04.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_walk/05.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_walk/06.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_walk/07.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_walk/08.png +0 -0
- package/engine/demos/long_scroll/frames/8bit_walk/meta.json +69 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/01.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/02.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/03.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/04.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/05.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/06.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/07.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/08.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_dodge/meta.json +69 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/01.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/02.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/03.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/04.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/05.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/06.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/07.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/08.png +0 -0
- package/engine/demos/long_scroll/frames/egypt_walk8/meta.json +69 -0
- package/engine/demos/long_scroll/frames/monet_throw/01.png +0 -0
- package/engine/demos/long_scroll/frames/monet_throw/02.png +0 -0
- package/engine/demos/long_scroll/frames/monet_throw/03.png +0 -0
- package/engine/demos/long_scroll/frames/monet_throw/04.png +0 -0
- package/engine/demos/long_scroll/frames/monet_throw/05.png +0 -0
- package/engine/demos/long_scroll/frames/monet_throw/06.png +0 -0
- package/engine/demos/long_scroll/frames/monet_throw/07.png +0 -0
- package/engine/demos/long_scroll/frames/monet_throw/08.png +0 -0
- package/engine/demos/long_scroll/frames/monet_throw/meta.json +71 -0
- package/engine/demos/long_scroll/frames/monet_walk/01.png +0 -0
- package/engine/demos/long_scroll/frames/monet_walk/02.png +0 -0
- package/engine/demos/long_scroll/frames/monet_walk/03.png +0 -0
- package/engine/demos/long_scroll/frames/monet_walk/04.png +0 -0
- package/engine/demos/long_scroll/frames/monet_walk/05.png +0 -0
- package/engine/demos/long_scroll/frames/monet_walk/06.png +0 -0
- package/engine/demos/long_scroll/frames/monet_walk/07.png +0 -0
- package/engine/demos/long_scroll/frames/monet_walk/08.png +0 -0
- package/engine/demos/long_scroll/frames/monet_walk/meta.json +71 -0
- package/engine/demos/long_scroll/lib/acts.js +87 -0
- package/engine/demos/long_scroll/lib/crossing.js +463 -0
- package/engine/demos/long_scroll/lib/world.js +143 -0
- package/engine/demos/long_scroll/lib/xing2_ding.js +42 -0
- package/engine/demos/long_scroll/lib/xing2_jia.js +114 -0
- package/engine/demos/long_scroll/lib/xing2_yi.js +133 -0
- package/engine/demos/long_scroll/segments/02_egypt.js +234 -0
- package/engine/demos/long_scroll/segments/14_8bit.js +118 -0
- package/engine/demos/long_scroll/segments/28_monet.js +86 -0
- package/engine/demos/t1_3b1b/eras.js +11 -0
- package/engine/demos/t1_3b1b/t1_s1.js +101 -0
- package/engine/demos/t1_3b1b/t1_s2.js +74 -0
- package/engine/demos/t1_3b1b/t1_s3.js +65 -0
- package/engine/demos/t2_keynote_ui/eras.js +10 -0
- package/engine/demos/t2_keynote_ui/t2_s1.js +57 -0
- package/engine/demos/t2_keynote_ui/t2_s2.js +70 -0
- package/engine/demos/t2_keynote_ui/t2_s3.js +61 -0
- package/engine/demos/t3_finance_chart/eras.js +11 -0
- package/engine/demos/t3_finance_chart/t3_s1.js +48 -0
- package/engine/demos/t3_finance_chart/t3_s2.js +42 -0
- package/engine/demos/t3_finance_chart/t3_s3.js +51 -0
- package/engine/demos/y1_kurzgesagt/eras.js +10 -0
- package/engine/demos/y1_kurzgesagt/kurzgesagt.js +347 -0
- package/engine/demos/y2_vox/eras.js +10 -0
- package/engine/demos/y2_vox/vox.js +240 -0
- package/engine/demos/y3_whiteboard/eras.js +10 -0
- package/engine/demos/y3_whiteboard/whiteboard.js +162 -0
- package/engine/demos/y4_storytime/eras.js +14 -0
- package/engine/demos/y4_storytime/s1_desk.js +18 -0
- package/engine/demos/y4_storytime/s2_screen.js +82 -0
- package/engine/demos/y4_storytime/s3_react.js +39 -0
- package/engine/demos/y4_storytime/s4_wide.js +16 -0
- package/engine/demos/y4_storytime/s5_close.js +18 -0
- package/engine/demos/y4_storytime/story.js +25 -0
- package/engine/demos/y4_storytime/vo_env.js +2 -0
- package/engine/demos/y4_storytime/world.js +73 -0
- package/engine/demos/y5_kinetic_type/eras.js +16 -0
- package/engine/demos/y5_kinetic_type/k1_title.js +38 -0
- package/engine/demos/y5_kinetic_type/k2_complete.js +55 -0
- package/engine/demos/y5_kinetic_type/k3_chat.js +47 -0
- package/engine/demos/y5_kinetic_type/k4_agent.js +49 -0
- package/engine/demos/y5_kinetic_type/kinetic.js +52 -0
- package/engine/engine.js +277 -0
- package/engine/eras.js +60 -0
- package/engine/eras_gallery.js +115 -0
- package/engine/eras_style_studies.js +20 -0
- package/engine/examples/assets/K/347/272/277/345/233/276_/346/250/252/345/261/217.png +0 -0
- 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
- package/engine/examples/assets//347/273/210/347/253/257_sum/346/261/202/345/222/214.png +0 -0
- package/engine/examples/assets//350/220/245/346/224/266/345/233/276_/347/253/226/345/261/217.png +0 -0
- 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
- package/engine/examples/t1_3b1b.json +19 -0
- package/engine/examples/t2_fullbleed_pulse.json +15 -0
- package/engine/examples/t2_keynote_ui.json +15 -0
- package/engine/examples/t3_finance_chart.json +29 -0
- package/engine/examples/y1_kurzgesagt.json +16 -0
- package/engine/examples/y2_vox.json +14 -0
- package/engine/examples/y3_whiteboard.json +17 -0
- package/engine/examples/y4_storytime.json +52 -0
- package/engine/examples/y5_kinetic_type.json +14 -0
- package/engine/index.html +42 -0
- package/engine/lib/THREE_LICENSE.txt +21 -0
- package/engine/lib/brush.js +149 -0
- package/engine/lib/camera.js +99 -0
- package/engine/lib/chart.js +152 -0
- package/engine/lib/collage.js +97 -0
- package/engine/lib/diagram.js +264 -0
- package/engine/lib/finish.js +70 -0
- package/engine/lib/fonts/AlfaSlabOne-400.woff +0 -0
- package/engine/lib/fonts/Anton-400.woff +0 -0
- package/engine/lib/fonts/ArchivoBlack-400.woff +0 -0
- package/engine/lib/fonts/Bangers-400.woff +0 -0
- package/engine/lib/fonts/Caveat-600.woff +0 -0
- package/engine/lib/fonts/Cinzel-400.woff +0 -0
- package/engine/lib/fonts/Cinzel-700.woff +0 -0
- package/engine/lib/fonts/CormorantGaramond-500.woff +0 -0
- package/engine/lib/fonts/CormorantGaramond-500i.woff +0 -0
- package/engine/lib/fonts/CormorantGaramond-700.woff +0 -0
- package/engine/lib/fonts/EBGaramond-400.woff +0 -0
- package/engine/lib/fonts/EBGaramond-400i.woff +0 -0
- package/engine/lib/fonts/EBGaramond-600.woff +0 -0
- package/engine/lib/fonts/Exo2-800i.woff +0 -0
- package/engine/lib/fonts/GochiHand-400.woff +0 -0
- package/engine/lib/fonts/IMFellEnglish-400.woff +0 -0
- package/engine/lib/fonts/IMFellEnglish-400i.woff +0 -0
- package/engine/lib/fonts/Inter-var.woff +0 -0
- package/engine/lib/fonts/JosefinSans-700.woff +0 -0
- package/engine/lib/fonts/Kalam-700.woff +0 -0
- package/engine/lib/fonts/LICENSES.md +40 -0
- package/engine/lib/fonts/LXGWWenKai-500.woff +0 -0
- package/engine/lib/fonts/LibreBaskerville-400.woff +0 -0
- package/engine/lib/fonts/LibreBaskerville-700.woff +0 -0
- package/engine/lib/fonts/LilitaOne-400.woff +0 -0
- package/engine/lib/fonts/Marcellus-400.woff +0 -0
- package/engine/lib/fonts/NotoSansSC-500.woff +0 -0
- package/engine/lib/fonts/NotoSansSC-700.woff +0 -0
- package/engine/lib/fonts/NotoSansSC-800.woff +0 -0
- package/engine/lib/fonts/NotoSansSC-900.woff +0 -0
- package/engine/lib/fonts/NotoSansSC-sub.woff +0 -0
- package/engine/lib/fonts/NotoSerifJP-600.woff +0 -0
- package/engine/lib/fonts/NotoSerifSC-sub.woff +0 -0
- package/engine/lib/fonts/OFL.txt +94 -0
- package/engine/lib/fonts/PlayfairDisplay-400i.woff +0 -0
- package/engine/lib/fonts/PlayfairDisplay-700.woff +0 -0
- package/engine/lib/fonts/PlayfairDisplay-700i.woff +0 -0
- package/engine/lib/fonts/Poppins-600.woff +0 -0
- package/engine/lib/fonts/Poppins-700.woff +0 -0
- package/engine/lib/fonts/Poppins-800.woff +0 -0
- package/engine/lib/fonts/PressStart2P-400.woff +0 -0
- package/engine/lib/fonts/Righteous-400.woff +0 -0
- package/engine/lib/fonts/RobotoCondensed-var.woff +0 -0
- package/engine/lib/fonts/Rye-400.woff +0 -0
- package/engine/lib/fonts/UnifrakturMaguntia-400.woff +0 -0
- package/engine/lib/fonts/cmunrm.woff +0 -0
- package/engine/lib/fonts/cmunti.woff +0 -0
- package/engine/lib/fonts/index.json +1 -0
- package/engine/lib/fonts.js +56 -0
- package/engine/lib/kit.js +91 -0
- package/engine/lib/media.js +118 -0
- package/engine/lib/medium.js +258 -0
- package/engine/lib/motion.js +111 -0
- package/engine/lib/paint.js +200 -0
- package/engine/lib/palettes.js +57 -0
- package/engine/lib/post.js +108 -0
- package/engine/lib/render.js +189 -0
- package/engine/lib/rig.js +390 -0
- package/engine/lib/rig_huashu.js +171 -0
- package/engine/lib/three.min.js +7 -0
- package/engine/lib/toon.js +186 -0
- package/engine/lib/typo.js +136 -0
- package/engine/lib/ui.js +141 -0
- package/engine/lib/util.js +92 -0
- package/engine/render.py +157 -0
- package/engine/scenes/01_cave.js +364 -0
- package/engine/scenes/02_egypt.js +524 -0
- package/engine/scenes/03_greek.js +448 -0
- package/engine/scenes/04_roman.js +555 -0
- package/engine/scenes/05_gothic.js +522 -0
- package/engine/scenes/06_renaissance.js +338 -0
- package/engine/scenes/08_impressionism.js +206 -0
- package/engine/scenes/09_postimp.js +183 -0
- package/engine/scenes/10_nouveau.js +334 -0
- package/engine/scenes/11_cubism.js +265 -0
- package/engine/scenes/12_bauhaus.js +214 -0
- package/engine/scenes/13_pop.js +269 -0
- package/engine/scenes/14_8bit.js +238 -0
- package/engine/scenes/15_raytrace.js +300 -0
- package/engine/scenes/16_2026.js +565 -0
- package/engine/scenes/17_ink.js +273 -0
- package/engine/scenes/18_klimt.js +227 -0
- package/engine/scenes/19_munch.js +153 -0
- package/engine/scenes/20_dunhuang.js +215 -0
- package/engine/scenes/21_kusama.js +124 -0
- package/engine/scenes/22_constructivism.js +205 -0
- package/engine/scenes/23_dali.js +221 -0
- package/engine/scenes/24_hopper.js +150 -0
- package/engine/scenes/25_ghibli.js +251 -0
- package/engine/scenes/26_vaporwave.js +191 -0
- package/engine/scenes/27_kirby.js +255 -0
- package/engine/scenes/28_monet.js +176 -0
- package/engine/scenes/29_seurat.js +104 -0
- package/engine/scenes/30_matisse.js +151 -0
- package/engine/scenes/31_haring.js +109 -0
- package/engine/scenes/32_rembrandt.js +217 -0
- package/engine/scenes/33_rubberhose.js +219 -0
- package/engine/scenes/34_shadowpuppet.js +195 -0
- package/engine/scenes/35_shinkai.js +197 -0
- package/engine/scenes/36_picasso_blue.js +123 -0
- package/engine/scenes/38_giotto.js +133 -0
- package/engine/scenes/39_piero.js +53 -0
- package/engine/scenes/40_raphael.js +96 -0
- package/engine/scenes/41_michelangelo.js +112 -0
- package/engine/scenes/42_guardi.js +70 -0
- package/engine/scenes/43_lorenzetti.js +145 -0
- package/engine/scenes/44_cortona.js +86 -0
- package/engine/scenes/45_renoir.js +86 -0
- package/engine/scenes/46_pissarro.js +82 -0
- package/engine/scenes/47_sisley.js +94 -0
- package/engine/scenes/48_gauguin.js +107 -0
- package/engine/scenes/49_chagall.js +60 -0
- package/engine/scenes/50_ma_yuan.js +131 -0
- package/engine/scenes/51_persian_miniature.js +149 -0
- package/engine/scenes/52_south_park.js +105 -0
- package/engine/scenes/53_pixar.js +60 -0
- package/engine/scenes/54_ghibli_lab.js +68 -0
- package/engine/scenes/55_dunhuang.js +161 -0
- package/engine/scenes/index.js +28 -0
- package/engine/spec_assets.py +76 -0
- package/engine/transitions.js +929 -0
- package/engine//346/227/266/344/273/243/344/275/234/350/200/205/350/247/204/350/214/203.md +49 -0
- package/lib/host.mjs +754 -0
- package/lib/render-bridge.mjs +543 -0
- package/lib/render-cli.mjs +98 -0
- package/lib/storyboard-lint.mjs +54 -0
- package/manifest.json +52 -0
- package/package.json +8 -4
- package/runtime/package.json +8 -0
- package/skills/biuart/SKILL.md +154 -0
- package/skills/biuart/references/01-/346/213/206/350/247/243.md +55 -0
- package/skills/biuart/references/02-/346/234/272/345/210/266.md +41 -0
- 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
- package/skills/biuart/references/04-/347/272/257/344/273/243/347/240/201/347/273/230/345/210/266.md +92 -0
- package/skills/biuart/references/05-/350/212/202/345/245/217/344/270/216/351/205/215/344/271/220.md +19 -0
- 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
- package/skills/biuart/references/07-/346/255/243/351/235/242/347/273/217/351/252/214.md +196 -0
- 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
- 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
- package/skills/biuart/references/10-/350/247/222/350/211/262.md +34 -0
- package/skills/biuart/references/11-/351/225/277/345/215/267/347/251/277/350/266/212/347/211/207.md +43 -0
- 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
- 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
- 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
- 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
- package/skills/biuart/references/about-author.md +99 -0
- package/skills/biuart/references/capabilities.md +91 -0
- package/skills/biuart/references/compatibility.md +34 -0
- package/skills/biuart/references/images.md +97 -0
- package/skills/biuart/references/voice-model-review.md +18 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/t1_3b1b.md +122 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/t2_keynote_ui.md +117 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/t3_finance_chart.md +116 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y1_kurzgesagt.md +193 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y2_vox.md +195 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y3_whiteboard.md +190 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y4_storytime.md +166 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y5_kinetic_type.md +158 -0
- package/skills/biuart/references//345/212/250/347/224/273/350/257/255/346/263/225/y6_presenter_explainer.md +24 -0
- package/skills/biuart/references//345/217/243/346/222/255API.md +125 -0
- package/skills/biuart/references//350/211/262/346/235/277.md +38 -0
- package/skills/biuart/references//350/247/222/350/211/262/347/262/276/347/273/206/345/272/246.md +61 -0
- package/skills/biuart/references//351/225/234/345/244/264/350/241/250/346/250/241/346/235/277.md +80 -0
- package/skills/biuart/references//351/225/234/345/244/264/350/257/255/350/250/200.md +73 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/01_cave.md +46 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/02_egypt.md +64 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/03_greek.md +50 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/04_roman.md +99 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/05_gothic.md +32 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/06_renaissance.md +41 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/08_impressionism.md +34 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/09_postimp.md +17 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/10_nouveau.md +31 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/11_cubism.md +30 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/12_bauhaus.md +30 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/13_pop.md +29 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/14_8bit.md +27 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/15_raytrace.md +32 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/16_2026.md +55 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/17_ink.md +50 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/18_klimt.md +37 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/19_munch.md +42 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/20_dunhuang.md +49 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/21_kusama.md +32 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/22_constructivism.md +35 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/23_dali.md +29 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/24_hopper.md +30 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/25_ghibli.md +55 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/26_vaporwave.md +27 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/27_kirby.md +30 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/28_monet.md +39 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/29_seurat.md +31 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/30_matisse.md +26 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/31_haring.md +22 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/32_rembrandt.md +29 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/33_rubberhose.md +31 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/34_shadowpuppet.md +40 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/35_shinkai.md +34 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/36_picasso_blue.md +25 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/37_rick_morty.md +48 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/38_giotto.md +67 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/39_piero.md +59 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/40_raphael.md +65 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/41_michelangelo.md +65 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/42_guardi.md +65 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/43_lorenzetti.md +69 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/44_cortona.md +66 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/45_renoir.md +66 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/46_pissarro.md +66 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/47_sisley.md +67 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/48_gauguin.md +66 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/49_chagall.md +59 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/50_ma_yuan.md +68 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/51_persian_miniature.md +66 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/52_south_park.md +65 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/53_pixar.md +61 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/55_dunhuang.md +41 -0
- package/skills/biuart/references//351/243/216/346/240/274/351/205/215/346/226/271/INDEX.md +89 -0
- 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
- 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
- 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
- package/studio/app.js +380 -0
- package/studio/assert-panel-permit.mjs +67 -0
- package/studio/composer.css +465 -0
- package/studio/composer.js +480 -0
- package/studio/index.html +97 -0
- package/studio/optimize-prompt.mjs +84 -0
- package/studio/optimize-prompt.test.mjs +47 -0
- package/studio/recipes.mjs +87 -0
- package/studio/select-menu.js +450 -0
- package/studio/styles.css +71 -0
- package/studio/to-english-prompt.mjs +149 -0
- package/studio/to-english-prompt.test.mjs +72 -0
- package/README.md +0 -3
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// 片段 · Kurzgesagt 式扁平科普(y1):深墨色平涂底(不写 theme 时;不再是靛紫渐变夜空)、扁平中心物=要讲的那个概念;每个要点是一颗卫星节点,
|
|
2
|
+
// 带标签胶囊弹出(过冲 108%)、信号沿连线流动。环境层(星闪、粒子、信号脉冲)一直轻轻动;中心物、节点、标签落定后停住,
|
|
3
|
+
// 相机不匀速慢推:只在 highlight 这一刻推近约 30%、连着的 highlight 同倍率移过去,然后停住。
|
|
4
|
+
// spec 级 data.camera 'smooth'(默认,推近 0.9s、换节点 0.7s,sineInOut)| 'snap'(重音:快推 0.28s、横移 0.30s)。
|
|
5
|
+
// 招牌是尺度穿行:enter 让相机对数推进一颗卫星,推到它的颜色填满整屏,再从同色的新中心物里拉出来——「里面是什么」。
|
|
6
|
+
// cues:title(顶部标题逐字上浮;text)· point(当前这一层的一个卫星节点+标签:text;data.color 换强调色)
|
|
7
|
+
// highlight(相机推近第 data.index 个节点(只数当前这一层的 point,从 0 起;默认最新),其余节点和标签变暗;1.2s 推完)
|
|
8
|
+
// enter(钻进第 data.index 个节点(默认最新):推进→满屏同色→从新的中心物拉出;text = 新中心的名字(默认用那个节点的名字);
|
|
9
|
+
// 之后的 point 都是新这一层的卫星;dur 默认 1.6s,at 这一帧开始推)
|
|
10
|
+
// data.center:第一层中心物的名字。data.center_style:{ color, shape: 'circle'|'rounded'|'hex'|'diamond', ring: true(加一道轨道环)},
|
|
11
|
+
// 不写就按名字生成一个中性样式(颜色、形状由文字决定,同名同样式;没有轨道环)。data.image:中心物里放一张图(截图、照片,按形状裁),名字改到下方胶囊里。
|
|
12
|
+
// 新一层(enter 钻进去的)中心物用那个节点的颜色,形状同样按名字生成;enter 的 data.style 可覆盖。
|
|
13
|
+
// data.flow:true:节点按顺序串成一条链(中心→1→2→3),信号依次流过,节点带序号——讲「先后」时用。
|
|
14
|
+
// 节点排法横竖屏一致:≤3 个排在上半圈(从左到右),≥4 个绕一整圈(从左边起顺时针)。safe:版面只排在 top..H−bottom 之间。
|
|
15
|
+
// spec.theme(全片色板 ctx.pal):底平涂 bg(不画渐变、不加发光),星点和微粒用 sub 低透明度,连线 sub、信号 accent2,
|
|
16
|
+
// 节点色 accent / accent2 及其明暗(data.color 照写的用),中心物默认 accent2,标签胶囊浅底色板 surface 底 ink 字、深底色板 ink 底 bg 字,标题 ink。
|
|
17
|
+
CLIPS.y1_kurzgesagt = (() => {
|
|
18
|
+
const { clamp, lerp, rng } = U;
|
|
19
|
+
// 默认(不写 theme):深墨色底,只有几乎看不出的同色相明暗;节点/中心物色里不放紫、品红、紫蓝
|
|
20
|
+
const P0 = { sky: ['#0F1114', '#121417', '#171A1E'], star: '#F2EFE8', line: 'rgba(125,211,220,.35)', pulse: '#32BAEE', pulseCore: '#e9fdff', mote: '#fff6d0', pill: '#ffffff', pillInk: '#1B1A17', title: '#fff', ring: 'rgba(255,230,190,.55)', arrow: 'rgba(125,211,220,.7)', shadow: 'rgba(20,40,20,.25)' };
|
|
21
|
+
const ACC0 = ['#32BAEE', '#FACC12', '#E07A3F', '#5CD0B3', '#FC6255', '#A3B84B'];
|
|
22
|
+
// 中心物的中性样式:按名字哈希取色和形状(同名同样式),没有轨道环
|
|
23
|
+
const CPAL0 = ['#4F86C6', '#3FA796', '#D99A3A', '#C8574A', '#5BA4CF', '#6FAF5A'], SHAPES = ['circle', 'rounded', 'hex'];
|
|
24
|
+
let P = P0, ACC = ACC0, CPAL = CPAL0, PL = null;
|
|
25
|
+
// 全片色板 → 本语法的一套色(只从 6 个键派生)
|
|
26
|
+
const fromPal = p => { const dk = PAL.dark(p), spot = dk ? p.ink : p.sub;
|
|
27
|
+
return { P: { sky: [p.bg], star: spot, line: PAL.alpha(p.sub, 0.5), pulse: p.accent2, pulseCore: PAL.mix(p.accent2, p.surface, 0.6), mote: spot, pill: dk ? p.ink : p.surface, pillInk: dk ? p.bg : p.ink, title: p.ink,
|
|
28
|
+
ring: PAL.alpha(p.ink, 0.4), arrow: PAL.alpha(p.sub, 0.85), shadow: PAL.alpha(dk ? '#000000' : p.ink, dk ? 0.3 : 0.14) },
|
|
29
|
+
ACC: [p.accent, p.accent2, PAL.mix(p.accent, p.ink, 0.35), PAL.mix(p.accent2, p.ink, 0.35), PAL.mix(p.accent, p.bg, 0.3), PAL.mix(p.accent2, p.bg, 0.3)],
|
|
30
|
+
CPAL: [p.accent2] }; };
|
|
31
|
+
const hashStr = s => { let h = 2166136261; for (const ch of String(s)) h = Math.imul(h ^ ch.codePointAt(0), 16777619); return h >>> 0; };
|
|
32
|
+
const styleFor = (name, over = {}, col) => { const h = hashStr(name || '·'); return { color: over.color || col || CPAL[h % CPAL.length], shape: over.shape || SHAPES[(h >>> 8) % SHAPES.length], ring: !!over.ring }; };
|
|
33
|
+
const shapePath = (kind, x, y, R) => { const p = new Path2D();
|
|
34
|
+
if (kind === 'rounded') p.roundRect(x - R * 0.92, y - R * 0.92, R * 1.84, R * 1.84, R * 0.36);
|
|
35
|
+
else if (kind === 'hex') for (let i = 0; i <= 6; i++) { const a = Math.PI / 6 + i * Math.PI / 3; i ? p.lineTo(x + Math.cos(a) * R * 1.06, y + Math.sin(a) * R * 1.06) : p.moveTo(x + Math.cos(a) * R * 1.06, y + Math.sin(a) * R * 1.06); }
|
|
36
|
+
else if (kind === 'diamond') { p.moveTo(x, y - R * 1.2); p.lineTo(x + R * 1.2, y); p.lineTo(x, y + R * 1.2); p.lineTo(x - R * 1.2, y); p.closePath(); }
|
|
37
|
+
else p.arc(x, y, R, 0, Math.PI * 2);
|
|
38
|
+
return p; };
|
|
39
|
+
const dark = (col, k = 0.72) => PAINT.rgb(PAINT.hex(col).map(v => v * k)), light = col => PAINT.rgb(PAINT.hex(col).map(v => v + (255 - v) * 0.4));
|
|
40
|
+
let S, levels, flow;
|
|
41
|
+
return {
|
|
42
|
+
fonts: ['PuHui-Heavy', 'PuHui-Medium'],
|
|
43
|
+
safe: true,
|
|
44
|
+
init(ctx) {
|
|
45
|
+
const { W, H, u, safe } = ctx, r = rng(11);
|
|
46
|
+
PL = ctx.pal; if (PL) ({ P, ACC, CPAL } = fromPal(PL)); else { P = P0; ACC = ACC0; CPAL = CPAL0; }
|
|
47
|
+
flow = !!ctx.data.flow;
|
|
48
|
+
const y0 = safe.top + (ctx.of('title').length ? (ctx.portrait ? 300 : 210) * u : 60 * u), y1 = H - safe.bottom - 40 * u;
|
|
49
|
+
const R = (ctx.portrait ? 160 : 140) * u, cx = W / 2, cy = (y0 + y1) / 2 + (ctx.portrait ? 0 : 30 * u);
|
|
50
|
+
S = { cx, cy, R, stars: Array.from({ length: 160 }, () => ({ x: r() * W * 1.4 - W * 0.2, y: r() * H * 1.4 - H * 0.2, r: (0.8 + r() * r() * 3) * u, ph: r() * 6.28, sp: 1 + r() * 2.5 })),
|
|
51
|
+
bokeh: Array.from({ length: 10 }, () => ({ x: r() * W * 1.3 - W * 0.15, y: r() * H * 1.3 - H * 0.15, r: (60 + r() * 150) * u, ph: r() * 6.28, a: 0.035 + r() * 0.04 })),
|
|
52
|
+
motes: Array.from({ length: 24 }, () => ({ x: r() * W, y: r() * H, ph: r() * 6.28, d: 0.8 + r() * 0.5, s: (2 + r() * 3) * u })) };
|
|
53
|
+
// 分层:enter 把后面的 point 放进新的一层
|
|
54
|
+
const cs0 = styleFor(ctx.data.center || '', ctx.data.center_style || {});
|
|
55
|
+
levels = [{ start: 0, center: ctx.data.center || '', col: cs0.color, style: cs0, img: ctx.data.image ? ctx.IMG[ctx.data.image] : null, pts: [], hls: [] }];
|
|
56
|
+
for (const q of ctx.cues) {
|
|
57
|
+
const L = levels[levels.length - 1];
|
|
58
|
+
if (q.kind === 'point') L.pts.push(q);
|
|
59
|
+
else if (q.kind === 'highlight') L.hls.push(q);
|
|
60
|
+
else if (q.kind === 'enter') { const D = q.dur || 1.6; L.exit = { q, at: q.at, D, sw: q.at + D * 0.55, idx: q.data && q.data.index };
|
|
61
|
+
levels.push({ start: q.at + D * 0.55, end0: q.at + D, center: '', col: null, pts: [], hls: [], from: L }); }
|
|
62
|
+
}
|
|
63
|
+
// 每层的节点位置(横竖屏同一套角度:≤3 上半圈左→右,≥4 从左边起顺时针一整圈)
|
|
64
|
+
const lab = 46 * u + 70 * u + 45 * u; // 节点半径+标签偏移+半个标签高:最下面的标签也要在 y1 之内
|
|
65
|
+
const rx = ctx.portrait ? W * 0.34 : Math.min(W * 0.32, 640 * u), ryHalf = Math.min(y1 - y0, H) * (ctx.portrait ? 0.3 : 0.42), ryRing = Math.min((y1 - y0) * (ctx.portrait ? 0.34 : 0.36), y1 - cy - lab, cy - y0 - 60 * u);
|
|
66
|
+
levels.forEach((L, li) => {
|
|
67
|
+
const n = L.pts.length, ring = n >= 4;
|
|
68
|
+
L.orbit = { rx, ry: ring ? ryRing : ryHalf, ring };
|
|
69
|
+
L.nodes = L.pts.map((q, i) => { const ang = ring ? Math.PI + i / n * Math.PI * 2 : Math.PI + (i + 0.5) / n * Math.PI;
|
|
70
|
+
const ry = ring ? ryRing : ryHalf; return { q, i, x: cx + Math.cos(ang) * rx, y: cy + Math.sin(ang) * ry, col: (q.data && q.data.color) || ACC[(i + li * 2) % ACC.length] }; });
|
|
71
|
+
if (L.exit) { const nd = L.exit.idx != null ? L.nodes[L.exit.idx] : L.nodes.filter(z => z.q.at <= L.exit.at).pop() || L.nodes[L.nodes.length - 1];
|
|
72
|
+
L.exit.node = nd; const N = levels[li + 1]; N.col = nd ? nd.col : ACC[0]; N.center = L.exit.q.text || (nd && nd.q.text) || '';
|
|
73
|
+
N.style = styleFor(N.center, (L.exit.q.data && L.exit.q.data.style) || {}, N.col); N.col = N.style.color; }
|
|
74
|
+
// 本层的镜头:停住;highlight 这一刻推近到节点(推近约 30%,目标点往中心物偏一点,中心不被裁掉太多),
|
|
75
|
+
// 已经推近时下一次 highlight 同倍率移到新节点;之后停住。时长按 data.camera
|
|
76
|
+
const Z = ctx.portrait ? 1.28 : 1.32; L.events = []; let zoomed = false;
|
|
77
|
+
for (const h of L.hls) {
|
|
78
|
+
const nd = h.data && h.data.index != null ? L.nodes[h.data.index] : L.nodes.filter(z => z.q.at <= h.at).pop(); if (!nd) continue;
|
|
79
|
+
const snap = (ctx.data && ctx.data.camera) === 'snap';
|
|
80
|
+
L.events.push(zoomed ? { at: h.at, kind: snap ? 'pan' : 'glide', x: lerp(nd.x, cx, 0.35), y: lerp(nd.y, cy, 0.35) } : { at: h.at, kind: 'to', x: lerp(nd.x, cx, 0.35), y: lerp(nd.y, cy, 0.35), z: Z, ...(snap ? { dur: MO.PULSE.punch, ease: MO.cubicInOut } : {}) });
|
|
81
|
+
zoomed = true; h.node = nd;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
draw(c, t, ctx) {
|
|
86
|
+
const { W, H, u } = ctx;
|
|
87
|
+
// 当前层:过了切换点就是新的一层;切换点 ±0.05s 两层交叉溶解(两边都是满屏同一种颜色,剪点看不见)
|
|
88
|
+
let k = 0; levels.forEach((L, i) => { if (i > 0 && t >= L.start) k = i; });
|
|
89
|
+
drawLevel(c, k, t, ctx);
|
|
90
|
+
const L = levels[k], ex = L.exit;
|
|
91
|
+
if (ex && t > ex.sw - 0.05 && t < ex.sw) { const buf = UI.scratch('clip_y1x', W, H), g = buf.getContext('2d'); g.setTransform(1, 0, 0, 1, 0, 0); g.clearRect(0, 0, W, H);
|
|
92
|
+
drawLevel(g, k + 1, t, ctx); c.save(); c.globalAlpha = (t - (ex.sw - 0.05)) / 0.1; c.drawImage(buf, 0, 0); c.restore(); }
|
|
93
|
+
// 标题:逐字上浮(屏幕层,不跟相机);第一次 highlight / enter 时淡出
|
|
94
|
+
const qt = ctx.of('title')[0];
|
|
95
|
+
if (qt) { const nx = ctx.of('highlight', 'enter')[0], out = nx ? 1 - MO.sineInOut(clamp((t - nx.at) / 0.5)) : 1;
|
|
96
|
+
if (out > 0) { c.save(); c.globalAlpha = out; const fs = TY.fit(c, qt.text || '', W * 0.88, (ctx.portrait ? 84 : 76) * u, 'PuHui-Heavy', { maxLines: 2, balance: true });
|
|
97
|
+
fs.lines.forEach((ln, i) => TY.charsIn(c, ln, W / 2, ctx.safe.top + (ctx.portrait ? 200 : 130) * u + i * fs.size * 1.2, ctx.lt(t, qt.at) - i * 0.2, { font: `${fs.size}px "PuHui-Heavy"`, color: P.title, dur: 0.5, stagger: 0.045, dy: 30 * u, align: 'center', shadow: PL && !ctx.alpha ? null : { color: 'rgba(0,0,0,.5)', y: 6 * u, blur: ctx.alpha ? 14 * u : 0 } }));
|
|
98
|
+
c.restore(); } }
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
// 相机:本层的镜头动作表;正在钻出去 → 推进那个节点到满屏;刚钻进来 → 从满屏的中心物拉出
|
|
102
|
+
function camAt(L, t, ctx) {
|
|
103
|
+
const { W, H } = ctx;
|
|
104
|
+
if (L.from && t < L.end0) { // 拉出:z 从「中心物盖满屏」对数降到 1,中心物从屏幕中心回到它的位置
|
|
105
|
+
const e = MO.cubicInOut(clamp((t - L.start) / (L.end0 - L.start))), Z = CAM.coverZoom(S.R * 0.84) * 1.15; // 0.84:菱形、六边形的内切圆比 R 小,也要盖满屏
|
|
106
|
+
return CAM.anchor(S.cx, S.cy, CAM.zlerp(Z, 1, e), lerp(W / 2, S.cx, e), lerp(H / 2, S.cy, e));
|
|
107
|
+
}
|
|
108
|
+
const ex = L.exit;
|
|
109
|
+
if (ex && t >= ex.at && ex.node) { // 推进:对数缩放,节点平滑移到画面中心,推到它的圆盖满屏
|
|
110
|
+
const base = CAM.track({}, L.events, ex.at), e = MO.cubicIn(clamp((t - ex.at) / (ex.sw - ex.at)) ) * 0.5 + MO.cubicInOut(clamp((t - ex.at) / (ex.sw - ex.at))) * 0.5;
|
|
111
|
+
return CAM.pushTo(base, ex.node.x, ex.node.y, CAM.coverZoom(46 * ctx.u) * 1.2, e, e);
|
|
112
|
+
}
|
|
113
|
+
return CAM.track({}, L.events, t);
|
|
114
|
+
}
|
|
115
|
+
function drawLevel(c, k, t, ctx) {
|
|
116
|
+
const { W, H, u } = ctx, L = levels[k], cam = camAt(L, t, ctx), { cx, cy, R } = S;
|
|
117
|
+
if (!ctx.alpha) {
|
|
118
|
+
c.fillStyle = P.sky.length > 1 ? TOON.vgrad(c, 0, H, P.sky) : P.sky[0]; c.fillRect(0, 0, W, H);
|
|
119
|
+
const sa = PL ? 0.35 : 1; // 色板底上星点只是一层很淡的纹理
|
|
120
|
+
CAM.layer(c, cam, 0.12, g => { for (const b of S.bokeh) { const k = 0.75 + 0.25 * Math.sin(t * 0.5 + b.ph); const bx = b.x + Math.sin(t * 0.2 + b.ph) * 14 * u, a = b.a * k * (PL ? 0.9 : 1.6), rgb = (PL ? PAINT.hex(P.star) : [125, 211, 220]).slice(0, 3).map(Math.round).join(','), gr = g.createRadialGradient(bx, b.y, b.r * 0.15, bx, b.y, b.r); gr.addColorStop(0, `rgba(${rgb},${a * 0.35})`); gr.addColorStop(0.8, `rgba(${rgb},${a * 0.55})`); gr.addColorStop(1, `rgba(${rgb},0)`); g.fillStyle = gr; g.beginPath(); g.arc(bx, b.y, b.r, 0, 7); g.fill(); } });
|
|
121
|
+
CAM.layer(c, cam, 0.05, g => { for (const s of S.stars) { g.globalAlpha = sa * (0.45 + 0.55 * (0.5 + 0.5 * Math.sin(t * s.sp + s.ph))); g.fillStyle = P.star; g.beginPath(); g.arc(s.x, s.y, s.r, 0, 7); g.fill(); } g.globalAlpha = 1; });
|
|
122
|
+
}
|
|
123
|
+
// highlight 生效时:被推近的节点亮,其余节点和标签暗下去
|
|
124
|
+
const hNow = [...L.hls].reverse().find(h => t >= h.at && h.node), dimK = hNow ? MO.smooth(clamp((t - hNow.at) / 0.6)) : 0;
|
|
125
|
+
const col = L.col, st = L.style, pull = L.from && t < L.end0 ? MO.smooth(clamp(((t - L.start) / (L.end0 - L.start) - 0.45) / 0.45)) : 1; // 刚钻进来:先是满屏纯色,名字和环在拉出后半段才出来
|
|
126
|
+
CAM.layer(c, cam, 1, g => {
|
|
127
|
+
// 节点轨道:一道很淡的虚线椭圆,让点位有「绕着中心」的结构(节点越多越像一圈,≤3 个是上半弧)
|
|
128
|
+
if (L.orbit && L.nodes.length) { const o = L.orbit, a0 = L.orbit.ring ? 0 : Math.PI, a1 = L.orbit.ring ? Math.PI * 2 : Math.PI * 2, show = MO.cubicOut(clamp(ctx.lt(t, L.nodes[0].q.at) / 0.8));
|
|
129
|
+
g.save(); g.globalAlpha = 0.5 * show * (1 - 0.6 * dimK); g.strokeStyle = P.line; g.lineWidth = 2 * u; g.setLineDash([2 * u, 14 * u]); g.lineCap = 'round'; g.beginPath(); g.ellipse(cx, cy, o.rx, o.ry, 0, a0, a0 + (a1 - a0) * show); g.stroke(); g.restore(); }
|
|
130
|
+
// 连线+信号脉冲(flow:中心→1→2→3 串成链,信号依次流过)
|
|
131
|
+
L.nodes.forEach((nd, i) => {
|
|
132
|
+
const l = ctx.lt(t, nd.q.at); if (l <= 0) return;
|
|
133
|
+
const from = flow && i > 0 ? L.nodes[i - 1] : { x: cx, y: cy }, p = MO.cubicOut(clamp(l / 0.4)), ex = lerp(from.x, nd.x, p), ey = lerp(from.y, nd.y, p);
|
|
134
|
+
g.save(); g.globalAlpha = 1 - 0.6 * dimK * (hNow && hNow.node === nd ? 0 : 1);
|
|
135
|
+
g.strokeStyle = P.line; g.lineWidth = 4 * u; g.beginPath(); g.moveTo(from.x, from.y); g.lineTo(ex, ey); g.stroke();
|
|
136
|
+
if (flow && l > 0.4) { const ax = lerp(from.x, nd.x, 0.55), ay = lerp(from.y, nd.y, 0.55), a = Math.atan2(nd.y - from.y, nd.x - from.x); g.fillStyle = P.arrow; g.translate(ax, ay); g.rotate(a); g.beginPath(); g.moveTo(10 * u, 0); g.lineTo(-8 * u, -9 * u); g.lineTo(-8 * u, 9 * u); g.closePath(); g.fill(); }
|
|
137
|
+
g.restore();
|
|
138
|
+
for (let kk = 0; kk < 3; kk++) { if (l < 0.4) break; const q = flow ? ((t * 0.7 - i * 0.33 + kk / 3) % 1 + 1) % 1 : ((l * 0.8 + kk / 3) % 1); const x = lerp(from.x, nd.x, q), y = lerp(from.y, nd.y, q); if (!PL) TOON.glow(g, x, y, 20 * u, P.pulse, 0.6 * (1 - 0.7 * dimK)); g.fillStyle = PL ? P.pulse : P.pulseCore; g.beginPath(); g.arc(x, y, (PL ? 7 : 5) * u, 0, 7); g.fill(); }
|
|
139
|
+
});
|
|
140
|
+
// 中心物:双色分面+柔光(形状、颜色来自 data.center_style 或按名字生成);可选轨道环;有 data.image 就把图裁进形状里
|
|
141
|
+
const breath = 1 + 0.018 * Math.sin(t * 1.7), shape = shapePath(st.shape, cx, cy, R * breath);
|
|
142
|
+
if (!PL) TOON.glow(g, cx, cy, R * 2.4, light(col), 0.28);
|
|
143
|
+
TOON.flat(g, shape, col, dark(col), light(col), [-18 * u, 14 * u], [-6 * u, 5 * u]);
|
|
144
|
+
if (L.img) { g.save(); g.clip(shapePath(st.shape, cx, cy, R * 0.9)); MD.cover(g, L.img, { x: cx - R, y: cy - R, w: 2 * R, h: 2 * R }); g.restore(); }
|
|
145
|
+
if (st.ring) { g.save(); g.globalAlpha = pull; g.strokeStyle = P.ring; g.lineWidth = 6 * u; g.beginPath(); g.ellipse(cx, cy, R * 1.55, R * 0.35, -0.25, Math.PI * 1.05, Math.PI * 1.95); g.stroke(); g.restore(); }
|
|
146
|
+
if (L.center && !L.img) { const f = TY.fitFloor(g, L.center, R * 1.7, 56 * u, 'PuHui-Heavy', { maxLines: 3, floor: 30 * u, balance: true }); // 名字放进中心物里:先缩字号到下限,再折行,再截断
|
|
147
|
+
g.save(); g.globalAlpha = pull; g.font = `${f.size}px "PuHui-Heavy"`; g.fillStyle = PL ? PAL.on(col, [PL.surface, PL.ink, PL.bg]) : '#fff'; g.textAlign = 'center'; g.textBaseline = 'middle';
|
|
148
|
+
f.lines.forEach((ln, i) => g.fillText(ln, cx, cy + (i - (f.lines.length - 1) / 2) * f.size * 1.1)); g.restore(); }
|
|
149
|
+
else if (L.center) { const f = TY.fit(g, L.center, R * 3, 44 * u, 'PuHui-Heavy', { maxLines: 1, min: 0.6 }); // 有图:名字放到下方白胶囊里
|
|
150
|
+
g.save(); g.globalAlpha = pull; g.font = `${f.size}px "PuHui-Heavy"`; const tw = g.measureText(f.lines[0] || '').width, bw = tw + 56 * u, bh = f.size * 1.6, by = cy + R * 1.12;
|
|
151
|
+
g.fillStyle = P.pill; g.beginPath(); g.roundRect(cx - bw / 2, by, bw, bh, bh / 2); g.fill(); g.fillStyle = P.pillInk; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText(f.lines[0] || '', cx, by + bh / 2 + 2 * u); g.restore(); }
|
|
152
|
+
// 节点+标签胶囊(弹出 0.5s,过冲 108%)
|
|
153
|
+
for (const nd of L.nodes) {
|
|
154
|
+
const l = ctx.lt(t, nd.q.at); if (l <= 0) continue;
|
|
155
|
+
const on = hNow && hNow.node === nd, a = 1 - 0.65 * dimK * (on ? 0 : 1);
|
|
156
|
+
const pop = MO.backOut(clamp(l / 0.5), 1.1), fy = l > 0.5 ? MO.float(t, 7 * u, 3.4, nd.i * 1.9) * clamp((l - 0.5) / 0.6) : 0, nr = 46 * u * pop * (on ? 1 + 0.15 * dimK : 1);
|
|
157
|
+
g.save(); g.globalAlpha = a;
|
|
158
|
+
if (!PL) TOON.glow(g, nd.x, nd.y + fy, nr * (on ? 4.2 : 2.6), nd.col, on ? 0.7 : 0.45);
|
|
159
|
+
TOON.flat(g, TOON.circle(nd.x, nd.y + fy, nr), nd.col, dark(nd.col), PL ? light(nd.col) : '#ffffff', [-8 * u, -8 * u], [-3 * u, -3 * u]);
|
|
160
|
+
if (l < 1.0) { const rp = clamp(l / 0.9); g.save(); g.globalAlpha = (1 - rp) * 0.55 * a; g.strokeStyle = nd.col; g.lineWidth = 4 * u * (1 - rp); g.beginPath(); g.arc(nd.x, nd.y + fy, nr * (1.1 + rp * 1.5), 0, 7); g.stroke(); g.restore(); }
|
|
161
|
+
if (flow) { g.font = `${30 * u * pop}px "PuHui-Heavy"`; g.fillStyle = PL ? PAL.on(nd.col, [PL.surface, PL.ink, PL.bg]) : P.pillInk; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText(String(nd.i + 1), nd.x, nd.y + fy + 2 * u); }
|
|
162
|
+
const gone = L.exit ? 1 - clamp((t - L.exit.at) / 0.2) : 1; // 钻进去时:推进一开始这一层的标签全收掉(不然被放大成半屏白块,邻居的也会)
|
|
163
|
+
const lp = MO.backOut(clamp((l - 0.15) / 0.5), 1.1) * gone; if (lp > 0) {
|
|
164
|
+
const sc = lp * (on ? 1 + 0.18 * dimK : 1);
|
|
165
|
+
const lf = TY.fitFloor(g, nd.q.text || '', ctx.W * 0.44 - 56 * u, (ctx.portrait ? 44 : 38) * u, 'PuHui-Medium', { maxLines: 1, floor: 30 * u }), lab = lf.lines[0] || ''; // 长标签先缩到下限,再截断加「…」,不缩到看不清
|
|
166
|
+
g.font = `${lf.size}px "PuHui-Medium"`; const tw = g.measureText(lab).width, bw = tw + 56 * u, bh = 70 * u;
|
|
167
|
+
const ly = nd.y + fy + (nd.y < cy - R * 0.5 && ctx.portrait ? -nr - 70 * u : nr + 70 * u); // 标签放节点下方(竖屏上半圈放上方)
|
|
168
|
+
const z = cam.z || 1, vx0 = cam.x - ctx.W / 2 / z, vx1 = cam.x + ctx.W / 2 / z, hw = bw * sc / 2 + 20 * u / z; // 当前镜头里看得见的世界范围(慢推会把边上的标签推出画)
|
|
169
|
+
const lx = nd.x > vx0 && nd.x < vx1 ? (vx1 - vx0 > 2 * hw ? clamp(nd.x, vx0 + hw, vx1 - hw) : cam.x) : nd.x; // 节点在画里,标签就夹回画里
|
|
170
|
+
g.translate(lx, ly); g.scale(sc, sc);
|
|
171
|
+
g.fillStyle = P.shadow; g.beginPath(); g.roundRect(-bw / 2 + 6 * u, -bh / 2 + 8 * u, bw, bh, bh / 2); g.fill();
|
|
172
|
+
g.fillStyle = P.pill; g.beginPath(); g.roundRect(-bw / 2, -bh / 2, bw, bh, bh / 2); g.fill();
|
|
173
|
+
if (on) { g.strokeStyle = nd.col; g.lineWidth = 5 * u * dimK; g.stroke(); }
|
|
174
|
+
g.fillStyle = P.pillInk; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText(lab, 0, 2 * u);
|
|
175
|
+
}
|
|
176
|
+
g.restore();
|
|
177
|
+
}
|
|
178
|
+
// 萤火微粒
|
|
179
|
+
for (const m of S.motes) { const x = m.x + Math.sin(t * 0.8 + m.ph) * 30 * u, y = m.y - ((t * 22 * u + m.ph * 40 * u) % (120 * u)); g.globalAlpha = (PL ? 0.3 : 1) * (0.4 + 0.4 * Math.sin(t * 2 + m.ph)); g.fillStyle = P.mote; g.beginPath(); g.arc(x, y, m.s, 0, 7); g.fill(); }
|
|
180
|
+
g.globalAlpha = 1;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
})();
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// 片段 · Vox 式拼贴(y2):一张大桌面=世界,素材按出场顺序摆在桌上、红线连起来;相机 60fps 平滑追过去,
|
|
2
|
+
// 桌上的东西(滑入、荧光笔、红圈、打字)按 12fps「一拍二」步进。
|
|
3
|
+
// cues:image | clip(截图/照片/剪报:image 路径,或 frames 录像帧序列;冲印件(白边、旧纸色、胶带,MD.print);text 可选=压在左下角的黑底撕边标签(12fps 打字);
|
|
4
|
+
// data.halftone:true 转成灰度网点,默认保留原色;data.fit:'cover' 按 data.focus=[fx,fy] 裁成 data.aspect(默认 16:9 / 竖屏 4:5)满框,默认等比不裁;at 这一帧开始滑入)
|
|
5
|
+
// title(Borders 式黑底白字标签条,打字 13 字/秒)· point(横线索引卡,打字)
|
|
6
|
+
// highlight(当前这张图上 data.rect=[x,y,w,h](0..1,相对图片):at 起荧光笔扫过 → 红笔圈住 → 相机推近那一块)
|
|
7
|
+
// 照片和截图是主角:镜头停在一张上时它占画面八九成(横屏宽 ≈90%、或高 ≈86%)。横屏素材左→右排、竖屏上→下排;
|
|
8
|
+
// 镜头:在下一件的 at 之前移过去(红线先描出去、镜头再追),highlight 时推近那一块,其余时间停住。
|
|
9
|
+
// spec 级 data.camera 'smooth'(默认,找下一件 0.7s、推近 0.9s,sineInOut,取 Vox 实测 0.5–1.5s)| 'snap'(重音:横移 0.30s、快推 0.28s)。
|
|
10
|
+
// spec.theme(全片色板 ctx.pal):桌面纸纹底色 bg,索引卡 surface、字 ink,标题条 ink 底 surface 字,红线/图钉/红圈 accent,
|
|
11
|
+
// 索引卡横线 accent2 淡色,荧光笔 accent 提亮;照片的冲印白边、胶带是实物,照旧。
|
|
12
|
+
CLIPS.y2_vox = (() => {
|
|
13
|
+
const { clamp, lerp, rng } = U;
|
|
14
|
+
const C0 = { desk: '#e4ddcf', ink: '#171716', red: '#b8433f', paper: '#eeedeb', card: '#f6f4ee', fiber: [110, 95, 70], rule: 'rgba(80,140,200,.3)', margin: 'rgba(184,67,63,.5)', vig: '40,30,20', hi: undefined };
|
|
15
|
+
let C = C0;
|
|
16
|
+
const fromPal = p => ({ desk: p.bg, ink: p.ink, red: p.accent, paper: p.surface, card: p.surface, fiber: PAL.rgb(p.sub), rule: PAL.alpha(p.accent2, 0.3), margin: PAL.alpha(p.accent, 0.5),
|
|
17
|
+
vig: PAL.rgb(PAL.dark(p) ? '#000000' : p.ink).join(','), hi: PAL.alpha(PAL.mix(p.accent, '#FFFFFF', 0.35), 0.85) });
|
|
18
|
+
const BOLD = '"PuHui-Heavy"', TXT = '"PuHui-Bold"';
|
|
19
|
+
let items, keys, strings;
|
|
20
|
+
// 一拍二的片内时间:at 这一帧就 >0
|
|
21
|
+
const ls = (t, at) => MO.step(t - at + 1e-6, 12) + 1 / 12;
|
|
22
|
+
return {
|
|
23
|
+
fonts: ['PuHui-Heavy', 'PuHui-Bold'],
|
|
24
|
+
safe: true,
|
|
25
|
+
init(ctx) {
|
|
26
|
+
const { W, H, u, portrait } = ctx, r = rng(7), g = document.createElement('canvas').getContext('2d');
|
|
27
|
+
C = ctx.pal ? fromPal(ctx.pal) : C0;
|
|
28
|
+
items = []; let cur = 0, prev = null;
|
|
29
|
+
for (const q of ctx.of('image', 'clip', 'title', 'point')) {
|
|
30
|
+
let w, h, kind = q.kind === 'clip' ? 'image' : q.kind, lines = null, size = 0;
|
|
31
|
+
if (kind === 'image') {
|
|
32
|
+
const im = ctx.still(q); if (!im) continue;
|
|
33
|
+
const [iw, ih] = MD.size(im), cov = q.data && q.data.fit === 'cover', ar = cov ? (q.data.aspect || (portrait ? 0.8 : 16 / 9)) : iw / ih;
|
|
34
|
+
const bw = W * (portrait ? 0.86 : 0.78), bh = H * (portrait ? 0.5 : 0.76), f = CLIP.fit(ar, 1, 0, 0, bw, bh);
|
|
35
|
+
w = f.w + 32 * u; h = f.h + 32 * u;
|
|
36
|
+
} else {
|
|
37
|
+
if (!String(q.text ?? '').trim()) { console.warn(`y2 ${q.kind} at=${q.at}:text 是空的,这张卡跳过`); continue; } // 空卡宽度是 −Infinity,撕边点列会死循环
|
|
38
|
+
size = (kind === 'title' ? 64 : 48) * u;
|
|
39
|
+
lines = TY.wrap(g, q.text || '', W * (portrait ? 0.74 : 0.5), `${size}px ${kind === 'title' ? BOLD : TXT}`);
|
|
40
|
+
g.font = `${size}px ${kind === 'title' ? BOLD : TXT}`;
|
|
41
|
+
w = Math.max(...lines.map(l => g.measureText(l).width)) + (kind === 'title' ? 80 : 110) * u; h = lines.length * size * 1.45 + (kind === 'title' ? 50 : 100) * u;
|
|
42
|
+
}
|
|
43
|
+
const gap = 260 * u, jitter = (r() - 0.5) * (portrait ? W * 0.12 : H * 0.12);
|
|
44
|
+
const x = portrait ? W / 2 + jitter : (prev ? prev.x + prev.w / 2 + gap + w / 2 : W / 2);
|
|
45
|
+
const y = portrait ? (prev ? prev.y + prev.h / 2 + gap + h / 2 : H / 2) : H / 2 + jitter;
|
|
46
|
+
const it = { q, kind, x, y, w, h, rot: (r() - 0.5) * 0.07, lines, size, seed: 11 + items.length * 7 };
|
|
47
|
+
items.push(it); prev = it; cur++;
|
|
48
|
+
}
|
|
49
|
+
// 文字卡的打字速度与「读完」时刻:默认 13 字/秒;离下一件太近就提速(最多 40 字/秒),保证相机走之前打完、再停 0.5s 让人读完
|
|
50
|
+
items.forEach((it, i) => {
|
|
51
|
+
if (it.kind === 'image') { it.done = it.q.at + 0.6; return; }
|
|
52
|
+
const nC = [...(it.q.text || '')].length, next = items[i + 1] ? items[i + 1].q.at : ctx.dur;
|
|
53
|
+
it.cps = clamp(nC / Math.max(0.3, next - it.q.at - 0.95 - 0.5), 13, 40);
|
|
54
|
+
it.done = it.q.at + nC / it.cps + 0.5;
|
|
55
|
+
});
|
|
56
|
+
// 红线:上一件的右缘(竖屏下缘)钉到下一件的左缘(上缘)
|
|
57
|
+
strings = items.slice(1).map((b, i) => { const a = items[i];
|
|
58
|
+
const pa = portrait ? [a.x + a.w * 0.2, a.y + a.h / 2 - 20 * u] : [a.x + a.w / 2 - 30 * u, a.y + a.h * 0.25];
|
|
59
|
+
const pb = portrait ? [b.x - b.w * 0.2, b.y - b.h / 2 + 20 * u] : [b.x - b.w / 2 + 30 * u, b.y - b.h * 0.25];
|
|
60
|
+
const pts = CL.stringPts(pa, pb, 0.06); return { a: pa, b: pb, pts, cum: DG.cum(pts), at: b.q.at }; });
|
|
61
|
+
// 相机关键帧:每件到场前 ≈0.9s 起追过去(长尾),在它的 at 之前落定;高亮时推近那一块
|
|
62
|
+
const Hs = H - ctx.safe.top - ctx.safe.bottom; // 让开字幕带后的可用高度;画面中心同步下移/上移(draw 里平移)
|
|
63
|
+
const zFor = it => it.kind === 'image' ? clamp(Math.min(W * 0.9 / it.w, Hs * 0.86 / it.h), 0.5, 2.4) : clamp(Math.min(W * 0.82 / it.w, Hs * 0.8 / it.h), 0.5, 1.6); // 照片撑满画面,文字卡别放太大
|
|
64
|
+
keys = []; let last = null;
|
|
65
|
+
const snap = (ctx.data && ctx.data.camera) === 'snap', MV = snap ? { next: MO.PULSE.pan, in: MO.PULSE.punch, ease: MO.cubicInOut } : { next: MO.PULSE.glide, in: MO.PULSE.push, ease: MO.sineInOut };
|
|
66
|
+
const hls = ctx.of('highlight');
|
|
67
|
+
const events = [...items.map(it => ({ at: it.q.at, it })), ...hls.map(h => ({ at: h.at, h }))].sort((a, b) => a.at - b.at);
|
|
68
|
+
let curItem = null;
|
|
69
|
+
for (const e of events) {
|
|
70
|
+
if (e.it) {
|
|
71
|
+
const tgt = { x: e.it.x, y: e.it.y, z: zFor(e.it) };
|
|
72
|
+
if (!last) { keys.push({ t: 0, ...tgt }); }
|
|
73
|
+
else { const P = MV.next, t0 = Math.max(last.t + 0.05, e.at - P - 1 / ctx.FPS), t1 = t0 + P; // 移到下一件,at 前一帧到位(上一个动作没走完就接着走)
|
|
74
|
+
keys.push({ t: t0, x: last.x, y: last.y, z: last.z }); keys.push({ t: t1, ...tgt, ease: MV.ease }); }
|
|
75
|
+
last = keys[keys.length - 1]; curItem = e.it;
|
|
76
|
+
} else if (curItem && curItem.kind === 'image' && e.h.data && e.h.data.rect) {
|
|
77
|
+
const R = e.h.data.rect, ib = imgBox(curItem, u);
|
|
78
|
+
const cx = curItem.x + (ib.x + (R[0] + R[2] / 2) * ib.w) - curItem.w / 2, cy = curItem.y + (ib.y + (R[1] + R[3] / 2) * ib.h) - curItem.h / 2;
|
|
79
|
+
const rw = R[2] * ib.w, rh = R[3] * ib.h;
|
|
80
|
+
let zFit = Math.min(W * 0.8 / (rw * 1.4 + 32 * u), Hs * 0.8 / (rh * 1.44 + 20 * u)); // 红圈(下面 drawItem 的椭圆)推近后整圈留在画里,左右各留一成(审片:0.9 时手抖的圈和运动模糊仍会出画)
|
|
81
|
+
if (R[2] >= 0.5) zFit = Math.min(zFit, Math.max(last.z, W * 0.96 / curItem.w)); // 框占了半张图以上:推近只会把这张图两侧的字切掉,不推
|
|
82
|
+
const z = Math.min(clamp(Math.min(W * (ctx.portrait ? 0.78 : 0.55) / rw, Hs * 0.55 / rh), last.z, last.z * 2.6), zFit); // 竖屏宽是瓶颈:框推到约八成画宽
|
|
83
|
+
// 框的起点(荧光笔从左缘扫起)在当前镜头里看得见:先画、at+0.2 再推近;看不见(上一次推近后它在画外):
|
|
84
|
+
// 先把镜头移过去、at 前一帧到位,再揭开——否则揭开那一帧在画外,观众晚 0.2s 以上才看到(2026-10 竖屏实测晚 7 帧)
|
|
85
|
+
const x0 = cx - rw / 2, inView = Math.abs(x0 - last.x) * last.z < W / 2 - 20 * u && Math.abs(cy - last.y) * last.z < Hs / 2 - rh * last.z / 2;
|
|
86
|
+
const P = MV.in, t0 = inView ? Math.max(last.t + 0.05, e.at + 0.2) : Math.max(last.t + 0.05, e.at - P - 1 / ctx.FPS), t1 = t0 + P; // 推近(默认缓推,snap 快推)
|
|
87
|
+
keys.push({ t: t0, x: last.x, y: last.y, z: last.z }); keys.push({ t: t1, x: cx, y: cy, z, ease: MV.ease });
|
|
88
|
+
last = keys[keys.length - 1];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!keys.length) keys.push({ t: 0, x: W / 2, y: H / 2, z: 1 });
|
|
92
|
+
const end = keys[keys.length - 1]; keys.push({ t: Math.max(end.t + 0.01, ctx.dur), x: end.x, y: end.y, z: end.z, ease: MO.linear }); // 最后一个动作之后停住
|
|
93
|
+
},
|
|
94
|
+
draw(c, t, ctx) {
|
|
95
|
+
const { W, H, u } = ctx, cam = CAM.at(keys, t);
|
|
96
|
+
const buf = UI.scratch('clip_vox', W, H), g = buf.getContext('2d'); g.reset();
|
|
97
|
+
g.save(); g.translate(0, (ctx.safe.top - ctx.safe.bottom) / 2); CAM.apply(g, cam);
|
|
98
|
+
if (!ctx.alpha) { g.fillStyle = g.createPattern(CL.paperTile('clipdesk' + (C === C0 ? '' : '_' + C.desk), C.desk, { amt: 12, fibers: 320, fiberCol: C.fiber }), 'repeat'); const s = 4 / cam.z; g.fillRect(cam.x - W * s, cam.y - H * s, W * 2 * s, H * 2 * s); }
|
|
99
|
+
// 红线:跟着下一件的到场描出(12fps)
|
|
100
|
+
for (const s of strings) { const q = MO.sineInOut(clamp(ls(t, s.at - 0.9) / 0.7)); if (t < s.at - 0.9) continue;
|
|
101
|
+
CL.string(g, s.pts, s.cum, s.cum[s.cum.length - 1] * q, { col: C.red, lw: 5 * u }); CL.pin(g, s.a[0], s.a[1], C.red); if (q >= 1) CL.pin(g, s.b[0], s.b[1], C.red); }
|
|
102
|
+
for (const it of items) {
|
|
103
|
+
const q = it.q; if (t < q.at - 1e-6) continue;
|
|
104
|
+
// 一拍二从 at 起算(ls),不从绝对 12fps 网格起算:按网格取整会让素材比 cue 早 1–2 帧露出(2026-10 竖屏实测早了 2 帧)
|
|
105
|
+
CL.slideIn(g, q.at + ls(t, q.at), q.at, it.x, it.y, it.rot, gg => drawItem(gg, it, t, ctx), { from: ctx.portrait ? [120 * u, 360 * u] : [420 * u, -60 * u], tilt: 0.2, dur: 0.55, fps: 1e6 });
|
|
106
|
+
}
|
|
107
|
+
g.restore();
|
|
108
|
+
const [vx, vy] = CAM.velocity(tt => CAM.at(keys, tt), t);
|
|
109
|
+
if (Math.hypot(vx, vy) >= 1.5) c.drawImage(buf, 0, 0); // 先垫一张不偏移的:motionBlur 第一张就是偏移的,画面四边会留一圈半透明(出片是灰边)
|
|
110
|
+
CAM.motionBlur(c, buf, vx * 0.6, vy * 0.6, 7);
|
|
111
|
+
if (!ctx.alpha) { // 暗角+静态颗粒(纸纹是贴死的,不逐帧抖)
|
|
112
|
+
const v = PAINT.cached('clip_vox_vig' + W + 'x' + H + C.vig, W, H, gg => { const r = gg.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.45, W / 2, H / 2, Math.max(W, H) * 0.62); r.addColorStop(0, `rgba(${C.vig},0)`); r.addColorStop(1, `rgba(${C.vig},${C === C0 ? .34 : .18})`); gg.fillStyle = r; gg.fillRect(0, 0, W, H); });
|
|
113
|
+
c.drawImage(v, 0, 0);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
function imgBox(it, u) { return { x: 16 * u, y: 16 * u, w: it.w - 32 * u, h: it.h - 32 * u }; } // 冲印件里图片的矩形(相对冲印件左上角)
|
|
118
|
+
function drawItem(g, it, t, ctx) {
|
|
119
|
+
const { u } = ctx, q = it.q, w = it.w, h = it.h;
|
|
120
|
+
if (it.kind === 'image') {
|
|
121
|
+
const f = imgBox(it, u), fr = ctx.frame(q, t), cov = q.data && q.data.fit === 'cover', fo = (q.data && q.data.focus) || [0.5, 0.5];
|
|
122
|
+
let src = fr;
|
|
123
|
+
if (q.data && q.data.halftone) { const hs = PAINT.canvas(Math.round(f.w), Math.round(f.h)); MD.cover(hs.getContext('2d'), fr, { x: 0, y: 0, w: hs.width, h: hs.height, fit: cov ? 'cover' : 'contain', fx: fo[0], fy: fo[1] }); src = CL.halftone('clipimg' + it.seed + (q.frames ? '_' + Math.floor((t - q.at) * (q.fps || 30)) : ''), hs, { cell: 5 * u, contrast: 1.25 }); }
|
|
124
|
+
MD.print(g, src, f.w, f.h, { border: 16 * u, fx: fo[0], fy: fo[1], tape: true, key: q.frames ? null : 'y2print' + it.seed }); // 冲印件:白边、旧纸色、投影、胶带
|
|
125
|
+
if (q.text) { const fs = TY.fit(g, q.text, w * 0.8, 40 * u, 'PuHui-Heavy', { maxLines: 1, min: 0.5 }); // 黑底撕边标签压在左下角,12fps 打字
|
|
126
|
+
MD.label(g, q.text, -w / 2 - 14 * u, h / 2 - 34 * u, { t, at: q.at + 0.25, size: fs.size, r: -0.02, bg: C.ink, fg: C.paper }); }
|
|
127
|
+
// 高亮:荧光笔(multiply,一拍二扫过)→ 红笔圈
|
|
128
|
+
for (const hq of ctx.of('highlight')) {
|
|
129
|
+
if (!hq.data || !hq.data.rect || hq.at < q.at) continue;
|
|
130
|
+
const own = ctx.of('image', 'clip').filter(z => z.at <= hq.at).pop(); if (own !== q) continue;
|
|
131
|
+
const l = t - hq.at; if (l < -1e-6) continue;
|
|
132
|
+
const R = hq.data.rect, x0 = -w / 2 + f.x + R[0] * f.w, y0 = -h / 2 + f.y + R[1] * f.h, rw = R[2] * f.w, rh = R[3] * f.h;
|
|
133
|
+
CL.highlight(g, x0 - 6 * u, y0 - 4 * u, rw + 12 * u, rh + 8 * u, MO.sineInOut(clamp(ls(t, hq.at) / 0.6)), C.hi);
|
|
134
|
+
const cq = MO.cubicOut(clamp((ls(t, hq.at) - 0.5) / 0.6));
|
|
135
|
+
const newer = ctx.of('highlight').find(z => z.at > hq.at && z.at <= t + 1e-6 && z.data && z.data.rect && ctx.of('image', 'clip').filter(y => y.at <= z.at).pop() === q);
|
|
136
|
+
const fa = newer ? 1 - 0.75 * clamp((t - newer.at) / 0.3) : 1; // 同一张图圈了下一处:旧红圈退成淡痕,免得两个圈叠着、旧圈出画
|
|
137
|
+
if (cq > 0) { const pts = DG.hand(DG.ellipsePts(x0 + rw / 2, y0 + rh / 2, rw * 0.7 + 16 * u, rh * 0.72 + 10 * u, -2.6, 1.08, 44), { amp: 2.2 * u, seed: it.seed }), L = DG.cum(pts);
|
|
138
|
+
g.save(); g.globalAlpha *= fa; g.strokeStyle = C.red; g.lineWidth = 7 * u; g.lineCap = 'round'; g.lineJoin = 'round'; DG.drawPartial(g, pts, L, L[L.length - 1] * cq); g.restore(); }
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
const n = Math.ceil(ls(t, q.at) * (it.cps || 13)), title = it.kind === 'title';
|
|
142
|
+
CL.shadowed(g, () => { g.fillStyle = title ? C.ink : C.card; g.save(); g.translate(-w / 2, -h / 2); CL.tornRect(g, w, h, it.seed, 3 * u); g.fill(); g.restore(); }, { blur: 10 * u, x: 3 * u, y: 6 * u });
|
|
143
|
+
if (!title) { g.strokeStyle = C.rule; g.lineWidth = 2 * u; for (let y = -h / 2 + 90 * u; y < h / 2; y += it.size * 1.45) { g.beginPath(); g.moveTo(-w / 2, y); g.lineTo(w / 2, y); g.stroke(); }
|
|
144
|
+
g.strokeStyle = C.margin; g.beginPath(); g.moveTo(-w / 2, -h / 2 + 56 * u); g.lineTo(w / 2, -h / 2 + 56 * u); g.stroke(); }
|
|
145
|
+
g.fillStyle = title ? C.paper : C.ink; g.font = `${it.size}px ${title ? BOLD : TXT}`; g.textBaseline = 'alphabetic';
|
|
146
|
+
let k = 0; it.lines.forEach((ln, li) => { let x = -w / 2 + (title ? 40 : 55) * u; const y = -h / 2 + (title ? 25 * u : 70 * u) + (li + 1) * it.size * 1.3;
|
|
147
|
+
for (const ch of ln) { if (k < n) g.fillText(ch, x, y); x += g.measureText(ch).width; k++; } });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// 片段 · 白板手绘(y3,RSA 型):说一句写一句、讲到东西就画出来。一支马克笔在白板上写字、画图标、画箭头、圈重点,相机跟着往下走,最后拉远看全图。
|
|
2
|
+
// cues:title(标题,7.5 字/秒起,赶不上下一个 cue 会自动提速)· point(要点:先画一个小圈当项目符号再写,16 字/秒起)
|
|
3
|
+
// draw(画一个手绘图标:data.icon 见下方 ICONS,text = 图标下的手写说明;和刚写的那条字并排;连续的 draw 用橙箭头连起来,像示范片「一摞图 → 网络」;data.arrow:false 不画箭头)
|
|
4
|
+
// image(截图/草图:笔走之字形把它「揭开」,再描一圈手绘框;等比放,不裁)
|
|
5
|
+
// highlight(橙圈圈住一条:data.index = 第几个要点(只数 point,从 0 起,和 t1/y1 同一口径);
|
|
6
|
+
// data.target = 'title' | 'draw' | 'image' 改圈那一类(配 index 数那一类,默认最新);不写 = 最新写出的那一条;data.style:'underline' 改成划线)
|
|
7
|
+
// 版式:横屏有 draw/image 时左文右图,竖屏有 draw 时图标在右侧窄栏;图标和刚写的那条字并排,连续的图标之间画往下的橙箭头。
|
|
8
|
+
// 横屏纯文字、要点 ≥5 条时分左右两栏。字号随内容多少在 0.72–1.2 倍之间取,尽量一屏放下并撑满;放不下才让相机往下走。
|
|
9
|
+
// 每一笔都在 at 这一帧落下第一点;写字速度=max(基准速度, 字数 / 到下一个 cue 的间隔),说到下一句时这句已写完。
|
|
10
|
+
// 笔:两笔之间空档 <0.6s 沿弧线滑过去;更长就 0.35s 退出右下屏外,下一笔前 0.3s 再进来;最后一笔画完一定退场,不留在字上。
|
|
11
|
+
// safe(spec.safe):内容只排在 top..H−bottom 之间,收尾拉远也只拉进这一带。alpha:墨色外面描一圈白边,叠在深色画面上也看得见。
|
|
12
|
+
// spec.theme(全片色板 ctx.pal):板面就是片子的底色 bg(和前后镜头连成一片;深底色板就是一块黑板),字和线 ink,
|
|
13
|
+
// 项目符号、箭头、圈重点 accent,图标里的浅色铺底是 accent 的淡色。
|
|
14
|
+
CLIPS.y3_whiteboard = (() => {
|
|
15
|
+
const { clamp, lerp } = U;
|
|
16
|
+
let INK = '#0A0503', ORANGE = '#EF7226', WASH = 'rgba(239,114,38,.22)', BOARD = '#FBFBFB';
|
|
17
|
+
const FONT = 'LXGWWenKai-500';
|
|
18
|
+
// ---------- 手绘图标:单位框 [-0.5,0.5]²(y 朝下)里的笔画;o:1 = 橙色强调,seg:1 = 快速直线(网络连线),fill = 铺浅橙 ----------
|
|
19
|
+
const A = (cx, cy, rx, ry, a0, a1, n = 22) => DG.arcPts(cx, cy, rx, ry, a0, a1, n);
|
|
20
|
+
const box = (x0, y0, x1, y1) => [[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0 + 0.01]];
|
|
21
|
+
const ICONS = {
|
|
22
|
+
person: () => [{ p: A(0, -0.3, 0.13, 0.13, -Math.PI / 2, Math.PI * 1.55) }, { p: [[0, -0.17], [0, 0.14]] }, { p: [[-0.24, -0.04], [0, -0.1], [0.24, -0.04]] }, { p: [[-0.17, 0.44], [0, 0.14], [0.17, 0.44]] }],
|
|
23
|
+
cat: () => { const k = 1 / 380, T = pts => pts.map(([x, y]) => [x * k, y * k + 0.44]);
|
|
24
|
+
return [{ p: T([[-60, -250], [-92, -330], [-30, -300], [30, -300], [92, -330], [60, -250]]) }, { p: T(A(0, -230, 100, 82, -0.55, Math.PI + 0.55, 26).reverse()) },
|
|
25
|
+
{ p: T([[-80, -170], [-120, -60], [-110, 0], [110, 0], [120, -60], [80, -170]]), fill: T([[-80, -170], [-120, -60], [-110, 0], [110, 0], [120, -60], [80, -170], [0, -150]]) },
|
|
26
|
+
{ p: T([[110, -20], [190, -40], [210, -130], [170, -190]]) }, { p: T(A(-38, -240, 12, 16, 0, Math.PI * 2, 14)), w: 0.8 }, { p: T(A(38, -240, 12, 16, 0, Math.PI * 2, 14)), w: 0.8 },
|
|
27
|
+
{ p: T([[-30, -196], [-110, -206]]), w: 0.6 }, { p: T([[30, -196], [110, -206]]), w: 0.6 }]; },
|
|
28
|
+
network: () => { const C = [[-0.38, [-0.28, 0, 0.28]], [0, [-0.33, -0.11, 0.11, 0.33]], [0.38, [-0.14, 0.14]]], r = 0.06, out = [];
|
|
29
|
+
for (const [x, ys] of C) for (const y of ys) out.push({ p: A(x, y, r, r, -Math.PI / 2, Math.PI * 1.55, 14), fast: 1 });
|
|
30
|
+
for (let a = 0; a < 2; a++) for (const y0 of C[a][1]) for (const y1 of C[a + 1][1]) out.push({ seg: [[C[a][0] + r, y0], [C[a + 1][0] - r, y1]] });
|
|
31
|
+
return out; },
|
|
32
|
+
doc: () => [{ p: [[0.14, -0.42], [-0.3, -0.42], [-0.3, 0.42], [0.3, 0.42], [0.3, -0.26], [0.14, -0.42], [0.14, -0.26], [0.3, -0.26]] },
|
|
33
|
+
{ p: [[-0.18, -0.12], [0.18, -0.12]], w: 0.7 }, { p: [[-0.18, 0.04], [0.18, 0.04]], w: 0.7 }, { p: [[-0.18, 0.2], [0.06, 0.2]], w: 0.7 }],
|
|
34
|
+
chat: () => [{ p: [[-0.3, -0.36], [0.3, -0.36], [0.42, -0.24], [0.42, 0.1], [0.3, 0.22], [-0.06, 0.22], [-0.24, 0.4], [-0.2, 0.22], [-0.3, 0.22], [-0.42, 0.1], [-0.42, -0.24], [-0.3, -0.36]] },
|
|
35
|
+
...[-0.18, 0, 0.18].map(x => ({ p: A(x, -0.07, 0.035, 0.035, 0, Math.PI * 2, 10), w: 0.9, fast: 1 }))],
|
|
36
|
+
bulb: () => [{ p: A(0, -0.12, 0.26, 0.26, Math.PI * 0.72, Math.PI * 2.28, 30), fill: A(0, -0.12, 0.26, 0.26, 0, Math.PI * 2, 30) }, { p: [[-0.11, 0.12], [-0.11, 0.27], [0.11, 0.27], [0.11, 0.12]] },
|
|
37
|
+
{ p: [[-0.08, 0.36], [0.08, 0.36]] }, ...[-2.4, -1.57, -0.74].map(a => ({ p: [[Math.cos(a) * 0.34, -0.12 + Math.sin(a) * 0.34], [Math.cos(a) * 0.46, -0.12 + Math.sin(a) * 0.46]], o: 1, fast: 1 }))],
|
|
38
|
+
check: () => [{ p: [[-0.32, 0.0], [-0.08, 0.26], [0.36, -0.32]], o: 1, w: 1.5 }],
|
|
39
|
+
cross: () => [{ p: [[-0.28, -0.28], [0.28, 0.28]], o: 1, w: 1.5 }, { p: [[0.28, -0.28], [-0.28, 0.28]], o: 1, w: 1.5 }],
|
|
40
|
+
screen: () => [{ p: box(-0.44, -0.36, 0.44, 0.2) }, { p: [[0, 0.2], [0, 0.34]] }, { p: [[-0.2, 0.36], [0.2, 0.36]] },
|
|
41
|
+
{ p: [[-0.32, -0.22], [0.06, -0.22]], w: 0.6, fast: 1 }, { p: [[-0.24, -0.1], [0.24, -0.1]], w: 0.6, fast: 1 }, { p: [[-0.24, 0.02], [0.12, 0.02]], w: 0.6, fast: 1 }],
|
|
42
|
+
stack: () => [2, 1, 0].map(i => ({ p: box(-0.4 + i * 0.09, -0.22 - i * 0.09, 0.2 + i * 0.09, 0.3 - i * 0.09), paper: 1 })).concat([{ p: [[-0.3, 0.2], [-0.14, 0.02], [0, 0.14], [0.1, 0.06], [0.2, 0.2]], w: 0.7 }]),
|
|
43
|
+
gear: () => { const pts = []; for (let i = 0; i <= 64; i++) { const a = i / 64 * Math.PI * 2, r = (Math.floor(i / 4) % 2) ? 0.3 : 0.4; pts.push([Math.cos(a) * r, Math.sin(a) * r]); }
|
|
44
|
+
return [{ p: pts }, { p: A(0, 0, 0.12, 0.12, 0, Math.PI * 2.1, 16) }]; },
|
|
45
|
+
target: () => [{ p: A(0, 0.04, 0.4, 0.4, -Math.PI / 2, Math.PI * 1.55, 32) }, { p: A(0, 0.04, 0.25, 0.25, -Math.PI / 2, Math.PI * 1.55, 24) }, { p: A(0, 0.04, 0.09, 0.09, 0, Math.PI * 2, 12), fill: A(0, 0.04, 0.09, 0.09, 0, Math.PI * 2, 12) },
|
|
46
|
+
{ p: [[0.46, -0.42], [0.02, 0.02]], o: 1 }, { p: [[0.3, -0.42], [0.46, -0.42], [0.46, -0.26]], o: 1 }],
|
|
47
|
+
clock: () => [{ p: A(0, 0, 0.4, 0.4, -Math.PI / 2, Math.PI * 1.55, 32) }, { p: [[0, -0.26], [0, 0], [0.18, 0.1]] }],
|
|
48
|
+
chart: () => [{ p: [[-0.42, -0.42], [-0.42, 0.38], [0.44, 0.38]] }, { p: [[-0.32, 0.22], [-0.12, 0.04], [0.06, 0.12], [0.34, -0.26]], o: 1 }, { p: [[0.18, -0.26], [0.34, -0.26], [0.34, -0.1]], o: 1 }],
|
|
49
|
+
};
|
|
50
|
+
let B, keys, items, OFF, lastEnd, band;
|
|
51
|
+
const halo = (c, src, W, H, col, r) => { // 透明底:墨色外描一圈白边(8 个方向各压一次剪影)
|
|
52
|
+
const s2 = UI.scratch('clip_y3halo', W, H), g = s2.getContext('2d'); g.globalCompositeOperation = 'source-over'; g.clearRect(0, 0, W, H);
|
|
53
|
+
g.drawImage(src, 0, 0); g.globalCompositeOperation = 'source-in'; g.fillStyle = col; g.fillRect(0, 0, W, H); g.globalCompositeOperation = 'source-over';
|
|
54
|
+
for (let i = 0; i < 8; i++) { const a = i / 8 * Math.PI * 2; c.drawImage(s2, Math.cos(a) * r, Math.sin(a) * r); }
|
|
55
|
+
c.drawImage(src, 0, 0);
|
|
56
|
+
};
|
|
57
|
+
// 折行但各行等长:先按栏宽折出 n 行,再找「仍是 n 行」的最窄宽度重折(不留一个字孤零零挂在第二行)
|
|
58
|
+
const balance = (g, text, maxW, font) => { const n = TY.wrap(g, text, maxW, font).length; if (n < 2) return [text];
|
|
59
|
+
let lo = maxW * 0.4, hi = maxW; for (let k = 0; k < 14; k++) { const m = (lo + hi) / 2; if (TY.wrap(g, text, m, font).length > n) lo = m; else hi = m; } return TY.wrap(g, text, hi, font); };
|
|
60
|
+
// 手绘矩形:先把四条边各加密成多点,再加抖动,角还是角(直接对 4 个角点做 Catmull-Rom 会画成一个圆团)
|
|
61
|
+
const rectPts = (x, y, w, h) => { const P = [[x, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y + 3]], out = [];
|
|
62
|
+
for (let i = 0; i < 4; i++) for (let k = 0; k < 12; k++) out.push([lerp(P[i][0], P[i + 1][0], k / 12), lerp(P[i][1], P[i + 1][1], k / 12)]); out.push(P[4]); return out; };
|
|
63
|
+
function layout(ctx, f) {
|
|
64
|
+
const { W, H, u, FPS, safe } = ctx, pad = (ctx.portrait ? 80 : 150) * u, lw = 6.5 * u, g = document.createElement('canvas').getContext('2d');
|
|
65
|
+
const cues = ctx.cues, next = q => { const i = cues.indexOf(q); const n = cues.slice(i + 1).find(z => z.at > q.at + 1e-6); return n ? n.at : ctx.dur; };
|
|
66
|
+
const hasDraw = cues.some(q => q.kind === 'draw'), hasImg = cues.some(q => q.kind === 'image' && ctx.IMG[q.image]);
|
|
67
|
+
// 栏:横屏有图就左文右图;竖屏有 draw 时图标排在右侧窄栏、和它说的那条字并排(截图仍整宽)
|
|
68
|
+
const S = (ctx.portrait ? 250 : 210) * u * f, gap = 70 * u * f;
|
|
69
|
+
const two = ctx.portrait ? hasDraw : (hasDraw || hasImg);
|
|
70
|
+
const colP = !two ? null : ctx.portrait ? { x: W - pad - S, w: S } : { x: W * 0.55, w: W * 0.45 - pad };
|
|
71
|
+
const nPt = cues.filter(q => q.kind === 'point').length, split = !ctx.portrait && !two && nPt >= 5 ? Math.ceil(nPt / 2) : 0; // 横屏纯文字、要点 ≥5 条:分左右两栏写
|
|
72
|
+
const colT = { x: pad, w: split ? W / 2 - pad - 40 * u : !two ? W - 2 * pad : ctx.portrait ? colP.x - gap - pad : W * 0.5 - pad };
|
|
73
|
+
let kPt = 0, col0 = null;
|
|
74
|
+
// 同一份清单的要点用同一个字号:取「放进一行」要缩到的最小那个(最小 0.72),还放不下的那条再折行
|
|
75
|
+
const pSize = Math.min(74 * u * f, ...cues.filter(q => q.kind === 'point').map(q => { const r = TY.fit(g, q.text || '', colT.w - 80 * u, 74 * u * f, FONT, { maxLines: 1, min: 0.72, soft: true }); return r.ok ? r.size : 74 * u * f * 0.72; }));
|
|
76
|
+
const B = DG.board({ ink: INK, lw, speed: 2600 * u, font: `"${FONT}"` }), items = [];
|
|
77
|
+
const top = (ctx.portrait ? 240 : 130) * u + safe.top;
|
|
78
|
+
let yT = top, yP = top, lastText = null, lastDraw = null; // 文字流 / 图流的当前 y
|
|
79
|
+
const start = q => B.at(Math.max(0, q.at - 1 / FPS)); // 第一笔在 at 这一帧可见
|
|
80
|
+
const capSz = (ctx.portrait ? 52 : 50) * u * Math.max(0.85, Math.min(1.1, f)), capRight = !ctx.portrait; // 横屏说明写在图标右边,竖屏写在下面
|
|
81
|
+
for (const q of cues) {
|
|
82
|
+
if (q.kind === 'title' || q.kind === 'point') {
|
|
83
|
+
const title = q.kind === 'title', indent = title ? 0 : 80 * u, colW = title ? W - 2 * pad : colT.w;
|
|
84
|
+
const ft = title ? TY.fit(g, q.text || '', colW - indent, 112 * u * f, FONT, { maxLines: 1, min: 0.72, soft: true }) : { size: pSize, lines: TY.wrap(g, q.text || '', colW - indent, `${pSize}px "${FONT}"`) }; // 标题先缩字号放一行,放不下再折行
|
|
85
|
+
const size = ft.size, lines = ft.lines.length > 1 ? balance(g, q.text || '', colW - indent, `${size}px "${FONT}"`) : ft.lines;
|
|
86
|
+
const n = [...(q.text || '')].length, room = Math.max(0.35, next(q) - q.at - 0.15 - (title ? 0 : 0.15)), rate = Math.max(title ? 7.5 : 16, n / room);
|
|
87
|
+
if (title) yT = Math.max(yT, yP);
|
|
88
|
+
else { if (col0 == null) col0 = yT; if (split && kPt === split) { colT.x = W / 2 + 40 * u; yT = col0; } kPt++; }
|
|
89
|
+
let y = yT + size * (title ? 1.1 : 1.0);
|
|
90
|
+
start(q);
|
|
91
|
+
if (!title) B.line(DG.ellipsePts(colT.x + 26 * u, y - size * 0.32, 15 * u, 15 * u, -1.6, 1.05, 18), { w: lw * 0.9, col: ORANGE, speed: 9000 * u, min: 0.08, gap: 0.02 });
|
|
92
|
+
const x = (title ? pad : colT.x) + indent, it = { q, kind: q.kind, x, y0: y - size, lines, size };
|
|
93
|
+
lines.forEach((ln, k) => B.text(ln, x, y + k * size * 1.3, size, { col: INK, rate }));
|
|
94
|
+
g.font = `${size}px "${FONT}"`; it.w = Math.max(...lines.map(l => g.measureText(l).width)); it.y1 = y + (lines.length - 1) * size * 1.3 + size * 0.25;
|
|
95
|
+
yT = it.y1 + (title ? 60 : 44) * u * f; items.push(it);
|
|
96
|
+
if (title) yP = Math.max(yP, yT); else lastText = it;
|
|
97
|
+
} else if (q.kind === 'draw') {
|
|
98
|
+
const icon = (q.data && q.data.icon) || '', mk = ICONS[icon];
|
|
99
|
+
if (!mk && icon) console.warn(`y3 draw:没有图标「${icon}」,可用:${Object.keys(ICONS).join(' ')}(只写说明字)`);
|
|
100
|
+
// 和刚写的那条字并排(那条字还没配图、且在图流下面);否则接在图流下面
|
|
101
|
+
const pair = lastText && !lastText.pic && lastText.y0 >= yP - 1;
|
|
102
|
+
const y0 = two ? (pair ? lastText.y0 + (lastText.y1 - lastText.y0) / 2 - S / 2 : yP) : yT;
|
|
103
|
+
const x0 = two ? colP.x : pad, cx = x0 + S / 2, cy = Math.max(y0, two ? yP : y0) + S / 2;
|
|
104
|
+
if (pair) lastText.pic = true;
|
|
105
|
+
const prev = lastDraw && !(q.data && q.data.arrow === false) ? lastDraw : null;
|
|
106
|
+
start(q);
|
|
107
|
+
const room = Math.max(0.5, next(q) - q.at - 0.15), strokes = mk ? mk() : [];
|
|
108
|
+
const P = pts => pts.map(([x, y]) => [cx + x * S, cy + y * S]);
|
|
109
|
+
const capN = [...(q.text || '')].length, arrowL = prev ? Math.max(0, cy - S / 2 - prev.y1) : 0;
|
|
110
|
+
const len = strokes.reduce((s, k) => s + (k.p ? DG.len(P(k.p)) : 0), 0) + arrowL;
|
|
111
|
+
const budget = clamp(room - (capN ? Math.max(0.3, capN / 16) : 0) - 0.1, 0.4, 1.4), sp = Math.max(2600 * u, len / budget);
|
|
112
|
+
if (prev && arrowL > 30 * u) { const ax = prev.cx, ay0 = prev.y1 + 8 * u, ay1 = cy - S / 2 - 8 * u; // 箭头:上一个图标 → 这一个(往下)
|
|
113
|
+
B.line([[ax, ay0], [ax + 6 * u, lerp(ay0, ay1, 0.5)], [ax, ay1]], { col: ORANGE, w: lw, speed: sp });
|
|
114
|
+
const h = Math.min(20 * u, (ay1 - ay0) * 0.4); B.line([[ax - h, ay1 - h], [ax, ay1], [ax + h, ay1 - h]], { col: ORANGE, w: lw, speed: sp, smooth: false }); }
|
|
115
|
+
for (const k of strokes) {
|
|
116
|
+
if (k.seg) { B.segment(P([k.seg[0]])[0], P([k.seg[1]])[0], { w: 2.5 * u, dur: 0.05, gap: 0.008 }); continue; }
|
|
117
|
+
const pts = P(k.p);
|
|
118
|
+
if (k.paper) B.fill(U.poly(pts), BOARD, 0.01);
|
|
119
|
+
B.line(pts, { col: k.o ? ORANGE : INK, w: lw * (k.w || 1), speed: sp * (k.fast ? 2 : 1), gap: 0.015 });
|
|
120
|
+
if (k.fill) B.fill(U.poly(P(k.fill)), WASH);
|
|
121
|
+
}
|
|
122
|
+
B.at(B.cur + 0.02);
|
|
123
|
+
const capMax = capRight ? W - pad - (x0 + S + 24 * u) : W - 2 * pad, cs = q.text ? TY.fit(g, q.text, capMax, capSz, FONT, { maxLines: 1 }).size : capSz; // 说明字放不下先缩字号
|
|
124
|
+
g.font = `${cs}px "${FONT}"`; const cw = q.text ? g.measureText(q.text).width : 0, crate = Math.max(16, capN / Math.max(0.3, room - (B.cur - q.at)));
|
|
125
|
+
const capX = capRight ? x0 + S + 24 * u : clamp(cx, pad + cw / 2, W - pad - cw / 2), capY = capRight ? cy + cs * 0.35 : cy + S / 2 + cs * 1.05; // 竖屏图标在右侧窄栏:居中的说明字夹回画内
|
|
126
|
+
if (q.text) B.text(q.text, capX, capY, cs, { col: INK, align: capRight ? 'left' : 'center', rate: crate });
|
|
127
|
+
const it = { q, kind: 'draw', x: x0, w: S, cx, y0: cy - S / 2, y1: cy + S / 2 + (capRight ? 0 : cs * 1.3),
|
|
128
|
+
hx: capRight ? x0 : Math.min(x0, capX - cw / 2), hw: capRight ? S + 24 * u + cw : Math.max(x0 + S, capX + cw / 2) - Math.min(x0, capX - cw / 2) };
|
|
129
|
+
items.push(it); lastDraw = it;
|
|
130
|
+
if (two) { yP = it.y1 + gap; if (!ctx.portrait) yT = Math.max(yT, pair ? it.y1 + gap * 0.5 : yT); else yT = Math.max(yT, it.y1 + gap * 0.6); }
|
|
131
|
+
else yT = it.y1 + gap;
|
|
132
|
+
} else if (q.kind === 'image') {
|
|
133
|
+
const im = ctx.IMG[q.image]; if (!im) continue;
|
|
134
|
+
// 右栏的图只用剩下的高度,不让相机为右栏往下走、把左栏的字推出画
|
|
135
|
+
const inCol = two && !ctx.portrait, y = (inCol ? yP : Math.max(yT, yP)) + 30 * u, box = inCol ? colP : { x: pad, w: W - 2 * pad };
|
|
136
|
+
const f2 = CLIP.fit(im.width, im.height, box.x, y, box.w, inCol ? Math.max(H * 0.2, Math.min(H * 0.5 * Math.max(0.8, f), H - safe.bottom - 80 * u - y)) : H * (ctx.portrait ? 0.32 : 0.45) * Math.max(0.8, f)), it = { q, kind: 'image', x: f2.x, y0: f2.y, w: f2.w, y1: f2.y + f2.h };
|
|
137
|
+
const zz = DG.zigzag(f2.x - 20 * u, f2.y - 10 * u, f2.w + 40 * u, f2.h + 20 * u, 7), cum = DG.cum(zz), rowH = (f2.h + 20 * u) / 7;
|
|
138
|
+
start(q);
|
|
139
|
+
const s = B.push({ kind: 'custom', t0: B.cur, t1: B.cur + Math.min(1.0, Math.max(0.4, next(q) - q.at - 0.5)), col: INK,
|
|
140
|
+
draw(gg, p) { gg.save(); if (p < 1) gg.clip(DG.revealMask(zz, cum, cum[cum.length - 1] * p, rowH * 2.6)); gg.drawImage(im, f2.x, f2.y, f2.w, f2.h); gg.restore(); return p < 1 ? DG.pointAt(zz, cum, cum[cum.length - 1] * p) : null; },
|
|
141
|
+
start() { return zz[0]; }, end() { return zz[zz.length - 1]; } });
|
|
142
|
+
B.at(s.t1 + 0.02);
|
|
143
|
+
B.line(rectPts(f2.x - 10 * u, f2.y - 10 * u, f2.w + 20 * u, f2.h + 20 * u), { w: lw * 0.8, speed: 9000 * u, amp: 1.0 * u });
|
|
144
|
+
yP = f2.y + f2.h + 60 * u; if (!inCol) yT = yP; items.push(it); lastDraw = null;
|
|
145
|
+
} else if (q.kind === 'highlight') {
|
|
146
|
+
const d = q.data || {}, written = items.filter(z => z.q.at <= q.at), kind = d.target || (d.index != null ? 'point' : null);
|
|
147
|
+
const pool = kind ? written.filter(z => z.kind === kind) : written, all = kind ? items.filter(z => z.kind === kind) : items;
|
|
148
|
+
const it = d.index != null ? all[d.index] : pool[pool.length - 1];
|
|
149
|
+
if (!it) { console.warn(`y3 highlight at=${q.at}:找不到要圈的条目(index=${d.index} target=${d.target || 'point'})`); continue; }
|
|
150
|
+
start(q);
|
|
151
|
+
const hx = it.hx ?? it.x, hw = it.hw ?? it.w;
|
|
152
|
+
if (d.style === 'underline') B.line([[hx - 10 * u, it.y1 + 12 * u], [hx + hw / 2, it.y1 + 18 * u], [hx + hw + 10 * u, it.y1 + 10 * u]], { col: ORANGE, w: lw * 1.3, speed: 3000 * u });
|
|
153
|
+
else B.line(DG.ellipsePts(hx + hw / 2, (it.y0 + it.y1) / 2, hw * 0.56 + 30 * u, (it.y1 - it.y0) * 0.62 + 24 * u, -2.9, 1.08, 44), { col: ORANGE, w: lw, speed: 3600 * u, amp: 1.6 * u });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const bottom = Math.max(...items.map(z => z.y1), top);
|
|
157
|
+
return { B, items, bottom, top, pad };
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
fonts: [FONT],
|
|
161
|
+
safe: true,
|
|
162
|
+
init(ctx) {
|
|
163
|
+
const { W, H, u, safe } = ctx, p = ctx.pal;
|
|
164
|
+
if (p) { INK = p.ink; ORANGE = p.accent; WASH = PAL.alpha(p.accent, 0.22); BOARD = p.bg; } else { INK = '#0A0503'; ORANGE = '#EF7226'; WASH = 'rgba(239,114,38,.22)'; BOARD = '#FBFBFB'; }
|
|
165
|
+
band = { y0: safe.top, y1: H - safe.bottom }; band.h = band.y1 - band.y0;
|
|
166
|
+
// 字号系数 f 从 1.2 往下试:内容少就放大撑满版面,内容多就缩(最小 0.72),还放不下才让相机往下走
|
|
167
|
+
let L; for (let f = 1.2; f >= 0.7; f -= 0.04) { L = layout(ctx, f); if (L.bottom + 70 * u <= band.y1) break; } // 内容少就放大(最大 1.2)把版面撑满
|
|
168
|
+
({ B, items } = L);
|
|
169
|
+
OFF = [W + 140 * u, H + 280 * u];
|
|
170
|
+
lastEnd = Math.max(0, ...B.S.map(s => s.t1));
|
|
171
|
+
// 相机:内容超出可用带就在下一条开写前平移过去(sineInOut);最后有空就拉远看全图(只拉进 safe 带)
|
|
172
|
+
keys = [{ t: 0, x: W / 2, y: H / 2, z: 1 }]; let cy = H / 2, panned = false;
|
|
173
|
+
for (const it of items) {
|
|
174
|
+
const need = it.y1 + 70 * u - (cy + band.y1 - H / 2); if (need <= 0) continue;
|
|
175
|
+
panned = true; cy += need + band.h * 0.12; const t1 = Math.max(keys[keys.length - 1].t + 0.2, it.q.at), t0 = Math.max(keys[keys.length - 1].t + 0.01, t1 - 0.6);
|
|
176
|
+
keys.push({ t: t0, x: W / 2, y: keys[keys.length - 1].y, z: 1 }); keys.push({ t: t1, x: W / 2, y: cy, z: 1, ease: MO.sineInOut });
|
|
177
|
+
}
|
|
178
|
+
const y0c = Math.min(L.top, ...items.map(z => z.y0)) - 50 * u, y1c = L.bottom + 40 * u, total = y1c - y0c;
|
|
179
|
+
const zEnd = Math.min(0.94, band.h / total * 0.96);
|
|
180
|
+
// 收尾拉远:最后一笔后有 ≥0.6s 才拉;相机走过了的话,就算时间紧也在最后 0.8s 拉(不然末帧标题被切在顶上)
|
|
181
|
+
const tz0 = Math.max(keys[keys.length - 1].t + 0.01, panned ? Math.min(lastEnd + 0.1, ctx.dur - 0.8) : lastEnd + 0.1);
|
|
182
|
+
if (ctx.dur - tz0 >= 0.6) {
|
|
183
|
+
const prev = keys[keys.length - 1]; keys.push({ ...prev, t: tz0, ease: undefined });
|
|
184
|
+
const midW = (y0c + y1c) / 2, ty = midW - ((band.y0 + band.y1) / 2 - H / 2) / zEnd; // 全图中心落在 safe 带中心
|
|
185
|
+
keys.push({ t: Math.min(ctx.dur - 0.15, tz0 + 0.8), x: W / 2, y: ty, z: zEnd, ease: MO.sineInOut });
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
draw(c, t, ctx) {
|
|
189
|
+
const { W, H, u } = ctx, cam = CAM.at(keys, t);
|
|
190
|
+
const g = ctx.alpha ? (() => { const b = UI.scratch('clip_y3buf', W, H), x = b.getContext('2d'); x.setTransform(1, 0, 0, 1, 0, 0); x.clearRect(0, 0, W, H); return x; })() : c;
|
|
191
|
+
if (!ctx.alpha) { c.fillStyle = BOARD; c.fillRect(0, 0, W, H); }
|
|
192
|
+
g.save(); CAM.apply(g, cam); const { tip, col } = B.draw(g, t); g.restore();
|
|
193
|
+
const pen = penAt(t, cam, tip, ctx);
|
|
194
|
+
if (pen) DG.pen(g, pen[0], pen[1], col || B.penColor(t), Math.sin(t * 7) * 0.03);
|
|
195
|
+
if (ctx.alpha) halo(c, g.canvas, W, H, 'rgba(255,255,255,0.92)', 4 * u);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
// 笔的位置(屏幕):在画 → 笔尖;空档短 → 沿弧线滑;空档长 → 退出右下再进来;画完最后一笔 → 0.35s 退场(返回 null = 不画)
|
|
199
|
+
function penAt(t, cam, tip, ctx) {
|
|
200
|
+
if (tip) return CAM.toScreen(cam, tip[0], tip[1]);
|
|
201
|
+
const hs = B.S.filter(s => s.kind !== 'pop' && s.kind !== 'fill' && (s.kind !== 'custom' || s.handed !== false));
|
|
202
|
+
let prev = null, next = null; for (const s of hs) { if (s.t1 <= t) prev = s; else if (s.t0 > t && !next) next = s; }
|
|
203
|
+
if (prev && next && next.t0 - prev.t1 <= 0.6) return B.penAt(t, cam, null);
|
|
204
|
+
const end = s => B.penAt(s.t1, cam, null); // 上一笔终点(屏幕)
|
|
205
|
+
if (next && next.t0 - t <= 0.3) { const b = CAM.toScreen(cam, ...(next.kind === 'custom' ? next.start() : next.kind === 'line' ? next.pts[0] : [next.x, next.y - next.size * 0.3])), q = MO.cubicOut(1 - (next.t0 - t) / 0.3); return [lerp(OFF[0], b[0], q), lerp(OFF[1], b[1], q)]; }
|
|
206
|
+
if (prev && t - prev.t1 < 0.35) { const a = end(prev), q = MO.cubicIn((t - prev.t1) / 0.35); return [lerp(a[0], OFF[0], q), lerp(a[1], OFF[1], q)]; }
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
})();
|