crazy.circles
Version:
Make your loading... window entertaining with Crazy Circles illusion effect
473 lines (378 loc) • 15.8 kB
JavaScript
/**
*
* @source: http://www.kopf.com.br/crazy.circles.js
*
* @licstart The following is the entire license notice for the
* JavaScript code in this page.
*
* Copyright (C) 2015 Bruno Marotta
*
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice
* for the JavaScript code in this page.
*
*/
// Date now prototype
if (!Date.now) {
Date.now = function () { return new Date().getTime(); };
}
// animation prototype
var requestAnimFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
setTimeout(callback, 16);
};
// Class declaration
var CrazyCircles = function (holderId, options) {
this.holderId = holderId;
this.options = options;
this.paper = null;
this.defaultOptions = {
// These are the defaults.
color: "multi",
backgroundColor: "white",
shadow: true,
size: 60,
totalCircles: 8,
cycleDuration: 2000,
circleSize: 0, // Calculate automatically
goCrazyCycle: 0, // Never go crazy
fadeIn: false,
image: "",
imageWidth: 0,
imageHeight: 0,
path: "",
goCrazyOnClick: false
};
this.initialize();
}
CrazyCircles.prototype.resetOptions = function(options) {
this.options = options;
this.initialize();
}
CrazyCircles.prototype.initialize = function() {
this.cleanOptions();
this.clear();
this.paper = Raphael(this.holderId, this.options.size, this.options.size);
try {
this.paper.canvas.style.backgroundColor = this.options.backgroundColor;
}
catch (ex) {
// just ignore it
}
Raphael.getColor.reset();
this.radius = this.options.size / 2 - this.options.circleSize - 1;
this.circleInfos = new Object();
this.start = null;
for (var i = 0; i < this.options.totalCircles; i++) {
this.circleInfos[i] = this.initializeCircle(i);
}
this.iAmCrazy = false;
// Save this to local variable to be accessible on the animationStep function
var current = this;
//var xxx = 0;
this.lastFrame = 0;
this.actualProgress = 0;
function animationStep(timestamp) {
if (timestamp == null)
timestamp = Date.now();
//timestamp = xxx;
//xxx = xxx + 60;
if (!current.start) {
current.start = timestamp;
current.lastFrame = timestamp;
}
this.actualProgress = (timestamp - current.start);
var delta = (timestamp - current.lastFrame);
current.lastFrame = timestamp;
for (var i = 0; i < current.options.totalCircles; i++) {
current.animateCircle(current.circleInfos[i], this.actualProgress, delta);
}
var currentCycle = this.actualProgress / current.options.cycleDuration;
// Check if we have to restart
if (current.options.restartAfterCycle > 0 &&
(currentCycle >= current.options.restartAfterCycle)) {
current.initialize();
return;
}
// Check if ball 0 is still stuck and we can go crazy
if (!current.circleInfos[0].free && current.options.goCrazyCycle > 0 &&
(currentCycle >= current.options.goCrazyCycle)) {
current.setFree(current.circleInfos[0], this.actualProgress);
}
// If balls are free. Check the collisions
if (current.iAmCrazy)
for (var i = 0; i < current.options.totalCircles; i++)
for (var j = i + 1; j < current.options.totalCircles; j++) {
current.calculateCollisions(current.circleInfos[i], current.circleInfos[j], this.actualProgress, delta);
}
requestAnimFrame(animationStep);
};
requestAnimFrame(animationStep);
};
CrazyCircles.prototype.clear = function() {
// Clear paper if already existing
if (this.paper != null) {
var paperDom = this.paper.canvas;
paperDom.parentNode.removeChild(paperDom);
}
}
CrazyCircles.prototype.cleanOptions = function() {
if (this.options == null)
this.options = this.defaultOptions;
if (this.options.size == null || this.options.size == 0) {
this.options.size = Math.min(document.getElementById(this.holderId).clientWidth,
document.getElementById(this.holderId).clientHeight);
}
if (this.options.size < 20 || isNaN(this.options.size))
this.options.size = 20;
if (this.options.totalCircles == null || this.options.totalCircles < 1)
this.options.totalCircles = this.defaultOptions.totalCircles;
if (this.options.cycleDuration == null)
this.options.cycleDuration = this.defaultOptions.cycleDuration;
if (this.options.cycleDuration < 500)
this.options.cycleDuration = 500;
this.hasImage = false;
this.isPath = false;
this.offSetX = 0;
this.offSetY = 0;
if (this.options.image != "" && this.options.image != undefined && this.options.imageWidth > 0 && this.options.imageHeight > 0) {
// Calculate circle size
this.hasImage = true;
this.offSetX = -(this.options.imageWidth / 2);
this.offSetY = -(this.options.imageHeight / 2);
this.options.circleSize = Math.max(this.options.imageWidth, this.options.imageHeight) / 2 + 1;
}
else if (this.options.path != "" && this.options.path != undefined) {
this.isPath = true;
var box = Raphael.pathBBox(this.options.path);
this.offSetX = -(box.width / 2) - box.x;
this.offSetY = -(box.height / 2) - box.y;
this.options.circleSize = Math.max(box.width, box.height) / 2 + 1;
}
if (this.options.circleSize == null || this.options.circleSize == 0 || this.options.circleSize == "auto") {
this.options.circleSize = this.options.size / 20;
}
if (this.options.circleSize < 1)
this.options.circleSize = 1;
if (this.options.goCrazyCycle != null && this.options.goCrazyCycle > 0) {
if (this.options.fadeIn && this.options.goCrazyCycle <= this.options.totalCircles) {
this.options.goCrazyCycle = Number(this.options.totalCircles) + Number(this.options.goCrazyCycle);
}
}
if (this.options.restartAfterCycle != null && this.options.restartAfterCycle > 0) {
if (this.options.fadeIn && this.options.restartAfterCycle <= this.options.totalCircles) {
this.options.restartAfterCycle = Number(this.options.totalCircles) + Number(this.options.restartAfterCycle);
}
}
};
CrazyCircles.prototype.initializeCircle = function(circleNum) {
var color = this.options.color == "multi" ? Raphael.getColor() : this.options.color;
if (color == undefined)
color = "#a0a0a0";
var circleInfo = {
num: circleNum,
angle: (180 / this.options.totalCircles * circleNum) / 180 * Math.PI,
pathOffSet: (50 / this.options.totalCircles * circleNum),
free: false
};
if (this.options.shadow && !this.hasImage) {
circleInfo.circleShadow = this.paper.circle(0, 0, this.options.circleSize).attr("fill", color).attr("stroke", color).attr("opacity", .3);
}
circleInfo.circle =
this.isPath ? this.paper.path(this.options.path).attr("fill", color).attr("stroke", color) :
this.hasImage ? this.paper.image(this.options.image, 0, 0, this.options.imageWidth, this.options.imageHeight) :
this.paper.circle(0, 0, this.options.circleSize).attr("fill", color).attr("stroke", color);
var current = this;
if (this.options.goCrazyOnClick) {
circleInfo.circle.mousedown(
function() {
current.setFree(circleInfo, this.actualProgress);
});
}
this.setCirclePosition(circleInfo, 0);
return circleInfo;
}
CrazyCircles.prototype.xattr = function() {
return this.hasImage ? "x" : "cx";
}
CrazyCircles.prototype.yattr = function() {
return this.hasImage ? "y" : "cy";
}
CrazyCircles.prototype.animateCircle = function(circleInfo, progressMs, deltaMs) {
if (circleInfo.free) {
if (circleInfo.circleShadow != null) {
this.setXY(
circleInfo.circleShadow,
circleInfo.circle.realX,
circleInfo.circle.realY);
}
var newPos = {
cx: Math.round(circleInfo.circle.realX + circleInfo.vx * deltaMs, 4),
cy: Math.round(circleInfo.circle.realY + circleInfo.vy * deltaMs, 4)
};
this.checkBoundaries(newPos, circleInfo);
this.setXY(
circleInfo.circle,
Math.round(newPos.cx, 4),
Math.round(newPos.cy, 4));
} else {
this.setCirclePosition(circleInfo, progressMs, deltaMs);
}
}
CrazyCircles.prototype.setXY = function(circle, x, y) {
circle.realX = x;
circle.realY = y;
var x = Math.round(x);
var y = Math.round(y);
if (this.isPath) {
circle.transform("T" + x + "," + y);
} else {
circle.attr(this.xattr(), x);
circle.attr(this.yattr(), y);
}
}
CrazyCircles.prototype.setCirclePosition = function(circleInfo, progressMs, deltaMs) {
var currentCycle = progressMs / this.options.cycleDuration;
if (circleInfo.circleShadow != null) {
var pathPosShadow = this.adjustPathPos(progressMs - 40, circleInfo.pathOffSet);
this.setXY(
circleInfo.circleShadow,
(this.options.size / 2) + this.radius * pathPosShadow * Math.cos(circleInfo.angle),
(this.options.size / 2) + this.radius * pathPosShadow * Math.sin(circleInfo.angle));
if (this.options.fadeIn) {
if (currentCycle < circleInfo.num) {
circleInfo.circleShadow.attr("opacity", 0);
} else if (currentCycle < circleInfo.num + 1) {
circleInfo.circleShadow.attr("opacity", 0.3 * (progressMs % this.options.cycleDuration) / this.options.cycleDuration);
} else {
circleInfo.circleShadow.attr("opacity", 0.3);
}
}
}
var pathPosAdjusted = this.adjustPathPos(progressMs, circleInfo.pathOffSet);
var oldPos = { x: circleInfo.circle.realX, y: circleInfo.circle.realY };
this.setXY(circleInfo.circle,
(this.options.size / 2) + this.radius * pathPosAdjusted * Math.cos(circleInfo.angle) + this.offSetX,
(this.options.size / 2) + this.radius * pathPosAdjusted * Math.sin(circleInfo.angle) + this.offSetY);
if (this.options.fadeIn) {
if (currentCycle < circleInfo.num) {
circleInfo.circle.attr("opacity", 0);
} else if (currentCycle < circleInfo.num + 1) {
circleInfo.circle.attr("opacity", (progressMs % this.options.cycleDuration) / this.options.cycleDuration);
} else {
circleInfo.circle.attr("opacity", 1);
}
}
// Calculate the current velocity for circles for the collision when we set them free
if (circleInfo.num > 0) {
circleInfo.vx = (circleInfo.circle.realX - oldPos.x) / deltaMs;
circleInfo.vy = (circleInfo.circle.realY - oldPos.y) / deltaMs;
}
}
CrazyCircles.prototype.adjustPathPos = function(progressMs, pathOffSet) {
var pathPos = progressMs / this.options.cycleDuration * 100 + pathOffSet;
pathPos = (pathPos % 100) / 50;
if (pathPos > 1)
pathPos = 2 - pathPos;
pathPos = pathPos * 2 - 1;
var negative = pathPos < 0;
// ease out sine
pathPos = Math.sin(Math.abs(pathPos) * (Math.PI/2));
if (negative)
pathPos = -pathPos;
return pathPos;
}
CrazyCircles.prototype.setFree = function(circleInfo, progressMs) {
this.iAmCrazy = true;
circleInfo.free = true;
if (circleInfo.vx == null || isNaN(circleInfo.vx) || isNaN(circleInfo.vy) || Math.abs(circleInfo.vx) < 0.03 || Math.abs(circleInfo.vy) < 0.03)
{
circleInfo.vx = Math.cos(circleInfo.angle) * (this.radius * 4 / this.options.cycleDuration);
circleInfo.vy = Math.sin(circleInfo.angle) * (this.radius * 4 / this.options.cycleDuration);
var pathPos = progressMs / this.options.cycleDuration * 100 + circleInfo.pathOffSet;
pathPos = (pathPos % 100) / 50;
if (pathPos > 1) {
// we are coming back
circleInfo.vx = circleInfo.vx * -1;
circleInfo.vy = circleInfo.vy * -1;
}
}
}
CrazyCircles.prototype.checkBoundaries = function(newPos, circleInfo) {
if (newPos.cx < this.options.circleSize + this.offSetX || isNaN(newPos.cx)) {
newPos.cx = this.options.circleSize + this.offSetX;
circleInfo.vx = circleInfo.vx * -1;
} else {
if (newPos.cx > this.options.size - this.options.circleSize + this.offSetX) {
newPos.cx = this.options.size - this.options.circleSize + this.offSetX;
circleInfo.vx = circleInfo.vx * -1;
}
}
if (newPos.cy < this.options.circleSize + this.offSetY || isNaN(newPos.cy)) {
newPos.cy = this.options.circleSize + this.offSetY;
circleInfo.vy = circleInfo.vy * -1;
} else {
if (newPos.cy > this.options.size - this.options.circleSize + this.offSetY) {
newPos.cy = this.options.size - this.options.circleSize + this.offSetY;
circleInfo.vy = circleInfo.vy * -1;
}
}
}
CrazyCircles.prototype.calculateCollisions = function(circleInfo1, circleInfo2, progress, delta) {
var firstBall = { x: circleInfo1.circle.realX, y: circleInfo1.circle.realY };
var secondBall = { x: circleInfo2.circle.realX, y: circleInfo2.circle.realY };
var distance = Math.sqrt(
((firstBall.x - secondBall.x) * (firstBall.x - secondBall.x))
+ ((firstBall.y - secondBall.y) * (firstBall.y - secondBall.y))
);
if (distance < this.options.circleSize * 2)
{
//balls have collided
if (!circleInfo1.free) {
this.setFree(circleInfo1, progress);
}
if (!circleInfo2.free) {
this.setFree(circleInfo2, progress);
}
var newVx = circleInfo2.vx;
var newVy = circleInfo2.vy;
circleInfo2.vx = circleInfo1.vx;
circleInfo2.vy = circleInfo1.vy;
circleInfo1.vx = newVx;
circleInfo1.vy = newVy;
// Move balls apart
this.animateCircle(circleInfo1, 0, delta);
var tentatives = 0;
// Move the second time as many times as needed for having them apart (avoid rounding error)
// maximum three (avoid infinte loop)
while (distance < this.options.circleSize * 2 && tentatives++ < 3) {
this.animateCircle(circleInfo2, 0, delta);
// Check again until they are really apart
firstBall = { x: circleInfo1.circle.realX, y: circleInfo1.circle.realY };
secondBall = { x: circleInfo2.circle.realX, y: circleInfo2.circle.realY };
distance = Math.sqrt(
((firstBall.x - secondBall.x) * (firstBall.x - secondBall.x))
+ ((firstBall.y - secondBall.y) * (firstBall.y - secondBall.y))
);
}
}
}