nova64 0.2.6 → 0.2.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/dist/examples/strider-demo-3d/fix-game.sh +0 -0
- package/dist/runtime/api-2d.js +1158 -0
- package/dist/runtime/api-3d/camera.js +73 -0
- package/dist/runtime/api-3d/instancing.js +180 -0
- package/dist/runtime/api-3d/lights.js +51 -0
- package/dist/runtime/api-3d/materials.js +47 -0
- package/dist/runtime/api-3d/models.js +84 -0
- package/dist/runtime/api-3d/particles.js +296 -0
- package/dist/runtime/api-3d/pbr.js +113 -0
- package/dist/runtime/api-3d/primitives.js +304 -0
- package/dist/runtime/api-3d/scene.js +169 -0
- package/dist/runtime/api-3d/transforms.js +161 -0
- package/dist/runtime/api-3d.js +166 -0
- package/dist/runtime/api-effects.js +840 -0
- package/dist/runtime/api-gameutils.js +476 -0
- package/dist/runtime/api-generative.js +610 -0
- package/dist/runtime/api-presets.js +85 -0
- package/dist/runtime/api-skybox.js +232 -0
- package/dist/runtime/api-sprites.js +100 -0
- package/dist/runtime/api-voxel.js +712 -0
- package/dist/runtime/api.js +201 -0
- package/dist/runtime/assets.js +27 -0
- package/dist/runtime/audio.js +114 -0
- package/dist/runtime/collision.js +47 -0
- package/dist/runtime/console.js +101 -0
- package/dist/runtime/editor.js +233 -0
- package/dist/runtime/font.js +233 -0
- package/dist/runtime/framebuffer.js +28 -0
- package/dist/runtime/fullscreen-button.js +185 -0
- package/dist/runtime/gpu-canvas2d.js +47 -0
- package/dist/runtime/gpu-threejs.js +643 -0
- package/dist/runtime/gpu-webgl2.js +310 -0
- package/dist/runtime/index.d.ts +682 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/input.js +225 -0
- package/dist/runtime/logger.js +60 -0
- package/dist/runtime/physics.js +101 -0
- package/dist/runtime/screens.js +213 -0
- package/dist/runtime/storage.js +38 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/textinput.js +68 -0
- package/dist/runtime/ui/buttons.js +124 -0
- package/dist/runtime/ui/panels.js +105 -0
- package/dist/runtime/ui/text.js +86 -0
- package/dist/runtime/ui/widgets.js +141 -0
- package/dist/runtime/ui.js +111 -0
- package/package.json +34 -32
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// runtime/editor.js
|
|
2
|
+
// Simple in-browser sprite editor for the loaded sprite sheet.
|
|
3
|
+
|
|
4
|
+
export class SpriteEditor {
|
|
5
|
+
constructor(spriteApi) {
|
|
6
|
+
this.spriteApi = spriteApi;
|
|
7
|
+
this.opened = false;
|
|
8
|
+
this.scale = 16; // canvas zoom
|
|
9
|
+
this.brush = { r: 255, g: 255, b: 255, a: 255 };
|
|
10
|
+
this._buildUI();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_buildUI() {
|
|
14
|
+
const wrap = document.createElement('div');
|
|
15
|
+
wrap.style.position = 'fixed';
|
|
16
|
+
wrap.style.inset = '0';
|
|
17
|
+
wrap.style.display = 'none';
|
|
18
|
+
wrap.style.background = 'rgba(0,0,0,0.8)';
|
|
19
|
+
wrap.style.zIndex = '9999';
|
|
20
|
+
wrap.style.placeItems = 'center';
|
|
21
|
+
wrap.style.gridTemplateRows = 'auto auto';
|
|
22
|
+
wrap.style.padding = '16px';
|
|
23
|
+
|
|
24
|
+
const panel = document.createElement('div');
|
|
25
|
+
panel.style.background = '#151822';
|
|
26
|
+
panel.style.border = '1px solid #1f2433';
|
|
27
|
+
panel.style.borderRadius = '12px';
|
|
28
|
+
panel.style.padding = '12px';
|
|
29
|
+
panel.style.display = 'grid';
|
|
30
|
+
panel.style.gap = '8px';
|
|
31
|
+
panel.style.color = '#dcdfe4';
|
|
32
|
+
|
|
33
|
+
const row = (children = []) => {
|
|
34
|
+
const r = document.createElement('div');
|
|
35
|
+
r.style.display = 'flex';
|
|
36
|
+
r.style.gap = '8px';
|
|
37
|
+
r.style.alignItems = 'center';
|
|
38
|
+
children.forEach(c => r.appendChild(c));
|
|
39
|
+
return r;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const title = document.createElement('div');
|
|
43
|
+
title.textContent = 'Sprite Editor';
|
|
44
|
+
title.style.fontWeight = '700';
|
|
45
|
+
const info = document.createElement('div');
|
|
46
|
+
info.textContent = 'Paint directly on the sprite sheet. Save applies to the running cart.';
|
|
47
|
+
info.style.fontSize = '12px';
|
|
48
|
+
info.style.opacity = '0.7';
|
|
49
|
+
|
|
50
|
+
const canvas = document.createElement('canvas');
|
|
51
|
+
canvas.style.background = '#000';
|
|
52
|
+
canvas.style.borderRadius = '8px';
|
|
53
|
+
canvas.style.imageRendering = 'pixelated';
|
|
54
|
+
this.canvas = canvas;
|
|
55
|
+
|
|
56
|
+
const color = document.createElement('input');
|
|
57
|
+
color.type = 'color';
|
|
58
|
+
color.value = '#ffffff';
|
|
59
|
+
color.addEventListener('input', () => {
|
|
60
|
+
const hex = color.value.replace('#', '');
|
|
61
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
62
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
63
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
64
|
+
this.brush.r = r;
|
|
65
|
+
this.brush.g = g;
|
|
66
|
+
this.brush.b = b;
|
|
67
|
+
});
|
|
68
|
+
const alpha = document.createElement('input');
|
|
69
|
+
alpha.type = 'range';
|
|
70
|
+
alpha.min = '0';
|
|
71
|
+
alpha.max = '255';
|
|
72
|
+
alpha.value = '255';
|
|
73
|
+
alpha.addEventListener('input', () => {
|
|
74
|
+
this.brush.a = parseInt(alpha.value, 10);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const scaleSel = document.createElement('select');
|
|
78
|
+
[8, 12, 16, 20, 24, 32].forEach(s => {
|
|
79
|
+
const opt = document.createElement('option');
|
|
80
|
+
opt.value = String(s);
|
|
81
|
+
opt.textContent = s + 'x';
|
|
82
|
+
if (s === this.scale) opt.selected = true;
|
|
83
|
+
scaleSel.appendChild(opt);
|
|
84
|
+
});
|
|
85
|
+
scaleSel.addEventListener('change', () => {
|
|
86
|
+
this.scale = parseInt(scaleSel.value, 10);
|
|
87
|
+
this._resizeCanvas();
|
|
88
|
+
this._redraw();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const saveBtn = document.createElement('button');
|
|
92
|
+
saveBtn.textContent = 'Save to runtime';
|
|
93
|
+
saveBtn.addEventListener('click', async () => {
|
|
94
|
+
await this.applyToRuntime();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const closeBtn = document.createElement('button');
|
|
98
|
+
closeBtn.textContent = 'Close';
|
|
99
|
+
closeBtn.addEventListener('click', () => this.close());
|
|
100
|
+
|
|
101
|
+
panel.appendChild(title);
|
|
102
|
+
panel.appendChild(info);
|
|
103
|
+
panel.appendChild(
|
|
104
|
+
row([
|
|
105
|
+
document.createTextNode('Zoom'),
|
|
106
|
+
scaleSel,
|
|
107
|
+
document.createTextNode('Color'),
|
|
108
|
+
color,
|
|
109
|
+
document.createTextNode('Alpha'),
|
|
110
|
+
alpha,
|
|
111
|
+
saveBtn,
|
|
112
|
+
closeBtn,
|
|
113
|
+
])
|
|
114
|
+
);
|
|
115
|
+
panel.appendChild(canvas);
|
|
116
|
+
|
|
117
|
+
wrap.appendChild(panel);
|
|
118
|
+
document.body.appendChild(wrap);
|
|
119
|
+
|
|
120
|
+
this.wrap = wrap;
|
|
121
|
+
|
|
122
|
+
// interactions
|
|
123
|
+
let painting = false;
|
|
124
|
+
const onPaint = e => {
|
|
125
|
+
if (!painting) return;
|
|
126
|
+
const rect = canvas.getBoundingClientRect();
|
|
127
|
+
const x = Math.floor((e.clientX - rect.left) / this.scale);
|
|
128
|
+
const y = Math.floor((e.clientY - rect.top) / this.scale);
|
|
129
|
+
this._plot(x, y, this.brush);
|
|
130
|
+
};
|
|
131
|
+
canvas.addEventListener('mousedown', e => {
|
|
132
|
+
painting = true;
|
|
133
|
+
onPaint(e);
|
|
134
|
+
});
|
|
135
|
+
window.addEventListener('mousemove', onPaint);
|
|
136
|
+
window.addEventListener('mouseup', () => {
|
|
137
|
+
painting = false;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async open() {
|
|
142
|
+
const img = this.spriteApi.getSpriteSheetImage?.();
|
|
143
|
+
if (!img) {
|
|
144
|
+
alert('Sprite sheet not loaded in this cart.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await this._loadImage(img);
|
|
148
|
+
this.opened = true;
|
|
149
|
+
this.wrap.style.display = 'grid';
|
|
150
|
+
}
|
|
151
|
+
close() {
|
|
152
|
+
this.opened = false;
|
|
153
|
+
this.wrap.style.display = 'none';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async _loadImage(img) {
|
|
157
|
+
// draw image into editor canvas
|
|
158
|
+
this.img = img;
|
|
159
|
+
this.sheetW = img.naturalWidth;
|
|
160
|
+
this.sheetH = img.naturalHeight;
|
|
161
|
+
this._resizeCanvas();
|
|
162
|
+
const ctx = this.canvas.getContext('2d');
|
|
163
|
+
ctx.imageSmoothingEnabled = false;
|
|
164
|
+
ctx.drawImage(img, 0, 0);
|
|
165
|
+
this._redraw(); // draw grid
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_resizeCanvas() {
|
|
169
|
+
this.canvas.width = this.sheetW * this.scale;
|
|
170
|
+
this.canvas.height = this.sheetH * this.scale;
|
|
171
|
+
this.canvas.style.width = this.canvas.width + 'px';
|
|
172
|
+
this.canvas.style.height = this.canvas.height + 'px';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_plot(x, y, { r, g, b, a }) {
|
|
176
|
+
if (x < 0 || y < 0 || x >= this.sheetW || y >= this.sheetH) return;
|
|
177
|
+
const ctx = this.canvas.getContext('2d');
|
|
178
|
+
const imgd = ctx.getImageData(x, y, 1, 1);
|
|
179
|
+
imgd.data[0] = r;
|
|
180
|
+
imgd.data[1] = g;
|
|
181
|
+
imgd.data[2] = b;
|
|
182
|
+
imgd.data[3] = a;
|
|
183
|
+
ctx.putImageData(imgd, x, y);
|
|
184
|
+
// upscale pixel block
|
|
185
|
+
ctx.imageSmoothingEnabled = false;
|
|
186
|
+
ctx.drawImage(this.canvas, x, y, 1, 1, x * this.scale, y * this.scale, this.scale, this.scale);
|
|
187
|
+
// grid overlay redraw for that cell
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_redraw() {
|
|
191
|
+
const ctx = this.canvas.getContext('2d');
|
|
192
|
+
ctx.imageSmoothingEnabled = false;
|
|
193
|
+
ctx.drawImage(this.img, 0, 0);
|
|
194
|
+
// draw grid overlay
|
|
195
|
+
ctx.save();
|
|
196
|
+
ctx.scale(this.scale, this.scale);
|
|
197
|
+
ctx.globalAlpha = 0.2;
|
|
198
|
+
ctx.strokeStyle = '#3a3f55';
|
|
199
|
+
ctx.lineWidth = 1 / this.scale;
|
|
200
|
+
for (let x = 0; x <= this.sheetW; x += 8) {
|
|
201
|
+
ctx.beginPath();
|
|
202
|
+
ctx.moveTo(x, 0);
|
|
203
|
+
ctx.lineTo(x, this.sheetH);
|
|
204
|
+
ctx.stroke();
|
|
205
|
+
}
|
|
206
|
+
for (let y = 0; y <= this.sheetH; y += 8) {
|
|
207
|
+
ctx.beginPath();
|
|
208
|
+
ctx.moveTo(0, y);
|
|
209
|
+
ctx.lineTo(this.sheetW, y);
|
|
210
|
+
ctx.stroke();
|
|
211
|
+
}
|
|
212
|
+
ctx.restore();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async applyToRuntime() {
|
|
216
|
+
const dataURL = this.canvas.toDataURL('image/png');
|
|
217
|
+
await this.spriteApi.applySpriteSheetDataURL(dataURL);
|
|
218
|
+
alert('Sprite sheet applied to runtime.');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function editorApi(spriteApi) {
|
|
223
|
+
const editor = new SpriteEditor(spriteApi);
|
|
224
|
+
return {
|
|
225
|
+
exposeTo(target) {
|
|
226
|
+
Object.assign(target, {
|
|
227
|
+
openSpriteEditor: () => editor.open(),
|
|
228
|
+
closeSpriteEditor: () => editor.close(),
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
open: () => editor.open(),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// runtime/font.js
|
|
2
|
+
// 5x7 bitmap font packed as strings for readability. Monospace, ASCII 32..126.
|
|
3
|
+
const FONT_W = 5,
|
|
4
|
+
FONT_H = 7,
|
|
5
|
+
FONT_SPACING = 1;
|
|
6
|
+
const GLYPHS = new Map();
|
|
7
|
+
|
|
8
|
+
// Source: simple 5x7 font, custom packed
|
|
9
|
+
const glyphRows = {
|
|
10
|
+
A: [' # ', ' # # ', '# #', '#####', '# #', '# #', '# #'],
|
|
11
|
+
B: ['#### ', '# #', '#### ', '# #', '# #', '# #', '#### '],
|
|
12
|
+
C: [' ### ', '# #', '# ', '# ', '# ', '# #', ' ### '],
|
|
13
|
+
D: ['#### ', '# #', '# #', '# #', '# #', '# #', '#### '],
|
|
14
|
+
E: ['#####', '# ', '### ', '# ', '# ', '# ', '#####'],
|
|
15
|
+
F: ['#####', '# ', '### ', '# ', '# ', '# ', '# '],
|
|
16
|
+
G: [' ### ', '# #', '# ', '# ###', '# #', '# #', ' ### '],
|
|
17
|
+
H: ['# #', '# #', '# #', '#####', '# #', '# #', '# #'],
|
|
18
|
+
I: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', '#####'],
|
|
19
|
+
J: ['#####', ' #', ' #', ' #', '# #', '# #', ' ### '],
|
|
20
|
+
K: ['# #', '# # ', '### ', '# # ', '# # ', '# #', '# #'],
|
|
21
|
+
L: ['# ', '# ', '# ', '# ', '# ', '# ', '#####'],
|
|
22
|
+
M: ['# #', '## ##', '# # #', '# #', '# #', '# #', '# #'],
|
|
23
|
+
N: ['# #', '## #', '# # #', '# ##', '# #', '# #', '# #'],
|
|
24
|
+
O: [' ### ', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
|
25
|
+
P: ['#### ', '# #', '# #', '#### ', '# ', '# ', '# '],
|
|
26
|
+
Q: [' ### ', '# #', '# #', '# #', '# # #', '# # ', ' ## #'],
|
|
27
|
+
R: ['#### ', '# #', '# #', '#### ', '# # ', '# # ', '# #'],
|
|
28
|
+
S: [' ### ', '# #', '# ', ' ### ', ' #', '# #', ' ### '],
|
|
29
|
+
T: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
|
30
|
+
U: ['# #', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
|
31
|
+
V: ['# #', '# #', '# #', '# #', '# #', ' # # ', ' # '],
|
|
32
|
+
W: ['# #', '# #', '# #', '# # #', '# # #', '## ##', '# #'],
|
|
33
|
+
X: ['# #', ' # # ', ' # ', ' # ', ' # ', ' # # ', '# #'],
|
|
34
|
+
Y: ['# #', ' # # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
|
35
|
+
Z: ['#####', ' #', ' # ', ' # ', ' # ', '# ', '#####'],
|
|
36
|
+
0: [' ### ', '# #', '# ##', '# # #', '## #', '# #', ' ### '],
|
|
37
|
+
1: [' # ', ' ## ', ' # ', ' # ', ' # ', ' # ', '#####'],
|
|
38
|
+
2: [' ### ', '# #', ' #', ' # ', ' # ', ' # ', '#####'],
|
|
39
|
+
3: [' ### ', '# #', ' #', ' ### ', ' #', '# #', ' ### '],
|
|
40
|
+
4: [' # ', ' ## ', ' # # ', '# # ', '#####', ' # ', ' # '],
|
|
41
|
+
5: ['#####', '# ', '#### ', ' #', ' #', '# #', ' ### '],
|
|
42
|
+
6: [' ### ', '# ', '# ', '#### ', '# #', '# #', ' ### '],
|
|
43
|
+
7: ['#####', ' #', ' # ', ' # ', ' # ', ' # ', ' # '],
|
|
44
|
+
8: [' ### ', '# #', '# #', ' ### ', '# #', '# #', ' ### '],
|
|
45
|
+
9: [' ### ', '# #', '# #', ' ####', ' #', ' #', ' ### '],
|
|
46
|
+
' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
47
|
+
'!': [' # ', ' # ', ' # ', ' # ', ' # ', ' ', ' # '],
|
|
48
|
+
'?': [' ### ', '# #', ' #', ' # ', ' # ', ' ', ' # '],
|
|
49
|
+
'.': [' ', ' ', ' ', ' ', ' ', ' ## ', ' ## '],
|
|
50
|
+
',': [' ', ' ', ' ', ' ', ' ', ' ## ', ' # '],
|
|
51
|
+
':': [' ', ' ## ', ' ## ', ' ', ' ## ', ' ## ', ' '],
|
|
52
|
+
'-': [' ', ' ', ' ', ' ### ', ' ', ' ', ' '],
|
|
53
|
+
_: [' ', ' ', ' ', ' ', ' ', ' ', '#####'],
|
|
54
|
+
'/': [' #', ' # ', ' # ', ' # ', ' # ', '# ', ' '],
|
|
55
|
+
// Lowercase letters
|
|
56
|
+
a: [' ', ' ', ' ### ', ' #', ' ####', '# #', ' ####'],
|
|
57
|
+
b: ['# ', '# ', '#### ', '# #', '# #', '# #', '#### '],
|
|
58
|
+
c: [' ', ' ', ' ### ', '# ', '# ', '# #', ' ### '],
|
|
59
|
+
d: [' #', ' #', ' ####', '# #', '# #', '# #', ' ####'],
|
|
60
|
+
e: [' ', ' ', ' ### ', '# #', '#####', '# ', ' ### '],
|
|
61
|
+
f: [' ## ', ' # #', ' # ', '#### ', ' # ', ' # ', ' # '],
|
|
62
|
+
g: [' ', ' ', ' ####', '# #', '# #', ' ####', ' #', ' ### '],
|
|
63
|
+
h: ['# ', '# ', '#### ', '# #', '# #', '# #', '# #'],
|
|
64
|
+
i: [' # ', ' ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
|
65
|
+
j: [' # ', ' ', ' # ', ' # ', ' # ', ' # ', '# # ', ' ## '],
|
|
66
|
+
k: ['# ', '# ', '# # ', '# # ', '### ', '# # ', '# #'],
|
|
67
|
+
l: [' # ', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
|
68
|
+
m: [' ', ' ', '## # ', '# # #', '# # #', '# # #', '# # #'],
|
|
69
|
+
n: [' ', ' ', '#### ', '# #', '# #', '# #', '# #'],
|
|
70
|
+
o: [' ', ' ', ' ### ', '# #', '# #', '# #', ' ### '],
|
|
71
|
+
p: [' ', ' ', '#### ', '# #', '# #', '#### ', '# ', '# '],
|
|
72
|
+
q: [' ', ' ', ' ####', '# #', '# #', ' ####', ' #', ' #'],
|
|
73
|
+
r: [' ', ' ', '# ## ', '## ', '# ', '# ', '# '],
|
|
74
|
+
s: [' ', ' ', ' ### ', '# ', ' ### ', ' #', '#### '],
|
|
75
|
+
t: [' # ', ' # ', '#### ', ' # ', ' # ', ' # #', ' ## '],
|
|
76
|
+
u: [' ', ' ', '# #', '# #', '# #', '# #', ' ####'],
|
|
77
|
+
v: [' ', ' ', '# #', '# #', '# #', ' # # ', ' # '],
|
|
78
|
+
w: [' ', ' ', '# #', '# # #', '# # #', '# # #', ' # # '],
|
|
79
|
+
x: [' ', ' ', '# #', ' # # ', ' # ', ' # # ', '# #'],
|
|
80
|
+
y: [' ', ' ', '# #', '# #', '# #', ' ####', ' #', ' ### '],
|
|
81
|
+
z: [' ', ' ', '#####', ' # ', ' # ', ' # ', '#####'],
|
|
82
|
+
// Additional punctuation and symbols
|
|
83
|
+
'(': [' # ', ' # ', '# ', '# ', '# ', ' # ', ' # '],
|
|
84
|
+
')': [' # ', ' # ', ' #', ' #', ' #', ' # ', ' # '],
|
|
85
|
+
'[': ['### ', '# ', '# ', '# ', '# ', '# ', '### '],
|
|
86
|
+
']': [' ###', ' #', ' #', ' #', ' #', ' #', ' ###'],
|
|
87
|
+
'{': [' ## ', ' # ', ' # ', '## ', ' # ', ' # ', ' ## '],
|
|
88
|
+
'}': ['## ', ' # ', ' # ', ' ##', ' # ', ' # ', '## '],
|
|
89
|
+
'<': [' # ', ' # ', ' # ', '# ', ' # ', ' # ', ' # '],
|
|
90
|
+
'>': ['# ', ' # ', ' # ', ' # ', ' # ', ' # ', '# '],
|
|
91
|
+
'=': [' ', ' ', '#####', ' ', '#####', ' ', ' '],
|
|
92
|
+
'+': [' ', ' # ', ' # ', '#####', ' # ', ' # ', ' '],
|
|
93
|
+
'*': [' ', '# # #', ' ### ', ' # ', ' ### ', '# # #', ' '],
|
|
94
|
+
'&': [' ## ', '# # ', ' ## ', ' ### ', '# # ', '# # ', ' ## #'],
|
|
95
|
+
'%': ['# #', ' # ', ' # ', ' # ', '# ', '# #', ' '],
|
|
96
|
+
$: [' # ', ' ####', '# # ', ' ### ', ' # #', '#### ', ' # '],
|
|
97
|
+
'#': [' # # ', ' # # ', '#####', ' # # ', '#####', ' ## #', ' # # '],
|
|
98
|
+
'@': [' ### ', '# #', '# # #', '# ###', '# ', '# #', ' ### '],
|
|
99
|
+
'^': [' # ', ' # # ', '# #', ' ', ' ', ' ', ' '],
|
|
100
|
+
'~': [' ', ' ## ', '# # ', ' ## ', ' ', ' ', ' '],
|
|
101
|
+
'`': [' # ', ' # ', ' ', ' ', ' ', ' ', ' '],
|
|
102
|
+
"'": [' # ', ' # ', ' ', ' ', ' ', ' ', ' '],
|
|
103
|
+
'"': [' # # ', ' # # ', ' ', ' ', ' ', ' ', ' '],
|
|
104
|
+
'|': [' # ', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
|
105
|
+
'\\': ['# ', ' # ', ' # ', ' # ', ' # ', ' #', ' '],
|
|
106
|
+
';': [' ', ' ## ', ' ## ', ' ', ' ## ', ' ## ', ' # '],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Add arrow characters (using Unicode arrow code points mapped to ASCII art)
|
|
110
|
+
// These handle both Unicode arrows and ASCII alternatives
|
|
111
|
+
const arrowMappings = [
|
|
112
|
+
['←', [' # ', ' ## ', ' ### ', '#### ', ' ### ', ' ## ', ' # ']], // Left arrow
|
|
113
|
+
['→', ['# ', '## ', '### ', '#### ', '### ', '## ', '# ']], // Right arrow
|
|
114
|
+
['↑', [' # ', ' ### ', '# # #', ' # ', ' # ', ' # ', ' # ']], // Up arrow
|
|
115
|
+
['↓', [' # ', ' # ', ' # ', ' # ', '# # #', ' ### ', ' # ']], // Down arrow
|
|
116
|
+
['↔', ['# #', '## #', ' ####', '#### ', ' ####', '## ##', '# #']], // Left-right arrow
|
|
117
|
+
['↕', [' # ', ' ### ', '# # #', ' # ', '# # #', ' ### ', ' # ']], // Up-down arrow
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const [ch, rows] of arrowMappings) {
|
|
121
|
+
GLYPHS.set(ch, rows);
|
|
122
|
+
glyphRows[ch] = rows;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const [ch, rows] of Object.entries(glyphRows)) GLYPHS.set(ch, rows);
|
|
126
|
+
|
|
127
|
+
// Emoji replacement map - replaces common emojis with ASCII equivalents or removes them
|
|
128
|
+
const EMOJI_REPLACEMENTS = {
|
|
129
|
+
'🎮': '', // game controller
|
|
130
|
+
'🚀': '', // rocket
|
|
131
|
+
'🏁': '', // checkered flag
|
|
132
|
+
'🏛️': '', // classical building
|
|
133
|
+
'🏰': '', // castle
|
|
134
|
+
'🔮': '*', // crystal ball
|
|
135
|
+
'🌃': '', // night cityscape
|
|
136
|
+
'⚡': '*', // lightning bolt (also used as special char)
|
|
137
|
+
'✨': '*', // sparkles
|
|
138
|
+
'✅': '+', // check mark
|
|
139
|
+
'🔘': 'o', // radio button
|
|
140
|
+
'🎯': 'o', // target
|
|
141
|
+
'🛡️': '', // shield
|
|
142
|
+
'🖱️': '', // computer mouse
|
|
143
|
+
'🖥️': '', // desktop computer
|
|
144
|
+
'⚙️': '*', // gear
|
|
145
|
+
'🔤': '', // ABC input symbols
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Helper function to clean text of unsupported characters
|
|
149
|
+
function cleanText(text) {
|
|
150
|
+
let result = '';
|
|
151
|
+
for (let i = 0; i < text.length; i++) {
|
|
152
|
+
const ch = text[i];
|
|
153
|
+
const code = ch.charCodeAt(0);
|
|
154
|
+
|
|
155
|
+
// Check for emoji replacement
|
|
156
|
+
if (Object.prototype.hasOwnProperty.call(EMOJI_REPLACEMENTS, ch)) {
|
|
157
|
+
result += EMOJI_REPLACEMENTS[ch];
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Skip multi-byte characters (emojis) that we don't have in our font
|
|
162
|
+
// Most emojis are in the range 0x1F000 and above
|
|
163
|
+
if (code > 0x7f && !GLYPHS.has(ch)) {
|
|
164
|
+
// Skip this character (or could add a space)
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
result += ch;
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const BitmapFont = {
|
|
174
|
+
w: FONT_W,
|
|
175
|
+
h: FONT_H,
|
|
176
|
+
spacing: FONT_SPACING,
|
|
177
|
+
draw(fb, text, x, y, colorBigInt, scale = 1) {
|
|
178
|
+
// Clean the text first to remove unsupported characters
|
|
179
|
+
text = cleanText(text);
|
|
180
|
+
|
|
181
|
+
const { r, g, b, a } = unpackRGBA64(colorBigInt);
|
|
182
|
+
const s = Math.max(1, Math.round(scale));
|
|
183
|
+
let cx = x | 0,
|
|
184
|
+
cy = y | 0;
|
|
185
|
+
for (let i = 0; i < text.length; i++) {
|
|
186
|
+
const ch = text[i];
|
|
187
|
+
if (ch === '\n') {
|
|
188
|
+
cy += (FONT_H + FONT_SPACING) * s;
|
|
189
|
+
cx = x | 0;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const rows = GLYPHS.get(ch) || GLYPHS.get('?');
|
|
193
|
+
for (let yy = 0; yy < FONT_H; yy++) {
|
|
194
|
+
const row = rows[yy]; // top-down: row 0 = top of glyph = smallest y on screen
|
|
195
|
+
for (let xx = 0; xx < FONT_W; xx++) {
|
|
196
|
+
if (row[xx] !== ' ') {
|
|
197
|
+
if (s === 1) {
|
|
198
|
+
fb.pset(cx + xx, cy + yy, r, g, b, a);
|
|
199
|
+
} else {
|
|
200
|
+
// Each bitmap pixel becomes an s×s block
|
|
201
|
+
for (let sy = 0; sy < s; sy++)
|
|
202
|
+
for (let sx = 0; sx < s; sx++)
|
|
203
|
+
fb.pset(cx + xx * s + sx, cy + yy * s + sy, r, g, b, a);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
cx += (FONT_W + FONT_SPACING) * s;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Local copy of unpack to avoid circular dep; duplicated from api.js
|
|
214
|
+
function unpackRGBA64(c) {
|
|
215
|
+
// Handle both BigInt and regular number inputs
|
|
216
|
+
if (typeof c === 'bigint') {
|
|
217
|
+
return {
|
|
218
|
+
r: Number((c >> 48n) & 0xffffn),
|
|
219
|
+
g: Number((c >> 32n) & 0xffffn),
|
|
220
|
+
b: Number((c >> 16n) & 0xffffn),
|
|
221
|
+
a: Number(c & 0xffffn),
|
|
222
|
+
};
|
|
223
|
+
} else {
|
|
224
|
+
// Handle regular number input - convert to BigInt first
|
|
225
|
+
const bigC = BigInt(c);
|
|
226
|
+
return {
|
|
227
|
+
r: Number((bigC >> 48n) & 0xffffn),
|
|
228
|
+
g: Number((bigC >> 32n) & 0xffffn),
|
|
229
|
+
b: Number((bigC >> 16n) & 0xffffn),
|
|
230
|
+
a: Number(bigC & 0xffffn),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// runtime/framebuffer.js
|
|
2
|
+
export class Framebuffer64 {
|
|
3
|
+
constructor(w, h) {
|
|
4
|
+
this.width = w;
|
|
5
|
+
this.height = h;
|
|
6
|
+
this.pixels = new Uint16Array(w * h * 4); // RGBA16 per pixel
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
fill(r = 0, g = 0, b = 0, a = 65535) {
|
|
10
|
+
const p = this.pixels;
|
|
11
|
+
for (let i = 0; i < p.length; i += 4) {
|
|
12
|
+
p[i] = r;
|
|
13
|
+
p[i + 1] = g;
|
|
14
|
+
p[i + 2] = b;
|
|
15
|
+
p[i + 3] = a;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pset(x, y, r, g, b, a = 65535) {
|
|
20
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
|
|
21
|
+
const i = (y * this.width + x) * 4;
|
|
22
|
+
const p = this.pixels;
|
|
23
|
+
p[i] = r;
|
|
24
|
+
p[i + 1] = g;
|
|
25
|
+
p[i + 2] = b;
|
|
26
|
+
p[i + 3] = a;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Fullscreen button for Nova64
|
|
2
|
+
// Creates a UI button in the lower-right corner to toggle fullscreen mode
|
|
3
|
+
|
|
4
|
+
export class FullscreenButton {
|
|
5
|
+
constructor(canvas) {
|
|
6
|
+
this.canvas = canvas;
|
|
7
|
+
this.button = null;
|
|
8
|
+
this.isFullscreen = false;
|
|
9
|
+
this.createButton();
|
|
10
|
+
this.attachListeners();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
createButton() {
|
|
14
|
+
// Create button element
|
|
15
|
+
this.button = document.createElement('button');
|
|
16
|
+
this.button.id = 'nova64-fullscreen-btn';
|
|
17
|
+
this.button.innerHTML = this.getExpandIcon();
|
|
18
|
+
this.button.title = 'Toggle Fullscreen (ESC to exit)';
|
|
19
|
+
|
|
20
|
+
// Style the button
|
|
21
|
+
Object.assign(this.button.style, {
|
|
22
|
+
position: 'fixed',
|
|
23
|
+
bottom: '20px',
|
|
24
|
+
right: '20px',
|
|
25
|
+
width: '48px',
|
|
26
|
+
height: '48px',
|
|
27
|
+
borderRadius: '8px',
|
|
28
|
+
border: '2px solid #00ffff',
|
|
29
|
+
background: 'rgba(21, 24, 34, 0.9)',
|
|
30
|
+
color: '#00ffff',
|
|
31
|
+
cursor: 'pointer',
|
|
32
|
+
display: 'flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
fontSize: '24px',
|
|
36
|
+
fontWeight: 'bold',
|
|
37
|
+
zIndex: '9999',
|
|
38
|
+
boxShadow: '0 0 20px rgba(0, 255, 255, 0.3), 0 4px 12px rgba(0, 0, 0, 0.5)',
|
|
39
|
+
transition: 'all 0.3s ease',
|
|
40
|
+
backdropFilter: 'blur(10px)',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Hover effect
|
|
44
|
+
this.button.addEventListener('mouseenter', () => {
|
|
45
|
+
this.button.style.background = 'rgba(0, 255, 255, 0.2)';
|
|
46
|
+
this.button.style.boxShadow =
|
|
47
|
+
'0 0 30px rgba(0, 255, 255, 0.6), 0 4px 16px rgba(0, 0, 0, 0.6)';
|
|
48
|
+
this.button.style.transform = 'scale(1.1)';
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.button.addEventListener('mouseleave', () => {
|
|
52
|
+
this.button.style.background = 'rgba(21, 24, 34, 0.9)';
|
|
53
|
+
this.button.style.boxShadow =
|
|
54
|
+
'0 0 20px rgba(0, 255, 255, 0.3), 0 4px 12px rgba(0, 0, 0, 0.5)';
|
|
55
|
+
this.button.style.transform = 'scale(1)';
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Add to document
|
|
59
|
+
document.body.appendChild(this.button);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getExpandIcon() {
|
|
63
|
+
return `
|
|
64
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
65
|
+
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
|
66
|
+
</svg>
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getCompressIcon() {
|
|
71
|
+
return `
|
|
72
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
73
|
+
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
|
|
74
|
+
</svg>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
attachListeners() {
|
|
79
|
+
// Click to toggle fullscreen
|
|
80
|
+
this.button.addEventListener('click', () => {
|
|
81
|
+
this.toggleFullscreen();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ESC key to exit fullscreen
|
|
85
|
+
document.addEventListener('keydown', e => {
|
|
86
|
+
if (e.key === 'Escape' && this.isFullscreen) {
|
|
87
|
+
this.exitFullscreen();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Listen for fullscreen changes (handles F11, ESC, etc.)
|
|
92
|
+
document.addEventListener('fullscreenchange', () => {
|
|
93
|
+
this.handleFullscreenChange();
|
|
94
|
+
});
|
|
95
|
+
document.addEventListener('webkitfullscreenchange', () => {
|
|
96
|
+
this.handleFullscreenChange();
|
|
97
|
+
});
|
|
98
|
+
document.addEventListener('mozfullscreenchange', () => {
|
|
99
|
+
this.handleFullscreenChange();
|
|
100
|
+
});
|
|
101
|
+
document.addEventListener('MSFullscreenChange', () => {
|
|
102
|
+
this.handleFullscreenChange();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
toggleFullscreen() {
|
|
107
|
+
if (this.isFullscreen) {
|
|
108
|
+
this.exitFullscreen();
|
|
109
|
+
} else {
|
|
110
|
+
this.enterFullscreen();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
enterFullscreen() {
|
|
115
|
+
const elem = this.canvas;
|
|
116
|
+
|
|
117
|
+
if (elem.requestFullscreen) {
|
|
118
|
+
elem.requestFullscreen();
|
|
119
|
+
} else if (elem.webkitRequestFullscreen) {
|
|
120
|
+
// Safari
|
|
121
|
+
elem.webkitRequestFullscreen();
|
|
122
|
+
} else if (elem.mozRequestFullScreen) {
|
|
123
|
+
// Firefox
|
|
124
|
+
elem.mozRequestFullScreen();
|
|
125
|
+
} else if (elem.msRequestFullscreen) {
|
|
126
|
+
// IE11
|
|
127
|
+
elem.msRequestFullscreen();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.isFullscreen = true;
|
|
131
|
+
this.updateButton();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
exitFullscreen() {
|
|
135
|
+
if (document.exitFullscreen) {
|
|
136
|
+
document.exitFullscreen();
|
|
137
|
+
} else if (document.webkitExitFullscreen) {
|
|
138
|
+
// Safari
|
|
139
|
+
document.webkitExitFullscreen();
|
|
140
|
+
} else if (document.mozCancelFullScreen) {
|
|
141
|
+
// Firefox
|
|
142
|
+
document.mozCancelFullScreen();
|
|
143
|
+
} else if (document.msExitFullscreen) {
|
|
144
|
+
// IE11
|
|
145
|
+
document.msExitFullscreen();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.isFullscreen = false;
|
|
149
|
+
this.updateButton();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
handleFullscreenChange() {
|
|
153
|
+
// Check if we're actually in fullscreen
|
|
154
|
+
const isInFullscreen = !!(
|
|
155
|
+
document.fullscreenElement ||
|
|
156
|
+
document.webkitFullscreenElement ||
|
|
157
|
+
document.mozFullScreenElement ||
|
|
158
|
+
document.msFullscreenElement
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
this.isFullscreen = isInFullscreen;
|
|
162
|
+
this.updateButton();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
updateButton() {
|
|
166
|
+
if (this.isFullscreen) {
|
|
167
|
+
this.button.innerHTML = this.getCompressIcon();
|
|
168
|
+
this.button.title = 'Exit Fullscreen (ESC)';
|
|
169
|
+
} else {
|
|
170
|
+
this.button.innerHTML = this.getExpandIcon();
|
|
171
|
+
this.button.title = 'Toggle Fullscreen (ESC to exit)';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
destroy() {
|
|
176
|
+
if (this.button && this.button.parentNode) {
|
|
177
|
+
this.button.parentNode.removeChild(this.button);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Export factory function
|
|
183
|
+
export function createFullscreenButton(canvas) {
|
|
184
|
+
return new FullscreenButton(canvas);
|
|
185
|
+
}
|