Colour mixer
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.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Colours as red, green and blue from 0 to 255, written #RRGGBB, mixed, added, scaled, graded and eased one channel at a time in whole numbers, with each share rounded two ways: truncated, and half up.
main.eml- the menu, the palette, and the five operationscolour.eml- the palette, reading and writing #RRGGBB, the two ways of rounding a share, and the operations
How each part works:
- As in the corpus case
rgb-palette-blender, every operation works one channel at a time, and adding clamps each channel at 255 on its own. When some channels clamp and others do not, the balance between them shifts. - Mixing w percent of one colour into another: truncation drops the share left over, so it is never above half up and at most 1 below. Black and white half and half: #7F7F7F truncated, #808080 half up.
- Scaling brightness by f percent and back by 100/f: by 100% or more, every channel that was not clamped comes back exactly; a clamped one is lost. Scaled down, a channel can come back 1 off.
- A gradient of n colours weights the two ends (n - 1 - k) : k.
- Easing moves each channel a percentage of the way that is left. It stops short when every step left rounds to 0: truncated, when the gap times the percentage is below 100, and half up, below 50. Black to white at 20% stops at #FBFBFB truncated and #FDFDFD half up.
What is checked: menu choices 1 to 7; colours as #RRGGBB or RRGGBB in either case, or a palette name; a weight from 0 to 100, a brightness from 1 to 400, 2 to 12 colours in a gradient, and an easing step from 1 to 99. An empty answer cancels.
Sessions: sessions/basic.in gives: - the palette; - black and white half and half, and 30% teal into rust; - #C8B4C8 + #3C3C3C (red and blue clamped) and white + red (all three); - rust times 200% and back (red lost), and #C9652F times 50% and back; - black to white in 5 colours; - easing black to white at 20%, and rust to #14A096 at 10%.
sessions/bad-input.in gives: - menu choices 0 and x; - colours: a cancel, pink, #12345G and #1234, then black with a cancel; - weights: a cancel, 101 and x; brightness 0 and 401; gradients of 1 and 13; easing steps of 0 and 100; easing grey to grey.
Built on the verified corpus case rgb-palette-blender (colours added and scaled one channel at a time, each clamped on its own).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Colour mixer ==
Colours as red, green and blue from 0 to 255, written #RRGGBB, worked one channel at a time in whole numbers.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 0
Pick a number from 1 to 7.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> x
Pick a number from 1 to 7.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour>
Cancelled.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> pink
Type #RRGGBB in hex, such as #C86432, or a colour from the palette.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> #12345G
Type #RRGGBB in hex, such as #C86432, or a colour from the palette.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> #1234
Type #RRGGBB in hex, such as #C86432, or a colour from the palette.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> black
second colour>
Cancelled.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> black
second colour> white
percent of the second (0 to 100)>
Cancelled.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> black
second colour> white
percent of the second (0 to 100)> 101
Type a whole number from 0 to 100.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> black
second colour> white
percent of the second (0 to 100)> x
Type a whole number from 0 to 100.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 4
colour> rust
brightness percent (1 to 400)> 0
Type a whole number from 1 to 400.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 4
colour> rust
brightness percent (1 to 400)> 401
Type a whole number from 1 to 400.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 5
first colour> black
second colour> white
colours in the gradient (2 to 12)> 1
Type a whole number from 2 to 12.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 5
first colour> black
second colour> white
colours in the gradient (2 to 12)> 13
Type a whole number from 2 to 12.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 6
first colour> black
second colour> white
percent of the way each step (1 to 99)> 0
Type a whole number from 1 to 99.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 6
first colour> black
second colour> white
percent of the way each step (1 to 99)> 100
Type a whole number from 1 to 99.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 6
first colour> grey
second colour> grey
percent of the way each step (1 to 99)> 50
Already there.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 7
Bye.
What was typed (52 lines)
0
x
2
2
pink
2
#12345G
2
#1234
2
black
2
black
white
2
black
white
101
2
black
white
x
4
rust
0
4
rust
401
5
black
white
1
5
black
white
13
6
black
white
0
6
black
white
100
6
grey
grey
50
7
basic
interpreter: byte-equal== Colour mixer ==
Colours as red, green and blue from 0 to 255, written #RRGGBB, worked one channel at a time in whole numbers.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 1
Palette:
black #000000 (0, 0, 0)
white #FFFFFF (255, 255, 255)
red #C81E28 (200, 30, 40)
orange #F08C14 (240, 140, 20)
teal #148C96 (20, 140, 150)
purple #7828A0 (120, 40, 160)
rust #C86432 (200, 100, 50)
grey #808080 (128, 128, 128)
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> black
second colour> white
percent of the second (0 to 100)> 50
Truncated: #7F7F7F (127, 127, 127)
Half up: #808080 (128, 128, 128)
Truncation is 1 lower in red, green and blue: the share left over is dropped, never rounded up.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 2
first colour> rust
second colour> teal
percent of the second (0 to 100)> 30
Truncated: #927050 (146, 112, 80)
Half up: #927050 (146, 112, 80)
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 3
first colour> #C8B4C8
second colour> #3C3C3C
Sum: #FFF0FF (255, 240, 255)
Clamped at 255: red 260 and blue 260. The other channels kept their sums, so the balance between channels shifts.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 3
first colour> white
second colour> red
Sum: #FFFFFF (255, 255, 255)
Clamped at 255: red 455, green 285 and blue 295. Every channel is clamped: the sum is white.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 4
colour> rust
brightness percent (1 to 400)> 200
Times 200%: #FFC864 (255, 200, 100)
Back, times 100/200: #806432 (128, 100, 50)
red was clamped at 255: its 200 is lost.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 4
colour> #C9652F
brightness percent (1 to 400)> 50
Times 50%: #653318 (101, 51, 24)
Back, times 100/50: #CA6630 (202, 102, 48)
red comes back as 202, not 201: scaling down to 101 rounded the detail away.
green comes back as 102, not 101: scaling down to 51 rounded the detail away.
blue comes back as 48, not 47: scaling down to 24 rounded the detail away.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 5
first colour> black
second colour> white
colours in the gradient (2 to 12)> 5
step truncated half up
0 #000000 (0, 0, 0) #000000 (0, 0, 0)
1 #3F3F3F (63, 63, 63) #404040 (64, 64, 64)
2 #7F7F7F (127, 127, 127) #808080 (128, 128, 128)
3 #BFBFBF (191, 191, 191) #BFBFBF (191, 191, 191)
4 #FFFFFF (255, 255, 255) #FFFFFF (255, 255, 255)
Half up is 1 higher in 6 of the 15 channel values: those where the share left over is a half or more.
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 6
first colour> black
second colour> white
percent of the way each step (1 to 99)> 20
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
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
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 6
first colour> rust
second colour> #14A096
percent of the way each step (1 to 99)> 10
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
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
1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit
choice> 7
Bye.
What was typed (34 lines)
1
2
black
white
50
2
rust
teal
30
3
#C8B4C8
#3C3C3C
3
white
red
4
rust
200
4
#C9652F
50
5
black
white
5
6
black
white
20
6
rust
#14A096
10
7
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P125 colour mixer: named colours, mixing two, adding with clamping,
# scaling and back, gradients and easing, each rounded two ways.
import colour
def plain(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def pad(s, n):
s => out
while len(out) < n:
out + " " => out
return out
def channels(c):
return "(" + str(c[0]) + ", " + str(c[1]) + ", " + str(c[2]) + ")"
def text(c):
return colour.hex_text(c) + " " + channels(c)
def ask_colour(prompt):
plain(input(prompt)) => t
if t == "":
"Cancelled." ^0
return []
colour.parse(t) => c
if len(c) == 0:
"Type #RRGGBB in hex, such as #C86432, or a colour from the palette." ^0
return c
def ask_int(prompt, low, high):
plain(input(prompt)) => t
if t == "":
"Cancelled." ^0
return 0 - 1
if len(t) == 0 or len(t) > 3:
("Type a whole number from " + str(low) + " to " + str(high) + ".") ^0
return 0 - 1
for ch in t:
if ch < "0" or ch > "9":
("Type a whole number from " + str(low) + " to " + str(high) + ".") ^0
return 0 - 1
if int(t) < low or int(t) > high:
("Type a whole number from " + str(low) + " to " + str(high) + ".") ^0
return 0 - 1
return int(t)
def show_palette():
colour.palette() => p
"Palette:" ^0
for name in p:
(" " + pad(name, 8) + text(p[name])) ^0
def names():
return ["red", "green", "blue"]
def two():
ask_colour("first colour> ") => a
if len(a) == 0:
return []
ask_colour("second colour> ") => b
if len(b) == 0:
return []
return [a, b]
def do_mix():
two() => ab
if len(ab) == 0:
return
ask_int("percent of the second (0 to 100)> ", 0, 100) => w
if w < 0:
return
colour.mix(ab[0], ab[1], w, "trunc") => t
colour.mix(ab[0], ab[1], w, "half") => h
("Truncated: " + text(t)) ^0
("Half up: " + text(h)) ^0
[] => lower
for k in [0:2]:
if t[k] != h[k]:
lower + [names()[k]] => lower
if len(lower) > 0:
("Truncation is 1 lower in " + list_text(lower) + ": the share left over is dropped, never rounded up.") ^0
def list_text(xs):
if len(xs) == 1:
return xs[0]
if len(xs) == 2:
return xs[0] + " and " + xs[1]
return xs[0] + ", " + xs[1] + " and " + xs[2]
def do_add():
two() => ab
if len(ab) == 0:
return
colour.add(ab[0], ab[1]) => r
("Sum: " + text(r[0])) ^0
[] => over
for k in [0:2]:
if r[1][k] > 255:
over + [names()[k] + " " + str(r[1][k])] => over
if len(over) == 3:
("Clamped at 255: " + list_text(over) + ". Every channel is clamped: the sum is white.") ^0
elif len(over) > 0:
("Clamped at 255: " + list_text(over) + ". The other channels kept their sums, so the balance between channels shifts.") ^0
def do_scale():
ask_colour("colour> ") => c
if len(c) == 0:
return
ask_int("brightness percent (1 to 400)> ", 1, 400) => f
if f < 0:
return
colour.scale(c, f) => r
colour.unscale(r[0], f) => back
("Times " + str(f) + "%: " + text(r[0])) ^0
("Back, times 100/" + str(f) + ": " + text(back)) ^0
for k in [0:2]:
if back[k] != c[k]:
if r[1][k]:
(" " + names()[k] + " was clamped at 255: its " + str(c[k]) + " is lost.") ^0
else:
(" " + names()[k] + " comes back as " + str(back[k]) + ", not " + str(c[k]) + ": scaling down to " + str(r[0][k]) + " rounded the detail away.") ^0
if back == c:
" Every channel came back." ^0
def do_gradient():
two() => ab
if len(ab) == 0:
return
ask_int("colours in the gradient (2 to 12)> ", 2, 12) => n
if n < 0:
return
colour.gradient(ab[0], ab[1], n, "trunc") => t
colour.gradient(ab[0], ab[1], n, "half") => h
" step truncated half up" ^0
for i in [0:n - 1]:
(" " + pad(str(i), 6) + pad(text(t[i]), 26) + text(h[i])) ^0
0 => diff
for i in [0:n - 1]:
for k in [0:2]:
if t[i][k] != h[i][k]:
diff + 1 => diff
if diff > 0:
(" 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
def do_ease():
two() => ab
if len(ab) == 0:
return
ask_int("percent of the way each step (1 to 99)> ", 1, 99) => t
if t < 0:
return
if ab[0] == ab[1]:
" Already there." ^0
return
for mode in ["trunc", "half"]:
colour.ease(ab[0], ab[1], t, mode, 60) => steps
steps[len(steps) - 1] => last
"truncating each step" => label
if mode == "half":
"rounding each step half up" => label
if last == ab[1]:
(" " + label + ": reaches " + colour.hex_text(ab[1]) + " in " + str(len(steps) - 1) + " steps") ^0
elif len(steps) == 61:
(" " + label + ": still moving after 60 steps, at " + text(last)) ^0
else:
(" " + 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
"== Colour mixer ==" ^0
"Colours as red, green and blue from 0 to 255, written #RRGGBB, worked one channel at a time in whole numbers." ^0
True => running
while running:
"" ^0
"1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
show_palette()
elif choice == "2":
do_mix()
elif choice == "3":
do_add()
elif choice == "4":
do_scale()
elif choice == "5":
do_gradient()
elif choice == "6":
do_ease()
elif choice == "7":
False => running
else:
"Pick a number from 1 to 7." ^0
"Bye." ^0
Python projection (main.py)
import colour
def plain(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def pad(s, n):
out = s
while len(out) < n:
out = out + " "
return out
def channels(c):
return "(" + str(c[0]) + ", " + str(c[1]) + ", " + str(c[2]) + ")"
def text(c):
return colour.hex_text(c) + " " + channels(c)
def ask_colour(prompt):
t = plain(input(prompt))
if t == "":
print("Cancelled.")
return []
c = colour.parse(t)
if len(c) == 0:
print("Type #RRGGBB in hex, such as #C86432, or a colour from the palette.")
return c
def ask_int(prompt, low, high):
t = plain(input(prompt))
if t == "":
print("Cancelled.")
return 0 - 1
if len(t) == 0 or len(t) > 3:
print("Type a whole number from " + str(low) + " to " + str(high) + ".")
return 0 - 1
for ch in t:
if ch < "0" or ch > "9":
print("Type a whole number from " + str(low) + " to " + str(high) + ".")
return 0 - 1
if int(t) < low or int(t) > high:
print("Type a whole number from " + str(low) + " to " + str(high) + ".")
return 0 - 1
return int(t)
def show_palette():
p = colour.palette()
print("Palette:")
for name in p:
print(" " + pad(name, 8) + text(p[name]))
def names():
return ["red", "green", "blue"]
def two():
a = ask_colour("first colour> ")
if len(a) == 0:
return []
b = ask_colour("second colour> ")
if len(b) == 0:
return []
return [a, b]
def do_mix():
ab = two()
if len(ab) == 0:
return
w = ask_int("percent of the second (0 to 100)> ", 0, 100)
if w < 0:
return
t = colour.mix(ab[0], ab[1], w, "trunc")
h = colour.mix(ab[0], ab[1], w, "half")
print("Truncated: " + text(t))
print("Half up: " + text(h))
lower = []
for k in range(0, 3):
if t[k] != h[k]:
lower = lower + [names()[k]]
if len(lower) > 0:
print("Truncation is 1 lower in " + list_text(lower) + ": the share left over is dropped, never rounded up.")
def list_text(xs):
if len(xs) == 1:
return xs[0]
if len(xs) == 2:
return xs[0] + " and " + xs[1]
return xs[0] + ", " + xs[1] + " and " + xs[2]
def do_add():
ab = two()
if len(ab) == 0:
return
r = colour.add(ab[0], ab[1])
print("Sum: " + text(r[0]))
over = []
for k in range(0, 3):
if r[1][k] > 255:
over = over + [names()[k] + " " + str(r[1][k])]
if len(over) == 3:
print("Clamped at 255: " + list_text(over) + ". Every channel is clamped: the sum is white.")
elif len(over) > 0:
print("Clamped at 255: " + list_text(over) + ". The other channels kept their sums, so the balance between channels shifts.")
def do_scale():
c = ask_colour("colour> ")
if len(c) == 0:
return
f = ask_int("brightness percent (1 to 400)> ", 1, 400)
if f < 0:
return
r = colour.scale(c, f)
back = colour.unscale(r[0], f)
print("Times " + str(f) + "%: " + text(r[0]))
print("Back, times 100/" + str(f) + ": " + text(back))
for k in range(0, 3):
if back[k] != c[k]:
if r[1][k]:
print(" " + names()[k] + " was clamped at 255: its " + str(c[k]) + " is lost.")
else:
print(" " + names()[k] + " comes back as " + str(back[k]) + ", not " + str(c[k]) + ": scaling down to " + str(r[0][k]) + " rounded the detail away.")
if back == c:
print(" Every channel came back.")
def do_gradient():
ab = two()
if len(ab) == 0:
return
n = ask_int("colours in the gradient (2 to 12)> ", 2, 12)
if n < 0:
return
t = colour.gradient(ab[0], ab[1], n, "trunc")
h = colour.gradient(ab[0], ab[1], n, "half")
print(" step truncated half up")
for i in range(0, n):
print(" " + pad(str(i), 6) + pad(text(t[i]), 26) + text(h[i]))
diff = 0
for i in range(0, n):
for k in range(0, 3):
if t[i][k] != h[i][k]:
diff = diff + 1
if diff > 0:
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.")
def do_ease():
ab = two()
if len(ab) == 0:
return
t = ask_int("percent of the way each step (1 to 99)> ", 1, 99)
if t < 0:
return
if ab[0] == ab[1]:
print(" Already there.")
return
for mode in ["trunc", "half"]:
steps = colour.ease(ab[0], ab[1], t, mode, 60)
last = steps[len(steps) - 1]
label = "truncating each step"
if mode == "half":
label = "rounding each step half up"
if last == ab[1]:
print(" " + label + ": reaches " + colour.hex_text(ab[1]) + " in " + str(len(steps) - 1) + " steps")
elif len(steps) == 61:
print(" " + label + ": still moving after 60 steps, at " + text(last))
else:
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")
print("== Colour mixer ==")
print("Colours as red, green and blue from 0 to 255, written #RRGGBB, worked one channel at a time in whole numbers.")
running = True
while running:
print("")
print("1) palette 2) mix 3) add 4) scale 5) gradient 6) ease 7) quit")
choice = plain(input("choice> "))
if choice == "1":
show_palette()
elif choice == "2":
do_mix()
elif choice == "3":
do_add()
elif choice == "4":
do_scale()
elif choice == "5":
do_gradient()
elif choice == "6":
do_ease()
elif choice == "7":
running = False
else:
print("Pick a number from 1 to 7.")
print("Bye.")
colour.eml
eml# P125 colour mixer - colours as three channels from 0 to 255, written as
# #RRGGBB. As in the corpus case rgb-palette-blender, every operation works
# one channel at a time, and adding clamps each channel on its own. All
# arithmetic is on whole numbers; the two ways of rounding a share are
# truncation (what int() of a positive share does) and half up.
def palette():
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]}
def hex_digit(c):
"0123456789ABCDEF" => ds
"0123456789abcdef" => ls
for k in [0:15]:
if ds[k] == c or ls[k] == c:
return k
return 0 - 1
def parse(text):
# [r, g, b] from #RRGGBB, RRGGBB or a palette name; [] if neither
palette() => p
if text in p:
return p[text]
text => t
if len(t) == 7 and t[0] == "#":
t[1:7] => t
if len(t) != 6:
return []
[] => out
for k in [0:2]:
hex_digit(t[2 * k]) => hi
hex_digit(t[2 * k + 1]) => lo
if hi < 0 or lo < 0:
return []
out + [16 * hi + lo] => out
return out
def hex_text(c):
"0123456789ABCDEF" => ds
"#" => out
for x in c:
out + ds[div(x, 16)] + ds[x - 16 * div(x, 16)] => out
return out
def div(p, q):
# floor of p / q, for q > 0 and any whole p
int(p / q) => f
while f * q > p:
f - 1 => f
while (f + 1) * q <= p:
f + 1 => f
return f
def share(x, q, mode):
# x / q for x >= 0: truncated, or rounded half up
if mode == "trunc":
return div(x, q)
return div(2 * x + q, 2 * q)
def mix(a, b, w, mode):
# w percent of b into a
[] => out
for k in [0:2]:
out + [share(a[k] * (100 - w) + b[k] * w, 100, mode)] => out
return out
def add(a, b):
# [clamped sum, raw sums]
[] => out
[] => raw
for k in [0:2]:
raw + [a[k] + b[k]] => raw
if a[k] + b[k] > 255:
out + [255] => out
else:
out + [a[k] + b[k]] => out
return [out, raw]
def scale(c, f):
# [each channel times f percent, rounded half up and clamped at 255,
# which were clamped]
[] => out
[] => clamped
for x in c:
share(x * f, 100, "half") => y
if y > 255:
out + [255] => out
clamped + [True] => clamped
else:
out + [y] => out
clamped + [False] => clamped
return [out, clamped]
def unscale(c, f):
# each channel times 100 / f, rounded half up, for f > 0
[] => out
for x in c:
out + [share(x * 100, f, "half")] => out
return out
def gradient(a, b, n, mode):
# n colours from a to b, the k-th weighted (n - 1 - k) : k
[] => out
for i in [0:n - 1]:
[] => c
for k in [0:2]:
c + [share(a[k] * (n - 1 - i) + b[k] * i, n - 1, mode)] => c
out + [c] => out
return out
def toward(x, target, t, mode):
# one easing step: t percent of the way, the step rounded toward zero
# (truncation) or half up, by its size
target - x => d
if d >= 0:
return x + share(d * t, 100, mode)
return x - share((0 - d) * t, 100, mode)
def ease(a, b, t, mode, limit):
# the colours from a, each step t percent of the way to b, until it
# reaches b, stops moving, or limit steps
[a] => out
a => c
for s in [1:limit]:
[] => nxt
for k in [0:2]:
nxt + [toward(c[k], b[k], t, mode)] => nxt
if nxt == c:
return out
nxt => c
out + [c] => out
if c == b:
return out
return out
Python projection (colour.py)
def palette():
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]}
def hex_digit(c):
ds = "0123456789ABCDEF"
ls = "0123456789abcdef"
for k in range(0, 16):
if ds[k] == c or ls[k] == c:
return k
return 0 - 1
def parse(text):
p = palette()
if text in p:
return p[text]
t = text
if len(t) == 7 and t[0] == "#":
t = t[1:7]
if len(t) != 6:
return []
out = []
for k in range(0, 3):
hi = hex_digit(t[2 * k])
lo = hex_digit(t[2 * k + 1])
if hi < 0 or lo < 0:
return []
out = out + [16 * hi + lo]
return out
def hex_text(c):
ds = "0123456789ABCDEF"
out = "#"
for x in c:
out = out + ds[div(x, 16)] + ds[x - 16 * div(x, 16)]
return out
def div(p, q):
f = int(p / q)
while f * q > p:
f = f - 1
while (f + 1) * q <= p:
f = f + 1
return f
def share(x, q, mode):
if mode == "trunc":
return div(x, q)
return div(2 * x + q, 2 * q)
def mix(a, b, w, mode):
out = []
for k in range(0, 3):
out = out + [share(a[k] * (100 - w) + b[k] * w, 100, mode)]
return out
def add(a, b):
out = []
raw = []
for k in range(0, 3):
raw = raw + [a[k] + b[k]]
if a[k] + b[k] > 255:
out = out + [255]
else:
out = out + [a[k] + b[k]]
return [out, raw]
def scale(c, f):
out = []
clamped = []
for x in c:
y = share(x * f, 100, "half")
if y > 255:
out = out + [255]
clamped = clamped + [True]
else:
out = out + [y]
clamped = clamped + [False]
return [out, clamped]
def unscale(c, f):
out = []
for x in c:
out = out + [share(x * 100, f, "half")]
return out
def gradient(a, b, n, mode):
out = []
for i in range(0, n):
c = []
for k in range(0, 3):
c = c + [share(a[k] * (n - 1 - i) + b[k] * i, n - 1, mode)]
out = out + [c]
return out
def toward(x, target, t, mode):
d = target - x
if d >= 0:
return x + share(d * t, 100, mode)
return x - share((0 - d) * t, 100, mode)
def ease(a, b, t, mode, limit):
out = [a]
c = a
for s in range(1, limit+1):
nxt = []
for k in range(0, 3):
nxt = nxt + [toward(c[k], b[k], t, mode)]
if nxt == c:
return out
c = nxt
out = out + [c]
if c == b:
return out
return out