Custom Code Experiment •
Quartic Plane Curve 2
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quartic Plane Curve 2</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #000000, #1a1a1a, #0a0a0a);
overflow: hidden;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
touch-action: pan-y;
color: #ffffff;
}
#canvas {
display: block;
background: transparent;
cursor: crosshair;
image-rendering: pixelated;
touch-action: none;
}
#canvas:active {
cursor: grabbing;
}
.fab {
position: fixed;
bottom: 20px;
right: 20px;
width: 56px;
height: 56px;
background: rgba(255, 255, 255, 0.95);
border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: 0 4px 20px rgba(255, 255, 255, 0.2);
display: none;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 1001;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 24px;
color: #000;
font-weight: bold;
transform-origin: center;
}
.fab:hover {
transform: scale(1.1);
box-shadow: 0 6px 30px rgba(255, 255, 255, 0.4);
}
.fab:active {
transform: scale(0.95);
}
.fab.active {
background: rgba(0, 0, 0, 0.95);
border-color: #ffffff;
color: #ffffff;
transform: rotate(90deg);
}
.control-panel {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.92);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
padding: 20px;
color: #ffffff;
font-size: 12px;
max-width: 300px;
z-index: 1000;
letter-spacing: 0.5px;
border-radius: 16px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateY(0);
opacity: 1;
overflow: hidden;
}
.control-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
}
.control-panel.hidden {
opacity: 0;
pointer-events: none;
transform: translateY(-30px) scale(0.95);
}
.control-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.control-title {
font-weight: 700;
color: #ffffff;
font-size: 14px;
letter-spacing: 1px;
position: relative;
padding-left: 8px;
}
.control-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: #ffffff;
}
.close-btn {
width: 28px;
height: 28px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: none;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
font-size: 18px;
color: #ffffff;
line-height: 1;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: rotate(90deg);
}
.control-group {
margin-bottom: 18px;
touch-action: pan-y;
position: relative;
padding-bottom: 10px;
}
.control-group::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: rgba(255, 255, 255, 0.05);
}
.control-group:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
.control-group:last-child::after {
display: none;
}
label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: #ffffff;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
display: flex;
align-items: center;
}
label::before {
content: "";
width: 6px;
height: 6px;
margin-right: 8px;
background: #ffffff;
border-radius: 50%;
display: inline-block;
transition: transform 0.2s;
}
.control-group:hover label::before {
transform: scale(1.3);
}
.slider-container {
position: relative;
height: 24px;
display: flex;
align-items: center;
}
input[type="range"] {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.1);
outline: none;
-webkit-appearance: none;
appearance: none;
border-radius: 10px;
touch-action: pan-x;
position: relative;
z-index: 1;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
background: #ffffff;
cursor: pointer;
border-radius: 50%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
border: 2px solid #000000;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.3);
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
background: #ffffff;
cursor: pointer;
border-radius: 50%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
border: 2px solid #000000;
}
input[type="range"]::-moz-range-thumb:hover {
transform: scale(1.2);
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.3);
}
.slider-fill {
position: absolute;
height: 4px;
background: #ffffff;
border-radius: 10px;
pointer-events: none;
z-index: 0;
left: 0;
top: 50%;
transform: translateY(-50%);
}
.button-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 5px;
}
button {
padding: 12px 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 11px;
text-transform: uppercase;
font-family: 'Inter', sans-serif;
border-radius: 8px;
touch-action: manipulation;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
}
button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.6s;
}
button:hover::before {
left: 100%;
}
button:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(0);
transition: all 0.1s;
}
.value-display {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
text-align: right;
margin-top: 8px;
font-family: 'Courier New', monospace;
letter-spacing: 0.5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.value-display span {
background: rgba(255, 255, 255, 0.05);
padding: 4px 10px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.toggle-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
padding: 12px;
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s;
cursor: pointer;
}
.toggle-container:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.2);
}
.toggle-label {
margin-bottom: 0;
flex-grow: 1;
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: #ffffff;
display: flex;
align-items: center;
}
.toggle-label::before {
content: "";
width: 6px;
height: 6px;
margin-right: 8px;
background: #ffffff;
border-radius: 50%;
display: inline-block;
transition: transform 0.2s;
}
.toggle-switch {
position: relative;
width: 50px;
height: 24px;
margin-left: 10px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.1);
transition: .4s;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 2px;
background-color: #ffffff;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.toggle-switch input:checked + .toggle-slider {
background-color: rgba(255, 255, 255, 0.3);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.toggle-container.active .toggle-slider {
background-color: rgba(255, 255, 255, 0.3);
}
.toggle-container.active .toggle-slider:before {
transform: translateX(26px);
}
.tooltip {
position: relative;
display: inline-block;
margin-left: 8px;
cursor: help;
}
.tooltip-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
font-size: 10px;
color: rgba(255, 255, 255, 0.7);
transition: all 0.3s;
}
.tooltip:hover .tooltip-icon {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 200px;
background-color: rgba(0, 0, 0, 0.95);
color: #fff;
text-align: left;
border-radius: 8px;
padding: 12px;
position: absolute;
z-index: 1002;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
font-size: 11px;
line-height: 1.4;
border: 1px solid rgba(255, 255, 255, 0.1);
text-transform: none;
letter-spacing: 0;
font-weight: normal;
backdrop-filter: blur(10px);
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.tooltip .tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: rgba(0, 0, 0, 0.95) transparent transparent transparent;
}
@media (max-width: 768px) {
.fab {
display: flex;
}
.close-btn {
display: flex;
}
.control-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
right: auto;
bottom: auto;
width: calc(100% - 40px);
max-width: 360px;
max-height: none;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 20px;
display: flex;
flex-direction: column;
overflow-y: visible;
background: rgba(0, 0, 0, 0.95);
backdrop-filter: blur(30px);
}
.control-panel.hidden {
transform: translate(-50%, -50%) scale(0.9);
}
.control-header {
margin-bottom: 15px;
padding-bottom: 10px;
}
.control-title {
font-size: 13px;
}
.close-btn {
width: 28px;
height: 28px;
font-size: 18px;
}
.control-group {
margin-bottom: 15px;
}
label {
font-size: 10px;
margin-bottom: 8px;
}
.button-group {
grid-template-columns: 1fr;
gap: 10px;
}
button {
padding: 14px 10px;
font-size: 11px;
}
.value-display {
font-size: 10px;
margin-top: 6px;
}
}
@media (min-width: 769px) {
.control-panel {
display: block !important;
opacity: 1 !important;
pointer-events: all !important;
transform: none !important;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.control-panel:not(.hidden) {
animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<canvas id="canvas"></canvas>
<div class="fab" id="fab">⚙</div>
<div class="control-panel hidden" id="controlPanel">
<div class="control-header">
<div class="control-title">QUARTIC CURVE CONTROLS</div>
<div class="close-btn" id="closeBtn">×</div>
</div>
<div class="control-group">
<label>Animation Speed
<span class="tooltip">
<span class="tooltip-icon">?</span>
<span class="tooltip-text">Controls the speed of the curve animation. Higher values create faster movement.</span>
</span>
</label>
<div class="slider-container">
<div class="slider-fill" id="speedFill"></div>
<input type="range" id="animationSpeed" min="0.1" max="3" value="1" step="0.1">
</div>
<div class="value-display">
<span>Speed</span>
<span id="speedValue">1.0</span>
</div>
</div>
<div class="control-group">
<label>Curve Detail
<span class="tooltip">
<span class="tooltip-icon">?</span>
<span class="tooltip-text">Adjusts the number of points on the curve. Higher values create smoother curves.</span>
</span>
</label>
<div class="slider-container">
<div class="slider-fill" id="detailFill"></div>
<input type="range" id="curveDetail" min="50" max="300" value="150" step="10">
</div>
<div class="value-display">
<span>Detail</span>
<span id="detailValue">150</span>
</div>
</div>
<div class="control-group">
<label>Curve Type
<span class="tooltip">
<span class="tooltip-icon">?</span>
<span class="tooltip-text">Selects the mathematical formula used to generate the curve.</span>
</span>
</label>
<div class="slider-container">
<div class="slider-fill" id="typeFill"></div>
<input type="range" id="curveType" min="0" max="4" value="0" step="1">
</div>
<div class="value-display">
<span>Type</span>
<span id="typeValue">Rose (0)</span>
</div>
</div>
<div class="control-group">
<label>Trail Length
<span class="tooltip">
<span class="tooltip-icon">?</span>
<span class="tooltip-text">Controls how long the trail of the curve remains visible.</span>
</span>
</label>
<div class="slider-container">
<div class="slider-fill" id="trailFill"></div>
<input type="range" id="trailLength" min="0.5" max="0.95" value="0.75" step="0.05">
</div>
<div class="value-display">
<span>Trail</span>
<span id="trailValue">0.75</span>
</div>
</div>
<!-- Fixed Toggle Container - Now properly connected -->
<div class="toggle-container" id="gridToggleContainer">
<div class="toggle-label">Show Grid
<span class="tooltip">
<span class="tooltip-icon">?</span>
<span class="tooltip-text">Toggles the background grid display on and off.</span>
</span>
</div>
<div class="toggle-switch">
<input type="checkbox" id="gridToggle" checked>
<span class="toggle-slider"></span>
</div>
</div>
<div class="control-group">
<div class="button-group">
<button id="resetBtn">Reset</button>
<button id="pauseBtn">Pause</button>
<button id="randomBtn">Random</button>
</div>
</div>
</div>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const fab = document.getElementById("fab");
const controlPanel = document.getElementById("controlPanel");
const closeBtn = document.getElementById("closeBtn");
let panelOpen = false;
fab.addEventListener('click', () => {
panelOpen = !panelOpen;
controlPanel.classList.toggle('hidden', !panelOpen);
fab.classList.toggle('active', panelOpen);
fab.textContent = panelOpen ? '×' : '⚙';
});
closeBtn.addEventListener('click', () => {
panelOpen = false;
controlPanel.classList.add('hidden');
fab.classList.remove('active');
fab.textContent = '⚙';
});
if (window.innerWidth <= 768) {
controlPanel.classList.add('hidden');
}
let isDragging = false;
let lastTouchX = 0;
let lastTouchY = 0;
let animationId;
let isPaused = false;
let time = 0;
let animationSpeed = 1;
let curveDetail = 150;
let curveType = 0;
let trailLength = 0.75;
let showGrid = true;
let gridDensity = 80;
let gridBursts = new Map();
const burstDuration = 40;
const bwColors = ["#ffffff", "#f0f0f0", "#d0d0d0", "#b0b0b0"];
function updateSliderFills() {
const speedSlider = document.getElementById("animationSpeed");
const detailSlider = document.getElementById("curveDetail");
const typeSlider = document.getElementById("curveType");
const trailSlider = document.getElementById("trailLength");
const speedFill = document.getElementById("speedFill");
const detailFill = document.getElementById("detailFill");
const typeFill = document.getElementById("typeFill");
const trailFill = document.getElementById("trailFill");
const speedPercent = (speedSlider.value - speedSlider.min) / (speedSlider.max - speedSlider.min) * 100;
const detailPercent = (detailSlider.value - detailSlider.min) / (detailSlider.max - detailSlider.min) * 100;
const typePercent = (typeSlider.value - typeSlider.min) / (typeSlider.max - typeSlider.min) * 100;
const trailPercent = (trailSlider.value - trailSlider.min) / (trailSlider.max - trailSlider.min) * 100;
speedFill.style.width = `${speedPercent}%`;
detailFill.style.width = `${detailPercent}%`;
typeFill.style.width = `${typePercent}%`;
trailFill.style.width = `${trailPercent}%`;
}
function updateGridToggleUI() {
const gridToggle = document.getElementById("gridToggle");
const gridToggleContainer = document.getElementById("gridToggleContainer");
gridToggle.checked = showGrid;
if (showGrid) {
gridToggleContainer.classList.add("active");
} else {
gridToggleContainer.classList.remove("active");
}
}
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.imageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
}
function getBWColor(index) {
return bwColors[index % bwColors.length];
}
function drawNonagon(x, y, size) {
ctx.beginPath();
for (let i = 0; i < 9; i++) {
const angle = (i * 2 * Math.PI) / 9;
const px = x + Math.cos(angle) * size;
const py = y + Math.sin(angle) * size;
if (i === 0) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.closePath();
}
function getNonagonGridPosition(row, col) {
const nonWidth = gridDensity;
const nonHeight = gridDensity * 0.866;
const x = col * nonWidth * 0.8;
const y = row * nonHeight + ((col % 2) * nonHeight) / 2;
return { x, y };
}
function getNearestNonagonNode(x, y) {
const nonWidth = gridDensity;
const nonHeight = gridDensity * 0.866;
const col = Math.round(x / (nonWidth * 0.8));
const row = Math.round((y - ((col % 2) * nonHeight) / 2) / nonHeight);
const nonPos = getNonagonGridPosition(row, col);
let closestDist = Infinity;
let closestPos = nonPos;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const testPos = getNonagonGridPosition(row + dr, col + dc);
const dist = Math.sqrt((x - testPos.x) ** 2 + (y - testPos.y) ** 2);
if (dist < closestDist) {
closestDist = dist;
closestPos = testPos;
}
}
}
return { pos: closestPos, distance: closestDist };
}
function checkNodeCollision(x, y) {
const nonResult = getNearestNonagonNode(x, y);
const distance = nonResult.distance;
if (distance < gridDensity * 0.4) {
const nodeKey = `${nonResult.pos.x.toFixed(0)},${nonResult.pos.y.toFixed(0)}`;
if (!gridBursts.has(nodeKey)) {
gridBursts.set(nodeKey, {
x: nonResult.pos.x,
y: nonResult.pos.y,
frame: 0,
intensity: 1,
color: getBWColor(Math.floor(Math.random() * bwColors.length))
});
}
}
}
function updateBursts() {
for (let [key, burst] of gridBursts) {
burst.frame++;
burst.intensity = Math.max(0, 1 - burst.frame / burstDuration);
if (burst.frame >= burstDuration) {
gridBursts.delete(key);
}
}
}
function drawBursts() {
for (let [key, burst] of gridBursts) {
const size = 8 + (1 - burst.intensity) * 15;
const alpha = burst.intensity;
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.8})`;
ctx.lineWidth = 2;
drawNonagon(burst.x, burst.y, size);
ctx.stroke();
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.fillRect(burst.x - 1, burst.y - 1, 2, 2);
}
}
function createQuarticCurvePoints(t) {
const points = [];
const scale = Math.min(canvas.width, canvas.height) * 0.4;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
for (let i = 0; i < curveDetail; i++) {
const param = (i / curveDetail) * Math.PI * 2;
let x, y;
switch (curveType) {
case 0:
const k = 4;
const r = Math.cos(k * param + t);
x = r * Math.cos(param);
y = r * Math.sin(param);
break;
case 1:
const a = 1;
const cos2t = Math.cos(2 * param + t);
if (cos2t >= 0) {
const r2 = a * a * cos2t;
const r_lem = Math.sqrt(r2);
x = r_lem * Math.cos(param);
y = r_lem * Math.sin(param);
} else {
continue;
}
break;
case 2:
const r_spiral = Math.pow(param / (Math.PI * 2), 0.25) + 0.3;
x = r_spiral * Math.cos(param + t);
y = r_spiral * Math.sin(param + t);
break;
case 3:
const r_butterfly =
Math.exp(Math.sin(param + t)) -
2 * Math.cos(4 * param) +
Math.pow(Math.sin((2 * param - Math.PI) / 24), 5);
x = r_butterfly * Math.cos(param) * 0.3;
y = r_butterfly * Math.sin(param) * 0.3;
break;
case 4:
const r_clover = Math.sin(2 * param + t) * Math.cos(2 * param);
x = r_clover * Math.cos(param);
y = r_clover * Math.sin(param);
break;
default:
x = Math.cos(param + t);
y = Math.sin(param + t);
}
points.push({
x: centerX + x * scale,
y: centerY + y * scale,
param: param,
index: i
});
}
return points;
}
function drawGrid() {
if (!showGrid) return;
const nonSize = gridDensity * 0.15;
const nonWidth = gridDensity;
const nonHeight = gridDensity * 0.866;
ctx.strokeStyle = "rgba(255, 255, 255, 0.05)";
ctx.lineWidth = 1;
const cols = Math.ceil(canvas.width / (nonWidth * 0.8)) + 2;
const rows = Math.ceil(canvas.height / nonHeight) + 2;
for (let row = -1; row < rows; row++) {
for (let col = -1; col < cols; col++) {
const pos = getNonagonGridPosition(row, col);
if (
pos.x > -nonWidth &&
pos.x < canvas.width + nonWidth &&
pos.y > -nonHeight &&
pos.y < canvas.height + nonHeight
) {
drawNonagon(pos.x, pos.y, nonSize);
ctx.stroke();
}
}
}
const nodeSize = 2;
let nodeIndex = 0;
for (let row = -1; row < rows; row++) {
for (let col = -1; col < cols; col++) {
const pos = getNonagonGridPosition(row, col);
if (
pos.x > -nonWidth &&
pos.x < canvas.width + nonWidth &&
pos.y > -nonHeight &&
pos.y < canvas.height + nonHeight
) {
ctx.fillStyle = `rgba(255, 255, 255, ${0.2 + 0.1 * (nodeIndex % 3)})`;
ctx.fillRect(
pos.x - nodeSize / 2,
pos.y - nodeSize / 2,
nodeSize,
nodeSize
);
nodeIndex++;
}
}
}
}
function animate() {
if (isPaused) {
animationId = requestAnimationFrame(animate);
return;
}
time += 0.02 * animationSpeed;
ctx.fillStyle = `rgba(0, 0, 0, ${1 - trailLength})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
updateBursts();
drawGrid();
drawBursts();
const curvePoints = createQuarticCurvePoints(time);
for (let i = 0; i < curvePoints.length - 1; i += 2) {
const p1 = curvePoints[i];
const p2 = curvePoints[i + 1];
if (p1 && p2) {
const distance = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
if (distance < 50) {
const alpha = Math.max(0, 1 - distance / 50) * 0.15;
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
}
}
curvePoints.forEach((point) => {
if (point) {
checkNodeCollision(point.x, point.y);
const size = 3 + Math.sin(point.param + time) * 1;
const colorIntensity = 0.7 + 0.3 * Math.sin(point.index * 0.1 + time);
ctx.fillStyle = `rgba(255, 255, 255, ${colorIntensity})`;
ctx.fillRect(point.x - size / 2, point.y - size / 2, size, size);
ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
ctx.fillRect(point.x - size / 4, point.y - size / 4, size / 2, size / 2);
}
});
animationId = requestAnimationFrame(animate);
}
const curveNames = ['Rose', 'Lemniscate', 'Spiral', 'Butterfly', 'Clover'];
function updateAllSliders() {
updateSliderFills();
updateGridToggleUI();
document.getElementById("speedValue").textContent = animationSpeed.toFixed(1);
document.getElementById("detailValue").textContent = curveDetail;
document.getElementById("typeValue").textContent = `${curveNames[curveType]} (${curveType})`;
document.getElementById("trailValue").textContent = trailLength.toFixed(2);
}
document.getElementById("animationSpeed").addEventListener("input", (e) => {
animationSpeed = parseFloat(e.target.value);
updateAllSliders();
});
document.getElementById("curveDetail").addEventListener("input", (e) => {
curveDetail = parseInt(e.target.value);
updateAllSliders();
});
document.getElementById("curveType").addEventListener("input", (e) => {
curveType = parseInt(e.target.value);
updateAllSliders();
});
document.getElementById("trailLength").addEventListener("input", (e) => {
trailLength = parseFloat(e.target.value);
updateAllSliders();
});
const gridToggle = document.getElementById("gridToggle");
const gridToggleContainer = document.getElementById("gridToggleContainer");
gridToggle.addEventListener("change", (e) => {
showGrid = e.target.checked;
updateGridToggleUI();
});
gridToggleContainer.addEventListener("click", (e) => {
if (e.target !== gridToggle && e.target.type !== "checkbox") {
showGrid = !showGrid;
gridToggle.checked = showGrid;
updateGridToggleUI();
}
});
document.getElementById("resetBtn").addEventListener("click", () => {
time = 0;
gridBursts.clear();
});
document.getElementById("pauseBtn").addEventListener("click", (e) => {
isPaused = !isPaused;
e.target.textContent = isPaused ? "Play" : "Pause";
e.target.style.background = isPaused ?
"rgba(255, 255, 255, 0.2)" :
"rgba(255, 255, 255, 0.05)";
e.target.style.borderColor = isPaused ?
"rgba(255, 255, 255, 0.5)" :
"rgba(255, 255, 255, 0.2)";
});
document.getElementById("randomBtn").addEventListener("click", () => {
animationSpeed = Math.random() * 2.9 + 0.1;
curveDetail = Math.floor(Math.random() * 251) + 50;
curveType = Math.floor(Math.random() * 5);
trailLength = Math.random() * 0.45 + 0.5;
document.getElementById("animationSpeed").value = animationSpeed;
document.getElementById("curveDetail").value = curveDetail;
document.getElementById("curveType").value = curveType;
document.getElementById("trailLength").value = trailLength;
updateAllSliders();
});
updateAllSliders();
canvas.addEventListener("touchstart", (e) => {
if (panelOpen && window.innerWidth <= 768) return;
e.preventDefault();
isDragging = true;
const touch = e.touches[0];
lastTouchX = touch.clientX;
lastTouchY = touch.clientY;
}, { passive: false });
canvas.addEventListener("touchmove", (e) => {
if (!isDragging) return;
e.preventDefault();
const touch = e.touches[0];
const deltaX = (touch.clientX - lastTouchX) * 0.01;
const deltaY = (touch.clientY - lastTouchY) * 0.01;
time += deltaX + deltaY;
lastTouchX = touch.clientX;
lastTouchY = touch.clientY;
}, { passive: false });
canvas.addEventListener("touchend", (e) => {
e.preventDefault();
isDragging = false;
}, { passive: false });
canvas.addEventListener("mousedown", (e) => {
isDragging = true;
lastTouchX = e.clientX;
lastTouchY = e.clientY;
});
canvas.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const deltaX = (e.clientX - lastTouchX) * 0.01;
const deltaY = (e.clientY - lastTouchY) * 0.01;
time += deltaX + deltaY;
lastTouchX = e.clientX;
lastTouchY = e.clientY;
});
canvas.addEventListener("mouseup", () => {
isDragging = false;
});
const controlGroups = document.querySelectorAll('.control-group');
controlGroups.forEach(group => {
group.addEventListener('mouseenter', () => {
group.style.transform = 'translateX(5px)';
});
group.addEventListener('mouseleave', () => {
group.style.transform = 'translateX(0)';
});
});
window.addEventListener("resize", () => {
resizeCanvas();
if (window.innerWidth > 768) {
controlPanel.classList.remove('hidden');
panelOpen = false;
} else {
controlPanel.classList.add('hidden');
panelOpen = false;
}
});
resizeCanvas();
if (window.innerWidth > 768) {
controlPanel.classList.remove('hidden');
}
animate();
</script>
</body>
</html>