├── favicon.ico ├── icon-16.png ├── brush ├── 25.png ├── 50.png ├── 75.png ├── b0.png ├── b1.png ├── b2.png ├── b3.png ├── b4.png ├── b5.png ├── chalk1.png ├── chalk2.png ├── chalk3.png ├── circle.png ├── pixel.png ├── acrylic1.png ├── acrylic2.png ├── acrylic3.png ├── acrylic4.png ├── acrylic5.png ├── charcoal1.png └── charcoal2.png ├── icon-128.png ├── screenshot.png ├── background.js ├── .gitmodules ├── manifest.json ├── README.md ├── icons ├── redo.svg ├── undo.svg ├── rubber.svg ├── pencil.svg ├── zoom-out.svg ├── picker.svg ├── line.svg └── zoom-in.svg ├── LICENSE.md ├── description.txt ├── handlers.js ├── keybindings.js ├── icon.svg ├── view.js ├── modal.js ├── ui.css ├── index.css ├── tool-options.js ├── filesaver └── FileSaver.js ├── index.html └── index.js /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/favicon.ico -------------------------------------------------------------------------------- /icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/icon-16.png -------------------------------------------------------------------------------- /brush/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/25.png -------------------------------------------------------------------------------- /brush/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/50.png -------------------------------------------------------------------------------- /brush/75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/75.png -------------------------------------------------------------------------------- /brush/b0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/b0.png -------------------------------------------------------------------------------- /brush/b1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/b1.png -------------------------------------------------------------------------------- /brush/b2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/b2.png -------------------------------------------------------------------------------- /brush/b3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/b3.png -------------------------------------------------------------------------------- /brush/b4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/b4.png -------------------------------------------------------------------------------- /brush/b5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/b5.png -------------------------------------------------------------------------------- /icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/icon-128.png -------------------------------------------------------------------------------- /brush/chalk1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/chalk1.png -------------------------------------------------------------------------------- /brush/chalk2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/chalk2.png -------------------------------------------------------------------------------- /brush/chalk3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/chalk3.png -------------------------------------------------------------------------------- /brush/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/circle.png -------------------------------------------------------------------------------- /brush/pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/pixel.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/screenshot.png -------------------------------------------------------------------------------- /brush/acrylic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/acrylic1.png -------------------------------------------------------------------------------- /brush/acrylic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/acrylic2.png -------------------------------------------------------------------------------- /brush/acrylic3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/acrylic3.png -------------------------------------------------------------------------------- /brush/acrylic4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/acrylic4.png -------------------------------------------------------------------------------- /brush/acrylic5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/acrylic5.png -------------------------------------------------------------------------------- /brush/charcoal1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/charcoal1.png -------------------------------------------------------------------------------- /brush/charcoal2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliverBalfour/SimplePaint/HEAD/brush/charcoal2.png -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | 2 | chrome.app.runtime.onLaunched.addListener(function () { 3 | chrome.app.window.create('index.html', { 4 | bounds: { 5 | width: window.screen.availWidth, 6 | height: window.screen.availHeight 7 | } 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tinycolor"] 2 | path = tinycolor 3 | url = https://github.com/bgrins/TinyColor 4 | [submodule "colourpicker"] 5 | path = colourpicker 6 | url = https://github.com/Tobsta/ColourPicker 7 | [submodule "croquis"] 8 | path = croquis 9 | url = https://github.com/disjukr/croquis.js 10 | [submodule "mousetrap"] 11 | path = mousetrap 12 | url = https://github.com/ccampbell/mousetrap 13 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Simple Paint", 3 | "description": "Simple yet powerful drawing tool and image editor for ChromeOS with no ads and stylus support.", 4 | "version": "1.0.2", 5 | "manifest_version": 2, 6 | "app": { 7 | "background": { 8 | "scripts": ["background.js"] 9 | } 10 | }, 11 | "icons": { 12 | "16": "icon-16.png", 13 | "128": "icon-128.png" 14 | }, 15 | "permissions": [ 16 | "app.window.fullscreen" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Paint 2 | 3 | Available at: https://oliverbalfour.github.io/SimplePaint/ 4 | 5 | ![Screenshot of the image editor](./screenshot.png) 6 | 7 | Basic HTML5 canvas based image editor built for the web and for ChromeOS with a neat colour picker, stylus/drawing tablet support, Photoshop-like brushes, and all the usual stuff you'd expect in an image editor. 8 | 9 | It is currently beta; it's usable and should work flawlessly in any modern browser — but it's not finished, and it's not necessarily bug-free either. 10 | -------------------------------------------------------------------------------- /icons/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2018 Oliver Balfour 3 | 4 | This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. 5 | 6 | Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 7 | 8 | 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 9 | 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 10 | 3. This notice may not be removed or altered from any source distribution. 11 | -------------------------------------------------------------------------------- /description.txt: -------------------------------------------------------------------------------- 1 | 2 | This is a simple and powerful ad-free image editor and drawing tool for ChromeOS, with an emphasis on stylus/tablet functionality. It is currently in beta, so it isn't a fully fledged image editor yet, but it is a functional drawing application at the moment. 3 | 4 | You can try it out in your browser (Chrome or Firefox are supported) at: https://tobsta.github.io/ImageEditor/ and view the source code here: https://github.com/Tobsta/ImageEditor 5 | 6 | Features: 7 | • 100% Free and Open Source (so no ads or tracking scripts) 8 | • Pressure sensitive stylus/drawing tablet support 9 | • 23 highly configurable brushes, and you can make your own! 10 | • Import and export images in a variety of formats 11 | • Fully fledged layer implementation 12 | • Distraction-free mode (just press F11 & Tab) 13 | • Intuitive HSV colour picker 14 | • Extremely accurate line stabilisation 15 | • Zoom in/out, undo/redo etc. 16 | 17 | And features on the roadmap include: 18 | • Colour filters (recolour, greyscale, Gaussian blur, edge detection, etc.) 19 | • Scaling/rotating/flipping the image 20 | • A text tool, with customisable fonts, colours, sizes, and emphasis 21 | • Editing multiple images at once 22 | • Automatically saving images 23 | 24 | Feel free to request features through feedback here on the Chrome Web Store or by opening a ticket over at the GitHub repository here: https://github.com/Tobsta/ImageEditor/issues 25 | -------------------------------------------------------------------------------- /handlers.js: -------------------------------------------------------------------------------- 1 | 2 | { 3 | const click = (className, handler) => 4 | Array.from(document.querySelectorAll('.js-click-' + className)) 5 | .forEach(el => el.addEventListener('pointerdown', handler)); 6 | 7 | click('todo', () => {modal.confirm('TODO','','alert')}); 8 | 9 | click('newImage', newImage); 10 | click('openImage', openImage); 11 | click('openImageAsLayer', openImageAsLayer); 12 | click('exportImageAsLast', e => { 13 | exportImageAs(e.target, 'last'); 14 | }); 15 | click('exportImageAsPNG', e => { 16 | exportImageAs(e.target, 'png'); 17 | }); 18 | click('exportImageAsJPG', e => { 19 | exportImageAs(e.target, 'jpg'); 20 | }); 21 | click('undo', croquis.undo); 22 | click('redo', croquis.redo); 23 | click('toggleFullscreen', toggleFullscreen); 24 | click('toggleMenu', toggleMenu); 25 | click('toggleToolBar', () => toggleView('toolBar')); 26 | click('toggleToolOptions', () => toggleView('toolOptions')); 27 | click('toggleStatusBar', () => toggleView('statusBar')); 28 | click('toggleLayerThumbnails', toggleLayerThumbnails); 29 | click('zoomIn', zoomIn); 30 | click('zoomOut', zoomOut); 31 | click('resetZoom', resetZoom); 32 | click('centerImage', centerImage); 33 | click('resizeCanvas', resizeCanvas); 34 | click('addLayer', addLayer); 35 | click('removeActiveLayer', removeActiveLayer); 36 | click('clearLayer', clearLayer); 37 | click('fillLayer', fillLayer); 38 | click('modalAbout', () => modal.open('modal-about')); 39 | click('changeTool', e => changeTool(e.target)); 40 | click('uploadBrush', uploadBrush); 41 | click('modalClose', e => { 42 | modal.close(e.target.parentElement.className.split(' ')[1]); 43 | }); 44 | click('modalImageDone', modal.imageDone); 45 | click('modalPromptDone', modal.promptDone); 46 | click('modalConfirmDone', e => { 47 | modal.confirmDone(e.target.getAttribute('data-value') === 'true'); 48 | }); 49 | click('optionsDone', modal.optionsDone); 50 | document.querySelector('.canvases').addEventListener('wheel', e => { 51 | if (e.ctrlKey || e.metaKey) { 52 | if (e.deltaY < 0) zoomIn(); 53 | else zoomOut(); 54 | } 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /keybindings.js: -------------------------------------------------------------------------------- 1 | 2 | // Keyboard shortcuts 3 | 4 | // New 5 | Mousetrap.bind('mod+n', newImage); 6 | 7 | // Import 8 | Mousetrap.bind('mod+o', e => { 9 | e.preventDefault(); 10 | openImage(); 11 | }); 12 | Mousetrap.bind('mod+alt+o', e => { 13 | e.preventDefault(); 14 | openImageAsLayer(); 15 | }); 16 | 17 | // New layer 18 | Mousetrap.bind('mod+alt+n', e => { 19 | e.preventDefault(); 20 | addLayer(); 21 | }); 22 | 23 | // Export 24 | Mousetrap.bind('mod+s', e => { 25 | e.preventDefault(); 26 | croquis.createFlattenThumbnail().toBlob(blob => { 27 | saveAs(blob, 'image.' + (lastImageExport === 'image/png' ? 'png' : 'jpg')); 28 | }); 29 | }); 30 | 31 | // Undo/Redo 32 | Mousetrap.bind(['mod+y', 'mod+shift+z'], croquis.redo); 33 | Mousetrap.bind('mod+z', croquis.undo); 34 | 35 | // Tools 36 | Mousetrap.bind(['p', 'n'], () => changeTool(document.querySelector('img[data-tool="pen"]'))); 37 | Mousetrap.bind('l', () => changeTool(document.querySelector('img[data-tool="line"]'))); 38 | Mousetrap.bind('e', () => changeTool(document.querySelector('img[data-tool="eraser"]'))); 39 | Mousetrap.bind(['o', 'c'], () => changeTool(document.querySelector('img[data-tool="picker"]'))); 40 | 41 | // Distraction free mode 42 | Mousetrap.bind('tab', e => { 43 | e.preventDefault(); 44 | toggleView('toolBar'); 45 | toggleView('toolOptions'); 46 | }); 47 | 48 | // Toggle menu 49 | Mousetrap.bind('mod+m', e => { 50 | e.preventDefault(); 51 | toggleMenu(); 52 | }); 53 | Mousetrap.bind('alt', e => { 54 | e.preventDefault(); 55 | if (!view.menu.open) { 56 | toggleMenu(); 57 | view.menu.altDown = true; 58 | } 59 | }, 'keydown'); 60 | Mousetrap.bind('alt', e => { 61 | e.preventDefault(); 62 | if (view.menu.open && view.menu.altDown) { 63 | toggleMenu(); 64 | view.menu.altDown = false; 65 | } 66 | }, 'keyup'); 67 | 68 | // Fullscreen 69 | Mousetrap.bind('f11', toggleFullscreen); 70 | 71 | // Zoom in/out, reset zoom 72 | Mousetrap.bind('mod+0', resetZoom); 73 | Mousetrap.bind('mod+9', e => { 74 | e.preventDefault(); 75 | centerImage(); 76 | }); 77 | Mousetrap.bind(['mod+=', 'mod+shift+='], e => { 78 | e.preventDefault(); 79 | zoomIn(); 80 | }); 81 | // Mousetrap isn't working with the Ctrl+Minus shortcut, so I have to manually implement it... 82 | document.addEventListener('keydown', e => { 83 | if ((e.which === 173 || e.which === 189) && (e.ctrlKey || e.metaKey)) { 84 | e.preventDefault(); 85 | zoomOut(); 86 | } 87 | }) 88 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 38 | 41 | 42 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 60 | 66 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /icons/rubber.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 41 | 44 | 45 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 69 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /view.js: -------------------------------------------------------------------------------- 1 | 2 | // View menu 3 | 4 | document.fullscreenEnabled = document.fullscreenEnabled || document.mozFullScreenEnabled || document.documentElement.webkitRequestFullScreen; 5 | 6 | function enterFullscreen (element) { 7 | if (element.requestFullscreen) { 8 | element.requestFullscreen(); 9 | } else if (element.mozRequestFullScreen) { 10 | element.mozRequestFullScreen(); 11 | } else if (element.webkitRequestFullScreen) { 12 | element.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); 13 | } 14 | } 15 | function exitFullscreen () { 16 | if (document.exitFullscreen) { 17 | document.exitFullscreen(); 18 | } else if (document.webkitExitFullscreen) { 19 | document.webkitExitFullscreen(); 20 | } else if (document.mozCancelFullScreen) { 21 | document.mozCancelFullScreen(); 22 | } else if (document.msExitFullscreen) { 23 | document.msExitFullscreen(); 24 | } 25 | } 26 | 27 | function toggleFullscreen () { 28 | if ( 29 | !(window.fullScreen || (window.innerWidth == screen.width && window.innerHeight == screen.height)) 30 | && document.fullscreenEnabled 31 | ) 32 | enterFullscreen(document.documentElement); 33 | else 34 | exitFullscreen(); 35 | } 36 | 37 | // section must be the name of a child of the `view` object (statusBar etc.) 38 | function toggleView (section) { 39 | if (view[section].open) { 40 | document.documentElement.style.setProperty(view[section].prop, '0px'); 41 | document.querySelector(view[section].className).classList.add('hidden'); 42 | } else { 43 | document.documentElement.style.setProperty(view[section].prop, view[section].size); 44 | document.querySelector(view[section].className).classList.remove('hidden'); 45 | } 46 | 47 | view[section].open = !view[section].open; 48 | updateCanvasContainerSize(); 49 | } 50 | 51 | function toggleMenu () { 52 | if (view.menu.open && !view.menu.altDown && !view.menu.promptAgain) { 53 | toggleView('menu'); 54 | view.menu.promptAgain = false; 55 | } else if (view.menu.open && !view.menu.altDown) { 56 | modal.confirm( 57 | 'Are you sure you want to close the menu?', 58 | 'You can re-open the menu by holding Alt/Option or pressing Ctrl+M', 59 | 'confirm' 60 | ) 61 | .then(() => { 62 | toggleView('menu'); 63 | view.menu.promptAgain = false; 64 | }) 65 | .catch(() => {}); 66 | } else { 67 | toggleView('menu'); 68 | } 69 | } 70 | 71 | let layerThumbInterval; 72 | if (view.layerThumbs) 73 | layerThumbInterval = setInterval(updateLayerThumbnails, 5 * 1000); 74 | 75 | function toggleLayerThumbnails () { 76 | if (view.layerThumbs) { 77 | clearInterval(layerThumbInterval); 78 | } else { 79 | layerThumbInterval = setInterval(updateLayerThumbnails, 5 * 1000); 80 | } 81 | 82 | view.layerThumbs = !view.layerThumbs; 83 | updateLayers(); 84 | } 85 | 86 | // Other stuff 87 | 88 | // Mouse position indicators 89 | document.addEventListener('mousemove', (e) => { 90 | mouse.x = e.clientX; 91 | mouse.y = e.clientY; 92 | getRelativePosition(); 93 | 94 | let colour = 'darkred', 95 | xel = document.querySelector('.js-x-coord'), 96 | yel = document.querySelector('.js-y-coord'); 97 | 98 | xel.innerText = Math.round(mouse.rx); 99 | yel.innerText = Math.round(mouse.ry); 100 | 101 | if (mouse.rx < 0 || mouse.rx > croquis.getCanvasWidth()) 102 | xel.style.background = colour; 103 | else 104 | xel.style.background = ''; 105 | 106 | if (mouse.ry < 0 || mouse.ry > croquis.getCanvasHeight()) 107 | yel.style.background = colour; 108 | else 109 | yel.style.background = ''; 110 | }); 111 | -------------------------------------------------------------------------------- /icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 43 | 48 | 53 | 58 | 63 | 66 | 67 | 69 | 71 | 72 | 74 | image/svg+xml 75 | 77 | 78 | 79 | 80 | 81 | 85 | 91 | 96 | 102 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /icons/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /icons/picker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 43 | 48 | 53 | 58 | 63 | 66 | 67 | 69 | 71 | 72 | 74 | image/svg+xml 75 | 77 | 78 | 79 | 80 | 81 | 85 | 91 | 97 | 103 | 109 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /icons/line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 43 | 48 | 53 | 58 | 63 | 66 | 67 | 69 | 71 | 72 | 74 | image/svg+xml 75 | 77 | 78 | 79 | 80 | 81 | 85 | 89 | 95 | 96 | 101 | 107 | 113 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /modal.js: -------------------------------------------------------------------------------- 1 | 2 | const modal = { 3 | promise: { resolve: null, reject: null }, 4 | data: null, 5 | 6 | open: function (className) { 7 | document.querySelector('.modals').classList.remove('hidden'); 8 | document.querySelector('.' + className).classList.remove('hidden'); 9 | document.querySelector('.' + className).focus(); 10 | }, 11 | close: function (className) { 12 | document.querySelector('.modals').classList.add('hidden'); 13 | // className might also be a close button 200 | 201 | 208 | 215 | 221 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | // Create croquis instance 3 | const croquis = new Croquis(); 4 | 5 | // Initialise brush 6 | const brush = new Croquis.Brush(); 7 | brush.setSize(20); 8 | brush.setColor('#000'); 9 | brush.setSpacing(0.02); 10 | brush.setFlow(0.04) 11 | croquis.setTool(brush); 12 | croquis.setToolStabilizeLevel(10); 13 | croquis.setToolStabilizeWeight(0.5); 14 | 15 | const croquisElement = croquis.getDOMElement(); 16 | const canvasContainer = document.querySelector('.canvas-container'); 17 | canvasContainer.appendChild(croquisElement); 18 | 19 | // Initialise croquis 20 | croquis.lockHistory(); 21 | let canvasSize = canvasContainer.getBoundingClientRect(); 22 | let canvasContainerSize = document.querySelector('.canvases').getBoundingClientRect(); 23 | croquis.setCanvasSize(canvasSize.width / 2, canvasSize.height / 2); 24 | canvasContainer.parentElement.scrollTop = canvasSize.height / 4 + 1; 25 | canvasContainer.parentElement.scrollLeft = canvasSize.width / 4 + 1; 26 | croquis.addLayer(); 27 | croquis.fillLayer('#fff'); 28 | croquis.addLayer(); 29 | croquis.selectLayer(1); 30 | croquis.setUndoLimit(1000); 31 | croquis.unlockHistory(); 32 | 33 | function initialiseCanvasNames () { 34 | let layers = croquis.getLayers(); 35 | layers[0].setAttribute('data-name', 'Background'); 36 | layers[1].setAttribute('data-name', 'Canvas'); 37 | } 38 | initialiseCanvasNames(); 39 | 40 | const mouse = { 41 | x: 0, y: 0, 42 | rx: 0, ry: 0 43 | } 44 | let tool = 'pen'; 45 | 46 | const rootStyle = getComputedStyle(document.documentElement); 47 | const view = { 48 | menu: { 49 | open: true, 50 | promptAgain: true, // has the user been prompted about wanting to close the menu? 51 | altDown: false, // is the user pressing alt to hold the menu open? 52 | prop: '--menu-height', 53 | size: rootStyle.getPropertyValue('--menu-height'), 54 | className: '.menu' 55 | }, 56 | statusBar: { 57 | open: true, 58 | prop: '--footer-height', 59 | size: rootStyle.getPropertyValue('--footer-height'), 60 | className: '.footer' 61 | }, 62 | toolBar: { 63 | open: true, 64 | prop: '--toolbar-width', 65 | size: rootStyle.getPropertyValue('--toolbar-width'), 66 | className: '.toolbar' 67 | }, 68 | toolOptions: { 69 | open: true, 70 | prop: '--options-width', 71 | size: rootStyle.getPropertyValue('--options-width'), 72 | className: '.tool-options' 73 | }, 74 | layerThumbs: true, 75 | zoom: 1 76 | } 77 | 78 | // pointer-events property support 79 | const pointerEvents = document.documentElement.style.pointerEvents !== undefined; 80 | 81 | let circleBrushes = Array.from(document.getElementsByClassName('circle-brush')), 82 | brushImages = Array.from(document.getElementsByClassName('brush-image')), 83 | currentBrush = circleBrushes[0]; 84 | 85 | // Register mouse events 86 | 87 | function canvasPointerDown (e) { 88 | mouse.x = e.clientX; 89 | mouse.y = e.clientY; 90 | setPointerEvent(e); 91 | getRelativePosition(); 92 | if (pointerEvents) 93 | canvasContainer.style.setProperty('cursor', 'none'); 94 | if (tool === 'picker') 95 | picker.setColour(tinycolor(croquis.eyeDrop(mouse.rx, mouse.ry))); 96 | if (tool === 'eraser' || (tool === 'pen' && e.pointerType === 'pen' && e.button == 5)) 97 | croquis.setPaintingKnockout(true); 98 | else 99 | croquis.setPaintingKnockout(false); 100 | if (tool !== 'picker') 101 | croquis.down(mouse.rx, mouse.ry, e.pointerType === 'pen' ? e.pressure : 1); 102 | if (tool !== 'line') 103 | document.addEventListener('pointermove', canvasPointerMove); 104 | document.addEventListener('pointerup', canvasPointerUp); 105 | } 106 | function canvasPointerMove (e) { 107 | setPointerEvent(e); 108 | mouse.x = e.clientX; 109 | mouse.y = e.clientY; 110 | getRelativePosition(); 111 | // only pick 1/10 of the time to reduce lag 112 | if (tool === 'picker' && Math.random() < 0.1) 113 | picker.setColour(tinycolor(croquis.eyeDrop(mouse.rx, mouse.ry))); 114 | else if (tool !== 'picker') 115 | croquis.move(mouse.rx, mouse.ry, e.pointerType === 'pen' ? e.pressure : 1); 116 | } 117 | function canvasPointerUp (e) { 118 | setPointerEvent(e); 119 | mouse.x = e.clientX; 120 | mouse.y = e.clientY; 121 | getRelativePosition(); 122 | if (pointerEvents) 123 | canvasContainer.style.setProperty('cursor', 'crosshair'); 124 | if (tool === 'picker') 125 | picker.setColour(tinycolor(croquis.eyeDrop(mouse.rx, mouse.ry))); 126 | else 127 | croquis.up(mouse.rx, mouse.ry, e.pointerType === 'pen' ? e.pressure : 1); 128 | if (e.pointerType === 'pen' && e.button == 5) 129 | setTimeout(function() {croquis.setPaintingKnockout(selectEraserCheckbox.checked)}, 30);//timeout should be longer than 20 (knockoutTickInterval in Croquis) 130 | document.removeEventListener('pointermove', canvasPointerMove); 131 | document.removeEventListener('pointerup', canvasPointerUp); 132 | } 133 | croquisElement.addEventListener('pointerdown', canvasPointerDown); 134 | 135 | function clearLayer () { 136 | croquis.clearLayer(); 137 | } 138 | function fillLayer () { 139 | var rgb = tinycolor(brush.getColor()).toRgb(); 140 | croquis.fillLayer(tinycolor({ 141 | r: rgb.r, 142 | g: rgb.g, 143 | b: rgb.b, 144 | a: croquis.getPaintingOpacity() 145 | }).toRgbString()); 146 | } 147 | 148 | //brush pointer 149 | var brushPointerContainer = document.createElement('div'); 150 | brushPointerContainer.className = 'brush-pointer'; 151 | 152 | if (pointerEvents) { 153 | croquisElement.addEventListener('pointerover', function () { 154 | croquisElement.addEventListener('pointermove', croquisPointerMove); 155 | document.body.appendChild(brushPointerContainer); 156 | }); 157 | croquisElement.addEventListener('pointerout', function () { 158 | croquisElement.removeEventListener('pointermove', croquisPointerMove); 159 | brushPointerContainer.parentElement.removeChild(brushPointerContainer); 160 | }); 161 | } 162 | 163 | function croquisPointerMove(e) { 164 | mouse.x = e.clientX; 165 | mouse.y = e.clientY; 166 | if (pointerEvents) { 167 | var x = mouse.x + window.pageXOffset; 168 | var y = mouse.y + window.pageYOffset; 169 | brushPointerContainer.style.setProperty('left', x + 'px'); 170 | brushPointerContainer.style.setProperty('top', y + 'px'); 171 | } 172 | } 173 | 174 | // Tools 175 | 176 | function changeTool (el) { 177 | tool = el.getAttribute('data-tool'); 178 | for (let i = 0; i < el.parentElement.children.length; i++) { 179 | el.parentElement.children[i].classList.remove('active'); 180 | } 181 | el.classList.add('active'); 182 | } 183 | 184 | // Utility functions 185 | 186 | function setPointerEvent(e) { 187 | if (e.pointerType !== 'pen' && Croquis.Tablet.pen() && Croquis.Tablet.pen().pointerType) {//it says it's not a pen but it might be a wacom pen 188 | e.pointerType = 'pen'; 189 | e.pressure = Croquis.Tablet.pressure(); 190 | if (Croquis.Tablet.isEraser()) { 191 | Object.defineProperties(e, { 192 | 'button': { value: 5 }, 193 | 'buttons': { value: 32 } 194 | }); 195 | } 196 | } 197 | } 198 | 199 | function updatePointer() { 200 | if (pointerEvents) { 201 | var image = currentBrush; 202 | var threshold; 203 | if (circleBrushes.indexOf(currentBrush) !== -1) { 204 | image = null; 205 | threshold = 0xff; 206 | } 207 | else { 208 | threshold = 0x30; 209 | } 210 | var brushPointer = Croquis.createBrushPointer( 211 | image, brush.getSize() * view.zoom, brush.getAngle(), threshold, true); 212 | brushPointer.style.setProperty('margin-left', 213 | '-' + (brushPointer.width * 0.5) + 'px'); 214 | brushPointer.style.setProperty('margin-top', 215 | '-' + (brushPointer.height * 0.5) + 'px'); 216 | brushPointerContainer.innerHTML = ''; 217 | brushPointerContainer.appendChild(brushPointer); 218 | } 219 | } 220 | updatePointer(); 221 | 222 | function getRelativePosition () { 223 | const rect = canvasContainer.querySelector('canvas').getBoundingClientRect(); 224 | mouse.rx = (mouse.x - rect.left) * (1 / view.zoom); 225 | mouse.ry = (mouse.y - rect.top) * (1 / view.zoom); 226 | return { x: mouse.rx, y: mouse.ry }; 227 | } 228 | 229 | // New image 230 | 231 | function newImage () { 232 | modal.confirm('Are you sure you want to make a new image?', 'This will overwrite your existing image.', 'confirm') 233 | .then(() => { 234 | modal.options('Canvas properties', [ 235 | { 236 | name: 'width', 237 | text: 'Canvas width (in pixels)', 238 | value: 1366, 239 | type: 'number' 240 | }, { 241 | name: 'height', 242 | text: 'Canvas height (in pixels)', 243 | value: 768, 244 | type: 'number' 245 | }, { 246 | name: 'colour', 247 | text: 'Background fill colour', 248 | value: 'white', 249 | type: 'text' 250 | } 251 | ]) 252 | .then(data => { 253 | croquis.setCanvasSize(data.width, data.height); 254 | updateCanvasContainerSize(); 255 | croquis.lockHistory(); 256 | let layers = croquis.getLayers(); 257 | for (let i = 0; i < layers.length - 1; i++) { 258 | croquis.removeLayer(0); 259 | } 260 | croquis.addLayer(); 261 | croquis.fillLayer('#fff'); 262 | croquis.addLayer(); 263 | croquis.selectLayer(1); 264 | croquis.unlockHistory(); 265 | initialiseCanvasNames(); 266 | updateLayers(); 267 | }) 268 | .catch(err); 269 | }) 270 | .catch(() => {}); 271 | } 272 | 273 | // Upload image 274 | 275 | function openImage () { 276 | modal.image( 277 | 'Please upload an image', 278 | 'The image will be blitted 1-to-1 to the canvas. (TODO: The canvas is not resized, this will be fixed later.)' 279 | ) 280 | .then((data) => { 281 | let img = document.createElement('img'); 282 | img.src = data; 283 | img.onload = () => { 284 | let canvas = croquis.getLayers()[croquis.getCurrentLayerIndex()].children[0], 285 | ctx = canvas.getContext('2d'); 286 | ctx.drawImage(img, 0, 0); 287 | } 288 | }) 289 | .catch(err); 290 | } 291 | function openImageAsLayer () { 292 | modal.image( 293 | 'Please upload an image to use as a layer', 294 | 'The image will be blitted 1-to-1 to the canvas. (TODO: The image is not resized, this will be fixed later.)' 295 | ) 296 | .then((data) => { 297 | let img = document.createElement('img'); 298 | img.src = data; 299 | croquis.addLayer(croquis.getLayerCount()).setAttribute('data-name', 'Pasted image'); 300 | croquis.selectLayer(croquis.getLayerCount() - 1); 301 | 302 | img.onload = () => { 303 | let canvas = croquis.getLayers()[croquis.getLayerCount() - 1].children[0], 304 | ctx = canvas.getContext('2d'); 305 | ctx.drawImage(img, 0, 0); 306 | updateLayers(); 307 | } 308 | }) 309 | .catch(err); 310 | } 311 | 312 | // Layers 313 | 314 | function getLayerThumbnail (i) { 315 | let w = croquis.getCanvasWidth(), 316 | h = croquis.getCanvasHeight(), 317 | size = 32, 318 | wm = w > h ? size : w / h * size, 319 | hm = h > w ? size : h / w * size; 320 | 321 | return croquis.createLayerThumbnail(i, wm, hm); 322 | } 323 | 324 | function updateLayers () { 325 | let shelf = document.querySelector('.layers-shelf'), 326 | num = croquis.getLayerCount(), 327 | layers = croquis.getLayers(); 328 | 329 | shelf.innerHTML = ''; 330 | 331 | for (let i = 0; i < num; i++) { 332 | let el = document.createElement('div'); 333 | el.classList.add('layer'); 334 | el.addEventListener('pointerdown', layerPointerDown); 335 | 336 | el.innerHTML = (view.layerThumbs ? "" : '') 337 | + '' + layers[i].getAttribute('data-name') + '' 338 | + "
×
"; 339 | 340 | shelf.appendChild(el); 341 | } 342 | 343 | Array.from(shelf.children)[croquis.getCurrentLayerIndex()].classList.add('active'); 344 | 345 | Array.from(document.querySelectorAll('.js-click-removeLayer')).forEach(el => { 346 | el.addEventListener('pointerdown', e => removeLayer(e.target)); 347 | }); 348 | } 349 | updateLayers(); 350 | 351 | function updateLayerThumbnails () { 352 | Array.from(document.querySelectorAll('.layer img')).forEach((img, i) => { 353 | img.src = getLayerThumbnail(i).toDataURL('image/png'); 354 | }); 355 | } 356 | 357 | function layerPointerDown (e) { 358 | // If no children, it must be the child or and not the parent layer (which we want) 359 | let layer = e.target; 360 | if (!e.target.children.length) 361 | layer = e.target.parentElement; 362 | 363 | let layers = Array.from(layer.parentElement.children); 364 | croquis.selectLayer(layers.indexOf(layer)); 365 | layers.forEach(l => { 366 | l.classList.remove('active'); 367 | }); 368 | layer.classList.add('active'); 369 | } 370 | 371 | function addLayer () { 372 | modal.prompt('Please enter a name for the new layer', '', 'text') 373 | .then((name) => { 374 | croquis.addLayer(croquis.getLayerCount()).setAttribute('data-name', name); 375 | croquis.selectLayer(croquis.getLayerCount() - 1); 376 | setZoom(view.zoom); 377 | updateLayers(); 378 | }) 379 | .catch(err); 380 | } 381 | 382 | function removeLayer (el) { 383 | croquis.removeLayer(Array.from(el.parentElement.parentElement.children).indexOf(el.parentElement)); 384 | updateLayers(); 385 | } 386 | 387 | function removeActiveLayer () { 388 | croquis.removeLayer(croquis.getCurrentLayerIndex()); 389 | updateLayers(); 390 | } 391 | 392 | // Export 393 | 394 | let lastImageExport = 'image/png'; 395 | function exportImageAs (el, ext) { 396 | const c = croquis.createFlattenThumbnail(); 397 | let type; 398 | switch (ext) { 399 | case 'png': 400 | type = 'image/png'; break; 401 | case 'jpg': 402 | case 'jpeg': 403 | type = 'image/jpeg'; break; 404 | case 'last': 405 | default: 406 | type = lastImageExport; 407 | } 408 | lastImageExport = type; 409 | 410 | el.parentElement.setAttribute('href', c.toDataURL(type)); 411 | el.parentElement.setAttribute( 412 | 'download', 413 | 'image.' + ( 414 | ext !== 'last' ? ext 415 | : (lastImageExport === 'image/png' ? 'png' : 'jpg') 416 | ) 417 | ); 418 | } 419 | 420 | // Resize 421 | 422 | function resizeCanvas () { 423 | modal.prompt('Width', '', 'number') 424 | .then((width) => { 425 | modal.prompt('Height', '', 'number') 426 | .then((height) => { 427 | croquis.setCanvasSize(width, height); 428 | zoomIn(); 429 | zoomOut(); 430 | updateCanvasContainerSize(); 431 | updateLayers(); 432 | }) 433 | .catch(err); 434 | }) 435 | .catch(err); 436 | } 437 | 438 | function err (e) { 439 | modal.confirm('Error', e, 'alert'); 440 | console.log(e); 441 | } 442 | 443 | function updateCanvasContainerSize () { 444 | canvasContainerSize = document.querySelector('.canvases').getBoundingClientRect(); 445 | 446 | canvasContainer.style.width = (canvasContainerSize.width + croquis.getCanvasWidth() * view.zoom) + 'px'; 447 | canvasContainer.style.height = (canvasContainerSize.height + croquis.getCanvasHeight() * view.zoom) + 'px'; 448 | } 449 | 450 | window.onresize = () => { 451 | updateCanvasContainerSize(); 452 | } 453 | 454 | // Zoom 455 | 456 | const zoomIn = () => setZoom(view.zoom + 0.1); 457 | const zoomOut = () => setZoom(view.zoom - 0.1); 458 | const resetZoom = () => setZoom(1); 459 | 460 | function setZoom (zoom) { 461 | view.zoom = zoom; 462 | if (view.zoom < 0.1) view.zoom = 0.1; 463 | if (view.zoom > 10) view.zoom = 10; 464 | document.querySelector('.js-zoom').innerText = Math.round(view.zoom * 100) + '%'; 465 | 466 | document.querySelectorAll('.canvases canvas').forEach(el => { 467 | el.style.width = Math.round(croquis.getCanvasWidth() * view.zoom + 2) + 'px'; 468 | el.style.height = Math.round(croquis.getCanvasHeight() * view.zoom + 2) + 'px'; 469 | }); 470 | 471 | updateCanvasContainerSize(); 472 | updatePointer(); 473 | } 474 | 475 | function centerImage () { 476 | canvasSize = canvasContainer.getBoundingClientRect(); 477 | 478 | canvasContainer.parentElement.scrollTop = (canvasSize.height - croquis.getCanvasHeight()) / 2 + 1; 479 | canvasContainer.parentElement.scrollLeft = (canvasSize.width - croquis.getCanvasWidth()) / 2 + 1; 480 | } 481 | --------------------------------------------------------------------------------