Skip to content

Commit

Permalink
fix: svgknob focus state, refactor, docs
Browse files Browse the repository at this point in the history
  • Loading branch information
eye-wave committed Dec 1, 2024
1 parent c58c184 commit 0725520
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 83 deletions.
6 changes: 3 additions & 3 deletions src/lib/ImageKnob.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
step,
acceleration,
maxSpeed,
initialDelay,
defaultValue,
param,
stiffness = 0.5,
Expand All @@ -40,7 +39,7 @@
colors = {}
}: Props = $props();
// TODO rewrite base knob logic
// TODO Refactor
const rotationDegrees = spring(normalize(value, param) * 270 - 135, { stiffness });
function draw() {
Expand All @@ -49,6 +48,7 @@
if (!image) return;
if (!('width' in image && 'height' in image)) return;
// TODO Refactor
const normalized = ($rotationDegrees + 135) / 270;
const i = Math.floor(normalized * numberOfFrames);
const y = image.width * i;
Expand Down Expand Up @@ -125,7 +125,6 @@
{step}
{style}
{unit}
{initialDelay}
bind:value
{rotationDegrees}
>
Expand All @@ -137,6 +136,7 @@
normalizedValue
})}
<canvas
{style}
role="slider"
tabindex="0"
aria-valuenow={normalizedValue}
Expand Down
83 changes: 43 additions & 40 deletions src/lib/KnobBase.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
/** multiplier for acceleration */
acceleration?: number;
maxSpeed?: number;
/** initial delay before acceleration starts (ms) */
initialDelay?: number;
defaultValue?: number | string;
label?: string;
Expand All @@ -43,9 +41,11 @@
import { clamp } from './helpers/clamp.js';
import { normalize, format, unnormalizeToString, unnormalizeToNumber } from './params.js';
import type { EnumParam, FloatParam } from './params.js';
import './shield.css';
type KnobBaseProps = {
ui: Snippet<[SnippetProps]>;
// FIX unwanted knob jiggle
rotationDegrees: Spring<number>;
} & SharedKnobProps;
Expand All @@ -57,9 +57,8 @@
onChange,
value = $bindable(),
step = 0.01,
acceleration = 1.4,
acceleration = 1.2,
maxSpeed = 0.2,
initialDelay = 100,
defaultValue,
param,
rotationDegrees,
Expand All @@ -77,8 +76,6 @@
let startY: number;
let startValue: number;
// This is needed in case some snap value is very close to the min or max range
// preventing the user from selecting that value
function completeFixedSnapValues(snapValues: number[]) {
if (param.type === 'enum-param') return [];
if (snapValues.length < 1) return [];
Expand All @@ -100,32 +97,26 @@
function toMobile(handler: ({ clientY }: MouseEvent) => void | boolean) {
return (event: TouchEvent) => {
const touch = event.touches?.[0];
if (touch === undefined) return;
if (!touch) return;
const clientY = touch.clientY;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
handler({ clientY } as MouseEvent) && event.preventDefault();
};
}
function handleMouseDown({ clientY }: MouseEvent) {
if (!draggable) return;
if (!draggable || isDisabled) return;
isDragging = true;
startY = clientY;
startValue = normalizedValue;
return true;
}
function handleMouseMove({ clientY }: MouseEvent) {
if (!draggable) return;
if (isDisabled) return;
if (!isDragging) return;
if (!draggable || isDisabled || !isDragging) return;
const deltaY = startY - clientY;
const deltaValue = deltaY / 200;
setValue(startValue + deltaValue);
return true;
}
Expand All @@ -136,8 +127,9 @@
function handleDblClick() {
const val =
defaultValue ??
(param as FloatParam)?.range.min ??
(param as FloatParam)?.range?.min ??
(param as EnumParam<string[]>).variants?.[0];
if (val === undefined) return;
setValue(normalize(val, param));
Expand All @@ -148,7 +140,6 @@
type Direction = 'left' | 'right';
let intervalId = -1;
let currentSpeed = step;
const directions: Record<string, Direction> = {
Expand All @@ -158,52 +149,49 @@
ArrowUp: 'right'
};
function adjustValue(direction: Direction) {
const delta = direction === 'right' ? currentSpeed : -currentSpeed;
console.log(direction);
setValue(normalizedValue + delta);
function handleKeyDown(e: KeyboardEvent) {
if (isDisabled || !(e.key in directions)) return;
isDragging = true;
const direction = directions[e.key];
currentSpeed = Math.min(maxSpeed, currentSpeed * acceleration);
}
if (param.type === 'enum-param') {
const i = param.variants.findIndex((v) => v === value) ?? 0;
const step = direction === 'right' ? 1 : -1;
function handleKeyDown(e: KeyboardEvent) {
if (isDisabled) return;
if (!(e.key in directions)) return;
if (intervalId > -1) return;
value = param.variants[clamp(i + step, 0, param.variants.length - 1)];
onChange?.(value);
intervalId = window.setInterval(() => adjustValue(directions[e.key]), initialDelay);
return;
}
const delta = direction === 'right' ? currentSpeed : -currentSpeed;
setValue(normalizedValue + delta);
currentSpeed = Math.min(maxSpeed, currentSpeed * acceleration);
}
function handleKeyUp() {
if (intervalId === -1) return;
window.clearInterval(intervalId);
intervalId = -1;
isDragging = false;
currentSpeed = step;
}
$effect(() => {
rotationDegrees.set(normalizedValue * 270 - 135);
// this was easier in svelte 4 :/
window.addEventListener('touchmove', handleTouchMove, { passive: false });
return () => window.removeEventListener('touchmove', handleTouchMove);
});
function setValue(newNormalizedValue: number) {
if (param.type === 'enum-param') {
const newValue = unnormalizeToString(newNormalizedValue, param);
const newValue = unnormalizeToString(clamp(newNormalizedValue, 0, 1), param);
if (value !== newValue) {
value = newValue;
onChange?.(value);
}
return;
}
let newValue = unnormalizeToNumber(clamp(newNormalizedValue, 0, 1), param);
if (fixedSnapValues.length > 0) {
const nearestSnapValue = fixedSnapValues.reduce((prev, curr) => {
const currNormalized = normalize(curr, param);
Expand All @@ -213,18 +201,34 @@
? curr
: prev;
});
const nearestSnapNormalized = normalize(nearestSnapValue, param);
if (Math.abs(nearestSnapNormalized - newNormalizedValue) <= snapThreshold) {
newValue = nearestSnapValue;
}
}
if (value !== newValue) {
if (isNaN(newValue)) {
newValue = 0;
console.warn('newValue is NaN');
}
value = newValue;
onChange?.(value);
}
}
let shield = document.createElement('div');
$effect(() => {
if (isDragging) {
shield.className = 'shield tf68Uh';
document.body.append(shield);
document.body.style.userSelect = 'none';
} else {
shield.remove();
document.body.style.userSelect = '';
}
});
</script>

<svelte:window
Expand Down Expand Up @@ -259,7 +263,6 @@
span {
user-select: none;
}
.container {
position: relative;
display: flex;
Expand Down
46 changes: 10 additions & 36 deletions src/lib/SvgKnob.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { spring } from 'svelte/motion';
import KnobBase from './KnobBase.svelte';
import type { SharedKnobProps } from './KnobBase.svelte';
import './svgknob.css';
type Props = {
size?: number;
Expand All @@ -28,7 +29,6 @@
step,
acceleration,
maxSpeed,
initialDelay,
defaultValue,
param,
stiffness = 0.5,
Expand Down Expand Up @@ -109,9 +109,8 @@
{snapThreshold}
{snapValues}
{step}
{style}
{unit}
{initialDelay}
{style}
bind:value
>
{#snippet ui({
Expand All @@ -122,16 +121,16 @@
handleKeyDown
})}
<svg
style="--stroke-width: {strokeWidth ?? lineWidth}px"
class={className}
role="slider"
tabindex="0"
aria-valuenow={normalizedValue}
style="--stroke-width:{strokeWidth ?? lineWidth}px;{style}"
class="dK3qx2 {className}"
width={size}
height={size}
viewBox="0 0 {size} {size}"
stroke-linecap="round"
stroke-linejoin="round"
role="slider"
tabindex="0"
aria-valuenow={normalizedValue}
onmousedown={handleMouseDown}
ontouchstart={handleTouchStart}
ondblclick={handleDblClick}
Expand All @@ -144,20 +143,20 @@
{/if}
<!-- Arcs -->
<path
class="line"
class="knob_line"
d={describeArc(center, center, arcRadius, $rotationDegrees, 135)}
stroke={bgColor}
fill="none"
/>
<path
class="line"
class="knob_line"
d={describeArc(center, center, arcRadius, -135, $rotationDegrees)}
stroke={arcColor2}
fill="none"
/>
<!-- Knob indicator -->
<line
class="line"
class="knob_line"
x1={center}
y1={center * 0.7}
x2={center}
Expand All @@ -168,28 +167,3 @@
</svg>
{/snippet}
</KnobBase>

<style>
svg {
outline: none;
}
.line {
transition: stroke-width 200ms;
stroke-width: var(--stroke-width);
}
.focus {
display: none;
}
svg:active .focus,
svg:focus .focus {
display: block;
}
svg:active .line,
svg:focus .line {
stroke-width: calc(var(--stroke-width) * 1.8);
}
</style>
5 changes: 3 additions & 2 deletions src/lib/VideoKnob.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
step,
acceleration,
maxSpeed,
initialDelay,
defaultValue,
param,
stiffness = 0.5,
Expand All @@ -51,12 +50,14 @@
colors = {}
}: Props = $props();
// TODO Refactor
const rotationDegrees = spring(normalize(value, param) * 270 - 135, { stiffness });
const frames: Array<HTMLImageElement> = [];
function draw() {
if (!ctx) return;
// TODO Refactor
const normalized = ($rotationDegrees + 135) / 270;
const i = Math.floor(normalized * numberOfFrames);
if (i < 0) return;
Expand Down Expand Up @@ -209,7 +210,6 @@
{defaultValue}
{disabled}
{draggable}
{initialDelay}
{label}
{maxSpeed}
{onChange}
Expand All @@ -230,6 +230,7 @@
normalizedValue
})}
<canvas
{style}
role="slider"
tabindex="0"
aria-valuenow={normalizedValue}
Expand Down
6 changes: 6 additions & 0 deletions src/lib/shield.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.shield.tf68Uh {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
}
Loading

0 comments on commit 0725520

Please sign in to comment.