from __future__ import annotations from pathlib import Path from typing import Iterable import numpy as np from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageFont ROOT = Path("/home/nikola/codex-cli/projects/potkrovlje-dizajn-2026") OUT = ROOT / "za-pregled" / "warm-nordic-highres" OUT.mkdir(parents=True, exist_ok=True) def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: options = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf", ] for path in options: try: return ImageFont.truetype(path, size) except OSError: continue return ImageFont.load_default() TITLE = font(66, bold=True) HEAD = font(38, bold=True) TEXT = font(28) SMALL = font(22) def add_noise(arr: np.ndarray, strength: float) -> np.ndarray: noise = np.random.normal(0, strength, arr.shape).astype(np.float32) out = np.clip(arr.astype(np.float32) + noise, 0, 255) return out.astype(np.uint8) def vertical_gradient(size: tuple[int, int], top: tuple[int, int, int], bottom: tuple[int, int, int]) -> Image.Image: w, h = size base = np.zeros((h, w, 3), dtype=np.uint8) for i in range(h): t = i / max(h - 1, 1) base[i, :, :] = [int(top[c] * (1 - t) + bottom[c] * t) for c in range(3)] return Image.fromarray(base, "RGB") def wood_texture(size: tuple[int, int], base=(194, 159, 120), dark=(125, 90, 58), direction: str = "horizontal") -> Image.Image: w, h = size axis = w if direction == "horizontal" else h img = np.zeros((h, w, 3), dtype=np.uint8) grain = np.zeros(axis, dtype=np.float32) phase = np.random.uniform(0, np.pi) for i in range(axis): t = i / axis wave = ( 0.55 * np.sin(t * 28 + phase) + 0.22 * np.sin(t * 91 + 0.7) + 0.18 * np.sin(t * 160 + 1.9) ) grain[i] = wave grain += np.random.normal(0, 0.09, axis) grain = (grain - grain.min()) / (grain.max() - grain.min() + 1e-6) if direction == "horizontal": val = np.tile(grain, (h, 1)) else: val = np.tile(grain[:, None], (1, w)) for c in range(3): img[:, :, c] = (dark[c] * (1 - val) + base[c] * val).astype(np.uint8) img = add_noise(img, 5) return Image.fromarray(img, "RGB").filter(ImageFilter.GaussianBlur(0.6)) def concrete_texture(size: tuple[int, int], base=(194, 190, 183)) -> Image.Image: w, h = size arr = np.full((h, w, 3), base, dtype=np.uint8) arr = add_noise(arr, 12) specks = np.random.uniform(0, 1, (h, w)) arr[specks > 0.995] = np.clip(arr[specks > 0.995] - 30, 0, 255) return Image.fromarray(arr, "RGB").filter(ImageFilter.GaussianBlur(0.4)) def brick_texture(size: tuple[int, int]) -> Image.Image: w, h = size img = Image.new("RGB", size, (206, 191, 177)) draw = ImageDraw.Draw(img) brick_h = 42 brick_w = 105 shades = [(163, 111, 84), (174, 122, 91), (145, 97, 74), (186, 133, 99)] for row, y in enumerate(range(0, h, brick_h)): offset = 0 if row % 2 == 0 else brick_w // 2 for x in range(-offset, w, brick_w): c = shades[(row + x // max(brick_w, 1)) % len(shades)] inset = 4 draw.rounded_rectangle( (x + inset, y + inset, x + brick_w - 8, y + brick_h - 6), radius=3, fill=c, ) return img.filter(ImageFilter.GaussianBlur(0.2)) def linen_texture(size: tuple[int, int], base=(223, 213, 198), accent=(190, 177, 159)) -> Image.Image: w, h = size arr = np.zeros((h, w, 3), dtype=np.uint8) for y in range(h): for x in range(w): mix = 0.55 + 0.25 * np.sin(x / 12.0) + 0.2 * np.sin(y / 14.0) mix = max(0.0, min(1.0, mix)) arr[y, x] = [int(base[c] * mix + accent[c] * (1 - mix)) for c in range(3)] arr = add_noise(arr, 9) return Image.fromarray(arr, "RGB").filter(ImageFilter.GaussianBlur(0.5)) def slat_texture(size: tuple[int, int]) -> Image.Image: w, h = size base = Image.new("RGB", size, (74, 67, 62)) draw = ImageDraw.Draw(base) wood = wood_texture((46, h), base=(172, 136, 94), dark=(110, 76, 46), direction="vertical") for x in range(0, w, 56): base.paste(wood, (x + 8, 0)) return base.filter(ImageFilter.GaussianBlur(0.3)) def rug_texture(size: tuple[int, int], base=(206, 197, 184), line=(161, 150, 132)) -> Image.Image: img = linen_texture(size, base, line) draw = ImageDraw.Draw(img) w, h = size for y in range(40, h, 58): draw.line((40, y, w - 40, y), fill=(line[0] - 8, line[1] - 8, line[2] - 8), width=2) return img def repeat_texture(tex: Image.Image, size: tuple[int, int]) -> Image.Image: w, h = size tw, th = tex.size out = Image.new("RGB", size) for y in range(0, h, th): for x in range(0, w, tw): out.paste(tex, (x, y)) return out def find_perspective_coeffs(dst_pts, src_pts): matrix = [] for (x, y), (u, v) in zip(dst_pts, src_pts): matrix.append([x, y, 1, 0, 0, 0, -u * x, -u * y]) matrix.append([0, 0, 0, x, y, 1, -v * x, -v * y]) a = np.array(matrix, dtype=np.float64) b = np.array(src_pts).reshape(8) coeffs = np.linalg.solve(a, b) return coeffs def polygon_mask(size: tuple[int, int], pts: list[tuple[int, int]]) -> Image.Image: mask = Image.new("L", size, 0) ImageDraw.Draw(mask).polygon(pts, fill=255) return mask def warp_texture_to_polygon(tex: Image.Image, polygon: list[tuple[int, int]], canvas_size: tuple[int, int]) -> tuple[Image.Image, Image.Image]: xs = [p[0] for p in polygon] ys = [p[1] for p in polygon] min_x, max_x = min(xs), max(xs) min_y, max_y = min(ys), max(ys) bw = max_x - min_x bh = max_y - min_y local = [(x - min_x, y - min_y) for x, y in polygon] src = [(0, 0), (tex.size[0], 0), (tex.size[0], tex.size[1]), (0, tex.size[1])] coeffs = find_perspective_coeffs(local, src) tiled = repeat_texture(tex, (bw + 20, bh + 20)) warped = tiled.transform((bw, bh), Image.PERSPECTIVE, coeffs, Image.Resampling.BICUBIC) layer = Image.new("RGBA", canvas_size, (0, 0, 0, 0)) mask = polygon_mask((bw, bh), local) layer.paste(warped.convert("RGBA"), (min_x, min_y), mask) canvas_mask = Image.new("L", canvas_size, 0) canvas_mask.paste(mask, (min_x, min_y)) return layer, canvas_mask def paste_polygon_texture(base: Image.Image, polygon: list[tuple[int, int]], tex: Image.Image, tint: tuple[int, int, int] | None = None, alpha: int = 255) -> None: layer, mask = warp_texture_to_polygon(tex, polygon, base.size) if tint is not None: tint_img = Image.new("RGBA", base.size, (*tint, 0)) tint_img.putalpha(mask.point(lambda p: int(p * alpha / 255))) layer = Image.blend(layer, tint_img, 0.18) base.alpha_composite(layer) def add_polygon_overlay(base: Image.Image, polygon: list[tuple[int, int]], color: tuple[int, int, int], opacity: int) -> None: overlay = Image.new("RGBA", base.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) draw.polygon(polygon, fill=(*color, opacity)) base.alpha_composite(overlay) def add_shadow(base: Image.Image, shape_box: tuple[int, int, int, int], offset=(18, 18), blur=24, opacity=80, radius=30) -> None: shadow = Image.new("RGBA", base.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(shadow) x1, y1, x2, y2 = shape_box draw.rounded_rectangle((x1 + offset[0], y1 + offset[1], x2 + offset[0], y2 + offset[1]), radius=radius, fill=(25, 24, 22, opacity)) shadow = shadow.filter(ImageFilter.GaussianBlur(blur)) base.alpha_composite(shadow) def rounded_box(base: Image.Image, box, fill, outline=None, width=1, radius=28, texture: Image.Image | None = None) -> None: if texture is not None: mask = Image.new("L", base.size, 0) ImageDraw.Draw(mask).rounded_rectangle(box, radius=radius, fill=255) tex = repeat_texture(texture, base.size).convert("RGBA") tex.putalpha(mask) base.alpha_composite(tex) overlay = Image.new("RGBA", base.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width) base.alpha_composite(overlay) def add_text_panel(base: Image.Image, title: str, subtitle: str) -> None: panel = Image.new("RGBA", base.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(panel) draw.rounded_rectangle((56, 48, 1030, 220), radius=30, fill=(244, 239, 230, 220)) draw.text((92, 78), title, font=HEAD, fill=(34, 34, 34)) draw.text((94, 138), subtitle, font=TEXT, fill=(82, 76, 68)) base.alpha_composite(panel) def draw_lounge_render() -> None: size = (3200, 1800) base = vertical_gradient(size, (243, 234, 223), (226, 214, 199)).convert("RGBA") left_roof = [(100, 450), (860, 170), (1180, 980), (420, 1270)] right_roof = [(2140, 170), (3100, 450), (2780, 1270), (2020, 980)] back_wall = [(860, 170), (2140, 170), (2560, 450), (2080, 980), (1180, 980), (640, 450)] floor = [(420, 1270), (2780, 1270), (2080, 980), (1180, 980)] paste_polygon_texture(base, floor, wood_texture((1200, 900), base=(152, 118, 84), dark=(98, 70, 44), direction="horizontal")) add_polygon_overlay(base, floor, (40, 24, 10), 24) paste_polygon_texture(base, back_wall, brick_texture((1400, 900))) add_polygon_overlay(base, back_wall, (86, 67, 48), 18) paste_polygon_texture(base, left_roof, wood_texture((1600, 1200), base=(215, 184, 147), dark=(160, 122, 84), direction="vertical")) paste_polygon_texture(base, right_roof, wood_texture((1600, 1200), base=(215, 184, 147), dark=(160, 122, 84), direction="vertical")) add_polygon_overlay(base, left_roof, (75, 50, 25), 16) add_polygon_overlay(base, right_roof, (75, 50, 25), 26) beam = Image.new("RGBA", size, (0, 0, 0, 0)) draw = ImageDraw.Draw(beam) draw.line((840, 200, 2160, 200), fill=(116, 73, 44, 255), width=30) for x in [660, 920, 1180, 1440, 1700, 1960, 2220, 2480]: draw.line((x, 280, x + 150, 930), fill=(124, 80, 50, 255), width=24) base.alpha_composite(beam) light = Image.new("RGBA", size, (0, 0, 0, 0)) draw = ImageDraw.Draw(light) draw.polygon([(640, 380), (1020, 250), (1060, 1270), (560, 1270)], fill=(255, 243, 214, 38)) draw.polygon([(0, 820), (520, 710), (1600, 1770), (0, 1770)], fill=(255, 218, 154, 36)) base.alpha_composite(light.filter(ImageFilter.GaussianBlur(35))) wall_overlay = Image.new("RGBA", size, (0, 0, 0, 0)) dwo = ImageDraw.Draw(wall_overlay) dwo.rounded_rectangle((860, 330, 1120, 930), radius=12, fill=(140, 97, 60, 255), outline=(104, 68, 41, 255), width=5) dwo.rectangle((885, 355, 1090, 900), fill=(181, 208, 223, 255)) dwo.line((988, 355, 988, 900), fill=(235, 241, 245, 255), width=5) dwo.rounded_rectangle((1360, 325, 1600, 935), radius=12, fill=(138, 95, 58, 255), outline=(100, 66, 39, 255), width=5) dwo.rectangle((1385, 350, 1570, 905), fill=(189, 209, 220, 255)) dwo.rounded_rectangle((1640, 345, 1870, 910), radius=12, fill=(138, 95, 58, 255), outline=(100, 66, 39, 255), width=5) dwo.rectangle((1664, 372, 1844, 880), fill=(192, 212, 222, 255)) base.alpha_composite(wall_overlay) # Central storage / media core core_front = [(1730, 575), (2070, 575), (2070, 1210), (1730, 1210)] core_side = [(2070, 575), (2285, 685), (2285, 1325), (2070, 1210)] paste_polygon_texture(base, core_front, slat_texture((600, 900))) paste_polygon_texture(base, core_side, wood_texture((500, 900), base=(183, 150, 113), dark=(118, 87, 58), direction="vertical")) add_polygon_overlay(base, core_front, (46, 38, 30), 22) add_polygon_overlay(base, core_side, (38, 30, 24), 28) storage_draw = ImageDraw.Draw(base) storage_draw.rounded_rectangle((1770, 660, 2030, 790), radius=12, fill=(28, 28, 30, 255)) storage_draw.rounded_rectangle((1788, 835, 2015, 960), radius=16, fill=(133, 98, 64, 255)) storage_draw.rectangle((1810, 870, 1992, 908), fill=(70, 56, 45, 255)) storage_draw.rounded_rectangle((1768, 1010, 2017, 1165), radius=18, outline=(206, 181, 148, 180), width=3) storage_draw.line((2190, 745, 2190, 1255), fill=(86, 64, 45, 200), width=3) # TV, floating media shelf rounded_box(base, (1835, 680, 2010, 775), (18, 18, 20, 255), outline=(42, 42, 42, 255), width=4, radius=10) rounded_box(base, (1815, 805, 2025, 845), (126, 93, 63, 220), radius=10) # Coffee nook add_shadow(base, (920, 935, 1275, 1210), blur=20, opacity=70, radius=24) rounded_box(base, (920, 935, 1275, 1210), (142, 109, 77, 255), texture=wood_texture((400, 400), base=(162, 126, 92), dark=(109, 79, 53), direction="vertical")) top = Image.new("RGBA", size, (0, 0, 0, 0)) ImageDraw.Draw(top).rounded_rectangle((900, 905, 1295, 965), radius=18, fill=(230, 224, 212, 255)) base.alpha_composite(top) coffee = Image.new("RGBA", size, (0, 0, 0, 0)) dc = ImageDraw.Draw(coffee) dc.rectangle((987, 820, 1180, 900), fill=(188, 170, 142, 220)) dc.rectangle((1055, 760, 1115, 820), fill=(230, 230, 228, 255)) dc.ellipse((1070, 730, 1110, 770), fill=(250, 250, 250, 255)) dc.rounded_rectangle((1210, 855, 1260, 905), radius=10, fill=(72, 72, 72, 255)) base.alpha_composite(coffee) # Sofa and rug add_shadow(base, (1090, 1110, 1730, 1445), blur=32, opacity=80, radius=46) rounded_box(base, (1010, 1135, 1760, 1450), (196, 181, 163, 235), texture=linen_texture((900, 500), base=(203, 189, 171), accent=(173, 158, 140))) rounded_box(base, (1010, 1055, 1450, 1205), (205, 193, 178, 245), texture=linen_texture((600, 260), base=(209, 196, 180), accent=(185, 170, 151))) rounded_box(base, (1350, 1015, 1745, 1185), (202, 189, 174, 245), texture=linen_texture((550, 260), base=(206, 193, 177), accent=(181, 166, 149))) add_shadow(base, (1115, 1270, 1615, 1530), blur=25, opacity=60, radius=60) rounded_box(base, (1125, 1280, 1625, 1525), (235, 227, 214, 215), texture=rug_texture((800, 420))) add_shadow(base, (1290, 1180, 1510, 1350), blur=20, opacity=55, radius=80) rounded_box(base, (1295, 1188, 1515, 1348), (188, 153, 112, 235), texture=wood_texture((300, 220), base=(192, 157, 114), dark=(128, 95, 62), direction="horizontal"), radius=90) # Sideboard under slope rounded_box(base, (2440, 980, 2890, 1135), (173, 154, 137, 220), texture=wood_texture((600, 220), base=(177, 152, 124), dark=(119, 94, 68), direction="horizontal")) deco = Image.new("RGBA", size, (0, 0, 0, 0)) dd = ImageDraw.Draw(deco) dd.ellipse((2480, 880, 2570, 1035), fill=(104, 124, 88, 255)) dd.rectangle((2515, 975, 2535, 1035), fill=(122, 94, 70, 255)) dd.ellipse((2605, 905, 2660, 990), fill=(205, 186, 166, 255)) dd.ellipse((2690, 912, 2745, 995), fill=(222, 214, 202, 255)) base.alpha_composite(deco) add_text_panel(base, "Warm Nordic Lounge", "Terrace side: living room, TV wall on storage core, integrated coffee nook.") draw = ImageDraw.Draw(base) draw.text((74, 1660), "Storage koncept: hrastove letvice oko centralnog volumena skrivaju posteljinu, kofer, sezonske stvari i utility zonu.", font=SMALL, fill=(58, 53, 47)) base = base.filter(ImageFilter.UnsharpMask(radius=2, percent=130, threshold=2)) base.save(OUT / "warm-nordic-lounge-render.png", quality=95) def draw_studio_render() -> None: size = (3200, 1800) base = vertical_gradient(size, (239, 233, 224), (219, 209, 196)).convert("RGBA") left_roof = [(90, 420), (880, 160), (1220, 980), (380, 1290)] right_roof = [(2060, 160), (3110, 420), (2820, 1290), (1980, 980)] back_wall = [(880, 160), (2060, 160), (2520, 420), (2040, 980), (1220, 980), (640, 420)] floor = [(380, 1290), (2820, 1290), (2040, 980), (1220, 980)] paste_polygon_texture(base, floor, wood_texture((1200, 900), base=(148, 115, 84), dark=(92, 67, 45), direction="horizontal")) add_polygon_overlay(base, floor, (30, 18, 8), 34) paste_polygon_texture(base, back_wall, brick_texture((1400, 900))) add_polygon_overlay(base, back_wall, (74, 56, 40), 30) paste_polygon_texture(base, left_roof, wood_texture((1600, 1200), base=(209, 177, 139), dark=(149, 111, 76), direction="vertical")) paste_polygon_texture(base, right_roof, wood_texture((1600, 1200), base=(209, 177, 139), dark=(149, 111, 76), direction="vertical")) add_polygon_overlay(base, left_roof, (68, 48, 28), 22) add_polygon_overlay(base, right_roof, (50, 34, 18), 35) beam = Image.new("RGBA", size, (0, 0, 0, 0)) draw = ImageDraw.Draw(beam) draw.line((860, 200, 2100, 200), fill=(96, 60, 38, 255), width=28) for x in [690, 965, 1240, 1515, 1790, 2065, 2340, 2615]: draw.line((x, 260, x - 160, 960), fill=(105, 68, 44, 255), width=23) base.alpha_composite(beam) win = Image.new("RGBA", size, (0, 0, 0, 0)) dw = ImageDraw.Draw(win) for box in [(1180, 325, 1400, 930), (1570, 345, 1795, 920)]: dw.rounded_rectangle(box, radius=10, fill=(137, 98, 61, 255), outline=(103, 67, 39, 255), width=5) x1, y1, x2, y2 = box dw.rectangle((x1 + 24, y1 + 24, x2 - 24, y2 - 24), fill=(190, 210, 222, 255)) dw.line(((x1 + x2) // 2, y1 + 24, (x1 + x2) // 2, y2 - 24), fill=(234, 240, 244, 255), width=4) base.alpha_composite(win) light = Image.new("RGBA", size, (0, 0, 0, 0)) dl = ImageDraw.Draw(light) dl.polygon([(1115, 380), (1835, 395), (1920, 1450), (1020, 1450)], fill=(255, 236, 205, 32)) base.alpha_composite(light.filter(ImageFilter.GaussianBlur(40))) # Storage / acoustic core core_front = [(930, 560), (1320, 560), (1320, 1240), (930, 1240)] core_side = [(1320, 560), (1545, 675), (1545, 1358), (1320, 1240)] paste_polygon_texture(base, core_front, slat_texture((650, 950))) paste_polygon_texture(base, core_side, wood_texture((400, 950), base=(176, 145, 110), dark=(112, 80, 54), direction="vertical")) add_polygon_overlay(base, core_front, (38, 33, 29), 28) add_polygon_overlay(base, core_side, (28, 22, 18), 22) drawc = ImageDraw.Draw(base) drawc.rounded_rectangle((980, 675, 1268, 1010), radius=18, fill=(160, 128, 92, 100), outline=(220, 192, 152, 180), width=3) drawc.rounded_rectangle((1000, 1035, 1248, 1180), radius=18, outline=(220, 192, 152, 180), width=3) drawc.rounded_rectangle((1360, 760, 1508, 900), radius=12, fill=(42, 42, 44, 255)) # Studio desk add_shadow(base, (1580, 990, 2500, 1235), blur=24, opacity=82, radius=24) rounded_box(base, (1580, 990, 2500, 1235), (120, 88, 61, 240), texture=wood_texture((1000, 320), base=(140, 105, 74), dark=(91, 64, 41), direction="horizontal"), radius=16) desk = Image.new("RGBA", size, (0, 0, 0, 0)) dd = ImageDraw.Draw(desk) dd.rounded_rectangle((1755, 830, 2005, 980), radius=16, fill=(28, 28, 30, 255)) dd.rounded_rectangle((2040, 830, 2295, 980), radius=16, fill=(28, 28, 30, 255)) dd.rectangle((1715, 905, 2340, 930), fill=(38, 38, 40, 255)) dd.rectangle((1720, 780, 1785, 990), fill=(45, 45, 47, 255)) dd.rectangle((2260, 780, 2325, 990), fill=(45, 45, 47, 255)) dd.rectangle((1910, 870, 2050, 955), fill=(231, 225, 214, 255)) dd.rectangle((2060, 1040, 2125, 1235), fill=(53, 53, 55, 255)) base.alpha_composite(desk) # Lounge daybed add_shadow(base, (430, 1090, 1220, 1435), blur=28, opacity=72, radius=40) rounded_box(base, (430, 1105, 1220, 1435), (191, 178, 161, 235), texture=linen_texture((900, 400), base=(198, 186, 170), accent=(166, 151, 135)), radius=34) rounded_box(base, (465, 1045, 1150, 1175), (202, 190, 175, 245), texture=linen_texture((760, 180), base=(205, 194, 179), accent=(172, 157, 141)), radius=28) rounded_box(base, (615, 1240, 900, 1380), (145, 110, 84, 235), texture=linen_texture((360, 180), base=(150, 118, 93), accent=(120, 93, 71)), radius=22) # Acoustic wall and guitars paste_polygon_texture(base, [(2530, 520), (2850, 635), (2850, 1160), (2530, 1060)], slat_texture((500, 700))) acc = Image.new("RGBA", size, (0, 0, 0, 0)) da = ImageDraw.Draw(acc) da.line((2670, 870, 2670, 1160), fill=(99, 74, 52, 255), width=10) da.ellipse((2630, 785, 2710, 905), outline=(148, 113, 76, 255), width=8) da.line((2760, 845, 2760, 1140), fill=(99, 74, 52, 255), width=10) da.ellipse((2720, 760, 2800, 880), outline=(148, 113, 76, 255), width=8) base.alpha_composite(acc) # Low storage under slope rounded_box(base, (2330, 1080, 2900, 1220), (173, 155, 137, 220), texture=wood_texture((620, 220), base=(178, 151, 123), dark=(118, 93, 67), direction="horizontal"), radius=20) # Rug add_shadow(base, (1520, 1200, 2450, 1490), blur=20, opacity=50, radius=40) rounded_box(base, (1540, 1210, 2460, 1490), (227, 221, 210, 220), texture=rug_texture((1000, 320), base=(221, 213, 201), line=(175, 162, 145)), radius=28) add_text_panel(base, "Warm Nordic Home Studio", "Stair side: recording desk, acoustic slat storage spine, lounge/daybed for listening sessions.") draw = ImageDraw.Draw(base) draw.text((74, 1660), "Storage spine: zatvoreni ormari gore za kabaste stvari, otvorene nishe za knjige/ploca, utility rack prema studiju.", font=SMALL, fill=(58, 53, 47)) base = base.filter(ImageFilter.UnsharpMask(radius=2, percent=130, threshold=2)) base.save(OUT / "warm-nordic-studio-render.png", quality=95) def draw_layout_plan() -> None: size = (2600, 1800) base = Image.new("RGBA", size, (244, 240, 234, 255)) draw = ImageDraw.Draw(base) draw.rounded_rectangle((160, 160, 2440, 1640), radius=24, fill=(248, 245, 241, 255), outline=(66, 60, 55, 255), width=6) draw.rectangle((300, 1220, 760, 1540), fill=(238, 236, 232, 255), outline=(140, 90, 90, 255), width=5) draw.text((362, 1350), "Stepenice", font=HEAD, fill=(124, 55, 55)) draw.rectangle((1040, 460, 1510, 1320), fill=(186, 156, 120, 255), outline=(110, 84, 58, 255), width=5) draw.text((1080, 780), "Storage\ncore", font=TITLE, fill=(60, 40, 26)) # lounge zone draw.rounded_rectangle((250, 250, 980, 1070), radius=26, fill=(230, 214, 195, 255), outline=(150, 123, 94, 255), width=4) draw.text((390, 310), "Living / Lounge", font=TITLE, fill=(82, 58, 39)) draw.rounded_rectangle((330, 655, 740, 820), radius=30, fill=(197, 183, 166, 255)) draw.rounded_rectangle((565, 875, 790, 995), radius=60, fill=(190, 153, 112, 255)) draw.rectangle((790, 640, 950, 760), fill=(28, 28, 30, 255)) draw.rectangle((300, 905, 500, 1025), fill=(145, 110, 80, 255)) draw.text((300, 1045), "coffee nook", font=TEXT, fill=(90, 65, 45)) draw.text((750, 785), "TV", font=TEXT, fill=(240, 240, 240)) # studio zone draw.rounded_rectangle((1560, 250, 2320, 1440), radius=26, fill=(220, 227, 221, 255), outline=(109, 128, 112, 255), width=4) draw.text((1720, 310), "Music Studio", font=TITLE, fill=(57, 80, 65)) draw.rectangle((1680, 650, 2120, 790), fill=(121, 88, 63, 255)) draw.rectangle((1760, 520, 1885, 630), fill=(28, 28, 30, 255)) draw.rectangle((1925, 520, 2050, 630), fill=(28, 28, 30, 255)) draw.rounded_rectangle((1680, 1030, 2150, 1210), radius=26, fill=(194, 181, 165, 255)) draw.text((1685, 800), "recording desk", font=TEXT, fill=(49, 40, 33)) draw.text((1710, 1225), "listening lounge / daybed", font=TEXT, fill=(67, 80, 72)) # storage logic draw.rounded_rectangle((930, 250, 1620, 390), radius=22, fill=(216, 204, 186, 255), outline=(147, 126, 104, 255), width=3) draw.text((960, 295), "gornji zatvoreni storage za posteljinu, kofere, sezonske stvari", font=TEXT, fill=(69, 56, 43)) draw.rounded_rectangle((930, 1380, 1620, 1520), radius=22, fill=(216, 204, 186, 255), outline=(147, 126, 104, 255), width=3) draw.text((995, 1425), "nise / utility / skriveni kablovi / rack", font=TEXT, fill=(69, 56, 43)) draw.text((170, 70), "Warm Nordic Loft - finalni koncept rasporeda", font=TITLE, fill=(28, 28, 28)) draw.text((170, 1700), "Centralni storage volumen resava stubove i odzak kao dizajn feature, a ne kao smetnju.", font=TEXT, fill=(44, 44, 44)) base.save(OUT / "warm-nordic-layout-plan.png", quality=95) def write_concept_note() -> None: note = """# Warm Nordic Lounge + Studio ## Finalni pravac - terasa strana: living room / lounge - TV je vezan za centralni storage volumen - coffee making kutak je uz lounge zonu, blizu terase - stepenice strana: home music recording studio + listening lounge - izmedju: storage spine oko stubova i odzaka ## Kako storage poboljsava ambijent - obloziti centralni volumen hrastovim vertikalnim letvicama ili finim furnirom - gornje fronte zatvorene i mirne, push-open, bez vizuelnog nereda - po jedna otvorena nisa prema svakoj zoni za dekor, knjige, ploce ili kafe set - TV strana ostaje cista i smirena - studio strana dobija integrisanu akusticku logiku i skriveni kablovski management ## Materijali - svetliji hrast / smoked oak u toplom tonu - opeka ostaje vidljiva i ociscena - plafonska drvena obloga prirodan mat ton - tekstil: lan, greige, pesak, maslina - metalni detalji: crni ili tamno bronzani ## Napomena Ovi vizuali su high-res textured concept renders, ne pravi V-Ray / Corona / AI photo renders. Za potpuno fotorealistican rezultat na tacnoj geometriji treba ili: 1. originalne fotografije kao lokalni fajlovi za photomontage, ili 2. 3D model u SketchUp / Blender + render engine. """ (OUT / "warm-nordic-koncept.md").write_text(note) if __name__ == "__main__": draw_layout_plan() draw_lounge_render() draw_studio_render() write_concept_note()