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