mod: оптимизация загрузки (svg-картинки подгружаются динамически при прокрутке, с джиттером и т.п.)
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 33s
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 33s
This commit is contained in:
@@ -76,6 +76,7 @@ if settings.DEBUG:
|
|||||||
urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns
|
urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns
|
||||||
urlpatterns = [*PUBLIC_ROOT_URLPATTERNS, *urlpatterns]
|
urlpatterns = [*PUBLIC_ROOT_URLPATTERNS, *urlpatterns]
|
||||||
urlpatterns += staticfiles_urlpatterns()
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# РАЗДАЧА МЕДИА-ФАЙЛОВ (/media/...)
|
# РАЗДАЧА МЕДИА-ФАЙЛОВ (/media/...)
|
||||||
|
|||||||
@@ -878,9 +878,9 @@ class CardBgStyleTests(BaseMediaTestCase):
|
|||||||
self.assertIn('id="test-svg"', item.card_svg)
|
self.assertIn('id="test-svg"', item.card_svg)
|
||||||
|
|
||||||
response = self.client.get(reverse("hypn0_site:index"))
|
response = self.client.get(reverse("hypn0_site:index"))
|
||||||
self.assertContains(response, 'template shadowrootmode="open"')
|
self.assertContains(response, "hypn0LazySvg")
|
||||||
self.assertContains(response, "hypn0-card-svg")
|
self.assertContains(response, "hypn0-card-svg")
|
||||||
self.assertContains(response, "--hypn0-play: running !important")
|
self.assertContains(response, item.file_svg.url)
|
||||||
|
|
||||||
|
|
||||||
class GalleryArchiveTests(BaseMediaTestCase):
|
class GalleryArchiveTests(BaseMediaTestCase):
|
||||||
@@ -1458,7 +1458,7 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
|||||||
("502", 502, "Контейнер потерял сознание"),
|
("502", 502, "Контейнер потерял сознание"),
|
||||||
("503", 503, "Плановый сеанс гипнотерапии"),
|
("503", 503, "Плановый сеанс гипнотерапии"),
|
||||||
("504", 504, "Разрыв астральной связи"),
|
("504", 504, "Разрыв астральной связи"),
|
||||||
("under_reconstruction", 200, "Сектор на реконструкции"),
|
("under_reconstruction", 200, "Отдел мозга на реконструкции"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for code, expected_status, text_fragment in expected_checks:
|
for code, expected_status, text_fragment in expected_checks:
|
||||||
|
|||||||
@@ -111,36 +111,19 @@ val=>{if(val)document.documentElement.classList.add('dark');else document.docume
|
|||||||
{% endblock EXTRA_LD_JSON %}
|
{% endblock EXTRA_LD_JSON %}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
<script src="{% static 'js/hypn0.js' %}" defer></script>
|
||||||
<script src="{% static 'js/alpine.min.js' %}" defer></script>
|
<script src="{% static 'js/alpine.min.js' %}" defer></script>
|
||||||
<script src="{% static 'js/htmx.min.js' %}" defer></script>
|
<script src="{% static 'js/htmx.min.js' %}" defer></script>
|
||||||
<script>
|
|
||||||
document.addEventListener('htmx:configRequest', function(evt) {
|
|
||||||
var token = '{{ csrf_token }}';
|
|
||||||
if (!token) {
|
|
||||||
var match = document.cookie.match(/csrftoken=([^;]+)/);
|
|
||||||
if (match) token = match[1];
|
|
||||||
}
|
|
||||||
if (token) {
|
|
||||||
evt.detail.headers['X-CSRFToken'] = token;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function attachShadowRoots(root) {
|
|
||||||
(root || document).querySelectorAll('template[shadowrootmode]').forEach(function(tmpl) {
|
|
||||||
if (!tmpl.parentElement.shadowRoot) {
|
|
||||||
var mode = tmpl.getAttribute('shadowrootmode') || 'open';
|
|
||||||
var shadow = tmpl.parentElement.attachShadow({ mode: mode });
|
|
||||||
shadow.appendChild(tmpl.content.cloneNode(true));
|
|
||||||
tmpl.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
document.addEventListener('DOMContentLoaded', function() { attachShadowRoots(document); });
|
|
||||||
document.addEventListener('htmx:afterSwap', function(evt) { attachShadowRoots(evt.detail.target); });
|
|
||||||
</script>
|
|
||||||
{% block EXTRA_HEAD %}{% endblock EXTRA_HEAD %}
|
{% block EXTRA_HEAD %}{% endblock EXTRA_HEAD %}
|
||||||
</head>
|
</head>
|
||||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' class="bg-stone-50 text-emerald-700 dark:bg-zinc-950 dark:text-emerald-100 font-mono">{% block ADD_CSS1 %}{% endblock %}{% block ADD_CSS2 %}{% endblock %}{% block ADD_CSS3 %}{% endblock %}{% block BODY %}
|
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' class="bg-stone-50 text-emerald-700 dark:bg-zinc-950 dark:text-emerald-100 font-mono">
|
||||||
|
<noscript>
|
||||||
|
<div class="sticky top-0 z-50 bg-amber-500 text-stone-950 px-4 py-2.5 text-center text-xs sm:text-sm font-bold shadow-lg border-b border-amber-600 flex items-center justify-center gap-2">
|
||||||
|
<span>⚠️</span>
|
||||||
|
<span>Для подчинения разума и генерации гипнотических SVG-матриц требуется включить <strong>JavaScript</strong> в вашем браузере!</span>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
{% block ADD_CSS1 %}{% endblock %}{% block ADD_CSS2 %}{% endblock %}{% block ADD_CSS3 %}{% endblock %}{% block BODY %}
|
||||||
{% include "block/header.html" %}
|
{% include "block/header.html" %}
|
||||||
<main id="main-content" class="mx-auto min-h-screen max-w-7xl px-4 py-8">
|
<main id="main-content" class="mx-auto min-h-screen max-w-7xl px-4 py-8">
|
||||||
{% block CONTENT %}{% endblock CONTENT %}
|
{% block CONTENT %}{% endblock CONTENT %}
|
||||||
|
|||||||
@@ -8,32 +8,14 @@
|
|||||||
class="absolute inset-0 z-10 flex items-center justify-center p-3 sm:p-4 border-0 border-none no-underline hover:border-0 hover:border-none focus:outline-none"
|
class="absolute inset-0 z-10 flex items-center justify-center p-3 sm:p-4 border-0 border-none no-underline hover:border-0 hover:border-none focus:outline-none"
|
||||||
title="«{{ item.s_title }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG"
|
title="«{{ item.s_title }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG"
|
||||||
aria-label="«{{ item.s_title }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG">
|
aria-label="«{{ item.s_title }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG">
|
||||||
<div class="hypn0-card-svg w-full h-full flex items-center justify-center drop-shadow-sm transition-transform duration-300 group-hover:scale-105 border-0">
|
<div x-data="hypn0LazySvg('{% if item.file_svg %}{{ item.file_svg.url }}{% endif %}')"
|
||||||
<template shadowrootmode="open">
|
class="hypn0-card-svg w-full h-full flex items-center justify-center drop-shadow-sm transition-transform duration-300 group-hover:scale-105 border-0">
|
||||||
{# Declarative Shadow DOM: изолирует ID (<defs id="s1"..>) и стили каждой картины в галерее. #}
|
<noscript>
|
||||||
{# В отличие от <img>, события мыши не блокируются, а в отличие от обычного inline SVG нет конфликтов идентификаторов. #}
|
<div class="flex flex-col items-center justify-center p-3 text-center gap-1">
|
||||||
<style>
|
<span class="text-[11px] font-bold text-amber-600 dark:text-amber-400">Требуется JS для анимации</span>
|
||||||
:host {
|
<span class="text-[10px] text-stone-500 dark:text-stone-400 underline">«{{ item.s_title }}»</span>
|
||||||
display: flex;
|
</div>
|
||||||
width: 100%;
|
</noscript>
|
||||||
height: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
{# В покое внутри SVG действует --hypn0-play: paused (0% нагрузки на CPU). #}
|
|
||||||
{# При наведении мыши на хост-карточку переопределяем переменную на running, и анимация оживает. #}
|
|
||||||
:host(:hover) svg, svg:hover {
|
|
||||||
--hypn0-play: running !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{ item.card_svg|safe }}
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* hypn0.js — Основной клиентский скрипт для hypn0.xyz
|
||||||
|
* Обеспечивает интеграцию HTMX, работу с Declarative Shadow DOM
|
||||||
|
* и ленивую асинхронную подгрузку SVG-халфтонов через Alpine.js.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Автоматическая передача CSRF-токена в AJAX-запросах HTMX
|
||||||
|
document.addEventListener('htmx:configRequest', function(evt) {
|
||||||
|
var csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
|
||||||
|
var token = csrfInput ? csrfInput.value : null;
|
||||||
|
if (!token) {
|
||||||
|
var match = document.cookie.match(/csrftoken=([^;]+)/);
|
||||||
|
if (match) token = match[1];
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
evt.detail.headers['X-CSRFToken'] = token;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Полифилл / обработка Declarative Shadow DOM для старых браузеров и динамических вставок
|
||||||
|
function attachShadowRoots(root) {
|
||||||
|
(root || document).querySelectorAll('template[shadowrootmode]').forEach(function(tmpl) {
|
||||||
|
if (!tmpl.parentElement.shadowRoot) {
|
||||||
|
var mode = tmpl.getAttribute('shadowrootmode') || 'open';
|
||||||
|
var shadow = tmpl.parentElement.attachShadow({ mode: mode });
|
||||||
|
shadow.appendChild(tmpl.content.cloneNode(true));
|
||||||
|
tmpl.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
attachShadowRoots(document);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка HTMX swap: инициализация теневого корня и Alpine.js компонентов в новом DOM-фрагменте
|
||||||
|
document.addEventListener('htmx:afterSwap', function(evt) {
|
||||||
|
attachShadowRoots(evt.detail.target);
|
||||||
|
if (window.Alpine && evt.detail && evt.detail.target) {
|
||||||
|
window.Alpine.initTree(evt.detail.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hypn0LazySvg — Alpine-компонент ленивой асинхронной загрузки SVG в Shadow DOM.
|
||||||
|
* Защищает от лагов быстрого скролла (AbortController + debounce),
|
||||||
|
* размазывает пиковые сетевые всплески (джиттер) и изолирует стили/ID.
|
||||||
|
*/
|
||||||
|
function hypn0LazySvg(svgUrl) {
|
||||||
|
return {
|
||||||
|
svgUrl: svgUrl || '',
|
||||||
|
loaded: false,
|
||||||
|
error: false,
|
||||||
|
_observer: null,
|
||||||
|
_controller: null,
|
||||||
|
_timer: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (!this.svgUrl) return;
|
||||||
|
var el = this.$el;
|
||||||
|
|
||||||
|
if (!('IntersectionObserver' in window)) {
|
||||||
|
this.fetchSvg(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this._observer = new IntersectionObserver(function(entries) {
|
||||||
|
entries.forEach(function(entry) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
var jitter = Math.floor(Math.random() * 60);
|
||||||
|
self._timer = setTimeout(function() {
|
||||||
|
self.fetchSvg(el);
|
||||||
|
}, 120 + jitter);
|
||||||
|
} else {
|
||||||
|
if (self._timer) {
|
||||||
|
clearTimeout(self._timer);
|
||||||
|
self._timer = null;
|
||||||
|
}
|
||||||
|
if (self._controller) {
|
||||||
|
self._controller.abort();
|
||||||
|
self._controller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
rootMargin: '120px 0px 120px 0px',
|
||||||
|
threshold: 0.01
|
||||||
|
});
|
||||||
|
|
||||||
|
this._observer.observe(el);
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchSvg(el) {
|
||||||
|
if (this.loaded) return;
|
||||||
|
if (this._controller) this._controller.abort();
|
||||||
|
this._controller = new AbortController();
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
fetch(this.svgUrl, { signal: this._controller.signal })
|
||||||
|
.then(function(resp) {
|
||||||
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||||
|
return resp.text();
|
||||||
|
})
|
||||||
|
.then(function(svgText) {
|
||||||
|
if (!svgText) return;
|
||||||
|
|
||||||
|
var shadow = el.shadowRoot;
|
||||||
|
if (!shadow) {
|
||||||
|
shadow = el.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.textContent = [
|
||||||
|
':host {',
|
||||||
|
' display: flex;',
|
||||||
|
' width: 100%;',
|
||||||
|
' height: 100%;',
|
||||||
|
' align-items: center;',
|
||||||
|
' justify-content: center;',
|
||||||
|
' pointer-events: auto;',
|
||||||
|
' opacity: 0;',
|
||||||
|
' transition: opacity 0.35s ease-out;',
|
||||||
|
'}',
|
||||||
|
':host(.is-loaded), :host([data-loaded]) {',
|
||||||
|
' opacity: 1;',
|
||||||
|
'}',
|
||||||
|
'svg {',
|
||||||
|
' width: 100%;',
|
||||||
|
' height: 100%;',
|
||||||
|
' object-fit: contain;',
|
||||||
|
'}',
|
||||||
|
':host(:hover) svg, svg:hover {',
|
||||||
|
' --hypn0-play: running !important;',
|
||||||
|
'}'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
var parser = new DOMParser();
|
||||||
|
var doc = parser.parseFromString(svgText, 'image/svg+xml');
|
||||||
|
var svgEl = doc.querySelector('svg');
|
||||||
|
|
||||||
|
shadow.innerHTML = '';
|
||||||
|
shadow.appendChild(style);
|
||||||
|
if (svgEl) {
|
||||||
|
shadow.appendChild(svgEl);
|
||||||
|
} else {
|
||||||
|
var container = document.createElement('div');
|
||||||
|
container.innerHTML = svgText;
|
||||||
|
var innerSvg = container.querySelector('svg');
|
||||||
|
if (innerSvg) shadow.appendChild(innerSvg);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loaded = true;
|
||||||
|
self.error = false;
|
||||||
|
el.setAttribute('data-loaded', 'true');
|
||||||
|
el.classList.add('is-loaded');
|
||||||
|
|
||||||
|
if (self._observer) {
|
||||||
|
self._observer.disconnect();
|
||||||
|
self._observer = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
if (err.name === 'AbortError') return;
|
||||||
|
self.error = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this._timer) clearTimeout(this._timer);
|
||||||
|
if (this._controller) this._controller.abort();
|
||||||
|
if (this._observer) this._observer.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user