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