Havent done a pen in the longest time, decided to put this demo together after playing around with IK, the effect turned out pretty cool! (in my biased opinion of course)
A Pen by Jason Brown on CodePen.
| <canvas id="canvas"></canvas> |
| 'use strict'; | |
| // requestanimation polyfill | |
| (function () { | |
| var lastTime = 0; | |
| var vendors = ['webkit', 'moz']; | |
| for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { | |
| window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; | |
| window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; | |
| } | |
| if (!window.requestAnimationFrame) window.requestAnimationFrame = function (callback, element) { | |
| var currTime = new Date().getTime(); | |
| var timeToCall = Math.max(0, 16 - (currTime - lastTime)); | |
| var id = window.setTimeout(function () { | |
| callback(currTime + timeToCall); | |
| }, | |
| timeToCall); | |
| lastTime = currTime + timeToCall; | |
| return id; | |
| }; | |
| if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function (id) { | |
| clearTimeout(id); | |
| }; | |
| }()); | |
| var canvas = document.getElementById("canvas"); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| var Segment = function (settings) { | |
| settings = settings || {}; | |
| this.angle = settings.angle || 0; | |
| this.x = settings.x || 0; | |
| this.y = settings.y || 0; | |
| this.width = settings.width || 10; | |
| this.height = settings.height || 2; | |
| this.color = settings.color || { | |
| r: 255, | |
| g: 50, | |
| b: 100 | |
| }; | |
| } | |
| Segment.prototype.getJoint = function () { | |
| var x = this.x + Math.cos(this.angle) * this.width, | |
| y = this.y + Math.sin(this.angle) * this.width; | |
| return { | |
| x: x, | |
| y: y | |
| }; | |
| }; | |
| Segment.prototype.render = function (ctx) { | |
| ctx.fillStyle = "rgb(" + this.color.r + "," + this.color.g + "," + this.color.b + ")"; | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.angle); | |
| ctx.fillRect(-this.height / 2, -this.height / 2, this.width + this.height, this.height); | |
| ctx.restore(); | |
| }; | |
| var Snake = function (settings) { | |
| settings = settings || {}; | |
| this.x = settings.x || 0; | |
| this.y = settings.y || 0; | |
| this.color = settings.color || { | |
| r: ~~ (Math.random() * 255), | |
| g: ~~ (Math.random() * 255), | |
| b: ~~ (Math.random() * 255) | |
| }; | |
| this.vX = 0; | |
| this.vY = 0; | |
| this.wiggle = 0; | |
| this.bounds = settings.bounds || { | |
| x: 0, | |
| y: 0, | |
| width: 100, | |
| height: 100 | |
| }; | |
| this.segNum = settings.segNum || 10; | |
| this.angle = (Math.random() * 360 - 180) * Math.PI / 180; | |
| this.setTarget(settings.target); | |
| this.segments = []; | |
| this.segments.push(new Segment({ | |
| x: this.x, | |
| y: this.y, | |
| angle: this.angle, | |
| color: this.color | |
| })); | |
| for (var s = 1; s < this.segNum; s++) { | |
| this.segments.push(new Segment({ | |
| angle: (Math.random() * 360 - 180) * Math.PI / 180, | |
| color: this.color | |
| })); | |
| } | |
| } | |
| Snake.prototype.setTarget = function (target) { | |
| var tX = Math.random() * this.bounds.width, | |
| tY = Math.random() * this.bounds.height; | |
| if (target) { | |
| tX = target.x; | |
| tY = target.y; | |
| } | |
| this.target = { | |
| x: tX, | |
| y: tY | |
| }; | |
| }; | |
| Snake.prototype.move = function (segment, x, y) { | |
| var dx = x - segment.x, | |
| dy = y - segment.y; | |
| segment.angle = Math.atan2(dy, dx); | |
| var w = segment.getJoint().x - segment.x, | |
| h = segment.getJoint().y - segment.y; | |
| segment.x = x - w; | |
| segment.y = y - h; | |
| } | |
| Snake.prototype.update = function () { | |
| this.move(this.segments[0], this.x, this.y); | |
| var move = this.move, | |
| color = this.color; | |
| this.segments.forEach(function (e, i, arr) { | |
| e.color = color; | |
| if (i !== 0) { | |
| move(e, arr[i - 1].x, arr[i - 1].y); | |
| } | |
| }); | |
| }; | |
| Snake.prototype.render = function (ctx) { | |
| this.segments.forEach(function (e) { | |
| e.render(ctx); | |
| }); | |
| }; | |
| var Light = function (settings) { | |
| this.ease = settings.ease || 20; | |
| this.bounds = settings.bounds || { | |
| x: 0, | |
| y: 0, | |
| width: 100, | |
| height: 100 | |
| }; | |
| this.angle = 0; | |
| this.x = Math.random() * this.bounds.width/2; | |
| this.y = Math.random() * this.bounds.height/2; | |
| this.setTarget(); | |
| }; | |
| Light.prototype.update = function () { | |
| var dX = this.target.x - this.x, | |
| dY = this.target.y - this.y, | |
| dist = Math.sqrt(dX * dX - dY * dY); | |
| if (dist > this.ease) { | |
| this.x += dX / this.ease; | |
| this.y += dY / this.ease; | |
| } else { | |
| this.setTarget(); | |
| } | |
| }; | |
| Light.prototype.setTarget = function () { | |
| var tX = Math.random() * this.bounds.width, | |
| tY = Math.random() * this.bounds.height; | |
| this.target = { | |
| x: tX, | |
| y: tY | |
| }; | |
| }; | |
| function Demo(canvas) { | |
| var that = this; | |
| this.canvas = canvas; | |
| this.ctx = canvas.getContext("2d"); | |
| this.ctx.fillStyle = "#000"; | |
| this.req = null; | |
| this.settings = { | |
| brightness: 120, | |
| lightness: 120, | |
| snakeSpread: 30, | |
| mouseFollow: false, | |
| snakeCount: 100, | |
| snakeLength: 8, | |
| snakeSpeed: 5, | |
| restart: function () { | |
| that.init(); | |
| } | |
| }; | |
| this.mouseX = 0; | |
| this.mouseY = 0; | |
| this.canvas.addEventListener("mousemove", function (e) { | |
| that.mouseX = e.pageX; | |
| that.mouseY = e.pageY; | |
| }); | |
| this.update = function () { | |
| this.brightness = this.settings.brightness; | |
| this.lightness = this.settings.lightness; | |
| this.ctx.fillStyle = "rgba(0,0,0,0.5)"; | |
| this.ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| this.light.update(); | |
| for (var s = 0, sLen = this.snakes.length; s < sLen; s++) { | |
| var snake = this.snakes[s], | |
| dX = snake.x - snake.target.x, | |
| dY = snake.y - snake.target.y, | |
| heading = Math.atan2(dY, dX), | |
| dist = Math.sqrt(dX * dX + dY * dY); | |
| snake.angle = heading + Math.cos(snake.wiggle += 0.1); | |
| snake.vX = Math.cos(snake.angle) * this.settings.snakeSpeed; | |
| snake.vY = Math.sin(snake.angle) * this.settings.snakeSpeed; | |
| snake.color = this.colorCycle(this.cycle, this.brightness, this.lightness); | |
| if (dist > 10) { | |
| snake.x -= snake.vX; | |
| snake.y -= snake.vY; | |
| } else { | |
| var x = this.light.x + this.snakeSpread - Math.random() * this.snakeSpread, | |
| y = this.light.y + this.snakeSpread - Math.random() * this.snakeSpread; | |
| if (this.settings.mouseFollow) { | |
| x = this.mouseX - this.snakeSpread*2 + Math.random() * this.snakeSpread*5; | |
| y = this.mouseY - this.snakeSpread*2 + Math.random() * this.snakeSpread*5; | |
| } | |
| snake.setTarget({ | |
| x: x, | |
| y: y | |
| }); | |
| } | |
| snake.update(); | |
| snake.render(this.ctx); | |
| } | |
| this.cycle += 0.4; | |
| this.req = requestAnimationFrame(this.update.bind(this)); | |
| } | |
| this.colorCycle = function (cycle, bright, light) { | |
| bright = bright || 255; | |
| light = light || 0; | |
| cycle *= .1; | |
| var r = ~~ (Math.sin(.3 * cycle + 0) * bright + light), | |
| g = ~~ (Math.sin(.3 * cycle + 2) * bright + light), | |
| b = ~~ (Math.sin(.3 * cycle + 4) * bright + light); | |
| return { | |
| r: Math.min(r, 255), | |
| g: Math.min(g, 255), | |
| b: Math.min(b, 255) | |
| }; | |
| } | |
| this.init = function () { | |
| cancelAnimationFrame(this.req); | |
| this.snakes = []; | |
| this.light = null; | |
| this.cycle = 0; | |
| this.brightness = this.settings.brightness; | |
| this.lightness = this.settings.lightness; | |
| this.snakeSpread = this.settings.snakeSpread; | |
| this.snakeCount = this.settings.snakeCount; | |
| this.light = new Light({ | |
| bounds: { | |
| x: 0, | |
| y: 0, | |
| width: this.canvas.width, | |
| height: this.canvas.height, | |
| ease: 25 - this.settings.snakeSpeed | |
| } | |
| }); | |
| for (var s = 0; s < this.snakeCount; s++) { | |
| var x = canvas.width/2 - Math.random() * canvas.width/4, | |
| y = canvas.height/2 - Math.random() * canvas.height/4; | |
| this.snakes.push(new Snake({ | |
| x: x, | |
| y: y, | |
| segNum: this.settings.snakeLength, | |
| bounds: { | |
| x: 0, | |
| y: 0, | |
| width: this.canvas.width, | |
| height: this.canvas.height | |
| }, | |
| target: { | |
| x: this.light.y, | |
| y: this.light.x | |
| } | |
| })); | |
| } | |
| this.update(); | |
| } | |
| var gui = new dat.GUI(); | |
| gui.add(this.settings, 'mouseFollow'); | |
| gui.add(this.settings, 'brightness', 1, 255); | |
| gui.add(this.settings, 'lightness', 1, 255); | |
| var snakeSettings = gui.addFolder('Snake Settings'); | |
| snakeSettings.add(this.settings, 'snakeCount', 1, 300); | |
| snakeSettings.add(this.settings, 'snakeLength', 1, 50); | |
| snakeSettings.add(this.settings, 'snakeSpeed', 1, 10); | |
| gui.add(this.settings, 'restart'); | |
| this.init(); | |
| } | |
| var demo = null; | |
| setTimeout(function () { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| demo = new Demo(canvas); | |
| }, 100); |
| body { | |
| margin:0; | |
| padding:0; | |
| overflow:hidden; | |
| } | |
| canvas { | |
| position:absolute; | |
| } |
Havent done a pen in the longest time, decided to put this demo together after playing around with IK, the effect turned out pretty cool! (in my biased opinion of course)
A Pen by Jason Brown on CodePen.