lightswind
Version:
A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.
420 lines (409 loc) • 18.3 kB
JavaScript
;
"use client";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.InfiniteDrift = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const THREE = __importStar(require("three"));
const utils_1 = require("../lib/utils");
const DEFAULT_BANDS = [
{
offsetY: -220,
speed: 1.0,
rotation: 7,
rotationType: "fromLeft",
curveAmount: 40.0,
curveDirection: 1,
images: [
"https://images.unsplash.com/photo-1550684848-fac1c5b4e853?w=400",
"https://images.unsplash.com/photo-1550684399-3f0f745771d1?w=400",
"https://images.unsplash.com/photo-1550684847-75bdda21cc95?w=400",
"https://images.unsplash.com/photo-1563089145-599997674d42?w=400",
],
},
{
offsetY: -110,
speed: 1.3,
rotation: 7,
rotationType: "fromCenter",
curveAmount: 35.0,
curveDirection: 1,
images: [
"https://images.unsplash.com/photo-1557683316-973673baf926?w=400",
"https://images.unsplash.com/photo-1557683311-eac922347aa1?w=400",
"https://images.unsplash.com/photo-1557683325-3ba8f0df79de?w=400",
"https://images.unsplash.com/photo-1561070791-2526d30994b5?w=400",
],
},
{
offsetY: 0,
speed: 0.7,
rotation: 7,
curveAmount: 40.0,
curveDirection: 1,
images: [
"https://images.unsplash.com/photo-1511447333849-2b89ae13b002?w=400",
"https://images.unsplash.com/photo-1536431311719-398b6704d4cc?w=400",
"https://images.unsplash.com/photo-1543966888-7c1dc482a810?w=400",
"https://images.unsplash.com/photo-1561070791-36c11767b26a?w=400",
],
},
{
offsetY: 110,
speed: 1.2,
rotation: 7,
curveAmount: 40.0,
curveDirection: 1,
images: [
"https://images.unsplash.com/photo-1493246507139-91e8bef99c02?w=400",
"https://images.unsplash.com/photo-1477346611705-65d1883cee1e?w=400",
"https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=400",
"https://images.unsplash.com/photo-1561070791-0626bcd0516e?w=400",
],
},
{
offsetY: 220,
speed: 0.9,
rotation: 7,
curveAmount: 35.0,
curveDirection: 1,
images: [
"https://images.unsplash.com/photo-1534067783941-51c9c23ecefd?w=400",
"https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400",
"https://images.unsplash.com/photo-1550684848-fac1c5b4e853?w=400",
"https://images.unsplash.com/photo-1557683316-973673baf926?w=400",
],
},
];
const InfiniteDrift = ({ bands = DEFAULT_BANDS, height = 600, gap = 20, imageHeight = 100, bandHeight = 120, maxImageWidth = 300, inertia = 0.92, preserveOriginalRatios = true, className, children, }) => {
const containerRef = (0, react_1.useRef)(null);
const canvasRef = (0, react_1.useRef)(null);
const scrollState = (0, react_1.useRef)({
scrollY: 0,
targetScrollY: 0,
scrollVelocity: 0,
isDragging: false,
lastMouseY: 0,
});
(0, react_1.useEffect)(() => {
if (!containerRef.current || !canvasRef.current)
return;
const container = containerRef.current;
const canvas = canvasRef.current;
// --- Three.js Setup ---
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
camera.position.z = 1;
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true,
});
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const meshes = [];
const materials = [];
const textureLoader = new THREE.TextureLoader();
// --- Loading State Management ---
let totalImages = bands.reduce((acc, band) => acc + band.images.length, 0);
let loadedImages = 0;
const updateProgress = () => {
loadedImages++;
};
// --- Texture Generation ---
const createBandTexture = async (band) => {
const images = await Promise.all(band.images.map((url) => new Promise((resolve) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
updateProgress();
resolve(img);
};
img.onerror = () => {
// Fallback colored canvas if image fails
const fallback = document.createElement("canvas");
fallback.width = 400;
fallback.height = 300;
const ctx = fallback.getContext("2d");
ctx.fillStyle = `hsl(${Math.random() * 360}, 70%, 60%)`;
ctx.fillRect(0, 0, 400, 300);
updateProgress();
resolve(fallback);
};
img.src = url;
})));
// Calculate total width of one sequence
let sequenceWidth = 0;
const imageInfos = images.map((img) => {
const ratio = img.naturalWidth / img.naturalHeight || 1.5;
let w, h;
if (preserveOriginalRatios) {
h = imageHeight;
w = Math.round(h * ratio);
if (w > maxImageWidth) {
w = maxImageWidth;
h = Math.round(w / ratio);
}
}
else {
h = imageHeight;
w = Math.round(h * 1.5);
}
sequenceWidth += w + gap;
return { img, w, h };
});
sequenceWidth -= gap;
const cloneCount = 3;
const totalWidth = sequenceWidth * cloneCount;
const offscreenCanvas = document.createElement("canvas");
offscreenCanvas.width = totalWidth;
offscreenCanvas.height = bandHeight;
const ctx = offscreenCanvas.getContext("2d");
let currentX = 0;
for (let c = 0; c < cloneCount; c++) {
imageInfos.forEach((info) => {
const centeredY = (bandHeight - info.h) / 2;
ctx.drawImage(info.img, currentX, centeredY, info.w, info.h);
currentX += info.w + gap;
});
}
const texture = new THREE.Texture(offscreenCanvas);
texture.needsUpdate = true;
return { texture, totalWidth, sequenceWidth };
};
// --- Shaders ---
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
precision highp float;
uniform vec2 uResolution;
uniform sampler2D uTexture;
uniform float uTextureWidth;
uniform float uSequenceWidth;
uniform float uBandHeight;
uniform float uScroll;
uniform float uSpeed;
uniform float uOffsetY;
uniform float uRotation;
uniform float uRotationType;
uniform float uHasRotation;
uniform float uCurveAmount;
uniform float uCurveDirection;
varying vec2 vUv;
mat2 rotate2d(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
void main() {
vec2 pixelCoord = vUv * uResolution;
vec2 originalPixelCoord = pixelCoord;
float normalizedX = pixelCoord.x / uResolution.x;
float curveFactor = 4.0 * (normalizedX - 0.5) * (normalizedX - 0.5);
float curveOffset = (0.5 - curveFactor) * uCurveAmount * uCurveDirection;
float bandTopBase = (uResolution.y - uBandHeight) * 0.5 + uOffsetY;
float bandTop = bandTopBase + curveOffset;
float bandBottom = bandTop + uBandHeight;
float bandCenterY = bandTopBase + (uBandHeight * 0.5);
if (uHasRotation > 0.5) {
vec2 rotationCenter = (uRotationType > 0.5) ? vec2(0.0, bandCenterY) : vec2(uResolution.x * 0.5, bandCenterY);
pixelCoord -= rotationCenter;
pixelCoord = rotate2d(uRotation) * pixelCoord;
pixelCoord += rotationCenter;
originalPixelCoord -= rotationCenter;
originalPixelCoord = rotate2d(uRotation) * originalPixelCoord;
originalPixelCoord += rotationCenter;
}
float margin = 3.0;
if (pixelCoord.y < bandTop - margin || pixelCoord.y > bandBottom + margin) {
discard;
}
float scrollPos = uScroll * uSpeed;
float wrappedX = mod(originalPixelCoord.x + scrollPos, uSequenceWidth);
float textureX = (wrappedX + uSequenceWidth) / uTextureWidth;
float texY = (pixelCoord.y - bandTop) / (bandBottom - bandTop);
if (textureX < 0.0 || textureX > 1.0 || texY < 0.0 || texY > 1.0) {
discard;
}
vec4 color = texture2D(uTexture, vec2(textureX, texY));
if (color.a < 0.1) discard;
float edge = min(pixelCoord.y - bandTop, bandBottom - pixelCoord.y);
if (edge < margin) color.a *= smoothstep(0.0, margin, edge);
gl_FragColor = color;
}
`;
// --- Initialize Bands ---
const initBands = async () => {
for (let i = 0; i < bands.length; i++) {
const config = bands[i];
const { texture, totalWidth, sequenceWidth } = await createBandTexture(config);
const material = new THREE.ShaderMaterial({
uniforms: {
uResolution: { value: new THREE.Vector2(container.clientWidth, container.clientHeight) },
uTexture: { value: texture },
uTextureWidth: { value: totalWidth },
uSequenceWidth: { value: sequenceWidth },
uBandHeight: { value: bandHeight },
uScroll: { value: 0 },
uSpeed: { value: config.speed || 1.0 },
uOffsetY: { value: config.offsetY || 0 },
uRotation: { value: ((config.rotation || 0) * Math.PI) / 180 },
uRotationType: { value: config.rotationType === "fromLeft" ? 1.0 : 0.0 },
uHasRotation: { value: config.rotation ? 1.0 : 0.0 },
uCurveAmount: { value: config.curveAmount || 0 },
uCurveDirection: { value: config.curveDirection || 1 },
},
vertexShader,
fragmentShader,
transparent: true,
depthTest: false,
depthWrite: false,
});
const geometry = new THREE.PlaneGeometry(2, 2);
const mesh = new THREE.Mesh(geometry, material);
mesh.position.z = i * -0.01;
scene.add(mesh);
meshes.push(mesh);
materials.push(material);
}
};
initBands();
// --- Animation & Input ---
let animationId;
const animate = () => {
animationId = requestAnimationFrame(animate);
const state = scrollState.current;
if (!state.isDragging) {
state.targetScrollY += state.scrollVelocity;
state.scrollVelocity *= inertia;
if (Math.abs(state.scrollVelocity) < 0.1)
state.scrollVelocity = 0;
}
state.scrollY += (state.targetScrollY - state.scrollY) * 0.1;
materials.forEach((mat) => {
mat.uniforms.uScroll.value = state.scrollY;
mat.uniforms.uResolution.value.set(container.clientWidth, container.clientHeight);
});
renderer.render(scene, camera);
};
animate();
// --- Event Handlers ---
// Local wheel (hover) – prevent default so page doesn't scroll while over component
const handleWheel = (e) => {
e.preventDefault();
scrollState.current.targetScrollY += e.deltaY;
scrollState.current.scrollVelocity = e.deltaY * 0.15;
};
// Global page scroll – drive drift with any scroll anywhere on the page
let lastScrollTop = window.scrollY;
const handleGlobalScroll = () => {
const current = window.scrollY;
const delta = current - lastScrollTop;
lastScrollTop = current;
scrollState.current.targetScrollY += delta * 2.5;
scrollState.current.scrollVelocity = delta * 0.3;
};
const handleMouseDown = (e) => {
scrollState.current.isDragging = true;
scrollState.current.lastMouseY = e.clientY;
scrollState.current.scrollVelocity = 0;
};
const handleMouseMove = (e) => {
if (!scrollState.current.isDragging)
return;
const deltaY = e.clientY - scrollState.current.lastMouseY;
scrollState.current.targetScrollY += deltaY * 2.0;
scrollState.current.lastMouseY = e.clientY;
scrollState.current.scrollVelocity = deltaY * 0.25;
};
const handleMouseUp = () => {
scrollState.current.isDragging = false;
};
const handleResize = () => {
renderer.setSize(container.clientWidth, container.clientHeight);
materials.forEach((mat) => {
mat.uniforms.uResolution.value.set(container.clientWidth, container.clientHeight);
});
};
container.addEventListener("wheel", handleWheel, { passive: false });
container.addEventListener("mousedown", handleMouseDown);
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
window.addEventListener("resize", handleResize);
window.addEventListener("scroll", handleGlobalScroll, { passive: true });
// Mobile events
const handleTouchStart = (e) => {
scrollState.current.lastMouseY = e.touches[0].clientY;
scrollState.current.isDragging = true;
};
const handleTouchMove = (e) => {
if (!scrollState.current.isDragging)
return;
const deltaY = e.touches[0].clientY - scrollState.current.lastMouseY;
scrollState.current.targetScrollY += deltaY * 2.5;
scrollState.current.lastMouseY = e.touches[0].clientY;
scrollState.current.scrollVelocity = deltaY * 0.3;
};
container.addEventListener("touchstart", handleTouchStart, { passive: false });
container.addEventListener("touchmove", handleTouchMove, { passive: false });
container.addEventListener("touchend", handleMouseUp);
return () => {
cancelAnimationFrame(animationId);
container.removeEventListener("wheel", handleWheel);
container.removeEventListener("mousedown", handleMouseDown);
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
window.removeEventListener("resize", handleResize);
window.removeEventListener("scroll", handleGlobalScroll);
container.removeEventListener("touchstart", handleTouchStart);
container.removeEventListener("touchmove", handleTouchMove);
container.removeEventListener("touchend", handleMouseUp);
meshes.forEach(m => {
scene.remove(m);
m.geometry.dispose();
m.material.dispose();
});
renderer.dispose();
};
}, [bands, gap, imageHeight, bandHeight, maxImageWidth, inertia, preserveOriginalRatios]);
return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: (0, utils_1.cn)("relative w-full overflow-hidden bg-background cursor-grab active:cursor-grabbing", className), style: { height }, children: [(0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, className: "absolute inset-0 h-full w-full" }), children] }));
};
exports.InfiniteDrift = InfiniteDrift;
exports.default = exports.InfiniteDrift;
//# sourceMappingURL=infinite-drift.js.map