# tk_subtractive_blend_polygons.py
import tkinter as tk
from PIL import Image, ImageDraw, ImageTk, ImageChops
import math
sz = 600
def center_coords(coords_list, sz):
xs = [x for coords in coords_list for x, y in coords]
ys = [y for coords in coords_list for x, y in coords]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
width = max_x - min_x
height = max_y - min_y
offset_x = (sz - width) / 2 - min_x
offset_y = (sz - height) / 2 - min_y
return [[(x + offset_x, y + offset_y) for x, y in coords] for coords in coords_list]
def srgb_from_linear(x):
if x <= 0.0031308:
return 12.92 * x
return 1.055 * (x ** (1/2.4)) - 0.055
def linear_from_srgb(x):
if x <= 0.04045:
return x / 12.92
return ((x + 0.055) / 1.055) ** 2.4
def channel_transmission(v_srgb, leak, gamma):
v_lin = linear_from_srgb(v_srgb)
t = leak + (1.0 - leak) * (v_lin ** gamma)
return max(0.0, min(1.0, t))
def subtractive_blend(c1, c2,
leak1=(0.02, 0.20, 0.85),
gamma1=(1.6, 1.4, 1.0),
leak2=(0.85, 0.90, 0.02),
gamma2=(1.0, 1.0, 1.6)):
r1, g1, b1 = [v/255.0 for v in c1[:3]]
r2, g2, b2 = [v/255.0 for v in c2[:3]]
t1_r = channel_transmission(r1, leak1[0], gamma1[0])
t1_g = channel_transmission(g1, leak1[1], gamma1[1])
t1_b = channel_transmission(b1, leak1[2], gamma1[2])
t2_r = channel_transmission(r2, leak2[0], gamma2[0])
t2_g = channel_transmission(g2, leak2[1], gamma2[1])
t2_b = channel_transmission(b2, leak2[2], gamma2[2])
t_r = t1_r * t2_r
t_g = t1_g * t2_g
t_b = t1_b * t2_b
R = int(round(255 * srgb_from_linear(t_r)))
G = int(round(255 * srgb_from_linear(t_g)))
B = int(round(255 * srgb_from_linear(t_b)))
A = max(c1[3], c2[3])
return (R, G, B, A)
def composite_shapes(shapes, sz):
result = Image.new("RGBA", (sz, sz), (255, 255, 255, 255))
accumulated_mask = Image.new("L", (sz, sz), 0)
accumulated_color = None
for coords, color in shapes:
layer = Image.new("RGBA", (sz, sz), (0, 0, 0, 0))
ImageDraw.Draw(layer).polygon(coords, fill=color)
mask = layer.split()[3]
overlap = ImageChops.multiply(accumulated_mask, mask)
only_new = ImageChops.subtract(mask, overlap)
if accumulated_color:
blended = subtractive_blend(accumulated_color, color)
result.paste(blended, mask=overlap)
result.paste(color, mask=only_new)
accumulated_mask = ImageChops.add(accumulated_mask, mask)
accumulated_color = color
return result
# Example: two triangles
blue_coords = [(0, 0), (500, 0), (250, 450)]
cx = sum(x for x, y in blue_coords) / 3
cy = sum(y for x, y in blue_coords) / 3
theta = math.radians(30)
yellow_coords = []
for (x, y) in blue_coords:
x_new = cx + (x - cx) * math.cos(theta) - (y - cy) * math.sin(theta)
y_new = cy + (x - cx) * math.sin(theta) + (y - cy) * math.cos(theta)
yellow_coords.append((x_new, y_new))
blue_coords, yellow_coords = center_coords([blue_coords, yellow_coords], sz)
shapes = [
(blue_coords, (0, 0, 255, 255)),
(yellow_coords, (255, 255, 0, 255)),
]
result = composite_shapes(shapes, sz)
root = tk.Tk()
root.geometry(f"{sz}x{sz}+0+0")
canvas = tk.Canvas(root, width=sz, height=sz, bg="white", highlightthickness=0)
canvas.pack()
tk_img = ImageTk.PhotoImage(result)
canvas.create_image(0, 0, anchor="nw", image=tk_img)
root.mainloop()