Flat, round, designer-friendly pseudo-3D engine for canvas & SVG
1/**
2 * Illustration
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('./anchor'),
10 require('./dragger') );
11 } else {
12 // browser global
13 var Zdog = root.Zdog;
14 Zdog.Illustration = factory( Zdog, Zdog.Anchor, Zdog.Dragger );
15 }
16}( this, function factory( utils, Anchor, Dragger ) {
17
18function noop() {}
19var TAU = utils.TAU;
20
21var Illustration = Anchor.subclass({
22 element: undefined,
23 centered: true,
24 zoom: 1,
25 dragRotate: false,
26 resize: false,
27 onPrerender: noop,
28 onDragStart: noop,
29 onDragMove: noop,
30 onDragEnd: noop,
31 onResize: noop,
32});
33Illustration.ignoreKeysJSON = [ 'dragRotate', 'element', 'resize' ];
34Illustration.type = 'Illustration';
35
36utils.extend( Illustration.prototype, Dragger.prototype );
37
38Illustration.prototype.create = function( options ) {
39 Anchor.prototype.create.call( this, options );
40 Dragger.prototype.create.call( this, options );
41 this.setElement( this.element );
42 this.setDragRotate( this.dragRotate );
43 this.setResize( this.resize );
44};
45
46Illustration.prototype.setElement = function( element ) {
47 element = this.getQueryElement( element );
48 if ( !element ) {
49 throw new Error( 'Zdog.Illustration element required. Set to ' + element );
50 }
51
52 var nodeName = element.nodeName.toLowerCase();
53 if ( nodeName == 'canvas' ) {
54 this.setCanvas( element );
55 } else if ( nodeName == 'svg' ) {
56 this.setSvg( element );
57 }
58};
59
60Illustration.prototype.setSize = function( width, height ) {
61 width = Math.round( width );
62 height = Math.round( height );
63 if ( this.isCanvas ) {
64 this.setSizeCanvas( width, height );
65 } else if ( this.isSvg ) {
66 this.setSizeSvg( width, height );
67 }
68};
69
70Illustration.prototype.setResize = function( resize ) {
71 this.resize = resize;
72 // create resize event listener
73 if ( !this.resizeListener ) {
74 this.resizeListener = this.onWindowResize.bind( this );
75 }
76 // add/remove event listener
77 if ( resize ) {
78 window.addEventListener( 'resize', this.resizeListener );
79 this.onWindowResize();
80 } else {
81 window.removeEventListener( 'resize', this.resizeListener );
82 }
83};
84
85// TODO debounce this?
86Illustration.prototype.onWindowResize = function() {
87 this.setMeasuredSize();
88 this.onResize( this.width, this.height );
89};
90
91Illustration.prototype.setMeasuredSize = function() {
92 var width, height;
93 var isFullscreen = this.resize == 'fullscreen';
94 if ( isFullscreen ) {
95 width = window.innerWidth;
96 height = window.innerHeight;
97 } else {
98 var rect = this.element.getBoundingClientRect();
99 width = rect.width;
100 height = rect.height;
101 }
102 this.setSize( width, height );
103};
104
105// ----- render ----- //
106
107Illustration.prototype.renderGraph = function( item ) {
108 if ( this.isCanvas ) {
109 this.renderGraphCanvas( item );
110 } else if ( this.isSvg ) {
111 this.renderGraphSvg( item );
112 }
113};
114
115// combo method
116Illustration.prototype.updateRenderGraph = function( item ) {
117 this.updateGraph();
118 this.renderGraph( item );
119};
120
121// ----- canvas ----- //
122
123Illustration.prototype.setCanvas = function( element ) {
124 this.element = element;
125 this.isCanvas = true;
126 // update related properties
127 this.ctx = this.element.getContext('2d');
128 // set initial size
129 this.setSizeCanvas( element.width, element.height );
130};
131
132Illustration.prototype.setSizeCanvas = function( width, height ) {
133 this.width = width;
134 this.height = height;
135 // up-rez for hi-DPI devices
136 var pixelRatio = this.pixelRatio = window.devicePixelRatio || 1;
137 this.element.width = this.canvasWidth = width * pixelRatio;
138 this.element.height = this.canvasHeight = height * pixelRatio;
139 var needsHighPixelRatioSizing = pixelRatio > 1 && !this.resize;
140 if ( needsHighPixelRatioSizing ) {
141 this.element.style.width = width + 'px';
142 this.element.style.height = height + 'px';
143 }
144};
145
146Illustration.prototype.renderGraphCanvas = function( item ) {
147 item = item || this;
148 this.prerenderCanvas();
149 Anchor.prototype.renderGraphCanvas.call( item, this.ctx );
150 this.postrenderCanvas();
151};
152
153Illustration.prototype.prerenderCanvas = function() {
154 var ctx = this.ctx;
155 ctx.lineCap = 'round';
156 ctx.lineJoin = 'round';
157 ctx.clearRect( 0, 0, this.canvasWidth, this.canvasHeight );
158 ctx.save();
159 if ( this.centered ) {
160 var centerX = this.width / 2 * this.pixelRatio;
161 var centerY = this.height / 2 * this.pixelRatio;
162 ctx.translate( centerX, centerY );
163 }
164 var scale = this.pixelRatio * this.zoom;
165 ctx.scale( scale, scale );
166 this.onPrerender( ctx );
167};
168
169Illustration.prototype.postrenderCanvas = function() {
170 this.ctx.restore();
171};
172
173// ----- svg ----- //
174
175Illustration.prototype.setSvg = function( element ) {
176 this.element = element;
177 this.isSvg = true;
178 this.pixelRatio = 1;
179 // set initial size from width & height attributes
180 var width = element.getAttribute('width');
181 var height = element.getAttribute('height');
182 this.setSizeSvg( width, height );
183};
184
185Illustration.prototype.setSizeSvg = function( width, height ) {
186 this.width = width;
187 this.height = height;
188 var viewWidth = width / this.zoom;
189 var viewHeight = height / this.zoom;
190 var viewX = this.centered ? -viewWidth/2 : 0;
191 var viewY = this.centered ? -viewHeight/2 : 0;
192 this.element.setAttribute( 'viewBox', viewX + ' ' + viewY + ' ' +
193 viewWidth + ' ' + viewHeight );
194 if ( this.resize ) {
195 // remove size attributes, let size be determined by viewbox
196 this.element.removeAttribute('width');
197 this.element.removeAttribute('height');
198 } else {
199 this.element.setAttribute( 'width', width );
200 this.element.setAttribute( 'height', height );
201 }
202};
203
204Illustration.prototype.renderGraphSvg = function( item ) {
205 item = item || this;
206 empty( this.element );
207 this.onPrerender( this.element );
208 Anchor.prototype.renderGraphSvg.call( item, this.element );
209};
210
211function empty( element ) {
212 while ( element.firstChild ) {
213 element.removeChild( element.firstChild );
214 }
215}
216
217// ----- drag ----- //
218
219Illustration.prototype.setDragRotate = function( item ) {
220 if ( !item ) {
221 return;
222 } else if ( item === true ) {
223 /* eslint consistent-this: "off" */
224 item = this;
225 }
226 this.dragRotate = item;
227
228 this.bindDrag( this.element );
229};
230
231Illustration.prototype.dragStart = function( /* event, pointer */) {
232 this.dragStartRX = this.dragRotate.rotate.x;
233 this.dragStartRY = this.dragRotate.rotate.y;
234 Dragger.prototype.dragStart.apply( this, arguments );
235};
236
237Illustration.prototype.dragMove = function( event, pointer ) {
238 var moveX = pointer.pageX - this.dragStartX;
239 var moveY = pointer.pageY - this.dragStartY;
240 var displaySize = Math.min( this.width, this.height );
241 var moveRY = moveX/displaySize * TAU;
242 var moveRX = moveY/displaySize * TAU;
243 this.dragRotate.rotate.x = this.dragStartRX - moveRX;
244 this.dragRotate.rotate.y = this.dragStartRY - moveRY;
245 Dragger.prototype.dragMove.apply( this, arguments );
246};
247
248return Illustration;
249
250} ) );