kasaframe 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +146 -0
- package/README.md +77 -2
- package/app/package.json +12 -0
- package/app/server/engine.ts +415 -0
- package/app/server/main.ts +205 -0
- package/app/web/env.d.ts +2 -0
- package/app/web/icons.ts +31 -0
- package/app/web/index.html +21 -0
- package/app/web/main.ts +1013 -0
- package/app/web/style.css +430 -0
- package/bin/hooks.mjs +51 -0
- package/bin/kf.mjs +14 -0
- package/docs/AGENTS.md +238 -0
- package/music/README.md +13 -0
- package/package.json +44 -4
- package/packages/adapters/package.json +36 -0
- package/packages/adapters/src/css.ts +11 -0
- package/packages/adapters/src/gsap.ts +26 -0
- package/packages/adapters/src/index.ts +9 -0
- package/packages/adapters/src/lottie.ts +22 -0
- package/packages/adapters/src/media.ts +59 -0
- package/packages/adapters/src/page.ts +229 -0
- package/packages/adapters/src/project.ts +214 -0
- package/packages/adapters/src/react.ts +36 -0
- package/packages/adapters/src/svelte.ts +22 -0
- package/packages/adapters/src/three.ts +18 -0
- package/packages/adapters/src/vue.ts +27 -0
- package/packages/check/package.json +19 -0
- package/packages/check/src/check.ts +273 -0
- package/packages/check/src/cli.ts +70 -0
- package/packages/check/src/image.ts +146 -0
- package/packages/check/src/index.ts +9 -0
- package/packages/check/src/ocr.swift +37 -0
- package/packages/check/src/probe.ts +328 -0
- package/packages/check/src/report.ts +44 -0
- package/packages/check/src/rules/assets.ts +42 -0
- package/packages/check/src/rules/beats.ts +212 -0
- package/packages/check/src/rules/camera.ts +217 -0
- package/packages/check/src/rules/captions.ts +49 -0
- package/packages/check/src/rules/flash.ts +235 -0
- package/packages/check/src/rules/frames.ts +135 -0
- package/packages/check/src/rules/layout.ts +257 -0
- package/packages/check/src/rules/page.ts +87 -0
- package/packages/check/src/rules/runs.ts +20 -0
- package/packages/check/src/rules/text.ts +326 -0
- package/packages/check/src/scan.ts +240 -0
- package/packages/check/src/serve.ts +67 -0
- package/packages/check/src/types.ts +80 -0
- package/packages/check/src/video.ts +137 -0
- package/packages/cli/package.json +29 -0
- package/packages/cli/src/blur.ts +68 -0
- package/packages/cli/src/browser.ts +203 -0
- package/packages/cli/src/command.ts +44 -0
- package/packages/cli/src/commands/app.ts +25 -0
- package/packages/cli/src/commands/beats.ts +36 -0
- package/packages/cli/src/commands/blender.ts +270 -0
- package/packages/cli/src/commands/check.ts +25 -0
- package/packages/cli/src/commands/doctor.ts +22 -0
- package/packages/cli/src/commands/gen.ts +173 -0
- package/packages/cli/src/commands/look.ts +39 -0
- package/packages/cli/src/commands/lyrics.ts +111 -0
- package/packages/cli/src/commands/make.ts +542 -0
- package/packages/cli/src/commands/new.ts +176 -0
- package/packages/cli/src/commands/preview.ts +43 -0
- package/packages/cli/src/commands/render.ts +216 -0
- package/packages/cli/src/commands/setup.ts +63 -0
- package/packages/cli/src/commands/sheet.ts +114 -0
- package/packages/cli/src/commands/skill.ts +43 -0
- package/packages/cli/src/commands/still.ts +40 -0
- package/packages/cli/src/commands/theme.ts +85 -0
- package/packages/cli/src/commands/transitions.ts +40 -0
- package/packages/cli/src/deps.ts +122 -0
- package/packages/cli/src/ffmpeg.ts +81 -0
- package/packages/cli/src/index.ts +8 -0
- package/packages/cli/src/kf.ts +53 -0
- package/packages/cli/src/lock.ts +72 -0
- package/packages/cli/src/out.ts +13 -0
- package/packages/cli/src/serve.ts +124 -0
- package/packages/clock/package.json +14 -0
- package/packages/clock/src/index.ts +25 -0
- package/packages/clock/src/install.ts +351 -0
- package/packages/clock/src/types.ts +45 -0
- package/packages/core/package.json +9 -0
- package/packages/core/src/beats.ts +283 -0
- package/packages/core/src/ease.ts +87 -0
- package/packages/core/src/frame.ts +99 -0
- package/packages/core/src/index.ts +27 -0
- package/packages/core/src/project.ts +146 -0
- package/packages/core/src/rand.ts +56 -0
- package/packages/core/src/spring.ts +52 -0
- package/packages/core/src/style.ts +100 -0
- package/packages/core/src/time.ts +88 -0
- package/packages/gen/PROVIDERS.md +55 -0
- package/packages/gen/package.json +9 -0
- package/packages/gen/src/generate.ts +204 -0
- package/packages/gen/src/image/og.ts +133 -0
- package/packages/gen/src/index.ts +18 -0
- package/packages/gen/src/keys.ts +60 -0
- package/packages/gen/src/media.ts +150 -0
- package/packages/gen/src/plan.ts +106 -0
- package/packages/gen/src/registry.ts +51 -0
- package/packages/gen/src/seam.ts +103 -0
- package/packages/gen/src/types.ts +86 -0
- package/packages/gen/src/video/fal.ts +140 -0
- package/packages/gen/src/video/h3.ts +107 -0
- package/packages/gen/src/video/http.ts +44 -0
- package/packages/gen/src/video/ltx.ts +60 -0
- package/packages/gen/src/video/minimax.ts +55 -0
- package/packages/gen/src/video/veo.ts +71 -0
- package/packages/jizura/package.json +16 -0
- package/packages/jizura/src/deny.ts +73 -0
- package/packages/jizura/src/index.ts +7 -0
- package/packages/jizura/src/install.ts +226 -0
- package/packages/jizura/src/layer.ts +282 -0
- package/packages/jizura/src/lyrics.ts +81 -0
- package/packages/jizura/src/pin.ts +5 -0
- package/packages/jizura/src/vite.ts +36 -0
- package/packages/motion/README.md +142 -0
- package/packages/motion/package.json +29 -0
- package/packages/motion/src/camera.ts +97 -0
- package/packages/motion/src/captions.ts +479 -0
- package/packages/motion/src/enter.ts +128 -0
- package/packages/motion/src/hangul.ts +129 -0
- package/packages/motion/src/impact.ts +100 -0
- package/packages/motion/src/index.ts +17 -0
- package/packages/motion/src/moves/brushWipe.ts +90 -0
- package/packages/motion/src/moves/cloneArray.ts +111 -0
- package/packages/motion/src/moves/curtains.ts +81 -0
- package/packages/motion/src/moves/echoStack.ts +70 -0
- package/packages/motion/src/moves/fit.ts +34 -0
- package/packages/motion/src/moves/flood.ts +61 -0
- package/packages/motion/src/moves/ghostStack.ts +79 -0
- package/packages/motion/src/moves/gridFloor.ts +100 -0
- package/packages/motion/src/moves/imageSwarm.ts +109 -0
- package/packages/motion/src/moves/index.ts +19 -0
- package/packages/motion/src/moves/irisBlades.ts +113 -0
- package/packages/motion/src/moves/karaokeLine.ts +94 -0
- package/packages/motion/src/moves/ledBoard.ts +94 -0
- package/packages/motion/src/moves/liquidTab.ts +72 -0
- package/packages/motion/src/moves/markerWrite.ts +63 -0
- package/packages/motion/src/moves/morphBox.ts +96 -0
- package/packages/motion/src/moves/neonSign.ts +77 -0
- package/packages/motion/src/moves/panelPop.ts +78 -0
- package/packages/motion/src/moves/paperTear.ts +75 -0
- package/packages/motion/src/moves/path.ts +64 -0
- package/packages/motion/src/moves/pathText.ts +107 -0
- package/packages/motion/src/moves/pointSphere.ts +109 -0
- package/packages/motion/src/moves/ransomLetters.ts +73 -0
- package/packages/motion/src/moves/shapeMorph.ts +143 -0
- package/packages/motion/src/moves/slamWords.ts +89 -0
- package/packages/motion/src/moves/sparkTrail.ts +114 -0
- package/packages/motion/src/moves/speedLines.ts +91 -0
- package/packages/motion/src/moves/splitFlap.ts +147 -0
- package/packages/motion/src/moves/stepWords.ts +117 -0
- package/packages/motion/src/moves/strikeOut.ts +98 -0
- package/packages/motion/src/moves/sunburst.ts +58 -0
- package/packages/motion/src/moves/tickerRows.ts +87 -0
- package/packages/motion/src/moves/tokenPicker.ts +114 -0
- package/packages/motion/src/moves/truchet.ts +85 -0
- package/packages/motion/src/moves/wordWall.ts +76 -0
- package/packages/motion/src/moves/worldRoll.ts +31 -0
- package/packages/motion/src/particles.ts +102 -0
- package/packages/motion/src/react/index.tsx +21 -0
- package/packages/motion/src/react/parts.tsx +431 -0
- package/packages/motion/src/react/time.tsx +97 -0
- package/packages/motion/src/text.ts +103 -0
- package/packages/motion/src/transition.ts +82 -0
- package/packages/motion/src/transition3d/cards.ts +65 -0
- package/packages/motion/src/transition3d/chain.ts +84 -0
- package/packages/motion/src/transition3d/dive.ts +181 -0
- package/packages/motion/src/transition3d/fold.ts +110 -0
- package/packages/motion/src/transition3d/gl.ts +219 -0
- package/packages/motion/src/transition3d/index.ts +228 -0
- package/packages/motion/src/transition3d/live.ts +70 -0
- package/packages/motion/src/transition3d/portal.ts +115 -0
- package/packages/motion/src/transition3d/react.tsx +29 -0
- package/packages/motion/src/transition3d/rig.ts +502 -0
- package/packages/motion/src/transition3d/shatter.ts +101 -0
- package/packages/motion/src/transition3d/whip.ts +65 -0
- package/packages/motion/src/transition3d/world.ts +683 -0
- package/packages/motion/src/transition3d/zoomOut.ts +114 -0
- package/packages/motion/src/transitions2d/catalog.ts +127 -0
- package/packages/motion/src/transitions2d/css.d.ts +2 -0
- package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
- package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
- package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
- package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
- package/packages/motion/src/transitions2d/index.ts +2 -0
- package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
- package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
- package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
- package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
- package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
- package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
- package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
- package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
- package/packages/motion/src/transitions2d/react.ts +13 -0
- package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
- package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
- package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
- package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
- package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
- package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
- package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
- package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
- package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
- package/packages/motion/src/type.ts +131 -0
- package/packages/motion/src/ui.ts +172 -0
- package/packages/plan/package.json +15 -0
- package/packages/plan/src/brief.ts +254 -0
- package/packages/plan/src/captions.ts +75 -0
- package/packages/plan/src/detect.ts +93 -0
- package/packages/plan/src/index.ts +23 -0
- package/packages/plan/src/layout.ts +176 -0
- package/packages/plan/src/llm.ts +110 -0
- package/packages/plan/src/look.ts +220 -0
- package/packages/plan/src/plan.ts +125 -0
- package/packages/plan/src/request.ts +55 -0
- package/packages/plan/src/studio.ts +53 -0
- package/packages/plan/src/templates.ts +42 -0
- package/packages/plan/src/themes.ts +48 -0
- package/packages/plan/src/types.ts +106 -0
- package/packages/studio/package.json +12 -0
- package/packages/studio/src/index.ts +1 -0
- package/packages/studio/src/plugin.ts +236 -0
- package/packages/studio/src/ui/main.ts +503 -0
- package/packages/studio/src/ui/studio.css +165 -0
- package/packages/studio/src/ui/timeline.ts +317 -0
- package/packages/themes/kit/THEME-MAKE.md +68 -0
- package/packages/themes/kit/THEME.md +30 -0
- package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
- package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
- package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
- package/packages/themes/kit/effects.ts +5 -0
- package/packages/themes/kit/sample/index.html +11 -0
- package/packages/themes/kit/sample/main.ts +9 -0
- package/packages/themes/kit/sample/scenes/scene.ts +15 -0
- package/packages/themes/package.json +16 -0
- package/packages/themes/src/factory.ts +296 -0
- package/packages/themes/src/fx.ts +14 -0
- package/packages/themes/src/index.ts +105 -0
- package/packages/themes/src/stage3d.ts +177 -0
- package/packages/themes/src/types.ts +58 -0
- package/presets/kawaii-pastel.json +94 -0
- package/presets/neutral-dark.json +82 -0
- package/presets/og.json +98 -0
- package/skills/kasaframe/SKILL.md +32 -0
- package/templates/_shared/project/index.html +11 -0
- package/templates/_shared/project/lib/react.ts +22 -0
- package/templates/_shared/project/lib/stage.ts +272 -0
- package/templates/_shared/project/main.ts +9 -0
- package/templates/_shared/scenes/default.ts +18 -0
- package/templates/_shared/scenes/drop.ts +30 -0
- package/templates/_shared/scenes/end.ts +23 -0
- package/templates/_shared/scenes/hook.ts +16 -0
- package/templates/_shared/scenes/seam.ts +41 -0
- package/templates/_shared/scenes/step.ts +40 -0
- package/templates/ad/preview.png +0 -0
- package/templates/ad/template.json +132 -0
- package/templates/demo/preview.png +0 -0
- package/templates/demo/template.json +130 -0
- package/templates/explainer/preview.png +0 -0
- package/templates/explainer/template.json +142 -0
- package/templates/launch/preview.png +0 -0
- package/templates/launch/template.json +130 -0
- package/templates/mv/preview.png +0 -0
- package/templates/mv/scenes/default.ts +95 -0
- package/templates/mv/template.json +176 -0
- package/templates/promo/preview.png +0 -0
- package/templates/promo/template.json +144 -0
- package/templates/shorts/preview.png +0 -0
- package/templates/shorts/scenes/hook.ts +34 -0
- package/templates/shorts/template.json +127 -0
- package/templates/square/preview.png +0 -0
- package/templates/square/template.json +113 -0
- package/themes/debug-hud/THEME.md +39 -0
- package/themes/debug-hud/effects.ts +389 -0
- package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
- package/themes/debug-hud/sample/beats.json +1 -0
- package/themes/debug-hud/sample/index.html +11 -0
- package/themes/debug-hud/sample/kf.json +59 -0
- package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
- package/themes/debug-hud/sample/lib/og.ts +29 -0
- package/themes/debug-hud/sample/main.ts +9 -0
- package/themes/debug-hud/sample/scenes/end.ts +62 -0
- package/themes/debug-hud/sample/scenes/gather.ts +67 -0
- package/themes/debug-hud/sample/scenes/models.ts +70 -0
- package/themes/debug-hud/sample/scenes/price.ts +79 -0
- package/themes/debug-hud/sample/style.json +91 -0
- package/themes/debug-hud/still.png +0 -0
- package/themes/debug-hud/style.json +91 -0
- package/themes/debug-hud/theme.json +68 -0
- package/themes/og-measure/THEME.md +47 -0
- package/themes/og-measure/effects.ts +432 -0
- package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
- package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
- package/themes/og-measure/sample/assets/data/measure.json +353 -0
- package/themes/og-measure/sample/beats.json +1 -0
- package/themes/og-measure/sample/index.html +11 -0
- package/themes/og-measure/sample/kf.json +66 -0
- package/themes/og-measure/sample/lib/data.ts +62 -0
- package/themes/og-measure/sample/main.ts +9 -0
- package/themes/og-measure/sample/measure.ts +78 -0
- package/themes/og-measure/sample/scenes/end.ts +47 -0
- package/themes/og-measure/sample/scenes/gather.ts +51 -0
- package/themes/og-measure/sample/scenes/models.ts +34 -0
- package/themes/og-measure/sample/scenes/one.ts +39 -0
- package/themes/og-measure/sample/scenes/price.ts +56 -0
- package/themes/og-measure/sample/style.json +97 -0
- package/themes/og-measure/still.png +0 -0
- package/themes/og-measure/style.json +97 -0
- package/themes/og-measure/theme.json +79 -0
- package/themes/riso-zine/THEME.md +43 -0
- package/themes/riso-zine/effects.ts +321 -0
- package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
- package/themes/riso-zine/sample/beats.json +1 -0
- package/themes/riso-zine/sample/index.html +11 -0
- package/themes/riso-zine/sample/kf.json +59 -0
- package/themes/riso-zine/sample/lib/og.ts +29 -0
- package/themes/riso-zine/sample/lib/shot.ts +86 -0
- package/themes/riso-zine/sample/lib/zine.ts +42 -0
- package/themes/riso-zine/sample/main.ts +9 -0
- package/themes/riso-zine/sample/scenes/end.ts +61 -0
- package/themes/riso-zine/sample/scenes/gather.ts +62 -0
- package/themes/riso-zine/sample/scenes/models.ts +52 -0
- package/themes/riso-zine/sample/scenes/price.ts +82 -0
- package/themes/riso-zine/sample/style.json +99 -0
- package/themes/riso-zine/still.png +0 -0
- package/themes/riso-zine/style.json +99 -0
- package/themes/riso-zine/theme.json +75 -0
- package/tools/beats/beats.py +372 -0
- package/tools/beats/requirements.txt +3 -0
- package/tools/lyrics/align.py +164 -0
- package/tools/lyrics/requirements.txt +9 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { setFrameSample, setFrameTime, type FrameSample, type KfMeta, type KfMotionEvent } from "@kasaframe/core";
|
|
2
|
+
import type { KfClock } from "@kasaframe/clock/types";
|
|
3
|
+
import { syncCss } from "./css.ts";
|
|
4
|
+
import { syncMedia } from "./media.ts";
|
|
5
|
+
|
|
6
|
+
export type KfMode = "seek" | "step";
|
|
7
|
+
|
|
8
|
+
export interface KfMarker {
|
|
9
|
+
t: number;
|
|
10
|
+
label: string;
|
|
11
|
+
kind: "scene" | "beat" | "drop" | "section";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface KfPageMeta extends KfMeta {
|
|
15
|
+
// 프로젝트 기준 곡 경로. 미리보기는 이걸 기준 시계로, 렌더는 이걸 영상에 얹는다.
|
|
16
|
+
music?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface KfPage {
|
|
20
|
+
meta: KfPageMeta;
|
|
21
|
+
mode: KfMode;
|
|
22
|
+
ready(): Promise<void>;
|
|
23
|
+
// sample — 렌더의 모션 블러 하위 프레임(몇 번째·몇 장 중). 없으면 보통 프레임
|
|
24
|
+
seek(t: number, sample?: FrameSample): Promise<void>;
|
|
25
|
+
markers?: KfMarker[];
|
|
26
|
+
events?: KfMotionEvent[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PageOptions {
|
|
30
|
+
meta: KfPageMeta;
|
|
31
|
+
mode?: KfMode;
|
|
32
|
+
markers?: KfMarker[];
|
|
33
|
+
ready?(): Promise<void>;
|
|
34
|
+
render?(t: number): void | Promise<void>;
|
|
35
|
+
// React 는 flushSync 를 넘겨 setFrameTime 과 render 를 한 번에 커밋한다.
|
|
36
|
+
commit?(fn: () => void): void;
|
|
37
|
+
// <video> 를 시각에 묶을지. 기본 켬.
|
|
38
|
+
media?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare global {
|
|
42
|
+
interface Window {
|
|
43
|
+
__kf?: Partial<KfPage>;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Syncer = (t: number) => void | Promise<void>;
|
|
48
|
+
const syncers = new Set<Syncer>();
|
|
49
|
+
|
|
50
|
+
// lottie·gsap·three 처럼 시각을 직접 맞춰야 하는 것들이 여기 건다. seek 마다 순서대로 불린다.
|
|
51
|
+
export function onSeek(fn: Syncer): () => void {
|
|
52
|
+
syncers.add(fn);
|
|
53
|
+
return () => {
|
|
54
|
+
syncers.delete(fn);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function clock(): KfClock | undefined {
|
|
59
|
+
return window.__kfClock;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fontsSettled(): Promise<void> {
|
|
63
|
+
const realTimeout = clock()?.real.setTimeout ?? setTimeout;
|
|
64
|
+
return Promise.race([document.fonts.ready.then(() => {}), new Promise<void>((r) => realTimeout(r, 10_000))]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// unicode-range 로 쪼갠 글꼴(구글 폰트)은 레이아웃이 그 글자를 만나야 조각을 받기 시작한다.
|
|
68
|
+
// 레이아웃을 먼저 강제해 요청을 내보낸 뒤 받을 때까지 기다린다 — font-display:block 이면 그동안 글자가 투명하다.
|
|
69
|
+
async function fontsForFrame(): Promise<void> {
|
|
70
|
+
for (let i = 0; i < 3; i++) {
|
|
71
|
+
void document.documentElement.getBoundingClientRect();
|
|
72
|
+
if (document.fonts.status !== "loading") return;
|
|
73
|
+
await fontsSettled();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function nextPaint(): Promise<void> {
|
|
78
|
+
return new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const IMAGE_PROPS = ["backgroundImage", "maskImage", "borderImageSource", "listStyleImage"] as const;
|
|
82
|
+
const URL_RE = /url\(\s*(['"]?)(.*?)\1\s*\)/g;
|
|
83
|
+
|
|
84
|
+
function grabUrls(value: string | undefined, base: string, into: Set<string>) {
|
|
85
|
+
if (!value || !value.includes("url(")) return;
|
|
86
|
+
for (const m of value.matchAll(URL_RE)) {
|
|
87
|
+
if (m[2] && !m[2].startsWith("data:")) into.add(new URL(m[2], base).href);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// CSS 로 깐 그림(background-image 등)은 <img> 와 달리 기다릴 손잡이가 없다. url() 을 모아 미리 받아 두면 첫 래스터에 들어간다.
|
|
92
|
+
function cssImageUrls(elements: Iterable<Element>, sheets: boolean): Set<string> {
|
|
93
|
+
const urls = new Set<string>();
|
|
94
|
+
for (const el of elements) {
|
|
95
|
+
if (!el.isConnected) continue;
|
|
96
|
+
const cs = getComputedStyle(el);
|
|
97
|
+
for (const p of IMAGE_PROPS) grabUrls(cs[p], document.baseURI, urls);
|
|
98
|
+
}
|
|
99
|
+
if (sheets) {
|
|
100
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
101
|
+
let rules: CSSRuleList;
|
|
102
|
+
try {
|
|
103
|
+
rules = sheet.cssRules;
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const r of Array.from(rules)) {
|
|
108
|
+
if (r instanceof CSSStyleRule) for (const p of IMAGE_PROPS) grabUrls(r.style[p], sheet.href ?? document.baseURI, urls);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return urls;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const preloaded = new Set<string>();
|
|
116
|
+
|
|
117
|
+
function preload(urls: Iterable<string>): Promise<unknown> {
|
|
118
|
+
const realTimeout = clock()?.real.setTimeout ?? setTimeout;
|
|
119
|
+
const jobs: Promise<unknown>[] = [];
|
|
120
|
+
for (const src of urls) {
|
|
121
|
+
if (preloaded.has(src)) continue;
|
|
122
|
+
preloaded.add(src);
|
|
123
|
+
const img = new Image();
|
|
124
|
+
img.src = src;
|
|
125
|
+
jobs.push(img.decode().catch(() => console.warn(`[kf] 그림을 못 받았다: ${src}`)));
|
|
126
|
+
}
|
|
127
|
+
if (!jobs.length) return Promise.resolve();
|
|
128
|
+
return Promise.race([Promise.allSettled(jobs), new Promise((r) => realTimeout(r, 15_000))]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 장면이 처음 그려질 때(React 장면 등) 생기는 배경 그림도 잡도록, 바뀐 요소만 골라 본다.
|
|
132
|
+
const touched = new Set<Element>();
|
|
133
|
+
function watchTouched() {
|
|
134
|
+
new MutationObserver((records) => {
|
|
135
|
+
for (const r of records) {
|
|
136
|
+
if (r.type === "attributes" && r.target instanceof Element) touched.add(r.target);
|
|
137
|
+
for (const n of Array.from(r.addedNodes)) if (n instanceof Element) touched.add(n);
|
|
138
|
+
}
|
|
139
|
+
}).observe(document, { subtree: true, childList: true, attributes: true, attributeFilter: ["style", "class"] });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function imagesForFrame(): Promise<unknown> {
|
|
143
|
+
if (!touched.size) return Promise.resolve();
|
|
144
|
+
const els: Element[] = [];
|
|
145
|
+
for (const el of touched) els.push(el, ...Array.from(el.querySelectorAll("*")));
|
|
146
|
+
touched.clear();
|
|
147
|
+
return preload(cssImageUrls(els, false));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function assetsReady(): Promise<void> {
|
|
151
|
+
await document.fonts.ready;
|
|
152
|
+
const realTimeout = clock()?.real.setTimeout ?? setTimeout;
|
|
153
|
+
const imgs = Array.from(document.images).filter((i) => i.src && !i.complete);
|
|
154
|
+
await Promise.race([
|
|
155
|
+
Promise.allSettled([...imgs.map((i) => i.decode()), preload(cssImageUrls(document.querySelectorAll("*"), true))]),
|
|
156
|
+
new Promise((r) => realTimeout(r, 15_000)),
|
|
157
|
+
]);
|
|
158
|
+
touched.clear();
|
|
159
|
+
const media = Array.from(document.querySelectorAll("video"));
|
|
160
|
+
await Promise.allSettled(
|
|
161
|
+
media.map(
|
|
162
|
+
(v) =>
|
|
163
|
+
new Promise<void>((resolve) => {
|
|
164
|
+
// 오류가 리스너를 걸기 전에 이미 났을 수 있다(없는 파일). 그땐 기다릴 것이 없다.
|
|
165
|
+
if (v.readyState >= 2 || v.error || v.networkState === HTMLMediaElement.NETWORK_NO_SOURCE) return resolve();
|
|
166
|
+
v.addEventListener("loadeddata", () => resolve(), { once: true });
|
|
167
|
+
v.addEventListener("error", () => resolve(), { once: true });
|
|
168
|
+
realTimeout(resolve, 15_000);
|
|
169
|
+
}),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
for (const v of media) {
|
|
173
|
+
if (v.readyState < 2) console.warn(`[kf] 영상을 못 받았다(빈 칸으로 찍힌다): ${v.currentSrc || v.src}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function definePage(o: PageOptions): KfPage {
|
|
178
|
+
const mode = o.mode ?? "seek";
|
|
179
|
+
const c0 = clock();
|
|
180
|
+
if (c0) {
|
|
181
|
+
c0.mode = mode;
|
|
182
|
+
c0.fps = o.meta.fps;
|
|
183
|
+
}
|
|
184
|
+
const commit = o.commit ?? ((fn: () => void) => fn());
|
|
185
|
+
watchTouched();
|
|
186
|
+
const page: KfPage = {
|
|
187
|
+
meta: o.meta,
|
|
188
|
+
mode,
|
|
189
|
+
markers: o.markers ?? [],
|
|
190
|
+
async ready() {
|
|
191
|
+
await o.ready?.();
|
|
192
|
+
await assetsReady();
|
|
193
|
+
await fontsForFrame();
|
|
194
|
+
},
|
|
195
|
+
async seek(t: number, sample?: FrameSample) {
|
|
196
|
+
setFrameSample(sample?.index ?? 0, sample?.count ?? 1);
|
|
197
|
+
const c = clock();
|
|
198
|
+
const draw = async (at: number) => {
|
|
199
|
+
let pending: void | Promise<void> = undefined;
|
|
200
|
+
commit(() => {
|
|
201
|
+
setFrameTime(at);
|
|
202
|
+
pending = o.render?.(at);
|
|
203
|
+
});
|
|
204
|
+
await pending;
|
|
205
|
+
};
|
|
206
|
+
if (c && mode === "step") {
|
|
207
|
+
// 자기 시계로 도는 컴포넌트가 중간 프레임의 props 변화도 겪어야 해서, 건너뛰는 프레임마다 그린다.
|
|
208
|
+
let drawn = -1;
|
|
209
|
+
await c.advanceTo(t * 1000, (ms) => draw((drawn = Math.round((ms / 1000) * o.meta.fps) / o.meta.fps)));
|
|
210
|
+
if (drawn !== t) await draw(t);
|
|
211
|
+
} else {
|
|
212
|
+
if (c) await c.jumpTo(t * 1000);
|
|
213
|
+
await draw(t);
|
|
214
|
+
}
|
|
215
|
+
if (o.media !== false) await syncMedia(t);
|
|
216
|
+
for (const fn of syncers) await fn(t);
|
|
217
|
+
await imagesForFrame();
|
|
218
|
+
if (c) await c.settle();
|
|
219
|
+
else {
|
|
220
|
+
if (mode === "seek") syncCss(t);
|
|
221
|
+
await nextPaint();
|
|
222
|
+
}
|
|
223
|
+
await fontsForFrame();
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
// 움직임 부품이 먼저 쌓아 둔 events 를 잃지 않게 덮어쓰지 않고 채운다.
|
|
227
|
+
window.__kf = Object.assign(window.__kf ?? {}, page);
|
|
228
|
+
return window.__kf as KfPage;
|
|
229
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBeats,
|
|
3
|
+
cssVars,
|
|
4
|
+
fontCss,
|
|
5
|
+
setBeats,
|
|
6
|
+
silentBeats,
|
|
7
|
+
type BeatsJson,
|
|
8
|
+
type KfStyle,
|
|
9
|
+
type SafeArea,
|
|
10
|
+
type SceneCtx,
|
|
11
|
+
} from "@kasaframe/core";
|
|
12
|
+
import { definePage, type KfMarker, type KfMode, type KfPage } from "./page.ts";
|
|
13
|
+
|
|
14
|
+
export interface KfSceneEntry {
|
|
15
|
+
id: string;
|
|
16
|
+
from: number;
|
|
17
|
+
dur: number;
|
|
18
|
+
file?: string;
|
|
19
|
+
role?: string;
|
|
20
|
+
idea?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// kf.json 중 엔진이 읽는 부분. 나머지 키는 그대로 둔다.
|
|
24
|
+
export interface KfProjectJson {
|
|
25
|
+
fps: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
duration: number;
|
|
29
|
+
mode?: KfMode;
|
|
30
|
+
music?: string;
|
|
31
|
+
safeArea?: SafeArea;
|
|
32
|
+
scenes: KfSceneEntry[];
|
|
33
|
+
[k: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type SceneDraw = (t: number) => void;
|
|
37
|
+
|
|
38
|
+
export interface SceneModule {
|
|
39
|
+
build?(root: HTMLElement, ctx: SceneCtx): SceneDraw | void;
|
|
40
|
+
react?: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MountOptions {
|
|
44
|
+
kf: KfProjectJson;
|
|
45
|
+
// import.meta.glob("./scenes/*.ts", { eager: true }) 그대로, 또는 { [id]: 모듈 }
|
|
46
|
+
scenes: Record<string, unknown>;
|
|
47
|
+
beats?: BeatsJson | string | null;
|
|
48
|
+
style?: KfStyle | string | null;
|
|
49
|
+
stage: HTMLElement;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface LiveScene {
|
|
53
|
+
entry: KfSceneEntry;
|
|
54
|
+
root: HTMLElement;
|
|
55
|
+
draw: SceneDraw;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ZERO: SafeArea = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
59
|
+
|
|
60
|
+
async function load<T>(v: T | string | null | undefined): Promise<T | null> {
|
|
61
|
+
if (v == null) return null;
|
|
62
|
+
if (typeof v !== "string") return v;
|
|
63
|
+
const res = await fetch(v);
|
|
64
|
+
if (!res.ok) return null;
|
|
65
|
+
return (await res.json()) as T;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function baseName(p: string): string {
|
|
69
|
+
return p.split("/").pop()!.replace(/\.[^.]+$/, "");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findModule(scenes: Record<string, unknown>, entry: KfSceneEntry): SceneModule {
|
|
73
|
+
const file = entry.file?.replace(/^\.\//, "");
|
|
74
|
+
for (const [key, mod] of Object.entries(scenes)) {
|
|
75
|
+
const k = key.replace(/^\.\//, "");
|
|
76
|
+
if (k === entry.id || (file && (k === file || k.endsWith(`/${file}`))) || baseName(k) === entry.id) {
|
|
77
|
+
const m = mod as { default?: SceneModule } & SceneModule;
|
|
78
|
+
return m.default ?? m;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`장면 "${entry.id}" 의 파일을 못 찾았다 (${entry.file ?? `scenes/${entry.id}.ts`})`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assetUrl(path: string): string {
|
|
85
|
+
const clean = path.replace(/^\.?\/+/, "").replace(/^assets\//, "");
|
|
86
|
+
return new URL(`assets/${clean}`, document.baseURI).href;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function loadSheet(href: string): Promise<void> {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const link = document.createElement("link");
|
|
92
|
+
link.rel = "stylesheet";
|
|
93
|
+
link.href = href;
|
|
94
|
+
link.onload = link.onerror = () => resolve();
|
|
95
|
+
document.head.prepend(link);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// font-display:block 이라 받는 동안 글자가 투명하다. 첫 레이아웃 전의 fonts.ready 는 아직 부르지도 않은 글꼴을 안 기다리므로,
|
|
100
|
+
// @import 대신 <link> 로 CSS 를 받아 끝을 확인하고, 쓰는 글꼴 스택마다 직접 load 를 건다.
|
|
101
|
+
async function injectStyle(style: KfStyle | null, kf: KfProjectJson) {
|
|
102
|
+
const el = document.createElement("style");
|
|
103
|
+
el.dataset.kf = "project";
|
|
104
|
+
el.textContent = [
|
|
105
|
+
"html, body { margin: 0; padding: 0; background: #000; overflow: hidden; }",
|
|
106
|
+
// 장면 루트의 display 는 장면 몫이라 켜고 끄기는 hidden 으로 한다.
|
|
107
|
+
"[data-kf-scene][hidden] { display: none !important; }",
|
|
108
|
+
style ? fontCss(style).split("\n").filter((l) => !l.startsWith("@import")).join("\n") : "",
|
|
109
|
+
style ? cssVars(style, kf) : "",
|
|
110
|
+
].join("\n");
|
|
111
|
+
document.head.prepend(el);
|
|
112
|
+
if (!style) return;
|
|
113
|
+
await Promise.all((style.fonts.css ?? []).map(loadSheet));
|
|
114
|
+
const weights = [...new Set([400, style.type.weightBody, style.type.weightDisplay])];
|
|
115
|
+
const stacks = [style.fonts.sans, style.fonts.display, style.fonts.mono];
|
|
116
|
+
await Promise.allSettled(
|
|
117
|
+
stacks.flatMap((stack) => weights.map((w) => document.fonts.load(`${w} 1em ${stack}`, "가A1"))),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function projectMarkers(kf: KfProjectJson, beats: BeatsJson | null): KfMarker[] {
|
|
122
|
+
const out: KfMarker[] = kf.scenes.map((s) => ({ t: s.from, label: s.id, kind: "scene" }));
|
|
123
|
+
if (beats) {
|
|
124
|
+
for (const t of beats.beats ?? []) out.push({ t, label: "beat", kind: "beat" });
|
|
125
|
+
(beats.drops ?? []).forEach((t, i) => out.push({ t, label: `drop ${i}`, kind: "drop" }));
|
|
126
|
+
for (const s of beats.sections ?? []) out.push({ t: s.t, label: s.kind, kind: "section" });
|
|
127
|
+
}
|
|
128
|
+
return out.sort((a, b) => a.t - b.t);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function mountProject(o: MountOptions): KfPage {
|
|
132
|
+
const { kf, stage } = o;
|
|
133
|
+
const live: LiveScene[] = [];
|
|
134
|
+
let captions: SceneDraw | null = null;
|
|
135
|
+
let built: Promise<void> | null = null;
|
|
136
|
+
let markers: KfMarker[] = projectMarkers(kf, typeof o.beats === "object" ? o.beats : null);
|
|
137
|
+
|
|
138
|
+
Object.assign(stage.style, {
|
|
139
|
+
position: "relative",
|
|
140
|
+
width: `${kf.width}px`,
|
|
141
|
+
height: `${kf.height}px`,
|
|
142
|
+
overflow: "hidden",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
async function build() {
|
|
146
|
+
const [beats, style] = await Promise.all([load<BeatsJson>(o.beats), load<KfStyle>(o.style)]);
|
|
147
|
+
if (beats) setBeats(beats);
|
|
148
|
+
markers = projectMarkers(kf, beats);
|
|
149
|
+
page.markers = markers;
|
|
150
|
+
await injectStyle(style, kf);
|
|
151
|
+
const meta = { fps: kf.fps, width: kf.width, height: kf.height, duration: kf.duration };
|
|
152
|
+
// 자막을 켜면 장면을 짓기 전에 안전 영역 아래쪽 자막 띠를 뺀다 — 주소 ?captions=on|off|fit 가 kf.json 을 그 판만 덮는다(kf render --captions)
|
|
153
|
+
const { captionsPlan } = await import("@kasaframe/motion/captions");
|
|
154
|
+
const caps = await captionsPlan({ kf: kf as Parameters<typeof captionsPlan>[0]["kf"], mode: new URLSearchParams(location.search).get("captions") });
|
|
155
|
+
for (const entry of kf.scenes) {
|
|
156
|
+
const mod = findModule(o.scenes, entry);
|
|
157
|
+
const root = document.createElement("div");
|
|
158
|
+
root.dataset.kfScene = entry.id;
|
|
159
|
+
root.style.cssText = "position:absolute;inset:0;overflow:hidden;";
|
|
160
|
+
root.hidden = true;
|
|
161
|
+
stage.appendChild(root);
|
|
162
|
+
const ctx: SceneCtx = {
|
|
163
|
+
id: entry.id,
|
|
164
|
+
from: entry.from,
|
|
165
|
+
dur: entry.dur,
|
|
166
|
+
meta,
|
|
167
|
+
beats: beats ? createBeats(beats, entry.from) : silentBeats(entry.dur),
|
|
168
|
+
style: style as KfStyle,
|
|
169
|
+
safe: caps?.safe ?? kf.safeArea ?? ZERO,
|
|
170
|
+
asset: assetUrl,
|
|
171
|
+
};
|
|
172
|
+
let draw: SceneDraw;
|
|
173
|
+
if (mod.react) {
|
|
174
|
+
const { reactScene } = await import("./react.ts");
|
|
175
|
+
draw = reactScene(root, mod.react, ctx);
|
|
176
|
+
} else if (mod.build) {
|
|
177
|
+
draw = mod.build(root, ctx) ?? (() => {});
|
|
178
|
+
} else {
|
|
179
|
+
throw new Error(`장면 "${entry.id}" 가 build 도 react 도 내보내지 않는다`);
|
|
180
|
+
}
|
|
181
|
+
live.push({ entry, root, draw });
|
|
182
|
+
}
|
|
183
|
+
// 자막 판은 장면들 위 한 겹 — 장면 파일은 그대로다
|
|
184
|
+
captions = (await caps?.mount(stage, style)) ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const last = kf.scenes[kf.scenes.length - 1];
|
|
188
|
+
function render(t: number) {
|
|
189
|
+
for (const s of live) {
|
|
190
|
+
const { from, dur } = s.entry;
|
|
191
|
+
// 마지막 장면은 끝 시각까지 붙든다 — duration 정각을 찍어도 빈 화면이 안 나오게.
|
|
192
|
+
const on = t >= from && (t < from + dur || (s.entry === last && t <= from + dur + 1e-6));
|
|
193
|
+
if (s.root.hidden === on) s.root.hidden = !on;
|
|
194
|
+
if (on) s.draw(t - from);
|
|
195
|
+
}
|
|
196
|
+
captions?.(t);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const page = definePage({
|
|
200
|
+
meta: {
|
|
201
|
+
fps: kf.fps,
|
|
202
|
+
width: kf.width,
|
|
203
|
+
height: kf.height,
|
|
204
|
+
duration: kf.duration,
|
|
205
|
+
...(kf.music ? { music: kf.music } : {}),
|
|
206
|
+
},
|
|
207
|
+
mode: kf.mode ?? "seek",
|
|
208
|
+
markers,
|
|
209
|
+
ready: () => (built ??= build()),
|
|
210
|
+
render,
|
|
211
|
+
});
|
|
212
|
+
built = build();
|
|
213
|
+
return page;
|
|
214
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createElement, type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import { flushSync } from "react-dom";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import type { SceneCtx } from "@kasaframe/core";
|
|
5
|
+
import { definePage, type KfPage, type PageOptions } from "./page.ts";
|
|
6
|
+
|
|
7
|
+
export interface SceneProps {
|
|
8
|
+
t: number;
|
|
9
|
+
ctx: SceneCtx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// mountProject 가 React 장면을 붙일 때 쓴다. seek 마다 동기로 커밋해야 찍을 때 화면이 맞다.
|
|
13
|
+
export function reactScene(root: HTMLElement, Comp: unknown, ctx: SceneCtx): (t: number) => void {
|
|
14
|
+
const r = createRoot(root);
|
|
15
|
+
const C = Comp as ComponentType<SceneProps>;
|
|
16
|
+
return (t) => flushSync(() => r.render(createElement(C, { t, ctx })));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 장면 시간표 없이 React 트리 하나를 영상으로 쓸 때. 자기 시계를 쓰는 컴포넌트면 mode: "step".
|
|
20
|
+
export function renderReact(
|
|
21
|
+
node: ReactNode | ComponentType<{ t: number }>,
|
|
22
|
+
o: Omit<PageOptions, "render" | "commit"> & { container: HTMLElement },
|
|
23
|
+
): KfPage {
|
|
24
|
+
const r = createRoot(o.container);
|
|
25
|
+
const isComponent = typeof node === "function";
|
|
26
|
+
let mounted = false;
|
|
27
|
+
return definePage({
|
|
28
|
+
...o,
|
|
29
|
+
commit: (fn) => flushSync(fn),
|
|
30
|
+
render(t) {
|
|
31
|
+
if (isComponent) r.render(createElement(node as ComponentType<{ t: number }>, { t }));
|
|
32
|
+
else if (!mounted) r.render(node as ReactNode);
|
|
33
|
+
mounted = true;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { flushSync, mount, type Component } from "svelte";
|
|
2
|
+
import { readable } from "svelte/store";
|
|
3
|
+
import { frameTime, onFrame } from "@kasaframe/core";
|
|
4
|
+
import { definePage, type KfPage, type PageOptions } from "./page.ts";
|
|
5
|
+
|
|
6
|
+
// 컴포넌트에서 $time 으로 영상 시각(초)을 읽는다. 프로젝트 vite.config 에 @sveltejs/vite-plugin-svelte 가 있어야 .svelte 가 묶인다.
|
|
7
|
+
export const time = readable(frameTime(), (set) => onFrame(set));
|
|
8
|
+
|
|
9
|
+
export function renderSvelte(
|
|
10
|
+
root: Component<Record<string, unknown>>,
|
|
11
|
+
o: Omit<PageOptions, "render" | "commit"> & { container: HTMLElement; props?: Record<string, unknown> },
|
|
12
|
+
): KfPage {
|
|
13
|
+
mount(root, { target: o.container, props: o.props ?? {} });
|
|
14
|
+
return definePage({
|
|
15
|
+
...o,
|
|
16
|
+
// 스토어 갱신은 다음 마이크로태스크에 반영되므로 setFrameTime 직후 동기로 밀어 넣는다.
|
|
17
|
+
commit(fn) {
|
|
18
|
+
fn();
|
|
19
|
+
flushSync();
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { onSeek } from "./page.ts";
|
|
2
|
+
|
|
3
|
+
interface RendererLike {
|
|
4
|
+
render(scene: unknown, camera: unknown): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// three.js 장면을 seek 마다 t 로 갱신하고 한 번 그린다. setAnimationLoop 대신 이걸 쓴다.
|
|
8
|
+
export function bindThree<S, C>(o: {
|
|
9
|
+
renderer: RendererLike;
|
|
10
|
+
scene: S;
|
|
11
|
+
camera: C;
|
|
12
|
+
update?(t: number, scene: S, camera: C): void;
|
|
13
|
+
}): () => void {
|
|
14
|
+
return onSeek((t) => {
|
|
15
|
+
o.update?.(t, o.scene, o.camera);
|
|
16
|
+
o.renderer.render(o.scene, o.camera);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createApp, h, nextTick, onBeforeUnmount, shallowRef, type Component, type ShallowRef } from "vue";
|
|
2
|
+
import { frameTime, onFrame } from "@kasaframe/core";
|
|
3
|
+
import { definePage, type KfPage, type PageOptions } from "./page.ts";
|
|
4
|
+
|
|
5
|
+
// 컴포넌트 안에서 영상 시각(초)을 읽는다. 다른 부품들과 같은 core 시각 저장소를 본다.
|
|
6
|
+
export function useTime(): ShallowRef<number> {
|
|
7
|
+
const t = shallowRef(frameTime());
|
|
8
|
+
const off = onFrame((v) => (t.value = v));
|
|
9
|
+
onBeforeUnmount(off);
|
|
10
|
+
return t;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 루트 컴포넌트는 props 로 t 를 받는다. Vue 는 갱신을 마이크로태스크로 미루므로 nextTick 까지 기다려 찍는다.
|
|
14
|
+
export function renderVue(
|
|
15
|
+
root: Component,
|
|
16
|
+
o: Omit<PageOptions, "render" | "commit"> & { container: HTMLElement },
|
|
17
|
+
): KfPage {
|
|
18
|
+
const t = shallowRef(0);
|
|
19
|
+
createApp({ render: () => h(root, { t: t.value }) }).mount(o.container);
|
|
20
|
+
return definePage({
|
|
21
|
+
...o,
|
|
22
|
+
async render(at) {
|
|
23
|
+
t.value = at;
|
|
24
|
+
await nextTick();
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kasaframe/check",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/pngjs": "^6.0.5"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@kasaframe/cli": "*",
|
|
14
|
+
"@kasaframe/clock": "*",
|
|
15
|
+
"@kasaframe/core": "*",
|
|
16
|
+
"playwright": "^1.64.0",
|
|
17
|
+
"pngjs": "^7.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|