Tutorial: Bouncing video

This is [the start of] a tutorial describing an HTML5 JavaScript application in which a video, masked to appear as a circle, bounces in a 2-D box. The working program is at http://faculty.purchase.edu/jeanine.meyer/html5/videobounce.html

This code works on Firefox, Chrome and Opera. It was necessary to make a few changes to make it work on Chrome that will be noted below.

Plan

The application can be divided into four challenges:

·  making something appear to bounce in a 2-D box, done using standard coding and the setInterval command;

·  getting video into an html document AND ensuring that the video or a black rectangle does not appear in the original location, accomplished by CSS. Making the video element visibility: hidden worked for Firefox. Using display: none worked for both Firefox and Chrome;

·  getting running video to appear in a specific spot on the screen, accomplished here by using drawImage to position the video in a canvas element;

·  masking video so it appears to be circular in shape, accomplished here by using beginPath and fill to draw a white shape that rides along over the video. A A mask made by a filled path of a circle inside a rectangle worked in Firefox, but not Chrome. Instead, I made two paths, shown below.

Bouncing in a box

The setInterval command is a part of standard HTML that provides a way to invoke a function at each interval of time. This can be used to re-position an object, call it the ball, on the screen. A canvas element in HTML is a canvas, not a Stage in which individual objects can be re-positioned. However, we can write code to erase (clear) the canvas and then re-draw things such as video, paths and rectangles, at the intervals of time. This is what is done in the function drawscene. The init function has the line:

setInterval(drawscene,50);

This sets up a call to the function every 50 milliseconds (20 times/second).

The drawscene function does the drawing after a call to moveandcheck to do the movement calculations. The movement of the ball (the thing being bounced) is accomplished using displacement values for the changes in the horizontal (x) coordinate and in the vertical (y) coordinates. The bouncing is accomplished by calculating if the ball position would hit (collide) with any of the four walls. Depending on which wall, the vertical or the horizontal displacement value is changed in sign. To put this another way, if the ball [virtually] hits a vertical wall, then we want the horizontal displacement value to change at the next iteration. If the ball [virtually] hits a horizontal wall, then we want the vertical displacement value to change at the next iteration.

The moveandcheck function does a tentative move, using ballx, bally for the position, and ballvx and ballvy for the displacement variables:

var nballx = ballx + ballvx;

var nbally = bally + ballvy;

and then checks the nballx and nbally values against the walls in 4 if statements, using variables representing the inner and outer limits of each wall. See full code in the Implementation section. There are sets of variables: ballx, bally, boxx, boxy, boxboundx, boxboundy, inboxboundx, inboxboundy that I developed to get the effects that I wanted: the ball object hits and goes behind (or seems to compress) and then bounces off. You can experiment with these to get the effects you want. Any hit will cause the appropriate position variables and displacement variables. After the if statements, the ball is re-positioned:

ballx = nballx;

bally = nbally;

The calculations done by the if statements are what is needed to make the ball bounce. This is independent of displaying the walls of the box. That is done using HTML5 methods of a canvas context. In my code, ctx is a variable set early on to hold the context. The statements:

ctx.lineWidth = ballrad;

ctx.strokeStyle ="rgb(200,0,50)";

ctx.strokeRect(boxx,boxy,boxwidth,boxheight);

set a line width, the color of the walls, and then draw the walls as an outline of a rectangle with upper left corner at boxx, boxy and with width boxwidth, and height boxheight. The stroke/line of the rectangle is relatively big. This is to prevent the ball from appearing on the outside of the box.

There are two frame-like operations going on here: that caused by the setInterval command and that of the video. There may be some interference that may need to be fixed in some situations.

You can build on this application by putting in forms with fields for varying the speed by changing ballvx and ballvy and implementing the ball slowing down when it hits a wall, by not simply changing the sign, but coding more complex expressions.

Displaying video in HTML5 and hiding the static video element

HTML5 provides a way to display video! A video element specifies the video source. Because of lack of agreement on what encoding (codec) to use for video, a recommended technique, supported by HTML5, is to create and specify multiple video files, each a version of the same video. The following is the video element used in this application.

<video id="vid" loop="loop" preload="auto">

<source src="joshuahomerun.webmvp8.webm" type='video/webm; codec="vp8, vorbis"'>

<source src="joshuahomerun.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>

<source src="joshuahomerun.theora.ogv" type='video/ogg; codecs="theora, vorbis"'>

Your browser does not accept the video tag.

</video>

Note: the loop setting does not work in Firefox so I used code to re-start the video. The preload setting indicates that the video is to start playing when it is loaded. I include code to play the video just in case this doesn't work. Lastly, though I have NOT tested it, and the spec says that the order should not matter, I have read that the webm source should be mentioned first for the video to play on an iPad. These are all matters that will sort themselves out over time.

You can view in a browser: http://faculty.purchase.edu/jeanine.meyer/html5/video.html

I have added controls="controls" to the <video> tag in this document to get controls displayed. I do not want controls in the bouncing video. Remember for this and all the examples, you can use view source to see the HTML source file.

The above code would be a way to display video in a fixed position in an HTML5 document. However, that is not what we want for the bouncing video. Instead the code will use the video element in a drawImage command. In the style element, I use CSS to set positioning to be absolute, set the original position, and make it NOT display, and also set the positioning of the canvas element and make z-index settings so the canvas is on top of the video:

<style>

#vid {position:absolute; display:none; z-index: 0;

left: 50px;

top: 60px;

}

#canvas {position:absolute; z-index:10;}

</style>

Video on canvas

So now the task is to get video to be drawn on the canvas. The drawImage method can be used with images or video. The code accesses (my term, may not be the best) the video element and uses it in a drawImage command. The code in the init function (invoked when the document is loaded) is

v = document.getElementById("vid");

v.addEventListener("ended",restart,false);

v.width = v.videoWidth/3;

v.height = v.videoHeight/3;

v.play();

This code sets the variable v for later use. The addEventListener sets up a function named restart to be invoked when the video ends. The restart function does just that: restart the video. Note: there appears to be no problem using this in Chrome, which does recognize the loop setting. Then v.width and v.height are set to be 1/3 the original settings. This has the effect of shrinking the video in a way that preserves the aspect ratio.

As indicated in the section on Bouncing, the drawscene function draws the ball object at a calculated position on the canvas. The variable ctx has been set to be what is termed the context of the canvas. The variable v is set to be the video element. The line:

ctx.drawImage(v,ballx+boxx, bally+boxy, v.width,v.height);

draws the video at the indicated position and with the width and height as specified.

Creating circular shape for video

The video appears as a circle in my program because the code creates something on top of it. This is termed a mask. Some applications, for example, PhotoShop, have built in mask structures. I created this mask-like shape on my own. I use the attributes of the video element to determine the size of the mask. In the init function, this code appears:

videow = v.width;

videoh = v.height;

maskrad = .4*Math.min(videow,videoh);

My initial idea for a mask was a path consisting of 4 lines that essentially were a rectangle, but not generated by strokeRect, and then an circle generated by arc. The shape had a hole:

ctx.beginPath();

ctx.moveTo(ballx+boxx,bally+boxy);

ctx.lineTo(ballx+boxx+v.width,bally+boxy);

ctx.lineTo(ballx+boxx+v.width,bally+boxy+v.height);

ctx.lineTo(ballx+boxx,bally+boxy+v.height);

ctx.lineTo(ballx+boxx,bally+boxy);

ctx.arc(ballx+boxx+.5*v.width,bally+boxy+.5*v.height,
maskrad,0,Math.PI*2,true);

ctx.fill();

This worked for Firefox, but the fill command did not work properly in Chrome. Instead I created the same mask with two shapes for this effect. For illustration, I produced this picture using ctx.stroke() inplace of ctx.fill().

The actual two shapes are produced using a fillStyle of white and no stroke so no lines appear.

The mask shape travels along with the video as shown in the drawscene code in the Implementation section.

Implementation

The application consists of four functions, described in the following table:

function / called/invoked by / calls / task
init / action of onload in <body> / drawscene / set variables, event handling (video ending and timed interval event)
restart / action of addEventListener in init for "ended" event / loop video
drawscene / called directly in init and then by action of setInterval in init / moveandcheck / draw moving video (including mask) after call to calculate new position.
moveandcheck / drawscene / calculates new position and checks if ball hits any walls

Note: the moveandcheck code could have been part of drawscene but I made it its own function.

<!DOCTYPE html> / standard boilerplate
<html> / open html
<head> / open head
<title>Video bounce</title> / title of document
<meta charset="UTF-8"> / standard boilerplace
<style> / open style
#vid {position:absolute; display:none; z-index: 0; / set absolute positioning, make invisible, set lowest z (layering)
left: 80px; / set at initial x of ball
top: 90px; / set at initial y of ball
} / close the style for the video
#canvas {position:absolute; z-index:10;} / set absolute positioning, higher z index to be on top of video
</style> / close style
<script type="text/javascript"> / standard boilerplate for javascript
var ctx; / will hold context, used for all drawing
var cwidth = 900; / width of canvas
var cheight = 600; / height of canvas
var ballrad = 50; / nominal ball radius
var boxx = 30; / starting x of box
var boxy = 30; / starting y of box
var boxwidth = 850; / box width
var boxheight = 550; / box height
var boxboundx = boxwidth+boxx-2*ballrad; / for comparisons
var boxboundy = boxheight+boxy-2*ballrad; / for comparisons
var inboxboundx = -10; / for comparisons
var inboxboundy = 0; / for comparisons
var ballx = 50; / initial x within box
var bally = 60; / initial y within box
var maskrad; / will be set based on video
var ballvx = 2; / initial x displacement
var ballvy = 4; / initial y displacement
var v; / will hold video element
function restart() { / function header for the restart function
v.currentTime=0; / set position in video
v.play(); / play the video
} / close restart function
function init(){ / function header for init function
ctx = document.getElementById('canvas').getContext('2d'); / set ctx
v = document.getElementById("vid"); / set v
v.addEventListener("ended",restart,false); / does looping
v.width = v.videoWidth/3; / set width based on original width
v.height = v.videoHeight/3; / set height based on original height
videow = v.width; / used for calculation
videoh = v.height; / used for calculation
maskrad = .4*Math.min(videow,videoh); / set mask radius to be 40% of the minimum of width and height
ctx.fillStyle="white"; / set for white mask
ctx.strokeStyle ="rgb(200,0,50)"; / set color for walls
} / close init
function drawscene(){ / function header for drawscene
ctx.clearRect(0,0,boxwidth+boxx,boxheight+boxy); / clear canvas
moveandcheck(); / do tentative move, check and then move
ctx.drawImage(v,ballx+boxx, bally+boxy, v.width,v.height); / draw the video
ctx.beginPath(); / begin path for the mask
ctx.moveTo(ballx+boxx,bally+boxy); / move to corner
ctx.lineTo(ballx+boxx+v.width,bally+boxy); / prepare line across
ctx.lineTo(ballx+boxx+v.width,bally+boxy+.5*v.height); / prepare line half way down
ctx.lineTo(ballx+boxx+.5*v.width+maskrad, bally+boxy+.5*v.height); / prepare line to edge of hole
ctx.arc(ballx+boxx+.5*v.width,bally+boxy+.5*v.height,maskrad,0,Math.PI,true); / prepare arc (half circle..a frown)
ctx.lineTo(ballx+boxx,bally+boxy+.5*v.height); / prepare line to edge
ctx.lineTo(ballx+boxx,bally+boxy); / prepare line to start
ctx.fill(); / fill in this shape
ctx.moveTo(ballx+boxx,bally+boxy+.5*v.height); / move half way down
ctx.lineTo(ballx+boxx,bally+boxy+v.height); / prepare line down
ctx.lineTo(ballx+boxx+v.width,bally+boxy+v.height); / prepare line over to left
ctx.lineTo(ballx+boxx+v.width,bally+boxy+.5*v.height); / prepare line halfway up
ctx.lineTo(ballx+boxx+.5*v.width+maskrad,bally+boxy+.5*v.height); / prepare line to edge of hole
ctx.arc(ballx+boxx+.5*v.width,bally+boxy+.5*v.height,maskrad,0,Math.PI,false); / prepare arc (half circle… a smile)
ctx.lineTo(ballx+boxx,bally+boxy+.5*v.height); / prepare line to start
ctx.fill(); / fill in this shape
ctx.strokeRect(boxx,boxy,boxwidth,boxheight); / draw box
} / close drawscene
function moveandcheck() { / function header for function that calculates the new position
var nballx = ballx + ballvx; / set tentative new x
var nbally = bally +ballvy; / set tentative new y
if (nballx > boxboundx) { / compare on right side
ballvx =-ballvx; / if hit, change horizontal displacement
nballx = boxboundx; / …set x to exactly at right boundary
} / close clause
if (nballx < inboxboundx) { / compare on left side
nballx = inboxboundx / if hit, set x exactly at left boundary
ballvx = -ballvx; / change horizontal displacement
} / close clause
if (nbally > boxboundy) { / compare to bottom
nbally = boxboundy; / if hit, set y exactly at bottom boundary
ballvy =-ballvy; / …change vertical displacement
} / close clause
if (nbally < inboxboundy) { / compare on top