feat: initial commit
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user