mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 17:56:46 +00:00
Improve color extraction for media control card (#5189)
* Improve color extraction * Lazy load media control * Update src/panels/lovelace/cards/hui-media-control-card.ts Co-Authored-By: Bram Kragten <mail@bramkragten.nl> * Extract constant * Fix media control demo * Remove quality(1) Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
0c3c007faf
commit
3c17ee03b6
BIN
gallery/public/images/album_cover_2.jpg
Normal file
BIN
gallery/public/images/album_cover_2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
@ -7,7 +7,7 @@ export const createMediaPlayerEntities = () => [
|
|||||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||||
media_artist: "Technohead",
|
media_artist: "Technohead",
|
||||||
supported_features: 64063,
|
supported_features: 64063,
|
||||||
entity_picture: "/images/album_cover.jpg",
|
entity_picture: "/images/album_cover_2.jpg",
|
||||||
media_duration: 300,
|
media_duration: 300,
|
||||||
media_position: 50,
|
media_position: 50,
|
||||||
media_position_updated_at: new Date(
|
media_position_updated_at: new Date(
|
||||||
|
@ -4,6 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|||||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||||
import "../components/demo-cards";
|
import "../components/demo-cards";
|
||||||
import { createMediaPlayerEntities } from "../data/media_players";
|
import { createMediaPlayerEntities } from "../data/media_players";
|
||||||
|
import "../../../src/panels/lovelace/cards/hui-media-control-card";
|
||||||
|
|
||||||
const CONFIGS = [
|
const CONFIGS = [
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@ export const SUPPORT_SELECT_SOURCE = 2048;
|
|||||||
export const SUPPORT_STOP = 4096;
|
export const SUPPORT_STOP = 4096;
|
||||||
export const SUPPORTS_PLAY = 16384;
|
export const SUPPORTS_PLAY = 16384;
|
||||||
export const SUPPORT_SELECT_SOUND_MODE = 65536;
|
export const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||||
export const CONTRAST_RATIO = 3.5;
|
export const CONTRAST_RATIO = 4.5;
|
||||||
|
|
||||||
export interface MediaPlayerThumbnail {
|
export interface MediaPlayerThumbnail {
|
||||||
content_type: string;
|
content_type: string;
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
import { styleMap } from "lit-html/directives/style-map";
|
import { styleMap } from "lit-html/directives/style-map";
|
||||||
import Vibrant from "node-vibrant";
|
import Vibrant from "node-vibrant";
|
||||||
import { Palette } from "node-vibrant/lib/color";
|
import { Swatch } from "node-vibrant/lib/color";
|
||||||
import "@polymer/paper-icon-button/paper-icon-button";
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
import "@polymer/paper-progress/paper-progress";
|
import "@polymer/paper-progress/paper-progress";
|
||||||
// tslint:disable-next-line: no-duplicate-imports
|
// tslint:disable-next-line: no-duplicate-imports
|
||||||
@ -59,6 +59,107 @@ function getContrastRatio(
|
|||||||
return Math.round((contrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
|
return Math.round((contrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How much the total diff between 2 RGB colors can be
|
||||||
|
// to be considered similar.
|
||||||
|
const COLOR_SIMILARITY_THRESHOLD = 150;
|
||||||
|
|
||||||
|
// For debug purposes, is being tree shaken.
|
||||||
|
const DEBUG_COLOR = __DEV__ && false;
|
||||||
|
|
||||||
|
const logColor = (
|
||||||
|
color: Swatch,
|
||||||
|
label: string = `${color.getHex()} - ${color.getPopulation()}`
|
||||||
|
) =>
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.log(
|
||||||
|
`%c${label}`,
|
||||||
|
`color: ${color.getBodyTextColor()}; background-color: ${color.getHex()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const customGenerator = (colors: Swatch[]) => {
|
||||||
|
colors.sort((colorA, colorB) => colorB.population - colorA.population);
|
||||||
|
|
||||||
|
const backgroundColor = colors[0];
|
||||||
|
let foregroundColor: string | undefined;
|
||||||
|
|
||||||
|
const contrastRatios = new Map<Swatch, number>();
|
||||||
|
const approvedContrastRatio = (color: Swatch) => {
|
||||||
|
if (!contrastRatios.has(color)) {
|
||||||
|
contrastRatios.set(
|
||||||
|
color,
|
||||||
|
getContrastRatio(backgroundColor.rgb, color.rgb)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return contrastRatios.get(color)! > CONTRAST_RATIO;
|
||||||
|
};
|
||||||
|
|
||||||
|
// We take each next color and find one that has better contrast.
|
||||||
|
for (let i = 1; i < colors.length && foregroundColor === undefined; i++) {
|
||||||
|
// If this color matches, score, take it.
|
||||||
|
if (approvedContrastRatio(colors[i])) {
|
||||||
|
if (DEBUG_COLOR) {
|
||||||
|
logColor(colors[i], "PICKED");
|
||||||
|
}
|
||||||
|
foregroundColor = colors[i].hex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This color has the wrong contrast ratio, but it is the right color.
|
||||||
|
// Let's find similar colors that might have the right contrast ratio
|
||||||
|
|
||||||
|
const currentColor = colors[i];
|
||||||
|
if (DEBUG_COLOR) {
|
||||||
|
logColor(colors[i], "Finding similar color with better contrast");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = i + 1; j < colors.length; j++) {
|
||||||
|
const compareColor = colors[j];
|
||||||
|
|
||||||
|
// difference. 0 is same, 765 max difference
|
||||||
|
const diffScore =
|
||||||
|
Math.abs(currentColor.rgb[0] - compareColor.rgb[0]) +
|
||||||
|
Math.abs(currentColor.rgb[1] - compareColor.rgb[1]) +
|
||||||
|
Math.abs(currentColor.rgb[2] - compareColor.rgb[2]);
|
||||||
|
|
||||||
|
if (DEBUG_COLOR) {
|
||||||
|
logColor(colors[j], `${colors[j].hex} - ${diffScore}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diffScore > COLOR_SIMILARITY_THRESHOLD) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approvedContrastRatio(compareColor)) {
|
||||||
|
if (DEBUG_COLOR) {
|
||||||
|
logColor(compareColor, "PICKED");
|
||||||
|
}
|
||||||
|
foregroundColor = compareColor.hex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foregroundColor === undefined) {
|
||||||
|
foregroundColor = backgroundColor.bodyTextColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG_COLOR) {
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.log();
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.log(
|
||||||
|
"%cPicked colors",
|
||||||
|
`color: ${foregroundColor}; background-color: ${backgroundColor.hex}; font-weight: bold; padding: 16px;`
|
||||||
|
);
|
||||||
|
colors.forEach((color) => logColor(color));
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [foregroundColor, backgroundColor.hex];
|
||||||
|
};
|
||||||
|
|
||||||
interface ControlButton {
|
interface ControlButton {
|
||||||
icon: string;
|
icon: string;
|
||||||
action: string;
|
action: string;
|
||||||
@ -594,50 +695,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Vibrant.from(this._image)
|
Vibrant.from(this._image)
|
||||||
.quality(1)
|
.useGenerator(customGenerator)
|
||||||
.getPalette()
|
.getPalette()
|
||||||
.then((palette: Palette) => {
|
.then(([foreground, background]: [string, string]) => {
|
||||||
const paletteColors: any[] = [];
|
this._backgroundColor = background;
|
||||||
|
this._foregroundColor = foreground;
|
||||||
Object.keys(palette).forEach((color) => {
|
|
||||||
paletteColors.push({
|
|
||||||
hex: palette[color]!.getHex(),
|
|
||||||
rgb: palette[color]!.getRgb(),
|
|
||||||
textColor: palette[color]!.getBodyTextColor(),
|
|
||||||
population: palette[color]!.getPopulation(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!paletteColors.length) {
|
|
||||||
this._foregroundColor = undefined;
|
|
||||||
this._backgroundColor = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
paletteColors.sort((colorA, colorB) => {
|
|
||||||
if (colorA.population > colorB.population) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (colorA.population < colorB.population) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._backgroundColor = paletteColors[0].hex;
|
|
||||||
|
|
||||||
for (let i = 1; i < paletteColors.length; i++) {
|
|
||||||
if (
|
|
||||||
getContrastRatio(paletteColors[0].rgb, paletteColors[i].rgb) >=
|
|
||||||
CONTRAST_RATIO
|
|
||||||
) {
|
|
||||||
this._foregroundColor = paletteColors[i].hex;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._foregroundColor = paletteColors[0].textColor;
|
|
||||||
})
|
})
|
||||||
.catch((err: any) => {
|
.catch((err: any) => {
|
||||||
// tslint:disable-next-line:no-console
|
// tslint:disable-next-line:no-console
|
||||||
|
@ -5,7 +5,6 @@ import "../cards/hui-glance-card";
|
|||||||
import "../cards/hui-history-graph-card";
|
import "../cards/hui-history-graph-card";
|
||||||
import "../cards/hui-horizontal-stack-card";
|
import "../cards/hui-horizontal-stack-card";
|
||||||
import "../cards/hui-light-card";
|
import "../cards/hui-light-card";
|
||||||
import "../cards/hui-media-control-card";
|
|
||||||
import "../cards/hui-sensor-card";
|
import "../cards/hui-sensor-card";
|
||||||
import "../cards/hui-thermostat-card";
|
import "../cards/hui-thermostat-card";
|
||||||
import "../cards/hui-vertical-stack-card";
|
import "../cards/hui-vertical-stack-card";
|
||||||
@ -25,7 +24,6 @@ const ALWAYS_LOADED_TYPES = new Set([
|
|||||||
"history-graph",
|
"history-graph",
|
||||||
"horizontal-stack",
|
"horizontal-stack",
|
||||||
"light",
|
"light",
|
||||||
"media-control",
|
|
||||||
"sensor",
|
"sensor",
|
||||||
"thermostat",
|
"thermostat",
|
||||||
"vertical-stack",
|
"vertical-stack",
|
||||||
@ -36,6 +34,7 @@ const LAZY_LOAD_TYPES = {
|
|||||||
"alarm-panel": () => import("../cards/hui-alarm-panel-card"),
|
"alarm-panel": () => import("../cards/hui-alarm-panel-card"),
|
||||||
"empty-state": () => import("../cards/hui-empty-state-card"),
|
"empty-state": () => import("../cards/hui-empty-state-card"),
|
||||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||||
|
"media-control": () => import("../cards/hui-media-control-card"),
|
||||||
"picture-elements": () => import("../cards/hui-picture-elements-card"),
|
"picture-elements": () => import("../cards/hui-picture-elements-card"),
|
||||||
"picture-entity": () => import("../cards/hui-picture-entity-card"),
|
"picture-entity": () => import("../cards/hui-picture-entity-card"),
|
||||||
"picture-glance": () => import("../cards/hui-picture-glance-card"),
|
"picture-glance": () => import("../cards/hui-picture-glance-card"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user