@next2d/builder 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,7 @@ Xbox対応は試作・開発段階です。
31
31
 
32
32
  | ガイド | 内容 |
33
33
  |---|---|
34
- | [Electron / Steam](docs/steam.md#日本語) | デスクトップ書き出し、アイコン、CPU、署名、Steam Depot、アップロード、ベータテスト |
34
+ | [Electron / Steam](docs/steam.md#日本語) | デスクトップ書き出し、アイコン、CPU、オーバーレイウィンドウ、署名、Steam Depot、アップロード、ベータテスト |
35
35
  | [ネイティブ機能の拡張](docs/native-bridge.md) | OS API・任意SDKとの連携、設定、プロトコル、実装例(OS情報取得・EOS) |
36
36
  | [iOS / Android(Capacitor)](docs/capacitor.md#日本語) | ネイティブ設定、SDK、プラグイン、ビルドコマンド、移行、Xcodeの設定 |
37
37
  | [Xbox(試作)](docs/xbox.md#日本語) | 開発状況、ホスト生成、ビルド環境、検証範囲 |
@@ -82,7 +82,7 @@ These guides cover platform-specific configuration, dependencies, builds and dis
82
82
 
83
83
  | Guide | Contents |
84
84
  |---|---|
85
- | [Electron / Steam](docs/steam.md#english) | Desktop exports, icons, architectures, signing, Steam depots, uploads and beta testing |
85
+ | [Electron / Steam](docs/steam.md#english) | Desktop exports, icons, architectures, overlay windows, signing, Steam depots, uploads and beta testing |
86
86
  | [Native extensions (Japanese)](docs/native-bridge.md) | OS APIs and SDK integration, configuration, protocol, and examples (system information / EOS) |
87
87
  | [iOS / Android (Capacitor)](docs/capacitor.md#english) | Native configuration, SDKs, plugins, build commands, migration and Xcode setup |
88
88
  | [Xbox (prototype)](docs/xbox.md#english) | Development status, host generation, build environment and validation scope |
@@ -1,5 +1,10 @@
1
1
  import type { INativeBridgeConfig } from "./electron-native.js";
2
2
  export type ElectronOS = "windows" | "macos" | "linux";
3
+ export interface IElectronOverlayConfig {
4
+ enabled: boolean;
5
+ anchor: "bottom";
6
+ clickThrough: boolean;
7
+ }
3
8
  export interface IElectronConfig {
4
9
  nativeBridge?: INativeBridgeConfig;
5
10
  appId: string;
@@ -13,6 +18,7 @@ export interface IElectronConfig {
13
18
  width: number;
14
19
  height: number;
15
20
  fullscreen: boolean;
21
+ overlay?: IElectronOverlayConfig;
16
22
  };
17
23
  steam?: {
18
24
  appId: string | null;
@@ -56,6 +56,24 @@ export const readElectronConfig = (root) => {
56
56
  throw new Error("Electron fullscreen/sign/notarize options must be booleans.");
57
57
  }
58
58
  }
59
+ const overlay = config.window.overlay;
60
+ if (overlay !== undefined) {
61
+ if (!overlay || typeof overlay !== "object" || Array.isArray(overlay)) {
62
+ throw new Error("electron.config.json window.overlay must be an object.");
63
+ }
64
+ config.window.overlay = { "enabled": false, "anchor": "bottom", "clickThrough": false, ...overlay };
65
+ const { enabled, anchor, clickThrough } = config.window.overlay;
66
+ if (typeof enabled !== "boolean" || typeof clickThrough !== "boolean") {
67
+ throw new Error("electron.config.json window.overlay.enabled/clickThrough must be booleans.");
68
+ }
69
+ if (anchor !== "bottom") {
70
+ throw new Error("electron.config.json window.overlay.anchor must be \"bottom\".");
71
+ }
72
+ // A transparent desktop overlay is never a fullscreen game window.
73
+ if (enabled) {
74
+ config.window.fullscreen = false;
75
+ }
76
+ }
59
77
  if (config.macos.notarize && !config.macos.sign) {
60
78
  throw new Error("macos.notarize requires macos.sign.");
61
79
  }
package/docs/steam.md CHANGED
@@ -78,6 +78,33 @@ Steamは複数Depotに対応。builderは各OSに1 IDまでで、一部OSだけ
78
78
  | `macos.sign` / `macos.notarize` | ローカル検証は `false`、配布時は両方 `true`。 |
79
79
  | `macos.localNetworkUsageDescription` | LAN通信の利用目的。指定時のみ、メインアプリとElectron HelperのInfo.plistへ`NSLocalNetworkUsageDescription`を設定する。macOSの許可操作はユーザーが行う。 |
80
80
 
81
+ #### オーバーレイウィンドウ
82
+
83
+ デスクトップ上に常駐するマスコット等向けに、透明・枠なし・最前面のウィンドウを作れます。`window.overlay` 未指定または `enabled: false` なら従来どおりです。
84
+
85
+ ```json
86
+ "window": {
87
+ "width": 1920, "height": 420, "fullscreen": false,
88
+ "overlay": { "enabled": true, "anchor": "bottom", "clickThrough": true }
89
+ }
90
+ ```
91
+
92
+ | 項目 | 内容 |
93
+ |---|---|
94
+ | `enabled` | `true` で透明・枠なし・リサイズ不可・影なし・最前面(macOSは全ワークスペース・全画面アプリ上にも表示)。タスクバーには表示。`fullscreen` は `false` 扱い、F11/Alt+Enterは無効。既定 `false` |
95
+ | `anchor` | `"bottom"` のみ(既定)。主ディスプレイの作業領域(タスクバー・Dockを除く)の下端・水平中央に配置。幅・高さは作業領域を超えない。ディスプレイ構成の変更時に再配置 |
96
+ | `clickThrough` | `true` で起動時からクリック透過(マウス移動は届く)。既定 `false` |
97
+
98
+ 有効時のみ、rendererへ `window.next2dWindow` を公開します(アプリのメインフレームのみ)。
99
+
100
+ ```ts
101
+ await window.next2dWindow?.setIgnoreMouseEvents(false); // キャラクター上では操作を受け付ける
102
+ const { x, y, width, height, scaleFactor } = await window.next2dWindow.getBounds();
103
+ await window.next2dWindow.quit();
104
+ ```
105
+
106
+ Linuxの透明表示はコンポジタが必要です。
107
+
81
108
  ### STEP4:認証を用意する
82
109
 
83
110
  #### macOSの署名・公証
@@ -267,6 +294,33 @@ Edit the configuration at the game root. The version comes from the root `packag
267
294
  | `steam.branch` | Target branch; defaults to `internal`. |
268
295
  | `macos.sign` / `macos.notarize` | Both `false` for local testing; both `true` for distribution. |
269
296
 
297
+ #### Overlay window
298
+
299
+ Creates a transparent, frameless, always-on-top window for desktop companions and similar apps. Without `window.overlay`, or with `enabled: false`, the window behaves as before.
300
+
301
+ ```json
302
+ "window": {
303
+ "width": 1920, "height": 420, "fullscreen": false,
304
+ "overlay": { "enabled": true, "anchor": "bottom", "clickThrough": true }
305
+ }
306
+ ```
307
+
308
+ | Field | Behaviour |
309
+ |---|---|
310
+ | `enabled` | `true` makes the window transparent, frameless, non-resizable, shadowless and always on top (on macOS also on every workspace and above fullscreen apps). It stays in the taskbar. `fullscreen` is treated as `false`; F11 / Alt+Enter are disabled. Default `false`. |
311
+ | `anchor` | Only `"bottom"` (default): bottom edge of the primary display's work area (excluding taskbar/Dock), centred horizontally, never larger than the work area. Repositioned when displays change. |
312
+ | `clickThrough` | `true` starts with mouse clicks passing through (mouse moves are still forwarded). Default `false`. |
313
+
314
+ Only when enabled, the renderer gets `window.next2dWindow` (app main frame only):
315
+
316
+ ```ts
317
+ await window.next2dWindow?.setIgnoreMouseEvents(false); // accept input while over the character
318
+ const { x, y, width, height, scaleFactor } = await window.next2dWindow.getBounds();
319
+ await window.next2dWindow.quit();
320
+ ```
321
+
322
+ Transparency on Linux requires a compositor.
323
+
270
324
  ### STEP4: Prepare credentials
271
325
 
272
326
  #### macOS signing and notarization
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next2d/builder",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Multi-platform builder for Next2d Framework, supporting export to various platforms such as macOS, Windows, iOS, Android and Web(HTML)",
5
5
  "author": "Toshiyuki Ienaga <ienaga@next2d.app>",
6
6
  "license": "MIT",
@@ -42,13 +42,13 @@
42
42
  "devDependencies": {
43
43
  "@eslint/eslintrc": "^3.3.7",
44
44
  "@eslint/js": "^10.0.1",
45
- "@types/node": "^26.6.4",
46
- "@typescript-eslint/eslint-plugin": "^8.71.0",
47
- "@typescript-eslint/parser": "^8.71.0",
48
- "eslint": "^10.11.0",
45
+ "@types/node": "^26.6.6",
46
+ "@typescript-eslint/eslint-plugin": "^8.72.1",
47
+ "@typescript-eslint/parser": "^8.72.1",
48
+ "eslint": "^10.14.0",
49
49
  "eslint-plugin-unused-imports": "^4.4.1",
50
50
  "globals": "^17.13.0",
51
51
  "typescript": "^6.0.3",
52
- "vite": "^8.3.2"
52
+ "vite": "^8.3.5"
53
53
  }
54
54
  }
@@ -24,25 +24,65 @@ protocol.registerSchemesAsPrivileged([{
24
24
  }]);
25
25
 
26
26
  const startURL = "next2d://game/index.html";
27
+ const overlay = config.window.overlay?.enabled ? config.window.overlay : null;
27
28
  let mainWindow;
29
+ // Bottom anchor: flush with the primary display's work area (above the taskbar/Dock), centred.
30
+ const overlayBounds = () => {
31
+ const { workArea } = screen.getPrimaryDisplay();
32
+ const width = Math.min(config.window.width, workArea.width);
33
+ const height = Math.min(config.window.height, workArea.height);
34
+ return {
35
+ x: workArea.x + Math.floor((workArea.width - width) / 2),
36
+ y: workArea.y + workArea.height - height,
37
+ width, height
38
+ };
39
+ };
40
+ const placeOverlay = () => {
41
+ if (mainWindow && !mainWindow.isDestroyed()) mainWindow.setBounds(overlayBounds());
42
+ };
28
43
  const createWindow = async () => {
29
44
  const { width, height } = screen.getPrimaryDisplay().workAreaSize;
45
+ // The preload reads these flags; each API is exposed only when its feature is configured.
46
+ const preloadFlags = [
47
+ ...(config.nativeBridge ? ["--next2d-native-bridge"] : []),
48
+ ...(overlay ? ["--next2d-overlay"] : [])
49
+ ];
30
50
  mainWindow = new BrowserWindow({
31
51
  width: Math.min(config.window.width, width),
32
52
  height: Math.min(config.window.height, height),
33
53
  title: config.appName,
34
- fullscreen: config.window.fullscreen,
54
+ fullscreen: overlay ? false : config.window.fullscreen,
35
55
  ...(config.icon ? { icon: path.join(assetRoot, config.icon) } : {}),
36
56
  useContentSize: true,
37
57
  backgroundColor: "#000000",
38
58
  autoHideMenuBar: true,
39
59
  show: false,
60
+ ...(overlay ? {
61
+ ...overlayBounds(), transparent: true, frame: false, resizable: false, hasShadow: false,
62
+ fullscreenable: false, skipTaskbar: false, backgroundColor: "#00000000",
63
+ // The overlay must never take keyboard focus or activation from the apps under it (Word etc.).
64
+ focusable: false
65
+ } : {}),
40
66
  webPreferences: {
41
67
  nodeIntegration: false, contextIsolation: true, sandbox: true,
42
- ...(config.nativeBridge ? { preload: path.join(__dirname, "preload.cjs"),
43
- backgroundThrottling: config.nativeBridge.backgroundThrottling ?? false } : {})
68
+ ...(preloadFlags.length ? { preload: path.join(__dirname, "preload.cjs"), additionalArguments: preloadFlags } : {}),
69
+ ...(config.nativeBridge ? { backgroundThrottling: config.nativeBridge.backgroundThrottling ?? false } : {})
44
70
  }
45
71
  });
72
+ if (overlay) {
73
+ mainWindow.setAlwaysOnTop(true, "floating");
74
+ if (process.platform === "darwin") {
75
+ mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
76
+ }
77
+ if (overlay.clickThrough) {
78
+ mainWindow.setIgnoreMouseEvents(true, { forward: true });
79
+ }
80
+ const events = ["display-metrics-changed", "display-added", "display-removed"];
81
+ for (const name of events) screen.on(name, placeOverlay);
82
+ mainWindow.once("closed", () => {
83
+ for (const name of events) screen.removeListener(name, placeOverlay);
84
+ });
85
+ }
46
86
  mainWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
47
87
  mainWindow.webContents.on("will-navigate", (event, url) => {
48
88
  if (!resolveAssetPath(assetRoot, url)) {
@@ -50,7 +90,7 @@ const createWindow = async () => {
50
90
  }
51
91
  });
52
92
  mainWindow.webContents.on("before-input-event", (event, input) => {
53
- if (input.type !== "keyDown") {
93
+ if (input.type !== "keyDown" || overlay) {
54
94
  return;
55
95
  }
56
96
  if (input.key === "F11" || (input.alt && input.key === "Enter")) {
@@ -66,7 +106,7 @@ const createWindow = async () => {
66
106
  closeNativeBridge();
67
107
  app.exit(1);
68
108
  });
69
- mainWindow.once("ready-to-show", () => mainWindow.show());
109
+ mainWindow.once("ready-to-show", () => (overlay ? mainWindow.showInactive() : mainWindow.show()));
70
110
  await mainWindow.loadURL(startURL);
71
111
  };
72
112
 
@@ -75,9 +115,9 @@ app.whenReady().then(async () => {
75
115
  { role: "appMenu" }, { role: "editMenu" }, { role: "windowMenu" }
76
116
  ]) : null);
77
117
  session.defaultSession.setPermissionRequestHandler((_contents, permission, callback) => {
78
- callback(permission === "fullscreen");
118
+ callback(!overlay && permission === "fullscreen");
79
119
  });
80
- session.defaultSession.setPermissionCheckHandler((_contents, permission) => permission === "fullscreen");
120
+ session.defaultSession.setPermissionCheckHandler((_contents, permission) => !overlay && permission === "fullscreen");
81
121
  protocol.handle("next2d", async (request) => {
82
122
  let asset = resolveAssetPath(assetRoot, request.url);
83
123
  if (!asset || request.method !== "GET") {
@@ -109,6 +149,26 @@ app.whenReady().then(async () => {
109
149
  return nativeBridge.request(method, params);
110
150
  });
111
151
  }
152
+ if (overlay) {
153
+ const trusted = (event) => {
154
+ if (!isTrustedSender(event, mainWindow)) throw new Error("Window API access denied");
155
+ };
156
+ ipcMain.handle("next2d:window:set-ignore-mouse-events", (event, ignore) => {
157
+ trusted(event);
158
+ if (typeof ignore !== "boolean") throw new TypeError("ignore must be a boolean");
159
+ mainWindow.setIgnoreMouseEvents(ignore, ignore ? { forward: true } : undefined);
160
+ });
161
+ ipcMain.handle("next2d:window:get-bounds", (event) => {
162
+ trusted(event);
163
+ const bounds = mainWindow.getBounds();
164
+ return { ...bounds, scaleFactor: screen.getDisplayMatching(bounds).scaleFactor };
165
+ });
166
+ ipcMain.handle("next2d:window:quit", (event) => {
167
+ trusted(event);
168
+ // Reply before the window is torn down.
169
+ setImmediate(() => app.quit());
170
+ });
171
+ }
112
172
  await createWindow();
113
173
  }).catch((error) => {
114
174
  console.error(error);
@@ -1,11 +1,26 @@
1
1
  const { contextBridge, ipcRenderer } = require("electron");
2
+ // Set by the main process through webPreferences.additionalArguments.
3
+ const flags = new Set(process.argv);
2
4
 
3
- contextBridge.exposeInMainWorld("next2dNative", {
4
- request: (method, params) => ipcRenderer.invoke("next2d:native:request", method, params),
5
- onEvent: (listener) => {
6
- if (typeof listener !== "function") throw new TypeError("Expected event listener");
7
- const handler = (_event, message) => listener(message);
8
- ipcRenderer.on("next2d:native:event", handler);
9
- return () => ipcRenderer.removeListener("next2d:native:event", handler);
10
- }
11
- });
5
+ if (flags.has("--next2d-native-bridge")) {
6
+ contextBridge.exposeInMainWorld("next2dNative", {
7
+ request: (method, params) => ipcRenderer.invoke("next2d:native:request", method, params),
8
+ onEvent: (listener) => {
9
+ if (typeof listener !== "function") throw new TypeError("Expected event listener");
10
+ const handler = (_event, message) => listener(message);
11
+ ipcRenderer.on("next2d:native:event", handler);
12
+ return () => ipcRenderer.removeListener("next2d:native:event", handler);
13
+ }
14
+ });
15
+ }
16
+
17
+ if (flags.has("--next2d-overlay")) {
18
+ contextBridge.exposeInMainWorld("next2dWindow", {
19
+ setIgnoreMouseEvents: async (ignore) => {
20
+ if (typeof ignore !== "boolean") throw new TypeError("Expected boolean");
21
+ await ipcRenderer.invoke("next2d:window:set-ignore-mouse-events", ignore);
22
+ },
23
+ getBounds: () => ipcRenderer.invoke("next2d:window:get-bounds"),
24
+ quit: async () => { await ipcRenderer.invoke("next2d:window:quit"); }
25
+ });
26
+ }