1
+ /**
2
+ * @class Bean
3
+ * Bean is a silly thing which uses Canvas to drop images onto the screen in
4
+ * a stack - imagine dropping a bunch of photographs onto a table and you'll
5
+ * understand what Bean does. Why is it called Bean though? I once knew a man named Bean.
6
+ */
7
+ Bean = function ( config ) {
8
+ config = config || { } ;
9
+
10
+ var defaults = {
11
+ /**
12
+ * @property imageUrls
13
+ * @type Array
14
+ * The array of image urls to use as photographs
15
+ */
16
+ imageUrls : [ ] ,
17
+
18
+ /**
19
+ * @property canvasId
20
+ * @type String
21
+ * The DOM id of the Canvas to use (required)
22
+ */
23
+ canvasId : '' ,
24
+
25
+ /**
26
+ * @property loopSlides
27
+ * @type boolean
28
+ * False to prevent repeat cycle of images
29
+ */
30
+ loopSlides : false ,
31
+
32
+ /**
33
+ * @property randomize
34
+ * @type Boolean
35
+ * True to drop photos in random order (defaults to true). Otherwise they are dropped
36
+ * in the order they are defined in the images array
37
+ */
38
+ randomize : false ,
39
+
40
+ /**
41
+ * @property interval
42
+ * @type Number
43
+ * Number of milliseconds between each drop (defaults to 5000)
44
+ */
45
+ interval : 4000 ,
46
+
47
+ /**
48
+ * @property fallDuration
49
+ * @type Number
50
+ * Number of milliseconds it takes for each image to fall (defaults to 3000)
51
+ */
52
+ fallDuration : 3000 ,
53
+
54
+ /**
55
+ * @property backgroundColor
56
+ * @type String
57
+ * The hex color code to use for the canvas background (defaults to black - "#000000")
58
+ */
59
+ backgroundColor : "#000000" ,
60
+
61
+ /**
62
+ * @property constrain
63
+ * @type Boolean
64
+ * True to ensure that the each image falls within the bounds of the canvas
65
+ */
66
+ constrain : true ,
67
+
68
+ /**
69
+ * @property fillBody
70
+ * @type Boolean
71
+ * True to resize the canvas to the full size of the window (defaults to false)
72
+ */
73
+ fillBody : false ,
74
+
75
+ /**
76
+ * @property useKeyFrames
77
+ * @type Boolean
78
+ * True to optimize animation by using key frames. This takes a snapshot of all
79
+ * Plungers that have already landed each time one lands, and then only redraws
80
+ * the moving Plungers. Defaults to true. Does not work with images loaded from
81
+ * another domain.
82
+ */
83
+ useKeyFrames : true
84
+ } ;
85
+
86
+ //apply defaults and config
87
+ for ( var key in defaults ) {
88
+ this [ key ] = config [ key ] || defaults [ key ] ;
89
+ }
90
+
91
+ //turn off key frames if any images are from another domain
92
+ //FIXME: this could identify local urls as cross-domain if they are fully specified
93
+ for ( var i = 0 , j = config . imageUrls . length ; i < j ; i ++ ) {
94
+ if ( / ^ h t t p / . test ( config . imageUrls [ i ] ) ) this . useKeyFrames = false ;
95
+ }
96
+
97
+ /**
98
+ * @property images
99
+ * @type Array
100
+ * The array of Image objects which are preloaded using the imageUrls config
101
+ */
102
+ this . images = [ ] ;
103
+
104
+ /**
105
+ * @property plungers
106
+ * @type Array
107
+ * The set of plunging objects. These are each drawn on every iteration
108
+ */
109
+ this . plungers = new Bean . Plungers ( ) ;
110
+
111
+ /**
112
+ * @property initialized
113
+ * @type Boolean
114
+ * True when Bean has been initialized and all images preloaded
115
+ */
116
+ this . initialized = false ;
117
+
118
+ /**
119
+ * @property onReadyCallbacks
120
+ * @type Array
121
+ * The array of callback functions to call when all images have been loaded
122
+ */
123
+ this . onReadyCallbacks = [ ] ;
124
+
125
+ /**
126
+ * @property lastPlungerAdded
127
+ * @type Date
128
+ * The time the last plunger was added
129
+ */
130
+ this . lastPlungerAdded = new Date ( new Date ( ) - this . interval ) ;
131
+
132
+ this . initialize ( ) ;
133
+ } ;
134
+
135
+ Bean . prototype = {
136
+
137
+ /**
138
+ * Sets up the canvas element
139
+ */
140
+ initialize : function ( ) {
141
+ /**
142
+ * @property canvas
143
+ * @type HTMLElement
144
+ * The Canvas element (NOT the context - see this.context)
145
+ */
146
+ this . canvas = document . getElementById ( this . canvasId ) ;
147
+
148
+ if ( this . fillBody ) {
149
+ var body = document . body ;
150
+
151
+ this . canvas . width = body . clientWidth ;
152
+ this . canvas . height = body . clientHeight ;
153
+ }
154
+
155
+ /**
156
+ * @property context
157
+ * @type Context2d
158
+ * The 2d canvas context
159
+ */
160
+ this . context = this . canvas . getContext ( '2d' ) ;
161
+
162
+ if ( this . useKeyFrames ) {
163
+ /**
164
+ * @property takeKeyFrame
165
+ * @type Boolean
166
+ * @private
167
+ * True if a keyframe should be generated next time the scene is drawn.
168
+ * This is usually set to true after a Plunger has landed so that it can
169
+ * be pruned from being re-rendered on every frame
170
+ */
171
+ this . takeKeyFrame = false ;
172
+
173
+ /**
174
+ * @property currentKeyFrame
175
+ * @type Image
176
+ * The current keyframe image.
177
+ */
178
+ this . currentKeyFrame = undefined ;
179
+
180
+ this . plungers . onPlungeComplete ( function ( ) {
181
+ this . takeKeyFrame = true ;
182
+ } , this ) ;
183
+ }
184
+
185
+ this . preloadImages ( ) ;
186
+ } ,
187
+
188
+ /**
189
+ * Iterates over this.imageUrls and preloads all images
190
+ */
191
+ preloadImages : function ( ) {
192
+ var urls = this . imageUrls ,
193
+ total = this . imageUrls . length ,
194
+ loaded = 0 ;
195
+
196
+ //used in the loadCallback below
197
+ var onReadyCallbacks = this . onReadyCallbacks ;
198
+ var me = this ;
199
+
200
+ //Returns a function which is called after each image loads.
201
+ //Calls each onReady callback when the final image has been loaded
202
+ var loadCallback = function ( image , id ) {
203
+ return function ( ) {
204
+ loaded += 1 ;
205
+ me . images [ id ] = image ;
206
+
207
+ if ( loaded == total ) {
208
+ for ( var i = 0 ; i < onReadyCallbacks . length ; i ++ ) {
209
+ var cfg = onReadyCallbacks [ i ] ;
210
+
211
+ cfg . fn . call ( cfg . scope , me ) ;
212
+ }
213
+ }
214
+ } ;
215
+ } ;
216
+
217
+ //load the actual images
218
+ for ( var i = 0 , j = urls . length ; i < j ; i ++ ) {
219
+ var image = new Image ( ) ;
220
+ image . onload = loadCallback ( image , i ) ;
221
+ image . src = urls [ i ] ;
222
+ }
223
+ } ,
224
+
225
+ /**
226
+ * Registers a function to be called when all images have been loaded.
227
+ * The function will be called with a single argument - this Bean instance.
228
+ * @param {Function } fn The function to call
229
+ * @param {Object } scope Optional scope to call the function with (defaults to this)
230
+ */
231
+ onReady : function ( fn , scope ) {
232
+ this . onReadyCallbacks . push ( {
233
+ fn : fn ,
234
+ scope : scope || this
235
+ } ) ;
236
+ } ,
237
+
238
+ /**
239
+ * Draws the frame with all existing and currently falling images
240
+ */
241
+ drawFrame : function ( ) {
242
+ var frameTime = new Date ( ) - this . lastFrameTime ,
243
+ totalTime = new Date ( ) - this . startTime ,
244
+ context = this . context ,
245
+ plungers = this . plungers ;
246
+
247
+ //create a new plunger if required
248
+ if ( new Date ( ) - this . lastPlungerAdded > this . interval ) {
249
+ ( this . loopSlides || ( this . counter ++ < this . imageUrls . length ) ) ? this . addPlunger ( ) : this . stop ( ) ;
250
+ }
251
+
252
+ //take a key frame if necessary
253
+ if ( this . takeKeyFrame ) {
254
+ this . currentKeyFrame = this . context . getImageData ( 0 , 0 , this . canvas . width , this . canvas . height ) ;
255
+
256
+ //we've now taken the keyframe, no need to take another next frame
257
+ this . takeKeyFrame = false ;
258
+ }
259
+
260
+ //clear canvas and redraw everything
261
+ this . clearCanvas ( ) ;
262
+ var moving = plungers . getMoving ( ) ;
263
+
264
+ for ( var i = 0 , j = moving . length ; i < j ; i ++ ) {
265
+ this . withContext ( function ( context ) {
266
+ moving [ i ] . draw ( context ) ;
267
+ } ) ;
268
+ }
269
+
270
+ this . lastFrameTime = new Date ( ) ;
271
+ } ,
272
+
273
+ /**
274
+ * Starts dropping the photos
275
+ */
276
+ start : function ( ) {
277
+ /**
278
+ * @property startTime
279
+ * @type Date
280
+ * The time the animation started. Used for calculating when to drop next plunger, etc
281
+ */
282
+ this . startTime = new Date ( ) ;
283
+
284
+ /**
285
+ * @property lastFrameTime
286
+ * @type Date
287
+ * The time the last frame was drawn
288
+ */
289
+ this . lastFrameTime = new Date ( ) ;
290
+ this . counter = 0 ;
291
+
292
+ var me = this ;
293
+
294
+ var looper = function ( ) {
295
+ me . drawFrame . call ( me ) ;
296
+ } ;
297
+
298
+ this . clearCanvas ( false ) ;
299
+
300
+ this . running = setInterval ( looper , 100 ) ;
301
+ } ,
302
+
303
+ /**
304
+ * Stop dropping the photos. Maybe.
305
+ */
306
+ stop : function ( ) {
307
+ clearInterval ( this . running ) ;
308
+ } ,
309
+
310
+ /**
311
+ * Adds a new plunger with randomised location and end rotation
312
+ */
313
+ addPlunger : function ( config ) {
314
+ config = config || { } ;
315
+
316
+ var imageId = 0 ;
317
+ if ( this . randomize ) {
318
+ imageId = Math . floor ( Math . random ( ) * this . images . length ) ;
319
+ } else {
320
+ this . lastImageId = this . lastImageId || 0 ;
321
+ this . lastImageId ++ ;
322
+ this . lastImageId = this . lastImageId % this . images . length ;
323
+ imageId = this . lastImageId ;
324
+ }
325
+
326
+ var defaults = {
327
+ fallDuration : this . fallDuration ,
328
+ image : this . images [ imageId ] ,
329
+ endRotation : ( Math . random ( ) * Math . PI / 2 ) - ( Math . PI / 4 ) ,
330
+ xPos : Math . random ( ) * ( this . canvas . width - 100 ) + 50 ,
331
+ yPos : Math . random ( ) * ( this . canvas . height - 100 ) + 50
332
+ } ;
333
+
334
+ for ( var key in config ) {
335
+ defaults [ key ] = config [ key ] ;
336
+ }
337
+
338
+ this . plungers . add ( new Bean . Plunger ( defaults ) ) ;
339
+
340
+ this . lastPlungerAdded = new Date ( ) ;
341
+ } ,
342
+
343
+ /**
344
+ * Clears the canvas by filling it with the backgroundColor
345
+ * @param {Boolean } redrawKeyFrame True to automatically redraw the background keyframe
346
+ * immediately after clearing (defaults to true)
347
+ */
348
+ clearCanvas : function ( redrawKeyFrame ) {
349
+ this . withContext ( function ( context ) {
350
+ if ( redrawKeyFrame !== false && this . currentKeyFrame != undefined ) {
351
+ context . putImageData ( this . currentKeyFrame , 0 , 0 , this . canvas . height , this . canvas . width ) ;
352
+ } else {
353
+ context . fillStyle = this . backgroundColor ;
354
+ context . fillRect ( 0 , 0 , this . canvas . width , this . canvas . height ) ;
355
+ }
356
+ } ) ;
357
+ } ,
358
+
359
+ /**
360
+ * Runs a function with the context as a single argument. Saves and restores the context
361
+ * so as not to pollute other drawing functions
362
+ * @param {Function } fn The function to run
363
+ * @param {Object } scope Optional scope to call the function in (defaults to this)
364
+ */
365
+ withContext : function ( fn , scope ) {
366
+ var context = this . context ;
367
+
368
+ context . save ( ) ;
369
+ fn . call ( scope || this , context ) ;
370
+ context . restore ( ) ;
371
+ }
372
+ } ;
0 commit comments