Files @ e7e03c203c99
Branch filter:

Location: light9/web/timeline/adjusters.ts

drewp@bigasterisk.com
resize cursor canvas for 400px tall spectros. fix canvas resolution code
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
  }
}