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 = '#000000'; const moduleSize = 32; const modules = 16; // QR code pattern (simplified) 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; }