Side-Scrolling Car Animation — Ranked by Visual Quality
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Driving Scene - Canvas Animation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
overflow: hidden;
background: #000;
font-family: Arial, sans-serif;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let time = 0;
const roadY = canvas.height * 0.6;
const carX = canvas.width * 0.3;
const carY = roadY - 40;
const carWidth = 80;
const carHeight = 40;
const wheelRadius = 8;
class ParallaxLayer {
constructor(speed, drawFn) {
this.speed = speed;
this.drawFn = drawFn;
this.offset = 0;
}
update(distance) {
this.offset = (this.offset - distance * this.speed) % (canvas.width * 2);
}
draw() {
this.drawFn(this.offset);
this.drawFn(this.offset + canvas.width * 2);
}
}
function drawSky() {
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#87CEEB');
gradient.addColorStop(0.5, '#FFB366');
gradient.addColorStop(1, '#FF8844');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawDistantMountains(offset) {
ctx.fillStyle = '#8B7355';
ctx.globalAlpha = 0.5;
const mountainWidth = canvas.width * 1.5;
for (let i = -1; i < 3; i++) {
const x = i * mountainWidth + offset;
ctx.beginPath();
ctx.moveTo(x, canvas.height * 0.35);
ctx.lineTo(x + mountainWidth * 0.3, canvas.height * 0.15);
ctx.lineTo(x + mountainWidth * 0.6, canvas.height * 0.25);
ctx.lineTo(x + mountainWidth, canvas.height * 0.35);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawTrees(offset) {
ctx.globalAlpha = 0.8;
const treeSpacing = canvas.width * 0.4;
for (let i = -2; i < 4; i++) {
const x = i * treeSpacing + offset;
const treeY = roadY - 60;
ctx.fillStyle = '#654321';
ctx.fillRect(x + 5, treeY, 10, 50);
ctx.fillStyle = '#228B22';
ctx.beginPath();
ctx.arc(x + 10, treeY, 25, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawPoles(offset) {
ctx.globalAlpha = 0.7;
const poleSpacing = canvas.width * 0.25;
for (let i = -2; i < 5; i++) {
const x = i * poleSpacing + offset;
ctx.strokeStyle = '#444';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(x, roadY - 80);
ctx.lineTo(x, roadY - 20);
ctx.stroke();
ctx.fillStyle = '#666';
ctx.fillRect(x - 15, roadY - 25, 30, 8);
}
ctx.globalAlpha = 1;
}
function drawRoadside(offset) {
const roadWidth = canvas.width * 3;
ctx.fillStyle = '#CCAA66';
for (let i = -1; i < 2; i++) {
ctx.fillRect(i * roadWidth + offset, roadY + 50, roadWidth, 30);
}
}
function drawGround(offset) {
const groundWidth = canvas.width * 2.5;
ctx.fillStyle = '#90EE90';
for (let i = -1; i < 2; i++) {
ctx.fillRect(i * groundWidth + offset, roadY + 80, groundWidth, canvas.height);
}
}
function drawRoad(offset) {
const roadWidth = canvas.width * 2;
ctx.fillStyle = '#222';
for (let i = -1; i < 2; i++) {
ctx.fillRect(i * roadWidth + offset, roadY, roadWidth, 50);
}
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 3;
ctx.setLineDash([20, 15]);
for (let i = -1; i < 2; i++) {
const x = i * roadWidth + offset;
ctx.beginPath();
ctx.moveTo(x, roadY + 25);
ctx.lineTo(x + roadWidth, roadY + 25);
ctx.stroke();
}
ctx.setLineDash([]);
}
function drawCar() {
const wheelY = carY + carHeight - 5;
const frontWheelX = carX + carWidth - 15;
const backWheelX = carX + 15;
ctx.save();
ctx.fillStyle = '#FF6B6B';
ctx.fillRect(carX, carY, carWidth, carHeight * 0.7);
ctx.fillStyle = '#B84040';
ctx.fillRect(carX + 10, carY - 10, carWidth - 20, carHeight * 0.5);
ctx.fillStyle = '#87CEEB';
ctx.fillRect(carX + 15, carY - 5, 15, 12);
ctx.fillRect(carX + 35, carY - 5, 15, 12);
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(backWheelX, wheelY, wheelRadius, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(frontWheelX, wheelY, wheelRadius, 0, Math.PI * 2);
ctx.fill();
const wheelRotation = (time * 0.15) % (Math.PI * 2);
function drawWheel(x, y, rotation) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.strokeStyle = '#555';
ctx.lineWidth = 1;
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(angle) * wheelRadius, Math.sin(angle) * wheelRadius);
ctx.stroke();
}
ctx.restore();
}
drawWheel(backWheelX, wheelY, wheelRotation);
drawWheel(frontWheelX, wheelY, wheelRotation);
ctx.restore();
}
const layers = [
new ParallaxLayer(0.15, (offset) => {
ctx.save();
drawDistantMountains(offset);
ctx.restore();
}),
new ParallaxLayer(0.3, (offset) => {
ctx.save();
drawTrees(offset);
ctx.restore();
}),
new ParallaxLayer(0.35, (offset) => {
ctx.save();
drawPoles(offset);
ctx.restore();
}),
new ParallaxLayer(0.5, (offset) => {
ctx.save();
drawRoadside(offset);
ctx.restore();
}),
new ParallaxLayer(0.6, (offset) => {
ctx.save();
drawGround(offset);
ctx.restore();
}),
new ParallaxLayer(0.55, (offset) => {
ctx.save();
drawRoad(offset);
ctx.restore();
})
];
function animate() {
drawSky();
const scrollDistance = 3;
for (const layer of layers) {
layer.update(scrollDistance);
layer.draw();
}
drawCar();
time += 1;
requestAnimationFrame(animate);
}
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
animate();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Side-Scrolling Car Animation - Kimi K2.5</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #000;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Set canvas size
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
// Animation variables
let frame = 0;
const speed = 2;
// Car position
const car = {
x: canvas.width * 0.3,
y: canvas.height * 0.7,
wheelRotation: 0,
bounce: 0
};
// Scrolling layers for parallax
const layers = [
{ offset: 0, speed: 0.2, elements: [] }, // Far mountains
{ offset: 0, speed: 0.5, elements: [] }, // Hills
{ offset: 0, speed: 1.0, elements: [] }, // Near ground
{ offset: 0, speed: 1.5, elements: [] } // Roadside elements
];
// Generate random elements for each layer
function generateElements() {
// Far mountains
for (let i = 0; i < 5; i++) {
layers[0].elements.push({
x: i * 400,
type: 'mountain',
height: 150 + Math.random() * 100,
width: 300 + Math.random() * 200
});
}
// Hills
for (let i = 0; i < 8; i++) {
layers[1].elements.push({
x: i * 250,
type: 'hill',
height: 80 + Math.random() * 60,
width: 200 + Math.random() * 150
});
}
// Ground elements (trees, poles)
for (let i = 0; i < 15; i++) {
layers[2].elements.push({
x: i * 150,
type: Math.random() > 0.5 ? 'tree' : 'pole',
height: 60 + Math.random() * 40
});
}
// Roadside details
for (let i = 0; i < 20; i++) {
layers[3].elements.push({
x: i * 100,
type: Math.random() > 0.7 ? 'rock' : 'grass',
size: 10 + Math.random() * 15
});
}
}
generateElements();
// Draw sky with gradient (sunset)
function drawSky() {
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#2c3e50');
gradient.addColorStop(0.3, '#4a235a');
gradient.addColorStop(0.6, '#d35400');
gradient.addColorStop(1, '#f39c12');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Sun
const sunGradient = ctx.createRadialGradient(
canvas.width * 0.7, canvas.height * 0.3, 0,
canvas.width * 0.7, canvas.height * 0.3, 100
);
sunGradient.addColorStop(0, 'rgba(255, 200, 100, 0.8)');
sunGradient.addColorStop(0.5, 'rgba(255, 150, 50, 0.4)');
sunGradient.addColorStop(1, 'rgba(255, 100, 0, 0)');
ctx.fillStyle = sunGradient;
ctx.beginPath();
ctx.arc(canvas.width * 0.7, canvas.height * 0.3, 100, 0, Math.PI * 2);
ctx.fill();
}
// Draw mountains
function drawMountains(layer) {
ctx.fillStyle = '#1a252f';
layer.elements.forEach(el => {
const x = (el.x - layer.offset) % (canvas.width + 400);
const drawX = x < -200 ? x + canvas.width + 400 : x;
ctx.beginPath();
ctx.moveTo(drawX, canvas.height * 0.6);
ctx.lineTo(drawX + el.width / 2, canvas.height * 0.6 - el.height);
ctx.lineTo(drawX + el.width, canvas.height * 0.6);
ctx.closePath();
ctx.fill();
});
}
// Draw hills
function drawHills(layer) {
ctx.fillStyle = '#2d5016';
layer.elements.forEach(el => {
const x = (el.x - layer.offset) % (canvas.width + 300);
const drawX = x < -150 ? x + canvas.width + 300 : x;
ctx.beginPath();
ctx.ellipse(
drawX + el.width / 2,
canvas.height * 0.65,
el.width / 2,
el.height,
0, 0, Math.PI * 2
);
ctx.fill();
});
}
// Draw ground elements
function drawGroundElements(layer) {
layer.elements.forEach(el => {
const x = (el.x - layer.offset) % (canvas.width + 200);
const drawX = x < -100 ? x + canvas.width + 200 : x;
if (el.type === 'tree') {
// Tree trunk
ctx.fillStyle = '#5d4037';
ctx.fillRect(drawX - 3, canvas.height * 0.65 - el.height, 6, el.height);
// Tree top
ctx.fillStyle = '#1b5e20';
ctx.beginPath();
ctx.moveTo(drawX - 25, canvas.height * 0.65 - el.height);
ctx.lineTo(drawX, canvas.height * 0.65 - el.height - 50);
ctx.lineTo(drawX + 25, canvas.height * 0.65 - el.height);
ctx.closePath();
ctx.fill();
} else {
// Pole
ctx.fillStyle = '#555';
ctx.fillRect(drawX - 2, canvas.height * 0.65 - el.height, 4, el.height);
}
});
}
// Draw car
function drawCar() {
const x = car.x;
const y = car.y + Math.sin(car.bounce) * 3;
// Car body shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x - 50, canvas.height * 0.72, 100, 10);
// Car body
ctx.fillStyle = '#c0392b';
ctx.beginPath();
ctx.moveTo(x - 50, y);
ctx.lineTo(x - 40, y - 30);
ctx.lineTo(x + 20, y - 30);
ctx.lineTo(x + 50, y);
ctx.closePath();
ctx.fill();
// Car cabin
ctx.fillStyle = '#922b21';
ctx.beginPath();
ctx.moveTo(x - 30, y - 30);
ctx.lineTo(x - 20, y - 50);
ctx.lineTo(x + 10, y - 50);
ctx.lineTo(x + 15, y - 30);
ctx.closePath();
ctx.fill();
// Windows
ctx.fillStyle = '#aed6f1';
ctx.fillRect(x - 18, y - 48, 20, 15);
// Wheels
const wheelY = y + 10;
[-25, 25].forEach(offset => {
const wx = x + offset;
// Tire
ctx.fillStyle = '#2c3e50';
ctx.beginPath();
ctx.arc(wx, wheelY, 15, 0, Math.PI * 2);
ctx.fill();
// Rim
ctx.fillStyle = '#bdc3c7';
ctx.beginPath();
ctx.arc(wx, wheelY, 8, 0, Math.PI * 2);
ctx.fill();
// Spokes (rotating)
ctx.strokeStyle = '#7f8c8d';
ctx.lineWidth = 2;
for (let i = 0; i < 4; i++) {
const angle = car.wheelRotation + (i * Math.PI / 2);
ctx.beginPath();
ctx.moveTo(wx, wheelY);
ctx.lineTo(wx + Math.cos(angle) * 8, wheelY + Math.sin(angle) * 8);
ctx.stroke();
}
});
// Headlights
ctx.fillStyle = '#f1c40f';
ctx.beginPath();
ctx.arc(x + 48, y - 10, 5, 0, Math.PI * 2);
ctx.fill();
// Light beam
const beamGradient = ctx.createLinearGradient(x + 50, y - 10, x + 200, y + 20);
beamGradient.addColorStop(0, 'rgba(241, 196, 15, 0.3)');
beamGradient.addColorStop(1, 'rgba(241, 196, 15, 0)');
ctx.fillStyle = beamGradient;
ctx.beginPath();
ctx.moveTo(x + 50, y - 10);
ctx.lineTo(x + 200, y - 30);
ctx.lineTo(x + 200, y + 30);
ctx.closePath();
ctx.fill();
}
// Draw road
function drawRoad() {
// Road surface
ctx.fillStyle = '#34495e';
ctx.fillRect(0, canvas.height * 0.72, canvas.width, canvas.height * 0.28);
// Road lines
ctx.strokeStyle = '#ecf0f1';
ctx.lineWidth = 3;
ctx.setLineDash([40, 40]);
const lineOffset = (frame * speed) % 80;
ctx.beginPath();
ctx.moveTo(-80 + lineOffset, canvas.height * 0.85);
ctx.lineTo(canvas.width + 80, canvas.height * 0.85);
ctx.stroke();
ctx.setLineDash([]);
}
// Animation loop
function animate() {
frame++;
// Update car
car.wheelRotation += 0.15;
car.bounce += 0.1;
// Update layer offsets
layers.forEach(layer => {
layer.offset += layer.speed * speed;
});
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw scene back to front
drawSky();
drawMountains(layers[0]);
drawHills(layers[1]);
drawGroundElements(layers[2]);
drawRoad();
drawCar();
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Side-Scrolling Car Animation - Qwen 3.6 27B</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
// Scene elements
let scrollX = 0;
const car = {
x: window.innerWidth * 0.2,
y: window.innerHeight * 0.68,
wheelAngle: 0,
bodyOffset: 0
};
// Background layers with parallax speeds
const bgLayers = [
{ speed: 0.1, items: [], type: 'mountain' },
{ speed: 0.3, items: [], type: 'hill' },
{ speed: 0.8, items: [], type: 'tree' },
{ speed: 1.2, items: [], type: 'ground' }
];
// Generate background items
function initBackground() {
// Mountains (far)
for (let i = 0; i < 6; i++) {
bgLayers[0].items.push({
x: i * 500,
w: 400 + Math.random() * 200,
h: 200 + Math.random() * 150
});
}
// Hills (mid-far)
for (let i = 0; i < 10; i++) {
bgLayers[1].items.push({
x: i * 300,
w: 250 + Math.random() * 100,
h: 100 + Math.random() * 80
});
}
// Trees (mid-near)
for (let i = 0; i < 12; i++) {
bgLayers[2].items.push({
x: i * 200,
h: 80 + Math.random() * 60,
variant: Math.floor(Math.random() * 3)
});
}
// Ground details (near)
for (let i = 0; i < 20; i++) {
bgLayers[3].items.push({
x: i * 120,
type: Math.random() > 0.6 ? 'rock' : 'bush',
size: 15 + Math.random() * 20
});
}
}
initBackground();
// Draw gradient sky (dusk theme)
function drawSky() {
const g = ctx.createLinearGradient(0, 0, 0, canvas.height);
g.addColorStop(0, '#1a1a2e');
g.addColorStop(0.4, '#16213e');
g.addColorStop(0.7, '#e94560');
g.addColorStop(1, '#f39c12');
ctx.fillStyle = g;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Distant stars
ctx.fillStyle = 'rgba(255,255,255,0.5)';
for (let i = 0; i < 50; i++) {
const sx = (i * 137) % canvas.width;
const sy = (i * 73) % (canvas.height * 0.4);
ctx.fillRect(sx, sy, 2, 2);
}
// Sun setting
const sunG = ctx.createRadialGradient(
canvas.width * 0.8, canvas.height * 0.4, 0,
canvas.width * 0.8, canvas.height * 0.4, 120
);
sunG.addColorStop(0, 'rgba(255, 100, 50, 0.6)');
sunG.addColorStop(0.5, 'rgba(255, 50, 50, 0.2)');
sunG.addColorStop(1, 'rgba(255, 0, 0, 0)');
ctx.fillStyle = sunG;
ctx.beginPath();
ctx.arc(canvas.width * 0.8, canvas.height * 0.4, 120, 0, Math.PI * 2);
ctx.fill();
}
// Draw mountains (layer 0)
function drawMountains(layer) {
ctx.fillStyle = '#0f0f23';
layer.items.forEach(item => {
const x = (item.x - scrollX * layer.speed) % (canvas.width + 600);
const drawX = x < -300 ? x + canvas.width + 600 : x - 300;
ctx.beginPath();
ctx.moveTo(drawX, canvas.height * 0.55);
ctx.lineTo(drawX + item.w / 2, canvas.height * 0.55 - item.h);
ctx.lineTo(drawX + item.w, canvas.height * 0.55);
ctx.lineTo(drawX, canvas.height * 0.55);
ctx.fill();
});
}
// Draw hills (layer 1)
function drawHills(layer) {
ctx.fillStyle = '#1e3a1e';
layer.items.forEach(item => {
const x = (item.x - scrollX * layer.speed) % (canvas.width + 400);
const drawX = x < -200 ? x + canvas.width + 400 : x - 200;
ctx.beginPath();
ctx.ellipse(drawX + item.w/2, canvas.height * 0.6, item.w/2, item.h, 0, 0, Math.PI * 2);
ctx.fill();
});
}
// Draw trees (layer 2)
function drawTrees(layer) {
layer.items.forEach(item => {
const x = (item.x - scrollX * layer.speed) % (canvas.width + 300);
const drawX = x < -100 ? x + canvas.width + 300 : x - 100;
const y = canvas.height * 0.62;
// Trunk
ctx.fillStyle = '#3d2817';
ctx.fillRect(drawX - 4, y - item.h, 8, item.h);
// Tree crown based on variant
const colors = ['#2d5016', '#1e3a1e', '#3d6b1e'];
ctx.fillStyle = colors[item.variant];
ctx.beginPath();
ctx.arc(drawX, y - item.h, 30, 0, Math.PI * 2);
ctx.arc(drawX - 15, y - item.h + 10, 25, 0, Math.PI * 2);
ctx.arc(drawX + 15, y - item.h + 10, 25, 0, Math.PI * 2);
ctx.fill();
});
}
// Draw ground details (layer 3)
function drawGroundDetails(layer) {
layer.items.forEach(item => {
const x = (item.x - scrollX * layer.speed) % (canvas.width + 250);
const drawX = x < -50 ? x + canvas.width + 250 : x - 50;
const y = canvas.height * 0.72;
if (item.type === 'rock') {
ctx.fillStyle = '#5a5a5a';
ctx.beginPath();
ctx.ellipse(drawX, y + 20, item.size, item.size * 0.6, 0, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.fillStyle = '#3d5c1e';
ctx.beginPath();
ctx.ellipse(drawX, y + 15, item.size * 1.5, item.size * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
}
});
}
// Draw road
function drawRoad() {
ctx.fillStyle = '#2c3e50';
ctx.fillRect(0, canvas.height * 0.72, canvas.width, canvas.height * 0.28);
// Road edge
ctx.fillStyle = '#34495e';
ctx.fillRect(0, canvas.height * 0.72, canvas.width, 5);
// Moving road lines
ctx.strokeStyle = '#ecf0f1';
ctx.lineWidth = 4;
ctx.setLineDash([50, 50]);
const dashOffset = -(scrollX * 1.5) % 100;
ctx.lineDashOffset = dashOffset;
ctx.beginPath();
ctx.moveTo(0, canvas.height * 0.86);
ctx.lineTo(canvas.width, canvas.height * 0.86);
ctx.stroke();
ctx.setLineDash([]);
ctx.lineDashOffset = 0;
}
// Draw car
function drawCar() {
const cx = car.x;
const cy = car.y + Math.sin(car.bodyOffset) * 2;
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.beginPath();
ctx.ellipse(cx, canvas.height * 0.76, 60, 10, 0, 0, Math.PI * 2);
ctx.fill();
// Car body (red sports car)
ctx.fillStyle = '#c0392b';
// Lower body
ctx.beginPath();
ctx.moveTo(cx - 55, cy);
ctx.lineTo(cx - 45, cy - 25);
ctx.lineTo(cx + 35, cy - 25);
ctx.lineTo(cx + 55, cy);
ctx.closePath();
ctx.fill();
// Upper cabin
ctx.fillStyle = '#a93226';
ctx.beginPath();
ctx.moveTo(cx - 35, cy - 25);
ctx.lineTo(cx - 25, cy - 50);
ctx.lineTo(cx + 20, cy - 50);
ctx.lineTo(cx + 30, cy - 25);
ctx.closePath();
ctx.fill();
// Windshield
ctx.fillStyle = '#85c1e9';
ctx.beginPath();
ctx.moveTo(cx - 20, cy - 48);
ctx.lineTo(cx + 15, cy - 48);
ctx.lineTo(cx + 25, cy - 25);
ctx.lineTo(cx - 18, cy - 25);
ctx.closePath();
ctx.fill();
// Side window
ctx.fillStyle = '#5dade2';
ctx.fillRect(cx - 30, cy - 45, 25, 18);
// Wheels
const wheelY = cy + 8;
[-30, 30].forEach((offset, i) => {
const wx = cx + offset;
// Tire
ctx.fillStyle = '#1c2833';
ctx.beginPath();
ctx.arc(wx, wheelY, 16, 0, Math.PI * 2);
ctx.fill();
// Rim
ctx.fillStyle = '#95a5a6';
ctx.beginPath();
ctx.arc(wx, wheelY, 10, 0, Math.PI * 2);
ctx.fill();
// Rotating spokes
ctx.strokeStyle = '#555';
ctx.lineWidth = 3;
for (let j = 0; j < 5; j++) {
const angle = car.wheelAngle + (j * Math.PI * 2 / 5);
ctx.beginPath();
ctx.moveTo(wx, wheelY);
ctx.lineTo(wx + Math.cos(angle) * 10, wheelY + Math.sin(angle) * 10);
ctx.stroke();
}
// Wheel hub
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(wx, wheelY, 4, 0, Math.PI * 2);
ctx.fill();
});
// Headlights
ctx.fillStyle = '#f1c40f';
ctx.beginPath();
ctx.ellipse(cx + 52, cy - 12, 6, 4, 0, 0, Math.PI * 2);
ctx.fill();
// Taillights
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.ellipse(cx - 52, cy - 8, 4, 3, 0, 0, Math.PI * 2);
ctx.fill();
// Headlight beam
const beamGrad = ctx.createLinearGradient(cx + 55, cy - 12, cx + 250, cy + 20);
beamGrad.addColorStop(0, 'rgba(241, 196, 15, 0.4)');
beamGrad.addColorStop(0.5, 'rgba(241, 196, 15, 0.1)');
beamGrad.addColorStop(1, 'rgba(241, 196, 15, 0)');
ctx.fillStyle = beamGrad;
ctx.beginPath();
ctx.moveTo(cx + 55, cy - 12);
ctx.lineTo(cx + 250, cy - 40);
ctx.lineTo(cx + 250, cy + 30);
ctx.closePath();
ctx.fill();
}
// Main loop
function loop() {
// Update
scrollX += 3;
car.wheelAngle += 0.2;
car.bodyOffset += 0.15;
// Clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw back to front
drawSky();
drawMountains(bgLayers[0]);
drawHills(bgLayers[1]);
drawTrees(bgLayers[2]);
drawRoad();
drawGroundDetails(bgLayers[3]);
drawCar();
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cinematic Drive</title>
<style>
body, html { margin: 0; padding: 0; overflow: hidden; background: #000; width: 100%; height: 100%; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="scene"></canvas>
<script>
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');
let width, height;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
let t = 0;
function drawMountains(yBase, amplitude, frequency, offset, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(0, height);
for (let x = 0; x <= width; x += 10) {
let y = yBase + Math.sin((x + offset) * frequency) * amplitude
+ Math.sin((x + offset) * frequency * 0.5) * amplitude * 0.5;
ctx.lineTo(x, y);
}
ctx.lineTo(width, height);
ctx.closePath();
ctx.fill();
}
function loop() {
t += 0.016; // Approx 60fps time step
ctx.clearRect(0, 0, width, height);
// 1. Sky Gradient (Sunset/Dusk)
let sky = ctx.createLinearGradient(0, 0, 0, height * 0.6);
sky.addColorStop(0, "#1a0b2e");
sky.addColorStop(0.4, "#4b1d52");
sky.addColorStop(0.8, "#d15c55");
sky.addColorStop(1, "#f2a65a");
ctx.fillStyle = sky;
ctx.fillRect(0, 0, width, height);
// 2. Setting Sun
ctx.fillStyle = "#ffcc77";
ctx.shadowColor = "#ffaa00";
ctx.shadowBlur = 60;
ctx.beginPath();
ctx.arc(width * 0.7, height * 0.45, 60, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// 3. Parallax Mountains (Distant, Mid, Near)
drawMountains(height * 0.5, 40, 0.003, t * 20, "#311442");
drawMountains(height * 0.55, 60, 0.005, t * 50, "#230e30");
drawMountains(height * 0.65, 30, 0.01, t * 120, "#110518");
// 4. Ground base
let groundY = height * 0.75;
ctx.fillStyle = "#0a030f";
ctx.fillRect(0, groundY, width, height - groundY);
// 5. Scenery: Passing Trees/Poles (Layer behind road)
ctx.fillStyle = "#050108";
let treeSpeed = t * 600;
let treeSpace = 350;
for (let x = -(treeSpeed % treeSpace); x < width + treeSpace; x += treeSpace) {
let hOff = Math.abs(Math.sin((x + treeSpeed) * 0.01)) * 80;
ctx.fillRect(x + 50, groundY - 60 - hOff, 15, 80 + hOff);
ctx.beginPath();
ctx.arc(x + 57, groundY - 60 - hOff, 35, 0, Math.PI*2);
ctx.fill();
}
// 6. Road Surface & Markings
ctx.fillStyle = "#1e1e24";
ctx.fillRect(0, groundY + 30, width, 80);
ctx.fillStyle = "#dddddd";
let dashSpeed = t * 1200;
let dashLen = 120;
let gap = 120;
let totalDash = dashLen + gap;
for (let x = -(dashSpeed % totalDash); x < width; x += totalDash) {
ctx.fillRect(x, groundY + 65, dashLen, 8);
}
// 7. Foreground Poles (Fastest)
ctx.fillStyle = "#020103";
let poleSpeed = t * 2000;
let poleSpace = 800;
for (let x = -(poleSpeed % poleSpace); x < width + poleSpace; x += poleSpace) {
ctx.fillRect(x, groundY + 110, 30, height);
}
// 8. The Car
let cx = width * 0.3;
let cy = groundY - 20;
// Subtle suspension bounce
let bounce = Math.sin(t * 18) * 1.5 + Math.sin(t * 31) * 0.5;
cy += bounce;
// Shadow beneath car
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.beginPath();
ctx.ellipse(cx, groundY + 40, 180, 12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.translate(cx, cy);
// Car Body (Cinematic silhouette profile)
ctx.fillStyle = "#0b0c10";
ctx.beginPath();
ctx.moveTo(-160, -10);
ctx.lineTo(-150, -35); // trunk
ctx.lineTo(-90, -42); // rear window
ctx.lineTo(-20, -70); // roof
ctx.lineTo(40, -70); // roof front
ctx.lineTo(95, -35); // windshield
ctx.lineTo(165, -20); // hood
ctx.lineTo(175, 10); // front bumper
ctx.lineTo(165, 30); // bottom front
ctx.lineTo(-160, 30); // bottom rear
ctx.lineTo(-170, 10); // rear bumper
ctx.fill();
// Windows (dark reflections)
ctx.fillStyle = "#22252c";
ctx.beginPath();
ctx.moveTo(-80, -40);
ctx.lineTo(-22, -65);
ctx.lineTo(5, -65);
ctx.lineTo(5, -36);
ctx.fill();
ctx.beginPath();
ctx.moveTo(12, -65);
ctx.lineTo(38, -65);
ctx.lineTo(85, -35);
ctx.lineTo(12, -35);
ctx.fill();
// Headlight Beam
let flicker = Math.random() * 0.05 + 0.95;
ctx.shadowColor = "#e0f7fa";
ctx.shadowBlur = 25;
ctx.fillStyle = "#e0f7fa";
ctx.beginPath();
ctx.arc(170, -5, 4, 0, Math.PI*2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.globalAlpha = 0.2 * flicker;
ctx.beginPath();
ctx.moveTo(170, -5);
ctx.lineTo(800, -25);
ctx.lineTo(800, 80);
ctx.fill();
ctx.globalAlpha = 1.0;
// Taillight
ctx.shadowColor = "#ff1744";
ctx.shadowBlur = 20;
ctx.fillStyle = "#ff1744";
ctx.beginPath();
ctx.arc(-165, -12, 5, 0, Math.PI*2);
ctx.fill();
ctx.shadowBlur = 0;
// Wheels Function
function drawWheel(wx, wy) {
ctx.save();
ctx.translate(wx, wy);
ctx.translate(0, -bounce * 0.8); // Wheels stay relatively grounded
ctx.rotate(t * 18); // Wheel spin
// Tire
ctx.fillStyle = "#111";
ctx.beginPath();
ctx.arc(0, 0, 34, 0, Math.PI * 2);
ctx.fill();
// Rim
ctx.strokeStyle = "#444";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, 22, 0, Math.PI * 2);
ctx.stroke();
// Spokes
ctx.strokeStyle = "#888";
ctx.lineWidth = 3;
for(let i=0; i<6; i++) {
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(22, 0);
ctx.stroke();
ctx.rotate(Math.PI / 3);
}
ctx.restore();
}
// Front and Rear Wheels
drawWheel(-100, 30);
drawWheel(110, 30);
ctx.restore();
// 9. Speed Lines (Foreground motion blur)
ctx.fillStyle = "rgba(255,255,255,0.06)";
let fgSpeed = t * 2800;
for(let i=0; i<4; i++) {
let y = groundY + 90 + i * 15;
let dashL = 250 + i * 80;
let spc = 500;
let x = -((fgSpeed * (1 + i*0.1)) % spc) + (i*100);
ctx.fillRect(x, y, dashL, 3);
ctx.fillRect(x + spc, y, dashL, 3);
ctx.fillRect(x + spc*2, y, dashL, 3);
}
requestAnimationFrame(loop);
}
// Start loop
requestAnimationFrame(loop);
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Car Journey</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
// Animation state
let time = 0;
const speed = 2;
// Parallax layers with different speeds
const layers = {
sky: { speed: 0.1, offset: 0 },
mountains: { speed: 0.2, offset: 0 },
hills: { speed: 0.4, offset: 0 },
trees: { speed: 0.8, offset: 0 },
poles: { speed: 1.2, offset: 0 },
ground: { speed: 2, offset: 0 }
};
// Car physics
const car = {
x: 0,
y: 0,
wheelRotation: 0,
bounce: 0,
bouncePhase: 0
};
// Scenery elements
const trees = [];
const poles = [];
const clouds = [];
// Initialize scenery
function initScenery() {
for (let i = 0; i < 8; i++) {
trees.push({
x: Math.random() * canvas.width * 2,
y: 0,
type: Math.floor(Math.random() * 3),
scale: 0.8 + Math.random() * 0.4
});
}
for (let i = 0; i < 6; i++) {
poles.push({
x: Math.random() * canvas.width * 2,
height: 80 + Math.random() * 40
});
}
for (let i = 0; i < 5; i++) {
clouds.push({
x: Math.random() * canvas.width,
y: 50 + Math.random() * 150,
scale: 0.5 + Math.random() * 0.8,
speed: 0.1 + Math.random() * 0.2
});
}
}
// Draw gradient sky with sunset colors
function drawSky() {
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.7);
gradient.addColorStop(0, '#1a1a3e');
gradient.addColorStop(0.3, '#4a3b6e');
gradient.addColorStop(0.5, '#d4706e');
gradient.addColorStop(0.7, '#f4a261');
gradient.addColorStop(1, '#e9c46a');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw sun
const sunY = canvas.height * 0.35;
const sunGradient = ctx.createRadialGradient(
canvas.width * 0.7, sunY, 0,
canvas.width * 0.7, sunY, 80
);
sunGradient.addColorStop(0, 'rgba(255, 200, 100, 0.9)');
sunGradient.addColorStop(0.5, 'rgba(255, 150, 80, 0.5)');
sunGradient.addColorStop(1, 'rgba(255, 100, 60, 0)');
ctx.fillStyle = sunGradient;
ctx.beginPath();
ctx.arc(canvas.width * 0.7, sunY, 80, 0, Math.PI * 2);
ctx.fill();
// Draw clouds
ctx.fillStyle = 'rgba(255, 200, 180, 0.3)';
clouds.forEach(cloud => {
cloud.x -= cloud.speed;
if (cloud.x < -200) cloud.x = canvas.width + 200;
ctx.beginPath();
ctx.arc(cloud.x, cloud.y, 30 * cloud.scale, 0, Math.PI * 2);
ctx.arc(cloud.x + 25 * cloud.scale, cloud.y - 10 * cloud.scale, 35 * cloud.scale, 0, Math.PI * 2);
ctx.arc(cloud.x + 50 * cloud.scale, cloud.y, 30 * cloud.scale, 0, Math.PI * 2);
ctx.fill();
});
}
// Draw distant mountains
function drawMountains() {
layers.mountains.offset -= layers.mountains.speed;
if (layers.mountains.offset < -canvas.width) layers.mountains.offset = 0;
const mountainY = canvas.height * 0.55;
ctx.fillStyle = '#2d3561';
for (let offset of [layers.mountains.offset, layers.mountains.offset + canvas.width]) {
ctx.beginPath();
ctx.moveTo(offset, canvas.height);
// Generate mountain peaks
const peaks = [0.1, 0.25, 0.4, 0.6, 0.75, 0.9];
peaks.forEach((peak, i) => {
const x = offset + peak * canvas.width;
const height = 100 + Math.sin(i * 2.5) * 60;
ctx.lineTo(x, mountainY - height);
});
ctx.lineTo(offset + canvas.width, canvas.height);
ctx.fill();
}
}
// Draw rolling hills
function drawHills() {
layers.hills.offset -= layers.hills.speed;
if (layers.hills.offset < -canvas.width) layers.hills.offset = 0;
const hillY = canvas.height * 0.65;
ctx.fillStyle = '#3d5a40';
for (let offset of [layers.hills.offset, layers.hills.offset + canvas.width]) {
ctx.beginPath();
ctx.moveTo(offset, canvas.height);
for (let x = 0; x <= canvas.width; x += 20) {
const y = hillY - Math.sin((x + offset) * 0.01) * 40 - Math.sin((x + offset) * 0.003) * 20;
ctx.lineTo(offset + x, y);
}
ctx.lineTo(offset + canvas.width, canvas.height);
ctx.fill();
}
}
// Draw trees
function drawTrees() {
layers.trees.offset -= layers.trees.speed;
if (layers.trees.offset < -canvas.width * 2) layers.trees.offset = 0;
const groundY = canvas.height * 0.78;
trees.forEach(tree => {
let x = tree.x + layers.trees.offset;
while (x < -100) x += canvas.width * 2;
if (x > canvas.width + 100) return;
const y = groundY;
const scale = tree.scale;
// Tree trunk
ctx.fillStyle = '#4a3728';
ctx.fillRect(x - 4 * scale, y - 60 * scale, 8 * scale, 60 * scale);
// Tree foliage
ctx.fillStyle = '#2d5016';
ctx.beginPath();
ctx.moveTo(x - 25 * scale, y - 50 * scale);
ctx.lineTo(x, y - 120 * scale);
ctx.lineTo(x + 25 * scale, y - 50 * scale);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - 20 * scale, y - 80 * scale);
ctx.lineTo(x, y - 140 * scale);
ctx.lineTo(x + 20 * scale, y - 80 * scale);
ctx.fill();
});
}
// Draw utility poles
function drawPoles() {
layers.poles.offset -= layers.poles.speed;
if (layers.poles.offset < -canvas.width * 2) layers.poles.offset = 0;
const groundY = canvas.height * 0.78;
poles.forEach(pole => {
let x = pole.x + layers.poles.offset;
while (x < -50) x += canvas.width * 2;
if (x > canvas.width + 50) return;
// Pole
ctx.fillStyle = '#5a5a5a';
ctx.fillRect(x - 2, groundY - pole.height, 4, pole.height);
// Crossbar
ctx.fillRect(x - 15, groundY - pole.height + 10, 30, 3);
// Insulators
ctx.fillStyle = '#8a7a6a';
ctx.beginPath();
ctx.arc(x - 12, groundY - pole.height + 18, 3, 0, Math.PI * 2);
ctx.arc(x + 12, groundY - pole.height + 18, 3, 0, Math.PI * 2);
ctx.fill();
});
}
// Draw road
function drawRoad() {
const groundY = canvas.height * 0.78;
const roadHeight = canvas.height * 0.22;
// Ground
ctx.fillStyle = '#4a6741';
ctx.fillRect(0, groundY, canvas.width, roadHeight);
// Road surface
ctx.fillStyle = '#3a3a3a';
ctx.fillRect(0, groundY + 20, canvas.width, roadHeight - 20);
// Road markings
layers.ground.offset -= layers.ground.speed;
if (layers.ground.offset < -100) layers.ground.offset = 0;
ctx.fillStyle = '#e0e0e0';
for (let x = layers.ground.offset; x < canvas.width; x += 100) {
ctx.fillRect(x, groundY + roadHeight * 0.4, 50, 4);
}
// Road edge
ctx.fillStyle = '#8a8a6a';
ctx.fillRect(0, groundY + 15, canvas.width, 5);
}
// Draw car with detailed rendering
function drawCar() {
const carX = canvas.width * 0.35;
const groundY = canvas.height * 0.78;
const carY = groundY - 35 + car.bounce;
// Car shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.beginPath();
ctx.ellipse(carX, groundY - 5, 70, 10, 0, 0, Math.PI * 2);
ctx.fill();
// Car body - main chassis
ctx.fillStyle = '#c0392b';
ctx.beginPath();
ctx.moveTo(carX - 80, carY);
ctx.lineTo(carX - 70, carY - 35);
ctx.lineTo(carX - 30, carY - 55);
ctx.lineTo(carX + 20, carY - 55);
ctx.lineTo(carX + 60, carY - 35);
ctx.lineTo(carX + 75, carY - 15);
ctx.lineTo(carX + 80, carY);
ctx.closePath();
ctx.fill();
// Car body highlight
ctx.fillStyle = '#d35400';
ctx.beginPath();
ctx.moveTo(carX - 65, carY - 5);
ctx.lineTo(carX - 60, carY - 30);
ctx.lineTo(carX - 30, carY - 48);
ctx.lineTo(carX + 15, carY - 48);
ctx.lineTo(carX + 50, carY - 32);
ctx.lineTo(carX + 70, carY - 15);
ctx.lineTo(carX + 75, carY - 5);
ctx.closePath();
ctx.fill();
// Windows
ctx.fillStyle = '#2c3e50';
ctx.beginPath();
ctx.moveTo(carX - 25, carY - 50);
ctx.lineTo(carX + 15, carY - 50);
ctx.lineTo(carX + 45, carY - 35);
ctx.lineTo(carX - 20, carY - 35);
ctx.closePath();
ctx.fill();
// Window reflection
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
ctx.beginPath();
ctx.moveTo(carX - 20, carY - 48);
ctx.lineTo(carX - 5, carY - 48);
ctx.lineTo(carX - 5, carY - 40);
ctx.lineTo(carX - 20, carY - 40);
ctx.closePath();
ctx.fill();
// Headlight
ctx.fillStyle = '#f1c40f';
ctx.beginPath();
ctx.ellipse(carX + 75, carY - 12, 6, 8, 0, 0, Math.PI * 2);
ctx.fill();
// Headlight glow
ctx.fillStyle = 'rgba(241, 196, 15, 0.3)';
ctx.beginPath();
ctx.moveTo(carX + 80, carY - 12);
ctx.lineTo(carX + 200, carY - 30);
ctx.lineTo(carX + 200, carY + 10);
ctx.closePath();
ctx.fill();
// Taillight
ctx.fillStyle = '#c0392b';
ctx.fillRect(carX - 82, carY - 15, 4, 12);
ctx.fillStyle = '#e74c3c';
ctx.fillRect(carX - 81, carY - 14, 2, 10);
// Door handle
ctx.fillStyle = '#a0a0a0';
ctx.fillRect(carX - 5, carY - 25, 12, 3);
// Bumper front
ctx.fillStyle = '#7f8c8d';
ctx.fillRect(carX + 78, carY - 5, 5, 8);
// Bumper rear
ctx.fillRect(carX - 83, carY - 5, 5, 8);
// Wheels
drawWheel(carX - 45, carY + 5, 22);
drawWheel(carX + 45, carY + 5, 22);
}
// Draw a wheel with rotation
function drawWheel(x, y, radius) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(car.wheelRotation);
// Tire
ctx.fillStyle = '#1a1a1a';
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
// Tire sidewall
ctx.fillStyle = '#2a2a2a';
ctx.beginPath();
ctx.arc(0, 0, radius - 3, 0, Math.PI * 2);
ctx.fill();
// Rim
ctx.fillStyle = '#95a5a6';
ctx.beginPath();
ctx.arc(0, 0, radius - 8, 0, Math.PI * 2);
ctx.fill();
// Spokes
ctx.strokeStyle = '#7f8c8d';
ctx.lineWidth = 3;
for (let i = 0; i < 5; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(
Math.cos(i * Math.PI * 2 / 5) * (radius - 10),
Math.sin(i * Math.PI * 2 / 5) * (radius - 10)
);
ctx.stroke();
}
// Hub
ctx.fillStyle = '#34495e';
ctx.beginPath();
ctx.arc(0, 0, 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Update physics and animation state
function update() {
time += 0.016;
// Wheel rotation based on speed
car.wheelRotation += speed * 0.05;
// Car bounce simulation
car.bouncePhase += 0.15;
car.bounce = Math.sin(car.bouncePhase) * 1.5 + Math.sin(car.bouncePhase * 2.3) * 0.5;
}
// Main render loop
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSky();
drawMountains();
drawHills();
drawTrees();
drawPoles();
drawRoad();
drawCar();
update();
requestAnimationFrame(render);
}
initScenery();
render();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Driving</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
resize();
window.addEventListener('resize', resize);
const SPEED = 3.5;
let offset = 0;
let frame = 0;
function roadY() { return H * 0.64; }
// Pre-generate scenery
const rand = (a, b) => a + Math.random() * (b - a);
const mtns = Array.from({length: 30}, (_, i) => ({
x: i * 220 + rand(0, 80),
h: rand(70, 160), w: rand(140, 220),
col: `hsl(230,${rand(15,30)}%,${rand(12,22)}%)`
}));
const hills = Array.from({length: 20}, (_, i) => ({
x: i * 180 + rand(0, 60),
h: rand(30, 70), w: rand(200, 350),
col: `hsl(130,${rand(20,35)}%,${rand(10,18)}%)`
}));
const bgTrees = Array.from({length: 40}, (_, i) => ({
x: i * 140 + rand(0, 60),
h: rand(55, 90), type: Math.random() > 0.5 ? 'pine' : 'round'
}));
const poles = Array.from({length: 15}, (_, i) => ({
x: i * 300 + rand(0, 120), h: rand(90, 130)
}));
const fgTrees = Array.from({length: 18}, (_, i) => ({
x: i * 260 + rand(0, 100), h: rand(60, 95)
}));
const stars = Array.from({length: 80}, () => ({
x: rand(0, 3000), y: rand(0, H * 0.45), r: rand(0.5, 1.8),
phase: rand(0, Math.PI * 2)
}));
const clouds = Array.from({length: 12}, (_, i) => ({
x: i * 350 + rand(0, 200), y: rand(20, 80),
w: rand(120, 220), h: rand(30, 55)
}));
function drawSky() {
const ry = roadY();
const g = ctx.createLinearGradient(0, 0, 0, ry);
g.addColorStop(0, '#05051a');
g.addColorStop(0.25, '#0d1035');
g.addColorStop(0.55, '#8b1a00');
g.addColorStop(0.78, '#e85d00');
g.addColorStop(0.92, '#f5a623');
g.addColorStop(1, '#ffd580');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, ry);
}
function drawStars() {
for (const s of stars) {
const x = ((s.x - offset * 0.008) % (W + 200) + W + 200) % (W + 200) - 100;
const alpha = 0.4 + 0.6 * Math.sin(frame * 0.04 + s.phase);
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
ctx.beginPath();
ctx.arc(x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
}
function drawSun() {
const ry = roadY();
const sx = W * 0.68, sy = ry * 0.82;
const g = ctx.createRadialGradient(sx, sy, 0, sx, sy, 130);
g.addColorStop(0, 'rgba(255,220,60,0.55)');
g.addColorStop(0.4, 'rgba(255,120,0,0.2)');
g.addColorStop(1, 'rgba(255,60,0,0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, ry);
ctx.fillStyle = '#ffc830';
ctx.beginPath();
ctx.arc(sx, sy, 38, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffe060';
ctx.beginPath();
ctx.arc(sx, sy, 28, 0, Math.PI * 2);
ctx.fill();
}
function drawClouds() {
for (const c of clouds) {
const x = ((c.x - offset * 0.018) % (W + 500) + W + 500) % (W + 500) - 250;
ctx.fillStyle = 'rgba(200,90,30,0.22)';
const drawEllipse = (dx, dy, rx, ry2) => {
ctx.beginPath(); ctx.ellipse(x + dx, c.y + dy, rx, ry2, 0, 0, Math.PI * 2); ctx.fill();
};
drawEllipse(0, 0, c.w * 0.45, c.h * 0.38);
drawEllipse(-c.w * 0.28, c.h * 0.08, c.w * 0.28, c.h * 0.28);
drawEllipse(c.w * 0.25, c.h * 0.1, c.w * 0.3, c.h * 0.25);
}
}
function drawMountains() {
const ry = roadY();
for (const m of mtns) {
const x = ((m.x - offset * 0.04) % (W + 500) + W + 500) % (W + 500) - 250;
ctx.fillStyle = m.col;
ctx.beginPath();
ctx.moveTo(x - m.w * 0.5, ry);
ctx.lineTo(x, ry - m.h);
ctx.lineTo(x + m.w * 0.5, ry);
ctx.closePath();
ctx.fill();
// Snow cap
ctx.fillStyle = 'rgba(255,255,255,0.07)';
ctx.beginPath();
ctx.moveTo(x - m.w * 0.1, ry - m.h + m.h * 0.15);
ctx.lineTo(x, ry - m.h);
ctx.lineTo(x + m.w * 0.1, ry - m.h + m.h * 0.15);
ctx.closePath();
ctx.fill();
}
}
function drawHillLayer() {
const ry = roadY();
// Wave hills
ctx.fillStyle = '#142a0e';
ctx.beginPath();
ctx.moveTo(0, ry);
for (let x = 0; x <= W; x += 6) {
const sx = x + offset * 0.12;
const y = ry - 28 - Math.sin(sx * 0.004) * 22 - Math.sin(sx * 0.0015) * 14;
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.lineTo(W, ry); ctx.closePath(); ctx.fill();
// Grassy edge top of road
ctx.fillStyle = '#1d3d12';
ctx.beginPath();
ctx.moveTo(0, ry);
for (let x = 0; x <= W; x += 6) {
const sx = x + offset * 0.22;
const y = ry - 8 - Math.sin(sx * 0.012) * 4;
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.lineTo(W, ry); ctx.closePath(); ctx.fill();
}
function drawBgTrees() {
const ry = roadY();
for (const t of bgTrees) {
const x = ((t.x - offset * 0.25) % (W + 400) + W + 400) % (W + 400) - 200;
drawTree(x, ry - 8, t.h * 0.72, t.type, '#1a4020', '#0e2310');
}
}
function drawTree(x, baseY, h, type, col1, col2) {
if (type === 'pine') {
ctx.fillStyle = col1;
for (let tier = 0; tier < 3; tier++) {
const ty = baseY - h * (0.3 * tier + 0.3);
const spread = h * (0.32 - tier * 0.07);
ctx.beginPath();
ctx.moveTo(x, ty - h * 0.4);
ctx.lineTo(x - spread, ty);
ctx.lineTo(x + spread, ty);
ctx.closePath(); ctx.fill();
}
ctx.fillStyle = col2;
ctx.fillRect(x - 3, baseY - h * 0.32, 6, h * 0.32);
} else {
ctx.fillStyle = '#3d2800';
const tw = 5, th = h * 0.32;
ctx.fillRect(x - tw * 0.5, baseY - th, tw, th);
ctx.fillStyle = col1;
ctx.beginPath();
ctx.arc(x, baseY - th - h * 0.38, h * 0.36, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = col2;
ctx.beginPath();
ctx.arc(x - h * 0.1, baseY - th - h * 0.42, h * 0.2, 0, Math.PI * 2);
ctx.fill();
}
}
function drawRoad() {
const ry = roadY();
const rh = H - ry;
const g = ctx.createLinearGradient(0, ry, 0, H);
g.addColorStop(0, '#2e2e2e');
g.addColorStop(1, '#484848');
ctx.fillStyle = g;
ctx.fillRect(0, ry, W, rh);
// Shoulder lines
ctx.fillStyle = '#f0f0b0';
ctx.fillRect(0, ry + 2, W, 4);
ctx.fillStyle = '#e0e0a0';
ctx.fillRect(0, H - 6, W, 4);
}
function drawLaneMarkings() {
const ry = roadY();
const laneH = H - ry;
const laneY = ry + laneH * 0.42;
const mw = 55, mh = 7, gap = 110;
ctx.fillStyle = '#e8e870';
for (let i = -2; i < W / gap + 3; i++) {
const x = ((i * gap - offset * 1) % (W + gap * 4) + W + gap * 4) % (W + gap * 4) - gap * 2;
if (x < W + mw) ctx.fillRect(x, laneY - mh * 0.5, mw, mh);
}
}
function drawPoles() {
const ry = roadY();
for (const p of poles) {
const x = ((p.x - offset * 0.75) % (W + 600) + W + 600) % (W + 600) - 300;
ctx.fillStyle = '#5a4a30';
ctx.fillRect(x - 4, ry - p.h, 8, p.h + 6);
// Cross-arm
ctx.fillStyle = '#4a3a22';
ctx.fillRect(x - 30, ry - p.h + 8, 60, 5);
// Wires to next pole position
ctx.strokeStyle = 'rgba(60,50,30,0.8)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x + 30, ry - p.h + 10);
ctx.bezierCurveTo(x + 130, ry - p.h + 26, x + 230, ry - p.h + 26, x + 330, ry - p.h + 10);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x - 30, ry - p.h + 10);
ctx.bezierCurveTo(x - 130, ry - p.h + 26, x - 230, ry - p.h + 26, x - 330, ry - p.h + 10);
ctx.stroke();
}
}
function drawFgTrees() {
const ry = roadY();
for (const t of fgTrees) {
const x = ((t.x - offset * 0.9) % (W + 600) + W + 600) % (W + 600) - 300;
drawTree(x, ry - 6, t.h, 'round', '#245c1a', '#153a0e');
}
}
function drawCar(bob) {
const ry = roadY();
const cx = W * 0.32;
const cy = ry + bob;
const bw = 210, bh = 62;
const wr = 30;
const bx = cx - bw * 0.42;
// Shadow
const shg = ctx.createRadialGradient(cx, ry + 10, 5, cx, ry + 10, bw * 0.52);
shg.addColorStop(0, 'rgba(0,0,0,0.45)');
shg.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = shg;
ctx.beginPath();
ctx.ellipse(cx, ry + 10, bw * 0.52, 10, 0, 0, Math.PI * 2);
ctx.fill();
// Lower body
ctx.fillStyle = '#b02010';
ctx.beginPath();
ctx.moveTo(bx + 18, cy - wr * 0.4);
ctx.lineTo(bx + 12, cy - bh * 0.6);
ctx.lineTo(bx + bw * 0.08, cy - bh * 0.88);
ctx.lineTo(bx + bw * 0.33, cy - bh * 1.08);
ctx.lineTo(bx + bw * 0.72, cy - bh * 1.08);
ctx.lineTo(bx + bw * 0.9, cy - bh * 0.68);
ctx.lineTo(bx + bw - 6, cy - wr * 0.4);
ctx.closePath();
ctx.fill();
// Body highlight
ctx.fillStyle = 'rgba(255,80,40,0.25)';
ctx.beginPath();
ctx.moveTo(bx + 30, cy - bh * 0.65);
ctx.lineTo(bx + bw * 0.1, cy - bh * 0.9);
ctx.lineTo(bx + bw * 0.6, cy - bh * 0.95);
ctx.lineTo(bx + bw * 0.72, cy - bh * 0.8);
ctx.closePath();
ctx.fill();
// Roof
ctx.fillStyle = '#901808';
ctx.beginPath();
ctx.moveTo(bx + bw * 0.19, cy - bh * 0.88);
ctx.lineTo(bx + bw * 0.31, cy - bh * 1.52);
ctx.lineTo(bx + bw * 0.7, cy - bh * 1.52);
ctx.lineTo(bx + bw * 0.84, cy - bh * 0.88);
ctx.closePath();
ctx.fill();
// Windshield (front)
ctx.fillStyle = 'rgba(130,200,240,0.68)';
ctx.beginPath();
ctx.moveTo(bx + bw * 0.56, cy - bh * 0.9);
ctx.lineTo(bx + bw * 0.64, cy - bh * 1.46);
ctx.lineTo(bx + bw * 0.79, cy - bh * 1.46);
ctx.lineTo(bx + bw * 0.82, cy - bh * 0.9);
ctx.closePath();
ctx.fill();
// Windshield reflection
ctx.fillStyle = 'rgba(255,255,255,0.18)';
ctx.beginPath();
ctx.moveTo(bx + bw * 0.57, cy - bh * 0.92);
ctx.lineTo(bx + bw * 0.63, cy - bh * 1.42);
ctx.lineTo(bx + bw * 0.67, cy - bh * 1.42);
ctx.lineTo(bx + bw * 0.61, cy - bh * 0.92);
ctx.closePath();
ctx.fill();
// Rear window
ctx.fillStyle = 'rgba(110,180,220,0.6)';
ctx.beginPath();
ctx.moveTo(bx + bw * 0.21, cy - bh * 0.9);
ctx.lineTo(bx + bw * 0.33, cy - bh * 1.46);
ctx.lineTo(bx + bw * 0.54, cy - bh * 1.46);
ctx.lineTo(bx + bw * 0.54, cy - bh * 0.9);
ctx.closePath();
ctx.fill();
// Door seam
ctx.strokeStyle = '#6a0c04';
ctx.lineWidth = 1.8;
ctx.beginPath();
ctx.moveTo(bx + bw * 0.54, cy - wr * 0.4);
ctx.lineTo(bx + bw * 0.54, cy - bh * 0.9);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bx + bw * 0.54, cy - bh * 0.56);
ctx.quadraticCurveTo(bx + bw * 0.37, cy - bh * 0.56, bx + bw * 0.28, cy - bh * 0.5);
ctx.stroke();
// Headlight
ctx.fillStyle = '#ffe870';
ctx.beginPath();
ctx.ellipse(bx + bw - 7, cy - bh * 0.52, 10, 6, -0.15, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff8c0';
ctx.beginPath();
ctx.ellipse(bx + bw - 8, cy - bh * 0.52, 6, 3.5, -0.15, 0, Math.PI * 2);
ctx.fill();
// Headlight beam
const beam = ctx.createLinearGradient(bx + bw, cy - bh * 0.55, bx + bw + 260, cy - 5);
beam.addColorStop(0, 'rgba(255,245,150,0.18)');
beam.addColorStop(1, 'rgba(255,245,150,0)');
ctx.fillStyle = beam;
ctx.beginPath();
ctx.moveTo(bx + bw - 4, cy - bh * 0.58);
ctx.lineTo(bx + bw + 260, cy - bh * 0.35);
ctx.lineTo(bx + bw + 260, cy - 2);
ctx.lineTo(bx + bw - 4, cy - bh * 0.46);
ctx.closePath();
ctx.fill();
// Tail light
ctx.fillStyle = '#dd0000';
ctx.beginPath();
ctx.ellipse(bx + 10, cy - bh * 0.52, 7, 5, 0.1, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,80,60,0.5)';
ctx.beginPath();
ctx.ellipse(bx + 9, cy - bh * 0.52, 4, 3, 0.1, 0, Math.PI * 2);
ctx.fill();
// Mirror
ctx.fillStyle = '#880e06';
ctx.beginPath();
ctx.moveTo(bx + bw * 0.82, cy - bh * 0.9);
ctx.lineTo(bx + bw * 0.9, cy - bh * 1.02);
ctx.lineTo(bx + bw * 0.96, cy - bh * 0.98);
ctx.lineTo(bx + bw * 0.86, cy - bh * 0.86);
ctx.closePath();
ctx.fill();
// Wheels
const fwx = bx + bw * 0.78, rwx = bx + bw * 0.2;
const wheelY = cy;
drawWheel(fwx, wheelY, wr);
drawWheel(rwx, wheelY, wr);
// Undercarriage
ctx.fillStyle = '#444';
ctx.fillRect(bx + 28, cy - 7, bw - 55, 7);
// Bumpers
ctx.fillStyle = '#6a0c04';
ctx.beginPath();
ctx.moveTo(bx + bw - 5, cy - bh * 0.4);
ctx.lineTo(bx + bw + 8, cy - bh * 0.3);
ctx.lineTo(bx + bw + 8, cy - wr * 0.3);
ctx.lineTo(bx + bw - 5, cy - wr * 0.3);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(bx + 18, cy - bh * 0.4);
ctx.lineTo(bx + 6, cy - bh * 0.3);
ctx.lineTo(bx + 6, cy - wr * 0.3);
ctx.lineTo(bx + 18, cy - wr * 0.3);
ctx.closePath();
ctx.fill();
}
function drawWheel(cx, cy, r) {
const angle = (frame * SPEED) / r;
// Tire
ctx.fillStyle = '#181818';
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fill();
// Tire wall highlight
ctx.strokeStyle = '#303030';
ctx.lineWidth = 4;
ctx.beginPath(); ctx.arc(cx, cy, r - 2, 0, Math.PI * 2); ctx.stroke();
// Rim
ctx.fillStyle = '#c8c8c8';
ctx.beginPath(); ctx.arc(cx, cy, r * 0.62, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#b0b0b0';
ctx.beginPath(); ctx.arc(cx, cy, r * 0.52, 0, Math.PI * 2); ctx.fill();
// Spokes
const nSpokes = 5;
ctx.lineWidth = 3.5;
for (let i = 0; i < nSpokes; i++) {
const a = angle + (i / nSpokes) * Math.PI * 2;
const ix = cx + Math.cos(a) * r * 0.52;
const iy = cy + Math.sin(a) * r * 0.52;
const grad = ctx.createLinearGradient(cx, cy, ix, iy);
grad.addColorStop(0, '#aaa');
grad.addColorStop(1, '#888');
ctx.strokeStyle = grad;
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(ix, iy); ctx.stroke();
}
// Lug nuts
for (let i = 0; i < nSpokes; i++) {
const a = angle + (i / nSpokes) * Math.PI * 2;
const nx = cx + Math.cos(a) * r * 0.37;
const ny = cy + Math.sin(a) * r * 0.37;
ctx.fillStyle = '#666';
ctx.beginPath(); ctx.arc(nx, ny, 3.5, 0, Math.PI * 2); ctx.fill();
}
// Center cap
ctx.fillStyle = '#888';
ctx.beginPath(); ctx.arc(cx, cy, r * 0.14, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#aaa';
ctx.beginPath(); ctx.arc(cx, cy, r * 0.08, 0, Math.PI * 2); ctx.fill();
}
function loop() {
ctx.clearRect(0, 0, W, H);
offset += SPEED;
frame++;
const bob = Math.sin(frame * 0.09) * 1.4 + Math.sin(frame * 0.15) * 0.7;
drawSky();
drawStars();
drawSun();
drawClouds();
drawMountains();
drawHillLayer();
drawBgTrees();
drawRoad();
drawLaneMarkings();
drawPoles();
drawFgTrees();
drawCar(bob);
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html><!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Driving Scene</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #090b14;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="scene"></canvas>
<script>
(() => {
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');
const DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
let W = 0;
let H = 0;
let roadY = 0;
let worldLength = 2400;
let props = [];
let last = performance.now();
const rand = (n) => {
const x = Math.sin(n * 999.123 + 7.77) * 43758.5453;
return x - Math.floor(x);
};
function resize() {
W = Math.max(1, window.innerWidth);
H = Math.max(1, window.innerHeight);
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
roadY = H * 0.78;
worldLength = Math.max(1800, Math.round(W * 2.2));
buildProps();
}
function buildProps() {
props = [];
for (let i = 0; i < 150; i++) {
const x = rand(i * 11.3) * worldLength;
const typeSeed = rand(i * 29.7);
const type = typeSeed < 0.38 ? 'tree' : typeSeed < 0.58 ? 'pole' : typeSeed < 0.8 ? 'bush' : 'sign';
props.push({
x,
type,
scale: 0.7 + rand(i * 5.1) * 1.5,
tint: rand(i * 8.9)
});
}
}
const repeat = (value, length) => ((value % length) + length) % length;
function skyGradient() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#08111f');
g.addColorStop(0.38, '#18263d');
g.addColorStop(0.68, '#4d4b72');
g.addColorStop(1, '#f2a15d');
return g;
}
function drawBackground(t) {
ctx.fillStyle = skyGradient();
ctx.fillRect(0, 0, W, H);
const sunX = W * 0.78;
const sunY = H * 0.25;
const glow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, H * 0.35);
glow.addColorStop(0, 'rgba(255,236,186,0.95)');
glow.addColorStop(0.18, 'rgba(255,192,120,0.4)');
glow.addColorStop(1, 'rgba(255,150,80,0)');
ctx.fillStyle = glow;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.globalAlpha = 0.4;
ctx.fillStyle = '#f7cda2';
for (let i = 0; i < 3; i++) {
const cx = W * (0.15 + i * 0.22) + Math.sin(t * 0.00005 + i) * 40;
const cy = H * (0.12 + i * 0.04);
const rw = W * (0.18 + i * 0.03);
const rh = H * 0.035;
ctx.beginPath();
ctx.ellipse(cx, cy, rw, rh, -0.08, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawMountains(t) {
const base = H * 0.64;
const layers = [
{ y: base + 42, h: 115, color: '#2c2f4c', speed: 0.02, scale: 0.65 },
{ y: base + 12, h: 150, color: '#3f456d', speed: 0.04, scale: 0.85 }
];
for (const layer of layers) {
ctx.fillStyle = layer.color;
const offset = repeat(t * 0.005 * layer.speed * worldLength, worldLength);
for (let x = -worldLength; x < W + worldLength; x += worldLength / 8) {
const px = x - offset * layer.scale;
const peak = 40 + rand((x + layer.h) * 0.01) * layer.h;
ctx.beginPath();
ctx.moveTo(px, layer.y);
ctx.quadraticCurveTo(px + worldLength * 0.12, layer.y - peak * 0.45, px + worldLength * 0.24, layer.y);
ctx.quadraticCurveTo(px + worldLength * 0.34, layer.y - peak, px + worldLength * 0.5, layer.y - peak * 0.1);
ctx.quadraticCurveTo(px + worldLength * 0.63, layer.y - peak * 0.55, px + worldLength * 0.82, layer.y);
ctx.lineTo(px + worldLength * 0.82, H);
ctx.lineTo(px, H);
ctx.closePath();
ctx.fill();
}
}
}
function drawRoadAndGround(t) {
const horizon = H * 0.67;
const roadTop = H * 0.77;
const ground = ctx.createLinearGradient(0, horizon, 0, H);
ground.addColorStop(0, '#3a3b33');
ground.addColorStop(0.45, '#2c2c28');
ground.addColorStop(1, '#151614');
ctx.fillStyle = ground;
ctx.fillRect(0, roadTop, W, H - roadTop);
ctx.fillStyle = '#22231e';
ctx.fillRect(0, roadTop - 14, W, 16);
ctx.fillStyle = '#4c4636';
ctx.fillRect(0, roadTop + 10, W, 24);
const laneOffset = repeat(t * 0.16 * worldLength, 220);
for (let x = -220; x < W + 220; x += 220) {
const px = x - laneOffset;
const w = 88;
const h = 7;
ctx.fillStyle = 'rgba(255,233,160,0.9)';
ctx.fillRect(px, roadTop + 19, w, h);
}
ctx.fillStyle = '#1e1d18';
ctx.fillRect(0, roadTop + 36, W, H - roadTop - 36);
const grassOffset = repeat(t * 0.28 * worldLength, 120);
for (let x = -120; x < W + 120; x += 120) {
const px = x - grassOffset;
const s = 0.7 + rand((x + 3) * 0.2) * 0.8;
ctx.strokeStyle = `rgba(${50 + s * 20}, ${90 + s * 60}, ${40 + s * 18}, 0.45)`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(px, roadTop + 40);
ctx.quadraticCurveTo(px + 18 * s, roadTop + 18 - 10 * s, px + 30 * s, roadTop + 34);
ctx.stroke();
}
}
function drawScenery(t) {
const roadTop = H * 0.77;
const layerSpeeds = {
tree: 0.36,
pole: 0.58,
bush: 0.8,
sign: 0.66
};
for (const item of props) {
const speed = layerSpeeds[item.type] || 0.5;
const px = repeat(item.x - t * 0.16 * speed * worldLength, worldLength);
const x = px - worldLength * 0.1;
const screenX = x;
const y = roadTop + 8;
const scale = item.scale;
if (item.type === 'tree') {
const trunkH = 34 * scale;
const trunkW = 8 * scale;
const crown = 38 * scale;
ctx.fillStyle = `rgba(68,43,28,0.95)`;
ctx.fillRect(screenX, y - trunkH, trunkW, trunkH);
ctx.fillStyle = `rgba(${28 + item.tint * 16}, ${72 + item.tint * 40}, ${34 + item.tint * 24}, 0.97)`;
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.ellipse(screenX + trunkW * 0.5 + (i - 1) * 14 * scale, y - trunkH - 10 * scale, crown * (0.55 + i * 0.1), crown * (0.42 + i * 0.1), 0, 0, Math.PI * 2);
ctx.fill();
}
} else if (item.type === 'pole') {
ctx.fillStyle = '#6f7080';
ctx.fillRect(screenX, y - 92 * scale, 6 * scale, 92 * scale);
ctx.strokeStyle = '#bbb8aa';
ctx.lineWidth = 3 * scale;
ctx.beginPath();
ctx.moveTo(screenX + 3 * scale, y - 88 * scale);
ctx.lineTo(screenX + 34 * scale, y - 78 * scale);
ctx.stroke();
} else if (item.type === 'sign') {
ctx.fillStyle = '#5f5f66';
ctx.fillRect(screenX, y - 70 * scale, 6 * scale, 70 * scale);
ctx.fillStyle = '#2b2f40';
ctx.fillRect(screenX - 10 * scale, y - 98 * scale, 52 * scale, 28 * scale);
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
ctx.lineWidth = 2;
ctx.strokeRect(screenX - 10 * scale, y - 98 * scale, 52 * scale, 28 * scale);
} else {
ctx.fillStyle = `rgba(${40 + item.tint * 20}, ${72 + item.tint * 36}, ${34 + item.tint * 12}, 0.9)`;
ctx.beginPath();
ctx.ellipse(screenX, y - 10 * scale, 18 * scale, 7 * scale, 0.2, 0, Math.PI * 2);
ctx.fill();
}
}
// Foreground streaks for motion depth.
const streakOffset = repeat(t * 0.35 * worldLength, 70);
ctx.strokeStyle = 'rgba(231,207,153,0.14)';
ctx.lineWidth = 3;
for (let x = -70; x < W + 70; x += 70) {
const px = x - streakOffset;
ctx.beginPath();
ctx.moveTo(px, roadTop + 68);
ctx.lineTo(px + 20, roadTop + 72);
ctx.stroke();
}
}
function drawWheel(x, y, r, angle, hubColor, tireColor) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillStyle = tireColor;
ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1d1d1f';
ctx.beginPath();
ctx.arc(0, 0, r * 0.78, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(220,220,225,0.82)';
ctx.lineWidth = Math.max(1.5, r * 0.12);
for (let i = 0; i < 6; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -r * 0.67);
ctx.stroke();
ctx.rotate(Math.PI / 3);
}
ctx.fillStyle = hubColor;
ctx.beginPath();
ctx.arc(0, 0, r * 0.28, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawCar(t) {
const speed = 0.0062;
const bob = Math.sin(t * 0.0028) * 2.5 + Math.sin(t * 0.007) * 0.9;
const pitch = Math.sin(t * 0.0032 + 1.5) * 0.02;
const wheelSpin = t * speed * 3.9;
const baseX = W * 0.31;
const baseY = H * 0.72 + bob;
const scale = Math.min(W, H) / 980;
const bodyW = 300 * scale;
const bodyH = 92 * scale;
const wheelR = 33 * scale;
const wheelY = baseY + 42 * scale;
const wheelFrontX = baseX + 180 * scale;
const wheelRearX = baseX + 76 * scale;
// shadow
ctx.save();
ctx.translate(baseX + 132 * scale, baseY + 56 * scale);
ctx.scale(1.25, 0.42);
const shadow = ctx.createRadialGradient(0, 0, 0, 0, 0, 110 * scale);
shadow.addColorStop(0, 'rgba(0,0,0,0.34)');
shadow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = shadow;
ctx.beginPath();
ctx.arc(0, 0, 110 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.save();
ctx.translate(baseX, baseY);
ctx.rotate(pitch);
// body
const bodyGrad = ctx.createLinearGradient(0, -bodyH * 0.6, 0, bodyH);
bodyGrad.addColorStop(0, '#f6d7a8');
bodyGrad.addColorStop(0.24, '#d98b4a');
bodyGrad.addColorStop(0.55, '#a54f24');
bodyGrad.addColorStop(1, '#6b2b17');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(18 * scale, -18 * scale, 40 * scale, -32 * scale);
ctx.quadraticCurveTo(74 * scale, -68 * scale, 140 * scale, -72 * scale);
ctx.quadraticCurveTo(202 * scale, -74 * scale, 246 * scale, -48 * scale);
ctx.quadraticCurveTo(288 * scale, -24 * scale, 300 * scale, 0);
ctx.quadraticCurveTo(304 * scale, 14 * scale, 292 * scale, 22 * scale);
ctx.quadraticCurveTo(276 * scale, 31 * scale, 250 * scale, 32 * scale);
ctx.quadraticCurveTo(172 * scale, 36 * scale, 92 * scale, 36 * scale);
ctx.quadraticCurveTo(24 * scale, 34 * scale, 0, 16 * scale);
ctx.closePath();
ctx.fill();
// lower skirt
ctx.fillStyle = 'rgba(34,21,20,0.9)';
ctx.beginPath();
ctx.roundRect(18 * scale, 20 * scale, 270 * scale, 18 * scale, 9 * scale);
ctx.fill();
// cabin
ctx.fillStyle = 'rgba(247,248,255,0.86)';
ctx.beginPath();
ctx.moveTo(75 * scale, -34 * scale);
ctx.quadraticCurveTo(92 * scale, -62 * scale, 126 * scale, -67 * scale);
ctx.lineTo(183 * scale, -67 * scale);
ctx.quadraticCurveTo(214 * scale, -66 * scale, 233 * scale, -41 * scale);
ctx.lineTo(237 * scale, -4 * scale);
ctx.lineTo(82 * scale, -4 * scale);
ctx.closePath();
ctx.fill();
// windows
ctx.fillStyle = 'rgba(28,44,66,0.78)';
ctx.beginPath();
ctx.moveTo(93 * scale, -32 * scale);
ctx.quadraticCurveTo(104 * scale, -55 * scale, 128 * scale, -58 * scale);
ctx.lineTo(175 * scale, -58 * scale);
ctx.quadraticCurveTo(200 * scale, -57 * scale, 213 * scale, -39 * scale);
ctx.lineTo(217 * scale, -10 * scale);
ctx.lineTo(93 * scale, -10 * scale);
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(233,245,255,0.28)';
ctx.fillRect(100 * scale, -47 * scale, 35 * scale, 5 * scale);
ctx.fillRect(141 * scale, -47 * scale, 56 * scale, 5 * scale);
// door line and highlights
ctx.strokeStyle = 'rgba(255,240,214,0.55)';
ctx.lineWidth = 1.2 * scale;
ctx.beginPath();
ctx.moveTo(167 * scale, -6 * scale);
ctx.lineTo(170 * scale, -55 * scale);
ctx.stroke();
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 2 * scale;
ctx.beginPath();
ctx.moveTo(48 * scale, -17 * scale);
ctx.quadraticCurveTo(120 * scale, -57 * scale, 212 * scale, -48 * scale);
ctx.stroke();
ctx.strokeStyle = 'rgba(255,246,228,0.16)';
ctx.lineWidth = 3 * scale;
ctx.beginPath();
ctx.moveTo(42 * scale, 2 * scale);
ctx.lineTo(250 * scale, -2 * scale);
ctx.stroke();
// mirrors and details
ctx.fillStyle = '#4b2a21';
ctx.fillRect(196 * scale, -16 * scale, 12 * scale, 7 * scale);
ctx.fillStyle = 'rgba(255,217,157,0.9)';
ctx.fillRect(18 * scale, 8 * scale, 12 * scale, 4 * scale);
ctx.fillRect(274 * scale, 6 * scale, 12 * scale, 4 * scale);
// wheel wells
ctx.fillStyle = '#2a1816';
ctx.beginPath();
ctx.arc(76 * scale, 36 * scale, 39 * scale, Math.PI, 0);
ctx.fill();
ctx.beginPath();
ctx.arc(180 * scale, 36 * scale, 39 * scale, Math.PI, 0);
ctx.fill();
// ground contact / suspension
ctx.strokeStyle = 'rgba(13,9,10,0.68)';
ctx.lineWidth = 6 * scale;
ctx.beginPath();
ctx.moveTo(56 * scale, 34 * scale);
ctx.lineTo(76 * scale, 38 * scale);
ctx.lineTo(181 * scale, 38 * scale);
ctx.lineTo(206 * scale, 34 * scale);
ctx.stroke();
// wheels
drawWheel(wheelRearX - baseX, wheelY - baseY, wheelR, wheelSpin, '#b8b7b0', '#171719');
drawWheel(wheelFrontX - baseX, wheelY - baseY, wheelR, wheelSpin * 1.03, '#b8b7b0', '#171719');
// headlight glow and taillight
const headGlow = ctx.createRadialGradient(290 * scale, 0, 0, 290 * scale, 0, 72 * scale);
headGlow.addColorStop(0, 'rgba(255,236,166,0.95)');
headGlow.addColorStop(1, 'rgba(255,236,166,0)');
ctx.fillStyle = headGlow;
ctx.beginPath();
ctx.arc(290 * scale, 2 * scale, 72 * scale, -0.2, 0.45);
ctx.fill();
ctx.fillStyle = 'rgba(255,103,80,0.92)';
ctx.fillRect(0, 8 * scale, 10 * scale, 8 * scale);
ctx.restore();
// motion blur streaks near wheels
ctx.strokeStyle = 'rgba(255,231,195,0.07)';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(baseX - 24, wheelY + 14 * scale);
ctx.lineTo(baseX - 110, wheelY + 12 * scale);
ctx.stroke();
}
function draw(t) {
ctx.clearRect(0, 0, W, H);
drawBackground(t);
drawMountains(t);
drawRoadAndGround(t);
drawScenery(t);
drawCar(t);
// subtle atmospheric vignette
const vignette = ctx.createRadialGradient(W * 0.5, H * 0.5, H * 0.2, W * 0.5, H * 0.5, Math.max(W, H) * 0.75);
vignette.addColorStop(0, 'rgba(0,0,0,0)');
vignette.addColorStop(1, 'rgba(0,0,0,0.24)');
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, W, H);
}
function loop(now) {
const dt = Math.min(32, now - last);
last = now;
draw(now);
requestAnimationFrame(loop);
}
window.addEventListener('resize', resize, { passive: true });
if (typeof CanvasRenderingContext2D !== 'undefined' && CanvasRenderingContext2D.prototype.roundRect === undefined) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
const radius = Array.isArray(r) ? r : [r, r, r, r];
this.beginPath();
this.moveTo(x + radius[0], y);
this.lineTo(x + w - radius[1], y);
this.quadraticCurveTo(x + w, y, x + w, y + radius[1]);
this.lineTo(x + w, y + h - radius[2]);
this.quadraticCurveTo(x + w, y + h, x + w - radius[2], y + h);
this.lineTo(x + radius[3], y + h);
this.quadraticCurveTo(x, y + h, x, y + h - radius[3]);
this.lineTo(x, y + radius[0]);
this.quadraticCurveTo(x, y, x + radius[0], y);
this.closePath();
};
}
resize();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Driving at Dusk</title>
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #0b1220;
}
body {
font-family: Arial, sans-serif;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="scene" aria-label="Animated side view of a car driving through a scrolling landscape"></canvas>
<script>
(() => {
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');
const DPR = () => Math.min(window.devicePixelRatio || 1, 2);
let width = 0;
let height = 0;
let horizon = 0;
let roadTop = 0;
let roadHeight = 0;
let centerY = 0;
const state = {
speed: 260,
start: performance.now()
};
function resize() {
const dpr = DPR();
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
horizon = height * 0.48;
roadTop = height * 0.72;
roadHeight = height - roadTop;
centerY = roadTop + roadHeight * 0.28;
}
function hash(n) {
const x = Math.sin(n * 127.1 + 311.7) * 43758.5453123;
return x - Math.floor(x);
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function mod(value, size) {
return ((value % size) + size) % size;
}
function drawSky(time) {
const sky = ctx.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, '#12203a');
sky.addColorStop(0.24, '#30466c');
sky.addColorStop(0.52, '#ff9f6e');
sky.addColorStop(0.7, '#f5c784');
sky.addColorStop(1, '#f0d6a8');
ctx.fillStyle = sky;
ctx.fillRect(0, 0, width, height);
const sunX = width * 0.78;
const sunY = horizon - height * 0.15;
const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, height * 0.35);
sunGlow.addColorStop(0, 'rgba(255, 243, 205, 0.95)');
sunGlow.addColorStop(0.25, 'rgba(255, 188, 126, 0.45)');
sunGlow.addColorStop(0.7, 'rgba(255, 136, 82, 0.12)');
sunGlow.addColorStop(1, 'rgba(255, 136, 82, 0)');
ctx.fillStyle = sunGlow;
ctx.fillRect(0, 0, width, height);
const haze = ctx.createLinearGradient(0, horizon - height * 0.06, 0, roadTop);
haze.addColorStop(0, 'rgba(255, 216, 177, 0.25)');
haze.addColorStop(1, 'rgba(255, 216, 177, 0)');
ctx.fillStyle = haze;
ctx.fillRect(0, horizon - height * 0.06, width, roadTop - horizon + height * 0.06);
drawCloudBand(time);
}
function drawCloudBand(time) {
const spacing = 260;
const offset = mod(time * 3, spacing);
for (let i = -2; i < Math.ceil(width / spacing) + 3; i++) {
const idx = i + 100;
const x = i * spacing - offset;
const y = height * (0.14 + hash(idx) * 0.16);
const w = 130 + hash(idx + 1) * 120;
const h = 34 + hash(idx + 2) * 24;
ctx.fillStyle = 'rgba(255,255,255,0.11)';
blob(x, y, w, h, 5);
ctx.fillStyle = 'rgba(255,220,198,0.07)';
blob(x + 16, y + 6, w * 0.8, h * 0.7, 4);
}
}
function blob(x, y, w, h, lobes) {
ctx.beginPath();
for (let i = 0; i < lobes; i++) {
const px = x + (i / Math.max(1, lobes - 1)) * w;
const py = y + Math.sin(i * 0.9) * h * 0.05;
const r = lerp(h * 0.35, h * 0.6, i / Math.max(1, lobes - 1));
ctx.moveTo(px + r, py);
ctx.arc(px, py, r, 0, Math.PI * 2);
}
ctx.fill();
}
function drawMountainLayer(time, baseY, amplitude, colorA, colorB, speed, segment, roughness, alpha) {
const offset = mod(time * speed, segment);
const start = -segment;
const end = width + segment * 2;
const grad = ctx.createLinearGradient(0, baseY - amplitude * 1.2, 0, baseY + amplitude * 0.6);
grad.addColorStop(0, colorA);
grad.addColorStop(1, colorB);
ctx.fillStyle = grad;
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.moveTo(start, height);
for (let x = start; x <= end; x += segment / 3) {
const worldX = x + offset;
const section = Math.floor(worldX / segment);
const local = mod(worldX, segment) / segment;
const peak = hash(section * 5 + 1) * 0.9 + 0.1;
const valley = hash(section * 5 + 2) * 0.7;
const sharp = Math.pow(Math.sin(local * Math.PI), lerp(0.8, 1.8, peak));
const profile = sharp * (0.65 + peak * 0.75) + Math.sin(local * Math.PI * 2) * 0.08 * valley;
const y = baseY - profile * amplitude * roughness;
ctx.lineTo(x, y);
}
ctx.lineTo(end, height);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 1;
}
function drawGroundLayers(time) {
drawMountainLayer(time, horizon + height * 0.035, height * 0.19, '#6f5c7e', '#4b425f', 10, 220, 1.1, 0.55);
drawMountainLayer(time, horizon + height * 0.07, height * 0.12, '#8b6c6f', '#64515b', 18, 170, 1.05, 0.7);
drawMountainLayer(time, horizon + height * 0.11, height * 0.08, '#93755d', '#755b4d', 26, 130, 0.95, 0.9);
const fieldGrad = ctx.createLinearGradient(0, horizon, 0, roadTop);
fieldGrad.addColorStop(0, '#806652');
fieldGrad.addColorStop(0.45, '#5b6b46');
fieldGrad.addColorStop(1, '#36422f');
ctx.fillStyle = fieldGrad;
ctx.fillRect(0, horizon, width, roadTop - horizon);
const fieldGlow = ctx.createLinearGradient(0, horizon, 0, roadTop);
fieldGlow.addColorStop(0, 'rgba(255,183,120,0.18)');
fieldGlow.addColorStop(1, 'rgba(255,183,120,0)');
ctx.fillStyle = fieldGlow;
ctx.fillRect(0, horizon, width, roadTop - horizon);
drawShrubs(time);
drawTrees(time);
drawPoles(time);
}
function drawShrubs(time) {
const spacing = 72;
const offset = mod(time * 120, spacing);
for (let i = -2; i < Math.ceil(width / spacing) + 4; i++) {
const idx = i + 2000;
const x = i * spacing - offset;
const y = roadTop - 8 + Math.sin(idx) * 1.5;
const r = 10 + hash(idx) * 12;
ctx.fillStyle = `rgba(${Math.round(35 + hash(idx + 1) * 25)}, ${Math.round(55 + hash(idx + 2) * 45)}, ${Math.round(28 + hash(idx + 3) * 18)}, 0.88)`;
ctx.beginPath();
ctx.ellipse(x, y, r * 1.8, r, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawTrees(time) {
const spacing = 180;
const offset = mod(time * 75, spacing);
for (let i = -2; i < Math.ceil(width / spacing) + 4; i++) {
const idx = i + 1000;
const x = i * spacing - offset;
const h = 70 + hash(idx) * 70;
const lean = (hash(idx + 1) - 0.5) * 8;
const trunkW = 4 + hash(idx + 2) * 4;
const baseY = roadTop - 10;
ctx.strokeStyle = 'rgba(51, 34, 24, 0.8)';
ctx.lineWidth = trunkW;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x, baseY);
ctx.lineTo(x + lean, baseY - h * 0.72);
ctx.stroke();
const crownY = baseY - h * 0.78;
const crownR = 22 + hash(idx + 3) * 22;
const crown = ctx.createRadialGradient(x, crownY - crownR * 0.3, 2, x, crownY, crownR * 1.6);
crown.addColorStop(0, 'rgba(104, 140, 84, 0.95)');
crown.addColorStop(0.55, 'rgba(61, 92, 51, 0.95)');
crown.addColorStop(1, 'rgba(34, 53, 33, 0)');
ctx.fillStyle = crown;
ctx.beginPath();
ctx.arc(x, crownY, crownR, 0, Math.PI * 2);
ctx.arc(x - crownR * 0.8, crownY + 5, crownR * 0.76, 0, Math.PI * 2);
ctx.arc(x + crownR * 0.78, crownY + 8, crownR * 0.68, 0, Math.PI * 2);
ctx.fill();
}
}
function drawPoles(time) {
const spacing = 230;
const offset = mod(time * 150, spacing);
const poleTop = roadTop - 120;
ctx.strokeStyle = 'rgba(58, 46, 37, 0.7)';
ctx.lineWidth = 3;
for (let i = -2; i < Math.ceil(width / spacing) + 4; i++) {
const idx = i + 3000;
const x = i * spacing - offset;
const h = 76 + hash(idx) * 34;
ctx.beginPath();
ctx.moveTo(x, roadTop - 3);
ctx.lineTo(x, roadTop - h);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x - 8, roadTop - h + 8);
ctx.lineTo(x + 10, roadTop - h + 8);
ctx.stroke();
}
ctx.strokeStyle = 'rgba(95, 76, 62, 0.32)';
ctx.lineWidth = 1.5;
ctx.beginPath();
for (let i = -2; i < Math.ceil(width / spacing) + 4; i++) {
const x = i * spacing - offset;
const y = poleTop + Math.sin((i + time * 0.25) * 0.7) * 6;
if (i === -2) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
}
function drawRoad(time) {
const shoulderGrad = ctx.createLinearGradient(0, roadTop - 20, 0, roadTop + roadHeight);
shoulderGrad.addColorStop(0, '#4f4238');
shoulderGrad.addColorStop(0.2, '#2e2b2d');
shoulderGrad.addColorStop(1, '#15181d');
ctx.fillStyle = shoulderGrad;
ctx.fillRect(0, roadTop - 10, width, roadHeight + 10);
const asphalt = ctx.createLinearGradient(0, roadTop, 0, height);
asphalt.addColorStop(0, '#39383e');
asphalt.addColorStop(0.55, '#23252b');
asphalt.addColorStop(1, '#12151a');
ctx.fillStyle = asphalt;
ctx.fillRect(0, roadTop + 18, width, roadHeight - 18);
ctx.fillStyle = 'rgba(255, 177, 108, 0.08)';
ctx.fillRect(0, roadTop + 18, width, 16);
const stripeY = roadTop + roadHeight * 0.53;
const stripeSpacing = 150;
const stripeOffset = mod(time * state.speed * 0.9, stripeSpacing);
for (let i = -2; i < Math.ceil(width / stripeSpacing) + 4; i++) {
const x = i * stripeSpacing - stripeOffset;
ctx.fillStyle = 'rgba(251, 229, 166, 0.9)';
ctx.fillRect(x, stripeY, 74, 6);
}
const shimmer = ctx.createLinearGradient(0, roadTop, 0, height);
shimmer.addColorStop(0, 'rgba(255,255,255,0.05)');
shimmer.addColorStop(0.5, 'rgba(255,255,255,0)');
shimmer.addColorStop(1, 'rgba(0,0,0,0.18)');
ctx.fillStyle = shimmer;
ctx.fillRect(0, roadTop, width, roadHeight);
}
function roundRectPath(x, y, w, h, r) {
const radius = Math.min(r, w * 0.5, h * 0.5);
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + w - radius, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
ctx.lineTo(x + w, y + h - radius);
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
ctx.lineTo(x + radius, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
function drawWheel(x, y, r, rotation) {
const tire = ctx.createRadialGradient(x - r * 0.18, y - r * 0.18, r * 0.2, x, y, r);
tire.addColorStop(0, '#4f5157');
tire.addColorStop(0.65, '#191b20');
tire.addColorStop(1, '#060708');
ctx.fillStyle = tire;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, r * 0.82, 0, Math.PI * 2);
ctx.stroke();
const rim = ctx.createRadialGradient(x - r * 0.12, y - r * 0.18, r * 0.1, x, y, r * 0.64);
rim.addColorStop(0, '#e4ebf2');
rim.addColorStop(0.4, '#b8c1ca');
rim.addColorStop(1, '#5c6470');
ctx.fillStyle = rim;
ctx.beginPath();
ctx.arc(x, y, r * 0.58, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.strokeStyle = 'rgba(65, 70, 78, 0.9)';
ctx.lineWidth = 3;
for (let i = 0; i < 6; i++) {
ctx.rotate(Math.PI / 3);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(r * 0.45, 0);
ctx.stroke();
}
ctx.restore();
ctx.fillStyle = '#4b525c';
ctx.beginPath();
ctx.arc(x, y, r * 0.14, 0, Math.PI * 2);
ctx.fill();
}
function drawCar(time) {
const carWidth = Math.min(width * 0.34, 420);
const carHeight = carWidth * 0.25;
const carX = width * 0.31;
const wheelR = carWidth * 0.11;
const bounce = Math.sin(time * 7.2) * 1.7 + Math.sin(time * 2.3) * 0.9;
const pitch = Math.sin(time * 3.6) * 0.004;
const baseY = roadTop + 14 + bounce;
const wheelY = baseY + carHeight * 0.62;
const frontWheelX = carX + carWidth * 0.72;
const rearWheelX = carX + carWidth * 0.23;
const bodyY = wheelY - wheelR * 1.55;
const rotation = -time * state.speed / (wheelR * 6.2);
const shadow = ctx.createRadialGradient(carX + carWidth * 0.48, wheelY + wheelR * 1.05, wheelR * 0.35, carX + carWidth * 0.48, wheelY + wheelR * 1.05, carWidth * 0.45);
shadow.addColorStop(0, 'rgba(0,0,0,0.28)');
shadow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = shadow;
ctx.beginPath();
ctx.ellipse(carX + carWidth * 0.48, wheelY + wheelR * 1.02, carWidth * 0.34, wheelR * 0.7, 0, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.translate(carX + carWidth * 0.5, bodyY + carHeight * 0.55);
ctx.rotate(pitch);
ctx.translate(-(carX + carWidth * 0.5), -(bodyY + carHeight * 0.55));
ctx.fillStyle = '#0f1116';
ctx.beginPath();
ctx.arc(rearWheelX, wheelY, wheelR * 1.28, Math.PI, 0, false);
ctx.arc(frontWheelX, wheelY, wheelR * 1.28, Math.PI, 0, false);
ctx.rect(rearWheelX, wheelY - wheelR * 1.28, frontWheelX - rearWheelX, wheelR * 1.28);
ctx.fill();
const bodyGrad = ctx.createLinearGradient(carX, bodyY, carX, bodyY + carHeight);
bodyGrad.addColorStop(0, '#e95b54');
bodyGrad.addColorStop(0.45, '#b7282d');
bodyGrad.addColorStop(0.75, '#83171c');
bodyGrad.addColorStop(1, '#5e1114');
ctx.fillStyle = bodyGrad;
roundRectPath(carX + carWidth * 0.03, bodyY + carHeight * 0.16, carWidth * 0.94, carHeight * 0.56, carHeight * 0.18);
ctx.fill();
ctx.beginPath();
ctx.moveTo(carX + carWidth * 0.22, bodyY + carHeight * 0.22);
ctx.lineTo(carX + carWidth * 0.35, bodyY - carHeight * 0.12);
ctx.lineTo(carX + carWidth * 0.64, bodyY - carHeight * 0.14);
ctx.lineTo(carX + carWidth * 0.82, bodyY + carHeight * 0.2);
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.14)';
roundRectPath(carX + carWidth * 0.08, bodyY + carHeight * 0.18, carWidth * 0.78, carHeight * 0.08, carHeight * 0.05);
ctx.fill();
const winGrad = ctx.createLinearGradient(carX, bodyY - carHeight * 0.12, carX, bodyY + carHeight * 0.35);
winGrad.addColorStop(0, 'rgba(219, 240, 255, 0.95)');
winGrad.addColorStop(0.55, 'rgba(105, 146, 178, 0.88)');
winGrad.addColorStop(1, 'rgba(39, 60, 80, 0.9)');
ctx.fillStyle = winGrad;
ctx.beginPath();
ctx.moveTo(carX + carWidth * 0.28, bodyY + carHeight * 0.18);
ctx.lineTo(carX + carWidth * 0.37, bodyY - carHeight * 0.06);
ctx.lineTo(carX + carWidth * 0.51, bodyY - carHeight * 0.08);
ctx.lineTo(carX + carWidth * 0.51, bodyY + carHeight * 0.18);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(carX + carWidth * 0.54, bodyY + carHeight * 0.18);
ctx.lineTo(carX + carWidth * 0.56, bodyY - carHeight * 0.08);
ctx.lineTo(carX + carWidth * 0.72, bodyY - carHeight * 0.02);
ctx.lineTo(carX + carWidth * 0.78, bodyY + carHeight * 0.18);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = 'rgba(28, 16, 18, 0.65)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(carX + carWidth * 0.53, bodyY - carHeight * 0.08);
ctx.lineTo(carX + carWidth * 0.53, bodyY + carHeight * 0.23);
ctx.stroke();
ctx.strokeStyle = 'rgba(0,0,0,0.22)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(carX + carWidth * 0.48, bodyY + carHeight * 0.2);
ctx.lineTo(carX + carWidth * 0.48, bodyY + carHeight * 0.65);
ctx.stroke();
ctx.fillStyle = '#c9ced6';
roundRectPath(carX + carWidth * 0.19, bodyY + carHeight * 0.38, carWidth * 0.04, carHeight * 0.08, 3);
ctx.fill();
const headlight = ctx.createRadialGradient(carX + carWidth * 0.96, bodyY + carHeight * 0.46, 0, carX + carWidth * 0.96, bodyY + carHeight * 0.46, carWidth * 0.22);
headlight.addColorStop(0, 'rgba(255, 245, 201, 0.95)');
headlight.addColorStop(0.28, 'rgba(255, 223, 154, 0.45)');
headlight.addColorStop(1, 'rgba(255, 223, 154, 0)');
ctx.fillStyle = headlight;
ctx.beginPath();
ctx.ellipse(carX + carWidth * 1.02, bodyY + carHeight * 0.48, carWidth * 0.22, carHeight * 0.23, -0.1, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffe6b0';
roundRectPath(carX + carWidth * 0.93, bodyY + carHeight * 0.4, carWidth * 0.04, carHeight * 0.12, 2);
ctx.fill();
ctx.fillStyle = '#ffb2a3';
roundRectPath(carX + carWidth * 0.05, bodyY + carHeight * 0.42, carWidth * 0.025, carHeight * 0.11, 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.12)';
ctx.beginPath();
ctx.moveTo(carX + carWidth * 0.17, bodyY + carHeight * 0.28);
ctx.quadraticCurveTo(carX + carWidth * 0.45, bodyY + carHeight * 0.12, carX + carWidth * 0.84, bodyY + carHeight * 0.25);
ctx.lineTo(carX + carWidth * 0.81, bodyY + carHeight * 0.3);
ctx.quadraticCurveTo(carX + carWidth * 0.48, bodyY + carHeight * 0.18, carX + carWidth * 0.2, bodyY + carHeight * 0.34);
ctx.closePath();
ctx.fill();
drawWheel(rearWheelX, wheelY, wheelR, rotation);
drawWheel(frontWheelX, wheelY, wheelR, rotation);
ctx.restore();
}
function drawForegroundBlur(time) {
const spacing = 90;
const offset = mod(time * 320, spacing);
for (let i = -3; i < Math.ceil(width / spacing) + 6; i++) {
const idx = i + 5000;
const x = i * spacing - offset;
const h = 14 + hash(idx) * 22;
ctx.fillStyle = `rgba(${45 + Math.round(hash(idx + 1) * 30)}, ${55 + Math.round(hash(idx + 2) * 40)}, ${25 + Math.round(hash(idx + 3) * 20)}, 0.85)`;
ctx.beginPath();
ctx.ellipse(x, roadTop + 6, 22 + hash(idx + 4) * 12, h, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function render(now) {
const time = (now - state.start) / 1000;
ctx.clearRect(0, 0, width, height);
drawSky(time);
drawGroundLayers(time);
drawRoad(time);
drawCar(time);
drawForegroundBlur(time);
requestAnimationFrame(render);
}
window.addEventListener('resize', resize);
resize();
requestAnimationFrame(render);
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sunset Drive</title>
<style>
html,body{margin:0;padding:0;height:100%;background:#000;overflow:hidden;font-family:sans-serif;}
canvas{display:block;width:100vw;height:100vh;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(() => {
const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
let W=0,H=0,DPR=Math.max(1,window.devicePixelRatio||1);
function resize(){
W = cv.clientWidth = window.innerWidth;
H = cv.clientHeight = window.innerHeight;
cv.width = W*DPR; cv.height = H*DPR;
ctx.setTransform(DPR,0,0,DPR,0,0);
buildSky();
}
window.addEventListener('resize',resize);
// ---------- Sky gradient (sunset/dusk) ----------
let skyGrad=null, sunGlow=null;
function buildSky(){
skyGrad = ctx.createLinearGradient(0,0,0,H);
skyGrad.addColorStop(0.00,'#1a1340');
skyGrad.addColorStop(0.25,'#3b1f6b');
skyGrad.addColorStop(0.50,'#a13a6b');
skyGrad.addColorStop(0.72,'#f08a4b');
skyGrad.addColorStop(0.85,'#fbc56a');
skyGrad.addColorStop(1.00,'#fde2a2');
sunGlow = ctx.createRadialGradient(W*0.72,H*0.62,5,W*0.72,H*0.62,Math.max(W,H)*0.55);
sunGlow.addColorStop(0,'rgba(255,220,150,0.9)');
sunGlow.addColorStop(0.15,'rgba(255,170,100,0.45)');
sunGlow.addColorStop(0.45,'rgba(255,120,80,0.15)');
sunGlow.addColorStop(1,'rgba(255,120,80,0)');
}
// ---------- Deterministic pseudo-random ----------
function rand(seed){
let s = seed*9301+49297;
return function(){
s = (s*9301+49297)%233280;
return s/233280;
};
}
// ---------- Layers ----------
// Each layer: speed multiplier, draw(offset) function with seamless tiling
let stars = [];
function buildStars(){
stars = [];
const r = rand(7);
for(let i=0;i<140;i++){
stars.push({x:r()*1, y:r()*0.45, s:r()*1.4+0.2, a:r()*0.6+0.2});
}
}
buildStars();
function drawStars(){
for(const s of stars){
const y = s.y*H;
// fade stars near sun horizon area
const alpha = s.a * Math.max(0, 1 - y/(H*0.55));
if(alpha<=0) continue;
ctx.fillStyle = `rgba(255,240,210,${alpha})`;
ctx.fillRect(s.x*W, y, s.s, s.s);
}
}
// Mountains (far) - drawn as tiled silhouettes
function makeMountainPath(seed, segWidth, amp, baseY){
const r = rand(seed);
const pts=[];
let x=0;
while(x < segWidth+200){
const step = 60+r()*120;
pts.push({x, y: baseY - (r()*amp + amp*0.2)});
x += step;
}
return pts;
}
const farMountains = makeMountainPath(11, 2400, 180, 0);
const midMountains = makeMountainPath(23, 2000, 120, 0);
const hills = makeMountainPath(37, 1600, 70, 0);
function drawMountainLayer(pts, segWidth, offset, baseY, color, glow){
// tile across screen
const totalW = segWidth;
let startX = -((offset % totalW) + totalW) % totalW;
ctx.fillStyle = color;
for(let tile=0; tile<3; tile++){
const ox = startX + tile*totalW;
ctx.beginPath();
ctx.moveTo(ox, baseY);
for(const p of pts){
ctx.lineTo(ox + p.x, baseY + p.y);
}
ctx.lineTo(ox + segWidth + 200, baseY);
ctx.lineTo(ox + segWidth + 200, H);
ctx.lineTo(ox, H);
ctx.closePath();
ctx.fill();
}
if(glow){
// rim light on top edge facing sun
ctx.save();
ctx.globalCompositeOperation='lighter';
ctx.strokeStyle = glow;
ctx.lineWidth = 1.2;
for(let tile=0; tile<3; tile++){
const ox = startX + tile*totalW;
ctx.beginPath();
ctx.moveTo(ox, baseY);
for(const p of pts){
ctx.lineTo(ox + p.x, baseY + p.y);
}
ctx.lineTo(ox + segWidth + 200, baseY);
ctx.stroke();
}
ctx.restore();
}
}
// Trees
function drawTree(x, groundY, scale, color){
ctx.fillStyle = color;
// trunk
ctx.fillRect(x-2*scale, groundY-22*scale, 4*scale, 22*scale);
// foliage: layered triangles
ctx.beginPath();
ctx.moveTo(x, groundY-60*scale);
ctx.lineTo(x-18*scale, groundY-25*scale);
ctx.lineTo(x+18*scale, groundY-25*scale);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x, groundY-48*scale);
ctx.lineTo(x-22*scale, groundY-15*scale);
ctx.lineTo(x+22*scale, groundY-15*scale);
ctx.closePath();
ctx.fill();
}
function drawBush(x, groundY, scale, color){
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(x, groundY-6*scale, 16*scale, 8*scale, 0, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x-10*scale, groundY-4*scale, 10*scale, 6*scale, 0, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x+10*scale, groundY-4*scale, 10*scale, 6*scale, 0, 0, Math.PI*2);
ctx.fill();
}
// Pre-generate roadside element positions per tile
function genRoadsideTile(seed, tileWidth){
const r = rand(seed);
const items=[];
let x=20;
while(x < tileWidth-20){
const t = r();
let kind='tree';
if(t<0.55) kind='tree';
else if(t<0.8) kind='bush';
else if(t<0.92) kind='pole';
else kind='sign';
items.push({x, kind, scale: 0.9+r()*0.6, jitterY: (r()-0.5)*4});
x += 50 + r()*70;
}
return items;
}
const farTreeTile = genRoadsideTile(101, 1800);
const midTreeTile = genRoadsideTile(202, 1400);
const nearTile = genRoadsideTile(303, 900);
function drawPole(x, groundY, scale){
ctx.fillStyle = '#1a1320';
ctx.fillRect(x-1.5*scale, groundY-70*scale, 3*scale, 70*scale);
// crossbar
ctx.fillRect(x-12*scale, groundY-66*scale, 24*scale, 2*scale);
// wires sag (simple)
ctx.strokeStyle = 'rgba(20,15,25,0.7)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x-12*scale, groundY-64*scale);
ctx.quadraticCurveTo(x, groundY-58*scale, x+12*scale, groundY-64*scale);
ctx.stroke();
}
function drawSign(x, groundY, scale){
ctx.fillStyle = '#221820';
ctx.fillRect(x-1*scale, groundY-30*scale, 2*scale, 30*scale);
ctx.fillStyle = '#e8d28a';
ctx.fillRect(x-10*scale, groundY-40*scale, 20*scale, 12*scale);
ctx.fillStyle = '#3a2a18';
ctx.fillRect(x-7*scale, groundY-36*scale, 14*scale, 2*scale);
ctx.fillRect(x-7*scale, groundY-32*scale, 10*scale, 2*scale);
}
function drawRoadsideTile(items, ox, groundY, treeColor, depth){
for(const it of items){
const x = ox + it.x;
const gy = groundY + it.jitterY;
if(it.kind==='tree') drawTree(x, gy, it.scale*depth, treeColor);
else if(it.kind==='bush') drawBush(x, gy, it.scale*depth, treeColor);
else if(it.kind==='pole') drawPole(x, gy, it.scale*depth);
else if(it.kind==='sign') drawSign(x, gy, it.scale*depth);
}
}
function drawTiledRoadside(items, tileWidth, offset, groundY, color, depth){
let startX = -((offset % tileWidth) + tileWidth) % tileWidth;
for(let tile=0; tile<3; tile++){
drawRoadsideTile(items, startX + tile*tileWidth, groundY, color, depth);
}
}
// Ground stripes / road dashes
function drawRoad(offset, roadTop, roadBottom){
// road body
const grd = ctx.createLinearGradient(0, roadTop, 0, roadBottom);
grd.addColorStop(0,'#2a2530');
grd.addColorStop(1,'#13101a');
ctx.fillStyle = grd;
ctx.fillRect(0, roadTop, W, roadBottom-roadTop);
// edge highlight (sunset reflection on asphalt)
const edge = ctx.createLinearGradient(0, roadTop, 0, roadTop+8);
edge.addColorStop(0,'rgba(255,170,100,0.35)');
edge.addColorStop(1,'rgba(255,170,100,0)');
ctx.fillStyle = edge;
ctx.fillRect(0, roadTop, W, 8);
// center dashed line
const dashLen = 40, gap = 30;
const period = dashLen+gap;
let startX = -((offset % period) + period) % period;
const lineY = (roadTop+roadBottom)/2 - 2;
ctx.fillStyle = 'rgba(255,220,150,0.85)';
for(let x=startX; x<W+period; x+=period){
ctx.fillRect(x, lineY, dashLen, 4);
}
// side curbs
ctx.fillStyle = 'rgba(255,255,255,0.06)';
ctx.fillRect(0, roadTop+2, W, 1);
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.fillRect(0, roadBottom-2, W, 2);
}
// Ground (foreground grass / dirt)
function drawGround(offset, groundY, roadTop){
// band between groundY (top of grass) and roadTop
const grd = ctx.createLinearGradient(0, groundY-10, 0, roadTop);
grd.addColorStop(0,'#3b2a3a');
grd.addColorStop(1,'#1f1722');
ctx.fillStyle = grd;
ctx.fillRect(0, groundY-10, W, roadTop-(groundY-10));
// moving grass tufts on near ground
const period = 18;
let startX = -((offset % period) + period) % period;
ctx.fillStyle = 'rgba(60,40,55,0.9)';
for(let x=startX; x<W+period; x+=period){
ctx.fillRect(x, groundY-3, 4, 3);
ctx.fillRect(x+8, groundY-2, 3, 2);
}
}
// ---------- Car ----------
function drawCar(cx, cy, bob, wheelAngle){
ctx.save();
ctx.translate(cx, cy + bob);
const scale = Math.min(W,H)/620;
ctx.scale(scale, scale);
// shadow
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.beginPath();
ctx.ellipse(0, 70, 150, 14, 0, 0, Math.PI*2);
ctx.fill();
// body main shape
// lower body
ctx.fillStyle = '#b53a2a';
roundedBody();
// window/cabin
ctx.fillStyle = '#0e1a2a';
ctx.beginPath();
ctx.moveTo(-58, 0);
ctx.lineTo(-30, -36);
ctx.lineTo(40, -36);
ctx.lineTo(70, 0);
ctx.closePath();
ctx.fill();
// window reflections (sunset)
const wr = ctx.createLinearGradient(-58,-36,70,0);
wr.addColorStop(0,'rgba(255,180,120,0.4)');
wr.addColorStop(0.5,'rgba(255,210,150,0.15)');
wr.addColorStop(1,'rgba(255,180,120,0.35)');
ctx.fillStyle = wr;
ctx.beginPath();
ctx.moveTo(-58, 0);
ctx.lineTo(-30, -36);
ctx.lineTo(40, -36);
ctx.lineTo(70, 0);
ctx.closePath();
ctx.fill();
// window divider (B-pillar)
ctx.fillStyle = '#7a2418';
ctx.fillRect(2, -36, 3, 36);
// body trim line
ctx.strokeStyle = 'rgba(0,0,0,0.35)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-110, 12);
ctx.lineTo(120, 12);
ctx.stroke();
// headlight glow
ctx.save();
ctx.globalCompositeOperation='lighter';
const hl = ctx.createRadialGradient(122, 8, 1, 122, 8, 60);
hl.addColorStop(0,'rgba(255,240,200,0.9)');
hl.addColorStop(1,'rgba(255,240,200,0)');
ctx.fillStyle = hl;
ctx.fillRect(80, -30, 120, 80);
ctx.restore();
// headlight bulb
ctx.fillStyle = '#fff5d6';
ctx.beginPath();
ctx.ellipse(120, 8, 5, 4, 0, 0, Math.PI*2);
ctx.fill();
// taillight
ctx.fillStyle = '#ff3a2a';
ctx.fillRect(-122, 4, 5, 8);
ctx.save();
ctx.globalCompositeOperation='lighter';
const tl = ctx.createRadialGradient(-120, 8, 1, -120, 8, 25);
tl.addColorStop(0,'rgba(255,60,40,0.7)');
tl.addColorStop(1,'rgba(255,60,40,0)');
ctx.fillStyle = tl;
ctx.fillRect(-150, -10, 50, 40);
ctx.restore();
// door handle
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(-30, -4, 14, 3);
ctx.fillRect(20, -4, 14, 3);
// wheels
drawWheel(-70, 50, wheelAngle);
drawWheel(70, 50, wheelAngle);
// wheel arches (over wheels for depth)
ctx.fillStyle = '#3a1a14';
ctx.beginPath();
ctx.arc(-70, 50, 28, Math.PI, 0, false);
ctx.lineTo(-42, 50);
ctx.arc(-70, 50, 22, 0, Math.PI, true);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.arc(70, 50, 28, Math.PI, 0, false);
ctx.lineTo(98, 50);
ctx.arc(70, 50, 22, 0, Math.PI, true);
ctx.closePath();
ctx.fill();
ctx.restore();
function roundedBody(){
ctx.beginPath();
ctx.moveTo(-130, 20);
ctx.quadraticCurveTo(-135, 0, -110, -4);
ctx.lineTo(-70, -6);
ctx.quadraticCurveTo(-60, -8, -52, -14);
ctx.lineTo(-30, -36);
ctx.quadraticCurveTo(-20, -42, -8, -42);
ctx.lineTo(35, -42);
ctx.quadraticCurveTo(48, -42, 56, -32);
ctx.lineTo(72, -10);
ctx.quadraticCurveTo(80, -4, 92, -2);
ctx.lineTo(120, 2);
ctx.quadraticCurveTo(135, 6, 132, 24);
ctx.lineTo(128, 50);
ctx.lineTo(98, 50);
ctx.arc(70, 50, 28, 0, Math.PI, true);
ctx.lineTo(-42, 50);
ctx.arc(-70, 50, 28, 0, Math.PI, true);
ctx.lineTo(-128, 50);
ctx.closePath();
ctx.fill();
// body shading
const bg = ctx.createLinearGradient(0, -42, 0, 50);
bg.addColorStop(0,'rgba(255,255,255,0.18)');
bg.addColorStop(0.5,'rgba(255,255,255,0)');
bg.addColorStop(1,'rgba(0,0,0,0.35)');
ctx.fillStyle = bg;
ctx.fill();
// sunset rim light on top
const rim = ctx.createLinearGradient(0,-42,0,-20);
rim.addColorStop(0,'rgba(255,180,120,0.55)');
rim.addColorStop(1,'rgba(255,180,120,0)');
ctx.fillStyle = rim;
ctx.fill();
}
}
function drawWheel(x, y, ang){
ctx.save();
ctx.translate(x,y);
// tire
ctx.fillStyle = '#0a0810';
ctx.beginPath();
ctx.arc(0,0,22,0,Math.PI*2);
ctx.fill();
// sidewall
ctx.fillStyle = '#181420';
ctx.beginPath();
ctx.arc(0,0,18,0,Math.PI*2);
ctx.fill();
// hub
ctx.fillStyle = '#cfcfd6';
ctx.beginPath();
ctx.arc(0,0,11,0,Math.PI*2);
ctx.fill();
// spokes
ctx.rotate(ang);
ctx.strokeStyle = '#2a2630';
ctx.lineWidth = 3;
for(let i=0;i<5;i++){
ctx.rotate(Math.PI*2/5);
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(10,0);
ctx.stroke();
}
// center cap
ctx.fillStyle = '#5a5560';
ctx.beginPath();
ctx.arc(0,0,3,0,Math.PI*2);
ctx.fill();
ctx.restore();
}
// ---------- Animation loop ----------
let last = performance.now();
let dist = 0; // road distance traveled
const baseSpeed = 260; // px/sec at the road layer
function frame(now){
const dt = Math.min(0.05,(now-last)/1000);
last = now;
dist += baseSpeed * dt;
render(now/1000);
requestAnimationFrame(frame);
}
function render(t){
// Layout
const horizon = H*0.58;
const groundY = H*0.78; // top of near grass
const roadTop = H*0.82;
const roadBottom = H*0.95;
// Sky
ctx.fillStyle = skyGrad;
ctx.fillRect(0,0,W,H);
// Stars
drawStars();
// Sun glow
ctx.fillStyle = sunGlow;
ctx.fillRect(0,0,W,H);
// Sun disk
ctx.save();
ctx.globalCompositeOperation='lighter';
const sx = W*0.72, sy = H*0.62;
const sun = ctx.createRadialGradient(sx,sy,2,sx,sy,80);
sun.addColorStop(0,'rgba(255,250,220,1)');
sun.addColorStop(0.4,'rgba(255,210,150,0.9)');
sun.addColorStop(1,'rgba(255,160,100,0)');
ctx.fillStyle = sun;
ctx.beginPath();
ctx.arc(sx,sy,80,0,Math.PI*2);
ctx.fill();
ctx.restore();
// Distant clouds (slow)
drawClouds(dist*0.04, horizon);
// Far mountains (very slow)
drawMountainLayer(farMountains, 2400, dist*0.08, horizon-10,
'rgba(40,25,55,0.85)', 'rgba(255,170,110,0.35)');
// Mid mountains
drawMountainLayer(midMountains, 2000, dist*0.18, horizon+30,
'rgba(28,18,40,0.92)', 'rgba(255,150,100,0.3)');
// Hills
drawMountainLayer(hills, 1600, dist*0.32, horizon+80,
'#150f20', null);
// Far roadside (small trees behind hills band)
drawTiledRoadside(farTreeTile, 1800, dist*0.45, horizon+95, '#0f0a18', 0.55);
// Mid roadside trees
drawTiledRoadside(midTreeTile, 1400, dist*0.7, groundY-18, '#120c1c', 0.85);
// Ground band
drawGround(dist*1.0, groundY, roadTop);
// Near roadside (poles, signs, bushes) - fastest
drawTiledRoadside(nearTile, 900, dist*1.0, groundY+2, '#0a0612', 1.05);
// Road
drawRoad(dist*1.2, roadTop, roadBottom);
// Car
const carX = W*0.42;
const carY = roadTop - 6; // wheels sit on road
const bob = Math.sin(t*6.2)*0.8 + Math.sin(t*2.1)*0.4;
// wheel angular speed proportional to ground speed
// wheel radius ~22 in car-local; effective on screen depends on scale, but we just animate
const wheelAng = (dist*0.06) % (Math.PI*2);
drawCar(carX, carY-50, bob, wheelAng);
// foreground vignette
const vg = ctx.createRadialGradient(W/2, H/2, Math.min(W,H)*0.35, W/2, H/2, Math.max(W,H)*0.75);
vg.addColorStop(0,'rgba(0,0,0,0)');
vg.addColorStop(1,'rgba(0,0,0,0.55)');
ctx.fillStyle = vg;
ctx.fillRect(0,0,W,H);
// subtle motion blur lines on road edges
ctx.save();
ctx.globalAlpha = 0.15;
ctx.fillStyle = '#000';
for(let i=0;i<6;i++){
const y = roadBottom - i*2;
ctx.fillRect(0, y, W, 1);
}
ctx.restore();
}
// Clouds
const cloudSeed = rand(55);
const clouds = [];
for(let i=0;i<8;i++){
clouds.push({
x: cloudSeed()*1800,
y: cloudSeed()*0.25 + 0.1,
w: 80+cloudSeed()*140,
h: 12+cloudSeed()*10,
a: 0.25+cloudSeed()*0.35
});
}
function drawClouds(offset, horizon){
const tileW = 1800;
let startX = -((offset % tileW) + tileW) % tileW;
for(let tile=0; tile<3; tile++){
const ox = startX + tile*tileW;
for(const c of clouds){
const x = ox + c.x;
const y = c.y * H;
ctx.fillStyle = `rgba(255,200,160,${c.a})`;
ctx.beginPath();
ctx.ellipse(x, y, c.w*0.5, c.h, 0, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x+c.w*0.2, y+2, c.w*0.35, c.h*0.8, 0, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x-c.w*0.25, y+2, c.w*0.3, c.h*0.7, 0, 0, Math.PI*2);
ctx.fill();
}
}
}
resize();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>