Long Shadow
Live previewPerformance varies by device
No account needed to copy or download. Check dependencies and thelicense status before publishing.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Long Shadow</title>
<style>
html {
height: 100%;
}
body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
cursor: none;
}
#canvas {
background-color: #2c343f;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var prefersReduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
function resize() {
var box = c.getBoundingClientRect();
c.width = box.width;
c.height = box.height;
}
var light = {
x: 160,
y: 200,
};
var colors = ["#f5c156", "#e6616b", "#5cd3ad"];
function drawLight() {
ctx.beginPath();
ctx.arc(light.x, light.y, 1000, 0, 2 * Math.PI);
var gradient = ctx.createRadialGradient(
light.x,
light.y,
0,
light.x,
light.y,
1000
);
gradient.addColorStop(0, "#3b4654");
gradient.addColorStop(1, "#2c343f");
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
ctx.arc(light.x, light.y, 20, 0, 2 * Math.PI);
gradient = ctx.createRadialGradient(
light.x,
light.y,
0,
light.x,
light.y,
5
);
gradient.addColorStop(0, "#fff");
gradient.addColorStop(1, "#3b4654");
ctx.fillStyle = gradient;
ctx.fill();
}
function Box() {
this.half_size = Math.floor(Math.random() * 50 + 1);
this.x = Math.floor(Math.random() * c.width + 1);
this.y = Math.floor(Math.random() * c.height + 1);
this.r = Math.random() * Math.PI;
this.shadow_length = 2000;
this.color = colors[Math.floor(Math.random() * colors.length)];
this.getDots = function () {
var full = (Math.PI * 2) / 4;
var p1 = {
x: this.x + this.half_size * Math.sin(this.r),
y: this.y + this.half_size * Math.cos(this.r),
};
var p2 = {
x: this.x + this.half_size * Math.sin(this.r + full),
y: this.y + this.half_size * Math.cos(this.r + full),
};
var p3 = {
x: this.x + this.half_size * Math.sin(this.r + full * 2),
y: this.y + this.half_size * Math.cos(this.r + full * 2),
};
var p4 = {
x: this.x + this.half_size * Math.sin(this.r + full * 3),
y: this.y + this.half_size * Math.cos(this.r + full * 3),
};
return { p1: p1, p2: p2, p3: p3, p4: p4 };
};
this.rotate = function () {
var speed = (60 - this.half_size) / 20;
this.r += speed * 0.002;
this.x += speed;
this.y += speed;
};
this.draw = function () {
var dots = this.getDots();
ctx.beginPath();
ctx.moveTo(dots.p1.x, dots.p1.y);
ctx.lineTo(dots.p2.x, dots.p2.y);
ctx.lineTo(dots.p3.x, dots.p3.y);
ctx.lineTo(dots.p4.x, dots.p4.y);
ctx.fillStyle = this.color;
ctx.fill();
if (this.y - this.half_size > c.height) {
this.y -= c.height + 100;
}
if (this.x - this.half_size > c.width) {
this.x -= c.width + 100;
}
};
this.drawShadow = function () {
var dots = this.getDots();
var angles = [];
var points = [];
for (var dot in dots) {
var angle = Math.atan2(
light.y - dots[dot].y,
light.x - dots[dot].x
);
var endX =
dots[dot].x + this.shadow_length * Math.sin(-angle - Math.PI / 2);
var endY =
dots[dot].y + this.shadow_length * Math.cos(-angle - Math.PI / 2);
angles.push(angle);
points.push({
endX: endX,
endY: endY,
startX: dots[dot].x,
startY: dots[dot].y,
});
}
for (var i = points.length - 1; i >= 0; i--) {
var n = i == 3 ? 0 : i + 1;
ctx.beginPath();
ctx.moveTo(points[i].startX, points[i].startY);
ctx.lineTo(points[n].startX, points[n].startY);
ctx.lineTo(points[n].endX, points[n].endY);
ctx.lineTo(points[i].endX, points[i].endY);
ctx.fillStyle = "#2c343f";
ctx.fill();
}
};
}
var boxes = [];
function draw() {
ctx.clearRect(0, 0, c.width, c.height);
drawLight();
for (var i = 0; i < boxes.length; i++) {
boxes[i].rotate();
boxes[i].drawShadow();
}
for (var i = 0; i < boxes.length; i++) {
collisionDetection(i);
boxes[i].draw();
}
if (!prefersReduced) requestAnimationFrame(draw);
}
resize();
draw();
while (boxes.length < 14) {
boxes.push(new Box());
}
if (prefersReduced) draw();
window.onresize = resize;
c.onmousemove = function (e) {
light.x = e.offsetX === undefined ? e.layerX : e.offsetX;
light.y = e.offsetY === undefined ? e.layerY : e.offsetY;
};
function collisionDetection(b) {
for (var i = boxes.length - 1; i >= 0; i--) {
if (i != b) {
var dx =
boxes[b].x + boxes[b].half_size -
(boxes[i].x + boxes[i].half_size);
var dy =
boxes[b].y + boxes[b].half_size -
(boxes[i].y + boxes[i].half_size);
var d = Math.sqrt(dx * dx + dy * dy);
if (d < boxes[b].half_size + boxes[i].half_size) {
boxes[b].half_size = boxes[b].half_size > 1 ? boxes[b].half_size - 1 : 1;
boxes[i].half_size = boxes[i].half_size > 1 ? boxes[i].half_size - 1 : 1;
}
}
}
}
</script>
</body>
</html>About this animation
Dynamic long shadow effect generator for icons.
Under the hood
Canvas Light-Source Box Shadows
Fourteen rotating, colored squares drift diagonally across a Canvas 2D scene and wrap at the edges, lit by a radial-gradient glow that follows the mouse via
onmousemove. For each box, the corner angles to the light are computed with Math.atan2() and projected 2000px outward to fill long shadow polygons in the background color. Overlapping boxes shrink by 1px per frame through a simple distance-based collision check.Related