Most of us have watched the movie Avengers : Infinity War where Thanos had killed half of the universe with just one snap. Here, in this tutorial we will be creating the same snap effect using JavaScript and CSS.
Follow this video for guidance :
Source Code
index.html
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="item disintegration-target">
<img style="height:300px;width:500px" src="thanos.png">
</div>
<script src='html2canvas.js'></script>
<script src="script.js"></script>
</body>
</html>
style.css
.disintegration-container {
position: absolute;
pointer-events: none;
}
.disintegration-container > canvas {
position: absolute;
left: 0;
top: 0;
-webkit-transition: opacity 1s ease-out, -webkit-transform 1s ease-out;
transition: opacity 1s ease-out, -webkit-transform 1s ease-out;
transition: transform 1s ease-out, opacity 1s ease-out;
transition: transform 1s ease-out, opacity 1s ease-out, -webkit-transform 1s ease-out;
opacity: 1;
color:black;
-webkit-transform: rotate(0deg) translate(0px, 0px) rotate(0deg);
transform: rotate(0deg) translate(0px, 0px) rotate(0deg);
}
@-webkit-keyframes debug-pulse {
0% {
-webkit-filter: none;
filter: none;
}
95% {
-webkit-filter: none;
filter: none;
}
95% {
-webkit-filter: drop-shadow(0 1px 0 red);
filter: drop-shadow(0 1px 0 red);
}
100% {
-webkit-filter: drop-shadow(0 1px 0 red);
filter: drop-shadow(0 1px 0 red);
}
}
@keyframes debug-pulse {
0% {
-webkit-filter: none;
filter: none;
}
95% {
-webkit-filter: none;
filter: none;
}
95% {
-webkit-filter: drop-shadow(0 1px 0 red);
filter: drop-shadow(0 1px 0 red);
}
100% {
-webkit-filter: drop-shadow(0 1px 0 red);
filter: drop-shadow(0 1px 0 red);
}
}
/* == Normal styling below == */
body {
margin: 0;
min-height: 100vh;
display: -webkit-box;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
-webkit-box-pack: center;
justify-content: center;
-webkit-box-align: center;
align-items: center;
font-family: Roboto, sans-serif;
}
.item {
margin-bottom: 1em;
padding: 1em;
border-radius: 5px;
}
.item h1 {
margin-top: 0;
}
.item p {
margin-bottom: 0;
}
.item img {
float: right;
height:200px;
width:500px;
}
script.js
const DEBUG = false;
const REPETITION_COUNT = 3; // number of times each pixel is assigned to a canvas
const NUM_FRAMES = 90;
/**
* Generates the individual subsets of pixels that are animated to create the effect
* @param {HTMLCanvasElement} ctx
* @param {number} count The higher the frame count, the less grouped the pixels will look - Google use 32, but for our elms we use 128 since we have images near the edges
* @return {HTMLCanvasElement[]} Each canvas contains a subset of the original pixels
*/
function generateFrames($canvas, count = 32) {
const { width, height } = $canvas;
const ctx = $canvas.getContext("2d");
const originalData = ctx.getImageData(0, 0, width, height);
const imageDatas = [...Array(count)].map(
(_, i) => ctx.createImageData(width, height));
// assign the pixels to a canvas
// each pixel is assigned to 2 canvas', based on its x-position
for (let x = 0; x < width; ++x) {
for (let y = 0; y < height; ++y) {
for (let i = 0; i < REPETITION_COUNT; ++i) {
const dataIndex = Math.floor(
count * (Math.random() + 2 * x / width) / 3);
const pixelIndex = (y * width + x) * 4;
// copy the pixel over from the original image
for (let offset = 0; offset < 4; ++offset) {
imageDatas[dataIndex].data[pixelIndex + offset] =
originalData.data[pixelIndex + offset];
}
}
}
}
// turn image datas into canvas'
return imageDatas.map(data => {
const $c = $canvas.cloneNode(true);
$c.getContext("2d").putImageData(data, 0, 0);
return $c;
});
}
/**
* Inserts a new element over an old one, hiding the old one
*/
function replaceElementVisually($old, $new) {
const $parent = $old.offsetParent;
$new.style.top = `${$old.offsetTop}px`;
$new.style.left = `${$old.offsetLeft}px`;
$new.style.width = `${$old.offsetWidth}px`;
$new.style.height = `${$old.offsetHeight}px`;
$parent.appendChild($new);
$old.style.visibility = "hidden";
}
/**
* Disintegrates an element
* @param {HTMLElement} $elm
*/
function disintegrate($elm) {
html2canvas($elm).then($canvas => {
// create the container we'll use to replace the element with
const $container = document.createElement("div");
$container.classList.add("disintegration-container");
// setup the frames for animation
const $frames = generateFrames($canvas, NUM_FRAMES);
$frames.forEach(($frame, i) => {
$frame.style.transitionDelay = `${10.35 * i / $frames.length}s`;
$container.appendChild($frame);
});
// then insert them into the DOM over the element
replaceElementVisually($elm, $container);
// then animate them
$container.offsetLeft; // forces reflow, so CSS we apply below does transition
if (!DEBUG) {
// set the values the frame should animate to
// note that this is done after reflow so the transitions trigger
$frames.forEach($frame => {
const randomRadian = 2 * Math.PI * (Math.random() - 0.5);
$frame.style.transform =
`rotate(${15 * (Math.random() - 0.5)}deg) translate(${60 * Math.cos(randomRadian)}px, ${30 * Math.sin(randomRadian)}px)
rotate(${15 * (Math.random() - 0.5)}deg)`;
$frame.style.opacity = 0;
});
} else {
$frames.forEach($frame => {
$frame.style.animation = `debug-pulse 10s ease ${$frame.style.transitionDelay} infinite alternate`;
});
}
});
}
/** === Below is just to bind the module and the DOM == */
[...document.querySelectorAll(".disintegration-target")].forEach($elm => {
$elm.addEventListener("click", () => {
if ($elm.disintegrated) {return;}
$elm.disintegrated = true;
disintegrate($elm);
});
});
