flip 一种简单的动画思路
无意间看到某博主文章,介绍关于 filp 如何制作动画,觉得有趣,便自己动手将dome实现了一遍,参考原文链接在此
FLIP
f - first 记录动画开始前的位置、大小等信息 ( translateY(0px) )
l - last 记录动画结束时的位置、大小等信息 ( translateY(100px) )
i - invert 对动画前后数据信息的计算(translateY --> 100px,同时利用translate等操作,将dom恢复到 first位置)
p - play 开始动画,并移除 i 步骤恢复至 first 的操作,启用tansition,动画就开始了
整个过程其实就是,先记录好动画前后的dom位置等数据信息 然后,利用css将dom恢复至初始位置,最后移除上一步恢复的状态(此时dom会自动回到last位置,只不过没有过渡效果,生硬的闪现),添加过渡效果,完成动画
栗子一(卡片增删动画)
效果如下: 思路整理:
需求分析:
1、没点击增加按钮,就创建一个dom,然后插入到当前列表前面
2、点击删除符号,就将其dom删除
实现1,2步骤不难,利用创建和删除dom的api即可
动画分析:
运用flip思路
1、f -- 记录当前dom的布局信息(getBoundingClientRect)可以实现< 2、l -- 同样运用 getBoundingClientRect 也可以获取到插入新增dom后的布局信息 3、i 和 p 过程,前两步拿到了 动画前和动画后 布局信息的变化,接下来就是 i 过程,运用css translate3d将dom恢复至动画前的位置(即插入dom前) 4、p -- 移除translate3d的恢复,此时dom会恢复到插入后的位置,此时再添加过渡效果,就可以了
具体代码
增加按钮
function addBtn() {
const divEle = document.getElementsByClassName('item')
// 初始位置
record(divEle)
addEle()
// 结束位置 + 复位 + play
last(divEle)
}
记录初始位置
function record(eleAll) {
for( let i = 0;i < eleAll.length; i++ ) {
const { top,left } = eleAll[i].getBoundingClientRect()
eleAll[i]._top_ = top
eleAll[i]._left_ = left
}
}
插入dom
function addEle() {
const Box = document.getElementsByClassName('box')[0]
const div = document.createElement('div'), span = document.createElement('span')
span.className = 'del'
div.className = 'item'
div.innerHTML = index++
div.appendChild(span)
span.style.color = randomHexColorCode()
div.style.borderColor = randomHexColorCode()
div.style.zIndex = 'auto'
span.onclick = delBtn
Box.insertBefore(div,Box.childNodes[0])
}
结束位置+动画
function last(eleAll) {
for( let i = 0;i < eleAll.length; i++ ) {
const dom = eleAll[i]
const { top,left } = dom.getBoundingClientRect()
// 新增dom时,逻辑应为 原有dom后移动,新增dom不动,故记录了位置的才添加动画
if(dom._left_) {
// 恢复至开始位置
dom.style.transform = `translate3d(${ dom._left_ - left }px, ${ dom._top_ - top }px,0px)`
// play 过程,移除开始位置的设置,添加过渡
requestAnimationFrame(function() {
//启用tansition,并移除翻转的改变
dom.classList.add('active')
dom.style.transform = 'none'
})
dom.addEventListener('transitionend', () => {
dom.classList.remove('active')
})
// 兼容性不好
// dom.animate([
// { transform: `translate(${dom._left_ - left}px, ${dom._top_ - top}px)` },
// { transform: `translate(0px, 0px)` },
// ], { duration: 400 })
}
}
}
栗子二(图片预览)
先看效果 效果我们可以看出,图片从初始位置,移动到中心位置,并且是带动画效果逐渐放大的
分析 还是依托flip那几步骤
1、初始位置和结束位置getBoundingClientRect同样可以得到,无非就是再加上一个缩放比 2、同样地,过程还是动画前,先恢复至小图位置 3、移除恢复,添加过渡效果
代码
点击预览
let originTop = 0,originLeft = 0,scale = 0;
// 点击预览
function preview(e) {
// 记录 first 位置
recordImg(e)
const { src } = e.childNodes[1]
// 插入dom,获取 last 位置
previewImg(src)
}
记录位置
function recordImg(ele) {
const { top,left } = ele.getBoundingClientRect()
originTop = top
originLeft = left
}
预览
function previewImg(src) {
const img = document.createElement('img')
img.src = src
img.className = 'preview_img'
document.body.appendChild(img)
img.onclick = previewClose
// 图片加载onload问题,直接出现,暂时设置不可见
img.style.display = 'none'
img.onload = function() {
img.style.display = 'block'
// 计算缩放比
scale = 50 / img.width
// 获取 last 位置
const { top,left } = img.getBoundingClientRect()
img._top_ = top
img._left_= left
// 计算 invert 图片回归原位(first)
img.style.transform = `translate3d(${ originLeft - left }px, ${ originTop - top }px,0px) scale(${scale})`
img.style.transformOrigin = '0 0'
// play 过程,移除回归原位的设置
playImg(img)
}
}
paly 过程
function playImg(ele) {
requestAnimationFrame(function() {
//启用tansition,并移除翻转的改变
ele.style.transform = `translate3d(0px,0px,0px) scale(1)`
ele.style.zIndex = `2`
// 遮罩
const preview = document.getElementsByClassName('preview')[0]
preview.style.opacity = '1'
preview.style.zIndex = '1'
preview.classList.add('active_preview')
ele.classList.add('active2')
})
}
个人理解,flip是一种做动画的思路,当面对一些复杂,难以通过一些简单的过渡来实现的,可以采取此思路
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body,h3{
padding: 0;
margin: 0;
}
.all{
padding: 30px;
width: 700px;
margin: 0 auto;
}
h3{
padding: 10px 0;
}
.box{
padding: 30px;
min-height: 100px;
overflow: hidden;
border: 1px solid greenyellow;
}
.item{
width: 150px;
height: 150px;
border: 2px solid #e8e8e8;
border-radius: 5px;
position: relative;
box-sizing: border-box;
margin-bottom: 10px;
margin-right: 12px;
float: left;
background-color: #fff;
padding: 5px;
}
.item:hover{
box-shadow: 0px 0px 15px #eee;
transform: translate3d(0px,2px,0px);
transition: all .3s ease-out;
}
.item:nth-child(4n) {
margin-right: 0px;
}
.del{
float: right;
display: flex;
padding: 5px;
width: 10px;
height: 10px;
position: relative;
margin-right: 10px;
cursor: pointer;
}
.del::before{
content: '一';
position: absolute;
top: 0;
transform: rotateZ(45deg);
}
.del::after{
content: '一';
position: absolute;
top: 0px;
transform: rotateZ(-45deg);
}
.active{
transition: transform 400ms ease-out;
}
.active2{
transition: all .6s ease-in-out;
}
.active_preview{
transition: opacity .6s ease-in-out;
}
hr{
margin: 20px 0;
}
.item_img{
width: 50px;
display: inline-flex;
border-radius: 5px;
overflow: hidden;
cursor: pointer;
position: relative;
}
.item_img:hover::before {
opacity: 1;
}
.item_img::before {
content: " ";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
background-color: rgba(0,0,0,.5);
transition: all .3s;
z-index: 1;
}
.item_img img{
width: 100%;
}
.preview{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,.7);
z-index: -1;
opacity: 0;
}
.preview_img{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="all" >
<h3>
<span>flip动画</span>
<button onclick="addBtn()" > 增加 </button>
</h3>
<div class="box" >
<div class="item"></div>
</div>
</div>
<hr>
<!-- 图片预览 -->
<h3>
<span>图片预览</span>
</h3>
<div class="box" style="max-width: 800px;" >
<div class="item_img" onclick="preview(this)" >
<img src="https://i.picsum.photos/id/155/467/680.jpg?hmac=sWw0LvYr7xc_V-bjOgZgwCTKCrWXXAfFzTLR5E16jG8" alt="">
</div>
</div>
<!-- 预览区域 -->
<div class="preview"></div>
<script>
// flip 动画思路
/**
* f - first 记录动画开始前的位置、大小等信息 ( translateY(0px) )
* l - last 记录动画结束时的位置、大小等信息 ( translateY(100px) )
* i - invert 对动画前后数据信息的计算(translateY --> 100px,同时利用translate等操作,将dom恢复到 first位置)
* p - play 开始动画,并移除 i 步骤恢复至 first 的操作,启用tansition,动画就开始了
*
* 整个过程其实就是,先记录好动画前后的dom位置等数据信息
* 然后,利用css将dom恢复至初始位置
* 最后,移除上一步恢复的状态(此时dom会自动回到last位置,只不过没有过渡效果,生硬的闪现),添加过渡效果,完成动画
* **/
let index = 1
// 增加 按钮
function addBtn() {
const divEle = document.getElementsByClassName('item')
// 初始位置
record(divEle)
addEle()
// 结束位置 + 复位 + play
last(divEle)
}
// 记录初始位置
function record(eleAll) {
for( let i = 0;i < eleAll.length; i++ ) {
const { top,left } = eleAll[i].getBoundingClientRect()
eleAll[i]._top_ = top
eleAll[i]._left_ = left
}
}
// 插入dom,此时dom结构已经发生变化
function addEle() {
const Box = document.getElementsByClassName('box')[0]
const div = document.createElement('div'), span = document.createElement('span')
span.className = 'del'
div.className = 'item'
div.innerHTML = index++
div.appendChild(span)
span.style.color = randomHexColorCode()
div.style.borderColor = randomHexColorCode()
div.style.zIndex = 'auto'
span.onclick = delBtn
Box.insertBefore(div,Box.childNodes[0])
}
// 结束位置
function last(eleAll) {
for( let i = 0;i < eleAll.length; i++ ) {
const dom = eleAll[i]
const { top,left } = dom.getBoundingClientRect()
// 新增dom时,逻辑应为 原有dom后移动,新增dom不动,故记录了位置的才添加动画
if(dom._left_) {
/**
* flip 思路:
* 1、计算出开始和结束的状态差
* 2、dom变化后,通过transform直接让dom位于最开始状态
* 3、添加 transition ,移除 transform(让dom位于初始状态的属性)
* 4、dom 自动恢复结束状态
* **/
// 恢复至开始位置
dom.style.transform = `translate3d(${ dom._left_ - left }px, ${ dom._top_ - top }px,0px)`
// play 过程,移除开始位置的设置,添加过渡
requestAnimationFrame(function() {
//启用tansition,并移除翻转的改变
dom.classList.add('active')
dom.style.transform = 'none'
})
dom.addEventListener('transitionend', () => {
dom.classList.remove('active')
})
// 兼容性不好
// dom.animate([
// { transform: `translate(${dom._left_ - left}px, ${dom._top_ - top}px)` },
// { transform: `translate(0px, 0px)` },
// ], { duration: 400 })
}
}
}
// 删除
function delBtn({ target }) {
const divEle = document.getElementsByClassName('item')
record(divEle)
delEle(target)
last(divEle)
}
// 删除 dom
function delEle(target) {
target.parentNode.remove()
}
// ----------图片预览--------
// 图片原始位置
let originTop = 0,originLeft = 0,scale = 0;
// 点击预览
function preview(e) {
// 记录 first 位置
recordImg(e)
const { src } = e.childNodes[1]
// 插入dom,获取 last 位置
previewImg(src)
}
// first 记录位置
function recordImg(ele) {
const { top,left } = ele.getBoundingClientRect()
originTop = top
originLeft = left
}
// 预览
function previewImg(src) {
const img = document.createElement('img')
img.src = src
img.className = 'preview_img'
document.body.appendChild(img)
img.onclick = previewClose
// 图片加载onload问题,直接出现,暂时设置不可见
img.style.display = 'none'
img.onload = function() {
img.style.display = 'block'
// 计算缩放比
scale = 50 / img.width
// 获取 last 位置
const { top,left } = img.getBoundingClientRect()
img._top_ = top
img._left_= left
// 计算 invert 图片回归原位(first)
img.style.transform = `translate3d(${ originLeft - left }px, ${ originTop - top }px,0px) scale(${scale})`
img.style.transformOrigin = '0 0'
// play 过程,移除回归原位的设置
playImg(img)
}
}
// paly 过程
function playImg(ele) {
requestAnimationFrame(function() {
//启用tansition,并移除翻转的改变
ele.style.transform = `translate3d(0px,0px,0px) scale(1)`
ele.style.zIndex = `2`
// 遮罩
const preview = document.getElementsByClassName('preview')[0]
preview.style.opacity = '1'
preview.style.zIndex = '1'
preview.classList.add('active_preview')
ele.classList.add('active2')
})
}
// 点击缩小
function previewClose({ target }) {
target.style.transform = `translate3d(${ originLeft - target._left_ }px, ${ originTop - target._top_ }px,0px) scale(${scale})`
const preview = document.getElementsByClassName('preview')[0]
preview.style.opacity = '0'
target.addEventListener('transitionend', () => {
target.remove()
preview.style.zIndex = '-1'
})
}
// 随机颜色
function randomHexColorCode() {
let n = (Math.random() * 0xfffff * 1000000).toString(16);
return '#' + n.slice(0, 6);
};
</script>
</body>
</html>