Files @ af83aeef8b0a
Branch filter:

Location: light9/web/timeline/adjustable.ts

drewp@bigasterisk.com
fancier spectrograms
import * as d3 from "d3";
import { debug } from "debug";
import * as ko from "knockout";
const log = debug("adjustable");

interface Config {
  //   getTarget -> vec2 of current target position
  getTarget: () => Vector;
  //   getSuggestedTargetOffset -> vec2 pixel offset from target
  getSuggestedTargetOffset: () => Vector;
  //   emptyBox -> true if you want no value display
  emptyBox: boolean;
}

export class Adjustable {
  config: any;
  handle: any;
  initialTarget: any;
  targetDraggedTo: any;
  root: any;
  // Some value you can edit in the UI, probably by dragging
  // stuff. Drawn by light9-adjusters-canvas. This object does the
  // layout and positioning.
  //
  // The way dragging should work is that you start in the yellow *adj
  // widget*, wherever it is, but your drag is moving the *target*. The
  // adj will travel around too, but it may do extra moves to not bump
  // into stuff or to get out from under your finger.

  constructor(config: any) {
    this.config = config;
    this.ctor2();
  }

  ctor2() {
    // updated later by layout algoritm
    return (this.handle = $V([0, 0]));
  }

  getDisplayValue() {
    if (this.config.emptyBox) {
      return "";
    }
    const defaultFormat = d3.format(".4g")(this._getValue());
    if (this.config.getDisplayValue != null) {
      return this.config.getDisplayValue(this._getValue(), defaultFormat);
    }
    return defaultFormat;
  }
  _getValue(): any {
    throw new Error("Method not implemented.");
  }

  getSuggestedHandle() {
    return this.getTarget().add(this.config.getSuggestedTargetOffset());
  }

  getHandle() {
    // vec2 of pixels
    return this.handle;
  }

  getTarget() {
    // vec2 of pixels
    return this.config.getTarget();
  }

  subscribe(onChange: any) {
    // change could be displayValue or center or target. This likely
    // calls onChange right away if there's any data yet.
    throw new Error("not implemented");
  }

  startDrag() {
    return (this.initialTarget = this.getTarget());
  }

  continueDrag(pos: { add: (arg0: any) => any }) {
    //# pos is vec2 of pixels relative to the drag start
    return (this.targetDraggedTo = pos.add(this.initialTarget));
  }

  endDrag() {}
  // override

  _editorCoordinates() {
    // vec2 of mouse relative to <l9-t-editor>
    let rootElem: { getBoundingClientRect: () => any };
    return this.targetDraggedTo;
    // let ev = d3.event.sourceEvent;

    // if (ev.target.tagName === "LIGHT9-TIMELINE-EDITOR") {
    //   rootElem = ev.target;
    // } else {
    //   rootElem = ev.target.closest("light9-timeline-editor");
    // }

    // if (ev.touches != null ? ev.touches.length : undefined) {
    //   ev = ev.touches[0];
    // }

    // // storing root on the object to remember it across calls in case
    // // you drag outside the editor.
    // if (rootElem) {
    //   this.root = rootElem.getBoundingClientRect();
    // }
    // const offsetParentPos = $V([ev.pageX - this.root.left, ev.pageY - this.root.top]);

    // return offsetParentPos;
  }
}

class AdjustableFloatObservable extends Adjustable {
  constructor(config: any) {
    // config also has:
    //   observable -> ko.observable we will read and write
    //   getValueForPos(pos) -> what should we set to if the user
    //                          moves target to this coord?
    this.config = config;
    super();
    this.ctor2();
  }

  _getValue() {
    return this.config.observable();
  }

  continueDrag(pos: any) {
    // pos is vec2 of pixels relative to the drag start.
    super.continueDrag(pos);
    const epos = this._editorCoordinates();
    const newValue = this.config.getValueForPos(epos);
    return this.config.observable(newValue);
  }

  subscribe(onChange: () => any) {
    log("AdjustableFloatObservable subscribe", this.config);
    return ko.computed(() => {
      this.config.observable();
      return onChange();
    });
  }
}

class AdjustableFloatObject extends Adjustable {
  _currentValue: any;
  _onChange: any;
  constructor(config: any) {
    // config also has:
    //   graph
    //   subj
    //   pred
    //   ctx
    //   getTargetPosForValue(value) -> getTarget result for value
    //   getValueForPos
    this.config = config;
    super();
    this.ctor2();
    if (this.config.ctx == null) {
      throw new Error("missing ctx");
    }
    // this seems to not fire enough.
    this.config.graph.runHandler(this._syncValue.bind(this), `adj sync ${this.config.subj.value} ${this.config.pred.value}`);
  }

  _syncValue() {
    this._currentValue = this.config.graph.floatValue(this.config.subj, this.config.pred);
    if (this._onChange) {
      return this._onChange();
    }
  }

  _getValue() {
    // this is a big speedup- callers use _getValue about 4x as much as
    // the graph changes and graph.floatValue is slow
    return this._currentValue;
  }

  getTarget() {
    return this.config.getTargetPosForValue(this._getValue());
  }

  subscribe(onChange: any) {
    // only works on one subscription at a time
    if (this._onChange) {
      throw new Error("multi subscribe not implemented");
    }
    return (this._onChange = onChange);
  }

  continueDrag(pos: any) {
    // pos is vec2 of pixels relative to the drag start
    super.continueDrag(pos);
    const newValue = this.config.getValueForPos(this._editorCoordinates());

    return this.config.graph.patchObject(this.config.subj, this.config.pred, this.config.graph.LiteralRoundedFloat(newValue), this.config.ctx);
    //@_syncValue()
  }
}

class AdjustableFade extends Adjustable {
  yForV: any;
  zoomInX: any;
  i0: any;
  i1: any;
  note: any;
  constructor(yForV: any, zoomInX: any, i0: any, i1: any, note: any, offset: any, ctx: any) {
    this.yForV = yForV;
    this.zoomInX = zoomInX;
    this.i0 = i0;
    this.i1 = i1;
    this.note = note;
    super();
    this.config = {
      getSuggestedTargetOffset() {
        return offset;
      },
      getTarget: this.getTarget.bind(this),
      ctx,
    };
    this.ctor2();
  }

  getTarget() {
    const mid = this.note.midPoint(this.i0, this.i1);
    return $V([this.zoomInX(mid.e(1)), this.yForV(mid.e(2))]);
  }

  _getValue() {
    return this.note.midPoint(this.i0, this.i1).e(1);
  }

  continueDrag(pos: { e: (arg0: number) => any }) {
    // pos is vec2 of pixels relative to the drag start
    super.continueDrag(pos);
    const { graph } = this.note;
    const U = (x: string) => graph.Uri(x);

    const goalCenterSec = this.zoomInX.invert(this.initialTarget.e(1) + pos.e(1));

    const diamSec = this.note.worldPts[this.i1].e(1) - this.note.worldPts[this.i0].e(1);
    const newSec0 = goalCenterSec - diamSec / 2;
    const newSec1 = goalCenterSec + diamSec / 2;

    const originSec = graph.floatValue(this.note.uri, U(":originTime"));

    const p0 = this._makePatch(graph, this.i0, newSec0, originSec, this.config.ctx);
    const p1 = this._makePatch(graph, this.i1, newSec1, originSec, this.config.ctx);

    return graph.applyAndSendPatch(this._addPatches(p0, p1));
  }

  _makePatch(
    graph: { getObjectPatch: (arg0: any, arg1: any, arg2: any, arg3: any) => any; Uri: (arg0: string) => any; LiteralRoundedFloat: (arg0: number) => any },
    idx: string | number,
    newSec: number,
    originSec: number,
    ctx: any
  ) {
    return graph.getObjectPatch(this.note.worldPts[idx].uri, graph.Uri(":time"), graph.LiteralRoundedFloat(newSec - originSec), ctx);
  }

  _addPatches(p0: { addQuads: { concat: (arg0: any) => any }; delQuads: { concat: (arg0: any) => any } }, p1: { addQuads: any; delQuads: any }) {
    return {
      addQuads: p0.addQuads.concat(p1.addQuads),
      delQuads: p0.delQuads.concat(p1.delQuads),
    };
  }
}