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