260 lines
6.8 KiB
JavaScript
260 lines
6.8 KiB
JavaScript
(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();
|
|
})();
|