Most of this code is adapted from my code.google.com/p/android-ui-utils project. Change-Id: If6763bc7a490e53bd31d5a713158577fa74cfd7b
518 lines
15 KiB
Plaintext
518 lines
15 KiB
Plaintext
page.title=Device Art Generator
|
|
@jd:body
|
|
|
|
<p>The device art generator allows you to quickly wrap your app screenshots in real device artwork.
|
|
This provides better visual context for your app screenshots on your web site or in other
|
|
promotional materials.</p>
|
|
|
|
<p class="note"><strong>Note</strong>: Do <em>not</em> use graphics created here in your 1024x500
|
|
feature image or screenshots for your Google Play app listing.</p>
|
|
|
|
<hr>
|
|
|
|
<div class="supported-browser">
|
|
|
|
<div class="layout-content-row">
|
|
<div class="layout-content-col span-3">
|
|
<h4>Step 1</h4>
|
|
<p>Drag a screenshot from your desktop onto a device to the right.</p>
|
|
</div>
|
|
<div class="layout-content-col span-10">
|
|
<ul id="device-list"></ul>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div class="layout-content-row">
|
|
<div class="layout-content-col span-3">
|
|
<h4>Step 2</h4>
|
|
<p>Customize the generated image and drag it to your desktop to save.</p>
|
|
<p id="frame-customizations">
|
|
<input type="checkbox" id="output-shadow" checked="checked" class="form-field-checkbutton">
|
|
<label for="output-shadow">Shadow</label><br>
|
|
<input type="checkbox" id="output-glare" checked="checked" class="form-field-checkbutton">
|
|
<label for="output-glare">Screen Glare</label><br><br>
|
|
<a class="button" id="rotate-button">Rotate</a>
|
|
</p>
|
|
</div>
|
|
<div class="layout-content-col span-10">
|
|
<div id="output">No input image.</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="unsupported-browser" style="display: none">
|
|
<p class="warning"><strong>Error:</strong> This page requires
|
|
<span id="unsupported-browser-reason">certain features</span>, which your web browser
|
|
doesn't support. To continue, navigate to this page on a supported web browser, such as
|
|
<strong>Google Chrome</strong>.</p>
|
|
<a href="https://www.google.com/chrome/" class="button">Get Google Chrome</a>
|
|
<br><br>
|
|
</div>
|
|
|
|
<style>
|
|
h4 {
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
#device-list {
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
#device-list li {
|
|
display: inline-block;
|
|
vertical-align: bottom;
|
|
margin: 0;
|
|
margin-right: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
#device-list li .thumb-container {
|
|
display: inline-block;
|
|
}
|
|
|
|
#device-list li .thumb-container img {
|
|
margin-bottom: 8px;
|
|
opacity: 0.6;
|
|
|
|
-webkit-transition: -webkit-transform 0.2s, opacity 0.2s;
|
|
-moz-transition: -moz-transform 0.2s, opacity 0.2s;
|
|
transition: transform 0.2s, opacity 0.2s;
|
|
}
|
|
|
|
#device-list li.drag-hover .thumb-container img {
|
|
opacity: 1;
|
|
|
|
-webkit-transform: scale(1.1);
|
|
-moz-transform: scale(1.1);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
#device-list li .device-details {
|
|
font-size: 13px;
|
|
line-height: 16px;
|
|
color: #888;
|
|
}
|
|
|
|
#device-list li .device-url {
|
|
font-weight: bold;
|
|
}
|
|
|
|
#output {
|
|
color: #f44;
|
|
font-style: italic;
|
|
}
|
|
|
|
#output img {
|
|
max-height: 500px;
|
|
}
|
|
</style>
|
|
<script>
|
|
// Global variables
|
|
var g_currentImage;
|
|
var g_currentDevice;
|
|
|
|
// Global constants
|
|
var MSG_INVALID_INPUT_IMAGE = 'Invalid screenshot provided. Screenshots must be PNG files '
|
|
+ 'matching the target device\'s screen resolution in either portrait or landscape.';
|
|
var MSG_NO_INPUT_IMAGE = 'Drag a screenshot (in PNG format) from your desktop onto a '
|
|
+ 'target device above.'
|
|
var MSG_GENERATING_IMAGE = 'Generating device art…';
|
|
|
|
var MAX_DISPLAY_HEIGHT = 126; // XOOM, to fit into 200px wide
|
|
|
|
// Device manifest.
|
|
var DEVICES = [
|
|
{
|
|
id: 'nexus_7',
|
|
title: 'Nexus 7',
|
|
url: 'http://www.android.com/devices/detail/nexus-7',
|
|
physicalSize: 7,
|
|
physicalHeight: 7.81,
|
|
density: 213,
|
|
landRes: ['shadow', 'back', 'fore'],
|
|
landOffset: [363,260],
|
|
portRes: ['shadow', 'back', 'fore'],
|
|
portOffset: [265,341],
|
|
portSize: [800,1280],
|
|
},
|
|
{
|
|
id: 'xoom',
|
|
title: 'Motorola XOOM',
|
|
url: 'http://www.google.com/phone/detail/motorola-xoom',
|
|
physicalSize: 10,
|
|
physicalHeight: 6.61,
|
|
density: 160,
|
|
landRes: ['shadow', 'back', 'fore'],
|
|
landOffset: [218,191],
|
|
portRes: ['shadow', 'back', 'fore'],
|
|
portOffset: [199,200],
|
|
portSize: [800,1280],
|
|
},
|
|
{
|
|
id: 'galaxy_nexus',
|
|
title: 'Galaxy Nexus',
|
|
url: 'http://www.android.com/devices/detail/galaxy-nexus',
|
|
physicalSize: 4.65,
|
|
physicalHeight: 5.33,
|
|
density: 320,
|
|
landRes: ['shadow', 'back', 'fore'],
|
|
landOffset: [371,199],
|
|
portRes: ['shadow', 'back', 'fore'],
|
|
portOffset: [216,353],
|
|
portSize: [720,1280],
|
|
},
|
|
{
|
|
id: 'nexus_s',
|
|
title: 'Nexus S',
|
|
url: 'http://www.google.com/phone/detail/nexus-s',
|
|
physicalSize: 4.0,
|
|
physicalHeight: 4.88,
|
|
density: 240,
|
|
landRes: ['shadow', 'back', 'fore'],
|
|
landOffset: [247,135],
|
|
portRes: ['shadow', 'back', 'fore'],
|
|
portOffset: [134,247],
|
|
portSize: [480,800],
|
|
}
|
|
];
|
|
|
|
DEVICES = DEVICES.sort(function(x, y) { return x.physicalSize - y.physicalSize; });
|
|
|
|
var MAX_HEIGHT = 0;
|
|
for (var i = 0; i < DEVICES.length; i++) {
|
|
MAX_HEIGHT = Math.max(MAX_HEIGHT, DEVICES[i].physicalHeight);
|
|
}
|
|
|
|
// Setup performed once the DOM is ready.
|
|
$(document).ready(function() {
|
|
if (!checkBrowser()) {
|
|
return;
|
|
}
|
|
|
|
setupUI();
|
|
|
|
// Set up Chrome drag-out
|
|
$.event.props.push("dataTransfer");
|
|
document.body.addEventListener('dragstart', function(e) {
|
|
var a = e.target;
|
|
if (a.classList.contains('dragout')) {
|
|
e.dataTransfer.setData('DownloadURL', a.dataset.downloadurl);
|
|
}
|
|
}, false);
|
|
});
|
|
|
|
/**
|
|
* Returns the device from DEVICES with the given id.
|
|
*/
|
|
function getDeviceById(id) {
|
|
for (var i = 0; i < DEVICES.length; i++) {
|
|
if (DEVICES[i].id == id)
|
|
return DEVICES[i];
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Checks to make sure the browser supports this page. If not,
|
|
* updates the UI accordingly and returns false.
|
|
*/
|
|
function checkBrowser() {
|
|
// Check for browser support
|
|
var browserSupportError = null;
|
|
|
|
// Must have <canvas>
|
|
var elem = document.createElement('canvas');
|
|
if (!elem.getContext || !elem.getContext('2d')) {
|
|
browserSupportError = 'HTML5 canvas.';
|
|
}
|
|
|
|
// Must have FileReader
|
|
if (!window.FileReader) {
|
|
browserSupportError = 'desktop file access';
|
|
}
|
|
|
|
if (browserSupportError) {
|
|
$('.supported-browser').hide();
|
|
|
|
$('#unsupported-browser-reason').html(browserSupportError);
|
|
$('.unsupported-browser').show();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function setupUI() {
|
|
$('#output').html(MSG_NO_INPUT_IMAGE);
|
|
|
|
$('#frame-customizations').hide();
|
|
|
|
$('#output-shadow, #output-glare').click(function() {
|
|
createFrame(g_currentDevice, g_currentImage);
|
|
});
|
|
|
|
// Build device list.
|
|
$.each(DEVICES, function() {
|
|
$('<li>')
|
|
.append($('<div>')
|
|
.addClass('thumb-container')
|
|
.append($('<img>')
|
|
.attr('src', 'device-art-resources/' + this.id + '/thumb.png')
|
|
.attr('height',
|
|
Math.floor(MAX_DISPLAY_HEIGHT * this.physicalHeight / MAX_HEIGHT))))
|
|
.append($('<div>')
|
|
.addClass('device-details')
|
|
.html((this.url
|
|
? ('<a class="device-url" href="' + this.url + '">' + this.title + '</a>')
|
|
: this.title) +
|
|
'<br>' + this.physicalSize + '" @ ' + this.density + 'dpi' +
|
|
'<br>' + this.portSize[0] + 'x' + this.portSize[1]))
|
|
.data('deviceId', this.id)
|
|
.appendTo('#device-list');
|
|
});
|
|
|
|
// Set up drag and drop.
|
|
$('#device-list li')
|
|
.live('dragover', function(evt) {
|
|
$(this).addClass('drag-hover');
|
|
evt.dataTransfer.dropEffect = 'link';
|
|
evt.preventDefault();
|
|
})
|
|
.live('dragleave', function(evt) {
|
|
$(this).removeClass('drag-hover');
|
|
})
|
|
.live('drop', function(evt) {
|
|
$('#output').empty().html(MSG_GENERATING_IMAGE);
|
|
$(this).removeClass('drag-hover');
|
|
g_currentDevice = getDeviceById($(this).closest('li').data('deviceId'));
|
|
evt.preventDefault();
|
|
loadImageFromFileList(evt.dataTransfer.files, function(data) {
|
|
if (data == null) {
|
|
$('#output').html(MSG_INVALID_INPUT_IMAGE);
|
|
return;
|
|
}
|
|
loadImageFromUri(data.uri, function(img) {
|
|
g_currentFilename = data.name;
|
|
g_currentImage = img;
|
|
createFrame();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Set up rotate button.
|
|
$('#rotate-button').click(function() {
|
|
if (!g_currentImage) {
|
|
return;
|
|
}
|
|
|
|
var w = g_currentImage.naturalHeight;
|
|
var h = g_currentImage.naturalWidth;
|
|
var canvas = $('<canvas>')
|
|
.attr('width', w)
|
|
.attr('height', h)
|
|
.get(0);
|
|
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.rotate(-Math.PI / 2);
|
|
ctx.translate(-h, 0);
|
|
ctx.drawImage(g_currentImage, 0, 0);
|
|
|
|
loadImageFromUri(canvas.toDataURL(), function(img) {
|
|
g_currentImage = img;
|
|
createFrame();
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generates the frame from the current selections (g_currentImage and g_currentDevice).
|
|
*/
|
|
function createFrame() {
|
|
var port;
|
|
if (g_currentImage.naturalWidth == g_currentDevice.portSize[0] &&
|
|
g_currentImage.naturalHeight == g_currentDevice.portSize[1]) {
|
|
if (!g_currentDevice.portRes) {
|
|
alert('No portrait frame is currently available for this device.');
|
|
$('#output').html(MSG_NO_INPUT_IMAGE);
|
|
return;
|
|
}
|
|
port = true;
|
|
} else if (g_currentImage.naturalWidth == g_currentDevice.portSize[1] &&
|
|
g_currentImage.naturalHeight == g_currentDevice.portSize[0]) {
|
|
if (!g_currentDevice.landRes) {
|
|
alert('No landscape frame is currently available for this device.');
|
|
$('#output').html(MSG_NO_INPUT_IMAGE);
|
|
return;
|
|
}
|
|
port = false;
|
|
} else {
|
|
alert('Screenshots for ' + g_currentDevice.title + ' must be ' +
|
|
g_currentDevice.portSize[0] + 'x' + g_currentDevice.portSize[1] +
|
|
' or ' +
|
|
g_currentDevice.portSize[1] + 'x' + g_currentDevice.portSize[0] + '.');
|
|
$('#output').html(MSG_INVALID_INPUT_IMAGE);
|
|
return;
|
|
}
|
|
|
|
// Load image resources
|
|
var res = port ? g_currentDevice.portRes : g_currentDevice.landRes;
|
|
var resList = {};
|
|
for (var i = 0; i < res.length; i++) {
|
|
resList[res[i]] = 'device-art-resources/' + g_currentDevice.id + '/' +
|
|
(port ? 'port_' : 'land_') + res[i] + '.png'
|
|
}
|
|
|
|
var resourceImages = {};
|
|
loadImageResources(resList, function(r) {
|
|
resourceImages = r;
|
|
continuation_();
|
|
});
|
|
|
|
function continuation_() {
|
|
var width = resourceImages['back'].naturalWidth;
|
|
var height = resourceImages['back'].naturalHeight;
|
|
var offset = port ? g_currentDevice.portOffset : g_currentDevice.landOffset;
|
|
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
var ctx = canvas.getContext('2d');
|
|
if (resourceImages['shadow'] && $('#output-shadow').is(':checked')) {
|
|
ctx.drawImage(resourceImages['shadow'], 0, 0);
|
|
}
|
|
ctx.drawImage(resourceImages['back'], 0, 0);
|
|
ctx.drawImage(g_currentImage, offset[0], offset[1]);
|
|
if (resourceImages['fore'] && $('#output-glare').is(':checked')) {
|
|
ctx.drawImage(resourceImages['fore'], 0, 0);
|
|
}
|
|
|
|
var dataUrl = canvas.toDataURL();
|
|
var filename = g_currentFilename
|
|
? ('framed_' + g_currentFilename)
|
|
: 'framed_screenshot.png';
|
|
|
|
var $link = $('<a>')
|
|
.attr('download', filename)
|
|
.attr('href', dataUrl)
|
|
.attr('draggable', true)
|
|
.attr('data-downloadurl', ['image/png', filename, dataUrl].join(':'))
|
|
.append($('<img>').attr('src', dataUrl))
|
|
.appendTo($('#output').empty());
|
|
|
|
$('#frame-customizations').show();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads an image from a data URI. The callback will be called with the <img> once
|
|
* it loads.
|
|
*/
|
|
function loadImageFromUri(uri, callback) {
|
|
callback = callback || function(){};
|
|
|
|
var img = document.createElement('img');
|
|
img.src = uri;
|
|
img.onload = function() {
|
|
callback(img);
|
|
};
|
|
img.onerror = function() {
|
|
callback(null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a set of images (organized by ID). Once all images are loaded, the callback
|
|
* is triggered with a dictionary of <img>'s, organized by ID.
|
|
*/
|
|
function loadImageResources(images, callback) {
|
|
var imageResources = {};
|
|
|
|
var checkForCompletion_ = function() {
|
|
for (var id in images) {
|
|
if (!(id in imageResources))
|
|
return;
|
|
}
|
|
(callback || function(){})(imageResources);
|
|
callback = null;
|
|
};
|
|
|
|
for (var id in images) {
|
|
var img = document.createElement('img');
|
|
img.src = images[id];
|
|
(function(img, id) {
|
|
img.onload = function() {
|
|
imageResources[id] = img;
|
|
checkForCompletion_();
|
|
};
|
|
img.onerror = function() {
|
|
imageResources[id] = null;
|
|
checkForCompletion_();
|
|
}
|
|
})(img, id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the first valid image from a FileList (e.g. drag + drop source), as a data URI. This
|
|
* method will throw an alert() in case of errors and call back with null.
|
|
*
|
|
* @param {FileList} fileList The FileList to load.
|
|
* @param {Function} callback The callback to fire once image loading is done (or fails).
|
|
* @return Returns an object containing 'uri' representing the loaded image. There will also be
|
|
* a 'name' field indicating the file name, if one is available.
|
|
*/
|
|
function loadImageFromFileList(fileList, callback) {
|
|
fileList = fileList || [];
|
|
|
|
var file = null;
|
|
for (var i = 0; i < fileList.length; i++) {
|
|
if (fileList[i].type.toLowerCase().match(/^image\/png/)) {
|
|
file = fileList[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!file) {
|
|
alert('Please use a valid screenshot file (PNG format).');
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
var fileReader = new FileReader();
|
|
|
|
// Closure to capture the file information.
|
|
fileReader.onload = function(e) {
|
|
callback({
|
|
uri: e.target.result,
|
|
name: file.name
|
|
});
|
|
};
|
|
fileReader.onerror = function(e) {
|
|
switch(e.target.error.code) {
|
|
case e.target.error.NOT_FOUND_ERR:
|
|
alert('File not found.');
|
|
break;
|
|
case e.target.error.NOT_READABLE_ERR:
|
|
alert('File is not readable.');
|
|
break;
|
|
case e.target.error.ABORT_ERR:
|
|
break; // noop
|
|
default:
|
|
alert('An error occurred reading this file.');
|
|
}
|
|
callback(null);
|
|
};
|
|
fileReader.onabort = function(e) {
|
|
alert('File read cancelled.');
|
|
callback(null);
|
|
};
|
|
|
|
fileReader.readAsDataURL(file);
|
|
}
|
|
</script>
|