var Draw = { initCanvas: null, SMALL: 4, MEDIUM: 6, BIG: 8, CROP_PADDING: 0.15, draw: null, // widget wrapper canvas: null, // drawable ctx: null, bg: null, // non drawable, only used for background drawing: false, mousePos: null, lastPos: null, drawingData: null, // data structure for saving drawn points control: null, actions: null, topNav: null, save: null, // button backgroundCanvas: null, // background coming from another widget as a canvas bgHeight: null, // natural height of background bgWidth: null, // natural width of background ratio: 1, // upscale ratio init: function () { MovimTpl.pushAnchorState('draw', function () { Draw.draw.classList.remove('bound', 'open'); }); Draw.drawingData = []; if (Draw.initCanvas) { Draw.backgroundCanvas = Draw.initCanvas; Draw.initCanvas = null; } else { Draw.backgroundCanvas = null; } Draw.topNav = document.querySelector('.draw-top-nav'); Draw.control = document.querySelector('.draw-control'); Draw.actions = document.querySelector('.draw-actions'); Draw.draw = document.getElementById('draw'); const canvasWrapper = document.querySelector('#draw .canvas'); const colors = document.querySelectorAll('.draw-colors li'); const widths = document.querySelectorAll('.draw-widths li'); const eraser = document.querySelector('.draw-eraser'); Draw.draw.classList.add('open'); let height, width; if (Draw.backgroundCanvas) { Draw.bgHeight = Draw.backgroundCanvas.height; Draw.bgWidth = Draw.backgroundCanvas.width; const dheight = document.body.clientHeight; const dwidth = document.body.clientWidth; if (Draw.bgHeight <= dheight && Draw.bgWidth <= dwidth) { height = Draw.bgHeight; width = Draw.bgWidth; } else { const bgTaller = Draw.bgHeight / Draw.bgWidth > dheight / dwidth; if (Draw.bgHeight <= dheight || !bgTaller) { width = dwidth; height = dwidth * Draw.bgHeight / Draw.bgWidth; Draw.ratio = Draw.bgWidth / dwidth; } else if (Draw.bgWidth <= dwidth || bgTaller) { height = dheight; width = dheight * Draw.bgWidth / Draw.bgHeight; Draw.ratio = Draw.bgHeight / dheight; } } } else { height = document.body.clientHeight; width = document.body.clientWidth; } canvasWrapper.style.height = `${height}px`; canvasWrapper.style.width = `${width}px`; Draw.canvas = document.getElementById('draw-canvas'); Draw.canvas.width = width; Draw.canvas.height = height; Draw.ctx = Draw.canvas.getContext('2d'); Draw.ctx.lineCap = 'round'; Draw.screen = document.getElementById('screen-canvas'); Draw.screen.width = width; Draw.screen.height = height; Draw.sctx = Draw.screen.getContext('2d'); Draw.sctx.lineCap = Draw.ctx.lineCap; Draw.bg = document.getElementById('draw-background'); Draw.bg.width = width; Draw.bg.height = height; bgctx = Draw.bg.getContext('2d'); if (Draw.backgroundCanvas) { // copy over background image bgctx.drawImage(Draw.backgroundCanvas, 0, 0, width, height); } else { // fill canvas with white bgctx.fillStyle = 'white'; bgctx.fillRect(0, 0, width, height); } // init controls Draw.ctx.strokeStyle = '#000'; Draw.sctx.strokeStyle = '#000'; colors.forEach(item => item.classList.remove('selected')); eraser.classList.remove('selected'); document.querySelector('[data-color=black]').classList.add('selected'); Draw.ctx.lineWidth = Draw.MEDIUM; Draw.sctx.lineWidth = Draw.MEDIUM; widths.forEach(item => item.classList.remove('selected')); document.querySelector('[data-width=medium]').classList.add('selected'); if (Draw.draw.classList.contains('bound')) return; // Get a regular interval for drawing to the screen window.requestAnimFrame = (function (callback) { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimaitonFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; })(); // Allow for animation (function drawLoop() { requestAnimFrame(drawLoop); Draw.renderCanvas(); })(); Draw.screen.addEventListener('mousedown', Draw.startDrawing, true); Draw.screen.addEventListener('mouseenter', Draw.startDrawing, false); Draw.screen.addEventListener('mouseup', Draw.stopDrawing, false); Draw.screen.addEventListener('mouseleave', Draw.stopDrawing, false); Draw.screen.addEventListener('mousemove', function (e) { Draw.mousePos = Draw.getPos(Draw.screen, e); }, false); // Set up touch events for mobile, etc Draw.screen.addEventListener('touchstart', function (e) { Draw.mousePos = Draw.getPos(Draw.screen, e); var touch = e.touches[0]; var mouseEvent = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY, buttons: 1 }); Draw.screen.dispatchEvent(mouseEvent); }, false); Draw.screen.addEventListener('touchend', Draw.stopDrawing, false); Draw.screen.addEventListener('touchmove', function (e) { var touch = e.touches[0]; var mouseEvent = new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY }); Draw.screen.dispatchEvent(mouseEvent); }, false); // Clear canvas const clear = document.getElementById('draw-clear'); clear.addEventListener('click', (e) => { const rect = Draw.screen.getBoundingClientRect(); Draw.ctx.clearRect(0, 0, rect.width, rect.height); Draw.drawingData = []; }, false); // Save (background +) drawing Draw.save = document.querySelector('#draw-save'); Draw.save.onclick = (e) => { const finalCanvas = document.createElement('canvas'); let crop, height, width, bg; if (Draw.backgroundCanvas) { width = Draw.bgWidth; height = Draw.bgHeight; bg = Draw.backgroundCanvas; } else { crop = Draw.autoCropDrawing(); width = crop.width; height = crop.height; bg = Draw.bg; } finalCanvas.setAttribute('width', width); finalCanvas.setAttribute('height', height); const finalctx = finalCanvas.getContext('2d'); finalctx.lineCap = 'round'; if (Draw.backgroundCanvas) { Draw.upscaleDrawing(finalctx); } else { finalctx.putImageData(crop, 0, 0); } // add background finalctx.globalCompositeOperation = 'destination-over'; finalctx.drawImage( bg, 0, 0, width, height ); // Disable button Draw.save.classList.add('disabled'); finalCanvas.toBlob( function (blob) { Upload.prepare(blob); Upload.name = 'drawing.jpg'; Upload.init(); }, 'image/jpeg', 0.85 ); }; // Use the eraser eraser.addEventListener('click', function (e) { colors.forEach(item => item.classList.remove('selected')); this.classList.add('selected'); Draw.ctx.globalCompositeOperation = 'destination-out'; }, false); // Change pencil color for (let i = 0; i < colors.length; i++) { colors[i].addEventListener('click', function (e) { colors.forEach(item => item.classList.remove('selected')); eraser.classList.remove('selected'); this.classList.add('selected'); Draw.ctx.globalCompositeOperation = 'source-over'; Draw.ctx.strokeStyle = window.getComputedStyle(colors[i].querySelector('span.primary')).backgroundColor; Draw.sctx.strokeStyle = window.getComputedStyle(colors[i].querySelector('span.primary')).backgroundColor; }); } // Change pencil thickness for (let i = 0; i < widths.length; i++) { widths[i].addEventListener('click', function (e) { widths.forEach(item => item.classList.remove('selected')); this.classList.add('selected'); let width; switch (this.dataset.width) { case 'small': width = Draw.SMALL; break; case 'medium': width = Draw.MEDIUM; break; case 'big': width = Draw.BIG; break; default: width = Draw.SMALL; } Draw.ctx.lineWidth = width; Draw.sctx.lineWidth = width; }); } const drawback = document.querySelector('#draw #drawback'); drawback.addEventListener('click', () => { history.back(); }); // Add a flag to not re-bind event listeners Draw.draw.classList.add('bound'); }, stopDrawing: function (e) { // print to actual canvas if (Draw.drawingData.length) Draw.smoothenLine(Draw.drawingData[Draw.drawingData.length - 1], Draw.ctx, 1) // cleanup screen const rect = Draw.screen.getBoundingClientRect(); Draw.sctx.clearRect(0, 0, rect.width, rect.height); Draw.drawing = false; Draw.lastPos = null; Draw.mousePos = null; // show coontrols Draw.topNav.classList.remove('drawing'); Draw.actions.classList.remove('drawing'); Draw.control.classList.remove('drawing'); Draw.ctx.beginPath(); Draw.sctx.beginPath(); }, startDrawing: function (e) { if (e.buttons == 1) { Draw.drawing = true; // hide controls Draw.topNav.classList.add('drawing'); Draw.actions.classList.add('drawing'); Draw.control.classList.add('drawing'); // save drawing data const data = { gco: Draw.sctx.globalCompositeOperation, width: Draw.sctx.lineWidth, color: Draw.sctx.strokeStyle, points: [] } Draw.drawingData.push(data); Draw.lastPos = Draw.getPos(Draw.screen, e); } }, upscaleDrawing: function (finalctx) { // re-draw upscaled for (let i = 0; i < Draw.drawingData.length; i++) { finalctx.globalCompositeOperation = Draw.drawingData[i].gco; finalctx.lineWidth = Draw.drawingData[i].width * Draw.ratio; finalctx.strokeStyle = Draw.drawingData[i].color; Draw.smoothenLine(Draw.drawingData[i], finalctx, Draw.ratio); finalctx.beginPath(); } }, smoothenLine(line, ctx, ratio) { let j = 0; // keep only half of the points const sample = line.points.filter((point, index) => index % 2 == 0) while (sample.length < 4) { sample.push(sample[sample.length - 1]); } if (sample.length >= 4) { // move to 1st point of the line ctx.moveTo( sample[j].x * ratio, sample[j].y * ratio ); for (j = 1; j < sample.length - 2; j++) { // define controle point as the mean point between current point and next point const c = (sample[j].x + sample[j + 1].x) / 2; const d = (sample[j].y + sample[j + 1].y) / 2; // draw a curve between ctx.quadraticCurveTo( sample[j].x * ratio, sample[j].y * ratio, c * ratio, d * ratio ); } ctx.quadraticCurveTo( sample[j].x * ratio, sample[j].y * ratio, sample[j + 1].x * ratio, sample[j + 1].y * ratio ); } ctx.stroke(); }, autoCropDrawing: function () { const rect = Draw.screen.getBoundingClientRect(); let height = rect.height; let width = rect.width; const imgData = Draw.ctx.getImageData(0, 0, width, height); let maxx = 0, minx = width, maxy = 0, miny = height, index; // list all non transparent pixels for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { index = (y * width + x) * 4; if (imgData.data[index + 3] > 0) { minx = Math.min(x, minx); miny = Math.min(y, miny); maxx = Math.max(x, maxx); maxy = Math.max(y, maxy); } } } width = maxx - minx + 1; height = maxy - miny + 1; return Draw.ctx.getImageData( minx - (width * Draw.CROP_PADDING), miny - (height * Draw.CROP_PADDING), width * (1 + 2 * Draw.CROP_PADDING), height * (1 + 2 * Draw.CROP_PADDING) ) }, // Get the position of the mouse/touch relative to the canvas getPos: function (canvasDom, event) { var rect = canvasDom.getBoundingClientRect(); let x, y; if (event.touches) { x = event.touches[0].clientX - rect.left; y = event.touches[0].clientY - rect.top; } else { x = event.clientX - rect.left; y = event.clientY - rect.top; } if (Draw.drawing) { let points = Draw.drawingData[Draw.drawingData.length - 1].points; points.push({ x, y }); } return { x, y }; }, // Draw to the canvas renderCanvas: function () { if (Draw.drawing && Draw.lastPos && Draw.mousePos) { Draw.sctx.moveTo(Draw.lastPos.x, Draw.lastPos.y); Draw.sctx.lineTo(Draw.mousePos.x, Draw.mousePos.y); Draw.sctx.stroke(); Draw.lastPos = Draw.mousePos; } }, disableForCanvas: function (e) { if (e.target.tagName == 'canvas') { e.preventDefault(); } } }; MovimEvents.registerWindow('loaded', 'draw', () => { Upload.attach((file) => { if (Draw.draw) Draw.draw.classList = ''; if (Draw.save) { Draw.save.classList.remove('disabled'); Draw.save.style.backgroundImage = ''; } }); Upload.fail(() => { if (Draw.draw) Draw.draw.classList = 'upload'; if (Draw.save) { Draw.save.classList.remove('disabled'); Draw.save.style.backgroundImage = ''; } }); Upload.progress((percent) => { if (Draw.save) { Draw.save.style.backgroundImage = 'linear-gradient(to top, rgba(0, 0, 0, 0.5) ' + percent + '%, transparent ' + percent + '%)'; } }); }); MovimEvents.registerBody('touchstart', 'draw', (e) => Draw.disableForCanvas(e)); MovimEvents.registerBody('touchend', 'draw', (e) => Draw.disableForCanvas(e)); MovimEvents.registerBody('touchmove', 'draw', (e) => Draw.disableForCanvas(e));