直播端尤其是观众端,交互很多,动画显得尤为重要,张狂地址赞、谈论,促进用户重视直播间,以及与抽奖的联动,提高用户留存率和转化率。

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

一、点赞动画

点赞是直播间很重要的交互,经过诱导用户张狂点赞来增加与直播间的交互,更能留住用户

作用示例

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

原理

  • 1、点击点赞按钮时,点赞作用图片曲线移动,能够用正弦函数描述(X:正弦函数/Y:匀速增加);
  • 2、图片先变大再变小;
  • 3、透明度0->1->0;
  • 4、接连点赞图片随机方向,不重叠
一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

完成

咱们先完成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代码
    一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

得到这样一段代码

<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>
一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

再参加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();
    }
一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖
  • 增加曲线作用,即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);
}

二、重视动画

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

重视动画最重要的便是的描绘,这里用到的仍旧是svg的描边动画。

  • 原理

stroke系列特点主要与边有关比如:stroke边的颜色;stroke-width边的粗细stroke-linecap边开始与结尾的形状;stroke-linejoin边折角时的形状;

而描边动画主要与以下两个特点有关:stroke-dasharraystroke-dashoffsetstroke-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>

一条线

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

参加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'

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖
stroke-dasharray='20 60'
一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖
stroke-dasharray='20 40 60'
一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

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

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

假如把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>

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

假如将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>

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

  • 完成 重视动画,咱们要描的是,能够运用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);
      }
}

三、抽奖倒计时

举一反三,仍旧能够将描边动画运用到抽奖的圆环倒计时中

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

 <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>

四、谈论翻翻滚画

谈论动画,需求每一条新的谈论向上翻滚,当用户翻滚翻看下方音讯时,底部出现新增条数

一篇搞定H5直播中的小动画,谈论/点赞/重视/抽奖

  • 谈论动画&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,动画完成途径不止一条,本文只提供思路。