UNPKG

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
"use strict"; "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