| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
- #홈페이지
- #동영상
- #cgimall
- 홈페이지제작
- #jQuery
- 해피CGI
- #웹솔루션
- 게시판
- 사이트제작
- 해피씨지아이
- 이미지
- #솔루션
- 웹솔루션
- #image
- CGIMALL
- happycgi
- jquery
- php
- javascript
- 홈페이지
- #해피CGI
- #업종별
- CSS
- #홈페이지제작
- 솔루션
- #happycgi
- #뉴스
- #CSS
- #쇼핑몰
- #이미지
- Today
- Total
웹솔루션개발 26년 노하우! 해피CGI의 모든것
[해피CGI][cgimall] 원형 애니메이션 타임라인 스크롤 갤러리 본문
스크롤 동작에 따라 카드가 원형으로 회전하며 강조되는 인터랙티브 갤러리 예제입니다.
중앙에 위치한 카드가 자연스럽게 확대되거나 강조되어 시각적인 집중 효과를 제공합니다.
HTML, CSS, JavaScript를 조합하여 부드러운 애니메이션과 스크롤 연동 기능을 구현하였습니다.
타임라인 형태의 레이아웃으로 콘텐츠를 순차적으로 보여주기에 적합합니다.
포트폴리오, 프로젝트 소개, 이미지 갤러리 등 다양한 웹페이지에 활용할 수 있는 UI 소스입니다.

HTML 구조
<section class="wrapper">
<div data-title="A misty Morning">
<img src="https://picsum.photos/id/634/1200/1200">
</div>
<div data-title="Harvest">
<img src="https://picsum.photos/id/228/1200/1200">
</div>
<div data-title="Waiting">
<img src="https://picsum.photos/id/661/1200/1200">
</div>
<div data-title="Time for Everything">
<img src="https://picsum.photos/id/380/1200/1200">
</div>
<div data-title="Cross over">
<img src="https://picsum.photos/id/392/1200/1200">
</div>
<div data-title="In The City">
<img src="https://picsum.photos/id/238/1200/1200">
</div>
<div id="img-7" data-title="A Boat Trip">
<img src="https://picsum.photos/id/469/1200/1200">
</div>
<div data-title="Waiting">
<img src="https://picsum.photos/id/311/1200/1200">
</div>
<div data-title="Stories to tell">
<img src="https://picsum.photos/id/515/1200/1200">
</div>
<div data-title="A Perfect Day">
<img src="https://picsum.photos/id/521/1200/1200">
</div>
<div data-title="Riding the Curve">
<img src="https://picsum.photos/id/549/1200/1200">
</div>
<div data-title="Raindrops">
<img src="https://picsum.photos/id/178/1200/1200">
</div>
<div data-title="Gone Sailing">
<img src="https://picsum.photos/id/637/1200/1200">
</div>
<div data-title="The Watch Tower">
<img src="https://picsum.photos/id/641/1200/1200">
</div>
<div data-title="Leaving">
<img src="https://picsum.photos/id/669/1200/1200">
</div>
<div data-title="Above the Clouds">
<img src="https://picsum.photos/id/685/1200/1200">
</div>
<div data-title="This is the title">
<img src="https://picsum.photos/id/505/1200/1200">
</div>
<div data-title="This is the title">
<img src="https://picsum.photos/id/699/1200/1200">
</div>
<div data-title="This is the title">
<img src="https://picsum.photos/id/513/1200/1200">
</div>
<div data-title="Contemplation!">
<img src="https://picsum.photos/id/773/1200/1200">
</div>
</section>
<div class="icon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 3m0 4a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v10a4 4 0 0 1 -4 4h-4a4 4 0 0 1 -4 -4z" />
<path d="M12 7l0 4" />
<path d="M8 26l4 4l4 -4">
<animateTransform attributeType="XML" attributeName="transform" type="translate" values="0 0; 0 4; 0 0" dur="1s" repeatCount="indefinite" />
</path>
</svg>
</div>
CSS 소스
@import url(https://fonts.bunny.net/css?family=just-me-again-down-here:400);
@layer base, mouse, demo;
@layer demo {
@property --rotate {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
body {
height: 1200svh;
margin: 0;
animation: --page-rotate 1s linear;
animation-timeline: scroll(nearest block);
/** this allows us to rotate to exactly each card.
BUT, it either needs to be set via JS or hardcoded as we can't accesss the .wrapper sibling-count() value from the body */
--cards: 20;
animation-timing-function: steps(var(--cards));
}
@keyframes --page-rotate {
to { --rotate: 1; }
}
.wrapper {
--card-border-radius: 14px;
--cards: sibling-count();
--card-width: max(150px, 20vw);
--card-height: calc(var(--card-width) * 6 / 4);
/* radius large enough so cards don't overlap */
--radius: calc(var(--card-width) * var(--cards) / (2 * 3.1416));
font-family: 'Just Me Again Down Here', handwriting;
position: fixed;
width: calc(var(--radius) * 2);
height: calc(var(--radius) * 2);
/* center circle so top card is visible */
top: calc(50% + var(--radius) + var(--card-height) * 2);
left: 50%;
transform-origin: center center;
transform: translateX(-50%) rotate(calc(var(--rotate) * 360deg));
transition: transform 300ms linear;
/*zoom: .3;*/
& > div {
--card-i: sibling-index();
/* card position around circle*/
--card-offset-radius: circle(var(--radius) at 50% 50%);
--card-offset-distance: calc((var(--card-i) - 1) / var(--cards) * 100%);
/* current card positioning relative to --rotate to detect "top" card */
--card-phase: calc((var(--card-i) - 1) / var(--cards) - 0.75);
--card-pos: mod(calc(var(--card-phase) + var(--rotate) + 1), 1);
--card-dist: min(var(--card-pos),calc(1 - var(--card-pos)));
--card-grayscale: clamp(0, calc(var(--card-dist) * var(--cards)), 1);
--card-opacity: calc(1 - (var(--card-dist) / 0.15 ));
/* blur */
--card-focus-range: .1;
--card-max-blur: 7px;
--card-norm-dist: min(var(--card-dist), var(--card-focus-range));
--card-blur-progress: calc(var(--card-norm-dist) / var(--card-focus-range));
--card-blur: calc(var(--card-blur-progress) * var(--card-max-blur));
/* caption */
--caption-active: clamp(0, 1 - (var(--card-dist) / 0.001), 1);
--caption-opacity: var(--caption-active);
--caption-y: calc(-150px * (1 - var(--caption-active)));
filter: blur(var(--card-blur)) grayscale(var(--card-grayscale));
opacity: var(--card-opacity);
container: size;
offset-path: var(--card-offset-radius);
offset-distance: var(--card-offset-distance);
offset-rotate: auto;
offset-anchor: 50% 100%;
position: absolute;
width: var(--card-width);
aspect-ratio: 4/6;
object-fit: cover;
border-radius: var(--card-border-radius);
transition: all 300ms ease-in-out;
transform-origin: center calc(var(--card-height) * 2 * -1);
/*
&::before{
content: counter(i);
counter-reset: i var(--caption-opacity);
}
*/
&::after {
content: attr(data-title);
position: absolute;
top: 100%;
left: 1rem;
opacity: var(--caption-opacity);
translate: 0 var(--caption-y);
font-size: clamp(1rem, 2vw + 0.045rem,1.6rem);
z-index: -1;
transition: opacity 300ms ease-in-out,translate 300ms ease-in-out;
}
& > img{
width: 100%;
height: 100%;
object-fit:cover;
border-radius: inherit;
}
}
}
}
@layer mouse{
.mouse {
position: fixed;
bottom:1rem;
left: 50%;
translate: -50% 0;
display: block;
width: 50px;
height: 50px;
opacity: 1;
color:var(--mouse-color);
display: none;
animation-name: mouse;
animation-duration: 1s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-timeline: scroll(nearest block);
@supports (animation-timeline: scroll()) {
display: block;
}
}
@keyframes mouse {
75% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
/* general styling not relevant for this demo */
@layer base {
* {
box-sizing: border-box;
}
:root {
color-scheme: light dark;
--bg-dark: rgb(16, 24, 40);
--bg-light: rgb(248, 244, 238);
--txt-light: rgb(10, 10, 10);
--txt-dark: rgb(245, 245, 245););
--line-light: rgba(0 0 0 / .25);
--line-dark: rgba(255 255 255 / .25);
--clr-bg: light-dark(var(--bg-light), var(--bg-dark));
--clr-txt: light-dark(var(--txt-light), var(--txt-dark));
--clr-lines: light-dark(var(--line-light), var(--line-dark));
}
body {
background-color: var(--clr-bg);
color: var(--clr-txt);
min-height: 100svh;
margin: 0;
padding: 2rem;
font-family: system, sans-serif;
font-size: 1rem;
line-height: 1.5;
display: grid;
place-items: center;
gap: 2rem;
& > * {
/*outline: 1px dashed red;*/
}
}
h1 {
margin: 0;
font-size: 1.2rem;
}
@supports not (animation-timeline: scroll()) {
body::before {
content:"Sorry, your browser doesn't support animation-timeline";
position: fixed;
top: 2rem;
left: 50%;
translate: -50% 0;
font-size: 0.8rem;
}
}
}
'웹프로그램밍 자료실 > 기타 자료' 카테고리의 다른 글
| [해피CGI][cgimall] 텍스트 프레임 테두리 애니메이션 회전 (CSS & SVG 기반) (0) | 2026.03.09 |
|---|---|
| [해피CGI][cgimall] Circular Gallery (0) | 2026.03.04 |
| [해피CGI][cgimall] Animated Slider (0) | 2026.03.03 |
| [해피CGI][cgimall] 바로가기 버튼이 들어간 카드 유형 UI Cards with inverted border-radius #scss (0) | 2026.02.27 |
| [해피CGI][cgimall] 카드 유형 마우스 오버 이펙트 cards hover effect (0) | 2026.02.26 |

