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.
- package/LICENSE +21 -0
- package/README.md +786 -0
- package/index.html +651 -0
- package/package.json +255 -0
- package/public/os9-shell/assets/index-B1Uvacma.js +32825 -0
- package/public/os9-shell/assets/index-B1Uvacma.js.map +1 -0
- package/public/os9-shell/assets/index-DIHfrTaW.css +1 -0
- package/public/os9-shell/index.html +14 -0
- package/public/os9-shell/nova-icon.svg +12 -0
- package/runtime/api-2d.js +878 -0
- package/runtime/api-3d/camera.js +73 -0
- package/runtime/api-3d/instancing.js +180 -0
- package/runtime/api-3d/lights.js +51 -0
- package/runtime/api-3d/materials.js +47 -0
- package/runtime/api-3d/models.js +84 -0
- package/runtime/api-3d/pbr.js +69 -0
- package/runtime/api-3d/primitives.js +304 -0
- package/runtime/api-3d/scene.js +169 -0
- package/runtime/api-3d/transforms.js +161 -0
- package/runtime/api-3d.js +154 -0
- package/runtime/api-effects.js +753 -0
- package/runtime/api-presets.js +85 -0
- package/runtime/api-skybox.js +178 -0
- package/runtime/api-sprites.js +100 -0
- package/runtime/api-voxel.js +601 -0
- package/runtime/api.js +201 -0
- package/runtime/assets.js +27 -0
- package/runtime/audio.js +114 -0
- package/runtime/collision.js +47 -0
- package/runtime/console.js +101 -0
- package/runtime/editor.js +233 -0
- package/runtime/font.js +233 -0
- package/runtime/framebuffer.js +28 -0
- package/runtime/fullscreen-button.js +185 -0
- package/runtime/gpu-canvas2d.js +47 -0
- package/runtime/gpu-threejs.js +639 -0
- package/runtime/gpu-webgl2.js +310 -0
- package/runtime/index.js +22 -0
- package/runtime/input.js +225 -0
- package/runtime/logger.js +60 -0
- package/runtime/physics.js +101 -0
- package/runtime/screens.js +213 -0
- package/runtime/storage.js +38 -0
- package/runtime/store.js +151 -0
- package/runtime/textinput.js +68 -0
- package/runtime/ui/buttons.js +124 -0
- package/runtime/ui/panels.js +105 -0
- package/runtime/ui/text.js +86 -0
- package/runtime/ui/widgets.js +141 -0
- package/runtime/ui.js +111 -0
- package/src/main.js +474 -0
- package/vite.config.js +63 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
// runtime/api-2d.js
|
|
2
|
+
// Nova64 Enhanced 2D Drawing API
|
|
3
|
+
// All primitives operate on the framebuffer (the 2D overlay that composites over the 3D scene).
|
|
4
|
+
//
|
|
5
|
+
// Exposed globals: drawGradient, drawRadialGradient, drawRoundedRect, poly, tristrip,
|
|
6
|
+
// printCentered, printRight, drawScanlines, drawNoise, drawProgressBar,
|
|
7
|
+
// drawGlowText, drawPixelBorder, drawCheckerboard, colorLerp, colorMix, hexColor,
|
|
8
|
+
// drawStarburst, drawDiamond, drawTriangle, drawWave, drawSpiral, scrollingText,
|
|
9
|
+
// drawPanel, drawHealthBar, drawMinimap, n64Palette
|
|
10
|
+
|
|
11
|
+
import { rgba8 } from './api.js';
|
|
12
|
+
|
|
13
|
+
// ─── colour helpers ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Unpack an rgba8() BigInt into {r,g,b,a} 0-255 floats */
|
|
16
|
+
function _unpack(c) {
|
|
17
|
+
if (typeof c === 'bigint') {
|
|
18
|
+
return {
|
|
19
|
+
r: Number((c >> 48n) & 0xffffn) / 257,
|
|
20
|
+
g: Number((c >> 32n) & 0xffffn) / 257,
|
|
21
|
+
b: Number((c >> 16n) & 0xffffn) / 257,
|
|
22
|
+
a: Number(c & 0xffffn) / 257,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const bc = BigInt(Math.floor(c));
|
|
26
|
+
return {
|
|
27
|
+
r: Number((bc >> 48n) & 0xffffn) / 257,
|
|
28
|
+
g: Number((bc >> 32n) & 0xffffn) / 257,
|
|
29
|
+
b: Number((bc >> 16n) & 0xffffn) / 257,
|
|
30
|
+
a: Number(bc & 0xffffn) / 257,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Linearly interpolate two rgba8 colours. t = 0..1 */
|
|
35
|
+
function colorLerp(c1, c2, t) {
|
|
36
|
+
const a = _unpack(c1),
|
|
37
|
+
b = _unpack(c2);
|
|
38
|
+
return rgba8(
|
|
39
|
+
a.r + (b.r - a.r) * t,
|
|
40
|
+
a.g + (b.g - a.g) * t,
|
|
41
|
+
a.b + (b.b - a.b) * t,
|
|
42
|
+
a.a + (b.a - a.a) * t
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Multiply/tint a colour by a brightness factor 0..2 */
|
|
47
|
+
function colorMix(c, factor) {
|
|
48
|
+
const { r, g, b, a } = _unpack(c);
|
|
49
|
+
return rgba8(Math.min(255, r * factor), Math.min(255, g * factor), Math.min(255, b * factor), a);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Convert 0xRRGGBB hex number to rgba8 */
|
|
53
|
+
function hexColor(hex, alpha = 255) {
|
|
54
|
+
return rgba8((hex >> 16) & 0xff, (hex >> 8) & 0xff, hex & 0xff, alpha);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Convert HSL(0-360, 0-1, 0-1) to rgba8 */
|
|
58
|
+
function hslColor(h, s = 1, l = 0.5, alpha = 255) {
|
|
59
|
+
h = ((h % 360) + 360) % 360;
|
|
60
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
61
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
62
|
+
const m = l - c / 2;
|
|
63
|
+
let r, g, b;
|
|
64
|
+
if (h < 60) {
|
|
65
|
+
r = c;
|
|
66
|
+
g = x;
|
|
67
|
+
b = 0;
|
|
68
|
+
} else if (h < 120) {
|
|
69
|
+
r = x;
|
|
70
|
+
g = c;
|
|
71
|
+
b = 0;
|
|
72
|
+
} else if (h < 180) {
|
|
73
|
+
r = 0;
|
|
74
|
+
g = c;
|
|
75
|
+
b = x;
|
|
76
|
+
} else if (h < 240) {
|
|
77
|
+
r = 0;
|
|
78
|
+
g = x;
|
|
79
|
+
b = c;
|
|
80
|
+
} else if (h < 300) {
|
|
81
|
+
r = x;
|
|
82
|
+
g = 0;
|
|
83
|
+
b = c;
|
|
84
|
+
} else {
|
|
85
|
+
r = c;
|
|
86
|
+
g = 0;
|
|
87
|
+
b = x;
|
|
88
|
+
}
|
|
89
|
+
return rgba8(
|
|
90
|
+
Math.round((r + m) * 255),
|
|
91
|
+
Math.round((g + m) * 255),
|
|
92
|
+
Math.round((b + m) * 255),
|
|
93
|
+
alpha
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Math utilities ──────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** Linear interpolation: lerp(a, b, t) → a + (b - a) * t */
|
|
100
|
+
function lerp(a, b, t) {
|
|
101
|
+
return a + (b - a) * t;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Clamp value between min and max */
|
|
105
|
+
function clamp(v, min = 0, max = 1) {
|
|
106
|
+
return v < min ? min : v > max ? max : v;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Random float in [min, max) */
|
|
110
|
+
function randRange(min, max) {
|
|
111
|
+
return min + Math.random() * (max - min);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Random integer in [min, max] inclusive */
|
|
115
|
+
function randInt(min, max) {
|
|
116
|
+
return (min + Math.random() * (max - min + 1)) | 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Distance between two 2D points */
|
|
120
|
+
function dist(x1, y1, x2, y2) {
|
|
121
|
+
const dx = x2 - x1,
|
|
122
|
+
dy = y2 - y1;
|
|
123
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Distance between two 3D points */
|
|
127
|
+
function dist3d(x1, y1, z1, x2, y2, z2) {
|
|
128
|
+
const dx = x2 - x1,
|
|
129
|
+
dy = y2 - y1,
|
|
130
|
+
dz = z2 - z1;
|
|
131
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Map a value from one range to another */
|
|
135
|
+
function remap(value, inMin, inMax, outMin, outMax) {
|
|
136
|
+
return outMin + ((value - inMin) * (outMax - outMin)) / (inMax - inMin);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Smooth pulse: returns 0-1-0 based on time with given frequency */
|
|
140
|
+
function pulse(time, frequency = 1) {
|
|
141
|
+
return Math.sin(time * frequency * Math.PI * 2) * 0.5 + 0.5;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Convert degrees to radians */
|
|
145
|
+
function deg2rad(d) {
|
|
146
|
+
return (d * Math.PI) / 180;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Convert radians to degrees */
|
|
150
|
+
function rad2deg(r) {
|
|
151
|
+
return (r * 180) / Math.PI;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Classic N64 / PS1 limited palette ───────────────────────────────────────
|
|
155
|
+
const n64Palette = {
|
|
156
|
+
black: rgba8(0, 0, 0),
|
|
157
|
+
white: rgba8(255, 255, 255),
|
|
158
|
+
red: rgba8(220, 30, 30),
|
|
159
|
+
green: rgba8(30, 200, 60),
|
|
160
|
+
blue: rgba8(30, 80, 220),
|
|
161
|
+
yellow: rgba8(255, 220, 0),
|
|
162
|
+
cyan: rgba8(0, 220, 220),
|
|
163
|
+
magenta: rgba8(200, 0, 200),
|
|
164
|
+
orange: rgba8(255, 140, 0),
|
|
165
|
+
purple: rgba8(120, 0, 200),
|
|
166
|
+
teal: rgba8(0, 160, 160),
|
|
167
|
+
brown: rgba8(140, 80, 30),
|
|
168
|
+
grey: rgba8(128, 128, 128),
|
|
169
|
+
darkGrey: rgba8(60, 60, 60),
|
|
170
|
+
lightGrey: rgba8(200, 200, 200),
|
|
171
|
+
sky: rgba8(70, 130, 200),
|
|
172
|
+
gold: rgba8(255, 210, 50),
|
|
173
|
+
silver: rgba8(192, 192, 210),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// ─── actual drawing functions ─────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export function api2d(gpu) {
|
|
179
|
+
const fb = gpu.getFramebuffer();
|
|
180
|
+
const W = fb.width; // 640
|
|
181
|
+
const H = fb.height; // 360
|
|
182
|
+
|
|
183
|
+
// cached camRef from std api - unused for 2D (no camera offset)
|
|
184
|
+
// const cam = { x: 0, y: 0 };
|
|
185
|
+
|
|
186
|
+
// helper: write a pixel with alpha blending
|
|
187
|
+
function _blend(x, y, r, g, b, a) {
|
|
188
|
+
if (x < 0 || y < 0 || x >= W || y >= H) return;
|
|
189
|
+
if (a >= 255) {
|
|
190
|
+
fb.pset(x, y, r * 257, g * 257, b * 257, 65535);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const i = (y * W + x) * 4;
|
|
194
|
+
const p = fb.pixels;
|
|
195
|
+
const af = a / 255;
|
|
196
|
+
const bf = 1 - af;
|
|
197
|
+
p[i] = Math.round(r * af + (p[i] / 257) * bf) * 257;
|
|
198
|
+
p[i + 1] = Math.round(g * af + (p[i + 1] / 257) * bf) * 257;
|
|
199
|
+
p[i + 2] = Math.round(b * af + (p[i + 2] / 257) * bf) * 257;
|
|
200
|
+
p[i + 3] = 65535;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** pset with color as rgba8 bigint */
|
|
204
|
+
function _pset(x, y, color) {
|
|
205
|
+
const c = _unpack(color);
|
|
206
|
+
_blend(x | 0, y | 0, c.r, c.g, c.b, c.a);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Gradient fills ──────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* drawGradient(x, y, w, h, c1, c2, dir)
|
|
213
|
+
* dir: 'v' vertical (top→bottom), 'h' horizontal (left→right), 'd' diagonal
|
|
214
|
+
*/
|
|
215
|
+
function drawGradient(x, y, w, h, c1, c2, dir = 'v') {
|
|
216
|
+
x |= 0;
|
|
217
|
+
y |= 0;
|
|
218
|
+
w |= 0;
|
|
219
|
+
h |= 0;
|
|
220
|
+
const a1 = _unpack(c1),
|
|
221
|
+
a2 = _unpack(c2);
|
|
222
|
+
const x1 = Math.max(0, x),
|
|
223
|
+
y1 = Math.max(0, y);
|
|
224
|
+
const x2 = Math.min(W, x + w),
|
|
225
|
+
y2 = Math.min(H, y + h);
|
|
226
|
+
for (let py = y1; py < y2; py++) {
|
|
227
|
+
for (let px = x1; px < x2; px++) {
|
|
228
|
+
let t;
|
|
229
|
+
if (dir === 'h') {
|
|
230
|
+
t = w > 0 ? (px - x) / w : 0;
|
|
231
|
+
} else if (dir === 'd') {
|
|
232
|
+
t = w + h > 0 ? (px - x + (py - y)) / (w + h) : 0;
|
|
233
|
+
} else {
|
|
234
|
+
t = h > 0 ? (py - y) / h : 0;
|
|
235
|
+
}
|
|
236
|
+
t = Math.max(0, Math.min(1, t));
|
|
237
|
+
_blend(
|
|
238
|
+
px,
|
|
239
|
+
py,
|
|
240
|
+
(a1.r + (a2.r - a1.r) * t) | 0,
|
|
241
|
+
(a1.g + (a2.g - a1.g) * t) | 0,
|
|
242
|
+
(a1.b + (a2.b - a1.b) * t) | 0,
|
|
243
|
+
(a1.a + (a2.a - a1.a) * t) | 0
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* drawRadialGradient(cx, cy, radius, innerColor, outerColor)
|
|
251
|
+
* Great for glows, halos, spot lights.
|
|
252
|
+
*/
|
|
253
|
+
function drawRadialGradient(cx, cy, radius, innerColor, outerColor) {
|
|
254
|
+
cx |= 0;
|
|
255
|
+
cy |= 0;
|
|
256
|
+
radius = radius | 0;
|
|
257
|
+
const ci = _unpack(innerColor),
|
|
258
|
+
co = _unpack(outerColor);
|
|
259
|
+
const x1 = Math.max(0, cx - radius),
|
|
260
|
+
y1 = Math.max(0, cy - radius);
|
|
261
|
+
const x2 = Math.min(W, cx + radius),
|
|
262
|
+
y2 = Math.min(H, cy + radius);
|
|
263
|
+
const r2 = radius * radius;
|
|
264
|
+
for (let py = y1; py < y2; py++) {
|
|
265
|
+
for (let px = x1; px < x2; px++) {
|
|
266
|
+
const dx = px - cx,
|
|
267
|
+
dy = py - cy;
|
|
268
|
+
const d2 = dx * dx + dy * dy;
|
|
269
|
+
if (d2 > r2) continue;
|
|
270
|
+
const t = Math.sqrt(d2) / radius;
|
|
271
|
+
_blend(
|
|
272
|
+
px,
|
|
273
|
+
py,
|
|
274
|
+
(ci.r + (co.r - ci.r) * t) | 0,
|
|
275
|
+
(ci.g + (co.g - ci.g) * t) | 0,
|
|
276
|
+
(ci.b + (co.b - ci.b) * t) | 0,
|
|
277
|
+
(ci.a + (co.a - ci.a) * t) | 0
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Rounded rect ────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* drawRoundedRect(x, y, w, h, radius, color, fill=true)
|
|
287
|
+
* PS1/N64-style rounded panel corners.
|
|
288
|
+
*/
|
|
289
|
+
function drawRoundedRect(x, y, w, h, radius, color, fill = true) {
|
|
290
|
+
x |= 0;
|
|
291
|
+
y |= 0;
|
|
292
|
+
w |= 0;
|
|
293
|
+
h |= 0;
|
|
294
|
+
radius = Math.min(radius | 0, Math.min(w, h) >> 1);
|
|
295
|
+
const { r, g, b, a } = _unpack(color);
|
|
296
|
+
|
|
297
|
+
if (fill) {
|
|
298
|
+
// Fill main body (three rects)
|
|
299
|
+
for (let py = y + radius; py < y + h - radius; py++) {
|
|
300
|
+
for (let px = x; px < x + w; px++) _blend(px, py, r, g, b, a);
|
|
301
|
+
}
|
|
302
|
+
for (let py = y; py < y + radius; py++) {
|
|
303
|
+
for (let px = x + radius; px < x + w - radius; px++) _blend(px, py, r, g, b, a);
|
|
304
|
+
}
|
|
305
|
+
for (let py = y + h - radius; py < y + h; py++) {
|
|
306
|
+
for (let px = x + radius; px < x + w - radius; px++) _blend(px, py, r, g, b, a);
|
|
307
|
+
}
|
|
308
|
+
// Fill corners with quarter-circles
|
|
309
|
+
const corners = [
|
|
310
|
+
[x + radius, y + radius],
|
|
311
|
+
[x + w - radius - 1, y + radius],
|
|
312
|
+
[x + radius, y + h - radius - 1],
|
|
313
|
+
[x + w - radius - 1, y + h - radius - 1],
|
|
314
|
+
];
|
|
315
|
+
for (const [cx, cy] of corners) {
|
|
316
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
317
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
318
|
+
if (dx * dx + dy * dy <= radius * radius) _blend(cx + dx, cy + dy, r, g, b, a);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Outline only — top/bottom edges
|
|
324
|
+
for (let px = x + radius; px < x + w - radius; px++) {
|
|
325
|
+
_blend(px, y, r, g, b, a);
|
|
326
|
+
_blend(px, y + h - 1, r, g, b, a);
|
|
327
|
+
}
|
|
328
|
+
// Left/right edges
|
|
329
|
+
for (let py = y + radius; py < y + h - radius; py++) {
|
|
330
|
+
_blend(x, py, r, g, b, a);
|
|
331
|
+
_blend(x + w - 1, py, r, g, b, a);
|
|
332
|
+
}
|
|
333
|
+
// Corners (just the arc outline)
|
|
334
|
+
const corners = [
|
|
335
|
+
[x + radius, y + radius, -1, -1],
|
|
336
|
+
[x + w - radius - 1, y + radius, 1, -1],
|
|
337
|
+
[x + radius, y + h - radius - 1, -1, 1],
|
|
338
|
+
[x + w - radius - 1, y + h - radius - 1, 1, 1],
|
|
339
|
+
];
|
|
340
|
+
for (const [cx, cy, sx, sy] of corners) {
|
|
341
|
+
for (let angle = 0; angle <= 90; angle += 1) {
|
|
342
|
+
const rad = (angle * Math.PI) / 180;
|
|
343
|
+
const px = (cx + sx * Math.cos(rad) * radius) | 0;
|
|
344
|
+
const py = (cy + sy * Math.sin(rad) * radius) | 0;
|
|
345
|
+
_blend(px, py, r, g, b, a);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Polygon ─────────────────────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* poly(points, color, fill=true)
|
|
355
|
+
* points: [[x0,y0],[x1,y1],...] — convex or concave polygon
|
|
356
|
+
* Uses scanline fill for filled, Bresenham for outline.
|
|
357
|
+
*/
|
|
358
|
+
function poly(points, color, fill = true) {
|
|
359
|
+
if (points.length < 3) return;
|
|
360
|
+
const { r, g, b, a } = _unpack(color);
|
|
361
|
+
|
|
362
|
+
if (fill) {
|
|
363
|
+
const minY = Math.max(0, Math.min(...points.map(p => p[1])) | 0);
|
|
364
|
+
const maxY = Math.min(H - 1, Math.max(...points.map(p => p[1])) | 0);
|
|
365
|
+
for (let py = minY; py <= maxY; py++) {
|
|
366
|
+
const intersections = [];
|
|
367
|
+
const n = points.length;
|
|
368
|
+
for (let i = 0; i < n; i++) {
|
|
369
|
+
const [x0, y0] = points[i];
|
|
370
|
+
const [x1, y1] = points[(i + 1) % n];
|
|
371
|
+
if ((y0 <= py && py < y1) || (y1 <= py && py < y0)) {
|
|
372
|
+
const t = (py - y0) / (y1 - y0);
|
|
373
|
+
intersections.push(x0 + t * (x1 - x0));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
intersections.sort((a, b) => a - b);
|
|
377
|
+
for (let i = 0; i + 1 < intersections.length; i += 2) {
|
|
378
|
+
const lx = Math.max(0, intersections[i] | 0);
|
|
379
|
+
const rx = Math.min(W - 1, intersections[i + 1] | 0);
|
|
380
|
+
for (let px = lx; px <= rx; px++) _blend(px, py, r, g, b, a);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
// Outline
|
|
385
|
+
const n = points.length;
|
|
386
|
+
for (let i = 0; i < n; i++) {
|
|
387
|
+
const [x0, y0] = points[i];
|
|
388
|
+
const [x1, y1] = points[(i + 1) % n];
|
|
389
|
+
// Bresenham
|
|
390
|
+
let dx = Math.abs(x1 - x0),
|
|
391
|
+
dy = -Math.abs(y1 - y0);
|
|
392
|
+
let sx = x0 < x1 ? 1 : -1,
|
|
393
|
+
sy = y0 < y1 ? 1 : -1;
|
|
394
|
+
let err = dx + dy,
|
|
395
|
+
px = x0 | 0,
|
|
396
|
+
py = y0 | 0;
|
|
397
|
+
for (;;) {
|
|
398
|
+
_blend(px, py, r, g, b, a);
|
|
399
|
+
if (px === (x1 | 0) && py === (y1 | 0)) break;
|
|
400
|
+
const e2 = 2 * err;
|
|
401
|
+
if (e2 >= dy) {
|
|
402
|
+
err += dy;
|
|
403
|
+
px += sx;
|
|
404
|
+
}
|
|
405
|
+
if (e2 <= dx) {
|
|
406
|
+
err += dx;
|
|
407
|
+
py += sy;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* drawTriangle(x0,y0, x1,y1, x2,y2, color, fill=true)
|
|
416
|
+
*/
|
|
417
|
+
function drawTriangle(x0, y0, x1, y1, x2, y2, color, fill = true) {
|
|
418
|
+
poly(
|
|
419
|
+
[
|
|
420
|
+
[x0, y0],
|
|
421
|
+
[x1, y1],
|
|
422
|
+
[x2, y2],
|
|
423
|
+
],
|
|
424
|
+
color,
|
|
425
|
+
fill
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* drawDiamond(cx, cy, halfW, halfH, color, fill=true)
|
|
431
|
+
*/
|
|
432
|
+
function drawDiamond(cx, cy, halfW, halfH, color, fill = true) {
|
|
433
|
+
poly(
|
|
434
|
+
[
|
|
435
|
+
[cx, cy - halfH],
|
|
436
|
+
[cx + halfW, cy],
|
|
437
|
+
[cx, cy + halfH],
|
|
438
|
+
[cx - halfW, cy],
|
|
439
|
+
],
|
|
440
|
+
color,
|
|
441
|
+
fill
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Text helpers ─────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
// We reach into the gpu's exposed stdApi print via globalThis
|
|
448
|
+
function _print(text, x, y, color, scale) {
|
|
449
|
+
if (typeof globalThis.print === 'function') globalThis.print(text, x, y, color, scale);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function measureText(text, scale = 1) {
|
|
453
|
+
// 5px glyph + 1px spacing = 6px per character; 7px tall
|
|
454
|
+
const s = Math.max(1, Math.round(scale));
|
|
455
|
+
return { width: text.length * 6 * s, height: 7 * s };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** printCentered(text, cx, y, color, scale=1) — centre on x */
|
|
459
|
+
function printCentered(text, cx, y, color, scale = 1) {
|
|
460
|
+
const w = measureText(text, scale).width;
|
|
461
|
+
_print(text, (cx - w / 2) | 0, y, color, scale);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** printRight(text, rightX, y, color, scale=1) — right-align */
|
|
465
|
+
function printRight(text, rightX, y, color, scale = 1) {
|
|
466
|
+
const w = measureText(text, scale).width;
|
|
467
|
+
_print(text, (rightX - w) | 0, y, color, scale);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* drawGlowText(text, x, y, color, glowColor, scale=1)
|
|
472
|
+
* Prints text with a soft glow halo by drawing the glowColor in 8 surrounding offsets
|
|
473
|
+
* then the main color on top. Pure N64/Dreamcast HUD aesthetic.
|
|
474
|
+
*/
|
|
475
|
+
function drawGlowText(text, x, y, color, glowColor, scale = 1) {
|
|
476
|
+
const offsets = [
|
|
477
|
+
[-1, -1],
|
|
478
|
+
[0, -1],
|
|
479
|
+
[1, -1],
|
|
480
|
+
[-1, 0],
|
|
481
|
+
[1, 0],
|
|
482
|
+
[-1, 1],
|
|
483
|
+
[0, 1],
|
|
484
|
+
[1, 1],
|
|
485
|
+
];
|
|
486
|
+
for (const [dx, dy] of offsets) {
|
|
487
|
+
_print(text, x + dx, y + dy, glowColor, scale);
|
|
488
|
+
}
|
|
489
|
+
_print(text, x, y, color, scale);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* drawGlowTextCentered(text, cx, y, color, glowColor, scale=1)
|
|
494
|
+
*/
|
|
495
|
+
function drawGlowTextCentered(text, cx, y, color, glowColor, scale = 1) {
|
|
496
|
+
const w = measureText(text, scale).width;
|
|
497
|
+
drawGlowText(text, (cx - w / 2) | 0, y, color, glowColor, scale);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* drawPulsingText(text, cx, y, color, time, opts)
|
|
502
|
+
* Centered text that pulses opacity and/or scale over time.
|
|
503
|
+
* opts: { frequency, minAlpha, glowColor, scale }
|
|
504
|
+
*/
|
|
505
|
+
function drawPulsingText(text, cx, y, color, time, opts = {}) {
|
|
506
|
+
const freq = opts.frequency ?? 3;
|
|
507
|
+
const minAlpha = opts.minAlpha ?? 120;
|
|
508
|
+
const scale = opts.scale ?? 1;
|
|
509
|
+
const alpha = Math.floor((Math.sin(time * freq) * 0.5 + 0.5) * (255 - minAlpha) + minAlpha);
|
|
510
|
+
const { r, g, b } = _unpack(color);
|
|
511
|
+
const pulsedColor = rgba8(r, g, b, alpha);
|
|
512
|
+
if (opts.glowColor) {
|
|
513
|
+
drawGlowTextCentered(text, cx, y, pulsedColor, opts.glowColor, scale);
|
|
514
|
+
} else {
|
|
515
|
+
printCentered(text, cx, y, pulsedColor, scale);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ── Screen overlays ──────────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* drawScanlines(alpha=80, spacing=2)
|
|
523
|
+
* CRT scanline overlay — horizontal dark bands every `spacing` rows.
|
|
524
|
+
*/
|
|
525
|
+
function drawScanlines(alpha = 80, spacing = 2) {
|
|
526
|
+
const p = fb.pixels;
|
|
527
|
+
const af = (255 - alpha) / 255;
|
|
528
|
+
for (let y = 0; y < H; y += spacing) {
|
|
529
|
+
for (let x = 0; x < W; x++) {
|
|
530
|
+
const i = (y * W + x) * 4;
|
|
531
|
+
p[i] = (p[i] * af) | 0;
|
|
532
|
+
p[i + 1] = (p[i + 1] * af) | 0;
|
|
533
|
+
p[i + 2] = (p[i + 2] * af) | 0;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* drawNoise(x, y, w, h, alpha=40, seed=0)
|
|
540
|
+
* Deterministic LCG grain/static overlay. Good for title card grit.
|
|
541
|
+
*/
|
|
542
|
+
function drawNoise(x, y, w, h, alpha = 40, seed = 0) {
|
|
543
|
+
x |= 0;
|
|
544
|
+
y |= 0;
|
|
545
|
+
w |= 0;
|
|
546
|
+
h |= 0;
|
|
547
|
+
let s = seed | 12345;
|
|
548
|
+
const x1 = Math.max(0, x),
|
|
549
|
+
y1 = Math.max(0, y);
|
|
550
|
+
const x2 = Math.min(W, x + w),
|
|
551
|
+
y2 = Math.min(H, y + h);
|
|
552
|
+
for (let py = y1; py < y2; py++) {
|
|
553
|
+
for (let px = x1; px < x2; px++) {
|
|
554
|
+
s = (s * 1664525 + 1013904223) & 0xffffffff;
|
|
555
|
+
const n = (s >>> 16) & 0xff;
|
|
556
|
+
_blend(px, py, n, n, n, ((n / 255) * alpha) | 0);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* drawCheckerboard(x, y, w, h, c1, c2, size=8)
|
|
563
|
+
* N64 loading pattern / retro background filler.
|
|
564
|
+
*/
|
|
565
|
+
function drawCheckerboard(x, y, w, h, c1, c2, size = 8) {
|
|
566
|
+
x |= 0;
|
|
567
|
+
y |= 0;
|
|
568
|
+
w |= 0;
|
|
569
|
+
h |= 0;
|
|
570
|
+
size |= 0;
|
|
571
|
+
const x1 = Math.max(0, x),
|
|
572
|
+
y1 = Math.max(0, y);
|
|
573
|
+
const x2 = Math.min(W, x + w),
|
|
574
|
+
y2 = Math.min(H, y + h);
|
|
575
|
+
for (let py = y1; py < y2; py++) {
|
|
576
|
+
for (let px = x1; px < x2; px++) {
|
|
577
|
+
const cell = ((((px - x) / size) | 0) + (((py - y) / size) | 0)) % 2;
|
|
578
|
+
_pset(px, py, cell === 0 ? c1 : c2);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ── Progress / HUD bars ───────────────────────────────────────────────────────
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* drawProgressBar(x, y, w, h, t, fgColor, bgColor, borderColor)
|
|
587
|
+
* t = 0..1 fill fraction.
|
|
588
|
+
*/
|
|
589
|
+
function drawProgressBar(x, y, w, h, t, fgColor, bgColor, borderColor) {
|
|
590
|
+
t = Math.max(0, Math.min(1, t));
|
|
591
|
+
// Background
|
|
592
|
+
if (bgColor !== undefined) {
|
|
593
|
+
const bg = _unpack(bgColor);
|
|
594
|
+
for (let py = y; py < y + h; py++)
|
|
595
|
+
for (let px = x; px < x + w; px++) _blend(px, py, bg.r, bg.g, bg.b, bg.a);
|
|
596
|
+
}
|
|
597
|
+
// Fill
|
|
598
|
+
const fill = (w * t) | 0;
|
|
599
|
+
const fg = _unpack(fgColor);
|
|
600
|
+
for (let py = y; py < y + h; py++)
|
|
601
|
+
for (let px = x; px < x + fill; px++) _blend(px, py, fg.r, fg.g, fg.b, fg.a);
|
|
602
|
+
// Border
|
|
603
|
+
if (borderColor !== undefined) {
|
|
604
|
+
const bc = _unpack(borderColor);
|
|
605
|
+
for (let px = x; px < x + w; px++) {
|
|
606
|
+
_blend(px, y, bc.r, bc.g, bc.b, bc.a);
|
|
607
|
+
_blend(px, y + h - 1, bc.r, bc.g, bc.b, bc.a);
|
|
608
|
+
}
|
|
609
|
+
for (let py = y; py < y + h; py++) {
|
|
610
|
+
_blend(x, py, bc.r, bc.g, bc.b, bc.a);
|
|
611
|
+
_blend(x + w - 1, py, bc.r, bc.g, bc.b, bc.a);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* drawHealthBar(x, y, w, h, current, max, opts)
|
|
618
|
+
* opts: { barColor, backgroundColor, borderColor, dangerColor, dangerThreshold }
|
|
619
|
+
* Auto-switches to dangerColor when hp < dangerThreshold (default 0.25).
|
|
620
|
+
*/
|
|
621
|
+
function drawHealthBar(x, y, w, h, current, max, opts = {}) {
|
|
622
|
+
const t = max > 0 ? current / max : 0;
|
|
623
|
+
const danger = opts.dangerThreshold ?? 0.25;
|
|
624
|
+
const fg =
|
|
625
|
+
t < danger ? (opts.dangerColor ?? rgba8(220, 40, 40)) : (opts.barColor ?? rgba8(50, 220, 80));
|
|
626
|
+
drawProgressBar(
|
|
627
|
+
x,
|
|
628
|
+
y,
|
|
629
|
+
w,
|
|
630
|
+
h,
|
|
631
|
+
t,
|
|
632
|
+
fg,
|
|
633
|
+
opts.backgroundColor ?? rgba8(30, 30, 30, 200),
|
|
634
|
+
opts.borderColor ?? rgba8(200, 200, 200, 180)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ── PS1/N64 panel border ──────────────────────────────────────────────────────
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* drawPixelBorder(x, y, w, h, lightColor, darkColor, thickness=2)
|
|
642
|
+
* 3D-embossed bevel — PS1/SNES-style UI panel border.
|
|
643
|
+
*/
|
|
644
|
+
function drawPixelBorder(x, y, w, h, lightColor, darkColor, thickness = 2) {
|
|
645
|
+
const lc = _unpack(lightColor),
|
|
646
|
+
dc = _unpack(darkColor);
|
|
647
|
+
for (let i = 0; i < thickness; i++) {
|
|
648
|
+
// Top & left (light)
|
|
649
|
+
for (let px = x + i; px < x + w - i; px++) _blend(px, y + i, lc.r, lc.g, lc.b, lc.a);
|
|
650
|
+
for (let py = y + i; py < y + h - i; py++) _blend(x + i, py, lc.r, lc.g, lc.b, lc.a);
|
|
651
|
+
// Bottom & right (dark)
|
|
652
|
+
for (let px = x + i; px < x + w - i; px++) _blend(px, y + h - 1 - i, dc.r, dc.g, dc.b, dc.a);
|
|
653
|
+
for (let py = y + i; py < y + h - i; py++) _blend(x + w - 1 - i, py, dc.r, dc.g, dc.b, dc.a);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ── Decorative shapes ─────────────────────────────────────────────────────────
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* drawStarburst(cx, cy, outerR, innerR, spikes, color, fill=true)
|
|
661
|
+
* Classic star shape — great for score popups and collectible icons.
|
|
662
|
+
*/
|
|
663
|
+
function drawStarburst(cx, cy, outerR, innerR, spikes, color, fill = true) {
|
|
664
|
+
const pts = [];
|
|
665
|
+
for (let i = 0; i < spikes * 2; i++) {
|
|
666
|
+
const angle = (i * Math.PI) / spikes - Math.PI / 2;
|
|
667
|
+
const r = i % 2 === 0 ? outerR : innerR;
|
|
668
|
+
pts.push([cx + Math.cos(angle) * r, cy + Math.sin(angle) * r]);
|
|
669
|
+
}
|
|
670
|
+
poly(pts, color, fill);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* drawWave(x, y, w, amplitude, frequency, phase, color, thickness=2)
|
|
675
|
+
* Animated sine wave — good for water, plasma, audio visualizers.
|
|
676
|
+
* phase = time * speed for animation.
|
|
677
|
+
*/
|
|
678
|
+
function drawWave(x, y, w, amplitude, frequency, phase, color, thickness = 2) {
|
|
679
|
+
const { r, g, b, a } = _unpack(color);
|
|
680
|
+
for (let px = x; px < x + w && px < W; px++) {
|
|
681
|
+
const py = (y + Math.sin((px - x) * frequency + phase) * amplitude) | 0;
|
|
682
|
+
for (let t = -thickness >> 1; t <= thickness >> 1; t++) {
|
|
683
|
+
const ny = py + t;
|
|
684
|
+
if (ny >= 0 && ny < H) _blend(px, ny, r, g, b, a);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* drawSpiral(cx, cy, turns, spacing, color)
|
|
691
|
+
* Decorative Archimedean spiral — psychedelic demoscene / loading screen art.
|
|
692
|
+
*/
|
|
693
|
+
function drawSpiral(cx, cy, turns, spacing, color) {
|
|
694
|
+
const { r, g, b, a } = _unpack(color);
|
|
695
|
+
const steps = turns * 360;
|
|
696
|
+
for (let i = 0; i < steps; i++) {
|
|
697
|
+
const angle = (i / 180) * Math.PI;
|
|
698
|
+
const rad = (i / 360) * spacing;
|
|
699
|
+
const px = (cx + Math.cos(angle) * rad) | 0;
|
|
700
|
+
const py = (cy + Math.sin(angle) * rad) | 0;
|
|
701
|
+
if (px >= 0 && px < W && py >= 0 && py < H) _blend(px, py, r, g, b, a);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ── Composite widgets ─────────────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* drawPanel(x, y, w, h, opts)
|
|
709
|
+
* A complete PS1/N64-style info panel with background, border, and optional title.
|
|
710
|
+
* opts: { bgColor, borderLight, borderDark, title, titleColor, padding, alpha }
|
|
711
|
+
*/
|
|
712
|
+
function drawPanel(x, y, w, h, opts = {}) {
|
|
713
|
+
const bg = opts.bgColor ?? rgba8(10, 10, 30, 200);
|
|
714
|
+
const bl = opts.borderLight ?? rgba8(180, 180, 220, 255);
|
|
715
|
+
const bd = opts.borderDark ?? rgba8(30, 30, 60, 255);
|
|
716
|
+
// opts.alpha available for callers but not used in fill path
|
|
717
|
+
|
|
718
|
+
// Fill panel (re-use opts.bgColor with alpha override)
|
|
719
|
+
const bgc = _unpack(bg);
|
|
720
|
+
for (let py = y + 2; py < y + h - 2; py++)
|
|
721
|
+
for (let px = x + 2; px < x + w - 2; px++) _blend(px, py, bgc.r, bgc.g, bgc.b, bgc.a);
|
|
722
|
+
|
|
723
|
+
// 3D border
|
|
724
|
+
drawPixelBorder(x, y, w, h, bl, bd, 2);
|
|
725
|
+
|
|
726
|
+
// Optional title text at top
|
|
727
|
+
if (opts.title) {
|
|
728
|
+
const tc = opts.titleColor ?? rgba8(255, 255, 255, 255);
|
|
729
|
+
const pad = opts.padding ?? 6;
|
|
730
|
+
printCentered(opts.title, x + w / 2, y + pad, tc);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* drawMinimap(x, y, size, entities, bgColor)
|
|
736
|
+
* entities: [{ x, y, color, worldW, worldH }] — dot map for players/enemies/items.
|
|
737
|
+
* worldW/worldH are the world-space bounds to normalise against.
|
|
738
|
+
*/
|
|
739
|
+
function drawMinimap(x, y, size, entities, bgColor) {
|
|
740
|
+
const bg = bgColor ?? rgba8(0, 0, 0, 180);
|
|
741
|
+
const bgc = _unpack(bg);
|
|
742
|
+
// Fill minimap bg
|
|
743
|
+
for (let py = y; py < y + size; py++)
|
|
744
|
+
for (let px = x; px < x + size; px++) _blend(px, py, bgc.r, bgc.g, bgc.b, bgc.a);
|
|
745
|
+
// Border
|
|
746
|
+
drawPixelBorder(x, y, size, size, rgba8(150, 150, 150), rgba8(50, 50, 50), 1);
|
|
747
|
+
// Dots
|
|
748
|
+
for (const e of entities) {
|
|
749
|
+
const ww = e.worldW ?? 100,
|
|
750
|
+
wh = e.worldH ?? 100;
|
|
751
|
+
const dx = (x + (e.x / ww) * size) | 0;
|
|
752
|
+
const dy = (y + (e.y / wh) * size) | 0;
|
|
753
|
+
const ec = _unpack(e.color ?? rgba8(255, 255, 255));
|
|
754
|
+
// 2x2 dot
|
|
755
|
+
for (let oy = 0; oy < 2; oy++)
|
|
756
|
+
for (let ox = 0; ox < 2; ox++) _blend(dx + ox, dy + oy, ec.r, ec.g, ec.b, ec.a);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* scrollingText(text, y, speed, time, color, scale=1, width=640)
|
|
762
|
+
* Marquee text scrolling left across the screen.
|
|
763
|
+
* time = accumulated seconds (from novaStore or cart-local variable).
|
|
764
|
+
*/
|
|
765
|
+
function scrollingText(text, y, speed, time, color, scale = 1, width = 640) {
|
|
766
|
+
const tw = measureText(text, scale).width;
|
|
767
|
+
const x = (width - ((time * speed) % (width + tw))) | 0;
|
|
768
|
+
_print(text, x, y, color, scale);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ── Full-screen helpers ───────────────────────────────────────────────────────
|
|
772
|
+
|
|
773
|
+
/** Full-screen vertical gradient — great for sky/title backgrounds */
|
|
774
|
+
function drawSkyGradient(topColor, bottomColor) {
|
|
775
|
+
drawGradient(0, 0, W, H, topColor, bottomColor, 'v');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Full-screen flash — use alpha 0..255 for a fade effect */
|
|
779
|
+
function drawFlash(color) {
|
|
780
|
+
const { r, g, b, a } = _unpack(color);
|
|
781
|
+
for (let py = 0; py < H; py++) for (let px = 0; px < W; px++) _blend(px, py, r, g, b, a);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Draw a crosshair / reticle at (cx, cy).
|
|
786
|
+
* @param {number} cx - centre X in screen pixels
|
|
787
|
+
* @param {number} cy - centre Y in screen pixels
|
|
788
|
+
* @param {number} [size=8] - half-length of each arm in pixels
|
|
789
|
+
* @param {number} [color=0xffffffff] - rgba8 or 0xRRGGBB colour
|
|
790
|
+
* @param {'cross'|'dot'|'circle'} [style='cross'] - reticle style
|
|
791
|
+
*/
|
|
792
|
+
function drawCrosshair(cx, cy, size = 8, color = 0xffffffff, style = 'cross') {
|
|
793
|
+
if (style === 'cross' || style === 'dot') {
|
|
794
|
+
// Horizontal arm
|
|
795
|
+
for (let x = cx - size; x <= cx + size; x++) _pset(x, cy, color);
|
|
796
|
+
// Vertical arm
|
|
797
|
+
for (let y = cy - size; y <= cy + size; y++) _pset(cx, y, color);
|
|
798
|
+
}
|
|
799
|
+
if (style === 'circle') {
|
|
800
|
+
// Thin circle
|
|
801
|
+
const steps = Math.max(32, size * 4);
|
|
802
|
+
for (let i = 0; i < steps; i++) {
|
|
803
|
+
const a = (i / steps) * Math.PI * 2;
|
|
804
|
+
_pset(Math.round(cx + Math.cos(a) * size), Math.round(cy + Math.sin(a) * size), color);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (style === 'dot') {
|
|
808
|
+
// Centre dot (extra pixels around the intersection)
|
|
809
|
+
_pset(cx - 1, cy, color);
|
|
810
|
+
_pset(cx + 1, cy, color);
|
|
811
|
+
_pset(cx, cy - 1, color);
|
|
812
|
+
_pset(cx, cy + 1, color);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ─── expose ──────────────────────────────────────────────────────────────────
|
|
817
|
+
return {
|
|
818
|
+
exposeTo(target) {
|
|
819
|
+
Object.assign(target, {
|
|
820
|
+
// Colour helpers
|
|
821
|
+
colorLerp,
|
|
822
|
+
colorMix,
|
|
823
|
+
hexColor,
|
|
824
|
+
hslColor,
|
|
825
|
+
n64Palette,
|
|
826
|
+
|
|
827
|
+
// Math utilities
|
|
828
|
+
lerp,
|
|
829
|
+
clamp,
|
|
830
|
+
randRange,
|
|
831
|
+
randInt,
|
|
832
|
+
dist,
|
|
833
|
+
dist3d,
|
|
834
|
+
remap,
|
|
835
|
+
pulse,
|
|
836
|
+
deg2rad,
|
|
837
|
+
rad2deg,
|
|
838
|
+
|
|
839
|
+
// Gradient fills
|
|
840
|
+
drawGradient,
|
|
841
|
+
drawRadialGradient,
|
|
842
|
+
drawSkyGradient,
|
|
843
|
+
drawFlash,
|
|
844
|
+
|
|
845
|
+
// Shapes
|
|
846
|
+
drawRoundedRect,
|
|
847
|
+
poly,
|
|
848
|
+
drawTriangle,
|
|
849
|
+
drawDiamond,
|
|
850
|
+
drawStarburst,
|
|
851
|
+
drawWave,
|
|
852
|
+
drawSpiral,
|
|
853
|
+
drawCheckerboard,
|
|
854
|
+
|
|
855
|
+
// Text
|
|
856
|
+
measureText,
|
|
857
|
+
printCentered,
|
|
858
|
+
printRight,
|
|
859
|
+
drawGlowText,
|
|
860
|
+
drawGlowTextCentered,
|
|
861
|
+
drawPulsingText,
|
|
862
|
+
|
|
863
|
+
// Overlays
|
|
864
|
+
drawScanlines,
|
|
865
|
+
drawNoise,
|
|
866
|
+
|
|
867
|
+
// HUD Widgets
|
|
868
|
+
drawProgressBar,
|
|
869
|
+
drawHealthBar,
|
|
870
|
+
drawPixelBorder,
|
|
871
|
+
drawPanel,
|
|
872
|
+
drawCrosshair,
|
|
873
|
+
drawMinimap,
|
|
874
|
+
scrollingText,
|
|
875
|
+
});
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|