<div class="outline"></div>
<div id="controls"></div>
<div id="block"></div>
<textarea id="result">
</textarea>
#block {
filter: url(#smooth-edges) ;
--border-color: #fffa;
--shadow-color: #000;
--top-protrusion-width: 30%;
--top-protrusion-height: 30px;
--bottom-protrusion-width: 40%;
--bottom-protrusion-height: 20px;
min-height: calc(
var(--top-protrusion-height)
+ var(--bottom-protrusion-height)
+ var(--border-width, 0px) * 2
+ 20px
);
}
#block:after {
content: "";
display: block;
bottom: var(--filter-margin-fix, 0);
left: var(--filter-margin-fix, 0);
top: var(--filter-margin-fix, 0);
right: var(--filter-margin-fix, 0);
background: linear-gradient(#fff7,#000);
clip-path: polygon(
calc(100% - var(--bottom-protrusion-width)) 0%,
100% 0,
100% calc(100% - var(--bottom-protrusion-height)),
var(--top-protrusion-width) calc(100% - var(--bottom-protrusion-height)),
var(--top-protrusion-width) 100%,
0 100%,
0 var(--top-protrusion-height),
calc(100% - var(--bottom-protrusion-width)) var(--top-protrusion-height)
);
position: absolute;
z-index: -1;
}
body {
padding: 10px;
margin: 0;
background: linear-gradient(90deg, purple, red)
}
.outline, #block {
margin: 0 4vw;
position: absolute;
top: 50px;
height: calc(90vh - 50px);
width: 90vw;
}
.outline {
outline:4px solid #00f;
}
.test {
position: absolute;
width: 150px;
height: 60px;
left: 100px;
top: 5px;
background: url('https://images.unsplash.com/photo-1561736778-92e52a7769ef?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
border: 8px solid var(--color-highlight);
border-radius: 25px;
}
:root {
--color-highlight: #71c150;
}
input, textarea {
border: 1px solid;
color: #fff;
background: #000;
}
#result {
margin-left: 4vw;
margin-top: 90vh;
height: 90vh;
width: 90vw;
}
#controls {
margin-left: 4vw;
color: #fff;
background: #000;
}
const GAUSSIAN_BLUR_SPREAD = 2.8; // ~3
function createFilter({borderRadius, borderWidth, shadow}) {
const [
shadowX,
shadowY,
shadowDeviation
] = Object.assign([0,0,0], shadow.match(/\d+(\.\d+)?/g)?.map(Number)) ?? [8, 8, 0];
const blur = parseInt(borderRadius, 10);
const border = parseInt(borderWidth, 10);
const erode = blur;
const margin = -(erode - border / 2);
let tableLength = Math.ceil(blur * GAUSSIAN_BLUR_SPREAD);
let tableSpace = (tableLength - border) / 2;
let ceilTableSpace = Math.ceil(tableSpace);
let floorTableSpace = Math.floor(tableSpace);
//if (tableSpace % 2) {
// tableLength *= 2;
//} else {
// tableSpace = tableSpace / 2 | 0;
//}
function arr(length, fill) {
return Array.from({length}, () => fill);
}
let borderTable = [
arr(floorTableSpace, 0),
arr(tableLength- ceilTableSpace - floorTableSpace , 1),
arr(ceilTableSpace, 0),
].join(' ');
let imageTable = [
arr(floorTableSpace, 1),
arr(tableLength - ceilTableSpace - floorTableSpace , 1),
arr(ceilTableSpace, 0),
].join(' ');
let shadowTable = [
arr(floorTableSpace, 0),
arr(tableLength - ceilTableSpace - floorTableSpace , 1),
arr(ceilTableSpace, 1),
].join(' ');
let filter = `
<!-- removing opacity -->
<feComponentTransfer in="SourceAlpha">
<feFuncA type="linear" slope="1000"/>
</feComponentTransfer>
<!-- shrinking so we can crop inside -->
<feMorphology operator="erode" radius="${erode}"/>
<!--bluring so we have smothed edges -->
<feGaussianBlur stdDeviation="${blur}" result="blur"></feGaussianBlur>
<!--border-->
<feFlood flood-color="var(--border-color)" result="borderColor"/>
<feComponentTransfer in="blur" result="border">
<feFuncA type="table" tableValues="${borderTable}"/>
</feComponentTransfer>
<feComposite in="borderColor" operator="in" result="border"/>
<!--shadow-->
<feComponentTransfer in="blur" result="cropedShadow">
<feFuncA type="table" tableValues="${shadowTable}"/>
</feComponentTransfer>
<feDropShadow dx="${shadowX}" dy="${shadowY}" stdDeviation="${shadowDeviation}" flood-color="var(--shadow-color)" />
<feComposite in2="cropedShadow" operator="out" result="shadow" />
<!--image-->
<feComponentTransfer in="blur">
<feFuncA type="table" tableValues="${imageTable}"/>
</feComponentTransfer>
<feComposite in="SourceGraphic" operator="out" result="cropedImage"/>
<feMerge>
<feMergeNode in="shadow" />
<feMergeNode in="border" />
<feMergeNode in="cropedImage" />
</feMerge>
`
block.style.setProperty('--filter-margin-fix', `${margin}px`);
block.style.setProperty('--border-width', `${border}px`);
block.innerHTML = rawPad`
<svg style="visibility: hidden; position: absolute;" width="0" height="0" xmlns="http://www.w3.org/2000/svg" version="1.1" color-interpolation-filters="sRGB">
<defs>
<filter id="smooth-edges">
${filter}
</filter>
</defs>
</svg>
`;
result.value = block.outerHTML;
}
function trimLinesLeft(str) {
return str.replace(/^([^\S\r\n]*[\r\n]+)+/, '');
}
function trimLinesRight(str) {
return str.replace(/([\r\n]+[^\S\r\n]*)+$/, '');
}
function trimLines(str) {
return trimLinesRight(trimLinesLeft(str));
}
function rawPad({raw}, args) {
const trim = true;
const {length} = args;
const strings = [raw];
//strings[0] = trimLinesLeft(strings[0]);
//strings[strings.length-1] = trimLinesRight(strings[strings.length-1]);
return strings.reduce((acc, value, i) => {
let arg = i <= length ? trimLines(String(args[i-1])) : '';
const space = acc.slice(acc.lastIndexOf('\n') + 1).replace(/[^\t]/g, ' ');
let lines = arg.split(/(\r?\n)/);
let linesLength = lines.length;
if (space && linesLength > 1) {
if (trim) {
const line = lines[0];
let toTrimLength = line.match(/^\s*/)[0].length;
if (toTrimLength) {
for(let i = 2; i < lines.length; i += 2) {
const lineLength = lines[i].length;
if (toTrimLength > lineLength)
toTrimLength = lineLength;
for(let j = 0; j < toTrimLength; j++) {
if (line[j] !== lines[i][j]) {
toTrimLength = j;
break;
}
}
if (!toTrimLength) break;
}
if (toTrimLength) {
for(let i = 0; i < lines.length; i += 2) {
lines[i] = lines[i].slice(toTrimLength);
}
}
}
}
for(let i = 2; i < lines.length; i += 2) {
lines[i] = space + lines[i];
}
return acc + lines.join('') + value;
}
return acc + arg + value;
})
}
function drawControls() {
const controls = [
{
type: 'text',
name: 'shadow',
value: '8 8 0'
},
{
type: 'number',
name: 'borderRadius',
value: '8',
valid({borderWidth}) {
const min = (+borderWidth + 1) / 2 | 0;
if (+this.value < min) {
this.value = min;
}
}
},
{
type: 'number',
name: 'borderWidth',
value: '4',
valid({borderRadius}) {
const max = borderRadius * 2 - 1;
const min = 0;
if (+this.value > max) {
this.value = max;
}
if (+this.value < min) {
this.value = min;
}
}
}
];
document.getElementById('controls').insertAdjacentHTML(
'afterbegin',
controls.map(({name, type, value}) => rawPad`
<label>
${name}
<input type="${type}" id=${name} value="${value}"/>
</label>
`).join('\n'));
const obj = {};
controls.forEach(({name, value, valid}) => {
obj[name] = value;
document.getElementById(name).addEventListener('input', ({currentTarget}) => {
valid?.call(currentTarget, obj);
obj[name] = currentTarget.value;
createFilter(obj);
});
});
createFilter(obj);
}
drawControls()
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.