From b1af2a77518630f4749fe4451094d5c2bddd1051 Mon Sep 17 00:00:00 2001 From: Leon Chen Date: Sun, 18 Sep 2016 19:57:07 -0400 Subject: [PATCH] start demos --- demos/index.html | 29 +++++++ demos/src/home.js | 9 ++ demos/src/index.js | 33 ++++++++ demos/src/menu.js | 35 ++++++++ demos/src/mnist-cnn.js | 157 +++++++++++++++++++++++++++++++++++ demos/styles.css | 144 ++++++++++++++++++++++++++++++++ demos/webpack.dev.config.js | 30 +++++++ demos/webpack.prod.config.js | 37 +++++++++ 8 files changed, 474 insertions(+) create mode 100644 demos/index.html create mode 100644 demos/src/home.js create mode 100644 demos/src/index.js create mode 100644 demos/src/menu.js create mode 100644 demos/src/mnist-cnn.js create mode 100644 demos/styles.css create mode 100644 demos/webpack.dev.config.js create mode 100644 demos/webpack.prod.config.js diff --git a/demos/index.html b/demos/index.html new file mode 100644 index 0000000..4ee4252 --- /dev/null +++ b/demos/index.html @@ -0,0 +1,29 @@ + + + + + + Keras.js Demos + + + + + + + +
+
+
+ +
+
+ +
+
+
+ + + + + + diff --git a/demos/src/home.js b/demos/src/home.js new file mode 100644 index 0000000..eb1d056 --- /dev/null +++ b/demos/src/home.js @@ -0,0 +1,9 @@ +/* global Vue */ +export const Home = Vue.extend({ + template: ` +
+
Home
+
+ ` + +}) diff --git a/demos/src/index.js b/demos/src/index.js new file mode 100644 index 0000000..a40dd7b --- /dev/null +++ b/demos/src/index.js @@ -0,0 +1,33 @@ +/* global Vue */ + +import { Menu } from './menu' +import { Home } from './home' +import { MnistCnn } from './mnist-cnn' + +Vue.component('menu', Menu) +Vue.component('home', Home) +Vue.component('mnist-cnn', MnistCnn) + +const app = new Vue({ + el: '#app', + data: { + currentView: 'home' + } +}) + +// Simple routing + +function matchRoute () { + const routes = ['mnist-cnn'] + + const { hash } = window.location + const route = hash.substr(2) + if (routes.indexOf(route) > -1) { + app.currentView = route + } else { + app.currentView = 'home' + } +} + +window.addEventListener('load', matchRoute) +window.addEventListener('hashchange', matchRoute) diff --git a/demos/src/menu.js b/demos/src/menu.js new file mode 100644 index 0000000..3b43853 --- /dev/null +++ b/demos/src/menu.js @@ -0,0 +1,35 @@ +/* global Vue */ + +export const Menu = Vue.extend({ + template: ` + + ` + +}) diff --git a/demos/src/mnist-cnn.js b/demos/src/mnist-cnn.js new file mode 100644 index 0000000..d9687da --- /dev/null +++ b/demos/src/mnist-cnn.js @@ -0,0 +1,157 @@ +/* global Vue */ +import debounce from 'lodash/debounce' + +const getMidpoint = (p1, p2) => { + const [x1, y1] = p1 + const [x2, y2] = p2 + return [ + x1 + (x2 - x1) / 2, + y1 + (y2 - y1) / 2 + ] +} + +const getCoordinates = e => { + let { clientX, clientY } = e + // for touch event + if (e.touches && e.touches.length) { + clientX = e.touches[0].clientX + clientY = e.touches[0].clientY + } + const { left, top } = e.target.getBoundingClientRect() + const [x, y] = [clientX - left, clientY - top] + return [x, y] +} + +export const MnistCnn = Vue.extend({ + template: ` +
+
Basic Convnet - MNIST
+
+ Loading...{{ loadingProgress }}% +
+
+
Draw any digit (0-9) here
+
+ + +
+
+ clearCLEAR +
+
+
+
+
+
+
+
+
+ `, + + data: function () { + return { + model: new KerasJS.Model({ + model: '/demos/mnist_cnn/mnist_cnn.json', + weights: '/demos/mnist_cnn/mnist_cnn_weights.buf', + metadata: '/demos/mnist_cnn/mnist_cnn_metadata.json' + }), + modelLoading: true, + inputData: { + 'input': new Float32Array(784) + }, + drawing: false, + strokes: [] + } + }, + + computed: { + loadingProgress: function () { + return this.model.getLoadingProgress() + } + }, + + created: function () { + // initialize KerasJS model + this.model.initialize() + this.model.ready().then(() => { + this.modelLoading = false + }) + }, + + ready: function () { + // initialize scaling helper canvas + const ctxScaled = document.getElementById('input-canvas-scaled').getContext('2d') + ctxScaled.scale(28 / 240, 28 / 240) + }, + + methods: { + clear: function (e) { + const ctx = document.getElementById('input-canvas').getContext('2d') + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) + this.drawing = false + this.strokes = [] + }, + activateDraw: function (e) { + this.drawing = true + this.strokes.push([]) + let points = this.strokes[this.strokes.length - 1] + points.push(getCoordinates(e)) + }, + deactivateDraw: function (e) { + this.drawing = false + this.processCanvasData() + this.model.predict(this.inputData) + }, + draw: function (e) { + if (!this.drawing) return + + const ctx = document.getElementById('input-canvas').getContext('2d') + + ctx.lineWidth = 12 + ctx.lineJoin = ctx.lineCap = 'round' + ctx.strokeStyle = '#393E46' + + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) + + let points = this.strokes[this.strokes.length - 1] + points.push(getCoordinates(e)) + + // draw individual strokes + for (let s = 0, slen = this.strokes.length; s < slen; s++) { + points = this.strokes[s] + + let p1 = points[0] + let p2 = points[1] + ctx.beginPath() + ctx.moveTo(...p1) + + // draw points in stroke + // quadratic bezier curve + for (let i = 1, len = points.length; i < len; i++) { + ctx.quadraticCurveTo(...p1, ...getMidpoint(p1, p2)) + p1 = points[i] + p2 = points[i + 1] + } + ctx.lineTo(...p1) + ctx.stroke() + } + }, + processCanvasData: function () { + const ctxScaled = document.getElementById('input-canvas-scaled').getContext('2d') + ctxScaled.drawImage(document.getElementById('input-canvas'), 0, 0) + const imageDataScaled = ctxScaled.getImageData(0, 0, ctxScaled.canvas.width, ctxScaled.canvas.height) + const { data } = imageDataScaled + for (let i = 0, len = data.length; i < len; i += 4) { + this.inputData['input'][i / 4] = data[i + 3] / 255 + } + } + } +}) diff --git a/demos/styles.css b/demos/styles.css new file mode 100644 index 0000000..72ee1d7 --- /dev/null +++ b/demos/styles.css @@ -0,0 +1,144 @@ +@import 'https://fonts.googleapis.com/css?family=Inconsolata'; +@import 'https://fonts.googleapis.com/css?family=Fira+Sans'; +@import 'https://fonts.googleapis.com/css?family=Nothing+You+Could+Do'; + +body { + background: #EEEEEE; + color: #393E46; + min-height: 100vh; + font-family: 'Fira Sans', sans-serif; +} + +.github-logo { + fill: #69707a; + margin: 3px 8px 3px 0; +} + +.title { + width: 100%; + color: #1BBC9B; + display: flex; + flex-direction: row; + align-items: center; +} + +.subtitle { + color: #393E46; +} + +.menu { + padding: 50px; + margin: 20px; + background: white; + border-radius: 10px; +} + +.menu h1 { + color: #CCCCCC; + font-family: 'Fira Sans', sans-serif; + font-size: 1.5rem; +} + +.menu-list li { + color: #69707a; +} + +.menu-list a { + display: flex; + align-items: center; +} + +.menu-list a:hover { + color: #1BBC9B; + background-color: whitesmoke; +} + +.menu-list a:hover .github-logo { + fill: #1BBC9B; +} + +.menu-list.contact li { + padding: 5px 10px; +} + +.menu-list.contact a { + color: #aaaaaa; + padding: 0; + display: inline-flex; + background-color: none; + transition: color 0.2s ease-in; +} + +.menu-list.contact a:hover { + color: #1BBC9B; + background: none; +} + +.demo { + padding: 50px 30px; +} + +.demo .loading-progress { + position: absolute; + top: 0; + right: 0; + padding: 30px; + color: #1BBC9B; + font-size: 18px; + font-family: 'Inconsolata', sans-serif; + padding: 20px 50px; + margin: 20px; + background: rgba(255, 255, 255, 0.5); + border-radius: 10px; +} + +.demo.mnist-cnn .input-container { + display: inline-flex; + flex-direction: column; + margin: 20px; + position: relative; +} + +.demo.mnist-cnn .input-label { + font-family: 'Nothing You Could Do', cursive; + font-size: 18px; + color: #69707a; + text-align: right; +} + +.demo.mnist-cnn .input-label span.arrow { + font-size: 36px; + color: #CCCCCC; + position: absolute; + right: -32px; + top: 8px; +} + +.demo.mnist-cnn .canvas-container { + display: inline-flex; + margin: 10px 0; + background: white; + border: 15px solid rgba(27, 188, 155, 0.3); + transition: border-color 0.2s ease-in; +} + +.demo.mnist-cnn .canvas-container:hover { + border-color: rgba(27, 188, 155, 0.6); +} + +.demo.mnist-cnn .canvas-container canvas:hover { + cursor: crosshair; +} + +.demo.mnist-cnn .input-clear { + display: flex; + align-items: center; + justify-content: flex-end; + color: #69707a; + transition: color 0.2s ease-in; +} + +.demo.mnist-cnn .input-clear:hover { + color: rgba(27, 188, 155, 0.3); + cursor: pointer; +} diff --git a/demos/webpack.dev.config.js b/demos/webpack.dev.config.js new file mode 100644 index 0000000..2f7bbf8 --- /dev/null +++ b/demos/webpack.dev.config.js @@ -0,0 +1,30 @@ +const path = require('path') +const webpack = require('webpack') + +module.exports = { + entry: [ + path.join(__dirname, 'src/index') + ], + output: { + path: __dirname, + filename: 'bundle.js' + }, + devtool: 'cheap-module-eval-source-map', + module: { + loaders: [ + { + test: /\.js$/, + loaders: ['babel-loader'], + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: ['', '.js'] + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development') + }) + ] +} diff --git a/demos/webpack.prod.config.js b/demos/webpack.prod.config.js new file mode 100644 index 0000000..2724d85 --- /dev/null +++ b/demos/webpack.prod.config.js @@ -0,0 +1,37 @@ +const path = require('path') +const webpack = require('webpack') + +module.exports = { + entry: [ + path.join(__dirname, 'src/index') + ], + output: { + path: __dirname, + filename: 'bundle.js' + }, + module: { + loaders: [ + { + test: /\.js$/, + loaders: ['babel-loader'], + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: ['', '.js'] + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('production') + } + }), + new webpack.optimize.DedupePlugin(), + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: false + } + }) + ] +}