подскажите , где и как включить (установить) "закруглённые формы аватарок" и " тени авотарок " ?
Отредактировано Николаев (Сб, 28 Сен 2024 16:27:26)
Единый форум поддержки |
Привет, Гость! Войдите или зарегистрируйтесь.
Вы здесь » Единый форум поддержки » Форум для новичков » Скрипты Украшаем Форум (обновленная тема)
подскажите , где и как включить (установить) "закруглённые формы аватарок" и " тени авотарок " ?
Отредактировано Николаев (Сб, 28 Сен 2024 16:27:26)
Николаев
В Администрирование - Формы - HTML-верх
<style>
.post .pa-avatar img {
border-radius: 10px; /* Закругление углов */
box-shadow: 0 0 10px 2px #333; /* Тень */
}
</style>Примерно как-то так. Значения можете поменять на свои.
Reysler Спасибо за полезную информацию. Всё получилось.
Отредактировано Николаев (Вс, 29 Сен 2024 12:41:23)
А можно пожалуйста какие-нибудь летние украшения?
Например солнце наверху форума или что-то подобное!
👻Итак, в связи с «Тыквенным Спасом»©, на моём форуме на несколько дней снова поселились 🎃тыковка и 🕸 паутинка.
🕸 Паутинка
находится в ↗правом верхнем углу
видна только на главной странице
при наведении курсора мышки исчезает на 2 секунды, потом опять появляется
на мобильнике не работает
но если вы в мобильном браузере включите режим "Версия для ПК", то сможете увидеть паутинку и закрыть её касанием пальцем (т.н."тапом") на 2 секунды
🎃Тыковка
всё как у паутинки, только находится в ↙нижнем левом углу и видна на всех страницах (потому, что не мешает🧡).
👉Установка: в HTML низ.
Код:<!-- Тыква внизу слева --> <style> body { margin: 0; } #pumpkin { position: fixed; left: 0; bottom: 0; z-index: 1001; /* Размер */ width: 40px; height: 40px; /* Фон */ background-image: url('https://i.gyazo.com/3fad566ba3f8b5d08c98524267404d71.png'); background-size: contain; background-repeat: no-repeat; background-position: center; /* Анимация и взаимодействие */ opacity: 1; transition: opacity 0.2s ease; pointer-events: auto; } #pumpkin.hidden { opacity: 0; pointer-events: none; } @media (max-width: 768px) { #pumpkin { display: none !important; } } </style> <div id="pumpkin"></div> <script> document.addEventListener('DOMContentLoaded', () => { const pumpkin = document.getElementById('pumpkin'); if (!pumpkin) return; let hideTimeout = null; const hideTemporarily = () => { if (hideTimeout) clearTimeout(hideTimeout); pumpkin.classList.add('hidden'); hideTimeout = setTimeout(() => { pumpkin.classList.remove('hidden'); hideTimeout = null; }, 2000); }; // Десктоп: наведение pumpkin.addEventListener('mouseenter', hideTemporarily); // Мобильные: касание pumpkin.addEventListener('touchstart', (e) => { hideTemporarily(); e.preventDefault(); }); }); </script> <!-- Конец: тыква внизу слева -->
Код:<!-- Паутина: будет создана только на главной странице и только на десктопе --> <script> document.addEventListener('DOMContentLoaded', () => { // Проверка: мобильное устройство? const isMobile = window.matchMedia('(max-width: 768px)').matches; // или используйте любой другой порог if (isMobile) return; // Определяем, является ли текущая страница главной const isHomePage = location.pathname === '/' || location.pathname === '/index.html' || location.pathname === '/index.htm'; // Если НЕ главная — ничего не делаем if (!isHomePage) return; // Создаём элемент паутины const spiderWeb = document.createElement('div'); spiderWeb.className = 'spider-web'; document.body.appendChild(spiderWeb); // === Стили === const style = document.createElement('style'); style.textContent = ` .spider-web { position: fixed; z-index: 1000; top: -20px; right: 4.5%; width: 20vw; max-width: 200px; aspect-ratio: 1 / 1; background-image: url('https://upforme.ru/uploads/0007/e3/f7/6822/348310.png'); background-size: contain; background-repeat: no-repeat; background-position: center; opacity: 1; transition: opacity 0.3s ease; pointer-events: auto; } .spider-web.hidden { opacity: 0; pointer-events: none; } `; document.head.appendChild(style); // === Поведение: исчезновение и возврат === let hideTimeout = null; const hideTemporarily = () => { if (hideTimeout) clearTimeout(hideTimeout); spiderWeb.classList.add('hidden'); hideTimeout = setTimeout(() => { spiderWeb.classList.remove('hidden'); hideTimeout = null; }, 2000); }; spiderWeb.addEventListener('mouseenter', hideTemporarily); spiderWeb.addEventListener('touchstart', (e) => { hideTemporarily(); e.preventDefault(); }); }); </script> <!-- Конец: паутина в углу -->
P.S. Сделано по мотивам идей на сайте ForumD (https://forumdes.mybb.ru/viewtopic.php?id=6665).
Отредактировано Merlin777 (Вс, 26 Окт 2025 19:04:03)
Галерея фотографий в виде колоды карт
[html]
<div class="cart_container">
<div class="cart">
<div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/591695.jpg" alt="" /></div>
</div>
<div class="cart">
<div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/492941.jpg" alt="" /></div>
</div>
<div class="cart">
<div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/865869.jpg" alt="" /></div>
</div>
<div class="cart">
<div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/119023.jpg" alt="" /></div>
</div>
<div class="cart">
<div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/961457.jpg" alt="" /></div>
</div>
<div class="cart">
<div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/502938.jpg" alt="" /></div>
</div>
</div>
<style>
/*
📦 Контейнер всей колоды. Позиционируется относительно, чтобы .cart с position: absolute
были привязаны именно к нему, а не к body.
*/
.cart_container {
margin: 20px 0;
position: relative;
}
/*
🃏 Стиль одной "карты". Все карты наложены друг на друга в центре.
Высота и ширина фиксированы: 400×300px — это размер "рамки" карты.
*/
.cart {
height: 400px;
width: 300px;
position: absolute;
top: 0;
/* Центрирование по горизонтали: 50% экрана минус половина ширины карты (150px) */
left: calc(50% - 150px);
/*
⚠️ ВАЖНО: overflow: visible — чтобы повёрнутые уголки не обрезались!
Обрезка изображения происходит внутри .img-wrap, а не здесь.
*/
overflow: visible;
/* Тень и рамка для объёма */
box-shadow: 0 4px 12px rgba(0,0,0,0.1), 0 10px 20px rgba(0,0,0,0.1);
cursor: pointer;
border-radius: 10px; /* Скруглённые углы */
border: 1px solid #337AB7; /* Синяя рамка */
/*
🖼️ padding создаёт "паспарту" — фоновую полоску (#BFE2FF) вокруг изображения.
Без него изображение вплотную прилегает к краю, и скруглённые углы становятся невидимы.
*/
padding: 10px;
z-index: 100;
background-color: #BFE2FF; /* Цвет паспарту */
}
/*
🌀 Каждой карте задаётся индивидуальный поворот для эффекта "разбросанной колоды".
У 3-й карты самый сильный поворот (8.5°), поэтому именно у неё чаще всего обрезаются уголки.
*/
.cart:nth-child(1) {
transform: rotate(-3deg);
position: relative; /* Только первая карта — relative, чтобы не мешать stacking context */
}
.cart:nth-child(2) { transform: rotate(4deg); }
.cart:nth-child(3) { transform: rotate(8.5deg); } /* ← максимальный поворот */
.cart:nth-child(4) { transform: rotate(-6deg); }
.cart:nth-child(5) { transform: rotate(-2deg); }
.cart:nth-child(6) { transform: rotate(7deg); }
/*
🖼️ Обёртка для изображения.
Здесь происходит обрезка изображения, чтобы оно не вылезало за скруглённые углы.
border-radius чуть меньше (6px), чтобы не касаться синей рамки карты.
*/
.img-wrap {
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 6px;
}
/*
📸 Изображение внутри карты.
object-fit: cover — масштабирует фото с сохранением пропорций так, чтобы полностью
заполнить область 300×400px. При этом часть изображения может обрезаться по краям,
если его пропорции не совпадают с 3:4.
*/
.cart img {
width: 100%;
height: 100%;
/*
* Режим масштабирования изображения:
* • cover — заполняет всю область (может обрезать края, но без пустот) ← используется сейчас
* • contain — показывает всё изображение (без обрезки, но могут быть пустые полосы)
* • fill — растягивает без пустот и обрезки, но искажает пропорции (не рекомендуется)
*/
object-fit: cover;
display: block;
}
/*
🎭 Классы анимации: добавляются при клике.
bottom — для всех карт, кроме последней в цикле.
bottom_last — для последней карты (чтобы после неё сбросить порядок).
*/
.bottom {
z-index: 50;
animation: move ease-in-out 1s forwards;
}
.bottom_last {
z-index: 30;
animation: move_last ease-in-out 1s forwards;
}
/*
🔄 Анимация "ухода вправо и возврата на место".
В середине анимации карта сдвигается вправо на 220px (всё ещё внутри контейнера),
а z-index меняется, чтобы визуально карта уходила "вниз колоды".
*/
@keyframes move {
0% {
left: calc(50% - 150px);
z-index: 150; /* временно поднимаем наверх для плавного старта */
}
50% {
left: calc(50% + 220px); /* улетает вправо */
}
100% {
left: calc(50% - 150px); /* возвращается на место */
z-index: 50; /* остаётся внизу колоды */
}
}
@keyframes move_last {
0% {
left: calc(50% - 150px);
z-index: 150;
}
50% {
left: calc(50% + 220px);
z-index: 50;
}
100% {
left: calc(50% - 150px);
z-index: 30; /* самая нижняя позиция */
}
}
/*
📱 Мобильная адаптация: карта становится уже (260px), чтобы повёрнутые края
не вылезали за границы узкого экрана. Центрирование сохраняется.
*/
@media (max-width: 768px) {
.cart {
width: 260px;
height: calc(260px * 4 / 3);
left: calc(50% - 130px); /* 260 / 2 = 130 */
}
/* Анимации адаптируются под новую ширину */
@keyframes move {
0% { left: calc(50% - 130px); z-index: 150; }
50% { left: calc(50% + 180px); }
100% { left: calc(50% - 130px); z-index: 50; }
}
@keyframes move_last {
0% { left: calc(50% - 130px); z-index: 150; }
50% { left: calc(50% + 180px); z-index: 50; }
100% { left: calc(50% - 130px); z-index: 30; }
}
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function () {
let count = 1; // 🔢 Счётчик кликов (начинается с 1)
let click = true; // 🛑 Флаг блокировки повторных кликов во время анимации
let num = $(".cart").length; // 🃏 Общее количество карт
$(".cart").click(function() {
if (!click) return; // Если анимация идёт — игнорируем клик
click = false;
// Добавляем нужный класс анимации
if (count < num) {
$(this).addClass("bottom");
count++;
} else {
$(this).addClass("bottom_last");
count++;
}
// После полного цикла (6 кликов) — сбрасываем все классы и счётчик
if (count === num + 1) {
setTimeout(function () {
$(".cart").removeClass("bottom bottom_last");
count = 1;
}, 1000);
}
// Разблокируем клики через 1 секунду (длительность анимации)
setTimeout(function () {
click = true;
}, 1000);
});
});
</script>
[/html]
Код:[html] <div class="cart_container"> <div class="cart"> <div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/591695.jpg" alt="" /></div> </div> <div class="cart"> <div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/492941.jpg" alt="" /></div> </div> <div class="cart"> <div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/865869.jpg" alt="" /></div> </div> <div class="cart"> <div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/119023.jpg" alt="" /></div> </div> <div class="cart"> <div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/961457.jpg" alt="" /></div> </div> <div class="cart"> <div class="img-wrap"><img src="https://upforme.ru/uploads/001a/f0/7d/57/502938.jpg" alt="" /></div> </div> </div> <style> /* 📦 Контейнер всей колоды. Позиционируется относительно, чтобы .cart с position: absolute были привязаны именно к нему, а не к body. */ .cart_container { margin: 20px 0; position: relative; } /* 🃏 Стиль одной "карты". Все карты наложены друг на друга в центре. Высота и ширина фиксированы: 400×300px — это размер "рамки" карты. */ .cart { height: 400px; width: 300px; position: absolute; top: 0; /* Центрирование по горизонтали: 50% экрана минус половина ширины карты (150px) */ left: calc(50% - 150px); /* ⚠️ ВАЖНО: overflow: visible — чтобы повёрнутые уголки не обрезались! Обрезка изображения происходит внутри .img-wrap, а не здесь. */ overflow: visible; /* Тень и рамка для объёма */ box-shadow: 0 4px 12px rgba(0,0,0,0.1), 0 10px 20px rgba(0,0,0,0.1); cursor: pointer; border-radius: 10px; /* Скруглённые углы */ border: 1px solid #337AB7; /* Синяя рамка */ /* 🖼️ padding создаёт "паспарту" — фоновую полоску (#BFE2FF) вокруг изображения. Без него изображение вплотную прилегает к краю, и скруглённые углы становятся невидимы. */ padding: 10px; z-index: 100; background-color: #BFE2FF; /* Цвет паспарту */ } /* 🌀 Каждой карте задаётся индивидуальный поворот для эффекта "разбросанной колоды". У 3-й карты самый сильный поворот (8.5°), поэтому именно у неё чаще всего обрезаются уголки. */ .cart:nth-child(1) { transform: rotate(-3deg); position: relative; /* Только первая карта — relative, чтобы не мешать stacking context */ } .cart:nth-child(2) { transform: rotate(4deg); } .cart:nth-child(3) { transform: rotate(8.5deg); } /* ← максимальный поворот */ .cart:nth-child(4) { transform: rotate(-6deg); } .cart:nth-child(5) { transform: rotate(-2deg); } .cart:nth-child(6) { transform: rotate(7deg); } /* 🖼️ Обёртка для изображения. Здесь происходит обрезка изображения, чтобы оно не вылезало за скруглённые углы. border-radius чуть меньше (6px), чтобы не касаться синей рамки карты. */ .img-wrap { width: 100%; height: 100%; overflow: hidden; border-radius: 6px; } /* 📸 Изображение внутри карты. object-fit: cover — масштабирует фото с сохранением пропорций так, чтобы полностью заполнить область 300×400px. При этом часть изображения может обрезаться по краям, если его пропорции не совпадают с 3:4. */ .cart img { width: 100%; height: 100%; /* * Режим масштабирования изображения: * • cover — заполняет всю область (может обрезать края, но без пустот) ← используется сейчас * • contain — показывает всё изображение (без обрезки, но могут быть пустые полосы) * • fill — растягивает без пустот и обрезки, но искажает пропорции (не рекомендуется) */ object-fit: cover; display: block; } /* 🎭 Классы анимации: добавляются при клике. bottom — для всех карт, кроме последней в цикле. bottom_last — для последней карты (чтобы после неё сбросить порядок). */ .bottom { z-index: 50; animation: move ease-in-out 1s forwards; } .bottom_last { z-index: 30; animation: move_last ease-in-out 1s forwards; } /* 🔄 Анимация "ухода вправо и возврата на место". В середине анимации карта сдвигается вправо на 220px (всё ещё внутри контейнера), а z-index меняется, чтобы визуально карта уходила "вниз колоды". */ @keyframes move { 0% { left: calc(50% - 150px); z-index: 150; /* временно поднимаем наверх для плавного старта */ } 50% { left: calc(50% + 220px); /* улетает вправо */ } 100% { left: calc(50% - 150px); /* возвращается на место */ z-index: 50; /* остаётся внизу колоды */ } } @keyframes move_last { 0% { left: calc(50% - 150px); z-index: 150; } 50% { left: calc(50% + 220px); z-index: 50; } 100% { left: calc(50% - 150px); z-index: 30; /* самая нижняя позиция */ } } /* 📱 Мобильная адаптация: карта становится уже (260px), чтобы повёрнутые края не вылезали за границы узкого экрана. Центрирование сохраняется. */ @media (max-width: 768px) { .cart { width: 260px; height: calc(260px * 4 / 3); left: calc(50% - 130px); /* 260 / 2 = 130 */ } /* Анимации адаптируются под новую ширину */ @keyframes move { 0% { left: calc(50% - 130px); z-index: 150; } 50% { left: calc(50% + 180px); } 100% { left: calc(50% - 130px); z-index: 50; } } @keyframes move_last { 0% { left: calc(50% - 130px); z-index: 150; } 50% { left: calc(50% + 180px); z-index: 50; } 100% { left: calc(50% - 130px); z-index: 30; } } } </style> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> $(function () { let count = 1; // 🔢 Счётчик кликов (начинается с 1) let click = true; // 🛑 Флаг блокировки повторных кликов во время анимации let num = $(".cart").length; // 🃏 Общее количество карт $(".cart").click(function() { if (!click) return; // Если анимация идёт — игнорируем клик click = false; // Добавляем нужный класс анимации if (count < num) { $(this).addClass("bottom"); count++; } else { $(this).addClass("bottom_last"); count++; } // После полного цикла (6 кликов) — сбрасываем все классы и счётчик if (count === num + 1) { setTimeout(function () { $(".cart").removeClass("bottom bottom_last"); count = 1; }, 1000); } // Разблокируем клики через 1 секунду (длительность анимации) setTimeout(function () { click = true; }, 1000); }); }); </script> [/html]
1. Имейте в виду, что скрипт показывает изображения в виде перевёрнутой колоды, т.е. с нижнего в списке до верхнего.
2. При изменении количества фотографий, нужно будет задать их поворот, добавив класс(ы)
Код:.cart:nth-child(номер_фото) { transform: rotate(радиус); }Поворот лучше повторяйте их уже ранее применённых, просто скопируйте повороты по порядку.
P.S. @Laurita вот ещё вариант слайдера картинок:)
Отредактировано Merlin777 (Пн, 3 Ноя 2025 12:49:41)
Туман на фотографии, который убирается:
⊹ курсором 🐭мыши на 💻компе
👇🏻пальцем на 📲сенсорных устройствах
[html]
<div class="fog-container">
<canvas id="fog"></canvas>
<canvas id="fog-bg"></canvas>
<canvas id="brush"></canvas>
<svg id="brush-cursor" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" fill="none" stroke="white" stroke-width="2" stroke-opacity="0.7"/>
<circle cx="24" cy="24" r="12" fill="white" fill-opacity="0.15"/>
</svg>
</div>
<button id="reset-fog" type="button">Вернуть туман</button>
<style>
/* "Crafting dreams and magic" by Merlin */
.fog-container {
aspect-ratio: 16 / 9;
position: relative;
overflow: hidden;
margin: 20px 0;
border-radius: 6px;
touch-action: none;
width: 100%;
cursor: none;
}
/* Уменьшаем размер только на десктопе */
@media (min-width: 768px) {
.fog-container {
max-width: 60rem;
margin-left: auto;
margin-right: auto;
}
}
.fog-container canvas {
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
}
#brush {
opacity: 0;
}
#brush-cursor {
position: fixed;
pointer-events: none;
width: 40px;
height: 40px;
margin: 0;
top: 0;
left: 0;
z-index: 1000;
opacity: 0;
transition: opacity 0.15s ease;
transform: translate(-50%, -50%);
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.4));
}
/* === Кнопка "Вернуть туман" под изображением === */
#reset-fog {
display: block;
margin: 20px auto;
padding: 12px 24px;
background: rgba(0, 0, 0, 0.3);
color: white;
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
backdrop-filter: blur(4px);
/* Сброс outline для кастомного focus */
outline: none;
}
/* Эффект "тумана" при наведении — ТОЛЬКО для мыши */
@media (hover: hover) and (pointer: fine) {
#reset-fog::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(4px);
transform: translateY(100%);
transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1);
z-index: 0;
}
#reset-fog span {
position: relative;
z-index: 1;
}
#reset-fog:hover {
border-color: rgba(255, 255, 255, 0.8);
}
#reset-fog:hover::before {
transform: translateY(0);
}
}
/* Активное состояние — для всех (мышь + тач) */
#reset-fog:active {
border-color: rgba(255, 255, 255, 0.9);
background: rgba(0, 0, 0, 0.4);
transform: scale(0.98);
transition: transform 0.1s ease;
}
/* Фокус для клавиатуры */
#reset-fog:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
/* Адаптивность */
@media (max-width: 480px) {
#reset-fog {
padding: 9px 20px;
font-size: 15px;
}
}
/* На сенсорных устройствах — возвращаем обычный курсор и скрываем кисть */
@media (pointer: coarse) {
.fog-container {
cursor: auto !important;
}
#brush-cursor {
display: none !important;
}
}
</style>
<script>
class FogParticle {
constructor(ctx, canvasWidth, canvasHeight) {
this.ctx = ctx;
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
this.x = 0;
this.y = 0;
}
setPosition(x, y) {
this.x = x;
this.y = y;
}
setVelocity(x, y) {
this.xVelocity = x;
this.yVelocity = y;
}
setImage(image) {
this.image = image;
}
render() {
if (!this.image) return;
this.ctx.drawImage(
this.image,
this.x - this.image.width / 2,
this.y - this.image.height / 2,
400,
400
);
this.x += this.xVelocity;
this.y += this.yVelocity;
if (this.x >= this.canvasWidth) {
this.xVelocity = -this.xVelocity;
this.x = this.canvasWidth;
} else if (this.x <= 0) {
this.xVelocity = -this.xVelocity;
this.x = 0;
}
if (this.y >= this.canvasHeight) {
this.yVelocity = -this.yVelocity;
this.y = this.canvasHeight;
} else if (this.y <= 0) {
this.yVelocity = -this.yVelocity;
this.y = 0;
}
}
}
class Fog {
constructor({ selector, density = 50, velocity = 2, particle, bgi } = {}) {
const canvas = document.querySelector(selector);
const bcr = canvas.parentElement.getBoundingClientRect();
this.ctx = canvas.getContext('2d');
this.canvasWidth = canvas.width = bcr.width;
this.canvasHeight = canvas.height = bcr.height;
this.particleCount = density;
this.maxVelocity = velocity;
this.particle = particle;
this.bgi = bgi;
this._createParticles();
this._setImage();
if (!this.bgi) return;
const img = new Image();
img.onload = () => {
const size = coverImg(img, this.canvasWidth, this.canvasHeight);
this.bgi = { img, w: size.w, h: size.h };
this._render();
};
img.src = this.bgi;
}
_createParticles() {
this.particles = [];
const random = (min, max) => Math.random() * (max - min) + min;
for (let i = 0; i < this.particleCount; i++) {
const particle = new FogParticle(this.ctx, this.canvasWidth, this.canvasHeight);
particle.setPosition(
random(0, this.canvasWidth),
random(0, this.canvasHeight)
);
particle.setVelocity(
random(-this.maxVelocity, this.maxVelocity),
random(-this.maxVelocity, this.maxVelocity)
);
this.particles.push(particle);
}
}
_setImage() {
if (!this.particle) return;
const img = new Image();
img.onload = () => this.particles.forEach(p => p.setImage(img));
img.src = this.particle;
}
_render() {
if (this.bgi) {
this.ctx.drawImage(this.bgi.img, 0, 0, this.bgi.w, this.bgi.h);
} else {
this.ctx.fillStyle = "rgba(0, 0, 0, 1)";
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
}
this.particles.forEach(p => p.render());
requestAnimationFrame(this._render.bind(this));
}
}
class Eraser {
constructor({ bgCanvas, brushCanvas, bgi, radius = 120 } = {}) {
this.bgCanvas = document.querySelector(bgCanvas);
this.brushCanvas = document.querySelector(brushCanvas);
this.bgCtx = this.bgCanvas.getContext('2d');
this.brushCtx = this.brushCanvas.getContext('2d');
this.parentElement = this.bgCanvas.parentElement;
const bcr = this.parentElement.getBoundingClientRect();
this.canvasWidth = this.bgCanvas.width = this.brushCanvas.width = bcr.width;
this.canvasHeight = this.bgCanvas.height = this.brushCanvas.height = bcr.height;
this.brushRadius = radius;
this.bgi = new Image();
this.bgi.src = bgi;
this.bgi.onload = this._attachEvents.bind(this);
const bgCanvasEl = this.bgCanvas;
this.utils = {
distanceBetween(point1, point2) {
return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
},
angleBetween(point1, point2) {
return Math.atan2(point2.x - point1.x, point2.y - point1.y);
},
getMousePos(e) {
const bcr = bgCanvasEl.getBoundingClientRect();
return { x: e.clientX - bcr.left, y: e.clientY - bcr.top };
},
getTouchPos(touch) {
const bcr = bgCanvasEl.getBoundingClientRect();
return { x: touch.clientX - bcr.left, y: touch.clientY - bcr.top };
}
};
}
_attachEvents() {
const parent = this.parentElement;
parent.addEventListener('mousemove', this._onMouseMove.bind(this));
parent.addEventListener('mouseleave', this._onMouseLeave.bind(this));
parent.addEventListener('touchstart', this._onTouchStart.bind(this), { passive: false });
parent.addEventListener('touchmove', this._onTouchMove.bind(this), { passive: false });
parent.addEventListener('touchend', this._onTouchEnd.bind(this), { passive: false });
}
_onMouseMove(e) {
const currentPoint = this.utils.getMousePos(e);
this._drawStroke(currentPoint);
}
_onMouseLeave() {
this.lastPoint = null;
}
_onTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
const pos = this.utils.getTouchPos(touch);
this.lastPoint = pos;
}
_onTouchMove(e) {
e.preventDefault();
const touch = e.touches[0];
const currentPoint = this.utils.getTouchPos(touch);
this._drawStroke(currentPoint);
}
_onTouchEnd(e) {
e.preventDefault();
this.lastPoint = null;
}
_drawStroke(currentPoint) {
this.lastPoint = this.lastPoint || currentPoint;
const dist = this.utils.distanceBetween(this.lastPoint, currentPoint);
const angle = this.utils.angleBetween(this.lastPoint, currentPoint);
for (let ii = 0; ii < dist; ii += 5) {
const x = this.lastPoint.x + (Math.sin(angle) * ii);
const y = this.lastPoint.y + (Math.cos(angle) * ii);
const brush = this.brushCtx.createRadialGradient(x, y, 0, x, y, this.brushRadius);
brush.addColorStop(0, 'rgba(0, 0, 0, 1)');
brush.addColorStop(0.3, 'rgba(0, 0, 0, 0.1)');
brush.addColorStop(1, 'rgba(0, 0, 0, 0)');
this.brushCtx.fillStyle = brush;
this.brushCtx.fillRect(
x - this.brushRadius,
y - this.brushRadius,
this.brushRadius * 2,
this.brushRadius * 2
);
}
this.lastPoint = currentPoint;
this.bgCtx.globalCompositeOperation = 'source-over';
const size = coverImg(this.bgi, this.canvasWidth, this.canvasHeight);
this.bgCtx.drawImage(this.bgi, 0, 0, size.w, size.h);
this.bgCtx.globalCompositeOperation = 'destination-in';
this.bgCtx.drawImage(this.brushCanvas, 0, 0);
}
}
const coverImg = (img, width, height) => {
const ratio = img.width / img.height;
let w = width;
let h = w / ratio;
if (h < height) {
h = height;
w = h * ratio;
}
return { w, h };
};
// ✅ Основное изображение
const bgi = 'https://upforme.ru/uploads/001a/f0/7d/2/35680.webp';
// Адаптивные параметры
function getFogDensity() {
if (window.innerWidth < 480) return 25;
if (window.innerWidth < 768) return 35;
return 80;
}
function getEraserRadius() {
if (window.innerWidth < 480) return 40;
if (window.innerWidth < 768) return 55;
return 60;
}
function resize() {
new Fog({
selector: '#fog',
// ✅ Туман
particle: 'https://upforme.ru/uploads/001a/f0/7d/2/814636.png',
density: getFogDensity(),
bgi,
});
new Eraser({
bgCanvas: '#fog-bg',
brushCanvas: '#brush',
radius: getEraserRadius(),
bgi,
});
}
// Инициализация
resize();
window.addEventListener("resize", resize);
// === ТОЧНЫЙ КУРСОР-КИСТЬ (только для мыши) ===
const customCursor = document.getElementById('brush-cursor');
const fogContainer = document.querySelector('.fog-container');
// Показываем кисть ТОЛЬКО если есть точный указатель (мышь)
if (customCursor && fogContainer && window.matchMedia('(pointer: fine)').matches) {
const updateCursor = (e) => {
customCursor.style.left = e.clientX + 'px';
customCursor.style.top = e.clientY + 'px';
customCursor.style.opacity = '1';
};
fogContainer.addEventListener('mousemove', updateCursor);
fogContainer.addEventListener('mouseenter', () => {
customCursor.style.opacity = '1';
});
fogContainer.addEventListener('mouseleave', () => {
customCursor.style.opacity = '0';
});
} else if (customCursor) {
// На тач-устройствах — скрываем
customCursor.style.display = 'none';
}
// === КНОПКА "ВЕРНУТЬ ТУМАН" ===
document.getElementById('reset-fog')?.addEventListener('click', () => {
['fog', 'fog-bg', 'brush'].forEach(id => {
const canvas = document.getElementById(id);
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
});
resize();
});
</script>
[/html]
Код:[html] <div class="fog-container"> <canvas id="fog"></canvas> <canvas id="fog-bg"></canvas> <canvas id="brush"></canvas> <svg id="brush-cursor" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"> <circle cx="24" cy="24" r="20" fill="none" stroke="white" stroke-width="2" stroke-opacity="0.7"/> <circle cx="24" cy="24" r="12" fill="white" fill-opacity="0.15"/> </svg> </div> <button id="reset-fog" type="button">Вернуть туман</button> <style> /* "Crafting dreams and magic" by Merlin */ .fog-container { aspect-ratio: 16 / 9; position: relative; overflow: hidden; margin: 20px 0; border-radius: 6px; touch-action: none; width: 100%; cursor: none; } /* Уменьшаем размер только на десктопе */ @media (min-width: 768px) { .fog-container { max-width: 60rem; margin-left: auto; margin-right: auto; } } .fog-container canvas { display: block; width: 100%; height: 100%; position: absolute; left: 50%; top: 0; transform: translateX(-50%); } #brush { opacity: 0; } #brush-cursor { position: fixed; pointer-events: none; width: 40px; height: 40px; margin: 0; top: 0; left: 0; z-index: 1000; opacity: 0; transition: opacity 0.15s ease; transform: translate(-50%, -50%); filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.4)); } /* === Кнопка "Вернуть туман" под изображением === */ #reset-fog { display: block; margin: 20px auto; padding: 12px 24px; background: rgba(0, 0, 0, 0.3); color: white; border: 1px solid rgba(255, 255, 255, 0.7); border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 500; backdrop-filter: blur(4px); /* Сброс outline для кастомного focus */ outline: none; } /* Эффект "тумана" при наведении — ТОЛЬКО для мыши */ @media (hover: hover) and (pointer: fine) { #reset-fog::before { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(4px); transform: translateY(100%); transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); z-index: 0; } #reset-fog span { position: relative; z-index: 1; } #reset-fog:hover { border-color: rgba(255, 255, 255, 0.8); } #reset-fog:hover::before { transform: translateY(0); } } /* Активное состояние — для всех (мышь + тач) */ #reset-fog:active { border-color: rgba(255, 255, 255, 0.9); background: rgba(0, 0, 0, 0.4); transform: scale(0.98); transition: transform 0.1s ease; } /* Фокус для клавиатуры */ #reset-fog:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } /* Адаптивность */ @media (max-width: 480px) { #reset-fog { padding: 9px 20px; font-size: 15px; } } /* На сенсорных устройствах — возвращаем обычный курсор и скрываем кисть */ @media (pointer: coarse) { .fog-container { cursor: auto !important; } #brush-cursor { display: none !important; } } </style> <script> class FogParticle { constructor(ctx, canvasWidth, canvasHeight) { this.ctx = ctx; this.canvasWidth = canvasWidth; this.canvasHeight = canvasHeight; this.x = 0; this.y = 0; } setPosition(x, y) { this.x = x; this.y = y; } setVelocity(x, y) { this.xVelocity = x; this.yVelocity = y; } setImage(image) { this.image = image; } render() { if (!this.image) return; this.ctx.drawImage( this.image, this.x - this.image.width / 2, this.y - this.image.height / 2, 400, 400 ); this.x += this.xVelocity; this.y += this.yVelocity; if (this.x >= this.canvasWidth) { this.xVelocity = -this.xVelocity; this.x = this.canvasWidth; } else if (this.x <= 0) { this.xVelocity = -this.xVelocity; this.x = 0; } if (this.y >= this.canvasHeight) { this.yVelocity = -this.yVelocity; this.y = this.canvasHeight; } else if (this.y <= 0) { this.yVelocity = -this.yVelocity; this.y = 0; } } } class Fog { constructor({ selector, density = 50, velocity = 2, particle, bgi } = {}) { const canvas = document.querySelector(selector); const bcr = canvas.parentElement.getBoundingClientRect(); this.ctx = canvas.getContext('2d'); this.canvasWidth = canvas.width = bcr.width; this.canvasHeight = canvas.height = bcr.height; this.particleCount = density; this.maxVelocity = velocity; this.particle = particle; this.bgi = bgi; this._createParticles(); this._setImage(); if (!this.bgi) return; const img = new Image(); img.onload = () => { const size = coverImg(img, this.canvasWidth, this.canvasHeight); this.bgi = { img, w: size.w, h: size.h }; this._render(); }; img.src = this.bgi; } _createParticles() { this.particles = []; const random = (min, max) => Math.random() * (max - min) + min; for (let i = 0; i < this.particleCount; i++) { const particle = new FogParticle(this.ctx, this.canvasWidth, this.canvasHeight); particle.setPosition( random(0, this.canvasWidth), random(0, this.canvasHeight) ); particle.setVelocity( random(-this.maxVelocity, this.maxVelocity), random(-this.maxVelocity, this.maxVelocity) ); this.particles.push(particle); } } _setImage() { if (!this.particle) return; const img = new Image(); img.onload = () => this.particles.forEach(p => p.setImage(img)); img.src = this.particle; } _render() { if (this.bgi) { this.ctx.drawImage(this.bgi.img, 0, 0, this.bgi.w, this.bgi.h); } else { this.ctx.fillStyle = "rgba(0, 0, 0, 1)"; this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); } this.particles.forEach(p => p.render()); requestAnimationFrame(this._render.bind(this)); } } class Eraser { constructor({ bgCanvas, brushCanvas, bgi, radius = 120 } = {}) { this.bgCanvas = document.querySelector(bgCanvas); this.brushCanvas = document.querySelector(brushCanvas); this.bgCtx = this.bgCanvas.getContext('2d'); this.brushCtx = this.brushCanvas.getContext('2d'); this.parentElement = this.bgCanvas.parentElement; const bcr = this.parentElement.getBoundingClientRect(); this.canvasWidth = this.bgCanvas.width = this.brushCanvas.width = bcr.width; this.canvasHeight = this.bgCanvas.height = this.brushCanvas.height = bcr.height; this.brushRadius = radius; this.bgi = new Image(); this.bgi.src = bgi; this.bgi.onload = this._attachEvents.bind(this); const bgCanvasEl = this.bgCanvas; this.utils = { distanceBetween(point1, point2) { return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2)); }, angleBetween(point1, point2) { return Math.atan2(point2.x - point1.x, point2.y - point1.y); }, getMousePos(e) { const bcr = bgCanvasEl.getBoundingClientRect(); return { x: e.clientX - bcr.left, y: e.clientY - bcr.top }; }, getTouchPos(touch) { const bcr = bgCanvasEl.getBoundingClientRect(); return { x: touch.clientX - bcr.left, y: touch.clientY - bcr.top }; } }; } _attachEvents() { const parent = this.parentElement; parent.addEventListener('mousemove', this._onMouseMove.bind(this)); parent.addEventListener('mouseleave', this._onMouseLeave.bind(this)); parent.addEventListener('touchstart', this._onTouchStart.bind(this), { passive: false }); parent.addEventListener('touchmove', this._onTouchMove.bind(this), { passive: false }); parent.addEventListener('touchend', this._onTouchEnd.bind(this), { passive: false }); } _onMouseMove(e) { const currentPoint = this.utils.getMousePos(e); this._drawStroke(currentPoint); } _onMouseLeave() { this.lastPoint = null; } _onTouchStart(e) { e.preventDefault(); const touch = e.touches[0]; const pos = this.utils.getTouchPos(touch); this.lastPoint = pos; } _onTouchMove(e) { e.preventDefault(); const touch = e.touches[0]; const currentPoint = this.utils.getTouchPos(touch); this._drawStroke(currentPoint); } _onTouchEnd(e) { e.preventDefault(); this.lastPoint = null; } _drawStroke(currentPoint) { this.lastPoint = this.lastPoint || currentPoint; const dist = this.utils.distanceBetween(this.lastPoint, currentPoint); const angle = this.utils.angleBetween(this.lastPoint, currentPoint); for (let ii = 0; ii < dist; ii += 5) { const x = this.lastPoint.x + (Math.sin(angle) * ii); const y = this.lastPoint.y + (Math.cos(angle) * ii); const brush = this.brushCtx.createRadialGradient(x, y, 0, x, y, this.brushRadius); brush.addColorStop(0, 'rgba(0, 0, 0, 1)'); brush.addColorStop(0.3, 'rgba(0, 0, 0, 0.1)'); brush.addColorStop(1, 'rgba(0, 0, 0, 0)'); this.brushCtx.fillStyle = brush; this.brushCtx.fillRect( x - this.brushRadius, y - this.brushRadius, this.brushRadius * 2, this.brushRadius * 2 ); } this.lastPoint = currentPoint; this.bgCtx.globalCompositeOperation = 'source-over'; const size = coverImg(this.bgi, this.canvasWidth, this.canvasHeight); this.bgCtx.drawImage(this.bgi, 0, 0, size.w, size.h); this.bgCtx.globalCompositeOperation = 'destination-in'; this.bgCtx.drawImage(this.brushCanvas, 0, 0); } } const coverImg = (img, width, height) => { const ratio = img.width / img.height; let w = width; let h = w / ratio; if (h < height) { h = height; w = h * ratio; } return { w, h }; }; // ✅ Основное изображение const bgi = 'https://upforme.ru/uploads/001a/f0/7d/2/35680.webp'; // Адаптивные параметры function getFogDensity() { if (window.innerWidth < 480) return 25; if (window.innerWidth < 768) return 35; return 80; } function getEraserRadius() { if (window.innerWidth < 480) return 40; if (window.innerWidth < 768) return 55; return 60; } function resize() { new Fog({ selector: '#fog', // ✅ Туман particle: 'https://upforme.ru/uploads/001a/f0/7d/2/814636.png', density: getFogDensity(), bgi, }); new Eraser({ bgCanvas: '#fog-bg', brushCanvas: '#brush', radius: getEraserRadius(), bgi, }); } // Инициализация resize(); window.addEventListener("resize", resize); // === ТОЧНЫЙ КУРСОР-КИСТЬ (только для мыши) === const customCursor = document.getElementById('brush-cursor'); const fogContainer = document.querySelector('.fog-container'); // Показываем кисть ТОЛЬКО если есть точный указатель (мышь) if (customCursor && fogContainer && window.matchMedia('(pointer: fine)').matches) { const updateCursor = (e) => { customCursor.style.left = e.clientX + 'px'; customCursor.style.top = e.clientY + 'px'; customCursor.style.opacity = '1'; }; fogContainer.addEventListener('mousemove', updateCursor); fogContainer.addEventListener('mouseenter', () => { customCursor.style.opacity = '1'; }); fogContainer.addEventListener('mouseleave', () => { customCursor.style.opacity = '0'; }); } else if (customCursor) { // На тач-устройствах — скрываем customCursor.style.display = 'none'; } // === КНОПКА "ВЕРНУТЬ ТУМАН" === document.getElementById('reset-fog')?.addEventListener('click', () => { ['fog', 'fog-bg', 'brush'].forEach(id => { const canvas = document.getElementById(id); if (canvas) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); } }); resize(); }); </script> [/html]
Отредактировано Merlin777 (Чт, 30 Окт 2025 22:21:22)
Merlin777
Думаю что имеет смысл выкладывать не только пример работающего "украшения", но и код, который его реализует.
Опытные пользователи конечно могут его получить непосредственно из имеющейся HTML вставки (например процитировав пост в режиме BB-кодов), но большинство таким навыком не обладает.
| Каталог скриптов/CSS | Форум для новичков | Вт, 1 Окт 2013 |
| Запросы по скриптам[2] | Архив | Пн, 25 Авг 2014 |
| Запросы по скриптам | Архив | Чт, 28 Ноя 2013 |
| не загружается изображение | Архив | Ср, 6 Дек 2017 |
| Общие вопросы по оформлению (65) #2 | Вопросы по оформлению форума | Чт, 30 Окт 2025 |
Вы здесь » Единый форум поддержки » Форум для новичков » Скрипты Украшаем Форум (обновленная тема)