// adapted from http://www.sitepoint.com/creating-accurate-timers-in-javascript/ , // https://bl.ocks.org/mbostock/5872848, Dispatching Events // and https://bl.ocks.org/mbostock/1166403, Axis Component (function () { var svg = d3.select('svg') .attr('width', window.innerWidth - 20) .attr('height', 432) // 500 - buttons .attr('viewbox', '0 0 ' + (window.innerWidth - 20) + ' ' + 432) .attr('class', 'black-bg'); var beats = { tick: 0, start: 0, beat: 4, measure: 4, measures: 0, max: 0, bpm: 60000 / 128, // 128 bpm, ~469 ms len: 253, toLoop: '', play: function () { var sync = this.tick && this.measures ? Math.round( this.bpm * (this.tick % this.measures) ): 0; this.start = Date.now() - sync; this.emit(); }, emit: function () { var real = Math.round(this.tick * this.bpm); var ideal = Date.now() - this.start; var diff = ideal - real; var td = function (type) { return { type: type, beat: this.tick, measure: this.measures, diff: diff, to: this.bpm - diff, max: this.max } }.bind(this); this.tick += 1; if (diff > this.max) { this.max = diff; } if (this.tick % this.beat === 0) { this.measures += 1; evts.measure(td('measure')); } else { evts.beat(td('beat')); } clearTimeout(this.toLoop); this.toLoop = setTimeout(function (self) { self.emit(); }, this.bpm - diff, this ); }, stop: function () { clearTimeout(this.toLoop); this.tick = 0; this.measures = 0; }, pause: function () { clearTimeout(this.toLoop); this.tick = this.tick % this.beat; } }; var evts = d3.dispatch('beat', 'measure', 'play', 'pause', 'stop' ); evts.on('measure', function (td) { lights.measure(); plot.append(td); plot.update(td); metronome.tick(td); log(adjLog, td.beat, td.diff, td.max); }); evts.on('beat', function (td) { lights.beat(); plot.append(td); plot.update(td); metronome.tick(td); log(adjLog, td.beat, td.diff, td.max) }); evts.on('play', function () { beats.play(); plot.play(); uncorrected.play(); }); evts.on('pause', function () { beats.pause(); uncorrected.stop(); }); evts.on('stop', function () { beats.stop(); uncorrected.stop(); }); var lights = { rad: 0, spRad: 0, flashRad: 0, grp: [], nodes: [], flashers: [], colors: [ 'hsl(341, 100%, 50%)', 'hsl(359, 100%, 50%)', 'hsl(18, 100%, 50%)', 'hsl(35, 100%, 50%)', 'hsl(52, 100%, 50%)', 'hsl(83, 100%, 50%)', 'hsl(127, 100%, 50%)', 'hsl(160, 100%, 50%)', 'hsl(190, 100%, 50%)', 'hsl(212, 100%, 50%)', 'hsl(227, 100%, 50%)', 'hsl(242, 100%, 50%)', 'hsl(259, 100%, 50%)', 'hsl(273, 100%, 50%)', 'hsl(296, 100%, 50%)' ], init: function () { var spacing = svg.attr('width') / (this.colors.length + 1); this.rad = spacing / 4; this.spRad = this.rad * 1.25; this.flashRad = this.rad * 4; this.grp = d3.select('#lights-grp') this.nodes = this.grp .selectAll('circle.light') .data(this.colors); this.nodes.enter() .append('circle') .attr('class', 'light') .attr('cx', function(d, i) { return ( i + 1 ) * spacing }) .attr('cy', this.spRad + 10) .attr('r', this.rad) .attr('fill', function(d) { return d }) .datum(function (d, i) { return { color: d, idx: i }; } ); this.flashers = this.grp .selectAll('circle.flasher') .data(this.colors); this.flashers.enter() .append('circle') .attr('class', 'flasher') .attr('cx', function(d, i) { return ( i + 1 ) * spacing }) .attr('cy', this.spRad + 10) .attr('r', this.rad) .attr('fill', function(d) { return d }) .datum(function (d, i) { return { color: d, idx: i }; } ); }, flash: function (flasher) { var self = this; flasher .transition() .duration(beats.bpm - 100) .attr('r', self.flashRad) .attr('opacity', 0) .each('end', function() { flasher .attr('r', self.rad) .attr('opacity', 1) }); }, beat: function () { var self = this; this.flashers.each(function(d,i) { self.flash(d3.select(this)); }); }, measure: function () { var self = this; this.nodes.each(function(d,i) { self.nextColor(d3.select(this)); }); this.flashers.each(function(d,i) { var flasher = d3.select(this); self.nextColor(flasher); self.flash(flasher); }); }, nextColor : function (light) { var nextIdx = light.datum().idx + 1 >= this.colors.length ? 0: light.datum().idx + 1; var nextColor = this.colors[nextIdx]; light.attr('fill', nextColor) .datum( { color: nextColor, idx: nextIdx } ); } }; var metronome = { grp: {}, init: function() { this.grp = d3.select('#metronome'); var metroDims = this.grp.node().getBoundingClientRect(); var svgDims = svg.node().getBoundingClientRect(); var lightDims = d3.select('#lights-grp').node().getBoundingClientRect(); this.grp.attr('transform', function () { return 'translate(' + ( (svgDims.width - metroDims.width) * .5) + ',' + // left ( lightDims.bottom - svgDims.top + 20 ) + ')'; // top }) .style('opacity', 1); this.pendulum = this.grp.select('#pendulum') this.pendL = this.pendulum.attr('d').substring(6) this.arcPendulum = this.grp.select('#arc-pendulum'); this.arcPendulumLength = this.arcPendulum.node().getTotalLength(); this.arcPendulumMove = this.arcPendulumLength / beats.beat; this.weight = this.grp.select('#weight'); this.arcWeight = this.grp.select('#arc-weight'); this.arcWeightLength = this.arcWeight.node().getTotalLength() this.arcWeightMove = this.arcWeightLength / beats.beat; this.direction = 1; }, tick: function (td) { var weightPt, pendulumPt, pathD, cx, cy, circle; var beat = td.beat % beats.beat; var self = this; circle = this.grp.append('circle') .attr('r', 5) .attr('fill', '#dadada') .attr('cx', function () { return self.weight.attr('cx') }) .attr('cy', function () { return self.weight.attr('cy') }) .transition() .delay(td.to) .duration(td.to) .attr('opacity', .5) .each('end', function () { d3.select(this).remove() }); if (td.type === 'beat') { if (this.direction === -1) { beat = beats.beat - beat; } pendulumPt = this.arcPendulum.node() .getPointAtLength(this.arcPendulumLength - (beat * this.arcPendulumMove)); weightPt = this.arcWeight.node() .getPointAtLength(this.arcWeightLength - (beat * this.arcWeightMove)); } if (td.type === 'measure') { if (this.direction === 1) { pendulumPt = this.arcPendulum.node().getPointAtLength(0); weightPt = this.arcWeight.node().getPointAtLength(0); this.direction = -1; } else { pendulumPt = this.arcPendulum.node().getPointAtLength(this.arcPendulumLength); weightPt = this.arcWeight.node().getPointAtLength(this.arcWeightLength); this.direction = 1; } } cx = weightPt.x; cy = weightPt.y; pathD = 'M' + pendulumPt.x + ',' + pendulumPt.y + ' ' + this.pendL; this.pendulum.transition() .duration(td.to) .ease('linear') .attr('d', pathD); this.weight.transition() .duration(td.to) .ease('linear') .attr('cx', cx) .attr('cy', cy); } }; var plot = { init: function () { var now = Date.now() - beats.bpm; var margins = {top: 6, right: 20, bottom: 20, left: 30}; var svgDims = svg.node().getBoundingClientRect(); var metroDims = d3.select('#metronome').node().getBoundingClientRect(); var height = svgDims.height - metroDims.bottom - margins.bottom - margins.top; var width = svgDims.width - margins.right - margins.left; d3.select('#plot-clip rect') .attr('width', width) .attr('height', height) this.xScale = d3.time.scale() .range([0, width]); this.yScale = d3.scale.linear() .range([height, 0]) .domain([-5, 30]); this.grp = svg.append('g') .attr('class', 'plot') .attr('transform', 'translate(' + margins.left + ',' + (metroDims.bottom + margins.top) + ')'); this.grp.append('text') .text('setTimeout deviation in ms') .attr('x', 20) .attr('y', 15); this.xAxis = this.grp.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + height + ')') .attr('opacity', 0) .call(this.xScale.axis = d3.svg.axis().scale(this.xScale).orient('bottom')); this.yAxis = this.grp.append('g') .attr('class', 'y axis') .attr('transform', 'translate(0,' + (margins.top - 5) + ')') .call(this.yScale.axis = d3.svg.axis().scale(this.yScale).orient('left')); this.dots = this.grp.append('g') .attr('id', 'dot-clip') .attr('clip-path', 'url(#plot-clip)') .append('g') .attr('id', 'dots'); }, append: function (td) { var plot = this; var last = this.xScale.domain()[1]; var datum = td; datum.then = Date.now(); this.dots.append('circle') .attr('class', 'dot ' + datum.beat + ' ' + datum.type ) .attr('cx', plot.xScale(last)) .attr('cy', plot.yScale(datum.diff)) .datum(datum); }, // http://bl.ocks.org/mbostock/1166403 update: function(td) { var plot = this; // update the x domain var now = Date.now(); this.xScale.domain([now - (beats.len - 2) * beats.bpm, now - beats.bpm]); // slide the x-axis left var trans = this.grp.transition().duration(td.to).ease('linear'); trans.select('.x.axis').call(plot.xScale.axis); this.dots.selectAll('.dot') .transition() .ease('linear') .duration(td.to) .attr('cx', function (d, i) { return plot.xScale(d.then - td.to); }) var goneDots = this.dots.selectAll('circle.dot') .filter(function () { var cx = parseInt(this.getAttribute('cx'), 10) return cx < 0; }); if (!goneDots.empty()) { goneDots.remove(); }; }, play: function () { var now = Date.now(); if (beats.measures === 0) { this.dots.selectAll('.dot').remove(); } this.xScale.domain([now - (beats.len - 2) * beats.bpm, now - beats.bpm]); this.xAxis.attr('opacity', 1) }, }; var playPauseCtrl = document.getElementById('play-pause-ctrl'); playPauseCtrl.addEventListener('click', function () { if ( this.classList.contains('play') ) { this.classList.add('pause'); this.classList.remove('play'); evts.play(); } else if ( this.classList.contains('pause') ) { this.classList.remove('pause'); this.classList.add('play'); evts.pause(); } }); var stopCtrl = document.getElementById('stop-ctrl'); stopCtrl.addEventListener('click', function () { playPauseCtrl.classList.add('play'); playPauseCtrl.classList.remove('pause'); evts.stop(); }); var adjLog = d3.select('#adjusted'); var uncLog = d3.select('#uncorrected'); var log = function (selection, count, dev, max) { var countEl = selection.select('.count').text(count); var devEl = selection.select('.dev').text(dev); var maxEl = selection.select('.max').text(max); }; var uncorrected = { tick: 0, start: 0, bpm: 60000 / 128, // 128 bpm, ~469 ms toLoop: '', max: 0, play: function () { this.start = Date.now(); this.tick = 0; this.loop(); }, loop: function () { var real = Math.round(this.tick * this.bpm); var ideal = Date.now() - this.start; var diff = ideal - real; this.tick += 1; if (diff > this.max ) { this.max = diff; } log(uncLog, this.tick, diff, this.max); clearTimeout(this.toLoop); this.toLoop = setTimeout(function (self) { self.loop(); }, this.bpm, this); }, stop: function () { clearTimeout(this.toLoop); this.tick = 0; } }; lights.init(); metronome.init(); plot.init(); }());