Import source: js/main
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
+544
@@ -0,0 +1,544 @@
|
|||||||
|
/* ============================================================
|
||||||
|
OTIS VANDERMEER — main.js
|
||||||
|
Three.js ember-field hero + GSAP/Lenis choreography
|
||||||
|
============================================================ */
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
document.documentElement.classList.add("js");
|
||||||
|
|
||||||
|
const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
const isFinePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Boot: wait for fonts + DOM so SplitLines measures correctly
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
const ready = Promise.all([
|
||||||
|
new Promise((res) => {
|
||||||
|
if (document.readyState !== "loading") res();
|
||||||
|
else document.addEventListener("DOMContentLoaded", res, { once: true });
|
||||||
|
}),
|
||||||
|
document.fonts ? document.fonts.ready : Promise.resolve(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ready.then(init);
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
|
const lenis = initSmoothScroll();
|
||||||
|
initClock();
|
||||||
|
initWebGL();
|
||||||
|
splitAll();
|
||||||
|
initPreloader();
|
||||||
|
initHeroIntro();
|
||||||
|
initScrollReveals();
|
||||||
|
initWorkList();
|
||||||
|
initMarquee();
|
||||||
|
initCounters();
|
||||||
|
initCursor();
|
||||||
|
initMagnetic(lenis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Smooth scroll (Lenis) driven by GSAP ticker
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initSmoothScroll() {
|
||||||
|
if (prefersReduced || typeof Lenis === "undefined") return null;
|
||||||
|
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
|
||||||
|
lenis.on("scroll", ScrollTrigger.update);
|
||||||
|
gsap.ticker.add((t) => lenis.raf(t * 1000));
|
||||||
|
gsap.ticker.lagSmoothing(0);
|
||||||
|
|
||||||
|
// Anchor links go through Lenis
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach((a) => {
|
||||||
|
a.addEventListener("click", (e) => {
|
||||||
|
const target = document.querySelector(a.getAttribute("href"));
|
||||||
|
if (!target) return;
|
||||||
|
e.preventDefault();
|
||||||
|
lenis.scrollTo(target, { offset: 0, duration: 1.4 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return lenis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Rotterdam clock
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initClock() {
|
||||||
|
const el = document.querySelector("[data-clock]");
|
||||||
|
if (!el) return;
|
||||||
|
const fmt = new Intl.DateTimeFormat("en-GB", {
|
||||||
|
timeZone: "Europe/Amsterdam",
|
||||||
|
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
||||||
|
});
|
||||||
|
const tick = () => (el.textContent = fmt.format(new Date()));
|
||||||
|
tick();
|
||||||
|
setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Three.js — "ember field" shader plane
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initWebGL() {
|
||||||
|
const canvas = document.querySelector("[data-webgl]");
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
let renderer;
|
||||||
|
try {
|
||||||
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: false, alpha: false });
|
||||||
|
} catch {
|
||||||
|
canvas.remove(); // graceful: CSS veil still gives a dark hero
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMobile = window.matchMedia("(max-width: 760px)").matches;
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.25 : 1.75));
|
||||||
|
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||||
|
|
||||||
|
const uniforms = {
|
||||||
|
uTime: { value: 0 },
|
||||||
|
uRes: { value: new THREE.Vector2(1, 1) },
|
||||||
|
uMouse: { value: new THREE.Vector2(0.5, 0.5) },
|
||||||
|
uScroll: { value: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const material = new THREE.ShaderMaterial({
|
||||||
|
uniforms,
|
||||||
|
vertexShader: /* glsl */ `
|
||||||
|
void main() { gl_Position = vec4(position, 1.0); }
|
||||||
|
`,
|
||||||
|
fragmentShader: /* glsl */ `
|
||||||
|
precision highp float;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform vec2 uRes;
|
||||||
|
uniform vec2 uMouse;
|
||||||
|
uniform float uScroll;
|
||||||
|
|
||||||
|
// --- simplex-ish value noise + fbm ---
|
||||||
|
vec2 hash(vec2 p) {
|
||||||
|
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
|
||||||
|
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
|
||||||
|
}
|
||||||
|
float noise(vec2 p) {
|
||||||
|
vec2 i = floor(p), f = fract(p);
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
return mix(
|
||||||
|
mix(dot(hash(i), f), dot(hash(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x),
|
||||||
|
mix(dot(hash(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),
|
||||||
|
dot(hash(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x),
|
||||||
|
u.y);
|
||||||
|
}
|
||||||
|
float fbm(vec2 p) {
|
||||||
|
float v = 0.0, a = 0.55;
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
v += a * noise(p);
|
||||||
|
p = p * 2.05 + vec2(11.3, 7.7);
|
||||||
|
a *= 0.5;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 uv = gl_FragCoord.xy / uRes;
|
||||||
|
vec2 p = uv;
|
||||||
|
p.x *= uRes.x / uRes.y;
|
||||||
|
|
||||||
|
float t = uTime * 0.05;
|
||||||
|
|
||||||
|
// mouse drift
|
||||||
|
vec2 m = (uMouse - 0.5) * 0.35;
|
||||||
|
|
||||||
|
// domain-warped fbm: slow molten flow
|
||||||
|
vec2 q = vec2(fbm(p * 1.4 + t + m), fbm(p * 1.4 - t * 0.7));
|
||||||
|
vec2 r = vec2(fbm(p * 1.8 + q * 1.6 + vec2(1.7, 9.2) + t * 0.6),
|
||||||
|
fbm(p * 1.8 + q * 1.6 + vec2(8.3, 2.8) - t * 0.4));
|
||||||
|
float f = fbm(p * 2.0 + r * 1.8);
|
||||||
|
|
||||||
|
// ridges -> thin ember veins
|
||||||
|
float vein = smoothstep(0.42, 0.5, f) * smoothstep(0.62, 0.5, f);
|
||||||
|
|
||||||
|
// palette: warm black -> deep rust -> ember -> pale gold
|
||||||
|
vec3 ink = vec3(0.047, 0.043, 0.035);
|
||||||
|
vec3 rust = vec3(0.30, 0.10, 0.04);
|
||||||
|
vec3 ember = vec3(1.0, 0.30, 0.14);
|
||||||
|
vec3 gold = vec3(1.0, 0.78, 0.5);
|
||||||
|
|
||||||
|
vec3 col = ink;
|
||||||
|
col = mix(col, rust, smoothstep(0.15, 0.75, f) * 0.85);
|
||||||
|
col = mix(col, ember, vein * 0.9);
|
||||||
|
col += gold * pow(vein, 3.0) * 0.6;
|
||||||
|
|
||||||
|
// glow pool near the mouse
|
||||||
|
float md = distance(uv, uMouse);
|
||||||
|
col += ember * 0.10 * smoothstep(0.45, 0.0, md) * (0.5 + 0.5 * f);
|
||||||
|
|
||||||
|
// top fade + vignette so type stays readable
|
||||||
|
col *= smoothstep(-0.15, 0.55, uv.y) * 0.9 + 0.1;
|
||||||
|
float vig = smoothstep(1.25, 0.45, length(uv - vec2(0.5, 0.42)));
|
||||||
|
col *= mix(0.55, 1.0, vig);
|
||||||
|
|
||||||
|
// scroll dims the field as the hero leaves
|
||||||
|
col *= 1.0 - uScroll * 0.85;
|
||||||
|
|
||||||
|
gl_FragColor = vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));
|
||||||
|
|
||||||
|
const hero = document.querySelector("[data-hero]");
|
||||||
|
const mouseTarget = new THREE.Vector2(0.5, 0.5);
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const w = hero.clientWidth, h = hero.clientHeight;
|
||||||
|
renderer.setSize(w, h, false);
|
||||||
|
renderer.getDrawingBufferSize(uniforms.uRes.value);
|
||||||
|
}
|
||||||
|
resize();
|
||||||
|
window.addEventListener("resize", resize);
|
||||||
|
|
||||||
|
if (isFinePointer) {
|
||||||
|
window.addEventListener("pointermove", (e) => {
|
||||||
|
mouseTarget.set(e.clientX / innerWidth, 1.0 - e.clientY / innerHeight);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// dim shader as hero scrolls away + pause when offscreen
|
||||||
|
let heroVisible = true;
|
||||||
|
new IntersectionObserver(([entry]) => (heroVisible = entry.isIntersecting), { threshold: 0 }).observe(hero);
|
||||||
|
|
||||||
|
const clock = new THREE.Clock();
|
||||||
|
gsap.ticker.add(() => {
|
||||||
|
if (!heroVisible) return;
|
||||||
|
uniforms.uTime.value = clock.getElapsedTime();
|
||||||
|
uniforms.uMouse.value.lerp(mouseTarget, 0.05);
|
||||||
|
uniforms.uScroll.value = Math.min(window.scrollY / Math.max(hero.clientHeight, 1), 1);
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Text splitting (words -> measured lines), no club plugins
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function splitToLines(el) {
|
||||||
|
const text = el.textContent.trim().replace(/\s+/g, " ");
|
||||||
|
const hasEm = el.querySelector("em");
|
||||||
|
// preserve <em> styling by re-walking original nodes
|
||||||
|
const frag = [];
|
||||||
|
el.childNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
node.textContent.split(/\s+/).filter(Boolean).forEach((w) => frag.push({ w, em: false }));
|
||||||
|
} else if (node.nodeName === "BR") {
|
||||||
|
frag.push({ br: true });
|
||||||
|
} else {
|
||||||
|
node.textContent.split(/\s+/).filter(Boolean).forEach((w) =>
|
||||||
|
frag.push({ w, em: node.nodeName === "EM" }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
el.innerHTML = "";
|
||||||
|
const words = frag.map((item) => {
|
||||||
|
if (item.br) {
|
||||||
|
const br = document.createElement("br");
|
||||||
|
el.appendChild(br);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.style.display = "inline-block";
|
||||||
|
if (item.em) {
|
||||||
|
const em = document.createElement("em");
|
||||||
|
em.textContent = item.w;
|
||||||
|
span.appendChild(em);
|
||||||
|
} else span.textContent = item.w;
|
||||||
|
el.appendChild(span);
|
||||||
|
el.appendChild(document.createTextNode(" "));
|
||||||
|
return span;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
// group by rendered line
|
||||||
|
const lines = [];
|
||||||
|
let lastTop = null;
|
||||||
|
words.forEach((w) => {
|
||||||
|
const top = w.offsetTop;
|
||||||
|
if (top !== lastTop) { lines.push([]); lastTop = top; }
|
||||||
|
lines[lines.length - 1].push(w);
|
||||||
|
});
|
||||||
|
|
||||||
|
el.innerHTML = "";
|
||||||
|
const lineSpans = lines.map((lineWords) => {
|
||||||
|
const outer = document.createElement("span");
|
||||||
|
outer.className = "split-line";
|
||||||
|
const inner = document.createElement("span");
|
||||||
|
inner.innerHTML = lineWords.map((w) => w.outerHTML).join(" ");
|
||||||
|
outer.appendChild(inner);
|
||||||
|
el.appendChild(outer);
|
||||||
|
return inner;
|
||||||
|
});
|
||||||
|
el.classList.add("is-split");
|
||||||
|
return lineSpans;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitAll() {
|
||||||
|
document.querySelectorAll("[data-reveal-lines]").forEach((el) => {
|
||||||
|
el._lines = splitToLines(el);
|
||||||
|
});
|
||||||
|
document.querySelectorAll("[data-reveal-words]").forEach((el) => {
|
||||||
|
el.classList.add("is-split"); // simple fade targets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Preloader
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initPreloader() {
|
||||||
|
const pre = document.querySelector(".preloader");
|
||||||
|
if (!pre) return;
|
||||||
|
const countEl = pre.querySelector("[data-preloader-count]");
|
||||||
|
const barEl = pre.querySelector("[data-preloader-bar]");
|
||||||
|
const wordEl = pre.querySelector("[data-preloader-word]");
|
||||||
|
const words = ["interfaces", "systems", "motion", "obsession"];
|
||||||
|
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
|
||||||
|
const state = { n: 0 };
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
pre.remove();
|
||||||
|
window._heroIntro && window._heroIntro.play();
|
||||||
|
ScrollTrigger.refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
tl.to(state, {
|
||||||
|
n: 100,
|
||||||
|
duration: prefersReduced ? 0.1 : 1.9,
|
||||||
|
ease: "power3.inOut",
|
||||||
|
onUpdate: () => {
|
||||||
|
const v = Math.round(state.n);
|
||||||
|
countEl.textContent = v;
|
||||||
|
barEl.style.width = v + "%";
|
||||||
|
wordEl.textContent = words[Math.min(Math.floor(v / 26), words.length - 1)];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tl.to(pre, {
|
||||||
|
clipPath: "inset(0 0 100% 0)",
|
||||||
|
duration: prefersReduced ? 0.1 : 0.9,
|
||||||
|
ease: "expo.inOut",
|
||||||
|
}, "+=0.15");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Hero intro (played by preloader)
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initHeroIntro() {
|
||||||
|
const lines = gsap.utils.toArray("[data-hero-line]");
|
||||||
|
const fades = gsap.utils.toArray("[data-hero-fade]");
|
||||||
|
const kicker = document.querySelector(".hero__kicker");
|
||||||
|
|
||||||
|
gsap.set(lines, { yPercent: 110 });
|
||||||
|
if (kicker) gsap.set(kicker, { opacity: 0, y: 14 });
|
||||||
|
|
||||||
|
const tl = gsap.timeline({ paused: true });
|
||||||
|
tl.to(lines, {
|
||||||
|
yPercent: 0,
|
||||||
|
duration: prefersReduced ? 0.1 : 1.3,
|
||||||
|
ease: "expo.out",
|
||||||
|
stagger: 0.12,
|
||||||
|
});
|
||||||
|
tl.to(kicker, { opacity: 1, y: 0, duration: 0.7, ease: "power2.out" }, "-=0.8");
|
||||||
|
tl.to(fades, { opacity: 1, duration: 0.9, ease: "power2.out", stagger: 0.07 }, "-=0.6");
|
||||||
|
|
||||||
|
window._heroIntro = tl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Scroll reveals
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initScrollReveals() {
|
||||||
|
// line-by-line masked rise
|
||||||
|
document.querySelectorAll("[data-reveal-lines]").forEach((el) => {
|
||||||
|
if (!el._lines) return;
|
||||||
|
gsap.set(el._lines, { yPercent: 115 });
|
||||||
|
gsap.to(el._lines, {
|
||||||
|
yPercent: 0,
|
||||||
|
duration: 1.1,
|
||||||
|
ease: "expo.out",
|
||||||
|
stagger: 0.09,
|
||||||
|
scrollTrigger: { trigger: el, start: "top 85%" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// label fades
|
||||||
|
document.querySelectorAll("[data-reveal-words]").forEach((el) => {
|
||||||
|
if (el.closest(".hero")) return; // hero handles its own
|
||||||
|
gsap.from(el, {
|
||||||
|
opacity: 0, y: 16, duration: 0.8, ease: "power2.out",
|
||||||
|
scrollTrigger: { trigger: el, start: "top 88%" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// work rows slide in
|
||||||
|
gsap.utils.toArray("[data-work-item]").forEach((item, i) => {
|
||||||
|
gsap.from(item.querySelector(".work__row"), {
|
||||||
|
opacity: 0, y: 40, duration: 0.9, ease: "power3.out",
|
||||||
|
scrollTrigger: { trigger: item, start: "top 90%" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// services
|
||||||
|
gsap.utils.toArray("[data-service]").forEach((item) => {
|
||||||
|
gsap.from(item, {
|
||||||
|
opacity: 0, y: 36, duration: 0.8, ease: "power3.out",
|
||||||
|
scrollTrigger: { trigger: item, start: "top 90%" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// portrait parallax
|
||||||
|
const portrait = document.querySelector("[data-parallax]");
|
||||||
|
if (portrait && !prefersReduced) {
|
||||||
|
gsap.fromTo(portrait, { y: 40 }, {
|
||||||
|
y: -40, ease: "none",
|
||||||
|
scrollTrigger: { trigger: portrait, start: "top bottom", end: "bottom top", scrub: 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// giant CTA lines
|
||||||
|
gsap.utils.toArray("[data-cta-line]").forEach((line, i) => {
|
||||||
|
gsap.from(line, {
|
||||||
|
yPercent: 60, opacity: 0, duration: 1.2, ease: "expo.out", delay: i * 0.08,
|
||||||
|
scrollTrigger: { trigger: ".contact__cta", start: "top 85%" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Work list: floating thumbnail follows cursor (desktop only)
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initWorkList() {
|
||||||
|
if (!isFinePointer) return;
|
||||||
|
const items = document.querySelectorAll("[data-work-item]");
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const thumb = item.querySelector("[data-work-thumb]");
|
||||||
|
if (!thumb) return;
|
||||||
|
|
||||||
|
const xTo = gsap.quickTo(thumb, "x", { duration: 0.5, ease: "power3.out" });
|
||||||
|
const yTo = gsap.quickTo(thumb, "y", { duration: 0.5, ease: "power3.out" });
|
||||||
|
// thumbs are position:fixed only on wide viewports; skip when static
|
||||||
|
const floating = () => getComputedStyle(thumb).position === "fixed";
|
||||||
|
|
||||||
|
item.addEventListener("pointerenter", (e) => {
|
||||||
|
if (!floating()) return;
|
||||||
|
gsap.set(thumb, { x: e.clientX, y: e.clientY });
|
||||||
|
gsap.to(thumb, {
|
||||||
|
opacity: 1, scale: 1, rotate: 0, duration: 0.45, ease: "power3.out",
|
||||||
|
transformOrigin: "center",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
item.addEventListener("pointermove", (e) => {
|
||||||
|
xTo(e.clientX);
|
||||||
|
yTo(e.clientY);
|
||||||
|
});
|
||||||
|
item.addEventListener("pointerleave", () => {
|
||||||
|
gsap.to(thumb, { opacity: 0, scale: 0.85, rotate: -4, duration: 0.35, ease: "power2.in" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Marquee: infinite loop + scroll-velocity skew
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initMarquee() {
|
||||||
|
const track = document.querySelector("[data-marquee-track]");
|
||||||
|
if (!track) return;
|
||||||
|
track.innerHTML += track.innerHTML; // duplicate for seamless wrap
|
||||||
|
|
||||||
|
if (prefersReduced) return;
|
||||||
|
const half = track.scrollWidth / 2;
|
||||||
|
const loop = gsap.to(track, {
|
||||||
|
x: -half, duration: 28, ease: "none", repeat: -1,
|
||||||
|
modifiers: { x: (x) => (parseFloat(x) % half) + "px" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// speed up with scroll velocity
|
||||||
|
ScrollTrigger.create({
|
||||||
|
trigger: document.body,
|
||||||
|
start: 0, end: "max",
|
||||||
|
onUpdate: (self) => {
|
||||||
|
const v = gsap.utils.clamp(0.5, 4, Math.abs(self.getVelocity()) / 280);
|
||||||
|
gsap.to(loop, { timeScale: v, duration: 0.4, overwrite: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Stat counters
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initCounters() {
|
||||||
|
document.querySelectorAll("[data-count]").forEach((el) => {
|
||||||
|
const target = +el.dataset.count;
|
||||||
|
const state = { n: 0 };
|
||||||
|
ScrollTrigger.create({
|
||||||
|
trigger: el, start: "top 88%", once: true,
|
||||||
|
onEnter: () =>
|
||||||
|
gsap.to(state, {
|
||||||
|
n: target, duration: 1.6, ease: "power3.out",
|
||||||
|
onUpdate: () => (el.textContent = Math.round(state.n)),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Custom cursor
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initCursor() {
|
||||||
|
if (!isFinePointer) return;
|
||||||
|
const root = document.querySelector(".cursor");
|
||||||
|
const dot = root.querySelector("[data-cursor-dot]");
|
||||||
|
const ring = root.querySelector("[data-cursor-ring]");
|
||||||
|
const label = root.querySelector("[data-cursor-label]");
|
||||||
|
document.body.classList.add("has-cursor");
|
||||||
|
|
||||||
|
const dotX = gsap.quickTo(dot, "x", { duration: 0.12, ease: "power2.out" });
|
||||||
|
const dotY = gsap.quickTo(dot, "y", { duration: 0.12, ease: "power2.out" });
|
||||||
|
const ringX = gsap.quickTo(ring, "x", { duration: 0.45, ease: "power3.out" });
|
||||||
|
const ringY = gsap.quickTo(ring, "y", { duration: 0.45, ease: "power3.out" });
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", (e) => {
|
||||||
|
dotX(e.clientX); dotY(e.clientY);
|
||||||
|
ringX(e.clientX); ringY(e.clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-cursor], a, button").forEach((el) => {
|
||||||
|
el.addEventListener("pointerenter", () => {
|
||||||
|
const text = el.dataset.cursor;
|
||||||
|
if (text) {
|
||||||
|
label.textContent = text;
|
||||||
|
root.classList.add("is-hover");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
el.addEventListener("pointerleave", () => root.classList.remove("is-hover"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
Magnetic links
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function initMagnetic() {
|
||||||
|
if (!isFinePointer || prefersReduced) return;
|
||||||
|
document.querySelectorAll("[data-magnetic]").forEach((el) => {
|
||||||
|
const xTo = gsap.quickTo(el, "x", { duration: 0.4, ease: "power3.out" });
|
||||||
|
const yTo = gsap.quickTo(el, "y", { duration: 0.4, ease: "power3.out" });
|
||||||
|
el.addEventListener("pointermove", (e) => {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
xTo((e.clientX - (r.left + r.width / 2)) * 0.35);
|
||||||
|
yTo((e.clientY - (r.top + r.height / 2)) * 0.35);
|
||||||
|
});
|
||||||
|
el.addEventListener("pointerleave", () => { xTo(0); yTo(0); });
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user