<div class="fish-wrapper">
	<div class="fish">
		<div class="fish__skeleton"></div>
		<div class="fish__inner">
			<!--body-->
			<div class="fish__body"></div>
			<div class="fish__body"></div>
			<div class="fish__body"></div>
			<div class="fish__body"></div>

			<!--head-->
			<div class="fish__head"></div>
			<div class="fish__head fish__head--2"></div>
			<div class="fish__head fish__head--3"></div>
			<div class="fish__head fish__head--4"></div>

			<div class="fish__tail-main"></div>
			<div class="fish__tail-fork"></div>

			<div class="fish__fin"></div>
			<div class="fish__fin fish__fin--2"></div>
		</div>
	</div>
</div>

<div class="bubbles">
	<div class="bubbles__inner">
		<div class="bubbles__bubble"></div>
		<div class="bubbles__bubble"></div>
		<div class="bubbles__bubble"></div>
	</div>
</div>

<div class="content">

	
</div>
@import url('https://fonts.googleapis.com/css2?family=Judson&display=swap');

* {
	box-sizing: border-box;
}

body {
	font-family: 'Judson', serif;
	background: linear-gradient(to bottom,
		rgba(99, 167, 191, 1),
		rgba(94, 86, 179, 1),
		);
	max-width: 100vw;
	min-height: 100vh;
	overflow-x: hidden;
	color: white;
	position: relative;
	margin: 0;
	
	&::after {
		position: fixed;
		content: '';
		pointer-events: none;
		top: 0;
		left: 0;
		width: 100%;
		height: 100vh;
		background: radial-gradient(circle at center, transparent, rgba(0, 0, 0, 0.5));
	}
	
	@media (min-width: 40em) {
		font-size: 2rem;
	}
}

.rays {
	--r: 10deg;
	--c: rgba(255, 251, 227, 0.2);
	--size: max(60vh, 80rem);
	--mask: radial-gradient(circle at center, black, transparent 50%);
	position: fixed;
	pointer-events: none;
	top: calc(var(--size) * -0.55);
	left: 50%;
	width: var(--size);
	height: var(--size);
	pointer-events: none;
	
	> div {
		width: 100%;
		height: 100%;
		border-radius: 50%;
		background: repeating-conic-gradient(var(--c), var(--c) var(--r), transparent var(--r), transparent calc(var(--r) * 2));
		// -webkit-mask-image: var(--mask);
		// mask-image: var(--mask);
		// animation: raysRotate 120000ms linear infinite;
	}
}

@keyframes raysRotate {
	50% {
		transform: rotate(180deg) scale(1.5);
	}
	100% {
		transform: rotate(360deg) scale(1);
	}
}

.fish-wrapper {
	--mask: linear-gradient(180deg, rgba(0, 0, 0, 1.0), transparent);
	width: 100%;
	height: 100vh;
	position: fixed;
	top: 0;
	left: 0;
	perspective: 100rem;
	perspective-origin: center center;
	transform-style: preserve-3d;
	pointer-events: none;
	-webkit-mask-image: var(--mask);
	mask-image: var(--mask);
	z-index: 2;
}

.fish {
	--bodyW: 4rem;
	--o: 0.95;
	--l: 100%;
	--c: hsla(250deg, 50%, var(--l), var(--o, 0.6));
	position: relative;
	width: 20rem;
	height: 20rem;
	transform-style: preserve-3d;
	transform-origin: center;
	transform: translate3d(10rem, 5rem, 0) rotateX(20deg) rotateY(0deg);
}

.fish__skeleton {
	--clip: polygon(0 4rem, 45% 0, 55% 0, 100% 4rem, 50% 100%);
	position: absolute;
	width: 100%;
	height: 100%;
	background: repeating-linear-gradient(0deg, var(--c), var(--c) 0.1rem, transparent 0, transparent 0.5rem), linear-gradient(var(--c) 4rem, transparent 4rem), linear-gradient(90deg, transparent 1.9rem, var(--c) 0, var(--c) 2.1rem, transparent 0);
	top: 1rem;
	left: 3rem;
	width: var(--bodyW);
	height: 16rem;
	-webkit-clip-path: var(--clip);
	clip-path: var(--clip);
	opacity: 0;
	transform: translate3d(0, 0, -2rem) rotate(90deg);
	transform-origin: center center;
}

.fish__inner {
	--a: 9.5deg;
	width: 6rem;
	height: 20rem;
	transform-style: preserve-3d;
	transform: rotate(90deg);
}

.fish__body {
	--l: 75%;
	--c: hsla(250deg, 50%, var(--l), var(--o, 0.6));
	position: absolute;
	top: 4rem;
	left: 0;
	width: var(--bodyW);
	height: 12rem;
	background: var(--c);
	clip-path: polygon(0 0, 100% 0, 50% 100%);
	transform: translateZ(-2rem) rotateX(var(--a));
	transform-origin: center top;
	
	&:nth-child(2) {
		--i: 2;
		--l: 75%;
		transform: translateZ(2rem) rotateX(calc(var(--a) * -1));
	}
	
	&:nth-child(3) {
		--i: 3;
		--l: 95%;
		transform: rotateY(90deg) translate3d(-2rem, 0, 0) rotateX(var(--a));
		transform-origin: left top;
	}
	
	&:nth-child(4) {
		--i: 4;
		--l: 50%;
		transform: rotateY(90deg) translate3d(2rem, 0, 0) rotateX(calc(var(--a) * -1));
		transform-origin: right top;
	}
}

.fish__head {
	--a: 23.5deg;
	--l: 85%;
	--c: hsla(250deg, 50%, var(--l), var(--o, 0.6));
	position: absolute;
	top: 0;
	left: 0;
	width: var(--bodyW);
	height: 4rem;
	background: var(--c);
	clip-path: polygon(40% 0, 60% 0, 100% 100%, 0 100%);
	transform: translateZ(2rem) rotateX(var(--a));
	transform-origin: center bottom;
	
	&--2 {
		--i: 2;
		--l: 80%;
		transform: translateZ(-2rem) rotateX(calc(var(--a) * -1));
	}
	
	&--3 {
		--i: 3;
		--l: 90%;
		transform: rotateY(90deg) translate3d(-2rem, 0, 0) rotateX(calc(var(--a) * -1));
		transform-origin: left bottom;
	}
	
	&--4 {
		--l: 55%;
		transform: rotateY(90deg) translate3d(2rem, 0, 0) rotateX(var(--a));
		transform-origin: right bottom;
	}
}

.fish__tail-main {
	--o: 0.9;
	--l: 90%;
	--c: hsla(250deg, 50%, var(--l), var(--o, 0.6));
	width: 4rem;
	height: 4rem;
	background-color: var(--c);
	position: absolute;
	left: 0;
	bottom: 4rem;
	clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

.fish__tail-fork {
	--o: 0.9;
	--l: 95%;
	--c: hsla(250deg, 50%, var(--l), var(--o, 0.6));
	width: 4rem;
	height: 4rem;
	background-color: var(--c);
	position: absolute;
	left: 0;
	bottom: 0;
	clip-path: polygon(0 0, 100% 0, 100% 70%, 90% 100%, 70% 70%, 50% 30%, 30% 70%, 10% 100%, 0 70%);
	transform-origin: top center;
	transform: rotateX(-45deg);
	animation: tail 1000ms infinite alternate;
}

.fish__fin {
	width: 1.5rem;
	height: 4rem;
	background-color: var(--c);
	position: absolute;
	top: 6rem;
	left: 1.5rem;
	clip-path: polygon(50% 0, 100% 30%, 100% 60%, 50% 100%, 0 60%, 0 30%);
	transform-origin: top center;
	transform: translateZ(2rem) rotateY(0deg) rotateX(5deg) rotate(10deg);
	animation: fin 1500ms infinite alternate linear;
	
	&--2 {
		transform: translateZ(-2rem) rotateY(0deg) rotateX(-5deg) rotate(10deg);
		animation: fin2 1500ms infinite alternate linear;
	}
}

@keyframes tail {
	to {
		transform: rotateX(45deg);
	}
}

@keyframes fin {
	100% {
		transform: translateZ(2rem) rotateY(10deg) rotateX(20deg) rotate(-10deg);
	}
}

@keyframes fin2 {
	100% {
		transform: translateZ(-2rem) rotateY(-10deg) rotateX(-20deg) rotate(-10deg);
	}
}

/* Lights */
.lights {
	position: fixed;
	pointer-events: none;
	top: 0;
	left: 0;
	width: 100%;
	height: 100vh;
}

.lights__group {
	position: relative;
	height: 100%;
}

.lights__light {
	--size: 0.35rem;
	width: var(--size);
	height: var(--size);
	position: absolute;
	background: rgba(255, 255, 255, 1);
	border-radius: 100%;
	top: 10%;
	left: 25%;
	filter: blur(0.1rem);
	animation: blink 2500ms var(--d, 0ms) infinite alternate;
	
	&:nth-child(2) {
		--d: 200ms;
		top: 40%;
		left: 12%;
	}
	
	&:nth-child(3) {
		--d: 350ms;
		top: 60%;
		left: 18%;
	}
	
	&:nth-child(4) {
		--d: 600ms;
		top: 25%;
		left: 66%;
	}
	
	&:nth-child(5) {
		--d: 1210ms;
		top: 43%;
		left: 55%;
	}
	
	&:nth-child(6) {
		--d: 420ms;
		top: 90%;
		left: 37%;
	}
	
	&:nth-child(7) {
		--d: 1100ms;
		top: 82%;
		left: 91%;
	}
	
	&:nth-child(8) {
		--d: 1560ms;
		top: 67%;
		left: 81%;
	}
}

@keyframes blink {
	to {
		opacity: 0;
	}
}


.content {
	position: relative;
	z-index: 1;
	padding-bottom: 100vh;
}

section {
	height: 100vh;
	width: 100%;
	margin-top: 100vh;
	
	&:nth-child(4n),
	&:nth-child(4n - 1) {
		--col: 3;
	}
}

.section__content {
	width: 100%;
	position: fixed;
	top: 0;
	height: 100vh;
	display: flex;
	justify-content: center;
	align-items: center;
	padding: 1rem;
	
	@media (min-width: 50rem) {
		display: grid;
		grid-template-columns: repeat(3, minmax(0, 25rem));
		gap: 2rem;
		padding: 3rem;
		
		> * {
			grid-column: var(--col, 1);
			opacity: 0;
		}
	}
}

.bubbles {
	position: fixed;
	top: 0;
	left: 5rem;
	// transform-style: preserve-3d;
	transform-origin: center;
	transform: translate3d(10rem, 5rem, 0) rotateX(20deg) rotateY(0deg)
}

.bubbles__inner {
	width: 10rem;
	height: 10rem;
}

.bubbles__bubble {
	--c: rgba(255, 255, 255, 0.4);
	--size: 2.5rem;
	position: absolute;
	width: var(--size);
	height: var(--size);
	border-radius: 50%;
	background: radial-gradient(transparent 30%, var(--c, white)), radial-gradient(circle at 100% 0%, transparent 30%, var(--c, white));
	transform-origin: center;
	transform: scale(0);
	opacity: 0;
	
	&:nth-child(2) {
		--size: 1.8rem;
		top: 3rem;
		left: 2rem;
	}
	
	&:nth-child(3) {
		--size: 1.2rem;
		top: 6rem;
		left: 0;
	}
}

.indicator {
	text-align: center;
	position: fixed;
	bottom: 1rem;
	left: 50%;
	transform: translate3d(-50%, 0, 0);
	font-size: 1.2rem;
	
	span {
		display: block;
		
		&:nth-child(2) {
			animation: arrowMove 600ms infinite alternate;
		}
	}
}

@keyframes arrowMove {
	to {
		transform: translate3d(0, 0.5rem, 0);
	}
}
View Compiled
gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(MotionPathPlugin)

const sections = [...document.querySelectorAll('section')]
const fish = document.querySelector('.fish')
const fishHeadAndBody = 
			[
				...document.querySelectorAll('.fish__head'),
				...document.querySelectorAll('.fish__body')
			]
const lights = [...document.querySelectorAll('[data-lights]')]
const rays = document.querySelector('[data-rays]')
const path = [
			// 1
			{ x: 800, y: 200 },
			{ x: 900, y: 20 },
			{ x: 1100, y: 100 },
			
			// 2
			{ x: 1000, y: 200 },
			{ x: 900, y: 20 },
			{ x: 10, y: 500 },
			
			// 3
			{ x: 100, y: 300 },
			{ x: 500, y: 400 },
			{ x: 1000, y: 200 },
			
			// 4
			{ x: 1100, y: 300 },
			{ x: 400, y: 400 },
			{ x: 200, y: 250 },
			
			// 5
			{ x: 100, y: 300 },
			{ x: 500, y: 450 },
			{ x: 1100, y: 500 }
		]

const bubbles = gsap.timeline()
bubbles.set('.bubbles__bubble', {
	y: 100,
})
bubbles.to('.bubbles__bubble', {
	scale: 1.2,
	y: -300,
	opacity: 1,
	duration: 2,
	stagger: 0.2,
})
bubbles.to('.bubbles__bubble', {
	scale: 1,
	opacity: 0,
	duration: 1,
}, '-=1')

bubbles.pause()

const tl = gsap.timeline({repeat:-1})
tl.to(fish, {
	motionPath: {
		path: path,
		align: 'self',
		alignOrigin: [0.5, 0.5],
		autoRotate: true
	},
	duration: 10,
	immediateRender: true,
	// ease: 'none'
})
tl.to('.indicator', {
	opacity: 0
}, 0)
tl.to(fish, {
	rotateX: 180
}, 1)
tl.to(fish, {
	rotateX: 0
}, 2.5)
tl.to(fish, {
	z: -500,
	duration: 2,
}, 2.5)
tl.to(fish, {
	rotateX: 180
}, 4)
tl.to(fish, {
	rotateX: 0
}, 5.5)
tl.to(fish, {
	z: -50,
	duration: 2,
}, 5)
tl.to(fish, {
	rotate: 0,
	duration: 1,
}, '-=1')
tl.to('.fish__skeleton', {
	opacity: 0.6,
	duration: 0.1,
	repeat: 4
}, '-=3')
tl.to(fishHeadAndBody, {
	opacity: 0,
	duration: 0.1,
	repeat: 4
}, '-=3')
tl.to('.fish__inner', {
	opacity: 0.1,
	duration: 1
}, '-=1')
tl.to('.fish__skeleton', {
	opacity: 0.1,
	duration: 1
}, '-=1')

bubbles.play()
// tl.pause()


const rotateFish = (self) => {
	if (self.direction === -1) {
		gsap.to(fish, { rotationY: 180, duration: 0.4 })
	} else {
		gsap.to(fish, { rotationY: 0, duration: 0.4 })
	}
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.5.1/gsap.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/ScrollTrigger.min.js
  3. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/MotionPathPlugin.min.js
  4. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/MotionPathHelper.min.js?v=46