Initial commit: phonestream project
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user