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