ezsepa-website/hero-scene-2d.js
2025-11-02 20:30:44 +01:00

464 lines
13 KiB
JavaScript

/**
* Create a 2D scene matching the screenshot exactly
* Features: Person with phone, large device with QR code, security shield, floating coins and circles
*/
export function initHeroScene2D() {
const container = document.getElementById('hero-3d-container');
if (!container) return;
// Create canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas size
const resizeCanvas = () => {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
};
resizeCanvas();
container.appendChild(canvas);
// Animation variables
let time = 0;
// Color palette
const colors = {
orange: '#FF7A59',
darkGray: '#4a5568',
mediumGray: '#64748b',
lightGray: '#94a3b8',
veryLightGray: '#cbd5e1',
white: '#ffffff',
darkBlue: '#1e3a5f',
skin: '#ffdbac',
darkNavy: '#1e293b',
};
// Draw rounded rectangle helper
function roundRect(x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
// Draw shield shape
function drawShield(x, y, width, height) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width, y + height * 0.6);
ctx.quadraticCurveTo(x + width, y + height * 0.85, x + width / 2, y + height);
ctx.quadraticCurveTo(x, y + height * 0.85, x, y + height * 0.6);
ctx.lineTo(x, y);
ctx.closePath();
}
// Draw QR code pattern
function drawQRCode(x, y, size) {
const moduleSize = size / 19;
const pattern = [
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1],
];
ctx.fillStyle = colors.darkGray;
pattern.forEach((row, i) => {
row.forEach((cell, j) => {
if (cell === 1) {
ctx.fillRect(x + j * moduleSize, y + i * moduleSize, moduleSize, moduleSize);
}
});
});
}
// Draw person
function drawPerson(baseX, baseY, scale, bobOffset) {
ctx.save();
ctx.translate(baseX, baseY + bobOffset);
ctx.scale(scale, scale);
// Head
ctx.fillStyle = colors.skin;
ctx.beginPath();
ctx.arc(0, -60, 25, 0, Math.PI * 2);
ctx.fill();
// Hair
ctx.fillStyle = colors.darkNavy;
ctx.beginPath();
ctx.arc(-8, -68, 18, 0, Math.PI * 2);
ctx.arc(8, -68, 18, 0, Math.PI * 2);
ctx.arc(0, -75, 20, 0, Math.PI * 2);
ctx.fill();
// Face details
// Eyes
ctx.fillStyle = colors.darkNavy;
ctx.beginPath();
ctx.arc(-8, -62, 2, 0, Math.PI * 2);
ctx.arc(8, -62, 2, 0, Math.PI * 2);
ctx.fill();
// Smile
ctx.strokeStyle = colors.darkNavy;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, -58, 8, 0.2, Math.PI - 0.2);
ctx.stroke();
// Neck
ctx.fillStyle = colors.skin;
ctx.fillRect(-8, -35, 16, 15);
// White shirt/collar
ctx.fillStyle = '#f8f8f8';
ctx.beginPath();
ctx.moveTo(-15, -25);
ctx.lineTo(15, -25);
ctx.lineTo(12, 0);
ctx.lineTo(-12, 0);
ctx.closePath();
ctx.fill();
// Orange jacket/blazer
ctx.fillStyle = colors.orange;
ctx.beginPath();
// Left side
ctx.moveTo(-25, -20);
ctx.lineTo(-12, -25);
ctx.lineTo(-15, 40);
ctx.lineTo(-28, 40);
ctx.closePath();
ctx.fill();
ctx.beginPath();
// Right side
ctx.moveTo(25, -20);
ctx.lineTo(12, -25);
ctx.lineTo(15, 40);
ctx.lineTo(28, 40);
ctx.closePath();
ctx.fill();
// Dark skirt
ctx.fillStyle = colors.darkBlue;
ctx.beginPath();
ctx.moveTo(-18, 40);
ctx.lineTo(18, 40);
ctx.lineTo(25, 100);
ctx.lineTo(-25, 100);
ctx.closePath();
ctx.fill();
// Legs
ctx.fillStyle = colors.skin;
ctx.fillRect(-12, 100, 8, 30);
ctx.fillRect(4, 100, 8, 30);
// Orange shoes
ctx.fillStyle = colors.orange;
roundRect(-15, 128, 12, 8, 2);
ctx.fill();
roundRect(3, 128, 12, 8, 2);
ctx.fill();
// Arm holding phone
ctx.fillStyle = colors.orange;
ctx.beginPath();
ctx.moveTo(25, -10);
ctx.lineTo(45, 5);
ctx.lineTo(42, 12);
ctx.lineTo(22, -3);
ctx.closePath();
ctx.fill();
// Hand
ctx.fillStyle = colors.skin;
ctx.beginPath();
ctx.arc(45, 10, 6, 0, Math.PI * 2);
ctx.fill();
// Small phone in hand
ctx.fillStyle = colors.darkNavy;
roundRect(40, 0, 12, 20, 2);
ctx.fill();
ctx.fillStyle = '#4A90E2';
roundRect(41, 1, 10, 18, 1);
ctx.fill();
// Small phone screen detail (checkmark icon)
ctx.fillStyle = colors.white;
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.fillText('✓', 46, 12);
ctx.restore();
}
// Draw tablet/phone device
function drawDevice(x, y, width, height, tilt) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(tilt);
// Shadow/base
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
roundRect(10, 10, width, height, 20);
ctx.fill();
// Device frame (dark gray)
ctx.fillStyle = colors.darkGray;
roundRect(0, 0, width, height, 20);
ctx.fill();
// Screen (white)
ctx.fillStyle = colors.white;
roundRect(15, 15, width - 30, height - 30, 15);
ctx.fill();
// QR Code
const qrSize = Math.min(width, height) * 0.45;
const qrX = (width - qrSize) / 2;
const qrY = 80;
// Orange frame around QR
ctx.strokeStyle = colors.orange;
ctx.lineWidth = 8;
roundRect(qrX - 10, qrY - 10, qrSize + 20, qrSize + 20, 15);
ctx.stroke();
// QR Code
drawQRCode(qrX, qrY, qrSize);
// Price text
ctx.fillStyle = colors.mediumGray;
ctx.font = 'bold 60px Arial';
ctx.textAlign = 'center';
ctx.fillText('$200', width / 2, qrY + qrSize + 80);
// Some text lines below (simulating details)
ctx.fillStyle = colors.veryLightGray;
const lineY = qrY + qrSize + 130;
roundRect(40, lineY, width - 80, 12, 6);
ctx.fill();
roundRect(60, lineY + 25, width - 120, 12, 6);
ctx.fill();
roundRect(50, lineY + 50, width - 100, 12, 6);
ctx.fill();
ctx.restore();
}
// Draw security shield
function drawSecurityShield(x, y, size, bobOffset) {
ctx.save();
ctx.translate(x, y + bobOffset);
// Shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
drawShield(3, 3, size, size * 1.3);
ctx.fill();
// Shield body
ctx.fillStyle = colors.orange;
drawShield(0, 0, size, size * 1.3);
ctx.fill();
// White stroke
ctx.strokeStyle = colors.white;
ctx.lineWidth = 3;
drawShield(0, 0, size, size * 1.3);
ctx.stroke();
// Checkmark
ctx.strokeStyle = colors.white;
ctx.lineWidth = 8;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(size * 0.25, size * 0.5);
ctx.lineTo(size * 0.45, size * 0.7);
ctx.lineTo(size * 0.8, size * 0.25);
ctx.stroke();
ctx.restore();
}
// Draw coin
function drawCoin(x, y, radius, rotation, showDollar = false) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
// Shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
ctx.beginPath();
ctx.arc(2, 2, radius, 0, Math.PI * 2);
ctx.fill();
// Coin body
ctx.fillStyle = colors.orange;
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
// Inner circle
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, radius * 0.8, 0, Math.PI * 2);
ctx.stroke();
// Dollar sign
if (showDollar) {
ctx.fillStyle = colors.white;
ctx.font = `bold ${radius * 1.2}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('$', 0, 0);
}
ctx.restore();
}
// Draw floating circle
function drawFloatingCircle(x, y, radius, opacity) {
ctx.fillStyle = `rgba(148, 163, 184, ${opacity})`;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
// Main animation loop
function animate() {
requestAnimationFrame(animate);
time += 0.01;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const scale = Math.min(canvas.width, canvas.height) / 800;
// Draw floating circles (background)
const circles = [
{ x: 0.15, y: 0.15, radius: 20, phase: 0 },
{ x: 0.2, y: 0.35, radius: 15, phase: 1 },
{ x: 0.85, y: 0.2, radius: 25, phase: 2 },
{ x: 0.8, y: 0.7, radius: 18, phase: 3 },
{ x: 0.25, y: 0.75, radius: 12, phase: 4 },
{ x: 0.9, y: 0.5, radius: 22, phase: 2.5 },
];
circles.forEach(circle => {
const x = canvas.width * circle.x;
const y = canvas.height * circle.y + Math.sin(time * 0.8 + circle.phase) * 10;
const opacity = 0.25 + Math.sin(time * 0.5 + circle.phase) * 0.1;
drawFloatingCircle(x, y, circle.radius * scale, opacity);
});
// Draw person (left side)
const personX = centerX - 200 * scale;
const personY = centerY + 120 * scale;
const personBob = Math.sin(time * 1.1) * 3;
drawPerson(personX, personY, scale * 1.1, personBob);
// Draw main device (center-right)
const deviceWidth = 320 * scale;
const deviceHeight = 480 * scale;
const deviceX = centerX - 40 * scale;
const deviceY = centerY - 150 * scale;
const deviceTilt = Math.sin(time * 0.5) * 0.02;
const deviceBob = Math.sin(time * 0.8) * 5;
ctx.save();
ctx.translate(0, deviceBob);
drawDevice(deviceX, deviceY, deviceWidth, deviceHeight, deviceTilt);
ctx.restore();
// Draw security shield (top right of device)
const shieldX = deviceX + deviceWidth - 20 * scale;
const shieldY = deviceY - 20 * scale;
const shieldSize = 70 * scale;
const shieldBob = Math.sin(time * 1.2) * 4;
drawSecurityShield(shieldX, shieldY, shieldSize, shieldBob);
// Draw coins
// Large coin (bottom right)
const largeCoinX = deviceX + deviceWidth + 40 * scale;
const largeCoinY = deviceY + deviceHeight - 60 * scale;
const largeCoinBob = Math.sin(time * 1.5) * 6;
drawCoin(largeCoinX, largeCoinY + largeCoinBob, 50 * scale, time * 0.5, true);
// Small coins
const smallCoins = [
{ x: 0.65, y: 0.25, radius: 20, phase: 0 },
{ x: 0.75, y: 0.45, radius: 18, phase: 1.5 },
{ x: 0.7, y: 0.15, radius: 15, phase: 2.5 },
];
smallCoins.forEach(coin => {
const x = canvas.width * coin.x;
const y = canvas.height * coin.y + Math.sin(time * 1.3 + coin.phase) * 8;
const rotation = time * 0.8 + coin.phase;
drawCoin(x, y, coin.radius * scale, rotation, false);
});
// Draw floating circles (foreground)
const foregroundCircles = [
{ x: 0.12, y: 0.5, radius: 16, phase: 1.5 },
{ x: 0.88, y: 0.35, radius: 20, phase: 3 },
];
foregroundCircles.forEach(circle => {
const x = canvas.width * circle.x;
const y = canvas.height * circle.y + Math.sin(time * 0.9 + circle.phase) * 8;
const opacity = 0.3 + Math.sin(time * 0.6 + circle.phase) * 0.15;
drawFloatingCircle(x, y, circle.radius * scale, opacity);
});
}
// Start animation
animate();
// Handle resize
function onWindowResize() {
resizeCanvas();
}
window.addEventListener('resize', onWindowResize);
// Cleanup function
return () => {
window.removeEventListener('resize', onWindowResize);
container.removeChild(canvas);
};
}