Files @ 40d5a54dec99
Branch filter:

Location: light9/web/timeline/Note.coffee - annotation

drewp@bigasterisk.com
ts warnings
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
log = debug('timeline')
debug.enable('*')

Drawing = window.Drawing
ROW_COUNT = 7

# 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'))
      # todo: maybe shoudl be :effectAttr?
      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,
      }

    @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
    @adjusterIds.add(adjId)
    @setAdjuster adjId, =>
      adj = new AdjustableFloatObject({
        graph: @graph
        subj: worldPts[pointNum].uri
        pred: U(':time')
        ctx: ctx
        getTargetPosForValue: (value) =>
          $V([@viewState.zoomInX(value), yForV(worldPts[pointNum].e(2))])
        getValueForPos: (pos) =>
          origin = @graph.floatValue(@uri, U(':originTime'))
          (@viewState.zoomInX.invert(pos.e(1)) - origin)
        getSuggestedTargetOffset: () => @_suggestedOffset(worldPts[pointNum]),
      })
      adj._getValue = (=>
        # note: don't use originTime from the closure- we need the
        # graph dependency
        adj._currentValue + @graph.floatValue(@uri, U(':originTime'))
        )
      adj

  _makeOffsetAdjuster: (yForV, curveWidthCalc, ctx) ->
    U = (x) => @graph.Uri(x)

    adjId = @uri.value + '/offset'
    @adjusterIds.add(adjId)
    @setAdjuster adjId, =>
      adj = new AdjustableFloatObject({
        graph: @graph
        subj: @uri
        pred: U(':originTime')
        ctx: ctx
        getDisplayValue: (v, dv) => "o=#{dv}"
        getTargetPosForValue: (value) =>
          # display bug: should be working from pt[0].t, not from origin
          $V([@viewState.zoomInX(value + curveWidthCalc() / 2), yForV(.5)])
        getValueForPos: (pos) =>
          @viewState.zoomInX.invert(pos.e(1)) - curveWidthCalc() / 2
        getSuggestedTargetOffset: () => $V([-10, 0])
      })
      adj

  _makeFadeAdjusters: (yForV, zoomInX, ctx, worldPts) ->
    U = (x) => @graph.Uri(x)
    @_makeFadeAdjuster(yForV, zoomInX, ctx, @uri.value + '/fadeIn', 0, 1, $V([-50, -10]))
    n = worldPts.length
    @_makeFadeAdjuster(yForV, zoomInX, ctx, @uri.value + '/fadeOut', n - 2, n - 1, $V([50, -10]))

  _makeFadeAdjuster: (yForV, zoomInX, ctx, adjId, i0, i1, offset) ->
    @adjusterIds.add(adjId)
    @setAdjuster adjId, =>
      new AdjustableFade(yForV, zoomInX, i0, i1, @, offset, ctx)

  _suggestedOffset: (pt) ->
    if pt.e(2) > .5
      $V([0, 30])
    else
      $V([0, -30])

  _onMouseDown: (ev) ->
    sel = @selection.selected()
    if ev.data.originalEvent.ctrlKey
      if @uri in sel
        sel = _.without(sel, @uri)
      else
        sel.push(@uri)
    else
      sel = [@uri]
    @selection.selected(sel)

  _noteColor: (effect) ->
    effect = effect.value
    if effect in ['http://light9.bigasterisk.com/effect/blacklight',
                  'http://light9.bigasterisk.com/effect/strobewarm']
      hue = 0
      sat = 100
    else
      hash = 0
      for i in [(effect.length-10)...effect.length]
        hash += effect.charCodeAt(i)
      hue = (hash * 8) % 360
      sat = 40 + (hash % 20) # don't conceal colorscale too much

    return parseInt(tinycolor.fromRatio({h: hue / 360, s: sat / 100, l: .58}).toHex(), 16)

    #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