class TFInput {
constructor(width, height, options = {}) {
let {
xdomain = [-1, 1],
palette = ["white", "orange", "green", "cyan", "magenta"],
values = []
} = options;
if (!TFInput.sanityCheck(values, xdomain)) throw "Bad values";
this.container = htl.html`<div>`;
this.colorInput = new paletteColorInput(palette);
this.container.append(this.colorInput);
this.plotContainer = htl.html`<div>`;
this.container.append(this.plotContainer);
Object.assign(this.container.style, { width, height });
Object.assign(this, { width, height, xdomain, palette, values });
this.draw();
this.callbackSetup();
}
selectAnchor(x, y) {
let selected = null;
let n = this.values.length;
let item = (i) => i % n;
let type = (i) => (i < n ? "x1" : i < 2 * n ? "x2" : "y");
this.anchors().forEach((d, i) => {
if (Math.abs(d.cx - x) <= 5 && Math.abs(d.cy - y) <= 5) {
selected = { item: item(i), type: type(i) };
}
});
this.selected = selected;
return selected;
}
callbackSetup() {
this.plotContainer.onmousedown = (e) => {
const [x, y] = [e.offsetX, e.offsetY];
this.selectAnchor(x, y);
if (!this.selected) {
let newvalue = {
x1: this.plot.scale("x").invert(x - 1),
x2: this.plot.scale("x").invert(x + 1),
y: this.plot.scale("y").invert(y),
color: this.colorInput.value
};
if (TFInput.sanityCheck([...this.values, newvalue], this.xdomain)) {
this.values.push(newvalue);
this.draw();
}
} else {
if (this.selected.type == "y") this.plotContainer.style.cursor = "move";
else this.plotContainer.style.cursor = "col-resize";
}
};
this.plotContainer.onmouseup = (e) => {
//this.plotContainer.releasePointerCapture(e.pointerId);
this.selected = null;
this.plotContainer.style.cursor = "default";
};
this.plotContainer.onmousemove = (e) => {
let [x, y] = [e.offsetX, e.offsetY];
if (this.selected) {
let { item, type } = this.selected;
if (type == "x1" || type == "x2") {
let newx = this.plot.scale("x").invert(x);
let old = Object.assign({}, this.values[item]);
this.values[item][type] = newx;
if (!TFInput.sanityCheck(this.values, this.xdomain))
this.values[item] = old;
this.draw();
} else if (type == "y") {
let newy = this.plot.scale("y").invert(y);
if (newy <= 0) {
this.values.splice(item, 1);
this.selected = null;
} else {
let old = Object.assign({}, this.values[item]);
this.values[item].y = newy;
let dx = old.x2 - old.x1;
let px = this.plot.scale("x").invert(x);
this.values[item].x1 = px - dx / 2;
this.values[item].x2 = px + dx / 2;
if (!TFInput.sanityCheck(this.values, this.xdomain))
this.values[item] = old;
}
this.draw();
}
} else {
let newvalue = {
x1: this.plot.scale("x").invert(x - 1),
x2: this.plot.scale("x").invert(x + 1),
y: this.plot.scale("y").invert(y),
color: this.colorInput.value
};
if (TFInput.sanityCheck([...this.values, newvalue], this.xdomain)) {
this.plotContainer.style.cursor = "pointer";
} else {
this.plotContainer.style.cursor = "default";
}
}
};
}
draw() {
let { xdomain, values, width, height } = this;
this.container.value = values;
this.container.dispatchEvent(new CustomEvent("input"));
this.plot = Plot.plot({
width,
height,
margin: 40,
style: {
userSelect: "none",
background: "black",
color: "white",
fontSize: "14px"
},
x: {
round: true,
grid: true,
domain: xdomain,
label: "Field value →"
},
y: {
grid: true,
label: "↑ alpha",
domain: [0, 1]
},
color: {
domain: [0, 1, 2, 3, 4],
range: this.palette
},
marks: [
Plot.rectY(values, {
x1: "x1",
x2: "x2",
y: "y",
fill: "color"
}),
Plot.dot(values, {
x: "x1",
y: 0,
r: 5,
stroke: "gray",
fill: "white"
}),
Plot.dot(values, {
x: "x2",
y: 0,
r: 5,
stroke: "gray",
fill: "white"
}),
Plot.dot(values, {
x: (d) => (d.x1 + d.x2) / 2,
y: "y",
r: 5,
stroke: "gray",
fill: "white"
}),
Plot.ruleY([0])
]
});
this.plotContainer.innerHTML = "";
this.plotContainer.append(this.plot);
}
anchors() {
let result = [];
d3.select(this.plot)
.selectAll("circle")
.each(function (d, i) {
const sel = d3.select(this);
result.push({ cx: sel.attr("cx"), cy: sel.attr("cy") });
});
return result;
}
static sanityCheck(values, xdomain = [-1, 1]) {
let data = [...values].sort((a, b) => a.x1 - b.x1);
const n = data.length;
for (let i = 0; i < n; i++) {
let { x1, x2, y } = data[i];
if (x1 < xdomain[0] || x2 > xdomain[1]) return false;
if (x1 >= x2) return false;
if (i + 1 < n && x2 >= data[i + 1].x1) return false;
if (y < 0 || y > 1) return false;
}
return true;
}
}