Единый форум поддержки

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » Единый форум поддержки » Форум для новичков » Скрипты Украшаем Форум (обновленная тема)


Скрипты Украшаем Форум (обновленная тема)

Сообщений 61 страница 69 из 69

61

подскажите , где и как включить (установить)  "закруглённые формы аватарок" и " тени авотарок "  ?

Отредактировано Николаев (Сб, 28 Сен 2024 16:27:26)

0

62

вот по такому типу
https://upforme.ru/uploads/0000/14/1c/39093/t204740.jpg

Отредактировано Николаев (Сб, 28 Сен 2024 16:34:52)

0

63

Николаев
В Администрирование - Формы - HTML-верх

Код:
<style>
.post .pa-avatar img {
  border-radius: 10px;              /* Закругление углов */
  box-shadow: 0 0 10px 2px #333;    /* Тень */
}
</style>

Примерно как-то так. Значения можете поменять на свои.

+3

64

Reysler  Спасибо за полезную информацию. Всё получилось.

Отредактировано Николаев (Вс, 29 Сен 2024 12:41:23)

0

65

А можно пожалуйста какие-нибудь летние украшения?
Например солнце наверху форума или что-то подобное!

+1

66

👻Итак, в связи с «Тыквенным Спасом»©,  на моём форуме на несколько дней  снова поселились 🎃тыковка и 🕸 паутинка.

🕸 Паутинка

  • находится в ↗правом верхнем углу

  • видна только на главной странице

  • при наведении курсора мышки исчезает на 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)

+3

67

Галерея фотографий в виде колоды карт

[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)

+3

68


Туман на фотографии, который убирается:
⊹ курсором 🐭мыши на 💻компе
👇🏻пальцем на 📲сенсорных устройствах

[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)

+1

69

Merlin777
Думаю что имеет смысл выкладывать не только пример работающего "украшения", но и код, который его реализует.
Опытные пользователи конечно могут его получить непосредственно из имеющейся HTML вставки (например процитировав пост в режиме BB-кодов), но большинство таким навыком не обладает.

+2


Вы здесь » Единый форум поддержки » Форум для новичков » Скрипты Украшаем Форум (обновленная тема)