wam-community
Version:
A collection of prebuilt Web Audio Modules ready for use
327 lines (274 loc) • 8 kB
JavaScript
export default class AutomationTrack extends HTMLElement {
constructor(paramId, param) {
super();
this.paramId = paramId;
this.param = param;
this.param.minValue = Math.max(-128, this.param.minValue);
this.param.maxValue = Math.min(128, this.param.maxValue);
this._root = this.attachShadow({ mode: "open" });
this.body = document.createElement("body");
this.createHeader();
this.createCanvas();
this.setStyle();
this._root.appendChild(this.body);
}
createHeader() {
let header = document.createElement("header");
let title = document.createElement("h2");
title.innerHTML = this.paramId;
let label = document.createElement("label");
label.innerHTML = "Duration (sec):";
label.setAttribute("for", "duration");
this.input = document.createElement("input");
this.input.id = "duration";
this.input.name = "duration";
this.input.type = "number";
this.input.min = 1;
this.input.value = 1;
let apply = document.createElement("button");
apply.innerHTML = "Apply automation";
apply.addEventListener("click", () => this.scheduleEvents());
let remove = document.createElement("button");
remove.id = "remove";
remove.innerHTML = "❌";
remove.addEventListener("click", () => {
window.pluginInfos[this.paramId].track = false;
this.remove();
});
header.appendChild(title);
header.appendChild(label);
header.appendChild(this.input);
header.appendChild(apply);
header.appendChild(remove);
this.body.appendChild(header);
}
scheduleEvents(clear = true) {
let rect = this.canvas.getBoundingClientRect();
let step = rect.width / 100;
let node = window.instance.audioNode;
node.context.resume();
let { currentTime } = node.context;
if (clear) node.clearEvents();
if (this.points.length > 1) {
let x1 = this.points[0].x;
let y1 = this.points[0].y;
for (let i = 1; i < this.points.length; i++) {
let { x: x2, y: y2 } = this.points[i];
let m = (y2 - y1) / (x2 - x1);
let b = y2 - m * x2;
for (let x = x1; x < x2; x += step) {
let y = m * x + b;
let time = this.getTime(x, false);
let value = this.getValue(y, false);
node.scheduleEvents({
type: "wam-automation",
data: { id: this.paramId, value },
time: time + currentTime,
});
}
x1 = this.points[i].x;
y1 = this.points[i].y;
}
} else {
this.canvas.setAttribute("invalid", "");
setTimeout(() => {
this.canvas.removeAttribute("invalid");
}, 500);
}
}
createCanvas() {
this.canvas = document.createElement("canvas");
this.canvas.width = "1500";
this.canvas.height = "200";
this.canvas.oncontextmenu = (e) => {
e.preventDefault();
};
let ctx = this.canvas.getContext("2d");
ctx.fillStyle = "white";
ctx.strokeStyle = "white";
ctx.font = "12px monospace";
this.points = [];
this.canvas.addEventListener("mousedown", (e) => {
e.preventDefault();
let { x, y, type } = this.getMouseData(e);
if (type == 0) {
let newPoint = true;
for (let point of this.points) {
if (point.x > x + 5) break;
if (Math.hypot(point.x - x, point.y - y) < 10) {
this.selected = point;
newPoint = false;
}
}
if (newPoint) {
this.points.push({ x, y });
}
}
if (type == 2) {
for (let point of this.points) {
if (point.x > x + 5) {
break;
}
if (Math.hypot(point.x - x, point.y - y) < 10) {
let index = this.points.indexOf(point);
this.points.splice(index, 1);
break;
}
}
}
this.points.sort((a, b) => a.x - b.x);
});
this.canvas.addEventListener("mousemove", (e) => {
e.preventDefault();
let { x, y } = this.getMouseData(e);
this.mouseX = x;
this.mouseY = y;
if (this.selected) {
this.selected.x = x;
this.selected.y = y;
}
});
const dropped = (e) => {
e.preventDefault();
this.selected = undefined;
this.points.sort((a, b) => a.x - b.x);
};
this.canvas.addEventListener("mouseup", dropped);
this.canvas.addEventListener("mouseleave", (e) => {
dropped(e);
this.mouseX = undefined;
this.mouseY = undefined;
});
const draw = () => {
let rect = this.canvas.getBoundingClientRect();
ctx.clearRect(0, 0, rect.width, rect.height);
let prevX;
let prevY;
for (let { x, y } of this.points) {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
if (prevX != undefined && prevY != undefined) {
ctx.beginPath();
ctx.moveTo(prevX, prevY);
ctx.lineTo(x, y);
ctx.stroke();
}
this.setText(ctx, x, y);
prevX = x;
prevY = y;
}
if (this.mouseX) {
this.setText(ctx, this.mouseX, this.mouseY);
}
window.requestAnimationFrame(draw);
};
window.requestAnimationFrame(draw);
this.body.appendChild(this.canvas);
}
getMouseData(e) {
let rect = this.canvas.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
let type = e.button;
return { x, y, type };
}
getTime(x, fixed = true) {
let rect = this.canvas.getBoundingClientRect();
let time = (x * this.input.value) / rect.width;
return fixed ? time.toFixed(2) : time;
}
getValue(y, fixed = true) {
let rect = this.canvas.getBoundingClientRect();
let value = (y * (this.param.minValue - this.param.maxValue)) / rect.height + this.param.maxValue;
return fixed ? value.toFixed(2) : value;
}
setText(ctx, x, y) {
let rect = this.canvas.getBoundingClientRect();
let firstHalf = `${this.getValue(y)} `;
let secondHalf = `| ${this.getTime(x)}s`;
let mid = ctx.measureText("|").width / 2;
let firstHalfWidth = ctx.measureText(firstHalf).width;
let secondHalfWidth = ctx.measureText(secondHalf).width;
let offsetX = firstHalfWidth + mid;
let offsetY = 12;
if (x + secondHalfWidth > rect.width) {
offsetX += x + secondHalfWidth - rect.width - mid;
}
if (x - firstHalfWidth < rect.x) {
offsetX -= firstHalfWidth + mid - x;
}
if (y - offsetY * 2 < 0) {
offsetY = y - offsetY;
}
ctx.fillText(`${firstHalf}${secondHalf}`, x - offsetX, y - offsetY);
}
setStyle() {
let style = document.createElement("style");
style.innerHTML = `
body {
margin: 0;
width: 1500px;
}
header {
display: flex;
background: black;
color: white;
justify-content: flex-end;
align-items: center;
padding: 0.5rem 0.1rem 0.5rem 1rem;
gap: 0.3rem;
}
h2 {
margin: 0;
margin-right: auto;
max-width: 70%;
}
input {
width: 3rem;
}
input,
button,
label {
flex-shrink: 0;
}
#remove {
background: none;
border: none;
margin-left: 3rem;
font-size: 2rem;
line-height: 0;
padding: 0;
}
canvas {
background: black;
border-top: 5px solid white;
}
canvas[invalid]{
border: 0px solid red;
animation: 0.5s invalid;
margin-top: 5px;
}
invalid {
0% {
border-width: 0px;
}
25% {
border-width: 3px;
}
50% {
border-width: 0px;
}
75% {
border-width: 3px;
}
100% {
border-width: 0px;
}
}
`;
this.body.appendChild(style);
}
}
customElements.define("automation-track", AutomationTrack);