// ============================================================================
// Stylescape | Image Compare Slider
// ============================================================================
// Interactive before/after image comparison slider with drag support.
// Supports data-ss-image-compare attributes for declarative configuration.
// ============================================================================
/**
* Configuration options for ImageCompareSlider
*/
export interface ImageCompareSliderOptions {
/** Initial slider position (0-100 percentage) */
initialPosition?: number;
/** CSS class for the slider handle */
sliderClass?: string;
/** CSS class for the overlay image */
overlayClass?: string;
/** Enable touch support */
touch?: boolean;
/** Show labels for before/after */
showLabels?: boolean;
/** Before image label text */
beforeLabel?: string;
/** After image label text */
afterLabel?: string;
/** Callback when slider position changes */
onChange?: (position: number) => void;
}
/**
* Interactive image comparison slider for before/after images.
* Supports mouse and touch interactions.
*
* @example JavaScript
* ```typescript
* const container = document.querySelector(".image__compare")
* const slider = new ImageCompareSlider(container, {
* initialPosition: 50,
* showLabels: true,
* onChange: (pos) => console.log(`Position: ${pos}%`)
* })
* ```
*
* @example HTML with data-ss
* ```html
*
*

*

*
*
* ```
*
* @example Static initialization for all sliders
* ```typescript
* ImageCompareSlider.initAll(".image__compare")
* ```
*/
export class ImageCompareSlider {
/** The container element for the slider */
private container: HTMLElement;
/** The overlay (before) image element */
private overlay: HTMLImageElement;
/** The base (after) image element */
private baseImage: HTMLImageElement;
/** The draggable slider handle element */
private slider: HTMLElement;
/** Whether the slider is currently being dragged */
private isActive: boolean = false;
/**
* Creates a new ImageCompareSlider instance.
*
* @param container - The container element holding both images and slider
*/
constructor(container: HTMLElement) {
this.container = container;
this.slider = container.querySelector(
".image__compare--slider",
) as HTMLElement;
this.overlay = container.querySelector(
".image__compare--overlay",
) as HTMLImageElement;
this.baseImage = container.querySelector(
"img.image__compare--image:not(.image__compare--overlay)",
) as HTMLImageElement;
if (
!this.container ||
!this.slider ||
!this.overlay ||
!this.baseImage
) {
console.warn(
`ImageCompareSlider skipped: required elements not found in`,
container,
);
return;
}
// Initialize brightness checks
this.checkAndInject(this.baseImage);
this.checkAndInject(this.overlay);
this.initEvents();
this.slideMove(this.container.offsetWidth / 2);
}
/**
* Checks image brightness and injects dark mode indicators if needed.
*
* @param image - The image element to check
*/
private checkAndInject(image: HTMLImageElement): void {
const side = image.dataset.darkSide;
if (!side) return;
const inject = () => {
this.isImageBright(image)
.then((isBright) => {
if (!isBright) return;
const el = document.createElement("div");
el.className = `dark--${side}`;
this.slider.appendChild(el);
// Ok rengini değiştir
const arrow = this.slider.querySelector(
`.arrow--${side}`,
) as HTMLElement;
if (arrow) {
arrow.style.borderColor = "var(--color_text_primary)";
}
})
.catch((err) => {
console.warn("Brightness check failed:", err);
});
};
if (image.complete && image.naturalWidth > 0) {
inject();
} else {
image.onload = () => {
if (image.naturalWidth > 0) inject();
};
}
}
/**
* Analyzes image brightness using canvas pixel sampling.
*
* @param image - The image element to analyze
* @returns Promise resolving to true if image is bright (avg > 160)
*/
private isImageBright(image: HTMLImageElement): Promise {
return new Promise((resolve) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return resolve(false);
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image, 0, 0);
const data = ctx.getImageData(
0,
0,
canvas.width,
canvas.height,
).data;
let r = 0,
g = 0,
b = 0,
count = 0;
const step = 4 * 20;
for (let i = 0; i < data.length; i += step) {
r += data[i];
g += data[i + 1];
b += data[i + 2];
count++;
}
const avg = (r + g + b) / (3 * count);
resolve(avg > 160);
});
}
/**
* Initializes mouse and touch event listeners for drag interaction.
*/
private initEvents(): void {
this.slider.addEventListener(
"mousedown",
() => (this.isActive = true),
);
window.addEventListener("mouseup", () => (this.isActive = false));
window.addEventListener("mousemove", (e) => {
if (this.isActive) this.slideMove(e.clientX);
});
this.slider.addEventListener(
"touchstart",
() => (this.isActive = true),
);
window.addEventListener("touchend", () => (this.isActive = false));
window.addEventListener("touchmove", (e) => {
if (this.isActive) this.slideMove(e.touches[0].clientX);
});
}
/**
* Moves the slider and adjusts overlay width based on position.
*
* @param x - The x-coordinate (client position) to move to
*/
private slideMove(x: number): void {
const bounds = this.container.getBoundingClientRect();
let pos = x - bounds.left;
pos = Math.max(0, Math.min(pos, bounds.width));
this.overlay.style.width = `${pos}px`;
this.slider.style.left = `${pos}px`;
}
/**
* Static factory method to initialize all image compare sliders on the page.
*
* @param selector - CSS selector for container elements (default: ".image__compare")
*/
public static initAll(selector: string = ".image__compare"): void {
const containers = document.querySelectorAll(selector);
containers.forEach((container) => {
new ImageCompareSlider(container);
});
}
}