Skip to content

Commit

Permalink
Merge pull request #45 from suryanshsingh2001/feature/autoPick
Browse files Browse the repository at this point in the history
Add Feature/AutoPick Colors
  • Loading branch information
suryanshsingh2001 authored Dec 5, 2024
2 parents 6a3d7b8 + 511cc8d commit e8e0332
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 11 deletions.
88 changes: 77 additions & 11 deletions components/shared/Editor.v2.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"use client";

import React, { useState, useRef, useEffect, useCallback } from "react";
import { Upload, X, RotateCcw, RotateCcwIcon, LinkIcon } from "lucide-react";
import {
Upload,
X,
RotateCcw,
RotateCcwIcon,
LinkIcon,
Sparkles,
} from "lucide-react";
import { useDropzone } from "react-dropzone";
import { saveAs } from "file-saver";
import jsPDF from "jspdf";
Expand Down Expand Up @@ -30,17 +37,22 @@ import { Separator } from "@radix-ui/react-select";
import ExportButton from "./buttons/ExportButton";
import { truncateFileName } from "@/lib/utils";
import { backgroundUrls, screenSizes } from "@/lib/constants";
import { getGradientFromImage } from "@/lib/extractColors";

const validationError = {
customHeight: "",
customWidth: "",
} satisfies ValidationError;

type BackgroundTab = "color" | "gradient" | "image";

const defaultSettings = {
image: null,
background: backgroundUrls[0],
backgroundTab: "image" as BackgroundTab,
customColor1: "#ffffff",
customColor2: "#000000",
customColor3: "#000000",
gradientAngle: 0,
screenSize: screenSizes[2],
zoom: 50,
Expand Down Expand Up @@ -76,6 +88,10 @@ const defaultSettings = {

export default function MockupEditor() {
const [image, setImage] = useState<string | null>(defaultSettings.image);

const [backgroundTab, setBackgroundTab] = useState<BackgroundTab>(
defaultSettings.backgroundTab
);
const [background, setBackground] = useState(defaultSettings.background);
const [isCustomBackground, setIsCustomBackground] = useState(false);
const [isUrlFormat, setIsUrlFormat] = useState<boolean>(true);
Expand All @@ -90,6 +106,9 @@ export default function MockupEditor() {
const [customColor2, setCustomColor2] = useState(
defaultSettings.customColor2
);
const [customColor3, setCustomColor3] = useState(
defaultSettings.customColor3
);
const [gradientAngle, setGradientAngle] = useState(
defaultSettings.gradientAngle
);
Expand Down Expand Up @@ -133,7 +152,6 @@ export default function MockupEditor() {
const [browsedFile, setIsBrowsedFile] = useState(false);
const [displayFileName, setDisplayFileName] = useState<string>("");


// Refs for canvas and container elements

const canvasRef = useRef<HTMLCanvasElement>(null);
Expand Down Expand Up @@ -220,6 +238,7 @@ export default function MockupEditor() {
setBackground(defaultSettings.background);
setCustomColor1(defaultSettings.customColor1);
setCustomColor2(defaultSettings.customColor2);
setCustomColor3(defaultSettings.customColor3);
setGradientAngle(defaultSettings.gradientAngle);
setScreenSize(defaultSettings.screenSize);
setPresetScreenSize(defaultSettings.screenSize);
Expand All @@ -237,10 +256,27 @@ export default function MockupEditor() {
setDownloadFormat(defaultSettings.format);
setLoadedImage(null);
setIsCustomBackground(false);
setBackgroundTab(defaultSettings.backgroundTab);
setCustomImg("");
setDisplayFileName("");
};

const autoPickColor = async () => {
if (!image) {
return;
}
getGradientFromImage(image).then((colors) => {
if (colors) {
console.log(colors);
setCustomColor1(colors[0]);
setCustomColor2(colors[1]);
setCustomColor3(colors[2]);
setBackground("gradient");
setBackgroundTab("gradient");
}
});
};

const updateCanvasScale = useCallback(() => {
if (containerRef.current && canvasRef.current) {
const containerWidth = containerRef.current.clientWidth;
Expand Down Expand Up @@ -330,7 +366,8 @@ export default function MockupEditor() {
Math.sin((gradientAngle * Math.PI) / 180) * screenSize.height
);
gradient.addColorStop(0, customColor1);
gradient.addColorStop(1, customColor2);
gradient.addColorStop(0.33, customColor2);
gradient.addColorStop(1, customColor3);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, screenSize.width, screenSize.height);
} else {
Expand Down Expand Up @@ -639,16 +676,14 @@ export default function MockupEditor() {
}
}, [displayFileName]);



return (
<div className="flex flex-col px-6">
<Header />
<main className="container mx-auto h-screen ">
<div className="flex flex-col lg:flex-row gap-8 ">
<div className="w-full lg:w-1/4 space-y-8 overflow-y-auto h-full p-2 ">
<div className="">
<Label htmlFor="image-upload" className="block mb-4">
<Label htmlFor="image-upload" className="block mb-4 text-md">
Upload Image
</Label>
<div
Expand Down Expand Up @@ -711,10 +746,32 @@ export default function MockupEditor() {
)}
</div>
<div className="w-full">
<Label htmlFor="background" className="block mb-4">
Background
</Label>
<Tabs defaultValue="image" className="w-full">
<div className="flex items-center justify-between mb-2">
<Label htmlFor="background" className="block text-md">
Background
</Label>

<Button
onClick={autoPickColor}
variant={"default"}
size={"sm"}
className="text-white font-semibold rounded transition transform hover:scale-105"
>
<>
<Sparkles className="h-4 w-4 mr-2" />
Auto Pick
</>
</Button>
</div>

<Tabs
defaultValue="image"
value={backgroundTab}
onValueChange={(value) => {
setBackgroundTab(value as BackgroundTab);
}}
className="w-full"
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="color">Color</TabsTrigger>
<TabsTrigger value="gradient">Gradient</TabsTrigger>
Expand Down Expand Up @@ -751,6 +808,15 @@ export default function MockupEditor() {
}}
className="w-1/2 h-10"
/>
<Input
type="color"
value={customColor3}
onChange={(e) => {
setCustomColor2(e.target.value);
setBackground("gradient");
}}
className="w-1/2 h-10"
/>
</div>
<Slider
min={0}
Expand Down Expand Up @@ -875,7 +941,7 @@ export default function MockupEditor() {
</Tabs>
</div>
<div className="w-full">
<Label htmlFor="screen-size" className="block mb-4">
<Label htmlFor="screen-size" className="block mb-4 text-md">
Screen Size
</Label>
<Tabs defaultValue="preset" className="w-full">
Expand Down
130 changes: 130 additions & 0 deletions lib/extractColors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// extractColors.ts
import {extractColors} from 'extract-colors';

/**
* Shuffle an array in place using the Fisher-Yates algorithm.
* @param array - The array to shuffle.
*/
const shuffleArray = <T>(array: T[]): void => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};

/**
* Select the top three most prominent colors.
* @param colors - Array of extracted color objects.
* @returns An array of three hex color strings.
*/
const selectTopThreeColors = (colors: { hex: string }[]): string[] => {
return colors.slice(0, 3).map(color => color.hex);
};

/**
* Select the first, middle, and last colors with slight randomness.
* @param colors - Array of extracted color objects.
* @returns An array of three hex color strings.
*/
const selectFirstMiddleLastColors = (colors: { hex: string }[]): string[] => {
const firstColor = colors[0].hex;

// Introduce slight randomness in selecting the middle color
const middleIndex = Math.floor(colors.length / 2);
const middleVariation = Math.floor(Math.random() * 3) - 1; // -1, 0, or +1
const adjustedMiddleIndex = Math.min(Math.max(middleIndex + middleVariation, 0), colors.length - 1);
const middleColor = colors[adjustedMiddleIndex].hex;

// Introduce slight randomness in selecting the last color
const lastIndex = colors.length - 1;
const lastVariation = Math.floor(Math.random() * 3) - 1; // -1, 0, or +1
const adjustedLastIndex = Math.min(Math.max(lastIndex + lastVariation, 0), colors.length - 1);
const lastColor = colors[adjustedLastIndex].hex;

return [firstColor, middleColor, lastColor];
};

/**
* Select three evenly distributed colors with added randomness.
* @param colors - Array of extracted color objects.
* @returns An array of three hex color strings.
*/
const selectEvenlyDistributedColors = (colors: { hex: string }[]): string[] => {
const interval = Math.floor(colors.length / 4);
const variation = Math.floor(Math.random() * interval) - Math.floor(interval / 2); // Introduce variation

const firstIndex = 0 + variation;
const secondIndex = interval + variation;
const thirdIndex = 2 * interval + variation;

// Ensure indices are within bounds
const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max);

const firstColor = colors[clamp(firstIndex, 0, colors.length - 1)].hex;
const secondColor = colors[clamp(secondIndex, 0, colors.length - 1)].hex;
const thirdColor = colors[clamp(thirdIndex, 0, colors.length - 1)].hex;

return [firstColor, secondColor, thirdColor];
};

/**
* Select three random distinct colors from the extracted palette.
* @param colors - Array of extracted color objects.
* @returns An array of three hex color strings.
*/
const selectRandomThreeColors = (colors: { hex: string }[]): string[] => {
shuffleArray(colors);
return colors.slice(0, 3).map(color => color.hex);
};

// Define the selection methods
const selectionMethods = [
selectTopThreeColors,
selectFirstMiddleLastColors,
selectEvenlyDistributedColors
// Removed selectRandomThreeColors to limit to three methods
];

/**
* Keeps track of the current selection method index.
*/
let currentSelectionIndex = 0;

/**
* Cycle through the selection methods to pick three colors.
* @param colors - Array of extracted color objects.
* @returns An array of three hex color strings or null if not enough colors.
*/
const selectThreeColors = (colors: { hex: string }[]): string[] | null => {
if (colors.length < 3) return null;

const selectedMethod = selectionMethods[currentSelectionIndex];
const selectedColors = selectedMethod(colors);

// Update the index for next call
currentSelectionIndex = (currentSelectionIndex + 1) % selectionMethods.length;

return selectedColors;
};

/**
* Extract three gradient colors from an image source.
* @param imageSrc - The source URL of the image.
* @returns An array of three hex color strings or null.
*/
export const getGradientFromImage = async (imageSrc: string): Promise<string[] | null> => {
try {
const colors = await extractColors(imageSrc); // Extract up to 10 colors

console.log("Extracted colors:", colors);
const selectedColors = selectThreeColors(colors);

if (selectedColors) {
return selectedColors;
}
return null;
} catch (error) {
console.error("Failed to extract colors:", error);
return null;
}
};
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@types/react-color": "^3.0.12",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"extract-colors": "^4.1.0",
"file-saver": "^2.0.5",
"framer-motion": "^11.3.31",
"gif.js": "^0.2.0",
Expand Down

0 comments on commit e8e0332

Please sign in to comment.