CHROMA BOARD
by ThunderPhoenix57239 lines7.2 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>CHROMA BOARD</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
body {
background-color: #050505;
overflow: hidden;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', sans-serif;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
#hint {
position: absolute;
top: 30px;
width: 100%;
text-align: center;
color: rgba(255, 255, 255, 0.2);
font-size: 0.85rem;
letter-spacing: 6px;
pointer-events: none;
text-transform: uppercase;
transition: opacity 1s ease;
}
</style>
</head>
<body>
<div id="hint">SWIPE & TAP</div>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const hint = document.getElementById('hint');
let width, height;
let grid = [];
let ripples = [];
// Performance and Visual Tuning
const SPACING = 25; // Distance between dots
const BASE_RADIUS = 1.5;
const MAX_RADIUS = 10;
const MOUSE_INFLUENCE = 120; // How wide your brush is
let pointer = { x: -1000, y: -1000, active: false };
let time = 0;
class Dot {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = BASE_RADIUS;
this.hue = 200;
this.lightness = 15;
// Base offset for organic idle movement
this.offset = (x + y) * 0.01;
}
update() {
let targetRadius = BASE_RADIUS;
let targetLightness = 15;
let targetHue = this.hue;
// 1. Mouse Interaction (Hover/Swipe)
if (pointer.active) {
let dx = pointer.x - this.x;
let dy = pointer.y - this.y;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist < MOUSE_INFLUENCE) {
let intensity = 1 - (dist / MOUSE_INFLUENCE);
targetRadius = BASE_RADIUS + (MAX_RADIUS * intensity);
targetLightness = 15 + (60 * intensity);
targetHue = (time * 50 + intensity * 100) % 360;
}
}
// 2. Ripple Interaction (Clicks)
for (let i = 0; i < ripples.length; i++) {
let r = ripples[i];
let dx = r.x - this.x;
let dy = r.y - this.y;
let dist = Math.sqrt(dx * dx + dy * dy);
// If dot is sitting on the expanding ripple ring
if (Math.abs(dist - r.radius) < r.thickness) {
let intensity = 1 - (Math.abs(dist - r.radius) / r.thickness);
let fade = Math.max(0, 1 - (r.radius / r.maxRadius));
targetRadius = Math.max(targetRadius, BASE_RADIUS + (MAX_RADIUS * 1.5 * intensity * fade));
targetLightness = Math.max(targetLightness, 15 + (85 * intensity * fade));
targetHue = r.hue;
}
}
// 3. Smooth Lerp (Interpolation) for buttery animation without physics lag
this.radius += (targetRadius - this.radius) * 0.15;
this.lightness += (targetLightness - this.lightness) * 0.1;
// Idle breathing effect
let idleBreath = Math.sin(time + this.offset) * 0.5;
this.displayRadius = Math.max(0.1, this.radius + idleBreath);
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.displayRadius, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${this.hue}, 80%, ${this.lightness}%)`;
ctx.fill();
}
}
class Ripple {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 0;
this.maxRadius = Math.max(width, height) * 0.8;
this.thickness = 60;
this.speed = 15;
this.hue = (time * 100) % 360;
this.active = true;
}
update() {
this.radius += this.speed;
this.thickness += 1; // Spreads out as it expands
if (this.radius > this.maxRadius) this.active = false;
}
}
function initGrid() {
grid = [];
// Add padding to ensure dots reach the edges beautifully
let cols = Math.ceil(width / SPACING) + 2;
let rows = Math.ceil(height / SPACING) + 2;
let startX = (width - (cols * SPACING)) / 2;
let startY = (height - (rows * SPACING)) / 2;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
grid.push(new Dot(startX + x * SPACING, startY + y * SPACING));
}
}
}
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
initGrid();
}
window.addEventListener('resize', resize);
resize();
function animate() {
// Pure black clear, no trailing effects, highly performant
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, width, height);
time += 0.05;
// Update & clear dead ripples
for (let i = ripples.length - 1; i >= 0; i--) {
ripples[i].update();
if (!ripples[i].active) ripples.splice(i, 1);
}
// Draw grid
for (let i = 0; i < grid.length; i++) {
grid[i].update();
grid[i].draw();
}
requestAnimationFrame(animate);
}
// Input Handling
function setPointer(e) {
if (e.touches) {
pointer.x = e.touches[0].clientX;
pointer.y = e.touches[0].clientY;
} else {
pointer.x = e.clientX;
pointer.y = e.clientY;
}
}
window.addEventListener('mousedown', (e) => {
pointer.active = true;
setPointer(e);
ripples.push(new Ripple(pointer.x, pointer.y));
hint.style.opacity = '0';
});
window.addEventListener('mousemove', (e) => {
if(pointer.active) setPointer(e);
});
window.addEventListener('mouseup', () => { pointer.active = false; });
window.addEventListener('touchstart', (e) => {
pointer.active = true;
setPointer(e);
ripples.push(new Ripple(pointer.x, pointer.y));
hint.style.opacity = '0';
}, {passive: false});
window.addEventListener('touchmove', (e) => {
setPointer(e);
e.preventDefault(); // Prevents scrolling on mobile
}, {passive: false});
window.addEventListener('touchend', () => { pointer.active = false; });
animate();
</script>
</body>
</html>
Game Source: CHROMA BOARD
Creator: ThunderPhoenix57
Libraries: none
Complexity: complex (239 lines, 7.2 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: chroma-board-thunderphoenix57" to link back to the original. Then publish at arcadelab.ai/publish.