афыафыаыфаа
фаыфыафыа
Сообщений 1 страница 7 из 7
Опрос
Поделиться22025-10-29 21:16:38
Туман на фотографии, который убирается курсором мыши на компе, и пальцем на телефоне
[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>
.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));
}
.fog-container:hover #brush-cursor {
opacity: 1;
}
/* === Кнопка "Вернуть туман" под изображением === */
#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);
}
/* "Туман", который заполняет кнопку при наведении */
#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);
}
/* Адаптивность */
@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://atuin.ru/demo/i/fog-bg.webp';
// Адаптивные параметры
function getFogDensity() {
if (window.innerWidth < 480) return 25;
if (window.innerWidth < 768) return 35;
return 60;
}
function getEraserRadius() {
if (window.innerWidth < 480) return 40;
if (window.innerWidth < 768) return 55;
return 80;
}
function resize() {
new Fog({
selector: '#fog',
particle: 'https://atuin.ru/demo/i/fog-particle.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';
};
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', () => {
// Очистим все три canvas
const canvases = ['fog', 'fog-bg', 'brush'];
canvases.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]
Поделиться32025-10-29 22:36:07
[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 };
};
// ✅ Исправлено: убраны пробелы в URL
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]
Поделиться42025-10-30 02:47:25
Галерея фотографий в виде колоды карт
[html]
<div class="cart_container">
<div class="cart">
<img src="https://upforme.ru/uploads/001a/f0/7d/57/591695.jpg" alt="" />
</div>
<div class="cart">
<img src="https://upforme.ru/uploads/001a/f0/7d/57/865869.jpg" alt="" />
</div>
<div class="cart">
<img src="https://upforme.ru/uploads/001a/f0/7d/57/865869.jpg" alt="" />
</div>
<div class="cart">
<img src="https://upforme.ru/uploads/001a/f0/7d/57/119023.jpg" alt="" />
</div>
<div class="cart">
<img src="https://upforme.ru/uploads/001a/f0/7d/57/961457.jpg" alt="" />
</div>
<div class="cart">
<img src="https://upforme.ru/uploads/001a/f0/7d/57/502938.jpg" alt="" />
</div>
</div>
<style>
@media (max-width: 768px) {
.cart {
/* Уменьшаем ширину, чтобы карта не вылезала при повороте */
width: 280px;
height: calc(280px * 4 / 3); /* сохраняем пропорции 3:4 → ~373px */
left: 0px; /* небольшой отступ от левого края */
padding: 0px;
}
.cart img {
width: calc(100% - 16px);
height: calc(100% - 16px);
margin: 8px;
}
/* Обновляем анимации под новую позицию */
@keyframes move {
0% { left: 8px; z-index: 150; }
50% { left: calc(8px + 200px); }
100% { left: 8px; z-index: 50; }
}
@keyframes move_last {
0% { left: 8px; z-index: 150; }
50% { left: calc(8px + 200px); z-index: 50; }
100% { left: 8px; z-index: 30; }
}
}
/*
Контейнер всей колоды. Позиционируется относительно, чтобы .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);
/* Обрезаем всё, что выходит за границы карты (важно для border-radius и поворотов) */
overflow: hidden;
/* Тень и рамка для объёма */
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 и использовать img { width: 100% }, изображение
будет включать padding в свой размер и всё равно касаться краёв → уголки "съедаются".
Поэтому в img мы используем calc(100% - 20px) и margin: 10px — это безопаснее.
*/
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); }
/*
Изображение внутри карты.
object-fit: cover — масштабирует фото с сохранением пропорций так, чтобы полностью
заполнить область 300×400px. При этом часть изображения может обрезаться по краям,
если его пропорции не совпадают с 3:4.
Чтобы изображение НЕ касалось краёв карты (и не перекрывало border-radius),
мы уменьшаем его на 20px по ширине и высоте и добавляем margin: 10px.
Это создаёт внутренний отступ, за счёт которого видны:
- скруглённые углы,
- синяя рамка,
- фон паспарту (#BFE2FF).
*/
.cart img {
width: calc(100% - 20px);
height: calc(100% - 20px);
margin: 10px;
/*
* Режим масштабирования изображения:
* • cover — заполняет всю область (может обрезать края, но без пустот) ← используется сейчас
* • contain — показывает всё изображение (без обрезки, но могут быть пустые полосы)
* • fill — растягивает без пустот и обрезки, но искажает пропорции (не рекомендуется)
*/
object-fit: cover;
display: block;
}
/*
Классы анимации: добавляются при клике.
bottom — для всех карт, кроме последней в цикле.
bottom_last — для последней карты (чтобы после неё сбросить порядок).
*/
.bottom {
z-index: 50;
animation: move ease-in-out 1s;
}
.bottom_last {
z-index: 30;
animation: move_last ease-in-out 1s;
}
/*
Анимация "ухода вправо и возврата на место".
В середине анимации карта сдвигается вправо на 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; /* самая нижняя позиция */
}
}
</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").removeClass("bottom_last");
count = 1;
}, 1000);
}
// Разблокируем клики через 1 секунду (длительность анимации)
setTimeout(function () {
click = true;
}, 1000);
});
});
</script>
[/html]
Поделиться52025-10-30 03:14:39
[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/865869.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_container {
margin: 20px 0;
position: relative;
}
.cart {
height: 400px;
width: 300px;
position: absolute;
top: 0;
left: calc(50% - 150px); /* ← оригинальное центрирование */
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: 10px;
z-index: 100;
background-color: #BFE2FF;
}
/* Повороты как в оригинале */
.cart:nth-child(1) { transform: rotate(-3deg); position: relative; }
.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); }
/* Защита уголков */
.img-wrap {
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 6px;
}
.cart img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Анимации — как в оригинале */
.bottom {
z-index: 50;
animation: move ease-in-out 1s forwards;
}
.bottom_last {
z-index: 30;
animation: move_last ease-in-out 1s forwards;
}
@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; }
}
/* Мобильная адаптация — центрирование сохраняется */
@media (max-width: 768px) {
.cart {
width: 260px;
height: calc(260px * 4 / 3);
left: calc(50% - 130px); /* 280 / 2 = 140 */
}
@keyframes move {
0% { left: calc(50% - 140px); z-index: 150; }
50% { left: calc(50% + 180px); }
100% { left: calc(50% - 140px); z-index: 50; }
}
@keyframes move_last {
0% { left: calc(50% - 140px); z-index: 150; }
50% { left: calc(50% + 180px); z-index: 50; }
100% { left: calc(50% - 140px); z-index: 30; }
}
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function () {
let count = 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++;
}
if (count === num + 1) {
setTimeout(function () {
$(".cart").removeClass("bottom bottom_last");
count = 1;
}, 1000);
}
setTimeout(function () {
click = true;
}, 1000);
});
});
</script>[/html]
Поделиться62025-10-31 15:30:49
Этот скрипт реализует интерактивные превью (тултипы) при наведении на ссылки на профили и темы на форуме. Работает только на десктопе (отключено на экранах ≤768px).
🔧 Основные компоненты
1. Инициализация и ограничения
● Скрипт не запускается на мобильных устройствах.
● Установлен максимальный уровень вложенности (MAX_NESTING_LEVEL = 10) для защиты от бесконечной рекурсии в цитатах/спойлерах.2. Извлечение ID из URL
Функция getJid(href) парсит URL вида:profile.php?id=123
viewtopic.php?id=456
и возвращает числовой ID.3. Система управления тултипами
● Поддерживается только один активный тултип одновременно.
● При попытке открыть новый, пока предыдущий закрывается — цель ставится в очередь (pendingTarget).
● Тултип автоматически скрывается через 2 секунды бездействия (scheduleHide).
● При наведении на тултип — таймер сбрасывается.
● При клике вне тултипа — он закрывается.4. Позиционирование
● Тултип позиционируется над целевым элементом.
● Для тем — смещается вправо для лучшей читаемости.
● Учитывается ширина окна: тултип не выходит за границы экрана.👤 Превью профиля
1. Запрашивается API: /api.php?method=users.get&user_id=...
2. Полученные данные:
● аватар (с fallback)
● ник
● группа (через group_id → CSS-класс usTitle-{id})
● пол, возраст, дата регистрации, количество сообщений, уважение, последняя активность3. Отображается в структурированном виде с разделителями (<hr>).
💬 Превью темы
1. Запрашиваются первое и последнее сообщения параллельно через:
/api.php?method=post.get&topic_id=...&sort_dir=asc
/api.php?method=post.get&topic_id=...&sort_dir=desc
● Отображаются вкладки: «Первое сообщение» / «Последнее сообщение».
● Сообщение проходит обработку через processMessage():
● Удаляются или заменяются опасные/неподдерживаемые BB-теги ([html], [table], [media]).
● Заменяются <details> на <div class="old-details">.
● Все вложенные блоки (цитаты, код, спойлеры) рекурсивно преобразуются в кастомные аккордеоны.
● Медиа-элементы масштабируются (max-width: 100%).
● Упоминания и цитаты заменяются на заглушки с последующей подгрузкой имён.🧩 Кастомные аккордеоны
● Реализованы без использования <details>, чтобы избежать конфликтов со стилями форума.
● Каждый блок (цитата, код, спойлер) оборачивается в:Код<div class="preview-accordion collapsed" data-type="quote">
<div class="preview-summary">📌 [Цитата]</div>
<div class="preview-content">...</div>
</div>● Раскрытие/сворачивание управляется только через CSS-классы (expanded / collapsed).
● Рекурсивная обработка вложенных блоков с ограничением глубины.
● При раскрытии — заголовок получает цветовую индикацию по типу (синий для цитат, зелёный для таблиц и т.д.).Таким образом, этот код — полноценная система интерактивных превью, сочетающая:
● API-интеграцию (запросы к /api.php),
● Безопасную обработку контента,
Рекурсивную трансформацию вложенных структур,
Адаптивное позиционирование,
Поддержку тёмной темы,
Оптимизацию UX (очередь, таймеры, плавные анимации).
Он не использует внешние библиотеки, кроме jQuery, и полностью автономен внутри замыкания.🎨 Стилизация (CSS)
● Тултипы имеют тень, скругления, указатель (треугольник).
● Для тем — градиентная полоса снизу.
● Аватары масштабируются с object-fit: contain.
● Группы пользователей стилизуются через CSS-классы (usTitle-1, usTitle-2 и т.д.).
● Поддержка тёмной темы через body.dark-theme.
● Все вложенные элементы в аккордеонах обнуляют отступы (margin: 0 !important), чтобы избежать визуального "наложения".⚙️ Применяемые алгоритмы и паттерны
● Управление состоянием тултипа:
State machine с флагами activeTooltip, isClosing, pendingTarget● Обработка вложенных блоков:
Рекурсивный обход DOM с ограничением глубины● Параллельная загрузка данных
$.when() для одновременного запроса первого и последнего сообщения● Ленивая подгрузка имён
Замена текста на заглушки → асинхронная подстановка имён по post_id● Позиционирование
Динамический расчёт координат с учётом границ окна● Изоляция стилей
Использование прямых потомков (>) и !important для предотвращения конфликтов● Таймеры и отмена
setTimeout + clearTimeout для управления временем жизни тултипаЭтот код — полноценная система интерактивных превью, сочетающая:
● API-интеграцию (запросы к /api.php),
● Безопасную обработку контента,
● Рекурсивную трансформацию вложенных структур,
● Адаптивное позиционирование,
● Поддержку тёмной темы,
● Оптимизацию UX (очередь, таймеры, плавные анимации).
Он не использует внешние библиотеки, кроме jQuery, и полностью автономен внутри замыкания.
Поделиться72025-11-05 16:05:44
афыафыаыфаа
00000000000
Быстрый ответ
Похожие темы
| фаыфыафыа | Новый форум | 2025-09-30 |
