diff --git a/TODO.md b/TODO.md index 41db1a9122..b41c31fb2f 100644 --- a/TODO.md +++ b/TODO.md @@ -44,6 +44,11 @@ Scaling 4. Prevent others from calling it 5. Caching strategy +Hardening + 1. Reduce API surface + 2. Limit length of input sentences + 3. Rate limiting + Presentation - Better frontend UI - Testing page (current frontend) diff --git a/deploy.sh b/deploy.sh index 39f6a68976..2e40e4f809 100755 --- a/deploy.sh +++ b/deploy.sh @@ -11,6 +11,7 @@ rsync -utrv dictionary bt@jungle.horse:/home/bt/dev/trumpet rsync -utrv config.toml bt@jungle.horse:/home/bt/dev/trumpet # Frontend +rsync -utrv web/images bt@jungle.horse:/home/bt/dev/trumpet/web rsync -utrv web/*css bt@jungle.horse:/home/bt/dev/trumpet/web rsync -utrv web/*html bt@jungle.horse:/home/bt/dev/trumpet/web pushd frontend diff --git a/dictionary/NEEDED.md b/dictionary/NEEDED.md index 45cc6ea4f6..a380f3ca50 100644 --- a/dictionary/NEEDED.md +++ b/dictionary/NEEDED.md @@ -6,6 +6,7 @@ banjo-tooie batshit charizard crafted +derping dipshit fam fearmongers diff --git a/frontend/animation.ts b/frontend/animation.ts new file mode 100644 index 0000000000..d1a5dbb4cf --- /dev/null +++ b/frontend/animation.ts @@ -0,0 +1,199 @@ +// Copyright (c) 2016 Brandon Thomas + +import PIXI = require('pixi.js'); + +// TODO: Clean up this abysmal garbage. +export function setup_animation() { + //let renderer = new PIXI.WebGLRenderer(800, 470); + let renderer = PIXI.autoDetectRenderer(800, 470); + renderer.backgroundColor = 0xffffff; + + document.getElementById('animation').appendChild(renderer.view); + + let stage = new PIXI.Container(); + + let asset: any = null; + let logo: any = null; + + (window).animationState = null; + let blinkingStart = null; + let talkingStart = null; + let start = null; + let curFrame = 0; + + function animate() { + // start the timer for the next animation loop + requestAnimationFrame(animate); + + if ((window).animationState === 'blinking') { + if (blinkingStart == null) { + blinkingStart = window.performance.now(); + curFrame = 0; + } else { + let delta = window.performance.now() - blinkingStart; + if (delta > 200) { + curFrame = 0; + blinkingStart = null; + (window).animationState = null; + } else if (delta > 150) { + curFrame = 4; + } else if (delta > 60) { + curFrame = 3; + } else if (delta > 20) { + curFrame = 2; + } else if (delta > 10) { + curFrame = 1; + } + } + } else if ((window).animationState === 'talking') { + if (talkingStart == null) { + talkingStart = window.performance.now(); + curFrame = 5; + } else { + let delta = window.performance.now() - talkingStart; + if (delta > 550) { + curFrame = 5; + talkingStart = null; + //(window).animationState = null; + } else if (delta > 580) { + curFrame = 6; + } else if (delta > 540) { + curFrame = 7; + } else if (delta > 500) { + curFrame = 8; + } else if (delta > 460) { + curFrame = 9; + } else if (delta > 420) { + curFrame = 10; + } else if (delta > 400) { + curFrame = 11; + } else if (delta > 360) { + curFrame = 12; + } else if (delta > 320) { + curFrame = 13; + } else if (delta > 280) { + curFrame = 12; + } else if (delta > 240) { + curFrame = 11; + } else if (delta > 200) { + curFrame = 10; + } else if (delta > 160) { + curFrame = 9; + } else if (delta > 120) { + curFrame = 8; + } else if (delta > 80) { + curFrame = 7; + } else if (delta > 40) { + curFrame = 6; + } + } + } else if ((window).animationState === 'stop_talking') { + if (curFrame > 13 || curFrame < 5) { + (window).animationState = null; + curFrame = 0; + } else { + if (start == null) { + start = window.performance.now(); + } + let delta = window.performance.now() - start; + if (delta > 40) { + curFrame -= 1; + start = window.performance.now(); + } + } + } + + asset.gotoAndStop(curFrame); + + // this is the main render call that makes pixi draw your container and its children. + renderer.render(stage); + } + + let idleImages = [ + '/assets/images/trumpette/idle_0.png', // 0 + '/assets/images/trumpette/idle_1.png', // 1 + '/assets/images/trumpette/idle_2.png', // 2 + '/assets/images/trumpette/idle_3.png', // 3 + '/assets/images/trumpette/idle_4.png', // 4 + ]; + + let talkImages = [ + '/assets/images/trumpette/talk_0.png', // 5 + '/assets/images/trumpette/talk_1.png', // 6 + '/assets/images/trumpette/talk_2.png', // 7 + '/assets/images/trumpette/talk_3.png', // 8 + '/assets/images/trumpette/talk_4.png', // 9 + '/assets/images/trumpette/talk_5.png', // 10 + '/assets/images/trumpette/talk_6.png', // 11 + '/assets/images/trumpette/talk_7.png', // 12 + '/assets/images/trumpette/talk_8.png', // 13 + ]; + + let idleTextures = []; + + for (let image of idleImages) { + let texture = PIXI.Texture.fromImage(image); + idleTextures.push(texture); + } + + for (let image of talkImages) { + let texture = PIXI.Texture.fromImage(image); + idleTextures.push(texture); + } + + logo = new PIXI.Text('Jungle Horse', + {font: '150px Impact Bold', fill: 0x000000, align: 'center'}); + + PIXI.loader.add('asset', '/assets/images/trumpette/idle_0.png') + .load(function (loader, resources) { + // This creates a texture from a 'asset.png' image. + //asset = new PIXI.Sprite(resources.asset.texture); + asset = new PIXI.extras.MovieClip(idleTextures); + + // Setup the position and scale of the asset + asset.position.x = 200; + asset.position.y = 0; + + asset.scale.x = 0.5; + asset.scale.y = 0.5; + + logo.position.y = 260; + + // Add the asset to the scene we are building. + stage.addChild(logo); + stage.addChild(asset); + + asset.play(); + // kick off the animation loop (defined below) + animate(); + }); + + blinkAnimation(); +} + +export function talk() { + (window).animationState = 'talking'; +} + +export function stopTalking() { + (window).animationState = 'stop_talking'; +} + +function blink() { + (window).animationState = 'blinking'; +} + +function blinkAnimation() { + if ((window).animationState == null) { + (window).animationState = 'blinking'; + } + let nextBlink = getRandomInt(1000, 4000); + setTimeout(blinkAnimation, nextBlink); +} + +function getRandomInt(min: number, max: number) : number { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; +} + diff --git a/frontend/audio.ts b/frontend/audio.ts index adca40c2f9..303c6172e6 100644 --- a/frontend/audio.ts +++ b/frontend/audio.ts @@ -2,6 +2,8 @@ declare var buzz: any; // TODO: static types. +import { talk, stopTalking } from "./animation"; + export default class Audio { sound: any; @@ -13,6 +15,15 @@ export default class Audio { this.stop(); this.sound = new buzz.sound(url); this.sound.play(); + + this.sound.bind('play playing', function() { + talk(); // UI + }); + + + this.sound.bind('ended pause', function() { + stopTalking(); // UI + }); } stop() { diff --git a/frontend/main.ts b/frontend/main.ts index 848a382a9e..ca0441f7df 100644 --- a/frontend/main.ts +++ b/frontend/main.ts @@ -1,19 +1,36 @@ // Copyright (c) 2016 Brandon Thomas +/// + +import Vue = require('vue'); + declare var $: any; +import { setup_animation } from "./animation"; import Audio from "./audio"; import { decode_url_hash, get_audio_api_url, set_url_hash } from "./url"; import { RawSentence, FilteredSentence } from "./sentence"; const INPUT = 'input#jungle'; +/*(window).V = new Vue({ + el: '#image-box', + data: { + content: 'foo', + }, +});*/ + $(function() { console.log('installing...'); - window.audio = new Audio(); + (window).audio = new Audio(); install_events(); initialize_from_url(); focus(); + + if ($('#animation').length > 0) { + // Only install animation if the page supports it. + setup_animation(); + } }); function install_events() { @@ -25,7 +42,7 @@ function install_events() { set_url_hash(filtered); let url = get_audio_api_url(filtered); - window.audio.playUrl(url); + (window).audio.playUrl(url); ev.preventDefault(); return false; @@ -36,7 +53,7 @@ function install_events() { // ESC key. focus(); $(INPUT).val(''); - window.audio.stop(); + (window).audio.stop(); } }); @@ -45,7 +62,7 @@ function install_events() { if (ev.keyCode === 27) { $(INPUT).val(''); // ESC key. - window.audio.stop(); + (window).audio.stop(); } }); @@ -57,7 +74,7 @@ function install_events() { $('button#clear').on('click', function(ev: any): any { $(INPUT).val(''); // ESC key. - window.audio.stop(); + (window).audio.stop(); ev.preventDefault(); return false; }); diff --git a/frontend/package.json b/frontend/package.json index bdcf222bac..6b08fa4d17 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,8 +2,10 @@ "dependencies": { "bower": "^1.7.9", "jquery": "^3.0.0", + "pixi.js": "^4.1.0", "ts-loader": "^0.8.2", "typescript": "^1.8.10", + "vue": "^1.0.28", "wavesurfer.js": "^1.1.12" } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000000..1e07597766 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,12 @@ +{ + "filesGlob": [ + "./**/*.ts", + "!./node_modules/**/*.ts" + ], + "files": [ + "./main.ts", + "./typings/pixi.js/pixi.js.d.ts", + "./typings/tsd.d.ts", + "./typings/vue/vue.d.ts" + ] +} diff --git a/frontend/tsd.json b/frontend/tsd.json index 389234e35c..d4a5aac2d5 100644 --- a/frontend/tsd.json +++ b/frontend/tsd.json @@ -7,6 +7,12 @@ "installed": { "jquery/jquery.d.ts": { "commit": "73110d92bdab2367e326f297bef3574285cba329" + }, + "vue/vue.d.ts": { + "commit": "3cc599268d2dabe3c3ee194e92965ee87440e9c3" + }, + "pixi.js/pixi.js.d.ts": { + "commit": "3cc599268d2dabe3c3ee194e92965ee87440e9c3" } } } diff --git a/frontend/typings.json b/frontend/typings.json new file mode 100644 index 0000000000..57c05035de --- /dev/null +++ b/frontend/typings.json @@ -0,0 +1,5 @@ +{ + "globalDependencies": { + "vue": "registry:dt/vue#1.0.21+20160929090511" + } +} diff --git a/frontend/url.ts b/frontend/url.ts index 76760c5782..93b63039bf 100644 --- a/frontend/url.ts +++ b/frontend/url.ts @@ -22,10 +22,10 @@ export function set_url_hash(sentence: FilteredSentence) { urlHash = ''; if (!json) { - urlHash = new URI('/').hash('').toString(); + urlHash = new URI('').hash('').toString(); } else { let uriEncoded = encodeURIComponent(json); - urlHash = new URI('/').hash(uriEncoded).toString(); + urlHash = new URI('').hash(uriEncoded).toString(); } window.history.replaceState(null, null, urlHash); diff --git a/web/demo.css b/web/demo.css index e2b4e3f4de..3d5c2fd862 100644 --- a/web/demo.css +++ b/web/demo.css @@ -23,6 +23,19 @@ h1 { text-align: center; } +#animation canvas { + display: block; + margin: auto; +} + +#logo { + display: none; + z-index: 100; + position: absolute; + top: 0; + width: 100%; +} + #image-box { width: 100%; } diff --git a/web/demo_jon.css b/web/demo_jon.css index b9bc76f1a9..4bff1dd3c0 100644 --- a/web/demo_jon.css +++ b/web/demo_jon.css @@ -1,28 +1,48 @@ -img#trump { - height: 300px; -} - -@media screen and (min-height: 600px) { - img#trump { - height: 400px; +@media screen and (min-width: 1px) { + #animation canvas { + width: 200px; } } -@media screen and (min-height: 800px) { - img#trump { - height: 500px; +@media screen and (min-width: 300px) { + #animation canvas { + width: 300px; } } -@media screen and (min-height: 1000px) { - img#trump { - height: 700px; +@media screen and (min-width: 400px) { + #animation canvas { + width: 400px; } } -@media screen and (min-height: 1500px) { - img#trump { - height: 1000px; +@media screen and (min-width: 450px) { + #animation canvas { + width: 450px; + } +} + +@media screen and (min-width: 500px) { + #animation canvas { + width: 500px; + } +} + +@media screen and (min-width: 600px) { + #animation canvas { + width: 600px; + } +} + +@media screen and (min-width: 800px) { + #animation canvas { + width: 800px; + } +} + +@media screen and (min-width: 1000px) { + #animation canvas { + width: 800px; } } diff --git a/web/demo_jon.html b/web/demo_jon.html index 19519df141..b197472d40 100644 --- a/web/demo_jon.html +++ b/web/demo_jon.html @@ -10,12 +10,15 @@ +
+
-

Jungle Horse

-
- -
+
@@ -32,7 +35,8 @@
-
+ +