Pages Hard
Multimedia Engineer Portfolio
Bold creative technologist portfolio with Canvas 2D particle waveform reacting to mouse, GSAP FLIP grid/list gallery toggle, discipline pills ticker, audio waveform visualizer, and glitch-hover name effect.
Open in Lab
MCP
gsap flip canvas-2d lenis scrolltrigger
Targets: JS HTML
Code
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
MULTIMEDIA ENGINEER PORTFOLIO โ styles.css
Dark, bold, magazine-meets-club-flyer aesthetic
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
/* โโ Custom Properties โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
:root {
--bg: #0c0c14;
--text: #ffffff;
--accent-1: #ff3cac; /* hot pink */
--accent-2: #ff6b2b; /* orange */
--accent-3: #00f5d4; /* cyan/mint */
--accent-4: #f5f700; /* yellow */
--muted: #444466;
--border: #1e1e30;
--font-display: "Impact", "Anton", "Arial Black", system-ui;
--font-body: "Inter", "Helvetica Neue", Arial, sans-serif;
--ease-expo: cubic-bezier(0.19, 1, 0.22, 1);
}
/* โโ Reset / Base โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: auto; /* Lenis handles smooth scrolling */
font-size: 16px;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
overflow-x: hidden;
line-height: 1.5;
}
a {
color: inherit;
text-decoration: none;
}
/* โโ Section Base โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.section {
position: relative;
padding: clamp(4rem, 10vw, 8rem) clamp(1.5rem, 6vw, 6rem);
}
.section-title {
font-family: var(--font-display);
font-size: clamp(2rem, 6vw, 4.5rem);
letter-spacing: 0.04em;
line-height: 1;
text-transform: uppercase;
color: var(--text);
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
HERO
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.hero {
position: relative;
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0 clamp(1.5rem, 6vw, 6rem);
border-bottom: 1px solid var(--border);
}
/* Canvas background โ full cover */
.hero-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
pointer-events: none;
z-index: 0;
}
.hero-content {
position: relative;
z-index: 1;
text-align: center;
max-width: 1400px;
width: 100%;
}
.hero-eyebrow {
font-family: var(--font-body);
font-size: clamp(0.7rem, 1.5vw, 0.9rem);
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(--accent-3);
margin-bottom: 1.5rem;
opacity: 0; /* animated in via GSAP */
}
/* โโ HERO NAME โ glitch wrapper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.hero-name-wrapper {
position: relative;
display: inline-block;
}
.hero-name {
font-family: var(--font-display);
font-size: clamp(4.5rem, 15vw, 13rem);
letter-spacing: -0.04em;
line-height: 0.88;
text-transform: uppercase;
color: var(--text);
cursor: default;
position: relative;
display: block;
opacity: 0; /* animated in via GSAP */
/* Paint isolation for clip-path glitch */
isolation: isolate;
}
/* โโ Glitch layers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.hero-name-glitch {
position: absolute;
inset: 0;
font-family: var(--font-display);
font-size: clamp(4.5rem, 15vw, 13rem);
letter-spacing: -0.04em;
line-height: 0.88;
text-transform: uppercase;
pointer-events: none;
opacity: 0;
white-space: nowrap;
}
/* Glitch only activates when .hero-name-wrapper has .is-hovered */
.hero-name-wrapper.is-hovered .hero-name {
animation: glitch-main 0.4s steps(1) infinite;
}
.hero-name-wrapper.is-hovered .hero-name-glitch--1 {
opacity: 1;
animation: glitch-layer1 0.4s steps(1) infinite;
color: var(--accent-1);
mix-blend-mode: screen;
}
.hero-name-wrapper.is-hovered .hero-name-glitch--2 {
opacity: 1;
animation: glitch-layer2 0.4s steps(1) infinite;
color: var(--accent-3);
mix-blend-mode: screen;
}
@keyframes glitch-main {
0% {
clip-path: inset(0 0 90% 0);
transform: translateX(-3px);
}
25% {
clip-path: inset(30% 0 50% 0);
transform: translateX(3px);
}
50% {
clip-path: inset(60% 0 20% 0);
transform: translateX(-2px);
}
75% {
clip-path: inset(80% 0 0 0);
transform: translateX(2px);
}
100% {
clip-path: none;
transform: none;
}
}
@keyframes glitch-layer1 {
0% {
clip-path: inset(40% 0 30% 0);
transform: translateX(6px);
}
33% {
clip-path: inset(70% 0 10% 0);
transform: translateX(-4px);
}
66% {
clip-path: inset(10% 0 60% 0);
transform: translateX(5px);
}
100% {
clip-path: inset(50% 0 20% 0);
transform: translateX(-3px);
}
}
@keyframes glitch-layer2 {
0% {
clip-path: inset(20% 0 60% 0);
transform: translateX(-5px) skewX(-2deg);
}
40% {
clip-path: inset(65% 0 5% 0);
transform: translateX(4px) skewX(1deg);
}
70% {
clip-path: inset(0 0 75% 0);
transform: translateX(-2px) skewX(-1deg);
}
100% {
clip-path: inset(35% 0 40% 0);
transform: translateX(3px) skewX(2deg);
}
}
.hero-tagline {
font-family: var(--font-body);
font-size: clamp(0.85rem, 2vw, 1.25rem);
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
margin-top: 2rem;
opacity: 0; /* animated in via GSAP */
}
.hero-tagline-sep {
color: var(--accent-1);
margin: 0 0.6em;
}
/* โโ Scroll cue โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.hero-scroll-cue {
position: absolute;
bottom: clamp(1.5rem, 4vw, 3rem);
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
opacity: 0;
font-size: 0.65rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: var(--muted);
}
.hero-scroll-line {
width: 1px;
height: 3rem;
background: linear-gradient(to bottom, var(--muted), transparent);
animation: scrollPulse 1.8s ease-in-out infinite;
}
@keyframes scrollPulse {
0%,
100% {
opacity: 0.3;
transform: scaleY(1);
}
50% {
opacity: 1;
transform: scaleY(1.15);
}
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
WORK / GALLERY
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.work-section {
border-bottom: 1px solid var(--border);
}
.section-header {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: clamp(2rem, 5vw, 4rem);
gap: 1rem;
flex-wrap: wrap;
}
/* โโ Layout Toggle Button โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.layout-toggle {
font-family: var(--font-body);
font-size: 0.75rem;
letter-spacing: 0.2em;
text-transform: uppercase;
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
padding: 0.5rem 1.25rem;
cursor: pointer;
transition: border-color 0.2s, color 0.2s, background 0.2s;
border-radius: 2px;
}
.layout-toggle:hover,
.layout-toggle:focus-visible {
border-color: var(--accent-3);
color: var(--accent-3);
outline: none;
}
.toggle-sep {
margin: 0 0.35em;
}
/* Highlight active mode */
.layout-toggle .toggle-label-grid {
opacity: 1;
}
.layout-toggle .toggle-label-list {
opacity: 0.45;
}
.layout-toggle.is-list .toggle-label-grid {
opacity: 0.45;
}
.layout-toggle.is-list .toggle-label-list {
opacity: 1;
}
/* โโ Media Gallery โ Grid layout (default) โโโโโโโโโโโโโโโโโโโโ */
.media-gallery {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: clamp(1rem, 2.5vw, 2rem);
}
/* โโ Media Card โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.media-card {
position: relative;
background: var(--border);
border: 1px solid var(--border);
overflow: hidden;
cursor: pointer;
/* FLIP needs transform-origin at top-left for cleanest anims */
transform-origin: top left;
transition: border-color 0.25s;
/* Initial state for scroll reveal */
opacity: 0;
transform: translateY(24px);
}
.media-card:hover {
border-color: var(--accent-3);
}
/* โโ Card colored header bars โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.card-header {
height: 12px;
width: 100%;
}
.card-header--1 {
background: var(--accent-1);
}
.card-header--2 {
background: var(--accent-2);
}
.card-header--3 {
background: var(--accent-3);
}
.card-header--4 {
background: var(--accent-4);
}
/* โโ Card body โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.card-body {
padding: clamp(1.25rem, 3vw, 2.5rem);
min-height: clamp(180px, 22vw, 280px);
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
}
/* โโ Play button โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.card-play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%);
color: rgba(255, 255, 255, 0.15);
transition: color 0.25s, transform 0.35s var(--ease-expo);
}
.media-card:hover .card-play {
color: rgba(255, 255, 255, 0.85);
transform: translate(-50%, -60%) scale(1.2);
}
/* โโ Card meta โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.card-meta {
margin-top: auto;
}
.card-title {
font-family: var(--font-display);
font-size: clamp(1.1rem, 2.5vw, 1.8rem);
letter-spacing: 0.02em;
text-transform: uppercase;
line-height: 1.1;
margin-bottom: 0.75rem;
color: var(--text);
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.tag {
font-size: 0.65rem;
letter-spacing: 0.18em;
text-transform: uppercase;
padding: 0.25em 0.65em;
border: 1px solid var(--muted);
color: var(--muted);
border-radius: 2px;
transition: border-color 0.2s, color 0.2s;
}
.media-card:hover .tag {
border-color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.6);
}
/* โโ Card number โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.card-index {
position: absolute;
top: clamp(0.75rem, 2vw, 1.5rem);
right: clamp(0.75rem, 2vw, 1.5rem);
font-family: var(--font-display);
font-size: clamp(2rem, 5vw, 4rem);
letter-spacing: -0.03em;
color: rgba(255, 255, 255, 0.06);
line-height: 1;
pointer-events: none;
user-select: none;
}
/* โโ LIST VIEW (toggled via JS) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.media-gallery.list-view {
grid-template-columns: 1fr;
gap: clamp(0.5rem, 1.5vw, 1rem);
}
.media-gallery.list-view .card-body {
min-height: 0;
flex-direction: row;
align-items: center;
padding: clamp(1rem, 2.5vw, 1.5rem) clamp(1.25rem, 3vw, 2.5rem);
gap: 2rem;
}
.media-gallery.list-view .card-play {
position: static;
transform: none;
flex-shrink: 0;
color: rgba(255, 255, 255, 0.35);
}
.media-gallery.list-view .media-card:hover .card-play {
transform: scale(1.1);
}
.media-gallery.list-view .card-meta {
margin-top: 0;
}
.media-gallery.list-view .card-title {
font-size: clamp(1rem, 2vw, 1.4rem);
margin-bottom: 0.4rem;
}
.media-gallery.list-view .card-index {
top: 50%;
transform: translateY(-50%);
font-size: clamp(2.5rem, 5vw, 5rem);
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DISCIPLINES
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.disciplines-section {
padding-top: clamp(2rem, 5vw, 4rem);
padding-bottom: clamp(2rem, 5vw, 4rem);
border-bottom: 1px solid var(--border);
overflow: hidden;
}
.disciplines-label {
font-size: 0.65rem;
letter-spacing: 0.3em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 1.5rem;
padding: 0 clamp(1.5rem, 6vw, 6rem);
}
/* โโ Marquee wrapper โ clips overflow, flex row โโโโโโโโโโโโโโโโ */
.disciplines-marquee {
display: flex;
overflow: hidden;
width: 100%;
/* pause animation via CSS custom prop set by JS */
--marquee-play-state: running;
}
/* Pause on hover */
.disciplines-marquee:hover {
--marquee-play-state: paused;
}
/* โโ Scrolling track โ one of two identical copies โโโโโโโโโโโโโ */
.disciplines-track {
display: flex;
gap: clamp(0.75rem, 2vw, 1.5rem);
flex-shrink: 0;
min-width: 100%; /* each copy fills at least viewport width */
padding: 0.5rem clamp(0.75rem, 2vw, 1.5rem);
white-space: nowrap;
animation: marquee-scroll 28s linear infinite;
animation-play-state: var(--marquee-play-state);
will-change: transform;
}
@keyframes marquee-scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
/* โโ Pill โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.discipline-pill {
display: inline-flex;
align-items: center;
white-space: nowrap;
font-family: var(--font-display);
font-size: clamp(0.85rem, 2vw, 1.1rem);
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.65em 1.5em;
border: 2px solid;
flex-shrink: 0;
border-radius: 2px;
transition: background 0.2s, color 0.2s;
}
.pill--1 {
border-color: var(--accent-1);
color: var(--accent-1);
}
.pill--1:hover {
background: var(--accent-1);
color: var(--bg);
}
.pill--2 {
border-color: var(--accent-2);
color: var(--accent-2);
}
.pill--2:hover {
background: var(--accent-2);
color: var(--bg);
}
.pill--3 {
border-color: var(--accent-3);
color: var(--accent-3);
}
.pill--3:hover {
background: var(--accent-3);
color: var(--bg);
}
.pill--4 {
border-color: var(--accent-4);
color: var(--accent-4);
}
.pill--4:hover {
background: var(--accent-4);
color: var(--bg);
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
WAVEFORM VISUALIZER
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.waveform-section {
padding-left: 0;
padding-right: 0;
border-bottom: 1px solid var(--border);
}
.waveform-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0 clamp(1.5rem, 6vw, 6rem);
margin-bottom: 1.5rem;
}
.waveform-badge {
font-size: 0.65rem;
letter-spacing: 0.25em;
text-transform: uppercase;
padding: 0.3em 0.8em;
background: var(--accent-1);
color: var(--bg);
font-weight: 700;
border-radius: 2px;
}
.waveform-sub {
font-size: 0.65rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
.waveform-canvas {
display: block;
width: 100%;
height: clamp(120px, 20vw, 240px);
background: transparent;
}
.waveform-footer {
display: flex;
justify-content: space-between;
padding: 0.75rem clamp(1.5rem, 6vw, 6rem) 0;
}
.wf-label {
font-size: 0.6rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.wf-label--1 {
color: var(--accent-1);
}
.wf-label--2 {
color: var(--accent-3);
}
.wf-label--3 {
color: var(--accent-4);
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
CONTACT
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.contact-section {
overflow: hidden;
position: relative;
min-height: 60vh;
display: flex;
align-items: center;
}
.contact-inner {
position: relative;
z-index: 1;
max-width: 900px;
}
.contact-heading {
font-family: var(--font-display);
font-size: clamp(4rem, 12vw, 11rem);
letter-spacing: -0.03em;
line-height: 0.9;
text-transform: uppercase;
background: linear-gradient(135deg, var(--accent-1), var(--accent-2), var(--accent-3));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 1.5rem;
opacity: 0;
}
.contact-sub {
font-size: clamp(0.85rem, 1.8vw, 1.1rem);
color: var(--muted);
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 2.5rem;
opacity: 0;
}
.contact-links {
display: flex;
flex-wrap: wrap;
gap: 1rem;
opacity: 0;
}
/* โโ Neon contact buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.contact-btn {
display: inline-block;
font-family: var(--font-body);
font-size: clamp(0.8rem, 1.5vw, 1rem);
letter-spacing: 0.08em;
text-transform: lowercase;
padding: 0.85em 2em;
border: 1px solid;
position: relative;
overflow: hidden;
transition: color 0.25s, background 0.25s, box-shadow 0.25s;
border-radius: 2px;
}
.contact-btn::before {
content: "";
position: absolute;
inset: 0;
background: currentColor;
opacity: 0;
transition: opacity 0.25s;
}
.contact-btn:hover::before {
opacity: 0.12;
}
.contact-btn--pink {
border-color: var(--accent-1);
color: var(--accent-1);
box-shadow: 0 0 20px rgba(255, 60, 172, 0.15);
}
.contact-btn--pink:hover {
box-shadow: 0 0 35px rgba(255, 60, 172, 0.4), inset 0 0 20px rgba(255, 60, 172, 0.05);
}
.contact-btn--cyan {
border-color: var(--accent-3);
color: var(--accent-3);
box-shadow: 0 0 20px rgba(0, 245, 212, 0.15);
}
.contact-btn--cyan:hover {
box-shadow: 0 0 35px rgba(0, 245, 212, 0.4), inset 0 0 20px rgba(0, 245, 212, 0.05);
}
/* โโ Background oversized text โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.contact-bg-text {
position: absolute;
right: -2%;
bottom: -5%;
font-family: var(--font-display);
font-size: clamp(6rem, 18vw, 18rem);
letter-spacing: -0.05em;
color: rgba(255, 255, 255, 0.025);
white-space: nowrap;
pointer-events: none;
user-select: none;
line-height: 1;
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
REDUCED MOTION
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
@media (prefers-reduced-motion: reduce) {
.hero-scroll-line {
animation: none;
}
.disciplines-track {
animation-play-state: paused;
}
.hero-name-wrapper.is-hovered .hero-name,
.hero-name-wrapper.is-hovered .hero-name-glitch--1,
.hero-name-wrapper.is-hovered .hero-name-glitch--2 {
animation: none;
opacity: 0;
}
}
.reduced-motion .hero-scroll-line {
animation: none;
}
.reduced-motion .disciplines-track {
animation-play-state: paused;
}
.reduced-motion .hero-name-wrapper.is-hovered .hero-name {
animation: none;
}
.reduced-motion .hero-name-wrapper.is-hovered .hero-name-glitch--1,
.reduced-motion .hero-name-wrapper.is-hovered .hero-name-glitch--2 {
opacity: 0 !important;
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
RESPONSIVE
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
@media (max-width: 640px) {
.media-gallery {
grid-template-columns: 1fr;
}
.contact-bg-text {
display: none;
}
.waveform-footer {
justify-content: center;
gap: 1.5rem;
}
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
/**
* 34-multimedia-portfolio / main.js
* Multimedia Engineer Portfolio โ GSAP FLIP, Canvas waveform,
* hero particle field, Lenis smooth scroll.
*/
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { Flip } from "gsap/Flip";
import { SplitText } from "gsap/SplitText";
import Lenis from "lenis";
gsap.registerPlugin(ScrollTrigger, Flip, SplitText);
/* โโ Shell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
initDemoShell({
title: "Multimedia Engineer Portfolio",
category: "pages",
tech: ["gsap", "flip", "canvas-2d", "lenis", "scrolltrigger"],
});
/* โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
/** Respect reduced-motion for durations */
function dur(base) {
return prefersReducedMotion() ? 0 : base;
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
LENIS + SCROLLTRIGGER WIRING
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
HERO โ PARTICLE WAVEFORM CANVAS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initHeroCanvas() {
const canvas = $("#heroCanvas");
if (!canvas) return;
const ctx = canvas.getContext("2d");
let W, H;
let mouseX = 0.5; // 0โ1, normalised
let mouseY = 0.5;
let raf;
let startTime = performance.now();
/* โโ Particles โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
const PARTICLE_COUNT = 220;
const particles = [];
class Particle {
constructor(i) {
this.index = i;
this.reset();
}
reset() {
// Distribute along the horizontal axis, wave-shaped Y
this.baseX = (this.index / PARTICLE_COUNT) * 2; // 0..2 โ we'll use W
this.baseXpx = 0; // set on resize
this.baseYpx = 0;
this.x = 0;
this.y = 0;
this.vx = 0;
this.vy = 0;
this.radius = Math.random() * 2.2 + 1;
// Each particle gets its own phase & colour
this.phase = (this.index / PARTICLE_COUNT) * Math.PI * 8;
const hues = ["#ff3cac", "#00f5d4", "#f5f700", "#ff6b2b"];
this.color = hues[Math.floor(Math.random() * hues.length)];
}
update(t, mxPx, myPx) {
// Waveform base position
const waveAmp = H * 0.18;
const freq = 1.8 + mouseX * 1.2; // frequency shifts with mouse X
this.baseXpx = (this.index / PARTICLE_COUNT) * W;
this.baseYpx =
H / 2 +
Math.sin(this.baseXpx * freq * 0.005 + t * 0.8 + this.phase) * waveAmp * 0.8 +
Math.sin(this.baseXpx * freq * 0.012 + t * 1.4 + this.phase) * waveAmp * 0.4;
// Repulsion from mouse
const dx = this.x - mxPx;
const dy = this.y - myPx;
const dist = Math.sqrt(dx * dx + dy * dy);
const repelRadius = prefersReducedMotion() ? 0 : 90;
if (dist < repelRadius && dist > 0) {
const force = (repelRadius - dist) / repelRadius;
const angle = Math.atan2(dy, dx);
this.vx += Math.cos(angle) * force * 5;
this.vy += Math.sin(angle) * force * 5;
}
// Spring back to waveform position
const springStr = 0.07;
this.vx += (this.baseXpx - this.x) * springStr;
this.vy += (this.baseYpx - this.y) * springStr;
// Damping
this.vx *= 0.82;
this.vy *= 0.82;
this.x += this.vx;
this.y += this.vy;
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.shadowBlur = 8;
ctx.shadowColor = this.color;
ctx.fill();
ctx.shadowBlur = 0;
}
}
function buildParticles() {
particles.length = 0;
for (let i = 0; i < PARTICLE_COUNT; i++) {
const p = new Particle(i);
p.x = (i / PARTICLE_COUNT) * W;
p.y = H / 2;
particles.push(p);
}
}
function resize() {
W = canvas.width = canvas.offsetWidth;
H = canvas.height = canvas.offsetHeight;
buildParticles();
}
function loop() {
raf = requestAnimationFrame(loop);
const t = (performance.now() - startTime) / 1000;
const mxPx = mouseX * W;
const myPx = mouseY * H;
ctx.clearRect(0, 0, W, H);
// Subtle radial gradient atmosphere
const grd = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, W * 0.7);
grd.addColorStop(0, "rgba(255,60,172,0.05)");
grd.addColorStop(0.5, "rgba(0,245,212,0.03)");
grd.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, W, H);
particles.forEach((p) => {
p.update(t, mxPx, myPx);
p.draw(ctx);
});
}
// Mouse tracking on hero section
const heroEl = $("#hero");
heroEl.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) / rect.width;
mouseY = (e.clientY - rect.top) / rect.height;
});
heroEl.addEventListener("mouseleave", () => {
mouseX = 0.5;
mouseY = 0.5;
});
window.addEventListener("resize", resize);
resize();
loop();
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
HERO โ GLITCH HOVER
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initGlitch() {
const nameEl = $(".hero-name");
if (!nameEl) return;
// Wrap name element in a glitch container if not already in HTML
// The HTML already positions glitch spans absolutely inside .hero-content
// We add/remove .is-hovered on the wrapper
const wrapper = document.createElement("div");
wrapper.className = "hero-name-wrapper";
nameEl.parentNode.insertBefore(wrapper, nameEl);
// Move name and glitch spans into wrapper
const glitch1 = $(".hero-name-glitch--1");
const glitch2 = $(".hero-name-glitch--2");
wrapper.appendChild(nameEl);
if (glitch1) wrapper.appendChild(glitch1);
if (glitch2) wrapper.appendChild(glitch2);
wrapper.addEventListener("mouseenter", () => {
if (!prefersReducedMotion()) wrapper.classList.add("is-hovered");
});
wrapper.addEventListener("mouseleave", () => {
wrapper.classList.remove("is-hovered");
});
// Motion preference change
window.addEventListener("motion-preference", () => {
wrapper.classList.remove("is-hovered");
});
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
HERO โ GSAP ENTRANCE ANIMATIONS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initHeroEntrance() {
const eyebrow = $(".hero-eyebrow");
const nameEl = $(".hero-name");
const tagline = $(".hero-tagline");
const scrollCue = $(".hero-scroll-cue");
const tl = gsap.timeline({ delay: 0.15 });
tl.to(eyebrow, {
opacity: 1,
y: 0,
duration: dur(0.6),
ease: "expo.out",
onStart() {
gsap.set(eyebrow, { y: 12 });
},
});
tl.to(
nameEl,
{
opacity: 1,
y: 0,
duration: dur(0.9),
ease: "expo.out",
onStart() {
gsap.set(nameEl, { y: 30 });
},
},
"-=0.35"
);
tl.to(
tagline,
{
opacity: 1,
y: 0,
duration: dur(0.7),
ease: "expo.out",
onStart() {
gsap.set(tagline, { y: 16 });
},
},
"-=0.55"
);
tl.to(
scrollCue,
{
opacity: 1,
duration: dur(0.6),
ease: "power2.out",
},
"-=0.2"
);
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
GALLERY โ GSAP FLIP LAYOUT TOGGLE
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initGallery() {
const gallery = $("#mediaGallery");
const toggleBtn = $("#layout-toggle");
if (!gallery || !toggleBtn) return;
toggleBtn.addEventListener("click", () => {
// Capture state BEFORE class change
const state = Flip.getState(".media-card");
gallery.classList.toggle("list-view");
toggleBtn.classList.toggle("is-list");
// Animate from old state to new
Flip.from(state, {
duration: dur(0.65),
ease: "expo.out",
stagger: { each: dur(0.06) },
absolute: true,
onEnter: (els) => gsap.fromTo(els, { opacity: 0 }, { opacity: 1, duration: dur(0.4) }),
onLeave: (els) => gsap.to(els, { opacity: 0, duration: dur(0.2) }),
});
});
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
SCROLL REVEAL โ MEDIA CARDS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initCardReveal() {
const cards = $$(".media-card");
if (!cards.length) return;
cards.forEach((card, i) => {
gsap.to(card, {
opacity: 1,
y: 0,
duration: dur(0.7),
ease: "expo.out",
delay: i * dur(0.1),
scrollTrigger: {
trigger: card,
start: "top 88%",
toggleActions: "play none none none",
},
});
});
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
CONTACT SECTION โ SCROLL REVEAL
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initContactReveal() {
const heading = $(".contact-heading");
const sub = $(".contact-sub");
const links = $(".contact-links");
if (!heading) return;
const tl = gsap.timeline({
scrollTrigger: {
trigger: "#contact",
start: "top 78%",
toggleActions: "play none none none",
},
});
tl.to(heading, {
opacity: 1,
y: 0,
duration: dur(0.9),
ease: "expo.out",
onStart() {
gsap.set(heading, { y: 40 });
},
});
tl.to(
sub,
{
opacity: 1,
y: 0,
duration: dur(0.6),
ease: "expo.out",
onStart() {
gsap.set(sub, { y: 16 });
},
},
"-=0.55"
);
tl.to(
links,
{
opacity: 1,
y: 0,
duration: dur(0.5),
ease: "expo.out",
onStart() {
gsap.set(links, { y: 12 });
},
},
"-=0.4"
);
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
WAVEFORM VISUALIZER CANVAS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initWaveform() {
const canvas = $("#waveformCanvas");
if (!canvas) return;
const ctx = canvas.getContext("2d");
let W, H;
let mouseX = 0; // raw px, updated on section mousemove
let animId;
let startTime = performance.now();
const waves = [
{ freq: 0.022, amp: 0.15, speed: 1.0, color: "#ff3cac", lineWidth: 2.5 },
{ freq: 0.034, amp: 0.1, speed: 1.6, color: "#00f5d4", lineWidth: 2 },
{ freq: 0.055, amp: 0.06, speed: 2.3, color: "#f5f700", lineWidth: 1.5 },
];
function resize() {
W = canvas.width = canvas.offsetWidth;
H = canvas.height = canvas.offsetHeight;
mouseX = W / 2;
}
function drawWave(t) {
ctx.clearRect(0, 0, W, H);
const centerY = H / 2;
const mxInfluence = (mouseX / W - 0.5) * 0.5;
waves.forEach((wave, wi) => {
const amp = wave.amp * H;
// Create horizontal gradient for this wave
const grd = ctx.createLinearGradient(0, 0, W, 0);
if (wi === 0) {
grd.addColorStop(0, "rgba(255,60,172,0)");
grd.addColorStop(0.3, "#ff3cac");
grd.addColorStop(0.7, "#ff6b2b");
grd.addColorStop(1, "rgba(255,107,43,0)");
} else if (wi === 1) {
grd.addColorStop(0, "rgba(0,245,212,0)");
grd.addColorStop(0.25, "#00f5d4");
grd.addColorStop(0.75, "#ff3cac");
grd.addColorStop(1, "rgba(255,60,172,0)");
} else {
grd.addColorStop(0, "rgba(245,247,0,0)");
grd.addColorStop(0.4, "#f5f700");
grd.addColorStop(0.6, "#00f5d4");
grd.addColorStop(1, "rgba(0,245,212,0)");
}
ctx.beginPath();
ctx.strokeStyle = grd;
ctx.lineWidth = wave.lineWidth;
ctx.shadowBlur = 16;
ctx.shadowColor = wave.color;
ctx.lineJoin = "round";
ctx.lineCap = "round";
for (let x = 0; x <= W; x += 2) {
const freqMod = wave.freq + mxInfluence;
const y =
centerY +
Math.sin(x * freqMod + t * wave.speed) * amp +
Math.sin(x * freqMod * 2.1 + t * wave.speed * 0.5) * amp * 0.3;
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
ctx.shadowBlur = 0;
});
// Subtle center line
ctx.beginPath();
ctx.setLineDash([4, 8]);
ctx.strokeStyle = "rgba(68,68,102,0.5)";
ctx.lineWidth = 1;
ctx.moveTo(0, centerY);
ctx.lineTo(W, centerY);
ctx.stroke();
ctx.setLineDash([]);
}
function loop() {
animId = requestAnimationFrame(loop);
if (prefersReducedMotion()) {
// Static single frame
drawWave(0);
cancelAnimationFrame(animId);
return;
}
const t = (performance.now() - startTime) / 1000;
drawWave(t);
}
// Mousemove on the waveform section
const wfSection = $("#waveform");
wfSection.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
});
wfSection.addEventListener("mouseleave", () => {
mouseX = W / 2;
});
// Restart animation on motion preference change
window.addEventListener("motion-preference", () => {
cancelAnimationFrame(animId);
if (!prefersReducedMotion()) {
startTime = performance.now();
loop();
} else {
drawWave(0);
}
});
window.addEventListener("resize", resize);
resize();
loop();
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DISCIPLINES MARQUEE โ CSS animation, paused when reduced-motion
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initDisciplinesMarquee() {
const marquee = $("#disciplinesMarquee");
if (!marquee) return;
// If user prefers reduced motion, freeze the animation immediately
function applyMotion() {
if (prefersReducedMotion()) {
marquee.style.setProperty("--marquee-play-state", "paused");
} else {
marquee.style.removeProperty("--marquee-play-state");
}
}
applyMotion();
window.addEventListener("motion-preference", applyMotion);
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
SECTION TITLE ANIMATIONS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
(function initSectionTitles() {
const titles = $$(".section-title");
titles.forEach((title) => {
gsap.fromTo(
title,
{ opacity: 0, x: -30 },
{
opacity: 1,
x: 0,
duration: dur(0.8),
ease: "expo.out",
scrollTrigger: {
trigger: title,
start: "top 85%",
toggleActions: "play none none none",
},
}
);
});
})();
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
REDUCED MOTION HANDLING (runtime toggle)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
window.addEventListener("motion-preference", () => {
if (prefersReducedMotion()) {
document.documentElement.classList.add("reduced-motion");
lenis.stop();
} else {
document.documentElement.classList.remove("reduced-motion");
lenis.start();
}
});
// Apply on load if needed
if (prefersReducedMotion()) {
document.documentElement.classList.add("reduced-motion");
lenis.stop();
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jordan Kim โ Multimedia Engineer Portfolio</title>
<link rel="stylesheet" href="style.css" />
<script type="importmap">{"imports":{"gsap":"https://esm.sh/gsap@3.13.0","gsap/ScrollTrigger":"https://esm.sh/gsap@3.13.0/ScrollTrigger","gsap/SplitText":"https://esm.sh/gsap@3.13.0/SplitText","gsap/Flip":"https://esm.sh/gsap@3.13.0/Flip","gsap/ScrambleTextPlugin":"https://esm.sh/gsap@3.13.0/ScrambleTextPlugin","gsap/TextPlugin":"https://esm.sh/gsap@3.13.0/TextPlugin","gsap/all":"https://esm.sh/gsap@3.13.0/all","gsap/":"https://esm.sh/gsap@3.13.0/","lenis":"https://esm.sh/lenis@1.1.13/dist/lenis.mjs","three":"https://esm.sh/three@0.171.0","three/addons/":"https://esm.sh/three@0.171.0/examples/jsm/"}}</script>
</head>
<body>
<!-- โโโ HERO โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
<section class="hero" id="hero">
<canvas class="hero-canvas" id="heroCanvas"></canvas>
<div class="hero-content">
<div class="hero-eyebrow">Portfolio 2025</div>
<h1 class="hero-name" aria-label="Jordan Kim">JORDAN KIM</h1>
<!-- Glitch layers (aria-hidden so screen readers skip duplicates) -->
<span class="hero-name-glitch hero-name-glitch--1" aria-hidden="true">JORDAN KIM</span>
<span class="hero-name-glitch hero-name-glitch--2" aria-hidden="true">JORDAN KIM</span>
<p class="hero-tagline">Multimedia Engineer <span class="hero-tagline-sep">/</span> Creative Technologist</p>
<div class="hero-scroll-cue" aria-hidden="true">
<span>Scroll</span>
<div class="hero-scroll-line"></div>
</div>
</div>
</section>
<!-- โโโ SELECTED WORK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
<section class="section work-section" id="work">
<div class="section-header">
<h2 class="section-title">SELECTED WORK</h2>
<button class="layout-toggle" id="layout-toggle" aria-label="Toggle gallery layout">
<span class="toggle-label-grid">Grid</span>
<span class="toggle-sep">/</span>
<span class="toggle-label-list">List</span>
</button>
</div>
<div class="media-gallery" id="mediaGallery">
<article class="media-card" data-index="0">
<div class="card-header card-header--1"></div>
<div class="card-body">
<div class="card-play" aria-hidden="true">
<svg viewBox="0 0 40 40" width="40" height="40" fill="none">
<circle cx="20" cy="20" r="19" stroke="currentColor" stroke-width="1.5"/>
<polygon points="16,13 30,20 16,27" fill="currentColor"/>
</svg>
</div>
<div class="card-meta">
<h3 class="card-title">Resonance Field</h3>
<div class="card-tags">
<span class="tag">Interactive</span>
<span class="tag">WebGL</span>
</div>
</div>
</div>
<div class="card-index">01</div>
</article>
<article class="media-card" data-index="1">
<div class="card-header card-header--2"></div>
<div class="card-body">
<div class="card-play" aria-hidden="true">
<svg viewBox="0 0 40 40" width="40" height="40" fill="none">
<circle cx="20" cy="20" r="19" stroke="currentColor" stroke-width="1.5"/>
<polygon points="16,13 30,20 16,27" fill="currentColor"/>
</svg>
</div>
<div class="card-meta">
<h3 class="card-title">Chromatic Drift</h3>
<div class="card-tags">
<span class="tag">Film</span>
<span class="tag">Motion Design</span>
</div>
</div>
</div>
<div class="card-index">02</div>
</article>
<article class="media-card" data-index="2">
<div class="card-header card-header--3"></div>
<div class="card-body">
<div class="card-play" aria-hidden="true">
<svg viewBox="0 0 40 40" width="40" height="40" fill="none">
<circle cx="20" cy="20" r="19" stroke="currentColor" stroke-width="1.5"/>
<polygon points="16,13 30,20 16,27" fill="currentColor"/>
</svg>
</div>
<div class="card-meta">
<h3 class="card-title">Spatial Echo</h3>
<div class="card-tags">
<span class="tag">Installation</span>
<span class="tag">Sound Design</span>
</div>
</div>
</div>
<div class="card-index">03</div>
</article>
<article class="media-card" data-index="3">
<div class="card-header card-header--4"></div>
<div class="card-body">
<div class="card-play" aria-hidden="true">
<svg viewBox="0 0 40 40" width="40" height="40" fill="none">
<circle cx="20" cy="20" r="19" stroke="currentColor" stroke-width="1.5"/>
<polygon points="16,13 30,20 16,27" fill="currentColor"/>
</svg>
</div>
<div class="card-meta">
<h3 class="card-title">Generative Topography</h3>
<div class="card-tags">
<span class="tag">Generative</span>
<span class="tag">Creative Coding</span>
</div>
</div>
</div>
<div class="card-index">04</div>
</article>
</div>
</section>
<!-- โโโ DISCIPLINES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
<section class="section disciplines-section" id="disciplines">
<div class="disciplines-label">DISCIPLINES</div>
<div class="disciplines-marquee" id="disciplinesMarquee" aria-label="Disciplines list">
<!-- First copy -->
<div class="disciplines-track" aria-hidden="true">
<div class="discipline-pill pill--1">Real-time Graphics</div>
<div class="discipline-pill pill--2">Motion Design</div>
<div class="discipline-pill pill--3">Generative Art</div>
<div class="discipline-pill pill--4">Audio-Visual</div>
<div class="discipline-pill pill--1">WebGL</div>
<div class="discipline-pill pill--2">Interactive Installation</div>
<div class="discipline-pill pill--3">Creative Coding</div>
<div class="discipline-pill pill--4">Sound Design</div>
</div>
<!-- Second copy โ seamless loop -->
<div class="disciplines-track" aria-hidden="true">
<div class="discipline-pill pill--1">Real-time Graphics</div>
<div class="discipline-pill pill--2">Motion Design</div>
<div class="discipline-pill pill--3">Generative Art</div>
<div class="discipline-pill pill--4">Audio-Visual</div>
<div class="discipline-pill pill--1">WebGL</div>
<div class="discipline-pill pill--2">Interactive Installation</div>
<div class="discipline-pill pill--3">Creative Coding</div>
<div class="discipline-pill pill--4">Sound Design</div>
</div>
</div>
</section>
<!-- โโโ WAVEFORM VISUALIZER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
<section class="section waveform-section" id="waveform">
<div class="waveform-header">
<span class="waveform-badge">Audio Engine</span>
<span class="waveform-sub">Simulated Waveform</span>
</div>
<canvas class="waveform-canvas" id="waveformCanvas"></canvas>
<div class="waveform-footer">
<span class="wf-label wf-label--1">440 Hz</span>
<span class="wf-label wf-label--2">880 Hz</span>
<span class="wf-label wf-label--3">1760 Hz</span>
</div>
</section>
<!-- โโโ CONTACT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
<section class="section contact-section" id="contact">
<div class="contact-inner">
<h2 class="contact-heading">COLLABORATE?</h2>
<p class="contact-sub">Open to commissions, residencies & full-time roles</p>
<div class="contact-links">
<a class="contact-btn contact-btn--pink" href="mailto:jordan@kimstudio.io">
jordan@kimstudio.io
</a>
<a class="contact-btn contact-btn--cyan" href="https://instagram.com" target="_blank" rel="noopener">
@jordankimstudio
</a>
</div>
</div>
<div class="contact-bg-text" aria-hidden="true">LET'S BUILD</div>
</section>
<script type="module" src="script.js"></script>
</body>
</html>Multimedia Engineer Portfolio
Bold creative technologist portfolio with Canvas 2D particle waveform reacting to mouse, GSAP FLIP grid/list gallery toggle, discipline pills ticker, audio waveform visualizer, and glitch-hover name effect.
Source
- Repository:
libs-genclaude - Original demo id:
34-multimedia-portfolio
Notes
Bold creative technologist portfolio with Canvas 2D particle waveform reacting to mouse, GSAP FLIP grid/list gallery toggle, discipline pills ticker, audio waveform visualizer, and glitch-hover name effect.