Files
phonestream/dots.html
T

356 lines
8.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connected Dots</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a0f;
overflow: hidden;
font-family: system-ui, sans-serif;
}
canvas {
display: block;
position: fixed;
top: 0;
left: 0;
}
/* Controls panel */
#controls {
position: fixed;
top: 16px;
right: 16px;
background: rgba(255,255,255,0.06);
backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 16px;
color: #fff;
font-size: 13px;
z-index: 10;
min-width: 180px;
user-select: none;
}
#controls h3 {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: rgba(255,255,255,0.4);
margin-bottom: 12px;
}
.ctrl-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.ctrl-row:last-child { margin-bottom: 0; }
.ctrl-row label {
color: rgba(255,255,255,0.7);
}
.ctrl-row input[type="range"] {
width: 90px;
accent-color: #7c6cf0;
cursor: pointer;
}
.ctrl-row .val {
color: rgba(255,255,255,0.5);
font-size: 11px;
min-width: 28px;
text-align: right;
}
#toggle-controls {
position: fixed;
top: 16px;
right: 16px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
color: rgba(255,255,255,0.5);
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
display: none;
z-index: 11;
backdrop-filter: blur(12px);
}
#toggle-controls:hover {
color: #fff;
background: rgba(255,255,255,0.12);
}
#controls.hidden { display: none; }
#controls.hidden + #toggle-controls { display: flex; align-items: center; justify-content: center; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="controls">
<h3>⚙ Settings</h3>
<div class="ctrl-row">
<label>Dots</label>
<input type="range" id="dotCount" min="20" max="200" value="80">
<span class="val" id="dotCountVal">80</span>
</div>
<div class="ctrl-row">
<label>Max speed</label>
<input type="range" id="speed" min="0.5" max="6" step="0.1" value="2.5">
<span class="val" id="speedVal">2.5</span>
</div>
<div class="ctrl-row">
<label>Jump chance</label>
<input type="range" id="jumpChance" min="0" max="100" value="30">
<span class="val" id="jumpChanceVal">30%</span>
</div>
<div class="ctrl-row">
<label>Connect dist</label>
<input type="range" id="connDist" min="50" max="300" value="140">
<span class="val" id="connDistVal">140</span>
</div>
<div class="ctrl-row">
<label>Trail</label>
<input type="range" id="trail" min="0" max="95" value="80">
<span class="val" id="trailVal">80%</span>
</div>
<div class="ctrl-row">
<label>Glow</label>
<input type="range" id="glow" min="0" max="30" value="12">
<span class="val" id="glowVal">12</span>
</div>
</div>
<button id="toggle-controls" onclick="document.getElementById('controls').classList.toggle('hidden')">⚙</button>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let W, H;
let dots = [];
let mouse = { x: -9999, y: -9999 };
// Settings
const settings = {
dotCount: 80,
maxSpeed: 2.5,
jumpChance: 0.3,
connDist: 140,
trail: 0.80,
glow: 12
};
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
class Dot {
constructor() {
this.x = Math.random() * W;
this.y = Math.random() * H;
this.vx = (Math.random() - 0.5) * settings.maxSpeed;
this.vy = (Math.random() - 0.5) * settings.maxSpeed;
this.radius = Math.random() * 2 + 1.5;
this.hue = Math.random() * 60 + 240; // blue-purple range
this.hueSpeed = (Math.random() - 0.5) * 0.5;
this.jumpCooldown = 0;
}
jump() {
// Teleport to a random position far away
const angle = Math.random() * Math.PI * 2;
const dist = Math.min(W, H) * (0.3 + Math.random() * 0.5);
this.x = W/2 + Math.cos(angle) * dist;
this.y = H/2 + Math.sin(angle) * dist;
// Clamp
this.x = Math.max(0, Math.min(W, this.x));
this.y = Math.max(0, Math.min(H, this.y));
// Random new velocity
this.vx = (Math.random() - 0.5) * settings.maxSpeed * 2;
this.vy = (Math.random() - 0.5) * settings.maxSpeed * 2;
this.jumpCooldown = 60 + Math.random() * 60;
}
update() {
const spd = settings.maxSpeed;
// Maybe jump (teleport)
if (this.jumpCooldown > 0) {
this.jumpCooldown--;
} else if (Math.random() < settings.jumpChance * 0.002) {
this.jump();
} else {
// Normal wandering movement
this.vx += (Math.random() - 0.5) * 0.15 * spd;
this.vy += (Math.random() - 0.5) * 0.15 * spd;
// Clamp speed
const maxSpd = spd;
const s = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
if (s > maxSpd) {
this.vx = (this.vx / s) * maxSpd;
this.vy = (this.vy / s) * maxSpd;
}
}
this.x += this.vx;
this.y += this.vy;
this.hue = (this.hue + this.hueSpeed + 360) % 360;
// Soft bounce off edges
if (this.x < 0) { this.x = 0; this.vx = Math.abs(this.vx) * 0.5; }
if (this.x > W) { this.x = W; this.vx = -Math.abs(this.vx) * 0.5; }
if (this.y < 0) { this.y = 0; this.vy = Math.abs(this.vy) * 0.5; }
if (this.y > H) { this.y = H; this.vy = -Math.abs(this.vy) * 0.5; }
}
draw() {
const glow = settings.glow;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius + glow, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${this.hue}, 80%, 65%, 0.08)`;
ctx.fill();
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${this.hue}, 90%, 75%, 1)`;
ctx.fill();
}
}
function initDots() {
dots = [];
for (let i = 0; i < settings.dotCount; i++) {
dots.push(new Dot());
}
}
function drawLines() {
const dist = settings.connDist;
const dist2 = dist * dist;
for (let i = 0; i < dots.length; i++) {
for (let j = i + 1; j < dots.length; j++) {
const dx = dots[i].x - dots[j].x;
const dy = dots[i].y - dots[j].y;
const d2 = dx * dx + dy * dy;
if (d2 < dist2) {
const alpha = (1 - Math.sqrt(d2) / dist) * 0.6;
const hue = (dots[i].hue + dots[j].hue) / 2;
ctx.beginPath();
ctx.moveTo(dots[i].x, dots[i].y);
ctx.lineTo(dots[j].x, dots[j].y);
ctx.strokeStyle = `hsla(${hue}, 70%, 60%, ${alpha})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
}
}
}
// Mouse interaction - dots are gently pushed away
function mouseInteract() {
if (mouse.x < 0) return;
const mouseDist = 150;
for (const dot of dots) {
const dx = dot.x - mouse.x;
const dy = dot.y - mouse.y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < mouseDist && d > 0) {
const force = (1 - d / mouseDist) * 0.8;
dot.vx += (dx / d) * force;
dot.vy += (dy / d) * force;
}
}
}
function animate() {
// Trail effect
ctx.fillStyle = `rgba(10, 10, 15, ${settings.trail / 100})`;
ctx.fillRect(0, 0, W, H);
mouseInteract();
drawLines();
for (const dot of dots) {
dot.update();
dot.draw();
}
requestAnimationFrame(animate);
}
// Init
resize();
initDots();
animate();
window.addEventListener('resize', () => {
resize();
});
window.addEventListener('mousemove', (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
window.addEventListener('mouseleave', () => {
mouse.x = -9999;
mouse.y = -9999;
});
// Touch support
window.addEventListener('touchmove', (e) => {
mouse.x = e.touches[0].clientX;
mouse.y = e.touches[0].clientY;
});
window.addEventListener('touchend', () => {
mouse.x = -9999;
mouse.y = -9999;
});
// ─── Controls binding ───
function bindSlider(id, settingKey, suffix = '') {
const el = document.getElementById(id);
const valEl = document.getElementById(id + 'Val');
el.addEventListener('input', () => {
const val = parseFloat(el.value);
settings[settingKey] = val;
valEl.textContent = el.value + suffix;
// Reinit dots if count changed
if (settingKey === 'dotCount') {
initDots();
}
});
}
bindSlider('dotCount', 'dotCount');
bindSlider('speed', 'maxSpeed');
bindSlider('jumpChance', 'jumpChance', '%');
bindSlider('connDist', 'connDist');
bindSlider('trail', 'trail', '%');
bindSlider('glow', 'glow');
</script>
</body>
</html>