Thumbnail.

Periodic System v1

Description

Another project of mine, called Periodic System, is an online and interactieve periodic table of the chemical elements that can be extended to arbitrary large (and small) elements on the fly.

I’ve always wanted to create an allrgb entry dedicated to it, but never got around to. So, as with many other unfinished projects these days, I asked Chatgpt to do it for me. It’s not as pretty as I’d like, but it’s technically correct, and like the meme states, that’s the best kind.

import numpy as np
from PIL import Image, ImageDraw, ImageFont
import colorsys
from collections import defaultdict

# === Constants ===
WIDTH, HEIGHT = 4096, 4096
TOTAL_COLORS = WIDTH * HEIGHT
ELEMENT_COUNT = 512
GRID_COLS = 32
CELL_W = WIDTH // GRID_COLS
CELL_H = HEIGHT // ((ELEMENT_COUNT + GRID_COLS - 1) // GRID_COLS)
TEXT_MARGIN = 4  # Increased margin for label padding

# === Generate and sort all RGB colors ===
colors = np.array([[r, g, b] for r in range(256)
                   for g in range(256)
                   for b in range(256)], dtype=np.uint8)

def rgb_to_hsb(c):
    r, g, b = [x / 255.0 for x in c]
    return colorsys.rgb_to_hsv(r, g, b)

colors_sorted_hsb = sorted(colors, key=rgb_to_hsb)
colors_sorted_brightness = sorted(colors, key=lambda rgb: 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2])

# === Font setup ===
try:
    font = ImageFont.truetype("PixelOperator8.ttf", 12)
except:
    font = ImageFont.load_default()

# === Symbol generator ===
def generate_element_symbol(n):
    roots = ['n', 'u', 'b', 't', 'q', 'p', 'h', 's', 'o', 'e']
    digits = list(map(int, str(n)))
    return ''.join(roots[d] for d in digits)[:3].capitalize()

# === Chemical classification ===
def classify_element(n):
    if n == 1: return "nonmetal"
    if n in [2, 10, 18, 36, 54, 86, 118]: return "noble_gas"
    if n in [5, 14, 32, 33, 51, 52, 84]: return "metalloid"
    if n in [9, 17, 35, 53, 85, 117]: return "halogen"
    if n in [6, 7, 8, 15, 16, 34]: return "nonmetal"
    if n in [3, 11, 19, 37, 55, 87]: return "alkali"
    if n in [4, 12, 20, 38, 56, 88]: return "alkaline"
    if 57 <= n <= 71: return "lanthanide"
    if 89 <= n <= 103: return "actinide"
    if 104 <= n <= 112 or (21 <= n <= 30) or (39 <= n <= 48) or (72 <= n <= 80): return "transition"
    if (13 <= n <= 16) or (31 <= n <= 34) or (49 <= n <= 52) or (81 <= n <= 84): return "post_transition"
    if n >= 119: return "superactinide"
    return "unknown"

element_categories = [classify_element(n) for n in range(1, ELEMENT_COUNT + 1)]
unique_categories = sorted(set(element_categories))
category_counts = {cat: element_categories.count(cat) for cat in unique_categories}

# === Reserve image canvas ===
image_array = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
used_pixels = np.zeros((HEIGHT, WIDTH), dtype=bool)
text_coords, border_coords = [], []
element_by_category = defaultdict(list)

# === Step 1: Reserve text label coordinates ===
for n in range(1, ELEMENT_COUNT + 1):
    row, col = (n - 1) // GRID_COLS, (n - 1) % GRID_COLS
    x0, y0 = col * CELL_W, row * CELL_H
    symbol = generate_element_symbol(n)
    label = f"{n}\n{symbol}"
    y_cursor = y0 + 4
    element_by_category[classify_element(n)].append((n, x0, y0))

    for line in label.split('\n'):
        bbox = font.getbbox(line)
        x_min, y_min, x_max, y_max = bbox
        width = x_max - x_min + 2 * TEXT_MARGIN
        height = y_max - y_min + 2 * TEXT_MARGIN
        x_cursor = x0 + (CELL_W - width) // 2

        mask = Image.new("1", (width, height), 0)
        draw = ImageDraw.Draw(mask)
        draw.text((TEXT_MARGIN - x_min, TEXT_MARGIN - y_min), line, fill=1, font=font)
        mask_array = np.array(mask)

        for dy in range(height):
            for dx in range(width):
                if mask_array[dy, dx] == 1:
                    px, py = x_cursor + dx, y_cursor + dy
                    if 0 <= px < WIDTH and 0 <= py < HEIGHT:
                        text_coords.append((py, px))
                        used_pixels[py, px] = True

        y_cursor += height

# === Step 2: Reserve 1px cell borders ===
for n in range(1, ELEMENT_COUNT + 1):
    row, col = (n - 1) // GRID_COLS, (n - 1) % GRID_COLS
    x0, y0 = col * CELL_W, row * CELL_H
    x1, y1 = x0 + CELL_W, y0 + CELL_H

    for x in range(x0, x1):
        for y in (y0, y1 - 1):  # top and bottom
            if not used_pixels[y, x]:
                border_coords.append((y, x))
                used_pixels[y, x] = True
    for y in range(y0 + 1, y1 - 1):
        for x in (x0, x1 - 1):  # left and right
            if not used_pixels[y, x]:
                border_coords.append((y, x))
                used_pixels[y, x] = True

# === Step 3: Assign RGBs to text and borders ===
colors_for_text = colors_sorted_brightness[:len(text_coords)]
colors_for_border = colors_sorted_brightness[len(text_coords):len(text_coords) + len(border_coords)]

for i, (py, px) in enumerate(text_coords):
    image_array[py, px] = colors_for_text[i]
for i, (py, px) in enumerate(border_coords):
    image_array[py, px] = colors_for_border[i]

used_color_set = set(tuple(c) for c in colors_for_text + colors_for_border)

# === Step 4: Fill remaining cell pixels, clustered by series ===
remaining_colors = [tuple(c) for c in colors_sorted_hsb if tuple(c) not in used_color_set]
color_index = 0

for category in unique_categories:
    for n, x0, y0 in element_by_category[category]:
        for y in range(y0, y0 + CELL_H):
            for x in range(x0, x0 + CELL_W):
                if not used_pixels[y, x]:
                    image_array[y, x] = remaining_colors[color_index]
                    used_pixels[y, x] = True
                    color_index += 1

# === Final Validation ===
#assert np.count_nonzero(used_pixels) == TOTAL_COLORS, "❌ Pixel count mismatch"
#assert color_index + len(text_coords) + len(border_coords) == TOTAL_COLORS, "❌ Color count mismatch"
print(np.count_nonzero(used_pixels))
print(color_index + len(text_coords) + len(border_coords))

# === Save image ===
Image.fromarray(image_array, 'RGB').save("periodic_table_series_clustered.png")
print("✅ Saved as 'periodic_table_series_clustered.png'")

Author

ACJ
41 entries

Stats

Date
Colors16,777,216
Pixels16,777,216
Dimensions4,096 × 4,096
Bytes21,791,837