Skip to content

Instantly share code, notes, and snippets.

@kenpenn
Last active July 5, 2016 01:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kenpenn/92ebaa71696b4c4c3acd672b1bb3f49a to your computer and use it in GitHub Desktop.
Save kenpenn/92ebaa71696b4c4c3acd672b1bb3f49a to your computer and use it in GitHub Desktop.
Self-adjusting setTimeout loop
license: gpl-3.0
height: 500

A setTimeout is essentially a polite request to queue code to execute after a specified delay.

The actual delay may vary widely.

This example executes setTimeouts recursively; it compares the ideal time to the actual system time, and adjusts the delay accordingly.

e.g., if the requested delay is 500ms, and the first setTimeout is executed after 510ms, the second setTimeout's delay is adjusted to 490ms, and so on.

The green dots show when each beat event is fired, the red dots show when each measure event is fired.

Uncorrected loops shows a setTimeout with a fixed delay, the same delay as the adjusted setTimeout target.

adapted from:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>setTimeout deviation</title>
<style>
body {
margin: 0;
font-family: sans-serif;
font-size: 16px;
}
svg { margin: 10px; }
.black-bg { background: hsl(0, 0%, 0%); }
.axis { font-size: 12px; }
.axis path, .axis line, .tick line {
fill: none;
stroke: lawngreen;
shape-rendering: crispEdges;
stroke-width: 1.5px;
}
.plot text {
fill: #dadada;
}
.tick text {
fill: lawngreen;
}
.dot {
r: 2;
}
.dot.beat { fill: lawngreen; }
.dot.measure { fill: crimson; }
.ctrl {
background: #dadada;
border-radius: 6px;
display: inline-block;
margin: 0 0 0 10px;
padding: 6px;
}
#play-icon {
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
border-left: 24px solid #777;
margin: 2px 2px 2px 6px;
width: 0;
height: 0;
}
#pause-left, #pause-right, #stop-icon {
background: #777;
}
#pause-left, #pause-right {
float: left;
margin-top: 4px;
margin-bottom: 4px;
width: 8px;
height: 24px;
}
#pause-left {
margin-left: 6px;
margin-right: 4px;
}
#pause-right {
margin-right: 6px;
}
#play-pause-ctrl.play #pause-icon { display: none; }
#play-pause-ctrl.pause #play-icon { display: none; }
#stop-icon {
border-radius: 3px;
margin: 6px;
width: 20px;
height: 20px;
}
#logs {
display:inline-block;
vertical-align: top;
}
.log {
margin: 0 0 4px 40px;
}
#adjusted {
margin-left: 65px;
}
.log div {
display: inline-block;
text-align: right;
}
.log .label {
margin-left: 20px;
}
.log .num {
width: 45px;
}
</style>
</head>
<body>
<svg></svg>
<div>
<div id="play-pause-ctrl" class="play ctrl">
<div id="play-icon"></div>
<div id="pause-icon">
<div id="pause-left"></div>
<div id="pause-right"></div>
</div>
</div>
<div id="stop-ctrl" class="ctrl">
<div id="stop-icon"></div>
</div>
<div id="logs">
<div id="uncorrected" class="log">
Uncorrected loops: <div class="count num">0</div><div class="label">deviation: </div><div class="dev num">0</div>ms<div class="label">max: </div><div class="max num">0</div>ms
</div>
<div id="adjusted" class="log">
Adjusted loops: <div class="count num">0</div><div class="label">deviation: </div><div class="dev num">0</div>ms<div class="label">max: </div><div class="max num">0</div>ms
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="plot.js"></script>
</script>
</body>
</html>
(function() {
var margin = {top: 6, right: 20, bottom: 20, left: 30},
width = window.innerWidth - 20 - margin.right - margin.left,
height = 432 - margin.top - margin.bottom;
var svg = d3.select('svg')
.attr('width', window.innerWidth - 20)
.attr('height', 432)
.attr('viewbox', '0 0 ' + width + ' ' + 432)
.attr('class', 'black-bg')
svg.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', width)
.attr('height', height);
var beats = {
tick: 0,
start: Date.now(),
beat: 4,
measure: 4,
measures: 0,
max: 0,
bpm: 60000 / 128, // 128 bpm, ~469 ms
len: 253,
toLoop: '',
play: function () {
var sync = this.counter && this.measures ?
Math.round( this.bpm * (this.counter % 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 (label) {
return {
label: label,
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.measure === 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.measure;
}
};
var evts = d3.dispatch('beat', 'measure', 'play', 'pause', 'stop' );
evts.on('measure', function (td) {
plot.append(td);
plot.update(td);
log(adjLog, td.beat, td.diff, td.max);
});
evts.on('beat', function (td) {
plot.append(td);
plot.update(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 plot = {
init: function () {
var now = Date.now() - beats.bpm;
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(' + margin.left + ',' + margin.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,' + (margin.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(#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.label )
//.attr('r', 2)
.attr('cx', plot.xScale(last))
.attr('cy', plot.yScale(datum.diff))
//.attr('fill', datum.fill)
.datum(datum);
},
// http://bl.ocks.org/mbostock/1166403
update: function(td) {
//this.paused = false;
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;
}
};
plot.init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment