2062
|
1 log = debug('timeline')
|
|
2 debug.enable('*')
|
|
3
|
|
4 Drawing = window.Drawing
|
|
5 ROW_COUNT = 7
|
|
6
|
|
7 # Maintains a pixi object, some adjusters, and inlineattrs corresponding to a note
|
|
8 # in the graph.
|
|
9 class Note
|
|
10 constructor: (@parentElem, @container, @project, @graph, @selection, @uri, @setAdjuster, @song, @viewState, @brickLayout) ->
|
|
11 @adjusterIds = new Set() # id string
|
|
12 @updateSoon = _.debounce(@update.bind(@), 30)
|
|
13
|
|
14 initWatchers: ->
|
|
15 @graph.runHandler(@update.bind(@), "note update #{@uri.value}")
|
|
16 ko.computed @update.bind(@)
|
|
17
|
|
18 destroy: ->
|
|
19 log('destroy', @uri.value)
|
|
20 @isDetached = true
|
|
21 @clearAdjusters()
|
|
22 @parentElem.updateInlineAttrs(@uri, null)
|
|
23
|
|
24 clearAdjusters: ->
|
|
25 @adjusterIds.forEach (i) =>
|
|
26 @setAdjuster(i, null)
|
|
27 @adjusterIds.clear()
|
|
28
|
|
29 getCurvePoints: (subj, curveAttr) ->
|
|
30 U = (x) => @graph.Uri(x)
|
|
31 originTime = @graph.floatValue(subj, U(':originTime'))
|
|
32
|
|
33 for curve in @graph.objects(subj, U(':curve'))
|
2105
|
34 # todo: maybe shoudl be :effectAttr?
|
2062
|
35 if @graph.uriValue(curve, U(':attr')).equals(curveAttr)
|
|
36 return @project.getCurvePoints(curve, originTime)
|
|
37 throw new Error("curve #{@uri.value} has no attr #{curveAttr.value}")
|
|
38
|
|
39 midPoint: (i0, i1) ->
|
|
40 p0 = @worldPts[i0]
|
|
41 p1 = @worldPts[i1]
|
|
42 p0.x(.5).add(p1.x(.5))
|
|
43
|
|
44 _planDrawing: ->
|
|
45 U = (x) => @graph.Uri(x)
|
|
46 [pointUris, worldPts] = @getCurvePoints(@uri, U(':strength'))
|
|
47 effect = @graph.uriValue(@uri, U(':effectClass'))
|
|
48
|
|
49 yForV = @brickLayout.yForVFor(@)
|
|
50 dependOn = [@viewState.zoomSpec.t1(),
|
|
51 @viewState.zoomSpec.t2(),
|
|
52 @viewState.width()]
|
|
53 screenPts = (new PIXI.Point(@viewState.zoomInX(pt.e(1)),
|
|
54 yForV(pt.e(2))) for pt in worldPts)
|
|
55 return {
|
|
56 yForV: yForV
|
|
57 worldPts: worldPts
|
|
58 screenPts: screenPts
|
|
59 effect: effect
|
|
60 hover: @uri.equals(@selection.hover())
|
|
61 selected: @selection.selected().filter((s) => s.equals(@uri)).length
|
|
62 }
|
|
63
|
|
64 onRowChange: ->
|
|
65 @clearAdjusters()
|
|
66 @updateSoon()
|
|
67
|
|
68 redraw: (params) ->
|
|
69 # no observable or graph deps in here
|
|
70 @container.removeChildren()
|
|
71 @graphics = new PIXI.Graphics({nativeLines: false})
|
|
72 @graphics.interactive = true
|
|
73 @container.addChild(@graphics)
|
|
74
|
|
75 if params.hover
|
|
76 @_traceBorder(params.screenPts, 12, 0x888888)
|
|
77 if params.selected
|
|
78 @_traceBorder(params.screenPts, 6, 0xff2900)
|
|
79
|
|
80 shape = new PIXI.Polygon(params.screenPts)
|
|
81 @graphics.beginFill(@_noteColor(params.effect), .313)
|
|
82 @graphics.drawShape(shape)
|
|
83 @graphics.endFill()
|
|
84
|
|
85 @_traceBorder(params.screenPts, 2, 0xffd900)
|
|
86
|
|
87 @_addMouseBindings()
|
|
88
|
|
89
|
|
90 update: ->
|
|
91 if not @parentElem.isActiveNote(@uri)
|
|
92 # stale redraw call
|
|
93 return
|
|
94
|
|
95 if @worldPts
|
|
96 @brickLayout.setNoteSpan(@, @worldPts[0].e(1),
|
|
97 @worldPts[@worldPts.length - 1].e(1))
|
|
98
|
|
99 params = @_planDrawing()
|
|
100 @worldPts = params.worldPts
|
|
101
|
|
102 @redraw(params)
|
|
103
|
|
104 curveWidthCalc = () => @project.curveWidth(@worldPts)
|
|
105 @_updateAdjusters(params.screenPts, @worldPts, curveWidthCalc,
|
|
106 params.yForV, @viewState.zoomInX, @song)
|
|
107 @_updateInlineAttrs(params.screenPts, params.yForV)
|
|
108 @parentElem.noteDirty()
|
|
109
|
|
110 _traceBorder: (screenPts, thick, color) ->
|
|
111 @graphics.lineStyle(thick, color, 1)
|
|
112 @graphics.moveTo(screenPts[0].x, screenPts[0].y)
|
|
113 for p in screenPts.slice(1)
|
|
114 @graphics.lineTo(p.x, p.y)
|
|
115
|
|
116 _addMouseBindings: () ->
|
|
117 @graphics.on 'mousedown', (ev) =>
|
|
118 @_onMouseDown(ev)
|
|
119
|
|
120 @graphics.on 'mouseover', =>
|
|
121 if @selection.hover() and @selection.hover().equals(@uri)
|
|
122 # Hovering causes a redraw, which would cause another
|
|
123 # mouseover event.
|
|
124 return
|
|
125 @selection.hover(@uri)
|
|
126
|
|
127 # mouseout never fires since we rebuild the graphics on mouseover.
|
|
128 @graphics.on 'mousemove', (ev) =>
|
|
129 if @selection.hover() and @selection.hover().equals(@uri) and ev.target != @graphics
|
|
130 @selection.hover(null)
|
|
131
|
|
132 onUri: ->
|
|
133 @graph.runHandler(@update.bind(@), "note updates #{@uri}")
|
|
134
|
|
135 patchCouldAffectMe: (patch) ->
|
|
136 if patch and patch.addQuads # sometimes patch is a polymer-sent value. @update is used as a listener too
|
|
137 if patch.addQuads.length == patch.delQuads.length == 1
|
|
138 add = patch.addQuads[0]
|
|
139 del = patch.delQuads[0]
|
|
140 if (add.predicate.equals(del.predicate) and del.predicate.equals(@graph.Uri(':time')) and add.subject.equals(del.subject))
|
|
141 timeEditFor = add.subject
|
|
142 if @worldPts and timeEditFor not in @pointUris
|
|
143 return false
|
|
144 return true
|
|
145
|
|
146 xupdate: (patch) ->
|
|
147 # update our note DOM and SVG elements based on the graph
|
|
148 if not @patchCouldAffectMe(patch)
|
|
149 # as autodep still fires all handlers on all patches, we just
|
|
150 # need any single dep to cause another callback. (without this,
|
|
151 # we would no longer be registered at all)
|
|
152 @graph.subjects(@uri, @uri, @uri)
|
|
153 return
|
|
154 if @isDetached?
|
|
155 return
|
|
156
|
|
157 @_updateDisplay()
|
|
158
|
|
159 _updateAdjusters: (screenPts, worldPts, curveWidthCalc, yForV, zoomInX, ctx) ->
|
|
160 # todo: allow offset even on more narrow notes
|
|
161 if screenPts[screenPts.length - 1].x - screenPts[0].x < 100 or screenPts[0].x > @parentElem.offsetWidth or screenPts[screenPts.length - 1].x < 0
|
|
162 @clearAdjusters()
|
|
163 else
|
|
164 @_makeOffsetAdjuster(yForV, curveWidthCalc, ctx)
|
|
165 @_makeCurvePointAdjusters(yForV, worldPts, ctx)
|
|
166 @_makeFadeAdjusters(yForV, zoomInX, ctx, worldPts)
|
|
167
|
|
168 _updateInlineAttrs: (screenPts, yForV) ->
|
|
169 w = 280
|
|
170
|
|
171 leftX = Math.max(2, screenPts[Math.min(1, screenPts.length - 1)].x + 5)
|
|
172 rightX = screenPts[Math.min(2, screenPts.length - 1)].x - 5
|
|
173 if screenPts.length < 3
|
|
174 rightX = leftX + w
|
|
175
|
|
176 if rightX - leftX < w or rightX < w or leftX > @parentElem.offsetWidth
|
|
177 @parentElem.updateInlineAttrs(@uri, null)
|
|
178 return
|
|
179
|
|
180 config = {
|
|
181 uri: @uri,
|
|
182 left: leftX,
|
|
183 top: yForV(1) + 5,
|
|
184 width: w,
|
|
185 height: yForV(0) - yForV(1) - 15,
|
|
186 }
|
|
187
|
|
188 @parentElem.updateInlineAttrs(@uri, config)
|
|
189
|
|
190 _makeCurvePointAdjusters: (yForV, worldPts, ctx) ->
|
|
191 for pointNum in [0...worldPts.length]
|
|
192 @_makePointAdjuster(yForV, worldPts, pointNum, ctx)
|
|
193
|
|
194 _makePointAdjuster: (yForV, worldPts, pointNum, ctx) ->
|
|
195 U = (x) => @graph.Uri(x)
|
|
196
|
|
197 adjId = @uri.value + '/p' + pointNum
|
|
198 @adjusterIds.add(adjId)
|
|
199 @setAdjuster adjId, =>
|
|
200 adj = new AdjustableFloatObject({
|
|
201 graph: @graph
|
|
202 subj: worldPts[pointNum].uri
|
|
203 pred: U(':time')
|
|
204 ctx: ctx
|
|
205 getTargetPosForValue: (value) =>
|
|
206 $V([@viewState.zoomInX(value), yForV(worldPts[pointNum].e(2))])
|
|
207 getValueForPos: (pos) =>
|
|
208 origin = @graph.floatValue(@uri, U(':originTime'))
|
|
209 (@viewState.zoomInX.invert(pos.e(1)) - origin)
|
|
210 getSuggestedTargetOffset: () => @_suggestedOffset(worldPts[pointNum]),
|
|
211 })
|
|
212 adj._getValue = (=>
|
|
213 # note: don't use originTime from the closure- we need the
|
|
214 # graph dependency
|
|
215 adj._currentValue + @graph.floatValue(@uri, U(':originTime'))
|
|
216 )
|
|
217 adj
|
|
218
|
|
219 _makeOffsetAdjuster: (yForV, curveWidthCalc, ctx) ->
|
|
220 U = (x) => @graph.Uri(x)
|
|
221
|
|
222 adjId = @uri.value + '/offset'
|
|
223 @adjusterIds.add(adjId)
|
|
224 @setAdjuster adjId, =>
|
|
225 adj = new AdjustableFloatObject({
|
|
226 graph: @graph
|
|
227 subj: @uri
|
|
228 pred: U(':originTime')
|
|
229 ctx: ctx
|
|
230 getDisplayValue: (v, dv) => "o=#{dv}"
|
|
231 getTargetPosForValue: (value) =>
|
|
232 # display bug: should be working from pt[0].t, not from origin
|
|
233 $V([@viewState.zoomInX(value + curveWidthCalc() / 2), yForV(.5)])
|
|
234 getValueForPos: (pos) =>
|
|
235 @viewState.zoomInX.invert(pos.e(1)) - curveWidthCalc() / 2
|
|
236 getSuggestedTargetOffset: () => $V([-10, 0])
|
|
237 })
|
|
238 adj
|
|
239
|
|
240 _makeFadeAdjusters: (yForV, zoomInX, ctx, worldPts) ->
|
|
241 U = (x) => @graph.Uri(x)
|
|
242 @_makeFadeAdjuster(yForV, zoomInX, ctx, @uri.value + '/fadeIn', 0, 1, $V([-50, -10]))
|
|
243 n = worldPts.length
|
|
244 @_makeFadeAdjuster(yForV, zoomInX, ctx, @uri.value + '/fadeOut', n - 2, n - 1, $V([50, -10]))
|
|
245
|
|
246 _makeFadeAdjuster: (yForV, zoomInX, ctx, adjId, i0, i1, offset) ->
|
|
247 @adjusterIds.add(adjId)
|
|
248 @setAdjuster adjId, =>
|
|
249 new AdjustableFade(yForV, zoomInX, i0, i1, @, offset, ctx)
|
|
250
|
|
251 _suggestedOffset: (pt) ->
|
|
252 if pt.e(2) > .5
|
|
253 $V([0, 30])
|
|
254 else
|
|
255 $V([0, -30])
|
|
256
|
|
257 _onMouseDown: (ev) ->
|
|
258 sel = @selection.selected()
|
|
259 if ev.data.originalEvent.ctrlKey
|
|
260 if @uri in sel
|
|
261 sel = _.without(sel, @uri)
|
|
262 else
|
|
263 sel.push(@uri)
|
|
264 else
|
|
265 sel = [@uri]
|
|
266 @selection.selected(sel)
|
|
267
|
|
268 _noteColor: (effect) ->
|
|
269 effect = effect.value
|
|
270 if effect in ['http://light9.bigasterisk.com/effect/blacklight',
|
|
271 'http://light9.bigasterisk.com/effect/strobewarm']
|
|
272 hue = 0
|
|
273 sat = 100
|
|
274 else
|
|
275 hash = 0
|
|
276 for i in [(effect.length-10)...effect.length]
|
|
277 hash += effect.charCodeAt(i)
|
|
278 hue = (hash * 8) % 360
|
|
279 sat = 40 + (hash % 20) # don't conceal colorscale too much
|
|
280
|
|
281 return parseInt(tinycolor.fromRatio({h: hue / 360, s: sat / 100, l: .58}).toHex(), 16)
|
|
282
|
|
283 #elem = @getOrCreateElem(uri+'/label', 'noteLabels', 'text', {style: "font-size:13px;line-height:125%;font-family:'Verana Sans';text-align:start;text-anchor:start;fill:#000000;"})
|
|
284 #elem.setAttribute('x', curvePts[0].e(1)+20)
|
|
285 #elem.setAttribute('y', curvePts[0].e(2)-10)
|
|
286 #elem.innerHTML = effectLabel
|