前端将一个canvas的动画,录制成一个视频

requestAnimationFrame

index.html

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<title>Document</title>
	<script src="whammy.js"></script>
</head>

<body>
	<canvas id="canvas" width="150" height="150"></canvas>
	<a style="display:none" id="download" download="clock.webm">Download video</a>
</body>
<script>
	function clock(time) {
		var now = new Date();
		now.setTime(time);
		var ctx = document.getElementById('canvas').getContext('2d');
		ctx.save();
		ctx.fillStyle = 'white'
		ctx.fillRect(0, 0, 150, 150); // videos cant handle transprency
		ctx.translate(75, 75);
		ctx.scale(0.4, 0.4);
		ctx.rotate(-Math.PI / 2);
		ctx.strokeStyle = "black";
		ctx.fillStyle = "white";
		ctx.lineWidth = 8;
		ctx.lineCap = "round";
		// Hour marks
		ctx.save();
		for (var i = 0; i < 12; i++) {
			ctx.beginPath();
			ctx.rotate(Math.PI / 6);
			ctx.moveTo(100, 0);
			ctx.lineTo(120, 0);
			ctx.stroke();
		}
		ctx.restore();
		// Minute marks
		ctx.save();
		ctx.lineWidth = 5;
		for (i = 0; i < 60; i++) {
			if (i % 5 != 0) {
				ctx.beginPath();
				ctx.moveTo(117, 0);
				ctx.lineTo(120, 0);
				ctx.stroke();
			}
			ctx.rotate(Math.PI / 30);
		}
		ctx.restore();

		var sec = now.getSeconds();
		var min = now.getMinutes();
		var hr = now.getHours();
		hr = hr >= 12 ? hr - 12 : hr;
		ctx.fillStyle = "black";
		// write Hours
		ctx.save();
		ctx.rotate(hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) * sec)
		ctx.lineWidth = 14;
		ctx.beginPath();
		ctx.moveTo(-20, 0);
		ctx.lineTo(80, 0);
		ctx.stroke();
		ctx.restore();
		// write Minutes
		ctx.save();
		ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec)
		ctx.lineWidth = 10;
		ctx.beginPath();
		ctx.moveTo(-28, 0);
		ctx.lineTo(112, 0);
		ctx.stroke();
		ctx.restore();
		// Write seconds
		ctx.save();
		ctx.rotate(sec * Math.PI / 30);
		ctx.strokeStyle = "#D40000";
		ctx.fillStyle = "#D40000";
		ctx.lineWidth = 6;
		ctx.beginPath();
		ctx.moveTo(-30, 0);
		ctx.lineTo(83, 0);
		ctx.stroke();
		ctx.beginPath();
		ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
		ctx.fill();
		ctx.beginPath();
		ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
		ctx.stroke();
		ctx.fillStyle = "#555";
		ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
		ctx.fill();
		ctx.restore();
		ctx.beginPath();
		ctx.lineWidth = 14;
		ctx.strokeStyle = '#325FA2';
		ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
		ctx.stroke();
		ctx.restore();
		return ctx;
	}

	var video = new Whammy.Video(15);
	var last_time = +new Date;
	var progress = 1;
	function next() {
		progress++;
		var context = clock(last_time += 1000);
		video.add(context);
		if (progress < 100) {
			requestAnimationFrame(next); //屏幕刷新时触发
		} else {
			requestAnimationFrame(finalizeVideo);
		}
	}

	function finalizeVideo() {
		var start_time = +new Date;
		video.compile(false, function (output) {

			var end_time = +new Date;
			var url = webkitURL.createObjectURL(output);
			console.log("url:" + url);
			document.getElementById('download').style.display = '';
			document.getElementById('download').href = url;
		});
	}
	next();
</script>

</html>

whammy.js

/*
	var vid = new Whammy.Video();
	vid.add(canvas or data url)
	vid.compile()
*/

window.Whammy = (function () {
	// in this case, frames has a very specific meaning, which will be
	// detailed once i finish writing the code

	function toWebM(frames, outputAsArray) {
		var info = checkFrames(frames);

		//max duration by cluster in milliseconds
		var CLUSTER_MAX_DURATION = 30000;

		var EBML = [
			{
				"id": 0x1a45dfa3, // EBML
				"data": [
					{
						"data": 1,
						"id": 0x4286 // EBMLVersion
					},
					{
						"data": 1,
						"id": 0x42f7 // EBMLReadVersion
					},
					{
						"data": 4,
						"id": 0x42f2 // EBMLMaxIDLength
					},
					{
						"data": 8,
						"id": 0x42f3 // EBMLMaxSizeLength
					},
					{
						"data": "webm",
						"id": 0x4282 // DocType
					},
					{
						"data": 2,
						"id": 0x4287 // DocTypeVersion
					},
					{
						"data": 2,
						"id": 0x4285 // DocTypeReadVersion
					}
				]
			},
			{
				"id": 0x18538067, // Segment
				"data": [
					{
						"id": 0x1549a966, // Info
						"data": [
							{
								"data": 1e6, //do things in millisecs (num of nanosecs for duration scale)
								"id": 0x2ad7b1 // TimecodeScale
							},
							{
								"data": "whammy",
								"id": 0x4d80 // MuxingApp
							},
							{
								"data": "whammy",
								"id": 0x5741 // WritingApp
							},
							{
								"data": doubleToString(info.duration),
								"id": 0x4489 // Duration
							}
						]
					},
					{
						"id": 0x1654ae6b, // Tracks
						"data": [
							{
								"id": 0xae, // TrackEntry
								"data": [
									{
										"data": 1,
										"id": 0xd7 // TrackNumber
									},
									{
										"data": 1,
										"id": 0x73c5 // TrackUID
									},
									{
										"data": 0,
										"id": 0x9c // FlagLacing
									},
									{
										"data": "und",
										"id": 0x22b59c // Language
									},
									{
										"data": "V_VP8",
										"id": 0x86 // CodecID
									},
									{
										"data": "VP8",
										"id": 0x258688 // CodecName
									},
									{
										"data": 1,
										"id": 0x83 // TrackType
									},
									{
										"id": 0xe0,  // Video
										"data": [
											{
												"data": info.width,
												"id": 0xb0 // PixelWidth
											},
											{
												"data": info.height,
												"id": 0xba // PixelHeight
											}
										]
									}
								]
							}
						]
					},
					{
						"id": 0x1c53bb6b, // Cues
						"data": [
							//cue insertion point
						]
					}

					//cluster insertion point
				]
			}
		];


		var segment = EBML[1];
		var cues = segment.data[2];

		//Generate clusters (max duration)
		var frameNumber = 0;
		var clusterTimecode = 0;
		while (frameNumber < frames.length) {

			var cuePoint = {
				"id": 0xbb, // CuePoint
				"data": [
					{
						"data": Math.round(clusterTimecode),
						"id": 0xb3 // CueTime
					},
					{
						"id": 0xb7, // CueTrackPositions
						"data": [
							{
								"data": 1,
								"id": 0xf7 // CueTrack
							},
							{
								"data": 0, // to be filled in when we know it
								"size": 8,
								"id": 0xf1 // CueClusterPosition
							}
						]
					}
				]
			};

			cues.data.push(cuePoint);

			var clusterFrames = [];
			var clusterDuration = 0;
			do {
				clusterFrames.push(frames[frameNumber]);
				clusterDuration += frames[frameNumber].duration;
				frameNumber++;
			} while (frameNumber < frames.length && clusterDuration < CLUSTER_MAX_DURATION);

			var clusterCounter = 0;
			var cluster = {
				"id": 0x1f43b675, // Cluster
				"data": [
					{
						"data": Math.round(clusterTimecode),
						"id": 0xe7 // Timecode
					}
				].concat(clusterFrames.map(function (webp) {
					var block = makeSimpleBlock({
						discardable: 0,
						frame: webp.data.slice(4),
						invisible: 0,
						keyframe: 1,
						lacing: 0,
						trackNum: 1,
						timecode: Math.round(clusterCounter)
					});
					clusterCounter += webp.duration;
					return {
						data: block,
						id: 0xa3
					};
				}))
			}

			//Add cluster to segment
			segment.data.push(cluster);
			clusterTimecode += clusterDuration;
		}

		//First pass to compute cluster positions
		var position = 0;
		for (var i = 0; i < segment.data.length; i++) {
			if (i >= 3) {
				cues.data[i - 3].data[1].data[1].data = position;
			}
			var data = generateEBML([segment.data[i]], outputAsArray);
			position += data.size || data.byteLength || data.length;
			if (i != 2) { // not cues
				//Save results to avoid having to encode everything twice
				segment.data[i] = data;
			}
		}

		return generateEBML(EBML, outputAsArray)
	}

	// sums the lengths of all the frames and gets the duration, woo

	function checkFrames(frames) {
		var width = frames[0].width,
			height = frames[0].height,
			duration = frames[0].duration;
		for (var i = 1; i < frames.length; i++) {
			// if (frames[i].width != width) throw "Frame " + (i + 1) + " has a different width";
			// if (frames[i].height != height) throw "Frame " + (i + 1) + " has a different height";
			// if (frames[i].duration < 0 || frames[i].duration > 0x7fff) throw "Frame " + (i + 1) + " has a weird duration (must be between 0 and 32767)";
			duration += frames[i].duration;
		}
		return {
			duration: duration,
			width: width,
			height: height
		};
	}


	function numToBuffer(num) {
		var parts = [];
		while (num > 0) {
			parts.push(num & 0xff)
			num = num >> 8
		}
		return new Uint8Array(parts.reverse());
	}

	function numToFixedBuffer(num, size) {
		var parts = new Uint8Array(size);
		for (var i = size - 1; i >= 0; i--) {
			parts[i] = num & 0xff;
			num = num >> 8;
		}
		return parts;
	}

	function strToBuffer(str) {
		// return new Blob([str]);

		var arr = new Uint8Array(str.length);
		for (var i = 0; i < str.length; i++) {
			arr[i] = str.charCodeAt(i)
		}
		return arr;
		// this is slower
		// return new Uint8Array(str.split('').map(function(e){
		// 	return e.charCodeAt(0)
		// }))
	}


	//sorry this is ugly, and sort of hard to understand exactly why this was done
	// at all really, but the reason is that there's some code below that i dont really
	// feel like understanding, and this is easier than using my brain.

	function bitsToBuffer(bits) {
		var data = [];
		var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
		bits = pad + bits;
		for (var i = 0; i < bits.length; i += 8) {
			data.push(parseInt(bits.substr(i, 8), 2))
		}
		return new Uint8Array(data);
	}

	function generateEBML(json, outputAsArray) {
		var ebml = [];
		for (var i = 0; i < json.length; i++) {
			if (!('id' in json[i])) {
				//already encoded blob or byteArray
				ebml.push(json[i]);
				continue;
			}

			var data = json[i].data;
			if (typeof data == 'object') data = generateEBML(data, outputAsArray);
			if (typeof data == 'number') data = ('size' in json[i]) ? numToFixedBuffer(data, json[i].size) : bitsToBuffer(data.toString(2));
			if (typeof data == 'string') data = strToBuffer(data);

			if (data.length) {
				var z = z;
			}

			var len = data.size || data.byteLength || data.length;
			var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
			var size_str = len.toString(2);
			var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
			var size = (new Array(zeroes)).join('0') + '1' + padded;

			//i actually dont quite understand what went on up there, so I'm not really
			//going to fix this, i'm probably just going to write some hacky thing which
			//converts that string into a buffer-esque thing

			ebml.push(numToBuffer(json[i].id));
			ebml.push(bitsToBuffer(size));
			ebml.push(data)


		}

		//output as blob or byteArray
		if (outputAsArray) {
			//convert ebml to an array
			var buffer = toFlatArray(ebml)
			return new Uint8Array(buffer);
		} else {
			return new Blob(ebml, { type: "video/webm" });
		}
	}

	function toFlatArray(arr, outBuffer) {
		if (outBuffer == null) {
			outBuffer = [];
		}
		for (var i = 0; i < arr.length; i++) {
			if (typeof arr[i] == 'object') {
				//an array
				toFlatArray(arr[i], outBuffer)
			} else {
				//a simple element
				outBuffer.push(arr[i]);
			}
		}
		return outBuffer;
	}

	//OKAY, so the following two functions are the string-based old stuff, the reason they're
	//still sort of in here, is that they're actually faster than the new blob stuff because
	//getAsFile isn't widely implemented, or at least, it doesn't work in chrome, which is the
	// only browser which supports get as webp

	//Converting between a string of 0010101001's and binary back and forth is probably inefficient
	//TODO: get rid of this function
	function toBinStr_old(bits) {
		var data = '';
		var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
		bits = pad + bits;
		for (var i = 0; i < bits.length; i += 8) {
			data += String.fromCharCode(parseInt(bits.substr(i, 8), 2))
		}
		return data;
	}

	function generateEBML_old(json) {
		var ebml = '';
		for (var i = 0; i < json.length; i++) {
			var data = json[i].data;
			if (typeof data == 'object') data = generateEBML_old(data);
			if (typeof data == 'number') data = toBinStr_old(data.toString(2));

			var len = data.length;
			var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
			var size_str = len.toString(2);
			var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
			var size = (new Array(zeroes)).join('0') + '1' + padded;

			ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data;

		}
		return ebml;
	}

	//woot, a function that's actually written for this project!
	//this parses some json markup and makes it into that binary magic
	//which can then get shoved into the matroska comtainer (peaceably)

	function makeSimpleBlock(data) {
		var flags = 0;
		if (data.keyframe) flags |= 128;
		if (data.invisible) flags |= 8;
		if (data.lacing) flags |= (data.lacing << 1);
		if (data.discardable) flags |= 1;
		if (data.trackNum > 127) {
			throw "TrackNumber > 127 not supported";
		}
		var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function (e) {
			return String.fromCharCode(e)
		}).join('') + data.frame;

		return out;
	}

	// here's something else taken verbatim from weppy, awesome rite?

	function parseWebP(riff) {
		var VP8 = riff.RIFF[0].WEBP[0];

		var frame_start = VP8.indexOf('\x9d\x01\x2a'); //A VP8 keyframe starts with the 0x9d012a header
		for (var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i);

		var width, horizontal_scale, height, vertical_scale, tmp;

		//the code below is literally copied verbatim from the bitstream spec
		tmp = (c[1] << 8) | c[0];
		width = tmp & 0x3FFF;
		horizontal_scale = tmp >> 14;
		tmp = (c[3] << 8) | c[2];
		height = tmp & 0x3FFF;
		vertical_scale = tmp >> 14;
		return {
			width: width,
			height: height,
			data: VP8,
			riff: riff
		}
	}

	// i think i'm going off on a riff by pretending this is some known
	// idiom which i'm making a casual and brilliant pun about, but since
	// i can't find anything on google which conforms to this idiomatic
	// usage, I'm assuming this is just a consequence of some psychotic
	// break which makes me make up puns. well, enough riff-raff (aha a
	// rescue of sorts), this function was ripped wholesale from weppy

	function parseRIFF(string) {
		var offset = 0;
		var chunks = {};

		while (offset < string.length) {
			var id = string.substr(offset, 4);
			chunks[id] = chunks[id] || [];
			if (id == 'RIFF' || id == 'LIST') {
				var len = parseInt(string.substr(offset + 4, 4).split('').map(function (i) {
					var unpadded = i.charCodeAt(0).toString(2);
					return (new Array(8 - unpadded.length + 1)).join('0') + unpadded
				}).join(''), 2);
				var data = string.substr(offset + 4 + 4, len);
				offset += 4 + 4 + len;
				chunks[id].push(parseRIFF(data));
			} else if (id == 'WEBP') {
				// Use (offset + 8) to skip past "VP8 "/"VP8L"/"VP8X" field after "WEBP"
				chunks[id].push(string.substr(offset + 8));
				offset = string.length;
			} else {
				// Unknown chunk type; push entire payload
				chunks[id].push(string.substr(offset + 4));
				offset = string.length;
			}
		}
		return chunks;
	}

	// here's a little utility function that acts as a utility for other functions
	// basically, the only purpose is for encoding "Duration", which is encoded as
	// a double (considerably more difficult to encode than an integer)
	function doubleToString(num) {
		return [].slice.call(
			new Uint8Array(
				(
					new Float64Array([num]) //create a float64 array
				).buffer) //extract the array buffer
			, 0) // convert the Uint8Array into a regular array
			.map(function (e) { //since it's a regular array, we can now use map
				return String.fromCharCode(e) // encode all the bytes individually
			})
			.reverse() //correct the byte endianness (assume it's little endian for now)
			.join('') // join the bytes in holy matrimony as a string
	}

	function WhammyVideo(speed, quality) { // a more abstract-ish API
		this.frames = [];
		this.duration = 1000 / speed;
		this.quality = quality || 0.8;
	}

	WhammyVideo.prototype.add = function (frame, duration) {
		if (typeof duration != 'undefined' && this.duration) throw "you can't pass a duration if the fps is set";
		if (typeof duration == 'undefined' && !this.duration) throw "if you don't have the fps set, you need to have durations here.";
		if (frame.canvas) { //CanvasRenderingContext2D
			frame = frame.canvas;
		}
		if (frame.toDataURL) {
			// frame = frame.toDataURL('image/webp', this.quality);
			// quickly store image data so we don't block cpu. encode in compile method.
			frame = frame.getContext('2d').getImageData(0, 0, frame.width, frame.height);
		} else if (typeof frame != "string") {
			throw "frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string"
		}
		if (typeof frame === "string" && !(/^data:image\/webp;base64,/ig).test(frame)) {
			throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp";
		}
		this.frames.push({
			image: frame,
			duration: duration || this.duration
		});
	};

	// deferred webp encoding. Draws image data to canvas, then encodes as dataUrl
	WhammyVideo.prototype.encodeFrames = function (callback) {

		if (this.frames[0].image instanceof ImageData) {

			var frames = this.frames;
			var tmpCanvas = document.createElement('canvas');
			var tmpContext = tmpCanvas.getContext('2d');
			tmpCanvas.width = this.frames[0].image.width;
			tmpCanvas.height = this.frames[0].image.height;

			var encodeFrame = function (index) {
				console.log('encodeFrame', index);
				var frame = frames[index];
				tmpContext.putImageData(frame.image, 0, 0);
				frame.image = tmpCanvas.toDataURL('image/webp', this.quality);
				if (index < frames.length - 1) {
					setTimeout(function () { encodeFrame(index + 1); }, 1);
				} else {
					callback();
				}
			}.bind(this);

			encodeFrame(0);
		} else {
			callback();
		}
	};

	WhammyVideo.prototype.compile = function (outputAsArray, callback) {

		this.encodeFrames(function () {

			var webm = new toWebM(this.frames.map(function (frame) {
				var webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
				webp.duration = frame.duration;
				return webp;
			}), outputAsArray);
			callback(webm);

		}.bind(this));
	};

	return {
		Video: WhammyVideo,
		fromImageArray: function (images, fps, outputAsArray) {
			return toWebM(images.map(function (image) {
				var webp = parseWebP(parseRIFF(atob(image.slice(23))))
				webp.duration = 1000 / fps;
				return webp;
			}), outputAsArray)
		},
		toWebM: toWebM
		// expose methods of madness
	}
})()
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值