├── .gitignore ├── LICENSE ├── README.md ├── canvid.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Gregor Aisch 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # canvid.js 2 | [![CDNJS](https://img.shields.io/cdnjs/v/canvid.svg)](https://cdnjs.com/libraries/canvid) 3 | 4 | **canvid** is a tiny dependency free library for playback of relatively short videos on canvas elements. 5 | 6 | * **Why not just use HTML5 video?** 7 | Because ~~you can't~~ until [Oct 2016](https://webkit.org/blog/6784/new-video-policies-for-ios/) you could not embed and autoplay HTML5 videos on iOS! Yeah, [that sucked](https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW4). 8 | 9 | * **Why is this better than using an animated GIF?** 10 | Videos look kind of gross when converted to animated GIFs because of the colors sampling. Also the file size of video GIFs can get pretty huge. GIF is just not made for videos. JPG does a much better job of compressing video frames. Also, animated GIFs don't give you any playback controls. You can't pause a GIF or delay it's playback. With canvid you can do that. 11 | 12 | * **Why only "relatively short" videos?** 13 | As you see further down, the container format for canvid is a big image sprite of all the frames of each clip. Sadly, iOS limits the maximum image size (bigger image get sampled down), so that puts a limit on the maximum frames you can store. 14 | 15 | * **Why is there no audio?** 16 | canvid doesn't support audio for the same reason animated GIF doesn't support audio either: because that's not what it was built for. (if you need audio, try [iphone-inline-video](https://github.com/bfred-it/iphone-inline-video)) 17 | 18 | ## Installation 19 | 20 | **npm** 21 | 22 | ``` 23 | $ npm install --save canvid 24 | ``` 25 | 26 | **git clone** 27 | 28 | ``` 29 | $ git clone git@github.com:gka/canvid.git 30 | ``` 31 | 32 | ## Usage 33 | 34 | You can use canvid.js with AMD, CommonJS and browser globals. 35 | 36 | ```js 37 | var canvidControl = canvid({ 38 | selector : '.video', 39 | videos: { 40 | clip1: { src: 'clip1.jpg', frames: 38, cols: 6, loops: 1, onEnd: function(){ 41 | console.log('clip1 ended.'); 42 | canvidControl.play('clip2'); 43 | }}, 44 | clip2: { src: 'clip2.jpg', frames: 43, cols: 6, fps: 24 } 45 | }, 46 | width: 500, 47 | height: 400, 48 | loaded: function() { 49 | canvidControl.play('clip1'); 50 | // reverse playback 51 | // canvidControl.play('clip1', true); 52 | } 53 | }); 54 | ``` 55 | If you want to use canvid with [React](https://facebook.github.io/react/) you can check this simple [react + canvid demo](http://codepen.io/moklick/pen/eJgbaL) to see how it works. 56 | 57 | ## Options 58 | 59 | * **videos** required 60 | Video/Sprite objects (videoKey : videoOptions). 61 | 62 | * **src** required 63 | Path of the sprite image. 64 | 65 | * **frames** required 66 | Number of frames. 67 | 68 | * **cols** required 69 | Number of columns. 70 | 71 | * **loops** optional 72 | Number of loops. 73 | 74 | * **fps** optional (default: 15) 75 | Frames per second. 76 | 77 | * **onEnd** optional 78 | Function that gets called when the clip ended. 79 | 80 | 81 | * **selector** optional 82 | The selector of the element where the video gets displayed. You can also pass a DOM element as a selector. 83 | `default: '.canvid-wrapper'` 84 | 85 | * **width** optional 86 | Width of the element where the video gets displayed. 87 | `default: 800` 88 | 89 | * **height** optional 90 | Height of the element where the video gets displayed. 91 | `default: 450` 92 | 93 | * **loaded** optional 94 | Function that gets called when all videos are loaded. 95 | 96 | * **srcGif** optional 97 | Path of the fallback gif, if canvas is not supported. 98 | 99 | 100 | ## Methods 101 | 102 | The canvid function returns an object to control the video: 103 | 104 | ```js 105 | var canvidControl = canvid(canvidOptions); 106 | ``` 107 | 108 | **play** 109 | Plays video of the passed videoKey. The parameters isReverse (default: false) and fps (default: 15) are optional. 110 | 111 | ```js 112 | canvidControl.play(videoKey [,isReverse, fps]); 113 | ``` 114 | 115 | **pause** 116 | Pause current video. 117 | 118 | ```js 119 | canvidControl.pause(); 120 | ``` 121 | 122 | **resume** 123 | Resume current video. 124 | 125 | ```js 126 | canvidControl.resume(); 127 | ``` 128 | 129 | **destroy** 130 | Stops video and removes the canvas of the current canvid element from the DOM. 131 | 132 | ```js 133 | canvidControl.destroy(); 134 | ``` 135 | 136 | **isPlaying** 137 | Returns true or false whether the video is playing or not. 138 | 139 | ```js 140 | canvidControl.isPlaying(); 141 | ``` 142 | 143 | **getCurrentFrame** 144 | Returns the current frame number. 145 | 146 | ```js 147 | canvidControl.getCurrentFrame(); 148 | ``` 149 | 150 | **setCurrentFrame** 151 | Sets the current frame number. 152 | 153 | ```js 154 | canvidControl.setCurrentFrame(0); 155 | ``` 156 | 157 | ## How to convert your video to a JPG sprite 158 | 159 | First, convert you video into single frames using [ffmpeg](https://www.ffmpeg.org/): 160 | 161 | ``` 162 | mkdir frames 163 | ffmpeg -i myvideo.mp4 -vf scale=375:-1 -r 5 frames/%04d.png 164 | ``` 165 | 166 | Then, use ImageMagicks [montage](http://www.imagemagick.org/script/montage.php) to stich all the frames into one big image: 167 | 168 | ``` 169 | montage -border 0 -geometry 375x -tile 6x -quality 60% frames/*.png myvideo.jpg 170 | ``` 171 | 172 | ## Is canvid responsive? 173 | 174 | Yes it is, thanks to a nice little trick. Regardless of what `width` and `height` parameters you set in the canvid constructor, you can use `style="width:100%"` on the canvas element and it will get scaled to the outer container and preserve its original aspect ratio. 175 | 176 | ```css 177 | canvas.canvid { 178 | width: 100%; 179 | } 180 | ``` 181 | 182 | ## Known Issues 183 | 184 | Some users encountered problems on mobile devices with large sprites. A workaround is to split the sprite into multiple sprites. 185 | 186 | If you're experiencing problems with `montage` (e.g. "unable to read font") try updating `imagemagick` to the latest version. 187 | 188 | ## Contributors 189 | 190 | * [Gregor Aisch](http://driven-by-data.net) 191 | * [Moritz Klack](http://moritzklack.com) 192 | -------------------------------------------------------------------------------- /canvid.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | if (typeof module !== 'undefined' && module.exports) { 3 | module.exports = factory(); 4 | } else if (typeof define === 'function' && define.amd) { 5 | define([], factory); 6 | } else { 7 | root.canvid = factory(); 8 | } 9 | }(this, function() { 10 | 11 | function canvid(params) { 12 | var defaultOptions = { 13 | width : 800, 14 | height : 450, 15 | selector: '.canvid-wrapper' 16 | }, 17 | firstPlay = true, 18 | control = { 19 | play: function() { 20 | console.log('Cannot play before images are loaded'); 21 | } 22 | }, 23 | _opts = merge(defaultOptions, params), 24 | el = typeof _opts.selector === 'string' ? document.querySelector(_opts.selector) : _opts.selector; 25 | 26 | if (!el) { 27 | return console.warn('Error. No element found for selector', _opts.selector); 28 | } 29 | 30 | if (!_opts.videos) { 31 | return console.warn('Error. You need to define at least one video object'); 32 | } 33 | 34 | if (hasCanvas()) { 35 | 36 | loadImages(_opts.videos, function(err, images) { 37 | if (err) return console.warn('Error while loading video sources.', err); 38 | 39 | var ctx = initCanvas(), 40 | requestAnimationFrame = reqAnimFrame(); 41 | 42 | control.play = function(key, reverse, fps) { 43 | if (control.pause) control.pause(); // pause current vid 44 | 45 | var img = images[key], 46 | opts = _opts.videos[key], 47 | frameWidth = img.width / opts.cols, 48 | frameHeight = img.height / Math.ceil(opts.frames / opts.cols); 49 | 50 | var curFps = fps || opts.fps || 15, 51 | curFrame = reverse ? opts.frames - 1 : 0, 52 | wait = 0, 53 | playing = true, 54 | loops = 0, 55 | delay = 60 / curFps; 56 | 57 | requestAnimationFrame(frame); 58 | 59 | control.resume = function() { 60 | playing = true; 61 | requestAnimationFrame(frame); 62 | }; 63 | 64 | control.pause = function() { 65 | playing = false; 66 | requestAnimationFrame(frame); 67 | }; 68 | 69 | control.isPlaying = function() { 70 | return playing; 71 | }; 72 | 73 | control.destroy = function(){ 74 | control.pause(); 75 | removeCanvid(); 76 | }; 77 | 78 | control.getCurrentFrame = function(){ 79 | return curFrame; 80 | }; 81 | 82 | control.setCurrentFrame = function(frameNumber){ 83 | if(frameNumber < 0 || frameNumber >= opts.frames){ 84 | return false; 85 | } 86 | 87 | if(!control.isPlaying()){ 88 | drawFrame(frameNumber); 89 | } 90 | 91 | curFrame = frameNumber; 92 | }; 93 | 94 | if (firstPlay) { 95 | firstPlay = false; 96 | hideChildren(); 97 | } 98 | 99 | function frame() { 100 | if (!wait) { 101 | drawFrame(curFrame); 102 | curFrame = (+curFrame + (reverse ? -1 : 1)); 103 | if (curFrame < 0) curFrame += +opts.frames; 104 | if (curFrame >= opts.frames) curFrame = 0; 105 | if (reverse ? curFrame == opts.frames - 1 : !curFrame) loops++; 106 | if (opts.loops && loops >= opts.loops){ 107 | playing = false; 108 | if(opts.onEnd && isFunction(opts.onEnd)){ 109 | opts.onEnd(); 110 | } 111 | } 112 | } 113 | wait = (wait + 1) % delay; 114 | if (playing && opts.frames > 1) requestAnimationFrame(frame); 115 | } 116 | 117 | function drawFrame(f) { 118 | var fx = Math.floor(f % opts.cols) * frameWidth, 119 | fy = Math.floor(f / opts.cols) * frameHeight; 120 | 121 | ctx.clearRect(0, 0, _opts.width, _opts.height); // clear frame 122 | ctx.drawImage(img, fx, fy, frameWidth, frameHeight, 0, 0, _opts.width, _opts.height); 123 | } 124 | 125 | }; // end control.play 126 | 127 | if (isFunction(_opts.loaded)) { 128 | _opts.loaded(control); 129 | } 130 | 131 | }); // end loadImages 132 | 133 | } else if (opts.srcGif) { 134 | var fallbackImage = new Image(); 135 | fallbackImage.src = opts.srcGif; 136 | 137 | el.appendChild(fallbackImage); 138 | } 139 | 140 | function loadImages(imageList, callback) { 141 | var images = {}, 142 | imagesToLoad = Object.keys(imageList).length; 143 | 144 | if(imagesToLoad === 0) { 145 | return callback('You need to define at least one video object.'); 146 | } 147 | 148 | for (var key in imageList) { 149 | images[key] = new Image(); 150 | images[key].onload = checkCallback; 151 | images[key].onerror = callback; 152 | images[key].src = imageList[key].src; 153 | } 154 | 155 | function checkCallback() { 156 | imagesToLoad--; 157 | if (imagesToLoad === 0) { 158 | callback(null, images); 159 | } 160 | } 161 | } 162 | 163 | function initCanvas() { 164 | var canvas = document.createElement('canvas'); 165 | canvas.width = _opts.width; 166 | canvas.height = _opts.height; 167 | canvas.classList.add('canvid'); 168 | 169 | el.appendChild(canvas); 170 | 171 | return canvas.getContext('2d'); 172 | } 173 | 174 | function hideChildren() { 175 | [].forEach.call(el.children, function(child){ 176 | if(!child.classList.contains('canvid') ){ 177 | child.style.display = 'none'; 178 | } 179 | }); 180 | } 181 | 182 | function removeCanvid(){ 183 | [].forEach.call(el.children, function(child){ 184 | if(child.classList.contains('canvid') ){ 185 | el.removeChild(child); 186 | } 187 | }); 188 | } 189 | 190 | function reqAnimFrame() { 191 | return window.requestAnimationFrame 192 | || window.webkitRequestAnimationFrame 193 | || window.mozRequestAnimationFrame 194 | || window.msRequestAnimationFrame 195 | || function(callback) { 196 | return setTimeout(callback, 1000 / 60); 197 | }; 198 | } 199 | 200 | function hasCanvas() { 201 | // taken from Modernizr 202 | var elem = document.createElement('canvas'); 203 | return !!(elem.getContext && elem.getContext('2d')); 204 | } 205 | 206 | function isFunction(obj) { 207 | // taken from jQuery 208 | return typeof obj === 'function' || !!(obj && obj.constructor && obj.call && obj.apply); 209 | } 210 | 211 | function merge() { 212 | var obj = {}, 213 | key; 214 | 215 | for (var i = 0; i < arguments.length; i++) { 216 | for (key in arguments[i]) { 217 | if (arguments[i].hasOwnProperty(key)) { 218 | obj[key] = arguments[i][key]; 219 | } 220 | } 221 | } 222 | return obj; 223 | } 224 | 225 | return control; 226 | }; // end canvid function 227 | 228 | return canvid; 229 | })); // end factory function -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvid", 3 | "version": "1.6.0", 4 | "description": "canvid is a tiny library for playback of relatively short videos on canvas elements.", 5 | "main": "canvid.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/gka/canvid.git" 12 | }, 13 | "keywords": [ 14 | "canvas", 15 | "video", 16 | "gif" 17 | ], 18 | "author": "Gregor Aisch", 19 | "contributors" : [{ 20 | "name" : "Gregor Aisch", 21 | "web" : "http://driven-by-data.net" 22 | },{ 23 | "name" : "Moritz Klack", 24 | "web" : "http://moritzklack.com" 25 | }], 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/gka/canvid/issues" 29 | }, 30 | "homepage": "https://github.com/gka/canvid" 31 | } 32 | --------------------------------------------------------------------------------