diff --git a/light9/web/timeline/timeline-elements.html b/light9/web/timeline/timeline-elements.html --- a/light9/web/timeline/timeline-elements.html +++ b/light9/web/timeline/timeline-elements.html @@ -198,15 +198,7 @@ background: rgba(126, 52, 245, 0.0784313 display: flex; } - + @@ -242,6 +234,7 @@ background: rgba(126, 52, 245, 0.0784313
+
@@ -331,5 +324,6 @@ background: rgba(126, 52, 245, 0.0784313 + diff --git a/light9/web/timeline/timeline.coffee b/light9/web/timeline/timeline.coffee --- a/light9/web/timeline/timeline.coffee +++ b/light9/web/timeline/timeline.coffee @@ -1,6 +1,25 @@ log = console.log RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' + +# polymer dom-repeat is happy to shuffle children by swapping their +# attribute values, and it's hard to correctly setup/teardown your +# side effects if your attributes are changing before the detach +# call. This alternative to dom-repeat never reassigns +# attributes. But, it can't set up property bindings. +updateChildren = (parent, newUris, makeChild) -> + childUris = [] + childByUri = {} + for e in parent.children + childUris.push(e.uri) + childByUri[e.uri] = e + + for uri in _.difference(childUris, newUris) + childByUri[uri].remove() + for uri in _.difference(newUris, childUris) + parent.appendChild(makeChild(uri)) + + Polymer is: 'light9-timeline-editor' behaviors: [ Polymer.IronResizableBehavior ] @@ -293,6 +312,7 @@ Polymer # for now since it's just one line calling dia, # light9-timeline-editor does our drawing work. + Polymer is: 'light9-timeline-graph-row' behaviors: [ Polymer.IronResizableBehavior ] @@ -304,17 +324,32 @@ Polymer noteUris: { type: Array, notify: true } rowIndex: { type: Object, notify: true } observers: [ - 'onGraph(graph)' + 'onGraph(graph, dia, setAdjuster, song, zoomInX)' 'update(song)' + 'onZoom(zoomInX)' ] onGraph: -> @graph.runHandler(@update.bind(@), "row notes #{@rowIndex}") update: -> U = (x) -> @graph.Uri(x) log("row #{@rowIndex} updating") - @noteUris = [] - for note in @graph.objects(@song, U(':note')) - @push('noteUris', note) + + notesForThisRow = @graph.objects(@song, U(':note')) + + updateChildren @, notesForThisRow, (newUri) => + child = document.createElement('light9-timeline-note') + child.graph = @graph + child.dia = @dia + child.uri = newUri + child.setAdjuster = @setAdjuster + child.song = @song # could change, but all the notes will be rebuilt + child.zoomInX = @zoomInX # missing binding; see onZoom + return child + + onZoom: -> + log('row onzoom') + for e in @children + e.zoomInX = @zoomInX getCurvePoints = (graph, curve, xOffset) -> @@ -338,81 +373,83 @@ Polymer zoomInX: { type: Object, notify: true } setAdjuster: {type: Function, notify: true} observers: [ - 'onUri(graph, dia, uri)' + 'onUri(graph, dia, uri, zoomInX, setAdjuster)' 'update(graph, dia, uri, zoomInX, setAdjuster)' ] ready: -> - @adjusterIds = [] + @adjusterIds = {} detached: -> - @dia.clearElem(@uri) - for i in @adjusterIds + log('detatch', @uri) + @dia.clearElem(@uri, ['/area', '/label']) + @isDetached = true + for i in Object.keys(@adjusterIds) @setAdjuster(i, null) onUri: -> @graph.runHandler(@update.bind(@), "note updates #{@uri}") update: -> + if @isDetached? + log('skipping update', @uri) + return # update our note DOM and SVG elements based on the graph U = (x) -> @graph.Uri(x) - try - worldPts = [] # (song time, value) - - yForV = (v) => @offsetTop + (1 - v) * @offsetHeight + worldPts = [] # (song time, value) - originTime = @graph.floatValue(@uri, U(':originTime')) - for curve in @graph.objects(@uri, U(':curve')) - if @graph.uriValue(curve, U(':attr')) == U(':strength') - worldPts = getCurvePoints(@graph, curve, originTime) + yForV = (v) => @offsetTop + (1 - v) * @offsetHeight - curveWidth = => - tMin = @graph.floatValue(worldPts[0].uri, U(':time')) - tMax = @graph.floatValue(worldPts[3].uri, U(':time')) - tMax - tMin - - @setAdjuster(@uri+'/offset', => new AdjustableFloatObject({ - graph: @graph - subj: @uri - pred: @graph.Uri(':originTime') - ctx: @graph.Uri(@song) - getDisplayValue: (v, dv) => "o=#{dv}" - getTargetPosForValue: (value) => - # display bug: should be working from pt[0].t, not from origin - $V([@zoomInX(value + curveWidth() / 2), yForV(.5)]) - getValueForPos: (pos) => - @zoomInX.invert(pos.e(1)) - curveWidth() / 2 - getSuggestedTargetOffset: () => $V([-10, 0]) - })) + originTime = @graph.floatValue(@uri, U(':originTime')) + for curve in @graph.objects(@uri, U(':curve')) + if @graph.uriValue(curve, U(':attr')) == U(':strength') + worldPts = getCurvePoints(@graph, curve, originTime) + + curveWidth = => + tMin = @graph.floatValue(worldPts[0].uri, U(':time')) + tMax = @graph.floatValue(worldPts[3].uri, U(':time')) + tMax - tMin - for pointNum in [0, 1, 2, 3] - @setAdjuster(@uri+'/p'+pointNum, => - adj = new AdjustableFloatObject({ - graph: @graph - subj: worldPts[pointNum].uri - pred: @graph.Uri(':time') - ctx: @graph.Uri(@song) - getTargetPosForValue: (value) => $V([@zoomInX(value), yForV(0)]) - getValueForPos: (pos) => - origin = @graph.floatValue(@uri, U(':originTime')) - (@zoomInX.invert(pos.e(1)) - origin) - getSuggestedTargetOffset: () => $V([0, -80]) - }) - adj._getValue = (=> - # note: don't use originTime from the closure- we need the - # graph dependency - adj._currentValue + @graph.floatValue(@uri, U(':originTime')) - ) - log('note made this point adj', adj) - adj - ) - - screenPos = (pt) => - $V([@zoomInX(pt.e(1)), @offsetTop + (1 - pt.e(2)) * @offsetHeight]) + @adjusterIds[@uri+'/offset'] = true + @setAdjuster(@uri+'/offset', => new AdjustableFloatObject({ + graph: @graph + subj: @uri + pred: @graph.Uri(':originTime') + ctx: @graph.Uri(@song) + getDisplayValue: (v, dv) => "o=#{dv}" + getTargetPosForValue: (value) => + # display bug: should be working from pt[0].t, not from origin + $V([@zoomInX(value + curveWidth() / 2), yForV(.5)]) + getValueForPos: (pos) => + @zoomInX.invert(pos.e(1)) - curveWidth() / 2 + getSuggestedTargetOffset: () => $V([-10, 0]) + })) - label = @graph.uriValue(@uri, U(':effectClass')).replace(/.*\//, '') - @dia.setNote(@uri, (screenPos(pt) for pt in worldPts), label) + for pointNum in [0, 1, 2, 3] + @adjusterIds[@uri+'/p'+pointNum] = true + @setAdjuster(@uri+'/p'+pointNum, => + adj = new AdjustableFloatObject({ + graph: @graph + subj: worldPts[pointNum].uri + pred: @graph.Uri(':time') + ctx: @graph.Uri(@song) + getTargetPosForValue: (value) => $V([@zoomInX(value), yForV(0)]) + getValueForPos: (pos) => + origin = @graph.floatValue(@uri, U(':originTime')) + (@zoomInX.invert(pos.e(1)) - origin) + getSuggestedTargetOffset: () => $V([0, -80]) + }) + adj._getValue = (=> + # note: don't use originTime from the closure- we need the + # graph dependency + adj._currentValue + @graph.floatValue(@uri, U(':originTime')) + ) + adj + ) - catch e - log("during resize of #{@uri}: #{@e}") + screenPos = (pt) => + $V([@zoomInX(pt.e(1)), @offsetTop + (1 - pt.e(2)) * @offsetHeight]) + + label = @graph.uriValue(@uri, U(':effectClass')).replace(/.*\//, '') + @dia.setNote(@uri, (screenPos(pt) for pt in worldPts), label) Polymer is: "light9-timeline-adjusters" @@ -429,8 +466,6 @@ Polymer # a function returning the Adjustable or it is null to clear any # adjusters with this id. - - if not @adjs[adjId] or not makeAdjustable? if not makeAdjustable? delete @adjs[adjId] @@ -439,35 +474,23 @@ Polymer @adjs[adjId] = adj adj.id = adjId @debounce('adjsChanged', @adjsChanged.bind(@), 1) + else + for e in @$.all.children + if e.id == adjId + e.updateDisplay() + window.debug_adjsCount = Object.keys(@adjs).length adjsChanged: -> - - added = removed = 0 - newIds = Object.keys(@adjs) - - parent = @$.all - haveIds = [] - for c in parent.children - id = c.getAttribute('id') - if newIds.indexOf(id) == -1 - c.remove() - removed++ - # ...? - else - haveIds.push(id) - - for id, adj of @adjs - if haveIds.indexOf(id) == -1 - log('need new one for', id) - child = document.createElement('light9-timeline-adjuster') - child.dia = @dia - child.graph = @graph - child.setAttribute('id', id) - child.adj = adj # set this last - parent.appendChild(child) - added++ - log("adjsChanged removed #{removed} added #{added}") if added or removed + updateChildren @$.all, Object.keys(@adjs), (newUri) => + child = document.createElement('light9-timeline-adjuster') + child.dia = @dia + child.graph = @graph + child.uri = newUri + child.id = newUri + child.adj = @adjs[newUri] + return child + @updateAllCoords() layoutCenters: -> # push Adjustable centers around to avoid overlaps @@ -504,27 +527,33 @@ Polymer is: 'light9-timeline-adjuster' properties: graph: { type: Object, notify: true } - adj: { type: Object, notify: true, observer: 'onAdj' } - target: { type: Object, notify: true } + adj: { type: Object, notify: true } + id: { type: String, notify: true } + displayValue: { type: String } centerStyle: { type: Object } spanClass: { type: String, value: '' } - onAdj: (adj) -> + observer: [ + 'onAdj(graph, adj, dia, id)' + ] + onAdj: -> log('onAdj', @id) @adj.subscribe(@updateDisplay.bind(this)) @graph.runHandler(@updateDisplay.bind(@)) updateDisplay: () -> - window.debug_adjUpdateDisplay++ - @spanClass = if @adj.config.emptyBox then 'empty' else '' - @displayValue = @adj.getDisplayValue() - center = @adj.getCenter() - target = @adj.getTarget() - #log("adj updateDisplay center #{center.elements} target #{target.elements}") - return if isNaN(center.e(1)) - @centerStyle = {x: center.e(1), y: center.e(2)} - @dia?.setAdjusterConnector(@adj.id + '/conn', center, target) + go = => + window.debug_adjUpdateDisplay++ + @spanClass = if @adj.config.emptyBox then 'empty' else '' + @displayValue = @adj.getDisplayValue() + center = @adj.getCenter() + target = @adj.getTarget() + #log("adj updateDisplay center #{center.elements} target #{target.elements}") + return if isNaN(center.e(1)) + @centerStyle = {x: center.e(1), y: center.e(2)} + @dia.setAdjusterConnector(@adj.id + '/conn', center, target) + @debounce('updateDisplay', go, 1) attached: -> drag = d3.drag() @@ -540,7 +569,7 @@ Polymer @updateDisplay() detached: -> - @dia.clearElem(@adj.id + '/conn') + @dia.clearElem(@adj.id, ['/conn']) svgPathFromPoints = (pts) -> @@ -582,11 +611,12 @@ Polymer elem.setAttribute(k, v) return elem - clearElem: (uri) -> - elem = @elemById[uri] - if elem - elem.remove() - delete @elemById[uri] + clearElem: (uri, suffixes) -> # todo: caller shouldn't have to know suffixes! + for suff in suffixes + elem = @elemById[uri+suff] + if elem + elem.remove() + delete @elemById[uri+suff] anyPointsInView: (pts) -> for pt in pts @@ -598,8 +628,7 @@ Polymer areaId = uri + '/area' labelId = uri + '/label' if not @anyPointsInView(curvePts) - @clearElem(areaId) - @clearElem(labelId) + @clearElem(uri, ['/area', '/label']) return elem = @getOrCreateElem(areaId, 'notes', 'path', {style:"fill:#53774b; stroke:#000000; stroke-width:1.5;"}) @@ -637,7 +666,7 @@ Polymer setAdjusterConnector: (uri, center, target) -> id = uri + '/adj' if not @anyPointsInView([center, target]) - @clearElem(uri) + @clearElem(id, ['']) return elem = @getOrCreateElem(uri, 'connectors', 'path', {style: "fill:none;stroke:#d4d4d4;stroke-width:0.9282527;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.78475821, 2.78475821;stroke-dashoffset:0;"}) elem.setAttribute('d', svgPathFromPoints([center, target]))