create-gamenative-app 0.1.1

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.
Files changed (39) hide show
  1. package/README.md +116 -0
  2. package/bin/create.js +255 -0
  3. package/create-gamenative-app/README.md +33 -0
  4. package/create-gamenative-app/index.js +107 -0
  5. package/create-gamenative-app/package.json +33 -0
  6. package/create-gamenative-app/sync-template.js +177 -0
  7. package/create-gamenative-app/template/FRAMEWORK.md +14 -0
  8. package/create-gamenative-app/template/README.md +10 -0
  9. package/create-gamenative-app/template/assets/icon.png +0 -0
  10. package/create-gamenative-app/template/game.config.json +9 -0
  11. package/create-gamenative-app/template/package.json +39 -0
  12. package/create-gamenative-app/template/scripts/copy-config.js +8 -0
  13. package/create-gamenative-app/template/src/Game.ts +97 -0
  14. package/create-gamenative-app/template/src/Graphics.ts +93 -0
  15. package/create-gamenative-app/template/src/Input.ts +83 -0
  16. package/create-gamenative-app/template/src/config.ts +25 -0
  17. package/create-gamenative-app/template/src/games/Game.ts +32 -0
  18. package/create-gamenative-app/template/src/icon.ts +19 -0
  19. package/create-gamenative-app/template/src/index.ts +6 -0
  20. package/create-gamenative-app/template/src/main.ts +39 -0
  21. package/create-gamenative-app/template/src/pngjs.d.ts +5 -0
  22. package/create-gamenative-app/template/src/types.ts +35 -0
  23. package/create-gamenative-app/template/tsconfig.json +23 -0
  24. package/example.ts +45 -0
  25. package/game.config.json +9 -0
  26. package/package.json +47 -0
  27. package/scripts/copy-config.js +8 -0
  28. package/src/Game.ts +97 -0
  29. package/src/Graphics.ts +93 -0
  30. package/src/Input.ts +83 -0
  31. package/src/assets/icon.png +0 -0
  32. package/src/config.ts +25 -0
  33. package/src/games/ExampleGame.ts +49 -0
  34. package/src/icon.ts +19 -0
  35. package/src/index.ts +6 -0
  36. package/src/main.ts +39 -0
  37. package/src/pngjs.d.ts +5 -0
  38. package/src/types.ts +35 -0
  39. package/tsconfig.json +18 -0
@@ -0,0 +1,35 @@
1
+ import type { InputState } from './Input.js'
2
+ import type { WebGLRenderingContext as GLContext } from '@kmamal/gl'
3
+
4
+ /**
5
+ * Config passed when creating the game window.
6
+ */
7
+ export interface GameConfig {
8
+ title?: string
9
+ width?: number
10
+ height?: number
11
+ vsync?: boolean
12
+ resizable?: boolean
13
+ /** Path to PNG for window/taskbar icon */
14
+ icon?: string
15
+ }
16
+
17
+ /**
18
+ * Context passed to init and available each frame. width/height update on resize.
19
+ */
20
+ export interface GameContext {
21
+ gl: GLContext
22
+ input: InputState
23
+ width: number
24
+ height: number
25
+ }
26
+
27
+ /**
28
+ * Your game implements this. Framework calls init once, then update(dt) and draw() each frame.
29
+ */
30
+ export interface IGame {
31
+ init?(ctx: GameContext, config: GameConfig): void | Promise<void>
32
+ update(dt: number): void
33
+ draw(): void
34
+ dispose?(): void
35
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": ".",
8
+ "strict": true,
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "skipLibCheck": true,
13
+ "esModuleInterop": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": [
17
+ "src/**/*.ts"
18
+ ],
19
+ "exclude": [
20
+ "node_modules",
21
+ "dist"
22
+ ]
23
+ }
package/example.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Minimal example: clear to a color, draw a moving rect, exit on Escape.
3
+ */
4
+ import { run, createDraw2D, SCANCODE } from './src/index.js'
5
+ import type { GameContext } from './src/types.js'
6
+
7
+ const game = {
8
+ ctx: null as GameContext | null,
9
+ draw2d: null as ReturnType<typeof createDraw2D> | null,
10
+ x: 100,
11
+ vx: 120,
12
+
13
+ async init(ctx: GameContext) {
14
+ this.ctx = ctx
15
+ this.draw2d = createDraw2D(ctx.gl)
16
+ },
17
+
18
+ update(dt: number) {
19
+ const c = this.ctx!
20
+ this.x += this.vx * dt
21
+ if (this.x < 0) {
22
+ this.x = 0
23
+ this.vx = -this.vx
24
+ }
25
+ if (this.x > c.width - 80) {
26
+ this.x = c.width - 80
27
+ this.vx = -this.vx
28
+ }
29
+ if (c.input.isKeyDown(SCANCODE.Escape)) {
30
+ process.exit(0)
31
+ }
32
+ },
33
+
34
+ draw() {
35
+ const d = this.draw2d!
36
+ d.clear(0.1, 0.1, 0.15, 1)
37
+ d.fillRect(this.x, 200, 80, 60, 0.2, 0.6, 0.9, 1)
38
+ },
39
+
40
+ dispose() {
41
+ this.draw2d?.dispose()
42
+ },
43
+ }
44
+
45
+ run(game, { title: 'GameNative Example', width: 640, height: 480 })
@@ -0,0 +1,9 @@
1
+ {
2
+ "window": {
3
+ "title": "My Game",
4
+ "width": 800,
5
+ "height": 600,
6
+ "icon": "assets/icon.png"
7
+ },
8
+ "game": "ExampleGame"
9
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "create-gamenative-app",
3
+ "version": "0.1.1",
4
+ "description": "Native TypeScript framework for desktop games — no game engine, just SDL + WebGL.",
5
+ "type": "module",
6
+ "main": "dist/main.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "dev": "cross-env NODE_ENV=development tsx src/main.ts",
10
+ "build": "tsc",
11
+ "start": "node dist/main.js",
12
+ "build:exe": "npm run build && node scripts/copy-config.js && pkg . --output release/GameNative.exe",
13
+ "example": "npm run build && npm start",
14
+ "create": "node bin/create.js",
15
+ "create:npx": "npx create-gamenative-app@latest"
16
+ },
17
+ "bin": {
18
+ "create-gamenative-app": "./bin/create.js"
19
+ },
20
+ "dependencies": {
21
+ "@kmamal/gl": "^9.1.0",
22
+ "@kmamal/sdl": "^0.11.13",
23
+ "pngjs": "^7.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.10.0",
27
+ "cross-env": "^7.0.3",
28
+ "inquirer": "^9.3.8",
29
+ "pkg": "^5.8.1",
30
+ "tsx": "^4.19.2",
31
+ "typescript": "^5.7.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "pkg": {
37
+ "scripts": "dist/main.js",
38
+ "assets": [
39
+ "node_modules/@kmamal/sdl/**",
40
+ "node_modules/@kmamal/gl/**",
41
+ "dist/games/**"
42
+ ],
43
+ "targets": [
44
+ "node18-win-x64"
45
+ ]
46
+ }
47
+ }
@@ -0,0 +1,8 @@
1
+ import { mkdirSync, copyFileSync, existsSync } from 'fs'
2
+ import { join } from 'path'
3
+ const release = join(process.cwd(), 'release')
4
+ mkdirSync(release, { recursive: true })
5
+ const src = join(process.cwd(), 'game.config.json')
6
+ if (existsSync(src)) {
7
+ copyFileSync(src, join(release, 'game.config.json'))
8
+ }
package/src/Game.ts ADDED
@@ -0,0 +1,97 @@
1
+ import sdl from '@kmamal/sdl'
2
+ import glPkg from '@kmamal/gl'
3
+ import type { WebGLRenderingContext as GLContext } from '@kmamal/gl'
4
+ import type { IGame, GameConfig, GameContext } from './types.js'
5
+ import { createInput } from './Input.js'
6
+
7
+ type CreateGL = (width: number, height: number, attrs?: { window?: unknown }) => GLContext
8
+ const createGLContext = (typeof glPkg === 'function' ? glPkg : (glPkg as { default: CreateGL }).default) as CreateGL
9
+
10
+ const DEFAULT_CONFIG: Required<Omit<GameConfig, 'icon'>> & Pick<GameConfig, 'icon'> = {
11
+ title: 'Game',
12
+ width: 800,
13
+ height: 600,
14
+ vsync: true,
15
+ resizable: true,
16
+ }
17
+
18
+ /**
19
+ * Runs an IGame: creates SDL window + WebGL context, drives update/draw loop, handles resize and close.
20
+ */
21
+ export async function run(game: IGame, config: GameConfig = {}): Promise<void> {
22
+ const opts = { ...DEFAULT_CONFIG, ...config }
23
+ const window = sdl.video.createWindow({
24
+ title: opts.title,
25
+ width: opts.width,
26
+ height: opts.height,
27
+ resizable: opts.resizable,
28
+ vsync: opts.vsync,
29
+ opengl: true,
30
+ })
31
+
32
+ if (opts.icon) {
33
+ try {
34
+ const { loadIcon } = await import('./icon.js')
35
+ await loadIcon(window as { setIcon: (w: number, h: number, stride: number, format: string, buffer: Buffer) => void }, opts.icon)
36
+ } catch {
37
+ // icon optional; ignore if loadIcon or file fails
38
+ }
39
+ }
40
+
41
+ let width = window.pixelWidth
42
+ let height = window.pixelHeight
43
+ const gl = createGLContext(width, height, { window: window.native })
44
+ const input = createInput(sdl)
45
+
46
+ const ctx: GameContext = {
47
+ gl,
48
+ input,
49
+ get width() {
50
+ return width
51
+ },
52
+ get height() {
53
+ return height
54
+ },
55
+ }
56
+
57
+ if (game.init) {
58
+ await game.init(ctx, { ...opts, width, height })
59
+ }
60
+
61
+ window.on('resize', (e: { pixelWidth: number; pixelHeight: number }) => {
62
+ width = e.pixelWidth
63
+ height = e.pixelHeight
64
+ gl.viewport(0, 0, width, height)
65
+ })
66
+
67
+ let running = true
68
+ window.on('close', () => {
69
+ running = false
70
+ })
71
+
72
+ let lastTime = performance.now()
73
+ const maxDt = 0.1
74
+
75
+ function loop() {
76
+ if (!running) {
77
+ if (game.dispose) game.dispose()
78
+ window.destroy()
79
+ return
80
+ }
81
+ const now = performance.now()
82
+ let dt = (now - lastTime) / 1000
83
+ lastTime = now
84
+ if (dt > maxDt) dt = maxDt
85
+
86
+ game.update(dt)
87
+ game.draw()
88
+ gl.swap()
89
+ setImmediate(loop)
90
+ }
91
+
92
+ setImmediate(loop)
93
+ }
94
+
95
+ export type { IGame, GameConfig }
96
+ export { sdl, createInput }
97
+ export { SCANCODE } from './Input.js'
@@ -0,0 +1,93 @@
1
+ import type { WebGLRenderingContext as GLContext } from '@kmamal/gl'
2
+
3
+ /**
4
+ * Minimal 2D helpers: clear, and draw solid-color quads with a simple shader.
5
+ * For anything else use the raw WebGL context.
6
+ */
7
+
8
+ const VERT = `
9
+ attribute vec2 a_position;
10
+ uniform vec2 u_resolution;
11
+ void main() {
12
+ vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
13
+ gl_Position = vec4(clip * vec2(1, -1), 0, 1);
14
+ }
15
+ `
16
+
17
+ const FRAG = `
18
+ precision mediump float;
19
+ uniform vec4 u_color;
20
+ void main() {
21
+ gl_FragColor = u_color;
22
+ }
23
+ `
24
+
25
+ export interface Draw2D {
26
+ clear(r: number, g: number, b: number, a?: number): void
27
+ fillRect(x: number, y: number, w: number, h: number, r: number, g: number, b: number, a?: number): void
28
+ dispose(): void
29
+ }
30
+
31
+ export function createDraw2D(gl: GLContext): Draw2D {
32
+ const vs = gl.createShader(gl.VERTEX_SHADER)!
33
+ gl.shaderSource(vs, VERT)
34
+ gl.compileShader(vs)
35
+ if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
36
+ throw new Error('vert: ' + gl.getShaderInfoLog(vs))
37
+ }
38
+ const fs = gl.createShader(gl.FRAGMENT_SHADER)!
39
+ gl.shaderSource(fs, FRAG)
40
+ gl.compileShader(fs)
41
+ if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
42
+ throw new Error('frag: ' + gl.getShaderInfoLog(fs))
43
+ }
44
+ const program = gl.createProgram()!
45
+ gl.attachShader(program, vs)
46
+ gl.attachShader(program, fs)
47
+ gl.linkProgram(program)
48
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
49
+ throw new Error('link: ' + gl.getProgramInfoLog(program))
50
+ }
51
+
52
+ const aPosition = gl.getAttribLocation(program, 'a_position')
53
+ const uResolution = gl.getUniformLocation(program, 'u_resolution')
54
+ const uColor = gl.getUniformLocation(program, 'u_color')
55
+
56
+ const buffer = gl.createBuffer()
57
+ const verts = new Float32Array(8)
58
+
59
+ return {
60
+ clear(r: number, g: number, b: number, a = 1) {
61
+ gl.clearColor(r, g, b, a)
62
+ gl.clear(gl.COLOR_BUFFER_BIT)
63
+ },
64
+
65
+ fillRect(x: number, y: number, w: number, h: number, r: number, g: number, b: number, a = 1) {
66
+ const x2 = x + w
67
+ const y2 = y + h
68
+ verts[0] = x
69
+ verts[1] = y
70
+ verts[2] = x2
71
+ verts[3] = y
72
+ verts[4] = x
73
+ verts[5] = y2
74
+ verts[6] = x2
75
+ verts[7] = y2
76
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
77
+ gl.bufferData(gl.ARRAY_BUFFER, verts, gl.DYNAMIC_DRAW)
78
+ gl.useProgram(program)
79
+ gl.uniform2f(uResolution, gl.drawingBufferWidth, gl.drawingBufferHeight)
80
+ gl.uniform4f(uColor, r, g, b, a)
81
+ gl.enableVertexAttribArray(aPosition)
82
+ gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
83
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
84
+ },
85
+
86
+ dispose() {
87
+ gl.deleteProgram(program)
88
+ gl.deleteShader(vs)
89
+ gl.deleteShader(fs)
90
+ gl.deleteBuffer(buffer)
91
+ },
92
+ }
93
+ }
package/src/Input.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Input state: keyboard and mouse. Read each frame in update().
3
+ * Keys use SDL scancode names; common ones: W, A, S, D, Up, Down, Left, Right, Space, Escape, etc.
4
+ */
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ type SDL = any
8
+
9
+ export interface InputState {
10
+ readonly mouseX: number
11
+ readonly mouseY: number
12
+ readonly mouseLeft: boolean
13
+ readonly mouseRight: boolean
14
+ readonly mouseMiddle: boolean
15
+ isKeyDown(scancode: number): boolean
16
+ }
17
+
18
+ export function createInput(sdl: SDL): InputState {
19
+ const keyboard = sdl.keyboard
20
+ const mouse = sdl.mouse
21
+
22
+ return {
23
+ get mouseX() {
24
+ return mouse.position.x
25
+ },
26
+ get mouseY() {
27
+ return mouse.position.y
28
+ },
29
+ get mouseLeft() {
30
+ return mouse.getButton(1)
31
+ },
32
+ get mouseRight() {
33
+ return mouse.getButton(3)
34
+ },
35
+ get mouseMiddle() {
36
+ return mouse.getButton(2)
37
+ },
38
+ isKeyDown(scancode: number) {
39
+ const state = keyboard.getState()
40
+ return !!(Array.isArray(state) ? state[scancode] : (state as { [k: number]: boolean })[scancode])
41
+ },
42
+ }
43
+ }
44
+
45
+ /** SDL scancode constants for use with input.isKeyDown(sdl.SCANCODE.X). Use the sdl instance from run(). */
46
+ export const SCANCODE = {
47
+ A: 4,
48
+ B: 5,
49
+ C: 6,
50
+ D: 7,
51
+ E: 8,
52
+ F: 9,
53
+ G: 10,
54
+ H: 11,
55
+ I: 12,
56
+ J: 13,
57
+ K: 14,
58
+ L: 15,
59
+ M: 16,
60
+ N: 17,
61
+ O: 18,
62
+ P: 19,
63
+ Q: 20,
64
+ R: 21,
65
+ S: 22,
66
+ T: 23,
67
+ U: 24,
68
+ V: 25,
69
+ W: 26,
70
+ X: 27,
71
+ Y: 28,
72
+ Z: 29,
73
+ Space: 44,
74
+ Escape: 41,
75
+ Up: 82,
76
+ Down: 81,
77
+ Left: 80,
78
+ Right: 79,
79
+ Enter: 40,
80
+ Shift: 225,
81
+ Ctrl: 224,
82
+ Tab: 43,
83
+ } as const
Binary file
package/src/config.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { readFile } from 'fs/promises'
2
+ import { join } from 'path'
3
+
4
+ export interface AppConfig {
5
+ window?: {
6
+ title?: string
7
+ width?: number
8
+ height?: number
9
+ icon?: string
10
+ }
11
+ /** Game name (no extension). Resolved to src/games/<game>.ts in dev, dist/games/<game>.js in prod. */
12
+ game?: string
13
+ }
14
+
15
+ const CONFIG_FILENAME = 'game.config.json'
16
+
17
+ export async function loadAppConfig(): Promise<AppConfig> {
18
+ const path = join(process.cwd(), CONFIG_FILENAME)
19
+ try {
20
+ const raw = await readFile(path, 'utf-8')
21
+ return JSON.parse(raw) as AppConfig
22
+ } catch {
23
+ return {}
24
+ }
25
+ }
@@ -0,0 +1,49 @@
1
+ import { createDraw2D, SCANCODE } from '../index.js'
2
+ import type { GameContext, IGame } from '../types.js'
3
+
4
+ interface ExampleGame extends IGame {
5
+ ctx: GameContext | null
6
+ draw2d: ReturnType<typeof createDraw2D> | null
7
+ x: number
8
+ vx: number
9
+ }
10
+
11
+ const game: ExampleGame = {
12
+ ctx: null,
13
+ draw2d: null,
14
+ x: 100,
15
+ vx: 120,
16
+
17
+ async init(ctx: GameContext) {
18
+ this.ctx = ctx
19
+ this.draw2d = createDraw2D(ctx.gl)
20
+ },
21
+
22
+ update(dt: number) {
23
+ const c = this.ctx!
24
+ this.x += this.vx * dt
25
+ if (this.x < 0) {
26
+ this.x = 0
27
+ this.vx = -this.vx
28
+ }
29
+ if (this.x > c.width - 80) {
30
+ this.x = c.width - 80
31
+ this.vx = -this.vx
32
+ }
33
+ if (c.input.isKeyDown(SCANCODE.Escape)) {
34
+ process.exit(0)
35
+ }
36
+ },
37
+
38
+ draw() {
39
+ const d = this.draw2d!
40
+ d.clear(0.1, 0.1, 0.15, 1)
41
+ d.fillRect(this.x, 200, 80, 60, 0.2, 0.6, 0.9, 1)
42
+ },
43
+
44
+ dispose() {
45
+ this.draw2d?.dispose()
46
+ },
47
+ }
48
+
49
+ export default game
package/src/icon.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { readFile } from 'fs/promises'
2
+ import { join } from 'path'
3
+
4
+ /**
5
+ * Load a PNG from path and set it as the window icon. Path can be absolute or relative to cwd.
6
+ */
7
+ export async function loadIcon(
8
+ window: { setIcon: (w: number, h: number, stride: number, format: string, buffer: Buffer) => void },
9
+ iconPath: string
10
+ ): Promise<void> {
11
+ const pngjs = await import('pngjs')
12
+ const PNG = pngjs.default?.PNG ?? pngjs.PNG
13
+ const abs = join(process.cwd(), iconPath)
14
+ const buf = await readFile(abs)
15
+ const png = PNG.sync.read(buf)
16
+ const stride = png.width * 4
17
+ const buffer = Buffer.from(png.data)
18
+ window.setIcon(png.width, png.height, stride, 'rgba32', buffer)
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { run, sdl, createInput, SCANCODE } from './Game.js'
2
+ export type { IGame, GameConfig } from './Game.js'
3
+ export type { GameContext } from './types.js'
4
+ export type { InputState } from './Input.js'
5
+ export { createDraw2D } from './Graphics.js'
6
+ export type { Draw2D } from './Graphics.js'
package/src/main.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { pathToFileURL } from 'url'
2
+ import { join, dirname } from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { run } from './Game.js'
5
+ import { loadAppConfig } from './config.js'
6
+ import type { IGame } from './types.js'
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url))
9
+
10
+ async function main() {
11
+ const appConfig = await loadAppConfig()
12
+ const gameName = appConfig.game ?? 'ExampleGame'
13
+ const isDev = process.env.NODE_ENV === 'development'
14
+ const isPkg = typeof (process as { pkg?: unknown }).pkg !== 'undefined'
15
+ const gamePath =
16
+ isPkg || !isDev
17
+ ? join(__dirname, 'games', `${gameName}.js`)
18
+ : join(process.cwd(), 'src', 'games', `${gameName}.ts`)
19
+ const href = pathToFileURL(gamePath).href
20
+
21
+ const mod = await import(/* @vite-ignore */ href)
22
+ const game: IGame = mod.default
23
+ if (!game || typeof game.update !== 'function' || typeof game.draw !== 'function') {
24
+ throw new Error(`Game "${gameName}" must export a default IGame (update/draw)`)
25
+ }
26
+
27
+ const windowOpts = appConfig.window ?? {}
28
+ await run(game, {
29
+ title: windowOpts.title ?? 'Game',
30
+ width: windowOpts.width ?? 800,
31
+ height: windowOpts.height ?? 600,
32
+ icon: windowOpts.icon,
33
+ })
34
+ }
35
+
36
+ main().catch((err) => {
37
+ console.error(err)
38
+ process.exit(1)
39
+ })
package/src/pngjs.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module 'pngjs' {
2
+ export const PNG: {
3
+ sync: { read(buffer: Buffer): { width: number; height: number; data: Buffer | Uint8Array } }
4
+ }
5
+ }
package/src/types.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { InputState } from './Input.js'
2
+ import type { WebGLRenderingContext as GLContext } from '@kmamal/gl'
3
+
4
+ /**
5
+ * Config passed when creating the game window.
6
+ */
7
+ export interface GameConfig {
8
+ title?: string
9
+ width?: number
10
+ height?: number
11
+ vsync?: boolean
12
+ resizable?: boolean
13
+ /** Path to PNG for window/taskbar icon */
14
+ icon?: string
15
+ }
16
+
17
+ /**
18
+ * Context passed to init and available each frame. width/height update on resize.
19
+ */
20
+ export interface GameContext {
21
+ gl: GLContext
22
+ input: InputState
23
+ width: number
24
+ height: number
25
+ }
26
+
27
+ /**
28
+ * Your game implements this. Framework calls init once, then update(dt) and draw() each frame.
29
+ */
30
+ export interface IGame {
31
+ init?(ctx: GameContext, config: GameConfig): void | Promise<void>
32
+ update(dt: number): void
33
+ draw(): void
34
+ dispose?(): void
35
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": ".",
8
+ "strict": true,
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "skipLibCheck": true,
13
+ "esModuleInterop": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": ["src/**/*.ts"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }