Files @ f2b3cfcc23d3
Branch filter:

Location: light9/web/timeline/viewstate.ts - annotation

drewp@bigasterisk.com
comment
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
import * as ko from "knockout";
import * as d3 from "d3";
import debug from "debug";

const log = debug("viewstate");
export class ViewState {
  zoomSpec: {
    duration: ko.Observable<number>; // current song duration
    t1: ko.Observable<number>;
    t2: ko.Observable<number>;
  };
  cursor: { t: ko.Observable<number> };
  mouse: { pos: ko.Observable<Vector> };
  width: ko.Observable<number>;
  coveredByDiagramTop: ko.Observable<number>;
  audioY: ko.Observable<number>;
  audioH: ko.Observable<number>;
  zoomedTimeY: ko.Observable<number>;
  zoomedTimeH: ko.Observable<number>;
  rowsY: ko.Observable<number>;
  fullZoomX: d3.ScaleLinear<number, number>;
  zoomInX: d3.ScaleLinear<number, number>;
  zoomAnimSec: number;
  constructor() {
    // caller updates all these observables
    this.zoomSpec = {
      duration: ko.observable(100), // current song duration
      t1: ko.observable(0),
      t2: ko.observable(100),
    };
    this.cursor = { t: ko.observable(20) }; // songTime
    this.mouse = { pos: ko.observable($V([0, 0])) };
    this.width = ko.observable(500);
    this.coveredByDiagramTop = ko.observable(0); // page coords
    // all these are relative to #coveredByDiagram:
    this.audioY = ko.observable(0);
    this.audioH = ko.observable(0);
    this.zoomedTimeY = ko.observable(0);
    this.zoomedTimeH = ko.observable(0);
    this.rowsY = ko.observable(0);

    this.fullZoomX = d3.scaleLinear();
    this.zoomInX = d3.scaleLinear();

    this.zoomAnimSec = 0.1;

    ko.computed(this.maintainZoomLimitsAndScales.bind(this));
  }

  setWidth(w: any) {
    this.width(w);
    this.maintainZoomLimitsAndScales(); // before other handlers run
  }

  maintainZoomLimitsAndScales() {
    // not for cursor updates

    if (this.zoomSpec.t1() < 0) {
      this.zoomSpec.t1(0);
    }
    if (this.zoomSpec.duration() && this.zoomSpec.t2() > this.zoomSpec.duration()) {
      this.zoomSpec.t2(this.zoomSpec.duration());
    }

    const rightPad = 5; // don't let time adjuster fall off right edge
    this.fullZoomX.domain([0, this.zoomSpec.duration()]);
    this.fullZoomX.range([0, this.width() - rightPad]);

    this.zoomInX.domain([this.zoomSpec.t1(), this.zoomSpec.t2()]);
    this.zoomInX.range([0, this.width() - rightPad]);
  }

  latestMouseTime(): number {
    return this.zoomInX.invert(this.mouse.pos().e(1));
  }

  onMouseWheel(deltaY: any) {
    const zs = this.zoomSpec;

    const center = this.latestMouseTime();
    const left = center - zs.t1();
    const right = zs.t2() - center;
    const scale = Math.pow(1.005, deltaY);

    zs.t1(center - left * scale);
    zs.t2(center + right * scale);
    log("view to", ko.toJSON(this));
  }

  frameCursor() {
    const zs = this.zoomSpec;
    const visSeconds = zs.t2() - zs.t1();
    const margin = visSeconds * 0.4;
    // buggy: really needs t1/t2 to limit their ranges
    if (this.cursor.t() < zs.t1() || this.cursor.t() > zs.t2() - visSeconds * 0.6) {
      const newCenter = this.cursor.t() + margin;
      this.animatedZoom(newCenter - visSeconds / 2, newCenter + visSeconds / 2, this.zoomAnimSec);
    }
  }
  frameToEnd() {
    this.animatedZoom(this.cursor.t() - 2, this.zoomSpec.duration(), this.zoomAnimSec);
  }
  frameAll() {
    this.animatedZoom(0, this.zoomSpec.duration(), this.zoomAnimSec);
  }
  animatedZoom(newT1: number, newT2: number, secs: number) {
    const fps = 30;
    const oldT1 = this.zoomSpec.t1();
    const oldT2 = this.zoomSpec.t2();
    let lastTime = 0;
    for (let step = 0; step < secs * fps; step++) {
      const frac = step / (secs * fps);
      ((frac) => {
        const gotoStep = () => {
          this.zoomSpec.t1((1 - frac) * oldT1 + frac * newT1);
          return this.zoomSpec.t2((1 - frac) * oldT2 + frac * newT2);
        };
        const delay = frac * secs * 1000;
        setTimeout(gotoStep, delay);
        lastTime = delay;
      })(frac);
    }
    setTimeout(() => {
      this.zoomSpec.t1(newT1);
      return this.zoomSpec.t2(newT2);
    }, lastTime + 10);
  }
}