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; // current song duration t1: ko.Observable; t2: ko.Observable; }; cursor: { t: ko.Observable }; mouse: { pos: ko.Observable }; width: ko.Observable; coveredByDiagramTop: ko.Observable; audioY: ko.Observable; audioH: ko.Observable; zoomedTimeY: ko.Observable; zoomedTimeH: ko.Observable; rowsY: ko.Observable; fullZoomX: d3.ScaleLinear; zoomInX: d3.ScaleLinear; 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); } }