미리보기 안에서 스크롤하거나 마우스를 움직여 모션을 확인하세요.
실행용 통합 HTML · 공식 GSAP 3.15.0
<!doctype html>
<!-- Archived from GSAP Demo Hub / image-sequence
Original editor: https://codepen.io/GreenSock/pen/VwgevYW
HTML/CSS/JS source panels are preserved separately. Runtime GSAP: 3.15.0 official distribution. -->
<html lang="en" class=""><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Image sequence</title>
<style>
body {
height: 300vh;
background: #000;
}
canvas {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 80vw;
max-height: 80vh;
}
</style></head><body>
<canvas id="image-sequence" width="1158" height="770" />
<script src="https://asset.wemashup.com/wp-content/uploads/wm-asset-library/gsap/shared/gsap-3.15.0-gsap.min.js"></script>
<script src="https://asset.wemashup.com/wp-content/uploads/wm-asset-library/gsap/shared/gsap-3.15.0-ScrollTrigger.min.js"></script>
<script src="https://asset.wemashup.com/wp-content/uploads/wm-asset-library/gsap/shared/vYbbEMa.js"></script>
<script src="https://asset.wemashup.com/wp-content/uploads/wm-asset-library/gsap/shared/61d97a803efd5a1b.js"></script>
<script>
// looking for a non-scrubbing version? https://codepen.io/GreenSock/pen/QWYdgjG
let frameCount = 147,
urls = new Array(frameCount).fill().map((o, i) => `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${(i+1).toString().padStart(4, '0')}.jpg`);
imageSequence({
urls, // Array of image URLs
canvas: "#image-sequence", // <canvas> object to draw images to
//clear: true, // only necessary if your images contain transparency
//onUpdate: (index, image) => console.log("drew image index", index, ", image:", image),
scrollTrigger: {
start: 0, // start at the very top
end: "max", // entire page
scrub: true, // important!
}
});
/*
Helper function that handles scrubbing through a sequence of images, drawing the appropriate one to the provided canvas.
Config object properties:
- urls [Array]: an Array of image URLs
- canvas [Canvas]: the <canvas> object to draw to
- scrollTrigger [Object]: an optional ScrollTrigger configuration object like {trigger: "#trigger", start: "top top", end: "+=1000", scrub: true, pin: true}
- clear [Boolean]: if true, it'll clear out the canvas before drawing each frame (useful if your images contain transparency)
- paused [Boolean]: true if you'd like the returned animation to be paused initially (this isn't necessary if you're passing in a ScrollTrigger that's scrubbed, but it is helpful if you just want a normal playback animation)
- fps [Number]: optional frames per second - this determines the duration of the returned animation. This doesn't matter if you're using a scrubbed ScrollTrigger. Defaults to 30fps.
- onUpdate [Function]: optional callback for when the Tween updates (probably not used very often). It'll pass two parameters: 1) the index of the image (zero-based), and 2) the Image that was drawn to the canvas
Returns a Tween instance
*/
function imageSequence(config) {
let playhead = {frame: 0},
canvas = gsap.utils.toArray(config.canvas)[0] || console.warn("canvas not defined"),
ctx = canvas.getContext("2d"),
curFrame = -1,
onUpdate = config.onUpdate,
images,
updateImage = function() {
let frame = Math.round(playhead.frame);
if (frame !== curFrame) { // only draw if necessary
config.clear && ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(images[Math.round(playhead.frame)], 0, 0);
curFrame = frame;
onUpdate && onUpdate.call(this, frame, images[frame]);
}
};
images = config.urls.map((url, i) => {
let img = new Image();
img.src = url;
i || (img.onload = updateImage);
return img;
});
return gsap.to(playhead, {
frame: images.length - 1,
ease: "none",
onUpdate: updateImage,
duration: images.length / (config.fps || 30),
paused: !!config.paused,
scrollTrigger: config.scrollTrigger
});
}
</script>
</body></html>
원본 HTML
<canvas id="image-sequence" width="1158" height="770" />원본 CSS
body {
height: 300vh;
background: #000;
}
canvas {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 80vw;
max-height: 80vh;
}원본 JavaScript
// looking for a non-scrubbing version? https://codepen.io/GreenSock/pen/QWYdgjG
let frameCount = 147,
urls = new Array(frameCount).fill().map((o, i) => `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${(i+1).toString().padStart(4, '0')}.jpg`);
imageSequence({
urls, // Array of image URLs
canvas: "#image-sequence", // <canvas> object to draw images to
//clear: true, // only necessary if your images contain transparency
//onUpdate: (index, image) => console.log("drew image index", index, ", image:", image),
scrollTrigger: {
start: 0, // start at the very top
end: "max", // entire page
scrub: true, // important!
}
});
/*
Helper function that handles scrubbing through a sequence of images, drawing the appropriate one to the provided canvas.
Config object properties:
- urls [Array]: an Array of image URLs
- canvas [Canvas]: the <canvas> object to draw to
- scrollTrigger [Object]: an optional ScrollTrigger configuration object like {trigger: "#trigger", start: "top top", end: "+=1000", scrub: true, pin: true}
- clear [Boolean]: if true, it'll clear out the canvas before drawing each frame (useful if your images contain transparency)
- paused [Boolean]: true if you'd like the returned animation to be paused initially (this isn't necessary if you're passing in a ScrollTrigger that's scrubbed, but it is helpful if you just want a normal playback animation)
- fps [Number]: optional frames per second - this determines the duration of the returned animation. This doesn't matter if you're using a scrubbed ScrollTrigger. Defaults to 30fps.
- onUpdate [Function]: optional callback for when the Tween updates (probably not used very often). It'll pass two parameters: 1) the index of the image (zero-based), and 2) the Image that was drawn to the canvas
Returns a Tween instance
*/
function imageSequence(config) {
let playhead = {frame: 0},
canvas = gsap.utils.toArray(config.canvas)[0] || console.warn("canvas not defined"),
ctx = canvas.getContext("2d"),
curFrame = -1,
onUpdate = config.onUpdate,
images,
updateImage = function() {
let frame = Math.round(playhead.frame);
if (frame !== curFrame) { // only draw if necessary
config.clear && ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(images[Math.round(playhead.frame)], 0, 0);
curFrame = frame;
onUpdate && onUpdate.call(this, frame, images[frame]);
}
};
images = config.urls.map((url, i) => {
let img = new Image();
img.src = url;
i || (img.onload = updateImage);
return img;
});
return gsap.to(playhead, {
frame: images.length - 1,
ease: "none",
onUpdate: updateImage,
duration: images.length / (config.fps || 30),
paused: !!config.paused,
scrollTrigger: config.scrollTrigger
});
}보관·활용 메모
GSAP Demo Hub의 공개 예제입니다. 원본 HTML·CSS·JavaScript와 설정, 시연 파일을 함께 보관했습니다. 통합 HTML은 공통 스타일과 실행 의존성을 연결한 파일이며 SCSS는 원본 컴파일 결과를 사용합니다.
구형 CodePen 전용 의존성을 그대로 배포하지 않고, 통합 실행에는 공식 GSAP 3.15.0을 연결했습니다. 원본 패널과 원본 설정은 ZIP에 별도로 보존되어 있습니다. 예제의 이미지·폰트 등 외부 소재 요청은 유지됩니다.
보관일 2026-10-26 · 원본 코드·작성자·설정 정보를 보존했습니다. 실제 프로젝트에서 코드와 이미지·폰트의 사용 조건을 확인하세요.