Changeset - 6001c86f9fca
[Not reviewed]
default
0 1 0
drewp@bigasterisk.com - 9 years ago 2016-06-11 21:16:06
drewp@bigasterisk.com
timeline: 1 row for now since i think the others were duplicating all notes (and also not showing them)
Ignore-this: a763f8f93b04f6473600944e9533437d
1 file changed with 2 insertions and 2 deletions:
0 comments (0 inline, 0 general)
light9/web/timeline/timeline.coffee
Show inline comments
 
@@ -20,462 +20,462 @@ Polymer
 
    zoomInX: {type: Object, notify: true}
 
  width: ko.observable(1)
 
  listeners:
 
    'iron-resize': '_onIronResize'
 
  observers: [
 
    'setSong(playerSong, followPlayerSong)'
 
    ]
 
  _onIronResize: ->
 
    @width(@offsetWidth)
 
  _onSongTime: (t) ->
 
    @viewState.cursor.t(t)
 
  _onSongDuration: (d) ->
 
    @viewState.zoomSpec.duration(d)
 
  setSong: (s) ->
 
    @song = @playerSong if @followPlayerSong
 

	
 
  ready: ->
 
    @viewState =
 
      zoomSpec:
 
        duration: ko.observable(100)
 
        t1: ko.observable(0) # need validation to stay in bounds and not go too close
 
        t2: ko.observable(100)
 
      cursor:
 
        t: ko.observable(20)
 
      mouse:
 
        pos: ko.observable($V([0,0]))
 
    @fullZoomX = d3.scaleLinear()
 
    @zoomInX = d3.scaleLinear()
 
    @setAdjuster = @$.adjusters.setAdjuster.bind(@$.adjusters)
 
    
 
  attached: ->
 
    @dia = @$.dia
 
    ko.computed(@zoomOrLayoutChanged.bind(@)).extend({rateLimit: 5})
 
    ko.computed(@songTimeChanged.bind(@))
 

	
 
    @trackMouse()
 
    @bindKeys()
 
    @bindWheelZoom()
 

	
 
    @makeZoomAdjs()
 

	
 
  zoomOrLayoutChanged: ->
 
    @fullZoomX.domain([0, @viewState.zoomSpec.duration()])
 
    @fullZoomX.range([0, @width()])
 

	
 
    # had trouble making notes update when this changes
 
    zoomInX = d3.scaleLinear()
 
    zoomInX.domain([@viewState.zoomSpec.t1(), @viewState.zoomSpec.t2()])
 
    zoomInX.range([0, @width()])
 
    @zoomInX = zoomInX
 

	
 
    # todo: these run a lot of work purely for a time change    
 
    @dia.setTimeAxis(@width(), @$.zoomed.$.audio.offsetTop, @zoomInX)
 
    @$.adjusters.updateAllCoords()
 

	
 
    @songTimeChanged()
 

	
 
  songTimeChanged: ->
 
    @dia.setCursor(@$.audio.offsetTop, @$.audio.offsetHeight,
 
                     @$.zoomed.$.time.offsetTop,
 
                     @$.zoomed.$.time.offsetHeight,
 
                     @fullZoomX, @zoomInX, @viewState.cursor)
 
    
 
  trackMouse: ->
 
    # not just for show- we use the mouse pos sometimes
 
    for evName in ['mousemove', 'touchmove']
 
      @addEventListener evName, (ev) =>
 
        ev.preventDefault()
 

	
 
        # todo: consolidate with _editorCoordinates version
 
        if ev.touches?.length
 
          ev = ev.touches[0]
 

	
 
        @root = @getBoundingClientRect()
 
        @viewState.mouse.pos($V([ev.pageX - @root.left, ev.pageY - @root.top]))
 

	
 
        @dia.setMouse(@viewState.mouse.pos())
 
        @sendMouseToVidref()
 

	
 
  sendMouseToVidref: ->
 
    now = Date.now()
 
    if (!@$.vidrefLastSent? || @$.vidrefLastSent < now - 200) && !@songPlaying
 
      @$.vidrefTime.body = {t: @latestMouseTime(), source: 'timeline'}
 
      @$.vidrefTime.generateRequest()
 
      @$.vidrefLastSent = now
 

	
 
  latestMouseTime: ->
 
    @zoomInX.invert(@viewState.mouse.pos().e(1))
 

	
 
  bindWheelZoom: ->
 
    @$.zoomed.addEventListener 'mousewheel', (ev) =>
 
      zs = @viewState.zoomSpec
 

	
 
      center = @latestMouseTime()
 
      left = center - zs.t1()
 
      right = zs.t2() - center
 
      scale = Math.pow(1.005, ev.deltaY)
 

	
 
      zs.t1(center - left * scale)
 
      zs.t2(center + right * scale)
 

	
 
  animatedZoom: (newT1, newT2, secs) ->
 
    fps = 30
 
    oldT1 = @viewState.zoomSpec.t1()
 
    oldT2 = @viewState.zoomSpec.t2()
 
    lastTime = 0
 
    for step in [0..secs * fps]
 
      frac = step / (secs * fps)
 
      do (frac) =>
 
        gotoStep = =>
 
          @viewState.zoomSpec.t1((1 - frac) * oldT1 + frac * newT1)
 
          @viewState.zoomSpec.t2((1 - frac) * oldT2 + frac * newT2)
 
        delay = frac * secs * 1000
 
        setTimeout(gotoStep, delay)
 
        lastTime = delay
 
    setTimeout(=>
 
        @viewState.zoomSpec.t1(newT1)
 
        @viewState.zoomSpec.t2(newT2)
 
      , lastTime + 10)  
 
    
 
  bindKeys: ->
 
    shortcut.add "Ctrl+P", (ev) =>
 
      @$.music.seekPlayOrPause(@latestMouseTime())
 

	
 
    zoomAnimSec = .1
 
    shortcut.add "Ctrl+Escape", =>
 
      @animatedZoom(0, @viewState.zoomSpec.duration(), zoomAnimSec)
 
    shortcut.add "Shift+Escape", =>
 
      @animatedZoom(@songTime - 2, @viewState.zoomSpec.duration(), zoomAnimSec)
 
    shortcut.add "Escape", =>
 
      zs = @viewState.zoomSpec
 
      visSeconds = zs.t2() - zs.t1()
 
      margin = visSeconds * .4
 
      # buggy: really needs t1/t2 to limit their ranges
 
      if @songTime < zs.t1() or @songTime > zs.t2() - visSeconds * .6
 
        newCenter = @songTime + margin
 
        @animatedZoom(newCenter - visSeconds / 2,
 
                      newCenter + visSeconds / 2, zoomAnimSec)
 

	
 
  makeZoomAdjs: ->
 
    log('makeZoomAdjs', @adjs)
 
    yMid = @$.audio.offsetTop + @$.audio.offsetHeight / 2
 
    dur = @viewState.zoomSpec.duration
 
    
 
    valForPos = (pos) =>
 
        x = pos.e(1)
 
        t = @fullZoomX.invert(x)
 
    @setAdjuster('zoom-left', new AdjustableFloatObservable({
 
      observable: @viewState.zoomSpec.t1,
 
      getTarget: () =>
 
        $V([@fullZoomX(@viewState.zoomSpec.t1()), yMid])
 
      getSuggestedTargetOffset: () => $V([-50, 0])
 
      getValueForPos: valForPos
 
    }))
 

	
 
    @setAdjuster('zoom-right', new AdjustableFloatObservable({
 
      observable: @viewState.zoomSpec.t2,
 
      getTarget: () =>
 
        $V([@fullZoomX(@viewState.zoomSpec.t2()), yMid])
 
      getSuggestedTargetOffset: () => $V([50, 0])
 
      getValueForPos: valForPos
 
    }))
 

	
 
    panObs = ko.pureComputed({
 
        read: () =>
 
          (@viewState.zoomSpec.t1() + @viewState.zoomSpec.t2()) / 2
 
        write: (value) =>
 
          zs = @viewState.zoomSpec
 
          span = zs.t2() - zs.t1()
 
          zs.t1(value - span / 2)
 
          zs.t2(value + span / 2)
 
      })
 

	
 
    @setAdjuster('zoom-pan', new AdjustableFloatObservable({
 
      observable: panObs
 
      emptyBox: true
 
      # fullzoom is not right- the sides shouldn't be able to go
 
      # offscreen
 
      getTarget: () => $V([@fullZoomX(panObs()), yMid])
 
      getSuggestedTargetOffset: () => $V([0, 0])
 
      getValueForPos: valForPos
 
      }))
 
      
 

	
 
Polymer
 
  is: 'light9-timeline-time-zoomed'
 
  behaviors: [ Polymer.IronResizableBehavior ]
 
  properties:
 
    graph: { type: Object, notify: true }
 
    dia: { type: Object, notify: true }
 
    song: { type: String, notify: true }
 
    zoomInX: { type: Object, notify: true }
 
    rows: { value: [0, 1, 2] }
 
    rows: { value: [0] }
 
    zoom: { type: Object, notify: true, observer: 'onZoom' }
 
    zoomFlattened: { type: Object, notify: true }
 
  onZoom: ->
 
    updateZoomFlattened = ->
 
      @zoomFlattened = ko.toJS(@zoom)
 
    ko.computed(updateZoomFlattened.bind(@))
 
  ready: ->
 

	
 
  attached: ->
 
    root = @closest('light9-timeline-editor')
 
    setupDrop @, @$.rows, root, (effect, pos) =>
 
      U = (x) -> @graph.Uri(x)
 

	
 
      # we could probably accept some initial overrides right on the
 
      # effect uri, maybe as query params
 
      
 
      if not @graph.contains(effect, RDF + 'type', U(':Effect'))
 
        log("drop #{effect} is not an effect")
 
        return
 
      
 
      dropTime = @zoomInX.invert(pos.e(1))
 
      @makeNewNote(effect, dropTime)
 
      
 
  makeNewNote: (effect, dropTime) ->
 
    U = (x) -> @graph.Uri(x)
 
    quad = (s, p, o) => {subject: s, predicate: p, object: o, graph: @song}
 
      
 
    newNote = @graph.nextNumberedResource("#{@song}/n")
 
    newCurve = @graph.nextNumberedResource("#{newNote}c")
 
    points = @graph.nextNumberedResources("#{newCurve}p", 4)
 

	
 
    curveQuads = [
 
        quad(@song, U(':note'), newNote)
 
        quad(newNote, RDF + 'type', U(':Note'))
 
        quad(newNote, U(':originTime'), @graph.LiteralRoundedFloat(dropTime))
 
        quad(newNote, U(':effectClass'), effect)
 
        quad(newNote, U(':curve'), newCurve)
 
        quad(newCurve, RDF + 'type', U(':Curve'))
 
        quad(newCurve, U(':attr'), U(':strength'))
 
      ]        
 
    pointQuads = []
 
    for i in [0...4]
 
      pt = points[i]
 
      pointQuads.push(quad(newCurve, U(':point'), pt))
 
      pointQuads.push(quad(pt, U(':time'), @graph.LiteralRoundedFloat(i)))
 
      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)'
 
    'update(song)'
 
    ]
 
  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)
 

	
 

	
 
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)'
 
    'update(graph, dia, uri, zoomInX, setAdjuster)'
 
    ]
 
  ready: ->
 

	
 
  detached: ->
 
    @dia.clearElem(@uri)
 

	
 
  onUri: ->
 
    @graph.runHandler(@update.bind(@), "note updates #{@uri}")
 
    
 
  update: ->
 
    # update our note DOM and SVG elements based on the graph
 
    U = (x) -> @graph.Uri(x)
 
    try
 
      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)
 
          @setAdjuster(@uri+'/offset', new AdjustableFloatObject({
 
            graph: @graph
 
            subj: @uri
 
            pred: @graph.Uri(':originTime')
 
            ctx: @graph.Uri(@song)
 
            getTargetPosForValue: (value) => $V([@zoomInX(value), 600])
 
            getValueForPos: (pos) => @zoomInX.invert(pos.e(1))
 
            getSuggestedTargetOffset: () => $V([0, -80])
 
          }))
 

	
 
          for pointNum in [0, 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), 600])
 
              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'))
 
              )
 
            console.log(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, adjustable) ->
 
    # callers register/unregister the Adjustables they want us
 
    # to make adjuster elements for. Caller invents adjId.
 
    adjustable.id = adjId
 
    if not @adjs[adjId] or not adjustable?
 
      @adjs[adjId] = adjustable
 
      @debounce('adjsChanged', @adjsChanged.bind(@), 1)
 
    
 
  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
 

	
 
  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 }
 
    displayValue: { type: String }
 
    centerStyle: { type: Object }
 
    spanClass: { type: String, value: '' }
 

	
 
  onAdj: (adj) ->
 
    log('onAdj', @id)
 
    @adj.subscribe(@updateDisplay.bind(this))
 
    @graph.runHandler(@updateDisplay.bind(@))
 

	
 
  updateDisplay: () ->
 
    log('updateDisplay', @id)
 
    @spanClass = if @adj.config.emptyBox then 'empty' else ''
 
    @displayValue = @adj.getDisplayValue()
 
    center = @adj.getCenter()
 
    target = @adj.getTarget()
 
    log('ct', center.elements, target.elements)
 
    return if isNaN(center.e(1))
 
    @centerStyle = {x: center.e(1), y: center.e(2)}
 
    @dia?.setAdjusterConnector(@adj.id + '/conn', center, target)
 
        
 
  attached: ->
 
    drag = d3.drag()
 
    sel = d3.select(@$.label)
 
    sel.call(drag)
 
    drag.subject((d) -> {x: @offsetLeft, y: @offsetTop})
0 comments (0 inline, 0 general)