(function () { 'use strict'; const initialized = new WeakSet(); const defaults = { modelUrl: '', autoRotateSpeed: 0.002, cursorStrength: 0.45, floatingStrength: 0.06, modelScale: 2.6, cameraZ: 5 }; function toNumber(value, fallback) { const number = Number(value); return Number.isFinite(number) ? number : fallback; } function getSettings(element) { let parsed = {}; try { parsed = JSON.parse(element.getAttribute('data-hashtalk-3d') || '{}'); } catch (error) { parsed = {}; } return { modelUrl: parsed.modelUrl || defaults.modelUrl, autoRotateSpeed: toNumber(parsed.autoRotateSpeed, defaults.autoRotateSpeed), cursorStrength: toNumber(parsed.cursorStrength, defaults.cursorStrength), floatingStrength: toNumber(parsed.floatingStrength, defaults.floatingStrength), modelScale: toNumber(parsed.modelScale, defaults.modelScale), cameraZ: toNumber(parsed.cameraZ, defaults.cameraZ) }; } function init3D(element) { if (!element || initialized.has(element)) { return; } if (!window.THREE || !THREE.GLTFLoader) { console.warn('[Hashtalk 3D] Three.js or GLTFLoader is not loaded.'); return; } initialized.add(element); const settings = getSettings(element); if (!settings.modelUrl) { console.warn('[Hashtalk 3D] Model URL is empty.'); return; } const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 35, element.clientWidth / Math.max(element.clientHeight, 1), 0.1, 100 ); camera.position.set(0, 0, settings.cameraZ); const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); renderer.setSize(element.clientWidth, element.clientHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setClearColor(0x000000, 0); if ('outputEncoding' in renderer && THREE.sRGBEncoding) { renderer.outputEncoding = THREE.sRGBEncoding; } element.innerHTML = ''; element.appendChild(renderer.domElement); const ambientLight = new THREE.AmbientLight(0xffffff, 0.95); scene.add(ambientLight); const keyLight = new THREE.DirectionalLight(0xffffff, 1.45); keyLight.position.set(3, 4, 5); scene.add(keyLight); const fillLight = new THREE.DirectionalLight(0xff9a3d, 0.6); fillLight.position.set(-4, -2, 3); scene.add(fillLight); const group = new THREE.Group(); scene.add(group); const targetRotation = { x: 0, y: 0, z: 0.45 }; const loader = new THREE.GLTFLoader(); loader.load( settings.modelUrl, function (gltf) { const model = gltf.scene; model.traverse(function (child) { if (child.isMesh) { child.castShadow = false; child.receiveShadow = false; if (child.material) { child.material.needsUpdate = true; } } }); const box = new THREE.Box3().setFromObject(model); const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); const maxAxis = Math.max(size.x, size.y, size.z) || 1; const scale = settings.modelScale / maxAxis; model.scale.setScalar(scale); model.position.set( -center.x * scale, -center.y * scale, -center.z * scale ); group.add(model); element.classList.add('hashtalk-3d-element--loaded'); }, undefined, function (error) { console.error('[Hashtalk 3D] GLTF loading error:', error); element.classList.add('hashtalk-3d-element--error'); } ); function updateCursor(event) { const rect = element.getBoundingClientRect(); const x = ((event.clientX - rect.left) / Math.max(rect.width, 1)) * 2 - 1; const y = -(((event.clientY - rect.top) / Math.max(rect.height, 1)) * 2 - 1); targetRotation.y = x * -settings.cursorStrength; targetRotation.x = y * -settings.cursorStrength * 0.55; targetRotation.z = x * -settings.cursorStrength * 0.16; } function resetCursor() { targetRotation.x = 0; targetRotation.y = 0; targetRotation.z = 0.45; } element.addEventListener('mousemove', updateCursor); element.addEventListener('mouseleave', resetCursor); function resize() { const width = element.clientWidth || 1; const height = element.clientHeight || 1; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); } const resizeObserver = window.ResizeObserver ? new ResizeObserver(resize) : null; if (resizeObserver) { resizeObserver.observe(element); } else { window.addEventListener('resize', resize); } let frameId = null; function destroy() { if (frameId) { cancelAnimationFrame(frameId); } if (resizeObserver) { resizeObserver.disconnect(); } else { window.removeEventListener('resize', resize); } element.removeEventListener('mousemove', updateCursor); element.removeEventListener('mouseleave', resetCursor); renderer.dispose(); } function animate() { if (!document.body.contains(element)) { destroy(); return; } frameId = requestAnimationFrame(animate); const time = performance.now() * 0.001; group.rotation.y += settings.autoRotateSpeed; group.rotation.x += (targetRotation.x - group.rotation.x) * 0.055; group.rotation.z += (targetRotation.z - group.rotation.z) * 0.055; group.position.y = Math.sin(time * 1.35) * settings.floatingStrength; const pulse = 1 + Math.sin(time * 1.8) * settings.floatingStrength * 0.35; group.scale.set(pulse, pulse, pulse); renderer.render(scene, camera); } resize(); animate(); } function initAll(scope) { const root = scope && scope.querySelectorAll ? scope : document; root.querySelectorAll('.hashtalk-3d-element').forEach(init3D); } function addElementorHook() { if (!window.elementorFrontend || !window.elementorFrontend.hooks) { return; } window.elementorFrontend.hooks.addAction( 'frontend/element_ready/3d_element.default', function ($scope) { const scopeElement = $scope && $scope[0] ? $scope[0] : $scope; initAll(scopeElement); } ); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { initAll(document); }); } else { initAll(document); } if (window.jQuery) { window.jQuery(window).on('elementor/frontend/init', addElementorHook); } addElementorHook(); })();