在层叠上下文中,将背景“裁剪”以查看其下方

10
[ 注意:寻找一个跨浏览器的解决方案,不会像ccprog的答案那样让背景在每一波硅胶之间短暂闪烁;理想情况下,解决方案不应该包括等待第一波结束才开始显示第二波,这样两个波浪可以同时运行。我愿意放弃动态随机化的硅胶,以得到理想的解决方案。]

有谁知道如何使橙色硅胶的第二波(.goo-two)“穿过”棕色硅胶的第一波(.goo-one)以及天蓝色容器(.goo-container)来显示或暴露红色的元素(body)或者其他处于堆叠上下文中它下面的任何元素?这种操作是否可行?

值得注意的是,我给容器(.goo-container)赋予了一个实心背景,因为我希望使用这个容器来覆盖网站其余部分的加载过程,我希望橙色硅胶(.goo-two)可以用来展示内容。这变得更加棘手,因为橙色硅胶开始滴落之前,棕色硅胶尚未完成,这将是把容器(.goo-container)的背景从skyblue更改为transparent的完美时机,虽然半透明渐变作为背景可能仍然可以用来实现此目的。(或者完全不同的事情,如复制橙色层并使用其中一个来裁剪棕色路径,另一个用于裁剪天蓝色层。)

有任何想法吗?

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>


@TemaniAfif,不可能使用某种剪辑或遮罩来完成吗?我也想到了复制橙色滴水层的想法,使用其中一个来剪辑棕色黏液,另一个来剪辑容器。我对你的任何想法都持开放态度 :) - oldboy
1
完全使用SVG,是的,在这种情况下可能是可行的,但可能还有其他想法,让我们等待更多的输入 ;) - Temani Afif
@CalebTaylor,我的演示没有技术上的问题。然而,我正在寻找一种使用第二波黏液“剪切”或暴露红色背景的方法。红色背景的闪烁是ccprog答案的唯一问题。在Firefox中会卡在天蓝色背景上的是什么? - oldboy
1
@CalebTaylor 在实际实现之前,我会尽可能多地进行检查。感谢您指出Firefox上的问题。天啊,我感觉现在许多东西,如果它们不是完全标准和基本的话,仍然存在很多错误:( - oldboy
@TemaniAfif 哇,我刚想到了一种巧妙的方法来实现这个,尽管它可能不太实用或者性能不佳?我可以使用每个像素宽度的一个 div 元素,并设置不同的高度来模拟波浪/黏性物质,然后协调每个 div 的高度的操作,使黏性物质活跃并滑动到屏幕下方。你认为任何设备,更不用说移动设备,都能处理这个吗?我猜在移动设备上,需要的 div 元素要少得多,因为它们的 width 通常小于桌面。 - oldboy
显示剩余14条评论
3个回答

7

我非常确定这不是最优的变体,但至少在Firefox中似乎可以工作。Chrome在每个动画部分的初始帧上存在一些问题。

  • 我已经重写了gooey过滤器代码,以提高可读性,同时保持相同的效果。有关解释,请参见我的文章
  • 只有.goo-one和子div获得背景颜色。这使得.goo-two能够变成透明。
  • 两个部分获得不同的过滤器,但是过滤器区域在垂直方向上增加,以便在转换开始时到达屏幕底部。
  • 第一个过滤器具有天蓝色作为背景填充。
  • 第二个过滤器具有棕色填充,但其应用被反转:仅在粘液区域外显示,留下内部区域为空白。组成粘液区域的div矩形并未跨越整个.gooTwo。为了填充(并在反转后清空)顶部部分,需要一个额外的<div class="first">
  • 在第二个粘液部分的转换开始时,将上限过滤器区域设置在屏幕下方。这同时隐藏了天蓝色背景,并使第二个粘液部分变得可见。
  • 请注意CSS中svg元素的轻微更改以获得更好的浏览器兼容性。
  • 仅作为概念验证,在容器div内添加了一些内容。它表明需要一个pointer-event:none;否则无法与页面交互。

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>


1
不,看一下JS中的filterOne变量,CSS已经被重写了。 - ccprog
两件事情:(1)filterOne.setAttribute('y', '100%')在第二个波浪或反转滤镜覆盖屏幕之前暴露了页面的背景。我找不到解决方法,已经尝试过setTimeouttransitionstart等。有什么解决方案吗?(2)我不完全理解div.first的重要性。你能详细说明它的作用吗? - oldboy
@oldboy,我会尽力留出时间来思考并找到解决方案。这不是一项容易的任务。 - Temani Afif
@TemaniAfif,你对我之前的想法有什么看法?不使用div元素,而是在Canvas或SVG元素上使用形状,这样在4K设备上就不会有4K个元素了。 - oldboy
1
@oldboy 但是你最终会得到一个非常大的DOM。不确定这是否是最好的想法。 - Temani Afif
显示剩余10条评论

4

首先,我将使用一个div和多个渐变来构建形状。

这里有一个使用不均匀渐变(相同宽度和不同高度)的想法,我们可以轻松定位:

: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>

我们还可以拥有可变宽度,这时需要使用JS来生成所有内容:

: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>

接下来,我们可以使用更多的CSS来制作我们的第一个动画:

: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>

还不是完美的,但我们可以添加一些渐变动画来调整大小:

: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>

上述内容有些复杂,因为每个渐变的位置都取决于之前所有渐变的大小(可能需要使用JS或SASS来生成代码)


对于第二个动画,我们将执行相同的操作,但是我们将考虑在mask属性中的渐变图层具有相反的效果(渐变图层将被删除以查看剩余部分)

: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>

我们进行了一些代码优化,只保留一个元素:

: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>

最后,使用SASS生成渐变和遮罩层的动态解决方案:https://codepen.io/t_afif/pen/oNzxYgV 更新:
另一个不使用遮罩的想法。诀窍是将渐变居中。这个解决方案将有更多的支持,但上下形状都将是对称的。

: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>

还有一个SASS版本:https://codepen.io/t_afif/pen/wvzGoeJ


评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew
Temani,如果你好奇的话,请查看聊天记录中的我的新解决方案。 - oldboy

2
这是一个尝试避免所有过滤器、掩码和合成难题的方法。它只是一些贝塞尔路径的SMIL动画,应该没有任何错误。我还没有找到一个解决方案,使第一和第二个波浪线同时出现在屏幕上。
我承认最费力的部分是设计路径算法,其他的都相对简单。
“goo”是一个带有上下边界的区域,在客户端区域内移动,同时路径的形状也会改变。我已经尝试在代码注释中描述哪些部分可以调整。路径组合的基本结构确保了重要的限制:整个路径在动画的不同关键帧中不能具有不同的路径命令序列,否则平滑的动画将失败。更改数字应该不是问题。
在“goo”的后面有一个不透明的矩形,最初隐藏内容。在“goo”在屏幕上运行时,适当的时间将其隐藏。
动画的时间是在 <set><animate> 元素的属性中定义的。请注意,goo 动画持续 6 秒,而背景矩形的隐藏发生在 3 秒后。这种分配与 <animate keyTimes> 属性的值相匹配: 0;0.5;1,可以将其解读为关键帧的时间,即0%,50%,100%。当 <set> 触发时的时间必须与中间关键帧匹配,因为那是 goo 覆盖整个客户端区域的时间。

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>


你认为或者有什么想法,这个比Temani的回答更受广泛支持吗? - oldboy
1
SMIL是一种W3C标准,它描述了使用XML标记声明式编程动画的方法。它本身并没有流行起来,但自1.0规范以来已成为SVG的一部分。它的支持几乎是普遍且非常成熟的。即使对于不支持它的浏览器(IE和Edge<79),也有一个填充程序(FakeSmile)可用。此外,目前这是在动画中变形路径而无需使用外部库的唯一方法。蒙版已经在过去的5年中实现,SMIL已成为浏览器的一部分超过10年。 - ccprog
顺便说一句,我正准备实现这个效果,但决定先在移动设备上进行测试。在iOS Safari和Chrome上,Temani的两个版本存在问题,但这个版本非常流畅。 - oldboy
1
当您调整窗口大小时,它不会响应,但如果在其他屏幕尺寸上显示,则始终会填充屏幕。这是一种权衡:要么让物品始终填充屏幕,要么保持物品舌头的宽度稳定,但冒着在物品可见期间调整大小而物品未填充屏幕的风险。只要在animate.beginElement()之前调用rect.getBoundingClientRect(); generateArea(rect.width, rect.height),大小就会自适应。 - ccprog
我猜尽管存在问题,这可能是最佳答案,因为它是最具跨平台兼容性的。 - oldboy
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接