let img;
let tiles = [];
let mode = 0; // 0 = image, 1 = mosaic, 2 = radius reveal
let timeCounter = 0;
let params = {
fillPercent: 1,
appearSpeed: 0.08,
minDelay: 0,
maxDelay: 30,
waveCount: 1,
contentJumble: 0.1,
prog1: 50,
prog2: 70,
prog3: 100,
// Ripple Specifics
waveSpeed: 2, // Pixels per frame the wave travels
rippleStrength: 1.4,
rippleLife: 0.05 // How fast it returns to normal
};
let waveOrigins = [];
let viewRadius = 150;
function preload() {
img = loadImage("Asset 4.png");
}
function setup() {
createCanvas(500, 500);
imageMode(CENTER);
rectMode(CENTER);
noStroke();
buildGrid(); // Initialize first grid
}
function draw() {
background("#000000");
if (mode === 0) {
image(img, width / 2, height / 2, width, height);
return;
}
timeCounter++;
let done = true;
for (let t of tiles) {
t.update(timeCounter);
t.show();
if (!t.done) done = false;
}
if (mode === 1 && done && !mouseIsPressed) noLoop();
}
function mousePressed() {
// Trigger the spreading ripple
let clickX = mouseX;
let clickY = mouseY;
for (let t of tiles) {
t.triggerRipple(clickX, clickY, timeCounter);
}
loop();
}
function keyPressed() {
if (key === '1' || key === '2') {
tiles = [];
timeCounter = 0;
waveOrigins = [];
for (let i = 0; i < params.waveCount; i++) {
waveOrigins.push({
x: random(width * 0.1, width * 0.9),
y: random(height * 0.1, height * 0.9)
});
}
buildGrid();
mode = int(key);
loop();
}
if (key === '0') {
mode = 0;
loop();
}
}
function buildGrid() {
tiles = [];
let base = params.prog1;
let cols = floor(width / base);
let rows = floor(height / base);
let occupied = Array(cols).fill().map(() => Array(rows).fill(false));
let sizes = [params.prog1, params.prog2, params.prog3];
for (let j = 0; j < rows; j++) {
for (let i = 0; i < cols; i++) {
if (occupied[i][j]) continue;
if (random() > params.fillPercent) continue;
let shuffled = shuffle(sizes.slice());
for (let s of shuffled) {
let wCells = floor(s / base);
let hCells = wCells;
if (i + wCells > cols || j + hCells > rows) continue;
let fits = true;
for (let dx = 0; dx < wCells; dx++) {
for (let dy = 0; dy < hCells; dy++) {
if (occupied[i + dx][j + dy]) { fits = false; break; }
}
if (!fits) break;
}
if (!fits) continue;
for (let dx = 0; dx < wCells; dx++) {
for (let dy = 0; dy < hCells; dy++) {
occupied[i + dx][j + dy] = true;
}
}
let x = i * base;
let y = j * base;
let offsetX = random(-params.contentJumble, params.contentJumble) * img.width * 0.5;
let offsetY = random(-params.contentJumble, params.contentJumble) * img.height * 0.5;
let imgX = constrain(int(map(x, 0, width, 0, img.width) + offsetX), 0, img.width - s);
let imgY = constrain(int(map(y, 0, height, 0, img.height) + offsetY), 0, img.height - s);
let cropped = img.get(imgX, imgY, s, s);
let minD = Infinity;
for (let o of waveOrigins) {
let d = dist(x + s / 2, y + s / 2, o.x, o.y);
if (d < minD) minD = d;
}
let norm = map(minD, 0, dist(0,0,width,height), 0, 1);
let delay = lerp(params.minDelay, params.maxDelay, norm);
tiles.push(new Tile(x, y, s, cropped, delay));
break;
}
}
}
}
class Tile {
constructor(x, y, s, imgCrop, delay) {
this.x = x;
this.y = y;
this.baseS = s;
this.currentS = 0; // Start at 0 for the entrance animation
this.targetS = s;
this.img = imgCrop;
this.delay = delay;
this.t = 0;
this.opacity = 0;
this.done = false;
// Ripple properties
this.rippleTriggerTime = -1;
this.isRippling = false;
}
triggerRipple(mx, my, currentTime) {
let d = dist(this.x + this.baseS/2, this.y + this.baseS/2, mx, my);
// Calculate when the wave hits this specific tile
this.rippleTriggerTime = currentTime + (d / params.waveSpeed);
this.isRippling = true;
}
update(counter) {
// 1. Handle Initial Appearance (from original code)
if (counter >= this.delay && !this.done) {
this.t += params.appearSpeed;
let tNorm = min(this.t, 1);
this.currentS = this.baseS * easeOutBack(tNorm);
this.opacity = tNorm;
if (this.t >= 1) {
this.done = true;
this.currentS = this.baseS;
}
}
// 2. Handle Click Ripple (Spreading Wave)
if (this.isRippling && counter >= this.rippleTriggerTime) {
this.targetS = this.baseS * params.rippleStrength;
this.isRippling = false; // Triggered!
}
// Smoothly settle back to base size
if (this.done) {
this.currentS = lerp(this.currentS, this.targetS, 0.15);
this.targetS = lerp(this.targetS, this.baseS, params.rippleLife);
}
}
show() {
if (timeCounter < this.delay) return;
if (mode === 2) {
let d = dist(mouseX, mouseY, this.x + this.baseS / 2, this.y + this.baseS / 2);
if (d > viewRadius) return;
}
push();
translate(this.x + this.baseS / 2, this.y + this.baseS / 2);
tint(255, this.opacity * 255);
image(this.img, 0, 0, this.currentS + 1, this.currentS + 1);
noFill();
stroke("#edff00");
strokeWeight(1);
rect(0, 0, this.currentS, this.currentS);
pop();
}
}
function easeOutBack(t) {
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2);
}