From 5904fc675dfc9b993e87bc978a0577276caef841 Mon Sep 17 00:00:00 2001 From: erjemin Date: Sun, 6 Sep 2026 00:55:22 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20=D1=8F=D0=B4=D1=80=D0=BE=20(14)=20=D1=86?= =?UTF-8?q?=D0=B8=D1=84=D0=B5=D1=80=D0=BA=D0=B8=20=D0=B2=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B4=D0=B5=20=D1=84=D0=B8=D0=B3=D1=83=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hypn0/hypn0_site/forms.py | 2 +- hypn0/hypn0_site/services/halftone.py | 57 ++++++++++++++++++++------- hypn0/hypn0_site/tests.py | 9 ++++- hypn0/templates/index.html | 4 +- 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/hypn0/hypn0_site/forms.py b/hypn0/hypn0_site/forms.py index ea4dd3c..0289fa0 100644 --- a/hypn0/hypn0_site/forms.py +++ b/hypn0/hypn0_site/forms.py @@ -16,7 +16,7 @@ class HalftoneGenerateForm(forms.Form): ("cross", "Крестик"), ("line", "Линия"), ("snowflake", "Снежинка"), - ("wave", "Волна"), + ("binary", "Матрица (0/1)"), ("heart", "Сердечко"), ] diff --git a/hypn0/hypn0_site/services/halftone.py b/hypn0/hypn0_site/services/halftone.py index 7b348f4..132d54e 100644 --- a/hypn0/hypn0_site/services/halftone.py +++ b/hypn0/hypn0_site/services/halftone.py @@ -3,7 +3,7 @@ import math import random import re from collections import defaultdict -from typing import BinaryIO, Union +from typing import BinaryIO, Optional, Union from PIL import Image @@ -24,7 +24,9 @@ def encode_to_base36(num: int) -> str: return "".join(reversed(result)) -def generate_shape_def(shape: str, radius: int, shape_id: str) -> str: +def generate_shape_def( + shape: str, radius: int, shape_id: str, max_radius: int = 8, char: Optional[str] = None +) -> str: """Генерирует SVG-элемент для тега в зависимости от типа фигуры.""" r = radius match shape: @@ -88,6 +90,18 @@ def generate_shape_def(shape: str, radius: int, shape_id: str) -> str: pts_str = " ".join(points) return f'' + case "binary": + if char is None: + # Вероятностная функция: вероятность "0" пропорциональна оптической плотности + # В тенях преобладают '0' (~85%), но с шансом ~15% выскакивают '1' + # В светах преобладают '1' (~85%), но с шансом ~15% выскакивают '0' + prob_zero = min(0.85, max(0.15, (r / max_radius) if max_radius else 0.5)) + char = "0" if random.random() < prob_zero else "1" + font_size = max(4, round(r * 2.0)) + return ( + f'{char}' + ) + case "heart": r_top = round(r * 0.3) r_mid = round(r * 0.4) @@ -194,7 +208,7 @@ def generate_halftone_svg( # 2. Обход пикселей и группировка elements_by_class = defaultdict(list) - unique_radii = set() + unique_shape_keys = set() is_animated = blink > 0 for y in range(grid_height): @@ -216,14 +230,24 @@ def generate_halftone_svg( anim_index = rng.randint(0, animation_variants - 1) if is_animated else 0 encoded_class = encode_to_base36(anim_index) - elements_by_class[encoded_class].append((radius, cx, cy, anim_index)) - unique_radii.add(radius) + if shape == "binary": + # Вероятность выпадения '0' плавно растет с оптической плотностью + # В тенях преобладают '0' (~85%), но с шансом ~15% выскакивают '1' + # В светах преобладают '1' (~85%), но с шансом ~15% выскакивают '0' + prob_zero = 0.15 + 0.70 * (radius / max_radius if max_radius else factor) + char = "0" if rng.random() < prob_zero else "1" + shape_id = f"s{encode_to_base36(radius)}{char}" + unique_shape_keys.add((radius, char, shape_id)) + else: + shape_id = f"s{encode_to_base36(radius)}" + unique_shape_keys.add((radius, None, shape_id)) + + elements_by_class[encoded_class].append((shape_id, cx, cy, anim_index)) # 3. Формирование defs_list = [""] - for radius in sorted(unique_radii): - shape_id = f"s{encode_to_base36(radius)}" - defs_list.append(generate_shape_def(shape, radius, shape_id)) + for radius, char, shape_id in sorted(unique_shape_keys, key=lambda k: (k[0], k[1] or "")): + defs_list.append(generate_shape_def(shape, radius, shape_id, max_radius=max_radius, char=char)) defs_list.append("") defs_html = "".join(defs_list) @@ -254,8 +278,8 @@ def generate_halftone_svg( animation_classes.append(f".a{encoded_class}{{--d:{delay_str}s}}") uses = "".join( - f'' - for radius, cx, cy, _ in group_elements + f'' + for shape_id, cx, cy, _ in group_elements ) if uses: groups.append(f'{uses}') @@ -299,9 +323,9 @@ def generate_halftone_svg( svg_css = ( f"" ) @@ -340,7 +364,7 @@ def prepare_gallery_svg(svg_content: str) -> str: hover_css = ( "svg{--hypn0-play:paused}" ":host(:hover) svg,svg:hover{--hypn0-play:running!important}" - "circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}" + "circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}" ) if "" in svg_content: @@ -364,8 +388,10 @@ def prepare_active_svg(svg_content: str) -> str: gallery_css_variants = [ "svg{--hypn0-play:paused}:host(:hover) svg,svg:hover{--hypn0-play:running!important}.shape,circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}", "svg{--hypn0-play:paused}:host(:hover) svg,svg:hover{--hypn0-play:running!important}circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}", + "svg{--hypn0-play:paused}:host(:hover) svg,svg:hover{--hypn0-play:running!important}circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}", "svg{--hypn0-play:paused}svg:hover{--hypn0-play:running}.shape,circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}", "svg{--hypn0-play:paused}svg:hover{--hypn0-play:running}circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}", + "svg{--hypn0-play:paused}svg:hover{--hypn0-play:running}circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}", ] cleaned = svg_content for gcss in gallery_css_variants: @@ -374,7 +400,7 @@ def prepare_active_svg(svg_content: str) -> str: cleaned = re.sub(r"svg\s*\{[^}]*--hypn0-play:\s*paused[^}]*\}", "", cleaned) cleaned = re.sub(r"(:host\(:hover\)\s*svg\s*,\s*)?svg:hover\s*\{[^}]*--hypn0-play:[^}]*\}", "", cleaned) cleaned = re.sub( - r"(\.shape,)?circle,rect,polygon,path\s*\{animation-play-state:\s*var\(--hypn0-play,\s*paused\)!important\}", + r"(\.shape,)?circle,rect,polygon,path(,text)?\s*\{animation-play-state:\s*var\(--hypn0-play,\s*paused\)!important\}", "", cleaned, ) @@ -423,7 +449,8 @@ def analyze_svg_structure(svg_content: str) -> dict: rect_count = len(re.findall(r"Generated by Hypn0", svg) def test_generate_shapes(self): - shapes = ["circle", "ring", "square", "diamond", "triangle", "hexagon", "star", "cross", "line", "snowflake", "heart"] + shapes = ["circle", "ring", "square", "diamond", "triangle", "hexagon", "star", "cross", "line", "snowflake", "binary", "heart"] for shape in shapes: svg = generate_halftone_svg(self.img, cols=20, max_radius=5, shape=shape) self.assertIn("0", binary_svg) + self.assertIn(">1", binary_svg) + def test_generate_static_blink_zero(self): svg = generate_halftone_svg(self.img, cols=20, blink=0) self.assertIn("animation:none", svg) diff --git a/hypn0/templates/index.html b/hypn0/templates/index.html index 43cb461..3ad4808 100644 --- a/hypn0/templates/index.html +++ b/hypn0/templates/index.html @@ -260,8 +260,8 @@ - -