cannon
Version:
A lightweight 3D physics engine written in JavaScript.
353 lines (314 loc) • 15.1 kB
JavaScript
// MIT License:
//
// Copyright (c) 2010-2011, Joe Walnes
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/**
* Smoothie Charts - http://smoothiecharts.org/
* (c) 2010-2012, Joe Walnes
*
* v1.0: Main charting library, by Joe Walnes
* v1.1: Auto scaling of axis, by Neil Dunn
* v1.2: fps (frames per second) option, by Mathias Petterson
* v1.3: Fix for divide by zero, by Paul Nikitochkin
* v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
* v1.5: Set default frames per second to 50... smoother.
* .start(), .stop() methods for conserving CPU, by Dmitry Vyal
* options.iterpolation = 'bezier' or 'line', by Dmitry Vyal
* options.maxValue to fix scale, by Dmitry Vyal
* v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla
* v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin
* Smooth rescaling, by Kostas Michalopoulos
* v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni
* v1.9: Display timestamps along the bottom, by Nick and Stev-io
* (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D)
* Refactored by Krishna Narni, to support timestamp formatting function
*/
function TimeSeries(options) {
options = options || {};
options.resetBoundsInterval = options.resetBoundsInterval || 3000; // Reset the max/min bounds after this many milliseconds
options.resetBounds = options.resetBounds === undefined ? true : options.resetBounds; // Enable or disable the resetBounds timer
this.options = options;
this.data = [];
this.label = options.label || "";
this.maxDataLength = options.maxDataLength || 1000;
this.dataPool = [];
this.maxValue = Number.NaN; // The maximum value ever seen in this time series.
this.minValue = Number.NaN; // The minimum value ever seen in this time series.
// Start a resetBounds Interval timer desired
if (options.resetBounds) {
this.boundsTimer = setInterval((function(thisObj) { return function() { thisObj.resetBounds(); } })(this), options.resetBoundsInterval);
}
}
// Reset the min and max for this timeseries so the graph rescales itself
TimeSeries.prototype.resetBounds = function() {
this.maxValue = Number.NaN;
this.minValue = Number.NaN;
for (var i = 0; i < this.data.length; i++) {
this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, this.data[i][1]) : this.data[i][1];
this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, this.data[i][1]) : this.data[i][1];
}
};
TimeSeries.prototype.append = function(timestamp, value) {
this.lastTimeStamp = timestamp;
var newData = this.dataPool.length ? this.dataPool.pop() : [timestamp, value];
newData[0] = timestamp;
newData[1] = value;
this.data.push(newData);
this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, value) : value;
this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, value) : value;
while(this.data.length > this.maxDataLength)
this.dataPool.push(this.data.shift());
};
function SmoothieChart(options) {
// Defaults
options = options || {};
options.grid = options.grid || { fillStyle:'#000000', strokeStyle: '#777777', lineWidth: 1, millisPerLine: 1000, verticalSections: 2 };
options.millisPerPixel = options.millisPerPixel || 20;
options.fps = options.fps || 50;
options.maxValueScale = options.maxValueScale || 1;
options.minValue = options.minValue;
options.maxValue = options.maxValue;
options.labels = options.labels || { fillStyle:'#ffffff' };
options.interpolation = options.interpolation || "bezier";
options.scaleSmoothing = options.scaleSmoothing || 0.125;
options.maxDataSetLength = options.maxDataSetLength || 2;
options.timestampFormatter = options.timestampFormatter || null;
this.options = options;
this.seriesSet = [];
this.currentValueRange = 1;
this.currentVisMinValue = 0;
}
SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
this.seriesSet.push({timeSeries: timeSeries, options: options || {}});
};
SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
this.seriesSet.splice(this.seriesSet.indexOf(timeSeries), 1);
};
SmoothieChart.prototype.streamTo = function(canvas, delay) {
var self = this;
this.render_on_tick = function() {
//self.render(canvas, new Date().getTime() - (delay || 0));
var timeSeries = self.seriesSet[0].timeSeries;
var dataSet = timeSeries.data;
self.render(canvas, timeSeries.lastTimeStamp);
};
this.start();
};
SmoothieChart.prototype.start = function() {
if (!this.timer)
this.timer = setInterval(this.render_on_tick, 1000/this.options.fps);
};
SmoothieChart.prototype.stop = function() {
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
};
// Sample timestamp formatting function
SmoothieChart.timeFormatter = function(dateObject) {
function pad2(number){return (number < 10 ? '0' : '') + number};
return pad2(dateObject.getHours())+':'+pad2(dateObject.getMinutes())+':'+pad2(dateObject.getSeconds());
};
SmoothieChart.prototype.render = function(canvas, time) {
var canvasContext = canvas.getContext("2d");
var options = this.options;
var dimensions = {top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight};
// Save the state of the canvas context, any transformations applied in this method
// will get removed from the stack at the end of this method when .restore() is called.
canvasContext.save();
// Round time down to pixel granularity, so motion appears smoother.
time = time - time % options.millisPerPixel;
// Move the origin.
canvasContext.translate(dimensions.left, dimensions.top);
// Create a clipped rectangle - anything we draw will be constrained to this rectangle.
// This prevents the occasional pixels from curves near the edges overrunning and creating
// screen cheese (that phrase should neeed no explanation).
canvasContext.beginPath();
canvasContext.rect(0, 0, dimensions.width, dimensions.height);
canvasContext.clip();
// Clear the working area.
canvasContext.save();
canvasContext.fillStyle = options.grid.fillStyle;
canvasContext.clearRect(0, 0, dimensions.width, dimensions.height);
canvasContext.fillRect(0, 0, dimensions.width, dimensions.height);
canvasContext.restore();
// Grid lines....
canvasContext.save();
canvasContext.lineWidth = options.grid.lineWidth || 1;
canvasContext.strokeStyle = options.grid.strokeStyle || '#ffffff';
// Vertical (time) dividers.
if (options.grid.millisPerLine > 0) {
for (var t = time - (time % options.grid.millisPerLine); t >= time - (dimensions.width * options.millisPerPixel); t -= options.grid.millisPerLine) {
canvasContext.beginPath();
var gx = Math.round(dimensions.width - ((time - t) / options.millisPerPixel));
canvasContext.moveTo(gx, 0);
canvasContext.lineTo(gx, dimensions.height);
canvasContext.stroke();
// To display timestamps along the bottom
// May have to adjust millisPerLine to display non-overlapping timestamps, depending on the canvas size
if (options.timestampFormatter){
var tx=new Date(t);
// Formats the timestamp based on user specified formatting function
// SmoothieChart.timeFormatter function above is one such formatting option
var ts = options.timestampFormatter(tx);
var txtwidth=(canvasContext.measureText(ts).width/2)+canvasContext.measureText(minValueString).width + 4;
if (gx<dimensions.width - txtwidth){
canvasContext.fillStyle = options.labels.fillStyle;
// Insert the time string so it doesn't overlap on the minimum value
canvasContext.fillText(ts, gx-(canvasContext.measureText(ts).width / 2), dimensions.height-2);
}
}
canvasContext.closePath();
}
}
// Horizontal (value) dividers.
for (var v = 1; v < options.grid.verticalSections; v++) {
var gy = Math.round(v * dimensions.height / options.grid.verticalSections);
canvasContext.beginPath();
canvasContext.moveTo(0, gy);
canvasContext.lineTo(dimensions.width, gy);
canvasContext.stroke();
canvasContext.closePath();
}
// Bounding rectangle.
canvasContext.beginPath();
canvasContext.strokeRect(0, 0, dimensions.width, dimensions.height);
canvasContext.closePath();
canvasContext.restore();
// Calculate the current scale of the chart, from all time series.
var maxValue = Number.NaN;
var minValue = Number.NaN;
for (var d = 0; d < this.seriesSet.length; d++) {
// TODO(ndunn): We could calculate / track these values as they stream in.
var timeSeries = this.seriesSet[d].timeSeries;
if (!isNaN(timeSeries.maxValue)) {
maxValue = !isNaN(maxValue) ? Math.max(maxValue, timeSeries.maxValue) : timeSeries.maxValue;
}
if (!isNaN(timeSeries.minValue)) {
minValue = !isNaN(minValue) ? Math.min(minValue, timeSeries.minValue) : timeSeries.minValue;
}
}
if (isNaN(maxValue) && isNaN(minValue)) {
canvasContext.restore(); // without this there is crash in Android browser
return;
}
// Scale the maxValue to add padding at the top if required
if (options.maxValue != null)
maxValue = options.maxValue;
else
maxValue = maxValue * options.maxValueScale;
// Set the minimum if we've specified one
if (options.minValue != null)
minValue = options.minValue;
var targetValueRange = maxValue - minValue;
this.currentValueRange += options.scaleSmoothing*(targetValueRange - this.currentValueRange);
this.currentVisMinValue += options.scaleSmoothing*(minValue - this.currentVisMinValue);
var valueRange = this.currentValueRange;
var visMinValue = this.currentVisMinValue;
// For each data set...
for (var d = 0; d < this.seriesSet.length; d++) {
canvasContext.save();
var timeSeries = this.seriesSet[d].timeSeries;
var dataSet = timeSeries.data;
var seriesOptions = this.seriesSet[d].options;
// Delete old data that's moved off the left of the chart.
// We must always keep the last expired data point as we need this to draw the
// line that comes into the chart, but any points prior to that can be removed.
while (dataSet.length >= options.maxDataSetLength && dataSet[1][0] < time - (dimensions.width * options.millisPerPixel)) {
dataSet.splice(0, 1);
}
// Set style for this dataSet.
canvasContext.lineWidth = seriesOptions.lineWidth || 1;
canvasContext.fillStyle = seriesOptions.fillStyle;
canvasContext.strokeStyle = seriesOptions.strokeStyle || '#ffffff';
// Draw the line...
canvasContext.beginPath();
// Retain lastX, lastY for calculating the control points of bezier curves.
var firstX = 0, lastX = 0, lastY = 0;
for (var i = 0; i < dataSet.length; i++) {
// TODO: Deal with dataSet.length < 2.
var x = Math.round(dimensions.width - ((time - dataSet[i][0]) / options.millisPerPixel));
var value = dataSet[i][1];
var offset = value - visMinValue;
var scaledValue = dimensions.height - (valueRange ? Math.round((offset / valueRange) * dimensions.height) : 0);
var y = Math.max(Math.min(scaledValue, dimensions.height - 1), 1); // Ensure line is always on chart.
if (i == 0) {
firstX = x;
canvasContext.moveTo(x, y);
}
// Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
//
// Assuming A was the last point in the line plotted and B is the new point,
// we draw a curve with control points P and Q as below.
//
// A---P
// |
// |
// |
// Q---B
//
// Importantly, A and P are at the same y coordinate, as are B and Q. This is
// so adjacent curves appear to flow as one.
//
else {
switch (options.interpolation) {
case "line":
canvasContext.lineTo(x,y);
break;
case "bezier":
default:
canvasContext.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
x, y); // endPoint (B)
break;
}
}
lastX = x, lastY = y;
}
if (dataSet.length > 0 && seriesOptions.fillStyle) {
// Close up the fill region.
canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
canvasContext.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
canvasContext.fill();
}
canvasContext.stroke();
canvasContext.closePath();
canvasContext.restore();
}
// Draw the axis values on the chart.
if (!options.labels.disabled) {
if(!options.labelOffsetY)
options.labelOffsetY = 0;
canvasContext.fillStyle = options.labels.fillStyle;
var maxValueString = parseFloat(maxValue).toFixed(2);
var minValueString = parseFloat(minValue).toFixed(2);
canvasContext.fillText(maxValueString, dimensions.width - canvasContext.measureText(maxValueString).width - 2, 10);
canvasContext.fillText(minValueString, dimensions.width - canvasContext.measureText(minValueString).width - 2, dimensions.height - 2);
for(var i=0; i<this.seriesSet.length; i++){
var timeSeries = this.seriesSet[i].timeSeries;
var label = timeSeries.label;
canvasContext.fillStyle = timeSeries.options.fillStyle||"rgb(255,255,255)";
if(label) canvasContext.fillText(label, 2, 10*(i+1) + options.labelOffsetY);
}
}
canvasContext.restore(); // See .save() above.
}