mod: ядро (16) наклон сетки
This commit is contained in:
@@ -146,18 +146,9 @@ def generate_halftone_svg(
|
|||||||
- animation_variants: число вариантов CSS-задержек
|
- animation_variants: число вариантов CSS-задержек
|
||||||
- seed: сид для детерминированного распределения анимаций
|
- seed: сид для детерминированного распределения анимаций
|
||||||
|
|
||||||
TODO ПОЧИНИТЬ-УЛУЧШИТЬ:
|
ПОЧИНИТЬ-УЛУЧШИТЬ:
|
||||||
- angle: сейчас это просто поворот картинки. Нужно чтобы:
|
-- ЕСЛИ ЦВЕТ ЧЕРЕЗ ИНТЕРФЕЙС НЕ МЕНЯЛСЯ (ДЕФОЛТНЫЙ), ТО ЛУЧШЕ (???) НАЗНАЧАТЬ СЛУЧАЙНЫЙ (или из набора).
|
||||||
- нужно чтобы это был поворт картинки (т.е. ряды-столбцы были со смещение-поворотом, как при полиграфии)
|
А то слишком однообразные цвета генераций в галерее
|
||||||
- blink: что-то страннное. пока не понял
|
|
||||||
|
|
||||||
-- ПРОЧЕЕ:
|
|
||||||
-- C "кольцами" что-то не так (они кружочки)
|
|
||||||
-- Проверить: scale и rotation кажется не вокруг центра.
|
|
||||||
-- ЕСЛИ ЦВЕТ ЧЕРЕЗ ИНТЕРФЕЙС НЕ МЕНЯЛСЯ (ДЕФОЛТНЫЙ), ТО ЛУЧШЕ (???) НАЗНАЧАТЬ СЛУЧАНЫЙ (или из набора).
|
|
||||||
А то слишком однообразные цвета герераций в галерее
|
|
||||||
-- Если задан большой размер точки, rotation или scale -- то нужно делать "паспорту", иначе точки "вылезают"
|
|
||||||
за пределы ViewPort
|
|
||||||
"""
|
"""
|
||||||
rng = random.Random(seed)
|
rng = random.Random(seed)
|
||||||
|
|
||||||
@@ -178,12 +169,8 @@ def generate_halftone_svg(
|
|||||||
# Конвертируем в Grayscale
|
# Конвертируем в Grayscale
|
||||||
img = pil_img.convert("L")
|
img = pil_img.convert("L")
|
||||||
|
|
||||||
# Обработка наклона сетки (angle): поворачиваем изображение при необходимости
|
# Вычисляем размеры базовой сетки по исходным пропорциям изображения
|
||||||
if angle != 0:
|
# Значение `cols` задает число ячеек по наибольшей стороне (ширине или высоте)
|
||||||
# Поворачиваем с сохранением пропорций и белым фоном (255 = прозрачно/пусто)
|
|
||||||
img = img.rotate(-angle, resample=Image.Resampling.BICUBIC, expand=True, fillcolor=255)
|
|
||||||
|
|
||||||
# Вычисляем размеры сетки: значение `cols` задает число точек по наибольшей стороне (ширине или высоте)
|
|
||||||
if img.width >= img.height:
|
if img.width >= img.height:
|
||||||
grid_width = max(1, cols)
|
grid_width = max(1, cols)
|
||||||
grid_height = max(1, round(cols * (img.height / img.width)))
|
grid_height = max(1, round(cols * (img.height / img.width)))
|
||||||
@@ -192,6 +179,7 @@ def generate_halftone_svg(
|
|||||||
grid_width = max(1, round(cols * (img.width / img.height)))
|
grid_width = max(1, round(cols * (img.width / img.height)))
|
||||||
|
|
||||||
img_resized = img.resize((grid_width, grid_height), Image.Resampling.LANCZOS)
|
img_resized = img.resize((grid_width, grid_height), Image.Resampling.LANCZOS)
|
||||||
|
img_w, img_h = img_resized.width, img_resized.height
|
||||||
|
|
||||||
# Базовый шаг ячейки сетки (номинальный радиус касания точек R0 = 10, диаметр D0 = 20)
|
# Базовый шаг ячейки сетки (номинальный радиус касания точек R0 = 10, диаметр D0 = 20)
|
||||||
STEP = 20
|
STEP = 20
|
||||||
@@ -206,15 +194,76 @@ def generate_halftone_svg(
|
|||||||
width = grid_width * STEP + 2 * margin
|
width = grid_width * STEP + 2 * margin
|
||||||
height = grid_height * STEP + 2 * margin
|
height = grid_height * STEP + 2 * margin
|
||||||
|
|
||||||
# 2. Обход пикселей и группировка
|
# Функция билинейной интерполяции для сэмплирования яркости в дробных координатах сетки
|
||||||
|
def sample_brightness(fx: float, fy: float) -> float:
|
||||||
|
"""Возвращает интерполированную яркость (0-255) в нормализованных координатах img_resized."""
|
||||||
|
if fx < 0 or fx > img_w - 1 or fy < 0 or fy > img_h - 1:
|
||||||
|
# За пределами изображения считаем фон белым (255)
|
||||||
|
if fx < -0.5 or fx > img_w - 0.5 or fy < -0.5 or fy > img_h - 0.5:
|
||||||
|
return 255.0
|
||||||
|
fx = max(0.0, min(img_w - 1.0, fx))
|
||||||
|
fy = max(0.0, min(img_h - 1.0, fy))
|
||||||
|
|
||||||
|
x0 = int(math.floor(fx))
|
||||||
|
y0 = int(math.floor(fy))
|
||||||
|
x1 = min(x0 + 1, img_w - 1)
|
||||||
|
y1 = min(y0 + 1, img_h - 1)
|
||||||
|
|
||||||
|
wx = fx - x0
|
||||||
|
wy = fy - y0
|
||||||
|
|
||||||
|
p00 = img_resized.getpixel((x0, y0))
|
||||||
|
p10 = img_resized.getpixel((x1, y0))
|
||||||
|
p01 = img_resized.getpixel((x0, y1))
|
||||||
|
p11 = img_resized.getpixel((x1, y1))
|
||||||
|
|
||||||
|
top = p00 * (1.0 - wx) + p10 * wx
|
||||||
|
bottom = p01 * (1.0 - wx) + p11 * wx
|
||||||
|
return top * (1.0 - wy) + bottom * wy
|
||||||
|
|
||||||
|
# 2. Обход узлов наклонной сетки и группировка
|
||||||
elements_by_class = defaultdict(list)
|
elements_by_class = defaultdict(list)
|
||||||
unique_shape_keys = set()
|
unique_shape_keys = set()
|
||||||
is_animated = blink > 0
|
is_animated = blink > 0
|
||||||
|
|
||||||
for y in range(grid_height):
|
# Центр изображения на холсте (в координатах SVG без марджина)
|
||||||
for x in range(grid_width):
|
canvas_cx = (grid_width * STEP) / 2.0
|
||||||
brightness = img_resized.getpixel((x, y))
|
canvas_cy = (grid_height * STEP) / 2.0
|
||||||
factor = (255 - brightness) / 255.0
|
|
||||||
|
rad = math.radians(angle)
|
||||||
|
cos_a = math.cos(rad)
|
||||||
|
sin_a = math.sin(rad)
|
||||||
|
|
||||||
|
# Векторы шага сетки вдоль осей U (строка) и V (столбец)
|
||||||
|
dx_u, dy_u = STEP * cos_a, STEP * sin_a
|
||||||
|
dx_v, dy_v = -STEP * sin_a, STEP * cos_a
|
||||||
|
|
||||||
|
# Диапазон охвата узлов сетки с запасом, чтобы перекрыть весь повернутый холст
|
||||||
|
max_dim = math.hypot(grid_width, grid_height)
|
||||||
|
u_range = int(math.ceil(max_dim / 2.0)) + 2
|
||||||
|
v_range = int(math.ceil(max_dim / 2.0)) + 2
|
||||||
|
|
||||||
|
# Перебираем узлы сетки вокруг центра
|
||||||
|
for v in range(-v_range, v_range + 1):
|
||||||
|
for u in range(-u_range, u_range + 1):
|
||||||
|
# Точные вещественные координаты узла относительно центра холста
|
||||||
|
rel_x = u * dx_u + v * dx_v
|
||||||
|
rel_y = u * dy_u + v * dy_v
|
||||||
|
|
||||||
|
# Координаты на видимом холсте (от 0 до grid_width * STEP)
|
||||||
|
pos_x = canvas_cx + rel_x
|
||||||
|
pos_y = canvas_cy + rel_y
|
||||||
|
|
||||||
|
# Проверяем, попадает ли узел в пределы видимой области холста
|
||||||
|
if not (0 <= pos_x <= grid_width * STEP and 0 <= pos_y <= grid_height * STEP):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Координаты в сетке img_resized
|
||||||
|
sample_fx = (pos_x / (grid_width * STEP)) * (img_w - 1)
|
||||||
|
sample_fy = (pos_y / (grid_height * STEP)) * (img_h - 1)
|
||||||
|
|
||||||
|
brightness = sample_brightness(sample_fx, sample_fy)
|
||||||
|
factor = (255.0 - brightness) / 255.0
|
||||||
|
|
||||||
# Отсекаем слишком светлые участки (шум фона)
|
# Отсекаем слишком светлые участки (шум фона)
|
||||||
if factor < 0.12:
|
if factor < 0.12:
|
||||||
@@ -224,16 +273,15 @@ def generate_halftone_svg(
|
|||||||
if radius == 0:
|
if radius == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cx = margin + x * STEP + STEP // 2
|
# Итоговые координаты центра точки в SVG (с учетом паспарту-margin)
|
||||||
cy = margin + y * STEP + STEP // 2
|
cx = round(margin + pos_x)
|
||||||
|
cy = round(margin + pos_y)
|
||||||
|
|
||||||
anim_index = rng.randint(0, animation_variants - 1) if is_animated else 0
|
anim_index = rng.randint(0, animation_variants - 1) if is_animated else 0
|
||||||
encoded_class = encode_to_base36(anim_index)
|
encoded_class = encode_to_base36(anim_index)
|
||||||
|
|
||||||
if shape == "binary":
|
if shape == "binary":
|
||||||
# Вероятность выпадения '0' плавно растет с оптической плотностью
|
# Вероятность выпадения '0' плавно растет с оптической плотностью
|
||||||
# В тенях преобладают '0' (~85%), но с шансом ~15% выскакивают '1'
|
|
||||||
# В светах преобладают '1' (~85%), но с шансом ~15% выскакивают '0'
|
|
||||||
prob_zero = 0.15 + 0.70 * (radius / max_radius if max_radius else factor)
|
prob_zero = 0.15 + 0.70 * (radius / max_radius if max_radius else factor)
|
||||||
char = "0" if rng.random() < prob_zero else "1"
|
char = "0" if rng.random() < prob_zero else "1"
|
||||||
shape_id = f"s{encode_to_base36(radius)}{char}"
|
shape_id = f"s{encode_to_base36(radius)}{char}"
|
||||||
|
|||||||
@@ -174,6 +174,16 @@ class HalftoneServiceTests(BaseMediaTestCase):
|
|||||||
svg_wide = generate_halftone_svg(wide_img, cols=30, max_radius=5)
|
svg_wide = generate_halftone_svg(wide_img, cols=30, max_radius=5)
|
||||||
self.assertIn('viewBox="0 0 608 208"', svg_wide)
|
self.assertIn('viewBox="0 0 608 208"', svg_wide)
|
||||||
|
|
||||||
|
def test_generate_screen_angles(self):
|
||||||
|
# Проверяем поворот растровой сетки под разными углами (-45, 0, 15, 45)
|
||||||
|
# Холст должен сохранять прямые пропорции (viewBox), а внутри генерироваться точки
|
||||||
|
for angle in [-45, -15, 0, 15, 45]:
|
||||||
|
svg = generate_halftone_svg(self.img, cols=20, max_radius=6, angle=angle)
|
||||||
|
self.assertIn("<svg", svg)
|
||||||
|
self.assertIn("<use href=", svg)
|
||||||
|
# viewbox не меняется от поворота сетки
|
||||||
|
self.assertIn('viewBox="0 0 408 408"', svg)
|
||||||
|
|
||||||
|
|
||||||
class HalftoneFormTests(BaseMediaTestCase):
|
class HalftoneFormTests(BaseMediaTestCase):
|
||||||
"""Тестирование формы валидации HalftoneGenerateForm."""
|
"""Тестирование формы валидации HalftoneGenerateForm."""
|
||||||
|
|||||||
Reference in New Issue
Block a user