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