1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-23 16:25:22 +03:00

improved logo customization UI

This commit is contained in:
Kiripolszky Károly 2018-03-18 01:05:53 +01:00
parent fd83241379
commit 9d9a395f6b
4 changed files with 318 additions and 121 deletions

View file

@ -2786,19 +2786,28 @@
"osdSetupOpenFont": {
"message": "Open Font File"
},
"osdSetupLogo": {
"message": "Logo in font:"
"osdSetupCustomLogoTitle": {
"message": "Boot logo:"
},
"osdSetupReplaceLogo": {
"message": "Replace Logo"
"osdSetupCustomLogoOpenImageButton": {
"message": "Select custom image…"
},
"osdSetupReplaceLogoHelp": {
"message": "Customized logo image has to be 288×72 pixels in size containing black and white pixels only on a completely green background."
"osdSetupCustomLogoInfoTitle": {
"message": "Custom image:"
},
"osdSetupReplaceLogoImageSizeError": {
"message": "Invalid image size; expected $1×$2 instead of $3×$4"
"osdSetupCustomLogoInfoImageSize": {
"message": "Size must be $t(logoWidthPx)×$t(logoHeightPx) pixels"
},
"osdSetupReplaceLogoImageColorsError": {
"osdSetupCustomLogoInfoColorMap": {
"message": "Must contain green, black and white pixels"
},
"osdSetupCustomLogoInfoUploadHint": {
"message": "Click <b>$t(osdSetupUploadFont.message)</b> to persist custom logo"
},
"osdSetupCustomLogoImageSizeError": {
"message": "Invalid image size: $1×$2 (expected $t(logoWidthPx)×$t(logoHeightPx))"
},
"osdSetupCustomLogoColorMapError": {
"message": "The image contains an invalid color palette (only green, black and white are allowed)"
},
"osdSetupUploadFont": {

View file

@ -336,11 +336,42 @@
#font-logo-preview-container {
background:rgba(0, 255, 0, 0.4);
margin-bottom: 10px;
width: 50%;
float: left;
}
#font-logo-preview {
background:rgba(0, 255, 0, 1);
line-height: 0;
margin: auto;
}
#font-logo-info {
margin-left: 18px;
font-size: 125%;
line-height: 150%;
float: left;
}
#font-logo-info h3 {
margin-bottom: 0.2em;
}
#font-logo-info ul li:before {
content: '• ';
}
#font-logo-info ul li.valid:before {
content: '✔ ';
}
#font-logo-info ul li.invalid:before {
content: '🞨 ';
}
#font-logo-info #font-logo-info-upload-hint {
margin-top: 1em;
display: none;
}
.tab-osd .content_wrapper {

View file

@ -76,20 +76,6 @@ FONT.constants = {
// white
2: 'rgba(255,255,255, 1)'
},
LOGO: {
TILES_NUM_HORIZ: 24,
TILES_NUM_VERT: 4,
MCM_COLORMAP: {
// background
'0-255-0': '01',
// black
'0-0-0': '00',
// white
'255-255-255': '10',
// fallback
'default': '01',
},
},
};
/**
@ -101,6 +87,8 @@ FONT.parseMCMFontFile = function(data) {
FONT.data.characters.length = 0;
FONT.data.characters_bytes.length = 0;
FONT.data.character_image_urls.length = 0;
// reset logo image info when font data is changed
LogoManager.resetImageInfo();
// make sure the font file is valid
if (data.shift().trim() != 'MAX7456') {
var msg = 'that font file doesnt have the MAX7456 header, giving up';
@ -163,74 +151,25 @@ FONT.openFontFile = function($preview) {
});
};
// show a file open dialog and yield an Image object
var openLogoImage = function() {
return new Promise(function(resolve, reject) {
var validateImage = function(img) {
return new Promise(function(resolve, reject) {
var expectedWidth = FONT.constants.SIZES.CHAR_WIDTH
* FONT.constants.LOGO.TILES_NUM_HORIZ,
expectedHeight = FONT.constants.SIZES.CHAR_HEIGHT
* FONT.constants.LOGO.TILES_NUM_VERT;
if (img.width != expectedWidth || img.height != expectedHeight) {
reject(i18n.getMessage("osdSetupReplaceLogoImageSizeError",
[expectedWidth, expectedHeight, img.width, img.height]));
return;
}
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
for (var y = 0, Y = canvas.height; y < Y; y++) {
for (var x = 0, X = canvas.width; x < X; x++) {
var rgbPixel = ctx.getImageData(x, y, 1, 1).data.slice(0, 3),
colorKey = rgbPixel.join("-");
if (!FONT.constants.LOGO.MCM_COLORMAP[colorKey]) {
reject(i18n.getMessage("osdSetupReplaceLogoImageColorsError"));
return;
}
}
}
resolve();
});
};
chrome.fileSystem.chooseEntry({ type: 'openFile', accepts: [{ extensions: ['png', 'bmp'] }] }, function(fileEntry) {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
return;
}
var img = new Image();
img.onload = function() {
validateImage(img).then(function() {
resolve(img);
}).catch(function(error) {
console.error(error);
reject(error);
});
};
img.onerror = function(error) {
reject(error);
};
fileEntry.file(function(file) {
img.src = "file://" + file.path;
});
});
});
};
// replaces the logo in the font based on an Image object
/**
* Replaces the logo in the loaded font based on an image.
*
* @param {HTMLImageElement} img
*/
FONT.replaceLogoFromImage = function(img) {
// takes image data from an ImageData object and returns an MCM symbol as an array of strings
/**
* Takes an ImageData object and returns an MCM symbol as an array of strings.
*
* @param {ImageData} data
*/
var imageToCharacter = function(data) {
var char = [],
line = "";
for (var i = 0, I = data.length; i < I; i += 4) {
var rgbPixel = data.slice(i, i + 3),
colorKey = rgbPixel.join("-");
line += FONT.constants.LOGO.MCM_COLORMAP[colorKey]
|| FONT.constants.LOGO.MCM_COLORMAP['default'];
line += LogoManager.constants.MCM_COLORMAP[colorKey]
|| LogoManager.constants.MCM_COLORMAP['default'];
if (line.length == 8) {
char.push(line);
line = "";
@ -238,13 +177,12 @@ FONT.replaceLogoFromImage = function(img) {
}
var fieldSize = FONT.constants.SIZES.MAX_NVM_FONT_CHAR_FIELD_SIZE;
if (char.length < fieldSize) {
var pad = FONT.constants.LOGO.MCM_COLORMAP['default'].repeat(4);
var pad = LogoManager.constants.MCM_COLORMAP['default'].repeat(4);
for (var i = 0, I = fieldSize - char.length; i < I; i++)
char.push(pad);
}
return char;
};
// takes an OSD symbol as an array of strings and replaces the in-memory character at charAddress with it
var replaceChar = function(lines, charAddress) {
var characterBits = [];
@ -262,7 +200,6 @@ FONT.replaceLogoFromImage = function(img) {
FONT.data.character_image_urls[charAddress] = null;
FONT.draw(charAddress);
};
// loop through an image and replace font symbols
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
@ -270,8 +207,8 @@ FONT.replaceLogoFromImage = function(img) {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
for (var y = 0; y < FONT.constants.LOGO.TILES_NUM_VERT; y++) {
for (var x = 0; x < FONT.constants.LOGO.TILES_NUM_HORIZ; x++) {
for (var y = 0; y < LogoManager.constants.TILES_NUM_VERT; y++) {
for (var x = 0; x < LogoManager.constants.TILES_NUM_HORIZ; x++) {
var imageData = ctx.getImageData(
x * FONT.constants.SIZES.CHAR_WIDTH,
y * FONT.constants.SIZES.CHAR_HEIGHT,
@ -285,6 +222,220 @@ FONT.replaceLogoFromImage = function(img) {
}
};
var LogoManager = LogoManager || {
// DOM elements to cache
elements: {
$preview: "#font-logo-preview",
$uploadHint: "#font-logo-info-upload-hint",
},
constants: {
TILES_NUM_HORIZ: 24,
TILES_NUM_VERT: 4,
MCM_COLORMAP: {
// background
'0-255-0': '01',
// black
'0-0-0': '00',
// white
'255-255-255': '10',
// fallback
'default': '01',
},
},
// config for logo image selection dialog
acceptFileTypes: [
{ extensions: ['png', 'bmp'] },
],
};
// custom logo image constraints
LogoManager.constraints = {
// test for image size
imageSize: {
$el: "#font-logo-info-size",
// calculate logo image size at runtime as it may change conditionally in the future
expectedWidth: FONT.constants.SIZES.CHAR_WIDTH
* LogoManager.constants.TILES_NUM_HORIZ,
expectedHeight: FONT.constants.SIZES.CHAR_HEIGHT
* LogoManager.constants.TILES_NUM_VERT,
/**
* @param {HTMLImageElement} img
*/
test: function(img) {
var constraint = LogoManager.constraints.imageSize;
if (img.width != constraint.expectedWidth
|| img.height != constraint.expectedHeight) {
GUI.log(i18n.getMessage("osdSetupCustomLogoImageSizeError", [
img.width,
img.height,
]));
return false;
}
return true;
},
},
// test for pixel colors
colorMap: {
$el: "#font-logo-info-colors",
/**
* @param {HTMLImageElement} img
*/
test: function(img) {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
for (var y = 0, Y = canvas.height; y < Y; y++) {
for (var x = 0, X = canvas.width; x < X; x++) {
var rgbPixel = ctx.getImageData(x, y, 1, 1).data.slice(0, 3),
colorKey = rgbPixel.join("-");
if (!LogoManager.constants.MCM_COLORMAP[colorKey]) {
GUI.log(i18n.getMessage("osdSetupCustomLogoColorMapError"));
return false;
}
}
}
return true;
},
},
};
LogoManager.resetImageInfo = function() {
LogoManager.hideUploadHint();
Object.values(LogoManager.constraints).forEach(function(constraint) {
var $el = constraint.$el;
$el.toggleClass("message-negative", false);
$el.toggleClass("invalid", false);
$el.toggleClass("message-positive", false);
$el.toggleClass("valid", false);
});
};
LogoManager.showConstraintNotSatisfied = function(constraint) {
constraint.$el.toggleClass("message-negative", true);
constraint.$el.toggleClass("invalid", true);
};
LogoManager.showConstraintSatisfied = function(constraint) {
constraint.$el.toggleClass("message-positive", true);
constraint.$el.toggleClass("valid", true);
};
LogoManager.showUploadHint = function() {
LogoManager.elements.$uploadHint.show();
};
LogoManager.hideUploadHint = function() {
LogoManager.elements.$uploadHint.hide();
};
/**
* Show a file open dialog and resolve to an Image object.
*
* @returns {Promise}
*/
LogoManager.openImage = function() {
return new Promise(function(resolve, reject) {
/**
* Validate image using defined constraints and display results on the UI.
*
* @param {HTMLImageElement} img
*/
var validateImage = function(img) {
return new Promise(function(resolve, reject) {
LogoManager.resetImageInfo();
for (var key in LogoManager.constraints) {
if (!LogoManager.constraints.hasOwnProperty(key)) {
continue;
}
var constraint = LogoManager.constraints[key],
satisfied = constraint.test(img);
if (satisfied) {
LogoManager.showConstraintSatisfied(constraint);
} else {
LogoManager.showConstraintNotSatisfied(constraint);
reject();
return;
}
}
resolve();
});
},
dialogOptions = {
type: 'openFile',
accepts: LogoManager.acceptFileTypes
};
chrome.fileSystem.chooseEntry(dialogOptions, function(fileEntry) {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
return;
}
// load and validate selected image
var img = new Image();
img.onload = function() {
validateImage(img).then(function() {
resolve(img);
}).catch(function(error) {
reject(error);
});
};
img.onerror = function(error) {
reject(error);
};
fileEntry.file(function(file) {
img.src = "file://" + file.path;
});
});
});
};
/**
* Draw the logo using the loaded font data.
*/
LogoManager.drawPreview = function() {
var $el = LogoManager.elements.$preview;
$el.empty();
for (var i = SYM.LOGO, I = FONT.constants.MAX_CHAR_COUNT; i < I; i++) {
var url = FONT.data.character_image_urls[i];
$el.append('<img src="' + url + '" title="0x' + i.toString(16) + '"></img>');
}
};
/**
* Initialize Logo Manager UI.
*/
LogoManager.init = function() {
// cache DOM elements
Object.keys(LogoManager.elements).forEach(function(key) {
LogoManager.elements[key] = $(LogoManager.elements[key]);
});
Object.keys(LogoManager.constraints).forEach(function(key) {
LogoManager.constraints[key].$el = $(LogoManager.constraints[key].$el);
});
// resize logo preview area to match tile size
var logoWidthPx = LogoManager.constraints.imageSize.expectedWidth,
logoHeightPx = LogoManager.constraints.imageSize.expectedHeight;
LogoManager.elements.$preview
.width(logoWidthPx)
.height(logoHeightPx);
// inject logo size variables for dynamic translation strings
var takeFirst = obj => {
if (obj.hasOwnProperty("length") && 0 < obj.length) {
return obj[0];
} else {
return obj;
}
};
var lang = takeFirst(i18next.options.fallbackLng),
ns = takeFirst(i18next.options.defaultNS);
i18next.addResourceBundle(lang, ns, {
logoWidthPx: logoWidthPx,
logoHeightPx: logoHeightPx,
}, true, true);
};
/**
* returns a canvas image with the character on it
*/
@ -346,16 +497,6 @@ FONT.preview = function($el) {
}
};
FONT.logoPreview = function($el) {
$el.empty()
.width(FONT.constants.LOGO.TILES_NUM_HORIZ * FONT.constants.SIZES.CHAR_WIDTH)
.height(FONT.constants.LOGO.TILES_NUM_VERT * FONT.constants.SIZES.CHAR_HEIGHT);
for (var i = SYM.LOGO, I = FONT.constants.MAX_CHAR_COUNT; i < I; i++) {
var url = FONT.data.character_image_urls[i];
$el.append('<img src="' + url + '" title="0x' + i.toString(16) + '"></img>');
}
};
FONT.symbol = function(hexVal) {
return String.fromCharCode(hexVal);
};
@ -1372,6 +1513,9 @@ TABS.osd.initialize = function (callback) {
fontbuttons.append($('<button>', { class: "load_font_file", i18n: "osdSetupOpenFont" }));
// must invoke before localizePage() since it adds translation keys for expected logo size
LogoManager.init();
// translate to user-selected language
i18n.localizePage();
@ -1383,7 +1527,10 @@ TABS.osd.initialize = function (callback) {
animation: false,
attach: $('#fontmanager'),
title: 'OSD Font Manager',
content: $('#fontmanagercontent')
content: $('#fontmanagercontent'),
onOpen: function() {
LogoManager.resetImageInfo();
},
});
$('.elements-container div.cf_tip').attr('title', i18n.getMessage('osdSectionHelpElements'));
@ -1833,10 +1980,9 @@ TABS.osd.initialize = function (callback) {
});
// font preview window
var $preview = $('.font-preview'),
$logoPreview = $('#font-logo-preview');
var $preview = $('.font-preview');
// init structs once, also clears current font
// init structs once, also clears current font
FONT.initData();
var $fontPicker = $('.fontbuttons button');
@ -1847,7 +1993,7 @@ TABS.osd.initialize = function (callback) {
$.get('./resources/osd/' + $(this).data('font-file') + '.mcm', function(data) {
FONT.parseMCMFontFile(data);
FONT.preview($preview);
FONT.logoPreview($logoPreview);
LogoManager.drawPreview();
updateOsdView();
});
});
@ -1859,9 +2005,9 @@ TABS.osd.initialize = function (callback) {
$fontPicker.removeClass('active');
FONT.openFontFile().then(function() {
FONT.preview($preview);
FONT.logoPreview($logoPreview);
LogoManager.drawPreview();
updateOsdView();
});
}).catch(error => console.error(error));
});
// font upload
@ -1882,13 +2028,11 @@ TABS.osd.initialize = function (callback) {
if (GUI.connect_lock) { // button disabled while flashing is in progress
return;
}
openLogoImage().then(function(ctx) {
LogoManager.openImage().then(function(ctx) {
FONT.replaceLogoFromImage(ctx);
FONT.logoPreview($logoPreview);
}).catch(function(error) {
console.error("error loading image:", error);
GUI.log(error);
});
LogoManager.drawPreview();
LogoManager.showUploadHint();
}).catch(error => console.error(error));
});
//Switch all elements

View file

@ -117,35 +117,48 @@
</div>
</div>
</div>
<!-- Font Manager dialog -->
<div id="fontmanagercontent" style="display:none; width:720px;">
<div class="font-picker" style="margin-bottom: 10px;">
<h1 class="tab_title" i18n="osdSetupFontPresets" />
<!-- Font preview and list -->
<div class="content_wrapper font-preview"></div>
<div class="fontbuttons">
</div>
<h1 class="tab_title" i18n="osdSetupLogo" />
<div class="helpicon cf_tip" i18n_title="osdSetupReplaceLogoHelp"></div>
<div class="fontbuttons"></div>
<!-- Boot logo setup -->
<h1 class="tab_title" i18n="osdSetupCustomLogoTitle" />
<div id="font-logo-preview-container" class="content_wrapper">
<div id="font-logo-preview">
<!-- this will be resized at runtime -->
</div>
</div>
<div class="default_btn" style="width:100%; float:left;
">
<a class="replace_logo" i18n="osdSetupReplaceLogo" />
<div id="font-logo-info">
<h3 i18n="osdSetupCustomLogoInfoTitle"></h3>
<ul>
<li id="font-logo-info-size" i18n="osdSetupCustomLogoInfoImageSize"></li>
<li id="font-logo-info-colors" i18n="osdSetupCustomLogoInfoColorMap"></li>
</ul>
<p id="font-logo-info-upload-hint" i18n="osdSetupCustomLogoInfoUploadHint"></p>
</div>
<div class="clear-both"></div>
<!-- Replace logo button -->
<div class="default_btn" style="width:100%; float:left;">
<a class="replace_logo" i18n="osdSetupCustomLogoOpenImageButton" />
</div>
<!-- Upload progress bar -->
<div class="info">
<a name="progressbar"></a>
<progress class="progress" value="0" min="0" max="100"></progress>
<div class="progressLabel" style="margin-top: -21px; width: 95%; text-align: center; color: white; position: absolute;"></div>
</div>
</div>
<!-- Upload button -->
<div class="default_btn green" style="width:100%; float:left;
">
<a class="flash_font active" i18n="osdSetupUploadFont" />
</div>
</div>
<div class="content_toolbar supported hide" style="left:0;">
<div class="btn save">
<a class="active save" href="#" i18n="osdSetupSave" />