<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>無限ループスライダー</title>
<!– Tailwind CSSを読み込み –>
<script src=”https://cdn.tailwindcss.com”></script>
<style>
/* Interフォントを読み込み(Tailwindで指定された場合) */
body {
font-family: “Inter”, sans-serif;
}
/* スライダーコンテナの基本スタイル */
.slider-container {
overflow: hidden;
width: 100%;
position: relative; /* スライダーコンテンツのアニメーションのために必要 */
/* 高さはこの中のコンテンツによって決まる */
}
/* スライダーコンテンツラッパー – スライダーコンテンツを保持 */
.slider-content-wrapper {
width: 100%;
height: auto; /* コンテンツの高さに合わせる */
}
/* スライダーコンテンツ(画像を流す部分) */
.slider-content {
display: flex;
white-space: nowrap; /* 画像が改行されないように */
height: auto; /* 子要素(画像)の高さに合わせる */
}
/* 各スライドアイテム */
.slider-item {
flex-shrink: 0; /* 幅が縮まないように */
height: auto; /* 画像のアスペクト比を維持しながら高さを自動調整 */
object-fit: cover; /* 画像をはみ出さないようにトリミング */
display: block;
cursor: pointer;
}
/* ライトボックスのスタイルは変更なし */
.lightbox-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.lightbox-overlay.active {
opacity: 1;
visibility: visible;
}
.lightbox-content {
position: relative;
max-width: 90%;
max-height: 90%;
overflow: hidden;
border-radius: 0.75rem; /* rounded-lg */
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-xl */
}
.lightbox-image {
width: auto; /* 画像の元の比率を維持 */
height: auto;
max-width: 100%;
max-height: 100%;
display: block;
border-radius: 0.75rem; /* rounded-lg */
}
.lightbox-close {
position: absolute;
top: 1rem; /* 4px */
right: 1rem; /* 4px */
background-color: rgba(255, 255, 255, 0.8);
color: #000;
border: none;
border-radius: 9999px; /* full rounded */
width: 2.5rem; /* w-10 */
height: 2.5rem; /* h-10 */
font-size: 1.25rem; /* text-xl */
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
z-index: 1001; /* 画像より手前に */
font-weight: bold;
}
.lightbox-close:hover {
background-color: #fff;
color: #ef4444; /* red-500 */
}
</style>
</head>
<body>
<!– 上のスライダー –>
<div class=”slider-wrapper”>
<div class=”slider-container”>
<div id=”slider-content-top” class=”slider-content”>
<!– JavaScriptで画像がここに追加されます –>
</div>
</div>
</div>
<!– 下のスライダー (上のスライダーとの間にmt-4で空白を空ける) –>
<div class=”slider-wrapper mt-4″>
<div class=”slider-container”>
<div id=”slider-content-bottom” class=”slider-content”>
<!– JavaScriptで画像がここに追加されます –>
</div>
</div>
</div>
<!– ライトボックス (共通) –>
<div id=”lightbox-overlay” class=”lightbox-overlay”>
<div class=”lightbox-content”>
<button id=”lightbox-close” class=”lightbox-close”>×</button>
<img id=”lightbox-image” class=”lightbox-image” src=”” alt=”拡大画像”>
</div>
</div>
<script>
document.addEventListener(‘DOMContentLoaded’, () => {
const lightboxOverlay = document.getElementById(‘lightbox-overlay’);
const lightboxImage = document.getElementById(‘lightbox-image’);
const lightboxClose = document.getElementById(‘lightbox-close’);
// — スライダー設定 —
// 上のスライダー用の画像
const imagesTop = [
‘https://placehold.co/1200×675/FFDDAA/000?text=Top+Image+1’,
‘https://placehold.co/1200×675/FFCC99/000?text=Top+Image+2’,
‘https://placehold.co/1200×675/FFBB88/000?text=Top+Image+3’,
‘https://placehold.co/1200×675/AAEEBB/000?text=Top+Image+4’,
‘https://placehold.co/1200×675/CCFFDD/000?text=Top+Image+5’,
‘https://placehold.co/1200×675/EEFFCC/000?text=Top+Image+6’,
‘https://placehold.co/1200×675/FFEEAA/000?text=Top+Image+7’,
‘https://placehold.co/1200×675/A0C4FF/000?text=Top+Image+8’,
‘https://placehold.co/1200×675/BBDDFF/000?text=Top+Image+9’,
‘https://placehold.co/1200×675/DDEEFF/000?text=Top+Image+10’
];
// 下のスライダー用の画像
const imagesBottom = [
‘https://placehold.co/1200×675/BBDDFF/000?text=Bottom+Image+1’,
‘https://placehold.co/1200×675/DDEEFF/000?text=Bottom+Image+2’,
‘https://placehold.co/1200×675/A0C4FF/000?text=Bottom+Image+3’,
‘https://placehold.co/1200×675/EEFFCC/000?text=Bottom+Image+4’,
‘https://placehold.co/1200×675/FFEEAA/000?text=Bottom+Image+5’,
‘https://placehold.co/1200×675/CCFFDD/000?text=Bottom+Image+6’,
‘https://placehold.co/1200×675/AAEEBB/000?text=Bottom+Image+7’,
‘https://placehold.co/1200×675/FFBB88/000?text=Bottom+Image+8’,
‘https://placehold.co/1200×675/FFCC99/000?text=Bottom+Image+9’,
‘https://placehold.co/1200×675/FFDDAA/000?text=Bottom+Image+10’
];
// スライダーの速度 (ピクセル/フレーム、数値が大きいほど速い)
const SLIDER_SPEED = 0.5;
// スライダーのロジックをカプセル化するクラス
class ImageSlider {
constructor(sliderContentId, direction, speed, imagesArray) {
this.sliderContent = document.getElementById(sliderContentId);
this.direction = direction; // 1: 右から左, -1: 左から右
this.speed = speed;
this.images = imagesArray;
this.animationId = null;
this.currentPosition = 0;
this.sliderWidth = 0;
this.imageWidth = 0;
this.initializeSliderImages();
this.addEventListeners();
}
/**
* スライダーに画像を生成して追加し、幅を調整します。
* 無限ループのために画像を複製します。
*/
initializeSliderImages() {
this.sliderContent.innerHTML = ”; // 既存の画像をクリア
const containerWidth = this.sliderContent.offsetWidth;
if (containerWidth === 0) {
requestAnimationFrame(() => this.initializeSliderImages());
return;
}
// 表示する画像の枚数を画面幅によって切り替え
const imagesToShow = window.innerWidth < 640 ? 1 : 3;
// 常に指定枚数の画像が表示されるように、1枚あたりの幅を計算
this.imageWidth = containerWidth / imagesToShow;
// スライダーにオリジナル画像とその複製を追加
const totalImagesToAppend = this.images.length * 2;
for (let i = 0; i < totalImagesToAppend; i++) {
const src = this.images[i % this.images.length];
const img = document.createElement(‘img’);
img.src = src;
img.classList.add(‘slider-item’);
img.alt = `スライダー画像 ${i + 1}`;
img.style.width = `${this.imageWidth}px`;
img.onerror = () => {
console.error(‘Failed to load image:’, src);
const fallbackHeight = this.imageWidth * (9 / 16);
img.src = `https://placehold.co/${Math.round(this.imageWidth)}x${Math.round(fallbackHeight)}/CCCCCC/000?text=Error`;
};
this.sliderContent.appendChild(img);
}
// スライダー全体の幅 (オリジナル画像のセットの幅) を計算
this.sliderWidth = this.imageWidth * this.images.length;
// スライダーの初期位置を設定
if (this.direction === 1) { // 右から左
this.currentPosition = 0;
} else { // 左から右
this.currentPosition = -this.sliderWidth;
}
this.startAnimation();
}
/**
* スライダーのアニメーションフレームを更新します。
*/
animateSlider() {
if (this.direction === 1) { // 右から左
this.currentPosition -= this.speed;
if (this.currentPosition <= -this.sliderWidth) {
this.currentPosition = 0;
}
} else { // 左から右
this.currentPosition += this.speed;
if (this.currentPosition >= 0) {
this.currentPosition = -this.sliderWidth;
}
}
this.sliderContent.style.transform = `translateX(${this.currentPosition}px)`;
this.animationId = requestAnimationFrame(() => this.animateSlider());
}
/**
* スライダーアニメーションを開始します。
*/
startAnimation() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
this.animationId = requestAnimationFrame(() => this.animateSlider());
}
/**
* スライダーアニメーションを停止します。
*/
stopAnimation() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
}
/**
* イベントリスナーを設定します。
*/
addEventListeners() {
this.sliderContent.addEventListener(‘click’, (event) => {
if (event.target.classList.contains(‘slider-item’)) {
const imgSrc = event.target.src;
lightboxImage.src = imgSrc;
lightboxOverlay.classList.add(‘active’);
}
});
}
}
// — スライダーのインスタンス化 —
// 上のスライダーにimagesTopを、下のスライダーにimagesBottomを使用
const topSlider = new ImageSlider(‘slider-content-top’, 1, SLIDER_SPEED, imagesTop); // 右から左
const bottomSlider = new ImageSlider(‘slider-content-bottom’, -1, SLIDER_SPEED, imagesBottom); // 左から右
// — ライトボックスのイベントリスナー (共通) —
lightboxClose.addEventListener(‘click’, () => {
lightboxOverlay.classList.remove(‘active’);
});
document.addEventListener(‘keydown’, (event) => {
if (event.key === ‘Escape’ && lightboxOverlay.classList.contains(‘active’)) {
lightboxOverlay.classList.remove(‘active’);
}
});
// ウィンドウサイズ変更時の処理
window.addEventListener(‘resize’, () => {
// 両方のスライダーを停止し、再初期化して、新しいサイズに対応
topSlider.stopAnimation();
bottomSlider.stopAnimation();
topSlider.initializeSliderImages();
bottomSlider.initializeSliderImages();
});
});
</script>
</body>
</html>


コメント