Custom Code Experiment •
Lemniscate of Bernoulli
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>Lemniscate of Bernoulli - Neon Dreams</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: radial-gradient(circle at center, #0a0a1a, #050510, #000000);
overflow: hidden;
font-family: "Courier New", monospace;
touch-action: manipulation;
}
#canvas {
display: block;
background: transparent;
cursor: crosshair;
image-rendering: pixelated;
}
#canvas:active {
cursor: grabbing;
}
.menu-toggle {
display: none;
position: fixed;
top: 15px;
right: 15px;
width: 36px;
height: 36px;
background: rgba(10, 10, 26, 0.7);
border: 2px solid #FF4FFB;
border-radius: 50%;
color: #FBF368;
font-size: 18px;
cursor: pointer;
z-index: 1001;
transition: all 0.3s ease;
box-shadow: 0 0 10px rgba(255, 79, 251, 0.5);
}
.menu-toggle:hover {
background: rgba(255, 79, 251, 0.2);
transform: scale(1.1);
}
.menu-toggle:active {
transform: scale(0.95);
}
.control-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(10, 10, 26, 0.95);
border: 2px solid #FF4FFB;
box-shadow: 0 0 20px #FF4FFB, inset 0 0 15px rgba(255, 79, 251, 0.1);
padding: 15px;
color: #E4B5EF;
font-size: 11px;
max-width: 250px;
z-index: 1000;
text-transform: uppercase;
letter-spacing: 1px;
border-radius: 10px;
backdrop-filter: blur(5px);
transition: transform 0.3s ease, opacity 0.3s ease;
}
.control-group {
margin-bottom: 12px;
}
.control-group:last-child {
margin-bottom: 0;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #15B1EA;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
font-size: 10px;
}
label::before {
content: "›";
margin-right: 5px;
color: #3BF854;
animation: blink 1.5s infinite;
font-size: 12px;
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0.3;
}
}
input[type="range"] {
width: 100%;
height: 6px;
background: linear-gradient(
90deg,
#0a1a1a,
#153535,
#15B1EA,
#153535,
#0a1a1a
);
outline: none;
-webkit-appearance: none;
appearance: none;
border: 1px solid #15B1EA;
box-shadow: 0 0 5px rgba(21, 177, 234, 0.3);
border-radius: 3px;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 14px;
height: 14px;
background: radial-gradient(circle, #FF4FFB, #B815EA);
cursor: pointer;
border: 2px solid #000000;
box-shadow: 0 0 8px #FF4FFB;
border-radius: 50%;
transition: transform 0.1s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: radial-gradient(circle, #FF4FFB, #B815EA);
cursor: pointer;
border: 2px solid #000000;
box-shadow: 0 0 8px #FF4FFB;
border-radius: 50%;
}
.value-display {
font-size: 9px;
color: #FF4FFB;
text-align: right;
margin-top: 3px;
font-family: "Courier New", monospace;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
.value-display::before {
content: "[";
color: #15B1EA;
margin-right: 2px;
}
.value-display::after {
content: "]";
color: #15B1EA;
margin-left: 2px;
}
.color-palette {
display: flex;
gap: 4px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255, 79, 251, 0.2);
}
.color-swatch {
flex: 1;
height: 4px;
border-radius: 2px;
}
@media (max-width: 768px) {
.menu-toggle {
display: flex;
align-items: center;
justify-content: center;
}
.control-panel {
position: fixed;
top: 50%;
right: -260px;
transform: translateY(-50%);
width: 250px;
max-width: 250px;
padding: 15px;
background: rgba(10, 10, 26, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 79, 251, 0.3);
box-shadow: 0 0 30px rgba(255, 79, 251, 0.2);
opacity: 0;
transition: right 0.3s ease, opacity 0.3s ease;
z-index: 1000;
}
.control-panel.active {
right: 10px;
opacity: 1;
}
.control-panel::after {
content: '';
position: absolute;
right: -10px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 10px solid rgba(255, 79, 251, 0.3);
display: none;
}
label {
font-size: 9px;
margin-bottom: 4px;
}
input[type="range"] {
height: 4px;
}
input[type="range"]::-webkit-slider-thumb {
width: 12px;
height: 12px;
}
.value-display {
font-size: 8px;
}
.color-palette {
margin-top: 6px;
padding-top: 6px;
}
}
@media (min-width: 769px) {
.control-panel {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.control-panel:hover {
transform: translateY(-2px);
box-shadow: 0 0 30px #FF4FFB, inset 0 0 20px rgba(255, 79, 251, 0.2);
}
}
@media (hover: none) and (pointer: coarse) {
input[type="range"] {
height: 8px;
}
input[type="range"]::-webkit-slider-thumb {
width: 20px;
height: 20px;
}
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="menu-toggle" onclick="toggleMenu()">☰</div>
<div class="control-panel" id="controlPanel">
<div class="control-group">
<label>Animation Speed</label>
<input type="range" id="animationSpeed" min="0.1" max="3" value="1" step="0.1">
<div class="value-display" id="speedValue">1.0</div>
</div>
<div class="control-group">
<label>Curve Detail</label>
<input type="range" id="curveDetail" min="30" max="150" value="80" step="5">
<div class="value-display" id="detailValue">80</div>
</div>
<div class="control-group">
<label>Trail Length</label>
<input type="range" id="trailLength" min="0.5" max="0.95" value="0.75" step="0.05">
<div class="value-display" id="trailValue">0.75</div>
</div>
<div class="color-palette">
<div class="color-swatch" style="background: #FF4FFB;"></div>
<div class="color-swatch" style="background: #15B1EA;"></div>
<div class="color-swatch" style="background: #E4B5EF;"></div>
<div class="color-swatch" style="background: #FBF368;"></div>
<div class="color-swatch" style="background: #3BF854;"></div>
</div>
</div>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const controlPanel = document.getElementById("controlPanel");
let isDragging = false;
let lastTouchX = 0;
let lastTouchY = 0;
let menuOpen = false;
let animationId;
let time = 0;
let animationSpeed = 1;
let curveDetail = 80;
let trailLength = 0.75;
let showGrid = false;
let gridDensity = 80;
let gridBursts = new Map();
const burstDuration = 40;
let colorOffset = 0;
const neonColors = [
"#FF4FFB", // Pink
"#15B1EA", // Blue
"#E4B5EF", // Light Purple
"#FBF368", // Yellow
"#3BF854" // Green
];
function toggleMenu() {
if (window.innerWidth <= 768) {
menuOpen = !menuOpen;
controlPanel.classList.toggle('active');
const menuToggle = document.querySelector('.menu-toggle');
menuToggle.textContent = menuOpen ? '✕' : '☰';
}
}
document.addEventListener('click', (e) => {
if (window.innerWidth <= 768 && menuOpen) {
if (!controlPanel.contains(e.target) && !e.target.classList.contains('menu-toggle')) {
toggleMenu();
}
}
});
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.imageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
}
function getNeonColor(index) {
const colorIndex = Math.floor(
(index + colorOffset * 0.3) % neonColors.length
);
return neonColors[colorIndex];
}
function drawHexagon(x, y, size) {
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
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 getHexGridPosition(row, col) {
const hexWidth = gridDensity;
const hexHeight = (gridDensity * Math.sqrt(3)) / 2;
const x = col * hexWidth * 0.75;
const y = row * hexHeight + ((col % 2) * hexHeight) / 2;
return { x, y };
}
function getNearestHexNode(x, y) {
const hexWidth = gridDensity;
const hexHeight = (gridDensity * Math.sqrt(3)) / 2;
const col = Math.round(x / (hexWidth * 0.75));
const row = Math.round((y - ((col % 2) * hexHeight) / 2) / hexHeight);
const hexPos = getHexGridPosition(row, col);
let closestDist = Infinity;
let closestPos = hexPos;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const testPos = getHexGridPosition(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 hexResult = getNearestHexNode(x, y);
const distance = hexResult.distance;
if (distance < gridDensity * 0.4) {
const nodeKey = `${hexResult.pos.x.toFixed(0)},${hexResult.pos.y.toFixed(
0
)}`;
if (!gridBursts.has(nodeKey)) {
gridBursts.set(nodeKey, {
x: hexResult.pos.x,
y: hexResult.pos.y,
frame: 0,
intensity: 1,
color: getNeonColor(Math.random() * neonColors.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 = 12 + (1 - burst.intensity) * 25;
const alpha = burst.intensity;
for (let ring = 0; ring < 3; ring++) {
const ringSize = size + ring * 8;
const ringAlpha = alpha * (1 - ring * 0.3);
ctx.strokeStyle = `${burst.color}${Math.floor(ringAlpha * 255)
.toString(16)
.padStart(2, "0")}`;
ctx.lineWidth = 3 - ring * 0.5;
ctx.shadowColor = burst.color;
ctx.shadowBlur = 15;
drawHexagon(burst.x, burst.y, ringSize);
ctx.stroke();
ctx.shadowBlur = 0;
}
const coreSize = 4 + Math.sin(time * 8) * 2;
ctx.fillStyle = `#ffffff${Math.floor(alpha * 255)
.toString(16)
.padStart(2, "0")}`;
ctx.shadowColor = burst.color;
ctx.shadowBlur = 10;
ctx.fillRect(
burst.x - coreSize / 2,
burst.y - coreSize / 2,
coreSize,
coreSize
);
ctx.shadowBlur = 0;
}
}
function createLemniscatePoints(t) {
const points = [];
const scale = Math.min(canvas.width, canvas.height) * 0.3;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const a = 1;
for (let i = 0; i < curveDetail; i++) {
const param = (i / curveDetail) * Math.PI * 2;
const animatedParam = param + t * animationSpeed;
const sinT = Math.sin(animatedParam);
const cosT = Math.cos(animatedParam);
const denominator = 1 + sinT * sinT;
const x = (a * cosT) / denominator;
const y = (a * sinT * cosT) / denominator;
for (let layer = 0; layer < 3; layer++) {
const layerScale = scale * (0.5 + layer * 0.3);
const layerPhase = (layer * Math.PI) / 3;
const layerT = animatedParam + layerPhase;
const layerSinT = Math.sin(layerT);
const layerCosT = Math.cos(layerT);
const layerDenom = 1 + layerSinT * layerSinT;
const layerX = (a * layerCosT) / layerDenom;
const layerY = (a * layerSinT * layerCosT) / layerDenom;
const rotationPhase = t * 0.5 + (layer * Math.PI) / 4;
const rotatedX =
layerX * Math.cos(rotationPhase) - layerY * Math.sin(rotationPhase);
const rotatedY =
layerX * Math.sin(rotationPhase) + layerY * Math.cos(rotationPhase);
points.push({
x: centerX + rotatedX * layerScale,
y: centerY + rotatedY * layerScale,
param: animatedParam,
index: i,
layer: layer
});
}
}
return points;
}
function drawGrid() {
if (!showGrid) return;
const hexSize = gridDensity * 0.2;
const hexWidth = gridDensity;
const hexHeight = (gridDensity * Math.sqrt(3)) / 2;
ctx.strokeStyle = "rgba(21, 177, 234, 0.2)";
ctx.lineWidth = 1;
ctx.shadowColor = "#15B1EA";
ctx.shadowBlur = 2;
const cols = Math.ceil(canvas.width / (hexWidth * 0.75)) + 2;
const rows = Math.ceil(canvas.height / hexHeight) + 2;
for (let row = -1; row < rows; row++) {
for (let col = -1; col < cols; col++) {
const pos = getHexGridPosition(row, col);
if (
pos.x > -hexWidth &&
pos.x < canvas.width + hexWidth &&
pos.y > -hexHeight &&
pos.y < canvas.height + hexHeight
) {
drawHexagon(pos.x, pos.y, hexSize);
ctx.stroke();
}
}
}
ctx.shadowBlur = 0;
const nodeSize = 2 + Math.sin(time * 3) * 0.5;
let nodeIndex = 0;
for (let row = -1; row < rows; row++) {
for (let col = -1; col < cols; col++) {
const pos = getHexGridPosition(row, col);
if (
pos.x > -hexWidth &&
pos.x < canvas.width + hexWidth &&
pos.y > -hexHeight &&
pos.y < canvas.height + hexHeight
) {
const color = getNeonColor(nodeIndex * 0.1);
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 5;
ctx.fillRect(
pos.x - nodeSize / 2,
pos.y - nodeSize / 2,
nodeSize,
nodeSize
);
ctx.shadowBlur = 0;
if (Math.random() < 0.05) {
ctx.fillStyle = "#ffffff";
ctx.shadowColor = "#ffffff";
ctx.shadowBlur = 8;
ctx.fillRect(pos.x - 1, pos.y - 1, 2, 2);
ctx.shadowBlur = 0;
}
nodeIndex++;
}
}
}
}
function animate() {
time += 0.02 * animationSpeed;
colorOffset += 0.05 * animationSpeed;
ctx.fillStyle = `rgba(10, 10, 26, ${1 - trailLength})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
updateBursts();
drawGrid();
drawBursts();
const curvePoints = createLemniscatePoints(time);
ctx.strokeStyle = "rgba(255, 79, 251, 0.1)";
ctx.lineWidth = 1;
for (let i = 0; i < curvePoints.length - 1; i += 3) {
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 < 40) {
const alpha = Math.max(0, 1 - distance / 40) * 0.25;
const color = getNeonColor(p1.layer);
ctx.strokeStyle = `${color}${Math.floor(alpha * 255)
.toString(16)
.padStart(2, "0")}`;
ctx.shadowColor = color;
ctx.shadowBlur = 2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
ctx.shadowBlur = 0;
}
}
}
curvePoints.forEach((point) => {
if (point) {
checkNodeCollision(point.x, point.y);
const size = 2 + Math.sin(point.param + time) * 0.8 + point.layer * 0.5;
const color = getNeonColor(point.layer);
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 6 + point.layer * 2;
ctx.fillRect(point.x - size / 2, point.y - size / 2, size, size);
ctx.fillStyle = "#ffffff";
ctx.shadowColor = "#ffffff";
ctx.shadowBlur = 3;
ctx.fillRect(point.x - size / 2, point.y - size / 2, 1, 1);
ctx.shadowBlur = 0;
}
});
animationId = requestAnimationFrame(animate);
}
document.getElementById("animationSpeed").addEventListener("input", (e) => {
animationSpeed = parseFloat(e.target.value);
document.getElementById("speedValue").textContent = animationSpeed.toFixed(1);
});
document.getElementById("curveDetail").addEventListener("input", (e) => {
curveDetail = parseInt(e.target.value);
document.getElementById("detailValue").textContent = curveDetail;
});
document.getElementById("trailLength").addEventListener("input", (e) => {
trailLength = parseFloat(e.target.value);
document.getElementById("trailValue").textContent = trailLength.toFixed(2);
});
canvas.addEventListener("touchstart", (e) => {
e.preventDefault();
isDragging = true;
const touch = e.touches[0];
lastTouchX = touch.clientX;
lastTouchY = touch.clientY;
});
canvas.addEventListener("touchmove", (e) => {
e.preventDefault();
if (!isDragging) return;
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;
});
canvas.addEventListener("touchend", (e) => {
e.preventDefault();
isDragging = 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;
});
window.addEventListener("resize", resizeCanvas);
document.querySelectorAll('#controlPanel input').forEach(input => {
input.addEventListener('change', () => {
if (window.innerWidth <= 768 && menuOpen) {
setTimeout(() => toggleMenu(), 1500);
}
});
});
resizeCanvas();
animate();
</script>
</body>
</html>