Open-source CRM framework

Shape the CRM your team needs, at AI speed

Corvane gives technical teams the building blocks for a custom CRM that meets complex business needs and keeps adapting as the business changes.

Self-hosting is free. The managed cloud has a free tier for small teams.

Scaffold A prompt is read line by line and its tokens stream into a workspace, where three record objects form beside a standard company object, header first and then field by field, relations join them once both ends exist, and a strip below counts every field. float ci(float x) { return floor((x - 4.0) / 7.0 + 0.5); } float ri(float y) { return floor((y - 3.5) / 9.4 + 0.5); } // a record object: a dense header with its name, a key field, then field rows (a name and a type tag), a rim and a // faint floor. Row r shows once b passes it, so the object forms header first and then field by field. float obj(vec2 p, float c0, float r0, float nc, float nr, float sd, float b, float env, float ph) { float c = ci(p.x) - c0, r = ri(p.y) - r0; if (c < 0.0 || c > nc - 1.0 || r < 0.0 || r > nr - 1.0) return 0.0; if ((c == 0.0 || c == nc - 1.0) && (r == 0.0 || r == nr - 1.0)) return 0.0; float on = clamp(b - r, 0.0, 1.0); if (on <= 0.0) return 0.0; float hs = hash(vec2(sd, r)); float floorv = 0.24 + 0.04 * sin(ph * 2.0 + hash(vec2(c, r + sd)) * 6.283); float v = floorv; if (r == 0.0) v = 1.05; else if (r == 1.0) v = (c >= 2.0 && c <= 6.0 + floor(4.0 * hs)) ? 0.7 : 1.05; else if (r == 2.0) v = c == 1.0 ? 1.05 : (c >= 3.0 && c <= 7.0 + floor(3.0 * hs)) ? 0.7 : floorv; else if (r == nr - 1.0) v = (c >= 1.0 && c <= 3.0) ? 0.3 : floorv; else { float len = 3.0 + floor(6.0 * hs); if (c >= 1.0 && c <= len && c != 1.0 + floor(2.0 + 3.0 * hash(vec2(r, sd + 4.0)))) v = 0.42; float tag = hash(vec2(sd + 9.0, r)); float t0 = nc - 4.0 - floor(3.0 * tag), tw = floor(3.0 * hash(vec2(r, sd + 2.0))); if (c >= t0 && c <= min(t0 + tw, nc - 2.0)) v = tag > 0.6 ? 1.05 : 0.7; if (c == 0.0 || c == nc - 1.0) v = max(v, 0.42); } return v * on * env; } // a relation: a soft band from a to b, at least a column wide and a cell tall 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 rel(vec2 p, vec2 a, vec2 b, float u, float env) { if (u <= 0.0) return 0.0; vec2 c = mix(a, b, max(ease(u), 0.02)); float v = lk(p, a, c, 0.5); v += blob(p, a, 5.0, 1.0); if (u < 1.0) v += blob(p, c, 7.0, 1.3); v += sm(0.95, 1.0, u) * blob(p, b, 5.0, 1.0); return v * env; } vec2 bez(vec2 a, vec2 c, vec2 b, float u) { return mix(mix(a, c, u), mix(c, b, u), u); } // tokens: a dense head with a short trail, travelling from the prompt to the object's header float tokens(vec2 p, vec2 a, vec2 c, vec2 b, float s, float t0, float t1) { float u = clamp((s - t0) / (t1 - t0), 0.0, 1.0); if (u <= 0.0 || u >= 1.0) return 0.0; float e = sm(0.0, 0.1, u) * (1.0 - sm(0.88, 1.0, u)); float v = blob(p, bez(a, c, b, ease(u)), 7.0, 1.25); v += blob(p, bez(a, c, b, ease(max(u - 0.07, 0.0))), 5.5, 0.75); v += blob(p, bez(a, c, b, ease(max(u - 0.14, 0.0))), 5.0, 0.45); return v * e; } // a slot of the field strip: col is its column in the strip, n its height in cells float slot(vec2 p, float col, float shown) { float c = ci(p.x) - 33.0 - col; if (c != 0.0) return 0.0; float rb = 44.0 - ri(p.y); float n = 2.0 + floor(3.0 * hash(vec2(col, 7.0))); if (rb < 0.0 || rb > n - 1.0) return 0.0; float v = rb == n - 1.0 ? 1.05 : 0.7; return max(v * shown, rb == 0.0 ? 0.16 : 0.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 ph = TAU * t / T; float s = mod(t + 5.6, T); // the previous run's scaffold is held until it clears float ss = s < 0.6 ? s + T : s; float env = 1.0 - sm(T + 0.1, T + 0.5, ss); float v = 0.0; // the prompt: a header, lines of words read one by one, an input row with a send key { float c = ci(p.x) - 5.0, r = ri(p.y) - 12.0; if (c >= 0.0 && c <= 18.0 && r >= 0.0 && r <= 20.0) { if (r == 0.0) v += (c <= 1.0) ? 1.05 : (c >= 3.0 && c <= 8.0) ? 0.42 : 0.0; else if (r >= 2.0 && r <= 15.0) { float ln = r - 2.0; float len = ln == 13.0 ? 8.0 : (ln == 6.0 ? 0.0 : 13.0 + floor(6.0 * hash(vec2(ln, 2.0)))); float gap = mod(c + floor(ln * 2.3), 5.0) == 4.0 && hash(vec2(ln, c)) > 0.25 ? 1.0 : 0.0; float rd = 0.4 + 0.07 * ln; float read = sm(rd, rd + 0.06, s) * (1.0 - sm(rd + 0.16, rd + 0.26, s)); if (c <= len - 1.0 && gap == 0.0) v += 0.42 + 0.4 * read; // the cursor, blinking after the last word if (ln == 13.0 && c == len + 1.0) v += 0.95 * step(0.0, sin(ph * 7.0 + 1.5)); } else if (r == 18.0 || r == 19.0) { if (c >= 16.0) v += 1.05; else if (r == 19.0 && c >= 0.0 && c <= 4.0) v += 0.42; else v += 0.17; } } } // the send: a ring from the send key { float u = clamp(s / 0.45, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += 0.7 * (1.0 - u) * exp(-pow(length(p - vec2(154.5, 290.2)) - 8.0 - 40.0 * u, 2.0) / 30.0); } // the standard company object, always there v += obj(p, 33.0, 7.0, 19.0, 14.0, 12.2, 14.0, 1.0, ph); // the generated objects, each built header first, then row by row v += obj(p, 61.0, 5.0, 17.0, 11.0, 22.6, clamp((ss - 1.8) / 0.70 * 11.0, 0.0, 11.0), env, ph); v += obj(p, 38.0, 26.0, 17.0, 11.0, 14.1, clamp((ss - 2.8) / 0.70 * 11.0, 0.0, 11.0), env, ph); v += obj(p, 78.0, 22.0, 17.0, 11.0, 28.9, clamp((ss - 3.8) / 0.70 * 11.0, 0.0, 11.0), env, ph); // the tokens, only after the prompt is read v += tokens(p, vec2(178.0, 236.0), vec2(420.0, 232.0), vec2(487.0, 50.5), s, 1.3, 1.8); v += tokens(p, vec2(178.0, 236.0), vec2(250.0, 290.0), vec2(326.0, 247.9), s, 2.3, 2.8); v += tokens(p, vec2(178.0, 236.0), vec2(420.0, 232.0), vec2(606.0, 210.3), s, 3.3, 3.8); // the relations, each only once both its ends exist v += rel(p, vec2(364.5, 112.0), vec2(427.5, 100.0), clamp((ss - 2.5) / 0.4, 0.0, 1.0), env); v += rel(p, vec2(300.0, 196.2), vec2(318.0, 243.2), clamp((ss - 3.5) / 0.4, 0.0, 1.0), env); v += rel(p, vec2(530.0, 149.2), vec2(592.0, 205.6), clamp((ss - 4.5) / 0.4, 0.0, 1.0), env); v += rel(p, vec2(385.5, 296.0), vec2(546.5, 272.0), clamp((ss - 4.7) / 0.4, 0.0, 1.0), env); // the field strip: company's fields always, the rest one bar per field as each row forms if (p.y > 360.0) { for (int i = 0; i < 12; i++) v += slot(p, float(i), 1.0); for (int i = 0; i < 9; i++) v += slot(p, 14.0 + float(i), env * clamp(clamp((ss - 1.8) / 0.70 * 11.0, 0.0, 11.0) - 2.0 - float(i), 0.0, 1.0)); for (int i = 0; i < 9; i++) v += slot(p, 25.0 + float(i), env * clamp(clamp((ss - 2.8) / 0.70 * 11.0, 0.0, 11.0) - 2.0 - float(i), 0.0, 1.0)); for (int i = 0; i < 9; i++) v += slot(p, 36.0 + float(i), env * clamp(clamp((ss - 3.8) / 0.70 * 11.0, 0.0, 11.0) - 2.0 - float(i), 0.0, 1.0)); } return v; }
Fig. 01A prompt turns into a working data model

Running in fintech, logistics, education, climate, healthcare, media, recruiting and real estate.

The problem

A custom CRM gives your org an edge, but building one comes with tradeoffs

Tradeoffs On the left a sealed monolith with a queue of changes at its wall: one goes in after six weeks and comes out as a release. On the right a hand-built stack of layers: a patch lands on top and cracks a layer below, which has to be welded back. // a filled rectangle with soft edges float rect(vec2 p, vec2 a, vec2 b, float k) { vec2 c = (a + b) * 0.5, h = (b - a) * 0.5; vec2 d = abs(p - c) - h; return k * (1.0 - sm(-1.0, 3.0, max(d.x, d.y))); } // a slot of a strip, filled from the baseline up float bar(vec2 p, float x0, float top, float base) { return step(abs(p.x - x0 - 3.5), 4.5) * step(top, p.y) * step(p.y, base) * (0.62 + 0.5 * step(p.y, top + 5.0)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float v = 0.0; // THE MONOLITH: a bright sealed rim, a dense opaque body with faint module seams, and a core that stirs only while a // change is inside it vec2 A = vec2(140.0, 84.0), B = vec2(310.0, 368.0); float body = rect(p, A, B, 1.0); float inner = rect(p, A + 16.0, B - 16.0, 1.0); float gapRing = rect(p, A + 8.0, B - 8.0, 1.0) - inner; float rim = body - inner - gapRing; float inside = sm(0.55, 0.75, t) * (1.0 - sm(1.25, 1.55, t)); float churn = noise(p * 0.035 + vec2(1.6 * cos(ph), 1.6 * sin(ph))); float seam = step(abs(mod(p.y - 84.0 + 5.0, 47.0) - 23.5), 1.6) * 0.18; v += rim * 1.08 + gapRing * 0.18; // idle, the body is solid and still: nothing inside can be seen; it churns only while a change is worked on v += inner * (1.1 - 2.6 * seam - 0.9 * inside * churn); v += inner * blob(p, vec2(225.0, 226.0), 58.0, 0.6 * inside * (0.75 + 0.25 * sin(t * 18.0))); // the release gate on the far face, open only while the change comes out float gate = sm(1.2, 1.35, t) * (1.0 - sm(1.75, 1.95, t)); v -= gate * rect(p, vec2(300.0, 226.0), vec2(312.0, 250.0), 0.9); // the queue's floor: a faint ledger line the changes wait on v += step(abs(p.y - 252.0), 2.4) * step(28.0, p.x) * step(p.x, 140.0) * 0.2; // the changes in the queue: the front one enters 0.0 to 0.6 s, the rest move up 0.7 to 1.3 s, a new one joins float mv = ease(clamp((t - 0.7) / 0.6, 0.0, 1.0)); v += blob(p, vec2(mix(36.0, 62.0, mv), 238.0), 10.0, 1.2); v += blob(p, vec2(mix(62.0, 88.0, mv), 238.0), 10.0, 1.2); v += blob(p, vec2(mix(88.0, 114.0, mv), 238.0), 10.0, 1.2); { float u = ease(clamp(t / 0.6, 0.0, 1.0)); float x = mix(114.0, 154.0, u); v += blob(p, vec2(x, 238.0), 10.0, 1.2 * (1.0 - sm(0.3, 0.6, t))); } // the new change at the back of the queue, fading in once the others have moved up v += blob(p, vec2(36.0, 238.0), 10.0, 1.2 * sm(1.3, 1.8, t) * step(0.0, t - 1.3)); // the release: out through the gate, onto the release mark, then live and gone { float u = ease(clamp((t - 1.3) / 0.6, 0.0, 1.0)); float k = sm(1.3, 1.4, t) * (1.0 - sm(2.6, 3.0, t)); v += blob(p, vec2(mix(306.0, 354.0, u), 238.0), 10.0, 1.2 * k); v += k * sm(1.85, 1.95, t) * 0.5 * exp(-pow(length(p - vec2(354.0, 238.0)) - 16.0 - 40.0 * clamp(t - 1.9, 0.0, 0.5), 2.0) / 20.0) * (1.0 - sm(1.9, 2.4, t)); } // the release mark: a small faint pad v += rect(p, vec2(341.0, 252.0), vec2(367.0, 255.0), 0.22); // the week strip: empty slots, then one bar a week; it empties when the waiting change goes in if (p.x > 139.0 && p.x < 262.0 && p.y > 388.0 && p.y < 413.0) { float slot = mod(p.x - 144.0, 21.0); v += slot < 8.0 ? 0.13 : 0.0; } float clr = 1.0 - sm(0.35, 0.6, t); v += max(clr, sm(1.4, 1.52, t)) * bar(p, 144.0, 390.0, 413.0); v += max(clr, sm(2.3, 2.37, t)) * bar(p, 165.0, 390.0, 413.0); v += max(clr, sm(3.1, 3.22, t)) * bar(p, 186.0, 390.0, 413.0); v += max(clr, sm(4.0, 4.07, t)) * bar(p, 207.0, 390.0, 413.0); v += max(clr, sm(4.8, 4.92, t)) * bar(p, 228.0, 390.0, 413.0); v += max(clr, sm(5.7, 5.77, t)) * bar(p, 249.0, 390.0, 413.0); // THE HAND-BUILT STACK: four thin layers, each set a little off the one under it, propped on shims // ui: a row of small panes vec4 ui = vec4(150.0, 172.0, 430.0, 672.0); float pane = step(mod(p.x - ui.z, 30.0), 24.0); v += rect(p, vec2(ui.z, ui.x), vec2(ui.w, ui.y), 0.42 + 0.3 * pane); // api: endpoint blocks with gaps between them vec4 ap = vec4(198.0, 222.0, 418.0, 662.0); float ep = step(mod(p.x - ap.z, 48.0), 36.0); v += rect(p, vec2(ap.z, ap.x), vec2(ap.w, ap.y), 0.2 + 0.55 * ep); // sync jobs: a thin layer with job dashes running along it, cracked by the patch and welded back vec4 sy = vec4(248.0, 270.0, 440.0, 684.0); float run = step(mod(p.x - sy.z - 3.5 - 42.0 * t / T * 3.0, 42.0), 26.0); float crackOpen = sm(1.0, 1.15, t) * (1.0 - sm(2.0, 2.9, t)); float flick = t > 1.1 && t < 2.0 ? 0.6 + 0.4 * sin(t * 90.0) : 1.0; float gap = step(abs(p.x - 556.0 - 3.0 * sin((p.y - sy.x) * 0.6)), 7.0 * crackOpen); float sl = rect(p, vec2(sy.z, sy.x), vec2(sy.w, sy.y), 0.34 + 0.4 * run); v += sl * (1.0 - gap) * mix(1.0, flick, crackOpen); // the weld: bright where the repair closes the crack float weld = sm(2.0, 2.3, t) * (1.0 - sm(2.8, 3.3, t)); v += weld * rect(p, vec2(548.0, sy.x - 3.0), vec2(564.0, sy.y + 3.0), 0.75); // database: a deep body, denser the deeper it goes, with table rows vec4 db = vec4(296.0, 356.0, 424.0, 668.0); float row = step(abs(mod(p.y - db.x, 14.0) - 7.0), 2.2); v += rect(p, vec2(db.z, db.x), vec2(db.w, db.y), 0.36 + 0.42 * clamp((p.y - db.x) / (db.y - db.x), 0.0, 1.0) + 0.16 * row); // shims between the layers, uneven v += rect(p, vec2(452.0, 172.0), vec2(459.0, 198.0), 0.45); v += rect(p, vec2(606.0, 172.0), vec2(613.0, 198.0), 0.45); v += rect(p, vec2(470.0, 222.0), vec2(477.0, 248.0), 0.45); v += rect(p, vec2(640.0, 222.0), vec2(647.0, 248.0), 0.45); v += rect(p, vec2(448.0, 270.0), vec2(455.0, 296.0), 0.45); v += rect(p, vec2(596.0, 270.0), vec2(603.0, 296.0), 0.45); // the shock running down from the patch once it lands { float g = t - 0.9; if (g > 0.0 && g < 0.4) v += 0.5 * (1.0 - g / 0.4) * exp(-pow(length((p - vec2(560.0, 150.0)) * vec2(0.5, 1.0)) - 160.0 * g, 2.0) / 30.0); } // the patch: appears above, drops onto the top layer, rests, then merges into it { float u = clamp((t - 0.4) / 0.5, 0.0, 1.0); float y = mix(70.0, 138.0, 1.0 - pow(1.0 - u, 2.0)); float k = sm(0.2, 0.4, t) * (1.0 - sm(3.6, 4.2, t)); v += rect(p, vec2(536.0, y - 8.0), vec2(584.0, y + 6.0), 1.05 * k); } // the repair log: a bar per repair; the sixth fills when the weld holds, then the log slides one place left float s = ease(clamp((t - 5.4) / 0.6, 0.0, 1.0)); if (p.x > 461.0 && p.x < 584.0 && p.y > 388.0 && p.y < 413.0) { float slot = mod(p.x - 466.0, 21.0); v += slot < 8.0 ? 0.13 : 0.0; } v += 1.0 * (1.0 - sm(0.0, 0.5, s)) * bar(p, 466.0 - 21.0 * s, 390.0, 413.0); v += 1.0 * bar(p, 487.0 - 21.0 * s, 390.0, 413.0); v += 1.0 * bar(p, 508.0 - 21.0 * s, 390.0, 413.0); v += 1.0 * bar(p, 529.0 - 21.0 * s, 390.0, 413.0); v += 1.0 * bar(p, 550.0 - 21.0 * s, 390.0, 413.0); v += sm(2.95, 3.1, t) * bar(p, 571.0 - 21.0 * s, 390.0, 413.0); return v; }
Fig. 02Two ways a custom CRM usually goes

A

The giant monolith

Proprietary languages, slow deploy cycles and logic you can't look inside.

B

The in-house burden

It's fragile. Version one ships fast, but every change after that is a long-term cost.

Stop settling for trade-offs

Assemble, iterate and adapt a sturdy CRM that bends when you need it

Compose your CRM and internal tools with one extensibility toolkit.

01Production-grade from day one

A recruiting platform used Corvane as the framework for its data model, permissions, sign-in and workflow engine, instead of rebuilding each one itself.

Head of Engineering, recruiting platform

02AI for quick iterations

An advisory firm used coding agents to turn what is usually weeks of migration work into something a single person could oversee in days.

Founding Partner, advisory firm

03Control without drag

An education group moved to a self-hosted Corvane instance: no vendor risk, no forced migration, and CRM costs cut by more than 90%.

CRM Engineer, education group

How it works

Three steps from blank repo to a CRM you own

01

Begin with production-grade building blocks

Compose your CRM and internal apps with one extensibility toolkit: data model, layout and automation.

Building blocks Three stacked tiers, a data model table, a layout of record cards and an automation workflow: a record is written into the table field by field, appears as the first card in the view, and its change fires a workflow that runs three steps in turn. float rc(vec2 p, float x0, float y0, float x1, float y1) { return step(x0, p.x) * step(p.x, x1) * step(y0, p.y) * step(p.y, y1); } // one row of the table, its top at y, its fields written to w (0 to 1 each) and lit by hi float row(vec2 p, float y, vec4 w, float hi, float ph) { float v = 0.0; v += rc(p, 156.0, y + 2.0, 156.0 + 79.1 * w[0], y + 13.0); v += rc(p, 264.0, y + 2.0, 264.0 + 78.1 * w[1], y + 13.0); v += rc(p, 404.0, y + 2.0, 404.0 + 92.0 * w[2], y + 13.0); v += rc(p, 536.0, y + 2.0, 536.0 + 49.0 * w[3], y + 13.0); float shim = noise(p * 0.08 + vec2(3.0 * cos(ph), 3.0 * sin(ph))); return v * (0.62 + hi * (0.45 + 0.2 * shim)); } // one card of the view, its left edge at x: a title, two lines of text, a status pill and an owner dot float card(vec2 p, float x, float a, float hi) { float y = 198.0; float v = rc(p, x, y, x + 86.0, 280.0) * (0.2 + 0.06 * hi); v += rc(p, x + 6.0, y + 8.0, x + 60.0, y + 18.0) * (0.72 + 0.4 * hi); v += rc(p, x + 6.0, y + 28.0, x + 76.0, y + 38.0) * (0.42 + 0.35 * hi); v += rc(p, x + 6.0, y + 42.0, x + 52.0, y + 50.0) * (0.42 + 0.35 * hi); v += rc(p, x + 6.0, y + 62.0, x + 36.0, y + 73.0) * (0.7 + 0.35 * hi); v += blob(p, vec2(x + 72.0, y + 67.0), 6.5, 0.9 + 0.25 * hi); return v * a; } // one workflow step: a frame with its parts inside; idle it is faint, lit it is dense, done it settles to the second shade float stepBlock(vec2 p, float cx, float lit, float done, float ph) { vec2 d = abs(p - vec2(cx, 366.0)); float inside = step(d.x, 38.0) * step(d.y, 20.0); float rim = inside * (1.0 - step(d.x, 31.0) * step(d.y, 11.0)); float churn = noise(p * 0.1 + vec2(2.0 * cos(ph) + cx, 2.0 * sin(ph))); float k = lit * (1.0 - done) * (0.85 + 0.35 * churn) + done * 0.62; float v = rim * (0.36 + 0.6 * k); v += rc(p, cx - 24.0, 358.0, cx + 18.0, 368.0) * (0.3 + 0.8 * k); v += rc(p, cx - 24.0, 372.0, cx + 4.0, 379.0) * (0.22 + 0.55 * k); return v; } float pkt(vec2 p, vec2 a, vec2 b, float t, float t0, float t1) { if (t < t0 || t > t1) return 0.0; float u = (t - t0) / (t1 - t0); float env = sm(0.0, 0.12, u) * (1.0 - sm(0.88, 1.0, u)); return env * blob(p, mix(a, b, ease(u)), 7.5, 1.35); } float ring(vec2 p, vec2 c, float t, float t0, float dur, float rmax, float k) { if (t < t0 || t > t0 + dur) return 0.0; float u = (t - t0) / dur; return k * (1.0 - u) * exp(-pow(length(p - c) - 12.0 - rmax * u, 2.0) / 24.0); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float v = 0.0; // the api: a small dense source, listening v += blob(p, vec2(72.0, 72.0), 14.0, 1.05 + 0.15 * sin(ph)); v += rc(p, 52.0, 52.0, 92.0, 92.0) * 0.16; // the data model: header rule, faint column guides, rows v += rc(p, 150.0, 53.0, 640.0, 59.0) * 0.62; v += rc(p, 150.0, 56.0, 640.0, 158.0) * 0.12; float s = ease(clamp((t - 0.6) / 0.35, 0.0, 1.0)); float y0 = 64.0; for (int k = 0; k < 5; k++) { float fk = float(k); float y = y0 + (fk + s) * 18.8; float a = k == 4 ? 1.0 - s : 1.0; float hi = k == 0 ? 1.0 - s : 0.0; v += a * row(p, y, vec4(1.0), hi, ph); } if (t > 1.0) { vec4 w = clamp((vec4(t) - vec4(1.0, 1.2, 1.4, 1.6)) / 0.2, 0.0, 1.0); v += row(p, y0, w, 1.0, ph); // the write cursor at the end of the field being written float c = clamp((t - 1.0) / 0.2, 0.0, 3.999); int ci = int(floor(c)); float cx = ci == 0 ? 156.0 + 79.1 * w[0] : ci == 1 ? 264.0 + 78.1 * w[1] : ci == 2 ? 404.0 + 92.0 * w[2] : ci == 3 ? 536.0 + 49.0 * w[3] : 0.0; v += (1.0 - sm(1.75, 1.85, t)) * blob(p, vec2(cx, y0 + 7.5), 6.5, 1.1); } v += pkt(p, vec2(90.0, 72.0), vec2(150.0, y0 + 7.5), t, 0.6, 1.0); // the record goes down to the layout v += pkt(p, vec2(193.0, y0 + 7.5), vec2(193.0, 238.0), t, 2.0, 2.5); // the layout: cards move right one, the newest arrives first float s2 = ease(clamp((t - 2.0) / 0.4, 0.0, 1.0)); for (int j = 0; j < 5; j++) { float fj = float(j); float x = 150.0 + (fj + s2) * 100.0; float a = j == 4 ? 1.0 - sm(0.0, 0.6, s2) : 1.0; float hi = j == 0 ? 1.0 - s2 : 0.0; v += card(p, x, a, hi) * step(p.x, 662.0); } v += card(p, 150.0, sm(2.4, 2.65, t), sm(2.4, 2.65, t)); // the change goes down to the trigger v += pkt(p, vec2(193.0, 238.0), vec2(193.0, 366.0), t, 2.65, 3.1); // the automation, read at t + T before 1 s so the finished run is held across the seam float tt = t < 1.0 ? t + T : t; float idle = 1.0 - sm(T + 0.3, T + 0.6, tt); // the connectors v += rc(p, 193.0, 363.0, 552.0, 369.0) * 0.2; // the trigger: a dense round node that fires float fired = sm(3.05, 3.15, tt) * idle; v += blob(p, vec2(193.0, 366.0), 16.0, 0.9 + 0.35 * fired); v += ring(p, vec2(193.0, 366.0), tt, 3.1, 0.45, 40.0, 0.7); { float lit = sm(3.5, 3.5, tt) * idle; float done = sm(3.8, 4.0, tt) * idle; v += stepBlock(p, 330.0, lit, done, ph); // the token along the connector, and the line behind it lit v += pkt(p, vec2(207.0, 366.0), vec2(292.0, 366.0), tt, 3.1, 3.5); float u = clamp((tt - 3.1) / 0.4, 0.0, 1.0); v += rc(p, 207.0, 363.0, mix(207.0, 292.0, ease(u)), 369.0) * 0.4 * idle * step(3.1, tt); // the run log: a segment that grows while the step works float g = clamp((tt - 3.5) / 0.4, 0.0, 1.0) * idle; v += rc(p, 292.0, 404.0, 292.0 + 70.0 * g, 414.0) * (0.7 + 0.35 * (1.0 - done)); v += ring(p, vec2(330.0, 366.0), tt, 3.5, 0.35, 34.0, 0.45); } { float lit = sm(4.2, 4.2, tt) * idle; float done = sm(4.5, 4.8, tt) * idle; v += stepBlock(p, 460.0, lit, done, ph); // the token along the connector, and the line behind it lit v += pkt(p, vec2(368.0, 366.0), vec2(422.0, 366.0), tt, 3.9, 4.2); float u = clamp((tt - 3.9) / 0.3, 0.0, 1.0); v += rc(p, 368.0, 363.0, mix(368.0, 422.0, ease(u)), 369.0) * 0.4 * idle * step(3.9, tt); // the run log: a segment that grows while the step works float g = clamp((tt - 4.2) / 0.4, 0.0, 1.0) * idle; v += rc(p, 422.0, 404.0, 422.0 + 70.0 * g, 414.0) * (0.7 + 0.35 * (1.0 - done)); v += ring(p, vec2(460.0, 366.0), tt, 4.2, 0.35, 34.0, 0.45); } { float lit = sm(4.9, 4.9, tt) * idle; float done = sm(5.2, 5.5, tt) * idle; v += stepBlock(p, 590.0, lit, done, ph); // the token along the connector, and the line behind it lit v += pkt(p, vec2(498.0, 366.0), vec2(552.0, 366.0), tt, 4.6, 4.9); float u = clamp((tt - 4.6) / 0.3, 0.0, 1.0); v += rc(p, 498.0, 363.0, mix(498.0, 552.0, ease(u)), 369.0) * 0.4 * idle * step(4.6, tt); // the run log: a segment that grows while the step works float g = clamp((tt - 4.9) / 0.4, 0.0, 1.0) * idle; v += rc(p, 552.0, 404.0, 552.0 + 70.0 * g, 414.0) * (0.7 + 0.35 * (1.0 - done)); v += ring(p, vec2(590.0, 366.0), tt, 4.9, 0.35, 34.0, 0.45); } // the log's empty track v += rc(p, 292.0, 404.0, 622.0, 414.0) * 0.13; return v; }

02

Keep iterating without friction

Customize without limits using the AI coding tools you already use. Adapt your CRM to the way your business grows and wins.

Iterate A code diff is typed line by line, a strip of tests passes one by one, and only then the live records table gains a new column that fills row by row, before the table scrolls on for the next change. // the test strip's state for one change: ticks light in turn, all fade at its merge float ticks(vec2 p, float tt, float s) { float k = floor((p.x - 35.5) / 21.0); if (k < 0.0 || k > 11.0) return 0.0; float lx = p.x - 39.0 - 21.0 * k; if (lx < -3.5 || lx > 10.5) return 0.0; float at = s + 1.85 + 0.075 * k; float lit = sm(at, at + 0.04, tt) * (1.0 - sm(s + 4.3, s + 4.55, tt)); float fresh = lit * exp(-max(tt - at, 0.0) / 0.1); return lit * (0.95 + 0.4 * fresh); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float v = 0.0; // the second change's own clock: it was typed in the last cycle's second half, so before 4 s it is read at t + T float t1 = t < 4.0 ? t + T : t; // ---- the diff: line numbers, context lines, and the hunk each change adds if (p.x > 30.0 && p.x < 300.0 && p.y > 80.0 && p.y < 322.0) { float m = floor((p.y - 3.5) / 9.4 + 0.5); float i = (m - 9.0) / 2.0; if (i >= 0.0 && i <= 12.0 && fract(i) < 0.01) { // the line number gutter, a short faint tick on every line v += 0.22 * step(abs(p.x - 39.0), 3.5); if (abs(i - 0.0) < 0.1) v += 0.3 * step(67.0, p.x) * step(p.x, 199.0); if (abs(i - 1.0) < 0.1) v += 0.3 * step(67.0, p.x) * step(p.x, 163.0); if (abs(i - 2.0) < 0.1) v += 0.3 * step(81.0, p.x) * step(p.x, 249.0); if (abs(i - 3.0) < 0.1) v += 0.3 * step(81.0, p.x) * step(p.x, 199.0); if (abs(i - 4.0) < 0.1) v += 0.3 * step(95.0, p.x) * step(p.x, 241.0); if (abs(i - 10.0) < 0.1) v += 0.3 * step(95.0, p.x) * step(p.x, 199.0); if (abs(i - 11.0) < 0.1) v += 0.3 * step(81.0, p.x) * step(p.x, 141.0); if (abs(i - 12.0) < 0.1) v += 0.3 * step(67.0, p.x) * step(p.x, 97.0); } { // the first change: typed from 0.75 s, merged at 4.3 s float gone = 1.0 - sm(4.3, 4.6, t); if (abs(p.y - 182.1) < 4.0) { float u = clamp((t - 0.75) / 0.17, 0.0, 1.0); float x0 = 95.0; float head = x0 + 158.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } if (abs(p.y - 200.9) < 4.0) { float u = clamp((t - 0.95) / 0.17, 0.0, 1.0); float x0 = 109.0; float head = x0 + 120.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } if (abs(p.y - 219.7) < 4.0) { float u = clamp((t - 1.15) / 0.17, 0.0, 1.0); float x0 = 109.0; float head = x0 + 176.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } if (abs(p.y - 238.5) < 4.0) { float u = clamp((t - 1.35) / 0.17, 0.0, 1.0); float x0 = 109.0; float head = x0 + 94.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } if (abs(p.y - 257.3) < 4.0) { float u = clamp((t - 1.55) / 0.17, 0.0, 1.0); float x0 = 95.0; float head = x0 + 64.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } } { // the second change: typed from 4.75 s, merged at 8.3 s (0.3 s into the next cycle) float gone = 1.0 - sm(8.3, 8.6, t1); if (abs(p.y - 182.1) < 4.0) { float u = clamp((t1 - 4.75) / 0.17, 0.0, 1.0); float x0 = 95.0; float head = x0 + 140.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } if (abs(p.y - 200.9) < 4.0) { float u = clamp((t1 - 4.95) / 0.17, 0.0, 1.0); float x0 = 109.0; float head = x0 + 172.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } if (abs(p.y - 219.7) < 4.0) { float u = clamp((t1 - 5.15) / 0.17, 0.0, 1.0); float x0 = 109.0; float head = x0 + 108.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } if (abs(p.y - 238.5) < 4.0) { float u = clamp((t1 - 5.35) / 0.17, 0.0, 1.0); float x0 = 95.0; float head = x0 + 150.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } if (abs(p.y - 257.3) < 4.0) { float u = clamp((t1 - 5.55) / 0.17, 0.0, 1.0); float x0 = 95.0; float head = x0 + 78.0 * u; if (u > 0.0) { v += gone * 1.05 * step(abs(p.x - 53.0), 3.0); v += gone * 0.72 * step(x0, p.x) * step(p.x, head); if (u < 1.0) v += 1.1 * step(abs(p.x - head - 4.0), 4.5); } } } } // ---- the test strip, its empty slots and its baseline if (p.y > 366.0 && p.y < 393.0) { float k = floor((p.x - 35.5) / 21.0); float lx = p.x - 39.0 - 21.0 * k; if (k >= 0.0 && k <= 11.0 && lx > -3.5 && lx < 10.5) v += 0.17; v += ticks(p, t, 0.0) + ticks(p, t1, 4.0); } // ---- the deploy: a packet up the lane and along the top to the new column, once the tests have passed for (int d = 0; d < 2; d++) { float s = float(d) * 4.0; float tt = d == 0 ? t : t1; float u = clamp((tt - s - 2.80) / 0.40, 0.0, 1.0); if (u > 0.0 && u < 1.0) { float L1 = 30.0, L2 = 328.0, L3 = 327.0; float q = ease(u) * (L1 + L2 + L3); vec2 c = q < L1 ? vec2(289.0 + q, 380.0) : (q < L1 + L2 ? vec2(319.0, 380.0 - (q - L1)) : vec2(319.0 + (q - L1 - L2), 52.0)); v += blob(p, c, 8.0, 1.4); // its wake along the lane v += 0.25 * (1.0 - u) * step(abs(p.x - 319.0), 3.0) * step(52.0, p.y) * step(p.y, 380.0) * step(380.0 - (q - L1), p.y); } } // ---- the records table: columns that scroll one place left as each change merges float S = ease(clamp((t - 0.3) / 0.4, 0.0, 1.0)) + ease(clamp((t - 4.3) / 0.4, 0.0, 1.0)); if (p.x > 312.0 && p.x < 676.0 && p.y > 78.0 && p.y < 358.0) { float gf = (p.x - 340.0) / 56.0 + S; float g = floor(gf); float lx = (gf - g) * 56.0; float kind = mod(g, 2.0); // the column leaving on the left fades as it passes the table's edge float clip = sm(322.0, 338.0, p.x); float m = floor((p.y - 3.5) / 9.4 + 0.5); // a column is live once it exists: up to 5 from the start, 6 from the first deploy, 7 from the second float base = g == 6.0 ? 3.2 : 7.2; float isNew = step(5.5, g); // the header row if (abs(m - 9.0) < 0.1) { float at = isNew > 0.0 ? base - 0.04 : -1.0; float on = isNew > 0.0 ? sm(at, at + 0.03, t) : 1.0; float fresh = isNew > 0.0 ? on * exp(-max(t - at, 0.0) / 0.12) : 0.0; float hl = kind < 0.5 ? 30.0 : 22.0; float hx = kind < 0.5 ? 2.0 : 44.0 - hl; float inCell = step(hx, lx) * step(lx, hx + hl); v += clip * inCell * mix(0.2, 0.95 + 0.4 * fresh, on); } // the dotted rules between rows if (fract((m - 10.0) / 2.0) < 0.01 && m >= 10.0 && m <= 36.0 && lx < 50.0) { v += clip * 0.16 * step(0.5, mod(floor(p.x / 7.0), 2.0)); } // the rows: a text column holds left-aligned names, a number column right-aligned figures float r = (m - 11.0) / 2.0; if (r >= 0.0 && r <= 13.0 && fract(r) < 0.01) { float h = hash(vec2(r, kind)); float len = kind < 0.5 ? 14.0 + 30.0 * h : 10.0 + 18.0 * h; float x0 = kind < 0.5 ? 2.0 : 44.0 - len; float inCell = step(x0, lx) * step(lx, x0 + len); float at = isNew > 0.0 ? base + 0.04 * r : -1.0; float on = isNew > 0.0 ? sm(at, at + 0.03, t) : 1.0; float fresh = isNew > 0.0 ? on * exp(-max(t - at, 0.0) / 0.09) : 0.0; float shade = kind < 0.5 ? 0.44 : 0.72; // an empty slot shows a faint placeholder tick until its value is written float empty = (1.0 - on) * 0.18 * step(2.0, lx) * step(lx, 9.0); v += clip * (inCell * on * (shade + 0.6 * fresh) + empty); } } // ---- the live dot beside the table's name, beating twice a cycle v += blob(p, vec2(424.0, 378.0), 5.0, 0.75 + 0.35 * sin(2.0 * ph)); return v; }

03

Stay in control with open-source software

Don't get locked into someone else's ecosystem. Building on Corvane looks like normal software: local setup, real data, live testing and no proprietary tooling.

Self hosted A closed boundary of your own servers holding a local instance and a production instance; a copy of real data moves to local, a change is tested there cell by cell, then promoted to production, where the racks roll it out and the database migrates, and nothing crosses the boundary. float rbox(vec2 p, vec2 c, vec2 b, float r) { vec2 q = abs(p - c) - b + vec2(r); return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; } // a lane: one cell row tall, from x1 to x2 at y, snapped to the grid so it never falls between samples float lane(vec2 p, float x1, float x2, float y, float k) { float cy = 3.5 + 9.4 * floor((y - 3.5) / 9.4 + 0.5); return k * step(abs(p.y - cy), 4.6) * step(x1, p.x) * step(p.x, x2); } 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 ph = TAU * t / T; float v = 0.0; // the boundary: a dense rim with traffic running round it, inside only float d = rbox(p, vec2(360.0, 230.0), vec2(330.0, 176.0), 22.0); float a = atan((p.y - 230.0) / 176.0, (p.x - 360.0) / 330.0); float run = pow(0.5 + 0.5 * cos(3.0 * a - ph), 8.0); v += (0.4 + 0.62 * run) * exp(-d * d / 30.0); // a faint inner ring just inside it v += 0.16 * exp(-pow(d + 12.0, 2.0) / 5.0); // LOCAL: the diff panel, context lines faint, a gutter of plus marks on the added lines float row = floor((p.y - 145.0) / 9.4); if (p.y > 145.0 && p.y < 145.0 + 9.4 * 9.0) { if (row == 0.0) v += 0.42 * step(90.0, p.x) * step(p.x, 210.0); if (row == 2.0) v += 0.42 * step(104.0, p.x) * step(p.x, 196.0); if (row == 4.0) v += 0.42 * step(104.0, p.x) * step(p.x, 244.0); if (row == 6.0) { float typed = clamp((t - 1.05) / 0.25, 0.0, 1.0) * (1.0 - sm(4.7, 5.1, t)); float on = step(0.001, typed); v += 0.95 * on * step(104.0, p.x) * step(p.x, 104.0 + 118.0 * typed); v += 1.05 * on * step(abs(p.x - 81.0), 2.0); } if (row == 8.0) { float typed = clamp((t - 1.3) / 0.25, 0.0, 1.0) * (1.0 - sm(4.7, 5.1, t)); float on = step(0.001, typed); v += 0.95 * on * step(118.0, p.x) * step(p.x, 118.0 + 84.0 * typed); v += 1.05 * on * step(abs(p.x - 81.0), 2.0); } } // LOCAL: the test strip, eight cells two columns wide; the last run fades as the next snapshot starts if (p.y > 250.0 && p.y < 270.0 && p.x > 90.0 && p.x < 254.0) { float c = floor((p.x - 4.0) / 7.0 + 0.5) - 13.0; float i = floor(c / 3.0); float inCell = step(mod(c, 3.0), 1.5) * step(0.0, i) * step(i, 7.0); float lit = 0.0; if (i == 0.0) lit = t < 1.0 ? 1.0 - sm(0.25, 0.55, t) : sm(1.6, 1.68, t); if (i == 1.0) lit = t < 1.0 ? 1.0 - sm(0.25, 0.55, t) : sm(1.77, 1.85, t); if (i == 2.0) lit = t < 1.0 ? 1.0 - sm(0.25, 0.55, t) : sm(1.94, 2.02, t); if (i == 3.0) lit = t < 1.0 ? 1.0 - sm(0.25, 0.55, t) : sm(2.11, 2.19, t); if (i == 4.0) lit = t < 1.0 ? 1.0 - sm(0.25, 0.55, t) : sm(2.28, 2.36, t); if (i == 5.0) lit = t < 1.0 ? 1.0 - sm(0.25, 0.55, t) : sm(2.45, 2.53, t); if (i == 6.0) lit = t < 1.0 ? 1.0 - sm(0.25, 0.55, t) : sm(2.62, 2.7, t); if (i == 7.0) lit = t < 1.0 ? 1.0 - sm(0.25, 0.55, t) : sm(2.79, 2.87, t); v += inCell * (0.2 + 0.62 * lit); } // LOCAL: the copy of the data, an oval that fills as the snapshot lands { float dd = length(vec2((p.x - 170.0) / 72.0, (p.y - 318.0) / 22.0)); float swell = sm(0.9, 1.0, t) * (1.0 - sm(1.0, 1.6, t)); float deep = clamp((p.y - 296.0) / 44.0, 0.0, 1.0); v += step(dd, 1.0) * (0.26 + 0.34 * deep + 0.4 * swell) + 0.3 * exp(-pow(dd - 1.0, 2.0) / 0.006); } // the lanes between the two instances, all inside the boundary v += lane(p, 264.0, 440.0, 180.0, 0.3); v += lane(p, 246.0, 452.0, 318.0, 0.3); // the snapshot: production to local, along the lower lane { float u = clamp((t - 0.1) / 0.85, 0.0, 1.0); v += blob(p, mix(vec2(446.0, 318.0), vec2(248.0, 318.0), ease(u)), 10.0, 1.1 * sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u))); } // the change: local to production, along the upper lane { float u = clamp((t - 3.05) / 0.7, 0.0, 1.0); v += blob(p, mix(vec2(262.0, 180.0), vec2(440.0, 180.0), ease(u)), 10.0, 1.1 * sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u))); } // PROD: four rack rows, each a band with drive lights at its right end, lit in turn as it takes the change if (p.x > 444.0 && p.x < 628.0 && p.y > 122.0 && p.y < 204.0) { float j = floor((p.y - 122.0) / 20.5); float inRow = step(mod(p.y - 122.0, 20.5), 13.0); float take = 0.0; if (j == 0.0) take = sm(3.8, 3.88, t) * (1.0 - sm(4.05, 4.4, t)); if (j == 1.0) take = sm(3.98, 4.06, t) * (1.0 - sm(4.23, 4.58, t)); if (j == 2.0) take = sm(4.16, 4.24, t) * (1.0 - sm(4.41, 4.76, t)); if (j == 3.0) take = sm(4.34, 4.42, t) * (1.0 - sm(4.59, 4.94, t)); float slot = step(mod(p.x - 444.0, 28.0), 21.0); float led = step(596.0, p.x) * step(0.55, hash(floor(p / vec2(7.0, 9.4)) + j)); v += inRow * (0.3 + 0.12 * slot + 0.5 * led * (0.6 + 0.4 * sin(ph * 2.0 + j * 1.7)) + 0.62 * take); } // PROD: the database, a crisp rim over a body that darkens with depth and a churning core { float dd = length(vec2((p.x - 536.0) / 88.0, (p.y - 300.0) / 44.0)); float deep = clamp((p.y - 256.0) / 88.0, 0.0, 1.0); float churn = noise(p * 0.07 + vec2(1.3 * cos(ph), 1.3 * sin(ph))); v += step(dd, 1.0) * (0.22 + 0.5 * deep) + 0.5 * exp(-pow(dd - 1.0, 2.0) / 0.004); v += blob(p, vec2(536.0, 312.0), 26.0, 0.45 + 0.4 * (churn - 0.5)); // the migration: a ring out from the rim once the last rack has the change float u = clamp((t - 4.65) / 0.6, 0.0, 1.0); v += 0.75 * sm(0.0, 0.08, u) * (1.0 - u) * exp(-pow(dd - 1.05 - 0.45 * u, 2.0) / 0.004); } return v; }

Skip the clunky UX that usually comes with custom

Give your sales team a CRM they'll actually open

Pipeline board A sales pipeline board with four stage columns of deal cards; a deal is dragged from proposal to won, the won total ticks up, and the deals behind it each move up a stage. // a lift bump while a card is carried float lift(float u, float a, float b) { return sm(a, a + 0.15, u) * (1.0 - sm(b - 0.1, b + 0.05, u)); } // a soft-edged band, at least a cell tall so it never falls between samples float band(vec2 p, float x0, float x1, float y0, float y1) { return sm(x0 - 2.0, x0 + 1.0, p.x) * (1.0 - sm(x1 - 1.0, x1 + 2.0, p.x)) * sm(y0 - 2.0, y0 + 1.0, p.y) * (1.0 - sm(y1 - 1.0, y1 + 2.0, p.y)); } // a deal card: a faint body, a dense title row, an amount row, a pip per stage reached and the owner's avatar float card(vec2 p, vec2 o, float pips, float lf, float a) { if (a <= 0.0) return 0.0; o += vec2(0.0, -5.0 * lf); vec2 q = p - o; if (q.x < -14.0 || q.x > 134.0 || q.y < -14.0 || q.y > 56.0) return 0.0; float v = 0.3 * band(p, o.x, o.x + 120.0, o.y, o.y + 44.0); v += 0.85 * band(p, o.x + 8.0, o.x + 78.0, o.y + 3.0, o.y + 12.5); v += 0.45 * band(p, o.x + 8.0, o.x + 50.0, o.y + 17.0, o.y + 26.5); for (int i = 0; i < 4; i++) { float fi = float(i); float x = o.x + 9.0 + 14.0 * fi; v += (fi < pips ? 0.55 : 0.14) * band(p, x, x + 7.0, o.y + 32.0, o.y + 40.5); } v += blob(p, o + vec2(102.0, 22.0), 9.0, 0.75); v *= 1.0 + 0.3 * lf; // the shadow of a lifted card, on the bed below it v += lf * 0.2 * band(p, o.x - 6.0, o.x + 126.0, o.y + 46.0, o.y + 56.0); return a * v; } // a column's total meter: one segment per deal, filled to n float meter(vec2 p, float x0, float n) { float on = band(p, x0, x0 + 120.0, 84.0, 93.0); if (on <= 0.0) return 0.0; float seg = mod(p.x - x0, 28.0) < 21.0 ? 1.0 : 0.0; float fill = 1.0 - sm(x0 + 28.0 * n - 4.0, x0 + 28.0 * n, p.x); return on * seg * (0.16 + 0.62 * fill); } 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 ph = TAU * t / T; float u = mod(t + 2.0, T); float v = 0.0; // the column beds, a faint field that shimmers slowly v += (0.12 + 0.03 * sin(ph + p.y * 0.045 + 0.7)) * band(p, 62.0, 194.0, 104.0, 336.0); v += (0.12 + 0.03 * sin(ph + p.y * 0.045 + 2.2)) * band(p, 218.0, 350.0, 104.0, 336.0); v += (0.12 + 0.03 * sin(ph + p.y * 0.045 + 3.8)) * band(p, 374.0, 506.0, 104.0, 336.0); v += (0.12 + 0.03 * sin(ph + p.y * 0.045 + 5.4)) * band(p, 530.0, 662.0, 104.0, 336.0); // the totals v += meter(p, 68.0, 3.0 - sm(3.45, 3.75, u) + sm(4.7, 5.0, u)); v += meter(p, 224.0, 2.0 - sm(2.05, 2.35, u) + sm(4.25, 4.55, u)); v += meter(p, 380.0, 2.0 - sm(0.5, 0.8, u) + sm(2.9, 3.2, u)); v += meter(p, 536.0, 3.0 + sm(1.5, 1.8, u) - sm(4.95, 5.25, u)); // the WON total ticks: a flash at its new segment as the deal lands v += blob(p, vec2(634.0, 88.0), 12.0, 0.9 * sm(1.45, 1.55, u) * (1.0 - sm(1.6, 2.0, u))); // the landing ripple round the dropped deal float g = u - 1.4; if (g > 0.0 && g < 0.5) v += 0.5 * (1.0 - g / 0.5) * exp(-pow(length((p - vec2(596.0, 299.0)) * vec2(0.55, 1.0)) - 30.0 - 60.0 * g, 2.0) / 30.0); // the cards float c = 0.0; c = max(c, card(p, mix(vec2(68.0, 116.0), vec2(224.0, 170.0), ease(clamp((u - 3.5) / 0.7, 0.0, 1.0))), 1.0 + 1.0 * step(4.2, u), lift(u, 3.3, 4.3), 1.0)); c = max(c, card(p, mix(vec2(68.0, 170.0), vec2(68.0, 116.0), ease(clamp((u - 4.3) / 0.3, 0.0, 1.0))), 1.0, 0.0, 1.0)); c = max(c, card(p, mix(vec2(68.0, 224.0), vec2(68.0, 170.0), ease(clamp((u - 4.3) / 0.3, 0.0, 1.0))), 1.0, 0.0, 1.0)); c = max(c, card(p, vec2(68.0, 224.0) + vec2(0.0, 12.0 * (1.0 - ease(clamp((u - 4.5) / 0.4, 0.0, 1.0)))), 1.0, 0.0, sm(4.5, 4.85, u))); c = max(c, card(p, mix(vec2(224.0, 116.0), vec2(380.0, 170.0), ease(clamp((u - 2.0) / 0.8, 0.0, 1.0))), 2.0 + 1.0 * step(2.8, u), lift(u, 1.9, 2.9), 1.0)); c = max(c, card(p, mix(vec2(224.0, 170.0), vec2(224.0, 116.0), ease(clamp((u - 2.9) / 0.4, 0.0, 1.0))), 2.0, 0.0, 1.0)); c = max(c, card(p, mix(vec2(380.0, 116.0), vec2(536.0, 278.0), ease(clamp((u - 0.5) / 0.8, 0.0, 1.0))), 3.0 + 1.0 * step(1.3, u), lift(u, 0.3, 1.4), 1.0 - step(1.45, u))); c = max(c, card(p, mix(vec2(380.0, 170.0), vec2(380.0, 116.0), ease(clamp((u - 1.5) / 0.4, 0.0, 1.0))), 3.0, 0.0, 1.0)); c = max(c, card(p, vec2(536.0, 116.0) + vec2(26.0 * ease(clamp((u - 4.9) / 0.4, 0.0, 1.0)), 0.0), 4.0, 0.0, 1.0 - sm(4.9, 5.25, u))); c = max(c, card(p, mix(vec2(536.0, 170.0), vec2(536.0, 116.0), ease(clamp((u - 5.3) / 0.5, 0.0, 1.0))), 4.0, 0.0, 1.0)); c = max(c, card(p, mix(vec2(536.0, 224.0), vec2(536.0, 170.0), ease(clamp((u - 5.3) / 0.5, 0.0, 1.0))), 4.0, 0.0, 1.0)); if (u >= 1.45) c = max(c, card(p, mix(vec2(536.0, 278.0), vec2(536.0, 224.0), ease(clamp((u - 5.3) / 0.5, 0.0, 1.0))), 4.0, 0.0, 1.0)); return v + c; }

Familiar, modern interface

Clean and intuitive, so your sales team feels at home on day one.

Live record One record table open in two views: a value cleared in one view travels through a sync server and clears in the other, then an assistant fills the empty field and the new value lands in both views at once. float rr(float x, float a, float b) { return step(a, x) * step(x, b); } float onRow(vec2 p, float j) { return step(abs(p.y - (3.5 + 9.4 * j)), 1.0); } // a pane: a frame with a title bar, a header rule, and its rows of records float pane(vec2 p, float ox) { float v = 0.0; float inX = rr(p.x, ox - 1.0, ox + 281.0); v += 0.42 * inX * (step(abs(p.x - ox), 1.0) + step(abs(p.x - ox - 280.0), 1.0)) * rr(p.y, 58.0, 316.0); v += 0.7 * inX * (onRow(p, 6.0) + onRow(p, 7.0)); v += 0.42 * inX * onRow(p, 33.0); v += 0.2 * rr(p.x, ox + 8.0, ox + 272.0) * onRow(p, 11.0); // the pane's own screen, faint ticks behind the records v += 0.17 * rr(p.x, ox + 6.0, ox + 274.0) * rr(p.y, 80.0, 306.0); return v; } // a squashed ring going out from c float ring(vec2 p, vec2 c, float t, float t0, float dur, float rmax, float k) { if (t < t0 || t > t0 + dur) return 0.0; float u = (t - t0) / dur; return k * (1.0 - u) * exp(-pow(length((p - c) * vec2(1.0, 1.7)) - 8.0 - rmax * u, 2.0) / 24.0); } // a value travelling from a to b between t0 and t1 float packet(vec2 p, vec2 a, vec2 b, float t, float t0, float t1) { if (t < t0 || t > t1) return 0.0; float u = (t - t0) / (t1 - t0); float k = 1.35 * sm(0.0, 0.08, u) * (1.0 - sm(0.92, 1.0, u)); return blob(p, mix(a, b, ease(u)), 7.5, k); } // a rolling log: a bar enters at the now edge when its event lands, and scrolls one strip a cycle float logBar(vec2 p, float xl, float xr, float te, float h, float t) { float d = mod(t - te, T); float x = xr - 50.4 * d; return step(abs(p.x - x), 4.0) * step(424.0 - h, p.y) * step(p.y, 425.0) * sm(xl, xl + 24.0, x); } float logStrip(vec2 p, float xl, float xr, float ta, float tb, float t) { float v = 0.0; if (p.x < xl - 4.0 || p.x > xr + 5.0 || p.y < 384.0 || p.y > 426.0) return 0.0; // baseline, and a heartbeat tick every half second v += 0.3 * step(abs(p.y - 424.0), 4.0); float hb = mod(p.x - xr + 50.4 * t, 25.2); v += 0.26 * step(hb, 4.5) * step(416.0, p.y) * step(p.y, 424.0); v += 0.95 * logBar(p, xl, xr, ta, 16.0, t); v += 1.05 * logBar(p, xl, xr, tb, 32.0, t); return v; } 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 ph = TAU * t / T; float v = 0.0; v += pane(p, 32.0) + pane(p, 403.0); if (abs(p.y - 125.7) < 1.0) { v += 1.05 * rr(p.x, 44.0, 52.0); v += 0.7 * rr(p.x, 62.0, 106.0); v += 0.42 * rr(p.x, 156.0, 186.0); v += 0.7 * rr(p.x, 228.0, 256.0); } if (abs(p.y - 153.9) < 1.0) { v += 1.05 * rr(p.x, 44.0, 52.0); v += 0.7 * rr(p.x, 62.0, 128.0); v += 0.7 * rr(p.x, 156.0, 200.0); v += 0.7 * rr(p.x, 228.0, 283.0); } if (abs(p.y - 182.1) < 1.0) { v += 1.05 * rr(p.x, 44.0, 52.0); v += 0.7 * rr(p.x, 62.0, 150.0); v += 0.42 * rr(p.x, 156.0, 178.0); v += 0.7 * rr(p.x, 228.0, 274.0); } if (abs(p.y - 210.3) < 1.0) { v += 1.05 * rr(p.x, 44.0, 52.0); v += 0.7 * rr(p.x, 62.0, 117.0); v += 0.42 * rr(p.x, 156.0, 194.0); } if (abs(p.y - 238.5) < 1.0) { v += 1.05 * rr(p.x, 44.0, 52.0); v += 0.7 * rr(p.x, 62.0, 139.0); v += 0.7 * rr(p.x, 156.0, 186.0); v += 0.7 * rr(p.x, 228.0, 256.0); } if (abs(p.y - 266.7) < 1.0) { v += 1.05 * rr(p.x, 44.0, 52.0); v += 0.7 * rr(p.x, 62.0, 106.0); v += 0.42 * rr(p.x, 156.0, 200.0); v += 0.7 * rr(p.x, 228.0, 283.0); } if (abs(p.y - 294.9) < 1.0) { v += 1.05 * rr(p.x, 44.0, 52.0); v += 0.7 * rr(p.x, 62.0, 128.0); v += 0.42 * rr(p.x, 156.0, 178.0); v += 0.7 * rr(p.x, 228.0, 274.0); } if (abs(p.y - 125.7) < 1.0) { v += 1.05 * rr(p.x, 415.0, 423.0); v += 0.7 * rr(p.x, 433.0, 477.0); v += 0.42 * rr(p.x, 527.0, 557.0); v += 0.7 * rr(p.x, 599.0, 627.0); } if (abs(p.y - 153.9) < 1.0) { v += 1.05 * rr(p.x, 415.0, 423.0); v += 0.7 * rr(p.x, 433.0, 499.0); v += 0.7 * rr(p.x, 527.0, 571.0); v += 0.7 * rr(p.x, 599.0, 654.0); } if (abs(p.y - 182.1) < 1.0) { v += 1.05 * rr(p.x, 415.0, 423.0); v += 0.7 * rr(p.x, 433.0, 521.0); v += 0.42 * rr(p.x, 527.0, 549.0); v += 0.7 * rr(p.x, 599.0, 645.0); } if (abs(p.y - 210.3) < 1.0) { v += 1.05 * rr(p.x, 415.0, 423.0); v += 0.7 * rr(p.x, 433.0, 488.0); v += 0.42 * rr(p.x, 527.0, 565.0); } if (abs(p.y - 238.5) < 1.0) { v += 1.05 * rr(p.x, 415.0, 423.0); v += 0.7 * rr(p.x, 433.0, 510.0); v += 0.7 * rr(p.x, 527.0, 557.0); v += 0.7 * rr(p.x, 599.0, 627.0); } if (abs(p.y - 266.7) < 1.0) { v += 1.05 * rr(p.x, 415.0, 423.0); v += 0.7 * rr(p.x, 433.0, 477.0); v += 0.42 * rr(p.x, 527.0, 571.0); v += 0.7 * rr(p.x, 599.0, 654.0); } if (abs(p.y - 294.9) < 1.0) { v += 1.05 * rr(p.x, 415.0, 423.0); v += 0.7 * rr(p.x, 433.0, 499.0); v += 0.42 * rr(p.x, 527.0, 549.0); v += 0.7 * rr(p.x, 599.0, 645.0); } // the edited cell in each view: cleared, then filled with the assistant's value vec2 cA = vec2(254.0, 210.3); vec2 cB = vec2(625.0, 210.3); float lenA = 52.0 * (1.0 - ease(clamp((t - 0.35) / 0.35, 0.0, 1.0)) + ease(clamp((t - 3.2) / 0.25, 0.0, 1.0))); float lenB = 52.0 * (1.0 - ease(clamp((t - 1.45) / 0.15, 0.0, 1.0)) + ease(clamp((t - 3.4) / 0.25, 0.0, 1.0))); float selA = sm(0.25, 0.35, t) * (1.0 - sm(0.8, 1.0, t)) + sm(3.15, 3.2, t) * (1.0 - sm(3.6, 3.85, t)); float selB = sm(1.4, 1.45, t) * (1.0 - sm(1.8, 2.0, t)) + sm(3.35, 3.4, t) * (1.0 - sm(3.8, 4.05, t)); float rowA = onRow(p, 22.0); v += rowA * 1.05 * rr(p.x, 228.0, 228.0 + lenA - 0.5); v += rowA * 1.05 * rr(p.x, 599.0, 599.0 + lenB - 0.5); // the selected cell glows while it is worked on vec2 qa = abs(p - cA) - vec2(34.0, 9.0); vec2 qb = abs(p - cB) - vec2(34.0, 9.0); v += 0.22 * selA * step(max(qa.x, qa.y), 0.0) * (1.0 - rowA); v += 0.22 * selB * step(max(qb.x, qb.y), 0.0) * (1.0 - rowA); // the caret, backspacing the value away in view A float car = sm(0.28, 0.32, t) * (1.0 - sm(0.72, 0.8, t)); v += car * blob(p, vec2(233.0 + lenA, 210.3), 5.0, 1.3); // the sync server: a rim, a core, links to both views and down to the assistant vec2 H = vec2(357.0, 210.3); float dh = length(p - H); v += 0.22 * step(abs(p.y - H.y), 4.0) * (rr(p.x, 316.0, H.x - 22.0) + rr(p.x, H.x + 22.0, 399.0)); v += 0.18 * step(abs(p.x - H.x), 3.0) * rr(p.y, H.y + 24.0, 370.0); float flash = exp(-pow(t - 1.0, 2.0) / 0.004) + exp(-pow(t - 2.82, 2.0) / 0.004); v += 0.5 * exp(-pow(dh - 20.0, 2.0) / 8.0); v += 0.2 * (1.0 - sm(16.0, 18.0, dh)); v += blob(p, H, 11.0, 1.0 + 0.5 * flash); v += ring(p, H, t, 1.0, 0.35, 26.0, 0.6) + ring(p, H, t, 2.82, 0.35, 26.0, 0.6); // the change travels: A to the server, the server to B vec2 eA = vec2(236.0, 210.3); vec2 eB = vec2(607.0, 210.3); v += packet(p, eA, H, t, 0.7, 1.0); v += packet(p, H, eB, t, 1.05, 1.45); v += ring(p, cB, t, 1.45, 0.4, 30.0, 0.5); // the assistant: a hot core with two sparks in orbit, brighter while it works vec2 A = vec2(357.0, 396.0); float work = sm(1.9, 2.05, t) * (1.0 - sm(2.7, 2.95, t)); v += blob(p, A, 11.0, 1.05 + 0.4 * work); v += 0.4 * exp(-pow(length(p - A) - 17.0, 2.0) / 6.0) * (0.6 + work); for (int i = 0; i < 2; i++) { float a = 2.0 * ph + 3.14159 * float(i) + 1.2 * work * sin(ph); v += blob(p, A + vec2(cos(a) * 17.0, sin(a) * 11.0), 4.5, 1.1); } // its answer rises to the server, three packets close behind for (int i = 0; i < 3; i++) { float t0 = 2.1 + 0.16 * float(i); v += packet(p, A - vec2(0.0, 12.0), H + vec2(0.0, 10.0), t, t0, t0 + 0.4); } // the value goes out to both views at once; the near view gets it first v += packet(p, H, eA, t, 2.9, 3.2); v += packet(p, H, eB, t, 2.9, 3.4); v += ring(p, cA, t, 3.2, 0.4, 30.0, 0.5) + ring(p, cB, t, 3.4, 0.4, 30.0, 0.5); // each view's rolling log v += logStrip(p, 46.0, 298.0, 0.35, 3.2, t); v += logStrip(p, 417.0, 669.0, 1.45, 3.4, t); return v; }

Live data, AI built in

Every view updates in real time, with an AI assistant ready to help you move faster.

Command palette A records list with eight rows selected; a key press opens a command palette, typing narrows its actions to export, and the export runs on the eight rows one by one, filling a file line by line. float R(vec2 p, float x0, float x1, float y0, float y1) { return step(x0, p.x) * step(p.x, x1) * step(y0, p.y) * step(p.y, y1); } 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 ph = TAU * t / T; float v = 0.0; // the records list, eight rows selected, each brightening while the action reads it { float a0 = sm(0.85, 0.93, t) * (1.0 - sm(1.07, 1.25, t)); float row = R(p, 32.0, 346.0, 115.0, 141.0); float ln = R(p, 0.0, 999.0, 123.3, 132.7); v += row * (0.17 + 0.3 * a0); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 1.05; v += ln * R(p, 58.0, 128.0, 0.0, 999.0) * (0.7 + 0.4 * a0); v += ln * R(p, 196.0, 236.0, 0.0, 999.0) * (0.42 + 0.4 * a0); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.42 + 0.4 * a0); } { float a1 = sm(1.13, 1.21, t) * (1.0 - sm(1.35, 1.53, t)); float row = R(p, 32.0, 346.0, 144.0, 170.0); float ln = R(p, 0.0, 999.0, 152.3, 161.7); v += row * (0.17 + 0.3 * a1); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 1.05; v += ln * R(p, 58.0, 154.0, 0.0, 999.0) * (0.7 + 0.4 * a1); v += ln * R(p, 196.0, 248.0, 0.0, 999.0) * (0.42 + 0.4 * a1); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.42 + 0.4 * a1); } { float a2 = sm(1.41, 1.49, t) * (1.0 - sm(1.63, 1.81, t)); float row = R(p, 32.0, 346.0, 173.0, 199.0); float ln = R(p, 0.0, 999.0, 181.3, 190.7); v += row * (0.17 + 0.3 * a2); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 1.05; v += ln * R(p, 58.0, 116.0, 0.0, 999.0) * (0.7 + 0.4 * a2); v += ln * R(p, 196.0, 226.0, 0.0, 999.0) * (0.42 + 0.4 * a2); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.42 + 0.4 * a2); } { float a3 = sm(1.69, 1.77, t) * (1.0 - sm(1.91, 2.09, t)); float row = R(p, 32.0, 346.0, 202.0, 228.0); float ln = R(p, 0.0, 999.0, 210.3, 219.7); v += row * (0.17 + 0.3 * a3); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 1.05; v += ln * R(p, 58.0, 142.0, 0.0, 999.0) * (0.7 + 0.4 * a3); v += ln * R(p, 196.0, 242.0, 0.0, 999.0) * (0.42 + 0.4 * a3); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.42 + 0.4 * a3); } { float a4 = sm(1.97, 2.05, t) * (1.0 - sm(2.19, 2.37, t)); float row = R(p, 32.0, 346.0, 231.0, 257.0); float ln = R(p, 0.0, 999.0, 239.3, 248.7); v += row * (0.17 + 0.3 * a4); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 1.05; v += ln * R(p, 58.0, 168.0, 0.0, 999.0) * (0.7 + 0.4 * a4); v += ln * R(p, 196.0, 232.0, 0.0, 999.0) * (0.42 + 0.4 * a4); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.42 + 0.4 * a4); } { float a5 = sm(2.25, 2.33, t) * (1.0 - sm(2.47, 2.65, t)); float row = R(p, 32.0, 346.0, 260.0, 286.0); float ln = R(p, 0.0, 999.0, 268.3, 277.7); v += row * (0.17 + 0.3 * a5); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 1.05; v += ln * R(p, 58.0, 124.0, 0.0, 999.0) * (0.7 + 0.4 * a5); v += ln * R(p, 196.0, 254.0, 0.0, 999.0) * (0.42 + 0.4 * a5); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.42 + 0.4 * a5); } { float a6 = sm(2.53, 2.61, t) * (1.0 - sm(2.75, 2.93, t)); float row = R(p, 32.0, 346.0, 289.0, 315.0); float ln = R(p, 0.0, 999.0, 297.3, 306.7); v += row * (0.17 + 0.3 * a6); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 1.05; v += ln * R(p, 58.0, 148.0, 0.0, 999.0) * (0.7 + 0.4 * a6); v += ln * R(p, 196.0, 240.0, 0.0, 999.0) * (0.42 + 0.4 * a6); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.42 + 0.4 * a6); } { float a7 = sm(2.81, 2.89, t) * (1.0 - sm(3.03, 3.21, t)); float row = R(p, 32.0, 346.0, 318.0, 344.0); float ln = R(p, 0.0, 999.0, 326.3, 335.7); v += row * (0.17 + 0.3 * a7); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 1.05; v += ln * R(p, 58.0, 134.0, 0.0, 999.0) * (0.7 + 0.4 * a7); v += ln * R(p, 196.0, 230.0, 0.0, 999.0) * (0.42 + 0.4 * a7); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.42 + 0.4 * a7); } { float a8 = 0.0; float row = R(p, 32.0, 346.0, 347.0, 373.0); float ln = R(p, 0.0, 999.0, 355.3, 364.7); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 0.22; v += ln * R(p, 58.0, 158.0, 0.0, 999.0) * (0.42 + 0.4 * a8); v += ln * R(p, 196.0, 246.0, 0.0, 999.0) * (0.25 + 0.4 * a8); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.25 + 0.4 * a8); } { float a9 = 0.0; float row = R(p, 32.0, 346.0, 376.0, 402.0); float ln = R(p, 0.0, 999.0, 384.3, 393.7); v += ln * R(p, 37.0, 47.0, 0.0, 999.0) * 0.22; v += ln * R(p, 58.0, 120.0, 0.0, 999.0) * (0.42 + 0.4 * a9); v += ln * R(p, 196.0, 234.0, 0.0, 999.0) * (0.25 + 0.4 * a9); v += ln * R(p, 290.0, 332.0, 0.0, 999.0) * (0.25 + 0.4 * a9); } // the palette: open from CMD K until ENTER float op = t < 2.0 ? 1.0 - sm(0.45, 0.75, t) : sm(4.3, 4.55, t); float pan = R(p, 366.0, 696.0, 64.0, 346.0); float inner = R(p, 373.0, 689.0, 73.4, 336.6); v += op * ((pan - inner) * 0.42 + inner * 0.18); // the footer: a rule, then the hints v += op * R(p, 380.0, 682.0, 307.3, 316.7) * 0.25; v += op * R(p, 380.0, 682.0, 80.0, 102.0) * 0.12; // the caret, after the letters typed so far, blinking six times a cycle float cx = 388.0 + 9.6 * (t < 2.0 ? 6.0 : step(4.75, t) + step(4.89, t) + step(5.03, t) + step(5.17, t) + step(5.31, t) + step(5.45, t)) + 4.0; cx = 4.0 + 7.0 * floor((cx - 4.0) / 7.0 + 0.5); v += op * step(abs(p.x - cx), 3.0) * R(p, 0.0, 999.0, 82.0, 100.0) * step(0.0, sin(6.0 * ph + 1.0)) * 1.05; // the actions, thinning as the query stops matching them { float ln = R(p, 0.0, 999.0, 127.3, 136.7); v += op * (t < 2.0 ? 0.0 : 1.0 - sm(4.80, 5.00, t)) * ln * (0.7 * R(p, 380.0, 394.0, 0.0, 999.0) + 0.25 * R(p, 644.0, 676.0, 0.0, 999.0)); } { float ln = R(p, 0.0, 999.0, 159.3, 168.7); v += op * (t < 2.0 ? 0.0 : 1.0 - sm(4.80, 5.00, t)) * ln * (0.7 * R(p, 380.0, 394.0, 0.0, 999.0) + 0.25 * R(p, 644.0, 676.0, 0.0, 999.0)); } { float ln = R(p, 0.0, 999.0, 191.3, 200.7); v += op * (t < 2.0 ? 0.0 : 1.0 - sm(4.95, 5.15, t)) * ln * (0.7 * R(p, 380.0, 394.0, 0.0, 999.0) + 0.25 * R(p, 644.0, 676.0, 0.0, 999.0)); } { float y = t < 2.0 ? 132.0 : mix(228.0, 132.0, ease(clamp((t - 5.15) / 0.35, 0.0, 1.0))); float hl = t < 2.0 ? 1.0 : sm(5.45, 5.6, t); float fl = exp(-pow(t - 0.36, 2.0) / 0.004); float ln = R(p, 0.0, 999.0, y - 4.7, y + 4.7); v += op * R(p, 372.0, 690.0, y - 13.0, y + 13.0) * (0.2 * hl + 0.5 * fl); v += op * ln * R(p, 380.0, 394.0, 0.0, 999.0) * (0.7 + 0.35 * hl); } { float ln = R(p, 0.0, 999.0, 255.3, 264.7); v += op * (t < 2.0 ? 0.0 : 1.0 - sm(4.95, 5.15, t)) * ln * (0.7 * R(p, 380.0, 394.0, 0.0, 999.0) + 0.25 * R(p, 644.0, 676.0, 0.0, 999.0)); } { float ln = R(p, 0.0, 999.0, 287.3, 296.7); v += op * (t < 2.0 ? 0.0 : 1.0 - sm(4.95, 5.15, t)) * ln * (0.7 * R(p, 380.0, 394.0, 0.0, 999.0) + 0.25 * R(p, 644.0, 676.0, 0.0, 999.0)); } // clear ground behind the palette's lettering: the query, each action name and the footer hints float ey = t < 2.0 ? 132.0 : mix(228.0, 132.0, ease(clamp((t - 5.15) / 0.35, 0.0, 1.0))); float m = R(p, 382.0, 388.0 + 9.6 * (t < 2.0 ? 6.0 : step(4.75, t) + step(4.89, t) + step(5.03, t) + step(5.17, t) + step(5.31, t) + step(5.45, t)) - 2.0, 80.0, 102.0) + R(p, 398.0, 480.0, ey - 8.0, ey + 8.0); m += R(p, 398.0, 480.0, 124.0, 140.0) + R(p, 646.0, 670.0, 134.0, 150.0); m += R(p, 398.0, 480.0, 156.0, 172.0) + R(p, 646.0, 670.0, 166.0, 182.0); m += R(p, 398.0, 480.0, 188.0, 204.0) + R(p, 646.0, 670.0, 198.0, 214.0); m += R(p, 398.0, 480.0, 252.0, 268.0) + R(p, 646.0, 670.0, 262.0, 278.0); m += R(p, 398.0, 480.0, 284.0, 300.0) + R(p, 646.0, 670.0, 294.0, 310.0); m += R(p, 380.0, 470.0, 318.0, 334.0) + R(p, 586.0, 680.0, 318.0, 334.0); v *= 1.0 - op * min(m, 1.0) * R(p, 366.0, 696.0, 64.0, 346.0); // the export file: it appears as the action runs, fills a line per record, then is saved and drops away float fp = sm(0.75, 0.95, t) * (1.0 - sm(3.6, 4.1, t)); float dy = 26.0 * ease(clamp((t - 3.55) / 0.55, 0.0, 1.0)); float fr = R(p, 440.0, 660.0, 112.0 + dy, 372.0 + dy); float fi = R(p, 447.0, 653.0, 121.4 + dy, 362.6 + dy); v += fp * (fr - fi) * 0.42; v += fp * R(p, 0.0, 999.0, 131.3 + dy, 140.7 + dy) * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * 0.42; { float u = clamp((t - 0.90) / 0.3, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(346.0, 128.0), vec2(462.0, 164.0), ease(u)), 6.5, 1.25 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); float fill = sm(1.18, 1.25, t); float L = fp * R(p, 0.0, 999.0, 159.3 + dy, 168.7 + dy); v += L * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * (0.14 + 0.6 * fill); } { float u = clamp((t - 1.18) / 0.3, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(346.0, 157.0), vec2(462.0, 188.0), ease(u)), 6.5, 1.25 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); float fill = sm(1.46, 1.53, t); float L = fp * R(p, 0.0, 999.0, 183.3 + dy, 192.7 + dy); v += L * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * (0.14 + 0.6 * fill); } { float u = clamp((t - 1.46) / 0.3, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(346.0, 186.0), vec2(462.0, 212.0), ease(u)), 6.5, 1.25 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); float fill = sm(1.74, 1.81, t); float L = fp * R(p, 0.0, 999.0, 207.3 + dy, 216.7 + dy); v += L * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * (0.14 + 0.6 * fill); } { float u = clamp((t - 1.74) / 0.3, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(346.0, 215.0), vec2(462.0, 236.0), ease(u)), 6.5, 1.25 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); float fill = sm(2.02, 2.09, t); float L = fp * R(p, 0.0, 999.0, 231.3 + dy, 240.7 + dy); v += L * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * (0.14 + 0.6 * fill); } { float u = clamp((t - 2.02) / 0.3, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(346.0, 244.0), vec2(462.0, 260.0), ease(u)), 6.5, 1.25 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); float fill = sm(2.30, 2.37, t); float L = fp * R(p, 0.0, 999.0, 255.3 + dy, 264.7 + dy); v += L * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * (0.14 + 0.6 * fill); } { float u = clamp((t - 2.30) / 0.3, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(346.0, 273.0), vec2(462.0, 284.0), ease(u)), 6.5, 1.25 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); float fill = sm(2.58, 2.65, t); float L = fp * R(p, 0.0, 999.0, 279.3 + dy, 288.7 + dy); v += L * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * (0.14 + 0.6 * fill); } { float u = clamp((t - 2.58) / 0.3, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(346.0, 302.0), vec2(462.0, 308.0), ease(u)), 6.5, 1.25 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); float fill = sm(2.86, 2.93, t); float L = fp * R(p, 0.0, 999.0, 303.3 + dy, 312.7 + dy); v += L * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * (0.14 + 0.6 * fill); } { float u = clamp((t - 2.86) / 0.3, 0.0, 1.0); if (u > 0.0 && u < 1.0) v += blob(p, mix(vec2(346.0, 331.0), vec2(462.0, 332.0), ease(u)), 6.5, 1.25 * sm(0.0, 0.15, u) * (1.0 - sm(0.85, 1.0, u))); float fill = sm(3.14, 3.21, t); float L = fp * R(p, 0.0, 999.0, 327.3 + dy, 336.7 + dy); v += L * (R(p, 456.0, 520.0, 0.0, 999.0) + R(p, 532.0, 600.0, 0.0, 999.0) + R(p, 612.0, 644.0, 0.0, 999.0)) * (0.14 + 0.6 * fill); } // the keys: CMD and K light together to open the palette, ENTER lights to run float kf = exp(-pow(t - 4.27, 2.0) / 0.006); float ef = exp(-pow(t - 0.36, 2.0) / 0.006); { float kk = R(p, 474.0, 516.0, 404.0, 434.0); float inner = R(p, 481.0, 509.0, 413.4, 424.6); v += (kk - inner) * 0.42 + inner * (0.12 + 0.95 * kf); } { float kk = R(p, 524.0, 558.0, 404.0, 434.0); float inner = R(p, 531.0, 551.0, 413.4, 424.6); v += (kk - inner) * 0.42 + inner * (0.12 + 0.95 * kf); } { float kk = R(p, 590.0, 660.0, 404.0, 434.0); float inner = R(p, 597.0, 653.0, 413.4, 424.6); v += (kk - inner) * 0.42 + inner * (0.12 + 0.95 * ef); } return v; }

Fast path to action

Shortcuts, smart patterns and layouts make everyday work quicker to do.

In production

Dev teams drive company-wide change with Corvane

  1. 12 wkFrom idea to launch

    Recruiting platform

    Ship a product on Corvane

    A hiring team built AI mock interviews at scale, with Corvane as the operational backbone.

    Read the story
  2. -90%CRM costs

    Education group

    Own your CRM end to end

    When its vendor CRM shut down, the group moved to self-hosted Corvane and kept every record.

    Read the story
  3. 3Business lines, one setup

    Climate marketplace

    Grow on a flexible foundation

    One modular Corvane setup runs carbon credits, farm products and industrial systems side by side.

    Read the story

They are the real sales team

The flexibility is what made the difference. Our needs change every week. I spot a new need and two clicks later it's handled. That is a real edge when you are moving fast.
Mara EllisonCo-founder, climate marketplace

It feels like our own code

We test changes locally against a copy of real data, open a pull request and ship. Our CRM finally moves at the same speed as the rest of our product.
Tobin AchterbergStaff engineer, recruiting platform

No more vendor roulette

Our old provider shut down with ninety days' notice. With Corvane we own the stack, the data and the roadmap, and the bill went down instead of up.
Priya RamaswamyHead of operations, education group

Any questions?

Straight answers before you start

Still unsure? Book a call and talk it through with an engineer.

Is Corvane really open-source?

Yes. The whole core is open-source under a permissive license. You can read every line, run it on your own servers and change anything.

How long does it take to get started?

A local instance runs in a few minutes. Most teams have their first custom objects and views in production within a week.

Can I migrate from my current CRM?

Import tools bring over records, relations and history from spreadsheets and the common CRM exports, and an API covers the rest.

Do I need a developer to customize Corvane?

No. Objects, fields, views and workflows can all be set up in the app. Developers can go further in code when they need to.

Can developers extend Corvane with code?

Yes. Define objects, views and logic in typed code, test them locally against real data and ship them like any other software.

Does it work with AI coding assistants?

It does. Everything is plain code and a documented schema, so the assistants your team already uses can read it, write it and test it.

What does Corvane cost?

Self-hosting is free. The managed cloud is priced per seat, with a free tier for small teams.

Stop fighting custom. Start building with Corvane.

Run it locally in a few minutes, then ship your first objects and views when you're ready.