RaveOS-Gnome/releng/airootfs/usr/share/gnome-shell/extensions/unity-like-appswitcher@gonza.com/utils.js
2025-03-15 15:27:53 +01:00

311 lines
8.2 KiB
JavaScript

import GdkPixbuf from "gi://GdkPixbuf";
import Gio from "gi://Gio";
import Gtk from "gi://Gtk";
import St from "gi://St";
// We need an icons theme object, this is the only way I managed to get
// pixel buffers that can be used for calculating the backlight color
let themeLoader = null;
// Global icon cache. Used for Unity7 styling.
let iconCacheMap = new Map();
// Max number of items to store
// We don't expect to ever reach this number, but let's put an hard limit to avoid
// even the remote possibility of the cached items to grow indefinitely.
const MAX_CACHED_ITEMS = 1000;
// When the size exceed it, the oldest 'n' ones are deleted
const BATCH_SIZE_TO_DELETE = 50;
// The icon size used to extract the dominant color
const DOMINANT_COLOR_ICON_SIZE = 64;
// Compute dominant color from the app icon.
// The color is cached for efficiency.
export class DominantColorExtractor {
constructor(app) {
this._app = app;
}
/**
* Try to get the pixel buffer for the current icon, if not fail gracefully
*/
_getIconPixBuf() {
let iconTexture = this._app.create_icon_texture(16);
const themeLoader = new St.IconTheme();
// Unable to load the icon texture, use fallback
if (iconTexture instanceof St.Icon === false)
return null;
iconTexture = iconTexture.get_gicon();
// Unable to load the icon texture, use fallback
if (!iconTexture)
return null;
if (iconTexture instanceof Gio.FileIcon) {
// Use GdkPixBuf to load the pixel buffer from the provided file path
return GdkPixbuf.Pixbuf.new_from_file(iconTexture.get_file().get_path());
} else if (iconTexture instanceof Gio.ThemedIcon) {
// Get the first pixel buffer available in the icon theme
const iconNames = iconTexture.get_names();
const iconInfo = themeLoader.choose_icon(iconNames, DOMINANT_COLOR_ICON_SIZE, 0);
if (iconInfo)
return iconInfo.load_icon();
else
return null;
}
// Use GdkPixBuf to load the pixel buffer from memory
// iconTexture.load is available unless iconTexture is not an instance of Gio.LoadableIcon
// this means that iconTexture is an instance of Gio.EmblemedIcon,
// which may be converted to a normal icon via iconTexture.get_icon?
const [iconBuffer] = iconTexture.load(DOMINANT_COLOR_ICON_SIZE, null);
return GdkPixbuf.Pixbuf.new_from_stream(iconBuffer, null);
}
/**
* The backlight color choosing algorithm was mostly ported to javascript from the
* Unity7 C++ source of Canonicals:
* https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp
* so it more or less works the same way.
*/
_getColorPalette() {
if (iconCacheMap.get(this._app.get_id())) {
// We already know the answer
return iconCacheMap.get(this._app.get_id());
}
const pixBuf = this._getIconPixBuf();
if (!pixBuf)
return null;
let pixels = pixBuf.get_pixels();
let total = 0,
rTotal = 0,
gTotal = 0,
bTotal = 0;
let resampleX = 1;
let resampleY = 1;
// Resampling of large icons
// We resample icons larger than twice the desired size, as the resampling
// to a size s
// DOMINANT_COLOR_ICON_SIZE < s < 2*DOMINANT_COLOR_ICON_SIZE,
// most of the case exactly DOMINANT_COLOR_ICON_SIZE as the icon size is
// typically a multiple of it.
const width = pixBuf.get_width();
const height = pixBuf.get_height();
// Resample
if (height >= 2 * DOMINANT_COLOR_ICON_SIZE)
resampleY = Math.floor(height / DOMINANT_COLOR_ICON_SIZE);
if (width >= 2 * DOMINANT_COLOR_ICON_SIZE)
resampleX = Math.floor(width / DOMINANT_COLOR_ICON_SIZE);
if (resampleX !== 1 || resampleY !== 1)
pixels = this._resamplePixels(pixels, resampleX, resampleY);
// computing the limit outside the for (where it would be repeated at each iteration)
// for performance reasons
const limit = pixels.length;
for (let offset = 0; offset < limit; offset += 4) {
const r = pixels[offset],
g = pixels[offset + 1],
b = pixels[offset + 2],
a = pixels[offset + 3];
const saturation = Math.max(r, g, b) - Math.min(r, g, b);
const relevance = 0.1 * 255 * 255 + 0.9 * a * saturation;
rTotal += r * relevance;
gTotal += g * relevance;
bTotal += b * relevance;
total += relevance;
}
total *= 255;
const r = rTotal / total,
g = gTotal / total,
b = bTotal / total;
const hsv = ColorUtils.RGBtoHSV(r * 255, g * 255, b * 255);
if (hsv.s > 0.15)
hsv.s = 0.65;
hsv.v = 0.90;
const rgb = ColorUtils.HSVtoRGB(hsv.h, hsv.s, hsv.v);
// Cache the result.
const backgroundColor = {
lighter: ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0.2),
original: ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0),
darker: ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, -0.5),
};
if (iconCacheMap.size >= MAX_CACHED_ITEMS) {
// delete oldest cached values (which are in order of insertions)
let ctr = 0;
for (const key of iconCacheMap.keys()) {
if (++ctr > BATCH_SIZE_TO_DELETE)
break;
iconCacheMap.delete(key);
}
}
iconCacheMap.set(this._app.get_id(), backgroundColor);
return backgroundColor;
}
/**
* Downscale large icons before scanning for the backlight color to
* improve performance.
*
* @param pixBuf
* @param pixels
* @param resampleX
* @param resampleY
*
* @returns [];
*/
_resamplePixels(pixels, resampleX, resampleY) {
const resampledPixels = [];
// computing the limit outside the for (where it would be repeated at each iteration)
// for performance reasons
const limit = pixels.length / (resampleX * resampleY) / 4;
for (let i = 0; i < limit; i++) {
const pixel = i * resampleX * resampleY;
resampledPixels.push(pixels[pixel * 4]);
resampledPixels.push(pixels[pixel * 4 + 1]);
resampledPixels.push(pixels[pixel * 4 + 2]);
resampledPixels.push(pixels[pixel * 4 + 3]);
}
return resampledPixels;
}
}
/**
* Color manipulation utilities
*/
export class ColorUtils {
// Darken or brigthen color by a fraction dlum
// Each rgb value is modified by the same fraction.
// Return "#rrggbb" string
static ColorLuminance(r, g, b, dlum) {
let rgbString = '#';
rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(r*(1+dlum), 0), 255)), 2);
rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(g*(1+dlum), 0), 255)), 2);
rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(b*(1+dlum), 0), 255)), 2);
return rgbString;
}
// Convert decimal to an hexadecimal string adding the desired padding
static _decimalToHex(d, padding) {
let hex = d.toString(16);
while (hex.length < padding)
hex = '0'+ hex;
return hex;
}
static _hexToRgb(h) {
return {
r: parseInt(h.substr(1, 2), 16),
g: parseInt(h.substr(3, 2), 16),
b: parseInt(h.substr(5, 2), 16)
}
}
// Convert hsv ([0-1, 0-1, 0-1]) to rgb ([0-255, 0-255, 0-255]).
// Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV
// here with h = [0,1] instead of [0, 360]
// Accept either (h,s,v) independently or {h:h, s:s, v:v} object.
// Return {r:r, g:g, b:b} object.
static HSVtoRGB(h, s, v) {
if (arguments.length === 1) {
s = h.s;
v = h.v;
h = h.h;
}
let r,g,b;
let c = v*s;
let h1 = h*6;
let x = c*(1 - Math.abs(h1 % 2 - 1));
let m = v - c;
if (h1 <=1)
r = c + m, g = x + m, b = m;
else if (h1 <=2)
r = x + m, g = c + m, b = m;
else if (h1 <=3)
r = m, g = c + m, b = x + m;
else if (h1 <=4)
r = m, g = x + m, b = c + m;
else if (h1 <=5)
r = x + m, g = m, b = c + m;
else
r = c + m, g = m, b = x + m;
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
// Convert rgb ([0-255, 0-255, 0-255]) to hsv ([0-1, 0-1, 0-1]).
// Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV
// here with h = [0,1] instead of [0, 360]
// Accept either (r,g,b) independently or {r:r, g:g, b:b} object.
// Return {h:h, s:s, v:v} object.
static RGBtoHSV(r, g, b) {
if (arguments.length === 1) {
r = r.r;
g = r.g;
b = r.b;
}
let h,s,v;
let M = Math.max(r, g, b);
let m = Math.min(r, g, b);
let c = M - m;
if (c == 0)
h = 0;
else if (M == r)
h = ((g-b)/c) % 6;
else if (M == g)
h = (b-r)/c + 2;
else
h = (r-g)/c + 4;
h = h/6;
v = M/255;
if (M !== 0)
s = c/M;
else
s = 0;
return {
h: h,
s: s,
v: v
};
}
};