app/template/default/index.twig line 1

Open in your IDE?
  1. {% extends 'default_frame.twig' %}
  2. {% set body_class = 'front_page' %}
  3. {% block main %}
  4. <div class="mycarousel-wrapper">
  5.     <div class="mycarousel-slider">
  6.         {# PC用画像リスト main-visual.jpg #}
  7.         {% set pc_images = [
  8.             'main-visual.jpg',
  9.             'main-visual.jpg',
  10.             'main-visual.jpg'
  11.         ] %}
  12.         
  13.         {# SP用画像リスト sp-topImages.jpg #}
  14.         {% set sp_images = [
  15.         'sp-topImages.jpg',
  16.         'sp-topImages.jpg',
  17.         'sp-topImages.jpg'
  18.         ] %}
  19.         
  20.         {# 実際に表示する画像を選択(0から始まるインデックス番号で指定) #}
  21.         {% set display_images = [0,1,2] %}
  22.         
  23.         {% for index in display_images %}
  24.             <div class="mycarousel-slide">
  25.                 <img class="desktop-image" src="{{ asset('/html/user_data/ContentImg/' ~ pc_images[index]) }}" alt="スライド画像">
  26.                 <img class="mobile-image" src="{{ asset('/html/user_data/ContentImg/' ~ sp_images[index]) }}" alt="スライド画像">
  27.             </div>
  28.         {% endfor %}
  29.     </div>
  30. </div>
  31. {% endblock %}
  32. {% block stylesheet %}
  33. <style>
  34. /* slickのデフォルト黒丸(before擬似要素)を無効化 */
  35. .mycarousel-slider .slick-dots li button::before {
  36.     display: none;
  37.     content: '';
  38. }
  39. .slick-dots li {
  40.     position:unset;
  41.     display: inline-block;
  42.     width: 20px;
  43.     height: 20px;
  44.     margin: 0 5px;
  45.     padding: 0;
  46.     cursor: pointer;
  47. }
  48. .mycarousel-slider .slick-dots li {
  49.     width: 10px;
  50.     height: 10px;
  51. }
  52. .mycarousel-slider .slick-dots li button {
  53.     width: 100%;
  54.     height: 100%;
  55.     padding: 0;
  56.     border: none;
  57.     border-radius: 50%;
  58.     background-color: #ccc;
  59.     opacity: 0.7;
  60.     transition: all 0.3s ease;
  61.     font-size: 0;
  62.     cursor: pointer;
  63. }
  64. .mycarousel-slider .slick-dots li.slick-active button {
  65.     background-color: #b8860b;
  66.     transform: scale(1.3);
  67.     opacity: 1;
  68. }
  69. /* フルワイドの外枠 */
  70. .mycarousel-wrapper {
  71.     width: 100vw;
  72.     max-width: none;
  73.     margin-left: calc(-50vw + 50%);
  74.     margin-right: calc(-50vw + 50%);
  75.     overflow: hidden;
  76.     padding: 0;
  77.     /* 初期化時のちらつき防止 */
  78.     visibility: hidden;
  79. }
  80. /* slickのトラック修正 */
  81. .slick-track {
  82.     display: flex !important;
  83.     align-items: center;
  84. }
  85. .slick-slide {
  86.     padding: 0 !important;
  87.     margin: 0 !important;
  88. }
  89. /* スライド本体(動的高さ対応) */
  90. .mycarousel-slide {
  91.     box-sizing: border-box;
  92.     padding: 0 !important;
  93.     margin: 0 !important;
  94.     position: relative;
  95.     transition: all 0.5s ease;
  96.     overflow: hidden;
  97.     /* 高さは動的に設定される */
  98.     height: 60vh; /* 初期値:JavaScript で調整される */
  99. }
  100. /* 画像の基本スタイル */
  101. .desktop-image,
  102. .mobile-image {
  103.     position: absolute;
  104.     top: 0;
  105.     left: 0;
  106.     width: 100%;
  107.     height: 100%;
  108.     transition: all 0.5s ease;
  109.     /* object-fit は JavaScript で動的に調整 */
  110.     object-fit: contain;
  111. }
  112. /* デスクトップ時のスタイル(768pxより大きい) */
  113. @media (min-width: 769px) {
  114.     .desktop-image {
  115.         display: block !important;
  116.     }
  117.     .mobile-image {
  118.         display: none !important;
  119.     }
  120. }
  121. /* モバイル時のスタイル(768px以下) */
  122. @media (max-width: 768px) {
  123.     .mycarousel-wrapper {
  124.         width: 100vw;
  125.         margin-left: calc(-50vw + 50%);
  126.         margin-right: calc(-50vw + 50%);
  127.     }
  128.     
  129.     .mycarousel-slide {
  130.         width: 100vw !important;
  131.         height: auto !important;
  132.         padding: 0 !important;
  133.         margin: 0 !important;
  134.         opacity: 1 !important;
  135.         filter: none !important;
  136.     }
  137.     .desktop-image {
  138.         display: none !important;
  139.     }
  140.     
  141.     .mobile-image {
  142.         display: block !important;
  143.         position: relative !important;
  144.         width: 100% !important;
  145.         height: auto !important;
  146.         object-fit: contain !important;
  147.         filter: brightness(1) contrast(1) !important;
  148.     }
  149. }
  150. /* 左右の画像を暗くするエフェクト(デスクトップのみ) */
  151. @media (min-width: 769px) {
  152.     .mycarousel-slide.side-slide {
  153.         opacity: 0.6;
  154.     }
  155.     .mycarousel-slide.side-slide .desktop-image {
  156.         filter: brightness(0.7) contrast(0.9);
  157.     }
  158.     .mycarousel-slide.center-slide {
  159.         opacity: 1;
  160.     }
  161.     .mycarousel-slide.center-slide .desktop-image {
  162.         filter: brightness(1) contrast(1);
  163.     }
  164.     .mycarousel-slide.side-slide:hover {
  165.         opacity: 0.8;
  166.         transform: scale(1.02);
  167.     }
  168.     .mycarousel-slide.side-slide:hover .desktop-image {
  169.         filter: brightness(0.85) contrast(0.95);
  170.     }
  171. }
  172. /* 動的高さ調整用のユーティリティクラス */
  173. .dynamic-fit-contain {
  174.     object-fit: contain;
  175. }
  176. .dynamic-fit-cover {
  177.     object-fit: cover;
  178. }
  179. </style>
  180. {% endblock %}
  181. {% block javascript %}
  182. <script>
  183. $(function() {
  184.     // 動的レスポンシブ設定
  185.     const RESPONSIVE_CONFIG = {
  186.         desktop: {
  187.             targetAspectRatio: 16/9,
  188.             minHeight: '30vh',
  189.             maxHeight: '60vh',
  190.             marginThreshold: 0.15
  191.         },
  192.         mobile: {
  193.             targetAspectRatio: 4/3,
  194.             minHeight: '35vh',
  195.             maxHeight: '85vh',
  196.             marginThreshold: 0.12
  197.         }
  198.     };
  199.     let isInitialized = false;
  200.     let currentImages = [];
  201.     // 画面幅に応じた動的centerPadding計算
  202.     function calculateCenterPadding() {
  203.         const windowWidth = $(window).width();
  204.         let padding;
  205.         
  206.         // 画面幅に応じてcenterPaddingを計算(調整可能な閾値)
  207.         if (windowWidth <= 768) {
  208.             // モバイル: 余白なし
  209.             padding = '0';
  210.         } else if (windowWidth <= 1024) {
  211.             // タブレット: 小さめの余白
  212.             padding = '8vw';
  213.         } else if (windowWidth <= 1200) {
  214.             // 小型デスクトップ: 中程度の余白
  215.             padding = '12vw';
  216.         }else if (windowWidth <= 1440) {
  217.             // 小型デスクトップ: 中程度の余白
  218.             padding = '15vw';
  219.         
  220.         } else if (windowWidth <= 1920) {
  221.             // 標準デスクトップ: やや大きめの余白
  222.             padding = '20vw';
  223.         } else {
  224.             // 大型ディスプレイ: 最大余白を制限
  225.             padding = '25vw';
  226.         }
  227.         
  228.         return padding;
  229.     }
  230.     // centerPaddingを動的に更新する関数
  231.     function updateCenterPadding() {
  232.         const newPadding = calculateCenterPadding();
  233.         const windowWidth = $(window).width();
  234.         
  235.         // モバイルはcenterMode自体を無効にする
  236.         const centerModeEnabled = windowWidth > 768;
  237.         
  238.         if ($('.mycarousel-slider').hasClass('slick-initialized')) {
  239.             $('.mycarousel-slider').slick('slickSetOption', 'centerMode', centerModeEnabled, false);
  240.             $('.mycarousel-slider').slick('slickSetOption', 'centerPadding', newPadding, true);
  241.         }
  242.     }
  243.     // 現在のデバイスに応じた画像を取得
  244.     function getCurrentImages() {
  245.         const isMobile = $(window).width() <= 768;
  246.         return isMobile ? $('.mobile-image') : $('.desktop-image');
  247.     }
  248.     // 画像プリロード関数
  249.     function preloadImages() {
  250.         return new Promise((resolve) => {
  251.             const images = getCurrentImages();
  252.             let loadedCount = 0;
  253.             const totalImages = images.length;
  254.             if (totalImages === 0) {
  255.                 resolve();
  256.                 return;
  257.             }
  258.             images.each(function() {
  259.                 const img = this;
  260.                 
  261.                 if (img.complete && img.naturalWidth > 0) {
  262.                     loadedCount++;
  263.                     if (loadedCount >= totalImages) {
  264.                         resolve();
  265.                     }
  266.                 } else {
  267.                     $(img).on('load error', function() {
  268.                         loadedCount++;
  269.                         if (loadedCount >= totalImages) {
  270.                             resolve();
  271.                         }
  272.                     });
  273.                     
  274.                     // 強制的に再読み込みをトリガー(キャッシュ対策)
  275.                     if (!img.src) {
  276.                         img.src = img.getAttribute('src');
  277.                     }
  278.                 }
  279.             });
  280.         });
  281.     }
  282.     // スライダー初期化(動的centerPadding対応版)
  283.     function initializeSlider() {
  284.         if ($('.mycarousel-slider').hasClass('slick-initialized')) {
  285.             $('.mycarousel-slider').slick('unslick');
  286.         }
  287.         const initialPadding = calculateCenterPadding();
  288.         const windowWidth = $(window).width();
  289.         const centerModeEnabled = windowWidth > 768;
  290.         $('.mycarousel-slider').slick({
  291.             centerMode: centerModeEnabled,
  292.             centerPadding: initialPadding,
  293.             slidesToShow: 1,
  294.             autoplay: false, // 初期化時は自動再生を無効にして安定性を確保
  295.             autoplaySpeed: 5000,
  296.             arrows: true,
  297.             dots: true,
  298.             adaptiveHeight: false,
  299.             fade: false,
  300.             speed: 300
  301.             // responsive設定は削除(動的制御に切り替え)
  302.         });
  303.         // スライド変更時にクラスを更新
  304.         $('.mycarousel-slider').on('afterChange', function(event, slick, currentSlide) {
  305.             updateSlideClasses();
  306.         });
  307.     }
  308.     // 動的な表示最適化メイン関数
  309.     function optimizeCarouselDisplay() {
  310.         const isMobile = $(window).width() <= 768;
  311.         const config = isMobile ? RESPONSIVE_CONFIG.mobile : RESPONSIVE_CONFIG.desktop;
  312.         const $currentImages = getCurrentImages();
  313.         
  314.         if ($currentImages.length === 0) return;
  315.         // モバイル時は簡単な設定にする
  316.         if (isMobile) {
  317.             // モバイル時:width 100vw、height auto
  318.             $('.mycarousel-slide').css({
  319.                 'height': 'auto',
  320.                 'width': '100vw'
  321.             });
  322.             $currentImages.css({
  323.                 'width': '100%',
  324.                 'height': 'auto',
  325.                 'object-fit': 'contain',
  326.                 'position': 'relative'
  327.             });
  328.             
  329.             // 初期化完了後に自動再生を開始
  330.             if (isInitialized && $('.mycarousel-slider').slick('slickGetOption', 'autoplay') === false) {
  331.                 $('.mycarousel-slider').slick('slickSetOption', 'autoplay', true, true);
  332.             }
  333.             return;
  334.         }
  335.         const $firstImage = $currentImages.filter(':visible').first();
  336.         if ($firstImage.length === 0 || !$firstImage[0].complete || !$firstImage[0].naturalWidth) {
  337.             return;
  338.         }
  339.         const imageWidth = $firstImage[0].naturalWidth;
  340.         const imageHeight = $firstImage[0].naturalHeight;
  341.         const imageAspectRatio = imageWidth / imageHeight;
  342.         
  343.         const containerWidth = $('.mycarousel-wrapper').width();
  344.         const containerAspectRatio = config.targetAspectRatio;
  345.         
  346.         const maxRenderWidth = Math.min(imageWidth, containerWidth);
  347.         const maxRenderHeight = maxRenderWidth / imageAspectRatio;
  348.         
  349.         const viewportMinHeightPx = parseFloat(config.minHeight) * $(window).height() / 100;
  350.         const viewportMaxHeightPx = parseFloat(config.maxHeight) * $(window).height() / 100;
  351.         
  352.         let optimalHeight;
  353.         let objectFitMode = 'contain';
  354.         const aspectRatioDiff = Math.abs(imageAspectRatio - containerAspectRatio) / containerAspectRatio;
  355.         if (maxRenderHeight <= viewportMaxHeightPx && maxRenderHeight >= viewportMinHeightPx) {
  356.             optimalHeight = maxRenderHeight;
  357.         } else if (maxRenderHeight > viewportMaxHeightPx) {
  358.             if (aspectRatioDiff <= config.marginThreshold) {
  359.                 optimalHeight = viewportMaxHeightPx;
  360.             } else {
  361.                 optimalHeight = Math.min(containerWidth / imageAspectRatio, viewportMaxHeightPx);
  362.             }
  363.         } else {
  364.             if (maxRenderHeight < viewportMinHeightPx) {
  365.                 const scaleUpLimit = Math.min(2.0, viewportMinHeightPx / maxRenderHeight);
  366.                 if (scaleUpLimit <= 1.5) {
  367.                     optimalHeight = viewportMinHeightPx;
  368.                 } else {
  369.                     optimalHeight = maxRenderHeight;
  370.                 }
  371.             } else {
  372.                 optimalHeight = maxRenderHeight;
  373.             }
  374.         }
  375.         // 最終的な制限適用
  376.         optimalHeight = Math.max(viewportMinHeightPx, Math.min(viewportMaxHeightPx, optimalHeight));
  377.         // 画像の品質を考慮したobject-fit調整
  378.         const renderScale = optimalHeight / maxRenderHeight;
  379.         if (renderScale > 1.2) {
  380.             objectFitMode = 'contain';
  381.         } else {
  382.             objectFitMode = 'contain';
  383.         }
  384.         // スタイル適用(デスクトップのみ)
  385.         $('.mycarousel-slide').css('height', optimalHeight + 'px');
  386.         $currentImages.css({
  387.             'object-fit': objectFitMode,
  388.             'image-rendering': renderScale > 1.1 ? 'smooth' : 'auto'
  389.         });
  390.         // 初期化完了後に自動再生を開始
  391.         if (isInitialized && $('.mycarousel-slider').slick('slickGetOption', 'autoplay') === false) {
  392.             $('.mycarousel-slider').slick('slickSetOption', 'autoplay', true, true);
  393.         }
  394.     }
  395.     // スライドのクラスを更新する関数
  396.     function updateSlideClasses() {
  397.         $('.mycarousel-slide').removeClass('center-slide side-slide');
  398.         
  399.         if ($(window).width() > 768) {
  400.             $('.slick-center .mycarousel-slide').addClass('center-slide');
  401.             $('.slick-slide:not(.slick-center) .mycarousel-slide').addClass('side-slide');
  402.         } else {
  403.             $('.mycarousel-slide').addClass('center-slide');
  404.         }
  405.     }
  406.     async function initialize() {
  407.         try {
  408.             // カルーセルを一時的に非表示にしてちらつき防止
  409.             $('.mycarousel-wrapper').css('visibility', 'hidden');
  410.             
  411.             await preloadImages();
  412.             
  413.             initializeSlider();
  414.             
  415.             optimizeCarouselDisplay();
  416.             
  417.             updateSlideClasses();
  418.             
  419.             isInitialized = true;
  420.             
  421.             // 6. 少し待ってから表示(slickの初期化完了を確実にする)
  422.             setTimeout(() => {
  423.                 $('.mycarousel-wrapper').css('visibility', 'visible');
  424.                 // 初期化後にもう一度表示を最適化
  425.                 optimizeCarouselDisplay();
  426.                 updateSlideClasses();
  427.             }, 50);
  428.             
  429.             console.log('Carousel initialized successfully');
  430.             
  431.         } catch (error) {
  432.             console.error('Carousel initialization failed:', error);
  433.             // エラー時も表示を戻す
  434.             $('.mycarousel-wrapper').css('visibility', 'visible');
  435.         }
  436.     }
  437.     let resizeTimer;
  438.     $(window).on('resize', function() {
  439.         clearTimeout(resizeTimer);
  440.         resizeTimer = setTimeout(async () => {
  441.             // centerPaddingを動的に更新
  442.             updateCenterPadding();
  443.             
  444.             // デバイス切り替わり時は再初期化
  445.             const newImages = getCurrentImages();
  446.             const imagesSwitched = !currentImages.length || 
  447.                 newImages.length !== currentImages.length ||
  448.                 newImages[0] !== currentImages[0];
  449.             
  450.             if (imagesSwitched) {
  451.                 currentImages = newImages;
  452.                 await preloadImages();
  453.             }
  454.             
  455.             optimizeCarouselDisplay();
  456.             updateSlideClasses();
  457.         }, 200);
  458.     });
  459.     
  460.     $(document).ready(function() {
  461.         currentImages = getCurrentImages();
  462.         initialize();
  463.     });
  464.     
  465.     $(window).on('load', function() {
  466.         if (!isInitialized) {
  467.             initialize();
  468.         }
  469.     });
  470. });
  471. </script>
  472. {% endblock %}