// SPDX-License-Identifier: GPL-2.0-or-later // Use Canvas to render a mesh gradient, passing the rendering to an image via a data stream. // Copyright: Tavmjong Bah 2018 // Distributed under GNU General Public License version 2 or later. See . (function() { var counter = 0; // Temp, number of calls to Canvas // Name spaces ----------------------------------- var svgNS = "http://www.w3.org/2000/svg"; var xlinkNS = "http://www.w3.org/1999/xlink" var xhtmlNS = "http://www.w3.org/1999/xhtml"; // Test if mesh gradients are supported. var m = document.createElementNS( svgNS, "meshgradient" ); if (m.x) { return; } // Test above test using known good SVG element // var l = document.createElementNS( svgNS, "linearGradient" ); // if (l.x1) { // console.log( "linearGradient has x1" ); // return; // } else { // console.log( "linearGradient does not have x1" ); // } // Point class ----------------------------------- function Point(x, y) { this.x = x || 0; this.y = y || 0; }; Point.prototype.x = null; Point.prototype.y = null; Point.prototype.get_x = function() { return this.x; } Point.prototype.get_y = function() { return this.y; } Point.prototype.clone = function() { return new Point(this.x, this.y); } Point.prototype.add = function(v) { return new Point(this.x + v.x, this.y + v.y); }; Point.prototype.scale = function(v) { if( v instanceof Point ) { return new Point(this.x * v.x, this.y * v.y); } return new Point(this.x * v, this.y * v); }; // Transform by affine Point.prototype.transform = function(a) { var x = this.x * a.a + this.y * a.c + a.e; var y = this.x * a.b + this.y * a.d + a.f; return new Point(x, y); }; Point.prototype.dist_sq = function(v) { var x = this.x - v.x; var y = this.y - v.y; return (x*x + y*y); }; Point.prototype.toString = function() { return "(x=" + this.x + ", y=" + this.y + ")"; }; // Affine class ----------------------------------- // As defined in the SVG spec // | a c e | // | b d f | // | 0 0 1 | function Affine(a, b, c, d, e, f) { if (a === undefined) { this.a = 1; this.b = 0; this.c = 0; this.d = 1; this.e = 0; this.f = 0; } else { this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; } }; Affine.prototype.a = null; Affine.prototype.b = null; Affine.prototype.c = null; Affine.prototype.d = null; Affine.prototype.e = null; Affine.prototype.f = null; Affine.prototype.append = function(v) { if ( !(v instanceof Affine) ) { console.log ( "mesh.js: argument to Affine.append is not affine!"); } var a = this.a * v.a + this.c * v.b; var b = this.b * v.a + this.d * v.b; var c = this.a * v.c + this.c * v.d; var d = this.b * v.c + this.d * v.d; var e = this.a * v.e + this.c * v.f + this.e; var f = this.b * v.e + this.d * v.f + this.f; return new Affine( a, b, c, d, e, f ); }; Affine.prototype.toString = function() { return ("affine: " + this.a + " " + this.c + " " + this.e + "\n " + this.b + " " + this.d + " " + this.f); }; // Utility functions --------------------------------- // Browsers return a string rather than a transform list for gradientTransform! function parseTransform(t) { // console.log( "parseTransform: " + t ); var affine = new Affine(); for (var i in t = t.match(/(\w+\(\s*(\-?\d+\.?\d*e?\-?\d*\s*,?\s*)+\))+/g)) { var c = t[i].match(/[\w\.\-]+/g); var type = c.shift(); switch ( type ) { case "translate": var trans; if (c.length == 2) { trans = new Affine( 1, 0, 0, 1, c[0], c[1] ); } else { console.log( "mesh.js: translate does not have 2 arguments!" ); trans = new Affine( 1, 0, 0, 1, 0, 0 ); } console.log( trans.toString() ); affine = affine.append( trans ); break; case "scale": var scale; if (c.length == 1 ) { scale = new Affine( c[0], 0, 0, c[0], 0, 0 ); } else if (c.length == 2) { scale = new Affine( c[0], 0, 0, c[1], 0, 0 ); } else { console.log( "mesh.js: scale does not have 1 or 2 arguments!" ); scale = new Affine( 1, 0, 0, 1, 0, 0 ); } affine = affine.append( scale ); break; case "rotate": if (c.length == 3 ) { var trans = new Affine( 1, 0, 0, 1, c[1], c[2]); affine = affine.append( trans ); } if (c[0]) { var radian = c[0] * Math.PI/180.0; var cos = Math.cos(radian); var sin = Math.sin(radian); if (Math.abs( cos ) < 1e-16) { // I hate rounding errors... cos = 0; } if (Math.abs( sin ) < 1e-16) { // I hate rounding errors... sin = 0; } var rotate = new Affine( cos, sin, -sin, cos, 0, 0 ); affine = affine.append( rotate ); } else { console.log( "math.js: No argument to rotate transform!" ); } if (c.length == 3 ) { var trans = new Affine( 1, 0, 0, 1, -c[1], -c[2]); affine = affine.append( trans ); } break; case "skewX": if (c[0]) { var radian = c[0] * Math.PI/180.0; var tan = Math.tan(radian); var skewx = new Affine( 1, 0, tan, 1, 0, 0 ); affine = affine.append( skewx ); } else { console.log( "math.js: No argument to skewX transform!" ); } break; case "skewY": if (c[0]) { var radian = c[0] * Math.PI/180.0; var tan = Math.tan(radian); var skewy = new Affine( 1, tan, 0, 1, 0, 0 ); affine = affine.append( skewy ); } else { console.log( "math.js: No argument to skewY transform!" ); } break; case "matrix": if (c.length == 6) { var matrix = new Affine( c[0], c[1], c[2], c[3], c[4], c[5] ); affine = affine.append( matrix ); } else { console.log( "math.js: Incorrect number of arguments for matrix!" ); } break; default: console.log( "mesh.js: Unhandled transform type: " + type); break; } } // console.log( " affine:\n" + affine.toString() ); return affine; }; function colorToString(c) { return ("rgb(" + Math.round(c[0]) + "," + Math.round(c[1]) + "," +Math.round(c[2]) + ")"); }; // Split Bezier using de Casteljau's method. function split_bezier(p0, p1, p2, p3) { // console.log( "split_bezier" ); var p00 = p0.clone(); var p13 = p3.clone(); var tmp = p1.add(p2).scale(0.5); var p01 = p0.add(p1).scale(0.5); var p12 = p2.add(p3).scale(0.5); var p02 = p01.add(tmp).scale(0.5); var p11 = tmp.add(p12).scale(0.5); var p03 = p02.add(p11).scale(0.5); var p10 = p03.clone(); return ([[p00, p01, p02, p03], [p10, p11, p12, p13]]); } // See Cairo: cairo-mesh-pattern-rasterizer.c function bezier_steps_sq(points) { var tmp = []; tmp[0] = points[0].dist_sq(points[1]); tmp[1] = points[2].dist_sq(points[3]); tmp[2] = points[0].dist_sq(points[2]) * 0.25; tmp[3] = points[1].dist_sq(points[3]) * 0.25; return Math.max.apply(null,tmp) * 18; }; // Curve class -------------------------------------- function Curve(nodes, colors) { this.nodes = nodes; // 4 Bezier points this.colors = colors; // 2 x 4 colors (two ends x R+G+B+A) }; // Paint a Bezier curve. w is width of Canvas window. Curve.prototype.paint_curve = function(v, w) { // console.log( "Curve.paint_curve" ); // If inside, see if we need to split var max = bezier_steps_sq(this.nodes); if (max > 2.0) { // Larger values leave holes, smaller take longer to render. var beziers = split_bezier(this.nodes[0],this.nodes[1],this.nodes[2],this.nodes[3]); var colors0 = [[],[]]; // ([start][end]) var colors1 = [[],[]]; for (var i = 0; i < 4; ++ i) { colors0[0][i] = this.colors[0][i]; colors0[1][i] = (this.colors[0][i] + this.colors[1][i])/2; colors1[0][i] = (this.colors[0][i] + this.colors[1][i])/2; colors1[1][i] = this.colors[1][i]; } var curve0 = new Curve( beziers[0], colors0 ); var curve1 = new Curve( beziers[1], colors1 ); curve0.paint_curve(v, w); curve1.paint_curve(v, w); } else { counter++; // Directly write data var x = Math.round(this.nodes[0].x); var y = Math.round(this.nodes[0].y); if (x >= 0 && x < w ) { var index = (y * w + x) * 4; v[index ] = Math.round(this.colors[0][0]); v[index + 1] = Math.round(this.colors[0][1]); v[index + 2] = Math.round(this.colors[0][2]); v[index + 3] = Math.round(this.colors[0][3]); // Alpha } // Draw curve, quick and dirty (via canvas context) // v.beginPath(); // v.moveTo( this.nodes[0].x, this.nodes[0].y ); // v.bezierCurveTo( this.nodes[1].x, this.nodes[1].y, // this.nodes[2].x, this.nodes[2].y, // this.nodes[3].x, this.nodes[3].y ); // v.strokeStyle = colorToString( this.colors[0] ); // v.stroke(); } } // Patch class ------------------------------------- function Patch(nodes, colors) { this.nodes = nodes; // 4x4 array of points this.colors = colors; // 2x2x4 colors (four corners x R+G+B+A) }; // Set path for future stroking or filling... useful for debugging. Patch.prototype.setOutline = function(v) { // Draw patch outline v.beginPath(); v.moveTo( this.nodes[0][0].x, this.nodes[0][0].y ); v.bezierCurveTo( this.nodes[0][1].x, this.nodes[0][1].y, this.nodes[0][2].x, this.nodes[0][2].y, this.nodes[0][3].x, this.nodes[0][3].y ); v.bezierCurveTo( this.nodes[1][3].x, this.nodes[1][3].y, this.nodes[2][3].x, this.nodes[2][3].y, this.nodes[3][3].x, this.nodes[3][3].y ); v.bezierCurveTo( this.nodes[3][2].x, this.nodes[3][2].y, this.nodes[3][1].x, this.nodes[3][1].y, this.nodes[3][0].x, this.nodes[3][0].y ); v.bezierCurveTo( this.nodes[2][0].x, this.nodes[2][0].y, this.nodes[1][0].x, this.nodes[1][0].y, this.nodes[0][0].x, this.nodes[0][0].y ); v.closePath(); }; // Draw stroke patch... useful if debugging. Patch.prototype.drawOutline = function(v) { this.setOutline(v); v.strokeStyle = "black"; v.stroke(); }; // Fill patch... useful if debugging. Patch.prototype.fillOutline = function(v) { this.setOutline(v); v.fillStyle = colorToString( this.colors[0] ); v.fill(); }; // Split patch horizontally into two patches. Patch.prototype.split = function() { // console.log( "Patch.split" ); var nodes0 = [[],[],[],[]]; var nodes1 = [[],[],[],[]]; var colors0 = [[[],[]],[[],[]]]; var colors1 = [[[],[]],[[],[]]]; for (var i = 0; i < 4; ++i) { var beziers = split_bezier( this.nodes[0][i], this.nodes[1][i], this.nodes[2][i], this.nodes[3][i] ); for (var j = 0; j < 4; ++j) { nodes0[0][i] = beziers[0][0]; nodes0[1][i] = beziers[0][1]; nodes0[2][i] = beziers[0][2]; nodes0[3][i] = beziers[0][3]; nodes1[0][i] = beziers[1][0]; nodes1[1][i] = beziers[1][1]; nodes1[2][i] = beziers[1][2]; nodes1[3][i] = beziers[1][3]; } } for (var i = 0; i < 4; ++ i) { colors0[0][0][i] = this.colors[0][0][i]; colors0[0][1][i] = this.colors[0][1][i]; colors0[1][0][i] = (this.colors[0][0][i] + this.colors[1][0][i])/2; colors0[1][1][i] = (this.colors[0][1][i] + this.colors[1][1][i])/2; colors1[0][0][i] = (this.colors[0][0][i] + this.colors[1][0][i])/2; colors1[0][1][i] = (this.colors[0][1][i] + this.colors[1][1][i])/2; colors1[1][0][i] = this.colors[1][0][i]; colors1[1][1][i] = this.colors[1][1][i]; } var patch0 = new Patch( nodes0, colors0 ); var patch1 = new Patch( nodes1, colors1 ); return ([patch0, patch1]); }; Patch.prototype.paint = function(v, w) { // console.log( "Patch.paint" ); // console.log( this.nodes ); // Check if patch is inside canvas (need canvas dimensions) // To be done..... // If inside, see if we need to split var tmp = []; for (var i = 0; i < 4; ++i ) { tmp[i] = bezier_steps_sq([this.nodes[0][i],this.nodes[1][i], this.nodes[2][i],this.nodes[3][i]]); } var max = Math.max.apply(null,tmp); // console.log( "Max: " + max ); if (max > 2.0) { // Larger values leave holes, smaller take longer to render. // console.log( "Paint: Splitting" ); var patches = this.split(); // console.log( patches ); patches[0].paint(v, w); patches[1].paint(v, w) } else { // console.log( "Paint: Filling" ); //this.fillOutline(v); this.paint_curve(v, w); } }; Patch.prototype.paint_curve = function(v, w) { // console.log( "Patch.paint_curve" ); // Paint a Bezier curve using just the top of the patch. If // the patch is thin enough this should work. We leave this // function here in case we want to do something more fancy. var curve = new Curve( [this.nodes[0][0],this.nodes[0][1],this.nodes[0][2],this.nodes[0][3]], [this.colors[0][0],this.colors[0][1]]); curve.paint_curve(v, w); } // Mesh class --------------------------------------- function Mesh(id) { // console.log( "Mesh: " + id ); this.id = id; var raw = this.readMesh( id ); this.nodes = raw.nodes; // (m*3+1) x (n*3+1) points this.colors = raw.colors; // (m+1) x (n+1) x 4 colors (R+G+B+A) // console.log( this.nodes ); // console.log( this.colors ); }; // Weighted average to find Bezier points for linear sides. function w_ave(p0, p1) { var p = p0.scale(2.0/3.0).add(p1.scale(1.0/3.0)); return p; } // Function to parse an SVG mesh and return an array of nodes (points) and an array of colors. Mesh.prototype.readMesh = function(id) { var nodes = []; var colors = []; // First, find the mesh var theMesh = document.getElementById(id); if (theMesh == null) { console.log( "mesh.js: Could not find mesh: " + id); } else { // console.log( "Reading mesh: " + id); nodes[0] = []; // Top row colors[0] = []; // Top row var x = Number(theMesh.getAttribute("x")); var y = Number(theMesh.getAttribute("y")); // console.log( " x: " + x + " y: " + y ); nodes[0][0] = new Point(x, y); var rows = theMesh.children for (var i = 0; i < rows.length; ++i ) { // Need to validate if meshrow... nodes[ 3*i+1] = []; // Need three extra rows for each meshrow. nodes[ 3*i+2] = []; nodes[ 3*i+3] = []; colors[i+1] = []; // Need one more row than number of meshrows. // console.log( " row: " + i); var patches = rows[i].children; for (var j = 0; j < patches.length; ++j) { // console.log( " patch: " + j); var stops = patches[j].children; for (var k = 0; k < stops.length; ++k) { var l = k; if (i != 0) { ++l; // There is no top if row isn't first row. } // console.log( " stop: " + k); var path = stops[k].getAttribute("path"); var type = "l"; // We need to still find mid-points even if no path. if (path != null) { var parts = path.match(/\s*([lLcC])\s*(.*)/); type = parts[1]; } var stop_nodes = parse_points( parts[2] ); switch (type) { case "l": if (l == 0) { // Top nodes[3*i][3*j+3] = stop_nodes[0].add(nodes[3*i][3*j]); nodes[3*i][3*j+1] = w_ave( nodes[3*i][3*j], nodes[3*i][3*j+3] ); nodes[3*i][3*j+2] = w_ave( nodes[3*i][3*j+3], nodes[3*i][3*j] ); } else if (l == 1) { // Right nodes[3*i+3][3*j+3] = stop_nodes[0].add(nodes[3*i][3*j+3]); nodes[3*i+1][3*j+3] = w_ave( nodes[3*i][3*j+3], nodes[3*i+3][3*j+3] ); nodes[3*i+2][3*j+3] = w_ave( nodes[3*i+3][3*j+3], nodes[3*i][3*j+3] ); } else if (l == 2) { // Bottom if(j==0) { nodes[3*i+3][3*j+0] = stop_nodes[0].add(nodes[3*i+3][3*j+3]); } nodes[3*i+3][3*j+1] = w_ave( nodes[3*i+3][3*j], nodes[3*i+3][3*j+3] ); nodes[3*i+3][3*j+2] = w_ave( nodes[3*i+3][3*j+3], nodes[3*i+3][3*j] ); } else { // Left nodes[3*i+1][3*j] = w_ave( nodes[3*i][3*j], nodes[3*i+3][3*j] ); nodes[3*i+2][3*j] = w_ave( nodes[3*i+3][3*j], nodes[3*i][3*j] ); } break; case "L": if (l == 0) { // Top nodes[3*i][3*j+3] = stop_nodes[0]; nodes[3*i][3*j+1] = w_ave( nodes[3*i][3*j], nodes[3*i][3*j+3] ); nodes[3*i][3*j+2] = w_ave( nodes[3*i][3*j+3], nodes[3*i][3*j] ); } else if (l == 1) { // Right nodes[3*i+3][3*j+3] = stop_nodes[0]; nodes[3*i+1][3*j+3] = w_ave( nodes[3*i][3*j+3], nodes[3*i+3][3*j+3] ); nodes[3*i+2][3*j+3] = w_ave( nodes[3*i+3][3*j+3], nodes[3*i][3*j+3] ); } else if (l == 2) { // Bottom if(j==0) { nodes[3*i+3][3*j+0] = stop_nodes[0]; } nodes[3*i+3][3*j+1] = w_ave( nodes[3*i+3][3*j], nodes[3*i+3][3*j+3] ); nodes[3*i+3][3*j+2] = w_ave( nodes[3*i+3][3*j+3], nodes[3*i+3][3*j] ); } else { // Left nodes[3*i+1][3*j] = w_ave( nodes[3*i][3*j], nodes[3*i+3][3*j] ); nodes[3*i+2][3*j] = w_ave( nodes[3*i+3][3*j], nodes[3*i][3*j] ); } break; case "c": if (l == 0) { // Top nodes[3*i][3*j+1] = stop_nodes[0].add(nodes[3*i][3*j]); nodes[3*i][3*j+2] = stop_nodes[1].add(nodes[3*i][3*j]); nodes[3*i][3*j+3] = stop_nodes[2].add(nodes[3*i][3*j]); } else if (l == 1) { // Right nodes[3*i+1][3*j+3] = stop_nodes[0].add(nodes[3*i][3*j+3]); nodes[3*i+2][3*j+3] = stop_nodes[1].add(nodes[3*i][3*j+3]); nodes[3*i+3][3*j+3] = stop_nodes[2].add(nodes[3*i][3*j+3]); } else if (l == 2) { // Bottom nodes[3*i+3][3*j+2] = stop_nodes[0].add(nodes[3*i+3][3*j+3]); nodes[3*i+3][3*j+1] = stop_nodes[1].add(nodes[3*i+3][3*j+3]); if(j==0) { nodes[3*i+3][3*j+0] = stop_nodes[2].add(nodes[3*i+3][3*j+3]); } } else { // Left nodes[3*i+2][3*j] = stop_nodes[0].add(nodes[3*i+3][3*j]); nodes[3*i+1][3*j] = stop_nodes[1].add(nodes[3*i+3][3*j]); } break; case "C": if (l == 0) { // Top nodes[3*i][3*j+1] = stop_nodes[0]; nodes[3*i][3*j+2] = stop_nodes[1]; nodes[3*i][3*j+3] = stop_nodes[2]; } else if (l == 1) { // Right nodes[3*i+1][3*j+3] = stop_nodes[0]; nodes[3*i+2][3*j+3] = stop_nodes[1]; nodes[3*i+3][3*j+3] = stop_nodes[2]; } else if (l == 2) { // Bottom nodes[3*i+3][3*j+2] = stop_nodes[0]; nodes[3*i+3][3*j+1] = stop_nodes[1]; if(j==0) { nodes[3*i+3][3*j+0] = stop_nodes[2]; } } else { // Left nodes[3*i+2][3*j] = stop_nodes[0]; nodes[3*i+1][3*j] = stop_nodes[1]; } break default: console.log("mesh.js: " + type + " invalid path type."); } if ( (i == 0 && j == 0) || k > 0 ) { var color_raw = getComputedStyle(stops[k]).stopColor.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i); var alpha_raw = getComputedStyle(stops[k]).stopOpacity; // console.log( " color_raw: " + color_raw + " alpha_raw: " + alpha_raw); var alpha = 255; if (alpha_raw) { alpha = parseInt(alpha_raw * 255); } // console.log( " alpha: " + alpha ); if (color_raw) { if (l == 0) { // upper left corner colors[i][j] = []; colors[i][j][0] = parseInt(color_raw[1]); colors[i][j][1] = parseInt(color_raw[2]); colors[i][j][2] = parseInt(color_raw[3]); colors[i][j][3] = alpha; // Alpha } else if (l == 1) { // upper right corner colors[i][j+1] = []; colors[i][j+1][0] = parseInt(color_raw[1]); colors[i][j+1][1] = parseInt(color_raw[2]); colors[i][j+1][2] = parseInt(color_raw[3]); colors[i][j+1][3] = alpha; // Alpha } else if (l == 2) { // lower right corner colors[i+1][j+1] = []; colors[i+1][j+1][0] = parseInt(color_raw[1]); colors[i+1][j+1][1] = parseInt(color_raw[2]); colors[i+1][j+1][2] = parseInt(color_raw[3]); colors[i+1][j+1][3] = alpha; // Alpha } else if (l == 3) { // lower left corner colors[i+1][j] = []; colors[i+1][j][0] = parseInt(color_raw[1]); colors[i+1][j][1] = parseInt(color_raw[2]); colors[i+1][j][2] = parseInt(color_raw[3]); colors[i+1][j][3] = alpha; // Alpha } } } } // SVG doesn't use tensor points but we need them for rendering. nodes[3*i+1][3*j+1] = new Point; nodes[3*i+1][3*j+2] = new Point; nodes[3*i+2][3*j+1] = new Point; nodes[3*i+2][3*j+2] = new Point; nodes[3*i+1][3*j+1].x = ( -4.0 * nodes[3*i ][3*j ].x + 6.0 * ( nodes[3*i ][3*j+1].x + nodes[3*i+1][3*j ].x ) + -2.0 * ( nodes[3*i ][3*j+3].x + nodes[3*i+3][3*j ].x ) + 3.0 * ( nodes[3*i+3][3*j+1].x + nodes[3*i+1][3*j+3].x ) + -1.0 * nodes[3*i+3][3*j+3].x ) / 9.0; nodes[3*i+1][3*j+2].x = ( -4.0 * nodes[3*i ][3*j+3].x + 6.0 * ( nodes[3*i ][3*j+2].x + nodes[3*i+1][3*j+3].x ) + -2.0 * ( nodes[3*i ][3*j ].x + nodes[3*i+3][3*j+3].x ) + 3.0 * ( nodes[3*i+3][3*j+2].x + nodes[3*i+1][3*j ].x ) + -1.0 * nodes[3*i+3][3*j ].x ) / 9.0; nodes[3*i+2][3*j+1].x = ( -4.0 * nodes[3*i+3][3*j ].x + 6.0 * ( nodes[3*i+3][3*j+1].x + nodes[3*i+2][3*j ].x ) + -2.0 * ( nodes[3*i+3][3*j+3].x + nodes[3*i ][3*j ].x ) + 3.0 * ( nodes[3*i ][3*j+1].x + nodes[3*i+2][3*j+3].x ) + -1.0 * nodes[3*i ][3*j+3].x ) / 9.0; nodes[3*i+2][3*j+2].x = ( -4.0 * nodes[3*i+3][3*j+3].x + 6.0 * ( nodes[3*i+3][3*j+2].x + nodes[3*i+2][3*j+3].x ) + -2.0 * ( nodes[3*i+3][3*j ].x + nodes[3*i ][3*j+3].x ) + 3.0 * ( nodes[3*i ][3*j+2].x + nodes[3*i+2][3*j ].x ) + -1.0 * nodes[3*i ][3*j ].x ) / 9.0; nodes[3*i+1][3*j+1].y = ( -4.0 * nodes[3*i ][3*j ].y + 6.0 * ( nodes[3*i ][3*j+1].y + nodes[3*i+1][3*j ].y ) + -2.0 * ( nodes[3*i ][3*j+3].y + nodes[3*i+3][3*j ].y ) + 3.0 * ( nodes[3*i+3][3*j+1].y + nodes[3*i+1][3*j+3].y ) + -1.0 * nodes[3*i+3][3*j+3].y ) / 9.0; nodes[3*i+1][3*j+2].y = ( -4.0 * nodes[3*i ][3*j+3].y + 6.0 * ( nodes[3*i ][3*j+2].y + nodes[3*i+1][3*j+3].y ) + -2.0 * ( nodes[3*i ][3*j ].y + nodes[3*i+3][3*j+3].y ) + 3.0 * ( nodes[3*i+3][3*j+2].y + nodes[3*i+1][3*j ].y ) + -1.0 * nodes[3*i+3][3*j ].y ) / 9.0; nodes[3*i+2][3*j+1].y = ( -4.0 * nodes[3*i+3][3*j ].y + 6.0 * ( nodes[3*i+3][3*j+1].y + nodes[3*i+2][3*j ].y ) + -2.0 * ( nodes[3*i+3][3*j+3].y + nodes[3*i ][3*j ].y ) + 3.0 * ( nodes[3*i ][3*j+1].y + nodes[3*i+2][3*j+3].y ) + -1.0 * nodes[3*i ][3*j+3].y ) / 9.0; nodes[3*i+2][3*j+2].y = ( -4.0 * nodes[3*i+3][3*j+3].y + 6.0 * ( nodes[3*i+3][3*j+2].y + nodes[3*i+2][3*j+3].y ) + -2.0 * ( nodes[3*i+3][3*j ].y + nodes[3*i ][3*j+3].y ) + 3.0 * ( nodes[3*i ][3*j+2].y + nodes[3*i+2][3*j ].y ) + -1.0 * nodes[3*i ][3*j ].y ) / 9.0; } } // console.log( nodes ); } return { nodes: nodes, colors: colors }; } // Extracts out each patch and then paints it Mesh.prototype.paint = function(v, w) { for (var i = 0; i < (this.nodes.length-1)/3; ++i) { for (var j = 0; j < (this.nodes[0].length-1)/3; ++j) { var slice_nodes = []; for ( var k = i*3; k < (i*3)+4; ++k ) { slice_nodes.push(this.nodes[k].slice(j*3,(j*3)+4)); } var slice_colors = []; slice_colors.push(this.colors[i ].slice(j,j+2)); slice_colors.push(this.colors[i+1].slice(j,j+2)); var patch = new Patch(slice_nodes, slice_colors); patch.paint(v, w); } } }; // Transforms mesh into coordinate space of canvas (t is either Point or Affine). Mesh.prototype.transform = function(t) { // console.log( "t: " + t ); if (t instanceof Point) { for (var i = 0; i < this.nodes.length; ++i) { for (var j = 0; j < this.nodes[0].length; ++j) { this.nodes[i][j] = this.nodes[i][j].add(t); } } } if (t instanceof Affine) { for (var i = 0; i < this.nodes.length; ++i) { for (var j = 0; j < this.nodes[0].length; ++j) { this.nodes[i][j] = this.nodes[i][j].transform(t); } } } }; // Scale mesh into coordinate space of canvas (t is a Point). Mesh.prototype.scale = function(t) { for (var i = 0; i < this.nodes.length; ++i) { for (var j = 0; j < this.nodes[0].length; ++j) { this.nodes[i][j] = this.nodes[i][j].scale(t); } } }; function parse_points(s) { var points = []; var values = s.split(/[ ,]+/); for (var i = 0; i < values.length-1; i += 2) { points.push( new Point( parseFloat( values[i]), parseFloat( values[i+1] ))); } return points; } // Start of document processing --------------------- var shapes = document.querySelectorAll('rect,circle,ellipse,path,text'); // console.log("Shapes: " + shapes.length); for (var i = 0; i < shapes.length; ++i) { var shape = shapes[i]; // console.log( shape.nodeName ); // Get id. If no id, create one. var shape_id = shape.getAttribute("id"); if (!shape_id) { shape_id = "patchjs_shape" + i; shape.setAttribute("id", shape_id); } // console.log( "id: " + shape_id ); var fill = shape.style.fill; var url_value = fill.match(/^url\(\s*\"?\s*#([^\s\"]+)\"?\s*\)/); if (url_value && url_value[1]) { // console.log( "Got url! " + url_value[1]); var mesh = document.getElementById(url_value[1]); // console.log( mesh ); // console.log( mesh.nodeName ); if (mesh.nodeName === "meshgradient" ) { // console.log( "Got mesh" ); var bbox = shape.getBBox(); // console.log( bbox ); // Create temporary canvas var my_canvas = document.createElementNS( xhtmlNS, "canvas" ); //var my_canvas = document.createElement( "canvas" ); // Both work for HTML... my_canvas.width = bbox.width; my_canvas.height = bbox.height; // console.log ( "Canvas: " + my_canvas.width + "x" + my_canvas.height ); var my_context = my_canvas.getContext("2d"); var my_canvas_image = my_context.getImageData( 0, 0, my_canvas.width, my_canvas.height); var my_data = my_canvas_image.data; // Draw a mesh var my_mesh = new Mesh( url_value[1] ); // Adjust for bounding box if necessary. if (mesh.getAttribute( "gradientUnits" ) === "objectBoundingBox") { my_mesh.scale( new Point( bbox.width, bbox.height ) ); } // Apply gradient transform. var gradientTransform = mesh.getAttribute("gradientTransform"); // console.log( typeof gradientTransform ); if ( gradientTransform != null ) { var affine = parseTransform( gradientTransform ); my_mesh.transform( affine ); } // Position to Canvas coordinate. var t = new Point( -bbox.x, -bbox.y ); if (mesh.getAttribute( "gradientUnits" ) === "userSpaceOnUse") { my_mesh.transform(t); } // Paint my_mesh.paint(my_data, my_canvas.width); my_context.putImageData(my_canvas_image, 0, 0); // Create image element of correct size var my_image = document.createElementNS( svgNS, "image" ); my_image.setAttribute("width", my_canvas.width); my_image.setAttribute("height",my_canvas.height); my_image.setAttribute("x", bbox.x); my_image.setAttribute("y", bbox.y); // Set image to data url var my_png = my_canvas.toDataURL(); my_image.setAttributeNS(xlinkNS, "xlink:href", my_png); // Insert image into document shape.parentNode.insertBefore( my_image, shape ); shape.style.fill = "none"; // Create clip referencing shape and insert into document var clip = document.createElementNS( svgNS, "clipPath"); var clip_id = "patchjs_clip" + i; clip.setAttribute("id", clip_id); var use = document.createElementNS( svgNS, "use"); use.setAttributeNS( xlinkNS, "xlink:href", "#" + shape_id); clip.appendChild(use); shape.parentElement.insertBefore(clip, shape); my_image.setAttribute("clip-path", "url(#" + clip_id + ")"); } } } })();