function get_brush_canvas_size(brush_size, brush_shape){ // brush_shape optional, only matters if it's circle // TODO: does it actually still matter? the ellipse drawing code has changed // round to nearest even number in order for the canvas to be drawn centered at a point reasonably return Math.ceil(brush_size * (brush_shape === "circle" ? 2.1 : 1) / 2) * 2; } function render_brush(ctx, shape, size){ // USAGE NOTE: must be called outside of any other usage of op_canvas (because of draw_ellipse) if(shape.match(/diagonal/)){ size -= 0.4; } var mid_x = Math.round(ctx.canvas.width / 2); var left = Math.round(mid_x - size/2); var right = Math.round(mid_x + size/2); var mid_y = Math.round(ctx.canvas.height / 2); var top = Math.round(mid_y - size/2); var bottom = Math.round(mid_y + size/2); if(shape === "circle"){ // TODO: ideally _without_pattern_support draw_ellipse(ctx, left, top, size, size, false, true); // was useful for testing: // ctx.fillStyle = "red"; // ctx.fillRect(mid_x, mid_y, 1, 1); }else if(shape === "square"){ ctx.fillRect(left, top, ~~size, ~~size); }else if(shape === "diagonal"){ draw_line_without_pattern_support(ctx, left, top, right, bottom); }else if(shape === "reverse_diagonal"){ draw_line_without_pattern_support(ctx, left, bottom, right, top); }else if(shape === "horizontal"){ draw_line_without_pattern_support(ctx, left, mid_y, size, mid_y); }else if(shape === "vertical"){ draw_line_without_pattern_support(ctx, mid_x, top, mid_x, size); } } function draw_ellipse(ctx, x, y, w, h, stroke, fill){ var center_x = x + w/2; var center_y = y + h/2; if(aliasing){ var points = []; var step = 0.05; for(var theta = 0; theta < TAU; theta += step){ points.push({ x: center_x + Math.cos(theta) * w/2, y: center_y + Math.sin(theta) * h/2, }); } draw_polygon(ctx, points, stroke, fill); }else{ if(w < 0){ x += w; w = -w; } if(h < 0){ y += h; h = -h; } ctx.beginPath(); ctx.ellipse(center_x, center_y, w/2, h/2, 0, TAU, false); ctx.stroke(); ctx.fill(); } } function draw_rounded_rectangle(ctx, x, y, width, height, radius_x, radius_y, stroke, fill){ if(aliasing){ var points = []; var lineTo = (x, y)=> { points.push({x, y}); }; var arc = (x, y, radius_x, radius_y, startAngle, endAngle)=> { var step = 0.05; for(var theta = startAngle; theta < endAngle; theta += step){ points.push({ x: x + Math.cos(theta) * radius_x, y: y + Math.sin(theta) * radius_y, }); } // not just doing `theta <= endAngle` above because that doesn't account for floating point rounding errors points.push({ x: x + Math.cos(endAngle) * radius_x, y: y + Math.sin(endAngle) * radius_y, }); }; var x2 = x + width; var y2 = y + height; arc(x2 - radius_x, y + radius_y, radius_x, radius_y, TAU*3/4, TAU, false); lineTo(x2, y2 - radius_y); arc(x2 - radius_x, y2 - radius_y, radius_x, radius_y, 0, TAU*1/4, false); lineTo(x + radius_x, y2); arc(x + radius_x, y2 - radius_y, radius_x, radius_y, TAU*1/4, TAU*1/2, false); lineTo(x, y + radius_y); arc(x + radius_x, y + radius_y, radius_x, radius_y, TAU/2, TAU*3/4, false); draw_polygon(ctx, points, stroke, fill); }else{ ctx.beginPath(); ctx.moveTo(x + radius_x, y); ctx.lineTo(x + width - radius_x, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius_y); ctx.lineTo(x + width, y + height - radius_y); ctx.quadraticCurveTo(x + width, y + height, x + width - radius_x, y + height); ctx.lineTo(x + radius_x, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius_y); ctx.lineTo(x, y + radius_y); ctx.quadraticCurveTo(x, y, x + radius_x, y); ctx.closePath(); if(stroke){ ctx.stroke(); } if(fill){ ctx.fill(); } } } var line_brush_canvas; var line_brush_canvas_rendered_shape; var line_brush_canvas_rendered_color; var line_brush_canvas_rendered_size; function update_brush_for_drawing_lines(stroke_size){ // USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush) if(aliasing && stroke_size > 1){ // TODO: DRY brush caching code if( line_brush_canvas_rendered_shape !== "circle" || line_brush_canvas_rendered_color !== stroke_color || line_brush_canvas_rendered_size !== stroke_size ){ // don't need to do brush_ctx.disable_image_smoothing() currently because images aren't drawn to the brush var csz = get_brush_canvas_size(stroke_size, "circle"); line_brush_canvas = new Canvas(csz, csz); line_brush_canvas.width = csz; line_brush_canvas.height = csz; line_brush_canvas.ctx.fillStyle = line_brush_canvas.ctx.strokeStyle = stroke_color; render_brush(line_brush_canvas.ctx, "circle", stroke_size); line_brush_canvas_rendered_shape = "circle"; line_brush_canvas_rendered_color = stroke_color; line_brush_canvas_rendered_size = stroke_size; } } } function draw_line_without_pattern_support(ctx, x1, y1, x2, y2, stroke_size){ stroke_size = stroke_size || 1; if(aliasing){ if(stroke_size > 1){ bresenham_line(x1, y1, x2, y2, function(x, y){ ctx.drawImage(line_brush_canvas, ~~(x - line_brush_canvas.width/2), ~~(y - line_brush_canvas.height/2)); }); }else{ bresenham_line(x1, y1, x2, y2, function(x, y){ ctx.fillRect(x, y, 1, 1); }); } }else{ ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineWidth = stroke_size; ctx.lineCap = "round"; ctx.stroke(); ctx.lineCap = "butt"; } } function bresenham_line(x1, y1, x2, y2, callback){ // Bresenham's line algorithm x1=~~x1, x2=~~x2, y1=~~y1, y2=~~y2; var dx = Math.abs(x2 - x1); var dy = Math.abs(y2 - y1); var sx = (x1 < x2) ? 1 : -1; var sy = (y1 < y2) ? 1 : -1; var err = dx - dy; // eslint-disable-next-line no-constant-condition while(true){ callback(x1, y1); if(x1===x2 && y1===y2) break; var e2 = err*2; if(e2 >-dy){ err -= dy; x1 += sx; } if(e2 < dx){ err += dx; y1 += sy; } } } function brosandham_line(x1, y1, x2, y2, callback){ // Bresenham's line argorithm with a callback between going horizontal and vertical x1=~~x1, x2=~~x2, y1=~~y1, y2=~~y2; var dx = Math.abs(x2 - x1); var dy = Math.abs(y2 - y1); var sx = (x1 < x2) ? 1 : -1; var sy = (y1 < y2) ? 1 : -1; var err = dx - dy; // eslint-disable-next-line no-constant-condition while(true){ callback(x1, y1); if(x1===x2 && y1===y2) break; var e2 = err*2; if(e2 >-dy){ err -= dy; x1 += sx; } callback(x1, y1); if(e2 < dx){ err += dx; y1 += sy; } } } function draw_fill(ctx, start_x, start_y, fill_r, fill_g, fill_b, fill_a){ // TODO: split up processing in case it takes too long? // progress bar and abort button (outside of image-manipulation.js) // or at least just free up the main thread every once in a while // TODO: speed up with typed arrays? https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipulation-with-typed-arrays/ // could avoid endianness issues if only copying colors // the jsperf only shows ~15% improvement // maybe do something fancier like special-casing large chunks of single-color image // (octree? or just have a higher level stack of chunks to fill and check at if a chunk is homogeneous) var stack = [[start_x, start_y]]; var c_width = canvas.width; var c_height = canvas.height; var id = ctx.getImageData(0, 0, c_width, c_height); pixel_pos = (start_y*c_width + start_x) * 4; var start_r = id.data[pixel_pos+0]; var start_g = id.data[pixel_pos+1]; var start_b = id.data[pixel_pos+2]; var start_a = id.data[pixel_pos+3]; if( fill_r === start_r && fill_g === start_g && fill_b === start_b && fill_a === start_a ){ return; } while(stack.length){ var new_pos, x, y, pixel_pos, reach_left, reach_right; new_pos = stack.pop(); x = new_pos[0]; y = new_pos[1]; pixel_pos = (y*c_width + x) * 4; while(matches_start_color(pixel_pos)){ y--; pixel_pos = (y*c_width + x) * 4; } reach_left = false; reach_right = false; // eslint-disable-next-line no-constant-condition while(true){ y++; pixel_pos = (y*c_width + x) * 4; if(!(y < c_height && matches_start_color(pixel_pos))){ break; } color_pixel(pixel_pos); if(x > 0){ if(matches_start_color(pixel_pos - 4)){ if(!reach_left){ stack.push([x - 1, y]); reach_left = true; } }else if(reach_left){ reach_left = false; } } if(x < c_width-1){ if(matches_start_color(pixel_pos + 4)){ if(!reach_right){ stack.push([x + 1, y]); reach_right = true; } }else if(reach_right){ reach_right = false; } } pixel_pos += c_width * 4; } } ctx.putImageData(id, 0, 0); function matches_start_color(pixel_pos){ return ( id.data[pixel_pos+0] === start_r && id.data[pixel_pos+1] === start_g && id.data[pixel_pos+2] === start_b && id.data[pixel_pos+3] === start_a ); } function color_pixel(pixel_pos){ id.data[pixel_pos+0] = fill_r; id.data[pixel_pos+1] = fill_g; id.data[pixel_pos+2] = fill_b; id.data[pixel_pos+3] = fill_a; } } function draw_noncontiguous_fill(ctx, x, y, fill_r, fill_g, fill_b, fill_a){ var c_width = canvas.width; var c_height = canvas.height; var id = ctx.getImageData(0, 0, c_width, c_height); pixel_pos = (y*c_width + x) * 4; var start_r = id.data[pixel_pos+0]; var start_g = id.data[pixel_pos+1]; var start_b = id.data[pixel_pos+2]; var start_a = id.data[pixel_pos+3]; if( fill_r === start_r && fill_g === start_g && fill_b === start_b && fill_a === start_a ){ return; } for(var i=0; i 1){ update_brush_for_drawing_lines(stroke_size); } var stroke_color = ctx.strokeStyle; var fill_color = ctx.fillStyle; var numPoints = points.length; var numCoords = numPoints * 2 if(numPoints === 0){ return; } var x_min = +Infinity; var x_max = -Infinity; var y_min = +Infinity; var y_max = -Infinity; for (var i = 0; i < numPoints; i++) { var {x, y} = points[i]; x_min = Math.min(x, x_min); x_max = Math.max(x, x_max); y_min = Math.min(y, y_min); y_max = Math.max(y, y_max); } x_max += 1; y_max += 1; x_min -= 1; y_min -= 1; op_canvas_webgl.width = x_max - x_min; op_canvas_webgl.height = y_max - y_min; gl.viewport(0, 0, op_canvas_webgl.width, op_canvas_webgl.height); var coords = new Float32Array(numCoords); for (let i = 0; i < numPoints; i++) { coords[i*2+0] = (points[i].x - x_min) / op_canvas_webgl.width * 2 - 1; coords[i*2+1] = 1 - (points[i].y - y_min) / op_canvas_webgl.height * 2; // TODO: investigate: does this cause resolution/information loss? can we change the coordinate system? } if(fill){ var contours = [coords]; var polyTriangles = triangulate(contours); let numVertices = initArrayBuffer(polyTriangles); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, numVertices); op_canvas_2d.width = op_canvas_webgl.width; op_canvas_2d.height = op_canvas_webgl.height; op_ctx_2d.drawImage(op_canvas_webgl, 0, 0); replace_colors_with_swatch(op_ctx_2d, fill_color, x_min, y_min); ctx.drawImage(op_canvas_2d, x_min, y_min); } if(stroke){ if(stroke_size > 1){ var stroke_margin = ~~(stroke_size * 1.1); var op_canvas_x = x_min - stroke_margin; var op_canvas_y = y_min - stroke_margin; op_canvas_2d.width = x_max - x_min + stroke_margin * 2; op_canvas_2d.height = y_max - y_min + stroke_margin * 2; for (let i = 0; i < numPoints - (close_path ? 0 : 1); i++) { var point_a = points[i]; var point_b = points[(i + 1) % numPoints]; // Note: update_brush_for_drawing_lines way above draw_line_without_pattern_support( op_ctx_2d, point_a.x - op_canvas_x, point_a.y - op_canvas_y, point_b.x - op_canvas_x, point_b.y - op_canvas_y, stroke_size ); } replace_colors_with_swatch(op_ctx_2d, stroke_color, x, y); ctx.drawImage(op_canvas_2d, x, y); }else{ let numVertices = initArrayBuffer(coords); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(close_path ? gl.LINE_LOOP : gl.LINE_STRIP, 0, numVertices); op_canvas_2d.width = op_canvas_webgl.width; op_canvas_2d.height = op_canvas_webgl.height; op_ctx_2d.drawImage(op_canvas_webgl, 0, 0); replace_colors_with_swatch(op_ctx_2d, stroke_color, x_min, y_min); ctx.drawImage(op_canvas_2d, x_min, y_min); } } } window.copy_contents_within_polygon = function(canvas, points, x_min, y_min, x_max, y_max){ // Copy the contents of the given canvas within the polygon given by points bounded by x/y_min/max x_max = Math.max(x_max, x_min + 1); y_max = Math.max(y_max, y_min + 1); var width = x_max - x_min; var height = y_max - y_min; // TODO: maybe have the cutout only the width/height of the bounds // var cutout = new Canvas(width, height); var cutout = new Canvas(canvas); cutout.ctx.save(); cutout.ctx.globalCompositeOperation = "destination-in"; draw_polygon(cutout.ctx, points, false, true); cutout.ctx.restore(); var cutout_crop = new Canvas(width, height); cutout_crop.ctx.drawImage(cutout, x_min, y_min, width, height, 0, 0, width, height); return cutout_crop; } // TODO: maybe shouldn't be external... window.draw_with_swatch = function(ctx, x_min, y_min, x_max, y_max, swatch, callback) { var stroke_margin = ~~(stroke_size * 1.1); x_max = Math.max(x_max, x_min + 1); y_max = Math.max(y_max, y_min + 1); op_canvas_2d.width = x_max - x_min + stroke_margin * 2; op_canvas_2d.height = y_max - y_min + stroke_margin * 2; var x = x_min - stroke_margin; var y = y_min - stroke_margin; op_ctx_2d.save(); op_ctx_2d.translate(-x, -y); callback(op_ctx_2d); op_ctx_2d.restore(); // for replace_colors_with_swatch! replace_colors_with_swatch(op_ctx_2d, swatch, x, y); ctx.drawImage(op_canvas_2d, x, y); // for debug: // ctx.fillStyle = "rgba(255, 0, 255, 0.1)"; // ctx.fillRect(x, y, op_canvas_2d.width, op_canvas_2d.height); } })();