├── .eslintrc ├── src ├── utils │ ├── degToRad.js │ ├── initializeRenderer.js │ ├── detectEdge.js │ ├── getImageDataFromDataUrl.js │ └── arToolkit.js ├── assets │ ├── hiro.png │ ├── pan.png │ ├── rose.jpg │ ├── pinch.png │ ├── rotate.png │ ├── drawing1.png │ ├── drawing2.png │ ├── drawing3.png │ ├── drawing4.png │ ├── drawing5.png │ ├── drawing6.png │ ├── drawing7.png │ ├── camera_para.dat │ └── patt.hiro ├── setupTests.js ├── App.test.js ├── index.js ├── GalleryItem.test.js ├── GalleryItem.js ├── MarkerSearch.js ├── Gallery.test.js ├── App.js ├── Gallery.js ├── Tips.js ├── registerServiceWorker.js ├── logo.svg ├── MoveControl.js ├── SketchRenderer.test.js ├── FileSelection.js ├── Sketch.js ├── MoveControl.test.js ├── SketchRenderer.js └── Settings.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── .travis.yml ├── makefile ├── .gitignore ├── package.json ├── LICENSE └── README.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/degToRad.js: -------------------------------------------------------------------------------- 1 | export default deg => ((deg + 360) % 360) * (Math.PI / 180); 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/hiro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/hiro.png -------------------------------------------------------------------------------- /src/assets/pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/pan.png -------------------------------------------------------------------------------- /src/assets/rose.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/rose.jpg -------------------------------------------------------------------------------- /src/assets/pinch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/pinch.png -------------------------------------------------------------------------------- /src/assets/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/rotate.png -------------------------------------------------------------------------------- /src/assets/drawing1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/drawing1.png -------------------------------------------------------------------------------- /src/assets/drawing2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/drawing2.png -------------------------------------------------------------------------------- /src/assets/drawing3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/drawing3.png -------------------------------------------------------------------------------- /src/assets/drawing4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/drawing4.png -------------------------------------------------------------------------------- /src/assets/drawing5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/drawing5.png -------------------------------------------------------------------------------- /src/assets/drawing6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/drawing6.png -------------------------------------------------------------------------------- /src/assets/drawing7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/drawing7.png -------------------------------------------------------------------------------- /src/assets/camera_para.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/sketch-by-phone/HEAD/src/assets/camera_para.dat -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import 'jest-enzyme'; 2 | 3 | global.THREE = {}; 4 | global.THREEx = {}; 5 | global.Hammer = null; 6 | global.requestAnimationFrame = null; 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm test 9 | - npm run build 10 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | install: 3 | npm install 4 | 5 | start: 6 | npm start 7 | 8 | build: 9 | npm run build 10 | 11 | deploy: build 12 | now ./build --public --name sketch-by-phone 13 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | import injectTapEventPlugin from 'react-tap-event-plugin'; 6 | 7 | injectTapEventPlugin(); 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | registerServiceWorker(); 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/utils/initializeRenderer.js: -------------------------------------------------------------------------------- 1 | /* globals THREE */ 2 | const { Color, WebGLRenderer } = THREE; 3 | 4 | export default (canvas) => { 5 | const renderer = new WebGLRenderer({ alpha: true, canvas }); 6 | 7 | renderer.setClearColor(new Color('lightgrey'), 0); 8 | renderer.setSize(window.innerWidth, window.innerHeight); 9 | renderer.domElement.style.position = 'absolute'; 10 | renderer.domElement.style.top = '0px'; 11 | renderer.domElement.style.left = '0px'; 12 | 13 | return renderer; 14 | }; 15 | -------------------------------------------------------------------------------- /src/GalleryItem.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import GalleryItem from './GalleryItem'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | 6 | describe('', () => { 7 | it('renders without crashing', () => { 8 | shallow(); 9 | }); 10 | 11 | it('trigger onSelected on click', () => { 12 | const onSelected = jest.fn(); 13 | const wrapper = shallow(); 14 | wrapper.find(RaisedButton).simulate('click'); 15 | 16 | expect(onSelected).toHaveBeenCalledWith('the_image'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-by-phone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "jsfeat": "0.0.8", 7 | "lodash.isequal": "4.5.0", 8 | "material-ui": "0.18.2", 9 | "react": "15.5.4", 10 | "react-dom": "15.5.4", 11 | "react-media": "1.5.1", 12 | "react-tap-event-plugin": "2.0.1" 13 | }, 14 | "devDependencies": { 15 | "enzyme": "2.8.2", 16 | "expect": "1.20.2", 17 | "jest-enzyme": "3.2.0", 18 | "react-scripts": "1.0.7", 19 | "react-test-renderer": "15.5.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/GalleryItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import RaisedButton from 'material-ui/RaisedButton'; 3 | 4 | const styles = { 5 | button: { 6 | height: '10rem', 7 | width: '10rem', 8 | margin: '0.5rem 0', 9 | }, 10 | image: { 11 | height: '9rem', 12 | width: '9rem', 13 | margin: '0.5rem', 14 | }, 15 | } 16 | export default class GalleryItem extends Component { 17 | handleClick = () => { 18 | this.props.onSelected(this.props.image); 19 | } 20 | render() { 21 | const { image } = this.props; 22 | return ( 23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MarkerSearch.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import hiro from './assets/hiro.png'; 4 | 5 | const styles = { 6 | container: { 7 | position: 'absolute', 8 | bottom: '5rem', 9 | left: 0, 10 | right: 0, 11 | textAlign: 'center', 12 | padding: 'auto auto', 13 | }, 14 | content: { 15 | display: 'inline-block', 16 | color: 'red', 17 | borderColor: 'red', 18 | borderWidth: 2, 19 | borderStyle: 'solid', 20 | maxWidth: 200, 21 | fontWeight: 'bold', 22 | fontSize: '1.5rem', 23 | padding: 10, 24 | }, 25 | img: { 26 | marginTop: '1rem', 27 | height: '5rem', 28 | width: '5rem', 29 | } 30 | }; 31 | 32 | export default () => ( 33 |
34 |
35 | Looking for Hiro Marker 36 | Hiro marker example 37 |
38 |
39 | ); 40 | -------------------------------------------------------------------------------- /src/Gallery.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import Gallery from './Gallery'; 4 | import GalleryItem from './GalleryItem'; 5 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | 8 | describe('', () => { 9 | const muiTheme = getMuiTheme(); 10 | 11 | it('renders without crashing', () => { 12 | shallow(, { context: { muiTheme } }); 13 | }); 14 | 15 | it('triggers onClose when clicking the cancel button', () => { 16 | const onClose = jest.fn(); 17 | 18 | const wrapper = shallow(, { context: { muiTheme } }); 19 | wrapper.find(RaisedButton).simulate('click'); 20 | expect(onClose).toHaveBeenCalled(); 21 | }); 22 | 23 | it('renders a list of GalleryItem', () => { 24 | const wrapper = shallow(, { context: { muiTheme } }); 25 | const items = wrapper.find(GalleryItem); 26 | expect(items.length).toEqual(2); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 marmelab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/detectEdge.js: -------------------------------------------------------------------------------- 1 | import { imgproc, matrix_t, U8C1_t } from 'jsfeat'; 2 | 3 | export default (imageData, { blur = 2, lowTreshold = 20, highTreshold = 50 } = {}) => { 4 | let matrix = new matrix_t(imageData.width, imageData.height, U8C1_t); 5 | imgproc.grayscale(imageData.data, imageData.width, imageData.height, matrix); 6 | 7 | var r = 0; 8 | var kernelSize = (r+1) << 1; 9 | imgproc.gaussian_blur(matrix, matrix, kernelSize, blur); 10 | 11 | imgproc.canny(matrix, matrix, lowTreshold, highTreshold); 12 | 13 | const canvas = document.createElement('canvas'); 14 | canvas.width = imageData.width; 15 | canvas.height = imageData.height; 16 | const ctx = canvas.getContext('2d'); 17 | const newImageData = ctx.createImageData(imageData); 18 | 19 | // put result into newImageData 20 | var data_u32 = new Uint32Array(newImageData.data.buffer); 21 | var alpha = (0xff << 24); 22 | var i = matrix.cols*matrix.rows, pix = 0; 23 | while (--i >= 0) { 24 | pix = matrix.data[i]; 25 | data_u32[i] = alpha | (pix << 16) | (pix << 8) | pix; 26 | } 27 | 28 | return newImageData; 29 | } 30 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 3 | import FileSelection from './FileSelection'; 4 | import Sketch from './Sketch'; 5 | 6 | const styles = { 7 | container: { 8 | position: 'fixed', 9 | top: 0, 10 | left: 0, 11 | right: 0, 12 | bottom: 0, 13 | fontFamily: "'Roboto', sans-serif", 14 | }, 15 | }; 16 | 17 | class App extends Component { 18 | state = { 19 | image: null, 20 | }; 21 | 22 | handleFileSelected = ({ image, whiteImage, blackImage }) => { 23 | this.setState({ image, whiteImage, blackImage }); 24 | } 25 | 26 | render() { 27 | const { image, whiteImage, blackImage } = this.state; 28 | 29 | return ( 30 | 31 |
32 | {!image && } 33 | {image && } 34 |
35 |
36 | ) 37 | } 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 |
archivedArchived Repository
5 | This code is no longer maintained. Feel free to fork it, but use it at your own risks. 6 |
9 | 10 | # sketch-by-phone 11 | 12 | A proof-of-concept of Augmented Reality with HTML5 using [AR.js](https://jeromeetienne.github.io/AR.js). Works only on Android for now. 13 | 14 | [Demo](https://sketch-by-phone.now.sh/) - [Article](https://marmelab.com/blog/2017/06/19/augmented-reality-html5.html) 15 | 16 | [![Watch the video](https://i.vimeocdn.com/video/639172331.webp?mw=800&mh=450)](https://vimeo.com/221006212) 17 | 18 | ## Installation 19 | 20 | ```sh 21 | make install 22 | ``` 23 | 24 | ## Development 25 | 26 | You'll need a sheet of paper with the [hero image](https://jeromeetienne.github.io/AR.js/data/images/HIRO.jpg) printed in the top left corner. 27 | You'll also need an external webcam connected to your computer. 28 | 29 | ```sh 30 | make start 31 | ``` 32 | 33 | This will open the app in your browser. Allow it to use your webcam then point the camera to the hiro sign you printed. 34 | -------------------------------------------------------------------------------- /src/Gallery.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GalleryItem from './GalleryItem'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | 5 | const styles = { 6 | container: { 7 | position: 'relative', 8 | height: '100%', 9 | }, 10 | 11 | gallery: { 12 | display: 'flex', 13 | flexWrap: 'wrap', 14 | justifyContent: 'space-between', 15 | padding: '0.5rem 0.5rem 2.5rem 0.5rem', 16 | position: 'relative', 17 | overflowY: 'scroll', 18 | height: '100%', 19 | } 20 | } 21 | const defaultImages = [ 22 | require('./assets/drawing1.png'), 23 | require('./assets/drawing2.png'), 24 | require('./assets/drawing3.png'), 25 | require('./assets/drawing4.png'), 26 | require('./assets/drawing5.png'), 27 | require('./assets/drawing6.png'), 28 | require('./assets/drawing7.png'), 29 | ]; 30 | 31 | const Gallery = ({ images = defaultImages, onClose, onSelected }) => ( 32 |
33 | 34 |
35 | {images.map(image => )} 36 |
37 |
38 | ) 39 | 40 | export default Gallery; 41 | -------------------------------------------------------------------------------- /src/utils/getImageDataFromDataUrl.js: -------------------------------------------------------------------------------- 1 | export default (dataUrl) => 2 | new Promise((resolve, reject) => { 3 | try { 4 | const img = new Image(); 5 | img.onload = () => { 6 | try { 7 | const canvas = document.createElement('canvas'); 8 | canvas.width = img.width; 9 | canvas.height = img.height; 10 | const ctx = canvas.getContext('2d'); 11 | ctx.drawImage(img,0,0); 12 | const whiteImage = ctx.createImageData(img.width, img.height); 13 | whiteImage.data.fill(255); 14 | 15 | const blackImage = ctx.createImageData(img.width, img.height); 16 | for (var i=0;i 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | 23 | 24 | 25 | 26 | Sketch By Phone 27 | 28 | 29 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Tips.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/img-redundant-alt: off */ 2 | import React from 'react'; 3 | import pan from './assets/pan.png'; 4 | import pinch from './assets/pinch.png'; 5 | import rotate from './assets/rotate.png'; 6 | import Media from 'react-media'; 7 | 8 | const styles = { 9 | tips: { 10 | marginLeft: 'auto', 11 | marginRight: 'auto', 12 | maxWidth: 600, 13 | display: 'flex', 14 | flexDirection: 'column', 15 | position: 'absolute', 16 | bottom: '5rem', 17 | left: '1rem', 18 | right: '1rem', 19 | padding: '1rem', 20 | backgroundColor: 'rgba(255, 255, 255, 0.75)', 21 | }, 22 | item: { 23 | display: 'flex', 24 | alignItems: 'center', 25 | }, 26 | text: { 27 | marginLeft: '1rem', 28 | } 29 | }; 30 | 31 | styles.tipsLandscape = { ...styles.tips, flexDirection: 'row' }; 32 | styles.itemLandscape = { ...styles.item, flexDirection: 'column', maxWidth: 200 }; 33 | 34 | export default ({ onHide }) => ( 35 | 38 | {matches => ( 39 |
40 |
41 | How to move the image 42 |
Pan with your finger to drag the picture on the paper
43 |
44 |
45 | How to zoom the image 46 |
Pinch to zoom the picture in or out and fit the sheet
47 |
48 |
49 | How to rotate the image 50 |
Rotate your fingers to rotate the picture and orient it on the sheet
51 |
52 |
53 | )} 54 |
55 | ); 56 | -------------------------------------------------------------------------------- /src/utils/arToolkit.js: -------------------------------------------------------------------------------- 1 | /* globals THREEx */ 2 | import cameraData from '../assets/camera_para.dat'; 3 | import hiro from '../assets/patt.hiro'; 4 | 5 | const { ArMarkerControls, ArToolkitContext, ArToolkitSource } = THREEx; 6 | 7 | /** 8 | * Initialize AR Toolkit from our three.js objects so that it can detect the Hiro marker 9 | * 10 | * @param {Object} renderer: the WebGL renderer from three.js 11 | * @param {Object} camera the camera object from three.js 12 | * @param {Array} onRenderFcts an array of functions which will be executed every frames 13 | * @returns {Object} An ArToolkitContext instance 14 | */ 15 | export function initializeArToolkit(renderer, camera, onRenderFcts) { 16 | ArToolkitContext.baseURL = '../'; 17 | 18 | const arToolkitSource = new ArToolkitSource({ sourceType : 'webcam' }); 19 | 20 | arToolkitSource.init(() => { 21 | arToolkitSource.onResize(renderer.domElement); 22 | }); 23 | 24 | window.addEventListener('resize', () => { 25 | arToolkitSource.onResize(renderer.domElement); 26 | }); 27 | 28 | // create atToolkitContext 29 | const arToolkitContext = new ArToolkitContext({ 30 | cameraParametersUrl: cameraData, 31 | detectionMode: 'mono', 32 | maxDetectionRate: 30, 33 | canvasWidth: 800, 34 | canvasHeight: 600, 35 | }); 36 | 37 | arToolkitContext.init(() => { 38 | camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix()); 39 | }); 40 | 41 | // update artoolkit on every frame 42 | onRenderFcts.push(() => { 43 | if(arToolkitSource.ready === false) return; 44 | 45 | arToolkitContext.update(arToolkitSource.domElement); 46 | }); 47 | 48 | return arToolkitContext; 49 | } 50 | 51 | /** 52 | * Initialize AR Toolkit Hiro marker 53 | * 54 | * @param {Object} arToolkitContext: the ArToolkitContext instance 55 | * @param {Object} markerRoot a DOM element where to put the marker 56 | * @returns {Object} An ArMarkerControls instance 57 | */ 58 | 59 | export function getMarker(arToolkitContext, markerRoot) { 60 | return new ArMarkerControls(arToolkitContext, markerRoot, { 61 | type : 'pattern', 62 | patternUrl : hiro, 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/MoveControl.js: -------------------------------------------------------------------------------- 1 | /* globals Hammer */ 2 | 3 | import React, { Component } from 'react'; 4 | 5 | import degToRad from './utils/degToRad'; 6 | 7 | const styles = { 8 | container: { 9 | position: 'fixed', 10 | top: 0, 11 | bottom: 0, 12 | left: 0, 13 | right: 0, 14 | } 15 | } 16 | 17 | export const moveControlFactory = Hammer => class MoveControl extends Component { 18 | state = { 19 | pan: { 20 | startX: 1, 21 | startZ: 2, 22 | }, 23 | rotation: { 24 | start: 0, 25 | }, 26 | scale: { 27 | startX: 2, 28 | startY: 2, 29 | } 30 | } 31 | 32 | componentDidMount() { 33 | this.hammer = new Hammer(this.div); 34 | 35 | this.hammer.get('pinch').set({ enable: true }); 36 | this.hammer.get('rotate').set({ enable: true }); 37 | this.hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL }); 38 | 39 | this.hammer.on('panstart', this.handlePan); 40 | 41 | this.hammer.on('panmove', this.handlePan); 42 | 43 | this.hammer.on('pinchstart', this.handlePinch); 44 | 45 | this.hammer.on('pinch', this.handlePinch); 46 | 47 | this.hammer.on('rotatestart', this.handleRotate); 48 | 49 | this.hammer.on('rotatemove', this.handleRotate); 50 | } 51 | 52 | handlePan = (ev) => { 53 | const { coordX, coordZ, onTranslateChange } = this.props; 54 | if (ev.type === 'panstart') { 55 | this.setState({ 56 | ...this.state, 57 | pan: { 58 | startX: coordX, 59 | startZ: coordZ, 60 | }, 61 | }); 62 | } 63 | onTranslateChange({ 64 | x: this.state.pan.startX + ev.deltaX / 200, 65 | z: this.state.pan.startZ + ev.deltaY / 200, 66 | }); 67 | } 68 | 69 | handlePinch = (ev) => { 70 | const { scaleX, scaleY, onZoomChange } = this.props; 71 | if (ev.type === 'pinchstart') { 72 | this.setState({ 73 | ...this.state, 74 | scale: { 75 | ...this.state.scale, 76 | startX: scaleX, 77 | startY: scaleY, 78 | }, 79 | }); 80 | } 81 | onZoomChange({ 82 | x: this.state.scale.startX * ev.scale, 83 | y: this.state.scale.startY * ev.scale, 84 | }); 85 | } 86 | 87 | handleRotate = (ev) => { 88 | const { rotation, onRotationChange } = this.props; 89 | if (ev.type === 'rotatestart') { 90 | this.setState({ 91 | ...this.state, 92 | rotation: { 93 | start: rotation + degToRad(ev.rotation), // the first rotation is the angle between the two finger ignoring it. 94 | }, 95 | }); 96 | return; 97 | } 98 | onRotationChange(this.state.rotation.start - degToRad(ev.rotation)); 99 | } 100 | 101 | storeRef = node => { 102 | this.div = node; 103 | } 104 | 105 | render() { 106 | return
; 110 | } 111 | } 112 | 113 | export default moveControlFactory(Hammer); 114 | -------------------------------------------------------------------------------- /src/SketchRenderer.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import expect, { createSpy } from 'expect'; 4 | 5 | import { sketchRendererFactory } from './SketchRenderer'; 6 | 7 | describe('SketchRenderer', () => { 8 | const mesh = { 9 | position: {}, 10 | scale: {}, 11 | rotation: {}, 12 | }; 13 | const material = {}; 14 | const SketchRenderer = sketchRendererFactory({ 15 | THREE: { 16 | Camera: createSpy(), 17 | Group: createSpy().andReturn({ add: createSpy() }), 18 | Mesh: createSpy().andReturn(mesh), 19 | MeshBasicMaterial: createSpy().andReturn(material), 20 | PlaneGeometry: createSpy(), 21 | Scene: createSpy().andReturn({ add: createSpy() }), 22 | Texture: createSpy().andCall(image => ({ image })), 23 | }, 24 | getMarker: createSpy().andReturn({ 25 | addEventListener: createSpy(), 26 | }), 27 | initializeArToolkit: createSpy(), 28 | initializeRenderer: createSpy().andReturn({ render: createSpy() }), 29 | requestAnimationFrame: createSpy(), 30 | detectEdge: createSpy().andReturn({ image: 'edge' }), 31 | }); 32 | 33 | it('should update mesh based on props', () => { 34 | const div = document.createElement('div'); 35 | const props = { 36 | coordX: 'coordX', 37 | coordZ: 'coordZ', 38 | scaleX: 'scaleX', 39 | scaleY: 'scaleY', 40 | rotation: 'rotation', 41 | }; 42 | const sketchRenderer = ReactDOM.render(, div); 43 | expect(sketchRenderer.mesh).toEqual({ 44 | position: { 45 | x: 'coordX', 46 | z: 'coordZ', 47 | }, 48 | scale: { 49 | x: 'scaleX', 50 | y: 'scaleY', 51 | }, 52 | rotation: { 53 | x: - Math.PI / 2, 54 | z: 'rotation' 55 | }, 56 | }); 57 | }); 58 | 59 | it('should update materials opacity based on props when not detecting edge', () => { 60 | const div = document.createElement('div'); 61 | const props = { 62 | opacity: 'opacity', 63 | image: 'image', 64 | blackImage: 'blackImage', 65 | }; 66 | const sketchRenderer = ReactDOM.render(, div); 67 | sketchRenderer.componentDidUpdate(); 68 | expect(sketchRenderer.material).toEqual({ 69 | alphaMap: null, 70 | map: { 71 | image: 'image', 72 | needsUpdate: true, 73 | }, 74 | needsUpdate: true, 75 | opacity: 'opacity', 76 | }); 77 | }); 78 | 79 | it('should update materials texture based on props when detecting edge', () => { 80 | const div = document.createElement('div'); 81 | const props = { 82 | isDetectingEdge: true, 83 | opacity: 'opacity', 84 | image: 'image', 85 | blackImage: 'blackImage', 86 | }; 87 | const sketchRenderer = ReactDOM.render(, div); 88 | sketchRenderer.componentDidUpdate(); 89 | expect(sketchRenderer.material).toEqual({ 90 | alphaMap: { 91 | image: { 92 | image: 'edge', 93 | }, 94 | needsUpdate: true, 95 | }, 96 | map: { 97 | image: 'blackImage', 98 | needsUpdate: true, 99 | }, 100 | needsUpdate: true, 101 | opacity: 1, 102 | }); 103 | }); 104 | }); 105 | 106 | -------------------------------------------------------------------------------- /src/FileSelection.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import getImageDataFromDataUrl from './utils/getImageDataFromDataUrl'; 3 | import hiro from './assets/hiro.png'; 4 | import rose from './assets/rose.jpg'; 5 | import Gallery from './Gallery'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | 8 | const styles = { 9 | container: { 10 | minHeight: '100%', 11 | position: 'absolute', 12 | top: 0, 13 | left: 0, 14 | right: 0, 15 | backgroundImage: `url(${rose})`, 16 | backgroundRepeat: 'no-repeat', 17 | backgroundSize: 'cover', 18 | backgroundPositionX: '50%', 19 | paddingTop: 100, 20 | paddingLeft: 10, 21 | paddingRight: 10, 22 | fontSize: '1.2rem', 23 | }, 24 | 25 | list: { 26 | paddingRight: 40, 27 | }, 28 | 29 | listItem: { 30 | paddingBottom: 15, 31 | }, 32 | 33 | title: { 34 | fontSize: '1.2rem', 35 | textAlign: 'center', 36 | fontWeight: 'bold', 37 | }, 38 | 39 | a: { 40 | textDecoration: 'underline', 41 | }, 42 | 43 | hiroMarker: { 44 | textAlign: 'center', 45 | }, 46 | 47 | btnFileInput: { 48 | marginTop: 15, 49 | marginBottom: 15, 50 | }, 51 | 52 | hiroMarkerImg: { 53 | marginTop: '1rem', 54 | height: '5rem', 55 | width: '5rem', 56 | border: '5px solid white', 57 | }, 58 | 59 | fileInput: { 60 | display: 'none', 61 | }, 62 | 63 | hr: { 64 | border: 0, 65 | borderTop: '1px solid black', 66 | marginBottom: '1rem', 67 | marginTop: '1rem', 68 | } 69 | }; 70 | 71 | class FileSelection extends Component { 72 | state = { 73 | showGallery: false, 74 | }; 75 | 76 | handleChange = (event) => { 77 | var reader = new FileReader(); 78 | reader.addEventListener('load', () => { 79 | getImageDataFromDataUrl(reader.result) 80 | .then(this.props.onFileSelected); 81 | }, false); 82 | 83 | reader.readAsDataURL(event.target.files[0]); 84 | } 85 | 86 | handleFileInputClick = () => { 87 | this.fileInput.click(); 88 | } 89 | 90 | handleOpenGalleryClick = () => { 91 | setTimeout(() => { 92 | this.setState({ showGallery: true }); 93 | }, 500); 94 | } 95 | 96 | handleCloseGalleryClick = () => { 97 | setTimeout(() => { 98 | this.setState({ showGallery: false }); 99 | }, 500); 100 | } 101 | 102 | handleGalleryImageSelected = (image) => { 103 | getImageDataFromDataUrl(image).then(this.props.onFileSelected); 104 | } 105 | 106 | storeFileInputRef = node => { 107 | this.fileInput = node; 108 | } 109 | 110 | render() { 111 | const { showGallery } = this.state; 112 | 113 | if (showGallery) { 114 | return ; 115 | } 116 | 117 | return ( 118 |
119 |

Sketch anything you want using your phone as a guide

120 |
121 |
    122 |
  1. 123 |
    124 | Print a hiro marker 125 |
    126 |
    127 | 128 | Hiro marker example 129 | 130 |
    131 |
  2. 132 |
  3. 133 | Put it on a sheet of paper 134 |
  4. 135 |
  5. 136 | Choose something to draw 137 |
    138 | 139 | 140 | 141 |
    142 | 143 |
  6. 144 |
145 |
146 | ); 147 | } 148 | } 149 | 150 | export default FileSelection; 151 | -------------------------------------------------------------------------------- /src/Sketch.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/img-redundant-alt: off */ 2 | import React, { Component } from 'react'; 3 | import isEqual from 'lodash.isequal'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | 6 | import Settings from './Settings'; 7 | import SketchRenderer from './SketchRenderer'; 8 | import MoveControl from './MoveControl'; 9 | import MarkerSearch from './MarkerSearch'; 10 | import Tips from './Tips'; 11 | 12 | const styles = { 13 | backButton: { 14 | zIndex: 1000, 15 | position: 'absolute', 16 | right: '1rem', 17 | top: '1rem', 18 | } 19 | } 20 | class Sketch extends Component { 21 | state = { 22 | showTips: true, 23 | markerFound: false, 24 | opacity: 1, 25 | isDetectingEdge: false, 26 | blur: 2, 27 | highTreshold: 20, 28 | lowTreshold: 50, 29 | coord: { 30 | x: 2, 31 | z: 1, 32 | }, 33 | rotation: 0, 34 | scale: { 35 | x: 2, 36 | y: 2, 37 | } 38 | }; 39 | 40 | renderer = null; 41 | 42 | shouldComponentUpdate(nextProps, state) { 43 | return !isEqual(state, this.state); 44 | } 45 | 46 | handleBack = () => { 47 | setTimeout(() => { 48 | // We can't reset the AR.js created elements (no dispose, reset or destroy methods available) 49 | window.location.reload(); 50 | }, 500); 51 | } 52 | 53 | handleTranslateChange = ({ x, z }) => this.setState({ coord: { x, z } }); 54 | 55 | handleZoomChange = ({ x, y }) => this.setState({ scale: { x, y } }); 56 | 57 | handleRotationChange = (rotation) => this.setState({ rotation }); 58 | 59 | handleOpacityChange = (event, opacity) => this.setState({ opacity }); 60 | 61 | handleDetectEdgeChange = () => this.setState({ isDetectingEdge: !this.state.isDetectingEdge }); 62 | 63 | handleBlurChange = (event, blur) => this.setState({ blur }); 64 | 65 | handleLowTresholdChange = (event, lowTreshold) => this.setState({ lowTreshold }); 66 | 67 | handleHighTresholdChange = (event, highTreshold) => this.setState({ highTreshold }); 68 | 69 | handleHideTips = () => this.setState({ showTips: false }); 70 | 71 | handleMarkerFound = () => this.setState({ markerFound: true }); 72 | 73 | render() { 74 | const { 75 | markerFound, 76 | showTips, 77 | opacity, 78 | isDetectingEdge, 79 | blur, 80 | lowTreshold, 81 | highTreshold, 82 | coord: { 83 | x: coordX, 84 | z: coordZ, 85 | }, 86 | scale: { 87 | x: scaleX, 88 | y: scaleY, 89 | }, 90 | rotation, 91 | } = this.state; 92 | 93 | const { image, blackImage } = this.props; 94 | 95 | return ( 96 |
97 | 112 | {!markerFound && } 113 | {markerFound && } 123 | {markerFound && showTips && } 124 | 125 | 137 |
138 | ); 139 | } 140 | } 141 | 142 | export default Sketch; 143 | -------------------------------------------------------------------------------- /src/MoveControl.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import expect, { createSpy } from 'expect'; 4 | 5 | import { moveControlFactory } from './MoveControl'; 6 | 7 | describe('MoveControl', () => { 8 | const MoveControl = moveControlFactory(createSpy().andReturn({ 9 | get: createSpy().andReturn({ set: createSpy() }), 10 | on: createSpy(), 11 | })); 12 | 13 | it('should initialize state', () => { 14 | const div = document.createElement('div'); 15 | const moveControl = ReactDOM.render(, div); 16 | expect(moveControl.state).toEqual({ 17 | pan: { 18 | startX: 1, 19 | startZ: 2, 20 | }, 21 | scale: { 22 | startX: 2, 23 | startY: 2, 24 | }, 25 | rotation: { 26 | start: 0 27 | }, 28 | }); 29 | }); 30 | 31 | it('handlePan should call onTranslateChange with new coord', () => { 32 | const div = document.createElement('div'); 33 | const onTranslateChange = createSpy(); 34 | const props = { 35 | onTranslateChange, 36 | coordX: 10, 37 | coordZ: 10, 38 | }; 39 | const moveControl = ReactDOM.render(, div); 40 | moveControl.handlePan({ 41 | deltaX: 1000, 42 | deltaY: 2000, 43 | }); 44 | 45 | expect(moveControl.state.pan).toEqual({ 46 | startX: 1, 47 | startZ: 2, 48 | }); 49 | 50 | expect(onTranslateChange).toHaveBeenCalledWith({ x: 6, z: 12 }); 51 | }); 52 | 53 | it('handlePan should update pan state if event type is panstart before calling onTranslateChange', () => { 54 | const div = document.createElement('div'); 55 | const onTranslateChange = createSpy(); 56 | const props = { 57 | onTranslateChange, 58 | coordX: 10, 59 | coordZ: 10, 60 | }; 61 | const moveControl = ReactDOM.render(, div); 62 | moveControl.handlePan({ 63 | type: 'panstart', 64 | deltaX: 1000, 65 | deltaY: 2000, 66 | }); 67 | 68 | expect(moveControl.state.pan).toEqual({ 69 | startX: 10, 70 | startZ: 10, 71 | }); 72 | 73 | expect(onTranslateChange).toHaveBeenCalledWith({ x: 15, z: 20 }); 74 | }); 75 | 76 | it('handlePinch should call onZoomChange with new scale', () => { 77 | const div = document.createElement('div'); 78 | const onZoomChange = createSpy(); 79 | const props = { 80 | onZoomChange, 81 | scaleX: 10, 82 | scaleY: 10, 83 | }; 84 | const moveControl = ReactDOM.render(, div); 85 | moveControl.handlePinch({ 86 | scale: 4, 87 | }); 88 | 89 | expect(moveControl.state.scale).toEqual({ 90 | startX: 2, 91 | startY: 2, 92 | }); 93 | 94 | expect(onZoomChange).toHaveBeenCalledWith({ x: 8, y: 8 }); 95 | }); 96 | 97 | it('handlePinch should update state before calling onZoomChange', () => { 98 | const div = document.createElement('div'); 99 | const onZoomChange = createSpy(); 100 | const props = { 101 | onZoomChange, 102 | scaleX: 10, 103 | scaleY: 10, 104 | }; 105 | const moveControl = ReactDOM.render(, div); 106 | moveControl.handlePinch({ 107 | type: 'pinchstart', 108 | scale: 10, 109 | }); 110 | 111 | expect(moveControl.state.scale).toEqual({ 112 | startX: 10, 113 | startY: 10, 114 | }); 115 | 116 | expect(onZoomChange).toHaveBeenCalledWith({ x: 100, y: 100 }); 117 | }); 118 | 119 | it('handleRotate should call onZoomChange with new rotation', () => { 120 | const div = document.createElement('div'); 121 | const onRotationChange = createSpy(); 122 | const props = { 123 | onRotationChange, 124 | rotation: 45, 125 | }; 126 | const moveControl = ReactDOM.render(, div); 127 | moveControl.handleRotate({ 128 | rotation: 180, 129 | }); 130 | 131 | expect(moveControl.state.rotation).toEqual({ start: 0 }); 132 | expect(onRotationChange).toHaveBeenCalledWith(-Math.PI); 133 | }); 134 | 135 | it('handleRotate should update state when event is rotatestart but not call onZoomChange', () => { 136 | const div = document.createElement('div'); 137 | const onRotationChange = createSpy(); 138 | const props = { 139 | onRotationChange, 140 | rotation: 0, 141 | }; 142 | const moveControl = ReactDOM.render(, div); 143 | moveControl.handleRotate({ 144 | type: 'rotatestart', 145 | rotation: 180, 146 | }); 147 | 148 | expect(moveControl.state.rotation).toEqual({ start: Math.PI }); 149 | expect(onRotationChange).toNotHaveBeenCalled(); 150 | }); 151 | }); 152 | 153 | -------------------------------------------------------------------------------- /src/SketchRenderer.js: -------------------------------------------------------------------------------- 1 | /* globals THREE, requestAnimationFrame */ 2 | import React, { Component } from 'react'; 3 | import initializeRenderer from './utils/initializeRenderer'; 4 | import { initializeArToolkit, getMarker } from './utils/arToolkit'; 5 | import detectEdge from './utils/detectEdge'; 6 | 7 | export const sketchRendererFactory = ({ THREE, initializeArToolkit, initializeRenderer, getMarker, requestAnimationFrame, detectEdge }) => { 8 | const { Camera, DoubleSide, Group, Mesh, MeshBasicMaterial, PlaneGeometry, Scene, Texture } = THREE; 9 | 10 | return class SketchRenderer extends Component { 11 | componentDidMount() { 12 | const { 13 | opacity, 14 | coordX, 15 | coordZ, 16 | scaleX, 17 | scaleY, 18 | rotation, 19 | onMarkerFound, 20 | } = this.props; 21 | 22 | const renderer = this.renderer = initializeRenderer(this.canvas); 23 | 24 | const scene = new Scene(); 25 | const camera = new Camera(); 26 | scene.add(camera); 27 | 28 | const markerRoot = new Group(); 29 | scene.add(markerRoot); 30 | const onRenderFcts = []; 31 | const arToolkitContext = initializeArToolkit(renderer, camera, onRenderFcts); 32 | const marker = getMarker(arToolkitContext, markerRoot); 33 | 34 | marker.addEventListener('markerFound', onMarkerFound); 35 | 36 | const geometry = new PlaneGeometry(1, 1, 1); 37 | 38 | this.image = this.props.image; 39 | this.blackImage = this.props.blackImage; 40 | 41 | const texture = new Texture(this.image); 42 | texture.needsUpdate = true; 43 | 44 | this.material = new MeshBasicMaterial({ 45 | map: texture, 46 | opacity, 47 | side: DoubleSide, 48 | transparent: true, 49 | }); 50 | 51 | this.mesh = new Mesh(geometry, this.material); 52 | this.mesh.rotation.x = - Math.PI / 2; // -90° 53 | this.mesh.rotation.z = rotation; 54 | this.mesh.position.x = coordX; 55 | this.mesh.position.z = coordZ; 56 | this.mesh.scale.x = scaleX; 57 | this.mesh.scale.y = scaleY; 58 | 59 | markerRoot.add(this.mesh); 60 | 61 | // render the scene 62 | onRenderFcts.push(function(){ 63 | renderer.render(scene, camera); 64 | }); 65 | 66 | // run the rendering loop 67 | var lastTimeMsec = null; 68 | 69 | function animate(nowMsec) { 70 | // keep looping 71 | requestAnimationFrame(animate); 72 | // measure time 73 | lastTimeMsec = lastTimeMsec || nowMsec - 1000 / 60; 74 | const deltaMsec = Math.min(200, nowMsec - lastTimeMsec); 75 | lastTimeMsec = nowMsec; 76 | // call each update function 77 | onRenderFcts.forEach(onRenderFct => { 78 | onRenderFct(deltaMsec / 1000, nowMsec / 1000); 79 | }); 80 | } 81 | requestAnimationFrame(animate); 82 | } 83 | 84 | componentWillUnmount() { 85 | this.renderer.dispose(); 86 | } 87 | 88 | storeRef = node => { 89 | this.canvas = node; 90 | } 91 | 92 | componentDidUpdate() { 93 | const { coordX, coordZ, scaleX, scaleY, rotation } = this.props; 94 | this.mesh.position.x = coordX; 95 | this.mesh.position.z = coordZ; 96 | this.mesh.scale.x = scaleX; 97 | this.mesh.scale.y = scaleY; 98 | this.mesh.rotation.z = rotation; 99 | this.mesh.needsUpdate = true; 100 | 101 | const { blackImage, image } = this.props; 102 | const { opacity, isDetectingEdge, blur, lowTreshold, highTreshold } = this.props; 103 | if (isDetectingEdge) { 104 | this.material.opacity = 1; 105 | const alphaImage = detectEdge(image, { blur, lowTreshold, highTreshold }); 106 | const alphaTexture = new Texture(alphaImage); 107 | alphaTexture.needsUpdate = true; 108 | this.material.alphaMap = alphaTexture; 109 | this.material.map.image = blackImage; 110 | this.material.map.needsUpdate = true; 111 | } else { 112 | this.material.opacity = opacity; 113 | this.material.alphaMap = null; 114 | const texture = new Texture(image); 115 | texture.needsUpdate = true; 116 | this.material.map = texture; 117 | } 118 | this.material.needsUpdate = true; 119 | } 120 | 121 | render() { 122 | return ( 123 | 124 | ); 125 | } 126 | } 127 | }; 128 | 129 | export default sketchRendererFactory({ 130 | THREE, 131 | initializeArToolkit, 132 | getMarker, 133 | initializeRenderer, 134 | requestAnimationFrame: requestAnimationFrame, 135 | detectEdge, 136 | }); 137 | -------------------------------------------------------------------------------- /src/Settings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import RaisedButton from 'material-ui/RaisedButton'; 3 | import Checkbox from 'material-ui/Checkbox'; 4 | import Slider from 'material-ui/Slider'; 5 | import Subheader from 'material-ui/Subheader'; 6 | 7 | const styles = { 8 | openButton: { 9 | position: 'absolute', 10 | bottom: '1rem', 11 | right: '1rem', 12 | }, 13 | modal: { 14 | position: 'absolute', 15 | bottom: 0, 16 | left: 0, 17 | right: 0, 18 | backgroundColor: 'white', 19 | padding: '0.5rem', 20 | }, 21 | modalItem: { 22 | padding: '0.5rem', 23 | }, 24 | slider: { 25 | marginTop: 0, 26 | marginBottom: '0.5rem', 27 | }, 28 | detectOptions: { 29 | display: 'flex', 30 | flexFlow: 'row', 31 | flexWrap: 'wrap', 32 | }, 33 | detectOptionItem: { 34 | boxSizing: 'border-box', 35 | width: '50%', 36 | padding: '0 0.5rem', 37 | }, 38 | detectOptionItemFull: { 39 | boxSizing: 'border-box', 40 | width: '100%', 41 | padding: '0 0.5rem', 42 | }, 43 | detectEdges: { 44 | marginBottom: '1rem', 45 | }, 46 | }; 47 | 48 | class Settings extends Component { 49 | state = { 50 | open: false, 51 | }; 52 | 53 | handleOpen = () => { 54 | setTimeout(() => { 55 | this.setState({ isOpen: true }); 56 | }, 500); 57 | } 58 | 59 | handleClose = () => { 60 | setTimeout(() => { 61 | this.setState({ isOpen: false }); 62 | }, 500); 63 | } 64 | 65 | render() { 66 | const { isOpen } = this.state; 67 | if (!isOpen) { 68 | return 69 | } 70 | 71 | const { 72 | blur, 73 | lowTreshold, 74 | highTreshold, 75 | opacity, 76 | isDetectingEdge, 77 | onBlurChange, 78 | onLowTresholdChange, 79 | onHighTresholdChange, 80 | onOpacityChange, 81 | onDetectEdgeChange 82 | } = this.props; 83 | 84 | return ( 85 |
86 | { !isDetectingEdge && 87 |
88 | Opacity: {opacity} 89 | 95 |
96 | } 97 | { 98 | isDetectingEdge && ( 99 |
100 |
101 | blur: {blur} 102 | 109 |
110 |
111 | low treshold: {lowTreshold} 112 | 119 |
120 |
121 | high treshold: {highTreshold} 122 | 129 |
130 |
131 | ) 132 | } 133 | 141 | 142 | 148 |
149 | ); 150 | } 151 | } 152 | 153 | 154 | export default Settings; 155 | -------------------------------------------------------------------------------- /src/assets/patt.hiro: -------------------------------------------------------------------------------- 1 | 234 235 240 233 240 234 240 235 240 237 240 238 240 240 240 232 2 | 229 240 240 240 240 240 240 240 240 240 240 240 240 240 240 228 3 | 227 240 240 240 240 240 240 240 240 240 240 240 240 240 240 239 4 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 5 | 236 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 6 | 234 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 7 | 236 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 8 | 231 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 9 | 229 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 10 | 225 149 240 240 186 216 225 174 240 240 240 237 238 240 240 240 11 | 150 107 238 231 75 208 115 147 238 228 223 226 237 180 226 240 12 | 150 62 181 213 62 187 113 169 197 72 29 237 120 50 53 207 13 | 149 63 47 78 53 184 113 101 142 5 150 150 45 217 186 83 14 | 121 84 220 222 58 180 121 92 128 109 237 124 155 232 161 64 15 | 149 71 240 240 76 210 98 109 122 108 240 129 51 119 161 155 16 | 149 186 240 240 98 219 135 152 207 191 236 227 152 77 175 209 17 | 235 235 240 233 240 234 240 235 240 236 240 238 240 240 240 240 18 | 229 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 19 | 227 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 20 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 21 | 236 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 22 | 234 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 23 | 236 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 24 | 232 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 25 | 229 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 26 | 225 156 240 240 186 216 225 186 240 240 240 240 240 240 240 240 27 | 150 117 240 231 72 206 115 162 240 232 223 237 240 180 226 240 28 | 150 74 187 213 51 184 103 168 197 78 29 237 120 50 53 216 29 | 144 77 51 74 61 184 106 101 142 5 150 152 52 217 186 85 30 | 117 89 219 219 65 184 121 92 128 100 236 125 156 240 170 73 31 | 148 71 240 240 76 210 109 109 121 99 240 137 51 120 166 164 32 | 140 186 240 240 98 220 150 156 207 192 236 230 152 77 176 212 33 | 234 235 240 233 240 234 240 235 240 236 240 238 240 240 240 233 34 | 229 240 240 240 240 240 240 240 240 240 240 240 240 240 240 239 35 | 227 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 36 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 37 | 234 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 38 | 232 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 39 | 235 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 40 | 232 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 41 | 228 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 42 | 225 156 240 240 182 212 225 180 240 240 240 240 240 240 240 240 43 | 150 116 238 228 66 205 115 151 238 236 225 240 240 180 226 240 44 | 156 84 186 211 47 184 109 170 200 92 30 240 120 50 53 216 45 | 147 83 51 73 50 184 106 110 148 17 151 150 45 217 186 85 46 | 127 98 219 219 58 179 109 101 128 107 237 125 155 240 163 72 47 | 155 86 240 240 76 201 85 108 121 95 232 137 51 118 153 155 48 | 149 189 240 240 98 220 141 154 206 178 235 230 152 77 175 209 49 | 50 | 232 228 239 240 240 240 240 240 240 240 240 207 83 64 155 209 51 | 240 240 240 240 240 240 240 240 240 240 226 53 186 161 161 175 52 | 240 240 240 240 240 240 240 240 240 240 180 50 217 232 119 77 53 | 240 240 240 240 240 240 240 240 240 238 237 120 45 155 51 152 54 | 238 240 240 240 240 240 240 240 240 237 226 237 150 124 129 227 55 | 240 240 240 240 240 240 240 240 240 240 223 29 150 237 240 236 56 | 237 240 240 240 240 240 240 240 240 240 228 72 5 109 108 191 57 | 240 240 240 240 240 240 240 240 240 240 238 197 142 128 122 207 58 | 235 240 240 240 240 240 240 240 240 174 147 169 101 92 109 152 59 | 240 240 240 240 240 240 240 240 240 225 115 113 113 121 98 135 60 | 234 240 240 240 240 240 240 240 240 216 208 187 184 180 210 219 61 | 240 240 240 240 240 240 240 240 240 186 75 62 53 58 76 98 62 | 233 240 240 240 240 240 240 240 240 240 231 213 78 222 240 240 63 | 240 240 240 240 240 240 240 240 240 240 238 181 47 220 240 240 64 | 235 240 240 240 240 240 240 240 240 149 107 62 63 84 71 186 65 | 234 229 227 240 236 234 236 231 229 225 150 150 149 121 149 149 66 | 240 240 240 240 240 240 240 240 240 240 240 216 85 73 164 212 67 | 240 240 240 240 240 240 240 240 240 240 226 53 186 170 166 176 68 | 240 240 240 240 240 240 240 240 240 240 180 50 217 240 120 77 69 | 240 240 240 240 240 240 240 240 240 240 240 120 52 156 51 152 70 | 238 240 240 240 240 240 240 240 240 240 237 237 152 125 137 230 71 | 240 240 240 240 240 240 240 240 240 240 223 29 150 236 240 236 72 | 236 240 240 240 240 240 240 240 240 240 232 78 5 100 99 192 73 | 240 240 240 240 240 240 240 240 240 240 240 197 142 128 121 207 74 | 235 240 240 240 240 240 240 240 240 186 162 168 101 92 109 156 75 | 240 240 240 240 240 240 240 240 240 225 115 103 106 121 109 150 76 | 234 240 240 240 240 240 240 240 240 216 206 184 184 184 210 220 77 | 240 240 240 240 240 240 240 240 240 186 72 51 61 65 76 98 78 | 233 240 240 240 240 240 240 240 240 240 231 213 74 219 240 240 79 | 240 240 240 240 240 240 240 240 240 240 240 187 51 219 240 240 80 | 235 240 240 240 240 240 240 240 240 156 117 74 77 89 71 186 81 | 235 229 227 240 236 234 236 232 229 225 150 150 144 117 148 140 82 | 233 239 240 240 240 240 240 240 240 240 240 216 85 72 155 209 83 | 240 240 240 240 240 240 240 240 240 240 226 53 186 163 153 175 84 | 240 240 240 240 240 240 240 240 240 240 180 50 217 240 118 77 85 | 240 240 240 240 240 240 240 240 240 240 240 120 45 155 51 152 86 | 238 240 240 240 240 240 240 240 240 240 240 240 150 125 137 230 87 | 240 240 240 240 240 240 240 240 240 240 225 30 151 237 232 235 88 | 236 240 240 240 240 240 240 240 240 240 236 92 17 107 95 178 89 | 240 240 240 240 240 240 240 240 240 240 238 200 148 128 121 206 90 | 235 240 240 240 240 240 240 240 240 180 151 170 110 101 108 154 91 | 240 240 240 240 240 240 240 240 240 225 115 109 106 109 85 141 92 | 234 240 240 240 240 240 240 240 240 212 205 184 184 179 201 220 93 | 240 240 240 240 240 240 240 240 240 182 66 47 50 58 76 98 94 | 233 240 240 240 240 240 240 240 240 240 228 211 73 219 240 240 95 | 240 240 240 240 240 240 240 240 240 240 238 186 51 219 240 240 96 | 235 240 240 240 240 240 240 240 240 156 116 84 83 98 86 189 97 | 234 229 227 240 234 232 235 232 228 225 150 156 147 127 155 149 98 | 99 | 209 175 77 152 227 236 191 207 152 135 219 98 240 240 186 149 100 | 155 161 119 51 129 240 108 122 109 98 210 76 240 240 71 149 101 | 64 161 232 155 124 237 109 128 92 121 180 58 222 220 84 121 102 | 83 186 217 45 150 150 5 142 101 113 184 53 78 47 63 149 103 | 207 53 50 120 237 29 72 197 169 113 187 62 213 181 62 150 104 | 240 226 180 237 226 223 228 238 147 115 208 75 231 238 107 150 105 | 240 240 240 238 237 240 240 240 174 225 216 186 240 240 149 225 106 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 229 107 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 231 108 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 236 109 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 234 110 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 236 111 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 112 | 239 240 240 240 240 240 240 240 240 240 240 240 240 240 240 227 113 | 228 240 240 240 240 240 240 240 240 240 240 240 240 240 240 229 114 | 232 240 240 240 238 240 237 240 235 240 234 240 233 240 235 234 115 | 212 176 77 152 230 236 192 207 156 150 220 98 240 240 186 140 116 | 164 166 120 51 137 240 99 121 109 109 210 76 240 240 71 148 117 | 73 170 240 156 125 236 100 128 92 121 184 65 219 219 89 117 118 | 85 186 217 52 152 150 5 142 101 106 184 61 74 51 77 144 119 | 216 53 50 120 237 29 78 197 168 103 184 51 213 187 74 150 120 | 240 226 180 240 237 223 232 240 162 115 206 72 231 240 117 150 121 | 240 240 240 240 240 240 240 240 186 225 216 186 240 240 156 225 122 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 229 123 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 232 124 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 236 125 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 234 126 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 236 127 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 128 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 227 129 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 229 130 | 240 240 240 240 238 240 236 240 235 240 234 240 233 240 235 235 131 | 209 175 77 152 230 235 178 206 154 141 220 98 240 240 189 149 132 | 155 153 118 51 137 232 95 121 108 85 201 76 240 240 86 155 133 | 72 163 240 155 125 237 107 128 101 109 179 58 219 219 98 127 134 | 85 186 217 45 150 151 17 148 110 106 184 50 73 51 83 147 135 | 216 53 50 120 240 30 92 200 170 109 184 47 211 186 84 156 136 | 240 226 180 240 240 225 236 238 151 115 205 66 228 238 116 150 137 | 240 240 240 240 240 240 240 240 180 225 212 182 240 240 156 225 138 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 228 139 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 232 140 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 235 141 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 232 142 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 234 143 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 144 | 240 240 240 240 240 240 240 240 240 240 240 240 240 240 240 227 145 | 239 240 240 240 240 240 240 240 240 240 240 240 240 240 240 229 146 | 233 240 240 240 238 240 236 240 235 240 234 240 233 240 235 234 147 | 148 | 149 149 121 149 150 150 225 229 231 236 234 236 240 227 229 234 149 | 186 71 84 63 62 107 149 240 240 240 240 240 240 240 240 235 150 | 240 240 220 47 181 238 240 240 240 240 240 240 240 240 240 240 151 | 240 240 222 78 213 231 240 240 240 240 240 240 240 240 240 233 152 | 98 76 58 53 62 75 186 240 240 240 240 240 240 240 240 240 153 | 219 210 180 184 187 208 216 240 240 240 240 240 240 240 240 234 154 | 135 98 121 113 113 115 225 240 240 240 240 240 240 240 240 240 155 | 152 109 92 101 169 147 174 240 240 240 240 240 240 240 240 235 156 | 207 122 128 142 197 238 240 240 240 240 240 240 240 240 240 240 157 | 191 108 109 5 72 228 240 240 240 240 240 240 240 240 240 237 158 | 236 240 237 150 29 223 240 240 240 240 240 240 240 240 240 240 159 | 227 129 124 150 237 226 237 240 240 240 240 240 240 240 240 238 160 | 152 51 155 45 120 237 238 240 240 240 240 240 240 240 240 240 161 | 77 119 232 217 50 180 240 240 240 240 240 240 240 240 240 240 162 | 175 161 161 186 53 226 240 240 240 240 240 240 240 240 240 240 163 | 209 155 64 83 207 240 240 240 240 240 240 240 240 239 228 232 164 | 140 148 117 144 150 150 225 229 232 236 234 236 240 227 229 235 165 | 186 71 89 77 74 117 156 240 240 240 240 240 240 240 240 235 166 | 240 240 219 51 187 240 240 240 240 240 240 240 240 240 240 240 167 | 240 240 219 74 213 231 240 240 240 240 240 240 240 240 240 233 168 | 98 76 65 61 51 72 186 240 240 240 240 240 240 240 240 240 169 | 220 210 184 184 184 206 216 240 240 240 240 240 240 240 240 234 170 | 150 109 121 106 103 115 225 240 240 240 240 240 240 240 240 240 171 | 156 109 92 101 168 162 186 240 240 240 240 240 240 240 240 235 172 | 207 121 128 142 197 240 240 240 240 240 240 240 240 240 240 240 173 | 192 99 100 5 78 232 240 240 240 240 240 240 240 240 240 236 174 | 236 240 236 150 29 223 240 240 240 240 240 240 240 240 240 240 175 | 230 137 125 152 237 237 240 240 240 240 240 240 240 240 240 238 176 | 152 51 156 52 120 240 240 240 240 240 240 240 240 240 240 240 177 | 77 120 240 217 50 180 240 240 240 240 240 240 240 240 240 240 178 | 176 166 170 186 53 226 240 240 240 240 240 240 240 240 240 240 179 | 212 164 73 85 216 240 240 240 240 240 240 240 240 240 240 240 180 | 149 155 127 147 156 150 225 228 232 235 232 234 240 227 229 234 181 | 189 86 98 83 84 116 156 240 240 240 240 240 240 240 240 235 182 | 240 240 219 51 186 238 240 240 240 240 240 240 240 240 240 240 183 | 240 240 219 73 211 228 240 240 240 240 240 240 240 240 240 233 184 | 98 76 58 50 47 66 182 240 240 240 240 240 240 240 240 240 185 | 220 201 179 184 184 205 212 240 240 240 240 240 240 240 240 234 186 | 141 85 109 106 109 115 225 240 240 240 240 240 240 240 240 240 187 | 154 108 101 110 170 151 180 240 240 240 240 240 240 240 240 235 188 | 206 121 128 148 200 238 240 240 240 240 240 240 240 240 240 240 189 | 178 95 107 17 92 236 240 240 240 240 240 240 240 240 240 236 190 | 235 232 237 151 30 225 240 240 240 240 240 240 240 240 240 240 191 | 230 137 125 150 240 240 240 240 240 240 240 240 240 240 240 238 192 | 152 51 155 45 120 240 240 240 240 240 240 240 240 240 240 240 193 | 77 118 240 217 50 180 240 240 240 240 240 240 240 240 240 240 194 | 175 153 163 186 53 226 240 240 240 240 240 240 240 240 240 240 195 | 209 155 72 85 216 240 240 240 240 240 240 240 240 240 239 233 196 | 197 | --------------------------------------------------------------------------------