Skip to content

Commit 0666d5d

Browse files
authored
feat: irradiation compensation for dark backgrounds (#24)
* feat: irradiation compensation for dark backgrounds Light content on dark backgrounds appears larger due to the optical irradiation illusion. Apply a small scale-down (up to 5%) proportional to how dark the detected background is. Exposes backgroundLuminance (Rec.601) on MeasurementResult for opaque images. Transparent images are unaffected. * feat: pass backgroundColor in Comparison story to activate irradiation compensation * refactor: density-aware irradiation compensation Scale compensation by darkness × density × 0.08 instead of flat 5%. Denser logos get more compensation (more surface area blooms). Thin wordmarks get less. Based on Helmholtz irradiation research. * refactor: use CSS color strings in Comparison story
1 parent 449a177 commit 0666d5d

File tree

4 files changed

+28
-4
lines changed

4 files changed

+28
-4
lines changed

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface MeasurementResult {
5252
contentBox?: BoundingBox;
5353
pixelDensity?: number;
5454
visualCenter?: VisualCenter;
55+
backgroundLuminance?: number;
5556
}
5657

5758
export type ImageRenderProps = ImgHTMLAttributes<HTMLImageElement> & {

src/utils/measure.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@ export function measureWithContentDetection(
373373
visualCenter,
374374
};
375375

376+
if (!alphaOnly) {
377+
result.backgroundLuminance = (bgR * 299 + bgG * 587 + bgB * 114) / 255000;
378+
}
379+
376380
if (includeDensity) {
377381
const scanArea = (maxX - minX + 1) * (maxY - minY + 1);
378382
if (scanArea === 0) {

src/utils/normalize.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,25 @@ export function calculateNormalizedDimensions(
5050
let normalizedWidth = aspectRatio ** scaleFactor * baseSize;
5151
let normalizedHeight = normalizedWidth / aspectRatio;
5252

53+
// Irradiation compensation: light content on dark backgrounds appears
54+
// larger/bolder due to the optical irradiation illusion. The effect is
55+
// more pronounced on dense/bold logos (more surface area "blooms") and
56+
// at higher contrast (darker backgrounds). Scale down proportionally
57+
// to darkness × density. Only applies to opaque images where
58+
// backgroundLuminance is available (transparent images are unaffected).
59+
//
60+
// References:
61+
// - Helmholtz irradiation illusion (1860s)
62+
// - https://gist.github.com/janogarcia/e9f57cd18ca85756743f81d9692764b7
63+
// - https://nerdy.dev/adjust-perceived-typepace-weight-for-dark-mode-without-layout-shift
64+
if (measurement.backgroundLuminance !== undefined) {
65+
const darkness = 1 - measurement.backgroundLuminance;
66+
const density = measurement.pixelDensity ?? 0.5;
67+
const irradiationScale = 1 - darkness * density * 0.08;
68+
normalizedWidth *= irradiationScale;
69+
normalizedHeight *= irradiationScale;
70+
}
71+
5372
// Apply density compensation if available
5473
// Dense logos (high pixel density) get scaled down
5574
// Light/thin logos (low pixel density) get scaled up

stories/LogoSoup.DarkMode.stories.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function InvertedSVGs({ count, shuffleSeed, ...rest }: StoryArgs) {
1818
}, [count, shuffleSeed]);
1919

2020
return (
21-
<div style={{ background: "#1a1a1a", padding: 16, borderRadius: 8 }}>
21+
<div style={{ background: "#0a0a0a", padding: 16, borderRadius: 8 }}>
2222
<StoryLogoSoup logos={logos} {...rest} />
2323
</div>
2424
);
@@ -44,10 +44,10 @@ function SideBySide({ count, shuffleSeed, ...rest }: StoryArgs) {
4444
return (
4545
<>
4646
<div style={{ background: "#ffffff", padding: 16, borderRadius: 8 }}>
47-
<StoryLogoSoup logos={originals} {...rest} />
47+
<StoryLogoSoup logos={originals} backgroundColor="#ffffff" {...rest} />
4848
</div>
49-
<div style={{ background: "#1a1a1a", padding: 16, borderRadius: 8 }}>
50-
<StoryLogoSoup logos={inverted} {...rest} />
49+
<div style={{ background: "#0a0a0a", padding: 16, borderRadius: 8 }}>
50+
<StoryLogoSoup logos={inverted} backgroundColor="#0a0a0a" {...rest} />
5151
</div>
5252
</>
5353
);

0 commit comments

Comments
 (0)