CSS transition scale() - 奇怪的延迟
CSS transition scale() - weird delay
运行 下面的代码片段 - 单击圆圈可切换动画。应该发生的是,子容器应该看起来好像没有随着父容器缩放而移动。
这是通过按与放大父组件相同的比例缩小子组件来实现的(父组件缩放到 4,子组件缩放到 0.25)。
动画完成后缩放是正确的,但在动画播放过程中它们似乎没有一致缩放。
这几乎就像父级先缩放,然后完成子级缩放。
这是某种浏览器限制吗?还是我做错了什么?
谢谢!
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 100px;
height: 100px;
overflow: hidden;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: top left;
transition: transform 1s;
cursor: pointer;
border: 1px solid black;
}
.outer--active {
transform: scale(4) translate(-50%, -50%);
}
.inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg') center repeat;
transform-origin: top left;
transition: transform 1s;
}
.outer--active .inner {
transform: scale(0.25) translate(-50%, -50%);
}
<div class="outer">
<div class="inner"></div>
</div>
问题
你看到的不是"browser bug",而是对两个组合缩放的计算方式的误解。
为简单起见,我们假设转换函数是 linear
(而不是 ease
,这是默认的计时函数)。在那种情况下,两个尺度的图如下:
由于我们想让内部元素的最终比例保持不变,那么所有时间参数的(放大函数)×(缩小函数)= 1。不幸的是,如果我们进行乘法运算,结果会得到一个平方函数(在我们的例子中是 -¾x² + 3x + ¾)。这是您在过渡过程中可以看到的最终缩放中的凸起。为了避免这种情况,我们需要在 scale(1/m)
css 规则中缩放 m
,而不是转换 scale(n)
值。不幸的是,我们不能这样做,即使我们使用 css 变量,因为这些变量(还)不允许转换(参见 答案)
为了缓解这种情况,我们可以设计一个自定义的 cubic-bezier
计时函数,它是平方函数的反函数,但我无法手动执行此操作,并且可能 cubic-bezier
不会给出所有时间值的精确曲线,特别是如果我们希望基本计时函数不是 linear
.
解决方案
方法 1:我们可以改变外部 div 的尺寸而不是缩放,如下所示:
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 100px;
height: 100px;
overflow: hidden;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: top left;
transition: all 1s;
cursor: pointer;
border: 1px solid black;
}
.outer--active {
width: 400px;
height: 400px;
}
.inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg') center repeat;
transform-origin: top left;
transition: transform 1s;
}
<div class="outer">
<div class="inner"></div>
</div>
优点:保留当前的 html 标记结构
缺点:由于有关子像素过渡平滑的浏览器错误(例如 firefox bug report),动画不稳定
方法二:圆形抠图效果使用剪贴蒙版,边框添加div:
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: top left;
cursor: pointer;
}
.rim {
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 100%;
border: 1px solid black;
transition: all 1s;
transform-origin: top left;
}
.outer--active .rim {
transform: scale(4) translate(-50%, -50%);
}
.inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg') center repeat;
transform-origin: top left;
transition: all 1s;
clip-path: circle(50px at 200px 200px);
}
.outer--active .inner {
clip-path: circle(200px at 200px 200px);
}
<div class="outer">
<div class="inner"></div>
<div class="rim"></div>
</div>
优点:平滑扩展
缺点:需要为循环 border/rim 添加另一个 html 标签。边缘有时看起来与内部图像脱节。
这是与其他答案类似的想法,您可以只用一个元素来做到这一点。
增加width/height。
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: calc(100px * var(--s,1));
height: calc(100px * var(--s,1));
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background:
url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg') center ;
transition: all 1.5s;
cursor: pointer;
border: 1px solid black;
}
.outer--active {
--s:4;
}
<div class="outer">
</div>
考虑 clip-path
我在其中添加 radial-gradient
来创建边框
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 400px;
height: 400px;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background:
radial-gradient(farthest-side,transparent calc(100% - 3px),#000 100%),
url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg');
background-size:40% 40%,auto;
background-position:center;
transition: all 1.5s;
cursor: pointer;
-webkit-clip-path: circle(20% at 50% 50%);
clip-path: circle(20% at 50% 50%);
}
.outer--active {
-webkit-clip-path: circle(50% at 50% 50%);
clip-path: circle(50% at 50% 50%);
background-size:100% 100%,auto;
}
<div class="outer">
</div>
也可以只考虑radial-gradient
,但是点击的区域会比较大,没有透明度:
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 400px;
height: 400px;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background:
radial-gradient(farthest-side,transparent calc(40% - 3px),#000 40%,#fff calc(40% + 1px)),
url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg');
background-size:100% 100%,auto;
background-position:center;
transition: all 1.5s;
cursor: pointer;
}
.outer--active {
background-size:240% 240%,auto;
}
<div class="outer">
</div>
运行 下面的代码片段 - 单击圆圈可切换动画。应该发生的是,子容器应该看起来好像没有随着父容器缩放而移动。
这是通过按与放大父组件相同的比例缩小子组件来实现的(父组件缩放到 4,子组件缩放到 0.25)。
动画完成后缩放是正确的,但在动画播放过程中它们似乎没有一致缩放。
这几乎就像父级先缩放,然后完成子级缩放。
这是某种浏览器限制吗?还是我做错了什么?
谢谢!
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 100px;
height: 100px;
overflow: hidden;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: top left;
transition: transform 1s;
cursor: pointer;
border: 1px solid black;
}
.outer--active {
transform: scale(4) translate(-50%, -50%);
}
.inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg') center repeat;
transform-origin: top left;
transition: transform 1s;
}
.outer--active .inner {
transform: scale(0.25) translate(-50%, -50%);
}
<div class="outer">
<div class="inner"></div>
</div>
问题
你看到的不是"browser bug",而是对两个组合缩放的计算方式的误解。
为简单起见,我们假设转换函数是 linear
(而不是 ease
,这是默认的计时函数)。在那种情况下,两个尺度的图如下:
由于我们想让内部元素的最终比例保持不变,那么所有时间参数的(放大函数)×(缩小函数)= 1。不幸的是,如果我们进行乘法运算,结果会得到一个平方函数(在我们的例子中是 -¾x² + 3x + ¾)。这是您在过渡过程中可以看到的最终缩放中的凸起。为了避免这种情况,我们需要在 scale(1/m)
css 规则中缩放 m
,而不是转换 scale(n)
值。不幸的是,我们不能这样做,即使我们使用 css 变量,因为这些变量(还)不允许转换(参见
为了缓解这种情况,我们可以设计一个自定义的 cubic-bezier
计时函数,它是平方函数的反函数,但我无法手动执行此操作,并且可能 cubic-bezier
不会给出所有时间值的精确曲线,特别是如果我们希望基本计时函数不是 linear
.
解决方案
方法 1:我们可以改变外部 div 的尺寸而不是缩放,如下所示:
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 100px;
height: 100px;
overflow: hidden;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: top left;
transition: all 1s;
cursor: pointer;
border: 1px solid black;
}
.outer--active {
width: 400px;
height: 400px;
}
.inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg') center repeat;
transform-origin: top left;
transition: transform 1s;
}
<div class="outer">
<div class="inner"></div>
</div>
优点:保留当前的 html 标记结构
缺点:由于有关子像素过渡平滑的浏览器错误(例如 firefox bug report),动画不稳定
方法二:圆形抠图效果使用剪贴蒙版,边框添加div:
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: top left;
cursor: pointer;
}
.rim {
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 100%;
border: 1px solid black;
transition: all 1s;
transform-origin: top left;
}
.outer--active .rim {
transform: scale(4) translate(-50%, -50%);
}
.inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg') center repeat;
transform-origin: top left;
transition: all 1s;
clip-path: circle(50px at 200px 200px);
}
.outer--active .inner {
clip-path: circle(200px at 200px 200px);
}
<div class="outer">
<div class="inner"></div>
<div class="rim"></div>
</div>
优点:平滑扩展
缺点:需要为循环 border/rim 添加另一个 html 标签。边缘有时看起来与内部图像脱节。
这是与其他答案类似的想法,您可以只用一个元素来做到这一点。
增加width/height。
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: calc(100px * var(--s,1));
height: calc(100px * var(--s,1));
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background:
url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg') center ;
transition: all 1.5s;
cursor: pointer;
border: 1px solid black;
}
.outer--active {
--s:4;
}
<div class="outer">
</div>
考虑 clip-path
我在其中添加 radial-gradient
来创建边框
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 400px;
height: 400px;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background:
radial-gradient(farthest-side,transparent calc(100% - 3px),#000 100%),
url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg');
background-size:40% 40%,auto;
background-position:center;
transition: all 1.5s;
cursor: pointer;
-webkit-clip-path: circle(20% at 50% 50%);
clip-path: circle(20% at 50% 50%);
}
.outer--active {
-webkit-clip-path: circle(50% at 50% 50%);
clip-path: circle(50% at 50% 50%);
background-size:100% 100%,auto;
}
<div class="outer">
</div>
也可以只考虑radial-gradient
,但是点击的区域会比较大,没有透明度:
const outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
outer.classList.toggle('outer--active');
});
body { overflow: hidden; }
.outer {
width: 400px;
height: 400px;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background:
radial-gradient(farthest-side,transparent calc(40% - 3px),#000 40%,#fff calc(40% + 1px)),
url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/14.jpg');
background-size:100% 100%,auto;
background-position:center;
transition: all 1.5s;
cursor: pointer;
}
.outer--active {
background-size:240% 240%,auto;
}
<div class="outer">
</div>