From 18c713727938fbf799c4d5cf96f83e8af6620d27 Mon Sep 17 00:00:00 2001
From: Oliver Pan <2216991777@qq.com>
Date: Tue, 31 Oct 2023 01:08:28 -0500
Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20added=20Contrast=20Ratio=20?=
=?UTF-8?q?plugin?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/web/app/layout.tsx | 8 +-
.../palette/base-color-palettes.tsx | 34 +++++---
.../palette/base-palette-button.tsx | 6 +-
.../components/palette/readability-string.tsx | 58 ++++++++++++++
apps/web/components/svg/eye-cvd.tsx | 39 ++++++++++
apps/web/components/toolbar/cvd.tsx | 34 ++++++++
apps/web/components/toolbar/index.tsx | 4 +
apps/web/components/toolbar/readability.tsx | 47 +++++++++++
.../ui/color-mode-dropdown-menu.tsx | 3 -
.../ui/desktop-primary-toolbar-buttons.tsx | 2 +
.../components/ui/primary-toolbar-menu.tsx | 15 +++-
apps/web/lib/generateReadability.tsx | 78 +++++++++++++++++++
apps/web/lib/getServerColors.ts | 26 ++++++-
apps/web/store/store.ts | 37 +++++++--
apps/web/types/app.ts | 19 +++++
15 files changed, 381 insertions(+), 29 deletions(-)
create mode 100644 apps/web/components/palette/readability-string.tsx
create mode 100644 apps/web/components/svg/eye-cvd.tsx
create mode 100644 apps/web/components/toolbar/cvd.tsx
create mode 100644 apps/web/components/toolbar/readability.tsx
create mode 100644 apps/web/lib/generateReadability.tsx
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 6d6b191..5242a07 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -52,17 +52,21 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
- const { baseColors, colorPalettes, colorMode } = await getServerColors();
+ const { baseColors, colorPalettes, colorMode, showReadability } =
+ await getServerColors();
useColorStore.setState({
baseColors,
colorPalettes,
colorMode,
+ showReadability,
});
return (
-
+
diff --git a/apps/web/components/palette/base-color-palettes.tsx b/apps/web/components/palette/base-color-palettes.tsx
index 6c4d12f..955a218 100644
--- a/apps/web/components/palette/base-color-palettes.tsx
+++ b/apps/web/components/palette/base-color-palettes.tsx
@@ -6,9 +6,11 @@ import React from "react";
import PaletteButton from "./base-palette-button";
import ColorString from "./color-string";
+import ReadabilityString from "./readability-string";
function BaseColorPalettes() {
- const { colorPalettes, colorMode } = useColorStore.getState();
+ const { colorPalettes, colorMode, showReadability } =
+ useColorStore.getState();
const animation = (i, type) => {
let baseDelay = 0.12;
switch (type) {
@@ -47,7 +49,7 @@ function BaseColorPalettes() {
const step = i;
return (
-
+
-
+ {showReadability ? (
+
+ ) : (
+
+ )}
diff --git a/apps/web/components/palette/base-palette-button.tsx b/apps/web/components/palette/base-palette-button.tsx
index bda5850..287600d 100644
--- a/apps/web/components/palette/base-palette-button.tsx
+++ b/apps/web/components/palette/base-palette-button.tsx
@@ -2,7 +2,7 @@
import { BaseColorTypes, RawColor } from "@/types/app";
import { cn } from "@ui/lib/utils";
-import { AnimatePresence, motion } from "framer-motion";
+import { motion } from "framer-motion";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { Copy } from "lucide-react";
@@ -97,7 +97,9 @@ const PaletteButton = ({
transitionDelay: `${animation}s`,
transitionDuration: `${animation * 1.2}s`,
}}
- initial={false}
+ initial={{
+ opacity: 1,
+ }}
animate={
performance === Performance.high
? {
diff --git a/apps/web/components/palette/readability-string.tsx b/apps/web/components/palette/readability-string.tsx
new file mode 100644
index 0000000..b4d45d0
--- /dev/null
+++ b/apps/web/components/palette/readability-string.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { useColorStore } from "@/store/store";
+import { BaseColorTypes } from "@/types/app";
+import { cn } from "@ui/lib/utils";
+
+function ReadabilityString({
+ type,
+ step,
+}: {
+ type: BaseColorTypes;
+ step: number;
+}) {
+ const { colorPalettes } = useColorStore();
+ const readability = colorPalettes[type][step]?.readability;
+ // Function to determine border style
+ const getBorderStyle = (readabilityValue: number) => {
+ if (readabilityValue >= 7) {
+ return "border-border ring-1 ring-inset ring-offset-2 ring-primary ring-offset-border";
+ } else if (readabilityValue >= 4.5) {
+ return "border border-accent-foreground";
+ } else if (readabilityValue >= 3) {
+ console.log(`readabilityValue: ${readabilityValue}`);
+ return "border border-dashed";
+ } else {
+ return "";
+ }
+ };
+
+ const foregroundBorderStyle = readability
+ ? getBorderStyle(readability.foreground.readability)
+ : "";
+
+ const backgroundBorderStyle = readability
+ ? getBorderStyle(readability.background.readability)
+ : "";
+ return (
+
+
+ {readability?.foreground?.readability}
+
+
+ {readability?.background?.readability}
+
+
+ );
+}
+
+export default ReadabilityString;
diff --git a/apps/web/components/svg/eye-cvd.tsx b/apps/web/components/svg/eye-cvd.tsx
new file mode 100644
index 0000000..b9b5a51
--- /dev/null
+++ b/apps/web/components/svg/eye-cvd.tsx
@@ -0,0 +1,39 @@
+import * as React from "react";
+import { SVGProps } from "react";
+const EyeCVD = (props: SVGProps) => (
+
+);
+export default EyeCVD;
diff --git a/apps/web/components/toolbar/cvd.tsx b/apps/web/components/toolbar/cvd.tsx
new file mode 100644
index 0000000..7adeaf2
--- /dev/null
+++ b/apps/web/components/toolbar/cvd.tsx
@@ -0,0 +1,34 @@
+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";
+
+function CVD() {
+ const menuItem = primaryToolbarMenu["Color Vision Deficiency"];
+ const { showReadability } = useColorStore();
+ return (
+
+ );
+}
+
+export default CVD;
+
+CVD.displayName = "CVD";
diff --git a/apps/web/components/toolbar/index.tsx b/apps/web/components/toolbar/index.tsx
index 25d25b8..c9dbd53 100644
--- a/apps/web/components/toolbar/index.tsx
+++ b/apps/web/components/toolbar/index.tsx
@@ -1,6 +1,8 @@
import ToolbarUploadImage from "./upload-image";
import ToolbarDownloadBasePalette from "./download-base-palette";
import ToolbarShareableLink from "./shareable-link";
+import Readability from "./readability";
+import CVD from "./cvd";
const ToolbarPrimative = ({ children }) => children;
@@ -10,4 +12,6 @@ export const Toolbar = Object.assign(ToolbarPrimative, {
UploadImage: ToolbarUploadImage,
DownloadBasePalette: ToolbarDownloadBasePalette,
ShareableLink: ToolbarShareableLink,
+ Readability: Readability,
+ CVD: CVD,
});
diff --git a/apps/web/components/toolbar/readability.tsx b/apps/web/components/toolbar/readability.tsx
new file mode 100644
index 0000000..5e1ebc5
--- /dev/null
+++ b/apps/web/components/toolbar/readability.tsx
@@ -0,0 +1,47 @@
+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";
+
+const ReadabilityPlugin = dynamic(() => import("./plugin/readability.plugin"), {
+ ssr: false,
+});
+
+function Readability() {
+ const menuItem = primaryToolbarMenu.Readability;
+ const [open, setOpen] = useState(false);
+ const { showReadability } = useColorStore();
+ return (
+
+
+
+
+
+ {open && }
+
+
+ );
+}
+
+export default Readability;
+
+Readability.displayName = "Readability";
diff --git a/apps/web/components/ui/color-mode-dropdown-menu.tsx b/apps/web/components/ui/color-mode-dropdown-menu.tsx
index f82f02c..392f3b0 100644
--- a/apps/web/components/ui/color-mode-dropdown-menu.tsx
+++ b/apps/web/components/ui/color-mode-dropdown-menu.tsx
@@ -10,10 +10,7 @@ import {
} from "@ui/components/ui/dropdown-menu";
import { useColorStore } from "@/store/store";
import { ColorMode } from "@/types/app";
-import { ChevronDownIcon } from "lucide-react";
-import { useEffect, useState } from "react";
function ColorModeDropdownMenu() {
- const [mounted, setMounted] = useState(false);
const { colorMode, setColorMode } = useColorStore();
const handleChangeColorMode = async (mode: ColorMode) => {
setColorMode(mode);
diff --git a/apps/web/components/ui/desktop-primary-toolbar-buttons.tsx b/apps/web/components/ui/desktop-primary-toolbar-buttons.tsx
index 0fde3de..3ce3d63 100644
--- a/apps/web/components/ui/desktop-primary-toolbar-buttons.tsx
+++ b/apps/web/components/ui/desktop-primary-toolbar-buttons.tsx
@@ -12,6 +12,8 @@ function DesktopPreviewToolbarButtons() {
)}
>
+
+ {/* coming soon */}
diff --git a/apps/web/components/ui/primary-toolbar-menu.tsx b/apps/web/components/ui/primary-toolbar-menu.tsx
index 6091dac..8c5112c 100644
--- a/apps/web/components/ui/primary-toolbar-menu.tsx
+++ b/apps/web/components/ui/primary-toolbar-menu.tsx
@@ -1,9 +1,12 @@
-import { Download, ImagePlus, Link } from "lucide-react";
+import { Download, Eye, ImagePlus, Link } from "lucide-react";
+import EyeCVD from "../svg/eye-cvd";
export enum ToolbarMenus {
UPLOAD_IMAGE = "Upload Image",
DOWNLOAD = "Download",
SHARE = "Share",
+ READABILITY = "Readability",
+ CVD = "Color Vision Deficiency",
}
export type ToolbarMenu = {
@@ -17,6 +20,16 @@ export type PrimaryToolbarMenu = {
};
const primaryToolbarMenu: PrimaryToolbarMenu = {
+ [ToolbarMenus.READABILITY]: {
+ title: "Readability",
+ description: "Check the contrast ratio of your palette",
+ icon: Eye,
+ },
+ [ToolbarMenus.CVD]: {
+ title: "Color Vision Deficiency",
+ description: "Simulate color vision deficiency",
+ icon: EyeCVD,
+ },
[ToolbarMenus.UPLOAD_IMAGE]: {
title: "Upload Image",
description: "Generate a color palette from an image",
diff --git a/apps/web/lib/generateReadability.tsx b/apps/web/lib/generateReadability.tsx
new file mode 100644
index 0000000..0aa5480
--- /dev/null
+++ b/apps/web/lib/generateReadability.tsx
@@ -0,0 +1,78 @@
+import { ColorPalettes, ColorValue } from "@/types/app";
+import tinycolor from "tinycolor2";
+
+export const generateReadability = ({
+ foreground,
+ background,
+ primaryPalette,
+ secondaryPalette,
+ accentPalette,
+ grayPalette,
+}: {
+ foreground: ColorValue;
+ background: ColorValue;
+ primaryPalette: ColorPalettes["primary"];
+ secondaryPalette: ColorPalettes["secondary"];
+ accentPalette: ColorPalettes["accent"];
+ grayPalette: ColorPalettes["gray"];
+}): ColorPalettes => {
+ /*
+ ColorValue = {
+ step: number;
+ color: string;
+ raw: RawColor;
+ readability?: {
+ foreground: ColorReadability;
+ background: ColorReadability;
+ };
+
+ type ColorReadability = {
+ readability: number;
+ isReadable: boolean;
+};
+
+tinycolor refs:
+readability: function(TinyColor, TinyColor) -> Object. Returns the contrast ratio between two colors.
+isReadable: function(TinyColor, TinyColor, Object) -> Boolean. Ensure that foreground and background color combinations meet WCAG guidelines.
+}
+ */
+ const foregroundColor = tinycolor(foreground.color);
+ const backgroundColor = tinycolor(background.color);
+ // compare each color in the palette to the foreground and background
+ // and insert the readability score into the palette
+ const [primary, secondary, accent, gray] = [
+ primaryPalette,
+ secondaryPalette,
+ accentPalette,
+ grayPalette,
+ ].map((palette) => {
+ return palette.map((color) => {
+ const colorValue = tinycolor(color.color);
+ return {
+ ...color,
+ readability: {
+ foreground: {
+ readability:
+ Math.round(
+ tinycolor.readability(foregroundColor, colorValue) * 100,
+ ) / 100,
+ isReadable: tinycolor.isReadable(foregroundColor, colorValue),
+ },
+ background: {
+ readability:
+ Math.round(
+ tinycolor.readability(backgroundColor, colorValue) * 100,
+ ) / 100,
+ isReadable: tinycolor.isReadable(backgroundColor, colorValue),
+ },
+ },
+ };
+ });
+ });
+ return {
+ primary,
+ secondary,
+ accent,
+ gray,
+ };
+};
diff --git a/apps/web/lib/getServerColors.ts b/apps/web/lib/getServerColors.ts
index 6edfc15..ddeef83 100644
--- a/apps/web/lib/getServerColors.ts
+++ b/apps/web/lib/getServerColors.ts
@@ -3,11 +3,13 @@ import { generateBaseColors } from "./generateBaseColors";
import { generateColorPalette } from "./colorCalculator";
import { BaseColorTypes, ColorMode } from "@/types/app";
import { colorHelper } from "./colorHelper";
+import { generateReadability } from "./generateReadability";
export const getServerColors = async () => {
const newBaseColors = generateBaseColors();
const cookieStore = cookies();
const mode = cookieStore.get("colorMode");
+ const showReadability = cookieStore.get("showReadability");
// short-hand
const [primaryPalette, secondaryPalette, accentPalette, grayPalette] = [
"primary",
@@ -21,14 +23,30 @@ export const getServerColors = async () => {
colorMode: mode ? (mode.value as ColorMode) : ColorMode.HEX,
}),
);
+ /* Calculate WCAG 2.0 */
+ const foreground = grayPalette[0];
+ const background = grayPalette[10];
+ const palettesWithReadability = generateReadability({
+ foreground,
+ background,
+ primaryPalette,
+ secondaryPalette,
+ accentPalette,
+ grayPalette,
+ });
return {
baseColors: newBaseColors,
colorPalettes: {
- primary: primaryPalette,
- secondary: secondaryPalette,
- accent: accentPalette,
- gray: grayPalette,
+ primary: palettesWithReadability.primary,
+ secondary: palettesWithReadability.secondary,
+ accent: palettesWithReadability.accent,
+ gray: palettesWithReadability.gray,
},
colorMode: mode ? (mode.value as ColorMode) : ColorMode.HEX,
+ showReadability: showReadability?.value
+ ? showReadability.value === "true"
+ ? true
+ : false
+ : false,
};
};
diff --git a/apps/web/store/store.ts b/apps/web/store/store.ts
index ae36735..544d960 100644
--- a/apps/web/store/store.ts
+++ b/apps/web/store/store.ts
@@ -20,12 +20,14 @@ import {
} from "zustand/middleware";
import { generateColorPalette } from "@/lib/colorCalculator";
import { updateCSSVariables } from "@/lib/updateCssVariables";
+import { generateReadability } from "@/lib/generateReadability";
// Zustand store type
export type ColorStore = {
colorMode: ColorMode;
baseColors: Omit;
colorPalettes: ColorPalettes;
+ showReadability: boolean;
setColorMode: (mode: ColorMode) => void;
generatePalette: (existing?: boolean) => void;
updateBaseColor: (newBaseColor: keyof BaseColors, newColor: RawColor) => void;
@@ -40,6 +42,7 @@ let localAndUrlStore = (set, get) => ({
accent: [],
gray: [],
},
+ showReadability: false,
setColorMode: (mode) => set({ colorMode: mode }),
updateBaseColor: (type: BaseColorTypes, newColor: RawColor) => {
const [newPalette, grayPalette] = [
@@ -54,11 +57,22 @@ let localAndUrlStore = (set, get) => ({
colorMode: get().colorMode,
}),
];
+ /* Calculate WCAG 2.0 */
+ const foreground = grayPalette[0];
+ const background = grayPalette[10];
+ const palettesWithReadability = generateReadability({
+ foreground,
+ background,
+ primaryPalette: newPalette,
+ secondaryPalette: get().colorPalettes.secondary,
+ accentPalette: get().colorPalettes.accent,
+ grayPalette,
+ });
set(
produce((state: ColorStore) => {
state.baseColors[type] = newColor;
- state.colorPalettes[type] = newPalette;
- state.colorPalettes.gray = grayPalette;
+ state.colorPalettes[type] = palettesWithReadability[type];
+ state.colorPalettes.gray = palettesWithReadability.gray;
}),
);
const { colorPalettes, baseColors, colorMode } = get();
@@ -88,14 +102,25 @@ let localAndUrlStore = (set, get) => ({
colorMode: get().colorMode,
}),
);
+ /* Calculate WCAG 2.0 */
+ const foreground = grayPalette[0];
+ const background = grayPalette[10];
+ const palettesWithReadability = generateReadability({
+ foreground,
+ background,
+ primaryPalette,
+ secondaryPalette,
+ accentPalette,
+ grayPalette,
+ });
set(
produce((state: ColorStore) => {
state.baseColors = newBaseColors;
state.colorPalettes = {
- primary: primaryPalette,
- secondary: secondaryPalette,
- accent: accentPalette,
- gray: grayPalette,
+ primary: palettesWithReadability.primary,
+ secondary: palettesWithReadability.secondary,
+ accent: palettesWithReadability.accent,
+ gray: palettesWithReadability.gray,
};
}),
);
diff --git a/apps/web/types/app.ts b/apps/web/types/app.ts
index e9c1793..3b53064 100644
--- a/apps/web/types/app.ts
+++ b/apps/web/types/app.ts
@@ -21,11 +21,30 @@ export type BaseColorTypes = "primary" | "secondary" | "accent" | "gray";
export type BaseColors = Record;
+export type ColorReadability = {
+ readability: number;
+ isReadable: boolean;
+};
+
+export enum CVDType {
+ DeficiencyProt = "protanomaly and protanopia",
+ DeficiencyDeuter = "deuteranomaly and deuteranopia",
+ DeficiencyTrit = "tritanomaly and tritanopia",
+}
+
export type ColorValue =
| {
step: number;
color: string;
raw: RawColor;
+ /**
+ * Readability is calculated using the WCAG 2.0 formula
+ * Compares the contrast ratio between the foreground and background colors
+ */
+ readability?: {
+ foreground: ColorReadability;
+ background: ColorReadability;
+ };
}
| undefined;