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

Show gamut in sliders #226

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
45 changes: 45 additions & 0 deletions src/lib/components/GamutSelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts">
import { GAMUTS } from '$lib/constants';
import { gamut } from '$lib/stores';
</script>

<div data-field="color-gamut">
<label for="color-gamut" data-label>Gamut</label>
<select name="color-gamut" id="color-gamut" bind:value={$gamut}>
{#each GAMUTS as gamut (gamut.format)}
{#if gamut}
<option value={gamut.format}>{gamut.name}</option>
{/if}
{/each}
</select>
</div>

<style lang="scss">
@use 'config';

[data-field='color-gamut'] {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copy pasted from SpaceSelect- should we make this a pattern somewhere?

align-items: center;
column-gap: var(--gutter);
display: grid;
grid-template:
'format-label' auto
'format-input' auto / 1fr;
justify-content: end;

@include config.above('sm-page-break') {
grid-template: 'format-label format-input' auto / 1fr minmax(10rem, auto);
}
}

label {
grid-area: format-label;

@include config.above('sm-page-break') {
text-align: right;
}
}

select {
grid-area: format-input;
}
</style>
2 changes: 2 additions & 0 deletions src/lib/components/Header.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import GamutSelect from '$lib/components/GamutSelect.svelte';
import SpaceSelect from '$lib/components/SpaceSelect.svelte';
import Icon from '$lib/components/util/Icon.svelte';
</script>
Expand All @@ -9,6 +10,7 @@
<span class="sr-only">OddContrast</span>
</h1>
<SpaceSelect />
<GamutSelect />
</header>

<style lang="scss">
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/SpaceSelect.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</script>

<div data-field="color-format">
<label for="color-format" data-label>Color Format</label>
<label for="color-format" data-label>Format</label>
<select name="color-format" id="color-format" bind:value={$format}>
{#each spaces as space (space.id)}
{#if space}
Expand Down
21 changes: 18 additions & 3 deletions src/lib/components/colors/Sliders.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { Writable } from 'svelte/store';

import { type ColorFormatId, SLIDERS } from '$lib/constants';
import { ColorSpace } from '$lib/stores';
import { ColorSpace, gamut } from '$lib/stores';
import { getSpaceFromFormatId, sliderGradient } from '$lib/utils';

interface Props {
Expand All @@ -20,7 +20,12 @@
SLIDERS[format].map((id) => {
const coord = spaceObject.coords[id];
const range = coord?.range ?? coord?.refRange ?? [0, 1];
const gradient = sliderGradient($color, id, range);
const gradient = sliderGradient({
color: $color,
channel: id,
range: range,
gamut: $gamut,
});
return {
id,
name: coord?.name ?? '',
Expand All @@ -37,7 +42,12 @@
);

let alphaGradient = $derived(
sliderGradient($color, 'alpha', [0, $color.alpha]),
sliderGradient({
color: $color,
channel: 'alpha',
range: [0, $color.alpha],
gamut: $gamut,
}),
);

const handleInput = (
Expand Down Expand Up @@ -95,6 +105,7 @@
style={`--stops: ${alphaGradient}`}
value={$color.alpha}
oninput={(e) => handleInput(e)}
data-channel="alpha"
/>
</div>
</div>
Expand All @@ -105,6 +116,10 @@
display: block;
appearance: none;
background: linear-gradient(to right, var(--stops));
&[data-channel='alpha'] {
background: linear-gradient(to right, var(--stops)),
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"><rect fill="%23e8e8e8" width="30" height="30"/><rect x="30" y="30" width="30" height="30" fill="%23e8e8e8"/></svg>');
}
}

[data-group~='sliders'] {
Expand Down
9 changes: 9 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export const FORMATS: ColorFormatId[] = [
'srgb',
];

export type ColorGamutId = 'srgb' | 'p3' | 'rec2020';

export const GAMUTS: { name: string; format: ColorGamutId | null }[] = [
{ name: 'None', format: null },
{ name: 'sRGB', format: 'srgb' },
{ name: 'P3', format: 'p3' },
{ name: 'Rec2020', format: 'rec2020' },
];

export interface FormatGroup {
name: string;
formats: ColorFormatId[];
Expand Down
4 changes: 3 additions & 1 deletion src/lib/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { writable } from 'svelte/store';

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

// Register supported color spaces
ColorSpace.register(HSL);
Expand All @@ -32,6 +32,7 @@ export { ColorSpace };

export const INITIAL_VALUES = {
format: 'p3' as ColorFormatId,
gamut: 'p3' as ColorGamutId,
bg_coord: [0.0967, 0.167, 0.4494] as [number, number, number],
fg_coord: [0.951, 0.675, 0.7569] as [number, number, number],
alpha: 1,
Expand All @@ -49,6 +50,7 @@ const INITIAL_FG = {
};

export const format = writable<ColorFormatId>(INITIAL_VALUES.format);
export const gamut = writable<ColorGamutId>(INITIAL_VALUES.gamut);
export const bg = writable<PlainColorObject>(INITIAL_BG);
export const fg = writable<PlainColorObject>(INITIAL_FG);

Expand Down
52 changes: 43 additions & 9 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,74 @@
import {
clone,
display,
inGamut,
type PlainColorObject,
serialize,
set,
steps,
to,
} from 'colorjs.io/fn';

import { type ColorFormatId, FORMATS } from '$lib/constants';
import { type ColorFormatId, type ColorGamutId, FORMATS } from '$lib/constants';

export const getSpaceFromFormatId = (formatId: ColorFormatId) =>
formatId === 'hex' ? 'srgb' : formatId;

export const sliderGradient = (
color: PlainColorObject,
channel: string,
range: [number, number],
) => {
export const sliderGradient = ({
color,
channel,
range,
gamut,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat computationally slow. I toyed around with caching results, but the cache quickly grew to 12 Mbs in about 30 seconds of heavy usage 🫨 . I could likely do some more optimization if it is slow on slower machines.

}: {
color: PlainColorObject;
channel: string;
range: [number, number];
gamut: ColorGamutId;
}) => {
const start = clone(color);
const end = clone(color);
if (channel === 'alpha') {
start.alpha = range[0];
end.alpha = range[1];
start.alpha = 0;
end.alpha = 1;
} else {
set(start, channel, range[0]);
start.alpha = 1;
set(end, channel, range[1]);
end.alpha = 1;
}

const gradientSteps = steps(start, end, {
steps: 10,
space: color.space,
hue: 'raw',
maxDeltaE: 10,
});
let wasInGamut: boolean;
const inGamutSteps: string[] = [];
const stepWidth = 100 / (gradientSteps.length - 1);

if (channel === 'alpha' || gamut === null) {
return gradientSteps.map((c) => display(c)).join(', ');
}

gradientSteps.forEach((step, index) => {
if (inGamut(step, gamut)) {
if (wasInGamut === false) {
inGamutSteps.push(`transparent ${stepWidth * (index + 1)}%`);
}
wasInGamut = true;
inGamutSteps.push(`${display(step)} ${stepWidth * index}%`);
} else {
if (wasInGamut === true) {
inGamutSteps.push(`transparent ${stepWidth * (index - 1)}%`);
}
inGamutSteps.push(`transparent ${stepWidth * index}%`);

wasInGamut = false;
}
});

return gradientSteps.map((c) => display(c)).join(', ');
return inGamutSteps.join(', ');
};

function decodeColor(colorHash: string, format: ColorFormatId) {
Expand Down
2 changes: 1 addition & 1 deletion src/sass/initial/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ body {
display: grid;
gap: var(--shim) var(--double-gutter);
grid-area: header;
grid-template: 'logo colorspace' auto / auto 1fr;
grid-template: 'logo colorspace gamut' auto / auto 1fr;

@include config.above('sm-page-break') {
gap: var(--double-gutter);
Expand Down
22 changes: 22 additions & 0 deletions test/lib/components/GamutSelect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { fireEvent, render } from '@testing-library/svelte';
import { get } from 'svelte/store';

import Gamut from '$lib/components/GamutSelect.svelte';
import { gamut, INITIAL_VALUES, reset } from '$lib/stores';

describe('Space', () => {
afterEach(() => {
reset();
});

it('renders editable gamut select', async () => {
const { getByLabelText } = render(Gamut);

expect(get(gamut)).toBe(INITIAL_VALUES.gamut);

const select = getByLabelText('Gamut');
await fireEvent.change(select, { target: { value: 'rec2020' } });

expect(get(gamut)).toBe('rec2020');
});
});
2 changes: 1 addition & 1 deletion test/lib/components/SpaceSelect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Space', () => {
expect(get(bg).space.id).toBe(INITIAL_VALUES.format);
expect(get(fg).space.id).toBe(INITIAL_VALUES.format);

const select = getByLabelText('Color Format');
const select = getByLabelText('Format');
await fireEvent.change(select, { target: { value: 'hsl' } });

expect(get(bg).space.id).toBe('hsl');
Expand Down
Loading