├── .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 |

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 | Archived Repository
5 | This code is no longer maintained. Feel free to fork it, but use it at your own risks.
6 | |
7 |
8 |
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 | [](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 |

42 |
Pan with your finger to drag the picture on the paper
43 |
44 |
45 |

46 |
Pinch to zoom the picture in or out and fit the sheet
47 |
48 |
49 |

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 |
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 | -
123 |
126 |
131 |
132 | -
133 | Put it on a sheet of paper
134 |
135 | -
136 | Choose something to draw
137 |
138 |
139 |
140 |
141 |
142 |
143 |
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 |
--------------------------------------------------------------------------------