Skip to content

Commit 5449be1

Browse files
committed
feat(analog-joystick): add wokwi-analog-joystick #54
close #54
1 parent 354c9f0 commit 5449be1

File tree

4 files changed

+322
-0
lines changed

4 files changed

+322
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { html } from 'lit-html';
2+
import './analog-joystick-element';
3+
4+
export default {
5+
title: 'Analog Joystick',
6+
component: 'wokwi-analog-joystick',
7+
};
8+
9+
export const Joystick = () => html`<wokwi-analog-joystick></wokwi-analog-joystick>`;

src/analog-joystick-element.ts

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import { css, customElement, html, LitElement, property } from 'lit-element';
2+
import { analog, ElementPin, GND, VCC } from './pin';
3+
import { SPACE_KEYS } from './utils/keys';
4+
5+
@customElement('wokwi-analog-joystick')
6+
export class AnalogJoystickElement extends LitElement {
7+
@property({ type: Number }) xValue = 0;
8+
@property({ type: Number }) yValue = 0;
9+
@property() pressed = false;
10+
11+
readonly pinInfo: ElementPin[] = [
12+
{ name: 'VCC', x: 33, y: 115.8, signals: [VCC()] },
13+
{ name: 'VERT', x: 42.6012, y: 115.8, signals: [analog(0)] },
14+
{ name: 'HORZ', x: 52.2024, y: 115.8, signals: [analog(1)] },
15+
{ name: 'SEL', x: 61.8036, y: 115.8, signals: [] },
16+
{ name: 'GND', x: 71.4048, y: 115.8, signals: [GND()] },
17+
];
18+
19+
static get styles() {
20+
return css`
21+
#knob {
22+
transition: transform 0.3s;
23+
}
24+
25+
#knob:hover {
26+
fill: #222;
27+
}
28+
29+
#knob:focus {
30+
outline: none;
31+
stroke: #4d90fe;
32+
stroke-width: 0.5;
33+
}
34+
35+
.controls {
36+
opacity: 0;
37+
transition: opacity 0.3s;
38+
cursor: pointer;
39+
}
40+
41+
#knob:hover ~ .controls {
42+
opacity: 1;
43+
}
44+
45+
.controls:hover {
46+
opacity: 1;
47+
}
48+
49+
.controls path {
50+
pointer-events: none;
51+
}
52+
53+
.controls .region {
54+
pointer-events: bounding-box;
55+
fill: none;
56+
}
57+
58+
.controls .region:hover + path {
59+
fill: #fff;
60+
}
61+
62+
.controls circle:hover {
63+
stroke: #fff;
64+
}
65+
66+
.controls circle.pressed {
67+
fill: #fff;
68+
}
69+
`;
70+
}
71+
72+
render() {
73+
const { xValue, yValue } = this;
74+
return html`
75+
<svg
76+
width="27.2mm"
77+
height="31.8mm"
78+
viewBox="0 0 27.2 31.8"
79+
xmlns="http://www.w3.org/2000/svg"
80+
xmlns:xlink="http://www.w3.org/1999/xlink"
81+
>
82+
<defs>
83+
<filter id="noise" primitiveUnits="objectBoundingBox">
84+
<feTurbulence baseFrequency="2 2" type="fractalNoise" />
85+
<feColorMatrix
86+
values=".1 0 0 0 .1
87+
.1 0 0 0 .1
88+
.1 0 0 0 .1
89+
0 0 0 0 1"
90+
/>
91+
<feComposite in2="SourceGraphic" operator="lighter" />
92+
<feComposite result="body" in2="SourceAlpha" operator="in" />
93+
</filter>
94+
<radialGradient id="g-knob" cx="13.6" cy="13.6" r="10.6" gradientUnits="userSpaceOnUse">
95+
<stop offset="0" />
96+
<stop offset="0.9" />
97+
<stop stop-color="#777" offset="1" />
98+
</radialGradient>
99+
<radialGradient
100+
id="g-knob-base"
101+
cx="13.6"
102+
cy="13.6"
103+
r="13.6"
104+
gradientUnits="userSpaceOnUse"
105+
>
106+
<stop offset="0" />
107+
<stop stop-color="#444" offset=".8" />
108+
<stop stop-color="#555" offset=".9" />
109+
<stop offset="1" />
110+
</radialGradient>
111+
<path
112+
id="pin"
113+
fill="silver"
114+
stroke="#a2a2a2"
115+
stroke-width=".024"
116+
d="M8.726 29.801a.828.828 0 00-.828.829.828.828 0 00.828.828.828.828 0 00.829-.828.828.828 0 00-.829-.829zm-.004.34a.49.49 0 01.004 0 .49.49 0 01.49.489.49.49 0 01-.49.49.49.49 0 01-.489-.49.49.49 0 01.485-.49z"
117+
/>
118+
</defs>
119+
<path
120+
d="M1.3 0v31.7h25.5V0zm2.33.683a1.87 1.87 0 01.009 0 1.87 1.87 0 011.87 1.87 1.87 1.87 0 01-1.87 1.87 1.87 1.87 0 01-1.87-1.87 1.87 1.87 0 011.87-1.87zm20.5 0a1.87 1.87 0 01.009 0 1.87 1.87 0 011.87 1.87 1.87 1.87 0 01-1.87 1.87 1.87 1.87 0 01-1.87-1.87 1.87 1.87 0 011.87-1.87zm-20.5 26.8a1.87 1.87 0 01.009 0 1.87 1.87 0 011.87 1.87 1.87 1.87 0 01-1.87 1.87 1.87 1.87 0 01-1.87-1.87 1.87 1.87 0 011.87-1.87zm20.4 0a1.87 1.87 0 01.009 0 1.87 1.87 0 011.87 1.87 1.87 1.87 0 01-1.87 1.87 1.87 1.87 0 01-1.87-1.87 1.87 1.87 0 011.87-1.87zm-12.7 2.66a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.489.489.489 0 01.485-.489zm2.57 0a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.489.489.489 0 01.485-.489zm2.49.013a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.489.489.489 0 01.485-.489zm-7.62.007a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.49.489.489 0 01.485-.488zm10.2.013a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.49.489.489 0 01.485-.488z"
121+
fill="#bd1e34"
122+
/>
123+
<g
124+
fill="#fff"
125+
font-family="sans-serif"
126+
text-anchor="middle"
127+
stroke-width=".03"
128+
font-size="1.2"
129+
>
130+
<text letter-spacing=".053">
131+
<tspan x="4.034" y="25.643">Analog</tspan>
132+
<tspan x="4.061" y="27.159">Joystick</tspan>
133+
</text>
134+
<text transform="rotate(-90)" text-anchor="start" font-size="1.2">
135+
<tspan x="-29.2" y="9.2">VCC</tspan>
136+
<tspan x="-29.2" y="11.74">VERT</tspan>
137+
<tspan x="-29.2" y="14.28">HORZ</tspan>
138+
<tspan x="-29.2" y="16.82">SEL</tspan>
139+
<tspan x="-29.2" y="19.36">GND</tspan>
140+
</text>
141+
</g>
142+
<ellipse cx="13.6" cy="13.7" rx="13.6" ry="13.7" fill="url(#g-knob-base)" />
143+
<path
144+
d="M48.2 65.5s.042.179-.093.204c-.094.017-.246-.077-.322-.17-.094-.115-.082-.205-.009-.285.11-.122.299-.075.299-.075s-.345-.303-.705-.054c-.32.22-.228.52.06.783.262.237.053.497-.21.463-.18-.023-.252-.167-.21-.256.038-.076.167-.122.167-.122s-.149-.06-.324.005c-.157.06-.286.19-.276.513v1.51s.162-.2.352-.403c.214-.229.311-.384.53-.366.415.026.714-.159.918-.454.391-.569.085-1.2-.178-1.29"
145+
fill="#fff"
146+
/>
147+
<circle
148+
id="knob"
149+
cx="13.6"
150+
cy="13.6"
151+
transform="translate(${2.5 * xValue}, ${2.5 * yValue})"
152+
r="10.6"
153+
fill="url(#g-knob)"
154+
filter="url(#noise)"
155+
tabindex="0"
156+
@keyup=${(e: KeyboardEvent) => this.keyup(e)}
157+
@keydown=${(e: KeyboardEvent) => this.keydown(e)}
158+
/>
159+
<g fill="none" stroke="#fff" stroke-width=".142">
160+
<path
161+
d="M7.8 31.7l-.383-.351v-1.31l.617-.656h1.19l.721.656.675-.656h1.18l.708.656.662-.656h1.25l.643.656.63-.656h1.21l.695.656.636-.656h1.17l.753.656v1.3l-.416.39"
162+
/>
163+
<path
164+
d="M9.5 31.7l.381-.344.381.331M12.1 31.7l.381-.344.381.331M14.7 31.7l.381-.344.381.331M17.2 31.7l.381-.344.381.331"
165+
stroke-linecap="square"
166+
stroke-linejoin="bevel"
167+
/>
168+
</g>
169+
<g class="controls" stroke-width="0.6" stroke-linejoin="bevel" fill="#aaa">
170+
<rect
171+
class="region"
172+
y="8.5"
173+
x="1"
174+
height="10"
175+
width="7"
176+
@mousedown=${() => this.mousedown(-1, 0)}
177+
@mouseup=${() => this.mouseup(true, false)}
178+
/>
179+
<path d="m 7.022,11.459 -3.202,2.497 3.202,2.497" />
180+
181+
<rect
182+
class="region"
183+
y="1.38"
184+
x="7.9"
185+
height="7"
186+
width="10"
187+
@mousedown=${() => this.mousedown(0, -1)}
188+
@mouseup=${() => this.mouseup(false, true)}
189+
/>
190+
<path d="m 16.615,7.095 -2.497,-3.202 -2.497,3.202" />
191+
192+
<rect
193+
class="region"
194+
y="8.5"
195+
x="18"
196+
height="10"
197+
width="7"
198+
@mousedown=${() => this.mousedown(1, 0)}
199+
@mouseup=${() => this.mouseup(true, false)}
200+
/>
201+
<path d="m 19.980,16.101 3.202,-2.497 -3.202,-2.497" />
202+
203+
<rect
204+
class="region"
205+
y="17"
206+
x="7.9"
207+
height="7"
208+
width="10"
209+
@mousedown=${() => this.mousedown(0, 1)}
210+
@mouseup=${() => this.mouseup(false, true)}
211+
/>
212+
<path d="m 11.620,20.112 2.497,3.202 2.497,-3.202" />
213+
214+
<circle
215+
cx="13.6"
216+
cy="13.6"
217+
r="3"
218+
stroke="#aaa"
219+
class=${this.pressed ? 'pressed' : ''}
220+
@mousedown=${() => this.press()}
221+
@mouseup=${() => this.release()}
222+
/>
223+
</g>
224+
<use xlink:href="#pin" x="0" />
225+
<use xlink:href="#pin" x="2.54" />
226+
<use xlink:href="#pin" x="5.08" />
227+
<use xlink:href="#pin" x="7.62" />
228+
<use xlink:href="#pin" x="10.16" />
229+
</svg>
230+
`;
231+
}
232+
233+
private keydown(e: KeyboardEvent) {
234+
switch (e.key) {
235+
case 'ArrowUp':
236+
this.yValue = -1;
237+
this.valueChanged();
238+
break;
239+
case 'ArrowDown':
240+
this.yValue = 1;
241+
this.valueChanged();
242+
break;
243+
case 'ArrowLeft':
244+
this.xValue = -1;
245+
this.valueChanged();
246+
break;
247+
case 'ArrowRight':
248+
this.xValue = 1;
249+
this.valueChanged();
250+
break;
251+
}
252+
if (SPACE_KEYS.includes(e.key)) {
253+
this.pressed = true;
254+
this.valueChanged();
255+
}
256+
}
257+
258+
private keyup(e: KeyboardEvent) {
259+
switch (e.key) {
260+
case 'ArrowUp':
261+
case 'ArrowDown':
262+
this.yValue = 0;
263+
this.valueChanged();
264+
break;
265+
case 'ArrowLeft':
266+
case 'ArrowRight':
267+
this.xValue = 0;
268+
this.valueChanged();
269+
break;
270+
}
271+
if (SPACE_KEYS.includes(e.key)) {
272+
this.pressed = false;
273+
this.valueChanged();
274+
}
275+
}
276+
277+
private mousedown(dx: number, dy: number) {
278+
if (dx) {
279+
this.xValue = dx;
280+
}
281+
if (dy) {
282+
this.yValue = dy;
283+
}
284+
this.valueChanged();
285+
}
286+
287+
private mouseup(x: boolean, y: boolean) {
288+
if (x) {
289+
this.xValue = 0;
290+
}
291+
if (y) {
292+
this.yValue = 0;
293+
}
294+
this.valueChanged();
295+
}
296+
297+
private press() {
298+
this.pressed = true;
299+
this.dispatchEvent(new InputEvent('button-press'));
300+
}
301+
302+
private release() {
303+
this.pressed = false;
304+
this.dispatchEvent(new InputEvent('button-release'));
305+
}
306+
307+
private valueChanged() {
308+
this.dispatchEvent(new InputEvent('input'));
309+
}
310+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export { LEDRingElement } from './led-ring-element';
2424
export { SlideSwitchElement } from './slide-switch-element';
2525
export { HCSR04Element } from './hc-sr04-element';
2626
export { LCD2004Element } from './lcd2004-element';
27+
export { AnalogJoystickElement } from './analog-joystick-element';

src/react-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { LEDRingElement } from './led-ring-element';
2323
import { SlideSwitchElement } from './slide-switch-element';
2424
import { HCSR04Element } from './hc-sr04-element';
2525
import { LCD2004Element } from './lcd2004-element';
26+
import { AnalogJoystickElement } from './analog-joystick-element';
2627

2728
type WokwiElement<T> = Partial<T> & React.ClassAttributes<T>;
2829

@@ -51,6 +52,7 @@ declare global {
5152
'wokwi-slide-switch': WokwiElement<SlideSwitchElement>;
5253
'wokwi-hc-sr04': WokwiElement<HCSR04Element>;
5354
'wokwi-lcd2004': WokwiElement<LCD2004Element>;
55+
'wokwi-analog-joystick': WokwiElement<AnalogJoystickElement>;
5456
}
5557
}
5658
}

0 commit comments

Comments
 (0)