Skip to content

Instantly share code, notes, and snippets.

@Klaudioz
Created May 12, 2025 15:24
Show Gist options
  • Select an option

  • Save Klaudioz/6fe3b421b609825483d6bda0be77bb50 to your computer and use it in GitHub Desktop.

Select an option

Save Klaudioz/6fe3b421b609825483d6bda0be77bb50 to your computer and use it in GitHub Desktop.
A p5.js sketch simulating a "Living Mona Lisa" with an ethereal Sfumato flow effect. Features interactive particles, Perlin noise-driven movement, and a Mona Lisa-inspired color palette.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Living Mona Lisa - Sfumato Flow</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>
<style>
body { margin: 0; overflow: hidden; background-color: #0A0A08; /* Very dark base */ }
canvas { display: block; }
/* Optional: Add a subtle vignette effect to focus the center */
html::before {
content: '';
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
box-shadow: inset 0 0 15vw 5vw rgba(0,0,0,0.5); /* Darker edges */
pointer-events: none; /* Allows interaction with canvas */
z-index: 10;
}
</style>
</head>
<body>
<script>
let glazes = [];
let numGlazes = 150; // Number of "glaze" elements
let noiseTime = 0; // Time variable for Perlin noise evolution
// Colors inspired by the Mona Lisa's palette
const MONA_LISA_PALETTE = [
'#2A251E', // Deep Umber/Shadow
'#4B4339', // Dark Brown/Robe
'#6F6250', // Muted Brown/Mid-tone
'#9A8C73', // Light Brown/Ochre
'#585A45', // Dark Olive Green/Landscape
'#8D8A6E', // Muted Green/Light Landscape
'#C0A88C', // Fleshtone/Highlight
'#DBCDBE', // Pale Ochre/Sky Haze
'#332D25', // Another dark tone for depth
'#7E7360' // A mid-brown for blending
];
function setup() {
createCanvas(windowWidth, windowHeight);
// Initialize glazes
for (let i = 0; i < numGlazes; i++) {
glazes.push(new GlazeParticle(random(width), random(height)));
}
// Initial background, though it will be mostly overwritten
background(10, 10, 8);
}
function draw() {
// Apply a very translucent background to create fading/layering trails (sfumato)
fill(10, 10, 8, 20); // Dark, slightly transparent
noStroke();
rect(0, 0, width, height);
noiseTime += 0.0005; // Slowly evolve the noise field
for (let i = glazes.length - 1; i >= 0; i--) {
let glaze = glazes[i];
glaze.applyFlow();
glaze.interact();
glaze.update();
glaze.display();
if (glaze.isFaded()) {
// Replace faded glazes with new ones
glazes.splice(i, 1);
glazes.push(new GlazeParticle(random(width), random(height * 0.2, height * 0.8))); // New ones appear more centrally vertically
}
}
}
class GlazeParticle {
constructor(x, y) {
this.pos = createVector(x, y);
this.vel = createVector(random(-0.2, 0.2), random(-0.2, 0.2)); // Initial gentle drift
this.acc = createVector(0, 0);
this.baseColor = color(random(MONA_LISA_PALETTE));
this.lifespan = random(300, 800); // How long it "persists"
this.initialLifespan = this.lifespan;
this.maxOpacity = random(30, 90); // Max opacity for sfumato effect
this.currentOpacity = 0;
this.baseSize = random(50, 250); // Base size of the glaze patch
this.currentSize = this.baseSize;
this.aspectRatio = random(0.3, 0.7); // For elongated shapes, like brush strokes
this.noiseOffsetX = random(1000); // Per-particle noise offset for unique movement
this.noiseOffsetY = random(2000);
this.rotation = random(TWO_PI);
this.rotationSpeed = random(-0.002, 0.002);
}
applyForce(force) {
this.acc.add(force);
}
// Apply a gentle, evolving flow field
applyFlow() {
let noiseScale = 0.002;
let angle = noise(this.pos.x * noiseScale + this.noiseOffsetX,
this.pos.y * noiseScale + this.noiseOffsetY,
noiseTime) * TWO_PI * 3; // Larger angle multiplier for more swirl
let flow = p5.Vector.fromAngle(angle);
flow.mult(0.05); // Very gentle force
this.applyForce(flow);
}
// Mouse interaction
interact() {
if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) {
let mouseVec = createVector(mouseX, mouseY);
let distToMouse = p5.Vector.dist(this.pos, mouseVec);
let interactionRadius = 150;
if (distToMouse < interactionRadius) {
// Gentle repulsion/disturbance from mouse
let repel = p5.Vector.sub(this.pos, mouseVec);
repel.normalize();
// Strength of repulsion inversely proportional to distance
let strength = map(distToMouse, 0, interactionRadius, 0.3, 0.01);
repel.mult(strength);
this.applyForce(repel);
// Slightly increase opacity or "awaken" the glaze when mouse is near
this.currentOpacity = min(this.maxOpacity + 20, 120); // Temporarily boost opacity
}
}
}
update() {
this.vel.add(this.acc);
this.vel.limit(0.5); // Max speed
this.pos.add(this.vel);
this.acc.mult(0); // Reset acceleration
this.lifespan--;
this.rotation += this.rotationSpeed;
// Opacity: fade in, stay, fade out
if (this.lifespan > this.initialLifespan - 100) { // Fading in
this.currentOpacity = map(this.lifespan, this.initialLifespan, this.initialLifespan - 100, 0, this.maxOpacity);
} else if (this.lifespan < 150) { // Fading out
this.currentOpacity = map(this.lifespan, 0, 150, 0, this.maxOpacity);
} else { // Stable period
this.currentOpacity = this.maxOpacity;
}
// Size pulsing slightly or shrinking with age
this.currentSize = this.baseSize * (0.8 + 0.2 * sin(this.lifespan * 0.05));
this.currentSize *= map(this.lifespan, 0, this.initialLifespan, 0.5, 1); // Shrink as it dies
// Keep within bounds by wrapping around (softly)
if (this.pos.x > width + this.currentSize) this.pos.x = -this.currentSize;
if (this.pos.x < -this.currentSize) this.pos.x = width + this.currentSize;
if (this.pos.y > height + this.currentSize) this.pos.y = -this.currentSize;
if (this.pos.y < -this.currentSize) this.pos.y = height + this.currentSize;
}
display() {
push(); // Isolate transformations
translate(this.pos.x, this.pos.y);
rotate(this.rotation);
// Set fill color with current opacity
let displayColor = color(red(this.baseColor), green(this.baseColor), blue(this.baseColor), this.currentOpacity);
fill(displayColor);
noStroke();
// Draw as an elongated ellipse to simulate a soft brush stroke or glaze
ellipse(0, 0, this.currentSize, this.currentSize * this.aspectRatio);
pop(); // Restore transformations
}
isFaded() {
return this.lifespan <= 0;
}
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
// Could re-initialize or simply let existing particles adapt.
// For simplicity, we'll let them adapt. A full reset might be jarring.
background(10,10,8); // Reset background on resize
}
// Optional: Add a subtle message or title
// function mousePressed() {
// // Example: could trigger a new burst of particles or change a parameter
// }
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment