UNPKG

cannon-es-control

Version:

A lightweight 3D physics engine written in JavaScript with control system tools

384 lines (345 loc) 15.5 kB
// 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. } export { TimeSeries, SmoothieChart }