├── module.suffix ├── module.prefix ├── .bowerrc ├── src ├── icon.png ├── favicon.ico ├── assets │ ├── audio │ │ ├── purr-01.mp3 │ │ ├── yawn-01.mp3 │ │ ├── angry-meow-01.mp3 │ │ ├── angry-meow-02.mp3 │ │ ├── angry-meow-03.mp3 │ │ ├── angry-meow-04.mp3 │ │ ├── excited-meow-01.mp3 │ │ ├── excited-meow-02.mp3 │ │ ├── excited-meow-03.mp3 │ │ ├── excited-purr-01.mp3 │ │ ├── ball-bounce-soft-01.mp3 │ │ ├── ball-bounce-default-01.mp3 │ │ ├── ball-bounce-hollow-01.mp3 │ │ ├── ball-bounce-football-01.mp3 │ │ ├── ball-bounce-pingpong-01.mp3 │ │ ├── ball-bounce-basketball-01.mp3 │ │ ├── ball-bounce-basketball-02.mp3 │ │ └── ball-bounce-bowlingball-01.mp3 │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.ttf │ │ ├── icomoon.woff │ │ └── Roboto-Regular.woff │ ├── images │ │ ├── ball01.png │ │ ├── ball02.png │ │ ├── ball03.png │ │ ├── ball04.png │ │ ├── ball05.png │ │ ├── ball06.png │ │ ├── ball07.png │ │ ├── ball08.png │ │ ├── ball09.png │ │ ├── ball10.png │ │ ├── ball11.png │ │ ├── ball12.png │ │ ├── ball13.png │ │ ├── cat-big.gif │ │ ├── default-image.gif │ │ ├── draw-guide-body.gif │ │ ├── draw-guide-head.gif │ │ ├── draw-guide-eyes-open.gif │ │ ├── draw-guide-left-leg.gif │ │ ├── draw-guide-mouth-open.gif │ │ ├── draw-guide-right-leg.gif │ │ ├── draw-guide-eyes-closed.gif │ │ └── draw-guide-mouth-closed.gif │ └── README.md ├── app │ ├── cat │ │ ├── services │ │ │ ├── _services.js │ │ │ ├── ratingService.js │ │ │ ├── catSimplifier.js │ │ │ └── emotion.js │ │ ├── directives │ │ │ ├── _directives.js │ │ │ ├── commentsLink.tpl.html │ │ │ ├── facebookButton.js │ │ │ ├── stage.tpl.html │ │ │ ├── commentsLink.js │ │ │ ├── twitterButton.js │ │ │ └── scrollTo.js │ │ ├── cat.less │ │ ├── cat.js │ │ └── cat.tpl.html │ ├── home │ │ ├── filters │ │ │ ├── _filters.js │ │ │ ├── byTag.js │ │ │ └── byTag.spec.js │ │ ├── home.spec.js │ │ ├── directives │ │ │ ├── tagSelector.tpl.html │ │ │ ├── pagination.tpl.html │ │ │ ├── tagSelector.js │ │ │ ├── previewPanel.tpl.html │ │ │ └── previewPanel.js │ │ ├── home.tpl.html │ │ └── home.js │ ├── draw │ │ ├── services │ │ │ ├── _services.js │ │ │ ├── thumbnailGenerator.js │ │ │ ├── drawHelper.js │ │ │ └── catBuilder.js │ │ ├── directives │ │ │ ├── _directives.js │ │ │ ├── canvas.tpl.html │ │ │ ├── drawInstructions.tpl.html │ │ │ ├── saveDialog.js │ │ │ ├── saveDialog.tpl.html │ │ │ └── drawInstructions.js │ │ ├── draw.tpl.html │ │ ├── draw.less │ │ └── draw.js │ ├── app.spec.js │ └── app.js ├── common │ ├── filters │ │ ├── _filters.js │ │ ├── urlFirendlyName.js │ │ ├── startsWith.js │ │ ├── startsWith.spec.js │ │ ├── urlFriendlyName.spec.js │ │ └── timeAgo.js │ ├── directives │ │ ├── _directives.js │ │ ├── modalDialog.tpl.html │ │ ├── infoPanel.tpl.html │ │ ├── modalDialog.js │ │ ├── infoPanel.js │ │ ├── dirDisqus.js │ │ └── touchEvents.js │ └── services │ │ ├── _services.js │ │ ├── userOptions.js │ │ ├── catFactory.spec.js │ │ ├── rafPolyfill.js │ │ ├── behaviourFactory.js │ │ ├── noiseFactory.js │ │ ├── datastore.js │ │ ├── serializer.js │ │ ├── noiseFactory.spec.js │ │ ├── datastore.spec.js │ │ ├── renderHelper.js │ │ ├── ipCookie.js │ │ ├── primitives.spec.js │ │ └── catFactory.js ├── e2e │ ├── protractor.conf.js │ └── tests │ │ └── index.e2e.js ├── less │ ├── variables.less │ ├── README.md │ ├── icons.less │ └── main.less ├── .htaccess └── index.html ├── api ├── .htaccess ├── Slim │ ├── Exception │ │ ├── Stop.php │ │ └── Pass.php │ ├── LogWriter.php │ ├── Http │ │ ├── Cookies.php │ │ └── Headers.php │ ├── Middleware.php │ └── Middleware │ │ ├── MethodOverride.php │ │ └── PrettyExceptions.php ├── Thumbnail.php └── staticPage.php ├── .gitignore ├── bower.json ├── changelog.tpl ├── package.json ├── README.md ├── karma └── karma-unit.tpl.js ├── karma.conf.js └── CHANGELOG.md /module.suffix: -------------------------------------------------------------------------------- 1 | })( window, window.angular ); 2 | -------------------------------------------------------------------------------- /module.prefix: -------------------------------------------------------------------------------- 1 | (function ( window, angular, undefined ) { 2 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "vendor", 3 | "json": "bower.json" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/icon.png -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /api/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteRule ^ index.php [QSA,L] -------------------------------------------------------------------------------- /src/assets/audio/purr-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/purr-01.mp3 -------------------------------------------------------------------------------- /src/assets/audio/yawn-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/yawn-01.mp3 -------------------------------------------------------------------------------- /src/assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /src/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /src/assets/images/ball01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball01.png -------------------------------------------------------------------------------- /src/assets/images/ball02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball02.png -------------------------------------------------------------------------------- /src/assets/images/ball03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball03.png -------------------------------------------------------------------------------- /src/assets/images/ball04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball04.png -------------------------------------------------------------------------------- /src/assets/images/ball05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball05.png -------------------------------------------------------------------------------- /src/assets/images/ball06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball06.png -------------------------------------------------------------------------------- /src/assets/images/ball07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball07.png -------------------------------------------------------------------------------- /src/assets/images/ball08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball08.png -------------------------------------------------------------------------------- /src/assets/images/ball09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball09.png -------------------------------------------------------------------------------- /src/assets/images/ball10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball10.png -------------------------------------------------------------------------------- /src/assets/images/ball11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball11.png -------------------------------------------------------------------------------- /src/assets/images/ball12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball12.png -------------------------------------------------------------------------------- /src/assets/images/ball13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/ball13.png -------------------------------------------------------------------------------- /src/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /src/assets/images/cat-big.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/cat-big.gif -------------------------------------------------------------------------------- /src/app/cat/services/_services.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 12/03/14. 3 | */ 4 | angular.module('drawACat.cat.services', []); -------------------------------------------------------------------------------- /src/app/home/filters/_filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 24/03/14. 3 | */ 4 | angular.module('drawACat.home.filters', []); -------------------------------------------------------------------------------- /src/assets/audio/angry-meow-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/angry-meow-01.mp3 -------------------------------------------------------------------------------- /src/assets/audio/angry-meow-02.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/angry-meow-02.mp3 -------------------------------------------------------------------------------- /src/assets/audio/angry-meow-03.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/angry-meow-03.mp3 -------------------------------------------------------------------------------- /src/assets/audio/angry-meow-04.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/angry-meow-04.mp3 -------------------------------------------------------------------------------- /src/assets/images/default-image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/default-image.gif -------------------------------------------------------------------------------- /src/common/filters/_filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 20/03/14. 3 | */ 4 | angular.module('drawACat.common.filters', []); -------------------------------------------------------------------------------- /src/app/cat/directives/_directives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 13/03/14. 3 | */ 4 | angular.module('drawACat.cat.directives', []); -------------------------------------------------------------------------------- /src/app/draw/services/_services.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 14/03/14. 3 | */ 4 | angular.module('drawACat.draw.services', []); -------------------------------------------------------------------------------- /src/assets/audio/excited-meow-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/excited-meow-01.mp3 -------------------------------------------------------------------------------- /src/assets/audio/excited-meow-02.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/excited-meow-02.mp3 -------------------------------------------------------------------------------- /src/assets/audio/excited-meow-03.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/excited-meow-03.mp3 -------------------------------------------------------------------------------- /src/assets/audio/excited-purr-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/excited-purr-01.mp3 -------------------------------------------------------------------------------- /src/assets/fonts/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/fonts/Roboto-Regular.woff -------------------------------------------------------------------------------- /src/assets/images/draw-guide-body.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/draw-guide-body.gif -------------------------------------------------------------------------------- /src/assets/images/draw-guide-head.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/draw-guide-head.gif -------------------------------------------------------------------------------- /src/assets/audio/ball-bounce-soft-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/ball-bounce-soft-01.mp3 -------------------------------------------------------------------------------- /src/common/directives/_directives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 17/03/14. 3 | */ 4 | angular.module('drawACat.common.directives', []); -------------------------------------------------------------------------------- /src/app/draw/directives/_directives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 18/03/14. 3 | */ 4 | angular.module('drawACat.draw.directives', []); 5 | -------------------------------------------------------------------------------- /src/assets/audio/ball-bounce-default-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/ball-bounce-default-01.mp3 -------------------------------------------------------------------------------- /src/assets/audio/ball-bounce-hollow-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/ball-bounce-hollow-01.mp3 -------------------------------------------------------------------------------- /src/assets/images/draw-guide-eyes-open.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/draw-guide-eyes-open.gif -------------------------------------------------------------------------------- /src/assets/images/draw-guide-left-leg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/draw-guide-left-leg.gif -------------------------------------------------------------------------------- /src/assets/images/draw-guide-mouth-open.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/draw-guide-mouth-open.gif -------------------------------------------------------------------------------- /src/assets/images/draw-guide-right-leg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/draw-guide-right-leg.gif -------------------------------------------------------------------------------- /src/assets/audio/ball-bounce-football-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/ball-bounce-football-01.mp3 -------------------------------------------------------------------------------- /src/assets/audio/ball-bounce-pingpong-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/ball-bounce-pingpong-01.mp3 -------------------------------------------------------------------------------- /src/assets/images/draw-guide-eyes-closed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/draw-guide-eyes-closed.gif -------------------------------------------------------------------------------- /src/assets/images/draw-guide-mouth-closed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/images/draw-guide-mouth-closed.gif -------------------------------------------------------------------------------- /src/assets/audio/ball-bounce-basketball-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/ball-bounce-basketball-01.mp3 -------------------------------------------------------------------------------- /src/assets/audio/ball-bounce-basketball-02.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/ball-bounce-basketball-02.mp3 -------------------------------------------------------------------------------- /src/assets/audio/ball-bounce-bowlingball-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/drawACatApp/HEAD/src/assets/audio/ball-bounce-bowlingball-01.mp3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | *~ 3 | build/ 4 | bin/ 5 | node_modules/ 6 | vendor/ 7 | api/db.php 8 | api/thumbnails/ 9 | .idea 10 | not_code 11 | src/assets-source -------------------------------------------------------------------------------- /src/assets/README.md: -------------------------------------------------------------------------------- 1 | # The `src/assets` Directory 2 | 3 | There's really not much to say here. Every file in this directory is recursively transferred to `dist/assets/`. 4 | 5 | -------------------------------------------------------------------------------- /src/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 14/03/14. 3 | */ 4 | exports.config = { 5 | specs: [ 6 | './tests/**/*.e2e.js' 7 | ], 8 | 9 | baseUrl: 'http://localhost/GitHub/drawACatApp/build/' 10 | }; -------------------------------------------------------------------------------- /src/less/variables.less: -------------------------------------------------------------------------------- 1 | /** 2 | * These are the variables used throughout the application. This is where 3 | * overwrites that are not specific to components should be maintained. 4 | */ 5 | 6 | @import '../../vendor/bootstrap/less/variables.less'; 7 | 8 | -------------------------------------------------------------------------------- /src/e2e/tests/index.e2e.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 14/03/14. 3 | */ 4 | 5 | describe('index page', function() { 6 | 7 | it('should have the correct title', function() { 8 | browser.get('#'); 9 | expect(browser.getTitle()).toEqual('Draw A Cat!'); 10 | }); 11 | }); -------------------------------------------------------------------------------- /src/app/cat/directives/commentsLink.tpl.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/services/_services.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The purpose of this file is to instantiate a new Angular module for all the common services. It is named with and underscore so that the 3 | * Grunt task that automatically includes all the .js during dev builds will include it first, before any of the services that depend on it. 4 | * Created by Michael on 11/03/14. 5 | */ 6 | angular.module('drawACat.common.services', []); -------------------------------------------------------------------------------- /src/common/directives/modalDialog.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
-------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-boilerplate", 3 | "version": "0.3.1", 4 | "devDependencies": { 5 | "angular": "~1.2.14", 6 | "angular-mocks": "~1.0.7", 7 | "bootstrap": "3.1.1", 8 | "angular-ui-router": "~0.2.10", 9 | "angular-ui-utils": "~0.0.3", 10 | "hammerjs": "~1.0.8" 11 | }, 12 | "dependencies": { 13 | "masonry": "~3.1.5", 14 | "angular-utils-pagination": "~0.4.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/common/directives/infoPanel.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 10 |
-------------------------------------------------------------------------------- /src/app/home/home.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests sit right alongside the file they are testing, which is more intuitive 3 | * and portable than separating `src` and `test` directories. Additionally, the 4 | * build process will exclude all `.spec.js` files from the build 5 | * automatically. 6 | */ 7 | describe( 'home section', function() { 8 | beforeEach( module( 'drawACat.home' ) ); 9 | 10 | it( 'should have a dummy test', inject( function() { 11 | expect( true ).toBeTruthy(); 12 | })); 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /src/common/filters/urlFirendlyName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 20/03/14. 3 | */ 4 | angular.module('drawACat.common.filters') 5 | 6 | .filter('urlFriendlyName', function() { 7 | return function(input) { 8 | 9 | var unicodeToPercentage = encodeURIComponent(input); 10 | var spacesToHyphens = unicodeToPercentage.replace(/%20/g, '-'); 11 | var nonAlphaRemoved = spacesToHyphens.replace(/[^a-zA-Z0-9\-%\s]/g, ''); 12 | return nonAlphaRemoved.toLowerCase(); 13 | }; 14 | }); -------------------------------------------------------------------------------- /src/app/app.spec.js: -------------------------------------------------------------------------------- 1 | describe( 'AppCtrl', function() { 2 | describe( 'isCurrentUrl', function() { 3 | var AppCtrl, $location, $scope; 4 | 5 | beforeEach( module( 'drawACat' ) ); 6 | 7 | beforeEach( inject( function( $controller, _$location_, $rootScope ) { 8 | $location = _$location_; 9 | $scope = $rootScope.$new(); 10 | AppCtrl = $controller( 'AppController', { $location: $location, $scope: $scope }); 11 | })); 12 | 13 | it( 'should pass a dummy test', inject( function() { 14 | expect( AppCtrl ).toBeTruthy(); 15 | })); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/common/filters/startsWith.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 21/03/14. 3 | */ 4 | angular.module('drawACat.common.filters') 5 | 6 | .filter('startsWith', function() { 7 | return function(array, search) { 8 | var matches = []; 9 | for(var i = 0; i < array.length; i++) { 10 | if (array[i].toLowerCase().indexOf(search.toLowerCase()) === 0 && 11 | search.length < array[i].length) { 12 | matches.push(array[i]); 13 | } 14 | } 15 | return matches; 16 | }; 17 | }); -------------------------------------------------------------------------------- /src/common/directives/modalDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 19/03/14. 3 | */ 4 | 5 | angular.module('drawACat.common.directives') 6 | 7 | .directive('dacModalDialog', function() { 8 | return { 9 | restrict: 'E', 10 | templateUrl: 'directives/modalDialog.tpl.html', 11 | transclude: true, 12 | scope: { 13 | show: '=' 14 | }, 15 | link: function(scope) { 16 | scope.hideModal = function() { 17 | scope.show = false; 18 | }; 19 | } 20 | }; 21 | }); -------------------------------------------------------------------------------- /src/app/cat/directives/facebookButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 28/03/14. 3 | */ 4 | angular.module('drawACat.cat.directives') 5 | 6 | .directive('facebookButton', function($location, ezfb) { 7 | 8 | return { 9 | restrict: 'AE', 10 | template: '
', 11 | link: function(scope, element) { 12 | scope.url = $location.absUrl(); 13 | 14 | ezfb.Event.subscribe('edge.create', function() { 15 | scope.rateCat(); 16 | }); 17 | } 18 | }; 19 | }); -------------------------------------------------------------------------------- /src/app/home/directives/tagSelector.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | 9 |
10 | 16 |
-------------------------------------------------------------------------------- /src/app/cat/directives/stage.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 |
8 |
9 |

Mouse coordinates: {{ x }}, {{ y }}

10 |

In drag mode?: {{ ball.isInDragMode() }}

11 |

Happy: {{ moodValue.happy }}

12 |

Angry: {{ moodValue.angry }}

13 |

Excited: {{ moodValue.excited }}

14 |

Bored: {{ moodValue.bored }}

15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/app/cat/services/ratingService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 28/03/14. 3 | */ 4 | 5 | angular.module('drawACat.cat.services') 6 | 7 | .factory('ratingService', function(ipCookie, datastore) { 8 | return { 9 | hasUserRatedThisCat: function(id) { 10 | var catsRated = angular.fromJson(ipCookie('rated')) || []; 11 | return (catsRated.indexOf(id) !== -1); 12 | }, 13 | setCatAsRated: function(id) { 14 | var catsRated = angular.fromJson(ipCookie('rated')) || []; 15 | catsRated.push(id); 16 | ipCookie('rated', angular.toJson(catsRated), { expires: 365 } ); 17 | datastore.rateCat(id); 18 | } 19 | }; 20 | }); -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | 4 | # www is the canonical URL 5 | RewriteCond %{HTTP_HOST} ^drawacat.net$ 6 | RewriteRule (.*) http://www.drawacat.net/$1 [R=301,L] 7 | 8 | # allow facebook & twitter crawler to work by redirecting it to a server-rendered static version on the page 9 | RewriteCond %{HTTP_USER_AGENT} (facebookexternalhit/[0-9]|Twitterbot) 10 | RewriteRule cat/(.*) http://www.drawacat.net/api/staticPage.php/$1 [P] 11 | 12 | # Required to allow direct-linking of pages so they can be processed by Angular 13 | 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteCond %{REQUEST_FILENAME} !-d 16 | RewriteCond %{REQUEST_URI} !index 17 | RewriteRule (.*) index.html [L] 18 | 19 | RewriteCond %{HTTP_HOST} ^.$ 20 | RewriteRule ^(.*) %HTACCESS_ROOT%/$1 [R=301] 21 | -------------------------------------------------------------------------------- /src/app/draw/directives/canvas.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | 9 |
10 |
11 | 12 |
13 | 14 |
-------------------------------------------------------------------------------- /changelog.tpl: -------------------------------------------------------------------------------- 1 | 2 | # <%= version%> (<%= today%>) 3 | 4 | <% if (_(changelog.feat).size() > 0) { %> ## Features 5 | <% _(changelog.feat).forEach(function(changes, scope) { %> 6 | - **<%= scope%>:** 7 | <% changes.forEach(function(change) { %> - <%= change.msg%> (<%= helpers.commitLink(change.sha1) %>) 8 | <% }); %> 9 | <% }); %> <% } %> 10 | 11 | <% if (_(changelog.fix).size() > 0) { %> ## Fixes 12 | <% _(changelog.fix).forEach(function(changes, scope) { %> 13 | - **<%= scope%>:** 14 | <% changes.forEach(function(change) { %> - <%= change.msg%> (<%= helpers.commitLink(change.sha1) %>) 15 | <% }); %> 16 | <% }); %> <% } %> 17 | 18 | <% if (_(changelog.breaking).size() > 0) { %> ## Breaking Changes 19 | <% _(changelog.breaking).forEach(function(changes, scope) { %> 20 | - **<%= scope%>:** 21 | <% changes.forEach(function(change) { %> <%= change.msg%> 22 | <% }); %> 23 | <% }); %> <% } %> 24 | -------------------------------------------------------------------------------- /src/common/filters/startsWith.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 21/03/14. 3 | */ 4 | 5 | describe('startsWith filter', function() { 6 | var startsWithFilter; 7 | var testArray; 8 | 9 | beforeEach(module('drawACat.common.filters')); 10 | beforeEach(inject(function(_$filter_) { 11 | testArray = [ 12 | 'cake', 13 | 'hammer', 14 | 'cup', 15 | 'earth', 16 | 'apple', 17 | 'tap' 18 | ]; 19 | startsWithFilter = _$filter_('startsWith'); 20 | })); 21 | 22 | it('should should return just the items starting with the search string', function() { 23 | expect(startsWithFilter(testArray, 'c')).toEqual(['cake', 'cup']); 24 | }); 25 | 26 | it('should not return items if the search appears mid way through the string', function() { 27 | expect(startsWithFilter(testArray, 'a')).toEqual(['apple']); 28 | }); 29 | 30 | }); -------------------------------------------------------------------------------- /src/app/home/filters/byTag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 24/03/14. 3 | */ 4 | angular.module('drawACat.home.filters') 5 | 6 | .filter('byTag', function() { 7 | return function(catListArray, tagArray) { 8 | if (tagArray.length === 0) { 9 | return catListArray; 10 | } 11 | 12 | var matches = []; 13 | for(var i = 0; i < catListArray.length; i++) { 14 | var catObj = catListArray[i]; 15 | 16 | var containsAllTags = (0 < catObj.tags.length); 17 | for(var j = 0; j < tagArray.length; j++) { 18 | if (catObj.tags.indexOf(tagArray[j]) === -1) { 19 | containsAllTags = false; 20 | } 21 | } 22 | 23 | if (containsAllTags) { 24 | matches.push(catObj); 25 | } 26 | } 27 | return matches; 28 | }; 29 | }); -------------------------------------------------------------------------------- /src/common/filters/urlFriendlyName.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 21/03/14. 3 | */ 4 | 5 | describe('urlFriendlyName filter', function() { 6 | var urlFriendlyNameFilter; 7 | 8 | beforeEach(module('drawACat.common.filters')); 9 | beforeEach(inject(function(_$filter_) { 10 | urlFriendlyNameFilter = _$filter_('urlFriendlyName'); 11 | })); 12 | 13 | /* it('should remove non-alphanumeric characters', function() { 14 | expect(urlFriendlyNameFilter('!"£$%^&*()?><#\'@')).toEqual(''); 15 | }); 16 | */ 17 | it('should convert spaces to hyphens', function() { 18 | expect(urlFriendlyNameFilter('name with some spaces')).toEqual('name-with--some-spaces'); 19 | }); 20 | 21 | it('should make lowercase', function() { 22 | expect(urlFriendlyNameFilter('I Am a CAT!')).toEqual('i-am-a-cat'); 23 | }); 24 | 25 | it('should encode unicode to percentage encoding', function() { 26 | expect(urlFriendlyNameFilter('中国')).toEqual('%e4%b8%ad%e5%9b%bd'); 27 | }); 28 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Michael Bromley", 3 | "name": "drawacat", 4 | "version": "0.0.1", 5 | "homepage": "www.michaelbromley.co.uk", 6 | "licenses": { 7 | "type": "MIT", 8 | "url": "https://raw.github.com/joshdmiller/ng-boilerplate/master/LICENSE" 9 | }, 10 | "bugs": "https://github.com/joshdmiller/ng-boilerplate/issues", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "grunt": "~0.4.1", 14 | "grunt-recess": "~0.3.5", 15 | "grunt-contrib-clean": "~0.4.1", 16 | "grunt-contrib-copy": "~0.4.1", 17 | "grunt-contrib-jshint": "~0.4.3", 18 | "grunt-contrib-concat": "~0.3.0", 19 | "grunt-contrib-watch": "~0.4.0", 20 | "grunt-contrib-uglify": "~0.2.0", 21 | "grunt-karma": "~0.5.0", 22 | "grunt-ngmin": "0.0.2", 23 | "grunt-html2js": "~0.1.3", 24 | "grunt-contrib-coffee": "~0.7.0", 25 | "grunt-coffeelint": "0.0.6", 26 | "grunt-conventional-changelog": "~0.1.1", 27 | "grunt-bump": "0.0.6", 28 | "grunt-contrib-less": "^0.10.0", 29 | "karma-jasmine": "^0.2.1", 30 | "grunt-string-replace": "^0.2.7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/cat/directives/commentsLink.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 26/04/14. 3 | */ 4 | 5 | angular.module('drawACat.cat.directives') 6 | 7 | .directive('dacCommentsLink', function($window) { 8 | 9 | return { 10 | restrict: 'AE', 11 | templateUrl: 'cat/directives/commentsLink.tpl.html', 12 | scope: true, 13 | link: function(scope) { 14 | scope.isTop = true; 15 | 16 | scope.$watch(function() { 17 | return $window.scrollY; 18 | }, function(newVal) { 19 | if (0 < newVal) { 20 | scope.isTop = false; 21 | } else { 22 | scope.isTop = true; 23 | } 24 | }); 25 | 26 | scope.scrollToTarget = function() { 27 | if (scope.isTop) { 28 | var comments = document.getElementById('comments'); 29 | $window.scrollTo(0, comments.offsetTop); 30 | } else { 31 | $window.scrollTo(0, 0); 32 | } 33 | }; 34 | } 35 | }; 36 | }); -------------------------------------------------------------------------------- /src/app/home/directives/pagination.tpl.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/draw/directives/drawInstructions.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ catParts[step].label }}

4 |
5 |

6 | {{ instruction }} 7 |

8 |
9 |
10 | 14 |
15 | 18 |
19 |
20 | 25 |
26 |
-------------------------------------------------------------------------------- /src/app/home/directives/tagSelector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 12/04/14. 3 | */ 4 | 5 | angular.module('drawACat.home.tagSelector', []) 6 | 7 | .directive('tagSelector', function() { 8 | return { 9 | restrict: 'E', 10 | templateUrl: 'home/directives/tagSelector.tpl.html', 11 | link: function(scope, element, attrs) { 12 | scope.keyHandler = function(e) { 13 | if (e.keyCode === 13) { 14 | if (scope.tagsInput === '') { 15 | addTag(); 16 | } 17 | } 18 | }; 19 | scope.removeTag = function(index) { 20 | scope.tagsArray.splice(index, 1); 21 | }; 22 | scope.tagSelectedHandler = function() { 23 | addTag(); 24 | }; 25 | 26 | function addTag() { 27 | if (scope.tagsArray.indexOf(scope.tagsInput) === -1) { 28 | scope.tagsArray.push(scope.tagsInput); 29 | } 30 | scope.tagsInput = ''; 31 | } 32 | } 33 | }; 34 | }) 35 | ; -------------------------------------------------------------------------------- /src/app/home/directives/previewPanel.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | 7 | Play with 8 | {{ cat.name }} 9 | 10 |

11 |
12 | 13 |
14 |
15 | {{ cat.created | timeAgo }} 16 |
17 |
18 | {{ cat.rating }} 19 |
20 |
21 |
22 |
23 |
24 |

by {{ cat.author }}

25 |
26 | 31 |
32 |
33 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Draw A Cat 2 | 3 | ### The world's #1 online digital cat-drawing community 4 | 5 | ### [www.drawacat.net](http://www.drawacat.net) 6 | 7 | This is a side-project I built to learn more about the Canvas API, and to get more familiar with the AngularJS 8 | framework. 9 | 10 | I've open-sourced the code in case anyone is interested to see how parts of it work. Disclaimer: I was learning 11 | a lot about Angular (and JavaScript in general) as I built this, so don't be surprised to see some 12 | inconsistencies in conventions and idioms in different parts of the code base, and patchy unit tests. 13 | 14 | ## Credits 15 | 16 | I had a lot of help in building this application in the form of open-source tools, online resources, and 17 | inspiration. Here are some noteworthy sources: 18 | 19 | * [Hammer.js](http://eightmedia.github.io/hammer.js/) Makes integration with touch devices simple. 20 | * [AngularUI Project](http://angular-ui.github.io/) A bunch of indispensable tools for working with Angular. 21 | * [ng-Boilerplate](https://github.com/ngbp/ngbp) I based the structure of my code on this pattern. 22 | * [Sketch Toy](http://sketchtoy.com/) I was inspired by the sketchy line-art style of this amazing app. 23 | * [Freesound.org](http://www.freesound.org/) I used some sounds from here. 24 | * [Twisted Wave](https://twistedwave.com/online/)An amazing HTML5-based audio editor I used to snip the audio clips. 25 | 26 | ## License 27 | 28 | Creative Commons Attribution NonCommercial (CC-NC) (see LICENSE file) -------------------------------------------------------------------------------- /src/common/directives/infoPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 26/04/14. 3 | */ 4 | 5 | angular.module('drawACat.common.directives') 6 | 7 | .directive('dacInfoPanel', function($timeout, $window) { 8 | 9 | return { 10 | restrict: 'AE', 11 | templateUrl: 'directives/infoPanel.tpl.html', 12 | replace: true, 13 | transclude: true, 14 | scope: true, 15 | link: function(scope, element, attrs) { 16 | scope.infoPanelVisible = true; 17 | 18 | scope.toggleInfoPanel = function() { 19 | if (scope.infoPanelVisible) { 20 | contract(); 21 | } else { 22 | expand(); 23 | } 24 | }; 25 | $timeout(contract, 5000); 26 | 27 | angular.element($window).bind('resize', function() { 28 | if (!scope.infoPanelVisible) { 29 | contract(); 30 | } 31 | }); 32 | 33 | function contract() { 34 | var panelHeight = element[0].offsetHeight; 35 | element.css('top', -panelHeight + 40 + 'px'); 36 | scope.infoPanelVisible = false; 37 | } 38 | 39 | function expand() { 40 | element.css('top', null); 41 | scope.infoPanelVisible = true; 42 | } 43 | } 44 | }; 45 | }); -------------------------------------------------------------------------------- /src/less/README.md: -------------------------------------------------------------------------------- 1 | # The `src/less` Directory 2 | 3 | This folder is actually fairly self-explanatory: it contains your LESS/CSS files to be compiled during the build. 4 | The only important thing to note is that *only* `main.less` will be processed during the build, meaning that all 5 | other stylesheets must be *imported* into that one. 6 | 7 | This should operate somewhat like the routing; the `main.less` file contains all of the site-wide styles, while 8 | any styles that are route-specific should be imported into here from LESS files kept alongside the JavaScript 9 | and HTML sources of that component. For example, the `home` section of the site has some custom styles, which 10 | are imported like so: 11 | 12 | ```css 13 | @import '../app/home/home.less'; 14 | ``` 15 | 16 | The same principal, though not demonstrated in the code, would also apply to reusable components. CSS or LESS 17 | files from external components would also be imported. If, for example, we had a Twitter feed directive with 18 | an accompanying template and style, we would similarly import it: 19 | 20 | ```css 21 | @import '../common/twitterFeed/twitterFeedDirective.less'; 22 | ``` 23 | 24 | Using this decentralized approach for all our code (JavaScript, HTML, and CSS) creates a framework where a 25 | component's directory can be dragged and dropped into *any other project* and it will "just work". 26 | 27 | I would like to eventually automate the importing during the build so that manually importing it here would no 28 | longer be required, but more thought must be put in to whether this is the best approach. 29 | -------------------------------------------------------------------------------- /src/common/services/userOptions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 06/04/14. 3 | */ 4 | angular.module('drawACat.common.services') 5 | 6 | .service('userOptions', function(ipCookie) { 7 | var options = angular.fromJson(ipCookie('options')) || {}; 8 | options.renderQuality = typeof options.renderQuality !== 'undefined' ? options.renderQuality : 10; 9 | options.audioSetting = typeof options.audioSetting !== 'undefined' ? options.audioSetting : true; 10 | options.helpHasBeenSeen = typeof options.helpHasBeenSeen !== 'undefined' ? options.helpHasBeenSeen : false; 11 | 12 | this.setRenderQuality = function(value) { 13 | if (0 <= value && value <= 10) { 14 | options.renderQuality = value; 15 | } 16 | save(); 17 | }; 18 | 19 | this.getRenderQuality = function() { 20 | return options.renderQuality; 21 | }; 22 | 23 | this.setAudioSetting = function(val) { 24 | if (val !== true) { 25 | options.audioSetting = false; 26 | } else { 27 | options.audioSetting = true; 28 | } 29 | save(); 30 | }; 31 | 32 | this.getAudioSetting = function() { 33 | return options.audioSetting; 34 | }; 35 | 36 | this.getHelpHasBeenSeen = function() { 37 | return options.helpHasBeenSeen; 38 | }; 39 | 40 | this.setHelpHasBeenSeen = function() { 41 | options.helpHasBeenSeen = true; 42 | save(); 43 | }; 44 | 45 | function save() { 46 | ipCookie('options', angular.toJson(options), { expires: 365 } ); 47 | } 48 | }) 49 | ; -------------------------------------------------------------------------------- /src/app/draw/services/thumbnailGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 19/03/14. 3 | */ 4 | 5 | angular.module('drawACat.draw.services') 6 | 7 | .factory('thumbnailGenerator', function(CONFIG, renderer) { 8 | 9 | var DIMENSION = 500; // thumbnail dimensions in pixels 10 | var canvas = angular.element('')[0]; 11 | canvas.width = DIMENSION; 12 | canvas.height = DIMENSION; 13 | 14 | function fillBackground() { 15 | var context = canvas.getContext('2d'); 16 | context.fillStyle = CONFIG.FILL_COLOUR; 17 | context.fillRect(0, 0, DIMENSION, DIMENSION); 18 | } 19 | 20 | function getScaledCatPath(cat) { 21 | var path = cat.getPath(); 22 | var width = cat.getWidth(); 23 | var height = cat.getHeight(); 24 | var scaleFactor = DIMENSION / Math.max(width, height); 25 | var scaledPath; 26 | scaledPath = path.map(function (line) { 27 | return line.map(function (point) { 28 | return [point[0] * scaleFactor, point[1] * scaleFactor]; 29 | }); 30 | } 31 | ); 32 | return scaledPath; 33 | } 34 | 35 | 36 | var generateThumbnailFromCat = function(cat) { 37 | renderer.setCanvas(canvas); 38 | renderer.fillStyle(CONFIG.FILL_COLOUR); 39 | renderer.strokeStyle(CONFIG.STROKE_COLOUR); 40 | renderer.lineWidth(3); 41 | 42 | fillBackground(); 43 | 44 | var path = getScaledCatPath(cat); 45 | renderer.renderPath(path); 46 | 47 | return canvas.toDataURL(); 48 | }; 49 | 50 | 51 | return { 52 | getDataUri: generateThumbnailFromCat 53 | }; 54 | }); -------------------------------------------------------------------------------- /src/app/draw/directives/saveDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 20/03/14. 3 | */ 4 | angular.module('drawACat.draw.directives') 5 | 6 | .directive('dacSaveDialog', function(datastore) { 7 | 8 | function getTagsFromDescription(description) { 9 | var regexp = /#([a-zA-Z0-9_]+)/g; 10 | var tags = []; 11 | var match; 12 | while (match = regexp.exec(description)) { 13 | if (tags.indexOf(match[1].toLowerCase()) === -1) { 14 | tags.push(match[1].toLowerCase()); 15 | } 16 | } 17 | return tags; 18 | } 19 | 20 | return { 21 | restrict: 'E', 22 | templateUrl: 'draw/directives/saveDialog.tpl.html', 23 | scope: { 24 | show: '=', 25 | submit: '&' 26 | }, 27 | link: function(scope) { 28 | scope.isPublic = true; 29 | scope.buttonText = "Save Cat"; 30 | 31 | scope.hashTags = []; 32 | datastore.getTags().then(function(data) { 33 | scope.hashTags = data.data; 34 | }); 35 | 36 | scope.hideModal = function() { 37 | scope.show = false; 38 | }; 39 | 40 | scope.submitForm = function() { 41 | scope.buttonText = "Saving..."; 42 | scope.submit({formData: { 43 | name: scope.name, 44 | description: scope.description, 45 | author: scope.author, 46 | isPublic: scope.isPublic, 47 | tags: getTagsFromDescription(scope.description) 48 | }}); 49 | }; 50 | } 51 | }; 52 | }); -------------------------------------------------------------------------------- /api/Slim/Exception/Stop.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2011 Josh Lockhart 7 | * @link http://www.slimframework.com 8 | * @license http://www.slimframework.com/license 9 | * @version 2.4.2 10 | * @package Slim 11 | * 12 | * MIT LICENSE 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | namespace Slim\Exception; 34 | 35 | /** 36 | * Stop Exception 37 | * 38 | * This Exception is thrown when the Slim application needs to abort 39 | * processing and return control flow to the outer PHP script. 40 | * 41 | * @package Slim 42 | * @author Josh Lockhart 43 | * @since 1.0.0 44 | */ 45 | class Stop extends \Exception 46 | { 47 | } 48 | -------------------------------------------------------------------------------- /src/app/draw/draw.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |

Draw A Cat

24 |

25 | Draw your own cat using your mouse or finger! Each part of the cat is drawn separately so that it can come to life when you're done. 26 | Follow the instructions below, and use the blueprint as a guide. 27 |

28 |

29 | If your hand slips, you can undo the last line you drew by pressing the 30 | "undo" button in the top right of the canvas. 31 |

32 |

33 | When you've drawn all the parts, you'll be able to save your cat and share it with the world. Have fun! 34 |

35 |

36 |
37 | 38 |
39 |
40 | 41 |
-------------------------------------------------------------------------------- /src/app/cat/directives/twitterButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 28/03/14. 3 | */ 4 | angular.module('drawACat.cat.directives') 5 | 6 | .directive('twitterButton', function($window, $location) { 7 | 8 | // load the Twitter button script 9 | // https://dev.twitter.com/docs/intents/events#events 10 | $window.twttr = (function (d,s,id) { 11 | var t, js, fjs = d.getElementsByTagName(s)[0]; 12 | if (d.getElementById(id)) { 13 | return; 14 | } 15 | js=d.createElement(s); 16 | js.id=id; 17 | js.src="https://platform.twitter.com/widgets.js"; 18 | fjs.parentNode.insertBefore(js, fjs); 19 | return window.twttr || (t = { _e: [], ready: function(f){ t._e.push(f); } }); 20 | }(document, "script", "twitter-wjs")); 21 | 22 | return { 23 | restrict: 'AE', 24 | link: function(scope, element) { 25 | 26 | var url = $location.absUrl(); 27 | 28 | scope.$watch(function() { 29 | return ($window.twttr.widgets !== undefined); 30 | }, function(isLoaded) { 31 | if (isLoaded) { 32 | $window.twttr.widgets.createShareButton( 33 | url, 34 | element[0], 35 | function (el) { 36 | }, 37 | { 38 | count: 'horizontal', 39 | hashtags: 'drawacat' 40 | } 41 | ); 42 | $window.twttr.events.bind('tweet', function (event) { 43 | scope.rateCat(); 44 | }); 45 | } 46 | }); 47 | 48 | } 49 | }; 50 | }); -------------------------------------------------------------------------------- /api/Slim/Exception/Pass.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2011 Josh Lockhart 7 | * @link http://www.slimframework.com 8 | * @license http://www.slimframework.com/license 9 | * @version 2.4.2 10 | * @package Slim 11 | * 12 | * MIT LICENSE 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | namespace Slim\Exception; 34 | 35 | /** 36 | * Pass Exception 37 | * 38 | * This Exception will cause the Router::dispatch method 39 | * to skip the current matching route and continue to the next 40 | * matching route. If no subsequent routes are found, a 41 | * HTTP 404 Not Found response will be sent to the client. 42 | * 43 | * @package Slim 44 | * @author Josh Lockhart 45 | * @since 1.0.0 46 | */ 47 | class Pass extends \Exception 48 | { 49 | } 50 | -------------------------------------------------------------------------------- /api/Thumbnail.php: -------------------------------------------------------------------------------- 1 | thumbnailDataString = $imageData; 21 | } 22 | 23 | function save() { 24 | if (isset($this->thumbnailDataString)) { 25 | $hash = md5($this->thumbnailDataString); 26 | $this->thumbnailFileName = $hash; 27 | 28 | 29 | $original = imagecreatefromstring(base64_decode($this->thumbnailDataString)); 30 | $originalWidth = imagesx($original); 31 | $originalHeight = imagesy($original); 32 | 33 | $thumbMid = imagecreatetruecolor(self::THUMBNAIL_MID_DIMENSIONS, self::THUMBNAIL_MID_DIMENSIONS); 34 | imagecopyresampled($thumbMid, $original, 0, 0, 0, 0, self::THUMBNAIL_MID_DIMENSIONS, self::THUMBNAIL_MID_DIMENSIONS, $originalWidth, $originalHeight); 35 | 36 | $thumbSmall = imagecreatetruecolor(self::THUMBNAIL_SMALL_DIMENSIONS, self::THUMBNAIL_SMALL_DIMENSIONS); 37 | imagecopyresampled($thumbSmall, $original, 0, 0, 0, 0, self::THUMBNAIL_SMALL_DIMENSIONS, self::THUMBNAIL_SMALL_DIMENSIONS, $originalWidth, $originalHeight); 38 | 39 | 40 | imagetruecolortopalette($original, false, 8); 41 | imagetruecolortopalette($thumbMid, false, 8); 42 | imagetruecolortopalette($thumbSmall, false, 8); 43 | imagegif($original, self::FILE_PATH.$this->thumbnailFileName.'.gif'); 44 | imagegif($thumbMid, self::FILE_PATH.$this->thumbnailFileName.'_m.gif'); 45 | imagegif($thumbSmall, self::FILE_PATH.$this->thumbnailFileName.'_s.gif'); 46 | } 47 | } 48 | 49 | function getFileName() { 50 | if (isset($this->thumbnailFileName)) { 51 | return $this->thumbnailFileName; 52 | } else { 53 | return 'file not saved!'; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/common/services/catFactory.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 14/03/14. 3 | */ 4 | describe('catFactory service', function() { 5 | 6 | var cat; 7 | var testPart; 8 | var testPart2; 9 | 10 | beforeEach( module( 'drawACat' ) ); 11 | beforeEach( module( 'drawACat.common.services' ) ); 12 | 13 | beforeEach( inject( function( _catFactory_, _primitives_ ) { 14 | cat = _catFactory_.newCat(); 15 | 16 | testPart = _primitives_.Part(); 17 | testPart.createFromPath('test', [[[0, 0],[25, 20]]]); 18 | testPart2 = _primitives_.Part(); 19 | testPart2.createFromPath('test', [[[10, 5],[10, 60]]]); 20 | })); 21 | 22 | it('should start as an empty cat', function() { 23 | expect(cat.bodyParts.head).toEqual({}); 24 | }); 25 | 26 | it('should return the correct path', function() { 27 | expect(cat.getPath()).toEqual([]); 28 | cat.bodyParts.head.part = testPart; 29 | expect(cat.getPath()).toEqual([ 30 | [ 31 | [0, 0], 32 | [25, 20] 33 | ] 34 | ]); 35 | }); 36 | 37 | it('should return the correct height', function() { 38 | cat.bodyParts.head.part = testPart; 39 | cat.bodyParts.body.part = testPart2; 40 | expect(cat.getHeight()).toEqual(60); 41 | }); 42 | 43 | it('should return the correct width', function() { 44 | cat.bodyParts.head.part = testPart; 45 | cat.bodyParts.body.part = testPart2; 46 | expect(cat.getWidth()).toEqual(25); 47 | }); 48 | 49 | it('should adjust the position of the parts', function() { 50 | cat.bodyParts.head.part = testPart; 51 | expect(cat.bodyParts.head.part.getTransformationData().centreX).toEqual(12.5); 52 | expect(cat.bodyParts.head.part.getTransformationData().centreY).toEqual(10); 53 | 54 | cat.adjustPosition(30, 25); 55 | 56 | expect(cat.bodyParts.head.part.getTransformationData().centreX).toEqual(42.5); 57 | expect(cat.bodyParts.head.part.getTransformationData().centreY).toEqual(35); 58 | }); 59 | 60 | }); -------------------------------------------------------------------------------- /src/app/home/home.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Draw A Cat! 4 |
5 |
6 |
7 |
8 | 9 |
10 |
11 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 | Showing {{ pageLower | number }} - {{ pageUpper | number }} 23 | of {{ totalItems | number }} cats 24 |
25 | 26 | 30 |
31 | 32 |
33 |
34 | 35 |
36 | 38 |
39 | 40 |
41 |
42 | 43 |

About Draw A Cat

44 |

45 | 46 |

47 |
48 | 49 |
50 |
-------------------------------------------------------------------------------- /karma/karma-unit.tpl.js: -------------------------------------------------------------------------------- 1 | module.exports = function ( karma ) { 2 | karma.set({ 3 | /** 4 | * From where to look for files, starting with the location of this file. 5 | */ 6 | basePath: '../', 7 | 8 | /** 9 | * This is the list of file patterns to load into the browser during testing. 10 | */ 11 | files: [ 12 | <% scripts.forEach( function ( file ) { %>'<%= file %>', 13 | <% }); %> 14 | 'src/**/*.js', 15 | 'src/**/*.coffee', 16 | ], 17 | exclude: [ 18 | 'src/assets/**/*.js', 19 | 'src/e2e/*', 20 | 'src/**/*.e2e.js' 21 | ], 22 | frameworks: [ 'jasmine' ], 23 | plugins: [ 'karma-jasmine', 'karma-firefox-launcher', 'karma-chrome-launcher', 'karma-phantomjs-launcher', 'karma-coffee-preprocessor' ], 24 | preprocessors: { 25 | '**/*.coffee': 'coffee', 26 | }, 27 | 28 | /** 29 | * How to report, by default. 30 | */ 31 | reporters: 'dots', 32 | 33 | /** 34 | * On which port should the browser connect, on which port is the test runner 35 | * operating, and what is the URL path for the browser to use. 36 | */ 37 | port: 9018, 38 | runnerPort: 9100, 39 | urlRoot: '/', 40 | 41 | /** 42 | * Disable file watching by default. 43 | */ 44 | autoWatch: false, 45 | 46 | /** 47 | * The list of browsers to launch to test on. This includes only "Firefox" by 48 | * default, but other browser names include: 49 | * Chrome, ChromeCanary, Firefox, Opera, Safari, PhantomJS 50 | * 51 | * Note that you can also use the executable name of the browser, like "chromium" 52 | * or "firefox", but that these vary based on your operating system. 53 | * 54 | * You may also leave this blank and manually navigate your browser to 55 | * http://localhost:9018/ when you're running tests. The window/tab can be left 56 | * open and the tests will automatically occur there during the build. This has 57 | * the aesthetic advantage of not launching a browser every time you save. 58 | */ 59 | browsers: [ 60 | 'PhantomJS' 61 | ] 62 | }); 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /src/app/draw/directives/saveDialog.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 24 |
25 |
26 | 30 |
31 | 32 |
33 |
34 |
35 |
-------------------------------------------------------------------------------- /src/app/home/filters/byTag.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 24/03/14. 3 | */ 4 | 5 | describe('byTag filter', function() { 6 | var byTagFilter; 7 | var catListArray; 8 | 9 | beforeEach(module('drawACat.home.filters')); 10 | beforeEach(inject(function(_$filter_) { 11 | catListArray = [ 12 | { 13 | name: 'cat1', 14 | tags: [] 15 | }, 16 | { 17 | name: 'cat2', 18 | tags: ['tag1'] 19 | }, 20 | { 21 | name: 'cat3', 22 | tags: ['tag1', 'tag2'] 23 | }, 24 | { 25 | name: 'cat4', 26 | tags: ['tag2', 'tag3', 'tag4'] 27 | } 28 | ]; 29 | byTagFilter = _$filter_('byTag'); 30 | })); 31 | 32 | it('should return full input array if empty search array', function() { 33 | expect(byTagFilter(catListArray, [])).toEqual(catListArray); 34 | }); 35 | 36 | it('should filter correctly with single tag', function() { 37 | var expectedResult = [ 38 | { 39 | name: 'cat2', 40 | tags: ['tag1'] 41 | }, 42 | { 43 | name: 'cat3', 44 | tags: ['tag1', 'tag2'] 45 | } 46 | ]; 47 | 48 | expect(byTagFilter(catListArray, ['tag1'])).toEqual(expectedResult); 49 | }); 50 | 51 | it('should filter correctly with multiple tags', function() { 52 | var expectedResult = [ 53 | { 54 | name: 'cat3', 55 | tags: ['tag1', 'tag2'] 56 | } 57 | ]; 58 | 59 | expect(byTagFilter(catListArray, ['tag1', 'tag2'])).toEqual(expectedResult); 60 | }); 61 | 62 | it('should filter correctly with multiple non-consecutive tags', function() { 63 | var expectedResult = [ 64 | { 65 | name: 'cat4', 66 | tags: ['tag2', 'tag3', 'tag4'] 67 | } 68 | ]; 69 | 70 | expect(byTagFilter(catListArray, ['tag2', 'tag4'])).toEqual(expectedResult); 71 | }); 72 | 73 | 74 | 75 | }); -------------------------------------------------------------------------------- /src/common/services/rafPolyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 12/03/14. 3 | */ 4 | angular.module('drawACat.cat.services') 5 | /** 6 | * Service to polyfill the $window service with fallbacks for requestAnimationFrame and cancelAnimationFrame for browsers that do not 7 | * support the native methods. 8 | * 9 | * --------------------------------------------------------------------------- 10 | * Adapted from https://gist.github.com/paulirish/1579671 which derived from 11 | * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 12 | * http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 13 | * 14 | * requestAnimationFrame polyfill by Erik Möller. 15 | * Fixes from Paul Irish, Tino Zijdel, Andrew Mao, Klemen Slavič, Darius Bacon 16 | * 17 | * MIT license 18 | * 19 | */ 20 | .factory('rafPolyfill', function($window, $timeout) 21 | { 22 | 23 | var runPolyfill = function() { 24 | 25 | 26 | if (!Date.now) { 27 | Date.now = function() { return new Date().getTime(); }; 28 | } 29 | 30 | var vendors = ['webkit', 'moz']; 31 | for (var i = 0; i < vendors.length && !$window.requestAnimationFrame; ++i) { 32 | var vp = vendors[i]; 33 | $window.requestAnimationFrame = $window[vp+'RequestAnimationFrame']; 34 | $window.cancelAnimationFrame = ($window[vp+'CancelAnimationFrame'] || 35 | $window[vp+'CancelRequestAnimationFrame']); 36 | } 37 | if (/iP(ad|hone|od).*OS 6/.test($window.navigator.userAgent) || // iOS6 is buggy 38 | !$window.requestAnimationFrame || !$window.cancelAnimationFrame) { 39 | var lastTime = 0; 40 | $window.requestAnimationFrame = function(callback) { 41 | var now = Date.now(); 42 | var nextTime = Math.max(lastTime + 16, now); 43 | return $timeout(function() { callback(lastTime = nextTime); }, 44 | nextTime - now); 45 | }; 46 | $window.cancelAnimationFrame = $timeout.cancel; 47 | } 48 | }; 49 | 50 | 51 | return { 52 | run: runPolyfill 53 | }; 54 | }); 55 | -------------------------------------------------------------------------------- /src/app/draw/directives/drawInstructions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 18/03/14. 3 | */ 4 | 5 | angular.module('drawACat.draw.directives') 6 | 7 | .directive('dacDrawInstructions', function(drawHelper) { 8 | 9 | var instructions = { 10 | head: 'Try to draw the outline of the head in one go. That way it\'ll get filled in (shown by a pink tint). Don\'t forget the nose and whiskers too!', 11 | eyesOpen: 'The eyes should be somewhere on the head, and there should be two of them!', 12 | eyesClosed: 'Draw the closed eyes over the top of the open eyes - you should still see a faint outline of them.', 13 | mouthClosed: 'The mouth should go somewhere under the nose!', 14 | mouthOpen: 'Draw the open mouth over the closed mouth - you should still see a faint outline of it.', 15 | body: 'The body should include the hind legs and tail. Make sure the neck comes well above the chin (it\'ll be covered by the head anyway).', 16 | leftLeg: 'Try to draw the outline of the leg all in one go so it gets filled in.', 17 | rightLeg: 'Same with the right leg. Once you\'re done click "Save Cat"!' 18 | }; 19 | 20 | return { 21 | restrict: 'E', 22 | templateUrl: 'draw/directives/drawInstructions.tpl.html', 23 | replace: true, 24 | link: function(scope) { 25 | scope.isFirstStep = true; 26 | scope.isLastStep = false; 27 | 28 | var showInstructions = function() { 29 | scope.currentStepLabel = drawHelper.getCurrentPartLabel(); 30 | scope.currentStep = drawHelper.getCurrentPartKey(); 31 | scope.instruction = instructions[scope.currentStep]; 32 | }; 33 | showInstructions(); 34 | 35 | scope.$watch(function() { 36 | return drawHelper.getCurrentPartKey(); 37 | }, function() { 38 | showInstructions(); 39 | scope.isFirstStep = (drawHelper.getCurrentPartKey() === 'head'); 40 | scope.isLastStep = (drawHelper.getCurrentPartKey() === 'rightLeg'); 41 | }); 42 | 43 | } 44 | }; 45 | }) 46 | ; 47 | 48 | -------------------------------------------------------------------------------- /src/common/services/behaviourFactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 08/03/14. 3 | */ 4 | angular.module('drawACat.common.services') 5 | 6 | .factory('behaviourFactory', function() { 7 | /** 8 | * A Behaviour object defines how a part will respond to user input. 9 | * 10 | * Behaviour is defined as how a part reacts to user input. The default behaviour is that 11 | * a part will not react at all. By setting the parameters of the Behaviour object, we are 12 | * defining how the transformation data of the part will be affected by the position of the 13 | * user input. Eg a positive value for xSkew will cause the part to skew towards the pointer or 14 | * ball or whatever is providing the input data. 15 | * @constructor 16 | */ 17 | var Behaviour = function() { 18 | 19 | // the following values should range between -1 ... 1. Default is 0. 20 | this.sensitivity = { 21 | xOffset: 0, 22 | yOffset: 0, 23 | xSkew: 0, 24 | ySkew: 0, 25 | rotation: 0 26 | }; 27 | 28 | this.range = 100; 29 | this.visible = true; 30 | }; 31 | 32 | /** 33 | * Takes an object containing one or more properties of the `sensitivity` member, and sets the 34 | * value of that property for the object instance. 35 | * @param sensitivityObject 36 | */ 37 | Behaviour.prototype.setSensitivity = function(sensitivityObject) { 38 | for(var key in sensitivityObject) { 39 | if (sensitivityObject.hasOwnProperty(key)) { 40 | this.sensitivity[key] = sensitivityObject[key]; 41 | } 42 | } 43 | }; 44 | 45 | /** 46 | * Returns a plain object that can be serialized into JSON. 47 | */ 48 | Behaviour.prototype.toSerializable = function() { 49 | var serializable = {}; 50 | serializable.sensitivity = this.sensitivity; 51 | serializable.range = this.range; 52 | return serializable; 53 | }; 54 | 55 | return { 56 | newBehaviour: function() { 57 | return new Behaviour(); 58 | } 59 | }; 60 | } 61 | ); -------------------------------------------------------------------------------- /src/app/app.js: -------------------------------------------------------------------------------- 1 | angular.module( 'drawACat', [ 2 | 'templates-app', 3 | 'templates-common', 4 | 'ui.router', 5 | 'drawACat.home', 6 | 'drawACat.cat', 7 | 'drawACat.draw', 8 | 'drawACat.common.services', 9 | 'drawACat.common.directives', 10 | 'drawACat.common.filters', 11 | 'ngAnimate' 12 | ]) 13 | 14 | .value('CONFIG', { 15 | API_URL: '%API_PATH%', 16 | THUMBNAILS_URL: '%THUMBNAILS_PATH%', 17 | AUDIO_FILES_URL: 'assets/audio/', 18 | FILL_COLOUR: '#f5f5f5', 19 | STROKE_COLOUR: '#333333' 20 | }) 21 | 22 | .config( function myAppConfig ( $stateProvider, $urlRouterProvider, $locationProvider ) { 23 | $locationProvider.html5Mode(true).hashPrefix('!'); 24 | $urlRouterProvider.otherwise( 'home' ); 25 | }) 26 | 27 | .run( function run (rafPolyfill) { 28 | rafPolyfill.run();// polyfill the $window.requestAnimationFrame, cancelAnimationFrame methods 29 | }) 30 | 31 | .controller( 'AppController', function AppController ( $scope, $timeout, $window, $state, $location, $anchorScroll ) { 32 | $scope.embed = $location.search().embed; 33 | 34 | $scope.$on('$stateChangeSuccess', function(event, toState){ 35 | if ( angular.isDefined( toState.data.pageTitle ) ) { 36 | $scope.pageTitle = toState.data.pageTitle ; 37 | } 38 | $anchorScroll(); 39 | if ($state.current.name === 'draw') { 40 | $scope.isDrawState = true; 41 | } else { 42 | $scope.isDrawState = false; 43 | } 44 | }); 45 | 46 | $scope.$on('metadata:updated', function(event, metaData) { 47 | $scope.metaData = metaData; 48 | $timeout(function () { 49 | // push event to google analytics. This is done in a $timeout 50 | // so the current $digest loop has a chance to actually update the 51 | // HTML with the correct page title etc. 52 | $window.ga('send', 'pageview', { page: $location.path() }); 53 | }); 54 | }); 55 | 56 | $scope.scrollTo = function(id) { 57 | var old = $location.hash(); 58 | $location.hash(id); 59 | $anchorScroll(); 60 | //reset to old to keep any additional routing logic from kicking in 61 | $location.hash(old); 62 | }; 63 | }) 64 | 65 | ; 66 | 67 | -------------------------------------------------------------------------------- /src/less/icons.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src:url('fonts/icomoon.eot?-bel06r'); 4 | src:url('fonts/icomoon.eot?#iefix-bel06r') format('embedded-opentype'), 5 | url('fonts/icomoon.woff?-bel06r') format('woff'), 6 | url('fonts/icomoon.ttf?-bel06r') format('truetype'), 7 | url('fonts/icomoon.svg?-bel06r#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | font-family: 'icomoon'; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .icon-caret-down:before { 27 | content: "\f0d7"; 28 | } 29 | .icon-caret-up:before { 30 | content: "\f0d8"; 31 | } 32 | .icon-chevron-circle-up:before { 33 | content: "\f139"; 34 | } 35 | .icon-home:before { 36 | content: "\e600"; 37 | } 38 | .icon-pencil:before { 39 | content: "\e616"; 40 | } 41 | .icon-pencil2:before { 42 | content: "\e601"; 43 | } 44 | .icon-tag:before { 45 | content: "\e603"; 46 | } 47 | .icon-tags:before { 48 | content: "\e604"; 49 | } 50 | .icon-envelope:before { 51 | content: "\e605"; 52 | } 53 | .icon-calendar:before { 54 | content: "\e606"; 55 | } 56 | .icon-undo:before { 57 | content: "\e607"; 58 | } 59 | .icon-bubbles:before { 60 | content: "\e608"; 61 | } 62 | .icon-spinner:before { 63 | content: "\e609"; 64 | } 65 | .icon-cog:before { 66 | content: "\e60a"; 67 | } 68 | .icon-menu:before { 69 | content: "\e60b"; 70 | } 71 | .icon-star:before { 72 | content: "\e60c"; 73 | } 74 | .icon-star2:before { 75 | content: "\e60d"; 76 | } 77 | .icon-info:before { 78 | content: "\e60e"; 79 | } 80 | .icon-info2:before { 81 | content: "\e60f"; 82 | } 83 | .icon-close:before { 84 | content: "\e617"; 85 | } 86 | .icon-volume-medium:before { 87 | content: "\e610"; 88 | } 89 | .icon-volume-mute:before { 90 | content: "\e611"; 91 | } 92 | .icon-arrow-right:before { 93 | content: "\e602"; 94 | } 95 | .icon-arrow-left:before { 96 | content: "\e615"; 97 | } 98 | .icon-code:before { 99 | content: "\e618"; 100 | } 101 | .icon-googleplus:before { 102 | content: "\e612"; 103 | } 104 | .icon-facebook:before { 105 | content: "\e613"; 106 | } 107 | .icon-twitter:before { 108 | content: "\e614"; 109 | } 110 | -------------------------------------------------------------------------------- /src/common/services/noiseFactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 12/03/14. 3 | */ 4 | 5 | angular.module('drawACat.common.services') 6 | /** 7 | * Service to generate 1-dimentional Perlin noiseFactory. Based on the excellent article at Scratchapixel: 8 | * http://www.scratchapixel.com/lessons/3d-advanced-lessons/noiseFactory-part-1/creating-a-simple-1d-noiseFactory/ 9 | * 10 | */ 11 | .factory('noiseFactory', function() { 12 | 13 | var Simple1DNoise = function() { 14 | var MAX_VERTICES = 256; 15 | var MAX_VERTICES_MASK = MAX_VERTICES -1; 16 | var amplitude = 1; 17 | var scale = 1; 18 | 19 | var r = []; 20 | 21 | for ( var i = 0; i < MAX_VERTICES; ++i ) { 22 | r.push(Math.random()); 23 | } 24 | 25 | var getVal = function( x ){ 26 | var scaledX = x * scale; 27 | var xFloor = Math.floor(scaledX); 28 | var t = scaledX - xFloor; 29 | var tRemapSmoothstep = t * t * ( 3 - 2 * t ); 30 | 31 | /// Modulo using & 32 | var xMin = xFloor & MAX_VERTICES_MASK; 33 | var xMax = ( xMin + 1 ) & MAX_VERTICES_MASK; 34 | 35 | var y = lerp( r[ xMin ], r[ xMax ], tRemapSmoothstep ); 36 | 37 | return y * amplitude; 38 | }; 39 | 40 | /** 41 | * Linear interpolation function. 42 | * @param a The lower integer value 43 | * @param b The upper integer value 44 | * @param t The value between the two 45 | * @returns {number} 46 | */ 47 | var lerp = function(a, b, t ) { 48 | return a * ( 1 - t ) + b * t; 49 | }; 50 | 51 | // return the API 52 | return { 53 | getVal: getVal, 54 | setAmplitude: function(newAmplitude) { 55 | amplitude = newAmplitude; 56 | }, 57 | setScale: function(newScale) { 58 | scale = newScale; 59 | }, 60 | // just an alias for setScale make the code more clear 61 | setFrequency: function(frequency) { 62 | scale = frequency; 63 | } 64 | }; 65 | }; 66 | 67 | return { 68 | newGenerator: function() { 69 | return new Simple1DNoise(); 70 | } 71 | }; 72 | }); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Karma config file for the internal PHPStorm Karma runner. 3 | */ 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | 8 | // base path, that will be used to resolve files and exclude 9 | basePath: '', 10 | 11 | 12 | // frameworks to use 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'vendor/angular/angular.js', 19 | 'vendor/angular-bootstrap/ui-bootstrap-tpls.min.js', 20 | 'vendor/placeholders/angular-placeholders-0.0.1-SNAPSHOT.min.js', 21 | 'vendor/angular-ui-router/release/angular-ui-router.js', 22 | 'vendor/angular-ui-utils/modules/route/route.js', 23 | 'vendor/angular-easyfb/angular-easyfb.js', 24 | 'build/templates-app.js', 25 | 'build/templates-common.js', 26 | 'vendor/angular-mocks/angular-mocks.js', 27 | 28 | 'src/**/*.js' 29 | ], 30 | 31 | 32 | // list of files to exclude 33 | exclude: [ 34 | 'src/e2e/**/*.js' 35 | ], 36 | 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 40 | reporters: ['progress'], 41 | 42 | 43 | // web server port 44 | port: 9876, 45 | 46 | 47 | // enable / disable colors in the output (reporters and logs) 48 | colors: true, 49 | 50 | 51 | // level of logging 52 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 53 | logLevel: config.LOG_INFO, 54 | 55 | 56 | // enable / disable watching file and executing tests whenever any file changes 57 | autoWatch: false, 58 | 59 | 60 | // Start these browsers, currently available: 61 | // - Chrome 62 | // - ChromeCanary 63 | // - Firefox 64 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 65 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 66 | // - PhantomJS 67 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 68 | browsers: ['Firefox'], 69 | 70 | 71 | // If browser does not capture in given timeout [ms], kill it 72 | captureTimeout: 60000, 73 | 74 | 75 | // Continuous Integration mode 76 | // if true, it capture browsers, run tests and exit 77 | singleRun: false 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /api/Slim/LogWriter.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2011 Josh Lockhart 7 | * @link http://www.slimframework.com 8 | * @license http://www.slimframework.com/license 9 | * @version 2.4.2 10 | * @package Slim 11 | * 12 | * MIT LICENSE 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | namespace Slim; 34 | 35 | /** 36 | * Log Writer 37 | * 38 | * This class is used by Slim_Log to write log messages to a valid, writable 39 | * resource handle (e.g. a file or STDERR). 40 | * 41 | * @package Slim 42 | * @author Josh Lockhart 43 | * @since 1.6.0 44 | */ 45 | class LogWriter 46 | { 47 | /** 48 | * @var resource 49 | */ 50 | protected $resource; 51 | 52 | /** 53 | * Constructor 54 | * @param resource $resource 55 | * @throws \InvalidArgumentException If invalid resource 56 | */ 57 | public function __construct($resource) 58 | { 59 | if (!is_resource($resource)) { 60 | throw new \InvalidArgumentException('Cannot create LogWriter. Invalid resource handle.'); 61 | } 62 | $this->resource = $resource; 63 | } 64 | 65 | /** 66 | * Write message 67 | * @param mixed $message 68 | * @param int $level 69 | * @return int|bool 70 | */ 71 | public function write($message, $level = null) 72 | { 73 | return fwrite($this->resource, (string) $message . PHP_EOL); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/common/filters/timeAgo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 24/03/14. 3 | */ 4 | angular.module('drawACat.common.filters') 5 | /** 6 | * Format a timestamp in terms of how long ago it was. Based on https://gist.github.com/rodyhaddad/5896883 7 | */ 8 | .filter("timeAgo", function () { 9 | //time: the time 10 | //local: compared to what time? default: now 11 | //raw: whether you want in a format of "5 minutes ago", or "5 minutes" 12 | return function (time, local, raw) { 13 | if (!time) { 14 | return "never"; 15 | } 16 | 17 | if (!local) { 18 | local = Date.now(); 19 | } 20 | 21 | if (angular.isDate(time)) { 22 | time = time.getTime(); 23 | } else if (typeof time === "string") { 24 | time = new Date(parseInt(time, 10)).getTime(); 25 | } 26 | 27 | if (angular.isDate(local)) { 28 | local = local.getTime(); 29 | }else if (typeof local === "string") { 30 | local = new Date(local).getTime(); 31 | } 32 | 33 | if (typeof time !== 'number' || typeof local !== 'number') { 34 | return time; 35 | } 36 | 37 | var 38 | offset = Math.abs((local - time) / 1000), 39 | span = [], 40 | MINUTE = 60, 41 | HOUR = 3600, 42 | DAY = 86400, 43 | WEEK = 604800, 44 | MONTH = 2629744, 45 | YEAR = 31556926, 46 | DECADE = 315569260; 47 | 48 | if (offset <= MINUTE) {span = [ '', raw ? 'now' : 'less than a minute' ];} 49 | else if (offset < (MINUTE * 60)) {span = [ Math.round(Math.abs(offset / MINUTE)), 'min' ];} 50 | else if (offset < (HOUR * 24)) {span = [ Math.round(Math.abs(offset / HOUR)), 'hr' ];} 51 | else if (offset < (DAY * 7)) {span = [ Math.round(Math.abs(offset / DAY)), 'day' ];} 52 | else if (offset < (WEEK * 52)) {span = [ Math.round(Math.abs(offset / WEEK)), 'week' ];} 53 | else if (offset < (YEAR * 10)) {span = [ Math.round(Math.abs(offset / YEAR)), 'year' ];} 54 | else if (offset < (DECADE * 100)) {span = [ Math.round(Math.abs(offset / DECADE)), 'decade' ];} 55 | else {span = [ '', 'a long time' ];} 56 | 57 | span[1] += (span[0] === 0 || span[0] > 1) ? 's' : ''; 58 | span = span.join(' '); 59 | 60 | if (raw === true) { 61 | return span; 62 | } else { 63 | return (time <= local) ? span + ' ago' : 'in ' + span; 64 | } 65 | }; 66 | }) 67 | ; -------------------------------------------------------------------------------- /src/app/home/directives/previewPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 29/04/14. 3 | */ 4 | 5 | 6 | 7 | angular.module('drawACat.home.previewPanel', []) 8 | 9 | .directive('dacPreviewPanel', function($rootScope, $location, $timeout, $filter, previewPanelService) { 10 | 11 | return { 12 | restrict: 'AE', 13 | templateUrl: 'home/directives/previewPanel.tpl.html', 14 | scope: { 15 | cat: '=' 16 | }, 17 | link: function(scope, element, attrs) { 18 | var previewPanel = angular.element(element[0].querySelector('.preview-panel')); 19 | var overlay = angular.element(document.getElementById('overlay')); 20 | 21 | scope.showInfo = ""; 22 | scope.isSelected = false; 23 | scope.urlFriendlyName = $filter('urlFriendlyName')(scope.cat.name); 24 | 25 | scope.clickHandler = function() { 26 | previewPanelService.currentlySelected = scope.cat.id; 27 | $rootScope.$broadcast('preview-click'); 28 | 29 | if (!scope.isSelected) { 30 | if (previewPanelService.switching) { 31 | $timeout(enable, 600); 32 | } else { 33 | enable(); 34 | } 35 | } else { 36 | disable(); 37 | } 38 | }; 39 | 40 | scope.$on('preview-click', function(event, closeAll) { 41 | if (scope.isSelected) { 42 | if (scope.cat.id !== previewPanelService.currentlySelected || closeAll){ 43 | previewPanelService.switching = true; 44 | disable(); 45 | } 46 | } 47 | }); 48 | 49 | scope.tagClick = function(tag) { 50 | $location.search('tags', tag); 51 | }; 52 | 53 | function enable() { 54 | previewPanelService.switching = false; 55 | scope.isSelected = true; 56 | scope.showInfo = "show-info"; 57 | previewPanel.addClass('selected'); 58 | overlay.addClass('displaying'); 59 | } 60 | 61 | function disable() { 62 | scope.isSelected = false; 63 | scope.showInfo = ""; 64 | previewPanel.removeClass('selected'); 65 | overlay.removeClass('displaying'); 66 | } 67 | } 68 | }; 69 | }) 70 | .service('previewPanelService', function() { 71 | this.switching = false; 72 | this.currentlySelected = null; 73 | }) 74 | ; 75 | -------------------------------------------------------------------------------- /src/common/services/datastore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 08/03/14. 3 | */ 4 | angular.module('drawACat.common.services') 5 | 6 | .factory('datastore', function($http, CONFIG) { 7 | 8 | var apiUrl = CONFIG.API_URL; 9 | 10 | /** 11 | * Calculate a value for the trendingScore, for viewing cats by trending. It's a function of the rating and how long ago 12 | * it was created, with older cats getting penalized. Inspired by the Reddit algorithm: http://amix.dk/blog/post/19588 13 | * @param rating 14 | * @param created 15 | * @returns {number} 16 | */ 17 | function getTrendingScore(rating, created) { 18 | var age = new Date().getTime() - created; 19 | var ageInDays = age / 86400000; 20 | 21 | return rating - ageInDays; 22 | } 23 | 24 | return { 25 | saveCat: function(catInfo) { 26 | return $http.post(apiUrl + 'cat/', { 27 | name: catInfo.name, 28 | description: catInfo.description, 29 | author: catInfo.author, 30 | isPublic: catInfo.isPublic, 31 | tags: catInfo.tags, 32 | thumbnail: catInfo.thumbnail, 33 | cat: catInfo.cat 34 | }); 35 | }, 36 | listCats: function(page, sort, tagsArray) { 37 | page = page || 1; 38 | sort = sort || "top"; 39 | var tags = tagsArray.join(' '); 40 | 41 | return $http.get(apiUrl + 'cat/', { 42 | params: { 43 | page: page, 44 | sort: sort, 45 | tags: tags 46 | }, 47 | cache: true, 48 | transformResponse: function(response) { 49 | 50 | var data = angular.fromJson(response); 51 | 52 | data.result = data.result.map(function(cat) { 53 | cat.thumbnail = CONFIG.THUMBNAILS_URL + cat.thumbnail; 54 | cat.created = cat.created + '000'; 55 | cat.rating = parseInt(cat.rating, 10); 56 | cat.trendingScore = getTrendingScore(cat.rating, cat.created); 57 | return cat; 58 | }); 59 | 60 | return data; 61 | } 62 | }); 63 | }, 64 | loadCat: function(id) { 65 | return $http.get(apiUrl + 'cat/' + id); 66 | }, 67 | getTags: function() { 68 | return $http.get(apiUrl + 'tags/', { cache: true }); 69 | }, 70 | rateCat: function(id) { 71 | return $http.get(apiUrl + 'cat/' + id + '/rated'); 72 | } 73 | }; 74 | } 75 | ); -------------------------------------------------------------------------------- /src/app/cat/directives/scrollTo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 27/04/14. 3 | * 4 | * Uses elements from https://github.com/arnaudbreton/angular-smoothscroll 5 | */ 6 | 7 | angular.module('drawACat.cat.directives') 8 | .directive('dacScrollTo', function($window, $timeout) { 9 | 10 | return { 11 | restrict: 'AE', 12 | templateUrl: '', 13 | link: function(scope, element, attrs) { 14 | 15 | var target = attrs.dacScrollTo; 16 | 17 | function currentYPosition() { 18 | if ($window.pageYOffset) { 19 | return $window.pageYOffset; 20 | } 21 | if ($window.document.documentElement && $window.document.documentElement.scrollTop) { 22 | return $window.document.documentElement.scrollTop; 23 | } 24 | if ($window.document.body.scrollTop) { 25 | return $window.document.body.scrollTop; 26 | } 27 | return 0; 28 | } 29 | 30 | function targetYPosition() { 31 | var elm, node, y; 32 | if (target === 'top') { 33 | return 0; 34 | } 35 | elm = document.getElementById(target); 36 | if (elm) { 37 | y = elm.offsetTop; 38 | node = elm; 39 | while (node.offsetParent && node.offsetParent !== document.body) { 40 | node = node.offsetParent; 41 | y += node.offsetTop; 42 | } 43 | return y; 44 | } else { 45 | return 0; 46 | } 47 | } 48 | 49 | function scrollToTarget(duration, callback) { 50 | duration = duration || 1000; // default to 1 second 51 | callback = callback || angular.noop; 52 | 53 | var startingPosition = currentYPosition(); 54 | var endingPosition = targetYPosition(); 55 | var distance = startingPosition - endingPosition; 56 | var timeLapsed = 0; 57 | var progress, yOffset; 58 | 59 | var animateScroll = function() { 60 | timeLapsed += 16; 61 | progress = ( timeLapsed / duration ); 62 | progress = ( progress > 1 ) ? 1 : progress; 63 | yOffset = startingPosition - (distance * Math.pow(progress, 3)); // a basic cubic easing function 64 | $window.scroll(0, yOffset); 65 | if (yOffset !== endingPosition) { 66 | $timeout(animateScroll, 16); 67 | } else { 68 | if (typeof callback === 'function') { 69 | callback(); 70 | } 71 | } 72 | }; 73 | animateScroll(); 74 | } 75 | 76 | element.on('click', function() { 77 | scrollToTarget(500); 78 | }); 79 | } 80 | }; 81 | }); -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Draw A Cat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% styles.forEach( function ( file ) { %> 26 | <% }); %> 27 | 28 | <% scripts.forEach( function ( file ) { %> 29 | <% }); %> 30 | 31 | 32 | 33 |
34 |

Meow! Loading, please wait!

35 |

Contact me at @DrawACat

36 | 37 |
38 |
39 | 40 | 51 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /api/staticPage.php: -------------------------------------------------------------------------------- 1 | get('/:id/:name', function($id, $name) use($app, $db) { 20 | $sql = "SELECT * FROM cats WHERE id = :id"; 21 | 22 | try { 23 | $stmt = $db->prepare($sql); 24 | $stmt->bindParam("id", $id); 25 | $stmt->execute(); 26 | $result = $stmt->fetch(PDO::FETCH_ASSOC); 27 | $dataDecoded = json_decode($result['data']); 28 | $result['data'] = $dataDecoded; 29 | 30 | makePageTemplate($result, $name); 31 | } catch(PDOException $e) { 32 | respondError($e->getMessage()); 33 | } 34 | }); 35 | 36 | $app->run(); 37 | 38 | function makePageTemplate($data, $name) { 39 | $pageUrl = "http://www.drawacat.net/cat/".$data["id"]."/".$name; 40 | $description = !empty($data["description"]) ? $data["description"] : "Come and play with ".$data["name"]."!"; 41 | $imgUrl = "http://www.drawacat.net/api/thumbnails/".$data["thumbnail"].".gif"; 42 | ?> 43 | 44 | 45 | 46 | 47 | 48 | <?php echo $data["name"]; ?> 49 | 50 | 51 | 52 | 53 | 54 | 55 | "> 56 | 57 | 58 | 59 | 60 | "> 61 | 62 | 63 | 64 | 65 | 66 | 67 |

68 |
69 |         
72 |     
73 | 83 | 84 | 85 | 6 | * @copyright 2011 Josh Lockhart 7 | * @link http://www.slimframework.com 8 | * @license http://www.slimframework.com/license 9 | * @version 2.4.2 10 | * @package Slim 11 | * 12 | * MIT LICENSE 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | namespace Slim\Http; 34 | 35 | class Cookies extends \Slim\Helper\Set 36 | { 37 | /** 38 | * Default cookie settings 39 | * @var array 40 | */ 41 | protected $defaults = array( 42 | 'value' => '', 43 | 'domain' => null, 44 | 'path' => null, 45 | 'expires' => null, 46 | 'secure' => false, 47 | 'httponly' => false 48 | ); 49 | 50 | /** 51 | * Set cookie 52 | * 53 | * The second argument may be a single scalar value, in which case 54 | * it will be merged with the default settings and considered the `value` 55 | * of the merged result. 56 | * 57 | * The second argument may also be an array containing any or all of 58 | * the keys shown in the default settings above. This array will be 59 | * merged with the defaults shown above. 60 | * 61 | * @param string $key Cookie name 62 | * @param mixed $value Cookie settings 63 | */ 64 | public function set($key, $value) 65 | { 66 | if (is_array($value)) { 67 | $cookieSettings = array_replace($this->defaults, $value); 68 | } else { 69 | $cookieSettings = array_replace($this->defaults, array('value' => $value)); 70 | } 71 | parent::set($key, $cookieSettings); 72 | } 73 | 74 | /** 75 | * Remove cookie 76 | * 77 | * Unlike \Slim\Helper\Set, this will actually *set* a cookie with 78 | * an expiration date in the past. This expiration date will force 79 | * the client-side cache to remove its cookie with the given name 80 | * and settings. 81 | * 82 | * @param string $key Cookie name 83 | * @param array $settings Optional cookie settings 84 | */ 85 | public function remove($key, $settings = array()) 86 | { 87 | $settings['value'] = ''; 88 | $settings['expires'] = time() - 86400; 89 | $this->set($key, array_replace($this->defaults, $settings)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/common/services/serializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 08/03/14. 3 | */ 4 | angular.module('drawACat.common.services') 5 | 6 | .factory('serializer', function(catFactory, primitives, behaviourFactory) { 7 | 8 | return { 9 | /** 10 | * Converts a Cat object into a JSON string containing all of its properties, so it can be 11 | * stored in a datastore. 12 | * @param cat 13 | * @returns {*} 14 | */ 15 | serializeCat: function(cat) { 16 | var serializable = {}; 17 | 18 | angular.forEach(cat.bodyParts, function(bodyPart, key) { 19 | serializable[key] = { 20 | part: {}, 21 | behaviour: {} 22 | }; 23 | if (bodyPart.part) { 24 | serializable[key].part.path = bodyPart.part.getPath(); 25 | serializable[key].part.name = bodyPart.part.getName(); 26 | if (bodyPart.part.getParent()) { 27 | serializable[key].part.parentName = bodyPart.part.getParent().getName(); 28 | } 29 | } 30 | if (bodyPart.behaviour) { 31 | serializable[key].behaviour = bodyPart.behaviour.toSerializable(); 32 | } 33 | }); 34 | 35 | return serializable; 36 | }, 37 | 38 | /** 39 | * Converts a JSON representation of the cat (as stored in the datastore) back into a Cat() object, 40 | * complete with Part() and Behaviour() objects for each bodyPart. 41 | * @param dataObject 42 | * @returns {*} 43 | */ 44 | unserializeCat: function(dataObject) { 45 | var cat = catFactory.newCat(); 46 | 47 | angular.forEach(dataObject, function(bodyPart, key) { 48 | if (bodyPart.part) { 49 | var newPart = primitives.Part(); 50 | 51 | if (bodyPart.part.path) { 52 | newPart.createFromPath(bodyPart.part.name, bodyPart.part.path); 53 | } 54 | cat.bodyParts[key].part = newPart; 55 | } 56 | if (bodyPart.behaviour) { 57 | var newBehaviour = behaviourFactory.newBehaviour(); 58 | newBehaviour.sensitivity = bodyPart.behaviour.sensitivity; 59 | newBehaviour.range = bodyPart.behaviour.range; 60 | newBehaviour.visible = bodyPart.behaviour.visible; 61 | cat.bodyParts[key].behaviour = newBehaviour; 62 | } 63 | }); 64 | 65 | // now we need to loop through the bodyParts once more to resolve the parent/child relationships 66 | angular.forEach(dataObject, function(bodyPart, key) { 67 | if (bodyPart.part) { 68 | if(bodyPart.part.parentName) { 69 | var parentName = bodyPart.part.parentName; 70 | cat.bodyParts[key].part.setParent(cat.bodyParts[parentName].part); 71 | } 72 | } 73 | }); 74 | 75 | return cat; 76 | } 77 | }; 78 | } 79 | ); -------------------------------------------------------------------------------- /src/app/cat/cat.less: -------------------------------------------------------------------------------- 1 | .header { 2 | a:link, a:visited { 3 | color: #999; 4 | } 5 | a:hover, a:active { 6 | color: #666; 7 | text-decoration: none; 8 | } 9 | .embed-site-link { 10 | font-size: 20px; 11 | font-family: @comic-sans; 12 | a:link, a:visited { 13 | color: @highlight-colour; 14 | } 15 | a:hover, a:active { 16 | color: lighten(@highlight-colour, 5%); 17 | } 18 | } 19 | 20 | .embed-panel { 21 | padding: 5px; 22 | } 23 | .control-panel { 24 | position: relative; 25 | z-index: 300; 26 | background-color: #f7f7f7; 27 | border-bottom: 1px solid #eee; 28 | padding-top: 5px; 29 | i { 30 | font-size: 20px; 31 | color: #999; 32 | @media (min-width: @screen-sm-min) { 33 | font-size: 24px; 34 | } 35 | } 36 | button:focus { 37 | outline: none; 38 | color: #555; 39 | } 40 | a.selected i, button.selected i { 41 | color: @highlight-colour; 42 | } 43 | } 44 | .button-bar { 45 | text-align: right; 46 | list-style-type: none; 47 | padding: 0; 48 | li { 49 | display: inline-block; 50 | padding: 0px 3px; 51 | } 52 | } 53 | .comments-link { 54 | width: 40px; 55 | text-align: center; 56 | &.floating { 57 | position: fixed; 58 | z-index: 500; 59 | top: -15px; 60 | padding-top: 20px; 61 | background-color: #f7f7f7; 62 | border-radius: 5px; 63 | -webkit-box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 64 | -moz-box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 65 | box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 66 | } 67 | } 68 | } 69 | 70 | .settings-panel, .embed-panel { 71 | padding: 10px; 72 | z-index: 210; 73 | margin-top: 55px; 74 | background-color: #fff; 75 | h3 { 76 | margin-top: 0px; 77 | } 78 | .render-quality { 79 | padding-top: 45px; 80 | } 81 | } 82 | 83 | .info-panel-container { 84 | position: absolute; 85 | width: 100%; 86 | left: 0; 87 | border-radius: 5px; 88 | background-color: #f7f7f7; 89 | min-width: 300px; 90 | top: -5px; 91 | z-index: 200; 92 | padding: 10px; 93 | -webkit-box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 94 | -moz-box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 95 | box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 96 | transition: top 0.5s; 97 | h1 { 98 | font-family: @comic-sans; 99 | margin: 2px 0px; 100 | } 101 | p { 102 | font-family: @comic-sans; 103 | } 104 | .extended-info { 105 | padding: 5px 0px; 106 | font-size: 18px; 107 | } 108 | ul.share-buttons { 109 | list-style-type: none; 110 | margin: 0; 111 | padding: 0; 112 | li { 113 | display: table-cell; 114 | padding-right: 5px; 115 | max-height: 20px; 116 | overflow: hidden; 117 | } 118 | } 119 | twitter-button iframe { 120 | margin-bottom: -5px; 121 | } 122 | .rate-cat-button { 123 | border: none; 124 | background: none; 125 | } 126 | button.toggle-info-panel { 127 | margin-top: -20px; 128 | border: none; 129 | background: none; 130 | &:focus { 131 | outline: none; 132 | color: #555; 133 | } 134 | } 135 | } 136 | 137 | #stage-container { 138 | height: 100vh; 139 | left: 0; 140 | top: 0; 141 | width: 100vw; 142 | } 143 | canvas { 144 | background-color: #f5f5f5; 145 | } 146 | #stage { 147 | position: absolute; 148 | top: 0; 149 | left: 0; 150 | z-index: 100; 151 | } -------------------------------------------------------------------------------- /api/Slim/Middleware.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2011 Josh Lockhart 7 | * @link http://www.slimframework.com 8 | * @license http://www.slimframework.com/license 9 | * @version 2.4.2 10 | * @package Slim 11 | * 12 | * MIT LICENSE 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | namespace Slim; 34 | 35 | /** 36 | * Middleware 37 | * 38 | * @package Slim 39 | * @author Josh Lockhart 40 | * @since 1.6.0 41 | */ 42 | abstract class Middleware 43 | { 44 | /** 45 | * @var \Slim\Slim Reference to the primary application instance 46 | */ 47 | protected $app; 48 | 49 | /** 50 | * @var mixed Reference to the next downstream middleware 51 | */ 52 | protected $next; 53 | 54 | /** 55 | * Set application 56 | * 57 | * This method injects the primary Slim application instance into 58 | * this middleware. 59 | * 60 | * @param \Slim\Slim $application 61 | */ 62 | final public function setApplication($application) 63 | { 64 | $this->app = $application; 65 | } 66 | 67 | /** 68 | * Get application 69 | * 70 | * This method retrieves the application previously injected 71 | * into this middleware. 72 | * 73 | * @return \Slim\Slim 74 | */ 75 | final public function getApplication() 76 | { 77 | return $this->app; 78 | } 79 | 80 | /** 81 | * Set next middleware 82 | * 83 | * This method injects the next downstream middleware into 84 | * this middleware so that it may optionally be called 85 | * when appropriate. 86 | * 87 | * @param \Slim|\Slim\Middleware 88 | */ 89 | final public function setNextMiddleware($nextMiddleware) 90 | { 91 | $this->next = $nextMiddleware; 92 | } 93 | 94 | /** 95 | * Get next middleware 96 | * 97 | * This method retrieves the next downstream middleware 98 | * previously injected into this middleware. 99 | * 100 | * @return \Slim\Slim|\Slim\Middleware 101 | */ 102 | final public function getNextMiddleware() 103 | { 104 | return $this->next; 105 | } 106 | 107 | /** 108 | * Call 109 | * 110 | * Perform actions specific to this middleware and optionally 111 | * call the next downstream middleware. 112 | */ 113 | abstract public function call(); 114 | } 115 | -------------------------------------------------------------------------------- /src/app/home/home.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module( 'drawACat.home', [ 3 | 'drawACat.home.filters', 4 | 'drawACat.home.tagSelector', 5 | 'drawACat.home.previewPanel', 6 | 'angularUtils.directives.dirPagination', 7 | 'ui.router' 8 | ]) 9 | 10 | 11 | .config(function config( $stateProvider ) { 12 | $stateProvider.state( 'home', { 13 | url: '/home?page&sort&tags', 14 | views: { 15 | "main": { 16 | controller: 'HomeController', 17 | templateUrl: 'home/home.tpl.html' 18 | } 19 | }, 20 | data:{ pageTitle: 'Draw A Cat!' } 21 | }); 22 | }) 23 | 24 | /** 25 | * And of course we define a controller for our route. 26 | */ 27 | .controller( 'HomeController', function HomeController( $scope, $location, $state, $stateParams, datastore ) { 28 | 29 | // emit an event to update the page metadata 30 | var metaData = { 31 | pageTitle: 'Draw A Cat!', 32 | title: 'Draw A Cat!', 33 | url: $location.absUrl(), 34 | image: 'assets/cat-big.gif' 35 | }; 36 | $scope.$emit('metadata:updated', metaData); 37 | 38 | $scope.sort = $stateParams.sort || "top"; 39 | $scope.currentPage = $stateParams.page || 1; 40 | $scope.tagsArray = $stateParams.tags ? $stateParams.tags.split(' ') : []; 41 | 42 | $scope.tags = []; 43 | datastore.getTags().then(function (data) { 44 | $scope.tags = data.data; 45 | }); 46 | 47 | $scope.searchInput = ""; 48 | 49 | 50 | $scope.$watchCollection('tagsArray', function(tagsArray) { 51 | $scope.setTags(tagsArray); 52 | }); 53 | 54 | $scope.tagLinkClicked = function(tag) { 55 | if ($scope.tagsArray.indexOf(tag) === -1) { 56 | $scope.tagsArray.push(tag); 57 | } 58 | }; 59 | 60 | $scope.pageChanged = function(pageNumber) { 61 | updateQueryString({ page: pageNumber }); 62 | }; 63 | 64 | $scope.sortBy = function(sort) { 65 | updateQueryString({ sort: sort }); 66 | }; 67 | 68 | $scope.setTags = function(tagsArray) { 69 | updateQueryString({tags: tagsArray.join(' ')}); 70 | }; 71 | 72 | $scope.closeOverlay = function() { 73 | $scope.$broadcast('preview-click', true); 74 | }; 75 | 76 | getPage($scope.currentPage, $scope.sort, $scope.tagsArray); 77 | 78 | function updateQueryString(params) { 79 | var options = {}; 80 | options.page = params.page || $scope.currentPage || null; 81 | options.sort = params.sort || $scope.sort || null; 82 | options.tags = params.tags || $scope.tagsArray.join(' ') || null; 83 | 84 | $state.transitionTo('home', options); 85 | } 86 | 87 | function getPage(pageNumber, sort, tagsArray) { 88 | datastore.listCats(pageNumber, sort, tagsArray).success(function(data) { 89 | $scope.cats = data.result; 90 | $scope.totalItems = data.totalCats; 91 | $scope.pageLower = ($scope.currentPage - 1) * 15 + 1; 92 | $scope.pageUpper = Math.min($scope.pageLower + 14, $scope.totalItems); 93 | }); 94 | } 95 | }) 96 | 97 | .filter('startFrom', function() { 98 | return function(input, start) { 99 | if (typeof input !== 'undefined' && 0 < input.length) { 100 | start = +start; //parse to int 101 | return input.slice(start); 102 | } 103 | }; 104 | }); -------------------------------------------------------------------------------- /src/common/services/noiseFactory.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 13/03/14. 3 | */ 4 | 5 | describe('noiseFactory service', function() { 6 | var generator; 7 | var generateLotsOfNoise; 8 | 9 | beforeEach(module('drawACat.common.services')); 10 | 11 | beforeEach(inject(function(_noiseFactory_) { 12 | generator = _noiseFactory_.newGenerator(); 13 | 14 | // generate 10,000 values as a large enough sample to check they fall within the expected bounds. 15 | generateLotsOfNoise = function(generator) { 16 | var noiseFactoryValues = []; 17 | for (var i = 0; i < 10000; i ++ ) { 18 | noiseFactoryValues.push(generator.getVal(i)); 19 | } 20 | return noiseFactoryValues; 21 | }; 22 | 23 | jasmine.addMatchers({ 24 | /** 25 | * Matcher to check that all values in the array fall within the specified range (inclusive of the bounds) 26 | * @returns {{compare: compare}} 27 | */ 28 | toAllBeWithinRange: function() { 29 | return { 30 | compare: function(valuesArray, lowerBound, upperBound) { 31 | var pass = true; 32 | var failingValue = 0; 33 | for (var i = 0; i < valuesArray.length; i ++) { 34 | if (valuesArray[i] < lowerBound || upperBound < valuesArray[i]) { 35 | pass = false; 36 | failingValue = valuesArray[i]; 37 | break; 38 | } 39 | } 40 | 41 | var result = { 42 | pass: pass 43 | }; 44 | if (!result.pass) { 45 | result.message = failingValue + ' is not between ' + lowerBound + ' and ' + upperBound; 46 | } 47 | return result; 48 | } 49 | }; 50 | }, 51 | /** 52 | * Matcher to check if at least one of the values in the array lies in the specified range 53 | * @returns {{compare: compare}} 54 | */ 55 | toIncludeRange: function() { 56 | return { 57 | compare: function(valuesArray, lowerBound, upperBound) { 58 | var pass = false; 59 | 60 | for (var i = 0; i < valuesArray.length; i ++) { 61 | if (valuesArray[i] > lowerBound && upperBound > valuesArray[i]) { 62 | pass = true; 63 | break; 64 | } 65 | } 66 | 67 | var result = { 68 | pass: pass 69 | }; 70 | if (!result.pass) { 71 | result.message = 'Array has no values between ' + lowerBound + ' and ' + upperBound; 72 | } 73 | return result; 74 | } 75 | }; 76 | } 77 | }); 78 | })); 79 | 80 | it('should generate values between 0 and 1 on default settings', function() { 81 | var noiseFactoryValues = generateLotsOfNoise(generator); 82 | expect(noiseFactoryValues).toAllBeWithinRange(0, 1); 83 | }); 84 | 85 | it('should amplify correctly', function() { 86 | generator.setAmplitude(2); 87 | var noiseFactoryValues = generateLotsOfNoise(generator); 88 | 89 | expect(noiseFactoryValues).toAllBeWithinRange(0, 2); 90 | expect(noiseFactoryValues).toIncludeRange(1, 2); 91 | }); 92 | }); -------------------------------------------------------------------------------- /src/common/directives/dirDisqus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A directive to embed a Disqus comments widget on your AngularJS page. 3 | * 4 | * For documentation, see the README.md file in this directory 5 | * 6 | * Created by Michael on 22/01/14. 7 | * Copyright Michael Bromley 2014 8 | * Available under the MIT license. 9 | */ 10 | angular.module('drawACat.common.directives') 11 | 12 | .directive('dirDisqus', function($window) { 13 | return { 14 | restrict: 'E', 15 | scope: { 16 | disqus_shortname: '@disqusShortname', 17 | disqus_identifier: '@disqusIdentifier', 18 | disqus_title: '@disqusTitle', 19 | disqus_url: '@disqusUrl', 20 | disqus_category_id: '@disqusCategoryId', 21 | disqus_disable_mobile: '@disqusDisableMobile', 22 | readyToBind: "@" 23 | }, 24 | template: '
comments powered by Disqus', 25 | link: function(scope) { 26 | 27 | // ensure that the disqus_identifier and disqus_url are both set, otherwise we will run in to identifier conflicts when using URLs with "#" in them 28 | // see http://help.disqus.com/customer/portal/articles/662547-why-are-the-same-comments-showing-up-on-multiple-pages- 29 | if (!scope.disqus_identifier || !scope.disqus_url) { 30 | throw "Please ensure that the `disqus-identifier` and `disqus-url` attributes are both set."; 31 | } 32 | 33 | scope.$watch("readyToBind", function(isReady) { 34 | 35 | // If the directive has been called without the 'ready-to-bind' attribute, we 36 | // set the default to "true" so that Disqus will be loaded straight away. 37 | if ( !angular.isDefined( isReady ) ) { 38 | isReady = "true"; 39 | } 40 | if (scope.$eval(isReady)) { 41 | // put the config variables into separate global vars so that the Disqus script can see them 42 | $window.disqus_shortname = scope.disqus_shortname; 43 | $window.disqus_identifier = scope.disqus_identifier; 44 | $window.disqus_title = scope.disqus_title; 45 | $window.disqus_url = scope.disqus_url; 46 | $window.disqus_category_id = scope.disqus_category_id; 47 | $window.disqus_disable_mobile = scope.disqus_disable_mobile; 48 | 49 | // get the remote Disqus script and insert it into the DOM, but only if it not already loaded (as that will cause warnings) 50 | if (!$window.DISQUS) { 51 | var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; 52 | dsq.src = '//' + scope.disqus_shortname + '.disqus.com/embed.js'; 53 | (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); 54 | } else { 55 | $window.DISQUS.reset({ 56 | reload: true, 57 | config: function () { 58 | this.page.identifier = scope.disqus_identifier; 59 | this.page.url = scope.disqus_url; 60 | this.page.title = scope.disqus_title; 61 | } 62 | }); 63 | } 64 | } 65 | }); 66 | } 67 | }; 68 | }); 69 | -------------------------------------------------------------------------------- /api/Slim/Middleware/MethodOverride.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2011 Josh Lockhart 7 | * @link http://www.slimframework.com 8 | * @license http://www.slimframework.com/license 9 | * @version 2.4.2 10 | * @package Slim 11 | * 12 | * MIT LICENSE 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | namespace Slim\Middleware; 34 | 35 | /** 36 | * HTTP Method Override 37 | * 38 | * This is middleware for a Slim application that allows traditional 39 | * desktop browsers to submit pseudo PUT and DELETE requests by relying 40 | * on a pre-determined request parameter. Without this middleware, 41 | * desktop browsers are only able to submit GET and POST requests. 42 | * 43 | * This middleware is included automatically! 44 | * 45 | * @package Slim 46 | * @author Josh Lockhart 47 | * @since 1.6.0 48 | */ 49 | class MethodOverride extends \Slim\Middleware 50 | { 51 | /** 52 | * @var array 53 | */ 54 | protected $settings; 55 | 56 | /** 57 | * Constructor 58 | * @param array $settings 59 | */ 60 | public function __construct($settings = array()) 61 | { 62 | $this->settings = array_merge(array('key' => '_METHOD'), $settings); 63 | } 64 | 65 | /** 66 | * Call 67 | * 68 | * Implements Slim middleware interface. This method is invoked and passed 69 | * an array of environment variables. This middleware inspects the environment 70 | * variables for the HTTP method override parameter; if found, this middleware 71 | * modifies the environment settings so downstream middleware and/or the Slim 72 | * application will treat the request with the desired HTTP method. 73 | * 74 | * @return array[status, header, body] 75 | */ 76 | public function call() 77 | { 78 | $env = $this->app->environment(); 79 | if (isset($env['HTTP_X_HTTP_METHOD_OVERRIDE'])) { 80 | // Header commonly used by Backbone.js and others 81 | $env['slim.method_override.original_method'] = $env['REQUEST_METHOD']; 82 | $env['REQUEST_METHOD'] = strtoupper($env['HTTP_X_HTTP_METHOD_OVERRIDE']); 83 | } elseif (isset($env['REQUEST_METHOD']) && $env['REQUEST_METHOD'] === 'POST') { 84 | // HTML Form Override 85 | $req = new \Slim\Http\Request($env); 86 | $method = $req->post($this->settings['key']); 87 | if ($method) { 88 | $env['slim.method_override.original_method'] = $env['REQUEST_METHOD']; 89 | $env['REQUEST_METHOD'] = strtoupper($method); 90 | } 91 | } 92 | $this->next->call(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /api/Slim/Http/Headers.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2011 Josh Lockhart 7 | * @link http://www.slimframework.com 8 | * @license http://www.slimframework.com/license 9 | * @version 2.4.2 10 | * @package Slim 11 | * 12 | * MIT LICENSE 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | namespace Slim\Http; 34 | 35 | /** 36 | * HTTP Headers 37 | * 38 | * @package Slim 39 | * @author Josh Lockhart 40 | * @since 1.6.0 41 | */ 42 | class Headers extends \Slim\Helper\Set 43 | { 44 | /******************************************************************************** 45 | * Static interface 46 | *******************************************************************************/ 47 | 48 | /** 49 | * Special-case HTTP headers that are otherwise unidentifiable as HTTP headers. 50 | * Typically, HTTP headers in the $_SERVER array will be prefixed with 51 | * `HTTP_` or `X_`. These are not so we list them here for later reference. 52 | * 53 | * @var array 54 | */ 55 | protected static $special = array( 56 | 'CONTENT_TYPE', 57 | 'CONTENT_LENGTH', 58 | 'PHP_AUTH_USER', 59 | 'PHP_AUTH_PW', 60 | 'PHP_AUTH_DIGEST', 61 | 'AUTH_TYPE' 62 | ); 63 | 64 | /** 65 | * Extract HTTP headers from an array of data (e.g. $_SERVER) 66 | * @param array $data 67 | * @return array 68 | */ 69 | public static function extract($data) 70 | { 71 | $results = array(); 72 | foreach ($data as $key => $value) { 73 | $key = strtoupper($key); 74 | if (strpos($key, 'X_') === 0 || strpos($key, 'HTTP_') === 0 || in_array($key, static::$special)) { 75 | if ($key === 'HTTP_CONTENT_LENGTH') { 76 | continue; 77 | } 78 | $results[$key] = $value; 79 | } 80 | } 81 | 82 | return $results; 83 | } 84 | 85 | /******************************************************************************** 86 | * Instance interface 87 | *******************************************************************************/ 88 | 89 | /** 90 | * Transform header name into canonical form 91 | * @param string $key 92 | * @return string 93 | */ 94 | protected function normalizeKey($key) 95 | { 96 | $key = strtolower($key); 97 | $key = str_replace(array('-', '_'), ' ', $key); 98 | $key = preg_replace('#^http #', '', $key); 99 | $key = ucwords($key); 100 | $key = str_replace(' ', '-', $key); 101 | 102 | return $key; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/app/cat/services/catSimplifier.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 06/04/14. 3 | */ 4 | 5 | angular.module('drawACat.cat.services') 6 | 7 | /** 8 | * This service takes a cat object and simplifies its path by removing points. How many points 9 | * get removed depends on the shape of the lines and also the quality setting. Quality ranges between 10 | * 0 and 1, with 1 being best quality. 11 | */ 12 | .service('catSimplifier', function(catFactory, primitives) { 13 | 14 | this.simplifyCat = function(cat, quality) { 15 | var simplifiedCat; 16 | simplifiedCat = catFactory.newCat(); 17 | 18 | angular.forEach(cat.bodyParts, function(bodyPart, partName) { 19 | var partPath = bodyPart.part.getPath(); 20 | var simplifiedPath = simplifyPath(partPath, quality); 21 | 22 | var newPart = primitives.Part(); 23 | newPart.createFromPath(partName, simplifiedPath); 24 | simplifiedCat.bodyParts[partName].part = newPart; 25 | 26 | simplifiedCat.bodyParts[partName].behaviour = cat.bodyParts[partName].behaviour; 27 | }); 28 | 29 | // now we need to loop through the bodyParts once more to resolve the parent/child relationships 30 | angular.forEach(simplifiedCat.bodyParts, function(bodyPart, partName) { 31 | var partShouldHaveParent = cat.bodyParts[partName].part.getParent(); 32 | if(partShouldHaveParent) { 33 | var parentName = cat.bodyParts[partName].part.getParent().getName(); 34 | var partParent = simplifiedCat.bodyParts[parentName].part; 35 | simplifiedCat.bodyParts[partName].part.setParent(partParent); 36 | } 37 | }); 38 | 39 | return simplifiedCat; 40 | }; 41 | 42 | var simplifyPath = function(path, quality) { 43 | return path.map(function(line) { 44 | return removeRedundantPoints(line, quality); 45 | }); 46 | }; 47 | 48 | var removeRedundantPoints = function(line, quality) { 49 | var tolerance = 1 - quality + 0.01; // angle in radians 50 | var pointsToRemove = []; 51 | var keepAngle1 = false; 52 | var angle1; 53 | var angle2; 54 | var dX, dY; 55 | // start with the second point (the first point in the line should never be removed) 56 | for (var i = 1; i < line.length - 2; i ++) { // -2 in order to always preserve the last point of the line 57 | // calculate the angle between x-axis and the line formed from line[0] - line[1] 58 | if (!keepAngle1) { 59 | dX= line[i][0] - line[i - 1][0]; 60 | dY= line[i][1] - line[i - 1][1]; 61 | angle1 = Math.atan2(dY, dX); 62 | } 63 | // move to the next point and get the angle for line[1] - line[2] 64 | dX= line[i + 1][0] - line[i][0]; 65 | dY= line[i + 1][1] - line[i][1]; 66 | angle2 = Math.atan2(dY, dX); 67 | // compare angle 2 to angle 1. 68 | if (Math.abs(angle2 - angle1) < tolerance) { 69 | // if the difference between the two angles is less than a specified delta, remove point line[1] and keep angle 1 70 | pointsToRemove.push(i); 71 | keepAngle1 = true; 72 | } else { 73 | // if it is more, angle 2 becomes angle 1 and we move to the next point to calculate a new angle 2 74 | keepAngle1 = false; 75 | } 76 | } 77 | 78 | return line.filter(function(point, i) { 79 | return pointsToRemove.indexOf(i) === -1; // return true if index not in the pointsToRemove array. 80 | }); 81 | }; 82 | }) 83 | ; -------------------------------------------------------------------------------- /src/common/services/datastore.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 13/03/14. 3 | */ 4 | 5 | xdescribe('datastore service', function() { 6 | 7 | var datastore; 8 | var $httpBackend; 9 | var mockApiUrl = 'http://www.mydomain.com/api/'; 10 | var mockThumbnailsUrl = 'http://www.mydomain.com/api/thumbnails'; 11 | 12 | beforeEach( module( 'drawACat' ) ); 13 | beforeEach( module( 'drawACat.common.services' ) ); 14 | 15 | beforeEach( function() { 16 | var mockCONFIG = { 17 | API_URL: mockApiUrl, 18 | THUMBNAILS_URL: mockThumbnailsUrl 19 | }; 20 | module(function ($provide) { 21 | $provide.value('CONFIG', mockCONFIG); 22 | }); 23 | 24 | inject( function( _$httpBackend_, _datastore_ ) { 25 | datastore = _datastore_; 26 | $httpBackend = _$httpBackend_; 27 | }); 28 | }); 29 | 30 | afterEach(inject(function($rootScope) { 31 | $rootScope.$apply(); 32 | $httpBackend.flush(); 33 | })); 34 | 35 | it('should make the correct api call on load', function() { 36 | $httpBackend.expectGET(mockApiUrl + 'cat/123').respond(200); 37 | datastore.loadCat(123); 38 | }); 39 | 40 | describe('listCats() method', function() { 41 | 42 | var response; 43 | 44 | beforeEach(function() { 45 | var mockResponse = [ 46 | { 47 | name: 'test cat', 48 | created: '123456789', 49 | thumbnail: 'thumb.png' 50 | } 51 | ]; 52 | $httpBackend.whenGET(mockApiUrl + 'cat/').respond(200, angular.toJson(mockResponse)); 53 | datastore.listCats().then(function(data) { 54 | response = data; 55 | }); 56 | }); 57 | 58 | it('should make correct api call on list', function() { 59 | $httpBackend.expectGET(mockApiUrl + 'cat/'); 60 | }); 61 | 62 | it('should transform the response timestamp with an extra 3 zeros', function() { 63 | expect(response.data[0].created).toEqual('123456789000'); 64 | }); 65 | 66 | it('should add the image path to the thumbnail', function() { 67 | expect(response.data[0].thumbnail).toEqual(mockThumbnailsUrl + 'thumb.png'); 68 | }); 69 | }); 70 | 71 | 72 | 73 | describe('saveCat() method', function() { 74 | 75 | var postData; 76 | 77 | beforeEach(function() { 78 | postData = { 79 | name: 'bobby', 80 | description: 'a testing cat', 81 | author: 'Jim Test', 82 | isPublic: true, 83 | tags: 'tag1 tag2', 84 | cat: { catObject: 'mocked!' }, 85 | thumbnail: 'wdawdawdawdawdawd' 86 | }; 87 | }); 88 | 89 | it('should make correct api call on save', function() { 90 | $httpBackend.expectPOST(mockApiUrl + 'cat/', postData).respond(201); 91 | datastore.saveCat(postData); 92 | }); 93 | }); 94 | 95 | describe('getTags() method', function() { 96 | 97 | var $httpCache; 98 | var $cacheFactory; 99 | 100 | beforeEach(inject(function(_$cacheFactory_) { 101 | //$httpCache = _$cacheFactory_.get('$http'); 102 | $cacheFactory = _$cacheFactory_; 103 | })); 104 | 105 | it('should make the correct api call', function() { 106 | $httpBackend.expectGET(mockApiUrl + 'tags/').respond(200, ['tag1', 'tag2']); 107 | datastore.getTags(); 108 | }); 109 | 110 | it('should not call the api the second time it is invoked', function() { 111 | $httpBackend.expectGET(mockApiUrl + 'tags/').respond(200, ['tag1', 'tag2']); 112 | datastore.getTags(); 113 | datastore.getTags(); 114 | }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /src/app/cat/cat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 10/03/14. 3 | */ 4 | 5 | angular.module( 'drawACat.cat', [ 6 | 'drawACat.common.services', 7 | 'ui.router', 8 | 'drawACat.cat.directives', 9 | 'drawACat.cat.services', 10 | 'ezfb', 11 | 'ngAnimate' 12 | ]) 13 | 14 | 15 | .config(function config( $stateProvider ) { 16 | $stateProvider.state( 'cat', { 17 | url: '/cat/{id:[0-9]+}/{name}', 18 | views: { 19 | "main": { 20 | controller: 'CatController', 21 | templateUrl: 'cat/cat.tpl.html', 22 | resolve: { 23 | catPromise: ['$stateParams', 'datastore', function($stateParams, datastore) { 24 | return datastore.loadCat($stateParams.id); 25 | }] 26 | } 27 | } 28 | }, 29 | data: { pageTitle: 'Cat' } 30 | }); 31 | }) 32 | 33 | .config(function (ezfbProvider) { 34 | ezfbProvider.setInitParams({ 35 | appId: '459619084171176' 36 | }); 37 | }) 38 | 39 | /** 40 | * And of course we define a controller for our route. 41 | */ 42 | .controller( 'CatController', function CatController( $scope, $location, CONFIG, serializer, catPromise, Ball, emotion, ratingService, userOptions, audioPlayer ) { 43 | // TODO: refactor all those dependencies above into helper services 44 | $scope.embed = $location.search().embed; 45 | $scope.pageUrl = $location.absUrl(); 46 | $scope.catData = catPromise.data; 47 | $scope.cat = serializer.unserializeCat(catPromise.data.data); 48 | $scope.cat.emotion = emotion; 49 | $scope.cat.emotion.start(); 50 | $scope.ball = [new Ball()]; 51 | $scope.renderQuality = userOptions.getRenderQuality(); 52 | $scope.help = { 53 | show: !userOptions.getHelpHasBeenSeen() 54 | }; 55 | 56 | // emit an event to update the page metadata 57 | var metaData = { 58 | pageTitle:$scope.catData.name + ': Draw A Cat!', 59 | title: $scope.catData.name, 60 | url: $location.absUrl(), 61 | image: CONFIG.THUMBNAILS_URL + $scope.catData.thumbnail 62 | }; 63 | $scope.$emit('metadata:updated', metaData); 64 | 65 | $scope.catHasBeenRated = ratingService.hasUserRatedThisCat($scope.catData.id); 66 | $scope.rateCat = function() { 67 | if (!$scope.catHasBeenRated) { 68 | ratingService.setCatAsRated($scope.catData.id); 69 | $scope.catHasBeenRated = true; 70 | var newRating; 71 | newRating = parseInt($scope.catData.rating, 10) + 1; 72 | $scope.catData.rating = newRating; 73 | } 74 | }; 75 | 76 | $scope.setRenderQuality = function(value) { 77 | userOptions.setRenderQuality(parseInt(value, 10)); 78 | }; 79 | 80 | $scope.addBall = function(e) { 81 | $scope.ball.push(new Ball(e.clientX, e.clientY)); 82 | }; 83 | 84 | $scope.audioSetting = userOptions.getAudioSetting(); 85 | $scope.toggleAudio = function() { 86 | if ($scope.audioSetting === true) { 87 | audioPlayer.setAudio(false); 88 | $scope.audioSetting = false; 89 | userOptions.setAudioSetting(false); 90 | } else { 91 | audioPlayer.setAudio(true); 92 | $scope.audioSetting = true; 93 | userOptions.setAudioSetting(true); 94 | } 95 | }; 96 | 97 | $scope.dismissHelp = function() { 98 | $scope.help.show = false; 99 | userOptions.setHelpHasBeenSeen(); 100 | }; 101 | 102 | $scope.$on('$destroy', function() { 103 | emotion.reset(); 104 | }); 105 | }) 106 | 107 | ; -------------------------------------------------------------------------------- /src/app/draw/draw.less: -------------------------------------------------------------------------------- 1 | .control-panel { 2 | h1 { 3 | font-family: @comic-sans; 4 | margin-top: 5px; 5 | } 6 | } 7 | 8 | .draw-container { 9 | padding-top: 40px; 10 | } 11 | .canvas-container { 12 | position: relative; 13 | width: 540px; 14 | height: 540px; 15 | } 16 | #canvas { 17 | position: absolute; 18 | top: 0px; 19 | left: 0px; 20 | z-index: 10; 21 | background-color: rgba(255,255,255,0.5); 22 | cursor: crosshair; 23 | } 24 | #canvas-frame { 25 | position: absolute; 26 | top: 20px; 27 | left: 20px; 28 | width: 500px; 29 | height: 500px; 30 | border: 1px solid #333; 31 | &.filled { 32 | background-color: #f5f5f5; 33 | } 34 | z-index: 9; 35 | } 36 | #draw-guide { 37 | position: absolute; 38 | top: 20px; 39 | left: 20px; 40 | width: 500px; 41 | height: 500px; 42 | z-index: 8; 43 | .lighten { 44 | opacity: 0.7; 45 | } 46 | } 47 | .undo-button { 48 | position: absolute; 49 | right: 22px; 50 | top: 22px; 51 | z-index: 15; 52 | } 53 | .draw-instructions { 54 | margin: auto; 55 | max-width: 400px; 56 | position: relative; 57 | background: #fff; 58 | border: 4px solid @highlight-colour; 59 | border-radius: 5px; 60 | z-index: 20; 61 | margin-bottom: 20px; 62 | h2 { 63 | font-family: @comic-sans; 64 | } 65 | .steps-list ul { 66 | border-top: 1px solid lighten(@highlight-colour, 20%); 67 | background: lighten(@highlight-colour, 50%); 68 | padding: 10px; 69 | margin-bottom: 0; 70 | margin-top: 10px; 71 | font-size: 0.85em; 72 | list-style-type: none; 73 | color: #999; 74 | li { 75 | display: inline-block; 76 | margin: 3px 3px; 77 | padding: 2px 3px; 78 | background-color: #efefef; 79 | border-radius: 3px; 80 | border: 1px solid #fff; 81 | } 82 | .selected { 83 | color: #333; 84 | background-color: #f5f5f5; 85 | border-color: #395c34; 86 | } 87 | .done { 88 | color: #393; 89 | background-color: #c2e8b7; 90 | } 91 | } 92 | 93 | .instruction-container { 94 | min-height: 100px; 95 | padding: 10px; 96 | h2 { 97 | margin-top: 2px; 98 | margin-bottom: 2px; 99 | } 100 | .instruction { 101 | color: #666; 102 | min-height: 60px; 103 | } 104 | } 105 | .checkbox { 106 | padding-left: 30px; 107 | } 108 | .nav-buttons { 109 | padding: 10px 110 | } 111 | } 112 | 113 | @media (max-width: @screen-xs-max) { 114 | 115 | .draw-instructions:after, .draw-instructions:before { 116 | bottom: 100%; 117 | left: 50%; 118 | border: solid transparent; 119 | content: " "; 120 | height: 0; 121 | width: 0; 122 | position: absolute; 123 | pointer-events: none; 124 | } 125 | 126 | .draw-instructions:after { 127 | border-color: rgba(255, 255, 255, 0); 128 | border-bottom-color: #fff; 129 | border-width: 30px; 130 | margin-left: -30px; 131 | } 132 | .draw-instructions:before { 133 | border-color: rgba(153, 153, 153, 0); 134 | border-bottom-color: @highlight-colour; 135 | border-width: 36px; 136 | margin-left: -36px; 137 | } 138 | } 139 | 140 | @media (min-width: @screen-sm-min) { 141 | .draw-instructions { 142 | margin-left: -20px; 143 | margin-top: 20px; 144 | } 145 | .draw-instructions:after, .draw-instructions:before { 146 | right: 100%; 147 | top: 50%; 148 | border: solid transparent; 149 | content: " "; 150 | height: 0; 151 | width: 0; 152 | position: absolute; 153 | pointer-events: none; 154 | } 155 | 156 | .draw-instructions:after { 157 | border-color: rgba(255, 255, 255, 0); 158 | border-right-color: #fff; 159 | border-width: 30px; 160 | margin-top: -30px; 161 | } 162 | .draw-instructions:before { 163 | border-color: rgba(153, 153, 153, 0); 164 | border-right-color: @highlight-colour; 165 | border-width: 36px; 166 | margin-top: -36px; 167 | } 168 | } 169 | 170 | .save-button { 171 | background-color: @highlight-colour; 172 | color: #fff; 173 | } 174 | 175 | .suggestions-container { 176 | background-color: rgba(255,255,255,0.95); 177 | border: 1px solid #999; 178 | -webkit-box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 179 | -moz-box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 180 | box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.15); 181 | } 182 | .suggestion { 183 | padding: 3px 10px; 184 | font-size: 1.1em; 185 | &.selected, &:hover { 186 | background-color: #00b3ee; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /api/Slim/Middleware/PrettyExceptions.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2011 Josh Lockhart 7 | * @link http://www.slimframework.com 8 | * @license http://www.slimframework.com/license 9 | * @version 2.4.2 10 | * @package Slim 11 | * 12 | * MIT LICENSE 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | namespace Slim\Middleware; 34 | 35 | /** 36 | * Pretty Exceptions 37 | * 38 | * This middleware catches any Exception thrown by the surrounded 39 | * application and displays a developer-friendly diagnostic screen. 40 | * 41 | * @package Slim 42 | * @author Josh Lockhart 43 | * @since 1.0.0 44 | */ 45 | class PrettyExceptions extends \Slim\Middleware 46 | { 47 | /** 48 | * @var array 49 | */ 50 | protected $settings; 51 | 52 | /** 53 | * Constructor 54 | * @param array $settings 55 | */ 56 | public function __construct($settings = array()) 57 | { 58 | $this->settings = $settings; 59 | } 60 | 61 | /** 62 | * Call 63 | */ 64 | public function call() 65 | { 66 | try { 67 | $this->next->call(); 68 | } catch (\Exception $e) { 69 | $log = $this->app->getLog(); // Force Slim to append log to env if not already 70 | $env = $this->app->environment(); 71 | $env['slim.log'] = $log; 72 | $env['slim.log']->error($e); 73 | $this->app->contentType('text/html'); 74 | $this->app->response()->status(500); 75 | $this->app->response()->body($this->renderBody($env, $e)); 76 | } 77 | } 78 | 79 | /** 80 | * Render response body 81 | * @param array $env 82 | * @param \Exception $exception 83 | * @return string 84 | */ 85 | protected function renderBody(&$env, $exception) 86 | { 87 | $title = 'Slim Application Error'; 88 | $code = $exception->getCode(); 89 | $message = $exception->getMessage(); 90 | $file = $exception->getFile(); 91 | $line = $exception->getLine(); 92 | $trace = $exception->getTraceAsString(); 93 | $html = sprintf('

%s

', $title); 94 | $html .= '

The application could not run because of the following error:

'; 95 | $html .= '

Details

'; 96 | $html .= sprintf('
Type: %s
', get_class($exception)); 97 | if ($code) { 98 | $html .= sprintf('
Code: %s
', $code); 99 | } 100 | if ($message) { 101 | $html .= sprintf('
Message: %s
', $message); 102 | } 103 | if ($file) { 104 | $html .= sprintf('
File: %s
', $file); 105 | } 106 | if ($line) { 107 | $html .= sprintf('
Line: %s
', $line); 108 | } 109 | if ($trace) { 110 | $html .= '

Trace

'; 111 | $html .= sprintf('
%s
', $trace); 112 | } 113 | 114 | return sprintf("%s%s", $title, $html); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/app/draw/draw.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 07/03/14. 3 | */ 4 | 5 | angular.module('drawACat.draw', [ 6 | 'ui.router', 7 | 'drawACat.draw.directives', 8 | 'drawACat.draw.services' 9 | ]) 10 | 11 | .constant('DRAW_GUIDE_IMAGES', { 12 | head: 'assets/images/draw-guide-head.gif', 13 | eyesOpen: 'assets/images/draw-guide-eyes-open.gif', 14 | eyesClosed: 'assets/images/draw-guide-eyes-closed.gif', 15 | mouthClosed: 'assets/images/draw-guide-mouth-closed.gif', 16 | mouthOpen: 'assets/images/draw-guide-mouth-open.gif', 17 | body: 'assets/images/draw-guide-body.gif', 18 | leftLeg: 'assets/images/draw-guide-left-leg.gif', 19 | rightLeg: 'assets/images/draw-guide-right-leg.gif' 20 | }) 21 | 22 | .config(function config( $stateProvider ) { 23 | $stateProvider.state( 'draw', { 24 | url: '/cat/new', 25 | views: { 26 | "main": { 27 | controller: 'DrawController', 28 | templateUrl: 'draw/draw.tpl.html' 29 | } 30 | }, 31 | data:{ pageTitle: 'Home' } 32 | }); 33 | }) 34 | 35 | .controller('DrawController', function($scope, $filter, $state, primitives, drawHelper, serializer, datastore, catBuilder, thumbnailGenerator) { 36 | $scope.help = { 37 | show: true 38 | }; 39 | $scope.catParts = drawHelper.catParts; 40 | $scope.steps = drawHelper.partKeys; 41 | $scope.currentStep = drawHelper.getCurrentPartKey(); 42 | $scope.lineCollection = primitives.LineCollection(); 43 | $scope.drawing = { 44 | completed: false, 45 | showGuide: true 46 | }; 47 | $scope.showSaveDialog = false; 48 | 49 | $scope.nextStep = function() { 50 | savePart(); 51 | drawHelper.next(); 52 | loadLineCollection(); 53 | }; 54 | 55 | $scope.previousStep = function() { 56 | savePart(); 57 | drawHelper.previous(); 58 | loadLineCollection(); 59 | }; 60 | 61 | $scope.saveCat = function(formData) { 62 | savePart(); 63 | var catInfo = makeCatInfoObject(formData); 64 | 65 | datastore.saveCat(catInfo).then( 66 | function(response) { 67 | var urlName = $filter('urlFriendlyName')(catInfo.name); 68 | $state.go('cat', { id: response.data.id, name: urlName}); 69 | }, 70 | function() { 71 | $scope.errorText = "An error occurred! Try again."; 72 | } 73 | ); 74 | }; 75 | 76 | $scope.$on("$destroy", function() { 77 | drawHelper.reset(); 78 | }); 79 | 80 | $scope.$on('$locationChangeStart', function(event) { 81 | if (!confirm('Are you sure you want to leave this drawing without saving?')) { 82 | event.preventDefault(); 83 | } 84 | }); 85 | 86 | function savePart() { 87 | var partName = $scope.currentStep; 88 | if (0 < $scope.lineCollection.count()) { 89 | $scope.catParts[partName].lineCollection = $scope.lineCollection; 90 | $scope.catParts[partName].done = true; 91 | } else { 92 | $scope.catParts[partName].done = false; 93 | } 94 | } 95 | 96 | function loadLineCollection() { 97 | if ($scope.catParts[drawHelper.getCurrentPartKey()].done === false) { 98 | // reset the lineCollection to an empty collection and move on to the next part to draw 99 | $scope.lineCollection = primitives.LineCollection(); 100 | } else { 101 | $scope.lineCollection = $scope.catParts[drawHelper.getCurrentPartKey()].lineCollection; 102 | } 103 | } 104 | 105 | function makeCatInfoObject(formData) { 106 | var finalCat = catBuilder.buildCatFromParts($scope.catParts); 107 | var thumbnail = thumbnailGenerator.getDataUri(finalCat); 108 | var serializedCat = serializer.serializeCat(finalCat); 109 | return { 110 | name: formData.name, 111 | description: formData.description, 112 | author: formData.author, 113 | isPublic: formData.isPublic, 114 | tags: formData.tags, 115 | cat: serializedCat, 116 | thumbnail: thumbnail 117 | }; 118 | } 119 | }); -------------------------------------------------------------------------------- /src/common/services/renderHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 03/04/14. 3 | */ 4 | 5 | angular.module('drawACat.common.services') 6 | /** 7 | * The renderer service is responsible for all drawing to the canvas element. 8 | */ 9 | .service('renderHelper', function() { 10 | 11 | this.applyTransformationsToLine = function(line, transformationData, canvasWidth, canvasHeight) { 12 | return line.map(function(point) { 13 | return applyTransformations(point, transformationData, canvasWidth, canvasHeight); 14 | }); 15 | }; 16 | 17 | /** 18 | * Apply the all the current transforms: 19 | * - offset 20 | * - skew 21 | * - rotation 22 | * to the coordinates (x,y) and return the new, transformed coordinates. 23 | * 24 | * @param coords 25 | * @param transformationData 26 | * @returns {Array} 27 | * @param canvasWidth 28 | * @param canvasHeight 29 | */ 30 | var applyTransformations = function(coords, transformationData, canvasWidth, canvasHeight) { 31 | var td; 32 | td = transformationData; 33 | var x = coords[0]; 34 | var y = coords[1]; 35 | 36 | // apply the offset 37 | x = x + td.xOffset; 38 | y = y + td.yOffset; 39 | var ppx = td.pivotPointX + td.xOffset; 40 | var ppy = td.pivotPointY + td.yOffset; 41 | 42 | // apply the skew 43 | var skewPointX = ppx + td.xSkew; 44 | var skewPointY = ppy + td.ySkew; 45 | var xFromSkew = x - skewPointX; 46 | var yFromSkew = y - skewPointY; 47 | var distanceToSkewPoint = Math.sqrt(Math.pow(xFromSkew, 2) + Math.pow(yFromSkew, 2)); 48 | 49 | var deltaY = Math.sin((td.ySkew/canvasHeight) * Math.PI/2) * 20; 50 | var deltaX = Math.sin((td.xSkew/canvasWidth) * Math.PI/2) * 20; 51 | y += deltaY + distanceToSkewPoint/td.height*deltaY*5 - td.ySkew * td.height/500; 52 | x += deltaX + distanceToSkewPoint/td.width*deltaX*5 - td.xSkew * td.width/500; 53 | 54 | // apply the rotation 55 | var rotationInRadians = td.rotation * (Math.PI/180); 56 | var xFromPivot = x - ppx; 57 | var yFromPivot = y - ppy; 58 | var startingAngle = Math.PI /2 - Math.atan2(yFromPivot, xFromPivot); 59 | var radius = Math.sqrt(Math.pow(xFromPivot, 2) + Math.pow(yFromPivot, 2)); 60 | x = ppx + Math.sin(startingAngle + rotationInRadians) * radius; 61 | y = ppy + Math.cos(startingAngle + rotationInRadians) * radius; 62 | 63 | return [x, y]; 64 | }; 65 | 66 | /** 67 | * Determine whether this line encloses an area and therefore should be filled in. A line is 68 | * considered a boundary if the width or height of the shape it creates is greater than the 69 | * distance between the start and end points. 70 | * @param line 71 | * @returns {boolean} 72 | */ 73 | this.lineIsABoundary = function(line) { 74 | var startPoint = line[0]; 75 | var endPoint = line[line.length - 1]; 76 | var deltaX = Math.abs(startPoint[0] - endPoint[0]); 77 | var deltaY = Math.abs(startPoint[1] - endPoint[1]); 78 | var distanceBetweenEndPoints = Math.sqrt(deltaX * deltaX + deltaY * deltaY); 79 | 80 | var limits = getLineLimits(line); 81 | 82 | var width = limits.maxX - limits.minX; 83 | var height = limits.maxY - limits.minY; 84 | 85 | return (width > distanceBetweenEndPoints || height > distanceBetweenEndPoints); 86 | }; 87 | 88 | /** 89 | * Find the maximum and minimum values of x and y for a given line. 90 | * @param line 91 | * @returns {{minX: number, maxX: number, minY: number, maxY: number}} 92 | */ 93 | function getLineLimits(line) { 94 | var minX = 100000, 95 | maxX = 0, 96 | minY = 10000, 97 | maxY = 0; 98 | 99 | for(var point = 1; point < line.length; point ++) { 100 | minX = line[point][0] < minX ? line[point][0] : minX; 101 | minY = line[point][1] < minY ? line[point][1] : minY; 102 | maxX = line[point][0] > maxX ? line[point][0] : maxX; 103 | maxY = line[point][1] > maxY ? line[point][1] : maxY; 104 | } 105 | return { 106 | minX: minX, 107 | maxX: maxX, 108 | minY: minY, 109 | maxY: maxY 110 | }; 111 | } 112 | 113 | }) 114 | ; -------------------------------------------------------------------------------- /src/common/services/ipCookie.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Ivan Pusic 3 | * Contributors: 4 | * Matjaz Lipus 5 | * https://github.com/ivpusic/angular-cookie 6 | */ 7 | angular.module('drawACat.common.services') 8 | .factory('ipCookie', ['$document', 9 | function ($document) { 10 | 'use strict'; 11 | 12 | return (function () { 13 | function cookieFun(key, value, options) { 14 | 15 | var cookies, 16 | list, 17 | i, 18 | cookie, 19 | pos, 20 | name, 21 | hasCookies, 22 | all, 23 | expiresFor; 24 | 25 | options = options || {}; 26 | 27 | if (value !== undefined) { 28 | // we are setting value 29 | value = typeof value === 'object' ? JSON.stringify(value) : String(value); 30 | 31 | if (typeof options.expires === 'number') { 32 | expiresFor = options.expires; 33 | options.expires = new Date(); 34 | // Trying to delete a cookie; set a date far in the past 35 | if (expiresFor === -1) { 36 | options.expires = new Date('Thu, 01 Jan 1970 00:00:00 GMT'); 37 | // A new 38 | } else { 39 | if (options.expirationUnit !== undefined) { 40 | if (options.expirationUnit === 'minutes') { 41 | options.expires.setMinutes(options.expires.getMinutes() + expiresFor); 42 | } else { 43 | options.expires.setDate(options.expires.getDate() + expiresFor); 44 | } 45 | } else { 46 | options.expires.setDate(options.expires.getDate() + expiresFor); 47 | } 48 | } 49 | } 50 | return ($document[0].cookie = [ 51 | encodeURIComponent(key), 52 | '=', 53 | encodeURIComponent(value), 54 | options.expires ? '; expires=' + options.expires.toUTCString() : '', 55 | options.path ? '; path=' + options.path : '', 56 | options.domain ? '; domain=' + options.domain : '', 57 | options.secure ? '; secure' : '' 58 | ].join('')); 59 | } 60 | 61 | list = []; 62 | all = $document[0].cookie; 63 | if (all) { 64 | list = all.split('; '); 65 | } 66 | 67 | cookies = {}; 68 | hasCookies = false; 69 | 70 | for (i = 0; i < list.length; ++i) { 71 | if (list[i]) { 72 | cookie = list[i]; 73 | pos = cookie.indexOf('='); 74 | name = cookie.substring(0, pos); 75 | value = decodeURIComponent(cookie.substring(pos + 1)); 76 | 77 | if (key === undefined || key === name) { 78 | try { 79 | cookies[name] = JSON.parse(value); 80 | } catch (e) { 81 | cookies[name] = value; 82 | } 83 | if (key === name) { 84 | return cookies[name]; 85 | } 86 | hasCookies = true; 87 | } 88 | } 89 | } 90 | if (hasCookies && key === undefined) { 91 | return cookies; 92 | } 93 | } 94 | cookieFun.remove = function (key, options) { 95 | var hasCookie = cookieFun(key) !== undefined; 96 | 97 | if (hasCookie) { 98 | if (!options) { 99 | options = {}; 100 | } 101 | options.expires = -1; 102 | cookieFun(key, '', options); 103 | } 104 | return hasCookie; 105 | }; 106 | return cookieFun; 107 | }()); 108 | } 109 | ]); -------------------------------------------------------------------------------- /src/less/main.less: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the main application stylesheet. It should include or import all 3 | * stylesheets used throughout the application as this is the only stylesheet in 4 | * the Grunt configuration that is automatically processed. 5 | */ 6 | 7 | 8 | /** 9 | * First, we include the Twitter Bootstrap LESS files. Only the ones used in the 10 | * project should be imported as the rest are just wasting space. 11 | */ 12 | 13 | //@import '../../vendor/bootstrap/less/mixins.less'; 14 | //@import '../../vendor/bootstrap/less/reset.less'; 15 | //@import '../../vendor/bootstrap/less/utilities.less'; 16 | //@import '../../vendor/bootstrap/less/scaffolding.less'; 17 | //@import '../../vendor/bootstrap/less/type.less'; 18 | //@import '../../vendor/bootstrap/less/grid.less'; 19 | //@import '../../vendor/bootstrap/less/layouts.less'; 20 | //@import '../../vendor/bootstrap/less/navs.less'; 21 | //@import '../../vendor/bootstrap/less/navbar.less'; 22 | //@import '../../vendor/bootstrap/less/buttons.less'; 23 | //@import '../../vendor/bootstrap/less/button-groups.less'; 24 | //@import '../../vendor/bootstrap/less/dropdowns.less'; 25 | //@import '../../vendor/bootstrap/less/theme.less'; 26 | //@import '../../vendor/bootstrap/less/grid.less'; 27 | @import '../../vendor/bootstrap/less/bootstrap.less'; 28 | //@import '../../vendor/bootstrap/less/normalize.less'; 29 | //@import '../../vendor/bootstrap/less/scaffolding.less'; 30 | 31 | 32 | /** 33 | * This is our main variables file. It in turn imports the `variables` file from 34 | * Twitter Bootstrap. We must include it last so we can overwrite any variable 35 | * definitions in our imported stylesheets. 36 | */ 37 | 38 | 39 | /** 40 | Colours 41 | */ 42 | 43 | @highlight-colour: #656de4; 44 | 45 | @import 'variables.less'; 46 | 47 | html { 48 | overflow-x: hidden; 49 | height: 100%; 50 | min-height: 100%; 51 | &.min-width { 52 | min-width: 540px; 53 | } 54 | } 55 | 56 | body { 57 | 58 | } 59 | 60 | img { 61 | width: auto; 62 | height: auto; 63 | max-width: 100%; 64 | max-height: 100%; 65 | } 66 | 67 | a:link { 68 | color: @highlight-colour; 69 | } 70 | a:visited { 71 | color: darken(@highlight-colour, 5%); 72 | } 73 | a:hover, a:active { 74 | color: lighten(@highlight-colour, 5%); 75 | } 76 | 77 | .main { 78 | min-height: 500px; 79 | background-color: #f5f5f5; 80 | padding-bottom: 100px; 81 | } 82 | .footer { 83 | font-family: @comic-sans; 84 | font-size: 18px; 85 | min-height: 200px; 86 | padding: 30px; 87 | color: #666; 88 | background-color: #fff; 89 | -webkit-box-shadow: inset 0px 8px 20px -7px rgba(0,0,0,0.42); 90 | -moz-box-shadow: inset 0px 8px 20px -7px rgba(0,0,0,0.42); 91 | box-shadow: inset 0px 8px 20px -7px rgba(0,0,0,0.42); 92 | } 93 | 94 | /** 95 | * Typography 96 | */ 97 | @comic-sans : 'Architects Daughter', 'Comic Sans', 'Comic Sans MS', 'Comic Sans MS5', 'Chalkboard', 'ChalkboardSE-Regular', 'Marker Felt', 'Purisa', 'URW Chancery L', sans-serif, arial; 98 | 99 | code, pre, .pre { 100 | padding: 5px; 101 | margin: 10px 0; 102 | background-color: #EFEFEF; 103 | border: 1px solid #DADADA; 104 | } 105 | 106 | code { 107 | padding: 0 3px; 108 | } 109 | 110 | pre { 111 | margin: 10px 0; 112 | padding: 5px; 113 | } 114 | 115 | h2 { 116 | margin: 20px 0; 117 | color: #666; 118 | } 119 | 120 | a:focus { 121 | text-decoration: none; 122 | } 123 | 124 | /** 125 | * Navigation 126 | */ 127 | 128 | /** 129 | * Directives 130 | */ 131 | .dac-modal-overlay { 132 | /* A dark translucent div that covers the whole screen */ 133 | position:fixed; 134 | z-index:9999; 135 | top:0; 136 | left:0; 137 | width:100%; 138 | min-width: 550px; 139 | height:100%; 140 | background-color:#000000; 141 | opacity: 0.8; 142 | } 143 | .dac-modal-dialog { 144 | /* A centered div above the overlay with a box shadow. */ 145 | z-index:10000; 146 | position: absolute; 147 | width: 98%; /* Default */ 148 | max-width: 500px; 149 | /* Center the dialog */ 150 | top: 50%; 151 | left: 50%; 152 | transform: translate(-50%, -50%); 153 | -webkit-transform: translate(-50%, -50%); 154 | -moz-transform: translate(-50%, -50%); 155 | 156 | background-color: #fff; 157 | box-shadow: 4px 4px 80px #000; 158 | } 159 | .dac-modal-dialog-content { 160 | padding:10px; 161 | text-align: left; 162 | } 163 | .dac-modal-close { 164 | position: absolute; 165 | top: 3px; 166 | right: 5px; 167 | padding: 5px; 168 | cursor: pointer; 169 | font-size: 120%; 170 | display: inline-block; 171 | font-weight: bold; 172 | font-family: 'arial', 'sans-serif'; 173 | } 174 | 175 | 176 | /** 177 | * Now that all app-wide styles have been applied, we can load the styles for 178 | * all the submodules and components we are using. 179 | */ 180 | 181 | @import 'icons.less'; 182 | @import '../app/home/home.less'; 183 | @import '../app/draw/draw.less'; 184 | @import '../app/cat/cat.less'; 185 | 186 | -------------------------------------------------------------------------------- /src/common/directives/touchEvents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 17/03/14. 3 | */ 4 | angular.module('drawACat.common.directives') 5 | /** 6 | * Directives that incorporate Hammer.js events to make the element respond to both mouse and touch input. 7 | */ 8 | .directive('dacPointerstart', function($parse) { 9 | return { 10 | restrict: 'A', 11 | compile: function($element, attr) { 12 | var fn = $parse(attr['dacPointerstart']); 13 | return function(scope, element) { 14 | Hammer(element[0], { prevent_default: true, drag_min_distance: 2 }).on('touch', function(event) { 15 | event = event.gesture; 16 | if (!event.clientX && event.touches) { 17 | var propertiesToCopy = [ 18 | 'clientX', 19 | 'clientY', 20 | 'screenX', 21 | 'screenY', 22 | 'pageX', 23 | 'pageY' 24 | ]; 25 | for (var prop = 0; prop < propertiesToCopy.length; prop ++) { 26 | event[propertiesToCopy[prop]] = event.touches[0][propertiesToCopy[prop]]; 27 | } 28 | } 29 | scope.$apply(function() { 30 | fn(scope, {$event:event}); 31 | }); 32 | }); 33 | }; 34 | } 35 | }; 36 | }) 37 | .directive('dacPointermove', function($parse) { 38 | return { 39 | restrict: 'A', 40 | compile: function($element, attr) { 41 | var fn = $parse(attr['dacPointermove']); 42 | return function(scope, element) { 43 | Hammer(element[0], { prevent_default: true}).on('drag', function(event) { 44 | event = event.gesture; 45 | if (!event.clientX && event.touches) { 46 | var propertiesToCopy = [ 47 | 'clientX', 48 | 'clientY', 49 | 'screenX', 50 | 'screenY', 51 | 'pageX', 52 | 'pageY' 53 | ]; 54 | for (var prop = 0; prop < propertiesToCopy.length; prop ++) { 55 | event[propertiesToCopy[prop]] = event.touches[0][propertiesToCopy[prop]]; 56 | } 57 | } 58 | scope.$apply(function() { 59 | fn(scope, {$event:event}); 60 | }); 61 | }); 62 | }; 63 | } 64 | }; 65 | }) 66 | .directive('dacPointerend', function($parse) { 67 | return { 68 | restrict: 'A', 69 | compile: function($element, attr) { 70 | var fn = $parse(attr['dacPointerend']); 71 | return function(scope, element) { 72 | Hammer(element[0], { prevent_default: true }).on('release', function(event) { 73 | scope.$apply(function() { 74 | fn(scope, {$event:event}); 75 | }); 76 | }); 77 | }; 78 | } 79 | }; 80 | }) 81 | 82 | .directive('dacDoubletap', function($parse) { 83 | return { 84 | restrict: 'A', 85 | compile: function($element, attr) { 86 | var fn = $parse(attr['dacDoubletap']); 87 | return function(scope, element) { 88 | Hammer(element[0], { prevent_default: true }).on('doubletap', function(event) { 89 | event = event.gesture; 90 | if (!event.clientX && event.touches) { 91 | var propertiesToCopy = [ 92 | 'clientX', 93 | 'clientY', 94 | 'screenX', 95 | 'screenY', 96 | 'pageX', 97 | 'pageY' 98 | ]; 99 | for (var prop = 0; prop < propertiesToCopy.length; prop ++) { 100 | event[propertiesToCopy[prop]] = event.touches[0][propertiesToCopy[prop]]; 101 | } 102 | } 103 | scope.$apply(function() { 104 | fn(scope, {$event:event}); 105 | }); 106 | }); 107 | }; 108 | } 109 | }; 110 | }) 111 | 112 | 113 | ; -------------------------------------------------------------------------------- /src/app/cat/cat.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 | 27 |
28 |
29 |
30 |
31 |

Embed This Cat

32 |

33 | Use the following code to embed {{ cat.name }} in another web page: 34 |

35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |

Set Render Quality

43 |

44 | If the animation is not smooth, try lowering the render quality. More complex cats consist of more lines and points, and can require faster 45 | hardware to run at full speed. 46 |

47 |
    48 |
  • Frames Per Second: {{ profileData.fps | number:2 }}
  • 49 |
  • Points Drawn: {{ profileData.pointsDrawn }}
  • 50 |
51 |
52 |
53 | 54 | 55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 |

63 | {{ catData.name }} by {{ catData.author }} 64 |

65 |
66 |

{{ catData.description }}

67 | 68 |
69 | 78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 |
86 | 87 | 88 |

Double-click for more balls!

89 |
90 | 91 |
92 |
-------------------------------------------------------------------------------- /src/common/services/primitives.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 13/03/14. 3 | */ 4 | describe( 'primitives service', function() { 5 | 6 | var primitives; 7 | 8 | beforeEach( module( 'drawACat.common.services' ) ); 9 | 10 | beforeEach( inject( function( _primitives_ ) { 11 | primitives = _primitives_; 12 | })); 13 | 14 | describe('Line object', function() { 15 | 16 | var line; 17 | 18 | beforeEach(function() { 19 | line = primitives.Line(); 20 | }); 21 | 22 | it('should start with an empty path', function() { 23 | expect(line.getPath()).toEqual([]); 24 | }); 25 | 26 | it('should add a point', function() { 27 | line.addPoint(10, 15); 28 | expect(line.getPath()).toEqual([[10, 15]]); 29 | }); 30 | }); 31 | 32 | describe('LineCollection object', function() { 33 | var lineCollection; 34 | var testLine; 35 | 36 | beforeEach(function() { 37 | lineCollection = primitives.LineCollection(); 38 | testLine = primitives.Line(); 39 | testLine.addPoint(1, 5); 40 | testLine.addPoint(3, 10); 41 | testLine.addPoint(7, 15); 42 | }); 43 | 44 | it('should start with an empty collection', function() { 45 | expect(lineCollection.getPath()).toEqual([]); 46 | }); 47 | 48 | it('should add a line', function() { 49 | lineCollection.addLine(testLine); 50 | expect(lineCollection.count()).toEqual(1); 51 | }); 52 | 53 | it('should remove a line', function() { 54 | lineCollection.addLine(testLine); 55 | expect(lineCollection.count()).toEqual(1); 56 | lineCollection.removeLine(); 57 | expect(lineCollection.count()).toEqual(0); 58 | }); 59 | 60 | it('should return the path', function() { 61 | lineCollection.addLine(testLine); 62 | lineCollection.addLine(testLine); 63 | 64 | var expectedResult = [ 65 | [ 66 | [1,5], 67 | [3,10], 68 | [7,15] 69 | ], 70 | [ 71 | [1,5], 72 | [3,10], 73 | [7,15] 74 | ] 75 | ]; 76 | 77 | expect(lineCollection.getPath()).toEqual(expectedResult); 78 | }); 79 | }); 80 | 81 | describe('Part object', function() { 82 | var part; 83 | var testPath; 84 | 85 | beforeEach(function() { 86 | part = primitives.Part(); 87 | testPath = [ 88 | [ 89 | [1,5], 90 | [3,10], 91 | [7,15] 92 | ], 93 | [ 94 | [1,5], 95 | [3,10], 96 | [7,15] 97 | ] 98 | ]; 99 | }); 100 | 101 | it('should start with an empty path', function() { 102 | expect(part.getPath()).toEqual([]); 103 | }); 104 | 105 | it('should populate via the createFromPath() method', function() { 106 | part.createFromPath('testPart', testPath); 107 | expect(part.getPath()).toEqual(testPath); 108 | expect(part.getName()).toEqual('testPart'); 109 | }); 110 | 111 | it('should handle bad input in createFromPath() method', function() { 112 | part.createFromPath({ notAString: true }, 'notAnArray'); 113 | expect(part.getPath()).toEqual([]); 114 | expect(part.getName()).toEqual(''); 115 | }); 116 | 117 | it('should set its parent correctly', function() { 118 | var parentPart = primitives.Part(); 119 | parentPart.createFromPath('parentPart', []); 120 | 121 | expect(part.getParent()).toBeNull(); 122 | 123 | part.setParent(parentPart); 124 | expect(part.getParent().getName()).toEqual('parentPart'); 125 | }); 126 | 127 | it('should have settable transformation properties', function() { 128 | part.setXOffset(12); 129 | part.setYOffset(13); 130 | part.setXSkew(10); 131 | part.setYSkew(11); 132 | part.setRotation(14); 133 | 134 | var expectedResult = { 135 | xOffset: 12, 136 | yOffset: 13, 137 | pivotPointX: 0, 138 | pivotPointY: 0, 139 | rotation: 14, 140 | xSkew: 10, 141 | ySkew: 11, 142 | width: 0, 143 | height: 0, 144 | centreX: 0, 145 | centreY: 0, 146 | scale: 1 147 | }; 148 | expect(part.getTransformationData()).toEqual(expectedResult); 149 | }); 150 | 151 | it('should correctly calculate part dimensions', function() { 152 | var path = [ 153 | [ 154 | [0, 0], 155 | [25, 20] 156 | ] 157 | ]; 158 | part.createFromPath('test', path); 159 | 160 | expect(part.getTransformationData().width).toEqual(25); 161 | expect(part.getTransformationData().height).toEqual(20); 162 | }); 163 | }); 164 | }); -------------------------------------------------------------------------------- /src/app/draw/services/drawHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 14/03/14. 3 | */ 4 | 5 | angular.module('drawACat.draw.services') 6 | /** 7 | * This service is used to control the sequence in which the parts of the cat are drawn. It also contains pre-configured settings which are used to 8 | * set up the new cat instance. 9 | */ 10 | .factory('drawHelper', function() { 11 | 12 | var catPartsTemplate = function() { 13 | return { 14 | head: { 15 | label: 'Head', 16 | lineCollection: false, 17 | behaviour:{ 18 | sensitivity: { 19 | xSkew: 0.25, 20 | ySkew: 0.25, 21 | xOffset: -0.05, 22 | rotation: 0.1 23 | }, 24 | range: 700 25 | }, 26 | done: false 27 | }, 28 | eyesOpen: { 29 | label: 'Eyes Open', 30 | lineCollection: false, 31 | parentPart: 'head', 32 | done: false 33 | }, 34 | eyesClosed: { 35 | label: 'Eyes Closed', 36 | lineCollection: false, 37 | parentPart: 'head', 38 | visible: false, 39 | done: false 40 | }, 41 | mouthOpen: { 42 | label: 'Mouth Open', 43 | lineCollection: false, 44 | parentPart: 'head', 45 | visible: false, 46 | done: false 47 | }, 48 | mouthClosed: { 49 | label: 'Mouth Closed', 50 | lineCollection: false, 51 | parentPart: 'head', 52 | done: false 53 | }, 54 | body: { 55 | label: 'Body', 56 | lineCollection: false, 57 | behaviour:{ 58 | sensitivity: { 59 | xSkew: 0.01, 60 | ySkew: 0.05, 61 | xOffset: -0.01, 62 | yOffset: -0.02, 63 | rotation: 0 64 | }, 65 | range: 300 66 | }, 67 | done: false 68 | }, 69 | leftLeg: { 70 | label: 'Left Leg', 71 | lineCollection: false, 72 | behaviour:{ 73 | sensitivity: { 74 | xSkew: 0.2, 75 | ySkew: 0.4, 76 | xOffset: 0.6, 77 | yOffset: 0.6, 78 | rotation: 0.4 79 | }, 80 | range: 200 81 | }, 82 | done: false 83 | }, 84 | rightLeg: { 85 | label: 'Right Leg', 86 | lineCollection: false, 87 | behaviour:{ 88 | sensitivity: { 89 | xSkew: 0.2, 90 | ySkew: 0.4, 91 | xOffset: 0.6, 92 | yOffset: 0.6, 93 | rotation: 0.4 94 | }, 95 | range: 200 96 | }, 97 | done: false 98 | } 99 | }; 100 | }; 101 | var catParts = catPartsTemplate(); 102 | var partKeys = [ 103 | 'head', 104 | 'eyesOpen', 105 | 'eyesClosed', 106 | 'mouthClosed', 107 | 'mouthOpen', 108 | 'body', 109 | 'leftLeg', 110 | 'rightLeg' 111 | ]; 112 | var currentPartIndex = 0; 113 | 114 | return { 115 | catParts: catParts, 116 | partKeys: partKeys, 117 | getCurrentPartLabel: function() { 118 | if (currentPartIndex < partKeys.length) { 119 | var currentPartKey = partKeys[currentPartIndex]; 120 | return catParts[currentPartKey].label; 121 | } else { 122 | return 'end'; 123 | } 124 | }, 125 | getCurrentPartKey: function() { 126 | if (currentPartIndex < partKeys.length) { 127 | return partKeys[currentPartIndex]; 128 | } else { 129 | return 'end'; 130 | } 131 | }, 132 | next: function() { 133 | if (currentPartIndex < partKeys.length - 1) { 134 | currentPartIndex ++; 135 | } 136 | }, 137 | previous: function() { 138 | if (0 < currentPartIndex) { 139 | currentPartIndex --; 140 | } 141 | }, 142 | reset: function() { 143 | currentPartIndex = 0; 144 | this.catParts = catPartsTemplate(); 145 | } 146 | }; 147 | }) 148 | 149 | 150 | ; -------------------------------------------------------------------------------- /src/app/draw/services/catBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 17/03/14. 3 | */ 4 | angular.module('drawACat.draw.services') 5 | 6 | .factory('catBuilder', function(catFactory, primitives, behaviourFactory) { 7 | 8 | /** 9 | * Takes the catParts object which is generated by the drawHelper service, and populated with data by the DrawController, and processes it 10 | * into a Cat which can then be saved. 11 | * 12 | * @returns Cat 13 | * @param catParts 14 | */ 15 | var buildCatFromParts = function(catParts) { 16 | var minimumCoordinates = getMinimumCoordinates(catParts); 17 | var minX = minimumCoordinates.x; 18 | var minY = minimumCoordinates.y; 19 | 20 | var normalCat = catFactory.newCat(); 21 | 22 | angular.forEach(catParts, function(partTemplate, partName) { 23 | var partPath = catParts[partName].lineCollection.getPath(); 24 | var normalizedPath = normalizePath(partPath, minX, minY); 25 | 26 | var newPart = primitives.Part(); 27 | newPart.createFromPath(partName, normalizedPath); 28 | normalCat.bodyParts[partName].part = newPart; 29 | 30 | var newBehaviour = behaviourFactory.newBehaviour(); 31 | newBehaviour = applyBehaviourTemplate(newBehaviour, partTemplate.behaviour); 32 | normalCat.bodyParts[partName].behaviour = newBehaviour; 33 | }); 34 | 35 | // now we need to loop through the bodyParts once more to resolve the parent/child relationships 36 | angular.forEach(catParts, function(partTemplate, partName) { 37 | if(partTemplate.parentPart) { 38 | normalCat.bodyParts[partName].part.setParent(normalCat.bodyParts[partTemplate.parentPart].part); 39 | } 40 | }); 41 | 42 | return normalCat; 43 | }; 44 | 45 | /** 46 | * Calculate the minimum x and y coordinates of all the lines in the catParts object. 47 | * @param catParts 48 | * @returns {{x: number, y: number}} 49 | */ 50 | var getMinimumCoordinates = function(catParts) { 51 | var minXCandidates = []; 52 | var minYCandidates = []; 53 | 54 | angular.forEach(catParts, function(catPart) { 55 | var partPath = catPart.lineCollection.getPath(); 56 | 57 | angular.forEach(partPath, function(line) { 58 | var minXCandidate = line.reduce(function(min, point) { 59 | return Math.min(min, point[0]); 60 | }, Infinity); 61 | minXCandidates.push(minXCandidate); 62 | 63 | var minYCandidate = line.reduce(function(min, point) { 64 | return Math.min(min, point[1]); 65 | }, Infinity); 66 | minYCandidates.push(minYCandidate); 67 | }); 68 | }); 69 | 70 | var minX = Math.min.apply(null, minXCandidates); 71 | var minY = Math.min.apply(null, minYCandidates); 72 | 73 | return { 74 | x: minX, 75 | y: minY 76 | }; 77 | }; 78 | 79 | /** 80 | * Make the cat's path origin start from coordinate 0,0. This will allow us to properly position the cat when it is 81 | * subsequently rendered. 82 | * 83 | * @param partPath 84 | * @param minX 85 | * @param minY 86 | * @returns {*|Array} 87 | */ 88 | var normalizePath = function(partPath, minX, minY) { 89 | return partPath.map(function(line) { 90 | return line 91 | // TODO: smart algorithm to smooth line and remove unnecessary points 92 | /*.filter(function(point, index) { 93 | // filter out every other element (starting from the second element) to reduce the amount of 94 | // data to be stored. Has no visible effect of the rendered shapes, but halves the storage space required 95 | // and vastly speeds up rendering. 96 | return (index + 1) % 2 === 0; 97 | })*/ 98 | .map(function(point) { 99 | // subtract the minX and minY values from each coordinate so that the cat is aligned to the top left 100 | // of the x/y origin point. 101 | return [ 102 | point[0] - minX, 103 | point[1] - minY 104 | ]; 105 | }); 106 | }); 107 | }; 108 | var applyBehaviourTemplate = function(newBehaviour, templateBehaviour) { 109 | if (templateBehaviour) { 110 | if (templateBehaviour.sensitivity) { 111 | newBehaviour.setSensitivity(templateBehaviour.sensitivity); 112 | } 113 | if (templateBehaviour.range) { 114 | newBehaviour.range = templateBehaviour.range; 115 | } 116 | if (templateBehaviour.visible) { 117 | newBehaviour.visible = templateBehaviour.visible; 118 | } 119 | } 120 | 121 | return newBehaviour; 122 | }; 123 | 124 | return { 125 | buildCatFromParts: buildCatFromParts 126 | }; 127 | }); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.1 (2013-09-13) 2 | 3 | ## Features 4 | 5 | * Page titles are now set via state objects ([33de8097](git@github.com:joshdmiller/ng-boilerplate/commit/33de8097)) 6 | * Append pkg.version to JS and CSS ([90e1b71f](git@github.com:joshdmiller/ng-boilerplate/commit/90e1b71f)) 7 | * Vendor CSS is copied and concatenated with the app's CSS ([dda8792c](git@github.com:joshdmiller/ng-boilerplate/commit/dda8792c)) 8 | * Vendor assets are copied to the build too ([29502bff](git@github.com:joshdmiller/ng-boilerplate/commit/29502bff)) 9 | * Treat JS in src/assets as assets (i.e. don't do anything with it) ([99b50751](git@github.com:joshdmiller/ng-boilerplate/commit/99b50751)) 10 | * Added PhantomJS support ([89acf5f6](git@github.com:joshdmiller/ng-boilerplate/commit/89acf5f6)) 11 | * Files for use only in testing are now configurable ([a04e663b](git@github.com:joshdmiller/ng-boilerplate/commit/a04e663b)) 12 | 13 | ## Bug fixes 14 | 15 | * CopyPasteException in index.html comments ([3a0596a7](git@github.com:joshdmiller/ng-boilerplate/commit/3a0596a7)) 16 | * Fixed typos in the README ([5ae95393](git@github.com:joshdmiller/ng-boilerplate/commit/5ae95393)), ([8c362208](git@github.com:joshdmiller/ng-boilerplate/commit/8c362208)), and ([6b617282](git@github.com:joshdmiller/ng-boilerplate/commit/6b617282)) 17 | * Vendor files were added to build twice ([09277b74](git@github.com:joshdmiller/ng-boilerplate/commit/09277b74)) 18 | * IE7 Font Awesome stylesheet pointed nowhere ([515673b1](git@github.com:joshdmiller/ng-boilerplate/commit/515673b1)) 19 | 20 | # 0.3.0 (2013-06-25) 21 | 22 | ## Features 23 | ### build 24 | 25 | * split build into build+compile ([97fb290d](https://github.com/joshdmiller/ng-boilerplate/commits/97fb290d)) 26 | * Moved config to separate file ([ff5d8b58](https://github.com/joshdmiller/ng-boilerplate/commits/ff5d8b58)) 27 | * Added grunt-bump to ease releasing ([27312de1](https://github.com/joshdmiller/ng-boilerplate/commits/27312de1)) 28 | * Added changelog generation ([328d25d2](https://github.com/joshdmiller/ng-boilerplate/commits/328d25d2)) 29 | * karma config managed automatically ([3384b6fd](https://github.com/joshdmiller/ng-boilerplate/commits/3384b6fd)) 30 | * CoffeeScript support ([0f308f2f](https://github.com/joshdmiller/ng-boilerplate/commits/0f308f2f)) 31 | 32 | ### * 33 | 34 | * switched to ui-router for state mgmt ([7bec0378](https://github.com/joshdmiller/ng-boilerplate/commits/7bec0378)) 35 | 36 | ## Bug fixes 37 | ### build 38 | 39 | * Karma no longer hangs the watch (([f66cfcc6])(https://github.com/joshdmiller/ng-boilerplate/commits/f66cfcc6)) 40 | 41 | 42 | 43 | # 0.2.0 (2013-05-10) 44 | 45 | ## Features 46 | ### build 47 | 48 | * live reload added through grunt-watch ([653df741](https://github.com/joshdmiller/ng-boilerplate/commits/653df741)) 49 | 50 | * Add grunt ng-min for annotation ([9c529ccb](https://github.com/joshdmiller/ng-boilerplate/commits/9c529ccb)) 51 | 52 | ### * 53 | 54 | * far better Bower integration ([864c2656](https://github.com/joshdmiller/ng-boilerplate/commits/864c2656)) 55 | 56 | * included AngularUI `utils` to use uiRoute ([df08e4be](https://github.com/joshdmiller/ng-boilerplate/commits/df08e4be)) 57 | 58 | 59 | 60 | 61 | 62 | 63 | # 0.1.0 (2013-03-11) 64 | 65 | ## Features 66 | ### * 67 | 68 | * Initial application structure ([7c149227](https://github.com/joshdmiller/ng-boilerplate/commits/7c149227)) 69 | 70 | * improved navigation styling and added home page tpl ([e1a655e0](https://github.com/joshdmiller/ng-boilerplate/commits/e1a655e0)) 71 | 72 | ### app 73 | 74 | * added current route indication to menu with appropriate unit test ([14d35da8](https://github.com/joshdmiller/ng-boilerplate/commits/14d35da8)) 75 | 76 | ### index 77 | 78 | * improved navbar style and added additional links ([a7c4504c](https://github.com/joshdmiller/ng-boilerplate/commits/a7c4504c)) 79 | 80 | ### about 81 | 82 | * Added an about page with some descriptive content ([290704ab](https://github.com/joshdmiller/ng-boilerplate/commits/290704ab)) 83 | 84 | * Added placeholders demo to about page ([89a06e9f](https://github.com/joshdmiller/ng-boilerplate/commits/89a06e9f)) 85 | 86 | ### titleService 87 | 88 | * dynamic title support) ([3db6ec2b](https://github.com/joshdmiller/ng-boilerplate/commits/3db6ec2b)) 89 | 90 | * suffix is now customizable ([9f8b4c73](https://github.com/joshdmiller/ng-boilerplate/commits/9f8b4c73)) 91 | 92 | ### activeIfCurrentDirective 93 | 94 | * created directory to test for current route ([0ac1f4b4](https://github.com/joshdmiller/ng-boilerplate/commits/0ac1f4b4)) 95 | 96 | ### home 97 | 98 | * replaced placeholder text with mrktg copy ([dcaf7237](https://github.com/joshdmiller/ng-boilerplate/commits/dcaf7237)) 99 | 100 | * added google +1 button ([98d3312b](https://github.com/joshdmiller/ng-boilerplate/commits/98d3312b)) 101 | 102 | 103 | 104 | ## Bug fixes 105 | ### build 106 | 107 | * Removed unnecessary step from delta:unittest ([5ffbfd78](https://github.com/joshdmiller/ng-boilerplate/commits/5ffbfd78)) 108 | 109 | * delta tasks that concat must also uglify ([926983f8](https://github.com/joshdmiller/ng-boilerplate/commits/926983f8)) 110 | 111 | ### test-config 112 | 113 | * Change browser-name case, add browser list ([682b1ea4](https://github.com/joshdmiller/ng-boilerplate/commits/682b1ea4)) 114 | 115 | ### home 116 | 117 | * corrected typo in tweet button URL ([b9920eea](https://github.com/joshdmiller/ng-boilerplate/commits/b9920eea)) 118 | 119 | ### testacular 120 | 121 | * fixed typo in browser docstring ([11a60fa7](https://github.com/joshdmiller/ng-boilerplate/commits/11a60fa7)) 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/app/cat/services/emotion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 16/03/14. 3 | */ 4 | angular.module('drawACat.cat.services') 5 | 6 | .factory('emotion', function($timeout) { 7 | // emotional constants 8 | var MAX_MOOD = 20; 9 | var MIN_MOOD = -20; 10 | var HAPPY_THRESHOLD = 15; 11 | var ANGRY_THRESHOLD = -10; 12 | var EXCITED_THRESHOLD = 15; 13 | var BORED_THRESHOLD = -10; 14 | var ATTENTION_SPAN_IN_SECONDS = 10; 15 | 16 | var happy = 0; 17 | var excited = 0; 18 | var timeSinceAnythingHappened = 0; 19 | var isBeingStroked = false; 20 | 21 | var timerId; 22 | function emotionLoop() { 23 | timeSinceAnythingHappened ++; 24 | 25 | if (ATTENTION_SPAN_IN_SECONDS < timeSinceAnythingHappened) { 26 | getBored(); 27 | } else if (excited < EXCITED_THRESHOLD) { 28 | calmDown(); 29 | } 30 | if (EXCITED_THRESHOLD < excited) { 31 | // being excited also sometimes makes a cat feel happy too 32 | var happyVal = (happy - HAPPY_THRESHOLD) / (MAX_MOOD - HAPPY_THRESHOLD); 33 | if (Math.random() > happyVal / 2 + 0.5) { 34 | getHappier(); 35 | } 36 | } 37 | if (HAPPY_THRESHOLD < happy) { 38 | // cat constantly gets less happy slowly 39 | if (Math.random() < 0.4) { 40 | calmDown(); 41 | } 42 | } else if (0 < happy) { 43 | // cat constantly gets less happy slowly 44 | if (Math.random() < 0.3) { 45 | calmDown(); 46 | } 47 | } 48 | if (excited < BORED_THRESHOLD) { 49 | // being bored makes the cat start to get angry 50 | getAngrier(); 51 | } 52 | if (0 < excited) { 53 | // cat constantly gets bored slowly 54 | if ((timeSinceAnythingHappened + 1) % 3 === 0) { 55 | getBored(); 56 | } 57 | } 58 | 59 | if (isBeingStroked) { 60 | // being stoked obviously makes the cat 100% happy 61 | happy = MAX_MOOD; 62 | } 63 | 64 | timerId = $timeout(emotionLoop, 1000); 65 | } 66 | 67 | function getHappier() { 68 | if (happy < MAX_MOOD) { 69 | happy ++; 70 | } 71 | } 72 | function calmDown() { 73 | if (0 < happy) { 74 | happy --; 75 | } 76 | } 77 | function getAngrier() { 78 | if (MIN_MOOD < happy) { 79 | happy --; 80 | } 81 | } 82 | function getExcited() { 83 | if (excited < 0) { 84 | excited += 2; 85 | } else if (excited < MAX_MOOD) { 86 | excited += 0.5; 87 | } 88 | if (happy < 0) { 89 | happy += 3; 90 | } 91 | timeSinceAnythingHappened = 0; 92 | } 93 | function getBored() { 94 | if (EXCITED_THRESHOLD < excited) { 95 | excited -= 2; 96 | } else if (MIN_MOOD < excited) { 97 | excited --; 98 | } 99 | } 100 | 101 | 102 | return { 103 | getHappier: getHappier, 104 | getAngrier: getAngrier, 105 | getExcited: getExcited, 106 | getBored: getBored, 107 | isHappy: function() { 108 | return HAPPY_THRESHOLD < happy; 109 | }, 110 | isAngry: function() { 111 | return happy < ANGRY_THRESHOLD; 112 | }, 113 | isExcited: function() { 114 | return EXCITED_THRESHOLD < excited; 115 | }, 116 | isBored: function() { 117 | return excited < BORED_THRESHOLD; 118 | }, 119 | startStroking: function() { 120 | isBeingStroked = true; 121 | }, 122 | stopStroking: function() { 123 | if (isBeingStroked) { 124 | isBeingStroked = false; 125 | happy = 18; 126 | } 127 | }, 128 | getMoodValue: function() { 129 | var happyVal = (happy - HAPPY_THRESHOLD) / (MAX_MOOD - HAPPY_THRESHOLD); 130 | var angryVal = (happy - ANGRY_THRESHOLD) / (MIN_MOOD - ANGRY_THRESHOLD); 131 | var excitedVal = (excited - EXCITED_THRESHOLD) / (MAX_MOOD - EXCITED_THRESHOLD); 132 | var boredVal = (excited - BORED_THRESHOLD) / (MIN_MOOD - BORED_THRESHOLD); 133 | return { 134 | happy: Math.max(happyVal, 0), 135 | angry: Math.max(angryVal, 0), 136 | excited: Math.max(excitedVal, 0), 137 | bored: Math.max(boredVal, 0) 138 | }; 139 | }, 140 | start: function() { 141 | emotionLoop(); 142 | }, 143 | stop: function() { 144 | $timeout.cancel(timerId); 145 | }, 146 | reset: function(){ 147 | $timeout.cancel(timerId); 148 | happy = 0; 149 | excited = 0; 150 | timeSinceAnythingHappened = 0; 151 | isBeingStroked = false; 152 | } 153 | }; 154 | }); -------------------------------------------------------------------------------- /src/common/services/catFactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 08/03/14. 3 | */ 4 | angular.module('drawACat.common.services') 5 | 6 | /** 7 | * This is the service that creates Cat objects. A Cat is a collections of specific Parts, such as head, body, rightLeg, leftLeg etc. Each part has an 8 | * associated behaviour object that defines how that part responds to events such as user interaction. 9 | */ 10 | .factory('catFactory', function() { 11 | var Cat = function() { 12 | var bodyParts = {}; 13 | bodyParts.head = {}; 14 | bodyParts.eyesOpen = {}; 15 | bodyParts.eyesClosed = {}; 16 | bodyParts.mouthOpen = {}; 17 | bodyParts.mouthClosed = {}; 18 | bodyParts.body = {}; 19 | bodyParts.leftLeg = {}; 20 | bodyParts.rightLeg = {}; 21 | this.bodyParts = bodyParts; 22 | }; 23 | 24 | /** 25 | * Get a path array for the whole cat, which concatenates each bodyPart path into one big array. 26 | * Omits the closedEyes and the openMouth parts from the path, and orders the layers in the correct 27 | * sequence for rendering. 28 | * @returns {Array} 29 | */ 30 | Cat.prototype.getPath = function() { 31 | 32 | // cause the parts to be rendered in a specific sequence, so that 33 | // the body is at the back, the head is on top of the body etc. 34 | var sequence = [ 35 | 'body', 36 | 'head', 37 | 'eyesOpen', 38 | 'mouthClosed', 39 | 'leftLeg', 40 | 'rightLeg' 41 | ]; 42 | var bodyParts = this.bodyParts; 43 | var path = []; 44 | angular.forEach(sequence, function(bodyPartName) { 45 | var bodyPart = bodyParts[bodyPartName]; 46 | if (bodyPart.part) { 47 | var partPath = bodyPart.part.getPath(); 48 | path = path.concat(partPath); 49 | } 50 | }); 51 | return path; 52 | }; 53 | 54 | /** 55 | * Adjust the position of the cat by setting the globalOffset on each of its parts. 56 | * @param x 57 | * @param y 58 | */ 59 | Cat.prototype.adjustPosition = function(x, y) { 60 | angular.forEach(this.bodyParts, function(bodyPart) { 61 | if (bodyPart.part) { 62 | bodyPart.part.setGlobalOffset(x, y); 63 | } 64 | }); 65 | }; 66 | 67 | Cat.prototype.getHeight = function() { 68 | var heightBoundaries = this.getBoundaries('y'); 69 | return heightBoundaries.max - heightBoundaries.min; 70 | }; 71 | 72 | Cat.prototype.getWidth = function() { 73 | var widthBoundaries = this.getBoundaries('x'); 74 | return widthBoundaries.max - widthBoundaries.min; 75 | }; 76 | 77 | /** 78 | * Calculate the dimension (x or y - width or height) of the entire cat. 79 | * @param dimension 80 | * @returns {{max: number, min: number}} 81 | */ 82 | Cat.prototype.getBoundaries = function(dimension) { 83 | var xOrY = dimension === 'x' ? 0 : 1; 84 | var path = this.getPath(); 85 | 86 | var maxCandidates = []; 87 | var minCandidates = []; 88 | 89 | angular.forEach(path, function(partPath) { 90 | // get the maximum value for each part 91 | var maxCandidate = partPath.reduce(function(max, arr) { 92 | return Math.max(max, arr[xOrY]); 93 | }, -Infinity); 94 | maxCandidates.push(maxCandidate); 95 | 96 | // get minimum values for each part 97 | var minCandidate = partPath.reduce(function(min, arr) { 98 | return Math.min(min, arr[xOrY]); 99 | }, Infinity); 100 | minCandidates.push(minCandidate); 101 | }); 102 | 103 | var max = Math.max.apply(null, maxCandidates); 104 | var min = Math.min.apply(null, minCandidates); 105 | 106 | return { 107 | max: max, 108 | min: min 109 | }; 110 | }; 111 | 112 | /** 113 | * Given window dimensions, resize the cat to fit, and reposition it to the centre of the window. 114 | * @param windowWidth 115 | * @param windowHeight 116 | */ 117 | Cat.prototype.resizeToWindow = function(windowWidth, windowHeight) { 118 | var ORIGINAL_CAT_DIMENSION = 500; 119 | var newDimension = Math.min(Math.min(windowWidth, windowHeight), ORIGINAL_CAT_DIMENSION); 120 | var scaleFactor = newDimension / ORIGINAL_CAT_DIMENSION; 121 | angular.forEach(this.bodyParts, function(bodyPart) { 122 | if (bodyPart.part) { 123 | bodyPart.part.setScale(scaleFactor); 124 | } 125 | }); 126 | 127 | var catWidth = this.getWidth(); 128 | var catHeight = this.getHeight(); 129 | var xAdjustment = (windowWidth / 2) - (catWidth / 2); 130 | var yAdjustment = windowHeight - (catHeight); 131 | this.adjustPosition(xAdjustment, yAdjustment); 132 | }; 133 | 134 | 135 | return { 136 | newCat: function() { 137 | return new Cat(); 138 | } 139 | }; 140 | } 141 | ); --------------------------------------------------------------------------------