Skip to content

Commit 213e17b

Browse files
OriROri Riner
authored and
Ori Riner
committed
initial commit
0 parents  commit 213e17b

17 files changed

+2738
-0
lines changed

.gitignore

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
.idea
14+
.DS_Store
15+
.env.local
16+
.env.development.local
17+
.env.test.local
18+
.env.production.local
19+
20+
npm-debug.log*
21+
yarn-debug.log*
22+
yarn-error.log*

README.md

+2,138
Large diffs are not rendered by default.

package.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "react-emojify",
3+
"version": "0.0.1",
4+
"homepage": "http://orir.github.io/react-emojify",
5+
"dependencies": {
6+
"react": "^15.6.1",
7+
"prop-types": "^15.5.10",
8+
"react-dom": "^15.6.1",
9+
"react-scripts": "1.0.10"
10+
},
11+
"scripts": {
12+
"start": "react-scripts start",
13+
"build": "react-scripts build",
14+
"test": "react-scripts test --env=jsdom",
15+
"eject": "react-scripts eject",
16+
"predeploy": "npm run build",
17+
"deploy": "gh-pages -d build"
18+
},
19+
"devDependencies": {
20+
"gh-pages": "^1.0.0"
21+
}
22+
}

public/index.html

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<meta name="theme-color" content="#000000">
7+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
8+
<title>React EmojifyImage</title>
9+
</head>
10+
<body>
11+
<noscript>
12+
You need to enable JavaScript to run this app.
13+
</noscript>
14+
<div id="root"></div>
15+
</body>
16+
</html>

public/manifest.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
}

src/App.css

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.App {
2+
text-align: center;
3+
display: flex;
4+
flex-flow: column;
5+
align-items: center;
6+
}
7+
8+
.input-container {
9+
display: flex;
10+
width: 40%;
11+
justify-content: space-between;
12+
}
13+
14+
.scale-container {
15+
display: flex;
16+
}

src/App.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React, { Component } from 'react';
2+
import './App.css';
3+
4+
import EmojifyImage from './EmojifyImage';
5+
6+
class App extends Component {
7+
constructor(props) {
8+
super(props);
9+
10+
this.state = { scale: 15 };
11+
}
12+
13+
render() {
14+
return (
15+
<div className="App">
16+
<div className="input-container">
17+
<input type="file" accept="image/*" onChange={(e) => {
18+
const [ file ] = e.currentTarget.files;
19+
if (!file) return;
20+
21+
createImageBitmap(file).then(ibm => {
22+
this.setState({ image: ibm });
23+
});
24+
}} />
25+
<div className="scale-container">
26+
{ 'Scale (px): ' }
27+
<input type="range" min="3" max="25" step="1" value={this.state.scale} onChange={(e) => { this.setState({ scale: Number(e.currentTarget.value) }) }}/>
28+
{ this.state.scale }
29+
</div>
30+
</div>
31+
{ this.state.image && <EmojifyImage scale={this.state.scale} image={this.state.image}/> }
32+
</div>
33+
);
34+
}
35+
}
36+
37+
export default App;

src/EmojifyImage.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import EmojifyImageCustom from './EmojifyImageCustom';
3+
import emojis from './emojis';
4+
5+
const EmojifyImage = (props) => <EmojifyImageCustom emojis={ emojis } { ...props }/>;
6+
const originalPropNames = Object.keys(EmojifyImageCustom.propTypes);
7+
const originalProps = Object.assign({}, EmojifyImageCustom.propTypes);
8+
const _reduce = (array, reducer, seed) => array.reduce((...args) => { reducer(...args); return args[0] }, seed);
9+
10+
EmojifyImage.propTypes = _reduce(originalPropNames, (props, key) => key === 'emojis' && delete props[key], originalProps);
11+
12+
export default EmojifyImage;
13+

src/EmojifyImageCustom.js

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import mapColors from './mapColorsWorker';
5+
import emojify from './emojifyWorker';
6+
7+
/**
8+
* @class EmojifyImageCustom
9+
**/
10+
export default class EmojifyImageCustom extends Component {
11+
12+
_remap(emojis) {
13+
mapColors(emojis).then(mapping => {
14+
this.setState({mapping, isMappingEmojis: false});
15+
});
16+
}
17+
18+
_redraw(props) {
19+
if (!this.canvas) return;
20+
21+
const context = this.canvas.getContext('2d');
22+
23+
this.canvas.width = props.image.width;
24+
this.canvas.height = props.image.height;
25+
context.drawImage(props.image, 0, 0);
26+
const imageData = context.getImageData(0, 0, this.canvas.width, this.canvas.height);
27+
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
28+
29+
this.setState({isDrawing: true});
30+
31+
emojify(this.state.mapping, props.scale, imageData).then((emojifiedData) => {
32+
context.font = `${props.scale * 1.3}px sans-serif`;
33+
emojifiedData.forEach(emojiData => {
34+
context.fillText(emojiData.emoji, emojiData.x, emojiData.y);
35+
});
36+
this.setState({isDrawing: false});
37+
});
38+
}
39+
40+
/**
41+
* @constructor EmojifyImageCustom
42+
**/
43+
constructor(props) {
44+
super(props);
45+
46+
this.state = {
47+
isMappingEmojis: true
48+
};
49+
50+
this._remap(props.emojis);
51+
}
52+
53+
componentWillReceiveProps(nextProps) {
54+
const shouldMapEmojis = nextProps.emojis !== this.props.emojis || nextProps.emojis.length !== this.props.emojis.length;
55+
56+
if (shouldMapEmojis) {
57+
this.setState({isMappingEmojis: true});
58+
this._remap(nextProps.emojis);
59+
}
60+
}
61+
62+
shouldComponentUpdate(nextProps, nextState) {
63+
const shouldMapEmojis = nextProps.emojis !== this.props.emojis || nextProps.emojis.length !== this.props.emojis.length;
64+
const hasNewProps = nextProps.scale !== this.props.scale || nextProps.image !== this.props.image;
65+
const hasMappedEmojis = nextState.isMappingEmojis !== this.state.isMappingEmojis && nextState.isMappingEmojis === false;
66+
67+
return shouldMapEmojis || hasNewProps || hasMappedEmojis;
68+
}
69+
70+
componentDidUpdate() {
71+
this._redraw(this.props);
72+
}
73+
74+
/**
75+
* Render
76+
* @returns {XML}
77+
**/
78+
render() {
79+
const Loader = this.props.loader;
80+
return (
81+
<div style={{display: 'flex', flexFlow: 'column'}}>
82+
{ (this.state.isDrawing || this.state.isMappingEmojis) ? <Loader /> : null }
83+
<canvas style={{width: this.props.image.width, height: this.props.image.height}} ref={r => (this.canvas = r)}/>
84+
</div>
85+
);
86+
}
87+
}
88+
89+
EmojifyImageCustom.defaultProps = {
90+
scale: 15,
91+
loader: () => <span>Loading...</span>
92+
};
93+
94+
EmojifyImageCustom.propTypes = {
95+
/**
96+
* An array of emojis to use when emojifying the image.
97+
*/
98+
emojis: PropTypes.arrayOf(PropTypes.string).isRequired,
99+
/**
100+
* An ImageBitmap object of the actual image to use.
101+
* The easiest way to get this object is to use `createImageBitmap()`.
102+
* It gets an image source, which can be an <img>, SVG <image>, <video>, OffscreenCanvas, or <canvas> element, a Blob, ImageData, or another ImageBitmap object.
103+
*/
104+
image: PropTypes.instanceOf(ImageBitmap).isRequired,
105+
/**
106+
* The number of (scale X scale) pixels to replace with one emoji.
107+
*/
108+
scale: PropTypes.number,
109+
/**
110+
* The loader component to render when either remapping colors to emojis or when emojifying the image
111+
*/
112+
loader: PropTypes.func
113+
};

src/emojify.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const emojify = (mapping, scale, imageData) => {
2+
const emojifiedData = [];
3+
const channels = ['red', 'green', 'blue', 'alpha'];
4+
5+
const _reduce = (array, reducer, seed) => array.reduce((...args) => {
6+
reducer(...args);
7+
return args[0];
8+
}, seed);
9+
10+
const distance = (colorA, colorB) => {
11+
return Math.sqrt(
12+
Math.pow(colorA.red - colorB.red, 2) +
13+
Math.pow(colorA.green - colorB.green, 2) +
14+
Math.pow(colorA.blue - colorB.blue, 2) +
15+
Math.pow(colorA.alpha - colorB.alpha, 2));
16+
};
17+
18+
for(let row = 0; row < imageData.height; row += scale) {
19+
for(let col = 0; col < imageData.width; col += scale) {
20+
const sums = { red: 0, green: 0, blue: 0, alpha: 0, total: 0 };
21+
22+
for(let pixelRowIndex = 0; pixelRowIndex < scale;pixelRowIndex++) {
23+
for(let pixelColIndex = 0; pixelColIndex < scale;pixelColIndex++) {
24+
const pixelIndex = (pixelColIndex + col + (row + pixelRowIndex) * imageData.width) * 4;
25+
const pixel = _reduce(channels,(pixel, channel, index) => pixel[channel] = imageData.data[index + pixelIndex], {});
26+
channels.forEach(channel => {
27+
sums[channel] += Number(pixel[channel]);
28+
});
29+
sums.total++;
30+
}
31+
}
32+
33+
const color = _reduce(channels, (color, channel) => color[channel] = sums[channel] / sums.total, {});
34+
35+
const closest = mapping.reduce((closest, current) => {
36+
const currentDistance = distance(current.color, color);
37+
38+
if(closest.distance > currentDistance) {
39+
return Object.assign({}, current, { distance: currentDistance });
40+
}
41+
42+
return closest;
43+
}, Object.assign({}, mapping[0], {distance: distance(mapping[0].color, color)}));
44+
45+
emojifiedData.push({ emoji: closest.emoji, x: col, y: row + scale });
46+
}
47+
}
48+
49+
return emojifiedData;
50+
};
51+
52+
export default emojify;

src/emojifyWorker.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import emojifier from './emojify';
2+
import promiseWorker from './promiseWorker';
3+
4+
const worker = promiseWorker({
5+
internal: {
6+
emojify: emojifier,
7+
execute: function(self, data) {
8+
const mapping = data.args[0];
9+
const scale = data.args[1];
10+
const imageData = data.args[2];
11+
self.postMessage({
12+
action: 'done',
13+
data: { id: data.id, emojifiedData: this.emojify(mapping, scale, imageData) }
14+
});
15+
}
16+
},
17+
external: {
18+
done: (worker, data, context) => {
19+
context.resolvers[data.id](data.emojifiedData);
20+
delete context.resolvers[data.id];
21+
}
22+
}
23+
});
24+
25+
export default (mapping, scale, imageData) => worker('execute', mapping, scale, imageData);

0 commit comments

Comments
 (0)