OSDN Git Service

Add Flo to device art generator.
[android-x86/frameworks-base.git] / docs / html / distribute / promote / device-art.jd
1 page.title=Device Art Generator
2 @jd:body
3 <div id="butterbar-wrapper" >
4   <div id="butterbar" >
5     <div id="butterbar-message">
6 <a target="_blank" href="https://docs.google.com/a/google.com/forms/d/1EHLPGqhbxj2HungHRRN4_0K9TGpc-Izy-u46vBDgS8Q/viewform">
7       Take the Android Developer Survey</a>
8     </div>
9   </div>
10 </div>
11
12 <p>The device art generator allows you to quickly wrap your app screenshots in real device artwork.
13 This provides better visual context for your app screenshots on your web site or in other
14 promotional materials.</p>
15
16 <p class="note"><strong>Note</strong>: Do <em>not</em> use graphics created here in your 1024x500
17 feature image or screenshots for your Google Play app listing.</p>
18
19 <hr>
20
21 <div class="supported-browser">
22
23 <div class="layout-content-row">
24   <div class="layout-content-col span-3">
25     <h4>Step 1</h4>
26     <p>Drag a screenshot from your desktop onto a device to the right.</p>
27   </div>
28   <div class="layout-content-col span-10">
29     <ul class="device-list primary"></ul>
30     <a href="#" id="archive-expando">Older devices</a>
31     <ul class="device-list archive"></ul>
32   </div>
33 </div>
34
35 <hr>
36
37 <div class="layout-content-row">
38   <div class="layout-content-col span-3">
39     <h4>Step 2</h4>
40     <p>Customize the generated image and drag it to your desktop to save.</p>
41     <p id="frame-customizations">
42       <input type="checkbox" id="output-shadow" checked="checked" class="form-field-checkbutton">
43       <label for="output-shadow">Shadow</label><br>
44       <input type="checkbox" id="output-glare" checked="checked" class="form-field-checkbutton">
45       <label for="output-glare">Screen Glare</label><br><br>
46       <a class="button" id="rotate-button">Rotate</a>
47     </p>
48   </div>
49   <div class="layout-content-col span-10">
50     <div id="output">No input image.</div>
51   </div>
52 </div>
53
54 </div>
55
56 <div class="unsupported-browser" style="display: none">
57   <p class="warning"><strong>Error:</strong> This page requires 
58     <span id="unsupported-browser-reason">certain features</span>, which your web browser
59     doesn't support. To continue, navigate to this page on a supported web browser, such as
60     <strong>Google Chrome</strong>.</p>
61   <a href="https://www.google.com/chrome/" class="button">Get Google Chrome</a>
62   <br><br>
63 </div>
64
65 <style>
66   h4 {
67     text-transform: uppercase;
68   }
69
70   .device-list {
71     padding: 0;
72     margin: 0;
73   }
74
75   .device-list li {
76     display: inline-block;
77     vertical-align: bottom;
78     margin: 0;
79     margin-right: 20px;
80     text-align: center;
81   }
82
83   .device-list li .thumb-container {
84     display: inline-block;
85   }
86
87   .device-list li .thumb-container img {
88     margin-bottom: 8px;
89     opacity: 0.6;
90
91     -webkit-transition: -webkit-transform 0.2s, opacity 0.2s;
92        -moz-transition:    -moz-transform 0.2s, opacity 0.2s;
93             transition:         transform 0.2s, opacity 0.2s;
94   }
95
96   .device-list li.drag-hover .thumb-container img {
97     opacity: 1;
98
99     -webkit-transform: scale(1.1);
100        -moz-transform: scale(1.1);
101             transform: scale(1.1);
102   }
103
104   .device-list li .device-details {
105     font-size: 13px;
106     line-height: 16px;
107     color: #888;
108   }
109
110   .device-list li .device-url {
111     font-weight: bold;
112   }
113
114   #archive-expando {
115     display: block;
116     font-size: 13px;
117     font-weight: bold;
118     color: #333;
119     text-transform: uppercase;
120     margin-top: 16px;
121     padding-top: 16px;
122     padding-left: 28px;
123     border-top: 1px solid transparent;
124     background: transparent url({@docRoot}assets/images/styles/disclosure_down.png)
125                 no-repeat scroll 0 8px;
126   }
127
128   #archive-expando.expanded {
129     background-image: url({@docRoot}assets/images/styles/disclosure_up.png);
130     border-top: 1px solid #ccc;
131   }
132
133   #output {
134     color: #f44;
135     font-style: italic;
136   }
137
138   #output img {
139     max-height: 500px;
140   }
141 </style>
142 <script>
143   // Global variables
144   var g_currentImage;
145   var g_currentDevice;
146
147   // Global constants
148   var MSG_INVALID_INPUT_IMAGE = 'Invalid screenshot provided. Screenshots must be PNG files '
149       + 'matching the target device\'s screen aspect ratio in either portrait or landscape.';
150   var MSG_NO_INPUT_IMAGE = 'Drag a screenshot (in PNG format) from your desktop onto a '
151       + 'target device above.'
152   var MSG_GENERATING_IMAGE = 'Generating device art&hellip;';
153
154   var MAX_DISPLAY_HEIGHT = 126; // XOOM, to fit into 200px wide
155
156   // Device manifest.
157   var DEVICES = [
158     {
159       id: 'nexus_4',
160       title: 'Nexus 4',
161       url: 'http://www.google.com/nexus/4/',
162       physicalSize: 4.7,
163       physicalHeight: 5.23,
164       density: 'XHDPI',
165       landRes: ['shadow', 'back', 'fore'],
166       landOffset: [349,214],
167       portRes: ['shadow', 'back', 'fore'],
168       portOffset: [213,350],
169       portSize: [768,1280]
170     },
171     {
172       id: 'nexus_7',
173       title: 'Nexus 7',
174       url: 'http://www.google.com/nexus/7/',
175       physicalSize: 7,
176       physicalHeight: 8,
177       actualResolution: [1200,1920],
178       density: 'XHDPI',
179       landRes: ['shadow', 'back', 'fore'],
180       landOffset: [326,245],
181       portRes: ['shadow', 'back', 'fore'],
182       portOffset: [244,326],
183       portSize: [800,1280]
184     },
185     {
186       id: 'nexus_10',
187       title: 'Nexus 10',
188       url: 'http://www.google.com/nexus/10/',
189       physicalSize: 10,
190       physicalHeight: 7,
191       actualResolution: [1600,2560],
192       density: 'XHDPI',
193       landRes: ['shadow', 'back', 'fore'],
194       landOffset: [227,217],
195       portRes: ['shadow', 'back', 'fore'],
196       portOffset: [217,223],
197       portSize: [800,1280]
198     },
199     {
200       id: 'xoom',
201       title: 'Motorola XOOM',
202       url: 'http://www.google.com/phone/detail/motorola-xoom',
203       physicalSize: 10,
204       physicalHeight: 6.61,
205       density: 'MDPI',
206       landRes: ['shadow', 'back', 'fore'],
207       landOffset: [218,191],
208       portRes: ['shadow', 'back', 'fore'],
209       portOffset: [199,200],
210       portSize: [800,1280],
211       archived: true
212     },
213     {
214       id: 'nexus_7_2012',
215       title: 'Nexus 7 (2012)',
216       url: 'http://www.google.com/nexus/7/',
217       physicalSize: 7,
218       physicalHeight: 7.81,
219       density: '213dpi',
220       landRes: ['shadow', 'back', 'fore'],
221       landOffset: [315,270],
222       portRes: ['shadow', 'back', 'fore'],
223       portOffset: [264,311],
224       portSize: [800,1280],
225       archived: true
226     },
227     {
228       id: 'galaxy_nexus',
229       title: 'Galaxy Nexus',
230       url: 'http://www.android.com/devices/detail/galaxy-nexus',
231       physicalSize: 4.65,
232       physicalHeight: 5.33,
233       density: 'XHDPI',
234       landRes: ['shadow', 'back', 'fore'],
235       landOffset: [371,199],
236       portRes: ['shadow', 'back', 'fore'],
237       portOffset: [216,353],
238       portSize: [720,1280],
239       archived: true
240     },
241     {
242       id: 'nexus_s',
243       title: 'Nexus S',
244       url: 'http://www.google.com/phone/detail/nexus-s',
245       physicalSize: 4.0,
246       physicalHeight: 4.88,
247       density: 'HDPI',
248       landRes: ['shadow', 'back', 'fore'],
249       landOffset: [247,135],
250       portRes: ['shadow', 'back', 'fore'],
251       portOffset: [134,247],
252       portSize: [480,800],
253       archived: true
254     }
255   ];
256
257   DEVICES = DEVICES.sort(function(x, y) { return x.physicalSize - y.physicalSize; });
258
259   var MAX_HEIGHT = 0;
260   for (var i = 0; i < DEVICES.length; i++) {
261     MAX_HEIGHT = Math.max(MAX_HEIGHT, DEVICES[i].physicalHeight);
262   }
263
264   // Setup performed once the DOM is ready.
265   $(document).ready(function() {
266     if (!checkBrowser()) {
267       return;
268     }
269
270     setupUI();
271
272     // Set up Chrome drag-out
273     $.event.props.push("dataTransfer");
274     document.body.addEventListener('dragstart', function(e) {
275       var a = e.target;
276       if (a.classList.contains('dragout')) {
277         e.dataTransfer.setData('DownloadURL', a.dataset.downloadurl);
278       }
279     }, false);
280   });
281
282   /**
283    * Returns the device from DEVICES with the given id.
284    */
285   function getDeviceById(id) {
286     for (var i = 0; i < DEVICES.length; i++) {
287       if (DEVICES[i].id == id)
288         return DEVICES[i];
289     }
290     return;
291   }
292
293   /**
294    * Checks to make sure the browser supports this page. If not,
295    * updates the UI accordingly and returns false.
296    */
297   function checkBrowser() {
298     // Check for browser support
299     var browserSupportError = null;
300
301     // Must have <canvas>
302     var elem = document.createElement('canvas');
303     if (!elem.getContext || !elem.getContext('2d')) {
304       browserSupportError = 'HTML5 canvas.';
305     }
306
307     // Must have FileReader
308     if (!window.FileReader) {
309       browserSupportError = 'desktop file access';
310     }
311
312     if (browserSupportError) {
313       $('.supported-browser').hide();
314
315       $('#unsupported-browser-reason').html(browserSupportError);
316       $('.unsupported-browser').show();
317       return false;
318     }
319
320     return true;
321   }
322
323   function setupUI() {
324     $('#output').html(MSG_NO_INPUT_IMAGE);
325
326     $('#frame-customizations').hide();
327     $('.device-list.archive').hide();
328
329     $('#output-shadow, #output-glare').click(function() {
330       createFrame();
331     });
332
333     // Build device list.
334     $.each(DEVICES, function() {
335       var resolution = this.actualResolution || this.portSize;
336       var scaleFactorText = '';
337       if (resolution[0] != this.portSize[0]) {
338         scaleFactorText = '<br>' + (100 * (this.portSize[0] / resolution[0])).toFixed(0) +
339             '% size output';
340       } else {
341         scaleFactorText = '<br>&nbsp;';
342       }
343
344       $('<li>')
345           .append($('<div>')
346               .addClass('thumb-container')
347               .append($('<img>')
348                   .attr('src', 'device-art-resources/' + this.id + '/thumb.png')
349                   .attr('height',
350                       Math.floor(MAX_DISPLAY_HEIGHT * this.physicalHeight / MAX_HEIGHT))))
351           .append($('<div>')
352               .addClass('device-details')
353               .html((this.url
354                   ? ('<a class="device-url" href="' + this.url + '">' + this.title + '</a>')
355                   : this.title) +
356                   '<br>' +  this.physicalSize + '" @ ' + this.density +
357                   '<br>' + (resolution[0] + 'x' + resolution[1]) + scaleFactorText))
358           .data('deviceId', this.id)
359           .appendTo(this.archived ? '.device-list.archive' : '.device-list.primary');
360     });
361
362     // Set up "older devices" expando.
363     $('#archive-expando').click(function() {
364       if ($(this).hasClass('expanded')) {
365         $(this).removeClass('expanded');
366         $('.device-list.archive').hide();
367       } else {
368         $(this).addClass('expanded');
369         $('.device-list.archive').show();
370       }
371       return false;
372     });
373
374     // Set up drag and drop.
375     $('.device-list li')
376         .live('dragover', function(evt) {
377           $(this).addClass('drag-hover');
378           evt.dataTransfer.dropEffect = 'link';
379           evt.preventDefault();
380         })
381         .live('dragleave', function(evt) {
382           $(this).removeClass('drag-hover');
383         })
384         .live('drop', function(evt) {
385           $('#output').empty().html(MSG_GENERATING_IMAGE);
386           $(this).removeClass('drag-hover');
387           g_currentDevice = getDeviceById($(this).closest('li').data('deviceId'));
388           evt.preventDefault();
389           loadImageFromFileList(evt.dataTransfer.files, function(data) {
390             if (data == null) {
391               $('#output').html(MSG_INVALID_INPUT_IMAGE);
392               return;
393             }
394             loadImageFromUri(data.uri, function(img) {
395               g_currentFilename = data.name;
396               g_currentImage = img;
397               createFrame();
398               // Send the event to Analytics
399               _gaq.push(['_trackEvent', 'Distribute', 'Create Device Art', g_currentDevice.title]);
400             });
401           });
402         });
403
404     // Set up rotate button.
405     $('#rotate-button').click(function() {
406       if (!g_currentImage) {
407         return;
408       }
409
410       var w = g_currentImage.naturalHeight;
411       var h = g_currentImage.naturalWidth;
412       var canvas = $('<canvas>')
413           .attr('width', w)
414           .attr('height', h)
415           .get(0);
416
417       var ctx = canvas.getContext('2d');
418       ctx.rotate(-Math.PI / 2);
419       ctx.translate(-h, 0);
420       ctx.drawImage(g_currentImage, 0, 0);
421
422       loadImageFromUri(canvas.toDataURL(), function(img) {
423         g_currentImage = img;
424         createFrame();
425       });
426     });
427   }
428
429   /**
430    * Generates the frame from the current selections (g_currentImage and g_currentDevice).
431    */
432   function createFrame() {
433     var port;
434
435     var aspect1 = g_currentImage.naturalWidth / g_currentImage.naturalHeight;
436     var aspect2 = g_currentDevice.portSize[0] / g_currentDevice.portSize[1];
437
438     if (aspect1 == aspect2) {
439       port = true;
440     } else if (aspect1 == 1 / aspect2) {
441       port = false;
442     } else {
443       alert('The screenshot must have an aspect ratio of ' +
444           aspect2.toFixed(3) + ' or ' + (1 / aspect2).toFixed(3) +
445           ' (ideally ' + g_currentDevice.portSize[0] + 'x' + g_currentDevice.portSize[1] +
446           ' or ' + g_currentDevice.portSize[1] + 'x' + g_currentDevice.portSize[0] + ').');
447       $('#output').html(MSG_INVALID_INPUT_IMAGE);
448       return;
449     }
450
451     // Load image resources
452     var res = port ? g_currentDevice.portRes : g_currentDevice.landRes;
453     var resList = {};
454     for (var i = 0; i < res.length; i++) {
455       resList[res[i]] = 'device-art-resources/' + g_currentDevice.id + '/' +
456           (port ? 'port_' : 'land_') + res[i] + '.png'
457     }
458
459     var resourceImages = {};
460     loadImageResources(resList, function(r) {
461       resourceImages = r;
462       continuation_();
463     });
464
465     function continuation_() {
466       var width = resourceImages['back'].naturalWidth;
467       var height = resourceImages['back'].naturalHeight;
468       var offset = port ? g_currentDevice.portOffset : g_currentDevice.landOffset;
469       var size = port
470           ? g_currentDevice.portSize
471           : [g_currentDevice.portSize[1], g_currentDevice.portSize[0]];
472
473       var canvas = document.createElement('canvas');
474       canvas.width = width;
475       canvas.height = height;
476
477       var ctx = canvas.getContext('2d');
478       if (resourceImages['shadow'] && $('#output-shadow').is(':checked')) {
479         ctx.drawImage(resourceImages['shadow'], 0, 0);
480       }
481       ctx.drawImage(resourceImages['back'], 0, 0);
482       ctx.fillStyle = '#000';
483       ctx.fillRect(offset[0], offset[1], size[0], size[1]);
484       ctx.drawImage(g_currentImage, offset[0], offset[1], size[0], size[1]);
485       if (resourceImages['fore'] && $('#output-glare').is(':checked')) {
486         ctx.drawImage(resourceImages['fore'], 0, 0);
487       }
488
489       var dataUrl = canvas.toDataURL();
490       var filename = g_currentFilename
491           ? ('framed_' + g_currentFilename)
492           : 'framed_screenshot.png';
493
494       var $link = $('<a>')
495           .attr('download', filename)
496           .attr('href', dataUrl)
497           .attr('draggable', true)
498           .attr('data-downloadurl', ['image/png', filename, dataUrl].join(':'))
499           .append($('<img>').attr('src', dataUrl))
500           .appendTo($('#output').empty());
501
502       $('#frame-customizations').show();
503     }
504   }
505
506   /**
507    * Loads an image from a data URI. The callback will be called with the <img> once
508    * it loads.
509    */
510   function loadImageFromUri(uri, callback) {
511     callback = callback || function(){};
512
513     var img = document.createElement('img');
514     img.src = uri;
515     img.onload = function() {
516       callback(img);
517     };
518     img.onerror = function() {
519       callback(null);
520     }
521   }
522
523   /**
524    * Loads a set of images (organized by ID). Once all images are loaded, the callback
525    * is triggered with a dictionary of <img>'s, organized by ID.
526    */
527   function loadImageResources(images, callback) {
528     var imageResources = {};
529
530     var checkForCompletion_ = function() {
531       for (var id in images) {
532         if (!(id in imageResources))
533           return;
534       }
535       (callback || function(){})(imageResources);
536       callback = null;
537     };
538
539     for (var id in images) {
540       var img = document.createElement('img');
541       img.src = images[id];
542       (function(img, id) {
543         img.onload = function() {
544           imageResources[id] = img;
545           checkForCompletion_();
546         };
547         img.onerror = function() {
548           imageResources[id] = null;
549           checkForCompletion_();
550         }
551       })(img, id);
552     }
553   }
554
555   /**
556    * Loads the first valid image from a FileList (e.g. drag + drop source), as a data URI. This
557    * method will throw an alert() in case of errors and call back with null.
558    *
559    * @param {FileList} fileList The FileList to load.
560    * @param {Function} callback The callback to fire once image loading is done (or fails).
561    * @return Returns an object containing 'uri' representing the loaded image. There will also be
562    *      a 'name' field indicating the file name, if one is available.
563    */
564   function loadImageFromFileList(fileList, callback) {
565     fileList = fileList || [];
566
567     var file = null;
568     for (var i = 0; i < fileList.length; i++) {
569       if (fileList[i].type.toLowerCase().match(/^image\/png/)) {
570         file = fileList[i];
571         break;
572       }
573     }
574
575     if (!file) {
576       alert('Please use a valid screenshot file (PNG format).');
577       callback(null);
578       return;
579     }
580
581     var fileReader = new FileReader();
582
583     // Closure to capture the file information.
584     fileReader.onload = function(e) {
585       callback({
586         uri: e.target.result,
587         name: file.name
588       });
589     };
590     fileReader.onerror = function(e) {
591       switch(e.target.error.code) {
592         case e.target.error.NOT_FOUND_ERR:
593           alert('File not found.');
594           break;
595         case e.target.error.NOT_READABLE_ERR:
596           alert('File is not readable.');
597           break;
598         case e.target.error.ABORT_ERR:
599           break; // noop
600         default:
601           alert('An error occurred reading this file.');
602       }
603       callback(null);
604     };
605     fileReader.onabort = function(e) {
606       alert('File read cancelled.');
607       callback(null);
608     };
609
610     fileReader.readAsDataURL(file);
611   }
612 </script>