Mercurial > code > home > repos > light9
diff light9/web/timeline/adjusters.ts @ 2062:d991f7c3485a
WIP rough porting of coffee to ts
author | drewp@bigasterisk.com |
---|---|
date | Mon, 16 May 2022 01:33:49 -0700 |
parents | light9/web/timeline/adjusters.coffee@7fe81130b735 |
children | e2ed5ce36253 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/timeline/adjusters.ts Mon May 16 01:33:49 2022 -0700 @@ -0,0 +1,273 @@ +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<QTreeData>; + 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<QTreeData>( + [], + (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 + } +} + +