"Clipping" Background to See Below Itself in Stacking Context
Asked Answered
F

3

10

[ 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.org/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>
Freestanding answered 23/11, 2020 at 21:1 Comment(19)
I may have an idea but it's support is far in the futur ...Delirious
@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 :)Freestanding
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-pathDelirious
@TemaniAfif initially i was thinking of using canvas (or SVG) and, i believe, quadratic curves to do the drip, but then somebody on here pointed me to a CSS-tricks article showing how to create the "goo" effect this way, which, compared to my understanding of canvas and the math involved in manipulating the curves, was a lot simpler. would it be possible to do if the drip was entirely an SVG or canvas instead of a bunch of separate divs?Freestanding
entirely SVG, yes it would be possible in this case but probably there is other ideas too, let's wait for more inputs ;)Delirious
kk, hopefully it does not take too long for others to respond. i shouldve had this done a while ago lol. do you think manipulating a single path of an SVG (or canvas?) would cost less than doing it the way im doing it? i suspected it would have, but chose this method to get it done faster :(Freestanding
I'm using Ubuntu 20.04.1 LTS x86_64, and on Chrome 87 it renders fine, I don't see a red background flash. But on Firefox 83.0 nothing happens and is stuck on skyblue background. @ccprog solution both show red background flash on both Chrome and Firefox.Diehard
@CalebTaylor theres nothing technically wrong with my demo. however, i was looking for a way to use the second wave of goo to "clip" through or expose the red background. the flashing of the red background is exclusively an issue with ccprog's answer. what gets stuck on a skyblue background in firefox?Freestanding
@CalebTaylor i was wrong. i didnt bother to check my demo in other browsers. you are right that it is just a static blue screen. i didnt realize it was not cross browser compatible. thought SVG wouldve had much wider support by now (similar to how i just found out that minlength and maxlength for input elements is surprisingly just beginning to receive support)Freestanding
@TemaniAfif any idea how i can go about this? i am willing to forego dynamically generated/randomized goop for a cross-browser solutionFreestanding
@CalebTaylor I just realized the reason it was not working on Firefox was because of svg { display: none }. seems to work fine when you disable thisFreestanding
@Freestanding No worries, and you should check Safari. I don't have access to mac during certain periods of the day. Don't be disappointed if it doesn't work on Safari, Webkit can be buggy when SVG is animated by CSS.Diehard
@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 completely standard and basic, are still so buggy these days :(Freestanding
@TemaniAfif i am starting to consider other options, so do you know if there is a way to clip elements that is widely supported across all modern-ish browsers? or are they all still hit or miss nowadays?Freestanding
@TemaniAfif wow i just thought of an ingenious way to accomplish this, although it may not be practical or perform well? i could use one div element per pixel of width and set different heights to imitate a wave/goo and then coordinate the manipulation of the height of every div to make the goo lively and also slide down the screen. do you think any devices, let alone mobile devices, would be able to handle this? i guess on mobile, far fewer div elements would be needed because their width is typically less than desktopFreestanding
You will still face the issue of using many divs. For me the solution need to consider one element (either one SVG or one div). I have half an idea about how to do this with one div. I will try and if it works fine will put an answer ;)Delirious
@TemaniAfif yeah, not sure if devices would be able handle so many elements, especially manipulating them :/ i suspect not :( even if it doesnt work, please share after for the sake of my curiosity :)Freestanding
PS: keep the bounty until it expires. It's known that the last 2 days of the bounty will bring the most attention (and probably more solutions)Delirious
@TemaniAfif i didnt know that. i wont assign it until it expires. thanks <33Freestanding
M
7

I 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 become 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 becomes visible.
  • Note the slight change in the CSS for the svg element for better browser compatibility.
  • 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.org/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>
Manteau answered 24/11, 2020 at 17:24 Comment(15)
ok, i am gonna take a look at this right now. have you only modified the HTML?Freestanding
no, take a look at the filterOne variable in JS, and the CSS has been rewritten.Manteau
two things: (1) filterOne.setAttribute('y', '100%') exposes the body's background for a moment before the second wave or inverted filter covers the screen. i cannot find a solution for this, have tried setTimeout, transitionstart, etc. any ideas for solutions? (2) i do not fully understand the significance of div.first. can you elaborate on the purpose its serving?Freestanding
@TemaniAfif any ideas or comments regarding my comment above?Freestanding
(1) There seems to be a fundamental difference in how firefox and Chrome handle the filter. With FF, everything looks fine, while Chrome seems to sort of "grow" the goo from zero when it starts its application. (2) comment out the line gooTwo.innerHTML = '<div class="first"></div>' and you'll see it doesn't work. More explanation see answer edit.Manteau
@Freestanding you cannot ping me on a discussion where I am not participating ;) The ping feature is not universalDelirious
@TemaniAfif i did not know that. thanks <3Freestanding
i created a version locally and, although a lot of the issues that are present on Chrome are not present in Firefox, the Firefox seems choppy. do you think the better approach would be to use pure SVG (i.e. make a wave with quadriatic bezier curves)? would this perform better and be more widely supported?Freestanding
@Freestanding I will try to reserve some time to this and think about a solution. This isn't an easy task.Delirious
@TemaniAfif thank you. id really appreciate that. i am hoping to include this in my landing page. my only other ideas are use and manipulate a quadratic bezier curve instead of divs or use a non-SVG version that doesnt look at prettyFreestanding
@TemaniAfif What are your thoughts on the prior idea i had, but instead of using div elements, use shapes on a Canvas or SVG element, so that there arent 4K elements on a 4K device?Freestanding
@Freestanding well, SVG and canvas aren't my best so I cannot advice but for sure there is some magic trick to be able to easily build the whole shape as one SVG element and combine it with with mask or background.Delirious
@TemaniAfif but instead of a single wave, using tons of pixel-wide rectangles, which, i wonder, may make the math a bit easier? lolFreestanding
@Freestanding but you will end with a very large DOM. not sure if it's the best idea.Delirious
@TemaniAfif ok, i was unaware of that. the single path would prolly be the best, but the math is daunting. i wouldnt even know where to start. although, im not sure if canvas and svg are as widely supported as your method?Freestanding
D
4

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.org/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.org/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.org/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.org/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-composite:destination-out;
  mask-composite: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.org/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.org/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.org/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

Delirious answered 5/12, 2020 at 0:8 Comment(2)
Comments are not for extended discussion; this conversation has been moved to chat.Antebi
Temani, check the chat for my new solution if youre curiousFreestanding
M
2

Here is an attempt to avoid all filter, masking and composition 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 comments which parts could be tweaked. The basic structure of the path composition ensures an important restriction: the path as a whole must not have a differing sequence of path commands 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.org/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>
Manteau answered 5/12, 2020 at 22:28 Comment(11)
what is a "Synchronized Multimedia Integration Language" animation exactly i.e. is anything involving SVG, including your other solution, considered a SMIL animation?Freestanding
do you think or any idea if this is more widely supported than Temani's answer?Freestanding
SMIL is a W3C standard that describes the method of programming animations declaratively with XML markup. On its own, it never took off, but it became part of SVG since the 1.0 specification. Its support is more or less universal and very mature. Even for browsers that do not support it (IE and Edge<79) there is a polyfill (FakeSmile) available. In addition, it currently is the only way to morph paths in animations without the use of external libraries. Masks have been implemented during the last 5 years, SMIL has been part of browsers for > 10 years.Manteau
FWIW, im just about this implement this effect, but decided to test it on mobile first. there were issues with Temani's two versions on iOS safari and chrome, but this one worked smooth like butterFreestanding
i noticed one small issue with your solution, which isnt really an issue with the solution at all. that said, its not responsive. i should be able to make this responsive by sizing #goo and #veil appropriately, right?Freestanding
It is not responsive when you resize the window, but if it is shown on other screen sizes, it will always fill the screen. This is a trade-off: either have the goo always fill the screen, or keep the goo tongue width stable, but risk the goo not filling the screen on the off chance that is it resized during visibility of the goo. - As long as you call rect.getBoundingClientRect(); generateArea(rect.width, rect.height) before animate.beginElement(), the size will adapt.Manteau
is it possible to cancel the animation if it is resized during the animation and then call generateArea() once again to re-establish the fullscreen animation like cancel/requestAnimationFrame? or from what angle would you suggest tackling this problem of non-responsiveness/making it responsive?Freestanding
there must be a way to interpolate the values of path as percentages of the screen dimensions on resize, but prolly not while the animation is in the middle of running, right?Freestanding
one simple solution that i can think of is to make the wave roughly 4K wide (or maybe a bit wider to play it safe) and then hide the overflow. what do u think?Freestanding
considering there are issues with all of the solutions on one device/platform or another, im again beginning to think something like this may be the best approach. i can give the canvas a solid background-color at load and then change its background-color to transparent in an animation frame once the animation meets a specific condition (i.e. the top of the second wave is about to appear on the screen). it seems like the easiest option for me, compared to modifying your answer and trying to debug Temani's :( the curve math is already thereFreestanding
i guess this is the best answer despite the issues with it because it is the most cross-compatibleFreestanding

© 2022 - 2024 — McMap. All rights reserved.