As this is what upstream does. See also https://github.com/tc39/proposal-decorators/issues/69
		
			
				
	
	
		
			614 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			614 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import React from 'react';
 | ||
| import PropTypes from 'prop-types';
 | ||
| import Button from 'flavours/glitch/components/button';
 | ||
| import ImmutablePureComponent from 'react-immutable-pure-component';
 | ||
| import Atrament from 'atrament'; // the doodling library
 | ||
| import { connect } from 'react-redux';
 | ||
| import ImmutablePropTypes from 'react-immutable-proptypes';
 | ||
| import { doodleSet, uploadCompose } from 'flavours/glitch/actions/compose';
 | ||
| import IconButton from 'flavours/glitch/components/icon_button';
 | ||
| import { debounce, mapValues } from 'lodash';
 | ||
| import classNames from 'classnames';
 | ||
| 
 | ||
| // palette nicked from MyPaint, CC0
 | ||
| const palette = [
 | ||
|   ['rgb(  0,    0,    0)', 'Black'],
 | ||
|   ['rgb( 38,   38,   38)', 'Gray 15'],
 | ||
|   ['rgb( 77,   77,   77)', 'Grey 30'],
 | ||
|   ['rgb(128,  128,  128)', 'Grey 50'],
 | ||
|   ['rgb(171,  171,  171)', 'Grey 67'],
 | ||
|   ['rgb(217,  217,  217)', 'Grey 85'],
 | ||
|   ['rgb(255,  255,  255)', 'White'],
 | ||
|   ['rgb(128,    0,    0)', 'Maroon'],
 | ||
|   ['rgb(209,    0,    0)', 'English-red'],
 | ||
|   ['rgb(255,   54,   34)', 'Tomato'],
 | ||
|   ['rgb(252,   60,    3)', 'Orange-red'],
 | ||
|   ['rgb(255,  140,  105)', 'Salmon'],
 | ||
|   ['rgb(252,  232,   32)', 'Cadium-yellow'],
 | ||
|   ['rgb(243,  253,   37)', 'Lemon yellow'],
 | ||
|   ['rgb(121,    5,   35)', 'Dark crimson'],
 | ||
|   ['rgb(169,   32,   62)', 'Deep carmine'],
 | ||
|   ['rgb(255,  140,    0)', 'Orange'],
 | ||
|   ['rgb(255,  168,   18)', 'Dark tangerine'],
 | ||
|   ['rgb(217,  144,   88)', 'Persian orange'],
 | ||
|   ['rgb(194,  178,  128)', 'Sand'],
 | ||
|   ['rgb(255,  229,  180)', 'Peach'],
 | ||
|   ['rgb(100,   54,   46)', 'Bole'],
 | ||
|   ['rgb(108,   41,   52)', 'Dark cordovan'],
 | ||
|   ['rgb(163,   65,   44)', 'Chestnut'],
 | ||
|   ['rgb(228,  136,  100)', 'Dark salmon'],
 | ||
|   ['rgb(255,  195,  143)', 'Apricot'],
 | ||
|   ['rgb(255,  219,  188)', 'Unbleached silk'],
 | ||
|   ['rgb(242,  227,  198)', 'Straw'],
 | ||
|   ['rgb( 53,   19,   13)', 'Bistre'],
 | ||
|   ['rgb( 84,   42,   14)', 'Dark chocolate'],
 | ||
|   ['rgb(102,   51,   43)', 'Burnt sienna'],
 | ||
|   ['rgb(184,   66,    0)', 'Sienna'],
 | ||
|   ['rgb(216,  153,   12)', 'Yellow ochre'],
 | ||
|   ['rgb(210,  180,  140)', 'Tan'],
 | ||
|   ['rgb(232,  204,  144)', 'Dark wheat'],
 | ||
|   ['rgb(  0,   49,   83)', 'Prussian blue'],
 | ||
|   ['rgb( 48,   69,  119)', 'Dark grey blue'],
 | ||
|   ['rgb(  0,   71,  171)', 'Cobalt blue'],
 | ||
|   ['rgb( 31,  117,  254)', 'Blue'],
 | ||
|   ['rgb(120,  180,  255)', 'Bright french blue'],
 | ||
|   ['rgb(171,  200,  255)', 'Bright steel blue'],
 | ||
|   ['rgb(208,  231,  255)', 'Ice blue'],
 | ||
|   ['rgb( 30,   51,   58)', 'Medium jungle green'],
 | ||
|   ['rgb( 47,   79,   79)', 'Dark slate grey'],
 | ||
|   ['rgb( 74,  104,   93)', 'Dark grullo green'],
 | ||
|   ['rgb(  0,  128,  128)', 'Teal'],
 | ||
|   ['rgb( 67,  170,  176)', 'Turquoise'],
 | ||
|   ['rgb(109,  174,  199)', 'Cerulean frost'],
 | ||
|   ['rgb(173,  217,  186)', 'Tiffany green'],
 | ||
|   ['rgb( 22,   34,   29)', 'Gray-asparagus'],
 | ||
|   ['rgb( 36,   48,   45)', 'Medium dark teal'],
 | ||
|   ['rgb( 74,  104,   93)', 'Xanadu'],
 | ||
|   ['rgb(119,  198,  121)', 'Mint'],
 | ||
|   ['rgb(175,  205,  182)', 'Timberwolf'],
 | ||
|   ['rgb(185,  245,  246)', 'Celeste'],
 | ||
|   ['rgb(193,  255,  234)', 'Aquamarine'],
 | ||
|   ['rgb( 29,   52,   35)', 'Cal Poly Pomona'],
 | ||
|   ['rgb(  1,   68,   33)', 'Forest green'],
 | ||
|   ['rgb( 42,  128,    0)', 'Napier green'],
 | ||
|   ['rgb(128,  128,    0)', 'Olive'],
 | ||
|   ['rgb( 65,  156,  105)', 'Sea green'],
 | ||
|   ['rgb(189,  246,   29)', 'Green-yellow'],
 | ||
|   ['rgb(231,  244,  134)', 'Bright chartreuse'],
 | ||
|   ['rgb(138,   23,  137)', 'Purple'],
 | ||
|   ['rgb( 78,   39,  138)', 'Violet'],
 | ||
|   ['rgb(193,   75,  110)', 'Dark thulian pink'],
 | ||
|   ['rgb(222,   49,   99)', 'Cerise'],
 | ||
|   ['rgb(255,   20,  147)', 'Deep pink'],
 | ||
|   ['rgb(255,  102,  204)', 'Rose pink'],
 | ||
|   ['rgb(255,  203,  219)', 'Pink'],
 | ||
|   ['rgb(255,  255,  255)', 'White'],
 | ||
|   ['rgb(229,   17,    1)', 'RGB Red'],
 | ||
|   ['rgb(  0,  255,    0)', 'RGB Green'],
 | ||
|   ['rgb(  0,    0,  255)', 'RGB Blue'],
 | ||
|   ['rgb(  0,  255,  255)', 'CMYK Cyan'],
 | ||
|   ['rgb(255,    0,  255)', 'CMYK Magenta'],
 | ||
|   ['rgb(255,  255,    0)', 'CMYK Yellow'],
 | ||
| ];
 | ||
| 
 | ||
| // re-arrange to the right order for display
 | ||
| let palReordered = [];
 | ||
| for (let row = 0; row < 7; row++) {
 | ||
|   for (let col = 0; col < 11; col++) {
 | ||
|     palReordered.push(palette[col * 7 + row]);
 | ||
|   }
 | ||
|   palReordered.push(null); // null indicates a <br />
 | ||
| }
 | ||
| 
 | ||
| // Utility for converting base64 image to binary for upload
 | ||
| // https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f
 | ||
| function dataURLtoFile(dataurl, filename) {
 | ||
|   let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
 | ||
|     bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
 | ||
|   while(n--){
 | ||
|     u8arr[n] = bstr.charCodeAt(n);
 | ||
|   }
 | ||
|   return new File([u8arr], filename, { type: mime });
 | ||
| }
 | ||
| /** Doodle canvas size options */
 | ||
| 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 => ({
 | ||
|   options: state.getIn(['compose', 'doodle']),
 | ||
| });
 | ||
| 
 | ||
| const mapDispatchToProps = dispatch => ({
 | ||
|   /** Set options in the redux store */
 | ||
|   setOpt: (opts) => dispatch(doodleSet(opts)),
 | ||
|   /** Submit doodle for upload */
 | ||
|   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
 | ||
|  */
 | ||
| export default @connect(mapStateToProps, mapDispatchToProps)
 | ||
| class DoodleModal extends ImmutablePureComponent {
 | ||
| 
 | ||
|   static propTypes = {
 | ||
|     options: ImmutablePropTypes.map,
 | ||
|     onClose: PropTypes.func.isRequired,
 | ||
|     setOpt: PropTypes.func.isRequired,
 | ||
|     submit: PropTypes.func.isRequired,
 | ||
|   };
 | ||
| 
 | ||
|   //region Option getters/setters
 | ||
| 
 | ||
|   /** Foreground color */
 | ||
|   get fg () {
 | ||
|     return this.props.options.get('fg');
 | ||
|   }
 | ||
|   set fg (value) {
 | ||
|     this.props.setOpt({ fg: value });
 | ||
|   }
 | ||
| 
 | ||
|   /** Background color */
 | ||
|   get bg () {
 | ||
|     return this.props.options.get('bg');
 | ||
|   }
 | ||
|   set 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 () {
 | ||
|     return this.props.options.get('mode');
 | ||
|   }
 | ||
|   set mode (value) {
 | ||
|     this.props.setOpt({ mode: value });
 | ||
|   }
 | ||
| 
 | ||
|   /** Base line weight */
 | ||
|   get weight () {
 | ||
|     return this.props.options.get('weight');
 | ||
|   }
 | ||
|   set weight (value) {
 | ||
|     this.props.setOpt({ weight: value });
 | ||
|   }
 | ||
| 
 | ||
|   /** Drawing opacity */
 | ||
|   get opacity () {
 | ||
|     return this.props.options.get('opacity');
 | ||
|   }
 | ||
|   set opacity (value) {
 | ||
|     this.props.setOpt({ opacity: value });
 | ||
|   }
 | ||
| 
 | ||
|   /** Adaptive stroke - change width with speed */
 | ||
|   get adaptiveStroke () {
 | ||
|     return this.props.options.get('adaptiveStroke');
 | ||
|   }
 | ||
|   set adaptiveStroke (value) {
 | ||
|     this.props.setOpt({ adaptiveStroke: value });
 | ||
|   }
 | ||
| 
 | ||
|   /** Smoothing (for mouse drawing) */
 | ||
|   get smoothing () {
 | ||
|     return this.props.options.get('smoothing');
 | ||
|   }
 | ||
|   set smoothing (value) {
 | ||
|     this.props.setOpt({ smoothing: value });
 | ||
|   }
 | ||
| 
 | ||
|   /** Size preset */
 | ||
|   get size () {
 | ||
|     return this.props.options.get('size');
 | ||
|   }
 | ||
|   set size (value) {
 | ||
|     this.props.setOpt({ size: value });
 | ||
|   }
 | ||
| 
 | ||
|   //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();
 | ||
|       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 () {
 | ||
|     this.controlHeld = false;
 | ||
|     this.shiftHeld = false;
 | ||
|     this.swapped = false;
 | ||
|     window.addEventListener('keyup', this.handleKeyUp, false);
 | ||
|     window.addEventListener('keydown', this.handleKeyDown, false);
 | ||
|   };
 | ||
| 
 | ||
|   /**
 | ||
|    * Tear component down
 | ||
|    */
 | ||
|   componentWillUnmount () {
 | ||
|     window.removeEventListener('keyup', this.handleKeyUp, false);
 | ||
|     window.removeEventListener('keydown', this.handleKeyDown, false);
 | ||
|     if (this.sketcher) this.sketcher.destroy();
 | ||
|   }
 | ||
| 
 | ||
|   /**
 | ||
|    * Set reference to the canvas element.
 | ||
|    * This is called during component init
 | ||
|    *
 | ||
|    * @param elem - canvas element
 | ||
|    */
 | ||
|   setCanvasRef = (elem) => {
 | ||
|     this.canvas = elem;
 | ||
|     if (elem) {
 | ||
|       elem.addEventListener('dirty', () => {
 | ||
|         this.saveUndo();
 | ||
|         this.sketcher._dirty = false;
 | ||
|       });
 | ||
| 
 | ||
|       elem.addEventListener('click', () => {
 | ||
|         // sketcher bug - does not fire dirty on fill
 | ||
|         if (this.mode === 'fill') {
 | ||
|           this.saveUndo();
 | ||
|         }
 | ||
|       });
 | ||
| 
 | ||
|       // 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.mode = 'draw'; // Reset mode - it's confusing if left at 'fill'
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   /**
 | ||
|    * Set up the sketcher instance
 | ||
|    *
 | ||
|    * @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
 | ||
|   };
 | ||
| 
 | ||
|   /**
 | ||
|    * Cancel button handler
 | ||
|    */
 | ||
|   onCancelButton = () => {
 | ||
|     if (this.undos.length > 1 && !confirm('Discard doodle? All changes will be lost!')) {
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     this.props.onClose(); // close dialog
 | ||
|   };
 | ||
| 
 | ||
|   /**
 | ||
|    * Update sketcher options based on state
 | ||
|    */
 | ||
|   updateSketcherSettings () {
 | ||
|     if (!this.sketcher) return;
 | ||
| 
 | ||
|     if (this.oldSize !== this.size) this.initSketcher();
 | ||
| 
 | ||
|     this.sketcher.color = (this.swapped ? this.bg : 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;
 | ||
| 
 | ||
|     this.oldSize = this.size;
 | ||
|   }
 | ||
| 
 | ||
|   /**
 | ||
|    * 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 = () => {
 | ||
|     if (this.undos.length > 1) {
 | ||
|       this.undos.pop();
 | ||
|       const buf = this.undos.pop();
 | ||
| 
 | ||
|       this.sketcher.clear();
 | ||
|       this.ctx.putImageData(buf, 0, 0);
 | ||
|       this.doSaveUndo();
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   /**
 | ||
|    * Save canvas content into the undo buffer immediately
 | ||
|    */
 | ||
|   doSaveUndo = () => {
 | ||
|     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(() => {
 | ||
|     this.doSaveUndo();
 | ||
|   }, 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 canvas size? This will erase your current drawing!')) {
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     this.size = newSize;
 | ||
|   };
 | ||
| 
 | ||
|   handleClearBtn = () => {
 | ||
|     if (this.undos.length > 1 && !confirm('Clear canvas? This will erase your current drawing!')) {
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     this.clearScreen();
 | ||
|   };
 | ||
| 
 | ||
|   /**
 | ||
|    * Render the component
 | ||
|    */
 | ||
|   render () {
 | ||
|     this.updateSketcherSettings();
 | ||
| 
 | ||
|     return (
 | ||
|       <div className='modal-root__modal doodle-modal'>
 | ||
|         <div className='doodle-modal__container'>
 | ||
|           <canvas ref={this.setCanvasRef} />
 | ||
|         </div>
 | ||
| 
 | ||
|         <div className='doodle-modal__action-bar'>
 | ||
|           <div className='doodle-toolbar'>
 | ||
|             <Button text='Done' onClick={this.onDoneButton} />
 | ||
|             <Button text='Cancel' onClick={this.onCancelButton} />
 | ||
|           </div>
 | ||
|           <div className='filler' />
 | ||
|           <div className='doodle-toolbar with-inputs'>
 | ||
|             <div>
 | ||
|               <label htmlFor='dd_smoothing'>Smoothing</label>
 | ||
|               <span className='val'>
 | ||
|                 <input type='checkbox' id='dd_smoothing' onChange={this.tglSmooth} checked={this.smoothing} />
 | ||
|               </span>
 | ||
|             </div>
 | ||
|             <div>
 | ||
|               <label htmlFor='dd_adaptive'>Adaptive</label>
 | ||
|               <span className='val'>
 | ||
|                 <input type='checkbox' id='dd_adaptive' onChange={this.tglAdaptive} checked={this.adaptiveStroke} />
 | ||
|               </span>
 | ||
|             </div>
 | ||
|             <div>
 | ||
|               <label htmlFor='dd_weight'>Weight</label>
 | ||
|               <span className='val'>
 | ||
|                 <input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} />
 | ||
|               </span>
 | ||
|             </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 className='doodle-toolbar'>
 | ||
|             <IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
 | ||
|             <IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
 | ||
|             <IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted />
 | ||
|             <IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted />
 | ||
|           </div>
 | ||
|           <div className='doodle-palette'>
 | ||
|             {
 | ||
|               palReordered.map((c, i) =>
 | ||
|                 c === null ?
 | ||
|                   <br key={i} /> :
 | ||
|                   <button
 | ||
|                     key={i}
 | ||
|                     style={{ backgroundColor: c[0] }}
 | ||
|                     onClick={this.onPaletteClick}
 | ||
|                     onContextMenu={this.onPaletteRClick}
 | ||
|                     data-color={c[0]}
 | ||
|                     title={c[1]}
 | ||
|                     className={classNames({
 | ||
|                       'foreground': this.fg === c[0],
 | ||
|                       'background': this.bg === c[0],
 | ||
|                     })}
 | ||
|                   />
 | ||
|               )
 | ||
|             }
 | ||
|           </div>
 | ||
|         </div>
 | ||
|       </div>
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
| }
 |