Changeset - d991f7c3485a
[Not reviewed]
.vscode/settings.json
Show inline comments
 
{
 
    "python.autoComplete.extraPaths": ["__pypackages__/3.9/lib"],
 
    "python.analysis.extraPaths": [
 
        "__pypackages__/3.9/lib"
 
    ],
 
    "python.analysis.diagnosticMode": "workspace",
 
    "cmake.configureOnOpen": false
 
  "python.autoComplete.extraPaths": ["__pypackages__/3.9/lib"],
 
  "python.analysis.extraPaths": ["__pypackages__/3.9/lib"],
 
  "python.analysis.diagnosticMode": "workspace",
 
  "cmake.configureOnOpen": false,
 
  "toTypeScript.fixUnreachableCode": false,
 
  "toTypeScript.fixUnusedLabel": false,
 
  "toTypeScript.forgottenThisPropertyAccess": false
 
}
 
  
light9/web/coffee_element.coffee
Show inline comments
 
deleted file
light9/web/timeline/Note.coffee
Show inline comments
 
new file 100644
 
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'))
 
      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
light9/web/timeline/Project.coffee
Show inline comments
 
new file 100644
 
log = debug('timeline')
 
debug.enable('*')
 

	
 
Drawing = window.Drawing
 
ROW_COUNT = 7
 

	
 
class Project
 
  constructor: (@graph) ->
 

	
 
  makeEffect: (uri) ->
 
    U = (x) => @graph.Uri(x)
 
    effect = U(uri.value + '/effect')
 
    quad = (s, p, o) => @graph.Quad(s, p, o, effect)
 

	
 
    quads = [
 
      quad(effect, U('rdf:type'), U(':Effect')),
 
      quad(effect, U(':copiedFrom'), uri),
 
      quad(effect, U('rdfs:label'), @graph.Literal(uri.replace(/.*capture\//, ''))),
 
      quad(effect, U(':publishAttr'), U(':strength')),
 
      ]
 

	
 
    fromSettings = @graph.objects(uri, U(':setting'))
 

	
 
    toSettings = @graph.nextNumberedResources(effect + '_set', fromSettings.length)
 

	
 
    for fs in fromSettings
 
      ts = toSettings.pop()
 
      # full copies of these since I may have to delete captures
 
      quads.push(quad(effect, U(':setting'), ts))
 
      quads.push(quad(ts, U(':device'), @graph.uriValue(fs, U(':device'))))
 
      quads.push(quad(ts, U(':deviceAttr'), @graph.uriValue(fs, U(':deviceAttr'))))
 
      try
 
        quads.push(quad(ts, U(':value'), @graph.uriValue(fs, U(':value'))))
 
      catch
 
        quads.push(quad(ts, U(':scaledValue'), @graph.uriValue(fs, U(':scaledValue'))))
 

	
 
    @graph.applyAndSendPatch({delQuads: [], addQuads: quads})
 
    return effect
 

	
 
  makeNewNote: (song, effect, dropTime, desiredWidthT) ->
 
    U = (x) => @graph.Uri(x)
 
    quad = (s, p, o) => @graph.Quad(s, p, o, song)
 

	
 
    newNote = @graph.nextNumberedResource("#{song.value}/n")
 
    newCurve = @graph.nextNumberedResource("#{newNote.value}c")
 
    points = @graph.nextNumberedResources("#{newCurve.value}p", 4)
 

	
 
    curveQuads = [
 
        quad(song, U(':note'), newNote)
 
        quad(newNote, U('rdf:type'), U(':Note'))
 
        quad(newNote, U(':originTime'), @graph.LiteralRoundedFloat(dropTime))
 
        quad(newNote, U(':effectClass'), effect)
 
        quad(newNote, U(':curve'), newCurve)
 
        quad(newCurve, U('rdf:type'), U(':Curve'))
 
        quad(newCurve, U(':attr'), U(':strength'))
 
      ]
 

	
 
    pointQuads = []
 
    for i in [0...4]
 
      pt = points[i]
 
      pointQuads.push(quad(newCurve, U(':point'), pt))
 
      pointQuads.push(quad(pt, U(':time'), @graph.LiteralRoundedFloat(i/3 * desiredWidthT)))
 
      pointQuads.push(quad(pt, U(':value'), @graph.LiteralRoundedFloat(i == 1 or i == 2)))
 

	
 
    patch = {
 
      delQuads: []
 
      addQuads: curveQuads.concat(pointQuads)
 
    }
 
    @graph.applyAndSendPatch(patch)
 

	
 
  getCurvePoints: (curve, xOffset) ->
 
    worldPts = []
 
    uris = @graph.objects(curve, @graph.Uri(':point'))
 
    for pt in uris
 
      tm = @graph.floatValue(pt, @graph.Uri(':time'))
 
      val = @graph.floatValue(pt, @graph.Uri(':value'))
 
      v = $V([xOffset + tm, val])
 
      v.uri = pt
 
      worldPts.push(v)
 
    worldPts.sort((a,b) -> a.e(1) - b.e(1))
 
    return [uris, worldPts]
 

	
 
  curveWidth: (worldPts) ->
 
    tMin = @graph.floatValue(worldPts[0].uri, @graph.Uri(':time'))
 
    tMax = @graph.floatValue(worldPts[3].uri, @graph.Uri(':time'))
 
    tMax - tMin
 

	
 
  deleteNote: (song, note, selection) ->
 
    patch = {delQuads: [@graph.Quad(song, graph.Uri(':note'), note, song)], addQuads: []}
 
    @graph.applyAndSendPatch(patch)
 
    if note in selection.selected()
 
      selection.selected(_.without(selection.selected(), note))
light9/web/timeline/TimeAxis.coffee
Show inline comments
 
new file 100644
 
log = debug('timeline')
 
debug.enable('*')
 

	
 
Drawing = window.Drawing
 
ROW_COUNT = 7
 

	
 

	
 

	
 

	
 
@customElement("light9-timeline-time-axis")
 
class TimeAxis extends LitElement
 
  @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)
 
\ No newline at end of file
light9/web/timeline/TimeZoomed.coffee
Show inline comments
 
new file 100644
 
log = debug('timeline')
 
debug.enable('*')
 

	
 
Drawing = window.Drawing
 
ROW_COUNT = 7
 

	
 

	
 
# 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.
 
@customElement('light9-timeline-time-zoomed')
 
class TimeZoomed extends LitElement
 
  @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
 
    imageSamples: { type: Array, value: [] }
 
  @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,
 
    })
 
    @bg = new PIXI.Container()
 
    @stage.addChild(@bg)
 

	
 
    @dirty = _.debounce(@_repaint.bind(@), 10)
 

	
 
  ready: ->
 
    super.ready()
 

	
 
    @imageSamples = ['one']
 

	
 
    @addEventListener('iron-resize', @_onResize.bind(@))
 
    Polymer.RenderStatus.afterNextRender(this, @_onResize.bind(@))
 

	
 
    @$.rows.appendChild(@renderer.view)
 

	
 
    # This works for display, but pixi hit events didn't correctly
 
    # move with the objects, so as a workaround, I extended the top of
 
    # the canvas in _onResize.
 
    #
 
    #ko.computed =>
 
    #  @stage.setTransform(0, -(@viewState.rowsY()), 1, 1, 0, 0, 0, 0, 0)
 

	
 
  _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 = ->
 
      @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())
 
    
 
    for uri in @graph.sortedUris(songNotes)
 
      had = toRemove.delete(uri.value)
 
      if not had
 
        @_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 = @brickLayout.rowBottom(row)
 
      gfx.moveTo(0, y)
 
      gfx.lineTo(@clientWidth, y)
 

	
 
  _addNote: (uri) ->
 
    U = (x) => @graph.Uri(x)
 
    
 
    con = new PIXI.Container()
 
    con.interactive=true
 
    @stage.addChild(con)
 
    
 
    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.onRowChange.bind(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
 

	
 
    if not @graph.contains(effect, U('rdf:type'), U(':Effect'))
 
      if @graph.contains(effect, U('rdf:type'), U(':LightSample'))
 
        effect = @project.makeEffect(effect)
 
      else
 
        log("drop #{effect} is not an effect")
 
        return
 

	
 
    dropTime = @viewState.zoomInX.invert(pos.e(1))
 

	
 
    desiredWidthX = @offsetWidth * .3
 
    desiredWidthT = @viewState.zoomInX.invert(desiredWidthX) - @viewState.zoomInX.invert(0)
 
    desiredWidthT = Math.min(desiredWidthT, @viewState.zoomSpec.duration() - dropTime)
 
    @project.makeNewNote(U(@song), U(effect), dropTime, desiredWidthT)
 

	
 
  updateInlineAttrs: (note, config) ->
 
    if not config?
 
      index = 0
 
      for c in @inlineAttrConfigs
 
        if c.uri.equals(note)
 
          @splice('inlineAttrConfigs', index)
 
          return
 
        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)
 

	
light9/web/timeline/TimelineEditor.coffee
Show inline comments
 
new file 100644
 
log = debug('timeline')
 
debug.enable('*')
 

	
 
Drawing = window.Drawing
 
ROW_COUNT = 7
 

	
 

	
 
@customElement('light9-timeline-editor')
 
class TimelineEditor extends LitElement
 
  @getter_properties:
 
    viewState: { type: Object }
 
    debug: {type: String}
 
    graph: {type: Object, notify: true}
 
    project: {type: Object}
 
    setAdjuster: {type: Function, notify: true}
 
    playerSong: {type: String, notify: true}
 
    followPlayerSong: {type: Boolean, notify: true, value: true}
 
    song: {type: String, notify: true}
 
    show: {type: String, notify: true}
 
    songTime: {type: Number, notify: true}
 
    songDuration: {type: Number, notify: true}
 
    songPlaying: {type: Boolean, notify: true}
 
    selection: {type: Object, notify: true}
 
  @getter_observers: [
 
    '_onSong(playerSong, followPlayerSong)',
 
    '_onGraph(graph)',
 
    '_onSongDuration(songDuration, viewState)',
 
    '_onSongTime(song, playerSong, songTime, viewState)',
 
    '_onSetAdjuster(setAdjuster)',
 
  ]
 
  constructor: ->
 
    super()
 
    @viewState = new ViewState()
 
    window.viewState = @viewState
 

	
 
  ready: ->
 
    super.ready()
 
    @addEventListener 'mousedown', (ev) => @$.adjustersCanvas.onDown(ev)
 
    @addEventListener 'mousemove', (ev) => @$.adjustersCanvas.onMove(ev)
 
    @addEventListener 'mouseup', (ev) => @$.adjustersCanvas.onUp(ev)
 

	
 
    ko.options.deferUpdates = true
 

	
 
    @selection = {hover: ko.observable(null), selected: ko.observable([])}
 

	
 
    window.debug_zoomOrLayoutChangedCount = 0
 
    window.debug_adjUpdateDisplay = 0
 

	
 
    ko.computed(@zoomOrLayoutChanged.bind(@))
 

	
 
    @trackMouse()
 
    @bindKeys()
 
    @bindWheelZoom(@)
 

	
 
    setInterval(@updateDebugSummary.bind(@), 100)
 

	
 
    @addEventListener('iron-resize', @_onIronResize.bind(@))
 
    Polymer.RenderStatus.afterNextRender(this, @_onIronResize.bind(@))
 

	
 
    Polymer.RenderStatus.afterNextRender this, =>
 
      setupDrop(@$.zoomed.$.rows, @$.zoomed.$.rows, @, @$.zoomed.onDrop.bind(@$.zoomed))
 

	
 
  _onIronResize: ->
 
    @viewState.setWidth(@offsetWidth)
 
    @viewState.coveredByDiagramTop(@$.coveredByDiagram.offsetTop)
 
    @viewState.rowsY(@$.zoomed.$.rows.offsetTop) if @$.zoomed?.$?.rows?
 
    @viewState.audioY(@$.audio.offsetTop)
 
    @viewState.audioH(@$.audio.offsetHeight)
 
    if @$.zoomed?.$?.time?
 
      @viewState.zoomedTimeY(@$.zoomed.$.time.offsetTop)
 
      @viewState.zoomedTimeH(@$.zoomed.$.time.offsetHeight)
 

	
 
  _onSongTime: (song, playerSong, t) ->
 
    if song != playerSong
 
      @viewState.cursor.t(0)
 
      return
 
    @viewState.cursor.t(t)
 

	
 
  _onSongDuration: (d) ->
 
    d = 700 if d < 1 # bug is that asco isn't giving duration, but 0 makes the scale corrupt
 
    @viewState.zoomSpec.duration(d)
 

	
 
  _onSong: (s) ->
 
    @song = @playerSong if @followPlayerSong
 

	
 
  _onGraph: (graph) ->
 
    @project = new Project(graph)
 
    @show = 'http://light9.bigasterisk.com/show/dance2019'
 

	
 
  _onSetAdjuster: () ->
 
    @makeZoomAdjs()
 

	
 
  updateDebugSummary: ->
 
    elemCount = (tag) -> document.getElementsByTagName(tag).length
 
    @debug = "#{window.debug_zoomOrLayoutChangedCount} layout change,
 
     #{elemCount('light9-timeline-note')} notes,
 
     #{@selection.selected().length} selected
 
     #{elemCount('light9-timeline-graph-row')} rows,
 
     #{window.debug_adjsCount} adjuster items registered,
 
     #{window.debug_adjUpdateDisplay} adjuster updateDisplay calls,
 
    "
 

	
 
  zoomOrLayoutChanged: ->
 
    vs = @viewState
 
    dependOn = [vs.zoomSpec.t1(), vs.zoomSpec.t2(), vs.width()]
 

	
 
    # shouldn't need this- deps should get it
 
    @$.zoomed.gatherNotes() if @$.zoomed?.gatherNotes?
 

	
 
    # todo: these run a lot of work purely for a time change
 
    if @$.zoomed?.$?.audio?
 
      #@dia.setTimeAxis(vs.width(), @$.zoomed.$.audio.offsetTop, vs.zoomInX)
 
      @$.adjustersCanvas.updateAllCoords()
 

	
 
  trackMouse: ->
 
    # not just for show- we use the mouse pos sometimes
 
    for evName in ['mousemove', 'touchmove']
 
      @addEventListener evName, (ev) =>
 
        ev.preventDefault()
 

	
 
        # todo: consolidate with _editorCoordinates version
 
        if ev.touches?.length
 
          ev = ev.touches[0]
 

	
 
        root = @$.cursorCanvas.getBoundingClientRect()
 
        @viewState.mouse.pos($V([ev.pageX - root.left, ev.pageY - root.top]))
 

	
 
        # should be controlled by a checkbox next to follow-player-song-choice
 
        @sendMouseToVidref() unless window.location.hash.match(/novidref/)
 

	
 
  sendMouseToVidref: ->
 
    now = Date.now()
 
    if (!@$.vidrefLastSent? || @$.vidrefLastSent < now - 200) && !@songPlaying
 
      @$.vidrefTime.body = {t: @viewState.latestMouseTime(), source: 'timeline', song: @song}
 
      @$.vidrefTime.generateRequest()
 
      @$.vidrefLastSent = now
 

	
 
  bindWheelZoom: (elem) ->
 
    elem.addEventListener 'mousewheel', (ev) =>
 
      @viewState.onMouseWheel(ev.deltaY)
 

	
 
  bindKeys: ->
 
    shortcut.add "Ctrl+P", (ev) =>
 
      @$.music.seekPlayOrPause(@viewState.latestMouseTime())
 
    shortcut.add "Ctrl+Escape", => @viewState.frameAll()
 
    shortcut.add "Shift+Escape", => @viewState.frameToEnd()
 
    shortcut.add "Escape", => @viewState.frameCursor()
 
    shortcut.add "L", =>
 
      @$.adjustersCanvas.updateAllCoords()
 
    shortcut.add 'Delete', =>
 
      for note in @selection.selected()
 
        @project.deleteNote(@graph.Uri(@song), note, @selection)
 

	
 
  makeZoomAdjs: ->
 
    yMid = => @$.audio.offsetTop + @$.audio.offsetHeight / 2
 

	
 
    valForPos = (pos) =>
 
      x = pos.e(1)
 
      t = @viewState.fullZoomX.invert(x)
 
    @setAdjuster('zoom-left', => new AdjustableFloatObservable({
 
      observable: @viewState.zoomSpec.t1,
 
      getTarget: () =>
 
        $V([@viewState.fullZoomX(@viewState.zoomSpec.t1()), yMid()])
 
      getSuggestedTargetOffset: () => $V([-50, 10])
 
      getValueForPos: valForPos
 
    }))
 

	
 
    @setAdjuster('zoom-right', => new AdjustableFloatObservable({
 
      observable: @viewState.zoomSpec.t2,
 
      getTarget: () =>
 
        $V([@viewState.fullZoomX(@viewState.zoomSpec.t2()), yMid()])
 
      getSuggestedTargetOffset: () => $V([50, 10])
 
      getValueForPos: valForPos
 
    }))
 

	
 
    panObs = ko.pureComputed({
 
      read: () =>
 
        (@viewState.zoomSpec.t1() + @viewState.zoomSpec.t2()) / 2
 
      write: (value) =>
 
        zs = @viewState.zoomSpec
 
        span = zs.t2() - zs.t1()
 
        zs.t1(value - span / 2)
 
        zs.t2(value + span / 2)
 
    })
 

	
 
    @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
 
    }))
light9/web/timeline/adjustable.ts
Show inline comments
 
file renamed from light9/web/timeline/adjustable.coffee to light9/web/timeline/adjustable.ts
 
log = debug('adjustable')
 
import * as d3 from "d3";
 
import { debug } from "debug";
 
import * as ko from "knockout";
 
const log = debug("adjustable");
 

	
 
interface Config {
 
  //   getTarget -> vec2 of current target position
 
  getTarget: () => Vector;
 
  //   getSuggestedTargetOffset -> vec2 pixel offset from target
 
  getSuggestedTargetOffset: () => Vector;
 
  //   emptyBox -> true if you want no value display
 
  emptyBox: boolean;
 
}
 

	
 
class Adjustable
 
  # Some value you can edit in the UI, probably by dragging
 
  # stuff. Drawn by light9-adjusters-canvas. This object does the
 
  # layout and positioning.
 
  #
 
  # The way dragging should work is that you start in the yellow *adj
 
  # widget*, wherever it is, but your drag is moving the *target*. The
 
  # adj will travel around too, but it may do extra moves to not bump
 
  # into stuff or to get out from under your finger.
 

	
 
  constructor: (@config) ->
 
    @ctor2()
 

	
 
  ctor2: () ->
 
    # config has:
 
    #   getTarget -> vec2 of current target position
 
    #   getSuggestedTargetOffset -> vec2 pixel offset from target
 
    #   emptyBox -> true if you want no value display
 
export class Adjustable {
 
  config: any;
 
  handle: any;
 
  initialTarget: any;
 
  targetDraggedTo: any;
 
  root: any;
 
  // Some value you can edit in the UI, probably by dragging
 
  // stuff. Drawn by light9-adjusters-canvas. This object does the
 
  // layout and positioning.
 
  //
 
  // The way dragging should work is that you start in the yellow *adj
 
  // widget*, wherever it is, but your drag is moving the *target*. The
 
  // adj will travel around too, but it may do extra moves to not bump
 
  // into stuff or to get out from under your finger.
 

	
 
    # updated later by layout algoritm
 
    @handle = $V([0, 0])
 
  constructor(config: any) {
 
    this.config = config;
 
    this.ctor2();
 
  }
 

	
 
  getDisplayValue: () ->
 
    return '' if @config.emptyBox
 
    defaultFormat = d3.format(".4g")(@_getValue())
 
    if @config.getDisplayValue?
 
      return @config.getDisplayValue(@_getValue(), defaultFormat)
 
    defaultFormat
 
  ctor2() {
 
    // updated later by layout algoritm
 
    return (this.handle = $V([0, 0]));
 
  }
 

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

	
 
  getHandle: () -> # vec2 of pixels
 
    @handle
 
  getDisplayValue() {
 
    if (this.config.emptyBox) {
 
      return "";
 
    }
 
    const defaultFormat = d3.format(".4g")(this._getValue());
 
    if (this.config.getDisplayValue != null) {
 
      return this.config.getDisplayValue(this._getValue(), defaultFormat);
 
    }
 
    return defaultFormat;
 
  }
 
  _getValue(): any {
 
    throw new Error("Method not implemented.");
 
  }
 

	
 
  getTarget: () -> # vec2 of pixels
 
    @config.getTarget()
 
  getSuggestedHandle() {
 
    return this.getTarget().add(this.config.getSuggestedTargetOffset());
 
  }
 

	
 
  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')
 
  getHandle() {
 
    // vec2 of pixels
 
    return this.handle;
 
  }
 

	
 
  getTarget() {
 
    // vec2 of pixels
 
    return this.config.getTarget();
 
  }
 

	
 
  startDrag: () ->
 
    @initialTarget = @getTarget()
 
  subscribe(onChange: any) {
 
    // 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");
 
  }
 

	
 
  continueDrag: (pos) ->
 
    ## pos is vec2 of pixels relative to the drag start
 
    @targetDraggedTo = pos.add(@initialTarget)
 
  startDrag() {
 
    return (this.initialTarget = this.getTarget());
 
  }
 

	
 
  endDrag: () ->
 
    # override
 
  continueDrag(pos: { add: (arg0: any) => any }) {
 
    //# pos is vec2 of pixels relative to the drag start
 
    return (this.targetDraggedTo = pos.add(this.initialTarget));
 
  }
 

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

	
 
  _editorCoordinates() {
 
    // vec2 of mouse relative to <l9-t-editor>
 
    let rootElem: { getBoundingClientRect: () => any };
 
    return this.targetDraggedTo;
 
    // let ev = d3.event.sourceEvent;
 

	
 
    if ev.target.tagName == "LIGHT9-TIMELINE-EDITOR"
 
      rootElem = ev.target
 
    else
 
      rootElem = ev.target.closest('light9-timeline-editor')
 
    // 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]
 
    // if (ev.touches != null ? ev.touches.length : undefined) {
 
    //   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])
 
    // // storing root on the object to remember it across calls in case
 
    // // you drag outside the editor.
 
    // if (rootElem) {
 
    //   this.root = rootElem.getBoundingClientRect();
 
    // }
 
    // const offsetParentPos = $V([ev.pageX - this.root.left, ev.pageY - this.root.top]);
 

	
 
    return offsetParentPos
 
    // 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()
 
class AdjustableFloatObservable extends Adjustable {
 
  constructor(config: any) {
 
    // 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?
 
    this.config = config;
 
    super();
 
    this.ctor2();
 
  }
 

	
 
  _getValue: () ->
 
    @config.observable()
 
  _getValue() {
 
    return this.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)
 
  continueDrag(pos: any) {
 
    // pos is vec2 of pixels relative to the drag start.
 
    super.continueDrag(pos);
 
    const epos = this._editorCoordinates();
 
    const newValue = this.config.getValueForPos(epos);
 
    return this.config.observable(newValue);
 
  }
 

	
 
  subscribe: (onChange) ->
 
    log('AdjustableFloatObservable subscribe', @config)
 
    ko.computed =>
 
      @config.observable()
 
      onChange()
 
  subscribe(onChange: () => any) {
 
    log("AdjustableFloatObservable subscribe", this.config);
 
    return ko.computed(() => {
 
      this.config.observable();
 
      return onChange();
 
    });
 
  }
 
}
 

	
 
class AdjustableFloatObject extends Adjustable {
 
  _currentValue: any;
 
  _onChange: any;
 
  constructor(config: any) {
 
    // config also has:
 
    //   graph
 
    //   subj
 
    //   pred
 
    //   ctx
 
    //   getTargetPosForValue(value) -> getTarget result for value
 
    //   getValueForPos
 
    this.config = config;
 
    super();
 
    this.ctor2();
 
    if (this.config.ctx == null) {
 
      throw new Error("missing ctx");
 
    }
 
    // this seems to not fire enough.
 
    this.config.graph.runHandler(this._syncValue.bind(this), `adj sync ${this.config.subj.value} ${this.config.pred.value}`);
 
  }
 

	
 
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} #{@config.pred.value}")
 
  _syncValue() {
 
    this._currentValue = this.config.graph.floatValue(this.config.subj, this.config.pred);
 
    if (this._onChange) {
 
      return this._onChange();
 
    }
 
  }
 

	
 
  _getValue() {
 
    // this is a big speedup- callers use _getValue about 4x as much as
 
    // the graph changes and graph.floatValue is slow
 
    return this._currentValue;
 
  }
 

	
 
  getTarget() {
 
    return this.config.getTargetPosForValue(this._getValue());
 
  }
 

	
 
  _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
 
  subscribe(onChange: any) {
 
    // only works on one subscription at a time
 
    if (this._onChange) {
 
      throw new Error("multi subscribe not implemented");
 
    }
 
    return (this._onChange = onChange);
 
  }
 

	
 
  getTarget: () ->
 
    @config.getTargetPosForValue(@_getValue())
 
  continueDrag(pos: any) {
 
    // pos is vec2 of pixels relative to the drag start
 
    super.continueDrag(pos);
 
    const newValue = this.config.getValueForPos(this._editorCoordinates());
 

	
 
  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())
 
    return this.config.graph.patchObject(this.config.subj, this.config.pred, this.config.graph.LiteralRoundedFloat(newValue), this.config.ctx);
 
    //@_syncValue()
 
  }
 
}
 

	
 
    @config.graph.patchObject(@config.subj, @config.pred,
 
                              @config.graph.LiteralRoundedFloat(newValue),
 
                              @config.ctx)
 
    #@_syncValue()
 
class AdjustableFade extends Adjustable {
 
  yForV: any;
 
  zoomInX: any;
 
  i0: any;
 
  i1: any;
 
  note: any;
 
  constructor(yForV: any, zoomInX: any, i0: any, i1: any, note: any, offset: any, ctx: any) {
 
    this.yForV = yForV;
 
    this.zoomInX = zoomInX;
 
    this.i0 = i0;
 
    this.i1 = i1;
 
    this.note = note;
 
    super();
 
    this.config = {
 
      getSuggestedTargetOffset() {
 
        return offset;
 
      },
 
      getTarget: this.getTarget.bind(this),
 
      ctx,
 
    };
 
    this.ctor2();
 
  }
 

	
 
class window.AdjustableFade extends Adjustable
 
  constructor: (@yForV, @zoomInX, @i0, @i1, @note, offset, ctx) ->
 
    super()
 
    @config = {
 
      getSuggestedTargetOffset: -> offset
 
      getTarget: @getTarget.bind(@)
 
      ctx: ctx
 
    }
 
    @ctor2()
 
  getTarget() {
 
    const mid = this.note.midPoint(this.i0, this.i1);
 
    return $V([this.zoomInX(mid.e(1)), this.yForV(mid.e(2))]);
 
  }
 

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

	
 
  _getValue: ->
 
    @note.midPoint(@i0, @i1).e(1)
 
  _getValue() {
 
    return this.note.midPoint(this.i0, this.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)
 
  continueDrag(pos: { e: (arg0: number) => any }) {
 
    // pos is vec2 of pixels relative to the drag start
 
    super.continueDrag(pos);
 
    const { graph } = this.note;
 
    const U = (x: string) => graph.Uri(x);
 

	
 
    const goalCenterSec = this.zoomInX.invert(this.initialTarget.e(1) + pos.e(1));
 

	
 
    goalCenterSec = @zoomInX.invert(@initialTarget.e(1) + pos.e(1))
 
    const diamSec = this.note.worldPts[this.i1].e(1) - this.note.worldPts[this.i0].e(1);
 
    const newSec0 = goalCenterSec - diamSec / 2;
 
    const newSec1 = goalCenterSec + diamSec / 2;
 

	
 
    diamSec = @note.worldPts[@i1].e(1) - @note.worldPts[@i0].e(1)
 
    newSec0 = goalCenterSec - diamSec / 2
 
    newSec1 = goalCenterSec + diamSec / 2
 
    const originSec = graph.floatValue(this.note.uri, U(":originTime"));
 

	
 
    originSec = graph.floatValue(@note.uri, U(':originTime'))
 
    const p0 = this._makePatch(graph, this.i0, newSec0, originSec, this.config.ctx);
 
    const p1 = this._makePatch(graph, this.i1, newSec1, originSec, this.config.ctx);
 

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

	
 
    graph.applyAndSendPatch(@_addPatches(p0, p1))
 
    return graph.applyAndSendPatch(this._addPatches(p0, p1));
 
  }
 

	
 
  _makePatch: (graph, idx, newSec, originSec, ctx) ->
 
    graph.getObjectPatch(@note.worldPts[idx].uri,
 
                         graph.Uri(':time'),
 
                         graph.LiteralRoundedFloat(newSec - originSec), ctx)
 
  _makePatch(
 
    graph: { getObjectPatch: (arg0: any, arg1: any, arg2: any, arg3: any) => any; Uri: (arg0: string) => any; LiteralRoundedFloat: (arg0: number) => any },
 
    idx: string | number,
 
    newSec: number,
 
    originSec: number,
 
    ctx: any
 
  ) {
 
    return graph.getObjectPatch(this.note.worldPts[idx].uri, graph.Uri(":time"), graph.LiteralRoundedFloat(newSec - originSec), ctx);
 
  }
 

	
 
  _addPatches: (p0, p1) ->
 
    {
 
  _addPatches(p0: { addQuads: { concat: (arg0: any) => any }; delQuads: { concat: (arg0: any) => any } }, p1: { addQuads: any; delQuads: any }) {
 
    return {
 
      addQuads: p0.addQuads.concat(p1.addQuads),
 
      delQuads: p0.delQuads.concat(p1.delQuads)
 
    }
 
\ No newline at end of file
 
      delQuads: p0.delQuads.concat(p1.delQuads),
 
    };
 
  }
 
}
light9/web/timeline/adjusters.ts
Show inline comments
 
file renamed from light9/web/timeline/adjusters.coffee to light9/web/timeline/adjusters.ts
 
log = debug('adjusters')
 
Drawing = window.Drawing
 
import { debug } from "debug";
 
import { LitElement } from "lit";
 
import { customElement } from "lit/decorators.js";
 
import { throttle } from "underscore";
 
import * as d3 from "d3";
 
import { Adjustable } from "./adjustable";
 
import * as Drawing from "./drawing";
 
// https://www.npmjs.com/package/@types/sylvester Global values: $L, $M, $P, $V, Line, Matrix, Plane, Sylvester, Vector
 
const log = debug("adjusters");
 

	
 
maxDist = 60
 
const 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(@)
 
interface Drag {
 
  start: Vector;
 
  adj: Adjustable;
 
  cur?: Vector;
 
}
 
type QTreeData = Vector & { adj: Adjustable };
 
@customElement("light9-adjusters-canvas")
 
class AdjustersCanvas extends LitElement {
 
  static getter_properties: { setAdjuster: { type: any; notify: boolean } };
 
  static getter_observers: {};
 
  redraw: any;
 
  adjs: { [id: string | number]: Adjustable };
 
  hoveringNear: any;
 
  ctx: any;
 
  $: any;
 
  setAdjuster: any;
 
  offsetParent: any;
 
  currentDrag?: Drag;
 
  qt?: d3.Quadtree<QTreeData>;
 
  canvasCenter: any;
 
  static initClass() {
 
    this.getter_properties = { setAdjuster: { type: Function, notify: true } };
 
    this.getter_observers = ["updateAllCoords(adjs)"];
 
  }
 
  constructor() {
 
    super();
 
    this.redraw = throttle(this._throttledRedraw.bind(this), 30, { leading: false });
 
    this.adjs = {};
 
    this.hoveringNear = null;
 
  }
 

	
 
    # These don't fire; TimelineEditor calls the handlers for us.
 
    @addEventListener('mousedown', @onDown.bind(@))
 
    @addEventListener('mousemove', @onMove.bind(@))
 
    @addEventListener('mouseup', @onUp.bind(@))
 
  ready() {
 
    this.addEventListener("iron-resize", this.resizeUpdate.bind(this));
 
    this.ctx = this.$.canvas.getContext("2d");
 

	
 
    this.redraw();
 
    this.setAdjuster = this._setAdjuster.bind(this);
 

	
 
  _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()
 
    // These don't fire; TimelineEditor calls the handlers for us.
 
    this.addEventListener("mousedown", this.onDown.bind(this));
 
    this.addEventListener("mousemove", this.onMove.bind(this));
 
    return this.addEventListener("mouseup", this.onUp.bind(this));
 
  }
 
  addEventListener(arg0: string, arg1: any) {
 
    throw new Error("Method not implemented.");
 
  }
 

	
 
  _mousePos(ev: MouseEvent) {
 
    return $V([ev.clientX, ev.clientY - this.offsetParent.offsetTop]);
 
  }
 

	
 
  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()
 
  onDown(ev: MouseEvent) {
 
    if (ev.buttons === 1) {
 
      const start = this._mousePos(ev);
 
      const adj = this._adjAtPoint(start);
 
      if (adj) {
 
        ev.stopPropagation();
 
        this.currentDrag = { start, adj };
 
        return adj.startDrag();
 
      }
 
    }
 
  }
 

	
 
  onMove(ev: MouseEvent) {
 
    const pos = this._mousePos(ev);
 
    if (this.currentDrag) {
 
      this.hoveringNear = null;
 
      this.currentDrag.cur = pos;
 
      this.currentDrag.adj.continueDrag(this.currentDrag.cur.subtract(this.currentDrag.start));
 
      this.redraw();
 
    } else {
 
      const near = this._adjAtPoint(pos);
 
      if (this.hoveringNear !== near) {
 
        this.hoveringNear = near;
 
        this.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 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
 
  onUp(ev: any) {
 
    if (!this.currentDrag) {
 
      return;
 
    }
 
    this.currentDrag.adj.endDrag();
 
    this.currentDrag = undefined;
 
  }
 

	
 
    @redraw()
 

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

	
 
  updateAllCoords: ->
 
    @redraw()
 
  _setAdjuster(adjId: string | number, makeAdjustable?: () => Adjustable) {
 
    // 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 undefined to clear any
 
    // adjusters with this id.
 
    if (makeAdjustable == null) {
 
      if (this.adjs[adjId]) {
 
        delete this.adjs[adjId];
 
      }
 
    } else {
 
      // this might be able to reuse an existing one a bit
 
      const adj = makeAdjustable();
 
      this.adjs[adjId] = adj;
 
      adj.id = adjId;
 
    }
 

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

	
 
    (window as any).debug_adjsCount = Object.keys(this.adjs).length;
 
  }
 

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

	
 
  _adjAtPoint(pt: Vector): Adjustable|undefined {
 
    const nearest = this.qt!.find(pt.e(1), pt.e(2));
 
    if (nearest == null || nearest.distanceFrom(pt) > maxDist) {
 
      return undefined;
 
    }
 
    return nearest != null ? nearest.adj : undefined;
 
  }
 

	
 
  _throttledRedraw: () ->
 
    return unless @ctx?
 
    console.time('adjs redraw')
 
    @_layoutCenters()
 
    
 
    @ctx.clearRect(0, 0, @$.canvas.width, @$.canvas.height)
 
  resizeUpdate(ev: { target: { offsetWidth: any; offsetHeight: any } }) {
 
    this.$.canvas.width = ev.target.offsetWidth;
 
    this.$.canvas.height = ev.target.offsetHeight;
 
    this.canvasCenter = $V([this.$.canvas.width / 2, this.$.canvas.height / 2]);
 
    return this.redraw();
 
  }
 

	
 
    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')
 
  _throttledRedraw() {
 
    if (this.ctx == null) {
 
      return;
 
    }
 
    console.time("adjs redraw");
 
    this._layoutCenters();
 

	
 
    this.ctx.clearRect(0, 0, this.$.canvas.width, this.$.canvas.height);
 

	
 
    for (let adjId in this.adjs) {
 
      const adj = this.adjs[adjId];
 
      const ctr = adj.getHandle();
 
      const target = adj.getTarget();
 
      if (this._isOffScreen(target)) {
 
        continue;
 
      }
 
      this._drawConnector(ctr, target);
 

	
 
  _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())
 
      this._drawAdjuster(adj.getDisplayValue(), ctr.e(1) - 20, ctr.e(2) - 10, ctr.e(1) + 20, ctr.e(2) + 10, adj === this.hoveringNear);
 
    }
 
    return console.timeEnd("adjs redraw");
 
  }
 

	
 
    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)
 
  _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
 
    const qt = d3.quadtree<QTreeData>(
 
      [],
 
      (d: QTreeData) => d.e(1),
 
      (d: QTreeData) => d.e(2)
 
    );
 
    this.qt = qt;
 

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

	
 
    let _: string | number, adj: { handle: any; getSuggestedHandle: () => any };
 
    for (_ in this.adjs) {
 
      adj = this.adjs[_];
 
      adj.handle = this._clampOnScreen(adj.getSuggestedHandle());
 
    }
 

	
 
  _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
 
    const numTries = 8;
 
    for (let tryn = 0; tryn < numTries; tryn++) {
 
      for (_ in this.adjs) {
 
        adj = this.adjs[_];
 
        let current = adj.handle;
 
        qt.remove(current);
 
        const nearest = qt.find(current.e(1), current.e(2), maxDist);
 
        if (nearest) {
 
          const dist = current.distanceFrom(nearest);
 
          if (dist < maxDist) {
 
            current = this._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)])
 
  }
 

	
 
  _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
 

	
 
  _stepAway(
 
    current: Vector,
 
    nearest: Vector,
 
    dx: number
 
  ) {
 
    const away = current.subtract(nearest).toUnitVector();
 
    const toScreenCenter = this.canvasCenter.subtract(current).toUnitVector();
 
    const goalSpacingPx = 20;
 
    return this._clampOnScreen(current.add(away.x(goalSpacingPx * dx)));
 
  }
 

	
 
  _isOffScreen(pos: Vector):boolean {
 
    return pos.e(1) < 0 || pos.e(1) > this.$.canvas.width || pos.e(2) < 0 || pos.e(2) > this.$.canvas.height;
 
  }
 

	
 
  _clampOnScreen(pos: Vector): Vector {
 
    const marg = 30;
 
    return $V([Math.max(marg, Math.min(this.$.canvas.width - marg, pos.e(1))), Math.max(marg, Math.min(this.$.canvas.height - marg, pos.e(2)))]);
 
  }
 

	
 
    @ctx.shadowColor = 'black'
 
    @ctx.shadowBlur = 15
 
    @ctx.shadowOffsetX = 5
 
    @ctx.shadowOffsetY = 9
 
    
 
    @ctx.fillStyle = if hover then '#ffff88' else 'rgba(255, 255, 0, 0.5)'
 
    @ctx.beginPath()
 
    Drawing.roundRect(@ctx, x1, y1, x2, y2, radius)
 
    @ctx.fill()
 
  _drawConnector(ctr: Vector, target: Vector) {
 
    this.ctx.strokeStyle = "#aaa";
 
    this.ctx.lineWidth = 2;
 
    this.ctx.beginPath();
 
    Drawing.line(this.ctx, ctr, target);
 
    this.ctx.stroke();
 
  }
 

	
 
  _drawAdjuster(label: any, x1: number, y1: number, x2: number, y2: number, hover: boolean) {
 
    const radius = 8;
 

	
 
    this.ctx.shadowColor = "black";
 
    this.ctx.shadowBlur = 15;
 
    this.ctx.shadowOffsetX = 5;
 
    this.ctx.shadowOffsetY = 9;
 

	
 
    this.ctx.fillStyle = hover ? "#ffff88" : "rgba(255, 255, 0, 0.5)";
 
    this.ctx.beginPath();
 
    Drawing.roundRect(this.ctx, x1, y1, x2, y2, radius);
 
    this.ctx.fill();
 

	
 
    @ctx.shadowColor = 'rgba(0,0,0,0)'
 
        
 
    @ctx.strokeStyle = 'yellow'
 
    @ctx.lineWidth = 2
 
    @ctx.setLineDash([3, 3])
 
    @ctx.beginPath()
 
    Drawing.roundRect(@ctx, x1, y1, x2, y2, radius)
 
    @ctx.stroke()
 
    @ctx.setLineDash([])
 
    this.ctx.shadowColor = "rgba(0,0,0,0)";
 

	
 
    this.ctx.strokeStyle = "yellow";
 
    this.ctx.lineWidth = 2;
 
    this.ctx.setLineDash([3, 3]);
 
    this.ctx.beginPath();
 
    Drawing.roundRect(this.ctx, x1, y1, x2, y2, radius);
 
    this.ctx.stroke();
 
    this.ctx.setLineDash([]);
 

	
 
    @ctx.font = "12px sans"
 
    @ctx.fillStyle = '#000'
 
    @ctx.fillText(label, x1 + 5, y2 - 5, x2 - x1 - 10)
 
    this.ctx.font = "12px sans";
 
    this.ctx.fillStyle = "#000";
 
    this.ctx.fillText(label, x1 + 5, y2 - 5, x2 - x1 - 10);
 

	
 
    # coords from a center that's passed in
 
    # # special layout for the thaeter ones with middinh
 
    # l/r arrows
 
    # mouse arrow cursor upon hover, and accent the hovered adjuster
 
    # connector
 
)
 
\ No newline at end of file
 
    // coords from a center that's passed in
 
    // # special layout for the thaeter ones with middinh
 
    // l/r arrows
 
    // mouse arrow cursor upon hover, and accent the hovered adjuster
 
    // connector
 
  }
 
}
 

	
 

	
light9/web/timeline/brick_layout.ts
Show inline comments
 
file renamed from light9/web/timeline/brick_layout.coffee to light9/web/timeline/brick_layout.ts
 
log = debug('brick')
 
import { debug } from "debug";
 
import { sortBy } from "underscore";
 
import { ViewState } from "viewstate";
 
const log = debug("brick");
 

	
 
interface Placement {
 
  row?: number;
 
  prev?: number;
 
  t0: number;
 
  t1: number;
 
  onRowChange: () => void;
 
}
 

	
 
export class BrickLayout {
 
  viewState: ViewState;
 
  numRows: number;
 
  noteRow: { [uri: string]: Placement };
 
  constructor(viewState: ViewState, numRows: number) {
 
    this.viewState = viewState;
 
    this.numRows = numRows;
 
    this.noteRow = {}; // uristr: row, t0, t1, onRowChange
 
  }
 

	
 
class window.BrickLayout
 
  constructor: (@viewState, @numRows) ->
 
    @noteRow = {} # uristr: row, t0, t1, onRowChange
 
    
 
  addNote: (n, onRowChange) ->
 
    @noteRow[n.uri.value] = {row: 0, t0: 0, t1: 0, onRowChange: onRowChange}
 
    
 
  setNoteSpan: (n, t0, t1) ->
 
    @noteRow[n.uri.value].t0 = t0
 
    @noteRow[n.uri.value].t1 = t1
 
    @_recompute()
 
    
 
  delNote: (n) ->
 
    delete @noteRow[n.uri.value]
 
    @_recompute()
 
    
 
  _recompute: ->
 
    for u, row of @noteRow
 
      row.prev = row.row
 
      row.row = null
 
    overlap = (a, b) -> a.t0 < b.t1 and a.t1 > b.t0
 
  addNote(n: { uri: { value: string } }, onRowChange: any) {
 
    this.noteRow[n.uri.value] = { row: 0, t0: 0, t1: 0, onRowChange };
 
  }
 

	
 
  setNoteSpan(n: { uri: { value: string } }, t0: any, t1: any) {
 
    this.noteRow[n.uri.value].t0 = t0;
 
    this.noteRow[n.uri.value].t1 = t1;
 
    this._recompute();
 
  }
 

	
 
  delNote(n: { uri: { value: string } }) {
 
    delete this.noteRow[n.uri.value];
 
    this._recompute();
 
  }
 

	
 
  _recompute() {
 
    for (let u in this.noteRow) {
 
      const row = this.noteRow[u];
 
      row.prev = row.row;
 
      row.row = undefined;
 
    }
 
    const overlap = (a: Placement, b: Placement) => a.t0 < b.t1 && a.t1 > b.t0;
 

	
 
    notesByWidth = _.sortBy(
 
      ({dur: row.t1 - row.t0 + row.t0 * .0001, uri: u} for u, row of @noteRow),
 
      'dur')
 
    notesByWidth.reverse()
 
    const result = [];
 
    for (let u in this.noteRow) {
 
      const row = this.noteRow[u];
 
      result.push({ dur: row.t1 - row.t0 + row.t0 * 0.0001, uri: u });
 
    }
 
    const notesByWidth = sortBy(result, "dur");
 
    notesByWidth.reverse();
 

	
 
    for n in notesByWidth
 
      blockedRows = new Set()
 
      for u, other of @noteRow
 
        if other.row != null
 
          if overlap(other, @noteRow[n.uri])
 
            blockedRows.add(other.row)
 
    for (let n of Array.from(notesByWidth)) {
 
      const blockedRows = new Set();
 
      for (let u in this.noteRow) {
 
        const other = this.noteRow[u];
 
        if (other.row !== null) {
 
          if (overlap(other, this.noteRow[n.uri])) {
 
            blockedRows.add(other.row);
 
          }
 
        }
 
      }
 

	
 
      for r in [0 ... @numRows]
 
        if not blockedRows.has(r)
 
          @noteRow[n.uri].row = r
 
          break
 
      if @noteRow[n.uri].row == null
 
        log("warning: couldn't place #{n.uri}")
 
        @noteRow[n.uri].row = 0
 
      if @noteRow[n.uri].row != @noteRow[n.uri].prev
 
        @noteRow[n.uri].onRowChange()
 
          
 
  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      
 
      for (let r = 0; r < this.numRows; r++) {
 
        if (!blockedRows.has(r)) {
 
          this.noteRow[n.uri].row = r;
 
          break;
 
        }
 
      }
 
      if (this.noteRow[n.uri].row === null) {
 
        log(`warning: couldn't place ${n.uri}`);
 
        this.noteRow[n.uri].row = 0;
 
      }
 
      if (this.noteRow[n.uri].row !== this.noteRow[n.uri].prev) {
 
        this.noteRow[n.uri].onRowChange();
 
      }
 
    }
 
  }
 

	
 
  rowBottom(row: number) {
 
    return this.viewState.rowsY() + 20 + 150 * row + 140;
 
  }
 

	
 
  yForVFor(n: { uri: { value: string } }) {
 
    const row = this.noteRow[n.uri.value].row;
 
    if (row === undefined) {
 
      throw new Error();
 
    }
 
    const rowBottom = this.rowBottom(row);
 
    const rowTop = rowBottom - 140;
 
    return (v: number) => rowBottom + (rowTop - rowBottom) * v;
 
  }
 
}
light9/web/timeline/drawing.ts
Show inline comments
 
(window as any).Drawing = {};
 

	
 
(window as any).Drawing.svgPathFromPoints = function (pts: { forEach: (arg0: (p: any) => void) => void }) {
 
export function svgPathFromPoints(pts: { forEach: (arg0: (p: any) => void) => void }) {
 
  let out = "";
 
  pts.forEach(function (p: Number[] | { elements: Number[] }) {
 
    let x, y;
 
@@ -20,7 +19,7 @@
 
  return out;
 
};
 

	
 
(window as any).Drawing.line = function (
 
export function line(
 
  ctx: { moveTo: (arg0: any, arg1: any) => void; lineTo: (arg0: any, arg1: any) => any },
 
  p1: { e: (arg0: number) => any },
 
  p2: { e: (arg0: number) => any }
 
@@ -30,7 +29,7 @@
 
};
 

	
 
// http://stackoverflow.com/a/4959890
 
(window as any).Drawing.roundRect = function (
 
export function roundRect(
 
  ctx: {
 
    beginPath: () => void;
 
    moveTo: (arg0: any, arg1: any) => void;
light9/web/timeline/index.html
Show inline comments
 
@@ -2,9 +2,8 @@
 
<html>
 
  <head>
 
    <title>timeline</title>
 
    <meta charset="utf-8" />
 
    <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
 
    <link rel="import" href="timeline-elements.html">
 
    <meta charset="utf-8">
 
    <script type="module" src="./timeline/timeline-elements.ts"></script>
 
  </head>
 
  <body>
 
    <light9-timeline-editor style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px;">
light9/web/timeline/light9-timeline-audio.ts
Show inline comments
 
file renamed from light9/web/timeline/light9-timeline-audio.html to light9/web/timeline/light9-timeline-audio.ts
 
<link rel="import" href="/lib/polymer/polymer.html">
 
import { debug } from "debug";
 

	
 
import { css, html, LitElement, TemplateResult } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 

	
 
const log = debug("audio");
 

	
 
<!-- (potentially-zoomed) spectrogram view -->
 
<dom-module id="light9-timeline-audio">
 
  <template>
 
    <style>
 
     :host {
 
         display: block;
 
         /* shouldn't be seen, but black is correct for 'no
 
// (potentially-zoomed) spectrogram view
 
@customElement("light9-timeline-audio")
 
export class Light9TimelineAudio extends LitElement {
 
  render() {
 
    return html`
 
      <style>
 
        :host {
 
          display: block;
 
          /* shouldn't be seen, but black is correct for 'no
 
         audio'. Maybe loading stripes would be better */
 
         background: #202322; 
 
     }
 
     div {
 
         width: 100%;
 
         height: 100%;
 
         overflow: hidden;
 
     }
 
     img {
 
         height: 100%;
 
         position: relative;
 
     }
 
    </style>
 
    <div>
 
      <img src="{{imgSrc}}"
 
           style="width: {{imgWidth}} ; left: {{imgLeft}}">
 
    </div>
 
  </template>
 
  <script>
 
   const log = debug('audio');
 
   Polymer({
 
       is: "light9-timeline-audio",
 
       properties: {
 
           graph: {type: Object, notify: true},
 
           show: {type: String, notify: true},
 
           song: {type: String, notify: true},
 
           zoom: {type: Object, notify: true},
 
           imgSrc: { type: String, notify: true},
 
           imgWidth: { computed: '_imgWidth(zoom)' },
 
           imgLeft: { computed: '_imgLeft(zoom)' },
 
       },
 
       observers: [
 
           'setImgSrc(graph, show, song)'
 
       ],
 
       ready: function() {
 
           this.zoom = {duration: 0};
 
       },       
 
       setImgSrc: function() {
 
           graph.runHandler(function() {
 
               try {
 
                   var root = this.graph.stringValue(
 
                       this.graph.Uri(this.show),
 
                       this.graph.Uri(':spectrogramUrlRoot'));
 
               } catch(e) {
 
                   return;
 
               }
 
               
 
               try {
 
                   var filename = this.graph.stringValue(
 
                       this.song, this.graph.Uri(':songFilename'));
 
               } catch(e) {
 
                   return;
 
               }
 
               
 
               this.imgSrc = root + '/' + filename.replace('.wav', '.png').replace('.ogg', '.png');
 
           }.bind(this), "timeline-audio " + this.song);
 
       },
 
       _imgWidth: function(zoom) {
 
           if (!zoom.duration) {
 
               return "100%";
 
           }
 
          background: #202322;
 
        }
 
        div {
 
          width: 100%;
 
          height: 100%;
 
          overflow: hidden;
 
        }
 
        img {
 
          height: 100%;
 
          position: relative;
 
        }
 
      </style>
 
      <div>
 
        <img src="{{imgSrc}}" style="width: {{imgWidth}} ; left: {{imgLeft}}" />
 
      </div>
 
    `;
 
  }
 
  //    properties= {
 
  //        graph: {type: Object, notify: true},
 
  //        show: {type: String, notify: true},
 
  //        song: {type: String, notify: true},
 
  //        zoom: {type: Object, notify: true},
 
  //        imgSrc: { type: String, notify: true},
 
  //        imgWidth: { computed: '_imgWidth(zoom)' },
 
  //        imgLeft: { computed: '_imgLeft(zoom)' },
 
  //    }
 
  //    observers= [
 
  //        'setImgSrc(graph, show, song)'
 
  //    ]
 
  ready() {
 
    this.zoom = { duration: 0 };
 
  }
 
  setImgSrc() {
 
    graph.runHandler(
 
      function () {
 
        try {
 
          var root = this.graph.stringValue(this.graph.Uri(this.show), this.graph.Uri(":spectrogramUrlRoot"));
 
        } catch (e) {
 
          return;
 
        }
 

	
 
           return (100 / ((zoom.t2 - zoom.t1) / zoom.duration)) + "%";
 
       },
 
       _imgLeft: function(zoom) {
 
           if (!zoom.duration) {
 
               return "0";
 
           }
 
        try {
 
          var filename = this.graph.stringValue(this.song, this.graph.Uri(":songFilename"));
 
        } catch (e) {
 
          return;
 
        }
 

	
 
           var percentPerSec = 100 / (zoom.t2 - zoom.t1);
 
           return (-percentPerSec * zoom.t1) + '%';
 
       },
 
   });
 
  </script>
 
</dom-module>
 
        this.imgSrc = root + "/" + filename.replace(".wav", ".png").replace(".ogg", ".png");
 
      }.bind(this),
 
      "timeline-audio " + this.song
 
    );
 
  }
 
  _imgWidth(zoom) {
 
    if (!zoom.duration) {
 
      return "100%";
 
    }
 

	
 
    return 100 / ((zoom.t2 - zoom.t1) / zoom.duration) + "%";
 
  }
 
  _imgLeft(zoom) {
 
    if (!zoom.duration) {
 
      return "0";
 
    }
 

	
 
    var percentPerSec = 100 / (zoom.t2 - zoom.t1);
 
    return -percentPerSec * zoom.t1 + "%";
 
  }
 
}
light9/web/timeline/timeline-elements.ts
Show inline comments
 
file renamed from light9/web/timeline/timeline-elements.html to light9/web/timeline/timeline-elements.ts
 
<script src="/lib/debug/debug-build.js"></script>
 
<script>
 
 debug.enable('*');
 
</script>
 
<link rel="import" href="/lib/polymer/polymer.html">
 
console.log("hi tl")
 
import { debug } from "debug";
 
import { css, html, LitElement, TemplateResult } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
export {Light9TimelineAudio} from "../timeline/light9-timeline-audio"
 
debug.enable("*");
 
/*
 
 <link rel="import" href="/lib/polymer/polymer.html">
 
<link rel="import" href="/lib/polymer/lib/utils/render-status.html">
 
<link rel="import" href="/lib/iron-resizable-behavior/iron-resizable-behavior.html">
 
<link rel="import" href="/lib/iron-ajax/iron-ajax.html">
 
@@ -16,12 +19,14 @@
 

	
 
<script type="module" src="/light9-vidref-replay-stack.js"></script>
 

	
 
*/
 

	
 
<!-- Whole editor- include this on your page.
 
     Most coordinates are relative to this element.
 
   -->
 
<dom-module id="light9-timeline-editor">
 
  <template>
 
// Whole editor- include this on your page.
 
// Most coordinates are relative to this element.
 
@customElement("light9-timeline-editor")
 
export class Light9TimelineEditor extends LitElement {
 
  render() {
 
    return html`
 
    <style>
 
     :host {
 
         background: #444;
 
@@ -94,13 +99,15 @@
 
      <light9-cursor-canvas id="cursorCanvas" view-state="{{viewState}}"></light9-cursor-canvas>
 
      <light9-vidref-replay-stack size="small"></light9-vidref-replay-stack>
 
    </div>
 
  </template>
 
  
 
</dom-module>
 
`;
 
  }
 
}
 

	
 
<!-- the whole section that pans/zooms in time (most of the editor) -->
 
<dom-module id="light9-timeline-time-zoomed">
 
  <template>
 
// the whole section that pans/zooms in time (most of the editor)
 
@customElement("light9-timeline-time-zoomed")
 
export class Light9TimelineTimeZoomed extends LitElement {
 
  render() {
 
    return html`
 
    <style>
 
     :host {
 
         display: flex;
 
@@ -147,95 +154,95 @@
 
                                         config="{{item}}">
 
      </light9-timeline-note-inline-attrs>
 
    </template>
 
  </template>
 
</dom-module>
 
`;
 
  } 
 
}
 

	
 
<dom-module id="light9-cursor-canvas">
 
  <template>
 
    <style>
 
     #canvas, :host {
 
         pointer-events: none;
 
     }
 
    </style>
 
    <canvas id="canvas"></canvas>
 
  </template>
 
</dom-module>
 
      
 
<dom-module id="light9-adjusters-canvas">
 
  <template>
 
    <style>
 
     :host {
 
         pointer-events: none;
 
     }
 
    </style>
 
    <canvas id="canvas"></canvas>
 
  </template>
 
</dom-module>
 
      
 
@customElement("light9-cursor-canvas")
 
export class Light9CursorCanvas extends LitElement {
 
  render() {
 
    return html`
 
      <style>
 
        #canvas,
 
        :host {
 
          pointer-events: none;
 
        }
 
      </style>
 
      <canvas id="canvas"></canvas>
 
    `;
 
  }
 
}
 

	
 
@customElement("light9-adjusters-canvas")
 
export class Light9AdjustersCanvas extends LitElement {
 
  render() {
 
    return html`
 
      <style>
 
        :host {
 
          pointer-events: none;
 
        }
 
      </style>
 
      <canvas id="canvas"></canvas>
 
    `;
 
  }
 
}      
 

	
 
<!-- seconds labels -->
 
<dom-module id="light9-timeline-time-axis">
 
  <template>
 
    <style>
 
     :host {
 
         display: block;
 
     }
 
     div {
 
         width: 100%;
 
         height: 31px;
 
     }
 
     svg {
 
         width: 100%;
 
         height: 30px;
 
     }
 
    </style>
 
    <svg id="timeAxis" xmlns="http://www.w3.org/2000/svg">
 
// seconds labels
 
@customElement("light9-timeline-time-axis")
 
export class Light9TimelineTimeAxis extends LitElement {
 
  render() {
 
    return html`
 
      <style>
 
       text  {
 
           fill: white;
 
           color: white;
 
           font-size: 135%;
 
           font-weight: bold;
 
       }
 
       
 
        :host {
 
          display: block;
 
        }
 
        div {
 
          width: 100%;
 
          height: 31px;
 
        }
 
        svg {
 
          width: 100%;
 
          height: 30px;
 
        }
 
      </style>
 
      <g id="axis" transform="translate(0,30)"></g>    
 
    </svg>
 
  </template>
 
</dom-module>
 
      <svg id="timeAxis" xmlns="http://www.w3.org/2000/svg">
 
        <style>
 
          text {
 
            fill: white;
 
            color: white;
 
            font-size: 135%;
 
            font-weight: bold;
 
          }
 
        </style>
 
        <g id="axis" transform="translate(0,30)"></g>
 
      </svg>
 
    `;
 
  }
 
}
 

	
 
<!-- All the adjusters you can edit or select. Tells a light9-adjusters-canvas how to draw them. Probabaly doesn't need to be an element.
 
     This element manages their layout and suppresion.
 
     Owns the selection.
 
     Maybe includes selecting things that don't even have adjusters.
 
     Maybe manages the layout of other labels and text too, to avoid overlaps.
 
   -->
 
<dom-module id="light9-timeline-adjusters">
 
  <template>
 
    <style>
 
     :host {
 
         pointer-events: none; /* restored on the individual adjusters */
 
     }
 
    </style>
 
  </template>
 
</dom-module>
 
// All the adjusters you can edit or select. Tells a light9-adjusters-canvas how to draw them. Probabaly doesn't need to be an element.
 
//  This element manages their layout and suppresion.
 
//  Owns the selection.
 
//  Maybe includes selecting things that don't even have adjusters.
 
//  Maybe manages the layout of other labels and text too, to avoid overlaps.
 
@customElement("light9-timeline-adjusters")
 
export class Light9TimelineAdjusters extends LitElement {
 
  render() {
 
    return html`
 
      <style>
 
        :host {
 
          pointer-events: none; /* restored on the individual adjusters */
 
        }
 
      </style>
 
    `;
 
  }
 
}
 

	
 

	
 
/*
 
<script src="/lib/async/dist/async.js"></script>
 
<script src="/lib/knockout/dist/knockout.js"></script>
 
<script src="/lib/shortcut/index.js"></script>
 
<script src="/lib/sylvester/sylvester.js"></script>
 
<script src="/lib/underscore/underscore-min.js"></script>
 
<script src="/node_modules/d3/dist/d3.min.js"></script>
 
<script src="/node_modules/n3/n3-browser.js"></script> 
 
<script src="/node_modules/pixi.js/dist/pixi.min.js"></script>
 
<script src="/node_modules/tinycolor2/dist/tinycolor-min.js"></script>
 

	
 
<script src="drawing.js"></script>
 
<script src="../coffee_element.js"></script>
 
<script src="viewstate.js"></script>
 
<script src="brick_layout.js"></script>
 
<script src="adjustable.js"></script>
 
<script src="adjusters.js"></script>
 
<script src="timeline.js"></script>
 
<script src="cursor_canvas.js"></script>
 
*/
light9/web/timeline/timeline.coffee
Show inline comments
 
deleted file
light9/web/timeline/viewstate.ts
Show inline comments
 
file renamed from light9/web/timeline/viewstate.coffee to light9/web/timeline/viewstate.ts
 
class window.ViewState
 
  constructor: () ->
 
    # caller updates all these observables
 
    @zoomSpec =
 
      duration: ko.observable(100) # current song duration
 
      t1: ko.observable(0)
 
      t2: ko.observable(100)
 
    @cursor =
 
      t: ko.observable(20) # songTime
 
    @mouse =
 
      pos: ko.observable($V([0,0]))
 
    @width = ko.observable(500)
 
    @coveredByDiagramTop = ko.observable(0) # page coords
 
    # all these are relative to #coveredByDiagram:
 
    @audioY = ko.observable(0)
 
    @audioH = ko.observable(0)
 
    @zoomedTimeY = ko.observable(0)
 
    @zoomedTimeH = ko.observable(0)
 
    @rowsY = ko.observable(0)
 
      
 
    @fullZoomX = d3.scaleLinear()
 
    @zoomInX = d3.scaleLinear()
 
import * as ko from "knockout";
 
import * as d3 from "d3";
 
import debug from "debug";
 

	
 
    @zoomAnimSec = .1
 
const log = debug("viewstate");
 
export class ViewState {
 
  zoomSpec: {
 
    duration: ko.Observable<number>; // current song duration
 
    t1: ko.Observable<number>;
 
    t2: ko.Observable<number>;
 
  };
 
  cursor: { t: ko.Observable<number> };
 
  mouse: { pos: ko.Observable<Vector> };
 
  width: ko.Observable<number>;
 
  coveredByDiagramTop: ko.Observable<number>;
 
  audioY: ko.Observable<number>;
 
  audioH: ko.Observable<number>;
 
  zoomedTimeY: ko.Observable<number>;
 
  zoomedTimeH: ko.Observable<number>;
 
  rowsY: ko.Observable<number>;
 
  fullZoomX: d3.ScaleLinear<number, number>;
 
  zoomInX: d3.ScaleLinear<number, number>;
 
  zoomAnimSec: number;
 
  constructor() {
 
    // caller updates all these observables
 
    this.zoomSpec = {
 
      duration: ko.observable(100), // current song duration
 
      t1: ko.observable(0),
 
      t2: ko.observable(100),
 
    };
 
    this.cursor = { t: ko.observable(20) }; // songTime
 
    this.mouse = { pos: ko.observable($V([0, 0])) };
 
    this.width = ko.observable(500);
 
    this.coveredByDiagramTop = ko.observable(0); // page coords
 
    // all these are relative to #coveredByDiagram:
 
    this.audioY = ko.observable(0);
 
    this.audioH = ko.observable(0);
 
    this.zoomedTimeY = ko.observable(0);
 
    this.zoomedTimeH = ko.observable(0);
 
    this.rowsY = ko.observable(0);
 

	
 
    ko.computed(@maintainZoomLimitsAndScales.bind(@))
 
 
 
  setWidth: (w) ->
 
    @width(w)
 
    @maintainZoomLimitsAndScales() # before other handlers run
 
    
 
  maintainZoomLimitsAndScales: () ->
 
    # not for cursor updates
 
    this.fullZoomX = d3.scaleLinear();
 
    this.zoomInX = d3.scaleLinear();
 

	
 
    this.zoomAnimSec = 0.1;
 

	
 
    ko.computed(this.maintainZoomLimitsAndScales.bind(this));
 
  }
 

	
 
    if @zoomSpec.t1() < 0
 
      @zoomSpec.t1(0)
 
    if @zoomSpec.duration() and @zoomSpec.t2() > @zoomSpec.duration()
 
      @zoomSpec.t2(@zoomSpec.duration())
 
  setWidth(w: any) {
 
    this.width(w);
 
    this.maintainZoomLimitsAndScales(); // before other handlers run
 
  }
 

	
 
    rightPad = 5 # don't let time adjuster fall off right edge
 
    @fullZoomX.domain([0, @zoomSpec.duration()])
 
    @fullZoomX.range([0, @width() - rightPad])
 
  maintainZoomLimitsAndScales() {
 
    // not for cursor updates
 

	
 
    if (this.zoomSpec.t1() < 0) {
 
      this.zoomSpec.t1(0);
 
    }
 
    if (this.zoomSpec.duration() && this.zoomSpec.t2() > this.zoomSpec.duration()) {
 
      this.zoomSpec.t2(this.zoomSpec.duration());
 
    }
 

	
 
    @zoomInX.domain([@zoomSpec.t1(), @zoomSpec.t2()])
 
    @zoomInX.range([0, @width() - rightPad])
 
    
 
  latestMouseTime: ->
 
    @zoomInX.invert(@mouse.pos().e(1))
 
    const rightPad = 5; // don't let time adjuster fall off right edge
 
    this.fullZoomX.domain([0, this.zoomSpec.duration()]);
 
    this.fullZoomX.range([0, this.width() - rightPad]);
 

	
 
  onMouseWheel: (deltaY) ->
 
    zs = @zoomSpec
 
    this.zoomInX.domain([this.zoomSpec.t1(), this.zoomSpec.t2()]);
 
    this.zoomInX.range([0, this.width() - rightPad]);
 
  }
 

	
 
  latestMouseTime(): number {
 
    return this.zoomInX.invert(this.mouse.pos().e(1));
 
  }
 

	
 
    center = @latestMouseTime()
 
    left = center - zs.t1()
 
    right = zs.t2() - center
 
    scale = Math.pow(1.005, deltaY)
 
  onMouseWheel(deltaY: any) {
 
    const zs = this.zoomSpec;
 

	
 
    zs.t1(center - left * scale)
 
    zs.t2(center + right * scale)
 
    log('view to', ko.toJSON(@))
 
    const center = this.latestMouseTime();
 
    const left = center - zs.t1();
 
    const right = zs.t2() - center;
 
    const scale = Math.pow(1.005, deltaY);
 

	
 
    zs.t1(center - left * scale);
 
    zs.t2(center + right * scale);
 
    log("view to", ko.toJSON(this));
 
  }
 

	
 
  frameCursor: ->
 
    zs = @zoomSpec
 
    visSeconds = zs.t2() - zs.t1()
 
    margin = visSeconds * .4
 
    # buggy: really needs t1/t2 to limit their ranges
 
    if @cursor.t() < zs.t1() or @cursor.t() > zs.t2() - visSeconds * .6
 
      newCenter = @cursor.t() + margin
 
      @animatedZoom(newCenter - visSeconds / 2,
 
                    newCenter + visSeconds / 2, @zoomAnimSec)
 
  frameToEnd: ->
 
    @animatedZoom(@cursor.t() - 2, @zoomSpec.duration(), @zoomAnimSec)
 
  frameAll: ->
 
    @animatedZoom(0, @zoomSpec.duration(), @zoomAnimSec)
 
  animatedZoom: (newT1, newT2, secs) ->
 
    fps = 30
 
    oldT1 = @zoomSpec.t1()
 
    oldT2 = @zoomSpec.t2()
 
    lastTime = 0
 
    for step in [0..secs * fps]
 
      frac = step / (secs * fps)
 
      do (frac) =>
 
        gotoStep = =>
 
          @zoomSpec.t1((1 - frac) * oldT1 + frac * newT1)
 
          @zoomSpec.t2((1 - frac) * oldT2 + frac * newT2)
 
        delay = frac * secs * 1000
 
        setTimeout(gotoStep, delay)
 
        lastTime = delay
 
    setTimeout(=>
 
      @zoomSpec.t1(newT1)
 
      @zoomSpec.t2(newT2)
 
    , lastTime + 10)
 
  frameCursor() {
 
    const zs = this.zoomSpec;
 
    const visSeconds = zs.t2() - zs.t1();
 
    const margin = visSeconds * 0.4;
 
    // buggy: really needs t1/t2 to limit their ranges
 
    if (this.cursor.t() < zs.t1() || this.cursor.t() > zs.t2() - visSeconds * 0.6) {
 
      const newCenter = this.cursor.t() + margin;
 
      this.animatedZoom(newCenter - visSeconds / 2, newCenter + visSeconds / 2, this.zoomAnimSec);
 
    }
 
  }
 
  frameToEnd() {
 
    this.animatedZoom(this.cursor.t() - 2, this.zoomSpec.duration(), this.zoomAnimSec);
 
  }
 
  frameAll() {
 
    this.animatedZoom(0, this.zoomSpec.duration(), this.zoomAnimSec);
 
  }
 
  animatedZoom(newT1: number, newT2: number, secs: number) {
 
    const fps = 30;
 
    const oldT1 = this.zoomSpec.t1();
 
    const oldT2 = this.zoomSpec.t2();
 
    let lastTime = 0;
 
    for (let step = 0; step < secs * fps; step++) {
 
      const frac = step / (secs * fps);
 
      ((frac) => {
 
        const gotoStep = () => {
 
          this.zoomSpec.t1((1 - frac) * oldT1 + frac * newT1);
 
          return this.zoomSpec.t2((1 - frac) * oldT2 + frac * newT2);
 
        };
 
        const delay = frac * secs * 1000;
 
        setTimeout(gotoStep, delay);
 
        lastTime = delay;
 
      })(frac);
 
    }
 
    setTimeout(() => {
 
      this.zoomSpec.t1(newT1);
 
      return this.zoomSpec.t2(newT2);
 
    }, lastTime + 10);
 
  }
 
}
package.json
Show inline comments
 
@@ -9,10 +9,18 @@
 
    "test": "test"
 
  },
 
  "dependencies": {
 
    "@types/d3": "^7.1.0",
 
    "@types/debug": "^4.1.7",
 
    "@types/node": "^17.0.31",
 
    "@types/sylvester": "^0.1.8",
 
    "@types/underscore": "^1.11.4",
 
    "d3": "^7.4.4",
 
    "debug": "^4.3.4",
 
    "knockout": "^3.5.1",
 
    "lit": "^2.2.3",
 
    "parse-prometheus-text-format": "^1.1.1",
 
    "sylvester": "^0.0.21",
 
    "underscore": "^1.13.3",
 
    "vite": "^2.9.1",
 
    "vite-plugin-rewrite-all": "^0.1.2"
 
  }
pnpm-lock.yaml
Show inline comments
 
lockfileVersion: 5.3
 

	
 
specifiers:
 
  '@types/d3': ^7.1.0
 
  '@types/debug': ^4.1.7
 
  '@types/node': ^17.0.31
 
  '@types/sylvester': ^0.1.8
 
  '@types/underscore': ^1.11.4
 
  d3: ^7.4.4
 
  debug: ^4.3.4
 
  knockout: ^3.5.1
 
  lit: ^2.2.3
 
  parse-prometheus-text-format: ^1.1.1
 
  sylvester: ^0.0.21
 
  underscore: ^1.13.3
 
  vite: ^2.9.1
 
  vite-plugin-rewrite-all: ^0.1.2
 

	
 
dependencies:
 
  '@types/d3': 7.1.0
 
  '@types/debug': 4.1.7
 
  '@types/node': 17.0.31
 
  '@types/sylvester': 0.1.8
 
  '@types/underscore': 1.11.4
 
  d3: 7.4.4
 
  debug: 4.3.4
 
  knockout: 3.5.1
 
  lit: 2.2.3
 
  parse-prometheus-text-format: 1.1.1
 
  sylvester: 0.0.21
 
  underscore: 1.13.3
 
  vite: 2.9.1
 
  vite-plugin-rewrite-all: 0.1.2_vite@2.9.1
 

	
 
@@ -22,19 +38,473 @@ packages:
 
    resolution: {integrity: sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA==}
 
    dev: false
 

	
 
  /@types/d3-array/3.0.2:
 
    resolution: {integrity: sha512-5mjGjz6XOXKOCdTajXTZ/pMsg236RdiwKPrRPWAEf/2S/+PzwY+LLYShUpeysWaMvsdS7LArh6GdUefoxpchsQ==}
 
    dev: false
 

	
 
  /@types/d3-axis/3.0.1:
 
    resolution: {integrity: sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==}
 
    dependencies:
 
      '@types/d3-selection': 3.0.2
 
    dev: false
 

	
 
  /@types/d3-brush/3.0.1:
 
    resolution: {integrity: sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==}
 
    dependencies:
 
      '@types/d3-selection': 3.0.2
 
    dev: false
 

	
 
  /@types/d3-chord/3.0.1:
 
    resolution: {integrity: sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==}
 
    dev: false
 

	
 
  /@types/d3-color/3.0.2:
 
    resolution: {integrity: sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ==}
 
    dev: false
 

	
 
  /@types/d3-contour/3.0.1:
 
    resolution: {integrity: sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==}
 
    dependencies:
 
      '@types/d3-array': 3.0.2
 
      '@types/geojson': 7946.0.8
 
    dev: false
 

	
 
  /@types/d3-delaunay/6.0.0:
 
    resolution: {integrity: sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w==}
 
    dev: false
 

	
 
  /@types/d3-dispatch/3.0.1:
 
    resolution: {integrity: sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==}
 
    dev: false
 

	
 
  /@types/d3-drag/3.0.1:
 
    resolution: {integrity: sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==}
 
    dependencies:
 
      '@types/d3-selection': 3.0.2
 
    dev: false
 

	
 
  /@types/d3-dsv/3.0.0:
 
    resolution: {integrity: sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==}
 
    dev: false
 

	
 
  /@types/d3-ease/3.0.0:
 
    resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==}
 
    dev: false
 

	
 
  /@types/d3-fetch/3.0.1:
 
    resolution: {integrity: sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==}
 
    dependencies:
 
      '@types/d3-dsv': 3.0.0
 
    dev: false
 

	
 
  /@types/d3-force/3.0.3:
 
    resolution: {integrity: sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==}
 
    dev: false
 

	
 
  /@types/d3-format/3.0.1:
 
    resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==}
 
    dev: false
 

	
 
  /@types/d3-geo/3.0.2:
 
    resolution: {integrity: sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==}
 
    dependencies:
 
      '@types/geojson': 7946.0.8
 
    dev: false
 

	
 
  /@types/d3-hierarchy/3.0.2:
 
    resolution: {integrity: sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ==}
 
    dev: false
 

	
 
  /@types/d3-interpolate/3.0.1:
 
    resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==}
 
    dependencies:
 
      '@types/d3-color': 3.0.2
 
    dev: false
 

	
 
  /@types/d3-path/3.0.0:
 
    resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==}
 
    dev: false
 

	
 
  /@types/d3-polygon/3.0.0:
 
    resolution: {integrity: sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==}
 
    dev: false
 

	
 
  /@types/d3-quadtree/3.0.2:
 
    resolution: {integrity: sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==}
 
    dev: false
 

	
 
  /@types/d3-random/3.0.1:
 
    resolution: {integrity: sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==}
 
    dev: false
 

	
 
  /@types/d3-scale-chromatic/3.0.0:
 
    resolution: {integrity: sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==}
 
    dev: false
 

	
 
  /@types/d3-scale/4.0.2:
 
    resolution: {integrity: sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==}
 
    dependencies:
 
      '@types/d3-time': 3.0.0
 
    dev: false
 

	
 
  /@types/d3-selection/3.0.2:
 
    resolution: {integrity: sha512-d29EDd0iUBrRoKhPndhDY6U/PYxOWqgIZwKTooy2UkBfU7TNZNpRho0yLWPxlatQrFWk2mnTu71IZQ4+LRgKlQ==}
 
    dev: false
 

	
 
  /@types/d3-shape/3.0.2:
 
    resolution: {integrity: sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw==}
 
    dependencies:
 
      '@types/d3-path': 3.0.0
 
    dev: false
 

	
 
  /@types/d3-time-format/4.0.0:
 
    resolution: {integrity: sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==}
 
    dev: false
 

	
 
  /@types/d3-time/3.0.0:
 
    resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==}
 
    dev: false
 

	
 
  /@types/d3-timer/3.0.0:
 
    resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==}
 
    dev: false
 

	
 
  /@types/d3-transition/3.0.1:
 
    resolution: {integrity: sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g==}
 
    dependencies:
 
      '@types/d3-selection': 3.0.2
 
    dev: false
 

	
 
  /@types/d3-zoom/3.0.1:
 
    resolution: {integrity: sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==}
 
    dependencies:
 
      '@types/d3-interpolate': 3.0.1
 
      '@types/d3-selection': 3.0.2
 
    dev: false
 

	
 
  /@types/d3/7.1.0:
 
    resolution: {integrity: sha512-gYWvgeGjEl+zmF8c+U1RNIKqe7sfQwIXeLXO5Os72TjDjCEtgpvGBvZ8dXlAuSS1m6B90Y1Uo6Bm36OGR/OtCA==}
 
    dependencies:
 
      '@types/d3-array': 3.0.2
 
      '@types/d3-axis': 3.0.1
 
      '@types/d3-brush': 3.0.1
 
      '@types/d3-chord': 3.0.1
 
      '@types/d3-color': 3.0.2
 
      '@types/d3-contour': 3.0.1
 
      '@types/d3-delaunay': 6.0.0
 
      '@types/d3-dispatch': 3.0.1
 
      '@types/d3-drag': 3.0.1
 
      '@types/d3-dsv': 3.0.0
 
      '@types/d3-ease': 3.0.0
 
      '@types/d3-fetch': 3.0.1
 
      '@types/d3-force': 3.0.3
 
      '@types/d3-format': 3.0.1
 
      '@types/d3-geo': 3.0.2
 
      '@types/d3-hierarchy': 3.0.2
 
      '@types/d3-interpolate': 3.0.1
 
      '@types/d3-path': 3.0.0
 
      '@types/d3-polygon': 3.0.0
 
      '@types/d3-quadtree': 3.0.2
 
      '@types/d3-random': 3.0.1
 
      '@types/d3-scale': 4.0.2
 
      '@types/d3-scale-chromatic': 3.0.0
 
      '@types/d3-selection': 3.0.2
 
      '@types/d3-shape': 3.0.2
 
      '@types/d3-time': 3.0.0
 
      '@types/d3-time-format': 4.0.0
 
      '@types/d3-timer': 3.0.0
 
      '@types/d3-transition': 3.0.1
 
      '@types/d3-zoom': 3.0.1
 
    dev: false
 

	
 
  /@types/debug/4.1.7:
 
    resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
 
    dependencies:
 
      '@types/ms': 0.7.31
 
    dev: false
 

	
 
  /@types/geojson/7946.0.8:
 
    resolution: {integrity: sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==}
 
    dev: false
 

	
 
  /@types/ms/0.7.31:
 
    resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
 
    dev: false
 

	
 
  /@types/node/17.0.31:
 
    resolution: {integrity: sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==}
 
    dev: false
 

	
 
  /@types/sylvester/0.1.8:
 
    resolution: {integrity: sha512-x1bzR4PCxvv1/9iPrbdQ15gWgP8Tp8EPjO4VLjhMijepB44BzJ/XvJavoPViSiHxlBX6NgzRgO0H+qa68lJFGA==}
 
    dev: false
 

	
 
  /@types/trusted-types/2.0.2:
 
    resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==}
 
    dev: false
 

	
 
  /@types/underscore/1.11.4:
 
    resolution: {integrity: sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==}
 
    dev: false
 

	
 
  /commander/7.2.0:
 
    resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
 
    engines: {node: '>= 10'}
 
    dev: false
 

	
 
  /connect-history-api-fallback/1.6.0:
 
    resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==}
 
    engines: {node: '>=0.8'}
 
    dev: false
 

	
 
  /d3-array/3.1.6:
 
    resolution: {integrity: sha512-DCbBBNuKOeiR9h04ySRBMW52TFVc91O9wJziuyXw6Ztmy8D3oZbmCkOO3UHKC7ceNJsN2Mavo9+vwV8EAEUXzA==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      internmap: 2.0.3
 
    dev: false
 

	
 
  /d3-axis/3.0.0:
 
    resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-brush/3.0.0:
 
    resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-dispatch: 3.0.1
 
      d3-drag: 3.0.0
 
      d3-interpolate: 3.0.1
 
      d3-selection: 3.0.0
 
      d3-transition: 3.0.1_d3-selection@3.0.0
 
    dev: false
 

	
 
  /d3-chord/3.0.1:
 
    resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-path: 3.0.1
 
    dev: false
 

	
 
  /d3-color/3.1.0:
 
    resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-contour/3.0.1:
 
    resolution: {integrity: sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-array: 3.1.6
 
    dev: false
 

	
 
  /d3-delaunay/6.0.2:
 
    resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      delaunator: 5.0.0
 
    dev: false
 

	
 
  /d3-dispatch/3.0.1:
 
    resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-drag/3.0.0:
 
    resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-dispatch: 3.0.1
 
      d3-selection: 3.0.0
 
    dev: false
 

	
 
  /d3-dsv/3.0.1:
 
    resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
 
    engines: {node: '>=12'}
 
    hasBin: true
 
    dependencies:
 
      commander: 7.2.0
 
      iconv-lite: 0.6.3
 
      rw: 1.3.3
 
    dev: false
 

	
 
  /d3-ease/3.0.1:
 
    resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-fetch/3.0.1:
 
    resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-dsv: 3.0.1
 
    dev: false
 

	
 
  /d3-force/3.0.0:
 
    resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-dispatch: 3.0.1
 
      d3-quadtree: 3.0.1
 
      d3-timer: 3.0.1
 
    dev: false
 

	
 
  /d3-format/3.1.0:
 
    resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-geo/3.0.1:
 
    resolution: {integrity: sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-array: 3.1.6
 
    dev: false
 

	
 
  /d3-hierarchy/3.1.2:
 
    resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-interpolate/3.0.1:
 
    resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-color: 3.1.0
 
    dev: false
 

	
 
  /d3-path/3.0.1:
 
    resolution: {integrity: sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-polygon/3.0.1:
 
    resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-quadtree/3.0.1:
 
    resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-random/3.0.1:
 
    resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-scale-chromatic/3.0.0:
 
    resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-color: 3.1.0
 
      d3-interpolate: 3.0.1
 
    dev: false
 

	
 
  /d3-scale/4.0.2:
 
    resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-array: 3.1.6
 
      d3-format: 3.1.0
 
      d3-interpolate: 3.0.1
 
      d3-time: 3.0.0
 
      d3-time-format: 4.1.0
 
    dev: false
 

	
 
  /d3-selection/3.0.0:
 
    resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-shape/3.1.0:
 
    resolution: {integrity: sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-path: 3.0.1
 
    dev: false
 

	
 
  /d3-time-format/4.1.0:
 
    resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-time: 3.0.0
 
    dev: false
 

	
 
  /d3-time/3.0.0:
 
    resolution: {integrity: sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-array: 3.1.6
 
    dev: false
 

	
 
  /d3-timer/3.0.1:
 
    resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /d3-transition/3.0.1_d3-selection@3.0.0:
 
    resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
 
    engines: {node: '>=12'}
 
    peerDependencies:
 
      d3-selection: 2 - 3
 
    dependencies:
 
      d3-color: 3.1.0
 
      d3-dispatch: 3.0.1
 
      d3-ease: 3.0.1
 
      d3-interpolate: 3.0.1
 
      d3-selection: 3.0.0
 
      d3-timer: 3.0.1
 
    dev: false
 

	
 
  /d3-zoom/3.0.0:
 
    resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-dispatch: 3.0.1
 
      d3-drag: 3.0.0
 
      d3-interpolate: 3.0.1
 
      d3-selection: 3.0.0
 
      d3-transition: 3.0.1_d3-selection@3.0.0
 
    dev: false
 

	
 
  /d3/7.4.4:
 
    resolution: {integrity: sha512-97FE+MYdAlV3R9P74+R3Uar7wUKkIFu89UWMjEaDhiJ9VxKvqaMxauImy8PC2DdBkdM2BxJOIoLxPrcZUyrKoQ==}
 
    engines: {node: '>=12'}
 
    dependencies:
 
      d3-array: 3.1.6
 
      d3-axis: 3.0.0
 
      d3-brush: 3.0.0
 
      d3-chord: 3.0.1
 
      d3-color: 3.1.0
 
      d3-contour: 3.0.1
 
      d3-delaunay: 6.0.2
 
      d3-dispatch: 3.0.1
 
      d3-drag: 3.0.0
 
      d3-dsv: 3.0.1
 
      d3-ease: 3.0.1
 
      d3-fetch: 3.0.1
 
      d3-force: 3.0.0
 
      d3-format: 3.1.0
 
      d3-geo: 3.0.1
 
      d3-hierarchy: 3.1.2
 
      d3-interpolate: 3.0.1
 
      d3-path: 3.0.1
 
      d3-polygon: 3.0.1
 
      d3-quadtree: 3.0.1
 
      d3-random: 3.0.1
 
      d3-scale: 4.0.2
 
      d3-scale-chromatic: 3.0.0
 
      d3-selection: 3.0.0
 
      d3-shape: 3.1.0
 
      d3-time: 3.0.0
 
      d3-time-format: 4.1.0
 
      d3-timer: 3.0.1
 
      d3-transition: 3.0.1_d3-selection@3.0.0
 
      d3-zoom: 3.0.0
 
    dev: false
 

	
 
  /debug/4.3.4:
 
    resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
 
    engines: {node: '>=6.0'}
 
@@ -47,6 +517,12 @@ packages:
 
      ms: 2.1.2
 
    dev: false
 

	
 
  /delaunator/5.0.0:
 
    resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==}
 
    dependencies:
 
      robust-predicates: 3.0.1
 
    dev: false
 

	
 
  /esbuild-android-64/0.14.34:
 
    resolution: {integrity: sha512-XfxcfJqmMYsT/LXqrptzFxmaR3GWzXHDLdFNIhm6S00zPaQF1TBBWm+9t0RZ6LRR7iwH57DPjaOeW20vMqI4Yw==}
 
    engines: {node: '>=12'}
 
@@ -274,12 +750,28 @@ packages:
 
      function-bind: 1.1.1
 
    dev: false
 

	
 
  /iconv-lite/0.6.3:
 
    resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
 
    engines: {node: '>=0.10.0'}
 
    dependencies:
 
      safer-buffer: 2.1.2
 
    dev: false
 

	
 
  /internmap/2.0.3:
 
    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /is-core-module/2.8.1:
 
    resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==}
 
    dependencies:
 
      has: 1.0.3
 
    dev: false
 

	
 
  /knockout/3.5.1:
 
    resolution: {integrity: sha512-wRJ9I4az0QcsH7A4v4l0enUpkS++MBx0BnL/68KaLzJg7x1qmbjSlwEoCNol7KTYZ+pmtI7Eh2J0Nu6/2Z5J/Q==}
 
    dev: false
 

	
 
  /lit-element/3.2.0:
 
    resolution: {integrity: sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==}
 
    dependencies:
 
@@ -343,6 +835,10 @@ packages:
 
      supports-preserve-symlinks-flag: 1.0.0
 
    dev: false
 

	
 
  /robust-predicates/3.0.1:
 
    resolution: {integrity: sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==}
 
    dev: false
 

	
 
  /rollup/2.70.1:
 
    resolution: {integrity: sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA==}
 
    engines: {node: '>=10.0.0'}
 
@@ -351,6 +847,14 @@ packages:
 
      fsevents: 2.3.2
 
    dev: false
 

	
 
  /rw/1.3.3:
 
    resolution: {integrity: sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=}
 
    dev: false
 

	
 
  /safer-buffer/2.1.2:
 
    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
 
    dev: false
 

	
 
  /shallow-equal/1.2.1:
 
    resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
 
    dev: false
 
@@ -365,6 +869,15 @@ packages:
 
    engines: {node: '>= 0.4'}
 
    dev: false
 

	
 
  /sylvester/0.0.21:
 
    resolution: {integrity: sha1-KYexzivS84sNzio0OIiEv6RADqc=}
 
    engines: {node: '>=0.2.6'}
 
    dev: false
 

	
 
  /underscore/1.13.3:
 
    resolution: {integrity: sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==}
 
    dev: false
 

	
 
  /vite-plugin-rewrite-all/0.1.2_vite@2.9.1:
 
    resolution: {integrity: sha512-hBFuG043kbixgZ/ke9SzKhkO6P8a5ryxD0CmZTe+/Cz17RIKi7uSeNUJy79V4FgavZ7pWVRg0tqVwJ7lP/A2/Q==}
 
    engines: {node: '>=12.0.0'}
0 comments (0 inline, 0 general)