cacophony.js | |
---|---|
A tool for creating and viewing interactive videos, especially music videos, using HTML5 and Javascript. Interactive elements include visuals/story adapting in response to user input as text, mouse movement, drawings, and choices (choose-your-own- adventure). Input from the viewer can affect the subsequent video, and also be sent to a server for integration with other web applications (social networking, sharing, geotagging), which is possible because the video is rendered on-the-fly in the browser, not pre-rendered like traditional video. Input can also come from external sources (RSS, JSON), so you can integrate external data, or previously generated data, back into subsequent views of the video. New effects can be written in Javascript, and several frameworks are already integrated into existing effects, including: The basic elements of a Cacophony video are:
The official homepage of Cacophony is www.cacophonyjs.com. Copyright 2010, Johnny Broadway. Released under the GPL Version 2 license. UsageStep 1: Include the scripts and stylesheet in your
Step 2: Create an element like this in your page, note the ID must be 'cacophony':
Step 3: Add the following script to your web page:
Step 4: Copy the You can also refer to the API Reference | |
Declare a global cacophony object in a closure, passing it the jQuery object. | var cacophony = (function ($) {
var c = {},
wrapper, wrapper_id, song, video, canvas_id, paper, html,
started = false,
width = 854,
height = 480,
w = Math.floor (width / 128),
scale = 32,
play = false,
time = 0,
bpm = 120,
fader = false,
fader_opacity = 1.0,
beat_length = 500,
interval_length = 125,
beat = 0,
new_beat = false,
new_time = false,
on_beat = false,
beats = [],
key = 'c',
eq, bg, bg2,
start_time = false,
is_playable = false,
percent = 0,
bg_initial_fill = 'rgba(227, 234, 234, 1)',
ipad = navigator.userAgent.match (/iPad/i),
iphone = navigator.userAgent.match (/iPhone|iPod/i),
intervals = [],
eq_data = [],
videos = [],
mousemove = [],
codecs = {
mp4: "video/mp4; codecs='avc1.42E01E, mp4a.40.2'",
webm: "video/webm; codecs='vp8, vorbis'",
ogv: "video/ogg"
}; |
The Cake.js canvas object for the first layer. | c.canvas; |
The jQuery object for the wrapper div. | c.wrapper; |
The ID of the wrapper div (defaults to "cacophony" and some effects assume this will be left as-is). | c.id; |
Width of the video (use | c.width = 854; |
Height of the video (use | c.height = 480;
c.w = Math.floor (width / 128);
c.scale = 32;
c.bg;
c.bg2; |
The current beat of the song. | c.beat = 0; |
The EQ data for the current beat. | c.eq; |
Whether the video is currently playing or not. | c.playing = false; |
Stops reading keyboard input while an effect is accepting input. | c.input = false; |
The current x position of the mouse, for plugins looking to react to mouse movements. | c.mousex = 427; |
The current y position of the mouse, for plugins looking to react to mouse movements. | c.mousey = 240; |
The list of preloaded images, available to effects via: | c.preloaded_images = {
context:null,
ribbon:null
}; |
Whether to allow iPad playback. Off by default because many interactive elements are too processor intensive for the iPad. | c.enable_ipad = false; |
Callback functions for certain actions (e.g., play, pause, etc.). Register a callback via: | c.callback = {
browser_check: false,
loading: false,
ready: false,
play: false,
pause: false,
timeupdate: false,
ended: false
}; |
This is a list of effects available as actions to a video's story. They are simply references to functions. To add new ones, use: | c.effects = []; |
This is a list of actions to perform on each beat. For example:
a is the action, d is the data to pass to it. | c.story = []; |
EQ data for each beat. An array of objects of the form:
You can generate EQ data for your song with the utils/eqdata tool, which uses the SoundManager 2 API to retrieve the EQ data from Flash, then reduces it to only the data for each down beat, since Flash provided way more data than we need or was feasible to include in a Javascript file. | c.eq_data = []; |
A list of data available to effects, fetched prior to the video starting to play. Access data from individual feeds via:
JSON feeds will be parsed into JSON objects, and XML/RSS/ATOM feeds will be parsed into XML documents. | c.datafeed = {}; |
Has the video ended. | c.ended = false; |
Duration of the video. Available after loading is complete. | c.duration = false; |
Sets the list of videos to play. It's a list so you can specify multiple formats. | c.setVideo = function () {
videos = arguments;
} |
Determine the codec to use. | c.codec = function (vid) {
if (vid.match (/\.(mp4|m4v)$/i)) {
return codecs.mp4;
} else if (vid.match (/\.webm$/i)) {
return codecs.webm;
} else if (vid.match (/\.(ogv|ogg|ogm)$/i)) {
return codecs.ogv;
}
return '';
} |
on is either a specific layer (0 through 4) or an array
specifying the visibility of each layer, e.g.,
| c.visible = function (on) {
if ($.isArray (on)) {
if (! on[0]) {
$('#cacophony-video').hide ();
} else {
$('#cacophony-video').show ();
}
if (! on[1]) {
$('#cacophony-canvas1').hide ();
} else {
$('#cacophony-canvas1').show ();
}
if (! on[2]) {
$('#cacophony-canvas2').hide ();
} else {
$('#cacophony-canvas2').show ();
}
if (! on[3]) {
$('#cacophony-canvas3').hide ();
} else {
$('#cacophony-canvas3').show ();
}
if (! on[4]) {
$('#cacophony-canvas4').hide ();
} else {
$('#cacophony-canvas4').show ();
}
} else {
if (on == 0) {
$('#cacophony-video').show ();
} else if (on == 1) {
$('#cacophony-canvas1').show ();
} else if (on == 2) {
$('#cacophony-canvas2').show ();
} else if (on == 3) {
$('#cacophony-canvas3').show ();
} else if (on == 4) {
$('#cacophony-canvas4').show ();
}
}
} |
Hide the specified layer (0 through 4). | c.hide = function (off) {
if (off == 0) {
$('#cacophony-video').hide ();
} else if (off == 1) {
$('#cacophony-canvas1').hide ();
} else if (off == 2) {
$('#cacophony-canvas2').hide ();
} else if (off == 3) {
$('#cacophony-canvas3').hide ();
} else if (off == 4) {
$('#cacophony-canvas4').hide ();
}
} |
Pause the song. | c.pause = function () {
button = $('#cacophony-play').blur ();
song.pause ();
c.clearIntervals ();
play = false;
c.playing = false;
button.html ('<span title="Play" style="font-size: 20px">‣<' + '/span>');
if (c.callback.pause) {
return c.callback.pause ();
}
return false;
} |
Play the song. | c.play = function (callback) {
if (! is_playable) {
return false;
}
if (c.ended) {
c.ended = false;
beat = 0;
song.currentTime = 0;
}
button = $('#cacophony-play').blur ();
started = true;
song.play ();
c.setIntervals ();
play = true;
c.playing = true;
button.html ('<span title="Pause">||<' + '/span>');
if (c.callback.play) {
return c.callback.play ();
}
return false;
} |
Play/pause toggle. | c.playPause = function () {
if (play) {
return c.pause ();
} else {
return c.play ();
}
} |
Check the status of the video as it's loading. | c.loading = function () {
if (ipad) {
percent += Math.round (Math.random () * 5 + 1);
if (percent >= 100) {
is_playable = true;
c.duration = song.duration;
if (c.callback.ready) {
return c.callback.ready ();
}
} else {
if (c.callback.loading) {
c.callback.loading (percent);
}
setTimeout (c.loading, Math.round (Math.random () * 6 + 2) * 100);
}
return;
}
if (song.readyState == 4 || ($.browser.mozilla && song.readyState >= 2) || ($.browser.msie && song.readyState >= 3)) {
is_playable = true;
c.duration = song.duration;
if (c.callback.ready) {
return c.callback.ready ();
}
} else {
try {
percent = parseInt ((song.buffered.end (song.buffered.length - 1) / song.duration) * 100);
if (c.callback.loading) {
c.callback.loading (percent);
}
} catch (ex) {
}
setTimeout (c.loading, 250);
}
} |
Initialize everything. | c.init = function () {
id = 'cacophony';
wrapper = $('#cacophony');
wrapper_id = id;
c.wrapper = wrapper;
c.id = wrapper_id;
sources = '';
for (var i = 0; i < videos.length; i++) {
sources += '<source src="' + videos[i] + '" type="' + c.codec (videos[i]) + '" />';
}
wrapper.html (
'<div id="cacophony-duration"><div id="cacophony-play" onclick="return cacophony.playPause (this)"><span title="Pause">||</span></div><span id="cacophony-time">0:00</span></div>' +
'<div id="cacophony-video-wrapper"><video id="cacophony-video" width="' + width + '" height="' + height + '" style="z-index: 2; margin: 0px; padding: 0px">' + sources + '</video></div>' +
'<div id="cacophony-canvas1" style="width:' + width + 'px; height:' + height + 'px; z-index: 3; margin: 0px; padding: 0px; margin-top: -' + (height + 2) + 'px; border: 1px solid transparent"></div>' +
'<div id="cacophony-canvas2" style="display: none; width:' + width + 'px; height:' + height + 'px; z-index: 4; margin: 0px; padding: 0px; margin-top: -' + (height + 2) + 'px; border: 1px solid transparent"></div>' +
'<div id="cacophony-canvas3" style="display: none; width:' + width + 'px; height:' + height + 'px; z-index: 5; margin: 0px; padding: 0px; margin-top: -' + (height + 2) + 'px; border: 1px solid transparent"></div>' +
'<div id="cacophony-canvas4" style="display: none; width:' + width + 'px; height:' + height + 'px; z-index: 6; margin: 0px; padding: 0px; margin-top: -' + (height + 2) + 'px; border: 1px solid transparent"></div>'
); |
Browser checking. | var browser_msg = 'This video is processor intensive. Please shut down other programs and close unnecessary browser tabs to ensure the best viewing experience. Thanks!',
browser_compat = true;
if ($.browser.msie && $.browser.version < '9.0') {
browser_msg = 'This video requires the Google Chrome Frame browser plugin in order to work in Internet Explorer 6, 7, or 8. Please <a href="http://google.com/chromeframe" target="_blank">install the plugin here</a>, or use one of the following browsers to view this video:'
+ '<ul><li><a href="http://www.google.com/chrome">Chrome</a></li>'
+ '<li><a href="http://www.apple.com/safari/">Safari</a></li>'
+ '<li><a href="http://www.mozilla.com/firefox/">Firefox</a></li></ul>';
browser_compat = false;
} else if ($.browser.mozilla && $.browser.version < '1.9.2') {
browser_msg = 'This video requires a newer version of the Firefox browser in order to work. Please <a href="http://www.mozilla.com/firefox/" target="_blank">install the latest version here</a>.';
browser_compat = false;
} else if (! c.enable_ipad && (iphone || ipad)) {
browser_msg = 'This video requires more processing power than is currently available on the iPad, iPhone, or iPod touch. Please use a computer with one of the following browsers to view this video:'
+ '<ul><li><a href="http://www.google.com/chrome">Chrome</a></li>'
+ '<li><a href="http://www.apple.com/safari/">Safari</a></li>'
+ '<li><a href="http://www.mozilla.com/firefox/">Firefox</a></li></ul>';
browser_compat = false;
}
if (c.callback.browser_check) {
c.callback.browser_check (browser_msg, browser_compat);
}
if (! browser_compat) {
return;
}
song = $('#cacophony-video').get (0);
song.load ();
paper = new Canvas ($('#cacophony-canvas1').get (0), width, height);
canvas_id = paper.canvas.getAttribute ('id');
c.canvas = paper;
bg = new Rectangle (width, height);
bg.x = 0;
bg.y = 0;
bg.fill = bg_initial_fill;
paper.append (bg);
c.bg = bg;
bg2 = new Rectangle (width, height);
bg2.x = 0;
bg2.y = 0;
bg2.fill = 'rgba(27, 1, 1, 0)';
paper.append (bg2);
c.bg2 = bg2;
setTimeout (c.loading, 250);
$('#cacophony-video').bind ('timeupdate', function () {
c.displayTime ();
if (c.callback.timeupdate) {
return c.callback.timeupdate (song.currentTime);
}
});
$('#cacophony-video').bind ('ended', function () {
c.pause ();
c.ended = true;
if (c.callback.ended) {
return c.callback.ended ();
}
}); |
Set the main loop in motion. | setInterval (function () {
c.downBeat (song.currentTime * 1000);
}, (beat_length * 1000) / 20); |
Set keyboard play/pause access. | $(document).keydown (function (evt) {
if (! c.input && evt.keyCode == 32) {
c.playPause ();
}
}); |
Handle mouse-based effects. | $('#cacophony').mousemove (function (e) {
cacophony.mousex = e.pageX - this.offsetLeft;
cacophony.mousey = e.pageY - this.offsetTop;
if (cacophony.playing) {
for (var i = 0; i < mousemove.length; i++) {
mousemove[i] ();
}
}
});
} |
Register functions to be called on mousemove for your
effect. Functions should refer to the mouse position via
| c.mousemove = function (f) {
mousemove.push (f);
} |
Update the clock. Called | c.displayTime = function () {
var ns = song.currentTime,
min = Math.floor (ns / 60),
sec = Math.ceil (ns - (min * 60)),
new_time;
if (sec == 60) {
min++;
sec = 0;
}
new_time = min + ':' + (sec < 10 ? '0' + sec : sec);
if (new_time != time) {
time = new_time;
$('#cacophony-time').html (time);
}
} |
This is the main loop. | c.downBeat = function (s) {
if (! started || ! play) {
return false;
}
if (beats[beat] && s < beats[beat]) {
return false;
}
on_beat = true;
c.beat = beat;
if (c.eq_data[beat]) {
bn = c.eq_data[beat]; // Get the next `eq_data` value.
eq = bn.e;
c.eq = eq;
} else {
eq = [];
c.eq = [];
}
if (c.story[beat]) {
for (var i = 0; i < c.story[beat].length; i++) {
if (c.story[beat][i].d) {
c.effects[c.story[beat][i].a] (c.story[beat][i].d);
} else {
c.effects[c.story[beat][i].a] ();
}
}
} |
Wait for the effects on this beat to finish before jumping. TODO: Find a way to make this sound smooth. | if (new_beat !== false) {
song.currentTime = song.currentTime + ((new_beat - beat) * beat_length);
beat = Math.ceil (new_beat);
new_beat = false;
new_time = false;
c.downBeat (song.currentTime);
return;
}
beat++;
on_beat = false;
} |
Converts seconds to beats. | c.timeToBeat = function (s) {
return Math.floor (s / beat_length);
} |
Jump to the specified beat of the song. Actually tells | c.jumpTo = function (b) {
if (! play) {
c.play ();
}
new_beat = b;
new_time = false;
} |
Jump to the specified point in the song by seconds instead of beat.
Useful for implementing a visual scrubber using the timeupdate callback.
Unlike | c.jumpToTime = function (s, play_if_paused) {
if (! play && play_if_paused) {
c.play ();
}
new_time = s;
new_beat = c.timeToBeat (s);
if (on_beat) {
return;
}
song.currentTime = new_time;
beat = new_beat;
new_beat = false;
new_time = false;
c.downBeat (song.currentTime);
} |
Renders the specified html into the canvas ( | c.html = function (htm, top, left) {
if (! htm) {
cacophony.canvas.remove (html);
html = false;
} else {
var margin_top, margin_left;
if (html) {
cacophony.canvas.remove (html);
html = false;
}
margin_top = (top != null) ? top : (c.height - (c.height * .35)) / 2 - 20;
margin_left = (left != null) ? left : margin_left = (c.width - (c.width * .6)) / 2 - 20;
html = new ElementNode (E('div', htm), {
x: margin_left,
y: margin_top,
noScaling: true
});
html.element.setAttribute ('id', 'cacophony-html');
c.canvas.append (html);
return html;
}
} |
Set the key signature of the music. | c.setKey = function (k) {
key = k;
} |
Set the width and height of the player. Default is | c.setSize = function (wid, h) {
width = wid;
height = h;
w = Math.floor (width / 128);
c.width = width;
c.height = height;
c.w = w;
} |
Set the volume of the audio. | c.setVolume = function (v) {
if (v < 0) {
v = 0;
} else if (v > 1) {
v = 1;
}
if (v == 0) {
song.muted = true;
} else {
song.volume = v;
if (song.muted) {
song.muted = false;
}
}
} |
Get the volume of the audio. | c.getVolume = function () {
return song.volume;
} |
Set the beats-per-minute of the music. | c.setBPM = function (bpm) {
bpm = bpm;
beat_length = 60 / bpm;
interval_length = beat_length / 4;
var until = beat_length * (bpm * 60 * 8);
beats = [];
for (var b = 0; b <= until; b += beat_length) {
beats.push (b * 1000);
}
} |
Get the beat length/duration in miliseconds. | c.beatLength = function () {
return beat_length * 1000;
} |
Preload a list of data feeds for use in the video (JSON, RSS, ATOM, XML). | c.preloadData = function () {
for (var i = 0; i < arguments.length; i++) {
$.ajax ({
url: arguments[i],
success: function (data) {
c.datafeed[$(this)[0].url] = data;
|
Try to preload any images in the data | try { |
Preload media:content tags for XML | var images = data.getElementsByTagName ('content');
for (var j = 0; j < images.length; j++) {
if (images[j].getAttribute ('type').match (/^image\//)) {
c.preloadImages (images[j].getAttribute ('url'));
}
} |
Preload img tags for html | images = data.getElementsByTagName ('img');
for (var j = 0; j < images.length; j++) {
c.preloadImages (images[j].getAttribute ('src'));
}
} catch (ex) {}
},
error: function (xhr, s, e) {
/*console.log (s);
console.log (e);*/
}
});
}
} |
Preload a list of images for use in the video. | c.preloadImages = function () {
for (var i = 0; i < arguments.length; i++) {
if (! c.preloaded_images[arguments[i]]) {
c.preloaded_images[arguments[i]] = document.createElement ('img');
c.preloaded_images[arguments[i]].src = arguments[i];
}
}
} |
Register a function to be managed on play/pause. To be used in
place of | c.addInterval = function (k, ms) {
intervals.push ({f: k, t: false, m: ms});
if (play) {
intervals[intervals.length - 1].t = setInterval (k, ms);
}
return intervals.length - 1;
} |
Remove an interval function. To be used in place of | c.removeInterval = function (i) {
intervals = intervals.slice (i, 1);
} |
Starts interval-based functions on play. | c.setIntervals = function () {
for (var i = 0; i < intervals.length; i++) {
intervals[i].t = setInterval (intervals[i].f, intervals[i].m);
}
} |
Stops interval-based functions on pause. | c.clearIntervals = function () {
for (var i = 0; i < intervals.length; i++) {
clearInterval (intervals[i].t);
}
} |
Helper function to calculate distance of two points. | c.dist = function (x1, y1, x2, y2) {
var dx = x1 - x2,
dy = y1 - y2;
return Math.sqrt (dx * dx + dy * dy);
}
return c;
}(jQuery)); |
Create aliases to shorten references in effects and settings. | _c = cacophony;
_s = cacophony.story;
_e = cacophony.effects;
_i = cacophony.preloaded_images;
|