function render_brush(ctx, shape, size){ if(shape === "circle"){ size /= 2; size += 0.25; }else if(shape.match(/diagonal/)){ size -= 0.4; } var mid_x = ctx.canvas.width / 2; var left = ~~(mid_x - size/2); var right = ~~(mid_x + size/2); var mid_y = ctx.canvas.height / 2; var top = ~~(mid_y - size/2); var bottom = ~~(mid_y + size/2); if(shape === "circle"){ draw_ellipse(ctx, left, top, size, size); }else if(shape === "square"){ ctx.fillRect(left, top, ~~size, ~~size); }else if(shape === "diagonal"){ draw_line(ctx, left, top, right, bottom); }else if(shape === "reverse_diagonal"){ draw_line(ctx, left, bottom, right, top); }else if(shape === "horizontal"){ draw_line(ctx, left, mid_y, size, mid_y); }else if(shape === "vertical"){ draw_line(ctx, mid_x, top, mid_x, size); } } function draw_ellipse(ctx, x, y, w, h, stroke, fill){ var stroke_color = ctx.strokeStyle; var fill_color = ctx.fillStyle; var cx = x + w/2; var cy = y + h/2; if(aliasing){ // @TODO: use proper raster ellipse algorithm var r1 = Math.round; var r2 = Math.round; ctx.fillStyle = stroke_color; for(var r=0; r 1){ var csz = stroke_size * 2.1; // XXX: magic constant duplicated from tools.js var brush_canvas = new Canvas(csz, csz); brush_canvas.width = csz; brush_canvas.height = csz; brush_canvas.ctx.fillStyle = brush_canvas.ctx.strokeStyle = stroke_color; render_brush(brush_canvas.ctx, "circle", stroke_size); bresenham_line(x1, y1, x2, y2, function(x, y){ ctx.drawImage(brush_canvas, ~~(x - brush_canvas.width/2), ~~(y - 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; while(1){ 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; while(1){ 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, x, 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 = [[x, y]]; 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; } 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; 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= y || points[j].y < y && points[i].y >= y ){ nodeX[nodes++] = points[i].x + (y - points[i].y) / (points[j].y - points[i].y) * (points[j].x - points[i].x); } j = i; } // Sort the nodes, via a simple “Bubble” sort. i = 0; while(i < nodes-1){ if(nodeX[i] > nodeX[i+1]){ swap = nodeX[i]; nodeX[i] = nodeX[i+1]; nodeX[i+1] = swap; if(i){ i--; } }else{ i++; } } // Browsers optimize sorting numbers, so just use Array::sort /*nodeX.sort(function(a, b){ return a - b; });*/ // But this array can contain undefineds [citation needed] // It's not defined by its length but by the variable `nodes` // Fill the pixels between node pairs. for(i = 0; i < nodes; i += 2){ if(nodeX[i+0] >= X_MAX) break; if(nodeX[i+1] > X_MIN){ if(nodeX[i+0] < X_MIN) nodeX[i+0] = X_MIN; if(nodeX[i+1] > X_MAX) nodeX[i+1] = X_MAX; for(var x = nodeX[i]; x < nodeX[i+1]; x++){ // fill pixel at (x, y) var idi = ((y-Y_MIN)*WIDTH + ~~(x-X_MIN))*4; id_dest.data[idi+0] = id_src.data[idi+0]; id_dest.data[idi+1] = id_src.data[idi+1]; id_dest.data[idi+2] = id_src.data[idi+2]; id_dest.data[idi+3] = id_src.data[idi+3]; } //edge_points.push({x: nodeX[i+0], y: y}); //edge_points.push({x: nodeX[i+1], y: y}); } } } // Done boom okay cutout.ctx.putImageData(id_dest, 0, 0); //cutout.edge_points = edge_points; return cutout; } (function(){ var tessy = (function initTesselator() { // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { // console.log(data[0], data[1]); polyVertArray[polyVertArray.length] = data[0]; polyVertArray[polyVertArray.length] = data[1]; } function begincallback(type) { if (type !== libtess.primitiveType.GL_TRIANGLES) { console.log('expected TRIANGLES but got type: ' + type); } } function errorcallback(errno) { console.log('error callback'); console.log('error number: ' + errno); } // callback for when segments intersect and must be split function combinecallback(coords, data, weight) { // console.log('combine callback'); return [coords[0], coords[1], coords[2]]; } function edgeCallback(flag) { // don't really care about the flag, but need no-strip/no-fan behavior // console.log('edge flag: ' + flag); } var tessy = new libtess.GluTesselator(); // tessy.gluTessProperty(libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_POSITIVE); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); return tessy; })(); function triangulate(contours) { // libtess will take 3d verts and flatten to a plane for tesselation // since only doing 2d tesselation here, provide z=1 normal to skip // iterating over verts only to get the same answer. tessy.gluTessNormal(0, 0, 1); var triangleVerts = []; tessy.gluTessBeginPolygon(triangleVerts); for (var i = 0; i < contours.length; i++) { tessy.gluTessBeginContour(); var contour = contours[i]; for (var j = 0; j < contour.length; j += 2) { var coords = [contour[j], contour[j + 1], 0]; tessy.gluTessVertex(coords, coords); } tessy.gluTessEndContour(); } tessy.gluTessEndPolygon(); return triangleVerts; } var gl; var positionLoc; var colorLoc; function initWebGL(canvas) { gl = canvas.getContext('webgl', { antialias: false }); var program = createShaderProgram(); positionLoc = gl.getAttribLocation(program, 'position'); gl.enableVertexAttribArray(positionLoc); colorLoc = gl.getUniformLocation(program, 'color'); } function initArrayBuffer(triangleVertexCoords) { // put triangle coordinates into a WebGL ArrayBuffer and bind to // shader's 'position' attribute variable var rawData = new Float32Array(triangleVertexCoords); var polygonArrayBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, polygonArrayBuffer); gl.bufferData(gl.ARRAY_BUFFER, rawData, gl.STATIC_DRAW); gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0); return triangleVertexCoords.length / 2; } function setDrawColor(color){ var color4fv = get_rgba_from_color(color).map((colorComponent)=> colorComponent / 255); gl.uniform4fv(colorLoc, color4fv); } function createShaderProgram() { // create vertex shader var vertexSrc = [ 'attribute vec4 position;', 'void main() {', ' /* already in normalized coordinates, so just pass through */', ' gl_Position = position;', '}' ].join(''); var vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexSrc); gl.compileShader(vertexShader); if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { console.log( 'Vertex shader failed to compile. Log: ' + gl.getShaderInfoLog(vertexShader) ); } // create fragment shader var fragmentSrc = [ 'precision mediump float;', 'uniform vec4 color;', 'void main() {', ' gl_FragColor = color;', '}' ].join(''); var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentSrc); gl.compileShader(fragmentShader); if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { console.log( 'Fragment shader failed to compile. Log: ' + gl.getShaderInfoLog(fragmentShader) ); } // link shaders to create our program var program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); return program; } var polygon_webgl_canvas = document.createElement('canvas'); var polygon_canvas_2d = document.createElement('canvas'); var polygon_ctx_2d = polygon_canvas_2d.getContext("2d"); initWebGL(polygon_webgl_canvas); window.draw_line_strip = function(ctx, points){ draw_polygon_or_line_strip(ctx, points, true, false, true); }; window.draw_polygon = function(ctx, points, stroke, fill){ draw_polygon_or_line_strip(ctx, points, stroke, fill, false); }; function replace_colors_with_swatch(ctx, swatch){ // mainly for patterns support (for black & white mode) ctx.globalCompositeOperation = "source-in"; ctx.fillStyle = swatch; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); } // TODO: cleanup: instead of as_polyline, the opposite, close_path function draw_polygon_or_line_strip(ctx, points, stroke, fill, as_polyline){ var stroke_color = ctx.strokeStyle; var fill_color = ctx.fillStyle; var numPoints = points.length; var numCoords = numPoints * 2 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; polygon_webgl_canvas.width = x_max - x_min; polygon_webgl_canvas.height = y_max - y_min; gl.viewport(0, 0, polygon_webgl_canvas.width, polygon_webgl_canvas.height); var coords = new Float32Array(numCoords); for (var i = 0; i < numPoints; i++) { coords[i*2+0] = (points[i].x - x_min) / polygon_webgl_canvas.width * 2 - 1; coords[i*2+1] = 1 - (points[i].y - y_min) / polygon_webgl_canvas.height * 2; // TODO: investigate: does this cause resolution/information loss? can we change the coordinate system? } if(fill){ // TODO: remove draw color if we're not using it since we're using replace_colors_with_swatch setDrawColor(fill_color); var contours = [coords]; var polyTriangles = triangulate(contours); var numVertices = initArrayBuffer(polyTriangles); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, numVertices); polygon_canvas_2d.width = polygon_webgl_canvas.width; polygon_canvas_2d.height = polygon_webgl_canvas.height; // polygon_ctx_2d.clearRect(0, 0, polygon_canvas_2d.width, polygon_canvas_2d.height); polygon_ctx_2d.drawImage(polygon_webgl_canvas, 0, 0); replace_colors_with_swatch(polygon_ctx_2d, fill_color); ctx.drawImage(polygon_canvas_2d, x_min, y_min); } if(stroke){ // polygon_ctx_2d.clearRect(0, 0, polygon_canvas_2d.width, polygon_canvas_2d.height); if(stroke_size > 1){ var polygon_stroke_margin = ~~(stroke_size * 1.1); polygon_canvas_2d.width = x_max - x_min + polygon_stroke_margin * 2; polygon_canvas_2d.height = y_max - y_min + polygon_stroke_margin * 2; for (var i = 0; i < numPoints - (as_polyline ? 1 : 0); i++) { var point_a = points[i]; var point_b = points[(i + 1) % numPoints]; draw_line( polygon_ctx_2d, point_a.x - x_min + polygon_stroke_margin, point_a.y - y_min + polygon_stroke_margin, point_b.x - x_min + polygon_stroke_margin, point_b.y - y_min + polygon_stroke_margin, stroke_size ) } // polygon_ctx_2d.beginPath(); // for (var i = 0; i < numPoints - (as_polyline ? 1 : 0); i++) { // var point_a = points[i]; // var point_b = points[(i + 1) % numPoints]; // ctx.moveTo( // point_a.x, // point_a.y // ) // ctx.lineTo( // point_b.x, // point_b.y // ) // } // polygon_ctx_2d.lineWidth = stroke_size; // polygon_ctx_2d.strokeStyle = "#f0f"; // polygon_ctx_2d.stroke(); replace_colors_with_swatch(polygon_ctx_2d, stroke_color); ctx.drawImage(polygon_canvas_2d, x_min - polygon_stroke_margin, y_min - polygon_stroke_margin); }else{ setDrawColor(stroke_color); var numVertices = initArrayBuffer(coords); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(as_polyline ? gl.LINE_STRIP : gl.LINE_LOOP, 0, numVertices); polygon_canvas_2d.width = polygon_webgl_canvas.width; polygon_canvas_2d.height = polygon_webgl_canvas.height; polygon_ctx_2d.drawImage(polygon_webgl_canvas, 0, 0); replace_colors_with_swatch(polygon_ctx_2d, stroke_color); ctx.drawImage(polygon_canvas_2d, x_min, y_min); } } }; })();