create-gamenative-app 0.1.6 → 0.1.7

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 CHANGED
@@ -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**: Draw quads with \`fillRect\` or textured quads; track \`ctx.input.mouseX/Y\` and \`mouseLeft\` for clicks; implement panels/buttons in \`update\`/\`draw\`.
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 optional helpers you add.
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)
@@ -327,7 +348,6 @@ function npmInstall(cwd) {
327
348
  const child = spawn(isWin ? 'npm.cmd' : 'npm', ['install'], {
328
349
  cwd,
329
350
  stdio: 'inherit',
330
- shell: isWin,
331
351
  })
332
352
  child.on('close', (code) => (code === 0 ? resolve() : reject(new Error('npm install failed'))))
333
353
  })
@@ -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,CA8DlD"}
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"}
@@ -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":"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"}
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;AAYD,MAAM,UAAU,YAAY,CAAC,EAAa;IACxC,MAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,aAAa,CAAE,CAAA;IAC7C,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IACzB,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;IACpB,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAA;IACrD,CAAC;IACD,MAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,eAAe,CAAE,CAAA;IAC/C,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IACzB,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;IACpB,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAA;IACrD,CAAC;IACD,MAAM,OAAO,GAAG,EAAE,CAAC,aAAa,EAAG,CAAA;IACnC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAC5B,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAC5B,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;IACvB,IAAI,CAAC,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAA;IAC3D,CAAC;IAED,MAAM,SAAS,GAAG,EAAE,CAAC,iBAAiB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IAC7D,MAAM,WAAW,GAAG,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;IAClE,MAAM,MAAM,GAAG,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IAExD,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,EAAE,CAAA;IAChC,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAA;IACjC,MAAM,WAAW,GAAG,CAAC,CAAA;IAErB,OAAO;QACL,KAAK,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAC,GAAG,CAAC;YAC1C,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YACzB,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAA;QAC/B,CAAC;QAED,QAAQ,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAC,GAAG,CAAC;YACzF,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;YAChB,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;YAChB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;YACZ,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;YACZ,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;YACb,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;YACZ,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;YACZ,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;YACb,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;YACb,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;YACb,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC,CAAA;YACtC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,CAAA;YACtD,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;YACtB,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,CAAC,kBAAkB,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAA;YACxE,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YAChC,EAAE,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAA;YACrC,EAAE,CAAC,mBAAmB,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YAC3D,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QACxC,CAAC;QAED,aAAa,CAAC,KAAmB,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAC,GAAG,CAAC;YACvE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC,CAAA;YACtC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,CAAA;YACtD,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;YACtB,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,CAAC,kBAAkB,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAA;YACxE,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YAChC,EAAE,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAA;YACrC,EAAE,CAAC,mBAAmB,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YAC3D,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAClD,CAAC;QAED,UAAU,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAC,GAAG,CAAC;YAC3F,MAAM,CAAC,GAAG,WAAW,CAAA;YACrB,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YACrC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YAC7C,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YACrC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC/C,CAAC;QAED,OAAO;YACL,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;YACzB,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;YACnB,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;YACnB,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;QACzB,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -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":"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"}
@@ -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"}
@@ -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"}
@@ -1,3 +1,5 @@
1
1
  export { run, sdl, createInput, SCANCODE } from './Game.js';
2
2
  export { createDraw2D } from './Graphics.js';
3
+ export { loadFont, getTextTriangles, drawText, measureText } from './Text.js';
4
+ export { isPointInRect } from './UI.js';
3
5
  //# sourceMappingURL=index.js.map
@@ -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.6",
3
+ "version": "0.1.7",
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": "./bin/create.js"
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": [
@@ -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)
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'earcut' {
2
+ function earcut(vertices: number[], holes?: number[], dimensions?: number): number[]
3
+ export default earcut
4
+ }
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,8 @@
1
+ declare module 'opentype.js' {
2
+ const opentype: {
3
+ parse(buffer: ArrayBuffer): {
4
+ getPath: (text: string, x: number, y: number, fontSize: number) => { commands: unknown[] }
5
+ }
6
+ }
7
+ export default opentype
8
+ }
@@ -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
+ }