diff web/timeline/viewstate.ts @ 2376:4556eebe5d73

topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author drewp@bigasterisk.com
date Sun, 12 May 2024 19:02:10 -0700
parents light9/web/timeline/viewstate.ts@d991f7c3485a
children 06da5db2fafe
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/timeline/viewstate.ts	Sun May 12 19:02:10 2024 -0700
@@ -0,0 +1,128 @@
+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);
+  }
+}