thats-me/test/anime-points-animation.html
2025-04-10 17:26:38 +02:00

1060 lines
49 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welle Hintergrund (SVG)</title>
<style type="text/css">
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f0f0f0;
margin: 0;
font-family: sans-serif;
}
h1 {
margin-bottom: 30px;
}
.svg-container {
width: 96%;
margin: 0 auto;
overflow: hidden;
border: 1px solid #ccc;
}
svg {
/* Optional: Rahmen zur Visualisierung */
overflow: visible; /* Damit Punkte am Rand nicht abgeschnitten werden */
cursor: grab; /* Zeigt an, dass der SVG verschiebbar ist */
}
.point {
fill: #000000; /* Farbe der Punkte */
opacity: 0.8; /* Punkte sichtbar machen */
r: 3.5; /* Einheitliche Punktgröße */
}
.line {
fill: none; /* Keine Füllung für die Linie */
/* stroke-width wird jetzt dynamisch gesetzt */
}
.offset-spline {
fill: none;
/* stroke-width wird jetzt dynamisch gesetzt */
stroke-opacity: 0.6; /* Angepasste Transparenz */
transition: stroke-opacity 0.3s ease;
}
.offset-spline-far {
fill: none;
/* stroke-width wird jetzt dynamisch gesetzt */
stroke-opacity: 0.4; /* Höhere Transparenz */
transition: stroke-opacity 0.3s ease;
}
.offset-spline-far-2 {
fill: none;
/* stroke-width wird jetzt dynamisch gesetzt */
stroke-opacity: 0.2; /* Höhere Transparenz */
transition: stroke-opacity 0.3s ease;
}
/* Steuerelemente für die Slider */
.controls {
margin-top: 20px;
padding: 15px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: 600px;
max-width: 100%;
}
.control-group {
margin-bottom: 15px;
}
.control-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.control-label {
width: 140px;
font-weight: bold;
}
.control-slider {
flex-grow: 1;
margin: 0 10px;
}
.control-value {
width: 50px;
text-align: right;
}
input[type="range"] {
width: 100%;
}
input[type="number"] {
width: 60px;
text-align: center;
}
.control-header {
font-weight: bold;
margin-bottom: 10px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
button {
padding: 8px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.3s;
margin-right: 10px;
}
button:hover {
background-color: #45a049;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 15px;
}
.focus-point {
fill: red;
r: 5;
opacity: 0.7;
pointer-events: none;
}
.zoom-instructions {
font-size: 0.9em;
color: #666;
margin-top: 10px;
text-align: center;
font-style: italic;
}
/* Farbverlauf-Steuerelemente */
.gradient-preview {
width: 100%;
height: 30px;
border-radius: 5px;
overflow: hidden;
margin-bottom: 15px;
border: 1px solid #ddd;
}
#gradient-preview-bar {
width: 100%;
height: 100%;
background: linear-gradient(to right, #e2f5ac 0%, #87f124 35%, #3ec8e8 70%, #04a2fe 100%);
}
.color-picker {
height: 30px;
width: 60px;
padding: 0;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
.color-stop {
margin-bottom: 8px;
}
</style>
</head>
<body>
<h1>Welleneffekt (SVG)</h1>
<div class="svg-container" style="display: flex; justify-content: center; align-items:center;">
<svg width="100%" height="400" id="point-canvas">
<defs>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(226, 245, 172);stop-opacity:1" />
<stop offset="35%" style="stop-color:rgb(135, 241, 36);stop-opacity:1" />
<stop offset="70%" style="stop-color:rgb(62, 200, 232);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(4, 162, 254);stop-opacity:1" />
</linearGradient>
</defs>
<!-- Punkte und Linie werden hier per JavaScript hinzugefügt -->
</svg>
</div>
<!-- Steuerelemente hinzufügen -->
<div class="controls">
<div class="control-header">Wellenanimation einstellen</div>
<div class="control-group">
<div class="control-row">
<div class="control-label">Zeitfaktor:</div>
<input type="range" id="time-factor" class="control-slider" min="0.2" max="3.0" step="0.1" value="2.0">
<div class="control-value"><span id="time-factor-value">2.0</span></div>
</div>
<div class="control-row">
<div class="control-label">Amplitude:</div>
<input type="range" id="amplitude-factor" class="control-slider" min="0.2" max="3.0" step="0.1" value="2.5">
<div class="control-value"><span id="amplitude-factor-value">2.5</span></div>
</div>
<div class="control-row">
<div class="control-label">Turbulenz:</div>
<input type="range" id="turbulence" class="control-slider" min="0" max="3" step="0.1" value="0.5">
<div class="control-value"><span id="turbulence-value">0.5</span></div>
</div>
<div class="control-row">
<div class="control-label">Geschwindigkeit:</div>
<input type="range" id="animation-speed" class="control-slider" min="0.2" max="10.0" step="0.1" value="8.0">
<div class="control-value"><span id="animation-speed-value">8.0</span></div>
</div>
<div class="control-row">
<div class="control-label">Zoom:</div>
<input type="range" id="zoom-factor" class="control-slider" min="0.5" max="3.0" step="0.1" value="1.0">
<div class="control-value"><span id="zoom-factor-value">1.0</span></div>
</div>
</div>
<div class="control-header">Farbverlauf</div>
<div class="control-group">
<div class="gradient-preview">
<div id="gradient-preview-bar"></div>
</div>
<div class="control-row color-stop" data-index="0">
<div class="control-label">Start (0%):</div>
<input type="color" class="color-picker" value="#e2f5ac">
</div>
<div class="control-row color-stop" data-index="1">
<div class="control-label">Position (35%):</div>
<input type="color" class="color-picker" value="#87f124">
</div>
<div class="control-row color-stop" data-index="2">
<div class="control-label">Position (70%):</div>
<input type="color" class="color-picker" value="#3ec8e8">
</div>
<div class="control-row color-stop" data-index="3">
<div class="control-label">Ende (100%):</div>
<input type="color" class="color-picker" value="#04a2fe">
</div>
</div>
<div class="control-header">Linieneinstellungen</div>
<div class="control-group">
<div class="control-row">
<div class="control-label">Linienanzahl:</div>
<input type="range" id="total-lines" class="control-slider" min="4" max="24" step="1" value="12">
<div class="control-value"><span id="total-lines-value">12</span></div>
</div>
<div class="control-row">
<div class="control-label">Linienabstand:</div>
<input type="range" id="line-offset" class="control-slider" min="5" max="30" step="1" value="12">
<div class="control-value"><span id="line-offset-value">12</span></div>
</div>
<div class="control-row">
<div class="control-label">Linienspannung:</div>
<input type="range" id="tension" class="control-slider" min="0.5" max="5.0" step="0.1" value="2.5">
<div class="control-value"><span id="tension-value">2.5</span></div>
</div>
<div class="control-row">
<div class="control-label">Hauptlinie Stärke:</div>
<input type="range" id="main-stroke-width" class="control-slider" min="0.1" max="5.0" step="0.1" value="1.0">
<div class="control-value"><span id="main-stroke-width-value">1.0</span></div>
</div>
<div class="control-row">
<div class="control-label">Offset-Linien Stärke:</div>
<input type="range" id="offset-stroke-width" class="control-slider" min="0.1" max="3.0" step="0.1" value="0.5">
<div class="control-value"><span id="offset-stroke-width-value">0.5</span></div>
</div>
</div>
<div class="button-group">
<button id="reset-button">Zurücksetzen</button>
<button id="reset-zoom">Zoom zurücksetzen</button>
<div class="control-value"><span id="zoom-info">100%</span></div>
</div>
<div class="zoom-instructions">
Zoomen mit dem Mausrad direkt unter dem Mauszeiger.
Halte die Maustaste gedrückt und ziehe, um den Inhalt zu verschieben, wenn du hineingezoomt hast.
</div>
</div>
<!-- Anime.js wird vorerst nicht benötigt, kann aber später wieder eingebunden werden -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<script>
// Konstanten und Konfigurationen
const svg = document.getElementById('point-canvas');
const svgNS = "http://www.w3.org/2000/svg";
const config = {
SVG_WIDTH: 600,
SVG_HEIGHT: 400,
MIN_OFFSET_FACTOR: 0.2,
DEFAULT_ZOOM_LEVEL: 2.0, // Standard-Zoom auf 100% geändert für initiale Ansicht
MIN_ZOOM_LEVEL: 0.5,
MAX_ZOOM_LEVEL: 10,
ZOOM_STEP: 0.1,
BASE_POINTS: [ // Basispunkte als Konstante definiert
{ x: 20, y: 100 },
{ x: 150, y: 60 },
{ x: 300, y: 140 },
{ x: 450, y: 60 },
{ x: 580, y: 100 }
],
INITIAL_GRADIENT_STOPS: [
{ offset: "0%", color: "#e2f5ac", opacity: 1 },
{ offset: "35%", color: "#87f124", opacity: 1 },
{ offset: "70%", color: "#3ec8e8", opacity: 1 },
{ offset: "100%", color: "#04a2fe", opacity: 1 }
],
DEFAULT_SETTINGS: { // Standardwerte für Reset
timeFactor: 2.0,
amplitudeFactor: 2.5,
turbulence: 0.5,
TOTAL_LINES: 12,
OFFSET_FAR: 12,
TENSION: 2.5,
animationSpeed: 8.0,
zoomFactor: 1.0,
mainStrokeWidth: 1.0,
offsetStrokeWidth: 0.5,
gradientStops: [
{ offset: "0%", color: "#e2f5ac", opacity: 1 },
{ offset: "35%", color: "#87f124", opacity: 1 },
{ offset: "70%", color: "#3ec8e8", opacity: 1 },
{ offset: "100%", color: "#04a2fe", opacity: 1 }
]
}
};
const state = {
TENSION: config.DEFAULT_SETTINGS.TENSION,
OFFSET_FAR: config.DEFAULT_SETTINGS.OFFSET_FAR,
TOTAL_LINES: config.DEFAULT_SETTINGS.TOTAL_LINES,
gradientStops: JSON.parse(JSON.stringify(config.INITIAL_GRADIENT_STOPS)), // Tiefe Kopie
animationSpeed: config.DEFAULT_SETTINGS.animationSpeed,
zoomFactor: config.DEFAULT_SETTINGS.zoomFactor,
timeFactor: config.DEFAULT_SETTINGS.timeFactor,
amplitudeFactor: config.DEFAULT_SETTINGS.amplitudeFactor,
turbulence: config.DEFAULT_SETTINGS.turbulence,
pointZoomLevel: config.DEFAULT_ZOOM_LEVEL,
focusPointX: config.SVG_WIDTH / 2,
focusPointY: config.SVG_HEIGHT / 2,
mainStrokeWidth: config.DEFAULT_SETTINGS.mainStrokeWidth,
offsetStrokeWidth: config.DEFAULT_SETTINGS.offsetStrokeWidth,
isPanning: false,
panStartX: 0,
panStartY: 0,
viewBoxStartX: 0,
viewBoxStartY: 0,
isMouseOver: false,
mouseX: 0,
mouseY: 0,
currentAnimation: null,
continuousAnimation: null,
points: JSON.parse(JSON.stringify(config.BASE_POINTS)), // Tiefe Kopie der Startpunkte
pointElements: [],
pathElements: {}, // Container für Pfad-Referenzen
// Performance-Optimierung - Cache für häufig verwendete Elemente
zoomInfoElement: document.getElementById('zoom-info'),
// DOM-Elemente für Steuerelemente
timeFactorSlider: document.getElementById('time-factor'),
timeFactorValue: document.getElementById('time-factor-value'),
amplitudeFactorSlider: document.getElementById('amplitude-factor'),
amplitudeFactorValue: document.getElementById('amplitude-factor-value'),
turbulenceSlider: document.getElementById('turbulence'),
turbulenceValue: document.getElementById('turbulence-value'),
totalLinesSlider: document.getElementById('total-lines'),
totalLinesValue: document.getElementById('total-lines-value'),
lineOffsetSlider: document.getElementById('line-offset'),
lineOffsetValue: document.getElementById('line-offset-value'),
tensionSlider: document.getElementById('tension'),
tensionValue: document.getElementById('tension-value'),
animationSpeedSlider: document.getElementById('animation-speed'),
animationSpeedValue: document.getElementById('animation-speed-value'),
zoomFactorSlider: document.getElementById('zoom-factor'),
zoomFactorValue: document.getElementById('zoom-factor-value'),
resetButton: document.getElementById('reset-button'),
resetZoomButton: document.getElementById('reset-zoom'),
mainStrokeWidthSlider: document.getElementById('main-stroke-width'),
mainStrokeWidthValue: document.getElementById('main-stroke-width-value'),
offsetStrokeWidthSlider: document.getElementById('offset-stroke-width'),
offsetStrokeWidthValue: document.getElementById('offset-stroke-width-value')
};
// Berechnet Normalen für alle Punkte - optimiert für weniger Wiederholungen
function calculateNormals(points) {
const normals = [];
const length = points.length;
for (let i = 0; i < length; i++) {
let dx, dy;
if (i === 0) {
// Erster Punkt - verwende Richtung zum nächsten Punkt
dx = points[1].x - points[0].x;
dy = points[1].y - points[0].y;
} else if (i === length - 1) {
// Letzter Punkt - verwende Richtung vom vorherigen Punkt
dx = points[i].x - points[i-1].x;
dy = points[i].y - points[i-1].y;
} else {
// Mittlere Punkte - verwende Durchschnitt der Richtungen
dx = points[i+1].x - points[i-1].x;
dy = points[i+1].y - points[i-1].y;
}
const len = Math.sqrt(dx * dx + dy * dy);
// Normale berechnen (senkrecht zur Richtung)
normals.push(len > 0
? { x: -dy / len, y: dx / len }
: { x: 0, y: 0 }
);
}
return normals;
}
// Erzeugt Pfad mit variablem Offset - Optimiert für Lesbarkeit und Effizienz
function generateOffsetSplinePath(originalPoints, normals, offsetDistance) {
if (originalPoints.length < 2) return "";
// Startpunkt des Offset-Pfades
const startNormal = normals[0];
const startOffsetX = originalPoints[0].x + startNormal.x * offsetDistance * config.MIN_OFFSET_FACTOR;
const startOffsetY = originalPoints[0].y + startNormal.y * offsetDistance * config.MIN_OFFSET_FACTOR;
let d = `M ${startOffsetX} ${startOffsetY}`;
for (let i = 0; i < originalPoints.length - 1; i++) {
// Einfachere Referenzen
const p0 = i > 0 ? originalPoints[i - 1] : originalPoints[0];
const p1 = originalPoints[i];
const p2 = originalPoints[i + 1];
const p3 = i < originalPoints.length - 2 ? originalPoints[i + 2] : originalPoints[originalPoints.length - 1];
const n1 = normals[i];
const n2 = normals[i+1];
// Kontrollpunkte für das Bézier-Segment
const cp1x = p1.x + (p2.x - p0.x) / 6 * state.TENSION;
const cp1y = p1.y + (p2.y - p0.y) / 6 * state.TENSION;
const cp2x = p2.x - (p3.x - p1.x) / 6 * state.TENSION;
const cp2y = p2.y - (p3.y - p1.y) / 6 * state.TENSION;
// Offset anwenden
const offsetCp1x = cp1x + n1.x * offsetDistance;
const offsetCp1y = cp1y + n1.y * offsetDistance;
const offsetCp2x = cp2x + n2.x * offsetDistance;
const offsetCp2y = cp2y + n2.y * offsetDistance;
const offsetP2x = p2.x + n2.x * offsetDistance * config.MIN_OFFSET_FACTOR;
const offsetP2y = p2.y + n2.y * offsetDistance * config.MIN_OFFSET_FACTOR;
// Kubisches Bézier-Segment hinzufügen
d += ` C ${offsetCp1x} ${offsetCp1y}, ${offsetCp2x} ${offsetCp2y}, ${offsetP2x} ${offsetP2y}`;
}
return d;
}
// Erstellt oder aktualisiert einen Pfad - unverändert, da effizient
function updateOrCreatePath(existingPath, points, normals, offsetDistance, className, strokeWidth) {
const d = generateOffsetSplinePath(points, normals, offsetDistance);
if (existingPath) {
existingPath.setAttribute('d', d);
existingPath.setAttribute('stroke-width', strokeWidth);
return existingPath;
} else {
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('class', className);
path.setAttribute('stroke', 'url(#lineGradient)');
path.setAttribute('stroke-width', strokeWidth);
path.setAttribute('d', d);
svg.appendChild(path);
return path;
}
}
// Erstellt SVG-Punktelemente - effizient
function createPoints(initialPoints) {
return initialPoints.map(p => {
const circle = document.createElementNS(svgNS, 'circle');
circle.setAttribute('class', 'point');
circle.setAttribute('cx', p.x);
circle.setAttribute('cy', p.y);
circle.setAttribute('r', 3.5);
svg.appendChild(circle);
return circle;
});
}
// Aktualisiert Punktpositionen - einfach und effizient
function updatePointsVisualization(points, pointElements) {
points.forEach((p, i) => {
if (pointElements[i]) {
pointElements[i].setAttribute('cx', p.x);
pointElements[i].setAttribute('cy', p.y);
}
});
}
// Hauptaktualisierungsfunktion für die Animation - optimiert für Leistung
function updateVisualization() {
const normals = calculateNormals(state.points);
const time = Date.now() / 1000;
// Turbulenz-Faktor
const turbulenceFactor = state.turbulence * Math.sin(time * 3);
// Zeichne die Hauptlinie
state.pathElements.main = updateOrCreatePath(state.pathElements.main, state.points, normals, 0, 'line', state.mainStrokeWidth);
// Zeichne mehrere Offset-Linien in unterschiedlichen Abständen
const maxOffset = state.OFFSET_FAR * 2.5 * state.zoomFactor;
for (let i = 1; i <= state.TOTAL_LINES; i++) {
// Berechne einen variablen Offset für jede Linie
const offsetFactor = i / state.TOTAL_LINES;
// Wellenkomponenten vorberechnen
const primaryWave = Math.sin(time * state.timeFactor * 0.8 + i * 0.2) * 2 * state.amplitudeFactor;
const secondaryWave = Math.cos(time * state.timeFactor * 0.5 + i * 0.3) * 2 * state.amplitudeFactor;
const tertiaryWave = Math.sin(time * state.timeFactor * 1.2 + i * 0.15) * turbulenceFactor;
// Kombinierte Wellenkomponenten
const wavePhase1 = primaryWave + tertiaryWave;
const wavePhase2 = secondaryWave + tertiaryWave * 0.7;
// CSS-Klasse basierend auf Abstand auswählen
const className = offsetFactor < 0.3 ? 'offset-spline' :
offsetFactor < 0.6 ? 'offset-spline-far' : 'offset-spline-far-2';
// Positive Offset-Linie
const offsetPositive = maxOffset * offsetFactor + wavePhase1 * state.zoomFactor;
const keyPos = `line_pos_${i}`;
state.pathElements[keyPos] = updateOrCreatePath(
state.pathElements[keyPos],
state.points,
normals,
offsetPositive,
className,
state.offsetStrokeWidth
);
// Negative Offset-Linie
const offsetNegative = -maxOffset * offsetFactor + wavePhase2 * state.zoomFactor;
const keyNeg = `line_neg_${i}`;
state.pathElements[keyNeg] = updateOrCreatePath(
state.pathElements[keyNeg],
state.points,
normals,
offsetNegative,
className,
state.offsetStrokeWidth
);
}
// Aktualisiere Punkte und bringe sie nach vorne
updatePointsVisualization(state.points, state.pointElements);
state.pointElements.forEach(p => svg.appendChild(p));
}
// Animation starten - optimiert mit Funktionsreferenzen
function startAnimation() {
// Bestehende Animationen stoppen
stopAnimations();
// Basis-Animation
state.currentAnimation = anime({
targets: state.points,
y: function(p, i) {
return config.BASE_POINTS[i].y;
},
duration: 100 / state.animationSpeed,
direction: 'alternate',
loop: true,
easing: 'easeInOutSine',
delay: anime.stagger(100 / state.animationSpeed),
update: updateVisualization,
complete: startContinuousAnimation
});
}
// Animationen stoppen - Hilfsfunktion zur Reduzierung von Code-Duplizierung
function stopAnimations() {
anime.remove(state.points);
if (state.currentAnimation) anime.remove(state.currentAnimation);
if (state.continuousAnimation) anime.remove(state.continuousAnimation);
state.currentAnimation = null;
state.continuousAnimation = null;
}
// Kontinuierliche wellenförmige Animation - optimiert für bessere Leistung
function startContinuousAnimation() {
if (state.continuousAnimation) anime.remove(state.continuousAnimation);
state.continuousAnimation = anime({
targets: state.points,
y: function(p, i) {
const baseY = config.BASE_POINTS[i].y;
const amplitude = 20;
return function() {
const t = Date.now() / 1000 * state.animationSpeed;
const dynamicAmplitude = amplitude * state.amplitudeFactor * state.zoomFactor;
// Wellen vorberechnen und kombinieren
const waves = dynamicAmplitude * Math.sin(t * state.timeFactor * 0.8 + i * 0.7) +
dynamicAmplitude/2 * Math.sin(t * state.timeFactor * 1.2 + i * 0.5) +
dynamicAmplitude/4 * Math.sin(t * state.timeFactor * 2.1 + i * 0.3);
// Mauseinfluss hinzufügen
let mouseEffect = 0;
if (state.isMouseOver) {
const dx = p.x - state.mouseX;
const distanceFactor = Math.max(0, 1 - Math.abs(dx) / 200);
mouseEffect = (state.mouseY - baseY) * distanceFactor * 0.3;
}
return baseY + waves + mouseEffect;
};
},
duration: (1000 / state.animationSpeed),
autoplay: true,
easing: 'linear',
loop: true,
update: function() {
requestAnimationFrame(updateVisualization);
}
});
}
// Optimierte Funktion für Mausrad-Zoom
function handleWheelZoom(e) {
e.preventDefault(); // Verhindere Standard-Scroll
// SVG-Koordinaten der Mausposition berechnen
const rect = svg.getBoundingClientRect();
const viewBoxValues = svg.getAttribute('viewBox').split(' ').map(parseFloat);
const scaleX = viewBoxValues[2] / rect.width;
const scaleY = viewBoxValues[3] / rect.height;
// Mausposition im Viewport und SVG-Koordinaten
const viewportX = e.clientX - rect.left;
const viewportY = e.clientY - rect.top;
const svgX = viewportX * scaleX + viewBoxValues[0];
const svgY = viewportY * scaleY + viewBoxValues[1];
// Altes Zoom-Level merken
const oldZoomLevel = state.pointZoomLevel;
// Zoom-Level anpassen
const zoomDelta = Math.abs(e.deltaY) > 50 ? config.ZOOM_STEP * 2 : config.ZOOM_STEP;
if (e.deltaY < 0) {
// Hineinzoomen mit Begrenzung
state.pointZoomLevel = Math.min(config.MAX_ZOOM_LEVEL, state.pointZoomLevel + zoomDelta);
} else {
// Herauszoomen mit Begrenzung
state.pointZoomLevel = Math.max(config.MIN_ZOOM_LEVEL, state.pointZoomLevel - zoomDelta);
}
// Neue ViewBox berechnen
updateViewBoxForZoom(svgX, svgY, oldZoomLevel);
}
// Hilfsfunktion zum Aktualisieren der ViewBox nach Zoom
function updateViewBoxForZoom(targetX, targetY, oldZoomLevel) {
// Neue ViewBox-Dimensionen
const newWidth = config.SVG_WIDTH / state.pointZoomLevel;
const newHeight = config.SVG_HEIGHT / state.pointZoomLevel;
// Verhältnis zwischen altem und neuem Zoom
const zoomRatio = oldZoomLevel / state.pointZoomLevel;
// ViewBox-Position berechnen
const rect = svg.getBoundingClientRect();
const oldViewBox = svg.getAttribute('viewBox').split(' ').map(parseFloat);
const scaleX = oldViewBox[2] / rect.width;
// Korrigierte Berechnung für neue X/Y Position
const mouseOffsetX = targetX - oldViewBox[0];
const mouseOffsetY = targetY - oldViewBox[1];
let newX = targetX - mouseOffsetX * (newWidth / oldViewBox[2]);
let newY = targetY - mouseOffsetY * (newHeight / oldViewBox[3]);
// Begrenze den Bereich
const limitedX = Math.max(0, Math.min(newX, config.SVG_WIDTH - newWidth));
const limitedY = Math.max(0, Math.min(newY, config.SVG_HEIGHT - newHeight));
// ViewBox aktualisieren
svg.setAttribute('viewBox', `${limitedX} ${limitedY} ${newWidth} ${newHeight}`);
// Fokuspunkt und UI aktualisieren
state.focusPointX = targetX;
state.focusPointY = targetY;
updateZoomInfo();
updateCursorStyle();
}
// Aktualisiert die Zoom-Info im UI - extrahiert zur Wiederverwendung
function updateZoomInfo() {
if (state.zoomInfoElement) {
state.zoomInfoElement.textContent = `${(state.pointZoomLevel * 100).toFixed(0)}%`;
}
}
// Zoom zurücksetzen auf Standardwert
function resetPointZoom() {
state.pointZoomLevel = config.DEFAULT_ZOOM_LEVEL;
// Standard-ViewBox berechnen
const zoomedWidth = config.SVG_WIDTH / state.pointZoomLevel;
const zoomedHeight = config.SVG_HEIGHT / state.pointZoomLevel;
const x = (config.SVG_WIDTH - zoomedWidth) / 2;
const y = 0 //(config.SVG_HEIGHT - zoomedHeight) / 2;
// ViewBox setzen
svg.setAttribute('viewBox', `${x} ${y} ${zoomedWidth} ${zoomedHeight}`);
// UI aktualisieren
updateZoomInfo();
updateCursorStyle();
}
// Pan-Funktionen - optimiert für Lesbarkeit und Effizienz
function startPan(e) {
if (state.pointZoomLevel <= 1) return; // Nur bei Zoom verschieben
state.isPanning = true;
svg.style.cursor = 'grabbing';
// Start-Werte speichern
state.panStartX = e.clientX;
state.panStartY = e.clientY;
const viewBoxValues = svg.getAttribute('viewBox').split(' ').map(parseFloat);
state.viewBoxStartX = viewBoxValues[0];
state.viewBoxStartY = viewBoxValues[1];
// Globale Event-Listener hinzufügen
document.addEventListener('mousemove', doPan);
document.addEventListener('mouseup', endPan);
}
function doPan(e) {
if (!state.isPanning) return;
e.preventDefault();
// Verschiebung berechnen
const deltaX = e.clientX - state.panStartX;
const deltaY = e.clientY - state.panStartY;
// Verschiebung in SVG-Koordinaten umrechnen
const rect = svg.getBoundingClientRect();
const viewBoxValues = svg.getAttribute('viewBox').split(' ').map(parseFloat);
const scaleX = viewBoxValues[2] / rect.width;
const scaleY = viewBoxValues[3] / rect.height;
// Neue Position berechnen und begrenzen
let newX = state.viewBoxStartX - deltaX * scaleX;
let newY = state.viewBoxStartY - deltaY * scaleY;
newX = Math.max(0, Math.min(newX, config.SVG_WIDTH - viewBoxValues[2]));
newY = Math.max(0, Math.min(newY, config.SVG_HEIGHT - viewBoxValues[3]));
// ViewBox aktualisieren
svg.setAttribute('viewBox', `${newX} ${newY} ${viewBoxValues[2]} ${viewBoxValues[3]}`);
}
function endPan() {
state.isPanning = false;
updateCursorStyle();
// Event-Listener entfernen
document.removeEventListener('mousemove', doPan);
document.removeEventListener('mouseup', endPan);
}
// Cursor-Stil basierend auf Zoom-Level
function updateCursorStyle() {
svg.style.cursor = state.pointZoomLevel > 1 ?
(state.isPanning ? 'grabbing' : 'grab') :
'default';
}
// Event-Listener für Steuerelemente - optimiert durch Funktionsreferenzen
function setupControlListeners() {
// Slider-Event-Handler
setupSliderListener(state.timeFactorSlider, state.timeFactorValue, val => state.timeFactor = val);
setupSliderListener(state.amplitudeFactorSlider, state.amplitudeFactorValue, val => state.amplitudeFactor = val);
setupSliderListener(state.turbulenceSlider, state.turbulenceValue, val => state.turbulence = val);
setupSliderListener(state.zoomFactorSlider, state.zoomFactorValue, val => state.zoomFactor = val);
setupSliderListener(state.mainStrokeWidthSlider, state.mainStrokeWidthValue, val => { state.mainStrokeWidth = val; updateStrokeWidths(); });
setupSliderListener(state.offsetStrokeWidthSlider, state.offsetStrokeWidthValue, val => { state.offsetStrokeWidth = val; updateStrokeWidths(); });
// Farbauswahl-Event-Handler
document.querySelectorAll('.color-stop').forEach(stopEl => {
const index = parseInt(stopEl.dataset.index);
const colorPicker = stopEl.querySelector('.color-picker');
colorPicker.addEventListener('input', function() {
state.gradientStops[index].color = this.value;
updateGradient();
});
});
// Animation-Speed-Slider mit Neustart
state.animationSpeedSlider.addEventListener('input', function() {
const newSpeed = parseFloat(this.value);
if (newSpeed !== state.animationSpeed) {
state.animationSpeed = newSpeed;
state.animationSpeedValue.textContent = state.animationSpeed.toFixed(1);
// Animation neu starten
stopAnimations();
startAnimation();
}
});
// Linien-Anzahl mit Aufräumen
state.totalLinesSlider.addEventListener('input', function() {
state.TOTAL_LINES = parseInt(this.value);
state.totalLinesValue.textContent = state.TOTAL_LINES;
// Überflüssige Linien entfernen
cleanupUnusedLines();
});
// Einfache Slider ohne Zusatzlogik
setupSliderListener(state.lineOffsetSlider, state.lineOffsetValue, val => state.OFFSET_FAR = parseInt(val), false);
setupSliderListener(state.tensionSlider, state.tensionValue, val => state.TENSION = val);
// Buttons
state.resetZoomButton.addEventListener('click', resetPointZoom);
state.resetButton.addEventListener('click', resetAllSettings);
// Zoom und Pan
svg.addEventListener('wheel', handleWheelZoom, { passive: false });
svg.addEventListener('mousedown', startPan);
// Mouse-Tracking
svg.addEventListener('mouseover', () => state.isMouseOver = true);
svg.addEventListener('mouseout', () => state.isMouseOver = false);
svg.addEventListener('mousemove', updateMousePosition);
}
// Hilfsfunktion zum Aufräumen ungenutzter Linien
function cleanupUnusedLines() {
for (let key in state.pathElements) {
if (key.startsWith('line_pos_') || key.startsWith('line_neg_')) {
const lineNum = parseInt(key.split('_').pop());
if (lineNum > state.TOTAL_LINES) {
if (state.pathElements[key].parentNode) {
state.pathElements[key].parentNode.removeChild(state.pathElements[key]);
}
delete state.pathElements[key];
}
}
}
}
// Hilfsfunktion für Slider-Event-Handler
function setupSliderListener(slider, valueElement, setter, useFloat = true) {
slider.addEventListener('input', function() {
const val = useFloat ? parseFloat(this.value) : this.value;
setter(val);
valueElement.textContent = useFloat ? val.toFixed(1) : val;
});
}
// Mausposition aktualisieren
function updateMousePosition(e) {
const rect = svg.getBoundingClientRect();
state.mouseX = e.clientX - rect.left;
state.mouseY = e.clientY - rect.top;
}
// Hilfsfunktion zum Updaten des SVG-Farbverlaufs
function updateGradient() {
const gradient = document.getElementById('lineGradient');
if (!gradient) return;
// Alle vorhandenen Stops entfernen
while (gradient.firstChild) {
gradient.removeChild(gradient.firstChild);
}
// Neue Stops aus der Konfiguration erstellen
state.gradientStops.forEach(stop => {
const stopElement = document.createElementNS(svgNS, 'stop');
stopElement.setAttribute('offset', stop.offset);
stopElement.setAttribute('style', `stop-color:${stop.color};stop-opacity:${stop.opacity}`);
gradient.appendChild(stopElement);
});
// CSS-Gradient für die Vorschau aktualisieren
updateGradientPreview();
}
// Vorschau im UI aktualisieren
function updateGradientPreview() {
const previewBar = document.getElementById('gradient-preview-bar');
const cssGradient = `linear-gradient(to right, ${
state.gradientStops.map(stop => `${stop.color} ${stop.offset}`).join(', ')
})`;
previewBar.style.background = cssGradient;
}
// Funktion zum Aktualisieren der Strichbreiten aller relevanten Pfade
function updateStrokeWidths() {
if (state.pathElements.main) {
state.pathElements.main.setAttribute('stroke-width', state.mainStrokeWidth);
}
for (let key in state.pathElements) {
if (key.startsWith('line_pos_') || key.startsWith('line_neg_')) {
if (state.pathElements[key]) {
state.pathElements[key].setAttribute('stroke-width', state.offsetStrokeWidth);
}
}
}
}
// Alle Einstellungen zurücksetzen
function resetAllSettings() {
// Standardwerte aus config holen
const defaultSettings = config.DEFAULT_SETTINGS;
// Slider-Werte und State zurücksetzen
state.timeFactorSlider.value = defaultSettings.timeFactor;
state.amplitudeFactorSlider.value = defaultSettings.amplitudeFactor;
state.turbulenceSlider.value = defaultSettings.turbulence;
state.totalLinesSlider.value = defaultSettings.TOTAL_LINES;
state.lineOffsetSlider.value = defaultSettings.OFFSET_FAR;
state.tensionSlider.value = defaultSettings.TENSION;
state.animationSpeedSlider.value = defaultSettings.animationSpeed;
state.zoomFactorSlider.value = defaultSettings.zoomFactor;
state.mainStrokeWidthSlider.value = defaultSettings.mainStrokeWidth;
state.offsetStrokeWidthSlider.value = defaultSettings.offsetStrokeWidth;
// State-Variablen direkt aktualisieren
state.timeFactor = defaultSettings.timeFactor;
state.amplitudeFactor = defaultSettings.amplitudeFactor;
state.turbulence = defaultSettings.turbulence;
state.TOTAL_LINES = defaultSettings.TOTAL_LINES;
state.OFFSET_FAR = defaultSettings.OFFSET_FAR;
state.TENSION = defaultSettings.TENSION;
state.animationSpeed = defaultSettings.animationSpeed;
state.zoomFactor = defaultSettings.zoomFactor;
state.mainStrokeWidth = defaultSettings.mainStrokeWidth;
state.offsetStrokeWidth = defaultSettings.offsetStrokeWidth;
// UI-Anzeigen aktualisieren
updateAllUIValues();
// Farbverlauf zurücksetzen
state.gradientStops = JSON.parse(JSON.stringify(defaultSettings.gradientStops));
state.gradientStops.forEach((stop, index) => {
const colorPicker = document.querySelector(`.color-stop[data-index="${index}"] .color-picker`);
if (colorPicker) colorPicker.value = stop.color;
});
updateGradient();
// Zoom zurücksetzen
resetPointZoom();
// Punkte auf ursprüngliche Positionen zurücksetzen
state.points = JSON.parse(JSON.stringify(config.BASE_POINTS));
// Animation neu starten (mit Standardwerten)
stopAnimations();
startAnimation();
// Sicherstellen, dass ungenutzte Linien entfernt werden
cleanupUnusedLines();
updateVisualization();
}
// Initialisierung - optimiert für Klarheit
function init() {
// Initialen Zoom anwenden
setupInitialViewBox();
// Punkte erzeugen und Listener einrichten
state.pointElements = createPoints(state.points);
setupControlListeners();
// Starte Animation und Visualisierung
updateStrokeWidths();
updateVisualization();
startAnimation();
// Farbverlauf initialisieren
updateGradient();
// UI-Elemente initialisieren
updateAllUIValues();
updateCursorStyle();
}
// Initialen ViewBox-Zustand einrichten
function setupInitialViewBox() {
const initialZoom = config.DEFAULT_ZOOM_LEVEL;
const zoomedWidth = config.SVG_WIDTH / initialZoom;
const zoomedHeight = config.SVG_HEIGHT / initialZoom;
const x = (config.SVG_WIDTH - zoomedWidth) / 2;
const y = 0; //(config.SVG_HEIGHT - zoomedHeight) / 2;
svg.setAttribute('viewBox', `${x} ${y} ${zoomedWidth} ${zoomedHeight}`);
state.pointZoomLevel = initialZoom;
}
// Alle UI-Werte aktualisieren
function updateAllUIValues() {
state.timeFactorValue.textContent = state.timeFactor.toFixed(1);
state.amplitudeFactorValue.textContent = state.amplitudeFactor.toFixed(1);
state.turbulenceValue.textContent = state.turbulence.toFixed(1);
state.animationSpeedValue.textContent = state.animationSpeed.toFixed(1);
state.totalLinesValue.textContent = state.TOTAL_LINES;
state.lineOffsetValue.textContent = state.OFFSET_FAR;
state.tensionValue.textContent = state.TENSION.toFixed(1);
state.zoomFactorValue.textContent = state.zoomFactor.toFixed(1);
state.mainStrokeWidthValue.textContent = state.mainStrokeWidth.toFixed(1);
state.offsetStrokeWidthValue.textContent = state.offsetStrokeWidth.toFixed(1);
updateZoomInfo();
}
// Starte die Anwendung
init();
</script>
</body>
</html>