I'm trying to create an effect where two rectangular shapes (with rounded ends), each containing text, move toward each other, merge/morph into a singular rounded rectangle as I scroll down the page, and separate again when I scroll up. The shapes need to remain sticky to the viewport position during scrolling.
What I've Tried:
The closest I've gotten is merely getting the shapes to move toward each other and slightly overlap. Other attempts have resulted in ellipse shapes, unnatural border-radius, or a lack of cohesion between the two elements.
document.addEventListener('scroll', () => {
const scrollPercent = window.scrollY / (document.body.scrollHeight - window.innerHeight);
const movement = (window.innerWidth / 2 - 115) * scrollPercent;
const shape1 = document.querySelector('.shape:nth-child(1)');
const shape2 = document.querySelector('.shape:nth-child(2)');
shape1.style.transform = `translateX(${movement}px)`;
shape2.style.transform = `translateX(-${movement}px)`;
});
body {
width: 90%;
margin: 0 auto;
padding: 20px;
height: 2000px;
}
.container {
position: sticky;
top: 20px;
display: flex;
justify-content: space-between;
}
.shape {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 30px;
border-radius: 15px;
background-color: #000;
color: #fff;
transition: transform 0.3s ease-out;
transform: translateX(0%);
}
<body>
<div class="container">
<div class="shape" id="shape1">Shape 1</div>
<div class="shape" id="shape2">Shape 2</div>
</div>
</body>
Question: I feel like I'm missing something with clip-path and that this is the route I need to take. How can I improve my CSS and adjust my JavaScript to achieve the morphing effect shown in the image above while maintaining rounded outer edges and building cohesion between the two containers? I appreciate any suggestions or corrections to my current approach. Thank you.
A common way is to use an SVG filter:
<svg class="morph-filter" viewbox="0 0 0 0">
<filter id="morph">
<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 64 -32" result="morph" />
<feBlend in="SourceGraphic" in2="morph" />
</filter>
</svg>
.container {
filter: url(#morph);
}
document.addEventListener('scroll', () => {
const scrollPercent = window.scrollY / (document.body.scrollHeight - window.innerHeight);
const movement = (window.innerWidth / 2 - 115) * scrollPercent;
const shape1 = document.querySelector('.shape:nth-child(1)');
const shape2 = document.querySelector('.shape:nth-child(2)');
shape1.style.transform = `translateX(${movement}px)`;
shape2.style.transform = `translateX(-${movement}px)`;
});
body {
width: 90%;
margin: 0 auto;
padding: 20px;
height: 2000px;
}
.container {
position: sticky;
top: 20px;
display: flex;
justify-content: space-between;
filter: url(#morph);
}
.shape {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 30px;
border-radius: 15px;
background-color: #000;
color: #fff;
transition: transform 0.3s ease-out;
transform: translateX(0%);
}
<body>
<div class="container">
<div class="shape" id="shape1">Shape 1</div>
<div class="shape" id="shape2">Shape 2</div>
</div>
<svg class="morph-filter" viewbox="0 0 0 0">
<filter id="morph">
<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 64 -32" result="morph" />
<feBlend in="SourceGraphic" in2="morph" />
</filter>
</svg>
</body>
First, it uses <feGaussianBlur />
to blur the two boxes. Much like filter: blur(10px)
, it makes the source image blurry, with a bigger size than its original shape, and stores the output image in channel blur
.
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
Then using <feColorMatrix />
, without touching the source image's RGB channel, it only "sharpens" the image's alpha channel with relatively big values (64 -32
), it makes alpha channels less than 0.5
invisible and alpha chanel more than 0.5
fully visible (alpha channel values are clamped from 0
to 1
). Then stores the output in channel morph
.
<feColorMatrix in="blur" mode="matrix" values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 64 -32" result="morph" />
Finally, it blends these channels with <feBlend />
, that when the two boxes almost touches, their overlaping blurry boundaries' alpha channels are greater than 0.5
, which creates the morphing effect.