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 image textures on flat surface

+408 -11
demos/textures/coin.png

This is a binary file and will not be displayed.

demos/textures/dice.png

This is a binary file and will not be displayed.

+102
demos/textures/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width" /> 6 + 7 + <title>Textures</title> 8 + 9 + <style> 10 + html { height: 100%; } 11 + 12 + body { 13 + min-height: 100%; 14 + margin: 0; 15 + } 16 + 17 + .illo { 18 + display: block; 19 + margin: 20px auto; 20 + background: #FDB; 21 + cursor: move; 22 + } 23 + .container { 24 + margin: 0 5px; 25 + } 26 + .row { 27 + display: flex; 28 + align-items: center; 29 + justify-content: center; 30 + font-family: sans-serif; 31 + text-align: center; 32 + } 33 + </style> 34 + 35 + </head> 36 + <body> 37 + 38 + <div class="row"> 39 + <h1>Canvas</h1> 40 + </div> 41 + <div class="row"> 42 + <div class="container"> 43 + <h3>Coin</h3> 44 + <canvas class="illo" id="illo_coin" width="300" height="300"></canvas> 45 + </div> 46 + 47 + <div class="container"> 48 + <h3>Box</h3> 49 + <canvas class="illo" id="illo_box" width="300" height="300"></canvas> 50 + </div> 51 + 52 + <div class="container"> 53 + <h3>Tetrahedron</h3> 54 + <canvas class="illo" id="illo_tetrahedron" width="300" height="300"></canvas> 55 + </div> 56 + </div> 57 + 58 + <div class="row"> 59 + <h1>SVG</h1> 60 + </div> 61 + <div class="row"> 62 + <div class="container"> 63 + <h3>Coin</h3> 64 + <svg class="illo" id="illo_coin_svg" width="300" height="300"></svg> 65 + </div> 66 + 67 + <div class="container"> 68 + <h3>Box</h3> 69 + <svg class="illo" id="illo_box_svg" width="300" height="300"></svg> 70 + </div> 71 + 72 + <div class="container"> 73 + <h3>Tetrahedron</h3> 74 + <svg class="illo" id="illo_tetrahedron_svg" width="300" height="300"></svg> 75 + </div> 76 + </div> 77 + 78 + <script src="../../js/boilerplate.js"></script> 79 + <script src="../../js/canvas-renderer.js"></script> 80 + <script src="../../js/svg-renderer.js"></script> 81 + <script src="../../js/vector.js"></script> 82 + <script src="../../js/anchor.js"></script> 83 + <script src="../../js/path-command.js"></script> 84 + <script src="../../js/shape.js"></script> 85 + <script src="../../js/ellipse.js"></script> 86 + <script src="../../js/rect.js"></script> 87 + <script src="../../js/rounded-rect.js"></script> 88 + <script src="../../js/polygon.js"></script> 89 + <script src="../../js/group.js"></script> 90 + <script src="../../js/hemisphere.js"></script> 91 + <script src="../../js/cylinder.js"></script> 92 + <script src="../../js/cone.js"></script> 93 + <script src="../../js/box.js"></script> 94 + <script src="../../js/dragger.js"></script> 95 + <script src="../../js/illustration.js"></script> 96 + 97 + <script src="../../js/texture.js"></script> 98 + 99 + <script src="textures.js"></script> 100 + 101 + </body> 102 + </html>
demos/textures/tetrahedron.png

This is a binary file and will not be displayed.

+111
demos/textures/textures.js
··· 1 + function loadImage(url) { 2 + return new Promise(r => { let i = new Image(); i.onload = (() => r(i)); i.src = url; }); 3 + } 4 + 5 + function createIllustration(selector) { 6 + // ----- variables ----- // 7 + var isSpinning = true; 8 + 9 + // ----- model ----- // 10 + var illo = new Zdog.Illustration({ 11 + element: selector, 12 + dragRotate: true, 13 + zoom: 1, 14 + onDragStart: function() { 15 + isSpinning = false; 16 + }, 17 + }); 18 + 19 + // ----- animate ----- // 20 + function animate() { 21 + illo.rotate.y += isSpinning ? 0.03 : 0; 22 + illo.updateRenderGraph(); 23 + requestAnimationFrame( animate ); 24 + } 25 + animate(); 26 + return illo; 27 + } 28 + 29 + function loadForBothRenderer(img, callback) { 30 + callback(img, ""); 31 + callback(img, "_svg"); 32 + } 33 + 34 + // Coin 35 + loadImage("coin.png").then(img => loadForBothRenderer(img, (img, suffix) => { 36 + new Zdog.Cylinder({ 37 + addTo: createIllustration("#illo_coin" + suffix), 38 + diameter: 200, 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]}), 43 + stroke: false, 44 + }); 45 + })); 46 + 47 + loadImage("dice.png").then(img => loadForBothRenderer(img, (img, suffix) => { 48 + new Zdog.Box({ 49 + addTo: createIllustration("#illo_box" + suffix), 50 + width: 120, 51 + height: 120, 52 + depth: 120, 53 + stroke: false, 54 + 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]}), 61 + }); 62 + })); 63 + 64 + 65 + // Tetrahedron 66 + loadImage("tetrahedron.png").then(img => loadForBothRenderer(img, (img, suffix) => { 67 + var tetrahedron = new Zdog.Anchor({ 68 + addTo: createIllustration("#illo_tetrahedron" + suffix), 69 + translate: { x: 0, y: 0 }, 70 + }); 71 + 72 + var radius = 80; 73 + var deg_120 = Zdog.TAU / 3; 74 + 75 + var depthFactor = radius * Math.sqrt(6) * 3 / 2; 76 + var r = -depthFactor / 12; 77 + 78 + var p1 = {x: 0, y : radius, z: r}; 79 + var p2 = {x: radius * Math.sin(deg_120), y : radius * Math.cos(deg_120), z: r}; 80 + var p3 = {x: radius * Math.sin(2 * deg_120), y : radius * Math.cos(2 * deg_120), z: r} 81 + var p4 = {x: 0, y: 0, z: depthFactor / 4}; 82 + 83 + new Zdog.Shape({ 84 + path: [p1, p2, p3, p1], 85 + addTo: tetrahedron, 86 + 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]}), 88 + fill: true, 89 + }); 90 + new Zdog.Shape({ 91 + path: [p1, p2, p4, p1], 92 + addTo: tetrahedron, 93 + 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]}), 95 + fill: true, 96 + }); 97 + new Zdog.Shape({ 98 + path: [p1, p4, p3, p1], 99 + addTo: tetrahedron, 100 + 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]}), 102 + fill: true, 103 + }); 104 + new Zdog.Shape({ 105 + path: [p4, p2, p3, p4], 106 + addTo: tetrahedron, 107 + 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]}), 109 + fill: true, 110 + }); 111 + }));
+7
js/boilerplate.js
··· 70 70 return isFirstHalf ? curve : 1 - curve; 71 71 }; 72 72 73 + Zdog.isColor = function(value) { 74 + return (typeof value == 'string') || (value && value.isTexture); 75 + } 76 + Zdog.cloneColor = function(value) { 77 + return (value && value.clone) ? value.clone() : value; 78 + } 79 + 73 80 return Zdog; 74 81 75 82 } ) );
+5 -1
js/box.js
··· 87 87 } 88 88 // update & add face 89 89 var options = this.getFaceOptions( faceName ); 90 - options.color = typeof value == 'string' ? value : this.color; 90 + if (utils.isColor(value)) { 91 + options.color = value; 92 + } else { 93 + options.color = utils.cloneColor(this.color); 94 + } 91 95 92 96 if ( rect ) { 93 97 // update previous
+18 -4
js/canvas-renderer.js
··· 51 51 if ( !isStroke ) { 52 52 return; 53 53 } 54 - ctx.strokeStyle = color; 55 54 ctx.lineWidth = lineWidth; 56 - ctx.stroke(); 55 + if (color && color.getCanvasFill) { 56 + ctx.save(); 57 + ctx.strokeStyle = color.getCanvasFill(ctx); 58 + ctx.stroke(); 59 + ctx.restore(); 60 + } else { 61 + ctx.strokeStyle = color; 62 + ctx.stroke(); 63 + } 57 64 }; 58 65 59 66 CanvasRenderer.fill = function( ctx, elem, isFill, color ) { 60 67 if ( !isFill ) { 61 68 return; 62 69 } 63 - ctx.fillStyle = color; 64 - ctx.fill(); 70 + if (color && color.getCanvasFill) { 71 + ctx.save(); 72 + ctx.fillStyle = color.getCanvasFill(ctx); 73 + ctx.fill(); 74 + ctx.restore(); 75 + } else { 76 + ctx.fillStyle = color; 77 + ctx.fill(); 78 + } 65 79 }; 66 80 67 81 CanvasRenderer.end = function() {};
+2 -2
js/cylinder.js
··· 120 120 color: this.color, 121 121 stroke: this.stroke, 122 122 fill: this.fill, 123 - backface: this.frontFace || baseColor, 123 + backface: utils.cloneColor(this.frontFace || baseColor), 124 124 visible: this.visible, 125 125 }); 126 126 // back outside base 127 127 this.rearBase = this.group.rearBase = this.frontBase.copy({ 128 128 translate: { z: -baseZ }, 129 129 rotate: { y: 0 }, 130 - backface: baseColor, 130 + backface: utils.cloneColor(baseColor), 131 131 }); 132 132 }; 133 133
+16 -4
js/shape.js
··· 91 91 this.pathCommands.forEach( function( command ) { 92 92 command.reset(); 93 93 } ); 94 + 95 + if (this.backface && this.backface.reset) { 96 + this.backface.reset(); 97 + } 98 + if (this.color && this.color.reset) { 99 + this.color.reset(); 100 + } 94 101 }; 95 102 96 103 Shape.prototype.transform = function( translation, rotation, scale ) { ··· 106 113 this.children.forEach( function( child ) { 107 114 child.transform( translation, rotation, scale ); 108 115 } ); 116 + if (this.backface && this.backface.transform) { 117 + this.backface.transform( translation, rotation, scale ); 118 + } 119 + if (this.color && this.color.transform) { 120 + this.color.transform( translation, rotation, scale ); 121 + } 109 122 }; 110 123 111 124 Shape.prototype.updateSortValue = function() { ··· 153 166 154 167 var TAU = utils.TAU; 155 168 // Safari does not render lines with no size, have to render circle instead 156 - Shape.prototype.renderCanvasDot = function( ctx ) { 169 + Shape.prototype.renderCanvasDot = function( ctx , renderer) { 157 170 var lineWidth = this.getLineWidth(); 158 171 if ( !lineWidth ) { 159 172 return; 160 173 } 161 - ctx.fillStyle = this.getRenderColor(); 162 174 var point = this.pathCommands[0].endRenderPoint; 163 175 ctx.beginPath(); 164 176 var radius = lineWidth/2; 165 177 ctx.arc( point.x, point.y, radius, 0, TAU ); 166 - ctx.fill(); 178 + renderer.fill(ctx, null, true, this.getRenderColor() ); 167 179 }; 168 180 169 181 Shape.prototype.getLineWidth = function() { ··· 178 190 179 191 Shape.prototype.getRenderColor = function() { 180 192 // use backface color if applicable 181 - var isBackfaceColor = typeof this.backface == 'string' && this.isFacingBack; 193 + var isBackfaceColor = utils.isColor(this.backface) && this.isFacingBack; 182 194 var color = isBackfaceColor ? this.backface : this.color; 183 195 return color; 184 196 };
+6
js/svg-renderer.js
··· 62 62 if ( !isStroke ) { 63 63 return; 64 64 } 65 + if (color && color.getSvgFill) { 66 + color = color.getSvgFill(svg); 67 + } 65 68 elem.setAttribute( 'stroke', color ); 66 69 elem.setAttribute( 'stroke-width', lineWidth ); 67 70 }; 68 71 69 72 SvgRenderer.fill = function( svg, elem, isFill, color ) { 70 73 var fillColor = isFill ? color : 'none'; 74 + if (fillColor && fillColor.getSvgFill) { 75 + fillColor = fillColor.getSvgFill(svg); 76 + } 71 77 elem.setAttribute( 'fill', fillColor ); 72 78 }; 73 79
+141
js/texture.js
··· 1 + /** 2 + * Texture 3 + */ 4 + ( function( root, factory ) { 5 + // module definition 6 + if ( typeof module == 'object' && module.exports ) { 7 + // CommonJS 8 + module.exports = factory(require('./vector')); 9 + } else { 10 + // browser global 11 + var Zdog = root.Zdog; 12 + Zdog.Texture = factory(Zdog.Vector); 13 + } 14 + }( this, function factory(Vector) { 15 + 16 + /** 17 + * Calculates the inverse of the matrix: 18 + * | x1 x2 x3 | 19 + * | y1 y2 y3 | 20 + * | 1 1 1 | 21 + */ 22 + function inverse(x1, y1, x2, y2, x3, y3) { 23 + let tp = [ 24 + y2 - y3, x3 - x2, x2*y3 - x3*y2, 25 + y3 - y1, x1 - x3, x3*y1 - x1*y3, 26 + y1 - y2, x2 - x1, x1*y2 - x2*y1]; 27 + let det = tp[2] + tp[5] + tp[8]; 28 + return tp.map(x => x / det); 29 + } 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) { 38 + if (!Array.isArray(map) || !map.length) { 39 + map = [0, 0, img.width, img.height]; 40 + } 41 + if (typeof(map[0]) == "number") { 42 + if (map.length < 4) { 43 + let tmp = map; 44 + map = [0, 0, img.width, img.height]; 45 + for (let i = 0; i < tmp.length; i++) { 46 + map[i] = tmp[i]; 47 + } 48 + } 49 + return [ 50 + new Vector({x:map[0], y:map[1], z:1}), 51 + new Vector({x:map[0] + map[2], y:map[1], z:1}), 52 + new Vector({x:map[0], y:map[1] + map[3], z:1}) 53 + ]; 54 + } else { 55 + return [new Vector(map[0]), new Vector(map[1]), new Vector(map[2])]; 56 + } 57 + } 58 + 59 + var idCounter = 0; 60 + function Texture(img, options ) { 61 + this.id = idCounter++; 62 + this.isTexture = true; 63 + this.img = img; 64 + 65 + options = options || { } 66 + this.src = parsePointMap(img, options.src); 67 + this.dst = parsePointMap(img, options.dst); 68 + 69 + this.srcInverse = inverse( 70 + this.src[0].x, this.src[0].y, 71 + this.src[1].x, this.src[1].y, 72 + this.src[2].x, this.src[2].y); 73 + this.p1 = new Vector(); 74 + this.p2 = new Vector(); 75 + this.p3 = new Vector(); 76 + this.matrix = [0, 0, 0, 0, 0, 0]; 77 + }; 78 + 79 + Texture.prototype.getMatrix = function() { 80 + let m = this.matrix; 81 + let inverse = this.srcInverse; 82 + m[0] = this.p1.x * inverse[0] + this.p2.x * inverse[3] + this.p3.x * inverse[6]; 83 + m[1] = this.p1.y * inverse[0] + this.p2.y * inverse[3] + this.p3.y * inverse[6]; 84 + m[2] = this.p1.x * inverse[1] + this.p2.x * inverse[4] + this.p3.x * inverse[7]; 85 + m[3] = this.p1.y * inverse[1] + this.p2.y * inverse[4] + this.p3.y * inverse[7]; 86 + m[4] = this.p1.x * inverse[2] + this.p2.x * inverse[5] + this.p3.x * inverse[8]; 87 + m[5] = this.p1.y * inverse[2] + this.p2.y * inverse[5] + this.p3.y * inverse[8]; 88 + return m; 89 + } 90 + 91 + Texture.prototype.getCanvasFill = function(ctx) { 92 + if (!this.pattern) { 93 + this.pattern = ctx.createPattern(this.img, "repeat"); 94 + } 95 + // pattern.setTransform is not supported in IE, 96 + // so transform the context instead 97 + ctx.transform(...this.getMatrix()); 98 + return this.pattern; 99 + }; 100 + 101 + const svgURI = 'http://www.w3.org/2000/svg'; 102 + Texture.prototype.getSvgFill = function(svg) { 103 + 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"); 109 + 110 + let img = document.createElementNS( svgURI, 'image'); 111 + img.setAttribute("href", this.img.src); 112 + this.svgPattern.appendChild(img); 113 + 114 + this.defs = document.createElementNS(svgURI, 'defs' ); 115 + this.defs.appendChild(this.svgPattern); 116 + 117 + } 118 + this.svgPattern.setAttribute("patternTransform", `matrix(${this.getMatrix().join(' ')})`); 119 + svg.appendChild( this.defs ); 120 + return `url(#texture_${this.id})`; 121 + } 122 + 123 + // ----- update ----- // 124 + Texture.prototype.reset = function() { 125 + this.p1.set(this.dst[0]); 126 + this.p2.set(this.dst[1]); 127 + this.p3.set(this.dst[2]); 128 + }; 129 + 130 + Texture.prototype.transform = function( translation, rotation, scale ) { 131 + this.p1.transform(translation, rotation, scale); 132 + this.p2.transform(translation, rotation, scale); 133 + this.p3.transform(translation, rotation, scale); 134 + }; 135 + 136 + Texture.prototype.clone = function() { 137 + return new Texture(this.img, this); 138 + }; 139 + 140 + return Texture; 141 + } ) );