├── .gitignore
├── LICENSE.md
├── README.md
├── demo
└── demo.html
├── dist
├── index.cjs
├── index.d.ts
├── index.mjs
└── index.umd.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── corners
│ ├── FigmaSquircle.ts
│ ├── Flat.ts
│ ├── Inset.ts
│ ├── Round.ts
│ ├── RoundInverse.ts
│ └── Squircle.ts
├── index.ts
└── utils
│ ├── ElementManager.ts
│ ├── Path.ts
│ └── Svg.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | doc/
3 |
4 | # Ignore Sublime Text files
5 | *.sublime-project
6 | *.sublime-workspace
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2024 Monokai (monokai.com)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the “Software”), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Monoco
2 |
3 | Custom (squircle) corners for html elements.
4 |
5 | - [Demo](https://somonoco.com)
6 | - [React version](https://github.com/monokai/monoco-react)
7 | - [Svelte version](https://github.com/monokai/monoco-svelte)
8 |
9 | ## Installation
10 |
11 | ```sh
12 | npm install @monokai/monoco
13 | ```
14 |
15 | ## Options
16 |
17 | | Option | Type | Description |
18 | | --- | --- | --- |
19 | | background | string | background color |
20 | | smoothing | number | Smoothing factor of the corners (between 0 and 1, default: 1) |
21 | | borderRadius | number \| number[] | Radius of the corners, or array [top left, top right, bottom right, bottom left] |
22 | | offset | number \| number[] | Offset of the corners, or array [top, right, bottom, left] |
23 | | border | [number, string][] | Border of the corners, or array of borders |
24 | | cornerType | {width:number, height:number, radii:number[], offsets:number[]} => (string\|number)[][] | Corner type (default: Squircle) |
25 | | clip | boolean | Use clip-path on element |
26 | | width | number | Width of the element (default: auto) |
27 | | height | number | Height of the element (default: auto) |
28 | | observe | boolean | Observe element for resize (default: true) |
29 | | onResize | (rect:DOMRect, element:HTMLElement) => void | Callback when element resizes |
30 | | precision | number | Precision of the path (default: 3) |
31 | | isRounded | boolean | Use rounded values for width and height (default: false)
32 |
33 | ## Notes
34 |
35 | Monoco generates an SVG representation of the provided options and stores it as a `background-image` on the element. This means that when the element already has a background color, it will show through. Any previously defined background image value will be overwritten.
36 |
37 | If you want to use a background color, you can omit the `background` option in Monoco and set the `clip` option to `true`. This will use `clip-path` instead of `background-image`.
38 |
39 | Using a clip path has some implications. If you want to use a `drop-shadow` css filter for example, you can't use `clip`, as clip paths also clip the shadow.
40 |
41 | Note that `box-shadow` css values are not supported, as it doesn't take the svg shape into account. You have to use something like this instead:
42 |
43 | ```css
44 | .element {
45 | filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.5));
46 | }
47 | ```
48 |
49 | ## Example usage
50 |
51 | ```ts
52 | import {
53 | addCorners,
54 | draw,
55 | unobserve,
56 |
57 | // corner types:
58 | FigmaSquircle,
59 | Flat,
60 | Inset
61 | Round,
62 | RoundInverse,
63 | Squircle
64 | } from '@monokai/monoco'
65 |
66 | const element = document.getElementById('element')
67 |
68 | // use clip
69 | addCorners(element, {
70 | smoothing: 1,
71 | borderRadius: 32,
72 | clip: true
73 | })
74 |
75 | // use background svg
76 | addCorners(element, {
77 | background: '#f00',
78 | smoothing: 1,
79 | borderRadius: 32,
80 | })
81 |
82 | // multi-borderRadius (top left, top right, bottom right, bottom left)
83 | addCorners(element, {
84 | background: '#f00',
85 | smoothing: 1,
86 | borderRadius: [32, 4, 8, 16],
87 | })
88 |
89 | // offset
90 | addCorners(element, {
91 | background: '#f00',
92 | smoothing: 1,
93 | borderRadius: 32,
94 | offset: 16
95 | })
96 |
97 | // multi-offset (top, right, bottom, left)
98 | addCorners(element, {
99 | background: '#f00',
100 | smoothing: 1,
101 | borderRadius: 32,
102 | offset: [16, 4, 2, 8]
103 | })
104 |
105 | // border
106 | addCorners(element, {
107 | background: '#f00',
108 | smoothing: 1,
109 | borderRadius: 32,
110 | border: [4, '#f00']
111 | })
112 |
113 | // multi-border
114 | addCorners(element, {
115 | background: '#f00',
116 | smoothing: 1,
117 | borderRadius: 32,
118 | border: [
119 | [4, '#f00'],
120 | [2, '#0f0'],
121 | [8, '#00f'],
122 | [16, '#ff0']
123 | ]
124 | })
125 |
126 | // corner type
127 | addCorners(element, {
128 | background: '#f00',
129 | smoothing: 1,
130 | borderRadius: 16,
131 | cornerType: RoundInverse
132 | })
133 |
134 | // observe (default: true) redraws when element triggers resize observer, you can optionally turn this off
135 | addCorners(element, {
136 | background: '#f00',
137 | smoothing: 1,
138 | borderRadius: 32,
139 | observe: false
140 | })
141 |
142 | // unobserve element, removes the element from the resize observer
143 | unobserve(element)
144 |
145 | // manual redraw corners on element, options are optional
146 | draw(element, options)
147 |
148 | // manual redraw corners on all elements
149 | draw()
150 | ```
151 |
152 | ## Difference between the default squircle and the Figma squircle.
153 |
154 | The default corner type is `Squircle`. You can also use the `FigmaSquircle` corner type to match the corners as they are calculated in Figma. The difference is subtle, but it has to do with the fact the Figma preserves more of the round shape in certain conditions where the radius is big, whereas `Squircle` preserves more of the smooth squircle shape.
155 |
156 | ## Custom corner types
157 |
158 | You can define your own corner types by providing a function to the `cornerType`. The function receives an object with the following properties:
159 |
160 | ```ts
161 | {
162 | width: number,
163 | height: number,
164 | radii: number[],
165 | offsets: number[]
166 | }
167 | ```
168 |
169 | You can optionally add your own properties to this options object and use them in your custom function. Your function should return an array of SVG path commands, for example:
170 |
171 | ```ts
172 | return [
173 | ['M', 0, 0],
174 | ['L', width, 0],
175 | ['L', width, height],
176 | ['L', 0, height],
177 | ['Z']
178 | ];
179 | ```
180 |
--------------------------------------------------------------------------------
/demo/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | monoco
5 |
6 |
7 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
75 |
76 |
--------------------------------------------------------------------------------
/dist/index.cjs:
--------------------------------------------------------------------------------
1 | "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=new class{elements;observer;constructor(){this.elements=null,this.observer=null}onElementResize(e){for(const t of e){const e=t.target.getBoundingClientRect(),r=this.elements?.get(t.target);if(!r)continue;const{previousW:n,previousH:s,draw:i,onResize:o}=r;n===e.width&&s===e.height||(i?.({width:e.width,height:e.height}),o?.(e,t.target),r.previousW=e.width,r.previousH=e.height)}}getDrawOptions(e){return this.elements?.get(e)?.cornerOptions??null}setCornerOptions(e,t){const r=this.elements?.get(e);r&&(r.cornerOptions=t,this.elements?.set(e,r))}addElement(e,t,r){this.elements||(this.elements=new Map),this.observer||(this.observer=new ResizeObserver((e=>this.onElementResize(e)))),this.unobserve(e);const{observe:n=!0,onResize:s}=t;if(n){this.observer.observe(e);const n=null,i=null;this.elements.set(e,{draw:r,cornerOptions:t,onResize:s,previousW:n,previousH:i,element:e})}return r}draw(e,t){e?(t&&this.setCornerOptions(e,t),this.elements?.get(e)?.draw?.()):this.elements?.forEach((e=>e.draw?.()))}unobserve(e){const t=t=>{this.observer?.unobserve(e),this.elements?.delete(e)};e?t():this.elements?.keys().forEach((e=>t()))}};function t(e,t,r,n,s,i,o,a){return r?[e?["c",...n]:[],s?["a",r,r,0,0,t,...i.map((e=>e*s))]:[],e?["c",...o]:[]]:[["l",...a]]}function r({width:e,height:r,radii:n,offsets:s,smoothing:i=1,preserveSmoothing:o=!0,sweepFlag:a=1}){const[h,,,c]=s,[d,p,l,u]=n.map((t=>function(e,t,r,n){let s=(1+t)*e;r||(t=Math.min(t,n/e-1),s=Math.min(s,n));const i=.5*Math.PI*(1-t),o=Math.sin(i/2)*e*2**.5,a=.25*Math.PI*t,h=e*Math.tan(.25*(.5*Math.PI-i))*Math.cos(a),c=h*Math.tan(a);let d=(s-o-h-c)/3,p=2*d;if(r&&s>n){const e=n-c-o-h,t=e-e/6;d=Math.min(d,t),p=e-d,s=Math.min(s,n)}return{a:p,b:d,c:h,d:c,p:s,arcLength:o,radius:e,ab:p+d,bc:d+h,abc:p+d+h}}(t,i,o,Math.max(t,.5*Math.min(e,r)))));return[["M",e-p.p+c,h],...t(i,a,p.radius,[p.a,0,p.ab,0,p.abc,p.d],p.arcLength,[1,1],[p.d,p.c,p.d,p.bc,p.d,p.abc],[p.p,0]),["L",e+c,r-l.p+h],...t(i,a,l.radius,[0,l.a,0,l.ab,-l.d,l.abc],l.arcLength,[-1,1],[-l.c,l.d,-l.bc,l.d,-l.abc,l.d],[0,l.p]),["L",u.p+c,r+h],...t(i,a,u.radius,[-u.a,0,-u.ab,0,-u.abc,-u.d],u.arcLength,[-1,-1],[-u.d,-u.c,-u.d,-u.bc,-u.d,-u.abc],[-u.p,0]),["L",c,d.p+h],...t(i,a,d.radius,[0,-d.a,0,-d.ab,d.d,-d.abc],d.arcLength,[1,-1],[d.c,-d.d,d.bc,-d.d,d.abc,-d.d],[0,-d.p]),["Z"]]}function n({width:e=0,height:t=0,borderRadius:n=0,offset:s=0,smoothing:i=1,cornerType:o=r,precision:a=3,isArray:h=!1}){if(!e||!t)return h?[]:"";const c=Array.isArray(s)?s:[s,s,s,s],[d,p,l,u]=c,g=e-u-p,m=t-d-l;let f,b;if(Array.isArray(n)){const e=n.map(((e,t)=>e+n[(t+1)%4])),t=Math.min(...e.map(((e,t)=>(t%2==0?g:m)/e)));f=t<1?n.map((e=>e*t)):n}else f=[n,n,n,n].map(((e,t)=>Math.max(0,Math.min(e-c[t],.5*Math.min(g,m)))));return b=o?o({width:g,height:m,radii:f,offsets:c,smoothing:i}):[[]],b=b.filter((e=>e[0])).map((([e,...t])=>{const r=t.map((e=>Number.isFinite(e)?Number(e.toFixed(a)):e)),n=[e,h?r:r.join(" ")];return h?n:n.join("")})),h?b:b.join("")}exports.FigmaSquircle=function(e){return r({...e,preserveSmoothing:!1,sweepFlag:1})},exports.Flat=function({width:e,height:t,radii:r,offsets:n}){const[s,,,i]=n,[o,a,h,c]=r;return[["M",i+o,s],["L",e-a+i,s],["L",e+i,s+a],["L",e+i,t-h+s],["L",e-h+i,t+s],["L",i+c,t+s],["L",i,t-c+s],["L",i,s+o],["Z"]]},exports.Inset=function({width:e,height:t,radii:r,offsets:n}){const[s,,,i]=n,[o,a,h,c]=r;return[["M",i+c,s],["L",e-a+i,s],["L",e-a+i,s+a],["L",e+i,s+a],["L",e+i,t-h+s],["L",e-h+i,t-h+s],["L",e-h+i,t+s],["L",i+c,t+s],["L",i+c,t-c+s],["L",i,t-c+s],["L",i,s+o],["L",i+o,s+o],["L",i+o,s],["Z"]]},exports.Round=function(e){return r({...e,smoothing:0,preserveSmoothing:!1,sweepFlag:1})},exports.RoundInverse=function(e){return r({...e,smoothing:0,preserveSmoothing:!1,sweepFlag:0})},exports.Squircle=r,exports.addCorners=function(t,r){e.setCornerOptions(t,r);const s=r=>{const s=e.getDrawOptions(t)??{};if(!s.width||!s.height){const e=t.getBoundingClientRect();s.width=e.width,s.height=e.height}const i={...s,...r};i.isRounded&&(i.width=i.width?Math.round(i.width):void 0,i.height=i.height?Math.round(i.height):void 0),t.style.clipPath=s.clip?`path('${n(i)}')`:"",(s.background||s.border)&&(t.style.backgroundImage=function(e){const{border:t=[],offset:r=0,strokeDrawType:s=0,background:i,clip:o,clipID:a,width:h,height:c}=e,d=[],p=Array.isArray(t?.[0])?t:[t],l=Array.isArray(r)?r:[r,r,r,r],u=o?null:n(e);if(p?.length){let t=0;const a=[];for(let i=0;i`),t+=o)}i&&(o?d.push(``):d.push(``)),d.push(...a.reverse())}return d.length?((e,t,r="c")=>{return`url('data:image/svg+xml,${(e=>encodeURIComponent(e).replace(/%20/g," ").replace(/%3D/g,"=").replace(/%3A/g,":").replace(/%2F/g,"/").replace(/%2C/g,",").replace(/%3B/g,";"))((n=(t?[``,`${e.join("")}`]:e).join(""),``))}')`;var n})(d,u,a):"none"}(i))};return s(),e.addElement(t,r,s)},exports.createPath=n,exports.draw=function(t,r){e.draw(t,r)},exports.unobserve=function(t){e.unobserve(t)};
2 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | interface RedrawOptions {
2 | width?: number;
3 | height?: number;
4 | }
5 | interface ElementOptions {
6 | observe?: boolean;
7 | onResize?: (rect?: DOMRect, element?: HTMLElement) => void;
8 | }
9 |
10 | interface BackgroundOptions extends PathOptions {
11 | background?: string;
12 | border?: [number, string] | [number, string][];
13 | strokeDrawType?: number;
14 | clipID?: string;
15 | clip?: boolean;
16 | }
17 | interface DefaultCornerTypeOptions {
18 | width: number;
19 | height: number;
20 | radii: number[];
21 | offsets: number[];
22 | }
23 | declare function createPath$6({ width: w, height: h, borderRadius: radiusOrArray, offset: offsetOrArray, smoothing, cornerType, precision, isArray }: PathOptions): string | (string | (string | number | (string | number)[])[])[];
24 | declare function addCorners(element: HTMLElement, cornerOptions: CornerOptions): (redrawOptions?: RedrawOptions) => void;
25 |
26 | declare function createPath$5(options: DefaultCornerTypeOptions & {
27 | smoothing?: number;
28 | }): (string | number)[][];
29 |
30 | declare function createPath$4({ width, height, radii, offsets }: {
31 | width: number;
32 | height: number;
33 | radii: number[];
34 | offsets: number[];
35 | }): (string | number)[][];
36 |
37 | declare function createPath$3({ width, height, radii, offsets }: {
38 | width: number;
39 | height: number;
40 | radii: number[];
41 | offsets: number[];
42 | }): (string | number)[][];
43 |
44 | declare function createPath$2(options: DefaultCornerTypeOptions): (string | number)[][];
45 |
46 | declare function createPath$1(options: DefaultCornerTypeOptions): (string | number)[][];
47 |
48 | declare function createPath({ width, height, radii, offsets, smoothing, preserveSmoothing, sweepFlag }: {
49 | width: number;
50 | height: number;
51 | radii: number[];
52 | offsets: number[];
53 | smoothing?: number;
54 | preserveSmoothing?: boolean;
55 | sweepFlag?: number;
56 | }): (string | number)[][];
57 |
58 | interface CornerOptions extends BackgroundOptions, ElementOptions {
59 | }
60 | interface CornerTypeOptions extends DefaultCornerTypeOptions {
61 | [_: string | number | symbol]: unknown;
62 | }
63 | interface DrawOptions {
64 | width?: number;
65 | height?: number;
66 | smoothing?: number;
67 | borderRadius?: number | number[];
68 | offset?: number | number[];
69 | cornerType?(options: CornerTypeOptions): (string | number)[][];
70 | precision?: number;
71 | isRounded?: boolean;
72 | }
73 | interface PathOptions extends DrawOptions {
74 | isArray?: boolean;
75 | }
76 |
77 | declare function draw(element?: HTMLElement, options?: CornerOptions): void;
78 | declare function unobserve(element: HTMLElement): void;
79 |
80 | export { type CornerOptions, type CornerTypeOptions, createPath$5 as FigmaSquircle, createPath$4 as Flat, createPath$3 as Inset, type PathOptions, createPath$2 as Round, createPath$1 as RoundInverse, createPath as Squircle, addCorners, createPath$6 as createPath, draw, unobserve };
81 |
--------------------------------------------------------------------------------
/dist/index.mjs:
--------------------------------------------------------------------------------
1 | var e=new class{elements;observer;constructor(){this.elements=null,this.observer=null}onElementResize(e){for(const t of e){const e=t.target.getBoundingClientRect(),n=this.elements?.get(t.target);if(!n)continue;const{previousW:i,previousH:r,draw:s,onResize:o}=n;i===e.width&&r===e.height||(s?.({width:e.width,height:e.height}),o?.(e,t.target),n.previousW=e.width,n.previousH=e.height)}}getDrawOptions(e){return this.elements?.get(e)?.cornerOptions??null}setCornerOptions(e,t){const n=this.elements?.get(e);n&&(n.cornerOptions=t,this.elements?.set(e,n))}addElement(e,t,n){this.elements||(this.elements=new Map),this.observer||(this.observer=new ResizeObserver((e=>this.onElementResize(e)))),this.unobserve(e);const{observe:i=!0,onResize:r}=t;if(i){this.observer.observe(e);const i=null,s=null;this.elements.set(e,{draw:n,cornerOptions:t,onResize:r,previousW:i,previousH:s,element:e})}return n}draw(e,t){e?(t&&this.setCornerOptions(e,t),this.elements?.get(e)?.draw?.()):this.elements?.forEach((e=>e.draw?.()))}unobserve(e){const t=t=>{this.observer?.unobserve(e),this.elements?.delete(e)};e?t():this.elements?.keys().forEach((e=>t()))}};function t(e,t,n,i,r,s,o,a){return n?[e?["c",...i]:[],r?["a",n,n,0,0,t,...s.map((e=>e*r))]:[],e?["c",...o]:[]]:[["l",...a]]}function n({width:e,height:n,radii:i,offsets:r,smoothing:s=1,preserveSmoothing:o=!0,sweepFlag:a=1}){const[h,,,c]=r,[d,l,u,g]=i.map((t=>function(e,t,n,i){let r=(1+t)*e;n||(t=Math.min(t,i/e-1),r=Math.min(r,i));const s=.5*Math.PI*(1-t),o=Math.sin(s/2)*e*2**.5,a=.25*Math.PI*t,h=e*Math.tan(.25*(.5*Math.PI-s))*Math.cos(a),c=h*Math.tan(a);let d=(r-o-h-c)/3,l=2*d;if(n&&r>i){const e=i-c-o-h,t=e-e/6;d=Math.min(d,t),l=e-d,r=Math.min(r,i)}return{a:l,b:d,c:h,d:c,p:r,arcLength:o,radius:e,ab:l+d,bc:d+h,abc:l+d+h}}(t,s,o,Math.max(t,.5*Math.min(e,n)))));return[["M",e-l.p+c,h],...t(s,a,l.radius,[l.a,0,l.ab,0,l.abc,l.d],l.arcLength,[1,1],[l.d,l.c,l.d,l.bc,l.d,l.abc],[l.p,0]),["L",e+c,n-u.p+h],...t(s,a,u.radius,[0,u.a,0,u.ab,-u.d,u.abc],u.arcLength,[-1,1],[-u.c,u.d,-u.bc,u.d,-u.abc,u.d],[0,u.p]),["L",g.p+c,n+h],...t(s,a,g.radius,[-g.a,0,-g.ab,0,-g.abc,-g.d],g.arcLength,[-1,-1],[-g.d,-g.c,-g.d,-g.bc,-g.d,-g.abc],[-g.p,0]),["L",c,d.p+h],...t(s,a,d.radius,[0,-d.a,0,-d.ab,d.d,-d.abc],d.arcLength,[1,-1],[d.c,-d.d,d.bc,-d.d,d.abc,-d.d],[0,-d.p]),["Z"]]}function i(e){return n({...e,preserveSmoothing:!1,sweepFlag:1})}function r({width:e,height:t,radii:n,offsets:i}){const[r,,,s]=i,[o,a,h,c]=n;return[["M",s+o,r],["L",e-a+s,r],["L",e+s,r+a],["L",e+s,t-h+r],["L",e-h+s,t+r],["L",s+c,t+r],["L",s,t-c+r],["L",s,r+o],["Z"]]}function s({width:e,height:t,radii:n,offsets:i}){const[r,,,s]=i,[o,a,h,c]=n;return[["M",s+c,r],["L",e-a+s,r],["L",e-a+s,r+a],["L",e+s,r+a],["L",e+s,t-h+r],["L",e-h+s,t-h+r],["L",e-h+s,t+r],["L",s+c,t+r],["L",s+c,t-c+r],["L",s,t-c+r],["L",s,r+o],["L",s+o,r+o],["L",s+o,r],["Z"]]}function o(e){return n({...e,smoothing:0,preserveSmoothing:!1,sweepFlag:1})}function a(e){return n({...e,smoothing:0,preserveSmoothing:!1,sweepFlag:0})}function h({width:e=0,height:t=0,borderRadius:i=0,offset:r=0,smoothing:s=1,cornerType:o=n,precision:a=3,isArray:h=!1}){if(!e||!t)return h?[]:"";const c=Array.isArray(r)?r:[r,r,r,r],[d,l,u,g]=c,p=e-g-l,m=t-d-u;let f,b;if(Array.isArray(i)){const e=i.map(((e,t)=>e+i[(t+1)%4])),t=Math.min(...e.map(((e,t)=>(t%2==0?p:m)/e)));f=t<1?i.map((e=>e*t)):i}else f=[i,i,i,i].map(((e,t)=>Math.max(0,Math.min(e-c[t],.5*Math.min(p,m)))));return b=o?o({width:p,height:m,radii:f,offsets:c,smoothing:s}):[[]],b=b.filter((e=>e[0])).map((([e,...t])=>{const n=t.map((e=>Number.isFinite(e)?Number(e.toFixed(a)):e)),i=[e,h?n:n.join(" ")];return h?i:i.join("")})),h?b:b.join("")}function c(t,n){e.setCornerOptions(t,n);const i=n=>{const i=e.getDrawOptions(t)??{};if(!i.width||!i.height){const e=t.getBoundingClientRect();i.width=e.width,i.height=e.height}const r={...i,...n};r.isRounded&&(r.width=r.width?Math.round(r.width):void 0,r.height=r.height?Math.round(r.height):void 0),t.style.clipPath=i.clip?`path('${h(r)}')`:"",(i.background||i.border)&&(t.style.backgroundImage=function(e){const{border:t=[],offset:n=0,strokeDrawType:i=0,background:r,clip:s,clipID:o,width:a,height:c}=e,d=[],l=Array.isArray(t?.[0])?t:[t],u=Array.isArray(n)?n:[n,n,n,n],g=s?null:h(e);if(l?.length){let t=0;const o=[];for(let r=0;r`),t+=s)}r&&(s?d.push(``):d.push(``)),d.push(...o.reverse())}return d.length?((e,t,n="c")=>{return`url('data:image/svg+xml,${(e=>encodeURIComponent(e).replace(/%20/g," ").replace(/%3D/g,"=").replace(/%3A/g,":").replace(/%2F/g,"/").replace(/%2C/g,",").replace(/%3B/g,";"))((i=(t?[``,`${e.join("")}`]:e).join(""),``))}')`;var i})(d,g,o):"none"}(r))};return i(),e.addElement(t,n,i)}function d(t,n){e.draw(t,n)}function l(t){e.unobserve(t)}export{i as FigmaSquircle,r as Flat,s as Inset,o as Round,a as RoundInverse,n as Squircle,c as addCorners,h as createPath,d as draw,l as unobserve};
2 |
--------------------------------------------------------------------------------
/dist/index.umd.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).monoco={})}(this,(function(e){"use strict";var t=new class{elements;observer;constructor(){this.elements=null,this.observer=null}onElementResize(e){for(const t of e){const e=t.target.getBoundingClientRect(),n=this.elements?.get(t.target);if(!n)continue;const{previousW:i,previousH:r,draw:s,onResize:o}=n;i===e.width&&r===e.height||(s?.({width:e.width,height:e.height}),o?.(e,t.target),n.previousW=e.width,n.previousH=e.height)}}getDrawOptions(e){return this.elements?.get(e)?.cornerOptions??null}setCornerOptions(e,t){const n=this.elements?.get(e);n&&(n.cornerOptions=t,this.elements?.set(e,n))}addElement(e,t,n){this.elements||(this.elements=new Map),this.observer||(this.observer=new ResizeObserver((e=>this.onElementResize(e)))),this.unobserve(e);const{observe:i=!0,onResize:r}=t;if(i){this.observer.observe(e);const i=null,s=null;this.elements.set(e,{draw:n,cornerOptions:t,onResize:r,previousW:i,previousH:s,element:e})}return n}draw(e,t){e?(t&&this.setCornerOptions(e,t),this.elements?.get(e)?.draw?.()):this.elements?.forEach((e=>e.draw?.()))}unobserve(e){const t=t=>{this.observer?.unobserve(e),this.elements?.delete(e)};e?t():this.elements?.keys().forEach((e=>t()))}};function n(e,t,n,i,r,s,o,a){return n?[e?["c",...i]:[],r?["a",n,n,0,0,t,...s.map((e=>e*r))]:[],e?["c",...o]:[]]:[["l",...a]]}function i({width:e,height:t,radii:i,offsets:r,smoothing:s=1,preserveSmoothing:o=!0,sweepFlag:a=1}){const[h,,,c]=r,[d,l,u,p]=i.map((n=>function(e,t,n,i){let r=(1+t)*e;n||(t=Math.min(t,i/e-1),r=Math.min(r,i));const s=.5*Math.PI*(1-t),o=Math.sin(s/2)*e*2**.5,a=.25*Math.PI*t,h=e*Math.tan(.25*(.5*Math.PI-s))*Math.cos(a),c=h*Math.tan(a);let d=(r-o-h-c)/3,l=2*d;if(n&&r>i){const e=i-c-o-h,t=e-e/6;d=Math.min(d,t),l=e-d,r=Math.min(r,i)}return{a:l,b:d,c:h,d:c,p:r,arcLength:o,radius:e,ab:l+d,bc:d+h,abc:l+d+h}}(n,s,o,Math.max(n,.5*Math.min(e,t)))));return[["M",e-l.p+c,h],...n(s,a,l.radius,[l.a,0,l.ab,0,l.abc,l.d],l.arcLength,[1,1],[l.d,l.c,l.d,l.bc,l.d,l.abc],[l.p,0]),["L",e+c,t-u.p+h],...n(s,a,u.radius,[0,u.a,0,u.ab,-u.d,u.abc],u.arcLength,[-1,1],[-u.c,u.d,-u.bc,u.d,-u.abc,u.d],[0,u.p]),["L",p.p+c,t+h],...n(s,a,p.radius,[-p.a,0,-p.ab,0,-p.abc,-p.d],p.arcLength,[-1,-1],[-p.d,-p.c,-p.d,-p.bc,-p.d,-p.abc],[-p.p,0]),["L",c,d.p+h],...n(s,a,d.radius,[0,-d.a,0,-d.ab,d.d,-d.abc],d.arcLength,[1,-1],[d.c,-d.d,d.bc,-d.d,d.abc,-d.d],[0,-d.p]),["Z"]]}function r({width:e=0,height:t=0,borderRadius:n=0,offset:r=0,smoothing:s=1,cornerType:o=i,precision:a=3,isArray:h=!1}){if(!e||!t)return h?[]:"";const c=Array.isArray(r)?r:[r,r,r,r],[d,l,u,p]=c,g=e-p-l,f=t-d-u;let m,b;if(Array.isArray(n)){const e=n.map(((e,t)=>e+n[(t+1)%4])),t=Math.min(...e.map(((e,t)=>(t%2==0?g:f)/e)));m=t<1?n.map((e=>e*t)):n}else m=[n,n,n,n].map(((e,t)=>Math.max(0,Math.min(e-c[t],.5*Math.min(g,f)))));return b=o?o({width:g,height:f,radii:m,offsets:c,smoothing:s}):[[]],b=b.filter((e=>e[0])).map((([e,...t])=>{const n=t.map((e=>Number.isFinite(e)?Number(e.toFixed(a)):e)),i=[e,h?n:n.join(" ")];return h?i:i.join("")})),h?b:b.join("")}e.FigmaSquircle=function(e){return i({...e,preserveSmoothing:!1,sweepFlag:1})},e.Flat=function({width:e,height:t,radii:n,offsets:i}){const[r,,,s]=i,[o,a,h,c]=n;return[["M",s+o,r],["L",e-a+s,r],["L",e+s,r+a],["L",e+s,t-h+r],["L",e-h+s,t+r],["L",s+c,t+r],["L",s,t-c+r],["L",s,r+o],["Z"]]},e.Inset=function({width:e,height:t,radii:n,offsets:i}){const[r,,,s]=i,[o,a,h,c]=n;return[["M",s+c,r],["L",e-a+s,r],["L",e-a+s,r+a],["L",e+s,r+a],["L",e+s,t-h+r],["L",e-h+s,t-h+r],["L",e-h+s,t+r],["L",s+c,t+r],["L",s+c,t-c+r],["L",s,t-c+r],["L",s,r+o],["L",s+o,r+o],["L",s+o,r],["Z"]]},e.Round=function(e){return i({...e,smoothing:0,preserveSmoothing:!1,sweepFlag:1})},e.RoundInverse=function(e){return i({...e,smoothing:0,preserveSmoothing:!1,sweepFlag:0})},e.Squircle=i,e.addCorners=function(e,n){t.setCornerOptions(e,n);const i=n=>{const i=t.getDrawOptions(e)??{};if(!i.width||!i.height){const t=e.getBoundingClientRect();i.width=t.width,i.height=t.height}const s={...i,...n};s.isRounded&&(s.width=s.width?Math.round(s.width):void 0,s.height=s.height?Math.round(s.height):void 0),e.style.clipPath=i.clip?`path('${r(s)}')`:"",(i.background||i.border)&&(e.style.backgroundImage=function(e){const{border:t=[],offset:n=0,strokeDrawType:i=0,background:s,clip:o,clipID:a,width:h,height:c}=e,d=[],l=Array.isArray(t?.[0])?t:[t],u=Array.isArray(n)?n:[n,n,n,n],p=o?null:r(e);if(l?.length){let t=0;const a=[];for(let s=0;s`),t+=o)}s&&(o?d.push(``):d.push(``)),d.push(...a.reverse())}return d.length?((e,t,n="c")=>{return`url('data:image/svg+xml,${(e=>encodeURIComponent(e).replace(/%20/g," ").replace(/%3D/g,"=").replace(/%3A/g,":").replace(/%2F/g,"/").replace(/%2C/g,",").replace(/%3B/g,";"))((i=(t?[``,`${e.join("")}`]:e).join(""),``))}')`;var i})(d,p,a):"none"}(s))};return i(),t.addElement(e,n,i)},e.createPath=r,e.draw=function(e,n){t.draw(e,n)},e.unobserve=function(e){t.unobserve(e)},Object.defineProperty(e,"__esModule",{value:!0})}));
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@monokai/monoco",
3 | "version": "0.2.2",
4 | "author": "Monokai",
5 | "license": "MIT",
6 | "description": "Custom (squircle) corners and borders for html elements",
7 | "keywords": [
8 | "corner",
9 | "border",
10 | "squircle",
11 | "svg",
12 | "html"
13 | ],
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/monokai/monoco.git"
17 | },
18 | "type": "module",
19 | "sideEffects": false,
20 | "types": "./dist/index.d.ts",
21 | "main": "./dist/index.umd.js",
22 | "module": "./dist/index.mjs",
23 | "exports": {
24 | ".": {
25 | "types": "./dist/index.d.ts",
26 | "import": "./dist/index.mjs",
27 | "require": "./dist/index.cjs"
28 | },
29 | "./source": "./src/index.ts"
30 | },
31 | "files": [
32 | "./dist",
33 | "./src"
34 | ],
35 | "scripts": {
36 | "build": "rollup -c",
37 | "build:production": "rollup -c --environment BUILD:production",
38 | "watch": "rollup src/index.ts --format umd --name monoco --watch --output.file=dist/umd/index.js"
39 | },
40 | "devDependencies": {
41 | "@clarifynl/eslint-config-clarify": "^0.14.0",
42 | "@rollup/plugin-commonjs": "^28.0.2",
43 | "@rollup/plugin-node-resolve": "^16.0.0",
44 | "@rollup/plugin-terser": "^0.4.4",
45 | "@rollup/plugin-typescript": "^12.1.2",
46 | "rollup": "^4.28.1",
47 | "rollup-plugin-delete": "^2.0.0",
48 | "rollup-plugin-dts": "^6.1.1",
49 | "tslib": "^2.8.1",
50 | "typescript": "^5.7.2"
51 | },
52 | "eslintConfig": {
53 | "extends": [
54 | "@clarifynl/clarify"
55 | ],
56 | "rules": {
57 | "import/extensions": 0
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import { dts } from "rollup-plugin-dts";
4 | import terser from '@rollup/plugin-terser';
5 | import del from 'rollup-plugin-delete';
6 |
7 | const dist = './dist';
8 | const name = 'monoco';
9 |
10 | const plugins = [
11 | commonjs(),
12 | typescript(),
13 | terser({
14 | compress: {
15 | drop_console: true,
16 | passes: 2
17 | }
18 | })
19 | ]
20 |
21 | export default [
22 | {
23 | input: './src/index.ts',
24 | output: [
25 | {
26 | file: `${dist}/index.cjs`,
27 | format: 'cjs',
28 | esModule: true
29 | },
30 | {
31 | format: 'esm',
32 | file: `${dist}/index.mjs`
33 | },
34 | {
35 | file: `${dist}/index.umd.js`,
36 | format: 'umd',
37 | esModule: true,
38 | name
39 | }
40 | ],
41 | plugins: [
42 | ...plugins,
43 | del({
44 | targets: `${dist}/*`
45 | })
46 | ]
47 | },
48 | {
49 | input: [
50 | 'src/index.ts'
51 | ],
52 | output: [
53 | {
54 | dir: `${dist}`,
55 | format: 'esm'
56 | }
57 | ],
58 | plugins: [
59 | typescript(),
60 | dts()
61 | ]
62 | }
63 | ];
--------------------------------------------------------------------------------
/src/corners/FigmaSquircle.ts:
--------------------------------------------------------------------------------
1 | import { DefaultCornerTypeOptions } from '../utils/Path'
2 | import { createPath as createSquirclePath } from './Squircle'
3 |
4 | export function createPath(options:DefaultCornerTypeOptions & {
5 | smoothing?:number
6 | }) {
7 | return createSquirclePath({
8 | ...options,
9 | preserveSmoothing: false,
10 | sweepFlag: 1
11 | })
12 | }
--------------------------------------------------------------------------------
/src/corners/Flat.ts:
--------------------------------------------------------------------------------
1 | export function createPath({
2 | width,
3 | height,
4 | radii,
5 | offsets
6 | }:{
7 | width:number,
8 | height:number,
9 | radii:number[],
10 | offsets:number[]
11 | }) {
12 | const [ot,,, ol] = offsets
13 | const [rtl, rtr, rbr, rbl] = radii
14 |
15 | return [
16 | ['M', ol + rtl, ot],
17 | ['L', width - rtr + ol, ot],
18 | ['L', width + ol, ot + rtr],
19 | ['L', width + ol, height - rbr + ot],
20 | ['L', width - rbr + ol, height + ot],
21 | ['L', ol + rbl, height + ot],
22 | ['L', ol, height - rbl + ot],
23 | ['L', ol, ot + rtl],
24 | ['Z']
25 | ]
26 | }
--------------------------------------------------------------------------------
/src/corners/Inset.ts:
--------------------------------------------------------------------------------
1 | export function createPath({
2 | width,
3 | height,
4 | radii,
5 | offsets
6 | }:{
7 | width:number,
8 | height:number,
9 | radii:number[],
10 | offsets:number[]
11 | }) {
12 | const [ot,,, ol] = offsets
13 | const [rtl, rtr, rbr, rbl] = radii
14 |
15 | return [
16 | ['M', ol + rbl, ot],
17 | ['L', width - rtr + ol, ot],
18 | ['L', width - rtr + ol, ot + rtr],
19 | ['L', width + ol, ot + rtr],
20 | ['L', width + ol, height - rbr + ot],
21 | ['L', width - rbr + ol, height - rbr + ot],
22 | ['L', width - rbr + ol, height + ot],
23 | ['L', ol + rbl, height + ot],
24 | ['L', ol + rbl, height - rbl + ot],
25 | ['L', ol, height - rbl + ot],
26 | ['L', ol, ot + rtl],
27 | ['L', ol + rtl, ot + rtl],
28 | ['L', ol + rtl, ot],
29 | ['Z']
30 | ]
31 | }
--------------------------------------------------------------------------------
/src/corners/Round.ts:
--------------------------------------------------------------------------------
1 | import { DefaultCornerTypeOptions } from '../utils/Path'
2 | import { createPath as createSquirclePath } from './Squircle'
3 |
4 | export function createPath(options:DefaultCornerTypeOptions) {
5 | return createSquirclePath({
6 | ...options,
7 | smoothing: 0,
8 | preserveSmoothing: false,
9 | sweepFlag: 1
10 | })
11 | }
--------------------------------------------------------------------------------
/src/corners/RoundInverse.ts:
--------------------------------------------------------------------------------
1 | import { DefaultCornerTypeOptions } from '../utils/Path'
2 | import { createPath as createSquirclePath } from './Squircle'
3 |
4 | export function createPath(options:DefaultCornerTypeOptions) {
5 | return createSquirclePath({
6 | ...options,
7 | smoothing: 0,
8 | preserveSmoothing: false,
9 | sweepFlag: 0
10 | })
11 | }
--------------------------------------------------------------------------------
/src/corners/Squircle.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Adapted from "figma-squircle" (https://github.com/phamfoo/figma-squircle),
3 | refactored and optimized by Monokai
4 |
5 | ---
6 |
7 | MIT License
8 |
9 | Copyright (c) 2021 Tien Pham
10 |
11 | Permission is hereby granted, free of charge, to any person obtaining a copy
12 | of this software and associated documentation files (the "Software"), to deal
13 | in the Software without restriction, including without limitation the rights
14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 | copies of the Software, and to permit persons to whom the Software is
16 | furnished to do so, subject to the following conditions:
17 |
18 | The above copyright notice and this permission notice shall be included in all
19 | copies or substantial portions of the Software.
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 | SOFTWARE.
28 | */
29 |
30 | function getSquircleCorner(
31 | radius:number,
32 | smoothing:number,
33 | preserveSmoothing:boolean,
34 | maxSize:number
35 | ) {
36 | let p = (1 + smoothing) * radius
37 |
38 | if (!preserveSmoothing) {
39 | smoothing = Math.min(smoothing, maxSize / radius - 1)
40 | p = Math.min(p, maxSize)
41 | }
42 |
43 | const arc = Math.PI * 0.5 * (1 - smoothing)
44 | const arcLength = Math.sin(arc / 2) * radius * 2 ** 0.5
45 | const angle = Math.PI * 0.25 * smoothing
46 | const c = radius * Math.tan((Math.PI * 0.5 - arc) * 0.25) * Math.cos(angle)
47 | const d = c * Math.tan(angle)
48 |
49 | let b = (p - arcLength - c - d) / 3
50 | let a = 2 * b
51 |
52 | if (preserveSmoothing && p > maxSize) {
53 | const pDist2 = maxSize - d - arcLength - c
54 | const maxB = pDist2 - pDist2 / 6
55 |
56 | b = Math.min(b, maxB)
57 | a = pDist2 - b
58 | p = Math.min(p, maxSize)
59 | }
60 |
61 | return {
62 | a,
63 | b,
64 | c,
65 | d,
66 | p,
67 | arcLength,
68 | radius,
69 | ab: a + b,
70 | bc: b + c,
71 | abc: a + b + c
72 | }
73 | }
74 |
75 |
76 | function createSquircleCorner(
77 | smoothing:number,
78 | sweepFlag:number,
79 | radius:number,
80 | c1:number[],
81 | arcLength:number,
82 | arcMultiplier:number[],
83 | c2:number[],
84 | l:number[]
85 | ) {
86 | if (radius) {
87 | return [
88 | ...[smoothing ? ['c', ...c1] : []],
89 | ...[arcLength ? ['a', radius, radius, 0, 0, sweepFlag, ...arcMultiplier.map(x => x * arcLength)] : []],
90 | ...[smoothing ? ['c', ...c2] : []],
91 | ]
92 | }
93 |
94 | return [['l', ...l]]
95 | }
96 |
97 | export function createPath({
98 | width,
99 | height,
100 | radii,
101 | offsets,
102 | smoothing = 1,
103 | preserveSmoothing = true,
104 | sweepFlag = 1
105 | }:{
106 | width:number,
107 | height:number,
108 | radii:number[],
109 | offsets:number[],
110 | smoothing?:number,
111 | preserveSmoothing?:boolean,
112 | sweepFlag?:number
113 | }) {
114 | const [ot,,, ol] = offsets
115 | const [c1, c2, c3, c4] = radii.map(radius => getSquircleCorner(
116 | radius,
117 | smoothing,
118 | preserveSmoothing,
119 | Math.max(radius, Math.min(width, height) * 0.5)
120 | ))
121 |
122 | return [
123 | ['M', width - c2.p + ol, ot],
124 | ...createSquircleCorner(
125 | smoothing,
126 | sweepFlag,
127 | c2.radius,
128 | [c2.a, 0, c2.ab, 0, c2.abc, c2.d],
129 | c2.arcLength, [1, 1],
130 | [c2.d, c2.c, c2.d, c2.bc, c2.d, c2.abc],
131 | [c2.p, 0]
132 | ),
133 | ['L', width + ol, height - c3.p + ot],
134 | ...createSquircleCorner(
135 | smoothing,
136 | sweepFlag,
137 | c3.radius,
138 | [0, c3.a, 0, c3.ab, -c3.d, c3.abc],
139 | c3.arcLength, [-1, 1],
140 | [-c3.c, c3.d, -c3.bc, c3.d, -c3.abc, c3.d],
141 | [0, c3.p]
142 | ),
143 | ['L', c4.p + ol, height + ot],
144 | ...createSquircleCorner(
145 | smoothing,
146 | sweepFlag,
147 | c4.radius,
148 | [-c4.a, 0, -c4.ab, 0, -c4.abc, -c4.d],
149 | c4.arcLength, [-1, -1],
150 | [-c4.d, -c4.c, -c4.d, -c4.bc, -c4.d, -c4.abc],
151 | [-c4.p, 0]
152 | ),
153 | ['L', ol, c1.p + ot],
154 | ...createSquircleCorner(
155 | smoothing,
156 | sweepFlag,
157 | c1.radius,
158 | [0, -c1.a, 0, -c1.ab, c1.d, -c1.abc],
159 | c1.arcLength, [1, -1],
160 | [c1.c, -c1.d, c1.bc, -c1.d, c1.abc, -c1.d],
161 | [0, -c1.p]
162 | ),
163 | ['Z']
164 | ]
165 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { BackgroundOptions, DefaultCornerTypeOptions } from "./utils/Path"
2 | import type { ElementOptions } from "./utils/ElementManager"
3 |
4 | import ElementManager from './utils/ElementManager'
5 |
6 | export { createPath as FigmaSquircle } from './corners/FigmaSquircle'
7 | export { createPath as Flat } from './corners/Flat'
8 | export { createPath as Inset } from './corners/Inset'
9 | export { createPath as Round } from './corners/Round'
10 | export { createPath as RoundInverse } from './corners/RoundInverse'
11 | export { createPath as Squircle } from './corners/Squircle'
12 |
13 | export interface CornerOptions extends BackgroundOptions, ElementOptions {}
14 |
15 | export interface CornerTypeOptions extends DefaultCornerTypeOptions {
16 | // allow any key / value combinations in addition to the default corner type options
17 | [_: string | number | symbol]: unknown;
18 | }
19 |
20 | interface DrawOptions {
21 | width?:number,
22 | height?:number,
23 | smoothing?:number,
24 | borderRadius?:number | number[],
25 | offset?:number | number[],
26 | cornerType?(options:CornerTypeOptions):(string | number)[][],
27 | precision?:number,
28 | isRounded?:boolean
29 | }
30 |
31 | export interface PathOptions extends DrawOptions {
32 | isArray?:boolean
33 | }
34 |
35 | export {
36 | createPath,
37 | addCorners
38 | } from './utils/Path'
39 |
40 | export function draw(element?:HTMLElement, options?:CornerOptions) {
41 | ElementManager.draw(element, options)
42 | }
43 |
44 | export function unobserve(element:HTMLElement) {
45 | ElementManager.unobserve(element)
46 | }
--------------------------------------------------------------------------------
/src/utils/ElementManager.ts:
--------------------------------------------------------------------------------
1 | import { CornerOptions } from ".."
2 |
3 | export interface RedrawOptions {
4 | width?:number,
5 | height?:number
6 | }
7 |
8 | interface ElementSpecs {
9 | draw: (redrawOptions?:RedrawOptions) => void,
10 | cornerOptions: CornerOptions,
11 | onResize?: (rect?:DOMRect, element?:HTMLElement) => void,
12 | previousW: number | null,
13 | previousH: number | null,
14 | element: HTMLElement
15 | }
16 |
17 | export interface ElementOptions {
18 | observe?:boolean,
19 | onResize?:(rect?:DOMRect, element?:HTMLElement) => void
20 | }
21 |
22 | export default new class ElementManager {
23 | elements:Map | null
24 | observer:ResizeObserver | null
25 |
26 | constructor() {
27 | this.elements = null
28 | this.observer = null
29 | }
30 |
31 | onElementResize(resizeList:ResizeObserverEntry[]) {
32 | for (const entry of resizeList) {
33 | const rect = entry.target.getBoundingClientRect()
34 | const specs = this.elements?.get(entry.target as HTMLElement)
35 |
36 | if (!specs) {
37 | continue
38 | }
39 |
40 | const {
41 | previousW,
42 | previousH,
43 | draw,
44 | onResize
45 | } = specs
46 |
47 | if (
48 | previousW !== rect.width ||
49 | previousH !== rect.height
50 | ) {
51 | draw?.({
52 | width: rect.width,
53 | height: rect.height
54 | })
55 |
56 | onResize?.(rect, entry.target as HTMLElement)
57 |
58 | specs.previousW = rect.width
59 | specs.previousH = rect.height
60 | }
61 | }
62 | }
63 |
64 | getDrawOptions(element:HTMLElement):CornerOptions | null {
65 | return this.elements?.get(element)?.cornerOptions ?? null;
66 | }
67 |
68 | setCornerOptions(element:HTMLElement, cornerOptions:CornerOptions) {
69 | const specs = this.elements?.get(element);
70 |
71 | if (specs) {
72 | specs.cornerOptions = cornerOptions;
73 |
74 | this.elements?.set(element, specs);
75 | }
76 | }
77 |
78 | addElement(element:HTMLElement, cornerOptions:CornerOptions & ElementOptions, draw:(redrawOptions?:RedrawOptions) => void) {
79 | if (!this.elements) {
80 | this.elements = new Map()
81 | }
82 |
83 | if (!this.observer) {
84 | this.observer = new ResizeObserver(resizeList => this.onElementResize(resizeList))
85 | }
86 |
87 | this.unobserve(element)
88 |
89 | const {
90 | observe = true,
91 | onResize
92 | } = cornerOptions
93 |
94 | if (observe) {
95 | this.observer.observe(element)
96 |
97 | const previousW = null
98 | const previousH = null
99 |
100 | this.elements.set(element, {
101 | draw,
102 | cornerOptions,
103 | onResize,
104 | previousW,
105 | previousH,
106 | element
107 | })
108 | }
109 |
110 | return draw
111 | }
112 |
113 | draw(element?:HTMLElement, cornerOptions?:CornerOptions) {
114 | if (element) {
115 | if (cornerOptions) {
116 | this.setCornerOptions(element, cornerOptions)
117 | }
118 |
119 | this.elements?.get(element)?.draw?.()
120 | } else {
121 | this.elements?.forEach((o:ElementSpecs) => o.draw?.())
122 | }
123 | }
124 |
125 | unobserve(element:HTMLElement) {
126 | const funk = (el:HTMLElement) => {
127 | this.observer?.unobserve(element)
128 | this.elements?.delete(element)
129 | }
130 |
131 | if (element) {
132 | funk(element)
133 | } else {
134 | this.elements?.keys().forEach(el => funk(el))
135 | }
136 | }
137 | }()
--------------------------------------------------------------------------------
/src/utils/Path.ts:
--------------------------------------------------------------------------------
1 | import type { CornerOptions } from '..'
2 | import type { RedrawOptions } from './ElementManager'
3 |
4 | import ElementManager from './ElementManager'
5 | import { createPath as Squircle } from '../corners/Squircle'
6 | import { createSVGDatURI } from './Svg'
7 | import { PathOptions } from '..'
8 |
9 | export interface BackgroundOptions extends PathOptions {
10 | background?:string,
11 | border?:[number, string] | [number, string][],
12 | strokeDrawType?:number
13 | clipID?:string,
14 | clip?:boolean
15 | }
16 |
17 | export interface DefaultCornerTypeOptions {
18 | width:number,
19 | height:number,
20 | radii:number[],
21 | offsets:number[]
22 | }
23 |
24 | function createBackground(options:BackgroundOptions) {
25 | const {
26 | border:borderOrArray = [],
27 | offset:offsetOrArray = 0,
28 | strokeDrawType = 0,
29 | background,
30 | clip,
31 | clipID,
32 | width,
33 | height
34 | } = options
35 |
36 | const paths:string[] = []
37 | const borderArray = Array.isArray(borderOrArray?.[0]) ? borderOrArray : [borderOrArray]
38 | const offsetArray = Array.isArray(offsetOrArray) ? offsetOrArray : [offsetOrArray, offsetOrArray, offsetOrArray, offsetOrArray]
39 | const clipPath = clip ? null : createPath(options)
40 |
41 | if (borderArray?.length) {
42 | let totalBorderRadius = 0
43 |
44 | const borderPaths:string[] = []
45 |
46 | for (let i = 0; i < borderArray.length; i++) {
47 | const [size, borderColor] = borderArray[i] as [number, string]
48 |
49 | const strokeWidth = strokeDrawType === 0 ? (totalBorderRadius + size) * 2 : size
50 |
51 | if (size) {
52 | borderPaths.push(``)
56 |
57 | totalBorderRadius += size
58 | }
59 | }
60 |
61 | if (background) {
62 | if (clip) {
63 | paths.push(``)
64 | } else {
65 | paths.push(``)
66 | }
67 | }
68 |
69 | paths.push(...borderPaths.reverse())
70 | }
71 |
72 | return paths.length ? createSVGDatURI(paths, clipPath as string, clipID) : 'none'
73 | }
74 |
75 | export function createPath({
76 | width:w = 0,
77 | height:h = 0,
78 | borderRadius:radiusOrArray = 0,
79 | offset:offsetOrArray = 0,
80 | smoothing = 1,
81 | cornerType = Squircle,
82 | precision = 3,
83 | isArray = false
84 | }:PathOptions) {
85 | if (!w || !h) {
86 | return isArray ? [] : ''
87 | }
88 |
89 | const offsets = Array.isArray(offsetOrArray) ? offsetOrArray : [offsetOrArray, offsetOrArray, offsetOrArray, offsetOrArray]
90 | const [ot, or, ob, ol] = offsets
91 | const width = w - ol - or
92 | const height = h - ot - ob
93 |
94 | let radii;
95 |
96 | if (Array.isArray(radiusOrArray)) {
97 | // https://drafts.csswg.org/css-backgrounds/#corner-overlap
98 | const sides = radiusOrArray.map((r, i) => r + radiusOrArray[(i + 1) % 4])
99 | const f = Math.min(...sides.map((s, i) => (i % 2 === 0 ? width : height) / s))
100 |
101 | if (f < 1) {
102 | radii = radiusOrArray.map(r => r * f);
103 | } else {
104 | radii = radiusOrArray;
105 | }
106 | } else {
107 | radii = [radiusOrArray, radiusOrArray, radiusOrArray, radiusOrArray].map((r, i) => Math.max(
108 | 0,
109 | Math.min(r - offsets[i], Math.min(width, height) * 0.5)
110 | ))
111 | }
112 |
113 | let path
114 |
115 | if (cornerType) {
116 | path = cornerType({
117 | width,
118 | height,
119 | radii,
120 | offsets,
121 | smoothing
122 | })
123 | } else {
124 | path = [[]]
125 | }
126 |
127 | path = path
128 | .filter(instructions => instructions[0])
129 | .map(([command, ...parameters]) => {
130 | const p = parameters.map(x => Number.isFinite(x) ? Number((x as number).toFixed(precision)) : x)
131 | const result = [
132 | command,
133 | isArray ? p : p.join(' ')
134 | ]
135 |
136 | return isArray ? result : result.join('')
137 | })
138 |
139 | return isArray ? path : path.join('')
140 | }
141 |
142 | export function addCorners(element:HTMLElement, cornerOptions:CornerOptions) {
143 | ElementManager.setCornerOptions(element, cornerOptions)
144 |
145 | const drawFunk = (redrawOptions?:RedrawOptions) => {
146 | const options = ElementManager.getDrawOptions(element) ?? {} as CornerOptions
147 |
148 | if (!(options.width && options.height)) {
149 | const rect = element.getBoundingClientRect()
150 |
151 | options.width = rect.width
152 | options.height = rect.height
153 | }
154 |
155 | const combinedOptions = {...options, ...redrawOptions}
156 |
157 | if (combinedOptions.isRounded) {
158 | combinedOptions.width = combinedOptions.width ? Math.round(combinedOptions.width) : undefined
159 | combinedOptions.height = combinedOptions.height ? Math.round(combinedOptions.height) : undefined
160 | }
161 |
162 | element.style.clipPath = options.clip ? `path('${createPath(combinedOptions)}')` : ''
163 |
164 | if (options.background || options.border) {
165 | element.style.backgroundImage = createBackground(combinedOptions)
166 | }
167 | }
168 |
169 | drawFunk()
170 |
171 | return ElementManager.addElement(element, cornerOptions, drawFunk)
172 | }
--------------------------------------------------------------------------------
/src/utils/Svg.ts:
--------------------------------------------------------------------------------
1 | const encode = (content:string) => encodeURIComponent(content)
2 | .replace(/%20/g, ' ')
3 | .replace(/%3D/g, '=')
4 | .replace(/%3A/g, ':')
5 | .replace(/%2F/g, '/')
6 | .replace(/%2C/g, ',')
7 | .replace(/%3B/g, ';')
8 |
9 | export const createSVG = (content:string) => ``
10 |
11 | export const createSVGDatURI = (paths:string[], clipPath:string, id:string = 'c') =>
12 | `url('data:image/svg+xml,${
13 | encode(createSVG((clipPath ? [
14 | ``,
15 | `${paths.join('')}`
16 | ] : paths).join('')))
17 | }')`
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "bundler",
4 | "removeComments": false,
5 | "target": "es2022",
6 | "rootDir": "./src",
7 | "strict": true,
8 | "module": "esnext",
9 | "noEmitOnError": true,
10 | "lib": ["dom", "dom.iterable", "esnext"],
11 | "checkJs": true,
12 | "esModuleInterop": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "resolveJsonModule": true,
15 | "skipLibCheck": true,
16 | "strict": true
17 | },
18 | "include": ["src"]
19 | }
--------------------------------------------------------------------------------