直播端尤其是观众端,交互很多,动画显得尤为重要,张狂地址赞、谈论,促进用户重视直播间,以及与抽奖的联动,提高用户留存率和转化率。
一、点赞动画
点赞是直播间很重要的交互,经过诱导用户张狂点赞来增加与直播间的交互,更能留住用户
作用示例
原理
- 1、点击点赞按钮时,点赞作用图片曲线移动,能够用正弦函数描述(X:正弦函数/Y:匀速增加);
- 2、图片先变大再变小;
- 3、透明度0->1->0;
- 4、接连点赞图片随机方向,不重叠
完成
咱们先完成1个图片的曲线运动
1、css完成
图片沿曲线运动,最简单想到的便是svg中的animateMotion
,沿运动途径位移
用法:
<animateMotion path="" dur="" repeatCount="" path="" begin=""/>
特点:
-
path
: 此特点界说运动的途径。 -
keyPoints
: 此特点表明在[0,1]范围内,每个keyTimes关联值的对象在途径中的间隔。 -
rotate
:此特点界说应用于沿途径动画的元素的旋转,通常是使其指向动画的方向。 -
repeatCount
:重复次数,默以为1次,indefinite无限循环。 -
dur
:继续时长。 -
begin
:开端方式。
因而,生成一段曲线的path,使图片沿着该path运动即可,能够凭借Adobe Illustrator来生成一段曲线并拿到path途径。(途径随便画的)然后 存储为->挑选svg格式->查看svg代码
得到这样一段代码
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 595.28 841.89" enable-background="new 0 0 595.28 841.89" xml:space="preserve">
<circle fill="#DCAB7F" cx="299.9" cy="725.24" r="35.44"/>
<path fill="none" stroke="#050101" stroke-miterlimit="10" d="M299.9,725.24c0-38.27-97.48-32.52-92.42-111
c5.06-78.48,36.73-81.01,92.42-105.06s122.77,0,124.04-97.47c1.27-97.47-92.38-87.34-124.04-94.94s-113.94,2.53-126.59-75.95
C160.65,162.34,299.9,100.69,299.9,80.02c0-26.6,0,26.6,0,0"/>
</svg>
path里边的途径便是图片的运动轨迹
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 595.28 841.89" enable-background="new 0 0 595.28 841.89" xml:space="preserve">
<circle fill="#DCAB7F" cx="0" cy="0" r="35.44">
<animateMotion path="M299.9,725.24c0-38.27-97.48-32.52-92.42-111
c5.06-78.48,36.73-81.01,92.42-105.06s122.77,0,124.04-97.47c1.27-97.47-92.38-87.34-124.04-94.94s-113.94,2.53-126.59-75.95
C160.65,162.34,299.9,100.69,299.9,80.02c0-26.6,0,26.6,0,0" dur="5s" repeatCount="1" begin="1s"/>
</circle>
</svg>
再参加css动画来操控巨细和透明度的改动即可,但运用css的坏处是,多次点赞时需求生成多个dom,在动画完毕后需求及时铲除这些dom,不然简单造成功能问题。
2、canvas动画
关于图片随机/开端运动方向随机,运用js肯定是最好的,运用canvas烘托能更好满足细节需求
- 新建canvas画布
<canvas id="thumbsCanvas" width="200" height="400"></canvas>
<button onclick="handleThumbsUp()">点赞</button>
- 烘托图片
var thumbsCanvas=document.getElementById('thumbsCanvas'),
ctx=thumbsCanvas.getContext('2d'),
w=thumbsCanvas.width,
h=thumbsCanvas.height,
img=new Image();
img.src="https://cdn.bfonline.com/scrm/heathH5/live/thumbsup1.png"
var imgX=w/2;
var imgY=h;
img.onload=function(){
imgX=w/2-img.width/6;
imgY=h-img.height/3;
ctx.drawImage(img,w/2-img.width/6,h-img.height/3,img.width/3,img.height/3)
}
- 增加动画
function trans(){
ctx.clearRect(imgX,imgY,img.width/3,img.height/3);//擦除之前画的图片
imgY-=1;//坠落速度可调节
ctx.drawImage(img,imgX,imgY,img.width/3,img.height/3);//从头画
if(imgY>0){
window.requestAnimationFrame(trans)
}
}
function handleThumbsUp(){
trans();
}
- 增加曲线作用,即imgX也增加改动,咱们想要图片左右摆动,正好能够学习正弦函数,但假如仅仅简单的
imgX=Math.sin(imgY)
,那么只会得到一个左右摇晃上升的作用。因而需求凭借正弦函数解析式y=Asin(wx+)+b
(死去的高中回忆开端攻击我),经过改动A
//
/
b
的值,得到一条比较舒畅的上升曲线。
A
:决议峰值(即纵向拉伸压缩的倍数) :决议周期
:决议波形与X轴方位联系或横向移动间隔(左加右减)
b
:表明波形在Y轴的方位联系或纵向移动间隔(上加下减)
onMounted(() => {
nextTick(() => {
thumbsUpAni = new ThumbsUpAni();
});
});
function getRandom(min, max) {
return min + Math.floor(Math.random() * (max - min + 1));
}
class ThumbsUpAni {
imgsList = [];
context;
width = 0;
height = 0;
scanning = false;
renderList = [];
scaleTime = 0.1; // 百分比
constructor() {
this.loadImages();
const canvas = document.getElementById("likeCanvas");
this.context = canvas.getContext("2d");
this.width = canvas.width;
this.height = canvas.height;
}
loadImages() {
const images = [
"thumbsup1.png",
"thumbsup2.png",
"thumbsup3.png",
"thumbsup4.png",
"thumbsup5.png",
];
const promiseAll = [];
images.forEach((src) => {
const p = new Promise(function (resolve) {
const img = new Image();
img.onerror = img.onload = resolve.bind(null, img);
img.src = "https://cdn.bfonline.com/scrm/heathH5/live/" + src;
});
promiseAll.push(p);
});
Promise.all(promiseAll).then((imgsList) => {
this.imgsList = imgsList.filter((d) => {
if (d && d.width > 0) return true;
return false;
});
// if (this.imgsList.length == 0) {
// dLog('error', 'imgsList load all error');
// return;
// }
});
}
createRender() {
if (this.imgsList.length == 0) return null;
const basicScale = [0.6, 0.9, 1][getRandom(0, 2)];
const getScale = (diffTime) => {
if (diffTime < this.scaleTime) {
return +(diffTime / this.scaleTime).toFixed(2) * basicScale;
} else {
return basicScale;
}
};
const context = this.context;
// 随机读取一个图片来烘托
const image = this.imgsList[getRandom(0, this.imgsList.length - 1)];
const offset = 20;
const basicX = this.width / 2 + getRandom(-offset, offset);
const angle = getRandom(2, 10);
let ratio = getRandom(5, 40) * (getRandom(0, 1) ? 1 : -1);
const getTranslateX = (diffTime) => {
if (diffTime < this.scaleTime) {
// 扩大期间,不进行摇晃位移
return basicX;
} else {
return basicX + ratio * Math.sin(angle * (diffTime - this.scaleTime));
}
};
const getTranslateY = (diffTime) => {
return (
image.height / 2 + (this.height - image.height / 2) * (1 - diffTime)
);
};
const fadeOutStage = getRandom(14, 18) / 100;
const getAlpha = (diffTime) => {
let left = 1 - +diffTime;
if (left > fadeOutStage) {
return 1;
} else {
return 1 - +((fadeOutStage - left) / fadeOutStage).toFixed(2);
}
};
return (diffTime) => {
// 差值满了,即完毕了 0 ---》 1
if (diffTime >= 1) return true;
context.save();
const scale = getScale(diffTime);
// const rotate = getRotate();
const translateX = getTranslateX(diffTime);
const translateY = getTranslateY(diffTime);
context.translate(translateX, translateY);
context.scale(scale, scale);
// context.rotate(rotate * Math.PI / 180);
context.globalAlpha = getAlpha(diffTime);
context.drawImage(
image,
-10,
-image.height / 2,
image.width / 2,
image.height / 2
);
context.restore();
};
}
scan() {
this.context.clearRect(0, 0, this.width, this.height);
this.context.fillStyle = "rgba(0,0,0,0)";
this.context.fillRect(0, 0, 200, 400);
let index = 0;
let length = this.renderList.length;
if (length > 0) {
requestFrame(this.scan.bind(this));
this.scanning = true;
} else {
this.scanning = false;
}
while (index < length) {
const child = this.renderList[index];
if (
!child ||
!child.render ||
child.render.call(null, (Date.now() - child.timestamp) / child.duration)
) {
// 完毕了,删除该动画
this.renderList.splice(index, 1);
length--;
} else {
// continue
index++;
}
}
}
start() {
const render = this.createRender();
const duration = getRandom(1500, 3000);
this.renderList.push({
render,
duration,
timestamp: Date.now(),
});
if (!this.scanning) {
this.scanning = true;
requestFrame(this.scan.bind(this));
}
return this;
}
}
function requestFrame(cb) {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
}
)(cb);
}
二、重视动画
重视动画最重要的便是✓
的描绘,这里用到的仍旧是svg
的描边动画。
- 原理
stroke
系列特点主要与边有关比如:stroke
边的颜色;stroke-width
边的粗细stroke-linecap
边开始与结尾的形状;stroke-linejoin
边折角时的形状;
而描边动画主要与以下两个特点有关:stroke-dasharray
和stroke-dashoffset
。
stroke-dasharray
该特点操控途径中虚线的长度以及虚线间的间隔;stroke-dashoffset
该特点指定了虚线开端时的偏移长度,正数从途径开始点向前偏移,负数则向后;
<svg width="600px" height="300px" viewBox="0 0 600 100">
<line x1="20" y1="20" x2="500" y2="20" style="stroke: black;"/>
</svg>
一条线
参加stroke-dasharray,成为一条虚线
<svg width="600px" height="300px" viewBox="0 0 600 100">
<line x1="20" y1="20" x2="500" y2="20" stroke-dasharray='20' style="stroke: black;"/>
</svg>
stroke-dasharray='20'
stroke-dasharray='20 60'
stroke-dasharray='20 40 60'
stroke-dashoffset
为虚线偏移长度
<svg width="600px" height="300px" viewBox="0 0 600 100">
<line x1="20" y1="20" x2="500" y2="20" stroke-dasharray='20' stroke-dashoffset='10' style="stroke: black;"/>
</svg>
虚线段仍为20,但向左偏移10
假如把stroke-dashoffset='10'
用在动画中
<svg width="600px" height="300px" viewBox="0 0 600 100">
<line x1="20" y1="20" x2="500" y2="20" stroke-dasharray='20' stroke-dashoffset="10" style="stroke: black;">
<animate sttributeType="CSS" attributeName="stroke-dashoffset" begin="0s" from="120" to="-120" dur="2" fill="freeze" repeatCount="indefinite"></animate>
</line>
</svg>
假如将stroke-dasharray
长度扩大至正好为直线的长度,就会得到一条渐隐或许逐渐出现的线,这便是svg的描边动画。
<svg width="600px" height="300px" viewBox="0 0 600 100">
<line x1="20" y1="20" x2="500" y2="20" stroke-dasharray='480' stroke-dashoffset="480" style="stroke: black;">
<animate sttributeType="CSS" attributeName="stroke-dashoffset" begin="0s" from="480" to="-480" dur="2" fill="freeze" repeatCount="indefinite"></animate>
</line>
</svg>
- 完成
重视动画,咱们要描的是
✓
,能够运用js的getTotalLength()
方法获取path的途径总长度
<svg viewBox="0 0 400 400" width="30" height="30">
<polyline id="followCheck"
fill="none"
stroke="#ffffff"
stroke-width="24"
points="88,214 173,284 304,138"
stroke-linecap="round"
stroke-linejoin="round"
></polyline>
</svg>
css
#followCheck {
stroke-dasharray: 350;
stroke-dashoffset: 0;
animation: check 1s ease-out forwards;
transform-origin: center;
}
@keyframes check {
0% {
stroke-dashoffset: 350;
transform: scale(1);
}
60% {
stroke-dashoffset: 0;
transform: scale(1);
}
100% {
stroke-dashoffset: 0;
transform: scale(0.8);
}
}
三、抽奖倒计时
举一反三,仍旧能够将描边动画运用到抽奖的圆环倒计时中
<svg class="cir_svg" width="100px" height="100px" viewbox="0,0,100,100">
<circle id="lottery_circle" cy="51" cx="50" r="47" stroke="#fff000" fill="none" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="300" stroke-dashoffset="-300">
<animate
sttributeType="CSS"
attributeName="stroke-dashoffset"
begin="0s"
from="0"
to="300"
dur="5s"
fill="freeze"
repeatCount="1"
></animate>
</circle>
</svg>
四、谈论翻翻滚画
谈论动画,需求每一条新的谈论向上翻滚,当用户翻滚翻看下方音讯时,底部出现新增条数
- 谈论动画&n条音讯 html
<div class="chatWrapper" ref="chatWrapperRef">
<ul class="chatList flex-ssl" ref="chatListRef">
<li
class="chatList_li fw-b"
v-for="(item, index) in chatList"
:key="index"
:id="`chatItem${item.comment_id}`"
>
<span :class="item.role == 'AUDIENCE' ? '' : 'colorHost'"
>{{ item.role == "AUDIENCE" ? item.user_name : "主播" }}:</span
>
<span>{{ item.comment }}</span>
</li>
</ul>
</div>
<div
class="newChat mt-8"
v-show="isRestVisiable && restComment > 0"
@click="handlerScrollBottom"
>
{{ restComment }}条音讯 <i class="icon-chakangengduo"></i>
</div>
js,每逢有一条新的音讯,就将其增参加queue()
中
// 聊天记录参加行列
const queue = async (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
addComment(data);
resolve();
}, 500);
});
};
function addComment(data) {
chatList.value.push(data);
nextTick(() => {
renderComment();
});
}
function addScroll() {
__.m.debounce(listScroll, 200);
isRestVisiable.value = true;
}
function listScroll() {
const ele = chatWrapperRef.value;
const isBottom = isScrollBottom(ele, ele.clientHeight);
if (isBottom) {
isRestVisiable.value = false;
restNums.value = 0;
restComment.value = 0;
}
}
function isScrollBottom(ele, wrapHeight, threshold = 30) {
const h1 = ele.scrollHeight - ele.scrollTop;
const h2 = wrapHeight + threshold;
const isBottom = h1 <= h2;
return isBottom;
}
// 翻滚到最底部
function handlerScrollBottom() {
restNums.value = 0; // 铲除剩余音讯
restComment.value = restNums.value;
isRestVisiable.value = false;
chatWrapperRef.value.removeEventListener("scroll", addScroll);
chatWrapperRef.value.scrollTo({
top: chatListRef.value.offsetHeight,
left: 0,
behavior: "smooth",
});
}
function renderComment() {
const listHight = chatListRef.value.offsetHeight;
const diff = listHight - chatWrapperRef.value.offsetHeight; // 列表高度与容器高度差值
const top = chatWrapperRef.value.scrollTop; // 列表翻滚高度
if (diff - top < 50) {
if (diff > 0) {
if (isRestVisiable.value) {
isRestVisiable.value = false;
chatWrapperRef.value.removeEventListener("scroll", addScroll);
}
chatWrapperRef.value.scrollTo({
top: diff + 10,
left: 0,
behavior: "smooth",
});
restNums.value = 0;
}
} else {
++restNums.value;
if (!isRestVisiable.value) {
isRestVisiable.value = true;
chatWrapperRef.value.addEventListener("scroll", addScroll);
}
}
restComment.value = restNums.value >= 99 ? "99+" : restNums.value;
}
- 滑动至@ html
<div class="newChat" @click="handleScrollToAt" v-show="isAtVisiable">
有人@你
</div>
js 运用scrollIntoView()
方法
function handleScrollToAt() {
isAtVisiable.value = false;
nextTick(() => {
const scrollItem = document.getElementById(`chatItem${atCommentId.value}`);
if (scrollItem && scrollItem.offsetTop) {
scrollItem.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "nearest",
});
}
});
}
- XXX来了 html
<transition name="newAud">
<div class="comeAudience flex-sc" v-show="isNewVisiable">
<div class="mr-4 elp1 audName">{{ newAudience }}</div>
来了
</div>
</transition>
这里的动画运用的是vue中的过渡作用
@keyframes appear {
0% {
transform: translateX(-200px);
}
100% {
transform: translateX(0px);
}
}
@keyframes disappear {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.newAud-enter-active {
animation: appear 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.newAud-leave-active {
animation: disappear 0.8s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
以上为直播间常用的一些动画,涉及到css/svg/canvas,动画完成途径不止一条,本文只提供思路。