前言

随着网络的发展,现在直播或者线上视频网站与app如雨后春笋,在观看视频的时候为了让用户互动,绚丽效果,弹幕应用而生,js dom操作臃肿且影响性能(大量重复的操作dom是开发大忌),为了更加美观且提高性能的前提下,使用canvas标签功能体现其独特的魅力。今天分享一下使用Canvas标签开发弹幕的经验!

Canvas 弹幕的优先选择-LMLPHP

		<div className="video-body">
			<canvas ref={el => (this.canvasId = el)} className="canvas-class" /> // 需要获取canvas的节点进行接下来的一系列操作
			<video controls className="video-class" ref={el => (this.videoId = el)}> // 主要目的为了获取视频播放窗口的大小
				<source src={require('assents/video/historical_track.mp4')} type="video/mp4" />
			</video>
		</div>
class CanvasBarrage {
	constructor(canvasId, videoId) {
		if (!canvasId || !videoId) return;
		this.canvasId = canvasId;
		this.canvasText = canvasId.getContext('2d');
		this.isPaused = true; // 判断是否关闭弹幕
		this.canvasText.height = videoId.offsetHeight; // 设置画布的宽高
		this.canvasText.width = videoId.offsetWidth;
	}
	// 渲染canvas绘制的弹幕
	render = () => {
		this.clear();
		// 渲染弹幕
		this.renderBarrage();
		// 如果没有暂停的话就继续渲染
		if (this.isPaused === true) {
			requestAnimationFrame(this.render.bind(this)); // 这个是浏览器预判下一帧提前运行的事件(给人视觉流畅性,无卡顿与跳跃感)
		}
		if (this.isPaused === false) {
			this.clear();
		}
	};
	// 清空弹幕的事件
	clear = () => {
		this.canvasText.clearRect(0, 0, this.canvasId.width, this.canvasId.height);
	};
	//关闭弹幕事件
	closeBarrage = () => {
		this.isPaused = false;
	};
	//重新打开弹幕事件
	resetOpen = () => {
		this.isPaused = true;
		this.render();
	};
}

上面代码介绍了打开,关闭与清空弹幕,但是怎么绘制弹幕呢?输入的内容怎么绘制到canvas上呢?因为之后弹幕多次导入重绘,因此咱们需要单独的类进行处理。

class Barrages {
	constructor(obj, props) {
		this.time = obj.time;
		this.obj = obj;
		this.canvasText = props.canvasText;//canvas模板导入
		this.Init();
	}
	// 初始化弹幕的状态,颜色、透明度、阴影、字体大小
	Init = () => {
		this.color = this.obj.color || '#ffffff';
		this.fontSize = this.obj.fontSize || '10';
		let opacity = this.obj.opacity || '0.8';
		let position = this.obj.position || 'all';
		this.canvasText.shadowColor = `rgba(0,0,0,${opacity})`;
		this.canvasText.shadowBlur = 0;
		this.canvasText.font = `${this.fontSize}px Arial`;
		this.width = this.canvasText.measureText(this.obj.value).width; // 这一点重点注意,根据输入的内容设置canvas的宽度
		// 设置弹幕出现的位置
		this.x = this.canvasText.width;
		this.y = this.canvasText.height;//暂时显示到顶层位置,之后优化随机高度
		// 做下超出范围处理
		if (this.y < this.fontSize) {
			this.y = this.fontSize;
		} else if (this.y > this.canvasText.height - this.fontSize) {
			this.y = this.canvasText.height  - this.fontSize;
		}
	};
	// 写了那么多就是为了服务这个渲染,绘制canvas内容
	render = () => {
		this.canvasText.font = `lighter ${this.fontSize}px Arial`;
		this.canvasText.fillStyle = this.color;
		this.canvasText.fillText(this.obj.value, this.x, this.y);
	};
}

咱们写过了canvas启动类,也写了绘制canvas的类,怎么结合使用?不急,接下来慢慢道来,因为咱们弹幕获取肯定以数组的形式,渲染也是需要以数组的形式遍历渲染

class CanvasBarrage {
	constructor(canvasId, videoId) {
		...
		this.barrages = []; // 添加一个列表,来存储之后添加,或者直接写入的弹幕内容
		...
	}
	...
	// 用于渲染组合弹幕的事件
	// this.barrages添加的并不是弹幕的直接对象,而是Barrages渲染的实体类, [new Barrages({ value: '小蚂蚁爬呀爬,爬到你的身体上!', color: '#3344dd' }, this)]
	renderBarrage = () => {
		if (this.barrages.length < 1) return;
		this.barrages = this.barrages.filter(barrage => barrage.x > -barrage.width);
		this.barrages.forEach(barrage => {
			barrage.x -= 1;
			barrage.render();
		});
	};
	// 手动添加弹幕类事件
	addBarrage = value => {
		this.barrages.push(new Barrages(value, this));
	};
	...
	}

这样咱们就大功告成了,大致的需求都满足了,但是应该怎么使用呢?咱们简单的写个小例子😝

	// 首先调起Canvas类
	componentDidMount() {
		setTimeout(() => { // 既然都在react DidMount内调起,为什么还要写到浏览器定时器内部呢?这个问题留给自己的小任务吧!我暂时😝
			this.canvasBarrage = new CanvasBarrage(this.canvasId, this.videoId);
			this.canvasBarrage.render();
		});
	}
	// 随机添加弹幕事件
	addBarrageClick = () => {
		let data = [
			{ value: '周杰伦的听妈妈的话,让我反复循环再循环', color: 'red' },
			{ value: '想快快长大,才能保护她', color: '#00a1f5' },
			{ value: '听妈妈的话吧,晚点再恋爱吧!' },
			{ value: '小蚂蚁爬呀爬,爬到你的身体上!', color: '#3344dd' },
			{ value: '晚安说晚安,再见了!' },
			{ value: '还想再活五百年,我还想再活五百米年😝' },
			{ value: '快来呀,造作呀,反正有大把时光!' },
			{ value: '我在马路边捡到一分钱,把它交到叔叔的手里面!', color: 'red' }
		];
		let num = Number.parseInt(Math.random() * 8);
		this.canvasBarrage.addBarrage(data[num]);
	};

现在咱们就可以愉快的弹幕奔跑了,手机端是不是有点不清晰,高度也不是随机?接下来咱们优化一下咱们的弹幕

  • 为什么咱们的弹幕不清晰,感觉有锯齿呢?
  • 怎么逻辑算法使高度随机呢?

普及一下:canvas 不是矢量图,而是像图片一样是位图模式的。高分辨率显示设备意味着每平方英寸有更多的像素。也就是说二倍屏,浏览器就会以2个像素点的宽度来渲染一个像素,该 canvas 在 Retina 屏幕下相当于占据了2倍的空间,相当于图片被放大了一倍,因此绘制出来的图片文字等会变模糊,因此咱们的CanvasBarrage需要优化一下!

class CanvasBarrage {
	constructor(canvasId, videoId) {
		...
		this.ratio = this.getPixelRatio(this.canvasText);
		canvasId.width = videoId.offsetWidth * this.ratio;
		canvasId.height = videoId.offsetHeight * this.ratio;
		this.canvasText.height = videoId.offsetHeight * this.ratio;
		this.canvasText.width = videoId.offsetWidth * this.ratio;
		this.canvasText.scale(this.ratio, this.ratio);
		...
	}
	// 获取当前设备的分辨率倍数
	getPixelRatio = context => {
		let backingStore =
			context.backingStorePixelRatio ||
			context.webkitBackingStorePixelRatio ||
			context.mozBackingStorePixelRatio ||
			context.msBackingStorePixelRatio ||
			context.oBackingStorePixelRatio ||
			context.backingStorePixelRatio ||
			1;
		return (window.devicePixelRatio || 1) / backingStore;
	};
	}

怎么样使弹幕的高度随机?(需要使用浏览器分辨率的倍数)

class Barrages {
	constructor(obj, props) {
		...
		this.ratio = props.ratio;
		...
	}
	Init = () => {
		...
		this.x = this.canvasText.width / this.ratio;
		this.y = this.gitHeight(this.canvasText.height / this.ratio, position);
		// 做下超出范围处理
		if (this.y < this.fontSize) {
			this.y = this.fontSize;
		} else if (this.y > this.canvasText.height / this.ratio - this.fontSize) {
			this.y = this.canvasText.height / this.ratio - this.fontSize;
		}
		...
	};
	gitHeight = (height, post) => {
		switch (post) {
			case 'all':
				return Math.round(height * Math.random());
			case 'top':
				return Math.round((height * Math.random()) / 2);
			case 'bottom':
				return Math.round(height * (Math.random() / 2 + 0.5));
			default:
				return Math.round(height * Math.random());
		}
	};
	}

这样咱们的代码就完成了,大功告成,尽情的放飞弹幕吧!此文章只为借鉴,如有不足尽情评论!

借鉴 掘金 chenhongdong: 弹幕,是怎样炼成的

02-26 01:22