create-gamenative-app 0.1.5 → 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 +170 -37
- package/dist/scripts/Game.d.ts +11 -0
- package/dist/scripts/Game.d.ts.map +1 -0
- package/dist/scripts/Game.js +83 -0
- package/dist/scripts/Game.js.map +1 -0
- package/dist/scripts/Graphics.d.ts +12 -0
- package/dist/scripts/Graphics.d.ts.map +1 -0
- package/dist/scripts/Graphics.js +96 -0
- package/dist/scripts/Graphics.js.map +1 -0
- package/dist/scripts/Input.d.ts +55 -0
- package/dist/scripts/Input.d.ts.map +1 -0
- package/dist/scripts/Input.js +69 -0
- package/dist/scripts/Input.js.map +1 -0
- 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/config.d.ts +12 -0
- package/dist/scripts/config.d.ts.map +1 -0
- package/dist/scripts/config.js +14 -0
- package/dist/scripts/config.js.map +1 -0
- package/dist/scripts/icon.d.ts +7 -0
- package/dist/scripts/icon.d.ts.map +1 -0
- package/dist/scripts/icon.js +16 -0
- package/dist/scripts/icon.js.map +1 -0
- package/dist/scripts/index.d.ts +10 -0
- package/dist/scripts/index.d.ts.map +1 -0
- package/dist/scripts/index.js +5 -0
- package/dist/scripts/index.js.map +1 -0
- package/dist/scripts/main.d.ts +2 -0
- package/dist/scripts/main.d.ts.map +1 -0
- package/dist/scripts/main.js +33 -0
- package/dist/scripts/main.js.map +1 -0
- package/dist/scripts/types.d.ts +33 -0
- package/dist/scripts/types.d.ts.map +1 -0
- package/dist/scripts/types.js +2 -0
- package/dist/scripts/types.js.map +1 -0
- package/dist/src/games/ExampleGame.d.ts +11 -0
- package/dist/src/games/ExampleGame.d.ts.map +1 -0
- package/dist/src/games/ExampleGame.js +36 -0
- package/dist/src/games/ExampleGame.js.map +1 -0
- package/package.json +15 -9
- package/{src → scripts}/Graphics.ts +24 -0
- package/scripts/Text.ts +159 -0
- package/scripts/UI.ts +8 -0
- package/{src → scripts}/config.ts +1 -1
- package/scripts/earcut.d.ts +4 -0
- package/{src → scripts}/index.ts +3 -0
- package/{src → scripts}/main.ts +1 -1
- package/scripts/opentype.d.ts +8 -0
- package/scripts/set-exe-icon.js +36 -0
- package/src/assets/icon.png +0 -0
- package/src/games/ExampleGame.ts +2 -2
- package/tsconfig.json +1 -1
- /package/{src → scripts}/Game.ts +0 -0
- /package/{src → scripts}/Input.ts +0 -0
- /package/{src → scripts}/icon.ts +0 -0
- /package/{src → scripts}/pngjs.d.ts +0 -0
- /package/{src → scripts}/types.ts +0 -0
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
|
+
}
|
|
@@ -8,7 +8,7 @@ export interface AppConfig {
|
|
|
8
8
|
height?: number
|
|
9
9
|
icon?: string
|
|
10
10
|
}
|
|
11
|
-
/** Game name (no extension). Resolved to src/games/<game>.ts in dev, dist/games/<game>.js in prod. */
|
|
11
|
+
/** Game name (no extension). Resolved to src/games/<game>.ts in dev, dist/src/games/<game>.js in prod. */
|
|
12
12
|
game?: string
|
|
13
13
|
}
|
|
14
14
|
|
package/{src → scripts}/index.ts
RENAMED
|
@@ -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'
|
package/{src → scripts}/main.ts
RENAMED
|
@@ -14,7 +14,7 @@ async function main() {
|
|
|
14
14
|
const isPkg = typeof (process as { pkg?: unknown }).pkg !== 'undefined'
|
|
15
15
|
const gamePath =
|
|
16
16
|
isPkg || !isDev
|
|
17
|
-
? join(__dirname, 'games', `${gameName}.js`)
|
|
17
|
+
? join(__dirname, '..', 'src', 'games', `${gameName}.js`)
|
|
18
18
|
: join(process.cwd(), 'src', 'games', `${gameName}.ts`)
|
|
19
19
|
const href = pathToFileURL(gamePath).href
|
|
20
20
|
|
|
@@ -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
|
+
}
|
package/src/assets/icon.png
CHANGED
|
Binary file
|
package/src/games/ExampleGame.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createDraw2D, SCANCODE } from '
|
|
2
|
-
import type { GameContext, IGame } from '
|
|
1
|
+
import { createDraw2D, SCANCODE } from '../../scripts/index.js'
|
|
2
|
+
import type { GameContext, IGame } from '../../scripts/types.js'
|
|
3
3
|
|
|
4
4
|
interface ExampleGame extends IGame {
|
|
5
5
|
ctx: GameContext | null
|
package/tsconfig.json
CHANGED
/package/{src → scripts}/Game.ts
RENAMED
|
File without changes
|
|
File without changes
|
/package/{src → scripts}/icon.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|