diff --git a/.vscode/settings.json b/.vscode/settings.json --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "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 } - diff --git a/light9/web/coffee_element.coffee b/light9/web/coffee_element.coffee deleted file mode 100644 --- a/light9/web/coffee_element.coffee +++ /dev/null @@ -1,15 +0,0 @@ -# Polymer seems to need static getters for 'observers' and -# 'properties', not just static attributes, though I don't know how it -# can even tell the difference. -# -# This workaround is to use names like '@getter_properties' in the -# class then register with this function that fixes them. -# -# Also see http://coffeescript.org/#unsupported-get-set -window.coffeeElementSetup = (cls) -> - for attr in ['properties', 'observers'] - val = cls['getter_' + attr] - if val? - do (val) -> - Object.defineProperty(cls, attr, {get: ( -> val)}) - customElements.define(cls.is, cls) diff --git a/light9/web/timeline/Note.coffee b/light9/web/timeline/Note.coffee new file mode 100644 --- /dev/null +++ b/light9/web/timeline/Note.coffee @@ -0,0 +1,285 @@ +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 diff --git a/light9/web/timeline/Project.coffee b/light9/web/timeline/Project.coffee new file mode 100644 --- /dev/null +++ b/light9/web/timeline/Project.coffee @@ -0,0 +1,92 @@ +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)) diff --git a/light9/web/timeline/TimeAxis.coffee b/light9/web/timeline/TimeAxis.coffee new file mode 100644 --- /dev/null +++ b/light9/web/timeline/TimeAxis.coffee @@ -0,0 +1,19 @@ +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 diff --git a/light9/web/timeline/TimeZoomed.coffee b/light9/web/timeline/TimeZoomed.coffee new file mode 100644 --- /dev/null +++ b/light9/web/timeline/TimeZoomed.coffee @@ -0,0 +1,174 @@ +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) + diff --git a/light9/web/timeline/TimelineEditor.coffee b/light9/web/timeline/TimelineEditor.coffee new file mode 100644 --- /dev/null +++ b/light9/web/timeline/TimelineEditor.coffee @@ -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 = '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 + })) diff --git a/light9/web/timeline/adjustable.coffee b/light9/web/timeline/adjustable.ts rename from light9/web/timeline/adjustable.coffee rename to light9/web/timeline/adjustable.ts --- a/light9/web/timeline/adjustable.coffee +++ b/light9/web/timeline/adjustable.ts @@ -1,190 +1,269 @@ -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 - return @targetDraggedTo - ev = d3.event.sourceEvent + endDrag() {} + // override + + _editorCoordinates() { + // vec2 of mouse relative to + 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), + }; + } +} diff --git a/light9/web/timeline/adjusters.coffee b/light9/web/timeline/adjusters.ts rename from light9/web/timeline/adjusters.coffee rename to light9/web/timeline/adjusters.ts --- a/light9/web/timeline/adjusters.coffee +++ b/light9/web/timeline/adjusters.ts @@ -1,198 +1,273 @@ -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; + 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( + [], + (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 + } +} + + diff --git a/light9/web/timeline/brick_layout.coffee b/light9/web/timeline/brick_layout.ts rename from light9/web/timeline/brick_layout.coffee rename to light9/web/timeline/brick_layout.ts --- a/light9/web/timeline/brick_layout.coffee +++ b/light9/web/timeline/brick_layout.ts @@ -1,53 +1,95 @@ -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; + } +} diff --git a/light9/web/timeline/drawing.ts b/light9/web/timeline/drawing.ts --- a/light9/web/timeline/drawing.ts +++ b/light9/web/timeline/drawing.ts @@ -1,6 +1,5 @@ -(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; diff --git a/light9/web/timeline/index.html b/light9/web/timeline/index.html --- a/light9/web/timeline/index.html +++ b/light9/web/timeline/index.html @@ -2,9 +2,8 @@ timeline - - - + + diff --git a/light9/web/timeline/light9-timeline-audio.html b/light9/web/timeline/light9-timeline-audio.ts rename from light9/web/timeline/light9-timeline-audio.html rename to light9/web/timeline/light9-timeline-audio.ts --- a/light9/web/timeline/light9-timeline-audio.html +++ b/light9/web/timeline/light9-timeline-audio.ts @@ -1,84 +1,85 @@ - +import { debug } from "debug"; + +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +const log = debug("audio"); - - - - - + 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 + "%"; + } +} diff --git a/light9/web/timeline/timeline-elements.html b/light9/web/timeline/timeline-elements.ts rename from light9/web/timeline/timeline-elements.html rename to light9/web/timeline/timeline-elements.ts --- a/light9/web/timeline/timeline-elements.html +++ b/light9/web/timeline/timeline-elements.ts @@ -1,8 +1,11 @@ - - - +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("*"); +/* + @@ -16,12 +19,14 @@ +*/ - - - - - - - - - +@customElement("light9-cursor-canvas") +export class Light9CursorCanvas extends LitElement { + render() { + return html` + + + `; + } +} + +@customElement("light9-adjusters-canvas") +export class Light9AdjustersCanvas extends LitElement { + render() { + return html` + + + `; + } +} - - - - + + + + + `; + } +} - - - - +// 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` + + `; + } +} - +/* - - - - - - - - - - - - +*/ diff --git a/light9/web/timeline/timeline.coffee b/light9/web/timeline/timeline.coffee deleted file mode 100644 --- a/light9/web/timeline/timeline.coffee +++ /dev/null @@ -1,746 +0,0 @@ -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)) - - -coffeeElementSetup(class TimelineEditor extends Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element) - @is: 'light9-timeline-editor' - @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 - })) -) - - -# 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. -coffeeElementSetup(class TimeZoomed extends Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element) - @is: 'light9-timeline-time-zoomed' - @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) -) - - - -coffeeElementSetup(class TimeAxis extends Polymer.Element - @is: "light9-timeline-time-axis", - @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) -) - - -# 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 diff --git a/light9/web/timeline/viewstate.coffee b/light9/web/timeline/viewstate.ts rename from light9/web/timeline/viewstate.coffee rename to light9/web/timeline/viewstate.ts --- a/light9/web/timeline/viewstate.coffee +++ b/light9/web/timeline/viewstate.ts @@ -1,92 +1,128 @@ -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; // current song duration + t1: ko.Observable; + t2: ko.Observable; + }; + cursor: { t: ko.Observable }; + mouse: { pos: ko.Observable }; + width: ko.Observable; + coveredByDiagramTop: ko.Observable; + audioY: ko.Observable; + audioH: ko.Observable; + zoomedTimeY: ko.Observable; + zoomedTimeH: ko.Observable; + rowsY: ko.Observable; + fullZoomX: d3.ScaleLinear; + zoomInX: d3.ScaleLinear; + 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); + } +} diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -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" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,18 +1,34 @@ 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'}