fix: ядро (15) правильные центры групп + оптимизация
This commit is contained in:
@@ -45,7 +45,7 @@ class HalftoneGenerateForm(forms.Form):
|
||||
required=False,
|
||||
)
|
||||
blink = forms.IntegerField(
|
||||
min_value=0,
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
initial=6,
|
||||
required=False,
|
||||
|
||||
@@ -269,13 +269,23 @@ def generate_halftone_svg(
|
||||
prime_base = PRIME_DELAYS[anim_index % len(PRIME_DELAYS)]
|
||||
# Масштабируем задержку относительно длительности анимации с округлением до десятых
|
||||
delay = round(prime_base * (duration / 2.0), 1)
|
||||
|
||||
# Вычисляем уникальный центр трансформации для каждой группы анимации.
|
||||
# Центры группируются вокруг центра холста (50% 50%) с органическим разбросом,
|
||||
# благодаря чему группы дышат и покачиваются вокруг слегка смещенных фокусов.
|
||||
angle_jitter = (anim_index * (2 * math.pi / max(1, animation_variants))) + (seed % 7)
|
||||
spread_factor = 0.02 + 0.13 * abs((scale - 1000) / 200.0) # от 2% до 15%
|
||||
orig_x = round(50.0 + math.cos(angle_jitter) * spread_factor * 100)
|
||||
orig_y = round(50.0 + math.sin(angle_jitter) * spread_factor * 100)
|
||||
origin_rule = f";transform-origin:{orig_x}% {orig_y}%"
|
||||
else:
|
||||
delay = 0.0
|
||||
origin_rule = ""
|
||||
|
||||
# Генерируем правило задержки в CSS
|
||||
# Генерируем правило задержки и центра в CSS
|
||||
if is_animated:
|
||||
delay_str = f"{int(delay)}" if delay == int(delay) else f"{delay:.1f}".rstrip("0").rstrip(".")
|
||||
animation_classes.append(f".a{encoded_class}{{--d:{delay_str}s}}")
|
||||
animation_classes.append(f".a{encoded_class}{{--d:{delay_str}s{origin_rule}}}")
|
||||
|
||||
uses = "".join(
|
||||
f'<use href="#{shape_id}" x="{cx}" y="{cy}"/>'
|
||||
@@ -290,15 +300,29 @@ def generate_halftone_svg(
|
||||
# 6. Стилизация и Keyframes
|
||||
scale_val = max(0.5, min(1.5, scale / 1000.0))
|
||||
scale_str = f"{scale_val:.3f}".rstrip("0").rstrip(".")
|
||||
rot_str = f"{rotation}deg"
|
||||
has_scale = scale_val != 1.0
|
||||
has_rotation = rotation != 0
|
||||
|
||||
# Корректный цвет с прозрачностью
|
||||
transform_0 = []
|
||||
transform_100 = []
|
||||
if has_scale:
|
||||
transform_0.append("scale(1)")
|
||||
transform_100.append(f"scale({scale_str})")
|
||||
if has_rotation:
|
||||
transform_0.append("rotate(0deg)")
|
||||
transform_100.append(f"rotate({rotation}deg)")
|
||||
|
||||
tf_0_rule = f";transform:{' '.join(transform_0)}" if transform_0 else ""
|
||||
tf_100_rule = f";transform:{' '.join(transform_100)}" if transform_100 else ""
|
||||
|
||||
# Корректный цвет и прозрачность
|
||||
clean_color = color.strip() if color else "#a855ff"
|
||||
if not clean_color.startswith("#"):
|
||||
clean_color = f"#{clean_color}"
|
||||
|
||||
# Добавляем альфа-канал в hex при необходимости или используем CSS opacity
|
||||
fill_style = f"fill:{clean_color};stroke:{clean_color};opacity:{opacity:.1f};"
|
||||
has_custom_opacity = round(opacity, 2) < 1.0
|
||||
opacity_rule = f"opacity:{opacity:.1f};" if has_custom_opacity else ""
|
||||
fill_style = f"fill:{clean_color};stroke:{clean_color};{opacity_rule}"
|
||||
|
||||
if is_animated:
|
||||
# Управление паузой/запуском через CSS Custom Property --hypn0-play.
|
||||
@@ -309,23 +333,29 @@ def generate_halftone_svg(
|
||||
f"animation-delay:var(--d,0s);"
|
||||
f"animation-play-state:var(--hypn0-play,running);"
|
||||
)
|
||||
op_0 = f"{max(0.2, opacity * 0.7):.1f}".rstrip("0").rstrip(".")
|
||||
op_50 = f"{opacity:.1f}".rstrip("0").rstrip(".")
|
||||
op_100 = f"{max(0.1, opacity * 0.5):.1f}".rstrip("0").rstrip(".")
|
||||
|
||||
keyframes_rule = (
|
||||
f"@keyframes noise{{"
|
||||
f"0%{{opacity:{max(0.2, opacity * 0.7):.1f};transform:scale(1) rotate(0deg)}}"
|
||||
f"50%{{opacity:{opacity:.1f}}}"
|
||||
f"100%{{opacity:{max(0.1, opacity * 0.5):.1f};transform:scale({scale_str}) rotate({rot_str})}}"
|
||||
f"0%{{opacity:{op_0}{tf_0_rule}}}"
|
||||
f"50%{{opacity:{op_50}}}"
|
||||
f"100%{{opacity:{op_100}{tf_100_rule}}}"
|
||||
f"}}"
|
||||
)
|
||||
else:
|
||||
anim_rule = "animation:none;"
|
||||
keyframes_rule = ""
|
||||
|
||||
frozen_opacity = f";opacity:{opacity:.1f}!important" if has_custom_opacity else ""
|
||||
svg_css = (
|
||||
f"<style>"
|
||||
f"svg{{background:transparent}}"
|
||||
f"circle,rect,polygon,path,text{{{fill_style}transform-origin:center;{anim_rule}transition:all .5s ease-out}}"
|
||||
f"circle,rect,polygon,path,text{{{fill_style}}}"
|
||||
f"g[class^=\"a\"]{{{anim_rule}transition:all .5s ease-out}}"
|
||||
f"{keyframes_rule}"
|
||||
f".frozen circle,.frozen rect,.frozen polygon,.frozen path,.frozen text{{animation:none!important;opacity:{opacity:.1f}!important;transform:none!important}}"
|
||||
f".frozen g[class^=\"a\"],.frozen circle,.frozen rect,.frozen polygon,.frozen path,.frozen text{{animation:none!important{frozen_opacity};transform:none!important}}"
|
||||
f"{animation_css}"
|
||||
f"</style>"
|
||||
)
|
||||
@@ -364,7 +394,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,text{animation-play-state:var(--hypn0-play,paused)!important}"
|
||||
"g[class^=\"a\"],circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}"
|
||||
)
|
||||
|
||||
if "</style>" in svg_content:
|
||||
@@ -386,12 +416,14 @@ def prepare_active_svg(svg_content: str) -> str:
|
||||
|
||||
# Удаляем внедренные правила паузы через CSS-переменные и старые форматы
|
||||
gallery_css_variants = [
|
||||
"svg{--hypn0-play:paused}:host(:hover) svg,svg:hover{--hypn0-play:running!important}g[class^=\"a\"],circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}",
|
||||
"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}",
|
||||
"svg{--hypn0-play:paused}svg:hover{--hypn0-play:running}g[class^=\"a\"],circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}",
|
||||
]
|
||||
cleaned = svg_content
|
||||
for gcss in gallery_css_variants:
|
||||
@@ -400,7 +432,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(,text)?\s*\{animation-play-state:\s*var\(--hypn0-play,\s*paused\)!important\}",
|
||||
r"(g\[class\^=\"a\"\],)?(\.shape,)?circle,rect,polygon,path(,text)?\s*\{animation-play-state:\s*var\(--hypn0-play,\s*paused\)!important\}",
|
||||
"",
|
||||
cleaned,
|
||||
)
|
||||
|
||||
@@ -126,7 +126,34 @@ class HalftoneServiceTests(BaseMediaTestCase):
|
||||
self.assertIn("rotate(12deg)", svg)
|
||||
self.assertIn("scale(0.92)", svg)
|
||||
self.assertIn("#ff5500", svg)
|
||||
self.assertIn(".aH{--d:0s}", svg)
|
||||
self.assertIn(".aH{--d:0s", svg)
|
||||
self.assertIn("transform-origin:", svg)
|
||||
|
||||
def test_generate_optimized_styles_and_transforms(self):
|
||||
# 1. При дефолтных параметрах (rotation=0, scale=1000) transform в @keyframes noise отсутствует
|
||||
svg_default = generate_halftone_svg(self.img, cols=20, rotation=0, scale=1000)
|
||||
self.assertNotIn("rotate(", svg_default)
|
||||
self.assertNotIn("scale(", svg_default)
|
||||
self.assertIn("@keyframes noise{0%{opacity:0.6}50%{opacity:0.9}100%{opacity:0.5}}", svg_default)
|
||||
|
||||
# 2. Только rotation задан -> scale отсутствует
|
||||
svg_rot = generate_halftone_svg(self.img, cols=20, rotation=15, scale=1000)
|
||||
self.assertIn("rotate(15deg)", svg_rot)
|
||||
self.assertNotIn("scale(", svg_rot)
|
||||
|
||||
# 3. Только scale задан -> rotate отсутствует
|
||||
svg_scale = generate_halftone_svg(self.img, cols=20, rotation=0, scale=850)
|
||||
self.assertIn("scale(0.85)", svg_scale)
|
||||
self.assertNotIn("rotate(", svg_scale)
|
||||
|
||||
# 4. Прозрачность 1.0 -> opacity отсутствует в fill_style
|
||||
svg_opaque = generate_halftone_svg(self.img, cols=20, opacity=1.0)
|
||||
self.assertIn("fill:#a855ff;stroke:#a855ff;}", svg_opaque)
|
||||
self.assertNotIn("opacity:1.0;", svg_opaque)
|
||||
|
||||
# 5. Прозрачность 0.8 -> opacity присутствует
|
||||
svg_trans = generate_halftone_svg(self.img, cols=20, opacity=0.8)
|
||||
self.assertIn("opacity:0.8;", svg_trans)
|
||||
|
||||
def test_generate_from_bytes(self):
|
||||
buf = io.BytesIO()
|
||||
|
||||
Reference in New Issue
Block a user