Mercurial > code > home > repos > light9
changeset 1377:2df0dc79ce76
modernize polymer on index page. move live and timeline to subdirs
Ignore-this: 99a836d91ddbc8117abbe04788772409
author | Drew Perttula <drewp@bigasterisk.com> |
---|---|
date | Wed, 08 Jun 2016 06:02:07 +0000 |
parents | 8b3f7f5656f4 |
children | 64239c6651de |
files | light9/web/adjustable.coffee light9/web/index.html light9/web/light9-timeline-audio.html light9/web/live.html light9/web/live/index.html light9/web/timeline-elements.html light9/web/timeline.coffee light9/web/timeline.html light9/web/timeline/adjustable.coffee light9/web/timeline/index.html light9/web/timeline/light9-timeline-audio.html light9/web/timeline/timeline-elements.html light9/web/timeline/timeline.coffee makefile |
diffstat | 14 files changed, 1160 insertions(+), 1159 deletions(-) [+] |
line wrap: on
line diff
--- a/light9/web/adjustable.coffee Wed Jun 08 05:47:49 2016 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,117 +0,0 @@ -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 - # 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) -> - # config has: - # getTarget -> vec2 of current target position - # getSuggestedTargetOffset -> vec2 pixel offset from target - # emptyBox -> true if you want no value display - - getDisplayValue: () -> - return '' if @config.emptyBox - d3.format(".4g")(@_getValue()) - - getCenter: () -> # vec2 of pixels - @getTarget().add(@config.getSuggestedTargetOffset()) - - getTarget: () -> # vec2 of pixels - @config.getTarget() - - 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') - - startDrag: () -> - # override - - continueDrag: (pos) -> - # pos is vec2 of pixels relative to the drag start - - # override - - endDrag: () -> - # override - - _editorCoordinates: () -> # vec2 of mouse relative to <l9-t-editor> - ev = d3.event.sourceEvent - - 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] - - # 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]) - - setMouse(offsetParentPos) # for debugging - 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? - - _getValue: () -> - @config.observable() - - continueDrag: (pos) -> - # pos is vec2 of pixels relative to the drag start. - - epos = @_editorCoordinates() - newValue = @config.getValueForPos(epos) - @config.observable(newValue) - - subscribe: (onChange) -> - ko.computed => - @config.observable() - onChange() - -class window.AdjustableFloatObject extends Adjustable - constructor: (@config) -> - # config also has: - # graph - # subj - # pred - # ctx - # getTargetTransform(value) -> getTarget result for value - # getValueForPos - - super(@config) - - _getValue: () -> - # this is a big speedup- callers use _getValue about 4x as much as - # the graph changes and graph.floatValue is slow - @_currentValue - - getTarget: () -> - @config.getTargetTransform(@_getValue()) - - subscribe: (onChange) -> - @config.graph.subscribe @config.subj, @config.pred, null, (patch) => - @_currentValue = @config.graph.floatValue(@config.subj, @config.pred) - onChange() - - continueDrag: (pos) -> - # pos is vec2 of pixels relative to the drag start - - newValue = @config.getValueForPos(@_editorCoordinates()) - @config.graph.patchObject(@config.subj, @config.pred, - @config.graph.LiteralRoundedFloat(newValue), - @config.ctx)
--- a/light9/web/index.html Wed Jun 08 05:47:49 2016 +0000 +++ b/light9/web/index.html Wed Jun 08 06:02:07 2016 +0000 @@ -3,49 +3,48 @@ <head> <title></title> <meta charset="utf-8" /> + <script src="/lib/webcomponentsjs/webcomponents-lite.min.js"></script> <link rel="stylesheet" href="/style.css"> <link rel="import" href="/lib/polymer/polymer.html"> </head> <body> <dom-module id="service-button-row"> - <style> - div { - display: flex; - justify-content: space-between; - padding: 2px 3px; - } - div:hover { - background: gray; + <template> + <style> + div { + display: flex; + justify-content: space-between; + padding: 2px 3px; } - .left { - display: inline-block; - margin-right: 3px; - } - </style> - <template> + div:hover { + background: gray; + } + .left { + display: inline-block; + margin-right: 3px; + } + </style> <div> - <span class="left"><a class="big" href="{{withSlash}}">{{name}}</a></span> + <span class="left"><a class="big" href="{{name}}/">{{name}}</a></span> <span><button on-click="click">window</button></span> </div> </template> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "service-button-row", + properties: { + name: String, + }, + click: function() { + window.open(this.name + '/', '_blank', + 'scrollbars=1,resizable=1,titlebar=0,location=0'); + } + }); + }); + </script> </dom-module> - <script> - Polymer({ - is: "service-button-row", - properties: { - name: String, - withSlash: { type: String, computed: 'computeWithSlash(name)' }, - }, - computeWithSlash: function (name) { - return name + '/'; - }, - click: function() { - window.open(this.withSlash, '_blank', - 'scrollbars=1,resizable=1,titlebar=0,location=0'); - } - }); - </script> - + <h1>light9 home page</h1> <div style="float: left"> @@ -57,6 +56,8 @@ <service-button-row name="picamserve"></service-button-row> <service-button-row name="vidref"></service-button-row> <service-button-row name="."></service-button-row> + <service-button-row name="live"></service-button-row> + <service-button-row name="timeline"></service-button-row> </div> </body>
--- a/light9/web/light9-timeline-audio.html Wed Jun 08 05:47:49 2016 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ -<link rel="import" href="/lib/polymer/polymer.html"> - -<!-- (potentially-zoomed) spectrogram view --> -<dom-module id="light9-timeline-audio"> - <template> - <style> - :host { - display: block; - /* shouldn't be seen, but black is correct for 'no - audio'. Maybe loading stripes would be better */ - background: black; - } - div { - width: 100%; - height: 100%; - overflow: hidden; - } - img { - height: 100%; - position: relative; - } - </style> - <div> - <img src="{{imgSrc}}" - style="width: {{imgWidth}} ; left: {{imgLeft}}"> - </div> - </template> - <script> - log = console.log - Polymer({ - is: "light9-timeline-audio", - properties: { - graph: {type: Object, notify: true}, - show: {type: String, notify: true}, - song: {type: String, notify: true}, - zoom: {type: Object, notify: true}, - imgSrc: { type: String, notify: true}, - imgWidth: { computed: '_imgWidth(zoom)' }, - imgLeft: { computed: '_imgLeft(zoom)' }, - }, - observers: [ - 'setImgSrc(graph, show, song)' - ], - ready: function() { - this.zoom = {duration: 0}; - }, - setImgSrc: function() { - graph.runHandler(function() { - try { - var root = this.graph.stringValue( - this.graph.Uri(this.show), - this.graph.Uri(':spectrogramUrlRoot')); - } catch(e) { - return; - } - - try { - var filename = this.graph.stringValue( - this.song, this.graph.Uri(':songFilename')); - } catch(e) { - return; - } - - this.imgSrc = root + '/' + filename.replace('.wav', '.png'); - }.bind(this)); - }, - _imgWidth: function(zoom) { - if (!zoom.duration) { - return "100%"; - } - - return (100 / ((zoom.t2 - zoom.t1) / zoom.duration)) + "%"; - }, - _imgLeft: function(zoom) { - if (!zoom.duration) { - return "0"; - } - - var percentPerSec = 100 / (zoom.t2 - zoom.t1); - return (-percentPerSec * zoom.t1) + '%'; - }, - }); - </script> -</dom-module>
--- a/light9/web/live.html Wed Jun 08 05:47:49 2016 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +0,0 @@ -<!doctype html> -<html> - <head> - <title></title> - <meta charset="utf-8" /> - <link rel="stylesheet" href="/style.css"> - <script src="/lib/webcomponentsjs/webcomponents-lite.min.js"></script> - <link rel="import" href="/lib/polymer/polymer.html"> - <link rel="import" href="/lib/paper-slider/paper-slider.html"> - <link rel="import" href="/lib/iron-ajax/iron-ajax.html"> - </head> - <body> - - <dom-module id="light9-collector-client"> - <template> - <iron-ajax url="/collector/attrs" method="PUT" id="put"></iron-ajax> - <span>{{status}}</span> - </template> - <script> - HTMLImports.whenReady(function () { - Polymer({ - is: "light9-collector-client", - properties: { - status: {type: String, value: 'init'}, - clientSession: {value: ""+Date.now()}, - self: {type: Object, notify: true} - }, - ready: function() { - this.self = this; - var self = this; - - self.$.put.addEventListener( - 'error', function() { self.status = 'err'; }); - self.$.put.addEventListener( - 'request', function() { self.status = 'send'; }); - self.$.put.addEventListener( - 'response', function() { self.status = 'ok'; }); - // collector gives up on clients after 10sec - setInterval(self.ping.bind(self), 9000); - self.status = 'ready'; - }, - ping: function() { - this.send([]); - }, - send: function(settings) { - this.$.put.body = JSON.stringify({ - "settings": settings, - "client": window.location.href, - "clientSession": this.clientSession}); - this.$.put.generateRequest(); - } - }); - }); - </script> - </dom-module> - - <dom-module id="light9-live-control"> - <template> - <style> - paper-slider { width: 100%; } - </style> - - <template is="dom-if" if="{{useSlider}}"> - <paper-slider min="0" - max="{{max}}" - step=".01" - editable - content-type="application/json" - immediate-value="{{immediateSlider}}"></paper-slider> - </template> - <template is="dom-if" if="{{useColor}}"> - <input type="color" - id="col" - on-input="onPickedColor" - value="{{pickedColor}}"> - </template> - </template> - <script> - HTMLImports.whenReady(function () { - Polymer({ - is: "light9-live-control", - properties: { - client: {type: Object}, - device: {type: String}, - attr: {type: String}, - max: {type: Number, value: 1}, - immediateSlider: {notify: true, observer: "onChange"}, - useSlider: {type: Boolean, computed: '_useSlider(attr)'}, - useColor: {type: Boolean, computed: '_useColor(attr)'}, - pickedColor: {observer: 'onPickedColor'}, - }, - ready: function() { - }, - onPickedColor: function(ev) { - this.onChange(ev.target.value); - }, - onChange: function(lev) { - this.client.send([[this.device, this.attr, lev]]); - }, - _useSlider: function(attr) { - return attr != 'http://light9.bigasterisk.com/color'; - }, - _useColor: function(attr) { - return attr == 'http://light9.bigasterisk.com/color'; - }, - }); - }); - </script> - </dom-module> - - <dom-module id="light9-live-controls"> - <template> - <style> - </style> - <light9-collector-client self="{{client}}"></light9-collector-client> - <h1>live</h1> - - <ul> - <li> - <h2>colorstrip</h2> - <light9-live-control - client="{{client}}" - device="http://light9.bigasterisk.com/device/colorStrip" - attr="http://light9.bigasterisk.com/color" - ></light9-live-control> - </li> - <li> - <h2>moving</h2> - rx <light9-live-control - client="{{client}}" - device="http://light9.bigasterisk.com/device/moving1" - attr="http://light9.bigasterisk.com/rx" max="540" - ></light9-live-control> - ry <light9-live-control - client="{{client}}" - device="http://light9.bigasterisk.com/device/moving1" - attr="http://light9.bigasterisk.com/ry" max="240" - ></light9-live-control> - color <light9-live-control - client="{{client}}" - device="http://light9.bigasterisk.com/device/moving1" - attr="http://light9.bigasterisk.com/color" - ></light9-live-control> - </li> - </ul> - </template> - <script> - HTMLImports.whenReady(function () { - Polymer({ - is: "light9-live-controls", - properties: { - - }, - ready: function() { - - }, - }); - }); - </script> - </dom-module> - - <light9-live-controls></light9-live-controls> - - </body> -</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/live/index.html Wed Jun 08 06:02:07 2016 +0000 @@ -0,0 +1,165 @@ +<!doctype html> +<html> + <head> + <title></title> + <meta charset="utf-8" /> + <link rel="stylesheet" href="/style.css"> + <script src="/lib/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="/lib/polymer/polymer.html"> + <link rel="import" href="/lib/paper-slider/paper-slider.html"> + <link rel="import" href="/lib/iron-ajax/iron-ajax.html"> + </head> + <body> + + <dom-module id="light9-collector-client"> + <template> + <iron-ajax url="/collector/attrs" method="PUT" id="put"></iron-ajax> + <span>{{status}}</span> + </template> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "light9-collector-client", + properties: { + status: {type: String, value: 'init'}, + clientSession: {value: ""+Date.now()}, + self: {type: Object, notify: true} + }, + ready: function() { + this.self = this; + var self = this; + + self.$.put.addEventListener( + 'error', function() { self.status = 'err'; }); + self.$.put.addEventListener( + 'request', function() { self.status = 'send'; }); + self.$.put.addEventListener( + 'response', function() { self.status = 'ok'; }); + // collector gives up on clients after 10sec + setInterval(self.ping.bind(self), 9000); + self.status = 'ready'; + }, + ping: function() { + this.send([]); + }, + send: function(settings) { + this.$.put.body = JSON.stringify({ + "settings": settings, + "client": window.location.href, + "clientSession": this.clientSession}); + this.$.put.generateRequest(); + } + }); + }); + </script> + </dom-module> + + <dom-module id="light9-live-control"> + <template> + <style> + paper-slider { width: 100%; } + </style> + + <template is="dom-if" if="{{useSlider}}"> + <paper-slider min="0" + max="{{max}}" + step=".01" + editable + content-type="application/json" + immediate-value="{{immediateSlider}}"></paper-slider> + </template> + <template is="dom-if" if="{{useColor}}"> + <input type="color" + id="col" + on-input="onPickedColor" + value="{{pickedColor}}"> + </template> + </template> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "light9-live-control", + properties: { + client: {type: Object}, + device: {type: String}, + attr: {type: String}, + max: {type: Number, value: 1}, + immediateSlider: {notify: true, observer: "onChange"}, + useSlider: {type: Boolean, computed: '_useSlider(attr)'}, + useColor: {type: Boolean, computed: '_useColor(attr)'}, + pickedColor: {observer: 'onPickedColor'}, + }, + ready: function() { + }, + onPickedColor: function(ev) { + this.onChange(ev.target.value); + }, + onChange: function(lev) { + this.client.send([[this.device, this.attr, lev]]); + }, + _useSlider: function(attr) { + return attr != 'http://light9.bigasterisk.com/color'; + }, + _useColor: function(attr) { + return attr == 'http://light9.bigasterisk.com/color'; + }, + }); + }); + </script> + </dom-module> + + <dom-module id="light9-live-controls"> + <template> + <style> + </style> + <light9-collector-client self="{{client}}"></light9-collector-client> + <h1>live</h1> + + <ul> + <li> + <h2>colorstrip</h2> + <light9-live-control + client="{{client}}" + device="http://light9.bigasterisk.com/device/colorStrip" + attr="http://light9.bigasterisk.com/color" + ></light9-live-control> + </li> + <li> + <h2>moving</h2> + rx <light9-live-control + client="{{client}}" + device="http://light9.bigasterisk.com/device/moving1" + attr="http://light9.bigasterisk.com/rx" max="540" + ></light9-live-control> + ry <light9-live-control + client="{{client}}" + device="http://light9.bigasterisk.com/device/moving1" + attr="http://light9.bigasterisk.com/ry" max="240" + ></light9-live-control> + color <light9-live-control + client="{{client}}" + device="http://light9.bigasterisk.com/device/moving1" + attr="http://light9.bigasterisk.com/color" + ></light9-live-control> + </li> + </ul> + </template> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "light9-live-controls", + properties: { + + }, + ready: function() { + + }, + }); + }); + </script> + </dom-module> + + <light9-live-controls></light9-live-controls> + + </body> +</html>
--- a/light9/web/timeline-elements.html Wed Jun 08 05:47:49 2016 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,337 +0,0 @@ -<link rel="import" href="/lib/polymer/polymer.html"> -<link rel="import" href="light9-timeline-audio.html"> -<link rel="import" href="/lib/iron-resizable-behavior/iron-resizable-behavior.html"> -<link rel="import" href="/lib/iron-ajax/iron-ajax.html"> -<link rel="import" href="rdfdb-synced-graph.html"> -<link rel="import" href="light9-music.html"> - - -<!-- Whole editor- include this on your page. - Most coordinates are relative to this element. - --> -<dom-module id="light9-timeline-editor"> - <template> - <style> - :host { - background: #444; - display: flex; - flex-direction: column; - position: relative; - border: 1px solid black; - overflow: hidden; - } - light9-timeline-audio { - width: 100%; - height: 30px; - } - light9-timeline-time-zoomed { - flex-grow: 1; - } - light9-timeline-diagram-layer, light9-timeline-adjusters { - position: absolute; - left: 0; top: 0; right: 0; bottom: 0; - } - </style> - <div> - <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph> - <light9-music id="music" - song="{{song}}" - t="{{songTime}}" - playing="{{songPlaying}}" - duration="{{songDuration}}"></light9-music> - timeline editor: song [{{song}}] <button>unlink</button> - <label><input type="checkbox"> follow player song choice</label> - </div> - <div>[[debug]]</div> - <iron-ajax id="vidrefTime" url="/vidref/time" method="PUT" content-type="application/json"></iron-ajax> - <light9-timeline-audio id="audio" - graph="{{graph}}" - show="{{show}}" - song="{{song}}"></light9-timeline-audio> - <light9-timeline-time-zoomed id="zoomed" - graph="{{graph}}" - song="{{song}}" - show="{{show}}" - zoom="{{viewState.zoomSpec}}" - 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}}" - parent-adjs="{{adjs}}" - zoom-in-x="{{zoomInX}}"> - </light9-timeline-adjusters> - </template> - -</dom-module> - -<!-- the whole section that pans/zooms in time (most of the editor) --> -<dom-module id="light9-timeline-time-zoomed"> - <template> - <style> - :host { - display: flex; - height: 100%; - } - div { - display: flex; - flex-direction: column; - height: 100%; - } - light9-timeline-audio { - width: 100%; - height: 100px; - } - light9-timeline-graph-row { - flex-grow: 1; - } - </style> - <div> - <light9-timeline-time-axis id="time"></light9-timeline-time-axis> - <light9-timeline-audio id="audio" - graph="{{graph}}" - song="{{song}}" - show="{{show}}" - zoom="{{zoomFlattened}}"> - </light9-timeline-audio> - <template is="dom-repeat" items="{{rows}}"> - <light9-timeline-graph-row graph="{{graph}}" - zoom-in-x="{{zoomInX}}" - row-index="{{item}}" - > - </light9-timeline-graph-row> - </template> - </div> - </template> - <script> - Polymer({ - is: "light9-timeline-time-zoomed", - behaviors: [ - Polymer.IronResizableBehavior - ], - properties: { - graph: { type: Object, notify: true }, - song: { type: String, notify: true }, - zoomInX: { type: Object, notify: true }, - rows: {value: [0,1,2]}, - zoom: {type: Object, notify: true, observer: 'onZoom'}, - zoomFlattened: {type: Object, notify: true} - }, - onZoom: function() { - ko.computed(function() { - this.zoomFlattened = ko.toJS(this.zoom); - }.bind(this)); - } - }); - </script> -</dom-module> - - -<!-- - SVG or canvas that draws these: - - background grids - - zoom arcs - - notes - - annotations on notes - - connectors between adjusters and their targets - - This element is not responsible for any hit detection. Things you click (rows, - notes, adjusters, etc) are caught on their respective elements. (But is that - wrong in the case of notes?) - --> -<dom-module id="light9-timeline-diagram-layer"> - <template> - <style> - :host { - pointer-events: none; - } - svg { - width: 100%; - height: 100%; - pointer-events: none; - } - </style> - <svg xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" > - <g id="layer1"> - <text - xml:space="preserve" - style="font-size:13px;line-height:125%;font-family:'Verana Sans';-inkscape-font-specification:'Verana Sans';text-align:start;text-anchor:start;fill:#000000;" - x="-338.38403" - y="631.3988" - id="text4290" - sodipodi:linespacing="125%" ><tspan sodipodi:role="line" id="tspan4292" x="-338.38403" y="631.3988">spotchase</tspan></text> - <g id="timeAxis" transform="translate(0,40)"></g> - <g id="mouse"></g> - <g id="notes"></g> - <g id="connectors"></g> - <g id="cursor"> - <path id="cursor1" style="fill:none; stroke:#ff0303; stroke-width:1.5; stroke-linecap:butt;" /> - <path id="cursor2" style="fill:#9c0303;" /> - <path id="cursor3" style="fill:none; stroke:#ff0303; stroke-width:3; stroke-linecap:butt;" /> - </g> - </g> - </svg> - </template> -</dom-module> - - -<!-- seconds labels --> -<dom-module id="light9-timeline-time-axis"> - <template> - <style> - div { - width: 100%; - height: 31px; - } - </style> - <div></div> - </template> -</dom-module> - -<!-- one row of notes --> -<dom-module id="light9-timeline-graph-row"> - <template> - <style> - :host { - border-top: 1px solid black; - display: flex; - } - </style> - <template is="dom-repeat" items="{{noteUris}}"> - <light9-timeline-note graph="{{graph}}" - uri="{{item}}" - zoom-in-x="{{zoomInX}}"> - </light9-timeline-note> - </template> - </template> -</dom-module> - -<!-- One trapezoid note shape in a row. - This element has the right Y coords. - We compute X coords from the zoom setting. - diagram-layer draws the note body. --> -<dom-module id="light9-timeline-note"> - <template> - <style> - :host { - display: block; - background: green; - /* outline: 2px solid red; */ - } - </style> - <light9-timeline-note-inline-attrs></light9-timeline-note-inline-attrs> - </template> -</dom-module> - -<!-- All the adjusters you can edit or select. - This element manages their layout and suppresion. - Owns the selection. - Maybe includes selecting things that don't even have adjusters. - Maybe manages the layout of other labels and text too, to avoid overlaps. - --> -<dom-module id="light9-timeline-adjusters"> - <template> - <style> - :host { - pointer-events: none; /* restored on the individual adjusters */ - } - - </style> - <template is="dom-repeat" items="{{adjs}}"> - <light9-timeline-adjuster dia="{{dia}}" - adj="{{item}}"></light9-timeline-adjuster> - </template> - </template> -</dom-module> - -<!-- Yellow dotted handle that you can adjust to edit something. - Knows an attribute to edit and the true screen location, even if - parent <light9-timeline-adjusters> has offset us a bit to avoid a - text overlap. - Draws affordance arrows and a connector line if we're far - away from the point that we edit. - May grow to a bigger editor when you click or drag. - --> -<dom-module id="light9-timeline-adjuster"> - <template> - <style> - /* - #top {outline: 2px solid rgba(255, 0, 0, 0.25);} - table {outline: 2px solid rgba(0, 0, 255, 0.19);} - */ - - #top { - position: absolute; - display: inline-block; - } - table { - position: relative; - left: -50%; - top: -40px; /* percent had no effect */ - z-index: 2; - border-collapse: collapse; - } - td { - text-align: center; - font-size: 20px; - } - span { - font-size: 16px; - background: rgba(255, 255, 0, 0.5); - border: 3px yellow dotted; - border-radius: 8px; - padding: 5px; - - cursor: ew-resize; - -webkit-user-select: none; - pointer-events: all; - } - span.empty { - width: 30px; /* todo: supposed to fill the whole visible section*/ - height: 13px; - display: inline-block; - background: rgba(0,0,0,0); - } - </style> - <div id="top" style$="left: [[centerStyle.x]]px; top: [[centerStyle.y]]px"> - <table> - <tr><td></td><td style="visibility: hidden">↑</td><td></td></tr> - <tr><td>←</td><td><span id="label" class$="[[spanClass]]">[[displayValue]]</span></td><td>→</td></tr> - <tr><td></td><td style="visibility: hidden">↓</td><td></td></tr> - </table> - </div> - - </template> -</dom-module> - -<!-- sometimes we draw attrs within the shape of a note. --> -<dom-module id="light9-timeline-note-inline-attrs"> - <template> - - </template> - <script> - Polymer({ - is: "light9-timeline-note-inline-attrs", - properties: { - } - }); - </script> -</dom-module> - -<script src="/lib/sylvester/sylvester.js"></script> -<script src="/lib/d3/build/d3.min.js"></script> - -<!-- version with https://github.com/RubenVerborgh/N3.js/pull/61 --> -<script src="/lib/N3.js-pull61/browser/n3-browser.js"></script> -<!-- master version --> -<xxscript src="/lib/N3.js/browser/n3-browser.js"></script> - -<script src="/lib/knockout/dist/knockout.debug.js"></script> -<script src="/lib/shortcut/index.js"></script> -<script src="/lib/async/dist/async.js"></script> -<script src="adjustable.js"></script> -<script src="timeline.js"></script>
--- a/light9/web/timeline.coffee Wed Jun 08 05:47:49 2016 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,410 +0,0 @@ -log = console.log - -Polymer - is: 'light9-timeline-editor' - behaviors: [ Polymer.IronResizableBehavior ] - properties: - viewState: { type: Object } - debug: {type: String} - graph: {type: Object, notify: true} - song: {type: String, notify: true} - show: {value: 'http://light9.bigasterisk.com/show/dance2016'} - songTime: {type: Number, notify: true, observer: '_onSongTime'} - songDuration: {type: Number, notify: true, observer: '_onSongDuration'} - songPlaying: {type: Boolean, notify: true} - fullZoomX: {type: Object, notify: true} - zoomInX: {type: Object, notify: true} - width: ko.observable(1) - listeners: - 'iron-resize': '_onIronResize' - _onIronResize: -> - @width(@offsetWidth) - _onSongTime: (t) -> - @viewState.cursor.t(t) - _onSongDuration: (d) -> - @viewState.zoomSpec.duration(d) - - ready: -> - @viewState = - zoomSpec: - duration: ko.observable(100) - t1: ko.observable(0) # need validation to stay in bounds and not go too close - t2: ko.observable(100) - cursor: - t: ko.observable(20) - mouse: - pos: ko.observable($V([0,0])) - @fullZoomX = d3.scaleLinear() - @zoomInX = d3.scaleLinear() - attached: -> - - ko.computed(@zoomOrLayoutChanged.bind(@)).extend({rateLimit: 5}) - - @adjs = @makeZoomAdjs() - - @trackMouse() - @bindKeys() - @bindWheelZoom() - - zoomOrLayoutChanged: -> - @fullZoomX.domain([0, @viewState.zoomSpec.duration()]) - @fullZoomX.range([0, @width()]) - - # had trouble making notes update when this changes - zoomInX = d3.scaleLinear() - zoomInX.domain([@viewState.zoomSpec.t1(), @viewState.zoomSpec.t2()]) - zoomInX.range([0, @width()]) - @zoomInX = zoomInX - - # todo: these run a lot of work purely for a time change - @$.dia.setTimeAxis(@width(), @$.zoomed.$.audio.offsetTop, @zoomInX) - @$.adjusters.updateAllCoords() - - @$.dia.setCursor(@$.audio.offsetTop, @$.audio.offsetHeight, - @$.zoomed.$.time.offsetTop, - @$.zoomed.$.time.offsetHeight, - @fullZoomX, @zoomInX, @viewState.cursor) - - 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 = @getBoundingClientRect() - @viewState.mouse.pos($V([ev.pageX - @root.left, ev.pageY - @root.top])) - - @$.dia.setMouse(@viewState.mouse.pos()) - #@sendMouseToVidref() - - sendMouseToVidref: -> - now = Date.now() - if (!@$.vidrefLastSent? || @$.vidrefLastSent < now - 200) && !@songPlaying - @$.vidrefTime.body = {t: @latestMouseTime(), source: 'timeline'} - @$.vidrefTime.generateRequest() - @$.vidrefLastSent = now - - latestMouseTime: -> - @zoomInX.invert(@viewState.mouse.pos().e(1)) - - bindWheelZoom: -> - @$.zoomed.addEventListener 'mousewheel', (ev) => - zs = @viewState.zoomSpec - - center = @latestMouseTime() - left = center - zs.t1() - right = zs.t2() - center - scale = Math.pow(1.005, ev.deltaY) - - zs.t1(center - left * scale) - zs.t2(center + right * scale) - - animatedZoom: (newT1, newT2, secs) -> - fps = 30 - oldT1 = @viewState.zoomSpec.t1() - oldT2 = @viewState.zoomSpec.t2() - lastTime = 0 - for step in [0..secs * fps] - frac = step / (secs * fps) - do (frac) => - gotoStep = => - @viewState.zoomSpec.t1((1 - frac) * oldT1 + frac * newT1) - @viewState.zoomSpec.t2((1 - frac) * oldT2 + frac * newT2) - delay = frac * secs * 1000 - setTimeout(gotoStep, delay) - lastTime = delay - setTimeout(=> - @viewState.zoomSpec.t1(newT1) - @viewState.zoomSpec.t2(newT2) - , lastTime + 10) - - bindKeys: -> - shortcut.add "Ctrl+P", (ev) => - @$.music.seekPlayOrPause(@latestMouseTime()) - - zoomAnimSec = .1 - shortcut.add "Ctrl+Escape", => - @animatedZoom(0, @viewState.zoomSpec.duration(), zoomAnimSec) - shortcut.add "Shift+Escape", => - @animatedZoom(@songTime - 2, @viewState.zoomSpec.duration(), zoomAnimSec) - shortcut.add "Escape", => - zs = @viewState.zoomSpec - visSeconds = zs.t2() - zs.t1() - margin = visSeconds * .4 - # buggy: really needs t1/t2 to limit their ranges - if @songTime < zs.t1() or @songTime > zs.t2() - visSeconds * .6 - newCenter = @songTime + margin - @animatedZoom(newCenter - visSeconds / 2, - newCenter + visSeconds / 2, zoomAnimSec) - - makeZoomAdjs: -> - yMid = @$.audio.offsetTop + @$.audio.offsetHeight / 2 - dur = @viewState.zoomSpec.duration - - valForPos = (pos) => - x = pos.e(1) - t = @fullZoomX.invert(x) - left = new AdjustableFloatObservable({ - observable: @viewState.zoomSpec.t1, - getTarget: () => - $V([@fullZoomX(@viewState.zoomSpec.t1()), yMid]) - getSuggestedTargetOffset: () => $V([-50, 0]) - getValueForPos: valForPos - }) - - right = new AdjustableFloatObservable({ - observable: @viewState.zoomSpec.t2, - getTarget: () => - $V([@fullZoomX(@viewState.zoomSpec.t2()), yMid]) - getSuggestedTargetOffset: () => $V([50, 0]) - 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) - }) - - pan = new AdjustableFloatObservable({ - observable: panObs - emptyBox: true - # fullzoom is not right- the sides shouldn't be able to go - # offscreen - getTarget: () => $V([@fullZoomX(panObs()), yMid]) - getSuggestedTargetOffset: () => $V([0, 0]) - getValueForPos: valForPos - }) - - return [left, right, pan] - -Polymer - is: "light9-timeline-time-axis", - # for now since it's just one line calling dia, - # light9-timeline-editor does our drawing work. - -Polymer - is: 'light9-timeline-graph-row' - behaviors: [ Polymer.IronResizableBehavior ] - properties: - graph: { type: Object, notify: true } - zoomInX: { type: Object, notify: true } - noteUris: { type: Array, notify: true } - rowIndex: { type: Object, notify: true } - observers: [ - 'onGraph(graph)' - ] - onGraph: -> - @graph.runHandler(@update.bind(@)) - update: -> - U = (x) -> @graph.Uri(x) - - @noteUris = [] - for note in @graph.objects(@song, U(':note')) - @push('noteUris', note) - - -Polymer - is: 'light9-timeline-note' - behaviors: [ Polymer.IronResizableBehavior ] - listeners: 'iron-resize': 'update' - properties: - graph: { type: Object, notify: true } - uri: { type: String, notify: true } - zoomInX: { type: Object, notify: true } - observers: [ - 'onUri(graph, uri)' - 'update(graph, uri, zoomInX)' - ] - ready: -> - - onUri: -> - @graph.runHandler(@update.bind(@)) - - update: -> - # update our note DOM and SVG elements based on the graph - U = (x) -> @graph.Uri(x) - try - worldPts = [] # (song time, value) - - originTime = @graph.floatValue(@uri, U(':originTime')) - for curve in @graph.objects(@uri, U(':curve')) - if @graph.uriValue(curve, U(':attr')) == U(':strength') - - for pt in @graph.objects(curve, U(':point')) - - worldPts.push($V([ - originTime + @graph.floatValue(pt, U(':time')), - @graph.floatValue(pt, U(':value')) - ])) - worldPts.sort((a,b) -> a.e(1) > b.e(1)) - - screenPos = (pt) => - $V([@zoomInX(pt.e(1)), @offsetTop + (1 - pt.e(2)) * @offsetHeight]) - - setNote(@uri, (screenPos(pt) for pt in worldPts)) - - catch e - log("during resize of #{@uri}: #{@e}") - -Polymer - is: "light9-timeline-adjusters" - properties: - adjs: { type: Array }, # our computed list - parentAdjs: { type: Array }, # incoming requests - graph: { type: Object, notify: true } - song: { type: String, notify: true } - zoomInX: { type: Object, notify: true } - dia: { type: Object } - observers: [ - 'update(parentAdjs, graph, song, dia)' - 'onGraph(graph, song)' - ] - onGraph: (graph, song, zoomInX) -> - graph.runHandler(@update.bind(@)) - update: (parentAdjs, graph, song, dia) -> - U = (x) -> @graph.Uri(x) - @adjs = (@parentAdjs || []).slice() - for note in @graph.objects(@song, U(':note')) - @push('adjs', new AdjustableFloatObject({ - graph: @graph - subj: note - pred: @graph.Uri(':originTime') - ctx: @graph.Uri(@song) - getTargetTransform: (value) => $V([@zoomInX(value), 600]) - getValueForPos: (pos) => @zoomInX.invert(pos.e(1)) - getSuggestedTargetOffset: () => $V([0, -80]) - })) - - updateAllCoords: -> - for elem in @querySelectorAll('light9-timeline-adjuster') - elem.updateDisplay() - - -_adjusterSerial = 0 - -Polymer - is: 'light9-timeline-adjuster' - properties: - adj: - type: Object - notify: true - observer: 'onAdj' - target: - type: Object - notify: true - displayValue: - type: String - centerStyle: - type: Object - spanClass: - type: String - value: '' - - onAdj: (adj) -> - @adj.subscribe(@updateDisplay.bind(this)) - - updateDisplay: () -> - @spanClass = if @adj.config.emptyBox then 'empty' else '' - @displayValue = @adj.getDisplayValue() - center = @adj.getCenter() - target = @adj.getTarget() - return if isNaN(center.e(1)) - @centerStyle = {x: center.e(1), y: center.e(2)} - @dia?.setAdjusterConnector(@myId, center, target) - - attached: -> - @myId = 'adjuster-' + _adjusterSerial - _adjusterSerial += 1 - - drag = d3.drag() - sel = d3.select(@$.label) - sel.call(drag) - drag.subject((d) -> {x: @offsetLeft, y: @offsetTop}) - drag.container(@offsetParent) - drag.on('start', () => @adj?.startDrag()) - drag.on 'drag', () => - @adj?.continueDrag($V([d3.event.x, d3.event.y])) - drag.on('end', () => @adj?.endDrag()) - - -svgPathFromPoints = (pts) -> - out = '' - pts.forEach (p) -> - p = p.elements if p.elements # for vec2 - if out.length == 0 - out = 'M ' - else - out += 'L ' - out += '' + p[0] + ',' + p[1] + ' ' - return - out - -Polymer - is: 'light9-timeline-diagram-layer' - properties: {} - ready: -> - @elemById = {} - window.setNote = @setNote.bind(this) - window.setMouse = @setMouse.bind(this) - - setTimeAxis: (width, yTop, scale) -> - pxPerTick = 50 - axis = d3.axisTop(scale).ticks(width / pxPerTick) - d3.select(@$.timeAxis).attr('transform', 'translate(0,'+yTop+')').call(axis) - - setMouse: (pos) -> - elem = @getOrCreateElem('mouse-x', 'mouse', 'path', {style: "fill:none;stroke:#fff;stroke-width:0.5;"}) - elem.setAttribute('d', svgPathFromPoints([[-9999, pos.e(2)], [9999, pos.e(2)]])) - elem = @getOrCreateElem('mouse-y', 'mouse', 'path', {style: "fill:none;stroke:#fff;stroke-width:0.5;"}) - elem.setAttribute('d', svgPathFromPoints([[pos.e(1), -9999], [pos.e(1), 9999]])) - - getOrCreateElem: (uri, groupId, tag, attrs) -> - elem = @elemById[uri] - if !elem - elem = @elemById[uri] = document.createElementNS("http://www.w3.org/2000/svg", tag) - @$[groupId].appendChild(elem) - elem.setAttribute('id', uri) - for k,v of attrs - elem.setAttribute(k, v) - return elem - - setNote: (uri, curvePts) -> - elem = @getOrCreateElem(uri, 'notes', 'path', {style:"fill:#53774b; stroke:#000000; stroke-width:1.5;"}) - elem.setAttribute('d', svgPathFromPoints(curvePts)) - - setCursor: (y1, h1, y2, h2, fullZoomX, zoomInX, cursor) -> - @cursorPath = - top: @querySelector('#cursor1') - mid: @querySelector('#cursor2') - bot: @querySelector('#cursor3') - return if !@cursorPath.top - - xZoomedOut = fullZoomX(cursor.t()) - xZoomedIn = zoomInX(cursor.t()) - @cursorPath.top.setAttribute 'd', svgPathFromPoints([ - [xZoomedOut, y1] - [xZoomedOut, y1 + h1] - ]) - @cursorPath.mid.setAttribute 'd', svgPathFromPoints([ - [xZoomedIn + 2, y2 + h2] - [xZoomedIn - 2, y2 + h2] - [xZoomedOut - 1, y1 + h1] - [xZoomedOut + 1, y1 + h1] - ]) + ' Z' - @cursorPath.bot.setAttribute 'd', svgPathFromPoints([ - [xZoomedIn, y2 + h2] - [xZoomedIn, @offsetParent.offsetHeight] - ]) - - setAdjusterConnector: (uri, center, target) -> - elem = @getOrCreateElem(uri, 'connectors', 'path', {style: "fill:none;stroke:#d4d4d4;stroke-width:0.9282527;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.78475821, 2.78475821;stroke-dashoffset:0;"}) - elem.setAttribute('d', svgPathFromPoints([center, target]))
--- a/light9/web/timeline.html Wed Jun 08 05:47:49 2016 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -<!doctype html> -<html> - <head> - <title>timeline</title> - <meta charset="utf-8" /> - <script src="/lib/webcomponentsjs/webcomponents-lite.min.js"></script> - <link rel="import" href="timeline-elements.html" - </head> - <body> - <light9-timeline-editor style="width: 100%; height: 600px"> - </light9-timeline-editor> - </body> -</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/timeline/adjustable.coffee Wed Jun 08 06:02:07 2016 +0000 @@ -0,0 +1,117 @@ +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 + # 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) -> + # config has: + # getTarget -> vec2 of current target position + # getSuggestedTargetOffset -> vec2 pixel offset from target + # emptyBox -> true if you want no value display + + getDisplayValue: () -> + return '' if @config.emptyBox + d3.format(".4g")(@_getValue()) + + getCenter: () -> # vec2 of pixels + @getTarget().add(@config.getSuggestedTargetOffset()) + + getTarget: () -> # vec2 of pixels + @config.getTarget() + + 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') + + startDrag: () -> + # override + + continueDrag: (pos) -> + # pos is vec2 of pixels relative to the drag start + + # override + + endDrag: () -> + # override + + _editorCoordinates: () -> # vec2 of mouse relative to <l9-t-editor> + ev = d3.event.sourceEvent + + 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] + + # 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]) + + setMouse(offsetParentPos) # for debugging + 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? + + _getValue: () -> + @config.observable() + + continueDrag: (pos) -> + # pos is vec2 of pixels relative to the drag start. + + epos = @_editorCoordinates() + newValue = @config.getValueForPos(epos) + @config.observable(newValue) + + subscribe: (onChange) -> + ko.computed => + @config.observable() + onChange() + +class window.AdjustableFloatObject extends Adjustable + constructor: (@config) -> + # config also has: + # graph + # subj + # pred + # ctx + # getTargetTransform(value) -> getTarget result for value + # getValueForPos + + super(@config) + + _getValue: () -> + # this is a big speedup- callers use _getValue about 4x as much as + # the graph changes and graph.floatValue is slow + @_currentValue + + getTarget: () -> + @config.getTargetTransform(@_getValue()) + + subscribe: (onChange) -> + @config.graph.subscribe @config.subj, @config.pred, null, (patch) => + @_currentValue = @config.graph.floatValue(@config.subj, @config.pred) + onChange() + + continueDrag: (pos) -> + # pos is vec2 of pixels relative to the drag start + + newValue = @config.getValueForPos(@_editorCoordinates()) + @config.graph.patchObject(@config.subj, @config.pred, + @config.graph.LiteralRoundedFloat(newValue), + @config.ctx)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/timeline/index.html Wed Jun 08 06:02:07 2016 +0000 @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <title>timeline</title> + <meta charset="utf-8" /> + <script src="/lib/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="timeline-elements.html" + </head> + <body> + <light9-timeline-editor style="width: 100%; height: 600px"> + </light9-timeline-editor> + </body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/timeline/light9-timeline-audio.html Wed Jun 08 06:02:07 2016 +0000 @@ -0,0 +1,84 @@ +<link rel="import" href="/lib/polymer/polymer.html"> + +<!-- (potentially-zoomed) spectrogram view --> +<dom-module id="light9-timeline-audio"> + <template> + <style> + :host { + display: block; + /* shouldn't be seen, but black is correct for 'no + audio'. Maybe loading stripes would be better */ + background: black; + } + div { + width: 100%; + height: 100%; + overflow: hidden; + } + img { + height: 100%; + position: relative; + } + </style> + <div> + <img src="{{imgSrc}}" + style="width: {{imgWidth}} ; left: {{imgLeft}}"> + </div> + </template> + <script> + log = console.log + Polymer({ + is: "light9-timeline-audio", + properties: { + graph: {type: Object, notify: true}, + show: {type: String, notify: true}, + song: {type: String, notify: true}, + zoom: {type: Object, notify: true}, + imgSrc: { type: String, notify: true}, + imgWidth: { computed: '_imgWidth(zoom)' }, + imgLeft: { computed: '_imgLeft(zoom)' }, + }, + observers: [ + 'setImgSrc(graph, show, song)' + ], + ready: function() { + this.zoom = {duration: 0}; + }, + setImgSrc: function() { + graph.runHandler(function() { + try { + var root = this.graph.stringValue( + this.graph.Uri(this.show), + this.graph.Uri(':spectrogramUrlRoot')); + } catch(e) { + return; + } + + try { + var filename = this.graph.stringValue( + this.song, this.graph.Uri(':songFilename')); + } catch(e) { + return; + } + + this.imgSrc = root + '/' + filename.replace('.wav', '.png'); + }.bind(this)); + }, + _imgWidth: function(zoom) { + if (!zoom.duration) { + return "100%"; + } + + return (100 / ((zoom.t2 - zoom.t1) / zoom.duration)) + "%"; + }, + _imgLeft: function(zoom) { + if (!zoom.duration) { + return "0"; + } + + var percentPerSec = 100 / (zoom.t2 - zoom.t1); + return (-percentPerSec * zoom.t1) + '%'; + }, + }); + </script> +</dom-module>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/timeline/timeline-elements.html Wed Jun 08 06:02:07 2016 +0000 @@ -0,0 +1,337 @@ +<link rel="import" href="/lib/polymer/polymer.html"> +<link rel="import" href="/lib/iron-resizable-behavior/iron-resizable-behavior.html"> +<link rel="import" href="/lib/iron-ajax/iron-ajax.html"> +<link rel="import" href="light9-timeline-audio.html"> +<link rel="import" href="../rdfdb-synced-graph.html"> +<link rel="import" href="../light9-music.html"> + + +<!-- Whole editor- include this on your page. + Most coordinates are relative to this element. + --> +<dom-module id="light9-timeline-editor"> + <template> + <style> + :host { + background: #444; + display: flex; + flex-direction: column; + position: relative; + border: 1px solid black; + overflow: hidden; + } + light9-timeline-audio { + width: 100%; + height: 30px; + } + light9-timeline-time-zoomed { + flex-grow: 1; + } + light9-timeline-diagram-layer, light9-timeline-adjusters { + position: absolute; + left: 0; top: 0; right: 0; bottom: 0; + } + </style> + <div> + <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph> + <light9-music id="music" + song="{{song}}" + t="{{songTime}}" + playing="{{songPlaying}}" + duration="{{songDuration}}"></light9-music> + timeline editor: song [{{song}}] <button>unlink</button> + <label><input type="checkbox"> follow player song choice</label> + </div> + <div>[[debug]]</div> + <iron-ajax id="vidrefTime" url="/vidref/time" method="PUT" content-type="application/json"></iron-ajax> + <light9-timeline-audio id="audio" + graph="{{graph}}" + show="{{show}}" + song="{{song}}"></light9-timeline-audio> + <light9-timeline-time-zoomed id="zoomed" + graph="{{graph}}" + song="{{song}}" + show="{{show}}" + zoom="{{viewState.zoomSpec}}" + 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}}" + parent-adjs="{{adjs}}" + zoom-in-x="{{zoomInX}}"> + </light9-timeline-adjusters> + </template> + +</dom-module> + +<!-- the whole section that pans/zooms in time (most of the editor) --> +<dom-module id="light9-timeline-time-zoomed"> + <template> + <style> + :host { + display: flex; + height: 100%; + } + div { + display: flex; + flex-direction: column; + height: 100%; + } + light9-timeline-audio { + width: 100%; + height: 100px; + } + light9-timeline-graph-row { + flex-grow: 1; + } + </style> + <div> + <light9-timeline-time-axis id="time"></light9-timeline-time-axis> + <light9-timeline-audio id="audio" + graph="{{graph}}" + song="{{song}}" + show="{{show}}" + zoom="{{zoomFlattened}}"> + </light9-timeline-audio> + <template is="dom-repeat" items="{{rows}}"> + <light9-timeline-graph-row graph="{{graph}}" + zoom-in-x="{{zoomInX}}" + row-index="{{item}}" + > + </light9-timeline-graph-row> + </template> + </div> + </template> + <script> + Polymer({ + is: "light9-timeline-time-zoomed", + behaviors: [ + Polymer.IronResizableBehavior + ], + properties: { + graph: { type: Object, notify: true }, + song: { type: String, notify: true }, + zoomInX: { type: Object, notify: true }, + rows: {value: [0,1,2]}, + zoom: {type: Object, notify: true, observer: 'onZoom'}, + zoomFlattened: {type: Object, notify: true} + }, + onZoom: function() { + ko.computed(function() { + this.zoomFlattened = ko.toJS(this.zoom); + }.bind(this)); + } + }); + </script> +</dom-module> + + +<!-- + SVG or canvas that draws these: + - background grids + - zoom arcs + - notes + - annotations on notes + - connectors between adjusters and their targets + + This element is not responsible for any hit detection. Things you click (rows, + notes, adjusters, etc) are caught on their respective elements. (But is that + wrong in the case of notes?) + --> +<dom-module id="light9-timeline-diagram-layer"> + <template> + <style> + :host { + pointer-events: none; + } + svg { + width: 100%; + height: 100%; + pointer-events: none; + } + </style> + <svg xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" > + <g id="layer1"> + <text + xml:space="preserve" + style="font-size:13px;line-height:125%;font-family:'Verana Sans';-inkscape-font-specification:'Verana Sans';text-align:start;text-anchor:start;fill:#000000;" + x="-338.38403" + y="631.3988" + id="text4290" + sodipodi:linespacing="125%" ><tspan sodipodi:role="line" id="tspan4292" x="-338.38403" y="631.3988">spotchase</tspan></text> + <g id="timeAxis" transform="translate(0,40)"></g> + <g id="mouse"></g> + <g id="notes"></g> + <g id="connectors"></g> + <g id="cursor"> + <path id="cursor1" style="fill:none; stroke:#ff0303; stroke-width:1.5; stroke-linecap:butt;" /> + <path id="cursor2" style="fill:#9c0303;" /> + <path id="cursor3" style="fill:none; stroke:#ff0303; stroke-width:3; stroke-linecap:butt;" /> + </g> + </g> + </svg> + </template> +</dom-module> + + +<!-- seconds labels --> +<dom-module id="light9-timeline-time-axis"> + <template> + <style> + div { + width: 100%; + height: 31px; + } + </style> + <div></div> + </template> +</dom-module> + +<!-- one row of notes --> +<dom-module id="light9-timeline-graph-row"> + <template> + <style> + :host { + border-top: 1px solid black; + display: flex; + } + </style> + <template is="dom-repeat" items="{{noteUris}}"> + <light9-timeline-note graph="{{graph}}" + uri="{{item}}" + zoom-in-x="{{zoomInX}}"> + </light9-timeline-note> + </template> + </template> +</dom-module> + +<!-- One trapezoid note shape in a row. + This element has the right Y coords. + We compute X coords from the zoom setting. + diagram-layer draws the note body. --> +<dom-module id="light9-timeline-note"> + <template> + <style> + :host { + display: block; + background: green; + /* outline: 2px solid red; */ + } + </style> + <light9-timeline-note-inline-attrs></light9-timeline-note-inline-attrs> + </template> +</dom-module> + +<!-- All the adjusters you can edit or select. + This element manages their layout and suppresion. + Owns the selection. + Maybe includes selecting things that don't even have adjusters. + Maybe manages the layout of other labels and text too, to avoid overlaps. + --> +<dom-module id="light9-timeline-adjusters"> + <template> + <style> + :host { + pointer-events: none; /* restored on the individual adjusters */ + } + + </style> + <template is="dom-repeat" items="{{adjs}}"> + <light9-timeline-adjuster dia="{{dia}}" + adj="{{item}}"></light9-timeline-adjuster> + </template> + </template> +</dom-module> + +<!-- Yellow dotted handle that you can adjust to edit something. + Knows an attribute to edit and the true screen location, even if + parent <light9-timeline-adjusters> has offset us a bit to avoid a + text overlap. + Draws affordance arrows and a connector line if we're far + away from the point that we edit. + May grow to a bigger editor when you click or drag. + --> +<dom-module id="light9-timeline-adjuster"> + <template> + <style> + /* + #top {outline: 2px solid rgba(255, 0, 0, 0.25);} + table {outline: 2px solid rgba(0, 0, 255, 0.19);} + */ + + #top { + position: absolute; + display: inline-block; + } + table { + position: relative; + left: -50%; + top: -40px; /* percent had no effect */ + z-index: 2; + border-collapse: collapse; + } + td { + text-align: center; + font-size: 20px; + } + span { + font-size: 16px; + background: rgba(255, 255, 0, 0.5); + border: 3px yellow dotted; + border-radius: 8px; + padding: 5px; + + cursor: ew-resize; + -webkit-user-select: none; + pointer-events: all; + } + span.empty { + width: 30px; /* todo: supposed to fill the whole visible section*/ + height: 13px; + display: inline-block; + background: rgba(0,0,0,0); + } + </style> + <div id="top" style$="left: [[centerStyle.x]]px; top: [[centerStyle.y]]px"> + <table> + <tr><td></td><td style="visibility: hidden">↑</td><td></td></tr> + <tr><td>←</td><td><span id="label" class$="[[spanClass]]">[[displayValue]]</span></td><td>→</td></tr> + <tr><td></td><td style="visibility: hidden">↓</td><td></td></tr> + </table> + </div> + + </template> +</dom-module> + +<!-- sometimes we draw attrs within the shape of a note. --> +<dom-module id="light9-timeline-note-inline-attrs"> + <template> + + </template> + <script> + Polymer({ + is: "light9-timeline-note-inline-attrs", + properties: { + } + }); + </script> +</dom-module> + +<script src="/lib/sylvester/sylvester.js"></script> +<script src="/lib/d3/build/d3.min.js"></script> + +<!-- version with https://github.com/RubenVerborgh/N3.js/pull/61 --> +<script src="/lib/N3.js-pull61/browser/n3-browser.js"></script> +<!-- master version --> +<xxscript src="/lib/N3.js/browser/n3-browser.js"></script> + +<script src="/lib/knockout/dist/knockout.debug.js"></script> +<script src="/lib/shortcut/index.js"></script> +<script src="/lib/async/dist/async.js"></script> +<script src="adjustable.js"></script> +<script src="timeline.js"></script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/timeline/timeline.coffee Wed Jun 08 06:02:07 2016 +0000 @@ -0,0 +1,410 @@ +log = console.log + +Polymer + is: 'light9-timeline-editor' + behaviors: [ Polymer.IronResizableBehavior ] + properties: + viewState: { type: Object } + debug: {type: String} + graph: {type: Object, notify: true} + song: {type: String, notify: true} + show: {value: 'http://light9.bigasterisk.com/show/dance2016'} + songTime: {type: Number, notify: true, observer: '_onSongTime'} + songDuration: {type: Number, notify: true, observer: '_onSongDuration'} + songPlaying: {type: Boolean, notify: true} + fullZoomX: {type: Object, notify: true} + zoomInX: {type: Object, notify: true} + width: ko.observable(1) + listeners: + 'iron-resize': '_onIronResize' + _onIronResize: -> + @width(@offsetWidth) + _onSongTime: (t) -> + @viewState.cursor.t(t) + _onSongDuration: (d) -> + @viewState.zoomSpec.duration(d) + + ready: -> + @viewState = + zoomSpec: + duration: ko.observable(100) + t1: ko.observable(0) # need validation to stay in bounds and not go too close + t2: ko.observable(100) + cursor: + t: ko.observable(20) + mouse: + pos: ko.observable($V([0,0])) + @fullZoomX = d3.scaleLinear() + @zoomInX = d3.scaleLinear() + attached: -> + + ko.computed(@zoomOrLayoutChanged.bind(@)).extend({rateLimit: 5}) + + @adjs = @makeZoomAdjs() + + @trackMouse() + @bindKeys() + @bindWheelZoom() + + zoomOrLayoutChanged: -> + @fullZoomX.domain([0, @viewState.zoomSpec.duration()]) + @fullZoomX.range([0, @width()]) + + # had trouble making notes update when this changes + zoomInX = d3.scaleLinear() + zoomInX.domain([@viewState.zoomSpec.t1(), @viewState.zoomSpec.t2()]) + zoomInX.range([0, @width()]) + @zoomInX = zoomInX + + # todo: these run a lot of work purely for a time change + @$.dia.setTimeAxis(@width(), @$.zoomed.$.audio.offsetTop, @zoomInX) + @$.adjusters.updateAllCoords() + + @$.dia.setCursor(@$.audio.offsetTop, @$.audio.offsetHeight, + @$.zoomed.$.time.offsetTop, + @$.zoomed.$.time.offsetHeight, + @fullZoomX, @zoomInX, @viewState.cursor) + + 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 = @getBoundingClientRect() + @viewState.mouse.pos($V([ev.pageX - @root.left, ev.pageY - @root.top])) + + @$.dia.setMouse(@viewState.mouse.pos()) + #@sendMouseToVidref() + + sendMouseToVidref: -> + now = Date.now() + if (!@$.vidrefLastSent? || @$.vidrefLastSent < now - 200) && !@songPlaying + @$.vidrefTime.body = {t: @latestMouseTime(), source: 'timeline'} + @$.vidrefTime.generateRequest() + @$.vidrefLastSent = now + + latestMouseTime: -> + @zoomInX.invert(@viewState.mouse.pos().e(1)) + + bindWheelZoom: -> + @$.zoomed.addEventListener 'mousewheel', (ev) => + zs = @viewState.zoomSpec + + center = @latestMouseTime() + left = center - zs.t1() + right = zs.t2() - center + scale = Math.pow(1.005, ev.deltaY) + + zs.t1(center - left * scale) + zs.t2(center + right * scale) + + animatedZoom: (newT1, newT2, secs) -> + fps = 30 + oldT1 = @viewState.zoomSpec.t1() + oldT2 = @viewState.zoomSpec.t2() + lastTime = 0 + for step in [0..secs * fps] + frac = step / (secs * fps) + do (frac) => + gotoStep = => + @viewState.zoomSpec.t1((1 - frac) * oldT1 + frac * newT1) + @viewState.zoomSpec.t2((1 - frac) * oldT2 + frac * newT2) + delay = frac * secs * 1000 + setTimeout(gotoStep, delay) + lastTime = delay + setTimeout(=> + @viewState.zoomSpec.t1(newT1) + @viewState.zoomSpec.t2(newT2) + , lastTime + 10) + + bindKeys: -> + shortcut.add "Ctrl+P", (ev) => + @$.music.seekPlayOrPause(@latestMouseTime()) + + zoomAnimSec = .1 + shortcut.add "Ctrl+Escape", => + @animatedZoom(0, @viewState.zoomSpec.duration(), zoomAnimSec) + shortcut.add "Shift+Escape", => + @animatedZoom(@songTime - 2, @viewState.zoomSpec.duration(), zoomAnimSec) + shortcut.add "Escape", => + zs = @viewState.zoomSpec + visSeconds = zs.t2() - zs.t1() + margin = visSeconds * .4 + # buggy: really needs t1/t2 to limit their ranges + if @songTime < zs.t1() or @songTime > zs.t2() - visSeconds * .6 + newCenter = @songTime + margin + @animatedZoom(newCenter - visSeconds / 2, + newCenter + visSeconds / 2, zoomAnimSec) + + makeZoomAdjs: -> + yMid = @$.audio.offsetTop + @$.audio.offsetHeight / 2 + dur = @viewState.zoomSpec.duration + + valForPos = (pos) => + x = pos.e(1) + t = @fullZoomX.invert(x) + left = new AdjustableFloatObservable({ + observable: @viewState.zoomSpec.t1, + getTarget: () => + $V([@fullZoomX(@viewState.zoomSpec.t1()), yMid]) + getSuggestedTargetOffset: () => $V([-50, 0]) + getValueForPos: valForPos + }) + + right = new AdjustableFloatObservable({ + observable: @viewState.zoomSpec.t2, + getTarget: () => + $V([@fullZoomX(@viewState.zoomSpec.t2()), yMid]) + getSuggestedTargetOffset: () => $V([50, 0]) + 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) + }) + + pan = new AdjustableFloatObservable({ + observable: panObs + emptyBox: true + # fullzoom is not right- the sides shouldn't be able to go + # offscreen + getTarget: () => $V([@fullZoomX(panObs()), yMid]) + getSuggestedTargetOffset: () => $V([0, 0]) + getValueForPos: valForPos + }) + + return [left, right, pan] + +Polymer + is: "light9-timeline-time-axis", + # for now since it's just one line calling dia, + # light9-timeline-editor does our drawing work. + +Polymer + is: 'light9-timeline-graph-row' + behaviors: [ Polymer.IronResizableBehavior ] + properties: + graph: { type: Object, notify: true } + zoomInX: { type: Object, notify: true } + noteUris: { type: Array, notify: true } + rowIndex: { type: Object, notify: true } + observers: [ + 'onGraph(graph)' + ] + onGraph: -> + @graph.runHandler(@update.bind(@)) + update: -> + U = (x) -> @graph.Uri(x) + + @noteUris = [] + for note in @graph.objects(@song, U(':note')) + @push('noteUris', note) + + +Polymer + is: 'light9-timeline-note' + behaviors: [ Polymer.IronResizableBehavior ] + listeners: 'iron-resize': 'update' + properties: + graph: { type: Object, notify: true } + uri: { type: String, notify: true } + zoomInX: { type: Object, notify: true } + observers: [ + 'onUri(graph, uri)' + 'update(graph, uri, zoomInX)' + ] + ready: -> + + onUri: -> + @graph.runHandler(@update.bind(@)) + + update: -> + # update our note DOM and SVG elements based on the graph + U = (x) -> @graph.Uri(x) + try + worldPts = [] # (song time, value) + + originTime = @graph.floatValue(@uri, U(':originTime')) + for curve in @graph.objects(@uri, U(':curve')) + if @graph.uriValue(curve, U(':attr')) == U(':strength') + + for pt in @graph.objects(curve, U(':point')) + + worldPts.push($V([ + originTime + @graph.floatValue(pt, U(':time')), + @graph.floatValue(pt, U(':value')) + ])) + worldPts.sort((a,b) -> a.e(1) > b.e(1)) + + screenPos = (pt) => + $V([@zoomInX(pt.e(1)), @offsetTop + (1 - pt.e(2)) * @offsetHeight]) + + setNote(@uri, (screenPos(pt) for pt in worldPts)) + + catch e + log("during resize of #{@uri}: #{@e}") + +Polymer + is: "light9-timeline-adjusters" + properties: + adjs: { type: Array }, # our computed list + parentAdjs: { type: Array }, # incoming requests + graph: { type: Object, notify: true } + song: { type: String, notify: true } + zoomInX: { type: Object, notify: true } + dia: { type: Object } + observers: [ + 'update(parentAdjs, graph, song, dia)' + 'onGraph(graph, song)' + ] + onGraph: (graph, song, zoomInX) -> + graph.runHandler(@update.bind(@)) + update: (parentAdjs, graph, song, dia) -> + U = (x) -> @graph.Uri(x) + @adjs = (@parentAdjs || []).slice() + for note in @graph.objects(@song, U(':note')) + @push('adjs', new AdjustableFloatObject({ + graph: @graph + subj: note + pred: @graph.Uri(':originTime') + ctx: @graph.Uri(@song) + getTargetTransform: (value) => $V([@zoomInX(value), 600]) + getValueForPos: (pos) => @zoomInX.invert(pos.e(1)) + getSuggestedTargetOffset: () => $V([0, -80]) + })) + + updateAllCoords: -> + for elem in @querySelectorAll('light9-timeline-adjuster') + elem.updateDisplay() + + +_adjusterSerial = 0 + +Polymer + is: 'light9-timeline-adjuster' + properties: + adj: + type: Object + notify: true + observer: 'onAdj' + target: + type: Object + notify: true + displayValue: + type: String + centerStyle: + type: Object + spanClass: + type: String + value: '' + + onAdj: (adj) -> + @adj.subscribe(@updateDisplay.bind(this)) + + updateDisplay: () -> + @spanClass = if @adj.config.emptyBox then 'empty' else '' + @displayValue = @adj.getDisplayValue() + center = @adj.getCenter() + target = @adj.getTarget() + return if isNaN(center.e(1)) + @centerStyle = {x: center.e(1), y: center.e(2)} + @dia?.setAdjusterConnector(@myId, center, target) + + attached: -> + @myId = 'adjuster-' + _adjusterSerial + _adjusterSerial += 1 + + drag = d3.drag() + sel = d3.select(@$.label) + sel.call(drag) + drag.subject((d) -> {x: @offsetLeft, y: @offsetTop}) + drag.container(@offsetParent) + drag.on('start', () => @adj?.startDrag()) + drag.on 'drag', () => + @adj?.continueDrag($V([d3.event.x, d3.event.y])) + drag.on('end', () => @adj?.endDrag()) + + +svgPathFromPoints = (pts) -> + out = '' + pts.forEach (p) -> + p = p.elements if p.elements # for vec2 + if out.length == 0 + out = 'M ' + else + out += 'L ' + out += '' + p[0] + ',' + p[1] + ' ' + return + out + +Polymer + is: 'light9-timeline-diagram-layer' + properties: {} + ready: -> + @elemById = {} + window.setNote = @setNote.bind(this) + window.setMouse = @setMouse.bind(this) + + setTimeAxis: (width, yTop, scale) -> + pxPerTick = 50 + axis = d3.axisTop(scale).ticks(width / pxPerTick) + d3.select(@$.timeAxis).attr('transform', 'translate(0,'+yTop+')').call(axis) + + setMouse: (pos) -> + elem = @getOrCreateElem('mouse-x', 'mouse', 'path', {style: "fill:none;stroke:#fff;stroke-width:0.5;"}) + elem.setAttribute('d', svgPathFromPoints([[-9999, pos.e(2)], [9999, pos.e(2)]])) + elem = @getOrCreateElem('mouse-y', 'mouse', 'path', {style: "fill:none;stroke:#fff;stroke-width:0.5;"}) + elem.setAttribute('d', svgPathFromPoints([[pos.e(1), -9999], [pos.e(1), 9999]])) + + getOrCreateElem: (uri, groupId, tag, attrs) -> + elem = @elemById[uri] + if !elem + elem = @elemById[uri] = document.createElementNS("http://www.w3.org/2000/svg", tag) + @$[groupId].appendChild(elem) + elem.setAttribute('id', uri) + for k,v of attrs + elem.setAttribute(k, v) + return elem + + setNote: (uri, curvePts) -> + elem = @getOrCreateElem(uri, 'notes', 'path', {style:"fill:#53774b; stroke:#000000; stroke-width:1.5;"}) + elem.setAttribute('d', svgPathFromPoints(curvePts)) + + setCursor: (y1, h1, y2, h2, fullZoomX, zoomInX, cursor) -> + @cursorPath = + top: @querySelector('#cursor1') + mid: @querySelector('#cursor2') + bot: @querySelector('#cursor3') + return if !@cursorPath.top + + xZoomedOut = fullZoomX(cursor.t()) + xZoomedIn = zoomInX(cursor.t()) + @cursorPath.top.setAttribute 'd', svgPathFromPoints([ + [xZoomedOut, y1] + [xZoomedOut, y1 + h1] + ]) + @cursorPath.mid.setAttribute 'd', svgPathFromPoints([ + [xZoomedIn + 2, y2 + h2] + [xZoomedIn - 2, y2 + h2] + [xZoomedOut - 1, y1 + h1] + [xZoomedOut + 1, y1 + h1] + ]) + ' Z' + @cursorPath.bot.setAttribute 'd', svgPathFromPoints([ + [xZoomedIn, y2 + h2] + [xZoomedIn, @offsetParent.offsetHeight] + ]) + + setAdjusterConnector: (uri, center, target) -> + elem = @getOrCreateElem(uri, 'connectors', 'path', {style: "fill:none;stroke:#d4d4d4;stroke-width:0.9282527;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.78475821, 2.78475821;stroke-dashoffset:0;"}) + elem.setAttribute('d', svgPathFromPoints([center, target]))