Library / SDK
CatsJuice/sticker-forge avatar
CatsJuice/sticker-forge

Three Vite configs, two deploy targets, and a bundle the README never mentions

A tactile WebGL sticker maker with rich text, image uploads, and interactive peel physics.

742 stars67 forksJavaScriptMIT

At a glance

What is it?
Sticker Forge renders text and uploaded images as die-cut WebGL stickers with peel physics, shipped as a web component, a classic script and an imperative API. The library half is documented carefully, down to the units. The application half is where the repo gets confusing: three Vite configs, two deployment runtimes, and an xhs build target that exists in the scripts and nowhere in the prose.
Who is it for?
The library half of Sticker Forge is the part worth reusing. Its option units are stated, its events are tabulated with the same normalized values on both APIs, and its SVG path removes scripts, event attributes, foreignObject and external references before rasterizing, which is the right order of operations for a component that accepts markup.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 64 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.

Editorial analysis

An xhs build target exists in the scripts and not in the docs

package.json carries three separate build paths that share one repository. `build` runs a typecheck, then the library build, then a production build through a runtime called vinext. `build:pages` sets `GITHUB_PAGES=true` and a site URL variable and calls a different builder, `next build --webpack`. And a third one, `build:xhs`, typechecks, builds with its own Vite configuration, `vite.xhs.config.ts`, and finishes by running a packaging script called `scripts/package-xhs.mjs`. There is a matching `dev:xhs`. None of that appears in the README, which only ever describes two deliverables, a builder in `app/` and the reusable library in `lib/`. Three Vite configurations sit at the root, one general, one for the library and one for this xhs target, along with an `xhs/` directory, a `scripts/` directory and a `worker/` directory. Whatever xhs packages, it is a third distribution surface built from the same source. The manifest also mixes three runners in one file, Node's test runner for the suite, eslint for linting and tsc for types, so a contributor has to know which of the three reports what before a failing command tells them.

The peel recording is inlined into both library bundles

The sound is treated as an audio sprite rather than a timeline, which is why it can be shipped inside the JavaScript at all. Four layers are separated: the lift, a light crackle, a strong tear and the release, each levelled against the others, with randomized grains driven by drag velocity and acceleration. A slow drag is sparse, a fast one denser and brighter, holding still produces nothing, and reattaching swaps in a quieter low-passed texture rather than running the audio backwards. The bundled file has been trimmed, converted to mono and lightly high-passed before inlining, and the untouched recording stays in `lib/assets/`. The consequence of that choice is that the audio ships in both bundles, so a page that loads the ES module and the classic script downloads the recording twice. It can be swapped for any browser-decodable audio URL through `sound.src`, in which case slicing falls back to a generic duration-relative profile. Muting is either `sound.enabled` set to false or a volume of zero, and the progress value is consulted only for the initial lift and the final release, not for the grains in between.

The test script runs a full production build before it runs anything

Running the suite is one command, and it is not a fast one. It is defined as the full build followed by the Node test runner over `tests/*.test.mjs`, so `npm test` performs a typecheck, builds the library bundle and then produces the application build before a single assertion runs. Getting a look at the thing in a browser is the shorter path:

bash
npm install
npm run dev

That dev server is started with a hostname of 0.0.0.0, so it is reachable from other machines on the network by default rather than only from loopback. The local production build is a separate pair of commands, `npm run build` for the hosted app and both library bundles, and `npm run build:pages` for the static export that the Pages workflow runs. The typecheck is deliberately non-incremental, `tsc --noEmit` with incremental disabled, which is the right call for a build that gets injected with different flags but does mean every invocation re-reads the whole tree. Linting ignores `dist` and `.next` and nothing else.

setSource is awaitable and setOptions is not

Both the custom element and the object returned by `createSticker()` expose the same six methods:

ts
setSource(source): Promise<void>
setOptions(partialOptions): void
reset(): void
resize(): void
getState(): StickerState
destroy(): void

`setOptions()` deep-merges nested groups, so a partial object leaves the other keys alone. The asymmetry worth planning around is the artwork path: calling `await sticker.setSource(...)` gives you a promise that settles when the texture is rebuilt, while passing a `source` object through `setOptions()` triggers the same rebuild but returns nothing to await. Text sources carry `text`, `color`, `fontFamily` and `fontWeight`, and the engine waits for the requested browser font before rebuilding, so a font that never loads leaves the texture waiting. Cleanup differs by API too. An imperative instance needs `destroy()` called before a single-page application removes its target, while a disconnected `<sticker-forge>` element cleans itself up. `reset()` returns the sticker to its flat state, and `resize()` is the hook for a host that changes the element's box without reloading the artwork.

The ready event carries different data depending on which API you used

Six events are documented, and one of them changes shape between the two surfaces. For `ready`, the imperative target receives a detail object with `width` and `height`, while the Web Component version receives no detail at all. The rest are consistent: `peelstart` gives `{ amount, progress, origin }`, `peelchange` adds an optional `direction`, `peelend` reports `{ amount, progress, willReset }`, `detachcomplete` reports a fixed `{ progress: 1 }`, and `error` carries a message. `amount` and `progress` are the same normalized value running from 0 when the sticker is flat to 1 when it is fully lifted, and `origin` and `direction` are points in sticker-local coordinates. Where you listen also differs. The imperative version dispatches from the element you passed in, and the Web Component version listens on the element itself, with peel and error events crossing the shadow boundary. A front end wrapping this in a framework has to pick one listening style and stay with it, because a listener attached to the wrong element never fires at all.

Radius is a fraction, maxAngle is radians, and tilt is degrees

The option block mixes at least four unit systems without a common base, so a copied config needs reading rather than pasting. `peel.radius` is normalized against the sticker's short side, where 0.12 means twelve percent, which makes it scale-free. `peel.maxAngle` is in radians, and the imperative example passes 3.55, a little over half a turn. `tilt` is in degrees. Shadow blur and distance are in CSS pixels, and `peel.grabWidth` is also in CSS pixels but measured at full scale and then scaled with the sticker. Lighting has two different ranges: `intensity` accepts 0 to 1.5, while `ambient` and `softness` are 0 to 1. `lighting.direction` is a normalized view-space vector pointing from the sticker toward the incoming directional light, and it drives surface shading and projected shadow direction together, so moving it changes the shadow as well as the highlight.

SVG is sanitized, HEIC is decoded, and neither is connected to the prose

Raw SVG markup can be handed to the engine directly and no pre-sanitization is required, because the library always strips scripts, event attributes, `foreignObject` and external URL references before rasterizing, and exports `sanitizeSvgMarkup()` when you want the cleaned markup itself. That list is the whole of what is removed, so it is worth knowing where the boundary sits. Image sources accept anything the browser can decode, and the documented set is PNG, WebP, JPEG, GIF, AVIF and SVG data URLs. HEIC is not in that list, yet the tree carries a HEIC decoder dependency and a hand-written declaration file for it at the root. The dependency list also includes a transformer runtime under the Hugging Face namespace that the README never explains. Transparent images use their decoded alpha as the die-cut silhouette, and fully opaque ones fall back to the rectangular image boundary.

The app runs on Workers while the static build runs on Pages

The two deployment stories are genuinely different targets. The development and production commands go through vinext with a wrangler log path, which puts the app on a workers runtime, and the repository carries `worker/`, `workers/` and a `cloudflare-env.d.ts` alongside a database layer, a `db/` directory and drizzle configuration with a `db:generate` script. The Pages path is the static one: pushes to `main` trigger a workflow, and the same artifact can be produced with `npm run build:pages`. Its DNS is a CNAME for the `sticker` subdomain pointing at `catsjuice.github.io`, and the site is served from a domain of that subdomain over HTTPS, although the repository homepage field records the same address with an `http` prefix. The package version is 0.1.0, the manifest is marked private, and there are no GitHub releases, so there is no published artifact to diff a deployment against.

Editorial conclusion

The library half of Sticker Forge is the part worth reusing. Its option units are stated, its events are tabulated with the same normalized values on both APIs, and its SVG path removes scripts, event attributes, foreignObject and external references before rasterizing, which is the right order of operations for a component that accepts markup. The application half is a Next-compatible app on Workers with a separate static export, and anyone adopting it wholesale inherits three build configs and an xhs packaging target that the documentation does not mention. Before you take either half, check the Node floor against your CI, since 22.13 is the stated minimum, and remember the tests only run after a full build.

Frequently asked questions

What does Sticker Forge need before it will run?

Node.js 22.13 or newer, as stated in the README and matched by the engines field in the manifest. From a clean clone you run `npm install` and then `npm run dev`, which serves on all network interfaces rather than only on loopback.

Can I use Sticker Forge without the custom element?

Yes. `createSticker()` is asynchronous and resolves to a control object that exposes the same methods as the element, and renderer events are dispatched from the target element you passed in. For classic scripts the IIFE bundle exposes `window.StickerForge`.

Does Sticker Forge accept raw SVG markup?

Yes, and no pre-sanitization is needed. The library always removes scripts, event attributes, foreignObject and external URL references before rasterizing, processing the input locally, and it exports `sanitizeSvgMarkup()` when you need the cleaned markup.

How does the Sticker Forge peel sound work?

It is an audio sprite rather than a timeline, split into lift, crackle, tear and release layers that are levelled against each other, with grains driven by drag velocity and acceleration. Reattaching uses a quieter low-passed texture instead of reversed audio, and muting is `sound.enabled: false` or a volume of 0.

What license is Sticker Forge under?

MIT, with the licence text at the repository root and a separate third party notices file beside it. The package manifest itself is marked private and its version is 0.1.0.

Official sources

  1. CatsJuice/sticker-forge on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/catsjuice-sticker-forge.svg)](https://hysenlabs.com/projects/catsjuice-sticker-forge)