🎈不知道大家平时在逛B站的时候有没有发现这么一个功能?在视频封面移入鼠标时我们可以对视频进行预览,预览过后再决定时候要点进去观看视频,那么这个实现具体是怎么实现的呢?让我们一起动手来试一下吧。



我们首先应该要对组件进行一个简单的设计。

主要的逻辑如上图☝️☝️☝️,可以拆分成这么几个步骤:
我们可以先将视频各个时间的关键帧截图保存,具体截取帧数可以使用传入参数控制。
在鼠标移入的时候我们应该要计算当前鼠标位置和视频宽度的比例关系,然后从视频帧列表中获取到对应的图片作为当前的视频封面图片。
这里我们是将用两个元素分别作为视频和封面,所以我们状态切换的时候需要控制两个元素的显示和隐藏。
显示并播放视频,隐藏封面。
显示封面,隐藏视频。
分析完组件的关键步骤之后我们便可以开始动手来实现相应的功能了。
视频关键帧的截取我们可以使用canvas来实现,具体实现方法如下:
/**
* @param {element} video
* @param {number} currentTime
* @return {void}
*/
cutCover(video, currentTime) {
video.currentTime = currentTime;
const canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = parseInt(this.width);
canvas.height = parseInt(this.height);
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const img = canvas.toDataURL("image/png");
return img;
},
通过该函数我们可以获取指定时间的视频图片帧。
/**
* @param {element} video
* @param {number} currentTime
*/
video为需要截取视频的dom元素,currentTime为要截取图片帧的时间点。
/**
* @return {Base64} img
*/
返回参数为截取的指定帧的Base64格式的图片。
stepNums为我们传入的组件参数,及需要截取的封面关键帧图片数量,数量越多,预览的效果越连贯,可以根据视频长度来调整截取的张数。
init(){
const videoContentShow = document.getElementById(
this.uid + "-video"
);
videoContentShow.style.height = this.height;
videoContentShow.style.width = this.width;
const videoContent = videoContentShow.cloneNode();
videoContent.addEventListener("canplay", () => {
if (this.currentTime < this.duration) this.cut(videoContent);
else this.progressValue = 0;
});
}
cut(video) {
const duration = video.duration;
this.duration = duration;
this.currentTime += duration / this.stepNums;
const img = this.cutCover(video, this.currentTime);
this.imgList.push(img);
if (this.imgList.length == 2) {
this.coverSrc = img;
const coverImg = document.getElementById(
this.uid + "-coverImg"
);
coverImg.setAttribute("src", img);
}
}
具体代码如上,首先我们应该先要获取到视频的dom元素,但是要注意:我们不在原始视频元素上进行截取操作,我们这里使用了cloneNode()来克隆一个dom元素进行操作。因为在进行截取的时候我们需要对视频的currentTime属性进行一个修改,也就是改变视频的播放进度,如果在原视频上截取的话,在未截取完成前播放视频会导致视频播放进度混乱,所以这里我们在克隆元素对象上进行操作。
我们总共需要截取stepNums张图片,所以每次截取的时间间隔应该为:duration / this.stepNums,即视频总时间长度/截取图片张数,循环截取即可。
鼠标移入封面的时候我们需要对封面图片进行切换。
<img
:id="uid + '-coverImg'"
:src="coverSrc"
class="j-coverImg"
@mousemove="imgHover"
@mouseleave="hoverOut"
@click="coverClick"
/>
这里我们使用vue中的mousemove和mouseleave对鼠标事件进行监听。
imgHover(e) {
const coverImg = document.getElementById(this.uid + "-coverImg");
const w = coverImg.offsetWidth / this.stepNums;
const x = e.offsetX - coverImg.offsetLeft;
const index = Math.min(
Math.max(Math.ceil(x / w), 1),
this.stepNums
);
if (this.imgList.length < index) return;
this.progressValue = index;
coverImg.setAttribute(
"src",
this.imgList[Math.min(this.imgList.length - 1, index)]
);
},
鼠标移入的时候我们需要根据鼠标的坐标位置来计算展示的帧数下标,具体计算如下:
const w = coverImg.offsetWidth / this.stepNums;
每个区间的大小我们只需要将封面的宽度除于图片帧列表的数量即可得到每张图片展示的区间大小。
const x = e.offsetX - coverImg.offsetLeft;
const index = Math.min(
Math.max(Math.ceil(x / w), 1),
this.stepNums
);
首先我们应该要计算当前鼠标在封面里的相对位置,这里我们只需要其横坐标x即可,然后将坐标除于区间大小,我们即可得到当前坐标所对应的区间下标。这里的最大值应该进行限制为1和stepNums。
鼠标移出的时候我们需要将封面恢复成当前视频的封面。
hoverOut(e) {
const coverImg = document.getElementById(this.uid + "-coverImg");
const step = this.duration / this.stepNums;
const index = Math.ceil(this.pauseTime / step);
this.progressValue = index;
coverImg.setAttribute("src", this.pauseCover || this.coverSrc);
},
封面和视频的显示隐藏需要根据播放状态来进行对应的切换。
点击封面的时候播放视频,需要隐藏封面及相关的进度条并显示视频
doHide(hide = false) {
const videoContent = document.getElementById(this.uid + "-video");
videoContent.style.display = hide ? "block" : "none";
videoContent.currentTime = this.pauseTime;
hide ? videoContent.play() : videoContent.pause();
const img = document.getElementById(this.uid + "-coverImg");
img.style.display = hide ? "none" : "block";
const progress = document.getElementById(this.uid + "-progress");
progress.style.display = hide ? "none" : "block";
const progress1 = document.getElementById(this.uid + "-progress1");
progress1.style.display = hide ? "none" : "block";
},
coverClick() {
this.doHide(true);
},
视频暂停时我们需要隐藏视频,截取当前帧作为封面并显示封面及相关的进度条。
videoContentShow.addEventListener("pause", e => {
this.pauseTime = videoContentShow.currentTime;
this.pauseCover = this.cutCover(
videoContentShow,
videoContentShow.currentTime
);
coverImg.setAttribute("src", this.pauseCover);
const step = this.duration / this.stepNums;
const index = Math.ceil(this.pauseTime / step);
this.progressValue = index;
setTimeout(() => {
if (videoContentShow.paused) this.doHide();
}, 200);
});
这里我使用了一个setTimeout来进行一个延时控制,大家知道为什么吗?因为视频有两种操作会触发视频的pause事件:
这里拉动进度条的时候会触发视频的pause事件并且马上继续播放,所以我们应该要过滤掉这一情况。
<template>
<div class="content">
<div class="video-list">
<j-video-cover
class="video"
:videoUrl="videoUrl"
stepNums="40"
></j-video-cover>
</div>
</div>
</template>
<script>
export default {
data() {
return {
videoUrl: require("../../assets/video/202112250058.mp4"),
}
}
}
</script>
这里我将这个组件打包进了自己的一个组件库,并将其发布到了npm上,有需要的同学也可以直接引入该组件进行使用。
引入教程可以看这里:http://jyeontu.xyz/jvuewheel/#/installView
引入后即可直接使用。
组件库已开源,想要查看完整源码的可以到 gitee 查看,自己也整理了相关的文档对其进行了简单介绍,具体如下:
jvuewheel: jyeontu.xyz/jvuewheel/#…
Gitee源码:gitee.com/zheng_yongt…
觉得有帮助的同学可以帮忙给我点个star,感激不尽~~~
有什么想法或者改良可以给我提个pr,十分欢迎~~~
有什么问题都可以在评论告诉我~~~
🎉这里是JYeontu,喜欢算法,GDCPC打过卡;热爱羽毛球,大运会打过酱油。毕业一年,两年前端开发经验,目前担任H5前端开发,算法业余爱好者,有空会刷刷算法题,平时喜欢打打羽毛球🏸 ,也喜欢写些东西,既为自己记录📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解🙇,写错的地方望指出,定会认真改进😊,在此谢谢大家的支持,我们下文再见🙌。
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的rubyyaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir
我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下:classTeam"Team"has_one:away_team,:class_name=>"Team"end我希望能够通过游戏访问一个团队,例如:Game.find(1).home_team但我收到一个单元化常量错误:Game::team。谁能告诉我我做错了什么?谢谢, 最佳答案 如果Gamehas_one:team那么Rails假设您的teams表有一个game_id列。不过,您想要的是games表有一个team_id列,在这种情况下