function make_guitar_string() {
let xmin = 0;
let xmax = 1;
let ymin = -1 / 2;
let ymax = 1 / 2;
let w = width < 800 ? width : 800;
let h = w * 0.6;
let pad = 15;
let xScale = d3
.scaleLinear()
.domain([xmin, xmax])
.range([pad, w - pad]);
let yScale = d3
.scaleLinear()
.domain([ymin, ymax])
.range([h - pad, pad]);
let pts_to_path = d3
.line()
.x(function (d) {
return xScale(d[0]);
})
.y(function (d) {
return yScale(d[1]);
});
let svg = d3
.create("svg")
.attr("width", w)
.attr("height", h)
.style("background-color", "white");
svg
.append("path")
.attr("class", "string")
.attr(
"d",
pts_to_path([
[0, 0],
[1, 0]
])
)
.style("stroke", "black")
.style("stroke-width", 0.8)
.style("fill", "none");
svg
.append("circle")
.attr("class", "pick")
.attr("r", 5)
.attr("cx", 0)
.attr("cy", yScale(0))
.attr("fill", "lightgray")
.attr("stroke", "black")
.attr("opacity", 0);
svg
.append("circle")
.attr("class", "pin")
.attr("r", 5)
.attr("cx", xScale(0))
.attr("cy", yScale(0))
.attr("fill", "red")
.attr("stroke", "black");
svg
.append("circle")
.attr("class", "pin")
.attr("r", 5)
.attr("cx", xScale(1))
.attr("cy", yScale(0))
.attr("fill", "red")
.attr("stroke", "black");
let interval_id = 0;
let state = "stopped";
svg
.on("touchmove", (e) => e.preventDefault()) // prevent scrolling
.on("pointermove", function (evt) {
if (state == "stopped") {
if (
Math.abs(yScale.invert(evt.layerY)) <= 0.02 &&
0 < xScale.invert(evt.layerX) &&
xScale.invert(evt.layerX) < 1
) {
svg.select("circle.pick").attr("cx", evt.layerX).attr("opacity", 1);
} else if (
Math.abs(yScale.invert(evt.layerY)) > 0.02 ||
xScale.invert(evt.layerX) < 0 ||
xScale.invert(evt.layerX) > 1
) {
svg.select("circle.pick").attr("opacity", 0);
}
} else if (state == "pluck") {
svg
.select("circle.pick")
.attr("cx", evt.layerX)
.attr("cy", evt.layerY)
.attr("opacity", 1);
svg.select("path.string").attr(
"d",
pts_to_path([
[0, 0],
[xScale.invert(evt.layerX), yScale.invert(evt.layerY)],
[1, 0]
])
);
}
})
.on("pointerdown", function (evt) {
if (state == "stopped" && Math.abs(yScale.invert(evt.layerY)) < 0.02) {
state = "pluck";
}
})
.on("pointerup", function (evt) {
if (state == "pluck") {
state = "vibrating";
svg.select("circle.pick").attr("cy", yScale(0)).attr("opacity", 0);
clearInterval(interval_id);
let i = 0;
interval_id = setInterval(function () {
let pts = build_samples(
(x) =>
u(
yScale.invert(evt.layerY),
xScale.invert(evt.layerX),
x,
i++ / 3000
),
0,
1
);
svg.select("path.string").attr("d", pts_to_path(pts));
// Trails don't look as nice as I might have hoped
// if (i % 10 == 1) {
// svg
// .append("path")
// .attr("class", "trail")
// .attr("d", pts_to_path(pts))
// .style("stroke", "black")
// .style("stroke-width", 0.8)
// .style("fill", "none")
// .style("opacity", 0.2);
// }
if (i > 120000) {
clearInterval(interval_id);
state = "stopped";
}
});
}
});
return svg.node();
}