Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
Expand All @@ -109,7 +112,7 @@
"marked": "^1.1.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"node-vibrant": "^3.1.6",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
"qrcode": "^1.4.4",
Expand Down
5 changes: 5 additions & 0 deletions src/common/color/rgb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ export const rgbContrast = (

return (lum2 + 0.05) / (lum1 + 0.05);
};

export const getRGBContrastRatio = (
rgb1: [number, number, number],
rgb2: [number, number, number]
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
132 changes: 132 additions & 0 deletions src/common/image/extract_color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Vibrant from "node-vibrant/lib/browser";
import MMCQ from "@vibrant/quantizer-mmcq";
import { BasicPipeline } from "@vibrant/core/lib/pipeline";
import { Swatch, Vec3 } from "@vibrant/color";
import { getRGBContrastRatio } from "../color/rgb";

const CONTRAST_RATIO = 4.5;

// 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 = `${color.hex} - ${color.population}`
) =>
// eslint-disable-next-line no-console
console.log(
`%c${label}`,
`color: ${color.bodyTextColor}; background-color: ${color.hex}`
);

const customGenerator = (colors: Swatch[]) => {
colors.sort((colorA, colorB) => colorB.population - colorA.population);

const backgroundColor = colors[0];
let foregroundColor: Vec3 | undefined;

const contrastRatios = new Map<string, number>();
const approvedContrastRatio = (hex: string, rgb: Swatch["rgb"]) => {
if (!contrastRatios.has(hex)) {
contrastRatios.set(hex, getRGBContrastRatio(backgroundColor.rgb, rgb));
}

return contrastRatios.get(hex)! > 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].hex, colors[i].rgb)) {
if (DEBUG_COLOR) {
logColor(colors[i], "PICKED");
}
foregroundColor = colors[i].rgb;
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.hex, compareColor.rgb)) {
if (DEBUG_COLOR) {
logColor(compareColor, "PICKED");
}
foregroundColor = compareColor.rgb;
break;
}
}
}

if (foregroundColor === undefined) {
foregroundColor =
// @ts-expect-error
backgroundColor.getYiq() < 200 ? [255, 255, 255] : [0, 0, 0];
}

if (DEBUG_COLOR) {
// eslint-disable-next-line no-console
console.log();
// eslint-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));
// eslint-disable-next-line no-console
console.log();
}

return {
foreground: new Swatch(foregroundColor, 0),
background: backgroundColor,
};
};

Vibrant.use(
new BasicPipeline().filter
.register(
"default",
(r: number, g: number, b: number, a: number) =>
a >= 125 && !(r > 250 && g > 250 && b > 250)
)
.quantizer.register("mmcq", MMCQ)
// Our generator has different output
// @ts-expect-error
.generator.register("default", customGenerator)
);

export const extractColors = (url: string, downsampleColors = 16) =>
new Vibrant(url, {
colorCount: downsampleColors,
})
.getPalette()
.then(({ foreground, background }) => {
return { background: background!, foreground: foreground! };
});
1 change: 0 additions & 1 deletion src/data/media-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const SUPPORT_STOP = 4096;
export const SUPPORT_PLAY = 16384;
export const SUPPORT_SELECT_SOUND_MODE = 65536;
export const SUPPORT_BROWSE_MEDIA = 131072;
export const CONTRAST_RATIO = 4.5;

export type MediaPlayerBrowseAction = "pick" | "play";

Expand Down
140 changes: 12 additions & 128 deletions src/panels/lovelace/cards/hui-media-control-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ import {
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { styleMap } from "lit-html/directives/style-map";
import Vibrant from "node-vibrant";
import { Swatch } from "node-vibrant/lib/color";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateIcon } from "../../../common/entity/state_icon";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { debounce } from "../../../common/util/debounce";
import { extractColors } from "../../../common/image/extract_color";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
Expand All @@ -33,15 +32,13 @@ import { UNAVAILABLE_STATES } from "../../../data/entity";
import {
computeMediaDescription,
computeMediaControls,
CONTRAST_RATIO,
getCurrentProgress,
MediaPickedEvent,
SUPPORT_BROWSE_MEDIA,
SUPPORT_SEEK,
SUPPORT_TURN_ON,
} from "../../../data/media-player";
import type { HomeAssistant, MediaEntity } from "../../../types";
import { contrast } from "../common/color/contrast";
import { findEntities } from "../common/find-entites";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { installResizeObserver } from "../common/install-resize-observer";
Expand All @@ -50,114 +47,6 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import { MediaControlCardConfig } from "./types";

function getContrastRatio(
rgb1: [number, number, number],
rgb2: [number, number, number]
): number {
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 = `${color.getHex()} - ${color.getPopulation()}`
) =>
// eslint-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) {
// eslint-disable-next-line no-console
console.log();
// eslint-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));
// eslint-disable-next-line no-console
console.log();
}

return [foregroundColor, backgroundColor.hex];
};

@customElement("hui-media-control-card")
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
Expand Down Expand Up @@ -636,26 +525,21 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
});
}

private _setColors(): void {
private async _setColors(): Promise<void> {
if (!this._image) {
return;
}

new Vibrant(this._image, {
colorCount: 16,
generator: customGenerator,
})
.getPalette()
.then(([foreground, background]: [string, string]) => {
this._backgroundColor = background;
this._foregroundColor = foreground;
})
.catch((err: any) => {
// eslint-disable-next-line no-console
console.error("Error getting Image Colors", err);
this._foregroundColor = undefined;
this._backgroundColor = undefined;
});
try {
const { foreground, background } = await extractColors(this._image);
this._backgroundColor = background.hex;
this._foregroundColor = foreground.hex;
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error getting Image Colors", err);
this._foregroundColor = undefined;
this._backgroundColor = undefined;
}
}

private _marqueeMouseOver(): void {
Expand Down
12 changes: 0 additions & 12 deletions src/panels/lovelace/common/color/contrast.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/panels/lovelace/common/color/luminanace.ts

This file was deleted.

Loading