|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Lissajous Curve Visualization</title> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
} |
|
|
|
:root { |
|
--primary: #6c5ce7; |
|
--secondary: #a29bfe; |
|
--accent: #fd79a8; |
|
--dark: #2d3436; |
|
--light: #f7f7f7; |
|
--success: #00b894; |
|
} |
|
|
|
body { |
|
background: linear-gradient(135deg, #1a1a2e, #16213e); |
|
color: var(--light); |
|
min-height: 100vh; |
|
overflow-x: hidden; |
|
padding: 20px; |
|
} |
|
|
|
.container { |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
} |
|
|
|
header { |
|
text-align: center; |
|
padding: 30px 0; |
|
margin-bottom: 20px; |
|
} |
|
|
|
h1 { |
|
font-size: 3.5rem; |
|
margin-bottom: 10px; |
|
background: linear-gradient(to right, var(--accent), var(--secondary)); |
|
-webkit-background-clip: text; |
|
background-clip: text; |
|
color: transparent; |
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
.subtitle { |
|
font-size: 1.2rem; |
|
color: var(--secondary); |
|
max-width: 700px; |
|
margin: 0 auto 20px; |
|
line-height: 1.6; |
|
} |
|
|
|
.content { |
|
display: grid; |
|
grid-template-columns: 1fr 1fr; |
|
gap: 30px; |
|
margin-bottom: 40px; |
|
} |
|
|
|
@media (max-width: 992px) { |
|
.content { |
|
grid-template-columns: 1fr; |
|
} |
|
} |
|
|
|
.visualization-panel { |
|
background: rgba(255, 255, 255, 0.05); |
|
backdrop-filter: blur(10px); |
|
border-radius: 20px; |
|
padding: 25px; |
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); |
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
} |
|
|
|
.panel-title { |
|
font-size: 1.8rem; |
|
margin-bottom: 20px; |
|
color: var(--accent); |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
} |
|
|
|
.panel-title i { |
|
background: linear-gradient(45deg, var(--primary), var(--accent)); |
|
-webkit-background-clip: text; |
|
background-clip: text; |
|
color: transparent; |
|
} |
|
|
|
.canvas-container { |
|
position: relative; |
|
width: 100%; |
|
height: 500px; |
|
background: rgba(0, 0, 0, 0.2); |
|
border-radius: 15px; |
|
overflow: hidden; |
|
} |
|
|
|
canvas { |
|
display: block; |
|
} |
|
|
|
.controls { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
|
gap: 20px; |
|
margin-top: 30px; |
|
} |
|
|
|
.control-group { |
|
background: rgba(255, 255, 255, 0.08); |
|
padding: 20px; |
|
border-radius: 15px; |
|
transition: transform 0.3s ease, box-shadow 0.3s ease; |
|
} |
|
|
|
.control-group:hover { |
|
transform: translateY(-5px); |
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); |
|
background: rgba(255, 255, 255, 0.12); |
|
} |
|
|
|
.control-group h3 { |
|
margin-bottom: 15px; |
|
color: var(--secondary); |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
} |
|
|
|
.slider-container { |
|
margin-bottom: 15px; |
|
} |
|
|
|
label { |
|
display: block; |
|
margin-bottom: 8px; |
|
font-weight: 500; |
|
display: flex; |
|
justify-content: space-between; |
|
} |
|
|
|
.value { |
|
color: var(--accent); |
|
font-weight: bold; |
|
} |
|
|
|
input[type="range"] { |
|
width: 100%; |
|
height: 8px; |
|
border-radius: 4px; |
|
background: rgba(255, 255, 255, 0.1); |
|
outline: none; |
|
-webkit-appearance: none; |
|
} |
|
|
|
input[type="range"]::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 20px; |
|
height: 20px; |
|
border-radius: 50%; |
|
background: var(--accent); |
|
cursor: pointer; |
|
box-shadow: 0 0 10px rgba(253, 121, 168, 0.5); |
|
} |
|
|
|
.buttons { |
|
display: flex; |
|
gap: 15px; |
|
margin-top: 20px; |
|
flex-wrap: wrap; |
|
} |
|
|
|
button { |
|
flex: 1; |
|
min-width: 120px; |
|
padding: 12px 20px; |
|
border: none; |
|
border-radius: 50px; |
|
background: linear-gradient(45deg, var(--primary), var(--secondary)); |
|
color: white; |
|
font-weight: 600; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
box-shadow: 0 4px 15px rgba(108, 92, 231, 0.3); |
|
} |
|
|
|
button:hover { |
|
transform: translateY(-3px); |
|
box-shadow: 0 6px 20px rgba(108, 92, 231, 0.5); |
|
} |
|
|
|
button:active { |
|
transform: translateY(1px); |
|
} |
|
|
|
button.reset { |
|
background: linear-gradient(45deg, #e17055, #fdcb6e); |
|
box-shadow: 0 4px 15px rgba(225, 112, 85, 0.3); |
|
} |
|
|
|
button.reset:hover { |
|
box-shadow: 0 6px 20px rgba(225, 112, 85, 0.5); |
|
} |
|
|
|
.info-panel { |
|
background: rgba(255, 255, 255, 0.05); |
|
backdrop-filter: blur(10px); |
|
border-radius: 20px; |
|
padding: 25px; |
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); |
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
} |
|
|
|
.info-content { |
|
line-height: 1.8; |
|
} |
|
|
|
.info-content h2 { |
|
color: var(--accent); |
|
margin: 20px 0 15px; |
|
font-size: 1.8rem; |
|
} |
|
|
|
.info-content p { |
|
margin-bottom: 15px; |
|
color: #ddd; |
|
} |
|
|
|
.formula { |
|
background: rgba(0, 0, 0, 0.3); |
|
padding: 20px; |
|
border-radius: 10px; |
|
margin: 20px 0; |
|
font-family: 'Courier New', monospace; |
|
font-size: 1.2rem; |
|
text-align: center; |
|
border-left: 4px solid var(--accent); |
|
} |
|
|
|
.examples { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
|
gap: 15px; |
|
margin-top: 20px; |
|
} |
|
|
|
.example { |
|
background: rgba(255, 255, 255, 0.08); |
|
padding: 15px; |
|
border-radius: 10px; |
|
text-align: center; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.example:hover { |
|
background: rgba(108, 92, 231, 0.2); |
|
transform: translateY(-3px); |
|
} |
|
|
|
.example h4 { |
|
color: var(--secondary); |
|
margin-bottom: 10px; |
|
} |
|
|
|
footer { |
|
text-align: center; |
|
padding: 30px 0; |
|
color: var(--secondary); |
|
font-size: 1rem; |
|
border-top: 1px solid rgba(255, 255, 255, 0.1); |
|
margin-top: 20px; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
h1 { |
|
font-size: 2.5rem; |
|
} |
|
|
|
.canvas-container { |
|
height: 400px; |
|
} |
|
|
|
.controls { |
|
grid-template-columns: 1fr; |
|
} |
|
} |
|
|
|
@media (max-width: 480px) { |
|
h1 { |
|
font-size: 2rem; |
|
} |
|
|
|
.canvas-container { |
|
height: 300px; |
|
} |
|
|
|
.buttons { |
|
flex-direction: column; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<header> |
|
<h1><i class="fas fa-wave-square"></i> Lissajous Curve Visualization</h1> |
|
<p class="subtitle">Explore the fascinating mathematical curves formed by the intersection of two harmonic motions at right angles. Adjust parameters to see how the curve changes in real-time.</p> |
|
</header> |
|
|
|
<div class="content"> |
|
<div class="visualization-panel"> |
|
<h2 class="panel-title"><i class="fas fa-chart-line"></i> Curve Visualization</h2> |
|
<div class="canvas-container"> |
|
<canvas id="lissajousCanvas"></canvas> |
|
</div> |
|
|
|
<div class="controls"> |
|
<div class="control-group"> |
|
<h3><i class="fas fa-sliders-h"></i> Parameters</h3> |
|
<div class="slider-container"> |
|
<label for="aSlider"> |
|
A Ratio: <span id="aValue" class="value">3</span> |
|
</label> |
|
<input type="range" id="aSlider" min="1" max="10" value="3" step="0.1"> |
|
</div> |
|
<div class="slider-container"> |
|
<label for="bSlider"> |
|
B Ratio: <span id="bValue" class="value">2</span> |
|
</label> |
|
<input type="range" id="bSlider" min="1" max="10" value="2" step="0.1"> |
|
</div> |
|
<div class="slider-container"> |
|
<label for="deltaSlider"> |
|
Phase Difference (δ): <span id="deltaValue" class="value">π/2</span> |
|
</label> |
|
<input type="range" id="deltaSlider" min="0" max="6.28" value="1.57" step="0.01"> |
|
</div> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<h3><i class="fas fa-palette"></i> Appearance</h3> |
|
<div class="slider-container"> |
|
<label for="speedSlider"> |
|
Animation Speed: <span id="speedValue" class="value">1.0</span>x |
|
</label> |
|
<input type="range" id="speedSlider" min="0.1" max="3" value="1" step="0.1"> |
|
</div> |
|
<div class="slider-container"> |
|
<label for="trailSlider"> |
|
Trail Length: <span id="trailValue" class="value">500</span> |
|
</label> |
|
<input type="range" id="trailSlider" min="50" max="2000" value="500" step="10"> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="buttons"> |
|
<button id="pauseBtn"><i class="fas fa-pause"></i> Pause</button> |
|
<button id="resetBtn" class="reset"><i class="fas fa-redo"></i> Reset</button> |
|
<button id="clearBtn"><i class="fas fa-eraser"></i> Clear</button> |
|
</div> |
|
</div> |
|
|
|
<div class="info-panel"> |
|
<h2 class="panel-title"><i class="fas fa-info-circle"></i> About Lissajous Curves</h2> |
|
<div class="info-content"> |
|
<p>Lissajous curves, also known as Lissajous figures or Bowditch curves, are the graphs of a system of parametric equations that describe complex harmonic motion.</p> |
|
|
|
<h2>Mathematical Formula</h2> |
|
<div class="formula"> |
|
x = A × sin(a × t + δ)<br> |
|
y = B × sin(b × t) |
|
</div> |
|
|
|
<p>Where:</p> |
|
<ul> |
|
<li><strong>A, B</strong>: Amplitudes of the waves</li> |
|
<li><strong>a, b</strong>: Frequencies of the waves</li> |
|
<li><strong>δ</strong>: Phase difference between the waves</li> |
|
<li><strong>t</strong>: Time parameter</li> |
|
</ul> |
|
|
|
<h2>Common Examples</h2> |
|
<div class="examples"> |
|
<div class="example" data-a="1" data-b="1" data-d="0"> |
|
<h4>Circle</h4> |
|
<p>a=1, b=1, δ=0</p> |
|
</div> |
|
<div class="example" data-a="1" data-b="1" data-d="1.57"> |
|
<h4>Ellipse</h4> |
|
<p>a=1, b=1, δ=π/2</p> |
|
</div> |
|
<div class="example" data-a="3" data-b="2" data-d="1.57"> |
|
<h4>Classic Curve</h4> |
|
<p>a=3, b=2, δ=π/2</p> |
|
</div> |
|
<div class="example" data-a="5" data-b="4" data-d="1.57"> |
|
<h4>Complex Pattern</h4> |
|
<p>a=5, b=4, δ=π/2</p> |
|
</div> |
|
</div> |
|
|
|
<h2>Applications</h2> |
|
<p>Lissajous curves have applications in physics, astronomy, and engineering:</p> |
|
<ul> |
|
<li>Oscilloscopes for signal analysis</li> |
|
<li>Harmonic oscillators in mechanics</li> |
|
<li>Optics and wave interference</li> |
|
<li>Art and design patterns</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<footer> |
|
<p>Lissajous Curve Visualization | Mathematical Beauty in Motion</p> |
|
</footer> |
|
</div> |
|
|
|
<script> |
|
|
|
const canvas = document.getElementById('lissajousCanvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
function resizeCanvas() { |
|
const container = canvas.parentElement; |
|
canvas.width = container.clientWidth; |
|
canvas.height = container.clientHeight; |
|
} |
|
|
|
window.addEventListener('resize', resizeCanvas); |
|
resizeCanvas(); |
|
|
|
|
|
let params = { |
|
a: 3, |
|
b: 2, |
|
delta: Math.PI/2, |
|
speed: 1, |
|
trailLength: 500, |
|
paused: false, |
|
time: 0 |
|
}; |
|
|
|
|
|
let points = []; |
|
|
|
|
|
const aSlider = document.getElementById('aSlider'); |
|
const bSlider = document.getElementById('bSlider'); |
|
const deltaSlider = document.getElementById('deltaSlider'); |
|
const speedSlider = document.getElementById('speedSlider'); |
|
const trailSlider = document.getElementById('trailSlider'); |
|
const aValue = document.getElementById('aValue'); |
|
const bValue = document.getElementById('bValue'); |
|
const deltaValue = document.getElementById('deltaValue'); |
|
const speedValue = document.getElementById('speedValue'); |
|
const trailValue = document.getElementById('trailValue'); |
|
const pauseBtn = document.getElementById('pauseBtn'); |
|
const resetBtn = document.getElementById('resetBtn'); |
|
const clearBtn = document.getElementById('clearBtn'); |
|
const examples = document.querySelectorAll('.example'); |
|
|
|
|
|
function updateValueDisplays() { |
|
aValue.textContent = params.a.toFixed(1); |
|
bValue.textContent = params.b.toFixed(1); |
|
deltaValue.textContent = (params.delta/Math.PI).toFixed(2) + 'π'; |
|
speedValue.textContent = params.speed.toFixed(1); |
|
trailValue.textContent = params.trailLength; |
|
} |
|
|
|
|
|
aSlider.addEventListener('input', () => { |
|
params.a = parseFloat(aSlider.value); |
|
updateValueDisplays(); |
|
}); |
|
|
|
bSlider.addEventListener('input', () => { |
|
params.b = parseFloat(bSlider.value); |
|
updateValueDisplays(); |
|
}); |
|
|
|
deltaSlider.addEventListener('input', () => { |
|
params.delta = parseFloat(deltaSlider.value); |
|
updateValueDisplays(); |
|
}); |
|
|
|
speedSlider.addEventListener('input', () => { |
|
params.speed = parseFloat(speedSlider.value); |
|
updateValueDisplays(); |
|
}); |
|
|
|
trailSlider.addEventListener('input', () => { |
|
params.trailLength = parseInt(trailSlider.value); |
|
updateValueDisplays(); |
|
}); |
|
|
|
|
|
pauseBtn.addEventListener('click', () => { |
|
params.paused = !params.paused; |
|
pauseBtn.innerHTML = params.paused ? |
|
'<i class="fas fa-play"></i> Play' : |
|
'<i class="fas fa-pause"></i> Pause'; |
|
}); |
|
|
|
resetBtn.addEventListener('click', () => { |
|
params.a = 3; |
|
params.b = 2; |
|
params.delta = Math.PI/2; |
|
params.speed = 1; |
|
params.trailLength = 500; |
|
aSlider.value = params.a; |
|
bSlider.value = params.b; |
|
deltaSlider.value = params.delta; |
|
speedSlider.value = params.speed; |
|
trailSlider.value = params.trailLength; |
|
updateValueDisplays(); |
|
}); |
|
|
|
clearBtn.addEventListener('click', () => { |
|
points = []; |
|
}); |
|
|
|
|
|
examples.forEach(example => { |
|
example.addEventListener('click', () => { |
|
params.a = parseFloat(example.dataset.a); |
|
params.b = parseFloat(example.dataset.b); |
|
params.delta = parseFloat(example.dataset.d); |
|
|
|
aSlider.value = params.a; |
|
bSlider.value = params.b; |
|
deltaSlider.value = params.delta; |
|
|
|
updateValueDisplays(); |
|
}); |
|
}); |
|
|
|
|
|
updateValueDisplays(); |
|
|
|
|
|
function animate() { |
|
if (!params.paused) { |
|
params.time += 0.02 * params.speed; |
|
|
|
|
|
const x = Math.sin(params.a * params.time + params.delta); |
|
const y = Math.sin(params.b * params.time); |
|
|
|
|
|
const canvasX = canvas.width/2 + x * (canvas.width/2 - 50); |
|
const canvasY = canvas.height/2 + y * (canvas.height/2 - 50); |
|
|
|
|
|
points.push({x: canvasX, y: canvasY}); |
|
|
|
|
|
if (points.length > params.trailLength) { |
|
points.shift(); |
|
} |
|
} |
|
|
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
drawGrid(); |
|
|
|
|
|
if (points.length > 1) { |
|
ctx.beginPath(); |
|
ctx.moveTo(points[0].x, points[0].y); |
|
|
|
for (let i = 1; i < points.length; i++) { |
|
const alpha = i / points.length; |
|
ctx.strokeStyle = `hsla(${240 + alpha * 120}, 100%, 70%, ${alpha})`; |
|
ctx.lineWidth = 2; |
|
ctx.lineTo(points[i].x, points[i].y); |
|
ctx.stroke(); |
|
ctx.beginPath(); |
|
ctx.moveTo(points[i].x, points[i].y); |
|
} |
|
|
|
ctx.stroke(); |
|
} |
|
|
|
|
|
if (points.length > 0) { |
|
const currentPoint = points[points.length - 1]; |
|
ctx.beginPath(); |
|
ctx.arc(currentPoint.x, currentPoint.y, 8, 0, Math.PI * 2); |
|
ctx.fillStyle = '#fd79a8'; |
|
ctx.fill(); |
|
ctx.strokeStyle = '#fff'; |
|
ctx.lineWidth = 2; |
|
ctx.stroke(); |
|
} |
|
|
|
requestAnimationFrame(animate); |
|
} |
|
|
|
|
|
function drawGrid() { |
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; |
|
ctx.lineWidth = 1; |
|
|
|
|
|
for (let x = 0; x <= canvas.width; x += canvas.width/10) { |
|
ctx.beginPath(); |
|
ctx.moveTo(x, 0); |
|
ctx.lineTo(x, canvas.height); |
|
ctx.stroke(); |
|
} |
|
|
|
|
|
for (let y = 0; y <= canvas.height; y += canvas.height/10) { |
|
ctx.beginPath(); |
|
ctx.moveTo(0, y); |
|
ctx.lineTo(canvas.width, y); |
|
ctx.stroke(); |
|
} |
|
|
|
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; |
|
ctx.lineWidth = 2; |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(canvas.width/2, 0); |
|
ctx.lineTo(canvas.width/2, canvas.height); |
|
ctx.stroke(); |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(0, canvas.height/2); |
|
ctx.lineTo(canvas.width, canvas.height/2); |
|
ctx.stroke(); |
|
} |
|
|
|
|
|
animate(); |
|
</script> |
|
</body> |
|
</html> |