// src/resources/js/scrollfx/ScrollTrigger.js export default class ScrollTrigger { constructor(config) { this.element = this.resolveElement(config.element); this.target = config.target ? this.element.querySelector(config.target) : this.element; if (config.target && !this.target) { throw new Error(`Target selector '${config.target}' not found inside element '${config.element}'.`); } this.start = config.start || 'top 80%'; this.end = config.end || 'bottom 20%'; this.scrub = config.scrub || false; this.onEnter = config.onEnter || null; this.onLeave = config.onLeave || null; this.onUpdate = config.onUpdate || null; this._wasVisible = false; this._progress = 0; } resolveElement(input) { if (typeof input === 'string') { const els = document.querySelectorAll(input); if (els.length === 1) return els[0]; throw new Error(`Selector '${input}' matched ${els.length} elements, expected exactly 1.`); } return input; } getScrollProgress(viewportHeight) { const rect = this.element.getBoundingClientRect(); const startPx = this.parsePosition(this.start, viewportHeight); const endPx = this.parsePosition(this.end, viewportHeight); const scrollRange = endPx - startPx; const current = rect.top - startPx; return 1 - Math.min(Math.max(current / scrollRange, 0), 1); } parsePosition(pos, viewportHeight) { const [edge, value] = pos.split(' '); const edgeOffset = edge === 'top' ? 0 : viewportHeight; const percentage = parseFloat(value) / 100; return edgeOffset - viewportHeight * percentage; } update(viewportHeight) { const rect = this.element.getBoundingClientRect(); const inViewport = rect.bottom > 0 && rect.top < viewportHeight; if (inViewport && !this._wasVisible) { this._wasVisible = true; if (this.onEnter) this.onEnter(this.target); } if (!inViewport && this._wasVisible) { this._wasVisible = false; if (this.onLeave) this.onLeave(this.target); } if (this.scrub && inViewport) { const progress = this.getScrollProgress(viewportHeight); if (this.onUpdate) this.onUpdate(this.target, progress); } } }