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…

这是什么?Viscose 是一个完全用单个 WebGL 片段着色器渲染的作品集轮播。卡片排在一个大部分在屏幕外的圆环上;当它们靠拢时会像液体一样彼此融合,分开时又会在相互之间拉出细丝。
这是现成的插件,还是供我在其上构建的模板?它是一个供你克隆并自定义的 Next.js 应用/模板,不是 npm 包。所有着色器、圆环和项目逻辑都放在 components/ring/ 和 components/shaders/ 文件夹里,你可以直接修改。
怎么让它跑起来?克隆仓库,用 npm 安装依赖(需要 Node 20+),然后运行 npm run dev 启动 Next.js 开发服务器。
着色器效果背后的技术栈是什么?它构建在 Next.js 和 React 之上,用 Three.js 驱动 WebGL 渲染,用 GSAP 做动画,用 lil-gui 做仅供开发调试用的调节面板。
点击卡片真的会打开项目吗?不会——点击卡片只会让它居中,「View」标签承诺的去处还不存在。此外也没有减弱动效支持、没有键盘导航,除了 lint 和 build 之外也没有自动化测试。
到底谁会用到它?是那些想要醒目、定制化作品集轮播,并且愿意自己改着色器代码、调约 136 个参数的开发者和设计师,而不是想要拿来即用组件的人。
它能在哪里运行——任何浏览器都行吗?它可以在任何现代浏览器中运行,响应式布局能适配到手机宽度,不过作者提到在约 500px 以下时紧凑断点会出现偏移。
附带的图片和字体可以商用吗?MIT 许可证只覆盖源代码——Behance 示例图片和内置的 PP Neue Montreal 字体不在覆盖范围内,商用必须替换或另行获得授权。
怎么把自己的项目加到圆环上?所有内容都在 components/ring/projects.js 的一个数组里——把图片放进 public/ 再添加一行即可。注意列表顺序就是圆环顺序,而不是文件名顺序,因此调整行的顺序就会改变轮播次序。
融合与细丝效果到底是怎么实现的?整个圆环、细丝、玻璃边缘和光标标签都是在一次着色器渲染中,用带符号距离场配合平滑最小值混合画出来的——正是这种柔和混合让相邻的卡片看起来像黏稠液体一样融合与分离。
触摸设备上没有悬停,那会怎样?滑动可以转动圆环,长按手势会触发悬停效果,因为手指本身没有悬停状态。
轮播开始前为什么会显示一个加载计数器?这个计数器不只是显示进度——它是道闸门:计数达到 100 后圆环才会启动,同时纹理图集在背后逐步填充,并优先加载第一张卡片的画面。
Size
63.6 MB
Stars
515
Updated
CommitOct 12
Age
2mo
Commits · 90 Days