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
+  }
+}
+
+