@luna-editor/engine 0.3.0 → 0.3.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.
@@ -10,115 +10,137 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { useEffect, useRef, useState } from "react";
11
11
  // グローバルなフォントキャッシュ(ページ内で共有)
12
12
  const fontCache = new Set();
13
+ /**
14
+ * Googleフォントの<link>タグを挿入し、CSSの読み込み完了を待つ
15
+ */
16
+ function loadGoogleFontCSS(fontFamily) {
17
+ const existingLink = document.querySelector(`link[data-font-family="${fontFamily}"]`);
18
+ if (existingLink) {
19
+ return Promise.resolve();
20
+ }
21
+ const link = document.createElement("link");
22
+ link.rel = "stylesheet";
23
+ // display=blockでフォント読み込みまでテキストを非表示にする(FOUTを防止)
24
+ link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@400;700&display=block`;
25
+ link.setAttribute("data-font-family", fontFamily);
26
+ document.head.appendChild(link);
27
+ return new Promise((resolve) => {
28
+ link.onload = () => resolve();
29
+ link.onerror = () => {
30
+ console.warn(`Failed to load Google font CSS: ${fontFamily}`);
31
+ resolve();
32
+ };
33
+ });
34
+ }
35
+ /**
36
+ * Googleフォントのフォントデータを事前にダウンロード
37
+ * 400/700両方のウェイトをプリロードする
38
+ */
39
+ function preloadGoogleFont(fontFamily, textToPreload) {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ try {
42
+ // 400と700両方のウェイトをプリロード
43
+ yield Promise.all([
44
+ document.fonts.load(`400 16px "${fontFamily}"`, textToPreload),
45
+ document.fonts.load(`700 16px "${fontFamily}"`, textToPreload),
46
+ ]);
47
+ }
48
+ catch (error) {
49
+ console.warn(`Failed to preload Google font: ${fontFamily}`, error);
50
+ }
51
+ });
52
+ }
53
+ /**
54
+ * カスタムフォントをFontFace APIで読み込み
55
+ */
56
+ function loadCustomFont(fontFamily, url, fontName) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ try {
59
+ // 既にdocument.fontsに登録されているかチェック
60
+ let fontExists = false;
61
+ for (const existingFont of document.fonts) {
62
+ if (existingFont.family === fontFamily) {
63
+ fontExists = true;
64
+ break;
65
+ }
66
+ }
67
+ if (!fontExists) {
68
+ const fontFace = new FontFace(fontFamily, `url(${url})`);
69
+ yield fontFace.load();
70
+ document.fonts.add(fontFace);
71
+ }
72
+ }
73
+ catch (error) {
74
+ console.warn(`Failed to load custom font: ${fontName}`, error);
75
+ }
76
+ });
77
+ }
13
78
  /**
14
79
  * フォントを読み込むカスタムフック
15
80
  * - Googleフォント: <link>タグで読み込み + document.fonts.load()でプリロード
16
81
  * - カスタムフォント: FontFace APIで読み込み
17
- * - すべてのフォントがプリロード&キャッシュされてからisLoaded=trueになる
82
+ * - すべてのフォントが完全にダウンロード&キャッシュされてからisLoaded=trueになる
83
+ * - 再生開始前に全フォントを一括ダウンロードし、再生中のフォントスワップを防止する
84
+ * - preloadText: シナリオ内の全テキストを渡すと、使用される全ての文字でフォントをプリロード
18
85
  */
19
- export function useFontLoader(fonts) {
86
+ export function useFontLoader(fonts, preloadText) {
20
87
  const [isLoaded, setIsLoaded] = useState(false);
21
88
  const [loadedFonts, setLoadedFonts] = useState([]);
22
- const loadingRef = useRef(false);
89
+ // 現在のロード処理をキャンセルするための世代カウンター
90
+ const generationRef = useRef(0);
23
91
  useEffect(() => {
24
92
  if (!fonts || fonts.length === 0) {
25
93
  setIsLoaded(true);
26
94
  return;
27
95
  }
28
- // 既に読み込み中の場合はスキップ
29
- if (loadingRef.current)
30
- return;
31
- loadingRef.current = true;
96
+ // 新しいロード処理を開始する際、前回の結果を無効化
97
+ const generation = ++generationRef.current;
98
+ setIsLoaded(false);
32
99
  const loadFonts = () => __awaiter(this, void 0, void 0, function* () {
100
+ var _a;
33
101
  const loadedFontFamilies = [];
34
- const fontLoadPromises = [];
35
- for (const font of fonts) {
36
- // キャッシュにあればスキップ
37
- if (fontCache.has(font.fontFamily)) {
102
+ const textToPreload = preloadText || "あいうえおABCabc123";
103
+ // すべてのGoogle FontsのCSS読み込みを並列で開始
104
+ const googleFonts = fonts.filter((f) => f.type === "google" && !fontCache.has(f.fontFamily));
105
+ const customFonts = fonts.filter((f) => f.type === "custom" && f.url && !fontCache.has(f.fontFamily));
106
+ const cachedFonts = fonts.filter((f) => fontCache.has(f.fontFamily));
107
+ // キャッシュ済みフォントは即座に追加
108
+ for (const font of cachedFonts) {
109
+ loadedFontFamilies.push(font.fontFamily);
110
+ }
111
+ // Google FontsのCSS読み込みを並列実行
112
+ yield Promise.all(googleFonts.map((font) => loadGoogleFontCSS(font.fontFamily)));
113
+ // キャンセルチェック
114
+ if (generation !== generationRef.current)
115
+ return;
116
+ // Google Fontsのフォントデータプリロード + カスタムフォント読み込みを並列実行
117
+ const allFontPromises = [];
118
+ for (const font of googleFonts) {
119
+ allFontPromises.push(preloadGoogleFont(font.fontFamily, textToPreload).then(() => {
120
+ fontCache.add(font.fontFamily);
38
121
  loadedFontFamilies.push(font.fontFamily);
39
- continue;
40
- }
41
- try {
42
- if (font.type === "google") {
43
- // Googleフォントを<link>タグで読み込み
44
- const existingLink = document.querySelector(`link[data-font-family="${font.fontFamily}"]`);
45
- if (!existingLink) {
46
- const link = document.createElement("link");
47
- link.rel = "stylesheet";
48
- link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(font.fontFamily)}:wght@400;700&display=swap`;
49
- link.setAttribute("data-font-family", font.fontFamily);
50
- document.head.appendChild(link);
51
- // CSSファイルの読み込み完了を待つ
52
- yield new Promise((resolve) => {
53
- link.onload = () => resolve();
54
- link.onerror = () => {
55
- console.warn(`Failed to load Google font CSS: ${font.fontFamily}`);
56
- resolve();
57
- };
58
- });
59
- }
60
- // フォントの実際の読み込み(プリロード)を開始
61
- // document.fonts.load()でフォントデータを事前にダウンロード
62
- const preloadPromise = document.fonts
63
- .load(`16px "${font.fontFamily}"`)
64
- .then(() => {
65
- fontCache.add(font.fontFamily);
66
- loadedFontFamilies.push(font.fontFamily);
67
- })
68
- .catch((error) => {
69
- console.warn(`Failed to preload Google font: ${font.fontFamily}`, error);
70
- // エラーでもキャッシュに追加(再試行防止)
71
- fontCache.add(font.fontFamily);
72
- loadedFontFamilies.push(font.fontFamily);
73
- });
74
- fontLoadPromises.push(preloadPromise);
75
- }
76
- else if (font.type === "custom" && font.url) {
77
- // カスタムフォントをFontFace APIで読み込み
78
- const loadCustomFont = () => __awaiter(this, void 0, void 0, function* () {
79
- try {
80
- // 既にdocument.fontsに登録されているかチェック
81
- let fontExists = false;
82
- for (const existingFont of document.fonts) {
83
- if (existingFont.family === font.fontFamily) {
84
- fontExists = true;
85
- break;
86
- }
87
- }
88
- if (!fontExists) {
89
- const fontFace = new FontFace(font.fontFamily, `url(${font.url})`);
90
- // フォントデータをダウンロード&パース
91
- yield fontFace.load();
92
- // document.fontsに追加(キャッシュとして機能)
93
- document.fonts.add(fontFace);
94
- }
95
- fontCache.add(font.fontFamily);
96
- loadedFontFamilies.push(font.fontFamily);
97
- }
98
- catch (error) {
99
- console.warn(`Failed to load custom font: ${font.name}`, error);
100
- // エラーでもキャッシュに追加(再試行防止)
101
- fontCache.add(font.fontFamily);
102
- loadedFontFamilies.push(font.fontFamily);
103
- }
104
- });
105
- fontLoadPromises.push(loadCustomFont());
106
- }
107
- }
108
- catch (error) {
109
- console.warn(`Failed to load font: ${font.name}`, error);
110
- }
122
+ }));
123
+ }
124
+ for (const font of customFonts) {
125
+ allFontPromises.push(loadCustomFont(font.fontFamily, (_a = font.url) !== null && _a !== void 0 ? _a : "", font.name).then(() => {
126
+ fontCache.add(font.fontFamily);
127
+ loadedFontFamilies.push(font.fontFamily);
128
+ }));
111
129
  }
112
- // すべてのフォント読み込みを並列で待つ
113
- yield Promise.all(fontLoadPromises);
130
+ yield Promise.all(allFontPromises);
131
+ // キャンセルチェック
132
+ if (generation !== generationRef.current)
133
+ return;
114
134
  // document.fonts.readyで全フォントの準備完了を確認
115
135
  yield document.fonts.ready;
136
+ // キャンセルチェック
137
+ if (generation !== generationRef.current)
138
+ return;
116
139
  setLoadedFonts(loadedFontFamilies);
117
140
  setIsLoaded(true);
118
- loadingRef.current = false;
119
141
  });
120
142
  loadFonts();
121
- }, [fonts]);
143
+ }, [fonts, preloadText]);
122
144
  return { isLoaded, loadedFonts };
123
145
  }
124
146
  /**
@@ -139,6 +161,23 @@ export function getFontFamilyStyle(selectedFontFamily, fonts) {
139
161
  // 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
140
162
  return `"${fonts[0].fontFamily}", sans-serif`;
141
163
  }
164
+ /**
165
+ * UIフォントファミリーに基づいてCSSのfont-family値を生成
166
+ * メニュー、ボタン、ラベルなどのUI要素に使用
167
+ */
168
+ export function getUIFontFamilyStyle(selectedUIFontFamily, fonts) {
169
+ if (!fonts || fonts.length === 0) {
170
+ return "sans-serif";
171
+ }
172
+ if (selectedUIFontFamily) {
173
+ const selectedFont = fonts.find((f) => f.fontFamily === selectedUIFontFamily);
174
+ if (selectedFont) {
175
+ return `"${selectedUIFontFamily}", sans-serif`;
176
+ }
177
+ }
178
+ // 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
179
+ return `"${fonts[0].fontFamily}", sans-serif`;
180
+ }
142
181
  /**
143
182
  * フォントがキャッシュに存在するかチェック
144
183
  */
@@ -0,0 +1,5 @@
1
+ import type { DisplayedCharacter } from "../types";
2
+ export declare const useImagePreloader: (
3
+ displayedCharacters: DisplayedCharacter[],
4
+ singleImageUrl?: string
5
+ ) => boolean;
@@ -0,0 +1,53 @@
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
+ };
@@ -37,6 +37,6 @@ export function useUIVisibility(type) {
37
37
  // UI 状態の変更を監視
38
38
  const unsubscribe = pluginManager.subscribeUIVisibility(type, setIsVisible);
39
39
  return unsubscribe;
40
- }, [pluginManager, type]);
40
+ }, [type]);
41
41
  return isVisible;
42
42
  }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { aspectRatio, BasisHeight, BasisWidth } from "./constants/screen-size";
5
5
  export type { AudioSettings } from "./contexts/AudioContext";
6
6
  export { useAudioSettings } from "./contexts/AudioContext";
7
7
  export { useDataAPI } from "./contexts/DataContext";
8
+ export { getFontFamilyStyle, getUIFontFamilyStyle, } from "./hooks/useFontLoader";
8
9
  export { setGlobalUIAPI, usePluginAPI, useUIVisibility, } from "./hooks/usePluginAPI";
9
10
  export { useScreenScale, useScreenSize, useToPixel, } from "./hooks/useScreenSize";
10
11
  export { Player } from "./Player";
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { OverlayUI } from "./components/OverlayUI";
5
5
  export { aspectRatio, BasisHeight, BasisWidth } from "./constants/screen-size";
6
6
  export { useAudioSettings } from "./contexts/AudioContext";
7
7
  export { useDataAPI } from "./contexts/DataContext";
8
+ export { getFontFamilyStyle, getUIFontFamilyStyle, } from "./hooks/useFontLoader";
8
9
  export { setGlobalUIAPI, usePluginAPI, useUIVisibility, } from "./hooks/usePluginAPI";
9
10
  export { useScreenScale, useScreenSize, useToPixel, } from "./hooks/useScreenSize";
10
11
  export { Player } from "./Player";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { type ActionExecutionContext, type ActionNodeDefinition, type BlockChangeContext, type CharacterContext, type CharacterSpeakContext, ComponentType, type DialogueContext, type LunaPlugin, type ScenarioContext, type ScenarioReadyContext, type UIAPI } from "../sdk";
2
+ import { type ActionExecutionContext, type ActionNodeDefinition, type BlockChangeContext, type CharacterContext, type CharacterSpeakContext, ComponentType, type DataAPI, type DialogueContext, type LunaPlugin, type ScenarioContext, type ScenarioReadyContext, type UIAPI } from "../sdk";
3
3
  export interface LoadedPlugin {
4
4
  plugin: LunaPlugin;
5
5
  instance: PluginInstance;
@@ -27,12 +27,15 @@ export interface PluginInstance {
27
27
  styles: Map<string, string[]>;
28
28
  effects: Map<string, HTMLElement>;
29
29
  overlays: Map<string, HTMLElement>;
30
- components: Map<ComponentType, React.ComponentType<unknown>>;
30
+ components: Map<ComponentType, React.ComponentType<{
31
+ dataAPI?: DataAPI;
32
+ }>>;
31
33
  assetUrls: Map<string, string>;
32
34
  }
33
35
  export declare class PluginManager {
34
36
  private plugins;
35
37
  private styleElements;
38
+ private originalClassNames;
36
39
  private injectedStyles;
37
40
  private storage;
38
41
  private componentRegistry;
@@ -40,6 +43,8 @@ export declare class PluginManager {
40
43
  private uiVisibilityListeners;
41
44
  private selectChoiceCallback;
42
45
  private getBranchStateCallback;
46
+ private emotionEffectUpdaterCallback;
47
+ private dataContextGetter;
43
48
  private preloadPromises;
44
49
  private muteAudio;
45
50
  private workSounds;
@@ -47,12 +52,20 @@ export declare class PluginManager {
47
52
  private clickSoundVolume;
48
53
  private clickSoundOverridden;
49
54
  constructor();
55
+ /**
56
+ * 開発環境かどうかを判定
57
+ * localhost、127.0.0.1、またはポート3000で実行されている場合に true を返す
58
+ */
59
+ private isDevelopment;
50
60
  /**
51
61
  * グローバルReactランタイムを初期化(クライアントサイドのみ)
52
62
  * 既に初期化済みの場合はスキップ
53
63
  */
54
64
  private initializeReactRuntime;
55
- loadPlugin(packageName: string, bundleUrl: string, config?: unknown): Promise<void>;
65
+ loadPlugin(packageName: string, bundleUrl: string, config?: unknown, assets?: {
66
+ filename: string;
67
+ url: string;
68
+ }[]): Promise<void>;
56
69
  private doLoadPlugin;
57
70
  private loadPluginScript;
58
71
  private createPluginAPI;
@@ -65,7 +78,9 @@ export declare class PluginManager {
65
78
  setPluginEnabled(packageName: string, enabled: boolean): void;
66
79
  cleanup(): void;
67
80
  private findPluginByComponentType;
68
- getComponent(type: ComponentType): React.ComponentType | null;
81
+ getComponent(type: ComponentType): React.ComponentType<{
82
+ dataAPI?: DataAPI;
83
+ }> | null;
69
84
  hasComponent(type: ComponentType): boolean;
70
85
  getRegisteredComponents(): ComponentType[];
71
86
  /**
@@ -119,6 +134,8 @@ export declare class PluginManager {
119
134
  hideUI(type: ComponentType): void;
120
135
  setSelectChoiceCallback(callback: (choiceId: string) => void): void;
121
136
  setGetBranchStateCallback(callback: () => import("../types").ConversationBranchState | null): void;
137
+ setEmotionEffectUpdaterCallback(callback: (state: import("../sdk").EmotionEffectState | null) => void): void;
138
+ setDataContextGetter(callback: () => import("../sdk").DataContext | null): void;
122
139
  /**
123
140
  * 全てのアセットプリロードが完了するまで待機
124
141
  * @returns プリロード完了時にresolveするPromise