1+ <!DOCTYPE html>
2+ < html lang ="en ">
3+
4+ < head >
5+ < meta charset ="UTF-8 ">
6+ < meta http-equiv ="X-UA-Compatible " content ="IE=edge ">
7+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
8+ < title > Radius expansion algorithms</ title >
9+ < style >
10+ body {
11+ display : flex;
12+ height : 100vh ;
13+ margin : 0 ;
14+ }
15+ # output {
16+ flex : 1 ;
17+ border-left : 1px solid;
18+ margin-left : 1em ;
19+ overflow : auto;
20+ }
21+ # output , form {
22+ padding : 1em ;
23+ }
24+ hr {
25+ border : none;
26+ border-top : 1px dotted;
27+ }
28+ label {
29+ display : flex;
30+ width : max-content;
31+ margin : .5em 0 ;
32+ }
33+
34+ article {
35+ padding : 1em ;
36+ }
37+
38+ input : not ([type ]) {
39+ font : 100% / 1.5 Consolas, Monaco, monospace;
40+ width : 80ch ;
41+ margin-bottom : .5em ;
42+ }
43+ </ style >
44+
45+ </ head >
46+ < body >
47+
48+ < form >
49+ < label >
50+ < input type ="radio " name ="algorithm " value ="do-not-polyfill " checked >
51+ Do not polyfill
52+ </ label >
53+ < label >
54+ < input type ="radio " name ="algorithm " value ="increase-by-spread ">
55+ Increase radius by spread
56+ </ label >
57+ < label >
58+ < input type ="radio " name ="algorithm " value ="old-spec ">
59+ Old spec (discontinuous)
60+ </ label >
61+ < label >
62+ < input type ="radio " name ="algorithm " value ="current-spec ">
63+ Current spec
64+ </ label >
65+ < label >
66+ < input type ="radio " name ="algorithm " value ="percentage-same-axis ">
67+ Percentage of same axis
68+ </ label >
69+ < label >
70+ < input type ="radio " name ="algorithm " value ="elika ">
71+ Elika’s Interpolation based on rounded/straight ratio
72+ </ label >
73+ </ form >
74+ < output id ="output "> </ output >
75+ < script >
76+ const { algorithm} = document . forms [ 0 ] . elements ;
77+ const testCases = [
78+ { width : 50 , height : 50 , spread : 50 , borderRadius : "0px" } ,
79+ { width : 50 , height : 50 , spread : 50 , borderRadius : "1px" } ,
80+ { width : 10 , height : 10 , spread : 70 , borderRadius : "100%" } ,
81+ { width : 200 , height : 40 , spread : 50 , borderRadius : "100px / 20px" } ,
82+ { width : 200 , height : 40 , spread : 50 , borderRadius : "20px / 4px" } ,
83+ { width : 500 , height : 50 , spread : 30 , borderRadius : "15px" } ,
84+ { width : 500 , height : 50 , spread : 30 , borderRadius : "25px" } ,
85+ { width : 500 , height : 50 , spread : 30 , borderRadius : "1px 1px 49px 49px" } ,
86+ { width : 500 , height : 50 , spread : 30 , borderRadius : "50%" } ,
87+ { width : 500 , height : 50 , spread : 30 , borderRadius : "50% 50% 1px 50%" } ,
88+ ] ;
89+
90+ function show ( { incremental = false } = { } ) {
91+ for ( let i = 0 , testCase ; testCase = testCases [ i ] ; i ++ ) {
92+ let container = output . children [ i ] ;
93+
94+ if ( ! container ) {
95+ container = document . createElement ( "article" ) ;
96+ container . id = `testcase${ i } ` ;
97+ output . appendChild ( container ) ;
98+ }
99+
100+ if ( ! incremental ) {
101+ let input = document . createElement ( "input" ) ;
102+ input . value = JSON . stringify ( testCase ) ;
103+ input . oninput = ( ) => {
104+ try {
105+ testCases [ i ] = JSON . parse ( input . value ) ;
106+ show ( { incremental : true } ) ;
107+ }
108+ catch ( e ) { }
109+ } ;
110+
111+ container . append ( input ) ;
112+ }
113+
114+ Array . from ( container . querySelectorAll ( "div" ) ) . forEach ( e => e . remove ( ) ) ;
115+
116+ const inner = document . createElement ( "div" ) ;
117+ inner . style . width = testCase . width + "px" ;
118+ inner . style . height = testCase . height + "px" ;
119+ inner . style . borderRadius = testCase . borderRadius ;
120+ inner . style . backgroundColor = "#fff" ;
121+
122+ const outer = document . createElement ( "div" ) ;
123+ outer . appendChild ( inner ) ;
124+
125+ if ( algorithm . value === "do-not-polyfill" ) {
126+ inner . style . boxShadow = `0 0 0 ${ testCase . spread } px #000` ;
127+ outer . style . padding = testCase . spread + "px" ;
128+ container . appendChild ( outer ) ;
129+ }
130+ else {
131+ outer . style . backgroundColor = "#000" ;
132+ outer . style . borderStyle = "solid" ;
133+ outer . style . borderWidth = testCase . spread + "px" ;
134+ outer . style . width = "max-content" ;
135+ container . appendChild ( outer ) ;
136+ outer . style . borderRadius = resolve ( testCase , inner ) ;
137+ }
138+ }
139+ }
140+
141+ function parseCorner ( value , testCase ) {
142+ const raw = value . split ( " " ) ;
143+ if ( raw . length === 1 ) {
144+ raw [ 1 ] = raw [ 0 ] ;
145+ }
146+ return [
147+ parseLength ( raw [ 0 ] , testCase . width ) ,
148+ parseLength ( raw [ 1 ] , testCase . height ) ,
149+ ] ;
150+ }
151+
152+ function parseLength ( value , percentageBasis ) {
153+ if ( value . endsWith ( "%" ) ) {
154+ return parseFloat ( value ) * percentageBasis / 100 ;
155+ }
156+
157+ return parseFloat ( value ) ;
158+ }
159+
160+ function resolve ( testCase , box ) {
161+ const cs = getComputedStyle ( box ) ;
162+
163+ // Corners to array[2] of radii
164+ const radii = {
165+ topLeft : parseCorner ( cs . borderTopLeftRadius , testCase ) ,
166+ topRight : parseCorner ( cs . borderTopRightRadius , testCase ) ,
167+ bottomLeft : parseCorner ( cs . borderBottomLeftRadius , testCase ) ,
168+ bottomRight : parseCorner ( cs . borderBottomRightRadius , testCase ) ,
169+ } ;
170+
171+ // Normalize radii that add up to > 100%
172+ const f = Math . min (
173+ testCase . width / ( radii . topLeft [ 0 ] + radii . topRight [ 0 ] ) ,
174+ testCase . width / ( radii . bottomLeft [ 0 ] + radii . bottomRight [ 0 ] ) ,
175+ testCase . height / ( radii . topLeft [ 1 ] + radii . bottomLeft [ 1 ] ) ,
176+ testCase . height / ( radii . topRight [ 1 ] + radii . bottomRight [ 1 ] )
177+ ) ;
178+ if ( f < 1 ) {
179+ for ( let corner in radii ) {
180+ radii [ corner ] = radii [ corner ] . map ( v => v * f ) ;
181+ }
182+ }
183+
184+ let r = {
185+ topLeft : radii . topLeft ,
186+ topRight : radii . topRight ,
187+ bottomLeft : radii . bottomLeft ,
188+ bottomRight : radii . bottomRight ,
189+ } ;
190+ const algorithm = document . forms [ 0 ] . elements . algorithm . value ;
191+
192+ let { width, height} = testCase ;
193+ let spreadWidth = width + testCase . spread * 2 ;
194+ let spreadHeight = height + testCase . spread * 2 ;
195+
196+ let percentageSameAxis = { } ;
197+
198+ for ( let corner in r ) {
199+ let c = r [ corner ] ;
200+ let [ rx , ry ] = c ;
201+
202+ let px = rx / width ;
203+ let py = ry / height ;
204+
205+ percentageSameAxis [ corner ] = [ px * spreadWidth , py * spreadHeight ] ;
206+ }
207+
208+ let currentSpec = { } ;
209+
210+ for ( let corner in r ) {
211+ currentSpec [ corner ] = r [ corner ] . map ( value => {
212+ if ( value >= testCase . spread ) {
213+ return value + testCase . spread ;
214+ }
215+ let r = value / testCase . spread ;
216+ return value + testCase . spread * ( 1 + ( r - 1 ) ** 3 ) ;
217+ } ) ;
218+ }
219+
220+ if ( algorithm === "increase-by-spread" ) {
221+ for ( let corner in r ) {
222+ r [ corner ] = r [ corner ] . map ( v => v + testCase . spread ) ;
223+ }
224+ }
225+ else if ( algorithm === "old-spec" ) {
226+ for ( let corner in r ) {
227+ let c = r [ corner ] ;
228+ r [ corner ] = c [ 0 ] + c [ 1 ] === 0 ? [ 0 , 0 ] : [ c [ 0 ] + testCase . spread , c [ 1 ] + testCase . spread ] ;
229+ }
230+ }
231+ else if ( algorithm === "percentage-same-axis" ) {
232+ r = percentageSameAxis ;
233+ }
234+ else if ( algorithm === "current-spec" ) {
235+ r = currentSpec ;
236+ }
237+ else if ( algorithm === "elika" ) {
238+ let { width, height} = testCase ;
239+ let spreadWidth = width + testCase . spread * 2 ;
240+ let spreadHeight = height + testCase . spread * 2 ;
241+
242+ let straights = {
243+ top : width - r . topLeft [ 0 ] - r . topRight [ 0 ] ,
244+ bottom : width - r . bottomLeft [ 0 ] - r . bottomRight [ 0 ] ,
245+ left : height - r . topLeft [ 1 ] - r . bottomLeft [ 1 ] ,
246+ right : height - r . topRight [ 1 ] - r . bottomRight [ 1 ] ,
247+ }
248+
249+ function getStraightSegment ( corner , axis ) {
250+ /*
251+ Example straight segment returned:
252+ topLeft, 0 --> top
253+ topLeft, 1 --> left
254+ bottomRight, 0 --> bottom
255+ bottomRight, 1 --> right
256+ */
257+
258+ let parts = corner . split ( / (? = [ A - Z ] ) / ) . map ( part => part . toLowerCase ( ) ) ;
259+ return straights [ parts [ axis ] ] ;
260+ }
261+
262+ for ( let corner in r ) {
263+ r [ corner ] = r [ corner ] . map ( ( value , axis ) => {
264+ let straight = getStraightSegment ( corner , axis ) ;
265+ let ratio = straight / value ;
266+ ratio = Math . min ( ratio , 1 ) ;
267+ let ret = ratio * currentSpec [ corner ] [ axis ] + ( 1 - ratio ) * percentageSameAxis [ corner ] [ axis ] ;
268+
269+ return Math . min ( ret , straight + value + testCase . spread ) ;
270+ } ) ;
271+
272+ }
273+ }
274+
275+ return `${ r . topLeft [ 0 ] } px ${ r . topRight [ 0 ] } px ${ r . bottomRight [ 0 ] } px ${ r . bottomLeft [ 0 ] } px / ${ r . topLeft [ 1 ] } px ${ r . topRight [ 1 ] } px ${ r . bottomRight [ 1 ] } px ${ r . bottomLeft [ 1 ] } px` ;
276+ }
277+
278+ show ( ) ;
279+ document . querySelector ( "form" ) . addEventListener ( "change" , evt => show ( { incremental : true } ) ) ;
280+
281+ </ script >
282+
283+ </ body >
284+ </ html >
0 commit comments