Split Vane
2026 · study
A feather is an argument about repetition: a thousand near-identical parts holding a shape until a few let go, and then you can finally see how it was made.
a single flight feather laid on a shallow diagonal across bare paper, calamus low at the left, tip breaking the top-right edge, the vane separating into loose locks of barbs near the tip
painted by Grok 4.6p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: one flight feather bowed along a shallow lower-left to upper-right diagonal, calamus in
// the bottom-left corner, tip breaking the top-right edge; the vane splits into loose locks past two
// thirds and that opening wedge of bare paper is the focus. layering: slate air, vane tone and its
// three soft bars, raked barb combing, split locks, then rachis and darks. upper-left stays dry paper.
const PALETTE = [
"#e8e2d3", // ground — warm laid paper
"#7f8b98", // mass — cool slate, the vane
"#5b6053", // mass — olive drab, the under-barbs
"#26251f", // shadow — near-black ink, rachis and notches
"#a5602c", // accent — rust inside the quill, rationed
];
const PAPER = "#e8e2d3";
const SLATE = "#7f8b98";
const DRAB = "#5b6053";
const INK = "#26251f";
const RUST = "#a5602c";
// the rachis: nine hand-set stations, each segment steeper than the last, so the shaft bows
const SHAFT = [
[52, 648], [146, 612], [241, 569], [336, 517], [430, 455],
[525, 383], [619, 298], [714, 198], [808, 81], [860, 10],
];
// perpendicular half-widths of the two vanes, thirteen stops from calamus to tip
const LOWER = [0, 0, 54, 94, 116, 122, 116, 106, 92, 74, 54, 31, 10];
const UPPER = [0, 0, 20, 34, 43, 46, 44, 41, 36, 29, 21, 13, 5];
// the trailing vane rakes at about 41 degrees off the shaft, the leading vane stands nearer 50
function leanOf(side) {
return side < 0 ? randomGaussian(50, 2.8) : randomGaussian(41, 3);
}
function rakeOf(side) {
return side < 0 ? 0.84 : 1.15;
}
let PATH = null;
function buildPath() {
const raw = [];
for (let i = 0; i < SHAFT.length - 1; i++) {
for (let k = 0; k < 18; k++) {
const u = k / 18;
raw.push([lerp(SHAFT[i][0], SHAFT[i + 1][0], u), lerp(SHAFT[i][1], SHAFT[i + 1][1], u)]);
}
}
raw.push(SHAFT[SHAFT.length - 1]);
const out = [];
for (let i = 0; i < raw.length; i++) {
const p = raw[max(0, i - 4)];
const q = raw[min(raw.length - 1, i + 4)];
out.push({
x: raw[i][0],
y: raw[i][1],
a: (Math.atan2(q[1] - p[1], q[0] - p[0]) * 180) / Math.PI,
});
}
return out;
}
function at(t) {
return PATH[constrain(floor(constrain(t, 0, 1) * (PATH.length - 1)), 0, PATH.length - 1)];
}
function wAt(tab, t) {
const u = constrain(t, 0, 1) * (tab.length - 1);
const i = min(floor(u), tab.length - 2);
return lerp(tab[i], tab[i + 1], u - i);
}
// a point out on the vane: perpendicular distance d, carried tipward by the rake of the barbs
function vanePt(t, side, d) {
const p = at(t);
const n = p.a + side * 90;
const r = rakeOf(side);
return [p.x + cos(n) * d + cos(p.a) * d * r, p.y + sin(n) * d + sin(p.a) * d * r];
}
// three soft bars cross the vane — where they fall the barbs darken
function barAt(t) {
const b1 = pow(2.718, -pow((t - 0.27) / 0.055, 2));
const b2 = pow(2.718, -pow((t - 0.46) / 0.05, 2));
const b3 = pow(2.718, -pow((t - 0.665) / 0.045, 2));
return min(1, b1 * 0.9 + b2 + b3 * 0.8);
}
// pigment for one barb: slate mostly, drab often, ink now and then, lightened toward the paper
function barbInk(shade) {
const r = random();
const base = r < 0.58 ? SLATE : r < 0.88 ? DRAB : INK;
if (random() < 0.76) {
return lerpColor(color(base), color(PAPER), constrain(randomGaussian(shade, 0.11), 0, 0.8));
}
return lerpColor(color(base), color(INK), constrain(randomGaussian(0.3, 0.16), 0, 0.7));
}
function tool() {
const r = random();
return r < 0.5 ? "HB" : r < 0.82 ? "2H" : "pen";
}
// a single barb, raked toward the tip and curving a little further out as it goes
function barb(t, side, reach, shade, weight, swing) {
const p = at(t);
const w = wAt(side < 0 ? UPPER : LOWER, t) * reach;
if (w < 3) return;
const lean = leanOf(side) + swing;
const len = w / max(0.35, sin(max(14, lean)));
const a = p.a + side * lean;
const mx = p.x + cos(a) * len * 0.54;
const my = p.y + sin(a) * len * 0.54;
const a2 = a + side * randomGaussian(side < 0 ? 4 : 7, 2.6);
brush.set(tool(), barbInk(shade), weight);
brush.noFill();
brush.beginShape(0.5);
brush.vertex(p.x + randomGaussian(0, 1.1), p.y + randomGaussian(0, 1.1));
brush.vertex(mx, my);
brush.vertex(mx + cos(a2) * len * 0.5, my + sin(a2) * len * 0.5);
brush.endShape(false);
}
// an even comb of barbs down one side of the rachis, darkening through the bars
function comb(tA, tB, step, side, reach, shade, weight) {
const n = min(320, max(1, floor((tB - tA) / step)));
for (let i = 0; i < n; i++) {
const t = tA + (i + randomGaussian(0, 0.18)) * step;
barb(t, side, reach * (1 + randomGaussian(0, 0.05)),
max(0.02, shade - barAt(t) * 0.2), weight * (1 + randomGaussian(0, 0.22)), randomGaussian(0, 1.5));
}
}
// a lock of barbs that has come away from its neighbours: same comb, swung off axis
function lock(tA, tB, count, swing, reach, shade) {
const n = min(26, count);
for (let i = 0; i < n; i++) {
const t = tA + ((tB - tA) * i) / n + randomGaussian(0, 0.0012);
barb(t, 1, reach * (1 + randomGaussian(0, 0.05)), shade, 0.68 + random(0, 0.4), swing);
}
}
// tonal band under the combing, from f0 to f1 of the vane half-width
function vaneBand(t0, t1, side, f0, f1, col, alpha) {
const w0 = wAt(side < 0 ? UPPER : LOWER, t0);
const w1 = wAt(side < 0 ? UPPER : LOWER, t1);
const a = vanePt(t0, side, w0 * f0);
const b = vanePt(t1, side, w1 * f0);
const c = vanePt(t1, side, w1 * f1);
const d = vanePt(t0, side, w0 * f1);
brush.noStroke();
brush.fill(col, alpha);
brush.beginShape(0.3);
brush.vertex(a[0], a[1]);
brush.vertex(b[0], b[1]);
brush.vertex(c[0], c[1]);
brush.vertex(d[0], d[1]);
brush.endShape(true);
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
PATH = buildPath();
// 1. slate air, kept on the broad side of the shaft so the upper-left stays dry
brush.noStroke();
brush.fillBleed(0.62, "out");
brush.fillTexture(0.4, 0.4);
brush.fill(SLATE, 26);
brush.circle(342, 570, 150, true);
brush.fill(SLATE, 22);
brush.circle(492, 486, 164, true);
brush.fill(DRAB, 19);
brush.circle(618, 388, 146, true);
brush.fill(SLATE, 15);
brush.circle(738, 262, 118, true);
brush.fill(DRAB, 13);
brush.circle(196, 650, 86, true);
// 2. vane tone — eight broad passes, a deeper valley hugging the rachis, then three bars
brush.fillBleed(0.22, "out");
brush.fillTexture(0.66, 0.42);
for (let k = 0; k < 8; k++) {
const t0 = 0.13 + (k / 8) * 0.45;
vaneBand(t0, t0 + 0.072, 1, 0.02, 1.0 + randomGaussian(0, 0.05),
random() < 0.6 ? SLATE : DRAB, max(66, randomGaussian(104, 14)));
}
for (let k = 0; k < 7; k++) {
const t0 = 0.145 + (k / 7) * 0.42;
vaneBand(t0, t0 + 0.07, 1, 0.03, 0.4 + randomGaussian(0, 0.06),
random() < 0.45 ? DRAB : INK, max(56, randomGaussian(88, 14)));
}
for (let k = 0; k < 9; k++) {
const t0 = 0.14 + (k / 9) * 0.7;
vaneBand(t0, t0 + 0.086, -1, 0.03, 0.95 + randomGaussian(0, 0.05),
random() < 0.5 ? SLATE : DRAB, max(58, randomGaussian(94, 14)));
}
// the three bars, laid across both vanes
vaneBand(0.228, 0.302, 1, 0.04, 0.98, DRAB, 74);
vaneBand(0.238, 0.296, -1, 0.05, 0.93, DRAB, 66);
vaneBand(0.418, 0.496, 1, 0.04, 0.96, INK, 62);
vaneBand(0.43, 0.488, -1, 0.05, 0.9, DRAB, 68);
vaneBand(0.632, 0.694, -1, 0.05, 0.88, DRAB, 56);
// loose patches of tone riding under the split locks only
vaneBand(0.596, 0.652, 1, 0.05, 0.74, DRAB, 52);
vaneBand(0.72, 0.77, 1, 0.05, 0.56, SLATE, 44);
vaneBand(0.82, 0.874, 1, 0.05, 0.62, DRAB, 38);
// 3. the combing. broad trailing vane below the shaft, narrow leading vane above
comb(0.15, 0.582, 0.0026, 1, 1.0, 0.28, 0.7);
comb(0.155, 0.575, 0.0082, 1, 0.98, 0.13, 0.82);
comb(0.16, 0.55, 0.013, 1, 0.48, 0.07, 0.9);
comb(0.145, 0.652, 0.003, -1, 0.99, 0.26, 0.62);
comb(0.15, 0.64, 0.0095, -1, 0.95, 0.12, 0.7);
comb(0.7, 0.9, 0.0042, -1, 0.97, 0.24, 0.58);
// downy barbs where the vane starts, softer and shorter than the rest
for (let i = 0; i < 30; i++) {
const t = 0.112 + i * 0.0031;
barb(t, random() < 0.5 ? 1 : -1, 0.4 + random(0, 0.22), 0.48 + random(0, 0.2), 0.52, randomGaussian(7, 7));
}
// 4. the split — six locks swung wide, bare paper opening in the wedges between them
lock(0.576, 0.606, 12, 3, 1.0, 0.16);
lock(0.616, 0.644, 11, 13, 0.92, 0.1);
lock(0.662, 0.696, 13, 26, 0.8, 0.06);
lock(0.714, 0.744, 10, 7, 0.98, 0.12);
lock(0.758, 0.796, 12, -7, 1.06, 0.09);
lock(0.812, 0.852, 9, 16, 0.88, 0.14);
// one lock lifted clear of the vane altogether, its base parted from the shaft
for (let i = 0; i < 11; i++) {
const t = 0.648 + i * 0.0026;
const p = at(t);
const len = wAt(LOWER, t) * 1.32;
const a = p.a + randomGaussian(34, 2);
const bx = p.x + cos(p.a + 90) * 16 + randomGaussian(0, 1.4);
const by = p.y + sin(p.a + 90) * 16 + randomGaussian(0, 1.4);
brush.set(random() < 0.4 ? "2B" : "HB", barbInk(0.06), 0.7 + random(0, 0.3));
brush.noFill();
brush.beginShape(0.55);
brush.vertex(bx, by);
brush.vertex(bx + cos(a) * len * 0.5, by + sin(a) * len * 0.5);
brush.vertex(bx + cos(a + 11) * len, by + sin(a + 11) * len);
brush.endShape(false);
}
// a notch missing altogether from the leading vane, then a lock leaning off it
comb(0.656, 0.69, 0.0072, -1, 0.28, 0.5, 0.5);
for (let i = 0; i < 10; i++) {
const t = 0.698 + i * 0.0038;
barb(t, -1, 1.06, 0.1, 0.62, -15 + randomGaussian(0, 2));
}
// 5. the tip, running off the top-right corner
comb(0.86, 1.0, 0.0048, 1, 0.95, 0.24, 0.6);
comb(0.94, 1.0, 0.006, -1, 0.9, 0.26, 0.6);
// 6. the leading edge, firm where the vane is whole and broken where it is not
brush.noFill();
const e1 = vanePt(0.19, -1, wAt(UPPER, 0.19) * 0.98);
const e2 = vanePt(0.33, -1, wAt(UPPER, 0.33) * 1.0);
const e3 = vanePt(0.47, -1, wAt(UPPER, 0.47) * 0.99);
brush.set("2B", lerpColor(color(INK), color(DRAB), 0.28), 1.1);
brush.beginShape(0.5);
brush.vertex(e1[0], e1[1]);
brush.vertex(e2[0], e2[1]);
brush.vertex(e3[0], e3[1]);
brush.endShape(false);
const e4 = vanePt(0.5, -1, wAt(UPPER, 0.5) * 0.97);
const e5 = vanePt(0.615, -1, wAt(UPPER, 0.615) * 1.0);
brush.set("2B", lerpColor(color(INK), color(SLATE), 0.22), 0.85);
brush.beginShape(0);
brush.vertex(e4[0], e4[1]);
brush.vertex(e5[0], e5[1]);
brush.endShape(false);
const e6 = vanePt(0.775, -1, wAt(UPPER, 0.775) * 0.96);
const e7 = vanePt(0.885, -1, wAt(UPPER, 0.885) * 0.94);
brush.set("2B", lerpColor(color(INK), color(SLATE), 0.34), 0.62);
brush.beginShape(0);
brush.vertex(e6[0], e6[1]);
brush.vertex(e7[0], e7[1]);
brush.endShape(false);
// 7. rachis: a pale spine, unrolled so the weight can taper, then a broken shadow beneath it
brush.set("cpencil", lerpColor(color(PAPER), color(SLATE), 0.12), 4.0);
brush.line(66, 644, 244, 567);
brush.set("cpencil", lerpColor(color(PAPER), color(SLATE), 0.1), 3.4);
brush.line(242, 568, 432, 453);
brush.set("cpencil", lerpColor(color(PAPER), color(SLATE), 0.16), 2.7);
brush.line(430, 455, 621, 296);
brush.set("cpencil", lerpColor(color(PAPER), color(SLATE), 0.22), 1.9);
brush.line(619, 298, 716, 195);
brush.set("cpencil", lerpColor(color(PAPER), color(SLATE), 0.28), 1.3);
brush.line(714, 198, 812, 76);
brush.set("2B", INK, 1.15);
brush.line(72, 651, 160, 617);
brush.line(174, 611, 252, 578);
brush.set("2B", lerpColor(color(INK), color(DRAB), 0.12), 1.0);
brush.line(260, 573, 344, 526);
brush.line(356, 519, 438, 463);
brush.set("2B", lerpColor(color(INK), color(DRAB), 0.18), 0.85);
brush.line(446, 457, 528, 392);
brush.line(542, 381, 612, 313);
brush.set("2B", lerpColor(color(INK), color(SLATE), 0.2), 0.7);
brush.line(626, 300, 694, 226);
brush.line(708, 208, 770, 136);
brush.set("2B", lerpColor(color(INK), color(SLATE), 0.34), 0.55);
brush.line(780, 122, 834, 54);
// 8. the quill: hollow calamus, a breath of rust inside it, one dark rim
brush.noStroke();
brush.fillBleed(0.38, "out");
brush.fillTexture(0.5, 0.6);
brush.fill(RUST, 108);
brush.circle(84, 636, 15, true);
brush.fill(RUST, 70);
brush.circle(118, 620, 11, true);
brush.fill(PAPER, 150);
brush.circle(62, 646, 9, true);
brush.noFill();
brush.set("2B", INK, 1.3);
brush.beginShape(0.6);
brush.vertex(50, 660);
brush.vertex(44, 646);
brush.vertex(56, 636);
brush.endShape(false);
brush.set("HB", lerpColor(color(RUST), color(INK), 0.5), 0.95);
brush.line(58, 654, 148, 614);
// 9. darks last: two tears opening along the shaft, and barbs adrift on the open paper
brush.set("2B", INK, 1.2);
brush.beginShape(0.5);
brush.vertex(548, 366);
brush.vertex(582, 384);
brush.vertex(596, 408);
brush.endShape(false);
brush.set("2B", lerpColor(color(INK), color(DRAB), 0.16), 1.0);
brush.beginShape(0.5);
brush.vertex(650, 274);
brush.vertex(686, 300);
brush.vertex(704, 334);
brush.endShape(false);
brush.set("2B", lerpColor(color(INK), color(SLATE), 0.26), 0.8);
brush.beginShape(0.5);
brush.vertex(730, 186);
brush.vertex(758, 214);
brush.vertex(770, 248);
brush.endShape(false);
brush.set("pen", lerpColor(color(DRAB), color(INK), 0.2), 0.8);
brush.beginShape(0.6);
brush.vertex(556, 592);
brush.vertex(608, 608);
brush.vertex(664, 598);
brush.endShape(false);
brush.set("pen", lerpColor(color(SLATE), color(INK), 0.15), 0.65);
brush.beginShape(0.6);
brush.vertex(694, 496);
brush.vertex(732, 520);
brush.vertex(762, 514);
brush.endShape(false);
brush.noStroke();
brush.fill(DRAB, 120);
brush.circle(626, 668, 3.4, true);
}