Codrops 원본

Building a Scroll-Revealed WebGL Gallery with GSAP, Three.js, Astro and Barba.js

스크롤 · MIT

스크롤 더 보기
ORIGINAL PREVIEW
Building a Scroll-Revealed WebGL Gallery with GSAP, Three.js, Astro and Barba.js 정적 미리보기

갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.

큰 화면으로 보기 새 탭

SOURCE FILES

원본 코드 읽기

24개 파일

수집한 원본 소스와 실행 안내를 함께 제공합니다.

astro.config.mjs
파일 저장

// @ts-check
import { defineConfig } from "astro/config"
import glsl from "vite-plugin-glsl"

// https://astro.build/config
export default defineConfig({
  devToolbar: {
    enabled: false,
  },
  vite: {
    plugins: [glsl()],
  },
  server: {
    host: true,
  },
})
src/app/components/canvas.ts
파일 저장

import * as THREE from "three"
import { Dimensions, Size } from "../types/types"

import Media from "./media"
import { ScrollTrigger } from "gsap/ScrollTrigger"

export default class Canvas {
  element: HTMLCanvasElement
  scene: THREE.Scene
  camera: THREE.PerspectiveCamera
  renderer: THREE.WebGLRenderer
  sizes: Size
  dimensions: Dimensions
  medias: (Media | null)[] | null

  constructor() {
    this.element = document.getElementById("webgl") as HTMLCanvasElement
    this.medias = []
    this.createScene()
    this.createCamera()
    this.createRenderer()
    this.setSizes()
  }

  createScene() {
    this.scene = new THREE.Scene()
  }

  createCamera() {
    this.camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      100,
    )
    this.scene.add(this.camera)
    this.camera.position.z = 10
  }

  createRenderer() {
    this.dimensions = {
      width: window.innerWidth,
      height: window.innerHeight,
      pixelRatio: Math.min(2, window.devicePixelRatio),
    }

    this.renderer = new THREE.WebGLRenderer({
      canvas: this.element,
      alpha: true,
    })
    this.renderer.setSize(this.dimensions.width, this.dimensions.height)
    this.renderer.render(this.scene, this.camera)

    this.renderer.setPixelRatio(this.dimensions.pixelRatio)
  }

  setSizes() {
    let fov = this.camera.fov * (Math.PI / 180)
    let height = this.camera.position.z * Math.tan(fov / 2) * 2
    let width = height * this.camera.aspect

    this.sizes = {
      width: width,
      height: height,
    }
  }

  onResize() {
    this.dimensions = {
      width: window.innerWidth,
      height: window.innerHeight,
      pixelRatio: Math.min(2, window.devicePixelRatio),
    }

    this.camera.aspect = window.innerWidth / window.innerHeight
    this.camera.updateProjectionMatrix()
    this.setSizes()

    this.renderer.setPixelRatio(this.dimensions.pixelRatio)
    this.renderer.setSize(this.dimensions.width, this.dimensions.height)

    //ScrollTrigger.refresh()
    this.medias?.forEach((media) => {
      media?.onResize(this.sizes)
    })
  }

  createMedias(activeElement?: HTMLImageElement) {
    const images = document.querySelectorAll("img")
    images.forEach((image) => {
      if (image !== activeElement) {
        const media = new Media({
          element: image,
          scene: this.scene,
          sizes: this.sizes,
        })

        this.medias?.push(media)
      }
    })

    this.medias?.forEach((media) => {
      media?.observe()
    })
  }

  render(scroll: number, updateScroll: boolean = true) {
    this.medias?.forEach((media) => {
      if (updateScroll) {
        media?.updateScroll(scroll)
      }
    })

    this.renderer.render(this.scene, this.camera)
  }
}
함께 쓰는 파일 22개 보기
src/app/components/media.ts
파일 저장

import { Position, Size } from "../types/types"
import * as THREE from "three"
import gsap from "gsap"

import vertexShader from "../shaders/vertex.glsl"
import fragmentShader from "../shaders/fragment.glsl"

interface Props {
  element: HTMLImageElement
  scene: THREE.Scene
  sizes: Size
}

export default class Media {
  element: HTMLImageElement
  anchorElement: HTMLAnchorElement | undefined
  scene: THREE.Scene
  sizes: Size
  material: THREE.ShaderMaterial
  geometry: THREE.PlaneGeometry
  mesh: THREE.Mesh
  nodeDimensions: Size
  meshDimensions: Size
  meshPostion: Position
  elementBounds: DOMRect
  currentScroll: number
  lastScroll: number
  scrollSpeed: number
  scrollTrigger: gsap.core.Tween
  onClickHandler: (e: PointerEvent) => void

  constructor({ element, scene, sizes }: Props) {
    this.element = element
    this.anchorElement = this.element.closest("a") as
      | HTMLAnchorElement
      | undefined
    this.scene = scene
    this.sizes = sizes

    this.currentScroll = 0
    this.lastScroll = 0
    this.scrollSpeed = 0

    this.createGeometry()
    this.createMaterial()
    this.createMesh()
    this.setNodeBounds()
    this.setMeshDimensions()
    this.setMeshPosition()
    this.setTexture()

    // Persist handler reference for correct removal on destroy
    this.onClickHandler = this.onClickLink.bind(this)
    this.anchorElement?.addEventListener("click", this.onClickHandler)

    this.scene.add(this.mesh)
  }

  onClickLink(e: PointerEvent) {
    ;(e.currentTarget as HTMLAnchorElement).setAttribute(
      "data-home-link-active",
      "true",
    )
  }

  createGeometry() {
    this.geometry = new THREE.PlaneGeometry(1, 1, 1, 1)
  }

  createMaterial() {
    this.material = new THREE.ShaderMaterial({
      vertexShader,
      fragmentShader,
      uniforms: {
        uTexture: new THREE.Uniform(new THREE.Vector4()),
        uResolution: new THREE.Uniform(new THREE.Vector2(0, 0)),
        uContainerRes: new THREE.Uniform(new THREE.Vector2(0, 0)),
        uProgress: new THREE.Uniform(0),
        uGridSize: new THREE.Uniform(20),
        uColor: new THREE.Uniform(new THREE.Color("#242424")),
      },
    })
  }

  createMesh() {
    this.mesh = new THREE.Mesh(this.geometry, this.material)
  }

  setNodeBounds() {
    this.elementBounds = this.element.getBoundingClientRect()

    this.nodeDimensions = {
      width: this.elementBounds.width,
      height: this.elementBounds.height,
    }
  }

  setMeshDimensions() {
    this.meshDimensions = {
      width: (this.nodeDimensions.width * this.sizes.width) / window.innerWidth,
      height:
        (this.nodeDimensions.height * this.sizes.height) / window.innerHeight,
    }

    this.mesh.scale.x = this.meshDimensions.width
    this.mesh.scale.y = this.meshDimensions.height
  }

  setMeshPosition() {
    this.meshPostion = {
      x: (this.elementBounds.left * this.sizes.width) / window.innerWidth,
      y: (-this.elementBounds.top * this.sizes.height) / window.innerHeight,
    }

    this.meshPostion.x -= this.sizes.width / 2
    this.meshPostion.x += this.meshDimensions.width / 2

    this.meshPostion.y -= this.meshDimensions.height / 2
    this.meshPostion.y += this.sizes.height / 2

    this.mesh.position.x = this.meshPostion.x
    this.mesh.position.y = this.meshPostion.y
  }

  setTexture() {
    this.material.uniforms.uTexture.value = new THREE.TextureLoader().load(
      this.element.src,
      ({ image }) => {
        const { naturalWidth, naturalHeight } = image

        this.material.uniforms.uResolution.value = new THREE.Vector2(
          naturalWidth,
          naturalHeight,
        )

        this.material.uniforms.uContainerRes.value = new THREE.Vector2(
          this.nodeDimensions.width,
          this.nodeDimensions.height,
        )
      },
    )
  }

  updateScroll(scrollY: number) {
    this.currentScroll = (-scrollY * this.sizes.height) / window.innerHeight

    const deltaScroll = this.currentScroll - this.lastScroll
    this.lastScroll = this.currentScroll

    this.updateY(deltaScroll)
  }

  updateY(deltaScroll: number) {
    this.meshPostion.y -= deltaScroll
    this.mesh.position.y = this.meshPostion.y
  }

  observe() {
    this.scrollTrigger = gsap.to(this.material.uniforms.uProgress, {
      value: 1,
      scrollTrigger: {
        trigger: this.element,
        start: "top bottom",
        end: "bottom top",
        toggleActions: "play reset restart reset",
      },
      duration: 1.6,
      ease: "linear",
    })
  }

  destroy() {
    this.scene.remove(this.mesh)
    this.scrollTrigger.scrollTrigger?.kill()
    this.scrollTrigger?.kill()
    this.anchorElement?.removeEventListener("click", this.onClickHandler)
    this.anchorElement?.removeAttribute("data-home-link-active")
    this.geometry.dispose()
    this.material.dispose()
  }

  onResize(sizes: Size) {
    this.sizes = sizes

    this.setNodeBounds()
    this.setMeshDimensions()
    this.setMeshPosition()

    this.material.uniforms.uContainerRes.value = new THREE.Vector2(
      this.nodeDimensions.width,
      this.nodeDimensions.height,
    )
  }
}
src/app/components/scroll.ts
파일 저장

import { ScrollSmoother } from "gsap/ScrollSmoother"
import { ScrollTrigger } from "gsap/ScrollTrigger"

export default class Scroll {
  scroll: number
  s: globalThis.ScrollSmoother | null

  constructor() {
    window.scrollTo(0, 0)
    this.init()
  }

  init() {
    this.scroll = 0

    // Initialize smoother with explicit content to ensure proper sync
    this.s = ScrollSmoother.create({
      smooth: 1,
      normalizeScroll: true,
      wrapper: document.getElementById("app") as HTMLElement,
      content: document.getElementById("smooth-content") as HTMLElement,
    })

    ScrollTrigger.refresh()
  }

  reset(immediate?: boolean) {
    if (immediate) this.s?.scrollTo(0, false, "top top")
    else this.s?.scrollTop(0)
  }

  destroy() {
    this.s?.kill()
    this.s = null
  }

  getScroll() {
    this.scroll = this.s?.scrollTop() || 0

    return this.scroll
  }
}
src/app/components/text-animation.ts
파일 저장

import gsap from "gsap"
import { SplitText } from "gsap/SplitText"

interface BaseAnimationProps {
  element: HTMLElement
  inDuration: number
  outDuration: number
  inDelay: number
}

interface SplitAnimationProps extends BaseAnimationProps {
  split: globalThis.SplitText
  inStagger?: number
  outStagger?: number
}

export default class TextAnimation {
  elements: HTMLElement[]
  splitAnimations: SplitAnimationProps[] = []
  fadeAnimations: BaseAnimationProps[] = []
  splitTweens: gsap.core.Tween[] = []
  fadeTweens: gsap.core.Tween[] = []
  ready: boolean = false

  constructor() {}

  init() {
    this.ready = true

    this.splitAnimations = []
    this.fadeAnimations = []

    this.elements = document.querySelectorAll(
      "[data-text-animation]",
    ) as unknown as HTMLElement[]

    this.elements.forEach((el) => {
      const inDuration = parseFloat(
        el.getAttribute("data-text-animation-in-duration") || "0.6",
      )

      const outDuration = parseFloat(
        el.getAttribute("data-text-animation-out-duration") || "0.3",
      )

      const inDelay = parseFloat(
        el.getAttribute("data-text-animation-in-delay") || "0",
      )

      // Check if this should be a split text animation
      if (el.hasAttribute("data-text-animation-split")) {
        const split = SplitText.create(el, {
          type: "lines",
          mask: "lines",
        })

        const inStagger = parseFloat(
          el.getAttribute("data-text-animation-in-stagger") || "0.06",
        )

        const outStagger = parseFloat(
          el.getAttribute("data-text-animation-out-stagger") || "0.06",
        )

        split.lines.forEach((line) => {
          gsap.set(line, { yPercent: 100 })
        })

        gsap.set(el, { autoAlpha: 1, visibility: "visible" })

        this.splitAnimations.push({
          element: el,
          split,
          inDuration,
          outDuration,
          inStagger,
          outStagger,
          inDelay,
        })
      } else {
        // Default fade animation
        gsap.set(el, { autoAlpha: 0, visibility: "hidden" })

        this.fadeAnimations.push({
          element: el,
          inDuration,
          outDuration,
          inDelay,
        })
      }
    })
  }

  animateIn({ delay = 0 } = {}) {
    // Split text animations

    this.splitAnimations.forEach(
      ({ element, split, inDuration, inStagger, inDelay }) => {
        const tweenWithScroll = gsap.to(split.lines, {
          yPercent: 0,
          stagger: inStagger,
          scrollTrigger: {
            trigger: element,
            start: "top bottom",
            end: "bottom top",
            toggleActions: "play reset restart reset",
          },
          ease: "expo",
          duration: inDuration,
          delay: inDelay + delay,
        })

        this.splitTweens.push(tweenWithScroll)
      },
    )

    // Fade animations
    this.fadeAnimations.forEach(({ element, inDuration, inDelay }) => {
      const fadeTween = gsap.to(element, {
        autoAlpha: 1,
        scrollTrigger: {
          trigger: element,
          start: "top bottom",
          end: "bottom top",
          toggleActions: "play reset restart reset",
        },
        ease: "power2.out",
        duration: inDuration,
        delay: inDelay + delay,
      })

      this.fadeTweens.push(fadeTween)
    })
    return gsap.timeline()
  }

  animateOut() {
    const tl = gsap.timeline()

    // Split animations
    this.splitAnimations.forEach(({ split, outDuration, outStagger }) => {
      tl.to(
        split.lines,
        {
          yPercent: 100,
          stagger: outStagger,
          ease: "power2.out",
          duration: outDuration,
        },
        0,
      )
    })

    // Fade animations
    this.fadeAnimations.forEach(({ element, outDuration }) => {
      tl.to(
        element,
        {
          autoAlpha: 0,
          ease: "power2.out",
          duration: outDuration,
        },
        0,
      )
    })

    return tl
  }

  onResize() {
    if (!this.ready) return

    this.destroy()
    this.init()

    this.animateIn()
  }

  destroy() {
    this.splitTweens.forEach((tween) => {
      tween.scrollTrigger?.kill()
      tween.kill()
    })

    this.fadeTweens.forEach((tween) => {
      tween.scrollTrigger?.kill()
      tween.kill()
    })

    this.splitAnimations.forEach(({ split }) => {
      split.revert()
    })

    this.splitTweens = []
    this.fadeTweens = []
  }
}
src/app/main.ts
파일 저장

import Canvas from "./components/canvas"
import Scroll from "./components/scroll"
//@ts-ignore
import barba from "@barba/core"

import { ScrollTrigger } from "gsap/ScrollTrigger"
import { ScrollSmoother } from "gsap/ScrollSmoother"
//@ts-ignore
import { Flip } from "gsap/Flip"
import gsap from "gsap"
import Media from "./components/media"
import { SplitText } from "gsap/SplitText"
import TextAnimation from "./components/text-animation"
import FontFaceObserver from "fontfaceobserver"

gsap.registerPlugin(ScrollTrigger, ScrollSmoother, Flip, SplitText)

class App {
  canvas: Canvas
  scroll: Scroll
  template: "home" | "detail"

  mediaHomeState: Flip.FlipState
  scrollBlocked: boolean = false
  scrollTop: number
  textAnimation: TextAnimation
  fontLoaded: boolean = false

  constructor() {
    if (typeof history !== "undefined" && "scrollRestoration" in history) {
      history.scrollRestoration = "manual"
    }

    this.scroll = new Scroll()
    this.canvas = new Canvas()
    this.textAnimation = new TextAnimation()
    this.loadFont(() => {
      this.textAnimation.init()
    })

    this.template = this.getCurrentTemplate()

    this.loadImages(() => {
      this.canvas.createMedias()
      if (this.fontLoaded) {
        this.textAnimation.init()
        this.textAnimation.animateIn()
      } else {
        window.addEventListener("fontLoaded", () => {
          gsap.delayedCall(0, () => {
            gsap.delayedCall(0, () => {
              this.textAnimation.init()
              this.textAnimation.animateIn({ delay: 0.3 })
            })
          })
        })
      }
    })

    let activeLinkImage: HTMLImageElement
    let scrollTop: number

    barba.init({
      prefetchIgnore: true,
      transitions: [
        {
          name: "default-transition",
          before: () => {
            this.scrollBlocked = true
            this.scroll.s?.paused(true)
          },
          leave: () => {
            const medias = this.canvas.medias && this.canvas.medias

            medias?.forEach((media) => {
              if (!media) return
              media.onResize(this.canvas.sizes)
              gsap.set(media.element, {
                visibility: "hidden",
                opacity: 0,
              })
            })

            return new Promise<void>((resolve) => {
              const tl = this.textAnimation.animateOut()

              this.canvas.medias?.forEach((media) => {
                if (!media) return
                tl.fromTo(
                  media.material.uniforms.uProgress,
                  { value: 1 },
                  {
                    duration: 1,
                    ease: "linear",
                    value: 0,
                  },
                  0,
                )
              })

              tl.call(() => {
                this.textAnimation.destroy()
                resolve()
              })
            })
          },
          beforeEnter: () => {
            this.canvas.medias?.forEach((media) => {
              media?.destroy()
              media = null
            })

            this.scrollBlocked = false

            this.scroll.reset()
            this.scroll.destroy()
          },
          after: () => {
            this.scroll.init()
            this.textAnimation.init()

            const template = this.getCurrentTemplate()
            this.setTemplate(template)

            this.loadImages(() => {
              this.canvas.medias = []
              this.canvas.createMedias()
              this.textAnimation.animateIn({ delay: 0.3 })
            })
          },
        },
        {
          name: "home-detail",
          from: {
            custom: () => {
              const activeLink = document.querySelector(
                'a[data-home-link-active="true"]',
              )
              if (!activeLink) return false

              return true
            },
          },
          before: () => {
            this.scrollBlocked = true
            this.scroll.s?.paused(true)

            const tl = this.textAnimation.animateOut()

            activeLinkImage = document.querySelector(
              'a[data-home-link-active="true"] img',
            ) as HTMLImageElement

            this.canvas.medias?.forEach((media) => {
              if (!media) return
              media.scrollTrigger.kill()

              const currentProgress = media.material.uniforms.uProgress.value
              const totalDuration = 1.2

              if (media.element !== activeLinkImage) {
                const remainingDuration = totalDuration * currentProgress

                tl.to(
                  media.material.uniforms.uProgress,
                  {
                    duration: remainingDuration,
                    value: 0,
                    ease: "linear",
                  },
                  0,
                )
              } else {
                const remainingDuration = totalDuration * (1 - currentProgress)

                tl.to(
                  media.material.uniforms.uProgress,
                  {
                    value: 1,
                    duration: remainingDuration,
                    ease: "linear",
                    onComplete: () => {
                      media.element.style.opacity = "1"
                      media.element.style.visibility = "visible"
                      gsap.set(media.material.uniforms.uProgress, { value: 0 })
                    },
                  },
                  0,
                )
              }
            })

            return new Promise<void>((resolve) => {
              tl.call(() => {
                resolve()
              })
            })
          },

          leave: () => {
            scrollTop = this.scroll.getScroll()

            const container = document.querySelector(
              ".container",
            ) as HTMLElement
            container.style.position = "fixed"
            container.style.top = `-${scrollTop}px`
            container.style.width = "100%"
            container.style.zIndex = "1000"

            this.mediaHomeState = Flip.getState(activeLinkImage)
            this.textAnimation.destroy()
          },
          beforeEnter: () => {
            this.scroll.reset()
            this.scroll.destroy()
          },
          after: () => {
            this.scroll.init()
            this.textAnimation.init()

            const detailContainer = document.querySelector(
              ".details-container",
            ) as HTMLElement

            detailContainer.innerHTML = ""
            detailContainer.append(activeLinkImage)

            const template = this.getCurrentTemplate()
            this.setTemplate(template)

            return new Promise<void>((resolve) => {
              let activeMedia: Media | null = null

              this.textAnimation.animateIn({ delay: 0.3 })

              Flip.from(this.mediaHomeState, {
                absolute: true,

                duration: 1,
                ease: "power3.inOut",

                onComplete: () => {
                  this.scrollBlocked = false
                  this.canvas.medias?.forEach((media) => {
                    if (!media) return
                    if (media.element !== activeLinkImage) {
                      media.destroy()
                      media = null
                    } else {
                      activeMedia = media
                    }
                  })

                  this.canvas.medias = [activeMedia]

                  resolve()
                },
              })
            })
          },
        },
      ],
    })

    window.addEventListener("resize", this.onResize.bind(this))

    this.render = this.render.bind(this)
    gsap.ticker.add(this.render)
  }

  getCurrentTemplate() {
    return document
      .querySelector("[data-page-template]")
      ?.getAttribute("data-page-template") as "home" | "detail"
  }

  setTemplate(template: string) {
    this.template = template as "home" | "detail"
  }

  loadImages(callback?: () => void) {
    const medias = document.querySelectorAll("img")
    let loadedImages = 0
    const totalImages = medias.length

    medias.forEach((img) => {
      if (img.complete) {
        loadedImages++
      } else {
        img.addEventListener("load", () => {
          loadedImages++
          if (loadedImages === totalImages) {
            this.onReady(callback)
          }
        })
      }
    })

    if (loadedImages === totalImages) {
      this.onReady(callback)
    }
  }

  onReady(callback?: () => void) {
    if (callback) callback()
    ScrollTrigger.refresh()
  }

  loadFont(onLoaded: () => void) {
    const satoshi = new FontFaceObserver("Satoshi")

    satoshi.load().then(() => {
      onLoaded()
      this.fontLoaded = true
      window.dispatchEvent(new Event("fontLoaded"))
    })
  }

  onResize() {
    this.textAnimation?.onResize()
    this.canvas?.onResize()
  }

  render() {
    this.scrollTop = this.scroll?.getScroll() || 0
    this.canvas?.render(this.scrollTop, !this.scrollBlocked)
  }
}

export default new App()
src/app/shaders/fragment.glsl
파일 저장

uniform sampler2D uTexture;
varying vec2 vUv;

uniform vec2 uResolution;
uniform float uProgress;
uniform vec3 uColor;

uniform vec2 uContainerRes;
uniform float uGridSize;

float random (vec2 st) {
    return fract(sin(dot(st.xy,
                         vec2(12.9898,78.233)))*
        43758.5453123);
}

vec2 squaresGrid(vec2 vUv)
{
    float imageAspectX = 1.;
    float imageAspectY = 1.;

    float containerAspectX = uResolution.x/uResolution.y;
    float containerAspectY = uResolution.y/uResolution.x;

    vec2 ratio = vec2(
        min(containerAspectX / imageAspectX, 1.0),
        min(containerAspectY / imageAspectY, 1.0)
    );

    vec2 squareUvs = vec2(
        vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
        vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
    );

    return squareUvs;
}


void main()
{
            
    vec2 newUvs = vUv;            

    float imageAspectX = uResolution.x/uResolution.y;
    float imageAspectY = uResolution.y/uResolution.x;
    
    float containerAspectX = uContainerRes.x/uContainerRes.y;
    float containerAspectY = uContainerRes.y/uContainerRes.x;

    vec2 ratio = vec2(
        min(containerAspectX / imageAspectX, 1.0),
        min(containerAspectY / imageAspectY, 1.0)
    );

    vec2 coverUvs = vec2(
        vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
        vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
    );


    //generate grid
    vec2 squareUvs = squaresGrid(coverUvs);
    float gridSize = floor(uContainerRes.x/20.);
    vec2 grid = vec2(floor(squareUvs.x*gridSize)/gridSize,floor(squareUvs.y*gridSize)/gridSize);
    vec4 gridTexture = vec4(uColor,0.);
    

    //image texture    
    vec4 texture = texture2D(uTexture,coverUvs);
    float height = 0.2;

    float progress = (1.+height)-(uProgress*(1.+height+height)); //goes from 1+height to -height


    float dist = 1.-distance(grid.y,progress);

    float clampedDist = smoothstep(height,0.,distance(grid.y,progress));

    float randDist=step(1.-height*random(grid),dist);
    dist=step(1.-height,dist);
    
    float rand = random(grid); 

    float alpha = dist*(clampedDist+rand-0.5*(1.-randDist));
    alpha=max(0.,alpha);
    gridTexture.a = alpha;


    texture.rgba *= step(progress,grid.y);
    
    gl_FragColor = vec4(mix(texture,gridTexture,gridTexture.a));
}
src/app/shaders/vertex.glsl
파일 저장

varying vec2 vUv;

void main()
{ 
    vUv=uv;

    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);

}
src/app/types/types.ts
파일 저장

export interface Size {
  width: number
  height: number
}

export interface Dimensions {
  width: number
  height: number
  pixelRatio: number
}

export interface Position {
  x: number
  y: number
}
src/components/arrow.astro

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

src/components/header.astro

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

src/layouts/Layout.astro

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

src/pages/[index].astro

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

src/pages/index.astro

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

src/styles/index.css
파일 저장

@font-face {
  font-family: "Satoshi";
  src: url("/fonts/satoshi/Satoshi-Variable.ttf") format("truetype");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  font-size: 13px;
  --color-text: #000000;
  --color-bg: #ffffff;
  --color-link: #000000;
  --color-link-hover: #000000;
  --page-padding: 2rem;
}

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  margin: 0;
  color: var(--color-text);
  background-color: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-y: scroll;
  font-family:
    "Satoshi",
    system-ui,
    -apple-system,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Arial,
    "Noto Sans",
    "Liberation Sans",
    sans-serif;
}

a {
  text-decoration: none;
  color: var(--color-link);
  outline: none;
  cursor: pointer;

  &:hover {
    text-decoration: none;
    color: var(--color-link-hover);
  }

  &:focus {
    outline: none;
    background: lightgrey;

    &:not(:focus-visible) {
      background: transparent;
    }

    &:focus-visible {
      outline: 2px solid red;
      background: transparent;
    }
  }
}

.frame {
  padding: 1rem var(--page-padding);
  display: grid;
  z-index: 1000;
  position: relative;
  grid-row-gap: 1rem;
  grid-column-gap: 2rem;
  pointer-events: none;
  justify-items: start;
  grid-template-columns: auto auto auto 1fr;
  grid-template-areas:
    "title title title title"
    "back archive github ..."
    "tags tags tags tags"
    "sponsor sponsor sponsor sponsor";

  #cdawrap {
    justify-self: start;
    grid-area: sponsor;
  }

  a,
  button {
    pointer-events: auto;
  }

  .frame__title {
    grid-area: title;
    font-size: inherit;
    margin: 0;
    font-weight: bold;
  }

  .frame__back {
    grid-area: back;
    justify-self: start;
  }

  .frame__archive {
    grid-area: archive;
    justify-self: start;
  }

  .frame__github {
    grid-area: github;
  }

  .frame__tags {
    grid-area: tags;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
  }

  @media screen and (min-width: 53em) {
    grid-template-columns: auto auto auto auto 1fr;
    grid-template-rows: auto auto;
    align-content: space-between;
    grid-template-areas:
      "title back github archive demos"
      "tags tags tags sponsor sponsor";

    .frame__tags {
      align-self: end;
    }

    .frame__title {
      padding-right: 2rem;
    }

    #cdawrap {
      justify-self: end;
      text-align: right;
      max-width: 300px;
    }
  }
}

#app {
  position: relative;
  z-index: 10;
}

#webgl {
  position: fixed;
  z-index: 0;
  inset: 0;
  top: 0;
  left: 0;
  pointer-events: none;
}

h1 {
  font-size: clamp(2rem, 5vw, 5rem);
  font-weight: 400;
  text-transform: uppercase;
  line-height: 100%;
  align-self: end;
  padding-bottom: 1.5rem;
  padding-left: 1rem;
}

.container {
  display: flex;
  flex-direction: column;
}

.grid-container {
  display: flex;
  flex-direction: column;
}

.grid {
  display: grid;
  padding: 10vh var(--page-padding);
  column-gap: 1rem;
  row-gap: 30vh;
  grid-template-columns: repeat(9, 1fr);
}

.grid__item {
  grid-column: var(--c) / span var(--s);
  grid-row: var(--r);
  aspect-ratio: var(--ar);
}

.grid__item p {
  padding-top: 0.5rem;
}

.container img {
  width: 100%;
  opacity: 0;
}

.details {
  max-width: 100%;
  padding: var(--page-padding);
}

.details header {
  padding-top: 3vmax;
}

.details-container {
  width: 100%;
  height: 100dvh;
  overflow: hidden;
}

.details img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

.details-data {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  gap: 2rem;
  padding-bottom: 0.5rem;
  align-items: end;
}

[data-text-animation],
[data-icon] {
  visibility: hidden;
}

.related {
  container-type: inline-size;
  padding-top: 40vh;
}

.related p {
  text-align: center;
  padding-top: 3rem;
}

.relgrid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
  padding: 5rem var(--page-padding) 10vh;
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
}

.relgrid__item {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  overflow: hidden;
}

.relgrid__item-img {
  width: 100%;
  aspect-ratio: 4 / 3;
  background-size: cover;
  background-position: center;
  filter: grayscale(1) brightness(0.8);
  transition: all 0.5s ease;
}

.relgrid__item:hover .relgrid__item-img {
  filter: grayscale(0) brightness(1);
}

.relgrid__item-title {
  padding: 0.5rem 0;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.4;
}

.relgrid__item:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 4px;
}

@container (max-width: 800px) {
  .relgrid {
    grid-template-columns: 1fr;
  }
  .relgrid__item-img {
    display: none;
  }
  .relgrid__item-title {
    padding: 0.25rem;
    line-height: 1;
  }
}
Original author attribution실행 안내·자료
파일 저장

Building a Scroll-Revealed WebGL Gallery with GSAP, Three.js, Astro and Barba.js
Original author: Chakib Mazouni
Published by Codrops. Copyright (c) 2026 Codrops.
License: MIT, pursuant to the publisher's downloadable-demo grant.
Keep CODROPS-MIT-2026.txt and the original project notices with copies.
Third-party media exceptions and credits are recorded separately in the source inventory.
Media credits and license evidence실행 안내·자료
파일 저장

README.md
## Credits

- Images generated with [Midjourney](https://midjourney.com)


## License

[MIT](LICENSE)
.preview/third-party-notices/@barba--core/LICENSE실행 안내·자료
파일 저장

MIT License

Copyright (c) 2024 Luigi De Rosa, Thierry Michel, Xavier Foucrier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
.preview/third-party-notices/astro/LICENSE실행 안내·자료
파일 저장

MIT License

Copyright (c) 2021 Fred K. Schott

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

"""
This license applies to parts of the `packages/create-astro` and `packages/astro` subdirectories originating from the https://github.com/sveltejs/kit repository:

Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

"""
This license applies to parts of the `packages/create-astro` and `packages/astro` subdirectories originating from the https://github.com/vitejs/vite repository:

MIT License

Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
.preview/third-party-notices/fontfaceobserver/LICENSE실행 안내·자료
파일 저장

Copyright (c) 2014 - Bram Stein

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.preview/third-party-notices/is-promise/LICENSE실행 안내·자료
파일 저장

Copyright (c) 2014 Forbes Lindesay

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
.preview/third-party-notices/path-to-regexp/LICENSE실행 안내·자료
파일 저장

The MIT License (MIT)

Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
.preview/third-party-notices/three/LICENSE실행 안내·자료
파일 저장

The MIT License

Copyright © 2010-2025 three.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Bundled dependency licenses실행 안내·자료
파일 저장

Isolated framework dependency inventory
{
  "id": "codrops-b7b34d81bf95",
  "packages": [
    {
      "package": "astro",
      "version": "5.16.15",
      "declaredLicense": "MIT",
      "noticeFiles": [
        ".preview/third-party-notices/astro/LICENSE"
      ],
      "noticeScope": "Original installed package notices; not a replacement license grant."
    },
    {
      "package": "@barba/core",
      "version": "2.10.3",
      "declaredLicense": "MIT",
      "noticeFiles": [
        ".preview/third-party-notices/@barba--core/LICENSE"
      ],
      "noticeScope": "Original installed package notices; not a replacement license grant."
    },
    {
      "package": "fontfaceobserver",
      "version": "2.3.0",
      "declaredLicense": "BSD-2-Clause",
      "noticeFiles": [
        ".preview/third-party-notices/fontfaceobserver/LICENSE"
      ],
      "noticeScope": "Original installed package notices; not a replacement license grant."
    },
    {
      "package": "gsap",
      "version": "3.14.2",
      "declaredLicense": "Standard 'no charge' license: https://gsap.com/standard-license.",
      "noticeFiles": [
        ".preview/third-party-notices/gsap/README.md",
        ".preview/third-party-notices/gsap/package.json",
        ".preview/third-party-notices/gsap/source-copyright-header.txt"
      ],
      "noticeScope": "Original installed package notices; not a replacement license grant."
    },
    {
      "package": "three",
      "version": "0.182.0",
      "declaredLicense": "MIT",
      "noticeFiles": [
        ".preview/third-party-notices/three/LICENSE"
      ],
      "noticeScope": "Original installed package notices; not a replacement license grant."
    },
    {
      "package": "is-promise",
      "version": "4.0.0",
      "declaredLicense": "MIT",
      "noticeFiles": [
        ".preview/third-party-notices/is-promise/LICENSE"
      ],
      "noticeScope": "Original installed package notices; not a replacement license grant."
    },
    {
      "package": "path-to-regexp",
      "version": "6.3.0",
      "declaredLicense": "MIT",
      "noticeFiles": [
        ".preview/third-party-notices/path-to-regexp/LICENSE"
      ],
      "noticeScope": "Original installed package notices; not a replacement license grant."
    }
  ],
  "publisherNotice": ".preview/CODROPS-MIT.txt",
  "authorAttribution": ".preview/ATTRIBUTION.txt"
}

.preview/ATTRIBUTION.txt
Building a Scroll-Revealed WebGL Gallery with GSAP, Three.js, Astro and Barba.js
Original author: Chakib Mazouni
Published by Codrops. Copyright (c) 2026 Codrops.
License: MIT, pursuant to the publisher's downloadable-demo grant.
Keep CODROPS-MIT-2026.txt and the original project notices with copies.
Third-party media exceptions and credits are recorded separately in the source inventory.


.preview/third-party-notices/@barba--core/LICENSE
MIT License

Copyright (c) 2024 Luigi De Rosa, Thierry Michel, Xavier Foucrier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


.preview/third-party-notices/astro/LICENSE
MIT License

Copyright (c) 2021 Fred K. Schott

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

"""
This license applies to parts of the `packages/create-astro` and `packages/astro` subdirectories originating from the https://github.com/sveltejs/kit repository:

Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

"""
This license applies to parts of the `packages/create-astro` and `packages/astro` subdirectories originating from the https://github.com/vitejs/vite repository:

MIT License

Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""


.preview/third-party-notices/fontfaceobserver/LICENSE
Copyright (c) 2014 - Bram Stein

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


.preview/third-party-notices/gsap/README.md
# GSAP (GreenSock Animation Platform)

[![GSAP - Animate anything](https://gsap.com/GSAP-share-image.png)](https://gsap.com)

GSAP is a **framework-agnostic** JavaScript animation library that turns developers into animation superheroes. Build high-performance animations that work in **every** major browser. Animate CSS, SVG, canvas, React, Vue, WebGL, colors, strings, motion paths, generic objects... anything JavaScript can touch! GSAP's <a href="https://gsap.com/docs/v3/Plugins/ScrollTrigger/">ScrollTrigger</a> plugin delivers jaw-dropping scroll-based animations with minimal code. <a href="https://gsap.com/docs/v3/GSAP/gsap.matchMedia()">gsap.matchMedia()</a> makes building responsive, accessibility-friendly animations a breeze.

No other library delivers such advanced sequencing, reliability, and tight control while solving real-world problems on over 12 million sites. GSAP works around countless browser inconsistencies; your animations ***just work***. At its core, GSAP is a high-speed property manipulator, updating values over time with extreme accuracy. It's up to 20x faster than jQuery!

GSAP is completely flexible; sprinkle it wherever you want. **Zero dependencies.**

There are many optional <a href="https://gsap.com/docs/v3/Plugins">plugins</a> and <a href="https://gsap.com/docs/v3/Eases">easing</a> functions for achieving advanced effects easily like <a href="https://gsap.com/docs/v3/Plugins/ScrollTrigger/">scrolling</a>, <a href="https://gsap.com/docs/v3/Plugins/MorphSVGPlugin">morphing</a>, [text splitting](https://gsap.com/docs/v3/Plugins/SplitText), animating along a <a href="https://gsap.com/docs/v3/Plugins/MotionPathPlugin">motion path</a> or <a href="https://gsap.com/docs/v3/Plugins/Flip/">FLIP</a> animations. There's even a handy <a href="https://gsap.com/docs/v3/Plugins/Observer/">Observer</a> for normalizing event detection across browsers/devices. 


### Get Started

[![Get Started with GSAP](https://gsap.com/_img/github/get-started.jpg)](https://gsap.com/get-started)


## Docs &amp; Installation

View the <a href="https://gsap.com/docs">full documentation here</a>, including an <a href="https://gsap.com/install">installation guide</a>.

### CDN

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/gsap.min.js"></script>
```

See <a href="https://www.jsdelivr.com/gsap">JSDelivr's dedicated GSAP page</a> for quick CDN links to the core files/plugins. There are more <a href="https://gsap.com/install">installation instructions</a> at gsap.com.

**Every major ad network excludes GSAP from file size calculations** and most have it on their own CDNs, so contact them for the appropriate URL(s). 

### NPM
See the <a href="https://gsap.com/install">guide to using GSAP via NPM here</a>.

```javascript
npm install gsap
```

GSAP's core can animate almost anything including CSS and attributes, plus it includes all of the <a href="https://gsap.com/docs/v3/GSAP/UtilityMethods">utility methods</a> like <a href="https://gsap.com/docs/v3/GSAP/UtilityMethods/interpolate()">interpolate()</a>, <a href="https://gsap.com/docs/v3/GSAP/UtilityMethods/mapRange()">mapRange()</a>, most of the <a href="https://gsap.com/docs/v3/Eases">eases</a>, and it can do snapping and modifiers. 

```javascript
// typical import
import gsap from "gsap";

// get other plugins:
import ScrollTrigger from "gsap/ScrollTrigger";
import Flip from "gsap/Flip";
import Draggable from "gsap/Draggable";

// or all tools are exported from the "all" file (excluding members-only plugins):
import { gsap, ScrollTrigger, Draggable, MotionPathPlugin } from "gsap/all";

// don't forget to register plugins
gsap.registerPlugin(ScrollTrigger, Draggable, Flip, MotionPathPlugin); 
```

The NPM files are ES modules, but there's also a /dist/ directory with <a href="https://www.davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/">UMD</a> files for extra compatibility.

## GSAP is FREE! 

Thanks to [Webflow](https://webflow.com), GSAP is now **100% FREE** including ALL of the bonus plugins like [SplitText](https://gsap.com/docs/v3/Plugins/SplitText), [MorphSVG](https://gsap.com/docs/v3/Plugins/MorphSVGPlugin), and all the others that were exclusively available to Club GSAP members. That's right - the entire GSAP toolset is FREE, even for commercial use! 🤯  Read more [here](https://webflow.com/blog/gsap-becomes-free)

### ScrollTrigger &amp; ScrollSmoother

If you're looking for scroll-driven animations, GSAP's <a href="https://gsap.com/docs/v3/Plugins/ScrollTrigger/">ScrollTrigger</a> plugin is the standard. There's a companion <a href="https://gsap.com/docs/v3/Plugins/ScrollSmoother/">ScrollSmoother</a> as well.

[![ScrollTrigger](https://gsap.com/_img/github/scrolltrigger.jpg)](https://gsap.com/docs/v3/Plugins/ScrollTrigger)

### Using React? 

There's a <a href="https://www.npmjs.com/package/@gsap/react">@gsap/react</a> package that exposes a `useGSAP()` hook which is a drop-in replacement for `useEffect()`/`useLayoutEffect()`, automating cleanup tasks. Please read the <a href="https://gsap.com/react">React guide</a> for details.

### Resources

* <a href="https://gsap.com/">gsap.com</a>
* <a href="https://gsap.com/get-started/">Getting started guide</a>
* <a href="https://gsap.com/docs/">Docs</a>
* <a href="https://gsap.com/demos">Demos &amp; starter templates</a>
* <a href="https://gsap.com/community/">Community forums</a>
* <a href="https://gsap.com/docs/v3/Eases">Ease Visualizer</a>
* <a href="https://gsap.com/showcase">Showcase</a>
* <a href="https://www.youtube.com/@GreenSockLearning">YouTube Channel</a>
* <a href="https://gsap.com/cheatsheet">Cheat sheet</a>
* <a href="https://webflow.com">Webflow</a>

### Need help?
Ask in the friendly <a href="https://gsap.com/community/">GSAP forums</a>. Or share your knowledge and help someone else - it's a great way to sharpen your skills! Report any bugs there too (or <a href="https://github.com/greensock/GSAP/issues">file an issue here</a> if you prefer).

### License
GreenSock's standard "no charge" license can be viewed at <a href="https://gsap.com/standard-license">https://gsap.com/standard-license</a>.

Copyright (c) 2008-2025, GreenSock. All rights reserved.

.preview/third-party-notices/gsap/source-copyright-header.txt
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }

/*!
 * GSAP 3.14.2
 * https://gsap.com
 *
 * @license Copyright 2008-2025, GreenSock. All rights reserved.
 * Subject to the terms at https://gsap.com/standard-license
 * @author: Jack Doyle, jack@greensock.com
*/


.preview/third-party-notices/is-promise/LICENSE
Copyright (c) 2014 Forbes Lindesay

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

.preview/third-party-notices/path-to-regexp/LICENSE
The MIT License (MIT)

Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


.preview/third-party-notices/three/LICENSE
The MIT License

Copyright © 2010-2025 three.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.