/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Particle class: represents a single particle in the swarm. var Particle = Container.expand(function () { var self = Container.call(this); // Attach the particle asset, centered var gfx = self.attachAsset('particle', { anchorX: 0.5, anchorY: 0.5 }); // Particle properties self.vx = 0; self.vy = 0; self.tx = 0; // target x self.ty = 0; // target y self.speed = 0.55 + Math.random() * 0.15; // much faster following self.color = 0xffffff; gfx.tint = self.color; // Animate color change self.setColor = function (newColor) { var startColor = gfx.tint; // Use a short duration for smooth, fast color transitions tween(gfx, { tint: newColor }, { duration: 200, easing: tween.cubicOut }); self.color = newColor; }; // Update method: move towards target self.update = function () { // Move towards target (tx, ty) with some inertia var dx = self.tx - self.x; var dy = self.ty - self.y; self.vx += dx * self.speed * 0.12; self.vy += dy * self.speed * 0.12; // --- Simple physics additions --- // Gravity (downwards) self.vy += 0.02; // Removed neighbor repulsion for performance // Damping for smoothness self.vx *= 0.85; self.vy *= 0.85; self.x += self.vx; self.y += self.vy; // Clamp position to stay within screen bounds if (self.x < 0) { self.x = 0; self.vx = 0; } if (self.x > 2048) { self.x = 2048; self.vx = 0; } if (self.y < 0) { self.y = 0; self.vy = 0; } if (self.y > 2732) { self.y = 2732; self.vy = 0; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 // Pure black background }); /**** * Game Code ****/ // We'll use the tween plugin for smooth color transitions. // We'll use simple colored ellipses for particles, and a background color for the game. // Number of particles: balance between performance and visual density var PARTICLE_COUNT = 800; // Lowered to 800 for much better performance on all devices // Particle storage var particles = []; // Swarm target position (where particles move towards) var swarmTarget = { x: 1024, y: 1366 }; // Start at center // Shape morphing: list of shape functions var shapes = [ // Circle function (i, N) { var angle = i / N * Math.PI * 2; var radius = 600; return { x: 1024 + Math.cos(angle) * radius, y: 1366 + Math.sin(angle) * radius }; }, // Horizontal wave function (i, N) { var x = 300 + i / N * 1448; var y = 1366 + Math.sin(i / N * Math.PI * 4 + LK.ticks * 0.01) * 400; return { x: x, y: y }; }, // Heart shape function (i, N) { var t = i / N * Math.PI * 2; var scale = 400; var x = 1024 + scale * 16 * Math.pow(Math.sin(t), 3); var y = 1200 - scale * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)); return { x: x, y: y }; }, // Spiral function (i, N) { var t = i / N * Math.PI * 8; var r = 80 + 500 * (i / N); var x = 1024 + Math.cos(t) * r; var y = 1366 + Math.sin(t) * r; return { x: x, y: y }; }, // Random cloud function (i, N) { var angle = i / N * Math.PI * 2; var radius = 400 + Math.sin(LK.ticks * 0.01 + i) * 120 + Math.random() * 40; return { x: 1024 + Math.cos(angle) * radius, y: 1366 + Math.sin(angle) * radius }; }, // Star shape function (i, N) { var angle = i / N * Math.PI * 2; var spikes = 5; var outerRadius = 600; var innerRadius = 250; var spike = Math.floor(i / (N / (spikes * 2))); var r = spike % 2 === 0 ? outerRadius : innerRadius; return { x: 1024 + Math.cos(angle) * r, y: 1366 + Math.sin(angle) * r }; }, // Square function (i, N) { var side = Math.floor(i / (N / 4)); var pos = i % (N / 4) / (N / 4); var size = 900; if (side === 0) { return { x: 1024 - size / 2 + pos * size, y: 1366 - size / 2 }; } else if (side === 1) { return { x: 1024 + size / 2, y: 1366 - size / 2 + pos * size }; } else if (side === 2) { return { x: 1024 + size / 2 - pos * size, y: 1366 + size / 2 }; } else { return { x: 1024 - size / 2, y: 1366 + size / 2 - pos * size }; } }, // Infinity (lemniscate) function (i, N) { var t = i / N * Math.PI * 2; var a = 350; var x = 1024 + a * Math.cos(t) / (1 + Math.sin(t) * Math.sin(t)); var y = 1366 + a * Math.sin(t) * Math.cos(t) / (1 + Math.sin(t) * Math.sin(t)); return { x: x, y: y }; }, // Flower (petals) function (i, N) { var angle = i / N * Math.PI * 2; var petals = 7; var r = 400 + 180 * Math.sin(petals * angle + LK.ticks * 0.01); return { x: 1024 + Math.cos(angle) * r, y: 1366 + Math.sin(angle) * r }; }]; // Current shape index and morph progress var currentShape = 0; var nextShape = 1; var morphProgress = 0; // 0 to 1 // Morph every X seconds var MORPH_TIME = 240; // frames (4 seconds) var morphTimer = 0; // Precompute target positions for shapes var shapeTargets = [[], []]; // Initialize particles for (var i = 0; i < PARTICLE_COUNT; i++) { var p = new Particle(); // Start at random position near center p.x = 1024 + (Math.random() - 0.5) * 200; p.y = 1366 + (Math.random() - 0.5) * 200; p.tx = p.x; p.ty = p.y; p._particleIndex = i; // Assign index for neighbor repulsion particles.push(p); game.addChild(p); } // Function to update shape targets function updateShapeTargets() { var N = PARTICLE_COUNT; for (var s = 0; s < 2; s++) { var shapeFn = shapes[s === 0 ? currentShape : nextShape]; for (var i = 0; i < N; i++) { shapeTargets[s][i] = shapeFn(i, N); } } } updateShapeTargets(); // Touch/mouse handling var isTouching = false; var lastTouch = { x: 1024, y: 1366 }; // Move handler: update swarm target to touch position function handleMove(x, y, obj) { // Clamp to game area if (x < 0) x = 0; if (x > 2048) x = 2048; if (y < 0) y = 0; if (y > 2732) y = 2732; swarmTarget.x = x; swarmTarget.y = y; lastTouch.x = x; lastTouch.y = y; isTouching = true; } game.move = handleMove; // Down handler: start following touch game.down = function (x, y, obj) { handleMove(x, y, obj); isTouching = true; }; // Up handler: stop following touch, revert to shape morphing game.up = function (x, y, obj) { isTouching = false; }; // (morphColors function removed, color is now global and animated in game.update) // Main update loop game.update = function () { // Shape morphing logic morphTimer++; if (morphTimer >= MORPH_TIME) { morphTimer = 0; currentShape = nextShape; nextShape = (nextShape + 1) % shapes.length; morphProgress = 0; updateShapeTargets(); // morphColors(); // Remove per-particle color morphing } if (!isTouching) { morphProgress += 1 / MORPH_TIME; if (morphProgress > 1) morphProgress = 1; } else { morphProgress = 0; } // Calculate a global color that changes over time var t = LK.ticks * 0.015; var r = Math.floor(180 + Math.sin(t) * 75); var g = Math.floor(180 + Math.sin(t + 2) * 75); var b = Math.floor(180 + Math.sin(t + 4) * 75); var globalColor = r << 16 | g << 8 | b; // For each particle, set its target and color for (var i = 0; i < particles.length; i++) { var p = particles[i]; // Only update if on screen (skip if far outside, for extra FPS) if (p.x > -32 && p.x < 2080 && p.y > -32 && p.y < 2764) { if (isTouching) { // Snake-like following: first particle follows touch, each next follows previous if (i === 0) { p.tx = swarmTarget.x; p.ty = swarmTarget.y; } else { // Follow previous particle with a delay/lag for snake effect var prev = particles[i - 1]; // Distance between particles in the chain var followDist = 12; var dx = prev.x - p.x; var dy = prev.y - p.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > followDist) { // Move target to a point followDist behind the previous particle var t = followDist / dist; p.tx = p.x + dx * t; p.ty = p.y + dy * t; } else { // Stay in place if close enough p.tx = p.x; p.ty = p.y; } } } else { // Morph between shapes var t0 = shapeTargets[0][i]; var t1 = shapeTargets[1][i]; var tMorph = morphProgress; p.tx = t0.x * (1 - tMorph) + t1.x * tMorph; p.ty = t0.y * (1 - tMorph) + t1.y * tMorph; } // Set all particles to the same color, but only tween the first particle and copy the tint to others for performance if (i === 0) { if (i === 0) { p.setColor(globalColor); var batchedTint = p.children[0].tint; } if (i > 0 && typeof batchedTint !== "undefined") { p.children[0].tint = batchedTint; } var batchedTint = p.children[0].tint; } if (i > 0 && typeof batchedTint !== "undefined") { p.children[0].tint = batchedTint; } p.update(); } else if (i % 8 === LK.ticks % 8) { // For offscreen particles, update less frequently (every 8th frame) p.update(); } } }; // No GUI or score, as this is a pure interactive art experience.
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Particle class: represents a single particle in the swarm.
var Particle = Container.expand(function () {
var self = Container.call(this);
// Attach the particle asset, centered
var gfx = self.attachAsset('particle', {
anchorX: 0.5,
anchorY: 0.5
});
// Particle properties
self.vx = 0;
self.vy = 0;
self.tx = 0; // target x
self.ty = 0; // target y
self.speed = 0.55 + Math.random() * 0.15; // much faster following
self.color = 0xffffff;
gfx.tint = self.color;
// Animate color change
self.setColor = function (newColor) {
var startColor = gfx.tint;
// Use a short duration for smooth, fast color transitions
tween(gfx, {
tint: newColor
}, {
duration: 200,
easing: tween.cubicOut
});
self.color = newColor;
};
// Update method: move towards target
self.update = function () {
// Move towards target (tx, ty) with some inertia
var dx = self.tx - self.x;
var dy = self.ty - self.y;
self.vx += dx * self.speed * 0.12;
self.vy += dy * self.speed * 0.12;
// --- Simple physics additions ---
// Gravity (downwards)
self.vy += 0.02;
// Removed neighbor repulsion for performance
// Damping for smoothness
self.vx *= 0.85;
self.vy *= 0.85;
self.x += self.vx;
self.y += self.vy;
// Clamp position to stay within screen bounds
if (self.x < 0) {
self.x = 0;
self.vx = 0;
}
if (self.x > 2048) {
self.x = 2048;
self.vx = 0;
}
if (self.y < 0) {
self.y = 0;
self.vy = 0;
}
if (self.y > 2732) {
self.y = 2732;
self.vy = 0;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000 // Pure black background
});
/****
* Game Code
****/
// We'll use the tween plugin for smooth color transitions.
// We'll use simple colored ellipses for particles, and a background color for the game.
// Number of particles: balance between performance and visual density
var PARTICLE_COUNT = 800; // Lowered to 800 for much better performance on all devices
// Particle storage
var particles = [];
// Swarm target position (where particles move towards)
var swarmTarget = {
x: 1024,
y: 1366
}; // Start at center
// Shape morphing: list of shape functions
var shapes = [
// Circle
function (i, N) {
var angle = i / N * Math.PI * 2;
var radius = 600;
return {
x: 1024 + Math.cos(angle) * radius,
y: 1366 + Math.sin(angle) * radius
};
},
// Horizontal wave
function (i, N) {
var x = 300 + i / N * 1448;
var y = 1366 + Math.sin(i / N * Math.PI * 4 + LK.ticks * 0.01) * 400;
return {
x: x,
y: y
};
},
// Heart shape
function (i, N) {
var t = i / N * Math.PI * 2;
var scale = 400;
var x = 1024 + scale * 16 * Math.pow(Math.sin(t), 3);
var y = 1200 - scale * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
return {
x: x,
y: y
};
},
// Spiral
function (i, N) {
var t = i / N * Math.PI * 8;
var r = 80 + 500 * (i / N);
var x = 1024 + Math.cos(t) * r;
var y = 1366 + Math.sin(t) * r;
return {
x: x,
y: y
};
},
// Random cloud
function (i, N) {
var angle = i / N * Math.PI * 2;
var radius = 400 + Math.sin(LK.ticks * 0.01 + i) * 120 + Math.random() * 40;
return {
x: 1024 + Math.cos(angle) * radius,
y: 1366 + Math.sin(angle) * radius
};
},
// Star shape
function (i, N) {
var angle = i / N * Math.PI * 2;
var spikes = 5;
var outerRadius = 600;
var innerRadius = 250;
var spike = Math.floor(i / (N / (spikes * 2)));
var r = spike % 2 === 0 ? outerRadius : innerRadius;
return {
x: 1024 + Math.cos(angle) * r,
y: 1366 + Math.sin(angle) * r
};
},
// Square
function (i, N) {
var side = Math.floor(i / (N / 4));
var pos = i % (N / 4) / (N / 4);
var size = 900;
if (side === 0) {
return {
x: 1024 - size / 2 + pos * size,
y: 1366 - size / 2
};
} else if (side === 1) {
return {
x: 1024 + size / 2,
y: 1366 - size / 2 + pos * size
};
} else if (side === 2) {
return {
x: 1024 + size / 2 - pos * size,
y: 1366 + size / 2
};
} else {
return {
x: 1024 - size / 2,
y: 1366 + size / 2 - pos * size
};
}
},
// Infinity (lemniscate)
function (i, N) {
var t = i / N * Math.PI * 2;
var a = 350;
var x = 1024 + a * Math.cos(t) / (1 + Math.sin(t) * Math.sin(t));
var y = 1366 + a * Math.sin(t) * Math.cos(t) / (1 + Math.sin(t) * Math.sin(t));
return {
x: x,
y: y
};
},
// Flower (petals)
function (i, N) {
var angle = i / N * Math.PI * 2;
var petals = 7;
var r = 400 + 180 * Math.sin(petals * angle + LK.ticks * 0.01);
return {
x: 1024 + Math.cos(angle) * r,
y: 1366 + Math.sin(angle) * r
};
}];
// Current shape index and morph progress
var currentShape = 0;
var nextShape = 1;
var morphProgress = 0; // 0 to 1
// Morph every X seconds
var MORPH_TIME = 240; // frames (4 seconds)
var morphTimer = 0;
// Precompute target positions for shapes
var shapeTargets = [[], []];
// Initialize particles
for (var i = 0; i < PARTICLE_COUNT; i++) {
var p = new Particle();
// Start at random position near center
p.x = 1024 + (Math.random() - 0.5) * 200;
p.y = 1366 + (Math.random() - 0.5) * 200;
p.tx = p.x;
p.ty = p.y;
p._particleIndex = i; // Assign index for neighbor repulsion
particles.push(p);
game.addChild(p);
}
// Function to update shape targets
function updateShapeTargets() {
var N = PARTICLE_COUNT;
for (var s = 0; s < 2; s++) {
var shapeFn = shapes[s === 0 ? currentShape : nextShape];
for (var i = 0; i < N; i++) {
shapeTargets[s][i] = shapeFn(i, N);
}
}
}
updateShapeTargets();
// Touch/mouse handling
var isTouching = false;
var lastTouch = {
x: 1024,
y: 1366
};
// Move handler: update swarm target to touch position
function handleMove(x, y, obj) {
// Clamp to game area
if (x < 0) x = 0;
if (x > 2048) x = 2048;
if (y < 0) y = 0;
if (y > 2732) y = 2732;
swarmTarget.x = x;
swarmTarget.y = y;
lastTouch.x = x;
lastTouch.y = y;
isTouching = true;
}
game.move = handleMove;
// Down handler: start following touch
game.down = function (x, y, obj) {
handleMove(x, y, obj);
isTouching = true;
};
// Up handler: stop following touch, revert to shape morphing
game.up = function (x, y, obj) {
isTouching = false;
};
// (morphColors function removed, color is now global and animated in game.update)
// Main update loop
game.update = function () {
// Shape morphing logic
morphTimer++;
if (morphTimer >= MORPH_TIME) {
morphTimer = 0;
currentShape = nextShape;
nextShape = (nextShape + 1) % shapes.length;
morphProgress = 0;
updateShapeTargets();
// morphColors(); // Remove per-particle color morphing
}
if (!isTouching) {
morphProgress += 1 / MORPH_TIME;
if (morphProgress > 1) morphProgress = 1;
} else {
morphProgress = 0;
}
// Calculate a global color that changes over time
var t = LK.ticks * 0.015;
var r = Math.floor(180 + Math.sin(t) * 75);
var g = Math.floor(180 + Math.sin(t + 2) * 75);
var b = Math.floor(180 + Math.sin(t + 4) * 75);
var globalColor = r << 16 | g << 8 | b;
// For each particle, set its target and color
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
// Only update if on screen (skip if far outside, for extra FPS)
if (p.x > -32 && p.x < 2080 && p.y > -32 && p.y < 2764) {
if (isTouching) {
// Snake-like following: first particle follows touch, each next follows previous
if (i === 0) {
p.tx = swarmTarget.x;
p.ty = swarmTarget.y;
} else {
// Follow previous particle with a delay/lag for snake effect
var prev = particles[i - 1];
// Distance between particles in the chain
var followDist = 12;
var dx = prev.x - p.x;
var dy = prev.y - p.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > followDist) {
// Move target to a point followDist behind the previous particle
var t = followDist / dist;
p.tx = p.x + dx * t;
p.ty = p.y + dy * t;
} else {
// Stay in place if close enough
p.tx = p.x;
p.ty = p.y;
}
}
} else {
// Morph between shapes
var t0 = shapeTargets[0][i];
var t1 = shapeTargets[1][i];
var tMorph = morphProgress;
p.tx = t0.x * (1 - tMorph) + t1.x * tMorph;
p.ty = t0.y * (1 - tMorph) + t1.y * tMorph;
}
// Set all particles to the same color, but only tween the first particle and copy the tint to others for performance
if (i === 0) {
if (i === 0) {
p.setColor(globalColor);
var batchedTint = p.children[0].tint;
}
if (i > 0 && typeof batchedTint !== "undefined") {
p.children[0].tint = batchedTint;
}
var batchedTint = p.children[0].tint;
}
if (i > 0 && typeof batchedTint !== "undefined") {
p.children[0].tint = batchedTint;
}
p.update();
} else if (i % 8 === LK.ticks % 8) {
// For offscreen particles, update less frequently (every 8th frame)
p.update();
}
}
};
// No GUI or score, as this is a pure interactive art experience.