A Pen by Andreas Borgen on CodePen.
Last active
September 21, 2018 14:56
-
-
Save Sphinxxxx/f03366b18a3af2515165b2b604eaac84 to your computer and use it in GitHub Desktop.
border-radius Playground
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script> | |
window.onerror = function (msg, url, linenumber) { | |
//alert('Error: ' + msg + '\nURL: ' + url + '\nLine Number: ' + linenumber); | |
}; | |
</script> | |
<script src="https://unpkg.com/vue@2"></script> | |
<script src="https://unpkg.com/drag-tracker@1"></script> | |
<h1><code>border-radius</code> playground</h1> | |
<div id="app"> | |
<div id="box" :style="sizeStyle"> | |
<div id="preview" :style="boxStyle"></div> | |
<div v-for="(corner, key) in corners" :data-corner="key" class="corner" :style="getCornerStyle(key)"> | |
<div class="resizer-corner" :data-corner="key" data-draggable></div> | |
</div> | |
<div id="resizer" data-draggable>⇲</div> | |
</div> | |
<div id="unit"> | |
<!-- //https://stackoverflow.com/questions/45187048/vue-binding-radio-to-boolean --> | |
<label> | |
<input name="unit" type="radio" v-model="pct" :value="true"/><span>%</span> | |
</label> | |
<label> | |
<input name="unit" type="radio" v-model="pct" :value="false"/><span>px</span> | |
</label> | |
</div> | |
<ul id="result" class="code"> | |
<li> | |
<span>width: </span> | |
<span>{{sizeStyle.width}};</span> | |
</li> | |
<li> | |
<span>height:</span> | |
<span>{{sizeStyle.height}};</span> | |
</li> | |
<li> | |
<span>border-radius:</span> | |
<span> | |
<span class="border-aligned"><pre> {{ borderRadiiAligned[0].join(' ') }}</pre> /</span> | |
<span class="border-aligned"><pre> {{ borderRadiiAligned[1].join(' ') }}</pre>;</span> | |
</span> | |
</li> | |
</ul> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
console.clear(); | |
function clamp(val, min, max) { | |
return Math.max(min, Math.min(val, max)); | |
} | |
String.prototype.padStart = String.prototype.padStart || function(targetLength, padString) { | |
let result = this; | |
while(result.length < targetLength) { | |
result = padString + result; | |
} | |
return result; | |
}; | |
new Vue({ | |
el: '#app', | |
mounted() { | |
dragTracker({ | |
container: document.querySelector('#box'), | |
selector: '[data-draggable]', | |
//The .resizer needs to be dragged outside.. | |
// dragOutside: false, | |
callback: this.onResize, | |
}); | |
}, | |
data: { | |
pct: true, | |
box: { | |
size: [ | |
clamp(window.innerWidth, 0, 900) * .9, | |
350 | |
], | |
}, | |
corners: { | |
'top|left': { size: [.4, .5] }, | |
'top|right': { size: [.6, .45] }, | |
'bottom|left': { size: [.7, .5] }, | |
'bottom|right': { size: [.3, .45] }, | |
}, | |
//Clockwise around the box, starting at the top-left corner (just like the border-radius syntax): | |
keysCW: ['top|left', 'top|right', 'bottom|right', 'bottom|left'], | |
}, | |
computed: { | |
//CSS for the box size: | |
sizeStyle() { | |
return { | |
width: this.box.size[0] + 'px', | |
height: this.box.size[1] + 'px', | |
}; | |
}, | |
//CSS for the box' border-radius: | |
boxStyle() { | |
const style = { | |
borderRadius: this.borderRadii.map(r => r.join(' ')).join(' / '), | |
}; | |
return style; | |
}, | |
borderRadii() { | |
let radii1 = [], | |
radii2 = []; | |
const sizes = this.keysCW.map(key => this.printCornerSize(this.findCorner(key))); | |
sizes.forEach(s => { | |
radii1.push(s[0]); | |
radii2.push(s[1]); | |
}); | |
return [radii1, radii2]; | |
}, | |
borderRadiiAligned() { | |
let [radii1, radii2] = this.borderRadii, | |
maxLen = Math.max.apply(null, radii1.concat(radii2).map(x => x.length)); | |
//console.log(maxLen); | |
radii1 = radii1.map(x => x.padStart(maxLen, ' ')); | |
radii2 = radii2.map(x => x.padStart(maxLen, ' ')); | |
return [radii1, radii2] | |
}, | |
}, | |
watch: { | |
//Calculate the new border sizes when the unit (% <-> px) changes: | |
pct(isPct, wasPct) { | |
const [boxW, boxH] = this.box.size; | |
this.keysCW.forEach(key => { | |
const corner = this.findCorner(key), | |
[w, h] = corner.size; | |
const ratio = wasPct ? corner.size : [w / boxW, h / boxH], | |
newSize = isPct ? ratio : [ratio[0] * boxW, ratio[1] * boxH]; | |
corner.size = newSize; | |
}); | |
} | |
}, | |
methods: { | |
//CSS for positioning the four border handles: | |
getCornerStyle(key) { | |
const keyParts = key.split('|'), | |
corner = this.findCorner(key), | |
[width, height] = this.printCornerSize(corner); | |
const style = { | |
width, | |
height, | |
}; | |
style[keyParts[0]] = 0; | |
style[keyParts[1]] = 0; | |
return style; | |
}, | |
printCornerSize(corner, round = true, units = true) { | |
const pct = this.pct; | |
function formatNumber(num) { | |
let res = num; | |
if(pct) { res *= 100; } | |
if(round) { res = Math.round(res); } | |
if(units) { res += pct ? '%' : 'px'; } | |
return res; | |
} | |
return corner.size.map(formatNumber); | |
}, | |
onResize(elm, pos) { | |
const cornerKey = elm.parentElement.dataset.corner; | |
if(cornerKey) { | |
this.resizeCorner(cornerKey, pos); | |
} | |
//Box resize: | |
else { | |
this.resizeBox(pos); | |
} | |
}, | |
//User resize - box size: | |
resizeBox(size) { | |
if(size) { | |
this.box.size = [ | |
Math.max(10, size[0]), | |
Math.max(10, size[1]), | |
]; | |
} | |
//If the border-radius uses fixed px values, see if some of the corners butt against each other. | |
//Pair up every adjacent corner and let pushCorners() have a look at them: | |
if(!this.pct) { | |
const keys = this.keysCW; | |
for(let i = 0; i < keys.length; i++) { | |
const corner1 = this.findCorner(keys[i]), | |
corner2 = this.findCorner(keys[(i + 1) % keys.length]); | |
this.pushCorners(corner1, corner2, (i % 2) === 0); | |
} | |
} | |
}, | |
//User resize - border handle: | |
resizeCorner(key, pos) { | |
const [boxW, boxH] = this.box.size, | |
x = clamp(pos[0], 0, boxW), | |
y = clamp(pos[1], 0, boxH); | |
//Calculate current corner's size: | |
const corner = this.findCorner(key), | |
keyParts = key.split('|'); | |
let cornerW = (keyParts[1] === 'left') ? x : (boxW - x), | |
cornerH = (keyParts[0] === 'top') ? y : (boxH - y); | |
if(this.pct) { | |
cornerW /= boxW; | |
cornerH /= boxH; | |
} | |
corner.size = [cornerW, cornerH]; | |
//See if the current corner pushes away any of the adjacent corners: | |
//Push left/right: | |
const otherHoriz = this.findCorner(key, true, false); | |
this.pushCorners(corner, otherHoriz, true, otherHoriz); | |
//Push up/down: | |
const otherVert = this.findCorner(key, false, true); | |
this.pushCorners(corner, otherVert, false, otherVert); | |
}, | |
//Shrink colliding corners to avoid overlapping border-radius values. | |
//While overlapping values are valid CSS, any such border-radius can be expressed | |
//(and presented cleaner on-screen) with non-overlapping values *) | |
// | |
// *) ..I think | |
pushCorners(corner1, corner2, horizontally, squeezedCorner) { | |
function getLen(size) { | |
return horizontally ? size[0] : size[1]; | |
} | |
function setLen(size, len) { | |
//https://vuejs.org/2016/02/06/common-gotchas/ | |
//https://vuejs.org/v2/guide/list.html#Caveats | |
// horizontally ? (size[0] = len) : (size[1] = len); | |
Vue.set(size, horizontally ? 0 : 1, len); | |
} | |
const lenAvail = (this.pct) ? 1 : getLen(this.box.size); | |
const len1 = getLen(corner1.size), | |
len2 = getLen(corner2.size), | |
lenTot = len1 + len2; | |
//Nothing to push aside: | |
if(lenTot <= lenAvail) { return; } | |
//Shrink one to give room for the other (user resizing a corner) | |
if(squeezedCorner) { | |
const [pri1, pri2] = (squeezedCorner === corner2) ? [corner1, corner2] : [corner2: corner1]; | |
//Give pri1 the length it wants (but within the box).. | |
const pri1Len = clamp(getLen(pri1.size), 0, lenAvail); | |
setLen(pri1.size, pri1Len); | |
//..and give the rest to pri2: | |
setLen(pri2.size, lenAvail - pri1Len); | |
} | |
//Shrink both equally (user resizing the box): | |
else { | |
const squeezeFactor = lenAvail / lenTot; | |
setLen(corner1.size, getLen(corner1.size) * squeezeFactor); | |
setLen(corner2.size, getLen(corner2.size) * squeezeFactor); | |
} | |
}, | |
findCorner(key, flipHoriz, flipVert) { | |
function flip(part) { | |
return ({ | |
top: 'bottom', | |
bottom: 'top', | |
left: 'right', | |
right: 'left', | |
})[part]; | |
} | |
const parts = key.split('|'); | |
if(flipHoriz) { parts[1] = flip(parts[1]); } | |
if(flipVert) { parts[0] = flip(parts[0]); } | |
const otherKey = parts.join('|'); | |
return this.corners[otherKey]; | |
}, | |
}, //methods | |
}); //Vue |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
text-align: center; | |
font-family: monospace; | |
font-size: 18px; | |
ul { | |
list-style: none; | |
margin: 0; | |
padding: 0; | |
} | |
label, label input { | |
cursor: pointer; | |
} | |
code, .code { | |
display: inline-block; | |
padding: .2em .5em; | |
box-sizing: border-box; | |
border: 1px solid skyblue; | |
background: aliceblue; | |
} | |
} | |
$c-tl: rgba(tomato, .4); | |
$c-tr: rgba(lime, .4); | |
$c-br: rgba(dodgerblue, .5); | |
$c-bl: rgba(gold, .4); | |
#unit { | |
display: table; | |
margin: auto; | |
margin-top: 2em; | |
label { | |
margin: 0 .5em; | |
} | |
} | |
#box { | |
position: relative; | |
margin: auto; | |
#preview { | |
position: absolute; | |
top:0; left:0; | |
width: 100%; | |
height: 100%; | |
background: gainsboro; | |
//outline: 1px dashed silver; | |
border: .2em solid black; | |
box-sizing: border-box; | |
} | |
[data-draggable] { | |
position: absolute; | |
line-height: 1; | |
text-align: center; | |
} | |
#resizer { | |
top: 100%; left: 100%; | |
width: 1em; | |
height: 1em; | |
font-size: 3rem; | |
cursor: nwse-resize; | |
background: whitesmoke; | |
outline: 1px solid gainsboro; | |
overflow: hidden; | |
} | |
} | |
#result { | |
text-align: left; | |
span { | |
display: inline-block; | |
vertical-align: top; | |
} | |
pre { | |
display: inline-block; | |
margin: 0; | |
} | |
.border-aligned { | |
display: table; | |
white-space: nowrap; | |
pre { | |
background: linear-gradient(90deg, $c-tl 25%, $c-tr 0, $c-tr 50%, $c-br 0, $c-br 75%, $c-bl 0); | |
} | |
} | |
} | |
.corner { | |
position: absolute; | |
width: 100px; | |
height: 100px; | |
background: currentColor; | |
//overflow: hidden; | |
.resizer-corner { | |
z-index: 99; | |
font-size: 3rem; | |
background: lime; | |
cursor: move; | |
//Trick to make dragging correct without recalcing coordinates: | |
width: 0; | |
height: 0; | |
&::before { | |
content: ''; | |
display: block; | |
position: absolute; | |
width: 1em; | |
height: 1em; | |
background: currentColor url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300' viewBox='-1 -1 24 24'%3E%3Cdefs%3E%3Cpath id='a' d='M10 11V3H8l3-3 3 3h-2v8'/%3E%3C/defs%3E%3Cg opacity='.7'%3E%3Cuse href='%23a'/%3E%3Cuse href='%23a' transform='rotate(90 11 11)'/%3E%3Cuse href='%23a' transform='rotate(180 11 11)'/%3E%3Cuse href='%23a' transform='rotate(270 11 11)'/%3E%3C/g%3E%3C/svg%3E") 85% 85% / 70% no-repeat; | |
border: dashed rgba(black, .6); | |
border-radius: 100% 0 0 0; | |
border-width: 4px 0 0 4px; | |
box-sizing: border-box; | |
} | |
} | |
&[data-corner="top|left"] { | |
color: $c-tl; | |
.resizer-corner { | |
bottom:0; right:0; | |
&::before { | |
bottom: 100%; | |
right: 100%; | |
} | |
} | |
} | |
&[data-corner="top|right"] { | |
color: $c-tr; | |
.resizer-corner { | |
bottom:0; left:0; | |
&::before { | |
bottom: 100%; | |
transform: rotate(90deg); | |
} | |
} | |
} | |
&[data-corner="bottom|right"] { | |
color: $c-br; | |
.resizer-corner { | |
top:0; left:0; | |
&::before { | |
top: 100%; | |
transform: rotate(180deg); | |
} | |
} | |
} | |
&[data-corner="bottom|left"] { | |
color: $c-bl; | |
.resizer-corner { | |
top:0; right:0; | |
&::before { | |
top: 100%; | |
right: 100%; | |
transform: rotate(-90deg); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment