<form data-blok="kaoss">
	<fieldset name="xypad" data-blok="xypad">
		<button type="button" name="xyPoint" aria-label="xy-point in coordinate"></button>
	</fieldset>
	<fieldset data-blok="radio-group">
		<label aria-label="sine wave">
			<input type="radio" name="wave" value="sine">
			<svg><use xlink:href="#icon-sine"></use></svg>
		</label>
		<label aria-label="saw wave">
			<input type="radio" name="wave" value="sawtooth" checked>
			<svg><use xlink:href="#icon-saw"></use></svg>
		</label>
		<label aria-label="square wave">
			<input type="radio" name="wave" value="square">
			<svg><use xlink:href="#icon-square"></use></svg>
		</label>
		<label aria-label="triangle wave">
			<input type="radio" name="wave" value="triangle">
			<svg><use xlink:href="#icon-triangle"></use></svg>
		</label>
	</fieldset>
	<fieldset data-blok="radio-group">
		<label aria-label="allpass filter">
			<input type="radio" name="filter" value="allpass">
			<span>allpass</span>
		</label>
		<label aria-label="lowpass filter">
			<input type="radio" name="filter" value="lowpass">
			<span>lowpass</span>
		</label>
		<label aria-label="highpass filter">
			<input type="radio" name="filter" value="highpass" checked>
			<span>highpass</span>
		</label>
		<label aria-label="bandpass filter">
			<input type="radio" name="filter" value="bandpass">
			<span>bandpass</span>
		</label>
	</fieldset>
	<label aria-label="main gain">
		<input type="range" data-blok="range" name="gain" max="1" step="0.05" value="0.5">
	</label>
	<label aria-label="Hue slider">
		<input type="range" data-blok="range" name="hue" max="360" value="180">
	</label>
	<details>
		<summary>KAOSS! v.1.0.0</summary>
		<strong>finger-users:</strong> for best results, drag your finger around the pad, instead of lifting and resting.
		<strong>keyboard-users:</strong> toggle sound with “space”, then use “arrow”-keys. Hold down “shift” to move in larger intervals.
	</details>
</form>

<svg style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
	<title>jsKaoss Icons</title>
	<symbol id="icon-saw" viewBox="0 0 24 24">
		<path d="M3 12h5l4 8v-16l4 8h5" />
	</symbol>
	<symbol id="icon-sine" viewBox="0 0 24 24">
		<path d="M21 12h-2c-.894 0 -1.662 -.857 -1.761 -2c-.296 -3.45 -.749 -6 -2.749 -6s-2.5 3.582 -2.5 8s-.5 8 -2.5 8s-2.452 -2.547 -2.749 -6c-.1 -1.147 -.867 -2 -1.763 -2h-2" />
	</symbol>
	<symbol id="icon-square" viewBox="0 0 24 24">
		<path d="M3 12h5v8h4v-16h4v8h5" />
	</symbol>
	<symbol id="icon-triangle" viewBox="0 0 24 24">
		<path d="M 4 14 L 8 6 L 16 20 L 21 12" />
	</symbol>
</svg>
body { font-family: ui-sans-serif, system-ui, sans-serif; }
[data-blok="xypad"] {
	--bgc: hsl(200, 25%, 85%);
	--thumb-bdc: hsl(200, 35%, 70%);
	--thumb-bdc--active: hsl(200, 35%, 60%);
	--thumb-bdw: 0.5rem;
	--thumb-bgc: hsl(200, 35%, 85%);
	--thumb-bgc--active: hsla(200, 35%, 65%, 0.5);
	--thumb-bxsh-w: .25rem;
	--thumb-w: 4rem;

	aspect-ratio: 1;
	background-color: var(--bgc);
	border: 0;
	margin: 0;
	max-width: var(--maw, 20rem);
	padding: 0;
	position: relative;
	touch-action: none;
	user-select: none;
	width: 100%;
}
[data-blok="xypad"] [name="xyPoint"] {
	background-color: var(--thumb-bgc);
	block-size: var(--thumb-w);
	border: var(--thumb-bdw) solid var(--thumb-bdc);
	border-radius: var(--thumb-bdrs, 50%);
	inline-size: var(--thumb-w);
	outline: none;
	touch-action: none;
	transform: translate3d(calc(1px * var(--tx)), calc(1px * var(--ty)), 0);
	user-select: none;
}
/* STATE */
[name="xyPoint"]:focus-visible {
	box-shadow: 0 0 0 var(--thumb-bxsh-w) var(--thumb-bdc-inner, var(--bgc)), 0 0 0 calc(2 * var(--thumb-bxsh-w)) var(--thumb-bdc--active);
}
[name="xyPoint"]:active {
	--thumb-bdc: var(--thumb-bdc--active);
	--thumb-bgc: var(--thumb-bgc--active);
}
/* Support for browsers, that do not support `aspect-ratio` */
@supports not (aspect-ratio: 1) {
	[data-blok="xypad"]:after {
		content: "";
		display: block;
		padding-bottom: calc(100% - var(--thumb-w));
	}
}
[data-blok="kaoss"] {
	--bdrs: 1.25rem;
	--bgc: hsl(var(--hue), 50%, 50%);
	--c: hsl(var(--hue), 40%, 20%);
	--gap: 0.8rem;
	--h: 0;
	--hue: 180;
	--s: 100%;
	--l: 50%;
	--a: 1;
	background-color: var(--bgc);
	border: 0;
	border-radius: var(--bdrs);
	padding: var(--gap);
	max-width: var(--w, 25rem);
}
[data-blok="kaoss"] [data-blok="xypad"] {
	--bgc: var(--c);
	--maw: auto;
	--thumb-bdc: hsl(var(--hue), 35%, 99%);
	--thumb-bdc--active: hsl(var(--hue), 50%, 60%);
	--thumb-bdc-inner: hsl(0, 0%, 33%);
	--thumb-bgc: transparent;
	--thumb-bgc--active: hsla(var(--hue), 100%, 60%, 0.8);
	border-radius: calc(var(--bdrs) - (var(--gap) / 2));
	margin-block-end: var(--gap);
}
[name="xyPoint"]:active {
	box-shadow: inset 0 0 .75rem 0.25rem hsla(var(--hue), 100%, 95%, 0.65), 0 0 1.5rem 0 hsl(calc(var(--hue) + 10) 100% 40%), 0 0 1.5rem 0 hsl(var(--hue) 100% 30%);
}
[data-blok="kaoss"] details {
	font-family: ui-monospace, monospace;
	font-size: small;
	text-align: center;
}
[data-blok="kaoss"] summary {
	outline: none;
}

[data-blok="kaoss"] [data-blok="radio-group"] {
	--bdc: var(--c);
	--icon-bgc--active: var(--c);
	--icon-c: var(--c);
	margin-block-end: var(--gap);
} 

[data-blok="radio-group"] {
	--bdc: currentColor;
	--bdrs: 0.25rem;
	--bdrs-inner: calc(var(--bdrs) - var(--bdw));
	--bdw: 1px;
	--bg: transparent;
	--fz: 0.675rem;
	--h: 1.5rem;
	--icon-bgc: transparent;
	--icon-bgc--active: #111;
	--icon-c: #111;
	--icon-c--active: #FFF;

	background: var(--bg);
	border: var(--bdw) solid var(--bdc);
	border-radius: var(--bdrs);
	box-sizing: border-box;
	display: flex;
	height: var(--h);
	margin: 0;
	padding: 0;
}
[data-blok="radio-group"] input { 
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	height: 1px;
	left: 0;
	overflow: hidden;
	position: absolute;
	white-space: nowrap;
	width: 1px;
}
[data-blok="radio-group"] input:checked + span,
[data-blok="radio-group"] input:checked + svg {
	--icon-bgc: var(--icon-bgc--active);
	--icon-c: var(--icon-c--active);
}
[data-blok="radio-group"] label {
	flex: 1;
}
[data-blok="radio-group"] label:not(:last-of-type) {
	border-inline-end: var(--bdw) solid var(--bdc);
}
[data-blok="radio-group"] label:first-of-type svg {
	border-end-start-radius: var(--bdrs-inner);
	border-start-start-radius: var(--bdrs-inner);
}
[data-blok="radio-group"] label:last-of-type svg {
	border-end-end-radius: var(--bdrs-inner);
	border-start-end-radius: var(--bdrs-inner);
}
[data-blok="radio-group"] span {
	align-items: center;
	background-color: var(--icon-bgc);
	color: var(--icon-c);
	display: flex;
	font-size: var(--fz);
	height: 100%;
	justify-content: center;
	user-select: none;
}
[data-blok="radio-group"] svg {
	background-color: var(--icon-bgc);
	fill: none;
	height: 100%;
	max-height: calc(var(--h) - (2 * var(--bdw)));
	pointer-events: none;
	stroke: var(--icon-c);
	stroke-linecap: round;
	stroke-linejoin: round;
	stroke-width: 1.5;
	width: 100%;
}

[data-blok="range"],
[data-blok="range"]::-webkit-slider-runnable-track,
[data-blok="range"]::-webkit-slider-thumb {
	-webkit-appearance: none;
	appearance: none;
}
[data-blok="range"] {
	background-color: transparent;
	background-repeat: no-repeat;
	background-position: center;
	background-size: 100% var(--track-h);
	border-radius: var(--bdrs, 0);
	height: var(--thumb-h);
	margin: 0;
	outline: 0;
	pointer-events: none;
	position: relative;
	width: var(--rng-w, inherit);
}
[data-blok="range"]::-moz-range-thumb {
	background: var(--thumb-bgc);
	border: var(--thumb-bdw) solid var(--thumb-bdc);
	border-radius: var(--thumb-bdrs, 50%);
	box-shadow: var(--bxsh, none);
	height: var(--thumb-h);
	pointer-events: auto;
	width: var(--thumb-w);
}
[data-blok="range"]::-webkit-slider-thumb {
	background: var(--thumb-bgc);
	border: var(--thumb-bdw) solid var(--thumb-bdc);
	border-radius: var(--thumb-bdrs, 50%);
	box-shadow: var(--bxsh, none);
	height: var(--thumb-h);
	pointer-events: auto;
	width: var(--thumb-w);
}
[data-blok="range"]:focus-visible {
	--bxsh: 0 0 0 1px #FFF, 0 0 0 4px var(--bgc);
}
[name="gain"],
[name="hue"] {
	--thumb-bdc: white;
	--thumb-bdw: 2px;
	--thumb-h: 1.5rem;
	--thumb-w: 1.5rem;
	--bdrs: .25rem;
	--rng-w: 100%;
	background-image: linear-gradient(to right, var(--c), var(--c));
	margin-block-end: var(--gap);
}
[name="hue"] {
	background-image: linear-gradient(to right,
		hsl(0, 50%, 50%), 
		hsl(30, 50%, 50%), 
		hsl(60, 50%, 50%),
		hsl(90, 50%, 50%),
		hsl(120, 50%, 50%),
		hsl(150, 50%, 50%),
		hsl(180, 50%, 50%),
		hsl(210, 50%, 50%),
		hsl(240, 50%, 50%),
		hsl(270, 50%, 50%),
		hsl(300, 50%, 50%),
		hsl(330, 50%, 50%));
}
/**
 * KAOSS!
 * @version 1.0.02
 * @summary 02-09-2021
 * @author Mads Stoumann
 * @description Web Audio KAOSS!
*/
function KAOSS(app) {
	const AudioContext = window.AudioContext || window.webkitAudioContext;
	const context = new AudioContext();
	const filter = context.createBiquadFilter();
	const gainNode = context.createGain();

	let frequency = 220;
	let oscillator;
	let playing = false;

	gainNode.connect(context.destination);
	gainNode.gain.value = 0.5;
	filter.connect(gainNode);

	app.addEventListener('xydown', down);
	app.addEventListener('xymove', e => move(e.detail.x, e.detail.y));
	app.addEventListener('xytoggle', toggle);
	app.addEventListener('xyup', up);
	app.elements.gain.addEventListener('input', () => gainNode.gain.value = app.elements.gain.valueAsNumber);
	app.elements.hue.addEventListener('input', () => app.style.setProperty('--hue', app.elements.hue.valueAsNumber));
	xyPad(app.elements.xypad, { scope:app, minY:27.5, maxY:440, y:220 });

	function down() {
		filter.type = app.elements.filter.value
		oscillator = context.createOscillator();
		oscillator.connect(filter);
		oscillator.frequency.value = frequency;
		oscillator.type = app.elements.wave.value;
		oscillator.start();
		playing = true;
	}

	function move(x, y) {
		if (playing) {
			frequency = y;
			oscillator.frequency.value = frequency;
			filter.frequency.value = frequency * 4;
			filter.Q.value = (x/4);
		}
	}

	function up() {
		if (playing) oscillator.stop(0);
		playing = false;
	}

	function toggle() {
		if (playing) {
			up();
		} else {
			down();
		}
	}
}

function xyPad(e,t){const n=Object.assign({leave:!0,minX:0,maxX:100,minY:0,maxY:100,scope:e,shift:10,x:50,y:50},t);let i,o,s,r,a,c,y,d,p,f=!1,l=n.x,u=n.y;const v=n.maxX-n.minX,x=n.maxY-n.minY,m=new ResizeObserver(()=>{y=e.getBoundingClientRect(),i=y.width,o=y.height,a=i/v,c=o/x,s=i-h,r=0-h,function(e,t){d=(e-n.minX)*a-h,p=(x-t+n.minY)*c-h}(l,u),g()}),h=e.elements.xyPoint.offsetWidth/2,w=()=>[parseInt(e.style.getPropertyValue("--tx")||0),parseInt(e.style.getPropertyValue("--ty")||0)];function g(e){var t,a;e&&(d=e.clientX-y.x-h,p=e.clientY-y.y-h),d>s&&(d=s),d<r&&(d=r),p>s&&(p=s),p<r&&(p=r),a=p,l=((t=d)+h)*(v/i)+n.minX,u=x-(a+h)*(x/o)+n.minY,b("--tx",t),b("--ty",a),E("xymove",{detail:{x:l,y:u,tx:t,ty:a}})}function E(e,t={}){n.scope.dispatchEvent(new CustomEvent(e,t))}function b(t,n,i=e){i.style.setProperty(t,n)}e.addEventListener("keydown",function(e){const t=e.shiftKey?n.shift*a:1*a,i=e.shiftKey?n.shift*c:1*c;let[o,s]=[...w()];"Tab"!==e.key&&e.preventDefault();switch(e.key){case"ArrowDown":s+=i;break;case"ArrowLeft":o-=t;break;case"ArrowRight":o+=t;break;case"ArrowUp":s-=i;break;case" ":E("xytoggle")}d=o,p=s,g()}),e.addEventListener("pointerdown",function(e){f=!0,E("xydown"),g(e)}),n.leave&&e.addEventListener("pointerleave",()=>e.dispatchEvent(new Event("pointerup"))),e.addEventListener("pointermove",function(e){f&&g(e)}),e.addEventListener("pointerup",function(){f=!1,E("xyup")}),m.observe(e)};

/* INIT */
const elm = document.querySelector(`[data-blok="kaoss"]`);
KAOSS(elm);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.