Skip to content

Commit 519bca8

Browse files
committed
improvements
1 parent f2b7048 commit 519bca8

File tree

5 files changed

+119
-64
lines changed

5 files changed

+119
-64
lines changed

src/generator.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,23 @@ export default function generateSvgSquircle(height, width, radius) {
1919
height = Number(height);
2020
width = Number(width);
2121

22-
radius = radius.map(radius => Math.min(Number(radius), height / 2, width / 2))
22+
const _rawRadius = [...radius].map(n => Number(n))
23+
const max = radius.length - 1
24+
const next = i => i === max ? 0 : i + 1
25+
const prev = i => i === 0 ? max : i - 1
26+
radius = _rawRadius.map((radius, i) =>
27+
Math.min(
28+
radius,
29+
Math.min(
30+
height - _rawRadius[i % 2 === 0 ? prev(i) : next(i)],
31+
height / 2
32+
),
33+
Math.min(
34+
width - _rawRadius[i % 2 === 0 ? next(i) : prev(i)],
35+
width / 2
36+
)
37+
)
38+
)
2339

2440
const [a0x, a1x, a2x, a3y, a3x, b1y, b1x] = Array(7)
2541
.fill(Array(4).fill(0))
@@ -34,17 +50,13 @@ export default function generateSvgSquircle(height, width, radius) {
3450
a0xw = a0xF(width),
3551
a0xh = a0xF(height)
3652

37-
function mapRange(number, in_min, in_max, out_min, out_max) {
53+
/*function mapRange(number, in_min, in_max, out_min, out_max) {
3854
return (number - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
39-
}
55+
}*/
4056

41-
const maxRadius = Math.max(...radius);
57+
// const maxRadius = Math.max(...radius);
4258

43-
const yOffsetF = (x) =>
44-
Math.max(0, Math.min(
45-
mapRange(maxRadius, (x / 2) * .90, x / 2, 0, 1),
46-
1
47-
)) * 200 / maxRadius,
59+
const yOffsetF = (x) => 0,
4860
hyOffset = yOffsetF(height) || 0,
4961
wyOffset = yOffsetF(width) || 0
5062

src/main.js

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import getMaskPaths from './mask-generator'
88

99
export default function RoundDiv({style, children, ...props}) {
1010
// welcome to react states hell
11+
const [position, setPosition] = useState([0, 0])
1112
const [height, setHeight] = useState(0)
1213
const [width, setWidth] = useState(0)
1314
const [radius, setRadius] = useState(Array(4).fill(0))
@@ -16,55 +17,67 @@ export default function RoundDiv({style, children, ...props}) {
1617
const [borderOpacity, setBorderOpacity] = useState(Array(4).fill(1))
1718
const [borderWidth, setBorderWidth] = useState(Array(4).fill(0))
1819

19-
const [isFlex, setIsFlex] = useState(false)
20+
const [path, setPath] = useState('Z')
21+
const [innerPath, setInnerPath] = useState('Z')
22+
const [maskPaths, setMaskPaths] = useState('Z')
2023

2124
const div = useRef()
2225

23-
useEffect(() => {
24-
// attach shadow root to div
25-
if (!div.current?.shadowRoot)
26-
div.current?.attachShadow({mode: 'open'})
27-
}, [])
28-
2926
const updateStatesWithArgs = useCallback(() => updateStates({
3027
div,
3128
style,
29+
setPosition,
3230
setHeight,
3331
setWidth,
3432
setRadius,
3533
setBorderColor,
3634
setBorderWidth,
37-
setBorderOpacity,
38-
setIsFlex
35+
setBorderOpacity
3936
}), [style])
4037

41-
useEffect(updateStatesWithArgs, [div, style, updateStatesWithArgs])
38+
useEffect(updateStatesWithArgs, [style, updateStatesWithArgs])
4239

4340
useEffect(() => {
4441
attachCSSWatcher(() => updateStatesWithArgs())
4542
}, [updateStatesWithArgs])
4643

47-
const path = generateSvgSquircle(height, width, radius)
48-
const innerPath = generateSvgSquircle(
49-
height - (borderWidth[0] + borderWidth[2]),
50-
width - (borderWidth[1] + borderWidth[3]),
51-
radius.map((val, i) =>
52-
Math.max(0,
53-
val - Math.max(borderWidth[i], borderWidth[i === 0 ? 3 : i - 1])
44+
useEffect(() => {
45+
setPath(generateSvgSquircle(height, width, radius))
46+
setInnerPath(generateSvgSquircle(
47+
height - (borderWidth[0] + borderWidth[2]),
48+
width - (borderWidth[1] + borderWidth[3]),
49+
radius.map((val, i) =>
50+
Math.max(0,
51+
val - Math.max(borderWidth[i], borderWidth[i === 0 ? 3 : i - 1])
52+
)
5453
)
55-
)
56-
).replace(
57-
/(\d+(\.\d+)?),(\d+(\.\d+)?)/g,
58-
match => match.split(',').map((number, i) =>
59-
Number(number) + (i === 0 ? borderWidth[3] : borderWidth[0])
60-
).join(',')
61-
)
54+
).replace(
55+
/(\d+(\.\d+)?),(\d+(\.\d+)?)/g,
56+
match => match.split(',').map((number, i) =>
57+
Number(number) + (i === 0 ? borderWidth[3] : borderWidth[0])
58+
).join(',')
59+
))
60+
61+
// prevents unnecessary re-renders:
62+
// single value states (numbers and strings) prevent this out of the box,
63+
// complex states (objects, arrays, etc.) don't, so here it is manually for objects (non-nested)
64+
const lazySetObjectsState = (setState, newState) =>
65+
setState(oldState => {
66+
if (areEqualObjects(oldState, newState)) return oldState
67+
else return newState
68+
})
69+
70+
function areEqualObjects(a, b) {
71+
if (Object.keys(a).length !== Object.keys(b).length) return false
72+
for (let key in a) {
73+
if (a[key] !== b[key]) return false
74+
}
75+
return true
76+
}
6277

63-
const maskPaths = getMaskPaths(borderWidth, height, width)
78+
lazySetObjectsState(setMaskPaths, getMaskPaths(borderWidth, height, width, radius))
79+
}, [height, width, radius, borderWidth])
6480

65-
const svgTransform = isFlex
66-
? `translate(${(borderWidth[1] - borderWidth[3]) / 2}px,${(borderWidth[2] - borderWidth[0]) / 2}px)`
67-
: `translate(${(borderWidth[1] - borderWidth[3]) / 2}px,-${borderWidth[0]}px)`
6881

6982
const divStyle = {
7083
...style,
@@ -75,16 +88,17 @@ export default function RoundDiv({style, children, ...props}) {
7588
return <div {...props} style={divStyle} ref={div}>
7689
<ShadowRoot>
7790
<svg viewBox={`0 0 ${width} ${height}`} style={{
78-
position: 'absolute',
91+
position: 'fixed',
92+
left: position[0],
93+
top: position[1],
7994
height,
80-
width: 1,
95+
width,
8196
overflow: 'visible',
8297
zIndex: -1,
83-
transform: svgTransform
8498
}} xmlnsXlink="http://www.w3.org/1999/xlink" preserveAspectRatio={'xMidYMid slice'}>
8599
<defs>
86100
<clipPath id="inner">
87-
<path d={`M0,0V${height}H${width}V0Z` + innerPath} fillRule={'evenodd'}/>
101+
<path d={`M0,0V${height}H${width}V0H0Z` + innerPath} fillRule={'evenodd'}/>
88102
</clipPath>
89103
</defs>
90104
{Object.keys(maskPaths).map((key, i) => {
@@ -100,7 +114,7 @@ export default function RoundDiv({style, children, ...props}) {
100114
fill={borderColor[i]} opacity={borderOpacity[i]}/>
101115
})}
102116
</svg>
103-
<slot/>
117+
<slot style={{overflow: 'visible'}}/>
104118
</ShadowRoot>
105119
{children}
106120
</div>

src/mask-generator.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default function getMaskPaths(borderWidth, height, width) {
1+
export default function getMaskPaths(borderWidth, height, width, radius) {
22
const allSides = ['top', 'right', 'bottom', 'left']
33
const max = allSides.length - 1
44
const next = i => i === max ? 0 : i + 1
@@ -9,7 +9,9 @@ export default function getMaskPaths(borderWidth, height, width) {
99
* @type {Array<number>}
1010
* */
1111
const allRatios = allSides.map((side, i) =>
12-
((i % 2 === 0 ? height : width) - borderWidth[next(next(i))])
12+
((i % 2 === 0 ? height : width)
13+
- Math.max(borderWidth[next(next(i))], radius[prev(i)], radius[prev(prev(i))])
14+
)
1315
/ borderWidth[i]
1416
)
1517

@@ -55,10 +57,10 @@ export default function getMaskPaths(borderWidth, height, width) {
5557
const nextIfH = isH ? nextSide : side
5658
const nextIfV = !isH ? nextSide : side
5759

58-
return 'M' + makePoint(prevIfV, prevIfH, true, side) +
60+
return ('M' + makePoint(prevIfV, prevIfH, true, side) +
5961
T + makeValue(nextSide, true, side) +
6062
'L' + makePoint(nextIfV, nextIfH) +
61-
T + makeValue(prevSide) + 'Z'
63+
T + makeValue(prevSide) + 'Z').replace(/NaN/g, '0')
6264
}
6365

6466
return Object.fromEntries(

src/styleSheetWatcher.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
const CSSChangeEvent = new CustomEvent('css-change');
22

33
export default function attachCSSWatcher(callback) {
4-
CSSWatcher.addEventListener('css-change', () => callback())
4+
// attach later, after loading is done
5+
setTimeout(() =>
6+
CSSWatcher.addEventListener('css-change', () => callback())
7+
, 20)
58
}
69

710
const CSSWatcher = new EventTarget()
@@ -13,7 +16,11 @@ const CSSWatcher = new EventTarget()
1316
if (CSS === newCSS) return
1417
CSS = newCSS
1518
CSSWatcher.dispatchEvent(CSSChangeEvent)
16-
}, 100)
19+
}, 30)
20+
window.addEventListener('resize', () => {
21+
CSS = getCSSText()
22+
CSSWatcher.dispatchEvent(CSSChangeEvent)
23+
})
1724
})()
1825

1926
function getCSSText() {

src/updateStates.js

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@ import {
66
htmlBorderRadiusNotSvgError
77
} from "./css-utils";
88
import getStyle from "./external/styles-extractor";
9+
import ReactDOM from 'react-dom'
10+
11+
// prevents unnecessary re-renders:
12+
// single value states (numbers and strings) prevent this out of the box,
13+
// complex states (objects, arrays, etc.) don't, so here it is manually for arrays (non-nested)
14+
const lazySetArrayState = (setState, newState) =>
15+
setState(oldState => {
16+
if (oldState.every((val, i) => val === newState[i])) return oldState
17+
else return newState
18+
})
919

1020
export default function updateStates(args) {
11-
const {div, setHeight, setWidth} = args
21+
const {div, setPosition, setHeight, setWidth} = args
1222
const boundingClientRect = div.current?.getBoundingClientRect()
23+
let height, width;
1324
if (boundingClientRect) {
14-
setHeight(boundingClientRect.height)
15-
setWidth(boundingClientRect.width)
25+
lazySetArrayState(setPosition, [boundingClientRect.x, boundingClientRect.y])
26+
height = boundingClientRect.height
27+
width = boundingClientRect.width
28+
setHeight(height)
29+
setWidth(width)
1630
}
1731

1832
function camelise(str) {
@@ -29,7 +43,7 @@ export default function updateStates(args) {
2943
? r.overwritten[n ?? 0].value
3044
: r.current?.value
3145

32-
const normal = getStyle(key, div.current);
46+
const normal = getStyle(key, div.current)
3347
const camelised = getStyle(camelise(key), div.current)
3448

3549
return returnNthOverwrittenOrCurrent(normal) || returnNthOverwrittenOrCurrent(camelised)
@@ -49,32 +63,38 @@ export default function updateStates(args) {
4963
getNthStyle('border-bottom-left-radius', n),
5064
]
5165

66+
const states = args
67+
const lazySetRadius = newState => lazySetArrayState(states.setRadius, newState),
68+
lazySetBorderColor = newState => lazySetArrayState(states.setBorderColor, newState),
69+
lazySetBorderOpacity = newState => lazySetArrayState(states.setBorderOpacity, newState),
70+
lazySetBorderWidth = newState => lazySetArrayState(states.setBorderWidth, newState)
71+
5272
const divStyle = div.current ? window?.getComputedStyle(div.current) : null
53-
if (divStyle) {
54-
let states = args
55-
states.setRadius(
73+
if (!divStyle) return
74+
ReactDOM.unstable_batchedUpdates(() => {
75+
lazySetRadius(
5676
getBorderRadii(1)
57-
.map(s => toNumber(s, div.current, htmlBorderRadiusNotSvgError))
77+
.map(s => Math.min(
78+
toNumber(s, div.current, htmlBorderRadiusNotSvgError),
79+
height / 2,
80+
width / 2
81+
))
5882
)
5983

6084
// get color
61-
states.setBorderColor(
85+
lazySetBorderColor(
6286
getBorderStyles('color', 1)
6387
.map(s => convertPlainColor(s))
6488
)
6589
// get alpha value of color
66-
states.setBorderOpacity(
90+
lazySetBorderOpacity(
6791
getBorderStyles('color', 1)
6892
.map(s => convertColorOpacity(s))
6993
)
7094

71-
states.setBorderWidth(
95+
lazySetBorderWidth(
7296
getBorderStyles('width', 0)
7397
.map(s => convertBorderWidth(s, div.current))
7498
)
75-
76-
states.setIsFlex(
77-
getNthStyle('display', 0)?.endsWith('flex') || false
78-
)
79-
}
99+
})
80100
}

0 commit comments

Comments
 (0)