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: `
+
+ `
+
+})
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 }}%
+
+
+
+
+ `,
+
+ 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
+ }
+ })
+ ]
+}