概要
Swiper.jsでは標準で実装されているZoom機能がslick.jsにはない。商品画像のスライド等でよく見るので、実装してみた。
条件
- jquery: ^3.7.1
- slick-carousel: "^1.8.1
コード
ピンチイン・アウトできるスライドに.is-zoomable
を付与しておく。
<div class="js-slider">
<div class="is-zoomable">your content</div>
<div class="is-zoomable">your content</div>
<div class="is-zoomable">your content</div>
</div>
cosnt $slider = $('.js-slider');
const option = { /** オプション */ }
// Initialize Slick slider with options
$slider.slick(option);
$slider.on('touchstart', '.is-zoomable', function (e, slick) {
onTouchStart(e, slick);
});
$slider.on('touchmove', '.is-zoomable', function (e) {
onTouchMove(e);
});
$slider.on('touchend', '.is-zoomable', function (e, slick) {
onTouchEnd(e, slick);
});
諸々の初期値
/** @type {number} The current zoom scale */
let scale = 1;
/** @type {number} The maximum / minimum zoom scale */
let maxScale = 2;
let minScale = 1;
/** @type {number} The starting distance between two touch points for pinch zoom */
let startDistance = 0;
/** @type {number} The current distance between two touch points */
let currentDistance = 0;
/** @type {number} The X/Y coordinates of the center point of pinch zoom (relative to image) */
let originX = 0;
let originY = 0;
/** @type {number} The current horizontal/vertical translation of the image */
let posX = 0;
let posY = 0;
/** @type {number} The previous touch X/Y coordinates for dragging */
let prevX = 0;
let prevY = 0;
/** @type {boolean} Flag to indicate if pinch zoom / drag is in progress */
let isZooming = false;
let isDragging = false;
タッチポイント(指)の間の直線距離の計算
dx
で二本の指の水平差分(距離の大きさと向き)、dy
で二本の指の垂直差分を計算する。
/**
* Calculates the distance between two touch points on the screen.
*
* @param {TouchList} touches - The list of touch points from a touch event
* @returns {number} The distance between the two touch points in pixels
*/
function calculateDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.hypot(dx * dx + dy * dy);
}
画像をパン(平行移動)するときに、あらかじめ計算した「許容できる動きの範囲」を超えないように位置を抑え込む。
/**
* Clamps a value within a specified range.
*
* @param {number} value - The value to be clamped
* @param {number} maxDistance - The maximum allowed distance (the range)
* @returns {number} The clamped value
*/
function limitPosition(value, maxDistance) {
return Math.min(Math.max(value, -maxDistance), maxDistance);
}
/**
* Applies the zoom and pan transformations to the image element.
* @param {HTMLImageElement} img - The image element to apply the transformation to
*/
function updateImageTransform(img) {
// Calculate the maximum allowable translation based on the image size and scale.
const maxOffsetX = (img.clientWidth * (scale - 1)) / 2;
const maxOffsetY = (img.clientHeight * (scale - 1)) / 2;
// Clamp the translation values to ensure the image stays within bounds
posX = limitPosition(posX, maxOffsetX);
posY = limitPosition(posY, maxOffsetY);
// When the zoom is reset (scale=1), add an animation to return the image to the center (0,0)
if (scale === 1) {
img.style.transition = 'transform 0.3s ease';
posX = 0;
posY = 0;
} else {
img.style.transition = 'none';
}
// Apply the transform (translation and scale) to the image
img.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;
img.style.transformOrigin = `${originX}px ${originY}px`;
}
Touchstart
ズーム/パン操作のためにスワイプ機能を無効化、e.touches.length
でピンチ or ドラッグを判定し、初期値・中心点を記録する。
ピンチの中心を、ビューポート座標から画像の矩形 (getBoundingClientRect
) を引くことで、画像内部のオフセットとして得る。
touches.length === 2
でピンチズーム開始処理、touches.length === 1 && scale > 1
でズーム中のドラッグパン開始。
/**
* Handles the touchstart event.
*
* - Initiates pinch zoom with two fingers.
* - Initiates dragging with one finger if zoom is applied.
* @param {TouchEvent} e - The touch event
*/
function onTouchStart(e, slick) {
const img = e.target;
if (e.touches.length === 2) {
startDistance = calculateDistance(e.touches);
// Start pinch zoom
isZooming = true;
// Calculate the center point of the pinch
const rect = img.getBoundingClientRect();
originX = ((e.touches[0].clientX + e.touches[1].clientX) / 2) - rect.left;
originY = ((e.touches[0].clientY + e.touches[1].clientY) / 2) - rect.top;
// Set the initial position of the pinch
prevX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
prevY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
// Disable swipe behavior during pinch zoom
$(slick.$slider).slick('slickSetOption', 'swipe', false, true);
} else if (e.touches.length === 1) {
// Start dragging when zoom is applied
isDragging = true;
// Calculate the initial position of the touch
prevX = e.touches[0].clientX;
prevY = e.touches[0].clientY;
}
}
Touchmove
「ズーム中なら倍率更新」「ドラッグ中なら平行移動更新」を行い、毎回updateImageTransform
でscale
/posX
/posY
に応じて位置制限(クランプ)とtransform
を再適用する。
ピンチズーム
calculateDistance で距離を再取得 → 新旧距離の比率でscale
に掛ける。距離計測→倍率更新→再計算のループにより連続的にズームする。
ドラッグパン
1本指の移動量 (dx
, dy
) を posX
, posY
に累積。前回のタッチ座標 (prevX
, prevY
) を更新して差分を常に算出する。
/**
* Handles the touchmove event.
*
* - Updates zoom scale during pinch zoom.
* - Updates position during drag movement.
* @param {TouchEvent} e - The touch event
*/
function onTouchMove(e) {
const img = e.target;
if (isZooming && e.touches.length === 2) {
// Handle pinch zoom
currentDistance = calculateDistance(e.touches);
scale *= currentDistance / startDistance;
startDistance = currentDistance;
// Clamp the zoom scale between [minScale, maxScale]
scale = Math.min(Math.max(minScale, scale), maxScale);
updateImageTransform(img);
} else if (isDragging && e.touches.length === 1 && scale > 1) {
// Handle dragging (pan)
const dx = e.touches[0].clientX - prevX;
const dy = e.touches[0].clientY - prevY;
// Update the position of the image
posX += dx;
posY += dy;
// Update the previous touch position
prevX = e.touches[0].clientX;
prevY = e.touches[0].clientY;
updateImageTransform(img);
}
}
Touchend
scale
が「ほぼ元サイズ」だったら厳密に 1 に戻し、updateImageTransform
内で中央へのアニメーション(transition)が発動するようにする。
ズーム/パン操作のために無効化していたスワイプ機能を、タッチ終了後に再有効化する。
/**
* Handles the touchend and touchcancel events.
*
* - Finalizes the zoom and drag operations.
* - Resets zoom scale to 1 if the zoom is minimal.
* @param {TouchEvent} e - The touch event
*/
function onTouchEnd(e, slick) {
const img = e.target;
isZooming = false;
isDragging = false;
// If the zoom scale is near 1, reset it to 1
scale <= 1.05 && (scale = 1);
updateImageTransform(img);
// Enable swipe behavior again after zooming and panning
$(slick.$slider).slick('slickSetOption', 'swipe', true, true);
}