Changeset - 46d1c0f0a5ce
[Not reviewed]
default
0 3 0
drewp@bigasterisk.com - 7 years ago 2018-06-08 11:50:13
drewp@bigasterisk.com
attempted redraw fixes, but the real problem is in autodeps
Ignore-this: 96cc361f1951d165131d46b8173b6685
3 files changed with 7 insertions and 2 deletions:
0 comments (0 inline, 0 general)
light9/web/timeline/adjustable.coffee
Show inline comments
 
@@ -17,171 +17,173 @@ class Adjustable
 
    # config has:
 
    #   getTarget -> vec2 of current target position
 
    #   getSuggestedTargetOffset -> vec2 pixel offset from target
 
    #   emptyBox -> true if you want no value display
 

	
 
    # updated later by layout algoritm
 
    @handle = $V([0, 0])
 

	
 
  getDisplayValue: () ->
 
    return '' if @config.emptyBox
 
    defaultFormat = d3.format(".4g")(@_getValue())
 
    if @config.getDisplayValue?
 
      return @config.getDisplayValue(@_getValue(), defaultFormat)
 
    defaultFormat
 

	
 
  getSuggestedHandle: () ->
 
    @getTarget().add(@config.getSuggestedTargetOffset())
 

	
 
  getHandle: () -> # vec2 of pixels
 
    @handle
 

	
 
  getTarget: () -> # vec2 of pixels
 
    @config.getTarget()
 

	
 
  subscribe: (onChange) ->
 
    # change could be displayValue or center or target. This likely
 
    # calls onChange right away if there's any data yet.
 
    throw new Error('not implemented')
 

	
 
  startDrag: () ->
 
    @initialTarget = @getTarget()
 

	
 
  continueDrag: (pos) ->
 
    ## pos is vec2 of pixels relative to the drag start
 
    @targetDraggedTo = pos.add(@initialTarget)
 

	
 
  endDrag: () ->
 
    # override
 

	
 
  _editorCoordinates: () -> # vec2 of mouse relative to <l9-t-editor>
 
    return @targetDraggedTo
 
    ev = d3.event.sourceEvent
 

	
 
    if ev.target.tagName == "LIGHT9-TIMELINE-EDITOR"
 
      rootElem = ev.target
 
    else
 
      rootElem = ev.target.closest('light9-timeline-editor')
 

	
 
    if ev.touches?.length
 
      ev = ev.touches[0]
 

	
 
    # storing root on the object to remember it across calls in case
 
    # you drag outside the editor.
 
    @root = rootElem.getBoundingClientRect() if rootElem
 
    offsetParentPos = $V([ev.pageX - @root.left, ev.pageY - @root.top])
 

	
 
    return offsetParentPos
 

	
 
class window.AdjustableFloatObservable extends Adjustable
 
  constructor: (@config) ->
 
    # config also has:
 
    #   observable -> ko.observable we will read and write
 
    #   getValueForPos(pos) -> what should we set to if the user
 
    #                          moves target to this coord?
 
    super()
 
    @ctor2()
 

	
 
  _getValue: () ->
 
    @config.observable()
 

	
 
  continueDrag: (pos) ->
 
    # pos is vec2 of pixels relative to the drag start.
 
    super(pos)
 
    epos = @_editorCoordinates()
 
    newValue = @config.getValueForPos(epos)
 
    @config.observable(newValue)
 

	
 
  subscribe: (onChange) ->
 
    log('AdjustableFloatObservable subscribe', @config)
 
    ko.computed =>
 
      @config.observable()
 
      onChange()
 

	
 
class window.AdjustableFloatObject extends Adjustable
 
  constructor: (@config) ->
 
    # config also has:
 
    #   graph
 
    #   subj
 
    #   pred
 
    #   ctx
 
    #   getTargetPosForValue(value) -> getTarget result for value
 
    #   getValueForPos
 
    super()
 
    @ctor2()
 
    if not @config.ctx?
 
      throw new Error("missing ctx")
 
    # this seems to not fire enough.
 
    @config.graph.runHandler(@_syncValue.bind(@),
 
                             "adj sync #{@config.subj.value}")
 
                             "adj sync #{@config.subj.value} #{@config.pred.value}")
 

	
 
  _syncValue: () ->
 
    @_currentValue = @config.graph.floatValue(@config.subj, @config.pred)
 
    @_onChange() if @_onChange
 

	
 
  _getValue: () ->
 
    # this is a big speedup- callers use _getValue about 4x as much as
 
    # the graph changes and graph.floatValue is slow
 
    @_currentValue
 

	
 
  getTarget: () ->
 
    @config.getTargetPosForValue(@_getValue())
 

	
 
  subscribe: (onChange) ->
 
    # only works on one subscription at a time
 
    throw new Error('multi subscribe not implemented') if @_onChange
 
    @_onChange = onChange
 

	
 
  continueDrag: (pos) ->
 
    # pos is vec2 of pixels relative to the drag start
 
    super(pos)
 
    newValue = @config.getValueForPos(@_editorCoordinates())
 

	
 
    @config.graph.patchObject(@config.subj, @config.pred,
 
                              @config.graph.LiteralRoundedFloat(newValue),
 
                              @config.ctx)
 
    #@_syncValue()
 

	
 
class window.AdjustableFade extends Adjustable
 
  constructor: (@yForV, @zoomInX, @i0, @i1, @note, offset, ctx) ->
 
    super()
 
    @config = {
 
      getSuggestedTargetOffset: -> offset
 
      getTarget: @getTarget.bind(@)
 
      ctx: ctx
 
    }
 
    @ctor2()
 

	
 
  getTarget: ->
 
    mid = @note.midPoint(@i0, @i1)
 
    $V([@zoomInX(mid.e(1)), @yForV(mid.e(2))])
 

	
 
  _getValue: ->
 
    @note.midPoint(@i0, @i1).e(1)
 

	
 
  continueDrag: (pos) ->
 
    # pos is vec2 of pixels relative to the drag start
 
    super(pos)
 
    graph = @note.graph
 
    U = (x) -> graph.Uri(x)
 

	
 
    goalCenterSec = @zoomInX.invert(@initialTarget.e(1) + pos.e(1))
 

	
 
    diamSec = @note.worldPts[@i1].e(1) - @note.worldPts[@i0].e(1)
 
    newSec0 = goalCenterSec - diamSec / 2
 
    newSec1 = goalCenterSec + diamSec / 2
 

	
 
    originSec = graph.floatValue(@note.uri, U(':originTime'))
 

	
 
    p0 = @_makePatch(graph, @i0, newSec0, originSec, @config.ctx)
 
    p1 = @_makePatch(graph, @i1, newSec1, originSec, @config.ctx)
 

	
 
    graph.applyAndSendPatch(@_addPatches(p0, p1))
 

	
 
  _makePatch: (graph, idx, newSec, originSec, ctx) ->
 
    graph.getObjectPatch(@note.worldPts[idx].uri,
 
                         graph.Uri(':time'),
 
                         graph.LiteralRoundedFloat(newSec - originSec), ctx)
 

	
 
  _addPatches: (p0, p1) ->
 
    {
 
      addQuads: p0.addQuads.concat(p1.addQuads),
 
      delQuads: p0.delQuads.concat(p1.delQuads)
 
    }
 
\ No newline at end of file
light9/web/timeline/adjusters.coffee
Show inline comments
 
log = console.log
 
Drawing = window.Drawing
 

	
 
maxDist = 60
 

	
 
coffeeElementSetup(class AdjustersCanvas extends Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element)
 
  @is: 'light9-adjusters-canvas'
 
  @getter_properties:
 
    setAdjuster: {type: Function, notify: true }
 
  @getter_observers: [
 
    'updateAllCoords(adjs)'
 
  ]
 
  constructor: ->
 
    super()
 
    @redraw = _.throttle(@_throttledRedraw.bind(@), 30, {leading: false})
 
    @adjs = {}
 
    @hoveringNear = null
 
    
 
  ready: ->
 
    super.ready()
 
    @addEventListener('iron-resize', @resizeUpdate.bind(@))
 
    @ctx = @$.canvas.getContext('2d')
 
    
 
    @redraw()
 
    @setAdjuster = @_setAdjuster.bind(@)
 

	
 
    # These don't fire; TimelineEditor calls the handlers for us.
 
    @addEventListener('mousedown', @onDown.bind(@))
 
    @addEventListener('mousemove', @onMove.bind(@))
 
    @addEventListener('mouseup', @onUp.bind(@))
 

	
 
  _mousePos: (ev) ->
 
    $V([ev.clientX, ev.clientY - @offsetParent.offsetTop])
 
  
 
  onDown: (ev) ->
 
    if ev.buttons == 1
 
      start = @_mousePos(ev)
 
      adj = @_adjAtPoint(start)
 
      if adj
 
        ev.stopPropagation()
 
        @currentDrag = {start: start, adj: adj}
 
        adj.startDrag()
 

	
 
  onMove: (ev) ->
 
    pos = @_mousePos(ev)
 
    if @currentDrag
 
      @hoveringNear = null
 
      @currentDrag.cur = pos
 
      @currentDrag.adj.continueDrag(
 
        @currentDrag.cur.subtract(@currentDrag.start))
 
      @redraw()
 
    else
 
      near = @_adjAtPoint(pos)
 
      if @hoveringNear != near
 
        @hoveringNear = near
 
        @redraw()
 

	
 
  onUp: (ev) ->
 
    return unless @currentDrag
 
    @currentDrag.adj.endDrag()
 
    @currentDrag = null
 
    
 
  _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?
 
      if @adjs[adjId]
 
        delete @adjs[adjId]
 
      else
 
      # this might be able to reuse an existing one a bit
 
        adj = makeAdjustable()
 
        @adjs[adjId] = adj
 
        adj.id = adjId
 

	
 
    @redraw()
 

	
 
    window.debug_adjsCount = Object.keys(@adjs).length
 

	
 
  updateAllCoords: ->
 
    @redraw()
 

	
 
  _adjAtPoint: (pt) ->
 
    nearest = @qt.find(pt.e(1), pt.e(2))
 
    if not nearest? or nearest.distanceFrom(pt) > maxDist
 
      return null
 
    return nearest?.adj
 

	
 
  resizeUpdate: (ev) ->
 
    @$.canvas.width = ev.target.offsetWidth
 
    @$.canvas.height = ev.target.offsetHeight
 
    @canvasCenter = $V([@$.canvas.width / 2, @$.canvas.height / 2])
 
    @redraw()
 

	
 
  _throttledRedraw: () ->
 
    return unless @ctx?
 
    console.time('adjs redraw')
 
    @_layoutCenters()
 
    
 
    @ctx.clearRect(0, 0, @$.canvas.width, @$.canvas.height)
 

	
 
    for adjId, adj of @adjs
 
      ctr = adj.getHandle()
 
      target = adj.getTarget()
 
      if @_isOffScreen(target)
 
        continue
 
      @_drawConnector(ctr, target)
 
      
 
      @_drawAdjuster(adj.getDisplayValue(),
 
                     ctr.e(1) - 20, ctr.e(2) - 10,
 
                     ctr.e(1) + 20, ctr.e(2) + 10,
 
                     adj == @hoveringNear)
 
    console.timeEnd('adjs redraw')
 

	
 
  _layoutCenters: ->
 
    # push Adjustable centers around to avoid overlaps
 
    # Todo: also don't overlap inlineattr boxes
 
    # Todo: don't let their connector lines cross each other
 
    @qt = d3.quadtree([], ((d)->d.e(1)), ((d)->d.e(2)))
 
    @qt.extent([[0,0], [8000,8000]])
 

	
 
    for _, adj of @adjs
 
      adj.handle = @_clampOnScreen(adj.getSuggestedHandle())
 

	
 
    numTries = 8
 
    for tries in [0...numTries]
 
      for _, adj of @adjs
 
        current = adj.handle
 
        @qt.remove(current)
 
        nearest = @qt.find(current.e(1), current.e(2), maxDist)
 
        if nearest
 
          dist = current.distanceFrom(nearest)
 
          if dist < maxDist
 
            current = @_stepAway(current, nearest, 1 / numTries)
 
            adj.handle = current
 
        current.adj = adj
 
        @qt.add(current)
 

	
 
      #if -50 < output.e(1) < 20 # mostly for zoom-left
 
      #  output.setElements([
 
      #    Math.max(20, output.e(1)),
 
      #    output.e(2)])
 

	
 
  _stepAway: (current, nearest, dx) ->
 
    away = current.subtract(nearest).toUnitVector()
 
    toScreenCenter = @canvasCenter.subtract(current).toUnitVector()
 
    goalSpacingPx = 20
 
    @_clampOnScreen(current.add(away.x(goalSpacingPx * dx)))
 

	
 
  _isOffScreen: (pos) ->
 
    pos.e(1) < 0 or pos.e(1) > @$.canvas.width or pos.e(2) < 0 or pos.e(2) > @$.canvas.height
 

	
 
  _clampOnScreen: (pos) ->    
 
    marg = 30
 
    $V([Math.max(marg, Math.min(@$.canvas.width - marg, pos.e(1))),
 
        Math.max(marg, Math.min(@$.canvas.height - marg, pos.e(2)))])
 
                        
 
  _drawConnector: (ctr, target) ->
 
    @ctx.strokeStyle = '#aaa'
 
    @ctx.lineWidth = 2
 
    @ctx.beginPath()
 
    Drawing.line(@ctx, ctr, target)
 
    @ctx.stroke()
 
    
 
  _drawAdjuster: (label, x1, y1, x2, y2, hover) ->
 
    radius = 8
 

	
light9/web/timeline/timeline.coffee
Show inline comments
 
@@ -445,192 +445,193 @@ coffeeElementSetup(class TimeZoomed exte
 

	
 

	
 
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, @brickLayout) ->
 
    @adjusterIds = new Set() # id string
 
    @updateSoon = _.debounce(@update.bind(@), 30)
 

	
 
  initWatchers: ->
 
    @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()
 

	
 
  getCurvePoints: (subj, curveAttr) ->
 
    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))
 

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

	
 
    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
 
      hover: @uri.equals(@selection.hover())
 
      selected: @selection.selected().filter((s) => s.equals(@uri)).length
 
    }
 

	
 
  onRowChange: ->
 
    @clearAdjusters()
 
    @updateSoon()
 

	
 
  redraw: (params) ->
 
    # no observable or graph deps in here
 
    @container.removeChildren()
 
    @graphics = new PIXI.Graphics({nativeLines: false})
 
    @graphics.interactive = true
 
    @container.addChild(@graphics)
 

	
 
    if params.hover
 
      @_traceBorder(params.screenPts, 12, 0x888888)
 
    if params.selected
 
      @_traceBorder(params.screenPts, 6, 0xff2900)
 

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

	
 
    @_traceBorder(params.screenPts, 2, 0xffd900)
 

	
 
    @_addMouseBindings()
 
                 
 
                 
 
  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)
 

	
 
    curveWidthCalc = () => @project.curveWidth(@worldPts)
 
    @_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)
 

	
 
    @graphics.on 'mouseover', =>
 
      if @selection.hover() and @selection.hover().equals(@uri)
 
        # Hovering causes a redraw, which would cause another
 
        # mouseover event.
 
        return
 
      @selection.hover(@uri)
 

	
 
    # mouseout never fires since we rebuild the graphics on mouseover.
 
    @graphics.on 'mousemove', (ev) =>
 
      if @selection.hover() and @selection.hover().equals(@uri) and ev.target != @graphics
 
        @selection.hover(null)
 

	
 
  onUri: ->
 
    @graph.runHandler(@update.bind(@), "note updates #{@uri}")
 

	
 
  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
 

	
 
  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, 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: yForV(1) + 5,
 
      width: w,
 
      height: yForV(0) - yForV(1) - 15,
0 comments (0 inline, 0 general)