diff web/timeline/TimelineEditor.coffee @ 2376:4556eebe5d73

topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author drewp@bigasterisk.com
date Sun, 12 May 2024 19:02:10 -0700
parents light9/web/timeline/TimelineEditor.coffee@611c3e97de2f
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/timeline/TimelineEditor.coffee	Sun May 12 19:02:10 2024 -0700
@@ -0,0 +1,194 @@
+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 = showRoot
+
+  _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
+    }))