From 349a9ddc0f640eb7bee4110bed83f242b0249c3b Mon Sep 17 00:00:00 2001 From: Ann Date: Fri, 7 Mar 2025 00:25:55 +0300 Subject: [PATCH] wm 1.0.0, wm demo, project structure --- .gitignore | 1 + README.md | 2 +- package.json | 29 +++++ scripts/finalizeBuild.js | 19 +++ src/index.ts | 60 +++++++++ src/lib/animations.ts | 4 + src/lib/windowManager.ts | 229 +++++++++++++++++++++++++++++++++ src/screens/crash/index.html | 9 ++ src/screens/mainmenu/test.html | 9 ++ src/types/wm.d.ts | 60 +++++++++ src/types/wm.ts | 43 +++++++ tsconfig.json | 14 ++ 12 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 package.json create mode 100644 scripts/finalizeBuild.js create mode 100644 src/index.ts create mode 100644 src/lib/animations.ts create mode 100644 src/lib/windowManager.ts create mode 100644 src/screens/crash/index.html create mode 100644 src/screens/mainmenu/test.html create mode 100644 src/types/wm.d.ts create mode 100644 src/types/wm.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index ceaea36..eab6e84 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* +pnpm-lock.yaml # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/README.md b/README.md index 07a56ac..75fd132 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # neofunkin -[planned] Electron-based Friday Night Funkin' engine with multi-window capabilities \ No newline at end of file +[RAW WIP] Electron-based Friday Night Funkin' engine with multi-window capabilities \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e19caa9 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "neofunkin-1", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "tsc && node scripts/finalizeBuild.js", + "electron": "electron dist" + }, + "keywords": [], + "author": "", + "license": "AGPL-3.0-only", + "packageManager": "pnpm@10.4.1", + "dependencies": { + "electron": "^35.0.0", + "express": "^4.21.2" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "electron" + ] + }, + "devDependencies": { + "@types/electron": "^1.6.12", + "@types/node": "^22.13.9", + "fs-extra": "^11.3.0", + "typescript": "^5.8.2" + } +} \ No newline at end of file diff --git a/scripts/finalizeBuild.js b/scripts/finalizeBuild.js new file mode 100644 index 0000000..4964955 --- /dev/null +++ b/scripts/finalizeBuild.js @@ -0,0 +1,19 @@ +const fs = require('fs-extra'); +const path = require('path'); + +const srcDir = path.join(process.cwd(), 'src'); +const destDir = path.join(process.cwd(), 'dist'); + +const copyAssets = async () => { + try { + await fs.copy(srcDir, destDir, { + filter: (file) => !file.endsWith('.ts') + }); + console.log('[info] Built Neofunkin'); + } catch (err) { + console.error('[error] Uh oh.'); + console.error(err); + } +}; + +copyAssets(); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..05522af --- /dev/null +++ b/src/index.ts @@ -0,0 +1,60 @@ +import wm from './lib/windowManager'; +import { easeInOutQuad } from './lib/animations'; +import { app } from 'electron'; + +app.on('ready', () => { + try { + const mw = wm.create({ + w: 10, + h: 10, + whp: true, + onCreate: (win: any) => { + win.loadURL('https://example.com'); + }, + }); + + const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + (async () => { + await wait(200); + wm.resize({ id: mw, w: 25, h: 25, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad}); + await wait(1000); + wm.resize({ id: mw, w: 50, h: 50, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad}); + await wait(650); + wm.move({ id: mw, x: 0, y: 50, p: true, fromCenter: true}); + wm.resize({ id: mw, w: 33, h: 100, p: true }); + wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'}); + await wait(500); + wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true}); + wm.resize({ id: mw, w: 33, h: 100, p: true }); + wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'}); + await wait(500); + wm.move({ id: mw, x: 100, y: 50, p: true, fromCenter: true}); + wm.resize({ id: mw, w: 33, h: 100, p: true }); + wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'}); + await wait(500); + wm.resize({ id: mw, w: 100, h: 100, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad}); + await wait(550); + wm.resize({ id: mw, w: 100, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'}); + await wait(550); + wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad, anchor: 'bottom'}); + await wait(500); + wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true, ease: easeInOutQuad, smooth: true}); + await wait(500); + wm.resize({ id: mw, w: 50, h: 50, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad}); + await wait(510); + wm.eval(mw, `document.querySelector('body').innerHTML = '

NeoFunkin

'`); + await wait(500); + wm.eval(mw, `document.querySelector('body').innerHTML = '

MrpGimlom

'`); + await wait(100); + wm.eval(mw, `document.querySelector('body').innerHTML = '

NeoFunkin: Window manager

'`); + await wait(500); + wm.eval(mw, `document.querySelector('body').innerHTML = '

NeoFunkin: Window manager


DEMO BY @TRUE1ANN, NEOFUNKIN IS PROPERTY OF ASPER'`); + await wait(1500); + wm.eval(mw, `document.querySelector('body').innerHTML = '

NeoFunkin: Window manager


DEMO BY @TRUE1ANN, NEOFUNKIN IS PROPERTY OF ASPER
this took so much to do omfg'`); + })(); + + } catch (e) { + console.error(e); + } +}); \ No newline at end of file diff --git a/src/lib/animations.ts b/src/lib/animations.ts new file mode 100644 index 0000000..34ed0b9 --- /dev/null +++ b/src/lib/animations.ts @@ -0,0 +1,4 @@ +export type EasingFunction = (t: number) => number; +export const easeInQuad: EasingFunction = (t) => t * t; +export const easeOutQuad: EasingFunction = (t) => t * (2 - t); +export const easeInOutQuad: EasingFunction = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; \ No newline at end of file diff --git a/src/lib/windowManager.ts b/src/lib/windowManager.ts new file mode 100644 index 0000000..3b3fdae --- /dev/null +++ b/src/lib/windowManager.ts @@ -0,0 +1,229 @@ +import { BrowserWindow, screen } from 'electron'; +import { wm_create_conf, wm_move_conf, wm_resize_conf } from './../types/wm'; + +interface WindowData { + win: BrowserWindow; + conf: wm_create_conf; +} + +let wdata: { [key: string]: WindowData } = {}; + +function toP(value: number, percentage: number) { + return value * (percentage / 100) as number; +} + +const wm = { + create: function (c: wm_create_conf) { + const wid = Array.from({ length: 8 }, () => '0123456789abcdef'[Math.floor(Math.random() * 16)]).join(''); + const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; + console.log('[wm] create', wid); + const win = new BrowserWindow({ + width: c.whp ? toP(sw, c.w!) : c.w, + height: c.whp ? toP(sh, c.h!) : c.h, + x: c.xyp ? toP(sw, c.x!) : c.x, + y: c.xyp ? toP(sh, c.y!) : c.x, + frame: !c.noBorder, + transparent: c.noBackground, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + }, + }); + + wdata[wid] = { win: win, conf: c }; // for further pullin + win.setMenuBarVisibility(false); // tell me WHY should i NOT make this the default + + if (typeof c.onMinimize === 'function') win.on('minimize', () => c.onMinimize!(win)); + if (typeof c.onMaximize === 'function') win.on('maximize', () => c.onMaximize!(win)); + if (typeof c.onRestore === 'function') win.on('restore', () => c.onRestore!(win)); + if (typeof c.onFocus === 'function') win.on('focus', () => c.onFocus!(win)); + if (typeof c.onUnfocus === 'function') win.on('blur', () => c.onUnfocus!(win)); + if (typeof c.onClose === 'function') win.on('close', () => c.onClose!(win)); + + if (c.onCreate) c.onCreate(win); + return wid // just so you can reference it later on + }, + destroy: async function (id: number) { + console.log('[wm] destroy', id); + const win = wdata[id]; + if (typeof win.conf.onDestroy === 'function') await win.conf.onDestroy!(win.win); + win.win.destroy(); + delete wdata[id]; + }, + move: function (c: wm_move_conf) { + console.log('[wm] move', c.id); + const win = wdata[c.id]; + const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; + let wb = win.win.getBounds(); + + const sX = wb.x; + const sY = wb.y; + c.x = c.x === undefined ? sX : c.x; + c.y = c.y === undefined ? sY : c.y; + + const duration = c.duration || 500; + const tick = c.tick || 16; + const totalSteps = Math.floor(duration / tick); + + let targetX = toP(c.p ? sw : 1, c.x); + let targetY = toP(c.p ? sh : 1, c.y); + + if (c.fromCenter) { + wb = win.win.getBounds(); + targetX = targetX - wb.width * 0.5; + targetY = targetY - wb.height * 0.5; + } + + const animate = () => { + let currentStep = 0; + const startX = sX; + const startY = sY; + + const step = () => { + currentStep++; + const t = currentStep / totalSteps; + const eT = c.ease ? c.ease(t) : t; + + const newX = startX + (targetX - startX) * eT; + const newY = startY + (targetY - startY) * eT; + + win.win.setPosition(Math.round(newX), Math.round(newY)); + + if (currentStep < totalSteps) { + setTimeout(step, tick); + } else { + win.win.setPosition(Math.round(targetX), Math.round(targetY)); + } + }; + + step(); + }; + + if (c.smooth) { + animate(); + } else { + win.win.setPosition(Math.round(targetX), Math.round(targetY)); + } + }, + resize: function (c: wm_resize_conf) { + console.log('[wm] resize', c.id); + const win = wdata[c.id]; + let wb = win.win.getBounds(); + const sW = wb.width, sH = wb.height; + c.w = c.w === undefined ? sW : c.w; + c.h = c.h === undefined ? sH : c.h; + const duration = c.duration || 500; + const tick = c.tick || 16; + const totalSteps = Math.floor(duration / tick); + const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; + let targetW = toP(c.p ? sw : 1, c.w); + let targetH = toP(c.p ? sh : 1, c.h); + const centerX = wb.x + wb.width / 2; + const centerY = wb.y + wb.height / 2; + const getAnchoredPosition = (curWidth: number, curHeight: number) => { + let newX = wb.x; + let newY = wb.y; + if (!c.fromCenter && c.anchor) { + switch (c.anchor) { + case 'top': + newX = wb.x; + newY = wb.y; + break; + case 'bottom': + newX = wb.x; + newY = wb.y + wb.height - curHeight; + break; + case 'left': + newX = wb.x; + newY = wb.y; + break; + case 'right': + newX = wb.x + wb.width - curWidth; + newY = wb.y; + break; + default: + newX = wb.x; + newY = wb.y; + } + } + return { newX, newY }; + }; + const animate = () => { + let currentStep = 0; + const startW = wb.width; + const startH = wb.height; + const step = () => { + currentStep++; + const t = currentStep / totalSteps; + const eT = c.ease ? c.ease(t) : t; + const newW = startW + (targetW - startW) * eT; + const newH = startH + (targetH - startH) * eT; + let newX: number, newY: number; + if (c.fromCenter) { + newX = Math.round(centerX - newW / 2); + newY = Math.round(centerY - newH / 2); + } else { + const pos = getAnchoredPosition(Math.round(newW), Math.round(newH)); + newX = pos.newX; + newY = pos.newY; + } + win.win.setBounds({ + x: newX, + y: newY, + width: Math.round(newW), + height: Math.round(newH) + }); + if (currentStep < totalSteps) { + setTimeout(step, tick); + } else { + let finalX: number, finalY: number; + if (c.fromCenter) { + finalX = Math.round(centerX - targetW / 2); + finalY = Math.round(centerY - targetH / 2); + } else { + const pos = getAnchoredPosition(Math.round(targetW), Math.round(targetH)); + finalX = pos.newX; + finalY = pos.newY; + } + win.win.setBounds({ + x: finalX, + y: finalY, + width: Math.round(targetW), + height: Math.round(targetH) + }); + } + }; + step(); + }; + if (c.smooth) { + animate(); + } else { + let finalX: number, finalY: number; + if (c.fromCenter) { + finalX = Math.round(centerX - targetW / 2); + finalY = Math.round(centerY - targetH / 2); + } else { + const pos = getAnchoredPosition(Math.round(targetW), Math.round(targetH)); + finalX = pos.newX; + finalY = pos.newY; + } + win.win.setBounds({ + x: finalX, + y: finalY, + width: Math.round(targetW), + height: Math.round(targetH) + }); + } + }, + eval: async function (id: string, code: string) { + console.log('[wm] eval', id); + try { + return await wdata[id].win.webContents.executeJavaScript(code); + } catch (error) { + console.error('Error executing JavaScript:', error); + } + } +}; + +export default wm; +export { wdata, wm }; \ No newline at end of file diff --git a/src/screens/crash/index.html b/src/screens/crash/index.html new file mode 100644 index 0000000..885f66a --- /dev/null +++ b/src/screens/crash/index.html @@ -0,0 +1,9 @@ + + + + NeoFunkin + + +

Uh oh.

+ + diff --git a/src/screens/mainmenu/test.html b/src/screens/mainmenu/test.html new file mode 100644 index 0000000..f22a6d9 --- /dev/null +++ b/src/screens/mainmenu/test.html @@ -0,0 +1,9 @@ + + + + Electron + + +

Hello, World!

+ + diff --git a/src/types/wm.d.ts b/src/types/wm.d.ts new file mode 100644 index 0000000..e3e2a55 --- /dev/null +++ b/src/types/wm.d.ts @@ -0,0 +1,60 @@ +import { BrowserWindow } from 'electron'; + +export type wm_create_conf = { + w?: number; // Width + h?: number; // Height + x?: number; // X start pos + y?: number; // Y start pos + whp?: boolean; // is w and h in percentages + xyp?: boolean; // is x and y in percentages + noBorder?: boolean; // Hide OS window decorations + noBackground?: boolean; // Hide window background + onCreate?: Function; // wm.create(...) + onMinimize?: Function; // USER pressed '_' + onMaximize?: Function; // USER pressed '[]' + onRestore?: Function; // USER pressed '[]]' + onUnfocus?: Function; // USER unfocused on the window + onFocus?: Function; // USER focused on the window + onClose?: Function; // USER pressed 'x' + onDestroy?: Function; // USER pressed 'x' OR wm.destroy(...) +}; + +export type wm_move_conf = { + id: string; // ID of the window to perform everythin on + x?: number; // New X + y?: number; // New Y + p?: boolean; // Use percentages + fromCenter?: boolean; // Should be performed from the center of the window? + smooth?: boolean; // Should it be smooth? + ease?: Function; // Calculates the easing (lib/animations.ts) + duration?: number; // (ms) The total duration of the animation + tick?: number; // (ms) Delay between window movenments +} + +export type wm_resize_conf = { + id: string; // ID of the window to perform everythin on + w?: number; // New width + h?: number; // New height + p?: boolean; // Use percentages + fromCenter?: boolean; // Should be performed from the center of the window? + smooth?: boolean; // Should it be smooth? + ease?: Function; // Calculates the easing (lib/animations.ts) + duration?: number; // (ms) The total duration of the animation + tick?: number; // (ms) Delay between window resizings +} + +interface WindowData { + win: BrowserWindow; + conf: wm_create_conf; +} + +declare const wm: { + create: (c: wm_create_conf) => string; + destroy: (id: string) => Promise; + move: (c: wm_move_conf) => void; + resize: (c: wm_resize_conf) => void; + eval: (id: string, code: Function) => Promise; +}; + +export default wm; +export { WindowData, wm }; \ No newline at end of file diff --git a/src/types/wm.ts b/src/types/wm.ts new file mode 100644 index 0000000..05ac8d2 --- /dev/null +++ b/src/types/wm.ts @@ -0,0 +1,43 @@ +export type wm_create_conf = { + w?: number; // Width + h?: number; // Height + x?: number; // X start pos + y?: number; // Y start pos + whp?: boolean; // is w and h in percentages + xyp?: boolean; // is x and y in percentages + noBorder?: boolean; // Hide OS window decorations + noBackground?: boolean; // Hide window background + onCreate?: Function; // wm.create(...) + onMinimize?: Function; // USER pressed '_' + onMaximize?: Function; // USER pressed '[]' + onRestore?: Function; // USER pressed '[]]' + onUnfocus?: Function; // USER unfocused on the window + onFocus?: Function; // USER focused on the window + onClose?: Function; // USER pressed 'x' + onDestroy?: Function; // USER pressed 'x' OR wm.destroy(...) +}; + +export type wm_move_conf = { + id: string; // ID of the window to perform everythin on + x?: number; // New X + y?: number; // New Y + p?: boolean; // Use percentages + fromCenter?: boolean; // Should be performed from the center of the window? + smooth?: boolean; // Should it be smooth? + ease?: Function; // Calculates the easing (lib/animations.ts) + duration?: number; // (ms) The total duration of the animation + tick?: number; // (ms) Delay between window movenments +} + +export type wm_resize_conf = { + id: string; // ID of the window to perform everythin on + w?: number; // New width + h?: number; // New height + p?: boolean; // Use percentages + fromCenter?: boolean; // Should be performed from the center of the window? + smooth?: boolean; // Should it be smooth? + ease?: Function; // Calculates the easing (lib/animations.ts) + duration?: number; // (ms) The total duration of the animation + tick?: number; // (ms) Delay between window resizings + anchor?: string; // bottom, top, left, right +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..654c118 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} \ No newline at end of file