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

437 lines
13 KiB
JavaScript

import * as THREE from 'three';
/**
* Create a 3D scene inspired by payment QR code illustration
* Features: Large phone/tablet with QR code, person figure, floating coins, and security shield
*/
export function initHeroScene() {
const container = document.getElementById('hero-3d-container');
if (!container) return;
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
50,
container.clientWidth / container.clientHeight,
0.1,
1000
);
camera.position.set(0, 0, 10);
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true
});
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
const orangeLight = new THREE.PointLight(0xFF7A59, 0.8, 100);
orangeLight.position.set(-3, 2, 3);
scene.add(orangeLight);
// Create large tablet/phone device (centerpiece)
const deviceGroup = new THREE.Group();
// Device frame
const frameGeometry = new THREE.BoxGeometry(3.5, 4.5, 0.15);
const frameMaterial = new THREE.MeshStandardMaterial({
color: 0x4a5568,
metalness: 0.7,
roughness: 0.3,
});
const frame = new THREE.Mesh(frameGeometry, frameMaterial);
deviceGroup.add(frame);
// Device screen (white background)
const screenGeometry = new THREE.BoxGeometry(3.3, 4.3, 0.1);
const screenMaterial = new THREE.MeshStandardMaterial({
color: 0xf8fafc,
metalness: 0.1,
roughness: 0.2,
});
const screen = new THREE.Mesh(screenGeometry, screenMaterial);
screen.position.z = 0.08;
deviceGroup.add(screen);
// QR Code on screen
const qrCodeTexture = createQRCodeTexture();
const qrGeometry = new THREE.PlaneGeometry(2, 2);
const qrMaterial = new THREE.MeshStandardMaterial({
map: qrCodeTexture,
transparent: false,
});
const qrCode = new THREE.Mesh(qrGeometry, qrMaterial);
qrCode.position.set(0, 0.3, 0.16);
deviceGroup.add(qrCode);
// Orange frame around QR code
const qrFrameShape = new THREE.Shape();
qrFrameShape.moveTo(-1.1, -1.1);
qrFrameShape.lineTo(1.1, -1.1);
qrFrameShape.lineTo(1.1, 1.1);
qrFrameShape.lineTo(-1.1, 1.1);
qrFrameShape.lineTo(-1.1, -1.1);
const qrFrameHole = new THREE.Path();
qrFrameHole.moveTo(-1, -1);
qrFrameHole.lineTo(1, -1);
qrFrameHole.lineTo(1, 1);
qrFrameHole.lineTo(-1, 1);
qrFrameHole.lineTo(-1, -1);
qrFrameShape.holes.push(qrFrameHole);
const qrFrameGeometry = new THREE.ShapeGeometry(qrFrameShape);
const qrFrameMaterial = new THREE.MeshStandardMaterial({
color: 0xFF7A59,
side: THREE.DoubleSide,
});
const qrFrame = new THREE.Mesh(qrFrameGeometry, qrFrameMaterial);
qrFrame.position.set(0, 0.3, 0.17);
deviceGroup.add(qrFrame);
// Price text "$200"
const priceGeometry = new THREE.PlaneGeometry(1.5, 0.5);
const priceTexture = createTextTexture('$200', '#64748b', 72);
const priceMaterial = new THREE.MeshBasicMaterial({
map: priceTexture,
transparent: true,
});
const priceText = new THREE.Mesh(priceGeometry, priceMaterial);
priceText.position.set(0, -1.5, 0.16);
deviceGroup.add(priceText);
deviceGroup.position.set(0.5, 0, 0);
deviceGroup.rotation.y = -0.1;
scene.add(deviceGroup);
// Create security shield
const shieldGroup = new THREE.Group();
const shieldShape = new THREE.Shape();
shieldShape.moveTo(0, 0.8);
shieldShape.lineTo(0.6, 0.6);
shieldShape.lineTo(0.6, -0.3);
shieldShape.quadraticCurveTo(0.6, -0.8, 0, -1);
shieldShape.quadraticCurveTo(-0.6, -0.8, -0.6, -0.3);
shieldShape.lineTo(-0.6, 0.6);
shieldShape.lineTo(0, 0.8);
const shieldExtrudeSettings = {
depth: 0.15,
bevelEnabled: true,
bevelThickness: 0.02,
bevelSize: 0.02,
bevelSegments: 3,
};
const shieldGeometry = new THREE.ExtrudeGeometry(shieldShape, shieldExtrudeSettings);
const shieldMaterial = new THREE.MeshStandardMaterial({
color: 0xFF7A59,
metalness: 0.3,
roughness: 0.4,
});
const shield = new THREE.Mesh(shieldGeometry, shieldMaterial);
shieldGroup.add(shield);
// Checkmark on shield
const checkShape = new THREE.Shape();
checkShape.moveTo(-0.2, 0);
checkShape.lineTo(-0.05, -0.2);
checkShape.lineTo(0.3, 0.3);
checkShape.lineTo(0.25, 0.35);
checkShape.lineTo(-0.05, -0.1);
checkShape.lineTo(-0.25, 0.05);
checkShape.lineTo(-0.2, 0);
const checkGeometry = new THREE.ShapeGeometry(checkShape);
const checkMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
});
const check = new THREE.Mesh(checkGeometry, checkMaterial);
check.position.z = 0.16;
shieldGroup.add(check);
shieldGroup.position.set(1.8, 2, 0.5);
shieldGroup.scale.set(1.2, 1.2, 1.2);
scene.add(shieldGroup);
// Create floating coins
const coins = [];
const coinGeometry = new THREE.CylinderGeometry(0.35, 0.35, 0.12, 32);
const coinMaterial = new THREE.MeshStandardMaterial({
color: 0xFF7A59,
metalness: 0.8,
roughness: 0.2,
});
// Large coin with dollar sign
const largeCoin = new THREE.Mesh(coinGeometry, coinMaterial);
largeCoin.position.set(2.8, -1.5, 1);
largeCoin.rotation.x = Math.PI / 2;
largeCoin.scale.set(1.3, 1.3, 1.3);
scene.add(largeCoin);
// Dollar sign on coin
const dollarTexture = createTextTexture('$', '#ffffff', 64);
const dollarGeometry = new THREE.CircleGeometry(0.4, 32);
const dollarMaterial = new THREE.MeshBasicMaterial({
map: dollarTexture,
transparent: true,
});
const dollarSign = new THREE.Mesh(dollarGeometry, dollarMaterial);
dollarSign.position.set(2.8, -1.5, 1.17);
scene.add(dollarSign);
// Smaller floating coins
const coinPositions = [
{ x: 2.2, y: 0.5, z: 1.5, scale: 0.7 },
{ x: 3.2, y: -0.8, z: 1.3, scale: 0.6 },
{ x: 2.5, y: 1.8, z: 1.2, scale: 0.5 },
];
coinPositions.forEach((pos, index) => {
const coin = new THREE.Mesh(coinGeometry, coinMaterial);
coin.position.set(pos.x, pos.y, pos.z);
coin.rotation.x = Math.PI / 2;
coin.scale.set(pos.scale, pos.scale, pos.scale);
coins.push({ mesh: coin, offset: index, baseY: pos.y });
scene.add(coin);
});
// Create simplified person figure (left side)
const personGroup = new THREE.Group();
// Head
const headGeometry = new THREE.SphereGeometry(0.3, 32, 32);
const skinMaterial = new THREE.MeshStandardMaterial({
color: 0xffdbac,
roughness: 0.6,
});
const head = new THREE.Mesh(headGeometry, skinMaterial);
head.position.y = 1.5;
personGroup.add(head);
// Body (torso)
const bodyGeometry = new THREE.CylinderGeometry(0.35, 0.4, 1.2, 8);
const jacketMaterial = new THREE.MeshStandardMaterial({
color: 0xFF7A59,
roughness: 0.5,
});
const body = new THREE.Mesh(bodyGeometry, jacketMaterial);
body.position.y = 0.5;
personGroup.add(body);
// Arm holding phone
const armGeometry = new THREE.CylinderGeometry(0.08, 0.1, 0.8, 8);
const arm = new THREE.Mesh(armGeometry, jacketMaterial);
arm.position.set(0.3, 0.8, 0.2);
arm.rotation.z = -0.5;
personGroup.add(arm);
// Small phone in hand
const handPhoneGeometry = new THREE.BoxGeometry(0.15, 0.25, 0.05);
const handPhoneMaterial = new THREE.MeshStandardMaterial({
color: 0x1e293b,
metalness: 0.6,
});
const handPhone = new THREE.Mesh(handPhoneGeometry, handPhoneMaterial);
handPhone.position.set(0.6, 1, 0.3);
handPhone.rotation.z = -0.3;
personGroup.add(handPhone);
// Legs
const legGeometry = new THREE.CylinderGeometry(0.12, 0.1, 1, 8);
const pantsMaterial = new THREE.MeshStandardMaterial({
color: 0x1e3a5f,
roughness: 0.6,
});
const leftLeg = new THREE.Mesh(legGeometry, pantsMaterial);
leftLeg.position.set(-0.15, -0.6, 0);
personGroup.add(leftLeg);
const rightLeg = new THREE.Mesh(legGeometry, pantsMaterial);
rightLeg.position.set(0.15, -0.6, 0);
personGroup.add(rightLeg);
personGroup.position.set(-3, -1.5, 1);
personGroup.scale.set(1.2, 1.2, 1.2);
scene.add(personGroup);
// Floating circles (decorative)
const circles = [];
const circleGeometry = new THREE.SphereGeometry(0.15, 16, 16);
const circleMaterial = new THREE.MeshStandardMaterial({
color: 0x94a3b8,
transparent: true,
opacity: 0.4,
});
const circlePositions = [
{ x: -2.5, y: 2.5, z: -1 },
{ x: -3.5, y: 1, z: 0.5 },
{ x: 3, y: 2.8, z: -0.5 },
{ x: -1, y: 3, z: -1 },
{ x: 3.5, y: 1.5, z: 0 },
];
circlePositions.forEach((pos, index) => {
const circle = new THREE.Mesh(circleGeometry, circleMaterial);
circle.position.set(pos.x, pos.y, pos.z);
const scale = 0.5 + Math.random() * 0.8;
circle.scale.set(scale, scale, scale);
circles.push({ mesh: circle, offset: index, baseY: pos.y });
scene.add(circle);
});
// Animation
let time = 0;
function animate() {
requestAnimationFrame(animate);
time += 0.01;
// Gentle rotation of device
deviceGroup.rotation.y = -0.1 + Math.sin(time * 0.5) * 0.05;
deviceGroup.position.y = Math.sin(time * 0.8) * 0.1;
// Shield bobbing
shieldGroup.position.y = 2 + Math.sin(time * 1.2) * 0.1;
shieldGroup.rotation.z = Math.sin(time * 0.6) * 0.05;
// Large coin rotation
largeCoin.rotation.y = time * 0.5;
largeCoin.position.y = -1.5 + Math.sin(time * 1.5) * 0.1;
// Small coins floating
coins.forEach((coinData) => {
const { mesh, offset, baseY } = coinData;
mesh.position.y = baseY + Math.sin(time * 1.3 + offset) * 0.15;
mesh.rotation.y = time * 0.8 + offset;
});
// Person slight movement
personGroup.rotation.y = Math.sin(time * 0.4) * 0.03;
personGroup.position.y = -1.5 + Math.sin(time * 1.1) * 0.05;
// Circles floating
circles.forEach((circleData) => {
const { mesh, offset, baseY } = circleData;
mesh.position.y = baseY + Math.sin(time * 0.9 + offset * 2) * 0.2;
mesh.material.opacity = 0.3 + Math.sin(time + offset) * 0.1;
});
renderer.render(scene, camera);
}
animate();
// Handle resize
function onWindowResize() {
if (!container) return;
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
}
window.addEventListener('resize', onWindowResize);
// Cleanup function
return () => {
window.removeEventListener('resize', onWindowResize);
renderer.dispose();
container.removeChild(renderer.domElement);
};
}
/**
* Create a procedural QR code texture
*/
function createQRCodeTexture() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
// White background
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, 512, 512);
// Draw QR code pattern
ctx.fillStyle = '#4a5568';
const moduleSize = 27;
// QR code pattern (simplified but realistic)
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],
];
const offset = (512 - (pattern.length * moduleSize)) / 2;
pattern.forEach((row, i) => {
row.forEach((cell, j) => {
if (cell === 1) {
ctx.fillRect(
offset + j * moduleSize,
offset + i * moduleSize,
moduleSize,
moduleSize
);
}
});
});
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
/**
* Create a text texture for labels
*/
function createTextTexture(text, color, fontSize) {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 256;
const ctx = canvas.getContext('2d');
// Clear background
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw text
ctx.fillStyle = color;
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}