@studiotoolkit/color-wheel 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Studio Toolkit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Color Wheel
2
+
3
+ Interactive color wheel React component for creating and manipulating color harmonies with support for multiple harmony modes and customization options.
4
+
5
+ **Play around and Generate component code here:**
6
+ https://studiotoolkit.github.io/build-color-wheel
7
+
8
+ **Complete Demo:**
9
+ https://studiotoolkit.github.io/demo-all
10
+
11
+ ## Features
12
+
13
+ ### Harmony Modes
14
+
15
+ <table>
16
+ <tr>
17
+ <td align="center"><img src="assets/analogous.png" width="100"/><br/>Analogous</td>
18
+ <td align="center"><img src="assets/complementary.png" width="100"/><br/>Complementary</td>
19
+ <td align="center"><img src="assets/monochromatic.png" width="100"/><br/>Monochromatic</td>
20
+ <td align="center"><img src="assets/simple.png" width="100"/><br/>Simple</td>
21
+ </tr>
22
+ <tr>
23
+ <td align="center"><img src="assets/split-complementary.png" width="100"/><br/>Split Complementary</td>
24
+ <td align="center"><img src="assets/square.png" width="100"/><br/>Square</td>
25
+ <td align="center"><img src="assets/triadic.png" width="100"/><br/>Triadic</td>
26
+ <td align="center"></td>
27
+ </tr>
28
+ </table>
29
+
30
+ ### Customization Options
31
+
32
+ <table>
33
+ <tr>
34
+ <td align="center"><img src="assets/background-image.png" width="100"/><br/>Background Color</td>
35
+ <td align="center"><img src="assets/center-dot.png" width="100"/><br/>Center Dot</td>
36
+ <td align="center"><img src="assets/harmony-polygon.png" width="100"/><br/>Harmony Polygon</td>
37
+ <td align="center"><img src="assets/thickness.png" width="100"/><br/>Thickness</td>
38
+ </tr>
39
+ <tr>
40
+ <td align="center"><img src="assets/thickness2.png" width="100"/><br/>Thickness variation</td>
41
+ <td align="center"></td>
42
+ <td align="center"></td>
43
+ <td align="center"></td>
44
+ </tr>
45
+ </table>
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ # Using pnpm
51
+ pnpm add @studiotoolkit/color-wheel
52
+
53
+ # Using npm
54
+ npm install @studiotoolkit/color-wheel
55
+
56
+ # Using yarn
57
+ yarn add @studiotoolkit/color-wheel
58
+ ```
59
+
60
+ **React Version:** `^18.0.0` or higher
61
+
62
+ ## Usage
63
+
64
+ ### Basic Usage
65
+
66
+ ```tsx
67
+ import { ColorWheel } from '@studiotoolkit/color-wheel';
68
+ import '@studiotoolkit/color-wheel/style.css';
69
+
70
+ function App() {
71
+ const handleColorsChange = (palette) => {
72
+ console.log('Selected colors:', palette);
73
+ };
74
+
75
+ return (
76
+ <ColorWheel
77
+ onColorsChange={handleColorsChange}
78
+ harmonyMode="complementary"
79
+ width={300}
80
+ height={300}
81
+ />
82
+ );
83
+ }
84
+ ```
85
+
86
+ **Sample Output:**
87
+
88
+ ```json
89
+ {
90
+ "split": ["#00b1ff", "#ff0031", "#ffce00"]
91
+ }
92
+ ```
93
+
94
+ ### With Custom Styling
95
+
96
+ ```tsx
97
+ import { ColorWheel } from '@studiotoolkit/color-wheel';
98
+ import '@studiotoolkit/color-wheel/style.css';
99
+
100
+ function App() {
101
+ const handleColorsChange = (palette) => {
102
+ console.log('Selected colors:', palette);
103
+ };
104
+
105
+ return (
106
+ <ColorWheel
107
+ onColorsChange={handleColorsChange}
108
+ harmonyMode="triadic"
109
+ saturation={100}
110
+ brightness={50}
111
+ width={400}
112
+ height={400}
113
+ wheelThickness={80}
114
+ dotSize={5}
115
+ dotPosition="outer"
116
+ handleThickness={2}
117
+ handleColor="#000000"
118
+ harmonyPolygon={true}
119
+ polygonColor="#000000"
120
+ polygonThickness={1}
121
+ showCenterDot={true}
122
+ centerDot="#ffffff"
123
+ borderColor="#ffffff"
124
+ canvasBackgroundColor="transparent"
125
+ />
126
+ );
127
+ }
128
+ ```
129
+
130
+ ### Harmony Modes
131
+
132
+ The ColorWheel supports the following harmony modes:
133
+
134
+ - `complementary` - Colors opposite on the color wheel
135
+ - `analogous` - Adjacent colors on the color wheel
136
+ - `monochromatic` - Variations of a single hue
137
+ - `split` - Split complementary harmony
138
+ - `triadic` - Three evenly spaced colors
139
+ - `square` - Four evenly spaced colors
140
+ - `simple` - Single color selection
141
+
142
+ ## Contributors
143
+
144
+ [StudioToolkit](https://github.com/studiotoolkit)
145
+
146
+ ## License
147
+
148
+ Licensed under the MIT License.
149
+
150
+ ## Keywords
151
+
152
+ color-wheel, color-picker, color-harmony, harmony-polygon, react-color, color-selection, design-tools, ui-components
@@ -0,0 +1,35 @@
1
+ import "./ColorWheel.css";
2
+ type HarmonyMode = "complementary" | "analogous" | "monochromatic" | "split" | "triadic" | "square" | "simple";
3
+ type DotPosition = "center" | "inner" | "outer";
4
+ export interface ColorData {
5
+ HEX: string;
6
+ RGB: [number, number, number];
7
+ HSL: [number, number, number];
8
+ }
9
+ export interface PaletteOutput {
10
+ [key: string]: string[];
11
+ }
12
+ interface ColorWheelProps {
13
+ onColorsChange?: (palette: PaletteOutput) => void;
14
+ harmonyMode?: HarmonyMode;
15
+ saturation?: number;
16
+ brightness?: number;
17
+ circleRadius?: number;
18
+ wheelThickness?: number;
19
+ dotSize?: number;
20
+ dotPosition?: DotPosition;
21
+ handleThickness?: number;
22
+ handleColor?: string;
23
+ harmonyPolygon?: boolean;
24
+ polygonColor?: string;
25
+ polygonThickness?: number;
26
+ centerDot?: string;
27
+ showCenterDot?: boolean;
28
+ borderColor?: string;
29
+ canvasBackgroundColor?: string;
30
+ width?: number;
31
+ height?: number;
32
+ }
33
+ export default function ColorWheel({ onColorsChange, harmonyMode: propHarmonyMode, saturation: propSaturation, brightness: propBrightness, circleRadius: propCircleRadius, wheelThickness: propWheelThickness, dotSize: propDotSize, dotPosition: propDotPosition, handleThickness: propHandleThickness, handleColor: propHandleColor, harmonyPolygon: propHarmonyPolygon, polygonColor: propPolygonColor, polygonThickness: propPolygonThickness, centerDot: propCenterDot, showCenterDot: propShowCenterDot, borderColor: propBorderColor, canvasBackgroundColor: propCanvasBackgroundColor, width, height, }: ColorWheelProps): import("react/jsx-runtime").JSX.Element;
34
+ export {};
35
+ //# sourceMappingURL=ColorWheel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorWheel.d.ts","sourceRoot":"","sources":["../src/ColorWheel.tsx"],"names":[],"mappings":"AACA,OAAO,kBAAkB,CAAC;AAG1B,KAAK,WAAW,GACZ,eAAe,GACf,WAAW,GACX,eAAe,GACf,OAAO,GACP,SAAS,GACT,QAAQ,GACR,QAAQ,CAAC;AACb,KAAK,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACzB;AAUD,UAAU,eAAe;IACvB,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAClD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAwHD,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EACjC,cAAc,EACd,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,cAAc,EAC1B,UAAU,EAAE,cAAc,EAC1B,YAAY,EAAE,gBAAgB,EAC9B,cAAc,EAAE,kBAAkB,EAClC,OAAO,EAAE,WAAW,EACpB,WAAW,EAAE,eAAe,EAC5B,eAAe,EAAE,mBAAmB,EACpC,WAAW,EAAE,eAAe,EAC5B,cAAc,EAAE,kBAAkB,EAClC,YAAY,EAAE,gBAAgB,EAC9B,gBAAgB,EAAE,oBAAoB,EACtC,SAAS,EAAE,aAAa,EACxB,aAAa,EAAE,iBAAyB,EACxC,WAAW,EAAE,eAA2B,EACxC,qBAAqB,EAAE,yBAAyC,EAChE,KAAW,EACX,MAAY,GACb,EAAE,eAAe,2CAuejB"}
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from 'react';
2
+ interface ThemeContextType {
3
+ isDark: boolean;
4
+ setIsDark: (v: boolean | ((prev: boolean) => boolean)) => void;
5
+ }
6
+ export declare function ThemeProvider({ children }: {
7
+ children: ReactNode;
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ export declare function useTheme(): ThemeContextType;
10
+ export {};
11
+ //# sourceMappingURL=ThemeContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeContext.d.ts","sourceRoot":"","sources":["../src/ThemeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvF,UAAU,gBAAgB;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,KAAK,IAAI,CAAC;CAChE;AAOD,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CASlE;AAED,wBAAgB,QAAQ,qBAEvB"}
@@ -0,0 +1,4 @@
1
+ export { default as ColorWheel } from "./ColorWheel";
2
+ export type { ColorData, PaletteOutput } from "./ColorWheel";
3
+ export { ThemeProvider, useTheme } from "./ThemeContext";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const F=require("react/jsx-runtime"),t=require("react"),me=t.createContext({isDark:!1,setIsDark:()=>{}});function Ye({children:S}){const[i,g]=t.useState(()=>localStorage.getItem("theme")==="dark");return t.useEffect(()=>{document.documentElement.setAttribute("data-theme",i?"dark":"light"),localStorage.setItem("theme",i?"dark":"light")},[i]),F.jsx(me.Provider,{value:{isDark:i,setIsDark:g},children:S})}function ge(){return t.useContext(me)}const he={complementary:[0,180],analogous:[-30,0,30],monochromatic:[0],split:[0,150,-150],triadic:[0,120,240],square:[0,90,180,270],simple:[0]},ze=[{name:"Harmony Mode",attribute:"harmony-mode",default:"split",show:!0,notes:""},{name:"Saturation",attribute:"saturation",default:100,show:!0,notes:""},{name:"Brightness",attribute:"brightness",default:50,show:!0,notes:""},{name:"Circle Radius",attribute:"circle-radius",default:1,show:!1,notes:"If not passed, default it is false"},{name:"Wheel Thickness",attribute:"wheel-thickness",default:80,show:!1,notes:"If not passed, default it is false"},{name:"Dot Size",attribute:"dot-size",default:5,show:!1,notes:"If not passed, default it is false"},{name:"Dot Position",attribute:"dot-position",default:"outer",show:!1,notes:"If not passed, default it is false"},{name:"Handle Thickness",attribute:"handle-thickness",default:2,show:!1,notes:"If not passed, default it is false"},{name:"Handle Color",attribute:"handle-color",default:"#000000",show:!1,notes:"If not passed, default it is false"},{name:"Show Harmony Polygon",attribute:"harmony-polygon",default:!1,show:!1,notes:"If not passed, default it is false"},{name:"Polygon Color",attribute:"polygon-color",default:"#000000",show:!1,notes:"If not passed, default it is false"},{name:"Polygon Thickness",attribute:"polygon-thickness",default:1,show:!1,notes:"If not passed, default it is false"},{name:"Center Dot",attribute:"center-dot",default:"#ffffff",show:!1,notes:"If not passed, default it is false"},{name:"Palette",attribute:"palette",default:null,show:!1,notes:"If not passed, default it is false"},{name:"Palette JSON",attribute:"palette-json",default:null,show:!1,notes:"If not passed, default it is false"}];function Ae({onColorsChange:S,harmonyMode:i,saturation:g,brightness:E,circleRadius:b,wheelThickness:y,dotSize:k,dotPosition:P,handleThickness:C,handleColor:I,harmonyPolygon:T,polygonColor:R,polygonThickness:D,centerDot:j,showCenterDot:H=!1,borderColor:U="#ffffff",canvasBackgroundColor:Y="transparent",width:W=450,height:L=450}){const O=t.useRef(null),[be,z]=t.useState(!1),{isDark:G}=ge(),l=e=>ze.find(s=>s.attribute===e),[c,ye]=t.useState(i||l("harmony-mode").default),[N,ke]=t.useState(g??l("saturation").default),[$,we]=t.useState(E??l("brightness").default),A=t.useCallback(()=>{const e=l("circle-radius").default;if(b!==void 0)return b;if(e===1){const s=Math.min(W,L),a=y??l("wheel-thickness").default,n=k??l("dot-size").default;return s/2-a/2-n-10}return e},[W,L,b,y,k]),[w,J]=t.useState(A()),[v,Se]=t.useState(y??l("wheel-thickness").default),[K,ve]=t.useState(k??l("dot-size").default),[X,Me]=t.useState(P||l("dot-position").default),[Q,Ee]=t.useState(C??l("handle-thickness").default),[V,Pe]=t.useState(I||(l("handle-color").default==="black"?"#000000":l("handle-color").default)),[Z,Ce]=t.useState(T??l("harmony-polygon").default),[x,Ie]=t.useState(R||(l("polygon-color").default==="black"?"#000000":l("polygon-color").default)),[p,Te]=t.useState(D??l("polygon-thickness").default),[ee,Re]=t.useState(j||(l("center-dot").default==="white"?"#ffffff":l("center-dot").default)),[te,De]=t.useState(H),[_,je]=t.useState(U),[M,We]=t.useState(Y),[d,se]=t.useState(0);t.useEffect(()=>{i!==void 0&&ye(i)},[i]),t.useEffect(()=>{g!==void 0&&ke(g)},[g]),t.useEffect(()=>{E!==void 0&&we(E)},[E]),t.useEffect(()=>{J(b!==void 0?b:A())},[b,A]),t.useEffect(()=>{y!==void 0&&Se(y)},[y]),t.useEffect(()=>{k!==void 0&&ve(k)},[k]),t.useEffect(()=>{P!==void 0&&Me(P)},[P]),t.useEffect(()=>{C!==void 0&&Ee(C)},[C]),t.useEffect(()=>{I!==void 0&&Pe(I)},[I]),t.useEffect(()=>{T!==void 0&&Ce(T)},[T]),t.useEffect(()=>{R!==void 0&&Ie(R)},[R]),t.useEffect(()=>{D!==void 0&&Te(D)},[D]),t.useEffect(()=>{j!==void 0&&Re(j)},[j]),t.useEffect(()=>{De(H)},[H]),t.useEffect(()=>{je(U)},[U]),t.useEffect(()=>{We(Y)},[Y]);const o={x:W/2,y:L/2},B=e=>(e+360)%360,ae=t.useCallback((e,s=N,a=$)=>`hsl(${e}, ${s}%, ${a}%)`,[N,$]),Le=(e,s,a)=>{s/=100,a/=100;const n=(1-Math.abs(2*a-1))*s,f=n*(1-Math.abs(e/60%2-1)),u=a-n/2;let r=0,h=0,m=0;return e<60?[r,h,m]=[n,f,0]:e<120?[r,h,m]=[f,n,0]:e<180?[r,h,m]=[0,n,f]:e<240?[r,h,m]=[0,f,n]:e<300?[r,h,m]=[f,0,n]:[r,h,m]=[n,0,f],{r:Math.round((r+u)*255),g:Math.round((h+u)*255),b:Math.round((m+u)*255)}},Oe=(e,s,a)=>"#"+[e,s,a].map(n=>n.toString(16).padStart(2,"0")).join(""),ne=t.useCallback(()=>X==="center"?w:X==="inner"?w-v/2:w+v/2,[X,w,v]),le=t.useCallback(e=>{const s=(e-90)*Math.PI/180,a=ne();return{x:o.x+a*Math.cos(s),y:o.y+a*Math.sin(s)}},[o.x,o.y,ne]),oe=(e,s)=>{const a=e-o.x,n=s-o.y,f=Math.atan2(n,a)*180/Math.PI;return B(f+90)},fe=t.useCallback(()=>{let e=[];c==="monochromatic"?e=[{h:d,l:30},{h:d,l:50},{h:d,l:70}]:e=he[c].map(a=>({h:B(d+a),l:$}));const s=e.map(a=>{const n=Le(a.h,N,a.l);return Oe(n.r,n.g,n.b)});return{[c]:s}},[c,d,N,$]),ie=t.useCallback(e=>{for(let s=0;s<360;s++)e.beginPath(),e.strokeStyle=`hsl(${s},100%,50%)`,e.lineWidth=v,e.arc(o.x,o.y,w,(s-90)*Math.PI/180,(s-89)*Math.PI/180),e.stroke()},[o.x,o.y,w,v]),ue=t.useCallback((e,s)=>{!Z||s.length<3||(e.beginPath(),e.moveTo(s[0].x,s[0].y),s.slice(1).forEach(a=>{e.lineTo(a.x,a.y)}),e.closePath(),e.strokeStyle=x,e.lineWidth=p,e.stroke())},[Z,x,p]),ce=t.useCallback(e=>{if(c==="simple")return;const s=he[c],a=[];s.forEach(n=>{const f=B(d+n),u=le(f);a.push(u),e.beginPath(),e.moveTo(o.x,o.y),e.lineTo(u.x,u.y),e.strokeStyle=V,e.lineWidth=Q,e.stroke(),e.beginPath(),e.arc(u.x,u.y,K,0,Math.PI*2),e.fillStyle=ae(f),e.fill(),e.strokeStyle="white",e.stroke()}),ue(e,a),te&&(e.beginPath(),e.arc(o.x,o.y,5,0,Math.PI*2),e.fillStyle=ee,e.fill(),e.strokeStyle="white",e.lineWidth=1,e.stroke())},[c,d,o.x,o.y,K,Q,V,ee,te,le,ae,ue]),de=t.useCallback(()=>{const e=O.current;if(!e)return;const s=e.getContext("2d");s&&(s.clearRect(0,0,e.width,e.height),M&&M!=="transparent"?(s.fillStyle=M,s.fillRect(0,0,e.width,e.height)):M==="transparent"&&(s.fillStyle=G?"#1a1a1a":"#ffffff",s.fillRect(0,0,e.width,e.height)),ie(s),ce(s),_&&(s.strokeStyle=_,s.lineWidth=1,s.strokeRect(0,0,e.width,e.height)))},[ie,ce,_,M,G]),Ne=()=>z(!0),$e=()=>z(!1),qe=()=>z(!1),He=e=>{if(!be)return;const s=O.current;if(!s)return;const a=s.getBoundingClientRect(),n=oe(e.clientX-a.left,e.clientY-a.top);se(n)},Ue=e=>{const s=O.current;if(!s)return;const a=s.getBoundingClientRect(),n=oe(e.clientX-a.left,e.clientY-a.top);se(n)};t.useEffect(()=>{de()},[de]);const q=t.useMemo(()=>fe(),[fe]);t.useEffect(()=>{S&&S(q)},[q,S]);const re=t.useCallback(()=>q,[q]);return t.useEffect(()=>(window.getColorWheelPalette=re,()=>{delete window.getColorWheelPalette}),[re]),F.jsx("div",{className:"color-wheel-container",children:F.jsx("canvas",{ref:O,width:W,height:L,onMouseDown:Ne,onMouseUp:$e,onMouseLeave:qe,onMouseMove:He,onClick:Ue,style:{display:"block",margin:"0 auto",cursor:"pointer"}})})}exports.ColorWheel=Ae;exports.ThemeProvider=Ye;exports.useTheme=ge;
package/dist/index.mjs ADDED
@@ -0,0 +1,364 @@
1
+ import { jsx as J } from "react/jsx-runtime";
2
+ import { useContext as Ae, useState as o, useEffect as a, createContext as Xe, useRef as _e, useCallback as d, useMemo as qe } from "react";
3
+ const ye = Xe({
4
+ isDark: !1,
5
+ setIsDark: () => {
6
+ }
7
+ });
8
+ function Ke({ children: P }) {
9
+ const [u, b] = o(() => localStorage.getItem("theme") === "dark");
10
+ return a(() => {
11
+ document.documentElement.setAttribute("data-theme", u ? "dark" : "light"), localStorage.setItem("theme", u ? "dark" : "light");
12
+ }, [u]), /* @__PURE__ */ J(ye.Provider, { value: { isDark: u, setIsDark: b }, children: P });
13
+ }
14
+ function Be() {
15
+ return Ae(ye);
16
+ }
17
+ const ge = {
18
+ complementary: [0, 180],
19
+ analogous: [-30, 0, 30],
20
+ monochromatic: [0],
21
+ split: [0, 150, -150],
22
+ triadic: [0, 120, 240],
23
+ square: [0, 90, 180, 270],
24
+ simple: [0]
25
+ }, Fe = [
26
+ {
27
+ name: "Harmony Mode",
28
+ attribute: "harmony-mode",
29
+ default: "split",
30
+ show: !0,
31
+ notes: ""
32
+ },
33
+ {
34
+ name: "Saturation",
35
+ attribute: "saturation",
36
+ default: 100,
37
+ show: !0,
38
+ notes: ""
39
+ },
40
+ {
41
+ name: "Brightness",
42
+ attribute: "brightness",
43
+ default: 50,
44
+ show: !0,
45
+ notes: ""
46
+ },
47
+ {
48
+ name: "Circle Radius",
49
+ attribute: "circle-radius",
50
+ default: 1,
51
+ show: !1,
52
+ notes: "If not passed, default it is false"
53
+ },
54
+ {
55
+ name: "Wheel Thickness",
56
+ attribute: "wheel-thickness",
57
+ default: 80,
58
+ show: !1,
59
+ notes: "If not passed, default it is false"
60
+ },
61
+ {
62
+ name: "Dot Size",
63
+ attribute: "dot-size",
64
+ default: 5,
65
+ show: !1,
66
+ notes: "If not passed, default it is false"
67
+ },
68
+ {
69
+ name: "Dot Position",
70
+ attribute: "dot-position",
71
+ default: "outer",
72
+ show: !1,
73
+ notes: "If not passed, default it is false"
74
+ },
75
+ {
76
+ name: "Handle Thickness",
77
+ attribute: "handle-thickness",
78
+ default: 2,
79
+ show: !1,
80
+ notes: "If not passed, default it is false"
81
+ },
82
+ {
83
+ name: "Handle Color",
84
+ attribute: "handle-color",
85
+ default: "#000000",
86
+ show: !1,
87
+ notes: "If not passed, default it is false"
88
+ },
89
+ {
90
+ name: "Show Harmony Polygon",
91
+ attribute: "harmony-polygon",
92
+ default: !1,
93
+ show: !1,
94
+ notes: "If not passed, default it is false"
95
+ },
96
+ {
97
+ name: "Polygon Color",
98
+ attribute: "polygon-color",
99
+ default: "#000000",
100
+ show: !1,
101
+ notes: "If not passed, default it is false"
102
+ },
103
+ {
104
+ name: "Polygon Thickness",
105
+ attribute: "polygon-thickness",
106
+ default: 1,
107
+ show: !1,
108
+ notes: "If not passed, default it is false"
109
+ },
110
+ {
111
+ name: "Center Dot",
112
+ attribute: "center-dot",
113
+ default: "#ffffff",
114
+ show: !1,
115
+ notes: "If not passed, default it is false"
116
+ },
117
+ {
118
+ name: "Palette",
119
+ attribute: "palette",
120
+ default: null,
121
+ show: !1,
122
+ notes: "If not passed, default it is false"
123
+ },
124
+ {
125
+ name: "Palette JSON",
126
+ attribute: "palette-json",
127
+ default: null,
128
+ show: !1,
129
+ notes: "If not passed, default it is false"
130
+ }
131
+ ];
132
+ function Qe({
133
+ onColorsChange: P,
134
+ harmonyMode: u,
135
+ saturation: b,
136
+ brightness: C,
137
+ circleRadius: w,
138
+ wheelThickness: k,
139
+ dotSize: v,
140
+ dotPosition: R,
141
+ handleThickness: T,
142
+ handleColor: D,
143
+ harmonyPolygon: E,
144
+ polygonColor: L,
145
+ polygonThickness: W,
146
+ centerDot: N,
147
+ showCenterDot: z = !1,
148
+ borderColor: A = "#ffffff",
149
+ canvasBackgroundColor: X = "transparent",
150
+ width: O = 450,
151
+ height: $ = 450
152
+ }) {
153
+ const j = _e(null), [be, _] = o(!1), { isDark: K } = Be(), l = (e) => Fe.find((t) => t.attribute === e), [c, we] = o(
154
+ u || l("harmony-mode").default
155
+ ), [H, ke] = o(
156
+ b ?? l("saturation").default
157
+ ), [U, ve] = o(
158
+ C ?? l("brightness").default
159
+ ), q = d(() => {
160
+ const e = l("circle-radius").default;
161
+ if (w !== void 0) return w;
162
+ if (e === 1) {
163
+ const t = Math.min(O, $), s = k ?? l("wheel-thickness").default, n = v ?? l("dot-size").default;
164
+ return t / 2 - s / 2 - n - 10;
165
+ }
166
+ return e;
167
+ }, [O, $, w, k, v]), [M, Q] = o(q()), [I, Me] = o(
168
+ k ?? l("wheel-thickness").default
169
+ ), [V, Pe] = o(
170
+ v ?? l("dot-size").default
171
+ ), [B, Ie] = o(
172
+ R || l("dot-position").default
173
+ ), [Z, Se] = o(
174
+ T ?? l("handle-thickness").default
175
+ ), [x, Ce] = o(
176
+ D || (l("handle-color").default === "black" ? "#000000" : l("handle-color").default)
177
+ ), [p, Re] = o(
178
+ E ?? l("harmony-polygon").default
179
+ ), [ee, Te] = o(
180
+ L || (l("polygon-color").default === "black" ? "#000000" : l("polygon-color").default)
181
+ ), [te, De] = o(
182
+ W ?? l("polygon-thickness").default
183
+ ), [se, Ee] = o(
184
+ N || (l("center-dot").default === "white" ? "#ffffff" : l("center-dot").default)
185
+ ), [ne, Le] = o(z), [F, We] = o(A), [S, Ne] = o(
186
+ X
187
+ ), [h, ae] = o(0);
188
+ a(() => {
189
+ u !== void 0 && we(u);
190
+ }, [u]), a(() => {
191
+ b !== void 0 && ke(b);
192
+ }, [b]), a(() => {
193
+ C !== void 0 && ve(C);
194
+ }, [C]), a(() => {
195
+ Q(w !== void 0 ? w : q());
196
+ }, [w, q]), a(() => {
197
+ k !== void 0 && Me(k);
198
+ }, [k]), a(() => {
199
+ v !== void 0 && Pe(v);
200
+ }, [v]), a(() => {
201
+ R !== void 0 && Ie(R);
202
+ }, [R]), a(() => {
203
+ T !== void 0 && Se(T);
204
+ }, [T]), a(() => {
205
+ D !== void 0 && Ce(D);
206
+ }, [D]), a(() => {
207
+ E !== void 0 && Re(E);
208
+ }, [E]), a(() => {
209
+ L !== void 0 && Te(L);
210
+ }, [L]), a(() => {
211
+ W !== void 0 && De(W);
212
+ }, [W]), a(() => {
213
+ N !== void 0 && Ee(N);
214
+ }, [N]), a(() => {
215
+ Le(z);
216
+ }, [z]), a(() => {
217
+ We(A);
218
+ }, [A]), a(() => {
219
+ Ne(X);
220
+ }, [X]);
221
+ const i = { x: O / 2, y: $ / 2 }, G = (e) => (e + 360) % 360, oe = d(
222
+ (e, t = H, s = U) => `hsl(${e}, ${t}%, ${s}%)`,
223
+ [H, U]
224
+ ), Oe = (e, t, s) => {
225
+ t /= 100, s /= 100;
226
+ const n = (1 - Math.abs(2 * s - 1)) * t, f = n * (1 - Math.abs(e / 60 % 2 - 1)), r = s - n / 2;
227
+ let m = 0, g = 0, y = 0;
228
+ return e < 60 ? [m, g, y] = [n, f, 0] : e < 120 ? [m, g, y] = [f, n, 0] : e < 180 ? [m, g, y] = [0, n, f] : e < 240 ? [m, g, y] = [0, f, n] : e < 300 ? [m, g, y] = [f, 0, n] : [m, g, y] = [n, 0, f], {
229
+ r: Math.round((m + r) * 255),
230
+ g: Math.round((g + r) * 255),
231
+ b: Math.round((y + r) * 255)
232
+ };
233
+ }, $e = (e, t, s) => "#" + [e, t, s].map((n) => n.toString(16).padStart(2, "0")).join(""), le = d(() => B === "center" ? M : B === "inner" ? M - I / 2 : M + I / 2, [B, M, I]), ie = d(
234
+ (e) => {
235
+ const t = (e - 90) * Math.PI / 180, s = le();
236
+ return {
237
+ x: i.x + s * Math.cos(t),
238
+ y: i.y + s * Math.sin(t)
239
+ };
240
+ },
241
+ [i.x, i.y, le]
242
+ ), fe = (e, t) => {
243
+ const s = e - i.x, n = t - i.y, f = Math.atan2(n, s) * 180 / Math.PI;
244
+ return G(f + 90);
245
+ }, de = d(() => {
246
+ let e = [];
247
+ c === "monochromatic" ? e = [
248
+ { h, l: 30 },
249
+ { h, l: 50 },
250
+ { h, l: 70 }
251
+ ] : e = ge[c].map((s) => ({
252
+ h: G(h + s),
253
+ l: U
254
+ }));
255
+ const t = e.map((s) => {
256
+ const n = Oe(s.h, H, s.l);
257
+ return $e(n.r, n.g, n.b);
258
+ });
259
+ return {
260
+ [c]: t
261
+ };
262
+ }, [c, h, H, U]), ue = d(
263
+ (e) => {
264
+ for (let t = 0; t < 360; t++)
265
+ e.beginPath(), e.strokeStyle = `hsl(${t},100%,50%)`, e.lineWidth = I, e.arc(
266
+ i.x,
267
+ i.y,
268
+ M,
269
+ (t - 90) * Math.PI / 180,
270
+ (t - 89) * Math.PI / 180
271
+ ), e.stroke();
272
+ },
273
+ [i.x, i.y, M, I]
274
+ ), re = d(
275
+ (e, t) => {
276
+ !p || t.length < 3 || (e.beginPath(), e.moveTo(t[0].x, t[0].y), t.slice(1).forEach((s) => {
277
+ e.lineTo(s.x, s.y);
278
+ }), e.closePath(), e.strokeStyle = ee, e.lineWidth = te, e.stroke());
279
+ },
280
+ [p, ee, te]
281
+ ), ce = d(
282
+ (e) => {
283
+ if (c === "simple")
284
+ return;
285
+ const t = ge[c], s = [];
286
+ t.forEach((n) => {
287
+ const f = G(h + n), r = ie(f);
288
+ s.push(r), e.beginPath(), e.moveTo(i.x, i.y), e.lineTo(r.x, r.y), e.strokeStyle = x, e.lineWidth = Z, e.stroke(), e.beginPath(), e.arc(r.x, r.y, V, 0, Math.PI * 2), e.fillStyle = oe(f), e.fill(), e.strokeStyle = "white", e.stroke();
289
+ }), re(e, s), ne && (e.beginPath(), e.arc(i.x, i.y, 5, 0, Math.PI * 2), e.fillStyle = se, e.fill(), e.strokeStyle = "white", e.lineWidth = 1, e.stroke());
290
+ },
291
+ [
292
+ c,
293
+ h,
294
+ i.x,
295
+ i.y,
296
+ V,
297
+ Z,
298
+ x,
299
+ se,
300
+ ne,
301
+ ie,
302
+ oe,
303
+ re
304
+ ]
305
+ ), he = d(() => {
306
+ const e = j.current;
307
+ if (!e) return;
308
+ const t = e.getContext("2d");
309
+ t && (t.clearRect(0, 0, e.width, e.height), S && S !== "transparent" ? (t.fillStyle = S, t.fillRect(0, 0, e.width, e.height)) : S === "transparent" && (t.fillStyle = K ? "#1a1a1a" : "#ffffff", t.fillRect(0, 0, e.width, e.height)), ue(t), ce(t), F && (t.strokeStyle = F, t.lineWidth = 1, t.strokeRect(0, 0, e.width, e.height)));
310
+ }, [
311
+ ue,
312
+ ce,
313
+ F,
314
+ S,
315
+ K
316
+ ]), je = () => _(!0), He = () => _(!1), Ue = () => _(!1), Ye = (e) => {
317
+ if (!be) return;
318
+ const t = j.current;
319
+ if (!t) return;
320
+ const s = t.getBoundingClientRect(), n = fe(e.clientX - s.left, e.clientY - s.top);
321
+ ae(n);
322
+ }, ze = (e) => {
323
+ const t = j.current;
324
+ if (!t) return;
325
+ const s = t.getBoundingClientRect(), n = fe(e.clientX - s.left, e.clientY - s.top);
326
+ ae(n);
327
+ };
328
+ a(() => {
329
+ he();
330
+ }, [he]);
331
+ const Y = qe(() => de(), [de]);
332
+ a(() => {
333
+ P && P(Y);
334
+ }, [Y, P]);
335
+ const me = d(
336
+ () => Y,
337
+ [Y]
338
+ );
339
+ return a(() => (window.getColorWheelPalette = me, () => {
340
+ delete window.getColorWheelPalette;
341
+ }), [me]), /* @__PURE__ */ J("div", { className: "color-wheel-container", children: /* @__PURE__ */ J(
342
+ "canvas",
343
+ {
344
+ ref: j,
345
+ width: O,
346
+ height: $,
347
+ onMouseDown: je,
348
+ onMouseUp: He,
349
+ onMouseLeave: Ue,
350
+ onMouseMove: Ye,
351
+ onClick: ze,
352
+ style: {
353
+ display: "block",
354
+ margin: "0 auto",
355
+ cursor: "pointer"
356
+ }
357
+ }
358
+ ) });
359
+ }
360
+ export {
361
+ Qe as ColorWheel,
362
+ Ke as ThemeProvider,
363
+ Be as useTheme
364
+ };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .color-wheel-container{margin:0;font-family:sans-serif;background-color:#fff}[data-theme=dark] .color-wheel-container{background-color:#1a1a1a!important}.color-wheel-container h2{text-align:center;padding:15px;margin:0;background:#222;color:#fff}.layout{display:grid;grid-template-columns:20% 30% 50%;height:100vh}.panel{padding:15px;overflow-y:auto;border-right:1px solid #ddd;background-color:#fff}[data-theme=dark] .panel{background-color:#1a1a1a!important;border-right-color:#444;color:#e0e0e0}[data-theme=dark] .panel label{color:#e0e0e0}[data-theme=dark] .panel input,[data-theme=dark] .panel select{background-color:#2a2a2a;border:1px solid #444;color:#e0e0e0}[data-theme=dark] .panel hr{border-color:#444}.panel canvas{display:block;margin:auto;cursor:pointer}.panel label{font-weight:700;display:block;margin-top:12px}.panel input,.panel select{width:100%;margin-top:4px}.panel hr{margin:18px 0}.colors{display:flex;flex-wrap:wrap;gap:12px}.swatchBox{text-align:center;font-size:13px}.swatch{width:70px;height:70px;border-radius:10px;border:1px solid #ccc}.panel pre{background:#f3f3f3;padding:10px;border-radius:10px;font-size:12px;overflow-x:auto;max-height:320px}.panel h3{margin-top:0}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@studiotoolkit/color-wheel",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "description": "Interactive color wheel React component for creating and manipulating color harmonies with multiple harmony modes",
6
+ "author": "StudioToolkit",
7
+ "homepage": "https://studiotoolkit.github.io/build-color-wheel",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/studiotoolkit/studiotoolkit",
11
+ "directory": "packages/color-wheel"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/studiotoolkit/studiotoolkit/issues"
15
+ },
16
+ "keywords": [
17
+ "color-wheel",
18
+ "color-picker",
19
+ "color-harmony",
20
+ "harmony-polygon",
21
+ "react-color",
22
+ "color-selection",
23
+ "design-tools",
24
+ "ui-components",
25
+ "complementary",
26
+ "analogous",
27
+ "triadic"
28
+ ],
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.mjs",
31
+ "types": "./dist/index.d.ts",
32
+ "sideEffects": [
33
+ "*.css"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.mjs",
39
+ "require": "./dist/index.js",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./style.css": "./dist/ColorWheel.css",
43
+ "./src/ColorWheel.css": "./src/ColorWheel.css",
44
+ "./package.json": "./package.json"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "engines": {
55
+ "node": ">=16.0.0"
56
+ },
57
+ "peerDependencies": {
58
+ "react": "^18.0.0",
59
+ "react-dom": "^18.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@types/react": "^18.2.0",
63
+ "@types/react-dom": "^18.2.0",
64
+ "@vitejs/plugin-react": "^4.2.0",
65
+ "typescript": "^5.3.3",
66
+ "vite": "^5.0.0"
67
+ },
68
+ "scripts": {
69
+ "build": "npx tsc --project tsconfig.json && vite build",
70
+ "dev": "vite build --watch",
71
+ "clean": "rm -rf dist",
72
+ "lint": "eslint src --ext .ts,.tsx",
73
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
74
+ "test": "vitest run",
75
+ "test:watch": "vitest",
76
+ "test:ui": "vitest --ui"
77
+ }
78
+ }