Круглые видео-отзывы в карусели с переключением звука.
<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.