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