Spaces:
Sleeping
Sleeping
| BSpline.prototype.getPlotlyCurve = function (nPoints) { | |
| let x = [], y = [] | |
| let delta = 1 / nPoints | |
| let domain = { l: this.knots[this.degree], h: this.knots[this.knots.length - 1 - this.degree] } | |
| let pL = this.getValue(domain.l) | |
| x.push(pL.x) | |
| y.push(pL.y) | |
| for (let i = 0; i <= nPoints; i++) { | |
| let t = i * delta | |
| if (t <= domain.l || t >= domain.h) | |
| continue | |
| let p = this.getValue(t) | |
| x.push(p.x) | |
| y.push(p.y) | |
| } | |
| x.push(pL.x) | |
| y.push(pL.y) | |
| return { "x": x, "y": y } | |
| }; | |
| BSpline.prototype.getPlotlyControlPoints = function () { | |
| let x = [], y = [] | |
| for (let i = 0; i < this.controlPoints.length - this.degree; i++) { | |
| x.push(this.controlPoints[i].x) | |
| y.push(this.controlPoints[i].y) | |
| } | |
| return { "x": x, "y": y } | |
| } | |
| function drawControlPoints(canvas) { | |
| let ctx = canvas.getContext("2d") | |
| for (let i = 1; i < this.controlPoints.length - 1; i++) { | |
| ctx.beginPath() | |
| let center = this.controlPoints[i] | |
| ctx.arc(center.x, center.y, 10, 0, 2 * Math.PI) | |
| ctx.strokeStyle = (i == 1 || i == this.controlPoints.length - 2) ? "blue" : "red"; | |
| ctx.lineWidth = 2 | |
| ctx.stroke() | |
| } | |
| }; | |
| function draw(canvas, nPoints) { | |
| let ctx = canvas.getContext("2d") | |
| ctx.lineWidth = 5 | |
| ctx.beginPath() | |
| let delta = 1 / nPoints | |
| let curP = this.getValue(0) | |
| ctx.moveTo(curP.x, curP.y) | |
| for (let i = 1; i <= nPoints; i++) { | |
| curP = this.getValue(i * delta) | |
| ctx.lineTo(curP.x, curP.y) | |
| ctx.strokeStyle = "black" | |
| ctx.lineCap = "round" | |
| ctx.stroke() | |
| } | |
| if (this.closed) { | |
| ctx.fillStyle = 'grey' | |
| ctx.fill() | |
| } | |
| }; | |
| function denormalize(val, range) { | |
| return val * (range[1] - range[0]) + range[0] | |
| } | |
| function clamp(v, l, h) { | |
| return Math.min(Math.max(v, l), h) | |
| } | |
| function getPlotPixelSize(div) { | |
| let clipRect = div._fullLayout._plots.xy.clipRect[0][0] | |
| return { "x": clipRect.width.baseVal.value, "y": clipRect.height.baseVal.value } | |
| } | |
| function getLayerPos(div, eventData) { | |
| let rect = div.getBoundingClientRect() | |
| let margins = div._fullLayout.margin | |
| return { | |
| "x": eventData.clientX - rect.x - margins.l + window.scrollX, | |
| "y": eventData.clientY - rect.y - margins.t + window.scrollY | |
| } | |
| } | |
| function getPlotPos(div, eventData) { | |
| let plotPos = getLayerPos(div, eventData) | |
| let plotSize = getPlotPixelSize(div) | |
| let x = plotPos.x / plotSize.x | |
| x = clamp(x, 0, 1) | |
| let y = 1 - plotPos.y / plotSize.y | |
| y = clamp(y, 0, 1) | |
| return { | |
| "x": denormalize(x, div._fullLayout.xaxis.range), | |
| "y": denormalize(y, div._fullLayout.yaxis.range) | |
| } | |
| } | |
| function isInsidePlot(div, eventData) { | |
| let docPos = { | |
| "x": div.getBoundingClientRect().x + window.scrollX, | |
| "y": div.getBoundingClientRect().y + window.scrollY | |
| } | |
| let margins = div._fullLayout.margin | |
| let localX = eventData.clientX - docPos.x | |
| let localY = eventData.clientY - docPos.y | |
| let plotSize = getPlotPixelSize(div) | |
| return localX > margins.l && localX < margins.l + plotSize.x && localY > margins.t && localY < margins.t + plotSize.y | |
| } | |
| class CurveEditor { | |
| constructor(div, curve, selectThreshold) { | |
| this.div = div | |
| this.curve = curve | |
| this.selectThreshold = selectThreshold | |
| this.enabled = true | |
| this.moving = false | |
| this.movingId = false | |
| this.isDeleteKeyDown = false | |
| this.offset = { "x": 0, "y": 0 } | |
| this.handler = { onupdate: () => { } } | |
| window.focus() | |
| this.div.ownerDocument.addEventListener("keydown", (event) => { | |
| if (event.key == "Control") | |
| this.isDeleteKeyDown = true | |
| }) | |
| this.div.ownerDocument.addEventListener("keyup", (event) => { | |
| if (event.key == "Control") | |
| this.isDeleteKeyDown = false | |
| }) | |
| this.div.ownerDocument.addEventListener("pointerdown", (evt) => { | |
| if (!isInsidePlot(this.div, evt) || !this.enabled) return | |
| let clickedPoint = getPlotPos(div, evt); | |
| this.curve.controlPoints.forEach((p, i) => { | |
| if (this.isWithinThreshold(clickedPoint, p) && this.isSelectable(i)) { | |
| this.moving = true; | |
| this.movingId = i; | |
| this.offset.x = clickedPoint.x - p.x | |
| this.offset.y = clickedPoint.y - p.y | |
| } | |
| }) | |
| }); | |
| this.div.ownerDocument.addEventListener("pointermove", (evt) => { | |
| let found = false; | |
| let mousePoint = getPlotPos(this.div, evt) | |
| this.curve.controlPoints.forEach((p, i) => { | |
| if (this.isWithinThreshold(mousePoint, p) && this.isSelectable(i)) | |
| found = found || true; | |
| }); | |
| this.div.style.cursor = found ? "pointer" : "default"; | |
| if (!this.moving) | |
| return | |
| this.curve.moveControlPoint(this.movingId, mousePoint.x - this.offset.x, mousePoint.y - this.offset.y) | |
| this.onupdate(); | |
| }); | |
| this.div.ownerDocument.addEventListener("pointerup", (evt) => { | |
| if (!this.moving) return; | |
| this.moving = false; | |
| }); | |
| this.div.ownerDocument.addEventListener('contextmenu', (event) => { | |
| if (!isInsidePlot(this.div, event) || !this.enabled) return | |
| event.preventDefault(); | |
| let clickedPoint = getPlotPos(this.div, event) | |
| if (this.isDeleteKeyDown) { | |
| if (this.curve.controlPoints.length - this.curve.degree == 3) | |
| return | |
| this.deletePoint(clickedPoint) | |
| } | |
| else | |
| this.insertPoint(clickedPoint) | |
| this.onupdate(); | |
| }); | |
| } | |
| deletePoint(clickedPoint) { | |
| let minDist = Number.MAX_VALUE | |
| let minId = 0 | |
| let nControls = this.curve.closed ? this.curve.controlPoints.length - this.curve.degree : this.curve.controlPoints.length | |
| for (let i = 0; i < nControls; i++) { | |
| let p = this.curve.controlPoints[i] | |
| let dist = this.getDistance(clickedPoint, p) | |
| if (dist < minDist) { | |
| minDist = dist | |
| minId = i | |
| } | |
| } | |
| this.curve.deleteControlPoint(minId) | |
| } | |
| insertPoint(clickedPoint) { | |
| let minDist = Number.MAX_VALUE | |
| let minId = 0 | |
| let nControls = this.curve.closed ? this.curve.controlPoints.length - this.curve.degree : this.curve.controlPoints.length | |
| for (let i = 0; i < nControls; i++) { | |
| let p = this.curve.controlPoints[i] | |
| let dist = this.getDistance(clickedPoint, p) | |
| if (dist < minDist) { | |
| minDist = dist | |
| minId = i | |
| } | |
| } | |
| let nextP = this.curve.controlPoints[(minId + 1) % nControls] | |
| let prevP = this.curve.controlPoints[(minId - 1 + nControls) % nControls] | |
| let minP = this.curve.controlPoints[minId] | |
| let nextVec = { x: nextP.x - minP.x, y: nextP.y - minP.y } | |
| let prevVec = { x: minP.x - prevP.x, y: minP.y - prevP.y } | |
| let halfVec = { x: (nextVec.x + prevVec.x) / 2, y: (nextVec.y + prevVec.y) / 2 } | |
| let clickedVec = { x: clickedPoint.x - minP.x, y: clickedPoint.y - minP.y } | |
| let dot = halfVec.x * clickedVec.x + halfVec.y * clickedVec.y | |
| if (dot > 0) | |
| minId = (minId + 1) % nControls | |
| this.curve.addControlPoint(new Point(clickedPoint.x, clickedPoint.y), minId); | |
| } | |
| getDistance(p1, p2) { | |
| return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)) | |
| } | |
| isWithinThreshold(pointer, tgt) { | |
| return Math.abs(pointer.x - tgt.x) < this.selectThreshold && Math.abs(pointer.y - tgt.y) < this.selectThreshold | |
| } | |
| isSelectable(i) { | |
| return i < this.curve.controlPoints.length - this.curve.degree | |
| } | |
| } |