Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take into account foreground alpha for contrast #120

Closed
wants to merge 6 commits into from
Closed
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
21 changes: 21 additions & 0 deletions src/lib/components/colors/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
export let type: 'bg' | 'fg';
export let color: Writable<PlainColorObject>;
export let format: ColorFormatId;
export let premultipliedFg: PlainColorObject | null;

$: targetSpace = getSpaceFromFormatId(format);
$: display = serialize($color, { inGamut: false, format });
$: displayType = type === 'bg' ? 'Background' : 'Foreground';
$: editing = false;
$: inputValue = '';
$: displayPremultipliedFg = premultipliedFg
? serialize(premultipliedFg, { inGamut: false, format })
: '';
let hasError = false;

// When not editing, sync input value with color (e.g. when sliders change)
Expand Down Expand Up @@ -89,6 +93,15 @@
{#if hasError}
<div data-color-info="warning">Could not parse input as a valid color.</div>
{/if}
{#if premultipliedFg}
<div
data-color-info="premultiplied"
style="--premultipliedFg:{serialize(premultipliedFg)}"
>
{displayPremultipliedFg}
<span class="premultipliedFg"></span>
</div>
{/if}
</div>

<style lang="scss">
Expand Down Expand Up @@ -153,6 +166,14 @@
}
}

.premultipliedFg {
background-color: var(--premultipliedFg);
width: 10pt;
height: 10pt;
border: 1pt solid black;
display: inline-block;
}

[data-input='color'] {
border-width: 0 0 var(--border-width) 0;
grid-area: input;
Expand Down
11 changes: 8 additions & 3 deletions src/lib/components/colors/index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
import Header from '$lib/components/colors/Header.svelte';
import Sliders from '$lib/components/colors/Sliders.svelte';
import SupportWarning from '$lib/components/colors/SupportWarning.svelte';
import { bg, fg, format } from '$lib/stores';
import { bg, fg, format, premultipliedFg } from '$lib/stores';
</script>

<h2 class="sr-only">Check the contrast ratio between two colors</h2>

<SupportWarning format={$format} />

<form data-form="contrast-checker" data-layout="color-form">
<Header type="bg" color={bg} format={$format} />
<Header type="bg" color={bg} format={$format} premultipliedFg={null} />
<Sliders type="bg" color={bg} format={$format} />

<Header type="fg" color={fg} format={$format} />
<Header
type="fg"
color={fg}
format={$format}
premultipliedFg={$premultipliedFg}
/>
<Sliders type="fg" color={fg} format={$format} />
</form>

Expand Down
35 changes: 31 additions & 4 deletions src/lib/components/ratio/index.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
<script lang="ts">
import { contrast } from 'colorjs.io/fn';
import { clone, contrast, serialize } from 'colorjs.io/fn';

import Result from '$lib/components/ratio/Result.svelte';
import ExternalLink from '$lib/components/util/ExternalLink.svelte';
import { RATIOS } from '$lib/constants';
import { bg, fg } from '$lib/stores';
import { bg, fg, premultipliedFg } from '$lib/stores';

$: ratio = contrast($bg, $fg, 'WCAG21');
let ratio: number;

$: {
const bgNoAlpha = clone($bg);
bgNoAlpha.alpha = 1;

ratio = contrast(bgNoAlpha, $premultipliedFg || $fg, 'WCAG21');
}
$: displayRatio = Math.round((ratio + Number.EPSILON) * 100) / 100;
$: pass = ratio >= RATIOS.AA.Large;
</script>
Expand All @@ -29,6 +36,17 @@
</p>
</div>

{#if $premultipliedFg}
Because WCAG 2 doesn't account for alpha, this is approximated by
premultiplying the foreground color in the sRGB space.
<div class="compare-alpha">
<div style="--alpha-color:{serialize($fg)}">With alpha</div>
<div style="--alpha-color:{serialize($premultipliedFg)}">
Alpha premultipled
</div>
</div>
{/if}

<div class="result-status">
<Result level="AA" type="Normal" {ratio} />
<Result level="AAA" type="Normal" {ratio} />
Expand All @@ -38,7 +56,6 @@

<div class="contrast-defined">
<h4 class="label">AA Contrast Ratio</h4>

<dl>
<dt><strong>{RATIOS.AA.Normal}</strong> : 1</dt>
<dd>Normal Text</dd>
Expand Down Expand Up @@ -157,4 +174,14 @@
display: none;
}
}
.compare-alpha {
display: flex;
color: black;
> div {
background-color: var(--alpha-color);
height: 4em;
border: 1pt solid white;
flex: auto;
}
}
</style>
5 changes: 4 additions & 1 deletion src/lib/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import {
sRGB,
} from 'colorjs.io/fn';
import type { PlainColorObject } from 'colorjs.io/types/src/color';
import { writable } from 'svelte/store';
import { derived, writable } from 'svelte/store';

// eslint-disable-next-line import/no-unresolved
import { browser, dev } from '$app/environment';
import type { ColorFormatId } from '$lib/constants';

import { premultiplyFG } from './utils';

// Register supported color spaces
ColorSpace.register(HSL);
ColorSpace.register(Lab);
Expand Down Expand Up @@ -51,6 +53,7 @@ const INITIAL_FG = {
export const format = writable<ColorFormatId>(INITIAL_VALUES.format);
export const bg = writable<PlainColorObject>(INITIAL_BG);
export const fg = writable<PlainColorObject>(INITIAL_FG);
export const premultipliedFg = derived([fg, bg, format], premultiplyFG);

export const reset = () => {
bg.set(INITIAL_BG);
Expand Down
21 changes: 20 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clone, display, serialize, set, steps, to } from 'colorjs.io/fn';
import { clone, display, mix, serialize, set, steps, to } from 'colorjs.io/fn';
import type { PlainColorObject } from 'colorjs.io/types/src/color';

import type { ColorFormatId } from '$lib/constants';
Expand Down Expand Up @@ -88,3 +88,22 @@ export const storeValuesToHash = (
const fgParam = encodeColor(fg, format);
return encodeURIComponent(`${format}__${bgParam}__${fgParam}`);
};

export const premultiplyFG = ([fg, bg, format]: [
fg: PlainColorObject,
bg: PlainColorObject,
format: ColorFormatId,
]) => {
if (fg.alpha === 1) return;
const bgNoAlpha = clone(bg);
bgNoAlpha.alpha = 1;
const fgNoAlpha = clone(fg);
fgNoAlpha.alpha = 1;

return mix(fgNoAlpha, bgNoAlpha, 1 - fg.alpha, {
// We always mix in srgb, as we are approximating the color as it will be
// displayed on a monitor.
space: 'srgb',
outputSpace: getSpaceFromFormatId(format),
});
};
4 changes: 4 additions & 0 deletions test/js/lib/components/colors/Header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('Header', () => {
const { getByLabelText } = render(Header, {
type: 'bg',
color,
premultipliedFg: HSL_WHITE,
format: 'hsl',
});
const input = getByLabelText('Background Color');
Expand All @@ -31,6 +32,7 @@ describe('Header', () => {
const { getByText, getByLabelText } = render(Header, {
type: 'fg',
color,
premultipliedFg: HSL_WHITE,
format: 'hsl',
});
const input = getByLabelText('Foreground Color');
Expand All @@ -55,6 +57,7 @@ describe('Header', () => {
const { getByText, getByLabelText } = render(Header, {
type: 'fg',
color,
premultipliedFg: HSL_WHITE,
format: 'hsl',
});
const input = getByLabelText('Foreground Color');
Expand All @@ -78,6 +81,7 @@ describe('Header', () => {
const { getByLabelText } = render(Header, {
type: 'fg',
color,
premultipliedFg: HSL_WHITE,
format: 'hsl',
});
const input = getByLabelText('Foreground Color');
Expand Down