Skip to content

Commit 38c19ac

Browse files
committed
feat: image knob
1 parent c7859a2 commit 38c19ac

File tree

11 files changed

+700
-226
lines changed

11 files changed

+700
-226
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "svelte-knobs",
33
"description": "Svelte component library for building customizable knob controls.",
4-
"version": "0.3.0",
4+
"version": "0.3.1",
55
"repository": {
66
"url": "https://github.com/eye-wave/svelte-knobs"
77
},

src/lib/ImageKnob.svelte

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<script lang="ts">
2+
import { normalize } from './params.js';
3+
import { onMount } from 'svelte';
4+
import { spring } from 'svelte/motion';
5+
import KnobBase from './KnobBase.svelte';
6+
import type { SharedKnobProps } from './KnobBase.svelte';
7+
8+
type Props = {
9+
stiffness?: number;
10+
class?: string;
11+
source: string;
12+
numberOfFrames?: number;
13+
width?: number;
14+
height?: number;
15+
} & SharedKnobProps;
16+
17+
let {
18+
style,
19+
source,
20+
numberOfFrames = $bindable(),
21+
class: className,
22+
label = '',
23+
unit = '',
24+
onChange,
25+
value = $bindable(),
26+
defaultValue,
27+
param,
28+
stiffness = 0.5,
29+
decimalDigits = 0,
30+
snapValues = [],
31+
snapThreshold = 0.1,
32+
width = 80,
33+
height = 80,
34+
disabled = false,
35+
draggable = true,
36+
colors = {}
37+
}: Props = $props();
38+
39+
const rotationDegrees = spring(normalize(value, param) * 270 - 135, { stiffness });
40+
41+
function draw() {
42+
if (!ctx) return;
43+
if (!numberOfFrames) return;
44+
if (!image) return;
45+
if (!('width' in image && 'height' in image)) return;
46+
47+
const normalized = ($rotationDegrees + 135) / 270;
48+
const i = Math.floor(normalized * numberOfFrames);
49+
const y = image.width * i;
50+
51+
if (i < 0) return;
52+
if (y >= image.height) return;
53+
54+
ctx.clearRect(0, 0, canvas.width, canvas.height);
55+
ctx.drawImage(
56+
image,
57+
0, // sx
58+
y, // sy
59+
image.width, // sWidth
60+
image.width, // sHeight
61+
0, // dx
62+
0, // dy
63+
canvas.width, // dWidth
64+
canvas.height // dHeight
65+
);
66+
}
67+
68+
onMount(() => {
69+
ctx = canvas.getContext('2d');
70+
ctx?.fillText('Loading...', 0, canvas.height / 2);
71+
});
72+
73+
onMount(() => {
74+
if (!source) return;
75+
76+
console.time('load image');
77+
78+
image = new Image();
79+
image.src = source;
80+
image.onload = () => {
81+
if (!image) throw Error('What');
82+
if (numberOfFrames === undefined) {
83+
if ('width' in image && 'height' in image) {
84+
console.warn('Automatic estimation of numberOfFrames might be inaccurate');
85+
numberOfFrames = Math.floor(image.height / image.width);
86+
} else {
87+
throw Error('Failed to estimate numberOfFrames');
88+
}
89+
}
90+
91+
console.timeEnd('load image');
92+
isLoading = false;
93+
};
94+
});
95+
96+
$effect(() => {
97+
if (isLoading) return;
98+
draw();
99+
});
100+
101+
let canvas: HTMLCanvasElement;
102+
let ctx: CanvasRenderingContext2D | null = null;
103+
let isLoading = $state(true);
104+
let image: HTMLImageElement | null = null;
105+
</script>
106+
107+
<KnobBase
108+
{colors}
109+
{decimalDigits}
110+
{defaultValue}
111+
{disabled}
112+
{draggable}
113+
{label}
114+
{onChange}
115+
{param}
116+
{snapThreshold}
117+
{snapValues}
118+
{style}
119+
{unit}
120+
bind:value
121+
{rotationDegrees}
122+
>
123+
{#snippet ui({ handleTouchStart, handleMouseDown, handleDblClick })}
124+
<canvas
125+
bind:this={canvas}
126+
{width}
127+
{height}
128+
class={className}
129+
onmousedown={handleMouseDown}
130+
ontouchstart={handleTouchStart}
131+
ondblclick={handleDblClick}
132+
oncontextmenu={(e) => e.preventDefault()}
133+
draggable={false}
134+
>
135+
</canvas>
136+
{/snippet}
137+
</KnobBase>

0 commit comments

Comments
 (0)