CRM as code

Keep your CRM in the same repo as your app

Write the companies, people, deals and stages your team sells with as code. Review each change in a pull request and ship it with the rest of your product.

Free while your team has five seats or fewer.

CRM model A request typed into a prompt box becomes a working data model: company, person and deal objects form field by field on a canvas, and the relations between them join once both ends exist. float band(float d, float a, float b) { return 1.0 - sm(a, b, d); } float rect(vec2 p, vec2 a, vec2 b) { return step(a.x, p.x) * step(p.x, b.x) * step(a.y, p.y) * step(p.y, b.y); } float win(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } // a soft link from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b) { vec2 ab = b - a; float h = clamp(dot(p - a, ab) / max(dot(ab, ab), 0.001), 0.0, 1.0); vec2 d = (p - a - ab * h) / vec2(3.6, 4.8); return 1.0 - sm(0.8, 1.05, length(d)); } // whole cells: a block of columns c0..c1 and cells k0..k1 (inclusive), in grid units float cells(vec2 p, float c0, float c1, float k0, float k1) { return rect(p, vec2(0.5 + 7.0 * c0, 9.4 * k0), vec2(7.5 + 7.0 * c1, 9.4 * k1 + 7.0)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; float s = mod(t + 6.4, T); float ph = TAU * t / T; float keep = (1.0 - sm(7.3, 7.7, s)); float v = 0.0; float col = floor((p.x - 4.0) / 7.0 + 0.5); float cel = floor((p.y - 3.5) / 9.4 + 0.5); // the prompt box: a rim, two attached files, a toolbar and the send button float focus = 0.62 + 0.3 * sm(0.3, 0.4, s) * (1.0 - sm(1.6, 1.8, s)); v += focus * (cells(p, 4.0, 26.0, 8.0, 8.0) + cells(p, 4.0, 26.0, 25.0, 25.0) + cells(p, 4.0, 4.0, 9.0, 24.0) + cells(p, 26.0, 26.0, 9.0, 24.0)); v += 0.75 * cells(p, 5.0, 5.0, 18.0, 18.0) + 0.75 * cells(p, 15.0, 15.0, 18.0, 18.0); v += 0.2 * cells(p, 6.0, 25.0, 20.0, 20.0); v += 0.75 * cells(p, 6.0, 6.0, 22.0, 22.0) + 0.42 * cells(p, 8.0, 13.0, 22.0, 22.0); float press = sm(1.58, 1.62, s) * (1.0 - sm(1.75, 1.95, s)); v += (0.7 + 0.45 * press) * cells(p, 22.0, 24.0, 21.0, 23.0); // the caret: it waits blinking at the start, runs along each line as it is typed, and goes once the request is sent { float k = s < 0.8 ? 11.0 : s < 1.25 ? 13.0 : 15.0; float u = s < 0.8 ? win(s, 0.35, 0.8) : s < 1.25 ? win(s, 0.8, 1.25) : win(s, 1.25, 1.6); float x = 38.0 + 2.0 + u * 118.8; float on = s < 0.35 || s > 7.7 ? step(0.5, fract(s * 2.2)) : s < 1.6 ? 1.0 : 0.0; v += on * 1.1 * step(abs(p.x - (4.0 + 7.0 * floor((x - 4.0) / 7.0 + 0.5))), 3.5) * band(abs(p.y - (3.5 + 9.4 * k)), 4.0, 4.6); } // the request on its way from the send button to the agent { float u = win(s, 1.65, 1.95); v += step(1.65, s) * step(s, 1.95) * blob(p, mix(vec2(165.0, 210.3), vec2(228.0, 210.3), ease(u)), 7.0, 1.15); } // the agent: a ring, and a core that turns slowly at rest and churns while it builds { float act = sm(1.9, 2.05, s) * (1.0 - sm(4.5, 4.9, s)); float d = length(p - vec2(228.0, 210.3)); v += (0.42 + 0.25 * act) * exp(-pow(d - 19.0, 2.0) / 30.0); float churn = noise(p * 0.16 + vec2(2.0 * cos(ph), 2.0 * sin(ph))); v += blob(p, vec2(228.0, 210.3), 10.0 + 2.0 * act, 0.95 + 0.3 * act + 0.3 * (churn - 0.5)); } // the canvas: a faint dot grid, clear of the objects and their names { float hole = 0.0; hole = max(hole, cells(p, 39.0, 61.0, 4.0, 18.0)); hole = max(hole, cells(p, 39.0, 61.0, 23.0, 37.0)); hole = max(hole, cells(p, 75.0, 97.0, 10.0, 26.0)); hole = max(hole, cells(p, 78.0, 98.0, 38.0, 45.0)); float dt = step(abs(mod(col, 3.0) - 1.0), 0.5) * step(abs(mod(cel, 2.0) - 1.0), 0.5); float in_ = rect(p, vec2(266.0, 34.0), vec2(692.0, 428.0)); v += in_ * (1.0 - hole) * dt * (0.2 + 0.1 * sin(ph + 6.2832 * hash(vec2(col, cel)))); } // the pieces in flight, from the agent to the header or field they become { float u = win(s, 2.0, 2.50); if (s > 2.00 && s < 2.50) v += blob(p, mix(vec2(228.0, 210.3), vec2(312.0, 59.9), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 2.1, 2.65); if (s > 2.15 && s < 2.65) v += blob(p, mix(vec2(228.0, 210.3), vec2(298.0, 88.1), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 2.3, 2.80); if (s > 2.30 && s < 2.80) v += blob(p, mix(vec2(228.0, 210.3), vec2(298.0, 106.9), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 2.5, 2.95); if (s > 2.45 && s < 2.95) v += blob(p, mix(vec2(228.0, 210.3), vec2(298.0, 125.7), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 2.6, 3.10); if (s > 2.60 && s < 3.10) v += blob(p, mix(vec2(228.0, 210.3), vec2(298.0, 144.5), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 2.8, 3.25); if (s > 2.75 && s < 3.25) v += blob(p, mix(vec2(228.0, 210.3), vec2(312.0, 238.5), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 2.9, 3.40); if (s > 2.90 && s < 3.40) v += blob(p, mix(vec2(228.0, 210.3), vec2(298.0, 266.7), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 3.0, 3.55); if (s > 3.05 && s < 3.55) v += blob(p, mix(vec2(228.0, 210.3), vec2(298.0, 285.5), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 3.2, 3.70); if (s > 3.20 && s < 3.70) v += blob(p, mix(vec2(228.0, 210.3), vec2(298.0, 304.3), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 3.3, 3.85); if (s > 3.35 && s < 3.85) v += blob(p, mix(vec2(228.0, 210.3), vec2(298.0, 323.1), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 3.5, 4.00); if (s > 3.50 && s < 4.00) v += blob(p, mix(vec2(228.0, 210.3), vec2(564.0, 116.3), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 3.6, 4.15); if (s > 3.65 && s < 4.15) v += blob(p, mix(vec2(228.0, 210.3), vec2(550.0, 144.5), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 3.8, 4.30); if (s > 3.80 && s < 4.30) v += blob(p, mix(vec2(228.0, 210.3), vec2(550.0, 163.3), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 4.0, 4.45); if (s > 3.95 && s < 4.45) v += blob(p, mix(vec2(228.0, 210.3), vec2(550.0, 182.1), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 4.1, 4.60); if (s > 4.10 && s < 4.60) v += blob(p, mix(vec2(228.0, 210.3), vec2(550.0, 200.9), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } { float u = win(s, 4.3, 4.75); if (s > 4.25 && s < 4.75) v += blob(p, mix(vec2(228.0, 210.3), vec2(550.0, 219.7), ease(u)), 7.0, 1.15 * sm(0.0, 0.12, u)); } // the objects: a header and a rim once their piece lands, then each field as its own piece lands (a flash that // settles to a faint row, with its type mark) { float fm = step(2.50, s) * keep; float fl = sm(2.50, 2.55, s) * (1.0 - sm(2.55, 3.00, s)); v += fm * (1.05 + 0.2 * fl) * cells(p, 40.0, 60.0, 6.0, 6.0); v += fm * 0.7 * cells(p, 40.0, 60.0, 7.0, 7.0); v += fm * 0.62 * (cells(p, 40.0, 40.0, 8.0, 17.0) + cells(p, 60.0, 60.0, 8.0, 17.0) + cells(p, 41.0, 59.0, 17.0, 17.0)); float fl2 = step(abs(mod(cel - 6.0, 2.0)), 0.5) * step(8.0, cel) * step(hash(vec2(col, cel) + 7.1), 0.42); v += fm * fl2 * (0.22 + 0.09 * sin(ph + 9.0 * hash(vec2(cel, col)))) * cells(p, 41.0, 59.0, 8.0, 16.0); } { float fm = step(2.65, s) * keep; float fl = sm(2.65, 2.70, s) * (1.0 - sm(2.70, 3.15, s)); v += fm * (0.75 * cells(p, 42.0, 42.0, 9.0, 9.0) + (1.0 + 0.6 * fl) * (0.42 * cells(p, 51.0, 57.0, 9.0, 9.0))); } { float fm = step(2.80, s) * keep; float fl = sm(2.80, 2.85, s) * (1.0 - sm(2.85, 3.30, s)); v += fm * (0.75 * cells(p, 42.0, 42.0, 11.0, 11.0) + (1.0 + 0.6 * fl) * (0.75 * cells(p, 51.0, 51.0, 11.0, 11.0) + 0.42 * cells(p, 52.0, 59.0, 11.0, 11.0))); } { float fm = step(2.95, s) * keep; float fl = sm(2.95, 3.00, s) * (1.0 - sm(3.00, 3.45, s)); v += fm * (0.75 * cells(p, 42.0, 42.0, 13.0, 13.0) + (1.0 + 0.6 * fl) * (0.7 * cells(p, 55.0, 59.0, 13.0, 13.0))); } { float fm = step(3.10, s) * keep; float fl = sm(3.10, 3.15, s) * (1.0 - sm(3.15, 3.60, s)); v += fm * (0.75 * cells(p, 42.0, 42.0, 15.0, 15.0) + (1.0 + 0.6 * fl) * (0.7 * cells(p, 51.0, 52.0, 15.0, 15.0) + 0.7 * cells(p, 54.0, 55.0, 15.0, 15.0) + 0.7 * cells(p, 57.0, 59.0, 15.0, 15.0))); } { float fm = step(3.25, s) * keep; float fl = sm(3.25, 3.30, s) * (1.0 - sm(3.30, 3.75, s)); v += fm * (1.05 + 0.2 * fl) * cells(p, 40.0, 60.0, 25.0, 25.0); v += fm * 0.7 * cells(p, 40.0, 60.0, 26.0, 26.0); v += fm * 0.62 * (cells(p, 40.0, 40.0, 27.0, 36.0) + cells(p, 60.0, 60.0, 27.0, 36.0) + cells(p, 41.0, 59.0, 36.0, 36.0)); float fl2 = step(abs(mod(cel - 25.0, 2.0)), 0.5) * step(27.0, cel) * step(hash(vec2(col, cel) + 7.1), 0.42); v += fm * fl2 * (0.22 + 0.09 * sin(ph + 9.0 * hash(vec2(cel, col)))) * cells(p, 41.0, 59.0, 27.0, 35.0); } { float fm = step(3.40, s) * keep; float fl = sm(3.40, 3.45, s) * (1.0 - sm(3.45, 3.90, s)); v += fm * (0.75 * cells(p, 42.0, 42.0, 28.0, 28.0) + (1.0 + 0.6 * fl) * (0.42 * cells(p, 51.0, 57.0, 28.0, 28.0))); } { float fm = step(3.55, s) * keep; float fl = sm(3.55, 3.60, s) * (1.0 - sm(3.60, 4.05, s)); v += fm * (0.75 * cells(p, 42.0, 42.0, 30.0, 30.0) + (1.0 + 0.6 * fl) * (0.42 * cells(p, 51.0, 54.0, 30.0, 30.0) + 0.75 * cells(p, 55.0, 55.0, 30.0, 30.0) + 0.42 * cells(p, 56.0, 58.0, 30.0, 30.0))); } { float fm = step(3.70, s) * keep; float fl = sm(3.70, 3.75, s) * (1.0 - sm(3.75, 4.20, s)); v += fm * (0.75 * cells(p, 42.0, 42.0, 32.0, 32.0) + (1.0 + 0.6 * fl) * (0.42 * cells(p, 51.0, 53.0, 32.0, 32.0) + 0.42 * cells(p, 55.0, 57.0, 32.0, 32.0) + 0.42 * cells(p, 59.0, 58.0, 32.0, 32.0))); } { float fm = step(3.85, s) * keep; float fl = sm(3.85, 3.90, s) * (1.0 - sm(3.90, 4.35, s)); v += fm * (1.1 * cells(p, 42.0, 42.0, 34.0, 34.0) + (1.0 + 0.6 * fl) * (0.7 * cells(p, 51.0, 59.0, 34.0, 34.0))); } { float fm = step(4.00, s) * keep; float fl = sm(4.00, 4.05, s) * (1.0 - sm(4.05, 4.50, s)); v += fm * (1.05 + 0.2 * fl) * cells(p, 76.0, 96.0, 12.0, 12.0); v += fm * 0.7 * cells(p, 76.0, 96.0, 13.0, 13.0); v += fm * 0.62 * (cells(p, 76.0, 76.0, 14.0, 25.0) + cells(p, 96.0, 96.0, 14.0, 25.0) + cells(p, 77.0, 95.0, 25.0, 25.0)); float fl2 = step(abs(mod(cel - 12.0, 2.0)), 0.5) * step(14.0, cel) * step(hash(vec2(col, cel) + 7.1), 0.42); v += fm * fl2 * (0.22 + 0.09 * sin(ph + 9.0 * hash(vec2(cel, col)))) * cells(p, 77.0, 95.0, 14.0, 24.0); } { float fm = step(4.15, s) * keep; float fl = sm(4.15, 4.20, s) * (1.0 - sm(4.20, 4.65, s)); v += fm * (0.75 * cells(p, 78.0, 78.0, 15.0, 15.0) + (1.0 + 0.6 * fl) * (0.42 * cells(p, 87.0, 93.0, 15.0, 15.0))); } { float fm = step(4.30, s) * keep; float fl = sm(4.30, 4.35, s) * (1.0 - sm(4.35, 4.80, s)); v += fm * (0.75 * cells(p, 78.0, 78.0, 17.0, 17.0) + (1.0 + 0.6 * fl) * (0.75 * cells(p, 89.0, 89.0, 17.0, 17.0) + 0.7 * cells(p, 90.0, 95.0, 17.0, 17.0))); } { float fm = step(4.45, s) * keep; float fl = sm(4.45, 4.50, s) * (1.0 - sm(4.50, 4.95, s)); v += fm * (0.75 * cells(p, 78.0, 78.0, 19.0, 19.0) + (1.0 + 0.6 * fl) * (0.42 * cells(p, 87.0, 92.0, 19.0, 19.0) + 0.75 * cells(p, 94.0, 95.0, 19.0, 19.0))); } { float fm = step(4.60, s) * keep; float fl = sm(4.60, 4.65, s) * (1.0 - sm(4.65, 5.10, s)); v += fm * (1.1 * cells(p, 78.0, 78.0, 21.0, 21.0) + (1.0 + 0.6 * fl) * (0.7 * cells(p, 87.0, 95.0, 21.0, 21.0))); } { float fm = step(4.75, s) * keep; float fl = sm(4.75, 4.80, s) * (1.0 - sm(4.80, 5.25, s)); v += fm * (1.1 * cells(p, 78.0, 78.0, 23.0, 23.0) + (1.0 + 0.6 * fl) * (0.7 * cells(p, 87.0, 95.0, 23.0, 23.0))); } // the relations, each from the field that holds it to the object it points at, once both exist float rl = 0.0; { float vv = v; v = 0.0; { float gg = win(s, 3.85, 4.30) * keep; float gl = gg * 56.4; vec2 hd = vec2(354.0, 229.1); { float u = clamp((gl - 0.0) / 56.4, 0.0, 1.0); if (u > 0.0) { vec2 e = mix(vec2(354.0, 229.1), vec2(354.0, 172.7), u); v = max(v, 0.6 * lk(p, vec2(354.0, 229.1), e)); hd = e; } } if (gg > 0.0 && gg < 1.0) v += blob(p, hd, 6.5, 1.1); } { float gg = win(s, 4.60, 5.05) * keep; float gl = gg * 253.0; vec2 hd = vec2(536.0, 200.9); { float u = clamp((gl - 0.0) / 56.0, 0.0, 1.0); if (u > 0.0) { vec2 e = mix(vec2(536.0, 200.9), vec2(480.0, 200.9), u); v = max(v, 0.6 * lk(p, vec2(536.0, 200.9), e)); hd = e; } } { float u = clamp((gl - 56.0) / 141.0, 0.0, 1.0); if (u > 0.0) { vec2 e = mix(vec2(480.0, 200.9), vec2(480.0, 59.9), u); v = max(v, 0.6 * lk(p, vec2(480.0, 200.9), e)); hd = e; } } { float u = clamp((gl - 197.0) / 56.0, 0.0, 1.0); if (u > 0.0) { vec2 e = mix(vec2(480.0, 59.9), vec2(424.0, 59.9), u); v = max(v, 0.6 * lk(p, vec2(480.0, 59.9), e)); hd = e; } } if (gg > 0.0 && gg < 1.0) v += blob(p, hd, 6.5, 1.1); } { float gg = win(s, 4.75, 5.20) * keep; float gl = gg * 130.8; vec2 hd = vec2(536.0, 219.7); { float u = clamp((gl - 0.0) / 28.0, 0.0, 1.0); if (u > 0.0) { vec2 e = mix(vec2(536.0, 219.7), vec2(508.0, 219.7), u); v = max(v, 0.6 * lk(p, vec2(536.0, 219.7), e)); hd = e; } } { float u = clamp((gl - 28.0) / 18.8, 0.0, 1.0); if (u > 0.0) { vec2 e = mix(vec2(508.0, 219.7), vec2(508.0, 238.5), u); v = max(v, 0.6 * lk(p, vec2(508.0, 219.7), e)); hd = e; } } { float u = clamp((gl - 46.8) / 84.0, 0.0, 1.0); if (u > 0.0) { vec2 e = mix(vec2(508.0, 238.5), vec2(424.0, 238.5), u); v = max(v, 0.6 * lk(p, vec2(508.0, 238.5), e)); hd = e; } } if (gg > 0.0 && gg < 1.0) v += blob(p, hd, 6.5, 1.1); } rl = v; v = vv; } v += rl; // the strip: one slot per field, filled as the field forms, as tall as the fields so far if (p.y > 289.9 && p.y < 422.1 && p.x > 21.0 && p.x < 197.0) { v += 0.17 * step(mod(col - 3.0, 2.0), 0.5) * step(44.0 - (col - 3.0) * 0.5, cel); v += step(2.65, s) * keep * cells(p, 3.0, 3.0, 44.0, 44.0) * (0.55 + 0.5 * step(cel, 44.5)); v += step(2.80, s) * keep * cells(p, 5.0, 5.0, 43.0, 44.0) * (0.55 + 0.5 * step(cel, 43.5)); v += step(2.95, s) * keep * cells(p, 7.0, 7.0, 42.0, 44.0) * (0.55 + 0.5 * step(cel, 42.5)); v += step(3.10, s) * keep * cells(p, 9.0, 9.0, 41.0, 44.0) * (0.55 + 0.5 * step(cel, 41.5)); v += step(3.40, s) * keep * cells(p, 11.0, 11.0, 40.0, 44.0) * (0.55 + 0.5 * step(cel, 40.5)); v += step(3.55, s) * keep * cells(p, 13.0, 13.0, 39.0, 44.0) * (0.55 + 0.5 * step(cel, 39.5)); v += step(3.70, s) * keep * cells(p, 15.0, 15.0, 38.0, 44.0) * (0.55 + 0.5 * step(cel, 38.5)); v += step(3.85, s) * keep * cells(p, 17.0, 17.0, 37.0, 44.0) * (0.55 + 0.5 * step(cel, 37.5)); v += step(4.15, s) * keep * cells(p, 19.0, 19.0, 36.0, 44.0) * (0.55 + 0.5 * step(cel, 36.5)); v += step(4.30, s) * keep * cells(p, 21.0, 21.0, 35.0, 44.0) * (0.55 + 0.5 * step(cel, 35.5)); v += step(4.45, s) * keep * cells(p, 23.0, 23.0, 34.0, 44.0) * (0.55 + 0.5 * step(cel, 34.5)); v += step(4.60, s) * keep * cells(p, 25.0, 25.0, 33.0, 44.0) * (0.55 + 0.5 * step(cel, 33.5)); v += step(4.75, s) * keep * cells(p, 27.0, 27.0, 32.0, 44.0) * (0.55 + 0.5 * step(cel, 32.5)); } return v; }

Used by engineers at payroll, robotics, travel, insurance and developer tool startups.

Where CRM changes get stuck

Sales waits a year for a new field, or engineers patch a homemade CRM every week

Two ways to build The same five change requests reach two systems over a year: a sealed monolith queues them until its one release and cannot be looked inside, while an in-house stack ships version one fast but breaks and needs a patch with every change, each costing more than the last. float band(float d, float a, float b) { return 1.0 - sm(a, b, d); } float rect(vec2 p, vec2 a, vec2 b) { return step(a.x, p.x) * step(p.x, b.x) * step(a.y, p.y) * step(p.y, b.y); } float win(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } // a soft link from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b) { vec2 ab = b - a; float h = clamp(dot(p - a, ab) / max(dot(ab, ab), 0.001), 0.0, 1.0); vec2 d = (p - a - ab * h) / vec2(3.6, 4.8); return 1.0 - sm(0.8, 1.05, length(d)); } // whole cells: a block of columns c0..c1 and cells k0..k1 (inclusive), in grid units float cells(vec2 p, float c0, float c1, float k0, float k1) { return rect(p, vec2(0.5 + 7.0 * c0, 9.4 * k0), vec2(7.5 + 7.0 * c1, 9.4 * k1 + 7.0)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; float s = mod(t + 6.3, T); float ph = TAU * t / T; float v = 0.0; float col = floor((p.x - 4.0) / 7.0 + 0.5); float cel = floor((p.y - 3.5) / 9.4 + 0.5); // ---- A: the monolith, dense right through, in strata, with nothing to see inside { float open = sm(6.45, 6.6, s) * (1.0 - sm(7.7, 7.9, s)); float body = cells(p, 12.0, 40.0, 13.0, 29.0); float gap = cells(p, 15.0, 17.0, 13.0, 13.0) * open; float strata = step(abs(mod(cel, 3.0)), 0.5); float churn = noise(p * 0.05 + vec2(1.5 * cos(ph), 1.5 * sin(ph))); v += body * (1.0 - gap) * (0.95 + 0.2 * strata + 0.12 * (churn - 0.5)); // the gate's posts and the queue's rail v += 0.75 * (cells(p, 14.0, 14.0, 10.0, 12.0) + cells(p, 18.0, 18.0, 10.0, 12.0)); v += 0.42 * cells(p, 14.0, 18.0, 12.0, 12.0) * (1.0 - open); v += 0.2 * cells(p, 19.0, 38.0, 10.0, 10.0); } // A's requests: each arrives and waits in its slot; at the release they go in through the gate, one after another { vec2 q = vec2(298.0, 41.1); float k = 0.0; if (s > 0.6 && s < 1.1) { float u = win(s, 0.6, 1.1); q = mix(vec2(298.0, 41.1), vec2(116.0, 88.1), ease(u)); k = 1.15 * sm(0.0, 0.15, u); } else if (s >= 1.1 && s < 6.6) { q = vec2(116.0, 88.1); k = 1.15; } else if (s >= 6.6 && s < 7.2) { float u = win(s, 6.6, 7.2); q = u < 0.55 ? mix(vec2(116.0, 88.1), vec2(116.0, 88.1), ease(u / 0.55)) : mix(vec2(116.0, 88.1), vec2(116.0, 163.3), ease((u - 0.55) / 0.45)); k = 1.15 * (1.0 - sm(0.8, 1.0, u)); } v += blob(p, q, 7.0, k); } { vec2 q = vec2(298.0, 41.1); float k = 0.0; if (s > 1.8 && s < 2.2) { float u = win(s, 1.8, 2.2); q = mix(vec2(298.0, 41.1), vec2(144.0, 88.1), ease(u)); k = 1.15 * sm(0.0, 0.15, u); } else if (s >= 2.2 && s < 6.8) { q = vec2(144.0, 88.1); k = 1.15; } else if (s >= 6.8 && s < 7.3) { float u = win(s, 6.8, 7.3); q = u < 0.55 ? mix(vec2(144.0, 88.1), vec2(116.0, 88.1), ease(u / 0.55)) : mix(vec2(116.0, 88.1), vec2(116.0, 163.3), ease((u - 0.55) / 0.45)); k = 1.15 * (1.0 - sm(0.8, 1.0, u)); } v += blob(p, q, 7.0, k); } { vec2 q = vec2(298.0, 41.1); float k = 0.0; if (s > 2.9 && s < 3.4) { float u = win(s, 2.9, 3.4); q = mix(vec2(298.0, 41.1), vec2(172.0, 88.1), ease(u)); k = 1.15 * sm(0.0, 0.15, u); } else if (s >= 3.4 && s < 6.9) { q = vec2(172.0, 88.1); k = 1.15; } else if (s >= 6.9 && s < 7.5) { float u = win(s, 6.9, 7.5); q = u < 0.55 ? mix(vec2(172.0, 88.1), vec2(116.0, 88.1), ease(u / 0.55)) : mix(vec2(116.0, 88.1), vec2(116.0, 163.3), ease((u - 0.55) / 0.45)); k = 1.15 * (1.0 - sm(0.8, 1.0, u)); } v += blob(p, q, 7.0, k); } { vec2 q = vec2(298.0, 41.1); float k = 0.0; if (s > 4.0 && s < 4.5) { float u = win(s, 4.0, 4.5); q = mix(vec2(298.0, 41.1), vec2(200.0, 88.1), ease(u)); k = 1.15 * sm(0.0, 0.15, u); } else if (s >= 4.5 && s < 7.0) { q = vec2(200.0, 88.1); k = 1.15; } else if (s >= 7.0 && s < 7.6) { float u = win(s, 7.0, 7.6); q = u < 0.55 ? mix(vec2(200.0, 88.1), vec2(116.0, 88.1), ease(u / 0.55)) : mix(vec2(116.0, 88.1), vec2(116.0, 163.3), ease((u - 0.55) / 0.45)); k = 1.15 * (1.0 - sm(0.8, 1.0, u)); } v += blob(p, q, 7.0, k); } { vec2 q = vec2(298.0, 41.1); float k = 0.0; if (s > 5.2 && s < 5.7) { float u = win(s, 5.2, 5.7); q = mix(vec2(298.0, 41.1), vec2(228.0, 88.1), ease(u)); k = 1.15 * sm(0.0, 0.15, u); } else if (s >= 5.7 && s < 7.2) { q = vec2(228.0, 88.1); k = 1.15; } else if (s >= 7.2 && s < 7.8) { float u = win(s, 7.2, 7.8); q = u < 0.55 ? mix(vec2(228.0, 88.1), vec2(116.0, 88.1), ease(u / 0.55)) : mix(vec2(116.0, 88.1), vec2(116.0, 163.3), ease((u - 0.55) / 0.45)); k = 1.15 * (1.0 - sm(0.8, 1.0, u)); } v += blob(p, q, 7.0, k); } // A's probe: it reaches the shell, stops, comes back and fades { float x = s < 2.7 ? mix(30.0, 77.0, ease(win(s, 2.2, 2.7))) : mix(77.0, 57.0, 1.0 - pow(1.0 - win(s, 2.7, 2.95), 2.0)); float k = sm(2.2, 2.3, s) * (1.0 - sm(2.95, 3.2, s)); v += blob(p, vec2(x, 200.9), 7.0, 1.15 * k); v += 0.6 * exp(-pow(s - 2.72, 2.0) / 0.004) * cells(p, 12.0, 12.0, 19.0, 23.0); } // A's ruler: an axis, faint weeks, the one release of the year, and the cursor v += 0.42 * cells(p, 12.0, 40.0, 41.0, 41.0); v += 0.2 * step(abs(mod(col - 12.0, 2.0)), 0.5) * cells(p, 12.0, 40.0, 40.0, 40.0); v += (0.42 + 0.68 * sm(6.6, 6.7, s)) * cells(p, 35.0, 35.0, 37.0, 40.0); { float x = 88.0 + s / T * 196.0; float cx = 4.0 + 7.0 * floor((x - 4.0) / 7.0 + 0.5); v += 1.1 * sm(0.0, 0.2, s) * (1.0 - sm(7.8, 8.0, s)) * step(abs(p.x - cx), 3.5) * step(347.3, p.y) * step(p.y, 392.9); } // ---- B: the in-house stack, built fast, each module open to read (its lines of code) float reset = 1.0 - sm(7.6, 7.95, s); { float fm = sm(0.1, 0.1, s) * reset; float fl = sm(0.1, 0.1, s) * (1.0 - sm(0.1, 0.5, s)); float wk = 0.0; float rim = cells(p, 60.0, 72.0, 25.0, 30.0) - cells(p, 61.0, 71.0, 26.0, 29.0); float ln = cells(p, 62.0, 70.0, 26.0, 29.0) * step(col - 62.0, 2.0 + floor(hash(vec2(cel, 0.0)) * 8.0)) * step(0.5, hash(vec2(cel, col * 0.0 + 3.0)) + 0.4); v += fm * (rim * (0.62 + 0.4 * wk) + ln * (0.42 + 0.35 * wk + 0.5 * fl)); } { float fm = sm(0.1, 0.2, s) * reset; float fl = sm(0.1, 0.2, s) * (1.0 - sm(0.2, 0.5, s)); float wk = sm(3.4, 3.4, s) * (1.0 - sm(3.5, 3.8, s)); float rim = cells(p, 75.0, 92.0, 25.0, 30.0) - cells(p, 76.0, 91.0, 26.0, 29.0); float ln = cells(p, 77.0, 90.0, 26.0, 29.0) * step(col - 77.0, 2.0 + floor(hash(vec2(cel, 1.0)) * 13.0)) * step(0.5, hash(vec2(cel, col * 0.0 + 4.0)) + 0.4); v += fm * (rim * (0.62 + 0.4 * wk) + ln * (0.42 + 0.35 * wk + 0.5 * fl)); } { float fm = sm(0.2, 0.3, s) * reset; float fl = sm(0.2, 0.3, s) * (1.0 - sm(0.3, 0.6, s)); float wk = sm(5.7, 5.7, s) * (1.0 - sm(5.8, 6.1, s)); float rim = cells(p, 62.0, 78.0, 18.0, 22.0) - cells(p, 63.0, 77.0, 19.0, 21.0); float ln = cells(p, 64.0, 76.0, 19.0, 21.0) * step(col - 64.0, 2.0 + floor(hash(vec2(cel, 2.0)) * 12.0)) * step(0.5, hash(vec2(cel, col * 0.0 + 5.0)) + 0.4); v += fm * (rim * (0.62 + 0.4 * wk) + ln * (0.42 + 0.35 * wk + 0.5 * fl)); } { float fm = sm(0.3, 0.3, s) * reset; float fl = sm(0.3, 0.3, s) * (1.0 - sm(0.3, 0.7, s)); float wk = sm(1.1, 1.1, s) * (1.0 - sm(1.2, 1.5, s)); float rim = cells(p, 81.0, 90.0, 18.0, 22.0) - cells(p, 82.0, 89.0, 19.0, 21.0); float ln = cells(p, 83.0, 88.0, 19.0, 21.0) * step(col - 83.0, 2.0 + floor(hash(vec2(cel, 3.0)) * 5.0)) * step(0.5, hash(vec2(cel, col * 0.0 + 6.0)) + 0.4); v += fm * (rim * (0.62 + 0.4 * wk) + ln * (0.42 + 0.35 * wk + 0.5 * fl)); } { float fm = sm(0.3, 0.4, s) * reset; float fl = sm(0.3, 0.4, s) * (1.0 - sm(0.4, 0.7, s)); float wk = sm(2.2, 2.3, s) * (1.0 - sm(2.4, 2.7, s)); float rim = cells(p, 60.0, 68.0, 11.0, 15.0) - cells(p, 61.0, 67.0, 12.0, 14.0); float ln = cells(p, 62.0, 66.0, 12.0, 14.0) * step(col - 62.0, 2.0 + floor(hash(vec2(cel, 4.0)) * 4.0)) * step(0.5, hash(vec2(cel, col * 0.0 + 7.0)) + 0.4); v += fm * (rim * (0.62 + 0.4 * wk) + ln * (0.42 + 0.35 * wk + 0.5 * fl)); } { float fm = sm(0.4, 0.5, s) * reset; float fl = sm(0.4, 0.5, s) * (1.0 - sm(0.5, 0.8, s)); float wk = sm(4.5, 4.5, s) * (1.0 - sm(4.7, 5.0, s)); float rim = cells(p, 71.0, 86.0, 11.0, 15.0) - cells(p, 72.0, 85.0, 12.0, 14.0); float ln = cells(p, 73.0, 84.0, 12.0, 14.0) * step(col - 73.0, 2.0 + floor(hash(vec2(cel, 5.0)) * 11.0)) * step(0.5, hash(vec2(cel, col * 0.0 + 8.0)) + 0.4); v += fm * (rim * (0.62 + 0.4 * wk) + ln * (0.42 + 0.35 * wk + 0.5 * fl)); } // B's joints: steady until a change breaks one (it flickers out), then a patch grows over it and stays the year { float fm = sm(0.1, 0.4, s) * reset; float br = sm(1.3, 1.3, s) * (1.0 - sm(1.6, 1.6, s)); float fx = sm(1.6, 1.8, s); float flick = 0.5 + 0.5 * sin(s * 90.0); v += fm * (0.7 * (1.0 - br) + br * 0.9 * flick) * cells(p, 66.0, 66.0, 23.0, 24.0); v += fm * fx * blob(p, vec2(466.0, 224.4), 9.0, 1.05); } { float fm = sm(0.1, 0.4, s) * reset; float br = sm(4.7, 4.7, s) * (1.0 - sm(5.0, 5.0, s)); float fx = sm(5.0, 5.2, s); float flick = 0.5 + 0.5 * sin(s * 90.0); v += fm * (0.7 * (1.0 - br) + br * 0.9 * flick) * cells(p, 76.0, 76.0, 23.0, 24.0); v += fm * fx * blob(p, vec2(536.0, 224.4), 9.0, 1.05); } { float fm = sm(0.1, 0.4, s) * reset; float br = sm(2.4, 2.4, s) * (1.0 - sm(2.7, 2.8, s)); float fx = sm(2.7, 2.9, s); float flick = 0.5 + 0.5 * sin(s * 90.0); v += fm * (0.7 * (1.0 - br) + br * 0.9 * flick) * cells(p, 86.0, 86.0, 23.0, 24.0); v += fm * fx * blob(p, vec2(606.0, 224.4), 9.0, 1.05); } { float fm = sm(0.1, 0.4, s) * reset; float br = sm(3.6, 3.6, s) * (1.0 - sm(3.9, 3.9, s)); float fx = sm(3.9, 4.1, s); float flick = 0.5 + 0.5 * sin(s * 90.0); v += fm * (0.7 * (1.0 - br) + br * 0.9 * flick) * cells(p, 64.0, 64.0, 16.0, 17.0); v += fm * fx * blob(p, vec2(452.0, 158.6), 9.0, 1.05); } { float fm = sm(0.1, 0.4, s) * reset; float br = 0.0; float fx = 0.0; float flick = 0.5 + 0.5 * sin(s * 90.0); v += fm * (0.7 * (1.0 - br) + br * 0.9 * flick) * cells(p, 74.0, 74.0, 16.0, 17.0); v += fm * fx * blob(p, vec2(522.0, 158.6), 9.0, 1.05); } { float fm = sm(0.1, 0.4, s) * reset; float br = sm(5.9, 5.9, s) * (1.0 - sm(6.2, 6.2, s)); float fx = sm(6.2, 6.4, s); float flick = 0.5 + 0.5 * sin(s * 90.0); v += fm * (0.7 * (1.0 - br) + br * 0.9 * flick) * cells(p, 83.0, 83.0, 16.0, 17.0); v += fm * fx * blob(p, vec2(585.0, 158.6), 9.0, 1.05); } // B's requests, dropping into the module they change { float u = win(s, 0.6, 1.1); if (s > 0.6 && s < 1.1) v += blob(p, mix(vec2(543.0, 41.1), vec2(602.5, 191.5), ease(u)), 7.0, 1.15 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); } { float u = win(s, 1.8, 2.2); if (s > 1.8 && s < 2.2) v += blob(p, mix(vec2(543.0, 41.1), vec2(452.0, 125.7), ease(u)), 7.0, 1.15 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); } { float u = win(s, 2.9, 3.4); if (s > 2.9 && s < 3.4) v += blob(p, mix(vec2(543.0, 41.1), vec2(588.5, 262.0), ease(u)), 7.0, 1.15 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); } { float u = win(s, 4.0, 4.5); if (s > 4.0 && s < 4.5) v += blob(p, mix(vec2(543.0, 41.1), vec2(553.5, 125.7), ease(u)), 7.0, 1.15 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); } { float u = win(s, 5.2, 5.7); if (s > 5.2 && s < 5.7) v += blob(p, mix(vec2(543.0, 41.1), vec2(494.0, 191.5), ease(u)), 7.0, 1.15 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); } // B's ruler: version one's small bar, then a cost bar per change, rising while its fix is made v += 0.42 * cells(p, 58.0, 92.0, 41.0, 41.0); v += 0.2 * step(abs(mod(col - 58.0, 2.0)), 0.5) * cells(p, 58.0, 92.0, 40.0, 40.0); { float c = 60.0; v += sm(0.4, 0.5, s) * reset * 0.9 * cells(p, c, c + 1.0, 40.0, 40.0); } { float h = 2.0 * win(s, 1.3, 1.6) * reset; float top = 41.0 - ceil(h - 0.01); if (h > 0.0) v += cells(p, 62.0, 63.0, top, 40.0) * (0.62 + 0.45 * step(cel, top + 0.5)); } { float h = 3.0 * win(s, 2.4, 2.8) * reset; float top = 41.0 - ceil(h - 0.01); if (h > 0.0) v += cells(p, 67.0, 68.0, top, 40.0) * (0.62 + 0.45 * step(cel, top + 0.5)); } { float h = 4.0 * win(s, 3.6, 4.0) * reset; float top = 41.0 - ceil(h - 0.01); if (h > 0.0) v += cells(p, 72.0, 73.0, top, 40.0) * (0.62 + 0.45 * step(cel, top + 0.5)); } { float h = 5.0 * win(s, 4.7, 5.1) * reset; float top = 41.0 - ceil(h - 0.01); if (h > 0.0) v += cells(p, 77.0, 78.0, top, 40.0) * (0.62 + 0.45 * step(cel, top + 0.5)); } { float h = 7.0 * win(s, 5.9, 6.3) * reset; float top = 41.0 - ceil(h - 0.01); if (h > 0.0) v += cells(p, 82.0, 83.0, top, 40.0) * (0.62 + 0.45 * step(cel, top + 0.5)); } { float x = 410.0 + s / T * 238.0; float cx = 4.0 + 7.0 * floor((x - 4.0) / 7.0 + 0.5); v += 1.1 * sm(0.0, 0.2, s) * (1.0 - sm(7.8, 8.0, s)) * step(abs(p.x - cx), 3.5) * step(347.3, p.y) * step(p.y, 392.9); } return v; }

A

The sealed vendor

Every request joins a queue for the yearly release. You can't see inside, so nobody can say why a rule fired.

B

The homemade stack

The first version goes out in a month. Then each change breaks something, and each fix takes longer than the last.

A third way

Treat your sales data like the rest of your code

Objects, fields, stages and rules sit in plain files next to your app.

01Reviewed like any change

A payroll startup moved its deal stages into the repo. Sales asks in an issue, an engineer opens a pull request, and the head of sales approves it.

Engineering lead, payroll startup

02Tested before sales sees it

A robotics leasing company runs every pipeline change on a laptop against last month's records before it goes live.

Platform engineer, robotics leasing

03One release calendar

A travel booking startup ships CRM changes in the same deploy as product changes, so there is one schedule to plan around, not two.

CTO, travel booking startup

How it works

From a model file to a working pipeline

01

Start from three kinds of blocks

A data model, a layout and an automation. Put them together for your CRM, or for a small internal app such as deal approvals.

Building blocks A toolkit of three blocks, a data model, a layout and an automation, composes two apps: copies go into a CRM window and an approvals window, each becomes its part, and each app saves a record and runs its rule. float band(float d, float a, float b) { return 1.0 - sm(a, b, d); } float rect(vec2 p, vec2 a, vec2 b) { return step(a.x, p.x) * step(p.x, b.x) * step(a.y, p.y) * step(p.y, b.y); } float win(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } // a soft link from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b) { vec2 ab = b - a; float h = clamp(dot(p - a, ab) / max(dot(ab, ab), 0.001), 0.0, 1.0); vec2 d = (p - a - ab * h) / vec2(3.6, 4.8); return 1.0 - sm(0.8, 1.05, length(d)); } // whole cells: a block of columns c0..c1 and cells k0..k1 (inclusive), in grid units float cells(vec2 p, float c0, float c1, float k0, float k1) { return rect(p, vec2(0.5 + 7.0 * c0, 9.4 * k0), vec2(7.5 + 7.0 * c1, 9.4 * k1 + 7.0)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; float s = mod(t + 6.5, T); float ph = TAU * t / T; float keep = 1.0 - sm(7.3, 7.7, s); float v = 0.0; float col = floor((p.x - 4.0) / 7.0 + 0.5); float cel = floor((p.y - 3.5) / 9.4 + 0.5); float grain = 0.1 * sin(ph + 6.2832 * hash(vec2(col, cel))); // ---- the toolkit. Each block glows while a copy leaves it. { float gdm = (sm(0.7, 0.8, s) * (1.0 - sm(0.8, 1.1, s))) + (sm(3.7, 3.8, s) * (1.0 - sm(3.9, 4.2, s))); float gly = (sm(0.3, 0.3, s) * (1.0 - sm(0.5, 0.8, s))) + (sm(3.3, 3.3, s) * (1.0 - sm(3.5, 3.8, s))); float gau = (sm(1.1, 1.2, s) * (1.0 - sm(1.3, 1.6, s))) + (sm(4.1, 4.1, s) * (1.0 - sm(4.3, 4.5, s))); // data model: a header and three field rows, each with its key mark v += (1.05 + 0.2 * gdm) * cells(p, 5.0, 25.0, 6.0, 6.0) + 0.62 * (cells(p, 5.0, 5.0, 7.0, 13.0) + cells(p, 25.0, 25.0, 7.0, 13.0) + cells(p, 6.0, 24.0, 13.0, 13.0)); v += (0.75 + 0.3 * gdm) * (cells(p, 7.0, 7.0, 8.0, 8.0) + cells(p, 7.0, 7.0, 10.0, 10.0) + cells(p, 7.0, 7.0, 12.0, 12.0)); v += (0.42 + 0.3 * gdm + grain) * (cells(p, 9.0, 20.0, 8.0, 8.0) + cells(p, 9.0, 23.0, 10.0, 10.0) + cells(p, 9.0, 17.0, 12.0, 12.0)); // layout: a page with its top bar, a sidebar and three panels v += 0.62 * (cells(p, 5.0, 25.0, 17.0, 17.0) + cells(p, 5.0, 25.0, 25.0, 25.0) + cells(p, 5.0, 5.0, 18.0, 24.0) + cells(p, 25.0, 25.0, 18.0, 24.0)); v += (0.75 + 0.3 * gly) * cells(p, 6.0, 24.0, 18.0, 18.0) + (0.7 + 0.3 * gly) * cells(p, 6.0, 8.0, 19.0, 24.0); v += (0.42 + 0.3 * gly + grain) * (cells(p, 10.0, 16.0, 20.0, 24.0) + cells(p, 18.0, 24.0, 20.0, 21.0) + cells(p, 18.0, 24.0, 23.0, 24.0)); // automation: a trigger, a link, a condition, a link, an action v += 0.42 * (cells(p, 5.0, 25.0, 28.0, 28.0) + cells(p, 5.0, 25.0, 36.0, 36.0) + cells(p, 5.0, 5.0, 29.0, 35.0) + cells(p, 25.0, 25.0, 29.0, 35.0)); v += blob(p, vec2(67.0, 304.3), 8.0, 1.0 + 0.2 * gau); v += (0.62 + 0.3 * gau) * (cells(p, 11.0, 14.0, 32.0, 32.0) + cells(p, 19.0, 20.0, 32.0, 32.0)); v += (0.85 + 0.3 * gau) * (cells(p, 16.0, 16.0, 31.0, 31.0) + cells(p, 15.0, 17.0, 32.0, 32.0) + cells(p, 16.0, 16.0, 33.0, 33.0)); v += (0.85 + 0.3 * gau) * cells(p, 22.0, 24.0, 31.0, 33.0); } // ---- the copies in flight, block to app part { float u = win(s, 0.3, 0.8); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(109.0, 200.9), vec2(291.0, 125.7), ease(u)), 8.0, 1.15 * sm(0.0, 0.1, u)); } { float u = win(s, 0.7, 1.2); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(109.0, 88.1), vec2(445.0, 125.7), ease(u)), 8.0, 1.15 * sm(0.0, 0.1, u)); } { float u = win(s, 1.1, 1.6); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(109.0, 304.3), vec2(620.0, 135.1), ease(u)), 8.0, 1.15 * sm(0.0, 0.1, u)); } { float u = win(s, 3.3, 3.8); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(109.0, 200.9), vec2(473.0, 266.7), ease(u)), 8.0, 1.15 * sm(0.0, 0.1, u)); } { float u = win(s, 3.7, 4.2); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(109.0, 88.1), vec2(424.0, 341.9), ease(u)), 8.0, 1.15 * sm(0.0, 0.1, u)); } { float u = win(s, 4.1, 4.6); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(109.0, 304.3), vec2(620.0, 341.9), ease(u)), 8.0, 1.15 * sm(0.0, 0.1, u)); } // ---- the windows: always there, empty until their parts land v += 0.62 * (cells(p, 37.0, 97.0, 3.0, 3.0) + cells(p, 37.0, 97.0, 21.0, 21.0) + cells(p, 37.0, 37.0, 4.0, 20.0) + cells(p, 97.0, 97.0, 4.0, 20.0)); v += 0.95 * (cells(p, 39.0, 39.0, 4.0, 4.0) + cells(p, 41.0, 41.0, 4.0, 4.0) + cells(p, 43.0, 43.0, 4.0, 4.0)) + 0.2 * cells(p, 58.0, 80.0, 4.0, 4.0); v += 0.62 * (cells(p, 37.0, 97.0, 25.0, 25.0) + cells(p, 37.0, 97.0, 44.0, 44.0) + cells(p, 37.0, 37.0, 26.0, 43.0) + cells(p, 97.0, 97.0, 26.0, 43.0)); v += 0.95 * (cells(p, 39.0, 39.0, 26.0, 26.0) + cells(p, 41.0, 41.0, 26.0, 26.0) + cells(p, 43.0, 43.0, 26.0, 26.0)) + 0.2 * cells(p, 58.0, 80.0, 26.0, 26.0); // ---- the CRM: a sidebar with its menu (layout), a record table (data model), its rule (automation) { float L = (step(0.8, s) * keep), Lf = (sm(0.8, 0.9, s) * (1.0 - sm(0.9, 1.3, s))); v += L * ((0.7 + 0.4 * Lf) * cells(p, 38.0, 43.0, 6.0, 20.0) + (0.3 + 0.5 * Lf) * cells(p, 46.0, 96.0, 6.0, 6.0)); v += L * 0.4 * (cells(p, 39.0, 42.0, 9.0, 9.0) + cells(p, 39.0, 41.0, 11.0, 11.0) + cells(p, 39.0, 42.0, 13.0, 13.0)); float D = (step(1.2, s) * keep), Df = (sm(1.2, 1.3, s) * (1.0 - sm(1.3, 1.7, s))); v += D * (1.05 + 0.2 * Df) * cells(p, 46.0, 80.0, 8.0, 8.0); for (int i = 0; i < 4; i++) { float k = 10.0 + 2.0 * float(i); float row = cells(p, 46.0, 80.0, k, k); float c = step(col, 46.0 + 5.0 + mod(float(i) * 3.0, 4.0)) * 0.42 + step(57.0, col) * step(col, 63.0 - float(i)) * 0.42 + step(66.0, col) * step(col, 70.0) * 0.7 + step(73.0, col) * step(col, 78.0) * 0.62; v += D * row * c * (1.0 + 0.6 * Df); } // the new record, saved when the app runs float N = (step(1.9, s) * keep), Nf = (sm(1.9, 1.9, s) * (1.0 - sm(1.9, 2.4, s))); v += N * cells(p, 46.0, 80.0, 18.0, 18.0) * (step(col, 52.0) * 0.42 + step(57.0, col) * step(col, 61.0) * 0.42 + step(66.0, col) * step(col, 70.0) * 0.7 + step(73.0, col) * step(col, 78.0) * 0.62 + 0.6 * Nf); // the rule: a trigger, a condition, an action, joined; each lights only when the step before it has passed { float on = ((step(1.6, s) * keep)); // a live rule's trigger breathes while it listens float br = (step(3.1, s) * keep) * 0.3 * sin(2.0 * ph); v += on * (0.95 + br + 0.4 * (sm(2.2, 2.2, s) * (1.0 - sm(2.4, 2.6, s))) + 0.3 * (sm(1.6, 1.7, s) * (1.0 - sm(1.7, 2.1, s)))) * blob(p, vec2(606.0, 97.5), 7.5 + 2.0 * br, 1.0); v += on * 0.62 * cells(p, 86.0, 86.0, 11.0, 12.0); v += on * (0.85 + 0.4 * (sm(2.5, 2.5, s) * (1.0 - sm(2.6, 2.9, s)))) * (cells(p, 86.0, 86.0, 13.0, 13.0) + cells(p, 85.0, 87.0, 14.0, 14.0) + cells(p, 86.0, 86.0, 15.0, 15.0)); v += on * 0.62 * cells(p, 86.0, 86.0, 16.0, 17.0); v += on * (0.85 + 0.45 * (sm(2.8, 2.8, s) * (1.0 - sm(3.1, 3.4, s)))) * cells(p, 85.0, 87.0, 18.0, 19.0); // the run passing down the rail { float u = win(s, 2.2, 2.5); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(606.0, 97.5), vec2(606.0, 135.1), ease(u)), 6.5, 1.1); } { float u = win(s, 2.5, 2.8); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(606.0, 135.1), vec2(606.0, 177.4), ease(u)), 6.5, 1.1); } } } // ---- approvals: a top bar with tabs (layout), request cards with a status (data model), its rule (automation) { float L = (step(3.8, s) * keep), Lf = (sm(3.8, 3.8, s) * (1.0 - sm(3.8, 4.3, s))); v += L * ((0.42 + 0.4 * Lf) * cells(p, 38.0, 96.0, 28.0, 28.0) + (0.8 + 0.3 * Lf) * (cells(p, 40.0, 45.0, 28.0, 28.0))); v += L * 0.2 * (cells(p, 47.0, 52.0, 28.0, 28.0) + cells(p, 54.0, 58.0, 28.0, 28.0)); float D = (step(4.2, s) * keep), Df = (sm(4.2, 4.3, s) * (1.0 - sm(4.3, 4.7, s))); float Nf = (sm(4.9, 5.0, s) * (1.0 - sm(5.0, 5.4, s))), N = (step(4.9, s) * keep); for (int i = 0; i < 3; i++) { float c0 = 40.0 + 14.0 * float(i); float card = cells(p, c0, c0 + 12.0, 31.0, 41.0) - cells(p, c0 + 1.0, c0 + 11.0, 32.0, 40.0); float title = cells(p, c0 + 2.0, c0 + 8.0, 33.0, 33.0); float lines = cells(p, c0 + 2.0, c0 + 10.0, 35.0, 35.0) + cells(p, c0 + 2.0, c0 + 7.0, 37.0, 37.0); float st = cells(p, c0 + 9.0, c0 + 10.0, 39.0, 39.0); float ok = i == 2 ? N : step(float(i), 0.5); v += D * (1.0 + 0.6 * Df) * (0.42 * card + 0.7 * title + 0.42 * lines + (0.42 + 0.63 * ok + (i == 2 ? 0.5 * Nf : 0.0)) * st); } // the rule: a trigger, a condition, an action, joined; each lights only when the step before it has passed { float on = ((step(4.6, s) * keep)); // a live rule's trigger breathes while it listens float br = (step(6.1, s) * keep) * 0.3 * sin(2.0 * ph); v += on * (0.95 + br + 0.4 * (sm(5.2, 5.3, s) * (1.0 - sm(5.4, 5.7, s))) + 0.3 * (sm(4.6, 4.6, s) * (1.0 - sm(4.6, 5.1, s)))) * blob(p, vec2(606.0, 294.9), 7.5 + 2.0 * br, 1.0); v += on * 0.62 * cells(p, 86.0, 86.0, 32.0, 33.0); v += on * (0.85 + 0.4 * (sm(5.5, 5.5, s) * (1.0 - sm(5.7, 6.0, s)))) * (cells(p, 86.0, 86.0, 34.0, 34.0) + cells(p, 85.0, 87.0, 35.0, 35.0) + cells(p, 86.0, 86.0, 36.0, 36.0)); v += on * 0.62 * cells(p, 86.0, 86.0, 37.0, 38.0); v += on * (0.85 + 0.45 * (sm(5.8, 5.9, s) * (1.0 - sm(6.1, 6.4, s)))) * cells(p, 85.0, 87.0, 39.0, 40.0); // the run passing down the rail { float u = win(s, 5.2, 5.5); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(606.0, 294.9), vec2(606.0, 332.5), ease(u)), 6.5, 1.1); } { float u = win(s, 5.5, 5.8); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(606.0, 332.5), vec2(606.0, 374.8), ease(u)), 6.5, 1.1); } } } return v; }

02

Ask for a change, get a pull request

Tell your coding tool you need a renewals stage. It edits the model, runs the tests and deploys. The board gets a new column, and the next won deal can start a renewal.

Keep iterating A coding tool takes a request, adds lines to a file, runs three tests and deploys; the running CRM's pipeline board gains a renewal column and the first renewal is made from a won deal. float band(float d, float a, float b) { return 1.0 - sm(a, b, d); } float rect(vec2 p, vec2 a, vec2 b) { return step(a.x, p.x) * step(p.x, b.x) * step(a.y, p.y) * step(p.y, b.y); } float win(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } // a soft link from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b) { vec2 ab = b - a; float h = clamp(dot(p - a, ab) / max(dot(ab, ab), 0.001), 0.0, 1.0); vec2 d = (p - a - ab * h) / vec2(3.6, 4.8); return 1.0 - sm(0.8, 1.05, length(d)); } // whole cells: a block of columns c0..c1 and cells k0..k1 (inclusive), in grid units float cells(vec2 p, float c0, float c1, float k0, float k1) { return rect(p, vec2(0.5 + 7.0 * c0, 9.4 * k0), vec2(7.5 + 7.0 * c1, 9.4 * k1 + 7.0)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; float s = mod(t + 6.0, T); float ph = TAU * t / T; float keep = 1.0 - sm(7.3, 7.7, s); float v = 0.0; float col = floor((p.x - 4.0) / 7.0 + 0.5); float cel = floor((p.y - 3.5) / 9.4 + 0.5); float grain = 0.1 * sin(ph + 6.2832 * hash(vec2(col, cel))); // ---- the coding tool (its window is the box): its title bar dots, the prompt, the file, the tests, the deploy, the run strip v += 0.95 * (cells(p, 6.0, 6.0, 5.0, 5.0) + cells(p, 8.0, 8.0, 5.0, 5.0) + cells(p, 10.0, 10.0, 5.0, 5.0)); v += 0.75 * cells(p, 6.0, 6.0, 8.0, 8.0); // the caret: at the prompt before typing, then waiting at the next prompt once the change is out { float bl = step(0.5, fract(s * 2.0)); v += 1.05 * bl * (step(s, 0.2) + step(7.7, s)) * cells(p, 8.0, 8.0, 8.0, 8.0); v += 1.05 * bl * step(3.7, s) * step(s, 7.7) * (cells(p, 8.0, 8.0, 34.0, 34.0)) + 0.75 * step(3.7, s) * step(s, 7.3) * cells(p, 6.0, 6.0, 34.0, 34.0); } // the file: thirteen lines with their numbers; three are added by the tool, one after another { float op_ = (step(1.0, s) * keep), of = (sm(1.0, 1.1, s) * (1.0 - sm(1.1, 1.5, s))); v += op_ * (0.95 + 0.2 * of) * cells(p, 6.0, 6.0, 11.0, 11.0); if (cel >= 13.0 && cel <= 25.0 && col >= 5.0 && col <= 33.0) { float ind = 8.0 + 2.0 * floor(hash(vec2(cel, 1.3)) * 3.0); float len = ind + 4.0 + floor(hash(vec2(cel, 7.7)) * 14.0); float txt = step(ind, col) * step(col, len) * step(0.18, hash(vec2(col * 0.37, cel))); float num = step(abs(col - 5.0), 0.5); if (abs(cel - 17.0) < 0.5) { float a = (step(1.3, s) * keep), af = (sm(1.3, 1.4, s) * (1.0 - sm(1.4, 1.8, s))); v += a * ((0.7 + 0.5 * af) * txt + 1.05 * step(abs(col - 6.0), 0.5) + 0.2 * num); } if (abs(cel - 18.0) < 0.5) { float a = (step(1.4, s) * keep), af = (sm(1.4, 1.5, s) * (1.0 - sm(1.5, 1.9, s))); v += a * ((0.7 + 0.5 * af) * txt + 1.05 * step(abs(col - 6.0), 0.5) + 0.2 * num); } if (abs(cel - 19.0) < 0.5) { float a = (step(1.6, s) * keep), af = (sm(1.6, 1.7, s) * (1.0 - sm(1.7, 2.1, s))); v += a * ((0.7 + 0.5 * af) * txt + 1.05 * step(abs(col - 6.0), 0.5) + 0.2 * num); } if (abs(cel - 17.0) > 0.5 && abs(cel - 18.0) > 0.5 && abs(cel - 19.0) > 0.5) v += op_ * ((0.42 + 0.3 * of + grain) * txt + 0.2 * num); } } // the tests, one after another, then the deploy bar filling v += (0.42 + 0.63 * (step(2.0, s) * keep) + 0.4 * (sm(2.0, 2.0, s) * (1.0 - sm(2.0, 2.5, s)))) * cells(p, 13.0, 13.0, 28.0, 28.0); v += (0.42 + 0.63 * (step(2.3, s) * keep) + 0.4 * (sm(2.3, 2.3, s) * (1.0 - sm(2.3, 2.8, s)))) * cells(p, 15.0, 15.0, 28.0, 28.0); v += (0.42 + 0.63 * (step(2.5, s) * keep) + 0.4 * (sm(2.5, 2.5, s) * (1.0 - sm(2.5, 3.0, s)))) * cells(p, 17.0, 17.0, 28.0, 28.0); v += 0.2 * cells(p, 12.0, 34.0, 31.0, 31.0); v += keep * step(2.7, s) * step(col, 12.0 + 22.0 * win(s, 2.7, 3.2)) * cells(p, 12.0, 34.0, 31.0, 31.0) * 0.9; // the run strip: a segment per step, filling as the step runs { float c0 = 5.0; v += 0.2 * cells(p, c0, c0 + 6.0, 38.0, 38.0) + keep * step(0.2, s) * step(col, c0 + 6.0 * win(s, 0.2, 0.9)) * cells(p, c0, c0 + 6.0, 38.0, 38.0) * (0.9 + 0.25 * (sm(0.9, 1.0, s) * (1.0 - sm(1.0, 1.4, s)))); } { float c0 = 13.0; v += 0.2 * cells(p, c0, c0 + 6.0, 38.0, 38.0) + keep * step(1.0, s) * step(col, c0 + 6.0 * win(s, 1.0, 1.8)) * cells(p, c0, c0 + 6.0, 38.0, 38.0) * (0.9 + 0.25 * (sm(1.8, 1.9, s) * (1.0 - sm(1.9, 2.3, s)))); } { float c0 = 21.0; v += 0.2 * cells(p, c0, c0 + 6.0, 38.0, 38.0) + keep * step(2.0, s) * step(col, c0 + 6.0 * win(s, 2.0, 2.6)) * cells(p, c0, c0 + 6.0, 38.0, 38.0) * (0.9 + 0.25 * (sm(2.6, 2.6, s) * (1.0 - sm(2.6, 3.1, s)))); } { float c0 = 29.0; v += 0.2 * cells(p, c0, c0 + 6.0, 38.0, 38.0) + keep * step(2.7, s) * step(col, c0 + 6.0 * win(s, 2.7, 3.6)) * cells(p, c0, c0 + 6.0, 38.0, 38.0) * (0.9 + 0.25 * (sm(3.6, 3.6, s) * (1.0 - sm(3.6, 4.1, s)))); } // the deploy on its way to the board { float u = win(s, 3.2, 3.6); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(242.0, 294.9), vec2(641.0, 69.3), ease(u)), 8.0, 1.15); } // ---- the CRM's pipeline board: a header per stage and its deals v += 1.05 * cells(p, 43.0, 51.0, 7.0, 7.0); v += (1.0 + grain) * (0.9 * cells(p, 43.0, 43.0, 10.0, 12.0) + 0.7 * cells(p, 44.0, 49.0, 10.0, 10.0) + 0.42 * cells(p, 44.0, 47.0, 11.0, 11.0) + 0.85 * cells(p, 49.0, 50.0, 11.0, 11.0) + 0.85 * cells(p, 44.0, 44.0, 12.0, 12.0) + 0.22 * cells(p, 46.0, 49.0, 12.0, 12.0)); v += (1.0 + grain) * (0.9 * cells(p, 43.0, 43.0, 15.0, 17.0) + 0.7 * cells(p, 44.0, 49.0, 15.0, 15.0) + 0.42 * cells(p, 44.0, 47.0, 16.0, 16.0) + 0.85 * cells(p, 49.0, 50.0, 16.0, 16.0) + 0.85 * cells(p, 44.0, 44.0, 17.0, 17.0) + 0.22 * cells(p, 46.0, 49.0, 17.0, 17.0)); v += (1.0 + grain) * (0.9 * cells(p, 43.0, 43.0, 20.0, 22.0) + 0.7 * cells(p, 44.0, 49.0, 20.0, 20.0) + 0.42 * cells(p, 44.0, 47.0, 21.0, 21.0) + 0.85 * cells(p, 49.0, 50.0, 21.0, 21.0) + 0.85 * cells(p, 44.0, 44.0, 22.0, 22.0) + 0.22 * cells(p, 46.0, 49.0, 22.0, 22.0)); v += (1.0 + grain) * (0.9 * cells(p, 43.0, 43.0, 25.0, 27.0) + 0.7 * cells(p, 44.0, 49.0, 25.0, 25.0) + 0.42 * cells(p, 44.0, 47.0, 26.0, 26.0) + 0.85 * cells(p, 49.0, 50.0, 26.0, 26.0) + 0.85 * cells(p, 44.0, 44.0, 27.0, 27.0) + 0.22 * cells(p, 46.0, 49.0, 27.0, 27.0)); v += (1.0 + grain) * (0.9 * cells(p, 43.0, 43.0, 30.0, 32.0) + 0.7 * cells(p, 44.0, 49.0, 30.0, 30.0) + 0.42 * cells(p, 44.0, 47.0, 31.0, 31.0) + 0.85 * cells(p, 49.0, 50.0, 31.0, 31.0) + 0.85 * cells(p, 44.0, 44.0, 32.0, 32.0) + 0.22 * cells(p, 46.0, 49.0, 32.0, 32.0)); v += (1.0 + grain) * (0.9 * cells(p, 43.0, 43.0, 35.0, 37.0) + 0.7 * cells(p, 44.0, 49.0, 35.0, 35.0) + 0.42 * cells(p, 44.0, 47.0, 36.0, 36.0) + 0.85 * cells(p, 49.0, 50.0, 36.0, 36.0) + 0.85 * cells(p, 44.0, 44.0, 37.0, 37.0) + 0.22 * cells(p, 46.0, 49.0, 37.0, 37.0)); v += 1.05 * cells(p, 54.0, 62.0, 7.0, 7.0); v += (1.0 + grain) * (0.9 * cells(p, 54.0, 54.0, 10.0, 12.0) + 0.7 * cells(p, 55.0, 60.0, 10.0, 10.0) + 0.42 * cells(p, 55.0, 58.0, 11.0, 11.0) + 0.85 * cells(p, 60.0, 61.0, 11.0, 11.0) + 0.85 * cells(p, 55.0, 55.0, 12.0, 12.0) + 0.22 * cells(p, 57.0, 60.0, 12.0, 12.0)); v += (1.0 + grain) * (0.9 * cells(p, 54.0, 54.0, 15.0, 17.0) + 0.7 * cells(p, 55.0, 60.0, 15.0, 15.0) + 0.42 * cells(p, 55.0, 58.0, 16.0, 16.0) + 0.85 * cells(p, 60.0, 61.0, 16.0, 16.0) + 0.85 * cells(p, 55.0, 55.0, 17.0, 17.0) + 0.22 * cells(p, 57.0, 60.0, 17.0, 17.0)); v += (1.0 + grain) * (0.9 * cells(p, 54.0, 54.0, 20.0, 22.0) + 0.7 * cells(p, 55.0, 60.0, 20.0, 20.0) + 0.42 * cells(p, 55.0, 58.0, 21.0, 21.0) + 0.85 * cells(p, 60.0, 61.0, 21.0, 21.0) + 0.85 * cells(p, 55.0, 55.0, 22.0, 22.0) + 0.22 * cells(p, 57.0, 60.0, 22.0, 22.0)); v += (1.0 + grain) * (0.9 * cells(p, 54.0, 54.0, 25.0, 27.0) + 0.7 * cells(p, 55.0, 60.0, 25.0, 25.0) + 0.42 * cells(p, 55.0, 58.0, 26.0, 26.0) + 0.85 * cells(p, 60.0, 61.0, 26.0, 26.0) + 0.85 * cells(p, 55.0, 55.0, 27.0, 27.0) + 0.22 * cells(p, 57.0, 60.0, 27.0, 27.0)); v += (1.0 + grain) * (0.9 * cells(p, 54.0, 54.0, 30.0, 32.0) + 0.7 * cells(p, 55.0, 60.0, 30.0, 30.0) + 0.42 * cells(p, 55.0, 58.0, 31.0, 31.0) + 0.85 * cells(p, 60.0, 61.0, 31.0, 31.0) + 0.85 * cells(p, 55.0, 55.0, 32.0, 32.0) + 0.22 * cells(p, 57.0, 60.0, 32.0, 32.0)); v += 1.05 * cells(p, 65.0, 73.0, 7.0, 7.0); v += (1.0 + grain) * (0.9 * cells(p, 65.0, 65.0, 10.0, 12.0) + 0.7 * cells(p, 66.0, 71.0, 10.0, 10.0) + 0.42 * cells(p, 66.0, 69.0, 11.0, 11.0) + 0.85 * cells(p, 71.0, 72.0, 11.0, 11.0) + 0.85 * cells(p, 66.0, 66.0, 12.0, 12.0) + 0.22 * cells(p, 68.0, 71.0, 12.0, 12.0)); v += (1.0 + grain) * (0.9 * cells(p, 65.0, 65.0, 15.0, 17.0) + 0.7 * cells(p, 66.0, 71.0, 15.0, 15.0) + 0.42 * cells(p, 66.0, 69.0, 16.0, 16.0) + 0.85 * cells(p, 71.0, 72.0, 16.0, 16.0) + 0.85 * cells(p, 66.0, 66.0, 17.0, 17.0) + 0.22 * cells(p, 68.0, 71.0, 17.0, 17.0)); v += (1.0 + grain) * (0.9 * cells(p, 65.0, 65.0, 20.0, 22.0) + 0.7 * cells(p, 66.0, 71.0, 20.0, 20.0) + 0.42 * cells(p, 66.0, 69.0, 21.0, 21.0) + 0.85 * cells(p, 71.0, 72.0, 21.0, 21.0) + 0.85 * cells(p, 66.0, 66.0, 22.0, 22.0) + 0.22 * cells(p, 68.0, 71.0, 22.0, 22.0)); v += 1.05 * cells(p, 76.0, 84.0, 7.0, 7.0); v += (1.0 + grain) * (0.9 * cells(p, 76.0, 76.0, 10.0, 12.0) + 0.7 * cells(p, 77.0, 82.0, 10.0, 10.0) + 0.42 * cells(p, 77.0, 80.0, 11.0, 11.0) + 0.85 * cells(p, 82.0, 83.0, 11.0, 11.0) + 0.85 * cells(p, 77.0, 77.0, 12.0, 12.0) + 0.22 * cells(p, 79.0, 82.0, 12.0, 12.0)); v += (1.0 + grain) * (0.9 * cells(p, 76.0, 76.0, 15.0, 17.0) + 0.7 * cells(p, 77.0, 82.0, 15.0, 15.0) + 0.42 * cells(p, 77.0, 80.0, 16.0, 16.0) + 0.85 * cells(p, 82.0, 83.0, 16.0, 16.0) + 0.85 * cells(p, 77.0, 77.0, 17.0, 17.0) + 0.22 * cells(p, 79.0, 82.0, 17.0, 17.0)); v += (1.0 + grain) * (0.9 * cells(p, 76.0, 76.0, 20.0, 22.0) + 0.7 * cells(p, 77.0, 82.0, 20.0, 20.0) + 0.42 * cells(p, 77.0, 80.0, 21.0, 21.0) + 0.85 * cells(p, 82.0, 83.0, 21.0, 21.0) + 0.85 * cells(p, 77.0, 77.0, 22.0, 22.0) + 0.22 * cells(p, 79.0, 82.0, 22.0, 22.0)); // the new stage, when the deploy lands, and the first renewal, made from a won deal once its column exists { float nc = (step(3.6, s) * keep), nf = (sm(3.6, 3.6, s) * (1.0 - sm(3.6, 4.1, s))); v += nc * (1.05 + 0.25 * nf) * cells(p, 87.0, 95.0, 7.0, 7.0); v += nc * 0.75 * nf * cells(p, 87.0, 95.0, 8.0, 40.0) * step(abs(mod(cel, 2.0)), 0.5); float u = win(s, 4.1, 4.6); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(564.0, 106.9), vec2(641.0, 106.9), ease(u)), 8.0, 1.15); v += 0.5 * (sm(4.1, 4.1, s) * (1.0 - sm(4.2, 4.5, s))) * cells(p, 76.0, 84.0, 10.0, 12.0); v += (step(4.6, s) * keep) * (1.0 + 0.5 * (sm(4.6, 4.6, s) * (1.0 - sm(4.6, 5.1, s)))) * (0.9 * cells(p, 87.0, 87.0, 10.0, 12.0) + 0.7 * cells(p, 88.0, 93.0, 10.0, 10.0) + 0.42 * cells(p, 88.0, 91.0, 11.0, 11.0) + 0.85 * cells(p, 93.0, 94.0, 11.0, 11.0) + 0.85 * cells(p, 88.0, 88.0, 12.0, 12.0) + 0.22 * cells(p, 90.0, 93.0, 12.0, 12.0)); } return v; }

03

Try it on your laptop first

Save a file and the CRM on localhost reloads with real rows from your local database. The tests run as you save, so you know before you push.

Own it On one machine, a line of code is edited and saved, the app running at localhost reloads and reads real rows from a local database, and the test run below passes test by test. // the point as a whole bar column and cell vec2 qq(vec2 p) { return vec2(floor((p.x - 4.0) / 7.0 + 0.5), floor((p.y - 3.5) / 9.4 + 0.5)); } // a block of whole columns c0..c1 and cells k0..k1 (inclusive) float R(vec2 q, float c0, float k0, float c1, float k1) { return step(c0 - 0.5, q.x) * step(q.x, c1 + 0.5) * step(k0 - 0.5, q.y) * step(q.y, k1 + 0.5); } // its rim, one column and one cell thick float rimR(vec2 q, float c0, float k0, float c1, float k1) { return R(q, c0, k0, c1, k1) * (1.0 - R(q, c0 + 1.0, k0 + 1.0, c1 - 1.0, k1 - 1.0)); } float win(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } // on from a to b, off from c to d float pon(float t, float a, float b, float c, float d) { return sm(a, b, t) * (1.0 - sm(c, d, t)); } // a stable random number for a few small whole numbers; small arguments keep it the same on every GPU, so the still // and the live field agree float rnd(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 4.1414))) * 437.585); } // a line of text: a run of cells from c0 whose length is set by a seed float txt(vec2 q, float c0, float k, float lmin, float lvar, float s) { return R(q, c0, k, c0 + lmin + floor(lvar * rnd(vec2(s, k * 1.7 + 3.1))), k); } float pk(vec2 p, vec2 a, vec2 b, float t, float t0, float t1) { return step(t0, t) * step(t, t1) * blob(p, mix(a, b, ease(win(t, t0, t1))), 8.0, 1.15); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; vec2 q = qq(p); float v = 0.0; // THE EDITOR: a rim, two tabs (the active one with its unsaved dot), the file tree, line numbers, the code v += 0.22 * rimR(q, 6.0, 6.0, 31.0, 33.0); v += 0.7 * R(q, 8.0, 8.0, 14.0, 8.0) + 0.22 * R(q, 18.0, 8.0, 23.0, 8.0); v += 1.05 * pon(t, 0.45, 0.5, 1.42, 1.5) * R(q, 16.0, 8.0, 16.0, 8.0); if (q.x >= 8.0 && q.x <= 13.0 && q.y >= 10.0 && q.y <= 31.0) { float ind = floor(3.0 * rnd(vec2(q.y, 7.7))); float len = 1.0 + floor(3.0 * rnd(vec2(q.y, 2.2))); if (rnd(vec2(q.y, 9.1)) > 0.15 && q.x >= 8.0 + ind && q.x <= min(13.0, 8.0 + ind + len)) v += q.y == 15.0 ? 0.7 : (ind < 0.5 ? 0.42 : 0.22); } v += 0.15 * R(q, 15.0, 10.0, 15.0, 31.0); if (q.x >= 17.0 && q.x <= 30.0 && q.y >= 10.0 && q.y <= 31.0) { float ind = 2.0 * floor(3.0 * rnd(vec2(q.y, 5.0))); float len = 3.0 + floor(8.0 * rnd(vec2(q.y, 6.0))); float blank = step(rnd(vec2(q.y, 8.0)), 0.18); if (q.y == 18.0) { ind = 2.0; len = 9.0; blank = 0.0; } float vis = len; float sel = 0.0; if (q.y == 18.0) { vis = t < 0.7 ? len * (1.0 - win(t, 0.45, 0.7)) : len * win(t, 0.7, 1.4); sel = pon(t, 0.25, 0.3, 0.42, 0.47); } float lc = q.x - 17.0 - ind; if (blank < 0.5 && lc >= 0.0 && lc < vis - 0.5) v += lc < 1.5 ? 0.7 + 0.35 * sel : 0.42 + 0.63 * sel; if (blank < 0.5 && q.x == 16.0) v += 0.22; } // the caret: solid while typing, blinking twice a second otherwise (its toggles fall clear of the seam) { float typing = step(0.45, t) * step(t, 1.4); float vis = t < 0.45 ? 9.0 : (t < 0.7 ? 9.0 * (1.0 - win(t, 0.45, 0.7)) : 9.0 * win(t, 0.7, 1.4)); float blink = step(0.5, fract(t * 2.0 + 0.25)); v += 1.05 * max(typing, blink) * R(q, 19.0 + floor(vis + 0.5), 18.0, 19.0 + floor(vis + 0.5), 18.0); } // the links between the parts, and what travels on them v += 0.22 * R(q, 32.0, 19.0, 36.0, 19.0) * step(0.5, mod(q.x, 2.0)); v += 0.22 * R(q, 69.0, 19.0, 75.0, 19.0) * step(0.5, mod(q.x, 2.0)); v += pk(p, vec2(207.0, 172.70), vec2(284.0, 182.10), t, 1.5, 1.85); v += pk(p, vec2(452.0, 182.10), vec2(566.0, 182.10), t, 2.15, 2.5); v += pk(p, vec2(566.0, 182.10), vec2(452.0, 182.10), t, 3.0, 3.35); // THE APP at localhost: a rim, the address bar (it fills while the page reloads), the nav, and the deals table v += 0.22 * rimR(q, 37.0, 6.0, 68.0, 33.0); { float rl = win(t, 1.85, 2.15) * (1.0 - sm(2.2, 2.4, t)); float fill = 39.0 + 27.0 * rl; v += R(q, 39.0, 8.0, 66.0, 8.0) * (q.x < fill && rl > 0.0 ? 0.7 : 0.22) + 0.48 * R(q, 40.0, 8.0, 40.0, 8.0); } for (int i = 0; i < 5; i++) v += R(q, 39.0, 11.0 + 2.0 * float(i), 42.0, 11.0 + 2.0 * float(i)) * (i == 1 ? 0.7 : 0.42); v += 0.7 * (R(q, 45.0, 11.0, 49.0, 11.0) + R(q, 53.0, 11.0, 56.0, 11.0) + R(q, 60.0, 11.0, 64.0, 11.0)); if (q.x >= 45.0 && q.x <= 66.0 && q.y >= 13.0 && q.y <= 31.0 && mod(q.y - 13.0, 2.0) < 0.5) { float i = (q.y - 13.0) * 0.5; float at = 3.35 + 0.055 * i; float wait = pon(t, 1.9, 2.05, at, at + 0.08); float fresh = pon(t, at, at + 0.05, at + 0.2, at + 0.5); float r = 0.0; if (q.x <= 47.0 + floor(3.0 * rnd(vec2(i, 1.0)))) r = 0.42; if (q.x >= 53.0 && q.x <= 55.0) r = 0.42; if (q.x >= 60.0 && q.x <= 61.0 + floor(3.0 * rnd(vec2(i, 2.0)))) r = 0.7 + 0.45 * fresh; v += r * (1.0 - 0.62 * wait); } // THE LOCAL DATABASE: a cylinder with a crisp top, two seams, and its rows of records, denser deeper down { vec2 c = vec2(606.0, 92.0); vec2 rr = vec2(70.0, 16.0); float top = length((p - c) / rr); v += 0.7 * (1.0 - sm(0.12, 0.3, abs(top - 1.0))); float side = abs(abs(p.x - c.x) - rr.x); if (p.y > c.y && p.y < 296.0) v += 0.42 * (1.0 - sm(2.0, 4.0, side)); for (int s = 0; s < 3; s++) { float y0 = 92.0 + 68.0 * float(s + 1); float e = length((p - vec2(c.x, y0)) / rr); if (p.y > y0) v += (s == 2 ? 0.7 : 0.42) * (1.0 - sm(0.12, 0.3, abs(e - 1.0))); } // the records: a row every other cell, a key and its fields float inside = step(abs(p.x - c.x), rr.x - 12.0) * step(c.y + 22.0, p.y) * step(p.y, 292.0); if (inside > 0.5 && mod(q.y, 2.0) < 0.5) { float depth = (p.y - c.y) / 204.0; float lc = q.x - 78.0; float rec = step(lc, 1.5) * 0.42 + step(3.0, lc) * step(lc, 4.0 + floor(5.0 * rnd(vec2(q.y, 3.3)))) * (0.2 + 0.22 * depth); // the read head scans down; the rows it matches stay lit until they have gone back to the app float head = mix(c.y + 22.0, 292.0, win(t, 2.5, 3.0)); float on = step(2.5, t) * step(t, 3.0) * (1.0 - step(9.4, abs(p.y - head))); float match = step(0.62, rnd(vec2(q.y, 1.9))) * step(p.y, head) * step(2.5, t) * (1.0 - sm(3.05, 3.4, t)); v += rec > 0.0 ? rec + 0.6 * max(on, match) : 0.6 * on * 0.4; } } // THE TESTS: one bar per test, as tall as it took; the last run's bars clear on save and the new run fills them { float slotc = q.x - 10.0; if (slotc >= 0.0 && slotc < 54.0 && mod(slotc, 3.0) < 1.5 && q.y <= 43.0 && q.y >= 38.0) { float i = floor(slotc / 3.0); float h = 1.0 + floor(4.0 * rnd(vec2(i, 4.4))); float ti = 2.0 + 0.15 * i; float shown = max(1.0 - sm(1.5, 1.8, t), sm(ti, ti + 0.12, t)); float lk = 43.0 - q.y; if (lk <= h) v += shown * (lk > h - 0.5 ? 1.05 : 0.7) + (lk < 0.5 ? 0.22 * (1.0 - shown) : 0.0); } } return v; }

For your sales team

Sales gets a clean CRM, not a repo

Pipeline board A sales pipeline board with four columns of deal cards; one deal is dragged from lead to qualified to proposal to won, the value strips over each column follow it, and a new lead comes in. // the point as a whole bar column and cell vec2 qq(vec2 p) { return vec2(floor((p.x - 4.0) / 7.0 + 0.5), floor((p.y - 3.5) / 9.4 + 0.5)); } // a block of whole columns c0..c1 and cells k0..k1 (inclusive) float R(vec2 q, float c0, float k0, float c1, float k1) { return step(c0 - 0.5, q.x) * step(q.x, c1 + 0.5) * step(k0 - 0.5, q.y) * step(q.y, k1 + 0.5); } // its rim, one column and one cell thick float rimR(vec2 q, float c0, float k0, float c1, float k1) { return R(q, c0, k0, c1, k1) * (1.0 - R(q, c0 + 1.0, k0 + 1.0, c1 - 1.0, k1 - 1.0)); } float win(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } // on from a to b, off from c to d float pon(float t, float a, float b, float c, float d) { return sm(a, b, t) * (1.0 - sm(c, d, t)); } // a stable random number for a few small whole numbers; small arguments keep it the same on every GPU, so the still // and the live field agree float rnd(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 4.1414))) * 437.585); } // a line of text: a run of cells from c0 whose length is set by a seed float txt(vec2 q, float c0, float k, float lmin, float lvar, float s) { return R(q, c0, k, c0 + lmin + floor(lvar * rnd(vec2(s, k * 1.7 + 3.1))), k); } // A deal card at o (its top-left cell), 19 columns by 7 cells: a faint rim; a title and the owner's avatar; the company // and the avatar's lower half; the amount, dense, and four stage dots lit up to its stage; two tags; a due date and an // activity count. lift: the card is held, so it reads a shade denser all through. float card(vec2 q, vec2 o, float s, float stage, float lift) { vec2 l = floor(q - o + 0.5); if (l.x < 0.0 || l.x > 18.0 || l.y < 0.0 || l.y > 6.0) return 0.0; if (l.y == 0.0 || l.y == 6.0 || l.x == 0.0 || l.x == 18.0) return 0.22 + 0.4 * lift; float v = 0.0; if (l.y == 1.0) { if (l.x >= 2.0 && l.x <= 6.0 + floor(5.0 * rnd(vec2(s, 1.0)))) v = 0.7; if (l.x >= 15.0 && l.x <= 16.0) v = 0.7; } else if (l.y == 2.0) { if (l.x >= 2.0 && l.x <= 4.0 + floor(5.0 * rnd(vec2(s, 2.0)))) v = 0.42; if (l.x >= 15.0 && l.x <= 16.0) v = 0.7; } else if (l.y == 3.0) { if (l.x >= 2.0 && l.x <= 4.0 + floor(3.0 * rnd(vec2(s, 3.0)))) v = 1.05; if (l.x >= 10.0 && l.x <= 16.0 && mod(l.x, 2.0) < 0.5) v = (l.x - 10.0) * 0.5 <= stage + 0.1 ? 0.7 : 0.22; } else if (l.y == 4.0) { if (l.x >= 2.0 && l.x <= 4.0 + floor(2.0 * rnd(vec2(s, 5.0)))) v = 0.42; if (l.x >= 7.0 && l.x <= 9.0) v = 0.42 * step(0.4, rnd(vec2(s, 6.0))); } else { if (l.x >= 2.0 && l.x <= 3.0) v = 0.22; if (l.x >= 5.0 && l.x <= 7.0 + floor(3.0 * rnd(vec2(s, 4.0)))) v = 0.22; if (l.x >= 15.0 && l.x <= 16.0) v = 0.42; } return v > 0.0 ? v + 0.32 * lift : 0.0; } // the drop slot a dragged card is headed for: a dashed outline float slot(vec2 q, vec2 o) { vec2 l = floor(q - o + 0.5); if (l.x < 0.0 || l.x > 18.0 || l.y < 0.0 || l.y > 6.0) return 0.0; bool rim = l.y == 0.0 || l.y == 6.0 || l.x == 0.0 || l.x == 18.0; return rim && mod(l.x + l.y, 2.0) < 0.5 ? 0.42 : 0.0; } // a column's value strip: filled to len columns, a faint track after it float strip(vec2 q, float c0, float len) { if (q.y != 6.0 || q.x < c0 - 0.5 || q.x > c0 + 18.5) return 0.0; return q.x - c0 < len - 0.5 ? 0.7 : 0.2; } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; vec2 q = qq(p); float v = 0.0; // the toolbar: a search field with its glass, two filter pills, and the view switch with BOARD on v += 0.22 * R(q, 8.0, 3.0, 24.0, 3.0) + 0.5 * R(q, 7.0, 3.0, 7.0, 3.0); v += 0.42 * (R(q, 28.0, 3.0, 32.0, 3.0) + R(q, 34.0, 3.0, 39.0, 3.0)); v += 0.9 * R(q, 81.0, 3.0, 85.0, 3.0) + 0.22 * (R(q, 87.0, 3.0, 89.0, 3.0) + R(q, 91.0, 3.0, 94.0, 3.0)); // the cards that stay put v = max(v, card(q, vec2(7.0, 9.0), 1.0, 0.0, 0.0)); v = max(v, card(q, vec2(7.0, 17.0), 2.0, 0.0, 0.0)); v = max(v, card(q, vec2(7.0, 25.0), 3.0, 0.0, 0.0)); v = max(v, card(q, vec2(30.0, 9.0), 6.0, 1.0, 0.0)); v = max(v, card(q, vec2(30.0, 17.0), 7.0, 1.0, 0.0)); v = max(v, card(q, vec2(30.0, 25.0), 8.0, 1.0, 0.0)); v = max(v, card(q, vec2(53.0, 9.0), 11.0, 2.0, 0.0)); v = max(v, card(q, vec2(53.0, 17.0), 12.0, 2.0, 0.0)); v = max(v, card(q, vec2(76.0, 9.0), 16.0, 3.0, 0.0)); v = max(v, card(q, vec2(76.0, 17.0), 17.0, 3.0, 0.0)); // the deal: lifted, carried from slot to slot, dropped; its stage changes only once it is dropped float u1 = ease(win(t, 0.35, 1.15)), u2 = ease(win(t, 1.80, 2.60)), u3 = ease(win(t, 3.25, 4.05)); vec2 o = vec2(7.0, 33.0); o = mix(o, vec2(30.0, 33.0), u1); o = mix(o, vec2(53.0, 25.0), u2); o = mix(o, vec2(76.0, 25.0), u3); float down = ease(win(t, 5.05, 5.65)); o.y = mix(o.y, 35.0, down); float lift = pon(t, 0.20, 0.35, 1.15, 1.30) + pon(t, 1.65, 1.80, 2.60, 2.75) + pon(t, 3.10, 3.25, 4.05, 4.20); float stage = step(1.15, t) + step(2.60, t) + step(4.05, t); float filed = 1.0 - sm(5.40, 5.65, t); v = max(v, filed * card(q, o, 50.0, stage, lift)); // the drop slot in the column it is headed for, as it nears v = max(v, pon(t, 0.6, 0.75, 1.1, 1.18) * slot(q, vec2(30.0, 33.0))); v = max(v, pon(t, 2.05, 2.2, 2.55, 2.63) * slot(q, vec2(53.0, 25.0))); v = max(v, pon(t, 3.5, 3.65, 4.0, 4.08) * slot(q, vec2(76.0, 25.0))); // the new lead, in through LEAD's add slot, in the place the first deal left v = max(v, sm(6.3, 6.95, t) * card(q, vec2(7.0, 33.0), 50.0, 0.0, 0.0)); // the value strips: each stage loses the deal's value as it leaves and gains it as it lands float d = 4.0; v += strip(q, 7.0, 9.0 + d * (1.0 - sm(1.15, 1.45, t)) + d * sm(6.45, 6.95, t)); v += strip(q, 30.0, 7.0 + d * (sm(1.15, 1.45, t) - sm(2.6, 2.9, t))); v += strip(q, 53.0, 6.0 + d * (sm(2.6, 2.9, t) - sm(4.05, 4.35, t))); // the add slots under the open columns; LEAD's pulses as the new lead comes in v += R(q, 7.0, 42.0, 25.0, 42.0) * (mod(q.x, 2.0) < 0.5 ? 0.22 : 0.0) + 0.42 * R(q, 15.0, 42.0, 17.0, 42.0); v += R(q, 30.0, 42.0, 48.0, 42.0) * (mod(q.x, 2.0) < 0.5 ? 0.22 : 0.0) + 0.42 * R(q, 38.0, 42.0, 40.0, 42.0); v += R(q, 53.0, 42.0, 71.0, 42.0) * (mod(q.x, 2.0) < 0.5 ? 0.22 : 0.0) + 0.42 * R(q, 61.0, 42.0, 63.0, 42.0); v += 0.7 * pon(t, 6.05, 6.15, 6.3, 6.55) * R(q, 7.0, 42.0, 25.0, 42.0); // CLOSED: the filed deals under WON, three ruled lines; the top one flashes as the won card goes in for (int i = 0; i < 3; i++) { float k = 35.0 + 2.0 * float(i); float len = 15.0 - 3.0 * float(i); float ln = R(q, 77.0, k, 77.0 + len, k); v += ln * (0.42 - 0.08 * float(i) + (i == 0 ? 0.65 * pon(t, 5.5, 5.62, 5.75, 6.0) : 0.0)); } return v; }

A board they already know

Four stages, deal cards and a running total over each column. Drag a deal and the totals follow it.

Live sync An assistant's suggested change is applied to a live record, and the same deal lights in a table, a board and a chart at once; then the assistant writes its next suggestion. // the point as a whole bar column and cell vec2 qq(vec2 p) { return vec2(floor((p.x - 4.0) / 7.0 + 0.5), floor((p.y - 3.5) / 9.4 + 0.5)); } // a block of whole columns c0..c1 and cells k0..k1 (inclusive) float R(vec2 q, float c0, float k0, float c1, float k1) { return step(c0 - 0.5, q.x) * step(q.x, c1 + 0.5) * step(k0 - 0.5, q.y) * step(q.y, k1 + 0.5); } // its rim, one column and one cell thick float rimR(vec2 q, float c0, float k0, float c1, float k1) { return R(q, c0, k0, c1, k1) * (1.0 - R(q, c0 + 1.0, k0 + 1.0, c1 - 1.0, k1 - 1.0)); } float win(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } // on from a to b, off from c to d float pon(float t, float a, float b, float c, float d) { return sm(a, b, t) * (1.0 - sm(c, d, t)); } // a stable random number for a few small whole numbers; small arguments keep it the same on every GPU, so the still // and the live field agree float rnd(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 4.1414))) * 437.585); } // a line of text: a run of cells from c0 whose length is set by a seed float txt(vec2 q, float c0, float k, float lmin, float lvar, float s) { return R(q, c0, k, c0 + lmin + floor(lvar * rnd(vec2(s, k * 1.7 + 3.1))), k); } // a faint link: a soft band from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b, float k) { vec2 ab = b - a; float h = clamp(dot(p - a, ab) / dot(ab, ab), 0.0, 1.0); vec2 d = (p - a - ab * h) / vec2(3.6, 4.8); return k * (1.0 - sm(0.8, 1.05, length(d))); } float pk(vec2 p, vec2 a, vec2 b, float t, float t0, float t1) { return step(t0, t) * step(t, t1) * blob(p, mix(a, b, ease(win(t, t0, t1))), 8.0, 1.15); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; vec2 q = qq(p); float ph = TAU * t / T; float v = 0.0; // THE ASSISTANT: a rim, its header, the context it is reading, the suggestion it writes, Apply, and the input q = qq(p) - vec2(0.0, 5.0); v += 0.22 * rimR(q, 6.0, 6.0, 30.0, 33.0); v += 0.7 * R(q, 8.0, 8.0, 9.0, 9.0) + 0.42 * R(q, 11.0, 8.0, 17.0, 8.0) + 0.22 * R(q, 11.0, 9.0, 15.0, 9.0); v += 0.22 * (R(q, 8.0, 11.0, 26.0, 11.0) + R(q, 8.0, 12.0, 21.0, 12.0)); float reveal = min(41.0, floor(win(t, 2.6, 5.6) * 15.0) * 3.0); float kept = 1.0 - sm(0.35, 0.75, t); float before = 0.0; if (q.y == 15.0 && q.x >= 8.0 && q.x <= 22.0) { float i = before + q.x - 8.0; float on = max(kept, step(i + 0.5, reveal) * step(2.6, t)); v += on * (q.x < 10.5 && q.y == 15.0 ? 0.7 : 0.42); v += step(2.6, t) * step(t, 5.6) * step(abs(i - reveal - 0.5), 0.6) * 1.05; } before += 15.0; if (q.y == 16.0 && q.x >= 8.0 && q.x <= 19.0) { float i = before + q.x - 8.0; float on = max(kept, step(i + 0.5, reveal) * step(2.6, t)); v += on * (q.x < 10.5 && q.y == 15.0 ? 0.7 : 0.42); v += step(2.6, t) * step(t, 5.6) * step(abs(i - reveal - 0.5), 0.6) * 1.05; } before += 12.0; if (q.y == 17.0 && q.x >= 8.0 && q.x <= 21.0) { float i = before + q.x - 8.0; float on = max(kept, step(i + 0.5, reveal) * step(2.6, t)); v += on * (q.x < 10.5 && q.y == 15.0 ? 0.7 : 0.42); v += step(2.6, t) * step(t, 5.6) * step(abs(i - reveal - 0.5), 0.6) * 1.05; } before += 14.0; // the change it proposes: a field, its old value struck through, its new value { float on = max(kept, sm(5.6, 6.0, t)); float blk = 0.22 * rimR(q, 8.0, 19.0, 28.0, 24.0); blk += 0.42 * (R(q, 10.0, 21.0, 13.0, 21.0) + R(q, 10.0, 22.0, 12.0, 22.0)); blk += 0.22 * (R(q, 15.0, 21.0, 18.0, 21.0) + R(q, 15.0, 22.0, 17.0, 22.0)); blk += 0.7 * (R(q, 21.0, 21.0, 26.0, 21.0) + R(q, 21.0, 22.0, 24.0, 22.0)); v += on * blk; } // Apply: ready (dense), pressed (brighter), spent, then ready again once the next suggestion is whole float ready = max(1.0 - sm(0.3, 0.45, t), sm(6.0, 6.2, t)); v += R(q, 8.0, 27.0, 15.0, 27.0) * (0.22 + 0.8 * ready + 0.4 * pon(t, 0.15, 0.2, 0.25, 0.35)); v += 0.22 * R(q, 17.0, 27.0, 22.0, 27.0); v += 0.22 * R(q, 8.0, 30.0, 28.0, 30.0) + 0.42 * R(q, 27.0, 30.0, 28.0, 30.0); q = qq(p); // THE RECORD: a bright rim, an inner ring, a speckled floor and a core that churns, and swells when it updates vec2 H = vec2(312.0, 238.50); { float d = length(p - H) / 34.0; float inside = 1.0 - sm(0.9, 1.0, d); float churn = noise(p * 0.09 + vec2(1.4 * cos(ph), 1.4 * sin(ph))); float up = pon(t, 0.8, 0.9, 1.1, 1.5); v += (0.44 + 0.4 * up) * exp(-pow(d - 1.0, 2.0) / 0.008); v += inside * 0.26 * exp(-pow(d - 0.62, 2.0) / 0.006); v += inside * (0.12 + 0.12 * step(0.78, rnd(floor(p / vec2(7.0, 9.4))))); v += blob(p, H, 15.0, 0.85 + 0.45 * (churn - 0.5) + 0.5 * up); float g = t - 0.85; if (g > 0.0 && g < 0.55) v += 0.7 * (1.0 - g / 0.55) * exp(-pow(length(p - H) - 38.0 - 110.0 * g, 2.0) / 30.0); } // the links: the assistant to the record, the record out to each view; and what travels on them vec2 A = vec2(221.0, 304.30); v += lk(p, A, H + vec2(-30.0, 14.0), 0.32); v += lk(p, H + normalize(vec2(403.0, 111.60) - H) * 36.0, vec2(403.0, 111.60), 0.32); v += pk(p, H + normalize(vec2(403.0, 111.60) - H) * 30.0, vec2(403.0, 111.60), t, 1.0, 1.4); v += lk(p, H + normalize(vec2(403.0, 233.80) - H) * 36.0, vec2(403.0, 233.80), 0.32); v += pk(p, H + normalize(vec2(403.0, 233.80) - H) * 30.0, vec2(403.0, 233.80), t, 1.0, 1.4); v += lk(p, H + normalize(vec2(403.0, 356.0) - H) * 36.0, vec2(403.0, 356.0), 0.32); v += pk(p, H + normalize(vec2(403.0, 356.0) - H) * 30.0, vec2(403.0, 356.0), t, 1.0, 1.4); v += pk(p, A, H - vec2(18.0, -8.0), t, 0.35, 0.8); v += pk(p, H - vec2(18.0, -8.0), A, t, 2.2, 2.6); q = qq(p) - vec2(0.0, 2.0); // THE VIEWS, each with the same deal in it, lit the moment the change arrives float lit = 0.62 * pon(t, 1.4, 1.45, 1.65, 2.1); // the table: a header, three rows; the deal is the middle row v += 0.22 * rimR(q, 58.0, 5.0, 95.0, 14.0); v += 0.7 * (R(q, 60.0, 7.0, 64.0, 7.0) + R(q, 70.0, 7.0, 73.0, 7.0) + R(q, 80.0, 7.0, 84.0, 7.0) + R(q, 89.0, 7.0, 92.0, 7.0)); for (int i = 0; i < 3; i++) { float k = 9.0 + 2.0 * float(i); float row = R(q, 60.0, k, 63.0 + floor(3.0 * rnd(vec2(k, 1.0))), k) + R(q, 70.0, k, 72.0, k) + R(q, 80.0, k, 81.0 + floor(3.0 * rnd(vec2(k, 2.0))), k) + R(q, 89.0, k, 90.0, k); v += row * (0.42 + (i == 1 ? lit : 0.0)) + (i == 1 ? 0.3 * lit * R(q, 59.0, k, 94.0, k) : 0.0); } // the board: three stages of small cards; the deal is the top card of the middle stage v += 0.22 * rimR(q, 58.0, 18.0, 95.0, 27.0); for (int s = 0; s < 3; s++) { float c0 = 60.0 + 12.0 * float(s); v += 0.42 * R(q, c0, 20.0, c0 + 4.0, 20.0); for (int j = 0; j < 2; j++) { float k = 22.0 + 3.0 * float(j); float deal = (s == 1 && j == 0) ? lit : 0.0; v += (0.7 + deal) * R(q, c0, k, c0 + 5.0 + float(s + j), k) + (0.22 + deal) * R(q, c0, k + 1.0, c0 + 8.0, k + 1.0); } } // the chart: value by week; the deal is in the sixth bar, whose top lights v += 0.22 * rimR(q, 58.0, 31.0, 95.0, 40.0); v += 0.22 * R(q, 60.0, 38.0, 93.0, 38.0); if (q.x >= 61.0 && q.x <= 92.0 && q.y >= 33.0 && q.y <= 37.0 && mod(q.x - 61.0, 4.0) < 1.5) { float i = floor((q.x - 61.0) / 4.0); float h = 1.0 + floor(4.0 * rnd(vec2(i, 6.6))) + (i == 5.0 ? 1.0 : 0.0); float lk2 = 37.0 - q.y; if (lk2 < h) v += 0.7 + (i == 5.0 && lk2 > h - 1.5 ? 0.35 + lit : 0.0); } return v; }

One record, every view

Accept a suggested edit on a deal and the table, the board and the chart change together.

Command actions A command bar over a list of deals: ENTER runs the picked action and the deal's row updates, then CMD K opens the bar again and three typed letters narrow seven actions to two. // the point as a whole bar column and cell vec2 qq(vec2 p) { return vec2(floor((p.x - 4.0) / 7.0 + 0.5), floor((p.y - 3.5) / 9.4 + 0.5)); } // a block of whole columns c0..c1 and cells k0..k1 (inclusive) float R(vec2 q, float c0, float k0, float c1, float k1) { return step(c0 - 0.5, q.x) * step(q.x, c1 + 0.5) * step(k0 - 0.5, q.y) * step(q.y, k1 + 0.5); } // its rim, one column and one cell thick float rimR(vec2 q, float c0, float k0, float c1, float k1) { return R(q, c0, k0, c1, k1) * (1.0 - R(q, c0 + 1.0, k0 + 1.0, c1 - 1.0, k1 - 1.0)); } float win(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } // on from a to b, off from c to d float pon(float t, float a, float b, float c, float d) { return sm(a, b, t) * (1.0 - sm(c, d, t)); } // a stable random number for a few small whole numbers; small arguments keep it the same on every GPU, so the still // and the live field agree float rnd(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 4.1414))) * 437.585); } // a line of text: a run of cells from c0 whose length is set by a seed float txt(vec2 q, float c0, float k, float lmin, float lvar, float s) { return R(q, c0, k, c0 + lmin + floor(lvar * rnd(vec2(s, k * 1.7 + 3.1))), k); } // a keycap: a rim three cells tall; pressed, it fills float key(vec2 q, float c0, float k0, float c1, float k1, float down) { return 0.42 * rimR(q, c0, k0, c1, k1) + R(q, c0, k0, c1, k1) * 0.95 * down; } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; vec2 q = qq(p); // before 1.0 s the bar still holds the last cycle's typing, so it is read at t + T there float tt = t < 1.0 ? t + T : t; float open = 1.0 - sm(0.4, 0.65, t) + sm(2.0, 2.25, t); float blink = step(0.5, fract(t * 2.0 + 0.25)); // THE DEALS LIST: a rim, a header, rows of avatar, name, company, stage, value, owner and last activity float lv = 0.22 * rimR(q, 5.0, 4.0, 94.0, 34.0); lv += 0.7 * (R(q, 7.0, 6.0, 11.0, 6.0) + R(q, 22.0, 6.0, 25.0, 6.0) + R(q, 40.0, 6.0, 43.0, 6.0) + R(q, 55.0, 6.0, 58.0, 6.0) + R(q, 70.0, 6.0, 73.0, 6.0) + R(q, 86.0, 6.0, 89.0, 6.0)); if (q.x >= 7.0 && q.x <= 92.0 && q.y >= 8.0 && q.y <= 32.0 && mod(q.y - 8.0, 3.0) < 0.5) { float r = (q.y - 8.0) / 3.0; float k = q.y; float row = 0.7 * R(q, 7.0, k, 8.0, k) + 0.42 * R(q, 10.0, k, 13.0 + floor(5.0 * rnd(vec2(r, 1.0))), k); row += 0.22 * R(q, 22.0, k, 24.0 + floor(4.0 * rnd(vec2(r, 2.0))), k); row += 0.42 * R(q, 40.0, k, 42.0 + floor(3.0 * rnd(vec2(r, 5.0))), k) + 0.7 * R(q, 55.0, k, 57.0 + floor(2.0 * rnd(vec2(r, 6.0))), k); row += 0.42 * R(q, 70.0, k, 71.0, k) + 0.42 * R(q, 86.0, k, 89.0, k); float logged = r == 3.0 ? pon(t, 0.65, 0.75, 0.95, 1.5) : 0.0; row += logged * (0.3 * R(q, 7.0, k, 76.0, k) * step(0.1, row) + 0.65 * R(q, 86.0, k, 89.0, k)); lv += row; } float v = lv * (1.0 - 0.35 * open); // THE COMMAND BAR, over the list while it is open; it is as tall as the actions that still match float count = 0.0; for (int i = 0; i < 7; i++) { float fi = float(i); float lvl = fi == 0.0 ? 3.0 : fi == 1.0 ? 0.0 : fi == 2.0 ? 2.0 : fi == 3.0 ? 1.0 : fi == 4.0 ? 3.0 : fi == 5.0 ? 0.0 : 1.0; float dies = lvl > 2.5 ? 99.0 : lvl < 0.5 ? 2.60 : lvl < 1.5 ? 2.85 : 3.10; count += 1.0 - sm(dies + 0.05, dies + 0.25, tt); } float bot = floor(11.0 + 3.0 * count + 0.5); float inPal = R(q, 22.0, 6.0, 78.0, bot); if (inPal > 0.5) { float pv = 0.42 * rimR(q, 22.0, 6.0, 78.0, bot); // the search line: a glass, the letters typed so far, the caret after them pv += 0.7 * R(q, 24.0, 8.0, 24.0, 8.0); float typed = 0.0; typed += step(2.60, tt); pv += step(2.60, tt) * 0.7 * R(q, 27.0, 8.0, 28.0, 8.0); typed += step(2.85, tt); pv += step(2.85, tt) * 0.7 * R(q, 30.0, 8.0, 31.0, 8.0); typed += step(3.10, tt); pv += step(3.10, tt) * 0.7 * R(q, 33.0, 8.0, 34.0, 8.0); float typing = 0.0; typing = max(typing, step(2.55, tt) * step(tt, 2.80)); typing = max(typing, step(2.80, tt) * step(tt, 3.05)); typing = max(typing, step(3.05, tt) * step(tt, 3.30)); pv += 1.05 * max(blink, typing) * R(q, 27.0 + 3.0 * typed, 8.0, 27.0 + 3.0 * typed, 8.0); pv += 0.22 * R(q, 23.0, 10.0, 77.0, 10.0) * step(0.5, mod(q.x, 2.0)); // the actions: an icon, a name, a hint, its own shortcut; those that no longer match fade and the rest close up float above = 0.0; for (int i = 0; i < 7; i++) { float fi = float(i); float lvl = fi == 0.0 ? 3.0 : fi == 1.0 ? 0.0 : fi == 2.0 ? 2.0 : fi == 3.0 ? 1.0 : fi == 4.0 ? 3.0 : fi == 5.0 ? 0.0 : 1.0; float dies = lvl > 2.5 ? 99.0 : lvl < 0.5 ? 2.60 : lvl < 1.5 ? 2.85 : 3.10; float alive = 1.0 - sm(dies + 0.05, dies + 0.25, tt); float k = 12.0 + floor(3.0 * above + 0.5); if (abs(q.y - k) < 0.5 && alive > 0.01) { float rv = 0.7 * R(q, 25.0, k, 26.0, k); rv += 0.42 * R(q, 29.0, k, 33.0 + floor(8.0 * rnd(vec2(fi, 3.0))), k); rv += 0.22 * R(q, 46.0, k, 48.0 + floor(5.0 * rnd(vec2(fi, 4.0))), k); rv += 0.42 * (R(q, 71.0, k, 72.0, k) + R(q, 74.0, k, 75.0, k)); // the picked action: its row lit behind, its content a shade denser, brighter still while ENTER runs it if (i == 0) rv = rv > 0.0 ? rv + 0.3 + 0.5 * pon(t, 0.2, 0.25, 0.35, 0.45) : 0.22 * R(q, 24.0, k, 76.0, k) + 1.05 * R(q, 23.0, k, 23.0, k); pv += alive * rv; } above += alive; } v = mix(v, pv, open); } // THE KEYS v += key(q, 30.0, 38.0, 37.0, 40.0, pon(t, 1.75, 1.8, 2.1, 2.15)); v += key(q, 39.0, 38.0, 43.0, 40.0, pon(t, 1.85, 1.9, 2.05, 2.1)); v += key(q, 66.0, 38.0, 76.0, 40.0, pon(t, 0.2, 0.25, 0.35, 0.4)); return v; }

Type to act

Press Cmd K, type three letters, and the list narrows to the action you want.

Customer stories

Startups that ship their CRM with their app

  1. 2 daysFrom request to live stage

    Payroll startup

    Sales asks, engineering merges

    A renewals stage went from a chat message to production in one review.

    Read the story
  2. 41Reviewed model changes this year

    Robotics leasing

    Every change has a reviewer

    Each new field and rule has an author, a reviewer and a passing test run in the history.

    Read the story
  3. 1Deploy for product and CRM

    Travel booking

    Nothing waits on a second system

    Booking features and the sales pipeline go out together, so neither team holds up the other.

    Read the story

Sales stopped filing tickets

Our head of sales opens issues in our repo now. Most of them are merged by Thursday, and she reviews the preview herself.
Juno HalvorsenEngineering lead, payroll startup

I can read the history

A deal jumped stages and nobody knew why. I found the commit in a minute. With our old vendor that was a support call and a week of waiting.
Ruben CastellanePlatform engineer, robotics leasing

Same tests, same deploy

We already had CI. Now the pipeline rules go through it like everything else, and sales gets new stages on our normal Tuesday release.
Lotte BrandvikCTO, travel booking startup

Questions

What engineers ask before they switch

Want to see a model file first? Book a call and we'll walk through one with you.

Where does the data model live?

In your repository, as typed files. Objects, fields, relations, stages and rules are all code your team can read and review.

Can sales still make small changes?

Yes. Views, filters and saved lists are set in the app. Anything that changes the model goes through a pull request.

How do I test a change?

Run Corvane on your laptop against a local database seeded with real records. The tests run each time you save.

How do changes reach production?

With your normal deploy. Corvane reads the model from your build, so the CRM and the app go out together.

Can a coding tool write the changes?

Yes. The model is plain code with types, so the coding tool your team already uses can edit it and run the tests.

Can we import our current records?

Export a CSV and run the import script. Map its columns to your objects in code and try it locally first.

What does Corvane cost?

Free for up to five seats. From the sixth seat on, you pay monthly for each one.

Put your CRM next to your code

Clone the starter repo, run it on your laptop and open your first pull request today.