From 7d4f7eb0457e549eda14f63f17da9443a08d62a0 Mon Sep 17 00:00:00 2001 From: tschw Date: Fri, 25 Sep 2015 04:24:08 +0200 Subject: [PATCH 01/17] Separate UI widgets from application-specific code --- src/layer_cabinet.js | 6 +++--- src/{ui => }/layer_view.js | 10 +++++----- src/timeliner.js | 4 ++-- src/{ => widget}/font.json | 0 src/{ => widget}/icon_button.js | 12 ++++++------ src/{ui => widget}/number.js | 4 ++-- src/{ui => widget}/scrollbar.js | 0 7 files changed, 18 insertions(+), 18 deletions(-) rename src/{ui => }/layer_view.js (95%) rename src/{ => widget}/font.json (100%) rename src/{ => widget}/icon_button.js (98%) rename src/{ui => widget}/number.js (95%) rename src/{ui => widget}/scrollbar.js (100%) diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index c25944a..9eac02a 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -1,10 +1,10 @@ var Settings = require('./settings'), - LayerUI = require('./ui/layer_view'), - IconButton = require('./icon_button'), + LayerUI = require('./layer_view'), + IconButton = require('./widget/icon_button'), style = require('./utils').style, Theme = require('./theme'), STORAGE_PREFIX = require('./utils').STORAGE_PREFIX, - NumberUI = require('./ui/number') + NumberUI = require('./widget/number') ; function LayerCabinet(data, dispatcher) { diff --git a/src/ui/layer_view.js b/src/layer_view.js similarity index 95% rename from src/ui/layer_view.js rename to src/layer_view.js index 99f42ac..0a44df8 100644 --- a/src/ui/layer_view.js +++ b/src/layer_view.js @@ -1,9 +1,9 @@ var - Theme = require('../theme'), - NumberUI = require('./number'), - Tweens = require('../tween'), - Settings = require('../settings'), - utils = require('../utils') + Theme = require('./theme'), + NumberUI = require('./widget/number'), + Tweens = require('./tween'), + Settings = require('./settings'), + utils = require('./utils') ; // TODO - tagged by index instead, work off layers. diff --git a/src/timeliner.js b/src/timeliner.js index 08da18e..09d6498 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -12,12 +12,12 @@ var undo = require('./undo'), LayerCabinet = require('./layer_cabinet'), TimelinePanel = require('./timeline_panel'), package_json = require('../package.json'), - IconButton = require('./icon_button'), + IconButton = require('./widget/icon_button'), style = utils.style, saveToFile = utils.saveToFile, openAs = utils.openAs, STORAGE_PREFIX = utils.STORAGE_PREFIX, - ScrollBar = require('./ui/scrollbar'), + ScrollBar = require('./widget/scrollbar'), DataStore = require('./datastore') ; diff --git a/src/font.json b/src/widget/font.json similarity index 100% rename from src/font.json rename to src/widget/font.json diff --git a/src/icon_button.js b/src/widget/icon_button.js similarity index 98% rename from src/icon_button.js rename to src/widget/icon_button.js index a821f1b..bcfeaf3 100644 --- a/src/icon_button.js +++ b/src/widget/icon_button.js @@ -1,6 +1,6 @@ var font = require('./font.json'), - Theme = require('./theme'), - style = require('./utils').style; + Theme = require('../theme'), + style = require('../utils').style; var dp; @@ -90,7 +90,7 @@ function IconButton(size, icon, tooltip, dp) { function clearLongHoldTimer() { clearTimeout(longHoldTimer); } - + button.addEventListener('mousedown', startHold); button.addEventListener('touchstart', startHold); button.addEventListener('mouseup', clearLongHoldTimer); @@ -122,7 +122,7 @@ function IconButton(size, icon, tooltip, dp) { button.addEventListener('mouseover', function() { // button.style.background = up; style(button, borders); - + ctx.fillStyle = Theme.d; // me.dropshadow = true; ctx.shadowColor = Theme.b; @@ -149,7 +149,7 @@ function IconButton(size, icon, tooltip, dp) { button.addEventListener('mouseout', function() { // ctx.fillStyle = Theme.c; - + button.style.background = normal; style(button, no_borders); @@ -233,4 +233,4 @@ IconButton.prototype.draw = function() { */ }; -module.exports = IconButton; \ No newline at end of file +module.exports = IconButton; diff --git a/src/ui/number.js b/src/widget/number.js similarity index 95% rename from src/ui/number.js rename to src/widget/number.js index 68635d6..010e2d6 100644 --- a/src/ui/number.js +++ b/src/widget/number.js @@ -87,8 +87,8 @@ function NumberUI(config) { }; this.paint = function() { - if (value) span.value = value.toFixed(precision); + if (value != null) span.value = value.toFixed(precision); }; } -module.exports = NumberUI; \ No newline at end of file +module.exports = NumberUI; diff --git a/src/ui/scrollbar.js b/src/widget/scrollbar.js similarity index 100% rename from src/ui/scrollbar.js rename to src/widget/scrollbar.js From 278798adc610b78d499515eeac047b4d5b1f76f9 Mon Sep 17 00:00:00 2001 From: tschw Date: Sun, 27 Sep 2015 15:51:52 +0200 Subject: [PATCH 02/17] Improve time scale control --- src/layer_cabinet.js | 20 ++++++------------- src/timeline_panel.js | 46 ++++++++++++++++++------------------------- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index 9eac02a..9f67da5 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -54,12 +54,11 @@ function LayerCabinet(data, dispatcher) { var range = document.createElement('input'); range.type = "range"; - range.min = 0; - range.max = 100; + range.value = 0; + range.min = -1; + range.max = +1; + range.step = 0.125; - range.value = convertTimeToPercent(Settings.time_scale); - - range.step = 0.01; style(range, { width: '70px' }); @@ -85,7 +84,7 @@ function LayerCabinet(data, dispatcher) { var time_options = { min: 0, - step: 0.01 + step: 0.125 }; var currentTime = new NumberUI(time_options); var totalTime = new NumberUI(time_options); @@ -314,15 +313,8 @@ function LayerCabinet(data, dispatcher) { } function changeRange() { - var t = range.value / range.max; - // 800px - 10 minutes - 100% - // 50% - 5 minutes - // 10* - // 100% - 60s / 1 second - - // TODO: scale time correctly - dispatcher.fire('update.scale', convertPercentToTime(t)); + dispatcher.fire('update.scale', Math.pow(100, -range.value) ); } var layer_uis = [], visible_layers = 0; diff --git a/src/timeline_panel.js b/src/timeline_panel.js index c18c02e..d8a7b67 100644 --- a/src/timeline_panel.js +++ b/src/timeline_panel.js @@ -22,27 +22,18 @@ var // drag current time // pointer on timescale -var subds, subd_type, subd1, subd2, subd3; +var tickMark1; +var tickMark2; +var tickMark3; function time_scaled() { - /* - * Subdivison LOD - * time_scale refers to number of pixels per unit - * Eg. 1 inch - 60s, 1 inch - 60fps, 1 inch - 6 mins - */ - - var a = time_scale / 60; // bigger wider, smaller narrower (40 - 80) - var b = time_scale / 20; // (1x or 2x a) - var c = time_scale / 5; // (4x or 5x a) - subds = [a, b, c, time_scale > 100 ? 'frames' : 'seconds']; + var div = 60; + + tickMark1 = time_scale / div; + tickMark2 = 2 * tickMark1; + tickMark3 = 10 * tickMark1; - // console.log(subds, subds[0] / time_scale, subds[1] / time_scale); - - subd1 = subds[0]; // big ticks / labels - subd2 = subds[1]; // medium ticks - subd3 = subds[2]; // small ticks - subd_type = subds[3]; } time_scaled(); @@ -324,6 +315,7 @@ function TimelinePanel(data, dispatcher) { function setTimeScale() { + var v = data.get('ui:timeScale').value; if (time_scale !== v) { time_scale = v; @@ -411,15 +403,15 @@ function TimelinePanel(data, dispatcher) { width = Settings.width, height = Settings.height; - var units = time_scale / subd1; + var units = time_scale / tickMark1; var offsetUnits = (frame_start * time_scale) % units; var count = (width - LEFT_GUTTER + offsetUnits) / units; - // console.log('time_scale', time_scale, 'subd1', subd1, 'units', units, 'offsetUnits', offsetUnits, frame_start); + // console.log('time_scale', time_scale, 'tickMark1', tickMark1, 'units', units, 'offsetUnits', offsetUnits, frame_start); // time_scale = pixels to 1 second (40) - // subd1 = marks per second (marks / s) + // tickMark1 = marks per second (marks / s) // units = pixels to every mark (40) // labels only @@ -437,11 +429,11 @@ function TimelinePanel(data, dispatcher) { ctx.textAlign = 'center'; var t = (i * units - offsetUnits) / time_scale + frame_start; - t = utils.format_friendly_seconds(t, subd_type); + t = utils.format_friendly_seconds(t); ctx.fillText(t, x, 38); } - units = time_scale / subd2; + units = time_scale / tickMark2; count = (width - LEFT_GUTTER + offsetUnits) / units; // marker lines - main @@ -454,8 +446,8 @@ function TimelinePanel(data, dispatcher) { ctx.stroke(); } - var mul = subd3 / subd2; - units = time_scale / subd3; + var mul = tickMark3 / tickMark2; + units = time_scale / tickMark3; count = (width - LEFT_GUTTER + offsetUnits) / units; // small ticks @@ -526,11 +518,11 @@ function TimelinePanel(data, dispatcher) { function x_to_time(x) { - var units = time_scale / subd3; + var units = time_scale / tickMark3; // return frame_start + (x - LEFT_GUTTER) / time_scale; - return frame_start + ((x - LEFT_GUTTER) / units | 0) / subd3; + return frame_start + ((x - LEFT_GUTTER) / units | 0) / tickMark3; } function time_to_x(s) { @@ -641,4 +633,4 @@ function TimelinePanel(data, dispatcher) { } -module.exports = TimelinePanel; \ No newline at end of file +module.exports = TimelinePanel; From 681cbf3e3da66d87a456b06b543a1b91660664c8 Mon Sep 17 00:00:00 2001 From: tschw Date: Sun, 27 Sep 2015 16:07:16 +0200 Subject: [PATCH 03/17] Adjust styling --- src/layer_cabinet.js | 46 +++++++++++---- src/layer_view.js | 3 +- src/settings.js | 4 +- src/timeliner.js | 130 +++++++++++++++++++++++++------------------ src/utils.js | 9 ++- 5 files changed, 121 insertions(+), 71 deletions(-) diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index 9f67da5..44c140a 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -9,7 +9,7 @@ var Settings = require('./settings'), function LayerCabinet(data, dispatcher) { var layer_store = data.get('layers'); - + var div = document.createElement('div'); var top = document.createElement('div'); @@ -31,23 +31,41 @@ function LayerCabinet(data, dispatcher) { var playing = false; + + var button_styles = { + width: '22px', + height: '22px', + padding: '2px' + }; + + var op_button_styles = { + width: '32px', + padding: '3px 4px 3px 4px' + }; + + var play_button = new IconButton(16, 'play', 'play', dispatcher); + style(play_button.dom, button_styles, { marginTop: '2px' } ); play_button.onClick(function(e) { e.preventDefault(); dispatcher.fire('controls.toggle_play'); }); var stop_button = new IconButton(16, 'stop', 'stop', dispatcher); + style(stop_button.dom, button_styles, { marginTop: '2px' } ); stop_button.onClick(function(e) { dispatcher.fire('controls.stop'); }); + var undo_button = new IconButton(16, 'undo', 'undo', dispatcher); + style(undo_button.dom, op_button_styles); undo_button.onClick(function() { dispatcher.fire('controls.undo'); }); var redo_button = new IconButton(16, 'repeat', 'redo', dispatcher); + style(redo_button.dom, op_button_styles); redo_button.onClick(function() { dispatcher.fire('controls.redo'); }); @@ -60,9 +78,11 @@ function LayerCabinet(data, dispatcher) { range.step = 0.125; style(range, { - width: '70px' + width: '90px', + margin: '0px', + marginLeft: '2px', + marginRight: '2px' }); - var draggingRange = 0; @@ -119,7 +139,7 @@ function LayerCabinet(data, dispatcher) { top.appendChild(play_button.dom); top.appendChild(stop_button.dom); top.appendChild(range); - + var operations_div = document.createElement('div'); style(operations_div, { @@ -129,8 +149,10 @@ function LayerCabinet(data, dispatcher) { top.appendChild(operations_div); // top.appendChild(document.createElement('br')); + // open _alt var file_open = new IconButton(16, 'folder_open_alt', 'Open', dispatcher); + style(file_open.dom, op_button_styles); operations_div.appendChild(file_open.dom); function populateOpen() { @@ -187,7 +209,7 @@ function LayerCabinet(data, dispatcher) { dispatcher.on('save:done', populateOpen); var dropdown = document.createElement('select'); - + style(dropdown, { position: 'absolute', // right: 0, @@ -234,13 +256,15 @@ function LayerCabinet(data, dispatcher) { // save var save = new IconButton(16, 'save', 'Save', dispatcher); + style(save.dom, op_button_styles); operations_div.appendChild(save.dom); save.onClick(function() { dispatcher.fire('save'); }); - // save as + // save as var save_as = new IconButton(16, 'paste', 'Save as', dispatcher); + style(save_as.dom, op_button_styles); operations_div.appendChild(save_as.dom); save_as.onClick(function() { dispatcher.fire('save_as'); @@ -248,12 +272,14 @@ function LayerCabinet(data, dispatcher) { // download json (export) var download_alt = new IconButton(16, 'download_alt', 'Download / Export JSON to file', dispatcher); + style(download_alt.dom, op_button_styles); operations_div.appendChild(download_alt.dom); download_alt.onClick(function() { dispatcher.fire('export'); }); var upload_alt = new IconButton(16, 'upload_alt', 'Load from file', dispatcher); + style(upload_alt.dom, op_button_styles); operations_div.appendChild(upload_alt.dom); upload_alt.onClick(function() { dispatcher.fire('openfile'); @@ -269,7 +295,7 @@ function LayerCabinet(data, dispatcher) { operations_div.appendChild(document.createElement('br')); // Cloud Download / Upload edit pencil - + /* // // show layer // var eye_open = new IconButton(16, 'eye_open', 'eye_open', dispatcher); @@ -315,7 +341,7 @@ function LayerCabinet(data, dispatcher) { function changeRange() { dispatcher.fire('update.scale', Math.pow(100, -range.value) ); - } + } var layer_uis = [], visible_layers = 0; var unused_layers = []; @@ -383,7 +409,7 @@ function LayerCabinet(data, dispatcher) { unused_layers.push(layer_uis.pop()); continue; } - + // console.log('yoz', states.get(i).value); layer_uis[i].setState(layers[i], layer_store.get(i)); // layer_uis[i].setState('layers'+':'+i); @@ -406,4 +432,4 @@ function LayerCabinet(data, dispatcher) { repaint(); } -module.exports = LayerCabinet; \ No newline at end of file +module.exports = LayerCabinet; diff --git a/src/layer_view.js b/src/layer_view.js index 0a44df8..7619dd3 100644 --- a/src/layer_view.js +++ b/src/layer_view.js @@ -28,10 +28,11 @@ function LayerView(layer, dispatcher) { dropdown.addEventListener('change', function(e) { dispatcher.fire('ease', layer, dropdown.value); }); + var height = (Settings.LINE_HEIGHT - 1); var keyframe_button = document.createElement('button'); keyframe_button.innerHTML = '◈'; // '♦' ◇ 9679 9670 9672 - keyframe_button.style.cssText = 'background: none; font-size: 12px; padding: 0px; font-family: monospace; float: right; width: 20px; border-style:none; outline: none;'; // border-style:inset; + keyframe_button.style.cssText = 'background: none; font-size: 12px; padding: 0px; font-family: monospace; float: right; width: 20px; height: ' + height + 'px; border-style:none; outline: none;'; // border-style:inset; keyframe_button.addEventListener('click', function(e) { console.log('clicked:keyframing...', state.get('_value').value); diff --git a/src/settings.js b/src/settings.js index ac3d3b0..bca4d37 100644 --- a/src/settings.js +++ b/src/settings.js @@ -4,11 +4,11 @@ var DEFAULT_TIME_SCALE = 60; // Dimensions module.exports = { LINE_HEIGHT: 26, - DIAMOND_SIZE: 12, + DIAMOND_SIZE: 10, MARKER_TRACK_HEIGHT: 60, width: 600, height: 200, TIMELINE_SCROLL_HEIGHT: 0, LEFT_PANE_WIDTH: 250, time_scale: DEFAULT_TIME_SCALE // number of pixels to 1 secon, -}; \ No newline at end of file +}; diff --git a/src/timeliner.js b/src/timeliner.js index 09d6498..1954f57 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -61,7 +61,7 @@ function Timeliner(target) { dispatcher.on('keyframe', function(layer, value) { var index = layers.indexOf(layer); - + var t = data.get('ui:currentTime').value; var v = utils.findTimeinLayer(layer, t); @@ -94,7 +94,7 @@ function Timeliner(target) { // dispatcher.fire('value.change', layer, me.value); dispatcher.on('value.change', function(layer, value, dont_save) { var t = data.get('ui:currentTime').value; - + var v = utils.findTimeinLayer(layer, t); console.log(v, 'value.change', layer, value, utils.format_friendly_seconds(t), typeof(v)); @@ -128,7 +128,7 @@ function Timeliner(target) { var start_play = null, played_from = 0; // requires some more tweaking - + dispatcher.on('controls.toggle_play', function() { if (start_play) { pausePlaying(); @@ -192,14 +192,14 @@ function Timeliner(target) { dispatcher.on('controls.undo', function() { var history = undo_manager.undo(); data.setJSONString(history.state); - + updateState(); }); dispatcher.on('controls.redo', function() { var history = undo_manager.redo(); data.setJSONString(history.state); - + updateState(); }); @@ -209,7 +209,7 @@ function Timeliner(target) { function paint() { requestAnimationFrame(paint); - + if (start_play) { var t = (performance.now() - start_play) / 1000; setCurrentTime(t); @@ -296,7 +296,7 @@ function Timeliner(target) { function load(o) { data.setJSON(o); - // + // if (data.getValue('ui') === undefined) { data.setValue('ui', { currentTime: 0, @@ -308,7 +308,7 @@ function Timeliner(target) { undo_manager.clear(); undo_manager.save(new UndoState(data, 'Loaded'), true); - + updateState(); } @@ -351,7 +351,7 @@ function Timeliner(target) { data.blank(); updateState(); }); - + dispatcher.on('openfile', function() { openAs(function(data) { // console.log('loaded ' + data); @@ -365,7 +365,7 @@ function Timeliner(target) { dispatcher.on('save', saveSimply); dispatcher.on('save_as', saveAs); - // Expose API + // Expose API this.save = save; this.load = load; @@ -375,34 +375,59 @@ function Timeliner(target) { var div = document.createElement('div'); div.style.cssText = 'position: absolute;'; - div.style.top = '16px'; + div.style.top = '22px'; var pane = document.createElement('div'); - + style(pane, { position: 'fixed', + top: '20px', + left: '20px', margin: 0, + border: '1px solid ' + Theme.a, padding: 0, - fontFamily: 'monospace', - zIndex: Z_INDEX, - border: '2px solid ' + Theme.a, - fontSize: '12px', - color: Theme.d, overflow: 'hidden', - top: '20px', - left: '20px' + backgroundColor: Theme.a, + color: Theme.d, + zIndex: Z_INDEX, + fontFamily: 'monospace', + fontSize: '12px' }); - pane.style.backgroundColor = Theme.a; + + var header_styles = { + position: 'absolute', + top: '0px', + width: '100%', + height: '22px', + lineHeight: '22px', + overflow: 'hidden' + }; + + var button_styles = { + width: '20px', + height: '20px', + padding: '2px', + marginRight: '2px' + }; var pane_title = document.createElement('div'); + style(pane_title, header_styles, { + borderBottom: '1px solid ' + Theme.b, + textAlign: 'center' + }); var title_bar = document.createElement('span'); pane_title.appendChild(title_bar); + title_bar.innerHTML = 'Timeliner ' + package_json.version; + pane_title.appendChild(title_bar); + + var top_right_bar = document.createElement('div'); + style(top_right_bar, header_styles, { + textAlign: 'right' + }); - var top_right_bar = document.createElement('span'); - top_right_bar.style.float = 'right'; pane_title.appendChild(top_right_bar); // resize minimize @@ -411,41 +436,30 @@ function Timeliner(target) { // resize full var resize_full = new IconButton(10, 'resize_full', 'maximize', dispatcher); + style(resize_full.dom, button_styles, { marginRight: '2px' }); top_right_bar.appendChild(resize_full.dom); - - style(pane_title, { - position: 'absolute', - width: '100%', - textAlign: 'left', - top: '0px', - height: '15px', - borderBottom: '1px solid ' + Theme.b, - overflow: 'hidden' - }); - - title_bar.innerHTML = 'Timeliner ' + package_json.version; var pane_status = document.createElement('div'); - style(pane_status, { + var footer_styles = { position: 'absolute', - height: '15px', - bottom: '0', width: '100%', + height: '22px', + lineHeight: '22px', + bottom: '0', // padding: '2px', background: Theme.a, - borderTop: '1px solid ' + Theme.b, fontSize: '11px' + }; + + style(pane_status, footer_styles, { + borderTop: '1px solid ' + Theme.b, }); pane.appendChild(div); pane.appendChild(pane_status); pane.appendChild(pane_title); - var button_styles = { - padding: '2px' - }; - var label_status = document.createElement('span'); label_status.textContent = 'hello!'; label_status.style.marginLeft = '10px'; @@ -461,6 +475,12 @@ function Timeliner(target) { dispatcher.on('status', this.setStatus); + var bottom_right = document.createElement('div'); + style(bottom_right, footer_styles, { + textAlign: 'right' + }); + + // var button_save = document.createElement('button'); // style(button_save, button_styles); // button_save.textContent = 'Save'; @@ -483,19 +503,17 @@ function Timeliner(target) { // bottom_right.appendChild(button_save); // bottom_right.appendChild(button_open); - var bottom_right = document.createElement('span'); - bottom_right.style.float = 'right'; - pane_status.appendChild(label_status); pane_status.appendChild(bottom_right); + /**/ // zoom in var zoom_in = new IconButton(12, 'zoom_in', 'zoom in', dispatcher); // zoom out var zoom_out = new IconButton(12, 'zoom_out', 'zoom out', dispatcher); // settings - var cog = new IconButton(12, 'cog', 'settings', dispatcher); + var cog = new IconButton(12, 'cog', 'settings', dispatcher); // bottom_right.appendChild(zoom_in.dom); // bottom_right.appendChild(zoom_out.dom); @@ -511,6 +529,7 @@ function Timeliner(target) { repaintAll(); }); + style(plus.dom, button_styles); bottom_right.appendChild(plus.dom); @@ -527,6 +546,7 @@ function Timeliner(target) { } } }); + style(trash.dom, button_styles, { marginRight: '2px' }); bottom_right.appendChild(trash.dom); @@ -599,7 +619,7 @@ function Timeliner(target) { document.addEventListener('keydown', function(e) { var play = e.keyCode == 32; // space - var enter = e.keyCode == 13; // + var enter = e.keyCode == 13; // var undo = e.metaKey && e.keyCode == 91 && !e.shiftKey; var active = document.activeElement; @@ -633,7 +653,7 @@ function Timeliner(target) { // }; // TODO: remove ugly hardcodes width -= 4; - height -= 32; + height -= 44; Settings.width = width - Settings.LEFT_PANE_WIDTH; Settings.height = height; @@ -643,7 +663,7 @@ function Timeliner(target) { scrollbar.setHeight(scrollable_height - 2); // scrollbar.setThumb - + style(scrollbar.dom, { top: Settings.MARKER_TRACK_HEIGHT + 'px', left: (width - 16) + 'px', @@ -695,7 +715,7 @@ function Timeliner(target) { var o = {}; for (var l = 0; l < layers.length; l++) { - var layer = layers[l]; + var layer = layers[l]; var m = utils.timeAtLayer(layer, t + u * interval); o[layer.name] = m.value; } @@ -797,7 +817,7 @@ function Timeliner(target) { document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); - // Touch events + // Touch events pane.addEventListener('touchstart', onTouchDown); document.addEventListener('touchmove', onTouchMove); document.addEventListener('touchend', onTouchEnd); @@ -809,7 +829,7 @@ function Timeliner(target) { } function onTouchMove(e) { - onMove(e.touches[0]); + onMove(e.touches[0]); } function onTouchEnd(e) { @@ -890,7 +910,7 @@ function Timeliner(target) { var currentWidth = Math.max(clicked.cx - e.clientX + clicked.w, minWidth); if (currentWidth > minWidth) { pane.style.width = currentWidth + 'px'; - pane.style.left = e.clientX + 'px'; + pane.style.left = e.clientX + 'px'; } } @@ -898,7 +918,7 @@ function Timeliner(target) { var currentHeight = Math.max(clicked.cy - e.clientY + clicked.h, minHeight); if (currentHeight > minHeight) { pane.style.height = currentHeight + 'px'; - pane.style.top = e.clientY + 'px'; + pane.style.top = e.clientY + 'px'; } } @@ -1063,4 +1083,4 @@ function Timeliner(target) { } -window.Timeliner = Timeliner; \ No newline at end of file +window.Timeliner = Timeliner; diff --git a/src/utils.js b/src/utils.js index fb0a3ec..1d4cfb9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -17,9 +17,12 @@ module.exports = { // Utils /**************************/ -function style(element, styles) { - for (var s in styles) { - element.style[s] = styles[s]; +function style(element, var_args) { + for (var i = 1; i < arguments.length; ++i) { + var styles = arguments[i]; + for (var s in styles) { + element.style[s] = styles[s]; + } } } From 9c3d8b885f1158c0f93b1847882d46e2e8fb4d86 Mon Sep 17 00:00:00 2001 From: tschw Date: Wed, 21 Oct 2015 03:13:43 +0200 Subject: [PATCH 04/17] GUI-only fork starts here --- README.md | 134 +- package.json | 25 +- screenshot.png | Bin 39384 -> 0 bytes src/datastore.js | 90 - src/handle_drag.js | 105 -- src/layer_cabinet.js | 82 +- src/layer_view.js | 107 +- src/save_format.js | 55 - src/timeline_panel.js | 166 +- src/timeliner.js | 325 ++-- src/tween.js | 24 - src/utils.js | 223 ++- src/widget/icon_button.js | 2 +- src/widget/number.js | 4 +- test.html | 44 +- test_ghosts.html | 112 -- timeliner.js | 3453 ------------------------------------- 17 files changed, 403 insertions(+), 4548 deletions(-) delete mode 100644 screenshot.png delete mode 100644 src/datastore.js delete mode 100644 src/handle_drag.js delete mode 100644 src/save_format.js delete mode 100644 src/tween.js delete mode 100644 test_ghosts.html delete mode 100644 timeliner.js diff --git a/README.md b/README.md index 42d7abf..dbd6bfb 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,6 @@ -# Timeliner +# Timeliner GUI -Timeliner is a graphical graphical tool to help create and prototype animations quickly. It is useful for adjusting variables and checking out how the effects changes over time with keyframing and easing/tweening functions. It may also have some similarities with the timeline component of adobe flash, after effects, edge animate or other animation software. +GUI-only fork of [Josh Koo's Timeliner](http://www.github.com/zz85/timeliner.git). -It is written in javascript and meant to work with different javascript libraries or webgl frameworks, in 1d, 2d, or 3d. It is built primary for myself, but feel free to send me suggestions or requests. +The class `Timeliner.Controller` provides a base / example of how to use this GUI with an existing animation system. -Follow [me](https://twitter.com/blurspline) on twitter for updates. - -## Demo - -# [Example](http://zz85.github.io/timeliner/test.html) - -[Video](https://plus.google.com/117614030945250277079/posts/BiWe8Z7nHdk?pid=6086039289973564578&oid=117614030945250277079) - -![screenshot](screenshot.png) - -## Another js timeline library? - -Below are some existing javascript timeline libraries which I think are pretty good. I decided to write mine partly to scratch my itch and partly to challange myself technically. There are challenges in writing one, but its nice to be in control of your own tools. - -1. [Timeline.js](https://github.com/vorg/timeline.js) by Marcin Ignac -2. [Keytime Editor](https://github.com/mattdesl/keytime-editor/) by Matt DesLauriers -3. [Frame.js](https://github.com/mrdoob/frame.js/) by Ricardo Cabello -(Side note: mrdoob's [talk on this](http://2013.jsconf.asia/blog/2013/11/8/jsconfasia-2013-mrdoob-ricardo-cabello-framejs) also showcase interesting editors used by the demoscene) -4. [TweenTime](https://github.com/idflood/TweenTime/) by idflood. - -I think the current version is much a work in progress. However Ben Schwarz says that a cat dies everytime code doesn't get publish during cssconf asia 2014, so I thought it would be a good idea to release this early. - -## Philosophy - -I wrote Timeliner to be as lightweight and embedable as possible. Styles, HTML, icons are all embeded in a single javascript file. This means it could work as an included script, bookmarklet, or part of a bigger project. I intent to have interoperablility with other controls tools like dat.gui or gui.js. - -## Usage - -Include the timeliner.js file. - -```js - -``` - -Load data by code, file upload or loading from saved localStorage. - -```js -// target is a "pojo" which gets updated when values change. -var target = { - name1: 1, - name2: 2, - name3: 3 -}; - -// initialize timeliner -var timeliner = new Timeliner(target); -timeliner.addLayer('name1'); -timeliner.addLayer('name2'); -timeliner.addLayer('name3'); -``` - -### Add a keyframe - -1. double click on the timeline - -or - -1. Select a time on the timeline -2. Click on the keyframe - -### Add a tween -1. Select time between 2 keyframes -2. Select easing type from the dropdown - -## Releases - -1.5.0 -- Fix package.json dependencies -- Easy way to move keyframes (reimplemented block dragging) - -1.4.0 -- Bug fix (insert keyframes should interpolate) -- ghosting / onioning skinning tweened values -- Icon and layout tweaks -- Basic time & vertical scrolling -- Simple Ghosting / Onion Skinning Support [Example](http://zz85.github.io/timeliner/test_ghosts.html) - -1.3.0 -- autosave -- load (localstorage, new, autosave, filesystem) -- save (export, localstorage, download) -- ui tweaks - -1.2.0 -- icons using extracted fontawesome data -- slightly npm-ify -- [window management](http://codepen.io/zz85/pen/gbOoVP) -- basic keyboard shortcuts -- basic hdpi -- basic touch support - -1.1.0 -- undo / redo (basic) - -1.0.0 -- slider time scale (basic) -- fix positioning mouse events -- basic play toggled with pause button -- basic hook playback to target object -- basic playback and pause -- semi-allow layer panel to repaint on data change -- show current easing of layers -- update tween props on insert -- show tween values on cursor movement -- edit tween (basic) -- insert tween (basic) -- drag keyframe -- insert keyframe on value adjust -- adjust value -- remove keyframe -- insert keyframe -- adjust values (basic) - -## TODO -- slider units -- usability improvements -- better marking time when scaling time -- better keyboard shortcuts -- move tween blocks -- custom context / popup menu -- attempt virtual-dom / v.rendering -- consider immutable js or localstorage for undo stack -- curve editor -- graph editor -- support audio -- support guestures -- remote control -- a whole ton more \ No newline at end of file diff --git a/package.json b/package.json index 76c9671..499abeb 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,35 @@ { - "name": "timeliner", - "version": "1.4.0", - "description": "simple js animation timeline library", + "name": "timeliner_gui", + "version": "0.0.1", + "description": "Timeliner GUI", "main": "timeliner.js", "scripts": { - "build": "browserify src/*.js --full-path=false -o timeliner.js", - "mini": "browserify src/*.js -g uglifyify --full-path=false -o timeliner.min.js", - "watch": "watchify src/*.js -o timeliner.js -v", + "build": "browserify src/*.js --full-path=false -o timeliner_gui.js", + "mini": "browserify src/*.js -g uglifyify --full-path=false -o timeliner_gui.min.js", + "watch": "watchify src/*.js -o timeliner_gui.js -v", "start": "npm run watch", "test": "echo \"Error: no tests :(\" && exit 1" }, "repository": { "type": "git", - "url": "https://github.com/zz85/timeliner.git" + "url": "https://github.com/tschw/timeliner.git" }, "keywords": [ "timeline", "animation", "keyframe", - "tween", - "ease", "controls", "gui" ], - "author": "joshua koo", + "author": "tschw (the fork)", + "contributors": [ + "Joshua 'zz85' Koo (original author)" + ], "license": "MIT", "bugs": { - "url": "https://github.com/zz85/timeliner/issues" + "url": "https://github.com/tschw/timeliner/issues" }, - "homepage": "https://github.com/zz85/timeliner", + "homepage": "https://github.com/tschw/timeliner", "devDependencies": { "do.js": "^1.0.0", "uglifyify": "^2.6.0" diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 233933d78b3b62cbaede92e94f11111808f07156..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39384 zcmeFZRX|nG7e7iIX*`EUx|Ig$kOnE~l@-;J01 z`iK3nnLV*)&6*XTHNz)4nYXA&ct}uCP^c2(A_`DY04yjd=r9BrNXfUurCG=ypUi}X zsKS z;L?Nzs6MRm9)|AO%_;UX*3|$Yd62%eyoEbmjCyV)(G=LPtyY^7JS}s(=e3h*XU9@* z&yL2{MHC1&ve7Oo^kEYJkX%-1w1!?$u=`QMQ<9ENI_-?yt}PRY&cTI4@VK5@IoqX( zh`TonSV2O+b9g>%%fQ*xrcO`e7A%1lGDIBYlcf|Lun~k=srtkjC*{;P&aO?Lp)#vR zew5D~jv1|sAS?zz*UzhYdjs?7ON-4ub}&_lyb)V`LHtES;R`Y;)d}^efOW24q8>;;%oR z=@4u`l^0L3u5iI|zm31}GEz^oaL;hRl)Htda4}oSl4^>9EK0hWlA5F1J85nM8!HAq zLmPb~23IRv$f7|(@w#$DUagEA^+;T;EUg{5UEh%XRf8My{juvmo)ZWRF z2;0~jk+3naGBA!Yl4v;bUn3-7F zc>ikffBW=n$lsc(|7yy@#qoQ~-@g3Ql9%!441Sx@-|YIU6v8h)Bwohc3ccTq>WMko{KPN8K8%R33kwj}_lFY10Jwo-3uc69gXqbYXrG-cRDDpf z>C(AiRF-ctkJ6|Qo+4ApaxHshPHih?S^bO#MhIYyh|WwU2t$$VPeHNnd(U6_k|$~3 zn!U2|=WmAF4U&oRP{&o13Hq=}%a(_Xh=)gGryoGv=Z}XK6f_YTsQy2t;sR6x4{cX} zRuvbZh2DiP$w`d(qX%)HTi+un#K_+t9~GDh;Cl&X;y;U_E?}3T(MkSzN&p4KRnhu< z|5LmR-vUb^0Q39v9#M|pl(Nh7_cF-)1ke-yf5@*w&jgQ12H*ToaRKr5^FPRhLNuim zc*u;yJd66BdML<1$Ao{9i9RCfb1O(oy-EHD^^k#9c>W}lA~O!^!cRqE;O!sOQ$QH% z@_!imYnlHKL;sJ2{;`+-e{A_SRG5Rw$pd2VeKFsWKFJ5r`{9^o(%*VNX7k0-4oZBT z`_Qo0t|%%}Ra<+P%(VCNw<;vQBP38;ez9>_@*1=axU;7lqb2t*s zRd+h_1kxN1nTq~RcL6>M+#PwB$@8}Ht%uf~$!v#RMxKd6DkfeAW<2l`+Lz7ZUd z_&06+f6`X|awY!kTd8f(4cWGLxkn2xOA!g4O{%0G)?p^Z(GvBLO}f zLxBuf0V10JFQ2s`pW`GIB5(idJ^at!ePv*{xrw=HtMfjvGW^B3oNwBBc09{jaA}0zw=_Kj;LJVnef|UySd(wR^WFEDN>_KwZ_y7ii1EYi zw?pdV*x!01@k9g!oP9eyxmFjIF>Fx zEvMz5(l(c#CMwHyg;wZ}b#!LYf8)2$9pJ<0jqbD*e*r?-TK>U+lb!hsZVDDQWy!(8 z$Q*Kp(|T`4no%ihPUR)_Bsc^;zb6+fye80hD0ZYCQL+jxS1Xmjuww)}|dF71^Fi^|#T%jQV; zG_GxAubU;RjQMY$`K%%UO3uE8Qz-c#WfwJ%0i~Yys+%xTIMWDFbq?#cdc*p{;^VQo z78mTb>vI&dHU(tbj$nVBfg~75c#l7uLLH0XU^tA^={!@#$#XDDf`|H<1cQx zO6zt8p>Zs^8f5T!$q8#{%%A+|=CIx;kagEA`Tl|Qr(6|ADEX`2eoH0sF0n4)S|$^H znM1Lbla-g(l^iOw{W_X){(Su_tZ>d33?t{o)7z^39|^?wYNbX)24w@dcvf-LLp1dB z*DGJOj0aw3 z%lxSVi^I#t)twji-Ad}btey><=?Pbxe5ZZFT%VF$=+ci@`VYC88`z>aeks|^w=gC{ zsa($cGdU75kt}3nWDE?Y-DK{@2v}Qwgcys;khuu5m)7qmW$>$08S!4u1!s*V*w67^ zRpaDSI|Gv{9d|B-JIxM0l>qr$OMa+)KQ|#x8bu2Qr6HV*%BjDnRDZoH8ksD`@AX5K z@d%r15Mz@e%^~cf!80rCtLb?Eu;)IL1a$-cvY%UfcK0D}49V`7HRCw)PAa7w2EVFt>r`CzPUeOXr?5~c$14n? z>a^Gm#}ImfN?fig_kT}gC$LlVhGX;lyLvatbsg+_C?cbczD};%usrcAfP4LY|v-^d8 zSnSOgnCHmiy}tFr()Y^m&naD+ z&FF%-VJ?^e6*~8h zKciKlR5seYWg#F>Pdtj(StjYVaOi^v>q;lfm7_{{bZ&-qv(2Md>->q?=&BYEK`Mp4 zR15cOB8`jd=*B#w_ghiT_v=SMAhLo=~+uvp>6RjS>ozijLZ6?9B5tNBEM*?8V+ z{~?Z!=gDqS8Tj}cJQfy~waO2@%gYGd_Ev z#u{)2|FVLIc02E9Q-|cu8?GsCk&-FH1mdld;B4dW7>g1Ir^`cb_Lp0I6WgYHp)X6| zg8EW`tq<#G^Xul$HdTWyyJPXPvVG49?#Fm3-aRKzKtIx{zWxTUb9js5ZrYqXHp{Y` z1+w`~(LM)&c74jtI(ad%vBB4?frEpC@Ia!8EbTC5f75m0;pRZw-NnW&ODzZ2TwElM=VVS+mx{sFljo3r_hBNphYQNO1Axz=f z?}19V-kZ8;S;eNn1<8SGLwBRqSxeXjygb?$y2izFs+H1as^)I6aR`%KZ0J)wlx4CP zk<{vvm`zR=7OJ+tU^J?<>S=xLQ)z7SiKpvfA|K%XVJ?!#cADiN6hlw`^qc4M22uAR zWv{f6fsyg(c(r2$cyV#zK2=&#YU{*TWnw6@;Fd2YzH1q^i@`WXsHoFGIHs!3GV|dJ z51xG;f>!3thyrEY0Us$mx%eil!)*6FcX&kq`%|jnL>`xLt$V)${M-gE?zuzDOmdyK zTpR9j7az(+DxtIQX!aVJ+c!3zBM-K*k%;eXrM>w+}8e*au7n8(w-belA*w#XH-nn-vAr@_%N=^0%)X$v8xiWo>YB7Ef zfk^&#%R$cp@pS614SPWT+$1Y{`!f|$QBkO{NENWZo3{DvjF_H78Qn~AP zf;LLcgymZbRyHumv4?S!XBKRCA54g3Qt$du_F9Zl=pS!*tWm=M{_wqDbz8yH5A0twCf>)7lr%r|_ ztPc8+j0OW~DrdXz0b{ZpkGmTYTfA#yJ`43l*!>qSR?QnTd0VPQDn4=ZdynPJm`*); zJ)lX8MIj+Aho=x1E$UrUzKc;=Vz6-GQMidtiv9+Ih%!B560G97MqI{__puQr5Pf=~ zb+?pOu`5ooQH8HbrI|VyIQ{F;SD^v6-lUFn@l59M50BLnYC#@?a8F@@nww*zn7nF^#L z1f`%TRbjULxxAIc!M?SUFLz|Jl6kzn+I;#7ber*vMeCH_`snm#@rqLj01ElxJd?v& z_nC$@h26F+VV07$tol=>(Rtd83NT@9B9DqLmPK3khj3U9xeTts>|%6Uj({Hhc~T8C zt$Ah52nkjos}w9fc9CmaHEzf=#e8=rC_>B&mjmr!qfAuag2jvB=N}0Y*#dx=7F)f&2c(uO47#^6ybpa{kDIJCB)Ew! zDHiUgHJ1QwbHe2khTS@5ti%v6>pzeWWjM=$Qm$j{g zoVHLxLJm7aq1v~^+n(DZJ2H}6*Jdus&a^hOON;NP(d~=02a{l}eJpoAVJv>6h)tW( zm^yfun_#r4>ceZ)$TnQBU~c%~S2VCdDW|0smL!GX5fl~%QQ2=iF6k~kZEMjPA+~dc zxlMNzNaPavOyV~uT25=seYX#>B;+ezjvM!?< zjDdzltu!LcxQX3GCJf+)bav6uSbnW96rw*8G&gY*IC=u8O>X$?_rbmi_zb~orK4Z? zG2p5r{<0#GKC6f$H<{h!+29dBvc2CSNG5hjXXoyG6F~PU!#BTo21g~Z8Psb=*Sw6jLW1eptFaDhBi7ep`@kd zwL3vAaCg{v)Ii&__j|uYKlu%7x`=fW2Ue@I)qD4DuvDuN2^l#hC8g0qdwK6_$-_od zGu6ZiZ1{$kc2A{{RQRK&)1)#pGqb$B{KM^ql$4Z(jSYNuS<5X_u>Se>XjfO4&(z4s zic0l>^e>%3F+vZSTZ8D2OH`d5cw88pws#zh{?I2&%^neiJX13>+ci$s*vD$Tb+h4t zv#ZW^x4T8FNxsv1?f!^z&yRTpc!M%9Uds66tYtjizce52>G3(IVPw22r^NnUg38e# z^AQuu@b)So_V@KG>HdWA5)zC1aS7otXpVFr>lQs*TMgvD*Mo#lRI=&3p4Yab>;;s7 z^Wj8h1BTZ7Qy|~<%13-_AAhJ?-Mv=_zgbN`NY{eX@N4QBDsqX{$q7Fn<-hWE5*m0slvpc)qnFR09+83 zIC}el$AInDC}~@RDIAbcNQ$9eeH_cjUt=b3<(sEyg8bZT9}W0;mLTl5+|w!eUl{p8Tabz>#uPm zY*)SRF4OqD=Bncay>OsOK(J~Ki0SZ-B6qR?H3QK+G;%S|lP(;=cRt^LId}rP2)rjE zx^t+GMF-995k%#NF$}d$%=|b|(MQshFLe9h-Cr5fM?% zZqj;8-UTCOz?emdj|5Iv%*CytF)9-`16kQ~P4 zTQRW)qA&XT=%;FQ*oI;={ zj}-qRX2+zE1@MIBIKCA;r{&{&yjyXaN+~>n>^ibMLvvu zxKoVH=nU^y>B%vCKKi?wnwfn|_nFbW52h{61T&qU zWn2BdbF%~?ie>rD+t1tZqr%e$cFXc3+AM~!InO<*5_@t)`GeX_<@sZw(24FvuKjJ@ zO>1Mg!i1sLpy8>SzF|DW^6jD1(87AGchGD}O{X{H~k4j@AInwW$rvd!Uq{~dyx+&;-a3T|5* zLL8wUw+fc)gAb`as7uy>GidMyD94xi4cz&fZRZzFLAZ4~YU(6TJF>pDiIR+E@Lh>% zMfb;8>?9^U=q!R`0NM@IgU^knSY_npzf>0d+}8=RtdY0SIval04=Vm91L<$@^?+*h zk|DTl+r&TVVRCkq;>`q0$G%1DiTm<}#-;yt>nMUQ(cI11mdpOkYX7q7)W2@bs!V2c zLvlaE%{e7PD7XzXd~I!QvC+l*Y>8=%G$c4}|gT7}(qKb-&_5c|Hm{Ji|%zk=^E6>{O)_bC0;h*D9iQ|d=qy4f3p;Gdy zy)WX+c^)FxUV3yXHkuSl%5}62;d*;mViy-;q za|EBzK*vxTPa(mFhn;coe~OSC)HcK2sSKd9xY;|_yN6u^l;^^-$B02ycsKYB->d!T z;kT-v432~jYKFtuuXy+dPpoUOBgNP}p>bRw#i@Y-*$$E!QVA**`gVP?9=6AUn z9m1%2x1HPscaPn=Z=EzcDZyS)<+IP?kNoWm%F0|CE++~V^UEcH&f}G4@I#uX7Y2F{ z|JmnNQ4)Z%KXDq+Rn;?>_@0Tt#dG|`i8%0w6S<}@5y)6T6lw?}paEVRamY&UTEEnh@`&lU|iJv}WKq*=Q~LP9Fn&HQ$^H^<9p7Svbqp?_SK zUrSy6X1{W{!Fp)Lr8$e)dVv#DwO!|Pc6RxPC!V7wgJTBT&1boc?MFlKE5YvV4=Nm1 za})vwlOzG07(eF+vW4MTbj0u%EYofV>BOd{^4hJ5SZ3?vU3GNW7RPFZSdYYvXYlrUtXx-hs%KhBs!+rEyTZ4GzR zJfn&+gPu;l3w#M_g+Gu!IghueJOl_$%%+CnLNN4ojGj0@`sBh$C;iMK>< z0A=53J&!M4OpIo_nW-5W5g+*;&J~;9&n#nf81=|RsNRq3S00_ZO285ZI>Y=-id=>B z9KNEV`M^Uc6>~SFn=!RdiP}}8UTKVq&yJE$^7-d1uUNlURtiG(Up?)}de0fZDL+si zu>--ucZX74EkZT$KHr7cV)}{>2_o7s>Q4J;Nl_pQqSpPwTn*OEM;WPn8VSw~;=Odq zT~~AybqLr(9Lu`9-j^8!Z`?8G*$$VQ@jnxLN;}E(9(y?~!{98A3c;f4=kA=JJBGeo zuvxIBY|63grA*NMg1?#Dbbs2vJ-=7h8oqJL#0)D;+2N$@C#XH<18&ps)ybgOs1Z08C3=<#6+i!J{a~Xu9f#bv4XAeg(L^u1t^9;i56C(%lOS5L*+} z01>Saz@dt=vXd&o>kdL;l&EPbylvRW>4*n&CgLABt=9ph4eJ4D+GD1srkq~4tToq| zR4Z@3(NQrDAlk|^nhCJqKOA}Ev|=XGUdLqI0|a7k4xtZI?gl+1U{SO0_@8cuvtBNH z9KAs!{n3IYM!iajnz=n)M%;Pnec$c9ve-rzlZb~W@(sv9eVhhjt#O6 zWF`O%Xn=HwricU5=Pa^J)J$R$e30k^#DUWb@>4^&@1(Dz5@GL@HCTflV+amO{dO@f z>QXfVN_)n@Ec$Y0+T%D{%*~A}uLhY40q=Vbr^vMK#lbM&9bMfLl3vLb>#@@d*;jO6Exfi} zF43$X%Lk6y#JPM4;#i8jDrKNO(-;p&eKv9fWl%s&kbzJJaX}o-))(Tco)}oF zOvE@YCvL@!E%gjbTGSodE{kbIX1Fyv=`Zq-Q3o~~&W3opf?fo#C7Gn7r85jmBk59W znYMwOM33jbwFbVhbJ!Ys<9bZ;F%)_~!Q`EX#E;v(vbQ;)*h`1uH(N>2A89ryQMX@h zn~z*ylzE3OKL;oXTk1H^8vS&@6$}c}-3=h3Qcknj`s4x00yDlj5ouHE#E^9zM`r9- z;CDrg?1F^cp+ZnH4v8Ps0l|XpH<^zFL5*a7WGv+%j4?U9_rbJm`21P9JPf9|t$x<( z+iy^r(LX$jtanV6X!yDN5;B@Wb5RG|Opo`MC!REU|GnUTK@NBN!v)*h=nY}h-7 z2Y8!E}9E)^CzD}`q+OV|J#a03x3;%}sC`TYpZ8xuzW7@)vZh{@S`!9L^mG<|jVdt`a{W zIQjPvB@jFF`Bcr18?RFeFYq~`&;S+{quzUx=l3$C`@0bRnwy)8ltCg1(LxS_S!p0t zvIdl9)Md<;6%su9Ad)%@8YK}CGqbzBvRV_vI*qBbduS)VHp^RLUTcX z(3RAn=32|yugxA;`ksDAdqFW`D8zg|Zy=D^2s-f1IS6~XQ_c2gT=>;M42fXx+8e*+ zmBFs4OBd?oyRh%B62fNBDwCpA?rdW<1Y?h*;VwHqZ2EIIuLaY_21tGb@Bra{FpWy# zk&A`)^!EA&w;fG$EPMI^z6YsgLrubs(q<*@QldH%=#5KSl{K9r@hZ`(6w8$YRNJNS zW*FdFaSXnP)^DYItc28E&RL*Q`&YyBJ>8$JtgZM~Kc0&M7H**U&iP%BqstK>8V3zg z*ENuOClXri_4bs8O&1-E=d=68BQU(i$qN)2IJEoY(YM5U+3Yg;kT}%jnK8qv_YE$f z?v-`t(xqWc$f0RgmJ-k|02=!Fh9nbJQw}MWlMnvpt*`?Pe5X0?SQ`apajX9_Ns#S* zw%#X0aXUbdP(dIms#8Lj4lc8>g4dg%G401Z-9i=V1tIU%=`L$)TbK-PCmPquSa{%& zY)g;;53vAMyWbOSP_wu^CvIz_>oNOJ$^~rEu2+HHLDbT?BY=Th2IIJZ8qvjqTWh!8NA0H5`iqaO4t1zvML}vj)t90o=6OLW z8G1L&la+?|#n)7q#_jj=GwSm4S-JK$rz@XyrNndL0PHtzj|}NOtq=39S<_{$j}64P zMduUwh#OqlanaG}88x}3nh)-Gm0H!P(*My=N0pfp%p|bxw_R9v4lAaFP}F`eUV-!J z16G!6qs!6v^t2!qC5Bsu3d3G>W`XH30Jd!>4bm+bhmnp>q}hQX-Q05%Pkd%otE)04OPVU(f}_7vzL;u*BRXuGSBXSb7V5exTn?MpwOL&{&Qd zzkMfpSD;w@@2Ll&6rTi|c20-`N(I50=qz8m9_to|XliH_Js%}{ZHSP4y0^Ov$~g+G z1;Em|LNP^mh!X~mk$v>RRsA$ht(b?v2n}Z#`384lmvgC|+ePwoIHKk8p)0deSwR=< zEkd`_hy;SPl!c`w=2!SWXg}9{kF^!icHgV}hpT1P&X3v=SY@>4Q)3KMt74Ai>qW@V ziV~EI1OG)?1dySJmHqWnf!lcyZnaE+A1XH}%*IxXs&@GlW<8Gr?4w2$_DP_bp_!Y;#58Z7!w zdIiX#_T~ExTI>tl5F{5nzDAMIdDhdBrpD*nOARkde=!T@_Nhdc?XO%|z-$~r8d8Wi z^ByJz@s&RA5xX|&3F|0N9ox4DZZSXr&roTc}fjYEpB{WE+wCWBo zM$ScioOoYCyyD%qH3LDrIvE&X@^o47I4NzB$6)R<4D!{UTE&9>+sc7Nh{P{^Mswy` zeYFkJvBUyfLg3p(s6&lL2`Tgq4C26gQN~pa-;*PqK{gFGuaAUy5m-LR3nBEMfH-WH zEVs1V%85$p_dQXtNWTh(m%gu-eCWh_oWlq*l%tuLDZT7Viuso*Bys?5B>+3!&!bmq zJcNU$8C)zvY>3OueotlB8vMGqoD0aDsm@qZuzJ=U&i>8&U;B^WO`8$v==l4$1!3z% zG3%enBkW%u%ro~XaeqKm;`HPQeO$U~;eZ4i`&~s<1r)Yui=Do-w(xOraXY2S*7Y0M z2tv7{`G})ekW}K_j#h`Zh;GE1vmE}XlV&mzJHlq+a{t#lGHHqicz7r_K|P^;C!wj_ ztWB}6@WS+dfq#pLBAVXKjt>_HGRmE>!0n~f1gu*P{yKIXK6tw0;E1(doAuHVSLbOb zHI|w&e{i|R!uQILRS6%o74l@b2no&0`WUt#r$|N#^KS*yFg_16wpWnMJfBigsF<;#iP1PIo$XZ~N$AiG)V;QUrqGt)tC=PcrpsXDd<~_-zaVswx z@xXQltEs)8K5V$k-^P3{59G@(y6{7=x@tjeo%E>{2e7}Q7ZPeSplU+x6Q%U6;3Ik^ zlSS4vyDaPr7x(N<-h@qp92MuR)~}OE0w6TGA_Qsk*O?oS<3I?gn3(8)42MJ9jt1k~ zh#R8?fwB?gyY4}uGfd?*Df)-dZ!c7ByVv@lrcdJe%Yt?kS|UL|!Nt6ni?$u8D@QZr zp+Bw;7em4d2yIS+liMKJ!TNTbeb)KJ5xpk@gZ&o`J84=Ek_NCmT7lI|8@aeWvT&;t zq<;Y$A2b96EIoh}ffeM86fU=8!QRobbNod$(Hfb5=U1ct{nxIxc$l!%)&6a^BRl8r zz}s-LZNB*NF2(W~kSUCHpe+BxM5TGh7fiL)ktL%79fEKJ_XD<|)L^jfVng?Br%DKsGahe-z0Gw}unrMWoZ|?Sh3~j#oiJo@_rKNJ{SYJc z2>T=*37XdN_HuTvDS$-ht&5c=z%IY=NOfg7kWVQx63>GHg;@bt44hd3V!^m42gJmY z42<(l1F=Ik8um)1BqiIkg(2{>nU&R~3@rV@!>=kI$At)_%0X=4( zx;1x_VYG=SYjzF=^lWUC)=`?pBf-b#KjUNZwjzhkfh6zyAH;z`Vlf$B5+{wBAXxms z#>csmq2Ve;7*{0D)Ft~pG6LlGXTDv|i!?n0zfg!w9_Zy=c)HL25!*8ky4~S~kjFua zcLnx7^aTr(<(LtvDqUuwXt1uE`coNMuwKLG4Cq4y`VaYyIQqTOl*W}WXw_$pt|4$i z=8Kg8PWSUya{>N814`z?-klGs4bu3E__|jo-w&12SXjr*pB01$*w(rPgXL7Wttkg& zf2BMcK*uHEx`UeeV=H_XLnbRkVTS|kjx(PwLr9{*J~XvKV?oTrs)w2nRyersIxqaB zfpg)@Y+@2&hi5>VL3j8w4Sz}R$D6Gn({Ab}VVN#JjtBc-G}3(`B$|x%F6x{#dr{V4 z)n`Yc9B{Zf#b3=}@82&0UXnPA7MB3zQ8gLjbzh5jo5B4ndIAU-$5~*IUBmT$aK%c! z!&a4WCzPtcbY^VJe9|X2g~rNGYp2n}ug~r-geKhkfOjV8`R>TTH$2Zk>k>Sgv zsFMY2#4miYJ=h0$0n3^G#sixrVF4YOXde?5#q6*t?SJ`r<^AN+BWs7)GaqmHCN{6) z2G&qpSs5$Om9)WYZ>nVG$nyGlRT9nxBYX{-P4@~>I*8k87tS274q}>X__N0_omD0>gr@O_;8EO{R7yN;ZX5d-A)ZHRgPB!iG{$XbUl_! zpf*1IHBel^_>$6IRdL9h{nSm4711DClr=qEaQirnD8S~^YsTl+*stKDpxsD(R`ZUv z;X0l{*XIuC3pE}K6>R;Otqw^o?+Zw2_tAy57}J^n`g%cGrv0^rNn8=GCHZx$b2V(5 zu{6%Ky)GuLM}6$u0*_lCFUk;(or*HEUK=!RpW`J|FWb_4xjbPk!jmfu?KD9{&obto?&%Zcd6n05D^V6xjR z6;G3BUt`2Nr@1F8nn6w{H{|Z`B#2y+j!s2FDz)*>N}7UF(qt50;;*I zpzV`YyaE-Y<6-~8aoqdONrknwAR z`sm1q%}2D6>|XywmPNr7`!%)W+7bK?`mcb2eg^Bcb8sljQSX)oo8p~Mir@o2+cfTi zng50HDL|V52sY}21<3@^u$Fr=Ul7RWOZu0d9!v!&OA>i@IzWv5SDo)}XzYFkQA0jife7(AMB2F=`v#7K? z^$68+gg))y^s>`K)C66RMZk_Hby{qWPm5ImN&q=pT|OZ31qlANEjrATO5$wMk8QRs zN6oG>S1r}BfxYGBz0bV!l0VA*B6@c>=Phv@yVY(_UPWEc8`pAwqY&BjSSp$QSC2o4 zr3%#VsnuGwZ#NjzY7Z}zOIVw%dz_y2QHV{?dU_?WPWVgsu}%Oc%>0`0&0`WTU;2Wk zfw*Q|7qHhD6%F(!UGHxeN?SG(vFFJ-wK*QKKI>hvRR~;g$Q|4!=gaSQZ$Hg2(-BPR z4Hv7AbrVZX>)6;VoefI?`DW}YyuEjT-jU_ggBgz$W^|=WSC4n-6#i?MojEPF%ki<) zK~G0dy*oPhLcK$Pe{>NbSRiOGP(bgXRLiphdwHgzzwKvMT8#9#6XUCEn-6$Zs8E)8 zCjckMqAc#P{;m5)oD4E4`sQGaEGd0?OmCL3f_D1(g2Pwvh=OL}c5XsWg~3MUuT1zN!h?~^CS$gE~Y^vu+g zQbBh#;dqor#*+2*V_2#^evh#5>pk!U)z$XxhS7QN9F#SO%z0M6T`wN3lQFiu+5?f2 zf?#}ukS!&k`9E@g`H~Y9^qe@LsrDD>6u!^J#{2%6A*h* zG$M4Kq%4c~C-g%P3bDUJd*Z};4O^M6iFs7ebqoAJuwpgSsQNtayFVqf_s+fBjkcGU z$@lj5Ay&nIj4((JCbbn)?%pyLAWnjdsJn^Yj&B>({CxF=-18ndf<9@AjfQcEw=)I= zlPIf5EvWl%W|)+YJ1;hB2e)mqJ0q6CB-UNp?h9U*V_|WCSZ2_&nXLE z0gRRVTmgRQ8i<|N6@W1r?(^-pvrCi8BNM`dy_MxZ(5I6@+epn|92RW1yO^}s6cP3# zurJBIUVfw^J-k6Or!t#08=M`J9#>{$Z5uc=Yb`%7C ztE^Ja$20n@16%J!v4M+NfwFWGR_NTRDzK6wF zH!BPAy4Y1TCCBu4bYWXESl1VfiBd1xjH2KGCTbmD9%TzTH9DK&^3+%C|Sp%_zFr^_L!Whm}2>10yJ#|1I`P$++suJ8n;|UYali*ZxzmTK1 z{se1>76|N@;?*NZAV{qD@6vGlXEXXhBg{PN7(pqT!u%!oAyDj|ZI`w)=+BD^^+Py} zj@T_ZOZK^%y<3|a7g&aS7y?*Ka-Oauh8Q^MpZAW|+L#bg2J9JE+Ecw8VR-+7Ng%!6 z{~@1^fT2dt)~;W)i$QV+9AaEYlG&2WG! zp?3pUJdCRn|Fxfg9fyJu^|>Yef2#bQAF#~wxeLPc1`IVG+spz76l|F|ReA318Ig$E z;H#r29Dg|rp2zg)XvaY|qS}oKAuD?Ik){_o$>;{Af0Go^hk@IEWMogvg z9n3J&*R}*jc;D}p5%$k%&Cb;xofgy4W6fc87KSKiT*gEv0#-69hl)_8)|RBIWZ4-@%9XGsV3^3*!e5yi}fy1|a(OHJ%9-bP21d#2*ce zXjjUbnQ}nvPOB`&c z)ZJ5Yw12(i%6I#lV7aUGRXA+~|9p`&n_ym54@i%*x74a%`}v}P=k#wkTUbvJWq1rU zsS$!q2NAk*c3*6^Cl_1d+U?cgHt^oi_KT3X*yI=cZo zhMJ1BrS>+MJ4{1fzO+S6+3QYu(nY5*gbCv3tuR0|k;^CMRw@`su9%q1-q1A|{u!tt zBO$@$by5K_Af{&~_8i){aHsJri&NH1(xFBBu+!~EisG|`hUR%n3tgEZC)SjMt<5_v z)-bEE*8B?KoEJ~cWriKwRlkHE!DF6Yq_vB|M{8ER-8B&moO!Yoyw9QyC0IK{Xx`&D zi8buQI0+FUHsP z^nIW(Nhe5$8_7C_A6%Bj&UaEqb&wVrObS(&l3 z=|sN86t7}^OX{nbGx_PTfe_1ynIY@!$WU_mV^4t$GzeoJk8u18gGGXlSBM$pAj02b zyH#%SjC4FX75FaK;z)yZP^0EmEBPaJkhm4(6xvdgTPZ3WAk#MOZM{WrpM>lui!aoS zG=&Nb&M{0zTkBL^Y?ISUs9OISUKR%L}Bu+*2Kc0xOp%s>CEWuoT?!DE)~v{R+dGOHTUnZwu+cET!(Pg%zw{ zM7^TSI?zGZ&|Nkou6%H2oJh8dg@d@~2HO!DBXE%bGFg55K)D<^Tcj`?2@< z$}1b(yc}+BSk&`e9u23y`+kQbt?=&AYT;ymywU$7lM&Au~%X|>Z$hrM7ctIQ!ChD3@!ENlkF;O*%0Pk}LP(Qe0WhK0G8hO^Yv1|*dJ z46b3C5(*v^nyA3^+JdnAvTGau5bNS_Q$-!aITC{M16yO~+SkUe2xAEq3*zu#Y6ZuCHe6Z!%fG zOMKSWL|@A~9O#`xI9ma$_+nZSrBcZYxk68Fi^6M#63wGOjpxZk-RVg`Xpg(~zy-tk z@$y#7fYJd*4N>}8Yh-e?fQ{_vFhof5orxl|5{&!Cq&EzAP?yudoE~g*;W0J`pzMB`SsK!7OhIqpzMlw9pqjDyVcx!n%Au28PnL8 zr6zMiurQLU#8p`xrIR7JMrme;r<{%HWYZJnoEF$pzxn3I*iL@cd7hqHO3A_Xf_v6Q zej@g(zV%YiJ+;Crwqd>9uJ}D~gWbhtwo0L`Q=gyg`o&O6^23rxSBW~uX8$1ZVpFMr zwNByioj=A22~Dp5F;VG>d5>8L;WWDU-m^;mxxAV8rXRCkl_*}vevsZ47Z~8-)*+_7 zM-1l{lxA%o$lgw5UwxA)J`%-WMCKbwv2ueBKV+LYra*sk(tC=w+~`8^ok^mZN9KoO zbQu53F7*u8L2p1ZlFCp@t$ltX(Y(@oCY$^0G#$?hqm3rc8{Ag=ew<+XcWi;&4BcJy z^N!+(&G-8KA!pw;D!aQN8+@ngK+No0Ut!49;Tt905({$^=v=Wu#8>d!@RWD)^cqYF z5?mJ~QLNTvWP=(s;WJ`~0r36!l+gsbUkl$2-) z>>zW(TRCMv9%5&GDB`$(x!+fgNpNFw7bBy~TGfA?6(el~pp>+kAHUwUiO zn{WL(!8)iY%k`MryRmOoxlQwPAJW;*>5>ldk@tO1rZ-U!uYBys^I1h|w&gV&UxF|w z@AgcomS)45B)$4&O=lx&Fj*Mw1Kd>3c}ZZT+*_7;w!d$&LP2{y088el>G(MhFFkDH zTOE_fzS3jqmn+mdEPjFMwtdG3xsolJx!K{e6qQ%%)#F}WxS7_0zx1M7FxuM7?qSYr znDKTpRUSizW5ZxFKDn6%UbB(qXo=oCL$&hk=H~1rYp&V0d|TEA6wAA*e&c2B?2CAsgE z%EmBBL3_n%p%sUfZ)K%~l~B8$pCWzwrrxdai(0)&|BsEx#+TS0A?Jp7qypW7Ul~nc)vE~FV zUMCe+@MSw}5NwpjceYVD2sJsjFl*wP>#cDEb3Ozae+N*!f_EaQv?W1E5b_jaiDyxO zNzj^pLFIz6Hh7%|xl?hj_aSx>g#+d}EM^&W>eACxWl1f+YrptFGgts3amy)~Ky3rm z7^GXNu_QGoowLo7#jnmwP^Lix$*9wOO73|QlAn00R#mT8bmWekiYcjzOT4zy?$VbV z_05e_F*)*eb-GasP8h@`Qndu^avz-4G1DiF!$E_IB5NLq)54&?Z|G z9np@9&+0z$d-P{`{2!HK#zya2uM+Zb(wCGe*{XYBk9WC^aGy(zaMDSAz!3V1ZCQL| zDfA`w%<1v5mbLJ3wt9EfOsh*`T$i93v3fcLj+;L~D zrn3&qYSM#9M=2WZ5zd zoP`_Z;`x(y2M`LUz40Qyi}Y>|e)qxq)@6!2;sqYe!c}eNlKS~C5ELqt1Ww{*zpR87 zB?LdI?^*Jr`?RbaJIMxrVr_pNjCkUck@*45R$MY@VXBT+ zU#RODJP}2zk{;zB`6+glB@1uLy1`|%+2k-~<^N*ut-|VPnzm8g39bpUaCZnHxN9J| zySuwP0fGki;O_2{Ai*uTyTby4u046)Z~x!>>}#LyvwdB2Kvt%^YpQE{x~uNGE6MN_ z_yYZg=QlMs2NEEfqocmk-VS{fI$G07Bp^8Cm8U7w&atH`c*itQPo0lUMQ>AxsnDD? z+9S%&t&X>}Pp77C_)<96YC0$sP3f=M4EPgU?T9`=1s8qBXI==tf(_TQwWHcYOS)M4 zA~OwJpH#@;#ZKdgae!SQ#$Ono>|RmNB4vgIc3=stk>98y2c$z(7Z*lCC`f+dF?bBUBUj)EQG0OP2-YGp)0{409|i6dg(3H?sm$+ z-r;KM45ACfRbx|_Yz{T3Pl^pkA@(u-rWWssraAr1R?mI6oFYE`y=QQajE&p)dkZSo zV@K5KV2CBVJQbWc%qps}dLni^`8NLJT}6ln^k|R>D&H%8Jq|{JxhxV1azlA7Mczd@ ztqkXdzvEEX^biG1{YHd|{A^$qnF%vx!raZv$eXLa7NOgIOEju%~W`0|bFI!$&${w3QbYXOkC ztjg1$4~KE;T}lvr=#N7kz)f0snG<>8qSqtVO zM(DddTiUJ1DKplhs*G}N@5Epck%LWS^^vUyT{3GR>=x*o=8^Kic{O0J7MNHaK%`sW7TS8X(2t6Lq^NTUBX z{D$uNv<>!#%_qyFm?~JZJ@?vA6bm3>JvC@CH}Ep?eN2N#C5VxUc3|3Q@m|aMYP&$B zSjBVX(!M!Aj>uT}XYRQTI4h7PG0mT?v@Y{plv%&HysZPwju`=s3Y_vFKtAX;$ zA74&*&KV;*)4C0}!0cAaCJ#Rj7PN}s&(E})oX84)rezW^rR5x0=8$THh8}*)NhdO9 z$N01b7adACOAKlFAsCO|Q>K`?lI+ZX+@I+*e@~PB=%qY#;xsQM-@G_sMcdW!o05V6 zR`5dm&cF&p*!|GXs;)G|S1!udI}f!10|)C0GZ(D+5f8P5rX(w3pfBJ@n2MZ0w_Gyj z7MQQL<`FZ}(!pbe8UQHUPPhIRl_L|=l(WS8GsNo8ZnMB-a9<`PNRI00$}_L35@KoN z1pm}Av^10b0W1xIGd`XxW7Meb1kyWo7lEv>iiaWGc0-H?}ef#T4}dOce;^O zy$E_)DQB2~=QAW8fY&@gCK48SRf(YDt5Kj22>K(#U`wPQUMol9 zwc|_D;b(q2^G%H46HO$CS9`juQ2BmduE7KTK)?Gn4>5$AtB>0ue-L*(GVsGupg-=B zx8tQ!NC`z!N_=b9n>{f2r4@VtU5ELoYEe~GUg&ZWPFh~YK!2)k+?awpHk+PBTBSxU zd)nj|zEHkxf3b9M3+}9XoTFD9BQD5tzk+fZqSHz=eZjmmnHsVL7Ogt&*Lrm=yWA*S z=E$-Q3qFjcP1h~IlM0tL-O&KAm>#pJIppIjf+U1*km)G1|U}iE#YK3g> zM-!m4cOg&YoC8iCIXZnFzI`r&J3z5g5cU}M<2I)vCGC$ma_NG+h zPuNIe0B@VopQ)E=vzSH(Mn*(*;@!9(XsZb1q0NyTY(6gv1Mx5$G;4{NY5mFHdq4tn zgw!%qXgk_C#Y^UNMjhd>#Wcg=4ir_HiT6rD7!C%OHbYq~EmWat{kWP}fl)G-oWh)& zv@stMWaEx8@5c~6VYHfK3e01JqVkwan8#zv9n@a#$$WTK&m^BYL7NuZSK zf39I`$3PMWE%=}p7 zwajlkKYumR`JOw50H&)`GLuPoUH-^(RHi>=VUkcgWSECcDRO*wUVQ5EqJ*w`3NZVW z=+ZTkhc{*#DcdEHKE<9>*L1LnKdbJ4R&AUIx<Q4S3cNpD|=o;WI?f_K;iyF3tPl_(>+wu#(=1Q?JeE#U%E4?!xb0={>ue z>u~XSca&2bd1msfyrJ02o1b@_n){t?C)rv}jnZG$m&=kJY;IYanvEvQ7V)5@mUO*l_fFL21 zH)!2-rL=O$WV7`eAEs)T<705t~M2yQktTVK?d1nfnh4#Wa z8t@}dLhm=BM#y+R))tW0OKFN3sVi@Ko$2XVPK5bOc-%1bUYy|VV8^a8MH2p1UNm)O zD4b|G;A0%-m!))v`)=_jFZPe2Wzr&?t~Am{RK)K)BufCVC~uYWKEgg}dCS_9_3oYN zz>>7sAb!Wtd|2pZlc&-)5gWA(Qw9}GUK!DY$1gH({WD6hyD6!IRD&eA$BsCUAPj;TZ?zSc%#k9UAB zlY;K@_icTJsr?(STF1u3F4#ge5x~cgIZJTljv08JVn&`;7H^pICvU&%se0RG)STFI z2QTIY0^z4oqh0Vf6ZE0|E`Y8^sFQ##rY!>71v+oOqLEUfj|7W~c0S!%{250dc~h@K zwTTD}HS)&RMc1`&qV?SJA^ST8zGOl^)rf#2R)DVl&EIS-fO)N3 z4sm5|#45V!9D^j+4Bhq*L%CPd{Jat_p8nXo6(gq;+|TH4k)hRbIG@quQT(3f`{tp5 zFoVGVhMr9F50NX+{_I9=!)=)KJpdOj>TNz-5)Tr z#^H(ehYWKt=5*8Zn2{xYJb=5rKLapgO(B2x9KcOn0_WW#b=yt&ZI$#v#Eyp8I;ORi zv)w4bl~UcHDz5c#wpgs#nO$t8{N1jWzNRp8eZGb)<#%GyeXIsbU`|A&07Ez=B|@hG z0-}6DznU#=!OeMP@6kwn*@>^F-kKb5TJu_3bG09Z*J0o!S$8X_n$rypy*CM0mfi4X z$umup5*Gt>-k%D{^heO=4v5YQ8^E+xImM$3bx+Bu`Y;{CqHXPsVm>_2ro3Nc?+#lBPps7g(R|7OL|L97Yk&+l7>3J?FG||l#KN;n zTz@UdQ?fU=fdlYd8x|%&w1Mc1#pGCS5dfge{J(+X`Nde_N5gEVoE`t2IRBTnfC#X} z$<{htV5>|EN1-r{m6L2j1UOqaX)seQ`vdyc5VijXv?+TH%- zb?L9c%>pLZ2k^dv2y=<;-sr*5$fZaye9+q~k0>sztOO?)wmxQDZ)5+=Dz3~rO>S+? zxTd65BSDQ~Xv%*)1t+%_XHZH|f4z6EpWWHcKtYX8e&(YI^(=*)uS5#^aXtcDQoedK zwZCZ;but8q`#kz6<80;ZZ2_pZ6^{(~+glZiiD+iDu>is2Wg5V>`twjT9?q4DZS%)U ziA4#9w8Gca!t2zOF7Nitb{BKz^p7+OZA;1;*9+xtLp=0`qZ!_i=jZk5?N{b+5s+5z zS$`e#+1;>v#j)cxr<%I!xSHy92Ng0c6@rP$N$foo)YPRfhj$JUp1e;A3O0qR`k`1-y@!WOL_0Y(gbE}xheV5}~g4DTGdZh=ZRwn#Z+9EJ~SO6I# zek<+I1{{nc)H(bKxG-GA>tM@0P~=NR{;|02=GphZL>1j2 z;g`*~)s^4iRw5}Hq^g=ju7j)`g78oB*N8IdSsgTRWL(A-3 ztAJp>+WWi7fvoAu*{d|!Xl!PZ3-ui4@jV#ncnZS!ln+50bmnwAZT_0tEtkP)KAu! zvq{Gj8M1f1^O%YnVwi%88S*eWm{V#(Y0miTLN2BL41iFSb?n3Liuliuv4LPi?*4H+uhigDrJMyT@!Ylhh!o zQ>e%Nfxe!_s;cjz;P$u2CHRlk(UFOyTv*r={;CbrXLAuZ)9EcT+HH|JS>$|JJ#`Tn ze`+?i8mwxBVT5N~^wd)sey~4^DXbp4zaO@33>=!Uu*M5!s_7Al!xJFmPT}@8(?~O8 z@wyY?Dq-FvR}h?Ni15N3|HEQcnKm^&`GOs`i1oK3-$v7?Z^?xzpZY$t(#Vv(LJgk0 zJT3hn7ke8jvQcyxosbHxqTeQvple74V|)v^Va2(TItIao4sWQ1c?d_8m;XkvHDKDq zx#lkd?3aGXR=n)3=&yzK@Z9?3toW*SPibEF&z{VEf7f%D7@HtGj8!Y&>96hy`w%nNA=Zdx~LIVSAe2Ir&~v(!cNdq)ek0FZVMSKK5g; zMw5Ar$w%MjJd4Fq*2NKR7dKxOXvyn1S8^_AVNUDb=r!Oj(ux$BzT>?9A_?;wYk`AJ z#Khd35=f%XBa(b+-H!mWL0xa6R`gjq_AS13AvE?*UiOU5>UWq8uiTp&qN~RNd9;dk z4t2A|UJ1a?N)zeuRp_jM`H6wxtD-`#v&l@(?QNpfLUMxP=j&-QdeY#%?7VD2P(0Ve zbOed zB3Z!Lm#gDU=k5Z9w{gX50vvwNu#7);T$k$2%4c$DPZ&OBAnY85DMdtrgQDI%{Q%Jw zuupK5D$2V_XmxFKUv>65vPKy#%@oN|t|TrpLpqP=9vpx6y>^HRUvbhIO?4w#1$t`! z`GZIBJ)9rN!~zIEoPG-L{{=bWL$BS2sMeeL?EQf+yBaW$O<7o+9360Gi=_@w{lWX= zSg;BN9e`EJy8a+@v7V+<$YsePGe6Ya*W=uDFgWG0u5(u~hfHT(c)U|Pc-j;dl67JN!Jw7d9i zz`I%Cb>$)eOlmhzt0=348B&YW6Pw(X9t4IN^OhzjhXXHSXs$@Kl7}m3 zwbr_^c}pCK36nCvMJaM;{-yuB}rHKA4j6ui@1>CwP++_Wj%N`xFvEG!n6_sK7D6qWiI55B^$Uq}~yKU$6-mpRPkpnmVkba!=LmDBhI? z#5X9$Fck78@^hFr|0b>&mn@RsaJVkBEL!8<461RP+R)@C>UiFzN)+NlhNlTV2v<-( zUU&-YGR!|-4-;>PpzwAg(L!_f0n?Qg9{o-TT4f=Y(`9FSX0J{13f6ogYiUlq&a8v~ zx$lrRM&DY zktd7Ga)Wmuh+L=Ilh01KVGvZ-owrWsu;D9|(324f>U@G#_a9H^WNaxo93Eky_Za=f ze3FAB;rh6wCTfb->A7(1mFGDXRg>+B*`zFbDE|Ji;`4$w-a5iG*`P4lQ|)5ez|}We zW4C)wUWx`B3<_B4P)a&|P!KuACe8~K!Qh=D1OBH(kCCDqP~bNZsMHwXH}*w z)a)}VEri0NyH8(|7`{Y*(jmbg6_8JqRg{aB`(jf?$I{$UVtUPkHUGA6?z1y9bevaj zFTD95+TfcO1K*%#e;(h~RTn)Bf^8A2lIuCIAEIL%7dY2GXmRw z6-w~u!FYpomC?2{0e=0t;8Qz7XA!WtynMf6E&``zVKHz6!xar;bm-J?4|7W3%lJ1u zAD^9MZvCzPde1DiuzlOvY(Zi;$rKSduy|8VsrwzP#-*p_OTiEig_K5M7f9uZBtUZ} z0H^6vcr!%HEz@4XDQH&?USNmBA6cJ_`HAS70+}#9u=^2Y^?2seV4KuRP4D&C<&nyy zv-2EMl2A7FPqq>pLr_{8AyYsUDAgS{EcDN0?uA|3RB;!s3qlGMT*%&x`O+j?xr;is zyeJi{yUUFZxB(Hfz~p-^4B<1VjpFcN+KxImkm1)wgBcf*Fqk1K8TvApfy`p*C0i! znRGF)i+s^wW`d#7g`tA#wttXHZHm*InTE1&5|fD(Ay!dV4p$sA3NheyZAzLqa4AMC zd1Bb$?FE6-?sbszq@2aBL4Ubyxz&Nziu;^RM$gbesvfY!+LFDy~% z(qA0`DaA(8;C$<;$yPY#T8vEEnlx==jRaA)|ZkDpJifU6BaMwU1v;M_Y z3M*j&nNYYGkAeb&41-kqgDI%>hJ=CKYOKOSbPByKd8J`fIWdKF( zd;nQQc)&D$ z;bv#^V1CzasF0uY|oF#$LB*DwZk|PB{D+b>-j&YO~v>1E9W6k>J3(B5W_by4nyb1VAV;;x? zARAr#hGl`{KXCC=kr$+V)=WU8V1m{-#s8>G97qdo#eO9H>78t^;>1VgKmXy85d<94 zn<|}^fU^N`JtPEjlptkmM7)a}kqAao7e9LwdkNaC);vW*&c|%s{p>LaEZV@0&oaP_ z=QjQa4i5(neS2uYuE7jDDA7dNi&p|dlyyo_3Wfe8@eT$%%{VklQd)@=N%k4Y$`^(~ zpaneOmx$swf8_*f1_)_;Da+YQvF8;f>yZM@nay|-hH3Z_NUT`=t$+T9HXidA@NB^k zN7L?qbpilHsI_Oe)7<8(Aoun^Wou+)WJ0A$}!wu`wM&(EFDOWYXC6zJSsGi)%i%i6TL zJaggfXJjz?M-c#T7({bKJvg}+AZoDqmFCpv4QEBb&-rn|+jgz}wqa86rV=3q8_xGJ zfr6TP2qSAKZ9vG*YnEh3=TPBdl^LLDF#Zq8VIyP%{TPuO0#yu9oI^KeD!k3@1=w^R zZ%;K+=J?iq?-Gb55q;$u2K4%5Qyr@s^N&m~g;333QhM3M&>`v7h&qtbVCT+=U|o&53! zTTC=T)Eqq2et}xnkJYF0dL0?Kvs9Y~FwE=;cI?+*>UdcsCG;aNo(+s_66c6PYP2hp zc5;u^^ba4Gz72-J^=ApI+Vdza?=lN^({Rs?Lxeb0BBT0-5K z&?gMo@AoWZy=bCYe4e?0@bxkK{(2`17gI(i4_nW4OI^}YNppzrrlPFwtt%7t9Pow| z_};}^iSd$U$S$(zEO5CD`8_<=;58c!gnQlWxBWmx65irdZ1GS?$vOqMwiaJgN~l%o zIj1Kgm=@gwJ0a`QXt~pKEzUQ?6EeUysJy1ElCJFdG1_!w^pm2@-%y_OHLCIG=mZNr z%kfMaDJ{wN*6Dlhssv@Pu;d(bIOKe(NlhtoWTfocn)A1Wb@y4%F1fcQ*$R}{OkMKM zNDFbwQs6I%I3dP9=wB7^ml74~J|N?FQ2Z_FP-;)))NIAi1&cj}29;>ZH_z&B(4%kj z`171HmyOm?`X!$0>O5>#JI>z!u}XB3G|p^$HEeb!&cWZ^&uB3}$zr}v6%)sD`C;ax zg(P!_;<@N-iUhq2kL%H|DjN*DwD+B3)ONHVJUF_yQbJoze-h1e0RMGZP4gD@vz;fris&|%0vN{(hsA|E7E8Ls#8aT z`3X`jQ^zk0`s2k`#E0Rq_jC^>BZ*;h#WT3HXI zFxGS+gf@TuvcXW>KF@u0T0ymgO{Da$!DH5!XpmazeCmhwdaDDy>(d_T$43PK_ zRlYZvhw;STI1v@eu=hpc-xz;ot>RqjUU>4Qo?X9QQVV(odiB!-ha$K zSTYq-EkfZrI;qj`T*s~+k_XPn5rL)N z^T;=^De~y17J=rMzv%wxGTSd;j}7YD${l&P)DFw0r;Bm47@z0Nd-0}d03?VeZK72_p_^~ z&uDhyo@@c|D{IOIH%kd9uf%G_sw&X=?D9Tzb0f$IU7CT0l@(PE7wMA7_3(`97MAms zdah~9=X0vbIZi#3;hKDcKaz#xdr9*If1YHv;G+H9HdK-R{29yOYtku(L(hx5msi{P zP+ub0aN}dk#bpu5^z0dp`J3~>pLHB=@YHckegFJOd04v3Bxm&Mz)rxjS?wd${9NWj zcM{(BNUFL%aENms2R6pq<8gKbqvpqB*VKvHo>PD;yeKfaS;q%MqgK?TnE-#Ue2)Ic z?4V}GBVAjhr)0l;fcD*rm?u%SKOZJWCz3Rh#^SovT^n~?D>eVuA6bFLWVZh}iPYgpa z$qu&O7Vf-q8Oz$g>ZdL@YX`&HRwygnNwGD>O6Vxx`^wBypRug{zPvq{ z)n^6Pz%7*h@)vlCxb;f9QC#l8wsKRG&uX$VG9vH%-mm{QXtWrEcF&9lds~g1XMMox z9O8j5zF_g%3n%|w0q=dpnlgOT@hCGI1P*c4kdT0X;3z67k+VQhlxA?>w9QABeT+&w zKB@FNDR(V4RZHX3qMwWZbdH5l3DuCkSBFHSzucdElsuJzTDot8uCqlm{sTpW&$6NLIU%IPVTqTFY+TJmFr&B=iB3Ha3aMsRFVj;Ck_idNg;S` zUTNsu>Q}-Zjf<-4QsE~y6)Z=Z(8on9Hv|ZZxg?;h5`H%V)*qIlW=6+@X|?u#es(Tz zZ{uazYL}Pt^)to>#adp$TYjm7qGAWtbiT|VG_zlStVaLDWbI@3I-T{Ud?F}Q6ZN** zlfwFundyRJkJ5jVu3nQ9VGy82-AU23XEcAHW{?qENM=d=e7K)dZdLT{n^XS)Y`T|M zS^vA815O#d-vq4*D5n)L_et!ro|1?a&mfj z%DNsFZaELqos8ls(=vXPT$_I;opQ)GI1d|<=T6sYeP9;FljY$~l22+liluUvIdhc`Vv? z=37=uVL`Lt=|3(S!>FwC2~D@eNKXfY=}sbnF=!fO??2~p!m~*SO*98X?}s_DPaKA7 zVCEgdE2G*DQMr7n)1idt3`ION4^QbP96UwSpH|}Pe~%2#jc?5-^VJu|}soV~lNx3OPqKeAzuFzF z8m3eeQ9etQ!yO_@4N6_C#ozP|9My!;ggVnw#H?*l@kC9GRoU5(lViXt zKKCEImp1fOHaI@g`@Y&NEn*~@~Cx^Nle{~KxOi8?Yz~Jl9q>wS~mXL%^IrsQA>2p6Xe&tp?GH2Wj7xv|-+Y4`7BR!^o{4}%IS0EG$#Kg`h z=lr#P0jscfgRRtHk9W%)Mo2Xgb|%v$O$=Ko_?=96PP89{XSKAvvaS}`Ev+KtI!_W+UlxK z+B$$$Hw&oTvUhOsR%{n#jZzxPBC;(*s-E-t`yOpI5nW7BVgGUD2iyT+WDQhs_%ju0 zOomn6toi!KTKL5d&)~!QpW8R@1@{rZ-09%O=)=fJCp$m&boE_hZ7)xj5B^hmk zynaGELVwBvubsOwXCtGa8#_4b*k=nm&6jT0&F_k>f4Ylqe)R6JN(;euvc^TyG^1u+ zYD4a7bC&YH%gL;9U|Fi|*!;ZrBkSIFh1)ZT%SRPFjlPcniqtL068Q4v3*BZPkB6?V zE{7eN=<=WO1qBWaV@6&s}+wHK7OLEj3uup6{tNdBz zTwA)!QH0aMT!QTT%*}UlNEMz+v9X@NJH0p?gp@F2&55Z(&%Sf^a}9CE8lKq? zN~px=?ldUW5y{#&*Olvp)G5v@w0LNSb0qijM194Tqe!^p5@>ekZZdoCAk4wjuubAP&wiL_1fYTxw{;`Bz3KA zJ<|A0Oj!Pp=${jZuH$wM1O1|E36JgiHJ73AOGNbS_Y=PEk})Ik%h7|DIGEot{4Lo~ zR*QyL9ltBKaxO_!l#?NUuyu#jKH9KrW^j)@59A2E4;%lMOHfz37fv>8X(SL%7r$3; zkT|lcB(bg}0FU;Pif>DeY?N73KXW_mBjLHRkrQLyXv)$M8(UrT!GN_qWXQ(lkYGmF zR8e{HCTsJla&+(Fw#Mr>^C`9G>&Lm%DBxLqP(80M31oRlMdwYKV?x`w9RQ+)}jEe^75+QAfR>Y#*5eif_Id~Or$5VeZv=9Akq zTWa6+eM7gcJB4=62*pST#n`i71Psnbuf?I}a`B>gKU_wVEnK;2%Q;$DP*tcMm}j^a zg$i#|DU2(_hABkh4GeH2?=rhaK8hL*!p z?=7W^yyrQv&6l>!c!}oJoz|~CxVhHY#X|pmNgOV} zem@i3xO`ORY1VYe21|TXkSIA7^3^C~&F?T3otxVyN;%D}@95QKRSlum9H}f+ODpOD zYu3I>ESiu{_-EaYhChwmX;hOML{};+x*RYYiI(WOrr)nU6e$0C+s>;OqZ1!ivRk8I zBxIQWOEusttFZ48cs>UG=(w`U0g0AHK-IL`aAkhpfd6NJHgft07qjsuw|i!kvo9Jd zzl%n9I-&0`h{Dn~2T%L)L@Yk(&fMBOAt_2@8~1mGhev@%>zs88Ad|p+dLUyPdy2{7 zhbKL*B$fpk=NX`AKpx?n9ELb~XFWb%^*R7=Ap*%YT4tbFpRf;y(jn))EOEtN;^`pl z1v%AVE#h*CgKLAoQW0Jl6%;~OnapblvRjZm^ZJU9m z3L)n9ERe&Sto!TNw`;fNR%S0TFRlb?0j@p+kQX$6y5n;WTFJ6e)tsDB(U`tv?ljwMMJfdaFdc7r?ii& zW~IJr-522>Prl=?qSNKk`PRvFKUhSvFthsblL2JBU94PYf_*b(z<>EFBFZZ?vC%x2 z6p@IFtWX>d!E3R6*Gh{n-ka~anGOOW1c%=YkZ&rnDG|alCy1X^gd-jg_p|MvRPf3wQPROJhepA znh_^T4ir7|xLEIU+WFCUA>gzM#g5@)Kd^KSh3}`gW*EmpgeUrqJ)sfLIKHT@ z+CDFLI1rDK+r=bOcT8Z#kAVLDOR=%4yoJuO>sMPl4l~i<@=piuPa$(tGFzW#bBHs@ zeE1E$$*-nXI8^TGK5!_kHGH%7>hwxgO`W7}wI!#ezE;})jNbDGLap`I*Yc>)h>c8~ zU%KcsIrqDCDq?9~8N)n6gnG76dGB6#N7Om56&m{xAZ)&f3(rndhq;T}yK*_NVIUJL zV%&(_y7ao~>w=DPf-cs1-a<(FCVAkxw%b0>rpvCjQRtoL_WZdjR#zB^`wLv5A0gGO zn1k7=_Qm^^jyPFPf1wlDeuRKj84OzGIHaxk;#<#~OX??bu5h51onDrf* z{wg7_=S<;z-8h|Ve)dm~jYVZ6c3~ehFay}l51Qv7>YAWO&blOYUCt>BNY>6p4Ci)` zH#+0spSO?Ghgi%-vOyER->9q@5y~SwsLD#7=f^x8T_pLHJPgAKVDdnROY{Yj>pUj5gJ2S zp$*DGJQ5+Mf<|UXdI#wo97pBfZd=1v_}D7*&eD$x#W0Tz0mYlNf)JsM{S!?VV4hsk zNVH{NLu{uob__AQ>QD>cfxZ1)8>3OX5XYG~KGuqOnqXwx;D`{0M><#A?h*$>C6LY7 zjZ`DCfb{=NBI4wmvl&xh#wPoZW%vN zuD$O92aweWIq)h?KgU5_6z|W3m^ATTK@>R#-cGkR)AWqrRrI~t6&Zyt z$KsQ4CpV*o56j%S`y-@8{yacl{G2NF>~7MA6g!rccJDvH0PLt>QqhX}a#e-^A4Eln zYvNweAnc3W@iUBq;QK+rX~P4(v{p~Yr>}GCL_+8`1DlGhE#evzy7?$wc za}@S`STDzOibxYUl}k^d{SxhhvvuUA9l>XlFb#J@mE)9qUnqT!f z#xnzMD-0AexT0ug6?1B1i=Gei(hw8n0oJpPZpf<-MbDn(V=}PB>((@Y$m}+F&7{_u_imRkk7~8+pY{ zYFX1#5l8mi6=sK}FhFs3yPtb5Ga-~FCJW5Rfswb-Co2UiY-HR$+YTn|rWCugcaE9r zlGkEQn~t^N$(WcJZ`xvak^p=4be!hKgo0)a5}_Jw#l zGq5fOpYlYOn9#w5{H^Vgzv(sYmWZ-8*>6cgbuDgvlwoF}x!J)|D)tFM-txPfsud!F zv*dNbf{%gBiNjn`2 z`KPE35ix`%@yMZI^hJ6v&atgCY>sSZGt#$~gKB{GbTscf|l z_u1pALXVaxHBuW~T~wYcdC$G6%Hi?H<%3Y%*QEvr7NDd0@eI`n&n(0fG2;|Os;d(O zmhHqINGYLSHreM&k>G@T8Ij^cd7XEM$zG{bX~OiU$!UrEc8G@O$RB8?6-+Ji&WNSi zdId(1Ln+7msRuDgAd*QX2`OPuPuQENWYZtI)i|sC#H~64&G`o0VD1ICY2XJL!0<ndd6koy>TZ5SBx%^o;u&{V(+JnRRW=`S)Bl>1Y(+eFXz50_y%H^0qf zK$o?l_S)B_KI)KtmxSI}_$sUx{P!$v@SRr({S?$n_+6>qB9XfyL+^m|RCz#bgA)P~O7X zza@2b`|ukgVqY)s`}nIFBk1&(i@&*YDs|qNw;JneNs_n7ADK?~l6z!9V_6(UWaVKq+S;#?vwbh0FfnvO$q|1rIlL|`!By7wB z+-2lQ?054O;*e+b@kdtp8ByWOm)=OQ7*PAppik671PYE{zSolLt7I%kI#ZdNr9;4W z>19-~Fl338!$^5@(NH80qWhtgWt1g#Os{R^yH{6FSk53jFS^UXr+c1o(xxDH)ZA0} z$zPjXtI_>Wa5|sTeA_7&y-8fFGn=28;kazCg8$QWXQ{SmE5hm{XBQc^a4PV0z^n9st}JR` z31Xt(g@p#@<9{!pxCti|4DeR`@3qMUeTZlw10q8Tg!=Cb44d;`4gcxH08#Qz%T@1QS%7XKyW-{!qqFjE*4iK2r4E95_89U}6> z{;!b#2t4uEtWt2VgZ?Ea@TzYT{r58he>aL65YsC(j5KkxO_WCD4v|^6vy(-f*2__`gEsJ^iccCJdlX z0z(kvf9n$I&j;XFp#Z2UzkfZZV-O%GYHd8;zvO(*0)EXEf;trZS0AmyfKK5xhf)5e zQ~yuEuajg@u4Mn}Ll@BXCJd7){J-WO`5TpmlG2U zsko;Q`nP3-3G3>2{V2q8lWF;jrL9PnIHX2;$h}|JSX7y`B_o6faOu1D@Wv zQw0BOjhD{+Fq<(ZEUW_UwvA?~MsA^~i#d zNYE_;p7hgE-`|;kD`A--wq{+mI} zwPpvFfI|!|ZI6}2!{FfH&>AqVD}!4Mc(QEPyc-upkV6dI90v|_zvx|0H?OwF3)?7E zIJ+Ileu>j3dinAh_?-RMyB&iMt@yy&q`%Z~+wlgd)pGCH(H|=1(PDBELXPJD&HmfZ zA3uNo{P9M&BTFw8!*{KTv8-bqf*uvY(sh1HV35_ZFhvb#&9Tsv`}V?@ea^Vby_V;S z<%0pabMSwS64;A`?D^}J9H~yl@+(>Coc+qJjBj$V{+I`0CQCA2s-6|zuH4=EODu!Z z;h(Eh2O%b%=CdR<;%hhT_6(MurrUUO%f-sE_%X^O{30HYwf0Z-MsRSh1|PkoU6F~O z8SrpA%vahaGD=Csgf(WzFTUI-Ctya6FR6p;!<}6TZ=q4jb4e6ZA@#RY9>z0|SRIxM zea>ge=L;va_jL<=PKDKD`H$Yqitn=oTWocv8OCF%Z5lUp^&0}dePNh~{Qg}vCtjC> z{e{uYG^-Y$MNzN8ZoHtBbVWi#YSts-%b4|1d8gyWx>scpiML7Mm(PW!vjjsritp>G z-u$oS01G9d8ca^TP-M1YaYQ^FkI6Yu#St0wrL^+aXOX$U<8=)oq%JxJN!}OVhj;G9KY-+z-#5WbSLguNxAon1kLGQ(nD2_rCO>Zb zYtb%dg!)}?Gt3Y+l|jfp;8@DNXtU4b_(-JU)Ifi>@N)JTt9f$0lzWqGpOpBNcpxyC za3a#9Jkp7@Ls-sQc{SdfoSn-2%fur6h*3mz!CH*DW0B&Wd_CIZx4Scy(C;}XQUb5c z)(-D!(VQwycQcOR8Ny_1>8S3Jm6@)bH02#NzH@k4D2K({Bx?5Tdi7HtmGI69jW5St zY|R*_1W+!i?%$pXg%1~moOr^%Sp- z{)lQvP?0Ix4;fUrvD-EtA?{#XaHRcoyPDjz4HCnv8myiw_10Nv!qY~}Ivt|2-ud`{ zs{p}mI=dl>n>JHj_V7d6&GIg)vfde(7jwkv&Y_|3yd=?D^LiV&dSXP)6O3|cKS#qZ ziq;b>$n(qULneWJ`FXN-YcK zIR{PcSwYB82ab*CR6Ijn;SSaj^9^F$D>7wu{3p&M$*;p^G8mIK!8gi4#NAKMHId6eF!+41Nd zrsn7F225-j!&}g?G>B?3#tofwOV6;?u)FioLijVgy?BY8%cT3p|Kqvdd1_Zv4$gq_ zps=)|w?N6hENo#EBtJC=D=H;SLNzlzhue};dl*9}dA+wDfw9sZAj{*BT>bDk4^L{j zLTaJE&Ra>m=@Rc;3z1AFNGB`TbP<0JzYUsjXhwuLDX3gC67SPS0L1k-3yb?GIiDGw zMhxM+yvu0B(y8BzD#LPgcVIFTbJrGV8K3kbLw6n~j8v^|(C)>$$-r_ULVn;7_ob@~ zjxZc#tnt2oRt(i{Koysa^G*WIOw_6?6Evhhd?`*ZtW`G4NqYs2*)&CO+~k6bU;#VJ zoMU*?SQkuS$j30qHBhw=JLewRLhnX3^pUYtOGSU6CFP+tmfHB;$eHG_~xkB zQ4#&B&2)s8y7dz*`d~nTGFHtREFoKaL(~u~{GbDLo%&VA-PTe^$x6~CZxrr zje)u;^;7DJ4x~sjOd#9P01SwIabi7B4bk80@#?ON?lLkmh}YRu zMDpmk)8SUh_A1tJcQ=y;KKX4e--DA$9p$pkJF44YV{}*KXD6i*k@wesBvv}^h3y!) z5Nv(*gZaof)uuJj=`-G`-T2EoHjg2D{J#F`z=oTkVe}I7KH+rckYCrVi=u&!vue0Z zjurjPSJLh1TcW=)+HeFUCDr!h2jH6XoZH%7Rw4`KTVM7dJ)*94*Z!?ZH0!HJWd{hB z#4;d#~}3(G1x1|0PMgy0o$d~@n5 z`}_~^agvjO(vgeV2X7cb|7+Y0NhYK%#Kz9LBv!QYj}`-CfYL9$8H7ho+?fh8OWEGF z6i$W-<-ubD#-V0neCvSGVF$!SvH(6Q*v?R5=Rkndl1n*n!w*H0Sn}~rW4KljDJpKA z?-n4FjRjUj?H$Zz`F1#I07$K5T&SrgpZ=puirlN*q8?*mb@|o>MCxz?UnMxFRs8?p zOGAV@poLJf2dR7bWCrjwU@xVJ!9{+j8Q7m=4J5 0;) { + // TODO needed? + for (var i = layer_uis.length; i-- > 0;) { // quick hack if (i >= layers.length) { layer_uis[i].dom.style.display = 'none'; @@ -411,17 +399,15 @@ function LayerCabinet(data, dispatcher) { } // console.log('yoz', states.get(i).value); - layer_uis[i].setState(layers[i], layer_store.get(i)); + layer_uis[i].setState(layers[i]); // layer_uis[i].setState('layers'+':'+i); - layer_uis[i].repaint(s); + layer_uis[i].repaint(time); } - visible_layers = layer_uis.length; - } this.repaint = repaint; - this.setState(layer_store); + this.updateState(); this.scrollTo = function(x) { layer_scroll.scrollTop = x * (layer_scroll.scrollHeight - layer_scroll.clientHeight); diff --git a/src/layer_view.js b/src/layer_view.js index 7619dd3..971f524 100644 --- a/src/layer_view.js +++ b/src/layer_view.js @@ -1,33 +1,16 @@ var Theme = require('./theme'), - NumberUI = require('./widget/number'), - Tweens = require('./tween'), Settings = require('./settings'), - utils = require('./utils') + utils = require('./utils') // TODO ; -// TODO - tagged by index instead, work off layers. +function LayerView(context, channelName) { -function LayerView(layer, dispatcher) { var dom = document.createElement('div'); - var label = document.createElement('span'); label.style.cssText = 'font-size: 12px; padding: 4px;'; - var dropdown = document.createElement('select'); - var option; - dropdown.style.cssText = 'font-size: 10px; width: 60px; margin: 0; float: right; text-align: right;'; - - for (var k in Tweens) { - option = document.createElement('option'); - option.text = k; - dropdown.appendChild(option); - } - - dropdown.addEventListener('change', function(e) { - dispatcher.fire('ease', layer, dropdown.value); - }); var height = (Settings.LINE_HEIGHT - 1); var keyframe_button = document.createElement('button'); @@ -35,8 +18,7 @@ function LayerView(layer, dispatcher) { keyframe_button.style.cssText = 'background: none; font-size: 12px; padding: 0px; font-family: monospace; float: right; width: 20px; height: ' + height + 'px; border-style:none; outline: none;'; // border-style:inset; keyframe_button.addEventListener('click', function(e) { - console.log('clicked:keyframing...', state.get('_value').value); - dispatcher.fire('keyframe', layer, state.get('_value').value); + context.dispatcher.fire('keyframe', channelName); }); /* @@ -51,90 +33,37 @@ function LayerView(layer, dispatcher) { button.textContent = '>'; button.style.cssText = 'font-size: 12px; padding: 1px; '; dom.appendChild(button); - - // Mute - button = document.createElement('button'); - button.textContent = 'M'; - button.style.cssText = 'font-size: 12px; padding: 1px; '; - dom.appendChild(button); - - // Solo - button = document.createElement('button'); - button.textContent = 'S'; - button.style.cssText = 'font-size: 12px; padding: 1px; '; - dom.appendChild(button); */ - var number = new NumberUI(layer, dispatcher); - - number.onChange.do(function(value, done) { - state.get('_value').value = value; - dispatcher.fire('value.change', layer, value, done); - }); - - utils.style(number.dom, { - float: 'right' - }); - dom.appendChild(label); dom.appendChild(keyframe_button); - dom.appendChild(number.dom); - dom.appendChild(dropdown); - - dom.style.cssText = 'margin: 0px; border-bottom:1px solid ' + Theme.b + '; top: 0; left: 0; height: ' + (Settings.LINE_HEIGHT - 1 ) + 'px; color: ' + Theme.c; - this.dom = dom; - - this.repaint = repaint; - var state; - this.setState = function(l, s) { - layer = l; - state = s; - - var tmp_value = state.get('_value'); - if (tmp_value.value === undefined) { - tmp_value.value = 0; - } - - number.setValue(tmp_value.value); - label.textContent = state.get('name').value; + dom.style.cssText = 'margin: 0px; border-bottom:1px solid ' + Theme.b + '; top: 0; left: 0; height: ' + height + 'px; line-height: ' + height + 'px; color: ' + Theme.c; + this.dom = dom; - repaint(); - }; - function repaint(s) { + var repaint = function repaint(time) { - dropdown.style.opacity = 0; - dropdown.disabled = true; keyframe_button.style.color = Theme.b; - // keyframe_button.disabled = false; - // keyframe_button.style.borderStyle = 'solid'; - var tween = null; - var o = utils.timeAtLayer(layer, s); + if (time == null) return; - if (!o) return; + if (context.controller.hasKeyframe(channelName, time)) { - if (o.can_tween) { - dropdown.style.opacity = 1; - dropdown.disabled = false; - // if (o.tween) - dropdown.value = o.tween ? o.tween : 'none'; - if (dropdown.value === 'none') dropdown.style.opacity = 0.5; - } - - if (o.keyframe) { keyframe_button.style.color = Theme.c; - // keyframe_button.disabled = true; - // keyframe_button.style.borderStyle = 'inset'; } - state.get('_value').value = o.value; - number.setValue(o.value); - number.paint(); + }; + + this.repaint = repaint; - dispatcher.fire('target.notify', layer.name, o.value); - } + this.setState = function(name) { + + channelName = name; + label.textContent = name; + + repaint(); + }; } diff --git a/src/save_format.js b/src/save_format.js deleted file mode 100644 index d299808..0000000 --- a/src/save_format.js +++ /dev/null @@ -1,55 +0,0 @@ -/* Layer Schema */ -/* -var layer_1 = [ - { - name: 'abc', - props: { - min: - max: - step: - real_step: - }, - values: [ - [t, v, ''], - {time: t, value: v, tween: bla, _color: 'red'}, - {time: t, value: v, tween: bla}, - {time: t, value: v, tween: bla}, - {time: t, value: v, tween: bla}, - {time: t, value: v}, - ], - ui: { - mute: true, // mute - solo: true, - - }, - tmp: { - value: value, - _color: - } - } - ,... -] currently_playing, scale. -*/ - -/* Timeline Data Schema */ - -var sample = { - version: '1.2.0', - modified: new Date, - - name: 'sample', - title: 'Sample Title', - - ui: { - current_time: 1, - duration: 100, - - position: '0:0:0', - bounds: '10 10 100 100', - snap: 'full | left-half | top-half | right-half | bottom-half' - }, - - layers: [{ - - }] -}; \ No newline at end of file diff --git a/src/timeline_panel.js b/src/timeline_panel.js index d8a7b67..fc48e6a 100644 --- a/src/timeline_panel.js +++ b/src/timeline_panel.js @@ -2,9 +2,7 @@ var Settings = require('./settings'), Theme = require('./theme'), utils = require('./utils'), - proxy_ctx = utils.proxy_ctx, - Tweens = require('./tween'), - handleDrag = require('./handle_drag'); + proxy_ctx = utils.proxy_ctx; var LINE_HEIGHT = Settings.LINE_HEIGHT, @@ -43,13 +41,23 @@ time_scaled(); // Timeline Panel /**************************/ -function TimelinePanel(data, dispatcher) { +function TimelinePanel(context) { + + var dispatcher = context.dispatcher; + + var scrollTop = 0, scrollLeft = 0, SCROLL_HEIGHT; var dpr = window.devicePixelRatio; var canvas = document.createElement('canvas'); - - var scrollTop = 0, scrollLeft = 0, SCROLL_HEIGHT; - var layers = data.get('layers').value; + + var layers; + + this.updateState = function() { + layers = Object.keys( context.controller.channelKeyTimes ); + repaint(); + }; + + this.updateState(); this.scrollTo = function(s, y) { scrollTop = s * Math.max(layers.length * LINE_HEIGHT - SCROLL_HEIGHT, 0); @@ -81,49 +89,7 @@ function TimelinePanel(data, dispatcher) { var needsRepaint = false; var renderItems = []; - function EasingRect(x1, y1, x2, y2, frame, frame2, values, layer, j) { - var self = this; - - this.path = function() { - ctx_wrap.beginPath() - .rect(x1, y1, x2-x1, y2-y1) - .closePath(); - }; - - this.paint = function() { - this.path(); - ctx.fillStyle = frame._color; - ctx.fill(); - }; - - this.mouseover = function() { - canvas.style.cursor = 'pointer'; // pointer move ew-resize - }; - - this.mouseout = function() { - canvas.style.cursor = 'default'; - }; - - this.mousedrag = function(e) { - var t1 = x_to_time(x1 + e.dx); - t1 = Math.max(0, t1); - // TODO limit moving to neighbours - frame.time = t1; - - var t2 = x_to_time(x2 + e.dx); - t2 = Math.max(0, t2); - frame2.time = t2; - - dispatcher.fire('time.update', t1); - }; - } - - function Diamond(frame, y) { - var x, y2; - - x = time_to_x(frame.time); - y2 = y + LINE_HEIGHT * 0.5 - DIAMOND_SIZE / 2; - + function Diamond(x, y) { var self = this; var isOver = false; @@ -131,10 +97,10 @@ function TimelinePanel(data, dispatcher) { this.path = function(ctx_wrap) { ctx_wrap .beginPath() - .moveTo(x, y2) - .lineTo(x + DIAMOND_SIZE / 2, y2 + DIAMOND_SIZE / 2) - .lineTo(x, y2 + DIAMOND_SIZE) - .lineTo(x - DIAMOND_SIZE / 2, y2 + DIAMOND_SIZE / 2) + .moveTo(x, y) + .lineTo(x + DIAMOND_SIZE / 2, y + DIAMOND_SIZE / 2) + .lineTo(x, y + DIAMOND_SIZE) + .lineTo(x - DIAMOND_SIZE / 2, y + DIAMOND_SIZE / 2) .closePath(); }; @@ -164,8 +130,7 @@ function TimelinePanel(data, dispatcher) { this.mousedrag = function(e) { var t = x_to_time(x + e.dx); t = Math.max(0, t); - // TODO limit moving to neighbours - frame.time = t; + // TODO implement dispatcher.fire('time.update', t); // console.log('frame', frame); // console.log(s, format_friendly_seconds(s), this); @@ -180,7 +145,9 @@ function TimelinePanel(data, dispatcher) { function drawLayerContents() { - renderItems = []; + + renderItems.length = 0; + // horizontal Layer lines for (i = 0, il = layers.length; i <= il; i++) { ctx.strokeStyle = Theme.b; @@ -193,63 +160,28 @@ function TimelinePanel(data, dispatcher) { .lineTo(width, y) .stroke(); } - - - var frame, frame2, j; - // Draw Easing Rects + // Draw Diamonds for (i = 0; i < il; i++) { // check for keyframes var layer = layers[i]; - var values = layer.values; + var times = context.controller.channelKeyTimes[ layer ]; y = i * LINE_HEIGHT; - for (j = 0; j < values.length - 1; j++) { - frame = values[j]; - frame2 = values[j + 1]; - - // Draw Tween Rect - x = time_to_x(frame.time); - x2 = time_to_x(frame2.time); - - if (!frame.tween || frame.tween == 'none') continue; - - var y1 = y + 2; - var y2 = y + LINE_HEIGHT - 2; - - renderItems.push(new EasingRect(x, y1, x2, y2, frame, frame2)); - - // // draw easing graph - // var color = parseInt(frame._color.substring(1,7), 16); - // color = 0xffffff ^ color; - // color = color.toString(16); // convert to hex - // color = '#' + ('000000' + color).slice(-6); - - // ctx.strokeStyle = color; - // var x3; - // ctx.beginPath(); - // ctx.moveTo(x, y2); - // var dy = y1 - y2; - // var dx = x2 - x; - - // for (x3=x; x3 < x2; x3++) { - // ctx.lineTo(x3, y2 + Tweens[frame.tween]((x3 - x)/dx) * dy); - // } - // ctx.stroke(); - } + // TODO use upper and lower bound here - for (j = 0; j < values.length; j++) { - // Dimonds - frame = values[j]; - renderItems.push(new Diamond(frame, y)); + for (var j = 0; j < times.length; j++) { + + renderItems.push(new Diamond( + time_to_x( times[ j ] ), + y + LINE_HEIGHT * 0.5 - DIAMOND_SIZE / 2)); } } - // render items - var item; + // render for (i = 0, il = renderItems.length; i < il; i++) { - item = renderItems[i]; + var item = renderItems[i]; item.paint(ctx_wrap); } } @@ -265,10 +197,8 @@ function TimelinePanel(data, dispatcher) { function drawScroller() { var w = width; - var totalTime = data.get('ui:totalTime').value; - var pixels_per_second = data.get('ui:timeScale').value; - - var viewTime = w / pixels_per_second; + var totalTime = context.totalTime; + var viewTime = w / time_scale; var k = w / totalTime; // pixels per seconds @@ -282,7 +212,7 @@ function TimelinePanel(data, dispatcher) { scroller.grip_length = viewTime * k; var h = TOP_SCROLL_TRACK; - scroller.left = data.get('ui:scrollTime').value * k; + scroller.left = context.scrollTime * k; scroller.left = Math.min(Math.max(0, scroller.left), w - scroller.grip_length); ctx.beginPath(); @@ -314,15 +244,16 @@ function TimelinePanel(data, dispatcher) { } - function setTimeScale() { + function setTimeScale(v) { - var v = data.get('ui:timeScale').value; if (time_scale !== v) { time_scale = v; time_scaled(); } } + this.setTimeScale = setTimeScale; + var over = null; var mousedownItem = null; @@ -383,10 +314,10 @@ function TimelinePanel(data, dispatcher) { return; } - setTimeScale(); + setTimeScale(context.timeScale); - current_frame = data.get('ui:currentTime').value; - frame_start = data.get('ui:scrollTime').value; + current_frame = context.currentTime; + frame_start = context.scrollTime; /**************************/ // background @@ -579,7 +510,7 @@ function TimelinePanel(data, dispatcher) { }); var mousedown2 = false, mouseDownThenMove = false; - handleDrag(canvas, function down(e) { + utils.handleDrag(canvas, function down(e) { mousedown2 = true; pointer = { x: e.offsetx, @@ -608,19 +539,14 @@ function TimelinePanel(data, dispatcher) { } ); - this.setState = function(state) { - layers = state.value; - repaint(); - }; - /** Handles dragging for scroll bar **/ var draggingx; - handleDrag(canvas, function down(e) { + utils.handleDrag(canvas, function down(e) { draggingx = scroller.left; }, function move(e) { - data.get('ui:scrollTime').value = Math.max(0, (draggingx + e.dx) / scroller.k); + context.scrollTime = Math.max(0, (draggingx + e.dx) / scroller.k); repaint(); }, function up() { }, function(e) { diff --git a/src/timeliner.js b/src/timeliner.js index 1954f57..9fb7237 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -17,8 +17,7 @@ var undo = require('./undo'), saveToFile = utils.saveToFile, openAs = utils.openAs, STORAGE_PREFIX = utils.STORAGE_PREFIX, - ScrollBar = require('./widget/scrollbar'), - DataStore = require('./datastore') + ScrollBar = require('./widget/scrollbar') ; var Z_INDEX = 999; @@ -27,8 +26,6 @@ function LayerProp(name) { this.name = name; this.values = []; - this._value = 0; - this._color = '#' + (Math.random() * 0xffffff | 0).toString(16); /* this.max @@ -37,98 +34,82 @@ function LayerProp(name) { */ } -function Timeliner(target) { - // Aka Layer Manager / Controller +function Timeliner( controller ) { - // Should persist current time too. - var data = new DataStore(); - var layer_store = data.get('layers'); - var layers = layer_store.value; + var dispatcher = new Dispatcher(); - window._data = data; + if ( ! controller ) { - var dispatcher = new Dispatcher(); + controller = new Timeliner.Controller(); - var timeline = new TimelinePanel(data, dispatcher); - var layer_panel = new LayerCabinet(data, dispatcher); + } - var undo_manager = new UndoManager(dispatcher); + controller.timeliner = this; + controller.init( this ); - setTimeout(function() { - // hack! - undo_manager.save(new UndoState(data, 'Loaded'), true); - }); + var context = { - dispatcher.on('keyframe', function(layer, value) { - var index = layers.indexOf(layer); + totalTime: 20.0, + timeScale: 6, - var t = data.get('ui:currentTime').value; - var v = utils.findTimeinLayer(layer, t); + currentTime: 0.0, + scrollTime: 0.0, - // console.log(v, '...keyframe index', index, utils.format_friendly_seconds(t), typeof(v)); - // console.log('layer', layer, value); + dispatcher: dispatcher, - if (typeof(v) === 'number') { - layer.values.splice(v, 0, { - time: t, - value: value, - _color: '#' + (Math.random() * 0xffffff | 0).toString(16) - }); + controller: controller - undo_manager.save(new UndoState(data, 'Add Keyframe')); - } else { - console.log('remove from index', v); - layer.values.splice(v.index, 1); + }; - undo_manager.save(new UndoState(data, 'Remove Keyframe')); - } + window.dbgTimelinerContext = context; // DEBUG - repaintAll(); - }); + var timeline = new TimelinePanel(context); + var layer_panel = new LayerCabinet(context); - dispatcher.on('keyframe.move', function(layer, value) { - undo_manager.save(new UndoState(data, 'Move Keyframe')); + var undo_manager = new UndoManager(dispatcher); +/* + setTimeout(function() { + // hack! + undo_manager.save(new UndoState(data, 'Loaded'), true); }); +*/ + dispatcher.on('keyframe', function(channelName) { + var time = context.currentTime; - // dispatcher.fire('value.change', layer, me.value); - dispatcher.on('value.change', function(layer, value, dont_save) { - var t = data.get('ui:currentTime').value; + if ( ! controller.hasKeyframe( channelName, time ) ) { - var v = utils.findTimeinLayer(layer, t); + controller.setKeyframe( channelName, time ); - console.log(v, 'value.change', layer, value, utils.format_friendly_seconds(t), typeof(v)); - if (typeof(v) === 'number') { - layer.values.splice(v, 0, { - time: t, - value: value, - _color: '#' + (Math.random() * 0xffffff | 0).toString(16) - }); - if (!dont_save) undo_manager.save(new UndoState(data, 'Add value')); +// undo_manager.save(new UndoState(data, 'Add Keyframe')); } else { - v.object.value = value; - if (!dont_save) undo_manager.save(new UndoState(data, 'Update value')); - } - repaintAll(); - }); + controller.delKeyframe( channelName, time ); - dispatcher.on('ease', function(layer, ease_type) { - var t = data.get('ui:currentTime').value; - var v = utils.timeAtLayer(layer, t); - // console.log('Ease Change > ', layer, value, v); - if (v && v.entry) { - v.entry.tween = ease_type; +// undo_manager.save(new UndoState(data, 'Remove Keyframe')); } - undo_manager.save(new UndoState(data, 'Add Ease')); + repaintAll(); // TODO repaint one channel would be enough - repaintAll(); }); + dispatcher.on('keyframe.move', function(layer, value) { +// undo_manager.save(new UndoState(data, 'Move Keyframe')); + }); + + var start_play = null, played_from = 0; // requires some more tweaking + var setCurrentTime = function setCurrentTime(value) { + var time = Math.min(Math.max(value, 0), context.totalTime); + context.currentTime = time; + controller.setDisplayTime( time ); + + if (start_play) start_play = performance.now() - value * 1000; + repaintAll(); + } + dispatcher.on('controls.toggle_play', function() { if (start_play) { pausePlaying(); @@ -150,7 +131,7 @@ function Timeliner(target) { function startPlaying() { // played_from = timeline.current_frame; - start_play = performance.now() - data.get('ui:currentTime').value * 1000; + start_play = performance.now() - context.currentTime * 1000; layer_panel.setControlStatus(true); // dispatcher.fire('controls.status', true); } @@ -166,41 +147,37 @@ function Timeliner(target) { setCurrentTime(0); }); - var currentTimeStore = data.get('ui:currentTime'); dispatcher.on('time.update', setCurrentTime); - function setCurrentTime(value) { - currentTimeStore.value = value; - - if (start_play) start_play = performance.now() - value * 1000; - repaintAll(); - // layer_panel.repaint(s); - } - dispatcher.on('target.notify', function(name, value) { - if (target) target[name] = value; + console.log(name, "=", value); + //if (target) target[name] = value; }); dispatcher.on('update.scale', function(v) { console.log('range', v); - data.get('ui:timeScale').value = v; - // timeline.setTimeScale(v); + context.timeScale = v; + timeline.setTimeScale(v); timeline.repaint(); }); // handle undo / redo dispatcher.on('controls.undo', function() { +/* var history = undo_manager.undo(); data.setJSONString(history.state); updateState(); +*/ }); dispatcher.on('controls.redo', function() { +/* var history = undo_manager.redo(); data.setJSONString(history.state); updateState(); +*/ }); /* @@ -215,7 +192,7 @@ function Timeliner(target) { setCurrentTime(t); - if (t > data.get('ui:totalTime').value) { + if (t > context.totalTime) { // simple loop start_play = performance.now(); } @@ -244,6 +221,7 @@ function Timeliner(target) { */ function save(name) { +/* if (!name) name = 'autosave'; var json = data.getJSONString(); @@ -254,19 +232,20 @@ function Timeliner(target) { } catch (e) { console.log('Cannot save', name, json); } +*/ } function saveAs(name) { - if (!name) name = data.get('name').value; + if (!name) name = context.name; name = prompt('Pick a name to save to (localStorage)', name); if (name) { - data.data.name = name; + context.name = name; save(name); } } function saveSimply() { - var name = data.get('name').value; + var name = context.name; if (name) { save(name); } else { @@ -275,52 +254,48 @@ function Timeliner(target) { } function exportJSON() { - var json = data.getJSONString(); - var ret = prompt('Hit OK to download otherwise Copy and Paste JSON', json); - console.log(JSON.stringify(data.data, null, '\t')); - if (!ret) return; + var structs = controller.serialize(); +// var ret = prompt('Hit OK to download otherwise Copy and Paste JSON'); +// if (!ret) { +// console.log(JSON.stringify(structs, null, '\t')); +// return; +// } - // make json downloadable - json = data.getJSONString('\t'); - var fileName = 'timeliner-test' + '.json'; + var fileName = 'animation.json'; - saveToFile(json, fileName); - } + saveToFile(JSON.stringify(structs, null, '\t'), fileName); - function loadJSONString(o) { - // should catch and check errors here - var json = JSON.parse(o); - load(json); } - function load(o) { - data.setJSON(o); - // - if (data.getValue('ui') === undefined) { - data.setValue('ui', { - currentTime: 0, - totalTime: 20, - scrollTime: 0, - timeScale: 40 - }); - } + function load(structs) { - undo_manager.clear(); - undo_manager.save(new UndoState(data, 'Loaded'), true); + controller.deserialize(structs); + + // TODO reset context + +// undo_manager.clear(); +// undo_manager.save(new UndoState(data, 'Loaded'), true); updateState(); + + } + + function loadJSONString(o) { + // should catch and check errors here + var json = JSON.parse(o); + load(json); } function updateState() { - layers = layer_store.value; // FIXME: support Arrays - layer_panel.setState(layer_store); - timeline.setState(layer_store); + layer_panel.updateState(); + timeline.updateState(); repaintAll(); } function repaintAll() { + var layers = Object.keys( context.controller.channelKeyTimes ); var content_height = layers.length * Settings.LINE_HEIGHT; scrollbar.setLength(Settings.TIMELINE_SCROLL_HEIGHT / content_height); @@ -420,7 +395,7 @@ function Timeliner(target) { var title_bar = document.createElement('span'); pane_title.appendChild(title_bar); - title_bar.innerHTML = 'Timeliner ' + package_json.version; + title_bar.innerHTML = package_json.description + " " + package_json.version; pane_title.appendChild(title_bar); var top_right_bar = document.createElement('div'); @@ -448,12 +423,12 @@ function Timeliner(target) { lineHeight: '22px', bottom: '0', // padding: '2px', - background: Theme.a, fontSize: '11px' }; style(pane_status, footer_styles, { borderTop: '1px solid ' + Theme.b, + background: Theme.a, }); pane.appendChild(div); @@ -461,19 +436,19 @@ function Timeliner(target) { pane.appendChild(pane_title); var label_status = document.createElement('span'); - label_status.textContent = 'hello!'; + label_status.textContent = 'Hello!'; label_status.style.marginLeft = '10px'; - this.setStatus = function(text) { + dispatcher.on('status', function(text) { label_status.textContent = text; - }; + }); + dispatcher.on('state:save', function(description) { dispatcher.fire('status', description); save('autosave'); }); - dispatcher.on('status', this.setStatus); var bottom_right = document.createElement('div'); style(bottom_right, footer_styles, { @@ -507,7 +482,7 @@ function Timeliner(target) { pane_status.appendChild(bottom_right); - /**/ +/* // zoom in var zoom_in = new IconButton(12, 'zoom_in', 'zoom in', dispatcher); // zoom out @@ -549,6 +524,7 @@ function Timeliner(target) { style(trash.dom, button_styles, { marginRight: '2px' }); bottom_right.appendChild(trash.dom); +*/ // pane_status.appendChild(document.createTextNode(' | TODO ')); @@ -692,7 +668,7 @@ function Timeliner(target) { layers = layer_store.value; layers.push(layer); - layer_panel.setState(layer_store); + layer_panel.updateState(); } this.addLayer = addLayer; @@ -701,34 +677,6 @@ function Timeliner(target) { timeline = t; }; - function getValueRanges(ranges, interval) { - interval = interval ? interval : 0.15; - ranges = ranges ? ranges : 2; - - // not optimized! - var t = data.get('ui:currentTime').value; - - var values = []; - - for (var u = -ranges; u <= ranges; u++) { - // if (u == 0) continue; - var o = {}; - - for (var l = 0; l < layers.length; l++) { - var layer = layers[l]; - var m = utils.timeAtLayer(layer, t + u * interval); - o[layer.name] = m.value; - } - - values.push(o); - - } - - return values; - } - - this.getValues = getValueRanges; - (function DockingWindow() { "use strict"; @@ -1083,4 +1031,85 @@ function Timeliner(target) { } +Timeliner.Controller = function ControllerInterface() { + + this.time = 0; + this.timeliner = null; + this.channelKeyTimes = {}; + +}; + +Timeliner.Controller.prototype = { + + constructor: Timeliner.Controller, + + init: function( timeliner ) { + + this.timeliner = timeliner; + + this.channelKeyTimes[ 'test1' ] = []; + this.channelKeyTimes[ 'test2' ] = []; + + }, + + serialize: function() { + + return this.channelKeyTimes; + + }, + + deserialize: function(structs) { + + this.channelKeyTimes = structs; + + }, + + setDisplayTime: function( time ) { + + //console.log( "setDisplayTime(%f)", time ); + this.time = time; + + }, + + setKeyframe: function( channelName, time ) { + + console.log( "setKeyframe('%s',%f)", channelName, time ); + + var keyTimes = this.channelKeyTimes[ channelName ]; + + keyTimes.push( time ); + keyTimes.sort(); + + }, + + delKeyframe: function( channelName, time ) { + + console.log( "delKeyframe('%s',%f)", channelName, time ); + + var keyTimes = this.channelKeyTimes[ channelName ]; + + var index = keyTimes.indexOf( time ); // TODO binary search + + if ( index !== -1 ) { + + keyTimes[ index ] = keyTimes[ keyTimes.length - 1 ]; + keyTimes.pop(); + keyTimes.sort(); + + } + + }, + + hasKeyframe: function( channelName, time ) { + + var keyTimes = this.channelKeyTimes[ channelName ]; + return keyTimes.indexOf( time ) >= 0; // TODO binary search + + } + +}; + + + + window.Timeliner = Timeliner; diff --git a/src/tween.js b/src/tween.js deleted file mode 100644 index 1f953d2..0000000 --- a/src/tween.js +++ /dev/null @@ -1,24 +0,0 @@ -/**************************/ -// Tweens -/**************************/ - -var Tweens = { - none: function(k) { - return 0; - }, - linear: function(k) { - return k; - }, - quadEaseIn: function(k) { - return k * k; - }, - quadEaseOut: function(k) { - return - k * ( k - 2 ); - }, - quadEaseInOut: function(k) { - if ( ( k *= 2 ) < 1 ) return 0.5 * k * k; - return - 0.5 * ( --k * ( k - 2 ) - 1 ); - } -}; - -module.exports = Tweens; \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 1d4cfb9..6339776 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,3 @@ -var - Tweens = require('./tween'); - module.exports = { STORAGE_PREFIX: 'timeliner-', Z_INDEX: 999, @@ -8,15 +5,118 @@ module.exports = { saveToFile: saveToFile, openAs: openAs, format_friendly_seconds: format_friendly_seconds, - findTimeinLayer: findTimeinLayer, - timeAtLayer: timeAtLayer, - proxy_ctx: proxy_ctx + proxy_ctx: proxy_ctx, + handleDrag: handleDrag }; /**************************/ // Utils /**************************/ +function handleDrag(element, ondown, onmove, onup, down_criteria) { + var pointer = null; + var bounds = element.getBoundingClientRect(); + + element.addEventListener('mousedown', onMouseDown); + + function onMouseDown(e) { + handleStart(e); + + if (down_criteria && !down_criteria(pointer)) { + pointer = null; + return; + } + + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + + ondown(pointer); + + e.preventDefault(); + } + + function onMouseMove(e) { + handleMove(e); + pointer.moved = true; + onmove(pointer); + } + + function handleStart(e) { + bounds = element.getBoundingClientRect(); + var currentx = e.clientX, currenty = e.clientY; + pointer = { + startx: currentx, + starty: currenty, + x: currentx, + y: currenty, + dx: 0, + dy: 0, + offsetx: currentx - bounds.left, + offsety: currenty - bounds.top, + moved: false + }; + } + + function handleMove(e) { + bounds = element.getBoundingClientRect(); + var currentx = e.clientX, + currenty = e.clientY, + offsetx = currentx - bounds.left, + offsety = currenty - bounds.top; + pointer.x = currentx; + pointer.y = currenty; + pointer.dx = e.clientX - pointer.startx; + pointer.dy = e.clientY - pointer.starty; + pointer.offsetx = offsetx; + pointer.offsety = offsety; + } + + function onMouseUp(e) { + handleMove(e); + onup(pointer); + pointer = null; + + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + } + + element.addEventListener('touchstart', onTouchStart); + + function onTouchStart(te) { + + if (te.touches.length == 1) { + + var e = te.touches[0]; + if (down_criteria && !down_criteria(e)) return; + te.preventDefault(); + handleStart(e); + ondown(pointer); + } + + element.addEventListener('touchmove', onTouchMove); + element.addEventListener('touchend', onTouchEnd); + } + + function onTouchMove(te) { + var e = te.touches[0]; + onMouseMove(e); + } + + function onTouchEnd(e) { + // var e = e.touches[0]; + onMouseUp(e); + element.removeEventListener('touchmove', onTouchMove); + element.removeEventListener('touchend', onTouchEnd); + } + + + this.release = function() { + element.removeEventListener('mousedown', onMouseDown); + element.removeEventListener('touchstart', onTouchStart); + }; +} + function style(element, var_args) { for (var i = 1; i < arguments.length; ++i) { var styles = arguments[i]; @@ -128,117 +228,6 @@ function format_friendly_seconds(s, type) { return str; } -// get object at time -function findTimeinLayer(layer, time) { - var values = layer.values; - var i, il; - - // TODO optimize by checking time / binary search - - for (i=0, il=values.length; i time) { - return i; - } - } - - return i; -} - - -function timeAtLayer(layer, t) { - // Find the value of layer at t seconds. - // this expect layer to be sorted - // not the most optimized for now, but would do. - - var values = layer.values; - var i, il, entry, prev_entry; - - il = values.length; - - // can't do anything - if (il === 0) return; - - // find boundary cases - entry = values[0]; - if (t < entry.time) { - return { - value: entry.value, - can_tween: false, // cannot tween - keyframe: false // not on keyframe - }; - } - - for (i=0; i 1, - value: entry.value, - keyframe: true - }; - } - return { - // index: i, - entry: entry, - tween: entry.tween, - can_tween: il > 1, - value: entry.value, - keyframe: true // il > 1 - }; - } - if (t < entry.time) { - // possibly a tween - if (!prev_entry.tween) { // or if value is none - return { - value: prev_entry.value, - tween: false, - entry: prev_entry, - can_tween: true, - keyframe: false - }; - } - - // calculate tween - var time_diff = entry.time - prev_entry.time; - var value_diff = entry.value - prev_entry.value; - var tween = prev_entry.tween; - - var dt = t - prev_entry.time; - var k = dt / time_diff; - var new_value = prev_entry.value + Tweens[tween](k) * value_diff; - - return { - entry: prev_entry, - value: new_value, - tween: prev_entry.tween, - can_tween: true, - keyframe: false - }; - } - } - // time is after all entries - return { - value: entry.value, - can_tween: false, - keyframe: false - }; - -} - - function proxy_ctx(ctx) { // Creates a proxy 2d context wrapper which // allows the fluent / chaining API. diff --git a/src/widget/icon_button.js b/src/widget/icon_button.js index bcfeaf3..852e0f1 100644 --- a/src/widget/icon_button.js +++ b/src/widget/icon_button.js @@ -131,7 +131,7 @@ function IconButton(size, icon, tooltip, dp) { ctx.shadowOffsetY = 1 * dpr; me.draw(); - if (tooltip && dp) dp.fire('status', 'button: ' + tooltip); + if (tooltip && dp) dp.fire('status', tooltip); }); button.addEventListener('mousedown', function() { diff --git a/src/widget/number.js b/src/widget/number.js index 010e2d6..4f47f50 100644 --- a/src/widget/number.js +++ b/src/widget/number.js @@ -1,7 +1,7 @@ var Theme = require('../theme'), Do = require('do.js'), - handleDrag = require('../handle_drag'), - style = require('../utils').style + style = require('../utils').style, + handleDrag = require('../utils').handleDrag ; /**************************/ diff --git a/test.html b/test.html index fe28dbe..ee9abc5 100644 --- a/test.html +++ b/test.html @@ -3,13 +3,6 @@ -
- + - \ No newline at end of file + diff --git a/test_ghosts.html b/test_ghosts.html deleted file mode 100644 index 175ecb1..0000000 --- a/test_ghosts.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - -
- - - - - - \ No newline at end of file diff --git a/timeliner.js b/timeliner.js deleted file mode 100644 index 9914512..0000000 --- a/timeliner.js +++ /dev/null @@ -1,3453 +0,0 @@ -(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0;) { - // quick hack - if (i >= layers.length) { - layer_uis[i].dom.style.display = 'none'; - unused_layers.push(layer_uis.pop()); - continue; - } - - // console.log('yoz', states.get(i).value); - layer_uis[i].setState(layers[i], layer_store.get(i)); - // layer_uis[i].setState('layers'+':'+i); - layer_uis[i].repaint(s); - } - - visible_layers = layer_uis.length; - - } - - this.repaint = repaint; - this.setState(layer_store); - - this.scrollTo = function(x) { - layer_scroll.scrollTop = x * (layer_scroll.scrollHeight - layer_scroll.clientHeight); - }; - - this.dom = div; - - repaint(); -} - -module.exports = LayerCabinet; -},{"./icon_button":"/Users/joshua/dev/gits/timeliner/src/icon_button.js","./settings":"/Users/joshua/dev/gits/timeliner/src/settings.js","./theme":"/Users/joshua/dev/gits/timeliner/src/theme.js","./ui/layer_view":"/Users/joshua/dev/gits/timeliner/src/ui/layer_view.js","./ui/number":"/Users/joshua/dev/gits/timeliner/src/ui/number.js","./utils":"/Users/joshua/dev/gits/timeliner/src/utils.js"}],"/Users/joshua/dev/gits/timeliner/src/save_format.js":[function(require,module,exports){ -/* Layer Schema */ -/* -var layer_1 = [ - { - name: 'abc', - props: { - min: - max: - step: - real_step: - }, - values: [ - [t, v, ''], - {time: t, value: v, tween: bla, _color: 'red'}, - {time: t, value: v, tween: bla}, - {time: t, value: v, tween: bla}, - {time: t, value: v, tween: bla}, - {time: t, value: v}, - ], - ui: { - mute: true, // mute - solo: true, - - }, - tmp: { - value: value, - _color: - } - } - ,... -] currently_playing, scale. -*/ - -/* Timeline Data Schema */ - -var sample = { - version: '1.2.0', - modified: new Date, - - name: 'sample', - title: 'Sample Title', - - ui: { - current_time: 1, - duration: 100, - - position: '0:0:0', - bounds: '10 10 100 100', - snap: 'full | left-half | top-half | right-half | bottom-half' - }, - - layers: [{ - - }] -}; -},{}],"/Users/joshua/dev/gits/timeliner/src/settings.js":[function(require,module,exports){ - -var DEFAULT_TIME_SCALE = 60; - -// Dimensions -module.exports = { - LINE_HEIGHT: 26, - DIAMOND_SIZE: 12, - MARKER_TRACK_HEIGHT: 60, - width: 600, - height: 200, - TIMELINE_SCROLL_HEIGHT: 0, - LEFT_PANE_WIDTH: 250, - time_scale: DEFAULT_TIME_SCALE // number of pixels to 1 secon, -}; -},{}],"/Users/joshua/dev/gits/timeliner/src/theme.js":[function(require,module,exports){ -module.exports = { - // photoshop colors - a: '#343434', - b: '#535353', - c: '#b8b8b8', - d: '#d6d6d6', -}; -},{}],"/Users/joshua/dev/gits/timeliner/src/timeline_panel.js":[function(require,module,exports){ -var - Settings = require('./settings'), - Theme = require('./theme'), - utils = require('./utils'), - Tweens = require('./tween'), - handleDrag = require('./handle_drag'); - - var - LINE_HEIGHT = Settings.LINE_HEIGHT, - DIAMOND_SIZE = Settings.DIAMOND_SIZE, - MARKER_TRACK_HEIGHT = Settings.MARKER_TRACK_HEIGHT, - - LEFT_PANE_WIDTH = Settings.LEFT_PANE_WIDTH, - time_scale = Settings.time_scale; - - - var frame_start = 0; // this is the current scroll position. -/* -Aka. Subdivison LOD -// Eg. 1 inch - 60s, 1 inch - 60fps, 1 inch - 6 mins -*/ -// TODO: refactor to use a nicer scale - -var subds, subd_type, subd1, subd2, subd3; - -function time_scaled() { - if (time_scale > 350) { - subds = [12, 12, 60, 'frames']; - } else if (time_scale > 250) { - subds = [6, 12, 60, 'frames']; - } else if (time_scale > 200) { - subds = [6, 6, 30, 'frames']; - } else if (time_scale > 150) { - subds = [4, 4, 20, 'frames']; - } else if (time_scale > 100) { - subds = [4, 4, 8, 'frames']; - } else if (time_scale > 90) { - subds = [4, 4, 8, 'seconds']; - } else if (time_scale > 60) { - subds = [2, 4, 8, 'seconds']; - } else if (time_scale > 40) { - subds = [1, 2, 10, 'seconds']; - } else if (time_scale > 30) { - subds = [1, 2, 10, 'seconds']; - } else if (time_scale > 10) { - subds = [1, 1, 4, 'seconds']; - } else if (time_scale > 4) { - subds = [1/5, 1/5, 1/5, 'seconds']; - } else if (time_scale > 3) { - subds = [1/10, 1/10, 1/5, 'seconds']; - } else if (time_scale > 1) { - subds = [1/20, 1/20, 1/10, 'seconds']; - } else if (time_scale >= 1) { - subds = [1/30, 1/30, 1/15, 'seconds']; - } else { // 1s per pixel - subds = [1/60, 1/60, 1/15, 'seconds']; - } - - console.log(subds); - - - subd1 = subds[0]; // big ticks / labels - subd2 = subds[1]; // medium ticks - subd3 = subds[2]; // small ticks - subd_type = subds[3]; -} - -time_scaled(); - - -/**************************/ -// Timeline Panel -/**************************/ - -function TimelinePanel(data, dispatcher) { - - var dpr = window.devicePixelRatio; - var canvas = document.createElement('canvas'); - - var scrollTop = 0, scrollLeft = 0, SCROLL_HEIGHT; - var layers = data.get('layers').value; - - this.scrollTo = function(s, y) { - scrollTop = s * Math.max(layers.length * LINE_HEIGHT - SCROLL_HEIGHT, 0); - repaint(); - }; - - this.resize = function() { - dpr = window.devicePixelRatio; - canvas.width = Settings.width * dpr; - canvas.height = Settings.height * dpr; - canvas.style.width = Settings.width + 'px'; - canvas.style.height = Settings.height + 'px'; - SCROLL_HEIGHT = Settings.height - MARKER_TRACK_HEIGHT; - }; - - this.dom = canvas; - this.resize(); - - var ctx = canvas.getContext('2d'); - - var current_frame; // currently in seconds - // var currentTime = 0; // in frames? could have it in string format (0:00:00:1-60) - - - var LEFT_GUTTER = 20; - var i, x, y, il, j; - - var needsRepaint = false; - - function repaint() { - needsRepaint = true; - } - - - function drawLayerContents() { - // horizontal Layer lines - for (i = 0, il = layers.length; i <= il; i++) { - ctx.strokeStyle = Theme.b; - ctx.beginPath(); - y = i * LINE_HEIGHT; - y = ~~y - 0.5; - - ctx.moveTo(0, y); - ctx.lineTo(width, y); - ctx.stroke(); - } - - // Draw Easing Rects - for (i = 0; i < il; i++) { - // check for keyframes - var layer = layers[i]; - var values = layer.values; - - y = i * LINE_HEIGHT; - - for (var j = 0; j < values.length - 1; j++) { - var frame = values[j]; - var frame2 = values[j + 1]; - ctx.fillStyle = frame._color; // Theme.c - - // Draw Tween Rect - x = time_to_x(frame.time); - x2 = time_to_x(frame2.time); - - if (!frame.tween || frame.tween == 'none') continue; - - var y1 = y + 2; - var y2 = y + LINE_HEIGHT - 2; - // console.log('concert', frame.time, '->', x, y2); - ctx.beginPath(); - ctx.moveTo(x, y1); - ctx.lineTo(x2, y1); - ctx.lineTo(x2, y2); - ctx.lineTo(x, y2); - ctx.closePath(); - ctx.fill(); - - // draw easing graph - var color = parseInt(frame._color.substring(1,7), 16); - color = 0xffffff ^ color; - color = color.toString(16); // convert to hex - color = '#' + ('000000' + color).slice(-6); - - ctx.strokeStyle = color; - var x3; - ctx.beginPath(); - ctx.moveTo(x, y2); - var dy = y1 - y2; - var dx = x2 - x; - - for (x3=x; x3 < x2; x3++) { - ctx.lineTo(x3, y2 + Tweens[frame.tween]((x3 - x)/dx) * dy); - } - ctx.stroke(); - } - - ctx.fillStyle = Theme.d; - ctx.strokeStyle = Theme.b; - - var j, frame; - - for (j = 0; j < values.length; j++) { - frame = values[j]; - - // Draw Diamond - x = time_to_x(frame.time); - - y2 = y + LINE_HEIGHT * 0.5 - DIAMOND_SIZE / 2; - // console.log('concert', frame.time, '->', x, y2); - ctx.beginPath(); - ctx.moveTo(x, y2); - ctx.lineTo(x + DIAMOND_SIZE / 2, y2 + DIAMOND_SIZE / 2); - ctx.lineTo(x, y2 + DIAMOND_SIZE); - ctx.lineTo(x - DIAMOND_SIZE / 2, y2 + DIAMOND_SIZE / 2); - ctx.closePath(); - ctx.fill(); - - } - - } - } - - var TOP_SCROLL_TRACK = 20; - var scroller = { - left: 0, - grip_length: 0, - k: 1 - }; - var left; - - function drawScroller() { - var w = width; - - var totalTime = data.get('ui:totalTime').value; - var pixels_per_second = data.get('ui:timeScale').value; - - var viewTime = w / pixels_per_second; - - - var k = w / totalTime; // pixels per seconds - scroller.k = k; - - // 800 / 5 = 180 - - // var k = Math.min(viewTime / totalTime, 1); - // var grip_length = k * w; - - scroller.grip_length = viewTime * k; - var h = TOP_SCROLL_TRACK; - - scroller.left = data.get('ui:scrollTime').value * k; - scroller.left = Math.min(Math.max(0, scroller.left), w - scroller.grip_length); - - ctx.beginPath(); - ctx.fillStyle = Theme.b; // 'cyan'; - ctx.rect(0, 5, w, h); - ctx.fill(); - - ctx.fillStyle = Theme.c; // 'yellow'; - - ctx.beginPath(); - ctx.rect(scroller.left, 5, scroller.grip_length, h); - ctx.fill(); - - var r = current_frame * k; - - // ctx.fillStyle = Theme.a; // 'yellow'; - // ctx.fillRect(0, 5, w, 2); - - ctx.fillStyle = 'red'; - ctx.fillRect(0, 5, r, 2); - - // ctx.strokeStyle = 'red'; - // ctx.lineWidth = 2; - // ctx.beginPath(); - // ctx.moveTo(r, 5); - // ctx.lineTo(r, 15); - // ctx.stroke(); - - } - - - function setTimeScale() { - var v = data.get('ui:timeScale').value; - if (time_scale !== v) { - time_scale = v; - time_scaled(); - } - } - - function _paint() { - if (!needsRepaint) return; - - setTimeScale(); - - current_frame = data.get('ui:currentTime').value; - frame_start = data.get('ui:scrollTime').value; - - /**************************/ - // background - - ctx.fillStyle = Theme.a; - //ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.save(); - ctx.scale(dpr, dpr); - - // - - ctx.lineWidth = 1; // .5, 1, 2 - - width = Settings.width, - height = Settings.height; - - var units = time_scale / subd1; - var offsetUnits = (frame_start * time_scale) % units; - - var count = (width - LEFT_GUTTER + offsetUnits) / units; - - // console.log('time_scale', time_scale, 'subd1', subd1, 'units', units, 'offsetUnits', offsetUnits, frame_start); - - // time_scale = pixels to 1 second (40) - // subd1 = marks per second (marks / s) - // units = pixels to every mark (40) - - // labels only - for (i = 0; i < count; i++) { - x = i * units + LEFT_GUTTER - offsetUnits; - - // vertical lines - ctx.strokeStyle = Theme.b; - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, height); - ctx.stroke(); - - ctx.fillStyle = Theme.d; - ctx.textAlign = 'center'; - - var t = (i * units - offsetUnits) / time_scale + frame_start; - t = utils.format_friendly_seconds(t, subd_type); - ctx.fillText(t, x, 38); - } - - units = time_scale / subd2; - count = (width - LEFT_GUTTER + offsetUnits) / units; - - // marker lines - main - for (i = 0; i < count; i++) { - ctx.strokeStyle = Theme.c; - ctx.beginPath(); - x = i * units + LEFT_GUTTER - offsetUnits; - ctx.moveTo(x, MARKER_TRACK_HEIGHT - 0); - ctx.lineTo(x, MARKER_TRACK_HEIGHT - 16); - ctx.stroke(); - } - - var mul = subd3 / subd2; - units = time_scale / subd3; - count = (width - LEFT_GUTTER + offsetUnits) / units; - - // small ticks - for (i = 0; i < count; i++) { - if (i % mul === 0) continue; - ctx.strokeStyle = Theme.c; - ctx.beginPath(); - x = i * units + LEFT_GUTTER - offsetUnits; - ctx.moveTo(x, MARKER_TRACK_HEIGHT - 0); - ctx.lineTo(x, MARKER_TRACK_HEIGHT - 10); - ctx.stroke(); - } - - // Encapsulate a scroll rect for the layers - ctx.save(); - ctx.translate(0, MARKER_TRACK_HEIGHT); - ctx.beginPath(); - ctx.rect(0, 0, Settings.width, SCROLL_HEIGHT); - ctx.translate(-scrollLeft, -scrollTop); - ctx.clip(); - drawLayerContents(); - ctx.restore(); - - - drawScroller(); - - // Current Marker / Cursor - ctx.strokeStyle = 'red'; // Theme.c - x = (current_frame - frame_start) * time_scale + LEFT_GUTTER; - - var txt = utils.format_friendly_seconds(current_frame); - var textWidth = ctx.measureText(txt).width; - - var base_line = MARKER_TRACK_HEIGHT- 5, half_rect = textWidth / 2 + 4; - - ctx.beginPath(); - ctx.moveTo(x, base_line); - ctx.lineTo(x, height); - ctx.stroke(); - - ctx.fillStyle = 'red'; // black - ctx.textAlign = 'center'; - ctx.beginPath(); - ctx.moveTo(x, base_line + 5); - ctx.lineTo(x + 5, base_line); - ctx.lineTo(x + half_rect, base_line); - ctx.lineTo(x + half_rect, base_line - 14); - ctx.lineTo(x - half_rect, base_line - 14); - ctx.lineTo(x - half_rect, base_line); - ctx.lineTo(x - 5, base_line); - ctx.closePath(); - ctx.fill(); - - ctx.fillStyle = 'white'; - ctx.fillText(txt, x, base_line - 4); - - ctx.restore(); - - needsRepaint = false; - - } - - function y_to_track(y) { - if (y - MARKER_TRACK_HEIGHT < 0) return -1; - return (y - MARKER_TRACK_HEIGHT + scrollTop) / LINE_HEIGHT | 0; - } - - - function x_to_time(x) { - - var units = time_scale / subd3; - - // return frame_start + (x - LEFT_GUTTER) / time_scale; - - return frame_start + ((x - LEFT_GUTTER) / units | 0) / subd3; - } - - function time_to_x(s) { - var ds = s - frame_start; - ds *= time_scale; - ds += LEFT_GUTTER; - - return ds; - } - - - var me = this; - this.repaint = repaint; - this._paint = _paint; - - repaint(); - - var mousedown = false, selection = false; - - var dragObject; - var canvasBounds; - - canvas.addEventListener('touchstart', function(ev) { - e = ev.touches[0]; - pointerStart(e); - ev.preventDefault(); - }); - - canvas.addEventListener('touchmove', function(ev) { - e = ev.touches[0]; - onMouseMove(e); - }); - - function pointerStart(e) { - canvasBounds = canvas.getBoundingClientRect(); - var mx = e.clientX - canvasBounds.left , my = e.clientY - canvasBounds.top; - - if (my <= TOP_SCROLL_TRACK) return false; - - mousedown = true; - - var track = y_to_track(my); - var s = x_to_time(mx); - - dragObject = null; - - console.log('track', track, 't', s, layers[track]); - - if (layers[track]) { - var tmp = utils.findTimeinLayer(layers[track], s); - var tmp2 = utils.timeAtLayer(layers[track], s); - - console.log('drag start', tmp, tmp2); - - if (typeof(tmp) !== 'number') dragObject = tmp; - } - - onPointerDrag(mx, my); - return true; - } - - canvas.addEventListener('mousedown', function(e) { - - var hit = pointerStart(e); - if (!hit) return; - - - e.preventDefault(); - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - }); - - canvas.addEventListener('dblclick', function(e) { - console.log('dblclick!'); - // canvasBounds = canvas.getBoundingClientRect(); - var mx = e.clientX - canvasBounds.left , my = e.clientY - canvasBounds.top; - - - var track = y_to_track(my); - var s = x_to_time(mx); - - - dispatcher.fire('keyframe', layers[track], current_frame); - - }); - - - var draggingx; - handleDrag(canvas, function down(e) { - draggingx = scroller.left; - }, function move(e) { - data.get('ui:scrollTime').value = Math.max(0, (draggingx + e.dx) / scroller.k); - repaint(); - }, function up() { - }, function(e) { - var bar = e.offsetx >= scroller.left && e.offsetx <= scroller.left + scroller.grip_length; - return e.offsety <= TOP_SCROLL_TRACK && bar; - } - ); - - function onMouseUp(e) { - // canvasBounds = canvas.getBoundingClientRect(); - var mx = e.clientX - canvasBounds.left , my = e.clientY - canvasBounds.top; - - onPointerDrag(mx, my); - if (dragObject) { - dispatcher.fire('keyframe.move'); - } - mousedown = false; - dragObject = null; - - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - } - - function onMouseMove(e) { - // canvasBounds = canvas.getBoundingClientRect(); - var mx = e.clientX - canvasBounds.left , my = e.clientY - canvasBounds.top; - - // offsetY d.getBoundingClientRect() d.offsetLeft - // console.log('...', mx, my, div.offsetLeft); - onPointerDrag(mx, my); - - } - - function onPointerDrag(x, y) { - - if (x < LEFT_GUTTER) x = LEFT_GUTTER; - if (x > width) return; - current = x; // <---- ??!??!! - - var s = x_to_time(x); - if (dragObject) { - dragObject.object.time = s; // hack! // needs reorder upon mouse up! - } else { - - } - - // Move the cursor; - dispatcher.fire('time.update', s); - - // console.log(s, format_friendly_seconds(s), this); - } - - this.setState = function(state) { - layers = state.value; - repaint(); - }; - -} - -module.exports = TimelinePanel; -},{"./handle_drag":"/Users/joshua/dev/gits/timeliner/src/handle_drag.js","./settings":"/Users/joshua/dev/gits/timeliner/src/settings.js","./theme":"/Users/joshua/dev/gits/timeliner/src/theme.js","./tween":"/Users/joshua/dev/gits/timeliner/src/tween.js","./utils":"/Users/joshua/dev/gits/timeliner/src/utils.js"}],"/Users/joshua/dev/gits/timeliner/src/timeliner.js":[function(require,module,exports){ -/* - * @author Joshua Koo http://joshuakoo.com - */ - -var undo = require('./undo'), - Dispatcher = require('./dispatcher'), - Theme = require('./theme'), - UndoManager = undo.UndoManager, - UndoState = undo.UndoState, - Settings = require('./settings'), - utils = require('./utils'), - LayerCabinet = require('./layer_cabinet'), - TimelinePanel = require('./timeline_panel'), - package_json = require('../package.json'), - IconButton = require('./icon_button'), - style = utils.style, - saveToFile = utils.saveToFile, - openAs = utils.openAs, - STORAGE_PREFIX = utils.STORAGE_PREFIX, - ScrollBar = require('./ui/scrollbar'), - DataStore = require('./datastore') - ; - -var Z_INDEX = 999; - -function LayerProp(name) { - this.name = name; - this.values = []; - - this._value = 0; - - this._color = '#' + (Math.random() * 0xffffff | 0).toString(16); - /* - this.max - this.min - this.step - */ -} - -function Timeliner(target) { - // Aka Layer Manager / Controller - - // Should persist current time too. - var data = new DataStore(); - var layer_store = data.get('layers'); - var layers = layer_store.value; - - window._data = data; - - var dispatcher = new Dispatcher(); - - var timeline = new TimelinePanel(data, dispatcher); - var layer_panel = new LayerCabinet(data, dispatcher); - - var undo_manager = new UndoManager(dispatcher); - - setTimeout(function() { - // hack! - undo_manager.save(new UndoState(data, 'Loaded'), true); - }); - - dispatcher.on('keyframe', function(layer, value) { - var index = layers.indexOf(layer); - - var t = data.get('ui:currentTime').value; - var v = utils.findTimeinLayer(layer, t); - - // console.log(v, '...keyframe index', index, utils.format_friendly_seconds(t), typeof(v)); - // console.log('layer', layer, value); - - if (typeof(v) === 'number') { - layer.values.splice(v, 0, { - time: t, - value: value, - _color: '#' + (Math.random() * 0xffffff | 0).toString(16) - }); - - undo_manager.save(new UndoState(data, 'Add Keyframe')); - } else { - console.log('remove from index', v); - layer.values.splice(v.index, 1); - - undo_manager.save(new UndoState(data, 'Remove Keyframe')); - } - - repaintAll(); - - }); - - dispatcher.on('keyframe.move', function(layer, value) { - undo_manager.save(new UndoState(data, 'Move Keyframe')); - }); - - // dispatcher.fire('value.change', layer, me.value); - dispatcher.on('value.change', function(layer, value, dont_save) { - var t = data.get('ui:currentTime').value; - - var v = utils.findTimeinLayer(layer, t); - - console.log(v, 'value.change', layer, value, utils.format_friendly_seconds(t), typeof(v)); - if (typeof(v) === 'number') { - layer.values.splice(v, 0, { - time: t, - value: value, - _color: '#' + (Math.random() * 0xffffff | 0).toString(16) - }); - if (!dont_save) undo_manager.save(new UndoState(data, 'Add value')); - } else { - v.object.value = value; - if (!dont_save) undo_manager.save(new UndoState(data, 'Update value')); - } - - repaintAll(); - }); - - dispatcher.on('ease', function(layer, ease_type) { - var t = data.get('ui:currentTime').value; - var v = utils.timeAtLayer(layer, t); - // console.log('Ease Change > ', layer, value, v); - if (v && v.entry) { - v.entry.tween = ease_type; - } - - undo_manager.save(new UndoState(data, 'Add Ease')); - - repaintAll(); - }); - - var start_play = null, - played_from = 0; // requires some more tweaking - - dispatcher.on('controls.toggle_play', function() { - if (start_play) { - pausePlaying(); - } else { - startPlaying(); - } - }); - - dispatcher.on('controls.restart_play', function() { - if (!start_play) { - startPlaying(); - } - - setCurrentTime(played_from); - }); - - dispatcher.on('controls.play', startPlaying); - dispatcher.on('controls.pause', pausePlaying); - - function startPlaying() { - // played_from = timeline.current_frame; - start_play = performance.now() - data.get('ui:currentTime').value * 1000; - layer_panel.setControlStatus(true); - // dispatcher.fire('controls.status', true); - } - - function pausePlaying() { - start_play = null; - layer_panel.setControlStatus(false); - // dispatcher.fire('controls.status', false); - } - - dispatcher.on('controls.stop', function() { - if (start_play !== null) pausePlaying(); - setCurrentTime(0); - }); - - var currentTimeStore = data.get('ui:currentTime'); - dispatcher.on('time.update', setCurrentTime); - - function setCurrentTime(value) { - currentTimeStore.value = value; - - if (start_play) start_play = performance.now() - value * 1000; - repaintAll(); - // layer_panel.repaint(s); - } - - dispatcher.on('target.notify', function(name, value) { - if (target) target[name] = value; - }); - - dispatcher.on('update.scale', function(v) { - console.log('range', v); - data.get('ui:timeScale').value = v; - // timeline.setTimeScale(v); - timeline.repaint(); - }); - - // handle undo / redo - dispatcher.on('controls.undo', function() { - var history = undo_manager.undo(); - data.setJSONString(history.state); - - updateState(); - }); - - dispatcher.on('controls.redo', function() { - var history = undo_manager.redo(); - data.setJSONString(history.state); - - updateState(); - }); - - /* - Paint Routines - */ - - function paint() { - requestAnimationFrame(paint); - - if (start_play) { - var t = (performance.now() - start_play) / 1000; - setCurrentTime(t); - - - if (t > data.get('ui:totalTime').value) { - // simple loop - start_play = performance.now(); - } - } - - if (needsResize) { - div.style.width = width + 'px'; - div.style.height = height + 'px'; - - restyle(layer_panel.dom, timeline.dom); - - timeline.resize(); - repaintAll(); - needsResize = false; - - dispatcher.fire('resize'); - } - - timeline._paint(); - } - - paint(); - - /* - End Paint Routines - */ - - function save(name) { - if (!name) name = 'autosave'; - - var json = data.getJSONString(); - - try { - localStorage[STORAGE_PREFIX + name] = json; - dispatcher.fire('save:done'); - } catch (e) { - console.log('Cannot save', name, json); - } - } - - function saveAs(name) { - if (!name) name = data.get('name').value; - name = prompt('Pick a name to save to (localStorage)', name); - if (name) { - data.data.name = name; - save(name); - } - } - - function saveSimply() { - var name = data.get('name').value; - if (name) { - save(name); - } else { - saveAs(name); - } - } - - function exportJSON() { - var json = data.getJSONString(); - var ret = prompt('Hit OK to download otherwise Copy and Paste JSON', json); - if (!ret) return; - - // make json downloadable - json = data.getJSONString('\t'); - var fileName = 'timeliner-test' + '.json'; - - saveToFile(json, fileName); - } - - function loadJSONString(o) { - // should catch and check errors here - var json = JSON.parse(o); - load(json); - } - - function load(o) { - data.setJSON(o); - // - if (data.getValue('ui') === undefined) { - data.setValue('ui', { - currentTime: 0, - totalTime: 20, - scrollTime: 0, - timeScale: 40 - }); - } - - undo_manager.clear(); - undo_manager.save(new UndoState(data, 'Loaded'), true); - - updateState(); - } - - function updateState() { - layers = layer_store.value; // FIXME: support Arrays - layer_panel.setState(layer_store); - timeline.setState(layer_store); - - repaintAll(); - } - - function repaintAll() { - var content_height = layers.length * Settings.LINE_HEIGHT; - scrollbar.setLength(Settings.TIMELINE_SCROLL_HEIGHT / content_height); - - layer_panel.repaint(); - timeline.repaint(); - } - - function promptImport() { - var json = prompt('Paste JSON in here to Load'); - if (!json) return; - console.log('Loading.. ', json); - loadJSONString(json); - } - - function open(title) { - if (title) { - loadJSONString(localStorage[STORAGE_PREFIX + title]); - } - } - - dispatcher.on('import', function() { - promptImport(); - }.bind(this)); - - dispatcher.on('new', function() { - data.blank(); - updateState(); - }); - - dispatcher.on('openfile', function() { - openAs(function(data) { - // console.log('loaded ' + data); - loadJSONString(data); - }, div); - }); - - dispatcher.on('open', open); - dispatcher.on('export', exportJSON); - - dispatcher.on('save', saveSimply); - dispatcher.on('save_as', saveAs); - - // Expose API - this.save = save; - this.load = load; - - /* - Start DOM Stuff (should separate file) - */ - - var div = document.createElement('div'); - div.style.cssText = 'position: absolute;'; - div.style.top = '16px'; - - var pane = document.createElement('div'); - - style(pane, { - position: 'fixed', - margin: 0, - padding: 0, - fontFamily: 'monospace', - zIndex: Z_INDEX, - border: '2px solid ' + Theme.a, - fontSize: '12px', - color: Theme.d, - overflow: 'hidden', - top: '20px', - left: '20px' - }); - - pane.style.backgroundColor = Theme.a; - - var pane_title = document.createElement('div'); - - var title_bar = document.createElement('span'); - pane_title.appendChild(title_bar); - - - var top_right_bar = document.createElement('span'); - top_right_bar.style.float = 'right'; - pane_title.appendChild(top_right_bar); - - // resize minimize - // var resize_small = new IconButton(10, 'resize_small', 'minimize', dispatcher); - // top_right_bar.appendChild(resize_small.dom); - - // resize full - var resize_full = new IconButton(10, 'resize_full', 'maximize', dispatcher); - top_right_bar.appendChild(resize_full.dom); - - style(pane_title, { - position: 'absolute', - width: '100%', - textAlign: 'left', - top: '0px', - height: '15px', - borderBottom: '1px solid ' + Theme.b, - overflow: 'hidden' - }); - - title_bar.innerHTML = 'Timeliner ' + package_json.version; - - var pane_status = document.createElement('div'); - - style(pane_status, { - position: 'absolute', - height: '15px', - bottom: '0', - width: '100%', - // padding: '2px', - background: Theme.a, - borderTop: '1px solid ' + Theme.b, - fontSize: '11px' - }); - - pane.appendChild(div); - pane.appendChild(pane_status); - pane.appendChild(pane_title); - - var button_styles = { - padding: '2px' - }; - - var label_status = document.createElement('span'); - label_status.textContent = 'hello!'; - label_status.style.marginLeft = '10px'; - - this.setStatus = function(text) { - label_status.textContent = text; - }; - - dispatcher.on('state:save', function(description) { - dispatcher.fire('status', description); - save('autosave'); - }); - - dispatcher.on('status', this.setStatus); - - // var button_save = document.createElement('button'); - // style(button_save, button_styles); - // button_save.textContent = 'Save'; - // button_save.onclick = function() { - // save(); - // }; - - // var button_load = document.createElement('button'); - // style(button_load, button_styles); - // button_load.textContent = 'Import'; - // button_load.onclick = this.promptLoad; - - // var button_open = document.createElement('button'); - // style(button_open, button_styles); - // button_open.textContent = 'Open'; - // button_open.onclick = this.promptOpen; - - - // bottom_right.appendChild(button_load); - // bottom_right.appendChild(button_save); - // bottom_right.appendChild(button_open); - - var bottom_right = document.createElement('span'); - bottom_right.style.float = 'right'; - - pane_status.appendChild(label_status); - pane_status.appendChild(bottom_right); - - /**/ - // zoom in - var zoom_in = new IconButton(12, 'zoom_in', 'zoom in', dispatcher); - // zoom out - var zoom_out = new IconButton(12, 'zoom_out', 'zoom out', dispatcher); - // settings - var cog = new IconButton(12, 'cog', 'settings', dispatcher); - - // bottom_right.appendChild(zoom_in.dom); - // bottom_right.appendChild(zoom_out.dom); - // bottom_right.appendChild(cog.dom); - - // add layer - var plus = new IconButton(12, 'plus', 'New Layer', dispatcher); - plus.onClick(function() { - var name = prompt('Layer name?'); - addLayer(name); - - undo_manager.save(new UndoState(data, 'Layer added')); - - repaintAll(); - }); - bottom_right.appendChild(plus.dom); - - - // trash - var trash = new IconButton(12, 'trash', 'Delete save', dispatcher); - trash.onClick(function() { - var name = data.get('name').value; - if (name && localStorage[STORAGE_PREFIX + name]) { - var ok = confirm('Are you sure you wish to delete ' + name + '?'); - if (ok) { - delete localStorage[STORAGE_PREFIX + name]; - dispatcher.fire('status', name + ' deleted'); - dispatcher.fire('save:done'); - } - } - }); - bottom_right.appendChild(trash.dom); - - - // pane_status.appendChild(document.createTextNode(' | TODO ')); - - /* - End DOM Stuff - */ - - var ghostpane = document.createElement('div'); - ghostpane.id = 'ghostpane'; - style(ghostpane, { - background: '#999', - opacity: 0.2, - position: 'fixed', - margin: 0, - padding: 0, - zIndex: (Z_INDEX - 1), - // transition: 'all 0.25s ease-in-out', - transitionProperty: 'top, left, width, height, opacity', - transitionDuration: '0.25s', - transitionTimingFunction: 'ease-in-out' - }); - - document.body.appendChild(pane); - document.body.appendChild(ghostpane); - - div.appendChild(layer_panel.dom); - div.appendChild(timeline.dom); - - var scrollbar = new ScrollBar(200, 10); - div.appendChild(scrollbar.dom); - - // percentages - scrollbar.onScroll.do(function(type, scrollTo) { - switch(type) { - case 'scrollto': - layer_panel.scrollTo(scrollTo); - timeline.scrollTo(scrollTo); - break; - // case 'pageup': - // scrollTop -= pageOffset; - // me.draw(); - // me.updateScrollbar(); - // break; - // case 'pagedown': - // scrollTop += pageOffset; - // me.draw(); - // me.updateScrollbar(); - // break; - } - }); - - - - // document.addEventListener('keypress', function(e) { - // console.log('kp', e); - // }); - // document.addEventListener('keyup', function(e) { - // if (undo) console.log('UNDO'); - - // console.log('kd', e); - // }); - - // TODO: Keyboard Shortcuts - // Esc - Stop and review to last played from / to the start? - // Space - play / pause from current position - // Enter - play all - // k - keyframe - - document.addEventListener('keydown', function(e) { - var play = e.keyCode == 32; // space - var enter = e.keyCode == 13; // - var undo = e.metaKey && e.keyCode == 91 && !e.shiftKey; - - var active = document.activeElement; - // console.log( active.nodeName ); - - if (active.nodeName.match(/(INPUT|BUTTON|SELECT)/)) { - active.blur(); - } - - if (play) { - dispatcher.fire('controls.toggle_play'); - } - else if (enter) { - // FIXME: Return should play from the start or last played from? - dispatcher.fire('controls.restart_play'); - // dispatcher.fire('controls.undo'); - } - else if (e.keyCode == 27) { - // Esc = stop. FIXME: should rewind head to last played from or Last pointed from? - dispatcher.fire('controls.pause'); - } - else console.log(e.keyCode); - }); - - var needsResize = true; - - function resize(width, height) { - // data.get('ui:bounds').value = { - // width: width, - // height: height - // }; - // TODO: remove ugly hardcodes - width -= 4; - height -= 32; - - Settings.width = width - Settings.LEFT_PANE_WIDTH; - Settings.height = height; - - Settings.TIMELINE_SCROLL_HEIGHT = height - Settings.MARKER_TRACK_HEIGHT; - var scrollable_height = Settings.TIMELINE_SCROLL_HEIGHT; - - scrollbar.setHeight(scrollable_height - 2); - // scrollbar.setThumb - - style(scrollbar.dom, { - top: Settings.MARKER_TRACK_HEIGHT + 'px', - left: (width - 16) + 'px', - }); - - needsResize = true; - } - - function restyle(left, right) { - left.style.cssText = 'position: absolute; left: 0px; top: 0px; height: ' + Settings.height + 'px;'; - style(left, { - // background: Theme.a, - overflow: 'hidden' - }); - left.style.width = Settings.LEFT_PANE_WIDTH + 'px'; - - // right.style.cssText = 'position: absolute; top: 0px;'; - right.style.position = 'absolute'; - right.style.top = '0px'; - right.style.left = Settings.LEFT_PANE_WIDTH + 'px'; - } - - function addLayer(name) { - var layer = new LayerProp(name); - - layers = layer_store.value; - layers.push(layer); - - layer_panel.setState(layer_store); - } - - this.addLayer = addLayer; - - this.setTarget = function(t) { - timeline = t; - }; - - function getValueRanges(ranges, interval) { - interval = interval ? interval : 0.15; - ranges = ranges ? ranges : 2; - - // not optimized! - var t = data.get('ui:currentTime').value; - - var values = []; - - for (var u = -ranges; u <= ranges; u++) { - // if (u == 0) continue; - var o = {}; - - for (var l = 0; l < layers.length; l++) { - var layer = layers[l]; - var m = utils.timeAtLayer(layer, t + u * interval); - o[layer.name] = m.value; - } - - values.push(o); - - } - - return values; - } - - this.getValues = getValueRanges; - - (function DockingWindow() { - "use strict"; - - // Minimum resizable area - var minWidth = 100; - var minHeight = 80; - - // Thresholds - var FULLSCREEN_MARGINS = 2; - var SNAP_MARGINS = 8; - var MARGINS = 2; - - // End of what's configurable. - - var clicked = null; - var onRightEdge, onBottomEdge, onLeftEdge, onTopEdge; - - var preSnapped; - - var b, x, y; - - var redraw = false; - - // var pane = document.getElementById('pane'); - // var ghostpane = document.getElementById('ghostpane'); - - var mouseOnTitle = false; - var snapType; - - pane_title.addEventListener('mouseover', function() { - mouseOnTitle = true; - }); - - pane_title.addEventListener('mouseout', function() { - mouseOnTitle = false; - }); - - resize_full.onClick(function() { - // TOOD toggle back to restored size - if (!preSnapped) preSnapped = { - width: b.width, - height: b.height - }; - - snapType = 'full-screen'; - resizeEdges(); - }); - - // pane_status.addEventListener('mouseover', function() { - // mouseOnTitle = true; - // }); - - // pane_status.addEventListener('mouseout', function() { - // mouseOnTitle = false; - // }); - - window.addEventListener('resize', function() { - if (snapType) - resizeEdges(); - else - needsResize = true; - }); - - // utils - function setBounds(element, x, y, w, h) { - element.style.left = x + 'px'; - element.style.top = y + 'px'; - element.style.width = w + 'px'; - element.style.height = h + 'px'; - - if (element === pane) { - resize(w, h); - } - } - - function hintHide() { - setBounds(ghostpane, b.left, b.top, b.width, b.height); - ghostpane.style.opacity = 0; - } - - setBounds(pane, 0, 0, Settings.width, Settings.height); - setBounds(ghostpane, 0, 0, Settings.width, Settings.height); - - // Mouse events - pane.addEventListener('mousedown', onMouseDown); - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - - // Touch events - pane.addEventListener('touchstart', onTouchDown); - document.addEventListener('touchmove', onTouchMove); - document.addEventListener('touchend', onTouchEnd); - - - function onTouchDown(e) { - onDown(e.touches[0]); - e.preventDefault(); - } - - function onTouchMove(e) { - onMove(e.touches[0]); - } - - function onTouchEnd(e) { - if (e.touches.length == 0) onUp(e.changedTouches[0]); - } - - function onMouseDown(e) { - onDown(e); - } - - function onDown(e) { - calc(e); - - var isResizing = onRightEdge || onBottomEdge || onTopEdge || onLeftEdge; - var isMoving = !isResizing && canMove(); - - clicked = { - x: x, - y: y, - cx: e.clientX, - cy: e.clientY, - w: b.width, - h: b.height, - isResizing: isResizing, - isMoving: isMoving, - onTopEdge: onTopEdge, - onLeftEdge: onLeftEdge, - onRightEdge: onRightEdge, - onBottomEdge: onBottomEdge - }; - - if (isResizing || isMoving) { - e.preventDefault(); - e.stopPropagation(); - } - } - - function canMove() { - return mouseOnTitle; - // return x > 0 && x < b.width && y > 0 && y < b.height - // && y < 18; - } - - function calc(e) { - b = pane.getBoundingClientRect(); - x = e.clientX - b.left; - y = e.clientY - b.top; - - onTopEdge = y < MARGINS; - onLeftEdge = x < MARGINS; - onRightEdge = x >= b.width - MARGINS; - onBottomEdge = y >= b.height - MARGINS; - } - - var e; // current mousemove event - - function onMove(ee) { - e = ee; - calc(e); - - redraw = true; - } - - function animate() { - - requestAnimationFrame(animate); - - if (!redraw) return; - - redraw = false; - - if (clicked && clicked.isResizing) { - - if (clicked.onRightEdge) pane.style.width = Math.max(x, minWidth) + 'px'; - if (clicked.onBottomEdge) pane.style.height = Math.max(y, minHeight) + 'px'; - - if (clicked.onLeftEdge) { - var currentWidth = Math.max(clicked.cx - e.clientX + clicked.w, minWidth); - if (currentWidth > minWidth) { - pane.style.width = currentWidth + 'px'; - pane.style.left = e.clientX + 'px'; - } - } - - if (clicked.onTopEdge) { - var currentHeight = Math.max(clicked.cy - e.clientY + clicked.h, minHeight); - if (currentHeight > minHeight) { - pane.style.height = currentHeight + 'px'; - pane.style.top = e.clientY + 'px'; - } - } - - hintHide(); - - resize(b.width, b.height); - - return; - } - - if (clicked && clicked.isMoving) { - - switch(checks()) { - case 'full-screen': - setBounds(ghostpane, 0, 0, window.innerWidth, window.innerHeight); - ghostpane.style.opacity = 0.2; - break; - case 'snap-top-edge': - setBounds(ghostpane, 0, 0, window.innerWidth, window.innerHeight / 2); - ghostpane.style.opacity = 0.2; - break; - case 'snap-left-edge': - setBounds(ghostpane, 0, 0, window.innerWidth / 2, window.innerHeight); - ghostpane.style.opacity = 0.2; - break; - case 'snap-right-edge': - setBounds(ghostpane, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight); - ghostpane.style.opacity = 0.2; - break; - case 'snap-bottom-edge': - setBounds(ghostpane, 0, window.innerHeight / 2, window.innerWidth, window.innerHeight / 2); - ghostpane.style.opacity = 0.2; - break; - default: - hintHide(); - } - - if (preSnapped) { - setBounds(pane, - e.clientX - preSnapped.width / 2, - e.clientY - Math.min(clicked.y, preSnapped.height), - preSnapped.width, - preSnapped.height - ); - return; - } - - // moving - pane.style.top = (e.clientY - clicked.y) + 'px'; - pane.style.left = (e.clientX - clicked.x) + 'px'; - - return; - } - - // This code executes when mouse moves without clicking - - // style cursor - if (onRightEdge && onBottomEdge || onLeftEdge && onTopEdge) { - pane.style.cursor = 'nwse-resize'; - } else if (onRightEdge && onTopEdge || onBottomEdge && onLeftEdge) { - pane.style.cursor = 'nesw-resize'; - } else if (onRightEdge || onLeftEdge) { - pane.style.cursor = 'ew-resize'; - } else if (onBottomEdge || onTopEdge) { - pane.style.cursor = 'ns-resize'; - } else if (canMove()) { - pane.style.cursor = 'move'; - } else { - pane.style.cursor = 'default'; - } - } - - function checks() { - /* - var rightScreenEdge, bottomScreenEdge; - - rightScreenEdge = window.innerWidth - MARGINS; - bottomScreenEdge = window.innerHeight - MARGINS; - - // Edge Checkings - // hintFull(); - if (b.top < FULLSCREEN_MARGINS || b.left < FULLSCREEN_MARGINS || b.right > window.innerWidth - FULLSCREEN_MARGINS || b.bottom > window.innerHeight - FULLSCREEN_MARGINS) - return 'full-screen'; - - // hintTop(); - if (b.top < MARGINS) return 'snap-top-edge'; - - // hintLeft(); - if (b.left < MARGINS) return 'snap-left-edge'; - - // hintRight(); - if (b.right > rightScreenEdge) return 'snap-right-edge'; - - // hintBottom(); - if (b.bottom > bottomScreenEdge) return 'snap-bottom-edge'; - */ - - if (e.clientY < FULLSCREEN_MARGINS) return 'full-screen'; - - if (e.clientY < SNAP_MARGINS) return 'snap-top-edge'; - - // hintLeft(); - if (e.clientX < SNAP_MARGINS) return 'snap-left-edge'; - - // hintRight(); - if (window.innerWidth - e.clientX < SNAP_MARGINS) return 'snap-right-edge'; - - // hintBottom(); - if (window.innerHeight- e.clientY < SNAP_MARGINS) return 'snap-bottom-edge'; - - } - - animate(); - - function resizeEdges() { - switch(snapType) { - case 'full-screen': - // hintFull(); - setBounds(pane, 0, 0, window.innerWidth, window.innerHeight); - break; - case 'snap-top-edge': - // hintTop(); - setBounds(pane, 0, 0, window.innerWidth, window.innerHeight / 2); - break; - case 'snap-left-edge': - // hintLeft(); - setBounds(pane, 0, 0, window.innerWidth / 2, window.innerHeight); - break; - case 'snap-right-edge': - setBounds(pane, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight); - break; - case 'snap-bottom-edge': - setBounds(pane, 0, window.innerHeight / 2, window.innerWidth, window.innerHeight / 2); - break; - } - } - - function onUp(e) { - calc(e); - - if (clicked && clicked.isMoving) { - // Snap - snapType = checks(); - if (snapType) { - preSnapped = { - width: b.width, - height: b.height - }; - resizeEdges(); - } else { - preSnapped = null; - } - - hintHide(); - - } - - clicked = null; - - } - })(); - -} - -window.Timeliner = Timeliner; -},{"../package.json":"/Users/joshua/dev/gits/timeliner/package.json","./datastore":"/Users/joshua/dev/gits/timeliner/src/datastore.js","./dispatcher":"/Users/joshua/dev/gits/timeliner/src/dispatcher.js","./icon_button":"/Users/joshua/dev/gits/timeliner/src/icon_button.js","./layer_cabinet":"/Users/joshua/dev/gits/timeliner/src/layer_cabinet.js","./settings":"/Users/joshua/dev/gits/timeliner/src/settings.js","./theme":"/Users/joshua/dev/gits/timeliner/src/theme.js","./timeline_panel":"/Users/joshua/dev/gits/timeliner/src/timeline_panel.js","./ui/scrollbar":"/Users/joshua/dev/gits/timeliner/src/ui/scrollbar.js","./undo":"/Users/joshua/dev/gits/timeliner/src/undo.js","./utils":"/Users/joshua/dev/gits/timeliner/src/utils.js"}],"/Users/joshua/dev/gits/timeliner/src/tween.js":[function(require,module,exports){ -/**************************/ -// Tweens -/**************************/ - -var Tweens = { - none: function(k) { - return 0; - }, - linear: function(k) { - return k; - }, - quadEaseIn: function(k) { - return k * k; - }, - quadEaseOut: function(k) { - return - k * ( k - 2 ); - }, - quadEaseInOut: function(k) { - if ( ( k *= 2 ) < 1 ) return 0.5 * k * k; - return - 0.5 * ( --k * ( k - 2 ) - 1 ); - } -}; - -module.exports = Tweens; -},{}],"/Users/joshua/dev/gits/timeliner/src/ui/layer_view.js":[function(require,module,exports){ -var - Theme = require('../theme'), - NumberUI = require('./number'), - Tweens = require('../tween'), - Settings = require('../settings'), - utils = require('../utils') -; - -// TODO - tagged by index instead, work off layers. - -function LayerView(layer, dispatcher) { - var dom = document.createElement('div'); - - var label = document.createElement('span'); - - label.style.cssText = 'font-size: 12px; padding: 4px;'; - - var dropdown = document.createElement('select'); - var option; - dropdown.style.cssText = 'font-size: 10px; width: 60px; margin: 0; float: right; text-align: right;'; - - for (var k in Tweens) { - option = document.createElement('option'); - option.text = k; - dropdown.appendChild(option); - } - - dropdown.addEventListener('change', function(e) { - dispatcher.fire('ease', layer, dropdown.value); - }); - - var keyframe_button = document.createElement('button'); - keyframe_button.innerHTML = '◈'; // '♦' ◇ 9679 9670 9672 - keyframe_button.style.cssText = 'background: none; font-size: 12px; padding: 0px; font-family: monospace; float: right; width: 20px; border-style:none; outline: none;'; // border-style:inset; - - keyframe_button.addEventListener('click', function(e) { - console.log('clicked:keyframing...', state.get('_value').value); - dispatcher.fire('keyframe', layer, state.get('_value').value); - }); - - /* - // Prev Keyframe - var button = document.createElement('button'); - button.textContent = '<'; - button.style.cssText = 'font-size: 12px; padding: 1px; '; - dom.appendChild(button); - - // Next Keyframe - button = document.createElement('button'); - button.textContent = '>'; - button.style.cssText = 'font-size: 12px; padding: 1px; '; - dom.appendChild(button); - - // Mute - button = document.createElement('button'); - button.textContent = 'M'; - button.style.cssText = 'font-size: 12px; padding: 1px; '; - dom.appendChild(button); - - // Solo - button = document.createElement('button'); - button.textContent = 'S'; - button.style.cssText = 'font-size: 12px; padding: 1px; '; - dom.appendChild(button); - */ - - var number = new NumberUI(layer, dispatcher); - - number.onChange.do(function(value, done) { - state.get('_value').value = value; - dispatcher.fire('value.change', layer, value, done); - }); - - utils.style(number.dom, { - float: 'right' - }); - - dom.appendChild(label); - dom.appendChild(keyframe_button); - dom.appendChild(number.dom); - dom.appendChild(dropdown); - - dom.style.cssText = 'margin: 0px; border-bottom:1px solid ' + Theme.b + '; top: 0; left: 0; height: ' + (Settings.LINE_HEIGHT - 1 ) + 'px; color: ' + Theme.c; - this.dom = dom; - - this.repaint = repaint; - var state; - - this.setState = function(l, s) { - layer = l; - state = s; - - var tmp_value = state.get('_value'); - if (tmp_value.value === undefined) { - tmp_value.value = 0; - } - - number.setValue(tmp_value.value); - label.textContent = state.get('name').value; - - repaint(); - }; - - function repaint(s) { - - dropdown.style.opacity = 0; - dropdown.disabled = true; - keyframe_button.style.color = Theme.b; - // keyframe_button.disabled = false; - // keyframe_button.style.borderStyle = 'solid'; - - var tween = null; - var o = utils.timeAtLayer(layer, s); - - if (!o) return; - - if (o.can_tween) { - dropdown.style.opacity = 1; - dropdown.disabled = false; - // if (o.tween) - dropdown.value = o.tween ? o.tween : 'none'; - if (dropdown.value === 'none') dropdown.style.opacity = 0.5; - } - - if (o.keyframe) { - keyframe_button.style.color = Theme.c; - // keyframe_button.disabled = true; - // keyframe_button.style.borderStyle = 'inset'; - } - - state.get('_value').value = o.value; - number.setValue(o.value); - number.paint(); - - dispatcher.fire('target.notify', layer.name, o.value); - } - -} - -module.exports = LayerView; - -},{"../settings":"/Users/joshua/dev/gits/timeliner/src/settings.js","../theme":"/Users/joshua/dev/gits/timeliner/src/theme.js","../tween":"/Users/joshua/dev/gits/timeliner/src/tween.js","../utils":"/Users/joshua/dev/gits/timeliner/src/utils.js","./number":"/Users/joshua/dev/gits/timeliner/src/ui/number.js"}],"/Users/joshua/dev/gits/timeliner/src/ui/number.js":[function(require,module,exports){ -var Theme = require('../theme'), - Do = require('do.js'), - handleDrag = require('../handle_drag'), - style = require('../utils').style - ; - -/**************************/ -// NumberUI -/**************************/ - -function NumberUI(config) { - config = config || {}; - var min = config.min === undefined ? -Infinity : config.min; - var step = config.step || 0.1; - var precision = config.precision || 3; - // Range - // Max - - var span = document.createElement('input'); - // span.type = 'number'; // spinner - - style(span, { - textAlign: 'center', - fontSize: '10px', - padding: '1px', - cursor: 'ns-resize', - width: '40px', - margin: 0, - marginRight: '10px', - appearance: 'none', - outline: 'none', - border: 0, - background: 'none', - borderBottom: '1px dotted '+ Theme.c, - color: Theme.c - }); - - var me = this; - var state, value = 0, unchanged_value; - - this.onChange = new Do(); - - span.addEventListener('change', function(e) { - console.log('input changed', span.value); - value = parseFloat(span.value, 10); - - fireChange(); - }); - - handleDrag(span, onDown, onMove, onUp); - - function onUp(e) { - if (e.moved) fireChange(); - else { - // single click - span.focus(); - } - } - - function onMove(e) { - var dx = e.dx; - var dy = e.dy; - - var stepping = 1 * step; - // value = unchanged_value + dx * 0.000001 + dy * -10 * 0.01; - value = unchanged_value + dx * stepping + dy * -stepping; - - value = Math.max(min, value); - - // value = +value.toFixed(precision); // or toFixed toPrecision - me.onChange.fire(value, true); - } - - function onDown(e) { - unchanged_value = value; - } - - function fireChange() { - me.onChange.fire(value); - } - - this.dom = span; - - // public - this.setValue = function(v) { - value = v; - }; - - this.paint = function() { - if (value) span.value = value.toFixed(precision); - }; -} - -module.exports = NumberUI; -},{"../handle_drag":"/Users/joshua/dev/gits/timeliner/src/handle_drag.js","../theme":"/Users/joshua/dev/gits/timeliner/src/theme.js","../utils":"/Users/joshua/dev/gits/timeliner/src/utils.js","do.js":"/Users/joshua/dev/gits/timeliner/node_modules/do.js/do.js"}],"/Users/joshua/dev/gits/timeliner/src/ui/scrollbar.js":[function(require,module,exports){ -var SimpleEvent = require('do.js'); -var utils = require('../utils'); -console.log(utils); - -// ********** class: ScrollBar ****************** // -/* - Simple UI widget that displays a scrolltrack - and slider, that fires some scroll events -*/ -// *********************************************** - -var scrolltrack_style = { - // float: 'right', - position: 'absolute', - // right: '0', - // top: '0', - // bottom: '0', - background: '-webkit-gradient(linear, left top, right top, color-stop(0, rgb(29,29,29)), color-stop(0.6, rgb(50,50,50)) )', - border: '1px solid rgb(29, 29, 29)', - // zIndex: '1000', - textAlign: 'center', - cursor: 'pointer' -}; - -var scrollbar_style = { - background: '-webkit-gradient(linear, left top, right top, color-stop(0.2, rgb(88,88,88)), color-stop(0.6, rgb(64,64,64)) )', - border: '1px solid rgb(25,25,25)', - // position: 'absolute', - position: 'relative', - borderRadius: '6px' -}; - -function ScrollBar(h, w, dispatcher) { - - var SCROLLBAR_WIDTH = w ? w : 12; - var SCROLLBAR_MARGIN = 3; - var SCROLL_WIDTH = SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2; - var MIN_BAR_LENGTH = 25; - - var scrolltrack = document.createElement('div'); - utils.style(scrolltrack, scrolltrack_style); - - var scrolltrackHeight = h - 2; - scrolltrack.style.height = scrolltrackHeight + 'px' ; - scrolltrack.style.width = SCROLL_WIDTH; //SCROLLBAR_WIDTH; - - // var scrollTop = 0; - var scrollbar = document.createElement('div'); - // scrollbar.className = 'scrollbar'; - utils.style(scrollbar, scrollbar_style); - scrollbar.style.width = SCROLLBAR_WIDTH; - scrollbar.style.height = h / 2; - scrollbar.style.top = 0; - scrollbar.style.left = SCROLLBAR_MARGIN + 'px'; // 0; //S - - scrolltrack.appendChild(scrollbar); - - var me = this; - - var bar_length, bar_y; - - // Sets lengths of scrollbar by percentage - this.setLength = function(l) { - // limit 0..1 - l = Math.max(Math.min(1, l), 0); - l *= scrolltrackHeight; - bar_length = Math.max(l, MIN_BAR_LENGTH); - scrollbar.style.height = bar_length; - }; - - this.setHeight = function(height) { - h = height; - - scrolltrackHeight = h - 2; - scrolltrack.style.height = scrolltrackHeight + 'px' ; - }; - - // Moves scrollbar to position by Percentage - this.setPosition = function(p) { - p = Math.max(Math.min(1, p), 0); - var emptyTrack = scrolltrackHeight - bar_length; - bar_y = p * emptyTrack; - scrollbar.style.top = bar_y; - }; - - this.setLength(1); - this.setPosition(0); - this.onScroll = new SimpleEvent(); - - var mouse_down_grip; - - function onDown(event) { - event.preventDefault(); - - if (event.target == scrollbar) { - mouse_down_grip = event.clientY; - document.addEventListener('mousemove', onMove, false); - document.addEventListener('mouseup', onUp, false); - } else { - if (event.clientY < bar_y) { - me.onScroll.fire('pageup'); - } else if (event.clientY > (bar_y + bar_length)) { - me.onScroll.fire('pagedown'); - } - // if want to drag scroller to empty track instead - // me.setPosition(event.clientY / (scrolltrackHeight - 1)); - } - } - - function onMove(event) { - event.preventDefault(); - - // event.target == scrollbar - var emptyTrack = scrolltrackHeight - bar_length; - var scrollto = (event.clientY - mouse_down_grip) / emptyTrack; - - // clamp limits to 0..1 - if (scrollto > 1) scrollto = 1; - if (scrollto < 0) scrollto = 0; - me.setPosition(scrollto); - me.onScroll.fire('scrollto', scrollto); - } - - function onUp(event) { - onMove(event); - document.removeEventListener('mousemove', onMove, false); - document.removeEventListener('mouseup', onUp, false); - } - - scrolltrack.addEventListener('mousedown', onDown, false); - this.dom = scrolltrack; - -} - -module.exports = ScrollBar; -},{"../utils":"/Users/joshua/dev/gits/timeliner/src/utils.js","do.js":"/Users/joshua/dev/gits/timeliner/node_modules/do.js/do.js"}],"/Users/joshua/dev/gits/timeliner/src/undo.js":[function(require,module,exports){ -/**************************/ -// Undo Manager -/**************************/ - -function UndoState(state, description) { - // this.state = JSON.stringify(state); - this.state = state.getJSONString(); - this.description = description; -} - -function UndoManager(dispatcher, max) { - this.dispatcher = dispatcher; - this.MAX_ITEMS = max || 100; - this.clear(); -} - -UndoManager.prototype.save = function(state, suppress) { - var states = this.states; - var next_index = this.index + 1; - var to_remove = states.length - next_index; - states.splice(next_index, to_remove, state); - - if (states.length > this.MAX_ITEMS) { - states.shift(); - } - - this.index = states.length - 1; - - // console.log('Undo State Saved: ', state.description); - if (!suppress) this.dispatcher.fire('state:save', state.description); -}; - -UndoManager.prototype.clear = function() { - this.states = []; - this.index = -1; - // FIXME: leave default state or always leave one state? -}; - -UndoManager.prototype.canUndo = function() { - return this.index > 0; - // && this.states.length > 1 -}; - -UndoManager.prototype.canRedo = function() { - return this.index < this.states.length - 1; -}; - -UndoManager.prototype.undo = function() { - if (this.canUndo()) { - this.dispatcher.fire('status', 'Undo: ' + this.get().description); - this.index--; - } else { - this.dispatcher.fire('status', 'Nothing to undo'); - } - - return this.get(); -}; - -UndoManager.prototype.redo = function() { - if (this.canRedo()) { - this.index++; - this.dispatcher.fire('status', 'Redo: ' + this.get().description); - } else { - this.dispatcher.fire('status', 'Nothing to redo'); - } - - return this.get(); -}; - -UndoManager.prototype.get = function() { - return this.states[this.index]; -}; - -module.exports = { - UndoState: UndoState, - UndoManager: UndoManager -}; -},{}],"/Users/joshua/dev/gits/timeliner/src/utils.js":[function(require,module,exports){ -var - Tweens = require('./tween'); - -module.exports = { - STORAGE_PREFIX: 'timeliner-', - Z_INDEX: 999, - style: style, - saveToFile: saveToFile, - openAs: openAs, - format_friendly_seconds: format_friendly_seconds, - findTimeinLayer: findTimeinLayer, - timeAtLayer: timeAtLayer -}; - -/**************************/ -// Utils -/**************************/ - -function style(element, styles) { - for (var s in styles) { - element.style[s] = styles[s]; - } -} - -function saveToFile(string, filename) { - var a = document.createElement("a"); - document.body.appendChild(a); - a.style = "display: none"; - - var blob = new Blob([string], { type: 'octet/stream' }), // application/json - url = window.URL.createObjectURL(blob); - - a.href = url; - a.download = filename; - - fakeClick(a); - - setTimeout(function() { - // cleanup and revoke - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - }, 500); -} - - - -var input, openCallback; - -function handleFileSelect(evt) { - var files = evt.target.files; // FileList object - - console.log('handle file select', files.length); - - var f = files[0]; - if (!f) return; - // Can try to do MINE match - // if (!f.type.match('application/json')) { - // return; - // } - console.log('match', f.type); - - var reader = new FileReader(); - - // Closure to capture the file information. - reader.onload = function(e) { - var data = e.target.result; - openCallback(data); - }; - - reader.readAsText(f); - - input.value = ''; -} - - -function openAs(callback, target) { - console.log('openfile...'); - openCallback = callback; - - if (!input) { - input = document.createElement('input'); - input.style.display = 'none'; - input.type = 'file'; - input.addEventListener('change', handleFileSelect); - target = target || document.body; - target.appendChild(input); - } - - fakeClick(input); -} - -function fakeClick(target) { - var e = document.createEvent("MouseEvents"); - e.initMouseEvent( - 'click', true, false, window, 0, 0, 0, 0, 0, - false, false, false, false, 0, null - ); - target.dispatchEvent(e); -} - -function format_friendly_seconds(s, type) { - // TODO Refactor to 60fps??? - // 20 mins * 60 sec = 1080 - // 1080s * 60fps = 1080 * 60 < Number.MAX_SAFE_INTEGER - - var raw_secs = s | 0; - var secs_micro = s % 60; - var secs = raw_secs % 60; - var raw_mins = raw_secs / 60 | 0; - var mins = raw_mins % 60; - var hours = raw_mins / 60 | 0; - - var secs_str = (secs / 100).toFixed(2).substring(2); - - var str = mins + ':' + secs_str; - - if (s % 1 > 0) { - var t2 = (s % 1) * 60; - if (type === 'frames') str = secs + '+' + t2.toFixed(0) + 'f'; - else str += ((s % 1).toFixed(2)).substring(1); - // else str = mins + ':' + secs_micro; - // else str = secs_micro + 's'; /// .toFixed(2) - } - return str; -} - -// get object at time -function findTimeinLayer(layer, time) { - var values = layer.values; - var i, il; - - // TODO optimize by checking time / binary search - - for (i=0, il=values.length; i time) { - return i; - } - } - - return i; -} - - -function timeAtLayer(layer, t) { - // Find the value of layer at t seconds. - // this expect layer to be sorted - // not the most optimized for now, but would do. - - var values = layer.values; - var i, il, entry, prev_entry; - - il = values.length; - - // can't do anything - if (il === 0) return; - - // find boundary cases - entry = values[0]; - if (t < entry.time) { - return { - value: entry.value, - can_tween: false, // cannot tween - keyframe: false // not on keyframe - }; - } - - for (i=0; i 1, - value: entry.value, - keyframe: true - }; - } - return { - // index: i, - entry: entry, - tween: entry.tween, - can_tween: il > 1, - value: entry.value, - keyframe: true // il > 1 - }; - } - if (t < entry.time) { - // possibly a tween - if (!prev_entry.tween) { // or if value is none - return { - value: prev_entry.value, - tween: false, - entry: prev_entry, - can_tween: true, - keyframe: false - }; - } - - // calculate tween - var time_diff = entry.time - prev_entry.time; - var value_diff = entry.value - prev_entry.value; - var tween = prev_entry.tween; - - var dt = t - prev_entry.time; - var k = dt / time_diff; - var new_value = prev_entry.value + Tweens[tween](k) * value_diff; - - return { - entry: prev_entry, - target_tween: entry, - value: new_value, - tween: prev_entry.tween, - can_tween: true, - keyframe: false - }; - } - } - // time is after all entries - return { - value: entry.value, - can_tween: false, - keyframe: false - }; - -} - -},{"./tween":"/Users/joshua/dev/gits/timeliner/src/tween.js"}]},{},["/Users/joshua/dev/gits/timeliner/src/datastore.js","/Users/joshua/dev/gits/timeliner/src/dispatcher.js","/Users/joshua/dev/gits/timeliner/src/handle_drag.js","/Users/joshua/dev/gits/timeliner/src/icon_button.js","/Users/joshua/dev/gits/timeliner/src/layer_cabinet.js","/Users/joshua/dev/gits/timeliner/src/save_format.js","/Users/joshua/dev/gits/timeliner/src/settings.js","/Users/joshua/dev/gits/timeliner/src/theme.js","/Users/joshua/dev/gits/timeliner/src/timeline_panel.js","/Users/joshua/dev/gits/timeliner/src/timeliner.js","/Users/joshua/dev/gits/timeliner/src/tween.js","/Users/joshua/dev/gits/timeliner/src/undo.js","/Users/joshua/dev/gits/timeliner/src/utils.js"]); From 953267e620ccf87b6ec0bd96ae177fac2ce3b0e2 Mon Sep 17 00:00:00 2001 From: tschw Date: Tue, 27 Oct 2015 23:29:21 +0100 Subject: [PATCH 05/17] Make Controller an example / refine interface --- README.md | 2 +- src/layer_cabinet.js | 4 +- src/timeline_panel.js | 6 +-- src/timeliner.js | 89 +---------------------------------- test.html | 105 ++++++++++++++++++++++++++++++++++++++---- 5 files changed, 103 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index dbd6bfb..0c6ebb8 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,5 @@ GUI-only fork of [Josh Koo's Timeliner](http://www.github.com/zz85/timeliner.git). -The class `Timeliner.Controller` provides a base / example of how to use this GUI with an existing animation system. +The class Controller class in `test.html`` defines the skeleton by which to use the GUI with an existing animation system. diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index 9a53f56..d391862 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -352,7 +352,7 @@ function LayerCabinet(context) { this.updateState = function() { - var layers = Object.keys( context.controller.channelKeyTimes ); + var layers = context.controller.getChannelNames(); console.log(layer_uis.length, layers); var i, layer; @@ -382,7 +382,7 @@ function LayerCabinet(context) { function repaint() { - var layers = Object.keys( context.controller.channelKeyTimes ); + var layers = context.controller.getChannelNames(); var time = context.currentTime; currentTime.setValue(time); totalTime.setValue(context.totalTime); diff --git a/src/timeline_panel.js b/src/timeline_panel.js index fc48e6a..2898802 100644 --- a/src/timeline_panel.js +++ b/src/timeline_panel.js @@ -53,7 +53,7 @@ function TimelinePanel(context) { var layers; this.updateState = function() { - layers = Object.keys( context.controller.channelKeyTimes ); + layers = context.controller.getChannelNames(); repaint(); }; @@ -131,7 +131,7 @@ function TimelinePanel(context) { var t = x_to_time(x + e.dx); t = Math.max(0, t); // TODO implement - dispatcher.fire('time.update', t); + //dispatcher.fire('time.update', t); // console.log('frame', frame); // console.log(s, format_friendly_seconds(s), this); }; @@ -165,7 +165,7 @@ function TimelinePanel(context) { for (i = 0; i < il; i++) { // check for keyframes var layer = layers[i]; - var times = context.controller.channelKeyTimes[ layer ]; + var times = context.controller.getChannelKeyTimes( layer ); y = i * LINE_HEIGHT; diff --git a/src/timeliner.js b/src/timeliner.js index 9fb7237..99582f0 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -38,12 +38,6 @@ function Timeliner( controller ) { var dispatcher = new Dispatcher(); - if ( ! controller ) { - - controller = new Timeliner.Controller(); - - } - controller.timeliner = this; controller.init( this ); @@ -295,7 +289,7 @@ function Timeliner( controller ) { } function repaintAll() { - var layers = Object.keys( context.controller.channelKeyTimes ); + var layers = context.controller.getChannelNames(); var content_height = layers.length * Settings.LINE_HEIGHT; scrollbar.setLength(Settings.TIMELINE_SCROLL_HEIGHT / content_height); @@ -1031,85 +1025,4 @@ function Timeliner( controller ) { } -Timeliner.Controller = function ControllerInterface() { - - this.time = 0; - this.timeliner = null; - this.channelKeyTimes = {}; - -}; - -Timeliner.Controller.prototype = { - - constructor: Timeliner.Controller, - - init: function( timeliner ) { - - this.timeliner = timeliner; - - this.channelKeyTimes[ 'test1' ] = []; - this.channelKeyTimes[ 'test2' ] = []; - - }, - - serialize: function() { - - return this.channelKeyTimes; - - }, - - deserialize: function(structs) { - - this.channelKeyTimes = structs; - - }, - - setDisplayTime: function( time ) { - - //console.log( "setDisplayTime(%f)", time ); - this.time = time; - - }, - - setKeyframe: function( channelName, time ) { - - console.log( "setKeyframe('%s',%f)", channelName, time ); - - var keyTimes = this.channelKeyTimes[ channelName ]; - - keyTimes.push( time ); - keyTimes.sort(); - - }, - - delKeyframe: function( channelName, time ) { - - console.log( "delKeyframe('%s',%f)", channelName, time ); - - var keyTimes = this.channelKeyTimes[ channelName ]; - - var index = keyTimes.indexOf( time ); // TODO binary search - - if ( index !== -1 ) { - - keyTimes[ index ] = keyTimes[ keyTimes.length - 1 ]; - keyTimes.pop(); - keyTimes.sort(); - - } - - }, - - hasKeyframe: function( channelName, time ) { - - var keyTimes = this.channelKeyTimes[ channelName ]; - return keyTimes.indexOf( time ) >= 0; // TODO binary search - - } - -}; - - - - window.Timeliner = Timeliner; diff --git a/test.html b/test.html index ee9abc5..ff045e9 100644 --- a/test.html +++ b/test.html @@ -1,18 +1,105 @@ + - - From 631786bdfda72db888ff56651d5eb7190be45dd0 Mon Sep 17 00:00:00 2001 From: tschw Date: Tue, 27 Oct 2015 23:30:03 +0100 Subject: [PATCH 06/17] Make styles work in HTML5 mode --- src/layer_cabinet.js | 2 +- src/timeliner.js | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index d391862..1cba24b 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -80,7 +80,7 @@ function LayerCabinet(context) { range.step = 0.125; style(range, { - width: '90px', + width: '80px', margin: '0px', marginLeft: '2px', marginRight: '2px' diff --git a/src/timeliner.js b/src/timeliner.js index 99582f0..403319b 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -343,8 +343,12 @@ function Timeliner( controller ) { */ var div = document.createElement('div'); - div.style.cssText = 'position: absolute;'; - div.style.top = '22px'; + style(div, { + textAlign: 'left', + lineHeight: '1em', + position: 'absolute', + top: '22px' + }); var pane = document.createElement('div'); From e531f9cee6eb1d2a22ea3c715aed3459db0a9f1b Mon Sep 17 00:00:00 2001 From: tschw Date: Tue, 27 Oct 2015 23:33:49 +0100 Subject: [PATCH 07/17] Uppercase tips --- src/layer_cabinet.js | 4 ++-- src/timeliner.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index 1cba24b..f2ff73e 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -342,11 +342,11 @@ function LayerCabinet(context) { playing = v; if (playing) { play_button.setIcon('pause'); - play_button.setTip('pause'); + play_button.setTip('Pause'); } else { play_button.setIcon('play'); - play_button.setTip('play'); + play_button.setTip('Play'); } }; diff --git a/src/timeliner.js b/src/timeliner.js index 403319b..5b258ab 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -408,7 +408,7 @@ function Timeliner( controller ) { // top_right_bar.appendChild(resize_small.dom); // resize full - var resize_full = new IconButton(10, 'resize_full', 'maximize', dispatcher); + var resize_full = new IconButton(10, 'resize_full', 'Maximize', dispatcher); style(resize_full.dom, button_styles, { marginRight: '2px' }); top_right_bar.appendChild(resize_full.dom); From eea3c5ec02c073d8fa32e860b4ea456badf8d640 Mon Sep 17 00:00:00 2001 From: tschw Date: Thu, 29 Oct 2015 14:50:30 +0100 Subject: [PATCH 08/17] Rename project timeliner -> timeliner_gui --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 499abeb..875ad0f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/tschw/timeliner.git" + "url": "https://github.com/tschw/timeliner_gui.git" }, "keywords": [ "timeline", @@ -27,9 +27,9 @@ ], "license": "MIT", "bugs": { - "url": "https://github.com/tschw/timeliner/issues" + "url": "https://github.com/tschw/timeliner_gui/issues" }, - "homepage": "https://github.com/tschw/timeliner", + "homepage": "https://github.com/tschw/timeliner_gui", "devDependencies": { "do.js": "^1.0.0", "uglifyify": "^2.6.0" From 7c28b34351cc9fff1f3044be32d285074eb2e873 Mon Sep 17 00:00:00 2001 From: tschw Date: Thu, 29 Oct 2015 15:07:52 +0100 Subject: [PATCH 09/17] Add .dispose() --- src/timeliner.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/timeliner.js b/src/timeliner.js index 5b258ab..58c4647 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -55,9 +55,6 @@ function Timeliner( controller ) { }; - window.dbgTimelinerContext = context; // DEBUG - - var timeline = new TimelinePanel(context); var layer_panel = new LayerCabinet(context); @@ -531,7 +528,7 @@ function Timeliner( controller ) { */ var ghostpane = document.createElement('div'); - ghostpane.id = 'ghostpane'; + //ghostpane.id = 'ghostpane'; style(ghostpane, { background: '#999', opacity: 0.2, @@ -671,6 +668,14 @@ function Timeliner( controller ) { this.addLayer = addLayer; + this.dispose = function dispose() { + + var domParent = pane.parentElement; + domParent.removeChild(pane); + domParent.removeChild(ghostpane); + + }; + this.setTarget = function(t) { timeline = t; }; From 9b07f3ba8bc7fa0aa6c9ec096f1f4c82ce15bdae Mon Sep 17 00:00:00 2001 From: tschw Date: Thu, 29 Oct 2015 15:08:30 +0100 Subject: [PATCH 10/17] Remove/disable defunct public methods --- src/timeliner.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/timeliner.js b/src/timeliner.js index 58c4647..9f5e31d 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -495,7 +495,7 @@ function Timeliner( controller ) { var name = prompt('Layer name?'); addLayer(name); - undo_manager.save(new UndoState(data, 'Layer added')); + // undo_manager.save(new UndoState(data, 'Layer added')); repaintAll(); }); @@ -657,6 +657,7 @@ function Timeliner( controller ) { right.style.left = Settings.LEFT_PANE_WIDTH + 'px'; } +/* function addLayer(name) { var layer = new LayerProp(name); @@ -667,6 +668,7 @@ function Timeliner( controller ) { } this.addLayer = addLayer; +*/ this.dispose = function dispose() { @@ -676,10 +678,6 @@ function Timeliner( controller ) { }; - this.setTarget = function(t) { - timeline = t; - }; - (function DockingWindow() { "use strict"; From 0718b2f75b27487659b7b9609ca876c468ae48e5 Mon Sep 17 00:00:00 2001 From: tschw Date: Thu, 29 Oct 2015 22:33:24 +0100 Subject: [PATCH 11/17] Fix duration control --- src/layer_cabinet.js | 6 +++--- src/timeliner.js | 6 ++++++ test.html | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index f2ff73e..ac1d8a2 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -113,12 +113,12 @@ function LayerCabinet(context) { currentTime.onChange.do(function(value, done) { dispatcher.fire('time.update', value); - // repaint(); + currentTime.paint(); }); totalTime.onChange.do(function(value, done) { - context.totalTime = value; - // repaint(); + dispatcher.fire('totalTime.update', value); + totalTime.paint(); }); // Play Controls diff --git a/src/timeliner.js b/src/timeliner.js index 9f5e31d..eb3517f 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -140,6 +140,12 @@ function Timeliner( controller ) { dispatcher.on('time.update', setCurrentTime); + dispatcher.on('totalTime.update', function(value) { + context.totalTime = value; + controller.setDuration(value); + timeline.repaint(); + }); + dispatcher.on('target.notify', function(name, value) { console.log(name, "=", value); //if (target) target[name] = value; diff --git a/test.html b/test.html index ff045e9..fbe784f 100644 --- a/test.html +++ b/test.html @@ -47,6 +47,12 @@ }, + setDuration: function( duration ) { + + console.log( "setDuration(%f)", duration ); + + }, + getChannelNames: function() { return Object.keys( this._channelKeyTimes ); From a754f99af783e1c3093512528f7421330524c1ac Mon Sep 17 00:00:00 2001 From: tschw Date: Thu, 29 Oct 2015 22:34:28 +0100 Subject: [PATCH 12/17] Remove unused parameter --- src/timeline_panel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timeline_panel.js b/src/timeline_panel.js index 2898802..b35f5b3 100644 --- a/src/timeline_panel.js +++ b/src/timeline_panel.js @@ -59,7 +59,7 @@ function TimelinePanel(context) { this.updateState(); - this.scrollTo = function(s, y) { + this.scrollTo = function(s) { scrollTop = s * Math.max(layers.length * LINE_HEIGHT - SCROLL_HEIGHT, 0); repaint(); }; From d00136588fef08bf345b4b2054e7e13a0b06ece1 Mon Sep 17 00:00:00 2001 From: tschw Date: Fri, 30 Oct 2015 16:13:05 +0100 Subject: [PATCH 13/17] Silence log messages --- src/layer_cabinet.js | 8 ++++---- src/timeliner.js | 10 ++-------- src/utils.js | 6 +++--- src/widget/icon_button.js | 4 ++-- src/widget/number.js | 2 +- src/widget/scrollbar.js | 3 +-- 6 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index ac1d8a2..fa713e1 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -328,7 +328,7 @@ function LayerCabinet(context) { } function changeRange() { - console.log("range.value", range.value); + //console.log("range.value", range.value); dispatcher.fire('update.scale', 6 * Math.pow(100, range.value)); } @@ -354,7 +354,7 @@ function LayerCabinet(context) { var layers = context.controller.getChannelNames(); - console.log(layer_uis.length, layers); + //console.log(layer_uis.length, layers); var i, layer; for (i = 0; i < layers.length; i++) { layer = layers[i]; @@ -375,8 +375,8 @@ function LayerCabinet(context) { layer_uis[i].setState(layer); } - console.log('Total layers (view, hidden, total)', layer_uis.length, unused_layers.length, - layer_uis.length + unused_layers.length); + //console.log('Total layers (view, hidden, total)', layer_uis.length, unused_layers.length, + // layer_uis.length + unused_layers.length); }; diff --git a/src/timeliner.js b/src/timeliner.js index eb3517f..07f741b 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -146,13 +146,7 @@ function Timeliner( controller ) { timeline.repaint(); }); - dispatcher.on('target.notify', function(name, value) { - console.log(name, "=", value); - //if (target) target[name] = value; - }); - dispatcher.on('update.scale', function(v) { - console.log('range', v); context.timeScale = v; timeline.setTimeScale(v); timeline.repaint(); @@ -303,7 +297,7 @@ function Timeliner( controller ) { function promptImport() { var json = prompt('Paste JSON in here to Load'); if (!json) return; - console.log('Loading.. ', json); + //console.log('Loading.. ', json); loadJSONString(json); } @@ -618,7 +612,7 @@ function Timeliner( controller ) { // Esc = stop. FIXME: should rewind head to last played from or Last pointed from? dispatcher.fire('controls.pause'); } - else console.log(e.keyCode); + //else console.log(e.keyCode); }); var needsResize = true; diff --git a/src/utils.js b/src/utils.js index 6339776..29a53c5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -153,7 +153,7 @@ var input, openCallback; function handleFileSelect(evt) { var files = evt.target.files; // FileList object - console.log('handle file select', files.length); + //console.log('handle file select', files.length); var f = files[0]; if (!f) return; @@ -161,7 +161,7 @@ function handleFileSelect(evt) { // if (!f.type.match('application/json')) { // return; // } - console.log('match', f.type); + //console.log('match', f.type); var reader = new FileReader(); @@ -178,7 +178,7 @@ function handleFileSelect(evt) { function openAs(callback, target) { - console.log('openfile...'); + //console.log('openfile...'); openCallback = callback; if (!input) { diff --git a/src/widget/icon_button.js b/src/widget/icon_button.js index 852e0f1..d7ceaa1 100644 --- a/src/widget/icon_button.js +++ b/src/widget/icon_button.js @@ -63,7 +63,7 @@ function IconButton(size, icon, tooltip, dp) { this.setIcon = function(icon) { me.icon = icon; - if (!font.fonts[icon]) console.warn('Font icon not found!'); + if (!font.fonts[icon]) console.error('Font icon not found!'); this.resize(); }; @@ -81,7 +81,7 @@ function IconButton(size, icon, tooltip, dp) { e.stopPropagation(); longHoldTimer = setTimeout(function() { if (longHoldTimer) { - console.log('LONG HOLD-ED!'); + //console.log('LONG HOLD-ED!'); f(); } }, LONG_HOLD_DURATION); diff --git a/src/widget/number.js b/src/widget/number.js index 4f47f50..f691d5b 100644 --- a/src/widget/number.js +++ b/src/widget/number.js @@ -41,7 +41,7 @@ function NumberUI(config) { this.onChange = new Do(); span.addEventListener('change', function(e) { - console.log('input changed', span.value); + //console.log('input changed', span.value); value = parseFloat(span.value, 10); fireChange(); diff --git a/src/widget/scrollbar.js b/src/widget/scrollbar.js index 4778975..ff5c07b 100644 --- a/src/widget/scrollbar.js +++ b/src/widget/scrollbar.js @@ -1,6 +1,5 @@ var SimpleEvent = require('do.js'); var utils = require('../utils'); -console.log(utils); // ********** class: ScrollBar ****************** // /* @@ -132,4 +131,4 @@ function ScrollBar(h, w, dispatcher) { } -module.exports = ScrollBar; \ No newline at end of file +module.exports = ScrollBar; From c3bd5166014243e17992762bb060c49646601b57 Mon Sep 17 00:00:00 2001 From: tschw Date: Fri, 30 Oct 2015 18:41:14 +0100 Subject: [PATCH 14/17] Consume events --- src/timeliner.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/timeliner.js b/src/timeliner.js index 07f741b..e48c3fa 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -763,6 +763,8 @@ function Timeliner( controller ) { // Mouse events pane.addEventListener('mousedown', onMouseDown); + pane.addEventListener('mouseover', onMouseOver); + pane.addEventListener('mouseout',onMouseOut); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); @@ -771,6 +773,9 @@ function Timeliner( controller ) { document.addEventListener('touchmove', onTouchMove); document.addEventListener('touchend', onTouchEnd); + var mouseOver = false; + function onMouseOver(e) { mouseOver = true; } + function onMouseOut(e) { mouseOver = false; } function onTouchDown(e) { onDown(e.touches[0]); @@ -812,8 +817,8 @@ function Timeliner( controller ) { if (isResizing || isMoving) { e.preventDefault(); - e.stopPropagation(); } + e.stopPropagation(); } function canMove() { @@ -840,6 +845,10 @@ function Timeliner( controller ) { calc(e); redraw = true; + + if (mouseOver) { + e.stopPropagation(); + } } function animate() { @@ -1027,6 +1036,9 @@ function Timeliner( controller ) { clicked = null; + if (mouseOver) { + e.stopPropagation(); + } } })(); From 2828a4e38ade06822d1e9b447dec3de5cab7e0a0 Mon Sep 17 00:00:00 2001 From: tschw Date: Sat, 31 Oct 2015 03:36:37 +0100 Subject: [PATCH 15/17] Settings -> LayoutConstants (immutable) --- src/layer_cabinet.js | 8 +++--- src/layer_view.js | 9 ++---- src/layout_constants.js | 11 +++++++ src/settings.js | 14 --------- src/timeline_panel.js | 64 +++++++++++++++++++---------------------- src/timeliner.js | 55 ++++++++++++++++------------------- 6 files changed, 73 insertions(+), 88 deletions(-) create mode 100644 src/layout_constants.js delete mode 100644 src/settings.js diff --git a/src/layer_cabinet.js b/src/layer_cabinet.js index fa713e1..32befe1 100644 --- a/src/layer_cabinet.js +++ b/src/layer_cabinet.js @@ -1,4 +1,4 @@ -var Settings = require('./settings'), +var LayoutConstants = require('./layout_constants'), LayerUI = require('./layer_view'), IconButton = require('./widget/icon_button'), style = require('./utils').style, @@ -12,14 +12,14 @@ function LayerCabinet(context) { var div = document.createElement('div'); var top = document.createElement('div'); - top.style.cssText = 'margin: 0px; top: 0; left: 0; height: ' + Settings.MARKER_TRACK_HEIGHT + 'px'; + top.style.cssText = 'margin: 0px; top: 0; left: 0; height: ' + LayoutConstants.MARKER_TRACK_HEIGHT + 'px'; // top.style.textAlign = 'right'; var layer_scroll = document.createElement('div'); style(layer_scroll, { position: 'absolute', - top: Settings.MARKER_TRACK_HEIGHT + 'px', - // height: (Settings.height - Settings.MARKER_TRACK_HEIGHT) + 'px' + top: LayoutConstants.MARKER_TRACK_HEIGHT + 'px', + // height: (height - LayoutConstants.MARKER_TRACK_HEIGHT) + 'px' left: 0, right: 0, bottom: 0, diff --git a/src/layer_view.js b/src/layer_view.js index 971f524..7a45359 100644 --- a/src/layer_view.js +++ b/src/layer_view.js @@ -1,8 +1,5 @@ -var - Theme = require('./theme'), - Settings = require('./settings'), - utils = require('./utils') // TODO -; +var Theme = require('./theme'), + LayoutConstants = require('./layout_constants'); function LayerView(context, channelName) { @@ -11,7 +8,7 @@ function LayerView(context, channelName) { label.style.cssText = 'font-size: 12px; padding: 4px;'; - var height = (Settings.LINE_HEIGHT - 1); + var height = (LayoutConstants.LINE_HEIGHT - 1); var keyframe_button = document.createElement('button'); keyframe_button.innerHTML = '◈'; // '♦' ◇ 9679 9670 9672 diff --git a/src/layout_constants.js b/src/layout_constants.js new file mode 100644 index 0000000..ba4fc6c --- /dev/null +++ b/src/layout_constants.js @@ -0,0 +1,11 @@ + +// Dimensions +module.exports = { + LINE_HEIGHT: 26, + DIAMOND_SIZE: 10, + MARKER_TRACK_HEIGHT: 60, + WIDTH: 600, + HEIGHT: 200, + LEFT_PANE_WIDTH: 250, + TIME_SCALE: 60 // number of pixels to 1 secon, +}; diff --git a/src/settings.js b/src/settings.js deleted file mode 100644 index bca4d37..0000000 --- a/src/settings.js +++ /dev/null @@ -1,14 +0,0 @@ - -var DEFAULT_TIME_SCALE = 60; - -// Dimensions -module.exports = { - LINE_HEIGHT: 26, - DIAMOND_SIZE: 10, - MARKER_TRACK_HEIGHT: 60, - width: 600, - height: 200, - TIMELINE_SCROLL_HEIGHT: 0, - LEFT_PANE_WIDTH: 250, - time_scale: DEFAULT_TIME_SCALE // number of pixels to 1 secon, -}; diff --git a/src/timeline_panel.js b/src/timeline_panel.js index b35f5b3..2fb1168 100644 --- a/src/timeline_panel.js +++ b/src/timeline_panel.js @@ -1,28 +1,24 @@ -var - Settings = require('./settings'), +var LayoutConstants = require('./layout_constants'), Theme = require('./theme'), utils = require('./utils'), proxy_ctx = utils.proxy_ctx; - var - LINE_HEIGHT = Settings.LINE_HEIGHT, - DIAMOND_SIZE = Settings.DIAMOND_SIZE, - MARKER_TRACK_HEIGHT = Settings.MARKER_TRACK_HEIGHT, - - LEFT_PANE_WIDTH = Settings.LEFT_PANE_WIDTH, - time_scale = Settings.time_scale; +var LINE_HEIGHT = LayoutConstants.LINE_HEIGHT, + DIAMOND_SIZE = LayoutConstants.DIAMOND_SIZE, + MARKER_TRACK_HEIGHT = LayoutConstants.MARKER_TRACK_HEIGHT, + LEFT_PANE_WIDTH = LayoutConstants.LEFT_PANE_WIDTH, + time_scale = LayoutConstants.TIME_SCALE; - var frame_start = 0; // this is the current scroll position. + +var frame_start = 0; // this is the current scroll position. // TODO // dirty rendering // drag block // drag current time // pointer on timescale -var tickMark1; -var tickMark2; -var tickMark3; +var tickMark1, tickMark2, tickMark3; function time_scaled() { @@ -45,7 +41,7 @@ function TimelinePanel(context) { var dispatcher = context.dispatcher; - var scrollTop = 0, scrollLeft = 0, SCROLL_HEIGHT; + var scrollTop = 0, scrollLeft = 0; var dpr = window.devicePixelRatio; var canvas = document.createElement('canvas'); @@ -60,17 +56,17 @@ function TimelinePanel(context) { this.updateState(); this.scrollTo = function(s) { - scrollTop = s * Math.max(layers.length * LINE_HEIGHT - SCROLL_HEIGHT, 0); + scrollTop = s * Math.max(layers.length * LINE_HEIGHT - context.scrollHeight, 0); repaint(); }; this.resize = function() { dpr = window.devicePixelRatio; - canvas.width = Settings.width * dpr; - canvas.height = Settings.height * dpr; - canvas.style.width = Settings.width + 'px'; - canvas.style.height = Settings.height + 'px'; - SCROLL_HEIGHT = Settings.height - MARKER_TRACK_HEIGHT; + canvas.width = context.width * dpr; + canvas.height = context.height * dpr; + canvas.style.width = context.width + 'px'; + canvas.style.height = context.height + 'px'; + context.scrollHeight = context.height - MARKER_TRACK_HEIGHT; }; this.dom = canvas; @@ -82,7 +78,7 @@ function TimelinePanel(context) { var current_frame; // currently in seconds // var currentTime = 0; // in frames? could have it in string format (0:00:00:1-60) - + var LEFT_GUTTER = 20; var i, x, y, il, j; @@ -174,7 +170,7 @@ function TimelinePanel(context) { for (var j = 0; j < times.length; j++) { renderItems.push(new Diamond( - time_to_x( times[ j ] ), + time_to_x( times[ j ] ), y + LINE_HEIGHT * 0.5 - DIAMOND_SIZE / 2)); } } @@ -226,7 +222,7 @@ function TimelinePanel(context) { ctx.rect(scroller.left, 5, scroller.grip_length, h); ctx.fill(); - var r = current_frame * k; + var r = current_frame * k; // ctx.fillStyle = Theme.a; // 'yellow'; // ctx.fillRect(0, 5, w, 2); @@ -301,7 +297,7 @@ function TimelinePanel(context) { .scale(dpr, dpr) .translate(0, MARKER_TRACK_HEIGHT) .beginPath() - .rect(0, 0, Settings.width, SCROLL_HEIGHT) + .rect(0, 0, context.width, context.scrollHeight) .translate(-scrollLeft, -scrollTop) .clip() .run(check) @@ -327,12 +323,12 @@ function TimelinePanel(context) { ctx.save(); ctx.scale(dpr, dpr); - // + // ctx.lineWidth = 1; // .5, 1, 2 - width = Settings.width, - height = Settings.height; + width = context.width, + height = context.height; var units = time_scale / tickMark1; var offsetUnits = (frame_start * time_scale) % units; @@ -340,11 +336,11 @@ function TimelinePanel(context) { var count = (width - LEFT_GUTTER + offsetUnits) / units; // console.log('time_scale', time_scale, 'tickMark1', tickMark1, 'units', units, 'offsetUnits', offsetUnits, frame_start); - + // time_scale = pixels to 1 second (40) // tickMark1 = marks per second (marks / s) // units = pixels to every mark (40) - + // labels only for (i = 0; i < count; i++) { x = i * units + LEFT_GUTTER - offsetUnits; @@ -380,7 +376,7 @@ function TimelinePanel(context) { var mul = tickMark3 / tickMark2; units = time_scale / tickMark3; count = (width - LEFT_GUTTER + offsetUnits) / units; - + // small ticks for (i = 0; i < count; i++) { if (i % mul === 0) continue; @@ -391,13 +387,13 @@ function TimelinePanel(context) { ctx.lineTo(x, MARKER_TRACK_HEIGHT - 10); ctx.stroke(); } - + // Encapsulate a scroll rect for the layers ctx_wrap .save() .translate(0, MARKER_TRACK_HEIGHT) .beginPath() - .rect(0, 0, Settings.width, SCROLL_HEIGHT) + .rect(0, 0, context.width, context.scrollHeight) .translate(-scrollLeft, -scrollTop) .clip() .run(drawLayerContents) @@ -418,7 +414,7 @@ function TimelinePanel(context) { ctx.moveTo(x, base_line); ctx.lineTo(x, height); ctx.stroke(); - + ctx.fillStyle = 'red'; // black ctx.textAlign = 'center'; ctx.beginPath(); @@ -487,7 +483,7 @@ function TimelinePanel(context) { dispatcher.fire('keyframe', layers[track], current_frame); - + }); function onMouseMove(e) { diff --git a/src/timeliner.js b/src/timeliner.js index e48c3fa..31fa370 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -7,7 +7,7 @@ var undo = require('./undo'), Theme = require('./theme'), UndoManager = undo.UndoManager, UndoState = undo.UndoState, - Settings = require('./settings'), + LayoutConstants = require('./layout_constants'), utils = require('./utils'), LayerCabinet = require('./layer_cabinet'), TimelinePanel = require('./timeline_panel'), @@ -43,6 +43,10 @@ function Timeliner( controller ) { var context = { + width: LayoutConstants.WIDTH, + height: LayoutConstants.HEIGHT, + scrollHeight: 0, + totalTime: 20.0, timeScale: 6, @@ -59,6 +63,11 @@ function Timeliner( controller ) { var layer_panel = new LayerCabinet(context); var undo_manager = new UndoManager(dispatcher); + + var scrollbar = new ScrollBar(0, 10); + + var div = document.createElement('div'); + /* setTimeout(function() { // hack! @@ -175,6 +184,7 @@ function Timeliner( controller ) { Paint Routines */ + var needsResize = true; function paint() { requestAnimationFrame(paint); @@ -190,8 +200,8 @@ function Timeliner( controller ) { } if (needsResize) { - div.style.width = width + 'px'; - div.style.height = height + 'px'; + div.style.width = context.width + 'px'; + div.style.height = context.height + 'px'; restyle(layer_panel.dom, timeline.dom); @@ -287,8 +297,8 @@ function Timeliner( controller ) { function repaintAll() { var layers = context.controller.getChannelNames(); - var content_height = layers.length * Settings.LINE_HEIGHT; - scrollbar.setLength(Settings.TIMELINE_SCROLL_HEIGHT / content_height); + var content_height = layers.length * LayoutConstants.LINE_HEIGHT; + scrollbar.setLength(context.scrollHeight / content_height); layer_panel.repaint(); timeline.repaint(); @@ -339,7 +349,6 @@ function Timeliner( controller ) { Start DOM Stuff (should separate file) */ - var div = document.createElement('div'); style(div, { textAlign: 'left', lineHeight: '1em', @@ -548,7 +557,6 @@ function Timeliner( controller ) { div.appendChild(layer_panel.dom); div.appendChild(timeline.dom); - var scrollbar = new ScrollBar(200, 10); div.appendChild(scrollbar.dom); // percentages @@ -615,46 +623,33 @@ function Timeliner( controller ) { //else console.log(e.keyCode); }); - var needsResize = true; - - function resize(width, height) { - // data.get('ui:bounds').value = { - // width: width, - // height: height - // }; + function resize(newWidth, newHeight) { // TODO: remove ugly hardcodes - width -= 4; - height -= 44; - - Settings.width = width - Settings.LEFT_PANE_WIDTH; - Settings.height = height; - - Settings.TIMELINE_SCROLL_HEIGHT = height - Settings.MARKER_TRACK_HEIGHT; - var scrollable_height = Settings.TIMELINE_SCROLL_HEIGHT; - - scrollbar.setHeight(scrollable_height - 2); - // scrollbar.setThumb + context.width = newWidth - LayoutConstants.LEFT_PANE_WIDTH - 4; + context.height = newHeight - 44; + context.scrollHeight = context.height - LayoutConstants.MARKER_TRACK_HEIGHT; + scrollbar.setHeight(context.scrollHeight - 2); style(scrollbar.dom, { - top: Settings.MARKER_TRACK_HEIGHT + 'px', - left: (width - 16) + 'px', + top: LayoutConstants.MARKER_TRACK_HEIGHT + 'px', + left: (newWidth - 16 - 4) + 'px', }); needsResize = true; } function restyle(left, right) { - left.style.cssText = 'position: absolute; left: 0px; top: 0px; height: ' + Settings.height + 'px;'; + left.style.cssText = 'position: absolute; left: 0px; top: 0px; height: ' + context.height + 'px;'; style(left, { // background: Theme.a, overflow: 'hidden' }); - left.style.width = Settings.LEFT_PANE_WIDTH + 'px'; + left.style.width = LayoutConstants.LEFT_PANE_WIDTH + 'px'; // right.style.cssText = 'position: absolute; top: 0px;'; right.style.position = 'absolute'; right.style.top = '0px'; - right.style.left = Settings.LEFT_PANE_WIDTH + 'px'; + right.style.left = LayoutConstants.LEFT_PANE_WIDTH + 'px'; } /* From b27c3bb87f4f3ef4412ecc8de2a016b377b2c875 Mon Sep 17 00:00:00 2001 From: tschw Date: Sat, 31 Oct 2015 03:37:24 +0100 Subject: [PATCH 16/17] Configure edge snap --- src/timeliner.js | 50 +++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/timeliner.js b/src/timeliner.js index 31fa370..392d393 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -677,22 +677,28 @@ function Timeliner( controller ) { "use strict"; // Minimum resizable area - var minWidth = 100; - var minHeight = 80; + var minWidth = 120; + var minHeight = 100; // Thresholds var FULLSCREEN_MARGINS = 2; - var SNAP_MARGINS = 8; + var SNAP_MARGINS = 12; var MARGINS = 2; + var DEFAULT_SNAP = 'snap-bottom-edge'; + // End of what's configurable. var clicked = null; var onRightEdge, onBottomEdge, onLeftEdge, onTopEdge; - var preSnapped; + var preSnapped = { + width: LayoutConstants.WIDTH, + height: LayoutConstants.HEIGHT + }; + var snapType = DEFAULT_SNAP; - var b, x, y; + var x, y, b = pane.getBoundingClientRect(); var redraw = false; @@ -700,7 +706,6 @@ function Timeliner( controller ) { // var ghostpane = document.getElementById('ghostpane'); var mouseOnTitle = false; - var snapType; pane_title.addEventListener('mouseover', function() { mouseOnTitle = true; @@ -753,8 +758,8 @@ function Timeliner( controller ) { ghostpane.style.opacity = 0; } - setBounds(pane, 0, 0, Settings.width, Settings.height); - setBounds(ghostpane, 0, 0, Settings.width, Settings.height); + setBounds(pane, 0, 0, context.width, context.height); + setBounds(ghostpane, 0, 0, context.width, context.height); // Mouse events pane.addEventListener('mousedown', onMouseDown); @@ -890,19 +895,19 @@ function Timeliner( controller ) { ghostpane.style.opacity = 0.2; break; case 'snap-top-edge': - setBounds(ghostpane, 0, 0, window.innerWidth, window.innerHeight / 2); + setBounds(ghostpane, 0, 0, window.innerWidth, window.innerHeight * 0.25); ghostpane.style.opacity = 0.2; break; case 'snap-left-edge': - setBounds(ghostpane, 0, 0, window.innerWidth / 2, window.innerHeight); + setBounds(ghostpane, 0, 0, window.innerWidth * 0.35, window.innerHeight); ghostpane.style.opacity = 0.2; break; case 'snap-right-edge': - setBounds(ghostpane, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight); + setBounds(ghostpane, window.innerWidth * 0.65, 0, window.innerWidth * 0.35, window.innerHeight); ghostpane.style.opacity = 0.2; break; case 'snap-bottom-edge': - setBounds(ghostpane, 0, window.innerHeight / 2, window.innerWidth, window.innerHeight / 2); + setBounds(ghostpane, 0, window.innerHeight * 0.75, window.innerWidth, window.innerHeight * 0.25); ghostpane.style.opacity = 0.2; break; default: @@ -987,26 +992,28 @@ function Timeliner( controller ) { animate(); function resizeEdges() { + var x, y, w, h; switch(snapType) { case 'full-screen': - // hintFull(); - setBounds(pane, 0, 0, window.innerWidth, window.innerHeight); + x = 0, y = 0, w = window.innerWidth, h = window.innerHeight; break; case 'snap-top-edge': - // hintTop(); - setBounds(pane, 0, 0, window.innerWidth, window.innerHeight / 2); + x = 0, y = 0, w = window.innerWidth, h = window.innerHeight * 0.25; break; case 'snap-left-edge': - // hintLeft(); - setBounds(pane, 0, 0, window.innerWidth / 2, window.innerHeight); + x = 0, y = 0, w = window.innerWidth * 0.35, h = window.innerHeight; break; case 'snap-right-edge': - setBounds(pane, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight); + x = window.innerWidth * 0.65, y = 0, w = window.innerWidth * 0.35, h = window.innerHeight; break; case 'snap-bottom-edge': - setBounds(pane, 0, window.innerHeight / 2, window.innerWidth, window.innerHeight / 2); + x = 0, y = window.innerHeight * 0.75, w = window.innerWidth, h = window.innerHeight * 0.25; break; + default: + return; } + setBounds(pane, x, y, w, h); + setBounds(ghostpane, x, y, w, h); } function onUp(e) { @@ -1035,6 +1042,9 @@ function Timeliner( controller ) { e.stopPropagation(); } } + + resizeEdges(); + })(); } From d5dc3880925751775d7e885fb229e1415b368f9a Mon Sep 17 00:00:00 2001 From: tschw Date: Sun, 1 Nov 2015 04:42:50 +0100 Subject: [PATCH 17/17] Re-enable keyframe dragging --- package.json | 2 +- src/layer_view.js | 9 ++++--- src/timeline_panel.js | 59 ++++++++++++++++++++++++++++++++----------- src/timeliner.js | 11 +++++++- src/utils.js | 39 +++++++++++++++++++++++----- test.html | 38 ++++++++++++++++++++-------- 6 files changed, 121 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 875ad0f..fcd013c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timeliner_gui", - "version": "0.0.1", + "version": "0.0.2", "description": "Timeliner GUI", "main": "timeliner.js", "scripts": { diff --git a/src/layer_view.js b/src/layer_view.js index 7a45359..a582090 100644 --- a/src/layer_view.js +++ b/src/layer_view.js @@ -1,5 +1,6 @@ var Theme = require('./theme'), - LayoutConstants = require('./layout_constants'); + LayoutConstants = require('./layout_constants'), + utils = require('./utils'); function LayerView(context, channelName) { @@ -43,9 +44,11 @@ function LayerView(context, channelName) { keyframe_button.style.color = Theme.b; - if (time == null) return; + if (time == null || context.draggingKeyframe || channelName == null ) return; - if (context.controller.hasKeyframe(channelName, time)) { + var keyTimes = context.controller.getChannelKeyTimes(channelName); + + if ( utils.binarySearch( keyTimes, time ) >= 0 ) { keyframe_button.style.color = Theme.c; } diff --git a/src/timeline_panel.js b/src/timeline_panel.js index 2fb1168..ad5a306 100644 --- a/src/timeline_panel.js +++ b/src/timeline_panel.js @@ -26,7 +26,7 @@ function time_scaled() { tickMark1 = time_scale / div; tickMark2 = 2 * tickMark1; - tickMark3 = 10 * tickMark1; + tickMark3 = 8 * tickMark1; } @@ -85,11 +85,17 @@ function TimelinePanel(context) { var needsRepaint = false; var renderItems = []; - function Diamond(x, y) { + var timeDrag = 0; + var channelDrag; + + function Diamond(t, x, y) { + var self = this; var isOver = false; + this.time = t; + this.path = function(ctx_wrap) { ctx_wrap .beginPath() @@ -123,13 +129,27 @@ function TimelinePanel(context) { self.paint(ctx_wrap); }; - this.mousedrag = function(e) { - var t = x_to_time(x + e.dx); - t = Math.max(0, t); - // TODO implement - //dispatcher.fire('time.update', t); - // console.log('frame', frame); - // console.log(s, format_friendly_seconds(s), this); + this.mousedrag = function(e, domEvent) { + + if ( channelDrag !== undefined ) { + + var t = x_to_time(e.offsetx), + delta = Math.max(t - timeDrag, - timeDrag), + shift = domEvent.shiftKey; + + if ( delta ) { + + context.draggingKeyframe = true; + + context.controller.moveKeyframe( channelDrag, timeDrag, delta, shift ); + + timeDrag += delta; + repaint(); + + } + + } + }; } @@ -169,8 +189,10 @@ function TimelinePanel(context) { for (var j = 0; j < times.length; j++) { + var time = times[ j ]; + renderItems.push(new Diamond( - time_to_x( times[ j ] ), + time, time_to_x( time ), y + LINE_HEIGHT * 0.5 - DIAMOND_SIZE / 2)); } } @@ -284,12 +306,11 @@ function TimelinePanel(context) { } } - - // console.log(pointer) } function pointerEvents() { + if (!pointer) return; ctx_wrap @@ -305,7 +326,8 @@ function TimelinePanel(context) { } function _paint() { - if (!needsRepaint) { + + if (! needsRepaint) { pointerEvents(); return; } @@ -513,14 +535,19 @@ function TimelinePanel(context) { y: e.offsety }; pointerEvents(); + if (mousedownItem instanceof Diamond) { + timeDrag = mousedownItem.time; + channelDrag = layers[ y_to_track(e.offsety) ]; + if (!channelDrag) mousedownItem = null; + } dispatcher.fire('time.update', x_to_time(e.offsetx)); // Hit criteria - }, function move(e) { + }, function move(e, domEvent) { mousedown2 = false; if (mousedownItem) { mouseDownThenMove = true; if (mousedownItem.mousedrag) { - mousedownItem.mousedrag(e); + mousedownItem.mousedrag(e,domEvent); } } else { dispatcher.fire('time.update', x_to_time(e.offsetx)); @@ -532,6 +559,8 @@ function TimelinePanel(context) { mousedown2 = false; mousedownItem = null; mouseDownThenMove = false; + context.draggingKeyframe = false; + repaint(); } ); diff --git a/src/timeliner.js b/src/timeliner.js index 392d393..338aa84 100644 --- a/src/timeliner.js +++ b/src/timeliner.js @@ -68,6 +68,8 @@ function Timeliner( controller ) { var div = document.createElement('div'); + controller.setDuration(context.totalTime); + /* setTimeout(function() { // hack! @@ -75,9 +77,14 @@ function Timeliner( controller ) { }); */ dispatcher.on('keyframe', function(channelName) { + var time = context.currentTime; - if ( ! controller.hasKeyframe( channelName, time ) ) { + if ( time == null || channelName == null ) return; + + var keyTimes = controller.getChannelKeyTimes( channelName, time ); + + if ( utils.binarySearch( keyTimes, time ) < 0 ) { controller.setKeyframe( channelName, time ); @@ -1049,4 +1056,6 @@ function Timeliner( controller ) { } +Timeliner.binarySearch = utils.binarySearch; + window.Timeliner = Timeliner; diff --git a/src/utils.js b/src/utils.js index 29a53c5..249b614 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,13 +6,40 @@ module.exports = { openAs: openAs, format_friendly_seconds: format_friendly_seconds, proxy_ctx: proxy_ctx, - handleDrag: handleDrag + handleDrag: handleDrag, + binarySearch: binarySearch }; /**************************/ // Utils /**************************/ +function binarySearch(arr, num) { + + var l = 0, r = arr.length, found = false; + + while ( l < r ) { + + var m = ( l + r ) >> 1; + + if ( arr[ m ] < num ) { + + l = m + 1; + + } else { + + r = m; + + found = arr[ m ] === num; + + } + + } + + return found ? l : ~l; + +} + function handleDrag(element, ondown, onmove, onup, down_criteria) { var pointer = null; var bounds = element.getBoundingClientRect(); @@ -22,7 +49,7 @@ function handleDrag(element, ondown, onmove, onup, down_criteria) { function onMouseDown(e) { handleStart(e); - if (down_criteria && !down_criteria(pointer)) { + if (down_criteria && !down_criteria(pointer,e)) { pointer = null; return; } @@ -31,7 +58,7 @@ function handleDrag(element, ondown, onmove, onup, down_criteria) { document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); - ondown(pointer); + ondown(pointer,e); e.preventDefault(); } @@ -39,7 +66,7 @@ function handleDrag(element, ondown, onmove, onup, down_criteria) { function onMouseMove(e) { handleMove(e); pointer.moved = true; - onmove(pointer); + onmove(pointer, e); } function handleStart(e) { @@ -74,7 +101,7 @@ function handleDrag(element, ondown, onmove, onup, down_criteria) { function onMouseUp(e) { handleMove(e); - onup(pointer); + onup(pointer,e); pointer = null; document.removeEventListener('mousemove', onMouseMove); @@ -91,7 +118,7 @@ function handleDrag(element, ondown, onmove, onup, down_criteria) { if (down_criteria && !down_criteria(e)) return; te.preventDefault(); handleStart(e); - ondown(pointer); + ondown(pointer,e); } element.addEventListener('touchmove', onTouchMove); diff --git a/test.html b/test.html index fbe784f..9a48982 100644 --- a/test.html +++ b/test.html @@ -69,10 +69,17 @@ console.log( "setKeyframe('%s',%f)", channelName, time ); - var keyTimes = this._channelKeyTimes[ channelName ]; + var keyTimes = this._channelKeyTimes[ channelName ], + index = Timeliner.binarySearch( keyTimes, time ); - keyTimes.push( time ); - keyTimes.sort(); + if ( index < 0 ) { + + // Note: Can use ~index to insert - we simply push & sort + + keyTimes.push( time ); + keyTimes.sort( Controller._compare ); + + } }, @@ -80,29 +87,38 @@ console.log( "delKeyframe('%s',%f)", channelName, time ); - var keyTimes = this._channelKeyTimes[ channelName ]; - - var index = keyTimes.indexOf( time ); // TODO binary search + var keyTimes = this._channelKeyTimes[ channelName ], + index = Timeliner.binarySearch( keyTimes, time ); - if ( index !== -1 ) { + if ( index >= 0 ) { keyTimes[ index ] = keyTimes[ keyTimes.length - 1 ]; keyTimes.pop(); - keyTimes.sort(); + keyTimes.sort( Controller._compare ); } }, - hasKeyframe: function( channelName, time ) { + moveKeyframe: function( channelName, time, delta, moveRemaining ) { - var keyTimes = this._channelKeyTimes[ channelName ]; - return keyTimes.indexOf( time ) >= 0; // TODO binary search + var keyTimes = this._channelKeyTimes[ channelName ], + index = Timeliner.binarySearch( keyTimes, time ); + + if ( index >= 0 ) { + + var endAt = moveRemaining ? keyTimes.length : index + 1; + while ( index !== endAt ) keyTimes[ index ++ ] += delta; + keyTimes.sort( Controller._compare ); + + } } }; +Controller._compare = function( a, b ) { return a - b; }; + var timeliner = new Timeliner( new Controller() );