canvasengine 2.0.0-beta.4 → 2.0.0-beta.40
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/DebugRenderer-DgECR3yZ.js +172 -0
- package/dist/DebugRenderer-DgECR3yZ.js.map +1 -0
- package/dist/components/Button.d.ts +183 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Canvas.d.ts +18 -0
- package/dist/components/Canvas.d.ts.map +1 -0
- package/dist/components/DOMElement.d.ts +44 -0
- package/dist/components/DOMElement.d.ts.map +1 -0
- package/dist/components/Graphic.d.ts +65 -0
- package/dist/components/Graphic.d.ts.map +1 -0
- package/dist/components/Joystick.d.ts +36 -0
- package/dist/components/Joystick.d.ts.map +1 -0
- package/dist/components/NineSliceSprite.d.ts +17 -0
- package/dist/components/NineSliceSprite.d.ts.map +1 -0
- package/dist/components/ParticleEmitter.d.ts +5 -0
- package/dist/components/ParticleEmitter.d.ts.map +1 -0
- package/dist/components/Scene.d.ts +2 -0
- package/dist/components/Scene.d.ts.map +1 -0
- package/dist/components/Text.d.ts +26 -0
- package/dist/components/Text.d.ts.map +1 -0
- package/dist/components/TilingSprite.d.ts +18 -0
- package/dist/components/TilingSprite.d.ts.map +1 -0
- package/dist/components/Video.d.ts +15 -0
- package/dist/components/Video.d.ts.map +1 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/types/DisplayObject.d.ts +110 -0
- package/dist/components/types/DisplayObject.d.ts.map +1 -0
- package/dist/components/types/MouseEvent.d.ts +4 -0
- package/dist/components/types/MouseEvent.d.ts.map +1 -0
- package/dist/components/types/Spritesheet.d.ts +248 -0
- package/dist/components/types/Spritesheet.d.ts.map +1 -0
- package/dist/components/types/index.d.ts +5 -0
- package/dist/components/types/index.d.ts.map +1 -0
- package/dist/directives/Controls.d.ts +113 -0
- package/dist/directives/Controls.d.ts.map +1 -0
- package/dist/directives/ControlsBase.d.ts +198 -0
- package/dist/directives/ControlsBase.d.ts.map +1 -0
- package/dist/directives/Drag.d.ts +70 -0
- package/dist/directives/Drag.d.ts.map +1 -0
- package/dist/directives/Flash.d.ts +117 -0
- package/dist/directives/Flash.d.ts.map +1 -0
- package/dist/directives/GamepadControls.d.ts +225 -0
- package/dist/directives/GamepadControls.d.ts.map +1 -0
- package/dist/directives/JoystickControls.d.ts +172 -0
- package/dist/directives/JoystickControls.d.ts.map +1 -0
- package/dist/directives/KeyboardControls.d.ts +219 -0
- package/dist/directives/KeyboardControls.d.ts.map +1 -0
- package/dist/directives/Scheduler.d.ts +36 -0
- package/dist/directives/Scheduler.d.ts.map +1 -0
- package/dist/directives/Shake.d.ts +98 -0
- package/dist/directives/Shake.d.ts.map +1 -0
- package/dist/directives/Sound.d.ts +26 -0
- package/dist/directives/Sound.d.ts.map +1 -0
- package/dist/directives/Transition.d.ts +11 -0
- package/dist/directives/Transition.d.ts.map +1 -0
- package/dist/directives/ViewportCull.d.ts +12 -0
- package/dist/directives/ViewportCull.d.ts.map +1 -0
- package/dist/directives/ViewportFollow.d.ts +19 -0
- package/dist/directives/ViewportFollow.d.ts.map +1 -0
- package/dist/directives/index.d.ts +13 -0
- package/dist/directives/index.d.ts.map +1 -0
- package/dist/engine/animation.d.ts +73 -0
- package/dist/engine/animation.d.ts.map +1 -0
- package/dist/engine/bootstrap.d.ts +16 -0
- package/dist/engine/bootstrap.d.ts.map +1 -0
- package/dist/engine/directive.d.ts +14 -0
- package/dist/engine/directive.d.ts.map +1 -0
- package/dist/engine/reactive.d.ts +105 -0
- package/dist/engine/reactive.d.ts.map +1 -0
- package/dist/engine/signal.d.ts +72 -0
- package/dist/engine/signal.d.ts.map +1 -0
- package/dist/engine/trigger.d.ts +50 -0
- package/dist/engine/trigger.d.ts.map +1 -0
- package/dist/engine/utils.d.ts +90 -0
- package/dist/engine/utils.d.ts.map +1 -0
- package/dist/hooks/addContext.d.ts +2 -0
- package/dist/hooks/addContext.d.ts.map +1 -0
- package/dist/hooks/useProps.d.ts +42 -0
- package/dist/hooks/useProps.d.ts.map +1 -0
- package/dist/hooks/useRef.d.ts +5 -0
- package/dist/hooks/useRef.d.ts.map +1 -0
- package/dist/index-gb763Hyx.js +12560 -0
- package/dist/index-gb763Hyx.js.map +1 -0
- package/dist/index.d.ts +15 -1083
- package/dist/index.d.ts.map +1 -0
- package/dist/index.global.js +29 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +81 -3041
- package/dist/index.js.map +1 -1
- package/dist/utils/Ease.d.ts +17 -0
- package/dist/utils/Ease.d.ts.map +1 -0
- package/dist/utils/GlobalAssetLoader.d.ts +141 -0
- package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
- package/dist/utils/RadialGradient.d.ts +58 -0
- package/dist/utils/RadialGradient.d.ts.map +1 -0
- package/dist/utils/functions.d.ts +2 -0
- package/dist/utils/functions.d.ts.map +1 -0
- package/package.json +13 -7
- package/src/components/Button.ts +396 -0
- package/src/components/Canvas.ts +61 -45
- package/src/components/Container.ts +21 -2
- package/src/components/DOMContainer.ts +123 -0
- package/src/components/DOMElement.ts +421 -0
- package/src/components/DisplayObject.ts +350 -197
- package/src/components/Graphic.ts +200 -34
- package/src/components/Joystick.ts +361 -0
- package/src/components/Mesh.ts +222 -0
- package/src/components/NineSliceSprite.ts +4 -1
- package/src/components/ParticleEmitter.ts +12 -8
- package/src/components/Sprite.ts +306 -30
- package/src/components/Text.ts +125 -18
- package/src/components/Video.ts +110 -0
- package/src/components/Viewport.ts +59 -43
- package/src/components/index.ts +8 -2
- package/src/components/types/DisplayObject.ts +34 -0
- package/src/components/types/Spritesheet.ts +0 -118
- package/src/directives/Controls.ts +254 -0
- package/src/directives/ControlsBase.ts +266 -0
- package/src/directives/Drag.ts +357 -52
- package/src/directives/Flash.ts +409 -0
- package/src/directives/GamepadControls.ts +537 -0
- package/src/directives/JoystickControls.ts +396 -0
- package/src/directives/KeyboardControls.ts +66 -424
- package/src/directives/Shake.ts +282 -0
- package/src/directives/Sound.ts +94 -31
- package/src/directives/ViewportFollow.ts +35 -7
- package/src/directives/index.ts +12 -6
- package/src/engine/animation.ts +175 -21
- package/src/engine/bootstrap.ts +23 -3
- package/src/engine/directive.ts +2 -2
- package/src/engine/reactive.ts +780 -177
- package/src/engine/signal.ts +35 -4
- package/src/engine/trigger.ts +21 -4
- package/src/engine/utils.ts +19 -3
- package/src/hooks/useProps.ts +1 -1
- package/src/index.ts +4 -2
- package/src/utils/GlobalAssetLoader.ts +257 -0
- package/src/utils/functions.ts +7 -0
- package/testing/index.ts +12 -0
- package/tsconfig.json +17 -0
- package/vite.config.ts +39 -0
|
@@ -1,54 +1,167 @@
|
|
|
1
|
-
import { effect, Signal } from "@signe/reactive";
|
|
2
|
-
import { Graphics as PixiGraphics } from "pixi.js";
|
|
3
|
-
import { createComponent, registerComponent } from "../engine/reactive";
|
|
4
|
-
import { DisplayObject } from "./DisplayObject";
|
|
1
|
+
import { Effect, effect, isSignal, signal, Signal, WritableSignal } from "@signe/reactive";
|
|
2
|
+
import { Assets, Graphics as PixiGraphics } from "pixi.js";
|
|
3
|
+
import { createComponent, Element, registerComponent } from "../engine/reactive";
|
|
4
|
+
import { ComponentInstance, DisplayObject } from "./DisplayObject";
|
|
5
5
|
import { DisplayObjectProps } from "./types/DisplayObject";
|
|
6
6
|
import { useProps } from "../hooks/useProps";
|
|
7
|
+
import { SignalOrPrimitive } from "./types";
|
|
8
|
+
import { isPercent } from "../utils/functions";
|
|
7
9
|
|
|
8
10
|
interface GraphicsProps extends DisplayObjectProps {
|
|
9
|
-
draw?: (graphics: PixiGraphics) => void;
|
|
11
|
+
draw?: (graphics: PixiGraphics, width: number, height: number) => void;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
interface RectProps extends DisplayObjectProps {
|
|
13
|
-
|
|
14
|
-
height: number;
|
|
15
|
-
color: string;
|
|
15
|
+
color: SignalOrPrimitive<string>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface CircleProps extends DisplayObjectProps {
|
|
19
|
-
radius: number
|
|
20
|
-
color: string
|
|
19
|
+
radius: SignalOrPrimitive<number>;
|
|
20
|
+
color: SignalOrPrimitive<string>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface EllipseProps extends DisplayObjectProps {
|
|
24
|
-
|
|
25
|
-
height: number;
|
|
26
|
-
color: string;
|
|
24
|
+
color: SignalOrPrimitive<string>;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
interface TriangleProps extends DisplayObjectProps {
|
|
30
|
-
base: number
|
|
31
|
-
|
|
32
|
-
color: string;
|
|
28
|
+
base: SignalOrPrimitive<number>;
|
|
29
|
+
color: SignalOrPrimitive<string>;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
interface SvgProps extends DisplayObjectProps {
|
|
36
|
-
|
|
33
|
+
/** SVG content as string (legacy prop) */
|
|
34
|
+
svg?: string;
|
|
35
|
+
/** URL source of the SVG file to load */
|
|
36
|
+
src?: string;
|
|
37
|
+
/** Direct SVG content as string */
|
|
38
|
+
content?: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
class CanvasGraphics extends DisplayObject(PixiGraphics) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
clearEffect: Effect;
|
|
43
|
+
_width: WritableSignal<number>;
|
|
44
|
+
_height: WritableSignal<number>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initializes the graphics component with reactive width and height handling.
|
|
48
|
+
*
|
|
49
|
+
* This method handles different types of width and height props:
|
|
50
|
+
* - **Numbers**: Direct pixel values
|
|
51
|
+
* - **Strings with %**: Percentage values that trigger flex layout and use layout box dimensions
|
|
52
|
+
* - **Signals**: Reactive values that update automatically
|
|
53
|
+
*
|
|
54
|
+
* When percentage values are detected, the component:
|
|
55
|
+
* 1. Sets `display: 'flex'` to enable layout calculations
|
|
56
|
+
* 2. Listens to layout events to get computed dimensions
|
|
57
|
+
* 3. Updates internal width/height signals with layout box values
|
|
58
|
+
*
|
|
59
|
+
* The draw function receives the reactive width and height signals as parameters.
|
|
60
|
+
*
|
|
61
|
+
* @param props - Component properties including width, height, and draw function
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // With pixel values
|
|
65
|
+
* Graphics({ width: 100, height: 50, draw: (g, w, h) => g.rect(0, 0, w(), h()) });
|
|
66
|
+
*
|
|
67
|
+
* // With percentage values (uses layout box)
|
|
68
|
+
* Graphics({ width: "50%", height: "100%", draw: (g, w, h) => g.rect(0, 0, w(), h()) });
|
|
69
|
+
*
|
|
70
|
+
* // With signals
|
|
71
|
+
* const width = signal(100);
|
|
72
|
+
* Graphics({ width, height: 50, draw: (g, w, h) => g.rect(0, 0, w(), h()) });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
async onInit(props) {
|
|
76
|
+
await super.onInit(props);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Called when the component is mounted to the scene graph.
|
|
81
|
+
* Creates the reactive effect for drawing using the original signals from propObservables.
|
|
82
|
+
* @param {Element<DisplayObject>} element - The element being mounted with props and propObservables.
|
|
83
|
+
* @param {number} [index] - The index of the component among its siblings.
|
|
84
|
+
*/
|
|
85
|
+
async onMount(element: Element<any>, index?: number): Promise<void> {
|
|
86
|
+
await super.onMount(element, index);
|
|
87
|
+
const { props, propObservables } = element;
|
|
88
|
+
|
|
89
|
+
// Use original signals from propObservables if available, otherwise create new ones
|
|
90
|
+
const width = (isSignal(propObservables?.width) ? propObservables.width : signal(props.width || 0)) as WritableSignal<number>;
|
|
91
|
+
const height = (isSignal(propObservables?.height) ? propObservables.height : signal(props.height || 0)) as WritableSignal<number>;
|
|
92
|
+
|
|
93
|
+
// Store as class properties for access in other methods
|
|
94
|
+
this._width = width;
|
|
95
|
+
this._height = height;
|
|
96
|
+
|
|
97
|
+
// Check if width or height are percentages to set display flex
|
|
98
|
+
const isWidthPercentage = isPercent(width());
|
|
99
|
+
const isHeightPercentage = isPercent(height());
|
|
100
|
+
|
|
42
101
|
if (props.draw) {
|
|
43
|
-
effect(() => {
|
|
102
|
+
this.clearEffect = effect(() => {
|
|
103
|
+
const w = width();
|
|
104
|
+
const h = height();
|
|
105
|
+
if (typeof w == 'string' || typeof h == 'string') {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
44
108
|
this.clear();
|
|
45
|
-
props.draw?.(this);
|
|
109
|
+
props.draw?.(this, w, h);
|
|
110
|
+
this.subjectInit.next(this)
|
|
46
111
|
});
|
|
47
112
|
}
|
|
113
|
+
|
|
114
|
+
this.on('layout', (event) => {
|
|
115
|
+
const layoutBox = event.computedLayout;
|
|
116
|
+
// Update width if it's a percentage and value has changed
|
|
117
|
+
if (isWidthPercentage && isSignal(width) && width() !== layoutBox.width) {
|
|
118
|
+
width.set(layoutBox.width);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Update height if it's a percentage and value has changed
|
|
122
|
+
if (isHeightPercentage && isSignal(height) && height() !== layoutBox.height) {
|
|
123
|
+
height.set(layoutBox.height);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
48
126
|
}
|
|
49
|
-
}
|
|
50
127
|
|
|
51
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Called when component props are updated.
|
|
130
|
+
* Updates the internal width and height signals when props change.
|
|
131
|
+
* @param props - Updated properties
|
|
132
|
+
*/
|
|
133
|
+
onUpdate(props: any) {
|
|
134
|
+
super.onUpdate(props);
|
|
135
|
+
|
|
136
|
+
// Update width signal if width prop changed and value is different
|
|
137
|
+
if (props.width !== undefined && this._width && this._width() !== props.width) {
|
|
138
|
+
this._width.set(props.width);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Update height signal if height prop changed and value is different
|
|
142
|
+
if (props.height !== undefined && this._height && this._height() !== props.height) {
|
|
143
|
+
this._height.set(props.height);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Called when the component is about to be destroyed.
|
|
149
|
+
* This method should be overridden by subclasses to perform any cleanup.
|
|
150
|
+
* It ensures that the clearEffect subscription is unsubscribed before calling the original afterDestroy callback.
|
|
151
|
+
* @param parent The parent element.
|
|
152
|
+
* @param afterDestroy A callback function to be executed after the component's own destruction logic.
|
|
153
|
+
* @example
|
|
154
|
+
* // This method is typically called by the engine internally.
|
|
155
|
+
* // await component.onDestroy(parentElement, () => console.log('Component destroyed'));
|
|
156
|
+
*/
|
|
157
|
+
async onDestroy(parent: Element<ComponentInstance>, afterDestroy: () => void): Promise<void> {
|
|
158
|
+
const _afterDestroyCallback = async () => {
|
|
159
|
+
this.clearEffect?.subscription.unsubscribe();
|
|
160
|
+
afterDestroy();
|
|
161
|
+
}
|
|
162
|
+
await super.onDestroy(parent, _afterDestroyCallback);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
52
165
|
|
|
53
166
|
registerComponent("Graphics", CanvasGraphics);
|
|
54
167
|
|
|
@@ -57,16 +170,17 @@ export function Graphics(props: GraphicsProps) {
|
|
|
57
170
|
}
|
|
58
171
|
|
|
59
172
|
export function Rect(props: RectProps) {
|
|
60
|
-
const {
|
|
173
|
+
const { color, borderRadius, border } = useProps(props, {
|
|
61
174
|
borderRadius: null,
|
|
62
175
|
border: null
|
|
63
176
|
})
|
|
177
|
+
|
|
64
178
|
return Graphics({
|
|
65
|
-
draw: (g) => {
|
|
179
|
+
draw: (g, width, height) => {
|
|
66
180
|
if (borderRadius()) {
|
|
67
|
-
g.roundRect(0, 0, width
|
|
181
|
+
g.roundRect(0, 0, width, height, borderRadius());
|
|
68
182
|
} else {
|
|
69
|
-
g.rect(0, 0, width
|
|
183
|
+
g.rect(0, 0, width, height);
|
|
70
184
|
}
|
|
71
185
|
if (border) {
|
|
72
186
|
g.stroke(border);
|
|
@@ -114,7 +228,13 @@ export function Ellipse(props: EllipseProps) {
|
|
|
114
228
|
border: null
|
|
115
229
|
})
|
|
116
230
|
return Graphics({
|
|
117
|
-
draw: (g
|
|
231
|
+
draw: (g, gWidth, gHeight) => {
|
|
232
|
+
g.ellipse(0, 0, gWidth / 2, gHeight / 2);
|
|
233
|
+
if (border()) {
|
|
234
|
+
g.stroke(border());
|
|
235
|
+
}
|
|
236
|
+
g.fill(color());
|
|
237
|
+
},
|
|
118
238
|
...props
|
|
119
239
|
})
|
|
120
240
|
}
|
|
@@ -125,11 +245,11 @@ export function Triangle(props: TriangleProps) {
|
|
|
125
245
|
color: '#000'
|
|
126
246
|
})
|
|
127
247
|
return Graphics({
|
|
128
|
-
draw: (g) => {
|
|
129
|
-
g.moveTo(0,
|
|
130
|
-
g.lineTo(
|
|
131
|
-
g.lineTo(
|
|
132
|
-
g.lineTo(0,
|
|
248
|
+
draw: (g, gWidth, gHeight) => {
|
|
249
|
+
g.moveTo(0, gHeight);
|
|
250
|
+
g.lineTo(gWidth / 2, 0);
|
|
251
|
+
g.lineTo(gWidth, gHeight);
|
|
252
|
+
g.lineTo(0, gHeight);
|
|
133
253
|
g.fill(color());
|
|
134
254
|
if (border) {
|
|
135
255
|
g.stroke(border);
|
|
@@ -139,9 +259,55 @@ export function Triangle(props: TriangleProps) {
|
|
|
139
259
|
})
|
|
140
260
|
}
|
|
141
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Creates an SVG component that can render SVG graphics from URL, content, or legacy svg prop.
|
|
264
|
+
*
|
|
265
|
+
* This component provides three ways to display SVG graphics:
|
|
266
|
+
* - **src**: Load SVG from a URL using Assets.load with parseAsGraphicsContext option
|
|
267
|
+
* - **content**: Render SVG directly from string content using Graphics.svg() method
|
|
268
|
+
* - **svg**: Legacy prop for SVG content (for backward compatibility)
|
|
269
|
+
*
|
|
270
|
+
* @param props - Component properties including src, content, or svg
|
|
271
|
+
* @returns A reactive SVG component
|
|
272
|
+
* @example
|
|
273
|
+
* ```typescript
|
|
274
|
+
* // Load from URL
|
|
275
|
+
* const svgFromUrl = Svg({ src: "/assets/logo.svg" });
|
|
276
|
+
*
|
|
277
|
+
* // Direct content
|
|
278
|
+
* const svgFromContent = Svg({
|
|
279
|
+
* content: `<svg viewBox="0 0 100 100">
|
|
280
|
+
* <circle cx="50" cy="50" r="40" fill="blue"/>
|
|
281
|
+
* </svg>`
|
|
282
|
+
* });
|
|
283
|
+
*
|
|
284
|
+
* // Legacy usage
|
|
285
|
+
* const svgLegacy = Svg({ svg: "<svg>...</svg>" });
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
142
288
|
export function Svg(props: SvgProps) {
|
|
143
289
|
return Graphics({
|
|
144
|
-
draw: (g) =>
|
|
290
|
+
draw: async (g) => {
|
|
291
|
+
if (props.src) {
|
|
292
|
+
// Load SVG from source URL with graphics context parsing
|
|
293
|
+
const svgData = await Assets.load({
|
|
294
|
+
src: props.src,
|
|
295
|
+
data: {
|
|
296
|
+
parseAsGraphicsContext: true,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Apply the loaded graphics context
|
|
301
|
+
const graphics = new PixiGraphics(svgData);
|
|
302
|
+
g.context = graphics.context;
|
|
303
|
+
} else if (props.content) {
|
|
304
|
+
// Render SVG directly from content string
|
|
305
|
+
g.svg(props.content);
|
|
306
|
+
} else if (props.svg) {
|
|
307
|
+
// Legacy prop support
|
|
308
|
+
g.svg(props.svg);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
145
311
|
...props
|
|
146
312
|
})
|
|
147
313
|
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Joystick
|
|
3
|
+
*
|
|
4
|
+
* Inspired by https://github.com/endel/pixi-virtual-joystick
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as PIXI from "pixi.js";
|
|
8
|
+
import { Container, Graphics, Sprite, h, signal, isSignal } from "../";
|
|
9
|
+
|
|
10
|
+
export interface JoystickChangeEvent {
|
|
11
|
+
angle: number;
|
|
12
|
+
direction: Direction;
|
|
13
|
+
power: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export enum Direction {
|
|
17
|
+
LEFT = "left",
|
|
18
|
+
TOP = "top",
|
|
19
|
+
BOTTOM = "bottom",
|
|
20
|
+
RIGHT = "right",
|
|
21
|
+
TOP_LEFT = "top_left",
|
|
22
|
+
TOP_RIGHT = "top_right",
|
|
23
|
+
BOTTOM_LEFT = "bottom_left",
|
|
24
|
+
BOTTOM_RIGHT = "bottom_right",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface JoystickSettings {
|
|
28
|
+
outer?: string;
|
|
29
|
+
inner?: string;
|
|
30
|
+
outerScale?: { x: number; y: number };
|
|
31
|
+
innerScale?: { x: number; y: number };
|
|
32
|
+
innerColor?: string;
|
|
33
|
+
outerColor?: string;
|
|
34
|
+
onChange?: (data: JoystickChangeEvent) => void;
|
|
35
|
+
onStart?: () => void;
|
|
36
|
+
onEnd?: () => void;
|
|
37
|
+
/** Controls instance to automatically apply joystick events to (e.g., JoystickControls or ControlsDirective) */
|
|
38
|
+
controls?: any;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Joystick(opts: JoystickSettings = {}) {
|
|
42
|
+
const settings = Object.assign(
|
|
43
|
+
{
|
|
44
|
+
outerScale: { x: 1, y: 1 },
|
|
45
|
+
innerScale: { x: 1, y: 1 },
|
|
46
|
+
innerColor: "black",
|
|
47
|
+
outerColor: "black",
|
|
48
|
+
},
|
|
49
|
+
opts
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Unwrap controls if it's a signal
|
|
53
|
+
const getControls = () => {
|
|
54
|
+
if (isSignal(settings.controls)) {
|
|
55
|
+
return settings.controls();
|
|
56
|
+
}
|
|
57
|
+
return settings.controls;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let outerRadius = 70;
|
|
61
|
+
let innerRadius = 10;
|
|
62
|
+
const innerAlphaStandby = 0.5;
|
|
63
|
+
|
|
64
|
+
let dragging = false;
|
|
65
|
+
let startPosition: PIXI.PointData | null = null;
|
|
66
|
+
let power = 0;
|
|
67
|
+
|
|
68
|
+
const innerPositionX = signal(0);
|
|
69
|
+
const innerPositionY = signal(0);
|
|
70
|
+
const innerAlpha = signal(innerAlphaStandby);
|
|
71
|
+
|
|
72
|
+
function getPower(centerPoint: PIXI.Point) {
|
|
73
|
+
const a = centerPoint.x - 0;
|
|
74
|
+
const b = centerPoint.y - 0;
|
|
75
|
+
return Math.min(1, Math.sqrt(a * a + b * b) / outerRadius);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getDirection(center: PIXI.Point) {
|
|
79
|
+
let rad = Math.atan2(center.y, center.x); // [-PI, PI]
|
|
80
|
+
if ((rad >= -Math.PI / 8 && rad < 0) || (rad >= 0 && rad < Math.PI / 8)) {
|
|
81
|
+
return Direction.RIGHT;
|
|
82
|
+
} else if (rad >= Math.PI / 8 && rad < (3 * Math.PI) / 8) {
|
|
83
|
+
return Direction.BOTTOM_RIGHT;
|
|
84
|
+
} else if (rad >= (3 * Math.PI) / 8 && rad < (5 * Math.PI) / 8) {
|
|
85
|
+
return Direction.BOTTOM;
|
|
86
|
+
} else if (rad >= (5 * Math.PI) / 8 && rad < (7 * Math.PI) / 8) {
|
|
87
|
+
return Direction.BOTTOM_LEFT;
|
|
88
|
+
} else if (
|
|
89
|
+
(rad >= (7 * Math.PI) / 8 && rad < Math.PI) ||
|
|
90
|
+
(rad >= -Math.PI && rad < (-7 * Math.PI) / 8)
|
|
91
|
+
) {
|
|
92
|
+
return Direction.LEFT;
|
|
93
|
+
} else if (rad >= (-7 * Math.PI) / 8 && rad < (-5 * Math.PI) / 8) {
|
|
94
|
+
return Direction.TOP_LEFT;
|
|
95
|
+
} else if (rad >= (-5 * Math.PI) / 8 && rad < (-3 * Math.PI) / 8) {
|
|
96
|
+
return Direction.TOP;
|
|
97
|
+
} else {
|
|
98
|
+
return Direction.TOP_RIGHT;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleDragStart(event: any) {
|
|
103
|
+
startPosition = event.getLocalPosition(this);
|
|
104
|
+
dragging = true;
|
|
105
|
+
innerAlpha.set(1);
|
|
106
|
+
settings.onStart?.();
|
|
107
|
+
|
|
108
|
+
// Notify controls if provided
|
|
109
|
+
const controls = getControls();
|
|
110
|
+
if (controls) {
|
|
111
|
+
// Check if it's JoystickControls instance
|
|
112
|
+
if (controls.handleJoystickStart) {
|
|
113
|
+
controls.handleJoystickStart();
|
|
114
|
+
}
|
|
115
|
+
// Check if it's ControlsDirective with joystick getter
|
|
116
|
+
else if (controls.joystick && controls.joystick.handleJoystickStart) {
|
|
117
|
+
controls.joystick.handleJoystickStart();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function handleDragEnd() {
|
|
123
|
+
if (!dragging) return;
|
|
124
|
+
innerPositionX.set(0);
|
|
125
|
+
innerPositionY.set(0);
|
|
126
|
+
dragging = false;
|
|
127
|
+
innerAlpha.set(innerAlphaStandby);
|
|
128
|
+
settings.onEnd?.();
|
|
129
|
+
|
|
130
|
+
// Notify controls if provided
|
|
131
|
+
const controls = getControls();
|
|
132
|
+
if (controls) {
|
|
133
|
+
// Check if it's JoystickControls instance
|
|
134
|
+
if (controls.handleJoystickEnd) {
|
|
135
|
+
controls.handleJoystickEnd();
|
|
136
|
+
}
|
|
137
|
+
// Check if it's ControlsDirective with joystick getter
|
|
138
|
+
else if (controls.joystick && controls.joystick.handleJoystickEnd) {
|
|
139
|
+
controls.joystick.handleJoystickEnd();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleDragMove(event: any) {
|
|
145
|
+
if (dragging == false) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let newPosition = event.getLocalPosition(this);
|
|
150
|
+
|
|
151
|
+
let sideX = newPosition.x - (startPosition?.x ?? 0);
|
|
152
|
+
let sideY = newPosition.y - (startPosition?.y ?? 0);
|
|
153
|
+
|
|
154
|
+
let centerPoint = new PIXI.Point(0, 0);
|
|
155
|
+
let angle = 0;
|
|
156
|
+
|
|
157
|
+
if (sideX == 0 && sideY == 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let calRadius = 0;
|
|
162
|
+
|
|
163
|
+
if (sideX * sideX + sideY * sideY >= outerRadius * outerRadius) {
|
|
164
|
+
calRadius = outerRadius;
|
|
165
|
+
} else {
|
|
166
|
+
calRadius = outerRadius - innerRadius;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* x: -1 <-> 1
|
|
171
|
+
* y: -1 <-> 1
|
|
172
|
+
* Y
|
|
173
|
+
* ^
|
|
174
|
+
* |
|
|
175
|
+
* 180 | 90
|
|
176
|
+
* ------------> X
|
|
177
|
+
* 270 | 360
|
|
178
|
+
* |
|
|
179
|
+
* |
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
let direction = Direction.LEFT;
|
|
183
|
+
|
|
184
|
+
if (sideX == 0) {
|
|
185
|
+
if (sideY > 0) {
|
|
186
|
+
centerPoint.set(0, sideY > outerRadius ? outerRadius : sideY);
|
|
187
|
+
angle = 270;
|
|
188
|
+
direction = Direction.BOTTOM;
|
|
189
|
+
} else {
|
|
190
|
+
centerPoint.set(
|
|
191
|
+
0,
|
|
192
|
+
-(Math.abs(sideY) > outerRadius ? outerRadius : Math.abs(sideY))
|
|
193
|
+
);
|
|
194
|
+
angle = 90;
|
|
195
|
+
direction = Direction.TOP;
|
|
196
|
+
}
|
|
197
|
+
innerPositionX.set(centerPoint.x);
|
|
198
|
+
innerPositionY.set(centerPoint.y);
|
|
199
|
+
power = getPower(centerPoint);
|
|
200
|
+
const changeEvent = { angle, direction, power };
|
|
201
|
+
settings.onChange?.(changeEvent);
|
|
202
|
+
|
|
203
|
+
// Notify controls if provided
|
|
204
|
+
const controls = getControls();
|
|
205
|
+
if (controls) {
|
|
206
|
+
// Check if it's JoystickControls instance
|
|
207
|
+
if (controls.handleJoystickChange) {
|
|
208
|
+
controls.handleJoystickChange(changeEvent);
|
|
209
|
+
}
|
|
210
|
+
// Check if it's ControlsDirective with joystick getter
|
|
211
|
+
else if (controls.joystick && controls.joystick.handleJoystickChange) {
|
|
212
|
+
controls.joystick.handleJoystickChange(changeEvent);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (sideY == 0) {
|
|
219
|
+
if (sideX > 0) {
|
|
220
|
+
centerPoint.set(
|
|
221
|
+
Math.abs(sideX) > outerRadius ? outerRadius : Math.abs(sideX),
|
|
222
|
+
0
|
|
223
|
+
);
|
|
224
|
+
angle = 0;
|
|
225
|
+
direction = Direction.RIGHT;
|
|
226
|
+
} else {
|
|
227
|
+
centerPoint.set(
|
|
228
|
+
-(Math.abs(sideX) > outerRadius ? outerRadius : Math.abs(sideX)),
|
|
229
|
+
0
|
|
230
|
+
);
|
|
231
|
+
angle = 180;
|
|
232
|
+
direction = Direction.LEFT;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
innerPositionX.set(centerPoint.x);
|
|
236
|
+
innerPositionY.set(centerPoint.y);
|
|
237
|
+
power = getPower(centerPoint);
|
|
238
|
+
const changeEvent = { angle, direction, power };
|
|
239
|
+
settings.onChange?.(changeEvent);
|
|
240
|
+
|
|
241
|
+
// Notify controls if provided
|
|
242
|
+
const controls = getControls();
|
|
243
|
+
if (controls) {
|
|
244
|
+
// Check if it's JoystickControls instance
|
|
245
|
+
if (controls.handleJoystickChange) {
|
|
246
|
+
controls.handleJoystickChange(changeEvent);
|
|
247
|
+
}
|
|
248
|
+
// Check if it's ControlsDirective with joystick getter
|
|
249
|
+
else if (controls.joystick && controls.joystick.handleJoystickChange) {
|
|
250
|
+
controls.joystick.handleJoystickChange(changeEvent);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let tanVal = Math.abs(sideY / sideX);
|
|
257
|
+
let radian = Math.atan(tanVal);
|
|
258
|
+
angle = (radian * 180) / Math.PI;
|
|
259
|
+
|
|
260
|
+
let centerX = 0;
|
|
261
|
+
let centerY = 0;
|
|
262
|
+
|
|
263
|
+
if (sideX * sideX + sideY * sideY >= outerRadius * outerRadius) {
|
|
264
|
+
centerX = outerRadius * Math.cos(radian);
|
|
265
|
+
centerY = outerRadius * Math.sin(radian);
|
|
266
|
+
} else {
|
|
267
|
+
centerX = Math.abs(sideX) > outerRadius ? outerRadius : Math.abs(sideX);
|
|
268
|
+
centerY = Math.abs(sideY) > outerRadius ? outerRadius : Math.abs(sideY);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (sideY < 0) {
|
|
272
|
+
centerY = -Math.abs(centerY);
|
|
273
|
+
}
|
|
274
|
+
if (sideX < 0) {
|
|
275
|
+
centerX = -Math.abs(centerX);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (sideX > 0 && sideY < 0) {
|
|
279
|
+
// < 90
|
|
280
|
+
} else if (sideX < 0 && sideY < 0) {
|
|
281
|
+
// 90 ~ 180
|
|
282
|
+
angle = 180 - angle;
|
|
283
|
+
} else if (sideX < 0 && sideY > 0) {
|
|
284
|
+
// 180 ~ 270
|
|
285
|
+
angle = angle + 180;
|
|
286
|
+
} else if (sideX > 0 && sideY > 0) {
|
|
287
|
+
// 270 ~ 369
|
|
288
|
+
angle = 360 - angle;
|
|
289
|
+
}
|
|
290
|
+
centerPoint.set(centerX, centerY);
|
|
291
|
+
power = getPower(centerPoint);
|
|
292
|
+
|
|
293
|
+
direction = getDirection(centerPoint);
|
|
294
|
+
innerPositionX.set(centerPoint.x);
|
|
295
|
+
innerPositionY.set(centerPoint.y);
|
|
296
|
+
const changeEvent = { angle, direction, power };
|
|
297
|
+
settings.onChange?.(changeEvent);
|
|
298
|
+
|
|
299
|
+
// Notify controls if provided
|
|
300
|
+
const controls = getControls();
|
|
301
|
+
if (controls) {
|
|
302
|
+
// Check if it's JoystickControls instance
|
|
303
|
+
if (controls.handleJoystickChange) {
|
|
304
|
+
controls.handleJoystickChange(changeEvent);
|
|
305
|
+
}
|
|
306
|
+
// Check if it's ControlsDirective with joystick getter
|
|
307
|
+
else if (controls.joystick && controls.joystick.handleJoystickChange) {
|
|
308
|
+
controls.joystick.handleJoystickChange(changeEvent);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let innerElement;
|
|
314
|
+
let outerElement;
|
|
315
|
+
|
|
316
|
+
if (!settings.outer) {
|
|
317
|
+
outerElement = h(Graphics, {
|
|
318
|
+
draw: (g) => {
|
|
319
|
+
g.circle(0, 0, outerRadius).fill(settings.outerColor);
|
|
320
|
+
},
|
|
321
|
+
alpha: 0.5,
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
outerElement = h(Sprite, {
|
|
325
|
+
image: settings.outer,
|
|
326
|
+
anchor: { x: 0.5, y: 0.5 },
|
|
327
|
+
scale: settings.outerScale,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const innerOptions: any = {
|
|
332
|
+
scale: settings.innerScale,
|
|
333
|
+
x: innerPositionX,
|
|
334
|
+
y: innerPositionY,
|
|
335
|
+
alpha: innerAlpha,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (!settings.inner) {
|
|
339
|
+
innerElement = h(Graphics, {
|
|
340
|
+
draw: (g) => {
|
|
341
|
+
g.circle(0, 0, innerRadius * 2.5).fill(settings.innerColor);
|
|
342
|
+
},
|
|
343
|
+
...innerOptions,
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
innerElement = settings.inner
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return h(
|
|
350
|
+
Container,
|
|
351
|
+
{
|
|
352
|
+
...opts,
|
|
353
|
+
pointerdown: handleDragStart,
|
|
354
|
+
pointerup: handleDragEnd,
|
|
355
|
+
pointerupoutside: handleDragEnd,
|
|
356
|
+
pointermove: handleDragMove,
|
|
357
|
+
},
|
|
358
|
+
outerElement,
|
|
359
|
+
innerElement,
|
|
360
|
+
);
|
|
361
|
+
}
|