Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Last active September 21, 2018 14:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Sphinxxxx/f03366b18a3af2515165b2b604eaac84 to your computer and use it in GitHub Desktop.
Save Sphinxxxx/f03366b18a3af2515165b2b604eaac84 to your computer and use it in GitHub Desktop.
border-radius Playground
<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:&nbsp;</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>
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
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