Files @ 40d5a54dec99
Branch filter:

Location: light9/web/timeline/Note.coffee

drewp@bigasterisk.com
ts warnings
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