chore: complete update
This commit is contained in:
25
resources/js/modules/config.js
Normal file
25
resources/js/modules/config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export const moduleConfig = {
|
||||
'noise': {
|
||||
selector: '.noise-overlay',
|
||||
toggleKey: 'g',
|
||||
className: 'grainy',
|
||||
enableTransition: true
|
||||
},
|
||||
'shortcut-handler.js': {
|
||||
debug: false
|
||||
},
|
||||
'scrollfx': {
|
||||
selector: '.fade-in-on-scroll, .zoom-in',
|
||||
offset: 0.8,
|
||||
baseDelay: 0.075,
|
||||
once: true
|
||||
},
|
||||
'scroll-timeline': {
|
||||
attribute: 'data-scroll-step',
|
||||
triggerPoint: 0.4,
|
||||
once: false
|
||||
},
|
||||
'smooth-scroll': {
|
||||
'speed': 0.2,
|
||||
}
|
||||
};
|
||||
37
resources/js/modules/example-module/index.js
Normal file
37
resources/js/modules/example-module/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// modules/example-module/index.js
|
||||
import { registerFrameTask, unregisterFrameTask } from '../../core/frameloop.js';
|
||||
|
||||
let frameId = 'example-module';
|
||||
let resizeHandler = null;
|
||||
|
||||
export function init(config = {}) {
|
||||
console.log('[example-module] init');
|
||||
|
||||
// z. B. Event-Listener hinzufügen
|
||||
resizeHandler = () => {
|
||||
console.log('Fenstergröße geändert');
|
||||
};
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
|
||||
// Scroll- oder Frame-Logik
|
||||
registerFrameTask(frameId, () => {
|
||||
// wiederkehrende Aufgabe
|
||||
const scrollY = window.scrollY;
|
||||
// ggf. transformieren oder Werte speichern
|
||||
}, { autoStart: true });
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
console.log('[example-module] destroy');
|
||||
|
||||
// EventListener entfernen
|
||||
if (resizeHandler) {
|
||||
window.removeEventListener('resize', resizeHandler);
|
||||
resizeHandler = null;
|
||||
}
|
||||
|
||||
// FrameTask entfernen
|
||||
unregisterFrameTask(frameId);
|
||||
|
||||
// weitere Aufräumarbeiten, z.B. Observer disconnect
|
||||
}
|
||||
47
resources/js/modules/index.js
Normal file
47
resources/js/modules/index.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { moduleConfig } from './config.js';
|
||||
import { Logger } from '../core/logger.js';
|
||||
|
||||
export const activeModules = new Map(); // key: modulename → { mod, config }
|
||||
|
||||
export async function registerModules() {
|
||||
const modules = import.meta.glob('./*/index.js', { eager: true });
|
||||
|
||||
const domModules = new Set(
|
||||
Array.from(document.querySelectorAll('[data-module]')).map(el => el.dataset.module).filter(Boolean)
|
||||
);
|
||||
|
||||
const usedModules = new Set(domModules);
|
||||
const fallbackMode = usedModules.size === 0;
|
||||
|
||||
Object.entries(modules).forEach(([path, mod]) => {
|
||||
const name = path.split('/').slice(-2, -1)[0]; // z.B. "noise-toggle.js"
|
||||
const config = moduleConfig[name] || {};
|
||||
|
||||
if(!fallbackMode && !usedModules.has(name)) {
|
||||
Logger.info(`⏭️ [Module] Skipped (not used in DOM): ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof mod.init === 'function') {
|
||||
mod.init(config);
|
||||
activeModules.set(name, { mod, config });
|
||||
Logger.info(`✅ [Module] Initialized: ${name}`);
|
||||
} else {
|
||||
Logger.warn(`⛔ [Module] No init() in ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (fallbackMode) {
|
||||
Logger.info('⚠️ [Module] No data-module usage detected, fallback to full init mode');
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyModules() {
|
||||
for (const [name, { mod }] of activeModules.entries()) {
|
||||
if (typeof mod.destroy === 'function') {
|
||||
mod.destroy();
|
||||
Logger.info(`🧹 [Module] Destroyed: ${name}`);
|
||||
}
|
||||
}
|
||||
activeModules.clear();
|
||||
}
|
||||
63
resources/js/modules/inertia-scroll/index.js
Normal file
63
resources/js/modules/inertia-scroll/index.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// modules/inertia-scroll/index.js
|
||||
import { registerFrameTask, unregisterFrameTask } from '../../core/frameloop.js';
|
||||
|
||||
let taskId = 'inertia-scroll';
|
||||
let velocity = 0;
|
||||
let lastY = window.scrollY;
|
||||
let active = false;
|
||||
let scrollEndTimer;
|
||||
let damping = 0.9;
|
||||
let minVelocity = 0.2;
|
||||
|
||||
export function init(config = {}) {
|
||||
damping = typeof config.damping === 'number' ? config.damping : 0.9;
|
||||
|
||||
minVelocity = typeof config.minVelocity === 'number' ? contig.minVelocity : 0.1;
|
||||
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
registerFrameTask(taskId, () => {
|
||||
const root = document.documentElement;
|
||||
const scrollY = window.scrollY;
|
||||
const delta = scrollY - lastY;
|
||||
const direction = delta > 0 ? 'down' : delta < 0 ? 'up' : 'none';
|
||||
const speed = Math.abs(delta);
|
||||
|
||||
if (!active && Math.abs(velocity) > minVelocity) {
|
||||
window.scrollTo(0, scrollY + velocity);
|
||||
velocity *= damping; // Trägheit / Dämpfung
|
||||
root.dataset.scrollState = 'inertia';
|
||||
} else if (active) {
|
||||
velocity = delta;
|
||||
lastY = scrollY;
|
||||
root.dataset.scrollState = 'active';
|
||||
} else {
|
||||
delete root.dataset.scrollState;
|
||||
}
|
||||
|
||||
root.dataset.scrollDirection = direction;
|
||||
root.dataset.scrollSpeed = speed.toFixed(2);
|
||||
}, { autoStart: true });
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
active = true;
|
||||
clearTimeout(scrollEndTimer);
|
||||
scrollEndTimer = setTimeout(() => {
|
||||
active = false;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
unregisterFrameTask(taskId);
|
||||
velocity = 0;
|
||||
lastY = window.scrollY;
|
||||
active = false;
|
||||
clearTimeout(scrollEndTimer);
|
||||
|
||||
const root = document.documentElement;
|
||||
delete root.dataset.scrollState;
|
||||
delete root.dataset.scrollDirection;
|
||||
delete root.dataset.scrollSpeed;
|
||||
}
|
||||
26
resources/js/modules/lightbox-trigger/index.js
Normal file
26
resources/js/modules/lightbox-trigger/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// modules/lightbox-trigger/index.js
|
||||
import { UIManager } from '../ui/UIManager.js';
|
||||
import { Logger } from '../../core/logger.js';
|
||||
import { useEvent } from '../../core/useEvent.js';
|
||||
|
||||
function onClick(e) {
|
||||
const img = e.target.closest('[data-lightbox]');
|
||||
if (!img || img.tagName !== 'IMG') return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
UIManager.open('lightbox', {
|
||||
content: `<img src="${img.src}" alt="${img.alt || ''}" />`
|
||||
});
|
||||
}
|
||||
|
||||
export function init() {
|
||||
Logger.info('[lightbox-trigger] init');
|
||||
useEvent(document, 'click', onClick);
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
Logger.info('[lightbox-trigger] destroy');
|
||||
// Automatische Entfernung über EventManager erfolgt über Modulkennung
|
||||
// Kein direkter Aufruf nötig, solange removeModules() global verwendet wird
|
||||
}
|
||||
42
resources/js/modules/noise/index.js
Normal file
42
resources/js/modules/noise/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// js/noise-toggle.js
|
||||
|
||||
import { Logger } from "../../core/logger.js";
|
||||
|
||||
export function init(config = {}) {
|
||||
Logger.log('Noise Toggle Init', config);
|
||||
|
||||
const {
|
||||
selector = ".noise-overlay",
|
||||
toggleKey = "g",
|
||||
className = "grainy",
|
||||
enableTransition = true,
|
||||
} = config;
|
||||
|
||||
const body = document.body;
|
||||
const noiseElement = document.querySelector(selector);
|
||||
|
||||
if (!noiseElement) return;
|
||||
|
||||
const isInput = noiseElement => ["input", "textarea"].includes(noiseElement.tagName.toLowerCase());
|
||||
|
||||
function update() {
|
||||
if (enableTransition) {
|
||||
noiseElement.classList.toggle("hidden", !body.classList.contains(className));
|
||||
} else {
|
||||
noiseElement.style.display = body.classList.contains(className) ? "block" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (
|
||||
e.key.toLowerCase() === toggleKey &&
|
||||
!e.ctrlKey && !e.metaKey && !e.altKey &&
|
||||
!isInput(e.target)
|
||||
) {
|
||||
body.classList.toggle(className);
|
||||
update();
|
||||
}
|
||||
})
|
||||
}
|
||||
27
resources/js/modules/parallax/index.js
Normal file
27
resources/js/modules/parallax/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// modules/parallax/index.js
|
||||
import { Logger } from '../../core/logger.js';
|
||||
import { registerFrameTask } from '../../core/frameloop.js';
|
||||
|
||||
export function init(config = {}) {
|
||||
Logger.info('Parallax init');
|
||||
|
||||
const defaultConfig = {
|
||||
selector: '[data-parallax]',
|
||||
speedAttr: 'data-parallax-speed',
|
||||
defaultSpeed: 0.5
|
||||
};
|
||||
|
||||
const settings = { ...defaultConfig, ...config };
|
||||
const elements = document.querySelectorAll(settings.selector);
|
||||
|
||||
function updateParallax() {
|
||||
const scrollY = window.scrollY;
|
||||
elements.forEach(el => {
|
||||
const speed = parseFloat(el.getAttribute(settings.speedAttr)) || settings.defaultSpeed;
|
||||
const offset = scrollY * speed;
|
||||
el.style.transform = `translateY(${offset}px)`;
|
||||
});
|
||||
}
|
||||
|
||||
registerFrameTask('parallax', updateParallax, { autoStart: true });
|
||||
}
|
||||
66
resources/js/modules/scroll-loop/index.js
Normal file
66
resources/js/modules/scroll-loop/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// modules/scroll-loop/index.js
|
||||
import { registerFrameTask } from '../../core/frameloop.js';
|
||||
|
||||
export function init(config = {}) {
|
||||
const elements = document.querySelectorAll('[data-scroll-loop]');
|
||||
|
||||
elements.forEach(el => {
|
||||
const type = el.dataset.scrollType || 'translate';
|
||||
if (type === 'translate' && el.children.length === 1) {
|
||||
const clone = el.firstElementChild.cloneNode(true);
|
||||
clone.setAttribute('aria-hidden', 'true');
|
||||
el.appendChild(clone);
|
||||
}
|
||||
});
|
||||
|
||||
registerFrameTask('scroll-loop', () => {
|
||||
const scrollY = window.scrollY;
|
||||
const scrollX = window.scrollX;
|
||||
|
||||
elements.forEach(el => {
|
||||
const factor = parseFloat(el.dataset.scrollSpeed || config.speed || 0.2);
|
||||
const axis = el.dataset.scrollAxis || 'y';
|
||||
const type = el.dataset.scrollType || 'translate';
|
||||
const pause = el.dataset.loopPause === 'true';
|
||||
const offsetStart = parseFloat(el.dataset.loopOffset || 0);
|
||||
const limit = parseFloat(el.dataset.loopLimit || 0);
|
||||
|
||||
const input = axis === 'x' ? scrollX : scrollY;
|
||||
if (limit && input > limit) return;
|
||||
if (pause && (el.matches(':hover') || el.matches(':active'))) return;
|
||||
|
||||
const offset = (input + offsetStart) * factor;
|
||||
|
||||
switch (type) {
|
||||
case 'translate': {
|
||||
const base = axis === 'x' ? el.offsetWidth : el.offsetHeight;
|
||||
const value = -(offset % base);
|
||||
const transform = axis === 'x' ? `translateX(${value}px)` : `translateY(${value}px)`;
|
||||
el.style.transform = transform;
|
||||
break;
|
||||
}
|
||||
case 'rotate': {
|
||||
const deg = offset % 360;
|
||||
el.style.transform = `rotate(${deg}deg)`;
|
||||
break;
|
||||
}
|
||||
case 'background': {
|
||||
const pos = offset % 100;
|
||||
if (axis === 'x') {
|
||||
el.style.backgroundPosition = `${pos}% center`;
|
||||
} else {
|
||||
el.style.backgroundPosition = `center ${pos}%`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'scale': {
|
||||
const scale = 1 + Math.sin(offset * 0.01) * 0.1;
|
||||
el.style.transform = `scale(${scale.toFixed(3)})`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}, { autoStart: true });
|
||||
}
|
||||
53
resources/js/modules/scroll-timeline/index.js
Normal file
53
resources/js/modules/scroll-timeline/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// modules/scroll-timeline/index.js
|
||||
import { Logger } from '../../core/logger.js';
|
||||
import { scrollSteps } from './steps.js';
|
||||
import {registerFrameTask, unregisterFrameTask} from '../../core/frameloop.js';
|
||||
|
||||
export function init(userConfig = {}) {
|
||||
Logger.info('ScrollTimeline init');
|
||||
|
||||
const defaultConfig = {
|
||||
attribute: 'data-scroll-step',
|
||||
triggerPoint: 0.4,
|
||||
once: true
|
||||
};
|
||||
|
||||
const config = { ...defaultConfig, ...userConfig };
|
||||
|
||||
const steps = Array.from(document.querySelectorAll(`[${config.attribute}]`)).map(el => ({
|
||||
el,
|
||||
index: parseInt(el.getAttribute(config.attribute), 10),
|
||||
active: false
|
||||
}));
|
||||
|
||||
function update() {
|
||||
const triggerY = window.innerHeight * config.triggerPoint;
|
||||
|
||||
steps.forEach(step => {
|
||||
const rect = step.el.getBoundingClientRect();
|
||||
const isVisible = rect.top < triggerY && rect.bottom > 0;
|
||||
|
||||
if (isVisible && !step.active) {
|
||||
step.active = true;
|
||||
step.el.classList.add('active');
|
||||
Logger.log(`➡️ ENTER step ${step.index}`);
|
||||
scrollSteps.onEnter?.(step.index, step.el);
|
||||
}
|
||||
|
||||
if (!isVisible && step.active) {
|
||||
step.active = false;
|
||||
step.el.classList.remove('active');
|
||||
Logger.log(`⬅️ LEAVE step ${step.index}`);
|
||||
if (!config.once) {
|
||||
scrollSteps.onLeave?.(step.index, step.el);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerFrameTask('scroll-timeline', update, { autoStart: true });
|
||||
}
|
||||
|
||||
export function destroy () {
|
||||
unregisterFrameTask('scroll-timeline');
|
||||
}
|
||||
33
resources/js/modules/scroll-timeline/steps.js
Normal file
33
resources/js/modules/scroll-timeline/steps.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export const scrollSteps = {
|
||||
onEnter(index, el) {
|
||||
el.classList.add('active');
|
||||
document.body.dataset.activeScrollStep = index;
|
||||
console.log(`[ScrollStep] Enter: ${index}`);
|
||||
// Beispielaktionen
|
||||
if (index === 1) showIntro();
|
||||
if (index === 2) activateChart();
|
||||
if (index === 3) revealQuote();
|
||||
},
|
||||
|
||||
onLeave(index, el) {
|
||||
el.classList.remove('active');
|
||||
el.style.transitionDelay = '';
|
||||
console.log(`[ScrollStep] Leave: ${index}`);
|
||||
|
||||
if (document.body.dataset.activeScrollStep === String(index)) {
|
||||
delete document.body.dataset.activeScrollStep;
|
||||
}
|
||||
|
||||
if (index === 1) hideIntro();
|
||||
if (index === 2) deactivateChart();
|
||||
if (index === 3) hideQuote();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function showIntro() { console.log('Intro sichtbar'); }
|
||||
function hideIntro() { console.log('Intro ausgeblendet'); }
|
||||
function activateChart() { console.log('Chart aktiviert'); }
|
||||
function deactivateChart() { console.log('Chart deaktiviert'); }
|
||||
function revealQuote() { console.log('Zitat eingeblendet'); }
|
||||
function hideQuote() { console.log('Zitat ausgeblendet'); }
|
||||
36
resources/js/modules/scrollfx/ScrollEngine.js
Normal file
36
resources/js/modules/scrollfx/ScrollEngine.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// src/resources/js/scrollfx/ScrollEngine.js
|
||||
|
||||
class ScrollEngine {
|
||||
constructor() {
|
||||
this.triggers = new Set();
|
||||
this.viewportHeight = window.innerHeight;
|
||||
this._loop = this._loop.bind(this);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.viewportHeight = window.innerHeight;
|
||||
});
|
||||
|
||||
requestAnimationFrame(this._loop);
|
||||
}
|
||||
|
||||
register(trigger) {
|
||||
this.triggers.add(trigger);
|
||||
}
|
||||
|
||||
unregister(trigger) {
|
||||
this.triggers.delete(trigger);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.triggers.clear();
|
||||
}
|
||||
|
||||
_loop() {
|
||||
this.triggers.forEach(trigger => {
|
||||
trigger.update(this.viewportHeight);
|
||||
});
|
||||
requestAnimationFrame(this._loop);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ScrollEngine();
|
||||
69
resources/js/modules/scrollfx/ScrollTrigger.js
Normal file
69
resources/js/modules/scrollfx/ScrollTrigger.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
177
resources/js/modules/scrollfx/Tween.js
Normal file
177
resources/js/modules/scrollfx/Tween.js
Normal file
@@ -0,0 +1,177 @@
|
||||
// resources/js/scrollfx/Tween.js
|
||||
|
||||
export const Easing = {
|
||||
linear: t => t,
|
||||
easeInQuad: t => t * t,
|
||||
easeOutQuad: t => t * (2 - t),
|
||||
easeInOutQuad: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
|
||||
};
|
||||
|
||||
function interpolate(start, end, t) {
|
||||
return start + (end - start) * t;
|
||||
}
|
||||
|
||||
function parseTransform(transform) {
|
||||
const result = {
|
||||
translateY: 0,
|
||||
scale: 1,
|
||||
rotate: 0
|
||||
};
|
||||
if (!transform || transform === 'none') return result;
|
||||
|
||||
const translateMatch = transform.match(/translateY\((-?\d+(?:\.\d+)?)px\)/);
|
||||
const scaleMatch = transform.match(/scale\((-?\d+(?:\.\d+)?)\)/);
|
||||
const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
|
||||
|
||||
if (translateMatch) result.translateY = parseFloat(translateMatch[1]);
|
||||
if (scaleMatch) result.scale = parseFloat(scaleMatch[1]);
|
||||
if (rotateMatch) result.rotate = parseFloat(rotateMatch[1]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function tweenTo(el, props = {}, duration = 300, easing = Easing.linear) {
|
||||
const start = performance.now();
|
||||
const initial = {};
|
||||
|
||||
const currentTransform = parseTransform(getComputedStyle(el).transform);
|
||||
|
||||
for (const key in props) {
|
||||
if (['translateY', 'scale', 'rotate'].includes(key)) {
|
||||
initial[key] = currentTransform[key];
|
||||
} else {
|
||||
const current = parseFloat(getComputedStyle(el)[key]) || 0;
|
||||
initial[key] = current;
|
||||
}
|
||||
}
|
||||
|
||||
function animate(now) {
|
||||
const t = Math.min((now - start) / duration, 1);
|
||||
const eased = easing(t);
|
||||
|
||||
let transformParts = [];
|
||||
|
||||
for (const key in props) {
|
||||
const startValue = initial[key];
|
||||
const endValue = parseFloat(props[key]);
|
||||
const current = interpolate(startValue, endValue, eased);
|
||||
|
||||
if (key === 'translateY') transformParts.push(`translateY(${current}px)`);
|
||||
else if (key === 'scale') transformParts.push(`scale(${current})`);
|
||||
else if (key === 'rotate') transformParts.push(`rotate(${current}deg)`);
|
||||
else el.style[key] = current + (key === 'opacity' ? '' : 'px');
|
||||
}
|
||||
|
||||
if (transformParts.length > 0) {
|
||||
el.style.transform = transformParts.join(' ');
|
||||
}
|
||||
|
||||
if (t < 1) requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
export function timeline(steps = []) {
|
||||
let index = 0;
|
||||
|
||||
function runNext() {
|
||||
if (index >= steps.length) return;
|
||||
const { el, props, duration, easing = Easing.linear, delay = 0 } = steps[index++];
|
||||
setTimeout(() => {
|
||||
tweenTo(el, props, duration, easing);
|
||||
setTimeout(runNext, duration);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
runNext();
|
||||
}
|
||||
|
||||
export function tweenFromTo(el, from = {}, to = {}, duration = 300, easing = Easing.linear) {
|
||||
for (const key in from) {
|
||||
if (['translateY', 'scale', 'rotate'].includes(key)) {
|
||||
el.style.transform = `${key}(${from[key]}${key === 'rotate' ? 'deg' : key === 'translateY' ? 'px' : ''})`;
|
||||
} else {
|
||||
el.style[key] = from[key] + (key === 'opacity' ? '' : 'px');
|
||||
}
|
||||
}
|
||||
tweenTo(el, to, duration, easing);
|
||||
}
|
||||
|
||||
// === Utility Animations ===
|
||||
|
||||
export function fadeIn(el, duration = 400, easing = Easing.easeOutQuad) {
|
||||
el.classList.remove('fade-out');
|
||||
el.classList.add('fade-in');
|
||||
tweenTo(el, { opacity: 1 }, duration, easing);
|
||||
}
|
||||
|
||||
export function fadeOut(el, duration = 400, easing = Easing.easeInQuad) {
|
||||
el.classList.remove('fade-in');
|
||||
el.classList.add('fade-out');
|
||||
tweenTo(el, { opacity: 0 }, duration, easing);
|
||||
}
|
||||
|
||||
export function zoomIn(el, duration = 500, easing = Easing.easeOutQuad) {
|
||||
el.classList.add('zoom-in');
|
||||
tweenTo(el, { opacity: 1, scale: 1 }, duration, easing);
|
||||
}
|
||||
|
||||
export function zoomOut(el, duration = 500, easing = Easing.easeInQuad) {
|
||||
el.classList.remove('zoom-in');
|
||||
tweenTo(el, { opacity: 0, scale: 0.8 }, duration, easing);
|
||||
}
|
||||
|
||||
export function triggerCssAnimation(el, className, duration = 1000) {
|
||||
el.classList.add(className);
|
||||
setTimeout(() => {
|
||||
el.classList.remove(className);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// === ScrollTrigger Presets ===
|
||||
|
||||
import { createTrigger } from './index.js';
|
||||
|
||||
export function fadeScrollTrigger(selector, options = {}) {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach(el => {
|
||||
createTrigger({
|
||||
element: el,
|
||||
start: options.start || 'top 80%',
|
||||
end: options.end || 'bottom 20%',
|
||||
onEnter: () => fadeIn(el),
|
||||
onLeave: () => fadeOut(el)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function zoomScrollTrigger(selector, options = {}) {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach(el => {
|
||||
createTrigger({
|
||||
element: el,
|
||||
start: options.start || 'top 80%',
|
||||
end: options.end || 'bottom 20%',
|
||||
onEnter: () => zoomIn(el),
|
||||
onLeave: () => zoomOut(el)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function fixedZoomScrollTrigger(selector, options = {}) {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach(el => {
|
||||
el.style.willChange = 'transform, opacity';
|
||||
el.style.opacity = 0;
|
||||
el.style.transform = 'scale(0.8)';
|
||||
|
||||
createTrigger({
|
||||
element: el,
|
||||
start: options.start || 'top 100%',
|
||||
end: options.end || 'top 40%',
|
||||
onEnter: () => zoomIn(el, 600),
|
||||
onLeave: () => zoomOut(el, 400)
|
||||
});
|
||||
});
|
||||
}
|
||||
57
resources/js/modules/scrollfx/index.js
Normal file
57
resources/js/modules/scrollfx/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// src/resources/js/scrollfx/index.js
|
||||
|
||||
import ScrollEngine from './ScrollEngine.js';
|
||||
import ScrollTrigger from './ScrollTrigger.js';
|
||||
import {fadeScrollTrigger, fixedZoomScrollTrigger, zoomScrollTrigger} from "./Tween";
|
||||
|
||||
export function createTrigger(config) {
|
||||
const elements = typeof config.element === 'string'
|
||||
? document.querySelectorAll(config.element)
|
||||
: [config.element];
|
||||
|
||||
const triggers = [];
|
||||
|
||||
elements.forEach(el => {
|
||||
const trigger = new ScrollTrigger({ ...config, element: el });
|
||||
ScrollEngine.register(trigger);
|
||||
triggers.push(trigger);
|
||||
});
|
||||
|
||||
return triggers.length === 1 ? triggers[0] : triggers;
|
||||
}
|
||||
|
||||
export function init(config = {}) {
|
||||
const {
|
||||
selector = '.fade-in-on-scroll, .zoom-in, .fade-out, .fade',
|
||||
offset = 0.85, // z.B. 85 % Viewport-Höhe
|
||||
baseDelay = 0.05, // delay pro Element (stagger)
|
||||
once = true // nur 1× triggern?
|
||||
} = config;
|
||||
|
||||
const elements = Array.from(document.querySelectorAll(selector))
|
||||
.map(el => ({ el, triggered: false }));
|
||||
|
||||
function update() {
|
||||
const triggerY = window.innerHeight * offset;
|
||||
|
||||
elements.forEach((obj, index) => {
|
||||
if (obj.triggered && once) return;
|
||||
|
||||
const rect = obj.el.getBoundingClientRect();
|
||||
|
||||
if (rect.top < triggerY) {
|
||||
obj.el.style.transitionDelay = `${index * baseDelay}s`;
|
||||
obj.el.classList.add('visible', 'entered');
|
||||
obj.el.classList.remove('fade-out');
|
||||
obj.triggered = true;
|
||||
} else if (!once) {
|
||||
obj.el.classList.remove('visible', 'entered');
|
||||
obj.triggered = false;
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
62
resources/js/modules/sidebar/index.js
Normal file
62
resources/js/modules/sidebar/index.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import {useEvent} from "../../core";
|
||||
import {removeModules} from "../../core/removeModules";
|
||||
|
||||
let keydownHandler = null;
|
||||
let clickHandler = null;
|
||||
|
||||
export function init() {
|
||||
const el = document.getElementById("sidebar-menu");
|
||||
const button = document.getElementById('menu-toggle');
|
||||
const aside = document.getElementById('sidebar');
|
||||
const backdrop = document.querySelector('.backdrop');
|
||||
|
||||
const footer = document.querySelector('footer');
|
||||
const headerLink = document.querySelector('header a');
|
||||
|
||||
|
||||
useEvent(button, 'click', (e) => {
|
||||
aside.classList.toggle('show');
|
||||
//aside.toggleAttribute('inert')
|
||||
|
||||
let isVisible = aside.classList.contains('show');
|
||||
|
||||
if (isVisible) {
|
||||
backdrop.classList.add('visible');
|
||||
|
||||
footer.setAttribute('inert', 'true');
|
||||
headerLink.setAttribute('inert', 'true');
|
||||
} else {
|
||||
backdrop.classList.remove('visible');
|
||||
|
||||
footer.removeAttribute('inert');
|
||||
headerLink.removeAttribute('inert');
|
||||
}
|
||||
})
|
||||
|
||||
keydownHandler = (e) => {
|
||||
if(e.key === 'Escape'){
|
||||
if(aside.classList.contains('show')){
|
||||
aside.classList.remove('show');
|
||||
backdrop.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEvent(document, 'keydown', keydownHandler)
|
||||
|
||||
clickHandler = (e) => {
|
||||
aside.classList.remove('show');
|
||||
backdrop.classList.remove('visible');
|
||||
|
||||
footer.removeAttribute('inert');
|
||||
headerLink.removeAttribute('inert');
|
||||
}
|
||||
|
||||
useEvent(backdrop, 'click' , clickHandler)
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
/*document.removeEventListener('keydown', keydownHandler);
|
||||
document.removeEventListener('click', clickHandler);*/
|
||||
removeModules('sidebar');
|
||||
}
|
||||
118
resources/js/modules/smooth-scroll/index.js
Normal file
118
resources/js/modules/smooth-scroll/index.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// modules/smooth-scroll/index.js
|
||||
import { Logger } from '../../core/logger.js';
|
||||
import { registerFrameTask } from '../../core/frameloop.js';
|
||||
|
||||
/*
|
||||
export function init(config = {}) {
|
||||
Logger.info('SmoothScroll init');
|
||||
|
||||
const defaultConfig = {
|
||||
speed: 0.12,
|
||||
scrollTarget: window,
|
||||
interceptWheel: true,
|
||||
interceptTouch: true,
|
||||
interceptKeys: true
|
||||
};
|
||||
|
||||
const settings = { ...defaultConfig, ...config };
|
||||
const scrollElement = settings.scrollTarget === window ? document.scrollingElement : settings.scrollTarget;
|
||||
|
||||
let current = scrollElement.scrollTop;
|
||||
let target = current;
|
||||
|
||||
function clampTarget() {
|
||||
const maxScroll = scrollElement.scrollHeight - window.innerHeight;
|
||||
target = Math.max(0, Math.min(target, maxScroll));
|
||||
}
|
||||
|
||||
function update() {
|
||||
const delta = target - current;
|
||||
current += delta * settings.speed;
|
||||
|
||||
const maxScroll = scrollElement.scrollHeight - window.innerHeight;
|
||||
current = Math.max(0, Math.min(current, maxScroll));
|
||||
|
||||
scrollElement.scrollTop = current;
|
||||
}
|
||||
|
||||
registerFrameTask('smooth-scroll', update, { autoStart: true });
|
||||
|
||||
function triggerScroll(immediate = false) {
|
||||
clampTarget();
|
||||
if (immediate) scrollElement.scrollTop = target;
|
||||
}
|
||||
|
||||
function onWheel(e) {
|
||||
if (!settings.interceptWheel) return;
|
||||
e.preventDefault();
|
||||
target += e.deltaY;
|
||||
triggerScroll(true);
|
||||
}
|
||||
|
||||
let lastTouchY = 0;
|
||||
|
||||
function onTouchStart(e) {
|
||||
if (!settings.interceptTouch) return;
|
||||
lastTouchY = e.touches[0].clientY;
|
||||
}
|
||||
|
||||
function onTouchMove(e) {
|
||||
if (!settings.interceptTouch) return;
|
||||
e.preventDefault();
|
||||
const touch = e.changedTouches[0];
|
||||
const dy = lastTouchY - touch.clientY;
|
||||
target += dy;
|
||||
lastTouchY = touch.clientY;
|
||||
triggerScroll(true);
|
||||
}
|
||||
|
||||
function onKeyDown(e) {
|
||||
if (!settings.interceptKeys) return;
|
||||
const keyScrollAmount = 60;
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
target += keyScrollAmount;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
target -= keyScrollAmount;
|
||||
break;
|
||||
case 'PageDown':
|
||||
e.preventDefault();
|
||||
target += window.innerHeight * 0.9;
|
||||
break;
|
||||
case 'PageUp':
|
||||
e.preventDefault();
|
||||
target -= window.innerHeight * 0.9;
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
target = 0;
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
target = scrollElement.scrollHeight;
|
||||
break;
|
||||
}
|
||||
triggerScroll(true);
|
||||
}
|
||||
|
||||
if (settings.interceptWheel) {
|
||||
window.addEventListener('wheel', onWheel, { passive: false });
|
||||
}
|
||||
|
||||
if (settings.interceptTouch) {
|
||||
window.addEventListener('touchstart', onTouchStart, { passive: false });
|
||||
window.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
}
|
||||
|
||||
if (settings.interceptKeys) {
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
}
|
||||
|
||||
// initial scroll alignment
|
||||
target = scrollElement.scrollTop;
|
||||
current = target;
|
||||
}
|
||||
*/
|
||||
67
resources/js/modules/sticky-fade/index.js
Normal file
67
resources/js/modules/sticky-fade/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// modules/sticky-fade/index.js
|
||||
import { registerFrameTask, unregisterFrameTask } from '../../core/frameloop.js';
|
||||
|
||||
let taskId = 'sticky-fade';
|
||||
let elements = [];
|
||||
let lastScrollY = window.scrollY;
|
||||
let activeMap = new WeakMap();
|
||||
let configCache = {
|
||||
direction: false,
|
||||
reset: false,
|
||||
};
|
||||
|
||||
|
||||
export function init(config = {}) {
|
||||
elements = Array.from(document.querySelectorAll('[data-sticky-fade]'));
|
||||
if (elements.length === 0) return;
|
||||
|
||||
configCache.direction = config.direction ?? false;
|
||||
configCache.reset = config.reset ?? false;
|
||||
|
||||
registerFrameTask(taskId, () => {
|
||||
const scrollY = window.scrollY;
|
||||
const direction = scrollY > lastScrollY ? 'down' : scrollY < lastScrollY ? 'up' : 'none';
|
||||
lastScrollY = scrollY;
|
||||
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
elements.forEach(el => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const progress = 1 - Math.min(Math.max(rect.top / viewportHeight, 0), 1);
|
||||
|
||||
el.style.opacity = progress.toFixed(3);
|
||||
el.style.transform = `translateY(${(1 - progress) * 20}px)`;
|
||||
|
||||
if(configCache.direction) {
|
||||
el.dataset.scrollDir = direction;
|
||||
}
|
||||
|
||||
if (configCache.reset) {
|
||||
const isVisible = progress >= 1;
|
||||
const wasActive = activeMap.get(el) || false;
|
||||
|
||||
if(isVisible && !wasActive) {
|
||||
el.classList.add('visible');
|
||||
activeMap.set(el, true);
|
||||
} else if(!isVisible && wasActive) {
|
||||
el.classList.remove('visible');
|
||||
activeMap.set(el, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { autoStart: true });
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
unregisterFrameTask(taskId);
|
||||
|
||||
elements.forEach(el => {
|
||||
el.style.opacity = '';
|
||||
el.style.transform = '';
|
||||
el.classList.remove('visible');
|
||||
delete el.dataset.scrollDir;
|
||||
});
|
||||
|
||||
elements = [];
|
||||
activeMap = new WeakMap();
|
||||
}
|
||||
45
resources/js/modules/sticky-steps/index.js
Normal file
45
resources/js/modules/sticky-steps/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// modules/sticky-steps/index.js
|
||||
import { Logger } from '../../core/logger.js';
|
||||
import {registerFrameTask, unregisterFrameTask} from '../../core/frameloop.js';
|
||||
|
||||
export function init(config = {}) {
|
||||
Logger.info('StickySteps init');
|
||||
|
||||
const defaultConfig = {
|
||||
containerSelector: '[data-sticky-container]',
|
||||
stepSelector: '[data-sticky-step]',
|
||||
activeClass: 'is-sticky-active',
|
||||
datasetKey: 'activeStickyStep'
|
||||
};
|
||||
|
||||
const settings = { ...defaultConfig, ...config };
|
||||
const containers = document.querySelectorAll(settings.containerSelector);
|
||||
|
||||
containers.forEach(container => {
|
||||
const steps = container.querySelectorAll(settings.stepSelector);
|
||||
const containerOffsetTop = container.offsetTop;
|
||||
|
||||
function update() {
|
||||
const scrollY = window.scrollY;
|
||||
const containerHeight = container.offsetHeight;
|
||||
|
||||
steps.forEach((step, index) => {
|
||||
const stepOffset = containerOffsetTop + index * (containerHeight / steps.length);
|
||||
const nextStepOffset = containerOffsetTop + (index + 1) * (containerHeight / steps.length);
|
||||
|
||||
const isActive = scrollY >= stepOffset && scrollY < nextStepOffset;
|
||||
step.classList.toggle(settings.activeClass, isActive);
|
||||
|
||||
if (isActive) {
|
||||
container.dataset[settings.datasetKey] = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerFrameTask(`sticky-steps-${container.dataset.moduleId || Math.random()}`, update, { autoStart: true });
|
||||
});
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
unregisterFrameTask('sticky-steps');
|
||||
}
|
||||
23
resources/js/modules/ui/UIManager.js
Normal file
23
resources/js/modules/ui/UIManager.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// modules/ui/UIManager.js
|
||||
import { Modal } from './components/Modal.js';
|
||||
|
||||
const components = {
|
||||
modal: Modal,
|
||||
};
|
||||
|
||||
export const UIManager = {
|
||||
open(type, props = {}) {
|
||||
const Component = components[type];
|
||||
if (!Component) {
|
||||
console.warn(`[UIManager] Unknown type: ${type}`);
|
||||
return null;
|
||||
}
|
||||
const instance = new Component(props);
|
||||
instance.open();
|
||||
return instance;
|
||||
},
|
||||
|
||||
close(instance) {
|
||||
if (instance?.close) instance.close();
|
||||
}
|
||||
};
|
||||
41
resources/js/modules/ui/components/Dialog.js
Normal file
41
resources/js/modules/ui/components/Dialog.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import {useEvent} from "../../../core/useEvent";
|
||||
|
||||
export class Dialog {
|
||||
constructor({content = '', className = '', onClose = null} = {}) {
|
||||
this.onClose = onClose;
|
||||
this.dialog = document.createElement('dialog');
|
||||
this.dialog.className = className;
|
||||
this.dialog.innerHTML = `
|
||||
<form method="dialog" class="${className}-content">
|
||||
${content}
|
||||
<button class="${className}-close" value="close">×</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
useEvent(this.dialog, 'click', (e) => {
|
||||
const isOutside = !e.target.closest(className+'-content');
|
||||
if (isOutside) this.close();
|
||||
})
|
||||
|
||||
useEvent(this.dialog, 'cancel', (e) => {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
open()
|
||||
{
|
||||
document.body.appendChild(this.dialog);
|
||||
this.dialog.showModal?.() || this.dialog.setAttribute('open', '');
|
||||
document.documentElement.dataset[`${this.dialog.className}Open`] = 'true';
|
||||
}
|
||||
|
||||
close()
|
||||
{
|
||||
this.dialog.close?.() || this.dialog.removeAttribute('open');
|
||||
this.dialog.remove();
|
||||
delete document.documentElement.dataset[`${this.dialog.className}Open`];
|
||||
this.onClose?.();
|
||||
}
|
||||
}
|
||||
6
resources/js/modules/ui/components/Lightbox.js
Normal file
6
resources/js/modules/ui/components/Lightbox.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Dialog } from './Dialog.js';
|
||||
export class Lightbox extends Dialog {
|
||||
constructor(props) {
|
||||
super({ ...props, className: 'lightbox' });
|
||||
}
|
||||
}
|
||||
6
resources/js/modules/ui/components/Modal.js
Normal file
6
resources/js/modules/ui/components/Modal.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Dialog } from './Dialog.js';
|
||||
export class Modal extends Dialog {
|
||||
constructor(props) {
|
||||
super({ ...props, className: 'modal' });
|
||||
}
|
||||
}
|
||||
9
resources/js/modules/ui/index.js
Normal file
9
resources/js/modules/ui/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { UIManager } from './UIManager.js';
|
||||
|
||||
export function init() {
|
||||
|
||||
/*UIManager.open('modal', {
|
||||
content: '<p>Hallo!</p><button class="modal-close">OK</button>',
|
||||
onClose: () => console.log('Modal wurde geschlossen')
|
||||
});*/
|
||||
}
|
||||
40
resources/js/modules/wheel-boost/index.js
Normal file
40
resources/js/modules/wheel-boost/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// modules/wheel-boost/index.js
|
||||
import { registerFrameTask, unregisterFrameTask } from '../../core/frameloop.js';
|
||||
|
||||
/*
|
||||
let velocity = 0;
|
||||
let taskId = 'wheel-boost';
|
||||
let damping = 0.85;
|
||||
let boost = 1.2;
|
||||
let enabled = true;
|
||||
|
||||
export function init(config = {}) {
|
||||
damping = typeof config.damping === 'number' ? config.damping : 0.85;
|
||||
boost = typeof config.boost === 'number' ? config.boost : 1.2;
|
||||
enabled = config.enabled !== false;
|
||||
|
||||
if (!enabled) return;
|
||||
|
||||
window.addEventListener('wheel', onWheel, { passive: false });
|
||||
|
||||
registerFrameTask(taskId, () => {
|
||||
if (Math.abs(velocity) > 0.1) {
|
||||
window.scrollBy(0, velocity);
|
||||
velocity *= damping;
|
||||
} else {
|
||||
velocity = 0;
|
||||
}
|
||||
}, { autoStart: true });
|
||||
}
|
||||
|
||||
function onWheel(e) {
|
||||
velocity += e.deltaY * boost;
|
||||
e.preventDefault(); // verhindert das native Scrollen
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
window.removeEventListener('wheel', onWheel);
|
||||
unregisterFrameTask(taskId);
|
||||
velocity = 0;
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user