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