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