wm 1.0.0, wm demo, project structure

This commit is contained in:
Annie 2025-03-07 00:25:55 +03:00
parent c6c8ebb945
commit 349a9ddc0f
12 changed files with 478 additions and 1 deletions

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
.pnpm-debug.log* .pnpm-debug.log*
pnpm-lock.yaml
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View file

@ -1,3 +1,3 @@
# neofunkin # neofunkin
[planned] Electron-based Friday Night Funkin' engine with multi-window capabilities [RAW WIP] Electron-based Friday Night Funkin' engine with multi-window capabilities

29
package.json Normal file
View file

@ -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"
}
}

19
scripts/finalizeBuild.js Normal file
View file

@ -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();

60
src/index.ts Normal file
View file

@ -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 = '<h1>NeoFunkin</h1>'`);
await wait(500);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>MrpGimlom</h1>'`);
await wait(100);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>NeoFunkin: Window manager</h1>'`);
await wait(500);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>NeoFunkin: Window manager</h1><br>DEMO BY @TRUE1ANN, NEOFUNKIN IS PROPERTY OF ASPER'`);
await wait(1500);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>NeoFunkin: Window manager</h1><br>DEMO BY @TRUE1ANN, NEOFUNKIN IS PROPERTY OF ASPER<br><small>this took so much to do omfg</small>'`);
})();
} catch (e) {
console.error(e);
}
});

4
src/lib/animations.ts Normal file
View file

@ -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;

229
src/lib/windowManager.ts Normal file
View file

@ -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 };

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>NeoFunkin</title>
</head>
<body>
<h1>Uh oh.</h1>
</body>
</html>

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Electron</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>

60
src/types/wm.d.ts vendored Normal file
View file

@ -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<void>;
move: (c: wm_move_conf) => void;
resize: (c: wm_resize_conf) => void;
eval: (id: string, code: Function) => Promise<any>;
};
export default wm;
export { WindowData, wm };

43
src/types/wm.ts Normal file
View file

@ -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
}

14
tsconfig.json Normal file
View file

@ -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"]
}