Clamped to the Bark
2026 · botanical
The orchid owns no soil, only a grip and a habit of reaching; I painted the reaching, and left the air it reaches into unpainted.
an epiphytic orchid gripping a slab of bark, aerial roots trailing into empty air, one flower spike arcing out
painted by GPT-5.6p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: a bark slab falls from the top edge to the left margin; the orchid clamps onto it at the upper-left thirds
// intersection, one flower spike arcing up into the right air, aerial roots trailing off into the empty lower right.
// layering: pale air washes, the bark's wet body, hatched grain, roots, leaves over them, spike and flowers, darks last.
const PALETTE = [
"#e9e3d1", // ground
"#8a6a4a", // bark
"#3f3226", // shadow
"#5c7346", // foliage
"#33482f", // deepshade
"#bcbfa8", // root
"#b04a6d", // accent
];
const GROUND = PALETTE[0];
const BARK = PALETTE[1];
const SHADOW = PALETTE[2];
const LEAF = PALETTE[3];
const DEEP = PALETTE[4];
const ROOT = PALETTE[5];
const LIP = PALETTE[6];
// the trunk's spine, top edge down to the left margin
const TRUNK = [
[258, -40], [250, 100], [228, 230], [190, 348], [134, 462], [46, 556], [-44, 622],
];
// the trunk's silhouette, right edge down then left edge back up
const SLAB = [
[332, -34], [324, 108], [298, 240], [258, 356], [198, 470], [110, 562], [18, 628],
[-40, 560], [58, 492], [120, 392], [160, 266], [188, 120], [192, -34],
];
function spineAt(t) {
const u = constrain(t, 0, 0.999) * (TRUNK.length - 1);
const i = floor(u);
const f = u - i;
return [lerp(TRUNK[i][0], TRUNK[i + 1][0], f), lerp(TRUNK[i][1], TRUNK[i + 1][1], f)];
}
// the outward (right-hand) normal of the spine at t
function spineNorm(t) {
const a = spineAt(constrain(t - 0.02, 0, 1));
const b = spineAt(constrain(t + 0.02, 0, 1));
const dx = b[0] - a[0];
const dy = b[1] - a[1];
const m = Math.hypot(dx, dy) || 1;
return [-dy / m, dx / m];
}
// one long stroke dragged down the trunk at lateral offset o (in half-widths)
function trunkStroke(o, t0, t1, name, col, w) {
brush.noFill();
brush.set(name, col, w);
brush.beginShape(0.5);
for (let k = 0; k <= 7; k++) {
const t = t0 + (t1 - t0) * (k / 7);
const s = spineAt(t);
const n = spineNorm(t);
const half = 72 - 20 * t;
const d = o * half + randomGaussian(0, 3.4);
brush.vertex(s[0] + n[0] * d, s[1] + n[1] * d);
}
brush.endShape(false);
}
function wash(x, y, r, col, op, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.55, 0.5);
brush.fill(col, op);
brush.circle(x, y, r, true);
}
function darkWash(x, y, r, op) {
brush.noStroke();
brush.fillBleed(0.22, "out");
brush.fillTexture(0.32, 0.35);
brush.fill(SHADOW, op);
brush.circle(x, y, r, true);
}
function slabPass(col, op, jitter) {
brush.noStroke();
brush.fillBleed(0.16, "out");
brush.fillTexture(0.28, 0.4);
brush.fill(col, op);
brush.beginShape(0.32);
for (const p of SLAB) brush.vertex(p[0] + randomGaussian(0, jitter), p[1] + randomGaussian(0, jitter));
brush.endShape(true);
}
// the trunk's right half, shaded as one shape so the slab has two values
function rightHalf(col, op) {
const pts = [];
for (let k = 0; k <= 8; k++) {
const t = k / 8;
const sp = spineAt(t);
const n = spineNorm(t);
const d = (72 - 20 * t) * 0.12;
pts.push([sp[0] + n[0] * d + randomGaussian(0, 5), sp[1] + n[1] * d + randomGaussian(0, 5)]);
}
for (let k = 8; k >= 0; k--) {
const t = k / 8;
const sp = spineAt(t);
const n = spineNorm(t);
const d = (72 - 20 * t) * 1.02;
pts.push([sp[0] + n[0] * d + randomGaussian(0, 6), sp[1] + n[1] * d + randomGaussian(0, 6)]);
}
brush.noStroke();
brush.fillBleed(0.2, "out");
brush.fillTexture(0.3, 0.4);
brush.fill(col, op);
brush.beginShape(0.3);
for (const q of pts) brush.vertex(q[0], q[1]);
brush.endShape(true);
}
function mossDab(x, y, r) {
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.65, 0.6);
brush.fill(random() < 0.62 ? LEAF : DEEP, max(70, randomGaussian(126, 20)));
brush.circle(x + randomGaussian(0, 3), y + randomGaussian(0, 3), max(5, randomGaussian(r, r * 0.24)), true);
}
// a silvered aerial root: grainy pale body, one shadowed flank, blunt swelling tip
function aerialRoot(pts, w, greenTip) {
const j = () => randomGaussian(0, 1.8);
brush.noFill();
brush.set("cpencil", ROOT, w);
brush.beginShape(0.62);
for (const p of pts) brush.vertex(p[0] + j(), p[1] + j());
brush.endShape(false);
brush.set("cpencil", ROOT, w * 0.55);
brush.beginShape(0.62);
for (const p of pts) brush.vertex(p[0] - w * 0.8 + j(), p[1] - w * 0.6 + j());
brush.endShape(false);
brush.set("2H", SHADOW, w * 0.26);
brush.beginShape(0.62);
for (const p of pts) brush.vertex(p[0] + w * 1.15 + j(), p[1] + w * 0.95 + j());
brush.endShape(false);
// a lit core, so the root reads as a tube of chalk rather than a green stroke
brush.set("2H", GROUND, w * 0.45);
brush.beginShape(0.62);
for (const p of pts) brush.vertex(p[0] - w * 0.3 + j() * 0.5, p[1] - w * 0.25 + j() * 0.5);
brush.endShape(false);
const tip = pts[pts.length - 1];
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fill(greenTip ? LEAF : ROOT, greenTip ? 168 : 132);
brush.circle(tip[0], tip[1], w * (greenTip ? 1.1 : 0.8), true);
}
// a thick strap leaf, folded down its length
function strapLeaf(x0, y0, x1, y1, w, op) {
const dx = x1 - x0;
const dy = y1 - y0;
const m = Math.hypot(dx, dy) || 1;
const nx = -dy / m;
const ny = dx / m;
const at = (t, o) => [
x0 + dx * t + nx * o + randomGaussian(0, 2.4),
y0 + dy * t + ny * o + randomGaussian(0, 2.4),
];
const ring = [
at(0.02, -w * 0.2), at(0.28, w * 0.96), at(0.64, w * 0.82),
at(0.94, w * 0.24), at(1, 0), at(0.68, -w * 0.56), at(0.32, -w * 0.72),
];
brush.noStroke();
brush.fillBleed(0.32, "out");
brush.fillTexture(0.5, 0.55);
brush.fill(LEAF, op);
brush.beginShape(0.62);
for (const p of ring) brush.vertex(p[0], p[1]);
brush.endShape(true);
// a deeper pass down the shaded flank, half the leaf's width
brush.fill(DEEP, op * 0.72);
brush.beginShape(0.62);
brush.vertex(ring[0][0], ring[0][1]);
brush.vertex(ring[1][0] - nx * w * 0.42, ring[1][1] - ny * w * 0.42);
brush.vertex(ring[3][0], ring[3][1]);
brush.vertex(ring[4][0], ring[4][1]);
brush.vertex(ring[5][0], ring[5][1]);
brush.endShape(true);
// the fold, drawn thin and last
brush.noFill();
brush.set("2B", DEEP, 1.1);
brush.beginShape(0.5);
brush.vertex(x0 + nx * w * 0.12, y0 + ny * w * 0.12);
brush.vertex(x0 + dx * 0.45 + nx * w * 0.2, y0 + dy * 0.45 + ny * w * 0.2);
brush.vertex(x1, y1);
brush.endShape(false);
}
// sepals and petals laid loose, then the saturated lip and a dark throat
function orchidFlower(x, y, ang, s) {
const segs = [
[-2.5, 0.66, 0.46, 138], [-1.15, 0.54, 0.42, 118], [0.35, 0.62, 0.48, 146],
[1.8, 0.5, 0.4, 108], [2.95, 0.58, 0.44, 128],
];
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fillTexture(0.45, 0.5);
for (const sg of segs) {
const a = ang + sg[0] + randomGaussian(0, 0.11);
const d = s * sg[2] * (1 + random(-0.1, 0.12));
brush.fill(LIP, max(60, sg[3] * (1 + random(-0.14, 0.14))));
brush.circle(x + Math.cos(a) * d, y + Math.sin(a) * d, s * sg[1] * (1 + random(-0.09, 0.12)), true);
}
brush.fillBleed(0.26, "out");
brush.fill(LIP, 205 + random(-18, 12));
brush.circle(x + Math.cos(ang + 1.5) * s * 0.42, y + Math.sin(ang + 1.5) * s * 0.42, s * 0.42, true);
brush.fill(SHADOW, 158);
brush.circle(x + Math.cos(ang + 1.15) * s * 0.16, y + Math.sin(ang + 1.15) * s * 0.16, s * 0.15, true);
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(GROUND);
// --- pale air, two big breaths only ---
wash(262, 262, 258, LEAF, 20, 0.68);
wash(438, 186, 186, LIP, 11, 0.7);
// --- the bark's wet body: a dense mottled field, thirty loads three deep ---
for (let i = 0; i < 30; i++) {
const t = -0.02 + (i / 29) * 1.06;
const sp = spineAt(t);
const n = spineNorm(t);
const half = 72 - 20 * constrain(t, 0, 1);
for (const lane of [-0.56, 0.02, 0.6]) {
const d = lane * half + randomGaussian(0, 9);
const r = max(34, randomGaussian(50, 7));
const right = lane > 0.3;
const col = right ? (random() < 0.55 ? SHADOW : BARK) : (random() < 0.84 ? BARK : SHADOW);
wash(sp[0] + n[0] * d + randomGaussian(0, 5), sp[1] + n[1] * d + randomGaussian(0, 7),
r, col, max(52, randomGaussian(right ? 88 : 76, 14)), 0.3);
}
}
rightHalf(SHADOW, 46);
// sixteen faint grain strokes, staggered so none runs the whole length
for (let i = 0; i < 16; i++) {
const o = -0.86 + (i / 15) * 1.74 + randomGaussian(0, 0.07);
const t0 = random(-0.05, 0.78);
const t1 = min(1.03, t0 + random(0.1, 0.3));
const col = o > 0.3 ? (random() < 0.6 ? SHADOW : BARK) : (random() < 0.82 ? BARK : SHADOW);
trunkStroke(o, t0, t1, random() < 0.5 ? "cpencil" : "2H", col,
max(1.2, randomGaussian(2.1, 0.6)));
}
// the two edges, drawn broken so the slab has a contour without an outline
trunkStroke(0.99, -0.03, 0.32, "charcoal", SHADOW, 2.6);
trunkStroke(0.95, 0.36, 0.66, "charcoal", SHADOW, 2.1);
trunkStroke(1.02, 0.74, 1.03, "charcoal", SHADOW, 2.8);
trunkStroke(-0.97, 0.08, 0.44, "cpencil", BARK, 2.2);
trunkStroke(-0.94, 0.56, 1.0, "cpencil", BARK, 2.5);
// one lit passage down the left cheek, where the light lands
trunkStroke(-0.58, 0.2, 0.46, "cpencil", GROUND, 3.2);
// fissures: short cross-flecks, nothing running the whole length
brush.set("2B", SHADOW, 1.6);
brush.line(238, 62, 268, 74);
brush.line(196, 140, 232, 152);
brush.line(268, 196, 296, 206);
brush.line(182, 262, 216, 272);
brush.line(244, 336, 272, 342);
brush.line(150, 388, 184, 398);
brush.line(196, 448, 224, 452);
brush.line(96, 486, 128, 494);
brush.line(58, 552, 88, 556);
brush.line(214, 24, 244, 34);
// --- the shadowed flank, deepest where the orchid is silhouetted against it ---
for (let i = 0; i < 14; i++) {
const t = 0.03 + (i / 13) * 0.94;
const sp = spineAt(t);
const n = spineNorm(t);
const d = (58 - 14 * t) + random(-10, 12);
const near = t < 0.55;
darkWash(sp[0] + n[0] * d, sp[1] + n[1] * d, max(18, randomGaussian(near ? 30 : 22, 5)),
max(76, randomGaussian(near ? 132 : 100, 20)));
}
// --- moss and lichen crusting the bark near the clamp ---
mossDab(230, 210, 16);
mossDab(204, 268, 19);
mossDab(250, 306, 13);
mossDab(194, 174, 11);
mossDab(172, 348, 17);
mossDab(222, 120, 9);
mossDab(146, 414, 14);
mossDab(276, 244, 10);
mossDab(116, 470, 12);
mossDab(256, 66, 8);
mossDab(64, 528, 13);
// --- aerial roots, laid down before the leaves so the leaves cross them ---
aerialRoot([[264, 300], [316, 378], [352, 456], [428, 508], [518, 536], [598, 548]], 6.4, false);
aerialRoot([[276, 312], [326, 424], [352, 534], [420, 618], [508, 668]], 5.2, true);
aerialRoot([[250, 306], [306, 396], [404, 452], [466, 494], [520, 556]], 3.4, false);
aerialRoot([[350, 442], [478, 546], [604, 646], [706, 728]], 2.1, false);
aerialRoot([[244, 290], [208, 352], [194, 430], [212, 492]], 3.8, false);
aerialRoot([[288, 292], [366, 320], [446, 330], [516, 352]], 2.7, true);
aerialRoot([[318, 396], [366, 476], [356, 560], [312, 616]], 1.9, true);
// --- the clamp: a swollen pseudobulb mass gripping the bark ---
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fillTexture(0.6, 0.55);
brush.fill(LEAF, 176);
brush.circle(266, 292, 32, true);
brush.fill(LEAF, 142);
brush.circle(244, 274, 25, true);
brush.fill(BARK, 118);
brush.circle(286, 308, 19, true);
brush.fill(DEEP, 132);
brush.circle(258, 316, 15, true);
// --- six strap leaves, fanning up and left off the bark ---
strapLeaf(268, 288, 112, 144, 33, 202);
strapLeaf(262, 296, 84, 306, 29, 162);
strapLeaf(276, 276, 308, 88, 26, 208);
strapLeaf(256, 302, 136, 406, 24, 150);
strapLeaf(290, 292, 378, 340, 19, 138);
strapLeaf(272, 268, 206, 56, 16, 126);
// --- the flower spike, arcing into the open right ---
brush.noFill();
brush.set("cpencil", LEAF, 3.4);
brush.beginShape(0.55);
brush.vertex(288, 284);
brush.vertex(332, 244);
brush.vertex(394, 210);
brush.vertex(456, 182);
brush.vertex(520, 164);
brush.vertex(574, 156);
brush.endShape(false);
brush.set("2H", DEEP, 1.0);
brush.beginShape(0.55);
brush.vertex(290, 289);
brush.vertex(336, 250);
brush.vertex(398, 216);
brush.vertex(460, 188);
brush.vertex(524, 170);
brush.endShape(false);
// pedicels down to each flower
brush.set("cpencil", LEAF, 1.5);
brush.line(340, 250, 360, 280);
brush.line(408, 216, 424, 244);
brush.line(474, 188, 486, 212);
brush.line(532, 168, 540, 188);
// --- four flowers and two buds, shrinking toward the tip ---
orchidFlower(362, 296, -0.5, 40);
orchidFlower(430, 258, -0.2, 32);
orchidFlower(492, 224, 0.14, 25);
orchidFlower(544, 198, 0.5, 18);
brush.noStroke();
brush.fillBleed(0.34, "out");
brush.fill(LIP, 156);
brush.circle(580, 172, 8.5, true);
brush.fill(LIP, 124);
brush.circle(600, 162, 6, true);
// --- darks last: broken bark contour, root shadows at the clamp, a few flecks ---
brush.noFill();
brush.set("charcoal", SHADOW, 1.5);
brush.beginShape(0.5);
brush.vertex(330, -20);
brush.vertex(318, 90);
brush.vertex(300, 172);
brush.endShape(false);
brush.beginShape(0.5);
brush.vertex(288, 232);
brush.vertex(262, 300);
brush.vertex(238, 354);
brush.endShape(false);
brush.beginShape(0.5);
brush.vertex(202, 452);
brush.vertex(140, 512);
brush.vertex(70, 566);
brush.endShape(false);
brush.set("2B", SHADOW, 1.3);
brush.line(302, 136, 276, 158);
brush.line(252, 292, 228, 316);
brush.line(194, 402, 170, 426);
brush.line(236, 212, 212, 230);
brush.line(116, 514, 92, 530);
brush.line(324, 44, 300, 62);
brush.set("rotring", SHADOW, 0.9);
brush.line(272, 306, 298, 342);
brush.line(258, 320, 278, 356);
brush.line(286, 300, 318, 316);
brush.noStroke();
brush.fill(SHADOW, 150);
brush.circle(452, 478, 2.6, true);
brush.circle(540, 530, 2.1, true);
brush.fill(LIP, 142);
brush.circle(622, 598, 4.2, true);
}