3 回答
TA贡献1765条经验 获得超5个赞
我很确定这不是最佳变体,但它似乎可行,至少在 Firefox 中是这样。Chrome 在动画每个部分的初始帧方面存在一些问题。
我稍微重写了 gooey 过滤器代码以提高可读性,同时保持相同的效果。
只有
.goo-one
和子 div 获得背景颜色。这使得.goo-two
变得透明成为可能。这两个部分有不同的过滤器,但过滤器区域垂直增加,以便在过渡开始时到达屏幕底部。
第一个滤镜使用天蓝色作为背景填充。
第二个过滤器有一个棕色填充,但它的应用是相反的:它只显示在 goo 区域之外,而内部区域是空的。构成 goo 区域的 div 矩形不跨越整个
.gooTwo
. 为了也填充(并在倒置后为空)顶部,<div class="first">
需要额外的部分。在第二个 goo 部分的过渡开始时,过滤器区域上限设置在屏幕下边界下方。这隐藏了天蓝色背景,同时第二个粘性部分变得可见。
请注意,为了更好的浏览器兼容性,元素的 CSS 略有变化
svg
。作为概念证明,在容器 div 中添加了一些内容。表明
pointer-event: none
需要a;否则无法与页面进行交互。
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>
TA贡献2036条经验 获得超8个赞
首先,我将开始使用一个 div 和多个渐变来构建形状。
这是一个使用我们可以轻松定位的不固定渐变(相同宽度和不同高度)的想法:
Hide code snippet
: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 来生成所有这些宽度:
Hide code snippet
: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 我们可以有我们的第一个动画:
Hide code snippet
: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>
展开片段
仍然不完美,但我们也可以添加一些渐变动画来调整大小:
Hide code snippet
: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 来生成代码)
对于第二个动画,我们将做同样的事情,但我们认为遮罩属性中的渐变层具有相反的效果(渐变层将被移除以查看剩余部分)
Hide code snippet
: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>
展开片段
我们做了一些代码优化,只保留一个元素:
Hide code snippet
: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
更新
不使用面具的另一个想法。诀窍是使渐变居中。此解决方案将有更多支持,但底部和顶部形状都是对称的
Hide code snippet
: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
TA贡献1859条经验 获得超6个赞
这是避免所有过滤器、掩蔽和组合困难的尝试。它只是一些 bezier 路径的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>
添加回答
举报