1060 lines
49 KiB
HTML
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>
|