Skip to content

Commit 8a31391

Browse files
committed
feat(ir-remote): add IR Remote
see wokwi/wokwi-features#90
1 parent 468ab48 commit 8a31391

File tree

4 files changed

+271
-0
lines changed

4 files changed

+271
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export { HCSR04Element } from './hc-sr04-element';
2626
export { LCD2004Element } from './lcd2004-element';
2727
export { AnalogJoystickElement } from './analog-joystick-element';
2828
export { IRReceiverElement } from './ir-receiver-element';
29+
export { IRRemoteElement } from './ir-remote-element';

src/ir-remote-element.stories.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { action } from '@storybook/addon-actions';
2+
import { html } from 'lit-html';
3+
import './ir-remote-element';
4+
5+
export default {
6+
title: 'IR Remote',
7+
component: 'wokwi-ir-remote',
8+
};
9+
10+
export const Default = () => html`<wokwi-ir-remote
11+
@button-press=${action('button-press')}
12+
@button-release=${action('button-release')}
13+
></wokwi-ir-remote>`;

src/ir-remote-element.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { css, customElement, html, LitElement } from 'lit-element';
2+
import { SPACE_KEYS } from './utils/keys';
3+
4+
const irKeyCodes: { [key: string]: number } = {
5+
power: 0xa2,
6+
menu: 0xe2,
7+
test: 0x22,
8+
plus: 0x02,
9+
back: 0xc2,
10+
prev: 0xe0,
11+
play: 0xa8,
12+
next: 0x90,
13+
0: 0x68,
14+
minus: 0x98,
15+
c: 0xb0,
16+
1: 0x30,
17+
2: 0x18,
18+
3: 0x7a,
19+
4: 0x10,
20+
5: 0x38,
21+
6: 0x5a,
22+
7: 0x42,
23+
8: 0x4a,
24+
9: 0x52,
25+
};
26+
27+
const keyboardKeyMap: { [key: string]: string } = {
28+
o: 'power',
29+
m: 'menu',
30+
t: 'test',
31+
'+': 'plus',
32+
b: 'back',
33+
arrowleft: 'prev',
34+
p: 'play',
35+
arrowright: 'next',
36+
0: '0',
37+
'-': 'minus',
38+
c: 'c',
39+
1: '1',
40+
2: '2',
41+
3: '3',
42+
4: '4',
43+
5: '5',
44+
6: '6',
45+
7: '7',
46+
8: '8',
47+
9: '9',
48+
};
49+
50+
@customElement('wokwi-ir-remote')
51+
export class IRRemoteElement extends LitElement {
52+
static get styles() {
53+
return css`
54+
use {
55+
fill: #fff;
56+
}
57+
58+
use.red {
59+
fill: #e6252e;
60+
}
61+
62+
use.black {
63+
fill: #121115;
64+
}
65+
66+
use[tabindex] {
67+
cursor: pointer;
68+
}
69+
70+
use.active {
71+
fill: #8c8;
72+
}
73+
74+
use.red.active,
75+
use.black.active {
76+
fill: green;
77+
}
78+
79+
use:focus {
80+
--circle-stroke-color: cyan;
81+
outline: none;
82+
}
83+
`;
84+
}
85+
86+
eventHandler(target: SVGElement, buttonId: string, type: 'up' | 'down') {
87+
target.focus();
88+
const irCode = irKeyCodes[buttonId];
89+
switch (type) {
90+
case 'up':
91+
target.classList.remove('active');
92+
this.dispatchEvent(
93+
new CustomEvent('button-release', {
94+
detail: { key: buttonId, irCode },
95+
})
96+
);
97+
break;
98+
case 'down':
99+
target.classList.add('active');
100+
this.dispatchEvent(
101+
new CustomEvent('button-press', {
102+
detail: { key: buttonId, irCode },
103+
})
104+
);
105+
break;
106+
}
107+
}
108+
109+
buttonEvent(event: Event, type: 'up' | 'down') {
110+
const target = event.target;
111+
if (!(target instanceof SVGElement)) {
112+
return null;
113+
}
114+
const buttonId = target.dataset.btn ?? '';
115+
if (buttonId == null) {
116+
return;
117+
}
118+
event.preventDefault();
119+
this.eventHandler(target, buttonId, type);
120+
}
121+
122+
keyboardEvent(event: KeyboardEvent, type: 'up' | 'down') {
123+
if (SPACE_KEYS.includes(event.key)) {
124+
this.buttonEvent(event, type);
125+
}
126+
const target = event.target;
127+
const buttonId = keyboardKeyMap[event.key.toLowerCase()];
128+
if (!(target instanceof SVGElement) || buttonId == null) {
129+
return;
130+
}
131+
const buttonElement = this.shadowRoot?.querySelector(`use[data-btn="${buttonId}"]`);
132+
if (buttonElement && buttonElement instanceof SVGElement) {
133+
this.eventHandler(buttonElement, buttonId, type);
134+
}
135+
}
136+
137+
render() {
138+
return html`
139+
<?xml version="1.0" encoding="UTF-8"?>
140+
<svg
141+
version="1.1"
142+
viewBox="0 0 151 316"
143+
width="40mm"
144+
height="83.653mm"
145+
font-family="sans-serif"
146+
xmlns="http://www.w3.org/2000/svg"
147+
@mousedown=${(e: MouseEvent) => this.buttonEvent(e, 'down')}
148+
@mouseup=${(e: MouseEvent) => this.buttonEvent(e, 'up')}
149+
@touchstart=${(e: TouchEvent) => this.buttonEvent(e, 'down')}
150+
@touchend=${(e: TouchEvent) => this.buttonEvent(e, 'up')}
151+
@keydown=${(e: KeyboardEvent) => this.keyboardEvent(e, 'down')}
152+
@keyup=${(e: KeyboardEvent) => this.keyboardEvent(e, 'up')}
153+
>
154+
<defs>
155+
<g id="button" stroke-width="1.29">
156+
<path
157+
fill="#272726"
158+
d="m0 -17.5c-9.73 0-17.6 7.9-17.6 17.6 0 9.73 7.9 17.6 17.6 17.6 9.73 0 17.6-7.9 17.6-17.6 0-9.73-7.9-17.6-17.6-17.6zm0 1.29c9.01 0 16.3 7.32 16.3 16.3 0 9.01-7.32 16.3-16.3 16.3-9.02 0-16.3-7.32-16.3-16.3 0-9.02 7.32-16.3 16.3-16.3z"
159+
/>
160+
<circle r="16.3" style="stroke: var(--circle-stroke-color)" />
161+
</g>
162+
<circle id="button2" r="16.3" style="stroke: var(--circle-stroke-color)" />
163+
</defs>
164+
<path
165+
d="m149 21.3c0-10.5-8.52-19-19-19h-109c-10.5 0-19 8.52-19 19v274c0 10.5 8.52 19 19 19h109c10.5 0 19-8.52 19-19z"
166+
fill="#fff"
167+
stroke="#272726"
168+
stroke-width="4.53px"
169+
/>
170+
<use xlink:href="#button2" x="39.2" y="37.9" data-btn="power" class="red" tabindex="0" />
171+
<use xlink:href="#button" x="120" y="37.9" data-btn="menu" tabindex="0" fill="#fff" />
172+
<use xlink:href="#button" x="39.2" y="75.2" data-btn="test" tabindex="0" fill="#fff" />
173+
<use xlink:href="#button2" x="79.9" y="75.2" data-btn="plus" class="black" tabindex="0" />
174+
<use xlink:href="#button" x="120" y="75.2" data-btn="back" tabindex="0" fill="#fff" />
175+
<use xlink:href="#button2" x="39.2" y="113" data-btn="prev" class="black" tabindex="0" />
176+
<use xlink:href="#button" x="79.9" y="113" data-btn="play" tabindex="0" fill="#fff" />
177+
<use xlink:href="#button2" x="120" y="113" data-btn="next" class="black" tabindex="0" />
178+
<use xlink:href="#button" x="39.2" y="152" data-btn="0" tabindex="0" fill="#fff" />
179+
<use xlink:href="#button2" x="79.9" y="152" data-btn="minus" class="black" tabindex="0" />
180+
<use xlink:href="#button" x="120" y="152" data-btn="c" tabindex="0" fill="#fff" />
181+
<use xlink:href="#button" x="39.2" y="190" data-btn="1" tabindex="0" fill="#fff" />
182+
<use xlink:href="#button" x="79.9" y="190" data-btn="2" tabindex="0" fill="#fff" />
183+
<use xlink:href="#button" x="120" y="190" data-btn="3" tabindex="0" fill="#fff" />
184+
<use xlink:href="#button" x="39.2" y="228" data-btn="4" tabindex="0" fill="#fff" />
185+
<use xlink:href="#button" x="79.9" y="228" data-btn="5" tabindex="0" fill="#fff" />
186+
<use xlink:href="#button" x="120" y="228" data-btn="6" tabindex="0" fill="#fff" />
187+
<use xlink:href="#button" x="39.2" y="266" data-btn="7" tabindex="0" fill="#fff" />
188+
<use xlink:href="#button" x="79.9" y="266" data-btn="8" tabindex="0" fill="#fff" />
189+
<use xlink:href="#button" x="120" y="266" data-btn="9" tabindex="0" fill="#fff" />
190+
<g style="pointer-events: none">
191+
<g fill="none" stroke="#fff" stroke-width="1.94px">
192+
<path
193+
d="m43.1 33c2.05 1.28 3.42 3.56 3.42 6.16 0 4.01-3.26 7.26-7.26 7.26-4.01 0-7.26-3.25-7.26-7.26 0-2.49 1.26-4.69 3.17-6"
194+
/>
195+
<path d="m39.2 29.3v7.41" />
196+
</g>
197+
<path d="m86.5 113-9.58 4.79v-9.58z" fill="#121115" stroke-width="1.29" />
198+
<path d="m122 113-9.58 4.79v-9.58z" fill="#fff" stroke-width="1.29" />
199+
<path d="m128 113-8.95 4.79v-9.58z" fill="#fff" stroke-width="1.29" />
200+
<path d="m128 109v9.58" fill="none" stroke="#fff" stroke-width="1.29" />
201+
<path d="m37.5 113 9.58 4.79v-9.58z" fill="#fff" stroke-width="1.29" />
202+
<path d="m31.4 113 8.95 4.79v-9.58z" fill="#fff" stroke-width="1.29" />
203+
<path d="m32 109v9.58" fill="none" stroke="#fff" stroke-width="1.29" />
204+
<text fill="#e6252e" font-size="9.72px" font-weight="700" stroke-width="1.29">
205+
<tspan x="105.492 114.069 121.032 128.531" y="41.288">
206+
MENU
207+
</tspan>
208+
<tspan x="26.088 32.504 39.466 46.429" y="78.679">
209+
TEST
210+
</tspan>
211+
</text>
212+
<g fill="none" stroke="#fff" stroke-width="1.29">
213+
<path d="m72.1 152h15.5" />
214+
<path d="m72.1 75.2h15.5M79.9 67.4v15.5" />
215+
</g>
216+
<g fill="#121115" stroke-width="1.29">
217+
<path
218+
d="m118 70.7v-3.25l-6.95 4.84 6.71 4.45 0.111-2.2s6.65-0.357 7.05 3.15c0.397 3.51-6.66 5.21-6.66 5.21s10.9-2.33 10.7-6.82c-0.276-5.4-10.9-5.39-10.9-5.39z"
219+
/>
220+
<text font-size="13.9px" font-weight="700">
221+
<tspan x="35.427" y="156.434">0</tspan>
222+
<tspan x="115.573" y="156.498">C</tspan>
223+
<tspan x="34.912" y="194.685">1</tspan>
224+
<tspan x="76.176" y="194.685">2</tspan>
225+
<tspan x="116.66" y="194.6">3</tspan>
226+
<tspan x="34.912" y="232.851">4</tspan>
227+
<tspan x="76.176" y="232.679">5</tspan>
228+
<tspan x="116.799" y="232.767">6</tspan>
229+
<tspan x="34.912" y="270.931">7</tspan>
230+
<tspan x="76.176" y="270.931">8</tspan>
231+
<tspan x="116.724" y="270.931">9</tspan>
232+
</text>
233+
</g>
234+
<g fill="#fff" stroke-width="1.29">
235+
<path
236+
d="m27.6 28.5c0.687-0.757 1.5-1.41 2.39-1.99 1.26-0.814 2.7-1.43 4.22-1.87 0.974-0.281 1.98-0.481 3-0.607 0.673-0.0828 1.35-0.129 2.03-0.147 0.68-0.0181 1.36-0.0078 2.03 0.0427 1.02 0.0789 2.03 0.243 3 0.511 2.48 0.686 4.72 2.02 6.31 4.19 0.0323 0.0479 0.097 0.0608 0.145 0.0298 0.0479-0.0323 0.0621-0.097 0.0298-0.145-0.846-1.45-1.96-2.62-3.27-3.53-0.894-0.623-1.87-1.12-2.91-1.5-1.19-0.433-2.45-0.709-3.73-0.828-0.543-0.0504-1.09-0.0698-1.64-0.0582-0.728 0.0155-1.46 0.0841-2.18 0.202-1.08 0.177-2.14 0.46-3.16 0.839-0.772 0.288-1.51 0.632-2.21 1.03-1.7 0.965-3.16 2.22-4.22 3.7-0.0362 0.0453-0.0298 0.111 0.0155 0.146 0.0453 0.0362 0.11 0.0298 0.146-0.0155z"
237+
/>
238+
<path
239+
d="m68.4 65.5c0.687-0.757 1.5-1.41 2.39-1.99 1.26-0.814 2.7-1.43 4.22-1.87 0.974-0.281 1.98-0.481 3-0.607 0.673-0.0815 1.35-0.129 2.03-0.147 0.679-0.0181 1.36-0.0078 2.03 0.044 1.02 0.0776 2.03 0.242 3 0.51 2.48 0.686 4.72 2.02 6.31 4.19 0.031 0.0479 0.0957 0.0621 0.145 0.0298 0.0479-0.031 0.0608-0.0957 0.0297-0.145-0.847-1.45-1.97-2.62-3.27-3.53-0.892-0.623-1.87-1.12-2.91-1.5-1.19-0.433-2.45-0.709-3.73-0.828-0.545-0.0504-1.09-0.0698-1.64-0.0582-0.728 0.0155-1.46 0.0841-2.18 0.202-1.08 0.177-2.14 0.46-3.16 0.839-0.772 0.288-1.51 0.632-2.22 1.03-1.7 0.965-3.16 2.22-4.22 3.7-0.0362 0.0453-0.0285 0.111 0.0155 0.147 0.0453 0.0362 0.111 0.0285 0.147-0.0168z"
240+
/>
241+
<path
242+
d="m27.6 104c0.687-0.757 1.5-1.42 2.39-1.99 1.26-0.814 2.7-1.43 4.22-1.87 0.974-0.281 1.98-0.481 3-0.607 0.673-0.0815 1.35-0.129 2.03-0.147 0.68-0.0181 1.36-8e-3 2.03 0.044 1.02 0.0776 2.03 0.242 3 0.51 2.48 0.686 4.72 2.02 6.31 4.19 0.0323 0.0478 0.097 0.0621 0.145 0.0297 0.0479-0.031 0.0621-0.0957 0.0298-0.145-0.846-1.45-1.96-2.62-3.27-3.53-0.894-0.623-1.87-1.12-2.91-1.5-1.19-0.433-2.45-0.709-3.73-0.828-0.543-0.0504-1.09-0.0698-1.64-0.0582-0.728 0.0155-1.46 0.0841-2.18 0.202-1.08 0.177-2.14 0.46-3.16 0.839-0.772 0.288-1.51 0.632-2.21 1.03-1.7 0.965-3.16 2.22-4.22 3.7-0.0362 0.0453-0.0298 0.111 0.0155 0.147 0.0453 0.0362 0.11 0.0285 0.146-0.0168z"
243+
/>
244+
<path
245+
d="m109 104c0.687-0.757 1.5-1.42 2.39-1.99 1.26-0.814 2.7-1.43 4.22-1.87 0.974-0.281 1.98-0.481 3-0.607 0.673-0.0815 1.35-0.129 2.03-0.147 0.68-0.0181 1.36-8e-3 2.03 0.044 1.02 0.0776 2.03 0.242 3 0.51 2.48 0.686 4.72 2.02 6.31 4.19 0.031 0.0478 0.0957 0.0621 0.145 0.0297 0.0479-0.031 0.0608-0.0957 0.0298-0.145-0.847-1.45-1.97-2.62-3.27-3.53-0.892-0.623-1.87-1.12-2.91-1.5-1.19-0.433-2.45-0.709-3.73-0.828-0.545-0.0504-1.09-0.0698-1.64-0.0582-0.728 0.0155-1.46 0.0841-2.18 0.202-1.08 0.177-2.14 0.46-3.16 0.839-0.772 0.288-1.51 0.632-2.22 1.03-1.7 0.965-3.16 2.22-4.22 3.7-0.0362 0.0453-0.0285 0.111 0.0155 0.147 0.0453 0.0362 0.111 0.0285 0.147-0.0168z"
246+
/>
247+
<path
248+
d="m68.4 142c0.687-0.758 1.5-1.42 2.39-1.99 1.26-0.815 2.7-1.43 4.22-1.87 0.974-0.279 1.98-0.481 3-0.605 0.673-0.0828 1.35-0.129 2.03-0.147 0.679-0.0181 1.36-9e-3 2.03 0.0427 1.02 0.0789 2.03 0.243 3 0.511 2.48 0.686 4.72 2.02 6.31 4.19 0.031 0.0491 0.0957 0.0621 0.145 0.031 0.0479-0.0323 0.0608-0.097 0.0297-0.145-0.847-1.45-1.97-2.62-3.27-3.54-0.892-0.623-1.87-1.12-2.91-1.5-1.19-0.435-2.45-0.71-3.73-0.829-0.545-0.0504-1.09-0.0698-1.64-0.0569-0.728 0.0155-1.46 0.0841-2.18 0.202-1.08 0.177-2.14 0.459-3.16 0.838-0.772 0.29-1.51 0.632-2.22 1.03-1.7 0.965-3.16 2.22-4.22 3.7-0.0362 0.044-0.0285 0.11 0.0155 0.146 0.0453 0.0362 0.111 0.0284 0.147-0.0155z"
249+
/>
250+
</g>
251+
</g>
252+
</svg>
253+
`;
254+
}
255+
}

src/react-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { HCSR04Element } from './hc-sr04-element';
2525
import { LCD2004Element } from './lcd2004-element';
2626
import { AnalogJoystickElement } from './analog-joystick-element';
2727
import { IRReceiverElement } from './ir-receiver-element';
28+
import { IRRemoteElement } from './ir-remote-element';
2829

2930
type WokwiElement<T> = Partial<T> & React.ClassAttributes<T>;
3031

@@ -55,6 +56,7 @@ declare global {
5556
'wokwi-lcd2004': WokwiElement<LCD2004Element>;
5657
'wokwi-analog-joystick': WokwiElement<AnalogJoystickElement>;
5758
'wokwi-ir-receiver': WokwiElement<IRReceiverElement>;
59+
'wokwi-ir-remote': WokwiElement<IRRemoteElement>;
5860
}
5961
}
6062
}

0 commit comments

Comments
 (0)