관리 메뉴

웹솔루션개발 26년 노하우! 해피CGI의 모든것

[해피CGI][cgimall] 원형 애니메이션 타임라인 스크롤 갤러리 본문

웹프로그램밍 자료실/기타 자료

[해피CGI][cgimall] 원형 애니메이션 타임라인 스크롤 갤러리

해피CGI윤실장 2026. 3. 10. 09:29

스크롤 동작에 따라 카드가 원형으로 회전하며 강조되는 인터랙티브 갤러리 예제입니다.

중앙에 위치한 카드가 자연스럽게 확대되거나 강조되어 시각적인 집중 효과를 제공합니다.
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;

}

}

}

 

 

Comments