Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ea1b98a
Initial commit and art
Spinkelben Apr 19, 2021
d9d7217
Improved visual design. Cleaned up svg
Spinkelben Apr 20, 2021
163d187
Add pins to svg
Spinkelben Apr 20, 2021
ae294b9
Add some interaction
Spinkelben Apr 21, 2021
71792bc
Move tip based on value property
Spinkelben Apr 22, 2021
8337474
WIP on moving tip with mouse
Spinkelben Apr 26, 2021
5d55f55
Implement touch and keyboard actions
Spinkelben Apr 30, 2021
8757130
Add pins and highlight
Spinkelben May 4, 2021
2953368
Improved mouse handling
Spinkelben May 4, 2021
1a109d7
Fix pin numbers
Spinkelben May 4, 2021
7a52041
Add mouse handling outside element
Spinkelben May 16, 2021
e501d39
Change pin names
Spinkelben May 16, 2021
e97d70f
Remove commented out code
Spinkelben May 16, 2021
6c32435
Remove events with no purpose
Spinkelben May 16, 2021
00bf779
Fix slider movement when potentiometer is rotated
Spinkelben May 23, 2021
a612951
Merge branch 'master' of https://github.com/wokwi/wokwi-elements into…
Spinkelben May 23, 2021
727da9d
Fix pin coordinates
Spinkelben May 23, 2021
4a1ce69
PR Comments: Remove extra properties, add portrait story
Spinkelben May 23, 2021
37e730a
Attempt at story where you can change the minValue and maxValue
Spinkelben May 23, 2021
4646419
Make the rotation rotated story configurable
Spinkelben May 23, 2021
8e026c2
Changed range and step
Spinkelben May 23, 2021
0079a50
Clamp function, PR comments
Spinkelben May 24, 2021
526d323
Use clamp module in potentiometer element
Spinkelben May 24, 2021
d97990b
Hookup the input events in the actions tab
Spinkelben May 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export { SlideSwitchElement } from './slide-switch-element';
export { HCSR04Element } from './hc-sr04-element';
export { LCD2004Element } from './lcd2004-element';
export { AnalogJoystickElement } from './analog-joystick-element';
export { SlidePotentiometerElement } from './slide-potentiometer-element';
2 changes: 2 additions & 0 deletions src/react-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SlideSwitchElement } from './slide-switch-element';
import { HCSR04Element } from './hc-sr04-element';
import { LCD2004Element } from './lcd2004-element';
import { AnalogJoystickElement } from './analog-joystick-element';
import { SlidePotentiometerElement } from './slide-potentiometer-element';

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

Expand Down Expand Up @@ -53,6 +54,7 @@ declare global {
'wokwi-hc-sr04': WokwiElement<HCSR04Element>;
'wokwi-lcd2004': WokwiElement<LCD2004Element>;
'wokwi-analog-joystick': WokwiElement<AnalogJoystickElement>;
'wokwi-slide-potentiometer': WokwiElement<SlidePotentiometerElement>;
}
}
}
27 changes: 27 additions & 0 deletions src/slide-potentiometer-element.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { html } from 'lit-html';
import { action } from '@storybook/addon-actions';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused import? or did you want to attach an action to the potentiometer input event?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never became good friends with storybook, what would be the advantage of adding the action on input event? :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means that when you move the potentiometer, you'll see the events firing in the "Actions" tab:

pot-actions.mp4

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Still not 100 % what you get from that, but it is quick to wire up, so I have done it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

It's just a quick way to test that the component emits the events correctly. There's an issue where it doesn't show the detail value of the events, otherwise this would also show users the information contained in events.

import './slide-potentiometer-element';

export default {
title: 'Slide Potentiometer',
component: 'wokwi-slide-potentiometer',
argTypes: {
value: { control: { type: 'number', min: 1, max: 10, step: 1 } },
},
args: {
value: 5,
},
};

const Template = ({ value }) =>
html`<wokwi-slide-potentiometer
value=${value}
@button-press=${action('button-press')}
@button-release=${action('button-release')}
/>`;

export const Default = Template.bind({});
Default.args = { value: 5 };

export const Large = Template.bind({});
Large.args = { value: 10 };
250 changes: 250 additions & 0 deletions src/slide-potentiometer-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import { css, customElement, html, LitElement, property, svg } from 'lit-element';
import { analog, ElementPin } from './pin';

@customElement('wokwi-slide-potentiometer')
export class SlidePotentiometerElement extends LitElement {
@property() value = 0;
@property() minValue = 0;
@property() maxValue = 50;
@property() mouseX = 0;
@property() mouseY = 0;
@property() rotation = 0;
private isPressed = false;
private zoom = 1;
readonly pinInfo: ElementPin[] = [
{ name: 'VCC', x: 17.5, y: 59, number: 1, signals: [{ type: 'power', signal: 'VCC' }] },
{ name: 'SIG', x: 17.5, y: 82.75, number: 2, signals: [analog(0)] },
{ name: 'GND', x: 222.25, y: 59, number: 3, signals: [{ type: 'power', signal: 'GND' }] },
];
private pageToLocalTransformationMatrix: DOMMatrix | null = null;

static get styles() {
return css`
.hide-input {
position: absolute;
clip: rect(0 0 0 0);
width: 1px;
height: 1px;
margin: -1px;
}
input:focus + svg #tip {
/* some style to add when the element has focus */
filter: url(#outline);
}
`;
}

render() {
const { value, minValue, maxValue } = this;
const tipTravelInMM = 30;
// Tip is centered by default
const tipBaseOffsetX = -(tipTravelInMM / 2);
const tipMovementX = (value / (maxValue - minValue)) * tipTravelInMM;
const tipOffSetX = tipMovementX + tipBaseOffsetX;
return html`
<input
tabindex="0"
type="range"
min="${this.minValue}"
max="${this.maxValue}"
value="${this.value}"
step="1"
aria-valuemin="${this.minValue}"
aria-valuenow="${this.value}"
aria-valuemax="${this.maxValue}"
@input="${this.onInputValueChange}"
class="hide-input"
/>
<svg
width="55mm"
height="29mm"
version="1.1"
viewBox="0 0 55 29"
xmlns="http://www.w3.org/2000/svg"
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<defs>
<filter id="outline">
<feDropShadow dx="0" dy="0" stdDeviation="1" flood-color="#4faaff" />
</filter>
<linearGradient
id="tipGradient"
x1="36.482"
x2="50.447"
y1="91.25"
y2="91.25"
gradientTransform="matrix(.8593 0 0 1.1151 -14.849 -92.256)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1a1a1a" offset="0" />
<stop stop-color="#595959" offset=".4" />
<stop stop-color="#595959" offset=".6" />
<stop stop-color="#1a1a1a" offset="1" />
</linearGradient>
<radialGradient
id="bodyGradient"
cx="62.59"
cy="65.437"
r="22.5"
gradientTransform="matrix(1.9295 3.7154e-8 0 .49697 -98.268 -23.02)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#d2d2d2" offset="0" />
<stop stop-color="#2a2a2a" offset="1" />
</radialGradient>
<g id="screw">
<circle cx="0" cy="0" r="1" fill="#858585" stroke="#000" stroke-width=".05" />
<path d="m0 1 0-2" fill="none" stroke="#000" stroke-width=".151" />
</g>
</defs>
<!-- pins -->
<g fill="#ccc">
<rect x="0" y="11" width="5" height="0.75" />
<rect x="50" y="11" width="5" height="0.75" />
<rect x="0" y="17.25" width="5" height="0.75" />
</g>
<g transform="translate(5 5)">
<!-- Body -->
<rect
id="sliderCase"
x="0"
y="5"
width="45"
height="9"
rx=".2"
ry=".2"
fill="url(#bodyGradient)"
fill-rule="evenodd"
/>
<rect x="3.25" y="8" width="38.5" height="3" rx=".1" ry=".1" fill="#3f1e1e" />
<!-- Screw Left -->
<g transform="translate(1.625 9.5) rotate(45)">
<use href="#screw" />
</g>
<!-- Screw Right -->
<g transform="translate(43.375 9.5) rotate(45)">
<use href="#screw" />
</g>
<!-- Tip -->
<g
id="tip"
transform="translate(${tipOffSetX} 0)"
@mousedown=${this.down}
@touchstart=${this.down}
@touchmove=${this.touchMove}
@touchend=${this.up}
@keydown=${this.down}
@keyup=${this.up}
@click="${this.focusInput}"
>
<rect x="19.75" y="8.6" width="5.5" height="1.8" />
<rect
x="16.5"
y="0"
width="12"
height="19"
fill="url(#tipGradient)"
stroke-width="2.6518"
rx=".1"
ry=".1"
/>
<rect x="22.2" y="0" width=".6" height="19" fill="#efefef" />
</g>
</g>
</svg>
`;
}

connectedCallback() {
super.connectedCallback();
window.addEventListener('mouseup', this.up);
window.addEventListener('mousemove', this.mouseMove);
window.addEventListener('mouseleave', this.up);
}

disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('mouseup', this.up);
window.removeEventListener('mousemove', this.mouseMove);
window.removeEventListener('mouseleave', this.up);
}

private focusInput() {
const inputEl: HTMLInputElement | null | undefined = this.shadowRoot?.querySelector(
'.hide-input'
);
inputEl?.focus();
}

private down(): void {
if (!this.isPressed) {
this.updateCaseDisplayProperties();
}
this.isPressed = true;
}

private up = () => {
if (this.isPressed) {
this.isPressed = false;
}
};

private updateCaseDisplayProperties(): void {
const element = this.shadowRoot?.querySelector<SVGRectElement>('#sliderCase');
if (element) {
this.pageToLocalTransformationMatrix = element.getScreenCTM()?.inverse() || null;
}

// Handle zooming in the storybook
const zoom = getComputedStyle(window.document.body)?.zoom;
if (zoom !== undefined) {
this.zoom = Number(zoom);
} else {
this.zoom = 1;
}
}

private onInputValueChange(event: KeyboardEvent): void {
const target = event.target as HTMLInputElement;
if (target.value) {
this.updateValue(Number(target.value));
}
}

private mouseMove = (event: MouseEvent) => {
if (this.isPressed) {
this.updateValueFromXCoordinate(new DOMPointReadOnly(event.pageX, event.pageY));
}
};

private touchMove(event: TouchEvent): void {
if (this.isPressed) {
if (event.targetTouches.length > 0) {
const touchTarget = event.targetTouches[0];
this.updateValueFromXCoordinate(new DOMPointReadOnly(touchTarget.pageX, touchTarget.pageY));
}
}
}

private updateValueFromXCoordinate(position: DOMPointReadOnly): void {
if (this.pageToLocalTransformationMatrix) {
// Handle zoom first, the transformation matrix does not take that into account
let localPosition = new DOMPointReadOnly(position.x / this.zoom, position.y / this.zoom);
// Converts the point from the page coordinate space to the #caseRect coordinate space
// It also translates the units from pixels to millimeters!
localPosition = localPosition.matrixTransform(this.pageToLocalTransformationMatrix);
const caseBorderWidth = 7.5;
const tipPositionXinMM = localPosition.x - caseBorderWidth;
const mmPerIncrement = 30 / (this.maxValue - this.minValue);
this.updateValue(Math.round(tipPositionXinMM / mmPerIncrement));
}
}

private updateValue(value: number) {
let clampedValue = Math.min(value, this.maxValue);
clampedValue = Math.max(clampedValue, this.minValue);
this.value = clampedValue;
this.dispatchEvent(new InputEvent('input', { detail: clampedValue }));
}
}