[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-94867":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":15,"stars7d":16,"stars30d":16,"stars90d":15,"forks30d":15,"starsTrendScore":17,"compositeScore":18,"rankGlobal":10,"rankLanguage":10,"license":19,"archived":20,"fork":20,"defaultBranch":21,"hasWiki":22,"hasPages":20,"topics":23,"createdAt":10,"pushedAt":10,"updatedAt":24,"readmeContent":25,"aiSummary":26,"trendingCount":15,"starSnapshotCount":15,"syncStatus":27,"lastSyncTime":28,"discoverSource":29},94867,"Viscose-carousel","Yousuf-developer\u002FViscose-carousel","Yousuf-developer","A wheel of work that never quite sets. Cards fuse as they meet, and draw into threads as they part.","https:\u002F\u002Fviscose-carousel.vercel.app",null,"JavaScript",391,64,129,0,223,14,75.44,"Other",false,"main",true,[],"2026-08-24 04:01:22","# Viscose\n\nA portfolio carousel rendered as a single WebGL shader. Project cards ride a\nring that sits mostly off-screen, so you see a tall arc of work sweeping past.\nScroll, drag or swipe to turn it; it settles with a card facing front.\n\n_Viscose: a viscous state, and a fibre spun by drawing that state into a\nthread. Which is what the cards do when they pull apart._\n\n[![License: MIT](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FLicense-MIT-black.svg)](LICENSE)\n![Next.js](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FNext.js-16-black)\n![Three.js](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FThree.js-r185-black)\n\n![The carousel at rest — the front card centred, its number and name to the\nleft, discipline and year to the right, and the full index top\nright](docs\u002Fcarousel.png)\n\nThe cards are not images in a grid. The whole ring is one full-screen fragment\nshader drawing signed distance fields, which is what lets neighbouring cards\nmelt into one another as they close up and string thin threads as they pull\napart. The cursor doesn't draw anything — it softens the surface under itself,\nleans nearby cards toward it and webs honey back between them.\n\n**[Read the breakdown →](BREAKDOWN.md)** — where the idea came from, how\nviscosity became a distance field, and the four things that only showed up\nonce it moved.\n\n\u003C!-- TODO: a screen recording would sell this far better than stills. The\n     whole point is what happens between frames. -->\n\n## Quick start\n\nRequires **Node 20 or newer**.\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002FYousuf-developer\u002Fviscose.git\ncd viscose\nnpm install\nnpm run dev\n```\n\nThen open \u003Chttp:\u002F\u002Flocalhost:3000>.\n\n|                 |                  |\n| --------------- | ---------------- |\n| `npm run dev`   | dev server       |\n| `npm run build` | production build |\n| `npm start`     | serve the build  |\n| `npm run lint`  | eslint           |\n\nBuilt with Next.js 16 (App Router), React 19, Three.js, GSAP and Tailwind v4.\n\n> **PP Neue Montreal is bundled for development only** and is not licensed for\n> commercial use by this project. See [Fonts](#fonts).\n\n## What you can interact with\n\n- **Scroll or drag** to turn the ring. It has momentum and snaps to the\n  nearest card.\n- **Hover a card** to soften the field around it, push its neighbours aside\n  and string threads between them.\n- **Click an off-centre card** to turn the ring smoothly until it's at the\n  front.\n- **On touch**, swipe to turn and press-and-hold for the hover effects — a\n  finger has no hover state, so it sits behind a deliberate gesture.\n\n![Hovering a card: the neighbours above and below have backed away and dimmed,\nthreads stretch between them, and the View tag has appeared on the\ncursor](docs\u002Fhover.png)\n\nThe threads are not drawn between the cards — they're part of the same\ndistance field, so they thin, sag and dissolve on their own as the gap opens.\nThe glass lip along the top and bottom edges is refracting the two neighbours\ninto those swept shapes, and that too is the same shader pass.\n\n## The dev panel\n\nIn development a [lil-gui](https:\u002F\u002Flil-gui.georgealways.com\u002F) panel appears\ntop-right with every tunable in the project — around 136 of them, grouped by\nwhat they affect. Drag a slider and the change is live.\n\nIt never ships: the panel and lil-gui itself are both behind a dynamic import\nguarded by `NODE_ENV === \"development\"`.\n\n**Before you tune anything**, open the **fit** folder and check that `scale`\nreads `1.000`. Every pixel measurement in the project is quoted against a\nreference window (a 14\" MacBook Pro, 1512 points wide). On a different screen\nyou'd be tuning against a scale factor that isn't 1, and the result will look\nright for you and wrong for everyone else. There's a **`use this window as\nref`** button that fixes this in one click.\n\n| Folder                   | What's in it                                                   |\n| ------------------------ | -------------------------------------------------------------- |\n| `fit`                    | reference window, scale clamps, the two responsive breakpoints |\n| `shape`                  | card size, count, ring radius, corner, art crossfade           |\n| `timing` \u002F `stage`       | the entry animation                                            |\n| `meta`                   | the type either side of the ring, and its morph                |\n| `pointer` \u002F `side cards` | how the ring reacts to a cursor                                |\n| `honey`                  | the threads between cards                                      |\n| `glass`                  | the refracting lip along the top and bottom edges              |\n\n## Making it yours\n\n### Projects\n\nEverything lives in one array, `components\u002Fring\u002Fprojects.js`:\n\n```js\n{ file: \"10.webp\", name: \"Matchday\", type: \"Motion\", year: \"2025\" }\n```\n\nDrop an image in `public\u002F`, add a row, done. The array does four jobs at once —\nit's the atlas packing order, the order art is dealt around the ring, the\nproject column top-right, and the `01`–`18` numbering. Reordering rows moves\nall four together.\n\n> **The list is in ring order, not filename order.** It reads shuffled against\n> the file numbers, and that's deliberate: entry _n_ sits one slot along the\n> ring from entry _n−1_, which is what lets the column count up as the carousel\n> turns. Reorder these rows to change the sequence; don't reach for\n> `imageOffset`, which rotates the art without moving the list with it.\n\nAll of the sample data is placeholder: every `type` and `year` is invented,\nnames marked `(*)` are guesses at the subject, and **the images are other\npeople's work** collected to build the layout against. See\n[About the artwork](#about-the-artwork).\n\n### Fonts\n\nThree families, declared in `app\u002Fglobals.css` and looked up **by name** from\n`components\u002Fring\u002Fparams.js` — so if you swap one, change it in both places.\n\n| Family           | Used for                               | Weights  | Licence                                                |\n| ---------------- | -------------------------------------- | -------- | ------------------------------------------------------ |\n| Satoshi          | project name, discipline, index column | 400, 500 | [Fontshare](https:\u002F\u002Ffontshare.com\u002Ffonts\u002Fsatoshi), free |\n| Geist            | number, year, load counter             | 400      | [OFL](https:\u002F\u002Fgithub.com\u002Fvercel\u002Fgeist-font)            |\n| PP Neue Montreal | intro heading, cursor tag              | 400      | Pangram Pangram, **commercial**                        |\n\n> [!IMPORTANT]\n> **PP Neue Montreal is not licensed by this project.** It is a commercial\n> typeface from [Pangram Pangram](https:\u002F\u002Fpangrampangram.com\u002Fproducts\u002Fneue-montreal)\n> and the file is included here **for local development and evaluation only**.\n> It is **not** covered by this repo's MIT licence and **must not be used for\n> commercial purposes**. If you ship anything with it, buy your own licence —\n> or swap `textFont` in `params.js` for Satoshi, which is free for commercial\n> work and already included.\n\nSatoshi and Geist are both free to redistribute and use commercially.\n\n> Fonts are served as `.otf`\u002F`.ttf` (~340 KB total). Converting to `woff2`\n> would cut that by roughly 60% — worth doing before this goes anywhere real.\n\n## How it's put together\n\n```\napp\u002F\n  page.js            renders \u003CCarousel \u002F>\n  globals.css        Tailwind, @font-face, page background\n\ncomponents\u002F\n  Carousel.jsx      the component — renderer, resize, input, per-frame\n                     layout, entry timeline\n  ring\u002F\n    projects.js      the work\n    params.js        every tunable\n    utils.js         maths helpers\n    atlas.js         packs all the art into one texture\n    meta.js          the type either side of the ring\n    splitText.js     the intro heading\n    tag.js           the \"View\" tag on the cursor\n    gui.js           the dev panel\n  shaders\u002F\n    planeShaders.js  the ring — SDFs, goo, glass, tag\n    textShaders.js   the heading's per-glyph reveal\n```\n\nTwo ideas explain most of the rest.\n\n**One shader, one draw call.** The ring, the threads between cards, the glass\nlip along the screen edges and the cursor tag are all evaluated per pixel in\n`planeShaders.js`. Card positions arrive as uniform arrays, and the fragment\nshader blends their distance fields with a smooth minimum — that soft blend is\nthe goo. Because the tag is drawn in the same pass, its label can invert\nagainst whatever pixels it happens to be sitting on.\n\n![The ring mid-entry: cards on the right have separated cleanly while those on\nthe left are still fused into one another with thick necks between\nthem](docs\u002Fentry.png)\n\nThe entry is the clearest look at what the smooth minimum is doing. Every card\nstarts merged inside the one before it and peels away in sequence, so at any\nmoment during the unfurl some pairs have fully separated, some are joined by a\nthinning neck, and some are still one blob.\n\n**Loading is part of the animation.** The atlas binds on the first frame and\nfills in as images arrive, with the first card's art requested at high priority\nso it can be shown while the rest are still downloading. The counter at the\nbottom of the screen isn't a readout beside the entry — it _is_ the gate. The\nseed card is born, holds at centre, and the ring launches on the frame the\nnumber reaches 100.\n\nFor the deeper technical notes — coordinate conventions, the responsive model,\nand the handful of things here that look like bugs but aren't — see\n[AGENTS.md](AGENTS.md).\n\n## Status\n\nThis is a work in progress, published because the rendering approach might be\nuseful to someone. Known gaps:\n\n- **Clicking a card centres it but doesn't open anything.** The \"View\" tag\n  promises a destination that doesn't exist yet.\n- **No reduced-motion support.** Six seconds of animated blur with no escape\n  hatch.\n- **No keyboard control.** Arrow keys should step the ring; the project column\n  can't be clicked to jump.\n- **Phone widths are approximate.** The tight breakpoint was tuned at the 640\n  end of its range and drifts below ~500px.\n- **No tests.** `npm run build` and `npm run lint` are the whole safety net,\n  and GLSL only compiles at runtime — a shader typo builds green.\n\n## Contributing\n\nIssues and pull requests are welcome.\n\n- Run `npm run lint` and `npx prettier --write .` before opening a PR.\n- Load the page after touching anything in `components\u002Fshaders\u002F` — GLSL is\n  compiled in the browser, so a build passing proves nothing about it.\n- New tunables belong in `params.js` with a matching control in `ring\u002Fgui.js`,\n  not hardcoded in the layout loop.\n- Please don't commit font binaries or client artwork.\n\n## Credits\n\n- The gooey text morph — two blurred copies fused through an SVG alpha\n  threshold — is a widely circulated CodePen technique, adapted here to run\n  one-shot per card change instead of on a loop.\n- Simplex noise in the ring shader is\n  [webgl-noise](https:\u002F\u002Fgithub.com\u002Fashima\u002Fwebgl-noise) by Ian McEwan (Ashima\n  Arts) and Stefan Gustavson, MIT.\n- Arrow icon from [SVG Repo](https:\u002F\u002Fwww.svgrepo.com\u002F).\n\n## License\n\n[MIT](LICENSE) for the source code. **Not** for anything in `public\u002F`.\n\n### About the artwork\n\nThe project images are **not mine**. I picked them up from Behance, more or\nless at random, purely so there was real work to build and judge the layout\nagainst — a carousel full of grey rectangles tells you nothing about whether\nthe thing looks good.\n\nI claim no ownership of any of it, the MIT licence above does not extend to\nit, and none of this is an endorsement of reusing it. If you fork this,\nreplace the images with your own work.\n\nThe names, disciplines and years shown next to them are invented placeholders\nand are not claims about who made what.\n\n> **If you made one of these pieces** and would like to be credited, or would\n> like it removed, please [open an issue](..\u002F..\u002Fissues) and I'll sort it out\n> straight away.\n\nThe bundled PP Neue Montreal is likewise not licensed by this project — see\n[Fonts](#fonts).\n","Viscose 是一个基于 WebGL 的极简主义作品集轮播组件，通过单个 fragment shader 渲染环形卡片布局，实现卡片融合、拉伸成线及物理感交互效果。核心采用 Three.js 与自定义 SDF（有向距离场）着色器，支持滚动、拖拽、悬停和点击驱动的动量式旋转，所有视觉变化（如卡片形变、线程生成、边缘折射）均由同一 GPU 计算通路实时生成。适用于设计师、前端开发者或创意技术从业者展示个人项目，强调视觉叙事性与性能一体化的数字作品集场景。",2,"2026-08-17 02:30:08","CREATED_QUERY"]