diff --git a/package.json b/package.json index 24b6b23..3e00a34 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,11 @@ "keywords": [], "author": "", "license": "AGPL-3.0-only", - "packageManager": "pnpm@10.4.1", + "packageManager": "pnpm@10.6.1+sha512.40ee09af407fa9fbb5fbfb8e1cb40fbb74c0af0c3e10e9224d7b53c7658528615b2c92450e74cfad91e3a2dcafe3ce4050d80bda71d757756d2ce2b66213e9a3", "dependencies": { - "express": "^4.21.2" + "@types/ws": "^8.18.0", + "express": "^4.21.2", + "ws": "^8.18.1" }, "pnpm": { "onlyBuiltDependencies": [ @@ -34,11 +36,11 @@ ] }, "devDependencies": { - "electron": "^35.0.0", "@types/express": "^5.0.0", "@types/node": "^22.13.9", + "electron": "^35.0.0", "electron-builder": "^25.1.8", "fs-extra": "^11.3.0", "typescript": "^5.8.2" } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 7a08f04..7a470a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -import wm from './lib/windowManager'; -import { wait } from './lib/utils'; import { app } from 'electron'; import displayCrashScreen from './lib/crash'; import express from 'express'; import path from 'path'; import { createServer } from 'http'; - +import WebSocket from 'ws'; +import { displayMainMenu } from './screens/mainmenu'; +import { start } from 'repl'; declare global { namespace NodeJS { interface Process { @@ -17,7 +17,8 @@ declare global { process.nfenv = { serverPort: 65034 as number, // todo: pick a random one instead, not for now bcuz easier to debug and use /eval endpoint crash: function (reason?: string) { - console.error('[nfcrash] Simulating a crash with reason:', reason || '*no reason*'); + // todo: disaply error in the crashScreen itself + console.error('[nfcrash] NeoFunking crashed with reason:', reason || '*no reason*'); displayCrashScreen(); } } @@ -25,6 +26,7 @@ process.nfenv = { async function startWebRoot() { const app = express(); const server = createServer(app); + const wss = new WebSocket.Server({ server }); app.use(express.static(path.join(__dirname, 'webroot'))); @@ -43,30 +45,30 @@ async function startWebRoot() { } }); - // to-do: add echo webhook endpoint and endpoints to create more webhook endpoints (and also delete those) + wss.on('connection', (ws: WebSocket) => { + console.log('[ws] client++'); + + ws.on('message', (message) => { + const msg = JSON.parse(message.toString()); + if (!msg.ch) msg.ch = 'public' + console.log('[ws]', msg.ch, '-', msg.m); + ws.send(JSON.stringify(msg)); + }); + + ws.on('close', () => { + console.log('[ws] client--'); + }); + }); server.listen(process.nfenv.serverPort, 'localhost'); }; -startWebRoot().then(() => { - app.on('ready', async () => { - try { - const mw = wm.create({ - w: 20, - h: 20, - whp: true, - onCreate: (win: any) => { - win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/mainmenu/test.html`); - }, - }); - - wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true }) - await wait(500); - wm.resize({ id: mw, w: 40, h: 40, smooth: true, p: true}); - - } catch (e) { - console.error(e); - process.nfenv.crash(); - } - }); -}) \ No newline at end of file +app.on('ready', async () => { + try { + startWebRoot(); + displayMainMenu(); + } catch (e: any) { + console.error(e); + process.nfenv.crash(e.message); + } +}); \ No newline at end of file diff --git a/src/lib/animations.ts b/src/lib/animations.ts index 34ed0b9..2a15d00 100644 --- a/src/lib/animations.ts +++ b/src/lib/animations.ts @@ -1,4 +1,41 @@ export type EasingFunction = (t: number) => number; + +// Quad 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 +export const easeInOutQuad: EasingFunction = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + +// Cubic +export const easeInCubic: EasingFunction = (t) => t * t * t; +export const easeOutCubic: EasingFunction = (t) => --t * t * t + 1; +export const easeInOutCubic: EasingFunction = (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; + +// Quart +export const easeInQuart: EasingFunction = (t) => t * t * t * t; +export const easeOutQuart: EasingFunction = (t) => 1 - --t * t * t * t; +export const easeInOutQuart: EasingFunction = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; + +// Quint +export const easeInQuint: EasingFunction = (t) => t * t * t * t * t; +export const easeOutQuint: EasingFunction = (t) => 1 + --t * t * t * t * t; +export const easeInOutQuint: EasingFunction = (t) => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; + +// Sine +export const easeInSine: EasingFunction = (t) => 1 - Math.cos((t * Math.PI) / 2); +export const easeOutSine: EasingFunction = (t) => Math.sin((t * Math.PI) / 2); +export const easeInOutSine: EasingFunction = (t) => -(Math.cos(Math.PI * t) - 1) / 2; + +// Exponential +export const easeInExpo: EasingFunction = (t) => (t === 0 ? 0 : Math.pow(2, 10 * (t - 1))); +export const easeOutExpo: EasingFunction = (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)); +export const easeInOutExpo: EasingFunction = (t) => { + if (t === 0) return 0; + if (t === 1) return 1; + if (t < 0.5) return Math.pow(2, 20 * t - 10) / 2; + return (2 - Math.pow(2, -20 * t + 10)) / 2; +}; + +// Circular +export const easeInCirc: EasingFunction = (t) => 1 - Math.sqrt(1 - t * t); +export const easeOutCirc: EasingFunction = (t) => Math.sqrt(1 - (--t) * t); +export const easeInOutCirc: EasingFunction = (t) => t < 0.5 ? (1 - Math.sqrt(1 - 4 * t * t)) / 2 : (Math.sqrt(1 - (--t * 2) * t * 2) + 1) / 2; diff --git a/src/lib/crash.ts b/src/lib/crash.ts index 8283a83..5782d72 100644 --- a/src/lib/crash.ts +++ b/src/lib/crash.ts @@ -1,19 +1,18 @@ -import { wm, wdata } from './windowManager'; -import { wait, toP } from './utils' +import { wm, wdata, WindowData } from './windowManager'; +import { toP } from './utils' import { BrowserWindow, screen } from 'electron'; export default async function displayCrashScreen() { const bwdata = { ...wdata }; + const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; const win = wm.create({ w: 40, h: 40, whp: true, - onCreate: (win: BrowserWindow) => { - win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/crash/index.html`); - }, - onClose: function () { - process.exit(1) - } + onCreate: (win: WindowData) => { win.win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/crash/index.html`) }, + onMaximize: (win: WindowData) => { win.win.restore() }, + onResize: (win: WindowData) => { win.win.setSize(win.conf.w, win.conf.h) }, + customProps: { maximizable: false } }); const button = wm.create({ w: 10, @@ -21,22 +20,19 @@ export default async function displayCrashScreen() { whp: true, noBorder: true, noBackground: true, - onCreate: (win: BrowserWindow) => { - win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/crash/exit.html`); - }, + onCreate: (win: WindowData) => { win.win.loadURL(`http://localhost:${process.nfenv.serverPort}/buttons/exit.html?code=1`) }, + onClose: () => { process.exit(1) }, + onMaximize: (win: WindowData) => { win.win.restore() }, + onResize: (win: WindowData) => { win.win.setSize(win.conf.w, win.conf.h) }, + customProps: { maximizable: false } }); - await wait(300); Object.keys(bwdata).forEach(async (id) => { bwdata[id].win.destroy(); // Note: We dont use wm.destroy(...) so ...onDestroy wont get called and cause trouble + // Note 2: We also don't delete bwdata[id] because these windows cannot be recreated after a crash + // (The crash screen only allows the user to view and then exit the engine which will do the same result) }); console.log('[nfcrash] Deleted all windows'); - const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; const { height: csh, y: csy } = wdata[win].win.getBounds(); - await wait(100); - // Gives more control on the padding - // vvvvvvvvvv padding between the screen and exit button - const yTo = csy + csh + toP(sh, 6); - const xTo = toP(sw, 50); - wm.move({ id: button, y: yTo, x: xTo, fromCenter: true }); + wm.move({ id: button, y: csy + csh + toP(sh, 6), x: toP(sw, 50), fromCenter: true }); wm.follow.start(button, win); }; \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e962f8b..0c53935 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,5 @@ +import { screen } from "electron"; + export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -export function toP(value: number, percentage: number) { - return value * (percentage / 100) as number; -} \ No newline at end of file +export function toP(value: number, percentage: number) { return value * (percentage / 100) as number }; +export function screenSize() { return screen.getPrimaryDisplay().size } \ No newline at end of file diff --git a/src/lib/windowManager.ts b/src/lib/windowManager.ts index 79fc38f..eae27ee 100644 --- a/src/lib/windowManager.ts +++ b/src/lib/windowManager.ts @@ -1,10 +1,11 @@ -import { BrowserWindow, Rectangle, screen } from 'electron'; +import { BrowserWindow, } from 'electron'; import { wm_create_conf, wm_move_conf, wm_resize_conf } from './../types/wm'; -import { toP } from './utils'; +import { toP, screenSize } from './utils'; -interface WindowData { +export type WindowData = { win: BrowserWindow; conf: wm_create_conf; + wid: string; follows?: string; followStuff?: any; } @@ -13,10 +14,14 @@ let wdata: { [key: string]: WindowData } = {}; 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; + let wid = Array.from({ length: 32 }, () => '0123456789abcdef'[Math.floor(Math.random() * 16)]).join(''); + while (wdata[wid]) { + wid = Array.from({ length: 32 }, () => '0123456789abcdef'[Math.floor(Math.random() * 16)]).join(''); + } + const { width: sw, height: sh } = screenSize(); console.log('[wm] create', wid); const win = new BrowserWindow({ + ...c.customProps, 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, @@ -26,23 +31,29 @@ const wm = { webPreferences: { nodeIntegration: true, contextIsolation: false, - }, + } }); - wdata[wid] = { win: win, conf: c }; + const wobj = { win: win, conf: c, wid: wid } as WindowData; + wdata[wid] = wobj; win.setMenuBarVisibility(false); // tell me WHY should i NOT make this the default + win.on('close', () => { delete wdata[wid]; this.follow.stop(wid) }); + if (c.onCreate) c.onCreate(wobj); - 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 (typeof c.onMinimize === 'function') win.on('minimize', () => c.onMinimize!(wobj)); + if (typeof c.onMaximize === 'function') win.on('maximize', () => c.onMaximize!(wobj)); + if (typeof c.onRestore === 'function') win.on('restore', () => c.onRestore!(wobj)); + if (typeof c.onFocus === 'function') win.on('focus', () => c.onFocus!(wobj)); + if (typeof c.onShow === 'function') win.on('show', () => c.onShow!(wobj)); + if (typeof c.onUnfocus === 'function') win.on('blur', () => c.onUnfocus!(wobj)); + if (typeof c.onClose === 'function') win.on('close', () => c.onClose!(wobj)); + if (typeof c.onMove === 'function') win.on('move', () => c.onMove!(wobj)); + if (typeof c.onResize === 'function') win.on('resize', () => c.onResize!(wobj)); - if (c.onCreate) c.onCreate(win); return wid // just so you can reference it later on }, destroy: async function (id: string) { + if (!wdata[id]) return console.log('[wm] destroy', id); const win = wdata[id]; if (typeof win.conf.onDestroy === 'function') await win.conf.onDestroy!(win.win); @@ -50,9 +61,11 @@ const wm = { delete wdata[id]; }, move: function (c: wm_move_conf) { + if (!wdata[c.id]) return console.log('[wm] move', c.id); const win = wdata[c.id]; - const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; + const { x: cwx, y: cwy } = win.win.getBounds(); + const { width: sw, height: sh } = screenSize(); let wb = win.win.getBounds(); const sX = wb.x; @@ -61,11 +74,15 @@ const wm = { c.y = c.y === undefined ? sY : c.y; const duration = c.duration || 500; - const tick = c.tick || 16; + const tick = c.tick || 1000 / 30; const totalSteps = Math.floor(duration / tick); - let targetX = c.p ? toP(sw, c.x) : c.x; - let targetY = c.p ? toP(sh, c.y) : c.y; + // is relative? add current window's x, y + // | relative | | absolute + // | | is %? | | | is %? + // | | | % | px | | | % px + let targetX = c.r ? c.p ? toP(sw, c.x) + cwx : c.x + cwx : c.p ? toP(sw, c.x) : c.x; + let targetY = c.r ? c.p ? toP(sh, c.y) + cwy : c.y + cwy : c.p ? toP(sh, c.y) : c.y; if (c.fromCenter) { wb = win.win.getBounds(); @@ -105,6 +122,7 @@ const wm = { } }, resize: function (c: wm_resize_conf) { + if (!wdata[c.id]) return console.log('[wm] resize', c.id); const win = wdata[c.id]; if (!win || !win.win) { @@ -116,9 +134,9 @@ const wm = { 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 tick = c.tick || 1000 / 30; const totalSteps = Math.floor(duration / tick); - const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds; + const { width: sw, height: sh } = screenSize(); const targetW = toP(c.p ? sw : 1, c.w); const targetH = toP(c.p ? sh : 1, c.h); @@ -139,7 +157,6 @@ const wm = { newX = wb.x; break; case 'right': - // Align to right newX = wb.x + wb.width - curWidth; break; default: @@ -193,28 +210,20 @@ const wm = { } }, eval: async function (id: string, code: string) { + if (!wdata[id]) return console.log('[wm] eval', id); try { return await wdata[id].win.webContents.executeJavaScript(code); } catch (error) { - console.error('Error executing JavaScript:', error); + console.error('[wm] eval e:', error); } }, follow: { INTERNAL_follow: function (id: string) { + if (!wdata[id]) return const leadWin = wdata[wdata[id].follows!].win; const followWin = wdata[id].win; const leadBounds = leadWin.getBounds(); - const followBounds = followWin.getBounds(); - - if (wdata[id].followStuff === undefined) { - wdata[id].followStuff = {}; - } - - if (wdata[id].followStuff.offsetX === undefined || wdata[id].followStuff.offsetY === undefined) { - wdata[id].followStuff.offsetX = followBounds.x - leadBounds.x; - wdata[id].followStuff.offsetY = followBounds.y - leadBounds.y; - } const newX = leadBounds.x + wdata[id].followStuff.offsetX; const newY = leadBounds.y + wdata[id].followStuff.offsetY; @@ -223,17 +232,29 @@ const wm = { followWin.show(); }, INTERNAL_followfocus: function (id: string) { - wdata[id].win.show(); + if (wdata[id]) wdata[id].win.show(); }, start: function (id: string, follows: string) { + if (!wdata[id] || !wdata[follows]) return console.log('[wm]', id, 'follows', follows); wdata[id].follows = follows; - wdata[follows].win.on('move', this.INTERNAL_follow.bind(this, id)); - wdata[follows].win.on('focus', this.INTERNAL_followfocus.bind(this, id)); + + const fb = wdata[follows].win.getBounds(); + const lb = wdata[id].win.getBounds(); + wdata[id].followStuff = { + offsetX: lb.x - fb.x, + offsetY: lb.y - fb.y + }; + + wdata[follows].win.addListener('move', this.INTERNAL_follow.bind(this, id)); + wdata[follows].win.addListener('focus', this.INTERNAL_followfocus.bind(this, id)); + wdata[follows].win.addListener('show', this.INTERNAL_followfocus.bind(this, id)); }, stop: function (id: string) { + if (!wdata[id]) return wdata[wdata[id].follows!].win.removeListener('move', this.INTERNAL_follow.bind(this, id)); wdata[wdata[id].follows!].win.removeListener('focus', this.INTERNAL_followfocus.bind(this, id)); + wdata[id].followStuff = {}; } } }; diff --git a/src/screens/mainmenu.ts b/src/screens/mainmenu.ts new file mode 100644 index 0000000..694b6f9 --- /dev/null +++ b/src/screens/mainmenu.ts @@ -0,0 +1,63 @@ +import { app } from "electron"; +import { easeOutCirc, easeOutQuart, easeOutQuint } from "../lib/animations"; +import { screenSize, toP, wait } from "../lib/utils"; +import wm, {WindowData, wdata} from "../lib/windowManager"; + +export async function displayMainMenu() { + await app.whenReady(); + const mw = wm.create({ + w: 20, + h: 20, + whp: true, + onCreate: (win: WindowData) => { win.win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/mainmenu/`) }, + onClose: () => { process.exit(0) } + }); + wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true }); + await wait(500); + wm.resize({ id: mw, w: 40, h: 40, smooth: true, p: true, ease: easeOutQuart }); + await wait(500); + const { height: csh, y: csy, width: csw, x: csx } = wdata[mw].win.getBounds(); + const { height: sh, width: sw } = screenSize(); + + const pb = wm.create({ + w: 10, + h: 5, + whp: true, + noBorder: true, + noBackground: true, + onCreate: (win: WindowData) => { win.win.loadURL(`http://localhost:${process.nfenv.serverPort}/buttons/openscreen.html?screen=Play`) }, + onMaximize: (win: WindowData) => { win.win.restore() }, + onResize: (win: WindowData) => { win.win.setSize(win.conf.w, win.conf.h) }, + customProps: { maximizable: false } + }); + wm.move({ id: pb, y: csy + toP(csh, 60), x: csx - toP(sw, -1.5), fromCenter: true }); + wm.follow.start(pb, mw); + + const eb = wm.create({ + w: 10, + h: 5, + whp: true, + noBorder: true, + noBackground: true, + onCreate: (win: WindowData) => { win.win.loadURL(`http://localhost:${process.nfenv.serverPort}/buttons/exit.html`) }, + onMaximize: (win: WindowData) => { win.win.restore() }, + onResize: (win: WindowData) => { win.win.setSize(win.conf.w, win.conf.h) }, + customProps: { maximizable: false } + }); + wm.move({ id: eb, y: csy + toP(csh, 75), x: csx - toP(sw, -1.5), fromCenter: true }); + wm.follow.start(eb, mw); + + const ob = wm.create({ + w: 10, + h: 5, + whp: true, + noBorder: true, + noBackground: true, + onCreate: (win: WindowData) => { win.win.loadURL(`http://localhost:${process.nfenv.serverPort}/buttons/openscreen.html?screen=Options`) }, + onMaximize: (win: WindowData) => { win.win.restore() }, + onResize: (win: WindowData) => { win.win.setSize(win.conf.w, win.conf.h) }, + customProps: { maximizable: false } + }); + wm.move({ id: ob, y: csy + toP(csh, 90), x: csx - toP(sw, -1.5), fromCenter: true }); + wm.follow.start(ob, mw); +}; \ No newline at end of file diff --git a/src/types/wm.ts b/src/types/wm.ts index a2868c1..c6b348b 100644 --- a/src/types/wm.ts +++ b/src/types/wm.ts @@ -11,10 +11,14 @@ export type wm_create_conf = { onMinimize?: Function; // USER pressed '_' onMaximize?: Function; // USER pressed '[]' onRestore?: Function; // USER pressed '[]]' + onShow?: Function; // USER showed the windo 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(...) + onMove?: Function; // Window moves + onResize?: Function; // Window resizes + customProps?: object; // for example for setting maximizable to false }; export type wm_move_conf = { @@ -27,6 +31,7 @@ export type wm_move_conf = { ease?: Function; // Calculates the easing (lib/animations.ts) duration?: number; // (ms) The total duration of the animation tick?: number; // (ms) Delay between window movenments + r?: boolean; // Should we set the location directly, or add to current location? } export type wm_resize_conf = { diff --git a/src/webroot/buttons/exit.html b/src/webroot/buttons/exit.html new file mode 100644 index 0000000..71e2c2c --- /dev/null +++ b/src/webroot/buttons/exit.html @@ -0,0 +1,24 @@ + + + + + + NeoFunkin + + + + + + + \ No newline at end of file diff --git a/src/webroot/buttons/openscreen.html b/src/webroot/buttons/openscreen.html new file mode 100644 index 0000000..ee1ef88 --- /dev/null +++ b/src/webroot/buttons/openscreen.html @@ -0,0 +1,29 @@ + + + + + + NeoFunkin + + + + + + + \ No newline at end of file diff --git a/src/webroot/screens/crash/exit.html b/src/webroot/screens/crash/exit.html deleted file mode 100644 index 5e664ad..0000000 --- a/src/webroot/screens/crash/exit.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - NeoFunkin - - - - - - \ No newline at end of file diff --git a/src/webroot/screens/mainmenu/index.html b/src/webroot/screens/mainmenu/index.html new file mode 100644 index 0000000..92f6fb7 --- /dev/null +++ b/src/webroot/screens/mainmenu/index.html @@ -0,0 +1,10 @@ + + + + NeoFunkin + + + MAINMENU + toTest + + diff --git a/src/webroot/screens/mainmenu/test.html b/src/webroot/screens/mainmenu/test.html index 7beefc9..788bc06 100644 --- a/src/webroot/screens/mainmenu/test.html +++ b/src/webroot/screens/mainmenu/test.html @@ -4,8 +4,7 @@ Electron -

Hello, World!

- - +

Hello, World!

+