464 lines
13 KiB
JavaScript
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);
|
|
};
|
|
}
|