Skip to content

Commit 90c69f2

Browse files
authored
feat: add rotary dialer (#26)
The dialer mimics an old rotary dial phone, inspired by a project from @talofer99
1 parent e0136ac commit 90c69f2

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export { PotentiometerElement } from './potentiometer-element';
1212
export { NeopixelMatrixElement } from './neopixel-matrix-element';
1313
export { SSD1306Element } from './ssd1306-element';
1414
export { BuzzerElement } from './buzzer-element';
15+
export { RotaryDialerElement } from './rotary-dialer-element';

src/rotary-dailer-element.stories.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { withKnobs, number } from '@storybook/addon-knobs';
2+
import { storiesOf } from '@storybook/web-components';
3+
import { html } from 'lit-html';
4+
import './rotary-dialer-element';
5+
import { logEvent } from 'storybook-events-logger';
6+
7+
storiesOf('Rotary Dialer', module)
8+
.addParameters({ component: 'wokwi-rotary-dialer' })
9+
.addDecorator(withKnobs)
10+
.add('Default', () => html` <wokwi-rotary-dialer @dial=${logEvent}></wokwi-rotary-dialer> `);

src/rotary-dialer-element.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { css, customElement, html, LitElement } from 'lit-element';
2+
import { styleMap } from 'lit-html/directives/style-map';
3+
import { classMap } from 'lit-html/directives/class-map';
4+
5+
type InitialValue = '';
6+
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
7+
8+
@customElement('wokwi-rotary-dialer')
9+
export class RotaryDialerElement extends LitElement {
10+
private digit: Digit | InitialValue = '';
11+
private stylesMapping = {};
12+
private classes: Record<string, boolean> = { 'rotate-path': true };
13+
private degrees = [320, 56, 87, 115, 143, 173, 204, 232, 260, 290];
14+
static get styles() {
15+
return css`
16+
.text {
17+
cursor: grab;
18+
user-select: none;
19+
}
20+
input:focus + svg #container {
21+
stroke: #4e50d7;
22+
stroke-width: 3;
23+
}
24+
.hide-input {
25+
position: absolute;
26+
clip: rect(0 0 0 0);
27+
width: 1px;
28+
height: 1px;
29+
margin: -1px;
30+
}
31+
.rotate-path {
32+
transform-origin: center;
33+
transition: transform 1000ms ease-in;
34+
}
35+
.dialer-anim {
36+
transform: rotate(var(--angle));
37+
}
38+
`;
39+
}
40+
41+
private removeDialerAnim() {
42+
this.classes = { ...this.classes, 'dialer-anim': false };
43+
this.stylesMapping = { '--angle': 0 };
44+
this.requestUpdate();
45+
}
46+
47+
/**
48+
* Exposed lazy dial by applying dial method with a given argument of number from 0-9
49+
* @param digit
50+
*/
51+
public dial(digit: Digit) {
52+
this.digit = digit;
53+
this.addDialerAnim(digit);
54+
}
55+
56+
private onValueChange(event: KeyboardEvent) {
57+
const target = event.target as HTMLInputElement;
58+
this.digit = parseInt(target.value) as Digit;
59+
this.dial(this.digit);
60+
target.value = '';
61+
}
62+
63+
private addDialerAnim(digit: Digit) {
64+
requestAnimationFrame(() => {
65+
// When you click on a digit, the circle-hole of that digit
66+
// should go all the way until the finger stop.
67+
this.classes = { ...this.classes, 'dialer-anim': true };
68+
const deg = this.degrees[digit as number];
69+
this.stylesMapping = { '--angle': deg + 'deg' };
70+
this.requestUpdate();
71+
});
72+
}
73+
74+
private focusInput() {
75+
const inputEl = this.shadowRoot?.querySelector('.hide-input') as HTMLInputElement;
76+
inputEl?.focus();
77+
}
78+
79+
render() {
80+
return html`
81+
<input
82+
tabindex="0"
83+
type="number"
84+
class="hide-input"
85+
value="${this.digit}"
86+
@input="${this.onValueChange}"
87+
/>
88+
<svg width="266" height="266" @click="${this.focusInput}" xmlns="http://www.w3.org/2000/svg">
89+
<g transform="translate(1 1)">
90+
<circle stroke="#979797" stroke-width="3" fill="#1F1F1F" cx="133" cy="133" r="131" />
91+
<circle stroke="#fff" stroke-width="2" fill="#D8D8D8" cx="133" cy="133" r="72" />
92+
<path
93+
class=${classMap(this.classes)}
94+
@transitionend="${() => {
95+
this.removeDialerAnim();
96+
this.dispatchEvent(new CustomEvent('dial', { detail: { digit: this.digit } }));
97+
}}"
98+
d="M133.5,210 C146.478692,210 157,220.521308 157,233.5 C157,246.478692 146.478692,257 133.5,257 C120.521308,257 110,246.478692 110,233.5 C110,220.521308 120.521308,210 133.5,210 Z M83.5,197 C96.4786916,197 107,207.521308 107,220.5 C107,233.478692 96.4786916,244 83.5,244 C70.5213084,244 60,233.478692 60,220.5 C60,207.521308 70.5213084,197 83.5,197 Z M45.5,163 C58.4786916,163 69,173.521308 69,186.5 C69,199.478692 58.4786916,210 45.5,210 C32.5213084,210 22,199.478692 22,186.5 C22,173.521308 32.5213084,163 45.5,163 Z M32.5,114 C45.4786916,114 56,124.521308 56,137.5 C56,150.478692 45.4786916,161 32.5,161 C19.5213084,161 9,150.478692 9,137.5 C9,124.521308 19.5213084,114 32.5,114 Z M234.5,93 C247.478692,93 258,103.521308 258,116.5 C258,129.478692 247.478692,140 234.5,140 C221.521308,140 211,129.478692 211,116.5 C211,103.521308 221.521308,93 234.5,93 Z M41.5,64 C54.4786916,64 65,74.5213084 65,87.5 C65,100.478692 54.4786916,111 41.5,111 C28.5213084,111 18,100.478692 18,87.5 C18,74.5213084 28.5213084,64 41.5,64 Z M214.5,46 C227.478692,46 238,56.5213084 238,69.5 C238,82.4786916 227.478692,93 214.5,93 C201.521308,93 191,82.4786916 191,69.5 C191,56.5213084 201.521308,46 214.5,46 Z M76.5,26 C89.4786916,26 100,36.5213084 100,49.5 C100,62.4786916 89.4786916,73 76.5,73 C63.5213084,73 53,62.4786916 53,49.5 C53,36.5213084 63.5213084,26 76.5,26 Z M173.5,15 C186.478692,15 197,25.5213084 197,38.5 C197,51.4786916 186.478692,62 173.5,62 C160.521308,62 150,51.4786916 150,38.5 C150,25.5213084 160.521308,15 173.5,15 Z M123.5,7 C136.478692,7 147,17.5213084 147,30.5 C147,43.4786916 136.478692,54 123.5,54 C110.521308,54 100,43.4786916 100,30.5 C100,17.5213084 110.521308,7 123.5,7 Z"
99+
id="slots"
100+
stroke="#fff"
101+
fill-opacity="0.5"
102+
fill="#D8D8D8"
103+
style=${styleMap(this.stylesMapping)}
104+
></path>
105+
</g>
106+
<circle id="container" fill-opacity=".5" fill="#070707" cx="132.5" cy="132.5" r="132.5" />
107+
<g class="text" font-family="Marker Felt, monospace" font-size="21" fill="#FFF">
108+
<text @mouseup=${() => this.dial(0)} x="129" y="243">0</text>
109+
<text @mouseup=${() => this.dial(9)} x="78" y="230">9</text>
110+
<text @mouseup=${() => this.dial(8)} x="40" y="194">8</text>
111+
<text @mouseup=${() => this.dial(7)} x="28" y="145">7</text>
112+
<text @mouseup=${() => this.dial(6)} x="35" y="97">6</text>
113+
<text @mouseup=${() => this.dial(5)} x="72" y="58">5</text>
114+
<text @mouseup=${() => this.dial(4)} x="117" y="41">4</text>
115+
<text @mouseup=${() => this.dial(3)} x="168" y="47">3</text>
116+
<text @mouseup=${() => this.dial(2)} x="210" y="79">2</text>
117+
<text @mouseup=${() => this.dial(1)} x="230" y="126">1</text>
118+
</g>
119+
<path
120+
d="M182.738529,211.096297 L177.320119,238.659185 L174.670528,252.137377 L188.487742,252.137377 L182.738529,211.096297 Z"
121+
stroke="#979797"
122+
fill="#D8D8D8"
123+
transform="translate(181.562666, 230.360231) rotate(-22.000000) translate(-181.562666, -230.360231)"
124+
></path>
125+
</svg>
126+
`;
127+
}
128+
}

0 commit comments

Comments
 (0)