like2d 1.0.0
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/README.md +66 -0
- package/dist/audio.d.ts +52 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +250 -0
- package/dist/events.d.ts +36 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +1 -0
- package/dist/gamecontrollerdb.txt +2219 -0
- package/dist/gamepad-button-map.d.ts +5 -0
- package/dist/gamepad-button-map.d.ts.map +1 -0
- package/dist/gamepad-button-map.js +56 -0
- package/dist/gamepad-db.d.ts +49 -0
- package/dist/gamepad-db.d.ts.map +1 -0
- package/dist/gamepad-db.js +192 -0
- package/dist/gamepad-mapping.d.ts +31 -0
- package/dist/gamepad-mapping.d.ts.map +1 -0
- package/dist/gamepad-mapping.js +191 -0
- package/dist/gamepad.d.ts +56 -0
- package/dist/gamepad.d.ts.map +1 -0
- package/dist/gamepad.js +216 -0
- package/dist/graphics.d.ts +80 -0
- package/dist/graphics.d.ts.map +1 -0
- package/dist/graphics.js +388 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +250 -0
- package/dist/input-state.d.ts +14 -0
- package/dist/input-state.d.ts.map +1 -0
- package/dist/input-state.js +50 -0
- package/dist/input.d.ts +36 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +127 -0
- package/dist/keyboard.d.ts +9 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +33 -0
- package/dist/mouse.d.ts +20 -0
- package/dist/mouse.d.ts.map +1 -0
- package/dist/mouse.js +84 -0
- package/dist/rect.d.ts +27 -0
- package/dist/rect.d.ts.map +1 -0
- package/dist/rect.js +132 -0
- package/dist/scene.d.ts +10 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +1 -0
- package/dist/timer.d.ts +19 -0
- package/dist/timer.d.ts.map +1 -0
- package/dist/timer.js +86 -0
- package/dist/vector2.d.ts +32 -0
- package/dist/vector2.d.ts.map +1 -0
- package/dist/vector2.js +105 -0
- package/package.json +64 -0
package/dist/gamepad.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { getButtonName, getButtonIndex } from './gamepad-button-map';
|
|
2
|
+
import { InputStateTracker } from './input-state';
|
|
3
|
+
import { gamepadMapping } from './gamepad-mapping';
|
|
4
|
+
export { getButtonName, getButtonIndex };
|
|
5
|
+
const AXIS_DEADZONE = 0.15;
|
|
6
|
+
function applyDeadzone(value, deadzone = AXIS_DEADZONE) {
|
|
7
|
+
if (Math.abs(value) < deadzone)
|
|
8
|
+
return 0;
|
|
9
|
+
const sign = value < 0 ? -1 : 1;
|
|
10
|
+
const magnitude = Math.abs(value);
|
|
11
|
+
return sign * (magnitude - deadzone) / (1 - deadzone);
|
|
12
|
+
}
|
|
13
|
+
function applyRadialDeadzone(x, y, deadzone = AXIS_DEADZONE) {
|
|
14
|
+
const magnitude = Math.sqrt(x * x + y * y);
|
|
15
|
+
if (magnitude < deadzone)
|
|
16
|
+
return { x: 0, y: 0 };
|
|
17
|
+
const scale = (magnitude - deadzone) / (magnitude * (1 - deadzone));
|
|
18
|
+
return { x: x * scale, y: y * scale };
|
|
19
|
+
}
|
|
20
|
+
export class Gamepad {
|
|
21
|
+
constructor() {
|
|
22
|
+
Object.defineProperty(this, "buttonTrackers", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: new Map()
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "connectedGamepads", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: new Map()
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "buttonMappings", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: new Map()
|
|
39
|
+
});
|
|
40
|
+
this.setupEventListeners();
|
|
41
|
+
}
|
|
42
|
+
async init() {
|
|
43
|
+
await gamepadMapping.loadDatabase();
|
|
44
|
+
}
|
|
45
|
+
extractVendorProduct(gamepad) {
|
|
46
|
+
const id = gamepad.id;
|
|
47
|
+
const vendorProductMatch = id.match(/Vendor:\s*([0-9a-fA-F]+)\s+Product:\s*([0-9a-fA-F]+)/i);
|
|
48
|
+
if (vendorProductMatch) {
|
|
49
|
+
const vendor = parseInt(vendorProductMatch[1], 16);
|
|
50
|
+
const product = parseInt(vendorProductMatch[2], 16);
|
|
51
|
+
if (!isNaN(vendor) && !isNaN(product)) {
|
|
52
|
+
return { vendor, product };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const hexMatch = id.match(/^([0-9a-fA-F]{4})[\s-]+([0-9a-fA-F]{4})/);
|
|
56
|
+
if (hexMatch) {
|
|
57
|
+
const vendor = parseInt(hexMatch[1], 16);
|
|
58
|
+
const product = parseInt(hexMatch[2], 16);
|
|
59
|
+
if (!isNaN(vendor) && !isNaN(product)) {
|
|
60
|
+
return { vendor, product };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
setupEventListeners() {
|
|
66
|
+
window.addEventListener('gamepadconnected', (e) => {
|
|
67
|
+
this.connectedGamepads.set(e.gamepad.index, e.gamepad);
|
|
68
|
+
this.buttonTrackers.set(e.gamepad.index, new InputStateTracker());
|
|
69
|
+
const mapping = gamepadMapping.getMapping(e.gamepad);
|
|
70
|
+
this.buttonMappings.set(e.gamepad.index, mapping);
|
|
71
|
+
console.log(`[Gamepad] Connected: "${e.gamepad.id}"`);
|
|
72
|
+
const vp = this.extractVendorProduct(e.gamepad);
|
|
73
|
+
if (vp) {
|
|
74
|
+
console.log(`[Gamepad] Vendor: 0x${vp.vendor.toString(16).padStart(4, '0')}, Product: 0x${vp.product.toString(16).padStart(4, '0')}`);
|
|
75
|
+
}
|
|
76
|
+
const mappingType = e.gamepad.mapping === 'standard' ? 'browser standard' : (mapping.hasMapping ? 'SDL DB' : 'unmapped');
|
|
77
|
+
console.log(`[Gamepad] Mapped as: "${mapping.controllerName}" (${mappingType})`);
|
|
78
|
+
});
|
|
79
|
+
window.addEventListener('gamepaddisconnected', (e) => {
|
|
80
|
+
this.connectedGamepads.delete(e.gamepad.index);
|
|
81
|
+
this.buttonTrackers.delete(e.gamepad.index);
|
|
82
|
+
this.buttonMappings.delete(e.gamepad.index);
|
|
83
|
+
});
|
|
84
|
+
window.addEventListener('blur', () => {
|
|
85
|
+
for (const tracker of this.buttonTrackers.values()) {
|
|
86
|
+
tracker.clear();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
update() {
|
|
91
|
+
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
|
|
92
|
+
const pressed = [];
|
|
93
|
+
const released = [];
|
|
94
|
+
for (let i = 0; i < gamepads.length; i++) {
|
|
95
|
+
const gamepad = gamepads[i];
|
|
96
|
+
if (gamepad) {
|
|
97
|
+
this.connectedGamepads.set(i, gamepad);
|
|
98
|
+
let tracker = this.buttonTrackers.get(i);
|
|
99
|
+
if (!tracker) {
|
|
100
|
+
tracker = new InputStateTracker();
|
|
101
|
+
this.buttonTrackers.set(i, tracker);
|
|
102
|
+
}
|
|
103
|
+
// Get or update the button mapping for this gamepad
|
|
104
|
+
let mapping = this.buttonMappings.get(i);
|
|
105
|
+
if (!mapping) {
|
|
106
|
+
mapping = gamepadMapping.getMapping(gamepad);
|
|
107
|
+
this.buttonMappings.set(i, mapping);
|
|
108
|
+
}
|
|
109
|
+
const pressedButtons = new Set();
|
|
110
|
+
for (let j = 0; j < gamepad.buttons.length; j++) {
|
|
111
|
+
if (gamepad.buttons[j].pressed) {
|
|
112
|
+
// Map the raw button index to standard button index
|
|
113
|
+
const standardIndex = mapping.toStandard.get(j);
|
|
114
|
+
if (standardIndex !== undefined) {
|
|
115
|
+
pressedButtons.add(standardIndex);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const changes = tracker.update(pressedButtons);
|
|
120
|
+
for (const buttonIndex of changes.justPressed) {
|
|
121
|
+
pressed.push({
|
|
122
|
+
gamepadIndex: i,
|
|
123
|
+
buttonIndex,
|
|
124
|
+
buttonName: getButtonName(buttonIndex),
|
|
125
|
+
rawButtonIndex: mapping.fromStandard.get(buttonIndex) ?? buttonIndex,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
for (const buttonIndex of changes.justReleased) {
|
|
129
|
+
released.push({
|
|
130
|
+
gamepadIndex: i,
|
|
131
|
+
buttonIndex,
|
|
132
|
+
buttonName: getButtonName(buttonIndex),
|
|
133
|
+
rawButtonIndex: mapping.fromStandard.get(buttonIndex) ?? buttonIndex,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { pressed, released };
|
|
139
|
+
}
|
|
140
|
+
isConnected(gamepadIndex) {
|
|
141
|
+
return this.connectedGamepads.has(gamepadIndex);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if a button is currently pressed on a specific gamepad
|
|
145
|
+
* Uses mapped button indices (standard layout)
|
|
146
|
+
*/
|
|
147
|
+
isButtonDown(gamepadIndex, button) {
|
|
148
|
+
const buttonIndex = typeof button === 'string' ? getButtonIndex(button) : button;
|
|
149
|
+
if (buttonIndex === undefined)
|
|
150
|
+
return false;
|
|
151
|
+
const tracker = this.buttonTrackers.get(gamepadIndex);
|
|
152
|
+
return tracker ? tracker.isDown(buttonIndex) : false;
|
|
153
|
+
}
|
|
154
|
+
isButtonDownOnAny(button) {
|
|
155
|
+
const buttonIndex = typeof button === 'string' ? getButtonIndex(button) : button;
|
|
156
|
+
if (buttonIndex === undefined)
|
|
157
|
+
return false;
|
|
158
|
+
for (const tracker of this.buttonTrackers.values()) {
|
|
159
|
+
if (tracker.isDown(buttonIndex))
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
getPressedButtons(gamepadIndex) {
|
|
165
|
+
const tracker = this.buttonTrackers.get(gamepadIndex);
|
|
166
|
+
return tracker ? tracker.getCurrentState() : new Set();
|
|
167
|
+
}
|
|
168
|
+
getConnectedGamepads() {
|
|
169
|
+
return Array.from(this.connectedGamepads.keys());
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the raw Gamepad object for a specific index
|
|
173
|
+
*/
|
|
174
|
+
getGamepad(gamepadIndex) {
|
|
175
|
+
return this.connectedGamepads.get(gamepadIndex);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get the button mapping for a specific gamepad
|
|
179
|
+
*/
|
|
180
|
+
getButtonMapping(gamepadIndex) {
|
|
181
|
+
return this.buttonMappings.get(gamepadIndex);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if a gamepad has a known mapping from the database
|
|
185
|
+
*/
|
|
186
|
+
hasMapping(gamepadIndex) {
|
|
187
|
+
const mapping = this.buttonMappings.get(gamepadIndex);
|
|
188
|
+
return mapping?.hasMapping ?? false;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get the controller name for a specific gamepad
|
|
192
|
+
*/
|
|
193
|
+
getControllerName(gamepadIndex) {
|
|
194
|
+
const mapping = this.buttonMappings.get(gamepadIndex);
|
|
195
|
+
return mapping?.controllerName;
|
|
196
|
+
}
|
|
197
|
+
getAxis(gamepadIndex, axisIndex) {
|
|
198
|
+
const gamepad = this.connectedGamepads.get(gamepadIndex);
|
|
199
|
+
if (!gamepad || axisIndex < 0 || axisIndex >= gamepad.axes.length)
|
|
200
|
+
return 0;
|
|
201
|
+
return applyDeadzone(gamepad.axes[axisIndex]);
|
|
202
|
+
}
|
|
203
|
+
getLeftStick(gamepadIndex) {
|
|
204
|
+
const gamepad = this.connectedGamepads.get(gamepadIndex);
|
|
205
|
+
if (!gamepad || gamepad.axes.length < 2)
|
|
206
|
+
return { x: 0, y: 0 };
|
|
207
|
+
return applyRadialDeadzone(gamepad.axes[0], gamepad.axes[1]);
|
|
208
|
+
}
|
|
209
|
+
getRightStick(gamepadIndex) {
|
|
210
|
+
const gamepad = this.connectedGamepads.get(gamepadIndex);
|
|
211
|
+
if (!gamepad || gamepad.axes.length < 4)
|
|
212
|
+
return { x: 0, y: 0 };
|
|
213
|
+
return applyRadialDeadzone(gamepad.axes[2], gamepad.axes[3]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export const gamepad = new Gamepad();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Vector2 } from './vector2';
|
|
2
|
+
import type { Rect } from './rect';
|
|
3
|
+
type DrawMode = 'fill' | 'line';
|
|
4
|
+
export type Color = [number, number, number, number?] | string;
|
|
5
|
+
export type Quad = Rect;
|
|
6
|
+
export type { Vector2, Rect };
|
|
7
|
+
export type Canvas = {
|
|
8
|
+
size: Vector2;
|
|
9
|
+
element: HTMLCanvasElement;
|
|
10
|
+
ctx: CanvasRenderingContext2D;
|
|
11
|
+
};
|
|
12
|
+
export type ShapeProps = {
|
|
13
|
+
color?: Color;
|
|
14
|
+
lineWidth?: number;
|
|
15
|
+
lineCap?: 'butt' | 'round' | 'square';
|
|
16
|
+
lineJoin?: 'bevel' | 'miter' | 'round';
|
|
17
|
+
miterLimit?: number;
|
|
18
|
+
};
|
|
19
|
+
export type DrawProps = ShapeProps & {
|
|
20
|
+
quad?: Quad;
|
|
21
|
+
r?: number;
|
|
22
|
+
scale?: number | Vector2;
|
|
23
|
+
origin?: number | Vector2;
|
|
24
|
+
};
|
|
25
|
+
export type PrintProps = {
|
|
26
|
+
font?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
align?: 'left' | 'center' | 'right';
|
|
29
|
+
};
|
|
30
|
+
export declare class ImageHandle {
|
|
31
|
+
readonly path: string;
|
|
32
|
+
private element;
|
|
33
|
+
private loadPromise;
|
|
34
|
+
private isLoaded;
|
|
35
|
+
constructor(path: string);
|
|
36
|
+
isReady(): boolean;
|
|
37
|
+
ready(): Promise<void>;
|
|
38
|
+
get size(): Vector2;
|
|
39
|
+
getElement(): HTMLImageElement | null;
|
|
40
|
+
}
|
|
41
|
+
export declare class Graphics {
|
|
42
|
+
private ctx;
|
|
43
|
+
private screenCtx;
|
|
44
|
+
private canvases;
|
|
45
|
+
private backgroundColor;
|
|
46
|
+
private images;
|
|
47
|
+
private defaultFont;
|
|
48
|
+
setContext(ctx: CanvasRenderingContext2D | null): void;
|
|
49
|
+
private applyColor;
|
|
50
|
+
private setStrokeProps;
|
|
51
|
+
clear(): void;
|
|
52
|
+
setBackgroundColor(color: Color): void;
|
|
53
|
+
rectangle(mode: DrawMode, color: Color, rect: Rect, props?: ShapeProps): void;
|
|
54
|
+
circle(mode: DrawMode, color: Color, position: Vector2, radii: number | Vector2, props?: ShapeProps & {
|
|
55
|
+
angle?: number;
|
|
56
|
+
arc?: [number, number];
|
|
57
|
+
}): void;
|
|
58
|
+
line(color: Color, points: Vector2[], props?: ShapeProps): void;
|
|
59
|
+
print(color: Color, text: string, position: Vector2, props?: PrintProps): void;
|
|
60
|
+
private wrapText;
|
|
61
|
+
private getFontHeight;
|
|
62
|
+
setFont(size: number, font?: string): void;
|
|
63
|
+
getFont(): string;
|
|
64
|
+
newImage(path: string): ImageHandle;
|
|
65
|
+
draw(handle: ImageHandle, position: Vector2, props?: DrawProps): void;
|
|
66
|
+
push(): void;
|
|
67
|
+
pop(): void;
|
|
68
|
+
translate(delta: Vector2): void;
|
|
69
|
+
rotate(angle: number): void;
|
|
70
|
+
scale(s: number | Vector2): void;
|
|
71
|
+
getCanvasSize(): Vector2;
|
|
72
|
+
newCanvas(size: Vector2): Canvas;
|
|
73
|
+
setCanvas(canvas?: Canvas | null): void;
|
|
74
|
+
clip(rect?: Rect): void;
|
|
75
|
+
polygon(mode: DrawMode, color: Color, points: Vector2[], props?: ShapeProps): void;
|
|
76
|
+
arc(mode: DrawMode, x: number, y: number, radius: number, angle1: number, angle2: number, props?: ShapeProps): void;
|
|
77
|
+
points(color: Color, points: Vector2[]): void;
|
|
78
|
+
}
|
|
79
|
+
export declare const graphics: Graphics;
|
|
80
|
+
//# sourceMappingURL=graphics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphics.d.ts","sourceRoot":"","sources":["../src/graphics.ts"],"names":[],"mappings":"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"}
|
package/dist/graphics.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
export class ImageHandle {
|
|
2
|
+
constructor(path) {
|
|
3
|
+
Object.defineProperty(this, "path", {
|
|
4
|
+
enumerable: true,
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true,
|
|
7
|
+
value: void 0
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(this, "element", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: null
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "loadPromise", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: void 0
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(this, "isLoaded", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: false
|
|
26
|
+
});
|
|
27
|
+
this.path = path;
|
|
28
|
+
this.loadPromise = new Promise((resolve, reject) => {
|
|
29
|
+
const img = new Image();
|
|
30
|
+
img.onload = () => {
|
|
31
|
+
this.element = img;
|
|
32
|
+
this.isLoaded = true;
|
|
33
|
+
resolve();
|
|
34
|
+
};
|
|
35
|
+
img.onerror = () => {
|
|
36
|
+
reject(new Error(`Failed to load image: ${path}`));
|
|
37
|
+
};
|
|
38
|
+
img.src = path;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
isReady() {
|
|
42
|
+
return this.isLoaded;
|
|
43
|
+
}
|
|
44
|
+
ready() {
|
|
45
|
+
return this.loadPromise;
|
|
46
|
+
}
|
|
47
|
+
get size() {
|
|
48
|
+
return [this.element?.width ?? 0, this.element?.height ?? 0];
|
|
49
|
+
}
|
|
50
|
+
getElement() {
|
|
51
|
+
return this.element;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function parseColor(color) {
|
|
55
|
+
if (typeof color === 'string') {
|
|
56
|
+
return color;
|
|
57
|
+
}
|
|
58
|
+
const [r, g, b, a = 1] = color;
|
|
59
|
+
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`;
|
|
60
|
+
}
|
|
61
|
+
export class Graphics {
|
|
62
|
+
constructor() {
|
|
63
|
+
Object.defineProperty(this, "ctx", {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
configurable: true,
|
|
66
|
+
writable: true,
|
|
67
|
+
value: null
|
|
68
|
+
});
|
|
69
|
+
Object.defineProperty(this, "screenCtx", {
|
|
70
|
+
enumerable: true,
|
|
71
|
+
configurable: true,
|
|
72
|
+
writable: true,
|
|
73
|
+
value: null
|
|
74
|
+
});
|
|
75
|
+
Object.defineProperty(this, "canvases", {
|
|
76
|
+
enumerable: true,
|
|
77
|
+
configurable: true,
|
|
78
|
+
writable: true,
|
|
79
|
+
value: new Map()
|
|
80
|
+
});
|
|
81
|
+
Object.defineProperty(this, "backgroundColor", {
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true,
|
|
84
|
+
writable: true,
|
|
85
|
+
value: [0, 0, 0, 1]
|
|
86
|
+
});
|
|
87
|
+
Object.defineProperty(this, "images", {
|
|
88
|
+
enumerable: true,
|
|
89
|
+
configurable: true,
|
|
90
|
+
writable: true,
|
|
91
|
+
value: new Map()
|
|
92
|
+
});
|
|
93
|
+
Object.defineProperty(this, "defaultFont", {
|
|
94
|
+
enumerable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
writable: true,
|
|
97
|
+
value: '16px sans-serif'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
setContext(ctx) {
|
|
101
|
+
this.screenCtx = ctx;
|
|
102
|
+
this.ctx = ctx;
|
|
103
|
+
if (ctx) {
|
|
104
|
+
ctx.font = this.defaultFont;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
applyColor(color) {
|
|
108
|
+
return parseColor(color ?? [1, 1, 1, 1]);
|
|
109
|
+
}
|
|
110
|
+
setStrokeProps(props) {
|
|
111
|
+
if (!this.ctx)
|
|
112
|
+
return;
|
|
113
|
+
// Always reset to defaults first, then apply any custom props
|
|
114
|
+
this.ctx.lineWidth = props?.lineWidth ?? 1;
|
|
115
|
+
this.ctx.lineCap = props?.lineCap ?? 'butt';
|
|
116
|
+
this.ctx.lineJoin = props?.lineJoin ?? 'miter';
|
|
117
|
+
this.ctx.miterLimit = props?.miterLimit ?? 10;
|
|
118
|
+
}
|
|
119
|
+
clear() {
|
|
120
|
+
if (!this.ctx)
|
|
121
|
+
return;
|
|
122
|
+
this.ctx.fillStyle = parseColor(this.backgroundColor);
|
|
123
|
+
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
124
|
+
}
|
|
125
|
+
setBackgroundColor(color) {
|
|
126
|
+
this.backgroundColor = color;
|
|
127
|
+
this.clear();
|
|
128
|
+
}
|
|
129
|
+
rectangle(mode, color, rect, props) {
|
|
130
|
+
if (!this.ctx)
|
|
131
|
+
return;
|
|
132
|
+
const [x, y, width, height] = rect;
|
|
133
|
+
const parsedColor = this.applyColor(color);
|
|
134
|
+
if (mode === 'fill') {
|
|
135
|
+
this.ctx.fillStyle = parsedColor;
|
|
136
|
+
this.ctx.fillRect(x, y, width, height);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.setStrokeProps(props);
|
|
140
|
+
this.ctx.strokeStyle = parsedColor;
|
|
141
|
+
this.ctx.strokeRect(x, y, width, height);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
circle(mode, color, position, radii, props) {
|
|
145
|
+
if (!this.ctx)
|
|
146
|
+
return;
|
|
147
|
+
const [x, y] = position;
|
|
148
|
+
const parsedColor = this.applyColor(color);
|
|
149
|
+
const [rx, ry] = typeof radii === 'number' ? [radii, radii] : radii;
|
|
150
|
+
const [startAngle, endAngle] = props?.arc ?? [0, Math.PI * 2];
|
|
151
|
+
const rotation = props?.angle ?? 0;
|
|
152
|
+
this.ctx.save();
|
|
153
|
+
this.ctx.translate(x, y);
|
|
154
|
+
this.ctx.rotate(rotation);
|
|
155
|
+
this.ctx.scale(rx, ry);
|
|
156
|
+
this.ctx.beginPath();
|
|
157
|
+
this.ctx.arc(0, 0, 1, startAngle, endAngle);
|
|
158
|
+
this.ctx.closePath();
|
|
159
|
+
this.ctx.restore();
|
|
160
|
+
if (mode === 'fill') {
|
|
161
|
+
this.ctx.fillStyle = parsedColor;
|
|
162
|
+
this.ctx.fill();
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this.setStrokeProps(props);
|
|
166
|
+
this.ctx.strokeStyle = parsedColor;
|
|
167
|
+
this.ctx.stroke();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
line(color, points, props) {
|
|
171
|
+
if (!this.ctx || points.length < 2)
|
|
172
|
+
return;
|
|
173
|
+
const parsedColor = this.applyColor(color);
|
|
174
|
+
this.setStrokeProps(props);
|
|
175
|
+
this.ctx.beginPath();
|
|
176
|
+
const [[x0, y0], ...rest] = points;
|
|
177
|
+
this.ctx.moveTo(x0, y0);
|
|
178
|
+
rest.forEach(([x, y]) => this.ctx.lineTo(x, y));
|
|
179
|
+
this.ctx.strokeStyle = parsedColor;
|
|
180
|
+
this.ctx.stroke();
|
|
181
|
+
}
|
|
182
|
+
print(color, text, position, props) {
|
|
183
|
+
if (!this.ctx)
|
|
184
|
+
return;
|
|
185
|
+
const [x, y] = position;
|
|
186
|
+
const { font = this.defaultFont, limit, align = 'left' } = props ?? {};
|
|
187
|
+
this.ctx.fillStyle = parseColor(color);
|
|
188
|
+
this.ctx.font = font;
|
|
189
|
+
if (limit !== undefined) {
|
|
190
|
+
const lines = this.wrapText(text, limit);
|
|
191
|
+
const lineHeight = this.getFontHeight();
|
|
192
|
+
lines.forEach((line, index) => {
|
|
193
|
+
const lineWidth = this.ctx.measureText(line).width;
|
|
194
|
+
const drawX = align === 'center' ? x + (limit - lineWidth) / 2 :
|
|
195
|
+
align === 'right' ? x + limit - lineWidth : x;
|
|
196
|
+
this.ctx.fillText(line, drawX, y + index * lineHeight);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.ctx.fillText(text, x, y);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
wrapText(text, maxWidth) {
|
|
204
|
+
if (!this.ctx)
|
|
205
|
+
return [text];
|
|
206
|
+
const words = text.split(' ');
|
|
207
|
+
const [first, ...rest] = words;
|
|
208
|
+
const lines = [];
|
|
209
|
+
let currentLine = first ?? '';
|
|
210
|
+
rest.forEach((word) => {
|
|
211
|
+
const width = this.ctx.measureText(currentLine + ' ' + word).width;
|
|
212
|
+
if (width < maxWidth) {
|
|
213
|
+
currentLine += ' ' + word;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
lines.push(currentLine);
|
|
217
|
+
currentLine = word;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
lines.push(currentLine);
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
223
|
+
getFontHeight() {
|
|
224
|
+
if (!this.ctx)
|
|
225
|
+
return 16;
|
|
226
|
+
const match = this.ctx.font.match(/(\d+)px/);
|
|
227
|
+
return match ? parseInt(match[1]) : 16;
|
|
228
|
+
}
|
|
229
|
+
setFont(size, font = 'sans-serif') {
|
|
230
|
+
if (!this.ctx)
|
|
231
|
+
return;
|
|
232
|
+
this.defaultFont = `${size}px ${font}`;
|
|
233
|
+
this.ctx.font = this.defaultFont;
|
|
234
|
+
}
|
|
235
|
+
getFont() {
|
|
236
|
+
return this.defaultFont;
|
|
237
|
+
}
|
|
238
|
+
newImage(path) {
|
|
239
|
+
let handle = this.images.get(path);
|
|
240
|
+
if (!handle) {
|
|
241
|
+
handle = new ImageHandle(path);
|
|
242
|
+
this.images.set(path, handle);
|
|
243
|
+
}
|
|
244
|
+
return handle;
|
|
245
|
+
}
|
|
246
|
+
draw(handle, position, props) {
|
|
247
|
+
if (!this.ctx)
|
|
248
|
+
return;
|
|
249
|
+
const imageHandle = this.images.get(handle.path);
|
|
250
|
+
if (!imageHandle?.isReady())
|
|
251
|
+
return;
|
|
252
|
+
const element = imageHandle.getElement();
|
|
253
|
+
if (!element)
|
|
254
|
+
return;
|
|
255
|
+
const [x, y] = position;
|
|
256
|
+
const { r = 0, scale = 1, origin = 0, quad } = props ?? {};
|
|
257
|
+
const [sx, sy] = typeof scale === 'number' ? [scale, scale] : scale;
|
|
258
|
+
const [ox, oy] = typeof origin === 'number' ? [origin, origin] : origin;
|
|
259
|
+
this.ctx.save();
|
|
260
|
+
this.ctx.translate(x, y);
|
|
261
|
+
this.ctx.rotate(r);
|
|
262
|
+
this.ctx.scale(sx, sy);
|
|
263
|
+
if (quad) {
|
|
264
|
+
const [qx, qy, qw, qh] = quad;
|
|
265
|
+
this.ctx.drawImage(element, qx, qy, qw, qh, -ox, -oy, qw, qh);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
this.ctx.drawImage(element, -ox, -oy);
|
|
269
|
+
}
|
|
270
|
+
this.ctx.restore();
|
|
271
|
+
}
|
|
272
|
+
push() {
|
|
273
|
+
if (!this.ctx)
|
|
274
|
+
return;
|
|
275
|
+
this.ctx.save();
|
|
276
|
+
}
|
|
277
|
+
pop() {
|
|
278
|
+
if (!this.ctx)
|
|
279
|
+
return;
|
|
280
|
+
this.ctx.restore();
|
|
281
|
+
}
|
|
282
|
+
translate(delta) {
|
|
283
|
+
if (!this.ctx)
|
|
284
|
+
return;
|
|
285
|
+
const [x, y] = delta;
|
|
286
|
+
this.ctx.translate(x, y);
|
|
287
|
+
}
|
|
288
|
+
rotate(angle) {
|
|
289
|
+
if (!this.ctx)
|
|
290
|
+
return;
|
|
291
|
+
this.ctx.rotate(angle);
|
|
292
|
+
}
|
|
293
|
+
scale(s) {
|
|
294
|
+
if (!this.ctx)
|
|
295
|
+
return;
|
|
296
|
+
if (typeof s === 'number') {
|
|
297
|
+
this.ctx.scale(s, s);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const [sx, sy] = s;
|
|
301
|
+
this.ctx.scale(sx, sy);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
getCanvasSize() {
|
|
305
|
+
const width = this.ctx?.canvas.width ?? 800;
|
|
306
|
+
const height = this.ctx?.canvas.height ?? 600;
|
|
307
|
+
return [width, height];
|
|
308
|
+
}
|
|
309
|
+
newCanvas(size) {
|
|
310
|
+
const [width, height] = size;
|
|
311
|
+
const element = document.createElement('canvas');
|
|
312
|
+
element.width = width;
|
|
313
|
+
element.height = height;
|
|
314
|
+
const ctx = element.getContext('2d');
|
|
315
|
+
if (!ctx) {
|
|
316
|
+
throw new Error('Failed to create canvas context');
|
|
317
|
+
}
|
|
318
|
+
const canvas = { size, element, ctx };
|
|
319
|
+
this.canvases.set(canvas, true);
|
|
320
|
+
return canvas;
|
|
321
|
+
}
|
|
322
|
+
setCanvas(canvas) {
|
|
323
|
+
if (canvas) {
|
|
324
|
+
this.ctx = canvas.ctx;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
this.ctx = this.screenCtx;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
clip(rect) {
|
|
331
|
+
if (!this.ctx)
|
|
332
|
+
return;
|
|
333
|
+
if (rect) {
|
|
334
|
+
const [x, y, w, h] = rect;
|
|
335
|
+
this.ctx.beginPath();
|
|
336
|
+
this.ctx.rect(x, y, w, h);
|
|
337
|
+
this.ctx.clip();
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
this.ctx.beginPath();
|
|
341
|
+
this.ctx.rect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
342
|
+
this.ctx.clip();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
polygon(mode, color, points, props) {
|
|
346
|
+
if (!this.ctx || points.length < 3)
|
|
347
|
+
return;
|
|
348
|
+
const parsedColor = this.applyColor(color);
|
|
349
|
+
this.ctx.beginPath();
|
|
350
|
+
const [[x0, y0], ...rest] = points;
|
|
351
|
+
this.ctx.moveTo(x0, y0);
|
|
352
|
+
rest.forEach(([x, y]) => this.ctx.lineTo(x, y));
|
|
353
|
+
this.ctx.closePath();
|
|
354
|
+
if (mode === 'fill') {
|
|
355
|
+
this.ctx.fillStyle = parsedColor;
|
|
356
|
+
this.ctx.fill();
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
this.setStrokeProps(props);
|
|
360
|
+
this.ctx.strokeStyle = parsedColor;
|
|
361
|
+
this.ctx.stroke();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
arc(mode, x, y, radius, angle1, angle2, props) {
|
|
365
|
+
if (!this.ctx)
|
|
366
|
+
return;
|
|
367
|
+
const color = this.applyColor(props?.color);
|
|
368
|
+
this.ctx.beginPath();
|
|
369
|
+
this.ctx.arc(x, y, radius, angle1, angle2);
|
|
370
|
+
if (mode === 'fill') {
|
|
371
|
+
this.ctx.fillStyle = color;
|
|
372
|
+
this.ctx.fill();
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
this.setStrokeProps(props);
|
|
376
|
+
this.ctx.strokeStyle = color;
|
|
377
|
+
this.ctx.stroke();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
points(color, points) {
|
|
381
|
+
if (!this.ctx)
|
|
382
|
+
return;
|
|
383
|
+
const parsedColor = this.applyColor(color);
|
|
384
|
+
this.ctx.fillStyle = parsedColor;
|
|
385
|
+
points.forEach(([x, y]) => this.ctx.fillRect(x, y, 1, 1));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
export const graphics = new Graphics();
|