jquery-image-explode
Version:
A jQuery explosion plugin.
579 lines (504 loc) • 20 kB
JavaScript
(function ($) {
"use strict";
const wrapperName = "explode-wrapper";
if(!$){
console.error("jQuery or Zepto is needed.");
return;
}
$.fn.explodeRestore = function () {
this.each(function () { //restore separately
const $dom = $(this);
const wrapper = $dom.prop(wrapperName);
if (wrapper) {
wrapper.replaceWith($dom);
$dom.prop(wrapperName, null);
}
});
};
$.fn.explode = function (opt) {
if (!opt || typeof opt !== "object") {
opt = {};
}
const {
minWidth = 3,
omitLastLine = false,
radius = 80,
minRadius = 0,
release = true,
fadeTime = 300,
recycle = true,
recycleDelay = 500,
fill = true,
explodeTime = 300,
maxAngle = 360,
gravity = 0,
round = false,
groundDistance = 400,
land=true,
checkOutBound,
finish,
} = opt;
let {
maxWidth
} = opt;
const $target = this;
let $targetImage;
const args = arguments;
if ($target.length > 1) { //explode separately
$target.each(function () {
const $dom = $(this);
$dom.explode.apply($dom, args);
});
return;
} else if (!$target.length) {
return;
}
if(!$.contains(document,$target[0])){
return ;
}
if ($target.prop("tagName") === "IMG") {
if (!$target.prop("complete")) {
$target.on("load", function () {
$target.explode.apply($target, args);
});
return;
}
$targetImage = $target;
} else if ($target.css("backgroundImage") !== "none") {
const src = $target.css("backgroundImage").match(/url\(\"([\S\s]*)\"\)/)[1];
$targetImage = $("<img/>", {
src
});
if (!opt.ignoreCompelete) {
$targetImage.on("load", function () {
opt.ignoreCompelete = true;
$target.explode.apply($target, [opt]);
});
return;
}
}
const w = $target.width();
const h = $target.height();
const minorDimension = Math.min(w, h);
const radiusData = getRadiusData();
const ctxWidth = Math.max(w, radius * 2);
const ctxHeight = Math.max(h, radius * 2, groundDistance * 2);
if (!maxWidth) {
maxWidth = minorDimension / 4;
}
const $wrapper = $("<div></div>", {
"class": wrapperName,
});
const syncStyles = ["width", "height", "margin-top", "margin-right", "margin-bottom", "margin-left", "position", "top", "right", "bottom", "left", "float", "display"];
syncStyles.forEach((v) => {
$wrapper.css(v, $target.css(v));
});
// $wrapper.css("background-color", "black");
if ($wrapper.css("position") === "static") {
$wrapper.css("position", "relative");
}
const startRatio = 0.3;
//generate rags' body
const rags = generateRags();
getRagsFinalState();
const $canvas = $("<canvas></canvas>");
//standard canvas, to draw the ideal target
const $canvas0 = $("<canvas></canvas>");
$canvas0.css({
width: w,
height: h,
});
$canvas0.attr({
width: w,
height: h,
});
$canvas.css({
position: "absolute",
left: (w - ctxWidth) / 2,
right: (w - ctxWidth) / 2,
top: (h - ctxHeight) / 2,
bottom: (h - ctxHeight) / 2,
margin: "auto",
width: ctxWidth,
height: ctxHeight,
});
$canvas.attr({
width: ctxWidth,
height: ctxHeight,
});
$wrapper.append($canvas);
const ctx = $canvas[0].getContext("2d");
const ctx0 = $canvas0[0].getContext("2d");
function warn(key,config) {
console.warn(`Unsupported ${key} style:${config[key]}`);
}
const {
naturalWidth,
naturalHeight
} = $targetImage?$targetImage[0]:{};
if ($target.prop("tagName") === "IMG") {
ctx0.drawImage($targetImage[0], 0, 0, naturalWidth, naturalHeight, 0, 0, w, h);
} else if ($target.css("backgroundImage") !== "none") {
let dx = 0,
dy = 0,
dWidth = naturalWidth,
dHeight = naturalHeight;
let config = {
"background-repeat": $target.css("background-repeat"),
"background-size": $target.css("background-size"),
"background-position-x": $target.css("background-position-x"),
"background-position-y": $target.css("background-position-y"),
};
const ratioW = w / naturalWidth;
const ratioH = h / naturalHeight;
if (config["background-size"] === "cover") {
const ratio = Math.max(ratioW, ratioH);
dWidth = naturalWidth * ratio;
dHeight = naturalHeight * ratio;
} else if (config["background-size"] === "contain") {
const ratio = Math.min(ratioW, ratioH);
dWidth = naturalWidth * ratio;
dHeight = naturalHeight * ratio;
} else {
warn("background-size",config);
}
dx = parseInt(config["background-position-x"]) / 100 * (w - dWidth);
dy = parseInt(config["background-position-y"]) / 100 * (h - dHeight);
if (config["background-repeat"] === "repeat") {
for (var i = 0 - Math.ceil(dx / dWidth); i < w / dWidth + Math.ceil(-dx / dWidth); i++) {
for (var j = 0 - Math.ceil(dy / dHeight); j < h / dHeight + Math.ceil(-dy / dHeight); j++) {
ctx0.drawImage($targetImage[0], 0, 0, naturalWidth, naturalHeight, dx + i * dWidth, dy + j * dHeight, dWidth, dHeight);
}
}
} else if (config["background-repeat"] === "no-repeat") {
ctx0.drawImage($targetImage[0], 0, 0, naturalWidth, naturalHeight, dx, dy, dWidth, dHeight);
} else {
warn("background-repeat",config);
}
} else if ($target.css("backgroundColor") !== "rgba(0, 0, 0, 0)") {
ctx0.fillStyle = $target.css("backgroundColor");
ctx0.fillRect(0, 0, w, h);
} else {
console.warn("There's nothing to explode.");
}
rags.forEach((rag) => {
const {
left,
top,
width: ragWidth,
height: ragHeight,
} = rag;
rag.naturalParams = [left, top, ragWidth, ragHeight];
});
$target.after($wrapper);
$target.prop(wrapperName, $wrapper);
$target.detach();
let biasVy = 0;
explode(function () {
if (release) {
doRelease();
} else if (recycle) {
doRecycle();
}else{
finish&&finish();
}
});
function doRelease(cb) {
const startTime = Date.now();
let leftCnt = rags.length;
rags.forEach((rag) => {
rag.time1 = 1000 / (rag.ratio * (maxWidth + 1 - rag.width) / maxWidth + 0.1);
rag.time2 = rag.time1 + fadeTime;
});
draw();
function draw() {
const time = Date.now();
const duration = time - startTime;
ctx.clearRect(0, 0, ctxWidth, ctxHeight);
rags.forEach((rag) => {
ctx.save();
const {
width: ragWidth,
height: ragHeight,
} = rag;
ctx.translate(rag.biasx, rag.biasy);
ctx.rotate(rag.lastAngle || rag.finalAngleRad);
if (round) {
ctx.beginPath();
ctx.arc(0, 0, ragWidth / 2, 0, Math.PI * 2, false);
ctx.closePath();
ctx.clip();
}
let alpha;
if (duration < rag.time1) {
alpha = 1;
} else if (duration > rag.time2) {
alpha = 0;
} else {
alpha = 1 - (duration - rag.time1) / fadeTime;
}
if (alpha === 0 && !rag.released) {
rag.released = true;
leftCnt--;
}
ctx.globalAlpha = alpha;
ctx.drawImage($canvas0[0], rag.left, rag.top, rag.width, rag.height, -ragWidth / 2, -ragHeight / 2, ragWidth, ragHeight);
ctx.restore();
});
if (!leftCnt) {
cb && cb();
} else {
window.requestAnimationFrame(draw);
}
}
}
function doRecycle() {
setTimeout(function () {
explode(function () {
$target.explodeRestore();
}, true);
}, recycleDelay);
}
function explode(cb, reverse) {
const startTime = Date.now();
let lastTime = startTime;
let leftCnt = rags.length;
if (!reverse) {
rags.forEach((rag) => {
rag.vx = rag.translateX / explodeTime * 1000;
rag.vy = rag.translateY / explodeTime * 1000;
rag.biasx = rag.translateX0;
rag.biasy = rag.translateY0;
if (gravity) {
rag.transYMax = ctxHeight / 2 + groundDistance - rag.height / 2;
}
});
}
draw();
function draw() {
const time = Date.now();
let ratio;
let angleRatio;
ratio = (time - lastTime) / 1000;
angleRatio = (time - startTime) / explodeTime;
if (reverse) {
angleRatio = 1 - angleRatio;
}
if (gravity) {
biasVy += (gravity * ratio) * 300;
} else {
if (angleRatio > 1 || angleRatio < 0) {
cb && cb();
return;
}
ratio *= Math.cos(angleRatio * Math.PI / 2) * Math.PI / 2;
}
if (reverse) {
ratio = -ratio;
}
lastTime = time;
ctx.clearRect(0, 0, ctxWidth, ctxHeight);
rags.forEach((rag) => {
ctx.save();
const {
width: ragWidth,
height: ragHeight,
} = rag;
if (!rag.land) {
rag.biasx += rag.vx * ratio;
rag.biasy += (rag.vy + biasVy) * ratio;
if (gravity) {
if (checkOutBound && checkOutBound(rag)
|| rag.biasy > rag.transYMax
|| rag.biasy < rag.height/2) {
leftCnt--;
rag.land = true;
rag.lastAngle = rag.finalAngleRad * angleRatio;
if(land){
rag.biasy = gravity>0?rag.transYMax:rag.height/2;
}else{
rag.biasy=rag.transYMax*2;//hide
}
}
}
}
ctx.translate(rag.biasx, rag.biasy);
if (rag.lastAngle) {
ctx.rotate(rag.lastAngle);
} else {
ctx.rotate(rag.finalAngleRad * angleRatio);
}
if (round) {
ctx.beginPath();
ctx.arc(0, 0, ragWidth / 2, 0, Math.PI * 2, false);
ctx.closePath();
ctx.clip();
}
ctx.drawImage($canvas0[0], rag.left, rag.top, rag.width, rag.height, -ragWidth / 2, -ragHeight / 2, ragWidth, ragHeight);
ctx.restore();
});
if (gravity && !leftCnt) {
cb();
} else {
window.requestAnimationFrame(draw);
}
}
}
function random(min, max) {
return parseInt(Math.random() * (max + 1 - min), 10) + min;
}
function shuffle(array) {
let currentIndex = array.length, temporaryValue, randomIndex;
while (currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
//generate final position and angle of rags
function getRagsFinalState() {
rags.forEach((v, i) => {
const finalAngle = (((Math.random() * maxAngle * 2) - maxAngle) / ((Math.random() + 2) * v.width)) * 10;
//coordinate based on center point
let x = v.left + v.width / 2 - w / 2;
let y = v.top + v.width / 2 - h / 2;
if (x === 0) {
x = i % 2 ? -1 : 1;
}
if (y === 0) {
y = (i % 4 < 2) ? -1 : 1;
}
const distance = Math.sqrt(x * x + y * y);
let ratio = ((1 - startRatio) * (1 - (v.width - minWidth) / (maxWidth - minWidth)) + startRatio) * Math.random();
ratio = 1 - (1 - ratio) * (1 - minRadius / radius);
const finalDistance = (radius - distance) * ratio + distance;
const distanceSquare = distance * distance;
const attach = {
finalDistance,
ratio,
x,
y,
distance,
translateX: (finalDistance - distance) * Math.sqrt((distanceSquare - y * y) / (distanceSquare)) * (x > 0 ? 1 : -1),
translateY: (finalDistance - distance) * Math.sqrt((distanceSquare - x * x) / (distanceSquare)) * (y > 0 ? 1 : -1),
translateX0: (ctxWidth - w) / 2 + v.left + v.width / 2,
translateY0: (ctxHeight - h) / 2 + v.top + v.height / 2,
finalAngle,
finalAngleRad: finalAngle * (Math.PI / 180),
};
for (let i in attach) {
v[i] = attach[i];
}
});
}
//generate inital position and dimension of rags
//rewrite it to fit for you demand
function generateRags() {
let rowCnt;
const base = [[0, 1], [1, 1], [1, 0], [0, 0]];
if (omitLastLine) {
rowCnt = Math.floor(h / maxWidth);
} else {
rowCnt = Math.ceil(h / maxWidth);
}
const rags = [];
const noRadius = radiusData.every(function (v) {
return v === 0;
});
for (let row = 0; row < rowCnt; row++) {
generateRow(row);
}
function isInner(x, y) {
if (x < radiusData[0] && y > h - radiusData[0] ||
x > w - radiusData[1] && y > h - radiusData[1] ||
x > w - radiusData[2] && y < radiusData[2] ||
x < radiusData[3] && y < radiusData[3]) {
return false;
}
return true;
}
function distanceLessThan(x1, y1, x2, y2, d) {
return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) < d * d;
}
function tryPushRag({
left,
top,
width,
height
}) {
const x = left + width / 2;
const y = h - top - height / 2;
if (noRadius || isInner(x, y) || radiusData.some(function (v, i) {
return distanceLessThan(x, y, base[i][0] * w + 2 * (0.5 - base[i][0]) * v, base[i][1] * h + 2 * (0.5 - base[i][1]) * v, v);
})) {
rags.push({
left,
top,
width,
height
});
}
}
function generateRow(row) {
let rowSum = 0;
const topBase = row * maxWidth;
function generate(width) {
const left = rowSum;
rowSum += width;
tryPushRag({
left,
top: topBase,
width,
height: width,
});
if (fill) {
for (let i = 1; i < parseInt(maxWidth / width); i++) {
tryPushRag({
left,
top: topBase + i * width,
width,
height: width,
});
}
}
}
let width;
do {
if (width) {
generate(width);
}
width = random(minWidth, maxWidth);
} while (w > rowSum + width);
if (w - rowSum >= minWidth) {
generate(w - rowSum);
}
}
shuffle(rags);
return rags;
}
//get an array of 4 corners of radius
function getRadiusData() {
let ret = ["border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius"];
const width = $target.width();
ret = ret.map(function (key) {
let radius = $target.css(key);
if (radius.match(/px$/)) {
return radius.match(/^\d+/)[0] * 1;
} else if (radius.match(/%$/)) {
return radius.match(/^\d+/)[0] / 100 * width;
}
return radius;
});
ret = ret.map(function (radius) {
if (radius > width / 2) {
radius = width / 2;
}
return radius;
});
return ret;
}
};
})(window.jQuery||window.Zepto);