[html]<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Книжные полки — загрузка и экспорт</title>
<style>
:root{
--bg-img: url('https://i.pinimg.com/736x/f5/18/52/f518527023840df8c66d9c66a6a0b95c.jpg');
--overlay: rgba(0,0,0,.35);
--shelf:#e6dfd4ea;
--card:#ffffff;
--ink:#2b2b2b;
--muted:#8a7f70;
--accent:#c49a6c;
--wrap-w: 700px; /* ключевая ширина макета (обычная) */
--gap:10px;
--cover-radius:10px;
}
/* фон */
body{
margin:0; color:var(--ink);
font:14px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
min-height:100dvh; background:#111; padding:16px;
/* центрирование страницы по горизонтали и вертикали */
display:grid;
place-content:center;
}
body::before{
content:""; position:fixed; inset:0;
background: var(--overlay), var(--bg-img) center/cover no-repeat fixed;
z-index:-1; filter:saturate(.9) contrast(.95);
}
.page{
width:100%; max-width: var(--wrap-w);
display:grid; gap:14px;
margin-inline:auto; /* перестраховка для центрирования по горизонтали */
}
/* панель добавления */
.uploader{
background: #ffffffc8;
backdrop-filter: blur(2px);
border:1px solid #00000010;
border-radius:14px; padding:12px;
box-shadow: 0 4px 16px rgba(0,0,0,.1);
}
.uploader h2{ margin:0 0 10px; font-size:15px; }
.u-grid{
display:grid; gap:10px;
grid-template-columns: repeat(5, 1fr);
}
.u-item{
display:flex; flex-direction:column; gap:6px; align-items:stretch;
}
.u-btn{
display:grid; place-items:center;
aspect-ratio: 2 / 3; border:2px dashed var(--muted);
border-radius: var(--cover-radius);
background: var(--card);
cursor:pointer; position:relative; overflow:hidden;
}
.u-btn span{
text-align:center; color:var(--muted); font-size:11px; line-height:1.1;
}
.u-btn input{ display:none; }
.u-preview{
position:absolute; inset:0; background-size:cover; background-position:center;
}
/* полки */
.shelves-wrap{
display:grid; gap:14px;
}
.shelf{
background:
linear-gradient(to bottom, var(--accent) 0 6px, transparent 6px),
var(--shelf);
padding: 18px 12px 12px;
border-radius: 14px;
box-shadow: 0 4px 16px rgba(0,0,0,.12);
}
.grid{
display:grid; grid-template-columns: repeat(5, 1fr); gap: var(--gap);
}
.book{ display:flex; flex-direction:column; align-items:center; gap:6px; text-align:center; }
.cover{
width:100%; aspect-ratio: 2 / 3; border:2px dashed var(--muted);
border-radius:var(--cover-radius); background:#fff;
background-size:cover; background-position:center; position:relative;
overflow:hidden;
}
.cover > span{
position:absolute; inset:0; display:grid; place-items:center;
color:var(--muted); font-size:11px; line-height:1.1; text-align:center;
white-space:pre-line;
}
.cover.filled{ border-style:solid; border-color:#c9c1b5; }
.label{
font-size:11.5px; line-height:1.25; color:#1f1b16; background:#fff8;
padding:5px 6px; border-radius:8px; border:1px solid #00000010; backdrop-filter: blur(2px);
}
.actions{
display:flex; gap:10px; justify-content:center; align-items:center;
}
.btn{
appearance:none; border:1px solid #00000020; background:#fff;
padding:10px 14px; border-radius:10px; cursor:pointer; font-weight:600;
box-shadow: 0 2px 8px rgba(0,0,0,.08);
}
.btn:active{ transform: translateY(1px); }
@media (max-width: 420px){
.u-grid, .grid{ grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 421px) and (max-width: 500px){
.u-grid, .grid{ grid-template-columns: repeat(4, 1fr); }
}
@media (min-width: 501px){
.u-grid, .grid{ grid-template-columns: repeat(5, 1fr); }
}
/* ------- ЭКСПОРТНЫЙ РЕЖИМ ДЛЯ ЧЁТКОГО PNG ------- */
body.exporting {
--wrap-w: 1500px; /* увеличиваем макет на время снимка */
}
body.exporting .shelf {
box-shadow: none; /* убираем тень (даёт «дымку» на рендере) */
}
body.exporting .label {
backdrop-filter: none; /* блюр съедает резкость */
background: #fff; /* чистый фон под текст */
}
/* при желании можно убрать пунктир/границы у обложек:
body.exporting .cover { border: none; }
*/
</style>
</head>
<body>
<div class="page">
<!-- ПАНЕЛЬ ЗАГРУЗКИ -->
<section class="uploader" aria-label="Добавить обложки">
<h2>Добавьте обложки (клик по ячейке)</h2>
<div class="u-grid" id="uGrid">
<!-- 10 ячеек загрузки: каждая связана со слотом data-slot="1..10" -->
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="1">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="2">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="3">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="4">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="5">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="6">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="7">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="8">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="9">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
<div class="u-item">
<label class="u-btn">
<input type="file" accept="image/*" data-slot="10">
<span>Добавить<br>обложку</span>
<div class="u-preview" hidden></div>
</label>
</div>
</div>
</section>
<!-- ПОЛКИ (ЭТО РЕНДЕРИМ В PNG) -->
<section class="shelves-wrap" id="exportArea" aria-label="Книжные полки">
<!-- Полка 1 -->
<div class="shelf">
<div class="grid">
<div class="book">
<div class="cover" data-slot="1"><span>Добавить<br>обложку</span></div>
<div class="label">Последняя книга, которую прочитал(а)</div>
</div>
<div class="book">
<div class="cover" data-slot="2"><span>Добавить<br>обложку</span></div>
<div class="label">Читаю сейчас</div>
</div>
<div class="book">
<div class="cover" data-slot="3"><span>Добавить<br>обложку</span></div>
<div class="label">Комфортная книга</div>
</div>
<div class="book">
<div class="cover" data-slot="4"><span>Добавить<br>обложку</span></div>
<div class="label">Последняя купленная</div>
</div>
<div class="book">
<div class="cover" data-slot="5"><span>Добавить<br>обложку</span></div>
<div class="label">Книга, которую я перечитываю снова и снова</div>
</div>
</div>
</div>
<!-- Полка 2 -->
<div class="shelf">
<div class="grid">
<div class="book">
<div class="cover" data-slot="6"><span>Добавить<br>обложку</span></div>
<div class="label">Книга, которую я хотел(а) бы забыть, чтобы прочитать заново</div>
</div>
<div class="book">
<div class="cover" data-slot="7"><span>Добавить<br>обложку</span></div>
<div class="label">Пугающая книга</div>
</div>
<div class="book">
<div class="cover" data-slot="8"><span>Добавить<br>обложку</span></div>
<div class="label">Книга, которая стала неожиданным открытием</div>
</div>
<div class="book">
<div class="cover" data-slot="9"><span>Добавить<br>обложку</span></div>
<div class="label">Книга с героем, на которого я похож(а)</div>
</div>
<div class="book">
<div class="cover" data-slot="10"><span>Добавить<br>обложку</span></div>
<div class="label">Книга, в которой хочется жить</div>
</div>
</div>
</div>
</section>
<div class="actions">
<button class="btn" id="openBtn">Скачать PNG</button>
</div>
</div>
<!-- html2canvas для экспорта PNG -->
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script>
(function(){
// связываем загрузчики и слоты
const fileInputs = document.querySelectorAll('input[type="file"][data-slot]');
const covers = [...document.querySelectorAll('.cover[data-slot]')]
.reduce((acc, el)=>{ acc[el.dataset.slot] = el; return acc; }, {});
const previews = [...document.querySelectorAll('.u-btn input[data-slot]')].reduce((acc, inp)=>{
const p = inp.parentElement.querySelector('.u-preview');
acc[inp.dataset.slot] = p;
return acc;
}, {});
function loadFileTo(elCover, elPreview, file){
if(!file) return;
const reader = new FileReader();
reader.onload = () => {
const url = reader.result; // data: URL
// превью в панели
if(elPreview){ elPreview.style.backgroundImage = `url('${url}')`; elPreview.hidden = false; }
// в слот
elCover.style.backgroundImage = `url('${url}')`;
elCover.classList.add('filled');
const hint = elCover.querySelector('span'); if(hint) hint.remove();
};
reader.readAsDataURL(file);
}
fileInputs.forEach(inp=>{
inp.addEventListener('change', ()=>{
const slot = inp.dataset.slot;
const elCover = covers[slot];
const elPreview = previews[slot];
const file = inp.files?.[0];
if(elCover && file) loadFileTo(elCover, elPreview, file);
});
});
// Экспорт PNG: временно увеличиваем макет и отключаем «мылкие» эффекты
const exportArea = document.getElementById('exportArea');
document.getElementById('openBtn').addEventListener('click', async ()=>{
try {
// 1) включаем экспортный режим
document.body.classList.add('exporting');
// 2) ждём один кадр, чтобы лэйаут пересчитался
await new Promise(r => requestAnimationFrame(r));
// 3) масштаб рендера: от 2 до 3 (больше — может быть тяжело на слабых устройствах)
const scale = Math.min(3, Math.max(2, window.devicePixelRatio || 1));
// 4) рендерим канву
const canvas = await html2canvas(exportArea, {
backgroundColor: null,
scale,
useCORS: false, // внутри exportArea — только data-URL из FileReader
allowTaint: false
});
// 5) отключаем экспортный режим
document.body.classList.remove('exporting');
// 6) открываем PNG в новой вкладке
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
// даём вкладке время забрать Blob и чистим URL
setTimeout(()=> URL.revokeObjectURL(url), 8000);
}, 'image/png');
} catch (e) {
console.error(e);
// обязательно возвращаем обычные стили в случае ошибки
document.body.classList.remove('exporting');
alert('Не удалось сформировать изображение.');
}
});
})();
</script>
</body>
</html>
[/html]