nova64 0.2.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +786 -0
  3. package/index.html +651 -0
  4. package/package.json +255 -0
  5. package/public/os9-shell/assets/index-B1Uvacma.js +32825 -0
  6. package/public/os9-shell/assets/index-B1Uvacma.js.map +1 -0
  7. package/public/os9-shell/assets/index-DIHfrTaW.css +1 -0
  8. package/public/os9-shell/index.html +14 -0
  9. package/public/os9-shell/nova-icon.svg +12 -0
  10. package/runtime/api-2d.js +878 -0
  11. package/runtime/api-3d/camera.js +73 -0
  12. package/runtime/api-3d/instancing.js +180 -0
  13. package/runtime/api-3d/lights.js +51 -0
  14. package/runtime/api-3d/materials.js +47 -0
  15. package/runtime/api-3d/models.js +84 -0
  16. package/runtime/api-3d/pbr.js +69 -0
  17. package/runtime/api-3d/primitives.js +304 -0
  18. package/runtime/api-3d/scene.js +169 -0
  19. package/runtime/api-3d/transforms.js +161 -0
  20. package/runtime/api-3d.js +154 -0
  21. package/runtime/api-effects.js +753 -0
  22. package/runtime/api-presets.js +85 -0
  23. package/runtime/api-skybox.js +178 -0
  24. package/runtime/api-sprites.js +100 -0
  25. package/runtime/api-voxel.js +601 -0
  26. package/runtime/api.js +201 -0
  27. package/runtime/assets.js +27 -0
  28. package/runtime/audio.js +114 -0
  29. package/runtime/collision.js +47 -0
  30. package/runtime/console.js +101 -0
  31. package/runtime/editor.js +233 -0
  32. package/runtime/font.js +233 -0
  33. package/runtime/framebuffer.js +28 -0
  34. package/runtime/fullscreen-button.js +185 -0
  35. package/runtime/gpu-canvas2d.js +47 -0
  36. package/runtime/gpu-threejs.js +639 -0
  37. package/runtime/gpu-webgl2.js +310 -0
  38. package/runtime/index.js +22 -0
  39. package/runtime/input.js +225 -0
  40. package/runtime/logger.js +60 -0
  41. package/runtime/physics.js +101 -0
  42. package/runtime/screens.js +213 -0
  43. package/runtime/storage.js +38 -0
  44. package/runtime/store.js +151 -0
  45. package/runtime/textinput.js +68 -0
  46. package/runtime/ui/buttons.js +124 -0
  47. package/runtime/ui/panels.js +105 -0
  48. package/runtime/ui/text.js +86 -0
  49. package/runtime/ui/widgets.js +141 -0
  50. package/runtime/ui.js +111 -0
  51. package/src/main.js +474 -0
  52. package/vite.config.js +63 -0
@@ -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
+ }