Flat, round, designer-friendly pseudo-3D engine for canvas & SVG
1/**
2 * Shape
3 */
4
5( function( root, factory ) {
6 // module definition
7 if ( typeof module == 'object' && module.exports ) {
8 // CommonJS
9 module.exports = factory( require('./boilerplate'), require('./vector'),
10 require('./path-command'), require('./anchor') );
11 } else {
12 // browser global
13 var Zdog = root.Zdog;
14 Zdog.Shape = factory( Zdog, Zdog.Vector, Zdog.PathCommand, Zdog.Anchor );
15 }
16}( this, function factory( utils, Vector, PathCommand, Anchor ) {
17
18var Shape = Anchor.subclass({
19 stroke: 1,
20 fill: false,
21 color: '#333',
22 closed: true,
23 visible: true,
24 path: [ {} ],
25 front: { z: 1 },
26 backface: true,
27});
28
29Shape.type = 'Shape';
30
31Shape.prototype.create = function( options ) {
32 Anchor.prototype.create.call( this, options );
33 this.updatePath();
34 // front
35 this.front = new Vector( options.front || this.front );
36 this.renderFront = new Vector( this.front );
37 this.renderNormal = new Vector();
38};
39
40var actionNames = [
41 'move',
42 'line',
43 'bezier',
44 'arc',
45];
46
47Shape.prototype.updatePath = function() {
48 this.setPath();
49 this.updatePathCommands();
50};
51
52// place holder for Ellipse, Rect, etc.
53Shape.prototype.setPath = function() {};
54
55// parse path into PathCommands
56Shape.prototype.updatePathCommands = function() {
57 var previousPoint;
58 this.pathCommands = this.path.map( function( pathPart, i ) {
59 // pathPart can be just vector coordinates -> { x, y, z }
60 // or path instruction -> { arc: [ {x0,y0,z0}, {x1,y1,z1} ] }
61 var keys = Object.keys( pathPart );
62 var method = keys[0];
63 var points = pathPart[ method ];
64 // default to line if no instruction
65 var isInstruction = keys.length == 1 && actionNames.indexOf( method ) != -1;
66 if ( !isInstruction ) {
67 method = 'line';
68 points = pathPart;
69 }
70 // munge single-point methods like line & move without arrays
71 var isLineOrMove = method == 'line' || method == 'move';
72 var isPointsArray = Array.isArray( points );
73 if ( isLineOrMove && !isPointsArray ) {
74 points = [ points ];
75 }
76
77 // first action is always move
78 method = i === 0 ? 'move' : method;
79 // arcs require previous last point
80 var command = new PathCommand( method, points, previousPoint );
81 // update previousLastPoint
82 previousPoint = command.endRenderPoint;
83 return command;
84 } );
85};
86
87// ----- update ----- //
88
89Shape.prototype.reset = function() {
90 this.renderOrigin.set( this.origin );
91 this.renderFront.set( this.front );
92 // reset command render points
93 this.pathCommands.forEach( function( command ) {
94 command.reset();
95 } );
96
97 if (this.backface && this.backface.reset) {
98 this.backface.reset();
99 }
100 if (this.color && this.color.reset) {
101 this.color.reset();
102 }
103};
104
105Shape.prototype.transform = function( translation, rotation, scale ) {
106 // calculate render points backface visibility & cone/hemisphere shapes
107 this.renderOrigin.transform( translation, rotation, scale );
108 this.renderFront.transform( translation, rotation, scale );
109 this.renderNormal.set( this.renderOrigin ).subtract( this.renderFront );
110 // transform points
111 this.pathCommands.forEach( function( command ) {
112 command.transform( translation, rotation, scale );
113 } );
114 // transform children
115 this.children.forEach( function( child ) {
116 child.transform( translation, rotation, scale );
117 } );
118 if (this.backface && this.backface.transform) {
119 this.backface.transform( translation, rotation, scale );
120 }
121 if (this.color && this.color.transform) {
122 this.color.transform( translation, rotation, scale );
123 }
124};
125
126Shape.prototype.updateSortValue = function() {
127 // sort by average z of all points
128 // def not geometrically correct, but works for me
129 var pointCount = this.pathCommands.length;
130 var firstPoint = this.pathCommands[0].endRenderPoint;
131 var lastPoint = this.pathCommands[ pointCount - 1 ].endRenderPoint;
132 // ignore the final point if self closing shape
133 var isSelfClosing = pointCount > 2 && firstPoint.isSame( lastPoint );
134 if ( isSelfClosing ) {
135 pointCount -= 1;
136 }
137
138 var sortValueTotal = 0;
139 for ( var i = 0; i < pointCount; i++ ) {
140 sortValueTotal += this.pathCommands[i].endRenderPoint.z;
141 }
142 this.sortValue = sortValueTotal/pointCount;
143};
144
145// ----- render ----- //
146
147Shape.prototype.render = function( ctx, renderer ) {
148 var length = this.pathCommands.length;
149 if ( !this.visible || !length ) {
150 return;
151 }
152 // do not render if hiding backface
153 this.isFacingBack = this.renderNormal.z > 0;
154 if ( !this.backface && this.isFacingBack ) {
155 return;
156 }
157 if ( !renderer ) {
158 throw new Error( 'Zdog renderer required. Set to ' + renderer );
159 }
160 // render dot or path
161 var isDot = length == 1;
162 if ( renderer.isCanvas && isDot ) {
163 this.renderCanvasDot( ctx, renderer );
164 } else {
165 this.renderPath( ctx, renderer );
166 }
167};
168
169var TAU = utils.TAU;
170// Safari does not render lines with no size, have to render circle instead
171Shape.prototype.renderCanvasDot = function( ctx , renderer) {
172 var lineWidth = this.getLineWidth();
173 if ( !lineWidth ) {
174 return;
175 }
176 var point = this.pathCommands[0].endRenderPoint;
177 ctx.beginPath();
178 var radius = lineWidth/2;
179 ctx.arc( point.x, point.y, radius, 0, TAU );
180 renderer.fill(ctx, null, true, this.getRenderColor() );
181};
182
183Shape.prototype.getLineWidth = function() {
184 if ( !this.stroke ) {
185 return 0;
186 }
187 if ( this.stroke == true ) {
188 return 1;
189 }
190 return this.stroke;
191};
192
193Shape.prototype.getRenderColor = function() {
194 // use backface color if applicable
195 var isBackfaceColor = utils.isColor(this.backface) && this.isFacingBack;
196 var color = isBackfaceColor ? this.backface : this.color;
197 return color;
198};
199
200Shape.prototype.renderPath = function( ctx, renderer ) {
201 var elem = this.getRenderElement( ctx, renderer );
202 var isTwoPoints = this.pathCommands.length == 2 &&
203 this.pathCommands[1].method == 'line';
204 var isClosed = !isTwoPoints && this.closed;
205 var color = this.getRenderColor();
206
207 renderer.renderPath( ctx, elem, this.pathCommands, isClosed );
208 renderer.stroke( ctx, elem, this.stroke, color, this.getLineWidth() );
209 renderer.fill( ctx, elem, this.fill, color );
210 renderer.end( ctx, elem );
211};
212
213var svgURI = 'http://www.w3.org/2000/svg';
214
215Shape.prototype.getRenderElement = function( ctx, renderer ) {
216 if ( !renderer.isSvg ) {
217 return;
218 }
219 if ( !this.svgElement ) {
220 // create svgElement
221 this.svgElement = document.createElementNS( svgURI, 'path' );
222 this.svgElement.setAttribute( 'stroke-linecap', 'round' );
223 this.svgElement.setAttribute( 'stroke-linejoin', 'round' );
224 }
225 return this.svgElement;
226};
227
228return Shape;
229
230} ) );