@luna-editor/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/Player.d.ts +3 -0
  2. package/dist/Player.js +336 -0
  3. package/dist/atoms/screen-size.d.ts +22 -0
  4. package/dist/atoms/screen-size.js +8 -0
  5. package/dist/components/BacklogUI.d.ts +2 -0
  6. package/dist/components/BacklogUI.js +115 -0
  7. package/dist/components/ConversationLogUI.d.ts +2 -0
  8. package/dist/components/ConversationLogUI.js +115 -0
  9. package/dist/components/DebugControls.d.ts +12 -0
  10. package/dist/components/DebugControls.js +5 -0
  11. package/dist/components/DialogueBox.d.ts +2 -0
  12. package/dist/components/DialogueBox.js +28 -0
  13. package/dist/components/EndScreen.d.ts +8 -0
  14. package/dist/components/EndScreen.js +5 -0
  15. package/dist/components/GameScreen.d.ts +9 -0
  16. package/dist/components/GameScreen.js +111 -0
  17. package/dist/components/OverlayUI.d.ts +13 -0
  18. package/dist/components/OverlayUI.js +21 -0
  19. package/dist/components/PluginComponentProvider.d.ts +14 -0
  20. package/dist/components/PluginComponentProvider.js +24 -0
  21. package/dist/constants/screen-size.d.ts +3 -0
  22. package/dist/constants/screen-size.js +6 -0
  23. package/dist/contexts/DataContext.d.ts +24 -0
  24. package/dist/contexts/DataContext.js +101 -0
  25. package/dist/hooks/useBacklog.d.ts +14 -0
  26. package/dist/hooks/useBacklog.js +82 -0
  27. package/dist/hooks/useConversationLog.d.ts +14 -0
  28. package/dist/hooks/useConversationLog.js +82 -0
  29. package/dist/hooks/usePlayerLogic.d.ts +21 -0
  30. package/dist/hooks/usePlayerLogic.js +145 -0
  31. package/dist/hooks/usePluginAPI.d.ts +19 -0
  32. package/dist/hooks/usePluginAPI.js +42 -0
  33. package/dist/hooks/usePluginEvents.d.ts +14 -0
  34. package/dist/hooks/usePluginEvents.js +197 -0
  35. package/dist/hooks/usePreloadImages.d.ts +2 -0
  36. package/dist/hooks/usePreloadImages.js +56 -0
  37. package/dist/hooks/useScreenSize.d.ts +89 -0
  38. package/dist/hooks/useScreenSize.js +87 -0
  39. package/dist/hooks/useTypewriter.d.ts +11 -0
  40. package/dist/hooks/useTypewriter.js +56 -0
  41. package/dist/hooks/useUIVisibility.d.ts +9 -0
  42. package/dist/hooks/useUIVisibility.js +19 -0
  43. package/dist/hooks/useVoice.d.ts +4 -0
  44. package/dist/hooks/useVoice.js +21 -0
  45. package/dist/index.d.ts +10 -0
  46. package/dist/index.js +9 -0
  47. package/dist/plugin/PluginManager.d.ts +108 -0
  48. package/dist/plugin/PluginManager.js +851 -0
  49. package/dist/plugin/luna-react.d.ts +41 -0
  50. package/dist/plugin/luna-react.js +99 -0
  51. package/dist/sdk.d.ts +512 -0
  52. package/dist/sdk.js +64 -0
  53. package/dist/types.d.ts +186 -0
  54. package/dist/types.js +2 -0
  55. package/dist/utils/attributeNormalizer.d.ts +5 -0
  56. package/dist/utils/attributeNormalizer.js +53 -0
  57. package/dist/utils/facePositionCalculator.d.ts +29 -0
  58. package/dist/utils/facePositionCalculator.js +127 -0
  59. package/package.json +55 -0
@@ -0,0 +1,145 @@
1
+ import { useCallback, useEffect, useMemo } from "react";
2
+ // ブロックインデックスから表示すべきキャラクター状態を計算する純粋関数
3
+ const calculateDisplayedCharacters = (blocks, currentIndex) => {
4
+ let characters = [];
5
+ // 現在位置から前方向に探索して、最後のcharacter_entranceまたはcharacter_exitを見つける
6
+ for (let i = currentIndex; i >= 0; i--) {
7
+ const block = blocks[i];
8
+ if (block.blockType === "character_entrance") {
9
+ // character_entranceを見つけたら、そこから現在位置までのキャラクター状態を構築
10
+ // character_entranceから現在位置までのブロックを処理
11
+ for (let j = i; j <= currentIndex; j++) {
12
+ const processBlock = blocks[j];
13
+ switch (processBlock.blockType) {
14
+ case "character_entrance":
15
+ if (processBlock.characters) {
16
+ const newCharacters = processBlock.characters.map((char) => ({
17
+ objectId: char.objectId,
18
+ entityStateId: char.entityStateId,
19
+ positionX: char.positionX,
20
+ positionY: char.positionY,
21
+ zIndex: char.zIndex,
22
+ scale: char.scale,
23
+ object: char.object,
24
+ entityState: char.entityState,
25
+ }));
26
+ characters = [
27
+ ...characters.filter((existing) => !newCharacters.some((newChar) => newChar.objectId === existing.objectId)),
28
+ ...newCharacters,
29
+ ];
30
+ }
31
+ break;
32
+ case "character_exit":
33
+ // character_exitに到達したら、キャラクター表示範囲外
34
+ return [];
35
+ case "dialogue":
36
+ if (processBlock.speakerId && processBlock.speakerStateId) {
37
+ characters = characters.map((char) => {
38
+ var _a, _b, _c, _d;
39
+ return char.objectId === processBlock.speakerId
40
+ ? Object.assign(Object.assign({}, char), { entityStateId: processBlock.speakerStateId || "", entityState: {
41
+ id: (_a = processBlock.speakerStateId) !== null && _a !== void 0 ? _a : "",
42
+ name: ((_b = processBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name) || "",
43
+ imageUrl: ((_c = processBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl) || null,
44
+ cropArea: ((_d = processBlock.speakerState) === null || _d === void 0 ? void 0 : _d.cropArea) || null,
45
+ } }) : char;
46
+ });
47
+ }
48
+ break;
49
+ }
50
+ }
51
+ break; // 最初のcharacter_entranceを見つけたら終了
52
+ }
53
+ if (block.blockType === "character_exit") {
54
+ // character_exitを見つけたら、キャラクター表示範囲外
55
+ return [];
56
+ }
57
+ }
58
+ return characters;
59
+ };
60
+ export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, }) => {
61
+ const isLastBlock = state.currentBlockIndex === scenario.blocks.length - 1;
62
+ // 現在のインデックスに基づいて表示すべきキャラクターを計算
63
+ const displayedCharacters = useMemo(() => calculateDisplayedCharacters(scenario.blocks, state.currentBlockIndex), [scenario.blocks, state.currentBlockIndex]);
64
+ const handleNext = useCallback(() => {
65
+ var _a;
66
+ // タイピング中の場合はスキップ
67
+ if (isTyping && currentBlock) {
68
+ skipTyping(currentBlock.content || "");
69
+ return;
70
+ }
71
+ if (isLastBlock) {
72
+ setState((prev) => (Object.assign(Object.assign({}, prev), { isEnded: true, isPlaying: false })));
73
+ onEnd === null || onEnd === void 0 ? void 0 : onEnd();
74
+ onScenarioEnd === null || onScenarioEnd === void 0 ? void 0 : onScenarioEnd();
75
+ // iframe向けのイベント発火
76
+ if (typeof window !== "undefined") {
77
+ window.postMessage({ type: "scenario-end" }, "*");
78
+ (_a = window.parent) === null || _a === void 0 ? void 0 : _a.postMessage({ type: "scenario-end" }, "*");
79
+ }
80
+ }
81
+ else {
82
+ setState((prev) => (Object.assign(Object.assign({}, prev), { currentBlockIndex: prev.currentBlockIndex + 1 })));
83
+ }
84
+ }, [
85
+ isLastBlock,
86
+ onEnd,
87
+ onScenarioEnd,
88
+ isTyping,
89
+ currentBlock,
90
+ skipTyping,
91
+ setState,
92
+ ]);
93
+ const handlePrevious = useCallback(() => {
94
+ if (state.currentBlockIndex > 0) {
95
+ setState((prev) => (Object.assign(Object.assign({}, prev), { currentBlockIndex: prev.currentBlockIndex - 1, isEnded: false })));
96
+ }
97
+ }, [state.currentBlockIndex, setState]);
98
+ const togglePlay = useCallback(() => {
99
+ if (state.isEnded)
100
+ return;
101
+ setState((prev) => (Object.assign(Object.assign({}, prev), { isPlaying: !prev.isPlaying })));
102
+ }, [state.isEnded, setState]);
103
+ const restart = useCallback(() => {
104
+ console.log("restart");
105
+ setState({
106
+ currentBlockIndex: 0,
107
+ isPlaying: autoplay,
108
+ isEnded: false,
109
+ });
110
+ }, [autoplay, setState]);
111
+ // キーボードナビゲーション
112
+ useEffect(() => {
113
+ const handleKeyDown = (event) => {
114
+ switch (event.key) {
115
+ case "ArrowRight":
116
+ case " ":
117
+ event.preventDefault();
118
+ handleNext();
119
+ break;
120
+ case "ArrowLeft":
121
+ event.preventDefault();
122
+ handlePrevious();
123
+ break;
124
+ case "Enter":
125
+ event.preventDefault();
126
+ togglePlay();
127
+ break;
128
+ case "Escape":
129
+ event.preventDefault();
130
+ restart();
131
+ break;
132
+ }
133
+ };
134
+ window.addEventListener("keydown", handleKeyDown);
135
+ return () => window.removeEventListener("keydown", handleKeyDown);
136
+ }, [handleNext, handlePrevious, togglePlay, restart]);
137
+ return {
138
+ handleNext,
139
+ handlePrevious,
140
+ togglePlay,
141
+ restart,
142
+ isLastBlock,
143
+ displayedCharacters,
144
+ };
145
+ };
@@ -0,0 +1,19 @@
1
+ import type { PluginManager } from "../plugin/PluginManager";
2
+ import type { ComponentType, UIAPI } from "../sdk";
3
+ /**
4
+ * グローバルなUIAPIとPluginManagerを設定(Player.tsxから呼ばれる)
5
+ */
6
+ export declare function setGlobalUIAPI(api: UIAPI, pluginManager: PluginManager): void;
7
+ /**
8
+ * プラグインコンポーネント内でUIAPIにアクセスするためのフック
9
+ * @returns UIAPI
10
+ */
11
+ export declare function usePluginAPI(): {
12
+ ui: UIAPI;
13
+ };
14
+ /**
15
+ * UI コンポーネントの表示状態を監視するフック(プラグインコンポーネント用)
16
+ * @param type - コンポーネントタイプ
17
+ * @returns 表示状態
18
+ */
19
+ export declare function useUIVisibility(type: ComponentType): boolean;
@@ -0,0 +1,42 @@
1
+ import { useEffect, useState } from "react";
2
+ /**
3
+ * グローバルなPluginAPIインスタンスを保持
4
+ * プラグインコンポーネントから呼び出された時に使用される
5
+ */
6
+ let globalUIAPI = null;
7
+ let globalPluginManager = null;
8
+ /**
9
+ * グローバルなUIAPIとPluginManagerを設定(Player.tsxから呼ばれる)
10
+ */
11
+ export function setGlobalUIAPI(api, pluginManager) {
12
+ globalUIAPI = api;
13
+ globalPluginManager = pluginManager;
14
+ }
15
+ /**
16
+ * プラグインコンポーネント内でUIAPIにアクセスするためのフック
17
+ * @returns UIAPI
18
+ */
19
+ export function usePluginAPI() {
20
+ if (!globalUIAPI) {
21
+ throw new Error("UIAPI is not initialized. Make sure this hook is used within a Luna Player context.");
22
+ }
23
+ return { ui: globalUIAPI };
24
+ }
25
+ /**
26
+ * UI コンポーネントの表示状態を監視するフック(プラグインコンポーネント用)
27
+ * @param type - コンポーネントタイプ
28
+ * @returns 表示状態
29
+ */
30
+ export function useUIVisibility(type) {
31
+ if (!globalPluginManager) {
32
+ throw new Error("PluginManager is not initialized. Make sure this hook is used within a Luna Player context.");
33
+ }
34
+ const pluginManager = globalPluginManager;
35
+ const [isVisible, setIsVisible] = useState(() => pluginManager.getUIVisibility(type));
36
+ useEffect(() => {
37
+ // UI 状態の変更を監視
38
+ const unsubscribe = pluginManager.subscribeUIVisibility(type, setIsVisible);
39
+ return unsubscribe;
40
+ }, [pluginManager, type]);
41
+ return isVisible;
42
+ }
@@ -0,0 +1,14 @@
1
+ import type { PluginManager } from "../plugin/PluginManager";
2
+ import type { DisplayedCharacter, ScenarioBlock } from "../types";
3
+ interface UsePluginEventsProps {
4
+ pluginManager: PluginManager;
5
+ currentBlock?: ScenarioBlock;
6
+ displayedCharacters: DisplayedCharacter[];
7
+ blockIndex: number;
8
+ isFirstRenderComplete: boolean;
9
+ allBlocks: ScenarioBlock[];
10
+ realBlockIndex: number;
11
+ pluginsLoaded: boolean;
12
+ }
13
+ export declare const usePluginEvents: ({ pluginManager, currentBlock, displayedCharacters, blockIndex, isFirstRenderComplete, allBlocks, realBlockIndex, pluginsLoaded, }: UsePluginEventsProps) => void;
14
+ export {};
@@ -0,0 +1,197 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { normalizeAttributes } from "../utils/attributeNormalizer";
3
+ import { calculateFacePosition } from "../utils/facePositionCalculator";
4
+ export const usePluginEvents = ({ pluginManager, currentBlock, displayedCharacters, blockIndex, isFirstRenderComplete, allBlocks, realBlockIndex, pluginsLoaded, }) => {
5
+ const previousSpeakerRef = useRef({});
6
+ const previousRealBlockIndexRef = useRef(-1);
7
+ const scenarioReadyCalledRef = useRef(false);
8
+ // シナリオ準備完了イベント(初回のみ、プラグイン読み込み完了後)
9
+ useEffect(() => {
10
+ var _a;
11
+ if (!isFirstRenderComplete ||
12
+ !currentBlock ||
13
+ !pluginsLoaded ||
14
+ scenarioReadyCalledRef.current) {
15
+ return;
16
+ }
17
+ pluginManager.callHook("onScenarioReady", {
18
+ scenario: {
19
+ id: ((_a = allBlocks[0]) === null || _a === void 0 ? void 0 : _a.scenarioId) || "unknown",
20
+ name: "scenario", // TODO: シナリオ名を適切に取得
21
+ },
22
+ allBlocks,
23
+ currentBlockIndex: blockIndex,
24
+ currentBlock,
25
+ });
26
+ scenarioReadyCalledRef.current = true;
27
+ }, [
28
+ pluginManager,
29
+ currentBlock,
30
+ allBlocks,
31
+ blockIndex,
32
+ isFirstRenderComplete,
33
+ pluginsLoaded,
34
+ ]);
35
+ // 通り過ぎたaction_nodeを実行
36
+ useEffect(() => {
37
+ if (!isFirstRenderComplete || !pluginsLoaded) {
38
+ return;
39
+ }
40
+ const previousIndex = previousRealBlockIndexRef.current;
41
+ const currentIndex = realBlockIndex;
42
+ // 前回から今回までに通り過ぎたブロックをチェック
43
+ // 初回の場合(previousIndex === -1)は0から処理、そうでなければpreviousIndex + 1から処理
44
+ if (currentIndex > previousIndex) {
45
+ const startIndex = previousIndex === -1 ? 0 : previousIndex + 1;
46
+ for (let i = startIndex; i <= currentIndex; i++) {
47
+ const block = allBlocks[i];
48
+ console.log("block", block);
49
+ if ((block === null || block === void 0 ? void 0 : block.blockType) === "action_node" && block.actionNode) {
50
+ // Normalize attributes from database format
51
+ const normalizedAttributes = normalizeAttributes(block);
52
+ console.log("Normalized attributes:", normalizedAttributes);
53
+ // Try multiple ways to find the ActionNode type - prioritize displayName matching
54
+ const possibleTypes = [
55
+ block.actionNode.nodeType,
56
+ block.actionNode.name, // This will be "背景" from database
57
+ ].filter(Boolean);
58
+ let actionNodeDef;
59
+ let foundActionNodeType;
60
+ // First try direct type matching
61
+ for (const actionNodeType of possibleTypes) {
62
+ if (actionNodeType) {
63
+ actionNodeDef =
64
+ pluginManager.getActionNodeDefinition(actionNodeType);
65
+ if (actionNodeDef) {
66
+ foundActionNodeType = actionNodeType;
67
+ console.log(`Found ActionNode definition with type: ${actionNodeType}`);
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ // If no direct match found, try displayName matching
73
+ if (!actionNodeDef && block.actionNode.name) {
74
+ actionNodeDef = pluginManager.getActionNodeDefinition(block.actionNode.name);
75
+ if (actionNodeDef) {
76
+ foundActionNodeType = block.actionNode.name;
77
+ console.log(`Found ActionNode definition with displayName: ${block.actionNode.name}`);
78
+ }
79
+ }
80
+ if (actionNodeDef === null || actionNodeDef === void 0 ? void 0 : actionNodeDef.execute) {
81
+ try {
82
+ // Create a clean context object that matches plugin expectations
83
+ // Note: DisplayedCharacter[] is used internally but needs to match ActionExecutionContext signature
84
+ actionNodeDef.execute({
85
+ attributes: normalizedAttributes,
86
+ currentBlock: block,
87
+ currentSpeaker: block.speaker,
88
+ displayedCharacters,
89
+ api: pluginManager,
90
+ });
91
+ console.log(`Successfully executed ActionNode: ${foundActionNodeType}`);
92
+ }
93
+ catch (error) {
94
+ console.error("Error executing ActionNode:", error);
95
+ }
96
+ }
97
+ else {
98
+ console.warn(`ActionNode definition not found for: ${block.actionNode.name} (tried types: ${possibleTypes.join(", ")})`);
99
+ }
100
+ // Legacy support: also call onActionExecute hook (for backward compatibility)
101
+ pluginManager.callHook("onActionExecute", {
102
+ attributes: normalizedAttributes,
103
+ currentBlock: block,
104
+ currentSpeaker: block.speaker,
105
+ displayedCharacters,
106
+ api: pluginManager,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ previousRealBlockIndexRef.current = currentIndex;
112
+ }, [
113
+ pluginManager,
114
+ realBlockIndex,
115
+ allBlocks,
116
+ displayedCharacters,
117
+ isFirstRenderComplete,
118
+ pluginsLoaded,
119
+ ]);
120
+ // ブロック変更イベント
121
+ useEffect(() => {
122
+ if (!isFirstRenderComplete || !currentBlock || !pluginsLoaded) {
123
+ return;
124
+ }
125
+ pluginManager.callHook("onBlockChange", {
126
+ currentBlock,
127
+ blockIndex,
128
+ });
129
+ }, [
130
+ pluginManager,
131
+ currentBlock,
132
+ blockIndex,
133
+ isFirstRenderComplete,
134
+ pluginsLoaded,
135
+ ]);
136
+ // キャラクター発話イベント
137
+ useEffect(() => {
138
+ var _a, _b;
139
+ if (!isFirstRenderComplete || !currentBlock || !pluginsLoaded) {
140
+ return;
141
+ }
142
+ // dialogueブロックの場合のみ処理
143
+ if (currentBlock.blockType === "dialogue" && currentBlock.speakerId) {
144
+ const currentSpeaker = {
145
+ id: currentBlock.speakerId,
146
+ name: ((_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name) || "",
147
+ };
148
+ // 前の話者と異なるかチェック
149
+ const isNewSpeaker = previousSpeakerRef.current.id !== currentSpeaker.id;
150
+ // 表示されているキャラクターから話者の位置を取得
151
+ const speakerCharacter = displayedCharacters.find((char) => char.objectId === currentSpeaker.id);
152
+ // キャラクター要素を取得
153
+ const characterElement = document.querySelector(`[data-character-id="${currentSpeaker.id}"]`);
154
+ // 顔位置を計算
155
+ let facePosition;
156
+ if (characterElement && currentBlock.speakerState) {
157
+ const calculatedPosition = calculateFacePosition(characterElement, {
158
+ cropArea: currentBlock.speakerState.cropArea,
159
+ scale: currentBlock.speakerState.scale,
160
+ translateX: currentBlock.speakerState.translateX,
161
+ translateY: currentBlock.speakerState.translateY,
162
+ });
163
+ if (calculatedPosition) {
164
+ facePosition = calculatedPosition;
165
+ }
166
+ }
167
+ // characterSpeakHandlerを呼び出し
168
+ pluginManager.callHook("characterSpeakHandler", {
169
+ speaker: {
170
+ id: currentSpeaker.id,
171
+ name: currentSpeaker.name,
172
+ state: (_b = currentBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name,
173
+ positionX: speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.positionX,
174
+ positionY: speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.positionY,
175
+ zIndex: speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.zIndex,
176
+ scale: speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.scale,
177
+ },
178
+ dialogue: {
179
+ content: currentBlock.content || "",
180
+ isNewSpeaker,
181
+ },
182
+ speakerElement: document.querySelector("[data-speaker-name-element]"),
183
+ characterElement,
184
+ previousSpeaker: isNewSpeaker ? previousSpeakerRef.current : undefined,
185
+ facePosition, // 顔位置情報を追加
186
+ });
187
+ // 現在の話者を記録
188
+ previousSpeakerRef.current = currentSpeaker;
189
+ }
190
+ }, [
191
+ pluginManager,
192
+ currentBlock,
193
+ displayedCharacters,
194
+ isFirstRenderComplete,
195
+ pluginsLoaded,
196
+ ]);
197
+ };
@@ -0,0 +1,2 @@
1
+ import type { PublishedScenario } from "../types";
2
+ export declare const usePreloadImages: (scenario: PublishedScenario) => boolean;
@@ -0,0 +1,56 @@
1
+ import { useEffect, useState } from "react";
2
+ export const usePreloadImages = (scenario) => {
3
+ const [isLoaded, setIsLoaded] = useState(false);
4
+ useEffect(() => {
5
+ const imageUrls = new Set();
6
+ // シナリオ全体から画像URLを収集
7
+ scenario.blocks.forEach((block) => {
8
+ var _a;
9
+ // dialogue/narrationブロックのspeaker画像
10
+ if ((_a = block.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl) {
11
+ imageUrls.add(block.speakerState.imageUrl);
12
+ }
13
+ // character_entranceブロックのキャラクター画像
14
+ if (block.characters) {
15
+ block.characters.forEach((char) => {
16
+ if (char.entityState.imageUrl) {
17
+ imageUrls.add(char.entityState.imageUrl);
18
+ }
19
+ });
20
+ }
21
+ });
22
+ // 画像がない場合は即座に完了
23
+ if (imageUrls.size === 0) {
24
+ setIsLoaded(true);
25
+ return;
26
+ }
27
+ let loadedCount = 0;
28
+ const totalCount = imageUrls.size;
29
+ // 各画像を事前読み込み
30
+ imageUrls.forEach((url) => {
31
+ const img = new Image();
32
+ const handleLoad = () => {
33
+ loadedCount++;
34
+ if (loadedCount === totalCount) {
35
+ setIsLoaded(true);
36
+ }
37
+ };
38
+ const handleError = () => {
39
+ console.error(`Failed to preload image: ${url}`);
40
+ loadedCount++;
41
+ if (loadedCount === totalCount) {
42
+ setIsLoaded(true);
43
+ }
44
+ };
45
+ img.addEventListener("load", handleLoad);
46
+ img.addEventListener("error", handleError);
47
+ img.src = url;
48
+ // クリーンアップ関数で画像を破棄
49
+ return () => {
50
+ img.removeEventListener("load", handleLoad);
51
+ img.removeEventListener("error", handleError);
52
+ };
53
+ });
54
+ }, [scenario]);
55
+ return isLoaded;
56
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Figmaの基準座標(1920×1080)から実際のピクセル座標への変換を提供するフック
3
+ */
4
+ export declare const useToPixel: () => {
5
+ /**
6
+ * 基準座標(px)を実際のピクセルに変換(高さ基準)
7
+ * @param basisX 基準座標での値
8
+ * @returns 実際のピクセル値
9
+ */
10
+ pixel: (basisX: number) => number;
11
+ /**
12
+ * 基準座標(px)を実際のピクセルに変換(幅基準)
13
+ * @param basisX 基準座標での値
14
+ * @returns 実際のピクセル値
15
+ */
16
+ pixelWidth: (basisX: number) => number;
17
+ /**
18
+ * アスペクト比を考慮したサイズを返す
19
+ * @param basisSize 基準サイズ
20
+ * @returns 幅と高さのピクセル値
21
+ */
22
+ aspectSize: (basisSize: number) => {
23
+ width: number;
24
+ height: number;
25
+ };
26
+ /**
27
+ * 2D座標を基準から実際のピクセルに変換
28
+ * @param basisPos 基準座標 {x, y}
29
+ * @returns 実際のピクセル座標
30
+ */
31
+ pos: (basisPos: {
32
+ x: number;
33
+ y: number;
34
+ }) => {
35
+ x: number;
36
+ y: number;
37
+ };
38
+ /**
39
+ * 3D座標を基準から実際のピクセルに変換
40
+ * @param basisPos 基準座標 {x, y, z}
41
+ * @returns 実際のピクセル座標
42
+ */
43
+ vec3d: (basisPos: {
44
+ x: number;
45
+ y: number;
46
+ z: number;
47
+ }) => {
48
+ x: number;
49
+ y: number;
50
+ z: number;
51
+ };
52
+ /**
53
+ * 相対幅(0-1の値)を実際のピクセル幅に変換
54
+ * @param basisWidth 基準幅(0-1)
55
+ * @returns 実際のピクセル幅
56
+ */
57
+ relativeWidth: (basisWidth: number) => number;
58
+ /**
59
+ * 相対高さ(0-1の値)を実際のピクセル高さに変換
60
+ * @param basisHeight 基準高さ(0-1)
61
+ * @returns 実際のピクセル高さ
62
+ */
63
+ relativeHeight: (basisHeight: number) => number;
64
+ /**
65
+ * 相対座標(0-1の値)を実際のピクセル座標に変換
66
+ * @param basisPos 相対座標 {x, y}(0-1)
67
+ * @returns 実際のピクセル座標
68
+ */
69
+ relativePos: (basisPos: {
70
+ x: number;
71
+ y: number;
72
+ }) => {
73
+ x: number;
74
+ y: number;
75
+ };
76
+ };
77
+ /**
78
+ * 画面サイズを取得するフック
79
+ * @returns 現在の画面サイズ {width, height}
80
+ */
81
+ export declare const useScreenSize: () => {
82
+ width: number;
83
+ height: number;
84
+ };
85
+ /**
86
+ * 画面のスケール倍率を取得するフック
87
+ * @returns 基準サイズに対するスケール倍率
88
+ */
89
+ export declare const useScreenScale: () => number;
@@ -0,0 +1,87 @@
1
+ import { useScreenSizeAtom } from "../atoms/screen-size";
2
+ import { BasisHeight, BasisWidth } from "../constants/screen-size";
3
+ /**
4
+ * Figmaの基準座標(1920×1080)から実際のピクセル座標への変換を提供するフック
5
+ */
6
+ export const useToPixel = () => {
7
+ const [{ width: screenWidth, height: screenHeight }] = useScreenSizeAtom();
8
+ return {
9
+ /**
10
+ * 基準座標(px)を実際のピクセルに変換(高さ基準)
11
+ * @param basisX 基準座標での値
12
+ * @returns 実際のピクセル値
13
+ */
14
+ pixel: (basisX) => (basisX / BasisHeight) * screenHeight,
15
+ /**
16
+ * 基準座標(px)を実際のピクセルに変換(幅基準)
17
+ * @param basisX 基準座標での値
18
+ * @returns 実際のピクセル値
19
+ */
20
+ pixelWidth: (basisX) => (basisX / BasisWidth) * screenWidth,
21
+ /**
22
+ * アスペクト比を考慮したサイズを返す
23
+ * @param basisSize 基準サイズ
24
+ * @returns 幅と高さのピクセル値
25
+ */
26
+ aspectSize: (basisSize) => ({
27
+ width: (basisSize / BasisHeight) * screenHeight,
28
+ height: (basisSize / BasisHeight) * screenHeight,
29
+ }),
30
+ /**
31
+ * 2D座標を基準から実際のピクセルに変換
32
+ * @param basisPos 基準座標 {x, y}
33
+ * @returns 実際のピクセル座標
34
+ */
35
+ pos: (basisPos) => ({
36
+ x: (basisPos.x / BasisWidth) * screenWidth,
37
+ y: (basisPos.y / BasisHeight) * screenHeight,
38
+ }),
39
+ /**
40
+ * 3D座標を基準から実際のピクセルに変換
41
+ * @param basisPos 基準座標 {x, y, z}
42
+ * @returns 実際のピクセル座標
43
+ */
44
+ vec3d: (basisPos) => ({
45
+ x: (basisPos.x / BasisWidth) * screenWidth,
46
+ y: (basisPos.y / BasisHeight) * screenHeight,
47
+ z: (basisPos.z / BasisHeight) * screenHeight,
48
+ }),
49
+ /**
50
+ * 相対幅(0-1の値)を実際のピクセル幅に変換
51
+ * @param basisWidth 基準幅(0-1)
52
+ * @returns 実際のピクセル幅
53
+ */
54
+ relativeWidth: (basisWidth) => basisWidth * screenWidth,
55
+ /**
56
+ * 相対高さ(0-1の値)を実際のピクセル高さに変換
57
+ * @param basisHeight 基準高さ(0-1)
58
+ * @returns 実際のピクセル高さ
59
+ */
60
+ relativeHeight: (basisHeight) => basisHeight * screenHeight,
61
+ /**
62
+ * 相対座標(0-1の値)を実際のピクセル座標に変換
63
+ * @param basisPos 相対座標 {x, y}(0-1)
64
+ * @returns 実際のピクセル座標
65
+ */
66
+ relativePos: (basisPos) => ({
67
+ x: basisPos.x * screenWidth,
68
+ y: basisPos.y * screenHeight,
69
+ }),
70
+ };
71
+ };
72
+ /**
73
+ * 画面サイズを取得するフック
74
+ * @returns 現在の画面サイズ {width, height}
75
+ */
76
+ export const useScreenSize = () => {
77
+ const [screenSize] = useScreenSizeAtom();
78
+ return screenSize;
79
+ };
80
+ /**
81
+ * 画面のスケール倍率を取得するフック
82
+ * @returns 基準サイズに対するスケール倍率
83
+ */
84
+ export const useScreenScale = () => {
85
+ const [{ height: screenHeight }] = useScreenSizeAtom();
86
+ return screenHeight / BasisHeight;
87
+ };
@@ -0,0 +1,11 @@
1
+ export interface UseTypewriterOptions {
2
+ speed?: number;
3
+ onComplete?: () => void;
4
+ }
5
+ export declare const useTypewriter: (options?: UseTypewriterOptions) => {
6
+ displayText: string;
7
+ isTyping: boolean;
8
+ startTyping: (text: string) => void;
9
+ skipTyping: (text: string) => void;
10
+ reset: () => void;
11
+ };