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…'; 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> '; 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