[ Note: Looking for a cross-browser solution that does not flash the body's background momentarily between each wave of goo as seen in ccprog's answer; Ideally, the solution should not involve waiting until the end of the first wave to begin displaying the second wave, so that both waves can run concurrently. I am willing to forego dynamically randomized goop in order for an ideal solution. ]
Does anybody know how I can make the second wave of orange goo (.goo-two
) "cut through" the first wave of brown goo (.goo-one
) and the skyblue container (.goo-container
) to show or expose the red body element (body
) or, for that matter, any other element below it in the stacking context? Is it possible?
Notably, the reason I have given the container (.goo-container
) a solid background is because I was using this to cover up the loading process of the rest of the website, whereby I was hoping the orange goo (.goo-two
) can be used to reveal the content. It gets even trickier because the orange goo starts dripping before the brown goo finishes, which would be the perfect time to change the background of the contianer (.goo-container
) from skyblue
to transparent
, although a semi-transparent gradient as the background can likely be used to still achieve this. (Either that or something altogether different like duplicating the orange layer and use one to clip the brown path and the other to clip skyblue layer.)
Any ideas?
const
gooCont = document.querySelector('div.goo-container'),
gooOne = gooCont.querySelector('div.goo-one'),
gooTwo = gooCont.querySelector('div.goo-two'),
rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
gooCont.style.setProperty('--translateY', `translateY(-${innerWidth * 0.21 / innerHeight * 100 + 100}%)`)
generateGoo(gooOne)
function generateGoo(goo) {
const
randQty = rand(20,30),
unit = innerWidth / (randQty - 1) / innerWidth * 100
if (getComputedStyle(goo).display === 'none') goo.style.display = 'block'
for (let i = 0; i < randQty; i++) {
const
div = document.createElement('div'),
minWidthPx = innerWidth < 500 ? innerWidth * 0.1 : innerWidth * 0.05,
minMaxWidthPx = innerWidth < 500 ? innerWidth * 0.2 : innerWidth * 0.1,
widthPx = rand(minWidthPx, minMaxWidthPx),
widthPerc = widthPx / innerWidth * 100,
heightPx = rand(widthPx / 2, widthPx * 3),
heightPerc = heightPx / gooCont.getBoundingClientRect().height * 100,
translateY = rand(45, 70),
targetTranslateY = rand(15, 100),
borderRadiusPerc = rand(40, 50)
div.style.width = widthPerc + '%'
div.style.height = heightPerc + '%'
div.style.left = i * unit + '%'
div.style.transform = `translate(-50%, ${translateY}%)`
div.style.borderRadius = borderRadiusPerc + '%'
div.setAttribute('data-translate', targetTranslateY)
goo.appendChild(div)
}
goo.style.transform = `translateY(0)`
goo.childNodes.forEach(
v => v.style.transform = `translateY(${v.getAttribute('data-translate')}%)`
)
}
setTimeout(() => {
gooTwo.innerHTML = ''
generateGoo(gooTwo)
}, 2300)
html,
body {
width: 100%;
height: 100%;
margin: 0;
background: red;
}
div.goo-container {
--translateY: translateY(-165%);
z-index: 1;
width: 100%;
height: 100%;
position: fixed;
overflow: hidden;
background: skyblue;
}
div.goo-container > div.goo-one,
div.goo-container > div.goo-two {
width: 100%;
height: 100%;
position: absolute;
transform: var(--translateY);
filter: url('#goo-filter');
background: #5b534a;
transition: transform 2.8s linear;
}
div.goo-container > div.goo-one > div,
div.goo-container > div.goo-two > div {
position: absolute;
bottom: 0;
background: #5b534a;
transition: transform 2.8s linear;
}
div.goo-container > div.goo-two {
display: none;
transition: transform 2.8s linear;
}
div.goo-container > div.goo-two,
div.goo-container > div.goo-two > div {
background: orange;
}
svg {
/* Prevents effect on Firefox */
/* display: none; */
}
<div class='goo-container'>
<div class='goo-one'></div>
<div class='goo-two'></div>
</div>
<svg xmlns='' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
[ Note: Looking for a cross-browser solution that does not flash the body's background momentarily between each wave of goo as seen in ccprog's answer; Ideally, the solution should not involve waiting until the end of the first wave to begin displaying the second wave, so that both waves can run concurrently. I am willing to forego dynamically randomized goop in order for an ideal solution. ]
Does anybody know how I can make the second wave of orange goo (.goo-two
) "cut through" the first wave of brown goo (.goo-one
) and the skyblue container (.goo-container
) to show or expose the red body element (body
) or, for that matter, any other element below it in the stacking context? Is it possible?
Notably, the reason I have given the container (.goo-container
) a solid background is because I was using this to cover up the loading process of the rest of the website, whereby I was hoping the orange goo (.goo-two
) can be used to reveal the content. It gets even trickier because the orange goo starts dripping before the brown goo finishes, which would be the perfect time to change the background of the contianer (.goo-container
) from skyblue
to transparent
, although a semi-transparent gradient as the background can likely be used to still achieve this. (Either that or something altogether different like duplicating the orange layer and use one to clip the brown path and the other to clip skyblue layer.)
Any ideas?
const
gooCont = document.querySelector('div.goo-container'),
gooOne = gooCont.querySelector('div.goo-one'),
gooTwo = gooCont.querySelector('div.goo-two'),
rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
gooCont.style.setProperty('--translateY', `translateY(-${innerWidth * 0.21 / innerHeight * 100 + 100}%)`)
generateGoo(gooOne)
function generateGoo(goo) {
const
randQty = rand(20,30),
unit = innerWidth / (randQty - 1) / innerWidth * 100
if (getComputedStyle(goo).display === 'none') goo.style.display = 'block'
for (let i = 0; i < randQty; i++) {
const
div = document.createElement('div'),
minWidthPx = innerWidth < 500 ? innerWidth * 0.1 : innerWidth * 0.05,
minMaxWidthPx = innerWidth < 500 ? innerWidth * 0.2 : innerWidth * 0.1,
widthPx = rand(minWidthPx, minMaxWidthPx),
widthPerc = widthPx / innerWidth * 100,
heightPx = rand(widthPx / 2, widthPx * 3),
heightPerc = heightPx / gooCont.getBoundingClientRect().height * 100,
translateY = rand(45, 70),
targetTranslateY = rand(15, 100),
borderRadiusPerc = rand(40, 50)
div.style.width = widthPerc + '%'
div.style.height = heightPerc + '%'
div.style.left = i * unit + '%'
div.style.transform = `translate(-50%, ${translateY}%)`
div.style.borderRadius = borderRadiusPerc + '%'
div.setAttribute('data-translate', targetTranslateY)
goo.appendChild(div)
}
goo.style.transform = `translateY(0)`
goo.childNodes.forEach(
v => v.style.transform = `translateY(${v.getAttribute('data-translate')}%)`
)
}
setTimeout(() => {
gooTwo.innerHTML = ''
generateGoo(gooTwo)
}, 2300)
html,
body {
width: 100%;
height: 100%;
margin: 0;
background: red;
}
div.goo-container {
--translateY: translateY(-165%);
z-index: 1;
width: 100%;
height: 100%;
position: fixed;
overflow: hidden;
background: skyblue;
}
div.goo-container > div.goo-one,
div.goo-container > div.goo-two {
width: 100%;
height: 100%;
position: absolute;
transform: var(--translateY);
filter: url('#goo-filter');
background: #5b534a;
transition: transform 2.8s linear;
}
div.goo-container > div.goo-one > div,
div.goo-container > div.goo-two > div {
position: absolute;
bottom: 0;
background: #5b534a;
transition: transform 2.8s linear;
}
div.goo-container > div.goo-two {
display: none;
transition: transform 2.8s linear;
}
div.goo-container > div.goo-two,
div.goo-container > div.goo-two > div {
background: orange;
}
svg {
/* Prevents effect on Firefox */
/* display: none; */
}
<div class='goo-container'>
<div class='goo-one'></div>
<div class='goo-two'></div>
</div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
Share
Improve this question
edited Dec 2, 2020 at 22:50
oldboy
asked Nov 23, 2020 at 21:01
oldboyoldboy
5,9747 gold badges42 silver badges99 bronze badges
19
- I may have an idea but it's support is far in the futur ... – Temani Afif Commented Nov 23, 2020 at 21:47
- @TemaniAfif is it not possible to do with some sort of clip or mask? i also just thought of the idea of duplicating the orange drip layer, using one to clip the brown goo and one to clip the container. im open to any of your ideas :) – oldboy Commented Nov 23, 2020 at 21:49
- to use clip-path or mask you need to build you goo layer using SVG so you can consider that SVG as the mask. With multiple div you cannot consider mask or clip-path – Temani Afif Commented Nov 23, 2020 at 21:51
- 1 entirely SVG, yes it would be possible in this case but probably there is other ideas too, let's wait for more inputs ;) – Temani Afif Commented Nov 23, 2020 at 22:02
- 1 @CalebTaylor i will be checking as much as i can before actually implementing it. thanks for pointing out the issue on Firefox. man, i feel like so many things, if they are not pletely standard and basic, are still so buggy these days :( – oldboy Commented Dec 3, 2020 at 1:00
3 Answers
Reset to default 7I am pretty sure this is not the optimal variant, but it seems to work out, at least in Firefox. Chrome has some issues with the initial frames of each part of the animation.
- I have rewritten the gooey filter code a bit to improve readability while maintaining the same effect. For an explanation, see my article.
- Only
.goo-one
and the child divs get a background color. This makes it possible for.goo-two
to bee transparent. - The two parts get different filters, but the filter region is increased vertically for both to reach the bottom of the screen at the start of the transition.
- The first filter has the skyblue color as a background fill.
- The second filter has a brown fill, but its application is inverted: it is shown only outside the goo areas, leaving the inside area empty. The div rectangles making up the goo area do not span the entire
.gooTwo
. To also fill (and after inversion, empty) the top part, an extra<div class="first">
is needed. - At the start of the transition for the second goo part, the upper filter region limit is set below the lower screen boundary. This hides the skyblue background at the same time the second goo part bees visible.
- Note the slight change in the CSS for the
svg
element for better browser patibility. - Just as a proof of concept, some content is added inside the container div. It shows that a
pointer-event: none
is needed; otherwise no interaction with the page would be possible.
const
gooCont = document.querySelector('div.goo-container'),
gooOne = gooCont.querySelector('div.goo-one'),
gooTwo = gooCont.querySelector('div.goo-two'),
filterOne = document.querySelector('#goo-filter-one')
rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
gooCont.style.setProperty('--translateY', `translateY(-${innerWidth * 0.21 / innerHeight * 100 + 100}%)`)
generateGoo(gooOne)
function generateGoo(goo) {
const
randQty = rand(20,30),
unit = innerWidth / (randQty - 1) / innerWidth * 100
if (getComputedStyle(goo).display === 'none') goo.style.display = 'block'
goo.removeAttribute('y')
for (let i = 0; i < randQty; i++) {
const
div = document.createElement('div'),
minWidthPx = innerWidth < 500 ? innerWidth * 0.1 : innerWidth * 0.05,
minMaxWidthPx = innerWidth < 500 ? innerWidth * 0.2 : innerWidth * 0.1,
widthPx = rand(minWidthPx, minMaxWidthPx),
widthPerc = widthPx / innerWidth * 100,
heightPx = rand(widthPx / 2, widthPx * 3),
heightPerc = heightPx / gooCont.getBoundingClientRect().height * 100,
translateY = rand(45, 70),
targetTranslateY = rand(15, 100),
borderRadiusPerc = rand(40, 50)
div.style.width = widthPerc + '%'
div.style.height = heightPerc + '%'
div.style.left = i * unit + '%'
div.style.transform = `translate(-50%, ${translateY}%)`
div.style.borderRadius = borderRadiusPerc + '%'
div.setAttribute('data-translate', targetTranslateY)
goo.appendChild(div)
}
goo.style.transform = `translateY(0)`
goo.childNodes.forEach(
v => v.style.transform = `translateY(${v.getAttribute('data-translate')}%)`
)
}
setTimeout(() => {
gooTwo.innerHTML = '<div class="first"></div>'
filterOne.setAttribute('y', '100%')
generateGoo(gooTwo, true)
}, 2300)
html,
body {
width: 100%;
height: 100%;
margin: 0;
background: red;
}
div.goo-container {
--translateY: translateY(-165%);
z-index: 1;
width: 100%;
height: 100%;
position: fixed;
overflow: hidden;
}
div.goo-container > div {
width: 100%;
height: 100%;
position: absolute;
pointer-events: none;
transform: var(--translateY);
transition: transform 2.8s linear;
}
div.goo-container > div.goo-one {
filter: url('#goo-filter-one');
background: #5b534a;
}
div.goo-container > div.goo-two {
display: none;
filter: url('#goo-filter-two');
}
div.goo-container > div.goo-one > div,
div.goo-container > div.goo-two > div {
position: absolute;
bottom: 0;
background: #5b534a;
transition: transform 2.8s linear;
}
div.goo-container > div.goo-two > div.first {
top: -10%;
width: 100%;
height: 110%;
}
svg {
width: 0;
height: 0;
}
<div class='goo-container'>
<div class='goo-one'></div>
<div class='goo-two'></div>
<p><a href="#">Click me</a> and read.</p>
</div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<filter id='goo-filter-one' height='200%'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feComponentTransfer in='blur' result='goo'>
<feFuncA type='linear' slope='18' intercept='-7' />
</feComponentTransfer>
<feFlood flood-color='skyblue' result='back' />
<feMerge>
<feMergeNode in='back' />
<feMergeNode in='goo' />
</feMerge>
</filter>
<filter id='goo-filter-two' height='200%'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feComponentTransfer in='blur' result='goo'>
<feFuncA type='linear' slope='18' intercept='-7' />
</feComponentTransfer>
<feFlood flood-color='#5b534a' result='back' />
<feComposite operator='out' in='back' in2='goo' />
</filter>
</svg>
First I will start building the shape using one div and multiple gradient.
Here is an idea using unfirm gradients (same width and different heights) that we can easily position:
:root {
--c:linear-gradient(red,red);
}
div.goo-container {
position:fixed;
top:0;
left:-20px;
right:-20px;
height:200px;
background:
var(--c) calc(0*100%/9) 0/calc(100%/10) 80%,
var(--c) calc(1*100%/9) 0/calc(100%/10) 60%,
var(--c) calc(2*100%/9) 0/calc(100%/10) 30%,
var(--c) calc(3*100%/9) 0/calc(100%/10) 50%,
var(--c) calc(4*100%/9) 0/calc(100%/10) 59%,
var(--c) calc(5*100%/9) 0/calc(100%/10) 48%,
var(--c) calc(6*100%/9) 0/calc(100%/10) 36%,
var(--c) calc(7*100%/9) 0/calc(100%/10) 70%,
var(--c) calc(8*100%/9) 0/calc(100%/10) 75%,
var(--c) calc(9*100%/9) 0/calc(100%/10) 35%;
background-repeat:no-repeat;
filter: url('#goo-filter');
}
<div class='goo-container'>
</div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
We can also have variable width and here the JS will be needed to generate all of them:
:root {
--c:linear-gradient(red,red);
}
div.goo-container {
position:fixed;
top:0;
left:-20px;
right:-20px;
height:200px;
background:
var(--c) 0 0/20px 80%,
var(--c) 20px 0/80px 60%,
var(--c) 100px 0/10px 30%,
var(--c) 110px 0/50px 50%,
var(--c) 160px 0/30px 59%,
var(--c) 190px 0/80px 48%,
var(--c) 270px 0/10px 36%,
var(--c) 280px 0/20px 70%,
var(--c) 300px 0/50px 75%,
var(--c) 350px 0/80px 35%
/* and so on ... */;
background-repeat:no-repeat;
filter: url('#goo-filter');
}
<div class='goo-container'>
</div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
Then with more CSS we can have our first animation:
:root {
--c:linear-gradient(red,red);
}
div.goo-container {
position:fixed;
height:100vh;
top:0;
left:0;
right:0;
background:red;
transform:translateY(-150vh);
animation:move 3s 1s forwards;
}
div.goo-container::after {
position:absolute;
content:"";
top:100%;
left:-20px;
right:-20px;
height:50vh;
margin:0 -20px;
background:
var(--c) calc(0*100%/9) 0/calc(100%/10) 80%,
var(--c) calc(1*100%/9) 0/calc(100%/10) 60%,
var(--c) calc(2*100%/9) 0/calc(100%/10) 30%,
var(--c) calc(3*100%/9) 0/calc(100%/10) 50%,
var(--c) calc(4*100%/9) 0/calc(100%/10) 59%,
var(--c) calc(5*100%/9) 0/calc(100%/10) 48%,
var(--c) calc(6*100%/9) 0/calc(100%/10) 36%,
var(--c) calc(7*100%/9) 0/calc(100%/10) 70%,
var(--c) calc(8*100%/9) 0/calc(100%/10) 75%,
var(--c) calc(9*100%/9) 0/calc(100%/10) 35%;
background-repeat:no-repeat;
filter: url('#goo-filter');
}
div.goo-container::before {
position:absolute;
content:"";
top:100%;
height:150vh;
background:blue;
left:0;
right:0;
}
@keyframes move {
to {
transform:translateY(0);
}
}
<div class='goo-container'>
</div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
Still not perfect but we can add some gradient animation too to adjust the sizes:
:root {
--c:linear-gradient(red,red);
}
div.goo-container {
position:fixed;
height:100vh;
top:0;
left:0;
right:0;
background:red;
transform:translateY(-150vh);
animation:move 5s 0.5s forwards;
}
div.goo-container::after {
position:absolute;
content:"";
top:100%;
left:-20px;
right:-20px;
height:50vh;
margin:0 -20px;
background:
var(--c) calc(0*100%/9) 0/calc(100%/10) 80%,
var(--c) calc(1*100%/9) 0/calc(100%/10) 60%,
var(--c) calc(2*100%/9) 0/calc(100%/10) 30%,
var(--c) calc(3*100%/9) 0/calc(100%/10) 50%,
var(--c) calc(4*100%/9) 0/calc(100%/10) 59%,
var(--c) calc(5*100%/9) 0/calc(100%/10) 48%,
var(--c) calc(6*100%/9) 0/calc(100%/10) 36%,
var(--c) calc(7*100%/9) 0/calc(100%/10) 70%,
var(--c) calc(8*100%/9) 0/calc(100%/10) 75%,
var(--c) calc(9*100%/9) 0/calc(100%/10) 35%;
background-repeat:no-repeat;
filter: url('#goo-filter');
animation:grad 4.5s 1s forwards;
}
div.goo-container::before {
position:absolute;
content:"";
top:100%;
height:150vh;
background:blue;
left:0;
right:0;
}
@keyframes move {
to {
transform:translateY(0);
}
}
@keyframes grad {
to {
background-size:
calc(100%/10) 50%,
calc(100%/10) 75%,
calc(100%/10) 20%,
calc(100%/10) 60%,
calc(100%/10) 55%,
calc(100%/10) 80%,
calc(100%/10) 23%,
calc(100%/10) 80%,
calc(100%/10) 90%,
calc(100%/10) 20%;
}
}
<div class='goo-container'>
</div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
The above is a bit tricky because the position of each gradient will depend on the size of all the previous one (probably need JS or SASS here to generate the code)
For the second animation we will do the same but we consider the gradient layers inside a mask property to have the opposite effect (the gradient layers will get removed to see the remaining part)
:root {
--c:linear-gradient(red,red);
background:pink;
}
div.goo-container {
position:fixed;
height:150vh;
top:0;
left:0;
right:0;
transform:translateY(-200vh);
animation:move 8s 0.5s forwards;
filter: url('#goo-filter');
}
div.goo-container > div {
height:100%;
background:red;
-webkit-mask:
var(--c) calc(0*100%/9) 0/calc(100%/10 + 4px) 40vh,
var(--c) calc(1*100%/9) 0/calc(100%/10 + 4px) 30vh,
var(--c) calc(2*100%/9) 0/calc(100%/10 + 4px) 15vh,
var(--c) calc(3*100%/9) 0/calc(100%/10 + 4px) 20vh,
var(--c) calc(4*100%/9) 0/calc(100%/10 + 4px) 29vh,
var(--c) calc(5*100%/9) 0/calc(100%/10 + 4px) 35vh,
var(--c) calc(6*100%/9) 0/calc(100%/10 + 4px) 12vh,
var(--c) calc(7*100%/9) 0/calc(100%/10 + 4px) 50vh,
var(--c) calc(8*100%/9) 0/calc(100%/10 + 4px) 48vh,
var(--c) calc(9*100%/9) 0/calc(100%/10 + 4px) 40vh,
linear-gradient(#fff,#fff);
-webkit-mask-posite:destination-out;
mask-posite:exclude;
-webkit-mask-repeat:no-repeat;
animation:mask 7.5s 1s forwards;
}
div.goo-container::after {
position:absolute;
content:"";
top:100%;
left:-20px;
right:-20px;
height:50vh;
margin:0 -20px;
background:
var(--c) calc(0*100%/9) 0/calc(100%/10) 80%,
var(--c) calc(1*100%/9) 0/calc(100%/10) 60%,
var(--c) calc(2*100%/9) 0/calc(100%/10) 30%,
var(--c) calc(3*100%/9) 0/calc(100%/10) 50%,
var(--c) calc(4*100%/9) 0/calc(100%/10) 59%,
var(--c) calc(5*100%/9) 0/calc(100%/10) 48%,
var(--c) calc(6*100%/9) 0/calc(100%/10) 36%,
var(--c) calc(7*100%/9) 0/calc(100%/10) 60%,
var(--c) calc(8*100%/9) 0/calc(100%/10) 65%,
var(--c) calc(9*100%/9) 0/calc(100%/10) 35%;
background-repeat:no-repeat;
filter: url('#goo-filter');
animation:grad 7.5s 1s forwards;
}
div.goo-container::before {
position:absolute;
content:"";
top:100%;
height:150vh;
background:blue;
left:0;
right:0;
}
@keyframes move {
to {
transform:translateY(150vh);
}
}
@keyframes grad {
to {
background-size:
calc(100%/10) 50%,
calc(100%/10) 75%,
calc(100%/10) 20%,
calc(100%/10) 60%,
calc(100%/10) 55%,
calc(100%/10) 80%,
calc(100%/10) 23%,
calc(100%/10) 80%,
calc(100%/10) 90%,
calc(100%/10) 20%;
}
}
@keyframes mask {
to {
-webkit-mask-size:
calc(100%/10) 30vh,
calc(100%/10) 10vh,
calc(100%/10) 50vh,
calc(100%/10) 45vh,
calc(100%/10) 12vh,
calc(100%/10) 22vh,
calc(100%/10) 60vh,
calc(100%/10) 10vh,
calc(100%/10) 8vh,
calc(100%/10) 35vh,
auto;
}
}
<h1>Lorem ipsum dolor sit amet</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu sodales lectus. Sed non erat accumsan, placerat purus quis, sodales mi. Suspendisse potenti. Sed eu viverra odio. </p>
<div class='goo-container'>
<div></div>
</div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
We do some code optimization and to keep only one element:
:root {
--c:linear-gradient(red,red);
background:pink;
}
div.goo-container {
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
transform:translateY(-150%);
animation:move 8s 0.5s forwards;
filter: url('#goo-filter');
}
div.goo-container::after {
position:absolute;
content:"";
top:-50%;
left:0;
right:0;
bottom:-50%;
background:
var(--c) calc(0*100%/9) 0/calc(100%/10) calc(100% - 40vh),
var(--c) calc(1*100%/9) 0/calc(100%/10) calc(100% - 30vh),
var(--c) calc(2*100%/9) 0/calc(100%/10) calc(100% - 35vh),
var(--c) calc(3*100%/9) 0/calc(100%/10) calc(100% - 50vh),
var(--c) calc(4*100%/9) 0/calc(100%/10) calc(100% - 10vh),
var(--c) calc(5*100%/9) 0/calc(100%/10) calc(100% - 15vh),
var(--c) calc(6*100%/9) 0/calc(100%/10) calc(100% - 30vh),
var(--c) calc(7*100%/9) 0/calc(100%/10) calc(100% - 28vh),
var(--c) calc(8*100%/9) 0/calc(100%/10) calc(100% - 30vh),
var(--c) calc(9*100%/9) 0/calc(100%/10) calc(100% - 50vh);
background-repeat:no-repeat;
-webkit-mask:
var(--c) calc(0*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 20vh),
var(--c) calc(1*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 10vh),
var(--c) calc(2*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 50vh),
var(--c) calc(3*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 30vh),
var(--c) calc(4*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 35vh),
var(--c) calc(5*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 10vh),
var(--c) calc(6*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 50vh),
var(--c) calc(7*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 40vh),
var(--c) calc(8*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 45vh),
var(--c) calc(9*100%/9) 100%/calc(100%/10 + 4px) calc(100% - 35vh);
-webkit-mask-repeat:no-repeat;
filter: inherit;
animation: inherit;
animation-name:grad, mask;
}
div.goo-container::before {
position:absolute;
content:"";
top:50%;
bottom:-150%;
background:blue;
left:0;
right:0;
}
@keyframes move {
to {
transform:translateY(200%);
}
}
@keyframes grad {
to {
background-size:
calc(100%/10) calc(100% - 10vh),
calc(100%/10) calc(100% - 50vh),
calc(100%/10) calc(100% - 30vh),
calc(100%/10) calc(100% - 10vh),
calc(100%/10) calc(100% - 40vh),
calc(100%/10) calc(100% - 25vh),
calc(100%/10) calc(100% - 32vh),
calc(100%/10) calc(100% - 18vh),
calc(100%/10) calc(100% - 50vh),
calc(100%/10) calc(100% - 10vh);
}
}
@keyframes mask {
to {
-webkit-mask-size:
calc(100%/10) calc(100% - 10vh),
calc(100%/10) calc(100% - 50vh),
calc(100%/10) calc(100% - 10vh),
calc(100%/10) calc(100% - 30vh),
calc(100%/10) calc(100% - 32vh),
calc(100%/10) calc(100% - 40vh),
calc(100%/10) calc(100% - 50vh),
calc(100%/10) calc(100% - 25vh),
calc(100%/10) calc(100% - 18vh),
calc(100%/10) calc(100% - 10vh);
}
}
<h1>Lorem ipsum dolor sit amet</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu sodales lectus. Sed non erat accumsan, placerat purus quis, sodales mi. Suspendisse potenti. Sed eu viverra odio. </p>
<div class='goo-container'></div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
And finally a dynamic solution using SASS to generate the gradient and mask layers: https://codepen.io/t_afif/pen/oNzxYgV
UPDATE
Another idea without using mask. The trick is to center the gradients. This solution will have more support but both bottom and top shape will be symetric
:root {
--c:linear-gradient(red,red);
background:pink;
}
div.goo-container {
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
transform:translateY(-150%);
animation:move 8s 0.5s forwards;
filter: url('#goo-filter');
}
div.goo-container::after {
position:absolute;
content:"";
top:-50%;
left:0;
right:0;
bottom:-50%;
background:
var(--c) calc(0*100%/9) 50%/calc(100%/10) calc(100% - 80vh),
var(--c) calc(1*100%/9) 50%/calc(100%/10) calc(100% - 60vh),
var(--c) calc(2*100%/9) 50%/calc(100%/10) calc(100% - 70vh),
var(--c) calc(3*100%/9) 50%/calc(100%/10) calc(100% - 100vh),
var(--c) calc(4*100%/9) 50%/calc(100%/10) calc(100% - 20vh),
var(--c) calc(5*100%/9) 50%/calc(100%/10) calc(100% - 30vh),
var(--c) calc(6*100%/9) 50%/calc(100%/10) calc(100% - 60vh),
var(--c) calc(7*100%/9) 50%/calc(100%/10) calc(100% - 56vh),
var(--c) calc(8*100%/9) 50%/calc(100%/10) calc(100% - 60vh),
var(--c) calc(9*100%/9) 50%/calc(100%/10) calc(100% - 100vh);
background-repeat:no-repeat;
filter: inherit;
animation:grad 8s 0.5s forwards;
}
div.goo-container::before {
position:absolute;
content:"";
top:50%;
bottom:-150%;
background:blue;
left:0;
right:0;
}
@keyframes move {
to {
transform:translateY(200%);
}
}
@keyframes grad {
to {
background-size:
calc(100%/10) calc(100% - 20vh),
calc(100%/10) calc(100% - 100vh),
calc(100%/10) calc(100% - 60vh),
calc(100%/10) calc(100% - 20vh),
calc(100%/10) calc(100% - 80vh),
calc(100%/10) calc(100% - 50vh),
calc(100%/10) calc(100% - 64vh),
calc(100%/10) calc(100% - 34vh),
calc(100%/10) calc(100% - 100vh),
calc(100%/10) calc(100% - 20vh);
}
}
<h1>Lorem ipsum dolor sit amet</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu sodales lectus. Sed non erat accumsan, placerat purus quis, sodales mi. Suspendisse potenti. Sed eu viverra odio. </p>
<div class='goo-container'></div>
<svg xmlns='http://www.w3/2000/svg' version='1.1'>
<defs>
<filter id='goo-filter'>
<feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur' />
<feColorMatrix in='blur' mode='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5' result='goo' />
<feBlend in='SourceGraphic' in2='goo' />
</filter>
</defs>
</svg>
And a SASS version: https://codepen.io/t_afif/pen/wvzGoeJ
Here is an attempt to avoid all filter, masking and position difficulties. It is just a SMIL animation of some bezier paths, which should be supported without any bugs. I have not found a solution yet where the first and second wave appear on the screen at the same time.
I admit the most laborious part was devising an algorithm for the path, everything else is relatively straight forward.
The "goo" is an area with upper and lower border that is moved across the client area, while at the same time the form of the path changes. I have tried to describe in code ments which parts could be tweaked. The basic structure of the path position ensures an important restriction: the path as a whole must not have a differing sequence of path mands for different keyframes of the animation, otherwise the smooth animation will fail. Changing numbers should be no problem.
Behind the goo sits an opaque rectangle that initially hides the content. It is hidden at an appropriate time while the goo runs across the screen.
The timing of the animation is defined in attributes of the <set>
and <animate>
elements. Take note that the goo animation runs for 6s, while the hiding of the background rectangle happens after 3s. This distribution matches the values of the <animate keyTimes>
attribute: 0;0.5;1
, which you could read as 0%, 50%, 100% as timing for the keyframes. The timing when <set>
triggers must match that middle keyframe, as that is the time when the goo covers the whole client area.
const
rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
flatten = (x, y) => `${x.toFixed(2)},${y.toFixed(2)}`
function randomPoints(width, height) {
const
from = [],
to = []
let x = 0, old_extent = 0
while (x + old_extent < width) {
//width of a single goo tongue
const extent = rand(5, 20)
// rand() part: distance between tongues
x += (from.length ? 1.5 : 0) * (old_extent + extent) + rand(0, 5)
const data = {
x1: x - extent,
x2: x + extent,
// "roundness": how far will the lowest point of the tongue
// stretch below its defining line (qualitative value)
dty: extent * rand(0.4, 1.4)
}
// y: tongue postition above screen border at start
// Note the -20 gives space for the "roundness" not to cross the threshold
from.push({ ...data, y: rand(-50, -20) })
// y: tongue postition below screen border at end
// Note the 10 gives space for the "roundness" not to cross the threshold
to.push({ ...data, y: rand(10, 105) + height })
old_extent = extent
}
return { from, to }
}
function generatePath(points, path, back) {
const qti = points.length
let old_dtx, old_dty
if (back) points.reverse()
for (let i = 0; i < qti; i++) {
const
x1 = back ? points[i].x2 : points[i].x1,
x2 = back ? points[i].x1 : points[i].x2,
dtx = (x2 - x1) / 2
let dty = 0
if (i == 0) {
path.push(
back ? 'L' : 'M',
flatten(x1, points[i].y),
'Q',
flatten(x1 + dtx, points[i].y),
flatten(x2, points[i].y)
);
} else {
if (i !== qti - 1) {
const
y0 = points[i - 1].y,
y1 = points[i].y,
y2 = points[i + 1].y,
// the numbers give a weight to the "roundness" value for different cases:
// a tongue stretching below its neighbors = 1 (rounding downwards)
// a tongue laging behind below its neighbors = -0.1 (rounding upwards)
// other cases = 0.5
down = y1 > y0 ? y1 > y2 ? 1 : 0.5 : y1 > y2 ? 0.5 : -0.1
dty = points[i].dty * down //min absichern
}
path.push(
'C',
flatten(points[i - 1][back ? 'x1' : 'x2'] + old_dtx / 2, points[i - 1].y - old_dty / 2),
flatten(x1 - dtx / 2, points[i].y - dty / 2),
flatten(x1, points[i].y),
'Q',
flatten(x1 + dtx, points[i].y + dty),
flatten(x2, points[i].y)
);
}
old_dtx = dtx, old_dty = dty
}
if (back) {
points.reverse()
path.push('Z')
}
}
function generateArea(width, height) {
const
// tongue control points for first wave
firstPoints = randomPoints(width, height),
// tongue control points for second wave
secondPoints = randomPoints(width, height),
start = [],
mid = [],
end = []
// first keyframe
generatePath(firstPoints.from, start, false)
generatePath(secondPoints.from, start, true)
// second keyframe
generatePath(firstPoints.to, mid, false)
generatePath(secondPoints.from, mid, true)
// third keyframe
generatePath(firstPoints.to, end, false)
generatePath(secondPoints.to, end, true)
return [
start.join(' '),
mid.join(' '),
end.join(' ')
]
}
const rect = document.querySelector('svg').getBoundingClientRect()
const animate = document.querySelector('#gooAnimate')
const areas = generateArea(rect.width, rect.height)
animate.setAttribute('values', areas.join(';'))
animate.beginElement() // trigger animation start
body {
position: relative;
margin: 0;
}
#content {
position: relative;
box-sizing: border-box;
background: #faa;
width: 100vw;
height: 100vh;
padding: 1em;
}
svg {
position: absolute;
width: 100%;
height: 100%;
top: 0%;
pointer-events: none;
}
#veil {
fill: skyblue;
}
#goo {
fill: #5b534a;
}
<div id="content">
<h1>Lorem ipsum dolor sit amet</h1>
<p>Lorem ipsum dolor sit amet, <a href="">consectetur</a> adipiscing elit. Nam eu sodales lectus. Sed non erat accumsan, placerat purus quis, sodales mi. Suspendisse potenti. Sed eu viverra odio. </p>
</div>
<svg xmlns="http://www.w3/2000/svg">
<rect id="veil" width="100%" height="100%">
<!-- background animation start time is relative to goo animation start time -->
<set attributeName="display" to="none" begin="gooAnimate.begin+3s" fill="freeze" />
</rect>
<path id="goo" d="" >
<animate id="gooAnimate" attributeName="d"
begin="indefinite" dur="6s" fill="freeze" keyTimes="0;0.5;1" />
</path>
</svg>