Тестовый

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

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


Вы здесь » Тестовый » Новый форум » фаыфыафыа


фаыфыафыа

Сообщений 1 страница 7 из 7

Опрос

фыафыафыа
ыаыфаа

0% - 0
фаыаа

0% - 0
Голосов: 0; Проголосовали: 0

1

афыафыаыфаа

0

2

Туман на фотографии, который убирается курсором мыши на компе, и пальцем на телефоне

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

0

3

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

0

4

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

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

0

5

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

0

6

Технические подробности

Этот скрипт реализует интерактивные превью (тултипы) при наведении на ссылки на профили и темы на форуме. Работает только на десктопе (отключено на экранах ≤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, и полностью автономен внутри замыкания.

0

7

Merlin написал(а):

афыафыаыфаа

00000000000

0

Быстрый ответ

Напишите ваше сообщение и нажмите «Отправить»


Похожие темы


Вы здесь » Тестовый » Новый форум » фаыфыафыа