|
6 | 6 | // https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius |
7 | 7 |
|
8 | 8 | import { buildXMLString, lengthToNumber } from '../utils.js' |
| 9 | +import { parseCornerShapeValue } from '../parser/corner-shape.js' |
9 | 10 |
|
10 | 11 | // Getting the intersection of a 45deg ray with the elliptical arc x^2/rx^2 + y^2/ry^2 = 1. |
11 | 12 | // Reference: |
@@ -66,6 +67,159 @@ function resolveRadius( |
66 | 67 | const radiusZeroOrNull = (_radius?: [number, number]) => |
67 | 68 | _radius && _radius[0] !== 0 && _radius[1] !== 0 |
68 | 69 |
|
| 70 | +function resolveCornerShape(value: unknown) { |
| 71 | + if (typeof value !== 'string') return 1 |
| 72 | + return parseCornerShapeValue(value) |
| 73 | +} |
| 74 | + |
| 75 | +type Point = [number, number] |
| 76 | + |
| 77 | +function cornerPoints( |
| 78 | + start: Point, |
| 79 | + end: Point, |
| 80 | + outer: Point, |
| 81 | + center: Point, |
| 82 | + shape: number |
| 83 | +) { |
| 84 | + if (shape === Infinity) return [start, outer, end] |
| 85 | + if (shape === -Infinity) return [start, center, end] |
| 86 | + if (shape === 0) { |
| 87 | + return [ |
| 88 | + start, |
| 89 | + [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] as Point, |
| 90 | + end, |
| 91 | + ] |
| 92 | + } |
| 93 | + |
| 94 | + const curveCenter = shape < 0 ? outer : center |
| 95 | + const exponent = Math.pow(2, 1 - Math.abs(shape)) |
| 96 | + if (exponent === 0) |
| 97 | + return shape > 0 ? [start, outer, end] : [start, center, end] |
| 98 | + const points: Point[] = [] |
| 99 | + const segments = 16 |
| 100 | + |
| 101 | + for (let i = 0; i <= segments; i++) { |
| 102 | + const angle = (Math.PI * i) / segments / 2 |
| 103 | + const x = Math.pow(Math.sin(angle), exponent) |
| 104 | + const y = Math.pow(Math.cos(angle), exponent) |
| 105 | + points.push([ |
| 106 | + curveCenter[0] + |
| 107 | + (end[0] - curveCenter[0]) * x + |
| 108 | + (start[0] - curveCenter[0]) * y, |
| 109 | + curveCenter[1] + |
| 110 | + (end[1] - curveCenter[1]) * x + |
| 111 | + (start[1] - curveCenter[1]) * y, |
| 112 | + ]) |
| 113 | + } |
| 114 | + |
| 115 | + return points |
| 116 | +} |
| 117 | + |
| 118 | +function pointString(point: Point) { |
| 119 | + return `${Math.round(point[0] * 1000) / 1000},${ |
| 120 | + Math.round(point[1] * 1000) / 1000 |
| 121 | + }` |
| 122 | +} |
| 123 | + |
| 124 | +function oppositeRadiiScale( |
| 125 | + first: [number, number], |
| 126 | + second: [number, number], |
| 127 | + width: number, |
| 128 | + height: number |
| 129 | +) { |
| 130 | + return Math.min( |
| 131 | + 1, |
| 132 | + width / (first[0] + second[0] || width), |
| 133 | + height / (first[1] + second[1] || height) |
| 134 | + ) |
| 135 | +} |
| 136 | + |
| 137 | +function shapedRadiusPath( |
| 138 | + left: number, |
| 139 | + top: number, |
| 140 | + width: number, |
| 141 | + height: number, |
| 142 | + radii: [number, number][], |
| 143 | + shapes: number[], |
| 144 | + partialSides?: boolean[] |
| 145 | +) { |
| 146 | + if (partialSides?.every(Boolean)) partialSides = undefined |
| 147 | + |
| 148 | + const right = left + width |
| 149 | + const bottom = top + height |
| 150 | + const [topLeft, topRight, bottomRight, bottomLeft] = radii |
| 151 | + const corners = [ |
| 152 | + cornerPoints( |
| 153 | + [left, top + topLeft[1]], |
| 154 | + [left + topLeft[0], top], |
| 155 | + [left, top], |
| 156 | + [left + topLeft[0], top + topLeft[1]], |
| 157 | + shapes[0] |
| 158 | + ), |
| 159 | + cornerPoints( |
| 160 | + [right - topRight[0], top], |
| 161 | + [right, top + topRight[1]], |
| 162 | + [right, top], |
| 163 | + [right - topRight[0], top + topRight[1]], |
| 164 | + shapes[1] |
| 165 | + ), |
| 166 | + cornerPoints( |
| 167 | + [right, bottom - bottomRight[1]], |
| 168 | + [right - bottomRight[0], bottom], |
| 169 | + [right, bottom], |
| 170 | + [right - bottomRight[0], bottom - bottomRight[1]], |
| 171 | + shapes[2] |
| 172 | + ), |
| 173 | + cornerPoints( |
| 174 | + [left + bottomLeft[0], bottom], |
| 175 | + [left, bottom - bottomLeft[1]], |
| 176 | + [left, bottom], |
| 177 | + [left + bottomLeft[0], bottom - bottomLeft[1]], |
| 178 | + shapes[3] |
| 179 | + ), |
| 180 | + ] |
| 181 | + |
| 182 | + if (partialSides) { |
| 183 | + let start = partialSides.indexOf(true) |
| 184 | + if (start === -1) throw new Error('Invalid `partialSides`.') |
| 185 | + while (partialSides[(start + 3) % 4]) start = (start + 3) % 4 |
| 186 | + |
| 187 | + const firstCorner = corners[start] |
| 188 | + const firstMiddle = Math.floor(firstCorner.length / 2) |
| 189 | + let path = `M${pointString(firstCorner[firstMiddle])}` |
| 190 | + let side = start |
| 191 | + |
| 192 | + do { |
| 193 | + const currentCorner = corners[side] |
| 194 | + const currentMiddle = Math.floor(currentCorner.length / 2) |
| 195 | + for (let i = currentMiddle + 1; i < currentCorner.length; i++) { |
| 196 | + path += ` L${pointString(currentCorner[i])}` |
| 197 | + } |
| 198 | + |
| 199 | + const nextCorner = corners[(side + 1) % 4] |
| 200 | + path += ` L${pointString(nextCorner[0])}` |
| 201 | + const nextMiddle = Math.floor(nextCorner.length / 2) |
| 202 | + for (let i = 1; i <= nextMiddle; i++) { |
| 203 | + path += ` L${pointString(nextCorner[i])}` |
| 204 | + } |
| 205 | + |
| 206 | + side = (side + 1) % 4 |
| 207 | + } while (partialSides[side] && side !== start) |
| 208 | + |
| 209 | + return path |
| 210 | + } |
| 211 | + |
| 212 | + let path = `M${pointString(corners[0][corners[0].length - 1])}` |
| 213 | + for (let side = 0; side < 4; side++) { |
| 214 | + const nextCorner = corners[(side + 1) % 4] |
| 215 | + path += ` L${pointString(nextCorner[0])}` |
| 216 | + for (let i = 1; i < nextCorner.length; i++) { |
| 217 | + path += ` L${pointString(nextCorner[i])}` |
| 218 | + } |
| 219 | + } |
| 220 | + return path + ' Z' |
| 221 | +} |
| 222 | + |
69 | 223 | export function getBorderRadiusClipPath( |
70 | 224 | { |
71 | 225 | id, |
@@ -126,6 +280,12 @@ export default function radius( |
126 | 280 | borderBottomRightRadius, |
127 | 281 | fontSize, |
128 | 282 | } = style |
| 283 | + const cornerShapes = [ |
| 284 | + resolveCornerShape(style.cornerTopLeftShape), |
| 285 | + resolveCornerShape(style.cornerTopRightShape), |
| 286 | + resolveCornerShape(style.cornerBottomRightShape), |
| 287 | + resolveCornerShape(style.cornerBottomLeftShape), |
| 288 | + ] |
129 | 289 |
|
130 | 290 | let singleAbsValueTopLeftCorner |
131 | 291 | let singleAbsValueTopRightCorner |
@@ -216,6 +376,51 @@ export default function radius( |
216 | 376 | makeSmaller(borderBottomRightRadius) |
217 | 377 | } |
218 | 378 |
|
| 379 | + if (cornerShapes.some((shape) => shape < 0)) { |
| 380 | + const scale = Math.min( |
| 381 | + oppositeRadiiScale( |
| 382 | + borderTopLeftRadius, |
| 383 | + borderBottomRightRadius, |
| 384 | + width, |
| 385 | + height |
| 386 | + ), |
| 387 | + oppositeRadiiScale( |
| 388 | + borderTopRightRadius, |
| 389 | + borderBottomLeftRadius, |
| 390 | + width, |
| 391 | + height |
| 392 | + ) |
| 393 | + ) |
| 394 | + if (scale < 1) { |
| 395 | + for (const corner of [ |
| 396 | + borderTopLeftRadius, |
| 397 | + borderTopRightRadius, |
| 398 | + borderBottomRightRadius, |
| 399 | + borderBottomLeftRadius, |
| 400 | + ]) { |
| 401 | + corner[0] *= scale |
| 402 | + corner[1] *= scale |
| 403 | + } |
| 404 | + } |
| 405 | + } |
| 406 | + |
| 407 | + if (cornerShapes.some((shape) => shape !== 1)) { |
| 408 | + return shapedRadiusPath( |
| 409 | + left, |
| 410 | + top, |
| 411 | + width, |
| 412 | + height, |
| 413 | + [ |
| 414 | + borderTopLeftRadius, |
| 415 | + borderTopRightRadius, |
| 416 | + borderBottomRightRadius, |
| 417 | + borderBottomLeftRadius, |
| 418 | + ], |
| 419 | + cornerShapes, |
| 420 | + partialSides |
| 421 | + ) |
| 422 | + } |
| 423 | + |
219 | 424 | type Arc = [[number, number], [number, number]] |
220 | 425 | const p: Arc[] = [] |
221 | 426 | p[0] = [borderTopRightRadius, borderTopRightRadius] |
|
0 commit comments