From 93468295514af7dfa51e13b9359d5baead4bfed3 Mon Sep 17 00:00:00 2001 From: sbrl Date: Tue, 7 Jun 2016 14:18:44 +0100 Subject: [PATCH] Initial commit. --- .gitignore | 0 BezierCurve.js | 117 ++++++++++++++++++++++++++ HillSet.js | 94 +++++++++++++++++++++ Range.js | 25 ++++++ SmoothLine.js | 139 +++++++++++++++++++++++++++++++ Utils.js | 19 +++++ Vector.js | 220 +++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 20 +++++ main.css | 11 +++ renderer.js | 80 ++++++++++++++++++ 10 files changed, 725 insertions(+) create mode 100644 .gitignore create mode 100644 BezierCurve.js create mode 100644 HillSet.js create mode 100644 Range.js create mode 100644 SmoothLine.js create mode 100644 Utils.js create mode 100644 Vector.js create mode 100644 index.html create mode 100644 main.css create mode 100644 renderer.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/BezierCurve.js b/BezierCurve.js new file mode 100644 index 0000000..a65665e --- /dev/null +++ b/BezierCurve.js @@ -0,0 +1,117 @@ +"use strict"; +/****************************************************************************** + *************************** ES6 Bezier Curve Class *************************** + ****************************************************************************** + * v0.4 + ****************************************************************************** + * A bezier curve class that supports an arbitrary number of control points. + * Originally written for my 2D Graphics coursework at University. + * + * Bug reports can be made as a comment on this gist, or + * sent to . Alternatively, you can tweet + * me at @SBRLabs. + ****************************************************************************** + * Author: Starbeamrainbowlabs + * + * Revisions: + * v0.1: Initial revision. + * v0.2: Include example test page. + * v0.3: Minor comment tweaks. + * v0.4: Fixed a few bugs. + */ + +class BezierCurve +{ + constructor(inControlPoints) + { + this.controlPoints = inControlPoints; + + // The interpolated points cache + this.lastSegmentCount = 0; + this.lastPoints = null; + } + + /** + * Interpolate to find the point at a specific percentage along the bezier curve. + * @param {number} time The time between 0-1 for which to calculate the point along the bezier curve. + * @param {[Vector]} controlPoints An array of control points to operate on. You shouldn't need to set this parameter - it's only here due to the recursive native of the interpolation algorithm. + * @return {Vector} The interpolated point. + */ + interpolate(time, controlPoints) + { + // If the control points are undefined, then pick up our own control points + if(typeof controlPoints == "undefined") + controlPoints = this.controlPoints; + + // Make sure that the time is between 0 and 1. + if(time < 0 || time > 1) + throw new Error(`The time specified was out of bounds! It should be between 0 and 1, but a value of ${time} was provided.`); + + // Create an array to store the interpolated points in + var interpolatedPoints = []; + + // Loop over all the control points, except the last one + for(let i = 0; i < controlPoints.length - 1; i++) + { + // Find the difference between the current point and the next one along + // To get the vector of the line between 2 points, you do b - a for the points a and b. + let difference = controlPoints[i + 1].clone().subtract(controlPoints[i]); + + // Multiply the line's vector by the time in order to extract a percentage along the line + difference.multiply(time); + + // Add the first point on to put the vector back in the right place, + // and then add it to the interpolated pionts array. + // It's important to add the first control point on again here as we + // made the vector relative to 0 in order to perform the + // interpolation rather than relative to the first point on the line + // as it should be. + interpolatedPoints.push(difference.add(controlPoints[i])); + } + + if(interpolatedPoints.length > 1) + { + // We have more than one interpolated point left, recurse to cut it + // down to a single point. + interpolatedPoints = this.interpolate(time, interpolatedPoints); + } + + // Return the first item of the array if we still have an array. If it + // isn't an array, it must mean that one of our recursive calls has + // already broken the vector out of the array. + if(Array.isArray(interpolatedPoints)) + return interpolatedPoints[0]; + else + return interpolatedPoints; + } + + /** + * Add the bezier curve to the current path on context. Remember that this method doesn't call context.beginPath(), nor does it fill or stroke anything. In addition, it does a lineTo the first point, so you'll need to do a moveTo manually if it is desired. + * @param {CanvasRenderingContext2D} context The context to render to. + * @param {number} segmentCount The number of segments to use when rendering the bezier curve. Note that this function caches the interpolation calculations for the last segment value you pass in, so you may want to have multiple different BezierCurve objects if you want to regularly change this value. + */ + curve(context, segmentCount) + { + if(segmentCount != this.lastSegmentCount) + { + // The interpolated points cache doesn't match the specified segment + // count - update it now + this.lastPoints = []; + for(let i = 0; i <= 1; i += 1 / segmentCount) + { + this.lastPoints.push(this.interpolate(i)); + } + + // Update the cached segment count to match the new contents of the + // cache. + this.lastSegmentCount = segmentCount; + } + + // Add the bezier curve to the current path using the cached points + // (that may have been recalculated above). + for(let point of this.lastPoints) + { + context.lineTo(point.x, point.y); + } + } +} diff --git a/HillSet.js b/HillSet.js new file mode 100644 index 0000000..974289e --- /dev/null +++ b/HillSet.js @@ -0,0 +1,94 @@ +"use strict"; + +class HillSet +{ + constructor(inSize, inHeightRange, inColour) + { + if(typeof inSize != "object") inSize = new Vector(2048, 500); + if(typeof inHeightRange != "number") inHeightRange = 0.4; + if(typeof inColour != "string") inColour = "green"; + + this.pos = new Vector(0, 0); + this.startPos = this.pos.clone(); + this.size = inSize; + this.colour = inColour; + this.speed = 300; // in pixels per second + + this.heightRangeMultiplier = inHeightRange; + this.heightRange = this.heightRangeMultiplier * this.size.y; + this.controlPointInterval = 128; // Must devide exactly into pos.size.x + + this.generate(); + } + + generate() + { + this.controlPoints = []; + + for (let x = 0, i = 0; x < this.size.x; x += this.controlPointInterval, i++) + { + this.controlPoints.push(new Vector( + x + (i !== 0 ? random(25) : 0), + random(this.heightRange) + ((random(2) == 0) ? ((this.size.y - this.heightRange) * random(0.5, 1, true)) : 0) + )); + } + + // Make everything as seamless as possible + this.controlPoints[this.controlPoints.length - 2].x = this.size.x - this.controlPoints[2].x; + this.controlPoints[this.controlPoints.length - 2].y = this.size.y; + + this.hillLine = new SmoothLine(); + + var prevPoints = []; + for(let point of this.controlPoints) + prevPoints.push(point.clone().subtract(new Vector(this.size.x, 0))); + prevPoints.pop(); // Remove the last point because it's essentially the same as the first regular point + this.hillLine.add(prevPoints); // The points to the left + + this.hillLine.add(this.controlPoints); // Add the regular points + } + + /** + * Updates the hillset ready for the next frame. + * @param {number} dt The number of seconds since the last frame. + */ + update(dt) + { + this.pos.x += this.speed * dt; + if(this.pos.x >= this.size.x) + this.pos.x = 0; + } + + render(context) + { + context.save(); + context.translate(this.pos.x, this.pos.y); + + context.fillStyle = "red"; + context.beginPath(); + context.ellipse(0, 0, 10, 10, 0, 0, Math.PI * 2, false); + context.fill(); + + context.beginPath(); + context.moveTo(0, this.size.y); + context.lineTo(-this.size.x, this.size.y); + /*for(let point of this.controlPoints) + { + context.lineTo(point.x, point.y); + }*/ + this.hillLine.line(context, 16); + context.lineTo(this.size.x, this.size.y); + context.closePath(); + + context.fillStyle = this.colour; + context.fill(); + + + context.restore(); + } + + toString() + { + return `HillSet @ ${this.pos} (${this.size}), Control Points x${this.controlPoints.length}: ${this.controlPoints.join(", ")}`; + } +} diff --git a/Range.js b/Range.js new file mode 100644 index 0000000..259b5ce --- /dev/null +++ b/Range.js @@ -0,0 +1,25 @@ +/******************************************************************************* + ******************************* ES6 Range Class ******************************* + ******************************************************************************* + * v0.1 + ******************************************************************************* + * A very simple range class. + ******************************************************************************* + * https://gist.github.com/sbrl/a725e32f14a3e4b94810 + * Author: Starbeamrainbowlabs + * + * Changelog: + * v0.1 - 24th Jan 2015: + * Uploaded to GitHub Gist. + */ + +/// Range.js@v0.1 by Starbeamrainbowlabs /// +class Range +{ + constructor(inMin, inMax) { + if(inMin > inMax) + throw new Error(`Min is bigger than max! (min: ${inMin}, max: ${inMax})`); + this.min = inMin; + this.max = inMax; + } +} diff --git a/SmoothLine.js b/SmoothLine.js new file mode 100644 index 0000000..7effd8a --- /dev/null +++ b/SmoothLine.js @@ -0,0 +1,139 @@ +"use strict"; +/******************************************************************************* + **************************** ES6 Smooth Line Class **************************** + ******************************************************************************* + * v0.1 + ******************************************************************************* + * A smooth line class built upon my earlier bezier curve and vector classes. + * + * Given a number of points (not all of which have to be specified at once), + * this class will add the appropriate lineTos to the given drawing context. + * + * This class was originally written on Codepen. Links: + * + * Codepen: https://codepen.io/sbrl/details/zrEyrg/ + * Blog post: (coming soon!) + * + * This class depends on my earler bezier curve and vector classes. Links: + * + * Vector class: https://gist.github.com/sbrl/69a8fa588865cacef9c0 + * Bezier curve class: https://gist.github.com/sbrl/efd57e458e71f8a54171 + * + * Bug reports can be made as a comment on this gist, or + * sent to . Alternatively, you can tweet + * me at @SBRLabs. + ******************************************************************************* + * Author: Starbeamrainbowlabs + * + * Changelog: + * v0.1: Initial revision. + * + */ + +class SmoothLine +{ + constructor() + { + this.points = []; + this.interpolatedPoints = []; + this.bezierCurves = []; + + this.lastPointLength = -1; + } + + /** + * Adds one or more points to the smooth line. + * @param {Vector} point A single vector or an array of vectors to add onto the end of the smooth line. + */ + add(point) + { + if (Array.isArray(point)) + this.points.push(...point); + else + this.points.push(point); + } + + /** + * Internal. Interpolates THe given array of vectors once. + * @param {Vector[]} points The array of vectors to interpolate. + * @param {number} time The percentage between 0 and 1 at which to interpolate. + * @return {Vector[]} The interpolated vectors. + */ + interpolateOnce(points, time) + { + // Input validation checks + if (time < 0 || time > 1) + throw new Error(`The time specified was out of bounds! It should be between 0 and 1, but a value of ${time} was provided.`); + if (!Array.isArray(points)) + throw new Error("THe points provided are not in an array!"); + if (points.length < 3) + throw new Error("A minimum of 3 points are required to draw a smooth line."); + + var result = []; + // Loop over all the points, except the last one + for (let i = 0; i < points.length - 1; i++) { + // Find the difference between the current point and the next one along + // To get the vector of the line between 2 points, you do b - a for the points a and b. + let difference = points[i + 1].clone().subtract(points[i]); + + // Multiply the line's vector by the time in order to extract a percentage along the line + difference.multiply(time); + + // Add the first point on to put the vector back in the right place, + // and then add it to the interpolated pionts array. + // It's important to add the first control point on again here as we + // made the vector relative to 0 in order to perform the + // interpolation rather than relative to the first point on the line + // as it should be. + result.push(difference.add(points[i])); + } + + return result; + } + + /** + * Adds the smooth line to the path of the given canvas drawing context. + * @param {CanvasDrawingContext2D} context The drawing context to add the smooth line to. + * @param {number} segmentCount The number of segments that each bezier curve should have. + */ + line(context, segmentCount) + { + if (this.points.length < 3) + throw new Error(`At least 3 points are required to draw a smooth line, but only ${this.points.length} points are currently specified.`); + + if (this.lastPointLength !== this.points.length) + { + // Reset the bezier curve cache + this.bezierCurves = []; + + this.interpolatedPoints = this.interpolateOnce(this.points, 0.5); + // Loop over every point except the frst & last ones + for (let i = 1; i < this.points.length - 1; i++) + { + let nextPointSet = [ + this.interpolatedPoints[i - 1], + this.points[i], + this.interpolatedPoints[i] + ]; + + // If this is the first iteration, make the first point of the bezier curve the first point that we were given + if (i == 1) + nextPointSet[0] = this.points[0]; + // If this is the last iteration, make the end point of the bezier curve the last point we were given + if (i == this.points.length - 2) + nextPointSet[2] = this.points[this.points.length - 1]; + // The above 2 checks are needed to make sure that the smooth line starts and ends at the points that we were given + + let nextBezier = new BezierCurve(nextPointSet); + this.bezierCurves.push(nextBezier); + } + } + + // Spin through all the bezier curves and get them to add themselves to the current path + for (let i = 0; i < this.bezierCurves.length; i++) + this.bezierCurves[i].curve(context, segmentCount); + + // Update the cached poits length + this.lastPointLength = this.points.length; + } +} diff --git a/Utils.js b/Utils.js new file mode 100644 index 0000000..831a5f2 --- /dev/null +++ b/Utils.js @@ -0,0 +1,19 @@ +/** + * Bounded random number generator. Has 3 forms: + * + ** Form 1 ** + * @param {number} a The minimum value. + * @param {number} b The maximum value. + * @param {boolean} c Whether the resulting number should be a float. [optional] + * @return {number} A random number. + * + ** Form 2 ** + * @param {number} a The maximum value. + * @param {boolean} b Whether the resulting number should be a float. [optional] + * @return {number} A random number. + * + ** Form 3 ** + * @return {number} A random number. + */ +function random(a,b,c,d) +{d=Math.random();if(typeof a!="number")return d;a=typeof b=="number"?d*(a-b)+b:d*a;a=(typeof b!="number"?b:c)?a:Math.floor(a);return a;} diff --git a/Vector.js b/Vector.js new file mode 100644 index 0000000..cb132d6 --- /dev/null +++ b/Vector.js @@ -0,0 +1,220 @@ +"use strict"; + +/****************************************************** + ************** Simple ES6 Vector Class ************** + ****************************************************** + * Author: Starbeamrainbowlabs + * Twitter: @SBRLabs + * Email: feedback at starbeamrainbowlabs dot com + * + * From https://gist.github.com/sbrl/69a8fa588865cacef9c0 + ****************************************************** + * Originally written for my 2D Graphics ACW at Hull + * University. + ****************************************************** + * Changelog + ****************************************************** + * 19th December 2015: + * Added this changelog. + * 28th December 2015: + * Rewrite tests with klud.js + Node.js + * 30th January 2016: + * Tweak angleFrom function to make it work properly. + * 31st January 2016: + * Add the moveTowards function. + * Add the minComponent getter. + * Add the maxComponent getter. + * Add the equalTo function. + * Tests still need to be written for all of the above. + */ + +class Vector { + // Constructor + constructor(inX, inY) { + if(typeof inX != "number") + throw new Error("Invalid x value."); + if(typeof inY != "number") + throw new Error("Invalid y value."); + + // Store the (x, y) coordinates + this.x = inX; + this.y = inY; + } + + /** + * Add another vector to this vector. + * @param {Vector} v The vector to add. + * @return {Vector} The current vector. useful for daisy-chaining calls. + */ + add(v) { + this.x += v.x; + this.y += v.y; + + return this; + } + + /** + * Take another vector from this vector. + * @param {Vector} v The vector to subtrace from this one. + * @return {Vector} The current vector. useful for daisy-chaining calls. + */ + subtract(v) { + this.x -= v.x; + this.y -= v.y; + + return this; + } + + /** + * Divide the current vector by a given value. + * @param {number} value The value to divide by. + * @return {Vector} The current vector. Useful for daisy-chaining calls. + */ + divide(value) { + if(typeof value != "number") + throw new Error("Can't divide by non-number value."); + + this.x /= value; + this.y /= value; + + return this; + } + + /** + * Multiply the current vector by a given value. + * @param {number} value The number to multiply the current vector by. + * @return {Vector} The current vector. useful for daisy-chaining calls. + */ + multiply(value) { + if(typeof value != "number") + throw new Error("Can't multiply by non-number value."); + + this.x *= value; + this.y *= value; + + return this; + } + + /** + * Move the vector towards the given vector by the given amount. + * @param {Vector} v The vector to move towards. + * @param {number} amount The distance to move towards the given vector. + */ + moveTowards(v, amount) + { + // From http://stackoverflow.com/a/2625107/1460422 + var dir = new Vector( + v.x - this.x, + v.y - this.y + ).limitTo(amount); + this.x += dir.x; + this.y += dir.y; + + return this; + } + + /** + * Limit the length of the current vector to value without changing the + * direction in which the vector is pointing. + * @param {number} value The number to limit the current vector's length to. + * @return {Vector} The current vector. useful for daisy-chaining calls. + */ + limitTo(value) { + if(typeof value != "number") + throw new Error("Can't limit to non-number value."); + + this.divide(this.length); + this.multiply(value); + + return this; + } + + /** + * Return the dot product of the current vector and another vector. + * @param {Vector} v The other vector we should calculate the dot product with. + * @return {Vector} The current vector. useful for daisy-chaining calls. + */ + dotProduct(v) { + return (this.x * v.x) + (this.y * v.y); + } + + /** + * Calculate the angle, in radians, from north to another vector. + * @param {Vector} v The other vector to which to calculate the angle. + * @return {Vector} The current vector. useful for daisy-chaining calls. + */ + angleFrom(v) { + // From http://stackoverflow.com/a/16340752/1460422 + var angle = Math.atan2(v.y - this.y, v.x - this.x) - (Math.PI / 2); + angle += Math.PI/2; + if(angle < 0) angle += Math.PI * 2; + return angle; + } + + /** + * Clones the current vector. + * @return {Vector} A clone of the current vector. Very useful for passing around copies of a vector if you don't want the original to be altered. + */ + clone() { + return new Vector(this.x, this.y); + } + + /* + * Returns a representation of the current vector as a string. + * @returns {string} A representation of the current vector as a string. + */ + toString() { + return `(${this.x}, ${this.y})`; + } + + /** + * Whether the vector is equal to another vector. + * @param {Vector} v The vector to compare to. + * @return {boolean} Whether the current vector is equal to the given vector. + */ + equalTo(v) + { + if(this.x == v.x && this.y == v.y) + return true; + else + return false; + } + + /** + * Get the unit vector of the current vector - that is a vector poiting in the same direction with a length of 1. Note that this does *not* alter the original vector. + * @return {Vector} The current vector's unit form. + */ + get unitVector() { + var length = this.length; + return new Vector( + this.x / length, + this.y / length); + } + + /** + * Get the length of the current vector. + * @return {number} The length of the current vector. + */ + get length() { + return Math.sqrt((this.x * this.x) + (this.y * this.y)); + } + + /** + * Get the value of the minimum component of the vector. + * @return {number} The minimum component of the vector. + */ + get minComponent() { + return Math.min(this.x, this.y); + } + + /** + * Get the value of the maximum component of the vector. + * @return {number} The maximum component of the vector. + */ + get maxComponent() { + return Math.min(this.x, this.y); + } +} +// Make Vector.js Node.js friendly +if(typeof module != "undefined" && module.exports) + module.exports = Vector; diff --git a/index.html b/index.html new file mode 100644 index 0000000..2364088 --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + Parallax Test + + + + + + + + + + + + + + + diff --git a/main.css b/main.css new file mode 100644 index 0000000..41c372c --- /dev/null +++ b/main.css @@ -0,0 +1,11 @@ +html, body { font-size: 100%; } +body +{ + font-family: sans-serif; +} + +#canvas-main +{ + position: absolute; + top: 0; right: 0; bottom: 0; left: 0; +} diff --git a/renderer.js b/renderer.js new file mode 100644 index 0000000..9fdb105 --- /dev/null +++ b/renderer.js @@ -0,0 +1,80 @@ +"use strict"; + +class Renderer +{ + constructor(canvas) + { + this.canvas = canvas; + this.context = canvas.getContext("2d"); + + this.trackWindowSize(); + + this.setup(); + } + + setup() + { + this.hillSet = new HillSet(); + console.log(this.hillSet.toString()); + } + + start() + { + this.lastTime = +new Date(); + this.nextFrame(); + } + + nextFrame() + { + let startTime = +new Date(); + this.update((+new Date() - this.lastTime) / 1000); + this.render(this.canvas, this.context); + + this.lastTime = startTime; + + requestAnimationFrame(this.nextFrame.bind(this)); + } + + /** + * Updates the simluation ready for the next frame. + * @param {number} dt The number of seconds since the last frame was rendered. + */ + update(dt) + { + this.hillSet.update(dt); + } + + render(canvas, context) + { + context.clearRect(0, 0, canvas.width, canvas.height); + + /*context.fillStyle = "red"; + context.fillRect(10, 10, 100, 100);*/ + this.hillSet.render(this.context); + } + + /** + * Updates the canvas size to match the current viewport size. + */ + matchWindowSize() { + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + + //this.render(this.context); + } + + /** + * Makes the canvas size track the window size. + */ + trackWindowSize() { + this.matchWindowSize(); + window.addEventListener("resize", this.matchWindowSize.bind(this)); + } +} + +window.addEventListener("load", function (event) { + var canvas = document.getElementById("canvas-main"), + renderer = new Renderer(canvas); + renderer.start(); + window.renderer = renderer; +});