Welcome to web-knobsv0.0.6
Cross-framework audio component library!
Cross-framework audio component library!
npm i @eyewave/web-knobsyarn add @eyewave/web-knobspnpm add @eyewave/web-knobsbun add @eyewave/web-knobs Basic styled svg knob
<div class="basic-knobs-example">
<div id="basic_knob"></div>
<span id="basic_knob_value"></span>
<div id="basic_knob_disabled"></div>
</div>
<style>
.basic-knobs-example {
color: #22bfee;
}
</style>
<script>
import { DEFAULT_KNOB_VALUE } from '@eyewave/web-knobs/core';
import { createSvgKnob } from '@eyewave/web-knobs/core/components/svg-knob';
declare const basic_knob: HTMLDivElement;
declare const basic_knob_disabled: HTMLDivElement;
declare const basic_knob_value: HTMLSpanElement;
const knob = createSvgKnob(basic_knob_disabled, { disabled: true });
const onChange = (v: number) => {
knob.setValue(v);
basic_knob_value.textContent = v.toFixed(2);
};
onChange(DEFAULT_KNOB_VALUE);
createSvgKnob(basic_knob, { onValueChange: onChange });
</script><script>
import { SvgKnob } from '@eyewave/web-knobs/svelte';
let value = $state(0.0);
</script>
<div>
<SvgKnob bind:value />
<span>{value.toFixed(2)}</span>
<SvgKnob bind:value disabled />
</div>
<style>
div {
color: #22bfee;
}
</style>import React, { useState } from 'react';
import { SvgKnob } from '@eyewave/web-knobs/react';
export default function () {
const [value, setValue] = useState(0.0);
return (
<div style={{ color: '#22bfee' }}>
<SvgKnob value={value} onValueChange={setValue} />
<span>{value.toFixed(2)}</span>
<SvgKnob value={value} disabled />
</div>
);
}<template>
<div>
<SvgKnob v-model="value" />
<span>{{ value.toFixed(2) }}</span>
<SvgKnob v-model="value" :disabled="true" />
</div>
</template>
<script>
import { ref } from 'vue';
import SvgKnob from '@eyewave/web-knobs/vue';
export default {
components: { SvgKnob },
setup() {
const value = ref(0.0);
return { value };
}
};
</script>
<style scoped>
div {
color: #22bfee;
}
</style> You can use an image strip for a knob too.
<div id="image_knob"></div>
<script>
import { createImageKnob } from '@eyewave/web-knobs/core/components/image-knob';
declare const image_knob: HTMLDivElement;
createImageKnob(image_knob, { src: '/web-knobs/PurpleKnob2.webp', width: 90, height: 90 });
</script><script lang="ts">
import { ImageKnob } from '@eyewave/web-knobs/svelte';
let value = $state(0.0);
</script>
<ImageKnob bind:value src="/web-knobs/PurpleKnob2.webp" width={90} height={90} />import React, { useState } from 'react';
import { ImageKnob } from '@eyewave/web-knobs/react';
export default function () {
const [value, setValue] = useState(0.0);
return (
<ImageKnob
value={value}
onValueChange={setValue}
src="/web-knobs/PurpleKnob2.webp"
width={90}
height={90}
/>
);
}<template>
<ImageKnob v-model="value" src="/web-knobs/PurpleKnob2.webp" :width="90" :height="90" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ImageKnob } from '@eyewave/web-knobs/vue';
const value = ref(0.0);
</script> In reality, svgknob and image knob are both based from a <Draggable/> component, that you can use as well, creating your very own custom made knob.
<div id="custom_knob">
<div class="custom-knob">
<div class="glow"></div>
<div class="thumb"></div>
<span></span>
</div>
</div>
<style>
#custom_knob .custom-knob {
z-index: 0;
user-select: none;
width: 60px;
height: 60px;
border-radius: 100px;
background: #000;
position: relative;
display: grid;
place-items: center;
}
#custom_knob .glow,
#custom_knob .thumb {
z-index: -1;
position: absolute;
background: red;
border-radius: 30px;
}
#custom_knob .glow {
width: 30px;
height: 30px;
}
#custom_knob .thumb {
width: 10px;
height: 20px;
}
</style>
<script>
import { createDraggable, DEFAULT_KNOB_VALUE, valueToAngle } from '@eyewave/web-knobs/core';
declare const custom_knob: HTMLDivElement;
const [glow, thumb, span] = custom_knob.children[0].children;
function onChange(value: number) {
const angle = valueToAngle(value, -135, 135);
glow.setAttribute('style', `filter:blur(${value * 5}px);transform:scale(${value})`);
thumb.setAttribute('style', `transform:rotate(${angle}deg) translateY(-10px)`);
span.textContent = value.toFixed(2);
}
onChange(DEFAULT_KNOB_VALUE);
createDraggable(custom_knob, { onValueChange: onChange });
</script><script lang="ts">
import { Draggable } from '@eyewave/web-knobs/svelte';
import { valueToAngle } from '@eyewave/web-knobs/core/helpers';
let value = $state(0.5);
let angle = $derived(valueToAngle(value, -135, 135));
</script>
<Draggable bind:value>
<div class="knob">
<div class="glow" style="filter: blur({value * 5}px);transform: scale({value})"></div>
<div class="thumb" style="transform: rotate({angle}deg) translateY(-10px)"></div>
<span>{value.toFixed(2)}</span>
</div>
</Draggable>
<style>
.knob {
z-index: 0;
user-select: none;
width: 60px;
height: 60px;
border-radius: 100px;
background: #000;
position: relative;
display: grid;
place-items: center;
}
.glow,
.thumb {
z-index: -1;
position: absolute;
background: red;
border-radius: 30px;
}
.glow {
width: 30px;
height: 30px;
}
.thumb {
width: 10px;
height: 20px;
}
</style>import React, { useState, useMemo } from 'react';
import { Draggable } from '@eyewave/web-knobs/react';
import { valueToAngle } from '@eyewave/web-knobs/core/helpers';
import styles from './CustomKnob.module.css';
export default function () {
const [value, setValue] = useState(0.5);
const angle = useMemo(() => valueToAngle(value, -135, 135), [value]);
return (
<Draggable value={value} onValueChange={setValue}>
<div className={styles.knob}>
<div
className={styles.glow}
style={{
filter: `blur(${value * 5}px)`,
transform: `scale(${value})`
}}
></div>
<div
className={styles.thumb}
style={{
transform: `rotate(${angle}deg) translateY(-10px)`
}}
></div>
<span>{value.toFixed(2)}</span>
</div>
</Draggable>
);
}<template>
<KnobDraggable v-model="value">
<div class="knob">
<div
class="glow"
:style="{
filter: `blur(${value * 5}px)`,
transform: `scale(${value})`
}"
></div>
<div
class="thumb"
:style="{
transform: `rotate(${angle}deg) translateY(-10px)`
}"
></div>
<span>{{ value.toFixed(2) }}</span>
</div>
</KnobDraggable>
</template>
<script setup>
import { ref, computed } from 'vue';
import { Draggable as KnobDraggable } from '@eyewave/web-knobs/vue';
import { valueToAngle } from '@eyewave/web-knobs/core/helpers';
const value = ref(0.5);
const angle = computed(() => valueToAngle(value.value, -135, 135));
</script>
<style scoped>
.knob {
z-index: 0;
user-select: none;
width: 60px;
height: 60px;
border-radius: 100px;
background: #000;
position: relative;
display: grid;
place-items: center;
}
.glow,
.thumb {
z-index: -1;
position: absolute;
background: red;
border-radius: 30px;
}
.glow {
width: 30px;
height: 30px;
}
.thumb {
width: 10px;
height: 20px;
}
</style> Since all knobs operate on values between 0 and 1 any type of scaling is done outside of the scope of a knob with parameter objects.
<section id="param-knob">
<div>
<div id="param_knob_freq"></div>
<span id="param_knob_freq_value"></span>
</div>
<div>
<div id="param_knob_lin"></div>
<span id="param_knob_lin_value"></span>
</div>
</section>
<style>
#param-knob section {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
</style>
<script>
import { DEFAULT_KNOB_VALUE } from '@eyewave/web-knobs/core';
import { createSvgKnob } from '@eyewave/web-knobs/core/components/svg-knob';
import { LinearParam, LogParam } from '@eyewave/web-knobs/core/params';
declare const param_knob_freq: HTMLDivElement;
declare const param_knob_freq_value: HTMLSpanElement;
declare const param_knob_lin: HTMLDivElement;
declare const param_knob_lin_value: HTMLSpanElement;
const freqParam = new LogParam(20, 20_000, 10);
const linParam = new LinearParam(0, 100);
let isSyncing = false;
function onChange(v: number) {
param_knob_freq_value.textContent = (freqParam.denormalize(v) | 0) + 'hz';
param_knob_lin_value.textContent = (linParam.denormalize(v) | 0) + '%';
}
onChange(DEFAULT_KNOB_VALUE);
const freqKnob = createSvgKnob(param_knob_freq, {
onValueChange: (v) => {
if (isSyncing) return;
isSyncing = true;
linKnob.setValue(v);
onChange(v);
isSyncing = false;
}
});
const linKnob = createSvgKnob(param_knob_lin, {
onValueChange: (v) => {
if (isSyncing) return;
isSyncing = true;
freqKnob.setValue(v);
onChange(v);
isSyncing = false;
}
});
</script><script lang="ts">
import { LinearParam, LogParam } from '@eyewave/web-knobs/core/params';
import { SvgKnob } from '@eyewave/web-knobs/svelte';
let value = $state(0.0);
const freqParam = new LogParam(20, 20_000, 10);
const linParam = new LinearParam(0, 100);
</script>
<section>
<div>
<SvgKnob bind:value />
<span>{freqParam.denormalize(value) | 0}hz</span>
</div>
<div>
<SvgKnob bind:value />
<span>{linParam.denormalize(value) | 0}%</span>
</div>
</section>
<style>
section {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
</style>import React, { useState } from 'react';
import { SvgKnob } from '@eyewave/web-knobs/react';
import { LinearParam, LogParam } from '@eyewave/web-knobs/core/params';
export default function () {
const [value, setValue] = useState(0.0);
const freqParam = new LogParam(20, 20_000, 10);
const linParam = new LinearParam(0, 100);
return (
<section style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<div>
<SvgKnob value={value} onValueChange={setValue} />
<span>{Math.floor(freqParam.denormalize(value))}hz</span>
</div>
<div>
<SvgKnob value={value} onValueChange={setValue} />
<span>{Math.floor(linParam.denormalize(value))}%</span>
</div>
</section>
);
}<template>
<section>
<div>
<SvgKnob v-model="value" />
<span>{{ Math.floor(freqParam.denormalize(value)) }} Hz</span>
</div>
<div>
<SvgKnob v-model="value" />
<span>{{ Math.floor(linParam.denormalize(value)) }}%</span>
</div>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { LinearParam, LogParam } from '@eyewave/web-knobs/core/params';
import { SvgKnob } from '@eyewave/web-knobs/vue';
const value = ref(0.0);
const freqParam = new LogParam(20, 20000, 10);
const linParam = new LinearParam(0, 100);
</script>
<style scoped>
section {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
</style> You can specify snap points and how strong the snapping is for your knob. The knob will automatically sort and insert 0 and 1 to your snap point list, so [0.6,0.3] will become [0.0,0.3,0.6,1.0].
When snapPoints are specified, arrow keys on the keyboard will make the knob jump between them. Pressing alt key will disable the snapping.
This concept will be importand later in the next example.
<div id="snap_knob"></div>
<span id="snap_knob_value"></span>
<script>
import { createSvgKnob } from '@eyewave/web-knobs/core/components/svg-knob';
declare const snap_knob: HTMLDivElement;
declare const snap_knob_value: HTMLSpanElement;
createSvgKnob(snap_knob, {
snapPoints: [0.5],
onValueChange(v) {
snap_knob_value.textContent = v.toFixed(2);
}
});
</script><script>
import { SvgKnob } from '@eyewave/web-knobs/svelte';
let value = $state(0.0);
</script>
<div>
<SvgKnob bind:value snapPoints={[0.5]} />
<span>{value.toFixed(2)}</span>
</div>import React, { useState } from 'react';
import { SvgKnob } from '@eyewave/web-knobs/react';
export default function SnapKnob() {
const [value, setValue] = useState(0.0);
return (
<div>
<SvgKnob value={value} onValueChange={setValue} snapPoints={[0.5]} />
<span>{value.toFixed(2)}</span>
</div>
);
}<template>
<div>
<SvgKnob v-model="value" :snapPoints="[0.5]" />
<span>{{ value.toFixed(2) }}</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { SvgKnob } from '@eyewave/web-knobs/vue';
const value = ref(0.0);
</script> Enums, or in typescript realm readonly string[] parameter are a special type of parameter that don't denormalize into a number, instead into a string. EnumParam class comes with helpful properties for knob ui with already calcualted snap points and snap threshold to make value changes 'instant'.
<div id="fruit_knob"></div>
<p id="fruit_knob_value"></p>
<div id="filter_knob"></div>
<p id="filter_knob_value"></p>
<div id="bool_knob"></div>
<p id="bool_knob_value"></p>
<script>
import { DEFAULT_KNOB_VALUE } from '@eyewave/web-knobs/core';
import { createSvgKnob, type SvgKnobApi } from '@eyewave/web-knobs/core/components/svg-knob';
import { BoolParam, EnumParam } from '@eyewave/web-knobs/core/params';
declare const fruit_knob: HTMLDivElement;
declare const fruit_knob_value: HTMLParagraphElement;
declare const filter_knob: HTMLDivElement;
declare const filter_knob_value: HTMLParagraphElement;
declare const bool_knob: HTMLDivElement;
declare const bool_knob_value: HTMLParagraphElement;
const fruitParam = new EnumParam(['🍍', '🍉', '🍌', '🍋', '🍇'] as const);
const filterTypeParam = new EnumParam([
'Low pass',
'High pass',
'Low shelf',
'High shelf',
'Bell',
'Notch',
'Allpass'
] as const);
const booleanParam = new BoolParam();
let isSyncing = false;
function onChange(v: number, toUpdate: SvgKnobApi[] = []) {
if (isSyncing) return;
isSyncing = true;
fruit_knob_value.textContent = fruitParam.denormalize(v);
filter_knob_value.textContent = filterTypeParam.denormalize(v);
bool_knob_value.textContent = booleanParam.denormalize(v).toString();
toUpdate.forEach((k) => k.setValue(v));
isSyncing = false;
}
onChange(DEFAULT_KNOB_VALUE);
const fruitKnob = createSvgKnob(fruit_knob, {
snapPoints: fruitParam.snapPoints,
snapThreshold: fruitParam.snapThreshold,
onValueChange: (v) => onChange(v, [boolKnob, filterKnob])
});
const filterKnob = createSvgKnob(filter_knob, {
...filterTypeParam.knobProps,
onValueChange: (v) => onChange(v, [fruitKnob, boolKnob])
});
const boolKnob = createSvgKnob(bool_knob, {
...booleanParam.knobProps,
onValueChange: (v) => onChange(v, [fruitKnob, filterKnob])
});
</script><script lang="ts">
import { BoolParam, EnumParam } from '@eyewave/web-knobs/core/params';
import { SvgKnob } from '@eyewave/web-knobs/svelte';
let value = $state(0);
const fruitParam = new EnumParam(['🍍', '🍉', '🍌', '🍋', '🍇'] as const);
const filterTypeParam = new EnumParam([
'Low pass',
'High pass',
'Low shelf',
'High shelf',
'Bell',
'Notch',
'Allpass'
] as const);
const booleanParam = new BoolParam();
</script>
<SvgKnob bind:value snapPoints={fruitParam.snapPoints} snapThreshold={fruitParam.snapThreshold} />
<p>{fruitParam.denormalize(value)}</p>
<SvgKnob bind:value {...filterTypeParam.knobProps} />
<p>{filterTypeParam.denormalize(value)}</p>
<SvgKnob bind:value {...booleanParam.knobProps} />
<p>{booleanParam.denormalize(value)}</p>import React, { useState } from 'react';
import { SvgKnob } from '@eyewave/web-knobs/react';
import { BoolParam, EnumParam } from '@eyewave/web-knobs/core/params';
export default function () {
const [value, setValue] = useState(0);
const fruitParam = new EnumParam(['🍍', '🍉', '🍌', '🍋', '🍇'] as const);
const filterTypeParam = new EnumParam([
'Low pass',
'High pass',
'Low shelf',
'High shelf',
'Bell',
'Notch',
'Allpass'
] as const);
const booleanParam = new BoolParam();
return (
<div>
<SvgKnob
value={value}
onValueChange={setValue}
snapPoints={fruitParam.snapPoints}
snapThreshold={fruitParam.snapThreshold}
/>
<p>{fruitParam.denormalize(value)}</p>
<SvgKnob value={value} onValueChange={setValue} {...filterTypeParam.knobProps} />
<p>{filterTypeParam.denormalize(value)}</p>
<SvgKnob value={value} onValueChange={setValue} {...booleanParam.knobProps} />
<p>{booleanParam.denormalize(value)}</p>
</div>
);
}<template>
<SvgKnob
v-model="value"
:snapPoints="fruitParam.snapPoints"
:snapThreshold="fruitParam.snapThreshold"
/>
<p>{{ fruitParam.denormalize(value) }}</p>
<SvgKnob v-model="value" v-bind="filterTypeParam.knobProps" />
<p>{{ filterTypeParam.denormalize(value) }}</p>
<SvgKnob v-model="value" v-bind="booleanParam.knobProps" />
<p>{{ booleanParam.denormalize(value) }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BoolParam, EnumParam } from '@eyewave/web-knobs/core/params';
import { SvgKnob } from '@eyewave/web-knobs/vue';
const value = ref(0);
const fruitParam = new EnumParam(['🍍', '🍉', '🍌', '🍋', '🍇'] as const);
const filterTypeParam = new EnumParam([
'Low pass',
'High pass',
'Low shelf',
'High shelf',
'Bell',
'Notch',
'Allpass'
] as const);
const booleanParam = new BoolParam();
</script>