create-gamenative-app 0.1.6 → 0.1.8
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/bin/create.js +32 -15
- package/dist/scripts/Graphics.d.ts +4 -0
- package/dist/scripts/Graphics.d.ts.map +1 -1
- package/dist/scripts/Graphics.js +18 -0
- package/dist/scripts/Graphics.js.map +1 -1
- package/dist/scripts/Text.d.ts +50 -0
- package/dist/scripts/Text.d.ts.map +1 -0
- package/dist/scripts/Text.js +136 -0
- package/dist/scripts/Text.js.map +1 -0
- package/dist/scripts/UI.d.ts +6 -0
- package/dist/scripts/UI.d.ts.map +1 -0
- package/dist/scripts/UI.js +8 -0
- package/dist/scripts/UI.js.map +1 -0
- package/dist/scripts/index.d.ts +3 -0
- package/dist/scripts/index.d.ts.map +1 -1
- package/dist/scripts/index.js +2 -0
- package/dist/scripts/index.js.map +1 -1
- package/package.json +9 -3
- package/scripts/Graphics.ts +24 -0
- package/scripts/Text.ts +159 -0
- package/scripts/UI.ts +8 -0
- package/scripts/earcut.d.ts +4 -0
- package/scripts/index.ts +3 -0
- package/scripts/opentype.d.ts +8 -0
- package/scripts/set-exe-icon.js +36 -0
package/bin/create.js
CHANGED
|
@@ -3,7 +3,7 @@ import inquirer from 'inquirer'
|
|
|
3
3
|
import { mkdir, writeFile, copyFile, readFile } from 'fs/promises'
|
|
4
4
|
import { join, dirname } from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
-
import {
|
|
6
|
+
import { execSync } from 'child_process'
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
9
|
const FRAMEWORK_ROOT = join(__dirname, '..')
|
|
@@ -43,7 +43,12 @@ async function scaffold(projectName, parentDir) {
|
|
|
43
43
|
indexTs,
|
|
44
44
|
iconTs,
|
|
45
45
|
pngjsDts,
|
|
46
|
+
opentypeDts,
|
|
47
|
+
earcutDts,
|
|
48
|
+
textTs,
|
|
49
|
+
uiTs,
|
|
46
50
|
copyConfigJs,
|
|
51
|
+
setExeIconJs,
|
|
47
52
|
] = await Promise.all([
|
|
48
53
|
copyFrameworkFile('scripts/main.ts'),
|
|
49
54
|
copyFrameworkFile('scripts/config.ts'),
|
|
@@ -54,7 +59,12 @@ async function scaffold(projectName, parentDir) {
|
|
|
54
59
|
copyFrameworkFile('scripts/index.ts'),
|
|
55
60
|
copyFrameworkFile('scripts/icon.ts'),
|
|
56
61
|
copyFrameworkFile('scripts/pngjs.d.ts'),
|
|
62
|
+
readFile(join(FRAMEWORK_ROOT, 'scripts/opentype.d.ts'), 'utf-8'),
|
|
63
|
+
readFile(join(FRAMEWORK_ROOT, 'scripts/earcut.d.ts'), 'utf-8'),
|
|
64
|
+
copyFrameworkFile('scripts/Text.ts'),
|
|
65
|
+
copyFrameworkFile('scripts/UI.ts'),
|
|
57
66
|
readFile(join(FRAMEWORK_ROOT, 'scripts/copy-config.js'), 'utf-8'),
|
|
67
|
+
readFile(join(FRAMEWORK_ROOT, 'scripts/set-exe-icon.js'), 'utf-8'),
|
|
58
68
|
])
|
|
59
69
|
|
|
60
70
|
const gameConfig = {
|
|
@@ -77,24 +87,28 @@ async function scaffold(projectName, parentDir) {
|
|
|
77
87
|
dev: 'cross-env NODE_ENV=development tsx scripts/main.ts',
|
|
78
88
|
build: 'tsc',
|
|
79
89
|
start: 'node dist/scripts/main.js',
|
|
80
|
-
'build:exe': 'npm run build && node scripts/copy-config.js && pkg . --output release/' + name + '.exe',
|
|
90
|
+
'build:exe': 'npm run build && node scripts/copy-config.js && pkg . --output release/' + name + '.exe && node scripts/set-exe-icon.js release/' + name + '.exe',
|
|
81
91
|
},
|
|
82
92
|
dependencies: {
|
|
83
93
|
'@kmamal/gl': '^9.1.0',
|
|
84
94
|
'@kmamal/sdl': '^0.11.13',
|
|
95
|
+
earcut: '^3.0.0',
|
|
85
96
|
pngjs: '^7.0.0',
|
|
97
|
+
'opentype.js': '^1.3.4',
|
|
86
98
|
},
|
|
87
99
|
devDependencies: {
|
|
88
100
|
'@types/node': '^22.10.0',
|
|
89
101
|
'cross-env': '^7.0.3',
|
|
90
102
|
pkg: '^5.8.1',
|
|
103
|
+
rcedit: '^5.0.0',
|
|
91
104
|
tsx: '^4.19.2',
|
|
105
|
+
'to-ico': '^1.1.5',
|
|
92
106
|
typescript: '^5.7.0',
|
|
93
107
|
},
|
|
94
108
|
engines: { node: '>=18' },
|
|
95
109
|
pkg: {
|
|
96
110
|
scripts: 'dist/scripts/main.js',
|
|
97
|
-
assets: ['node_modules/@kmamal/sdl/**', 'node_modules/@kmamal/gl/**', 'dist/src/games/**'],
|
|
111
|
+
assets: ['node_modules/@kmamal/sdl/**', 'node_modules/@kmamal/gl/**', 'node_modules/opentype.js/**', 'node_modules/earcut/**', 'dist/src/games/**'],
|
|
98
112
|
targets: ['node18-win-x64'],
|
|
99
113
|
},
|
|
100
114
|
}
|
|
@@ -278,8 +292,13 @@ export default game
|
|
|
278
292
|
writeFile(join(scripts, 'index.ts'), indexTs),
|
|
279
293
|
writeFile(join(scripts, 'icon.ts'), iconTs),
|
|
280
294
|
writeFile(join(scripts, 'pngjs.d.ts'), pngjsDts),
|
|
295
|
+
writeFile(join(scripts, 'opentype.d.ts'), opentypeDts),
|
|
296
|
+
writeFile(join(scripts, 'earcut.d.ts'), earcutDts),
|
|
297
|
+
writeFile(join(scripts, 'Text.ts'), textTs),
|
|
298
|
+
writeFile(join(scripts, 'UI.ts'), uiTs),
|
|
281
299
|
writeFile(join(games, 'Game.ts'), gameTemplate),
|
|
282
300
|
writeFile(join(scripts, 'copy-config.js'), copyConfigJs),
|
|
301
|
+
writeFile(join(scripts, 'set-exe-icon.js'), setExeIconJs),
|
|
283
302
|
])
|
|
284
303
|
|
|
285
304
|
const iconSrc = join(FRAMEWORK_ROOT, 'src', 'assets', 'icon.png')
|
|
@@ -295,6 +314,7 @@ Game built with [GameNative](https://github.com/your-org/GameNative).
|
|
|
295
314
|
|
|
296
315
|
- \`npm run dev\` — run from source
|
|
297
316
|
- \`npm run build\` then \`npm start\` — run built app
|
|
317
|
+
- \`npm run build:exe\` — build Windows .exe (taskbar uses \`assets/icon.png\`; under \`npm run dev\` the taskbar shows Node’s icon)
|
|
298
318
|
- Edit \`game.config.json\` for window title, size, icon
|
|
299
319
|
- Edit \`src/games/Game.ts\` to build your game
|
|
300
320
|
|
|
@@ -304,16 +324,17 @@ See FRAMEWORK.md for 2D, 3D, lighting, sound, UI, and camera.
|
|
|
304
324
|
|
|
305
325
|
Your game has access to:
|
|
306
326
|
|
|
307
|
-
- **2D**: \`createDraw2D(ctx.gl)\` — \`clear()\`, \`fillRect()\`. Use \`ctx.gl\` (WebGL 1) for textures, sprites, batching.
|
|
327
|
+
- **2D**: \`createDraw2D(ctx.gl)\` — \`clear()\`, \`fillRect()\`, \`fillTriangles()\`, \`strokeRect()\`. Use \`ctx.gl\` (WebGL 1) for textures, sprites, batching.
|
|
328
|
+
- **Text**: \`loadFont(\'path/to/font.ttf\')\` (TTF/OTF), \`getTextTriangles(font, text, x, y, size)\` → triangles, \`drawText(draw2d, font, text, x, y, size, r, g, b, a)\`, \`measureText(font, text, size)\`. Any font, any size; draw with \`fillTriangles\` for full control.
|
|
308
329
|
- **3D**: Use \`ctx.gl\` directly: buffers, shaders, matrices. Implement camera (view/projection), meshes, and lighting in shaders.
|
|
309
330
|
- **Lighting**: In 3D shaders use uniforms for light position/color; in 2D use tint or custom fragment shaders.
|
|
310
331
|
- **Assets**: Load images (decode PNG with pngjs or similar), upload to \`ctx.gl\` textures; load audio (see sound).
|
|
311
332
|
- **Sound**: \`ctx.sdl.audio\` (SDL audio) — open device, enqueue buffers. Or add a small wrapper in your game.
|
|
312
333
|
- **Visuals**: Full WebGL — post-process by rendering to framebuffer, then to screen; particles, blur, etc. in shaders.
|
|
313
|
-
- **UI**:
|
|
334
|
+
- **UI / Buttons**: \`fillRect\` + \`strokeRect\` for panels/buttons, \`drawText\` for labels. \`isPointInRect(px, py, x, y, w, h)\` for hit-test. Track \`ctx.input.mouseX\`, \`mouseY\`, \`mouseLeft\` in \`update\` and draw any style in code.
|
|
314
335
|
- **Camera**: 2D: store \`offsetX, offsetY, scale\` and pass to your draw calls or a uniform. 3D: \`view\` and \`projection\` matrices from position/target/up and perspective.
|
|
315
336
|
|
|
316
|
-
All of this is done in your \`Game.ts\` using \`ctx.gl\`, \`ctx.input\`, and
|
|
337
|
+
All of this is done in your \`Game.ts\` using \`ctx.gl\`, \`ctx.input\`, and the exported helpers (createDraw2D, loadFont, drawText, isPointInRect, etc.).
|
|
317
338
|
`
|
|
318
339
|
await writeFile(join(projectPath, 'README.md'), readme)
|
|
319
340
|
await writeFile(join(projectPath, 'FRAMEWORK.md'), frameworkMd)
|
|
@@ -322,15 +343,11 @@ All of this is done in your \`Game.ts\` using \`ctx.gl\`, \`ctx.input\`, and opt
|
|
|
322
343
|
}
|
|
323
344
|
|
|
324
345
|
function npmInstall(cwd) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
shell: isWin,
|
|
331
|
-
})
|
|
332
|
-
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error('npm install failed'))))
|
|
333
|
-
})
|
|
346
|
+
try {
|
|
347
|
+
execSync('npm install', { cwd, stdio: 'inherit' })
|
|
348
|
+
} catch {
|
|
349
|
+
throw new Error('npm install failed')
|
|
350
|
+
}
|
|
334
351
|
}
|
|
335
352
|
|
|
336
353
|
async function main() {
|
|
@@ -2,6 +2,10 @@ import type { WebGLRenderingContext as GLContext } from '@kmamal/gl';
|
|
|
2
2
|
export interface Draw2D {
|
|
3
3
|
clear(r: number, g: number, b: number, a?: number): void;
|
|
4
4
|
fillRect(x: number, y: number, w: number, h: number, r: number, g: number, b: number, a?: number): void;
|
|
5
|
+
/** Draw triangles in pixel space. verts: [x,y, x,y, x,y, ...] (3 verts per triangle). */
|
|
6
|
+
fillTriangles(verts: Float32Array, r: number, g: number, b: number, a?: number): void;
|
|
7
|
+
/** Draw rectangle outline (1px stroke). */
|
|
8
|
+
strokeRect(x: number, y: number, w: number, h: number, r: number, g: number, b: number, a?: number): void;
|
|
5
9
|
dispose(): void;
|
|
6
10
|
}
|
|
7
11
|
export declare function createDraw2D(gl: GLContext): Draw2D;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Graphics.d.ts","sourceRoot":"","sources":["../../scripts/Graphics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,IAAI,SAAS,EAAE,MAAM,YAAY,CAAA;AAwBpE,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxD,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvG,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"Graphics.d.ts","sourceRoot":"","sources":["../../scripts/Graphics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,IAAI,SAAS,EAAE,MAAM,YAAY,CAAA;AAwBpE,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxD,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvG,yFAAyF;IACzF,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrF,2CAA2C;IAC3C,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzG,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,MAAM,CAkFlD"}
|
package/dist/scripts/Graphics.js
CHANGED
|
@@ -42,6 +42,7 @@ export function createDraw2D(gl) {
|
|
|
42
42
|
const uColor = gl.getUniformLocation(program, 'u_color');
|
|
43
43
|
const buffer = gl.createBuffer();
|
|
44
44
|
const verts = new Float32Array(8);
|
|
45
|
+
const strokeWidth = 1;
|
|
45
46
|
return {
|
|
46
47
|
clear(r, g, b, a = 1) {
|
|
47
48
|
gl.clearColor(r, g, b, a);
|
|
@@ -67,6 +68,23 @@ export function createDraw2D(gl) {
|
|
|
67
68
|
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
|
|
68
69
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
69
70
|
},
|
|
71
|
+
fillTriangles(verts, r, g, b, a = 1) {
|
|
72
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
73
|
+
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.DYNAMIC_DRAW);
|
|
74
|
+
gl.useProgram(program);
|
|
75
|
+
gl.uniform2f(uResolution, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
76
|
+
gl.uniform4f(uColor, r, g, b, a);
|
|
77
|
+
gl.enableVertexAttribArray(aPosition);
|
|
78
|
+
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
|
|
79
|
+
gl.drawArrays(gl.TRIANGLES, 0, verts.length / 2);
|
|
80
|
+
},
|
|
81
|
+
strokeRect(x, y, w, h, r, g, b, a = 1) {
|
|
82
|
+
const s = strokeWidth;
|
|
83
|
+
this.fillRect(x, y, w, s, r, g, b, a);
|
|
84
|
+
this.fillRect(x, y + h - s, w, s, r, g, b, a);
|
|
85
|
+
this.fillRect(x, y, s, h, r, g, b, a);
|
|
86
|
+
this.fillRect(x + w - s, y, s, h, r, g, b, a);
|
|
87
|
+
},
|
|
70
88
|
dispose() {
|
|
71
89
|
gl.deleteProgram(program);
|
|
72
90
|
gl.deleteShader(vs);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Graphics.js","sourceRoot":"","sources":["../../scripts/Graphics.ts"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,MAAM,IAAI,GAAG;;;;;;;CAOZ,CAAA;AAED,MAAM,IAAI,GAAG;;;;;;CAMZ,CAAA;
|
|
1
|
+
{"version":3,"file":"Graphics.js","sourceRoot":"","sources":["../../scripts/Graphics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type Font = {
|
|
2
|
+
getPath: (text: string, x: number, y: number, fontSize: number) => {
|
|
3
|
+
commands: PathCommand[];
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
type PathCommand = {
|
|
7
|
+
type: 'M';
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'L';
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'Q';
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
x1: number;
|
|
19
|
+
y1: number;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'C';
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
x1: number;
|
|
25
|
+
y1: number;
|
|
26
|
+
x2: number;
|
|
27
|
+
y2: number;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Load a TTF or OTF font from path (relative to cwd or absolute).
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadFont(path: string): Promise<Font>;
|
|
33
|
+
/**
|
|
34
|
+
* Tessellate text into triangles in pixel space. (x, y) = top-left of text; y-axis down.
|
|
35
|
+
* Use with draw2d.fillTriangles(triangles, r, g, b, a).
|
|
36
|
+
*/
|
|
37
|
+
export declare function getTextTriangles(font: Font, text: string, x: number, y: number, size: number): Float32Array;
|
|
38
|
+
/**
|
|
39
|
+
* Measure text width and height at given size (approximate from path bounds).
|
|
40
|
+
*/
|
|
41
|
+
export declare function measureText(font: Font, text: string, size: number): {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
};
|
|
45
|
+
/** Draw text with a Draw2D instance. (x,y) = top-left, y-axis down. */
|
|
46
|
+
export declare function drawText(draw2d: {
|
|
47
|
+
fillTriangles: (v: Float32Array, r: number, g: number, b: number, a?: number) => void;
|
|
48
|
+
}, font: Font, text: string, x: number, y: number, size: number, r: number, g: number, b: number, a?: number): void;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=Text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Text.d.ts","sourceRoot":"","sources":["../../scripts/Text.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,IAAI,GAAG;IACjB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,WAAW,EAAE,CAAA;KAAE,CAAA;CAC/F,CAAA;AAED,KAAK,WAAW,GACZ;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AA6DvF;;GAEG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK1D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CA2B3G;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAiBrG;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CACtB,MAAM,EAAE;IAAE,aAAa,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACjG,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,SAAI,GACJ,IAAI,CAGN"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text rendering: load TTF/OTF fonts, tessellate glyphs to triangles, draw at any size.
|
|
3
|
+
* Use with createDraw2D().fillTriangles() for full control over color and style.
|
|
4
|
+
*/
|
|
5
|
+
import opentype from 'opentype.js';
|
|
6
|
+
import earcut from 'earcut';
|
|
7
|
+
import { readFile } from 'fs/promises';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
function flattenCommands(commands) {
|
|
10
|
+
const contours = [];
|
|
11
|
+
let current = [];
|
|
12
|
+
let lastX = 0;
|
|
13
|
+
let lastY = 0;
|
|
14
|
+
const push = (x, y) => {
|
|
15
|
+
current.push(x, y);
|
|
16
|
+
lastX = x;
|
|
17
|
+
lastY = y;
|
|
18
|
+
};
|
|
19
|
+
for (const cmd of commands) {
|
|
20
|
+
if (cmd.type === 'M') {
|
|
21
|
+
if (current.length >= 6)
|
|
22
|
+
contours.push(current);
|
|
23
|
+
current = [];
|
|
24
|
+
push(cmd.x, cmd.y);
|
|
25
|
+
}
|
|
26
|
+
else if (cmd.type === 'L') {
|
|
27
|
+
push(cmd.x, cmd.y);
|
|
28
|
+
}
|
|
29
|
+
else if (cmd.type === 'Q') {
|
|
30
|
+
for (let t = 1; t <= 4; t++) {
|
|
31
|
+
const s = t / 4;
|
|
32
|
+
const u = 1 - s;
|
|
33
|
+
const x = u * u * lastX + 2 * u * s * cmd.x1 + s * s * cmd.x;
|
|
34
|
+
const y = u * u * lastY + 2 * u * s * cmd.y1 + s * s * cmd.y;
|
|
35
|
+
push(x, y);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (cmd.type === 'C') {
|
|
39
|
+
for (let t = 1; t <= 8; t++) {
|
|
40
|
+
const s = t / 8;
|
|
41
|
+
const u = 1 - s;
|
|
42
|
+
const x = u * u * u * lastX +
|
|
43
|
+
3 * u * u * s * cmd.x1 +
|
|
44
|
+
3 * u * s * s * cmd.x2 +
|
|
45
|
+
s * s * s * cmd.x;
|
|
46
|
+
const y = u * u * u * lastY +
|
|
47
|
+
3 * u * u * s * cmd.y1 +
|
|
48
|
+
3 * u * s * s * cmd.y2 +
|
|
49
|
+
s * s * s * cmd.y;
|
|
50
|
+
push(x, y);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (current.length >= 6)
|
|
55
|
+
contours.push(current);
|
|
56
|
+
return contours;
|
|
57
|
+
}
|
|
58
|
+
function signedArea(contour) {
|
|
59
|
+
let sum = 0;
|
|
60
|
+
const n = contour.length / 2;
|
|
61
|
+
for (let i = 0; i < n; i++) {
|
|
62
|
+
const j = (i + 1) % n;
|
|
63
|
+
sum += (contour[2 * j] - contour[2 * i]) * (contour[2 * j + 1] + contour[2 * i + 1]);
|
|
64
|
+
}
|
|
65
|
+
return sum * 0.5;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load a TTF or OTF font from path (relative to cwd or absolute).
|
|
69
|
+
*/
|
|
70
|
+
export async function loadFont(path) {
|
|
71
|
+
const abs = path.startsWith('/') || /^[A-Za-z]:/.test(path) ? path : join(process.cwd(), path);
|
|
72
|
+
const buffer = await readFile(abs);
|
|
73
|
+
const ab = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
74
|
+
return opentype.parse(ab);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Tessellate text into triangles in pixel space. (x, y) = top-left of text; y-axis down.
|
|
78
|
+
* Use with draw2d.fillTriangles(triangles, r, g, b, a).
|
|
79
|
+
*/
|
|
80
|
+
export function getTextTriangles(font, text, x, y, size) {
|
|
81
|
+
const path = font.getPath(text, 0, 0, size);
|
|
82
|
+
const commands = path.commands;
|
|
83
|
+
const contours = flattenCommands(commands);
|
|
84
|
+
if (contours.length === 0)
|
|
85
|
+
return new Float32Array(0);
|
|
86
|
+
const areas = contours.map((c) => signedArea(c));
|
|
87
|
+
const outerIdx = areas.reduce((best, a, i) => (Math.abs(a) > Math.abs(areas[best]) ? i : best), 0);
|
|
88
|
+
const outer = contours[outerIdx];
|
|
89
|
+
const holes = contours.filter((_, i) => i !== outerIdx);
|
|
90
|
+
const verts = [...outer];
|
|
91
|
+
const holeIndices = [];
|
|
92
|
+
for (const h of holes) {
|
|
93
|
+
holeIndices.push(verts.length / 2);
|
|
94
|
+
verts.push(...h);
|
|
95
|
+
}
|
|
96
|
+
const indices = earcut(verts, holeIndices, 2);
|
|
97
|
+
const out = [];
|
|
98
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
99
|
+
const i0 = indices[i] * 2;
|
|
100
|
+
const i1 = indices[i + 1] * 2;
|
|
101
|
+
const i2 = indices[i + 2] * 2;
|
|
102
|
+
out.push(x + verts[i0], y + size - verts[i0 + 1]);
|
|
103
|
+
out.push(x + verts[i1], y + size - verts[i1 + 1]);
|
|
104
|
+
out.push(x + verts[i2], y + size - verts[i2 + 1]);
|
|
105
|
+
}
|
|
106
|
+
return new Float32Array(out);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Measure text width and height at given size (approximate from path bounds).
|
|
110
|
+
*/
|
|
111
|
+
export function measureText(font, text, size) {
|
|
112
|
+
const path = font.getPath(text, 0, 0, size);
|
|
113
|
+
const commands = path.commands;
|
|
114
|
+
let x1 = Infinity;
|
|
115
|
+
let y1 = Infinity;
|
|
116
|
+
let x2 = -Infinity;
|
|
117
|
+
let y2 = -Infinity;
|
|
118
|
+
for (const c of commands) {
|
|
119
|
+
const x = 'x' in c ? c.x : 0;
|
|
120
|
+
const y = 'y' in c ? c.y : 0;
|
|
121
|
+
x1 = Math.min(x1, x);
|
|
122
|
+
y1 = Math.min(y1, y);
|
|
123
|
+
x2 = Math.max(x2, x);
|
|
124
|
+
y2 = Math.max(y2, y);
|
|
125
|
+
}
|
|
126
|
+
if (x1 === Infinity)
|
|
127
|
+
return { width: 0, height: size };
|
|
128
|
+
return { width: x2 - x1, height: y2 - y1 };
|
|
129
|
+
}
|
|
130
|
+
/** Draw text with a Draw2D instance. (x,y) = top-left, y-axis down. */
|
|
131
|
+
export function drawText(draw2d, font, text, x, y, size, r, g, b, a = 1) {
|
|
132
|
+
const tri = getTextTriangles(font, text, x, y, size);
|
|
133
|
+
if (tri.length > 0)
|
|
134
|
+
draw2d.fillTriangles(tri, r, g, b, a);
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=Text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Text.js","sourceRoot":"","sources":["../../scripts/Text.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,QAAQ,MAAM,aAAa,CAAA;AAClC,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAY3B,SAAS,eAAe,CAAC,QAAuB;IAC9C,MAAM,QAAQ,GAAe,EAAE,CAAA;IAC/B,IAAI,OAAO,GAAa,EAAE,CAAA;IAC1B,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE;QACpC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAClB,KAAK,GAAG,CAAC,CAAA;QACT,KAAK,GAAG,CAAC,CAAA;IACX,CAAC,CAAA;IAED,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YACrB,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC;gBAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YAC/C,OAAO,GAAG,EAAE,CAAA;YACZ,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;QACpB,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YAC5B,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;QACpB,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC5B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBACf,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBACf,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;gBAC5D,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;gBAC5D,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YACZ,CAAC;QACH,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC5B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBACf,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBACf,MAAM,CAAC,GACL,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK;oBACjB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE;oBACtB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE;oBACtB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;gBACnB,MAAM,CAAC,GACL,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK;oBACjB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE;oBACtB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE;oBACtB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;gBACnB,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YACZ,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC;QAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/C,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,UAAU,CAAC,OAAiB;IACnC,IAAI,GAAG,GAAG,CAAC,CAAA;IACX,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAA;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;QACrB,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACtF,CAAC;IACD,OAAO,GAAG,GAAG,GAAG,CAAA;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAA;IAC9F,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAA;IAClC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,CAAA;IACxF,OAAO,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAoB,CAAA;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAU,EAAE,IAAY,EAAE,CAAS,EAAE,CAAS,EAAE,IAAY;IAC3F,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAA;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAyB,CAAA;IAC/C,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IAC1C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,CAAA;IAErD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IAChD,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;IAClG,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAChC,MAAM,KAAK,GAAe,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAA;IACnE,MAAM,KAAK,GAAa,CAAC,GAAG,KAAK,CAAC,CAAA;IAClC,MAAM,WAAW,GAAa,EAAE,CAAA;IAChC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAClC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IAClB,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,CAAA;IAC7C,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACzB,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;QAC7B,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;QAC7B,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;QACjD,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;QACjD,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IACnD,CAAC;IACD,OAAO,IAAI,YAAY,CAAC,GAAG,CAAC,CAAA;AAC9B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,IAAU,EAAE,IAAY,EAAE,IAAY;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAA;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAyB,CAAA;IAC/C,IAAI,EAAE,GAAG,QAAQ,CAAA;IACjB,IAAI,EAAE,GAAG,QAAQ,CAAA;IACjB,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAA;IAClB,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAA;IAClB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC5B,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC5B,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;QACpB,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;QACpB,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;QACpB,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IACtB,CAAC;IACD,IAAI,EAAE,KAAK,QAAQ;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IACtD,OAAO,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,EAAE,CAAA;AAC5C,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,QAAQ,CACtB,MAAiG,EACjG,IAAU,EACV,IAAY,EACZ,CAAS,EACT,CAAS,EACT,IAAY,EACZ,CAAS,EACT,CAAS,EACT,CAAS,EACT,CAAC,GAAG,CAAC;IAEL,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAA;IACpD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;AAC3D,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal UI helpers. Build buttons, panels, and any style in your game code.
|
|
3
|
+
*/
|
|
4
|
+
/** Hit-test: true if (px, py) is inside the rectangle [x, y, w, h]. */
|
|
5
|
+
export declare function isPointInRect(px: number, py: number, x: number, y: number, w: number, h: number): boolean;
|
|
6
|
+
//# sourceMappingURL=UI.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UI.d.ts","sourceRoot":"","sources":["../../scripts/UI.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzG"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal UI helpers. Build buttons, panels, and any style in your game code.
|
|
3
|
+
*/
|
|
4
|
+
/** Hit-test: true if (px, py) is inside the rectangle [x, y, w, h]. */
|
|
5
|
+
export function isPointInRect(px, py, x, y, w, h) {
|
|
6
|
+
return px >= x && px < x + w && py >= y && py < y + h;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=UI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UI.js","sourceRoot":"","sources":["../../scripts/UI.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,EAAU,EAAE,EAAU,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS;IAC9F,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;AACvD,CAAC"}
|
package/dist/scripts/index.d.ts
CHANGED
|
@@ -4,4 +4,7 @@ export type { GameContext } from './types.js';
|
|
|
4
4
|
export type { InputState } from './Input.js';
|
|
5
5
|
export { createDraw2D } from './Graphics.js';
|
|
6
6
|
export type { Draw2D } from './Graphics.js';
|
|
7
|
+
export { loadFont, getTextTriangles, drawText, measureText } from './Text.js';
|
|
8
|
+
export type { Font } from './Text.js';
|
|
9
|
+
export { isPointInRect } from './UI.js';
|
|
7
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../scripts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAC3D,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAClD,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../scripts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAC3D,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAClD,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC7E,YAAY,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA"}
|
package/dist/scripts/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../scripts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAI3D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../scripts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAI3D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE7E,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-gamenative-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Native TypeScript framework for desktop games — no game engine, just SDL + WebGL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/scripts/main.js",
|
|
@@ -9,25 +9,29 @@
|
|
|
9
9
|
"dev": "cross-env NODE_ENV=development tsx scripts/main.ts",
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"start": "node dist/scripts/main.js",
|
|
12
|
-
"build:exe": "npm run build && node scripts/copy-config.js && pkg . --output release/GameNative.exe",
|
|
12
|
+
"build:exe": "npm run build && node scripts/copy-config.js && pkg . --output release/GameNative.exe && node scripts/set-exe-icon.js release/GameNative.exe",
|
|
13
13
|
"example": "npm run build && npm start",
|
|
14
14
|
"create": "node bin/create.js",
|
|
15
15
|
"create:npx": "npx create-gamenative-app@latest"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"create-gamenative-app": "
|
|
18
|
+
"create-gamenative-app": "bin/create.js"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@kmamal/gl": "^9.1.0",
|
|
22
22
|
"@kmamal/sdl": "^0.11.13",
|
|
23
|
+
"earcut": "^3.0.0",
|
|
23
24
|
"inquirer": "^9.3.8",
|
|
25
|
+
"opentype.js": "^1.3.4",
|
|
24
26
|
"pngjs": "^7.0.0"
|
|
25
27
|
},
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@types/node": "^22.10.0",
|
|
28
30
|
"cross-env": "^7.0.3",
|
|
29
31
|
"pkg": "^5.8.1",
|
|
32
|
+
"rcedit": "^5.0.0",
|
|
30
33
|
"tsx": "^4.19.2",
|
|
34
|
+
"to-ico": "^1.1.5",
|
|
31
35
|
"typescript": "^5.7.0"
|
|
32
36
|
},
|
|
33
37
|
"engines": {
|
|
@@ -38,6 +42,8 @@
|
|
|
38
42
|
"assets": [
|
|
39
43
|
"node_modules/@kmamal/sdl/**",
|
|
40
44
|
"node_modules/@kmamal/gl/**",
|
|
45
|
+
"node_modules/opentype.js/**",
|
|
46
|
+
"node_modules/earcut/**",
|
|
41
47
|
"dist/src/games/**"
|
|
42
48
|
],
|
|
43
49
|
"targets": [
|
package/scripts/Graphics.ts
CHANGED
|
@@ -25,6 +25,10 @@ void main() {
|
|
|
25
25
|
export interface Draw2D {
|
|
26
26
|
clear(r: number, g: number, b: number, a?: number): void
|
|
27
27
|
fillRect(x: number, y: number, w: number, h: number, r: number, g: number, b: number, a?: number): void
|
|
28
|
+
/** Draw triangles in pixel space. verts: [x,y, x,y, x,y, ...] (3 verts per triangle). */
|
|
29
|
+
fillTriangles(verts: Float32Array, r: number, g: number, b: number, a?: number): void
|
|
30
|
+
/** Draw rectangle outline (1px stroke). */
|
|
31
|
+
strokeRect(x: number, y: number, w: number, h: number, r: number, g: number, b: number, a?: number): void
|
|
28
32
|
dispose(): void
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -55,6 +59,7 @@ export function createDraw2D(gl: GLContext): Draw2D {
|
|
|
55
59
|
|
|
56
60
|
const buffer = gl.createBuffer()
|
|
57
61
|
const verts = new Float32Array(8)
|
|
62
|
+
const strokeWidth = 1
|
|
58
63
|
|
|
59
64
|
return {
|
|
60
65
|
clear(r: number, g: number, b: number, a = 1) {
|
|
@@ -83,6 +88,25 @@ export function createDraw2D(gl: GLContext): Draw2D {
|
|
|
83
88
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
|
84
89
|
},
|
|
85
90
|
|
|
91
|
+
fillTriangles(verts: Float32Array, r: number, g: number, b: number, a = 1) {
|
|
92
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
|
|
93
|
+
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.DYNAMIC_DRAW)
|
|
94
|
+
gl.useProgram(program)
|
|
95
|
+
gl.uniform2f(uResolution, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
|
96
|
+
gl.uniform4f(uColor, r, g, b, a)
|
|
97
|
+
gl.enableVertexAttribArray(aPosition)
|
|
98
|
+
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
|
|
99
|
+
gl.drawArrays(gl.TRIANGLES, 0, verts.length / 2)
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
strokeRect(x: number, y: number, w: number, h: number, r: number, g: number, b: number, a = 1) {
|
|
103
|
+
const s = strokeWidth
|
|
104
|
+
this.fillRect(x, y, w, s, r, g, b, a)
|
|
105
|
+
this.fillRect(x, y + h - s, w, s, r, g, b, a)
|
|
106
|
+
this.fillRect(x, y, s, h, r, g, b, a)
|
|
107
|
+
this.fillRect(x + w - s, y, s, h, r, g, b, a)
|
|
108
|
+
},
|
|
109
|
+
|
|
86
110
|
dispose() {
|
|
87
111
|
gl.deleteProgram(program)
|
|
88
112
|
gl.deleteShader(vs)
|
package/scripts/Text.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text rendering: load TTF/OTF fonts, tessellate glyphs to triangles, draw at any size.
|
|
3
|
+
* Use with createDraw2D().fillTriangles() for full control over color and style.
|
|
4
|
+
*/
|
|
5
|
+
import opentype from 'opentype.js'
|
|
6
|
+
import earcut from 'earcut'
|
|
7
|
+
import { readFile } from 'fs/promises'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
|
|
10
|
+
export type Font = {
|
|
11
|
+
getPath: (text: string, x: number, y: number, fontSize: number) => { commands: PathCommand[] }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type PathCommand =
|
|
15
|
+
| { type: 'M'; x: number; y: number }
|
|
16
|
+
| { type: 'L'; x: number; y: number }
|
|
17
|
+
| { type: 'Q'; x: number; y: number; x1: number; y1: number }
|
|
18
|
+
| { type: 'C'; x: number; y: number; x1: number; y1: number; x2: number; y2: number }
|
|
19
|
+
|
|
20
|
+
function flattenCommands(commands: PathCommand[]): number[][] {
|
|
21
|
+
const contours: number[][] = []
|
|
22
|
+
let current: number[] = []
|
|
23
|
+
let lastX = 0
|
|
24
|
+
let lastY = 0
|
|
25
|
+
|
|
26
|
+
const push = (x: number, y: number) => {
|
|
27
|
+
current.push(x, y)
|
|
28
|
+
lastX = x
|
|
29
|
+
lastY = y
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const cmd of commands) {
|
|
33
|
+
if (cmd.type === 'M') {
|
|
34
|
+
if (current.length >= 6) contours.push(current)
|
|
35
|
+
current = []
|
|
36
|
+
push(cmd.x, cmd.y)
|
|
37
|
+
} else if (cmd.type === 'L') {
|
|
38
|
+
push(cmd.x, cmd.y)
|
|
39
|
+
} else if (cmd.type === 'Q') {
|
|
40
|
+
for (let t = 1; t <= 4; t++) {
|
|
41
|
+
const s = t / 4
|
|
42
|
+
const u = 1 - s
|
|
43
|
+
const x = u * u * lastX + 2 * u * s * cmd.x1 + s * s * cmd.x
|
|
44
|
+
const y = u * u * lastY + 2 * u * s * cmd.y1 + s * s * cmd.y
|
|
45
|
+
push(x, y)
|
|
46
|
+
}
|
|
47
|
+
} else if (cmd.type === 'C') {
|
|
48
|
+
for (let t = 1; t <= 8; t++) {
|
|
49
|
+
const s = t / 8
|
|
50
|
+
const u = 1 - s
|
|
51
|
+
const x =
|
|
52
|
+
u * u * u * lastX +
|
|
53
|
+
3 * u * u * s * cmd.x1 +
|
|
54
|
+
3 * u * s * s * cmd.x2 +
|
|
55
|
+
s * s * s * cmd.x
|
|
56
|
+
const y =
|
|
57
|
+
u * u * u * lastY +
|
|
58
|
+
3 * u * u * s * cmd.y1 +
|
|
59
|
+
3 * u * s * s * cmd.y2 +
|
|
60
|
+
s * s * s * cmd.y
|
|
61
|
+
push(x, y)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (current.length >= 6) contours.push(current)
|
|
66
|
+
return contours
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function signedArea(contour: number[]): number {
|
|
70
|
+
let sum = 0
|
|
71
|
+
const n = contour.length / 2
|
|
72
|
+
for (let i = 0; i < n; i++) {
|
|
73
|
+
const j = (i + 1) % n
|
|
74
|
+
sum += (contour[2 * j] - contour[2 * i]) * (contour[2 * j + 1] + contour[2 * i + 1])
|
|
75
|
+
}
|
|
76
|
+
return sum * 0.5
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Load a TTF or OTF font from path (relative to cwd or absolute).
|
|
81
|
+
*/
|
|
82
|
+
export async function loadFont(path: string): Promise<Font> {
|
|
83
|
+
const abs = path.startsWith('/') || /^[A-Za-z]:/.test(path) ? path : join(process.cwd(), path)
|
|
84
|
+
const buffer = await readFile(abs)
|
|
85
|
+
const ab = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
|
|
86
|
+
return opentype.parse(ab) as unknown as Font
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tessellate text into triangles in pixel space. (x, y) = top-left of text; y-axis down.
|
|
91
|
+
* Use with draw2d.fillTriangles(triangles, r, g, b, a).
|
|
92
|
+
*/
|
|
93
|
+
export function getTextTriangles(font: Font, text: string, x: number, y: number, size: number): Float32Array {
|
|
94
|
+
const path = font.getPath(text, 0, 0, size)
|
|
95
|
+
const commands = path.commands as PathCommand[]
|
|
96
|
+
const contours = flattenCommands(commands)
|
|
97
|
+
if (contours.length === 0) return new Float32Array(0)
|
|
98
|
+
|
|
99
|
+
const areas = contours.map((c) => signedArea(c))
|
|
100
|
+
const outerIdx = areas.reduce((best, a, i) => (Math.abs(a) > Math.abs(areas[best]) ? i : best), 0)
|
|
101
|
+
const outer = contours[outerIdx]
|
|
102
|
+
const holes: number[][] = contours.filter((_, i) => i !== outerIdx)
|
|
103
|
+
const verts: number[] = [...outer]
|
|
104
|
+
const holeIndices: number[] = []
|
|
105
|
+
for (const h of holes) {
|
|
106
|
+
holeIndices.push(verts.length / 2)
|
|
107
|
+
verts.push(...h)
|
|
108
|
+
}
|
|
109
|
+
const indices = earcut(verts, holeIndices, 2)
|
|
110
|
+
const out: number[] = []
|
|
111
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
112
|
+
const i0 = indices[i] * 2
|
|
113
|
+
const i1 = indices[i + 1] * 2
|
|
114
|
+
const i2 = indices[i + 2] * 2
|
|
115
|
+
out.push(x + verts[i0], y + size - verts[i0 + 1])
|
|
116
|
+
out.push(x + verts[i1], y + size - verts[i1 + 1])
|
|
117
|
+
out.push(x + verts[i2], y + size - verts[i2 + 1])
|
|
118
|
+
}
|
|
119
|
+
return new Float32Array(out)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Measure text width and height at given size (approximate from path bounds).
|
|
124
|
+
*/
|
|
125
|
+
export function measureText(font: Font, text: string, size: number): { width: number; height: number } {
|
|
126
|
+
const path = font.getPath(text, 0, 0, size)
|
|
127
|
+
const commands = path.commands as PathCommand[]
|
|
128
|
+
let x1 = Infinity
|
|
129
|
+
let y1 = Infinity
|
|
130
|
+
let x2 = -Infinity
|
|
131
|
+
let y2 = -Infinity
|
|
132
|
+
for (const c of commands) {
|
|
133
|
+
const x = 'x' in c ? c.x : 0
|
|
134
|
+
const y = 'y' in c ? c.y : 0
|
|
135
|
+
x1 = Math.min(x1, x)
|
|
136
|
+
y1 = Math.min(y1, y)
|
|
137
|
+
x2 = Math.max(x2, x)
|
|
138
|
+
y2 = Math.max(y2, y)
|
|
139
|
+
}
|
|
140
|
+
if (x1 === Infinity) return { width: 0, height: size }
|
|
141
|
+
return { width: x2 - x1, height: y2 - y1 }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Draw text with a Draw2D instance. (x,y) = top-left, y-axis down. */
|
|
145
|
+
export function drawText(
|
|
146
|
+
draw2d: { fillTriangles: (v: Float32Array, r: number, g: number, b: number, a?: number) => void },
|
|
147
|
+
font: Font,
|
|
148
|
+
text: string,
|
|
149
|
+
x: number,
|
|
150
|
+
y: number,
|
|
151
|
+
size: number,
|
|
152
|
+
r: number,
|
|
153
|
+
g: number,
|
|
154
|
+
b: number,
|
|
155
|
+
a = 1
|
|
156
|
+
): void {
|
|
157
|
+
const tri = getTextTriangles(font, text, x, y, size)
|
|
158
|
+
if (tri.length > 0) draw2d.fillTriangles(tri, r, g, b, a)
|
|
159
|
+
}
|
package/scripts/UI.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal UI helpers. Build buttons, panels, and any style in your game code.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Hit-test: true if (px, py) is inside the rectangle [x, y, w, h]. */
|
|
6
|
+
export function isPointInRect(px: number, py: number, x: number, y: number, w: number, h: number): boolean {
|
|
7
|
+
return px >= x && px < x + w && py >= y && py < y + h
|
|
8
|
+
}
|
package/scripts/index.ts
CHANGED
|
@@ -4,3 +4,6 @@ export type { GameContext } from './types.js'
|
|
|
4
4
|
export type { InputState } from './Input.js'
|
|
5
5
|
export { createDraw2D } from './Graphics.js'
|
|
6
6
|
export type { Draw2D } from './Graphics.js'
|
|
7
|
+
export { loadFont, getTextTriangles, drawText, measureText } from './Text.js'
|
|
8
|
+
export type { Font } from './Text.js'
|
|
9
|
+
export { isPointInRect } from './UI.js'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeds assets/icon.png into the built .exe so the Windows taskbar shows it.
|
|
3
|
+
* Run after pkg on Windows only. Usage: node scripts/set-exe-icon.js release/MyGame.exe
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
6
|
+
import { join, dirname } from 'path'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
|
|
11
|
+
if (process.platform !== 'win32') process.exit(0)
|
|
12
|
+
|
|
13
|
+
const exePath = process.argv[2]
|
|
14
|
+
if (!exePath) process.exit(0)
|
|
15
|
+
|
|
16
|
+
const iconPng = join(process.cwd(), 'assets', 'icon.png')
|
|
17
|
+
if (!existsSync(iconPng)) process.exit(0)
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const toIco = (await import('to-ico')).default
|
|
21
|
+
const { rcedit } = await import('rcedit')
|
|
22
|
+
|
|
23
|
+
const pngBuffer = readFileSync(iconPng)
|
|
24
|
+
const icoBuffer = await toIco([pngBuffer])
|
|
25
|
+
const releaseDir = join(process.cwd(), 'release')
|
|
26
|
+
mkdirSync(releaseDir, { recursive: true })
|
|
27
|
+
const icoPath = join(releaseDir, 'icon.ico')
|
|
28
|
+
writeFileSync(icoPath, icoBuffer)
|
|
29
|
+
|
|
30
|
+
const absExe = join(process.cwd(), exePath)
|
|
31
|
+
if (existsSync(absExe)) {
|
|
32
|
+
await rcedit(absExe, { icon: icoPath })
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Optional: rcedit/to-ico may be missing or fail
|
|
36
|
+
}
|