<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Parallax Skyline</title>
<style>
/*
Parallax Skyline
----------------
This page draws a simple skyline made of multiple layers of
rectangles. Each layer moves at a different speed to produce a
parallax effect. The horizontal speed varies with the user's
mouse position: moving the cursor to the right makes the buildings
drift faster. The design is intentionally minimalist, focusing on
crisp shapes and smooth animation rather than realism.
*/
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: linear-gradient(#1e3c72, #2a5298);
font-family: sans-serif;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="skyline"></canvas>
<script>
(function() {
const canvas = document.getElementById('skyline');
const ctx = canvas.getContext('2d');
let mouseX = 0.5; // normalised mouse position [0,1]
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
buildLayers();
}
window.addEventListener('resize', resize);
resize();
// Capture mouse movement to influence speed.
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX / window.innerWidth;
});
// Define a building layer. Each layer has its own speed and
// building specifications. We populate an array of buildings
// spanning the width of the canvas.
class Layer {
constructor(opts) {
Object.assign(this, opts);
this.buildings = [];
this.populate();
}
populate() {
this.buildings = [];
let x = 0;
while (x < canvas.width + this.maxWidth) {
const width = this.minWidth + Math.random() * (this.maxWidth - this.minWidth);
const height = this.minHeight + Math.random() * (this.maxHeight - this.minHeight);
this.buildings.push({ x, width, height });
x += width + this.gap;
}
}
update(speedScale) {
// Move buildings leftwards. The mouse controls the overall speed.
const delta = this.speed * speedScale;
for (const b of this.buildings) {
b.x -= delta;
}
// Recycle buildings that have moved off the left edge.
while (this.buildings.length && this.buildings[0].x + this.buildings[0].width < 0) {
const b = this.buildings.shift();
// Place it at the end with new dimensions.
const width = this.minWidth + Math.random() * (this.maxWidth - this.minWidth);
const height = this.minHeight + Math.random() * (this.maxHeight - this.minHeight);
const lastX = this.buildings.length ? this.buildings[this.buildings.length - 1].x + this.buildings[this.buildings.length - 1].width + this.gap : 0;
b.x = lastX;
b.width = width;
b.height= height;
this.buildings.push(b);
}
}
draw(ctx) {
ctx.fillStyle = this.colour;
for (const b of this.buildings) {
ctx.fillRect(b.x, canvas.height - b.height, b.width, b.height);
}
}
}
// Create layers with different depths. Closer layers have
// larger buildings and move faster.
let layers = [];
function buildLayers() {
layers = [
new Layer({
speed: 0.5,
minWidth: 40,
maxWidth: 80,
gap: 15,
minHeight: canvas.height * 0.2,
maxHeight: canvas.height * 0.4,
colour: 'rgba(0,0,0,0.6)'
}),
new Layer({
speed: 1.0,
minWidth: 30,
maxWidth: 60,
gap: 10,
minHeight: canvas.height * 0.3,
maxHeight: canvas.height * 0.6,
colour: 'rgba(0,0,0,0.8)'
}),
new Layer({
speed: 2.0,
minWidth: 20,
maxWidth: 40,
gap: 8,
minHeight: canvas.height * 0.4,
maxHeight: canvas.height * 0.8,
colour: 'rgba(0,0,0,1)'
})
];
}
// Animation loop
function render() {
// Draw sky gradient: dark at top, lighter near horizon.
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#152850');
grad.addColorStop(1, '#113366');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw stars. We use a simple seeded random distribution each frame.
ctx.fillStyle = 'rgba(255,255,255,0.5)';
for (let i = 0; i < 80; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height * 0.6;
ctx.fillRect(x, y, 2, 2);
}
// Update and draw each layer. Speed scale increases with mouseX.
const scale = 0.5 + mouseX * 1.5;
for (const layer of layers) {
layer.update(scale);
layer.draw(ctx);
}
requestAnimationFrame(render);
}
render();
})();
</script>
</body>
</html>
<canvas id="skyline"></canvas>
/*
Parallax Skyline
----------------
This page draws a simple skyline made of multiple layers of
rectangles. Each layer moves at a different speed to produce a
parallax effect. The horizontal speed varies with the user's
mouse position: moving the cursor to the right makes the buildings
drift faster. The design is intentionally minimalist, focusing on
crisp shapes and smooth animation rather than realism.
*/
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: linear-gradient(#1e3c72, #2a5298);
font-family: sans-serif;
}
canvas {
display: block;
}
(function() {
const canvas = document.getElementById('skyline');
const ctx = canvas.getContext('2d');
let mouseX = 0.5; // normalised mouse position [0,1]
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
buildLayers();
}
window.addEventListener('resize', resize);
resize();
// Capture mouse movement to influence speed.
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX / window.innerWidth;
});
// Define a building layer. Each layer has its own speed and
// building specifications. We populate an array of buildings
// spanning the width of the canvas.
class Layer {
constructor(opts) {
Object.assign(this, opts);
this.buildings = [];
this.populate();
}
populate() {
this.buildings = [];
let x = 0;
while (x < canvas.width + this.maxWidth) {
const width = this.minWidth + Math.random() * (this.maxWidth - this.minWidth);
const height = this.minHeight + Math.random() * (this.maxHeight - this.minHeight);
this.buildings.push({ x, width, height });
x += width + this.gap;
}
}
update(speedScale) {
// Move buildings leftwards. The mouse controls the overall speed.
const delta = this.speed * speedScale;
for (const b of this.buildings) {
b.x -= delta;
}
// Recycle buildings that have moved off the left edge.
while (this.buildings.length && this.buildings[0].x + this.buildings[0].width < 0) {
const b = this.buildings.shift();
// Place it at the end with new dimensions.
const width = this.minWidth + Math.random() * (this.maxWidth - this.minWidth);
const height = this.minHeight + Math.random() * (this.maxHeight - this.minHeight);
const lastX = this.buildings.length ? this.buildings[this.buildings.length - 1].x + this.buildings[this.buildings.length - 1].width + this.gap : 0;
b.x = lastX;
b.width = width;
b.height= height;
this.buildings.push(b);
}
}
draw(ctx) {
ctx.fillStyle = this.colour;
for (const b of this.buildings) {
ctx.fillRect(b.x, canvas.height - b.height, b.width, b.height);
}
}
}
// Create layers with different depths. Closer layers have
// larger buildings and move faster.
let layers = [];
function buildLayers() {
layers = [
new Layer({
speed: 0.5,
minWidth: 40,
maxWidth: 80,
gap: 15,
minHeight: canvas.height * 0.2,
maxHeight: canvas.height * 0.4,
colour: 'rgba(0,0,0,0.6)'
}),
new Layer({
speed: 1.0,
minWidth: 30,
maxWidth: 60,
gap: 10,
minHeight: canvas.height * 0.3,
maxHeight: canvas.height * 0.6,
colour: 'rgba(0,0,0,0.8)'
}),
new Layer({
speed: 2.0,
minWidth: 20,
maxWidth: 40,
gap: 8,
minHeight: canvas.height * 0.4,
maxHeight: canvas.height * 0.8,
colour: 'rgba(0,0,0,1)'
})
];
}
// Animation loop
function render() {
// Draw sky gradient: dark at top, lighter near horizon.
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#152850');
grad.addColorStop(1, '#113366');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw stars. We use a simple seeded random distribution each frame.
ctx.fillStyle = 'rgba(255,255,255,0.5)';
for (let i = 0; i < 80; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height * 0.6;
ctx.fillRect(x, y, 2, 2);
}
// Update and draw each layer. Speed scale increases with mouseX.
const scale = 0.5 + mouseX * 1.5;
for (const layer of layers) {
layer.update(scale);
layer.draw(ctx);
}
requestAnimationFrame(render);
}
render();
})();