dsh-drawai 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4154 -0
- package/README.md +49 -11
- package/lib/audio-media.js +150 -0
- package/lib/audio-service.js +66 -0
- package/lib/canvas-format.js +134 -0
- package/lib/client.js +5090 -184
- package/lib/comfyui-default-workflow.mjs +1825 -0
- package/lib/comfyui-workflow.mjs +426 -0
- package/lib/document-commands.js +12 -0
- package/lib/document-core.js +29 -0
- package/lib/drawing-runner.js +6 -4
- package/lib/drawing-tasks.js +26 -0
- package/lib/ffmpeg-manager.js +1056 -0
- package/lib/imagegen.mjs +596 -0
- package/lib/index.js +1762 -129
- package/lib/media.js +26 -13
- package/lib/mgstyle-core.js +75 -0
- package/lib/motion/apply.js +181 -0
- package/lib/motion/asset-pack-lock.js +10 -0
- package/lib/motion/asset-pack.js +335 -0
- package/lib/motion/assets.js +291 -0
- package/lib/motion/audio.js +103 -0
- package/lib/motion/audiolevel.js +63 -0
- package/lib/motion/audiomix.js +144 -0
- package/lib/motion/beats.js +338 -0
- package/lib/motion/builtin-assets.js +62 -0
- package/lib/motion/canvas-spec.js +442 -0
- package/lib/motion/capture.js +95 -0
- package/lib/motion/cards.js +80 -0
- package/lib/motion/collect.js +449 -0
- package/lib/motion/delivery.js +140 -0
- package/lib/motion/edge-speech.py +90 -0
- package/lib/motion/entry.jsx +73 -0
- package/lib/motion/export.js +200 -0
- package/lib/motion/ffmpeg.js +43 -0
- package/lib/motion/fonts.js +71 -0
- package/lib/motion/jobs.js +117 -0
- package/lib/motion/json.js +4 -0
- package/lib/motion/media-catalog.js +59 -0
- package/lib/motion/mgstyles/BRIEF-TEMPLATE.md +63 -0
- package/lib/motion/mgstyles/briefs/01-flat-vector.md +104 -0
- package/lib/motion/mgstyles/briefs/02-line-art.md +97 -0
- package/lib/motion/mgstyles/briefs/03-isometric.md +108 -0
- package/lib/motion/mgstyles/briefs/05-cel-boil.md +145 -0
- package/lib/motion/mgstyles/briefs/06-collage.md +149 -0
- package/lib/motion/mgstyles/briefs/07-liquid.md +106 -0
- package/lib/motion/mgstyles/briefs/08-morph.md +111 -0
- package/lib/motion/mgstyles/briefs/09-bauhaus.md +150 -0
- package/lib/motion/mgstyles/briefs/10-synthwave.md +151 -0
- package/lib/motion/mgstyles/briefs/12-aurora-glass.md +147 -0
- package/lib/motion/mgstyles/briefs/18-hanazi.md +140 -0
- package/lib/motion/mgstyles/briefs/19-paperclip.md +156 -0
- package/lib/motion/mgstyles/briefs/20-pixel.md +118 -0
- package/lib/motion/mgstyles/briefs/22-hud.md +118 -0
- package/lib/motion/mgstyles/catalog.d/01-flat-vector.json +34 -0
- package/lib/motion/mgstyles/catalog.d/02-line-art.json +34 -0
- package/lib/motion/mgstyles/catalog.d/03-isometric.json +35 -0
- package/lib/motion/mgstyles/catalog.d/05-cel-boil.json +28 -0
- package/lib/motion/mgstyles/catalog.d/06-collage.json +28 -0
- package/lib/motion/mgstyles/catalog.d/07-liquid.json +28 -0
- package/lib/motion/mgstyles/catalog.d/08-morph.json +28 -0
- package/lib/motion/mgstyles/catalog.d/09-bauhaus.json +28 -0
- package/lib/motion/mgstyles/catalog.d/10-synthwave.json +28 -0
- package/lib/motion/mgstyles/catalog.d/12-aurora-glass.json +28 -0
- package/lib/motion/mgstyles/catalog.d/18-hanazi.json +28 -0
- package/lib/motion/mgstyles/catalog.d/19-paperclip.json +28 -0
- package/lib/motion/mgstyles/catalog.d/20-pixel.json +28 -0
- package/lib/motion/mgstyles/catalog.d/22-hud.json +28 -0
- package/lib/motion/mgstyles/catalog.json +1356 -0
- package/lib/motion/mgstyles/upstream/README.md +147 -0
- package/lib/motion/mgstyles/upstream/README.zh-CN.md +147 -0
- package/lib/motion/mgstyles/upstream/demos/01-flat-vector/cues.json +1 -0
- package/lib/motion/mgstyles/upstream/demos/01-flat-vector/index.html +712 -0
- package/lib/motion/mgstyles/upstream/demos/02-line-art/cues.json +331 -0
- package/lib/motion/mgstyles/upstream/demos/02-line-art/film.js +676 -0
- package/lib/motion/mgstyles/upstream/demos/02-line-art/index.html +18 -0
- package/lib/motion/mgstyles/upstream/demos/02-line-art/tools/dump_timing.mjs +26 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/cues.json +109 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/index.html +25 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/js/anim.js +82 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/js/main.js +219 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/js/night5.js +180 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/js/render.js +202 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/js/title.js +167 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/js/title5.js +294 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/js/ui.js +141 -0
- package/lib/motion/mgstyles/upstream/demos/03-isometric/js/world.js +1399 -0
- package/lib/motion/mgstyles/upstream/demos/05-cel-boil/cues.json +157 -0
- package/lib/motion/mgstyles/upstream/demos/05-cel-boil/index.html +34 -0
- package/lib/motion/mgstyles/upstream/demos/05-cel-boil/js/engine.js +326 -0
- package/lib/motion/mgstyles/upstream/demos/05-cel-boil/js/scene.js +2036 -0
- package/lib/motion/mgstyles/upstream/demos/06-collage/assets/gen/layout.json +157 -0
- package/lib/motion/mgstyles/upstream/demos/06-collage/cues.json +80 -0
- package/lib/motion/mgstyles/upstream/demos/06-collage/index.html +889 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/cues.json +100 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/index.html +108 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/js/engine.js +153 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/js/links.js +64 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/js/lookdev.js +46 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/js/scene.js +963 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/js/shaders.js +647 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/js/textsdf.js +114 -0
- package/lib/motion/mgstyles/upstream/demos/07-liquid/js/type.js +50 -0
- package/lib/motion/mgstyles/upstream/demos/08-morph/cues.json +131 -0
- package/lib/motion/mgstyles/upstream/demos/08-morph/glyphs.json +1 -0
- package/lib/motion/mgstyles/upstream/demos/08-morph/index.html +901 -0
- package/lib/motion/mgstyles/upstream/demos/09-bauhaus/cues.json +270 -0
- package/lib/motion/mgstyles/upstream/demos/09-bauhaus/film.js +697 -0
- package/lib/motion/mgstyles/upstream/demos/09-bauhaus/index.html +12 -0
- package/lib/motion/mgstyles/upstream/demos/10-synthwave/cues.json +44 -0
- package/lib/motion/mgstyles/upstream/demos/10-synthwave/index.html +1028 -0
- package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/cues.json +67 -0
- package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/index.html +16 -0
- package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/main.js +967 -0
- package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/shaders.js +468 -0
- package/lib/motion/mgstyles/upstream/demos/18-hanazi/cues.json +117 -0
- package/lib/motion/mgstyles/upstream/demos/18-hanazi/index.html +541 -0
- package/lib/motion/mgstyles/upstream/demos/19-paperclip/assets/countries-110m.json +1 -0
- package/lib/motion/mgstyles/upstream/demos/19-paperclip/assets/stickers/meta.json +34 -0
- package/lib/motion/mgstyles/upstream/demos/19-paperclip/cues.json +285 -0
- package/lib/motion/mgstyles/upstream/demos/19-paperclip/index.html +599 -0
- package/lib/motion/mgstyles/upstream/demos/20-pixel/cues.json +42 -0
- package/lib/motion/mgstyles/upstream/demos/20-pixel/film.js +914 -0
- package/lib/motion/mgstyles/upstream/demos/20-pixel/index.html +18 -0
- package/lib/motion/mgstyles/upstream/demos/22-hud/cues.json +108 -0
- package/lib/motion/mgstyles/upstream/demos/22-hud/index.html +125 -0
- package/lib/motion/mgstyles/upstream/demos/22-hud/js/holo.js +275 -0
- package/lib/motion/mgstyles/upstream/demos/22-hud/js/hud.js +855 -0
- package/lib/motion/mgstyles/upstream/demos/22-hud/js/post.js +150 -0
- package/lib/motion/mgstyles/upstream/demos/22-hud/js/util.js +65 -0
- package/lib/motion/mgstyles/upstream/demos/22-hud/js/v5.js +142 -0
- package/lib/motion/mgstyles/upstream/harness/render.mjs +319 -0
- package/lib/motion/mgstyles/upstream/manifest.json +269 -0
- package/lib/motion/mgstyles/upstream/prompts/01-flat-vector.md +98 -0
- package/lib/motion/mgstyles/upstream/prompts/02-line-art.md +95 -0
- package/lib/motion/mgstyles/upstream/prompts/03-isometric.md +97 -0
- package/lib/motion/mgstyles/upstream/prompts/05-cel-boil.md +95 -0
- package/lib/motion/mgstyles/upstream/prompts/06-collage.md +95 -0
- package/lib/motion/mgstyles/upstream/prompts/07-liquid.md +98 -0
- package/lib/motion/mgstyles/upstream/prompts/08-morph.md +95 -0
- package/lib/motion/mgstyles/upstream/prompts/09-bauhaus.md +96 -0
- package/lib/motion/mgstyles/upstream/prompts/10-synthwave.md +95 -0
- package/lib/motion/mgstyles/upstream/prompts/12-aurora-glass.md +96 -0
- package/lib/motion/mgstyles/upstream/prompts/18-hanazi.md +95 -0
- package/lib/motion/mgstyles/upstream/prompts/19-paperclip.md +96 -0
- package/lib/motion/mgstyles/upstream/prompts/20-pixel.md +96 -0
- package/lib/motion/mgstyles/upstream/prompts/22-hud.md +95 -0
- package/lib/motion/mgstyles/upstream/rubric.md +51 -0
- package/lib/motion/mgstyles/upstream/template.md +36 -0
- package/lib/motion/pixelqa.js +198 -0
- package/lib/motion/preview.js +97 -0
- package/lib/motion/project.js +395 -0
- package/lib/motion/qa.js +290 -0
- package/lib/motion/reference-media.jsx +10 -0
- package/lib/motion/render-settings.js +112 -0
- package/lib/motion/render.js +161 -0
- package/lib/motion/runtime/package-lock.json +3058 -0
- package/lib/motion/runtime/package.json +16 -0
- package/lib/motion/scene-kit.js +183 -0
- package/lib/motion/sfx.js +232 -0
- package/lib/motion/shotcraft/ClipCard.tsx +204 -0
- package/lib/motion/shotcraft/catalog.json +2461 -0
- package/lib/motion/shotcraft/upstream/SKILL.md +286 -0
- package/lib/motion/shotcraft/upstream/assets/lib/Caption.tsx +54 -0
- package/lib/motion/shotcraft/upstream/assets/lib/ClipCard.tsx +200 -0
- package/lib/motion/shotcraft/upstream/assets/lib/DigitRoll.tsx +45 -0
- package/lib/motion/shotcraft/upstream/assets/lib/FlashCut.tsx +20 -0
- package/lib/motion/shotcraft/upstream/assets/lib/FlatPanel.tsx +65 -0
- package/lib/motion/shotcraft/upstream/assets/lib/PageCam.tsx +151 -0
- package/lib/motion/shotcraft/upstream/assets/lib/VerticalTicker.tsx +129 -0
- package/lib/motion/shotcraft/upstream/assets/lib/helpers/camera.tsx +75 -0
- package/lib/motion/shotcraft/upstream/assets/lib/helpers/motion.ts +40 -0
- package/lib/motion/shotcraft/upstream/assets/lib/helpers/rand.ts +11 -0
- package/lib/motion/shotcraft/upstream/assets/lib/helpers/shake.ts +10 -0
- package/lib/motion/shotcraft/upstream/assets/scripts/__pycache__/sfx-source.cpython-313.pyc +0 -0
- package/lib/motion/shotcraft/upstream/assets/scripts/build-readme-cover.mjs +49 -0
- package/lib/motion/shotcraft/upstream/assets/scripts/capture-template.mjs +210 -0
- package/lib/motion/shotcraft/upstream/assets/scripts/sfx-source.py +423 -0
- package/lib/motion/shotcraft/upstream/assets/scripts/smoke-render-demos.py +170 -0
- package/lib/motion/shotcraft/upstream/assets/scripts/test_sfx_source.py +157 -0
- package/lib/motion/shotcraft/upstream/demos/README.md +120 -0
- package/lib/motion/shotcraft/upstream/demos/_fixtures/Brand.tsx +112 -0
- package/lib/motion/shotcraft/upstream/demos/_fixtures/Fixtures.tsx +647 -0
- package/lib/motion/shotcraft/upstream/demos/_fixtures/Look.tsx +316 -0
- package/lib/motion/shotcraft/upstream/demos/_fixtures/Motion.tsx +82 -0
- package/lib/motion/shotcraft/upstream/demos/_fixtures/PageCam2D.tsx +115 -0
- package/lib/motion/shotcraft/upstream/demos/_fixtures/Polish.tsx +243 -0
- package/lib/motion/shotcraft/upstream/demos/_textures/live-layout.json +389 -0
- package/lib/motion/shotcraft/upstream/demos/camera/basic-3d-scene/Basic3DScene.tsx +250 -0
- package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashImpactReal.tsx +305 -0
- package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashZoomReal.tsx +246 -0
- package/lib/motion/shotcraft/upstream/demos/camera/cursor-flyover/CursorFlyover.tsx +390 -0
- package/lib/motion/shotcraft/upstream/demos/camera/depth-layer-moves/MultiplaneReal.tsx +138 -0
- package/lib/motion/shotcraft/upstream/demos/camera/graze-face-tour/GrazeFaceTour.tsx +542 -0
- package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/OverheadTabletopDrop.tsx +315 -0
- package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/TiltReveal.tsx +299 -0
- package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/DroneDiveLanding.tsx +173 -0
- package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/ExplodedView.tsx +348 -0
- package/lib/motion/shotcraft/upstream/demos/camera/steep-tilt-glide/SteepTiltGlide.tsx +366 -0
- package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx +279 -0
- package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/PullBackIsolation.tsx +184 -0
- package/lib/motion/shotcraft/upstream/demos/camera/terminal-3d/Terminal3D.tsx +390 -0
- package/lib/motion/shotcraft/upstream/demos/data/before-after-slider-scrub/BeforeAfterSliderScrub.tsx +155 -0
- package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/AxisRescaleShockV2.tsx +303 -0
- package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/OscilloscopeStreamV2.tsx +290 -0
- package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx +278 -0
- package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx +307 -0
- package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx +215 -0
- package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/BrakeReticleLock.tsx +299 -0
- package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx +232 -0
- package/lib/motion/shotcraft/upstream/demos/data/timeline-travel/TimelineTravel.tsx +290 -0
- package/lib/motion/shotcraft/upstream/demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx +273 -0
- package/lib/motion/shotcraft/upstream/demos/effects/aurora-bloom-bg-flip/AuroraBloomBgFlip.tsx +167 -0
- package/lib/motion/shotcraft/upstream/demos/effects/brand-frame-snap/BrandFrameSnap.tsx +536 -0
- package/lib/motion/shotcraft/upstream/demos/effects/fui-hud-moves/ReticleLockOn.tsx +164 -0
- package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/FlylineArc.tsx +175 -0
- package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx +197 -0
- package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx +241 -0
- package/lib/motion/shotcraft/upstream/demos/effects/icon-performance-moves/AttentionBounce.tsx +279 -0
- package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/AnimeImpact.tsx +230 -0
- package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/HitCounter.tsx +345 -0
- package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SheenSweepRetry.tsx +275 -0
- package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SpotlightSweepReveal.tsx +229 -0
- package/lib/motion/shotcraft/upstream/demos/effects/radial-ripple-phone-chips/RadialRipplePhoneChips.tsx +312 -0
- package/lib/motion/shotcraft/upstream/demos/effects/riso-print-hits/RisoMisregistrationHit.tsx +222 -0
- package/lib/motion/shotcraft/upstream/demos/effects/scan-bracket-sweep/ScanBracketSweep.tsx +295 -0
- package/lib/motion/shotcraft/upstream/demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.tsx +329 -0
- package/lib/motion/shotcraft/upstream/demos/effects/scanline-assemble-flyin/ScanlineAssembleFlyin.tsx +315 -0
- package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ImpactBurstKit.tsx +264 -0
- package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/KanadaPerspectiveSnap.tsx +260 -0
- package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ScoreSlam.tsx +263 -0
- package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/CornerSpotlightReveal.tsx +123 -0
- package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/GlowWakeSleepPanel.tsx +187 -0
- package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/SlideSpotlightPan.tsx +175 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx +305 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/DiagramCascadeBuild.tsx +350 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/PanelToCanvasMaterialize.tsx +352 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx +218 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx +297 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/glass-pill-dictation-typing/GlassPillDictationTyping.tsx +157 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx +218 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx +202 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/segmented-thumb-hero/SegmentedThumbHero.tsx +265 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx +457 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx +446 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/type-and-filter/TypeAndFilter.tsx +249 -0
- package/lib/motion/shotcraft/upstream/demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx +278 -0
- package/lib/motion/shotcraft/upstream/demos/opening/crane-rise-reveal/CraneRiseReveal.tsx +332 -0
- package/lib/motion/shotcraft/upstream/demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.tsx +358 -0
- package/lib/motion/shotcraft/upstream/demos/opening/fracture/Fracture.tsx +201 -0
- package/lib/motion/shotcraft/upstream/demos/opening/icon-field-colorize/IconFieldColorize.tsx +200 -0
- package/lib/motion/shotcraft/upstream/demos/opening/letterspace-materialize/LetterspaceMaterialize.tsx +208 -0
- package/lib/motion/shotcraft/upstream/demos/opening/magician-card-flourish/MagicianCardFlourish.tsx +341 -0
- package/lib/motion/shotcraft/upstream/demos/opening/orbit-ring-title-open/OrbitRingTitleOpen.tsx +389 -0
- package/lib/motion/shotcraft/upstream/demos/opening/spotlight-hero-card/SpotlightHeroCard.tsx +293 -0
- package/lib/motion/shotcraft/upstream/demos/opening/stroke-segment-build/StrokeSegmentBuild.tsx +183 -0
- package/lib/motion/shotcraft/upstream/demos/opening/text-as-mask/TextAsMask.tsx +245 -0
- package/lib/motion/shotcraft/upstream/demos/outro/grain-dissolve/GrainDissolve.tsx +222 -0
- package/lib/motion/shotcraft/upstream/demos/outro/logo-shrink-wordmark-lockup/LogoShrinkWordmarkLockup.tsx +223 -0
- package/lib/motion/shotcraft/upstream/demos/outro/neon-triple-marquee/NeonTripleMarquee.tsx +217 -0
- package/lib/motion/shotcraft/upstream/demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx +343 -0
- package/lib/motion/shotcraft/upstream/demos/outro/ui-strip-away-outro/UiStripAwayOutro.tsx +357 -0
- package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx +187 -0
- package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx +333 -0
- package/lib/motion/shotcraft/upstream/demos/rhythm/beat-cut-moves/PaparazziFlash.tsx +274 -0
- package/lib/motion/shotcraft/upstream/demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx +270 -0
- package/lib/motion/shotcraft/upstream/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx +430 -0
- package/lib/motion/shotcraft/upstream/demos/rhythm/quad-split-parallel-scenes/QuadSplitParallelScenes.tsx +252 -0
- package/lib/motion/shotcraft/upstream/demos/rhythm/rhythm-interrupt-moves/JumpCutPunchIn.tsx +106 -0
- package/lib/motion/shotcraft/upstream/demos/rhythm/sakuga-timing-shift/SakugaTimingShift.tsx +288 -0
- package/lib/motion/shotcraft/upstream/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx +362 -0
- package/lib/motion/shotcraft/upstream/demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx +168 -0
- package/lib/motion/shotcraft/upstream/demos/transition/bottom-push-stack-wipe/BottomPushStackWipe.tsx +310 -0
- package/lib/motion/shotcraft/upstream/demos/transition/card-flip-reveal/CardFlipReveal.tsx +261 -0
- package/lib/motion/shotcraft/upstream/demos/transition/card-flock-tumble/CardFlockTumble.tsx +459 -0
- package/lib/motion/shotcraft/upstream/demos/transition/circle-match-iris/CircleMatchIris.tsx +294 -0
- package/lib/motion/shotcraft/upstream/demos/transition/gradient-transition/GradientTransition.tsx +161 -0
- package/lib/motion/shotcraft/upstream/demos/transition/line-carry-transition/LineCarryTransition.tsx +248 -0
- package/lib/motion/shotcraft/upstream/demos/transition/mosaic-reframe/MosaicReframe.tsx +359 -0
- package/lib/motion/shotcraft/upstream/demos/transition/page-turn-transitions/BarnDoorSplit.tsx +227 -0
- package/lib/motion/shotcraft/upstream/demos/transition/print-texture-transitions/InkBleedReveal.tsx +162 -0
- package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/PortalWipeV2.tsx +248 -0
- package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/WhipBrakeReal.tsx +186 -0
- package/lib/motion/shotcraft/upstream/demos/transition/tear-streak-transitions/GlitchDisplace.tsx +355 -0
- package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/InvisibleCut.tsx +272 -0
- package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/LightLeakBurn.tsx +262 -0
- package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/VersusSlam.tsx +217 -0
- package/lib/motion/shotcraft/upstream/demos/transition/transition-travel/LetterformZoom.tsx +285 -0
- package/lib/motion/shotcraft/upstream/demos/transition/white-flash-logo-simplify-cut/WhiteFlashLogoSimplifyCut.tsx +205 -0
- package/lib/motion/shotcraft/upstream/demos/transition/wipe-transitions/BlindsSlice.tsx +178 -0
- package/lib/motion/shotcraft/upstream/demos/typography/blur-slide/BlurSlide.tsx +147 -0
- package/lib/motion/shotcraft/upstream/demos/typography/brace-expand/BraceExpand.tsx +143 -0
- package/lib/motion/shotcraft/upstream/demos/typography/brand-ink-open/BrandInkOpen.tsx +134 -0
- package/lib/motion/shotcraft/upstream/demos/typography/countdown-arc-scatter/CountdownArcScatter.tsx +217 -0
- package/lib/motion/shotcraft/upstream/demos/typography/flying-words/FlyingWords.tsx +198 -0
- package/lib/motion/shotcraft/upstream/demos/typography/glitch-cycle/GlitchCycle.tsx +221 -0
- package/lib/motion/shotcraft/upstream/demos/typography/gradient-word-sweep/GradientWordSweep.tsx +312 -0
- package/lib/motion/shotcraft/upstream/demos/typography/lead-word-zoom-assemble/LeadWordZoomAssemble.tsx +261 -0
- package/lib/motion/shotcraft/upstream/demos/typography/marker-underline-title/MarkerUnderlineTitle.tsx +121 -0
- package/lib/motion/shotcraft/upstream/demos/typography/outline-word-fill/OutlineWordFill.tsx +153 -0
- package/lib/motion/shotcraft/upstream/demos/typography/paper-title-card/PaperTitleCard.tsx +179 -0
- package/lib/motion/shotcraft/upstream/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx +140 -0
- package/lib/motion/shotcraft/upstream/demos/typography/pill-slot-cycle/PillSlotCycle.tsx +309 -0
- package/lib/motion/shotcraft/upstream/demos/typography/text-column-converge/TextColumnConverge.tsx +191 -0
- package/lib/motion/shotcraft/upstream/demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx +292 -0
- package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx +135 -0
- package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/SplitTextStagger.tsx +124 -0
- package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/TextOnPath.tsx +255 -0
- package/lib/motion/shotcraft/upstream/demos/typography/type-entrance-moves/LetterDropPhysics.tsx +173 -0
- package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/FontWeightPump.tsx +225 -0
- package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/KaraokeFillSync.tsx +141 -0
- package/lib/motion/shotcraft/upstream/demos/typography/typewriter-moves/TypewriterErrorRetype.tsx +137 -0
- package/lib/motion/shotcraft/upstream/demos/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.tsx +147 -0
- package/lib/motion/shotcraft/upstream/demos/typography/word-relay-filmstrip/WordRelayFilmstrip.tsx +409 -0
- package/lib/motion/shotcraft/upstream/demos/typography/word-relay-geometry/WordRelayGeometry.tsx +280 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/avatar-bracket-carousel/AvatarBracketCarousel.tsx +271 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/bezier-source-converge-merge/BezierSourceConvergeMerge.tsx +385 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/card-stack/CardStack.tsx +312 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/carousel-3d/Carousel3D.tsx +303 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/clipcard-looping/ClipCardLooping.tsx +77 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/cloner-depth-echo/ClonerDepthEcho.tsx +311 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/deck-deal-flyin/DeckDealFlyin.tsx +307 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/doc-park-left-pill-deal/DocParkLeftPillDeal.tsx +349 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/document-typewriter-reveal/DocumentTypewriterReveal.tsx +255 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/draw-svg-trace/DrawSvgTrace.tsx +244 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/element-body-moves/ContactShadowLift.tsx +217 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/integration-hub-map/IntegrationHubMap.tsx +652 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-reveal/ListReveal.tsx +153 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-stack-press/ListStackPress.tsx +238 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-forerun/NeonFrameForerun.tsx +324 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-orbit-drop/NeonFrameForerunOrbit.tsx +354 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx +101 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx +129 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx +297 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/radial-wave/RadialWave.tsx +57 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/research-card-stack-scroll/ResearchCardStackScroll.tsx +155 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/row-embed/RowEmbed.tsx +175 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/value-stagger-gradient/ValueStaggerGradient.tsx +153 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/BentoLightUp.tsx +146 -0
- package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/WireframeDrawOn.tsx +226 -0
- package/lib/motion/shotcraft/upstream/gallery/api/library.json +1 -0
- package/lib/motion/shotcraft/upstream/manifest.json +2715 -0
- package/lib/motion/shotcraft/upstream/references/aesthetic-rules.md +193 -0
- package/lib/motion/shotcraft/upstream/references/final-review.md +121 -0
- package/lib/motion/shotcraft/upstream/references/guided-free-creation.md +199 -0
- package/lib/motion/shotcraft/upstream/references/jianying-export.md +183 -0
- package/lib/motion/shotcraft/upstream/references/music-beat-sync.md +257 -0
- package/lib/motion/shotcraft/upstream/references/pipeline.md +382 -0
- package/lib/motion/shotcraft/upstream/references/publish-upload-post.md +111 -0
- package/lib/motion/shotcraft/upstream/references/sequences/promo-energy-arc.md +50 -0
- package/lib/motion/shotcraft/upstream/references/sfx-sourcing.md +95 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/basic-3d-scene.md +41 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/crash-zoom-punch.md +52 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/cursor-flyover.md +39 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/depth-layer-moves.md +39 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/graze-face-tour.md +57 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/overhead-camera-moves.md +51 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/space-camera-moves.md +46 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/steep-tilt-glide.md +52 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/tension-camera-moves.md +43 -0
- package/lib/motion/shotcraft/upstream/references/shots/camera/terminal-3d.md +42 -0
- package/lib/motion/shotcraft/upstream/references/shots/data/before-after-slider-scrub.md +48 -0
- package/lib/motion/shotcraft/upstream/references/shots/data/chart-live-moves.md +52 -0
- package/lib/motion/shotcraft/upstream/references/shots/data/gauge-readout-moves.md +45 -0
- package/lib/motion/shotcraft/upstream/references/shots/data/scroll-brake-moves.md +46 -0
- package/lib/motion/shotcraft/upstream/references/shots/data/timeline-travel.md +44 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/assemble-then-type-flyin.md +63 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/aurora-bloom-bg-flip.md +54 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/brand-frame-snap.md +58 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/fui-hud-moves.md +35 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/glow-flyline-moves.md +53 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/icon-performance-moves.md +42 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/impact-feedback.md +50 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/light-play-moves.md +46 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/radial-ripple-phone-chips.md +56 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/riso-print-hits.md +49 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/scan-bracket-sweep.md +50 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-annotate-focus.md +53 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-assemble-flyin.md +53 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/slam-entrance-moves.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/effects/spotlight-sweep-moves.md +52 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/autolayout-gap-dial.md +60 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/canvas-materialize-moves.md +57 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/chip-grid-single-select-blackout.md +51 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/collab-cursor-moves.md +41 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/glass-pill-dictation-typing.md +53 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/hashtag-to-pill-materialize.md +60 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/picker-carousel-feature-cycle.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/segmented-thumb-hero.md +57 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/theme-switch-moves.md +47 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/type-and-filter.md +37 -0
- package/lib/motion/shotcraft/upstream/references/shots/interaction/voice-waveform-live.md +54 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/brand-ink-open.md +36 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/crane-rise-reveal.md +56 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/dataviz-landscape-open.md +56 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/fracture.md +40 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/icon-field-colorize.md +60 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/letterspace-materialize.md +57 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/magician-card-flourish.md +73 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/orbit-ring-title-open.md +87 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/spotlight-hero-card.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/stroke-segment-build.md +50 -0
- package/lib/motion/shotcraft/upstream/references/shots/opening/text-as-mask.md +59 -0
- package/lib/motion/shotcraft/upstream/references/shots/outro/grain-dissolve.md +38 -0
- package/lib/motion/shotcraft/upstream/references/shots/outro/logo-shrink-wordmark-lockup.md +38 -0
- package/lib/motion/shotcraft/upstream/references/shots/outro/neon-triple-marquee.md +59 -0
- package/lib/motion/shotcraft/upstream/references/shots/outro/outro-group-photo-launch.md +39 -0
- package/lib/motion/shotcraft/upstream/references/shots/outro/ui-strip-away-outro.md +61 -0
- package/lib/motion/shotcraft/upstream/references/shots/outro/ui-to-brand-morph.md +52 -0
- package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-cut-moves.md +50 -0
- package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-step-list-theme-cycle.md +66 -0
- package/lib/motion/shotcraft/upstream/references/shots/rhythm/panel-grid-moves.md +43 -0
- package/lib/motion/shotcraft/upstream/references/shots/rhythm/quad-split-parallel-scenes.md +38 -0
- package/lib/motion/shotcraft/upstream/references/shots/rhythm/rhythm-interrupt-moves.md +38 -0
- package/lib/motion/shotcraft/upstream/references/shots/rhythm/sakuga-timing-shift.md +56 -0
- package/lib/motion/shotcraft/upstream/references/shots/rhythm/speed-ramp-freeze.md +44 -0
- package/lib/motion/shotcraft/upstream/references/shots/rhythm/trailer-grammar-moves.md +38 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/bottom-push-stack-wipe.md +63 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/card-flip-reveal.md +57 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/card-flock-tumble.md +63 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/circle-match-iris.md +58 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/gradient-transition.md +45 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/line-carry-transition.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/mosaic-reframe.md +49 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/page-turn-transitions.md +42 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/print-texture-transitions.md +42 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/shot-transitions.md +45 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/tear-streak-transitions.md +48 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/transition-hidden-cut.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/transition-travel.md +43 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/white-flash-logo-simplify-cut.md +41 -0
- package/lib/motion/shotcraft/upstream/references/shots/transition/wipe-transitions.md +42 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/blur-slide.md +59 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/brace-expand.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/countdown-arc-scatter.md +65 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/document-typewriter-reveal.md +42 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/flying-words.md +64 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/glitch-cycle.md +64 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/gradient-word-sweep.md +54 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/lead-word-zoom-assemble.md +91 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/marker-underline-title.md +51 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/outline-word-fill.md +66 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/paper-title-card.md +38 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/pill-chip-slot-cycle-handled.md +66 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/pill-slot-cycle.md +58 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/text-column-converge.md +61 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/title-demote-to-label.md +64 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/type-assembly-moves.md +45 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/type-entrance-moves.md +41 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/type-rhythm-sync.md +51 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/typewriter-moves.md +42 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/vertical-word-roll-blur-cycle.md +63 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-filmstrip.md +57 -0
- package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-geometry.md +75 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/avatar-bracket-carousel.md +59 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/bezier-source-converge-merge.md +67 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/card-stack.md +60 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/carousel-3d.md +66 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/cloner-depth-echo.md +40 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/deck-deal-flyin.md +45 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/doc-park-left-pill-deal.md +62 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/draw-svg-trace.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/element-body-moves.md +37 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/integration-hub-map.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-reveal.md +50 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-stack-press.md +43 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-forerun.md +55 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-orbit-drop.md +51 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/page-waterfall-wall.md +51 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/product-card-progressive-assemble.md +56 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/radial-wave.md +49 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/research-card-stack-scroll.md +57 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/row-embed.md +36 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/value-stagger-gradient.md +64 -0
- package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/wall-reveal-moves.md +47 -0
- package/lib/motion/shotcraft/upstream/references/sound-design.md +170 -0
- package/lib/motion/shotcraft/upstream/references/workbench.md +157 -0
- package/lib/motion/shotcraft/upstream/template/TEMPLATE.md +103 -0
- package/lib/motion/shotcraft/upstream/template/THEMES.md +44 -0
- package/lib/motion/shotcraft/upstream/template/package.json +20 -0
- package/lib/motion/shotcraft/upstream/template/remotion.config.ts +6 -0
- package/lib/motion/shotcraft/upstream/template/src/Root.tsx +17 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/Caption.tsx +65 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/DigitRoll.tsx +48 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/FlashCut.tsx +24 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/Main.tsx +192 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/PaperTitleCard.tsx +111 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/live/PageCam.tsx +150 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneDetail.tsx +158 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneFlyIn.tsx +468 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOpen.tsx +552 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOutroLive.tsx +411 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/live/ScenePapers.tsx +200 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneWbr.tsx +324 -0
- package/lib/motion/shotcraft/upstream/template/src/aifl/live-layout.json +389 -0
- package/lib/motion/shotcraft/upstream/template/src/index.ts +4 -0
- package/lib/motion/shotcraft/upstream/template/src/themes/ThemedFilm.tsx +7 -0
- package/lib/motion/shotcraft/upstream/template/src/themes/manifest.tsx +68 -0
- package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.json +1 -0
- package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.ts +24 -0
- package/lib/motion/shotcraft/upstream/template/src/themes/palettes.json +74 -0
- package/lib/motion/shotcraft/upstream/template/src/themes/visual-theme.tsx +91 -0
- package/lib/motion/shotcraft/upstream/template/src/workbench.ts +213 -0
- package/lib/motion/shotcraft/upstream/template/tsconfig.json +15 -0
- package/lib/motion/spec.js +113 -0
- package/lib/motion/style-engine.js +153 -0
- package/lib/motion/style-frames.js +271 -0
- package/lib/motion/sync.js +214 -0
- package/lib/motion/theme.js +121 -0
- package/lib/motion/video-runtime.js +192 -0
- package/lib/motion/voice.js +417 -0
- package/lib/motion-agent-files.js +82 -0
- package/lib/motion-agent-protocol.js +66 -0
- package/lib/motion-agent-tools.js +196 -0
- package/lib/motion-agent.js +171 -0
- package/lib/motion-capture.js +93 -0
- package/lib/motion-core.js +238 -0
- package/lib/motion-film-review.js +71 -0
- package/lib/motion-library.js +210 -0
- package/lib/motion-planner.js +254 -0
- package/lib/motion-project.js +87 -0
- package/lib/motion-render-worker.js +153 -0
- package/lib/motion-session.js +32 -0
- package/lib/motion-source.js +49 -0
- package/lib/motion-tasks.js +452 -0
- package/lib/motion-vision.js +44 -0
- package/lib/mxfile.js +27 -7
- package/lib/routing/node-runtime.js +14 -0
- package/lib/routing/router.js +211 -0
- package/lib/routing/vendor/README.md +96 -0
- package/lib/routing/vendor/libavoid-routing.js +753 -0
- package/lib/routing/vendor/libavoid.min.js +59 -0
- package/lib/shotcard-core.js +39 -0
- package/lib/shotcard-reference.js +56 -0
- package/lib/style-kernel.js +37 -3
- package/lib/task-patches.js +26 -2
- package/lib/video-export.js +49 -22
- package/package.json +34 -4
|
@@ -0,0 +1,2036 @@
|
|
|
1
|
+
// 05-cel-boil — 「手作」 a matchstick, a flame spirit, and a title written by sparks.
|
|
2
|
+
import * as G from './engine.js';
|
|
3
|
+
const { PAL, K, E, hs, hr, lerp, clamp, inv, sstep, spline, resample, xf, circlePts, boil, outline, fillPoly,
|
|
4
|
+
inkStroke, inkLoop, fillShape, hatch, Layers, blit, prefix, arclen, polyPath } = G;
|
|
5
|
+
|
|
6
|
+
export const W = 1920, H = 1080, FPS = 24;
|
|
7
|
+
// ---- reel v5 (?v5=1): title 逐帧手绘 / FRAME BY FRAME, window.linkAt(t), ?nolink=dot. Without v5 every code path is unchanged.
|
|
8
|
+
const QS = new URLSearchParams(typeof location !== 'undefined' ? location.search : '');
|
|
9
|
+
export const V5 = QS.get('v5') === '1';
|
|
10
|
+
const NOLINK = new Set(V5 ? (QS.get('nolink') || '').split(',').map((q) => q.trim()).filter(Boolean) : []);
|
|
11
|
+
const NODOT = NOLINK.has('dot');
|
|
12
|
+
let ctx, MAIN, SHEETC, NIGHTC, LY, IMG = {}, DPR = 1, FONT_TAG = null, CUES = null, TOTAL = 146;
|
|
13
|
+
const REG = { dx: -5, dy: 4 }; // colour-to-line registration offset (hand colouring)
|
|
14
|
+
|
|
15
|
+
// ------------------------------------------------------------------ exposure sheet (frames)
|
|
16
|
+
// [from, to, exposure] 2 = on twos, 1 = on ones, 3 = on threes
|
|
17
|
+
const SCHED = [[0, 8, 2], [8, 24, 1], [24, 48, 2], [48, 60, 1], [60, 108, 2], [108, 120, 3], [120, 132, 2],
|
|
18
|
+
[132, 168, 1], [168, 224, 2], [224, 230, 1], [230, 240, 3]] // R2: pull-back f218, riffle f224-229, end hold on 3s from f230;
|
|
19
|
+
export function drawing(f) {
|
|
20
|
+
let id = 0;
|
|
21
|
+
for (const [a, b, s] of SCHED) {
|
|
22
|
+
if (f >= b) { id += Math.ceil((b - a) / s); continue; }
|
|
23
|
+
const k = Math.floor((f - a) / s);
|
|
24
|
+
return { fq: a + k * s, id: id + k, step: s };
|
|
25
|
+
}
|
|
26
|
+
return { fq: 239, id, step: 3 };
|
|
27
|
+
}
|
|
28
|
+
// boil variant: static things cycle through 4 drawings (classic boil loop), 3 on the hold
|
|
29
|
+
const bv = (D) => (D.step === 3 ? D.id % 3 : D.id % 4);
|
|
30
|
+
|
|
31
|
+
// ------------------------------------------------------------------ helpers
|
|
32
|
+
const dens = (poly, step = 4) => resample(poly, step, true);
|
|
33
|
+
function affineTo(c, O, X, Y, w, h) { // map text box (0..w, 0..h) to parallelogram O->X (x axis), O->Y (y axis)
|
|
34
|
+
c.transform((X[0] - O[0]) / w, (X[1] - O[1]) / w, (Y[0] - O[0]) / h, (Y[1] - O[1]) / h, O[0], O[1]);
|
|
35
|
+
}
|
|
36
|
+
function camApply(c, cam) {
|
|
37
|
+
c.translate(cam.x, cam.y);
|
|
38
|
+
}
|
|
39
|
+
const STAGE = { s: 1.4, px: 930, py: 880, X: 960, Y: 1002 };
|
|
40
|
+
const CLOSE = { s: 2.6, px: 872, py: 790, X: 960, Y: 600 }; // shot A: close-up on the striker (frames 0–11)
|
|
41
|
+
const MED = { s: 2.05, px: 845, py: 650, X: 960, Y: 560 }; // shot C: cut-in on the ta-da (frames 108–131)
|
|
42
|
+
const WAKE = { s: 1.75, px: 1150, py: 520, X: 1270, Y: 500 }; // step-in when the eyes pop (re-framed drawings, 2 drawings on 2s)
|
|
43
|
+
const LOW = { s: 1.5, px: 830, py: 700, X: 800, Y: 848 }; // R2s2 (P4): low re-staged angle; flame x=640 at 3.0, match + smoke curl fill the right third
|
|
44
|
+
const JT = [0, 25, 50, 68, 78, 80, 76, 64, 48, 30, 14, 4]; // R1s2: tilt follows only ~40% of the rise so the arc reads on screen // camera tilt following the jump arc (on 1s)
|
|
45
|
+
const mixShot = (A, B, u) => ({ s: lerp(A.s, B.s, u), px: lerp(A.px, B.px, u), py: lerp(A.py, B.py, u), X: lerp(A.X, B.X, u), Y: lerp(A.Y, B.Y, u) });
|
|
46
|
+
function shotFor(f) {
|
|
47
|
+
if (f < 12) return CLOSE;
|
|
48
|
+
if (f >= 30 && f < 48) return f < 32 ? mixShot(STAGE, WAKE, 0.6) : WAKE;
|
|
49
|
+
if (f >= 48 && f < 60) return { ...STAGE, Y: STAGE.Y + JT[f - 48] };
|
|
50
|
+
if (f >= 60 && f < 108) return LOW;
|
|
51
|
+
if (f >= 108 && f < 132) return MED;
|
|
52
|
+
return STAGE;
|
|
53
|
+
}
|
|
54
|
+
let SHOT = STAGE;
|
|
55
|
+
function stageApply(c) { c.translate(SHOT.X, SHOT.Y); c.scale(SHOT.s, SHOT.s); c.translate(-SHOT.px, -SHOT.py); }
|
|
56
|
+
function edge(c, a, b, seed, o = {}) { // hand-inked straight edge with corner overshoot
|
|
57
|
+
const L = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1, ux = (b[0] - a[0]) / L, uy = (b[1] - a[1]) / L;
|
|
58
|
+
const e0 = o.over ?? 7, e1 = o.over ?? 7;
|
|
59
|
+
const s0 = e0 * (0.4 + hs(seed, 1)), s1 = e1 * (0.4 + hs(seed, 2));
|
|
60
|
+
const A = [a[0] - ux * s0, a[1] - uy * s0], B = [b[0] + ux * s1, b[1] + uy * s1];
|
|
61
|
+
const M = [lerp(A[0], B[0], 0.5) + (hs(seed, 3) - 0.5) * 2.5 * -uy * (L / 300), lerp(A[1], B[1], 0.5) + (hs(seed, 3) - 0.5) * 2.5 * ux * (L / 300)];
|
|
62
|
+
inkStroke(c, [A, M, B], { seed, w: o.w ?? 6, t0: 12, t1: 16, jit: 0.3, heavy: 0, amp: o.amp ?? 1.8, min: 0.2 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ------------------------------------------------------------------ shapes
|
|
66
|
+
function starPts(n, rOut, rIn, seed, rot = 0, jit = 0.25) {
|
|
67
|
+
const P = [];
|
|
68
|
+
for (let i = 0; i < n; i++) {
|
|
69
|
+
const a = rot + (i / n) * Math.PI * 2 + (hs(seed, i, 1) - 0.5) * (Math.PI / n) * 0.6;
|
|
70
|
+
const ro = rOut * (1 - jit + jit * 2 * hs(seed, i, 2));
|
|
71
|
+
P.push([Math.cos(a) * ro, Math.sin(a) * ro, 1]);
|
|
72
|
+
const b = a + Math.PI / n + (hs(seed, i, 3) - 0.5) * (Math.PI / n) * 0.3;
|
|
73
|
+
const ri = rIn * (0.85 + 0.3 * hs(seed, i, 4));
|
|
74
|
+
P.push([Math.cos(b) * ri, Math.sin(b) * ri]);
|
|
75
|
+
}
|
|
76
|
+
return P;
|
|
77
|
+
}
|
|
78
|
+
function twinklePts(R, seed, rot = 0) {
|
|
79
|
+
const P = [];
|
|
80
|
+
for (let i = 0; i < 4; i++) {
|
|
81
|
+
const a = rot + (i * Math.PI) / 2;
|
|
82
|
+
const r = R * (i % 2 ? 0.78 : 1) * (0.92 + 0.16 * hs(seed, i));
|
|
83
|
+
P.push([Math.cos(a) * r, Math.sin(a) * r, 1]);
|
|
84
|
+
const b = a + Math.PI / 4;
|
|
85
|
+
P.push([Math.cos(b) * R * 0.2, Math.sin(b) * R * 0.2]);
|
|
86
|
+
}
|
|
87
|
+
return P;
|
|
88
|
+
}
|
|
89
|
+
function drawTwinkle(c, x, y, R, seed, o = {}) {
|
|
90
|
+
if (R < 2) return;
|
|
91
|
+
const P = xf(twinklePts(R, seed, o.rot ?? 0), { x, y });
|
|
92
|
+
const d = spline(P, true, 3);
|
|
93
|
+
fillShape(c, d, o.fill ?? PAL.yel, { seed, amp: 1.2, dx: REG.dx * 0.6, dy: REG.dy * 0.6 });
|
|
94
|
+
inkLoop(c, d, { seed, w: o.w ?? Math.max(2.5, R * 0.09), amp: 1.2, t0: 4, t1: 8, heavy: 0.2 });
|
|
95
|
+
}
|
|
96
|
+
// teardrop spark oriented along (vx,vy)
|
|
97
|
+
function drawSpark(c, x, y, vx, vy, size, seed, o = {}) {
|
|
98
|
+
const sp = Math.hypot(vx, vy) || 1, ux = vx / sp, uy = vy / sp;
|
|
99
|
+
const len = size * (1.4 + Math.min(5, sp / 450));
|
|
100
|
+
const r = size * 0.55;
|
|
101
|
+
const ang = Math.atan2(uy, ux);
|
|
102
|
+
const P = [[r, 0], [r * 0.7, r * 0.72], [0, r], [-len, 0, 1], [0, -r], [r * 0.7, -r * 0.72]];
|
|
103
|
+
const d = spline(xf(P, { x, y, r: ang }), true, 3);
|
|
104
|
+
if (o.inkOnly) { fillShape(c, d, PAL.ink, { seed, amp: 1 }); return; }
|
|
105
|
+
fillShape(c, d, o.fill ?? PAL.yel, { seed, amp: 1.2, dx: REG.dx * 0.5, dy: REG.dy * 0.5 });
|
|
106
|
+
inkLoop(c, d, { seed, w: Math.max(2.2, size * 0.16), amp: 1, t0: 3, t1: 10, heavy: 0.2 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ------------------------------------------------------------------ paper + tooth
|
|
110
|
+
function drawPaper(c, cam) {
|
|
111
|
+
c.save();
|
|
112
|
+
const s = 1.045;
|
|
113
|
+
c.drawImage(IMG.paper, cam.x + (W - W * s) / 2, cam.y + (H - H * s) / 2, W * s, H * s);
|
|
114
|
+
c.restore();
|
|
115
|
+
}
|
|
116
|
+
function drawTooth(c, D) {
|
|
117
|
+
// multiply pass of the paper tooth over everything (boils with the drawing: every cel is a new sheet)
|
|
118
|
+
const v = bv(D);
|
|
119
|
+
const ox = Math.floor(hs(v, 51) * 470), oy = Math.floor(hs(v, 52) * 310);
|
|
120
|
+
c.save();
|
|
121
|
+
c.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
122
|
+
c.globalCompositeOperation = 'multiply';
|
|
123
|
+
c.globalAlpha = 0.5;
|
|
124
|
+
c.drawImage(IMG.tooth, ox, oy, W, H, 0, 0, W, H);
|
|
125
|
+
// scanner dust / graphite specks that change with every drawing
|
|
126
|
+
c.globalCompositeOperation = 'source-over';
|
|
127
|
+
c.globalAlpha = 0.22;
|
|
128
|
+
const dm = IMG.dust[v % 4];
|
|
129
|
+
c.save(); c.translate(W / 2, H / 2); c.scale(v % 2 ? -1 : 1, v % 3 ? 1 : -1); c.drawImage(dm, -W / 2, -H / 2, W, H); c.restore();
|
|
130
|
+
c.restore();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ------------------------------------------------------------------ MATCHBOX
|
|
134
|
+
const BOXP = { px: 930, py: 880 };
|
|
135
|
+
function boxState(t) {
|
|
136
|
+
let sq = 0;
|
|
137
|
+
for (const tl of [1.0, 2.5, 3.0, 3.5, 4.0, 5.0, 5.25]) {
|
|
138
|
+
const d = t - tl;
|
|
139
|
+
if (d >= 0 && d < 0.3) sq += (tl === 2.5 ? 0.06 : 0.035) * Math.exp(-d * 12) * Math.cos(d * 28);
|
|
140
|
+
}
|
|
141
|
+
const st = { x: 0, y: 0, r: 0, sy: 1 - sq, sx: 1 + sq * 0.4, vis: true, blown: 0 };
|
|
142
|
+
if (t >= 6.54) {
|
|
143
|
+
const u = t - 6.54;
|
|
144
|
+
st.x = -1500 * u - 1400 * u * u; st.y = 380 * u + 2600 * u * u; st.r = -4.2 * u - 3 * u * u; st.blown = u;
|
|
145
|
+
if (u > 0.5) st.vis = false;
|
|
146
|
+
}
|
|
147
|
+
return st;
|
|
148
|
+
}
|
|
149
|
+
function drawBox(c, t, D, st) {
|
|
150
|
+
if (!st.vis) return;
|
|
151
|
+
const sd = 100 + bv(D);
|
|
152
|
+
const T = (P) => xf(P, { x: BOXP.px + st.x, y: BOXP.py + st.y, r: st.r, sx: st.sx, sy: st.sy });
|
|
153
|
+
const low = SHOT === LOW; // R2s2 (P4): real low eye line for the dance shot - more front face, top plane foreshortened to ~62 %
|
|
154
|
+
const x0 = -370, x1 = 370, y1 = low ? -112 : -90, y2 = 0, dx = low ? 40 : 46, dy = low ? -64 : -104;
|
|
155
|
+
const a = [x0, y1], b = [x1, y1], cc = [x1 + dx, y1 + dy], d = [x0 + dx, y1 + dy], e = [x0, y2], g = [x1, y2], h = [x1 + dx, y2 + dy];
|
|
156
|
+
const top = T([a, b, cc, d]), front = T([a, b, g, e]), right = T([b, cc, h, g]);
|
|
157
|
+
// cast shadow (hatched) — only while resting
|
|
158
|
+
if (!st.blown) {
|
|
159
|
+
const S = [x0 + 30, y2], S2 = [x1, y2], S3 = [x1 + dx, y2 + dy], S4 = [x1 + dx + (low ? 34 : 70), y2 + dy + 40], S5 = [x1 + 60, y2 + 34], S6 = [x0 + 60, y2 + 30];
|
|
160
|
+
const sh = T([S, S2, S3, S4, S5, S6]);
|
|
161
|
+
hatch(c, dens(sh, 6), { seed: sd * 3, gap: 13, ang: -0.95, w: 2.6, amp: 1.2 });
|
|
162
|
+
}
|
|
163
|
+
const o = (k) => ({ seed: sd * 10 + k, amp: 1.8, dx: REG.dx, dy: REG.dy });
|
|
164
|
+
fillShape(c, dens(top), PAL.red, o(1));
|
|
165
|
+
fillShape(c, dens(front), PAL.yel, o(2));
|
|
166
|
+
fillShape(c, dens(right), PAL.red, o(3));
|
|
167
|
+
// right face shading hatch
|
|
168
|
+
hatch(c, dens(right, 6), { seed: sd * 5, gap: 11, ang: 1.15, w: 2.2, amp: 1.0 });
|
|
169
|
+
// striker band
|
|
170
|
+
const band = T([[x0 + 22, y1 + 20], [x1 - 22, y1 + 20], [x1 - 22, y2 - 20], [x0 + 22, y2 - 20]]);
|
|
171
|
+
fillShape(c, dens(band), PAL.ink, { seed: sd * 10 + 4, amp: 1.4 });
|
|
172
|
+
// grit specks
|
|
173
|
+
c.fillStyle = PAL.cream;
|
|
174
|
+
for (let i = 0; i < 90; i++) {
|
|
175
|
+
const px = lerp(x0 + 30, x1 - 30, hs(i, 3)), py = lerp(y1 + 26, y2 - 26, hs(i, 4));
|
|
176
|
+
const [q] = boil(T([[px, py]]), sd, 1.0, 40);
|
|
177
|
+
c.beginPath(); c.arc(q[0], q[1], 0.9 + 1.4 * hs(i, 5), 0, 7); c.fill();
|
|
178
|
+
}
|
|
179
|
+
// strike scratch (after the strike passes)
|
|
180
|
+
const sp = matchStrikeProgress(t);
|
|
181
|
+
if (sp > 0) {
|
|
182
|
+
const L = T([[x0 + 40, (y1 + y2) / 2 + 2], [lerp(x0 + 40, x1 - 30, sp * 0.5), (y1 + y2) / 2 - 3], [lerp(x0 + 40, x1 - 30, sp), (y1 + y2) / 2 + 1]]);
|
|
183
|
+
inkStroke(c, L, { seed: sd + 7, w: 4.5, t0: 30, t1: 60, amp: 1.0, color: PAL.cream, heavy: 0 });
|
|
184
|
+
}
|
|
185
|
+
// label on the top face (星火牌 安全火柴)
|
|
186
|
+
const LP = (u, v) => [x0 + u * (x1 - x0) + v * dx, y1 + v * dy];
|
|
187
|
+
const u0 = 0.25, u1 = 0.78, v0 = 0.2, v1 = 0.82;
|
|
188
|
+
const lab = T([LP(u0, v0), LP(u1, v0), LP(u1, v1), LP(u0, v1)]);
|
|
189
|
+
fillShape(c, dens(lab), PAL.hi, { seed: sd * 10 + 5, amp: 1.4, dx: REG.dx * 0.6, dy: REG.dy * 0.6 });
|
|
190
|
+
const [O] = T([LP(u0, v1)]), [X] = T([LP(u1, v1)]), [Y] = T([LP(u0, v0)]);
|
|
191
|
+
c.save();
|
|
192
|
+
affineTo(c, O, X, Y, 540, 120);
|
|
193
|
+
const jx = (hs(bv(D), 61) - 0.5) * 1.6, jy = (hs(bv(D), 62) - 0.5) * 1.6;
|
|
194
|
+
c.translate(jx, jy);
|
|
195
|
+
// little flame mark
|
|
196
|
+
const fl = spline(xf(flameUnit(4, 0.1), { x: 62, y: 104, sx: 72, sy: 90 }), true, 3);
|
|
197
|
+
fillShape(c, fl, PAL.red, { seed: 3, amp: 0.8 });
|
|
198
|
+
c.fillStyle = PAL.ink;
|
|
199
|
+
c.font = '64px "LXGW Marker Gothic"';
|
|
200
|
+
c.textBaseline = 'alphabetic';
|
|
201
|
+
c.fillText('星火牌', 118, 84);
|
|
202
|
+
c.font = '30px "LXGW Marker Gothic"';
|
|
203
|
+
c.fillText('安 全 火 柴', 330, 82);
|
|
204
|
+
c.fillRect(118, 98, 400, 5);
|
|
205
|
+
c.restore();
|
|
206
|
+
inkLoop(c, dens(lab), { seed: sd * 10 + 6, w: 3.4, amp: 1.2, heavy: 0.2 });
|
|
207
|
+
// edges
|
|
208
|
+
const E9 = [[a, b], [b, cc], [cc, d], [d, a], [a, e], [e, g], [g, b], [g, h], [h, cc]];
|
|
209
|
+
E9.forEach(([p, q], i) => { const [P2, Q2] = T([p, q]); edge(c, P2, Q2, sd * 20 + i, { w: 5.2 }); });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ------------------------------------------------------------------ MATCH
|
|
213
|
+
// head centre (hx,hy), rotation r (0 = upright: stick hangs below the head), bend (px)
|
|
214
|
+
function matchStrikeProgress(t) {
|
|
215
|
+
return clamp(K(t, [[0.333, 0], [0.375, 0.3, 'lin'], [0.417, 0.64, 'lin'], [0.458, 0.9, 'lin'], [0.5, 1.0, 'out2']]));
|
|
216
|
+
}
|
|
217
|
+
const STICK = 262, BUTT = [1150, 742];
|
|
218
|
+
function matchState(t) {
|
|
219
|
+
const s = { hx: 0, hy: 0, r: 0, bend: 0, burnt: t >= 2.0, vis: true, smear: 0 };
|
|
220
|
+
if (t < 0.333) { // anticipation: rear back, bow the stick
|
|
221
|
+
const u = t / 0.333;
|
|
222
|
+
s.hx = K(t, [[0, 660], [0.25, 590, 'out2'], [0.333, 586]]);
|
|
223
|
+
s.hy = K(t, [[0, 800], [0.25, 752, 'out2'], [0.333, 760]]);
|
|
224
|
+
s.r = K(t, [[0, 2.3], [0.25, 2.62, 'out2'], [0.333, 2.56]]);
|
|
225
|
+
s.bend = K(t, [[0, -4], [0.25, -20], [0.333, -24]]);
|
|
226
|
+
s.hx += Math.sin(u * 40) * 1.5; // nervous tremble
|
|
227
|
+
} else if (t < 0.5) { // strike drag across the striker (on 1s)
|
|
228
|
+
const p = matchStrikeProgress(t);
|
|
229
|
+
s.hx = lerp(612, 1284, p); s.hy = 836 - 4 * Math.sin(p * Math.PI);
|
|
230
|
+
s.r = lerp(2.2, 2.05, p); s.bend = 22; s.smear = t < 0.44 ? 1 : 0;
|
|
231
|
+
} else if (t < 1.0) { // flip up + land upright on the box top
|
|
232
|
+
const rE = -0.12, endX = BUTT[0] + Math.sin(rE) * STICK, endY = BUTT[1] - Math.cos(rE) * STICK;
|
|
233
|
+
// tossed up spinning about its middle (~600 deg), rise ease-out, one hang drawing at the apex (f18), fall ease-in
|
|
234
|
+
const fi = clamp(t * FPS - 12, 0, 12), H2 = STICK / 2;
|
|
235
|
+
const c0x = 1290 - Math.sin(2.05) * H2, c0y = 832 + Math.cos(2.05) * H2;
|
|
236
|
+
const c1x = endX - Math.sin(rE) * H2, c1y = endY + Math.cos(rE) * H2;
|
|
237
|
+
const CY = [0, 110, 200, 270, 315, 340, 350, 351, 359, 375, 399, 434, 481].map((d) => c0y - d); // relative climb (stage units)
|
|
238
|
+
const CYF = CY.map((y, i) => i <= 6 ? y : lerp(CY[6], c1y, [0, 0.004, 0.03, 0.11, 0.27, 0.55, 1][i - 6])); // R2: screen spacing ~7/21/43/75/120 px
|
|
239
|
+
const i0 = Math.floor(fi), i1 = Math.min(12, i0 + 1), fr = fi - i0;
|
|
240
|
+
const cy = lerp(CYF[i0], CYF[i1], fr), cx = lerp(c0x, c1x, fi / 12);
|
|
241
|
+
const RT = [0, 0.115, 0.23, 0.34, 0.45, 0.555, 0.655, 0.745, 0.83, 0.9, 0.955, 0.99, 1]; // R2: spin decelerates -> falls near upright
|
|
242
|
+
s.r = lerp(2.05, rE + Math.PI * 4, lerp(RT[i0], RT[i1], fr));
|
|
243
|
+
if (i0 === 11) s.sy = 1.05; // last airborne drawing stretched along the fall
|
|
244
|
+
s.hx = cx + Math.sin(s.r) * H2; s.hy = cy - Math.cos(s.r) * H2;
|
|
245
|
+
s.bend = lerp(10, -6, fi / 12); s.toss = true;
|
|
246
|
+
} else {
|
|
247
|
+
const d = t - 1.0;
|
|
248
|
+
const k = Math.round(d * 12); // R2: landing wobble about the butt on 2s: +8 deg, -4 deg, +1.5 deg, rest
|
|
249
|
+
s.r = k < 3 ? [0.14, -0.07, 0.025][k] : 0;
|
|
250
|
+
s.bend = k < 3 ? [7, -4, 1.5][k] : 0;
|
|
251
|
+
if (k === 0) s.sy = 0.96;
|
|
252
|
+
s.hx = BUTT[0] + Math.sin(s.r) * STICK; s.hy = BUTT[1] - Math.cos(s.r) * STICK; // rotate about the butt on the box
|
|
253
|
+
}
|
|
254
|
+
if (t >= 6.54) { // blown away
|
|
255
|
+
const u = t - 6.54;
|
|
256
|
+
s.hx += 1700 * u + 600 * u * u; s.hy += -900 * u + 1800 * u * u; s.r += 13 * u; s.bend = 10;
|
|
257
|
+
if (u > 0.45) s.vis = false;
|
|
258
|
+
}
|
|
259
|
+
return s;
|
|
260
|
+
}
|
|
261
|
+
function matchLocal(bend) {
|
|
262
|
+
// stick centreline from y=18 (under the head) to y=318 (butt)
|
|
263
|
+
const cl = [];
|
|
264
|
+
for (let i = 0; i <= 8; i++) { const u = i / 8; cl.push([bend * Math.sin(Math.PI * u), 18 + (STICK - 18) * u]); }
|
|
265
|
+
return cl;
|
|
266
|
+
}
|
|
267
|
+
function drawMatchStick(c, s, D, seedBase) {
|
|
268
|
+
if (!s.vis) return;
|
|
269
|
+
const sd = seedBase + bv(D);
|
|
270
|
+
const cl = matchLocal(s.bend);
|
|
271
|
+
const T = (P) => xf(P, { x: s.hx, y: s.hy, r: s.r, sy: s.sy ?? 1 });
|
|
272
|
+
// stick outline polygon (local)
|
|
273
|
+
const L = [], R = [];
|
|
274
|
+
cl.forEach(([x, y], i) => { const w = 10 + (i / 8) * 1.8; L.push([x - w, y]); R.push([x + w, y]); });
|
|
275
|
+
const poly = T([...L, ...R.reverse()]);
|
|
276
|
+
const d = dens(poly, 4);
|
|
277
|
+
fillShape(c, d, PAL.hi, { seed: sd * 3, amp: 1.4, dx: REG.dx * 0.7, dy: REG.dy * 0.7 });
|
|
278
|
+
// grain line
|
|
279
|
+
inkStroke(c, T(cl.slice(2, 7).map(([x, y]) => [x + 3, y])), { seed: sd * 3 + 1, w: 2.2, t0: 20, t1: 30, amp: 1.2, heavy: 0 });
|
|
280
|
+
inkLoop(c, d, { seed: sd * 3 + 2, w: 4.4, amp: 1.5, t0: 8, t1: 16, heavy: 0.3 });
|
|
281
|
+
}
|
|
282
|
+
// SMEAR / MULTIPLES frames of the strike (on 1s): f9 = stretch smear, f10 = multiples
|
|
283
|
+
function stickEnds(s) { return [[s.hx, s.hy], [s.hx - Math.sin(s.r) * STICK, s.hy + Math.cos(s.r) * STICK]]; }
|
|
284
|
+
function drawStrikeSmear(c, s, D, t) {
|
|
285
|
+
const f = Math.round(t * FPS);
|
|
286
|
+
const pv = matchState(t - 1 / FPS);
|
|
287
|
+
const [H1, B1] = stickEnds(s), [H0, B0] = stickEnds(pv);
|
|
288
|
+
const sd = 330 + f;
|
|
289
|
+
if (f === 9) {
|
|
290
|
+
// stick smear: the swept area, streaked
|
|
291
|
+
const quad = [B0, H0, H1, B1];
|
|
292
|
+
fillShape(c, dens(quad, 5), PAL.hi, { seed: sd, amp: 1.5, dx: REG.dx, dy: REG.dy });
|
|
293
|
+
for (let k = 0; k < 6; k++) {
|
|
294
|
+
const u = 0.12 + k * 0.15 + hs(sd, k) * 0.05;
|
|
295
|
+
const a = [lerp(H0[0], B0[0], u), lerp(H0[1], B0[1], u)], b = [lerp(H1[0], B1[0], u), lerp(H1[1], B1[1], u)];
|
|
296
|
+
inkStroke(c, [a, b], { seed: sd * 3 + k, w: 3, t0: 60, t1: 12, min: 0.05, amp: 1.2, heavy: 0 });
|
|
297
|
+
}
|
|
298
|
+
drawMatchStick(c, { ...s, smear: 0 }, D, 200);
|
|
299
|
+
// head stretched ~300% back along the path
|
|
300
|
+
const dx = H1[0] - H0[0], dy = H1[1] - H0[1], L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L;
|
|
301
|
+
if (!NODOT) {
|
|
302
|
+
const back = L * 1.1, r = 27;
|
|
303
|
+
const P = [[r, 0], [r * 0.6, r * 0.85], [-back * 0.45, r * 0.55], [-back, 0, 1], [-back * 0.45, -r * 0.55], [r * 0.6, -r * 0.85]];
|
|
304
|
+
const d = spline(xf(P, { x: H1[0], y: H1[1], r: Math.atan2(uy, ux) }), true, 4);
|
|
305
|
+
fillShape(c, d, PAL.red, { seed: sd + 1, amp: 1.5, dx: REG.dx, dy: REG.dy });
|
|
306
|
+
inkLoop(c, d, { seed: sd + 2, w: 5.5, amp: 1.5 });
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// multiples: three drawn echoes between the previous and current pose
|
|
310
|
+
for (let k = 0; k < 3; k++) {
|
|
311
|
+
const u = 0.25 + k * 0.25;
|
|
312
|
+
const e = { ...s, hx: lerp(pv.hx, s.hx, u), hy: lerp(pv.hy, s.hy, u), r: lerp(pv.r, s.r, u), bend: s.bend, smear: 0 };
|
|
313
|
+
const [He, Be] = stickEnds(e);
|
|
314
|
+
inkStroke(c, [Be, He], { seed: sd + 10 + k, w: 3.2, t0: 40, t1: 10, min: 0.1, amp: 1.2, heavy: 0 });
|
|
315
|
+
const hd = headPts(e);
|
|
316
|
+
if (!NODOT) {
|
|
317
|
+
fillShape(c, hd, PAL.red, { seed: sd + 20 + k, amp: 1.2, dx: REG.dx, dy: REG.dy });
|
|
318
|
+
inkLoop(c, hd, { seed: sd + 30 + k, w: 3.6, amp: 1.2 });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
drawMatchStick(c, { ...s, smear: 0 }, D, 200);
|
|
322
|
+
const hd = headPts(s);
|
|
323
|
+
if (!NODOT) {
|
|
324
|
+
fillShape(c, hd, PAL.red, { seed: sd + 40, amp: 1.4, dx: REG.dx, dy: REG.dy });
|
|
325
|
+
inkLoop(c, hd, { seed: sd + 41, w: 5.5, amp: 1.4, heavy: 0.4 });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// speed lines trailing along the striker
|
|
329
|
+
for (let k = 0; k < 4; k++) {
|
|
330
|
+
const yy = s.hy - 26 + k * 17 + hs(sd, k) * 5;
|
|
331
|
+
const x1 = Math.min(H0[0], H1[0]) - 30 - hs(sd, k, 2) * 50;
|
|
332
|
+
inkStroke(c, [[x1 - 200 - hs(sd, k, 3) * 160, yy], [x1, yy]], { seed: sd + 50 + k, w: 4.2, t0: 120, t1: 8, min: 0.05, amp: 1.2, heavy: 0, color: PAL.hi });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const HEAD_R = 27; // V5: the round match head (reported as the dot, r = 27 stage units)
|
|
336
|
+
function headPts(s, stretch = 1) {
|
|
337
|
+
const P = (V5 ? circlePts(0, -2, 28, 28, 26) : circlePts(0, -2, 30, 28, 22)).map(([x, y]) => [x, y < 0 ? y * stretch : y]);
|
|
338
|
+
return xf(P, { x: s.hx, y: s.hy, r: s.r });
|
|
339
|
+
}
|
|
340
|
+
function drawMatchHead(c, s, D, seedBase, t) {
|
|
341
|
+
if (!s.vis) return;
|
|
342
|
+
const sd = seedBase + bv(D);
|
|
343
|
+
if (s.smear && t >= 0.36) { drawStrikeSmear(c, s, D, t); return; }
|
|
344
|
+
if (t >= 0.44 && t < 0.5) { // last drag frame: speed lines + the phosphor starting to spit
|
|
345
|
+
const sd2 = 340 + Math.round(t * FPS);
|
|
346
|
+
for (let k = 0; k < 4; k++) {
|
|
347
|
+
const yy = s.hy - 26 + k * 17 + hs(sd2, k) * 5, x1 = s.hx - 60 - hs(sd2, k, 2) * 40;
|
|
348
|
+
inkStroke(c, [[x1 - 260 - hs(sd2, k, 3) * 160, yy], [x1, yy]], { seed: sd2 + 50 + k, w: 4.2, t0: 120, t1: 8, min: 0.05, amp: 1.2, heavy: 0, color: PAL.hi });
|
|
349
|
+
}
|
|
350
|
+
for (let k = 0; k < 5; k++) {
|
|
351
|
+
const a = Math.PI + (k - 2) * 0.35, dd = 40 + 25 * hs(sd2, k, 4);
|
|
352
|
+
drawSpark(c, s.hx + Math.cos(a) * dd, s.hy + Math.sin(a) * dd - 10, Math.cos(a) * 700, Math.sin(a) * 700, 6, sd2 * 3 + k);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (NODOT) return; // v5 ?nolink=dot: the match head is the reel's dot
|
|
356
|
+
const d = headPts(s);
|
|
357
|
+
fillShape(c, d, s.burnt ? PAL.ink : PAL.red, { seed: sd * 7, amp: 1.4, dx: REG.dx * 0.8, dy: REG.dy * 0.8 });
|
|
358
|
+
inkLoop(c, d, { seed: sd * 7 + 1, w: 5.5, amp: 1.4, heavy: 0.4 });
|
|
359
|
+
// highlight / ember
|
|
360
|
+
const hl = xf([[-11, -14], [-5, -20]], { x: s.hx, y: s.hy, r: s.r });
|
|
361
|
+
inkStroke(c, hl, { seed: sd + 5, w: 5, t0: 3, t1: 5, color: PAL.hi, amp: 0.8, heavy: 0 });
|
|
362
|
+
if (s.burnt) {
|
|
363
|
+
const em = xf([[6, -16]], { x: s.hx, y: s.hy, r: s.r })[0];
|
|
364
|
+
c.fillStyle = PAL.red; c.beginPath(); c.arc(em[0], em[1], 4.5, 0, 7); c.fill();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ------------------------------------------------------------------ FLAME SPIRIT
|
|
369
|
+
// flame silhouette in unit space (bottom centre at 0,0; height 1 up = -y)
|
|
370
|
+
function flameUnit(seed, wild = 0.15, lick = 1) {
|
|
371
|
+
const j = (k, a) => (hs(seed, k) - 0.5) * 2 * a;
|
|
372
|
+
const hr_ = 0.66 + j(1, 0.06 + wild * 0.3) * lick;
|
|
373
|
+
const hl_ = 0.76 + j(2, 0.06 + wild * 0.3) * lick;
|
|
374
|
+
const tipx = j(3, 0.04 + wild * 0.2), tipy = -1.0 + j(4, 0.03 + wild * 0.12);
|
|
375
|
+
return [
|
|
376
|
+
[0, 0.0],
|
|
377
|
+
[0.3, -0.035],
|
|
378
|
+
[0.43, -0.2],
|
|
379
|
+
[0.415, -0.4],
|
|
380
|
+
[0.3 + j(5, 0.03 + wild * 0.1), -hr_, 1],
|
|
381
|
+
[0.17, -0.55 + j(6, 0.04)],
|
|
382
|
+
[tipx, tipy, 1],
|
|
383
|
+
[-0.15, -0.6 + j(7, 0.04)],
|
|
384
|
+
[-0.34 + j(8, 0.03 + wild * 0.1), -hl_, 1],
|
|
385
|
+
[-0.44, -0.46],
|
|
386
|
+
[-0.44, -0.24],
|
|
387
|
+
[-0.31, -0.04],
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
function innerUnit(seed, wild) {
|
|
391
|
+
const j = (k, a) => (hs(seed, k + 20) - 0.5) * 2 * a;
|
|
392
|
+
return [
|
|
393
|
+
[0, 0.0], [0.24, -0.04], [0.33, -0.18], [0.3, -0.36],
|
|
394
|
+
[0.02 + j(1, 0.04 + wild * 0.1), -0.66 + j(2, 0.04), 1],
|
|
395
|
+
[-0.3, -0.38], [-0.34, -0.18], [-0.24, -0.04],
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
// warp: shear + curl (top bends more) + squash/stretch about the base
|
|
399
|
+
function spiritXf(P, s) {
|
|
400
|
+
const Hh = s.H;
|
|
401
|
+
return P.map(([x, y, cr]) => {
|
|
402
|
+
const up = -y; // 0..1
|
|
403
|
+
let X = x * Hh * s.sx, Y = y * Hh * s.sy;
|
|
404
|
+
X += (s.kx * up + s.curl * up * up) * Hh;
|
|
405
|
+
if (s.spinW !== undefined) X *= s.spinW;
|
|
406
|
+
const c = Math.cos(s.r || 0), sn = Math.sin(s.r || 0);
|
|
407
|
+
return [s.x + X * c - Y * sn, s.y + X * sn + Y * c, cr];
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function spiritPt(s, x, y) { return spiritXf([[x, y]], s)[0]; }
|
|
411
|
+
|
|
412
|
+
function drawSpirit(c, s, D, t) {
|
|
413
|
+
if (!s || s.H < 3) return;
|
|
414
|
+
const sd = s.seed ?? (500 + D.id);
|
|
415
|
+
const wild = s.wild ?? 0.12;
|
|
416
|
+
if (s.smearUp) { // take-off smear: speed lines left behind under the stretched body
|
|
417
|
+
for (let k = 0; k < 5; k++) {
|
|
418
|
+
const x = s.x + (k - 2) * s.H * 0.11 + (hs(sd, k) - 0.5) * 10;
|
|
419
|
+
const y0 = s.y + 10 + hs(sd, k, 2) * 30, L = s.H * (0.45 + 0.5 * hs(sd, k, 3));
|
|
420
|
+
inkStroke(c, [[x, y0 + L], [x, y0]], { seed: sd + 200 + k, w: 4.5, t0: 60, t1: 8, min: 0.05, amp: 1, heavy: 0 });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const outer = spline(spiritXf(flameUnit(sd, wild, s.lick ?? 1), s), true, 4);
|
|
424
|
+
const inner = spline(spiritXf(innerUnit(sd, wild).map(([x, y, k]) => [x, y * 0.98 - 0.02, k]), s), true, 4);
|
|
425
|
+
// detached flame droplet (fire FX): a lick breaks off and rises on some drawings
|
|
426
|
+
const dph = Math.abs(Math.round(sd)) % 3;
|
|
427
|
+
if (s.drops !== false && s.H > 120 && dph !== 2) { // lick cycle: low -> high -> gone (reads as fire, not a stuck speck)
|
|
428
|
+
const k = dph * 0.8, hx = (dph ? 0.1 : -0.12) + (hs(sd, 92) - 0.5) * 0.08;
|
|
429
|
+
const up = 1.08 + k * 0.28, sz = 0.07 * (1 - k * 0.6);
|
|
430
|
+
const P = [[0, 0.06], [0.05, 0.0], [0, -0.13, 1], [-0.05, 0.0]].map(([x, y, cr]) => [hx + x * sz / 0.07, -up + y * sz / 0.07, cr]);
|
|
431
|
+
const dd = spline(spiritXf(P, s), true, 3);
|
|
432
|
+
fillShape(c, dd, PAL.red, { seed: sd + 3, amp: 1, dx: REG.dx * 0.5, dy: REG.dy * 0.5 });
|
|
433
|
+
inkLoop(c, dd, { seed: sd + 4, w: Math.min(3.4, s.H * 0.016), amp: 1, t0: 3, t1: 6 });
|
|
434
|
+
}
|
|
435
|
+
fillShape(c, outer, PAL.red, { seed: sd * 2, amp: 1.8, dx: REG.dx, dy: REG.dy });
|
|
436
|
+
fillShape(c, inner, PAL.yel, { seed: sd * 2 + 1, amp: 1.6, dx: REG.dx, dy: REG.dy });
|
|
437
|
+
// paper-white glint on the inner flame
|
|
438
|
+
if (s.H > 60) {
|
|
439
|
+
const g = spiritXf([[-0.2, -0.16], [-0.23, -0.28], [-0.19, -0.4]], s);
|
|
440
|
+
inkStroke(c, g, { seed: sd + 8, w: Math.max(3, s.H * 0.035), t0: 6, t1: 12, color: PAL.hi, amp: 1, heavy: 0 });
|
|
441
|
+
}
|
|
442
|
+
const lw = Math.max(3.0, Math.min(7, s.H * 0.024));
|
|
443
|
+
inkLoop(c, outer, { seed: sd * 2 + 2, w: lw, amp: 1.8, splits: 2, heavy: 0.45, t0: 10, t1: 20 });
|
|
444
|
+
// back-facing (during spin) -> no face
|
|
445
|
+
if (s.face) drawFace(c, s, sd, lw);
|
|
446
|
+
if (s.arms) drawArms(c, s, sd, lw);
|
|
447
|
+
}
|
|
448
|
+
function drawFace(c, s, sd, lw) {
|
|
449
|
+
const f = s.face;
|
|
450
|
+
const Hh = s.H;
|
|
451
|
+
const lx = (f.look ?? 0) * 0.045, ly = (f.lookY ?? 0) * 0.03;
|
|
452
|
+
const ex = 0.1 * (f.wide ?? 1), ey = -0.37 + (f.fy ?? 0);
|
|
453
|
+
const eyeRX = 0.047, eyeRY = 0.078 * (f.pop ?? 1);
|
|
454
|
+
for (const side of [-1, 1]) {
|
|
455
|
+
const cx = side * ex + lx, cy = ey + ly;
|
|
456
|
+
const mode = f.eyes ?? 'open';
|
|
457
|
+
if (mode === 'open' || mode === 'fierce' || (mode === 'wink' && side < 0)) {
|
|
458
|
+
if (mode === 'fierce') inkStroke(c, spiritXf([[cx + side * eyeRX * 1.5, cy - 0.12], [cx - side * eyeRX * 1.1, cy - 0.065]], s), { seed: sd + 46 + side, w: lw * 1.0, t0: 3, t1: 3, amp: 0.6, heavy: 0 });
|
|
459
|
+
const P = circlePts(cx, cy, eyeRY, 18, eyeRX * (f.pop ?? 1)).map(([x, y]) => [x, y]);
|
|
460
|
+
const d = spline(spiritXf(P, s), true, 3);
|
|
461
|
+
fillShape(c, d, PAL.ink, { seed: sd + 30 + side, amp: 0.9 });
|
|
462
|
+
const g = spiritPt(s, cx - eyeRX * 0.35, cy - eyeRY * 0.45);
|
|
463
|
+
c.fillStyle = PAL.hi; c.beginPath(); c.arc(g[0], g[1], Math.max(1.6, Hh * 0.013), 0, 7); c.fill();
|
|
464
|
+
} else if (mode === 'closed' || mode === 'wink') {
|
|
465
|
+
inkStroke(c, spiritXf([[cx - eyeRX * 1.1, cy + 0.01], [cx, cy + 0.02], [cx + eyeRX * 1.1, cy + 0.01]], s), { seed: sd + 40 + side, w: lw * 0.9, t0: 4, t1: 4, amp: 0.8, heavy: 0 });
|
|
466
|
+
} else if (mode === 'happy') { // ^ ^
|
|
467
|
+
inkStroke(c, spiritXf([[cx - eyeRX * 1.2, cy + 0.02], [cx, cy - 0.04], [cx + eyeRX * 1.2, cy + 0.02]], s), { seed: sd + 40 + side, w: lw * 0.95, t0: 4, t1: 4, amp: 0.8, heavy: 0 });
|
|
468
|
+
} else if (mode === 'squeeze') { // > <
|
|
469
|
+
const k = side < 0 ? 1 : -1;
|
|
470
|
+
inkStroke(c, spiritXf([[cx - eyeRX * k, cy - 0.035], [cx + eyeRX * k * 0.9, cy], [cx - eyeRX * k, cy + 0.035]], s), { seed: sd + 40 + side, w: lw * 0.9, t0: 4, t1: 4, amp: 0.8, heavy: 0 });
|
|
471
|
+
} else if (mode === 'squint') {
|
|
472
|
+
const P = circlePts(cx, cy + 0.015, eyeRY * 0.55, 14, eyeRX);
|
|
473
|
+
fillShape(c, spline(spiritXf(P, s), true, 3), PAL.ink, { seed: sd + 30 + side, amp: 0.9 });
|
|
474
|
+
inkStroke(c, spiritXf([[cx - eyeRX * 1.3, cy - 0.06 - side * 0.012], [cx + eyeRX * 1.3, cy - 0.06 + side * 0.012]], s), { seed: sd + 44 + side, w: lw * 0.8, t0: 3, t1: 3, amp: 0.6, heavy: 0 });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const m = f.mouth ?? 'smile';
|
|
478
|
+
const my = -0.245 + (f.fy ?? 0) + ly * 0.6, mx = lx * 0.8;
|
|
479
|
+
if (m === 'smile') {
|
|
480
|
+
inkStroke(c, spiritXf([[mx - 0.06, my - 0.01], [mx, my + 0.025], [mx + 0.06, my - 0.01]], s), { seed: sd + 50, w: lw * 0.85, t0: 3, t1: 3, amp: 0.7, heavy: 0 });
|
|
481
|
+
} else if (m === 'o' || m === 'grin' || m === 'open') {
|
|
482
|
+
const P = m === 'o' ? circlePts(mx, my + 0.005, 0.03, 14, 0.024)
|
|
483
|
+
: [[mx - 0.075, my - 0.02], [mx + 0.075, my - 0.02], [mx + 0.05, my + 0.04], [mx, my + 0.06], [mx - 0.05, my + 0.04]];
|
|
484
|
+
const d = spline(spiritXf(P, s), true, 3);
|
|
485
|
+
fillShape(c, d, PAL.ink, { seed: sd + 52, amp: 0.8 });
|
|
486
|
+
if (m !== 'o') {
|
|
487
|
+
const tg = spline(spiritXf(circlePts(mx + 0.01, my + 0.04, 0.02, 12, 0.03), s), true, 3);
|
|
488
|
+
c.save(); c.beginPath(); polyPath(c, d); c.clip();
|
|
489
|
+
fillShape(c, tg, PAL.red, { seed: sd + 53, amp: 0.6 });
|
|
490
|
+
c.restore();
|
|
491
|
+
}
|
|
492
|
+
} else if (m === 'grit') {
|
|
493
|
+
inkStroke(c, spiritXf([[mx - 0.055, my], [mx + 0.055, my]], s), { seed: sd + 54, w: lw * 0.9, t0: 3, t1: 3, amp: 0.6, heavy: 0 });
|
|
494
|
+
} else if (m === 'puff') {
|
|
495
|
+
const d = spline(spiritXf(circlePts(mx, my, 0.02, 12, 0.045), s), true, 3);
|
|
496
|
+
fillShape(c, d, PAL.ink, { seed: sd + 55, amp: 0.6 });
|
|
497
|
+
}
|
|
498
|
+
// cheeks
|
|
499
|
+
if (f.cheeks) {
|
|
500
|
+
for (const side of [-1, 1]) {
|
|
501
|
+
const d = spline(spiritXf(circlePts(side * 0.2 + lx, -0.27 + (f.fy ?? 0), 0.025, 12, 0.04), s), true, 3);
|
|
502
|
+
fillShape(c, d, PAL.red, { seed: sd + 60 + side, amp: 0.6 });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// rubber-hose arms with paper-white gloves
|
|
507
|
+
function drawArms(c, s, sd, lw) {
|
|
508
|
+
const A = s.arms;
|
|
509
|
+
for (const side of [-1, 1]) {
|
|
510
|
+
const a = side < 0 ? A.L : A.R;
|
|
511
|
+
if (!a) continue;
|
|
512
|
+
const sh = [side * 0.4, -0.36];
|
|
513
|
+
const ang = a.ang, len = (a.len ?? 0.42);
|
|
514
|
+
// direction in unit space: outward = side*x, positive ang = up
|
|
515
|
+
const dx = side * Math.cos(ang), dy = -Math.sin(ang);
|
|
516
|
+
const bx = -dy * (a.bend ?? 0.3) * side, by = dx * (a.bend ?? 0.3) * side;
|
|
517
|
+
const mid = [sh[0] + dx * len * 0.5 + bx * len * 0.35, sh[1] + dy * len * 0.5 + by * len * 0.35];
|
|
518
|
+
const hand = [sh[0] + dx * len, sh[1] + dy * len];
|
|
519
|
+
// arms don't squash with the body: build in a non-squashed frame
|
|
520
|
+
const s2 = { ...s, sx: (s.sx + 1) / 2, sy: (s.sy + 1) / 2 };
|
|
521
|
+
const P = spiritXf([sh, mid, hand], s2);
|
|
522
|
+
inkStroke(c, P, { seed: sd + 70 + side, w: lw * 1.05, t0: 4, t1: 2, min: 0.6, jit: 0.2, amp: 1.2, heavy: 0 });
|
|
523
|
+
const hp = P[2], R = s.H * 0.082 * (a.big ?? 1);
|
|
524
|
+
const g = circlePts(hp[0], hp[1], R, 16, R * 1.08);
|
|
525
|
+
fillShape(c, g, PAL.hi, { seed: sd + 80 + side, amp: 1, dx: REG.dx * 0.5, dy: REG.dy * 0.5 });
|
|
526
|
+
inkLoop(c, g, { seed: sd + 82 + side, w: lw * 0.9, amp: 1, t0: 4, t1: 8 });
|
|
527
|
+
// two finger lines
|
|
528
|
+
const fa = Math.atan2(P[2][1] - P[1][1], P[2][0] - P[1][0]);
|
|
529
|
+
for (const k of [-1, 1]) {
|
|
530
|
+
const a0 = fa + k * 0.35;
|
|
531
|
+
inkStroke(c, [[hp[0] + Math.cos(a0) * R * 0.2, hp[1] + Math.sin(a0) * R * 0.2], [hp[0] + Math.cos(a0) * R * 0.8, hp[1] + Math.sin(a0) * R * 0.8]], { seed: sd + 90 + k + side * 3, w: lw * 0.55, t0: 2, t1: 3, amp: 0.5, heavy: 0 });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ------------------------------------------------------------------ choreography of the spirit
|
|
537
|
+
function hopEnv(t, hops) { // returns { y offset, sq (sy multiplier), spin }
|
|
538
|
+
let y = 0, sq = 1, spin = 0;
|
|
539
|
+
for (const h of hops) {
|
|
540
|
+
const { t0, t1, h: hh } = h;
|
|
541
|
+
if (t > t0 && t < t1) { const p = inv(t0, t1, t); y -= hh * 4 * p * (1 - p); if (h.spin) spin = p; }
|
|
542
|
+
// anticipation squash before take-off, stretch after, squash on landing, overshoot
|
|
543
|
+
sq *= 1 + K(t, [[t0 - 0.125, 0], [t0 - 0.02, -0.2, 'out2'], [t0 + 0.04, 0.22, 'out2'], [lerp(t0, t1, 0.5), 0.0], [t1 - 0.04, 0.14], [t1, -0.3, 'in2'], [t1 + 0.085, 0.1, 'out2'], [t1 + 0.18, 0]]) * ((t > t0 - 0.13 && t < t1 + 0.2) ? 1 : 0);
|
|
544
|
+
}
|
|
545
|
+
return { y, sq, spin };
|
|
546
|
+
}
|
|
547
|
+
const GROUND = 745;
|
|
548
|
+
const HOPS = [
|
|
549
|
+
{ t0: 2.833, t1: 3.0, h: 120 },
|
|
550
|
+
{ t0: 3.333, t1: 3.5, h: 115 },
|
|
551
|
+
{ t0: 3.75, t1: 4.0, h: 175, spin: true },
|
|
552
|
+
];
|
|
553
|
+
function spiritState(t, D, m) {
|
|
554
|
+
if (t < 0.5 || t >= 6.5) return null;
|
|
555
|
+
const s = { x: 0, y: 0, H: 100, sx: 1, sy: 1, kx: 0, curl: 0, r: 0, wild: 0.12, face: null, arms: null };
|
|
556
|
+
if (t < 2.0) {
|
|
557
|
+
// sitting on the match head
|
|
558
|
+
const pm = matchState(Math.max(0, t - 1 / FPS));
|
|
559
|
+
s.x = m.hx; s.y = m.hy + 18;
|
|
560
|
+
s.H = K(t, [[0.5, 40], [0.542, 180, 'out3'], [0.625, 225, 'out2'], [0.792, 168], [1.0, 172], [1.25, 188, 'out2'], [1.75, 190]]);
|
|
561
|
+
s.wild = K(t, [[0.5, 1.2], [0.8, 0.5], [1.1, 0.15]]);
|
|
562
|
+
const vx = (m.hx - pm.hx) * FPS, vy = (m.hy - pm.hy) * FPS;
|
|
563
|
+
s.kx = clamp(-vx * 0.00035, -0.7, 0.7);
|
|
564
|
+
const sp = Math.hypot(vx, vy);
|
|
565
|
+
s.sy = 1 + Math.min(0.45, sp * 0.00012); s.sx = 1 / Math.sqrt(s.sy);
|
|
566
|
+
if (t >= 1.25) {
|
|
567
|
+
const pop = K(t, [[1.25, 1.35], [1.333, 1.0, 'out2']]);
|
|
568
|
+
const blink = t >= CUES.blink - 0.01 && t < CUES.blink + 0.07;
|
|
569
|
+
const look = K(t, [[1.458, 0], [1.5, -1, 'out3'], [1.625, -1], [1.667, 1, 'out3'], [1.75, 1], [1.833, 0, 'out2']]);
|
|
570
|
+
s.face = { eyes: blink ? 'closed' : 'open', look, pop, mouth: t < 1.5 ? 'o' : 'smile', fy: -0.1 };
|
|
571
|
+
s.kx += look * 0.06;
|
|
572
|
+
if (t >= 1.75) { // crouch: anticipation for the jump
|
|
573
|
+
const q = K(t, [[1.75, 1], [1.917, 0.64, 'out2'], [2.0, 0.6]]);
|
|
574
|
+
s.sy *= q; s.sx *= 1 / Math.pow(q, 0.8);
|
|
575
|
+
s.face.eyes = 'fierce'; s.face.mouth = 'grit';
|
|
576
|
+
s.x += (hs(D.id, 3) - 0.5) * 4;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return s;
|
|
580
|
+
}
|
|
581
|
+
if (t < 2.5) { // JUMP (on 1s)
|
|
582
|
+
const fi = clamp(Math.round(t * FPS) - 48, 0, 11);
|
|
583
|
+
s.x = lerp(1150, 840, fi / 12);
|
|
584
|
+
// R1s2 screen spacing: rise 80/52/27/10, hang (95% squash), fall 15/40/70/95/120/140 (ease-in)
|
|
585
|
+
s.y = [478, 403, 348, 316, 301, 300, 314, 351, 412, 493, 590, 697][fi];
|
|
586
|
+
s.H = K(t, [[2.0, 190], [2.4, 228, 'out2']]);
|
|
587
|
+
s.sy = [2.6, 1.3, 1.18, 1.06, 1.0, 0.95, 1.0, 1.06, 1.12, 1.18, 1.24, 1.26][fi];
|
|
588
|
+
if (fi === 0) s.smearUp = true;
|
|
589
|
+
s.sx = 1 / Math.pow(s.sy, 0.8);
|
|
590
|
+
s.kx = K(t, [[2.0, 0.25], [2.25, 0.05], [2.458, -0.15]]);
|
|
591
|
+
s.r = K(t, [[2.0, -0.25], [2.25, 0.1], [2.458, 0.05]]);
|
|
592
|
+
s.wild = 0.35;
|
|
593
|
+
s.face = { eyes: t < 2.2 ? 'squeeze' : 'open', mouth: t < 2.2 ? 'grin' : 'o', pop: 1.1 };
|
|
594
|
+
return s;
|
|
595
|
+
}
|
|
596
|
+
// ---- on the matchbox: landing, dance, spin, squash
|
|
597
|
+
s.H = 250; s.y = GROUND;
|
|
598
|
+
const xk = [[2.5, 840], [2.833, 840], [3.0, 690, 'io2'], [3.333, 690], [3.5, 965, 'io2'], [3.75, 965], [4.0, 810, 'io2'], [5.5, 820], [6.0, 860, 'io2']];
|
|
599
|
+
s.x = K(t, xk);
|
|
600
|
+
const vx = (K(t + 0.02, xk) - K(t - 0.02, xk)) / 0.04;
|
|
601
|
+
const he = hopEnv(t, HOPS);
|
|
602
|
+
s.y += he.y;
|
|
603
|
+
// landing from the big jump
|
|
604
|
+
let sq = he.sq * (1 + K(t, [[2.5, -0.44], [2.583, 0.14, 'out2'], [2.667, -0.05], [2.75, 0]]));
|
|
605
|
+
// groove bounce on the beats while grounded
|
|
606
|
+
if (t >= 4.0 && t < 4.5) sq *= 1 + 0.06 * Math.cos((t - 4.0) * Math.PI * 8);
|
|
607
|
+
sq = Math.max(0.62, sq);
|
|
608
|
+
s.sy = sq; s.sx = 1 / Math.pow(sq, 0.75);
|
|
609
|
+
s.kx = clamp(-vx * 0.0006, -0.35, 0.35);
|
|
610
|
+
s.curl = clamp(-vx * 0.0004, -0.3, 0.3);
|
|
611
|
+
s.wild = 0.14;
|
|
612
|
+
// arms pop out after landing
|
|
613
|
+
const armOn = t >= 2.583;
|
|
614
|
+
let AL = { ang: -0.6, bend: 0.3 }, AR = { ang: -0.6, bend: 0.3 };
|
|
615
|
+
let face = { eyes: 'open', mouth: 'smile' };
|
|
616
|
+
if (t < 2.75) { face = { eyes: t < 2.583 ? 'squeeze' : 'open', mouth: t < 2.583 ? 'grin' : 'open', pop: t < 2.667 ? 1.2 : 1 }; }
|
|
617
|
+
// dance arms: swing with the hops
|
|
618
|
+
const beatPh = (t - 2.5) * 2; // beats
|
|
619
|
+
const swing = Math.sin(beatPh * Math.PI);
|
|
620
|
+
AL = { ang: 0.2 + 0.9 * swing, bend: 0.5 }; AR = { ang: 0.2 - 0.9 * swing, bend: 0.5 };
|
|
621
|
+
if (t >= 2.5 && t < 2.83) { AL = { ang: K(t, [[2.583, 1.3], [2.75, 0.5]]), bend: 0.3 }; AR = { ang: K(t, [[2.583, 1.3], [2.75, 0.5]]), bend: 0.3 }; }
|
|
622
|
+
face.look = clamp(vx * 0.004, -1, 1);
|
|
623
|
+
// twirl in hop 3
|
|
624
|
+
if (he.spin > 0 && he.spin < 1) {
|
|
625
|
+
s.spinW = Math.cos(he.spin * Math.PI * 2);
|
|
626
|
+
if (Math.abs(s.spinW) < 0.35) s.spinW = Math.sign(s.spinW || 1) * 0.35;
|
|
627
|
+
face = s.spinW > 0 ? { eyes: 'happy', mouth: 'open' } : null;
|
|
628
|
+
AL = { ang: 1.4, bend: 0.2 }; AR = { ang: 1.4, bend: 0.2 };
|
|
629
|
+
}
|
|
630
|
+
if (t >= 4.0 && t < 4.5) { // shimmy: alternating lean every drawing
|
|
631
|
+
const k = Math.round((t - 4.0) * 12) % 2 ? 1 : -1;
|
|
632
|
+
s.kx += k * 0.14; s.curl += k * 0.08;
|
|
633
|
+
AL = { ang: 1.0 + k * 0.35, bend: -0.4 * k }; AR = { ang: 1.0 - k * 0.35, bend: 0.4 * k };
|
|
634
|
+
face = { eyes: 'happy', mouth: 'open', look: k * 0.5 };
|
|
635
|
+
}
|
|
636
|
+
if (t >= 4.5 && t < 5.0) { // TA-DA (held on 3s)
|
|
637
|
+
const q = K(t, [[4.5, 1.18], [4.625, 1.08, 'out2']]);
|
|
638
|
+
s.sy = q; s.sx = 1 / Math.pow(q, 0.8);
|
|
639
|
+
s.kx = 0; s.curl = 0;
|
|
640
|
+
AL = { ang: 0.85, bend: -0.15, len: 0.48, big: 1.15 }; AR = { ang: 0.85, bend: -0.15, len: 0.48, big: 1.15 };
|
|
641
|
+
face = { eyes: 'happy', mouth: 'grin', cheeks: true };
|
|
642
|
+
}
|
|
643
|
+
if (t >= 5.0 && t < 5.5) { // gathering: dips on 5.0 and 5.25, arms pulled in
|
|
644
|
+
const q = 1 + K(t, [[5.0, -0.2], [5.083, 0.05, 'out2'], [5.25, -0.24, 'in2'], [5.333, 0.08, 'out2'], [5.5, 0]]);
|
|
645
|
+
s.sy = q; s.sx = 1 / Math.pow(q, 0.8);
|
|
646
|
+
AL = { ang: K(t, [[5.0, 0.4], [5.4, -0.9]]), bend: 0.8 }; AR = { ang: K(t, [[5.0, 0.4], [5.4, -0.9]]), bend: 0.8 };
|
|
647
|
+
face = { eyes: 'fierce', mouth: 'smile' };
|
|
648
|
+
}
|
|
649
|
+
if (t >= 5.5 && t < 6.25) { // SPIN wind-up (on 1s)
|
|
650
|
+
const u = inv(5.5, 6.25, t);
|
|
651
|
+
const ph = 1.3 * u + 3.2 * u * u; // turns
|
|
652
|
+
s.spinW = Math.cos(ph * Math.PI * 2);
|
|
653
|
+
const back = s.spinW < 0;
|
|
654
|
+
if (Math.abs(s.spinW) < 0.22) s.spinW = (s.spinW < 0 ? -1 : 1) * 0.22;
|
|
655
|
+
s.y -= 70 * E.out2(u);
|
|
656
|
+
s.H = lerp(250, 330, E.io2(u));
|
|
657
|
+
s.wild = lerp(0.2, 0.7, u);
|
|
658
|
+
s.kx = 0.12 * Math.sin(ph * Math.PI * 2); s.curl = 0.25 * Math.sin(ph * Math.PI * 2 + 1);
|
|
659
|
+
s.sy = 1 + 0.08 * Math.sin(ph * 12); s.sx = 1;
|
|
660
|
+
face = back ? null : { eyes: 'fierce', mouth: 'grit' };
|
|
661
|
+
AL = null; AR = null;
|
|
662
|
+
s.spinU = u; s.spinPh = ph;
|
|
663
|
+
}
|
|
664
|
+
if (t >= 6.25) { // crouch, trembling, before the burst
|
|
665
|
+
const u = inv(6.25, 6.5, t);
|
|
666
|
+
s.y -= 70;
|
|
667
|
+
s.H = 330 * (1 + 0.1 * u);
|
|
668
|
+
const q = lerp(0.78, 0.6, E.out2(u));
|
|
669
|
+
s.sy = q; s.sx = 1 / Math.pow(q, 0.7);
|
|
670
|
+
s.x += (hs(D.id, 5) - 0.5) * 10 * (0.5 + u); s.y += (hs(D.id, 6) - 0.5) * 6;
|
|
671
|
+
s.wild = 0.9;
|
|
672
|
+
face = { eyes: 'squeeze', mouth: 'puff', cheeks: true };
|
|
673
|
+
AL = null; AR = null;
|
|
674
|
+
}
|
|
675
|
+
s.face = face;
|
|
676
|
+
s.arms = armOn && (AL || AR) ? { L: AL, R: AR } : null;
|
|
677
|
+
return s;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ------------------------------------------------------------------ small cel FX
|
|
681
|
+
function drawPuff(c, x, y, r, seed, o = {}) { // one little cloud puff (3–5 circles)
|
|
682
|
+
const n = o.n ?? 4;
|
|
683
|
+
const cs = [];
|
|
684
|
+
for (let i = 0; i < n; i++) {
|
|
685
|
+
const a = (i / n) * Math.PI * 2 + hs(seed, i) * 1.2;
|
|
686
|
+
const d = i === 0 ? 0 : r * 0.55;
|
|
687
|
+
cs.push({ x: x + Math.cos(a) * d * (o.wide ?? 1), y: y + Math.sin(a) * d * 0.7 * (o.flat ?? 1), r: r * (i === 0 ? 0.8 : 0.5 + 0.25 * hs(seed, i, 2)) });
|
|
688
|
+
}
|
|
689
|
+
cloud(c, cs, { seed, fill: o.fill ?? PAL.hi, w: o.w ?? Math.max(3, r * 0.08), holes: o.holes });
|
|
690
|
+
}
|
|
691
|
+
// union-of-circles cloud with a single boiling ink outline + optional dissipation holes
|
|
692
|
+
function cloud(c, cs, o) {
|
|
693
|
+
const { seed = 1, fill = PAL.hi, w = 6, amp = 1.8 } = o;
|
|
694
|
+
const L = LY.get(0);
|
|
695
|
+
L.save(); L.setTransform(c.getTransform());
|
|
696
|
+
const circ = (k, x, y, r, rx) => boil(circlePts(x, y, Math.max(1, r), Math.max(12, Math.round((rx ?? r) / 3)), rx), seed + k * 7, amp, 60);
|
|
697
|
+
// outer ink (radius varies per circle with a shadow-side bias)
|
|
698
|
+
L.fillStyle = PAL.ink;
|
|
699
|
+
cs.forEach((q, k) => {
|
|
700
|
+
if (q.r < 1) return;
|
|
701
|
+
const qrx = q.rx ?? q.r;
|
|
702
|
+
const P = circlePts(0, 0, 1, Math.max(12, Math.round(Math.max(q.r, qrx) / 3))).map(([ca, sa]) => {
|
|
703
|
+
const a = Math.atan2(sa, ca);
|
|
704
|
+
const ww = w * (0.75 + 0.55 * Math.max(0, Math.cos(a - 0.9)) + 0.25 * G.N3(ca * 2, sa * 2, seed + k));
|
|
705
|
+
return [q.x + ca * (qrx + ww * 0.5), q.y + sa * (q.r + ww * 0.5)];
|
|
706
|
+
});
|
|
707
|
+
L.beginPath(); polyPath(L, boil(P, seed + k * 7, amp, 60)); L.fill();
|
|
708
|
+
});
|
|
709
|
+
// colour (off-register) goes UNDER the ink: draw it on main first
|
|
710
|
+
c.save();
|
|
711
|
+
cs.forEach((q, k) => { if (q.r >= 1) fillShape(c, circlePts(q.x, q.y, q.r, Math.max(12, Math.round((q.rx ?? q.r) / 3)), q.rx), fill, { seed: seed + k * 7 + 3, amp, dx: REG.dx, dy: REG.dy }); });
|
|
712
|
+
c.restore();
|
|
713
|
+
// cut the inside of the ring
|
|
714
|
+
L.globalCompositeOperation = 'destination-out';
|
|
715
|
+
cs.forEach((q, k) => { if (q.r >= 1) { L.beginPath(); polyPath(L, circ(k, q.x, q.y, q.r - w * 0.5, q.rx !== undefined ? q.rx - w * 0.5 : undefined)); L.fill(); } });
|
|
716
|
+
L.globalCompositeOperation = 'source-over';
|
|
717
|
+
L.restore();
|
|
718
|
+
if (o.holes && o.holes.length) {
|
|
719
|
+
// dissipation: holes eat the colour + get their own inked rim
|
|
720
|
+
c.save();
|
|
721
|
+
// cut holes in colour on main by painting paper back is impossible -> use a second layer for the whole puff
|
|
722
|
+
c.restore();
|
|
723
|
+
}
|
|
724
|
+
blit(c, L);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ------------------------------------------------------------------ SMOKE group with dissipation (own layer)
|
|
728
|
+
function smokeGroup(c, puffs, o) {
|
|
729
|
+
// puffs: [{x,y,r,holes:[{x,y,r}]}]; drawn onto layer 1 (fill + ink), holes cut + rimmed there, then composited
|
|
730
|
+
const { seed = 1, w = 6, fill = PAL.hi } = o;
|
|
731
|
+
const L = LY.get(1);
|
|
732
|
+
L.save(); L.setTransform(c.getTransform());
|
|
733
|
+
const ring = LY.get(2); ring.save(); ring.setTransform(c.getTransform());
|
|
734
|
+
for (const [k, p] of puffs.entries()) {
|
|
735
|
+
if (p.r < 2) continue;
|
|
736
|
+
const cs = p.cs || [{ x: p.x, y: p.y, r: p.r }];
|
|
737
|
+
cs.forEach((q, j) => {
|
|
738
|
+
if (q.r < 1.5) return;
|
|
739
|
+
const n = Math.max(12, Math.round(q.r / 3));
|
|
740
|
+
fillShape(L, circlePts(q.x, q.y, q.r, n), fill, { seed: seed + k * 31 + j * 5, amp: 1.6, dx: REG.dx, dy: REG.dy });
|
|
741
|
+
ring.fillStyle = PAL.ink;
|
|
742
|
+
const P = circlePts(q.x, q.y, q.r, n).map(([x, y]) => {
|
|
743
|
+
const a = Math.atan2(y - q.y, x - q.x);
|
|
744
|
+
const ww = w * (0.7 + 0.6 * Math.max(0, Math.cos(a - 0.9)));
|
|
745
|
+
return [q.x + Math.cos(a) * (q.r + ww * 0.5), q.y + Math.sin(a) * (q.r + ww * 0.5)];
|
|
746
|
+
});
|
|
747
|
+
ring.beginPath(); polyPath(ring, boil(P, seed + k * 31 + j * 5, 1.6, 60)); ring.fill();
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
ring.globalCompositeOperation = 'destination-out';
|
|
751
|
+
for (const [k, p] of puffs.entries()) {
|
|
752
|
+
const cs = p.cs || [{ x: p.x, y: p.y, r: p.r }];
|
|
753
|
+
cs.forEach((q, j) => { if (q.r >= 1.5) { ring.beginPath(); polyPath(ring, boil(circlePts(q.x, q.y, q.r - w * 0.5, Math.max(12, Math.round(q.r / 3))), seed + k * 31 + j * 5 + 1, 1.6, 60)); ring.fill(); } });
|
|
754
|
+
}
|
|
755
|
+
ring.globalCompositeOperation = 'source-over';
|
|
756
|
+
ring.restore();
|
|
757
|
+
// combine ring over fill in layer 1
|
|
758
|
+
L.save(); L.setTransform(1, 0, 0, 1, 0, 0); L.drawImage(ring.canvas, 0, 0); L.restore();
|
|
759
|
+
// volume: one inked shade arc inside each puff (lower-right), like a hand-drawn smoke cel
|
|
760
|
+
for (const [k, p] of puffs.entries()) {
|
|
761
|
+
if (p.r < 20) continue;
|
|
762
|
+
const q = (p.cs || [p])[0];
|
|
763
|
+
const P = [];
|
|
764
|
+
const a0 = 0.15 + hs(seed, k, 5) * 0.3, a1 = a0 + 1.1 + hs(seed, k, 6) * 0.5;
|
|
765
|
+
for (let i = 0; i <= 10; i++) { const a = lerp(a0, a1, i / 10); P.push([q.x + Math.cos(a) * q.r * 0.66, q.y + Math.sin(a) * q.r * 0.62]); }
|
|
766
|
+
inkStroke(L, P, { seed: seed + 40 + k, w: w * 0.75, t0: 14, t1: 20, min: 0.1, amp: 1.2, heavy: 0 });
|
|
767
|
+
}
|
|
768
|
+
// holes: rim (source-atop) then cut
|
|
769
|
+
for (const [k, p] of puffs.entries()) {
|
|
770
|
+
for (const [j, hq] of (p.holes || []).entries()) {
|
|
771
|
+
if (hq.r < 1) continue;
|
|
772
|
+
const n = Math.max(10, Math.round(hq.r / 3));
|
|
773
|
+
L.globalCompositeOperation = 'source-atop';
|
|
774
|
+
L.fillStyle = PAL.ink; L.beginPath(); polyPath(L, boil(circlePts(hq.x, hq.y, hq.r + w * 0.9, n), seed + 900 + k * 17 + j, 1.4, 60)); L.fill();
|
|
775
|
+
L.globalCompositeOperation = 'destination-out';
|
|
776
|
+
L.beginPath(); polyPath(L, boil(circlePts(hq.x, hq.y, hq.r, n), seed + 950 + k * 17 + j, 1.4, 60)); L.fill();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
L.globalCompositeOperation = 'source-over';
|
|
780
|
+
L.restore();
|
|
781
|
+
blit(c, L);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ------------------------------------------------------------------ smoke wisp from the burnt match
|
|
785
|
+
function drawWisp(c, x, y, t, D, amt = 1, big = 0) {
|
|
786
|
+
if (amt <= 0) return;
|
|
787
|
+
const ph = D.fq / FPS * 5;
|
|
788
|
+
const Hh = 170 * (1 + 0.22 * big), dr = 250 * big; // R2s2 (P4): in the low shot the curl rises higher and drifts right into the empty third
|
|
789
|
+
const strand = (sd, off, w, k) => {
|
|
790
|
+
const P = [];
|
|
791
|
+
for (let i = 0; i <= 8; i++) {
|
|
792
|
+
const u = i / 8;
|
|
793
|
+
const v = u * (off ? 0.72 : 1);
|
|
794
|
+
P.push([x + off * v + Math.sin(v * 5 + ph + k) * (6 + (26 + 24 * big) * v) * amt + dr * v * v * amt, y - v * Hh * amt - off * 0.3 * v]);
|
|
795
|
+
}
|
|
796
|
+
inkStroke(c, P, { seed: sd + D.id, w, t0: 20, t1: 40, amp: 1.2, heavy: 0 });
|
|
797
|
+
return P;
|
|
798
|
+
};
|
|
799
|
+
const P = strand(700, 0, 5, 0);
|
|
800
|
+
if (big > 0) strand(710, 34 * big, 3.6, 1.7); // second, thinner strand
|
|
801
|
+
// curl at the top
|
|
802
|
+
const tp = P[P.length - 1];
|
|
803
|
+
const r = (16 + 24 * big) * amt;
|
|
804
|
+
const cp = [];
|
|
805
|
+
for (let i = 0; i <= 12; i++) { const a = -Math.PI / 2 + i * 0.55 + ph; const rr = r * (1 - i / 16); cp.push([tp[0] + Math.cos(a) * rr, tp[1] - r + Math.sin(a) * rr]); }
|
|
806
|
+
inkStroke(c, cp, { seed: 720 + D.id, w: 4 + big, t0: 6, t1: 20, amp: 1, heavy: 0 });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ------------------------------------------------------------------ TITLE 手作
|
|
810
|
+
// skeletons in unit char space (0..1, y down); type: h (横) v (竖) p (撇) g (竖钩)
|
|
811
|
+
const CHAR_SHOU = [
|
|
812
|
+
{ k: 'p', P: [[0.76, 0.055], [0.56, 0.13], [0.27, 0.185]] },
|
|
813
|
+
{ k: 'h', P: [[0.17, 0.385], [0.5, 0.36], [0.83, 0.335]] },
|
|
814
|
+
{ k: 'h', P: [[0.05, 0.61], [0.5, 0.58], [0.95, 0.545]] },
|
|
815
|
+
{ k: 'g', P: [[0.52, 0.15], [0.527, 0.55], [0.515, 0.875, 1], [0.32, 0.765]] },
|
|
816
|
+
];
|
|
817
|
+
const CHAR_ZUO = [
|
|
818
|
+
{ k: 'p', P: [[0.34, 0.04], [0.25, 0.26], [0.05, 0.48]] },
|
|
819
|
+
{ k: 'v', P: [[0.215, 0.31], [0.22, 0.62], [0.215, 0.95]] },
|
|
820
|
+
{ k: 'p', P: [[0.6, 0.05], [0.53, 0.2], [0.41, 0.35]] },
|
|
821
|
+
{ k: 'h', P: [[0.5, 0.235], [0.72, 0.225], [0.94, 0.215]] },
|
|
822
|
+
{ k: 'v', P: [[0.575, 0.25], [0.58, 0.6], [0.575, 0.95]] },
|
|
823
|
+
{ k: 'h', P: [[0.585, 0.47], [0.74, 0.465], [0.88, 0.46]] },
|
|
824
|
+
{ k: 'h', P: [[0.585, 0.7], [0.75, 0.695], [0.9, 0.69]] },
|
|
825
|
+
];
|
|
826
|
+
const TITLE = V5 ? { cx: 960, cy: 432, S: 300, gap: 8, w: 0.1, ink: 5.6 } : { cx: 960, cy: 440, S: 352, gap: 18 };
|
|
827
|
+
const V5CH = '逐帧手绘';
|
|
828
|
+
function charXf(i) {
|
|
829
|
+
const S = TITLE.S;
|
|
830
|
+
if (V5) { const J = [[-6, -0.04], [8, 0.03], [-5, -0.03], [7, 0.035]][i]; return { x: TITLE.cx + (i - 1.5) * (S + TITLE.gap), y: TITLE.cy + J[0], r: J[1], S }; }
|
|
831
|
+
return i === 0
|
|
832
|
+
? { x: TITLE.cx - S / 2 - TITLE.gap / 2 - 8, y: TITLE.cy - 6, r: -0.045, S }
|
|
833
|
+
: { x: TITLE.cx + S / 2 + TITLE.gap / 2 + 8, y: TITLE.cy + 8, r: 0.035, S: S * 1.0 };
|
|
834
|
+
}
|
|
835
|
+
function strokeList() {
|
|
836
|
+
const L = [];
|
|
837
|
+
[CHAR_SHOU, CHAR_ZUO].forEach((ch, ci) => ch.forEach((st, si) => L.push({ ...st, ci, si })));
|
|
838
|
+
return L;
|
|
839
|
+
}
|
|
840
|
+
let STROKES = strokeList(); // V5: replaced in init() by the 32 strokes of 逐帧手绘 (Make Me a Hanzi medians, real stroke order)
|
|
841
|
+
// V5 stroke kinds from the median shape: d (点), g (hook at the end), p (撇, ends down-left), n (捺, ends down-right), b (bone)
|
|
842
|
+
function v5Kind(P) {
|
|
843
|
+
const seg = [], L = [0];
|
|
844
|
+
for (let i = 1; i < P.length; i++) { seg.push([P[i][0] - P[i - 1][0], P[i][1] - P[i - 1][1]]); L.push(L[i - 1] + Math.hypot(...seg[i - 1])); }
|
|
845
|
+
const tot = L.at(-1);
|
|
846
|
+
if (tot < 0.13) return { k: 'd' };
|
|
847
|
+
for (let i = seg.length - 1; i >= 1; i--) { // last sharp reversal near the end = hook
|
|
848
|
+
const a = Math.atan2(seg[i - 1][1], seg[i - 1][0]), b = Math.atan2(seg[i][1], seg[i][0]);
|
|
849
|
+
let dA = Math.abs(b - a); if (dA > Math.PI) dA = 2 * Math.PI - dA;
|
|
850
|
+
const tail = tot - L[i];
|
|
851
|
+
if (tail > 0.3 * tot) break;
|
|
852
|
+
if (dA > 1.75 && tail > 0.03) return { k: 'g', h: L[i] / tot };
|
|
853
|
+
}
|
|
854
|
+
let j = 0; while (j < L.length - 1 && L[j] < 0.7 * tot) j++;
|
|
855
|
+
const ex = P.at(-1)[0] - P[Math.min(j, P.length - 2)][0], ey = P.at(-1)[1] - P[Math.min(j, P.length - 2)][1], el = Math.hypot(ex, ey) || 1;
|
|
856
|
+
if (ex < -0.3 * el && ey > 0.1 * el) return { k: 'p' };
|
|
857
|
+
const sx = P.at(-1)[0] - P[0][0], sy = P.at(-1)[1] - P[0][1];
|
|
858
|
+
if (ex > 0.4 * el && ey > 0.15 * el && sy > 0.12 && sx > 0.12 && tot > 0.25) return { k: 'n' };
|
|
859
|
+
return { k: 'b' };
|
|
860
|
+
}
|
|
861
|
+
async function loadV5Strokes() {
|
|
862
|
+
const out = [], per = 0.125, dur = 0.14, cStart = [7.25, 7.292, 7.333, 7.375]; // 4 fuses, one per character, on 1s; strokes on 16ths (+1-frame flam per character)
|
|
863
|
+
for (const [ci, ch] of [...V5CH].entries()) {
|
|
864
|
+
const d = await (await fetch(`/demos/05-cel-boil/assets/hanzi/${encodeURIComponent(ch)}.json`)).json();
|
|
865
|
+
d.medians.forEach((m, si) => {
|
|
866
|
+
const P = m.map(([x, y]) => [x / 1024, (900 - y) / 1024]);
|
|
867
|
+
for (let i = 1; i < P.length - 1; i++) { // keep real corners (横折) sharp
|
|
868
|
+
const ax = P[i][0] - P[i - 1][0], ay = P[i][1] - P[i - 1][1], bx = P[i + 1][0] - P[i][0], by = P[i + 1][1] - P[i][1];
|
|
869
|
+
if (Math.hypot(ax, ay) < 0.035 || Math.hypot(bx, by) < 0.035) continue;
|
|
870
|
+
let dA = Math.abs(Math.atan2(by, bx) - Math.atan2(ay, ax)); if (dA > Math.PI) dA = 2 * Math.PI - dA;
|
|
871
|
+
if (dA > 1.0) P[i][2] = 1;
|
|
872
|
+
}
|
|
873
|
+
out.push({ P, ...v5Kind(P), ci, si, first: si === 0, t0: cStart[ci] + si * per, dur });
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
return out;
|
|
877
|
+
}
|
|
878
|
+
function strokeWorld(st) {
|
|
879
|
+
const X = charXf(st.ci);
|
|
880
|
+
return xf(st.P.map(([x, y, cr]) => [(x - 0.5) * X.S, (y - 0.5) * X.S, cr]), { x: X.x, y: X.y, r: X.r });
|
|
881
|
+
}
|
|
882
|
+
function strokeWidth(k, u, W0, st) {
|
|
883
|
+
if (V5 && st) {
|
|
884
|
+
if (k === 'd') return W0 * (0.95 + 0.45 * Math.sin(Math.PI * Math.min(1, u * 1.1)));
|
|
885
|
+
if (k === 'n') return W0 * (u < 0.82 ? 0.9 + 0.4 * sstep(0, 0.82, u) : lerp(1.3, 0.6, (u - 0.82) / 0.18));
|
|
886
|
+
if (k === 'g') return W0 * (u < st.h ? 1.0 + 0.06 * Math.sin(u * Math.PI / st.h) : lerp(1.0, 0.45, (u - st.h) / (1 - st.h)));
|
|
887
|
+
}
|
|
888
|
+
if (k === 'p') return W0 * (1.12 - 0.62 * Math.pow(u, 1.4));
|
|
889
|
+
if (k === 'g') return W0 * (u < 0.8 ? 1.0 + 0.08 * Math.sin(u * Math.PI) : lerp(1.0, 0.5, (u - 0.8) / 0.2));
|
|
890
|
+
// bone: a touch fatter at both ends
|
|
891
|
+
return W0 * (0.94 + 0.16 * Math.pow(Math.abs(u - 0.5) * 2, 2));
|
|
892
|
+
}
|
|
893
|
+
function titleDense(st, seed, amp) {
|
|
894
|
+
const pts = resample(spline(strokeWorld(st), false, 5), 3.5);
|
|
895
|
+
return boil(pts, seed, amp, 80);
|
|
896
|
+
}
|
|
897
|
+
// draws the title strokes (each with progress 0..1) with union ink outline, off-register fill, drop shadow
|
|
898
|
+
function drawTitle(c, prog, D, o) {
|
|
899
|
+
const W0 = TITLE.S * (TITLE.w ?? 0.165), ink = TITLE.ink ?? 6.8;
|
|
900
|
+
const sv = bv(D);
|
|
901
|
+
const polys = (grow, seedOff, amp, prg = prog) => STROKES.map((st, i) => {
|
|
902
|
+
const p = prg[i]; if (p <= 0) return null;
|
|
903
|
+
const full = titleDense(st, 3000 + i * 11 + seedOff + sv * 101, amp);
|
|
904
|
+
const part = prefix(full, E.out2(p) * 0.999 + 0.001 * p);
|
|
905
|
+
if (part.length < 2) return null;
|
|
906
|
+
const tot = arclen(full).at(-1);
|
|
907
|
+
return outline(part, (u, s) => strokeWidth(st.k, s / tot, W0, st) + grow(s, i), true);
|
|
908
|
+
}).filter(Boolean);
|
|
909
|
+
// drop shadow (ink block shadow, pops at the settle)
|
|
910
|
+
if (o.shadow > 0) {
|
|
911
|
+
const sx = 9 * o.shadow, sy = 12 * o.shadow;
|
|
912
|
+
c.save(); c.translate(sx, sy);
|
|
913
|
+
for (const p of polys(() => ink * 2, 0, 1.6)) fillPoly(c, p, PAL.ink);
|
|
914
|
+
c.restore();
|
|
915
|
+
}
|
|
916
|
+
// fill (off-register, independently boiled)
|
|
917
|
+
c.save(); c.translate(REG.dx * 0.9, REG.dy * 0.9);
|
|
918
|
+
for (const p of polys(() => 1.5, 55, 1.8)) fillPoly(c, p, PAL.yel);
|
|
919
|
+
c.restore();
|
|
920
|
+
// highlights
|
|
921
|
+
if (o.shine > 0) {
|
|
922
|
+
STROKES.forEach((st, i) => {
|
|
923
|
+
if (prog[i] < 0.6) return;
|
|
924
|
+
const full = titleDense(st, 5000 + i * 7 + sv * 13, 1.2);
|
|
925
|
+
const n = full.length;
|
|
926
|
+
const a = Math.floor(n * 0.14), b = Math.floor(n * (st.k === 'p' ? 0.4 : 0.36));
|
|
927
|
+
const seg = full.slice(a, b).map((q, j, arr) => {
|
|
928
|
+
const q0 = arr[Math.max(0, j - 1)], q1 = arr[Math.min(arr.length - 1, j + 1)];
|
|
929
|
+
let tx = q1[0] - q0[0], ty = q1[1] - q0[1]; const tl = Math.hypot(tx, ty) || 1; tx /= tl; ty /= tl;
|
|
930
|
+
// offset toward upper-left side
|
|
931
|
+
let nx = -ty, ny = tx; if (nx + ny > 0) { nx = -nx; ny = -ny; }
|
|
932
|
+
return [q[0] + nx * W0 * 0.24, q[1] + ny * W0 * 0.24];
|
|
933
|
+
});
|
|
934
|
+
if (seg.length > 2) inkStroke(c, seg, { seed: 5100 + i + sv, w: W0 * 0.16 * o.shine, t0: 10, t1: 18, color: PAL.hi, amp: 0.6, heavy: 0, dense: true });
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
// ink ring = fat minus thin (union outline)
|
|
938
|
+
const L = LY.get(0);
|
|
939
|
+
L.save(); L.setTransform(c.getTransform());
|
|
940
|
+
for (const p of polys((s, i) => ink * 2 * (0.8 + 0.35 * G.N3(s / 50, i, sv * 3.3)), 0, 1.6)) fillPoly(L, p, PAL.ink);
|
|
941
|
+
L.globalCompositeOperation = 'destination-out';
|
|
942
|
+
for (const p of polys(() => 0, 0, 1.6)) fillPoly(L, p, '#000');
|
|
943
|
+
L.restore();
|
|
944
|
+
blit(c, L);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ------------------------------------------------------------------ hand-lettered HAND MADE (monoline marker caps)
|
|
948
|
+
const LET = {
|
|
949
|
+
H: { w: 0.62, s: [[[0, 0], [0, 1]], [[0.62, 0], [0.62, 1]], [[0, 0.5], [0.62, 0.48]]] },
|
|
950
|
+
A: { w: 0.7, s: [[[0, 1], [0.35, 0, 1], [0.7, 1]], [[0.15, 0.64], [0.55, 0.62]]] },
|
|
951
|
+
N: { w: 0.62, s: [[[0, 1], [0, 0, 1], [0.62, 1, 1], [0.62, 0]]] },
|
|
952
|
+
D: { w: 0.62, s: [[[0, 0], [0, 1]], [[0, 0, 1], [0.34, 0.02], [0.6, 0.28], [0.62, 0.52], [0.56, 0.8], [0.32, 0.99], [0, 1, 1]]] },
|
|
953
|
+
M: { w: 0.8, s: [[[0, 1], [0.03, 0, 1], [0.4, 0.68, 1], [0.77, 0, 1], [0.8, 1]]] },
|
|
954
|
+
E: { w: 0.54, s: [[[0.54, 0], [0, 0, 1], [0, 1, 1], [0.56, 1]], [[0, 0.5], [0.44, 0.49]]] },
|
|
955
|
+
// V5 only (FRAME BY FRAME)
|
|
956
|
+
F: { w: 0.54, s: [[[0.55, 0], [0, 0, 1], [0, 1]], [[0, 0.49], [0.42, 0.48]]] },
|
|
957
|
+
R: { w: 0.6, s: [[[0, 1], [0, 0]], [[0, 0, 1], [0.34, 0.01], [0.56, 0.1], [0.6, 0.26], [0.5, 0.43], [0.28, 0.5], [0, 0.5, 1]], [[0.26, 0.5], [0.62, 1]]] },
|
|
958
|
+
B: { w: 0.6, s: [[[0, 0], [0, 1]], [[0, 0, 1], [0.3, 0.01], [0.5, 0.09], [0.53, 0.25], [0.4, 0.43], [0.18, 0.48], [0, 0.48, 1]], [[0, 0.48, 1], [0.34, 0.5], [0.58, 0.61], [0.62, 0.8], [0.5, 0.95], [0.28, 1], [0, 1, 1]]] },
|
|
959
|
+
Y: { w: 0.64, s: [[[0, 0], [0.32, 0.5, 1], [0.64, 0]], [[0.32, 0.5], [0.32, 1]]] },
|
|
960
|
+
};
|
|
961
|
+
function drawWord(c, word, x0, y0, h, D, o = {}) {
|
|
962
|
+
const tr = o.track ?? 0.36;
|
|
963
|
+
let x = x0;
|
|
964
|
+
const out = [];
|
|
965
|
+
for (let i = 0; i < word.length; i++) {
|
|
966
|
+
const ch = word[i];
|
|
967
|
+
if (ch === ' ') { x += h * 0.55; continue; }
|
|
968
|
+
const L = LET[ch];
|
|
969
|
+
out.push({ ch, x, L, i });
|
|
970
|
+
x += (L.w + tr) * h;
|
|
971
|
+
}
|
|
972
|
+
const total = x - x0 - tr * h;
|
|
973
|
+
const shift = o.center ? -total / 2 : 0;
|
|
974
|
+
const SL = [];
|
|
975
|
+
for (const g of out) {
|
|
976
|
+
const pop = o.pop ? o.pop(g.i) : 1;
|
|
977
|
+
if (pop <= 0) continue;
|
|
978
|
+
const bounce = Math.sin(g.i * 2.1) * h * 0.06;
|
|
979
|
+
const rot = (hs(g.i, 77) - 0.5) * 0.12;
|
|
980
|
+
for (const [k, S] of g.L.s.entries()) {
|
|
981
|
+
const P = xf(S.map(([u, v, cr]) => [(u - g.L.w / 2) * h, (v - 0.5) * h, cr]), { x: g.x + shift + g.L.w * h / 2, y: y0 + bounce, r: rot, sx: pop, sy: pop });
|
|
982
|
+
SL.push({ P, st: { seed: 8000 + g.i * 13 + k + bv(D) * 101, w: h * (o.wk ?? 0.15), t0: 6, t1: 6, min: 0.75, jit: 0.15, heavy: 0, amp: 1.3, step: 3 } });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (o.outline) {
|
|
986
|
+
const ow = o.outline * 2, sh = o.shadow ?? 0;
|
|
987
|
+
if (sh) SL.forEach(({ P, st }) => inkStroke(c, P.map(([x, y, cr]) => [x + sh, y + sh, cr]), { ...st, w: st.w + ow, color: PAL.ink }));
|
|
988
|
+
SL.forEach(({ P, st }) => inkStroke(c, P, { ...st, w: st.w + ow, color: PAL.ink }));
|
|
989
|
+
}
|
|
990
|
+
SL.forEach(({ P, st }) => inkStroke(c, P, { ...st, color: o.color ?? PAL.ink }));
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ------------------------------------------------------------------ tagline from real glyph outlines (boiled)
|
|
994
|
+
function glyphPolys(font, text, size, tracking) {
|
|
995
|
+
const polys = [];
|
|
996
|
+
let x = 0;
|
|
997
|
+
for (const ch of text) {
|
|
998
|
+
const g = font.charToGlyph(ch);
|
|
999
|
+
const path = g.getPath(x, 0, size);
|
|
1000
|
+
let cur = [], lx = 0, ly = 0;
|
|
1001
|
+
for (const cmd of path.commands) {
|
|
1002
|
+
if (cmd.type === 'M') { if (cur.length > 2) polys.push(cur); cur = [[cmd.x, cmd.y]]; lx = cmd.x; ly = cmd.y; }
|
|
1003
|
+
else if (cmd.type === 'L') { cur.push([cmd.x, cmd.y]); lx = cmd.x; ly = cmd.y; }
|
|
1004
|
+
else if (cmd.type === 'Q') { for (let k = 1; k <= 6; k++) { const u = k / 6, a = (1 - u) * (1 - u), b = 2 * u * (1 - u), cc = u * u; cur.push([a * lx + b * cmd.x1 + cc * cmd.x, a * ly + b * cmd.y1 + cc * cmd.y]); } lx = cmd.x; ly = cmd.y; }
|
|
1005
|
+
else if (cmd.type === 'C') { for (let k = 1; k <= 8; k++) { const u = k / 8, a = (1 - u) ** 3, b = 3 * u * (1 - u) ** 2, cc = 3 * u * u * (1 - u), d = u ** 3; cur.push([a * lx + b * cmd.x1 + cc * cmd.x2 + d * cmd.x, a * ly + b * cmd.y1 + cc * cmd.y2 + d * cmd.y]); } lx = cmd.x; ly = cmd.y; }
|
|
1006
|
+
else if (cmd.type === 'Z') { if (cur.length > 2) polys.push(cur); cur = []; }
|
|
1007
|
+
}
|
|
1008
|
+
if (cur.length > 2) polys.push(cur);
|
|
1009
|
+
x += g.advanceWidth * (size / font.unitsPerEm) + tracking;
|
|
1010
|
+
}
|
|
1011
|
+
return { polys, width: x - tracking };
|
|
1012
|
+
}
|
|
1013
|
+
let TAG = null;
|
|
1014
|
+
function drawTagline(c, D, reveal, x, y) {
|
|
1015
|
+
if (!TAG || reveal <= 0) return;
|
|
1016
|
+
const sv = bv(D);
|
|
1017
|
+
const total = TAG.a.width + TAG.gap + TAG.b.width;
|
|
1018
|
+
const x0 = x - total / 2;
|
|
1019
|
+
c.save();
|
|
1020
|
+
// brush-reveal mask stepping left→right
|
|
1021
|
+
c.beginPath(); c.rect(x0 - 20, y - 90, (total + 40) * reveal, 160); c.clip();
|
|
1022
|
+
c.beginPath();
|
|
1023
|
+
for (const [run, ox] of [[TAG.a, x0], [TAG.b, x0 + TAG.a.width + TAG.gap]]) {
|
|
1024
|
+
for (const P of run.polys) {
|
|
1025
|
+
const Q = boil(resample(P.map(([px, py]) => [px + ox, py + y]), 2.2, true), 6000 + sv * 17, 0.9, 30);
|
|
1026
|
+
polyPath(c, Q);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
c.fillStyle = PAL.ink; c.fill('nonzero');
|
|
1030
|
+
c.restore();
|
|
1031
|
+
const tx = x0 + TAG.a.width + TAG.gap / 2;
|
|
1032
|
+
if (reveal > (tx - x0 + 10) / (total + 40)) drawTwinkle(c, tx, y - 21, 20 + 3 * (sv % 2), 6100 + sv, { w: 3 });
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ------------------------------------------------------------------ BURST / BADGE choreography
|
|
1036
|
+
const C0 = [862, 614]; // burst centre (where the spirit crouches, in screen space)
|
|
1037
|
+
const BADGE = V5 ? { x: 960, y: 460, rx: 700, ry: 300 } : { x: 960, y: 464, rx: 520, ry: 262 }; // V5: wider badge for 4 characters
|
|
1038
|
+
let BADGE_PERIM = null;
|
|
1039
|
+
// R2 (REVIEW_2 P2): fireball -> badge in 4 drawings on 2s (f168/170/172/174), ease-out spacing .32/.28/.22/.18
|
|
1040
|
+
function morphP(t) { return t < 6.99 ? 0 : t < 7.07 ? 0.32 : t < 7.15 ? 0.6 : t < 7.24 ? 0.82 : 1; }
|
|
1041
|
+
function badgeCircles(t, D) {
|
|
1042
|
+
// fire circles: explode out of C0, then migrate into a scalloped badge around the title
|
|
1043
|
+
const N = 22;
|
|
1044
|
+
if (!BADGE_PERIM) {
|
|
1045
|
+
const dense = circlePts(0, 0, BADGE.ry, 600, BADGE.rx);
|
|
1046
|
+
const per = arclen(dense, true).at(-1);
|
|
1047
|
+
BADGE_PERIM = resample(dense, per / N, true).slice(0, N);
|
|
1048
|
+
}
|
|
1049
|
+
const out = [];
|
|
1050
|
+
const tb = t - 6.583;
|
|
1051
|
+
for (let k = 0; k < N; k++) {
|
|
1052
|
+
const [px, py] = BADGE_PERIM[k];
|
|
1053
|
+
const a0 = Math.atan2(py / BADGE.ry, px / BADGE.rx);
|
|
1054
|
+
// exploded state (fireball lumps)
|
|
1055
|
+
const rr = 200 + 240 * hs(k, 2);
|
|
1056
|
+
const e = E.out3(clamp(tb / 0.4));
|
|
1057
|
+
const ex = C0[0] + Math.cos(a0 + 0.25) * rr * e, ey = C0[1] + Math.sin(a0 + 0.25) * rr * 0.8 * e;
|
|
1058
|
+
const er = (58 + 70 * hs(k, 3)) * E.outBack(clamp(tb / 0.25), 1.4);
|
|
1059
|
+
// badge state: evenly spaced scallops, alternating size
|
|
1060
|
+
const bx = BADGE.x + px, by = BADGE.y + py;
|
|
1061
|
+
const br = (k % 2 ? 84 : 100) + 14 * hs(k, 6);
|
|
1062
|
+
const sk = 0.14 * hs(k, 7); // small per-scallop stagger; all home on f174
|
|
1063
|
+
const mu = clamp((morphP(t) - sk) / (1 - sk));
|
|
1064
|
+
out.push({ x: lerp(ex, bx, mu), y: lerp(ey, by, mu), r: Math.max(0, lerp(er, br, mu)), mu });
|
|
1065
|
+
}
|
|
1066
|
+
// core: grows out of the starburst into the badge body (an ellipse)
|
|
1067
|
+
const coreU = E.out3(inv(6.62, 6.9, t));
|
|
1068
|
+
const mv = morphP(t);
|
|
1069
|
+
const cx = lerp(C0[0], BADGE.x, mv), cy = lerp(C0[1], BADGE.y, mv);
|
|
1070
|
+
const core = [{ x: cx, y: cy, r: lerp(260, BADGE.ry * 1.0, mv) * coreU * (1 + 0.06 * Math.sin(Math.PI * mv)), rx: lerp(260, BADGE.rx * 1.0, mv) * coreU }];
|
|
1071
|
+
// R1s3: while migrating, clamp every scallop so it overlaps the core ellipse by >= 0.5 r (no channel / bite / paper hole).
|
|
1072
|
+
// Weighted by mu so the exploded fireball lumps (mu = 0) stay free; at rest (centre on the rim) the clamp is a no-op.
|
|
1073
|
+
{
|
|
1074
|
+
const erx = core[0].rx, ery = core[0].r;
|
|
1075
|
+
if (erx > 1 && ery > 1) for (const s of out) {
|
|
1076
|
+
const w = clamp(s.mu / 0.3);
|
|
1077
|
+
if (w <= 0) continue;
|
|
1078
|
+
const dx = s.x - cx, dy = s.y - cy, d = Math.hypot(dx, dy);
|
|
1079
|
+
if (d < 1) continue;
|
|
1080
|
+
const ca = dx / d, sa = dy / d, Re = 1 / Math.hypot(ca / erx, sa / ery);
|
|
1081
|
+
const dt = Re + 0.5 * s.r;
|
|
1082
|
+
if (d > dt) { const dn = lerp(d, dt, w); s.x = cx + ca * dn; s.y = cy + sa * dn; }
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
for (let k = 0; k < 6; k++) {
|
|
1086
|
+
const a = k * 1.047 + 0.4;
|
|
1087
|
+
const d = lerp(170, 120, mv);
|
|
1088
|
+
core.push({ x: cx + Math.cos(a) * d * lerp(1, 2.2, mv), y: cy + Math.sin(a) * d * lerp(1, 0.9, mv), r: 150 * coreU });
|
|
1089
|
+
}
|
|
1090
|
+
// R1s2: while the scallops migrate, bridge each neighbour pair with a circle pulled toward the core -> the union has no
|
|
1091
|
+
// inward bite (jury: notch at 7.20). At rest the bridges sit inside the badge ellipse, so the final outline is unchanged.
|
|
1092
|
+
const brg = [];
|
|
1093
|
+
for (let k = 0; k < N; k++) {
|
|
1094
|
+
const a = out[k], b = out[(k + 1) % N], w = clamp((Math.min(a.mu, b.mu) - 0.2) / 0.3);
|
|
1095
|
+
if (w <= 0) continue;
|
|
1096
|
+
const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
|
|
1097
|
+
brg.push({ x: lerp(mx, cx, 0.3), y: lerp(my, cy, 0.3), r: (a.r + b.r) * 0.5 * w });
|
|
1098
|
+
}
|
|
1099
|
+
const all = [...core, ...out, ...brg];
|
|
1100
|
+
if (t > 6.6 && t < 7.45) all.push(...holeFillers(all));
|
|
1101
|
+
return all;
|
|
1102
|
+
}
|
|
1103
|
+
// R1s3: coarse raster of the circle union; flood the outside from the border; any uncovered cell NOT reached is an
|
|
1104
|
+
// enclosed paper hole -> plug it with a small circle (appended last, so no other circle's boil seed changes).
|
|
1105
|
+
function holeFillers(cs) {
|
|
1106
|
+
const S = 6;
|
|
1107
|
+
let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
|
|
1108
|
+
for (const q of cs) { if (q.r < 1) continue; const rx = q.rx ?? q.r; x0 = Math.min(x0, q.x - rx); x1 = Math.max(x1, q.x + rx); y0 = Math.min(y0, q.y - q.r); y1 = Math.max(y1, q.y + q.r); }
|
|
1109
|
+
if (x1 <= x0) return [];
|
|
1110
|
+
x0 -= 2 * S; y0 -= 2 * S;
|
|
1111
|
+
const W = Math.ceil((x1 - x0) / S) + 4, H = Math.ceil((y1 - y0) / S) + 4;
|
|
1112
|
+
const g = new Uint8Array(W * H); // 1 = covered, 2 = outside
|
|
1113
|
+
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) {
|
|
1114
|
+
const px = x0 + (i + 0.5) * S, py = y0 + (j + 0.5) * S;
|
|
1115
|
+
for (const q of cs) {
|
|
1116
|
+
if (q.r < 1) continue;
|
|
1117
|
+
const rx = q.rx ?? q.r, u = (px - q.x) / rx, v = (py - q.y) / q.r;
|
|
1118
|
+
if (u * u + v * v <= 1) { g[j * W + i] = 1; break; }
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const st = [0];
|
|
1122
|
+
g[0] = 2;
|
|
1123
|
+
while (st.length) {
|
|
1124
|
+
const p = st.pop(), i = p % W, j = (p - i) / W;
|
|
1125
|
+
for (const [a, b] of [[i + 1, j], [i - 1, j], [i, j + 1], [i, j - 1]]) {
|
|
1126
|
+
if (a < 0 || b < 0 || a >= W || b >= H) continue;
|
|
1127
|
+
const n = b * W + a;
|
|
1128
|
+
if (g[n] === 0) { g[n] = 2; st.push(n); }
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const f = [];
|
|
1132
|
+
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) if (g[j * W + i] === 0) f.push({ x: x0 + (i + 0.5) * S, y: y0 + (j + 0.5) * S, r: S * 1.3 + 6 });
|
|
1133
|
+
return f;
|
|
1134
|
+
}
|
|
1135
|
+
function drawBadge(c, t, D) {
|
|
1136
|
+
if (t < 6.583) return;
|
|
1137
|
+
const sv = bv(D);
|
|
1138
|
+
const cs = badgeCircles(t, D);
|
|
1139
|
+
// settle boing at 8.5
|
|
1140
|
+
const sc = 1 + K(t, [[8.5, 0], [8.54, 0.045, 'out2'], [8.625, -0.02], [8.75, 0.008], [8.875, 0]]);
|
|
1141
|
+
const ox = t >= 7.24 && t < 7.32 ? 1.03 : 1, oy = t >= 7.24 && t < 7.32 ? 0.99 : 1; // R2: +3 % width overshoot on f174
|
|
1142
|
+
const cs2 = cs.map((q) => ({ x: BADGE.x + (q.x - BADGE.x) * sc * ox, y: BADGE.y + (q.y - BADGE.y) * sc * oy, r: q.r * sc * oy, rx: (q.rx ?? q.r) * sc * ox }));
|
|
1143
|
+
cloud(c, cs2, { seed: 1200 + sv * 57 + (t < 7.0 ? D.id : 0), fill: PAL.red, w: 8.5, amp: 2.0 });
|
|
1144
|
+
const q = cs[0];
|
|
1145
|
+
// R2 (REVIEW_2 P2): 6 big fire lumps (90-140 px, tomato-on-tomato, ink C-curl, yellow highlight) slide OUT into the
|
|
1146
|
+
// scallops with the morph; 3 faint lumps keep boiling inside the badge 7.2-8.5
|
|
1147
|
+
if (t >= 6.7 && t < 7.3 && q.r > 20) {
|
|
1148
|
+
const lu = morphP(t);
|
|
1149
|
+
for (let k = 0; k < 6; k++) {
|
|
1150
|
+
const a = k * 1.05 + 0.5 + hs(k, 81) * 0.4, d0 = 0.34 + 0.2 * hs(k, 82);
|
|
1151
|
+
const rim = BADGE_PERIM[(k * 4 + 1) % BADGE_PERIM.length];
|
|
1152
|
+
const x = lerp(q.x + Math.cos(a) * (q.rx ?? q.r) * d0, BADGE.x + rim[0] * 0.9, lu);
|
|
1153
|
+
const y = lerp(q.y + Math.sin(a) * q.r * d0, BADGE.y + rim[1] * 0.9, lu);
|
|
1154
|
+
const r = (46 + 24 * hs(k, 83)) * (1 - 0.3 * lu) * E.outBack(clamp((t - 6.7) / 0.1), 1.5);
|
|
1155
|
+
fillShape(c, circlePts(x, y, r, 30), '#c9311f', { seed: 1340 + k + D.id, amp: 2.5 });
|
|
1156
|
+
fillShape(c, circlePts(x - r * 0.26, y - r * 0.28, r * 0.5, 20), PAL.yel, { seed: 1345 + k + D.id, amp: 1.5 });
|
|
1157
|
+
const P = [], c0 = hs(k, 84) * 6.28 + D.id * 0.3;
|
|
1158
|
+
for (let i = 0; i <= 14; i++) { const aa = c0 + (i / 14) * 3.9, rr = r * (0.95 - 0.4 * i / 14); P.push([x + Math.cos(aa) * rr, y + Math.sin(aa) * rr * 0.85]); }
|
|
1159
|
+
inkStroke(c, P, { seed: 1350 + k * 3 + D.id, w: 6, t0: 6, t1: 22, min: 0.2, amp: 1.2, heavy: 0 });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (t >= 7.2 && t < 8.5) {
|
|
1163
|
+
[[540, 470, 50], [1378, 410, 44], [1380, 548, 34]].forEach(([x, y, r0], k) => {
|
|
1164
|
+
const rr = r0 * E.outBack(clamp((t - 7.2) / 0.1), 1.4) * (1 + 0.08 * Math.sin(D.id * 1.9 + k * 2.1));
|
|
1165
|
+
fillShape(c, circlePts(x + 5 * Math.sin(D.id * 0.7 + k), y + 4 * Math.cos(D.id * 0.9 + k), rr, 26), '#d8392a', { seed: 1370 + k * 5 + bv(D), amp: 3 });
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
// R2 (REVIEW_2 P2): yellow core holds in the fireball through f170, then travels in 2 drawings (f172, f174) to the start
|
|
1169
|
+
// of 手's first stroke, 100 -> 45 -> 20 %, with a 3-dot comet trail, and lights the fuse on arrival
|
|
1170
|
+
if (t < 7.3 && q.r > 20) {
|
|
1171
|
+
const p0 = strokeWorld(STROKES[0])[0];
|
|
1172
|
+
const stp = t < 7.12 ? 0 : t < 7.2 ? 1 : 2;
|
|
1173
|
+
const e = [0, 0.55, 1][stp], R = Math.min(q.r * 0.8, 240) * [1, 0.45, 0.2][stp];
|
|
1174
|
+
const ax = q.x + 6, ay = q.y + 4;
|
|
1175
|
+
const mx = (ax + p0[0]) / 2 - (p0[1] - ay) * 0.35, my = (ay + p0[1]) / 2 + (p0[0] - ax) * 0.35;
|
|
1176
|
+
const at = (u) => [(1 - u) * (1 - u) * ax + 2 * u * (1 - u) * mx + u * u * p0[0], (1 - u) * (1 - u) * ay + 2 * u * (1 - u) * my + u * u * p0[1]];
|
|
1177
|
+
const [gx, gy] = at(e), gs = stp === 0 && !V5 ? (q.rx ?? q.r) / q.r : 1; // V5: the core stays round (it is the reel's dot here)
|
|
1178
|
+
if (!NODOT) {
|
|
1179
|
+
if (stp > 0) for (let j = 1; j <= 3; j++) { // comet trail
|
|
1180
|
+
const [tx, ty] = at(Math.max(0, e - 0.13 * j)), tr = R * [0.62, 0.45, 0.3][j - 1];
|
|
1181
|
+
const T = circlePts(tx, ty, tr, 16);
|
|
1182
|
+
fillShape(c, T, PAL.yel, { seed: 1360 + j + D.id, amp: 1 });
|
|
1183
|
+
inkLoop(c, T, { seed: 1364 + j + D.id, w: 3, amp: 1, t0: 3, t1: 6 });
|
|
1184
|
+
}
|
|
1185
|
+
const P = spline(xf(starPts(9, R, R * 0.62, 1300 + D.id, D.id * 0.5, 0.3), { x: gx, y: gy, sx: gs }), true, 5);
|
|
1186
|
+
fillShape(c, P, PAL.yel, { seed: 1310 + D.id, amp: 2 });
|
|
1187
|
+
if (stp > 0) inkLoop(c, P, { seed: 1315 + D.id, w: 4, amp: 1.5, t0: 3, t1: 6 });
|
|
1188
|
+
const Q = spline(xf(starPts(7, R * 0.45, R * 0.28, 1320 + D.id, D.id * 0.9, 0.3), { x: gx, y: gy }), true, 4);
|
|
1189
|
+
fillShape(c, Q, PAL.hi, { seed: 1330 + D.id, amp: 2 });
|
|
1190
|
+
}
|
|
1191
|
+
if (stp === 2) drawTwinkle(c, p0[0], p0[1], 58, 1335 + D.id, { rot: 0.3, fill: PAL.hi }); // ignition
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function drawStarburst(c, t, D) {
|
|
1195
|
+
if (t < 6.583 || t > 6.88) return;
|
|
1196
|
+
const R = K(t, [[6.583, 1320], [6.625, 1240], [6.875, 250, 'in2']]);
|
|
1197
|
+
const sd = 1400 + D.id;
|
|
1198
|
+
const rot = D.id * 0.37;
|
|
1199
|
+
const ri = lerp(0.52, 0.3, inv(6.708, 6.875, t));
|
|
1200
|
+
const outer = spline(xf(starPts(15, R, R * ri, sd, rot, 0.28), { x: C0[0], y: C0[1] }), true, 6);
|
|
1201
|
+
fillShape(c, outer, PAL.yel, { seed: sd, amp: 2.5, dx: REG.dx, dy: REG.dy });
|
|
1202
|
+
const mid = spline(xf(starPts(11, R * 0.66, R * 0.36, sd + 1, rot + 0.2, 0.3), { x: C0[0], y: C0[1] }), true, 5);
|
|
1203
|
+
fillShape(c, mid, PAL.red, { seed: sd + 2, amp: 2.5, dx: REG.dx, dy: REG.dy });
|
|
1204
|
+
const core = spline(xf(starPts(9, R * 0.34, R * 0.2, sd + 3, rot + 0.5, 0.3), { x: C0[0], y: C0[1] }), true, 4);
|
|
1205
|
+
fillShape(c, core, PAL.hi, { seed: sd + 4, amp: 2 });
|
|
1206
|
+
inkLoop(c, outer, { seed: sd + 5, w: 9, amp: 2.5, heavy: 0.3, splits: 2 });
|
|
1207
|
+
}
|
|
1208
|
+
function speedLinesRadial(c, cx, cy, r0, r1, n, seed, w = 7, dir = 1) {
|
|
1209
|
+
for (let k = 0; k < n; k++) {
|
|
1210
|
+
const a = (k / n) * Math.PI * 2 + hs(seed, k) * 0.5;
|
|
1211
|
+
const a0 = r0 * (0.9 + 0.3 * hs(seed, k, 2)), a1 = r1 * (0.8 + 0.4 * hs(seed, k, 3));
|
|
1212
|
+
const P = [[cx + Math.cos(a) * a0, cy + Math.sin(a) * a0], [cx + Math.cos(a) * a1, cy + Math.sin(a) * a1]];
|
|
1213
|
+
inkStroke(c, dir > 0 ? P : P.reverse(), { seed: seed * 7 + k, w: w * (0.6 + 0.8 * hs(seed, k, 4)), t0: 12, t1: 90, min: 0.05, amp: 1.5, heavy: 0 });
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
// sparks from the burst: some of them become the fuse heads that write the strokes
|
|
1217
|
+
const NSPARK = 30;
|
|
1218
|
+
const SMOKE = (() => { // 7 clusters, irregular angles; lead puff class + 1-2 satellites
|
|
1219
|
+
const cl = [[-2.62, 1], [-1.72, 2], [-0.86, 0], [0.12, 2], [0.8, 1], [1.62, 0], [2.4, 2]];
|
|
1220
|
+
const out = [];
|
|
1221
|
+
cl.forEach(([a, c0], i) => {
|
|
1222
|
+
const n = 2 + (i % 2);
|
|
1223
|
+
for (let j = 0; j < n; j++) {
|
|
1224
|
+
const cls = j === 0 ? c0 : (c0 + 1 + j) % 3;
|
|
1225
|
+
out.push({ k: out.length, lead: j === 0, a: a + (j ? (j === 1 ? 0.2 : -0.24) : 0) + (hs(i, j, 42) - 0.5) * 0.12,
|
|
1226
|
+
dist: j ? 0.78 + 0.25 * hs(i, j, 43) : 1, R: [60, 120, 240][cls] * (0.85 + 0.3 * hs(i, j, 41)) * (j ? 0.8 : 1),
|
|
1227
|
+
td: 7.25 + (2 / 24) * ((i * 2 + j * 3) % 5) });
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
return out;
|
|
1231
|
+
})();
|
|
1232
|
+
function sparkState(k, t) {
|
|
1233
|
+
const a = (k / NSPARK) * Math.PI * 2 + (hs(k, 11) - 0.5) * 0.87;
|
|
1234
|
+
const v0 = 1700 + 1500 * hs(k, 12);
|
|
1235
|
+
const u = Math.max(0, t - 6.583);
|
|
1236
|
+
const drag = 4.2;
|
|
1237
|
+
const d = 110 + 90 * hs(k, 14) + (v0 / drag) * (1 - Math.exp(-drag * u));
|
|
1238
|
+
const x = C0[0] + Math.cos(a) * d, y = C0[1] + Math.sin(a) * d * 0.85 + 260 * u * u;
|
|
1239
|
+
const sp = v0 * Math.exp(-drag * u);
|
|
1240
|
+
return { x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp * 0.85 + 520 * u, size: [9, 15, 24][k % 3] * (0.85 + 0.3 * hs(k, 13)) };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// ------------------------------------------------------------------ light of the flame: stepped (cel) glow pool, multiplied into the paper
|
|
1244
|
+
function drawLightPool(c, tq, D, ss) {
|
|
1245
|
+
if (tq < 0.5 || tq >= 6.5 || !ss) return;
|
|
1246
|
+
let R = K(tq, [[0.5, 60], [0.542, 330, 'out3'], [0.625, 380], [1.0, 310], [2.0, 315], [2.5, 345], [5.5, 350], [6.25, 430, 'io2'], [6.5, 470]]);
|
|
1247
|
+
const fv = bv(D) % 3; // 3 boil variants, radius flickers +-3 % on 2s
|
|
1248
|
+
R *= 1 + 0.03 * (fv - 1);
|
|
1249
|
+
const cx = ss.x, cy = ss.y - ss.H * 0.42 * ss.sy;
|
|
1250
|
+
const scal = (r, n, ph) => { // hand-cut scalloped edge (scissor bites)
|
|
1251
|
+
const P = [];
|
|
1252
|
+
for (let i = 0; i < 120; i++) { const a = (i / 120) * Math.PI * 2; const q = Math.abs(Math.sin(a * n / 2 + ph)); P.push([cx + Math.cos(a) * r * (0.975 + 0.035 * Math.sqrt(q)), cy + Math.sin(a) * r * (0.975 + 0.035 * Math.sqrt(q))]); }
|
|
1253
|
+
return P;
|
|
1254
|
+
};
|
|
1255
|
+
c.save();
|
|
1256
|
+
c.globalCompositeOperation = 'multiply';
|
|
1257
|
+
c.globalAlpha = 0.72; // R2s2 (P4): pool 28 % lighter so the character, not the disc, owns the frame
|
|
1258
|
+
fillShape(c, scal(R * 1.25, 22, fv * 0.7), '#fff7df', { seed: 8100 + bv(D), amp: 4, scale: 90 });
|
|
1259
|
+
fillShape(c, scal(R, 18, 1 + fv * 0.9), '#fff0b4', { seed: 8200 + bv(D), amp: 4, scale: 90 });
|
|
1260
|
+
c.restore();
|
|
1261
|
+
}
|
|
1262
|
+
function hopTrails(c, tq, D) {
|
|
1263
|
+
for (const h of HOPS) { // red-pencil dotted arc guide for the whole hop (the animator's path), R2s2: drawn on ahead of the hop
|
|
1264
|
+
if (tq < h.t0 - 0.25 || tq > h.t1 + 0.2) continue;
|
|
1265
|
+
const src = [];
|
|
1266
|
+
for (let i = 0; i <= 48; i++) { const st = spiritState(lerp(h.t0, h.t1, i / 48), D, null); src.push([st.x, st.y - st.H * 0.42 * st.sy]); }
|
|
1267
|
+
const step = 22 / SHOT.s, grow = clamp((Math.floor((tq - h.t0 + 0.25) * 12) + 1) / 3); // appears in 3 drawings (on 2s)
|
|
1268
|
+
let acc = 0, k = 0, tot = 0;
|
|
1269
|
+
for (let i = 1; i < src.length; i++) tot += Math.hypot(src[i][0] - src[i - 1][0], src[i][1] - src[i - 1][1]);
|
|
1270
|
+
let run = 0;
|
|
1271
|
+
for (let i = 1; i < src.length; i++) {
|
|
1272
|
+
const L = Math.hypot(src[i][0] - src[i - 1][0], src[i][1] - src[i - 1][1]);
|
|
1273
|
+
acc += L; run += L;
|
|
1274
|
+
while (acc >= step) {
|
|
1275
|
+
acc -= step; k++;
|
|
1276
|
+
if (run - acc > tot * grow) break;
|
|
1277
|
+
const f2 = 1 - acc / L, x = lerp(src[i - 1][0], src[i][0], f2) + (hs(k, D.id, 5) - 0.5) * 1.5, y = lerp(src[i - 1][1], src[i][1], f2) + (hs(k, D.id, 6) - 0.5) * 1.5;
|
|
1278
|
+
c.save(); c.globalAlpha = 0.8;
|
|
1279
|
+
fillShape(c, circlePts(x, y, (1.9 + 0.5 * hs(k, 7)) * 1.6 / SHOT.s * 1.1, 8), PAL.pencil, { seed: 8400 + k * 3 + bv(D), amp: 0.35 });
|
|
1280
|
+
c.restore();
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
for (const h of HOPS) {
|
|
1285
|
+
if (tq <= (h.t0 + h.t1) / 2 || tq > h.t1 + 0.05) continue;
|
|
1286
|
+
const b = Math.min(tq, h.t1) - 0.03, a = b - 0.16;
|
|
1287
|
+
const pts = [];
|
|
1288
|
+
for (let i = 0; i <= 6; i++) {
|
|
1289
|
+
const tt = lerp(a, b, i / 6);
|
|
1290
|
+
const st = spiritState(tt, D, null);
|
|
1291
|
+
pts.push([st.x, st.y - st.H * 0.4 * st.sy]);
|
|
1292
|
+
}
|
|
1293
|
+
const cur = spiritState(tq, D, null);
|
|
1294
|
+
const back = pts.filter(([x, y]) => Math.hypot(x - cur.x, y - (cur.y - cur.H * 0.4 * cur.sy)) > cur.H * 0.42);
|
|
1295
|
+
if (back.length < 3) continue;
|
|
1296
|
+
[-0.22, 0.05, 0.3].forEach((off, j) => {
|
|
1297
|
+
const P = back.map(([x, y], i, arr) => {
|
|
1298
|
+
const q = arr[Math.min(arr.length - 1, i + 1)], r = arr[Math.max(0, i - 1)];
|
|
1299
|
+
let tx = q[0] - r[0], ty = q[1] - r[1]; const tl = Math.hypot(tx, ty) || 1;
|
|
1300
|
+
return [x - ty / tl * off * cur.H, y + tx / tl * off * cur.H];
|
|
1301
|
+
});
|
|
1302
|
+
inkStroke(c, P, { seed: 8300 + D.id * 3 + j, w: 4.2, t0: 50, t1: 12, min: 0.05, amp: 1.2, heavy: 0 });
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// ------------------------------------------------------------------ FRAME
|
|
1308
|
+
let CAM = { x: 0, y: 0 };
|
|
1309
|
+
function camera(f) {
|
|
1310
|
+
const t = f / FPS;
|
|
1311
|
+
let x = 0, y = 0;
|
|
1312
|
+
if (t >= 6.583 && t < 7.2) { const a = 18 * Math.pow(1 - (t - 6.583) / 0.62, 2); x = (hs(f, 1) - 0.5) * 2 * a; y = (hs(f, 2) - 0.5) * 2 * a; }
|
|
1313
|
+
if (t >= 0.5 && t < 0.62) { const a = 5; x = (hs(f, 3) - 0.5) * 2 * a; y = (hs(f, 4) - 0.5) * 2 * a; }
|
|
1314
|
+
if (t >= 2.5 && t < 2.6) { y = 5 * (1 - (t - 2.5) / 0.1); }
|
|
1315
|
+
return { x, y };
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
export async function init(canvas, dpr) {
|
|
1319
|
+
DPR = dpr;
|
|
1320
|
+
canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr);
|
|
1321
|
+
ctx = MAIN = canvas.getContext('2d');
|
|
1322
|
+
const off = () => { const cv = document.createElement('canvas'); cv.width = canvas.width; cv.height = canvas.height; return cv.getContext('2d'); };
|
|
1323
|
+
SHEETC = off(); NIGHTC = off();
|
|
1324
|
+
TOTAL = drawing(239).id + 1 - RIFFLE.length; // unique drawings in the film (the riffle re-shows old ones)
|
|
1325
|
+
LY = new Layers(W, H, dpr);
|
|
1326
|
+
const load = (src) => new Promise((res, rej) => { const im = new Image(); im.onload = () => res(im); im.onerror = rej; im.src = src; });
|
|
1327
|
+
IMG.paper = await load('/demos/05-cel-boil/assets/paper_base.jpg');
|
|
1328
|
+
IMG.tooth = await load('/demos/05-cel-boil/assets/paper_tooth.png');
|
|
1329
|
+
IMG.dust = [];
|
|
1330
|
+
for (let k = 1; k <= 4; k++) { const im = await load(`/demos/05-cel-boil/assets/dust_ink_0${k}.png`); await im.decode(); IMG.dust.push(im); }
|
|
1331
|
+
await IMG.paper.decode(); await IMG.tooth.decode();
|
|
1332
|
+
CUES = await (await fetch('/demos/05-cel-boil/cues.json')).json();
|
|
1333
|
+
const opentype = (await import('opentype')).default || (await import('opentype'));
|
|
1334
|
+
const buf = await (await fetch('/assets/fonts/cjk/lxgwmarkergothic/LXGWMarkerGothic-Regular.ttf')).arrayBuffer();
|
|
1335
|
+
const font = opentype.parse(buf);
|
|
1336
|
+
TAG = { a: glyphPolys(font, '一帧一帧', 60, 13), b: glyphPolys(font, '亲手点亮', 60, 13), gap: 92 };
|
|
1337
|
+
if (V5) STROKES = await loadV5Strokes();
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// ------------------------------------------------------------------ END: pull back to the sheet on the peg bar; thumb riffles the stack
|
|
1341
|
+
const RIFFLE = [150, 121, 108, 61, 30, 13]; // earlier drawings flashed by the riffle (backwards through the stack)
|
|
1342
|
+
const PIV = [960, 540];
|
|
1343
|
+
function pullScale(fq) { return fq < 218 ? 1 : fq < 220 ? 0.9 : fq < 222 ? 0.86 : 0.84; } // R2: starts f218 (full lockup held from f212) // 3 drawings on 2s, ease-out
|
|
1344
|
+
export function render(t) {
|
|
1345
|
+
const f = Math.min(239, Math.max(0, Math.round(t * FPS)));
|
|
1346
|
+
const D = drawing(f);
|
|
1347
|
+
const sc = pullScale(D.fq);
|
|
1348
|
+
if (sc >= 1) { ctx = MAIN; drawFrame(f); return; }
|
|
1349
|
+
const rif = f >= 224 && f <= 229 ? f - 224 : -1;
|
|
1350
|
+
const part = V5 ? rif >= 0 : rif === 0 || rif === 5; // R2: on these the top sheet stays; only the lifted corner shows the drawing below (V5: on all six, so the title never leaves)
|
|
1351
|
+
ctx = SHEETC; drawFrame(rif >= 0 && !part ? RIFFLE[rif] : f); ctx = MAIN;
|
|
1352
|
+
drawDesk(MAIN, D, f, sc, rif, part);
|
|
1353
|
+
}
|
|
1354
|
+
const rectPts = (x, y, w, h) => [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
|
|
1355
|
+
let ONION = null;
|
|
1356
|
+
function onionGhost() { // R2: two earlier poses of the tiny spirit, ink only, as blue col-erase pencil (cached: the sheets below do not boil)
|
|
1357
|
+
if (ONION) return ONION;
|
|
1358
|
+
const RX = 1250, RY = 0, RW = 420, RH = 420, cw = Math.round(RW * DPR), ch = Math.round(RH * DPR);
|
|
1359
|
+
ONION = [9.135, 9.25].map((tq) => {
|
|
1360
|
+
const sc = document.createElement('canvas'); sc.width = Math.round(W * DPR); sc.height = Math.round(H * DPR);
|
|
1361
|
+
const g = sc.getContext('2d'); g.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
1362
|
+
const D = drawing(Math.round(tq * FPS));
|
|
1363
|
+
drawSpirit(g, tinySpirit(tq, D), D, tq);
|
|
1364
|
+
const im = g.getImageData(Math.round(RX * DPR), Math.round(RY * DPR), cw, ch), d = im.data;
|
|
1365
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
1366
|
+
const lum = (0.3 * d[i] + 0.59 * d[i + 1] + 0.11 * d[i + 2]) / 255, a = d[i + 3] / 255;
|
|
1367
|
+
const k = a * clamp((0.42 - lum) / 0.25);
|
|
1368
|
+
d[i] = 0x5a; d[i + 1] = 0x7b; d[i + 2] = 0xb5; d[i + 3] = Math.round(255 * k);
|
|
1369
|
+
}
|
|
1370
|
+
const oc = document.createElement('canvas'); oc.width = cw; oc.height = ch; oc.getContext('2d').putImageData(im, 0, 0);
|
|
1371
|
+
return { cv: oc, RX, RY, RW, RH };
|
|
1372
|
+
});
|
|
1373
|
+
return ONION;
|
|
1374
|
+
}
|
|
1375
|
+
function drawDesk(c, D, f, s, rif, part) {
|
|
1376
|
+
const v = bv(D);
|
|
1377
|
+
c.setTransform(DPR, 0, 0, DPR, 0, 0); c.globalAlpha = 1; c.globalCompositeOperation = 'source-over';
|
|
1378
|
+
drawPaper(c, { x: 0, y: 0 });
|
|
1379
|
+
c.fillStyle = 'rgba(28,22,19,0.86)'; c.fillRect(0, 0, W, H); // the desk
|
|
1380
|
+
const x0 = PIV[0] - PIV[0] * s, y0 = PIV[1] - PIV[1] * s, w = W * s, h = H * s;
|
|
1381
|
+
// R2 (REVIEW_2 P3): the desk is a LIGHT TABLE - warm underlight glowing round the stack in hard cel steps
|
|
1382
|
+
[[124, 0.14], [80, 0.24], [44, 0.38]].forEach(([m, a], k) => {
|
|
1383
|
+
fillShape(c, dens(rectPts(x0 - m, y0 - m * 0.7, w + 2 * m + 18, h + 1.5 * m + 13), 24), `rgba(255,240,180,${a})`, { seed: 9170 + k * 3 + v, amp: 3 });
|
|
1384
|
+
});
|
|
1385
|
+
// peg bar taped to the desk; its pegs come up through the sheet's holes
|
|
1386
|
+
const by = y0 - 30, bar = dens(rectPts(PIV[0] - 700, by, 1400, 70), 6);
|
|
1387
|
+
fillShape(c, bar, '#4a3f38', { seed: 9100 + v, amp: 1 });
|
|
1388
|
+
inkLoop(c, bar, { seed: 9110 + v, w: 3.5, amp: 1 });
|
|
1389
|
+
inkStroke(c, [[PIV[0] - 680, by + 10], [PIV[0] + 680, by + 10]], { seed: 9120 + v, w: 3, t0: 40, t1: 40, min: 0.1, amp: 1, heavy: 0, color: PAL.hi });
|
|
1390
|
+
for (const sx of [-1, 1]) { // masking-tape tabs holding the bar
|
|
1391
|
+
const T = dens(xf(rectPts(-46, -24, 92, 48), { x: PIV[0] + sx * 700, y: by + 12, r: sx * 0.12 }), 6);
|
|
1392
|
+
fillShape(c, T, '#e9dcc0', { seed: 9125 + v + sx, amp: 1.2 }); inkLoop(c, T, { seed: 9127 + v + sx, w: 2.5, amp: 1 });
|
|
1393
|
+
}
|
|
1394
|
+
// hard (cel) shadow of the stack, then 3 sheets beneath, offset and each 3 % darker
|
|
1395
|
+
fillShape(c, dens(rectPts(x0 + 30, y0 + 24, w, h), 20), 'rgba(0,0,0,0.42)', { seed: 9130 + v, amp: 1.5 });
|
|
1396
|
+
[[18, 13, 0.09], [12, 9, 0.06], [6, 4, 0.03]].forEach(([dx, dy, dk], k) => {
|
|
1397
|
+
c.save(); c.beginPath(); c.rect(x0 + dx, y0 + dy, w, h); c.clip();
|
|
1398
|
+
c.drawImage(IMG.paper, x0 + dx, y0 + dy, w, h);
|
|
1399
|
+
c.fillStyle = `rgba(28,22,19,${0.02 + dk})`; c.fillRect(x0 + dx, y0 + dy, w, h);
|
|
1400
|
+
c.restore();
|
|
1401
|
+
c.globalAlpha = 0.55; inkLoop(c, dens(rectPts(x0 + dx, y0 + dy, w, h), 10), { seed: 9140 + k * 11 + v, w: 2, amp: 0.8 }); c.globalAlpha = 1;
|
|
1402
|
+
});
|
|
1403
|
+
// the top sheet = the film (on a riffle drawing it is lifted and slightly skewed)
|
|
1404
|
+
const ox = rif >= 0 ? (hs(f, 1) - 0.5) * 12 : 0, oy = rif >= 0 ? -6 - 8 * hs(f, 2) : 0, rot = rif >= 0 ? (hs(f, 3) - 0.5) * 0.014 : 0;
|
|
1405
|
+
c.save();
|
|
1406
|
+
c.translate(PIV[0] + ox, PIV[1] + oy); c.rotate(rot); c.translate(-PIV[0], -PIV[1]);
|
|
1407
|
+
c.drawImage(SHEETC.canvas, 0, 0, SHEETC.canvas.width, SHEETC.canvas.height, x0, y0, w, h);
|
|
1408
|
+
// R2: underlight through the sheet (+ tooth lifted 6 %) and the two drawings beneath as blue onion skin
|
|
1409
|
+
c.save(); c.beginPath(); c.rect(x0, y0, w, h); c.clip();
|
|
1410
|
+
c.globalCompositeOperation = 'screen'; c.fillStyle = 'rgba(255,240,180,0.16)'; c.fillRect(x0, y0, w, h);
|
|
1411
|
+
c.globalCompositeOperation = 'multiply'; c.globalAlpha = 0.06;
|
|
1412
|
+
c.drawImage(IMG.tooth, Math.floor(hs(v, 53) * 470), Math.floor(hs(v, 54) * 310), W, H, x0, y0, w, h);
|
|
1413
|
+
if (!(rif >= 0 && !part)) {
|
|
1414
|
+
onionGhost().forEach((O, k) => {
|
|
1415
|
+
const [dx, dy] = k ? [-5, 7] : [8, -4];
|
|
1416
|
+
c.globalAlpha = k ? 0.16 : 0.12;
|
|
1417
|
+
c.drawImage(O.cv, x0 + (O.RX + dx) * s, y0 + (O.RY + dy) * s, O.RW * s, O.RH * s);
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
c.restore();
|
|
1421
|
+
c.globalAlpha = 0.6; inkLoop(c, dens(rectPts(x0, y0, w, h), 10), { seed: 9150 + v, w: 2.2, amp: 0.8 }); c.globalAlpha = 1;
|
|
1422
|
+
// Acme peg holes (round centre + two slots) with the pegs showing through
|
|
1423
|
+
const hy = y0 + 40 * s;
|
|
1424
|
+
const holes = [circlePts(PIV[0], hy, 15 * s, 24), ...[-1, 1].map((sx) => {
|
|
1425
|
+
const cx = PIV[0] + sx * 600 * s, hw = 62 * s, hr = 14 * s, P = [];
|
|
1426
|
+
for (let i = 0; i <= 12; i++) { const a = -Math.PI / 2 + (i / 12) * Math.PI; P.push([cx + hw + Math.cos(a) * hr, hy + Math.sin(a) * hr]); }
|
|
1427
|
+
for (let i = 0; i <= 12; i++) { const a = Math.PI / 2 + (i / 12) * Math.PI; P.push([cx - hw + Math.cos(a) * hr, hy + Math.sin(a) * hr]); }
|
|
1428
|
+
return P;
|
|
1429
|
+
})];
|
|
1430
|
+
holes.forEach((P, k) => {
|
|
1431
|
+
const Q = dens(P, 3);
|
|
1432
|
+
fillShape(c, Q, '#4a3f38', { seed: 9160 + k + v, amp: 0.6 });
|
|
1433
|
+
inkLoop(c, Q, { seed: 9165 + k + v, w: 2.6, amp: 0.6, t0: 3, t1: 6 });
|
|
1434
|
+
});
|
|
1435
|
+
// R2 (REVIEW_2 P3): riffle = the lower-right corner lifts and curls as a flap (on 1s)
|
|
1436
|
+
let tip = [x0 + w, y0 + h];
|
|
1437
|
+
if (rif >= 0) {
|
|
1438
|
+
const cx = x0 + w, cy = y0 + h;
|
|
1439
|
+
const a = [350, 530, 475, 560, 495, 300][rif] * s, b = [250, 380, 340, 405, 358, 214][rif] * s;
|
|
1440
|
+
const P1 = [cx - a, cy], P2 = [cx, cy - b];
|
|
1441
|
+
const dx = P2[0] - P1[0], dy = P2[1] - P1[1], L2 = dx * dx + dy * dy;
|
|
1442
|
+
const tt = ((cx - P1[0]) * dx + (cy - P1[1]) * dy) / L2, fx = P1[0] + dx * tt, fy = P1[1] + dy * tt;
|
|
1443
|
+
const Cf = [lerp(fx, 2 * fx - cx, 0.8), lerp(fy, 2 * fy - cy, 0.8)]; // corner reflected over the fold, curled back 20 %
|
|
1444
|
+
const Q1 = [lerp(P1[0], Cf[0], 0.74) - 10, lerp(P1[1], Cf[1], 0.74) + 6], Q2 = [lerp(P2[0], Cf[0], 0.74) + 6, lerp(P2[1], Cf[1], 0.74) - 10]; // R2s2: wider free edge -> trapezoid flap, not a narrow curl
|
|
1445
|
+
tip = [Q2[0] + 8, Q2[1] + 4]; // the thumb pinches the free edge from the right
|
|
1446
|
+
const bow = (A, B, k) => { const mx = (A[0] + B[0]) / 2, my = (A[1] + B[1]) / 2, nx = A[1] - B[1], ny = B[0] - A[0], l = Math.hypot(nx, ny) || 1; return [mx + nx / l * k, my + ny / l * k]; };
|
|
1447
|
+
// (1) what the lifted corner uncovers
|
|
1448
|
+
c.save(); c.beginPath(); c.moveTo(P1[0], P1[1]); c.lineTo(cx, cy); c.lineTo(P2[0], P2[1]); c.closePath(); c.clip();
|
|
1449
|
+
if (part) { ctx = SHEETC; drawFrame(RIFFLE[rif]); ctx = MAIN; c.drawImage(SHEETC.canvas, 0, 0, SHEETC.canvas.width, SHEETC.canvas.height, x0, y0, w, h); }
|
|
1450
|
+
else { c.drawImage(IMG.paper, x0, y0, w, h); c.fillStyle = 'rgba(28,22,19,0.06)'; c.fillRect(x0, y0, w, h); }
|
|
1451
|
+
c.restore();
|
|
1452
|
+
const F = dens([P1, bow(P1, Q1, 16), Q1, bow(Q1, Q2, 12), Q2, bow(Q2, P2, 16), P2], 4);
|
|
1453
|
+
// (2) hard cast shadow of the flap (#1c1613 @ 18 %), kept on the sheet
|
|
1454
|
+
c.save(); c.beginPath(); c.rect(x0, y0, w, h); c.clip();
|
|
1455
|
+
fillShape(c, xf(F, { x: 22 * s, y: 18 * s }), 'rgba(28,22,19,0.18)', { seed: 9180 + f, amp: 1.2 });
|
|
1456
|
+
c.restore();
|
|
1457
|
+
// (3) the flap: back of the page, a shade band along the fold, a heavy 12 px ink free edge
|
|
1458
|
+
fillShape(c, F, '#eadfc6', { seed: 9182 + f, amp: 1.2 });
|
|
1459
|
+
fillShape(c, dens([P1, P2, [lerp(P2[0], Cf[0], 0.18), lerp(P2[1], Cf[1], 0.18)], [lerp(P1[0], Cf[0], 0.18), lerp(P1[1], Cf[1], 0.18)]], 4), 'rgba(28,22,19,0.1)', { seed: 9184 + f, amp: 1 });
|
|
1460
|
+
inkLoop(c, F, { seed: 9186 + f, w: 4, amp: 1.2 });
|
|
1461
|
+
inkStroke(c, [Q1, bow(Q1, Q2, 12), Q2], { seed: 9188 + f, w: 12, t0: 16, t1: 16, min: 0.3, amp: 1, heavy: 0 });
|
|
1462
|
+
// (4) edge smear: dry streaks the free edge leaves behind as it sweeps up
|
|
1463
|
+
c.globalAlpha = 0.45;
|
|
1464
|
+
for (let k = 0; k < 3; k++) {
|
|
1465
|
+
const u = 0.22 + 0.28 * k, E0 = [lerp(Q1[0], Q2[0], u), lerp(Q1[1], Q2[1], u)], l = (60 + 50 * hs(f, k, 7)) * s;
|
|
1466
|
+
inkStroke(c, [[E0[0] + 14, E0[1] + 12], [E0[0] + 14 + l * 0.78, E0[1] + 12 + l * 0.62]], { seed: 9190 + f * 5 + k, w: 5, t0: 3, t1: 24, min: 0.1, amp: 1, heavy: 0 });
|
|
1467
|
+
}
|
|
1468
|
+
c.globalAlpha = 1;
|
|
1469
|
+
}
|
|
1470
|
+
c.restore();
|
|
1471
|
+
if (f >= 222 && f <= 232) drawThumb(c, D, f, tip[0], tip[1], rif);
|
|
1472
|
+
}
|
|
1473
|
+
function drawThumb(c, D, f, cx, cy, rif) {
|
|
1474
|
+
// white cartoon glove thumb at the stack's corner; lifted page corners fan up while it riffles
|
|
1475
|
+
const v = bv(D);
|
|
1476
|
+
const enter = f === 222 ? 1 : f === 223 ? 0.25 : f >= 230 ? [0.35, 0.8, 1.4][f - 230] : 0;
|
|
1477
|
+
const bob = rif >= 0 ? (rif % 2 ? -10 : 4) : 0;
|
|
1478
|
+
const tx = cx - 34 + enter * 190, ty = cy - 16 + bob + enter * 140;
|
|
1479
|
+
const ang = rif >= 0 ? -2.95 : -2.45; // R2: on the riffle the thumb comes in from the right so the flap stays readable
|
|
1480
|
+
c.save(); c.translate(tx, ty); c.scale(1.4, 1.4); c.translate(-tx, -ty); // R2: thumb 1.4x (reads at phone size)
|
|
1481
|
+
const thumb = dens(xf(circlePts(0, 0, 44, 60, 118), { x: tx - Math.cos(ang) * 96, y: ty - Math.sin(ang) * 96, r: ang }), 3);
|
|
1482
|
+
fillShape(c, thumb, PAL.hi, { seed: 9250 + v, amp: 1.2, dx: REG.dx, dy: REG.dy });
|
|
1483
|
+
inkLoop(c, thumb, { seed: 9255 + v, w: 6, amp: 1.2, heavy: 0.4 });
|
|
1484
|
+
// nail + knuckle crease
|
|
1485
|
+
const nx = tx - Math.cos(ang) * 38, ny = ty - Math.sin(ang) * 38;
|
|
1486
|
+
const nail = dens(xf(circlePts(0, 0, 20, 24, 30), { x: nx, y: ny, r: ang }), 3);
|
|
1487
|
+
inkLoop(c, nail, { seed: 9260 + v, w: 3, amp: 1, t0: 3, t1: 6 });
|
|
1488
|
+
const kx = tx - Math.cos(ang) * 120, ky = ty - Math.sin(ang) * 120;
|
|
1489
|
+
inkStroke(c, [[kx - Math.sin(ang) * 26, ky + Math.cos(ang) * 26], [kx + Math.cos(ang) * 8, ky + Math.sin(ang) * 8], [kx + Math.sin(ang) * 26, ky - Math.cos(ang) * 26]], { seed: 9265 + v, w: 3.5, t0: 8, t1: 8, amp: 1, heavy: 0 });
|
|
1490
|
+
c.restore();
|
|
1491
|
+
}
|
|
1492
|
+
function shakeMarks(c, ms, D, color, w) {
|
|
1493
|
+
for (const side of [-1, 1]) {
|
|
1494
|
+
for (let k = 0; k < 3; k++) {
|
|
1495
|
+
const a = (side < 0 ? Math.PI : 0) + (k - 1) * 0.5 + (hs(D.id, k, side) - 0.5) * 0.2;
|
|
1496
|
+
const r0 = 48 + (D.id % 2) * 5, r1 = r0 + 24;
|
|
1497
|
+
inkStroke(c, [[ms.hx + Math.cos(a) * r0, ms.hy + Math.sin(a) * r0], [ms.hx + Math.cos(a) * r1, ms.hy + Math.sin(a) * r1]], { seed: 60 + D.id * 7 + k + side, w, t0: 6, t1: 6, amp: 0.8, heavy: 0, color });
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
// true night before the strike: ink wash + hand-cut vignette; only the line work catches a red rim light; chalk shake marks
|
|
1502
|
+
function nightPass(c, D, f, ms) {
|
|
1503
|
+
const N = NIGHTC, cw = N.canvas.width, ch = N.canvas.height;
|
|
1504
|
+
N.setTransform(1, 0, 0, 1, 0, 0);
|
|
1505
|
+
N.globalCompositeOperation = 'copy'; N.drawImage(c.canvas, 0, 0);
|
|
1506
|
+
// R1s2: rim-light mask from luminance only -> just the ink line work catches the red (no green/blue tints from fills)
|
|
1507
|
+
N.globalCompositeOperation = 'saturation'; N.fillStyle = '#808080'; N.fillRect(0, 0, cw, ch);
|
|
1508
|
+
N.globalCompositeOperation = 'difference'; N.fillStyle = '#ffffff'; N.fillRect(0, 0, cw, ch);
|
|
1509
|
+
N.globalCompositeOperation = 'multiply'; N.drawImage(N.canvas, 0, 0); N.drawImage(N.canvas, 0, 0); // lum^4
|
|
1510
|
+
N.fillStyle = PAL.red; N.fillRect(0, 0, cw, ch);
|
|
1511
|
+
N.globalCompositeOperation = 'source-over';
|
|
1512
|
+
const dark = f <= 8 ? 0.62 : [0.56, 0.5, 0.42][f - 9];
|
|
1513
|
+
c.save();
|
|
1514
|
+
c.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
1515
|
+
// R1s2: kill the plate's colour first (the yellow strip went olive, the paper went grey)
|
|
1516
|
+
c.globalCompositeOperation = 'saturation'; c.globalAlpha = 0.85; c.fillStyle = '#808080'; c.fillRect(0, 0, W, H);
|
|
1517
|
+
c.globalCompositeOperation = 'multiply'; c.globalAlpha = 1; c.fillStyle = '#6b5446'; c.fillRect(0, 0, W, H);
|
|
1518
|
+
c.globalCompositeOperation = 'source-over';
|
|
1519
|
+
c.globalAlpha = dark; c.fillStyle = PAL.ink; c.fillRect(0, 0, W, H);
|
|
1520
|
+
c.globalAlpha = 0.6; c.beginPath(); c.rect(0, 0, W, H);
|
|
1521
|
+
polyPath(c, boil(circlePts(1010, 610, 470, 90, 860), 9400 + bv(D), 7, 140)); c.fill('evenodd');
|
|
1522
|
+
c.setTransform(1, 0, 0, 1, 0, 0);
|
|
1523
|
+
c.globalCompositeOperation = 'screen'; c.globalAlpha = 1; c.drawImage(N.canvas, 0, 0); c.globalAlpha = f <= 8 ? 0.6 : 0.8; c.drawImage(N.canvas, 0, 0);
|
|
1524
|
+
c.globalCompositeOperation = 'source-over'; c.globalAlpha = 1;
|
|
1525
|
+
c.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
1526
|
+
if (f < 8) { camApply(c, CAM); SHOT = CLOSE; stageApply(c); shakeMarks(c, ms, D, PAL.hi, 5.5); }
|
|
1527
|
+
c.restore();
|
|
1528
|
+
}
|
|
1529
|
+
function flashFrame(c, ms) { // f12: a full-frame paper-white flash, star centred where the spark sits in the wide
|
|
1530
|
+
const hx = lerp(1296, ms.hx, 0.6), hy = lerp(828, ms.hy, 0.6);
|
|
1531
|
+
const X = lerp(STAGE.X + (hx - STAGE.px) * STAGE.s, 960, 0.35), Y = lerp(STAGE.Y + (hy - STAGE.py) * STAGE.s, 540, 0.35);
|
|
1532
|
+
c.save(); c.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
1533
|
+
c.fillStyle = PAL.hi; c.fillRect(0, 0, W, H);
|
|
1534
|
+
fillShape(c, spline(xf(starPts(12, 760, 330, 4401, 0.2, 0.3), { x: X, y: Y }), true, 6), PAL.yel, { seed: 4402, amp: 2 });
|
|
1535
|
+
fillShape(c, spline(xf(starPts(9, 330, 170, 4403, 0.5, 0.3), { x: X, y: Y }), true, 5), PAL.hi, { seed: 4404, amp: 2 });
|
|
1536
|
+
speedLinesRadial(c, X, Y, 800, 1500, 16, 4405, 8, 1);
|
|
1537
|
+
c.restore();
|
|
1538
|
+
}
|
|
1539
|
+
function drawFrame(f) {
|
|
1540
|
+
const D = drawing(f);
|
|
1541
|
+
const tq = D.fq / FPS; // main drawing time (on 2s / 1s / 3s)
|
|
1542
|
+
const t1 = f / FPS; // on-1s time for fast FX
|
|
1543
|
+
const c = ctx;
|
|
1544
|
+
CAM = camera(f);
|
|
1545
|
+
c.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
1546
|
+
c.globalCompositeOperation = 'source-over'; c.globalAlpha = 1;
|
|
1547
|
+
drawPaper(c, CAM);
|
|
1548
|
+
c.save(); camApply(c, CAM);
|
|
1549
|
+
|
|
1550
|
+
const bs = boxState(tq);
|
|
1551
|
+
const ms = matchState(tq);
|
|
1552
|
+
const ss = spiritState(tq, D, ms);
|
|
1553
|
+
|
|
1554
|
+
// ---------------- stage (before the burst)
|
|
1555
|
+
SHOT = shotFor(f);
|
|
1556
|
+
const zr = Math.min(1, STAGE.s / SHOT.s);
|
|
1557
|
+
G.GLOBAL.ink = SHOT === CLOSE ? 0.58 : SHOT === MED ? 0.7 : 0.2 + 0.8 * zr;
|
|
1558
|
+
G.GLOBAL.amp = SHOT === CLOSE ? 0.55 : SHOT === MED ? 0.66 : 0.2 + 0.8 * zr;
|
|
1559
|
+
if (f >= 132) { // camera push on the spin (on 1s, accelerating), zooming about the spirit -> burst centre
|
|
1560
|
+
const u = E.in2(inv(5.5, 6.5, t1));
|
|
1561
|
+
const sc = lerp(STAGE.s, 2.05, u);
|
|
1562
|
+
SHOT = { s: sc, px: 860, py: 606, X: 862, Y: 618 };
|
|
1563
|
+
G.GLOBAL.ink = lerp(1, 0.8, u); G.GLOBAL.amp = lerp(1, 0.75, u);
|
|
1564
|
+
}
|
|
1565
|
+
c.save(); stageApply(c);
|
|
1566
|
+
drawLightPool(c, tq, D, ss);
|
|
1567
|
+
drawBox(c, tq, D, bs);
|
|
1568
|
+
if (ms.vis) {
|
|
1569
|
+
if (!(ms.smear && tq >= 0.36)) drawMatchStick(c, ms, D, 200);
|
|
1570
|
+
drawMatchHead(c, ms, D, 300, tq);
|
|
1571
|
+
if (f >= 24 && f < 28) { // R2: landing dust tick at the butt (woodblock at 1.0)
|
|
1572
|
+
const k = f < 26 ? 0 : 1;
|
|
1573
|
+
for (const sg of [-1, 1]) {
|
|
1574
|
+
drawPuff(c, BUTT[0] + sg * (30 + 18 * k), BUTT[1] - 8 - 5 * k, (15 + 4 * k) * (k ? 0.75 : 1), 7700 + (sg + 1) * 3 + k, { w: 3.5 });
|
|
1575
|
+
inkStroke(c, [[BUTT[0] + sg * 20, BUTT[1] - 20], [BUTT[0] + sg * (40 + 12 * k), BUTT[1] - 40 - 10 * k]], { seed: 7710 + sg + k * 5, w: 4, t0: 4, t1: 8, min: 0.3, amp: 0.8, heavy: 0 });
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (ms.toss && f > 12 && f < 24) {
|
|
1579
|
+
for (let k = 1; k <= 4; k++) {
|
|
1580
|
+
const pm = matchState((f - k * 0.7) / FPS), ends = stickEnds(pm);
|
|
1581
|
+
ends.forEach(([x, y], e) => fillShape(c, circlePts(x, y, (6.5 - k * 1.1) * (e ? 0.8 : 1), 10), PAL.ink, { seed: 4500 + f * 9 + k * 2 + e, amp: 0.6 }));
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (ss && tq < 2.0) drawSpirit(c, ss, D, tq);
|
|
1585
|
+
// (nervous shake marks are drawn in chalk after the night pass)
|
|
1586
|
+
if (ms.burnt && tq < 6.5) drawWisp(c, ms.hx + 4, ms.hy - 30, tq, D, sstep(2.0, 2.4, tq), SHOT === LOW ? 1 : 0);
|
|
1587
|
+
}
|
|
1588
|
+
// ignition burst on the head (on 1s)
|
|
1589
|
+
if (t1 >= 0.5 && t1 < 0.625) {
|
|
1590
|
+
const u = inv(0.5, 0.625, t1);
|
|
1591
|
+
const R = lerp(150, 215, E.out2(u)) * (u > 0.7 ? 0.75 : 1); // pops in big on the flash frame
|
|
1592
|
+
const hx = lerp(1296, ms.hx, 0.6), hy = lerp(828, ms.hy, 0.6);
|
|
1593
|
+
const P = spline(xf(starPts(10, R, R * 0.45, 40 + f, f * 0.4, 0.35), { x: hx, y: hy }), true, 4);
|
|
1594
|
+
if (f <= 13) speedLinesRadial(c, hx, hy, R * 1.12, R * (f === 12 ? 2.1 : 2.5), 13, 300 + f, 5);
|
|
1595
|
+
if (u < 0.75) {
|
|
1596
|
+
fillShape(c, P, PAL.yel, { seed: 41 + f, amp: 1.5, dx: REG.dx, dy: REG.dy });
|
|
1597
|
+
if (f <= 13) { // two-tone core on the first two drawings
|
|
1598
|
+
const Q = spline(xf(starPts(8, R * 0.5, R * 0.26, 45 + f, f * 0.4 + 0.3, 0.35), { x: hx, y: hy }), true, 4);
|
|
1599
|
+
fillShape(c, Q, f === 12 ? PAL.hi : PAL.red, { seed: 46 + f, amp: 1.5, dx: REG.dx, dy: REG.dy });
|
|
1600
|
+
}
|
|
1601
|
+
inkLoop(c, P, { seed: 42 + f, w: 6, amp: 1.5 });
|
|
1602
|
+
}
|
|
1603
|
+
for (let k = 0; k < 9; k++) {
|
|
1604
|
+
const a = -Math.PI * 0.95 + k * 0.33 + hs(f, k) * 0.2, d = R * (0.9 + u * 1.6) + hs(k, 3) * 60;
|
|
1605
|
+
drawSpark(c, hx + Math.cos(a) * d, hy + Math.sin(a) * d, Math.cos(a) * 900, Math.sin(a) * 900, 7 + 5 * hs(k, 4), 60 + f * 13 + k);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
// spirit on stage
|
|
1609
|
+
if (ss && tq >= 2.0) {
|
|
1610
|
+
// spin: whirlwind wrap lines, back halves behind the body, front halves over it (on 1s)
|
|
1611
|
+
const spinRings = [];
|
|
1612
|
+
if (tq >= 5.5 && tq < 6.25) {
|
|
1613
|
+
const u = inv(5.5, 6.25, t1);
|
|
1614
|
+
const n = 2 + Math.floor(u * 5);
|
|
1615
|
+
for (let k = 0; k < n; k++) {
|
|
1616
|
+
const hk = 0.1 + 0.8 * ((k + 0.5) / n) + (hs(f, k) - 0.5) * 0.08;
|
|
1617
|
+
const y = ss.y - ss.H * hk * ss.sy;
|
|
1618
|
+
const rx = ss.H * (0.5 + 0.12 * Math.sin(hk * 3)) * (1 + 0.25 * u), ry = rx * 0.16;
|
|
1619
|
+
const a0 = hs(f, k, 2) * 6.28, len = 2.2 + 2.5 * hs(f, k, 3);
|
|
1620
|
+
spinRings.push({ y, rx, ry, a0, len, k });
|
|
1621
|
+
}
|
|
1622
|
+
const arc = (R, front) => {
|
|
1623
|
+
const P = [];
|
|
1624
|
+
for (let i = 0; i <= 24; i++) { const a = R.a0 + (i / 24) * R.len; const sn = Math.sin(a); if ((sn > 0) === front) P.push([ss.x + Math.cos(a) * R.rx, R.y + sn * R.ry]); else if (P.length) break; }
|
|
1625
|
+
return P;
|
|
1626
|
+
};
|
|
1627
|
+
spinRings.forEach((R) => { const P = arc(R, false); if (P.length > 3) inkStroke(c, P, { seed: 900 + f * 5 + R.k, w: 5, t0: 10, t1: 40, min: 0.1, amp: 1.0, heavy: 0, dense: false }); });
|
|
1628
|
+
spinRings.front = (cc) => spinRings.forEach((R) => { const P = arc(R, true); if (P.length > 3) inkStroke(cc, P, { seed: 950 + f * 5 + R.k, w: 5.5, t0: 10, t1: 40, min: 0.1, amp: 1.0, heavy: 0 }); });
|
|
1629
|
+
}
|
|
1630
|
+
hopTrails(c, tq, D);
|
|
1631
|
+
// hatched contact shadow on the box top (shrinks as the spirit leaves the ground)
|
|
1632
|
+
if (tq >= 2.5 && bs.vis) {
|
|
1633
|
+
const lift = clamp((GROUND - ss.y) / 220);
|
|
1634
|
+
const rx = ss.H * 0.46 * (1 - 0.55 * lift) * Math.sqrt(ss.sx), ry = rx * 0.2;
|
|
1635
|
+
if (rx > 8) hatch(c, circlePts(ss.x + 14, GROUND + 3, ry, 40, rx), { seed: 7700 + bv(D), gap: 9, ang: -0.95, w: 2.4, amp: 0.9 });
|
|
1636
|
+
}
|
|
1637
|
+
drawSpirit(c, ss, D, tq);
|
|
1638
|
+
if (spinRings.front) spinRings.front(c);
|
|
1639
|
+
// dust puffs on landings
|
|
1640
|
+
for (const tl of [2.5, 3.0, 3.5, 4.0]) {
|
|
1641
|
+
const u = inv(tl, tl + 0.33, tq);
|
|
1642
|
+
if (tq > tl + 0.01 && u < 1) {
|
|
1643
|
+
const big = tl === 2.5 ? 1 : 0.6;
|
|
1644
|
+
if (tl === 2.5 && u >= 0.75) continue;
|
|
1645
|
+
for (const side of [-1, 1]) {
|
|
1646
|
+
for (let j = 0; j < 3; j++) {
|
|
1647
|
+
const uu = tl === 2.5 ? inv(2.5, 2.75, tq) : u;
|
|
1648
|
+
const d = ss.H * 0.36 + [40, 105, 190][j] * big * E.out2(Math.min(1, uu + 0.25));
|
|
1649
|
+
const x = ss.x + side * d, y = GROUND - 8 - [8, 24, 44][j] * big * (0.4 + uu);
|
|
1650
|
+
const r = [22, 36, 54][j] * big * (1 - uu * 0.55);
|
|
1651
|
+
const sd = 1000 + Math.round(tl * 10) * 7 + side * 3 + j * 11 + D.id;
|
|
1652
|
+
drawPuff(c, x, y, r, sd, { n: 4, w: 3.8, fill: PAL.cream, flat: 0.7, wide: 1.2 });
|
|
1653
|
+
const P = [], c0 = side > 0 ? 3.6 : -0.5;
|
|
1654
|
+
for (let i = 0; i <= 10; i++) { const a = c0 + side * (i / 10) * 3.4, rr = r * 0.5 * (1 - 0.5 * i / 10); P.push([x + Math.cos(a) * rr, y + Math.sin(a) * rr * 0.8]); }
|
|
1655
|
+
inkStroke(c, P, { seed: sd + 5, w: 3.2, t0: 4, t1: 10, min: 0.2, amp: 0.8, heavy: 0 });
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
// ta-da sparkles (on 3s hold, popping)
|
|
1661
|
+
if (tq >= 4.5 && tq < 5.1) {
|
|
1662
|
+
const sp = [[-0.95, -1.1, 0.0], [0.98, -1.18, 0.083], [-1.2, -0.55, 0.208], [1.18, -0.62, 0.333], [0.1, -1.45, 0.167]];
|
|
1663
|
+
sp.forEach(([ux, uy, dt], k) => {
|
|
1664
|
+
const u = inv(4.5 + dt, 4.5 + dt + 0.4, tq);
|
|
1665
|
+
if (tq < 4.5 + dt || u >= 1) return;
|
|
1666
|
+
const R = 34 * Math.sin(Math.PI * Math.min(1, u * 1.15)) * (k === 4 ? 1.3 : 1);
|
|
1667
|
+
drawTwinkle(c, ss.x + ux * ss.H * 0.55, ss.y + uy * ss.H * 0.55, R, 1100 + k * 3 + D.id);
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
// anticipation lines converging before the burst (on 1s)
|
|
1671
|
+
if (t1 >= 6.25 && t1 < 6.5) {
|
|
1672
|
+
const u = inv(6.25, 6.5, t1);
|
|
1673
|
+
speedLinesRadial(c, ss.x, ss.y - ss.H * 0.3, lerp(560, 330, u), lerp(900, 620, u), 18, 1500 + f, 6, -1);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
c.restore(); // stage
|
|
1677
|
+
G.GLOBAL.ink = 1; G.GLOBAL.amp = 1;
|
|
1678
|
+
|
|
1679
|
+
// ---------------- BURST (hero)
|
|
1680
|
+
if (t1 >= 6.583) {
|
|
1681
|
+
// cream smoke: clusters of 2-3 puffs in 3 size classes; each dissipates over 4 drawings on 2s
|
|
1682
|
+
// (solid -> hole -> bigger hole -> broken ring -> gone)
|
|
1683
|
+
const puffs = [], tails = [];
|
|
1684
|
+
const u = tq - 6.583;
|
|
1685
|
+
for (const P of SMOKE) {
|
|
1686
|
+
const st = tq < P.td ? 0 : Math.floor((tq - P.td) / (2 / FPS)) + 1;
|
|
1687
|
+
if (st >= 4) continue;
|
|
1688
|
+
const dist = (330 + 600 * (1 - Math.exp(-3.2 * u)) * (0.8 + 0.4 * hs(P.k, 22))) * P.dist;
|
|
1689
|
+
const px = C0[0] + Math.cos(P.a) * dist * 1.25, py = C0[1] + Math.sin(P.a) * dist * 0.8 - 60 * u;
|
|
1690
|
+
const r = P.R * E.outBack(clamp(u / 0.3), 1.5) * (1 + 0.3 * u) * (1 - 0.08 * st);
|
|
1691
|
+
const cs = [0, 1, 2, 3, 4].map((j) => {
|
|
1692
|
+
const aa = j * 1.35 + P.k * 2.1 + hs(P.k, j, 31) * 0.8;
|
|
1693
|
+
return { x: px + Math.cos(aa) * r * (j ? 0.5 + 0.25 * hs(P.k, j, 32) : 0), y: py + Math.sin(aa) * r * (j ? 0.38 : 0), r: r * (j ? 0.38 + 0.3 * hs(P.k, j, 33) : 0.8) };
|
|
1694
|
+
});
|
|
1695
|
+
const ha = hs(P.k, 26) * 6.28;
|
|
1696
|
+
const holes = st === 0 ? [] : st === 1 ? [{ x: px + Math.cos(ha) * r * 0.25, y: py + Math.sin(ha) * r * 0.2, r: r * 0.3 }]
|
|
1697
|
+
: st === 2 ? [{ x: px + Math.cos(ha) * r * 0.15, y: py + Math.sin(ha) * r * 0.12, r: r * 0.62 }]
|
|
1698
|
+
: [{ x: px, y: py, r: r * 0.92 }, { x: px + Math.cos(ha + 2.6) * r * 0.9, y: py + Math.sin(ha + 2.6) * r * 0.6, r: r * 0.45 }, { x: px + Math.cos(ha - 0.6) * r * 0.95, y: py + Math.sin(ha - 0.6) * r * 0.5, r: r * 0.32 }];
|
|
1699
|
+
puffs.push({ x: px, y: py, r, cs, holes });
|
|
1700
|
+
if (P.lead && st <= 1 && r > 40) tails.push({ px, py, r, k: P.k });
|
|
1701
|
+
}
|
|
1702
|
+
// R2 (REVIEW_2 P7): dry-brush soot = 3 wide brush strokes (24-40 px), each 6-10 broken bristle sub-strokes (2-5 px),
|
|
1703
|
+
// ink @ 70 %, tapered frayed tails; grow on 1s, fade over 3 drawings on 2s (7.0 / 7.083 / 7.167)
|
|
1704
|
+
if (t1 >= 6.625 && tq < 7.25) {
|
|
1705
|
+
const fade = t1 < 7.0 ? 1 : [0.85, 0.55, 0.28][Math.min(2, Math.round((tq - 7.0) * 12))];
|
|
1706
|
+
const sv = t1 < 7 ? f : bv(D);
|
|
1707
|
+
c.save(); c.globalAlpha = 0.7 * fade;
|
|
1708
|
+
[[-0.42, 36], [2.45, 40], [4.05, 26]].forEach(([a0, BW], i) => {
|
|
1709
|
+
const g = E.out2(clamp((t1 - 6.625) / 0.2));
|
|
1710
|
+
const nb = 6 + Math.floor(hs(i, 50) * 5);
|
|
1711
|
+
const r0 = 340 + 40 * hs(i, 52), L = (320 + 170 * hs(i, 53)) * g, bend = (hs(i, 55) - 0.5) * 0.35;
|
|
1712
|
+
const at = (v, off) => {
|
|
1713
|
+
const ang = a0 + bend * v * v, rr = r0 + L * v, nx = -Math.sin(ang), ny = Math.cos(ang), o2 = off * (1 + 0.2 * v);
|
|
1714
|
+
return [C0[0] + Math.cos(ang) * rr * 1.1 + nx * o2, C0[1] + Math.sin(ang) * rr * 0.85 + ny * o2];
|
|
1715
|
+
};
|
|
1716
|
+
for (let j = 0; j < nb; j++) {
|
|
1717
|
+
const off = (j / (nb - 1) - 0.5) * BW + (hs(i, j, 51) - 0.5) * 4, edgeK = Math.abs(off) / (BW * 0.5);
|
|
1718
|
+
const vs = 0.05 * hs(i, j, 56), ve = 1 - (0.08 + 0.3 * edgeK) * hs(i, j, 57); // outer bristles run out first
|
|
1719
|
+
// broken dry-brush gaps: 1-2 gaps per bristle
|
|
1720
|
+
const cuts = [vs];
|
|
1721
|
+
const ng = 1 + (hs(i, j, 58) > 0.55 ? 1 : 0);
|
|
1722
|
+
for (let q = 0; q < ng; q++) { const m = lerp(vs, ve, (q + 1) / (ng + 1) + (hs(i, j, q, 59) - 0.5) * 0.2), gw = 0.03 + 0.05 * hs(i, j, q, 60); cuts.push(m - gw / 2, m + gw / 2); }
|
|
1723
|
+
cuts.push(ve);
|
|
1724
|
+
for (let q = 0; q + 1 < cuts.length; q += 2) {
|
|
1725
|
+
const va = cuts[q], vb = cuts[q + 1], last = q + 2 >= cuts.length;
|
|
1726
|
+
if (vb - va < 0.02) continue;
|
|
1727
|
+
const P = [0, 0.25, 0.5, 0.75, 1].map((u) => at(lerp(va, vb, u), off));
|
|
1728
|
+
inkStroke(c, P, { seed: 1900 + i * 97 + j * 7 + q + sv * 3, w: 2 + 3 * hs(i, j, 54), t0: 3, t1: last ? 45 : 5, min: last ? 0.05 : 0.4, amp: 1.2, heavy: 0 });
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
c.restore();
|
|
1733
|
+
}
|
|
1734
|
+
// radial speed lines, badge (fire), starburst, smoke
|
|
1735
|
+
if (t1 < 6.92) speedLinesRadial(c, C0[0], C0[1], lerp(560, 760, inv(6.583, 6.92, t1)), 1350, 26, 1700 + f, 9, 1);
|
|
1736
|
+
drawBadge(c, t1 < 7.0 ? t1 : tq, D);
|
|
1737
|
+
drawStarburst(c, t1, D);
|
|
1738
|
+
smokeGroup(c, puffs, { seed: 1600 + (t1 < 7.0 ? f : bv(D)) * 7, w: 6 });
|
|
1739
|
+
// inked curl tails on the lead puffs (point back at the blast)
|
|
1740
|
+
for (const T of tails) {
|
|
1741
|
+
const bx = C0[0] - T.px, by = C0[1] - T.py, bl = Math.hypot(bx, by) || 1, ux = bx / bl, uy = by / bl;
|
|
1742
|
+
const P = [];
|
|
1743
|
+
for (let i = 0; i <= 16; i++) {
|
|
1744
|
+
const v = i / 16, d = T.r * (0.85 + 0.75 * v), cr = T.r * 0.28 * v * v;
|
|
1745
|
+
const ca = 5.2 * v * v;
|
|
1746
|
+
P.push([T.px + ux * d + (-uy * Math.sin(ca) + ux * (Math.cos(ca) - 1)) * cr, T.py + uy * d + (ux * Math.sin(ca) + uy * (Math.cos(ca) - 1)) * cr]);
|
|
1747
|
+
}
|
|
1748
|
+
inkStroke(c, P, { seed: 1650 + T.k * 7 + (t1 < 7.0 ? f : bv(D)), w: 5.5, t0: 6, t1: 30, min: 0.15, amp: 1.3, heavy: 0 });
|
|
1749
|
+
}
|
|
1750
|
+
// flying sparks (on 1s)
|
|
1751
|
+
if (t1 < 7.35) {
|
|
1752
|
+
for (let k = 0; k < NSPARK; k++) {
|
|
1753
|
+
const s = sparkState(k, t1);
|
|
1754
|
+
const fade = 1 - inv(7.0, 7.35, t1);
|
|
1755
|
+
if (fade <= 0) continue;
|
|
1756
|
+
if (k % 4 === 3) drawTwinkle(c, s.x, s.y, s.size * 1.6 * fade, 1800 + k * 7 + f, { rot: f * 0.3 + k });
|
|
1757
|
+
else { const j = (hs(k, 15) - 0.5) * 0.87, cj = Math.cos(j), sj = Math.sin(j); drawSpark(c, s.x, s.y, s.vx * cj - s.vy * sj, s.vx * sj + s.vy * cj, s.size * fade, 1800 + k * 7 + f, { inkOnly: k % 4 === 1 }); }
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
// ---------------- TITLE
|
|
1762
|
+
if (t1 >= 7.2) {
|
|
1763
|
+
const st = CUES.strokes;
|
|
1764
|
+
const prog = V5 ? STROKES.map((S) => inv(S.t0, S.t0 + S.dur, t1)) : STROKES.map((S, i) => inv(st[i] - 0.04, st[i] + (S.k === 'p' ? 0.1 : 0.14), t1));
|
|
1765
|
+
const settle = K(t1, [[8.5, 0], [8.583, 1.25, 'out2'], [8.667, 0.9], [8.75, 1.0]]);
|
|
1766
|
+
// title bounce with the badge
|
|
1767
|
+
const sc = 1 + K(t1, [[8.5, 0], [8.54, 0.05, 'out2'], [8.625, -0.02], [8.75, 0.008], [8.875, 0]]);
|
|
1768
|
+
c.save(); c.translate(BADGE.x, BADGE.y); c.scale(sc, sc); c.translate(-BADGE.x, -BADGE.y);
|
|
1769
|
+
drawTitle(c, prog, t1 < 8.5 ? { id: f, step: 1 } : D, { shadow: settle, shine: inv(8.5, 8.6, t1) });
|
|
1770
|
+
// fuse heads writing the strokes
|
|
1771
|
+
STROKES.forEach((S, i) => {
|
|
1772
|
+
const p = prog[i];
|
|
1773
|
+
if (p <= 0 || p >= 1) return;
|
|
1774
|
+
const full = titleDense(S, 3000 + i * 11, 1.5);
|
|
1775
|
+
const part = prefix(full, E.out2(p));
|
|
1776
|
+
const tip = part[part.length - 1];
|
|
1777
|
+
const sb = spline(xf(starPts(8, 34 + 8 * hs(f, i), 16, 1990 + f + i, f * 0.7, 0.35), { x: tip[0], y: tip[1] }), true, 3);
|
|
1778
|
+
fillShape(c, sb, PAL.yel, { seed: 1995 + f + i, amp: 1 });
|
|
1779
|
+
inkLoop(c, sb, { seed: 1996 + f + i, w: 3.2, amp: 1, t0: 3, t1: 6 });
|
|
1780
|
+
drawTwinkle(c, tip[0], tip[1], 26 + 6 * hs(f, i), 2000 + f * 3 + i, { rot: f * 0.5, fill: PAL.hi });
|
|
1781
|
+
for (let k = 0; k < 3; k++) {
|
|
1782
|
+
const a = hs(f, i, k) * 6.28, d = 20 + 30 * hs(f, i, k, 2);
|
|
1783
|
+
drawSpark(c, tip[0] + Math.cos(a) * d, tip[1] + Math.sin(a) * d, Math.cos(a) * 600, Math.sin(a) * 600, 5, 2100 + f * 5 + k + i);
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
c.restore();
|
|
1787
|
+
// incoming fuse sparks: travel from their burst position to the stroke starts (on 1s)
|
|
1788
|
+
STROKES.forEach((S, i) => {
|
|
1789
|
+
const tS = V5 ? S.t0 : st[i] - 0.04, tA = Math.max(7.0, tS - 0.3);
|
|
1790
|
+
if ((V5 ? !S.first || S.ci === 0 : i === 0) || t1 < tA || t1 >= tS) return; // V5: one incoming fuse per character
|
|
1791
|
+
const k = (i * 3 + 1) % NSPARK;
|
|
1792
|
+
const p0 = sparkState(k, tA), p1 = strokeWorld(S)[0];
|
|
1793
|
+
const u = E.io2(inv(tA, tS, t1));
|
|
1794
|
+
const mx = (p0.x + p1[0]) / 2 + (p1[1] - p0.y) * 0.3, my = (p0.y + p1[1]) / 2 - (p1[0] - p0.x) * 0.3;
|
|
1795
|
+
const x = (1 - u) * (1 - u) * p0.x + 2 * u * (1 - u) * mx + u * u * p1[0];
|
|
1796
|
+
const y = (1 - u) * (1 - u) * p0.y + 2 * u * (1 - u) * my + u * u * p1[1];
|
|
1797
|
+
const dx = 2 * (1 - u) * (mx - p0.x) + 2 * u * (p1[0] - mx), dy = 2 * (1 - u) * (my - p0.y) + 2 * u * (p1[1] - my);
|
|
1798
|
+
drawSpark(c, x, y, dx * 2, dy * 2, 11, 2200 + f * 7 + i);
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
// ---------------- HAND MADE + tagline + tiny spirit
|
|
1802
|
+
if (t1 >= 8.5) {
|
|
1803
|
+
const pop = (i) => { const t0 = CUES.letters[Math.min(3, Math.floor(i / 2))] - 0.001; return tq < t0 ? 0 : K(tq, [[t0, 1.35], [t0 + 0.083, 0.92], [t0 + 0.167, 1.0]]); };
|
|
1804
|
+
if (V5) { // V5: FRAME BY FRAME in the same marker caps, popping in 4 groups on the same cues
|
|
1805
|
+
const popV = (i) => { const t0 = CUES.letters[Math.min(3, Math.floor(i / 3.5))] - 0.001; return tq < t0 ? 0 : K(tq, [[t0, 1.35], [t0 + 0.083, 0.92], [t0 + 0.167, 1.0]]); };
|
|
1806
|
+
drawWord(c, 'FRAME BY FRAME', BADGE.x, 690, 58, D, { center: true, pop: popV, color: PAL.hi, track: 0.34, wk: 0.16, outline: 3.0, shadow: 4.5 });
|
|
1807
|
+
} else
|
|
1808
|
+
drawWord(c, 'HAND MADE', BADGE.x, 716, 78, D, { center: true, pop, color: PAL.hi, track: 0.37, wk: 0.155, outline: 3.2, shadow: 5 });
|
|
1809
|
+
drawTagline(c, D, inv(CUES.tagline[0], CUES.tagline[1], tq), 960, 930);
|
|
1810
|
+
// twinkles popping around the lockup (held on 3s)
|
|
1811
|
+
const TW = [[392, 250, 8.583, 30], V5 ? [1470, 104, 8.75, 34] : [1640, 190, 8.75, 34], [1560, 748, 8.917, 26], [372, 712, 9.0, 24], [690, 140, 9.25, 20], [1640, 532, 9.375, 18]];
|
|
1812
|
+
TW.forEach(([x, y, t0, R], k) => {
|
|
1813
|
+
if (tq < t0) return;
|
|
1814
|
+
const cyc = [1.0, 0.55, 0.8][Math.floor((tq - t0) * 8) % 3];
|
|
1815
|
+
const pop = E.outBack(clamp((tq - t0) / 0.12), 2.2);
|
|
1816
|
+
drawTwinkle(c, x, y, R * pop * cyc, 6200 + k * 13 + bv(D), { rot: 0.1 * k });
|
|
1817
|
+
});
|
|
1818
|
+
// tiny spirit reborn on top of the badge
|
|
1819
|
+
const SP = CUES.spirit_pop, WK = CUES.wink;
|
|
1820
|
+
if (tq >= SP - 0.001) {
|
|
1821
|
+
drawSpirit(c, tinySpirit(tq, D), D, tq);
|
|
1822
|
+
if (V5 && !NODOT) { const em = emberState(tq, D); if (em) drawEmber(c, em, D); }
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
c.restore();
|
|
1826
|
+
// ---------------- before the match is lit the sheet is in shadow; the strike flashes it on
|
|
1827
|
+
if (f < 12) nightPass(c, D, f, ms);
|
|
1828
|
+
else if (f === 12) flashFrame(c, ms);
|
|
1829
|
+
// ---------------- pencil annotations (on the sheet)
|
|
1830
|
+
drawPencil(c, D, f);
|
|
1831
|
+
drawTooth(c, D);
|
|
1832
|
+
// ---------------- IMPACT FRAMES (hero): two full-frame flat colour frames
|
|
1833
|
+
if (f === 156 || f === 157) impactFrame(c, f);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function tinySpirit(tq, D) { // tiny spirit reborn on top of the badge
|
|
1837
|
+
const SP = CUES.spirit_pop, WK = CUES.wink;
|
|
1838
|
+
const u = tq - SP;
|
|
1839
|
+
const sq = 1 + K(tq, [[SP, -0.45], [SP + 0.125, 0.22, 'out2'], [SP + 0.25, -0.06], [SP + 0.375, 0]]);
|
|
1840
|
+
return { x: V5 ? 1666 : 1430, y: (V5 ? 294 : 262) - K(tq, [[SP, 0], [SP + 0.125, 24, 'out2'], [SP + 0.25, 0]]), H: 178 * E.outBack(clamp((u + 0.04) / 0.16), 2), sx: 1 / Math.pow(sq, 0.8), sy: sq, kx: 0, curl: 0, r: 0.08, wild: 0.15,
|
|
1841
|
+
drops: false, face: { eyes: tq >= WK - 0.001 && tq < WK + 0.2 ? 'wink' : 'open', mouth: tq >= WK - 0.001 ? 'grin' : 'smile', look: -0.6 }, seed: 3000 + D.id,
|
|
1842
|
+
arms: u > 0.1 ? { L: { ang: -0.5, bend: 0.4 }, R: V5 ? { ang: 1.05, bend: -0.3 } : { ang: tq >= WK - 0.13 ? 1.25 + 0.35 * Math.sin(tq * 26) : 0.2, bend: -0.3 } } : null };
|
|
1843
|
+
}
|
|
1844
|
+
// V5: the tiny spirit holds up an ember (a fire lump from the badge: tomato disc, yellow highlight, boiling ink) = the film's closing dot
|
|
1845
|
+
function armHand(s, side, a) {
|
|
1846
|
+
const sh = [side * 0.4, -0.36], len = a.len ?? 0.42, dx = side * Math.cos(a.ang), dy = -Math.sin(a.ang);
|
|
1847
|
+
return spiritXf([[sh[0] + dx * len, sh[1] + dy * len]], { ...s, sx: (s.sx + 1) / 2, sy: (s.sy + 1) / 2 })[0];
|
|
1848
|
+
}
|
|
1849
|
+
const EMBER_R = 46;
|
|
1850
|
+
function emberState(tq, D) {
|
|
1851
|
+
const s = tinySpirit(tq, D), a = s.arms && s.arms.R;
|
|
1852
|
+
if (!a || s.H < 20) return null;
|
|
1853
|
+
const hp = armHand(s, 1, a), gR = s.H * 0.082 * (a.big ?? 1);
|
|
1854
|
+
const r = EMBER_R * E.outBack(clamp((tq - (CUES.spirit_pop + 0.075)) / 0.12), 2);
|
|
1855
|
+
return { x: hp[0] + 6, y: hp[1] - gR - r * 0.72, r };
|
|
1856
|
+
}
|
|
1857
|
+
function drawEmber(c, e, D) {
|
|
1858
|
+
const sv = bv(D), d = circlePts(e.x, e.y, e.r, 30);
|
|
1859
|
+
fillShape(c, d, PAL.red, { seed: 9600 + sv, amp: 1.5, dx: REG.dx * 0.7, dy: REG.dy * 0.7 });
|
|
1860
|
+
fillShape(c, circlePts(e.x - e.r * 0.26, e.y - e.r * 0.28, e.r * 0.5, 20), PAL.yel, { seed: 9605 + sv, amp: 1.3 });
|
|
1861
|
+
inkLoop(c, dens(d, 4), { seed: 9610 + sv, w: 5, amp: 1.4, heavy: 0.35, t0: 4, t1: 8 });
|
|
1862
|
+
inkStroke(c, [[e.x - e.r * 0.5, e.y - e.r * 0.06], [e.x - e.r * 0.36, e.y - e.r * 0.42]], { seed: 9615 + sv, w: 5, t0: 3, t1: 5, color: PAL.hi, amp: 0.8, heavy: 0 });
|
|
1863
|
+
[-2.35, -1.6, -0.85].forEach((a, k) => { // glow ticks
|
|
1864
|
+
const r0 = e.r * (1.3 + 0.08 * ((sv + k) % 2)), r1 = r0 + e.r * 0.34;
|
|
1865
|
+
inkStroke(c, [[e.x + Math.cos(a) * r0, e.y + Math.sin(a) * r0], [e.x + Math.cos(a) * r1, e.y + Math.sin(a) * r1]], { seed: 9620 + k + sv * 3, w: 4.5, t0: 4, t1: 5, amp: 0.8, heavy: 0 });
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
function impactFrame(c, f) {
|
|
1869
|
+
c.save();
|
|
1870
|
+
c.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
1871
|
+
c.fillStyle = f === 156 ? PAL.ink : PAL.red;
|
|
1872
|
+
c.fillRect(0, 0, W, H);
|
|
1873
|
+
const P = spline(xf(starPts(14, f === 156 ? 520 : 700, f === 156 ? 250 : 330, 77 + f, f * 0.3, 0.3), { x: C0[0], y: C0[1] }), true, 6);
|
|
1874
|
+
if (f === 156) {
|
|
1875
|
+
fillShape(c, P, PAL.hi, { seed: f, amp: 2 });
|
|
1876
|
+
const Q = spline(xf(starPts(10, 260, 130, 99, 0.4, 0.3), { x: C0[0], y: C0[1] }), true, 5);
|
|
1877
|
+
fillShape(c, Q, PAL.yel, { seed: f + 1, amp: 2 });
|
|
1878
|
+
} else {
|
|
1879
|
+
speedLinesRadial(c, C0[0], C0[1], 380, 1300, 30, 2400, 16, 1);
|
|
1880
|
+
fillShape(c, P, PAL.yel, { seed: f, amp: 2 });
|
|
1881
|
+
inkLoop(c, P, { seed: f + 3, w: 12, amp: 2 });
|
|
1882
|
+
const Q = spline(xf(starPts(10, 360, 190, 98, 0.7, 0.3), { x: C0[0], y: C0[1] }), true, 5);
|
|
1883
|
+
fillShape(c, Q, PAL.hi, { seed: f + 1, amp: 2 });
|
|
1884
|
+
}
|
|
1885
|
+
c.restore();
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function drawPencil(c, D, f) {
|
|
1889
|
+
c.save();
|
|
1890
|
+
c.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
1891
|
+
const jx = (hs(D.id, 71) - 0.5) * 1.2, jy = (hs(D.id, 72) - 0.5) * 1.2;
|
|
1892
|
+
const ex = D.step === 1 ? 'on 1s' : D.step === 2 ? 'on 2s' : 'on 3s';
|
|
1893
|
+
if (D.fq < 218) {
|
|
1894
|
+
if (f > 12) { // R2: hand-cut paper knock-out, always on, so the notes never sit on a coloured cel
|
|
1895
|
+
fillShape(c, dens(rectPts(W - 290, H - 156, 226, 126), 8), PAL.cream, { seed: 9500 + bv(D), amp: 2.5 });
|
|
1896
|
+
}
|
|
1897
|
+
c.globalAlpha = 0.8; c.fillStyle = PAL.pencil; c.textAlign = 'right';
|
|
1898
|
+
c.font = '600 42px "Caveat"';
|
|
1899
|
+
c.fillText('No. ' + String(D.id + 1).padStart(3, '0'), W - 88 + jx, H - 56 + jy);
|
|
1900
|
+
c.font = '500 30px "Caveat"';
|
|
1901
|
+
c.fillText(ex, W - 88 + jx, H - 98 + jy);
|
|
1902
|
+
} else {
|
|
1903
|
+
const x1 = W - 128 + jx, yb = H - 70 + jy;
|
|
1904
|
+
c.globalAlpha = 0.93; c.fillStyle = PAL.pencil; c.textAlign = 'right';
|
|
1905
|
+
c.font = '400 86px "Long Cang"'; c.fillText('张', x1, yb + 2); // R1s2: hero note 80->92 px (~77 px on screen after the 84 % pull-back)
|
|
1906
|
+
const zw = c.measureText('张').width;
|
|
1907
|
+
c.font = '700 92px "Caveat"'; const txt = String(TOTAL); c.fillText(txt, x1 - zw - 14, yb); // R2: '第 146 张' (was 'No. 146 张')
|
|
1908
|
+
const nw = c.measureText(txt).width;
|
|
1909
|
+
c.font = '400 86px "Long Cang"'; c.fillText('第', x1 - zw - 14 - nw - 16, yb + 2);
|
|
1910
|
+
const tw = c.measureText('第').width + 16 + nw + 14 + zw;
|
|
1911
|
+
const cx = x1 - tw / 2 - 4, cy = yb - 27, rx = tw / 2 + 40, ry = 68;
|
|
1912
|
+
const p = D.fq >= 220 ? 1 : 0.5; // R2: loop starts with the pull (f218), closes f220
|
|
1913
|
+
c.globalAlpha = 1;
|
|
1914
|
+
if (p > 0) { // red loop, drawn on across 2 drawings, overshooting its start
|
|
1915
|
+
const P = [], n = 44, m = Math.round(n * 1.16 * p);
|
|
1916
|
+
for (let i = 0; i <= m; i++) { const a = -2.7 + (i / n) * Math.PI * 2, g = 1 + 0.07 * (i / n); P.push([cx + Math.cos(a) * rx * g, cy + Math.sin(a) * ry * g - 5 * (i / n)]); }
|
|
1917
|
+
inkStroke(c, xf(P.map(([x, y]) => [x - cx, y - cy]), { x: cx, y: cy, r: -0.05 }), { seed: 9300 + (p < 1 ? 0 : bv(D)), w: 5.5, t0: 10, t1: 26, min: 0.35, amp: 1.2, heavy: 0, color: PAL.pencil });
|
|
1918
|
+
}
|
|
1919
|
+
if (D.fq >= 222) { // arrow to the tagline
|
|
1920
|
+
const A = [cx - rx - 8, cy - 22], B = [1318, 918];
|
|
1921
|
+
const M = [(A[0] + B[0]) / 2 + 6, Math.min(A[1], B[1]) - 46];
|
|
1922
|
+
inkStroke(c, [A, M, B], { seed: 9320 + bv(D), w: 5, t0: 8, t1: 6, min: 0.4, amp: 1.2, heavy: 0, color: PAL.pencil });
|
|
1923
|
+
const d = Math.atan2(B[1] - M[1], B[0] - M[0]);
|
|
1924
|
+
for (const sg of [-1, 1]) {
|
|
1925
|
+
const a = d + Math.PI + sg * 0.55;
|
|
1926
|
+
inkStroke(c, [B, [B[0] + Math.cos(a) * 30, B[1] + Math.sin(a) * 30]], { seed: 9330 + sg + bv(D), w: 5, t0: 4, t1: 8, min: 0.4, amp: 1, heavy: 0, color: PAL.pencil });
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
c.globalAlpha = 0.78; c.font = '500 32px "Caveat"'; c.fillText(ex, W - 96 + jx, H - 160 + jy);
|
|
1930
|
+
}
|
|
1931
|
+
c.globalAlpha = 0.78; c.fillStyle = PAL.pencil; c.textAlign = 'left';
|
|
1932
|
+
c.font = '600 34px "Caveat"';
|
|
1933
|
+
if (f > 12 && D.fq < 218) { c.globalAlpha = 1; fillShape(c, dens(rectPts(80, 72, 150, 58), 8), PAL.cream, { seed: 9510 + bv(D), amp: 2 }); c.globalAlpha = 0.78; }
|
|
1934
|
+
c.fillStyle = PAL.pencil; // R2s2 fix: fillShape left fillStyle = cream, so the slate text was invisible
|
|
1935
|
+
c.fillText('SC. 05', 100 + jx, 114 + jy); // R2: moved top-left (was bottom-left, sat on the box corner)
|
|
1936
|
+
c.restore();
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// ------------------------------------------------------------------ reel v5: link elements, a pure function of t (1920×1080 output px)
|
|
1940
|
+
function shotAt(f) { // the stage transform drawFrame() uses for frame f (no globals touched)
|
|
1941
|
+
if (f >= 132) { const u = E.in2(inv(5.5, 6.5, f / FPS)); return { s: lerp(STAGE.s, 2.05, u), px: 860, py: 606, X: 862, Y: 618 }; }
|
|
1942
|
+
return shotFor(f);
|
|
1943
|
+
}
|
|
1944
|
+
export function linkAt(t) {
|
|
1945
|
+
const f = Math.min(239, Math.max(0, Math.round(t * FPS)));
|
|
1946
|
+
const D = drawing(f), tq = D.fq / FPS, t1 = f / FPS, cam = camera(f);
|
|
1947
|
+
const sc = pullScale(D.fq), rif = f >= 224 && f <= 229 ? f - 224 : -1;
|
|
1948
|
+
const ox = rif >= 0 ? (hs(f, 1) - 0.5) * 12 : 0, oy = rif >= 0 ? -6 - 8 * hs(f, 2) : 0, rot = rif >= 0 ? (hs(f, 3) - 0.5) * 0.014 : 0;
|
|
1949
|
+
const sheet = ([x, y]) => { // sheet px -> frame px (end pull-back onto the light table + riffle lift)
|
|
1950
|
+
if (sc >= 1) return [x, y];
|
|
1951
|
+
const X = (x - PIV[0]) * sc, Y = (y - PIV[1]) * sc, c = Math.cos(rot), s = Math.sin(rot);
|
|
1952
|
+
return [PIV[0] + ox + X * c - Y * s, PIV[1] + oy + X * s + Y * c];
|
|
1953
|
+
};
|
|
1954
|
+
const sh = shotAt(f);
|
|
1955
|
+
const stage = ([x, y]) => sheet([cam.x + sh.X + (x - sh.px) * sh.s, cam.y + sh.Y + (y - sh.py) * sh.s]);
|
|
1956
|
+
const onSheet = ([x, y]) => sheet([x + cam.x, y + cam.y]);
|
|
1957
|
+
const R1 = (v) => Math.round(v * 10) / 10;
|
|
1958
|
+
const out = [];
|
|
1959
|
+
// ---- dot 1: the match head (0 – 6.49 s). Red until 2.0 (night-darkened before 0.5), burnt ink-black after; hidden under the flame 0.5–2.0
|
|
1960
|
+
const ms = matchState(tq);
|
|
1961
|
+
if (ms.vis && tq < 6.5 && f !== 156 && f !== 157) {
|
|
1962
|
+
const [hc] = xf([[0, -2]], { x: ms.hx, y: ms.hy, r: ms.r });
|
|
1963
|
+
const [x, y] = stage(hc);
|
|
1964
|
+
const a = f <= 8 || f === 11 ? 1 : f === 9 || f === 10 ? 0.4 : f === 12 ? 0 : f < 49 ? 0 : 1;
|
|
1965
|
+
const night = f < 12;
|
|
1966
|
+
out.push({ id: 'dot', type: 'circle', x: R1(x), y: R1(y), r: R1(HEAD_R * sh.s * sc), a, look: 'ink',
|
|
1967
|
+
fill: ms.burnt ? PAL.ink : night ? '#44241b' : PAL.red, stroke: PAL.ink, sw: R1(5.5 * sh.s * (f < 12 ? 0.58 : 1)),
|
|
1968
|
+
note: ms.burnt ? 'burnt match head (ink disc, red ember speck), boiling ink outline' : night ? 'match head in the dark close-up (red, night-darkened), trembling' : a ? 'match head' : 'match head under the flame spirit / smear' });
|
|
1969
|
+
}
|
|
1970
|
+
// ---- dot 2: the explosion core (6.92 – 7.29 s): yellow starburst ball with a paper-white core; travels to 逐's first stroke and lights the fuse
|
|
1971
|
+
const tb = t1 < 7.0 ? t1 : tq;
|
|
1972
|
+
if (t1 > 6.88 && tb < 7.3) {
|
|
1973
|
+
const coreU = E.out3(inv(6.62, 6.9, tb)), mv = morphP(tb);
|
|
1974
|
+
const qx = lerp(C0[0], BADGE.x, mv), qy = lerp(C0[1], BADGE.y, mv), qr = lerp(260, BADGE.ry, mv) * coreU * (1 + 0.06 * Math.sin(Math.PI * mv));
|
|
1975
|
+
if (qr > 20) {
|
|
1976
|
+
const p0 = strokeWorld(STROKES[0])[0], stp = tb < 7.12 ? 0 : tb < 7.2 ? 1 : 2;
|
|
1977
|
+
const e = [0, 0.55, 1][stp], R = Math.min(qr * 0.8, 240) * [1, 0.45, 0.2][stp];
|
|
1978
|
+
const ax = qx + 6, ay = qy + 4, mx = (ax + p0[0]) / 2 - (p0[1] - ay) * 0.35, my = (ay + p0[1]) / 2 + (p0[0] - ax) * 0.35;
|
|
1979
|
+
const gx = (1 - e) * (1 - e) * ax + 2 * e * (1 - e) * mx + e * e * p0[0], gy = (1 - e) * (1 - e) * ay + 2 * e * (1 - e) * my + e * e * p0[1];
|
|
1980
|
+
const [x, y] = onSheet([gx, gy]);
|
|
1981
|
+
out.push({ id: 'dot', type: 'circle', x: R1(x), y: R1(y), r: R1(R * 0.8), a: 1, look: 'ink', fill: PAL.yel, stroke: stp ? PAL.ink : PAL.yel, sw: stp ? 4 : 0,
|
|
1982
|
+
grad: [PAL.hi, PAL.yel], note: `explosion core (9-point yellow star ball, paper-white centre; r = mean star radius)${stp ? ' flying to the first stroke, lights the fuse' : ''}` });
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
// ---- dot 3 (V5): the ember the tiny spirit holds up at the end (9.25 – 10 s)
|
|
1986
|
+
if (tq >= CUES.spirit_pop - 0.001) {
|
|
1987
|
+
const em = emberState(tq, D);
|
|
1988
|
+
if (em && em.r > 1) {
|
|
1989
|
+
const [x, y] = onSheet([em.x, em.y]);
|
|
1990
|
+
out.push({ id: 'dot', type: 'circle', x: R1(x), y: R1(y), r: R1((em.r + 2.5) * sc), a: 1, look: 'ink', fill: PAL.red, stroke: PAL.ink, sw: R1(5 * sc),
|
|
1991
|
+
grad: [PAL.yel, PAL.red], note: 'ember held up by the tiny flame spirit (tomato disc, yellow highlight up-left, boiling ink outline, 3 glow ticks)' });
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
// ---- other link candidates
|
|
1995
|
+
const ss = spiritState(tq, D, ms);
|
|
1996
|
+
if (ss && tq >= 0.5 && tq < 6.5 && f !== 156 && f !== 157) { // the stepped light pool behind the spirit (big pale disc)
|
|
1997
|
+
let R = K(tq, [[0.5, 60], [0.542, 330, 'out3'], [0.625, 380], [1.0, 310], [2.0, 315], [2.5, 345], [5.5, 350], [6.25, 430, 'io2'], [6.5, 470]]);
|
|
1998
|
+
R *= 1 + 0.03 * (bv(D) % 3 - 1);
|
|
1999
|
+
const [x, y] = stage([ss.x, ss.y - ss.H * 0.42 * ss.sy]);
|
|
2000
|
+
out.push({ id: 'pool', type: 'circle', x: R1(x), y: R1(y), r: R1(R * sh.s), a: f === 12 ? 0 : 1, look: 'flat', fill: '#f0d794', stroke: '#f0d794', sw: 0,
|
|
2001
|
+
note: 'flickering light pool of the flame (inner step; outer pale step at 1.25 r), multiplied into the paper' });
|
|
2002
|
+
}
|
|
2003
|
+
if (ms.vis && tq >= 1.0 && tq < 6.5 && f !== 156 && f !== 157) { // the (burnt) match stick, vertical
|
|
2004
|
+
const [H0, B0] = stickEnds(ms), [x0, y0] = stage(H0), [x1, y1] = stage(B0);
|
|
2005
|
+
out.push({ id: 'stick', type: 'line', x0: R1(x0), y0: R1(y0), x1: R1(x1), y1: R1(y1), w: R1(21 * sh.s), a: 1, note: 'match stick (head to butt)' });
|
|
2006
|
+
}
|
|
2007
|
+
const bs = boxState(tq);
|
|
2008
|
+
if (bs.vis && !bs.blown && f > 12 && tq < 6.5 && f !== 156 && f !== 157) { // matchbox top front edge = the stage line
|
|
2009
|
+
const y1 = shotFor(f) === LOW && f < 132 ? -112 : -90;
|
|
2010
|
+
const T = (P) => xf(P, { x: BOXP.px + bs.x, y: BOXP.py + bs.y, r: bs.r, sx: bs.sx, sy: bs.sy });
|
|
2011
|
+
const [A, B] = T([[-370, y1], [370, y1]]), [x0, y0] = stage(A), [x1, y1b] = stage(B);
|
|
2012
|
+
out.push({ id: 'box', type: 'line', x0: R1(x0), y0: R1(y0), x1: R1(x1), y1: R1(y1b), w: R1(5.2 * sh.s), a: 1, note: 'matchbox front top edge (red top / yellow front)' });
|
|
2013
|
+
}
|
|
2014
|
+
if (V5 && tq >= 7.33) { // the settled scalloped fire badge + the title block
|
|
2015
|
+
const bb = K(t1, [[8.5, 0], [8.54, 0.045, 'out2'], [8.625, -0.02], [8.75, 0.008], [8.875, 0]]) + 1;
|
|
2016
|
+
const [x, y] = onSheet([BADGE.x, BADGE.y]);
|
|
2017
|
+
out.push({ id: 'badge', type: 'rect', x: R1(x), y: R1(y), w: R1((BADGE.rx + 110) * 2 * bb * sc), h: R1((BADGE.ry + 100) * 2 * bb * sc), rot: R1(rot * 1000) / 1000, a: 1,
|
|
2018
|
+
fill: PAL.red, note: 'scalloped tomato fire badge (a boiling blob of circles) behind the title' });
|
|
2019
|
+
if (t1 >= 8.375) {
|
|
2020
|
+
const tb2 = TITLE_BOX(), ts = 1 + K(t1, [[8.5, 0], [8.54, 0.05, 'out2'], [8.625, -0.02], [8.75, 0.008], [8.875, 0]]);
|
|
2021
|
+
const [cx, cy] = onSheet([BADGE.x + (tb2.x - BADGE.x) * ts, BADGE.y + (tb2.y - BADGE.y) * ts]);
|
|
2022
|
+
out.push({ id: 'title', type: 'rect', x: R1(cx), y: R1(cy), w: R1(tb2.w * ts * sc), h: R1(tb2.h * ts * sc), rot: R1(rot * 1000) / 1000, a: 1,
|
|
2023
|
+
fill: PAL.yel, note: 'title 逐帧手绘 (yellow, ink outline, block shadow)' });
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return out;
|
|
2027
|
+
}
|
|
2028
|
+
let TBOX = null;
|
|
2029
|
+
function TITLE_BOX() { // bounds of the written title (stroke centre-lines ± half width + ink ring)
|
|
2030
|
+
if (TBOX) return TBOX;
|
|
2031
|
+
let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
|
|
2032
|
+
const m = TITLE.S * (TITLE.w ?? 0.165) * 0.65 + (TITLE.ink ?? 6.8);
|
|
2033
|
+
for (const st of STROKES) for (const [x, y] of spline(strokeWorld(st), false, 5)) { x0 = Math.min(x0, x - m); x1 = Math.max(x1, x + m); y0 = Math.min(y0, y - m); y1 = Math.max(y1, y + m); }
|
|
2034
|
+
TBOX = { x: (x0 + x1) / 2, y: (y0 + y1) / 2, w: x1 - x0, h: y1 - y0 };
|
|
2035
|
+
return TBOX;
|
|
2036
|
+
}
|