fix: единая загрузка примеров и стабильный первый кадр (api#301)
deploy-preland / deploy zoomstage.online (push) Successful in 33s

This commit is contained in:
Panfilov Konstantin committed 2026-10-20 19:52:30 +03:00
1 parent 75ccf14889
commit 0c9a406d68
10 files changed
+347 -257

No files matched your search

+31 -220
View File
@@ -1,101 +1,39 @@
<?php
$demoBasePath = rtrim($assetsUriPrefix, '/') . "/{$indpic}";
$demoLocalPath = __DIR__ . "/../{$indpic}";
$getAssetUrl = function (int $number, string $device, string $role) use ($demoBasePath, $demoLocalPath): string {
$relativePath = sprintf('%02d', $number) . "_{$device}_{$role}.avif";
$fullPath = $demoLocalPath . '/' . ltrim($relativePath, '/');
$relativePath = sprintf('%02d', $number) . "_{$device}_{$role}.avif";
$fullPath = $demoLocalPath . '/' . $relativePath;
$version = file_exists($fullPath) ? filemtime($fullPath) : '1';
return $demoBasePath . '/' . ltrim($relativePath, '/') . '?v=' . $version;
return $demoBasePath . '/' . $relativePath . '?v=' . $version;
};
$labelBefore = Yii::t('app', 'До');
$labelAfter = Yii::t('app', 'После');
$sourceMob = $getAssetUrl(1, 'mob', '1-source');
$sourceOrig = $getAssetUrl(1, 'desk', '1-source');
$resultMob = $getAssetUrl(1, 'mob', '2-result');
$resultOrig = $getAssetUrl(1, 'desk', '2-result');
$labelAfter = Yii::t('app', 'После');
?>
<div id="examples">
<div class="image-compare" id="image-compare">
<img src="<?= $sourceMob ?>"
srcset="<?= $sourceMob ?> 768w, <?= $sourceOrig ?> 1696w"
sizes="(max-width: 767.98px) 100vw, 728px"
id="image-compare-source"
fetchpriority="high"
loading="eager"
width="728"
height="395"
alt="<?= htmlspecialchars($labelBefore) ?>">
<img src="<?= $resultMob ?>"
srcset="<?= $resultMob ?> 768w, <?= $resultOrig ?> 1696w"
sizes="(max-width: 767.98px) 100vw, 728px"
id="image-compare-result"
fetchpriority="high"
loading="eager"
width="728"
height="395"
alt="<?= htmlspecialchars($labelAfter) ?>">
</div>
<div class="image-examples">
<div class="image-examples-label"><?= Yii::t('app', 'Примеры улучшения') ?></div>
<div class="image-examples-list">
<?php for ($i = 1; $i <= 5; $i++):
$thumbMob = $getAssetUrl($i, 'mob', '3-thumb');
$thumbOrig = $getAssetUrl($i, 'desk', '3-thumb');
?>
<div class="image-examples-item <?= $i === 1 ? 'active' : '' ?>"
data-index="<?= $i ?>"
data-source="<?= $getAssetUrl($i, 'desk', '1-source') ?>"
data-result="<?= $getAssetUrl($i, 'desk', '2-result') ?>"
data-source-mob="<?= $getAssetUrl($i, 'mob', '1-source') ?>"
data-result-mob="<?= $getAssetUrl($i, 'mob', '2-result') ?>">
<div class="image-examples-image">
<?php if ($i === 1): ?>
<picture>
<source media="(max-width: 767.98px)" srcset="<?= $thumbMob ?>">
<img src="<?= $thumbOrig ?>" width="53" height="53" alt="Example 1">
</picture>
<?php else: ?>
<picture class="deferred-picture">
<source media="(max-width: 767.98px)" data-srcset="<?= $thumbMob ?>">
<img data-src="<?= $thumbOrig ?>" width="53" height="53" alt="Example <?= $i ?>" class="deferred-thumb">
</picture>
<?php endif; ?>
</div>
<div id="examples" data-examples-state="loading" aria-busy="true">
<div data-examples-visual style="visibility:hidden" aria-hidden="true" inert>
<div class="image-compare" id="image-compare">
<img id="image-compare-source" width="728" height="395" alt="<?= htmlspecialchars($labelBefore) ?>">
<img id="image-compare-result" width="728" height="395" alt="<?= htmlspecialchars($labelAfter) ?>">
</div>
<div class="image-examples">
<div class="image-examples-label"><?= Yii::t('app', 'Примеры улучшения') ?></div>
<div class="image-examples-list">
<?php for ($i=1; $i<=5; $i++): ?>
<div class="image-examples-item <?= $i===1 ? 'active' : '' ?>" data-example="<?= $i ?>" data-index="<?= $i ?>"
data-source="<?= $getAssetUrl($i, 'desk', '1-source') ?>" data-result="<?= $getAssetUrl($i, 'desk', '2-result') ?>"
data-source-mob="<?= $getAssetUrl($i, 'mob', '1-source') ?>" data-result-mob="<?= $getAssetUrl($i, 'mob', '2-result') ?>"
data-thumb="<?= $getAssetUrl($i, 'desk', '3-thumb') ?>" data-thumb-mob="<?= $getAssetUrl($i, 'mob', '3-thumb') ?>" aria-disabled="true" tabindex="-1">
<div class="image-examples-image"><picture><img width="100" height="100" alt="Example <?= $i ?>"></picture></div>
</div>
<?php endfor; ?>
<?php endfor; ?>
</div>
</div>
</div>
<button type="button" class="btn button-outline examples-retry" data-examples-retry hidden><?= Yii::t('app', 'Повторить') ?></button>
</div>
<script>
const examples = {
el: document.getElementById('examples'),
titleEl: document.getElementById('form-title'),
hide() {
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: 'instant' });
if (this.el) this.el.style.display = 'none';
},
show() {
if (this.el) this.el.style.display = '';
},
setTitle(html) {
if (this.titleEl) this.titleEl.innerHTML = html;
}
};
<?= require __DIR__ . '/example-loader.php' ?>
function setupNativeCompare(element, labelBefore, labelAfter) {
if (!element || element.classList.contains('native-icv') || element.classList.contains('icv')) return null;
@@ -132,15 +70,18 @@ $resultOrig = $getAssetUrl(1, 'desk', '2-result');
element.appendChild(lblBefore);
element.appendChild(lblAfter);
let sliderPercent = 50;
element.addEventListener('examples:images', () => updateSlider(sliderPercent));
function updateSlider(percent) {
sliderPercent = percent;
percent = Math.max(0, Math.min(100, percent));
beforeWrap.style.width = percent + '%';
handle.style.left = percent + '%';
sourceImg.style.width = element.clientWidth + 'px';
element.querySelector('#image-compare-source').style.width = element.clientWidth + 'px';
}
window.addEventListener('resize', () => {
sourceImg.style.width = element.clientWidth + 'px';
element.querySelector('#image-compare-source').style.width = element.clientWidth + 'px';
});
let isDragging = false;
@@ -175,140 +116,10 @@ $resultOrig = $getAssetUrl(1, 'desk', '2-result');
updateSlider(50);
return {
setImages(sourceUrl, resultUrl) {
sourceImg.removeAttribute('srcset');
resultImg.removeAttribute('srcset');
sourceImg.src = sourceUrl;
resultImg.src = resultUrl;
}
};
}
function initCompareViewer() {
const element = document.getElementById('image-compare');
const beforeTxt = '<?= addslashes($labelBefore) ?>';
const afterTxt = '<?= addslashes($labelAfter) ?>';
if (element && typeof ImageCompare !== 'undefined') {
const options = {
controlColor: "#FFFFFF",
controlShadow: true,
addCircle: true,
addCircleBlur: true,
showLabels: true,
labelOptions: { before: beforeTxt, after: afterTxt, onHover: false },
smoothing: true,
smoothingAmount: 100,
hoverStart: false,
verticalMode: false,
startingPoint: 50,
fluidMode: false
};
window.icvInstance = new ImageCompare(element, options).mount();
} else if (element) {
window.nativeCompareInstance = setupNativeCompare(element, beforeTxt, afterTxt);
}
const exampleItems = document.querySelectorAll('.image-examples-item');
exampleItems.forEach(item => {
item.onclick = function() {
const isMobile = window.innerWidth < 768;
// На мобильном берем source_mob, на ПК — полный оригинал
const source = (isMobile && this.getAttribute('data-source-mob'))
? this.getAttribute('data-source-mob')
: this.getAttribute('data-source');
const result = (isMobile && this.getAttribute('data-result-mob'))
? this.getAttribute('data-result-mob')
: this.getAttribute('data-result');
if (window.icvInstance) {
window.icvInstance.images.start.element.removeAttribute('srcset');
window.icvInstance.images.end.element.removeAttribute('srcset');
window.icvInstance.images.start.element.src = source;
window.icvInstance.images.end.element.src = result;
} else if (window.nativeCompareInstance) {
window.nativeCompareInstance.setImages(source, result);
} else {
const sourceEl = document.getElementById('image-compare-source');
const resultEl = document.getElementById('image-compare-result');
if (sourceEl) {
sourceEl.removeAttribute('srcset');
sourceEl.src = source;
}
if (resultEl) {
resultEl.removeAttribute('srcset');
resultEl.src = result;
}
}
exampleItems.forEach(el => el.classList.remove('active'));
this.classList.add('active');
};
});
}
function initStrictImagePipeline() {
initCompareViewer();
const preloadUrl = (url) => {
return new Promise(resolve => {
const img = new Image();
img.onload = img.onerror = resolve;
img.src = url;
});
};
const deferredPics = document.querySelectorAll('.deferred-picture');
const thumbPromises = [];
deferredPics.forEach(pic => {
const source = pic.querySelector('source');
const img = pic.querySelector('img');
if (source && source.getAttribute('data-srcset')) {
source.srcset = source.getAttribute('data-srcset');
}
if (img && img.getAttribute('data-src')) {
const p = new Promise(resolve => {
img.onload = img.onerror = resolve;
img.src = img.getAttribute('data-src');
});
thumbPromises.push(p);
}
});
Promise.all(thumbPromises).then(() => {
const queue = [2, 3, 4, 5];
async function loadSequentially() {
const isMobile = window.innerWidth < 768;
for (const num of queue) {
const item = document.querySelector(`.image-examples-item[data-index="${num}"]`);
if (!item) continue;
const s = (isMobile && item.getAttribute('data-source-mob'))
? item.getAttribute('data-source-mob')
: item.getAttribute('data-source');
const r = (isMobile && item.getAttribute('data-result-mob'))
? item.getAttribute('data-result-mob')
: item.getAttribute('data-result');
await Promise.all([preloadUrl(s), preloadUrl(r)]);
}
}
if ('requestIdleCallback' in window) {
requestIdleCallback(() => loadSequentially());
} else {
setTimeout(loadSequentially, 150);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initStrictImagePipeline);
} else {
initStrictImagePipeline();
}
window.ZoomzillaExamples.attach(element => {
setupNativeCompare(element, <?= json_encode($labelBefore, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, <?= json_encode($labelAfter, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>);
});
</script>