Skip to content

Commit

Permalink
WIP: fix: chart periodically jumping one pixel back and forth
Browse files Browse the repository at this point in the history
At `millisPerPixel` slightly above `requestAnimationFrame` period
  • Loading branch information
WofWca committed Sep 29, 2021
1 parent 1fffdd8 commit 025d608
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 15 deletions.
16 changes: 13 additions & 3 deletions examples/example1.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@
}, 500);

function createTimeline() {
var chart = new SmoothieChart();
var chart = new SmoothieChart({
// millisPerPixel: 1000 / (3 * 75 + 1),
// millisPerPixel: 1000 / 80,
// millisPerPixel: 1000 / 75.5,
// millisPerPixel: 1000 / 75,
millisPerPixel: 1000 / 74.6,
// millisPerPixel: 1000 / 72,
// millisPerPixel: 1000 / 1,
maxValue: 10000,
minValue: 0,
});
chart.addTimeSeries(random, { strokeStyle: 'rgba(0, 255, 0, 1)', fillStyle: 'rgba(0, 255, 0, 0.2)', lineWidth: 4 });
chart.streamTo(document.getElementById("chart"), 500);
chart.streamTo(document.getElementById("chart"), 0);
}
</script>
</head>
<body onload="createTimeline()" style="background-color:#333333">

<canvas id="chart" width="100" height="100"></canvas>
<canvas id="chart" width="400" height="100"></canvas>

</body>
</html>
72 changes: 60 additions & 12 deletions smoothie.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@
this.currentVisMinValue = 0;
this.lastRenderTimeMillis = 0;
this.lastChartTimestamp = 0;
// this.lastFrameWindowStart = Date.now();
this.lastFrameWindowStart = 0;

this.mousemove = this.mousemove.bind(this);
this.mouseout = this.mouseout.bind(this);
Expand Down Expand Up @@ -787,22 +789,68 @@

time = time || nowMillis - (this.delay || 0);

// Round time down to pixel granularity, so motion appears smoother.
time -= time % this.options.millisPerPixel;

if (!this.isAnimatingScale) {
// We're not animating. We can use the last render time and the scroll speed to work out whether
// we actually need to paint anything yet. If not, we can return immediately.
var sameTime = this.lastChartTimestamp === time;
if (sameTime) {
// Render at least every 1/6th of a second. The canvas may be resized, which there is
// no reliable way to detect.
var needToRenderInCaseCanvasResized = nowMillis - this.lastRenderTimeMillis > 1000/6;
if (!needToRenderInCaseCanvasResized) {
// time -= time % this.options.millisPerPixel;

// if (!this.isAnimatingScale) {
// // We're not animating. We can use the last render time and the scroll speed to work out whether
// // we actually need to paint anything yet. If not, we can return immediately.
// var sameTime = this.lastChartTimestamp === time;
// if (sameTime) {
// // Render at least every 1/6th of a second. The canvas may be resized, which there is
// // no reliable way to detect.
// var needToRenderInCaseCanvasResized = nowMillis - this.lastRenderTimeMillis > 1000/6;
// if (!needToRenderInCaseCanvasResized) {
// return;
// }
// }
// }

// We increment time in pixel steps, so pixel interpolation on the image is always the same,
// so lines do not appear to wobble as they move along the canvas.
const period = this.options.millisPerPixel;
// This is like `lastFrameWindowEnd = this.lastFrameWindowStart + period`, but
// with truncation / floating point error mitigation.
const lastFrameWindowEnd = Math.round(this.lastFrameWindowStart / period + 1) * period;
let nextFrameWindowStart = lastFrameWindowEnd;
console.log((time - nextFrameWindowStart) / period, time - nextFrameWindowStart);
if (time >= nextFrameWindowStart) {
// The amount of frame timing accuracy (frame temporal placement) (how much off a frame can be from the time it ideally belongs to) we allow to sacrifice in order to keep chart movement speed smooth
// const maxFrameTimeOffsetMillis = 1000 / 50; // one 50 FPS frame should be an alright margin.
const maxFrameTimeOffsetMillis = 20;
// const maxFrameTimeOffsetMillis = 100;
const minNextFrameTime = time - maxFrameTimeOffsetMillis;
const minNextFrameWindowStart = minNextFrameTime - minNextFrameTime % period;

if (nextFrameWindowStart < minNextFrameWindowStart) {
// Have to skip some intermediate frames.
console.log('Skipping n frames', (nextFrameWindowStart - minNextFrameWindowStart) / period, nextFrameWindowStart - minNextFrameWindowStart)
nextFrameWindowStart = minNextFrameWindowStart;
}
// nextFrameWindowStart = Math.max(nextFrameWindowStart, minNextFrameWindowStart);
} else {
if (time < this.lastFrameWindowStart) {
// This can only happen if the value of the `time` argument is smaller than the value
// passed to it on the previous call. Which currenly only happens when the user calls it manually.
nextFrameWindowStart = time - time % period;
} else {
if (!this.isAnimatingScale) {
// Don't have to redraw anything.
console.log('same frame, until next frame', time - this.lastFrameWindowStart)
return;
}
}
}
this.lastFrameWindowStart = nextFrameWindowStart;
time = nextFrameWindowStart;

// // Debug assertions.
// const num = (time / this.options.millisPerPixel);
// if (Math.abs(num - Math.round(num)) > 0.0001) {
// console.error('Time not rounded', num - Math.round(num), time % this.options.millisPerPixel, time, this.options.millisPerPixel)
// }
if (time <= this.lastChartTimestamp) {
console.warn('same or smaller time', time, this.lastChartTimestamp)
}

this.lastRenderTimeMillis = nowMillis;
this.lastChartTimestamp = time;
Expand Down

0 comments on commit 025d608

Please sign in to comment.