{"_id":"@beautifullife/golden-text","_rev":"2-56a33624ec5eded5b2176367f6feec67","name":"@beautifullife/golden-text","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@beautifullife/golden-text","version":"0.1.0","keywords":["three","threejs","webgl","typography","3d-text","header","vue","custom-element"],"author":{"url":"https://github.com/nrrb","name":"Nicholas Bennett"},"license":"MIT","_id":"@beautifullife/golden-text@0.1.0","maintainers":[{"name":"beautifullife","email":"nicholasbennett.work@gmail.com"}],"homepage":"https://github.com/nrrb/golden-text#readme","bugs":{"url":"https://github.com/nrrb/golden-text/issues"},"dist":{"shasum":"b781b2c926bd91599a896bf624720cb4ab902ff0","tarball":"https://registry.npmjs.org/@beautifullife/golden-text/-/golden-text-0.1.0.tgz","fileCount":15,"integrity":"sha512-zWwBArYIOodtqt8c1hEBj6H92aIMBytrj3EZsFoNYmoURZRTOeZS5+hrSj7FybPrbGKnzbiHLWy75dzzIZXaoA==","signatures":[{"sig":"MEQCIAyuOcc0Y7p0Oy3gdR+GJhER52eu/f3MWHxyzZa178zBAiAF1FwI+pEKocp6C12KBytyfQ5PTDZsiOeDi3PKCJ9V7A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":107382},"type":"module","exports":{".":{"types":"./types/index.d.ts","import":"./dist/index.js"},"./vue":{"types":"./types/vue.d.ts","import":"./dist/vue.js"},"./core":{"types":"./types/core.d.ts","import":"./dist/core.js"},"./element":{"types":"./types/element.d.ts","import":"./dist/element.js"},"./presets":{"types":"./types/presets.d.ts","import":"./dist/presets.js"},"./package.json":"./package.json"},"gitHead":"2fb99641cf1e22bd35b7037345fba82f35046917","scripts":{"dev":"vite","build":"npm run build:lib && npm run build:studio","preview":"vite preview --port 4173","build:lib":"vite build --config vite.config.lib.js","pack:check":"npm run build:lib && npm pack --dry-run","preview:lib":"npm run build:lib && vite preview --config vite.config.lib.js --port 4174","build:studio":"vite build"},"_npmUser":{"name":"beautifullife","email":"nicholasbennett.work@gmail.com"},"repository":{"url":"git+https://github.com/nrrb/golden-text.git","type":"git"},"_npmVersion":"11.3.0","description":"Gold 3D type as a drop-in web header — a transparent WebGL canvas driven by a preset.","directories":{},"sideEffects":["./dist/element.js"],"_nodeVersion":"22.14.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vue":"^3.5.41","vite":"^8.2.2","three":"^0.185.1","@vitejs/plugin-vue":"^6.0.8"},"peerDependencies":{"vue":">=3.4.0","three":">=0.180.0"},"peerDependenciesMeta":{"vue":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/golden-text_0.1.0_1787453348680_0.0535140921596593","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@beautifullife/golden-text","version":"0.2.0","description":"Gold 3D type as a drop-in web header — a transparent WebGL canvas driven by a preset.","type":"module","license":"MIT","author":{"name":"Nicholas Bennett","url":"https://github.com/nrrb"},"repository":{"type":"git","url":"git+https://github.com/nrrb/golden-text.git"},"homepage":"https://github.com/nrrb/golden-text#readme","bugs":{"url":"https://github.com/nrrb/golden-text/issues"},"publishConfig":{"access":"public"},"keywords":["three","threejs","webgl","typography","3d-text","header","vue","custom-element"],"exports":{".":{"types":"./types/index.d.ts","import":"./dist/index.js"},"./core":{"types":"./types/core.d.ts","import":"./dist/core.js"},"./vue":{"types":"./types/vue.d.ts","import":"./dist/vue.js"},"./element":{"types":"./types/element.d.ts","import":"./dist/element.js"},"./presets":{"types":"./types/presets.d.ts","import":"./dist/presets.js"},"./package.json":"./package.json"},"sideEffects":["./dist/element.js"],"scripts":{"dev":"vite","build":"npm run build:lib && npm run build:studio","build:lib":"vite build --config vite.config.lib.js","build:studio":"vite build","preview":"vite preview --port 4173","preview:lib":"npm run build:lib && vite preview --config vite.config.lib.js --port 4174","pack:check":"npm run build:lib && npm pack --dry-run"},"peerDependencies":{"three":">=0.180.0","vue":">=3.4.0"},"peerDependenciesMeta":{"vue":{"optional":true}},"devDependencies":{"@vitejs/plugin-vue":"^6.0.8","three":"^0.185.1","vite":"^8.2.2","vue":"^3.5.41"},"_id":"@beautifullife/golden-text@0.2.0","gitHead":"70b68442eb85aca8ae990e65ef5bb7a43c41823d","_nodeVersion":"22.14.0","_npmVersion":"11.3.0","dist":{"integrity":"sha512-NtZEf1yGiNz0DdV61HwTc4WMJjPGM8LBLTc4IwVVeYf1bp7JZ/PfS3WeqIGMn2al5924vOkeMLTAQyo26wMQMQ==","shasum":"f62c678b15f41a2e42f8fc4c5f6dbe45e7d4d6f0","tarball":"https://registry.npmjs.org/@beautifullife/golden-text/-/golden-text-0.2.0.tgz","fileCount":16,"unpackedSize":121370,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIB0eElsgQYkmQwu+0qW48PXhAtrbSXYNM8P6QVL8/nCvAiAw26W24Gq7iLzmrZET0zaZ0YNM98Dfyg0dz8GFMsiPwA=="}]},"_npmUser":{"name":"beautifullife","email":"nicholasbennett.work@gmail.com"},"directories":{},"maintainers":[{"name":"beautifullife","email":"nicholasbennett.work@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/golden-text_0.2.0_1787458497179_0.30780969554478776"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-23T02:49:08.555Z","modified":"2026-08-23T04:14:57.474Z","0.1.0":"2026-08-23T02:49:08.823Z","0.2.0":"2026-08-23T04:14:57.325Z"},"bugs":{"url":"https://github.com/nrrb/golden-text/issues"},"author":{"name":"Nicholas Bennett","url":"https://github.com/nrrb"},"license":"MIT","homepage":"https://github.com/nrrb/golden-text#readme","keywords":["three","threejs","webgl","typography","3d-text","header","vue","custom-element"],"repository":{"type":"git","url":"git+https://github.com/nrrb/golden-text.git"},"description":"Gold 3D type as a drop-in web header — a transparent WebGL canvas driven by a preset.","maintainers":[{"name":"beautifullife","email":"nicholasbennett.work@gmail.com"}],"readme":"# golden-text\n\nGold 3D type as a drop-in web header: a transparent WebGL canvas that hovers over your page,\ndriven by a preset you capture in the bundled studio.\n\nTwo halves live in this repo: the **package** (`src/lib`), and the **studio** (`src/studio`), a\ndevelopment tool for finding a look and exporting it as JSON the package eats.\n\n```\nnpm i @beautifullife/golden-text three\n```\n\n`three` is a peer dependency, so your bundler uses the copy you already have rather than loading a\nsecond one. Vue is an optional peer, needed only for the `/vue` entry.\n\n## Using it\n\n### Any project\n\n```js\nimport { createGoldenText } from '@beautifullife/golden-text'\n\nconst header = createGoldenText(document.querySelector('.headline'), {\n  preset,                       // the JSON the studio's Copy button produces\n  lines: ['Selected', 'Work'],  // optional: override the preset's wording\n  transparent: true,            // default\n  interactive: false,           // default: click-through, never takes the wheel\n  animate: false                // default: draws one frame, runs no rAF\n})\n\nawait header.ready\nheader.setLines('Say', 'Hello')\nheader.setPreset(anotherPreset)  // swaps the frame, keeps the context\nheader.destroy()\n```\n\nPass a `<canvas>` to use it directly, or any element to have one created and appended. Either way\n**the box needs a height of its own**. The canvas fills it and nothing more.\n\n### Plain HTML, React, Astro, Svelte, Webflow\n\n```html\n<script type=\"module\" src=\"/path/to/@beautifullife/golden-text/dist/element.js\"></script>\n\n<div style=\"position: relative\">\n  <h2 class=\"visually-hidden\">Selected Work</h2>\n  <golden-text lines=\"Selected|Work\" preset='{\"camera\":{ ... }}' style=\"height: 18rem\"></golden-text>\n</div>\n```\n\nAttributes: `preset` (JSON string), `lines` (`|`-separated), `transparent`, `interactive`,\n`animate`. Frameworks that set properties rather than attributes can assign an object straight to\n`element.preset`. See [`examples/plain.html`](examples/plain.html) for a working page with no build\nstep at all.\n\n### Vue\n\n```vue\n<script setup>\nimport { GoldenText } from '@beautifullife/golden-text/vue'\n</script>\n\n<template>\n  <div class=\"headline\">\n    <h2 class=\"visually-hidden\">Selected Work</h2>\n    <GoldenText :preset=\"sectionHeader\" :lines=\"['Selected', 'Work']\" />\n  </div>\n</template>\n\n<style>\n.headline { position: relative; height: 18rem; }\n</style>\n```\n\n### Entry points\n\n| Import | What you get |\n|---|---|\n| `@beautifullife/golden-text` | Everything, with the Helvetiker typeface bundled (~23KB gzipped) |\n| `.../core` | The same API with **no font**: pass `font` or `fontUrl` yourself |\n| `.../vue` | The `GoldenText` Vue component |\n| `.../element` | Registers `<golden-text>` (importing it is the registration) |\n| `.../presets` | Just the preset helpers and the built-in presets |\n\nThe package ships no CSS. Types are included.\n\n## Presets\n\nA preset is everything needed to reproduce one frame:\n\n```json\n{\n  \"name\": \"Section header\",\n  \"camera\": { \"position\": [0, 0.05, 4.25], \"target\": [0, 0, 0], \"fov\": 28 },\n  \"text\": { \"line1\": \"Golden\", \"line2\": \"Rings\", \"rotation\": [0, -25, 0] },\n  \"scene\": { \"metal\": \"gold\", \"ringCount\": 900, \"seed\": 7, \"clearance\": 1.6,\n             \"time\": 6.2, \"motion\": false, \"autoRotate\": false }\n}\n```\n\n`seed` and `time` are what make this reproducible rather than approximate: the ring field is\ngenerated from the seed rather than `Math.random`, and `time` is the frozen animation clock, so the\nrings come back in exactly the arrangement you captured, not just the camera. Anything you leave\nout falls back to a default; anything of the wrong shape throws with the field named.\n\nMetals: `gold`, `rose`, `copper`, `steel`.\n\n## Motion\n\nThree layers, each switched on independently. All of them are driven by time and scroll only.\nNothing here responds to the pointer.\n\n```js\ncreateGoldenText(el, {\n  preset,\n  shine: true,                       // or { cycleSeconds: 18, fps: 30 }\n  reveal: true,                      // or { tilt: 0.25, lift: 0.12, travel: 0.6 }\n  glitch: { threshold: 2.5, duration: 180, cooldown: 900, strength: 1 }\n})\n```\n\nThe same three are attributes on the custom element (`shine reveal glitch`) and props on the Vue\ncomponent.\n\n**shine** crawls the specular highlight across the gold on a slow clock, 18 seconds per turn by\ndefault. The type itself does not move.\n\nThere is no light in this scene to swing around and no environment map to rotate: the gold is a\nmatcap, a picture of a lit sphere looked up by the view-space normal. So the shine rotates that\nlookup instead. The whole baked lighting rig turns with it and the highlight travels exactly as a\nmoving key light would, while the gold ramp itself is untouched. It is two extra instructions in\nthe fragment shader, which is cheaper than the light rig it replaces.\n\n**reveal** tilts the type in from 0.25 radians and lifts it into place as the header scrolls in,\neased with smoothstep rather than linearly. It runs once. When progress reaches 1 the header stops\nrecalculating, and leaving the viewport re-arms it. The tilt adds to whatever rotation the preset\nasked for rather than replacing it.\n\n**glitch** throws an RGB split and horizontal slice tearing across the type for 180ms when scroll\nvelocity crosses 2.5 pixels per millisecond, then snaps back. It decays in two hard steps rather\nthan fading, because a clean fade reads as a transition and a stepped one reads as a fault. A 900ms\ncooldown stops a long fling from firing it repeatedly.\n\nThis is done in the existing single pass, as extra samples of the matcap, so it needs no\n`EffectComposer` and no new dependency. A full-screen version that also displaced the page behind\nthe canvas would need postprocessing, which is not set up here.\n\n### What each layer costs\n\n| Layer | Render loop | Listeners |\n|---|---|---|\n| shine | one loop while the header is on screen, capped to 30fps on touch devices | none |\n| reveal | none: renders on demand while arriving, then stops | shared |\n| glitch | borrows the loop for the 180ms burst, hands it back | shared |\n\nOne render loop per header at most, never two, and it only runs while the header is on screen. One\nscroll listener serves the whole page however many headers are on it, rAF-gated, so raw scroll\nevents never drive animation directly.\n\n`prefers-reduced-motion` disables all three, leaving the still frame the header would have settled\ninto.\n\nThe number most likely to need tuning on real hardware is the glitch threshold. 2.5 px/ms sits\nabove thumb-scrolling and below a hard fling in testing here, but momentum scrolling varies by\ndevice, so try it on the phones you care about and raise it if the glitch fires during ordinary\nreading.\n\n## Finding a look: the studio\n\n```bash\nnpm install\nnpm run dev          # the studio at http://localhost:5178\n                     # headers on a page at http://localhost:5178/demo.html\n```\n\nTo create the standalone static site:\n\n```bash\nnpm run build:studio # -> dist-studio/\nnpm run preview      # verify the production build locally\n```\n\nDeploy the contents of `dist-studio/`. This build contains only the studio's `index.html` and its\nassets; the development-only `demo.html` is not emitted. Asset URLs are relative, so the directory\ncan be hosted at the root of a domain or under a subpath without rebuilding.\n\nIt opens in overlay mode: the type floating over a dark stand-in page, framed with the **Wide\nbanner** preset, with the wheel scrolling that page so you can judge the header against real\ncontent.\n\n| Drag | orbit the camera |\n|---|---|\n| Scroll / pinch | scroll the page behind, or dolly the camera (see <kbd>S</kbd>) |\n| <kbd>S</kbd> | switch the wheel between the page and the title |\n| <kbd>H</kbd> | show / hide the control panel |\n\nTo capture a look:\n\n1. Turn off **Motion** and **Orbit** so the frame holds still (the starting preset already has).\n2. Orbit and zoom to taste, turn the type on the **X / Y / Z** sliders, and set the **Lens**. A\n   long lens (20 to 35°) flattens the type for a wide header; a short one (70 to 100°) throws it\n   into dramatic perspective.\n3. Raise **Clearance** until the rings stop crossing the letters. It is the radius of the empty\n   bubble the type sits in, so a wide value turns the field into a halo around the words. Then\n   **Shuffle** until the remaining rings sit where you want them.\n4. Hit **Capture**, then **Copy** or **Download**, and paste the JSON into your project.\n\nPaste a preset back into the panel and hit **Apply** to return to it, or add it to `PRESETS` in\n`src/lib/presets.js` to keep it in the **Start from** dropdown for good.\n\nIn the studio, `window.__goldenRings` exposes the scene, camera and controls for tuning from the\nconsole. The library never sets that global; the studio opts in via `debug: true`.\n\n## Limits to know about\n\n- **One WebGL context per header.** Browsers keep only so many and drop the oldest past roughly a\n  dozen, which looks like headers going blank as you scroll. Fine for the handful of section\n  headers a page normally has; you get a console warning past eight. For more than that, render\n  them to images instead.\n- **Framing is relative to the box's aspect ratio.** A preset captured in a wide studio window\n  frames tighter in a short header strip. Expect to re-tune the camera distance.\n- **The heading text is yours.** The canvas is `aria-hidden` decoration. Put the real heading in\n  the markup and hide it visually, as every example here does. Screen readers and search engines\n  need it.\n- **Reduced motion is respected**: `animate` is ignored when the reader has asked for less motion.\n\n## How it works\n\nVue owns state and the DOM in the studio; Three.js owns the frame loop. They meet in exactly one\nplace, a watcher calling imperative setters, so a 60fps loop never touches Vue reactivity.\n\n```\nsrc/lib/                     published\n  index.js                   createGoldenText with the typeface bundled\n  core.js                    the same, bring your own font\n  core/experience.js         renderer, camera, controls, frame loop, setters\n  core/goldenText.js         the drop-in: preset in, rendered header out\n  core/goldMatcap.js         procedural matcap generator (gold/rose/copper/steel)\n  core/ringField.js          the instanced, seeded torus field\n  core/goldText.js           extruded type, baseline-aligned\n  presets.js                 preset shape, capture, parse\n  element.js                 <golden-text>\n  vue/GoldenText.vue         Vue wrapper over createGoldenText\nsrc/studio/                  development only, never published\ntypes/                       hand-written declarations\nexamples/plain.html          the built package, no bundler\n```\n\n### Notable decisions\n\n**One draw call instead of a thousand.** The scene this grew out of adds 1000 separate `Mesh`\nobjects. Here it is a single `InstancedMesh` with a per-instance matrix, which leaves enough\nheadroom to spin every ring on its own axis each frame, and to raise the cap to 2000 rings.\n\n**The matcap is generated, not downloaded.** `goldMatcap.js` draws the lit-sphere image into a\ncanvas at runtime: a key light, a warm bounce, a horizon band (metals reflect the sky/ground\nsplit), a tight specular, and a darkened silhouette. No binary texture assets, and each metal is a\nreal colour ramp rather than a tint multiplied over gold. Multiplying blue over gold gives olive,\nnot steel. It costs ~190ms, so it is drawn once per metal and shared; the parsed font is shared\ntoo.\n\n**Type is aligned on the baseline.** `geometry.center()` aligns each line by its bounding box, so a\nline with a descender (\"Rings\") sits higher than one without (\"Golden\") and the two collide. Each\nline is centred on X and Z only, then positioned by baseline using the font's cap height and a\nfixed leading.\n\n**A still header runs no loop.** It draws one frame and stops. With `animate` or `interactive` it\nruns a loop, but only while on screen (`IntersectionObserver`), and it never claims the wheel. A\nheader that eats page scroll traps the reader.\n\n### Other details\n\n- The loop pauses on `visibilitychange` and resizes from a `ResizeObserver` on the canvas.\n- Pointer parallax rotates a wrapper group, not the camera, so it never fights `OrbitControls`, and\n  it settles to zero when motion is off so a frozen frame depends only on what a preset captures.\n- Coarse-pointer devices build the torus at lower resolution.\n- Nothing in `src/lib` touches `window` at import time, so server rendering is safe.\n\n## Releasing\n\n```bash\nnpm run build:lib     # -> dist/\nnpm run pack:check    # build, then show exactly what would ship\nnpm publish\n```\n\nNo `--access public` flag is needed: `publishConfig` in the manifest marks the package public, so a\nscoped package cannot go private by forgetting it. Private scoped packages need a paid npm plan.\n\n## Credits\n\nScene concept from the [Three.js Journey](https://threejs-journey.com/) 3D-text lesson, by way of\nthe Golden Rings Webflow demo. `helvetiker_regular.typeface.json` ships with three.js (MIT).\n","readmeFilename":"README.md"}