{
  "id": "P125",
  "slug": "colour-mixer",
  "key": "P125-colour-mixer",
  "title": "Colour mixer",
  "summary": "Colours as #RRGGBB mixed, added with clamping, scaled and back, graded and eased one channel at a time in whole numbers, each share truncated and rounded half up side by side.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P125 - Colour mixer\n\nColours as red, green and blue from 0 to 255, written #RRGGBB, mixed,\nadded, scaled, graded and eased one channel at a time in whole numbers,\nwith each share rounded two ways: truncated, and half up.\n\n- `main.eml` - the menu, the palette, and the five operations\n- `colour.eml` - the palette, reading and writing #RRGGBB, the two ways of\n  rounding a share, and the operations\n\nHow each part works:\n\n- As in the corpus case `rgb-palette-blender`, every operation works one\n  channel at a time, and adding clamps each channel at 255 on its own.\n  When some channels clamp and others do not, the balance between them\n  shifts.\n- Mixing w percent of one colour into another: truncation drops the share\n  left over, so it is never above half up and at most 1 below. Black and\n  white half and half: #7F7F7F truncated, #808080 half up.\n- Scaling brightness by f percent and back by 100/f: by 100% or more,\n  every channel that was not clamped comes back exactly; a clamped one is\n  lost. Scaled down, a channel can come back 1 off.\n- A gradient of n colours weights the two ends (n - 1 - k) : k.\n- Easing moves each channel a percentage of the way that is left. It\n  stops short when every step left rounds to 0: truncated, when the gap\n  times the percentage is below 100, and half up, below 50. Black to white\n  at 20% stops at #FBFBFB truncated and #FDFDFD half up.\n\nWhat is checked: menu choices 1 to 7; colours as #RRGGBB or RRGGBB in\neither case, or a palette name; a weight from 0 to 100, a brightness from\n1 to 400, 2 to 12 colours in a gradient, and an easing step from 1 to 99.\nAn empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the palette;\n- black and white half and half, and 30% teal into rust;\n- #C8B4C8 + #3C3C3C (red and blue clamped) and white + red (all three);\n- rust times 200% and back (red lost), and #C9652F times 50% and back;\n- black to white in 5 colours;\n- easing black to white at 20%, and rust to #14A096 at 10%.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- colours: a cancel, pink, #12345G and #1234, then black with a cancel;\n- weights: a cancel, 101 and x; brightness 0 and 401; gradients of 1\n  and 13; easing steps of 0 and 100; easing grey to grey.\n\nBuilt on the verified corpus case `rgb-palette-blender` (colours added and\nscaled one channel at a time, each clamped on its own).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P125 colour mixer: named colours, mixing two, adding with clamping,\n# scaling and back, gradients and easing, each rounded two ways.\nimport colour\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef channels(c):\n    return \"(\" + str(c[0]) + \", \" + str(c[1]) + \", \" + str(c[2]) + \")\"\n\ndef text(c):\n    return colour.hex_text(c) + \" \" + channels(c)\n\ndef ask_colour(prompt):\n    plain(input(prompt)) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return []\n    colour.parse(t) => c\n    if len(c) == 0:\n        \"Type #RRGGBB in hex, such as #C86432, or a colour from the palette.\" ^0\n    return c\n\ndef ask_int(prompt, low, high):\n    plain(input(prompt)) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return 0 - 1\n    if len(t) == 0 or len(t) > 3:\n        (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n        return 0 - 1\n    for ch in t:\n        if ch < \"0\" or ch > \"9\":\n            (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n            return 0 - 1\n    if int(t) < low or int(t) > high:\n        (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n        return 0 - 1\n    return int(t)\n\ndef show_palette():\n    colour.palette() => p\n    \"Palette:\" ^0\n    for name in p:\n        (\"  \" + pad(name, 8) + text(p[name])) ^0\n\ndef names():\n    return [\"red\", \"green\", \"blue\"]\n\ndef two():\n    ask_colour(\"first colour> \") => a\n    if len(a) == 0:\n        return []\n    ask_colour(\"second colour> \") => b\n    if len(b) == 0:\n        return []\n    return [a, b]\n\ndef do_mix():\n    two() => ab\n    if len(ab) == 0:\n        return\n    ask_int(\"percent of the second (0 to 100)> \", 0, 100) => w\n    if w < 0:\n        return\n    colour.mix(ab[0], ab[1], w, \"trunc\") => t\n    colour.mix(ab[0], ab[1], w, \"half\") => h\n    (\"Truncated: \" + text(t)) ^0\n    (\"Half up:   \" + text(h)) ^0\n    [] => lower\n    for k in [0:2]:\n        if t[k] != h[k]:\n            lower + [names()[k]] => lower\n    if len(lower) > 0:\n        (\"Truncation is 1 lower in \" + list_text(lower) + \": the share left over is dropped, never rounded up.\") ^0\n\ndef list_text(xs):\n    if len(xs) == 1:\n        return xs[0]\n    if len(xs) == 2:\n        return xs[0] + \" and \" + xs[1]\n    return xs[0] + \", \" + xs[1] + \" and \" + xs[2]\n\ndef do_add():\n    two() => ab\n    if len(ab) == 0:\n        return\n    colour.add(ab[0], ab[1]) => r\n    (\"Sum: \" + text(r[0])) ^0\n    [] => over\n    for k in [0:2]:\n        if r[1][k] > 255:\n            over + [names()[k] + \" \" + str(r[1][k])] => over\n    if len(over) == 3:\n        (\"Clamped at 255: \" + list_text(over) + \". Every channel is clamped: the sum is white.\") ^0\n    elif len(over) > 0:\n        (\"Clamped at 255: \" + list_text(over) + \". The other channels kept their sums, so the balance between channels shifts.\") ^0\n\ndef do_scale():\n    ask_colour(\"colour> \") => c\n    if len(c) == 0:\n        return\n    ask_int(\"brightness percent (1 to 400)> \", 1, 400) => f\n    if f < 0:\n        return\n    colour.scale(c, f) => r\n    colour.unscale(r[0], f) => back\n    (\"Times \" + str(f) + \"%: \" + text(r[0])) ^0\n    (\"Back, times 100/\" + str(f) + \": \" + text(back)) ^0\n    for k in [0:2]:\n        if back[k] != c[k]:\n            if r[1][k]:\n                (\"  \" + names()[k] + \" was clamped at 255: its \" + str(c[k]) + \" is lost.\") ^0\n            else:\n                (\"  \" + names()[k] + \" comes back as \" + str(back[k]) + \", not \" + str(c[k]) + \": scaling down to \" + str(r[0][k]) + \" rounded the detail away.\") ^0\n    if back == c:\n        \"  Every channel came back.\" ^0\n\ndef do_gradient():\n    two() => ab\n    if len(ab) == 0:\n        return\n    ask_int(\"colours in the gradient (2 to 12)> \", 2, 12) => n\n    if n < 0:\n        return\n    colour.gradient(ab[0], ab[1], n, \"trunc\") => t\n    colour.gradient(ab[0], ab[1], n, \"half\") => h\n    \"  step  truncated                 half up\" ^0\n    for i in [0:n - 1]:\n        (\"  \" + pad(str(i), 6) + pad(text(t[i]), 26) + text(h[i])) ^0\n    0 => diff\n    for i in [0:n - 1]:\n        for k in [0:2]:\n            if t[i][k] != h[i][k]:\n                diff + 1 => diff\n    if diff > 0:\n        (\"  Half up is 1 higher in \" + str(diff) + \" of the \" + str(3 * n) + \" channel values: those where the share left over is a half or more.\") ^0\n\ndef do_ease():\n    two() => ab\n    if len(ab) == 0:\n        return\n    ask_int(\"percent of the way each step (1 to 99)> \", 1, 99) => t\n    if t < 0:\n        return\n    if ab[0] == ab[1]:\n        \"  Already there.\" ^0\n        return\n    for mode in [\"trunc\", \"half\"]:\n        colour.ease(ab[0], ab[1], t, mode, 60) => steps\n        steps[len(steps) - 1] => last\n        \"truncating each step\" => label\n        if mode == \"half\":\n            \"rounding each step half up\" => label\n        if last == ab[1]:\n            (\"  \" + label + \": reaches \" + colour.hex_text(ab[1]) + \" in \" + str(len(steps) - 1) + \" steps\") ^0\n        elif len(steps) == 61:\n            (\"  \" + label + \": still moving after 60 steps, at \" + text(last)) ^0\n        else:\n            (\"  \" + label + \": stops after \" + str(len(steps) - 1) + \" steps at \" + text(last) + \", short of \" + colour.hex_text(ab[1]) + \": the steps left are too small to round to 1\") ^0\n\n\"== Colour mixer ==\" ^0\n\"Colours as red, green and blue from 0 to 255, written #RRGGBB, worked one channel at a time in whole numbers.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_palette()\n    elif choice == \"2\":\n        do_mix()\n    elif choice == \"3\":\n        do_add()\n    elif choice == \"4\":\n        do_scale()\n    elif choice == \"5\":\n        do_gradient()\n    elif choice == \"6\":\n        do_ease()\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import colour\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef channels(c):\n    return \"(\" + str(c[0]) + \", \" + str(c[1]) + \", \" + str(c[2]) + \")\"\n\ndef text(c):\n    return colour.hex_text(c) + \" \" + channels(c)\n\ndef ask_colour(prompt):\n    t = plain(input(prompt))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return []\n    c = colour.parse(t)\n    if len(c) == 0:\n        print(\"Type #RRGGBB in hex, such as #C86432, or a colour from the palette.\")\n    return c\n\ndef ask_int(prompt, low, high):\n    t = plain(input(prompt))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return 0 - 1\n    if len(t) == 0 or len(t) > 3:\n        print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n        return 0 - 1\n    for ch in t:\n        if ch < \"0\" or ch > \"9\":\n            print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n            return 0 - 1\n    if int(t) < low or int(t) > high:\n        print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n        return 0 - 1\n    return int(t)\n\ndef show_palette():\n    p = colour.palette()\n    print(\"Palette:\")\n    for name in p:\n        print(\"  \" + pad(name, 8) + text(p[name]))\n\ndef names():\n    return [\"red\", \"green\", \"blue\"]\n\ndef two():\n    a = ask_colour(\"first colour> \")\n    if len(a) == 0:\n        return []\n    b = ask_colour(\"second colour> \")\n    if len(b) == 0:\n        return []\n    return [a, b]\n\ndef do_mix():\n    ab = two()\n    if len(ab) == 0:\n        return\n    w = ask_int(\"percent of the second (0 to 100)> \", 0, 100)\n    if w < 0:\n        return\n    t = colour.mix(ab[0], ab[1], w, \"trunc\")\n    h = colour.mix(ab[0], ab[1], w, \"half\")\n    print(\"Truncated: \" + text(t))\n    print(\"Half up:   \" + text(h))\n    lower = []\n    for k in range(0, 3):\n        if t[k] != h[k]:\n            lower = lower + [names()[k]]\n    if len(lower) > 0:\n        print(\"Truncation is 1 lower in \" + list_text(lower) + \": the share left over is dropped, never rounded up.\")\n\ndef list_text(xs):\n    if len(xs) == 1:\n        return xs[0]\n    if len(xs) == 2:\n        return xs[0] + \" and \" + xs[1]\n    return xs[0] + \", \" + xs[1] + \" and \" + xs[2]\n\ndef do_add():\n    ab = two()\n    if len(ab) == 0:\n        return\n    r = colour.add(ab[0], ab[1])\n    print(\"Sum: \" + text(r[0]))\n    over = []\n    for k in range(0, 3):\n        if r[1][k] > 255:\n            over = over + [names()[k] + \" \" + str(r[1][k])]\n    if len(over) == 3:\n        print(\"Clamped at 255: \" + list_text(over) + \". Every channel is clamped: the sum is white.\")\n    elif len(over) > 0:\n        print(\"Clamped at 255: \" + list_text(over) + \". The other channels kept their sums, so the balance between channels shifts.\")\n\ndef do_scale():\n    c = ask_colour(\"colour> \")\n    if len(c) == 0:\n        return\n    f = ask_int(\"brightness percent (1 to 400)> \", 1, 400)\n    if f < 0:\n        return\n    r = colour.scale(c, f)\n    back = colour.unscale(r[0], f)\n    print(\"Times \" + str(f) + \"%: \" + text(r[0]))\n    print(\"Back, times 100/\" + str(f) + \": \" + text(back))\n    for k in range(0, 3):\n        if back[k] != c[k]:\n            if r[1][k]:\n                print(\"  \" + names()[k] + \" was clamped at 255: its \" + str(c[k]) + \" is lost.\")\n            else:\n                print(\"  \" + names()[k] + \" comes back as \" + str(back[k]) + \", not \" + str(c[k]) + \": scaling down to \" + str(r[0][k]) + \" rounded the detail away.\")\n    if back == c:\n        print(\"  Every channel came back.\")\n\ndef do_gradient():\n    ab = two()\n    if len(ab) == 0:\n        return\n    n = ask_int(\"colours in the gradient (2 to 12)> \", 2, 12)\n    if n < 0:\n        return\n    t = colour.gradient(ab[0], ab[1], n, \"trunc\")\n    h = colour.gradient(ab[0], ab[1], n, \"half\")\n    print(\"  step  truncated                 half up\")\n    for i in range(0, n):\n        print(\"  \" + pad(str(i), 6) + pad(text(t[i]), 26) + text(h[i]))\n    diff = 0\n    for i in range(0, n):\n        for k in range(0, 3):\n            if t[i][k] != h[i][k]:\n                diff = diff + 1\n    if diff > 0:\n        print(\"  Half up is 1 higher in \" + str(diff) + \" of the \" + str(3 * n) + \" channel values: those where the share left over is a half or more.\")\n\ndef do_ease():\n    ab = two()\n    if len(ab) == 0:\n        return\n    t = ask_int(\"percent of the way each step (1 to 99)> \", 1, 99)\n    if t < 0:\n        return\n    if ab[0] == ab[1]:\n        print(\"  Already there.\")\n        return\n    for mode in [\"trunc\", \"half\"]:\n        steps = colour.ease(ab[0], ab[1], t, mode, 60)\n        last = steps[len(steps) - 1]\n        label = \"truncating each step\"\n        if mode == \"half\":\n            label = \"rounding each step half up\"\n        if last == ab[1]:\n            print(\"  \" + label + \": reaches \" + colour.hex_text(ab[1]) + \" in \" + str(len(steps) - 1) + \" steps\")\n        elif len(steps) == 61:\n            print(\"  \" + label + \": still moving after 60 steps, at \" + text(last))\n        else:\n            print(\"  \" + label + \": stops after \" + str(len(steps) - 1) + \" steps at \" + text(last) + \", short of \" + colour.hex_text(ab[1]) + \": the steps left are too small to round to 1\")\n\nprint(\"== Colour mixer ==\")\nprint(\"Colours as red, green and blue from 0 to 255, written #RRGGBB, worked one channel at a time in whole numbers.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_palette()\n    elif choice == \"2\":\n        do_mix()\n    elif choice == \"3\":\n        do_add()\n    elif choice == \"4\":\n        do_scale()\n    elif choice == \"5\":\n        do_gradient()\n    elif choice == \"6\":\n        do_ease()\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "colour.eml",
      "eml": "# P125 colour mixer - colours as three channels from 0 to 255, written as\n# #RRGGBB. As in the corpus case rgb-palette-blender, every operation works\n# one channel at a time, and adding clamps each channel on its own. All\n# arithmetic is on whole numbers; the two ways of rounding a share are\n# truncation (what int() of a positive share does) and half up.\n\ndef palette():\n    return {\"black\": [0, 0, 0], \"white\": [255, 255, 255], \"red\": [200, 30, 40], \"orange\": [240, 140, 20], \"teal\": [20, 140, 150], \"purple\": [120, 40, 160], \"rust\": [200, 100, 50], \"grey\": [128, 128, 128]}\n\ndef hex_digit(c):\n    \"0123456789ABCDEF\" => ds\n    \"0123456789abcdef\" => ls\n    for k in [0:15]:\n        if ds[k] == c or ls[k] == c:\n            return k\n    return 0 - 1\n\ndef parse(text):\n    # [r, g, b] from #RRGGBB, RRGGBB or a palette name; [] if neither\n    palette() => p\n    if text in p:\n        return p[text]\n    text => t\n    if len(t) == 7 and t[0] == \"#\":\n        t[1:7] => t\n    if len(t) != 6:\n        return []\n    [] => out\n    for k in [0:2]:\n        hex_digit(t[2 * k]) => hi\n        hex_digit(t[2 * k + 1]) => lo\n        if hi < 0 or lo < 0:\n            return []\n        out + [16 * hi + lo] => out\n    return out\n\ndef hex_text(c):\n    \"0123456789ABCDEF\" => ds\n    \"#\" => out\n    for x in c:\n        out + ds[div(x, 16)] + ds[x - 16 * div(x, 16)] => out\n    return out\n\ndef div(p, q):\n    # floor of p / q, for q > 0 and any whole p\n    int(p / q) => f\n    while f * q > p:\n        f - 1 => f\n    while (f + 1) * q <= p:\n        f + 1 => f\n    return f\n\ndef share(x, q, mode):\n    # x / q for x >= 0: truncated, or rounded half up\n    if mode == \"trunc\":\n        return div(x, q)\n    return div(2 * x + q, 2 * q)\n\ndef mix(a, b, w, mode):\n    # w percent of b into a\n    [] => out\n    for k in [0:2]:\n        out + [share(a[k] * (100 - w) + b[k] * w, 100, mode)] => out\n    return out\n\ndef add(a, b):\n    # [clamped sum, raw sums]\n    [] => out\n    [] => raw\n    for k in [0:2]:\n        raw + [a[k] + b[k]] => raw\n        if a[k] + b[k] > 255:\n            out + [255] => out\n        else:\n            out + [a[k] + b[k]] => out\n    return [out, raw]\n\ndef scale(c, f):\n    # [each channel times f percent, rounded half up and clamped at 255,\n    #  which were clamped]\n    [] => out\n    [] => clamped\n    for x in c:\n        share(x * f, 100, \"half\") => y\n        if y > 255:\n            out + [255] => out\n            clamped + [True] => clamped\n        else:\n            out + [y] => out\n            clamped + [False] => clamped\n    return [out, clamped]\n\ndef unscale(c, f):\n    # each channel times 100 / f, rounded half up, for f > 0\n    [] => out\n    for x in c:\n        out + [share(x * 100, f, \"half\")] => out\n    return out\n\ndef gradient(a, b, n, mode):\n    # n colours from a to b, the k-th weighted (n - 1 - k) : k\n    [] => out\n    for i in [0:n - 1]:\n        [] => c\n        for k in [0:2]:\n            c + [share(a[k] * (n - 1 - i) + b[k] * i, n - 1, mode)] => c\n        out + [c] => out\n    return out\n\ndef toward(x, target, t, mode):\n    # one easing step: t percent of the way, the step rounded toward zero\n    # (truncation) or half up, by its size\n    target - x => d\n    if d >= 0:\n        return x + share(d * t, 100, mode)\n    return x - share((0 - d) * t, 100, mode)\n\ndef ease(a, b, t, mode, limit):\n    # the colours from a, each step t percent of the way to b, until it\n    # reaches b, stops moving, or limit steps\n    [a] => out\n    a => c\n    for s in [1:limit]:\n        [] => nxt\n        for k in [0:2]:\n            nxt + [toward(c[k], b[k], t, mode)] => nxt\n        if nxt == c:\n            return out\n        nxt => c\n        out + [c] => out\n        if c == b:\n            return out\n    return out\n",
      "python": "def palette():\n    return {\"black\": [0, 0, 0], \"white\": [255, 255, 255], \"red\": [200, 30, 40], \"orange\": [240, 140, 20], \"teal\": [20, 140, 150], \"purple\": [120, 40, 160], \"rust\": [200, 100, 50], \"grey\": [128, 128, 128]}\n\ndef hex_digit(c):\n    ds = \"0123456789ABCDEF\"\n    ls = \"0123456789abcdef\"\n    for k in range(0, 16):\n        if ds[k] == c or ls[k] == c:\n            return k\n    return 0 - 1\n\ndef parse(text):\n    p = palette()\n    if text in p:\n        return p[text]\n    t = text\n    if len(t) == 7 and t[0] == \"#\":\n        t = t[1:7]\n    if len(t) != 6:\n        return []\n    out = []\n    for k in range(0, 3):\n        hi = hex_digit(t[2 * k])\n        lo = hex_digit(t[2 * k + 1])\n        if hi < 0 or lo < 0:\n            return []\n        out = out + [16 * hi + lo]\n    return out\n\ndef hex_text(c):\n    ds = \"0123456789ABCDEF\"\n    out = \"#\"\n    for x in c:\n        out = out + ds[div(x, 16)] + ds[x - 16 * div(x, 16)]\n    return out\n\ndef div(p, q):\n    f = int(p / q)\n    while f * q > p:\n        f = f - 1\n    while (f + 1) * q <= p:\n        f = f + 1\n    return f\n\ndef share(x, q, mode):\n    if mode == \"trunc\":\n        return div(x, q)\n    return div(2 * x + q, 2 * q)\n\ndef mix(a, b, w, mode):\n    out = []\n    for k in range(0, 3):\n        out = out + [share(a[k] * (100 - w) + b[k] * w, 100, mode)]\n    return out\n\ndef add(a, b):\n    out = []\n    raw = []\n    for k in range(0, 3):\n        raw = raw + [a[k] + b[k]]\n        if a[k] + b[k] > 255:\n            out = out + [255]\n        else:\n            out = out + [a[k] + b[k]]\n    return [out, raw]\n\ndef scale(c, f):\n    out = []\n    clamped = []\n    for x in c:\n        y = share(x * f, 100, \"half\")\n        if y > 255:\n            out = out + [255]\n            clamped = clamped + [True]\n        else:\n            out = out + [y]\n            clamped = clamped + [False]\n    return [out, clamped]\n\ndef unscale(c, f):\n    out = []\n    for x in c:\n        out = out + [share(x * 100, f, \"half\")]\n    return out\n\ndef gradient(a, b, n, mode):\n    out = []\n    for i in range(0, n):\n        c = []\n        for k in range(0, 3):\n            c = c + [share(a[k] * (n - 1 - i) + b[k] * i, n - 1, mode)]\n        out = out + [c]\n    return out\n\ndef toward(x, target, t, mode):\n    d = target - x\n    if d >= 0:\n        return x + share(d * t, 100, mode)\n    return x - share((0 - d) * t, 100, mode)\n\ndef ease(a, b, t, mode, limit):\n    out = [a]\n    c = a\n    for s in range(1, limit+1):\n        nxt = []\n        for k in range(0, 3):\n            nxt = nxt + [toward(c[k], b[k], t, mode)]\n        if nxt == c:\n            return out\n        c = nxt\n        out = out + [c]\n        if c == b:\n            return out\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n\n2\npink\n2\n#12345G\n2\n#1234\n2\nblack\n\n2\nblack\nwhite\n\n2\nblack\nwhite\n101\n2\nblack\nwhite\nx\n4\nrust\n0\n4\nrust\n401\n5\nblack\nwhite\n1\n5\nblack\nwhite\n13\n6\nblack\nwhite\n0\n6\nblack\nwhite\n100\n6\ngrey\ngrey\n50\n7\n",
      "screen": "== Colour mixer ==\nColours as red, green and blue from 0 to 255, written #RRGGBB, worked one channel at a time in whole numbers.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 0\nPick a number from 1 to 7.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> x\nPick a number from 1 to 7.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> \nCancelled.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> pink\nType #RRGGBB in hex, such as #C86432, or a colour from the palette.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> #12345G\nType #RRGGBB in hex, such as #C86432, or a colour from the palette.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> #1234\nType #RRGGBB in hex, such as #C86432, or a colour from the palette.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> black\nsecond colour> \nCancelled.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> black\nsecond colour> white\npercent of the second (0 to 100)> \nCancelled.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> black\nsecond colour> white\npercent of the second (0 to 100)> 101\nType a whole number from 0 to 100.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> black\nsecond colour> white\npercent of the second (0 to 100)> x\nType a whole number from 0 to 100.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 4\ncolour> rust\nbrightness percent (1 to 400)> 0\nType a whole number from 1 to 400.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 4\ncolour> rust\nbrightness percent (1 to 400)> 401\nType a whole number from 1 to 400.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 5\nfirst colour> black\nsecond colour> white\ncolours in the gradient (2 to 12)> 1\nType a whole number from 2 to 12.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 5\nfirst colour> black\nsecond colour> white\ncolours in the gradient (2 to 12)> 13\nType a whole number from 2 to 12.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 6\nfirst colour> black\nsecond colour> white\npercent of the way each step (1 to 99)> 0\nType a whole number from 1 to 99.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 6\nfirst colour> black\nsecond colour> white\npercent of the way each step (1 to 99)> 100\nType a whole number from 1 to 99.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 6\nfirst colour> grey\nsecond colour> grey\npercent of the way each step (1 to 99)> 50\n  Already there.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\nblack\nwhite\n50\n2\nrust\nteal\n30\n3\n#C8B4C8\n#3C3C3C\n3\nwhite\nred\n4\nrust\n200\n4\n#C9652F\n50\n5\nblack\nwhite\n5\n6\nblack\nwhite\n20\n6\nrust\n#14A096\n10\n7\n",
      "screen": "== Colour mixer ==\nColours as red, green and blue from 0 to 255, written #RRGGBB, worked one channel at a time in whole numbers.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 1\nPalette:\n  black   #000000 (0, 0, 0)\n  white   #FFFFFF (255, 255, 255)\n  red     #C81E28 (200, 30, 40)\n  orange  #F08C14 (240, 140, 20)\n  teal    #148C96 (20, 140, 150)\n  purple  #7828A0 (120, 40, 160)\n  rust    #C86432 (200, 100, 50)\n  grey    #808080 (128, 128, 128)\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> black\nsecond colour> white\npercent of the second (0 to 100)> 50\nTruncated: #7F7F7F (127, 127, 127)\nHalf up:   #808080 (128, 128, 128)\nTruncation is 1 lower in red, green and blue: the share left over is dropped, never rounded up.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 2\nfirst colour> rust\nsecond colour> teal\npercent of the second (0 to 100)> 30\nTruncated: #927050 (146, 112, 80)\nHalf up:   #927050 (146, 112, 80)\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 3\nfirst colour> #C8B4C8\nsecond colour> #3C3C3C\nSum: #FFF0FF (255, 240, 255)\nClamped at 255: red 260 and blue 260. The other channels kept their sums, so the balance between channels shifts.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 3\nfirst colour> white\nsecond colour> red\nSum: #FFFFFF (255, 255, 255)\nClamped at 255: red 455, green 285 and blue 295. Every channel is clamped: the sum is white.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 4\ncolour> rust\nbrightness percent (1 to 400)> 200\nTimes 200%: #FFC864 (255, 200, 100)\nBack, times 100/200: #806432 (128, 100, 50)\n  red was clamped at 255: its 200 is lost.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 4\ncolour> #C9652F\nbrightness percent (1 to 400)> 50\nTimes 50%: #653318 (101, 51, 24)\nBack, times 100/50: #CA6630 (202, 102, 48)\n  red comes back as 202, not 201: scaling down to 101 rounded the detail away.\n  green comes back as 102, not 101: scaling down to 51 rounded the detail away.\n  blue comes back as 48, not 47: scaling down to 24 rounded the detail away.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 5\nfirst colour> black\nsecond colour> white\ncolours in the gradient (2 to 12)> 5\n  step  truncated                 half up\n  0     #000000 (0, 0, 0)         #000000 (0, 0, 0)\n  1     #3F3F3F (63, 63, 63)      #404040 (64, 64, 64)\n  2     #7F7F7F (127, 127, 127)   #808080 (128, 128, 128)\n  3     #BFBFBF (191, 191, 191)   #BFBFBF (191, 191, 191)\n  4     #FFFFFF (255, 255, 255)   #FFFFFF (255, 255, 255)\n  Half up is 1 higher in 6 of the 15 channel values: those where the share left over is a half or more.\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 6\nfirst colour> black\nsecond colour> white\npercent of the way each step (1 to 99)> 20\n  truncating each step: stops after 21 steps at #FBFBFB (251, 251, 251), short of #FFFFFF: the steps left are too small to round to 1\n  rounding each step half up: stops after 21 steps at #FDFDFD (253, 253, 253), short of #FFFFFF: the steps left are too small to round to 1\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 6\nfirst colour> rust\nsecond colour> #14A096\npercent of the way each step (1 to 99)> 10\n  truncating each step: stops after 33 steps at #1D978D (29, 151, 141), short of #14A096: the steps left are too small to round to 1\n  rounding each step half up: stops after 33 steps at #189C92 (24, 156, 146), short of #14A096: the steps left are too small to round to 1\n\n1) palette  2) mix  3) add  4) scale  5) gradient  6) ease  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "rgb-palette-blender",
      "caseId": "165-rgb-palette-blender",
      "title": "Blending colours, one channel at a time"
    }
  ],
  "updated": "2026-10-17"
}
