# 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()