document.addEventListener('DOMContentLoaded', function () { const slider = document.querySelector('.pickup-slider'); if (!slider) { return; } const track = slider.querySelector( '.pickup-slider__track' ); const dotsArea = slider.querySelector( '.pickup-slider__dots' ); if (!track || !dotsArea) { return; } const originalItems = Array.from( track.querySelectorAll('.pickup-slider__item') ); if (originalItems.length === 0) { return; } const autoPlayDelay = 3000; /* * 左右に複製する画像数 */ const loopCloneCount = Math.min( 2, originalItems.length ); let autoPlayTimer = null; let scrollEndTimer = null; let animationFrameId = null; let isPointerDown = false; let activePointerId = null; let dragStartX = 0; let dragStartY = 0; let dragStartScrollLeft = 0; let dragStartTrackIndex = 0; let dragAxis = null; let didDrag = false; let lastPointerX = 0; let lastPointerTime = 0; let dragVelocity = 0; let suppressClick = false; /* * 複製画像内のリンクなどを * キーボード操作の対象外にする */ function disableCloneInteraction(item) { item.setAttribute('aria-hidden', 'true'); const interactiveElements = item.querySelectorAll( 'a, button, input, select, textarea' ); interactiveElements.forEach(function (element) { element.setAttribute('tabindex', '-1'); }); } /* * 左右に2枚ずつ複製して無限ループを作る * * 切り替え地点でも中央画像の左右に * 必ず画像が表示される状態を保つ */ if (originalItems.length >= 2) { const leftCloneSources = originalItems.slice( -loopCloneCount ); const rightCloneSources = originalItems.slice( 0, loopCloneCount ); leftCloneSources.forEach(function (sourceItem) { const clone = sourceItem.cloneNode(true); clone.classList.add('is-clone'); disableCloneInteraction(clone); track.insertBefore( clone, originalItems[0] ); }); rightCloneSources.forEach(function (sourceItem) { const clone = sourceItem.cloneNode(true); clone.classList.add('is-clone'); disableCloneInteraction(clone); track.appendChild(clone); }); } /* * 複製画像を含む全画像 */ const items = Array.from( track.querySelectorAll('.pickup-slider__item') ); /* * 丸印を生成 */ dotsArea.innerHTML = ''; originalItems.forEach(function (_, index) { const dot = document.createElement('button'); dot.type = 'button'; dot.className = 'pickup-slider__dot'; dot.setAttribute( 'aria-label', `${index + 1}枚目の画像を表示` ); dot.addEventListener('click', function () { stopAutoPlay(); const trackIndex = originalItems.length >= 2 ? index + loopCloneCount : index; scrollToTrackItem(trackIndex, true); }); dotsArea.appendChild(dot); }); const dots = Array.from( dotsArea.querySelectorAll('.pickup-slider__dot') ); /* * 指定画像を中央に表示する位置を取得 */ function getItemPosition(index) { const item = items[index]; if (!item) { return 0; } return ( item.offsetLeft - (track.clientWidth - item.offsetWidth) / 2 ); } /* * 指定画像まで移動 */ function scrollToTrackItem(index, smooth) { const item = items[index]; if (!item) { return; } track.scrollTo({ left: getItemPosition(index), behavior: smooth ? 'smooth' : 'auto' }); } /* * 動きを見せずに指定画像へ移動 */ function jumpToTrackItem(index) { track.style.scrollBehavior = 'auto'; track.scrollLeft = getItemPosition(index); updateSlider(); requestAnimationFrame(function () { track.style.scrollBehavior = ''; }); } /* * 中央に一番近い画像番号を取得 */ function getNearestTrackIndex() { const trackCenter = track.scrollLeft + track.clientWidth / 2; let nearestIndex = 0; let nearestDistance = Infinity; items.forEach(function (item, index) { const itemCenter = item.offsetLeft + item.offsetWidth / 2; const distance = Math.abs( trackCenter - itemCenter ); if (distance < nearestDistance) { nearestDistance = distance; nearestIndex = index; } }); return nearestIndex; } /* * 複製画像の番号を * 本来の画像番号へ変換 */ function getOriginalIndex(trackIndex) { if (originalItems.length === 1) { return 0; } return ( ( trackIndex - loopCloneCount + originalItems.length ) % originalItems.length ); } /* * 中央との距離に応じて * 各画像の高さを変更 */ function updateSlider() { const trackCenter = track.scrollLeft + track.clientWidth / 2; const scale = Math.min( 1, track.clientWidth / 600 ); const minHeight = 470 * scale; const maxHeight = 540 * scale; let itemStep = items[0].offsetWidth; if (items.length >= 2) { itemStep = items[1].offsetLeft - items[0].offsetLeft; } let nearestTrackIndex = 0; let nearestDistance = Infinity; items.forEach(function (item, index) { const itemCenter = item.offsetLeft + item.offsetWidth / 2; const distance = Math.abs( trackCenter - itemCenter ); const progress = Math.max( 0, Math.min( 1, 1 - distance / itemStep ) ); const currentHeight = minHeight + (maxHeight - minHeight) * progress; item.style.height = `${currentHeight}px`; if (distance < nearestDistance) { nearestDistance = distance; nearestTrackIndex = index; } }); const activeOriginalIndex = getOriginalIndex(nearestTrackIndex); dots.forEach(function (dot, index) { const isActive = index === activeOriginalIndex; dot.classList.toggle( 'is-active', isActive ); dot.setAttribute( 'aria-current', isActive ? 'true' : 'false' ); }); animationFrameId = null; } /* * 複製画像から * 本来の画像位置へ瞬時に戻す */ function normalizeLoopPosition() { if (originalItems.length < 2) { return; } const nearestIndex = getNearestTrackIndex(); /* * 左側の複製へ移動した場合 */ if (nearestIndex < loopCloneCount) { jumpToTrackItem( nearestIndex + originalItems.length ); return; } /* * 右側の複製へ移動した場合 */ if ( nearestIndex >= loopCloneCount + originalItems.length ) { jumpToTrackItem( nearestIndex - originalItems.length ); } } /* * 自動再生を停止 */ function stopAutoPlay() { window.clearTimeout(autoPlayTimer); autoPlayTimer = null; } /* * 3秒後に次の画像へ移動 */ function startAutoPlay() { stopAutoPlay(); if ( originalItems.length < 2 || isPointerDown ) { return; } autoPlayTimer = window.setTimeout( function () { const currentIndex = getNearestTrackIndex(); scrollToTrackItem( currentIndex + 1, true ); }, autoPlayDelay ); } /* * スクロール終了後の処理を予約 */ function scheduleScrollEnd() { window.clearTimeout(scrollEndTimer); scrollEndTimer = window.setTimeout( function () { if (isPointerDown) { return; } normalizeLoopPosition(); updateSlider(); startAutoPlay(); }, 120 ); } /* * スクロール中に高さを更新 */ track.addEventListener( 'scroll', function () { stopAutoPlay(); if (animationFrameId === null) { animationFrameId = requestAnimationFrame(function () { updateSlider(); }); } if (!isPointerDown) { scheduleScrollEnd(); } }, { passive: true } ); /* * 横スライド開始 */ track.addEventListener( 'pointerdown', function (event) { if (event.pointerType !== 'mouse') { return; } if (event.button !== 0) { return; } normalizeLoopPosition(); stopAutoPlay(); window.clearTimeout(scrollEndTimer); isPointerDown = true; activePointerId = event.pointerId; dragStartX = event.clientX; dragStartY = event.clientY; dragStartScrollLeft = track.scrollLeft; dragStartTrackIndex = getNearestTrackIndex(); lastPointerX = event.clientX; lastPointerTime = event.timeStamp; dragVelocity = 0; dragAxis = null; didDrag = false; suppressClick = false; track.classList.add('is-dragging'); try { track.setPointerCapture( event.pointerId ); } catch (error) { /* * 取得できない端末では何もしない */ } } ); /* * 横スライド中 */ track.addEventListener( 'pointermove', function (event) { if ( !isPointerDown || event.pointerId !== activePointerId ) { return; } const moveX = event.clientX - dragStartX; const moveY = event.clientY - dragStartY; /* * 縦移動か横移動かを判定 */ if (dragAxis === null) { if ( Math.abs(moveX) < 6 && Math.abs(moveY) < 6 ) { return; } dragAxis = Math.abs(moveX) > Math.abs(moveY) ? 'horizontal' : 'vertical'; } if (dragAxis !== 'horizontal') { return; } if (event.cancelable) { event.preventDefault(); } didDrag = true; /* * 指を動かした速さを取得 */ const elapsedTime = event.timeStamp - lastPointerTime; if (elapsedTime > 0) { dragVelocity = (lastPointerX - event.clientX) / elapsedTime; } lastPointerX = event.clientX; lastPointerTime = event.timeStamp; /* * 操作開始時の前後1枚までに * 移動範囲を制限 */ const previousIndex = Math.max( 0, dragStartTrackIndex - 1 ); const nextIndex = Math.min( items.length - 1, dragStartTrackIndex + 1 ); const minimumPosition = getItemPosition(previousIndex); const maximumPosition = getItemPosition(nextIndex); const desiredPosition = dragStartScrollLeft - moveX; track.scrollLeft = Math.max( minimumPosition, Math.min( maximumPosition, desiredPosition ) ); }, { passive: false } ); /* * 横スライド終了 */ function finishPointerDrag( event, cancelled ) { if ( !isPointerDown || event.pointerId !== activePointerId ) { return; } const movedDistance = track.scrollLeft - dragStartScrollLeft; const scale = Math.min( 1, track.clientWidth / 600 ); const distanceThreshold = Math.max( 18, 36 * scale ); const velocityThreshold = 0.35; let targetIndex = dragStartTrackIndex; if ( !cancelled && dragAxis === 'horizontal' ) { if ( movedDistance > distanceThreshold || dragVelocity > velocityThreshold ) { targetIndex = dragStartTrackIndex + 1; } else if ( movedDistance < -distanceThreshold || dragVelocity < -velocityThreshold ) { targetIndex = dragStartTrackIndex - 1; } } targetIndex = Math.max( 0, Math.min( items.length - 1, targetIndex ) ); const shouldSuppressClick = didDrag; isPointerDown = false; activePointerId = null; track.classList.remove('is-dragging'); try { if ( track.hasPointerCapture( event.pointerId ) ) { track.releasePointerCapture( event.pointerId ); } } catch (error) { /* * 解放できない端末では何もしない */ } /* * 必ず開始位置から * 前後1枚以内に止める */ scrollToTrackItem(targetIndex, true); scheduleScrollEnd(); if (shouldSuppressClick) { suppressClick = true; window.setTimeout(function () { suppressClick = false; }, 350); } dragAxis = null; didDrag = false; dragVelocity = 0; } track.addEventListener( 'pointerup', function (event) { finishPointerDrag(event, false); } ); track.addEventListener( 'pointercancel', function (event) { finishPointerDrag(event, true); } ); /* * スライド後の誤クリックを防ぐ */ track.addEventListener( 'click', function (event) { if (!suppressClick) { return; } event.preventDefault(); event.stopPropagation(); suppressClick = false; }, true ); /* * パソコンで画像だけが * ドラッグされるのを防ぐ */ track.addEventListener( 'dragstart', function (event) { event.preventDefault(); } ); /* * 画面幅変更時に * 位置と高さを再計算 */ window.addEventListener( 'resize', function () { stopAutoPlay(); if (animationFrameId !== null) { cancelAnimationFrame( animationFrameId ); animationFrameId = null; } requestAnimationFrame(function () { const nearestIndex = getNearestTrackIndex(); jumpToTrackItem(nearestIndex); startAutoPlay(); }); } ); /* * 最初は2枚目を中央に表示 */ const initialOriginalIndex = originalItems.length >= 2 ? 1 : 0; const initialTrackIndex = originalItems.length >= 2 ? initialOriginalIndex + loopCloneCount : initialOriginalIndex; requestAnimationFrame(function () { jumpToTrackItem(initialTrackIndex); requestAnimationFrame(function () { updateSlider(); startAutoPlay(); }); }); }); /* * 遊べる女の子一覧・注目の女の子一覧 * 自動移動は行わず、手動操作だけに対応 */ document.addEventListener( 'DOMContentLoaded', function () { const tracks = document.querySelectorAll( '.girls-list__track, .featured-list__track' ); tracks.forEach(function (track) { let isPointerDown = false; let activePointerId = null; let startX = 0; let startY = 0; let startScrollLeft = 0; let dragAxis = null; let didDrag = false; let suppressClick = false; track.addEventListener( 'pointerdown', function (event) { /* * スマホはブラウザ標準のスワイプに任せる * JavaScriptはパソコンのマウス操作だけに使う */ if (event.pointerType !== 'mouse') { return; } if (event.button !== 0) { return; } isPointerDown = true; activePointerId = event.pointerId; startX = event.clientX; startY = event.clientY; startScrollLeft = track.scrollLeft; dragAxis = null; didDrag = false; suppressClick = false; track.classList.add('is-dragging'); try { track.setPointerCapture( event.pointerId ); } catch (error) { /* * 対応していない端末では何もしない */ } } ); track.addEventListener( 'pointermove', function (event) { if ( !isPointerDown || event.pointerId !== activePointerId ) { return; } const moveX = event.clientX - startX; const moveY = event.clientY - startY; /* * 縦操作か横操作かを判定 */ if (dragAxis === null) { if ( Math.abs(moveX) < 6 && Math.abs(moveY) < 6 ) { return; } dragAxis = Math.abs(moveX) > Math.abs(moveY) ? 'horizontal' : 'vertical'; } if (dragAxis !== 'horizontal') { return; } if (event.cancelable) { event.preventDefault(); } didDrag = true; track.scrollLeft = startScrollLeft - moveX; }, { passive: false } ); function finishDrag(event) { if ( !isPointerDown || event.pointerId !== activePointerId ) { return; } isPointerDown = false; activePointerId = null; track.classList.remove( 'is-dragging' ); try { if ( track.hasPointerCapture( event.pointerId ) ) { track.releasePointerCapture( event.pointerId ); } } catch (error) { /* * 対応していない端末では何もしない */ } /* * ドラッグ直後の誤クリックを防ぐ */ if (didDrag) { suppressClick = true; window.setTimeout(function () { suppressClick = false; }, 350); } dragAxis = null; didDrag = false; } track.addEventListener( 'pointerup', finishDrag ); track.addEventListener( 'pointercancel', finishDrag ); track.addEventListener( 'click', function (event) { if (!suppressClick) { return; } event.preventDefault(); event.stopPropagation(); suppressClick = false; }, true ); /* * パソコンで画像だけが * ドラッグされるのを防ぐ */ track.addEventListener( 'dragstart', function (event) { event.preventDefault(); } ); }); } );