HTML
html
<div>
<input type="file">
</div>
<div>
<button class="download">下载</button>
</div>
<video></video>
<div class="preview">
<span class="slider"></span>
<div class="picArea"></div>
</div>
css
.preview {
position: relative;
overflow: hidden;
}
.slider {
position: absolute;
left: 0;
top: 0;
background-color: rgba(255, 255, 255, .8);
width: 15px;
height: 100%;
cursor: pointer;
z-index: 99;
}
具体实现
初始化
js
// DOM
const inp = document.querySelector('input'),
preview = document.querySelector('.preview'), // 预览小图区域
slider = document.querySelector('.slider'), // 滑块
picArea = preview.querySelector('.picArea'), // 每张小图容器
video = document.querySelector('video'),
download = document.querySelector('.download')
// 尺寸
const sliderWidth = getStyle(slider, 'width')
// 每张预览图信息
let picArr = [],
isFirst = true,
sliderX = 0, // 鼠标移动滑块坐标
perPicsWidth = 0, // 每张小图大小
selectedIndex = 0, // 选中的预览图数组索引
picsWidth = 0, // 预览小图容器宽度
maxPicsOffsetLeft = 0 // 预览小图最大偏移值
// 配置
let VIDEO_HEIGHT = 0
const VIDEO_WIDTH = 500,
RATIO = 5 // 缩放比例
获取具体帧画面
js
/**
* 生成视频某秒图片 大于总时长则用最后一秒
* @param {File} file
* @param {number | number[]} timeOrArray
*/
async function captureFrame(file, timeOrArray) {
if (typeof timeOrArray === 'number') {
return await genFrame(timeOrArray)
}
else {
const arr = []
timeOrArray.forEach((t) => {
arr.push(genFrame(t))
})
return Promise.all(arr)
}
}
这个函数,能根据给定的视频文件和某一秒,生成那一秒的视频画面
时间也可以给个数组,最终返回对应的Blob
和url
对象
接下来需要实现genFrame
函数
js
// 生成指定秒画面
async function genFrame(time) {
const vdo = document.createElement('video'),
src = url = URL.createObjectURL(file)
vdo.currentTime = time
vdo.muted = true
vdo.autoplay = true
vdo.src = src
return new Promise((resolve, reject) => {
vdo.oncanplay = () => {
resolve(videoToCanvas(vdo))
}
vdo.onerror = (err) => {
reject(err)
}
})
}
首先在生成一个video
元素,把文件源转成临时的url
,并赋值
然后设置在视频第几秒和静音,这样视频才能播放,再开启自动播放
由于视频没有加入页面,所以只会停留在那一秒
然后再视频加载完成的事件oncanplay
里,把这一画面放入canvas
里
js
/**
* 根据视频文件 生成对应时间的封面
*/
function videoToCanvas(vdo) {
const cvs = document.createElement('canvas'),
ctx = cvs.getContext('2d'),
{ videoWidth, videoHeight } = vdo,
w = VIDEO_WIDTH / RATIO,
h = VIDEO_HEIGHT / RATIO
// 每张小图宽度
perPicsWidth = w
cvs.height = h
cvs.width = w
// 生成预览小图
ctx.drawImage(vdo, 0, 0, w, h)
picArea.appendChild(cvs)
// 预览图高度设置一致
picArea.style.height = h + 'px'
// 存入原图
const oriCvs = document.createElement('canvas'),
orictx = oriCvs.getContext('2d')
oriCvs.height = vdo.videoHeight
oriCvs.width = vdo.videoWidth
orictx.drawImage(vdo, 0, 0)
return new Promise((resolve) => {
oriCvs.toBlob(blob => resolve({
blob,
url: URL.createObjectURL(blob)
}))
})
}
由于视频一般都很大,比如1920 * 1080
,这样就全屏了,所以我把canvas
等比例缩小了,再放入容器
这个仅仅作为预览图,如果要下载的话,需要原始大小,不然很模糊
oriCvs
就是原始大小
最终画入canvas
上下文,转成Blob
和对应的url
,返回一个Promise
对象即可
然后绑定一下input
的选择文件事件
js
// 给视频添加选择的文件 并生成预览条
inp.onchange = function () {
const file = this.files[0]
video.src = URL.createObjectURL(file)
video.oncanplay = async () => {
video.style.width = VIDEO_WIDTH + 'px'
// 宽 / 原始宽 = 比例; 比例 * 原始高度 = 最终高度
VIDEO_HEIGHT = VIDEO_WIDTH / video.videoWidth * video.videoHeight
if (isFirst) {
isFirst = false
picArr = await captureFrame(file, [1, 2, 3, 4, 5, 6, 7])
// 设置小图容器宽度
picsWidth = picArr.length * VIDEO_WIDTH / RATIO
picArea.style.width = picsWidth + 'px'
maxPicsOffsetLeft = picsWidth - VIDEO_WIDTH
}
}
}
这里的captureFrame
,我生成了前7秒的画面
因为后续需要多次改动视频时间,会触发oncanplay
事件,导致频繁生成预览图,所以加个isFirst
条件判断
然后选择文件,就生成预览图和视频了
接着给预览区域滑块绑定事件
js
slider.addEventListener('mousedown', (e) => {
setSliderPos(e)
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
})
这里用事件委托,让用户交互体验更好,绑定在window
可以在任意地方滑动
js
// 事件函数
function onMouseMove(e) {
setSliderPos(e)
selectedIndex = Math.floor(sliderX / perPicsWidth)
if (selectedIndex >= picArr.length - 1) {
selectedIndex = picArr.length - 1
}
else if (selectedIndex === -0 || selectedIndex <= 0) {
selectedIndex = 0
}
// 视频从第一秒开始 索引从0开始
video.currentTime = selectedIndex + 1
}
function onMouseUp() {
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
}
鼠标移动时,记录位置,注意算出来的值可能是-0
或者超出范围
需要判断一下
鼠标抬起时,全部解绑
移动时设置位置和视频的时间
这个setSliderPos
是重点
js
function setSliderPos(e) {
const { left } = preview.getBoundingClientRect()
sliderX = e.clientX - left
const x = sliderX - sliderWidth / 2 // 居中
slider.style.transform = `translateX(${x}px)`
用鼠标位置减去整块预览区域,得到偏移值
然后减去一半居中,再进行移动
这样就初步实现了功能
但是移动位置没有限制
而且要是生成预览图太多,后面的就看不见了,所以预览区域需要在特定时机移动
那么什么时候移动呢,当滑块超过一半时移动最佳,并设置最大偏移值
完整做法如下
js
function setSliderPos(e) {
const { left } = preview.getBoundingClientRect()
sliderX = e.clientX - left
const x = sliderX - sliderWidth / 2 // 居中
if (!canMove()) return
movePicArea()
slider.style.transform = `translateX(${x}px)`
function canMove() {
if (x > 0 && x < VIDEO_WIDTH - sliderWidth) {
return true
}
}
function movePicArea() {
const threshold = VIDEO_WIDTH / 2
if (x > threshold) {
let offsetLeft = x - threshold
offsetLeft >= maxPicsOffsetLeft && (offsetLeft = maxPicsOffsetLeft)
picArea.style.transform = `translateX(${-offsetLeft}px)`
}
}
}
这样能用吗,如果不仔细测试的话,看起来很完美
但是这里picArea
也移动了,所以他们的坐标会出问题,导致后面加载的画面不一致
这时怎么办呢??
那现在就需要一个能相对于整个容器的坐标,所以要额外加个标签
html
<div class="preview">
<span class="slider"></span>
<div class="picArea">
<div class="get-offset-area"></div>
</div>
</div>
.get-offset-area {
position: absolute;
inset: 0;
}
这个get-offset-area
会铺满整个容器,然后给他绑定事件,就能获取他的offsetX
也就是相对于picArea
里的真正坐标
但是现在你绑定事件他能触发吗?
答案是不能
为什么呢?
因为你需要移动滑块,而他在滑块下面
所以需要把mousemove
事件改成mouseover
,让他冒泡上去
但这样移动就没那么丝滑了,因为mouseover
触发的没有那么频繁
js
slider.addEventListener('mousedown', (e) => {
e.preventDefault() // 防止鼠标移不动
setSliderPos(e)
// 使用`mouseover`是因为需要冒泡到`getOffsetArea` 获取他的相对坐标
window.addEventListener('mouseover', onMouseOver)
window.addEventListener('mouseup', onMouseUp)
getOffsetArea.addEventListener('mouseover', onGetOffsetAreaMouseOver)
})
注意!!这里mousedown
的默认行为一定要阻止,不然鼠标样式会被改
现在把真实的值赋值即可
js
function onGetOffsetAreaMouseOver(e) {
realSilderX = e.offsetX
}
在改一下计算索引的值
js
function onMouseOver(e) {
setSliderPos(e)
selectedIndex = Math.floor(realSilderX / perPicsWidth)
if (selectedIndex >= picArr.length - 1) {
selectedIndex = picArr.length - 1
}
else if (selectedIndex === -0 || selectedIndex <= 0) {
selectedIndex = 0
}
// 视频从第一秒开始 索引从0开始
video.currentTime = selectedIndex + 1
}
再加个下载事件就可以了
js
download.onclick = function () {
const url = picArr[selectedIndex].url,
a = document.createElement('a')
a.href = url
a.download = Date.now()
a.click()
}