<style>
.t396__elem[class*="sphere-gallery-"]:not(.sphere-gallery-top):not(.sphere-gallery-bottom):not(.sphere-gallery-container):not([class*="sphere-gallery-container-"]) {
zoom: 1 !important;
}
.t396__elem[class*="sphere-gallery-"] .tn-atom {
border-radius: 10px !important;
}
.t396__elem.sphere-gallery-container,
.t396__elem[class*="sphere-gallery-container-"] {
overflow: visible !important;
}
.t396__elem.sphere-gallery-container .tn-atom,
.t396__elem[class*="sphere-gallery-container-"] .tn-atom {
position: relative !important;
width: 100% !important;
height: 100% !important;
transform-style: preserve-3d;
perspective: var(
--sphere-perspective,
1750px
);
perspective-origin: center center;
overflow: visible !important;
pointer-events: none;
}
.sphere-gallery-card {
position: absolute !important;
left: 50% !important;
top: 50% !important;
margin: 0 !important;
transform-style: preserve-3d;
transform-origin: center center !important;
backface-visibility: visible;
will-change: transform;
pointer-events: auto;
}
</style>
<script src="https://matilda-design.ru/library/GSAP.js"></script>
<script src="https://matilda-design.ru/library/ScrollTrigger.js"></script>
<script>
(function () {
const SPHERE_SETTINGS = {
perspective: 1750,
autoCenterX: true,
rotations: 1,
direction: 1,
startAngle: 0,
initialTiltX: -0.52,
initialTiltZ: 0,
downTiltX: -0.40,
downTiltZ: 0.22,
upTiltX: 0.10,
upTiltZ: -0.20,
cameraNear: 100,
cameraFar: -76,
scaleNear: 1.125,
scaleFar: 0.885,
maxVelocity: 1400,
minIntensity: 0,
maxIntensity: 0.94,
minScrollDelta: 0.01,
reactionSmoothTime: 0.12,
reverseSmoothTime: 0.055,
returnDelay: 80,
returnSmoothTime: 0.34,
maxImpulseSpeed: 8,
impulseStopThreshold: 0.00005,
velocityStopThreshold: 0.0005,
zoneTolerance: 35,
zoneExitTolerance: 75,
// Разрешения и параметры
desktop: {
minWidth: 1200,
radiusX: 450,
radiusZ: 250,
centerX: 40,
centerY: 75,
scaleBack: 0.72,
scaleFront: 0.93
},
laptop: {
minWidth: 960,
radiusX: 400,
radiusZ: 250,
centerX: 0,
centerY: 65,
scaleBack: 0.74,
scaleFront: 0.94
},
tablet: {
minWidth: 640,
radiusX: 280,
radiusZ: 250,
centerX: 0,
centerY: 55,
scaleBack: 0.76,
scaleFront: 0.95
},
mobileWide: {
minWidth: 480,
radiusX: 220,
radiusZ: 185,
centerX: 0,
centerY: 45,
scaleBack: 0.78,
scaleFront: 0.96
},
mobile: {
minWidth: 0,
radiusX: 200,
radiusZ: 130,
centerX: 0,
centerY: 35,
scaleBack: 0.80,
scaleFront: 0.97
}
};
document.documentElement.style.setProperty(
"--sphere-perspective",
SPHERE_SETTINGS.perspective + "px"
);
let sphereStarted = false;
let resizeTimer = null;
function domReady(callback) {
if (
document.readyState ===
"loading"
) {
document.addEventListener(
"DOMContentLoaded",
callback
);
} else {
callback();
}
}
function windowLoaded(callback) {
if (
document.readyState ===
"complete"
) {
callback();
} else {
window.addEventListener(
"load",
callback,
{
once: true
}
);
}
}
function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
function waitForFonts() {
if (
document.fonts &&
document.fonts.ready
) {
return document.fonts.ready.catch(
() => {}
);
}
return Promise.resolve();
}
function updateTildaLazyLoad() {
if (
typeof window.t_lazyload_update ===
"function"
) {
try {
window.t_lazyload_update();
} catch (error) {}
}
if (
typeof window.t396_lazyload_update ===
"function"
) {
try {
window.t396_lazyload_update();
} catch (error) {}
}
}
function waitForImagesIn(container) {
const images = Array.from(
container.querySelectorAll("img")
);
if (!images.length) {
return Promise.resolve();
}
return Promise.all(
images.map(img => {
if (
img.complete &&
img.naturalWidth > 0
) {
return Promise.resolve();
}
if (img.decode) {
return img
.decode()
.catch(() => {});
}
return new Promise(resolve => {
img.addEventListener(
"load",
resolve,
{
once: true
}
);
img.addEventListener(
"error",
resolve,
{
once: true
}
);
});
})
);
}
function waitForSphereContent(
maxAttempts,
delay
) {
let attempt = 0;
return new Promise(resolve => {
function check() {
attempt++;
const containers = Array.from(
document.querySelectorAll(
'[class*="uc-sphere-gallery-"]'
)
).filter(element => {
return Array.from(
element.classList
).some(className => {
return /^uc-sphere-gallery-\d+$/.test(
className
);
});
});
if (!containers.length) {
if (
attempt >=
maxAttempts
) {
resolve([]);
return;
}
setTimeout(
check,
delay
);
return;
}
const readyContainers =
containers.filter(container => {
const triggerTop =
container.querySelector(
".sphere-gallery-top"
);
const triggerBottom =
container.querySelector(
".sphere-gallery-bottom"
);
const sceneWrapper =
container.querySelector(
".sphere-gallery-container"
);
const scene =
sceneWrapper
? sceneWrapper.querySelector(
".tn-atom"
)
: null;
const cards =
getSphereCards(container);
return (
triggerTop &&
triggerBottom &&
scene &&
cards.length
);
});
if (
readyContainers.length
) {
resolve(
readyContainers
);
return;
}
if (
attempt >=
maxAttempts
) {
resolve(containers);
return;
}
setTimeout(
check,
delay
);
}
check();
});
}
async function startSphereWhenReady() {
if (sphereStarted) return;
windowLoaded(async function () {
if (sphereStarted) return;
updateTildaLazyLoad();
await waitForFonts();
await wait(300);
const containers =
await waitForSphereContent(
30,
200
);
for (
const container
of containers
) {
await waitForImagesIn(
container
);
}
await wait(300);
updateTildaLazyLoad();
initSphere();
setTimeout(
hardRefresh,
100
);
setTimeout(
hardRefresh,
500
);
});
}
function hardRefresh() {
updateTildaLazyLoad();
if (
window.__sphereInstances &&
window.__sphereInstances.length
) {
window.__sphereInstances.forEach(
instance => {
instance.prepareItems();
instance.resetResponsiveState();
}
);
}
if (
typeof ScrollTrigger !==
"undefined"
) {
ScrollTrigger.refresh(true);
}
}
function clamp(
value,
min,
max
) {
return Math.max(
min,
Math.min(
max,
value
)
);
}
function lerp(
from,
to,
progress
) {
return (
from +
(
to -
from
) *
progress
);
}
function smoothDamp(
current,
target,
currentVelocity,
smoothTime,
maxSpeed,
deltaTime
) {
smoothTime =
Math.max(
0.0001,
smoothTime
);
deltaTime =
Math.max(
0.0001,
deltaTime
);
const omega =
2 /
smoothTime;
const x =
omega *
deltaTime;
const exponential =
1 /
(
1 +
x +
0.48 * x * x +
0.235 * x * x * x
);
let change =
current -
target;
const originalTarget =
target;
const maxChange =
maxSpeed *
smoothTime;
change =
clamp(
change,
-maxChange,
maxChange
);
target =
current -
change;
const temporary =
(
currentVelocity +
omega * change
) *
deltaTime;
let newVelocity =
(
currentVelocity -
omega * temporary
) *
exponential;
let output =
target +
(
change +
temporary
) *
exponential;
const crossedTarget =
(
originalTarget -
current >
0
) ===
(
output >
originalTarget
);
if (crossedTarget) {
output =
originalTarget;
newVelocity = 0;
}
return {
value: output,
velocity: newVelocity
};
}
function rotatePointX(
point,
angle
) {
if (!angle) return point;
const cos =
Math.cos(angle);
const sin =
Math.sin(angle);
return {
x: point.x,
y:
point.y * cos -
point.z * sin,
z:
point.y * sin +
point.z * cos
};
}
function rotatePointZ(
point,
angle
) {
if (!angle) return point;
const cos =
Math.cos(angle);
const sin =
Math.sin(angle);
return {
x:
point.x * cos -
point.y * sin,
y:
point.x * sin +
point.y * cos,
z: point.z
};
}
function getSphereIndex(element) {
const className =
element.className || "";
const match =
className.match(
/(?:^|\s)sphere-gallery-(\d+)(?:\s|$)/
);
return match
? parseInt(
match[1],
10
)
: 9999;
}
function hasExactClassByRegex(
element,
regex
) {
return [
...element.classList
].some(className => {
return regex.test(
className
);
});
}
function getSphereCards(container) {
return gsap.utils
.toArray(
container.querySelectorAll(
'[class*="sphere-gallery-"]'
)
)
.filter(element => {
if (
element.classList.contains(
"sphere-gallery-top"
)
) {
return false;
}
if (
element.classList.contains(
"sphere-gallery-bottom"
)
) {
return false;
}
if (
element.classList.contains(
"sphere-gallery-container"
)
) {
return false;
}
if (
hasExactClassByRegex(
element,
/^uc-sphere-gallery-\d+$/
)
) {
return false;
}
if (
hasExactClassByRegex(
element,
/^sphere-gallery-container-\d+$/
)
) {
return false;
}
return hasExactClassByRegex(
element,
/^sphere-gallery-\d+$/
);
})
.sort(
(
first,
second
) => {
return (
getSphereIndex(
first
) -
getSphereIndex(
second
)
);
}
);
}
function getCurrentLayoutSettings() {
const width =
window.innerWidth;
if (
width >=
SPHERE_SETTINGS.desktop.minWidth
) {
return SPHERE_SETTINGS.desktop;
}
if (
width >=
SPHERE_SETTINGS.laptop.minWidth
) {
return SPHERE_SETTINGS.laptop;
}
if (
width >=
SPHERE_SETTINGS.tablet.minWidth
) {
return SPHERE_SETTINGS.tablet;
}
if (
width >=
SPHERE_SETTINGS.mobileWide.minWidth
) {
return SPHERE_SETTINGS.mobileWide;
}
return SPHERE_SETTINGS.mobile;
}
function getEffectiveHeight(element) {
if (!element) return 0;
const height =
element.offsetHeight;
const zoom =
parseFloat(
getComputedStyle(
element
).zoom || "1"
);
return (
height *
zoom
);
}
function getAutoCenterOffsetX(stage) {
if (
!SPHERE_SETTINGS.autoCenterX ||
!stage
) {
return 0;
}
const rect =
stage.getBoundingClientRect();
const stageCenterX =
rect.left +
rect.width / 2;
const screenCenterX =
window.innerWidth / 2;
return (
screenCenterX -
stageCenterX
);
}
function getScrollY() {
return (
window.scrollY ||
window.pageYOffset ||
0
);
}
function initSphere() {
if (sphereStarted) return;
sphereStarted = true;
gsap.registerPlugin(
ScrollTrigger
);
ScrollTrigger.config({
autoRefreshEvents:
"visibilitychange,DOMContentLoaded,load",
ignoreMobileResize: true
});
const sphereInstances = [];
window.__sphereInstances =
sphereInstances;
let instanceIndex = 1;
while (true) {
const container =
document.querySelector(
`.uc-sphere-gallery-${instanceIndex}`
);
if (!container) {
break;
}
const triggerTop =
container.querySelector(
".sphere-gallery-top"
);
const triggerBottom =
container.querySelector(
".sphere-gallery-bottom"
);
const sceneWrapper =
container.querySelector(
".sphere-gallery-container"
);
const stage =
sceneWrapper
? sceneWrapper.querySelector(
".tn-atom"
)
: null;
const cards =
getSphereCards(
container
);
if (
!triggerTop ||
!triggerBottom ||
!sceneWrapper ||
!stage ||
!cards.length
) {
console.warn(
"",
{
instance:
instanceIndex,
triggerTop:
!!triggerTop,
triggerBottom:
!!triggerBottom,
sceneWrapper:
!!sceneWrapper,
stage:
!!stage,
cards:
cards.length
}
);
instanceIndex++;
continue;
}
const state = {
progress: 0
};
const motionState = {
impulse: 0,
targetImpulse: 0,
impulseVelocity: 0
};
const itemData = [];
let layoutSettings =
getCurrentLayoutSettings();
let autoCenterOffsetX = 0;
let sphereScrollTrigger = null;
let zoneIsActive = false;
let lastScrollY =
getScrollY();
let lastScrollTime =
performance.now();
let lastMovementTime =
performance.now();
let lastFrameTime =
performance.now();
let animationFrameId = null;
function prepareItems() {
layoutSettings =
getCurrentLayoutSettings();
itemData.length = 0;
cards.forEach(card => {
card.classList.add(
"sphere-gallery-card"
);
if (
card.parentNode !==
stage
) {
stage.appendChild(
card
);
}
gsap.set(
card,
{
clearProps:
"transform",
x: 0,
y: 0,
z: 0,
scale: 1,
opacity: 1,
rotationX: 0,
rotationY: 0,
rotationZ: 0
}
);
});
autoCenterOffsetX =
getAutoCenterOffsetX(
stage
);
cards.forEach(
(
card,
index
) => {
const rect =
card.getBoundingClientRect();
itemData.push({
card: card,
index: index,
width:
rect.width,
height:
rect.height
});
}
);
}
function update() {
const count =
itemData.length;
if (!count) return;
const radiusX =
layoutSettings.radiusX;
const radiusZ =
layoutSettings.radiusZ;
const centerX =
layoutSettings.centerX;
const centerY =
layoutSettings.centerY;
const scaleBack =
layoutSettings.scaleBack;
const scaleFront =
layoutSettings.scaleFront;
const cardAngleStep =
Math.PI *
2 /
count;
const scrollAngle =
state.progress *
Math.PI *
2 *
SPHERE_SETTINGS.rotations *
SPHERE_SETTINGS.direction;
const impulse =
clamp(
motionState.impulse,
-SPHERE_SETTINGS.maxIntensity,
SPHERE_SETTINGS.maxIntensity
);
const intensity =
Math.abs(
impulse
);
let cameraZ = 0;
let motionScale = 1;
let currentTiltX =
SPHERE_SETTINGS.initialTiltX;
let currentTiltZ =
SPHERE_SETTINGS.initialTiltZ;
if (impulse > 0) {
cameraZ =
SPHERE_SETTINGS.cameraNear *
intensity;
motionScale =
lerp(
1,
SPHERE_SETTINGS.scaleNear,
intensity
);
currentTiltX =
lerp(
SPHERE_SETTINGS.initialTiltX,
SPHERE_SETTINGS.downTiltX,
intensity
);
currentTiltZ =
lerp(
SPHERE_SETTINGS.initialTiltZ,
SPHERE_SETTINGS.downTiltZ,
intensity
);
} else if (
impulse < 0
) {
cameraZ =
SPHERE_SETTINGS.cameraFar *
intensity;
motionScale =
lerp(
1,
SPHERE_SETTINGS.scaleFar,
intensity
);
currentTiltX =
lerp(
SPHERE_SETTINGS.initialTiltX,
SPHERE_SETTINGS.upTiltX,
intensity
);
currentTiltZ =
lerp(
SPHERE_SETTINGS.initialTiltZ,
SPHERE_SETTINGS.upTiltZ,
intensity
);
}
const depthData =
itemData
.map(item => {
const angle =
SPHERE_SETTINGS.startAngle +
item.index *
cardAngleStep +
scrollAngle;
return {
index: item.index,
baseZ:
Math.cos(angle) *
radiusZ
};
})
.sort((first, second) => {
return (
second.baseZ -
first.baseZ
);
});
const fifthDepth =
depthData[
Math.min(
4,
depthData.length - 1
)
].baseZ;
const opacityTransitionRange =
radiusZ * 0.35;
itemData.forEach(item => {
const angle =
SPHERE_SETTINGS.startAngle +
item.index *
cardAngleStep +
scrollAngle;
const baseZ =
Math.cos(angle) *
radiusZ;
let point = {
x:
Math.sin(angle) *
radiusX,
y: 0,
z:
baseZ
};
point =
rotatePointX(
point,
currentTiltX
);
point =
rotatePointZ(
point,
currentTiltZ
);
const depthProgress =
clamp(
(
baseZ +
radiusZ
) /
(
radiusZ *
2
),
0,
1
);
const depthScale =
scaleBack +
depthProgress *
(
scaleFront -
scaleBack
);
const minBackOpacity = 0;
const fadeProgress =
clamp(
(
baseZ -
(
fifthDepth -
opacityTransitionRange
)
) /
opacityTransitionRange,
0,
1
);
const smoothFadeProgress =
fadeProgress *
fadeProgress *
(
3 -
2 * fadeProgress
);
const baseDepthOpacity =
minBackOpacity +
depthProgress *
(
1 -
minBackOpacity
);
const depthOpacity =
baseDepthOpacity +
(
1 -
baseDepthOpacity
) *
smoothFadeProgress;
const finalScale =
depthScale *
motionScale;
const finalZ =
point.z +
cameraZ;
gsap.set(
item.card,
{
x:
autoCenterOffsetX +
centerX +
point.x -
item.width / 2,
y:
centerY +
point.y -
item.height / 2,
z:
finalZ,
scale:
finalScale,
opacity:
depthOpacity,
rotationX: 0,
rotationY: 0,
rotationZ: 0,
force3D: true
}
);
});
}
function updateZoneState(
scrollY
) {
if (
!sphereScrollTrigger
) {
zoneIsActive = false;
return false;
}
const tolerance =
zoneIsActive
? SPHERE_SETTINGS.zoneExitTolerance
: SPHERE_SETTINGS.zoneTolerance;
const isInside =
scrollY >=
sphereScrollTrigger.start -
tolerance &&
scrollY <=
sphereScrollTrigger.end +
tolerance;
zoneIsActive =
isInside;
return isInside;
}
function movementCrossesZone(
previousY,
currentY
) {
if (
!sphereScrollTrigger
) {
return false;
}
const movementStart =
Math.min(
previousY,
currentY
);
const movementEnd =
Math.max(
previousY,
currentY
);
const zoneStart =
sphereScrollTrigger.start -
SPHERE_SETTINGS.zoneTolerance;
const zoneEnd =
sphereScrollTrigger.end +
SPHERE_SETTINGS.zoneTolerance;
return (
movementEnd >=
zoneStart &&
movementStart <=
zoneEnd
);
}
function setMotionTarget(
direction,
intensity
) {
const nextImpulse =
clamp(
direction > 0
? intensity
: -intensity,
-SPHERE_SETTINGS.maxIntensity,
SPHERE_SETTINGS.maxIntensity
);
const currentDirection =
Math.sign(motionState.impulse);
const nextDirection =
Math.sign(nextImpulse);
if (
currentDirection !== 0 &&
nextDirection !== 0 &&
currentDirection !== nextDirection
) {
motionState.impulseVelocity *= 0.08;
}
motionState.targetImpulse =
nextImpulse;
}
function handleRealScroll() {
const currentScrollY =
getScrollY();
const currentTime =
performance.now();
const previousScrollY =
lastScrollY;
const deltaY =
currentScrollY -
previousScrollY;
const deltaTime =
Math.max(
currentTime -
lastScrollTime,
8
);
lastScrollY =
currentScrollY;
lastScrollTime =
currentTime;
if (
Math.abs(deltaY) <
SPHERE_SETTINGS.minScrollDelta
) {
return;
}
const isInside =
updateZoneState(
currentScrollY
);
const crossedZone =
movementCrossesZone(
previousScrollY,
currentScrollY
);
if (
!isInside &&
!crossedZone
) {
motionState.targetImpulse = 0;
return;
}
const direction =
deltaY > 0
? 1
: -1;
const velocity =
Math.abs(deltaY) /
deltaTime *
1000;
const velocityProgress =
clamp(
velocity /
SPHERE_SETTINGS.maxVelocity,
0,
1
);
const smoothVelocityProgress =
velocityProgress *
velocityProgress *
(3 - 2 * velocityProgress);
const intensity =
SPHERE_SETTINGS.maxIntensity *
smoothVelocityProgress;
lastMovementTime =
currentTime;
setMotionTarget(
direction,
intensity
);
}
window.addEventListener(
"scroll",
handleRealScroll,
{
passive: true
}
);
function motionLoop(
currentTime
) {
const deltaSeconds =
Math.min(
(
currentTime -
lastFrameTime
) /
1000,
0.05
);
lastFrameTime =
currentTime;
const currentScrollY =
getScrollY();
const isInside =
updateZoneState(
currentScrollY
);
const timeSinceMovement =
currentTime -
lastMovementTime;
if (
!isInside ||
timeSinceMovement >
SPHERE_SETTINGS.returnDelay
) {
motionState.targetImpulse = 0;
}
const isReturning =
Math.abs(
motionState.targetImpulse
) <
0.000001;
const currentDirection =
Math.sign(motionState.impulse);
const targetDirection =
Math.sign(motionState.targetImpulse);
const isChangingDirection =
!isReturning &&
currentDirection !== 0 &&
targetDirection !== 0 &&
currentDirection !== targetDirection;
const currentSmoothTime =
isReturning
? SPHERE_SETTINGS.returnSmoothTime
: isChangingDirection
? SPHERE_SETTINGS.reverseSmoothTime
: SPHERE_SETTINGS.reactionSmoothTime;
const dampedState =
smoothDamp(
motionState.impulse,
motionState.targetImpulse,
motionState.impulseVelocity,
currentSmoothTime,
SPHERE_SETTINGS.maxImpulseSpeed,
deltaSeconds
);
motionState.impulse =
dampedState.value;
motionState.impulseVelocity =
dampedState.velocity;
if (
isReturning &&
Math.abs(
motionState.impulse
) <
SPHERE_SETTINGS.impulseStopThreshold &&
Math.abs(
motionState.impulseVelocity
) <
SPHERE_SETTINGS.velocityStopThreshold
) {
motionState.impulse = 0;
motionState.impulseVelocity = 0;
}
update();
animationFrameId =
requestAnimationFrame(
motionLoop
);
}
function resetScrollMeasurement() {
const currentTime =
performance.now();
lastScrollY =
getScrollY();
lastScrollTime =
currentTime;
lastMovementTime =
currentTime;
lastFrameTime =
currentTime;
zoneIsActive =
false;
}
function resetResponsiveState() {
layoutSettings =
getCurrentLayoutSettings();
motionState.impulse = 0;
motionState.targetImpulse = 0;
motionState.impulseVelocity = 0;
resetScrollMeasurement();
autoCenterOffsetX =
getAutoCenterOffsetX(
stage
);
update();
}
prepareItems();
update();
function createScrollTrigger() {
const offsetTop =
getEffectiveHeight(
triggerTop
);
const offsetBottom =
getEffectiveHeight(
triggerBottom
);
const containerHeight =
container.offsetHeight;
const startTrigger =
`top+=${offsetTop} center`;
const endTrigger =
`top+=${containerHeight - offsetBottom} center`;
const sphereTween =
gsap.to(
state,
{
progress: 1,
ease: "none",
scrollTrigger: {
trigger:
container,
start:
startTrigger,
end:
endTrigger,
scrub: true,
invalidateOnRefresh:
true,
markers: false,
onRefresh:
function () {
prepareItems();
resetScrollMeasurement();
updateZoneState(
getScrollY()
);
update();
}
}
}
);
sphereScrollTrigger =
sphereTween.scrollTrigger;
updateZoneState(
getScrollY()
);
}
createScrollTrigger();
animationFrameId =
requestAnimationFrame(
motionLoop
);
sphereInstances.push({
prepareItems:
prepareItems,
update:
update,
resetResponsiveState:
resetResponsiveState,
destroy:
function () {
window.removeEventListener(
"scroll",
handleRealScroll
);
if (
animationFrameId
) {
cancelAnimationFrame(
animationFrameId
);
}
}
});
instanceIndex++;
}
let lastViewportWidth = window.innerWidth;
function refreshSphereLayout() {
sphereInstances.forEach(
instance => {
instance.prepareItems();
instance.resetResponsiveState();
}
);
ScrollTrigger.refresh(true);
}
window.addEventListener(
"resize",
() => {
const currentWidth = window.innerWidth;
if (
Math.abs(
currentWidth -
lastViewportWidth
) < 2
) {
return;
}
lastViewportWidth = currentWidth;
clearTimeout(
resizeTimer
);
resizeTimer =
setTimeout(
refreshSphereLayout,
250
);
},
{ passive: true }
);
window.addEventListener(
"orientationchange",
() => {
clearTimeout(
resizeTimer
);
resizeTimer =
setTimeout(
() => {
lastViewportWidth =
window.innerWidth;
refreshSphereLayout();
},
600
);
},
{ passive: true }
);
}
domReady(
startSphereWhenReady
);
})();
</script>