Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ Svifa follows [semantic versioning](https://semver.org).

### Added

- Drawing between pixels is on by default, with no CSS. Svifa chooses the blocks that scroll with the content and gives each a layer
of its own (`will-change: translate`, which it sets for as long as it runs and then puts back), splitting a block around a fixed
element inside it or a `z-index` that reaches past it, and holding sticky elements still while they are held. It chooses once the
page is idle and again when the page changes; an element that becomes fixed inside a block loses that block's layer before the
page is drawn again. Where a layer would change what is drawn over what (a fixed or sticky element with no `z-index`, followed by
content), it draws on whole pixels and says why once in the console. `data-svifa-subpixel="off"` leaves an element on whole
pixels, and `subpixel: false` turns it off. Firefox, which at a screen scale of 100% draws whole pixels whatever it is asked, and
a device with only a touch screen get no layers.
- Scrolling boxes inside the scroller glide the wheel themselves, with the same feel and drawn between pixels, and hand it on at
their end. Boxes found on the page are ready before the wheel first turns over them; a box with a Svifa of its own keeps it.
`nested: 'native'` leaves boxes to the browser, and a function decides for each box, giving one a feel of its own if it returns
options. `configure` passes on to them. While the page glides, a box's content moves with it exactly: the browser moves a box
with a layer of its own only by whole pixels, and Svifa puts back the part of a pixel it leaves out, in every box inside, also
one whose wheel is left to the browser.
- `snap`: a scroller with `scroll-snap-type` glides to the snap points its CSS sets. The wheel moves as far as it would without
snapping and the glide rests on the nearest snap point; with `mandatory`, one notch or one trackpad swipe moves one point.
`scrollTo` and same-page links rest on the snap point nearest their target. `scroll-padding`, `scroll-margin`,
Expand All @@ -20,14 +34,42 @@ Svifa follows [semantic versioning](https://semver.org).
is built with its framework's own tool from the package as it would be published, served as its framework serves it, and driven
in a browser, on each change.

### Changed

- `nested: true`, the default, now glides a scrolling box inside the scroller; it left the box to the browser before. `nested:
'native'` is the earlier behaviour.
- A page that names its elements in `subpixel` keeps them and no longer needs a CSS rule for them: Svifa sets each one's layer
itself.
- Drawn on whole pixels (with `subpixel: false`, in Firefox, which at a screen scale of 100% draws whole pixels, or where Svifa
cannot draw between pixels), a glide never moves slower than one pixel a sixtieth of a second (60 px a second), at any refresh
rate. Its last pixels come at an even pace instead of ever more slowly, which showed as the page standing still for up to about
180 ms and then hopping a pixel. On whole pixels a glide therefore ends sooner (one wheel notch at the default `lerp`: about 530 ms
instead of 874 ms), and a timed glide whose easing starts or ends slowly arrives a little before its time. Drawn between pixels, a
glide slows to a stop along its own curve, as before.

### Fixed

- Zoomed to a part of a pixel (a browser zoom such as 80% or 125%, or a screen scale such as 125%), a page drawn between pixels
shimmered as it glided: the browser draws each scroll position on a whole device pixel, up to half of one off it, and Svifa drew
the rest of the way from the scroll position, not from where the browser drew it. It now draws from there, and the glide ends
where the browser draws the page at rest. The position Svifa reports is unchanged.
- At a device scale of a part of a pixel, a glide drawn between pixels could report a position up to half a device pixel past
where it was sent, before the browser had shown which places it keeps (at 125%, 6.4 for a glide sent to 6). A glide already past
the place it then aims at is held there.
- Drawn on whole pixels at a device scale of a part of a pixel, a glide could take its last pixel early (at 125%, a glide sent to 1
rested while its curve was at 0.42, since on the device grid both are 0.8). Until the browser shows which places it keeps, a glide
rests only once it is on its last pixel on both grids.
- Drawn between pixels, a glide could go up to a pixel past the end of the page: the fraction drawn on the last block made the page
that much longer for the browser. The scroller is now put on the whole pixel just before the place, and the content is only ever
moved toward the start.
- In Firefox, the wheel over a `<textarea>` that scrolls glided the page (Firefox reports a textarea's overflow as visible). It is
left to the textarea, as is every form control and editable text.
- Installing Svifa in a Yarn Plug'n'Play project no longer turns on Yarn's experimental ES module loader, nor its warning that the
loader is experimental. The package is `"type": "commonjs"`; `dist/package.json` marks the built ES modules for Node, every file
keeps its name, and each framework entry imports the core from the file beside it.
- React: a prop taken off `SmoothScroll` (left out of its props) now goes back to its default; it kept the value it had.
- The README gave the size as under 8 kB minified and gzipped, which 1.1.0 already passed (8.6 kB). It now gives the size as
measured, and a test keeps it true.
measured, and a test keeps it true. It also compares Svifa with the most used smooth scroll libraries, as measured.

## 1.1.0

Expand Down
60 changes: 47 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ For a question, or to share a site that uses Svifa, start a [discussion](https:/

## Changing the code

Development needs Node 22.18, 24.11, 26 or later.
Development needs Node 22.18, 24.11, 26 or later. To set up, install the tools, then the browsers the tests run in (once):

```sh
npm ci
npx playwright install
```

Each check, on its own:

```sh
npm ci # the tools (and npx playwright install, once, for the browsers)
npm run lint # oxlint for defects, and Prettier for the code style (npm run format applies it)
npm run typecheck # the library, then the tests and the config files
npm run build # dist/: the ES module, the CommonJS file, the script builds, the framework entries, the types, the SBOM
Expand All @@ -27,8 +33,13 @@ npm run check:examples # builds every example from the packed package with its
npm run check:oldest # each framework entry's tests on the oldest version of its framework that package.json supports
```

`SVIFA_<NAME>_DIR=<a node_modules folder> npx playwright test tests/<name>.spec.ts` runs a framework entry's tests against another
version of its framework (`SVIFA_REACT_DIR`, `SVIFA_VUE_DIR`, `SVIFA_SVELTE_DIR`, `SVIFA_SOLID_DIR` or `SVIFA_ANGULAR_DIR`);
A framework entry's tests run against another version of its framework when its variable names a `node_modules` folder that holds
it (`SVIFA_REACT_DIR`, `SVIFA_VUE_DIR`, `SVIFA_SVELTE_DIR`, `SVIFA_SOLID_DIR` or `SVIFA_ANGULAR_DIR`), for example:

```sh
SVIFA_VUE_DIR=../vue-3.3/node_modules npx playwright test tests/vue.spec.ts
```

`check:oldest` does this for the oldest version of each.

### When the checks run
Expand All @@ -48,15 +59,19 @@ place by the time since the last frame. Each move is written on the browser's ow
drawn on the chosen elements. Anything else that moves the scroller (a key, the scroll bar, a script) takes over at once. The files:

- `src/svifa.ts`: the `Svifa` class: options, input (wheel, keys, touch, anchors), the frame loop, the motion (a damped approach by
`lerp`, or a timed curve), what it reports (`scroll` events, one `scrollend` per glide, classes).
`lerp`, or a timed curve), what it reports (`scroll` events, one `scrollend` per glide, classes), and the Svifas it makes for the
scrolling boxes inside its scroller.
- `src/scroller.ts`: the page or a scrolling element along one axis: reading and writing its position on the browser's pixel grid
(device pixels, or whole CSS pixels in browsers that keep those, learned from the first move), its end, snapping, overflow.
- `src/subpixel.ts`: draws the part of a pixel the browser cannot scroll by as a `translate` on the chosen elements, holding sticky
elements still while they are held.
elements still while they are held; gives each its layer for as long as Svifa runs, and chooses again when the page changes.
- `src/pick.ts`: which elements Svifa draws between pixels when it chooses them itself: the scroller's top-level blocks, split
around anything a layer would change.
- `src/target.ts`: what a `scrollTo` target names and the position it stands for (scroll-margin, scroll-padding, nested boxes).
- `src/snap.ts`: snapping (`snap`): the places a scroller may rest, read from its CSS, and which of them a movement rests on. The
browser has the last word: `src/svifa.ts` asks it where it would rest the scroller before each glide.
- `src/wheel.ts`: a wheel event in pixels, and whether an element inside the scroller takes it itself.
- `src/wheel.ts`: a wheel event in pixels, and whether an element inside the scroller takes it itself (the browser, or a box that
glides).
- `src/types.ts`: the public types; their JSDoc is the reference editors show.
- `src/react.ts`, `src/vue.ts`, `src/svelte.ts`, `src/solid.ts`, `src/angular.ts` and `astro/SmoothScroll.astro`: the framework
entries, each importing the core by the package's own name (`tsconfig.json` maps it to `src/index.ts`), so a project holds one
Expand Down Expand Up @@ -98,17 +113,36 @@ too, and CI checks every file for them.
- No dependencies. Anything a browser from the last few years lacks is detected and left to the browser, never polyfilled.
- Every exported function, method and accessor states its types (`isolatedDeclarations`): the declarations are written from the
source alone, so the published types are exactly what the code says.
- esbuild runs an install script, approved by version in `allowScripts` in `package.json`. After updating esbuild, approve it again
with `npm install-scripts approve esbuild`.
- esbuild runs an install script, approved by version in `allowScripts` in `package.json`. After updating esbuild, approve it
again:

```sh
npm install-scripts approve esbuild
```
- The JSDoc in `src/`, `README.md` and `llms.txt` describe the same API and change with the code. A statement in any of them is
something a test or a measurement shows.

### Signing off

Every commit carries a `Signed-off-by:` line with its author's name and email, which `git commit -s` adds. It certifies the
[Developer Certificate of Origin](https://developercertificate.org): that you wrote the change, or otherwise have the right to give
it under the project's MIT license. CI checks every commit of a pull request for it. To sign off commits made without it, run
`git commit --amend -s` for the last one, or `git rebase --signoff main` for all of them.
Every commit carries a `Signed-off-by:` line with its author's name and email, which `-s` adds:

```sh
git commit -s
```

It certifies the [Developer Certificate of Origin](https://developercertificate.org): that you wrote the change, or otherwise have
the right to give it under the project's MIT license. CI checks every commit of a pull request for it. To sign off the last commit
made without it:

```sh
git commit --amend -s
```

Or every commit of the branch:

```sh
git rebase --signoff main
```

Open a pull request against `main` with one change in it, and say what it fixes and how you checked it. CI runs every check on the
newest Node release and on the newest long-term support release.
Expand Down
Loading
Loading