Changeset - 59be536746eb
[Not reviewed]
default
0 1 0
drewp@bigasterisk.com - 7 years ago 2018-06-02 05:52:33
drewp@bigasterisk.com
refactor: add BrickLayout object. no major changes yet
Ignore-this: 176e207524ddc729c4cfdf122beb2c99
1 file changed with 74 insertions and 35 deletions:
0 comments (0 inline, 0 general)
light9/web/timeline/timeline.coffee
Show inline comments
 
@@ -267,40 +267,59 @@ coffeeElementSetup(class TimelineEditor 
 

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

	
 
nr=0
 
class BrickLayout
 
  constructor: (@viewState, @numRows) ->
 
    @noteRow = {} # uristr: row, t0, t1
 
  addNote: (n) ->
 
    @noteRow[n.uri.value] = {row: nr%6, t0: 0, t1: 0}
 
    nr++
 
  setNoteSpan: (n, t0, t1) ->
 
    @noteRow[n.uri.value].t0 = t0
 
    @noteRow[n.uri.value].t1 = t1
 
  delNote: (n) ->
 
    delete @noteRow[n.uri.value]
 
  rowBottom: (row) -> @viewState.rowsY() + 20 + 150 * row + 140
 
  yForVFor: (n) ->
 
    row = @noteRow[n.uri.value].row
 
    rowBottom = @rowBottom(row)
 
    rowTop = rowBottom - 140
 
    (v) => rowBottom + (rowTop - rowBottom) * v      
 

	
 
# plan: in here, turn all the notes into simple js objects with all
 
# their timing data and whatever's needed for adjusters. From that, do
 
# the brick layout. update only changing adjusters.
 
coffeeElementSetup(class TimeZoomed extends Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element)
 
  @is: 'light9-timeline-time-zoomed'
 
  @getter_properties:
 
    graph: { type: Object, notify: true }
 
    project: { type: Object }
 
    selection: { type: Object, notify: true }
 
    song: { type: String, notify: true }
 
    viewState: { type: Object, notify: true }
 
    inlineAttrConfigs: { type: Array, value: [] } # only for inlineattrs that should be displayed
 
  @getter_observers: [
 
    '_onGraph(graph, setAdjuster, song, viewState, project)',
 
    'onZoom(viewState)',
 
    '_onViewState(viewState)',
 
  ]
 
  constructor: ->
 
    super()
 
    @numRows = 6
 
    @noteByUriStr = new Map()
 
    @stage = new PIXI.Container()
 
    @stage.interactive=true
 

	
 
    @renderer = PIXI.autoDetectRenderer({
 
      backgroundColor: 0x606060,
 
      antialias: true,
 
      forceCanvas: true,
 
@@ -328,88 +347,85 @@ coffeeElementSetup(class TimeZoomed exte
 
  _onResize: ->
 
    @$.rows.firstChild.style.position = 'relative'
 
    @$.rows.firstChild.style.top = -@viewState.rowsY() + 'px'
 

	
 
    @renderer.resize(@clientWidth, @clientHeight + @viewState.rowsY())
 

	
 
    @dirty()
 

	
 
  _onGraph: (graph, setAdjuster, song, viewState, project)->
 
    return unless @song # polymer will call again
 
    @graph.runHandler(@gatherNotes.bind(@), 'zoom notes')
 

	
 
  _onViewState: (viewState) ->
 
    @brickLayout = new BrickLayout(@viewState, @numRows)
 

	
 
  noteDirty: -> @dirty()
 
    
 
  onZoom: ->
 
    updateZoomFlattened = ->
 
      log('updateZoomFlattened')
 
      @zoomFlattened = ko.toJS(@viewState.zoomSpec)
 
    ko.computed(updateZoomFlattened.bind(@))
 

	
 
  gatherNotes: ->
 
    U = (x) => @graph.Uri(x)
 
    return unless @song?
 
    songNotes = @graph.objects(U(@song), U(':note'))
 

	
 
    toRemove = new Set(@noteByUriStr.keys())
 
    
 
    noteNum = 0
 
    for uri in _.sortBy(songNotes, 'id')
 
      had = toRemove.delete(uri.value)
 
      if not had
 
        @_addNote(uri, noteNum)
 
      noteNum = noteNum + 1
 
        @_addNote(uri)
 

	
 
    toRemove.forEach @_delNote.bind(@)
 

	
 
    @dirty()
 

	
 
  isActiveNote: (note) -> @noteByUriStr.has(note.value)
 

	
 
  _repaint: ->
 
    @_drawGrid()  
 
    @renderer.render(@stage)
 

	
 
  _drawGrid: ->
 
    # maybe someday this has snappable timing markers too
 
    @bg.removeChildren()
 
    gfx = new PIXI.Graphics()
 
    @bg.addChild(gfx)
 

	
 
    gfx.lineStyle(1, 0x222222, 1)
 
    for row in [0...@numRows]
 
      y = @rowBottom(row)
 
      y = @brickLayout.rowBottom(row)
 
      gfx.moveTo(0, y)
 
      gfx.lineTo(@clientWidth, y)
 

	
 
  rowTop: (n) -> @viewState.rowsY() + 20 + 150 * n
 

	
 
  rowBottom: (n) -> @rowTop(n) + 140
 

	
 
  _addNote: (uri, noteNum) ->
 
  _addNote: (uri) ->
 
    U = (x) => @graph.Uri(x)
 
    
 
    con = new PIXI.Container()
 
    con.interactive=true
 
    @stage.addChild(con)
 
    
 
    row = noteNum % @numRows
 
    rowTop = @rowTop(row)
 
    note = new Note(@, con, @project, @graph, @selection, uri, @setAdjuster, U(@song), @viewState, @rowTop(row), @rowBottom(row))
 
    note = new Note(@, con, @project, @graph, @selection, uri, @setAdjuster, U(@song), @viewState, @brickLayout)
 
    # this must come before the first Note.draw
 
    @noteByUriStr.set(uri.value, note)
 
    @brickLayout.addNote(note)
 
    note.initWatchers()
 

	
 
  _delNote: (uriStr) ->
 
    n = @noteByUriStr.get(uriStr)
 
    @brickLayout.delNote(n)
 
    @stage.removeChild(n.container)
 
    n.destroy()
 
    @noteByUriStr.delete(uriStr)
 
            
 
  onDrop: (effect, pos) ->
 
    U = (x) => @graph.Uri(x)
 

	
 
    return unless effect and effect.match(/^http/)
 

	
 
    # we could probably accept some initial overrides right on the
 
    # effect uri, maybe as query params
 

	
 
@@ -437,46 +453,47 @@ coffeeElementSetup(class TimeZoomed exte
 
        index += 1
 
    else
 
      index = 0
 
      for c in @inlineAttrConfigs
 
        if c.uri.equals(note)
 
          @splice('inlineAttrConfigs', index, 1, config)
 
          return
 
        index += 1
 
      @push('inlineAttrConfigs', config)
 
)
 

	
 

	
 

	
 
coffeeElementSetup(class TimeAxis extends Polymer.Element
 
  @is: "light9-timeline-time-axis",
 
  @getter_properties:
 
    viewState: { type: Object, notify: true, observer: "onViewState" }
 
  onViewState: ->
 
    ko.computed =>
 
      dependOn = [@viewState.zoomSpec.t1(), @viewState.zoomSpec.t2()]
 
      pxPerTick = 50
 
      axis = d3.axisTop(@viewState.zoomInX).ticks(@viewState.width() / pxPerTick)
 
      d3.select(@$.axis).call(axis)
 
)
 

	
 

	
 
# Maintains a pixi object, some adjusters, and inlineattrs corresponding to a note
 
# in the graph.
 
class Note
 
  constructor: (@parentElem, @container, @project, @graph, @selection, @uri, @setAdjuster, @song, @viewState, @rowTopY, @rowBotY) ->
 
  constructor: (@parentElem, @container, @project, @graph, @selection, @uri, @setAdjuster, @song, @viewState, @brickLayout) ->
 
    @adjusterIds = new Set() # id string
 

	
 
  initWatchers: ->
 
    @graph.runHandler(@draw.bind(@), "note draw #{@uri.value}")
 
    ko.computed @draw.bind(@)
 
    @graph.runHandler(@update.bind(@), "note update #{@uri.value}")
 
    ko.computed @update.bind(@)
 

	
 
  destroy: ->
 
    log('destroy', @uri.value)
 
    @isDetached = true
 
    @clearAdjusters()
 
    @parentElem.updateInlineAttrs(@uri, null)
 

	
 
  clearAdjusters: ->
 
    @adjusterIds.forEach (i) =>
 
      @setAdjuster(i, null)
 
    @adjusterIds.clear()
 

	
 
@@ -484,60 +501,82 @@ class Note
 
    U = (x) => @graph.Uri(x)
 
    originTime = @graph.floatValue(subj, U(':originTime'))
 

	
 
    for curve in @graph.objects(subj, U(':curve'))
 
      if @graph.uriValue(curve, U(':attr')).equals(curveAttr)
 
        return @project.getCurvePoints(curve, originTime)
 
    throw new Error("curve #{@uri.value} has no attr #{curveAttr.value}")
 

	
 
  midPoint: (i0, i1) ->
 
    p0 = @worldPts[i0]
 
    p1 = @worldPts[i1]
 
    p0.x(.5).add(p1.x(.5))
 
    
 
  draw: ->
 
    if not @parentElem.isActiveNote(@uri)
 
      # stale redraw call
 
      return
 

	
 
  _planDrawing: ->
 
    U = (x) => @graph.Uri(x)
 
    [pointUris, @worldPts] = @getCurvePoints(@uri, U(':strength'))
 
    [pointUris, worldPts] = @getCurvePoints(@uri, U(':strength'))
 
    effect = @graph.uriValue(@uri, U(':effectClass'))
 

	
 
    yForV = (v) => @rowBotY + (@rowTopY - @rowBotY) * v
 
    dependOn = [@viewState.zoomSpec.t1(), @viewState.zoomSpec.t2(), @viewState.width()]
 
    screenPts = (new PIXI.Point(@viewState.zoomInX(pt.e(1)), yForV(pt.e(2))) for pt in @worldPts)
 
    yForV = @brickLayout.yForVFor(@)
 
    dependOn = [@viewState.zoomSpec.t1(),
 
                @viewState.zoomSpec.t2(),
 
                @viewState.width()]
 
    screenPts = (new PIXI.Point(@viewState.zoomInX(pt.e(1)),
 
                                yForV(pt.e(2))) for pt in worldPts)
 
    return {
 
      yForV: yForV
 
      worldPts: worldPts
 
      screenPts: screenPts
 
      effect: effect
 
    }
 

	
 
  redraw: (params) ->
 
    @container.removeChildren()
 
    @graphics = new PIXI.Graphics({nativeLines: false})
 
    @graphics.interactive = true
 
    @container.addChild(@graphics)
 

	
 
    if @uri.equals(@selection.hover())
 
      @_traceBorder(screenPts, 12, 0x888888)
 
      @_traceBorder(params.screenPts, 12, 0x888888)
 
    @selection.selected().forEach (s) =>
 
      if s.equals(@uri)
 
        @_traceBorder(screenPts, 6, 0xff2900)
 
        @_traceBorder(params.screenPts, 6, 0xff2900)
 

	
 
    shape = new PIXI.Polygon(screenPts)
 
    @graphics.beginFill(@_noteColor(effect), .313)
 
    shape = new PIXI.Polygon(params.screenPts)
 
    @graphics.beginFill(@_noteColor(params.effect), .313)
 
    @graphics.drawShape(shape)
 
    @graphics.endFill()
 

	
 
    @_traceBorder(screenPts, 2, 0xffd900)
 
    @_traceBorder(params.screenPts, 2, 0xffd900)
 
                 
 
  update: ->
 
    if not @parentElem.isActiveNote(@uri)
 
      # stale redraw call
 
      return
 

	
 
    if @worldPts
 
      @brickLayout.setNoteSpan(@, @worldPts[0].e(1),
 
                               @worldPts[@worldPts.length - 1].e(1))
 

	
 
    params = @_planDrawing()
 
    @worldPts = params.worldPts
 

	
 
    @redraw(params)
 

	
 
    @_addMouseBindings()
 

	
 
    curveWidthCalc = () => @project.curveWidth(@worldPts)
 
    @_updateAdjusters(screenPts, @worldPts, curveWidthCalc, yForV, @viewState.zoomInX, @song)
 
    @_updateInlineAttrs(screenPts)
 
    @_updateAdjusters(params.screenPts, @worldPts, curveWidthCalc,
 
                      params.yForV, @viewState.zoomInX, @song)
 
    @_updateInlineAttrs(params.screenPts, params.yForV)
 
    @parentElem.noteDirty()
 

	
 
  _traceBorder: (screenPts, thick, color) ->
 
    @graphics.lineStyle(thick, color, 1)
 
    @graphics.moveTo(screenPts[0].x, screenPts[0].y)
 
    for p in screenPts.slice(1)
 
      @graphics.lineTo(p.x, p.y)
 

	
 
  _addMouseBindings: () ->
 
    @graphics.on 'mousedown', (ev) =>
 
      @_onMouseDown(ev)
 

	
 
@@ -558,64 +597,64 @@ class Note
 

	
 
  patchCouldAffectMe: (patch) ->
 
    if patch and patch.addQuads # sometimes patch is a polymer-sent value. @update is used as a listener too
 
      if patch.addQuads.length == patch.delQuads.length == 1
 
        add = patch.addQuads[0]
 
        del = patch.delQuads[0]
 
        if (add.predicate.equals(del.predicate) and del.predicate.equals(@graph.Uri(':time')) and add.subject.equals(del.subject))
 
          timeEditFor = add.subject
 
          if @worldPts and timeEditFor not in @pointUris
 
            return false
 
    return true
 

	
 
  update: (patch) ->
 
  xupdate: (patch) ->
 
    # update our note DOM and SVG elements based on the graph
 
    if not @patchCouldAffectMe(patch)
 
      # as autodep still fires all handlers on all patches, we just
 
      # need any single dep to cause another callback. (without this,
 
      # we would no longer be registered at all)
 
      @graph.subjects(@uri, @uri, @uri)
 
      return
 
    if @isDetached?
 
      return
 

	
 
    @_updateDisplay()
 

	
 
  _updateAdjusters: (screenPts, worldPts, curveWidthCalc, yForV, zoomInX, ctx) ->
 
    # todo: allow offset even on more narrow notes
 
    if screenPts[screenPts.length - 1].x - screenPts[0].x < 100 or screenPts[0].x > @parentElem.offsetWidth or screenPts[screenPts.length - 1].x < 0
 
      @clearAdjusters()
 
    else
 
      @_makeOffsetAdjuster(yForV, curveWidthCalc, ctx)
 
      @_makeCurvePointAdjusters(yForV, worldPts, ctx)
 
      @_makeFadeAdjusters(yForV, zoomInX, ctx, worldPts)
 

	
 
  _updateInlineAttrs: (screenPts) ->
 
  _updateInlineAttrs: (screenPts, yForV) ->
 
    w = 280
 

	
 
    leftX = Math.max(2, screenPts[Math.min(1, screenPts.length - 1)].x + 5)
 
    rightX = screenPts[Math.min(2, screenPts.length - 1)].x - 5
 
    if screenPts.length < 3
 
      rightX = leftX + w
 

	
 
    if rightX - leftX < w or rightX < w or leftX > @parentElem.offsetWidth
 
      @parentElem.updateInlineAttrs(@uri, null)
 
      return
 

	
 
    config = {
 
      uri: @uri,
 
      left: leftX,
 
      top: @rowTopY + 5,
 
      top: yForV(1) + 5,
 
      width: w,
 
      height: @rowBotY - @rowTopY - 15,
 
      height: yForV(0) - yForV(1) - 15,
 
      }
 

	
 
    @parentElem.updateInlineAttrs(@uri, config)
 

	
 
  _makeCurvePointAdjusters: (yForV, worldPts, ctx) ->
 
    for pointNum in [0...worldPts.length]
 
      @_makePointAdjuster(yForV, worldPts, pointNum, ctx)
 

	
 
  _makePointAdjuster: (yForV, worldPts, pointNum, ctx) ->
 
    U = (x) => @graph.Uri(x)
 

	
 
    adjId = @uri.value + '/p' + pointNum
0 comments (0 inline, 0 general)