Custom image comparison slider component

This commit is contained in:
Martin Prokoph
2025-01-01 14:08:25 +01:00
parent 00aaba5baa
commit e2cbde4126
12 changed files with 268 additions and 91 deletions

View File

@@ -0,0 +1,139 @@
---
interface Props {
firstTitle?: string
secondTitle?: string
sliderColor?: string
sliderShadowColor?: string
}
const { firstTitle, secondTitle, sliderColor = "white", sliderShadowColor = "black" } = Astro.props
import { Icon } from 'astro-icon/components'
---
<image-comparison>
<section class="my-6 overflow-hidden">
<div class="relative">
<div class="left-half relative">
<div class="w-full">
<slot name="first-image" />
</div>
<slot name="first-title" >
<p class="absolute top-1/2 left-1 translate-x-1/2 -translate-y-1/2 text-2xl font-bold text-center bg-[var(--background)] px-2 py-1 rounded-md">{firstTitle}</p>
</slot>
</div>
<div class="right-half absolute left-0 top-0 right-0 h-full">
<div class="clip-area">
<div class="w-full">
<slot name="second-image" />
</div>
<slot name="second-title">
<p class="absolute top-1/2 right-1 -translate-x-1/2 -translate-y-1/2 text-2xl font-bold text-center bg-[var(--background)] px-2 py-1 rounded-md">{secondTitle}</p>
</slot>
</div>
<div class="handle-container px-4 top-0 absolute z-10 h-full">
<div class="divider w-0.5 h-full block" style=`background-color: ${sliderColor}; box-shadow: 0 0 8px ${sliderShadowColor};`/>
<div class="handle top-[50%] absolute pointer-events-none ml-[1px] w-8 h-8" id="handle" style=`color: ${sliderColor};`>
<slot name="handle-icon">
<Icon name="double-arrow-slider" width="32" height="32" />
</slot>
</div>
</div>
</div>
</div>
</image-comparison>
<script>
// Define the behaviour for our new type of HTML element.
class ImageComparison extends HTMLElement {
connectedCallback() {
const buttons = this.querySelectorAll('.right-half');
buttons.forEach((button) => {
console.log(button);
dragElement(button);
});
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
function dragElement(elmnt) {
elmnt.onmousedown = dragMouseDown;
elmnt.addEventListener("touchstart", () => {
elmnt.addEventListener("touchmove", elementTouch, false);
}, false);
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
document.onmousemove = elementDrag;
document.onmouseup = closeDragElement;
setProgress(e.clientX);
}
function setProgress(clientX) {
let rect = elmnt.getBoundingClientRect();
let progress = (clientX - rect.left) / elmnt.offsetWidth * 100;
progress = clamp(progress, 0, 100);
elmnt.style.setProperty('--progress', `${progress}%`);
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
elmnt.style.setProperty('--transition-time', `0ms`);
elmnt.style.setProperty('cursor', 'col-resize');
setProgress(e.clientX);
}
function elementTouch(e) {
e = e || window.event;
e.preventDefault();
elmnt.addEventListener("touchend", closeDragElement, false);
elmnt.style.setProperty('--transition-time', `0ms`);
setProgress(e.touches[0].clientX);
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
elmnt.removeEventListener("touchend", closeDragElement, false);
elmnt.removeEventListener("touchmove", elementTouch, false);
elmnt.style.setProperty('--transition-time', '0.25s');
elmnt.style.setProperty('cursor', 'initial');
}
}
}
}
customElements.define('image-comparison', ImageComparison);
</script>
<style lang="scss" is:global>
image-comparison {
img {
max-width: 100%;
width: 100%;
}
.right-half {
--progress: 50%;
--transition-time: 0.25s;
.clip-area {
clip-path: inset(0 0 0 var(--progress));
transition: clip-path var(--transition-time);
}
.handle-container {
left: calc(var(--progress) - 1rem);
transition: left var(--transition-time),bottom var(--transition-time);
.handle {
box-sizing: border-box;
transform: translate(-50%, -50%);
color: var(--action-color);
}
}
}
}
</style>

View File

@@ -1,71 +0,0 @@
---
interface Props {
imgBefore: string
imgAfter: string
captionBefore?: string
captionAfter?: string
}
const { imgBefore, imgAfter, captionBefore, captionAfter } = Astro.props
---
<img-comparison-slider class="slider-with-animated-handle">
<figure slot="first" class="before">
<img width="100%" src={imgBefore}>
{captionBefore ? <figcaption>{captionBefore}</figcaption> : ""}
</figure>
<figure slot="second" class="after">
<img width="100%" src={imgAfter}>
{captionAfter ? <figcaption>{captionAfter}</figcaption> : ""}
</figure>
</img-comparison-slider>
<style lang="scss" is:global>
#handle {
transition: transform 0.2s;
}
#handle:hover #handle {
transform: scale(1.2);
}
img-comparison-slider:focus, img-comparison-slider:focus-visible {
outline: unset;
box-shadow: unset;
}
.before,
.after {
margin: 0;
}
.before figcaption,
.after figcaption {
background: #fff;
border: 1px solid #c0c0c0;
border-radius: 12px;
color: #2e3452;
opacity: 0.8;
padding: 12px;
position: absolute;
top: 50%;
transform: translateY(-50%);
line-height: 100%;
}
.before figcaption {
left: 12px;
}
.after figcaption {
right: 12px;
}
</style>
<script>
import "img-comparison-slider/dist/index"
import "img-comparison-slider/dist/styles.css"
</script>
<!-- <script>
import { HTMLImgComparisonSliderElement } from "img-comparison-slider";
customElements.define('img-comparison-slider', HTMLImgComparisonSliderElement);
</script> -->