@luna-editor/engine 0.4.5 → 0.5.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/dist/Player.js +24 -7
- package/dist/components/GameScreen.js +215 -22
- package/dist/contexts/DataContext.d.ts +2 -0
- package/dist/contexts/DataContext.js +14 -2
- package/dist/data-api-types.d.ts +6 -0
- package/dist/hooks/usePlayerLogic.js +3 -0
- package/dist/hooks/useSoundPlayer.js +33 -5
- package/dist/plugin/PluginManager.js +3 -0
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
- package/dist/hooks/useImagePreloader.d.ts +0 -5
- package/dist/hooks/useImagePreloader.js +0 -53
- package/dist/plugin/luna-react.d.ts +0 -41
- package/dist/plugin/luna-react.js +0 -99
package/dist/Player.js
CHANGED
|
@@ -49,7 +49,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
49
49
|
return (Object.assign(Object.assign({}, scenarioProp), { blocks: (_a = scenarioProp.blocks) !== null && _a !== void 0 ? _a : [] }));
|
|
50
50
|
}, [scenarioProp]);
|
|
51
51
|
// デフォルト値とマージ
|
|
52
|
-
const mergedSettings = Object.assign({ textSpeed: 80, autoPlaySpeed: 3, bgmVolume: 0.5, seVolume: 1.0, voiceVolume: 1.0, effectVolume: 1.0, textSoundVolume: 1.0, defaultBackgroundFadeDuration: 0, inactiveCharacterBrightness: 0.8 }, settings);
|
|
52
|
+
const mergedSettings = Object.assign({ textSpeed: 80, autoPlaySpeed: 3, bgmVolume: 0.5, seVolume: 1.0, voiceVolume: 1.0, effectVolume: 1.0, textSoundVolume: 1.0, defaultBackgroundFadeDuration: 0, inactiveCharacterBrightness: 0.8, characterSpacing: 0.2 }, settings);
|
|
53
53
|
// プラグインからの設定更新ハンドラ
|
|
54
54
|
const handleSettingsUpdate = useCallback((updatedSettings) => {
|
|
55
55
|
var _a, _b;
|
|
@@ -645,6 +645,16 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
645
645
|
scenario.id,
|
|
646
646
|
scenario.name,
|
|
647
647
|
]);
|
|
648
|
+
// プラグインからシナリオを中断するためのコールバック
|
|
649
|
+
const cancelScenario = useCallback(() => {
|
|
650
|
+
if (hasStarted && !state.isEnded) {
|
|
651
|
+
onScenarioCancelled === null || onScenarioCancelled === void 0 ? void 0 : onScenarioCancelled();
|
|
652
|
+
pluginManager.callHook("onScenarioEnd", {
|
|
653
|
+
scenarioId: scenario.id,
|
|
654
|
+
scenarioName: scenario.name || "scenario",
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}, [hasStarted, state.isEnded, onScenarioCancelled, pluginManager, scenario.id, scenario.name]);
|
|
648
658
|
const { handleNext: handleNextInternal, handlePrevious: handlePreviousInternal, togglePlay: _togglePlay, restart: _restartInternal, displayedCharacters, } = usePlayerLogic({
|
|
649
659
|
state: Object.assign(Object.assign({}, state), { currentBlockIndex: actualBlockIndex }),
|
|
650
660
|
setState: (newState) => {
|
|
@@ -918,18 +928,25 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
918
928
|
touchAction: "none",
|
|
919
929
|
userSelect: "none",
|
|
920
930
|
WebkitUserSelect: "none",
|
|
921
|
-
}, children: _jsx("div", { ref: aspectRatioContainerRef, className: "relative bg-white flex flex-col overflow-hidden", style:
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
931
|
+
}, children: _jsx("div", { ref: aspectRatioContainerRef, className: "relative bg-white flex flex-col overflow-hidden", style: screenSizeProp
|
|
932
|
+
? {
|
|
933
|
+
// プレビューモード: 親コンテナ(固定論理サイズ)を100%で埋める
|
|
934
|
+
width: "100%",
|
|
935
|
+
height: "100%",
|
|
936
|
+
}
|
|
937
|
+
: {
|
|
938
|
+
// 通常再生: ビューポートに合わせてアスペクト比を維持
|
|
939
|
+
aspectRatio: getAspectRatio(),
|
|
940
|
+
width: `min(100vw, calc(100vh * ${getAspectRatioValue()}))`,
|
|
941
|
+
height: `min(100vh, calc(100vw / ${getAspectRatioValue()}))`,
|
|
942
|
+
}, children: _jsx(DataProvider, { data: dataContext, onSettingsUpdate: handleSettingsUpdate, onEmotionEffectUpdate: handleEmotionEffectUpdate, onCancelScenario: cancelScenario, children: _jsx(AudioProvider, { settings: {
|
|
926
943
|
bgmVolume: mergedSettings.bgmVolume,
|
|
927
944
|
seVolume: mergedSettings.seVolume,
|
|
928
945
|
voiceVolume: mergedSettings.voiceVolume,
|
|
929
946
|
effectVolume: mergedSettings.effectVolume,
|
|
930
947
|
textSoundVolume: mergedSettings.textSoundVolume,
|
|
931
948
|
muteAudio: (_b = mergedSettings.muteAudio) !== null && _b !== void 0 ? _b : false,
|
|
932
|
-
}, children: _jsxs(PlaybackTextProvider, { displayText: displayTextElement, isTyping: isTyping, children: [_jsxs("div", { className: "h-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.Background, pluginManager: pluginManager, fallback: BackgroundLayer }, "background"), _jsx(GameScreen, { scenario: scenario, currentBlock: currentBlock, previousBlock: previousBlock, displayedCharacters: displayedCharacters }, "game-screen")] }), _jsx(OverlayUI, { children: _jsxs("div", { className: "h-full w-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.DialogueBox, pluginManager: pluginManager, fallback: DialogueBox }, "dialogue-box"), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "conversation_branch" && (_jsx(PluginComponentProvider, { type: ComponentType.ConversationBranch, pluginManager: pluginManager, fallback: ConversationBranchBox }, "conversation-branch")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "fullscreen_text" && (_jsx(FullscreenTextBox, { onComplete: () => handleNext("click") }, "fullscreen-text")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "click_wait" && (_jsx(ClickWaitIndicator, {}, "click-wait")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "time_wait" && (_jsx(TimeWaitIndicator, { duration: (_d = (_c = currentBlock.options) === null || _c === void 0 ? void 0 : _c.duration) !== null && _d !== void 0 ? _d : 1, onComplete: () => handleNext("time_wait") }, "time-wait")), pluginManager
|
|
949
|
+
}, children: _jsxs(PlaybackTextProvider, { displayText: displayTextElement, isTyping: isTyping, children: [_jsxs("div", { className: "h-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.Background, pluginManager: pluginManager, fallback: BackgroundLayer }, "background"), _jsx(GameScreen, { scenario: scenario, currentBlock: currentBlock, previousBlock: previousBlock, displayedCharacters: displayedCharacters, inactiveCharacterBrightness: mergedSettings.inactiveCharacterBrightness, characterSpacing: mergedSettings.characterSpacing }, "game-screen")] }), _jsx(OverlayUI, { children: _jsxs("div", { className: "h-full w-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.DialogueBox, pluginManager: pluginManager, fallback: DialogueBox }, "dialogue-box"), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "conversation_branch" && (_jsx(PluginComponentProvider, { type: ComponentType.ConversationBranch, pluginManager: pluginManager, fallback: ConversationBranchBox }, "conversation-branch")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "fullscreen_text" && (_jsx(FullscreenTextBox, { onComplete: () => handleNext("click") }, "fullscreen-text")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "click_wait" && (_jsx(ClickWaitIndicator, {}, "click-wait")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "time_wait" && (_jsx(TimeWaitIndicator, { duration: (_d = (_c = currentBlock.options) === null || _c === void 0 ? void 0 : _c.duration) !== null && _d !== void 0 ? _d : 1, onComplete: () => handleNext("time_wait") }, "time-wait")), pluginManager
|
|
933
950
|
.getRegisteredComponents()
|
|
934
951
|
.filter((type) => type !== ComponentType.DialogueBox &&
|
|
935
952
|
type !== ComponentType.ConversationBranch)
|
|
@@ -5,7 +5,7 @@ import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "re
|
|
|
5
5
|
* React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
|
|
6
6
|
* これにより displayText の更新による不要な再描画を防止
|
|
7
7
|
*/
|
|
8
|
-
export const GameScreen = memo(function GameScreen({ scenario, currentBlock, previousBlock, displayedCharacters, inactiveCharacterBrightness = 0.8, characterSpacing = 0.
|
|
8
|
+
export const GameScreen = memo(function GameScreen({ scenario, currentBlock, previousBlock, displayedCharacters, inactiveCharacterBrightness = 0.8, characterSpacing = 0.2, }) {
|
|
9
9
|
var _a;
|
|
10
10
|
// キャラクターごとのフェード状態を管理
|
|
11
11
|
const [fadeStates, setFadeStates] = useState(new Map());
|
|
@@ -13,6 +13,142 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
13
13
|
const fadeStartTimeRef = useRef(new Map());
|
|
14
14
|
// 完了したフェードを追跡(同じpendingFadeで再度フェードしないため)
|
|
15
15
|
const completedFadesRef = useRef(new Set());
|
|
16
|
+
// 登場・退場フェード管理
|
|
17
|
+
const ENTRANCE_FADE_DURATION = 300; // ms
|
|
18
|
+
const [entranceFades, setEntranceFades] = useState(new Map()); // objectId -> opacity (0-1)
|
|
19
|
+
const [exitingCharacters, setExitingCharacters] = useState([]);
|
|
20
|
+
const prevDisplayedCharIdsRef = useRef(new Set());
|
|
21
|
+
const entranceFadeStartRef = useRef(new Map());
|
|
22
|
+
const entranceAnimFrameRef = useRef(null);
|
|
23
|
+
const prevDisplayedCharsRef = useRef([]);
|
|
24
|
+
const pendingEntranceRef = useRef([]);
|
|
25
|
+
// 退場キャラの最終位置を保存(auto-layout再計算に依存しないため)
|
|
26
|
+
const exitPositionsRef = useRef(new Map());
|
|
27
|
+
// 登場・退場の検出
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
const currentIds = new Set(displayedCharacters.map((c) => c.objectId));
|
|
31
|
+
const prevIds = prevDisplayedCharIdsRef.current;
|
|
32
|
+
const prevChars = prevDisplayedCharsRef.current;
|
|
33
|
+
// 初回レンダリング(前回データなし)はスキップ
|
|
34
|
+
if (prevIds.size === 0 && prevChars.length === 0) {
|
|
35
|
+
prevDisplayedCharIdsRef.current = currentIds;
|
|
36
|
+
prevDisplayedCharsRef.current = displayedCharacters;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// キャラ構成が変わったか判定(IDセットまたはentityStateIdの変化)
|
|
40
|
+
const hasChange = currentIds.size !== prevIds.size ||
|
|
41
|
+
[...currentIds].some((id) => !prevIds.has(id)) ||
|
|
42
|
+
[...prevIds].some((id) => !currentIds.has(id));
|
|
43
|
+
if (!hasChange) {
|
|
44
|
+
prevDisplayedCharsRef.current = displayedCharacters;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// キャラ構成が変わった場合: 前回の全キャラを退場、現在の全キャラを登場
|
|
48
|
+
// これにより同じキャラが含まれていても必ずフェードイン/アウトが発生
|
|
49
|
+
const allExitChars = [...prevChars];
|
|
50
|
+
const allEnterIds = [...currentIds];
|
|
51
|
+
// 退場フェードアウト開始
|
|
52
|
+
if (allExitChars.length > 0) {
|
|
53
|
+
const now = performance.now();
|
|
54
|
+
// 退場キャラの現在の位置を保存
|
|
55
|
+
for (const char of allExitChars) {
|
|
56
|
+
if (char.positionX !== null && char.positionY !== null) {
|
|
57
|
+
exitPositionsRef.current.set(char.objectId, {
|
|
58
|
+
x: (_a = char.positionX) !== null && _a !== void 0 ? _a : 0,
|
|
59
|
+
y: (_b = char.positionY) !== null && _b !== void 0 ? _b : 0,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const autoPositions = calculateAutoLayout(prevChars, characterSpacing !== null && characterSpacing !== void 0 ? characterSpacing : 0.2);
|
|
64
|
+
const pos = autoPositions.get(char.objectId);
|
|
65
|
+
if (pos) {
|
|
66
|
+
exitPositionsRef.current.set(char.objectId, pos);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
setExitingCharacters((prev) => [...prev, ...allExitChars]);
|
|
71
|
+
const newFades = new Map(entranceFades);
|
|
72
|
+
for (const char of allExitChars) {
|
|
73
|
+
newFades.set(char.objectId, 1);
|
|
74
|
+
entranceFadeStartRef.current.set(char.objectId, now);
|
|
75
|
+
}
|
|
76
|
+
setEntranceFades(newFades);
|
|
77
|
+
}
|
|
78
|
+
// 登場フェードイン開始(退場完了を待ってから)
|
|
79
|
+
if (allEnterIds.length > 0) {
|
|
80
|
+
const delay = allExitChars.length > 0 ? ENTRANCE_FADE_DURATION + 300 : 0;
|
|
81
|
+
pendingEntranceRef.current = allEnterIds;
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
const ids = pendingEntranceRef.current;
|
|
84
|
+
if (ids.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
pendingEntranceRef.current = [];
|
|
87
|
+
const now = performance.now();
|
|
88
|
+
setEntranceFades((prev) => {
|
|
89
|
+
const newFades = new Map(prev);
|
|
90
|
+
for (const id of ids) {
|
|
91
|
+
newFades.set(id, 0);
|
|
92
|
+
entranceFadeStartRef.current.set(id, now);
|
|
93
|
+
}
|
|
94
|
+
return newFades;
|
|
95
|
+
});
|
|
96
|
+
}, delay);
|
|
97
|
+
}
|
|
98
|
+
prevDisplayedCharIdsRef.current = currentIds;
|
|
99
|
+
prevDisplayedCharsRef.current = displayedCharacters;
|
|
100
|
+
}, [displayedCharacters]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
101
|
+
// 登場・退場フェードのアニメーションループ
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (entranceFades.size === 0)
|
|
104
|
+
return;
|
|
105
|
+
const exitingIds = new Set(exitingCharacters.map((c) => c.objectId));
|
|
106
|
+
const animate = () => {
|
|
107
|
+
const now = performance.now();
|
|
108
|
+
let hasActive = false;
|
|
109
|
+
const newFades = new Map();
|
|
110
|
+
const completedExitIds = [];
|
|
111
|
+
for (const [objectId] of entranceFades) {
|
|
112
|
+
const startTime = entranceFadeStartRef.current.get(objectId);
|
|
113
|
+
if (!startTime)
|
|
114
|
+
continue;
|
|
115
|
+
const elapsed = now - startTime;
|
|
116
|
+
const progress = Math.min(elapsed / ENTRANCE_FADE_DURATION, 1);
|
|
117
|
+
const isExiting = exitingIds.has(objectId);
|
|
118
|
+
// 退場: 1→0, 登場: 0→1
|
|
119
|
+
const opacity = isExiting ? 1 - progress : progress;
|
|
120
|
+
newFades.set(objectId, opacity);
|
|
121
|
+
if (progress < 1) {
|
|
122
|
+
hasActive = true;
|
|
123
|
+
}
|
|
124
|
+
else if (isExiting) {
|
|
125
|
+
completedExitIds.push(objectId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
setEntranceFades(newFades);
|
|
129
|
+
// 退場フェード完了したキャラを削除
|
|
130
|
+
if (completedExitIds.length > 0) {
|
|
131
|
+
setExitingCharacters((prev) => prev.filter((c) => !completedExitIds.includes(c.objectId)));
|
|
132
|
+
// 保存位置もクリーンアップ
|
|
133
|
+
for (const id of completedExitIds) {
|
|
134
|
+
exitPositionsRef.current.delete(id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (hasActive) {
|
|
138
|
+
entranceAnimFrameRef.current = requestAnimationFrame(animate);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
setEntranceFades(new Map());
|
|
142
|
+
entranceFadeStartRef.current.clear();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
entranceAnimFrameRef.current = requestAnimationFrame(animate);
|
|
146
|
+
return () => {
|
|
147
|
+
if (entranceAnimFrameRef.current) {
|
|
148
|
+
cancelAnimationFrame(entranceAnimFrameRef.current);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}, [entranceFades.size > 0, exitingCharacters]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
16
152
|
// コンテナサイズを取得(ResizeObserverで確実に取得)
|
|
17
153
|
const containerRef = useRef(null);
|
|
18
154
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
@@ -59,7 +195,7 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
59
195
|
// character_entranceブロックのキャラクター画像(レイヤー情報含む)
|
|
60
196
|
if (block.characters) {
|
|
61
197
|
block.characters.forEach((char) => {
|
|
62
|
-
var _a, _b, _c, _d, _e, _f;
|
|
198
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
63
199
|
if (char.entityState.imageUrl || ((_a = char.baseBodyState) === null || _a === void 0 ? void 0 : _a.imageUrl)) {
|
|
64
200
|
const key = `${char.objectId}-${char.entityStateId}`;
|
|
65
201
|
images.set(key, {
|
|
@@ -75,6 +211,10 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
75
211
|
baseBodyTranslateX: (_e = char.baseBodyState) === null || _e === void 0 ? void 0 : _e.translateX,
|
|
76
212
|
baseBodyTranslateY: (_f = char.baseBodyState) === null || _f === void 0 ? void 0 : _f.translateY,
|
|
77
213
|
layers: char.entityState.layers,
|
|
214
|
+
// EntityStateのcrop値を優先、なければScenarioBlockCharacterのcrop値
|
|
215
|
+
cropLeft: (_g = char.entityState.cropLeft) !== null && _g !== void 0 ? _g : char.cropLeft,
|
|
216
|
+
cropRight: (_h = char.entityState.cropRight) !== null && _h !== void 0 ? _h : char.cropRight,
|
|
217
|
+
cropFade: (_j = char.entityState.cropFade) !== null && _j !== void 0 ? _j : char.cropFade,
|
|
78
218
|
});
|
|
79
219
|
}
|
|
80
220
|
});
|
|
@@ -251,7 +391,7 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
251
391
|
};
|
|
252
392
|
// キャラクター描画用のヘルパー関数
|
|
253
393
|
const renderCharacter = (image, displayedChar, isCurrentSpeaker, keyPrefix) => {
|
|
254
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
394
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
255
395
|
// フェード状態を確認
|
|
256
396
|
const fadeState = fadeStates.get(image.objectId);
|
|
257
397
|
const isFadingIn = fadeState && fadeState.previousEntityStateId !== image.entityStateId;
|
|
@@ -266,8 +406,14 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
266
406
|
(pendingFade === null || pendingFade === void 0 ? void 0 : pendingFade.objectId) === image.objectId &&
|
|
267
407
|
pendingFade.previousEntityStateId === image.entityStateId &&
|
|
268
408
|
!completedFadesRef.current.has(pendingFade.fadeKey);
|
|
409
|
+
// 退場中のキャラクターか判定
|
|
410
|
+
const isExitingChar = exitingCharacters.some((c) => c.objectId === image.objectId);
|
|
269
411
|
// 表示すべきかどうか
|
|
270
|
-
const shouldDisplay = displayedChar ||
|
|
412
|
+
const shouldDisplay = displayedChar ||
|
|
413
|
+
isCurrentSpeaker ||
|
|
414
|
+
isFadingOut ||
|
|
415
|
+
isPendingFadeOut ||
|
|
416
|
+
isExitingChar;
|
|
271
417
|
// opacityを決定
|
|
272
418
|
let opacity = 1;
|
|
273
419
|
if (isFadingIn || isPendingFadeIn) {
|
|
@@ -278,6 +424,15 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
278
424
|
// フェードアウト中
|
|
279
425
|
opacity = fadeState ? 1 - fadeState.fadeProgress : 1;
|
|
280
426
|
}
|
|
427
|
+
// 登場・退場フェード(character_entrance/exit時)
|
|
428
|
+
const entranceFadeOpacity = entranceFades.get(image.objectId);
|
|
429
|
+
if (entranceFadeOpacity !== undefined) {
|
|
430
|
+
opacity = entranceFadeOpacity;
|
|
431
|
+
}
|
|
432
|
+
// 登場待ち(退場フェード完了待ち)のキャラは非表示
|
|
433
|
+
if (pendingEntranceRef.current.includes(image.objectId)) {
|
|
434
|
+
opacity = 0;
|
|
435
|
+
}
|
|
281
436
|
// 明るさを決定
|
|
282
437
|
let brightness = 1;
|
|
283
438
|
if (displayedCharacters.length > 0 && displayedChar) {
|
|
@@ -298,19 +453,31 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
298
453
|
// 位置を決定(カスタム位置またはデフォルト位置)
|
|
299
454
|
// 新座標系: x: -1=左見切れ, 0=中央, 1=右見切れ / y: -1=上見切れ, 0=中央, 1=下見切れ
|
|
300
455
|
let finalPosition = { x: 0, y: 0 };
|
|
301
|
-
|
|
456
|
+
// 退場中のキャラは保存された位置情報を使用(移動しない)
|
|
457
|
+
const exitingChar = isExitingChar
|
|
458
|
+
? exitingCharacters.find((c) => c.objectId === image.objectId)
|
|
459
|
+
: null;
|
|
460
|
+
const positionSource = displayedChar !== null && displayedChar !== void 0 ? displayedChar : exitingChar;
|
|
461
|
+
if (isExitingChar) {
|
|
462
|
+
// 退場中のキャラ: 保存された位置を使用(絶対に移動しない)
|
|
463
|
+
const savedPos = exitPositionsRef.current.get(image.objectId);
|
|
464
|
+
if (savedPos) {
|
|
465
|
+
finalPosition = savedPos;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (positionSource) {
|
|
302
469
|
// カスタム位置が設定されている場合(詳細モード)
|
|
303
|
-
if (
|
|
304
|
-
|
|
470
|
+
if (positionSource.positionX !== null &&
|
|
471
|
+
positionSource.positionY !== null) {
|
|
305
472
|
finalPosition = {
|
|
306
|
-
x: (_e =
|
|
307
|
-
y: (_f =
|
|
473
|
+
x: (_e = positionSource.positionX) !== null && _e !== void 0 ? _e : 0,
|
|
474
|
+
y: (_f = positionSource.positionY) !== null && _f !== void 0 ? _f : 0,
|
|
308
475
|
};
|
|
309
476
|
}
|
|
310
477
|
else {
|
|
311
478
|
// 簡易モード: 自動配置
|
|
312
|
-
const autoPositions = calculateAutoLayout(displayedCharacters, characterSpacing !== null && characterSpacing !== void 0 ? characterSpacing : 0.
|
|
313
|
-
const autoPos = autoPositions.get(
|
|
479
|
+
const autoPositions = calculateAutoLayout(displayedCharacters, characterSpacing !== null && characterSpacing !== void 0 ? characterSpacing : 0.2);
|
|
480
|
+
const autoPos = autoPositions.get(positionSource.objectId);
|
|
314
481
|
if (autoPos) {
|
|
315
482
|
finalPosition = autoPos;
|
|
316
483
|
}
|
|
@@ -346,19 +513,24 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
346
513
|
// 位置をピクセルで計算(CharacterEditDialogと同じ)
|
|
347
514
|
const leftPx = (leftPercent / 100) * containerSize.width;
|
|
348
515
|
const topPx = (topPercent / 100) * containerSize.height;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
516
|
+
// トリミング用のCSS mask計算
|
|
517
|
+
// displayedCharacterのcrop値を優先(キャラクター登場ブロックで設定)
|
|
518
|
+
const cLeft = (_k = (_j = displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.cropLeft) !== null && _j !== void 0 ? _j : image.cropLeft) !== null && _k !== void 0 ? _k : 0;
|
|
519
|
+
const cRight = (_m = (_l = displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.cropRight) !== null && _l !== void 0 ? _l : image.cropRight) !== null && _m !== void 0 ? _m : 0;
|
|
520
|
+
const cFade = (_p = (_o = displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.cropFade) !== null && _o !== void 0 ? _o : image.cropFade) !== null && _p !== void 0 ? _p : 0;
|
|
521
|
+
const hasCrop = cLeft > 0 || cRight > 0;
|
|
522
|
+
// フェード量をpx基準値からコンテナ高さ比でスケーリング
|
|
523
|
+
const fadePx = hasCrop && cFade > 0 ? (cFade / 1080) * containerSize.height : 0;
|
|
524
|
+
const cropMaskStyle = hasCrop
|
|
525
|
+
? {
|
|
526
|
+
WebkitMaskImage: `linear-gradient(to right, transparent ${cLeft * 100}%, black ${cLeft * 100 + (fadePx > 0 ? (fadePx / baseHeight) * 100 : 0)}%, black ${(1 - cRight) * 100 - (fadePx > 0 ? (fadePx / baseHeight) * 100 : 0)}%, transparent ${(1 - cRight) * 100}%)`,
|
|
527
|
+
maskImage: `linear-gradient(to right, transparent ${cLeft * 100}%, black ${cLeft * 100 + (fadePx > 0 ? (fadePx / baseHeight) * 100 : 0)}%, black ${(1 - cRight) * 100 - (fadePx > 0 ? (fadePx / baseHeight) * 100 : 0)}%, transparent ${(1 - cRight) * 100}%)`,
|
|
528
|
+
}
|
|
529
|
+
: {};
|
|
530
|
+
return (_jsx("div", { style: Object.assign({ position: "absolute", visibility: shouldDisplay ? "visible" : "hidden", opacity: opacity, filter: `brightness(${brightness})`, zIndex: zIndex,
|
|
355
531
|
// CharacterEditDialogと完全に同じ: left/topを0にしてtransformで位置制御
|
|
356
532
|
// translate(%)は画像サイズに対する割合
|
|
357
|
-
left: 0,
|
|
358
|
-
top: 0,
|
|
359
|
-
transform: `translate(${leftPx}px, ${topPx}px) translate(${translateXPercent}%, ${translateYPercent}%)`,
|
|
360
|
-
transition: fadeState ? "none" : undefined,
|
|
361
|
-
}, "data-character-id": image.objectId, "data-character-sprite": true, children: hasLayerFeature && image.baseBodyUrl ? (_jsxs(_Fragment, { children: [_jsx("img", { src: image.baseBodyUrl, alt: "", style: {
|
|
533
|
+
left: 0, top: 0, transform: `translate(${leftPx}px, ${topPx}px) translate(${translateXPercent}%, ${translateYPercent}%)`, transition: fadeState ? "none" : undefined }, cropMaskStyle), "data-character-id": image.objectId, "data-character-sprite": true, children: hasLayerFeature && image.baseBodyUrl ? (_jsxs(_Fragment, { children: [_jsx("img", { src: image.baseBodyUrl, alt: "", style: {
|
|
362
534
|
height: `${baseHeight}px`,
|
|
363
535
|
width: "auto",
|
|
364
536
|
objectFit: "contain",
|
|
@@ -407,6 +579,27 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
407
579
|
return renderCharacter(fallbackImage, char, false, "char");
|
|
408
580
|
}
|
|
409
581
|
return renderCharacter(image, char, false, "char");
|
|
582
|
+
}), exitingCharacters.map((char) => {
|
|
583
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
584
|
+
const imageKey = `${char.objectId}-${char.entityStateId}`;
|
|
585
|
+
const image = imageDataMap.get(imageKey);
|
|
586
|
+
if (!image) {
|
|
587
|
+
const fallbackImage = {
|
|
588
|
+
url: (_b = (_a = char.entityState) === null || _a === void 0 ? void 0 : _a.imageUrl) !== null && _b !== void 0 ? _b : "",
|
|
589
|
+
objectId: char.objectId,
|
|
590
|
+
entityStateId: char.entityStateId,
|
|
591
|
+
scale: (_c = char.entityState) === null || _c === void 0 ? void 0 : _c.scale,
|
|
592
|
+
translateX: (_d = char.entityState) === null || _d === void 0 ? void 0 : _d.translateX,
|
|
593
|
+
translateY: (_e = char.entityState) === null || _e === void 0 ? void 0 : _e.translateY,
|
|
594
|
+
baseBodyUrl: (_f = char.baseBodyState) === null || _f === void 0 ? void 0 : _f.imageUrl,
|
|
595
|
+
baseBodyScale: (_g = char.baseBodyState) === null || _g === void 0 ? void 0 : _g.scale,
|
|
596
|
+
baseBodyTranslateX: (_h = char.baseBodyState) === null || _h === void 0 ? void 0 : _h.translateX,
|
|
597
|
+
baseBodyTranslateY: (_j = char.baseBodyState) === null || _j === void 0 ? void 0 : _j.translateY,
|
|
598
|
+
layers: (_k = char.entityState) === null || _k === void 0 ? void 0 : _k.layers,
|
|
599
|
+
};
|
|
600
|
+
return renderCharacter(fallbackImage, char, false, "exit");
|
|
601
|
+
}
|
|
602
|
+
return renderCharacter(image, char, false, "exit");
|
|
410
603
|
}), displayedCharacters.length === 0 &&
|
|
411
604
|
currentBlock.speakerId &&
|
|
412
605
|
currentBlock.speakerStateId && (_jsxs(_Fragment, { children: [(() => {
|
|
@@ -2,6 +2,7 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
import type { DataAPI, DataContext, EmotionEffectState, PlayerSettingsData } from "../sdk";
|
|
3
3
|
type SettingsUpdater = (settings: Partial<PlayerSettingsData>) => void;
|
|
4
4
|
type EmotionEffectUpdater = (state: EmotionEffectState | null) => void;
|
|
5
|
+
type ScenarioCanceller = () => void;
|
|
5
6
|
/**
|
|
6
7
|
* データプロバイダー
|
|
7
8
|
* プラグインがシナリオ情報にリアクティブにアクセスするためのProvider
|
|
@@ -19,6 +20,7 @@ export declare const DataProvider: React.FC<{
|
|
|
19
20
|
data: DataContext;
|
|
20
21
|
onSettingsUpdate?: SettingsUpdater;
|
|
21
22
|
onEmotionEffectUpdate?: EmotionEffectUpdater;
|
|
23
|
+
onCancelScenario?: ScenarioCanceller;
|
|
22
24
|
children: ReactNode;
|
|
23
25
|
}>;
|
|
24
26
|
/**
|
|
@@ -6,6 +6,7 @@ const DataRefContext = createContext(null);
|
|
|
6
6
|
const SubscribersContext = createContext(null);
|
|
7
7
|
const SettingsUpdaterContext = createContext(null);
|
|
8
8
|
const EmotionEffectUpdaterContext = createContext(null);
|
|
9
|
+
const ScenarioCancellerContext = createContext(null);
|
|
9
10
|
/**
|
|
10
11
|
* データプロバイダー
|
|
11
12
|
* プラグインがシナリオ情報にリアクティブにアクセスするためのProvider
|
|
@@ -19,7 +20,7 @@ const EmotionEffectUpdaterContext = createContext(null);
|
|
|
19
20
|
* </DataProvider>
|
|
20
21
|
* ```
|
|
21
22
|
*/
|
|
22
|
-
export const DataProvider = ({ data, onSettingsUpdate, onEmotionEffectUpdate, children }) => {
|
|
23
|
+
export const DataProvider = ({ data, onSettingsUpdate, onEmotionEffectUpdate, onCancelScenario, children }) => {
|
|
23
24
|
const subscribers = useMemo(() => new Map(), []);
|
|
24
25
|
const previousDataRef = useRef(data);
|
|
25
26
|
// 安定したデータ参照ホルダー(Context valueとして提供)
|
|
@@ -61,7 +62,7 @@ export const DataProvider = ({ data, onSettingsUpdate, onEmotionEffectUpdate, ch
|
|
|
61
62
|
}
|
|
62
63
|
previousDataRef.current = data;
|
|
63
64
|
}, [data, subscribers]);
|
|
64
|
-
return (_jsx(SubscribersContext.Provider, { value: subscribers, children: _jsx(SettingsUpdaterContext.Provider, { value: onSettingsUpdate !== null && onSettingsUpdate !== void 0 ? onSettingsUpdate : null, children: _jsx(EmotionEffectUpdaterContext.Provider, { value: onEmotionEffectUpdate !== null && onEmotionEffectUpdate !== void 0 ? onEmotionEffectUpdate : null, children: _jsx(DataRefContext.Provider, { value: dataRefHolder, children: _jsx(DataContextInstance.Provider, { value: data, children: children }) }) }) }) }));
|
|
65
|
+
return (_jsx(SubscribersContext.Provider, { value: subscribers, children: _jsx(SettingsUpdaterContext.Provider, { value: onSettingsUpdate !== null && onSettingsUpdate !== void 0 ? onSettingsUpdate : null, children: _jsx(EmotionEffectUpdaterContext.Provider, { value: onEmotionEffectUpdate !== null && onEmotionEffectUpdate !== void 0 ? onEmotionEffectUpdate : null, children: _jsx(ScenarioCancellerContext.Provider, { value: onCancelScenario !== null && onCancelScenario !== void 0 ? onCancelScenario : null, children: _jsx(DataRefContext.Provider, { value: dataRefHolder, children: _jsx(DataContextInstance.Provider, { value: data, children: children }) }) }) }) }) }));
|
|
65
66
|
};
|
|
66
67
|
/**
|
|
67
68
|
* DataAPI実装を提供するフック
|
|
@@ -75,6 +76,7 @@ export function useDataAPI() {
|
|
|
75
76
|
const subscribers = useContext(SubscribersContext);
|
|
76
77
|
const settingsUpdater = useContext(SettingsUpdaterContext);
|
|
77
78
|
const emotionEffectUpdater = useContext(EmotionEffectUpdaterContext);
|
|
79
|
+
const scenarioCanceller = useContext(ScenarioCancellerContext);
|
|
78
80
|
// 注意: usePlaybackTextOptional()をここで呼ぶと、PlaybackTextContextの更新で
|
|
79
81
|
// useDataAPIを使う全コンポーネントが再レンダリングされてしまう
|
|
80
82
|
// 代わりに、displayTextが必要なコンポーネントはusePlaybackText()を直接使う
|
|
@@ -173,6 +175,14 @@ export function useDataAPI() {
|
|
|
173
175
|
console.warn("updateEmotionEffect called but no emotion effect updater provided");
|
|
174
176
|
}
|
|
175
177
|
}, [emotionEffectUpdater]);
|
|
178
|
+
const cancelScenario = useCallback(() => {
|
|
179
|
+
if (scenarioCanceller) {
|
|
180
|
+
scenarioCanceller();
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
console.warn("cancelScenario called but no scenario canceller provided");
|
|
184
|
+
}
|
|
185
|
+
}, [scenarioCanceller]);
|
|
176
186
|
return useMemo(() => ({
|
|
177
187
|
get,
|
|
178
188
|
subscribe,
|
|
@@ -184,6 +194,7 @@ export function useDataAPI() {
|
|
|
184
194
|
setVolumes,
|
|
185
195
|
getBlockOption,
|
|
186
196
|
updateEmotionEffect,
|
|
197
|
+
cancelScenario,
|
|
187
198
|
}), [
|
|
188
199
|
get,
|
|
189
200
|
subscribe,
|
|
@@ -195,5 +206,6 @@ export function useDataAPI() {
|
|
|
195
206
|
setVolumes,
|
|
196
207
|
getBlockOption,
|
|
197
208
|
updateEmotionEffect,
|
|
209
|
+
cancelScenario,
|
|
198
210
|
]);
|
|
199
211
|
}
|
package/dist/data-api-types.d.ts
CHANGED
|
@@ -248,4 +248,10 @@ export interface DataAPI {
|
|
|
248
248
|
* @param state - 更新する感情エフェクトの状態
|
|
249
249
|
*/
|
|
250
250
|
updateEmotionEffect(state: EmotionEffectState | null): void;
|
|
251
|
+
/**
|
|
252
|
+
* シナリオを中断する
|
|
253
|
+
* プラグインのGameMenuなどから呼び出し、シナリオ再生を中断する。
|
|
254
|
+
* Player の onScenarioCancelled コールバックが発火される。
|
|
255
|
+
*/
|
|
256
|
+
cancelScenario(): void;
|
|
251
257
|
}
|
|
@@ -20,6 +20,9 @@ const calculateDisplayedCharacters = (blocks, currentIndex) => {
|
|
|
20
20
|
positionY: char.positionY,
|
|
21
21
|
zIndex: char.zIndex,
|
|
22
22
|
scale: char.scale,
|
|
23
|
+
cropLeft: char.cropLeft,
|
|
24
|
+
cropRight: char.cropRight,
|
|
25
|
+
cropFade: char.cropFade,
|
|
23
26
|
object: char.object,
|
|
24
27
|
entityState: char.entityState,
|
|
25
28
|
// レイヤー機能用
|
|
@@ -40,13 +40,36 @@ export const useSoundPlayer = ({ soundBlocks, isFirstRenderComplete, muteAudio =
|
|
|
40
40
|
isBGM,
|
|
41
41
|
isVoice,
|
|
42
42
|
]);
|
|
43
|
-
|
|
43
|
+
const bgmFadeIntervalRef = useRef(null);
|
|
44
|
+
// BGMをフェードアウトして停止
|
|
44
45
|
const stopBGM = useCallback(() => {
|
|
45
|
-
if (bgmRef.current)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
if (!bgmRef.current)
|
|
47
|
+
return;
|
|
48
|
+
// 既にフェード中なら前回のをクリア
|
|
49
|
+
if (bgmFadeIntervalRef.current) {
|
|
50
|
+
clearInterval(bgmFadeIntervalRef.current);
|
|
49
51
|
}
|
|
52
|
+
const audio = bgmRef.current.audio;
|
|
53
|
+
const fadeDuration = 1000; // 1秒フェードアウト
|
|
54
|
+
const fadeSteps = 20;
|
|
55
|
+
const stepTime = fadeDuration / fadeSteps;
|
|
56
|
+
const volumeStep = audio.volume / fadeSteps;
|
|
57
|
+
let stepsRemaining = fadeSteps;
|
|
58
|
+
bgmFadeIntervalRef.current = setInterval(() => {
|
|
59
|
+
stepsRemaining--;
|
|
60
|
+
if (stepsRemaining <= 0) {
|
|
61
|
+
if (bgmFadeIntervalRef.current) {
|
|
62
|
+
clearInterval(bgmFadeIntervalRef.current);
|
|
63
|
+
bgmFadeIntervalRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
audio.pause();
|
|
66
|
+
audio.currentTime = 0;
|
|
67
|
+
bgmRef.current = null;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
audio.volume = Math.max(0, volumeStep * stepsRemaining);
|
|
71
|
+
}
|
|
72
|
+
}, stepTime);
|
|
50
73
|
}, []);
|
|
51
74
|
// 特定のSEを停止
|
|
52
75
|
const stopSE = useCallback((soundId) => {
|
|
@@ -185,6 +208,11 @@ export const useSoundPlayer = ({ soundBlocks, isFirstRenderComplete, muteAudio =
|
|
|
185
208
|
// クリーンアップ(直接refにアクセスして依存配列の問題を回避)
|
|
186
209
|
useEffect(() => {
|
|
187
210
|
return () => {
|
|
211
|
+
// フェードintervalをクリア
|
|
212
|
+
if (bgmFadeIntervalRef.current) {
|
|
213
|
+
clearInterval(bgmFadeIntervalRef.current);
|
|
214
|
+
bgmFadeIntervalRef.current = null;
|
|
215
|
+
}
|
|
188
216
|
// BGMを停止
|
|
189
217
|
if (bgmRef.current) {
|
|
190
218
|
bgmRef.current.audio.pause();
|
|
@@ -870,6 +870,9 @@ export class PluginManager {
|
|
|
870
870
|
console.warn("updateEmotionEffect callback is not registered. Make sure to call setEmotionEffectUpdaterCallback.");
|
|
871
871
|
}
|
|
872
872
|
},
|
|
873
|
+
cancelScenario: () => {
|
|
874
|
+
throw new Error("DataAPI.cancelScenario() can only be called from within DataProvider context");
|
|
875
|
+
},
|
|
873
876
|
},
|
|
874
877
|
};
|
|
875
878
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -101,6 +101,9 @@ export interface ScenarioBlockCharacter {
|
|
|
101
101
|
positionY?: number | null;
|
|
102
102
|
zIndex?: number | null;
|
|
103
103
|
scale?: number | null;
|
|
104
|
+
cropLeft?: number | null;
|
|
105
|
+
cropRight?: number | null;
|
|
106
|
+
cropFade?: number | null;
|
|
104
107
|
object: {
|
|
105
108
|
id: string;
|
|
106
109
|
name: string;
|
|
@@ -114,6 +117,9 @@ export interface ScenarioBlockCharacter {
|
|
|
114
117
|
scale?: number;
|
|
115
118
|
translateY?: number;
|
|
116
119
|
translateX?: number;
|
|
120
|
+
cropLeft?: number | null;
|
|
121
|
+
cropRight?: number | null;
|
|
122
|
+
cropFade?: number | null;
|
|
117
123
|
isDefault?: boolean;
|
|
118
124
|
layers?: EntityStateLayer[];
|
|
119
125
|
};
|
|
@@ -201,6 +207,9 @@ export interface DisplayedCharacter {
|
|
|
201
207
|
positionY?: number | null;
|
|
202
208
|
zIndex?: number | null;
|
|
203
209
|
scale?: number | null;
|
|
210
|
+
cropLeft?: number | null;
|
|
211
|
+
cropRight?: number | null;
|
|
212
|
+
cropFade?: number | null;
|
|
204
213
|
object: {
|
|
205
214
|
id: string;
|
|
206
215
|
name: string;
|
|
@@ -214,6 +223,9 @@ export interface DisplayedCharacter {
|
|
|
214
223
|
scale?: number;
|
|
215
224
|
translateY?: number;
|
|
216
225
|
translateX?: number;
|
|
226
|
+
cropLeft?: number | null;
|
|
227
|
+
cropRight?: number | null;
|
|
228
|
+
cropFade?: number | null;
|
|
217
229
|
isDefault?: boolean;
|
|
218
230
|
layers?: EntityStateLayer[];
|
|
219
231
|
};
|
package/package.json
CHANGED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
export var useImagePreloader = function (displayedCharacters, singleImageUrl) {
|
|
3
|
-
var _a = useState(true),
|
|
4
|
-
isLoading = _a[0],
|
|
5
|
-
setIsLoading = _a[1];
|
|
6
|
-
useEffect(
|
|
7
|
-
function () {
|
|
8
|
-
var imageUrls = [];
|
|
9
|
-
// 複数キャラクター表示の場合
|
|
10
|
-
if (displayedCharacters.length > 0) {
|
|
11
|
-
displayedCharacters.forEach(function (char) {
|
|
12
|
-
if (char.entityState.imageUrl) {
|
|
13
|
-
imageUrls.push(char.entityState.imageUrl);
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
} else if (singleImageUrl) {
|
|
17
|
-
// 単一キャラクター表示の場合
|
|
18
|
-
imageUrls.push(singleImageUrl);
|
|
19
|
-
}
|
|
20
|
-
// 画像がない場合は即座に完了
|
|
21
|
-
if (imageUrls.length === 0) {
|
|
22
|
-
setIsLoading(false);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
setIsLoading(true);
|
|
26
|
-
// すべての画像を並列でプリロード
|
|
27
|
-
var loadPromises = imageUrls.map(function (url) {
|
|
28
|
-
return new Promise(function (resolve, reject) {
|
|
29
|
-
var img = new Image();
|
|
30
|
-
img.onload = function () {
|
|
31
|
-
return resolve();
|
|
32
|
-
};
|
|
33
|
-
img.onerror = function () {
|
|
34
|
-
return reject(new Error("Failed to load image: ".concat(url)));
|
|
35
|
-
};
|
|
36
|
-
img.src = url;
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
// すべての画像の読み込みを待つ
|
|
40
|
-
Promise.all(loadPromises)
|
|
41
|
-
.then(function () {
|
|
42
|
-
setIsLoading(false);
|
|
43
|
-
})
|
|
44
|
-
.catch(function (error) {
|
|
45
|
-
console.error("Image preload error:", error);
|
|
46
|
-
// エラーが発生しても続行
|
|
47
|
-
setIsLoading(false);
|
|
48
|
-
});
|
|
49
|
-
},
|
|
50
|
-
[displayedCharacters, singleImageUrl]
|
|
51
|
-
);
|
|
52
|
-
return isLoading;
|
|
53
|
-
};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* プラグインAPIからReactインスタンスを設定
|
|
3
|
-
*/
|
|
4
|
-
export declare function setReactRuntime(react: any): void;
|
|
5
|
-
/**
|
|
6
|
-
* JSX Transform用のjsx関数
|
|
7
|
-
*/
|
|
8
|
-
export declare function jsx(type: any, props: any, key?: any): any;
|
|
9
|
-
/**
|
|
10
|
-
* JSX Transform用のjsxs関数(複数子要素用)
|
|
11
|
-
*/
|
|
12
|
-
export declare function jsxs(type: any, props: any, key?: any): any;
|
|
13
|
-
/**
|
|
14
|
-
* Fragment用
|
|
15
|
-
*/
|
|
16
|
-
export declare function Fragment(props: {
|
|
17
|
-
children?: any;
|
|
18
|
-
}): any;
|
|
19
|
-
/**
|
|
20
|
-
* Reactフックと関数のプロキシ
|
|
21
|
-
*/
|
|
22
|
-
export declare const useState: (...args: any[]) => any;
|
|
23
|
-
export declare const useEffect: (...args: any[]) => any;
|
|
24
|
-
export declare const useCallback: (...args: any[]) => any;
|
|
25
|
-
export declare const useMemo: (...args: any[]) => any;
|
|
26
|
-
export declare const useRef: (...args: any[]) => any;
|
|
27
|
-
export declare const useContext: (...args: any[]) => any;
|
|
28
|
-
export declare const useReducer: (...args: any[]) => any;
|
|
29
|
-
export declare const createElement: (...args: any[]) => any;
|
|
30
|
-
declare const _default: {
|
|
31
|
-
createElement: (...args: any[]) => any;
|
|
32
|
-
Fragment: typeof Fragment;
|
|
33
|
-
useState: (...args: any[]) => any;
|
|
34
|
-
useEffect: (...args: any[]) => any;
|
|
35
|
-
useCallback: (...args: any[]) => any;
|
|
36
|
-
useMemo: (...args: any[]) => any;
|
|
37
|
-
useRef: (...args: any[]) => any;
|
|
38
|
-
useContext: (...args: any[]) => any;
|
|
39
|
-
useReducer: (...args: any[]) => any;
|
|
40
|
-
};
|
|
41
|
-
export default _default;
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
/* eslint-disable import/no-anonymous-default-export */
|
|
3
|
-
let runtimeReact = null;
|
|
4
|
-
/**
|
|
5
|
-
* プラグインAPIからReactインスタンスを設定
|
|
6
|
-
*/
|
|
7
|
-
export function setReactRuntime(react) {
|
|
8
|
-
runtimeReact = react;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* JSX Transform用のjsx関数
|
|
12
|
-
*/
|
|
13
|
-
export function jsx(type, props, key) {
|
|
14
|
-
if (!runtimeReact) {
|
|
15
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
16
|
-
}
|
|
17
|
-
return runtimeReact.createElement(type, key ? Object.assign(Object.assign({}, props), { key }) : props);
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* JSX Transform用のjsxs関数(複数子要素用)
|
|
21
|
-
*/
|
|
22
|
-
export function jsxs(type, props, key) {
|
|
23
|
-
if (!runtimeReact) {
|
|
24
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
25
|
-
}
|
|
26
|
-
return runtimeReact.createElement(type, key ? Object.assign(Object.assign({}, props), { key }) : props);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Fragment用
|
|
30
|
-
*/
|
|
31
|
-
export function Fragment(props) {
|
|
32
|
-
if (!runtimeReact) {
|
|
33
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
34
|
-
}
|
|
35
|
-
return runtimeReact.createElement(runtimeReact.Fragment, null, props.children);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Reactフックと関数のプロキシ
|
|
39
|
-
*/
|
|
40
|
-
export const useState = (...args) => {
|
|
41
|
-
if (!runtimeReact) {
|
|
42
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
43
|
-
}
|
|
44
|
-
return runtimeReact.useState(...args);
|
|
45
|
-
};
|
|
46
|
-
export const useEffect = (...args) => {
|
|
47
|
-
if (!runtimeReact) {
|
|
48
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
49
|
-
}
|
|
50
|
-
return runtimeReact.useEffect(...args);
|
|
51
|
-
};
|
|
52
|
-
export const useCallback = (...args) => {
|
|
53
|
-
if (!runtimeReact) {
|
|
54
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
55
|
-
}
|
|
56
|
-
return runtimeReact.useCallback(...args);
|
|
57
|
-
};
|
|
58
|
-
export const useMemo = (...args) => {
|
|
59
|
-
if (!runtimeReact) {
|
|
60
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
61
|
-
}
|
|
62
|
-
return runtimeReact.useMemo(...args);
|
|
63
|
-
};
|
|
64
|
-
export const useRef = (...args) => {
|
|
65
|
-
if (!runtimeReact) {
|
|
66
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
67
|
-
}
|
|
68
|
-
return runtimeReact.useRef(...args);
|
|
69
|
-
};
|
|
70
|
-
export const useContext = (...args) => {
|
|
71
|
-
if (!runtimeReact) {
|
|
72
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
73
|
-
}
|
|
74
|
-
return runtimeReact.useContext(...args);
|
|
75
|
-
};
|
|
76
|
-
export const useReducer = (...args) => {
|
|
77
|
-
if (!runtimeReact) {
|
|
78
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
79
|
-
}
|
|
80
|
-
return runtimeReact.useReducer(...args);
|
|
81
|
-
};
|
|
82
|
-
export const createElement = (...args) => {
|
|
83
|
-
if (!runtimeReact) {
|
|
84
|
-
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
85
|
-
}
|
|
86
|
-
return runtimeReact.createElement(...args);
|
|
87
|
-
};
|
|
88
|
-
// デフォルトエクスポート(互換性用)
|
|
89
|
-
export default {
|
|
90
|
-
createElement,
|
|
91
|
-
Fragment,
|
|
92
|
-
useState,
|
|
93
|
-
useEffect,
|
|
94
|
-
useCallback,
|
|
95
|
-
useMemo,
|
|
96
|
-
useRef,
|
|
97
|
-
useContext,
|
|
98
|
-
useReducer,
|
|
99
|
-
};
|