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