diff --git a/public/images/ai-bot-closed.png b/public/images/ai-bot-closed.png index bf64e52c..d45ee573 100644 Binary files a/public/images/ai-bot-closed.png and b/public/images/ai-bot-closed.png differ diff --git a/public/index.html b/public/index.html index 82dc97b8..55d349c5 100644 --- a/public/index.html +++ b/public/index.html @@ -4,12 +4,6 @@ ML Activities Playground -
-
+
-
- -
- -
diff --git a/src/MLActivities.jsx b/src/MLActivities.jsx deleted file mode 100644 index 5a93e00f..00000000 --- a/src/MLActivities.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import RPS from './activities/rps/RPS'; -import ImageRecognition from './activities/imageRecognition/ImageRecognition'; -import Button from 'react-bootstrap/lib/Button'; -import Row from 'react-bootstrap/lib/Row'; -import Col from 'react-bootstrap/lib/Col'; -import Grid from 'react-bootstrap/lib/Grid'; -import Panel from 'react-bootstrap/lib/Panel'; - -const Activity = Object.freeze({ - None: 0, - RPS: 1, - ImageRecognition: 2 -}); - -module.exports = class MLActivities extends React.Component { - state = { - currentActivity: Activity.None - }; - - render() { - return ( - - - - -

ML Activities Playground

- {this.state.currentActivity !== Activity.None && ( - - )} - {this.state.currentActivity === Activity.None && ( -
- - -
- )} - {this.state.currentActivity === Activity.RPS && ( - - - - )} - {this.state.currentActivity === Activity.ImageRecognition && ( - - - - )} - - -
-
- ); - } -}; diff --git a/src/activities/imageRecognition/Draggable.jsx b/src/activities/imageRecognition/Draggable.jsx deleted file mode 100644 index 8bbcd8a6..00000000 --- a/src/activities/imageRecognition/Draggable.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import $ from 'jquery'; -import 'jquery-ui/ui/widgets/draggable'; -import * as PropTypes from 'react/lib/ReactPropTypes'; -window.jQuery = $; -require('jquery-ui-touch-punch'); - -module.exports = class ImageRecognition extends React.Component { - componentDidMount() { - $(this.draggableDiv).draggable({revert: true}); - } - - render() { - return ( -
(this.draggableDiv = element)} - > - {this.props.children} -
- ); - } -}; - -module.exports.propTypes = { - children: PropTypes.node, - guid: PropTypes.string -}; diff --git a/src/activities/imageRecognition/Droppable.jsx b/src/activities/imageRecognition/Droppable.jsx deleted file mode 100644 index b70676ef..00000000 --- a/src/activities/imageRecognition/Droppable.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import $ from 'jquery'; -import 'jquery-ui/ui/effects/effect-drop'; -import 'jquery-ui/ui/widgets/droppable'; -import * as PropTypes from 'react/lib/ReactPropTypes'; -window.jQuery = $; -require('jquery-ui-touch-punch'); - -module.exports = class ImageRecognition extends React.Component { - componentDidMount() { - $(this.droppableDiv).droppable({ - classes: { - 'ui-droppable-active': 'ui-state-active', - 'ui-droppable-hover': 'ui-state-hover' - }, - tolerance: 'touch', - drop: (event, ui) => { - this.props.onDrop(ui.draggable.data('guid')); - } - }); - } - - render() { - return ( -
(this.droppableDiv = element)} - > - {this.props.children} -
- ); - } -}; - -module.exports.propTypes = { - children: PropTypes.node, - onDrop: PropTypes.func -}; diff --git a/src/activities/imageRecognition/ImageRecognition.jsx b/src/activities/imageRecognition/ImageRecognition.jsx deleted file mode 100644 index c3bfe012..00000000 --- a/src/activities/imageRecognition/ImageRecognition.jsx +++ /dev/null @@ -1,320 +0,0 @@ -import React from 'react'; -import SimpleTrainer from '../../utils/SimpleTrainer'; -import Draggable from './Draggable'; -import Droppable from './Droppable'; -import TrainingImageUpload from './TrainingImageUpload'; -import PredictionUpload from './PredictionUpload'; -import Row from 'react-bootstrap/lib/Row'; -import Col from 'react-bootstrap/lib/Col'; -import Button from 'react-bootstrap/lib/Button'; - -const ActivityState = Object.freeze({ - Loading: 0, - Training: 1, - Playing: 2 -}); - -const NO_PREDICTION = -1; -const defaultState = { - classes: [ - { - name: 'Dogs', - examples: 0 - }, - { - name: 'Cats', - examples: 0 - } - ], - activityState: ActivityState.Loading, - predictedClass: NO_PREDICTION -}; - -const activityImages = [ - {guid: 'a', url: 'images/dog1.png'}, - {guid: 'b', url: 'images/dog2.png'}, - {guid: 'c', url: 'images/dog3.png'}, - {guid: 'd', url: 'images/cat1.jpg'}, - {guid: 'e', url: 'images/cat2.jpg'}, - {guid: 'f', url: 'images/cat3.jpg'} -]; - -const IMAGE_SIZE = 227; - -function loadImage(url, size) { - return new Promise(resolve => { - const image = new Image(); - image.width = size; - image.height = size; - image.addEventListener('load', () => { - resolve(image); - }); - image.src = url; - }); -} - -module.exports = class ImageRecognition extends React.Component { - constructor(props) { - super(props); - this.simpleTrainer = new SimpleTrainer(); - } - - state = defaultState; - - componentDidMount() { - this.simpleTrainer.initializeClassifiers().then(() => { - this.setState({activityState: ActivityState.Training}); - }); - } - - render() { - if (this.state.activityState === ActivityState.Loading) { - return
Loading machine learning model data...
; - } - - return ( -
- {this.state.activityState === ActivityState.Training && ( -
- - - {this.state.classes.map((classData, i) => { - return ( - { - this.simpleTrainer.addExampleImage(image, i); - this.updateExampleCounts(i); - }} - /> - ); - })} - - - - -

Drag images to train your machine learning algorithm

- -
- - - - - - - - {activityImages.map((image, i) => { - return ( - - { - // loadImage(image.url, IMAGE_SIZE).then((img) => { - // this.simpleTrainer.predictFromImage(img).then((result) => { - // console.log(result); - // }); - // }); - // }} - src={image.url} - className="thumbnail" - style={{ - display: 'inline-block' - }} - width={100} - height={100} - /> - - ); - })} - - - - {this.state.classes.map((classData, i) => { - return ( - - { - const image = activityImages.find(e => { - return e.guid === guid; - }); - loadImage(image.url, IMAGE_SIZE).then(image => { - this.simpleTrainer.addExampleImage(image, i); - this.updateExampleCounts(i); - }); - }} - > - {classData.name} - -
- {classData.examples.toString()} -
- - ); - })} -
-
- )} - {this.state.activityState === ActivityState.Training && ( - - - - - - - )} - {this.state.activityState === ActivityState.Playing && ( -
- - - { - this.simpleTrainer.predictFromImage(img).then(trainingResult => { - this.setState({trainingResult}); - }); - }} - /> - - - - -

Tap an image to classify it

- {activityImages.map((image, i) => { - return ( - { - loadImage(image.url, IMAGE_SIZE).then(img => { - this.simpleTrainer.predictFromImage(img).then(result => { - this.setState({ - trainingResult: result - }); - }); - }); - }} - width={100} - height={100} - /> - ); - })} - -
-
- )} - {this.state.activityState === ActivityState.Playing && - !!this.state.trainingResult && ( - - -

Predicted Category:

-

- { - this.state.classes[ - this.state.trainingResult.predictedClassId - ].name - } -

- - {JSON.stringify(this.state.trainingResult)} - -
- )} - {this.state.activityState === ActivityState.Playing && ( - - - - - - )} -
- ); - } - - resetAllExampleCounts() { - const classes = this.state.classes; - this.state.classes.forEach((c, i) => { - classes[i].examples = 0; - }); - this.setState({classes: classes}); - } - - async updateExampleCounts(i) { - const classes = this.state.classes; - classes[i].examples = this.simpleTrainer.getExampleCount(i); - return this.setState({classes: classes}); - } - - async playRound() { - if (this.video.isPlaying()) { - if (this.simpleTrainer.getNumClasses() > 0) { - let frameDataURI = this.video.getFrameDataURI(400); - - let predictionResult = await this.simpleTrainer.predictFromImage( - this.video.getVideoElement() - ); - this.setState( - { - lastPrediction: { - predictedClass: predictionResult.predictedClassId, - confidence: - predictionResult.confidencesByClassId[ - predictionResult.predictedClassId - ], - playerPlayedImage: frameDataURI, - confidencesByClassId: predictionResult.confidencesByClassId - } - }, - null - ); - } - } - } -}; diff --git a/src/activities/imageRecognition/PredictionUpload.jsx b/src/activities/imageRecognition/PredictionUpload.jsx deleted file mode 100644 index d9a7e7af..00000000 --- a/src/activities/imageRecognition/PredictionUpload.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, {PropTypes} from 'react'; - -module.exports = class SingleUpload extends React.Component { - static propTypes = { - predictClass: PropTypes.func - }; - - onUpload() { - var file = document.getElementById('predictfile').files[0]; - if (!file) { - return; - } - var url = URL.createObjectURL(file), // create an Object URL - img = new Image(); // create a temp. image object - - var _this = this; - img.onload = function() { - _this.props.predictClass(img); - }; - - img.src = url; // start convertion file - } - - render() { - return ( -
- Upload your own: - this.onUpload()} /> -
- ); - } -}; diff --git a/src/activities/imageRecognition/TrainingImageUpload.jsx b/src/activities/imageRecognition/TrainingImageUpload.jsx deleted file mode 100644 index 207d33ee..00000000 --- a/src/activities/imageRecognition/TrainingImageUpload.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, {PropTypes} from 'react'; - -module.exports = class MultiUpload extends React.Component { - static propTypes = { - className: PropTypes.string, - addTrainingExample: PropTypes.func - }; - - onUpload() { - var files = document.getElementById(this.props.className + 'file').files; - for (var i = 0; i < files.length; ++i) { - var file = files[i]; - if (!file) { - return; - } - var url = URL.createObjectURL(file), // create an Object URL - img = new Image(); // create a temp. image object - var _this = this; - img.onload = function() { - // The height and width doesn't always load so set them if they're 0 - if (!img.width) { - img.width = 500; - } - if (!img.height) { - img.height = 500; - } - _this.props.addTrainingExample(img); - URL.revokeObjectURL(url); - }; - - img.src = url; // start convertion file - } - } - - render() { - return ( -
- {'Upload your own ' + this.props.className + ':'} - this.onUpload()} - multiple - /> -
- ); - } -}; diff --git a/src/activities/rps/IntroScreen.jsx b/src/activities/rps/IntroScreen.jsx deleted file mode 100644 index 0575ebaf..00000000 --- a/src/activities/rps/IntroScreen.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import Col from 'react-bootstrap/lib/Col'; -import Row from 'react-bootstrap/lib/Row'; -import Button from 'react-bootstrap/lib/Button'; -import * as PropTypes from 'react/lib/ReactPropTypes'; - -module.exports = class IntroScreen extends React.Component { - constructor(props) { - super(props); - } - - render() { - return ( - - -

Train a computer to “see” Rock, Paper, Scissors

-

- On the next screen, you’ll be prompted to let your browser to access - the camera. Please click “Allow” -

- -

- If your device doesn’t have a camera: -

-

- Visit code.org/ai from using a modern smartphone that has a camera. -

-

- What is the camera access for?: -

-

You’ll be “training” a computer vision algorithm.

-

- None of the images seen by the camera will ever leave your computer - or be shared or stored by anybody. They’ll be used only for this - tutorial and deleted after you leave this web page. -

- -
- ); - } -}; - -module.exports.propTypes = { - onClickContinue: PropTypes.func -}; diff --git a/src/activities/rps/PlayRound.jsx b/src/activities/rps/PlayRound.jsx deleted file mode 100644 index 1ffde6b0..00000000 --- a/src/activities/rps/PlayRound.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import Col from 'react-bootstrap/lib/Col'; -import Row from 'react-bootstrap/lib/Row'; -import * as PropTypes from 'react/lib/ReactPropTypes'; - -module.exports = class PlayRound extends React.Component { - constructor(props) { - super(props); - } - - state = { - countdownNumber: 3 - }; - - componentDidMount() { - setTimeout(() => this.decrementCountdown(), 1000); - setTimeout(() => this.decrementCountdown(), 2000); - setTimeout(() => this.decrementCountdown(), 3000); - setTimeout(() => this.props.onPlayRound(), 3500); - } - - componentWillUnmount() { - console.log('unounting'); - this.hasMounted = false; - } - - decrementCountdown() { - this.setState({countdownNumber: this.state.countdownNumber - 1}); - } - - render() { - return ( - - - - ); - } -}; - -module.exports.propTypes = { - onPlayRound: PropTypes.func, - onMountVideo: PropTypes.func, - imageSize: PropTypes.number -}; diff --git a/src/activities/rps/PlayRoundInstructions.jsx b/src/activities/rps/PlayRoundInstructions.jsx deleted file mode 100644 index 4a3e86ce..00000000 --- a/src/activities/rps/PlayRoundInstructions.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import Col from 'react-bootstrap/lib/Col'; -import Row from 'react-bootstrap/lib/Row'; -import Button from 'react-bootstrap/lib/Button'; -import * as PropTypes from 'react/lib/ReactPropTypes'; - -module.exports = class PlayRoundInstructions extends React.Component { - constructor(props) { - super(props); - } - - render() { - return ( -
- - -

- Have you provided enough training data? -

-

- Did you take enough photos for the machine learning algorithm to - distinguish ROCK from PAPER from SCISSORS? Let’s find out! -

- -
- - - - - - - -

- If the computer vision doesn’t seem to work very well, try going - back to Train more. -

-

- If it works well, see if it can recognize somebody else’s hand, - especially somebody with different skin color. -

- -
-
- ); - } -}; - -module.exports.propTypes = { - onClickContinue: PropTypes.func -}; diff --git a/src/activities/rps/PlayRoundResult.jsx b/src/activities/rps/PlayRoundResult.jsx deleted file mode 100644 index 857b4d36..00000000 --- a/src/activities/rps/PlayRoundResult.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import Col from 'react-bootstrap/lib/Col'; -import Button from 'react-bootstrap/lib/Button'; -import Row from 'react-bootstrap/lib/Row'; -import * as PropTypes from 'react/lib/ReactPropTypes'; - -module.exports = class PlayRound extends React.Component { - constructor(props) { - super(props); - } - - componentDidMount() {} - - render() { - return ( -
- - -

- {this.props.winner > 0 - ? 'YOU WIN! 🙌' - : this.props.winner === 0 - ? 'DRAW 🤷‍♀️' - : 'YOU LOSE 😭'} -

- -
- - -

- You: -

- -

- {this.props.playerPlayed} ( - {Math.floor(this.props.confidence * 100)}% sure) -

- - -

- Computer: -

-

- {this.props.computerPlayedEmoji} -

-

{this.props.computerPlayed}

- -
- - - - - - - - - - - - - -
- ); - } -}; - -module.exports.propTypes = { - onPlayAgain: PropTypes.func, - onTrainMore: PropTypes.func, - onContinue: PropTypes.func, - roundResult: PropTypes.object, - - confidence: PropTypes.number, - - playerPlayed: PropTypes.string, - playerPlayedImage: PropTypes.string, - computerPlayed: PropTypes.string, - computerPlayedEmoji: PropTypes.string, - - winner: PropTypes.number -}; diff --git a/src/activities/rps/RPS.jsx b/src/activities/rps/RPS.jsx deleted file mode 100644 index 32707777..00000000 --- a/src/activities/rps/RPS.jsx +++ /dev/null @@ -1,246 +0,0 @@ -/** - * RPS Activity. Based off of: - * - https://github.com/ryansloan/rps-ml - * - https://github.com/googlecreativelab/teachable-machine-boilerplate - */ - -import React from 'react'; - -import SimpleTrainer from '../../utils/SimpleTrainer'; -import Video from '../../utils/Video.js'; - -import IntroScreen from './IntroScreen'; -import TrainingScreen from './TrainingScreen'; -import PlayRound from './PlayRound'; -import PlayRoundInstructions from './PlayRoundInstructions'; -import PlayRoundResult from './PlayRoundResult'; - -const IMAGE_SIZE = 227; -const CLASS_NAMES = ['rock', 'paper', 'scissors']; - -const NO_CLASS = -1; - -const ActivityScreen = Object.freeze({ - IntroInstructions: 0, - TrainClass1: 1, - TrainClass2: 2, - TrainClass3: 3, - PlayRoundInstructions: 4, - PlayRound: 5, - PlayRoundResult: 6 -}); - -const defaultState = { - currentScreen: ActivityScreen.IntroInstructions, - predictedClass: NO_CLASS, - confidencesByClassId: [], - trainingImages0: [], - trainingImages1: [], - trainingImages2: [], - roundResult: null, - roundPrediction: null -}; -module.exports = class Main extends React.Component { - constructor(props) { - super(props); - this.video = new Video(IMAGE_SIZE); - this.simpleTrainer = new SimpleTrainer(); - } - - state = defaultState; - - componentDidMount() { - this.simpleTrainer.initializeClassifiers(); - } - - rpsToEmoji(rps) { - switch (rps) { - case 'rock': - return '✊'; - case 'scissors': - return '✌'; - default: - return '✋'; - } - } - - render() { - return ( -
- {this.state.currentScreen === ActivityScreen.IntroInstructions && ( - { - this.setState( - { - currentScreen: ActivityScreen.TrainClass1 - }, - null - ); - }} - /> - )} - {this.trainingScreen(0)} - {this.trainingScreen(1)} - {this.trainingScreen(2)} - {this.state.currentScreen === ActivityScreen.PlayRoundInstructions && ( - { - this.setState( - { - currentScreen: ActivityScreen.PlayRound - }, - null - ); - }} - /> - )} - {this.state.currentScreen === ActivityScreen.PlayRound && ( - { - this.video.loadVideo(videoElement); - }} - onPlayRound={() => { - this.playRound().then(() => { - return this.setState( - { - currentScreen: ActivityScreen.PlayRoundResult - }, - null - ); - }); - }} - /> - )} - {this.state.currentScreen === ActivityScreen.PlayRoundResult && ( - { - this.setState( - { - currentScreen: ActivityScreen.PlayRound - }, - null - ); - }} - onTrainMore={() => { - this.setState( - { - currentScreen: ActivityScreen.TrainClass1 - }, - null - ); - }} - onContinue={() => { - this.setState(defaultState, null); - }} - /> - )} -
- ); - } - - trainingScreen(index) { - const thisScreen = ActivityScreen[`TrainClass${index + 1}`]; - const nextScreen = - index + 1 >= CLASS_NAMES.length - ? ActivityScreen.PlayRoundInstructions - : ActivityScreen[`TrainClass${index + 2}`]; - return ( - this.state.currentScreen === thisScreen && ( - { - this.trainExample(index); - }} - onContinueClicked={() => { - this.setState( - { - currentScreen: nextScreen - }, - null - ); - }} - onMountVideo={videoElement => { - this.video.loadVideo(videoElement); - }} - imageSize={IMAGE_SIZE} - trainingClass={CLASS_NAMES[index]} - exampleCount={this.simpleTrainer.getExampleCount(index)} - trainingImages={this.state[`trainingImages${index}`]} - /> - ) - ); - } - - /** - * @param {number} index - * @returns {Promise} - */ - async trainExample(index) { - if (this.video.isPlaying()) { - this.simpleTrainer.addExampleImage(this.video.getVideoElement(), index); - this.setState({ - ['trainingImages' + index]: this.state['trainingImages' + index].concat( - this.video.getFrameDataURI() - ) - }); - } - } - - async playRound() { - if (this.video.isPlaying()) { - if (this.simpleTrainer.getNumClasses() > 0) { - let frameDataURI = this.video.getFrameDataURI(400); - - let predictionResult = await this.simpleTrainer.predictFromImage( - this.video.getVideoElement() - ); - let computerChoice = CLASS_NAMES[Math.floor(Math.random() * 3)]; - let playerChoice = CLASS_NAMES[predictionResult.predictedClassId]; - const winner = this.pickWinner(playerChoice, computerChoice); - - this.setState( - { - roundPrediction: { - predictedClass: predictionResult.predictedClassId, - confidence: - predictionResult.confidencesByClassId[ - predictionResult.predictedClassId - ], - playerPlayedImage: frameDataURI, - - playerPlayed: playerChoice, - computerPlayed: computerChoice, - computerPlayedEmoji: this.rpsToEmoji(computerChoice), - - confidencesByClassId: predictionResult.confidencesByClassId - }, - roundResult: { - winner: winner - } - }, - null - ); - } - } - } - - pickWinner(playerChoice, computerChoice) { - return this.beats(playerChoice, computerChoice); - } - - beats(x, y) { - const keyBeatsValue = { - rock: 'scissors', - paper: 'rock', - scissors: 'paper' - }; - - return keyBeatsValue[x] === y ? 1 : keyBeatsValue[y] === x ? -1 : 0; - } -}; diff --git a/src/activities/rps/TrainingScreen.jsx b/src/activities/rps/TrainingScreen.jsx deleted file mode 100644 index cf0cf9c7..00000000 --- a/src/activities/rps/TrainingScreen.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import Col from 'react-bootstrap/lib/Col'; -import Button from 'react-bootstrap/lib/Button'; -import Row from 'react-bootstrap/lib/Row'; -import * as PropTypes from 'react/lib/ReactPropTypes'; - -module.exports = class TrainingScreen extends React.Component { - constructor(props) { - super(props); - } - - componentWillUnmount() { - this.hasMounted = false; - } - - render() { - return ( -
- - -

- - “Train” the computer to see{' '} - {this.props.trainingClass.endsWith('s') ? '' : 'a'}{' '} - {this.props.trainingClass} - -

-

- Show a rock, and click Train to take photos, so the machine - learning algorithm can “learn” how to recognize a rock. -

- -
- - - - - - - - - - - {this.props.trainingImages.map((image, i) => { - return ( - - ); - })} - - - {this.props.trainingImages && this.props.trainingImages.length > 0 && ( - - - - - - )} -
- ); - } -}; - -module.exports.propTypes = { - onMountVideo: PropTypes.func, - onTrainClicked: PropTypes.func, - onContinueClicked: PropTypes.func, - imageSize: PropTypes.number, - trainingClass: PropTypes.string, - - exampleCount: PropTypes.number, - trainingImages: PropTypes.arrayOf(PropTypes.string) -}; diff --git a/src/demo/helpers.js b/src/demo/helpers.js index bd6a77cd..c9b406b4 100644 --- a/src/demo/helpers.js +++ b/src/demo/helpers.js @@ -9,6 +9,9 @@ export const backgroundPathForMode = mode => { imgName = 'underwater'; } + // Temporarily show background for every mode. + imgName = 'underwater'; + return imgName ? backgroundPath(imgName) : null; }; diff --git a/src/demo/index.jsx b/src/demo/index.jsx index bc163147..3901928e 100644 --- a/src/demo/index.jsx +++ b/src/demo/index.jsx @@ -1,28 +1,51 @@ import $ from 'jquery'; import constants, {Modes} from './constants'; -import {setState} from './state'; +import {setState, setSetStateCallback} from './state'; import {init as initScene} from './init'; import {render} from './renderer'; +import 'babel-polyfill'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import UI from './ui'; + $(document).ready(() => { - // Set up initial state + // Set up canvases. const canvas = document.getElementById('activity-canvas'); const backgroundCanvas = document.getElementById('background-canvas'); canvas.width = backgroundCanvas.width = constants.canvasWidth; canvas.height = backgroundCanvas.height = constants.canvasHeight; + // Temporarily use URL parameter to set some state. + const smallWordSet = window.location.href.indexOf("words=small") !== -1; + + // Set initial state for UI elements. setState({ currentMode: Modes.Loading, canvas, backgroundCanvas, uiContainer: document.getElementById('ui-container'), headerContainer: document.getElementById('header-container'), - footerContainer: document.getElementById('footer-container') + footerContainer: document.getElementById('footer-container'), + smallWordSet: smallWordSet }); + // Initialize our first model. initScene(); - // Start the renderer. It will self-perpetute by calling + // Start the canvas renderer. It will self-perpetute by calling // requestAnimationFrame on itself. render(); + + // Render the UI. + renderUI(); + + // And have the render UI handler be called every time state is set. + setSetStateCallback(renderUI); }); + +// Tell React to explicitly render the UI. +export const renderUI = () => { + const renderElement = document.getElementById('container-react'); + ReactDOM.render(, renderElement); +}; diff --git a/src/demo/models/train.js b/src/demo/models/train.js index c0547d3f..352106ca 100644 --- a/src/demo/models/train.js +++ b/src/demo/models/train.js @@ -71,7 +71,7 @@ const uiElements = state => { ]; }; -const onClassifyFish = doesLike => { +export const onClassifyFish = doesLike => { const state = getState(); // No-op if animation is currently in progress. diff --git a/src/demo/renderer.js b/src/demo/renderer.js index c157e5c7..1bef3ffc 100644 --- a/src/demo/renderer.js +++ b/src/demo/renderer.js @@ -166,7 +166,7 @@ const getOffsetForTime = (t, totalFish) => { let amount = t / moveTime; // Apply an S-curve to that amount. - amount = amount - Math.sin(amount*2*Math.PI) / (2*Math.PI); + amount = amount - Math.sin(amount * 2 * Math.PI) / (2 * Math.PI); return ( constants.fishCanvasWidth * totalFish - @@ -195,7 +195,7 @@ const getYForFish = (numFish, fishIdx, state, offsetX, predictedClassId) => { // Move fish down a little on predict screen. if (state.currentMode === Modes.Predicting) { - y += 100; + y += 130; // And drop the fish down even more if they are not liked. const doesLike = predictedClassId === ClassType.Like; @@ -207,6 +207,12 @@ const getYForFish = (numFish, fishIdx, state, offsetX, predictedClassId) => { y += screenX - midScreenX; } } + + // And sway fish vertically on the predicting screen. + const swayValue = + (($time() * 360) / (20 * 1000) + (fishIdx + 1) * 10) % 360; + const swayOffsetY = Math.sin(((swayValue * Math.PI) / 180) * 6) * 8; + y += swayOffsetY; } return y; @@ -248,7 +254,11 @@ const drawMovingFish = state => { if (state.currentMode === Modes.Predicting) { if (fish.result) { - drawPrediction(fish.result.predictedClassId, state.word, x, y, ctx); + const midScreenX = + constants.canvasWidth / 2 - constants.fishCanvasWidth / 2; + if (x > midScreenX) { + drawPrediction(fish.result.predictedClassId, state.word, x, y, ctx); + } } else { predictFish(state, i).then(prediction => { fish.result = prediction; @@ -289,7 +299,7 @@ const drawFrame = state => { size, size, '#F0F0F0', - '#000000' + '#F0F0F0' ); }; @@ -339,9 +349,10 @@ const drawPondFishImages = () => { const canvas = getState().canvas; const ctx = canvas.getContext('2d'); getState().pondFish.forEach(fish => { - var swayValue = (($time() * 360) / (20 * 1000) + (fish.id + 1) * 10) % 360; - var swayOffsetX = Math.sin(((swayValue * Math.PI) / 180) * 2) * 120; - var swayOffsetY = Math.sin(((swayValue * Math.PI) / 180) * 6) * 8; + const swayValue = + (($time() * 360) / (20 * 1000) + (fish.id + 1) * 10) % 360; + const swayOffsetX = Math.sin(((swayValue * Math.PI) / 180) * 2) * 120; + const swayOffsetY = Math.sin(((swayValue * Math.PI) / 180) * 6) * 8; drawSingleFish(fish, fish.x + swayOffsetX, fish.y + swayOffsetY, ctx); }); @@ -445,8 +456,8 @@ export const clearCanvas = canvas => { // Draw an overlay over the whole scene. Used for fades. function drawOverlays() { - var duration = $time() - currentModeStartTime; - var amount = 1 - duration / 800; + const duration = $time() - currentModeStartTime; + let amount = 1 - duration / 800; if (amount < 0) { amount = 0; } diff --git a/src/demo/state.js b/src/demo/state.js index 708edf9f..20617f00 100644 --- a/src/demo/state.js +++ b/src/demo/state.js @@ -1,3 +1,5 @@ +let setStateCallback = null; + const initialState = { currentMode: null, fishData: [], @@ -26,5 +28,14 @@ export const getState = function() { export const setState = function(newState) { state = {...state, ...newState}; + + if (setStateCallback) { + setStateCallback(); + } + return state; }; + +export const setSetStateCallback = (callback) => { + setStateCallback = callback; +} diff --git a/src/demo/ui.jsx b/src/demo/ui.jsx new file mode 100644 index 00000000..a1f91a44 --- /dev/null +++ b/src/demo/ui.jsx @@ -0,0 +1,416 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {getState, setState} from './state'; +import {Modes} from './constants'; +import {toMode} from './helpers'; +import {init as initScene} from './init'; +import {onClassifyFish} from './models/train'; + +const styles = { + header: { + position: 'absolute', + top: 10, + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 48 + }, + footer: { + position: 'absolute', + bottom: 0, + width: '100%', + display: 'flex', + justifyContent: 'space-between' + }, + body: { + position: 'relative', + width: '100%', + paddingTop: '56.25%' // for 16:9 + }, + content: { + position: 'absolute', + top: '10%', + left: 0, + width: '100%' + }, + button: { + cursor: 'pointer' + }, + continueButton: { + marginLeft: 'auto', + marginRight: 10, + marginBottom: 10 + }, + button1col: { + width: '20%', + display: 'block', + margin: '0 auto', + marginTop: '2%', + marginBottom: '2%' + }, + button3col: { + width: '20%', + marginLeft: '6%', + marginRight: '6%', + marginTop: '2%', + marginBottom: '2%' + }, + activityIntroText: { + position: 'absolute', + fontSize: 22, + top: '20%', + left: '50%', + width: '80%', + transform: 'translateX(-50%)', + textAlign: 'center' + }, + trainingIntroBot: { + position: 'absolute', + transform: 'translateX(-50%)', + top: '30%', + left: '50%' + }, + activityIntroBot: { + position: 'absolute', + transform: 'translateX(-50%)', + top: '50%', + left: '50%' + }, + wordsText: { + textAlign: 'center', + marginTop: 20, + fontSize: 22 + }, + trainQuestionText: { + position: 'absolute', + top: '18%', + left: '50%', + transform: 'translateX(-50%)', + fontSize: 22 + }, + trainQuestionTextDisabled: { + position: 'absolute', + top: '18%', + left: '50%', + transform: 'translateX(-50%)', + fontSize: 22, + opacity: 0.5 + }, + trainButtonYes: { + position: 'absolute', + top: '80%', + left: '30%' + }, + trainButtonNo: { + position: 'absolute', + top: '80%', + left: '60%' + }, + trainBot: { + position: 'absolute', + height: '50%', + top: '20%', + left: '70%' + }, + predictBot: { + position: 'absolute', + height: '50%', + top: '2%', + left: '50%', + transform: 'translateX(-50%)' + }, + pondText: { + position: 'absolute', + bottom: '3%', + left: '55%', + transform: 'translateX(-45%)', + fontSize: 22, + width: '70%', + backgroundColor: 'rgba(0,0,0,0.5)', + padding: '2%', + borderRadius: 10, + color: 'white', + lineHeight: '32px' + }, + pondBot: { + position: 'absolute', + height: '50%', + left: 0, + bottom: 0 + } +}; + +class Body extends React.Component { + static propTypes = { + children: PropTypes.node + }; + + render() { + return
{this.props.children}
; + } +} + +class Header extends React.Component { + static propTypes = { + children: PropTypes.node + }; + + render() { + return
{this.props.children}
; + } +} + +class Content extends React.Component { + static propTypes = { + children: PropTypes.node + }; + + render() { + return
{this.props.children}
; + } +} + +class Footer extends React.Component { + static propTypes = { + children: PropTypes.node + }; + + render() { + return
{this.props.children}
; + } +} + +class Button extends React.Component { + static propTypes = { + style: PropTypes.object, + children: PropTypes.node, + onClick: PropTypes.func + }; + + render() { + return ( + + ); + } +} + +class ActivityIntro extends React.Component { + render() { + return ( +
+ +
Meet A.I.
+
+ Machine learning and Artificial Intelligence (AI) can give + recommendations, like when a computer suggests videos to watch or + products to buy. What else can we teach a computer? +
+
+ Next, you’re going to teach A.I. a new word just by showing examples + of that type of fish. +
+ +
+ +
+ +
+ ); + } +} + +class Words extends React.Component { + items = [ + ['Blue', 'Green', 'Red', 'Round', 'Square'], + [ + 'Friendly', + 'Funny', + 'Bizarre', + 'Shy', + 'Glitchy', + 'Delicious', + 'Fun', + 'Angry', + 'Fast', + 'Smart', + 'Brave', + 'Scary', + 'Wild', + 'Fierce', + 'Tropical' + ] + ]; + + currentItems() { + const state = getState(); + const itemSet = state.smallWordSet ? 0 : 1; + + return this.items[itemSet]; + } + + onChangeWord(itemIndex) { + setState({ + word: this.currentItems()[itemIndex], + currentMode: Modes.TrainingIntro + }); + initScene(); + } + + render() { + const state = getState(); + const currentItems = this.currentItems(); + const buttonStyle = state.smallWordSet + ? styles.button1col + : styles.button3col; + + return ( + +
Choose Fish Type
+ +
+ What type of fish do you want to train A.I. to detect? +
+ {currentItems.map((item, itemIndex) => ( + + ))} +
+ + ); + } +} + +class TrainingIntro extends React.Component { + render() { + const state = getState(); + + return ( + +
+
+ Now let's teach A.I. what {state.word.toUpperCase()} fish look + like. +
+ +
+ +
+ + ); + } +} + +class Train extends React.Component { + render() { + const state = getState(); + const questionText = `Is this fish ${state.word.toUpperCase()}?`; + const trainQuestionTextStyle = state.isRunning + ? styles.trainQuestionTextDisabled + : styles.trainQuestionText; + + return ( + +
A.I. Training
+
{questionText}
+ + + +
+ +
+ + ); + } +} + +class Predict extends React.Component { + render() { + return ( + +
A.I. Sorting
+ +
+ +
+ + ); + } +} + +class Pond extends React.Component { + render() { + const state = getState(); + const pondText = `Out of ${ + state.fishData.length + } objects, A.I. identified ${ + state.pondFish.length + } that it classified as ${state.word.toUpperCase()}.`; + + return ( + +
A.I. Results
+
{pondText}
+ + + ); + } +} + +module.exports = class UI extends React.Component { + render() { + const mode = getState().currentMode; + + return ( +
+ {mode === Modes.ActivityIntro && } + {mode === Modes.Words && } + {mode === Modes.TrainingIntro && } + {mode === Modes.Training && } + {mode === Modes.Predicting && } + {mode === Modes.Pond && } +
+ ); + } +}; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 98b566e2..00000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -alert('Hello world! -index.jsx'); diff --git a/webpack.config.js b/webpack.config.js index 29e95e54..3bd469a2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,6 @@ module.exports = { extensions: ['.js', '.jsx'], }, entry: { - main: './src/index.js', demo: './src/demo/index.jsx', }, output: {