Viscose-carousel README

The project's README from github.com/Yousuf-developer/Viscose-carousel. As one markdown file: README.md.

Viscose

A portfolio carousel rendered as a single WebGL shader. Project cards ride a ring that sits mostly off-screen, so you see a tall arc of work sweeping past. Scroll, drag or swipe to turn it; it settles with a card facing front.

Viscose: a viscous state, and a fibre spun by drawing that state into a thread. Which is what the cards do when they pull apart.

License: MIT Next.js Three.js

The carousel at rest — the front card centred, its number and name to the
left, discipline and year to the right, and the full index top
right

The cards are not images in a grid. The whole ring is one full-screen fragment shader drawing signed distance fields, which is what lets neighbouring cards melt into one another as they close up and string thin threads as they pull apart. The cursor doesn't draw anything — it softens the surface under itself, leans nearby cards toward it and webs honey back between them.

Read the breakdown → — where the idea came from, how viscosity became a distance field, and the four things that only showed up once it moved.

Quick start

Requires Node 20 or newer.

git clone https://github.com/Yousuf-developer/viscose.git
cd viscose
npm install
npm run dev

Then open http://localhost:3000.

npm run dev dev server
npm run build production build
npm start serve the build
npm run lint eslint

Built with Next.js 16 (App Router), React 19, Three.js, GSAP and Tailwind v4.

PP Neue Montreal is bundled for development only and is not licensed for commercial use by this project. See Fonts.

What you can interact with

Hovering a card: the neighbours above and below have backed away and dimmed,
threads stretch between them, and the View tag has appeared on the
cursor

The threads are not drawn between the cards — they're part of the same distance field, so they thin, sag and dissolve on their own as the gap opens. The glass lip along the top and bottom edges is refracting the two neighbours into those swept shapes, and that too is the same shader pass.

The dev panel

In development a lil-gui panel appears top-right with every tunable in the project — around 136 of them, grouped by what they affect. Drag a slider and the change is live.

It never ships: the panel and lil-gui itself are both behind a dynamic import guarded by NODE_ENV === "development".

Before you tune anything, open the fit folder and check that scale reads 1.000. Every pixel measurement in the project is quoted against a reference window (a 14" MacBook Pro, 1512 points wide). On a different screen you'd be tuning against a scale factor that isn't 1, and the result will look right for you and wrong for everyone else. There's a use this window as ref button that fixes this in one click.

Folder What's in it
fit reference window, scale clamps, the two responsive breakpoints
shape card size, count, ring radius, corner, art crossfade
timing / stage the entry animation
meta the type either side of the ring, and its morph
pointer / side cards how the ring reacts to a cursor
honey the threads between cards
glass the refracting lip along the top and bottom edges

Making it yours

Projects

Everything lives in one array, components/ring/projects.js:

{ file: "10.webp", name: "Matchday", type: "Motion", year: "2025" }

Drop an image in public/, add a row, done. The array does four jobs at once — it's the atlas packing order, the order art is dealt around the ring, the project column top-right, and the 01–18 numbering. Reordering rows moves all four together.

The list is in ring order, not filename order. It reads shuffled against the file numbers, and that's deliberate: entry n sits one slot along the ring from entry n−1, which is what lets the column count up as the carousel turns. Reorder these rows to change the sequence; don't reach for imageOffset, which rotates the art without moving the list with it.

All of the sample data is placeholder: every type and year is invented, names marked (*) are guesses at the subject, and the images are other people's work collected to build the layout against. See About the artwork.

Fonts

Three families, declared in app/globals.css and looked up by name from components/ring/params.js — so if you swap one, change it in both places.

Family Used for Weights Licence
Satoshi project name, discipline, index column 400, 500 Fontshare, free
Geist number, year, load counter 400 OFL
PP Neue Montreal intro heading, cursor tag 400 Pangram Pangram, commercial

[!IMPORTANT] PP Neue Montreal is not licensed by this project. It is a commercial typeface from Pangram Pangram and the file is included here for local development and evaluation only. It is not covered by this repo's MIT licence and must not be used for commercial purposes. If you ship anything with it, buy your own licence — or swap textFont in params.js for Satoshi, which is free for commercial work and already included.

Satoshi and Geist are both free to redistribute and use commercially.

Fonts are served as .otf/.ttf (~340 KB total). Converting to woff2 would cut that by roughly 60% — worth doing before this goes anywhere real.

How it's put together

app/
  page.js            renders <Carousel />
  globals.css        Tailwind, @font-face, page background

components/
  Carousel.jsx      the component — renderer, resize, input, per-frame
                     layout, entry timeline
  ring/
    projects.js      the work
    params.js        every tunable
    utils.js         maths helpers
    atlas.js         packs all the art into one texture
    meta.js          the type either side of the ring
    splitText.js     the intro heading
    tag.js           the "View" tag on the cursor
    gui.js           the dev panel
  shaders/
    planeShaders.js  the ring — SDFs, goo, glass, tag
    textShaders.js   the heading's per-glyph reveal

Two ideas explain most of the rest.

One shader, one draw call. The ring, the threads between cards, the glass lip along the screen edges and the cursor tag are all evaluated per pixel in planeShaders.js. Card positions arrive as uniform arrays, and the fragment shader blends their distance fields with a smooth minimum — that soft blend is the goo. Because the tag is drawn in the same pass, its label can invert against whatever pixels it happens to be sitting on.

The ring mid-entry: cards on the right have separated cleanly while those on
the left are still fused into one another with thick necks between
them

The entry is the clearest look at what the smooth minimum is doing. Every card starts merged inside the one before it and peels away in sequence, so at any moment during the unfurl some pairs have fully separated, some are joined by a thinning neck, and some are still one blob.

Loading is part of the animation. The atlas binds on the first frame and fills in as images arrive, with the first card's art requested at high priority so it can be shown while the rest are still downloading. The counter at the bottom of the screen isn't a readout beside the entry — it is the gate. The seed card is born, holds at centre, and the ring launches on the frame the number reaches 100.

For the deeper technical notes — coordinate conventions, the responsive model, and the handful of things here that look like bugs but aren't — see AGENTS.md.

Status

This is a work in progress, published because the rendering approach might be useful to someone. Known gaps:

Contributing

Issues and pull requests are welcome.

Credits

License

MIT for the source code. Not for anything in public/.

About the artwork

The project images are not mine. I picked them up from Behance, more or less at random, purely so there was real work to build and judge the layout against — a carousel full of grey rectangles tells you nothing about whether the thing looks good.

I claim no ownership of any of it, the MIT licence above does not extend to it, and none of this is an endorsement of reusing it. If you fork this, replace the images with your own work.

The names, disciplines and years shown next to them are invented placeholders and are not claims about who made what.

If you made one of these pieces and would like to be credited, or would like it removed, please open an issue and I'll sort it out straight away.

The bundled PP Neue Montreal is likewise not licensed by this project — see Fonts.

Viscose-carousel (Yousuf-developer/Viscose-carousel)

Searching Database…

What is this?Viscose is a portfolio carousel rendered entirely as a single WebGL fragment shader. Cards sit on a ring that's mostly off-screen; as they come together they melt into each other like liquid, and as they pull apart they draw thin threads between them.
Is this a ready-made plugin or a template I build on?It's a Next.js app/template you clone and customize, not an npm package. All the shader, ring and project logic lives in the components/ring/ and components/shaders/ folders for you to edit directly.
How do I get it running?Clone the repo, install dependencies with npm (Node 20+ required), then run npm run dev to start the Next.js dev server.
What's the tech stack behind the shader effect?It's built on Next.js and React, with Three.js driving the WebGL rendering, GSAP for animation and lil-gui for the dev-only tuning panel.
Does clicking a card actually open the project?No — clicking a card only centers it, the 'View' tag promises a destination that doesn't exist yet. There's also no reduced-motion support, no keyboard navigation, and no automated tests besides lint and build.
Who would actually use this?Developers and designers who want a striking, custom-built portfolio carousel and are comfortable editing shader code and tuning ~136 parameters themselves, rather than people who want a drop-in widget.
Where does this run — any browser?It runs in any modern browser, responsive down to phone widths, though the author notes the tight breakpoint drifts below ~500px.
Can I use the included images and fonts commercially?The MIT licence covers only the source code — the sample Behance images and the bundled PP Neue Montreal font are not covered and must be replaced or separately licensed for commercial use.
How do I add my own projects to the ring?Everything lives in one array in components/ring/projects.js — drop an image into public/ and add a row. Note the list order is the ring order, not the filename order, so reordering rows changes the sequence.
How does the melt-and-thread effect actually work?The whole ring, the threads, the glass lip and the cursor tag are all drawn in one shader pass using signed distance fields blended with a smooth minimum — that soft blend is what makes neighbouring cards appear to fuse and separate like goo.
What happens on touch devices since there's no hover?Swipe turns the ring and a press-and-hold gesture triggers the hover effects, since a finger has no hover state of its own.
Why does a loading counter appear before the carousel starts?The counter isn't just a readout — it's the gate: the ring only launches once the counter reaches 100, while the texture atlas fills in behind it with the first card's art prioritized.
Size
63.6 MB
Stars
514
Updated
CommitOct 12
Age
2mo
Commits · 90 Days