WIP: Add initial meme generator! #748
15 changed files with 771 additions and 37 deletions
|
@ -4,6 +4,7 @@
|
||||||
// be a flex container for children
|
// be a flex container for children
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropzone {
|
.dropzone {
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
// be a flex container for children
|
// be a flex container for children
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 1em;
|
||||||
-webkit-flex-direction: column;
|
-webkit-flex-direction: column;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -52,8 +54,37 @@
|
||||||
|
|
||||||
.dropzone-preview-image {
|
.dropzone-preview-image {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1em;
|
width: 100%;
|
||||||
width: calc(100% - 2em);
|
}
|
||||||
|
|
||||||
|
.dropzone-preview-memeify {
|
||||||
|
margin-top: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-memeify-button {
|
||||||
|
background: $primary-color;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: .8em;
|
||||||
|
padding: 3px 6px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-memeify-saveMessage {
|
||||||
|
padding-top: .25em;
|
||||||
|
position: relative;
|
||||||
|
top: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-memeify-toolbar {
|
||||||
|
/* TODO: Cleanup `!important` */
|
||||||
|
background: $primary-color !important;
|
||||||
|
left: -1em !important;
|
||||||
|
right: -1em !important;
|
||||||
|
top: -4em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropzone-instructions-display__chooser-label {
|
.dropzone-instructions-display__chooser-label {
|
||||||
|
|
99
client/src/components/Creatify/EditableFontface/index.js
Normal file
99
client/src/components/Creatify/EditableFontface/index.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
const DEFAULT_TEXT_RENDER = (text) => text;
|
||||||
|
|
||||||
|
export default class EditableFontface extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
blinkSelection: props.blinkSelection == false ? false : true,
|
||||||
|
value: props.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.textInput = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const textInput = this.textInput.current;
|
||||||
|
|
||||||
|
if(textInput) {
|
||||||
|
textInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const me = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
blinkSelection,
|
||||||
|
value
|
||||||
|
} = me.state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
editable = true,
|
||||||
|
fontFace,
|
||||||
|
preview,
|
||||||
|
} = me.props;
|
||||||
|
|
||||||
|
const textRender = fontFace.textRender || DEFAULT_TEXT_RENDER;
|
||||||
|
|
||||||
|
const textStyles = Object.assign({
|
||||||
|
...(blinkSelection ? {
|
||||||
|
animation: 'textBlink 1s infinite',
|
||||||
|
} : {}),
|
||||||
|
minHeight: '20px',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
MozOsxFontSmoothing: 'grayscale',
|
||||||
|
}, fontFace.text, preview ? fontFace.previewOverrides : {});
|
||||||
|
|
||||||
|
const fontInput = (editable === true) ? (
|
||||||
|
<input ref={this.textInput} type="text" onKeyPress={(e) => me.onKeyPress(e)} onChange={(e) => me.onChange(e)} style={{
|
||||||
|
...{
|
||||||
|
bottom: 0,
|
||||||
|
opacity: 0,
|
||||||
|
padding: 0,
|
||||||
|
left: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
width: '100%',
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
...(fontFace.editorStyle || {}),
|
||||||
|
}} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<style scoped>{'@keyframes textBlink { 0% { opacity: 1 } 30% { opacity: 0.6 } 60% { opacity: 1 } }'}</style>
|
||||||
|
{fontInput}
|
||||||
|
<div ref={me.state.fontRender} style={textStyles} title={value}>{textRender(value)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyPress(e) {
|
||||||
|
this.setState({
|
||||||
|
blinkSelection: false,
|
||||||
|
value: e.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(e) {
|
||||||
|
this.setState({
|
||||||
|
blinkSelection: false,
|
||||||
|
value: e.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PRESETS = {
|
||||||
|
'Green Machine': require('../FontFaces/GreenMachine'),
|
||||||
|
'Inferno': require('../FontFaces/Inferno'),
|
||||||
|
'Lazer': require('../FontFaces/Lazer'),
|
||||||
|
'Neon': require('../FontFaces/Neon'),
|
||||||
|
'Old Blue': require('../FontFaces/OldBlue'),
|
||||||
|
'Retro Rainbow': require('../FontFaces/RetroRainbow'),
|
||||||
|
'The Special': require('../FontFaces/TheSpecial'),
|
||||||
|
'Vapor Wave': require('../FontFaces/VaporWave'),
|
||||||
|
}
|
0
client/src/components/Creatify/FontFaces/.gitkeep
Normal file
0
client/src/components/Creatify/FontFaces/.gitkeep
Normal file
15
client/src/components/Creatify/FontFaces/GreenMachine.js
Normal file
15
client/src/components/Creatify/FontFaces/GreenMachine.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module.exports = {
|
||||||
|
container: {},
|
||||||
|
editorStyle: {
|
||||||
|
fontFamily: 'courier, Courier New',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '2em',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: '#00b700',
|
||||||
|
fontFamily: 'courier, Courier New',
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textShadow: '1px 1px 2px #003605',
|
||||||
|
},
|
||||||
|
};
|
19
client/src/components/Creatify/FontFaces/Inferno.js
Normal file
19
client/src/components/Creatify/FontFaces/Inferno.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
module.exports = {
|
||||||
|
container: {},
|
||||||
|
editorStyle: {
|
||||||
|
fontFamily: 'helvetica, Helvetica Nue',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '2em',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: 'helvetica, Helvetica Nue',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '2em',
|
||||||
|
color: '#fff',
|
||||||
|
textShadow: '0px 0px 3px #c20000, 0px 0px 3px #e0f2ff, 0px 0px 5px #532761, 0px 0px 20px #670606, 0 0 10px rgba(0, 0, 0, .8), 0 0 10px #fefcc9, 5px -5px 15px #feec85, -10px -10px 20px #ffae34, 10px -20px 25px #ec760c, -10px -30px 30px #cd4606, 0 -40px 35px #973716, 5px -45px 40px #451b0e, 0 -2px 15px rgba(255, 200, 160, .5)',
|
||||||
|
},
|
||||||
|
previewOverrides: {
|
||||||
|
fontSize: '1.5em',
|
||||||
|
padding: '0 1rem 0 1rem',
|
||||||
|
},
|
||||||
|
};
|
23
client/src/components/Creatify/FontFaces/Lazer.js
Normal file
23
client/src/components/Creatify/FontFaces/Lazer.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
module.exports = {
|
||||||
|
container: {},
|
||||||
|
editorStyle: {
|
||||||
|
fontFamily: 'helvetica, Helvetica Nue',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '2em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: 'helvetica, Helvetica Nue',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
backgroundImage: 'linear-gradient(180deg, #249bff 0%, #e1f8ff 44%, #3a006b 44%, #ff57d6 100%)',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
fontSize: '2em',
|
||||||
|
color: 'transparent',
|
||||||
|
filter: 'drop-shadow(0 0 .05rem black)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextStroke: '0.03em rgba(255, 255, 255, 0.6)',
|
||||||
|
},
|
||||||
|
};
|
21
client/src/components/Creatify/FontFaces/Neon.js
Normal file
21
client/src/components/Creatify/FontFaces/Neon.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
module.exports = {
|
||||||
|
container: {},
|
||||||
|
editorStyle: {
|
||||||
|
fontFamily: 'Helvetica, Arial',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '2em',
|
||||||
|
letterSpacing: '.1em',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: '#fff',
|
||||||
|
fontFamily: 'Helvetica, Arial',
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
letterSpacing: '.1em',
|
||||||
|
textShadow: '0 0 0.05em #fff, 0 0 0.1em #fff, 0 0 0.2em #fff, 0 0 .2em #ff1de2, 0 0 .4em #ff26e3, 0 0 .5em #ff00de, 0 0 1em #ff61eb, 0 0 1.5em #ff7cee',
|
||||||
|
},
|
||||||
|
previewOverrides: {
|
||||||
|
fontSize: '1.8em',
|
||||||
|
padding: '0 1rem 0 1rem',
|
||||||
|
},
|
||||||
|
};
|
39
client/src/components/Creatify/FontFaces/OldBlue.js
Normal file
39
client/src/components/Creatify/FontFaces/OldBlue.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const charToFullWidth = char => {
|
||||||
|
const c = char.charCodeAt( 0 )
|
||||||
|
return c >= 33 && c <= 126
|
||||||
|
? String.fromCharCode( ( c - 33 ) + 65281 )
|
||||||
|
: char
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringToFullWidth =
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
container: {},
|
||||||
|
editorStyle: {},
|
||||||
|
text: {
|
||||||
|
fontFamily: 'Segoe UI,Helvetica,Arial',
|
||||||
|
},
|
||||||
|
previewOverrides: {
|
||||||
|
height: '2.6rem',
|
||||||
|
},
|
||||||
|
textRender: (text) => {
|
||||||
|
const id = `curve-${text.replace(/[^A-Za-z0-9]/g, '')}-oceanwave`
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 500 50" style={{ height: '4em', fontFamily: 'Arial', fontWeight: 'bold' }}>
|
||||||
|
<path id={id} fill="transparent" d="M 0 50 Q 50 0 100 50 Q 150 100 200 50 Q 250 0 300 50 Q 350 100 400 50 Q 450 0 500 50 Q 550 100 600 50 " transform="scale(1 0.5) translate(0 15)" />
|
||||||
|
<text x="10" style={{ fill: '#4dc2fe', fontWeight: 900, letterSpacing: '-0.15em', textShadow: '0.15em -0.1em #1c55a0' }}>
|
||||||
|
<textPath href={`#${id}`}>
|
||||||
|
{text}
|
||||||
|
</textPath>
|
||||||
|
</text>
|
||||||
|
<text x="10" style={{ fill: 'transparent', stroke: '#1c55a0', strokeWidth: '.012em', fontWeight: 900, letterSpacing: '-0.15em' }}>
|
||||||
|
<textPath href={`#${id}`}>
|
||||||
|
{text}
|
||||||
|
</textPath>
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
21
client/src/components/Creatify/FontFaces/RetroRainbow.js
Normal file
21
client/src/components/Creatify/FontFaces/RetroRainbow.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
module.exports = {
|
||||||
|
container: {},
|
||||||
|
editorStyle: {
|
||||||
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '1.2em',
|
||||||
|
transform: 'scale(1, 1.5)',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
backgroundImage: 'linear-gradient(to right, #b306a9, #ef2667, #f42e2c, #ffa509, #fdfc00, #55ac2f, #0b13fd, #a804af)',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
fontSize: '1.2em',
|
||||||
|
transform: 'scale(1, 1.5)',
|
||||||
|
color: 'transparent',
|
||||||
|
paddingBottom: '.25em',
|
||||||
|
paddingTop: '.1em',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
},
|
||||||
|
};
|
41
client/src/components/Creatify/FontFaces/TheSpecial.js
Normal file
41
client/src/components/Creatify/FontFaces/TheSpecial.js
Normal file
File diff suppressed because one or more lines are too long
43
client/src/components/Creatify/FontFaces/VaporWave.js
Normal file
43
client/src/components/Creatify/FontFaces/VaporWave.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const charToFullWidth = char => {
|
||||||
|
const c = char.charCodeAt( 0 )
|
||||||
|
return c >= 33 && c <= 126
|
||||||
|
? String.fromCharCode( ( c - 33 ) + 65281 )
|
||||||
|
: char
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringToFullWidth =
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
container: {},
|
||||||
|
editorStyle: {},
|
||||||
|
text: {
|
||||||
|
fontFamily: 'Segoe UI,Helvetica,Arial',
|
||||||
|
},
|
||||||
|
previewOverrides: {
|
||||||
|
transform: 'rotate(39deg)',
|
||||||
|
height: '7rem',
|
||||||
|
paddingLeft: '2rem',
|
||||||
|
margin: '-2rem 0',
|
||||||
|
},
|
||||||
|
textRender: (text) => {
|
||||||
|
const formattedText = text.toLowerCase().split('').map((char) => {
|
||||||
|
const c = char.charCodeAt( 0 )
|
||||||
|
return (c >= 33 && c <= 126) ? String.fromCharCode(c + 65248) : char
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// TODO: Inline the path
|
||||||
|
const id = `curve-${text.replace(/[^A-Za-z0-9]/g, '')}-oceanwave`
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 500 160" style={{ height: '10em' }}>
|
||||||
|
<path id={id} fill="transparent" d="M6,150C49.63,93,105.79,36.65,156.2,47.55,207.89,58.74,213,131.91,264,150c40.67,14.43,108.57-6.91,229-145" />
|
||||||
|
<text x="10">
|
||||||
|
<textPath href={`#${id}`}>
|
||||||
|
{formattedText}
|
||||||
|
</textPath>
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
60
client/src/components/Creatify/RichDraggable/index.js
Normal file
60
client/src/components/Creatify/RichDraggable/index.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Draggable from 'react-draggable';
|
||||||
|
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = document.body;
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
export default class RichDraggable extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.contents = React.createRef();
|
||||||
|
this.state = {
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const height = this.contents.current.offsetHeight;
|
||||||
|
const width = this.contents.current.offsetWidth;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const me = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
props,
|
||||||
|
state,
|
||||||
|
} = me;
|
||||||
|
|
||||||
|
const {
|
||||||
|
height: bottom,
|
||||||
|
width: right,
|
||||||
|
} = props.bounds;
|
||||||
|
|
||||||
|
const bounds = {
|
||||||
|
//top: 0,
|
||||||
|
//left: 0,
|
||||||
|
right: right - state.width,
|
||||||
|
bottom: bottom - state.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable {...props} bounds={bounds} offsetParent={body} cancel=".no-drag">
|
||||||
|
<div ref={me.contents} style={{ padding: '15px', position: 'absolute', border: '4px dashed #ddd', cursor: 'move' }} className="creatifyDecor">
|
||||||
|
<div className="no-drag" style={{ position: 'relative', cursor: 'auto' }}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
236
client/src/components/Creatify/index.js
Normal file
236
client/src/components/Creatify/index.js
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Select from 'react-select'
|
||||||
|
|
||||||
|
import RichDraggable from './RichDraggable';
|
||||||
|
import EditableFontface, { PRESETS as FontPresets } from './EditableFontface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
faFont,
|
||||||
|
faMinusCircle,
|
||||||
|
faPlusCircle,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
const getRasterizedCanvas = (contents, width, height) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Parse to xHTML for SVG/foreignObject rendering
|
||||||
|
contents = new XMLSerializer().serializeToString(
|
||||||
|
new DOMParser().parseFromString(contents, 'text/html')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resolves a bug in Chrome where it renders correctly, but
|
||||||
|
// replaces the inline styles with an invalid `background-clip`.
|
||||||
|
if(/Chrome/.test(navigator.userAgent)) {
|
||||||
|
contents = contents.replace(/background\-clip:(\s*text\s*)[;$]/g,
|
||||||
|
(match, group) => (`-webkit-background-clip:text;${match}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to match font kerning with the DOM.
|
||||||
|
const kerningAndPadding = '<style>svg{font-kerning:normal}body{padding:0;margin:0}</style>';
|
||||||
|
const svgContents = `<svg xmlns="http://www.w3.org/2000/svg" width="${width * 2}" height="${height * 2}">
|
||||||
|
<foreignObject x="0" y="0" width="${width * 2}" height="${height * 2}" externalResourcesRequired="true">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"><head>${kerningAndPadding}</head><body>${contents}</body></html>
|
||||||
|
</foreignObject></svg>`;
|
||||||
|
|
||||||
|
const pixelRatio = 2;
|
||||||
|
|
||||||
|
let img = document.createElement('img');
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
img.height = canvas.height = height * pixelRatio;
|
||||||
|
img.width = canvas.width = width * pixelRatio;
|
||||||
|
canvas.style.height = `${height}px`;
|
||||||
|
canvas.style.width = `${width}px`;
|
||||||
|
|
||||||
|
let shadowNode = document.createElement('div');
|
||||||
|
Object.assign(shadowNode.style, {
|
||||||
|
height: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
document.body.appendChild(shadowNode);
|
||||||
|
|
||||||
|
shadowNode.appendChild(img);
|
||||||
|
|
||||||
|
var svg64 = btoa(unescape(encodeURIComponent(svgContents)));
|
||||||
|
var b64Start = 'data:image/svg+xml;base64,';
|
||||||
|
var image64 = b64Start + svg64;
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
// We still can't trust Firefox's %$%&* engine, add another 5ms timeout
|
||||||
|
// `background-clip: text` is very broken and does not always render in time.
|
||||||
|
setTimeout(() => {
|
||||||
|
let context = canvas.getContext('2d', { alpha: false });
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.fillStyle = 'white';
|
||||||
|
context.imageSmoothingEnabled = false;
|
||||||
|
context.scale(pixelRatio, pixelRatio);
|
||||||
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
document.body.removeChild(shadowNode);
|
||||||
|
|
||||||
|
resolve(canvas);
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
img.src = image64;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Creatify extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const fontKeys = Object.keys(FontPresets);
|
||||||
|
|
||||||
|
this.contents = React.createRef();
|
||||||
|
|
||||||
|
const fontOptions = fontKeys.map(
|
||||||
|
(fontName) => (
|
||||||
|
{
|
||||||
|
value: fontName,
|
||||||
|
label: (
|
||||||
|
<div style={{ maxHeight: '150px', maxWidth: '100%', fontSize: '16px', overflow: 'hidden' }}>
|
||||||
|
<EditableFontface key={fontName} fontFace={FontPresets[fontName]} preview={true} value={fontName} editable={false} blinkSelection={false} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
fontName,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
activeElement: false,
|
||||||
|
bounds: {},
|
||||||
|
fontName: fontKeys[0],
|
||||||
|
elements: [],
|
||||||
|
fontOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// TODO: Fix bounds
|
||||||
|
/*
|
||||||
|
const bounds = this.contents.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
bounds,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
bounds
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveElement(activeElement) {
|
||||||
|
this.setState({ activeElement });
|
||||||
|
}
|
||||||
|
|
||||||
|
addElement() {
|
||||||
|
const {
|
||||||
|
state
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const newElementKey = `element-${state.elements.length}-${Date.now()}`;
|
||||||
|
|
||||||
|
const newElement = (
|
||||||
|
<RichDraggable key={newElementKey} bounds={state.bounds} onStart={() => this.setActiveElement(newElement)}>
|
||||||
|
<EditableFontface fontFace={FontPresets[state.fontName]} value="Start Typing!" />
|
||||||
|
</RichDraggable>
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
elements: [...state.elements, newElement],
|
||||||
|
activeElement: newElement,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeElement() {
|
||||||
|
const {
|
||||||
|
state
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const activeElementIndex = state.elements.indexOf(state.activeElement);
|
||||||
|
|
||||||
|
if(state.elements.length === 0 || activeElementIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = [...state.elements];
|
||||||
|
elements.splice(activeElementIndex, 1)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
activeElement: false,
|
||||||
|
elements,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSave() {
|
||||||
|
const renderedCanvas = await this.renderContentsToCanvas();
|
||||||
|
|
||||||
|
if(this.props.onSave) {
|
||||||
|
this.props.onSave(renderedCanvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderContentsToCanvas() {
|
||||||
|
const me = this;
|
||||||
|
|
||||||
|
const contentsElement = me.contents.current;
|
||||||
|
let contents = contentsElement.outerHTML;
|
||||||
|
|
||||||
|
// Cheap border/handles removal
|
||||||
|
contents = `<style>.creatifyDecor{border-color:transparent!important;background-color:transparent!important}</style>` + contents;
|
||||||
|
|
||||||
|
const contentsWidth = contentsElement.offsetWidth;
|
||||||
|
const contentsHeight = contentsElement.offsetHeight;
|
||||||
|
|
||||||
|
// Fix the dimensions, fixes when flex is used.
|
||||||
|
contents = `<div style="height:${contentsHeight}px;width:${contentsWidth}px">${contents}</div>`;
|
||||||
|
|
||||||
|
return await getRasterizedCanvas(contents, contentsWidth, contentsHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const me = this;
|
||||||
|
const {
|
||||||
|
props,
|
||||||
|
state,
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
// TODO: Abstract into separate package & use CSS Modules.
|
||||||
|
const spacerCss = { width: '.3em' };
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', flex: props.flex === true ? 1 : props.flex, display: props.flex ? 'flex' : 'block' }}>
|
||||||
|
<div className={props.toolbarClassName} style={{ alignItems: 'center', color: '#fff', display: 'flex', padding: '.3em', position: 'absolute', top: 0, left: 0, right: 0, background: '#333', flexDirection: 'row', zIndex: 2 }}>
|
||||||
|
<FontAwesomeIcon icon={faPlusCircle} size="2x" onClick={() => this.addElement()} />
|
||||||
|
<div style={spacerCss} />
|
||||||
|
<FontAwesomeIcon icon={faMinusCircle} size="2x" onClick={() => this.removeElement()} />
|
||||||
|
<div style={spacerCss} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Select style={{ flex: 1 }} isSearchable={false} options={state.fontOptions} onChange={(option) => this.setFont(option.fontName)} />
|
||||||
|
</div>
|
||||||
|
<div style={spacerCss} />
|
||||||
|
<div onClick={() => this.onSave()} style={{ alignItems: 'center', alignSelf: 'stretch', border: '1px solid #fff', borderRadius: '4px', color: '#fff', display: 'flex', padding: '0 0.6em' }}>
|
||||||
|
<span>Save</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref={me.contents} style={{ fontSize: '22px', overflow: 'hidden', transform: 'translateZ(0)', flex: 1 }}>
|
||||||
|
{state.elements}
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFont(fontName) {
|
||||||
|
this.setState({
|
||||||
|
fontName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,17 +1,34 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { validateFile } from '../../utils/file';
|
import { validateFile } from '../../utils/file';
|
||||||
|
import Creatify from '@components/Creatify';
|
||||||
import DropzonePreviewImage from '@components/DropzonePreviewImage';
|
import DropzonePreviewImage from '@components/DropzonePreviewImage';
|
||||||
import DropzoneDropItDisplay from '@components/DropzoneDropItDisplay';
|
import DropzoneDropItDisplay from '@components/DropzoneDropItDisplay';
|
||||||
import DropzoneInstructionsDisplay from '@components/DropzoneInstructionsDisplay';
|
import DropzoneInstructionsDisplay from '@components/DropzoneInstructionsDisplay';
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faEdit } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
class Dropzone extends React.Component {
|
class Dropzone extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
dragOver : false,
|
dragOver : false,
|
||||||
mouseOver : false,
|
mouseOver : false,
|
||||||
dimPreview : false,
|
dimPreview : false,
|
||||||
|
filePreview: null,
|
||||||
|
memeify : false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(props.file) {
|
||||||
|
// No side effects allowed with `getDerivedStateFromProps`, so
|
||||||
|
// we must use `componentDidUpdate` and `constructor` routines.
|
||||||
|
// Note: `FileReader` has an `onloadend` side-effect
|
||||||
|
this.updateFilePreview();
|
||||||
|
}
|
||||||
|
|
||||||
this.handleDrop = this.handleDrop.bind(this);
|
this.handleDrop = this.handleDrop.bind(this);
|
||||||
this.handleDragOver = this.handleDragOver.bind(this);
|
this.handleDragOver = this.handleDragOver.bind(this);
|
||||||
this.handleDragEnd = this.handleDragEnd.bind(this);
|
this.handleDragEnd = this.handleDragEnd.bind(this);
|
||||||
|
@ -23,6 +40,25 @@ class Dropzone extends React.Component {
|
||||||
this.handleFileInput = this.handleFileInput.bind(this);
|
this.handleFileInput = this.handleFileInput.bind(this);
|
||||||
this.chooseFile = this.chooseFile.bind(this);
|
this.chooseFile = this.chooseFile.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if(prevProps.file !== this.props.file) {
|
||||||
|
this.updateFilePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilePreview() {
|
||||||
|
if (this.props.file) {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.readAsDataURL(this.props.file);
|
||||||
|
fileReader.onloadend = () => {
|
||||||
|
this.setState({
|
||||||
|
filePreview: fileReader.result
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleDrop (event) {
|
handleDrop (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.setState({dragOver: false});
|
this.setState({dragOver: false});
|
||||||
|
@ -35,9 +71,11 @@ class Dropzone extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragOver (event) {
|
handleDragOver (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragEnd (event) {
|
handleDragEnd (event) {
|
||||||
var dt = event.dataTransfer;
|
var dt = event.dataTransfer;
|
||||||
if (dt.items) {
|
if (dt.items) {
|
||||||
|
@ -48,27 +86,34 @@ class Dropzone extends React.Component {
|
||||||
event.dataTransfer.clearData();
|
event.dataTransfer.clearData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragEnter () {
|
handleDragEnter () {
|
||||||
this.setState({dragOver: true, dimPreview: true});
|
this.setState({dragOver: true, dimPreview: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragLeave () {
|
handleDragLeave () {
|
||||||
this.setState({dragOver: false, dimPreview: false});
|
this.setState({dragOver: false, dimPreview: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseEnter () {
|
handleMouseEnter () {
|
||||||
this.setState({mouseOver: true, dimPreview: true});
|
this.setState({mouseOver: true, dimPreview: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseLeave () {
|
handleMouseLeave () {
|
||||||
this.setState({mouseOver: false, dimPreview: false});
|
this.setState({mouseOver: false, dimPreview: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick (event) {
|
handleClick (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.getElementById('file_input').click();
|
document.getElementById('file_input').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileInput (event) {
|
handleFileInput (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const fileList = event.target.files;
|
const fileList = event.target.files;
|
||||||
this.chooseFile(fileList[0]);
|
this.chooseFile(fileList[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
chooseFile (file) {
|
chooseFile (file) {
|
||||||
if (file) {
|
if (file) {
|
||||||
try {
|
try {
|
||||||
|
@ -80,15 +125,72 @@ class Dropzone extends React.Component {
|
||||||
this.props.selectFile(file);
|
this.props.selectFile(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectFileFromCanvas (canvas) {
|
||||||
|
const destinationFormat = 'image/jpeg';
|
||||||
|
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
const file = new File([blob], `memeify-${Math.random().toString(36).substring(7)}.jpg`, {
|
||||||
|
type: destinationFormat,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.selectFile(file);
|
||||||
|
|
||||||
|
// TODO: Add ability to reset.
|
||||||
|
this.setState({
|
||||||
|
memeify: false,
|
||||||
|
});
|
||||||
|
}, destinationFormat, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { dragOver, mouseOver, dimPreview } = this.state;
|
const { dragOver, mouseOver, dimPreview, filePreview, memeify } = this.state;
|
||||||
const { file, thumbnail, fileError, isUpdate, sourceUrl, fileExt } = this.props;
|
const { file, thumbnail, fileError, isUpdate, sourceUrl, fileExt } = this.props;
|
||||||
|
|
||||||
|
const hasContent = !!(file || isUpdate);
|
||||||
|
|
||||||
|
const dropZoneHooks = file ? {} : {
|
||||||
|
onDrop: this.handleDrop,
|
||||||
|
onDragOver: this.handleDragOver,
|
||||||
|
onDragEnd: this.handleDragEnd,
|
||||||
|
onDragEnter: this.handleDragEnter,
|
||||||
|
onDragLeave: this.handleDragLeave,
|
||||||
|
onMouseEnter: this.handleMouseEnter,
|
||||||
|
onMouseLeave: this.handleMouseLeave,
|
||||||
|
onClick: this.handleClick,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropZonePreviewProps = file ? {
|
||||||
|
dimPreview,
|
||||||
|
file,
|
||||||
|
thumbnail,
|
||||||
|
} : {
|
||||||
|
dimPreview: true,
|
||||||
|
isUpdate: true,
|
||||||
|
sourceUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const memeifyContent = memeify && file && filePreview ? (
|
||||||
|
<Creatify flex toolbarClassName={'dropzone-memeify-toolbar'} onSave={(canvas) => this.selectFileFromCanvas(canvas)}>
|
||||||
|
<div style={{ background: '#fff', flex: 1, pointerEvents: 'none' }}>
|
||||||
|
<img style={{ width: '100%' }} src={filePreview} />
|
||||||
|
</div>
|
||||||
|
</Creatify>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const dropZoneClassName = 'dropzone' + (dragOver ? ' dropzone--drag-over' : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{isUpdate && fileExt === 'mp4' ? (
|
{isUpdate && fileExt === 'mp4' ? (
|
||||||
<p>Video updates are currently disabled. This feature will be available soon. You can edit metadata.</p>
|
<p>Video updates are currently disabled. This feature will be available soon. You can edit metadata.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className='dropzone-wrapper'>
|
<div className={'dropzone-wrapper'}>
|
||||||
|
{ hasContent && !memeify && fileExt !== 'mp4' && (
|
||||||
|
<div className={'dropzone-memeify-button'} onClick={() => this.setState({ memeify: !memeify })}>
|
||||||
|
<FontAwesomeIcon icon={faEdit} /> Memeify
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<form>
|
<form>
|
||||||
<input
|
<input
|
||||||
className='input-file'
|
className='input-file'
|
||||||
|
@ -100,31 +202,10 @@ class Dropzone extends React.Component {
|
||||||
encType='multipart/form-data'
|
encType='multipart/form-data'
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<div
|
<div className={dropZoneClassName} {...dropZoneHooks}>
|
||||||
className={'dropzone' + (dragOver ? ' dropzone--drag-over' : '')}
|
{hasContent ? (
|
||||||
onDrop={this.handleDrop}
|
<div className={'dropzone-preview-wrapper' + (memeifyContent ? ' dropzone-preview-memeify' : '')}>
|
||||||
onDragOver={this.handleDragOver}
|
<DropzonePreviewImage {...dropZonePreviewProps} />
|
||||||
onDragEnd={this.handleDragEnd}
|
|
||||||
onDragEnter={this.handleDragEnter}
|
|
||||||
onDragLeave={this.handleDragLeave}
|
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
onClick={this.handleClick}>
|
|
||||||
{file || isUpdate ? (
|
|
||||||
<div className={'dropzone-preview-wrapper'}>
|
|
||||||
{file ? (
|
|
||||||
<DropzonePreviewImage
|
|
||||||
dimPreview={dimPreview}
|
|
||||||
file={file}
|
|
||||||
thumbnail={thumbnail}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DropzonePreviewImage
|
|
||||||
dimPreview
|
|
||||||
isUpdate
|
|
||||||
sourceUrl={sourceUrl}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={'dropzone-preview-overlay'}>
|
<div className={'dropzone-preview-overlay'}>
|
||||||
{ dragOver ? <DropzoneDropItDisplay /> : null }
|
{ dragOver ? <DropzoneDropItDisplay /> : null }
|
||||||
{ mouseOver ? (
|
{ mouseOver ? (
|
||||||
|
@ -133,15 +214,15 @@ class Dropzone extends React.Component {
|
||||||
message={fileExt === 'mp4' ? 'Drag & drop new thumbnail' : null}
|
message={fileExt === 'mp4' ? 'Drag & drop new thumbnail' : null}
|
||||||
/>
|
/>
|
||||||
) : null }
|
) : null }
|
||||||
|
{memeifyContent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
dragOver ? <DropzoneDropItDisplay /> : (
|
dragOver ? <DropzoneDropItDisplay /> : (
|
||||||
<DropzoneInstructionsDisplay
|
<DropzoneInstructionsDisplay fileError={fileError} />
|
||||||
fileError={fileError}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
{memeifyContent ? <div className={'dropzone-memeify-saveMessage'}>{`Don't forget to save before you publish.`}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -46,6 +46,9 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/lbryio/spee.ch#readme",
|
"homepage": "https://github.com/lbryio/spee.ch#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.8",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.5.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.3",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"bcrypt": "^2.0.1",
|
"bcrypt": "^2.0.1",
|
||||||
"body-parser": "^1.18.3",
|
"body-parser": "^1.18.3",
|
||||||
|
@ -67,12 +70,14 @@
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.4.2",
|
"react": "^16.4.2",
|
||||||
"react-feather": "^1.1.4",
|
|
||||||
"react-dom": "^16.6.1",
|
"react-dom": "^16.6.1",
|
||||||
|
"react-draggable": "^3.0.5",
|
||||||
|
"react-feather": "^1.1.4",
|
||||||
"react-ga": "^2.5.3",
|
"react-ga": "^2.5.3",
|
||||||
"react-helmet": "^5.2.0",
|
"react-helmet": "^5.2.0",
|
||||||
"react-redux": "^5.1.1",
|
"react-redux": "^5.1.1",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
|
"react-select": "^2.1.1",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
"redux-saga": "^0.16.2",
|
"redux-saga": "^0.16.2",
|
||||||
"sequelize": "^4.41.1",
|
"sequelize": "^4.41.1",
|
||||||
|
@ -94,9 +99,9 @@
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@babel/preset-stage-2": "^7.0.0",
|
"@babel/preset-stage-2": "^7.0.0",
|
||||||
"@babel/register": "^7.0.0",
|
"@babel/register": "^7.0.0",
|
||||||
|
"babel-eslint": "9.0.0-beta.3",
|
||||||
"babel-loader": "^7.1.2",
|
"babel-loader": "^7.1.2",
|
||||||
"babel-plugin-module-resolver": "^3.1.1",
|
"babel-plugin-module-resolver": "^3.1.1",
|
||||||
"babel-eslint": "9.0.0-beta.3",
|
|
||||||
"builder": "^4.0.0",
|
"builder": "^4.0.0",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chai-http": "^4.2.0",
|
"chai-http": "^4.2.0",
|
||||||
|
|
Loading…
Reference in a new issue