mod: Куча оптимизаций: группировки кружочков одного радиуса, переиспользование кружочков через use и т.п. Размер результирующего файла меньше на ~30-70%

This commit is contained in:
2026-07-18 17:53:04 +03:00
parent 6176626ca0
commit 4dd039d6e7
7 changed files with 45 additions and 17 deletions
+2 -3
View File
@@ -8,8 +8,7 @@
Генерирует SVG халфтоновый эффект из изображения с анимацией мерцания точек. Генерирует SVG халфтоновый эффект из изображения с анимацией мерцания точек.
Например: Например:
![Пример](img/lp.svg)
![Пример](img/img_2j.svg)
## Что делает ## Что делает
@@ -48,4 +47,4 @@ poetry run python main.py img/img_2.jpg
или наоборот ""распустить") или наоборот ""распустить")
- `fill:#80000090` -- цвет точек (с прозрачностью) - `fill:#80000090` -- цвет точек (с прозрачностью)
![Еще пример](img/lp_8j.svg) ![Еще пример](img/cc.svg)
+1
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

-1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 80 KiB

+1
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 50 KiB

-1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 81 KiB

+41 -12
View File
@@ -8,7 +8,7 @@ def encode_to_base36(num):
if num < 0: if num < 0:
raise ValueError("Number must be non-negative") raise ValueError("Number must be non-negative")
charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
if num < len(charset): if num < len(charset):
return charset[num] return charset[num]
@@ -19,8 +19,9 @@ def encode_to_base36(num):
return ''.join(reversed(result)) return ''.join(reversed(result))
def generate_halftone_svg(image_path, output_path, cols=80, max_radius=6, animation_variants=12): def generate_halftone_svg(image_path, output_path, cols=80, max_radius=8, animation_variants=12):
import random import random
from collections import defaultdict
img = Image.open(image_path).convert('L') img = Image.open(image_path).convert('L')
aspect_ratio = img.height / img.width aspect_ratio = img.height / img.width
@@ -31,14 +32,21 @@ def generate_halftone_svg(image_path, output_path, cols=80, max_radius=6, animat
width = cols * step width = cols * step
height = rows * step height = rows * step
svg_elements = []
# Генерируем CSS-классы для разных задержек # Генерируем CSS-классы для разных задержек
animation_classes = [] animation_classes = []
for i in range(animation_variants): for i in range(animation_variants):
delay = (i / animation_variants) * 3 delay = (i / animation_variants) * 3
encoded_i = encode_to_base36(i) encoded_i = encode_to_base36(i)
animation_classes.append(f".a{encoded_i}{{--d:{delay:.2f}s}}") # Убрать .0 для целых чисел: 1.0s → 1s
if delay == int(delay):
delay_str = f"{int(delay)}"
else:
delay_str = f"{delay:.1f}".lstrip('0')
animation_classes.append(f".{encoded_i}{{--d:{delay_str}s}}")
# Собираем элементы группируя по классам: {class: [(radius, cx, cy), ...]}
elements_by_class = defaultdict(list)
unique_radii = set()
for y in range(rows): for y in range(rows):
for x in range(cols): for x in range(cols):
@@ -48,20 +56,41 @@ def generate_halftone_svg(image_path, output_path, cols=80, max_radius=6, animat
if factor < 0.15: if factor < 0.15:
continue continue
radius = factor * max_radius radius = int(factor * max_radius)
if radius == 0:
continue
cx = x * step + step // 2 cx = x * step + step // 2
cy = y * step + step // 2 cy = y * step + step // 2
# Каждой точке свой класс с уникальной задержкой
anim_class = random.randint(0, animation_variants - 1) anim_class = random.randint(0, animation_variants - 1)
encoded_class = encode_to_base36(anim_class) encoded_class = encode_to_base36(anim_class)
svg_elements.append(
f'<circle cx="{cx}" cy="{cy}" r="{int(radius)}" class="a{encoded_class}"/>' elements_by_class[encoded_class].append((radius, cx, cy))
) unique_radii.add(radius)
# Создаем <defs> с определениями кружков для каждого радиуса
defs = '<defs>'
for radius in sorted(unique_radii):
encoded_radius = encode_to_base36(radius)
defs += f'<circle id="{encoded_radius}" r="{radius}"/>'
defs += '</defs>'
# Создаем группы по классам
groups = []
for anim_class in sorted(elements_by_class.keys()):
group_elements = elements_by_class[anim_class]
uses = ''.join(
f'<use href="#{encode_to_base36(radius)}" x="{cx}" y="{cy}"/>'
for radius, cx, cy in group_elements
)
if uses:
groups.append(f'<g class="{anim_class}">{uses}</g>')
animation_css = "".join(animation_classes) animation_css = "".join(animation_classes)
groups_html = "".join(groups)
svg_template = f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" width="100%" height="100%"><style>svg{{background:transparent}}circle{{fill:#80000090;transform-origin:center;animation:noise 1s ease-in-out infinite alternate;animation-delay:var(--d);transition:all 0.5s ease-out}} @keyframes noise{{0%{{opacity:0.7;transform:scale(1)}}50%{{opacity:1}}100%{{opacity:0.5;transform:scale(0.99)}}}} .frozen circle{{animation:none!important;opacity:1!important;transform:none!important}} {animation_css}</style><g id="dots">{"".join(svg_elements)}</g></svg>""" svg_template = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" width="100%" height="100%"><style>svg{{background:transparent}}circle{{fill:#80000090;transform-origin:center;animation:noise 1s ease-in-out infinite alternate;animation-delay:var(--d);transition:all .5s ease-out}} @keyframes noise{{0%{{opacity:.7;transform:scale(1)}}50%{{opacity:1}}100%{{opacity:.5;transform:scale(.995)}}}}.frozen circle{{animation:none!important;opacity:1!important;transform:none!important}}{animation_css}</style>{defs}<g id="dots">{groups_html}</g></svg>'''
with open(output_path, 'w') as f: with open(output_path, 'w') as f:
f.write(svg_template) f.write(svg_template)
@@ -69,4 +98,4 @@ def generate_halftone_svg(image_path, output_path, cols=80, max_radius=6, animat
if __name__ == '__main__': if __name__ == '__main__':
file_name_in = sys.argv[-1] file_name_in = sys.argv[-1]
generate_halftone_svg(file_name_in, f'{file_name_in.split(".")[0]}j.svg', cols=90, max_radius=5) generate_halftone_svg(file_name_in, f'{file_name_in.split(".")[0]}j.svg', cols=63, max_radius=7)