Skip to content

Commit 83f2537

Browse files
authored
feat: add potentiometer element
* Added the potentiometer-element.ts component by [gilf](https://github.com/gilf)
1 parent e348c1c commit 83f2537

File tree

3 files changed

+196
-0
lines changed

3 files changed

+196
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export { NeoPixelElement } from './neopixel-element';
88
export { PushbuttonElement } from './pushbutton-element';
99
export { ResistorElement } from './resistor-element';
1010
export { MembraneKeypadElement } from './membrane-keypad-element';
11+
export { PotentiometerElement } from './potentiometer-element';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { storiesOf } from '@storybook/web-components';
2+
import { action } from '@storybook/addon-actions';
3+
import { html } from 'lit-html';
4+
import './potentiometer-element';
5+
6+
storiesOf('potentiometer', module).add(
7+
'potentiometer',
8+
() =>
9+
html`
10+
<wokwi-potentiometer
11+
min="0"
12+
max="200"
13+
@input=${action('potentiometer-value-changed')}
14+
></wokwi-potentiometer>
15+
`
16+
);

src/potentiometer-element.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { css, customElement, html, LitElement, property } from 'lit-element';
2+
3+
interface Point {
4+
x: number;
5+
y: number;
6+
}
7+
8+
/** The potentiometer SVG is taken from https://freesvg.org/potentiometer and some of the
9+
functions are taken from https://github.com/vitaliy-bobrov/js-rocks knob component */
10+
@customElement('wokwi-potentiometer')
11+
export class PotentiometerElement extends LitElement {
12+
@property() min = 0;
13+
@property() max = 100;
14+
@property() value = 0;
15+
@property() step = 1;
16+
@property() startDegree = -135;
17+
@property() endDegree = 135;
18+
private center: Point = { x: 0, y: 0 };
19+
private pressed = false;
20+
21+
static get styles() {
22+
return css`
23+
#rotating {
24+
transform-origin: 10px 8px;
25+
transform: rotate(var(--knob-angle, 0deg));
26+
}
27+
28+
svg text {
29+
font-size: 1px;
30+
line-height: 1.25;
31+
letter-spacing: 0px;
32+
word-spacing: 0px;
33+
fill: #ffffff;
34+
}
35+
`;
36+
}
37+
38+
clamp(min: number, max: number, value: number): number {
39+
return Math.min(Math.max(value, min), max);
40+
}
41+
42+
mapToMinMax(value: number, min: number, max: number): number {
43+
return value * (max - min) + min;
44+
}
45+
46+
percentFromMinMax(value: number, min: number, max: number): number {
47+
return (value - min) / (max - min);
48+
}
49+
50+
render() {
51+
return html`
52+
<svg
53+
width="20mm"
54+
height="20mm"
55+
version="1.1"
56+
viewBox="0 0 20 20"
57+
xmlns="http://www.w3.org/2000/svg"
58+
@mousedown=${this.down}
59+
@mousemove=${this.move}
60+
@mouseup=${this.up}
61+
@touchstart=${this.down}
62+
@touchmove=${this.move}
63+
@touchend=${this.up}
64+
>
65+
<rect
66+
x=".15"
67+
y=".15"
68+
width="19.5"
69+
height="19.5"
70+
ry="1.23"
71+
fill="#045881"
72+
stroke="#045881"
73+
stroke-width=".30"
74+
/>
75+
<rect x="5.4" y=".70" width="9.1" height="1.9" fill="#ccdae3" stroke-width=".15" />
76+
<ellipse cx="9.91" cy="8.18" rx="7.27" ry="7.43" fill="#e4e8eb" stroke-width=".15" />
77+
<rect
78+
x="6.6"
79+
y="17"
80+
width="6.5"
81+
height="2"
82+
fill-opacity="0"
83+
stroke="#fff"
84+
stroke-width=".30"
85+
/>
86+
<g stroke-width=".15">
87+
<text x="6.21" y="16.6">GND</text>
88+
<text x="8.75" y="16.63">VCC</text>
89+
<text x="11.25" y="16.59">SIG</text>
90+
</g>
91+
<g fill="#fff" stroke-width=".15">
92+
<ellipse cx="1.68" cy="1.81" rx=".99" ry=".96" />
93+
<ellipse cx="1.48" cy="18.37" rx=".99" ry=".96" />
94+
<ellipse cx="17.97" cy="18.47" rx=".99" ry=".96" />
95+
<ellipse cx="18.07" cy="1.91" rx=".99" ry=".96" />
96+
</g>
97+
<g fill="#b3b1b0" stroke-width=".15">
98+
<ellipse cx="7.68" cy="18" rx=".61" ry=".63" />
99+
<ellipse cx="9.75" cy="18" rx=".61" ry=".63" />
100+
<ellipse cx="11.87" cy="18" rx=".61" ry=".63" />
101+
</g>
102+
<ellipse cx="9.95" cy="8.06" rx="6.60" ry="6.58" fill="#c3c2c3" stroke-width=".15" />
103+
<rect id="rotating" x="10" y="2" width=".42" height="3.1" stroke-width=".15" />
104+
</svg>
105+
`;
106+
}
107+
108+
private down(event: MouseEvent) {
109+
if (event.button === 0 || window.navigator.maxTouchPoints) {
110+
this.pressed = true;
111+
this.updatePotentiometerPosition(event);
112+
}
113+
}
114+
115+
private move(event: MouseEvent) {
116+
const { pressed } = this;
117+
if (pressed) {
118+
this.rotateHandler(event);
119+
}
120+
}
121+
122+
private up() {
123+
this.pressed = false;
124+
}
125+
126+
private updatePotentiometerPosition(event: MouseEvent | TouchEvent) {
127+
event.stopPropagation();
128+
event.preventDefault();
129+
130+
const potentiometerRect = this.getBoundingClientRect();
131+
132+
this.center = {
133+
x: window.scrollX + potentiometerRect.left + potentiometerRect.width / 2,
134+
y: window.scrollY + potentiometerRect.top + potentiometerRect.height / 2
135+
};
136+
}
137+
138+
private rotateHandler(event: MouseEvent | TouchEvent) {
139+
event.stopPropagation();
140+
event.preventDefault();
141+
142+
const isTouch = event.type === 'touchmove';
143+
const pageX = isTouch ? (event as TouchEvent).touches[0].pageX : (event as MouseEvent).pageX;
144+
const pageY = isTouch ? (event as TouchEvent).touches[0].pageY : (event as MouseEvent).pageY;
145+
const x = this.center.x - pageX;
146+
const y = this.center.y - pageY;
147+
let deg = Math.round((Math.atan2(y, x) * 180) / Math.PI);
148+
149+
if (deg < 0) {
150+
deg += 360;
151+
}
152+
153+
deg -= 90;
154+
155+
if (x > 0 && y <= 0) {
156+
deg -= 360;
157+
}
158+
159+
deg = this.clamp(this.startDegree, this.endDegree, deg);
160+
const percent = this.percentFromMinMax(deg, this.startDegree, this.endDegree);
161+
const value = this.mapToMinMax(percent, this.min, this.max);
162+
163+
this.updateValue(value);
164+
}
165+
166+
private updatePotentiometerPointer(value: number) {
167+
const percent = this.percentFromMinMax(value, this.min, this.max);
168+
const deg = Math.round((this.endDegree - this.startDegree) * percent + this.startDegree);
169+
this.style.setProperty('--knob-angle', `${deg}deg`);
170+
}
171+
172+
private updateValue(value: number) {
173+
const clamped = this.clamp(this.min, this.max, value);
174+
const updated = Math.round(clamped / this.step) * this.step;
175+
const rounded = Math.round(updated * 100) / 100;
176+
this.updatePotentiometerPointer(rounded);
177+
this.dispatchEvent(new InputEvent('input', { data: `${rounded}` }));
178+
}
179+
}

0 commit comments

Comments
 (0)