KINETIC MESH v2
by ThunderPhoenix57357 lines10.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>KINETIC MESH v2</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
body {
background-color: #030408;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
font-family: 'Segoe UI', system-ui, sans-serif;
}
canvas {
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
#ui-layer {
position: absolute;
bottom: 40px;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
pointer-events: none; /* Let clicks pass through to canvas */
}
#hint {
color: rgba(255, 255, 255, 0.3);
font-size: 0.85rem;
letter-spacing: 3px;
text-transform: uppercase;
transition: opacity 1s ease;
}
#rebuild-btn {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
padding: 12px 30px;
font-size: 1rem;
letter-spacing: 2px;
border-radius: 30px;
cursor: pointer;
pointer-events: auto; /* Re-enable clicks for the button */
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
#rebuild-btn:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
box-shadow: 0 0 20px rgba(255, 51, 153, 0.4);
}
#rebuild-btn:active {
transform: translateY(2px);
}
</style>
</head>
<body>
<div id="ui-layer">
<div id="hint">Drag to stretch. Swipe fast to tear.</div>
<button id="rebuild-btn" onclick="initMesh()">REBUILD</button>
</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 points = [];
let sticks = [];
let sparks = [];
// Physics & Material properties
const gravity = 0.4;
const friction = 0.98;
const spacing = 24;
const stiffness = 3;
const snapTension = 2.8; // How much it can stretch before breaking naturally
// Interaction
let mouse = { x: -1000, y: -1000, px: -1000, py: -1000, isDown: false, speed: 0 };
const grabDistance = 35;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
initMesh();
}
window.addEventListener('resize', resize);
class Point {
constructor(x, y, pinned = false) {
this.x = x;
this.y = y;
this.oldX = x;
this.oldY = y;
this.pinned = pinned;
}
update() {
if (this.pinned) return;
let vx = (this.x - this.oldX) * friction;
let vy = (this.y - this.oldY) * friction;
this.oldX = this.x;
this.oldY = this.y;
this.x += vx;
this.y += vy + gravity;
}
}
class Stick {
constructor(p0, p1) {
this.p0 = p0;
this.p1 = p1;
this.length = Math.hypot(p1.x - p0.x, p1.y - p0.y);
this.isActive = true;
this.tension = 0;
}
update() {
if (!this.isActive) return;
let dx = this.p1.x - this.p0.x;
let dy = this.p1.y - this.p0.y;
let distance = Math.hypot(dx, dy);
// Calculate tension for colors and snapping
this.tension = distance / this.length;
// Auto-snap if pulled too hard
if (this.tension > snapTension) {
this.tear();
return;
}
let difference = this.length - distance;
let percent = difference / distance / 2;
let offsetX = dx * percent;
let offsetY = dy * percent;
if (!this.p0.pinned) { this.p0.x -= offsetX; this.p0.y -= offsetY; }
if (!this.p1.pinned) { this.p1.x += offsetX; this.p1.y += offsetY; }
}
draw() {
if (!this.isActive) return;
// Color shifts from deep blue (220) to hot pink (330) based on tension
let hue = 220 + (Math.min(this.tension - 1, 1.5) * 80);
let alpha = Math.min(0.3 + (this.tension * 0.3), 1);
let width = Math.max(0.5, 3 - this.tension); // gets thinner as it stretches
ctx.strokeStyle = `hsla(${hue}, 100%, 65%, ${alpha})`;
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(this.p0.x, this.p0.y);
ctx.lineTo(this.p1.x, this.p1.y);
ctx.stroke();
}
tear() {
this.isActive = false;
createSparks((this.p0.x + this.p1.x) / 2, (this.p0.y + this.p1.y) / 2);
}
}
class Spark {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 15;
this.vy = (Math.random() - 0.5) * 15;
this.life = 1.0;
this.decay = Math.random() * 0.05 + 0.02;
this.hue = Math.random() * 60 + 280; // Pink/Purple sparks
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += gravity * 0.5; // Sparks float a bit
this.vx *= 0.95;
this.life -= this.decay;
}
draw() {
ctx.fillStyle = `hsla(${this.hue}, 100%, 70%, ${this.life})`;
ctx.beginPath();
ctx.arc(this.x, this.y, Math.random() * 2 + 1, 0, Math.PI * 2);
ctx.fill();
}
}
function createSparks(x, y) {
for(let i=0; i<4; i++) {
sparks.push(new Spark(x, y));
}
}
function initMesh() {
points = [];
sticks = [];
sparks = [];
// Make the mesh span most of the screen width, hanging from the top
let cols = Math.floor(width / spacing) - 2;
let rows = Math.floor(height / spacing) - 6;
// Centering
let startX = (width - cols * spacing) / 2;
let startY = 20;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
// Pin the top row
let p = new Point(startX + x * spacing, startY + y * spacing, y === 0);
points.push(p);
}
}
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let i = y * cols + x;
// Right connect
if (x < cols - 1) sticks.push(new Stick(points[i], points[i + 1]));
// Bottom connect
if (y < rows - 1) sticks.push(new Stick(points[i], points[i + cols]));
}
}
}
function interact() {
if (!mouse.isDown) return;
let dx = mouse.x - mouse.px;
let dy = mouse.y - mouse.py;
mouse.speed = Math.hypot(dx, dy);
points.forEach(p => {
let dist = Math.hypot(mouse.x - p.x, mouse.y - p.y);
if (dist < grabDistance) {
if (mouse.speed > 35) {
// Fast swipe cuts nearby threads
sticks.forEach(s => {
if (s.isActive && (s.p0 === p || s.p1 === p)) s.tear();
});
} else {
// Soft pull
p.x += dx * 0.6;
p.y += dy * 0.6;
}
}
});
}
function animate() {
// Dark trail effect for smooth motion
ctx.fillStyle = 'rgba(3, 4, 8, 0.3)';
ctx.fillRect(0, 0, width, height);
points.forEach(p => p.update());
for (let i = 0; i < stiffness; i++) {
sticks.forEach(s => s.update());
}
interact();
// Draw glow effects
ctx.globalCompositeOperation = 'lighter';
sticks.forEach(s => s.draw());
// Update and draw sparks
for (let i = sparks.length - 1; i >= 0; i--) {
sparks[i].update();
sparks[i].draw();
if (sparks[i].life <= 0) sparks.splice(i, 1);
}
ctx.globalCompositeOperation = 'source-over';
mouse.px = mouse.x;
mouse.py = mouse.y;
requestAnimationFrame(animate);
}
function setMouse(e) {
if (e.touches) {
mouse.x = e.touches[0].clientX;
mouse.y = e.touches[0].clientY;
} else {
mouse.x = e.clientX;
mouse.y = e.clientY;
}
}
window.addEventListener('mousedown', (e) => {
if (e.target.id === 'rebuild-btn') return;
mouse.isDown = true;
setMouse(e);
hint.style.opacity = '0';
});
window.addEventListener('mousemove', setMouse);
window.addEventListener('mouseup', () => { mouse.isDown = false; });
window.addEventListener('touchstart', (e) => {
if (e.target.id === 'rebuild-btn') return;
mouse.isDown = true;
setMouse(e);
hint.style.opacity = '0';
}, {passive: false});
window.addEventListener('touchmove', (e) => {
setMouse(e);
if (e.target.id !== 'rebuild-btn') e.preventDefault();
}, {passive: false});
window.addEventListener('touchend', () => { mouse.isDown = false; });
resize();
animate();
</script>
</body>
</html>
Game Source: KINETIC MESH v2
Creator: ThunderPhoenix57
Libraries: none
Complexity: complex (357 lines, 10.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: kinetic-mesh-v2-thunderphoenix57" to link back to the original. Then publish at arcadelab.ai/publish.