Ordinis includes 3 GSAP animations. Each is self-contained — adjust or remove any one without affecting the others. All timing values live in a single cfg object at the top of each function.
| Animation | Code Location | Scope |
|---|---|---|
| 01 — Hero Image Slider | Home Page → Before </body> | Home only |
| 02 — Case Study List Hover | Home Page → Before </body> | Home only |
| 03 — Testimony Slider Drag | Site Settings → Before </body> | All pages |
Location: Home Page → Before </body> function initHeroGsapAnimations
What It Does
Auto-cycles through hero images every 4 seconds using a crossfade with a subtle scale-in effect on each incoming slide. As each image transitions, the matching stat number counts up from zero and the SVG path strokes in. If prefers-reduced-motion is active, the animation is skipped entirely.
Code Block
if (typeof gsap !== 'undefined') gsap.delayedCall(0, initHeroGsapAnimations);
function initHeroGsapAnimations() {
gsap.config({ nullTargetWarn: false });
var heroImage = document.querySelector('.hero_image');
var root = document.querySelector('.hero_image_stats');
if (!heroImage || !root) return;
var slides = gsap.utils.toArray(heroImage.querySelectorAll('.hero_image_item'));
if (slides.length < 2) return;
var titleEls = gsap.utils.toArray(root.querySelectorAll('.hero_image_overlay_title'));
var textEls = gsap.utils.toArray(root.querySelectorAll('.hero_image_overlay_text'));
var N = Math.min(slides.length, titleEls.length, textEls.length);
if (N < 2) return;
var cfg = { fade: 0.85, hold: 4, num: 1.15, scaleIn: 1.25 };
var path = root.querySelector('.info_stats_indicator path');
var proxies = [];
while (proxies.length < N) proxies.push({ value: 0 });
var gsapToggleActive = function (el, on) {
var c = String(gsap.getProperty(el, 'className') || '')
.replace(/\bis-active\b/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (on) c = c ? c + ' is-active' : 'is-active';
gsap.set(el, { attr: { class: c } });
};
var meta = [];
gsap.utils.toArray(titleEls.slice(0, N)).forEach(function (el, i) {
var raw = el.textContent.trim();
var m = raw.match(/^([\d.,]+)([\s\S]*)$/);
var target = m ? parseFloat(m[1].replace(/,/g, '')) : NaN;
if (!m || !Number.isFinite(target)) { meta[i] = { ok: false }; return; }
gsap.set(el, { innerHTML: '<span class="hero_overlay_num">0</span><span class="hero_overlay_sfx"></span>' });
var num = el.querySelector('.hero_overlay_num');
var sfx = el.querySelector('.hero_overlay_sfx');
gsap.set(sfx, { textContent: m[2] });
meta[i] = { ok: true, num: num, target: target, setNum: gsap.quickSetter(num, 'textContent') };
});
var numLayout = function () {
var tl = gsap.timeline();
gsap.utils.toArray(meta).forEach(function (o) {
if (!o.ok) return;
var ch = Math.max(1, String(Math.round(o.target)).length);
tl.set(o.num, { display: 'inline-block', minWidth: ch + 'ch', textAlign: 'right', fontVariantNumeric: 'tabular-nums' }, 0);
});
};
var dashPrep = function () {
if (!path) return 0;
var L = path.getTotalLength();
if (L > 0) gsap.set(path, { strokeDasharray: L, strokeDashoffset: L });
return L;
};
var drawPath = function () {
if (!path || dashPrep() <= 0) return;
gsap.killTweensOf(path);
gsap.set(path, { strokeDashoffset: path.getTotalLength() });
gsap.to(path, { strokeDashoffset: 0, duration: cfg.fade, ease: 'power2.inOut' });
};
var setStat = function (i) {
gsap.utils.toArray(titleEls).forEach(function (el, j) {
var inRange = j < N;
var on = inRange && j === i;
gsapToggleActive(el, on);
var o = inRange ? meta[j] : null;
if (inRange && !on && o && o.ok) {
gsap.killTweensOf(proxies[j]);
gsap.set(proxies[j], { value: 0 });
o.setNum('0');
}
});
gsap.utils.toArray(textEls).forEach(function (el, j) {
gsapToggleActive(el, j < N && j === i);
});
};
var countUp = function (idx) {
var o = meta[idx];
if (!o || !o.ok) return;
var p = proxies[idx];
gsap.killTweensOf(p);
gsap.set(p, { value: 0 });
o.setNum('0');
gsap.to(p, {
value: o.target, duration: cfg.num, ease: 'power2.out',
onUpdate: function () { o.setNum(String(Math.round(p.value))); },
onComplete: function () { o.setNum(String(Math.round(o.target))); }
});
};
gsap.context(function () {
numLayout();
dashPrep();
gsap.set(heroImage, { position: 'relative', overflow: 'hidden' });
var bootTl = gsap.timeline();
gsap.utils.toArray(slides).forEach(function (item, i) {
bootTl.set(item, {
position: 'absolute', top: 0, left: 0, width: '100%', height: '100%',
display: 'block', autoAlpha: i < N && i === 0 ? 1 : 0, scale: 1,
transformOrigin: '50% 50%', pointerEvents: i < N ? 'auto' : 'none'
}, 0);
});
var cur = 0;
var holdDc;
var advance = function () {
var next = (cur + 1) % N;
var outEl = slides[cur];
var inEl = slides[next];
gsap.set(inEl, { autoAlpha: 0, scale: cfg.scaleIn });
gsap.timeline({
onComplete: function () {
gsap.set(outEl, { autoAlpha: 0 });
gsap.set(inEl, { scale: 1 });
cur = next;
holdDc = gsap.delayedCall(cfg.hold, advance);
}
})
.call(function () { setStat(next); countUp(next); drawPath(); })
.to(outEl, { autoAlpha: 0, duration: cfg.fade, ease: 'power2.inOut' }, 0)
.to(inEl, { autoAlpha: 1, scale: 1, duration: cfg.fade, ease: 'power2.inOut' }, 0);
};
setStat(0);
countUp(0);
gsap.delayedCall(0, function () { gsap.delayedCall(0, drawPath); });
holdDc = gsap.delayedCall(cfg.hold, advance);
}, heroImage);
}Element Map
Customizing Key Variables
cfg.hold — autoplay delay (seconds)
How long each slide stays visible before advancing. Default: 4s.
var cfg = { fade: 0.85, hold: 6, num: 1.15, scaleIn: 1.25 };cfg.fade — crossfade duration (seconds)
Transition speed. Also controls the SVG stroke draw. Default: 0.85s. Range: 0.5–1.2.
var cfg = { fade: 0.6, hold: 4, num: 1.15, scaleIn: 1.25 };cfg.num — counter duration (seconds)
How long the number counts up from 0 to target. Default: 1.15s.
var cfg = { fade: 0.85, hold: 4, num: 2, scaleIn: 1.25 };cfg.scaleIn — incoming slide start scale
Scale the next slide starts at before settling to 1×. Default: 1.25. Lower = subtler zoom.
var cfg = { fade: 0.85, hold: 4, num: 1.15, scaleIn: 1.1 };Removing the Animation
initHeroGsapAnimations function and its call line.hero_image_item is visible in the Designer.info_stats_indicator will not animate — set stroke-dashoffset: 0 in CSS to keep it always visibleLocation: Home Page → Before </body> function initCaseStudyListGsap
What It Does
On desktop (≥ 992 px), hovering a case study row crossfades in the matching image with a subtle scale-zoom from 1.25×. The active row's link text turns white; inactive rows dim to grey. No animation fires on tablet or mobile — layout falls back to default Webflow styles.
Code Block
if (typeof gsap !== 'undefined') gsap.delayedCall(0, initCaseStudyListGsap);
function initCaseStudyListGsap() {
gsap.config({ nullTargetWarn: false });
var list = document.querySelector('.case-study_list');
if (!list) return;
var items = gsap.utils.toArray(list.querySelectorAll('.case-study_item'));
if (items.length < 1) return;
var imageWraps = [];
var links = [];
items.forEach(function (item) {
links.push(item.querySelector('.case-study_link'));
imageWraps.push(item.querySelector('.case-study_image'));
});
if (!links[0] || !imageWraps[0]) return;
gsap.matchMedia().add('(min-width: 992px)', function () {
var cfg = { fade: 0.85, scaleIn: 1.25 };
var colorActive = '#ffffff';
var colorInactive = '#9C9C9C';
function coversFor(wrap) {
return wrap ? gsap.utils.toArray(wrap.querySelectorAll('.object-fit-cover')) : [];
}
var settledIndex = 0;
var swapTl = null;
var ac = new AbortController();
function hardResetLayering(exceptVisibleIndex) {
imageWraps.forEach(function (w, i) {
if (!w) return;
gsap.killTweensOf(w);
var cov = coversFor(w);
gsap.killTweensOf(cov);
var on = i === exceptVisibleIndex;
gsap.set(w, { autoAlpha: on ? 1 : 0, zIndex: on ? 1 : 0 });
gsap.set(cov, { scale: 1, transformOrigin: '50% 50%' });
});
}
function applyLinkColors(activeIdx) {
gsap.killTweensOf(links);
links.forEach(function (link, j) {
if (!link) return;
gsap.to(link, { color: j === activeIdx ? colorActive : colorInactive, duration: cfg.fade * 0.45, ease: 'power2.out' });
});
}
function goTo(nextIndex) {
if (swapTl) swapTl.kill();
hardResetLayering(settledIndex);
if (nextIndex === settledIndex) { applyLinkColors(settledIndex); return; }
var outWrap = imageWraps[settledIndex];
var inWrap = imageWraps[nextIndex];
if (!outWrap || !inWrap) return;
var outCovers = coversFor(outWrap);
var inCovers = coversFor(inWrap);
gsap.set(inWrap, { autoAlpha: 0, zIndex: 2 });
gsap.set(inCovers, { scale: cfg.scaleIn, transformOrigin: '50% 50%' });
gsap.set(outWrap, { zIndex: 1, autoAlpha: 1 });
swapTl = gsap.timeline({
onComplete: function () {
imageWraps.forEach(function (w, i) {
if (!w) return;
gsap.set(w, i === nextIndex ? { autoAlpha: 1, zIndex: 1 } : { autoAlpha: 0, zIndex: 0 });
});
imageWraps.forEach(function (w) {
if (!w) return;
gsap.set(coversFor(w), { scale: 1 });
});
settledIndex = nextIndex;
}
})
.to(outWrap, { autoAlpha: 0, duration: cfg.fade, ease: 'power2.inOut' }, 0)
.to(inWrap, { autoAlpha: 1, duration: cfg.fade, ease: 'power2.inOut' }, 0)
.to(inCovers, { scale: 1, duration: cfg.fade, ease: 'power2.inOut' }, 0);
applyLinkColors(nextIndex);
}
var ctx = gsap.context(function () {
hardResetLayering(0);
gsap.set(links[0], { color: colorActive });
for (var k = 1; k < links.length; k++) {
if (links[k]) gsap.set(links[k], { color: colorInactive });
}
items.forEach(function (_item, i) {
var link = links[i];
if (!link) return;
link.addEventListener('mouseenter', function () { goTo(i); }, { signal: ac.signal });
});
}, list);
return function () { if (swapTl) swapTl.kill(); ac.abort(); ctx.revert(); };
});
}Element Map
Customizing Key Variables
cfg.fade — crossfade duration (seconds)
Image swap speed. Default: 0.85s. Link colour transition runs at ~45% of this value.
var cfg = { fade: 0.5, scaleIn: 1.25 };cfg.scaleIn — image entry scale
Scale the incoming image starts at before settling to 1×. Default: 1.25. Set to 1.1 for a subtler zoom.
var cfg = { fade: 0.85, scaleIn: 1.1 };colorActive / colorInactive — link text colours
Text colour for the hovered row and all others. Match to your brand palette if changed.
var colorActive = '#ffffff';
var colorInactive = '#9C9C9C';Removing the Animation
initCaseStudyListGsap function and its call line.case-study_image elements will be visible simultaneously — add display: none or hide all but the first in the Designergsap.matchMedia('(min-width: 992px)') — will not fire on tablet or mobile. Removing it does not affect those breakpoints.Location: Site Settings → Before </body> function initTestimonySlider Plugin: Observer
What It Does
Turns the testimony section into a draggable horizontal slider using GSAP Observer. Dragging or flicking snaps to the nearest slide with rubber-band resistance at both edges. On mouse devices, a custom pill cursor follows the pointer and fades in/out. This code runs site-wide — it exits silently on pages without the testimony section.
Code Block
if (typeof gsap !== 'undefined') {
gsap.registerPlugin(Observer);
gsap.delayedCall(0, initTestimonySlider);
}
function initTestimonySlider() {
gsap.config({ nullTargetWarn: false });
document.querySelectorAll('.testimony_content').forEach(function (root) {
var wrapper = root.querySelector('.testimony_slider_wrapper');
var slides = gsap.utils.toArray(root.querySelectorAll('.testimony_slider_item'));
var pill = root.querySelector('.testimony_hover_indicator');
if (!wrapper || slides.length < 2) return;
var activeIndex = 0;
var pressStartX = 0;
var resizeDc;
function slideGap() { return parseFloat(getComputedStyle(wrapper).columnGap) || 24; }
function slideOffset(i) {
var gap = slideGap();
var offset = 0;
for (var j = 0; j < i; j++) offset += slides[j].offsetWidth + gap;
return offset;
}
function maxTravel() { return Math.min(0, root.offsetWidth - wrapper.scrollWidth); }
function animateIn(i) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var content = slides[i] && slides[i].querySelector('.testimony_slider_video_content, .testimony_slider_text_content');
if (!content) return;
gsap.killTweensOf(content);
gsap.fromTo(content, { opacity: 0.94 }, { opacity: 1, duration: 0.28, ease: 'power2.out', overwrite: 'auto' });
}
function goTo(i) {
var mx = maxTravel();
var tx = Math.round(gsap.utils.clamp(mx, 0, -slideOffset(i)));
if (i !== activeIndex) { activeIndex = i; animateIn(i); }
gsap.to(wrapper, { x: tx, duration: 0.55, ease: 'power3.out' });
}
function snap(velocityX, totalDelta) {
var sw = slides[0] ? slides[0].offsetWidth : 200;
var isFlick = Math.abs(velocityX) > 200;
var isDraggedFar = Math.abs(totalDelta) > Math.max(60, sw * 0.15);
if (isFlick || isDraggedFar) {
goTo(gsap.utils.clamp(0, slides.length - 1, activeIndex + (totalDelta < 0 ? 1 : -1)));
return;
}
var cx = gsap.getProperty(wrapper, 'x');
var best = 0;
var bestDist = Infinity;
for (var i = 0; i < slides.length; i++) {
var d = Math.abs(cx + slideOffset(i));
if (d < bestDist) { bestDist = d; best = i; }
}
goTo(best);
}
gsap.set(root, { touchAction: 'pan-y', userSelect: 'none', cursor: 'grab' });
Observer.create({
target: root, type: 'pointer,touch', preventDefault: false,
onPress: function () { gsap.killTweensOf(wrapper); pressStartX = gsap.getProperty(wrapper, 'x'); gsap.set(root, { cursor: 'grabbing' }); },
onDrag: function (self) {
var mx = maxTravel();
var rawX = pressStartX + (self.x - self.startX);
gsap.set(wrapper, { x: rawX > 0 ? rawX * 0.25 : rawX < mx ? mx + (rawX - mx) * 0.25 : rawX });
},
onRelease: function (self) { gsap.set(root, { cursor: 'grab' }); snap(self.velocityX, self.x - self.startX); }
});
window.addEventListener('resize', function () {
if (resizeDc) resizeDc.kill();
resizeDc = gsap.delayedCall(0.15, function () { goTo(activeIndex); });
});
if (pill) {
gsap.set(pill, { left: 0, top: 0, xPercent: -50, yPercent: -50, autoAlpha: 0, pointerEvents: 'none' });
var qx = gsap.quickTo(pill, 'x', { duration: 0.42, ease: 'power2.out' });
var qy = gsap.quickTo(pill, 'y', { duration: 0.42, ease: 'power2.out' });
Observer.create({
target: root, type: 'pointer',
onMove: function (self) {
if (self.event.pointerType !== 'mouse') return;
gsap.to(pill, { autoAlpha: 1, duration: 0.15 });
var b = root.getBoundingClientRect();
qx(self.x - b.left);
qy(self.y - b.top);
},
onHoverEnd: function () { gsap.to(pill, { autoAlpha: 0, duration: 0.15 }); }
});
}
});
}Element Map
Customizing Key Variables
snap duration — slide animation speed
How fast the track snaps to position after releasing. Default: 0.55s. Range: 0.3–0.8.
gsap.to(wrapper, { x: tx, duration: 0.4, ease: 'power3.out' });flick velocity threshold (px/s)
Minimum Observer velocity to count as a flick and advance one slide. Default: 200 px/s. Lower = more sensitive.
var isFlick = Math.abs(velocityX) > 150;drag distance threshold
Minimum travel to advance one slide. Default: larger of 60 px or 15% of slide width.
var isDraggedFar = Math.abs(totalDelta) > Math.max(40, sw * 0.10);pill cursor follow speed
How fast the pill cursor chases the pointer. Default: 0.42s. Lower = snappier.
var qx = gsap.quickTo(pill, 'x', { duration: 0.28, ease: 'power2.out' });
var qy = gsap.quickTo(pill, 'y', { duration: 0.28, ease: 'power2.out' });Removing the Animation
gsap.registerPlugin(Observer) line, the entire initTestimonySlider function and its call line.testimony_slider_wrapper retains no inline transform — scrolling reverts to native CSS overflow.testimony_hover_indicator pill remains in the DOM but hidden by its default CSS; no visual side effectdocument.querySelectorAll('.testimony_content') — if the selector is not found on a page, it exits silently with no side effects.