Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'master' into foreignobject-margin

  • Loading branch information...
commit d85f3da9ece7b28e97712b628d998f5fda7ecc66 2 parents bfe4cd6 + f7fc408
@klipstein klipstein authored
Showing with 1,320 additions and 53 deletions.
  1. +14 −4 CHANGELOG
  2. +6 −0 LICENSE
  3. +1 −1  example/library/index.html
  4. BIN  example/library/movies/assets/cinematicBoomNorm.m4a
  5. BIN  example/library/movies/assets/cinematicBoomNorm.ogg
  6. BIN  example/library/movies/assets/ninja-sprite.jpg
  7. BIN  example/library/movies/assets/piano-sprite.m4a
  8. BIN  example/library/movies/assets/piano-sprite.mp3
  9. BIN  example/library/movies/assets/piano-sprite.ogg
  10. BIN  example/library/movies/assets/pong.mp3
  11. BIN  example/library/movies/assets/pong.ogg
  12. BIN  example/library/movies/assets/tick16.m4a
  13. BIN  example/library/movies/assets/tick60.mp3
  14. +40 −0 example/library/movies/audio-boom.js
  15. +52 −0 example/library/movies/audio-button.js
  16. +45 −0 example/library/movies/audio-piano-sprite.js
  17. +8 −0 example/library/movies/audio-simple.js
  18. +5 −0 example/library/movies/audio-tick-16.js
  19. +6 −0 example/library/movies/audio-volume.js
  20. +1 −3 example/library/movies/filter-blur.js
  21. +13 −2 example/library/movies/movie_list.js
  22. +11 −0 example/library/movies/overlapping-paths.js
  23. +487 −0 example/library/movies/pong.js
  24. +52 −0 example/library/movies/sprite-ninja.js
  25. +1 −1  package.json
  26. +9 −3 src/asset/asset_controller.js
  27. +43 −1 src/asset/asset_handler.js
  28. +5 −6 src/asset/asset_resource.js
  29. +91 −0 src/asset/audio_handler.js
  30. +12 −9 src/asset/video_handler.js
  31. +64 −15 src/renderer/svg/svg.js
  32. +182 −0 src/runner/audio.js
  33. +3 −1 src/runner/environment.js
  34. +1 −1  src/version.js
  35. +28 −0 test/asset_audio_handler-spec.js
  36. +16 −6 test/asset_handler-spec.js
  37. +71 −0 test/audio-spec.js
  38. +51 −0 test/renderer/svg-spec.js
  39. +2 −0  test/runner.html
View
18 CHANGELOG
@@ -1,17 +1,27 @@
-v0.3.8
+v0.3.9
-------------------
+v0.3.8 / 2012-09-28
+-------------------
+
+* Add `Audio` a new DisplayObject including new API
+* Add `drawAudio` method and according tests in renderer
+* Add `AudioHandler` a new AssetHandler for Audio resources
+
v0.3.7 / 2012-09-26
-------------------
* Fix issue where click event would be fired twice on touch devices (#60)
* Change display list implementation from mixin to composition
* Make the default textOrigin in the Text class 'top'
-* Cleanup KeyframeAnimation and remove setSubject(s) methods to bring inline with current Animation class
-* Fix internal asset messaging so that assetData (loadData) is correctly sent to the runner (AssetLoader)
+* Cleanup KeyframeAnimation and remove setSubject(s) methods
+ to bring inline with current Animation class
+* Fix internal asset messaging so that assetData (loadData) is
+ correctly sent to the runner (AssetLoader)
* Add missing `delay` option to KeyframeAnimation
-* Fix issue where TextSpan child default strokeWidth took precedence over parent (Text instance) strokeWidth
+* Fix issue where TextSpan child default strokeWidth took
+ precedence over parent (Text instance) strokeWidth
* Add the ability to execute tests via commandline: `make test-phantom`
* Allow to pass a function within `code` in `bonsai.run`
* Make `player.Renderer` configurable via `player.setup()`
View
6 LICENSE
@@ -28,3 +28,9 @@ External libraries (each with their own license):
- phantom-jasmine: Apache 2 - https://github.com/jcarver989/phantom-jasmine/issues/8
- closure: http://code.google.com/p/closure-compiler/source/browse/trunk/COPYING
- qc.js: https://github.com/darrint/qc.js/blob/master/COPYING
+
+External resources (each with their own license):
+
+ - cinematicBoomNorm(ogg/mp3/m4a): http://creativecommons.org/licenses/by/3.0/
+ http://www.freesound.org/people/HerbertBoland/sounds/33637/
+
View
2  example/library/index.html
@@ -1,7 +1,7 @@
<!doctype html>
-<meta name=viewport content=width=device-width,initial-scale=1>
<head>
<title>Bonsai Movie Library</title>
+ <meta name=viewport content=width=device-width,initial-scale=1>
<script src=../../lib/requirejs/require.js></script>
<!--<script src="http://localhost:8080/socket.io/socket.io.js"></script>-->
<script src="movies/movie_list.js"></script>
View
BIN  example/library/movies/assets/cinematicBoomNorm.m4a
Binary file not shown
View
BIN  example/library/movies/assets/cinematicBoomNorm.ogg
Binary file not shown
View
BIN  example/library/movies/assets/ninja-sprite.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  example/library/movies/assets/piano-sprite.m4a
Binary file not shown
View
BIN  example/library/movies/assets/piano-sprite.mp3
Binary file not shown
View
BIN  example/library/movies/assets/piano-sprite.ogg
Binary file not shown
View
BIN  example/library/movies/assets/pong.mp3
Binary file not shown
View
BIN  example/library/movies/assets/pong.ogg
Binary file not shown
View
BIN  example/library/movies/assets/tick16.m4a
Binary file not shown
View
BIN  example/library/movies/assets/tick60.mp3
Binary file not shown
View
40 example/library/movies/audio-boom.js
@@ -0,0 +1,40 @@
+
+new Rect(145, 290, 1000, 60).fill('black').addTo(stage);
+
+var teamText = new Text('loading...').attr({
+ textFillColor: 'white',
+ scale:3,
+ x: 150,
+ y: 300
+}).addTo(stage);
+
+var movie = new Movie();
+
+var booom = new Audio([
+ { src: 'assets/cinematicBoomNorm.m4a' },
+ { src: 'assets/cinematicBoomNorm.ogg' }
+]).attr({
+ volume: 1
+}).addTo(stage).on('load', function() {
+
+ var i = 0, teaser = ['A Path', 'a red panda', 'and an Audio', 'walked into a bar'];
+
+ movie.frames({
+ 0: function() {
+ booom.play();
+ teamText.attr({
+ text: teaser[i]
+ });
+ },
+ '2s': function() {
+ if (i === teaser.length) {
+ movie.stop();
+ }
+ booom.stop();
+ i++;
+ }
+ }).addTo(stage);
+
+});
+
+
View
52 example/library/movies/audio-button.js
@@ -0,0 +1,52 @@
+
+var audio = new Audio([
+ { src: 'assets/cinematicBoomNorm.m4a' },
+ { src: 'assets/cinematicBoomNorm.ogg' }
+]).attr({
+ volume: 1
+}).addTo(stage);
+
+audio.prepareUserEvent(); // iOS devices
+
+new Button('Boooom!', 50, 50).on('click', function() {
+ audio.play(0);
+});
+
+
+
+
+// BUTTON IMPLEMENTATION
+function Button(text, x, y) {
+ var button = new bonsai.Group().addTo(stage).attr({x: x, y: y})
+
+ button.bg = bonsai.Path.rect(0, 0, 100, 40, 5).attr({
+ fillGradient: bonsai.gradient.radial(['#19D600', '#0F8000'], 100, 50, -20),
+ strokeColor: '#CCC',
+ strokeWidth: 0
+ }).addTo(button);
+
+ button
+ .on('mouseover', function() {
+ button.bg.animate('.2s', {
+ fillGradient: bonsai.gradient.radial(['#9CFF8F', '#0F8000'], 100, 50, -20),
+ strokeWidth: 3
+ })
+ })
+ .on('mouseout', function() {
+ button.bg.animate('.2s', {
+ fillGradient: bonsai.gradient.radial(['#19D600', '#0F8000'], 100, 50, -20),
+ strokeWidth: 0
+ })
+ });
+
+ button.text = new bonsai.Text(text).attr({
+ x: 10,
+ y: 13,
+ fontFamily: 'Arial',
+ fontSize: '20px',
+ textFillColor: 'white'
+ }).addTo(button);
+
+ return button;
+
+}
View
45 example/library/movies/audio-piano-sprite.js
@@ -0,0 +1,45 @@
+var pianoSprite = [], changePianoSprite = 0;
+
+function aSound(atTime) {
+ // TODO: test multiple audio "confirmations" on iOS.
+ var aPianoSprite = pianoSprite[0];
+ changePianoSprite += 1;
+ new Movie().addTo(stage).frames({
+ 0: function() {
+ aPianoSprite.play(atTime);
+ },
+ '1s': function() {
+ aPianoSprite.stop();
+ this.stop();
+ stage.removeChild(this);
+ }
+ });
+}
+
+// load indicator
+var aTextField = new Text('loading...').addTo(stage);
+
+function ready() {
+ aTextField.attr({ text: 'Start typing: [1-8]', scale:0.5 }).animate('0.5s', {
+ opacity: 1, scale: 1
+ }, { easing: 'elasticOut' });
+
+ aSound(0);
+
+ stage.on('key', function(e) {
+ var atTime = +String.fromCharCode(e.keyCode);
+ if (['C', 'D', 'E', 'F', 'G', 'A', 'H', 'C2'][atTime]) {
+ aSound(atTime-1);
+ }
+ });
+}
+
+pianoSprite.push(new Audio([
+ { src: 'assets/piano-sprite.m4a' },
+ { src: 'assets/piano-sprite.mp3' },
+ { src: 'assets/piano-sprite.ogg' }
+]).attr('prepareUserEvent', true));
+pianoSprite[0].addTo(stage).on('load', function() {
+ //pianoSprite[1] = pianoSprite[0].clone().addTo(stage);
+ aTextField.animate('0.5s', { opacity:0 }, { onEnd: ready });
+});
View
8 example/library/movies/audio-simple.js
@@ -0,0 +1,8 @@
+new Audio([
+ { src: 'assets/cinematicBoomNorm.m4a' },
+ { src: 'assets/cinematicBoomNorm.ogg' }
+]).addTo(stage).on('load', function() {
+ this.play();
+});
+
+
View
5 example/library/movies/audio-tick-16.js
@@ -0,0 +1,5 @@
+var tick = new Audio('assets/tick16.m4a').addTo(stage);
+stage.on('tick', function() {
+ // soooooound!!
+ //tick.play();
+});
View
6 example/library/movies/audio-volume.js
@@ -0,0 +1,6 @@
+var vol = 0.0;
+var tick = new Audio('assets/tick16.m4a').addTo(stage);
+stage.on('tick', function() {
+ vol += 0.01;
+ //tick.play().attr({ volume: Math.min(vol, 1) });
+});
View
4 example/library/movies/filter-blur.js
@@ -1,13 +1,11 @@
// image
-new bonsai.Bitmap('assets/redpanda.jpg', {
- onload: function() {
+new bonsai.Bitmap('assets/redpanda.jpg', function() {
this.attr({
y: 100,
x: 100,
filters: 'blur'
});
stage.addChild(this);
- }
});
// shape
View
15 example/library/movies/movie_list.js
@@ -16,13 +16,22 @@ movieList = {
'animation-stars-2.js',
'animation-circles.js'
],
+ 'Audio': [
+ 'audio-simple.js',
+ 'audio-tick-16.js',
+ 'audio-volume.js',
+ 'audio-boom.js',
+ 'audio-piano-sprite.js',
+ 'audio-button.js'
+ ],
'Bitmaps and Sprites': [
'bitmap.js',
'bitmap-source.js',
'fill-image.js',
'sprite.js',
'data-url.js',
- 'bitmap-memory-check.js'
+ 'bitmap-memory-check.js',
+ 'sprite-ninja.js'
],
'Color': [
'color.js',
@@ -77,6 +86,7 @@ movieList = {
'sprite-properties.js'
],
'Games': [
+ 'pong.js',
'asteroids.js',
'breakout.js',
'pacman.js'
@@ -125,7 +135,8 @@ movieList = {
'shape-arc.js',
'shape-getPointAtLength.js',
'shape-center-of-arc.js',
- 'shape-fill-rule.js'
+ 'shape-fill-rule.js',
+ 'overlapping-paths.js'
],
'Test': [
'text.js',
View
11 example/library/movies/overlapping-paths.js
@@ -0,0 +1,11 @@
+// http://jsbin.com/ociyiw/24/edit
+
+new Rect(100, 100, 100, 100).fill("yellow").attr({opacity:0.5}).addTo(stage);
+new Rect(160, 100, 100, 100).fill("green").attr({opacity:0.5}).addTo(stage);
+
+var group = new Group().addTo(stage).attr({
+ y:120,
+ opacity:0.5
+});
+new Rect(100, 100, 100, 100).fill("yellow").addTo(group);
+new Rect(160, 100, 100, 100).fill("green").addTo(group);
View
487 example/library/movies/pong.js
@@ -0,0 +1,487 @@
+var Pong, audioSprite;
+
+/**
+ * PONG
+ */
+
+Pong = (function(){
+
+ /**
+ * Setup default settings
+ */
+ var defaults = {
+ width: 500,
+ height: 350,
+ ballSpeed: 7,
+ paddleSpeed: 7,
+ ball: {
+ width: 20,
+ height: 20,
+ attr: {
+ fillColor: 'rgb(255,255,255)'
+ }
+ },
+ topPaddle: {
+ width: 80,
+ height: 20,
+ left: "a",
+ right: "s",
+ attr: {
+ fillColor: '#0077FF',
+ fillGradient: gradient.linear(0, ['rgba(0,0,0,.2)', 'rgba(0,0,0,0)'])
+ }
+ },
+ bottomPaddle: {
+ width: 80,
+ height: 20,
+ left: "left",
+ right: "right",
+ attr: {
+ fillColor: '#FF9500',
+ fillGradient: gradient.linear(0, ['rgba(0,0,0,.2)', 'rgba(0,0,0,0)'])
+ }
+ }
+ };
+
+ new Rect(0, 0, defaults.width, defaults.height).fill('black').addTo(stage);
+
+ /**
+ * Constructor for Pong, i.e. a new game of Pong
+ */
+ function Pong() {
+
+ this.config = defaults;
+
+ this.height = this.config.height;
+ this.width = this.config.width;
+
+ this.paddleSpeed = this.config.paddleSpeed;
+ this.ballSpeed = this.config.ballSpeed;
+
+ this.newGame();
+
+ }
+
+ /**
+ * keyIsDown method, used in Paddle instances to determine
+ * whether a key is down at any time
+ */
+ Pong.keyIsDown = (function(){
+
+ var keys = {
+ 37: "left",
+ 39: "right",
+ 65: "a",
+ 83: "s"
+ };
+
+ var down = {};
+
+ stage.on('keydown', function(e) {
+ var key = e.keyCode;
+ down[keys[key]] = true;
+ });
+
+ stage.on('keyup', function(e) {
+ var key = e.keyCode;
+ down[keys[key]] = false;
+ });
+
+ return function(key){
+ return !!down[key];
+ };
+
+ })();
+
+ /**
+ * Paddle constructor, prepares Paddle instances, nothing special
+ */
+ function Paddle(pong, isAuto, config, position){
+
+ this.isAuto = isAuto;
+ this.config = config;
+ this.width = config.width;
+ this.height = config.height;
+ this.bs = new Rect(0, 0, this.width, this.height, 5).attr(config.attr).addTo(stage);
+ this.x = position.x;
+ this.y = position.y;
+ this.pong = pong;
+
+ }
+
+ /**
+ * Ball constructor, prepares Ball instances,
+ * determines initial deltaX and deltaY fields
+ * dependent on specified ballSpeed. E.g. if we want
+ * a speed of 5, then we must make sure that:
+ * Math.abs(deltaX) + Math.abs(deltaY) === 5
+ */
+ function Ball(pong, config, position){
+
+ this.config = config;
+ this.width = config.width;
+ this.height = config.height;
+
+ this.bs = new Rect(0, 0, this.width, this.height, this.width/2).attr(config.attr);
+
+ this.deltaY = Math.floor(Math.random() * pong.ballSpeed) + 1;
+ this.deltaX = pong.ballSpeed - this.deltaY;
+
+ // Half the time, we want to reverse deltaY
+ // (making the ball begin in a random direction)
+ if ( Math.random() > 0.5 ) {
+ this.deltaY = -this.deltaY;
+ }
+
+ // Half the time, we want to reverse deltaX
+ // (making the ball begin in a random direction)
+ if ( Math.random() > 0.5 ) {
+ this.deltaX = -this.deltaX;
+ }
+
+ this.x = position.x;
+ this.y = position.y;
+ this.pong = pong;
+ this.isInitiated = false;
+
+ this.setLocation(this.x, this.y);
+ this.bs.addTo(stage);
+
+ }
+
+ /**
+ * The setLocation method is the same for the Ball and
+ * Paddle classes, for now, we're just drawing rectangles!
+ */
+ Paddle.prototype.setLocation = Ball.prototype.setLocation = function( x, y ) {
+
+ this.bs.attr({
+ x: x - this.width / 2,
+ y: y - this.height / 2
+ });
+
+ };
+
+ tools.mixin( Pong.prototype, {
+
+ /**
+ * A new game, initialises a top and bottom paddle, and
+ * calls newRound
+ */
+ newGame: function() {
+
+ this.topPaddle = new Paddle(this, true, this.config.topPaddle, {
+ x: this.width / 2,
+ y: this.config.ball.height + this.config.topPaddle.height/2
+ });
+
+ var userPaddle = this.bottomPaddle = new Paddle(this, false, this.config.bottomPaddle, {
+ x: this.width / 2,
+ y: this.height - this.config.ball.height - this.config.bottomPaddle.height/2
+ });
+
+ stage.on('pointermove', function(e) {
+ if (e.target !== stage) return;
+ userPaddle.x = e.stageX;
+ });
+
+ this.newRound();
+
+ },
+
+ /**
+ * newRound initalises a new Ball!
+ */
+ newRound: function() {
+
+ if (this.ball) {
+
+ playSprite(6);
+
+ var oldBall = this.ball;
+ oldBall.bs.animate('.5s', {
+ opacity: 0
+ }, {
+ onEnd: function() {
+ oldBall.bs.remove(); // clear old ball
+ }
+ });
+ }
+
+ this.ball = new Ball(this, this.config.ball, {
+ x: this.width / 2,
+ y: this.height / 2
+ });
+
+ var ball = this.ball;
+ ball.bs.attr({ opacity: 0 }).animate('.5s', {
+ opacity: 1
+ }, {
+ onEnd: function() {
+ setTimeout(function() {
+ ball.start();
+ }, 1000);
+ }
+ });
+
+ },
+
+ /**
+ * Starting a new game involves initialising an
+ * interval which will run every 20 milliseconds
+ */
+ start: function() {
+
+ var pong = this;
+
+ stage.on('tick', function() {
+ pong.draw();
+ });
+
+ // Return this for chainability
+ return this;
+
+ },
+
+ /**
+ * draw, called every few milliseconds to draw each
+ * object to the canvas
+ */
+ draw: function() {
+ this.topPaddle.draw();
+ this.bottomPaddle.draw();
+ this.ball.draw();
+ }
+
+ });
+
+ tools.mixin( Paddle.prototype, {
+
+ /**
+ * If this is the TOP paddle
+ */
+ isTop: function() {
+ return this === this.pong.topPaddle;
+ },
+
+ /**
+ * intersectsBall, determines whether a ball is currently
+ * touching the paddle
+ */
+ intersectsBall: function() {
+
+ var bX = this.pong.ball.x,
+ bY = this.pong.ball.y,
+ bW = this.pong.ball.width,
+ bH = this.pong.ball.height;
+
+ return (
+ this.isTop() ?
+ (bY - bH/2 <= this.y + this.height/2 && bY + bH/2 > this.y + this.height/2) :
+ (bY + bH/2 >= this.y - this.height/2 && bY - bH/2 < this.y - this.height/2)
+ ) &&
+ bX + bW/2 >= this.x - this.width/2 &&
+ bX - bW/2 <= this.x + this.width/2;
+
+ },
+
+ /**
+ * Calculate AI movements
+ */
+ calculateAI: function() {
+
+ var pong = this.pong,
+ ball = this.pong.ball;
+
+ if (ball.isMoving) {
+ if (Math.abs(ball.x - this.x) < 30) {
+ //console.log('Smal');
+ //this.x += this.lastAutoDeltaX || 0;
+ return; // prevent shaking
+ }
+ if (ball.x > this.x && !this.isAtRightWall()) {
+ this.x += this.lastAutoDeltaX = pong.paddleSpeed;
+ } else if (ball.x < this.x && !this.isAtLeftWall()) {
+ this.x += this.lastAutoDeltaX = -pong.paddleSpeed;
+ } else {
+ this.lastAutoDeltaX = 0;
+ }
+ }
+
+ },
+
+ /**
+ * Prepares a new frame, by taking into account the current
+ * position of the paddle and the ball. setLocation is called
+ * at the end to actually draw to the canvas!
+ */
+ draw: function() {
+
+ var config = this.config,
+ pong = this.pong,
+ ball = pong.ball,
+ ballSpeed = pong.ballSpeed,
+
+ xFromPaddleCenter,
+ newDeltaX,
+ newDeltaY;
+
+ if (this.isAuto) {
+
+ this.calculateAI();
+
+ } else {
+
+ if ( Pong.keyIsDown(config.left) && !this.isAtLeftWall() ) {
+ this.x -= pong.paddleSpeed;
+ }
+
+ if ( Pong.keyIsDown(config.right) && !this.isAtRightWall() ) {
+ this.x += pong.paddleSpeed;
+ }
+
+ }
+
+ if ( this.intersectsBall() ) {
+
+ playSprite(0);
+
+ xFromPaddleCenter = (ball.x - this.x) / (this.width / 2);
+ xFromPaddleCenter = xFromPaddleCenter > 0 ? Math.min(1, xFromPaddleCenter) : Math.max(-1, xFromPaddleCenter);
+
+ if ( Math.abs(xFromPaddleCenter) > 0.5 ) {
+ ballSpeed += ballSpeed * Math.abs(xFromPaddleCenter);
+ }
+
+ newDeltaX = Math.min( ballSpeed - 2, xFromPaddleCenter * (ballSpeed - 2) );
+ newDeltaY = ballSpeed - Math.abs(newDeltaX);
+
+ ball.deltaY = this.isTop() ? Math.abs(newDeltaY) : -Math.abs(newDeltaY);
+ ball.deltaX = newDeltaX;
+
+ }
+
+ this.setLocation( this.x , this.y );
+
+ },
+
+ /**
+ * If the paddle is currently touching the right wall
+ */
+ isAtRightWall: function() {
+ return this.x + this.width/2 >= this.pong.width;
+ },
+
+ /**
+ * If the paddle is currently touching the left wall
+ */
+ isAtLeftWall: function() {
+ return this.x - this.width/2 <= 0;
+ }
+
+ });
+
+ tools.mixin( Ball.prototype, {
+
+ start: function() {
+ this.isInitiated = true;
+ },
+
+ /**
+ * If the ball is currently touching a wall
+ */
+ isAtWall: function() {
+ return this.x + this.width/2 >= this.pong.width || this.x - this.width/2 <= 0;
+ },
+
+ /**
+ * If the ball is currently at the top
+ */
+ isAtTop: function() {
+ return this.y + this.height/2 <= 0;
+ },
+
+ /**
+ * If the paddle is currently at the bottom
+ */
+ isAtBottom: function() {
+ return this.y - this.height/2 >= this.pong.height;
+ },
+
+ /**
+ * Simply continues the progression of the ball, by
+ * setting the location to the prepared x/y values
+ */
+ persist: function() {
+ this.setLocation(
+ this.x,
+ this.y
+ );
+ },
+
+ /**
+ * Prepares the ball to be drawn to the canvas,
+ * to bounce the ball off the walls, the deltaX
+ * field is simply inverted (+5 becomes -5).
+ */
+ draw: function() {
+
+ if (!this.isInitiated) {
+ return;
+ }
+
+ if ( this.isAtWall() ) {
+ playSprite(4);
+ this.deltaX = -this.deltaX;
+ }
+
+ if ( this.isAtBottom() || this.isAtTop() ) {
+ this.pong.newRound();
+ return;
+ }
+
+ this.isMoving = true;
+ this.x = this.x + this.deltaX;
+ this.y = this.y + this.deltaY;
+
+ this.persist();
+
+ }
+
+ });
+
+ return Pong;
+
+})();
+
+// popup
+var popup = new Group().addTo(stage).attr({ x: 140, y: 120});
+new Rect(0, 0, 200, 100, 10)
+ .fill(gradient.linear(0, ['red', 'yellow']))
+ .stroke('green', 2)
+ .addTo(popup);
+new Text('Go!').attr({
+ textFillColor: 'white', fontFamily: 'Arial', fontSize: 60, x: 50, y: 30
+}).addTo(popup);
+
+// sound
+audioSprite = new Audio([
+ { src: 'assets/pong.mp3' },
+ { src: 'assets/pong.ogg' }
+]).prepareUserEvent().addTo(stage).on('load', function() {
+ popup.destroy();
+ new Pong().start();
+});
+
+var timeoutId = null;
+function playSprite(time) {
+ audioSprite.play(time);
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(function() {
+ audioSprite.pause();
+ }, 500);
+}
+
+
+
+
View
52 example/library/movies/sprite-ninja.js
@@ -0,0 +1,52 @@
+/**
+ * Sprited Bitmap Implementation
+ *
+ * @param {String} src Source URL of Sprite image
+ * @param {Number} imgWithinSpriteX Location of the target bitmap within the entire
+ * sprite (X)
+ * @param {Number} imgWithinSpriteX Location of the target bitmap within the entire
+ * sprite (Y)
+ * @param {Number} imgWithinSpriteWidth Width of the target bitmap within the entire
+ * sprite
+ * @param {Number} imgWithinSpriteHeight Height of the target bitmap within the entire
+ * sprite
+ */
+function SpritedBitmap(src, imgWithinSpriteX, imgWithinSpriteY, imgWithinSpriteWidth, imgWithinSpriteHeight) {
+ Group.call(this);
+ new Bitmap(src).attr({
+ x: -imgWithinSpriteX,
+ y: -imgWithinSpriteY
+ }).addTo(this);
+ this.attr(
+ 'clip',
+ new Rect(0, 0, imgWithinSpriteWidth, imgWithinSpriteHeight)
+ );
+}
+
+SpritedBitmap.prototype = Object.create(Group.prototype);
+
+
+
+
+// Ninja movie:
+
+
+// IMAGE FROM http://www.36peas.com/blog/2010/9/13/free-japanese-ninja-shinobi-sprite-sheet.html
+var src = 'assets/ninja-sprite.jpg';
+var ninjas = [
+ new SpritedBitmap(src, 0, 0, 95, 140).attr({visible: false}).addTo(stage),
+ new SpritedBitmap(src, 92, 0, 95, 140).attr({visible: false}).addTo(stage),
+ new SpritedBitmap(src, 188, 0, 95, 140).attr({visible: false}).addTo(stage)
+];
+var lastIndex = 0;
+
+
+stage.setFramerate(3);
+
+stage.on('tick', function(s, f) {
+ ninjas[lastIndex].attr({visible: false});
+ ninjas[lastIndex = f % ninjas.length].attr({visible: true});
+});
+
+
+new Text('Image attributed to http://www.36peas.com/blog/2010/9/13/free-japanese-ninja-shinobi-sprite-sheet.html').addTo(stage).attr({y: 200, x: 20})
View
2  package.json
@@ -3,7 +3,7 @@
"name": "bonsai",
"description": "Bonsai runtime and node server",
"main": "src/bootstrapper/_dev/node.js",
- "version": "0.3.7",
+ "version": "0.3.8",
"private": true,
"homepage": "https://github.com/uxebu/bonsai/",
"maintainers": [],
View
12 src/asset/asset_controller.js
@@ -9,16 +9,17 @@ define([
'./asset_request',
'./font_handler',
'./video_handler',
+ './audio_handler',
'./bitmap_handler',
'./raw_handler'
],
function(
tools, EventEmitter, AssetRequest,
- FontHandler, VideoHandler, BitmapHandler, RawHandler
+ FontHandler, VideoHandler, AudioHandler, BitmapHandler, RawHandler
) {
'use strict';
- // save references to all assets (TODO: rethink)
+ // save references to all assets
AssetController.assets = {};
AssetController.hasVideoSupport = function() {
@@ -52,6 +53,11 @@ function(
Font: FontHandler,
/**
+ * Type handler for audio
+ */
+ Audio: AudioHandler,
+
+ /**
* Type handler for video
*/
Video: VideoHandler,
@@ -67,7 +73,7 @@ function(
/**
* Destroys our reference to the asset's corresponding data/element.
- * (<img> or <video> etc.)
+ * (<img> or <video> or <audio> etc.)
*/
destroy: function(assetId) {
delete AssetController.assets[assetId];
View
44 src/asset/asset_handler.js
@@ -5,8 +5,48 @@ define([
], function(AssetRequest, EventEmitter, tools) {
'use strict';
+ var forEach = tools.forEach;
+ var slice = [].slice;
+
AssetHandler.DEFAULT_TIMEOUT = 10000;
+ AssetHandler.MIME_TYPES = (function(navigatorMimeTypes) {
+
+ var mimeTypes = {
+ audio: {},
+ video: {},
+ image: {}
+ };
+
+ if (!navigatorMimeTypes) {
+ return mimeTypes;
+ }
+
+ forEach(slice.call(navigatorMimeTypes), function(navMimeType) {
+
+ // in most cases the name of the handler is part of the description
+ var handler = (navMimeType.description.match(/audio|video|image/i) || [])[0];
+
+ // return early when the mime-type is irrelevant
+ if (!handler) {
+ return;
+ }
+
+ // case insensitive, support "video" and "Video"
+ handler = handler.toLowerCase();
+
+ // store a mime-type per suffix (overwrites existing suffixes)
+ forEach(navMimeType.suffixes.split(','), function(suffix) {
+ if (suffix) {
+ mimeTypes[handler][suffix] = navMimeType.type;
+ }
+ });
+ });
+
+ return mimeTypes;
+
+ })(typeof navigator !== 'undefined' && navigator.mimeTypes);
+
/**
* A helper class for asset handling (different asset types: video, bitmap, etc.)
*
@@ -33,13 +73,15 @@ define([
this.initTimeout();
- tools.forEach(this.resources, function(resource) {
+ forEach(this.resources, function(resource) {
this.loadResource(
resource,
this.resourceLoadSuccess,
this.resourceLoadError
);
}, this);
+
+ return this;
},
initTimeout: function() {
View
11 src/asset/asset_resource.js
@@ -14,25 +14,24 @@ define([
*/
function AssetResource(param) {
- var i, src, type;
+ var src, type;
if (!param) {
throw Error('AssetResource needs at least a valid url as parameter.');
}
- if (typeof param == 'string') {
+ if (typeof param === 'string') {
src = param;
} else {
src = param.src;
type = param.type;
}
- if (!src || typeof src != 'string') {
+ if (!src || typeof src !== 'string') {
throw Error('AssetResource: src parameter invalid: ' + src);
}
this.src = src;
-
if (type && typeof type === 'string') {
this.type = type;
} else {
@@ -40,13 +39,13 @@ define([
var re = uri.scheme === 'data' ?
/^(\w+\/[\w+]+)[;,]/ : // extract mime type from beginning
/\.([^.]+)$/; // extract file extension
- this.type = (uri.path.match(re) || [, null])[1];
+ this.type = (uri.path.match(re) || [])[1];
}
if (!this.type) {
throw Error('Cannot determine type of resource with src: ' + src);
}
- };
+ }
return AssetResource;
});
View
91 src/asset/audio_handler.js
@@ -0,0 +1,91 @@
+/**
+ * Type handler for audio
+ */
+define([
+ './asset_handler'
+], function(AssetHandler) {
+ 'use strict';
+
+ var AUDIO_MIME_TYPES = AssetHandler.MIME_TYPES.audio;
+
+ var domAudio;
+ try {
+ domAudio = document.createElement('audio');
+ } catch (e) {}
+
+ var events = {
+ 'progress': 'progress',
+ 'loadstart': 'loadstart',
+ 'loadedmetadata': 'loadedmetadata',
+ 'loadeddata': 'loadeddata',
+ 'canplay': 'canplay',
+ 'canplaythrough': 'canplaythrough'
+ };
+
+ function AudioHandler() {
+ AssetHandler.apply(this, arguments);
+ }
+
+ var getPlayableMimeType = AudioHandler.getPlayableMimeType = function(mimeType) {
+
+ // check environment and make sure `domAudio` is available
+ if (!domAudio) {
+ return '';
+ }
+
+ // 1) user's mimetype
+ if (domAudio.canPlayType(mimeType)) {
+ return mimeType;
+ }
+
+ // 2nd fallback - lookup browser's mimetype table
+ var vendorMimeType = AUDIO_MIME_TYPES[mimeType];
+ if (vendorMimeType && domAudio.canPlayType(vendorMimeType)) {
+ return vendorMimeType;
+ }
+
+ // 3rd fallback - prepend "audio/"
+ var audioSlashMimeType = 'audio/' + mimeType;
+ if (domAudio.canPlayType(audioSlashMimeType)) {
+ return audioSlashMimeType;
+ }
+
+ return '';
+ };
+
+ AudioHandler.prototype = Object.create(AssetHandler.prototype);
+
+ AudioHandler.prototype.loadResource = function(resource, doDone, doError) {
+
+ var audioElement;
+ var assetId = this.id,
+ loadLevel = this.request.loadLevel || 'canplay',
+ mimeType = getPlayableMimeType(resource.type),
+ src = resource.src;
+
+ if (!mimeType || this.hasInitiatedLoad) {
+ this.resourcesExpectedLength--;
+ return;
+ }
+
+ this.hasInitiatedLoad = true;
+
+ // Start loading audio
+ audioElement = document.createElement('audio');
+ audioElement.setAttribute('id', assetId);
+ audioElement.setAttribute('type', mimeType);
+ audioElement.src = src;
+ // Triggers partial content loading (206)
+ audioElement.load();
+ this.registerElement(audioElement);
+
+ // don't send *DOM* Events to the runner
+ function onload() {
+ doDone();
+ }
+
+ audioElement.addEventListener(events[loadLevel], onload, false);
+ };
+
+ return AudioHandler;
+});
View
21 src/asset/video_handler.js
@@ -6,15 +6,18 @@ define([
], function(AssetHandler) {
'use strict';
- var domVideo = typeof document !== 'undefined' && document.createElement ?
- document.createElement('video') : 0;
+ var domVideo;
+ try {
+ domVideo = document.createElement('video');
+ } catch (e) {}
var events = {
- 'start-with-nothing': 'loadstart',
- 'metadata': 'loadedmetadata',
- 'risky-to-play': 'loadeddata',
- 'can-play': 'canplay',
- 'can-play-through': 'canplaythrough'
+ 'progress': 'progress',
+ 'loadstart': 'loadstart',
+ 'loadedmetadata': 'loadedmetadata',
+ 'loadeddata': 'loadeddata',
+ 'canplay': 'canplay',
+ 'canplaythrough': 'canplaythrough'
};
function VideoHandler() {
@@ -27,7 +30,7 @@ define([
var video,
assetId = this.id,
- loadLevel = this.request.loadLevel || 'can-play',
+ loadLevel = this.request.loadLevel || 'canplay',
mimeType = resource.type,
src = resource.src;
@@ -46,7 +49,7 @@ define([
this.registerElement(video);
- function onload(e) {
+ function onload() {
doDone({
width: video.videoWidth,
height: video.videoHeight
View
79 src/renderer/svg/svg.js
@@ -20,6 +20,10 @@ define([
// targets webkit based browsers from version 530.0 to 534.4
var isWebkitPatternBug = /AppleWebKit\/53([0-3]|4.([0-4]))/.test(navigator.appVersion);
+ // Math
+ var min = Math.min;
+ var max = Math.max;
+
// svgHelper
var cssClasses = svgHelper.cssClasses,
matrixToString = svgHelper.matrixToString,
@@ -30,7 +34,6 @@ define([
// svgFilters
var isFEColorMatrixEnabled = svgFilters.isFEColorMatrixEnabled,
- colorApplyColorMatrix = svgFilters.colorApplyColorMatrix,
filterElementsFromList = svgFilters.filterElementsFromList;
// AssetController
@@ -295,6 +298,8 @@ define([
if (type === 'DOMElement') {
element = svg[id] = document.createElement(message.attributes.nodeName);
element.setAttribute('data-bs-id', id);
+ } else if (type === 'Audio') {
+ element = svg[id] = AssetController.assets[id];
} else {
element = svg[id] = createElement(typesToTags[type], id);
}
@@ -423,7 +428,6 @@ define([
var filters = attr.filters;
var fillColor = attr.fillColor;
var fillGradient = attr.fillGradient;
- var svg = this.svg;
// when filter is applied, force fillColor change on UA w/o SVG Filter support
if (!isFEColorMatrixEnabled && !fillColor && filters && element._fillColorSignature) {
@@ -554,9 +558,7 @@ define([
proto.drawTextSpan = function(tspan, message) {
- var attributes = message.attributes,
- fontSize = attributes.fontSize,
- fontFamily = attributes.fontFamily;
+ var attributes = message.attributes;
tspan.setAttributeNS(xlink, 'text-anchor', 'start');
tspan.setAttribute('alignment-baseline', 'inherit');
@@ -590,9 +592,7 @@ define([
proto.drawText = function(text, message) {
- var attributes = message.attributes,
- fontSize = attributes.fontSize,
- fontFamily = attributes.fontFamily;
+ var attributes = message.attributes;
if (attributes.selectable !== false) {
cssClasses.add(text, 'selectable');
@@ -621,10 +621,9 @@ define([
var video = AssetController.assets[id];
if (typeof video === 'undefined') {
- throw Error('asset <' + id + '> is unkown.');
+ throw Error('asset <' + id + '> is unknown.');
}
- var obj = this.svg[id];
var width = attributes.width || 100;
var height = attributes.height || 100;
var matrix = attributes.matrix || {tx: 0, ty: 0};
@@ -651,16 +650,66 @@ define([
foreignObject.appendChild(video);
};
+ proto.drawAudio = function(audioElement, message) {
+
+ var volume;
+ var attributes = message.attributes;
+ var id = message.id;
+ var playing = attributes.playing;
+
+ if (typeof audioElement === 'undefined') {
+ throw Error('asset <' + id + '> is unknown.');
+ }
+
+ if (attributes.prepareUserEvent && 'ontouchstart' in document) {
+ // We bind to the next touch-event and play/pause the audio to cause
+ // iOS devices to allow subsequent play/pause commands on the audio el.
+ // --
+ // (Usually, iOS Devices will only allow play/pause methods to be called
+ // after a user event. Due to bonsai's async nature, a movie programmer
+ // can never achieve this. So we setup a fake one here...)
+ var touchStartHandler = function() {
+ audioElement.play();
+ audioElement.pause();
+ document.removeEventListener('touchstart', touchStartHandler, true);
+ };
+ document.addEventListener('touchstart', touchStartHandler, true);
+ }
+
+ if ('volume' in attributes) {
+ // Value between 0-1. NaN is treated as `0`
+ audioElement.volume = min(max(+attributes.volume || 0, 0), 1);
+ }
+
+ // Time in seconds. `currentTime` throws when there's no
+ // current playback state machine
+ if ('time' in attributes) {
+ // Set volume to 0 to avoid "clicks"
+ volume = audioElement.volume;
+ audioElement.volume = 0;
+ try {
+ // Some browsers ignore `0`, that's why we set it to `0.01`
+ audioElement.currentTime = +attributes.time || 0.01;
+ } catch(e) {}
+ // Set volume back to the initial value
+ audioElement.volume = volume;
+ }
+
+ if (playing === true) {
+ audioElement.play();
+ }
+ if (playing === false) {
+ audioElement.pause();
+ }
+
+ };
+
proto.drawDOMElement = function(element, message) {
// assuming a valid assetId
var body,
attributes = message.attributes,
- css = attributes.css,
- id = message.id,
- parent = this.svg[message.parent],
- width = attributes.width,
- height = attributes.height;
+ parent = this.svg[message.parent];
// Parent may not be defined if message is a NeedsDraw and *not* a NeedsInsertion
if (parent && !element._root && !(parent instanceof HTMLElement)) {
View
182 src/runner/audio.js
@@ -0,0 +1,182 @@
+define([
+ './asset_display_object',
+ '../tools'
+], function(AssetDisplayObject, tools) {
+ 'use strict';
+
+ var data = tools.descriptorData;
+ var accessor = tools.descriptorAccessor;
+ var getter = tools.getter;
+
+ /** Getters & Setters */
+ function getTime() {
+ return this._time;
+ }
+ function setTime(time) {
+ time = +time;
+ if (typeof time === 'number' && !isNaN(time) && isFinite(time)) {
+ this._time = time;
+ }
+ }
+
+ function getVolume() {
+ return this._volume;
+ }
+ function setVolume(volume) {
+ if (volume != null) {
+ volume = +volume;
+ volume = Math.min( Math.max(volume, 0), 1 ); // between 0 and 1 inclusive
+ this._volume = volume;
+ }
+ }
+
+ /**
+ * The Audio constructor
+ *
+ * @constructor
+ * @name Audio
+ * @extends AssetDisplayObject
+ *
+ * @param {String|Array} aRequest The request needs to accomplish the requirements of AssetRequest
+ * @param {Function} [callback] A callback to be called when your movie has
+ * loaded (only called if you passed a `aRequest`). The callback will be called
+ * with it's first argument signifying an error. So, if the first argument
+ * is `null` you can assume the movie was loaded successfully.
+ *
+ * @property {__list__} __supportedAttributes__ List of supported attribute names.
+ * In addition to the property names listed for DisplayObject,
+ * these are the attribute names you can pass to the attr() method. Note
+ * that this property is not available in your code, it's just here for
+ * documentation purposes.
+ * @property {string} __supportedAttributes__.source The source of the audio.
+ * @property {string} __supportedAttributes__.volume The volume of the audio
+ * (between 0 and 1 inclusive)
+ *
+ */
+ function Audio(loader, aRequest, callback, options) {
+ options || (options = {});
+
+ AssetDisplayObject.call(this, loader, aRequest, callback);
+
+ this.type = 'Audio';
+
+ Object.defineProperties(this._attributes, {
+ playing: data(!!options.autoplay, true, true),
+ prepareUserEvent: data(false, true, true),
+ volume: accessor(getVolume, setVolume, true),
+ _volume: data(1, true, true),
+ time: accessor(getTime, setTime, true),
+ _time: data(0, true, true)
+ });
+
+ var rendererAttributes = this._renderAttributes;
+ rendererAttributes.playing = 'playing';
+ rendererAttributes.volume = '_volume';
+ rendererAttributes.time = '_time';
+ rendererAttributes.prepareUserEvent = 'prepareUserEvent';
+
+ this.request(aRequest);
+ }
+
+ var parentPrototype = AssetDisplayObject.prototype;
+ var parentPrototypeDestroy = parentPrototype.destroy;
+
+ /** @lends Audio.prototype */
+ var proto = Audio.prototype = Object.create(parentPrototype);
+
+ /**
+ * Clones the method
+ *
+ * @returns {Audio} Cloned instance
+ */
+ proto.clone = function() {
+ // options are missing
+ return new Audio(this._loader, this._request);
+ };
+
+ /**
+ * Destroys the DisplayObject and removes any references to the
+ * asset, including data held by the renderer's assetController about the
+ * source of the audio
+ *
+ * @returns {this}
+ */
+ proto.destroy = function() {
+ parentPrototypeDestroy.call(this);
+ this._loader.destroyAsset(this);
+ return this;
+ };
+
+ /**
+ * Notify the audio that the corresponding data has been loaded. To be used
+ * by the asset loader.
+ *
+ * @private
+ * @param {string} type Either 'load' or 'error'
+ * @param data
+ */
+ proto.notify = function(type, data) {
+
+ switch (type) {
+ case 'load':
+ // We trigger the event asynchronously so as to ensure that any events
+ // bound after instantiation are still triggered:
+ this.emitAsync('load', this);
+ break;
+ case 'error':
+ // We trigger the event asynchronously so as to ensure that any events
+ // bound after instantiation are still triggered:
+ this.emitAsync('error', Error(data.error));
+ }
+
+ return this;
+ };
+
+ /**
+ * Play the audio
+ * @param {Number} [time] Time to seek playhead to (in seconds)
+ *
+ * @returns {Audio} this
+ */
+ proto.play = function(time) {
+ if (time !== undefined) {
+ this.attr('time', time);
+ }
+ return this.attr('playing', true);
+ };
+
+ /**
+ * Prepare the Audio object for a user-event.
+ * (currently this is for iOS devices, see drawAudio method in svg.js)
+ *
+ * @returns {Audio} this
+ */
+ proto.prepareUserEvent = function() {
+ return this.attr('prepareUserEvent', true);
+ };
+
+ /**
+ * Pause the audio
+ *
+ * @returns {Audio} this
+ */
+ proto.pause = function() {
+ return this.attr('playing', false);
+ };
+
+ /**
+ * Stop/pause the audio
+ *
+ * @returns {Audio} this
+ */
+ proto.stop = function() {
+ return this.attr({
+ playing: false,
+ time: 0
+ });
+ };
+
+ proto.getComputed = function(key) {};
+
+ return Audio;
+});
View
4 src/runner/environment.js
@@ -24,6 +24,7 @@ define([
'./gradient',
'./text',
'./text_span',
+ './audio',
'./video',
'./filter/builtin',
'./display_list',
@@ -34,7 +35,7 @@ define([
Path, SpecialAttrPath, Rect, Polygon, Star, Ellipse, Circle, Arc,
Bitmap, DisplayObject, Group,
Animation, KeyframeAnimation, easing, FontFamily, Matrix,
- Sprite, color, gradient, Text, TextSpan, Video, filter,
+ Sprite, color, gradient, Text, TextSpan, Audio, Video, filter,
displayList, DOMElement, version
) {
'use strict';
@@ -101,6 +102,7 @@ define([
exports.Movie = bindConstructorToParameters(Movie, [stage]);
exports.Sprite = bindConstructorToParameters(Sprite, [assetLoader]);
exports.Video = bindConstructorToParameters(Video, [assetLoader]);
+ exports.Audio = bindConstructorToParameters(Audio, [assetLoader]);
exports.bonsai = exports;
View
2  src/version.js
@@ -1,3 +1,3 @@
define(function() {
- return '0.3.7';
+ return '0.3.8';
});
View
28 test/asset_audio_handler-spec.js
@@ -0,0 +1,28 @@
+require([
+ 'bonsai/asset/audio_handler',
+ 'bonsai/asset/asset_request'
+], function(AudioHandler, AssetRequest) {
+
+ function makeAssetRequest(suffix) {
+ return new AssetRequest('somefile.' + suffix);
+ }
+
+ describe('AudioAssetHandler', function() {
+
+ it('Accepts valid arg signatures', function() {
+ new AudioHandler(makeAssetRequest('mp3'), 1);
+ new AudioHandler(makeAssetRequest('ogg'), 1, 30000);
+ });
+
+ describe('AudioHandler.getPlayableMimeType', function() {
+ it('is a function', function() {
+ expect(typeof AudioHandler.getPlayableMimeType).toBe('function');
+ });
+ /*it('mimetype for unknown is ""', function() {
+ expect(AudioHandler.playableMimeType('unknown')).toBe('');
+ });*/
+ });
+
+ });
+
+});
View
22 test/asset_handler-spec.js
@@ -1,8 +1,9 @@
define([
'bonsai/asset/asset_handler',
- 'bonsai/asset/asset_request',
- 'bonsai/asset/asset_resource'
-], function(AssetHandler, AssetRequest, AssetResource) {
+ 'bonsai/asset/asset_request'
+], function(AssetHandler, AssetRequest) {
+
+ var toString = {}.toString;
function makeAssetRequest() {
return new AssetRequest('somefile.txt');
@@ -39,12 +40,11 @@ define([
it('Timeout will trigger an error + custom timeout', function() {
- var called = 0;
- var request = new AssetRequest('b/i/k/e/s/h/e/d.txt');
+ var request = new AssetRequest('b/o/n/s/a/i/j/s.txt');
var handler = new AssetHandler(request, 1, 10 /* 10ms */);
var errorHandler = jasmine.createSpy('errorHandler');
- handler.loadResource = function(resource) { /* do nothing */ };
+ handler.loadResource = function() { /* do nothing */ };
handler.on('error', errorHandler);
handler.load();
@@ -57,6 +57,16 @@ define([
});
+ describe('AssetHandler.MIME_TYPES', function() {
+ it('returns an object', function() {
+ expect(toString.call(AssetHandler.MIME_TYPES)).toBe('[object Object]');
+ });
+ it('has at least a `video` and `audio` key', function() {
+ expect(toString.call(AssetHandler.MIME_TYPES.video)).toBe('[object Object]');
+ expect(toString.call(AssetHandler.MIME_TYPES.audio)).toBe('[object Object]');
+ });
+ });
+
});
});
View
71 test/audio-spec.js
@@ -0,0 +1,71 @@
+require([
+ 'bonsai/runner/audio',
+ 'bonsai/runner/group',
+ './runner.js'
+], function(Audio, Group) {
+ describe('Audio', function() {
+
+ it('Provides destroy method which will remove the item from stage and call destroyAsset on its loader', function() {
+ var loader = {
+ destroyAsset: jasmine.createSpy('destroyAsset'),
+ request: function() {}
+ };
+ var d = new Audio(loader, 'abc.mp3', null);
+ var parent = new Group();
+ parent.addChild(d);
+ expect(parent.children()[0]).toBe(d);
+ d.destroy();
+ expect(loader.destroyAsset).toHaveBeenCalled();
+ expect(parent.children()[0]).toBe(void 0);
+ });
+
+ describe('attr', function() {
+ it('Can get and set the volume (0..1)', function() {
+ var a = new Audio();
+ expect(a.attr('volume')).toBe(1);
+ a.attr('volume', 0.98);
+ expect(a.attr('volume')).toBe(0.98);
+ a.attr('volume', null);
+ expect(a.attr('volume')).toBe(0.98); // unchanged
+ a.attr('volume', -1);
+ expect(a.attr('volume')).toBe(0); // nearest acceptable value
+ a.attr('volume', 99);
+ expect(a.attr('volume')).toBe(1); // nearest acceptable value
+ });
+ });
+
+ it('Can play()', function() {
+ var a = new Audio();
+ expect(a.attr('playing')).toBe(false);
+ a.play();
+ expect(a.attr('playing')).toBe(true);
+ expect(a.play()).toBe(a);
+ });
+
+ it('play(undefined) does not send `time` to the renderer', function() {
+ var a = new Audio();
+ a.play(0);
+ expect(a.composeRenderMessage().attributes.time).toBe(0);
+ a.play();
+ expect(a.composeRenderMessage().attributes.time).not.toBeDefined();
+ });
+
+ it('Can play(time)', function() {
+ var a = new Audio();
+ expect(a.attr('playing')).toBe(false);
+ expect(a.attr('time')).toBe(0);
+ a.play(5.17);
+ expect(a.attr('playing')).toBe(true);
+ expect(a.attr('time')).toBe(5.17);
+ });
+
+ it('Can stop()', function() {
+ var a = new Audio();
+ expect(a.attr('playing')).toBe(false);
+ a.attr('playing', true);
+ a.stop();
+ expect(a.attr('playing')).toBe(false);
+ });
+
+ })
+});
View
51 test/renderer/svg-spec.js
@@ -81,5 +81,56 @@ define([
expect(node._filterSignature).toBe('filter:colorMatrix()');
});
});
+
+ describe('drawAudio', function() {
+ it('is a function', function() {
+ expect(typeof createSvgRenderer().drawAudio).toBe('function');
+ });
+ describe('handles a Video Object depending on `message.attributes`', function() {
+ it('attributes.playing=true', function() {
+ var audioElement = { play: jasmine.createSpy('play') };
+ var message = { attributes: { playing: true } };
+ createSvgRenderer().drawAudio(audioElement, message);
+ expect(audioElement.play).toHaveBeenCalled();
+ });
+ it('attributes.playing=false', function() {
+ var audioElement = { pause: jasmine.createSpy('pause') };
+ var message = { attributes: { playing: false } };
+ createSvgRenderer().drawAudio(audioElement, message);
+ expect(audioElement.pause).toHaveBeenCalled();
+ });
+ it('volume is not changed w/o attributes.volume', function() {
+ var audioElement = { volume: 0.123 };
+ var message = { attributes: {} };
+ createSvgRenderer().drawAudio(audioElement, message);
+ expect(audioElement.volume).toBe(0.123);
+ });
+ it('attributes.volume=0', function() {
+ var audioElement = { volume: -1 };
+ var message = { attributes: { volume: 0 } };
+ createSvgRenderer().drawAudio(audioElement, message);
+ expect(audioElement.volume).toBe(0);
+ });
+ it('attributes.volume=0.5', function() {
+ var audioElement = { volume: -1 };
+ var message = { attributes: { volume: 0.5 } };
+ createSvgRenderer().drawAudio(audioElement, message);
+ expect(audioElement.volume).toBe(0.5);
+ });
+ it('attributes.volume=1', function() {
+ var audioElement = { volume: -1 };
+ var message = { attributes: { volume: 1.0 } };
+ createSvgRenderer().drawAudio(audioElement, message);
+ expect(audioElement.volume).toBe(1.0);
+ });
+ it('attributes.volume=NaN (casted to `0`)', function() {
+ var audioElement = { volume: -1 };
+ var message = { attributes: { volume: NaN } };
+ createSvgRenderer().drawAudio(audioElement, message);
+ expect(audioElement.volume).toBe(0.0);
+ });
+ });
+ });
+
});
});
View
2  test/runner.html
@@ -67,9 +67,11 @@
'./asset-loading-spec',
'./asset-request-spec',
'./asset_handler-spec',
+ './asset_audio_handler-spec',
'./asset-resource-spec',
'./uri-spec',
'./bitmap-spec',
+ './audio-spec',
'./video-spec',
'./ui_event-spec',
Please sign in to comment.
Something went wrong with that request. Please try again.