<div id="container">
<canvas></canvas>
</div>
html, body { height: 100%; }
body {
margin: 0;
display: grid;
justify-items: center;
align-items: center;
}
#container {
width: 100vmin;
height: 100vmin;
}
/////////////////////////
// Mostly helper stuff //
/////////////////////////
const container = document.querySelector("#container");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
// Set in our resize function
let width, height, minX, maxX, minY, maxY, cellDiff, halfGridItemThickness, numColors, gridItems, parallelograms;
// Init some settings to use
const settings = {
gridRows: 9,
gridDotsPerCol: 5,
clearColor: "#e0e0e0",
gridItemColor: "#9e9e9e",
gridItemThickness: 1,
numParallelograms: 150,
parallelogramColors: ["#fc705b", "#212f42", "#3f5c86"],
shadowColor: "#443f3c",
};
// Alternative theme from https://twitter.com/MAKIO135/status/1404488303102005250
// settings.clearColor = "#dfede0";
// settings.parallelogramColors = ["#dfede0", "#201f1e", "#f5ce4e"];
// Alternative theme from https://twitter.com/MAKIO135/status/1404397957810688013
// settings.clearColor = "#7be4e1";
// settings.parallelogramColors = ["#44c0d5", "#162c9b", "#fefac1"];
// settings.shadowColor = "#0f2034";
numColors = settings.parallelogramColors.length;
halfGridItemThickness = settings.gridItemThickness / 2;
// Some helper functions
const wrap01 = gsap.utils.wrap(0, 1);
const progressEase = CustomEase.create("custom", "M0,0,C0,0,0.3,0.5,0.5,0.5,0.7,0.5,1,1,1,1");
const normalizeUpperHalf = gsap.utils.normalize(0.5, 1);
// An animation that drives the progress of all other animations
const overallProgress = { val: 0 };
const progressAnim = gsap.to(overallProgress, {
val: 1,
repeat: -1,
ease: "none",
duration: 2
});
// An animation that rotates the pluses in the background grid
const rot = { val: 0 };
const rotAnim = gsap.to(rot, {
val: Math.PI / 2,
duration: 1,
repeat: -1,
repeatDelay: 3,
ease: "none"
});
// Creates our grid and parallelograms with the proper sizing
const resize = () => {
gridItems = [];
parallelograms = [];
width = height = canvas.width = canvas.height = container.offsetWidth;
cellDiff = width / (settings.gridRows * (settings.gridDotsPerCol - 1));
minX = minY = width * 0.1;
maxX = maxY = width * 0.9;
createGridItems();
createParallelograms();
};
// Throttle the resize event
let resizeTimeout;
const handleResize = () => {
if(resizeTimeout) resizeTimeout.kill();
resizeTimeout = gsap.delayedCall(0.3, resize);
};
////////////////////////////
// The "meat" of the code //
////////////////////////////
// Create the grid background
const createGridItems = () => {
for(let row = 0; row < settings.gridRows * (settings.gridDotsPerCol - 1); row++) {
for(let col = 0; col < settings.gridRows * (settings.gridDotsPerCol - 1); col++) {
// const index = col * settings.gridRows + row;
const gridItem = {
x: col * cellDiff + cellDiff / 2,
y: row * cellDiff + cellDiff / 2,
type: col % settings.gridDotsPerCol === 0 && row % settings.gridDotsPerCol === 0 ? "plus" : "dot"
};
gridItems.push(gridItem);
}
}
};
const getRestOfValues = (parallelogram) => {
parallelogram.distanceX = parallelogram.horizDir * parallelogram.length;
parallelogram.distanceY = parallelogram.vertDir * parallelogram.length;
parallelogram.endX = parallelogram.startX + parallelogram.distanceX;
parallelogram.endY = parallelogram.startY + parallelogram.distanceY;
parallelogram.mirrorStartX = width - parallelogram.startX - parallelogram.thickness;
parallelogram.mirrorEndX = parallelogram.mirrorStartX - parallelogram.distanceX;
};
// Create the foreground parallelograms
const createParallelograms = () => {
for(let i = 0; i < settings.numParallelograms; i++) {
// Randomly generate some parameters to use
const parallelogram = {
color: settings.parallelogramColors[i % numColors],
startX: gsap.utils.random(0, width, 1),
startY: gsap.utils.random(0, height, 1),
horizDir: Math.random() < 0.5 ? 1 : -1, // Determines the angle of the p-gram
vertDir: Math.random() < 0.5 ? 1 : -1, // Determines the way the p-gram "grows"
thickness: gsap.utils.random(10, 50, 1),
length: gsap.utils.random(width / 10, width / 2),
progress: Math.random(),
shadowOffset: gsap.utils.random(2, 10, 1)
};
getRestOfValues(parallelogram);
// Cheap attempt to improve the choices of the horizontal and vertical directions
let hasChanged = false;
if(parallelogram.endX > maxX || parallelogram.endX < minX) {
parallelogram.horizDir *= -1;
hasChanged = true;
}
if(parallelogram.endY > maxY || parallelogram.endY < minY) {
parallelogram.vertDir *= -1;
hasChanged = true;
}
// Recalculate if necessary
if(hasChanged) getRestOfValues(parallelogram);
parallelograms.push(parallelogram);
}
};
// Draws the given grid item based on its info
const drawGridItem = (item) => {
if(item.type === "dot") {
ctx.fillRect(item.x - halfGridItemThickness, item.y - halfGridItemThickness, settings.gridItemThickness, settings.gridItemThickness);
} else if(item.type === "plus") {
// Rotate and draw if need be - if nothing to rotate, avoid to save on work
if(rot.val) {
ctx.save();
ctx.translate(item.x, item.y);
ctx.rotate(rot.val);
ctx.fillRect(-settings.gridItemThickness * 3, -halfGridItemThickness, settings.gridItemThickness * 6, settings.gridItemThickness);
ctx.fillRect(-halfGridItemThickness, -settings.gridItemThickness * 3, settings.gridItemThickness, settings.gridItemThickness * 6);
ctx.restore();
}
// Just draw the plus
else {
ctx.fillRect(item.x - settings.gridItemThickness * 2, item.y - halfGridItemThickness, settings.gridItemThickness * 4, settings.gridItemThickness);
ctx.fillRect(item.x - halfGridItemThickness, item.y - settings.gridItemThickness * 2, settings.gridItemThickness, settings.gridItemThickness * 4);
}
}
};
// Draws the given parallelogram based on its info
const drawParallelogram = (parallelogram) => {
// Get the progress values between 0 and 1
let currProgress = progressEase(wrap01(overallProgress.val + parallelogram.progress));
let startX, startY, endX, endY, mirrorStartX, mirrorEndX;
// Handle the "first half" of the animation ("growing")
if(currProgress < 0.5) {
currProgress *= 2;
startX = parallelogram.startX;
startY = parallelogram.startY;
endX = parallelogram.startX + parallelogram.distanceX * currProgress;
endY = parallelogram.startY + parallelogram.distanceY * currProgress;
mirrorStartX = parallelogram.mirrorStartX;
mirrorEndX = parallelogram.mirrorStartX - parallelogram.distanceX * currProgress;
}
// Handle the "second half" of the animation ("shrinking")
else {
currProgress = 1 - normalizeUpperHalf(currProgress);
startX = parallelogram.endX;
startY = parallelogram.endY;
endX = startX - parallelogram.distanceX * currProgress;
endY = startY - parallelogram.distanceY * currProgress;
mirrorStartX = parallelogram.mirrorEndX;
mirrorEndX = mirrorStartX + parallelogram.distanceX * currProgress;
}
// Prevent duplicate calculations
const shadowStartY = startY + parallelogram.shadowOffset;
const shadowEndY = endY + parallelogram.shadowOffset;
const startXPlusThickness = startX + parallelogram.thickness;
const endXPlusThickness = endX + parallelogram.thickness;
const mirrorStartXPlusThickness = mirrorStartX + parallelogram.thickness;
const mirrorEndXPlusThickness = mirrorEndX + parallelogram.thickness;
// Draw the shadows first
ctx.fillStyle = settings.shadowColor;
// First shadow
ctx.beginPath();
ctx.moveTo(startX, shadowStartY);
ctx.lineTo(startXPlusThickness, shadowStartY);
ctx.lineTo(endXPlusThickness, shadowEndY);
ctx.lineTo(endX, shadowEndY);
ctx.closePath();
ctx.fill();
// Second shadow
ctx.beginPath();
ctx.moveTo(mirrorStartX, shadowStartY);
ctx.lineTo(mirrorStartXPlusThickness, shadowStartY);
ctx.lineTo(mirrorEndXPlusThickness, shadowEndY);
ctx.lineTo(mirrorEndX, shadowEndY);
ctx.closePath();
ctx.fill();
// Then draw the coloreds
ctx.fillStyle = parallelogram.color;
// First colored
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startXPlusThickness, startY);
ctx.lineTo(endXPlusThickness, endY);
ctx.lineTo(endX, endY);
ctx.closePath();
ctx.fill();
// Second colored
ctx.beginPath();
ctx.moveTo(mirrorStartX, startY);
ctx.lineTo(mirrorStartXPlusThickness, startY);
ctx.lineTo(mirrorEndXPlusThickness, endY);
ctx.lineTo(mirrorEndX, endY);
ctx.closePath();
ctx.fill();
};
// Handle all of our drawing
const draw = () => {
// Clear
ctx.fillStyle = settings.clearColor;
ctx.fillRect(0, 0, width, height);
// Draw background grid
ctx.fillStyle = settings.gridItemColor; // They're all the same color so we set it here
gridItems.forEach(drawGridItem);
// Draw the parallelograms
parallelograms.forEach(drawParallelogram);
};
/////////////////////
// Kick things off //
/////////////////////
const init = () => {
document.body.style.backgroundColor = settings.clearColor;
resize(); // Also creates our items
// Listen for future resizes
window.addEventListener("resize", handleResize);
update(); // Start the drawing process
};
// Our update loop
const update = () => {
draw();
requestAnimationFrame(update);
};
init();
This Pen doesn't use any external CSS resources.