Weight Against Edge
2026 · abstract
Pressure is not an event but a direction: everything on one side leans, and the paper on the other has to hold by being empty.
a heavy near-black mass building against the right edge behind a torn vertical seam, the pale field beside it stretched thin
painted by Kimi K2.6p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: one heavy near-black mass builds against the right edge, its torn seam running top to bottom;
// the left two-fifths stays open paper, drawn out into thin vertical scratches that read as tension, not incident.
// layering: pale ground washes, slate under-field, four dark band passes, scraped grain, then the sharpened seam and rationed rust last.
const PALETTE = [
"#e9e2d4", // ground
"#232227", // mass
"#131316", // core
"#4a4a51", // shadow
"#8b8271", // grain
"#a2412a", // accent
];
// the torn seam, hand-set anchor by anchor: mostly plumb, then a wedge of paper
// bites far right at the upper third and the field re-closes below it
const SEAM = [
[-40, 530], [42, 512], [98, 528], [152, 468], [198, 488],
[244, 606], [268, 678], [294, 542], [342, 448], [394, 506],
[448, 490], [504, 550], [560, 510], [618, 566], [674, 518],
[732, 554], [790, 506], [840, 532],
];
function seamX(y, jig) {
let i = 0;
while (i < SEAM.length - 2 && SEAM[i + 1][0] < y) i++;
const a = SEAM[i];
const b = SEAM[i + 1];
const t = constrain((y - a[0]) / (b[0] - a[0]), 0, 1);
return lerp(a[1], b[1], t) + randomGaussian(0, jig);
}
function massInk() {
const r = random();
if (r < 0.6) return "#232227";
if (r < 0.9) return "#131316";
return "#4a4a51";
}
// one knife-width of the mass: a trapezoid from the seam out past the right edge
function massBand(y0, h, dx, col, op, bleed, jig) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.16, 0.3);
brush.fill(col, op);
brush.beginShape(0.06);
for (let k = 0; k <= 4; k++) {
const y = y0 + (h * k) / 4;
brush.vertex(seamX(y, jig) + dx * (k === 0 || k === 4 ? 1 : 0.7), y);
}
brush.vertex(842, y0 + h);
brush.vertex(842, y0);
brush.endShape(true);
}
function massPass(dx, opMean, bleed, jig, hMean) {
let y = -50;
for (let i = 0; i < 26; i++) {
if (y > 812) break;
const h = Math.max(48, randomGaussian(hMean, 14));
massBand(y, h, dx + randomGaussian(0, 3), massInk(), Math.max(60, randomGaussian(opMean, 16)), bleed, jig);
y += h * (0.44 + random(0, 0.14));
}
}
// how far a stroke at x can run before the seam gets in its way
function slabRun(x, yc, want, clear) {
let y0 = yc;
let y1 = yc;
for (let k = 0; k < 60; k++) {
if (y1 - y0 >= want) break;
const up = y0 > -44 && seamX(y0 - 12, 0) < x - clear;
const down = y1 < 838 && seamX(y1 + 12, 0) < x - clear;
if (!up && !down) break;
if (up) y0 -= 12;
if (down) y1 += 12;
}
return [y0, y1];
}
// one wide vertical stroke of the mass — knife-loaded, near plumb, never quite straight
function slab(x, y0, y1, w, col, name) {
brush.set(name, col, w);
brush.noFill();
brush.beginShape(0.25);
brush.vertex(x + randomGaussian(0, 2.2), y0);
brush.vertex(x + randomGaussian(0, 3.4), lerp(y0, y1, 0.38));
brush.vertex(x + randomGaussian(0, 3.4), lerp(y0, y1, 0.74));
brush.vertex(x + randomGaussian(0, 2.2), y1);
brush.endShape(false);
}
function slabField(n, xMean, xSpread, wMean, lenMean) {
for (let i = 0; i < n; i++) {
const x = constrain(randomGaussian(xMean, xSpread), 430, 836);
const yc = constrain(randomGaussian(400, 268), -30, 826);
const w = Math.max(2.4, randomGaussian(wMean, 3.2));
const clear = 16 + w * 2.2;
if (seamX(yc, 0) > x - clear) continue;
const run = slabRun(x, yc, Math.max(90, randomGaussian(lenMean, 150)), clear);
if (run[1] - run[0] < 128) continue;
const r = random();
const col = r < 0.66 ? "#131316" : r < 0.9 ? "#232227" : "#4a4a51";
slab(x, run[0], run[1], w, col, random() < 0.72 ? "charcoal" : "2B");
}
}
// dry vertical grain inside the mass — a few of them scratch back to warm grey
function grainStroke(x, y, len, col, w) {
brush.set("charcoal", col, w);
brush.noFill();
brush.beginShape(0.2);
brush.vertex(x + randomGaussian(0, 2), y);
brush.vertex(x + randomGaussian(0, 3.2), y + len * 0.46);
brush.vertex(x + randomGaussian(0, 3.2), y + len);
brush.endShape(false);
}
function grainField(n) {
for (let i = 0; i < n; i++) {
const y = constrain(randomGaussian(400, 250), -40, 764);
const x = seamX(y, 0) + 34 + Math.abs(randomGaussian(0, 116));
if (x > 826 || x < 300) continue;
const r = random();
const col = r < 0.66 ? "#4a4a51" : r < 0.78 ? "#8b8271" : "#131316";
const w = col === "#131316" ? 1.2 + random(0, 1.6) : col === "#8b8271" ? 0.5 + random(0, 0.4) : 0.7 + random(0, 0.8);
grainStroke(x, y, Math.max(30, randomGaussian(104, 52)), col, w);
}
}
// the left field: long thin marks, stretched far past their width
function thinStroke(x, y, len, lean, col, w, name) {
brush.set(name, col, w);
brush.noFill();
brush.beginShape(0.35);
brush.vertex(x, y);
brush.vertex(x + lean * 0.4 + randomGaussian(0, 2.2), y + len * 0.36);
brush.vertex(x + lean * 0.8 + randomGaussian(0, 2.6), y + len * 0.72);
brush.vertex(x + lean, y + len);
brush.endShape(false);
}
function thinField(n) {
for (let i = 0; i < n; i++) {
const band = random() < 0.62 ? 196 : 322;
const x = constrain(randomGaussian(band, 40), 96, 396);
const y = randomGaussian(300, 190);
const len = random() < 0.24 ? Math.max(54, randomGaussian(110, 46)) : Math.max(120, randomGaussian(300, 140));
const pale = random() < 0.72;
thinStroke(
x,
y,
len,
randomGaussian(0, 9),
pale ? "#8b8271" : "#4a4a51",
pale ? 0.45 + random(0, 0.3) : 0.55 + random(0, 0.4),
random() < 0.5 ? "2H" : "pen",
);
}
}
// a stretched pale wash — tall, narrow, barely there
function stretchWash(x, y, w, h, col, op) {
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.38, 0.45);
brush.fill(col, op);
brush.beginShape(0.2);
brush.vertex(x + randomGaussian(0, 4), y);
brush.vertex(x + w + randomGaussian(0, 5), y + h * 0.3);
brush.vertex(x + w * 0.82 + randomGaussian(0, 5), y + h);
brush.vertex(x - w * 0.14 + randomGaussian(0, 4), y + h * 0.62);
brush.endShape(true);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#e9e2d4");
// 1 — ground: the faintest cold breath where the mass will settle, and warmth held to the left
stretchWash(470, -30, 300, 880, "#4a4a51", 16);
stretchWash(612, 120, 210, 640, "#4a4a51", 12);
stretchWash(150, 60, 120, 520, "#8b8271", 13);
stretchWash(300, 380, 90, 420, "#8b8271", 10);
// 2 — a stain under the mass: two quiet passes, held inside the tear so nothing haloes it
massPass(14, 96, 0.12, 3.0, 190);
massPass(9, 122, 0.1, 2.4, 150);
// 3 — the mass proper, laid on in wide vertical strokes that stop dead at the seam
slabField(90, 640, 118, 11, 420);
slabField(80, 560, 96, 8, 320);
slabField(64, 748, 74, 13, 480);
slabField(56, 600, 130, 5, 240);
// 4 — weight pressed hard into the right edge, where the mass has nowhere left to go
slab(806, -34, 470, 17, "#131316", "charcoal");
slab(828, -30, 372, 15, "#131316", "charcoal");
slab(792, 402, 836, 19, "#131316", "charcoal");
slab(824, 486, 834, 14, "#232227", "2B");
slab(766, 118, 618, 12, "#131316", "charcoal");
slab(700, 512, 834, 13, "#131316", "charcoal");
slab(688, -34, 246, 10, "#232227", "charcoal");
// 5 — hatched tooth low in the mass, so the dark holds grain rather than going flat
brush.hatchStyle("2B", "#131316", 0.8);
brush.hatch(5.5, 88, { rand: 0.34, continuous: false, gradient: 0.45 });
brush.noStroke();
brush.fillBleed(0.06, "out");
brush.fill("#232227", 72);
brush.beginShape(0.06);
brush.vertex(566, 356);
brush.vertex(842, 320);
brush.vertex(842, 836);
brush.vertex(528, 828);
brush.endShape(true);
brush.noHatch();
// 6 — grain: dry scrapes, most of them lifting light back out of the dark
grainField(46);
// 7 — weld a hard strip along the seam: this is the tear, and it takes no transition
brush.noStroke();
brush.fillTexture(0.14, 0.2);
for (let i = 0; i < SEAM.length - 1; i++) {
const a = SEAM[i];
const b = SEAM[i + 1];
const w = 22 + random(0, 16);
brush.fillBleed(0.03 + random(0, 0.05), "out");
brush.fill(i % 5 === 0 ? "#232227" : "#131316", 188 + random(-22, 30));
brush.beginShape(0.04);
brush.vertex(a[1] + randomGaussian(1.5, 1.8), a[0]);
brush.vertex(b[1] + randomGaussian(1.5, 1.8), b[0]);
brush.vertex(b[1] + w, b[0]);
brush.vertex(a[1] + w + randomGaussian(0, 6), a[0]);
brush.endShape(true);
// torn ticks, biting inward from the edge, never tracing it
if (random() < 0.65) {
brush.set("2B", "#131316", 1.1 + random(0, 1.2));
const ty = lerp(a[0], b[0], random(0.2, 0.8));
const tx = seamX(ty, 1.2) + 10 + random(0, 12);
brush.line(tx, ty, tx + randomGaussian(24, 10), ty + randomGaussian(0, 9));
}
}
// 8 — the left field, stretched thin
thinField(26);
thinStroke(126, 96, 520, -7, "#8b8271", 0.5, "2H");
thinStroke(356, 214, 468, 12, "#4a4a51", 0.6, "pen");
thinStroke(238, 466, 332, 4, "#8b8271", 0.55, "2H");
thinStroke(408, 560, 246, -9, "#8b8271", 0.5, "pen");
// 9 — one thin dark stake dropped into the open paper, stretched until it gives out
slab(261, -34, 208, 4.6, "#131316", "charcoal");
slab(265, -30, 186, 3.2, "#131316", "2B");
slab(258, 42, 244, 2.4, "#232227", "charcoal");
brush.set("2B", "#131316", 1.6);
brush.noFill();
brush.beginShape(0.2);
brush.vertex(263, 214);
brush.vertex(266, 272);
brush.vertex(262, 318);
brush.endShape(false);
brush.set("2H", "#4a4a51", 0.55);
brush.line(264, 322, 269, 398);
// 10 — rust, rationed: a hot line along one seam segment and two flecks
brush.set("2B", "#a2412a", 1.9);
brush.noFill();
brush.beginShape(0.25);
brush.vertex(488, 348);
brush.vertex(494, 402);
brush.vertex(486, 452);
brush.endShape(false);
brush.set("2B", "#a2412a", 1.1);
brush.line(266, 606, 269, 638);
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fill("#a2412a", 150);
brush.circle(652, 262, 4.6, true);
// 11 — last darks: a few flecks off the mass, thrown into the paper
brush.fillBleed(0.34, "out");
brush.fill("#131316", 122);
brush.circle(414, 700, 5.6, true);
brush.fill("#232227", 96);
brush.circle(392, 118, 4.4, true);
brush.fill("#131316", 108);
brush.circle(430, 366, 3.4, true);
brush.fill("#4a4a51", 84);
brush.circle(196, 742, 6.2, true);
}