script
apple/Macサイトでみられるスクロール量によるオブジェクト拡大
最終更新日: 2025年7月1日
表題の件、そのためのスクリプト他を作った。
落ちてるもんではなく、都合にあったもん。入れ子がビューポートに入ったら発火し、スクロール量に応じ拡大する。
サンプル
画像がビューポートに入ったらscale変更開始
画像(ラップ)の70%がビューポートに入るとscaleが1=100%になる設定。
これら設定は適宜変更可能。
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン
スクロールするためのマージン

以下コード
スケールのデフォルト値 (個別の画像で上書きされない場合)
<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も設置する場合により適宜変更がベター。