fix: ядро (15) правильные центры групп + оптимизация
This commit is contained in:
@@ -45,7 +45,7 @@ class HalftoneGenerateForm(forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
blink = forms.IntegerField(
|
blink = forms.IntegerField(
|
||||||
min_value=0,
|
min_value=1,
|
||||||
max_value=10,
|
max_value=10,
|
||||||
initial=6,
|
initial=6,
|
||||||
required=False,
|
required=False,
|
||||||
|
|||||||
@@ -269,13 +269,23 @@ def generate_halftone_svg(
|
|||||||
prime_base = PRIME_DELAYS[anim_index % len(PRIME_DELAYS)]
|
prime_base = PRIME_DELAYS[anim_index % len(PRIME_DELAYS)]
|
||||||
# Масштабируем задержку относительно длительности анимации с округлением до десятых
|
# Масштабируем задержку относительно длительности анимации с округлением до десятых
|
||||||
delay = round(prime_base * (duration / 2.0), 1)
|
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:
|
else:
|
||||||
delay = 0.0
|
delay = 0.0
|
||||||
|
origin_rule = ""
|
||||||
|
|
||||||
# Генерируем правило задержки в CSS
|
# Генерируем правило задержки и центра в CSS
|
||||||
if is_animated:
|
if is_animated:
|
||||||
delay_str = f"{int(delay)}" if delay == int(delay) else f"{delay:.1f}".rstrip("0").rstrip(".")
|
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(
|
uses = "".join(
|
||||||
f'<use href="#{shape_id}" x="{cx}" y="{cy}"/>'
|
f'<use href="#{shape_id}" x="{cx}" y="{cy}"/>'
|
||||||
@@ -290,15 +300,29 @@ def generate_halftone_svg(
|
|||||||
# 6. Стилизация и Keyframes
|
# 6. Стилизация и Keyframes
|
||||||
scale_val = max(0.5, min(1.5, scale / 1000.0))
|
scale_val = max(0.5, min(1.5, scale / 1000.0))
|
||||||
scale_str = f"{scale_val:.3f}".rstrip("0").rstrip(".")
|
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"
|
clean_color = color.strip() if color else "#a855ff"
|
||||||
if not clean_color.startswith("#"):
|
if not clean_color.startswith("#"):
|
||||||
clean_color = f"#{clean_color}"
|
clean_color = f"#{clean_color}"
|
||||||
|
|
||||||
# Добавляем альфа-канал в hex при необходимости или используем CSS opacity
|
has_custom_opacity = round(opacity, 2) < 1.0
|
||||||
fill_style = f"fill:{clean_color};stroke:{clean_color};opacity:{opacity:.1f};"
|
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:
|
if is_animated:
|
||||||
# Управление паузой/запуском через CSS Custom Property --hypn0-play.
|
# Управление паузой/запуском через CSS Custom Property --hypn0-play.
|
||||||
@@ -309,23 +333,29 @@ def generate_halftone_svg(
|
|||||||
f"animation-delay:var(--d,0s);"
|
f"animation-delay:var(--d,0s);"
|
||||||
f"animation-play-state:var(--hypn0-play,running);"
|
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 = (
|
keyframes_rule = (
|
||||||
f"@keyframes noise{{"
|
f"@keyframes noise{{"
|
||||||
f"0%{{opacity:{max(0.2, opacity * 0.7):.1f};transform:scale(1) rotate(0deg)}}"
|
f"0%{{opacity:{op_0}{tf_0_rule}}}"
|
||||||
f"50%{{opacity:{opacity:.1f}}}"
|
f"50%{{opacity:{op_50}}}"
|
||||||
f"100%{{opacity:{max(0.1, opacity * 0.5):.1f};transform:scale({scale_str}) rotate({rot_str})}}"
|
f"100%{{opacity:{op_100}{tf_100_rule}}}"
|
||||||
f"}}"
|
f"}}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
anim_rule = "animation:none;"
|
anim_rule = "animation:none;"
|
||||||
keyframes_rule = ""
|
keyframes_rule = ""
|
||||||
|
|
||||||
|
frozen_opacity = f";opacity:{opacity:.1f}!important" if has_custom_opacity else ""
|
||||||
svg_css = (
|
svg_css = (
|
||||||
f"<style>"
|
f"<style>"
|
||||||
f"svg{{background:transparent}}"
|
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"{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"{animation_css}"
|
||||||
f"</style>"
|
f"</style>"
|
||||||
)
|
)
|
||||||
@@ -364,7 +394,7 @@ def prepare_gallery_svg(svg_content: str) -> str:
|
|||||||
hover_css = (
|
hover_css = (
|
||||||
"svg{--hypn0-play:paused}"
|
"svg{--hypn0-play:paused}"
|
||||||
":host(:hover) svg,svg:hover{--hypn0-play:running!important}"
|
":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:
|
if "</style>" in svg_content:
|
||||||
@@ -386,12 +416,14 @@ def prepare_active_svg(svg_content: str) -> str:
|
|||||||
|
|
||||||
# Удаляем внедренные правила паузы через CSS-переменные и старые форматы
|
# Удаляем внедренные правила паузы через CSS-переменные и старые форматы
|
||||||
gallery_css_variants = [
|
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}.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{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}: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}.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{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}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
|
cleaned = svg_content
|
||||||
for gcss in gallery_css_variants:
|
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"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"(:host\(:hover\)\s*svg\s*,\s*)?svg:hover\s*\{[^}]*--hypn0-play:[^}]*\}", "", cleaned)
|
||||||
cleaned = re.sub(
|
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,
|
cleaned,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -126,7 +126,34 @@ class HalftoneServiceTests(BaseMediaTestCase):
|
|||||||
self.assertIn("rotate(12deg)", svg)
|
self.assertIn("rotate(12deg)", svg)
|
||||||
self.assertIn("scale(0.92)", svg)
|
self.assertIn("scale(0.92)", svg)
|
||||||
self.assertIn("#ff5500", 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):
|
def test_generate_from_bytes(self):
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
|
|||||||
@@ -351,12 +351,12 @@
|
|||||||
<div class="flex justify-between mb-1">
|
<div class="flex justify-between mb-1">
|
||||||
<label class="text-xs font-bold text-stone-400 uppercase">Мерцание</label>
|
<label class="text-xs font-bold text-stone-400 uppercase">Мерцание</label>
|
||||||
<span class="text-xs transition-colors duration-300"
|
<span class="text-xs transition-colors duration-300"
|
||||||
:class="blink < 1 ? 'text-emerald-600' : (blink < 2 ? 'text-red-600' : (blink < 4 ? 'text-yellow-600' : 'text-emerald-600'))"
|
:class="blink <= 7 ? 'accent-emerald-600' : (blink > 9 ? 'accent-red-600' : (blink > 7 ? 'accent-yellow-500' : 'accent-emerald-500'))"
|
||||||
x-text="blink"></span>
|
x-text="blink"></span>
|
||||||
</div>
|
</div>
|
||||||
<input type="range" name="blink" min="0" max="10" x-model="blink"
|
<input type="range" name="blink" min="1" max="10" x-model="blink"
|
||||||
@input="$el.dispatchEvent(new CustomEvent('input-change'))"
|
@input="$el.dispatchEvent(new CustomEvent('input-change'))"
|
||||||
:class="blink < 1 ? 'accent-emerald-600' : (blink < 2 ? 'accent-red-600' : (blink < 4 ? 'accent-yellow-500' : 'accent-emerald-500'))"
|
:class="blink <= 7 ? 'accent-emerald-600' : (blink > 9 ? 'accent-red-600' : (blink > 7 ? 'accent-yellow-500' : 'accent-emerald-500'))"
|
||||||
class="w-full accent-emerald-500">
|
class="w-full accent-emerald-500">
|
||||||
</div>
|
</div>
|
||||||
{# ПОВОРОТЫ #}<div>
|
{# ПОВОРОТЫ #}<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user