Flat, round, designer-friendly pseudo-3D engine for canvas & SVG
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Adding support for linear and radial radients

+135 -47
demos/textures/coin.png

This is a binary file and will not be displayed.

+2 -2
demos/textures/index.html
··· 40 40 </div> 41 41 <div class="row"> 42 42 <div class="container"> 43 - <h3>Coin</h3> 43 + <h3>Gradient</h3> 44 44 <canvas class="illo" id="illo_coin" width="300" height="300"></canvas> 45 45 </div> 46 46 ··· 60 60 </div> 61 61 <div class="row"> 62 62 <div class="container"> 63 - <h3>Coin</h3> 63 + <h3>Gradient</h3> 64 64 <svg class="illo" id="illo_coin_svg" width="300" height="300"></svg> 65 65 </div> 66 66
+28 -20
demos/textures/textures.js
··· 26 26 return illo; 27 27 } 28 28 29 - function loadForBothRenderer(img, callback) { 30 - callback(img, ""); 31 - callback(img, "_svg"); 29 + function loadForBothRenderer(val, callback) { 30 + callback("", val); 31 + callback("_svg", val); 32 32 } 33 33 34 34 // Coin 35 - loadImage("coin.png").then(img => loadForBothRenderer(img, (img, suffix) => { 35 + loadForBothRenderer(0, suffix => { 36 36 new Zdog.Cylinder({ 37 37 addTo: createIllustration("#illo_coin" + suffix), 38 38 diameter: 200, 39 39 length: 10, 40 - color: "#8D5C1E", 41 - backface: new Zdog.Texture(img, {dst: [-100, -100, 200, 200], src:[390, 10, -380, 380]}), 42 - frontFace: new Zdog.Texture(img, {dst: [-100, -100, 200, 200], src:[790, 10, -380, 380]}), 40 + color: "#000", 41 + backface: new Zdog.Texture({ 42 + linearGrad:[0, 0, 100, 0], 43 + colorStops:[0, '#833ab4', .5, '#fd1d1d', 1, '#fcb045'], 44 + dst: [-100, -100, 200, 200], 45 + src:[0, 0, 100, 100]}), 46 + frontFace: new Zdog.Texture({ 47 + radialGrad:[50, 50, 0, 50, 50, 50], 48 + colorStops:[0, '#3f5efb', .25, '#b471a3', .5, '#adb753', .75, '#5fd0d8', 1, '#fc466b'], 49 + dst: [-100, -100, 200, 200], 50 + src:[0, 0, 100, 100]}), 43 51 stroke: false, 44 52 }); 45 - })); 53 + }); 46 54 47 - loadImage("dice.png").then(img => loadForBothRenderer(img, (img, suffix) => { 55 + loadImage("dice.png").then(img => loadForBothRenderer(img, (suffix, img) => { 48 56 new Zdog.Box({ 49 57 addTo: createIllustration("#illo_box" + suffix), 50 58 width: 120, ··· 52 60 depth: 120, 53 61 stroke: false, 54 62 color: '#F00', 55 - frontFace: new Zdog.Texture(img, {src:[0, 0, 200, 200], dst: [-60, -60, 120, 120]}), 56 - rearFace: new Zdog.Texture(img, {src:[0, 200, 200, 200], dst: [-60, -60, 120, 120]}), 57 - leftFace: new Zdog.Texture(img, {src:[200, 0, 200, 200], dst: [-60, -60, 120, 120]}), 58 - rightFace: new Zdog.Texture(img, {src:[200, 200, 200, 200], dst: [-60, -60, 120, 120]}), 59 - topFace: new Zdog.Texture(img, {src:[400, 0, 200, 200], dst: [-60, -60, 120, 120]}), 60 - bottomFace: new Zdog.Texture(img, {src:[400, 200, 200, 200], dst: [-60, -60, 120, 120]}), 63 + frontFace: new Zdog.Texture({img: img, src:[0, 0, 200, 200], dst: [-60, -60, 120, 120]}), 64 + rearFace: new Zdog.Texture({img: img, src:[0, 200, 200, 200], dst: [-60, -60, 120, 120]}), 65 + leftFace: new Zdog.Texture({img: img, src:[200, 0, 200, 200], dst: [-60, -60, 120, 120]}), 66 + rightFace: new Zdog.Texture({img: img, src:[200, 200, 200, 200], dst: [-60, -60, 120, 120]}), 67 + topFace: new Zdog.Texture({img: img, src:[400, 0, 200, 200], dst: [-60, -60, 120, 120]}), 68 + bottomFace: new Zdog.Texture({img: img, src:[400, 200, 200, 200], dst: [-60, -60, 120, 120]}), 61 69 }); 62 70 })); 63 71 64 72 65 73 // Tetrahedron 66 - loadImage("tetrahedron.png").then(img => loadForBothRenderer(img, (img, suffix) => { 74 + loadImage("tetrahedron.png").then(img => loadForBothRenderer(img, (suffix, img) => { 67 75 var tetrahedron = new Zdog.Anchor({ 68 76 addTo: createIllustration("#illo_tetrahedron" + suffix), 69 77 translate: { x: 0, y: 0 }, ··· 84 92 path: [p1, p2, p3, p1], 85 93 addTo: tetrahedron, 86 94 stroke: 1, 87 - color: new Zdog.Texture(img, {src:[{x:10, y: 210}, {x:210, y: 210}, {x:110, y: 36.8}], dst: [p1, p2, p3]}), 95 + color: new Zdog.Texture({img: img, src:[{x:10, y: 210}, {x:210, y: 210}, {x:110, y: 36.8}], dst: [p1, p2, p3]}), 88 96 fill: true, 89 97 }); 90 98 new Zdog.Shape({ 91 99 path: [p1, p2, p4, p1], 92 100 addTo: tetrahedron, 93 101 stroke: 1, 94 - color: new Zdog.Texture(img, {src:[{x:210, y: 210}, {x:410, y: 210}, {x:310, y: 36.8}], dst: [p1, p2, p4]}), 102 + color: new Zdog.Texture({img: img, src:[{x:210, y: 210}, {x:410, y: 210}, {x:310, y: 36.8}], dst: [p1, p2, p4]}), 95 103 fill: true, 96 104 }); 97 105 new Zdog.Shape({ 98 106 path: [p1, p4, p3, p1], 99 107 addTo: tetrahedron, 100 108 stroke: 1, 101 - color: new Zdog.Texture(img, {src:[{x:410, y: 210}, {x:610, y: 210}, {x:510, y: 36.8}], dst: [p1, p4, p3]}), 109 + color: new Zdog.Texture({img: img, src:[{x:410, y: 210}, {x:610, y: 210}, {x:510, y: 36.8}], dst: [p1, p4, p3]}), 102 110 fill: true, 103 111 }); 104 112 new Zdog.Shape({ 105 113 path: [p4, p2, p3, p4], 106 114 addTo: tetrahedron, 107 115 stroke: 1, 108 - color: new Zdog.Texture(img, {src:[{x:610, y: 210}, {x:810, y: 210}, {x:710, y: 36.8}], dst: [p4, p2, p3]}), 116 + color: new Zdog.Texture({img: img, src:[{x:610, y: 210}, {x:810, y: 210}, {x:710, y: 36.8}], dst: [p4, p2, p3]}), 109 117 fill: true, 110 118 }); 111 119 }));
+105 -25
js/texture.js
··· 28 28 return tp.map(x => x / det); 29 29 } 30 30 31 - /** 32 - * Possible values of a point map: 33 - * [x, y, width, height] => We use 3 points top-left, top-right, bottom-left 34 - * [x, y] => image size is used for width and height with the above rule 35 - * [vector, vector, vector] => provided points are used 36 - */ 37 - function parsePointMap(img, map) { 31 + function parsePointMap(size, map) { 38 32 if (!Array.isArray(map) || !map.length) { 39 - map = [0, 0, img.width, img.height]; 33 + map = [0, 0, size[0], size[1]]; 40 34 } 41 35 if (typeof(map[0]) == "number") { 42 36 if (map.length < 4) { 43 37 let tmp = map; 44 - map = [0, 0, img.width, img.height]; 38 + map = [0, 0, size[0], size[1]]; 45 39 for (let i = 0; i < tmp.length; i++) { 46 40 map[i] = tmp[i]; 47 41 } ··· 57 51 } 58 52 59 53 var idCounter = 0; 60 - function Texture(img, options ) { 54 + 55 + const optionKeys = [ 56 + 'img', 57 + 'linearGrad', 58 + 'radialGrad', 59 + 'colorStops', 60 + 'src', 61 + 'dst' 62 + ] 63 + 64 + /** 65 + * Creates a tecture map. Possible options: 66 + * img: Image object to be used as texture 67 + * linearGrad: [x1, y1, x2, y2] Array defining the linear gradient 68 + * radialGrad: [x0, y0, r0, x1, y1, r1] Array defining the radial gradient 69 + * colorStops: [offset1, color1, offset2, color2...] Array defining the color 70 + * stops for the gradient, offset must be in range [0, 1] 71 + * 72 + * src: <surface definition> Represents the surface for the texture. Above 73 + * gradient definition should be represented in this coordinate space 74 + * dst: <surface definition> Represents the surface of the object. This allows 75 + * keeping the texture definition independent of the surface definition 76 + * 77 + * <surface definition> Can be represented in one of the following ways: 78 + * [x, y, width, height] => We use 3 points top-left, top-right, bottom-left 79 + * [x, y] => image/gradient size is used for width and height with the above rule 80 + * [vector, vector, vector] => provided points are used 81 + */ 82 + function Texture(options) { 61 83 this.id = idCounter++; 62 84 this.isTexture = true; 63 - this.img = img; 64 85 65 86 options = options || { } 66 - this.src = parsePointMap(img, options.src); 67 - this.dst = parsePointMap(img, options.dst); 87 + for (var key in options ) { 88 + if (optionKeys.indexOf( key ) != -1 ) { 89 + this[key] = options[key]; 90 + } 91 + } 92 + 93 + var size; 94 + if (options.img) { 95 + size = [options.img.width, options.img.height]; 96 + } else if (options.linearGrad) { 97 + size = [Math.abs(options.linearGrad[2] - options.linearGrad[0]), Math.abs(options.linearGrad[3] - options.linearGrad[1])]; 98 + } else if (options.radialGrad) { 99 + size = [Math.abs(options.radialGrad[3] - options.radialGrad[0]), Math.abs(options.radialGrad[4] - options.radialGrad[1])]; 100 + } else { 101 + throw "One of [img, linearGrad, radialGrad] is required"; 102 + } 103 + if (size[0] == 0) size[0] = size[1]; 104 + if (size[1] == 0) size[1] = size[0]; 105 + 106 + this.src = parsePointMap(size, options.src); 107 + this.dst = parsePointMap(size, options.dst); 68 108 69 109 this.srcInverse = inverse( 70 110 this.src[0].x, this.src[0].y, ··· 90 130 91 131 Texture.prototype.getCanvasFill = function(ctx) { 92 132 if (!this.pattern) { 93 - this.pattern = ctx.createPattern(this.img, "repeat"); 133 + if (this.img) { 134 + this.pattern = ctx.createPattern(this.img, "repeat"); 135 + } else { 136 + this.pattern = this.linearGrad 137 + ? ctx.createLinearGradient(...this.linearGrad) 138 + : ctx.createRadialGradient(...this.radialGrad); 139 + if (this.colorStops) { 140 + for (var i = 0; i < this.colorStops.length; i+=2) { 141 + this.pattern.addColorStop(this.colorStops[i], this.colorStops[i+1]); 142 + } 143 + } 144 + } 94 145 } 95 146 // pattern.setTransform is not supported in IE, 96 147 // so transform the context instead ··· 101 152 const svgURI = 'http://www.w3.org/2000/svg'; 102 153 Texture.prototype.getSvgFill = function(svg) { 103 154 if (!this.svgPattern) { 104 - this.svgPattern = document.createElementNS( svgURI, 'pattern'); 105 - this.svgPattern.setAttribute("id", "texture_" + this.id); 106 - this.svgPattern.setAttribute("width", this.img.width); 107 - this.svgPattern.setAttribute("height", this.img.height); 108 - this.svgPattern.setAttribute("patternUnits", "userSpaceOnUse"); 155 + if (this.img) { 156 + this.svgPattern = document.createElementNS( svgURI, 'pattern'); 157 + this.svgPattern.setAttribute("width", this.img.width); 158 + this.svgPattern.setAttribute("height", this.img.height); 159 + this.svgPattern.setAttribute("patternUnits", "userSpaceOnUse"); 160 + this.attrTransform = "patternTransform"; 161 + 162 + let img = document.createElementNS( svgURI, 'image'); 163 + img.setAttribute("href", this.img.src); 164 + this.svgPattern.appendChild(img); 165 + } else { 166 + var type, vals, keys; 167 + if (this.linearGrad) { 168 + type = "linearGradient"; 169 + vals = this.linearGrad; 170 + keys = ["x1", "y1", "x2", "y2"] 171 + } else { 172 + type = "radialGradient"; 173 + vals = this.radialGrad; 174 + keys = ["fx", "fy", "fr", "cx", "cy", "r"] 175 + } 176 + this.svgPattern = document.createElementNS( svgURI, type); 177 + for (var i = 0; i < keys.length; i++) { 178 + this.svgPattern.setAttribute(keys[i], vals[i]); 179 + } 109 180 110 - let img = document.createElementNS( svgURI, 'image'); 111 - img.setAttribute("href", this.img.src); 112 - this.svgPattern.appendChild(img); 181 + if (this.colorStops) { 182 + for (var i = 0; i < this.colorStops.length; i+=2) { 183 + let colorStop = document.createElementNS(svgURI, 'stop' ); 184 + colorStop.setAttribute("offset", this.colorStops[i]); 185 + colorStop.setAttribute("style", "stop-color:" + this.colorStops[i+1]); 186 + this.svgPattern.appendChild(colorStop); 187 + } 188 + } 189 + this.svgPattern.setAttribute("gradientUnits", "userSpaceOnUse"); 190 + this.attrTransform = "gradientTransform"; 191 + } 192 + this.svgPattern.setAttribute("id", "texture_" + this.id); 113 193 114 194 this.defs = document.createElementNS(svgURI, 'defs' ); 115 195 this.defs.appendChild(this.svgPattern); 196 + } 116 197 117 - } 118 - this.svgPattern.setAttribute("patternTransform", `matrix(${this.getMatrix().join(' ')})`); 198 + this.svgPattern.setAttribute(this.attrTransform, `matrix(${this.getMatrix().join(' ')})`); 119 199 svg.appendChild( this.defs ); 120 200 return `url(#texture_${this.id})`; 121 201 } ··· 134 214 }; 135 215 136 216 Texture.prototype.clone = function() { 137 - return new Texture(this.img, this); 217 + return new Texture(this); 138 218 }; 139 219 140 220 return Texture;