Repository navigation
Conversation
|
Size Change: +183 B (+1.57%) Total Size: 11.9 kB 📦 View Changed
ℹ️ View Unchanged
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #157 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 41 41
Lines 636 629 -7
Branches 143 145 +2
=========================================
- Hits 636 629 -7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟢 Approval recommended
The change is well-scoped, removes the root cause of HEX alpha drift, and is backed by exhaustive tests across core conversions, plugins, and minify behavior.
Pull request overview
This PR fixes HEX4/HEX8 alpha parsing so the stored alpha preserves the exact 8-bit byte ratio (instead of being rounded during parsing), and centralizes alpha output rounding via a shared roundAlpha helper to prevent alpha-byte drift across round-trips and conversions.
Changes:
- Parse HEX alpha bytes without premature rounding, ensuring HEX alpha round-trips accurately.
- Introduce
roundAlphaand use it across core + plugin color models andColord#alpha()to keep output stable (≤ 3 decimals, with 2 decimals when it maps to the same byte). - Update
minifylosslessness checks and fallback behavior; add comprehensive tests covering all 256 alpha bytes and plugin conversion paths; add a changelog entry.
File summaries
| File | Description |
|---|---|
src/colorModels/hex.ts |
Stops rounding alpha during HEX parsing so the original byte ratio is preserved. |
src/helpers.ts |
Adds roundAlpha to standardize alpha rounding at output time. |
src/colord.ts |
Routes alpha() getter through roundAlpha for consistent output behavior. |
src/colorModels/{rgb,hsl,hsv,hwb,lab,lch,xyz,cmyk}.ts |
Replaces ALPHA_PRECISION rounding with roundAlpha for consistent alpha serialization across models. |
src/plugins/minify.ts |
Uses roundAlpha for losslessness checks and ensures a string result even when all notations are disabled. |
src/constants.ts |
Removes now-unused ALPHA_PRECISION. |
tests/colord.test.ts |
Adds exhaustive core alpha-byte round-trip and output-precision invariants. |
tests/plugins.test.ts |
Adds plugin conversion + minify alpha-byte coverage and precision invariants. |
CHANGELOG.md |
Documents the fixes for the upcoming release entry. |
Review details
- Files reviewed: 16/16 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`parseHex` stored the alpha rounded to two decimals while every other model
rounds to three, so 155 of the 256 HEX8 bytes and 10 of the 16 HEX4 digits came
back as a different byte: `#cc88` serialized as `#cccc8887`, `#3366cc01` went
fully transparent and `#3366ccfe` went opaque. Two decimals cannot address an
8-bit alpha at all — the step is 1/255 and the rounding error is up to 1/200.
The byte is now kept exactly, like every other channel, and the rounding moved
to output in a shared `roundAlpha`: three digits, except where two name the very
same byte. That keeps `0x80` printing `0.5` rather than `0.502`, so nothing that
round-trips today changes — verified over 2886 inputs against 11 outputs, where
the only rows that move are the 165 that were already broken. It also matches
what a browser prints: Chrome 148 serializes those bytes the same way on all 255.
The rule looks at the value, not at the notation it arrived in, so an 8-bit ratio
handed over directly shortens as well: `{ a: 128 / 255 }` now reports `0.5` where
2.10.0 reported `0.502`. A literal `0.502` is untouched — the two are different
numbers, and only a value that is exactly `k / 255` is affected, which is what
dividing an 8-bit source gives you. That is 75 of the 256 ratios, and a deliberate
change: it is the same color as `#RRGGBB80`, which 2.10.0 already reported as `0.5`,
so the two paths agreed on the color and disagreed on how to print it. Both cases
are pinned by tests.
`ALPHA_PRECISION` had no consumers left, so its rationale moved into
`roundAlpha`, which spells out its factors instead of paying for `Math.pow` on
every serialization — parsing a HEX into an RGB string is now ~25% faster.
Closes #97
The losslessness check carried its own hard-coded two decimals, a copy of the
assumption the HEX parser just lost. With the parser fixed it turned down the
byte that holds the color exactly, and it had been waving through bytes that
did not: `minify("#cc88", { alphaHex: true })` returned `#cccc8887`.
Checking with `roundAlpha` instead ties the plugin to the rule everything else
prints through. Colors read from a HEX keep their own byte, and the alphas an
8-bit hex can hold go from 99 of 999 to 254 — `rgba(0,0,0,.075)` now minifies
to `#13`, which reads back as exactly `.075`. `.5` still becomes
`#ff000080`, so nothing that shortens today stops shortening.
The method could also return `undefined` rather than a string once every
notation was either switched off or turned down as lossy; the RGB form is the
one that can hold any color, so it is the answer in that case.
The HSL variant is never checked for losslessness and can still name a
different color — that is an older, separate issue and is left alone here.
c6a215a to
8744a3b
Compare
Fixes #97. Supersedes #137, whose diagnosis this builds on — credit to @spokodev in the changelog.
The bug
parseHexstored the alpha rounded to two decimals while every other model rounds to three:Two decimals cannot address an 8-bit alpha: the step is 1/255 ≈ 0.0039 and the rounding error reaches 0.005. 155 of the 256 HEX8 bytes and 10 of the 16 HEX4 digits came back as a different byte.
The fix
The byte is now read exactly, like every other channel, and the rounding moved to output in a shared
roundAlpha: three digits, except where two name the very same byte.ALPHA_PRECISIONhad no consumers left, so its rationale moved into that comment.Why not just
round(byte / 255, 3)That also fixes #97, but it repaints 75 of the 256 bytes for everyone —
#80808080starts reporting0.502, andtoRgbString()printsrgba(128, 128, 128, 0.502)where it printed0.5. Three reasons not to:rgb()/hex alpha as 8-bit:rgba(128,128,128,0.502)computes torgba(128, 128, 128, 0.5), andrgba(128,128,128,0.999)torgb(128, 128, 128). Over all 255 fractional bytes, the alpha Chrome prints matchesroundAlpha255/255; plain three digits matches 180/255.round(0.5 * 255)is 128 — the original byte. Same principle asNumber#toStringprinting0.1instead of0.1000000000000000055.minifycan no longer offer#ff000080for.5, and thealphaHexoutput grows 73% on a typical palette.Second commit:
minifyThe losslessness check carried its own hard-coded two decimals — the same assumption the parser just lost — so with the parser fixed it turned down the byte that holds the color exactly, and it had been waving through bytes that did not (
minify("#cc88", { alphaHex: true })returned#cccc8887). It now checks withroundAlpha, which ties the plugin to the rule everything else prints through. Alphas an 8-bit hex can hold go from 99 of 999 to 254 —rgba(0,0,0,.075)now minifies to#00000013, which reads back as exactly.075..5still becomes#ff000080.minify()could also returnundefinedrather than a string once every notation was switched off or turned down; the RGB form is the one that can hold any color, so it answers in that case.Compatibility
Measured over 2886 inputs against 11 outputs (
alpha, every serializer,toLab, threeminifymodes,mix), current release vs this branch:rgba()/hsla(), alpha step 0.001minifyreturning a different color than it was given: 0 of 56 832 inalphaHexmode, before and after.One deliberate change beyond that, which the sweep above missed because a 0.001 grid almost never lands on
k / 255: the rule looks at the value, not at the notation it arrived in, so an alpha handed over as an exact 8-bit ratio shortens too.Note the second line:
0.502and128 / 255are different numbers, and only a value that is exactlyk / 255shortens — which is what dividing an 8-bit source gives you, so it shows up for code reading canvasImageDatarather than for anything hand-written. That covers 75 of the 256 ratios.It is also the same color as
#00000080, which 2.10.0 already reported as0.5: the two paths agreed on the color and disagreed on how to print it. Both cases are pinned by tests.Left out on purpose
The HSL variant in
minifyis never checked for losslessness and can name a different color —rgba(200,200,200,.55)→hsla(0,0%,78%,.55), which is 199, not 200. That is 3034 of 56 832 in the default mode, it predates this change, and it is even in the README as expected output. This branch does not make it reachable in any mode where it was not already, so it belongs in its own PR.Checks
minify, and pin the "alpha never exceeds three digits" invariant across every output. All of them fail onmasterhex.tsat 100%minify557 B (limit 600 B)Math.powtwice. Benchmark floors all clear: 6.33× / 2.85× / 4.52×