Skip to content

Commit

Permalink
feat: ✨ New CVD (Color Vision Deficiency) plugin!
Browse files Browse the repository at this point in the history
  • Loading branch information
fluid-design-io committed Oct 31, 2023
1 parent 502a7c8 commit b6363f1
Show file tree
Hide file tree
Showing 11 changed files with 507 additions and 27 deletions.
3 changes: 1 addition & 2 deletions apps/web/app/api/og/route.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { colorHelper } from "@/lib/colorHelper";
import { ImageResponse } from "next/server";
import { ImageResponse } from "next/og";
// App router includes @vercel/og.
// No need to install it.

Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default async function RootLayout({
<SiteFooter />
<Toaster />
</ThemeProvider>
<Analytics />
{/* <Analytics /> */}
</body>
</html>
);
Expand Down
53 changes: 33 additions & 20 deletions apps/web/components/toolbar/cvd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,43 @@ import primaryToolbarMenu from "../ui/primary-toolbar-menu";
import { useColorStore } from "@/store/store";
import ToolbarMenuItem from "./toolbar-menu-item";
import { cn } from "@ui/lib/utils";
import { handleToggleReadability } from "@/app/actions";
import { useState } from "react";

import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@ui/components/ui/popover";

import dynamic from "next/dynamic";
import { usePluginCvdStore } from "./plugin/cvd.plugin";

const CVDPlugin = dynamic(() => import("./plugin/cvd.plugin"), {
ssr: false,
});
function CVD() {
const menuItem = primaryToolbarMenu["Color Vision Deficiency"];
const { showReadability } = useColorStore();
const maybePluginStore = usePluginCvdStore();
const [open, setOpen] = useState(false);
// const { showReadability } = useColorStore();
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleToggleReadability(`${!showReadability}`);
useColorStore.setState({ showReadability: !showReadability });
}}
>
<button
type="submit"
aria-label="Toggle Readability"
className={cn(
showReadability &&
"-mx-1.5 rounded-sm bg-primary/20 px-1.5 lg:mx-0 lg:px-0",
)}
>
<ToolbarMenuItem {...menuItem} />
</button>
</form>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
aria-label="Show cvd Plugin"
className={cn(
maybePluginStore?.isOn() &&
"-mx-1.5 rounded-sm bg-primary/20 px-1.5 lg:mx-0 lg:px-0",
)}
>
<ToolbarMenuItem {...menuItem} />
</button>
</PopoverTrigger>
<PopoverContent className="w-[18rem] sm:w-[24rem]" align="end">
{open && <CVDPlugin key={`shareable-${open}`} />}
</PopoverContent>
</Popover>
);
}

Expand Down
288 changes: 288 additions & 0 deletions apps/web/components/toolbar/plugin/cvd.plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { useColorStore } from "@/store/store";
import { Label } from "@ui/components/ui/label";
import { useEffect, useState } from "react";
import {
filterDeficiencyProt,
filterDeficiencyDeuter,
filterDeficiencyTrit,
parse,
formatHex,
hsl,
} from "culori";
import { Slider } from "@ui/components/ui/slider";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@ui/components/ui/accordion";
import { ScrollArea } from "@ui/components/ui/scroll-area";

import { create } from "zustand";
import { produce } from "immer";

import { devtools, persist } from "zustand/middleware";

export type PluginCvdState = {
isOn: () => boolean;
protanopia: [number];
deuteranopia: [number];
tritanopia: [number];
setProtanopia: (value: [number]) => void;
setDeuteranopia: (value: [number]) => void;
setTritanopia: (value: [number]) => void;
};

export const usePluginCvdStore = create<PluginCvdState>()(
devtools(
persist(
(set, get) => ({
isOn: () => {
const { protanopia, deuteranopia, tritanopia } = get();
return (
protanopia[0] !== 0 || deuteranopia[0] !== 0 || tritanopia[0] !== 0
);
},
protanopia: [0],
deuteranopia: [0],
tritanopia: [0],
setProtanopia: (value) =>
set(
produce((state) => {
state.protanopia = value;
}),
),
setDeuteranopia: (value) =>
set(
produce((state) => {
state.deuteranopia = value;
}),
),
setTritanopia: (value) =>
set(
produce((state) => {
state.tritanopia = value;
}),
),
}),
{
name: "plugin-cvd",
},
),
),
);

function CVDPlugin() {
const { colorPalettes, generatePalette, baseColors } = useColorStore();
const {
protanopia,
deuteranopia,
tritanopia,
setProtanopia,
setDeuteranopia,
setTritanopia,
} = usePluginCvdStore();
const [isMounted, setIsMounted] = useState(false);
// make a copy of the colorPalettes object
const colorPalettesCopy = { ...colorPalettes };

const handleToggleCVD = () => {
if (protanopia[0] === 0 && deuteranopia[0] === 0 && tritanopia[0] === 0) {
generatePalette(true);
} else {
const newColorPalettes = Object.keys(colorPalettesCopy).reduce(
(acc, key) => {
const newColorPalette = colorPalettesCopy[key].map(
({ color }, index) => {
const rgb = parse(color);
let newColor = filterDeficiencyProt(protanopia[0])(rgb);
newColor = filterDeficiencyDeuter(deuteranopia[0])(newColor);
newColor = filterDeficiencyTrit(tritanopia[0])(newColor);
const hslColor = hsl(newColor);
const hex = formatHex(newColor);
return {
...colorPalettesCopy[key][index],
raw: {
h: hslColor.h,
s: hslColor.s,
l: hslColor.l,
a: hslColor?.alpha ?? 1,
},
color: hex,
};
},
);
acc[key] = newColorPalette;
return acc;
},
{} as any,
);
useColorStore.setState({ colorPalettes: newColorPalettes });
}
};
useEffect(() => {
if (!isMounted) {
setIsMounted(true);
return;
}
handleToggleCVD();
}, [protanopia, deuteranopia, tritanopia, baseColors]);
return (
<div className="w-full">
{/* Added fixed faded gradient */}
<div className="relative">
<div className="pointer-events-none absolute inset-x-0 top-[1px] z-10 h-6 w-full rounded-t bg-gradient-to-t from-transparent to-background" />
<div className="pointer-events-none absolute inset-x-0 bottom-[1px] z-10 h-6 w-full rounded-b bg-gradient-to-b from-transparent to-background" />
<ScrollArea className="relative max-h-[min(20rem,70vh)] w-full overflow-y-auto rounded border">
<div className="prose prose-sm p-4 dark:prose-invert">
<h2>CVD (Color Vision Deficiency)</h2>
<h3>Protanopia and Protanomaly</h3>
<ul>
<li>
<strong>Protanopia</strong>: This is a form of red-green color
blindness where the red cones in the eye are absent. As a
result, red appears as black, and certain shades of orange,
yellow, and green may appear as yellow.
</li>
<li>
<strong>Protanomaly</strong>: This is a milder form of red-green
color blindness compared to Protanopia. In this condition, red
cones are present but do not function properly. Red, orange, and
yellow may appear greener than they actually are.
</li>
</ul>
<h3>Deuteranopia and Deuteranomaly</h3>
<ul>
<li>
<strong>Deuteranopia</strong>: This is another form of red-green
color blindness where the green cones in the eye are absent. As
a result, green appears as beige, and red may appear as brown.
</li>
<li>
<strong>Deuteranomaly</strong>: This is the most common form of
red-green color blindness. Green cones are present but are
dysfunctional. Yellow and green appear redder than they actually
are, and it is difficult to distinguish violet from blue.
</li>
</ul>
<h3>Tritanopia and Tritanomaly</h3>
<ul>
<li>
<strong>Tritanopia</strong>: This is a blue-yellow color
blindness where the blue cones in the eye are missing or
non-functional. As a result, blue appears as green, and yellow
appears as violet or light gray.
</li>
<li>
<strong>Tritanomaly</strong>: This is a milder form of
blue-yellow color blindness where blue cones are present but
dysfunctional. Blue appears greener, and it can be difficult to
distinguish yellow and red from pink.
</li>
</ul>
<p>
This feature is made available via{" "}
<a
href="https://culorijs.org/"
target="_blank"
rel="noopener noreferrer"
referrerPolicy="no-referrer"
>
Culori
</a>
</p>
<Accordion type="single" className="mt-0 border-b-0" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger className="py-1.5 text-sm">
References
</AccordionTrigger>
<AccordionContent>
<p className="text-xs">
Based on the work of Machado, Oliveira and Fernandes (2009),
using&nbsp;
<a href="https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html">
precomputed matrices
</a>
&nbsp;provided by the authors. References thanks to
the&nbsp;
<a href="http://colorspace.r-forge.r-project.org/reference/simulate_cvd.html">
<code>colorspace</code>&nbsp;package for R
</a>
.
</p>
<p className="text-xs">
G. M. Machado, M. M. Oliveira and L. A. F. Fernandes,&nbsp;
<em>
"A Physiologically-based Model for Simulation of Color
Vision Deficiency,"
</em>
&nbsp;in IEEE Transactions on Visualization and Computer
Graphics, vol. 15, no. 6, pp. 1291-1298, Nov.-Dec.
2009,&nbsp;
<a href="https://doi.ieeecomputersociety.org/10.1109/TVCG.2009.113">
doi: 10.1109/TVCG.2009.113
</a>
.
</p>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</ScrollArea>
</div>
<div className="mt-2 flex w-full items-center justify-between gap-4">
<Label
htmlFor="protanopia-slider"
className="flex min-w-[7.5rem] items-center justify-start text-foreground/80"
>
<div className="mr-2 h-4 w-4 rounded-full bg-red-400 dark:bg-red-600" />
Protanopia
</Label>
<Slider
defaultValue={protanopia}
max={0.8}
step={0.1}
min={0}
id="protanopia-slider"
onValueChange={setProtanopia}
/>
</div>
<div className="mt-2 flex w-full items-center justify-between gap-4">
<Label
htmlFor="protanomaly-slider"
className="flex min-w-[7.5rem] items-center justify-start text-foreground/80"
>
<div className="mr-2 h-4 w-4 rounded-full bg-green-400 dark:bg-green-600" />
Protanomaly
</Label>
<Slider
defaultValue={deuteranopia}
max={0.8}
step={0.1}
min={0}
id="protanomaly-slider"
onValueChange={setDeuteranopia}
/>
</div>
<div className="mt-2 flex w-full items-center justify-between gap-4">
<Label
htmlFor="deuteranopia-slider"
className="flex min-w-[7.5rem] items-center justify-start text-foreground/80"
>
<div className="mr-2 h-4 w-4 rounded-full bg-blue-400 dark:bg-blue-600" />
Deuteranopia
</Label>
<Slider
defaultValue={tritanopia}
max={0.8}
step={0.1}
min={0}
id="deuteranopia-slider"
onValueChange={setTritanopia}
/>
</div>
</div>
);
}

export default CVDPlugin;
3 changes: 1 addition & 2 deletions apps/web/components/toolbar/readability.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import primaryToolbarMenu from "../ui/primary-toolbar-menu";
import { useColorStore } from "@/store/store";
import ToolbarMenuItem from "./toolbar-menu-item";
import { cn } from "@ui/lib/utils";
import { handleToggleReadability } from "@/app/actions";
import { useState } from "react";

import {
Expand All @@ -23,7 +22,7 @@ function Readability() {
const { showReadability } = useColorStore();
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger>
<PopoverTrigger asChild>
<button
type="button"
aria-label="Show Readability Plugin"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/ui/desktop-primary-toolbar-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function DesktopPreviewToolbarButtons() {
>
<Toolbar>
<Toolbar.Readability />
{/* <Toolbar.CVD /> coming soon */}
<Toolbar.CVD />
<Toolbar.UploadImage />
<Toolbar.DownloadBasePalette />
<Toolbar.ShareableLink />
Expand Down
Loading

0 comments on commit b6363f1

Please sign in to comment.