AI Ink Flow Meditation - Zen Calligraphy Brush Simulation - xelsed.ai

57 views ยท Dec 14, 2025

Experience the tranquility of Zen calligraphy. Paint flowing ink strokes that pool, bleed, and dry organically on aged rice paper. Move slowly for thick pooling strokes, quickly for sharp lines. Watch cherry blossoms drift across your canvas when you pause. Created with xelsed-alpha6-flash.

#xelsedia#xelsedai#generativeart#p5js#creativecoding#calligraphy#zen#meditation#interactive
๐Ÿ”€ Remix This ๐Ÿ“– View Docs โ›ถ Full Screen โ† Back to Gallery
๐Ÿ“‹ Embed Code

Copy this code to embed this sketch on your website:

<iframe src="https://p5js.ai/embed/69d114a6-d477-4d37-bb90-a8843304a494" width="400" height="400" frameborder="0" allow="autoplay" title="AI Ink Flow Meditation - Zen Calligraphy Brush Simulation - xelsed.ai"></iframe>

Adjust width and height as needed. Preview embed โ†’

โ“ About This Sketch 3 questions

What visual experience does the AI Ink Flow Meditation sketch provide?

The sketch creates a tranquil visual experience by simulating flowing ink strokes on aged rice paper, which pool, bleed, and dry organically, accompanied by drifting cherry blossoms when the user pauses.

How can users interact with the Zen Calligraphy Brush simulation?

Users can interact by moving their mouse to create ink strokes; moving slowly produces thick, pooling strokes while quick movements generate sharp lines.

What creative coding concepts are showcased in this p5.js sketch?

The sketch demonstrates techniques such as real-time stroke rendering, dynamic wetness effects for ink simulation, and blending modes for achieving organic visual effects.

๐Ÿ’ฌ AI Chat History 2 messages
๐Ÿ‘ค You
Create an immersive Zen calligraphy experience. Background is aged rice paper texture (warm cream/beige with subtle fiber patterns). Mouse movements paint flowing black ink strokes that respond to speed - fast movements create thin sharp lines, slow movements create thick pooling strokes. The ink should: 1) Flow and bleed realistically using Perlin noise for organic edges, 2) Pool darker where strokes overlap or slow down, 3) Gradually 'dry' over time becoming slightly lighter and more defined, 4) Have subtle paper absorption effects where ink bleeds into fibers. Add a gentle ripple effect when new ink touches the canvas. Use blendMode(MULTIPLY) for layering. Include a soft gradient vignette around edges. When mouse is still for 3+ seconds, add floating cherry blossom petals that drift across the canvas. The overall effect should be peaceful, meditative, and authentic to Asian calligraphy aesthetics.
๐Ÿค– AI
This is a wonderful and evocative request! Creating an immersive Zen calligraphy experience with realistic ink physics is a fascinating challenge.

I've designed a p5.js sketch that attempts to capture all the elements you've described:

1. **Aged Rice Paper Background:** A warm cream/beige background with subtle, animating fiber patterns created using Perlin noise.
2. **Flowing Black Ink Strokes:**
* Tracks mouse speed to vary `strokeWeight`: faster movements create thinner lines, slower movements create thicker, pooling strokes.
* Uses `curveVertex()` with `beginShape()` and `endShape()` to create smooth, flowing lines.
* Perlin noise is applied along the edges of the stroke, combined with `blendMode(ADD)`, to simulate organic bleeding and absorption into the paper fibers.
* `blendMode(MULTIPLY)` is used for the main stroke body, ensuring that overlapping strokes darken realistically.
* Ink `wetness` is tracked for each stroke, gradually decreasing over time.
* As ink dries, it becomes slightly lighter and more "defined" (the bleed effect lessens). Dry strokes are eventually removed to maintain performance.
3. **Gentle Ripple Effect:** A subtle dark "glow" is drawn at the mouse position when new ink is applied, simulating the initial wet contact with the paper.
4. **Soft Gradient Vignette:** A radial gradient around the edges of the canvas darkens the scene slightly, enhancing the focus on the center and adding to the aged aesthetic.
5. **Floating Cherry Blossom Petals:** If the mouse (or touch) is idle for 3 seconds, delicate cherry blossom petals will start to drift across the canvas, adding a peaceful, meditative element.
6. **Touch Screen Support:** The sketching interaction is also enabled for touch screen devices, making it accessible across various inputs.

To manage the complexity and performance, especially with the dynamic ink drying and blending, I've utilized `createGraphics()` buffers for layering the paper texture, ink, and vignette.

Here is the complete code for your project:

***

**File: sketch.js**

```js
// Global variables
let pg; // Graphics buffer for ink and paper texture
let vignetteGraphics; // Graphics buffer for vignette
let mouseIdleTimer = 0;
let lastMouseMovedTime = 0;
let cherryBlossomPetals = [];

// Ink stroke object
class InkStroke {
constructor(x, y, speed, weight) {
this.points = [{ x, y, speed, weight }];
this.wetness = 1.0; // 1.0 = fully wet, 0.0 = fully dry
this.dryingRate = 0.005; // How fast wetness decreases per frame
this.baseWeight = weight; // Store initial weight for consistent bleed
}

addPoint(x, y, speed, weight) {
this.points.push({ x, y, speed, weight });
}

draw(g) {
if (this.points.length < 2) return;

// Gradual drying effect: reduce wetness over time
if (this.wetness > 0) {
this.wetness -= this.dryingRate;
this.wetness = max(0, this.wetness);
}

// --- Draw the main stroke body ---
g.blendMode(MULTIPLY); // Ink darkens overlaps
g.noStroke();

// Calculate current alpha based on wetness
// Dries lighter, not fully transparent, to maintain definition
let mainAlpha = map(this.wetness, 0, 1, 100, 255);
g.fill(0, mainAlpha); // Black ink

g.beginShape();
// Repeat first and last points for curveVertex to work properly with fewer points
g.curveVertex(this.points[0].x, this.points[0].y);
for (let i = 0; i < this.points.length; i++) {
let pt = this.points[i];
g.curveVertex(pt.x, pt.y);
}
g.curveVertex(this.points[this.points.length - 1].x, this.points[this.points.length - 1].y);
g.endShape();

// --- Draw organic edges / bleeding effect ---
g.blendMode(ADD); // Makes bleeding brighter and more diffuse
g.noStroke();

// Bleed effect is more prominent when ink is wet
let bleedAlpha = map(this.wetness, 0, 1, 0, 15); // Fades out as ink dries
g.fill(0, bleedAlpha);

for (let i = 0; i < this.points.length; i++) {
let pt = this.points[i];
let strokeWidth = map(pt.speed, 0, 20, this.baseWeight * 1.5, this.baseWeight * 0.5, true); // Bleed width scales with stroke

// Determine stroke direction to calculate perpendicular angle for edges
let prevPt = this.points[max(0, i - 1)];
let angle = atan2(pt.y - prevPt.y, pt.x - prevPt.x);
let perpAngle = angle + HALF_PI;

// Draw small circles along the edges with noise offset for organic texture
let edgeCount = 5; // More circles for smoother bleed
for (let j = 0; j < edgeCount; j++) {
let offset = map(j, 0, edgeCount - 1, -strokeWidth / 2, strokeWidth / 2);
let noiseOffset = noise(pt.x * 0.01, pt.y * 0.01 + offset * 0.05, frameCount * 0.01);
noiseOffset = map(noiseOffset, 0, 1, -strokeWidth * 0.5, strokeWidth * 0.5); // More dramatic bleed

let edgeX1 = pt.x + cos(perpAngle) * (offset + noiseOffset * this.wetness); // Noise effect reduces as ink dries
let edgeY1 = pt.y + sin(perpAngle) * (offset + noiseOffset * this.wetness);

let bleedSize = map(this.wetness, 0, 1, 5, strokeWidth * 0.7); // Bleed is wider when wet
g.ellipse(edgeX1, edgeY1, bleedSize, bleedSize);
}
}
}

isDry() {
return this.wetness <= 0;
}
}

let inkStrokes = [];
let currentStroke = null;

// Petal object for cherry blossoms
class Petal {
constructor() {
this.x = random(-width * 0.1, width * 1.1);
this.y = random(-height * 0.1, height * 0.1);
this.vx = random(-0.5, 0.5);
this.vy = random(0.5, 1.5);
this.rotation = random(TWO_PI);
this.rotationSpeed = random(-0.02, 0.02);
this.size = random(10, 25);
this.color = color(255, 150, 180, 200); // Pink, semi-transparent
}

update() {
this.x += this.vx;
this.y += this.vy;
this.rotation += this.rotationSpeed;
}

draw() {
push();
translate(this.x, this.y);
rotate(this.rotation);
fill(this.color);
noStroke();
// Simple petal shape
beginShape();
vertex(0, -this.size / 2);
bezierVertex(this.size / 2, -this.size / 2, this.size / 2, this.size / 2, 0, this.size / 2);
bezierVertex(-this.size / 2, this.size / 2, -this.size / 2, -this.size / 2, 0, -this.size / 2);
endShape();
pop();
}

isOffScreen() {
return this.y > height * 1.1;
}
}

function setup() {
createCanvas(windowWidth, windowHeight);
pixelDensity(1); // Ensure consistent pixel density across devices

// Create graphics buffer for ink and paper texture
pg = createGraphics(width, height);
pg.pixelDensity(1);

// Create graphics buffer for vignette
vignetteGraphics = createGraphics(width, height);
vignetteGraphics.pixelDensity(1);
drawVignette(); // Draw vignette once, it's static

// Initial paper texture
drawPaperTexture(pg);

// Set last mouse moved time
lastMouseMovedTime = millis();
}

function draw() {
// Clear the main canvas
background(255);

// Clear the ink buffer (pg) with transparency
pg.clear();

// Redraw the paper texture (with animating fibers)
drawPaperTexture(pg);

// Draw all existing ink strokes
for (let i = inkStrokes.length - 1; i >= 0; i--) {
inkStrokes[i].draw(pg);
if (inkStrokes[i].isDry()) {
inkStrokes.splice(i, 1); // Remove dry strokes to save memory and performance
}
}

// Draw current ink stroke being drawn
if (currentStroke) {
currentStroke.draw(pg);
}

// Draw the combined paper and ink from pg to the main canvas
image(pg, 0, 0);

// --- Handle cherry blossom petals if mouse is idle ---
mouseIdleTimer = millis() - lastMouseMovedTime;
if (mouseIdleTimer > 3000) { // 3 seconds idle
if (frameCount % 30 == 0) { // Add a new petal every 30 frames
cherryBlossomPetals.push(new Petal());
}
for (let i = cherryBlossomPetals.length - 1; i >= 0; i--) {
cherryBlossomPetals[i].update();
cherryBlossomPetals[i].draw();
if (cherryBlossomPetals[i].isOffScreen()) {
cherryBlossomPetals.splice(i, 1);
}
}
}

// Draw vignette on top of everything
image(vignetteGraphics, 0, 0);

// Handle ink ripple effect (simple version)
// Only draw if a stroke is actively being made
if (currentStroke) {
drawRipple(pg, mouseX, mouseY);
}
}

// --- Mouse and Touch Interaction ---

function mouseDragged() {
// If no current stroke, start a new one
if (!currentStroke) {
let speed = dist(pmouseX, pmouseY, mouseX, mouseY);
let weight = map(speed, 0, 20, 30, 5, true); // Slower = thicker, faster = thinner
currentStroke = new InkStroke(mouseX, mouseY, speed, weight);
inkStrokes.push(currentStroke);
} else {
let speed = dist(pmouseX, pmouseY, mouseX, mouseY);
let weight = map(speed, 0, 20, 30, 5, true);
currentStroke.addPoint(mouseX, mouseY, speed, weight);
}
lastMouseMovedTime = millis();
return false; // Prevent default browser behavior (e.g., text selection)
}

function mouseReleased() {
currentStroke = null; // End the current stroke
return false;
}

function mouseMoved() {
lastMouseMovedTime = millis();
return false;
}

function touchStarted() {
lastMouseMovedTime = millis();
return false; // Prevent default browser touch behavior (e.g., zooming)
}

function touchMoved() {
if (touches.length > 0) {
let touch = touches[0];
// If no current stroke, start a new one
if (!currentStroke) {
let speed = dist(pmouseX, pmouseY, touch.x, touch.y);
let weight = map(speed, 0, 20, 30, 5, true);
currentStroke = new InkStroke(touch.x, touch.y, speed, weight);
inkStrokes.push(currentStroke);
} else {
let speed = dist(pmouseX, pmouseY, touch.x, touch.y);
let weight = map(speed, 0, 20, 30, 5, true);
currentStroke.addPoint(touch.x, touch.y, speed, weight);
}
}
lastMouseMovedTime = millis();
return false; // Prevent default browser touch behavior
}

function touchEnded() {
currentStroke = null; // End the current stroke
return false;
}

function windowResized() {
resizeCanvas(windowWidth, windowHeight);
// Re-create graphics buffers with new dimensions
pg = createGraphics(width, height);
pg.pixelDensity(1);
vignetteGraphics = createGraphics(width, height);
vignetteGraphics.pixelDensity(1);
drawVignette(); // Redraw static vignette
// Note: Existing ink strokes are not resized, they will be drawn at their original coordinates.
// For a more robust solution, stroke points would need to be remapped.
}

// --- Helper functions ---

function drawPaperTexture(g) {
g.push();
g.noStroke();
g.fill(240, 230, 210); // Warm cream/beige for rice paper
g.rect(0, 0, width, height);

g.blendMode(MULTIPLY); // Darken the texture slightly
g.fill(0, 10); // Very subtle dark lines

// Draw subtle horizontal fibers with Perlin noise
for (let y = 0; y < height; y += 5) {
let noiseVal = noise(y * 0.005, frameCount * 0.001);
let alpha = map(noiseVal, 0, 1, 0, 5); // Vary transparency
g.fill(0, alpha);
g.rect(0, y, width, 1);
}

// Draw subtle vertical fibers with Perlin noise
for (let x = 0; x < width; x += 5) {
let noiseVal = noise(x * 0.005, frameCount * 0.001 + 100);
let alpha = map(noiseVal, 0, 1, 0, 5); // Vary transparency
g.fill(0, alpha);
g.rect(x, 0, 1, height);
}
g.pop();
}

function drawVignette() {
vignetteGraphics.push();
vignetteGraphics.noFill();
vignetteGraphics.blendMode(MULTIPLY); // Darkens the edges

let maxDist = dist(0, 0, width / 2, height / 2);
for (let i = 0; i < maxDist; i += 5) {
let alpha = map(i, 0, maxDist, 0, 50); // Darker further from center
vignetteGraphics.stroke(0, alpha);
vignetteGraphics.strokeWeight(10); // Thicker stroke for smoother gradient
vignetteGraphics.ellipse(width / 2, height / 2, width - i * 2, height - i * 2);
}
vignetteGraphics.pop();
}

function drawRipple(g, x, y) {
g.push();
g.blendMode(ADD); // Makes it brighter/more diffuse
g.noStroke();
g.fill(0, 10); // Subtle black glow

// Animate a short-lived ripple
g.ellipse(x, y, 30, 30);
g.pop();
}
```

***

**File: index.html**

This HTML file loads the p5.js library and then your `sketch.js` file. It also includes the `style.css` file.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zen Calligraphy Experience</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
```

***

**File: style.css**

This CSS ensures the canvas fills the window and removes default browser margins and scrollbars, which is typical for p5.js sketches.

```css
html, body {
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars */
background-color: #f0e6d2; /* Match the paper background */
}

canvas {
display: block; /* Remove extra space below canvas */
}
```

***

**How to Use:**

1. **Save the files:** Save the three code blocks above into files named `sketch.js`, `index.html`, and `style.css` respectively, all in the same folder.
2. **Open `index.html`:** Open the `index.html` file in your web browser.

You can then use your mouse or touch screen to draw on the canvas. Try varying your stroke speed to see the effects on ink thickness and bleeding. If you stop drawing for a few seconds, the cherry blossom petals will appear!

Enjoy the meditative experience!
๐ŸŽฌ Video Preview