Что делает скрипт

Круглые видео-отзывы в карусели с переключением звука.

Как подключить

  1. Вставьте код в блок T123 «HTML-код» на страницу (или в «Настройки страницы» → «HTML-код для вставки внутрь head»).
  2. Замените ссылки на видео в разметке.
  3. Опубликуйте страницу: код выполняется только на опубликованной странице.

Код

Код для вставки
<script src="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick.min.js"></script>

<style>
    body.grab * {
        cursor: grab !important;
        user-select: none;
    }

    .reviews-block {
        width: 100%;
        overflow: hidden;
    }
    .reviews-container {
        width: 100%;
    }
    .reviews-container .slick-track {
        display: flex;
    }

    .review {
        position: relative;

        --size: 330px;
        width: calc(var(--size) + 20px); height: calc(var(--size) + 20px);

        margin: 0 -55px;
        border-radius: 1000px;
        clip-path: border-box;

        display: flex;
        align-items: center; justify-content: center;

        transition: transform 0.3s, margin 0.3s;

        cursor: pointer;
    }
    .review:is(.slick-current, .active) {
        z-index: 2;
    }
    .review .progress {
        width: calc(var(--size) + 20px); height: calc(var(--size) + 20px);
        position: absolute;
        border-radius: 1000px;
        transform: rotate(-90deg);
        touch-action: none;
        display: none;
    }
    .review.active .progress {
        display: block;
        cursor: grab;
    }
    .review .progress :is(.circle, .dot) {
        --stroke: 7.6px;

        --radius: calc((var(--size) - var(--stroke)) / 2);
        --length: calc(6.2832 * var(--radius));
        --center: calc(var(--size) / 2 + 10px);

        stroke-dasharray: var(--length);

        r: var(--radius);
        cx: var(--center);
        cy: var(--center);

        /* Чтобы при клике на .progress в e.target был .progress, а не circle */
        pointer-events: none;
    }
    .review .progress .circle {
        stroke-width: var(--stroke);
        stroke-dashoffset: calc(var(--length) * (1 - var(--progress)));
    }
    .review .progress .dot {
        stroke-width: 20px;
        stroke-dashoffset: calc(var(--length) - 0.01px);
        transform-origin: center;
        transform: rotate(calc(6.283rad * var(--progress)));
        opacity: 0;
        transition: opacity 0.3s;
    }
    :is(body.grab .review.active, .review.active:hover) .progress .dot {
        opacity: 1;
    }
    .review .background {
        position: absolute;
        width: 300px; height: 300px;
        border-radius: 1000px;
        border: 0.55px solid rgba(255, 255, 255, 0.4);
        backdrop-filter: blur(4.24px);
        pointer-events: none;
    }
    .review .video-container {
        position: absolute;
        width: 271.7px; height: 271.7px;
        border-radius: 1000px;
        clip-path: border-box;
        overflow: hidden;
    }
    .review video {
        width: 100%; height: 100%;
        object-fit: cover;
    }
    .review .sound {
        position: absolute;
        width: 29px; height: 29px;
        background-repeat: no-repeat;
        background-position: center;
        background-size: cover;

        bottom: 49px;

        transform: scaleX(-1);
        background-image: url(https://static.tildacdn.com/tild3766-6339-4463-a434-653166313263/sound_off.svg);
    }
    .review:not(.muted) .sound {
        background-image: url(https://static.tildacdn.com/tild3664-3731-4837-b237-623433393037/sound_on.svg);
    }
    .review:not(.active) .sound {
        pointer-events: none;
    }

    @media (min-width: 391px) {
        .review {
            transform: translateX(-30px) scale(0.78);
        }
        .review.slick-current {
            transform: none;
        }
        .review.slick-current ~ .review {
            transform: translateX(30px) scale(0.78);
        }
    }
    @media (max-width: 390px) {
        .reviews-block {
            display: flex;
            justify-content: center;
        }
        .reviews-container {
            width: 320px;
        }
        .reviews-container .slick-track {
            height: 210px;
            align-items: center;
        }

        .review {
            transform: scale(0.5);
            margin: 0 -100px;
        }
        .review.active {
            transform: scale(0.6);
        }
        .review .progress .dot {
            opacity: 0 !important;
        }
    }
</style>

<div class="reviews-block">
    <div class="reviews-container">

    </div>
</div>

<script>
const VIDEOS = [
    {
        url: "https://cdnv.boomstream.com/balancer/xbu9fQct-utKZWvcV.mp4"
    },
    {
        url: "https://cdnv.boomstream.com/balancer/RNyqtTWV-utKZWvcV.mp4"
    },
    {
        url: "https://cdnv.boomstream.com/balancer/lYh2Jplb-utKZWvcV.mp4"
    },
    {
        url: "https://cdnv.boomstream.com/balancer/lumHa8ip-utKZWvcV.mp4"
    },
    {
        url: "https://cdnv.boomstream.com/balancer/AAE8EYCJ-utKZWvcV.mp4"
    },
//    {
//        url: "https://cdnv.boomstream.com/balancer/fLjBx7pI-utKZWvcV.mp4"
//    },
//    {
//        url: "https://cdnv.boomstream.com/balancer/DEyxq6Zr-utKZWvcV.mp4"
//    },
];

$(() => {
    main();
})


const MOBILE_WIDTH = 390;

const main = async () => {
    const $container = $(".reviews-container");

    for(const video of VIDEOS) {
        const $review = $(`<div class="review muted">
            <svg class="progress" version="1.1" xmlns="http://www.w3.org/2000/svg">
                <circle class="circle" stroke="#001EA54D" stroke-linecap="round" fill="transparent"></circle>
                <circle class="dot" stroke="#001EA54D" stroke-linecap="round" fill="transparent"></circle>
            </svg>
            <div class="background" draggable="true"></div>
            <div class="video-container">
                <video src="${video.url}#t=0.001" preload="metadata" muted playsinline></video>
            </div>
            <div class="sound"></div>
        </div>`).appendTo($container);
    }

    let mode;
    function renderSlick() {
        let newMode;
        if(window.innerWidth <= MOBILE_WIDTH)
            newMode = "390";
        else
            newMode = "default";

        if(newMode === mode) return;
        if(mode)
            $container.slick("unslick");
        mode = newMode;

        if(mode === "390") {
            $container.slick({
                dots: false,
                arrows: false,
                infinite: false,
                speed: 300,
                slidesToShow: 2,
                slidesToScroll: 2,
                initialSlide: 0,
                centerMode: false,
                variableWidth: false,
                draggable: true,
                focusOnSelect: false,
            })
        } else {
            $container.slick({
                dots: false,
                arrows: false,
                infinite: false,
                speed: 300,
                slidesToShow: 1,
                initialSlide: 2,
                centerMode: true,
                variableWidth: true,
                draggable: false,
                swipe: true,
                focusOnSelect: true,

                responsive: [
                    {
                      breakpoint: 1300,
                      settings: {
                        initialSlide: 1,
                      }
                    },
                    {
                      breakpoint: 800,
                      settings: {
                        initialSlide: 0,
                      }
                    },
                ]
            })
        }
    }
    renderSlick();
    $(window).on("resize", () => renderSlick());


    // $(".reviews-container .review.slick-current").addClass("active");

    const reviews = document.querySelectorAll(".review");

    let muted = false;

    for(const review of reviews) {
        const $review = $(review);
        const video = $review.find("video")[0];

        $review
            .mouseenter(() => {
                if(!$review.hasClass("active") && window.innerWidth > MOBILE_WIDTH)
                    playVideo(video);
            })
            .mouseleave(() => {
                if(!$review.hasClass("active")) {
                    video.pause();
                    video.currentTime = 0;
                }
            });

        $review.find(".video-container")
            .click(() => {
                if(video.paused || !$review.hasClass("active"))
                    playReview(review);

                else if($review.hasClass("active"))
                    video.pause();

            })

        $sound = $review.find(".sound");
        $sound.click((e) => {
            if(!$review.hasClass("active")) return;

            e.stopPropagation();

            video.muted = muted = !muted;
            $review.toggleClass("muted", muted);
        });

        const progress = $review.find(".progress")[0];
        progress.style.setProperty("--progress", 0);
        let intervalId;
        $(video).on("play", () => {
            console.log("play");
            if(intervalId !== undefined) clearInterval(intervalId);
            intervalId = setInterval(() => {
                console.log(video.currentTime, video.duration);
                progress.style.setProperty("--progress", video.currentTime / video.duration)
            }, 50);
        });
        $(video).on("pause", () => {
            console.log("pause");
            clearInterval(intervalId);
        })
    }

    (() => {
        let review;
        let progressElement;
        let video;
        let wasPaused;

        const _2PI = Math.PI*2;

        let throttleTime = Date.now();
        function onMove(e) {
            e.preventDefault();
            e.stopPropagation();
            e = e.originalEvent;

            const rect = progressElement.getBoundingClientRect();
            const x = e.clientX - rect.left - rect.width/2;
            const y = e.clientY - rect.top - rect.height/2;

            const atan2 = Math.atan2(x, -y);
            let progress = (atan2 + _2PI) % _2PI / _2PI;
            progress = (progress * 1000) / 1000;

            progressElement.style.setProperty("--progress", progress);

            if ((throttleTime + 200 - Date.now()) >= 0)
                return;
            throttleTime = Date.now();

            video.currentTime = (video.duration * progress * 100) / 100;
        }

        $(window).on("mousedown", (e) => {
            let target = e.target;

            if(!target.classList.contains("progress")) return;

            let $review = $(target).closest(".review");
            if(!$review.hasClass("active")) return;

            e.preventDefault();
            e.stopPropagation();

            review = $review[0];
            video = $review.find("video")[0];
            progressElement = e.target;
            wasPaused = video.paused;

            video.pause();

            $(window).off("mousemove", onMove);
            $(window).on("mousemove", onMove);

            $(document.body).addClass("grab");

            $(window).one("mouseup", () => {
                $(document.body).removeClass("grab");
                $(window).off("mousemove", onMove);
                if(!wasPaused) playVideo(video);
            });
        });
    })()

    function playReview(review) {
        console.log("playReview");
        const video = $(review).find("video")[0];

        if(!$(review).hasClass("active")) {
            for(const otherReview of $container[0].querySelectorAll(".review.active")) {
                const $otherVideo = $(otherReview).find("video");

                $(otherReview).removeClass("active");
                $(otherReview).addClass("muted");

                $otherVideo[0].muted = true;
                // $otherVideo[0].loop = true;

                // if(window.innerWidth <= MOBILE_WIDTH) {
                    $otherVideo[0].currentTime = 0;
                    $(otherReview).find(".progress")[0].style.setProperty("--progress", 0);
                // }

                $otherVideo[0].pause();
                // $otherVideo[0].play();
            }

            $(review).addClass("active");
            video.currentTime = 0;
        }

        video.muted = muted;
        // video.loop = false;
        $(review).toggleClass("muted", muted);
        playVideo(video);
    }

    async function playVideo(video) {
        try {
            await video.play();
        } catch(e) {
            if(e.name == "AbortError") {
                video.src = video.src;
                video.play;
            } else {
                throw e;
            }
        }
    }

    // $(window).on("mousemove", throttle((e) => {
    //     document.body.style.setProperty("--mouse-x", e.originalEvent.clientX);
    //     document.body.style.setProperty("--mouse-y", e.originalEvent.clientY);
    // }, 100));
}

// function throttle(func, timeFrame) {
//   var lastTime = 0;
//   return function () {
//       var now = Date.now();
//       if (now - lastTime >= timeFrame) {
//           func.apply(this, arguments);
//           lastTime = now;
//       }
//   };
// }
</script>

Подключается библиотека slick-carousel и нужен jQuery.

тут нужно написать стили и js/css для того, чтобы отображались файлы скриптов