- Only process images when they are on the screen.

This commit is contained in:
dudubtw
2024-10-24 19:21:45 -03:00
parent b209facf43
commit b72e05f8b6
5 changed files with 60 additions and 71 deletions

View File

@@ -1,17 +1,12 @@
import "../assets/css/gradient.css";
import Impulse from "../lib/Impulse";
import { Component, createEffect, createSignal, JSXElement, onCleanup, onMount } from "solid-js";
import { Component, JSXElement, onCleanup, onMount } from "solid-js";
export type GradientColors = {
top: string;
bottom: string;
};
export const [gradientColors, setGradientColors] = createSignal<GradientColors>({
top: "dodgerblue",
bottom: "crimson",
});
type GradientProps = {
classTop?: string;
classBottom?: string;
@@ -52,13 +47,6 @@ const Gradient: Component<GradientProps> = (props) => {
window.removeEventListener("resize", calculateBackground);
});
createEffect(() => {
const colors = gradientColors();
document.documentElement.style.setProperty("--circle-0", colors.top);
document.documentElement.style.setProperty("--circle-1", colors.bottom);
});
return (
<div ref={bottomLayer} class={`bottom-layer ${props.classBottom ?? ""}`}>
<div class={`gradient-layer`}>

View File

@@ -11,6 +11,7 @@ type SongImageProps = {
src: string | undefined | Accessor<string | undefined>;
group?: string;
instantLoad?: boolean;
onImageLoaded?: (src: string) => void;
} & JSX.IntrinsicElements["div"];
const SongImage: Component<SongImageProps> = (props) => {
@@ -23,6 +24,7 @@ const SongImage: Component<SongImageProps> = (props) => {
}
setSrc(evt.detail);
props.onImageLoaded?.(evt.detail);
delete image?.dataset.eventHandler;
};

View File

@@ -1,4 +1,3 @@
import { setGradientColors } from "../Gradient";
import { extractColors } from "extract-colors";
import { lighten, darken, getContrast, parseToHsl, hslToColorString } from "polished";
import { Accessor, createSignal } from "solid-js";
@@ -12,40 +11,57 @@ const AREA_WEIGHT = 0.3;
type UseColorExtractorResult = {
primaryColor: Accessor<string | undefined>;
secondaryColor: Accessor<string | undefined>;
processImage(src: string): void;
};
export function useColorExtractor() {
const extractColorFromImage = (song: Song): UseColorExtractorResult => {
const songId = song.audio;
const [primaryColor, setPrimartColor] = createSignal<string | undefined>(song.primaryColor);
const [secondaryColor, setSecondaryColor] = createSignal<string | undefined>(
song.secondaryColor,
);
// State
const [primaryColor, setPrimartColor] = createSignal<string>();
const [secondaryColor, setSecondaryColor] = createSignal<string>();
const processImage = async (src: string) => {
if (primaryColor() || secondaryColor()) {
return;
}
// Check if color already exists
if (song.primaryColor && song.secondaryColor) {
document.documentElement.style.setProperty(
"--extracted-color-rgb",
hexToRgb(song.primaryColor),
);
setPrimartColor(song.primaryColor);
setSecondaryColor(song.secondaryColor);
return { primaryColor, secondaryColor };
}
try {
const colors = await extractColorsFromImage(src);
console.log("colors", colors);
if (!colors.primaryColor || !colors.secondaryColor) {
return;
}
if (!song.bg) {
console.error("No background image found for color extraction.");
setPrimartColor("gray");
return { primaryColor, secondaryColor };
}
setPrimartColor(colors.primaryColor);
setSecondaryColor(colors.secondaryColor);
const img = new Image();
img.crossOrigin = "Anonymous";
img.src = `file://${song.bg}`;
await window.api.request(
"save::songColors",
colors.primaryColor,
colors.secondaryColor,
song.audio,
);
} catch (err) {
console.error("Error extracting color:", err);
}
};
return { primaryColor, secondaryColor, processImage };
};
return { extractColorFromImage };
}
type ExtractColorsFromImageResult = { primaryColor: string; secondaryColor: string };
function extractColorsFromImage(src: string): Promise<ExtractColorsFromImageResult> {
const img = new Image();
img.crossOrigin = "Anonymous";
img.src = src;
return new Promise((resolve, reject) => {
img.onload = async () => {
try {
console.log("calculating", songId);
const colors = await extractColors(img.src);
const validColors = colors.filter((color) => {
@@ -61,7 +77,7 @@ export function useColorExtractor() {
return scoreB - scoreA;
});
let songPrimaryColor = sortedColors[0]?.hex || "gray";
let primaryColor = sortedColors[0]?.hex || "gray";
// Find the first valid color that meets contrast and vibrancy thresholds
for (const color of sortedColors) {
@@ -70,48 +86,29 @@ export function useColorExtractor() {
contrast >= MIN_CONTRAST_RATIO &&
getVibrancy(parseToHsl(color.hex)) >= MIN_VIBRANCY_THRESHOLD
) {
songPrimaryColor = color.hex;
primaryColor = color.hex;
break;
}
}
songPrimaryColor = alterUnappealingColor(songPrimaryColor);
primaryColor = alterUnappealingColor(primaryColor);
// Improve contrast if necessary
if (getContrast(songPrimaryColor, "#FFFFFF") < MIN_CONTRAST_RATIO) {
songPrimaryColor = improveContrast(songPrimaryColor);
if (getContrast(primaryColor, "#FFFFFF") < MIN_CONTRAST_RATIO) {
primaryColor = improveContrast(primaryColor);
}
const { lightness } = parseToHsl(songPrimaryColor);
const songSecondaryColor =
lightness < 0.2 ? lighten(0.1, songPrimaryColor) : darken(0.1, songPrimaryColor);
const { lightness } = parseToHsl(primaryColor);
const secondaryColor =
lightness < 0.2 ? lighten(0.1, primaryColor) : darken(0.1, primaryColor);
setPrimartColor(songPrimaryColor);
setSecondaryColor(songSecondaryColor);
document.documentElement.style.setProperty(
"--extracted-color-rgb",
hexToRgb(songPrimaryColor),
);
// Set gradient colors if needed
setGradientColors({
top: songPrimaryColor,
bottom: lighten(0.2, songPrimaryColor),
});
await window.api.request("save::songColors", songPrimaryColor, songSecondaryColor, songId);
document.documentElement.style.setProperty("--extracted-color-rgb", hexToRgb(primaryColor));
resolve({ primaryColor, secondaryColor });
} catch (err) {
console.error("Error extracting color:", err);
setPrimartColor(undefined);
setSecondaryColor(undefined);
reject(err);
}
};
return { primaryColor, secondaryColor };
};
return { extractColorFromImage };
});
}
// Helper function to improve contrast by darkening the color

View File

@@ -1,6 +1,6 @@
import formatTime from "../../../lib/time-formatter";
import SongImage from "../SongImage";
import { useColorExtractor } from "../colorExtractor";
import { useColorExtractor } from "../color-extractor";
import SongControls from "./SongControls";
import Slider from "@renderer/components/slider/Slider";
import {
@@ -24,6 +24,7 @@ const SongDetail: Component = () => {
<SongImage
src={song().bg}
instantLoad={true}
onImageLoaded={colorData().processImage}
class="size-80 rounded-lg bg-cover bg-center object-cover shadow-lg"
/>
</div>

View File

@@ -2,7 +2,7 @@ import { ResourceID, Song } from "../../../../../@types";
import draggable from "../../../lib/draggable/draggable";
import SongHint from "../SongHint";
import SongImage from "../SongImage";
import { useColorExtractor } from "../colorExtractor";
import { useColorExtractor } from "../color-extractor";
import { ignoreClickInContextMenu } from "../context-menu/SongContextMenu";
import { song as selectedSong } from "../song.utils";
import { transparentize } from "polished";
@@ -30,7 +30,7 @@ const SongItem: Component<SongItemProps> = ({
const [, setCoords] = createSignal<[number, number]>([0, 0], { equals: false });
const { extractColorFromImage } = useColorExtractor();
const { primaryColor, secondaryColor } = extractColorFromImage(song);
const { primaryColor, secondaryColor, processImage } = extractColorFromImage(song);
onMount(() => {
if (!item) return;
@@ -97,6 +97,7 @@ const SongItem: Component<SongItemProps> = ({
class={`absolute inset-0 z-[-1] h-full w-full rounded-md bg-cover bg-center bg-no-repeat`}
src={song.bg}
group={group}
onImageLoaded={processImage}
/>
<div
class="flex flex-col justify-center overflow-hidden rounded-md p-3"