Changeset - b72198260bf0
[Not reviewed]
default
0 2 0
Drew Perttula - 9 years ago 2016-06-12 10:01:24
drewp@bigasterisk.com
large fixes to timeline note&adjuster refreshes. seems ok now
Ignore-this: 985e3de87d1896c89f93701473a4b86
2 files changed with 81 insertions and 58 deletions:
0 comments (0 inline, 0 general)
light9/web/timeline/timeline-elements.html
Show inline comments
 
@@ -177,92 +177,85 @@ background: rgba(126, 52, 245, 0.0784313
 

	
 

	
 
<!-- seconds labels -->
 
<dom-module id="light9-timeline-time-axis">
 
  <template>
 
    <style>
 
     div {
 
         width: 100%;
 
         height: 31px;
 
     }
 
    </style>
 
    <div></div>
 
  </template>
 
</dom-module>
 

	
 
<!-- one row of notes -->
 
<dom-module id="light9-timeline-graph-row">
 
  <template>
 
    <style>
 
     :host {
 
         border-top: 1px solid black;
 
         display: flex;
 
     }
 
    </style>
 
    <template is="dom-repeat" items="{{noteUris}}">
 
      <light9-timeline-note graph="{{graph}}"
 
                            dia="{{dia}}"
 
                            uri="{{item}}"
 
                            set-adjuster="{{setAdjuster}}"
 
                            song="{{song}}"
 
                            zoom-in-x="{{zoomInX}}">
 
      </light9-timeline-note>
 
    </template>
 
    <!-- light9-timeline-note repeated here -->
 
  </template>
 
</dom-module>
 

	
 
<!-- One trapezoid note shape in a row.
 
     This element has the right Y coords.
 
     We compute X coords from the zoom setting.
 
     diagram-layer draws the note body. -->
 
<dom-module id="light9-timeline-note">
 
  <template>
 
    <style>
 
     :host {
 
         display: block;
 
         background: green;
 
         /* outline: 2px solid red; */
 
     }
 
    </style>
 
    <light9-timeline-note-inline-attrs></light9-timeline-note-inline-attrs>
 
  </template>
 
</dom-module>
 

	
 
<!-- All the adjusters you can edit or select.
 
     This element manages their layout and suppresion.
 
     Owns the selection.
 
     Maybe includes selecting things that don't even have adjusters.
 
     Maybe manages the layout of other labels and text too, to avoid overlaps.
 
   -->
 
<dom-module id="light9-timeline-adjusters">
 
  <template>
 
    <style>
 
     :host {
 
         pointer-events: none; /* restored on the individual adjusters */
 
     }
 

	
 
    </style>
 
    <div id="all">
 
      <!-- light9-timeline-adjuster repeated here  -->
 
    </div>
 
  </template>
 
</dom-module>
 

	
 
<!-- Yellow dotted handle that you can adjust to edit something.
 
     Knows an attribute to edit and the true screen location, even if
 
     parent <light9-timeline-adjusters> has offset us a bit to avoid a
 
     text overlap.
 
     Draws affordance arrows and a connector line if we're far
 
     away from the point that we edit.
 
     May grow to a bigger editor when you click or drag.
 
   -->
 
<dom-module id="light9-timeline-adjuster">
 
  <template>
 
    <style>
 
     /* 
 
     #top {outline: 2px solid rgba(255, 0, 0, 0.25);}
 
     table {outline: 2px solid rgba(0, 0, 255, 0.19);}
 
     */
 
     
 
     #top {
 
         position: absolute;
 
         display: inline-block;
 
     }
 
@@ -310,26 +303,27 @@ background: rgba(126, 52, 245, 0.0784313
 
<dom-module id="light9-timeline-note-inline-attrs">
 
  <template>
 

	
 
  </template>
 
  <script>
 
   Polymer({
 
       is: "light9-timeline-note-inline-attrs",
 
       properties: {
 
       }
 
   });
 
  </script>
 
</dom-module>
 

	
 
<script src="/lib/sylvester/sylvester.js"></script>
 
<script src="/lib/d3/build/d3.min.js"></script>
 

	
 
<!-- version with https://github.com/RubenVerborgh/N3.js/pull/61 -->
 
<script src="/lib/N3.js-pull61/browser/n3-browser.js"></script>
 
<!-- master version -->
 
<xxscript src="/lib/N3.js/browser/n3-browser.js"></script>
 
  
 
<script src="/lib/knockout/dist/knockout.debug.js"></script>
 
<script src="/lib/shortcut/index.js"></script>
 
<script src="/lib/async/dist/async.js"></script>
 
<script src="/lib/underscore/underscore-min.js"></script>
 
<script src="adjustable.js"></script>
 
<script src="timeline.js"></script>
light9/web/timeline/timeline.coffee
Show inline comments
 
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 ]
 
  properties:
 
    viewState: { type: Object }
 
    debug: {type: String}
 
    graph: {type: Object, notify: true}
 
    setAdjuster: {type: Function, notify: true}
 
    playerSong: {type: String, notify: true}
 
    followPlayerSong: {type: Boolean, notify: true, value: true}
 
    song: {type: String, notify: true}
 
    show: {value: 'http://light9.bigasterisk.com/show/dance2016'}
 
    songTime: {type: Number, notify: true, observer: '_onSongTime'}
 
    songDuration: {type: Number, notify: true, observer: '_onSongDuration'}
 
    songPlaying: {type: Boolean, notify: true}
 
    fullZoomX: {type: Object, notify: true}
 
    zoomInX: {type: Object, notify: true}
 
  width: ko.observable(1)
 
  listeners:
 
    'iron-resize': '_onIronResize'
 
  observers: [
 
    'setSong(playerSong, followPlayerSong)'
 
    ]
 
  _onIronResize: ->
 
@@ -272,372 +291,382 @@ Polymer
 
        quad(newCurve, U(':attr'), U(':strength'))
 
      ]        
 
    pointQuads = []
 

	
 
    desiredWidthX = @offsetWidth * .1
 
    desiredWidthT = @zoomInX.invert(desiredWidthX) - @zoomInX.invert(0)
 
    
 
    for i in [0...4]
 
      pt = points[i]
 
      pointQuads.push(quad(newCurve, U(':point'), pt))
 
      pointQuads.push(quad(pt, U(':time'), @graph.LiteralRoundedFloat(i/3 * desiredWidthT)))
 
      pointQuads.push(quad(pt, U(':value'), @graph.LiteralRoundedFloat(i == 1 or i == 2)))
 

	
 
    patch = {
 
      delQuads: []
 
      addQuads: curveQuads.concat(pointQuads)
 
      }
 
    @graph.applyAndSendPatch(patch)
 

	
 
Polymer
 
  is: "light9-timeline-time-axis",
 
  # 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 ]
 
  properties:
 
    graph: { type: Object, notify: true }
 
    dia: { type: Object, notify: true }
 
    song:  { type: String, notify: true }
 
    zoomInX: { type: Object, notify: true }
 
    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) ->
 
  worldPts = []
 
  for pt in graph.objects(curve, graph.Uri(':point'))
 
    v = $V([xOffset + graph.floatValue(pt, graph.Uri(':time')),
 
            graph.floatValue(pt, graph.Uri(':value'))])
 
    v.uri = pt
 
    worldPts.push(v)
 
  worldPts.sort((a,b) -> a.e(1) > b.e(1))
 
  return worldPts
 

	
 
Polymer
 
  is: 'light9-timeline-note'
 
  behaviors: [ Polymer.IronResizableBehavior ]
 
  listeners: 'iron-resize': 'update'
 
  properties:
 
    graph: { type: Object, notify: true }
 
    dia: { type: Object, notify: true }
 
    uri: { type: String, notify: true }
 
    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
 

	
 
      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            
 
          
 
        @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])
 
          }))
 

	
 
          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'))
 
                  )
 
                log('note made this point adj', adj)
 
                adj
 
              )
 
            
 
      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)
 

	
 
    catch e
 
      log("during resize of #{@uri}: #{@e}")
 

	
 
Polymer
 
  is: "light9-timeline-adjusters"
 
  properties:
 
    adjs: { type: Object, notify: true }, # adjId: Adjustable
 
    dia: { type: Object }
 

	
 
  ready: ->
 
    @adjs = {}
 
    
 
  setAdjuster: (adjId, makeAdjustable) ->
 
    # callers register/unregister the Adjustables they want us to make
 
    # adjuster elements for. Caller invents adjId.  makeAdjustable is
 
    # 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]
 
      else
 
        adj = makeAdjustable()
 
        @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)
 
    updateChildren @$.all, Object.keys(@adjs), (newUri) =>
 
        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
 
      child.uri = newUri
 
      child.id = newUri
 
      child.adj = @adjs[newUri]
 
      return child
 
    @updateAllCoords()
 

	
 
  layoutCenters: ->
 
    # push Adjustable centers around to avoid overlaps
 
    qt = d3.quadtree()
 
    qt.extent([[0,0], [8000,8000]])
 
    for _, adj of @adjs
 
      desired = adj.getSuggestedCenter()
 
      output = desired
 
      for tries in [0...2]
 
        nearest = qt.find(output.e(1), output.e(2))
 
        if nearest
 
          dist = output.distanceFrom(nearest)
 
          if dist < 60
 
            away = output.subtract(nearest).toUnitVector()
 
            toScreenCenter = $V([500,200]).subtract(output).toUnitVector()
 
            output = output.add(away.x(60).add(toScreenCenter.x(10)))
 

	
 
      if -50 < output.e(1) < 20 # mostly for zoom-left
 
        output.setElements([
 
          Math.max(20, output.e(1)),
 
          output.e(2)])
 
        
 
      adj.centerOffset = output.subtract(adj.getTarget())
 
      qt.add(output.elements)
 

	
 
  updateAllCoords: ->
 
    @layoutCenters()
 
    
 
    for elem in @querySelectorAll('light9-timeline-adjuster')
 
      elem.updateDisplay()
 
    
 

	
 
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: () ->
 
    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)
 
      @dia.setAdjusterConnector(@adj.id + '/conn', center, target)
 
    @debounce('updateDisplay', go, 1)
 
        
 
  attached: ->
 
    drag = d3.drag()
 
    sel = d3.select(@$.label)
 
    sel.call(drag)
 
    drag.subject((d) -> {x: @offsetLeft, y: @offsetTop})
 
    drag.container(@offsetParent)
 
    drag.on('start', () => @adj?.startDrag())
 
    drag.on 'drag', () =>
 
      @adj?.continueDrag($V([d3.event.x, d3.event.y]))
 
    drag.on('end', () => @adj?.endDrag())
 

	
 
    @updateDisplay()
 

	
 
  detached: ->
 
    @dia.clearElem(@adj.id + '/conn')
 
    @dia.clearElem(@adj.id, ['/conn'])
 

	
 

	
 
svgPathFromPoints = (pts) ->
 
  out = ''
 
  pts.forEach (p) ->
 
    p = p.elements if p.elements # for vec2
 
    if out.length == 0
 
      out = 'M '
 
    else
 
      out += 'L '
 
    out += '' + p[0] + ',' + p[1] + ' '
 
    return
 
  out
 

	
 
Polymer
 
  is: 'light9-timeline-diagram-layer'
 
  properties: {}
 
  ready: ->
 
    @elemById = {}
 

	
 
  setTimeAxis: (width, yTop, scale) ->
 
    pxPerTick = 50
 
    axis = d3.axisTop(scale).ticks(width / pxPerTick)
 
    d3.select(@$.timeAxis).attr('transform', 'translate(0,'+yTop+')').call(axis)
 

	
 
  setMouse: (pos) ->
 
    elem = @getOrCreateElem('mouse-x', 'mouse', 'path', {style: "fill:none;stroke:#fff;stroke-width:0.5;"})
 
    elem.setAttribute('d', svgPathFromPoints([[-9999, pos.e(2)], [9999, pos.e(2)]]))
 
    elem = @getOrCreateElem('mouse-y', 'mouse', 'path', {style: "fill:none;stroke:#fff;stroke-width:0.5;"})
 
    elem.setAttribute('d', svgPathFromPoints([[pos.e(1), -9999], [pos.e(1), 9999]]))   
 

	
 
  getOrCreateElem: (uri, groupId, tag, attrs) ->
 
    elem = @elemById[uri]
 
    if !elem
 
      elem = @elemById[uri] = document.createElementNS("http://www.w3.org/2000/svg", tag)
 
      @$[groupId].appendChild(elem)
 
      elem.setAttribute('id', uri)
 
      for k,v of attrs
 
        elem.setAttribute(k, v)
 
    return elem
 

	
 
  clearElem: (uri) ->
 
    elem = @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]
 
        delete @elemById[uri+suff]
 

	
 
  anyPointsInView: (pts) ->
 
    for pt in pts
 
      if pt.e(1) > -100 && pt.e(1) < 2500
 
        return true
 
    return false
 
    
 
  setNote: (uri, curvePts, effectLabel) ->
 
    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;"})
 
    elem.setAttribute('d', svgPathFromPoints(curvePts))
 

	
 
    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;"})
 
    elem.setAttribute('x', curvePts[0].e(1)+20)
 
    elem.setAttribute('y', curvePts[0].e(2)-10)
 
    elem.innerHTML = effectLabel;
 

	
 
  setCursor: (y1, h1, y2, h2, fullZoomX, zoomInX, cursor) ->
 
    @cursorPath =
 
      top: @querySelector('#cursor1')
 
      mid: @querySelector('#cursor2')
 
      bot: @querySelector('#cursor3')
 
    return if !@cursorPath.top
 
    
 
    xZoomedOut = fullZoomX(cursor.t())
 
    xZoomedIn = zoomInX(cursor.t())
 
    @cursorPath.top.setAttribute 'd', svgPathFromPoints([
 
      [xZoomedOut, y1]
 
      [xZoomedOut, y1 + h1]
 
    ])
 
    @cursorPath.mid.setAttribute 'd', svgPathFromPoints([
 
      [xZoomedIn + 2, y2 + h2]
 
      [xZoomedIn - 2, y2 + h2]
 
      [xZoomedOut - 1, y1 + h1]
 
      [xZoomedOut + 1, y1 + h1]
 
    ]) + ' Z'
 
    @cursorPath.bot.setAttribute 'd', svgPathFromPoints([
 
      [xZoomedIn, y2 + h2]
 
      [xZoomedIn, @offsetParent.offsetHeight]
 
    ])
 

	
 
  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]))
0 comments (0 inline, 0 general)