PAINT

Diviertete pintando con esta version Paint, espero que te guste

/************************************************************************ * * Eloquent JavaScript * by Marijn Haverbeke * * Chapter 19 * Project: A Paint Program * * Just so you know, most of this code is Marijn's, comments are mine. * This exercise is meant to be a fun way of exploring how JavaScript can * interact with form elements while reviewing previous material in the * book. If you want to learn how to program with JavaScript, I sincerely * hope you check his book out. It's free online: * * https://eloquentjavascript.net/ * * but I suggest buying it and marking it all up with your own notes * and thoughts. * * Now, TO THE CODE!!! * * Ryan Boone * falldowngoboone@gmail.com * ***********************************************************************/ // the core of the program; appends the paint interface to the // DOM element given as an argument (parent) function createPaint(parent) { var canvas = elt('canvas', {width: 500, height: 300}); var cx = canvas.getContext('2d'); var toolbar = elt('div', {class: 'toolbar'}); // calls the every function in controls, passing in context, // then appending the returned results to the toolbar for (var name in controls) toolbar.appendChild(controls[name](cx)); var panel = elt('div', {class: 'picturepanel'}, canvas); parent.appendChild(elt('div', null, panel, toolbar)); } /************************************************************************ * helper functions ***********************************************************************/ // creates an element with a name and object (attributes) // appends all further arguments it gets as child nodes // string arguments create text nodes // ex: elt('div', {class: 'foo'}, 'Hello, world!'); function elt(name, attributes) { var node = document.createElement(name); if (attributes) { for (var attr in attributes) if (attributes.hasOwnProperty(attr)) node.setAttribute(attr, attributes[attr]); } for (var i = 2; i < arguments.length; i++) { var child = arguments[i]; // if this argument is a string, create a text node if (typeof child == 'string') child = document.createTextNode(child); node.appendChild(child); } return node; } // figures out canvas relative coordinates for accurate functionality function relativePos(event, element) { var rect = element.getBoundingClientRect(); return {x: Math.floor(event.clientX - rect.left), y: Math.floor(event.clientY - rect.top)}; } // registers and unregisters listeners for tools function trackDrag(onMove, onEnd) { function end(event) { removeEventListener('mousemove', onMove); removeEventListener('mouseup', end); if (onEnd) onEnd(event); } addEventListener('mousemove', onMove); addEventListener('mouseup', end); } // loads an image from a URL and replaces the contents of the canvas function loadImageURL(cx, url) { var image = document.createElement('img'); image.addEventListener('load', function() { var color = cx.fillStyle, size = cx.lineWidth; cx.canvas.width = image.width; cx.canvas.height = image.height; cx.drawImage(image, 0, 0); cx.fillStyle = color; cx.strokeStyle = color; cx.lineWidth = size; }); image.src = url; } // used by tools.Spray // randomly positions dots function randomPointInRadius(radius) { for (;;) { var x = Math.random() * 2 - 1; var y = Math.random() * 2 - 1; // uses the Pythagorean theorem to test if a point is inside a circle if (x * x + y * y <= 1) return {x: x * radius, y: y * radius}; } } /************************************************************************ * controls ***********************************************************************/ // holds static methods to initialize the various controls; // Object.create() is used to create a truly empty object var controls = Object.create(null); controls.tool = function(cx) { var select = elt('select'); // populate the tools for (var name in tools) select.appendChild(elt('option', null, name)); // calls the particular method associated with the current tool cx.canvas.addEventListener('mousedown', function(event) { // is the left mouse button being pressed? if (event.which == 1) { // the event needs to be passed to the method to determine // what the mouse is doing and where it is tools[select.value](event, cx); // don't select when user is clicking and dragging event.preventDefault(); } }); return elt('span', null, 'Tool: ', select); }; // color module controls.color = function(cx) { var input = elt('input', {type: 'color'}); // on change, set the new color style for fill and stroke input.addEventListener('change', function() { cx.fillStyle = input.value; cx.strokeStyle = input.value; }); return elt('span', null, 'Color: ', input); }; // brush size module controls.brushSize = function(cx) { var select = elt('select'); // various brush sizes var sizes = [1, 2, 3, 5, 8, 12, 25, 35, 50, 75, 100]; // build up a select group of size options sizes.forEach(function(size) { select.appendChild(elt('option', {value: size}, size + ' pixels')); }); // on change, set the new stroke thickness select.addEventListener('change', function() { cx.lineWidth = select.value; }); return elt('span', null, 'Brush size: ', select); }; // save controls.save = function(cx) { // MUST open in a new window because of iframe security stuff var link = elt('a', {href: '/', target: '_blank'}, 'Save'); function update() { try { link.href = cx.canvas.toDataURL(); } catch(e) { // some browsers choke on big data URLs // also, if the server response doesn't include a header that tells the browser it // can be used on other domains, the script won't be able to look at it; // this is in order to prevent private information from leaking to a script; // pixel data, data URL or otherwise, cannot be extracted from a "tainted canvas" // and a SecurityError is thrown if (e instanceof SecurityError) link.href = 'javascript:alert(' + JSON.stringify('Can\'t save: ' + e.toString()) + ')'; else window.alert("Nope."); throw e; } } link.addEventListener('mouseover', update); link.addEventListener('focus', update); return link; }; // open a file controls.openFile = function(cx) { var input = elt('input', {type: 'file'}); input.addEventListener('change', function() { if (input.files.length == 0) return; var reader = new FileReader(); reader.addEventListener('load', function() { loadImageURL(cx, reader.result); }); reader.readAsDataURL(input.files[0]); }); return elt('div', null, 'Open file: ', input); }; // open a URL controls.openURL = function(cx) { var input = elt('input', {type: 'text'}); var form = elt('form', null, 'Open URL: ', input, elt('button', {type: 'submit'}, 'load')); form.addEventListener('submit', function(event) { event.preventDefault(); loadImageURL(cx, form.querySelector('input').value); }); return form; }; /************************************************************************ * tools ***********************************************************************/ // drawing tools var tools = Object.create(null); // line tool // onEnd is for the erase function, which uses it to reset // the globalCompositeOperation to source-over tools.Line = function(event, cx, onEnd) { cx.lineCap = 'round'; // mouse position relative to the canvas var pos = relativePos(event, cx.canvas); trackDrag(function(event) { cx.beginPath(); // move to current mouse position cx.moveTo(pos.x, pos.y); // update mouse position pos = relativePos(event, cx.canvas); // line to updated mouse position cx.lineTo(pos.x, pos.y); // stroke the line cx.stroke(); }, onEnd); }; // erase tool tools.Erase = function(event, cx) { // globalCompositeOperation determines how drawing operations // on a canvas affect what's already there // 'destination-out' makes pixels transparent, 'erasing' them // NOTE: this has been deprecated cx.globalCompositeOperation = 'destination-out'; tools.Line(event, cx, function() { cx.globalCompositeOperation = 'source-over'; }); }; // text tool tools.Text = function(event, cx) { var text = prompt('Text:', ''); if (text) { var pos = relativePos(event, cx.canvas); // for simplicity, text size is brush size, locked to sans-serif cx.font = Math.max(7, cx.lineWidth) + 'px sans-serif'; cx.fillText(text, pos.x, pos.y); } } // spray paint tool tools.Spray = function(event, cx) { var radius = cx.lineWidth / 2; var area = radius * radius * Math.PI; var dotsPerTick = Math.ceil(area / 30); var currentPos = relativePos(event, cx.canvas); var spray = setInterval(function() { for (var i = 0; i < dotsPerTick; i++) { var offset = randomPointInRadius(radius); cx.fillRect(currentPos.x + offset.x, currentPos.y + offset.y, 1, 1); } }, 25); trackDrag(function(event) { currentPos = relativePos(event, cx.canvas); }, function() { clearInterval(spray); }); }; /************************************************************************ * exercises ***********************************************************************/ /** * allows the user to click and drag out a rectangle on the canvas * * @param {Object} event - mousedown event (specifically left button) * @param {Object} cx - the canvas 2d context object */ tools.Rectangle = function(event, cx) { var leftX, rightX, topY, bottomY var clientX = event.clientX, clientY = event.clientY; // placeholder rectangle var placeholder = elt('div', {class: 'placeholder'}); // cache the relative position of mouse x and y on canvas var initialPos = relativePos(event, cx.canvas); // used for determining correct placeholder position var xOffset = clientX - initialPos.x, yOffset = clientY - initialPos.y; trackDrag(function(event) { document.body.appendChild(placeholder); var currentPos = relativePos(event, cx.canvas); var startX = initialPos.x, startY = initialPos.y; // assign leftX, rightX, topY and bottomY if (startX < currentPos.x) { leftX = startX; rightX = currentPos.x; } else { leftX = currentPos.x; rightX = startX; } if (startY < currentPos.y) { topY = startY; bottomY = currentPos.y; } else { topY = currentPos.y; bottomY = startY; } // set the style to reflect current fill placeholder.style.background = cx.fillStyle; // set div.style.left to leftX, width to rightX - leftX placeholder.style.left = leftX + xOffset + 'px'; placeholder.style.top = topY + yOffset + 'px'; placeholder.style.width = rightX - leftX + 'px'; placeholder.style.height = bottomY - topY + 'px'; }, function() { // add rectangle to canvas with leftX, rightX, topY and bottomY cx.fillRect(leftX, topY, rightX - leftX, bottomY - topY); // destroy placeholder document.body.removeChild(placeholder); }); }; /** * allows the user to load the color of a specific pixel * * @param {Object} event - mousedown event (specifically left button) * @param {Object} cx - the canvas 2d context object */ // TODO: rewrite with pixel object tools['Pick Color'] = function(event, cx) { try { var colorPos = relativePos(event, cx.canvas), // returns an array [r, g, b, opacity]; imageData = cx.getImageData(colorPos.x, colorPos.y, 1, 1), colorVals = imageData.data, color = ''; // build the color color += 'rgb('; // only need first three args for (var i = 0; i < colorVals.length - 1; i++) { color += colorVals[i]; // only need two commas if (i < 2) color += ','; } color += ')'; cx.fillStyle = color; cx.strokeStyle = color; } catch(e) { if (e instanceof SecurityError) alert('Whoops! Looks like you don\'t have permission to do that!'); else throw e; } }; // helpers for flood fill // iterates over N, S, E and W neighbors and performs a function fn function forEachNeighbor(point, fn) { fn({x: point.x - 1, y: point.y}); fn({x: point.x + 1, y: point.y}); fn({x: point.x, y: point.y - 1}); fn({x: point.x, y: point.y + 1}); } // checks if 2 points in data, point1 and point2, have the same color function isSameColor(data, point1, point2) { var offset1 = (point1.x + point1.y * data.width) * 4; var offset2 = (point2.x + point2.y * data.width) * 4; for (var i = 0; i < 4; i++) { if (data.data[offset1 + i] != data.data[offset2 + i]) { return false; } } return true; } // end flood fill helpers // NOTE: in my first attempt, I was creating Pixel objects and pushing them // to isPainted instead of Booleans; that wasn't a great idea... // I suspect there was too much initialization for the browser to handle // and it choked fatally :( // I think I would still like to see this done with a Pixel object, if not // merely for the ability to extend it to done some more advanced things /** * paints all adjacent matching pixels */ tools["Flood Fill"] = function(event, cx) { var imageData = cx.getImageData(0, 0, cx.canvas.width, cx.canvas.height), // get sample point at current position, {x: int, y: int} sample = relativePos(event, cx.canvas), isPainted = new Array(imageData.width * imageData.height), toPaint = [sample]; // while toPaint.length > 0 while(toPaint.length) { // current point to check var current = toPaint.pop(), id = current.x + current.y * imageData.width; // check if current has already been painted if (isPainted[id]) continue; else { // if it hasn't, paint current and set isPainted to true cx.fillRect(current.x, current.y, 1, 1); isPainted[id] = true; } // for every neighbor (new function) forEachNeighbor(current, function(neighbor) { if (neighbor.x >= 0 && neighbor.x < imageData.width && neighbor.y >= 0 && neighbor.y < imageData.height && isSameColor(imageData, sample, neighbor)) { toPaint.push(neighbor); } }); } }; // initialize the app var appDiv = document.querySelector('#paint-app'); createPaint(appDiv); /************************************************************************ * resources * * Canvas Rendering Context 2D API * https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D * * Eloquent JavaScript Ch 19, Project: A Paint Program * https://eloquentjavascript.net/19_paint.html ***********************************************************************/
© 2019 Heavy Lab. Todos los derechos reservados.
Creado con Webnode Cookies
¡Crea tu página web gratis! Esta página web fue creada con Webnode. Crea tu propia web gratis hoy mismo! Comenzar