Skip to content

Commit 14cbba6

Browse files
committed
masks for borders
1 parent cd884e7 commit 14cbba6

File tree

5 files changed

+120
-76
lines changed

5 files changed

+120
-76
lines changed

src/css-utils.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import CSS_COLOR_NAMES from "./html-colors";
2-
import toPx from "./css-length-converter";
1+
import CSS_COLOR_NAMES from "./external/bobspace:html-colors";
2+
import toPx from "./external/heygrady:units:length";
33

44
/** @returns {string} */
55
function convertPlainColor(val) {
@@ -31,6 +31,8 @@ function convertColorOpacity(val) {
3131
const htmlLengthNotSvgErrorTemplate = (a, b) => `<RoundDiv> ${a} must be ${b ? `either ${b}, or` : ''} in one of the following units: ch, cm, em, ex, in, mm, pc, pt, px, rem, vh, vmax, vmin, vw.`
3232
const htmlBorderLengthNotSvgError =
3333
new Error(htmlLengthNotSvgErrorTemplate('border lengths', '"thin", "medium", "thick"'))
34+
const htmlBorderRadiusNotSvgError =
35+
new Error(htmlLengthNotSvgErrorTemplate('border radii'))
3436

3537
function toNumber(length, element, err) {
3638
if (!length) return false
@@ -58,4 +60,4 @@ function convertBorderWidth(val, element) {
5860
return toNumber(val, element, htmlBorderLengthNotSvgError) || 0
5961
}
6062

61-
export {convertPlainColor, convertColorOpacity, convertBorderWidth}
63+
export {convertPlainColor, convertColorOpacity, convertBorderWidth, toNumber, htmlBorderRadiusNotSvgError}

src/generator.js

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,95 @@
1-
export default function generateSvgSquircle(height, width, radius, clip) {
1+
/**
2+
* @param {number} height
3+
* @param {number} width
4+
* @param {number | Array<number>} radius
5+
*
6+
* @returns {string} SVG path data
7+
* */
8+
export default function generateSvgSquircle(height, width, radius) {
29
/* from right to left top left corner upper (right half) */
310
const ratios = [1.528665037, 1.0884928889, 0.8684068148, 0.07491140741, 0.6314939259, 0.1690595556, 0.3728238519];
411

12+
if (typeof radius === 'number')
13+
radius = Array(4).fill(radius)
14+
else if (radius.length === 2)
15+
radius.push(radius[0])
16+
if (radius.length === 3)
17+
radius.push(radius[1])
18+
519
height = Number(height);
620
width = Number(width);
721

8-
radius = clip === false
9-
? Number(radius)
10-
: Math.min(Number(radius), height / 2, width / 2);
22+
radius = radius.map(radius => Math.min(Number(radius), height / 2, width / 2))
1123

12-
const [a0x, a1x, a2x, a3y, a3x, b1y, b1x] = Array(7).fill(0).map((a, i) => radius * ratios[i]),
24+
const [a0x, a1x, a2x, a3y, a3x, b1y, b1x] = Array(7)
25+
.fill(Array(4).fill(0))
26+
.map((a, i) => a.map((b, j) => radius[j] * ratios[i])),
1327
[b0y, b0x] = [a3y, a3x]
1428

1529
if (isNaN(height)) throw new Error(`'height' must be a number`);
1630
if (isNaN(width)) throw new Error(`'width' must be a number`);
17-
if (isNaN(radius)) throw new Error(`'radius' must be a number`);
31+
if (radius.includes(NaN)) throw new Error(`'radius' must be a number or an array containing 2 to 4 numbers`);
1832

19-
const a0xF = x => Math.min(x / 2, a0x),
33+
const a0xF = x => Math.min(x / 2, a0x[0]),
2034
a0xw = a0xF(width),
2135
a0xh = a0xF(height)
2236

2337
function mapRange(number, in_min, in_max, out_min, out_max) {
2438
return (number - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
2539
}
2640

41+
const maxRadius = Math.max(...radius);
42+
2743
const yOffsetF = (x) =>
2844
Math.max(0, Math.min(
29-
mapRange(radius, (x / 2) * .90, x / 2, 0, 1),
45+
mapRange(maxRadius, (x / 2) * .90, x / 2, 0, 1),
3046
1
31-
)) * 1.7,
32-
hyOffset = clip !== false ? yOffsetF(height) : 0,
33-
wyOffset = clip !== false ? yOffsetF(width) : 0
47+
)) * 200 / maxRadius,
48+
hyOffset = yOffsetF(height),
49+
wyOffset = yOffsetF(width)
50+
51+
console.log(hyOffset, wyOffset)
3452

3553
const startPoint = `${a0xw},${wyOffset}`
3654

3755
return `M${startPoint}
38-
${width / 2 < a0x && clip !== false
56+
${width / 2 < a0x[1]
3957
? ''
4058
: `L${width - a0xw},0`
4159
}
4260
43-
C${width - a1x},0,${width - a2x},0,${width - a3x},${a3y}
44-
C${width - b1x},${b1y},${width - b1y},${b1x},${width - b0y},${b0x}
45-
C${width},${a2x},${width},${a1x},
61+
C${width - a1x[1]},0,${width - a2x[1]},0,${width - a3x[1]},${a3y[1]}
62+
C${width - b1x[1]},${b1y[1]},${width - b1y[1]},${b1x[1]},${width - b0y[1]},${b0x[1]}
63+
C${width},${a2x[1]},${width},${a1x[1]},
4664
4765
${width - hyOffset},${a0xh}
48-
${height / 2 < a0x && clip !== false
66+
${height / 2 < a0x[2]
4967
? ''
5068
: `L${width},${height - a0xh}`
5169
}
5270
53-
C${width},${height - a1x},${width},${height - a2x},${width - a3y},${height - a3x}
54-
C${width - b1y},${height - b1x},${width - b1x},${height - b1y},${width - b0x},${height - b0y}
55-
C${width - a2x},${height},${width - a1x},${height},
71+
C${width},${height - a1x[2]},${width},${height - a2x[2]},${width - a3y[2]},${height - a3x[2]}
72+
C${width - b1y[2]},${height - b1x[2]},${width - b1x[2]},${height - b1y[2]},${width - b0x[2]},${height - b0y[2]}
73+
C${width - a2x[2]},${height},${width - a1x[2]},${height},
5674
5775
${width - a0xw},${height - wyOffset}
58-
${width / 2 < a0x && clip !== false
76+
${width / 2 < a0x[3]
5977
? ''
6078
: `L${a0xw},${height}`
6179
}
6280
63-
C${a1x},${height},${a2x},${height},${a3x},${height - a3y}
64-
C${b1x},${height - b1y},${b1y},${height - b1x},${b0y},${height - b0x}
65-
C0,${height - a2x},0,${height - a1x},
81+
C${a1x[3]},${height},${a2x[3]},${height},${a3x[3]},${height - a3y[3]}
82+
C${b1x[3]},${height - b1y[3]},${b1y[3]},${height - b1x[3]},${b0y[3]},${height - b0x[3]}
83+
C0,${height - a2x[3]},0,${height - a1x[3]},
6684
6785
${hyOffset},${height - a0xh}
68-
${height / 2 < a0x && clip !== false
86+
${height / 2 < a0x[0]
6987
? ''
7088
: `L0,${a0xh}`
7189
}
7290
73-
C0,${a1x},0,${a2x},${a3y},${a3x}
74-
C${b1y},${b1x},${b1x},${b1y},${b0x},${b0y}
75-
C${a2x},0,${a1x},0,${startPoint}
91+
C0,${a1x[0]},0,${a2x[0]},${a3y[0]},${a3x[0]}
92+
C${b1y[0]},${b1x[0]},${b1x[0]},${b1y[0]},${b0x[0]},${b0y[0]}
93+
C${a2x[0]},0,${a1x[0]},0,${startPoint}
7694
Z`.replace(/\n */g, '');
7795
}

src/main.js

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import React, {useRef, useEffect, useState, useCallback} from 'react'
22
import generateSvgSquircle from './generator'
3-
import './getMatchedCSSRules-polyfill'
3+
import './external/getMatchedCSSRules-polyfill'
44
import updateStates from "./updateStates"
5-
import ShadowRoot from "./react-shadow-dom"
6-
import attachCSSWatcher from './style-sheet-watcher'
5+
import ShadowRoot from "./external/apearce:eact-shadow-dom"
6+
import attachCSSWatcher from './styleSheetWatcher'
7+
import getMaskPaths from './mask-generator'
78

8-
export default function RoundDiv({clip, style, children, ...props}) {
9+
export default function RoundDiv({style, children, ...props}) {
910
// welcome to react states hell
1011
const [height, setHeight] = useState(0)
1112
const [width, setWidth] = useState(0)
12-
const [radius, setRadius] = useState(0)
13+
const [radius, setRadius] = useState(Array(4).fill(0))
1314

1415
const [borderColor, setBorderColor] = useState(Array(4).fill('transparent'))
1516
const [borderOpacity, setBorderOpacity] = useState(Array(4).fill(1))
@@ -37,13 +38,13 @@ export default function RoundDiv({clip, style, children, ...props}) {
3738
setIsFlex
3839
}), [style])
3940

40-
useEffect(updateStatesWithArgs, [div, clip, style, updateStatesWithArgs])
41+
useEffect(updateStatesWithArgs, [div, style, updateStatesWithArgs])
4142

4243
useEffect(() => {
4344
attachCSSWatcher(() => updateStatesWithArgs())
4445
}, [updateStatesWithArgs])
4546

46-
const path = generateSvgSquircle(height, width, radius, clip)
47+
const path = generateSvgSquircle(height, width, radius)
4748
const maxBorderWidth = Math.max(...borderWidth)
4849
const diffBorderMaxWidthLeftWidth = maxBorderWidth - borderWidth[3]
4950
const diffBorderMaxWidthTopWidth = maxBorderWidth - borderWidth[0]
@@ -52,37 +53,14 @@ export default function RoundDiv({clip, style, children, ...props}) {
5253
const borderPath = generateSvgSquircle(
5354
height + diffBorderMaxWidthTopWidth + (maxBorderWidth - borderWidth[2]),
5455
width + diffBorderMaxWidthLeftWidth + (maxBorderWidth - borderWidth[1]),
55-
radius,
56-
clip)
57-
58-
const maskPointsA = {
59-
to: -borderWidth[0],
60-
ti: borderWidth[0] * 2,
61-
ro: width + borderWidth[1],
62-
ri: width - borderWidth[1] * 2,
63-
bo: height + borderWidth[2],
64-
bi: height - borderWidth[2] * 2,
65-
lo: -borderWidth[3],
66-
li: borderWidth[3] * 2,
67-
}
68-
69-
const maskPoints = {
70-
tlo: maskPointsA.lo + ',' + maskPointsA.to,
71-
tli: maskPointsA.li + ',' + maskPointsA.ti,
72-
tro: maskPointsA.ro + ',' + maskPointsA.to,
73-
tri: maskPointsA.ri + ',' + maskPointsA.ti,
74-
blo: maskPointsA.lo + ',' + maskPointsA.bo,
75-
bli: maskPointsA.li + ',' + maskPointsA.bi,
76-
bro: maskPointsA.ro + ',' + maskPointsA.bo,
77-
bri: maskPointsA.ri + ',' + maskPointsA.bi,
78-
}
79-
80-
const maskPaths = {
81-
top: `M${maskPoints.tli}H${maskPointsA.ri}L${maskPoints.tro}H${maskPointsA.lo}Z`,
82-
right: `M${maskPoints.tri}V${maskPointsA.bi}L${maskPoints.bro}V${maskPointsA.to}Z`,
83-
bottom: `M${maskPoints.bri}H${maskPointsA.li}L${maskPoints.blo}H${maskPointsA.ro}Z`,
84-
left: `M${maskPoints.bli}V${maskPointsA.ti}L${maskPoints.tlo}V${maskPointsA.bo}Z`,
85-
}
56+
[
57+
radius[0] + maxBorderWidth - Math.min(borderWidth[3], borderWidth[0]),
58+
radius[1] + maxBorderWidth - Math.min(borderWidth[0], borderWidth[1]),
59+
radius[2] + maxBorderWidth - Math.min(borderWidth[1], borderWidth[2]),
60+
radius[3] + maxBorderWidth - Math.min(borderWidth[2], borderWidth[3])
61+
])
62+
63+
const maskPaths = getMaskPaths(borderWidth, height, width)
8664

8765
// console.log(borderWidth, borderColor, borderOpacity)
8866
// console.log(isFlex)

src/mask-generator.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export default function getMaskPaths(borderWidth, height, width) {
2+
const n = 10;
3+
// todo: make adaptive
4+
5+
const maskPointsA = {
6+
to: -borderWidth[0],
7+
ti: borderWidth[0] * n,
8+
ro: width + borderWidth[1],
9+
ri: width - borderWidth[1] * n,
10+
bo: height + borderWidth[2],
11+
bi: height - borderWidth[2] * n,
12+
lo: -borderWidth[3],
13+
li: borderWidth[3] * n,
14+
}
15+
16+
const maskPoints = {
17+
tlo: maskPointsA.lo + ',' + maskPointsA.to,
18+
tli: maskPointsA.li + ',' + maskPointsA.ti,
19+
tro: maskPointsA.ro + ',' + maskPointsA.to,
20+
tri: maskPointsA.ri + ',' + maskPointsA.ti,
21+
blo: maskPointsA.lo + ',' + maskPointsA.bo,
22+
bli: maskPointsA.li + ',' + maskPointsA.bi,
23+
bro: maskPointsA.ro + ',' + maskPointsA.bo,
24+
bri: maskPointsA.ri + ',' + maskPointsA.bi,
25+
}
26+
27+
return {
28+
top: `M${maskPoints.tli}H${maskPointsA.ri}L${maskPoints.tro}H${maskPointsA.lo}Z`,
29+
right: `M${maskPoints.tri}V${maskPointsA.bi}L${maskPoints.bro}V${maskPointsA.to}Z`,
30+
bottom: `M${maskPoints.bri}H${maskPointsA.li}L${maskPoints.blo}H${maskPointsA.ro}Z`,
31+
left: `M${maskPoints.bli}V${maskPointsA.ti}L${maskPoints.tlo}V${maskPointsA.bo}Z`,
32+
}
33+
}

src/updateStates.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import {convertPlainColor, convertColorOpacity, convertBorderWidth} from "./css-utils";
2-
import getStyle from "./styles-extractor";
1+
import {
2+
convertPlainColor,
3+
convertColorOpacity,
4+
convertBorderWidth,
5+
toNumber,
6+
htmlBorderRadiusNotSvgError
7+
} from "./css-utils";
8+
import getStyle from "./external/styles-extractor";
39

410
export default function updateStates(args) {
511
const {div, setHeight, setWidth} = args
@@ -19,9 +25,9 @@ export default function updateStates(args) {
1925
const getNthStyle = (key, n) => {
2026
const returnNthOverwrittenOrCurrent = r =>
2127
!r ? false :
22-
r?.overwritten.length > 1
23-
? r.overwritten[n ?? 0].value
24-
: r.current?.value
28+
r?.overwritten.length > 1
29+
? r.overwritten[n ?? 0].value
30+
: r.current?.value
2531

2632
const normal = getStyle(key, div.current);
2733
const camelised = getStyle(camelise(key), div.current)
@@ -36,12 +42,19 @@ export default function updateStates(args) {
3642
getNthStyle('border-left-' + key, n),
3743
]
3844

45+
const getBorderRadii = (n) => [
46+
getNthStyle('border-top-right-radius', n),
47+
getNthStyle('border-top-left-radius', n),
48+
getNthStyle('border-bottom-right-radius', n),
49+
getNthStyle('border-bottom-left-radius', n),
50+
]
51+
3952
const divStyle = div.current ? window?.getComputedStyle(div.current) : null
4053
if (divStyle) {
4154
let states = args
42-
states.setRadius(Number(
43-
(divStyle.borderRadius || divStyle.borderTopLeftRadius)
44-
.replace('px', ''))
55+
states.setRadius(
56+
getBorderRadii(1)
57+
.map(s => toNumber(s, div.current, htmlBorderRadiusNotSvgError))
4558
)
4659

4760
// get color

0 commit comments

Comments
 (0)