blob: b6517b356d3d929d42d43764d2ea43dbb77435c6 [file] [log] [blame]
page.title=Device Art Generator
page.image=/images/device-art-ex-crop.jpg
page.metaDescription=为了更好看的宣传图片和改善视觉语境,拖放你的应用程序的屏幕截图到真实设备的图稿。
@jd:body
<p>你可以使用 Device Art Generator 方便快捷地将应用截图嵌入到真实设备的效果图中。这样,当用户在你的网站上或其他宣传材料中看到你的应用截图时,就能更加直观地了解应用的内容环境</p>
<p class="note"><strong>注意</strong>:请勿将此处生成的图片用作 Google Play 应用商品详情中的 1024x500 置顶大图或屏幕截图</p>
<div class="supported-browser">
<div class="layout-content-row">
<div class="layout-content-col span-3">
<h4> 1 步</h4>
<p>将屏幕截图从桌面拖动到右侧的设备上生成效果图。</p>
</div>
<div class="layout-content-col span-10">
<ul class="device-list primary"></ul>
<a href="#" id="archive-expando">旧款设备</a>
<ul class="device-list archive"></ul>
</div>
</div>
<div class="layout-content-row">
<div class="layout-content-col span-3">
<h4> 2 步</h4>
<p>你可以对此效果图进行优化,然后将其拖动到桌面上保存。</p>
<p id="frame-customizations">
<input type="checkbox" id="output-shadow" checked="checked" class="form-field-checkbutton">
<label for="output-shadow">阴影</label><br>
<input type="checkbox" id="output-glare" checked="checked" class="form-field-checkbutton">
<label for="output-glare">屏幕反光</label><br><br>
<a class="button" id="rotate-button">旋转</a>
</p>
</div>
<div class="layout-content-col span-10">
<!-- position:relative fixes an issue where dragging an image out of a inline-block container
produced no drag feedback image in Chrome 28. -->
<div id="output" style="position:relative">无输入图片。</div>
</div>
</div>
</div>
<div class="unsupported-browser" style="display: none">
<p class="warning"><strong>错误</strong>:此页面需要使用<span id="unsupported-browser-reason">特定功能</span>才能打开,但你的网络浏览器不支持这些功能。要继续,请在受支持的网络浏览器(如 <strong>Google Chrome</strong>)中打开此页面。</p>
<a href="https://www.google.com/chrome/" class="button">下载 Google Chrome</a>
<br><br>
</div>
<style>
h4 {
text-transform: uppercase;
}
.device-list {
padding: 1em 0 0 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;
}
#archive-expando {
display: block;
font-size: 13px;
font-weight: bold;
color: #333;
text-transform: uppercase;
margin-top: 16px;
padding-top: 16px;
padding-left: 28px;
border-top: 1px solid transparent;
background: transparent url({@docRoot}assets/images/styles/disclosure_down.png)
no-repeat scroll 0 8px;
-webkit-transition: border 0.2s;
-moz-transition: border 0.2s;
transition: border 0.2s;
}
#archive-expando.expanded {
background-image: url({@docRoot}assets/images/styles/disclosure_up.png);
border-top: 1px solid #ccc;
}
.device-list.archive {
max-height: 0;
overflow: hidden;
opacity: 0;
-webkit-transition: max-height 0.2s, opacity 0.2s;
-moz-transition: max-height 0.2s, opacity 0.2s;
transition: max-height 0.2s, opacity 0.2s;
}
.device-list.archive.expanded {
opacity: 1;
max-height: 300px;
}
#output {
color: #f44;
font-style: italic;
}
#output img {
max-height: 500px;
}
</style>
<script>
// Global variables
var g_currentImage;
var g_currentDevice;
var g_currentObjectURL;
var g_currentBlob;
// Global constants
var MSG_INVALID_INPUT_IMAGE = 'Invalid screenshot provided. Screenshots must be PNG files '
+ 'matching the target device\'s screen aspect ratio in either portrait or landscape.';
var MSG_NO_INPUT_IMAGE = '将屏幕截图(.PNG)从桌面拖动到以上所列设备。';
var MSG_GENERATING_IMAGE = 'Generating device art&hellip;';
var MAX_DISPLAY_HEIGHT = 126; // XOOM, to fit into 200px wide
// Device manifest.
var DEVICES = [
{
id: 'nexus_5',
title: 'Nexus 5',
url: 'http://www.google.com/nexus/5/',
physicalSize: 5,
physicalHeight: 5.43,
density: 'XXHDPI',
landRes: ['shadow', 'back', 'fore'],
landOffset: [436,306],
portRes: ['shadow', 'back', 'fore'],
portOffset: [304,436],
portSize: [1080,1920],
},
{
id: 'nexus_6',
title: 'Nexus 6',
url: 'http://www.google.com/nexus/6/',
physicalSize: 6,
physicalHeight: 6.27,
density: '560DPI',
landRes: ['shadow', 'back', 'fore'],
landOffset: [489,327],
portRes: ['shadow', 'back', 'fore'],
portOffset: [327,489],
portSize: [1440, 2560],
},
{
id: 'nexus_7',
title: 'Nexus 7',
url: 'http://www.google.com/nexus/7/',
physicalSize: 7,
physicalHeight: 8,
actualResolution: [1200,1920],
density: 'XHDPI',
landRes: ['shadow', 'back', 'fore'],
landOffset: [326,245],
portRes: ['shadow', 'back', 'fore'],
portOffset: [244,326],
portSize: [800,1280]
},
{
id: 'nexus_9',
title: 'Nexus 9',
url: 'http://www.google.com/nexus/9/',
physicalSize: 9,
physicalHeight: 8.98,
actualResolution: [1536,2048],
density: 'XHDPI',
landRes: ['shadow', 'back', 'fore'],
landOffset: [514,350],
portRes: ['shadow', 'back', 'fore'],
portOffset: [348,514],
portSize: [1536,2048],
},
{
id: 'nexus_10',
title: 'Nexus 10',
url: 'http://www.google.com/nexus/10/',
physicalSize: 10,
physicalHeight: 7,
actualResolution: [1600,2560],
density: 'XHDPI',
landRes: ['shadow', 'back', 'fore'],
landOffset: [227,217],
portRes: ['shadow', 'back', 'fore'],
portOffset: [217,223],
portSize: [800,1280],
archived: true
},
{
id: 'nexus_7_2012',
title: 'Nexus 7 (2012)',
url: 'http://www.google.com/nexus/7/',
physicalSize: 7,
physicalHeight: 7.81,
density: '213dpi',
landRes: ['shadow', 'back', 'fore'],
landOffset: [315,270],
portRes: ['shadow', 'back', 'fore'],
portOffset: [264,311],
portSize: [800,1280],
archived: true
},
{
id: 'nexus_4',
title: 'Nexus 4',
url: 'http://www.google.com/nexus/4/',
physicalSize: 4.7,
physicalHeight: 5.27,
density: 'XHDPI',
landRes: ['shadow', 'back', 'fore'],
landOffset: [349,214],
portRes: ['shadow', 'back', 'fore'],
portOffset: [213,350],
portSize: [768,1280],
archived: true
},
];
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;
}
polyfillCanvasToBlob();
setupUI();
// Set up Chrome drag-out
$.event.props.push("dataTransfer");
document.body.addEventListener('dragstart', function(e) {
var target = e.target;
if (target.classList.contains('dragout')) {
e.dataTransfer.setData('DownloadURL', target.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();
});
// Build device list.
$.each(DEVICES, function() {
var resolution = this.actualResolution || this.portSize;
var scaleFactorText = '';
if (resolution[0] != this.portSize[0]) {
scaleFactorText = '<br>等比例缩小至' + (100 * (this.portSize[0] / resolution[0])).toFixed(0) +
'% 输出';
} else {
scaleFactorText = '<br>&nbsp;';
}
$('<li>')
.append($('<div>')
.addClass('thumb-container')
.append($('<img>')
.attr('src', '../../../../../distribute/tools/promote/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 +
'<br>' + (resolution[0] + 'x' + resolution[1]) + scaleFactorText))
.data('deviceId', this.id)
.appendTo(this.archived ? '.device-list.archive' : '.device-list.primary');
});
// Set up "older devices" expando.
$('#archive-expando').click(function() {
if ($(this).hasClass('expanded')) {
$(this).removeClass('expanded');
$('.device-list.archive').removeClass('expanded');
} else {
$(this).addClass('expanded');
$('.device-list.archive').addClass('expanded');
}
return false;
});
// 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();
// Send the event to Analytics
ga('send', 'event', 'Distribute', 'Create Device Art', g_currentDevice.title);
});
});
});
// 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('image/png'), function(img) {
g_currentImage = img;
createFrame();
});
});
}
/**
* Generates the frame from the current selections (g_currentImage and g_currentDevice).
*/
function createFrame() {
var port;
var aspect1 = g_currentImage.naturalWidth / g_currentImage.naturalHeight;
var aspect2 = g_currentDevice.portSize[0] / g_currentDevice.portSize[1];
if (aspect1 == aspect2) {
port = true;
} else if (aspect1 == 1 / aspect2) {
port = false;
} else {
alert('The screenshot must have an aspect ratio of ' +
aspect2.toFixed(3) + ' or ' + (1 / aspect2).toFixed(3) +
' (ideally ' + 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]] = '../../../../../distribute/tools/promote/device-art-resources/' + g_currentDevice.id + '/' +
(port ? 'port_' : 'land_') + res[i] + '.png'
}
var resourceImages = {};
loadImageResources(resList, function(r) {
resourceImages = r;
continueWithResources_();
});
function continueWithResources_() {
var width = resourceImages['back'].naturalWidth;
var height = resourceImages['back'].naturalHeight;
var offset = port ? g_currentDevice.portOffset : g_currentDevice.landOffset;
var size = port
? g_currentDevice.portSize
: [g_currentDevice.portSize[1], g_currentDevice.portSize[0]];
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.fillStyle = '#000';
ctx.fillRect(offset[0], offset[1], size[0], size[1]);
ctx.drawImage(g_currentImage, offset[0], offset[1], size[0], size[1]);
if (resourceImages['fore'] && $('#output-glare').is(':checked')) {
ctx.drawImage(resourceImages['fore'], 0, 0);
}
window.URL = window.URL || window.webkitURL;
if (canvas.toBlob && window.URL.createObjectURL) {
if (g_currentObjectURL) {
window.URL.revokeObjectURL(g_currentObjectURL);
g_currentObjectURL = null;
}
if (g_currentBlob) {
if (g_currentBlob.close) {
g_currentBlob.close();
}
g_currentBlob = null;
}
canvas.toBlob(function(blob) {
if (!blob) {
continueWithFinalUrl_(canvas.toDataURL('image/png'));
return;
}
g_currentBlob = blob;
g_currentObjectURL = window.URL.createObjectURL(blob);
continueWithFinalUrl_(g_currentObjectURL);
}, 'image/png');
} else {
continueWithFinalUrl_(canvas.toDataURL('image/png'));
}
}
function continueWithFinalUrl_(imageUrl) {
var filename = g_currentFilename
? g_currentFilename.replace(/^(.+?)(\.\w+)?$/, '$1_framed.png')
: 'framed_screenshot.png';
var $link = $('<a>')
.attr('download', filename)
.attr('href', imageUrl)
.append($('<img>')
.addClass('dragout')
.attr('src', imageUrl)
.attr('draggable', true)
.attr('data-downloadurl', ['image/png', filename, imageUrl].join(':')))
.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|jpeg|jpg)/)) {
file = fileList[i];
break;
}
}
if (!file) {
alert('Please use a valid screenshot file (PNG or JPEG 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);
}
/**
* Adds a simple version of Canvas.toBlob if toBlob isn't available.
*/
function polyfillCanvasToBlob() {
if (!HTMLCanvasElement.prototype.toBlob && window.Blob) {
HTMLCanvasElement.prototype.toBlob = function(callback, mimeType, quality) {
if (typeof callback != 'function') {
throw new TypeError('Function expected');
}
var dataURL = this.toDataURL(mimeType, quality);
mimeType = dataURL.split(';')[0].split(':')[1];
var bs = window.atob(dataURL.split(',')[1]);
if (dataURL == 'data:,' || !bs.length) {
callback(null);
return;
}
for (var ui8arr = new Uint8Array(bs.length), i = 0; i < bs.length; ++i) {
ui8arr[i] = bs.charCodeAt(i);
}
callback(new Blob([ui8arr.buffer /* req'd for Safari */ || ui8arr], {type: mimeType}));
};
}
}
</script>