Steamer Lane Studio技術備忘録Script

apple/Macサイトでみられるスクロール量によるオブジェクト拡大

script apple/Macサイトでみられるスクロール量によるオブジェクト拡大
最終更新日: 2025年7月1日

表題の件、そのためのスクリプト他を作った。
落ちてるもんではなく、都合にあったもん。入れ子がビューポートに入ったら発火し、スクロール量に応じ拡大する。

サンプル
画像がビューポートに入ったらscale変更開始
画像(ラップ)の70%がビューポートに入るとscaleが1=100%になる設定。
これら設定は適宜変更可能。

スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン

apple/Macサイトでみられるスクロール量によるオブジェクト拡大

以下コード

スケールのデフォルト値 (個別の画像で上書きされない場合)

<script>// IntersectionObserverのグローバルなオプション設定window.scrollEffectGlobalOptions = {root: null,rootMargin: '0% 0% 0% 0%', // ビューポートに入った際の動作開始マージンthreshold: 0, // ビューポートに少しでも入ったらスケール変更開始targetSelector: 'img.scrolleffect', // 対象セレクタscaleOnePosition: 0.7 // スケールが1になるビューポートの位置(0~1、0.7はビューポート下端から30%)};// デバイスの画面幅に基づく最大スケールのデフォルト値設定window.scrollEffectDefaultMaxScale = {mobile: 1.0,desktop: 1.0};// 最小スケールのデフォルト値window.scrollEffectDefaultMinScale = 0.2;</script>

scrollingscale.js

// IIFEで全体をラップ(function() {const GLOBAL_OBSERVER_OPTIONS = window.scrollEffectGlobalOptions;const DEFAULT_MAX_SCALE_CONFIG = window.scrollEffectDefaultMaxScale;const DEFAULT_MIN_SCALE = window.scrollEffectDefaultMinScale;const TARGET_SELECTOR = GLOBAL_OBSERVER_OPTIONS.targetSelector || 'img.scroll-effect';const intersectingElements = new Set();

function getMaxScaleForElement(element) {const screenWidth = window.innerWidth;if (screenWidth < 768) {return parseFloat(element.dataset.maxScaleMobile) || DEFAULT_MAX_SCALE_CONFIG.mobile;} else {return parseFloat(element.dataset.maxScaleDesktop) || DEFAULT_MAX_SCALE_CONFIG.desktop;}}

function getMinScaleForElement(element) {return parseFloat(element.dataset.minScale) || DEFAULT_MIN_SCALE;}

function updateImageScale(element, container) {const rect = container.getBoundingClientRect();const viewportHeight = window.innerHeight;const containerTop = rect.top;const MIN_SCALE = getMinScaleForElement(element);const MAX_SCALE = getMaxScaleForElement(element);

const animationStartPoint = viewportHeight; // ビューポート下端const animationMidPoint = viewportHeight * (1 - GLOBAL_OBSERVER_OPTIONS.scaleOnePosition); // スケール1の位置(例: 0.7なら viewportHeight * 0.3)const animationEndPoint = 0; // ビューポート上端

let scale;if (containerTop >= animationMidPoint) {// ビューポート下半分(MIN_SCALEから1へ)let progress = 1 - (containerTop - animationMidPoint) / (animationStartPoint - animationMidPoint);progress = Math.max(0, Math.min(1, progress));scale = MIN_SCALE + (1 - MIN_SCALE) * progress;} else {// ビューポート上半分(1からMAX_SCALEへ)let progress = 1 - (containerTop - animationEndPoint) / (animationMidPoint - animationEndPoint);progress = Math.max(0, Math.min(1, progress));scale = 1 + (MAX_SCALE - 1) * progress;}

element.style.setProperty('--current-scale', scale);}

let ticking = false;const globalScrollHandler = () => {if (!ticking) {requestAnimationFrame(() => {intersectingElements.forEach(imageElement => {updateImageScale(imageElement, imageElement);});ticking = false;});ticking = true;}};

const globalResizeHandler = () => {intersectingElements.forEach(element => {updateImageScale(element, element);});};

const observer = new IntersectionObserver((entries) => {entries.forEach(entry => {const imageElement = entry.target;if (entry.isIntersecting) {intersectingElements.add(imageElement);updateImageScale(imageElement, imageElement);} else {intersectingElements.delete(imageElement);const rect = imageElement.getBoundingClientRect();const viewportHeight = window.innerHeight;const currentMaxScale = getMaxScaleForElement(imageElement);const currentMinScale = getMinScaleForElement(imageElement);

if (rect.bottom <= 0) {imageElement.style.setProperty('--current-scale', currentMaxScale);} else if (rect.top >= viewportHeight) {imageElement.style.setProperty('--current-scale', currentMinScale);}}});}, GLOBAL_OBSERVER_OPTIONS);

function initializeScrollEffect() {window.addEventListener('scroll', globalScrollHandler);window.addEventListener('resize', globalResizeHandler);document.querySelectorAll(TARGET_SELECTOR).forEach(element => {observer.observe(element);updateImageScale(element, element);});}

document.addEventListener('DOMContentLoaded', initializeScrollEffect);})();

CSS

.scrolleffectwrap {display:inline-block;width:100%;max-width:100%;height:auto;margin:0 auto 50px auto;overflow:hidden;}.scrolleffect {    width: 100%;    height: auto;    transform: scale(var(--current-scale, 1));    transform-origin: center top;    object-fit: cover;    transition: transform 0.0s linear;}


<style><!--.scroll-effect {position:relative;display:inline-block;width:100%;max-width:1000px;text-align:center;}

.scroll-effect img {width: 100%;height: auto;transform: scale(var(--current-scale, 1));transform-origin: center bottom;transition: transform 0.05s linear;object-fit: cover;}--></style>

属性はscript css共に適宜変更。

.scrolleffectwrap
これは無くても良いが、ラップする親要素は最低限高さ可変+対象要素center topに。
widthも設置する場合により適宜変更がベター。

EffectDefaultMinScale = 0.5;