Image aspect ratio (#1665)

* Allow user to specify an aspect ratio for various images

* added a comment on what is supported

* fixed typo

* Fixed lint and test errors
This commit is contained in:
randellhodges 2018-09-17 14:16:00 -05:00 committed by Fabian Affolter
parent a32809e14b
commit ce3b53a920
7 changed files with 134 additions and 15 deletions

3
.gitignore vendored
View File

@ -21,6 +21,9 @@ lib
bin
dist
# vscode
.vscode
# Secrets
.lokalise_token
yarn-error.log

View File

@ -0,0 +1,24 @@
export default function parseAspectRatio(input) {
// Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78
// Ignore everything else
function parseOrThrow(number) {
const parsed = parseFloat(number);
if (isNaN(parsed)) throw new Error(`${number} is not a number`);
return parsed;
}
try {
if (input) {
const arr = input.replace(':', 'x').split('x');
if (arr.length === 0) {
return null;
}
return arr.length === 1 ?
{ w: parseOrThrow(arr[0]), h: 1 } :
{ w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) };
}
} catch (err) {
// Ignore the error
}
return null;
}

View File

@ -58,6 +58,7 @@ class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
state-image="[[_config.state_image]]"
camera-image="[[_getCameraImage(_config)]]"
entity="[[_config.entity]]"
aspect-ratio="[[_config.aspect_ratio]]"
></hui-image>
<template is="dom-if" if="[[_showNameAndState(_config)]]">
<div class="footer both">

View File

@ -78,6 +78,7 @@ class HuiPictureGlanceCard extends NavigateMixin(LocalizeMixin(EventsMixin(Polym
state-image="[[_config.state_image]]"
camera-image="[[_config.camera_image]]"
entity="[[_config.entity]]"
aspect-ratio="[[_config.aspect_ratio]]"
></hui-image>
<div class="box">
<template is="dom-if" if="[[_config.title]]">

View File

@ -5,6 +5,8 @@ import '@polymer/paper-toggle-button/paper-toggle-button.js';
import { STATES_OFF } from '../../../common/const.js';
import LocalizeMixin from '../../../mixins/localize-mixin.js';
import parseAspectRatio from '../../../common/util/parse-aspect-ratio.js';
const UPDATE_INTERVAL = 10000;
const DEFAULT_FILTER = 'grayscale(100%)';
@ -13,8 +15,23 @@ const DEFAULT_FILTER = 'grayscale(100%)';
*/
class HuiImage extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
${this.styleTemplate}
<div id="wrapper">
<img
id="image"
src="[[_imageSrc]]"
on-error="_onImageError"
on-load="_onImageLoad" />
<div id="brokenImage"></div>
</div>
`;
}
static get styleTemplate() {
return html`
<style>
img {
display: block;
height: auto;
@ -30,19 +47,26 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
display: none;
}
.ratio {
position: relative;
width: 100%;
height: 0
}
.ratio img, .ratio div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#brokenImage {
background: grey url('/static/images/image-broken.svg') center/36px no-repeat;
}
</style>
<img
id="image"
src="[[_imageSrc]]"
on-error="_onImageError"
on-load="_onImageLoad" />
<div id="brokenImage"></div>
`;
`;
}
static get properties() {
@ -55,6 +79,7 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
image: String,
stateImage: Object,
cameraImage: String,
aspectRatio: String,
filter: String,
stateFilter: Object,
_imageSrc: String
@ -62,7 +87,7 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
}
static get observers() {
return ['_configChanged(image, stateImage, cameraImage)'];
return ['_configChanged(image, stateImage, cameraImage, aspectRatio)'];
}
connectedCallback() {
@ -77,7 +102,14 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
clearInterval(this.timer);
}
_configChanged(image, stateImage, cameraImage) {
_configChanged(image, stateImage, cameraImage, aspectRatio) {
const ratio = parseAspectRatio(aspectRatio);
if (ratio && ratio.w > 0 && ratio.h > 0) {
this.$.wrapper.style.paddingBottom = `${((100 * ratio.h) / ratio.w).toFixed(2)}%`;
this.$.wrapper.classList.add('ratio');
}
if (cameraImage) {
this._updateCameraImageSrc();
} else if (image && !stateImage) {
@ -88,14 +120,18 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
_onImageError() {
this._imageSrc = null;
this.$.image.classList.add('hidden');
this.$.brokenImage.style.setProperty('height', `${this._lastImageHeight || '100'}px`);
if (!this.$.wrapper.classList.contains('ratio')) {
this.$.brokenImage.style.setProperty('height', `${this._lastImageHeight || '100'}px`);
}
this.$.brokenImage.classList.remove('hidden');
}
_onImageLoad() {
this.$.image.classList.remove('hidden');
this.$.brokenImage.classList.add('hidden');
this._lastImageHeight = this.$.image.offsetHeight;
if (!this.$.wrapper.classList.contains('ratio')) {
this._lastImageHeight = this.$.image.offsetHeight;
}
}
_hassChanged(hass) {

View File

@ -13,11 +13,11 @@ class HuiImageElement extends ElementClickMixin(PolymerElement) {
return html`
<style>
:host(.clickable) {
cursor: pointer;
}
cursor: pointer;
}
hui-image {
overflow-y: hidden;
}
}
</style>
<hui-image
hass="[[hass]]"
@ -28,6 +28,7 @@ class HuiImageElement extends ElementClickMixin(PolymerElement) {
filter="[[_config.filter]]"
state-filter="[[_config.state_filter]]"
title$="[[computeTooltip(hass, _config)]]"
aspect-ratio="[[_config.aspect_ratio]]"
></hui-image>
`;
}

View File

@ -0,0 +1,53 @@
import assert from 'assert';
import parseAspectRatio from '../../../src/common/util/parse-aspect-ratio.js';
describe('parseAspectRatio', () => {
const ratio16by9 = { w: 16, h: 9 };
const ratio178 = { w: 1.78, h: 1 };
it('Parses 16x9', () => {
const r = parseAspectRatio('16x9');
assert.deepEqual(r, ratio16by9);
});
it('Parses 16:9', () => {
const r = parseAspectRatio('16:9');
assert.deepEqual(r, ratio16by9);
});
it('Parses 1.78x1', () => {
const r = parseAspectRatio('1.78x1');
assert.deepEqual(r, ratio178);
});
it('Parses 1.78:1', () => {
const r = parseAspectRatio('1.78:1');
assert.deepEqual(r, ratio178);
});
it('Parses 1.78', () => {
const r = parseAspectRatio('1.78');
assert.deepEqual(r, ratio178);
});
it('Skips null states', () => {
const r = parseAspectRatio(null);
assert.equal(r, null);
});
it('Skips empty states', () => {
const r = parseAspectRatio(' ');
assert.equal(r, null);
});
it('Skips invalid input', () => {
const r = parseAspectRatio('mary had a little lamb');
assert.equal(r, null);
});
it('Skips invalid, but close input', () => {
const r = parseAspectRatio('mary:lamb');
assert.equal(r, null);
});
});