npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@threecyborgs/scroll-compress

v0.2.0

Published

Framework-free Three.js vertical scroll compression for browser apps.

Readme

Scroll Compress Core

Framework-free Three.js scroll compression for browser apps installed through npm.

The package is generic JavaScript. It can be used from Node-based build systems like Vite, Next, Astro, Webpack, or Rollup, but the effect itself runs in the browser because it needs DOM measurement, pointer events, canvas, and WebGL.

Install

Published to the public npm registry under the @threecyborgs scope:

npm install @threecyborgs/scroll-compress three

Quick start

Minimal, copy-paste, and guaranteed to work — no CSS and no canvas id required. The library sizes the canvas itself and runs only in the browser.

<canvas></canvas>
<script type="module">
  import { createScrollCompression } from '@threecyborgs/scroll-compress';

  await createScrollCompression({
    canvas: document.querySelector('canvas'),
    sections: [
      { id: 'a', height: 480, html: '<h1>Hello</h1><p>Real HTML, rendered in WebGL.</p>' },
      { id: 'b', height: 480, html: '<h2>Scroll</h2><p>Into the compression bands.</p>' },
    ],
  });
</script>

There is a runnable version in examples/minimal/ (npm run dev, then open /examples/minimal/).

Frameworks: call createScrollCompression client-side only (inside useEffect / onMounted / a client component, or behind a dynamic import). It throws a clear error if run during SSR.

React and Vue

Optional wrappers handle the client-only lifecycle and cleanup for you. react and vue are optional peer dependencies.

// React
import { useScrollCompression } from '@threecyborgs/scroll-compress/react';

function Hero() {
  const { canvasRef } = useScrollCompression({
    sections: [{ id: 'a', height: 480, html: '<h1>Hi</h1>' }],
  });
  return <canvas ref={canvasRef} />;
}
<!-- Vue -->
<script setup>
import { ref } from 'vue';
import { useScrollCompression } from '@threecyborgs/scroll-compress/vue';

const canvas = ref(null);
useScrollCompression(canvas, {
  sections: [{ id: 'a', height: 480, html: '<h1>Hi</h1>' }],
});
</script>

<template><canvas ref="canvas"></canvas></template>

Both create the effect on mount and destroy() it on unmount. Options are read once on mount (reset React via key).

Use

import { createScrollCompression } from '@threecyborgs/scroll-compress';

const controller = await createScrollCompression({
  canvas: document.querySelector('#stage'),
  sections: [
    {
      id: 'intro',
      height: 560,
      html: '<h1>Intro</h1><p>Real HTML, rendered through Three.js.</p>',
    },
    {
      id: 'chapter',
      height: 620,
      html: '<h2>Chapter</h2><p>Scroll into fixed compression slots.</p>',
    },
  ],
  // Styles applied to the captured HTML. Pass CSS text directly so it works in
  // any consumer app. (See "Styling the captured HTML" below for @import.)
  pageStyles: `
    h1, h2 { font-family: system-ui, sans-serif; margin: 0 0 12px; }
    p { color: #333; line-height: 1.5; }
  `,
  debugGlobal: '__scrollCompressDebug',
});

// Optional: wait until images have been inlined before, e.g., screenshotting.
await controller.ready;

The canvas does not need any special attributes — createScrollCompression sets the required layoutsubtree attribute on it automatically.

API

const controller = await createScrollCompression({
  canvas,           // HTMLCanvasElement (or use canvasSelector)
  sections,         // Iterable of { id, height, html } or { element }
  settings,         // Optional tuning overrides
  pageStyles,       // CSS text applied to the captured HTML
  sourceRoot,       // Where section DOM is mounted (defaults to the canvas)
  eventTarget,      // Scroll/resize target (defaults to window)
  debugGlobal,      // Name to expose the controller on window for debugging
  onAction,         // Callback for clicks on [data-action] elements
  resolveImageUrl,  // (absoluteUrl) => url — rewrite/proxy image URLs
  manageCanvasStyle,// Default true: library sizes the canvas (fixed, full-viewport)
});

By default the canvas is styled as a fixed, full-viewport overlay so the effect works with zero CSS. To place it in your own layout, pass manageCanvasStyle: false and give the canvas a size yourself.

Controller:

controller.getState();
controller.scrollTo(400);
controller.jumpTo(400);
controller.refresh();
controller.destroy();
await controller.ready; // resolves after the first image-inlining pass

Images

The renderer rasterizes each section by serializing it to an SVG snapshot, which means every image must be embeddable as a data: URI. This package inlines images for you — both <img src> and inline-style background-image — fetching each unique URL once, converting it to a data: URI, and swapping it in. (Background images coming from external/stylesheet rules are not inlined; use inline styles or pageStyles.) Because of that, images must be one of:

  • same-origin, or
  • cross-origin with CORS enabled (Access-Control-Allow-Origin), or
  • already a data: URI.

A plain <img> from a CDN that doesn't send CORS headers cannot be read in the browser. For those, use resolveImageUrl to route through a CORS proxy or a same-origin mirror:

await createScrollCompression({
  // ...
  resolveImageUrl: (url) => `/img-proxy?u=${encodeURIComponent(url)}`,
});

If an image can't be inlined, it's logged once (not every frame) and falls back to a transparent placeholder marked with data-scroll-compress-inline-failed so you can style a fallback in pageStyles.

Styling the captured HTML

pageStyles is injected into the SVG snapshot. Prefer passing CSS text directly (as above) or importing your stylesheet as a string via your bundler (e.g. Vite's ?inline/?raw):

import sheet from './my-styles.css?inline';
await createScrollCompression({ /* ... */, pageStyles: sheet });

@import url(...) also works, but the URL is resolved by the browser at runtime and must be reachable from the consumer app — a relative path like /src/theme.css only resolves inside this repo, not in your app.

Optional demo theme

The package ships an optional cosmetic theme used by the prototype. It is not required and contains global element selectors, so import it only if you want that look:

import '@threecyborgs/scroll-compress/theme.css';

(The old @threecyborgs/scroll-compress/styles.css path still works but just re-exports theme.css.)

Gotchas

  • No required CSSmanageCanvasStyle sizes the canvas for you. Pass manageCanvasStyle: false to position it yourself.
  • Client-side only — call it in the browser; it throws during SSR.
  • layoutsubtree is automatic — you don't need to add it to the canvas.
  • Image origin matters — see Images. Non-CORS CDN images can't be inlined; use resolveImageUrl.
  • One warning, not thousands — a failed image logs once per URL, then degrades.
  • net::ERR_ABORTED in the network panel — when you pass external image URLs, this package replaces them with inlined copies, so the original <img> request is intentionally cancelled. This is expected, not a bug. Inlining as data: URIs up front avoids it entirely.

Agent skill

This package ships an agent skill at skills/scroll-compress/SKILL.md so AI coding agents get versioned, correct integration guidance straight from the dependency. Tools that follow the npm skills convention (e.g. skills-npm, skillpm) discover it under node_modules/**/skills/*/SKILL.md and symlink it into your agent directory:

npx skills-npm setup   # links bundled skills into .cursor/skills, .claude/skills, ...

Current Renderer

  • One full-window Three.js renderer.
  • Real HTML captured into textured planes.
  • Fixed top and bottom compression bands.
  • Fixed slice slots.
  • Scroll changes source sampling, not slot position.
  • Width never narrows.
  • Partial edge slices are skipped instead of stretched.
  • No full-page framebuffer.

Demo

Run the local prototype:

npm run dev -- --port 4173

Then open:

http://127.0.0.1:4173/