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 => ({ | ||||
|   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 { doodleSet, uploadCompose } from '../../../actions/compose'; | ||||
| import IconButton from '../../../components/icon_button'; | ||||
| import { debounce } from 'lodash'; | ||||
| import { debounce, mapValues } from 'lodash'; | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
| // palette nicked from MyPaint, CC0
 | ||||
| const palette = [ | ||||
|  | @ -110,16 +111,40 @@ function dataURLtoFile(dataurl, filename) { | |||
|   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 => ({ | ||||
|   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 | ||||
|  */ | ||||
| @connect(mapStateToProps, mapDispatchToProps) | ||||
| export default class DoodleModal extends ImmutablePureComponent { | ||||
| 
 | ||||
|  | @ -132,121 +157,145 @@ export default class DoodleModal extends ImmutablePureComponent { | |||
| 
 | ||||
|   //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 }); | ||||
|   } | ||||
| 
 | ||||
|   //endregion
 | ||||
| 
 | ||||
|   handleKeyUp = (e) => { | ||||
|     if (e.key === 'Delete' || e.key === 'Backspace') { | ||||
|       e.preventDefault(); | ||||
|       this.clearScreen(); | ||||
|   /** Size preset */ | ||||
|   get size () { | ||||
|     return this.props.options.get('size'); | ||||
|   } | ||||
|   set size (value) { | ||||
|     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(); | ||||
|       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(); | ||||
|   } | ||||
| 
 | ||||
|   clearScreen = () => { | ||||
|     this.sketcher.context.fillStyle = this.bg; | ||||
|     this.sketcher.context.fillRect(0, 0, this.canvas.width, this.canvas.height); | ||||
|     this.undos = []; | ||||
| 
 | ||||
|     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(); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Set reference to the canvas element. | ||||
|    * This is called during component init | ||||
|    * | ||||
|    * @param elem - canvas element | ||||
|    */ | ||||
|   setCanvasRef = (elem) => { | ||||
|     this.canvas = elem; | ||||
|     if (elem) { | ||||
|  | @ -254,6 +303,7 @@ export default class DoodleModal extends ImmutablePureComponent { | |||
|         this.saveUndo(); | ||||
|         this.sketcher._dirty = false; | ||||
|       }); | ||||
| 
 | ||||
|       elem.addEventListener('click', () => { | ||||
|         // sketcher bug - does not fire dirty on 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.mode = 'draw'; // Reset mode - it's confusing if left at 'fill'
 | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   onPaletteClick = (e) => { | ||||
|     this.fg = e.target.dataset.color; | ||||
|     e.target.blur(); | ||||
|   /** | ||||
|    * 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
 | ||||
|   }; | ||||
| 
 | ||||
|   setModeDraw = (e) => { | ||||
|     this.mode = 'draw'; | ||||
|     e.target.blur(); | ||||
|   /** | ||||
|    * Cancel button handler | ||||
|    */ | ||||
|   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'; | ||||
|     e.target.blur(); | ||||
|   }; | ||||
| 
 | ||||
|   tglSmooth = (e) => { | ||||
|     this.smoothing = !this.smoothing; | ||||
|     e.target.blur(); | ||||
|   }; | ||||
| 
 | ||||
|   tglAdaptive = (e) => { | ||||
|     this.adaptiveStroke = !this.adaptiveStroke; | ||||
|     e.target.blur(); | ||||
|   }; | ||||
| 
 | ||||
|   setWeight = (e) => { | ||||
|     this.weight = +e.target.value || 1; | ||||
|   /** | ||||
|    * 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.sketcher.context.putImageData(buf, 0, 0); | ||||
|       this.ctx.putImageData(buf, 0, 0); | ||||
|       this.doSaveUndo(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Save canvas content into the undo buffer immediately | ||||
|    */ | ||||
|   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(() => { | ||||
|     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 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 () { | ||||
|     this.updateSketcherSettings(); | ||||
| 
 | ||||
|  | @ -323,7 +548,10 @@ export default class DoodleModal extends ImmutablePureComponent { | |||
|         </div> | ||||
| 
 | ||||
|         <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='doodle-toolbar with-inputs'> | ||||
|             <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} /> | ||||
|               </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' 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='undo' label='Undo' onClick={this.undo} size={18} inverted /> | ||||
|             <IconButton icon='trash' label='Clear' onClick={this.clearScreen} size={18} inverted /> | ||||
|             <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'> | ||||
|             { | ||||
|  | @ -360,9 +595,13 @@ export default class DoodleModal extends ImmutablePureComponent { | |||
|                     key={i} | ||||
|                     style={{ backgroundColor: c[0] }} | ||||
|                     onClick={this.onPaletteClick} | ||||
|                     onContextMenu={this.onPaletteRClick} | ||||
|                     data-color={c[0]} | ||||
|                     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) => { | ||||
|     if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) | ||||
|          && !!this.props.type) { | ||||
|          && !!this.props.type && !this.props.props.noEsc) { | ||||
|       this.props.onClose(); | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -65,7 +65,9 @@ const initialState = ImmutableMap({ | |||
|   doodle: ImmutableMap({ | ||||
|     fg: 'rgb(  0,    0,    0)', | ||||
|     bg: 'rgb(255,  255,  255)', | ||||
|     swapped: false, | ||||
|     mode: 'draw', | ||||
|     size: 'normal', | ||||
|     weight: 2, | ||||
|     opacity: 1, | ||||
|     adaptiveStroke: true, | ||||
|  |  | |||
|  | @ -1,12 +1,15 @@ | |||
| $doodleBg: #d9e1e8; | ||||
| .doodle-modal { | ||||
|   @extend .boost-modal; | ||||
|   width: unset; | ||||
| } | ||||
| 
 | ||||
| .doodle-modal__container { | ||||
|   background: $doodleBg; | ||||
|   text-align: center; | ||||
|   line-height: 0; // remove weird gap under canvas | ||||
|   canvas { | ||||
|     border: 5px solid #d9e1e8; | ||||
|     border: 5px solid $doodleBg; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -15,9 +18,13 @@ | |||
| 
 | ||||
|   .filler { | ||||
|     flex-grow: 1; | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|   } | ||||
| 
 | ||||
|   .doodle-toolbar { | ||||
|     line-height: 1; | ||||
| 
 | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     flex-grow: 0; | ||||
|  | @ -60,10 +67,19 @@ | |||
|       cursor: pointer; | ||||
|       box-shadow: inset 0 0 1px rgba(white, .5); | ||||
|       border: 1px solid black; | ||||
| 
 | ||||
|       &.selected { | ||||
|       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