Mercurial > code > home > repos > light9
diff web/timeline/adjustable.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/adjustable.ts@d991f7c3485a |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/timeline/adjustable.ts Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,269 @@ +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), + }; + } +}