{
const width = 1000;
const height = width;
const innerRadius = 120;
const middleRadius = 250;
const outerRadius = Math.min(width, height) / 2;
const controlRadius = innerRadius / 2;
const sounds = [
await FileAttachment("Bass.m4a").url(),
await FileAttachment("Drum.m4a").url(),
await FileAttachment("Other.m4a").url(),
await FileAttachment("Piano.m4a").url(),
await FileAttachment("Vocal.m4a").url()
];
const urls = d3.shuffle([...sounds, ...sounds]);
const tracks = urls.map((url) => {
const player = new Tone.Player(url).toDestination();
const meter = new Tone.Meter();
const wave = new Tone.Waveform(256);
meter.normalRange = true;
player.connect(meter);
player.connect(wave);
return {
player,
meter,
wave,
setVolume: (val) => player.volume.rampTo(val)
};
});
let isPlaying = false;
const toggle = () => {
Tone.start();
tracks.forEach(({ player }) => {
if (player.state === "started") {
player.stop();
isPlaying = false;
} else {
player.start();
isPlaying = true;
}
});
};
const colors = ["#DAB55D", "#3A705A"];
const scaleX = d3
.scaleBand()
.domain(urls.map((_, i) => i))
.range([0, 2 * Math.PI])
.align(0);
const scaleY = d3
.scaleRadial()
.domain([0, 1])
.range([middleRadius, outerRadius]);
const scaleColor = d3.scaleOrdinal().domain([0, 1]).range(colors);
const scaleColor2 = d3
.scaleLinear()
.domain([0, urls.length])
.range(colors)
.interpolate(d3.interpolateRgb);
const bars = ({ volumes }) => {
const series = d3
.stack()
.keys(d3.union(volumes.map((d) => d.level)))
.value(([, D], key) => D.get(key).volume)(
d3.index(
volumes,
(d) => d.index,
(d) => d.level
)
);
const arc = d3
.arc()
.innerRadius((d) => scaleY(d[0]))
.outerRadius((d) => scaleY(d[1]))
.startAngle((d) => scaleX(d.data[0]))
.endAngle((d) => scaleX(d.data[0]) + scaleX.bandwidth())
.padAngle(1.5 / middleRadius)
.padRadius(middleRadius);
return series.map((S) => {
const mapped = S.map((d) => ({ d: arc(d) }));
mapped.key = S.key;
return mapped;
});
};
const lines = ({ waveforms }) => {
const series = d3.groups(waveforms, (d) => d.key);
if (series.length === 0) return [];
const scaleX = d3
.scaleLinear()
.domain([0, series[0][1].length])
.range([0, 2 * Math.PI]);
const d = middleRadius - innerRadius;
const scaleY = d3
.scaleLinear()
.domain([-1, 1])
.range([middleRadius - d * 2, middleRadius]);
const scaleStroke = d3.scaleLinear().domain([0, 1]).range([0, 6]);
const scaleOpacity = d3.scaleLinear().domain([0, 1]).range([0.75, 1]);
const line = d3
.lineRadial()
.curve(d3.curveCardinalClosed)
.angle((d) => scaleX(d.index))
.radius((d) => scaleY(d.value));
return series.map(([key, S], i) => {
const extent = d3.extent(S, (d) => d.value);
const diff = Math.abs(extent[1] - extent[0]);
return {
d: line(S),
color: scaleColor2(key),
strokeWidth: scaleStroke(diff),
strokeOpacity: scaleOpacity(diff)
};
});
};
const randomRadius = cm.randomNoise(0, innerRadius / 2);
const randomAngle = cm.randomNoise(0, Math.PI * 2);
let count = 0;
let preX;
let preY;
let offsetX = 0;
let offsetY = 0;
const loop = (state, { frameCount }) => {
state.volumes = tracks.flatMap(({ meter }, i) => [
{
index: i,
level: 0,
volume: meter.getValue() / 2
},
{
index: i,
level: 1,
volume: meter.getValue() / 2
}
]);
state.waveforms = tracks.flatMap(({ wave }, i) => {
const waves = Array.from(wave.getValue());
return waves.map((d, index) => ({ value: d, index, key: i }));
});
const isStop = tracks.every((d) => d.player.state === "stopped");
if (state.isDragging) return;
if (!isPlaying) return;
if (isStop) return;
const radius = randomRadius(count / 100);
const angle = randomAngle(count / 100);
let x = radius * Math.cos(angle) + offsetX;
let y = radius * Math.sin(angle) + offsetY;
[x, y] = constrain(innerRadius / 2, x, y);
state.controlX = x;
state.controlY = y;
count++;
};
const draggable = ({ ondrag, onstart, onend }, nodes) => {
const drag = d3
.drag()
.on("drag", ondrag)
.on("start", onstart)
.on("end", onend);
const node = svg.g({}, nodes);
d3.select(node).call(drag);
return node;
};
const constrain = (r, x, y) => {
const distance = Math.sqrt(x * x + y * y);
if (distance > r) {
const scale = r / distance;
x = x * scale;
y = y * scale;
}
return [x, y];
};
const ondrag = (event) => {
const r = innerRadius / 2;
let { x, y } = event;
[x, y] = constrain(r, x, y);
state.controlX = x;
state.controlY = y;
};
const onstart = () => {
state.isDragging = true;
preX = state.controlX;
preY = state.controlY;
};
const onend = () => {
state.isDragging = false;
offsetX += state.controlX - preX;
offsetY += state.controlY - preY;
};
const updateVolume = (state) => {
const x = state.controlX;
const y = state.controlY;
const r = innerRadius / 2;
for (let i = 0; i < tracks.length; i++) {
const { player } = tracks[i];
const pa = scaleX(i) + scaleX.bandwidth() / 2 - Math.PI / 2;
const pr = innerRadius;
const px = pr * Math.cos(pa);
const py = pr * Math.sin(pa);
const dx = px - x;
const dy = py - y;
const pd = Math.sqrt(dx * dx + dy * dy);
const density = cm.map(pd, r * 2, r * 3, 0, 1);
const clamped = Math.min(1, Math.max(density, 0));
const t = clamped * -60;
player.volume.rampTo(t);
}
};
const [state, dispose] = cm
.flow()
.let("volumes", [])
.let("waveforms", [])
.let("controlX", 0)
.let("controlY", 0)
.let("isDragging", false)
.let("isHovering", false)
.derive("bars", bars)
.derive("lines", lines)
.observe(updateVolume)
.on("loop", loop)
.join();
invalidation.then(() => {
dispose();
tracks.forEach((d) => d.player.stop());
});
await Tone.loaded;
const node = svg.svg(
{
width,
height,
viewBox: [-width / 2, -height / 2, width, height],
style: "width: 100%; height: auto; font: 10px sans-serif;",
onclick: toggle
},
[
svg.rect({ x: -width / 2, y: -height / 2, width, height, fill: "black" }),
draggable({ ondrag, onstart, onend }, [
svg.circle({
cx: cm.$(() => state.controlX),
cy: cm.$(() => state.controlY),
r: controlRadius,
fill: cm.$(() => (state.isDragging ? colors[0] : colors[1])),
style: cm.$(() =>
state.isHovering ? "cursor:pointer" : "cusor:default"
),
onmousemove: () => (state.isHovering = true),
onmouseout: () => (state.isHovering = false)
})
// svg.g(
// {
// transform: cm.$(
// () => `translate(${state.controlX}, ${state.controlY})`
// )
// },
// [
// svg.path({
// d: d3.symbol(d3.symbolStar, controlRadius ** 2)(),
// fill: cm.$(() => (state.isDragging ? colors[0] : colors[1])),
// style: cm.$(() =>
// state.isHovering ? "cursor:pointer" : "cusor:default"
// ),
// onmousemove: () => (state.isHovering = true),
// onmouseout: () => (state.isHovering = false)
// })
// ]
// )
]),
cm.$(() =>
state.bars.map((S) =>
svg.g(
{ fill: scaleColor(S.key) },
S.map(({ d }) => svg.path({ d }))
)
)
),
cm.$(() =>
state.lines.map((d) =>
svg.path({
d: d.d,
stroke: d.color,
fill: "none",
"stroke-width": d.strokeWidth,
"stroke-opacity": d.strokeOpacity
})
)
)
]
);
yield node;
fullscreen(node, { center: true });
}