Attempt at replicating the Jelly effect from The Floor is Jelly (http://thefloorisjelly.com/).
A Pen by Thom Chiovoloni on CodePen.
| <div id="content"> | |
| <div id="wrap"> | |
| <div id="main" class="blob"> | |
| <canvas id="screen" width="700" height="500"> | |
| HTML5 Canvas support required. | |
| </canvas> | |
| </div> | |
| <div id="sidebar" class="blob"> | |
| <span id="fps" class="blob">0 fps</span> | |
| <div id="controls"> | |
| <label class="blob option" data-active="1"> | |
| <input type="checkbox" id="draw-curvy" checked> | |
| Draw Curvy | |
| </label> | |
| <label class="blob option" data-active="0"> | |
| <input type="checkbox" id="draw-outline"> | |
| Draw Outline | |
| </label> | |
| <label class="blob option" data-active="1"> | |
| <input type="checkbox" id="draw-jellies" checked> | |
| Draw Points | |
| </label> | |
| <label class="blob option" data-active="0"> | |
| <input type="checkbox" id="draw-anchors"> | |
| Draw Anchors | |
| </label> | |
| <label class="blob option" data-active="1"> | |
| <input type="checkbox" id="draw-mouse" checked> | |
| Draw Mouse | |
| </label> | |
| <a class="blob" href="http://thomcc.io"> | |
| thomcc.io | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> |
Attempt at replicating the Jelly effect from The Floor is Jelly (http://thefloorisjelly.com/).
A Pen by Thom Chiovoloni on CodePen.
| /* | |
| Copyright (c) 2014 Thom Chiovoloni (web: thomcc.io, github: github.com/thomcc) | |
| 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. | |
| */ | |
| (function() { | |
| "use strict"; | |
| var COLOR_LINE = "rgba(243, 218, 131, 0.5)"; | |
| var COLOR_FILL = "#ed8958"; | |
| var COLOR_JELLY_DOT = "rgb(230, 90, 70)"; | |
| var COLOR_ANCHOR_DOT = "rgba(152, 65, 52, 0.5)"; | |
| var COLOR_MOUSE_FILL = "rgba(141, 46, 86, 0.5)"; | |
| var COLOR_MOUSE_STROKE = "rgba(170, 60, 82, 0.5)"; | |
| var SCALE = 50; | |
| var DOT_RADIUS = 6; | |
| var ANCHOR_STIFFNESS = 0.9; | |
| var ANCHOR_DAMP = 0.7; | |
| var MOUSE_FORCE = 5; | |
| var MOUSE_RADIUS = SCALE; | |
| var SIMULATION_RATE = 60; | |
| var XOFF = 1.5; | |
| var YOFF = 1.5; | |
| var MAX_ACROSS_NEIGHBOR_DIST = SCALE; | |
| var POINTS = [ | |
| [0, 0], [1, 0], [2, 0], [3, 0], | |
| [3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1], [9, 1], | |
| [9, 2], [8, 2], [7, 2], [6, 2], [5, 2], | |
| [5, 3], [4, 3], [3, 3], | |
| [3, 4], [3, 5], [3, 6], [3, 7], | |
| [2, 7], [2, 6], [2, 5], [2, 4], [2, 3], | |
| [1, 3], [0, 3], | |
| [0, 2], [0, 1] // final [0, 0] is implicit | |
| ].map(function(xy) { return [(xy[0] + XOFF) * SCALE, (xy[1] + YOFF) * SCALE]; }); | |
| function Vec2(x, y) { this.x = x; this.y = y; } | |
| Vec2.prototype.set = function(x, y) { this.x = x; this.y = y; return this; }; | |
| Vec2.prototype.copy = function(v) { return this.set(v.x, v.y); }; | |
| Vec2.prototype.translate = function(x, y) { return this.set(this.x + x, this.y + y); }; | |
| Vec2.prototype.scale = function(v) { return this.set(this.x * v, this.y * v); }; | |
| Vec2.prototype.distance = function(o) { | |
| var dx = this.x - o.x, dy = this.y - o.y; | |
| return Math.sqrt(dx * dx + dy * dy); | |
| }; | |
| function JellyPoint(x, y) { | |
| this.pos = new Vec2(x, y); | |
| this.last = new Vec2(x, y); | |
| this.anchor = new Vec2(x, y); | |
| this.vel = new Vec2(0, 0); | |
| this.neighbors = []; | |
| } | |
| JellyPoint.prototype.addAcrossNeighbor = function(n) { | |
| this.addNeighbor(n, 1, 2); | |
| }; | |
| JellyPoint.prototype.addNeighbor = function(n, c, s) { | |
| this.neighbors.push({ | |
| pos: n.pos, vel: n.vel, dist: this.pos.distance(n.pos), compress: c, strength: s | |
| }); | |
| }; | |
| JellyPoint.prototype.setNeighbors = function(p, n) { | |
| this.addNeighbor(p, 30, 0.5); | |
| }; | |
| JellyPoint.prototype.move = function(t, m) { | |
| if (m.down) { | |
| var dx = m.pos.x - this.pos.x; | |
| var dy = m.pos.y - this.pos.y; | |
| var dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < MOUSE_RADIUS) { | |
| this.vel.x -= dx * MOUSE_FORCE; | |
| this.vel.y -= dy * MOUSE_FORCE; | |
| } | |
| } | |
| this.vel.scale(ANCHOR_DAMP); | |
| var offx = (this.anchor.x - this.pos.x) * ANCHOR_STIFFNESS; | |
| var offy = (this.anchor.y - this.pos.y) * ANCHOR_STIFFNESS; | |
| this.vel.translate(offx, offy); | |
| var time = t * t * 0.5; | |
| var nx = this.pos.x + (this.pos.x - this.last.x) * 0.9 + this.vel.x * time; | |
| var ny = this.pos.y + (this.pos.y - this.last.y) * 0.9 + this.vel.y * time; | |
| this.last.copy(this.pos); | |
| this.pos.set(nx, ny); | |
| }; | |
| JellyPoint.prototype.think = function() { | |
| for (var i = 0, len = this.neighbors.length; i < len; i++) { | |
| var n = this.neighbors[i]; | |
| var dx = this.pos.x - n.pos.x; | |
| var dy = this.pos.y - n.pos.y; | |
| var d = Math.sqrt(dx * dx + dy * dy); | |
| var a = (n.dist - d) / d * n.strength; | |
| if (d < n.dist) { | |
| a /= n.compress; | |
| } | |
| var ox = dx * a; | |
| var oy = dy * a; | |
| this.vel.translate(+ox, +oy); | |
| n.vel.translate(-ox, -oy); | |
| } | |
| }; | |
| function JellyIsland(pts) { | |
| this.points = []; | |
| for (var i = 0, ptslen = pts.length; i < ptslen; i++) { | |
| this.points.push(new JellyPoint(pts[i][0], pts[i][1])); | |
| } | |
| // fixme: finding across neighbors makes this O(n^2) | |
| for (var i = 0, len = this.points.length; i < len; i++) { | |
| var jp = this.points[i]; | |
| var pi = i === 0 ? len-1 : i - 1; | |
| var ni = i === len-1 ? 0 : i + 1; | |
| jp.setNeighbors(this.points[pi], this.points[ni]); | |
| for (var j = 0; j < len; j++) { | |
| var ojp = this.points[j]; | |
| if (ojp !== jp && ojp !== this.points[pi] && ojp !== this.points[ni] | |
| && jp.pos.distance(ojp.pos) <= MAX_ACROSS_NEIGHBOR_DIST) { | |
| jp.addAcrossNeighbor(ojp); | |
| } | |
| } | |
| } | |
| } | |
| JellyIsland.prototype.update = function(mouse) { | |
| var i, len = this.points.length; | |
| for (i = 0; i < len; i++) this.points[i].think(); | |
| for (i = 0; i < len; i++) this.points[i].move(SIMULATION_RATE / 1000, mouse); | |
| }; | |
| function DrawOption(elem) { | |
| this.value = !!elem.checked; | |
| var self = this; | |
| elem.onclick = function() { | |
| self.value = !!this.checked; | |
| this.parentNode.setAttribute('data-active', self.value?"1":"0"); | |
| }; | |
| } | |
| function DrawOptions() { | |
| this.drawAnchors = new DrawOption(document.getElementById('draw-anchors')); | |
| this.drawJellies = new DrawOption(document.getElementById('draw-jellies')); | |
| this.drawMouse = new DrawOption(document.getElementById('draw-mouse')); | |
| this.drawOutline = new DrawOption(document.getElementById('draw-outline')); | |
| this.drawCurvy = new DrawOption(document.getElementById('draw-curvy')); | |
| } | |
| DrawOptions.prototype.shouldDrawAnchors = function() { return this.drawAnchors.value; }; | |
| DrawOptions.prototype.shouldDrawJellies = function() { return this.drawJellies.value; }; | |
| DrawOptions.prototype.shouldDrawMouse = function() { return this.drawMouse.value; }; | |
| DrawOptions.prototype.shouldDrawOutline = function() { return this.drawOutline.value; }; | |
| DrawOptions.prototype.shouldDrawCurvy = function() { return this.drawCurvy.value; }; | |
| function Screen(view) { | |
| this.dImg = this.cacheDotImg(COLOR_JELLY_DOT); | |
| this.aImg = this.cacheDotImg(COLOR_ANCHOR_DOT); | |
| this.view = view; | |
| this.ctx = this.view.getContext('2d'); | |
| this.ctx.lineWidth = 4; | |
| this.ctx.lineCap = 'round'; | |
| this.ctx.lineJoin = 'round'; | |
| this.opts = new DrawOptions(); | |
| } | |
| Screen.prototype.cacheDotImg = function(color) { | |
| var c = document.createElement('canvas'); | |
| c.width = (DOT_RADIUS+5)*2; | |
| c.height = (DOT_RADIUS+5)*2; | |
| var x = c.getContext('2d'); | |
| x.lineWidth = 3; | |
| x.lineCap = 'round'; | |
| x.lineJoin = 'round'; | |
| x.strokeStyle = color; | |
| x.beginPath(); | |
| x.arc(DOT_RADIUS+5, DOT_RADIUS+5, DOT_RADIUS, 0, Math.PI*2, true); | |
| x.stroke(); | |
| return c; | |
| }; | |
| Screen.prototype.clear = function() { | |
| this.ctx.clearRect(0, 0, this.view.width, this.view.height); | |
| }; | |
| Screen.prototype.drawDots = function(jellies, which, img) { | |
| for (var i = 0, len = jellies.length; i < len; i++) { | |
| this.ctx.drawImage(img, jellies[i][which].x-img.width/2, jellies[i][which].y-img.height/2, img.width, img.height); | |
| // this.ctx.beginPath(); | |
| // this.ctx.arc(jellies[i][which].x, jellies[i][which].y, DOT_RADIUS, 0, Math.PI * 2, true); | |
| // this.ctx.stroke(); | |
| } | |
| }; | |
| Screen.prototype.curveBetween = function(p0, p1) { | |
| this.ctx.quadraticCurveTo(p0.x, p0.y, (p0.x+p1.x) * 0.5, (p0.y+p1.y) * 0.5); | |
| }; | |
| Screen.prototype.outlineCurvePath = function(jellies) { | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(jellies[0].pos.x, jellies[0].pos.y); | |
| for (var i = 0, jlen=jellies.length; i <= jlen; ++i) { | |
| var p0 = jellies[i+0 >= jlen ? i+0-jlen : i+0].pos; | |
| var p1 = jellies[i+1 >= jlen ? i+1-jlen : i+1].pos; | |
| this.ctx.quadraticCurveTo(p0.x, p0.y, (p0.x+p1.x) * 0.5, (p0.y+p1.y) * 0.5) | |
| } | |
| }; | |
| Screen.prototype.outlineSolidPath = function(jellies) { | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(jellies[0].pos.x, jellies[0].pos.y); | |
| for (var idx = 1, jlen = jellies.length; idx < jlen; ++idx) | |
| this.ctx.lineTo(jellies[idx].pos.x, jellies[idx].pos.y); | |
| this.ctx.closePath(); | |
| }; | |
| Screen.prototype.drawIsland = function(island) { | |
| var jellies = island.points; | |
| var jlen = jellies.length; | |
| this.ctx.fillStyle = COLOR_FILL; | |
| var curvy = this.opts.shouldDrawCurvy(); | |
| if (curvy) { | |
| this.outlineCurvePath(jellies); | |
| this.ctx.fill(); | |
| } | |
| var outline = this.opts.shouldDrawOutline(); | |
| if (outline || !curvy) { | |
| if (outline) this.ctx.strokeStyle = COLOR_LINE; | |
| this.outlineSolidPath(jellies); | |
| if (outline) this.ctx.stroke(); | |
| if (!curvy) this.ctx.fill(); | |
| } | |
| if (this.opts.shouldDrawAnchors()) this.drawDots(jellies, 'anchor', this.aImg); | |
| if (this.opts.shouldDrawJellies()) this.drawDots(jellies, 'pos', this.dImg); | |
| }; | |
| Screen.prototype.drawMouse = function(m) { | |
| if (!this.opts.shouldDrawMouse() || !m.down) return; | |
| this.ctx.fillStyle = COLOR_MOUSE_FILL; | |
| this.ctx.strokeStyle = COLOR_MOUSE_STROKE; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(m.pos.x, m.pos.y, MOUSE_RADIUS, 0, Math.PI * 2, true); | |
| this.ctx.stroke(); | |
| this.ctx.fill(); | |
| }; | |
| function Mouse(canvas) { | |
| this.pos = new Vec2(0, 0); | |
| this.down = false; | |
| var self = this; | |
| canvas.onmousemove = function(e) { | |
| var r = canvas.getBoundingClientRect(); | |
| self.pos.set(e.clientX - r.left, e.clientY - r.top); | |
| return e.preventDefault(); | |
| }; | |
| canvas.onmouseup = function(e) { | |
| self.down = false; | |
| return e.preventDefault(); | |
| }; | |
| canvas.onmousedown = function(e) { | |
| self.down = true; | |
| var r = canvas.getBoundingClientRect(); | |
| self.pos.set(e.clientX - r.left, e.clientY - r.top); | |
| return e.preventDefault(); | |
| }; | |
| } | |
| var animationFrame = window.requestAnimationFrame || | |
| window.webkitRequestAnimationFrame || | |
| window.mozRequestAnimationFrame || | |
| window.oRequestAnimationFrame || | |
| window.msRequestAnimationFrame || | |
| function (callback) { window.setTimeout(callback, 1000 / 60); }; | |
| function JellyDemo(canvas, points) { | |
| this.canvas = canvas; | |
| this.canvasCtx = this.canvas.getContext('2d'); | |
| this.buffer = this.createBuffer(this.canvas); | |
| this.screen = new Screen(this.buffer); | |
| this.island = new JellyIsland(points); | |
| this.mouse = new Mouse(this.canvas); | |
| this.tick = JellyDemo.prototype.tick.bind(this); | |
| this.fps = document.getElementById('fps'); | |
| } | |
| JellyDemo.prototype.createBuffer = function(canvas) { | |
| var buffer = document.createElement('canvas'); | |
| buffer.width = canvas.width; | |
| buffer.height = canvas.height; | |
| return buffer; | |
| }; | |
| JellyDemo.prototype.update = function() { | |
| this.island.update(this.mouse); | |
| }; | |
| JellyDemo.prototype.render = function() { | |
| this.screen.clear(); | |
| this.screen.drawIsland(this.island); | |
| this.screen.drawMouse(this.mouse); | |
| this.canvasCtx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
| this.canvasCtx.drawImage(this.buffer, 0, 0, this.buffer.width, this.buffer.height); | |
| }; | |
| JellyDemo.prototype.stop = function() { | |
| this.running = false; | |
| }; | |
| JellyDemo.prototype.start = function() { | |
| this.lastTick = new Date().getTime(); | |
| this.lastPrint = new Date().getTime(); | |
| this.running = true; | |
| this.tick(); | |
| }; | |
| JellyDemo.prototype.tick = function() { | |
| if (!this.running) return; | |
| var current = new Date().getTime(); | |
| var fps = 1000/(current-this.lastTick); | |
| var needed = (SIMULATION_RATE/1000)*(current-this.lastTick); | |
| while (needed-- >= 0) { | |
| this.update(); | |
| } | |
| this.lastTick = current; // new Date().getTime(); | |
| animationFrame(this.tick); | |
| this.render(); | |
| if (current-this.lastPrint > 250) { | |
| fps = Math.floor(fps*100)/100; | |
| this.fps.innerHTML = fps+" fps"; | |
| this.lastPrint = current; | |
| } | |
| }; | |
| function main() { | |
| var demo = new JellyDemo(document.getElementById('screen'), POINTS); | |
| demo.start(); | |
| }; | |
| main(); | |
| }()); |
| html { margin:0; padding:0; border:0; } | |
| body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, | |
| abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, | |
| fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, | |
| article, aside, dialog, figure, footer, header, hgroup, nav, section { | |
| margin: 0; | |
| padding: 0; | |
| border: 0; | |
| font-weight: inherit; | |
| font-style: inherit; | |
| font-size: 100%; | |
| font-family: inherit; | |
| vertical-align: baseline; | |
| } | |
| article, aside, dialog, figure, footer, header, hgroup, nav, section { | |
| display:block; | |
| } | |
| body { | |
| line-height: 1.5; | |
| background: white; | |
| } | |
| #screen { | |
| position: relative; | |
| } | |
| table { | |
| border-collapse: separate; | |
| border-spacing: 0; | |
| } | |
| caption, th, td { | |
| text-align: left; | |
| font-weight: normal; | |
| float: none !important; | |
| } | |
| table, th, td { | |
| vertical-align: middle; | |
| } | |
| a img { border: none; } | |
| html { height: 100%; } | |
| .hidden { | |
| display: none !important; | |
| visibility: hidden !important; | |
| } | |
| .clearfix:before, .clearfix:after { | |
| content: " "; | |
| display: table; | |
| } | |
| .clearfix:after { | |
| clear: both; | |
| } | |
| body { | |
| background-color: #333; | |
| color: #ccc; | |
| padding: 30px; | |
| font-family: "Helvetica Neue", "Verdana", sans-serif; | |
| } | |
| a { | |
| text-decoration: none; | |
| color: #999; | |
| } | |
| #content { | |
| float: left; | |
| position: relative; | |
| left: 50%; | |
| } | |
| #wrap { | |
| float: left; | |
| display: inline; | |
| margin: auto; | |
| margin-left: -50%; | |
| } | |
| #main { | |
| text-align: center; | |
| padding: 24px; | |
| } | |
| .blob { | |
| border-radius:8px; | |
| float: left; | |
| display: inline; | |
| margin: 10px; | |
| padding: 15px; | |
| background-color: rgba(255,255,255,0.3); | |
| -webkit-transition: all 0.2s ease-in-out; | |
| -moz-transition: all 0.2s ease-in-out; | |
| -o-transition: all 0.2s ease-in-out; | |
| -ms-transition: all 0.2s ease-in-out; | |
| transition: all 0.2s ease-in-out; | |
| } | |
| #fps { | |
| text-align: center; | |
| margin: 10px; | |
| width: 150px; | |
| background-color: rgba(0, 0, 0, 0); | |
| } | |
| .option { | |
| text-align: center; | |
| cursor: pointer; | |
| width: 150px; | |
| background-color: rgba(0, 0, 0, 0.3); | |
| } | |
| .option:hover { | |
| background-color: rgba(0, 0, 0, 0.2); | |
| } | |
| .option[data-active="1"] { | |
| background-color: rgba(255, 255, 255, 0.3); | |
| } | |
| .option[data-active="1"]:hover { | |
| background-color: rgba(255, 255, 255, 0.5); | |
| } | |
| .option > input[type=checkbox] { | |
| display: none; | |
| } | |
| #sidebar { | |
| width: 200px; | |
| } | |
| a.blob { | |
| width: 150px; | |
| background-color: rgba(52, 93, 115, 0.8); | |
| color: rgb(30, 34, 62); | |
| text-align: center; | |
| } | |
| a.blob:hover { | |
| background-color: rgba(54, 141, 148, 0.8); | |
| } | |
Ha, you're totally right, that is my code. I don't think they included the copyright notice though... 🤷♂️
Nice work! Looks like it’s used here? https://creative.starbucks.com/
Just looked through their crumbled up code and found similar variables :)