/* Impressionistic Red-Breasted Bird | Pure ink Kinetic Force Can be executed directly without images, media files, or extra libraries. Note: 1. It is recommended that the short side of the paper is at least 900 pixels; the details of feathers and eyes will be clearer. The program directly uses width and height without calling size(). 2. BACKGROUND_ALPHA defaults to 0 to keep the artwork on the root paper layer. Changing it to 255 allows using an opaque beige background, but it will cover the root paper layer. 3. The painting will be drawn layer by layer; please wait for completion. It will not disappear automatically once finished. Press R to clear and redraw this Kinetic Force layer without clearing the root paper layer. 4. The bird, feathers, and branches are all drawn by PureBrush.write(). Water diffusion, drying, and clearing are all handled by the Pure ink API. */ import java.util.*; final int BACKGROUND_ALPHA = 0; final int ACTIVE_PENS = 4; final int LIFT_FRAMES = 22; PurePaper inkPaper; ArrayList passes = new ArrayList(); StrokePlan[] active = new StrokePlan[ACTIVE_PENS]; float unit; float originX; float originY; int passIndex; int nextStroke; int settleLeft; int clearingFrames; boolean passStarted; boolean finished; void setup() { colorMode(RGB, 255); frameRate(60); inkPaper = new PurePaper(); repaintBird(); } void draw() { // clear() is a continuously active setting; it must be explicitly turned off after clearing. if (clearingFrames > 0) { clearingFrames--; if (clearingFrames == 0) { inkPaper.clear(0); } return; } // Keep draw() running to let the internal ink effects of Pure ink update normally. if (finished) return; PaintPass current = passes.get(passIndex); if (!passStarted) { if (current.dryBefore) { // Each phase requiring drying only executes once, rather than drying frame-by-frame. inkPaper.quickDry(1); } nextStroke = 0; settleLeft = current.settleFrames; passStarted = true; } // Each active brush only draws once per frame along its own trajectory. for (int i = 0; i < active.length; i++) { if (active[i] == null && nextStroke < current.strokes.size()) { active[i] = current.strokes.get(nextStroke++); active[i].start(); } if (active[i] != null) { active[i].update(); if (active[i].done) { active[i] = null; } } } boolean allReleased = true; for (int i = 0; i < active.length; i++) { if (active[i] != null) { allReleased = false; break; } } if (nextStroke == current.strokes.size() && allReleased) { if (settleLeft > 0) { settleLeft--; } else { passIndex++; passStarted = false; if (passIndex >= passes.size()) { finished = true; } } } } void keyPressed() { if (key == 'r' || key == 'R') { repaintBird(); } } void repaintBird() { for (int i = 0; i < active.length; i++) { if (active[i] != null) { active[i].cancel(); active[i] = null; } } // Only set the background at the start or when redrawing; do not erase accumulated ink every frame. background(251, 247, 237, BACKGROUND_ALPHA); inkPaper.clear(1); clearingFrames = 30; unit = min(width, height) / 1000.0; originX = (width - 1000 * unit) * 0.5; originY = (height - 1000 * unit) * 0.5; randomSeed(271828); noiseSeed(271828); passes.clear(); buildBird(); passIndex = 0; nextStroke = 0; settleLeft = 0; passStarted = false; finished = false; } // ------------------------------------------------------------ // Composition: Withered branch → Tail feathers → Body watercolor → Breast feathers → Wings → Fine feathers → Dotting the eyes // Coordinates are calculated in a 1000 × 1000 composition space, then converted to actual paper dimensions. // ------------------------------------------------------------ void buildBird() { BrushStyle wood = new BrushStyle( 25, 0.85, 0.05, color(68, 55, 42), 0.16, color(153, 128, 92), 4.5, false, 0.8 ); BrushStyle twig = new BrushStyle( 5, 0.95, 0.0, color(76, 65, 47), 0.0, color(124, 109, 79), 2.5, true, 0.45 ); PaintPass branches = addPass(true, 12); curveStroke(branches, wood, 95, 753, 270, 726, 611, 611, 884, 575); curveStroke(branches, twig, 315, 699, 270, 669, 246, 633, 221, 591); curveStroke(branches, twig, 266, 652, 247, 644, 221, 644, 199, 632); curveStroke(branches, twig, 755, 598, 818, 573, 864, 528, 900, 483); curveStroke(branches, twig, 853, 540, 848, 525, 841, 511, 833, 500); // Slanted brush, low ink content, low moisture: creates a natural "flying white" (dry brush) effect on branches. BrushStyle bark = new BrushStyle( 10, 0.30, 0.0, color(52, 48, 39), 0.0, color(116, 106, 79), 5.2, false, 0.7 ); bark.sizeValue = constrain(0.07 * unit, 0, 0.30); bark.tilt = 30; bark.rotation = 75; BrushStyle leaf = new BrushStyle( 14, 0.82, 0.16, color(98, 101, 60), 0.30, color(166, 155, 91), 2.3, true, 0.6 ); PaintPass barkAndLeaves = addPass(true, 14); curveStroke(barkAndLeaves, bark, 116, 748, 310, 713, 610, 616, 872, 579); curveStroke(barkAndLeaves, bark, 167, 746, 340, 702, 584, 641, 797, 591); curveStroke(barkAndLeaves, leaf, 857, 535, 847, 528, 833, 515, 822, 512); curveStroke(barkAndLeaves, leaf, 878, 514, 881, 505, 883, 489, 887, 479); curveStroke(barkAndLeaves, leaf, 892, 493, 902, 490, 910, 487, 919, 480); BrushStyle tail = new BrushStyle( 20, 0.92, 0.10, color(49, 56, 51), 0.24, color(113, 105, 82), 3.7, true, 0.7 ); PaintPass tailPass = addPass(false, 16); curveStroke(tailPass, tail, 461, 527, 420, 590, 353, 652, 315, 710); curveStroke(tailPass, tail, 472, 529, 435, 594, 365, 669, 332, 722); curveStroke(tailPass, tail, 484, 536, 447, 602, 385, 681, 354, 731); // Two-tone wet brush to construct body volume, avoiding geometric fills instead of brush strokes. BrushStyle body = new BrushStyle( 62, 0.78, 0.38, color(166, 168, 146), 0.62, color(232, 225, 198), 3.1, false, 1.2 ); BrushStyle head = new BrushStyle( 42, 0.85, 0.26, color(113, 121, 102), 0.46, color(180, 183, 153), 2.6, false, 0.8 ); PaintPass bodyPass = addPass(true, 28); curveStroke(bodyPass, body, 508, 376, 459, 412, 445, 514, 501, 557); curveStroke(bodyPass, body, 532, 366, 479, 422, 483, 541, 542, 572); curveStroke(bodyPass, body, 562, 362, 514, 429, 524, 552, 575, 571); curveStroke(bodyPass, body, 590, 371, 565, 432, 571, 532, 608, 546); curveStroke(bodyPass, body, 615, 389, 645, 427, 649, 487, 620, 519); curveStroke(bodyPass, head, 546, 329, 558, 288, 615, 281, 643, 311); curveStroke(bodyPass, head, 548, 345, 572, 307, 625, 309, 650, 335); curveStroke(bodyPass, head, 558, 361, 588, 338, 629, 339, 650, 363); BrushStyle breast = new BrushStyle( 48, 0.86, 0.28, color(167, 66, 34), 0.50, color(233, 145, 73), 2.7, false, 0.9 ); BrushStyle cheek = new BrushStyle( 22, 0.85, 0.20, color(178, 78, 36), 0.35, color(233, 151, 79), 2.0, false, 0.3 ); // Do not dry first here, allowing the warm colors of the breast to blend softly with the base layer. PaintPass breastPass = addPass(false, 30); curveStroke(breastPass, breast, 631, 350, 659, 380, 662, 441, 635, 481); curveStroke(breastPass, breast, 609, 359, 637, 402, 645, 465, 610, 514); curveStroke(breastPass, breast, 588, 382, 610, 428, 613, 480, 581, 536); curveStroke(breastPass, cheek, 637, 337, 651, 345, 649, 366, 637, 378); BrushStyle wing = new BrushStyle( 40, 0.90, 0.16, color(60, 68, 57), 0.34, color(135, 131, 104), 2.9, true, 0.7 ); BrushStyle shoulder = new BrushStyle( 25, 0.88, 0.13, color(82, 90, 70), 0.28, color(153, 151, 118), 2.4, false, 0.5 ); BrushStyle belly = new BrushStyle( 35, 0.92, 0.03, color(217, 209, 178), 0.14, color(246, 237, 208), 2.4, false, 0.7 ); PaintPass wingPass = addPass(true, 24); curveStroke(wingPass, belly, 595, 530, 598, 552, 588, 578, 572, 581); curveStroke(wingPass, belly, 523, 545, 534, 575, 551, 584, 567, 579); curveStroke(wingPass, wing, 497, 403, 467, 440, 453, 514, 410, 589); curveStroke(wingPass, wing, 519, 413, 492, 463, 476, 531, 432, 578); curveStroke(wingPass, wing, 535, 436, 527, 481, 494, 537, 454, 562); curveStroke(wingPass, shoulder, 489, 407, 509, 393, 534, 412, 539, 435); // Fully dry after wet painting is complete, then draw feather shafts and fine feathers. BrushStyle featherDark = new BrushStyle( 3.5, 0.98, 0.0, color(48, 54, 46), 0.0, color(89, 89, 66), 2.4, true, 0.2 ); BrushStyle crown = new BrushStyle( 2.7, 0.85, 0.0, color(91, 104, 82), 0.0, color(140, 149, 117), 1.7, true, 0.25 ); BrushStyle contour = new BrushStyle( 3.4, 0.80, 0.0, color(126, 119, 89), 0.0, color(176, 163, 124), 1.7, true, 0.35 ); BrushStyle featherLight = new BrushStyle( 2.2, 0.88, 0.0, color(191, 184, 146), 0.0, color(153, 153, 119), 2.1, true, 0.15 ); BrushStyle covert = new BrushStyle( 3.1, 0.85, 0.0, color(180, 178, 139), 0.0, color(137, 144, 109), 1.7, true, 0.22 ); PaintPass featherDarkPass = addPass(true, 12); PaintPass featherLightPass = addPass(false, 12); // Fan-arranged flight feathers; lengths and directions change progressively. for (int i = 0; i < 9; i++) { float x0 = 485 + i * 5.9; float y0 = 434 + i * 5.1; float x1 = 464 + i * 6.6; float y1 = 482 + i * 1.0; float x2 = 439 + i * 5.4; float y2 = 545 - i * 1.1; float x3 = 407 + i * 6.8; float y3 = 585 - i * 3.1; curveStroke(featherDarkPass, featherDark, x0, y0, x1, y1, x2, y2, x3, y3); curveStroke(featherLightPass, featherLight, x0 + 3, y0 - 1, x1 + 3, y1, x2 + 3, y2, x3 + 3, y3 - 1); } // Crown feathers grow along the arc; do not wrap the head in a complete outline. for (int i = 0; i < 11; i++) { float t = i / 10.0; float x = 551 + 7.2 * i; float y = 323 - 22 * sin(PI * t); curveStroke(featherDarkPass, crown, x, y, x + 4, y - 6, x + 12, y - 7, x + 18, y - 3); } curveStroke(featherDarkPass, contour, 540, 340, 532, 350, 526, 368, 519, 380); curveStroke(featherDarkPass, contour, 478, 413, 463, 433, 454, 451, 452, 470); curveStroke(featherDarkPass, contour, 628, 515, 616, 542, 603, 558, 585, 567); // Scattered small coverts: each row features small procedural perturbations. for (int row = 0; row < 3; row++) { int count = 5 + row; for (int j = 0; j < count; j++) { float x = 487 - row * 10 + j * 9.2 + random(-1.5, 1.5); float y = 423 + row * 19 + random(-2.2, 2.2); float featherLength = random(12, 17); curveStroke(featherLightPass, covert, x, y, x - 4, y + 5, x - 3, y + featherLength - 2, x + 3, y + featherLength); } } BrushStyle warmFeather = new BrushStyle( 2.4, 0.75, 0.0, color(177, 93, 46), 0.0, color(218, 142, 77), 1.6, true, 0.18 ); BrushStyle warmHighlight = new BrushStyle( 2.3, 0.76, 0.0, color(235, 167, 95), 0.0, color(218, 144, 75), 1.6, true, 0.18 ); // Breast feathers are distributed in an elliptical region, but each individual stroke is a brush curve. for (int i = 0; i < 30; i++) { float a = random(TWO_PI); float r = sqrt(random(0.82)); float x = 610 + 34 * r * cos(a); float y = 446 + 65 * r * sin(a); float len = random(7, 15); BrushStyle chosen = (i % 4 == 0) ? warmHighlight : warmFeather; curveStroke(featherLightPass, chosen, x, y, x - 1, y + len * 0.30, x - 2, y + len * 0.72, x - 4, y + len); } BrushStyle bellyFeather = new BrushStyle( 2.5, 0.80, 0.0, color(180, 173, 143), 0.0, color(226, 216, 184), 1.7, true, 0.2 ); BrushStyle paleFeather = new BrushStyle( 2.6, 0.90, 0.0, color(243, 232, 201), 0.0, color(218, 209, 176), 1.8, true, 0.15 ); for (int i = 0; i < 20; i++) { float a = random(TWO_PI); float r = sqrt(random(0.85)); float x = 550 + 43 * r * cos(a); float y = 558 + 21 * r * sin(a); float len = random(8, 14); curveStroke(featherLightPass, bellyFeather, x, y, x + 1, y + len * 0.35, x - 1, y + len * 0.75, x - 4, y + len); } for (int i = 0; i < 10; i++) { float x = 490 + i * 9; float y = 577 + 9 * sin(PI * i / 9.0); curveStroke(featherLightPass, paleFeather, x, y, x + 3, y + 3, x + 4, y + 6, x + 3, y + 9); } curveStroke(featherLightPass, featherLight, 446, 563, 407, 621, 362, 674, 324, 711); curveStroke(featherLightPass, featherLight, 462, 563, 424, 626, 379, 683, 345, 722); curveStroke(featherLightPass, paleFeather, 645, 337, 639, 344, 628, 347, 624, 345); BrushStyle beak = new BrushStyle( 9, 1.0, 0.0, color(34, 41, 36), 0.0, color(89, 81, 57), 2.0, true, 0.05 ); BrushStyle lowerBeak = new BrushStyle( 6.3, 1.0, 0.0, color(50, 49, 35), 0.0, color(115, 97, 63), 1.9, true, 0.04 ); BrushStyle leg = new BrushStyle( 5.5, 1.0, 0.0, color(75, 63, 44), 0.0, color(151, 129, 88), 1.8, true, 0.2 ); BrushStyle toe = new BrushStyle( 3.5, 1.0, 0.0, color(70, 59, 43), 0.0, color(139, 117, 79), 1.5, true, 0.1 ); BrushStyle eyeRim = new BrushStyle( 13, 1.0, 0.0, color(220, 194, 139), 0.0, color(241, 216, 164), 0.65, false, 0 ); PaintPass features = addPass(false, 14); // Upper beak, lower beak, and the central stroke together form the pointed beak. curveStroke(features, beak, 657, 342, 673, 338, 697, 338, 716, 341); curveStroke(features, lowerBeak, 657, 350, 681, 348, 699, 343, 716, 341); curveStroke(features, lowerBeak, 657, 346, 676, 345, 695, 343, 711, 342); curveStroke(features, leg, 521, 584, 523, 605, 527, 626, 531, 647); curveStroke(features, leg, 578, 583, 576, 598, 580, 614, 585, 631); // Toes wrap around branches rather than stopping on top of them. curveStroke(features, toe, 531, 647, 536, 642, 548, 642, 553, 648); curveStroke(features, toe, 531, 648, 527, 643, 518, 643, 516, 650); curveStroke(features, toe, 531, 648, 538, 649, 542, 654, 540, 660); curveStroke(features, toe, 585, 631, 594, 625, 607, 625, 611, 631); curveStroke(features, toe, 584, 631, 578, 627, 569, 631, 566, 639); curveStroke(features, toe, 585, 632, 590, 636, 591, 642, 588, 647); dab(features, eyeRim, 637, 329); BrushStyle eyeBlack = new BrushStyle( 8.5, 1.0, 0.0, color(19, 25, 22), 0.0, color(28, 30, 23), 0.55, false, 0 ); BrushStyle fineDark = new BrushStyle( 1.8, 1.0, 0.0, color(36, 39, 30), 0.0, color(59, 55, 38), 1.3, true, 0 ); BrushStyle fineLight = new BrushStyle( 1.8, 1.0, 0.0, color(218, 204, 158), 0.0, color(188, 177, 133), 1.3, true, 0 ); PaintPass eyePass = addPass(false, 14); dab(eyePass, eyeBlack, 637, 329); curveStroke(eyePass, fineDark, 662, 347, 681, 346, 699, 342, 715, 341); curveStroke(eyePass, fineLight, 666, 341, 677, 339, 687, 339, 696, 340); curveStroke(eyePass, fineLight, 579, 590, 579, 602, 582, 615, 584, 623); curveStroke(eyePass, fineLight, 523, 594, 524, 608, 527, 622, 529, 633); curveStroke(eyePass, fineDark, 611, 631, 613, 632, 613, 634, 612, 636); curveStroke(eyePass, fineDark, 566, 639, 565, 641, 564, 642, 563, 643); curveStroke(eyePass, fineDark, 540, 659, 540, 661, 539, 663, 537, 664); // Add the catchlight at the very end; light pigments with zero water can overlay dark eyes. BrushStyle glint = new BrushStyle( 2.3, 1.0, 0.0, color(255, 249, 225), 0.0, color(255, 249, 225), 0.45, false, 0 ); PaintPass glintPass = addPass(false, 0); dab(glintPass, glint, 635.7, 327.1); } // ------------------------------------------------------------ // Path Construction: Processing only calculates curves, it does not handle ink rendering. // ------------------------------------------------------------ PaintPass addPass(boolean dryBefore, int settleFrames) { PaintPass p = new PaintPass(dryBefore, settleFrames); passes.add(p); return p; } void curveStroke( PaintPass pass, BrushStyle style, float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3 ) { float guideLength = dist(x0, y0, x1, y1) + dist(x1, y1, x2, y2) + dist(x2, y2, x3, y3); int samples = max(12, min(180, (int)ceil(guideLength / 4.0))); PVector[] points = new PVector[samples + 1]; float noiseKey = random(2000); for (int i = 0; i <= samples; i++) { float t = i / (float)samples; float x = bezierPoint(x0, x1, x2, x3, t); float y = bezierPoint(y0, y1, y2, y3, t); float dx = bezierTangent(x0, x1, x2, x3, t); float dy = bezierTangent(y0, y1, y2, y3, t); float magnitude = max(0.0001, sqrt(dx * dx + dy * dy)); // Low-frequency perturbation only shifts stroke direction; dry brush and diffusion are still generated by the real brush. float irregularity = (noise(noiseKey, t * 2.4) - 0.5) * 2 * style.wobble * sin(PI * t); x += -dy / magnitude * irregularity; y += dx / magnitude * irregularity; points[i] = new PVector( originX + x * unit, originY + y * unit ); } pass.strokes.add(new StrokePlan(style, points)); } void dab(PaintPass pass, BrushStyle style, float x, float y) { curveStroke(pass, style, x, y - 0.35, x, y - 0.10, x + 0.15, y + 0.10, x + 0.15, y + 0.35); } // Roughly estimate the brush size during construction based on the reference vertical brush widths in the document. // The actual width, ink density, and bleeding are still calculated by Pure ink. float fitBrushSize(float desiredPixels, float wetness) { float low = 0; float high = 1; for (int i = 0; i < 14; i++) { float b = (low + high) * 0.5; float dryWidth = 2 + 22 * b + 26 * b * b; float wetWidth = 43 + 102 * b - 5 * b * b; float estimatedWidth = lerp(dryWidth, wetWidth, wetness); if (estimatedWidth < desiredPixels) { low = b; } else { high = b; } } return constrain((low + high) * 0.5, 0, 1); } // ------------------------------------------------------------ // The following are custom stroke data and scheduling classes for this program. // ------------------------------------------------------------ class BrushStyle { float sizeValue; float load; float tipWet; float tuftWet; color tipColor; color tuftColor; float speed; boolean tapered; float wobble; float tilt = 0; float rotation = 0; BrushStyle( float diameter, float pigmentLoad, float tipWater, color tipInk, float tuftWater, color tuftInk, float pace, boolean taper, float irregularity ) { // Reduce water moderately for smaller papers to prevent diffusion from overwriting details. float waterScale = constrain(unit, 0.30, 1.0); load = constrain(pigmentLoad, 0, 1); tipWet = constrain(tipWater * waterScale, 0, 1); tuftWet = constrain(tuftWater * waterScale, 0, 1); tipColor = tipInk; tuftColor = tuftInk; sizeValue = fitBrushSize( diameter * unit, (tipWet + tuftWet) * 0.5 ); speed = pace; tapered = taper; wobble = irregularity; } } class PaintPass { ArrayList strokes = new ArrayList(); boolean dryBefore; int settleFrames; PaintPass(boolean dryFirst, int settling) { dryBefore = dryFirst; settleFrames = settling; } } class StrokePlan { BrushStyle style; PVector[] points; float[] cumulative; float totalLength; float travelled; int segment; int liftAge; boolean lifting; boolean done; PureBrush brush; StrokePlan(BrushStyle brushStyle, PVector[] path) { style = brushStyle; points = path; cumulative = new float[points.length]; cumulative[0] = 0; for (int i = 1; i < points.length; i++) { cumulative[i] = cumulative[i - 1] + dist( points[i - 1].x, points[i - 1].y, points[i].x, points[i].y ); } totalLength = cumulative[cumulative.length - 1]; } void start() { // Create a brush for each stroke; brush size is only determined at construction. brush = new PureBrush( style.sizeValue, style.load, style.tipWet, style.tipColor, style.tuftWet, style.tuftColor ); brush.setTilt(constrain(style.tilt, 0, 66)); // Do not call setRotation() for vertical brushes to avoid accidentally rotating them. if (style.tilt > 0) { brush.setRotation(constrain(style.rotation, 0, 360)); } travelled = 0; segment = 0; liftAge = 0; lifting = false; done = false; } void update() { if (done) return; if (lifting) { liftAge++; // Retain for several frames after lift() before calling dispose(). if (liftAge >= LIFT_FRAMES) { brush.stopWrite(); brush.dispose(); brush = null; done = true; } return; } float t = (totalLength > 0.0001) ? constrain(travelled / totalLength, 0, 1) : 1; while ( segment < points.length - 2 && cumulative[segment + 1] < travelled ) { segment++; } float span = cumulative[segment + 1] - cumulative[segment]; float fraction = (span > 0.0001) ? constrain((travelled - cumulative[segment]) / span, 0, 1) : 0; float x = lerp(points[segment].x, points[segment + 1].x, fraction); float y = lerp(points[segment].y, points[segment + 1].y, fraction); float loadFactor = style.tapered ? 1.0 - 0.78 * pow(t, 2.3) : 1.0; brush.setBrushLoad(constrain(style.load * loadFactor, 0, 1)); // Real pigments only fall onto the paper of Kinetic Force from here. brush.write(x, y); if (travelled >= totalLength) { brush.lift(); lifting = true; liftAge = 0; return; } // Advance according to the arc length to avoid local ink buildup caused by uneven Bezier parameter speeds. // Accelerate slightly when lifting brush to form a thin tail in combination with brush load control. float movement = style.speed * unit * (0.84 + 0.16 * sin(PI * t) + 0.35 * t * t); movement = constrain(movement, 0.30, 8.0); // Keep dry vertical strokes with low ink content slow to avoid failing to leave ink entirely. if ( style.tilt == 0 && style.load <= 0.35 && max(style.tipWet, style.tuftWet) <= 0.30 ) { movement = min(movement, 1.0); } travelled = min(totalLength, travelled + movement); } void cancel() { if (brush != null) { brush.stopWrite(); brush.dispose(); brush = null; } done = true; } }