Files @ 06da5db2fafe
Branch filter:

Location: light9/web/timeline/viewstate.ts

drewp@bigasterisk.com
rewrite ascoltami to use the graph for more playback data
import * as d3 from "d3";
import debug from "debug";
import * as ko from "knockout";

import Sylvester from "sylvester";
const $V = Sylvester.Vector.create;

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);
  }
}