Mercurial > code > home > repos > light9
comparison 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 |
comparison
equal
deleted
inserted
replaced
2375:623836db99af | 2376:4556eebe5d73 |
---|---|
1 import * as d3 from "d3"; | |
2 import { debug } from "debug"; | |
3 import * as ko from "knockout"; | |
4 const log = debug("adjustable"); | |
5 | |
6 interface Config { | |
7 // getTarget -> vec2 of current target position | |
8 getTarget: () => Vector; | |
9 // getSuggestedTargetOffset -> vec2 pixel offset from target | |
10 getSuggestedTargetOffset: () => Vector; | |
11 // emptyBox -> true if you want no value display | |
12 emptyBox: boolean; | |
13 } | |
14 | |
15 export class Adjustable { | |
16 config: any; | |
17 handle: any; | |
18 initialTarget: any; | |
19 targetDraggedTo: any; | |
20 root: any; | |
21 // Some value you can edit in the UI, probably by dragging | |
22 // stuff. Drawn by light9-adjusters-canvas. This object does the | |
23 // layout and positioning. | |
24 // | |
25 // The way dragging should work is that you start in the yellow *adj | |
26 // widget*, wherever it is, but your drag is moving the *target*. The | |
27 // adj will travel around too, but it may do extra moves to not bump | |
28 // into stuff or to get out from under your finger. | |
29 | |
30 constructor(config: any) { | |
31 this.config = config; | |
32 this.ctor2(); | |
33 } | |
34 | |
35 ctor2() { | |
36 // updated later by layout algoritm | |
37 return (this.handle = $V([0, 0])); | |
38 } | |
39 | |
40 getDisplayValue() { | |
41 if (this.config.emptyBox) { | |
42 return ""; | |
43 } | |
44 const defaultFormat = d3.format(".4g")(this._getValue()); | |
45 if (this.config.getDisplayValue != null) { | |
46 return this.config.getDisplayValue(this._getValue(), defaultFormat); | |
47 } | |
48 return defaultFormat; | |
49 } | |
50 _getValue(): any { | |
51 throw new Error("Method not implemented."); | |
52 } | |
53 | |
54 getSuggestedHandle() { | |
55 return this.getTarget().add(this.config.getSuggestedTargetOffset()); | |
56 } | |
57 | |
58 getHandle() { | |
59 // vec2 of pixels | |
60 return this.handle; | |
61 } | |
62 | |
63 getTarget() { | |
64 // vec2 of pixels | |
65 return this.config.getTarget(); | |
66 } | |
67 | |
68 subscribe(onChange: any) { | |
69 // change could be displayValue or center or target. This likely | |
70 // calls onChange right away if there's any data yet. | |
71 throw new Error("not implemented"); | |
72 } | |
73 | |
74 startDrag() { | |
75 return (this.initialTarget = this.getTarget()); | |
76 } | |
77 | |
78 continueDrag(pos: { add: (arg0: any) => any }) { | |
79 //# pos is vec2 of pixels relative to the drag start | |
80 return (this.targetDraggedTo = pos.add(this.initialTarget)); | |
81 } | |
82 | |
83 endDrag() {} | |
84 // override | |
85 | |
86 _editorCoordinates() { | |
87 // vec2 of mouse relative to <l9-t-editor> | |
88 let rootElem: { getBoundingClientRect: () => any }; | |
89 return this.targetDraggedTo; | |
90 // let ev = d3.event.sourceEvent; | |
91 | |
92 // if (ev.target.tagName === "LIGHT9-TIMELINE-EDITOR") { | |
93 // rootElem = ev.target; | |
94 // } else { | |
95 // rootElem = ev.target.closest("light9-timeline-editor"); | |
96 // } | |
97 | |
98 // if (ev.touches != null ? ev.touches.length : undefined) { | |
99 // ev = ev.touches[0]; | |
100 // } | |
101 | |
102 // // storing root on the object to remember it across calls in case | |
103 // // you drag outside the editor. | |
104 // if (rootElem) { | |
105 // this.root = rootElem.getBoundingClientRect(); | |
106 // } | |
107 // const offsetParentPos = $V([ev.pageX - this.root.left, ev.pageY - this.root.top]); | |
108 | |
109 // return offsetParentPos; | |
110 } | |
111 } | |
112 | |
113 class AdjustableFloatObservable extends Adjustable { | |
114 constructor(config: any) { | |
115 // config also has: | |
116 // observable -> ko.observable we will read and write | |
117 // getValueForPos(pos) -> what should we set to if the user | |
118 // moves target to this coord? | |
119 this.config = config; | |
120 super(); | |
121 this.ctor2(); | |
122 } | |
123 | |
124 _getValue() { | |
125 return this.config.observable(); | |
126 } | |
127 | |
128 continueDrag(pos: any) { | |
129 // pos is vec2 of pixels relative to the drag start. | |
130 super.continueDrag(pos); | |
131 const epos = this._editorCoordinates(); | |
132 const newValue = this.config.getValueForPos(epos); | |
133 return this.config.observable(newValue); | |
134 } | |
135 | |
136 subscribe(onChange: () => any) { | |
137 log("AdjustableFloatObservable subscribe", this.config); | |
138 return ko.computed(() => { | |
139 this.config.observable(); | |
140 return onChange(); | |
141 }); | |
142 } | |
143 } | |
144 | |
145 class AdjustableFloatObject extends Adjustable { | |
146 _currentValue: any; | |
147 _onChange: any; | |
148 constructor(config: any) { | |
149 // config also has: | |
150 // graph | |
151 // subj | |
152 // pred | |
153 // ctx | |
154 // getTargetPosForValue(value) -> getTarget result for value | |
155 // getValueForPos | |
156 this.config = config; | |
157 super(); | |
158 this.ctor2(); | |
159 if (this.config.ctx == null) { | |
160 throw new Error("missing ctx"); | |
161 } | |
162 // this seems to not fire enough. | |
163 this.config.graph.runHandler(this._syncValue.bind(this), `adj sync ${this.config.subj.value} ${this.config.pred.value}`); | |
164 } | |
165 | |
166 _syncValue() { | |
167 this._currentValue = this.config.graph.floatValue(this.config.subj, this.config.pred); | |
168 if (this._onChange) { | |
169 return this._onChange(); | |
170 } | |
171 } | |
172 | |
173 _getValue() { | |
174 // this is a big speedup- callers use _getValue about 4x as much as | |
175 // the graph changes and graph.floatValue is slow | |
176 return this._currentValue; | |
177 } | |
178 | |
179 getTarget() { | |
180 return this.config.getTargetPosForValue(this._getValue()); | |
181 } | |
182 | |
183 subscribe(onChange: any) { | |
184 // only works on one subscription at a time | |
185 if (this._onChange) { | |
186 throw new Error("multi subscribe not implemented"); | |
187 } | |
188 return (this._onChange = onChange); | |
189 } | |
190 | |
191 continueDrag(pos: any) { | |
192 // pos is vec2 of pixels relative to the drag start | |
193 super.continueDrag(pos); | |
194 const newValue = this.config.getValueForPos(this._editorCoordinates()); | |
195 | |
196 return this.config.graph.patchObject(this.config.subj, this.config.pred, this.config.graph.LiteralRoundedFloat(newValue), this.config.ctx); | |
197 //@_syncValue() | |
198 } | |
199 } | |
200 | |
201 class AdjustableFade extends Adjustable { | |
202 yForV: any; | |
203 zoomInX: any; | |
204 i0: any; | |
205 i1: any; | |
206 note: any; | |
207 constructor(yForV: any, zoomInX: any, i0: any, i1: any, note: any, offset: any, ctx: any) { | |
208 this.yForV = yForV; | |
209 this.zoomInX = zoomInX; | |
210 this.i0 = i0; | |
211 this.i1 = i1; | |
212 this.note = note; | |
213 super(); | |
214 this.config = { | |
215 getSuggestedTargetOffset() { | |
216 return offset; | |
217 }, | |
218 getTarget: this.getTarget.bind(this), | |
219 ctx, | |
220 }; | |
221 this.ctor2(); | |
222 } | |
223 | |
224 getTarget() { | |
225 const mid = this.note.midPoint(this.i0, this.i1); | |
226 return $V([this.zoomInX(mid.e(1)), this.yForV(mid.e(2))]); | |
227 } | |
228 | |
229 _getValue() { | |
230 return this.note.midPoint(this.i0, this.i1).e(1); | |
231 } | |
232 | |
233 continueDrag(pos: { e: (arg0: number) => any }) { | |
234 // pos is vec2 of pixels relative to the drag start | |
235 super.continueDrag(pos); | |
236 const { graph } = this.note; | |
237 const U = (x: string) => graph.Uri(x); | |
238 | |
239 const goalCenterSec = this.zoomInX.invert(this.initialTarget.e(1) + pos.e(1)); | |
240 | |
241 const diamSec = this.note.worldPts[this.i1].e(1) - this.note.worldPts[this.i0].e(1); | |
242 const newSec0 = goalCenterSec - diamSec / 2; | |
243 const newSec1 = goalCenterSec + diamSec / 2; | |
244 | |
245 const originSec = graph.floatValue(this.note.uri, U(":originTime")); | |
246 | |
247 const p0 = this._makePatch(graph, this.i0, newSec0, originSec, this.config.ctx); | |
248 const p1 = this._makePatch(graph, this.i1, newSec1, originSec, this.config.ctx); | |
249 | |
250 return graph.applyAndSendPatch(this._addPatches(p0, p1)); | |
251 } | |
252 | |
253 _makePatch( | |
254 graph: { getObjectPatch: (arg0: any, arg1: any, arg2: any, arg3: any) => any; Uri: (arg0: string) => any; LiteralRoundedFloat: (arg0: number) => any }, | |
255 idx: string | number, | |
256 newSec: number, | |
257 originSec: number, | |
258 ctx: any | |
259 ) { | |
260 return graph.getObjectPatch(this.note.worldPts[idx].uri, graph.Uri(":time"), graph.LiteralRoundedFloat(newSec - originSec), ctx); | |
261 } | |
262 | |
263 _addPatches(p0: { addQuads: { concat: (arg0: any) => any }; delQuads: { concat: (arg0: any) => any } }, p1: { addQuads: any; delQuads: any }) { | |
264 return { | |
265 addQuads: p0.addQuads.concat(p1.addQuads), | |
266 delQuads: p0.delQuads.concat(p1.delQuads), | |
267 }; | |
268 } | |
269 } |