Skip to content

Commit e8e341a

Browse files
author
Sebastian Schürmann
committed
feature(component-webaudio): a bunch of test components in relation to web audio for a more complex case
1 parent 6d995a2 commit e8e341a

File tree

6 files changed

+289
-0
lines changed

6 files changed

+289
-0
lines changed

packages/component-webaudio/README.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# @vanillin/component-webaudio
2+
3+
A collection of Web Components for creating audio control interfaces. Based on the webaudio-controls project but reimplemented in TypeScript with modern web standards.
4+
5+
## Components
6+
7+
- `webaudio-knob`: Rotary control for parameters
8+
- `webaudio-slider`: Linear slider control
9+
- `webaudio-switch`: Toggle or momentary switch
10+
- `webaudio-param`: Parameter display
11+
12+
## Installation
13+
14+
```bash
15+
npm install @vanillin/component-webaudio
16+
```
17+
18+
## Usage
19+
20+
```html
21+
<script type="module">
22+
import { WebAudioKnob, WebAudioSlider, WebAudioSwitch, WebAudioParam }
23+
from '@vanillin/component-webaudio';
24+
</script>
25+
26+
<webaudio-knob value="50" min="0" max="100"></webaudio-knob>
27+
<webaudio-slider value="50" min="0" max="100"></webaudio-slider>
28+
<webaudio-switch value="0"></webaudio-switch>
29+
<webaudio-param value="50"></webaudio-param>
30+
```
31+
32+
## Development
33+
34+
```bash
35+
# Install dependencies
36+
npm install
37+
38+
# Build the package
39+
npm run build
40+
41+
# Run tests
42+
npm test
43+
```
44+
45+
## License
46+
47+
MIT
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@vanillin/component-webaudio",
3+
"version": "0.1.0",
4+
"description": "WebAudio control components built with Vanillin.js",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"type": "module",
8+
"scripts": {
9+
"test": "node --import tsx --test ./test/*",
10+
"build": "tsc"
11+
},
12+
"keywords": [
13+
"web-components",
14+
"vanillin",
15+
"webaudio",
16+
"audio-controls"
17+
],
18+
"author": "Sebastian Schürmann",
19+
"license": "MIT",
20+
"dependencies": {
21+
"vanillin": "file:../vanillin"
22+
},
23+
"devDependencies": {
24+
"typescript": "5.7.2",
25+
"tsx": "4.7.0"
26+
}
27+
}
+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* WebAudio Knob Control
3+
*/
4+
class WAKnob extends HTMLElement {
5+
private _value: number = 0;
6+
private _min: number = 0;
7+
private _max: number = 127;
8+
private _default: number = 0;
9+
10+
static get observedAttributes() {
11+
return ['value', 'min', 'max', 'default'];
12+
}
13+
14+
constructor() {
15+
super();
16+
this.attachShadow({ mode: 'open' });
17+
this.render();
18+
}
19+
20+
get value() {
21+
return this._value;
22+
}
23+
24+
set value(val: number) {
25+
const newValue = this.constrainValue(val);
26+
if (newValue !== this._value) {
27+
this._value = newValue;
28+
this.setAttribute('value', newValue.toString());
29+
this.render();
30+
}
31+
}
32+
33+
get min() {
34+
return this._min;
35+
}
36+
37+
set min(val: number) {
38+
this._min = val;
39+
this.value = this._value; // Recheck constraints
40+
}
41+
42+
get max() {
43+
return this._max;
44+
}
45+
46+
set max(val: number) {
47+
this._max = val;
48+
this.value = this._value; // Recheck constraints
49+
}
50+
51+
get default() {
52+
return this._default;
53+
}
54+
55+
set default(val: number) {
56+
this._default = val;
57+
if (this._value === 0) {
58+
this.value = val;
59+
}
60+
}
61+
62+
private constrainValue(val: number): number {
63+
return Math.min(this._max, Math.max(this._min, val));
64+
}
65+
66+
private render() {
67+
if (!this.shadowRoot) return;
68+
this.shadowRoot.innerHTML = `
69+
<div>Value: ${this._value} (Min: ${this._min}, Max: ${this._max})</div>
70+
`;
71+
}
72+
73+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
74+
if (oldValue === newValue) return;
75+
76+
switch (name) {
77+
case 'value': {
78+
const val = parseFloat(newValue || '0');
79+
const constrained = this.constrainValue(val);
80+
this._value = constrained;
81+
this.setAttribute('value', constrained.toString());
82+
this.render();
83+
break;
84+
}
85+
case 'min':
86+
this.min = parseFloat(newValue || '0');
87+
break;
88+
case 'max':
89+
this.max = parseFloat(newValue || '127');
90+
break;
91+
case 'default':
92+
this.default = parseFloat(newValue || '0');
93+
break;
94+
}
95+
}
96+
97+
connectedCallback() {
98+
// Set to default value if no value was specified
99+
if (this._value === 0 && this._default !== 0) {
100+
this.value = this._default;
101+
}
102+
this.setAttribute('value', this._value.toString());
103+
this.setAttribute('default', this._default.toString());
104+
this.setAttribute('min', this._min.toString());
105+
this.setAttribute('max', this._max.toString());
106+
this.render();
107+
}
108+
}
109+
110+
customElements.define('wa-knob', WAKnob);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, it, before, after } from 'node:test';
2+
import assert from 'node:assert';
3+
import { resolve, dirname } from 'path';
4+
import { fileURLToPath } from 'url';
5+
import { TestHelper, MountContext } from 'vanillin';
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = dirname(__filename);
9+
10+
const widgetPath = resolve(__dirname, '../src/wa-knob.ts');
11+
12+
describe('wa-knob defaults', () => {
13+
let mountContext: MountContext;
14+
let widget: HTMLElement;
15+
16+
before(async () => {
17+
const helper = new TestHelper();
18+
mountContext = await helper.compileAndMountAsScript('wa-knob', widgetPath);
19+
const { document } = mountContext;
20+
widget = document.querySelector('wa-knob')!;
21+
});
22+
23+
after(() => {
24+
mountContext.jsdom.window.close();
25+
});
26+
27+
it('should create a knob element', () => {
28+
assert.ok(widget);
29+
});
30+
31+
it('initially value is the default', () => {
32+
assert.equal(widget.getAttribute('value'), '0');
33+
});
34+
35+
it('initially min is the 0', () => {
36+
assert.equal(widget.getAttribute('min'), '0');
37+
});
38+
39+
it('initially max is the 127', () => {
40+
assert.equal(widget.getAttribute('max'), '127');
41+
});
42+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, it, before, after } from 'node:test';
2+
import assert from 'node:assert';
3+
import { resolve, dirname } from 'path';
4+
import { fileURLToPath } from 'url';
5+
import { TestHelper, MountContext } from 'vanillin';
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = dirname(__filename);
9+
10+
const widgetPath = resolve(__dirname, '../src/wa-knob.ts');
11+
12+
describe('wa-knob min/max', () => {
13+
let mountContext: MountContext;
14+
let widget: HTMLElement;
15+
16+
before(async () => {
17+
const helper = new TestHelper();
18+
mountContext = await helper.compileAndMountAsScript('wa-knob', widgetPath);
19+
const { document } = mountContext;
20+
widget = document.querySelector('wa-knob')!;
21+
});
22+
23+
after(() => {
24+
mountContext.jsdom.window.close();
25+
});
26+
27+
it('setting below min gets you a result of min', () => {
28+
widget.setAttribute('value', '-1');
29+
const value = widget.getAttribute('value');
30+
assert.strictEqual(value, '0');
31+
});
32+
33+
it('setting above max gets you a result of max', () => {
34+
widget.setAttribute('value', '200');
35+
const value = widget.getAttribute('value');
36+
assert.strictEqual(value, '127');
37+
});
38+
39+
it('you can set max', () => {
40+
widget.setAttribute('max', '200');
41+
widget.setAttribute('value', '200');
42+
const value = widget.getAttribute('value');
43+
assert.strictEqual(value, '200');
44+
});
45+
});
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"outDir": "./dist",
4+
"rootDir": "./src",
5+
"module": "ES2022",
6+
"target": "ES2022",
7+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
8+
"strict": true,
9+
"esModuleInterop": true,
10+
"skipLibCheck": true,
11+
"forceConsistentCasingInFileNames": true,
12+
"moduleResolution": "node",
13+
"declaration": true,
14+
"experimentalDecorators": true
15+
},
16+
"include": ["src/**/*"],
17+
"exclude": ["node_modules", "test", "dist"]
18+
}

0 commit comments

Comments
 (0)