/* ============================================================================ 特番サイト(単体サイト版) main.js ---------------------------------------------------------------------------- ■ 必要なもの:jQuery 3.x(index.html の最後で、このファイルより先に読み込み) ■ やっていること 1. 設定(CONFIG) … 動きの速さなどはここで調整 2. 共通の準備 3. オープニング(ローディング画面) 4. ヘッダー(スクロールで背景をつける/下スクロールで隠す) 5. スマホメニュー(ハンバーガー) 6. ページ内リンクのスムーススクロール 7. 今いるセクションのナビをハイライト 8. スクロールで要素をふわっと表示 9. 番組名を1文字ずつ表示 10. 予告動画(サムネイルをクリックで再生) 11. 写真の拡大表示(ライトボックス)… コーナー・生中継の写真 12. ページトップボタン(読んだ量のリングつき) 13. 起動 ■ HTML側の目印 data-reveal … スクロールで表示する要素 値なし:下からふわっと/mask:めくれるように/zoom:拡大から戻る/pop:ポンと出る data-reveal-delay … 表示を遅らせる秒数(例:data-reveal-delay="0.3") data-reveal-stagger … 親につけると、中の data-reveal が順番に表示される js-○○ … JSで動かす要素の目印(クラス名を変えると動かなくなるので注意) ============================================================================ */ (function ($) { 'use strict'; /* ========================================================================== 1. 設定(CONFIG)… ここの数字を変えると、動きを調整できます ========================================================================== */ var CONFIG = { loaderMinTime: 2200, // オープニングを最低限見せる時間(ミリ秒) loaderMaxTime: 4000, // 画像の読み込みが遅くても、この時間で必ず閉じる(ミリ秒) loaderOnce: true, // true:同じタブで2回目以降はオープニングを出さない scrollSpeed: 900, // ページ内リンクでスクロールする時間(ミリ秒) headerHideFrom: 480, // ここより下では、下スクロール中にヘッダーを隠す(px) revealLine: 0.88, // 要素の上端が「画面の上から88%」まで来たら表示 splitStep: 0.07, // 番組名を1文字ずつ出す間隔(秒) pagetopFrom: 600 // ページトップボタンを出し始める位置(px) }; /* ========================================================================== 2. 共通の準備 ========================================================================== */ var $win = $(window); var $doc = $(document); var $html = $('html'); var reduceMotion = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); var started = false; // オープニングが終わって、ページの演出が始まったか // すっと動いて、ゆっくり止まる動き(スムーススクロール用) $.easing.tvspEase = function (p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }; // 固定ヘッダーの高さ function headerHeight() { return $('.js-header').outerHeight() || 0; } // 後ろのページのスクロールを止める/戻す(メニュー・拡大表示中) function lockScroll() { $html.addClass('is-locked'); } function unlockScroll() { $html.removeClass('is-locked'); } /* ========================================================================== 3. オープニング(ローディング画面) ========================================================================== */ function initLoader() { var $loader = $('.js-loader'); // オープニングなし(2回目以降・動きを減らす設定・ローダーが無い) if (!$loader.length || $html.hasClass('is-loader-skip')) { $loader.remove(); startPage(); return; } var startTime = Date.now(); var closed = false; function close() { if (closed) return; closed = true; $loader.addClass('is-done'); // 中身が消えて、画面が上へ抜ける setTimeout(startPage, 450); // 画面が上へ抜け始めたら、ページの演出スタート setTimeout(function () { $loader.remove(); }, 1400); if (CONFIG.loaderOnce) { try { sessionStorage.setItem('tvsp-loaded', '1'); } catch (e) { /* 保存できなくても問題なし */ } } } function closeAfterMinTime() { setTimeout(close, Math.max(0, CONFIG.loaderMinTime - (Date.now() - startTime))); } if (document.readyState === 'complete') closeAfterMinTime(); else $win.on('load', closeAfterMinTime); setTimeout(close, CONFIG.loaderMaxTime); // 読み込みが遅くても必ず閉じる } function startPage() { if (started) return; started = true; // ファーストビュー(HERO)の要素は、画面の下の方にあっても最初にまとめて表示する $reveals = $reveals.filter(function () { if (!$(this).closest('.tvsp-hero').length) return true; var el = this; $(el).addClass('is-inview'); setTimeout(function () { el.style.transitionDelay = ''; }, 2200); return false; }); update(); } /* ========================================================================== 4. ヘッダー(スクロールで背景をつける/下スクロールで隠す) ========================================================================== */ var $header = $('.js-header'); var lastY = 0; function updateHeader(y) { $header.toggleClass('is-scrolled', y > 10); if ($html.hasClass('is-menu-open')) return; var diff = y - lastY; if (Math.abs(diff) < 6) return; // 小さな動きは無視 if (diff > 0 && y > CONFIG.headerHideFrom) $header.addClass('is-hidden'); // 下スクロール:隠す else $header.removeClass('is-hidden'); // 上スクロール:出す lastY = y; } function initHeader() { // キーボードでヘッダーにフォーカスが来たら、隠れていても出す $header.on('focusin', function () { $header.removeClass('is-hidden'); }); } /* ========================================================================== 5. スマホメニュー(ハンバーガー) ========================================================================== */ var $menu = $('.js-menu'); var $menuBtn = $('.js-menu-btn'); function openMenu() { $html.addClass('is-menu-open'); lockScroll(); $menuBtn.attr({ 'aria-expanded': 'true', 'aria-label': 'メニューを閉じる' }); $menu.attr('aria-hidden', 'false'); } function closeMenu() { if (!$html.hasClass('is-menu-open')) return; $html.removeClass('is-menu-open'); unlockScroll(); $menuBtn.attr({ 'aria-expanded': 'false', 'aria-label': 'メニューを開く' }); $menu.attr('aria-hidden', 'true'); } function initMenu() { $menuBtn.on('click', function () { if ($html.hasClass('is-menu-open')) closeMenu(); else openMenu(); }); // Esc キーで閉じる $doc.on('keydown', function (e) { if (e.key === 'Escape' && $html.hasClass('is-menu-open')) { closeMenu(); $menuBtn.trigger('focus'); } }); // PCの幅になったら閉じる $win.on('resize', function () { if (window.innerWidth >= 960) closeMenu(); }); } /* ========================================================================== 6. ページ内リンクのスムーススクロール href="#○○" のリンクすべてが対象。固定ヘッダーの高さ分ずらして止めます ========================================================================== */ function initSmoothScroll() { $doc.on('click', 'a[href^="#"]', function (e) { var hash = $(this).attr('href'); if (hash === '#') return; var isTop = (hash === '#top'); var $target = isTop ? $('body') : $(hash); if (!$target.length) return; e.preventDefault(); closeMenu(); var y = isTop ? 0 : Math.max(0, Math.round($target.offset().top - headerHeight())); $('html, body').stop().animate( { scrollTop: y }, reduceMotion ? 0 : CONFIG.scrollSpeed, 'tvspEase' ).promise().then(function () { // 読み上げソフト・キーボード操作のために、移動先へフォーカスを移す if (!isTop) { if (!$target.is('a, button, input, [tabindex]')) $target.attr('tabindex', '-1'); $target[0].focus({ preventScroll: true }); } }); }); } /* ========================================================================== 7. 今いるセクションのナビをハイライト ========================================================================== */ var $spyLinks = $('.js-spy'); var spyTargets = []; function initSpy() { var seen = {}; $spyLinks.each(function () { var hash = $(this).attr('href'); if (seen[hash] || hash === '#top' || !$(hash).length) return; seen[hash] = true; spyTargets.push(hash); }); } function updateSpy(y) { if (!spyTargets.length) return; var line = y + headerHeight() + window.innerHeight * 0.3; var current = ''; $.each(spyTargets, function (_, hash) { if ($(hash).offset().top <= line) current = hash; }); // 一番下まで来たら最後のセクション if (y + window.innerHeight >= $doc.height() - 4) current = spyTargets[spyTargets.length - 1]; $spyLinks.each(function () { var on = $(this).attr('href') === current; $(this).toggleClass('is-current', on); if (on) $(this).attr('aria-current', 'true'); else $(this).removeAttr('aria-current'); }); } /* ========================================================================== 8. スクロールで要素をふわっと表示(data-reveal) ========================================================================== */ var $reveals = $(); function initReveal() { $reveals = $('[data-reveal]'); // data-reveal-delay の秒数だけ遅らせる $reveals.each(function () { var delay = parseFloat($(this).attr('data-reveal-delay')); if (delay) this.style.transitionDelay = delay + 's'; }); // data-reveal-stagger の中は、同じ行の中で左から順番に少しずつ遅らせる setStagger(); $win.on('resize', setStagger); // 画面幅で列数が変わるので、そのたびに計算し直す } function setStagger() { $('[data-reveal-stagger]').each(function () { var $items = $(this).find('[data-reveal]').not('.is-inview'); // まだ表示されていないものだけ var rowTop = null; var col = 0; $items.each(function () { var top = Math.round(this.offsetTop); if (rowTop === null || Math.abs(top - rowTop) > 4) { rowTop = top; col = 0; } // 新しい行 this.style.transitionDelay = (col * 0.12) + 's'; col++; }); }); } function updateReveal() { if (!started || !$reveals.length) return; var line = window.innerHeight * CONFIG.revealLine; $reveals = $reveals.filter(function () { if (this.getBoundingClientRect().top >= line) return true; // まだ画面に入っていない var el = this; $(el).addClass('is-inview'); // 表示し終わったら遅延を消す(マウスをのせた時の動きが遅れないように) setTimeout(function () { el.style.transitionDelay = ''; }, 2200); return false; }); } /* ========================================================================== 9. 番組名を1文字ずつ表示(class="js-split") 文字を に分けて、少しずつ遅らせて表示します (読み上げ用に、分ける前の文字を別に残しています) ========================================================================== */ function initSplit() { $('.js-split').each(function () { var el = this; var fullText = $.trim($(el).text()); var baseDelay = parseFloat($(el).attr('data-reveal-delay')) || 0; var count = 0; (function walk(node) { $(node).contents().each(function () { if (this.nodeType === 3) { // 文字 var frag = document.createDocumentFragment(); Array.from(this.nodeValue).forEach(function (ch) { if (/\s/.test(ch)) { frag.appendChild(document.createTextNode(ch)); return; } var span = document.createElement('span'); span.className = 'tvsp-char'; span.textContent = ch; span.style.transitionDelay = (baseDelay + count * CONFIG.splitStep).toFixed(2) + 's'; count++; frag.appendChild(span); }); this.parentNode.replaceChild(frag, this); } else if (this.nodeType === 1) { // などの中も分ける walk(this); } }); })(el); $(el) .wrapInner('') .append($('').text(fullText)); }); } /* ========================================================================== 10. 予告動画(class="js-movie") サムネイルをクリックすると、その場でYouTubeを再生します (最初から動画を埋め込まないので、ページの表示が軽くなります) ========================================================================== */ function initMovie() { // 高画質サムネイルが無い動画は、標準画質に切り替える $('.js-movie img').each(function () { var img = this; function fallback() { if (img.naturalWidth && img.naturalWidth <= 120) { img.src = img.src.replace('maxresdefault', 'hqdefault'); } } if (img.complete) fallback(); else $(img).one('load', fallback); }); $doc.on('click', '.js-movie', function (e) { var id = $(this).attr('data-youtube-id'); if (!id) return; e.preventDefault(); var $iframe = $('