1. Mouse move listener now on document (not just hero section) 2. Removed hover effect on outer cards, kept only for center กำไร card 3. Bigger text: card-tag 20px, card-desc 16px 4. Hero overflow visible on desktop (cards can extend left) 5. Hero overflow clip on mobile (normal containment)
319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
const root = document.documentElement;
|
|
const body = document.body;
|
|
const nav = document.querySelector('[data-nav]');
|
|
const navToggle = document.querySelector('[data-nav-toggle]');
|
|
const navMenu = document.querySelector('[data-nav-menu]');
|
|
const serviceToggle = document.querySelector('[data-service-toggle]');
|
|
const serviceWrap = serviceToggle?.closest('.nav-service');
|
|
const panel = document.querySelector('[data-lead-panel]');
|
|
const backdrop = document.querySelector('[data-panel-backdrop]');
|
|
const form = document.querySelector('[data-lead-form]');
|
|
const statusEl = document.querySelector('[data-form-status]');
|
|
const floatingCta = document.querySelector('[data-floating-cta]');
|
|
const openButtons = document.querySelectorAll('[data-open-lead]');
|
|
const closeButtons = document.querySelectorAll('[data-close-lead]');
|
|
|
|
const diagnosisByProblem = {
|
|
ads_not_worth_it: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากการดูข้อมูลแอด กลุ่มเป้าหมาย และคุณภาพลูกค้าที่ทักเข้ามาก่อน',
|
|
wrong_leads: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากการดูข้อมูลแอด กลุ่มเป้าหมาย และคุณภาพลูกค้าที่ทักเข้ามาก่อน',
|
|
website_no_leads: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากการดูเว็บ เส้นทางลูกค้า และจุดที่ควรชวนให้ติดต่อก่อน',
|
|
slow_or_error_work: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากขั้นตอนทำงานซ้ำ จุดที่ช้า และจุดที่ผิดพลาดบ่อยก่อน',
|
|
ai_not_sure: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากงานจริงของทีม แล้วค่อยเลือกจุดที่ AI ช่วยได้อย่างเหมาะสม',
|
|
not_sure: 'ได้รับโจทย์แล้ว เราจะเริ่มจากการทำความเข้าใจธุรกิจและข้อมูลที่มีอยู่ก่อน',
|
|
};
|
|
|
|
function setStatus(message, tone = '') {
|
|
if (!statusEl) return;
|
|
statusEl.textContent = message;
|
|
statusEl.dataset.tone = tone;
|
|
}
|
|
|
|
function openPanel() {
|
|
body.classList.add('panel-open');
|
|
panel?.setAttribute('aria-hidden', 'false');
|
|
window.setTimeout(() => {
|
|
panel?.querySelector('input, textarea, button')?.focus();
|
|
}, 80);
|
|
}
|
|
|
|
function closePanel() {
|
|
body.classList.remove('panel-open');
|
|
panel?.setAttribute('aria-hidden', 'true');
|
|
}
|
|
|
|
openButtons.forEach((button) => button.addEventListener('click', openPanel));
|
|
closeButtons.forEach((button) => button.addEventListener('click', closePanel));
|
|
backdrop?.addEventListener('click', closePanel);
|
|
|
|
window.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
closePanel();
|
|
serviceWrap?.classList.remove('is-open');
|
|
}
|
|
});
|
|
|
|
navToggle?.addEventListener('click', () => {
|
|
const isOpen = nav?.classList.toggle('is-open');
|
|
navToggle.setAttribute('aria-expanded', String(Boolean(isOpen)));
|
|
});
|
|
|
|
serviceToggle?.addEventListener('click', () => {
|
|
const isOpen = serviceWrap?.classList.toggle('is-open');
|
|
serviceToggle.setAttribute('aria-expanded', String(Boolean(isOpen)));
|
|
});
|
|
|
|
navMenu?.addEventListener('click', (event) => {
|
|
const target = event.target;
|
|
if (target instanceof HTMLAnchorElement) {
|
|
nav?.classList.remove('is-open');
|
|
navToggle?.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
|
|
let ticking = false;
|
|
|
|
function updateScrollState() {
|
|
const scrolled = window.scrollY > 28;
|
|
const pastHero = window.scrollY > window.innerHeight * 0.62;
|
|
const heroProgress = Math.min(Math.max(window.scrollY / Math.max(window.innerHeight, 1), 0), 1);
|
|
const maxScroll = Math.max(document.documentElement.scrollHeight - window.innerHeight, 1);
|
|
const pageProgress = Math.min(Math.max(window.scrollY / maxScroll, 0), 1);
|
|
const navProbeY = (nav?.getBoundingClientRect().bottom || 80) + 24;
|
|
const sceneAtNav = document.elementFromPoint(window.innerWidth / 2, navProbeY)?.closest('[data-scene]');
|
|
const isOverDark = sceneAtNav?.dataset.scene === 'dark';
|
|
nav?.classList.toggle('is-scrolled', scrolled);
|
|
nav?.classList.toggle('is-over-dark', isOverDark);
|
|
floatingCta?.classList.toggle('is-visible', pastHero);
|
|
root.style.setProperty('--scroll', heroProgress.toFixed(4));
|
|
root.style.setProperty('--page-scroll', pageProgress.toFixed(4));
|
|
root.style.setProperty('--scroll-y', `${Math.round(window.scrollY)}px`);
|
|
ticking = false;
|
|
}
|
|
|
|
function requestScrollUpdate() {
|
|
if (!ticking) {
|
|
ticking = true;
|
|
window.requestAnimationFrame(updateScrollState);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('scroll', requestScrollUpdate, { passive: true });
|
|
window.addEventListener('resize', requestScrollUpdate, { passive: true });
|
|
updateScrollState();
|
|
|
|
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
let pointerFrame = 0;
|
|
window.addEventListener('pointermove', (event) => {
|
|
if (pointerFrame) return;
|
|
const { clientX, clientY } = event;
|
|
pointerFrame = window.requestAnimationFrame(() => {
|
|
const mx = (clientX / window.innerWidth - 0.5) * 2;
|
|
const my = (clientY / window.innerHeight - 0.5) * 2;
|
|
root.style.setProperty('--mx', mx.toFixed(4));
|
|
root.style.setProperty('--my', my.toFixed(4));
|
|
pointerFrame = 0;
|
|
});
|
|
}, { passive: true });
|
|
}
|
|
|
|
form?.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
|
|
const data = new FormData(form);
|
|
const name = String(data.get('name') || '').trim();
|
|
const phone = String(data.get('phone') || '').trim();
|
|
const email = String(data.get('email') || '').trim();
|
|
const problems = data.getAll('problems').map(String);
|
|
const message = String(data.get('message') || '').trim();
|
|
const endpoint = panel?.dataset.endpoint || '';
|
|
|
|
if (!name) {
|
|
setStatus('กรุณาใส่ชื่อก่อนส่ง', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!phone && !email) {
|
|
setStatus('ใส่เบอร์โทรหรืออีเมลอย่างใดอย่างหนึ่งก็ได้', 'error');
|
|
return;
|
|
}
|
|
|
|
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
setStatus('รูปแบบอีเมลไม่ถูกต้อง', 'error');
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
name,
|
|
phone,
|
|
email,
|
|
problems,
|
|
message,
|
|
pageUrl: window.location.href,
|
|
userAgent: navigator.userAgent,
|
|
website: String(data.get('website') || ''),
|
|
};
|
|
|
|
const diagnosis = problems.map((key) => diagnosisByProblem[key]).find(Boolean)
|
|
|| 'ได้รับโจทย์แล้ว เราจะเริ่มจากการทำความเข้าใจธุรกิจและข้อมูลที่มีอยู่ก่อน';
|
|
|
|
if (!endpoint) {
|
|
setStatus('ฟอร์มพร้อมแล้ว เหลือใส่ Google Apps Script Web App URL ก่อนใช้งานจริง', 'error');
|
|
return;
|
|
}
|
|
|
|
setStatus('กำลังส่งโจทย์...', '');
|
|
|
|
try {
|
|
await fetch(endpoint, {
|
|
method: 'POST',
|
|
mode: 'no-cors',
|
|
headers: {
|
|
'Content-Type': 'text/plain;charset=utf-8',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
form.reset();
|
|
setStatus(`ได้รับโจทย์แล้ว ${diagnosis} เราจะติดต่อกลับทางเบอร์หรืออีเมลที่ให้ไว้`, 'success');
|
|
} catch (error) {
|
|
setStatus('ส่งไม่สำเร็จ กรุณาลองใหม่อีกครั้ง', 'error');
|
|
}
|
|
});
|
|
|
|
// Neural Network Hero - True 3D with Dynamic Lines
|
|
const heroNeural = document.querySelector('.hero-neural');
|
|
const neuralScene = document.querySelector('.neural-scene');
|
|
const canvas = document.querySelector('.neural-canvas');
|
|
const ctx = canvas?.getContext('2d');
|
|
const nodes = document.querySelectorAll('.neural-node');
|
|
|
|
if (heroNeural && neuralScene && canvas && ctx && nodes.length > 0) {
|
|
// 3D rotation state
|
|
let targetRotateX = 0;
|
|
let targetRotateY = 0;
|
|
let currentRotateX = 0;
|
|
let currentRotateY = 0;
|
|
|
|
// Canvas setup
|
|
function resizeCanvas() {
|
|
const rect = heroNeural.getBoundingClientRect();
|
|
canvas.width = rect.width * window.devicePixelRatio;
|
|
canvas.height = rect.height * window.devicePixelRatio;
|
|
canvas.style.width = rect.width + 'px';
|
|
canvas.style.height = rect.height + 'px';
|
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
}
|
|
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
// Find intersection point on node border
|
|
function findBorderPoint(nodeRect, targetX, targetY) {
|
|
const cx = nodeRect.left + nodeRect.width / 2;
|
|
const cy = nodeRect.top + nodeRect.height / 2;
|
|
const hw = nodeRect.width / 2;
|
|
const hh = nodeRect.height / 2;
|
|
|
|
const dx = targetX - cx;
|
|
const dy = targetY - cy;
|
|
|
|
if (dx === 0 && dy === 0) return { x: cx, y: cy };
|
|
|
|
const absDx = Math.abs(dx);
|
|
const absDy = Math.abs(dy);
|
|
|
|
let scale;
|
|
if (absDx * hh > absDy * hw) {
|
|
scale = hw / absDx;
|
|
} else {
|
|
scale = hh / absDy;
|
|
}
|
|
|
|
return {
|
|
x: cx + dx * scale,
|
|
y: cy + dy * scale
|
|
};
|
|
}
|
|
|
|
// Draw connections
|
|
function drawConnections() {
|
|
const rect = heroNeural.getBoundingClientRect();
|
|
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
|
|
const centerNode = document.querySelector('[data-node="center"]');
|
|
const outerNodes = document.querySelectorAll('.neural-card');
|
|
|
|
if (!centerNode) return;
|
|
|
|
const centerRect = centerNode.getBoundingClientRect();
|
|
const centerX = centerRect.left + centerRect.width / 2 - rect.left;
|
|
const centerY = centerRect.top + centerRect.height / 2 - rect.top;
|
|
|
|
outerNodes.forEach(node => {
|
|
const nodeRect = node.getBoundingClientRect();
|
|
const nodeX = nodeRect.left + nodeRect.width / 2 - rect.left;
|
|
const nodeY = nodeRect.top + nodeRect.height / 2 - rect.top;
|
|
|
|
const startPt = findBorderPoint(
|
|
{ left: centerRect.left - rect.left, top: centerRect.top - rect.top,
|
|
width: centerRect.width, height: centerRect.height },
|
|
nodeX, nodeY
|
|
);
|
|
|
|
const endPt = findBorderPoint(
|
|
{ left: nodeRect.left - rect.left, top: nodeRect.top - rect.top,
|
|
width: nodeRect.width, height: nodeRect.height },
|
|
centerX, centerY
|
|
);
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(startPt.x, startPt.y);
|
|
ctx.lineTo(endPt.x, endPt.y);
|
|
ctx.strokeStyle = 'rgba(254, 212, 0, 0.5)';
|
|
ctx.lineWidth = 3;
|
|
ctx.lineCap = 'round';
|
|
ctx.stroke();
|
|
});
|
|
}
|
|
|
|
// Mouse move handler
|
|
document.addEventListener('mousemove', (e) => {
|
|
const rect = heroNeural.getBoundingClientRect();
|
|
const x = (e.clientX - rect.left) / rect.width;
|
|
const y = (e.clientY - rect.top) / rect.height;
|
|
|
|
targetRotateY = (x - 0.5) * 30;
|
|
targetRotateX = (y - 0.5) * -30;
|
|
});
|
|
|
|
document.addEventListener('mouseleave', () => {
|
|
targetRotateX = 0;
|
|
targetRotateY = 0;
|
|
});
|
|
|
|
// Animation loop
|
|
function animate() {
|
|
currentRotateX += (targetRotateX - currentRotateX) * 0.08;
|
|
currentRotateY += (targetRotateY - currentRotateY) * 0.08;
|
|
|
|
neuralScene.style.transform =
|
|
`rotateX(${currentRotateX}deg) rotateY(${currentRotateY}deg)`;
|
|
|
|
drawConnections();
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
animate();
|
|
|
|
// Mobile: Device orientation
|
|
if (window.DeviceOrientationEvent && 'ontouchstart' in window) {
|
|
window.addEventListener('deviceorientation', (e) => {
|
|
if (e.gamma !== null && e.beta !== null) {
|
|
targetRotateY = e.gamma * 0.3;
|
|
targetRotateX = (e.beta - 45) * 0.3;
|
|
}
|
|
});
|
|
}
|
|
}
|