Doodle improvements 2 (#176)
* Fix some doodle bugs and added Background color functionality * added protections against accidental doodle erase, screen size changing * resolve react warning about 'selected' on <option>
This commit is contained in:
parent
a438c1dc83
commit
f92d0bbda4
5 changed files with 342 additions and 85 deletions
|
@ -8,7 +8,7 @@ const mapStateToProps = state => ({
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onOpenCanvas () {
|
onOpenCanvas () {
|
||||||
dispatch(openModal('DOODLE', {}));
|
dispatch(openModal('DOODLE', { noEsc: true }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { connect } from 'react-redux';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { doodleSet, uploadCompose } from '../../../actions/compose';
|
import { doodleSet, uploadCompose } from '../../../actions/compose';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import { debounce } from 'lodash';
|
import { debounce, mapValues } from 'lodash';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
// palette nicked from MyPaint, CC0
|
// palette nicked from MyPaint, CC0
|
||||||
const palette = [
|
const palette = [
|
||||||
|
@ -110,16 +111,40 @@ function dataURLtoFile(dataurl, filename) {
|
||||||
return new File([u8arr], filename, { type: mime });
|
return new File([u8arr], filename, { type: mime });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DOODLE_SIZES = {
|
||||||
|
normal: [500, 500, 'Square 500'],
|
||||||
|
tootbanner: [702, 330, 'Tootbanner'],
|
||||||
|
s640x480: [640, 480, '640×480 - 480p'],
|
||||||
|
s800x600: [800, 600, '800×600 - SVGA'],
|
||||||
|
s720x480: [720, 405, '720x405 - 16:9'],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
options: state.getIn(['compose', 'doodle']),
|
options: state.getIn(['compose', 'doodle']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
/** Set options in the redux store */
|
||||||
setOpt: (opts) => dispatch(doodleSet(opts)),
|
setOpt: (opts) => dispatch(doodleSet(opts)),
|
||||||
|
/** Submit doodle for upload */
|
||||||
submit: (file) => dispatch(uploadCompose([file])),
|
submit: (file) => dispatch(uploadCompose([file])),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doodling dialog with drawing canvas
|
||||||
|
*
|
||||||
|
* Keyboard shortcuts:
|
||||||
|
* - Delete: Clear screen, fill with background color
|
||||||
|
* - Backspace, Ctrl+Z: Undo one step
|
||||||
|
* - Ctrl held while drawing: Use background color
|
||||||
|
* - Shift held while clicking screen: Use fill tool
|
||||||
|
*
|
||||||
|
* Palette:
|
||||||
|
* - Left mouse button: pick foreground
|
||||||
|
* - Ctrl + left mouse button: pick background
|
||||||
|
* - Right mouse button: pick background
|
||||||
|
*/
|
||||||
@connect(mapStateToProps, mapDispatchToProps)
|
@connect(mapStateToProps, mapDispatchToProps)
|
||||||
export default class DoodleModal extends ImmutablePureComponent {
|
export default class DoodleModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -132,121 +157,145 @@ export default class DoodleModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
//region Option getters/setters
|
//region Option getters/setters
|
||||||
|
|
||||||
|
/** Foreground color */
|
||||||
get fg () {
|
get fg () {
|
||||||
return this.props.options.get('fg');
|
return this.props.options.get('fg');
|
||||||
}
|
}
|
||||||
|
|
||||||
set fg (value) {
|
set fg (value) {
|
||||||
this.props.setOpt({ fg: value });
|
this.props.setOpt({ fg: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Background color */
|
||||||
get bg () {
|
get bg () {
|
||||||
return this.props.options.get('bg');
|
return this.props.options.get('bg');
|
||||||
}
|
}
|
||||||
|
|
||||||
set bg (value) {
|
set bg (value) {
|
||||||
this.props.setOpt({ bg: value });
|
this.props.setOpt({ bg: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Swap Fg and Bg for drawing */
|
||||||
|
get swapped () {
|
||||||
|
return this.props.options.get('swapped');
|
||||||
|
}
|
||||||
|
set swapped (value) {
|
||||||
|
this.props.setOpt({ swapped: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mode - 'draw' or 'fill' */
|
||||||
get mode () {
|
get mode () {
|
||||||
return this.props.options.get('mode');
|
return this.props.options.get('mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
set mode (value) {
|
set mode (value) {
|
||||||
this.props.setOpt({ mode: value });
|
this.props.setOpt({ mode: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Base line weight */
|
||||||
get weight () {
|
get weight () {
|
||||||
return this.props.options.get('weight');
|
return this.props.options.get('weight');
|
||||||
}
|
}
|
||||||
|
|
||||||
set weight (value) {
|
set weight (value) {
|
||||||
this.props.setOpt({ weight: value });
|
this.props.setOpt({ weight: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Drawing opacity */
|
||||||
get opacity () {
|
get opacity () {
|
||||||
return this.props.options.get('opacity');
|
return this.props.options.get('opacity');
|
||||||
}
|
}
|
||||||
|
|
||||||
set opacity (value) {
|
set opacity (value) {
|
||||||
this.props.setOpt({ opacity: value });
|
this.props.setOpt({ opacity: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adaptive stroke - change width with speed */
|
||||||
get adaptiveStroke () {
|
get adaptiveStroke () {
|
||||||
return this.props.options.get('adaptiveStroke');
|
return this.props.options.get('adaptiveStroke');
|
||||||
}
|
}
|
||||||
|
|
||||||
set adaptiveStroke (value) {
|
set adaptiveStroke (value) {
|
||||||
this.props.setOpt({ adaptiveStroke: value });
|
this.props.setOpt({ adaptiveStroke: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Smoothing (for mouse drawing) */
|
||||||
get smoothing () {
|
get smoothing () {
|
||||||
return this.props.options.get('smoothing');
|
return this.props.options.get('smoothing');
|
||||||
}
|
}
|
||||||
|
|
||||||
set smoothing (value) {
|
set smoothing (value) {
|
||||||
this.props.setOpt({ smoothing: value });
|
this.props.setOpt({ smoothing: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
/** Size preset */
|
||||||
|
get size () {
|
||||||
handleKeyUp = (e) => {
|
return this.props.options.get('size');
|
||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
}
|
||||||
e.preventDefault();
|
set size (value) {
|
||||||
this.clearScreen();
|
this.props.setOpt({ size: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
|
//endregion
|
||||||
|
|
||||||
|
/** Key up handler */
|
||||||
|
handleKeyUp = (e) => {
|
||||||
|
if (e.target.nodeName === 'INPUT') return;
|
||||||
|
|
||||||
|
if (e.key === 'Delete') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handleClearBtn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Backspace' || (e.key === 'z' && (e.ctrlKey || e.metaKey))) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.undo();
|
this.undo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Control' || e.key === 'Meta') {
|
||||||
|
this.controlHeld = false;
|
||||||
|
this.swapped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Shift') {
|
||||||
|
this.shiftHeld = false;
|
||||||
|
this.mode = 'draw';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Key down handler */
|
||||||
|
handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'Control' || e.key === 'Meta') {
|
||||||
|
this.controlHeld = true;
|
||||||
|
this.swapped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Shift') {
|
||||||
|
this.shiftHeld = true;
|
||||||
|
this.mode = 'fill';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component installed in the DOM, do some initial set-up
|
||||||
|
*/
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
this.controlHeld = false;
|
||||||
|
this.shiftHeld = false;
|
||||||
|
this.swapped = false;
|
||||||
window.addEventListener('keyup', this.handleKeyUp, false);
|
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||||
|
window.addEventListener('keydown', this.handleKeyDown, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tear component down
|
||||||
|
*/
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
window.removeEventListener('keyup', this.handleKeyUp, false);
|
window.removeEventListener('keyup', this.handleKeyUp, false);
|
||||||
|
window.removeEventListener('keydown', this.handleKeyDown, false);
|
||||||
|
if (this.sketcher) this.sketcher.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearScreen = () => {
|
/**
|
||||||
this.sketcher.context.fillStyle = this.bg;
|
* Set reference to the canvas element.
|
||||||
this.sketcher.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
* This is called during component init
|
||||||
this.undos = [];
|
*
|
||||||
|
* @param elem - canvas element
|
||||||
this.doSaveUndo();
|
*/
|
||||||
};
|
|
||||||
|
|
||||||
handleDone = () => {
|
|
||||||
const dataUrl = this.sketcher.toImage();
|
|
||||||
const file = dataURLtoFile(dataUrl, 'doodle.png');
|
|
||||||
this.props.submit(file);
|
|
||||||
|
|
||||||
this.sketcher.destroy();
|
|
||||||
this.props.onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateSketcherSettings () {
|
|
||||||
if (!this.sketcher) return;
|
|
||||||
|
|
||||||
this.sketcher.color = this.fg;
|
|
||||||
this.sketcher.opacity = this.opacity;
|
|
||||||
this.sketcher.weight = this.weight;
|
|
||||||
this.sketcher.mode = this.mode;
|
|
||||||
this.sketcher.smoothing = this.smoothing;
|
|
||||||
this.sketcher.adaptiveStroke = this.adaptiveStroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
initSketcher (elem) {
|
|
||||||
this.sketcher = new Atrament(elem, 500, 500);
|
|
||||||
|
|
||||||
this.mode = 'draw'; // Reset mode - it's confusing if left at 'fill'
|
|
||||||
|
|
||||||
this.updateSketcherSettings();
|
|
||||||
this.clearScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanvasRef = (elem) => {
|
setCanvasRef = (elem) => {
|
||||||
this.canvas = elem;
|
this.canvas = elem;
|
||||||
if (elem) {
|
if (elem) {
|
||||||
|
@ -254,6 +303,7 @@ export default class DoodleModal extends ImmutablePureComponent {
|
||||||
this.saveUndo();
|
this.saveUndo();
|
||||||
this.sketcher._dirty = false;
|
this.sketcher._dirty = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
elem.addEventListener('click', () => {
|
elem.addEventListener('click', () => {
|
||||||
// sketcher bug - does not fire dirty on fill
|
// sketcher bug - does not fire dirty on fill
|
||||||
if (this.mode === 'fill') {
|
if (this.mode === 'fill') {
|
||||||
|
@ -261,58 +311,233 @@ export default class DoodleModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// prevent context menu
|
||||||
|
elem.addEventListener('contextmenu', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
elem.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.button === 2) {
|
||||||
|
this.swapped = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
elem.addEventListener('mouseup', (e) => {
|
||||||
|
if (e.button === 2) {
|
||||||
|
this.swapped = this.controlHeld;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.initSketcher(elem);
|
this.initSketcher(elem);
|
||||||
|
this.mode = 'draw'; // Reset mode - it's confusing if left at 'fill'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onPaletteClick = (e) => {
|
/**
|
||||||
this.fg = e.target.dataset.color;
|
* Set up the sketcher instance
|
||||||
e.target.blur();
|
*
|
||||||
|
* @param canvas - canvas element. Null if we're just resizing
|
||||||
|
*/
|
||||||
|
initSketcher (canvas = null) {
|
||||||
|
const sizepreset = DOODLE_SIZES[this.size];
|
||||||
|
|
||||||
|
if (this.sketcher) this.sketcher.destroy();
|
||||||
|
this.sketcher = new Atrament(canvas || this.canvas, sizepreset[0], sizepreset[1]);
|
||||||
|
|
||||||
|
if (canvas) {
|
||||||
|
this.ctx = this.sketcher.context;
|
||||||
|
this.updateSketcherSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Done button handler
|
||||||
|
*/
|
||||||
|
onDoneButton = () => {
|
||||||
|
const dataUrl = this.sketcher.toImage();
|
||||||
|
const file = dataURLtoFile(dataUrl, 'doodle.png');
|
||||||
|
this.props.submit(file);
|
||||||
|
this.props.onClose(); // close dialog
|
||||||
};
|
};
|
||||||
|
|
||||||
setModeDraw = (e) => {
|
/**
|
||||||
this.mode = 'draw';
|
* Cancel button handler
|
||||||
e.target.blur();
|
*/
|
||||||
|
onCancelButton = () => {
|
||||||
|
if (this.undos.length > 1 && !confirm('Discard doodle? All changes will be lost!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onClose(); // close dialog
|
||||||
};
|
};
|
||||||
|
|
||||||
setModeFill = (e) => {
|
/**
|
||||||
this.mode = 'fill';
|
* Update sketcher options based on state
|
||||||
e.target.blur();
|
*/
|
||||||
};
|
updateSketcherSettings () {
|
||||||
|
if (!this.sketcher) return;
|
||||||
tglSmooth = (e) => {
|
|
||||||
this.smoothing = !this.smoothing;
|
if (this.oldSize !== this.size) this.initSketcher();
|
||||||
e.target.blur();
|
|
||||||
};
|
this.sketcher.color = (this.swapped ? this.bg : this.fg);
|
||||||
|
this.sketcher.opacity = this.opacity;
|
||||||
tglAdaptive = (e) => {
|
this.sketcher.weight = this.weight;
|
||||||
this.adaptiveStroke = !this.adaptiveStroke;
|
this.sketcher.mode = this.mode;
|
||||||
e.target.blur();
|
this.sketcher.smoothing = this.smoothing;
|
||||||
};
|
this.sketcher.adaptiveStroke = this.adaptiveStroke;
|
||||||
|
|
||||||
setWeight = (e) => {
|
this.oldSize = this.size;
|
||||||
this.weight = +e.target.value || 1;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill screen with background color
|
||||||
|
*/
|
||||||
|
clearScreen = () => {
|
||||||
|
this.ctx.fillStyle = this.bg;
|
||||||
|
this.ctx.fillRect(-1, -1, this.canvas.width+2, this.canvas.height+2);
|
||||||
|
this.undos = [];
|
||||||
|
|
||||||
|
this.doSaveUndo();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo one step
|
||||||
|
*/
|
||||||
undo = () => {
|
undo = () => {
|
||||||
if (this.undos.length > 1) {
|
if (this.undos.length > 1) {
|
||||||
this.undos.pop();
|
this.undos.pop();
|
||||||
const buf = this.undos.pop();
|
const buf = this.undos.pop();
|
||||||
|
|
||||||
this.sketcher.clear();
|
this.sketcher.clear();
|
||||||
this.sketcher.context.putImageData(buf, 0, 0);
|
this.ctx.putImageData(buf, 0, 0);
|
||||||
this.doSaveUndo();
|
this.doSaveUndo();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save canvas content into the undo buffer immediately
|
||||||
|
*/
|
||||||
doSaveUndo = () => {
|
doSaveUndo = () => {
|
||||||
this.undos.push(this.sketcher.context.getImageData(0, 0, this.canvas.width, this.canvas.height));
|
this.undos.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on each canvas change.
|
||||||
|
* Saves canvas content to the undo buffer after some period of inactivity.
|
||||||
|
*/
|
||||||
saveUndo = debounce(() => {
|
saveUndo = debounce(() => {
|
||||||
this.doSaveUndo();
|
this.doSaveUndo();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Palette left click.
|
||||||
|
* Selects Fg color (or Bg, if Control/Meta is held)
|
||||||
|
*
|
||||||
|
* @param e - event
|
||||||
|
*/
|
||||||
|
onPaletteClick = (e) => {
|
||||||
|
const c = e.target.dataset.color;
|
||||||
|
|
||||||
|
if (this.controlHeld) {
|
||||||
|
this.bg = c;
|
||||||
|
} else {
|
||||||
|
this.fg = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.target.blur();
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Palette right click.
|
||||||
|
* Selects Bg color
|
||||||
|
*
|
||||||
|
* @param e - event
|
||||||
|
*/
|
||||||
|
onPaletteRClick = (e) => {
|
||||||
|
this.bg = e.target.dataset.color;
|
||||||
|
e.target.blur();
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click on the Draw mode button
|
||||||
|
*
|
||||||
|
* @param e - event
|
||||||
|
*/
|
||||||
|
setModeDraw = (e) => {
|
||||||
|
this.mode = 'draw';
|
||||||
|
e.target.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click on the Fill mode button
|
||||||
|
*
|
||||||
|
* @param e - event
|
||||||
|
*/
|
||||||
|
setModeFill = (e) => {
|
||||||
|
this.mode = 'fill';
|
||||||
|
e.target.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click on Smooth checkbox
|
||||||
|
*
|
||||||
|
* @param e - event
|
||||||
|
*/
|
||||||
|
tglSmooth = (e) => {
|
||||||
|
this.smoothing = !this.smoothing;
|
||||||
|
e.target.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click on Adaptive checkbox
|
||||||
|
*
|
||||||
|
* @param e - event
|
||||||
|
*/
|
||||||
|
tglAdaptive = (e) => {
|
||||||
|
this.adaptiveStroke = !this.adaptiveStroke;
|
||||||
|
e.target.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle change of the Weight input field
|
||||||
|
*
|
||||||
|
* @param e - event
|
||||||
|
*/
|
||||||
|
setWeight = (e) => {
|
||||||
|
this.weight = +e.target.value || 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set size - clalback from the select box
|
||||||
|
*
|
||||||
|
* @param e - event
|
||||||
|
*/
|
||||||
|
changeSize = (e) => {
|
||||||
|
let newSize = e.target.value;
|
||||||
|
if (newSize === this.oldSize) return;
|
||||||
|
|
||||||
|
if (this.undos.length > 1 && !confirm('Change size? This will erase your drawing!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.size = newSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClearBtn = () => {
|
||||||
|
if (this.undos.length > 1 && !confirm('Clear screen? This will erase your drawing!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearScreen();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the component
|
||||||
|
*/
|
||||||
render () {
|
render () {
|
||||||
this.updateSketcherSettings();
|
this.updateSketcherSettings();
|
||||||
|
|
||||||
|
@ -323,7 +548,10 @@ export default class DoodleModal extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='doodle-modal__action-bar'>
|
<div className='doodle-modal__action-bar'>
|
||||||
<Button text='Done' onClick={this.handleDone} />
|
<div className='doodle-toolbar'>
|
||||||
|
<Button text='Done' onClick={this.onDoneButton} />
|
||||||
|
<Button text='Cancel' onClick={this.onCancelButton} />
|
||||||
|
</div>
|
||||||
<div className='filler' />
|
<div className='filler' />
|
||||||
<div className='doodle-toolbar with-inputs'>
|
<div className='doodle-toolbar with-inputs'>
|
||||||
<div>
|
<div>
|
||||||
|
@ -344,12 +572,19 @@ export default class DoodleModal extends ImmutablePureComponent {
|
||||||
<input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} />
|
<input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<select aria-label='Canvas size' onInput={this.changeSize} defaultValue={this.size}>
|
||||||
|
{ Object.values(mapValues(DOODLE_SIZES, (val, k) =>
|
||||||
|
<option key={k} value={k}>{val[2]}</option>
|
||||||
|
)) }
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='doodle-toolbar'>
|
<div className='doodle-toolbar'>
|
||||||
<IconButton icon='pencil' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
|
<IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
|
||||||
<IconButton icon='bath' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
|
<IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
|
||||||
<IconButton icon='undo' label='Undo' onClick={this.undo} size={18} inverted />
|
<IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted />
|
||||||
<IconButton icon='trash' label='Clear' onClick={this.clearScreen} size={18} inverted />
|
<IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted />
|
||||||
</div>
|
</div>
|
||||||
<div className='doodle-palette'>
|
<div className='doodle-palette'>
|
||||||
{
|
{
|
||||||
|
@ -360,9 +595,13 @@ export default class DoodleModal extends ImmutablePureComponent {
|
||||||
key={i}
|
key={i}
|
||||||
style={{ backgroundColor: c[0] }}
|
style={{ backgroundColor: c[0] }}
|
||||||
onClick={this.onPaletteClick}
|
onClick={this.onPaletteClick}
|
||||||
|
onContextMenu={this.onPaletteRClick}
|
||||||
data-color={c[0]}
|
data-color={c[0]}
|
||||||
title={c[1]}
|
title={c[1]}
|
||||||
className={this.fg === c[0] ? 'selected' : ''}
|
className={classNames({
|
||||||
|
'foreground': this.fg === c[0],
|
||||||
|
'background': this.bg === c[0],
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class ModalRoot extends React.PureComponent {
|
||||||
|
|
||||||
handleKeyUp = (e) => {
|
handleKeyUp = (e) => {
|
||||||
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
|
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
|
||||||
&& !!this.props.type) {
|
&& !!this.props.type && !this.props.props.noEsc) {
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,9 @@ const initialState = ImmutableMap({
|
||||||
doodle: ImmutableMap({
|
doodle: ImmutableMap({
|
||||||
fg: 'rgb( 0, 0, 0)',
|
fg: 'rgb( 0, 0, 0)',
|
||||||
bg: 'rgb(255, 255, 255)',
|
bg: 'rgb(255, 255, 255)',
|
||||||
|
swapped: false,
|
||||||
mode: 'draw',
|
mode: 'draw',
|
||||||
|
size: 'normal',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
adaptiveStroke: true,
|
adaptiveStroke: true,
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
|
$doodleBg: #d9e1e8;
|
||||||
.doodle-modal {
|
.doodle-modal {
|
||||||
@extend .boost-modal;
|
@extend .boost-modal;
|
||||||
width: unset;
|
width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doodle-modal__container {
|
.doodle-modal__container {
|
||||||
|
background: $doodleBg;
|
||||||
|
text-align: center;
|
||||||
line-height: 0; // remove weird gap under canvas
|
line-height: 0; // remove weird gap under canvas
|
||||||
canvas {
|
canvas {
|
||||||
border: 5px solid #d9e1e8;
|
border: 5px solid $doodleBg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,9 +18,13 @@
|
||||||
|
|
||||||
.filler {
|
.filler {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doodle-toolbar {
|
.doodle-toolbar {
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -60,10 +67,19 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: inset 0 0 1px rgba(white, .5);
|
box-shadow: inset 0 0 1px rgba(white, .5);
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
|
|
||||||
&.selected {
|
|
||||||
outline-offset:-1px;
|
outline-offset:-1px;
|
||||||
outline: 1px dotted white;
|
|
||||||
|
&.foreground {
|
||||||
|
outline: 1px dashed white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.background {
|
||||||
|
outline: 1px dashed red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.foreground.background {
|
||||||
|
outline: 1px dashed red;
|
||||||
|
border-color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue