UNPKG

@giro3d/function-curve-editor

Version:
1,238 lines 46.6 kB
import { nextTick } from "./Utils.js"; import { createInterpolatorWithFallback } from "commons-math-interpolation"; import * as DialogManager from "dialog-manager"; class PointUtils { static clone(p) { return { x: p.x, y: p.y }; } static computeDistance(point1, point2) { const dx = point1.x - point2.x; const dy = point1.y - point2.y; return Math.sqrt(dx * dx + dy * dy); } static computeCenter(point1, point2) { return { x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2 }; } static mapPointIndex(points1, points2, pointIndex) { if (pointIndex == undefined) { return; } const point = points1[pointIndex]; return PointUtils.findPoint(points2, point); } static findPoint(points, point) { if (!point) { return; } const i = points.indexOf(point); return (i >= 0) ? i : undefined; } static makeXValsStrictMonotonic(points) { for (let i = 1; i < points.length; i++) { if (points[i].x <= points[i - 1].x) { points[i].x = points[i - 1].x + 1E-6; } } } static dumpPoints(points) { for (let i = 0; i < points.length; i++) { console.log("[" + i + "] = (" + points[i].x + ", " + points[i].y + ")"); } } static encodeCoordinateList(points) { let s = ""; for (const point of points) { if (s.length > 0) { s += ", "; } s += "[" + formatCoordinateValue(point.x) + ", " + formatCoordinateValue(point.y) + "]"; } return s; } static decodeCoordinateList(s) { const a = JSON.parse("[" + s + "]"); const points = Array(a.length); for (let i = 0; i < a.length; i++) { const e = a[i]; if (!Array.isArray(e) || e.length != 2 || typeof e[0] != "number" || typeof e[1] != "number") { throw new Error("Invalid syntax in element " + i + "."); } points[i] = { x: e[0], y: e[1] }; } return points; } } function formatCoordinateValue(v) { let s = String(v); if (s.length > 10) { s = v.toPrecision(6); } return s; } class FunctionPlotter { constructor(wctx) { this.wctx = wctx; const ctx = wctx.canvas.getContext("2d"); if (!ctx) { throw new Error("Canvas 2D context not available."); } this.ctx = ctx; } clearCanvas() { const wctx = this.wctx; const ctx = this.ctx; ctx.save(); const width = wctx.canvas.width; const height = wctx.canvas.height; const xMin = (wctx.eState.relevantXMin != undefined) ? Math.max(0, Math.min(width, wctx.mapLogicalToCanvasXCoordinate(wctx.eState.relevantXMin))) : 0; const xMax = (wctx.eState.relevantXMax != undefined) ? Math.max(xMin, Math.min(width, wctx.mapLogicalToCanvasXCoordinate(wctx.eState.relevantXMax))) : width; if (xMin > 0) { ctx.fillStyle = wctx.eState.secondaryBackground; ctx.fillRect(0, 0, xMin, height); } if (xMax > xMin) { ctx.fillStyle = wctx.eState.background; ctx.fillRect(xMin, 0, xMax - xMin, height); } if (xMax < width) { ctx.fillStyle = wctx.eState.secondaryBackground; ctx.fillRect(xMax, 0, width - xMax, height); } ctx.restore(); } drawKnot(knotNdx) { const wctx = this.wctx; const ctx = this.ctx; const knot = wctx.eState.knots[knotNdx]; const point = wctx.mapLogicalToCanvasCoordinates(knot); ctx.save(); ctx.beginPath(); const isDragging = knotNdx == wctx.iState.selectedKnotNdx && wctx.iState.knotDragging; const isSelected = knotNdx == wctx.iState.selectedKnotNdx; const isPotential = knotNdx == wctx.iState.potentialKnotNdx; const bold = isDragging || isSelected || isPotential; const r = bold ? 5 : 4; ctx.arc(point.x, point.y, r, 0, 2 * Math.PI); ctx.lineWidth = bold ? 3 : 1; ctx.strokeStyle = (isDragging || isPotential) ? wctx.eState.activeKnotColor : isSelected ? wctx.eState.selectedKnotColor : wctx.eState.defaultKnotColor; ctx.stroke(); ctx.restore(); } drawKnots() { const knots = this.wctx.eState.knots; for (let knotNdx = 0; knotNdx < knots.length; knotNdx++) { this.drawKnot(knotNdx); } } formatLabel(value, decPow) { let s = (decPow <= 7 && decPow >= -6) ? value.toFixed(Math.max(0, -decPow)) : value.toExponential(); if (s.length > 10) { s = value.toPrecision(6); } return s; } drawLabel(cPos, value, decPow, xy) { const wctx = this.wctx; const ctx = this.ctx; ctx.save(); ctx.textBaseline = "bottom"; ctx.font = "12px"; ctx.fillStyle = wctx.eState.labelColor; const x = xy ? cPos + 5 : 5; const y = xy ? wctx.canvas.height - 2 : cPos - 2; const s = this.formatLabel(value, decPow); ctx.fillText(s, x, y); ctx.restore(); } drawGridLine(p, cPos, xy) { const wctx = this.wctx; const ctx = this.ctx; ctx.save(); ctx.fillStyle = (p == 0) ? wctx.eState.axisColor : (p % 10 == 0) ? wctx.eState.secondaryLineColor : wctx.eState.primaryLineColor; ctx.fillRect(xy ? cPos : 0, xy ? 0 : cPos, xy ? 1 : wctx.canvas.width, xy ? wctx.canvas.height : 1); ctx.restore(); } drawXYGrid(xy) { const wctx = this.wctx; const gp = wctx.getGridParms(xy); if (!gp) { return; } let p = gp.pos; let loopCtr = 0; while (true) { const lPos = p * gp.space; const cPos = xy ? wctx.mapLogicalToCanvasXCoordinate(lPos) : wctx.mapLogicalToCanvasYCoordinate(lPos); if (xy ? (cPos > wctx.canvas.width) : (cPos < 0)) { break; } this.drawGridLine(p, cPos, xy); this.drawLabel(cPos, lPos, gp.decPow, xy); p += gp.span; if (loopCtr++ > 100) { break; } } } drawGrid() { this.drawXYGrid(true); this.drawXYGrid(false); } drawFunctionCurve(uniFunction, lxMin, lxMax) { const wctx = this.wctx; const ctx = this.ctx; ctx.save(); ctx.beginPath(); const cxMin = Math.max(0, Math.ceil(wctx.mapLogicalToCanvasXCoordinate(lxMin))); const cxMax = Math.min(wctx.canvas.width, Math.floor(wctx.mapLogicalToCanvasXCoordinate(lxMax))); for (let cx = cxMin; cx <= cxMax; cx++) { const lx = wctx.mapCanvasToLogicalXCoordinate(cx); const ly = uniFunction(lx); const cy = Math.max(-1E6, Math.min(1E6, wctx.mapLogicalToCanvasYCoordinate(ly))); ctx.lineTo(cx, cy); } ctx.strokeStyle = wctx.eState.curveColor; ctx.stroke(); ctx.restore(); } drawFunctionCurveFromKnots() { const wctx = this.wctx; const knots = wctx.eState.knots; if (knots.length < 2 && !wctx.eState.extendedDomain) { return; } const xMin = wctx.eState.extendedDomain ? -1E99 : knots[0].x; const xMax = wctx.eState.extendedDomain ? 1E99 : knots[knots.length - 1].x; const uniFunction = wctx.createInterpolationFunction(); this.drawFunctionCurve(uniFunction, xMin, xMax); } paint() { const wctx = this.wctx; if (!this.newCanvasWidth || !this.newCanvasHeight) { return; } if (this.newCanvasWidth != wctx.canvas.width || this.newCanvasHeight != wctx.canvas.height) { wctx.canvas.width = this.newCanvasWidth; wctx.canvas.height = this.newCanvasHeight; } this.clearCanvas(); if (wctx.eState.gridEnabled) { this.drawGrid(); } this.drawFunctionCurveFromKnots(); this.drawKnots(); } resize(width, height) { const wctx = this.wctx; if (this.newCanvasWidth == width && this.newCanvasHeight == height) { return; } this.newCanvasWidth = width; this.newCanvasHeight = height; wctx.requestRefresh(); } } class PointerController { constructor(wctx) { this.zooming = false; this.pointerDownEventListener = (event) => { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || (event.pointerType == "mouse" && event.button != 0)) { return; } if (this.isPointerInResizeHandle(event)) { return; } this.trackPointer(event); if ((event.pointerType == "touch" || event.pointerType == "pen") && this.pointers.size == 1) { if (this.lastTouchTime > 0 && performance.now() - this.lastTouchTime <= 300) { this.lastTouchTime = 0; this.processDoubleClickTouch(); event.preventDefault(); return; } this.lastTouchTime = performance.now(); } this.switchMode(); event.preventDefault(); }; this.pointerUpEventListener = (event) => { if (!this.pointers.has(event.pointerId)) { return; } this.releasePointer(event.pointerId); this.switchMode(); event.preventDefault(); }; this.pointerMoveEventListener = (event) => { if (!this.pointers.has(event.pointerId)) { this.updatePotentialKnot(event); return; } this.trackPointer(event); if (this.pointers.size == 1) { this.drag(); } else if (this.pointers.size == 2 && this.zooming) { this.zoom(); } event.preventDefault(); }; this.wheelEventListener = (event) => { const wctx = this.wctx; if (wctx.eState.focusShield && !wctx.hasFocus()) { return; } event.preventDefault(); const isProbablyPad = event.deltaMode == 0 && Math.abs(event.deltaY) < 50 || event.deltaX != 0; if (isProbablyPad && !event.ctrlKey) { this.moveByWheel(event); return; } if (event.deltaY == 0) { return; } const f0 = isProbablyPad ? 1.05 : Math.SQRT2; const f = (event.deltaY > 0) ? 1 / f0 : f0; let zoomMode; if (event.shiftKey) { zoomMode = 1; } else if (event.altKey) { zoomMode = 0; } else if (event.ctrlKey && !isProbablyPad) { zoomMode = 2; } else { zoomMode = wctx.eState.primaryZoomMode; } let fx; let fy; switch (zoomMode) { case 0: { fx = f; fy = 1; break; } case 1: { fx = 1; fy = f; break; } default: { fx = f; fy = f; } } const cPoint = this.getCanvasCoordinatesFromEvent(event); wctx.zoom(fx, fy, cPoint); wctx.requestRefresh(); wctx.fireViewportChangeEvent(); }; this.dblClickEventListener = (event) => { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.button != 0) { return; } const cPoint = this.getCanvasCoordinatesFromEvent(event); this.createKnot(cPoint); event.preventDefault(); }; this.wctx = wctx; this.pointers = new Map(); wctx.canvas.addEventListener("pointerdown", this.pointerDownEventListener); wctx.canvas.addEventListener("pointerup", this.pointerUpEventListener); wctx.canvas.addEventListener("pointercancel", this.pointerUpEventListener); wctx.canvas.addEventListener("pointermove", this.pointerMoveEventListener); wctx.canvas.addEventListener("dblclick", this.dblClickEventListener); wctx.canvas.addEventListener("wheel", this.wheelEventListener); } dispose() { const wctx = this.wctx; wctx.canvas.removeEventListener("pointerdown", this.pointerDownEventListener); wctx.canvas.removeEventListener("pointerup", this.pointerUpEventListener); wctx.canvas.removeEventListener("pointercancel", this.pointerUpEventListener); wctx.canvas.removeEventListener("pointermove", this.pointerMoveEventListener); wctx.canvas.removeEventListener("dblclick", this.dblClickEventListener); wctx.canvas.removeEventListener("wheel", this.wheelEventListener); this.releaseAllPointers(); } processEscKey() { this.abortDragging(); } switchMode() { const wctx = this.wctx; this.stopDragging(); this.stopZooming(); if (this.pointers.size == 1) { this.startDragging(); wctx.canvas.focus(); } else if (this.pointers.size == 2) { this.startZooming(); } } trackPointer(event) { const wctx = this.wctx; const pointerId = event.pointerId; if (!this.pointers.has(pointerId)) { wctx.canvas.setPointerCapture(pointerId); } this.pointers.set(pointerId, event); } releasePointer(pointerId) { const wctx = this.wctx; this.pointers.delete(pointerId); wctx.canvas.releasePointerCapture(pointerId); } releaseAllPointers() { while (this.pointers.size > 0) { const pointerId = this.pointers.keys().next().value; this.releasePointer(pointerId); } } startDragging() { const wctx = this.wctx; const cPoint = this.getCanvasCoordinates(); const lPoint = wctx.mapCanvasToLogicalCoordinates(cPoint); const pointerType = this.pointers.values().next().value.pointerType; const knotNdx = this.findNearKnot(cPoint, pointerType); wctx.iState.selectedKnotNdx = knotNdx; wctx.iState.knotDragging = knotNdx != undefined; wctx.iState.planeDragging = knotNdx == undefined; this.dragStartLPos = lPoint; this.dragStartCPos = cPoint; this.dragCount = 0; wctx.iState.potentialKnotNdx = undefined; wctx.requestRefresh(); } abortDragging() { const wctx = this.wctx; if (wctx.iState.knotDragging && this.dragCount > 0) { wctx.undo(); wctx.fireChangeEvent(); } if (wctx.iState.planeDragging && this.dragStartCPos && this.dragStartLPos) { wctx.moveCoordinatePlane(this.dragStartCPos, this.dragStartLPos); wctx.fireViewportChangeEvent(); } this.stopDragging(); wctx.requestRefresh(); } stopDragging() { const wctx = this.wctx; if (wctx.iState.knotDragging || wctx.iState.planeDragging) { wctx.requestRefresh(); } this.dragStartLPos = undefined; this.dragStartCPos = undefined; wctx.iState.knotDragging = false; wctx.iState.planeDragging = false; } drag() { const wctx = this.wctx; const cPoint = this.getCanvasCoordinates(); if (wctx.iState.knotDragging && wctx.iState.selectedKnotNdx != undefined) { if (this.dragCount++ == 0) { wctx.pushUndoHistoryState(); } const lPoint = wctx.mapCanvasToLogicalCoordinates(cPoint); const lPoint2 = this.snapToGrid(lPoint); wctx.moveKnot(wctx.iState.selectedKnotNdx, lPoint2); wctx.requestRefresh(); wctx.fireChangeEvent(); } else if (wctx.iState.planeDragging && this.dragStartLPos) { wctx.moveCoordinatePlane(cPoint, this.dragStartLPos); wctx.requestRefresh(); wctx.fireViewportChangeEvent(); } } startZooming() { const wctx = this.wctx; const pointerValues = this.pointers.values(); const event1 = pointerValues.next().value; const event2 = pointerValues.next().value; const cPoint1 = this.getCanvasCoordinatesFromEvent(event1); const cPoint2 = this.getCanvasCoordinatesFromEvent(event2); const cCenter = PointUtils.computeCenter(cPoint1, cPoint2); const xDist = Math.abs(cPoint1.x - cPoint2.x); const yDist = Math.abs(cPoint1.y - cPoint2.y); this.zoomLCenter = wctx.mapCanvasToLogicalCoordinates(cCenter); this.zoomStartDist = PointUtils.computeDistance(cPoint1, cPoint2); this.zoomStartFactorX = wctx.getZoomFactor(true); this.zoomStartFactorY = wctx.getZoomFactor(false); const t = Math.tan(Math.PI / 8); this.zoomX = xDist > t * yDist; this.zoomY = yDist > t * xDist; this.zooming = true; } stopZooming() { this.zooming = false; } zoom() { const wctx = this.wctx; const eState = wctx.eState; const pointerValues = this.pointers.values(); const event1 = pointerValues.next().value; const event2 = pointerValues.next().value; const cPoint1 = this.getCanvasCoordinatesFromEvent(event1); const cPoint2 = this.getCanvasCoordinatesFromEvent(event2); const newCCenter = PointUtils.computeCenter(cPoint1, cPoint2); const newDist = PointUtils.computeDistance(cPoint1, cPoint2); const f = newDist / this.zoomStartDist; if (this.zoomX) { eState.xMax = eState.xMin + wctx.canvas.width / (this.zoomStartFactorX * f); } if (this.zoomY) { eState.yMax = eState.yMin + wctx.canvas.height / (this.zoomStartFactorY * f); } wctx.moveCoordinatePlane(newCCenter, this.zoomLCenter); wctx.requestRefresh(); wctx.fireViewportChangeEvent(); } moveByWheel(event) { const wctx = this.wctx; const f = (event.deltaMode == 1) ? 15 : (event.deltaMode == 2) ? 100 : 1; const dx = f * event.deltaX; const dy = f * event.deltaY; if (dx == 0 && dy == 0) { return; } wctx.moveCoordinatePlaneRelPx(dx, -dy); wctx.requestRefresh(); wctx.fireViewportChangeEvent(); } processDoubleClickTouch() { const cPoint = this.getCanvasCoordinates(); this.createKnot(cPoint); } createKnot(cPoint) { const wctx = this.wctx; wctx.pushUndoHistoryState(); const lPoint = wctx.mapCanvasToLogicalCoordinates(cPoint); const knotNdx = wctx.addKnot(lPoint); wctx.iState.selectedKnotNdx = knotNdx; wctx.iState.potentialKnotNdx = knotNdx; wctx.iState.knotDragging = false; wctx.iState.planeDragging = false; wctx.requestRefresh(); wctx.fireChangeEvent(); } updatePotentialKnot(event) { const wctx = this.wctx; const cPoint = this.getCanvasCoordinatesFromEvent(event); const knotNdx = this.findNearKnot(cPoint, event.pointerType); if (wctx.iState.potentialKnotNdx != knotNdx) { wctx.iState.potentialKnotNdx = knotNdx; wctx.requestRefresh(); } } findNearKnot(cPoint, pointerType) { const wctx = this.wctx; const r = wctx.findNearestKnot(cPoint); const proximityRange = (pointerType == "touch") ? 30 : 15; return (r && r.distance <= proximityRange) ? r.knotNdx : undefined; } snapToGrid(lPoint) { const wctx = this.wctx; if (!wctx.eState.gridEnabled || !wctx.eState.snapToGridEnabled) { return lPoint; } return { x: this.snapToGrid2(lPoint.x, true), y: this.snapToGrid2(lPoint.y, false) }; } snapToGrid2(lPos, xy) { const maxDistance = 5; const wctx = this.wctx; const gp = wctx.getGridParms(xy); if (!gp) { return lPos; } const gridSpace = gp.space * gp.span; const gridPos = Math.round(lPos / gridSpace) * gridSpace; const lDist = Math.abs(lPos - gridPos); const cDist = lDist * wctx.getZoomFactor(xy); if (cDist > maxDistance) { return lPos; } return gridPos; } getCanvasCoordinates() { if (this.pointers.size < 1) { throw new Error("No active pointers."); } const event = this.pointers.values().next().value; return this.getCanvasCoordinatesFromEvent(event); } getCanvasCoordinatesFromEvent(event) { const wctx = this.wctx; return wctx.mapViewportToCanvasCoordinates({ x: event.clientX, y: event.clientY }); } isPointerInResizeHandle(event) { const wctx = this.wctx; const parentElement = wctx.canvas.parentNode; if (!(parentElement instanceof HTMLElement)) { return false; } if (getComputedStyle(parentElement).resize != "both") { return false; } const rect = parentElement.getBoundingClientRect(); const dx = rect.right - event.clientX; const dy = rect.bottom - event.clientY; const handleSize = 18; return dx >= 0 && dx < handleSize && dy >= 0 && dy < handleSize; } } class KeyboardController { constructor(wctx) { this.keyDownEventListener = (event) => { const keyName = genKeyName(event); if (this.processKeyDown(keyName)) { event.preventDefault(); } }; this.keyPressEventListener = (event) => { const keyName = genKeyName(event); if (this.processKeyPress(keyName)) { event.preventDefault(); } }; this.wctx = wctx; wctx.canvas.addEventListener("keydown", this.keyDownEventListener); wctx.canvas.addEventListener("keypress", this.keyPressEventListener); } dispose() { const wctx = this.wctx; wctx.canvas.removeEventListener("keydown", this.keyDownEventListener); wctx.canvas.removeEventListener("keypress", this.keyPressEventListener); } processKeyDown(keyName) { const wctx = this.wctx; switch (keyName) { case "Backspace": case "Delete": { if (wctx.iState.selectedKnotNdx != undefined) { wctx.iState.knotDragging = false; wctx.pushUndoHistoryState(); wctx.deleteKnot(wctx.iState.selectedKnotNdx); wctx.requestRefresh(); wctx.fireChangeEvent(); } return true; } case "Ctrl+z": case "Alt+Backspace": { if (wctx.undo()) { wctx.requestRefresh(); wctx.fireChangeEvent(); } return true; } case "Ctrl+y": case "Ctrl+Z": { if (wctx.redo()) { wctx.requestRefresh(); wctx.fireChangeEvent(); } return true; } case "Escape": { wctx.pointerController.processEscKey(); return true; } default: { return false; } } } processKeyPress(keyName) { const wctx = this.wctx; const eState = wctx.eState; switch (keyName) { case "+": case "-": case "x": case "X": case "y": case "Y": { const fx = (keyName == '+' || keyName == 'X') ? Math.SQRT2 : (keyName == '-' || keyName == 'x') ? Math.SQRT1_2 : 1; const fy = (keyName == '+' || keyName == 'Y') ? Math.SQRT2 : (keyName == '-' || keyName == 'y') ? Math.SQRT1_2 : 1; wctx.zoom(fx, fy); wctx.requestRefresh(); wctx.fireViewportChangeEvent(); return true; } case "i": { wctx.reset(); wctx.requestRefresh(); wctx.fireChangeEvent(); return true; } case "c": { wctx.pushUndoHistoryState(); wctx.clearKnots(); wctx.requestRefresh(); wctx.fireChangeEvent(); return true; } case "e": { eState.extendedDomain = !eState.extendedDomain; wctx.requestRefresh(); return true; } case "g": { eState.gridEnabled = !eState.gridEnabled; wctx.requestRefresh(); return true; } case "s": { eState.snapToGridEnabled = !eState.snapToGridEnabled; return true; } case "l": { eState.interpolationMethod = (eState.interpolationMethod == "linear") ? "akima" : "linear"; wctx.requestRefresh(); wctx.fireChangeEvent(); return true; } case "k": { void this.promptKnots(); return true; } case "r": { void this.resample1(); return true; } default: { return false; } } } async promptKnots() { const wctx = this.wctx; const s1 = wctx.getKnotCoordinateString(); const s2 = await DialogManager.promptInput({ promptText: "Knot coordinates:", defaultValue: s1, rows: 5 }); if (!s2 || s1 == s2) { return; } await wctx.setKnotCoordinateString(s2); } async resample1() { const n = await this.promptResampleCount(); if (!n) { return; } this.resample2(n); } resample2(n) { const wctx = this.wctx; const oldKnots = wctx.eState.knots; if (oldKnots.length < 1) { void DialogManager.showMsg({ msgText: "No knots." }); return; } const xMin = oldKnots[0].x; const xMax = oldKnots[oldKnots.length - 1].x; const uniFunction = wctx.createInterpolationFunction(); const newKnots = Array(n); for (let i = 0; i < n; i++) { const x = xMin + (xMax - xMin) / (n - 1) * i; const y = uniFunction(x); newKnots[i] = { x, y }; } wctx.pushUndoHistoryState(); wctx.replaceKnots(newKnots); wctx.requestRefresh(); wctx.fireChangeEvent(); } async promptResampleCount() { const wctx = this.wctx; const oldN = wctx.eState.knots.length; const s = await DialogManager.promptInput({ titleText: "Re-sample", promptText: "Number of knots:", defaultValue: String(oldN) }); if (!s) { return; } const n = Number(s); if (!Number.isInteger(n) || n < 2 || n > 1E7) { await DialogManager.showMsg({ titleText: "Error", msgText: "Invalid number: " + s }); return; } return n; } } function genKeyName(event) { const s = (event.altKey ? "Alt+" : "") + (event.ctrlKey ? "Ctrl+" : "") + (event.shiftKey && event.key.length > 1 ? "Shift+" : "") + (event.metaKey ? "Meta+" : "") + event.key; return s; } class WidgetContext { constructor(canvas, widget) { this.animationFrameHandler = () => { this.animationFramePending = false; if (!this.isConnected) { return; } this.refresh(); }; this.resizeObserverCallback = (entries) => { const box = entries[0].contentBoxSize[0]; const width = box.inlineSize; const height = box.blockSize; this.plotter.resize(width, height); }; globalInit(); this.widget = widget; this.canvas = canvas; this.canvasStyle = getComputedStyle(canvas); this.eventTarget = new EventTarget(); this.isConnected = false; this.animationFramePending = false; this.resizeObserver = new ResizeObserver(this.resizeObserverCallback); this.setEditorState({}); } setConnected(connected) { if (connected == this.isConnected) { return; } if (connected) { this.plotter = new FunctionPlotter(this); this.pointerController = new PointerController(this); this.kbController = new KeyboardController(this); this.resizeObserver.observe(this.canvas); canvasMap.set(this.canvas, this.widget); } else { this.pointerController.dispose(); this.kbController.dispose(); this.resizeObserver.unobserve(this.canvas); canvasMap.delete(this.canvas); } this.isConnected = connected; this.requestRefresh(); } setEditorState(eState) { this.eState = cloneEditorState(eState); this.initialEState = cloneEditorState(eState); this.resetInteractionState(); this.resetHistoryState(); this.requestRefresh(); } getEditorState() { return cloneEditorState(this.eState); } resetInteractionState() { this.iState = { selectedKnotNdx: undefined, potentialKnotNdx: undefined, knotDragging: false, planeDragging: false }; } reset() { this.setEditorState(this.initialEState); } clearKnots() { this.eState.knots = []; this.resetInteractionState(); } resetHistoryState() { this.hState = { undoStack: [], undoStackPos: 0 }; } pushUndoHistoryState() { const hState = this.hState; hState.undoStack.length = hState.undoStackPos; hState.undoStack.push(this.eState.knots.slice()); hState.undoStackPos = hState.undoStack.length; } undo() { const hState = this.hState; if (hState.undoStackPos < 1) { return false; } if (hState.undoStackPos == hState.undoStack.length) { hState.undoStack.push(this.eState.knots.slice()); } hState.undoStackPos--; this.eState.knots = hState.undoStack[hState.undoStackPos].slice(); this.resetInteractionState(); return true; } redo() { const hState = this.hState; if (hState.undoStackPos >= hState.undoStack.length - 1) { return false; } hState.undoStackPos++; this.eState.knots = hState.undoStack[hState.undoStackPos].slice(); this.resetInteractionState(); return true; } mapLogicalToCanvasXCoordinate(lx) { return (lx - this.eState.xMin) * this.canvas.width / (this.eState.xMax - this.eState.xMin); } mapLogicalToCanvasYCoordinate(ly) { return this.canvas.height - (ly - this.eState.yMin) * this.canvas.height / (this.eState.yMax - this.eState.yMin); } mapLogicalToCanvasCoordinates(lPoint) { return { x: this.mapLogicalToCanvasXCoordinate(lPoint.x), y: this.mapLogicalToCanvasYCoordinate(lPoint.y) }; } mapCanvasToLogicalXCoordinate(cx) { return this.eState.xMin + cx * (this.eState.xMax - this.eState.xMin) / this.canvas.width; } mapCanvasToLogicalYCoordinate(cy) { return this.eState.yMin + (this.canvas.height - cy) * (this.eState.yMax - this.eState.yMin) / this.canvas.height; } mapCanvasToLogicalCoordinates(cPoint) { return { x: this.mapCanvasToLogicalXCoordinate(cPoint.x), y: this.mapCanvasToLogicalYCoordinate(cPoint.y) }; } mapViewportToCanvasCoordinates(vPoint) { const canvasStyle = this.canvasStyle; const rect = this.canvas.getBoundingClientRect(); const paddingLeft = getPx(canvasStyle.paddingLeft); const paddingRight = getPx(canvasStyle.paddingRight); const paddingTop = getPx(canvasStyle.paddingTop); const paddingBottom = getPx(canvasStyle.paddingBottom); const borderLeft = getPx(canvasStyle.borderLeftWidth); const borderTop = getPx(canvasStyle.borderTopWidth); const width = this.canvas.clientWidth - paddingLeft - paddingRight; const height = this.canvas.clientHeight - paddingTop - paddingBottom; const x1 = vPoint.x - rect.left - borderLeft - paddingLeft; const y1 = vPoint.y - rect.top - borderTop - paddingTop; const x = x1 / width * this.canvas.width; const y = y1 / height * this.canvas.height; return { x, y }; function getPx(s) { return s ? parseFloat(s) : 0; } } moveCoordinatePlane(cPoint, lPoint) { const eState = this.eState; const lWidth = eState.xMax - eState.xMin; const lHeight = eState.yMax - eState.yMin; const cWidth = this.canvas.width; const cHeight = this.canvas.height; eState.xMin = lPoint.x - cPoint.x * lWidth / cWidth; eState.xMax = eState.xMin + lWidth; eState.yMin = lPoint.y - (cHeight - cPoint.y) * lHeight / cHeight; eState.yMax = eState.yMin + lHeight; } moveCoordinatePlaneRelPx(dx, dy) { const eState = this.eState; const lWidth = eState.xMax - eState.xMin; const lHeight = eState.yMax - eState.yMin; const cWidth = this.canvas.width; const cHeight = this.canvas.height; eState.xMin = eState.xMin + dx / cWidth * lWidth; eState.xMax = eState.xMin + lWidth; eState.yMin = eState.yMin + dy / cHeight * lHeight; eState.yMax = eState.yMin + lHeight; } getZoomFactor(xy) { const eState = this.eState; return xy ? this.canvas.width / (eState.xMax - eState.xMin) : this.canvas.height / (eState.yMax - eState.yMin); } zoom(fx, fyOpt, cCenterOpt) { const eState = this.eState; const fy = (fyOpt != undefined) ? fyOpt : fx; const cCenter = cCenterOpt ? cCenterOpt : { x: this.canvas.width / 2, y: this.canvas.height / 2 }; const lCenter = this.mapCanvasToLogicalCoordinates(cCenter); eState.xMax = eState.xMin + (eState.xMax - eState.xMin) / fx; eState.yMax = eState.yMin + (eState.yMax - eState.yMin) / fy; this.moveCoordinatePlane(cCenter, lCenter); } deleteKnot(knotNdx) { const knots = this.eState.knots; const oldKnots = knots.slice(); knots.splice(knotNdx, 1); this.fixUpKnotIndexes(oldKnots); } moveKnot(knotNdx, newPosition) { this.eState.knots[knotNdx] = newPosition; this.revampKnots(); } addKnot(newKnot) { const knot = PointUtils.clone(newKnot); this.eState.knots.push(knot); this.revampKnots(); const knotNdx = PointUtils.findPoint(this.eState.knots, knot); if (knotNdx == undefined) { throw new Error("Program logic error."); } return knotNdx; } replaceKnots(newKnots) { this.eState.knots = newKnots; this.resetInteractionState(); this.revampKnots(); } revampKnots() { this.sortKnots(); PointUtils.makeXValsStrictMonotonic(this.eState.knots); } sortKnots() { const oldKnots = this.eState.knots.slice(); this.eState.knots.sort(function (p1, p2) { return (p1.x != p2.x) ? p1.x - p2.x : p1.y - p2.y; }); this.fixUpKnotIndexes(oldKnots); } fixUpKnotIndexes(oldKnots) { this.iState.selectedKnotNdx = PointUtils.mapPointIndex(oldKnots, this.eState.knots, this.iState.selectedKnotNdx); this.iState.potentialKnotNdx = PointUtils.mapPointIndex(oldKnots, this.eState.knots, this.iState.potentialKnotNdx); this.iState.knotDragging = this.iState.knotDragging && this.iState.selectedKnotNdx != undefined; } findNearestKnot(cPoint) { const knots = this.eState.knots; let minDist = undefined; let nearestKnotNdx = undefined; for (let i = 0; i < knots.length; i++) { const lKnot = knots[i]; const cKnot = this.mapLogicalToCanvasCoordinates(lKnot); const d = PointUtils.computeDistance(cKnot, cPoint); if (minDist == undefined || d < minDist) { nearestKnotNdx = i; minDist = d; } } return (nearestKnotNdx != undefined) ? { knotNdx: nearestKnotNdx, distance: minDist } : undefined; } getKnotCoordinateString() { return PointUtils.encodeCoordinateList(this.eState.knots); } async setKnotCoordinateString(s) { let newKnots; try { newKnots = PointUtils.decodeCoordinateList(s); } catch (e) { await DialogManager.showMsg({ titleText: "Error", msgText: "Knot coordinates could not be decoded. " + e }); return; } this.pushUndoHistoryState(); this.replaceKnots(newKnots); this.requestRefresh(); this.fireChangeEvent(); } getGridParms(xy) { const minSpaceC = xy ? 66 : 50; const edge = xy ? this.eState.xMin : this.eState.yMin; const minSpaceL = minSpaceC / this.getZoomFactor(xy); const decPow = Math.ceil(Math.log(minSpaceL / 5) / Math.LN10); const edgeDecPow = (edge == 0) ? -99 : Math.log(Math.abs(edge)) / Math.LN10; if (edgeDecPow - decPow > 10) { return undefined; } const space = Math.pow(10, decPow); const f = minSpaceL / space; const span = (f > 2.001) ? 5 : (f > 1.001) ? 2 : 1; const p1 = Math.ceil(edge / space); const pos = span * Math.ceil(p1 / span); return { space, span, pos, decPow }; } createInterpolationFunction() { const knots = this.eState.knots; const n = knots.length; const xVals = new Float64Array(n); const yVals = new Float64Array(n); for (let i = 0; i < n; i++) { xVals[i] = knots[i].x; yVals[i] = knots[i].y; } return createInterpolatorWithFallback(this.eState.interpolationMethod, xVals, yVals); } requestRefresh() { if (this.animationFramePending || !this.isConnected) { return; } requestAnimationFrame(this.animationFrameHandler); this.animationFramePending = true; } refresh() { this.plotter.paint(); this.updateCanvasCursorStyle(); } updateCanvasCursorStyle() { const style = (this.iState.knotDragging || this.iState.planeDragging) ? "move" : "auto"; this.canvas.style.cursor = style; } fireChangeEvent() { this.fireEvent("change"); } fireViewportChangeEvent() { this.fireEvent("viewportchange"); } fireEvent(eventName) { const event = new CustomEvent(eventName); nextTick(() => { this.eventTarget.dispatchEvent(event); }); } hasFocus() { return document.activeElement === this.canvas; } } function cloneEditorState(eState) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w; return { knots: ((_a = eState.knots) !== null && _a !== void 0 ? _a : []).slice(), xMin: (_b = eState.xMin) !== null && _b !== void 0 ? _b : 0, xMax: (_c = eState.xMax) !== null && _c !== void 0 ? _c : 1, yMin: (_d = eState.yMin) !== null && _d !== void 0 ? _d : 0, yMax: (_e = eState.yMax) !== null && _e !== void 0 ? _e : 1, extendedDomain: (_f = eState.extendedDomain) !== null && _f !== void 0 ? _f : true, relevantXMin: eState.relevantXMin, relevantXMax: eState.relevantXMax, gridEnabled: (_g = eState.gridEnabled) !== null && _g !== void 0 ? _g : true, snapToGridEnabled: (_h = eState.snapToGridEnabled) !== null && _h !== void 0 ? _h : true, interpolationMethod: (_j = eState.interpolationMethod) !== null && _j !== void 0 ? _j : "akima", primaryZoomMode: (_k = eState.primaryZoomMode) !== null && _k !== void 0 ? _k : 2, focusShield: (_l = eState.focusShield) !== null && _l !== void 0 ? _l : false, curveColor: (_m = eState.curveColor) !== null && _m !== void 0 ? _m : "#44CC44", defaultKnotColor: (_o = eState.defaultKnotColor) !== null && _o !== void 0 ? _o : "#CC4444", selectedKnotColor: (_p = eState.selectedKnotColor) !== null && _p !== void 0 ? _p : "#0080FF", activeKnotColor: (_q = eState.activeKnotColor) !== null && _q !== void 0 ? _q : "#EE5500", labelColor: (_r = eState.labelColor) !== null && _r !== void 0 ? _r : "#707070", axisColor: (_s = eState.axisColor) !== null && _s !== void 0 ? _s : "#989898", secondaryLineColor: (_t = eState.secondaryLineColor) !== null && _t !== void 0 ? _t : "#D4D4D4", primaryLineColor: (_u = eState.primaryLineColor) !== null && _u !== void 0 ? _u : "#EEEEEE", secondaryBackground: (_v = eState.secondaryBackground) !== null && _v !== void 0 ? _v : "#F8F8F8", background: (_w = eState.background) !== null && _w !== void 0 ? _w : "#FFFFFF" }; } export class Widget { constructor(canvas, connected = true) { this.wctx = new WidgetContext(canvas, this); if (connected) { this.setConnected(true); } } setEventTarget(eventTarget) { this.wctx.eventTarget = eventTarget; } setConnected(connected) { this.wctx.setConnected(connected); } addEventListener(type, listener) { this.wctx.eventTarget.addEventListener(type, listener); } removeEventListener(type, listener) { this.wctx.eventTarget.removeEventListener(type, listener); } getEditorState() { return this.wctx.getEditorState(); } setEditorState(eState) { const wctx = this.wctx; wctx.setEditorState(eState); } getFunction() { return this.wctx.createInterpolationFunction(); } getRawHelpText() { const pz = this.wctx.eState.primaryZoomMode; const primaryZoomAxis = (pz == 0) ? "x-axis" : (pz == 1) ? "y-axis" : "both axes"; return [ "drag knot with mouse or touch", "move a knot", "drag plane with mouse or touch", "move the coordinate space", "click or tap on knot", "select a knot", "Delete / Backspace", "delete the selected knot", "double-click or double-tap", "create a new knot", "Esc", "abort moving", "Ctrl+Z / Alt+Backspace", "undo", "Ctrl+Y / Ctrl+Shift+Z", "redo", "mouse wheel", "zoom " + primaryZoomAxis, "shift + mouse wheel", "zoom y-axis", "ctrl + mouse wheel", "zoom both axes", "alt + mouse wheel", "zoom x-axis", "touch zoom gesture", "zoom in any direction", "+ / -", "zoom both axes in/out", "X / x", "zoom x-axis in/out", "Y / y", "zoom y-axis in/out", "e", "toggle extended function domain", "g", "toggle coordinate grid", "s", "toggle snap to grid", "l", "toggle between linear interpolation and Akima", "k", "knots (display prompt with coordinate values)", "Clipboard copy / paste", "copy/paste knot coordinates", "r", "re-sample knots", "c", "clear the canvas", "i", "reset to the initial state" ]; } getFormattedHelpText() { const t = this.getRawHelpText(); const a = []; a.push("<table class='functionCurveEditorHelp'>"); a.push("<colgroup>"); a.push("<col class='functionCurveEditorHelpCol1'>"); a.push("<col class='functionCurveEditorHelpCol2'>"); a.push("</colgroup>"); a.push("<tbody>"); for (let i = 0; i < t.length; i += 2) { a.push("<tr><td>"); a.push(t[i]); a.push("</td><td>"); a.push(t[i + 1]); a.push("</td>"); } a.push("</tbody>"); a.push("</table>"); return a.join(""); } clipboardCopyEventHandler(event) { if (!event.clipboardData) { return; } event.preventDefault(); const s = this.wctx.getKnotCoordinateString(); event.clipboardData.setData("text", s); } clipboardPasteEventHandler(event) { var _a; const s = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text"); if (!s) { return; } event.preventDefault(); void this.wctx.setKnotCoordinateString(s); } } var globalInitDone = false; var canvasMap; function getActiveWidget() { var _a; let e = document.activeElement; while (true) { if (!e) { return; } if (e.tagName == "CANVAS") { return canvasMap.get(e); } e = (_a = e.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement; } } function globalCopyEventListener(event) { var _a; (_a = getActiveWidget()) === null || _a === void 0 ? void 0 : _a.clipboardCopyEventHandler(event); } function globalPasteEventListener(event) { var _a; (_a = getActiveWidget()) === null || _a === void 0 ? void 0 : _a.clipboardPasteEventHandler(event); } function globalInit() { if (globalInitDone) { return; } canvasMap = new Map(); document.addEventListener("copy", globalCopyEventListener); document.addEventListener("paste", globalPasteEventListener); globalInitDone = true; } //# sourceMappingURL=FunctionCurveEditor.js.map