import { debug } from "debug"; import { LitElement } from "lit"; import { customElement } from "lit/decorators.js"; import { throttle } from "underscore"; import * as d3 from "d3"; import { Adjustable } from "./adjustable"; import * as Drawing from "../drawing"; // https://www.npmjs.com/package/@types/sylvester Global values: $L, $M, $P, $V, Line, Matrix, Plane, Sylvester, Vector const log = debug("adjusters"); const maxDist = 60; interface Drag { start: Vector; adj: Adjustable; cur?: Vector; } type QTreeData = Vector & { adj: Adjustable }; @customElement("light9-adjusters-canvas") class AdjustersCanvas extends LitElement { static getter_properties: { setAdjuster: { type: any; notify: boolean } }; static getter_observers: {}; redraw: any; adjs: { [id: string | number]: Adjustable }; hoveringNear: any; ctx: any; $: any; setAdjuster: any; offsetParent: any; currentDrag?: Drag; qt?: d3.Quadtree; canvasCenter: any; static initClass() { this.getter_properties = { setAdjuster: { type: Function, notify: true } }; this.getter_observers = ["updateAllCoords(adjs)"]; } constructor() { super(); this.redraw = throttle(this._throttledRedraw.bind(this), 30, { leading: false }); this.adjs = {}; this.hoveringNear = null; } ready() { this.addEventListener("iron-resize", this.resizeUpdate.bind(this)); this.ctx = this.$.canvas.getContext("2d"); this.redraw(); this.setAdjuster = this._setAdjuster.bind(this); // These don't fire; TimelineEditor calls the handlers for us. this.addEventListener("mousedown", this.onDown.bind(this)); this.addEventListener("mousemove", this.onMove.bind(this)); return this.addEventListener("mouseup", this.onUp.bind(this)); } addEventListener(arg0: string, arg1: any) { throw new Error("Method not implemented."); } _mousePos(ev: MouseEvent) { return $V([ev.clientX, ev.clientY - this.offsetParent.offsetTop]); } onDown(ev: MouseEvent) { if (ev.buttons === 1) { const start = this._mousePos(ev); const adj = this._adjAtPoint(start); if (adj) { ev.stopPropagation(); this.currentDrag = { start, adj }; return adj.startDrag(); } } } onMove(ev: MouseEvent) { const pos = this._mousePos(ev); if (this.currentDrag) { this.hoveringNear = null; this.currentDrag.cur = pos; this.currentDrag.adj.continueDrag(this.currentDrag.cur.subtract(this.currentDrag.start)); this.redraw(); } else { const near = this._adjAtPoint(pos); if (this.hoveringNear !== near) { this.hoveringNear = near; this.redraw(); } } } onUp(ev: any) { if (!this.currentDrag) { return; } this.currentDrag.adj.endDrag(); this.currentDrag = undefined; } _setAdjuster(adjId: string | number, makeAdjustable?: () => Adjustable) { // callers register/unregister the Adjustables they want us to make // adjuster elements for. Caller invents adjId. makeAdjustable is // a function returning the Adjustable or it is undefined to clear any // adjusters with this id. if (makeAdjustable == null) { if (this.adjs[adjId]) { delete this.adjs[adjId]; } } else { // this might be able to reuse an existing one a bit const adj = makeAdjustable(); this.adjs[adjId] = adj; adj.id = adjId; } this.redraw(); (window as any).debug_adjsCount = Object.keys(this.adjs).length; } updateAllCoords() { this.redraw(); } _adjAtPoint(pt: Vector): Adjustable|undefined { const nearest = this.qt!.find(pt.e(1), pt.e(2)); if (nearest == null || nearest.distanceFrom(pt) > maxDist) { return undefined; } return nearest != null ? nearest.adj : undefined; } resizeUpdate(ev: { target: { offsetWidth: any; offsetHeight: any } }) { this.$.canvas.width = ev.target.offsetWidth; this.$.canvas.height = ev.target.offsetHeight; this.canvasCenter = $V([this.$.canvas.width / 2, this.$.canvas.height / 2]); return this.redraw(); } _throttledRedraw() { if (this.ctx == null) { return; } console.time("adjs redraw"); this._layoutCenters(); this.ctx.clearRect(0, 0, this.$.canvas.width, this.$.canvas.height); for (let adjId in this.adjs) { const adj = this.adjs[adjId]; const ctr = adj.getHandle(); const target = adj.getTarget(); if (this._isOffScreen(target)) { continue; } this._drawConnector(ctr, target); this._drawAdjuster(adj.getDisplayValue(), ctr.e(1) - 20, ctr.e(2) - 10, ctr.e(1) + 20, ctr.e(2) + 10, adj === this.hoveringNear); } return console.timeEnd("adjs redraw"); } _layoutCenters() { // push Adjustable centers around to avoid overlaps // Todo: also don't overlap inlineattr boxes // Todo: don't let their connector lines cross each other const qt = d3.quadtree( [], (d: QTreeData) => d.e(1), (d: QTreeData) => d.e(2) ); this.qt = qt; qt.extent([ [0, 0], [8000, 8000], ]); let _: string | number, adj: { handle: any; getSuggestedHandle: () => any }; for (_ in this.adjs) { adj = this.adjs[_]; adj.handle = this._clampOnScreen(adj.getSuggestedHandle()); } const numTries = 8; for (let tryn = 0; tryn < numTries; tryn++) { for (_ in this.adjs) { adj = this.adjs[_]; let current = adj.handle; qt.remove(current); const nearest = qt.find(current.e(1), current.e(2), maxDist); if (nearest) { const dist = current.distanceFrom(nearest); if (dist < maxDist) { current = this._stepAway(current, nearest, 1 / numTries); adj.handle = current; } } current.adj = adj; qt.add(current); } } //if -50 < output.e(1) < 20 # mostly for zoom-left // output.setElements([ // Math.max(20, output.e(1)), // output.e(2)]) } _stepAway( current: Vector, nearest: Vector, dx: number ) { const away = current.subtract(nearest).toUnitVector(); const toScreenCenter = this.canvasCenter.subtract(current).toUnitVector(); const goalSpacingPx = 20; return this._clampOnScreen(current.add(away.x(goalSpacingPx * dx))); } _isOffScreen(pos: Vector):boolean { return pos.e(1) < 0 || pos.e(1) > this.$.canvas.width || pos.e(2) < 0 || pos.e(2) > this.$.canvas.height; } _clampOnScreen(pos: Vector): Vector { const marg = 30; return $V([Math.max(marg, Math.min(this.$.canvas.width - marg, pos.e(1))), Math.max(marg, Math.min(this.$.canvas.height - marg, pos.e(2)))]); } _drawConnector(ctr: Vector, target: Vector) { this.ctx.strokeStyle = "#aaa"; this.ctx.lineWidth = 2; this.ctx.beginPath(); Drawing.line(this.ctx, ctr, target); this.ctx.stroke(); } _drawAdjuster(label: any, x1: number, y1: number, x2: number, y2: number, hover: boolean) { const radius = 8; this.ctx.shadowColor = "black"; this.ctx.shadowBlur = 15; this.ctx.shadowOffsetX = 5; this.ctx.shadowOffsetY = 9; this.ctx.fillStyle = hover ? "#ffff88" : "rgba(255, 255, 0, 0.5)"; this.ctx.beginPath(); Drawing.roundRect(this.ctx, x1, y1, x2, y2, radius); this.ctx.fill(); this.ctx.shadowColor = "rgba(0,0,0,0)"; this.ctx.strokeStyle = "yellow"; this.ctx.lineWidth = 2; this.ctx.setLineDash([3, 3]); this.ctx.beginPath(); Drawing.roundRect(this.ctx, x1, y1, x2, y2, radius); this.ctx.stroke(); this.ctx.setLineDash([]); this.ctx.font = "12px sans"; this.ctx.fillStyle = "#000"; this.ctx.fillText(label, x1 + 5, y2 - 5, x2 - x1 - 10); // coords from a center that's passed in // # special layout for the thaeter ones with middinh // l/r arrows // mouse arrow cursor upon hover, and accent the hovered adjuster // connector } }