changeset 1515:37dd11031bcf

draw timeline adjusters in canvas Ignore-this: e9e518e7a0700da1e7771dd559ddc7ed
author Drew Perttula <drewp@bigasterisk.com>
date Thu, 06 Apr 2017 10:04:27 +0000
parents d5c3dec3dbd9
children b1fed8880ada
files light9/web/timeline/adjustable.coffee light9/web/timeline/timeline-elements.html light9/web/timeline/timeline.coffee
diffstat 3 files changed, 195 insertions(+), 105 deletions(-) [+]
line wrap: on
line diff
--- a/light9/web/timeline/adjustable.coffee	Thu Mar 30 09:52:21 2017 +0000
+++ b/light9/web/timeline/adjustable.coffee	Thu Apr 06 10:04:27 2017 +0000
@@ -1,8 +1,8 @@
 log = console.log
 
 class Adjustable
-  # Some value you can edit in the UI, probably by dragging stuff. May
-  # have a <light9-timeline-adjuster> associated. This object does the
+  # 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
--- a/light9/web/timeline/timeline-elements.html	Thu Mar 30 09:52:21 2017 +0000
+++ b/light9/web/timeline/timeline-elements.html	Thu Apr 06 10:04:27 2017 +0000
@@ -28,7 +28,7 @@
      light9-timeline-time-zoomed {
          flex-grow: 1;
      }
-     #dia, #adjusters, #cursorCanvas {
+     #dia, #adjusters, #cursorCanvas, #adjustersCanvas {
          position: absolute;
          left: 0; top: 0; right: 0; bottom: 0;
      }
@@ -65,13 +65,8 @@
                                  zoom-in-x="{{zoomInX}}">
     </light9-timeline-time-zoomed>
     <light9-timeline-diagram-layer id="dia"></light9-timeline-diagram-layer>
-    <light9-timeline-adjusters id="adjusters"
-                               dia="{{dia}}"
-                               graph="{{graph}}"
-                               song="{{song}}"
-                               set-adjuster="{{setAdjuster}}"
-                               zoom-in-x="{{zoomInX}}">
-    </light9-timeline-adjusters>
+    <light9-adjusters-canvas id="adjustersCanvas" set-adjuster="{{setAdjuster}}">
+    </light9-adjusters-canvas>
     <light9-cursor-canvas id="cursorCanvas"></light9-cursor-canvas>
   </template>
  
@@ -182,6 +177,17 @@
   </template>
 </dom-module>
       
+<dom-module id="light9-adjusters-canvas">
+  <template>
+    <style>
+     #canvas, :host {
+         pointer-events: none;
+     }
+    </style>
+    <canvas id="canvas"></canvas>
+  </template>
+</dom-module>
+      
 
 <!-- seconds labels -->
 <dom-module id="light9-timeline-time-axis">
@@ -231,7 +237,7 @@
   </template>
 </dom-module>
 
-<!-- All the adjusters you can edit or select.
+<!-- 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.
@@ -245,9 +251,6 @@
      }
 
     </style>
-    <div id="all">
-      <!-- light9-timeline-adjuster repeated here  -->
-    </div>
   </template>
 </dom-module>
 
--- a/light9/web/timeline/timeline.coffee	Thu Mar 30 09:52:21 2017 +0000
+++ b/light9/web/timeline/timeline.coffee	Thu Apr 06 10:04:27 2017 +0000
@@ -70,7 +70,7 @@
         pos: ko.observable($V([0,0]))
     @fullZoomX = d3.scaleLinear()
     @zoomInX = d3.scaleLinear()
-    @setAdjuster = @$.adjusters.setAdjuster.bind(@$.adjusters)
+    @setAdjuster = @$.adjustersCanvas.setAdjuster.bind(@$.adjustersCanvas)
 
     setInterval(@updateDebugSummary.bind(@), 100)
 
@@ -82,7 +82,6 @@
     elemCount = (tag) -> document.getElementsByTagName(tag).length
     @debug = "#{window.debug_zoomOrLayoutChangedCount} layout change,
      #{elemCount('light9-timeline-note')} notes,
-     #{elemCount('light9-timeline-adjuster')} adjusters,
      #{elemCount('light9-timeline-graph-row')} rows,
      #{window.debug_adjsCount} adjuster items registered,
      #{window.debug_adjUpdateDisplay} adjuster updateDisplay calls,
@@ -96,6 +95,7 @@
     @trackMouse()
     @bindKeys()
     @bindWheelZoom()
+    @forwardMouseEventsToAdjustersCanvas()
 
     @makeZoomAdjs()
 
@@ -114,7 +114,7 @@
 
     # todo: these run a lot of work purely for a time change    
     @dia.setTimeAxis(@width(), @$.zoomed.$.audio.offsetTop, @zoomInX)
-    @$.adjusters.updateAllCoords()
+    @$.adjustersCanvas.updateAllCoords()
 
     # cursor needs update when layout changes, but I don't want
     # zoom/layout to depend on the playback time
@@ -164,6 +164,12 @@
       zs.t1(center - left * scale)
       zs.t2(center + right * scale)
 
+  forwardMouseEventsToAdjustersCanvas: ->
+    ac = @$.adjustersCanvas
+    @addEventListener('mousedown', ac.onDown.bind(ac))
+    @addEventListener('mousemove', ac.onMove.bind(ac))
+    @addEventListener('mouseup', ac.onUp.bind(ac))
+
   animatedZoom: (newT1, newT2, secs) ->
     fps = 30
     oldT1 = @viewState.zoomSpec.t1()
@@ -202,7 +208,7 @@
         @animatedZoom(newCenter - visSeconds / 2,
                       newCenter + visSeconds / 2, zoomAnimSec)
     shortcut.add "L", =>
-      @$.adjusters.updateAllCoords()
+      @$.adjustersCanvas.updateAllCoords()
 
   makeZoomAdjs: ->
     yMid = => @$.audio.offsetTop + @$.audio.offsetHeight / 2
@@ -574,86 +580,9 @@
     patch = {delQuads: [{subject: @song, predicate: @graph.Uri(':note'), object: @uri, graph: @song}], addQuads: []}
     @graph.applyAndSendPatch(patch)
 
-
-Polymer
-  is: "light9-timeline-adjusters"
-  properties:
-    adjs: { type: Object, notify: true }, # adjId: Adjustable
-    dia: { type: Object }
-
-  ready: ->
-    @adjs = {}
     
-  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 @adjs[adjId] or not makeAdjustable?
-      if not makeAdjustable?
-        delete @adjs[adjId]
-      else
-        adj = makeAdjustable()
-        @adjs[adjId] = adj
-        adj.id = adjId
-      @debounce('adjsChanged', @adjsChanged.bind(@), 1)
-    else
-      for e in @$.all.children
-        if e.id == adjId
-          e.updateDisplay()
-
-    window.debug_adjsCount = Object.keys(@adjs).length
-    
-  adjsChanged: ->
-    updateChildren @$.all, Object.keys(@adjs), (newUri) =>
-      child = document.createElement('light9-timeline-adjuster')
-      child.dia = @dia
-      child.graph = @graph
-      child.uri = newUri
-      child.id = newUri
-      child.visible = true
-      child.adj = @adjs[newUri]
-      return child
-    @updateAllCoords()
-
-  layoutCenters: ->
-    # push Adjustable centers around to avoid overlaps
-    qt = d3.quadtree()
-    qt.extent([[0,0], [8000,8000]])
-    for _, adj of @adjs
-      desired = adj.getSuggestedCenter()
-      output = desired
-      for tries in [0...2]
-        nearest = qt.find(output.e(1), output.e(2))
-        if nearest
-          dist = output.distanceFrom(nearest)
-          if dist < 60
-            away = output.subtract(nearest).toUnitVector()
-            toScreenCenter = $V([500,200]).subtract(output).toUnitVector()
-            output = output.add(away.x(60).add(toScreenCenter.x(10)))
-
-      if -50 < output.e(1) < 20 # mostly for zoom-left
-        output.setElements([
-          Math.max(20, output.e(1)),
-          output.e(2)])
-        
-      adj.centerOffset = output.subtract(adj.getTarget())
-      qt.add(output.elements)
-
-  updateAllCoords: ->
-    @layoutCenters()
-    
-    for elem in @querySelectorAll('light9-timeline-adjuster')
-      elem.updateDisplay()
-    
-
-Polymer
-  is: 'light9-timeline-adjuster'
-  properties:
-    graph: { type: Object, notify: true }
-    adj: { type: Object, notify: true }
-    id: { type: String, notify: true }
+class deleteme
+  go: ->
     visible: { type: Boolean, notify: true }
     
     displayValue: { type: String }
@@ -714,6 +643,28 @@
     return
   out
 
+_line = (ctx, p1, p2) ->
+  ctx.moveTo(p1.e(1), p1.e(2))
+  ctx.lineTo(p2.e(1), p2.e(2))
+
+# http://stackoverflow.com/a/4959890
+_roundRect = (ctx, sx,sy,ex,ey,r) ->
+    d2r = Math.PI/180
+    r = ( ( ex - sx ) / 2 ) if ( ex - sx ) - ( 2 * r ) < 0 # ensure that the radius isn't too large for x
+    r = ( ( ey - sy ) / 2 ) if ( ey - sy ) - ( 2 * r ) < 0 # ensure that the radius isn't too large for y
+    ctx.beginPath();
+    ctx.moveTo(sx+r,sy);
+    ctx.lineTo(ex-r,sy);
+    ctx.arc(ex-r,sy+r,r,d2r*270,d2r*360,false);
+    ctx.lineTo(ex,ey-r);
+    ctx.arc(ex-r,ey-r,r,d2r*0,d2r*90,false);
+    ctx.lineTo(sx+r,ey);
+    ctx.arc(sx+r,ey-r,r,d2r*90,d2r*180,false);
+    ctx.lineTo(sx,sy+r);
+    ctx.arc(sx+r,sy+r,r,d2r*180,d2r*270,false);
+    ctx.closePath();
+
+
 Polymer
   is: 'light9-cursor-canvas'
   behaviors: [ Polymer.IronResizableBehavior ]
@@ -752,25 +703,21 @@
     }
     @redraw()
 
-  _line: (p1, p2) ->
-    @ctx.moveTo(p1.e(1), p1.e(2))
-    @ctx.lineTo(p2.e(1), p2.e(2))
-
   redraw: ->
     @ctx.clearRect(0, 0, @$.canvas.width, @$.canvas.height)
 
     @ctx.strokeStyle = '#fff'
     @ctx.lineWidth = 0.5
     @ctx.beginPath()
-    @_line($V([0, @mouseY]), $V([@$.canvas.width, @mouseY]), '#fff', '0.5px')
-    @_line($V([@mouseX, 0]), $V([@mouseX, @$.canvas.height]), '#fff', '0.5px')
+    _line(@ctx, $V([0, @mouseY]), $V([@$.canvas.width, @mouseY]))
+    _line(@ctx, $V([@mouseX, 0]), $V([@mouseX, @$.canvas.height]))
     @ctx.stroke()
 
     if @cursorPath
       @ctx.strokeStyle = '#ff0303'
       @ctx.lineWidth = 1.5
       @ctx.beginPath()
-      @_line(@cursorPath.top0, @cursorPath.top1, '#ff0303', 1.5)
+      _line(@ctx, @cursorPath.top0, @cursorPath.top1)
       @ctx.stroke()
 
       @ctx.fillStyle = '#9c0303'
@@ -783,11 +730,151 @@
       @ctx.strokeStyle = '#ff0303'
       @ctx.lineWidth = 3
       @ctx.beginPath()
-      @_line(@cursorPath.bot0, @cursorPath.bot1, '#ff0303', '3px')
+      _line(@ctx, @cursorPath.bot0, @cursorPath.bot1, '#ff0303', '3px')
       @ctx.stroke()
     
     
 Polymer
+  is: 'light9-adjusters-canvas'
+  behaviors: [ Polymer.IronResizableBehavior ]
+  properties:
+    adjs: { type: Object, notify: true }, # adjId: Adjustable
+  listeners: 'iron-resize': 'update'
+  ready: ->
+    @adjs = {}
+    @ctx = @$.canvas.getContext('2d')
+    
+    @redraw()
+   
+  onDown: (ev) ->
+    if ev.buttons == 1
+      ev.stopPropagation()
+      start = $V([ev.x, ev.y])
+      adj = @adjAtPoint(start)
+      if adj
+        @currentDrag = {start: start, adj: adj}
+        adj.startDrag()
+
+  onMove: (ev) ->
+    pos = $V([ev.x, ev.y])
+    if @currentDrag
+      @currentDrag.cur = pos
+      @currentDrag.adj.continueDrag(@currentDrag.cur.subtract(@currentDrag.start))
+
+  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 @adjs[adjId] or not makeAdjustable?
+      if not makeAdjustable?
+        delete @adjs[adjId]
+      else
+        adj = makeAdjustable()
+        @adjs[adjId] = adj
+        adj.id = adjId
+
+    # this is relying on makeCurveAdjusters always calling setAdjuster
+    # whenever the values may have changed
+    @debounce('adjsChanged', @adjsChanged.bind(@), 10)
+
+    window.debug_adjsCount = Object.keys(@adjs).length
+    
+  adjsChanged: ->
+    @updateAllCoords()
+
+  layoutCenters: ->
+    # push Adjustable centers around to avoid overlaps
+    # Todo: also don't overlap inlineattr boxes
+    @qt = d3.quadtree([], ((d)->d.e(1)), ((d)->d.e(2)))
+    @qt.extent([[0,0], [8000,8000]])
+    for _, adj of @adjs
+      desired = adj.getSuggestedCenter()
+      output = desired
+      for tries in [0...2]
+        nearest = @qt.find(output.e(1), output.e(2))
+        if nearest
+          dist = output.distanceFrom(nearest)
+          if dist < 60
+            away = output.subtract(nearest).toUnitVector()
+            toScreenCenter = $V([500,200]).subtract(output).toUnitVector()
+            output = output.add(away.x(60).add(toScreenCenter.x(10)))
+
+      if -50 < output.e(1) < 20 # mostly for zoom-left
+        output.setElements([
+          Math.max(20, output.e(1)),
+          output.e(2)])
+        
+      adj.centerOffset = output.subtract(adj.getTarget())
+      output.adj = adj
+      @qt.add(output)
+
+  adjAtPoint: (pt) ->
+    nearest = @qt.find(pt.e(1), pt.e(2))
+    if not nearest? or nearest.distanceFrom(pt) > 50
+      return null
+    return nearest?.adj
+
+  updateAllCoords: ->
+    @layoutCenters()
+    @redraw()
+
+  update: (ev) ->
+    @$.canvas.width = ev.target.offsetWidth
+    @$.canvas.height = ev.target.offsetHeight
+    @redraw()
+    
+  redraw: (adjs) ->
+    @ctx.clearRect(0, 0, @$.canvas.width, @$.canvas.height)
+
+    for adjId, adj of @adjs
+      ctr = adj.getCenter()
+      target = adj.getTarget()
+      @drawConnector(ctr, target)
+      
+      @drawAdjuster(adj.getDisplayValue(),
+                    Math.floor(ctr.e(1)) - 20, Math.floor(ctr.e(2)) - 10,
+                    Math.floor(ctr.e(1)) + 20, Math.floor(ctr.e(2)) + 10)
+
+
+  drawConnector: (ctr, target) ->
+    @ctx.strokeStyle = '#aaa'
+    @ctx.lineWidth = 2
+    @ctx.beginPath()
+    _line(@ctx, ctr, target)
+    @ctx.stroke()
+    
+  drawAdjuster: (label, x1, y1, x2, y2) ->
+    radius = 8
+    @ctx.fillStyle = 'rgba(255, 255, 0, 0.5)'
+    @ctx.beginPath()
+    _roundRect(@ctx, x1, y1, x2, y2, radius)
+    @ctx.fill()
+    
+    @ctx.strokeStyle = 'yellow'
+    @ctx.lineWidth = 3
+    @ctx.setLineDash([3, 3])
+    @ctx.beginPath()
+    _roundRect(@ctx, x1, y1, x2, y2, radius)
+    @ctx.stroke()
+    @ctx.setLineDash([])
+
+    @ctx.font = "12px sans"
+    @ctx.fillStyle = '#000'
+    @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
+    # connector
+
+  
+Polymer
   is: 'light9-timeline-diagram-layer'
   properties: {}
   ready: ->