document.addEventListener('DOMContentLoaded',()=>{ if(!window.gsap||!window.ScrollTrigger)return; gsap.registerPlugin(ScrollTrigger,gsap.ScrollToPlugin); ScrollTrigger.getAll().forEach(st=>st.kill()); const wrappers=[...document.querySelectorAll('.container_st[data-step]')]; const circles=[...document.querySelectorAll('.step_circle[data-step]')]; const stepLists=[...document.querySelectorAll('.step_list[data-step]')]; const offsets=[{x:-220,y:-220},{x:240,y:-220},{x:-260,y:20},{x:260,y:20},{x:200,y:240},{x:-200,y:240}]; const CIRC=2*Math.PI*26; const MOBILE_MAX=767; const mobileQuery=window.matchMedia(`(max-width:${MOBILE_MAX}px)`); let currentStep=wrappers[0]?.dataset.step??'1'; let manualTargetStep=null; const isMobile = mobileQuery.matches; // КРИТИЧНО: Фиксируем высоту viewport один раз и не меняем const fixedViewportHeight = window.innerHeight; document.documentElement.style.setProperty('--fixed-vh', `${fixedViewportHeight}px`); // Глобальная переменная для отслеживания скролла let isScrolling = false; let scrollTimeout; // Отслеживаем скролл на мобильных if (isMobile) { window.addEventListener('scroll', () => { isScrolling = true; clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { isScrolling = false; }, 200); }, { passive: true }); } const scrollHeights={'1':1.5,'2':1.5,'3':2,'4':1.5}; const animationDuration=0.62; // Функция установки стилей с фиксацией высоты на мобильных const setWrapperStyles=(wrapper,step,height)=>{ wrapper.style.marginTop='0'; wrapper.style.marginBottom='0'; wrapper.style.paddingTop='0'; wrapper.style.paddingBottom='0'; wrapper.style.height=`${height}px`; wrapper.style.position='relative'; const sceneEl=wrapper.querySelector('.visual_step'); if(sceneEl){ // КРИТИЧНО: Используем фиксированную высоту на мобильных const sceneHeight = isMobile ? fixedViewportHeight : window.innerHeight; sceneEl.style.height=`${sceneHeight}px`; sceneEl.style.minHeight=`${sceneHeight}px`; sceneEl.style.maxHeight=`${sceneHeight}px`; sceneEl.style.position='sticky'; sceneEl.style.top='0'; sceneEl.style.width='100%'; sceneEl.style.zIndex='1'; // Предотвращаем изменение layout sceneEl.style.overflow = 'hidden'; } }; const syncAccordionVisibility=(nextStep=currentStep)=>{ const hideOthers=mobileQuery.matches; stepLists.forEach(list=>{ list.style.display=!hideOthers||list.dataset.step===nextStep?'':'none'; }); }; mobileQuery.addEventListener('change',()=>syncAccordionVisibility()); const typeTarget=document.querySelector('[data-typewriter="query"]'); let textSpan; if(typeTarget){ const wrapper=document.createElement('span'); wrapper.className='typewriter-wrapper'; textSpan=document.createElement('span'); textSpan.className='typewriter-text'; textSpan.textContent=typeTarget.textContent.trim(); wrapper.append(textSpan); typeTarget.textContent=''; typeTarget.append(wrapper); } const placeholder=typeTarget?.dataset.placeholder||''; const request=typeTarget?.dataset.request||''; const placeholderLen=placeholder.length; const requestLen=request.length; const renderTypewriter=(progress,animationDuration)=>{ if(!textSpan)return; const normalizedProgress=Math.min(1,progress/animationDuration); if(normalizedProgress<=0.2){ const ratio=gsap.utils.clamp(0,1,normalizedProgress/0.2); textSpan.textContent=placeholder.slice(0,Math.round(placeholderLen*(1-ratio))); typeTarget.style.opacity=0.45; textSpan.classList.add('is-typing'); }else if(normalizedProgress<1){ const ratio=gsap.utils.clamp(0,1,(normalizedProgress-0.2)/0.6); textSpan.textContent=request.slice(0,Math.round(requestLen*ratio)); typeTarget.style.opacity=1; textSpan.classList.add('is-typing'); }else{ textSpan.textContent=request; textSpan.classList.remove('is-typing'); } }; const setAccordion=step=>{ currentStep=step; syncAccordionVisibility(step); stepLists.forEach(list=>{ const match=list.dataset.step===step; list.classList.toggle('is-active',match); const wrap=list.querySelector('.desc_step-wrapper'); if(wrap)wrap.style.maxHeight=match?wrap.scrollHeight+'px':'0px'; }); circles.forEach(circle=>circle.classList.toggle('is-active',circle.dataset.step===step)); }; const setRing=(step,progress)=>{ const circle=circles.find(c=>c.dataset.step===step); const ring=circle?.querySelector('.ring-progress'); if(ring)ring.style.strokeDashoffset=CIRC*(1-progress); }; const scrollToStep=step=>{ const target=wrappers.find(w=>w.dataset.step===step); if(!target)return; manualTargetStep=step; const animationCenter=animationDuration/2; const rect=target.getBoundingClientRect(); const targetY=rect.top+window.pageYOffset+rect.height*animationCenter; const maxScroll=document.documentElement.scrollHeight-window.innerHeight; gsap.to(window,{ scrollTo:Math.min(targetY,maxScroll), duration:0.8, ease:'power2.out', onComplete:()=>{ if(manualTargetStep===step)manualTargetStep=null; } }); }; const handleStepInteraction=step=>{ if(!step)return; setAccordion(step); scrollToStep(step); }; stepLists.forEach(list=>list.addEventListener('click',()=>handleStepInteraction(list.dataset.step))); circles.forEach(circle=>circle.addEventListener('click',()=>handleStepInteraction(circle.dataset.step))); function animateStandard(sceneEl,progress,animationDuration){ const normalizedProgress=Math.min(1,progress/animationDuration); const layers=sceneEl.querySelectorAll('.visual-layer'); layers.forEach((layer,idx)=>{ const offset=offsets[idx]||{x:0,y:0}; let x,y,scale,opacity,blur; if(normalizedProgress<=0.5){ const t=normalizedProgress/0.5; x=gsap.utils.interpolate(offset.x,0,t); y=gsap.utils.interpolate(offset.y,0,t); scale=gsap.utils.interpolate(0.9,1.15,t); opacity=t; blur=gsap.utils.interpolate(12,0,t); }else{ const t=(normalizedProgress-0.5)/0.5; x=0; y=gsap.utils.interpolate(0,-30,t); scale=gsap.utils.interpolate(1.15,0.05,t); opacity=Math.max(1-t,0); blur=gsap.utils.interpolate(0,10,t); } layer.style.transform=`translate(${x}px, ${y}px) scale(${scale})`; layer.style.opacity=opacity; layer.style.filter=`blur(${blur}px)`; }); } function animateStep3(sceneEl,progress,animationDuration){ const normalizedProgress=Math.min(1,progress/animationDuration); const cards=sceneEl.querySelectorAll('.visual-card[data-index]'); const stage=sceneEl.querySelector('.timeline-stage'); const sceneRect=sceneEl.getBoundingClientRect(); const timelineBoxes=stage?[stage.querySelector('.box_timeline_first'),stage.querySelector('.box_timeline_second'),stage.querySelector('.box_timeline_third')]:[]; const timelineTargets=timelineBoxes.map(box=>{ if(!box)return null; const rect=box.getBoundingClientRect(); return{x:rect.left-sceneRect.left+rect.width/2,y:rect.top-sceneRect.top+rect.height/2}; }); const idxToSlot={1:0,3:1,5:2}; const slotSizing={1:{width:1.06},3:{width:1.12},5:{width:1.06}}; const TARGET_RATIO=1.72; const timelineProgress=gsap.utils.clamp(0,1,(normalizedProgress-0.7)/0.4); if(stage){ const stageEase=gsap.utils.clamp(0,1,(normalizedProgress-0.6)/0.25); stage.style.opacity=stageEase; stage.style.transform=`translateY(${gsap.utils.interpolate(24,0,stageEase)}px) scale(${gsap.utils.interpolate(0.94,1,stageEase)})`; } cards.forEach(card=>{ const idx=Number(card.dataset.index); if(!card.dataset.originX){ const rect=card.getBoundingClientRect(); // ИСПРАВЛЕНО: было card.rect.width, стало rect.width card.dataset.originX=rect.left-sceneRect.left+rect.width/2; card.dataset.originY=rect.top-sceneRect.top+rect.height/2; card.dataset.baseWidth=rect.width; } const originX=Number(card.dataset.originX); const originY=Number(card.dataset.originY); const baseWidth=Number(card.dataset.baseWidth); const baseTransform=card.dataset.baseTransform||(card.dataset.baseTransform=getComputedStyle(card).transform==='none'?'':getComputedStyle(card).transform); const appear=gsap.utils.clamp(0,1,normalizedProgress/0.3); let opacity=appear; let blur=12*(1-appear); let scale=gsap.utils.interpolate(0.9,1,appear); let liftY=gsap.utils.interpolate(20,0,appear); let shiftX=0; const fadeOut=gsap.utils.clamp(0,1,(normalizedProgress-0.3)/0.2); if(idx%2===0){ opacity*=1-fadeOut; blur=gsap.utils.interpolate(blur,12,fadeOut); scale=gsap.utils.interpolate(scale,0.9,fadeOut); } const isActive=idx%2!==0; card.classList.toggle('is-active',isActive); if(isActive){ const lift=gsap.utils.clamp(0,1,(normalizedProgress-0.5)/0.25); liftY=gsap.utils.interpolate(0,-18,lift); scale=gsap.utils.interpolate(scale,1.08,lift); const cluster=gsap.utils.clamp(0,1,(normalizedProgress-0.35)/0.35); const order=idx===1?-1:idx===3?0:1; shiftX=order*gsap.utils.interpolate(70,12,cluster); } const slotIdx=idxToSlot[idx]; const target=slotIdx!==undefined?timelineTargets[slotIdx]:null; let transformString=`${baseTransform} ${[shiftX?`translateX(${shiftX}px)`:'',liftY?`translateY(${liftY}px)`:'',`scale(${scale})`].filter(Boolean).join(' ')}`.trim(); if(isActive&&target&&timelineProgress>0){ const arriveEase=gsap.parseEase('expo.out')(timelineProgress); const sizeEase=gsap.parseEase('power2.out')(timelineProgress); const deltaX=target.x-originX; const deltaY=target.y-originY; const translateX=gsap.utils.interpolate(shiftX,deltaX,arriveEase); const translateY=gsap.utils.interpolate(liftY,deltaY,arriveEase); const widthFactor=gsap.utils.interpolate(1,(slotSizing[idx]||{width:1}).width,sizeEase); const targetWidth=baseWidth*widthFactor; const targetHeight=targetWidth/TARGET_RATIO; // На мобильных используем transform: scale вместо изменения width/height для предотвращения изменения layout if (isMobile) { const scaleX = targetWidth / baseWidth; const scaleY = targetHeight / (baseWidth / TARGET_RATIO); transformString=[`translateX(${translateX}px)`,`translateY(${translateY}px)`,`scale(${scale * scaleX}, ${scale * scaleY})`].join(' '); } else { card.style.width=`${targetWidth}px`; card.style.height=`${targetHeight}px`; transformString=[`translateX(${translateX}px)`,`translateY(${translateY}px)`,`scale(${gsap.utils.interpolate(scale,0.98,arriveEase)})`].join(' '); } }else{ if (!isMobile) { card.style.width=''; card.style.height=''; } } card.style.opacity=opacity; card.style.filter=`blur(${blur}px)`; card.style.transform=transformString; }); } function animateStep4(sceneEl,progress,animationDuration){ const normalizedProgress=Math.min(1,progress/animationDuration); const button=sceneEl.querySelector('.button_visual.export_button'); if(button){ const btnProgress=gsap.utils.clamp(0,1,normalizedProgress/0.25); const scale=0.85+0.15*btnProgress; button.style.opacity=btnProgress; button.style.transform=`scale(${scale})`; } const logos=sceneEl.querySelectorAll('.soft_logo'); if(!logos.length)return; const start=0.25; const available=Math.max(normalizedProgress-start,0); const segment=0.75/logos.length; logos.forEach((logo,index)=>{ const local=gsap.utils.clamp(0,1,(available-segment*index)/segment); const scale=0.85+0.15*local; logo.style.opacity=local; logo.style.transform=`scale(${scale})`; }); } // Используем фиксированную высоту для расчета const viewportHeightForCalc = isMobile ? fixedViewportHeight : window.innerHeight; wrappers.forEach(wrapper=>{ const step=wrapper.dataset.step; const scrollMultiplier=scrollHeights[step]||3; setWrapperStyles(wrapper,step,viewportHeightForCalc*scrollMultiplier); }); requestAnimationFrame(()=>{ wrappers.forEach(wrapper=>{ const step=wrapper.dataset.step; const sceneEl=wrapper.querySelector('.visual_step'); if(!sceneEl)return; ScrollTrigger.create({ trigger:wrapper, start:'top top', end:'bottom top', scrub:0.5, invalidateOnRefresh:!isMobile, refreshPriority:isMobile ? -1 : 0, onEnter:()=>{ if(!manualTargetStep||manualTargetStep===step){ setAccordion(step); if(manualTargetStep===step)manualTargetStep=null; } }, onEnterBack:()=>{ if(!manualTargetStep||manualTargetStep===step){ setAccordion(step); if(manualTargetStep===step)manualTargetStep=null; } }, onUpdate:({progress})=>{ setRing(step,progress); if(step==='3')animateStep3(sceneEl,progress,animationDuration); else if(step==='4')animateStep4(sceneEl,progress,animationDuration); else animateStandard(sceneEl,progress,animationDuration); if(step==='2')renderTypewriter(progress,animationDuration); } }); }); setAccordion(wrappers[0]?.dataset.step); setRing(wrappers[0]?.dataset.step||'1',0); renderTypewriter(0,animationDuration); syncAccordionVisibility(currentStep); ScrollTrigger.refresh(); setTimeout(()=>ScrollTrigger.refresh(),200); }); let resizeTimeout; window.addEventListener('resize',()=>{ clearTimeout(resizeTimeout); resizeTimeout=setTimeout(()=>{ // КРИТИЧНО: На мобильных не обновляем высоту во время скролла и не меняем после первой загрузки if (isMobile) { if (isScrolling) return; // Не пересчитываем высоту на мобильных - используем зафиксированную } const newViewportHeight = isMobile ? fixedViewportHeight : window.innerHeight; wrappers.forEach(wrapper=>{ const step=wrapper.dataset.step; const scrollMultiplier=scrollHeights[step]||3; setWrapperStyles(wrapper,step,newViewportHeight*scrollMultiplier); }); if (!isMobile) { ScrollTrigger.refresh(); } },150); }); });