mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: Smooth change in colour, no hard cut
Added option smoothing on colour change so there is no hard cut made when switching subjects
This commit is contained in:
@@ -9,12 +9,80 @@ import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
|
||||
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
|
||||
import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
|
||||
|
||||
const ADAPTIVE_THEME_TRANSITION_MS = 400;
|
||||
|
||||
let colorTransitionRafId: number | null = null;
|
||||
let lastInterpolatedHex: string | null = null;
|
||||
|
||||
// Helper functions
|
||||
const setCSSVar = (varName: any, value: any) =>
|
||||
document.documentElement.style.setProperty(varName, value);
|
||||
const applyProperties = (props: any) =>
|
||||
Object.entries(props).forEach(([key, value]) => setCSSVar(key, value));
|
||||
|
||||
function easeInOutCubic(t: number): number {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
|
||||
/** Best-effort parse of a single sRGB hex from a colour string (hex, rgb, or gradient). */
|
||||
function parseRepresentativeHex(s: string): string | null {
|
||||
if (!s || !s.trim()) return null;
|
||||
const trimmed = s.trim();
|
||||
try {
|
||||
return Color(trimmed).hex();
|
||||
} catch {
|
||||
// continue
|
||||
}
|
||||
if (trimmed.includes("gradient")) {
|
||||
const regex =
|
||||
/#[0-9a-fA-F]{6}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)/gi;
|
||||
const stops = trimmed.match(regex);
|
||||
if (stops?.length) {
|
||||
try {
|
||||
return Color(stops[0]).hex();
|
||||
} catch {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}
|
||||
const hexMatch = trimmed.match(/#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})\b/);
|
||||
if (hexMatch) {
|
||||
try {
|
||||
return Color(hexMatch[0]).hex();
|
||||
} catch {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
const rgbaMatch = trimmed.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
||||
if (rgbaMatch) {
|
||||
try {
|
||||
return Color.rgb(
|
||||
Number(rgbaMatch[1]),
|
||||
Number(rgbaMatch[2]),
|
||||
Number(rgbaMatch[3]),
|
||||
).hex();
|
||||
} catch {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getFromHex(): string | null {
|
||||
const fromComputed = parseRepresentativeHex(
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--better-main").trim(),
|
||||
);
|
||||
if (fromComputed) return fromComputed;
|
||||
return lastInterpolatedHex;
|
||||
}
|
||||
|
||||
function cancelColorTransition() {
|
||||
if (colorTransitionRafId !== null) {
|
||||
cancelAnimationFrame(colorTransitionRafId);
|
||||
colorTransitionRafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function applyColorsWith(selectedColor: string) {
|
||||
if (settingsState.transparencyEffects) {
|
||||
document.documentElement.classList.add("transparencyEffects");
|
||||
@@ -89,15 +157,71 @@ export async function updateAllColors() {
|
||||
? settingsState.selectedColor
|
||||
: "#007bff";
|
||||
|
||||
let adaptiveHex: string | null = null;
|
||||
|
||||
if (settingsState.adaptiveThemeColour) {
|
||||
const adaptiveColor = await getAdaptiveColour();
|
||||
if (adaptiveColor) {
|
||||
effectiveColor =
|
||||
settingsState.adaptiveThemeGradient
|
||||
? toSoftGradient(adaptiveColor)
|
||||
: adaptiveColor;
|
||||
adaptiveHex = adaptiveColor;
|
||||
effectiveColor = settingsState.adaptiveThemeGradient
|
||||
? toSoftGradient(adaptiveColor)
|
||||
: adaptiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
applyColorsWith(effectiveColor);
|
||||
const baseSelected =
|
||||
settingsState.selectedColor !== "" ? settingsState.selectedColor : "#007bff";
|
||||
const toHex =
|
||||
adaptiveHex ?? parseRepresentativeHex(baseSelected);
|
||||
|
||||
const shouldAnimate =
|
||||
settingsState.adaptiveThemeColour &&
|
||||
(settingsState.adaptiveThemeColourTransition ?? true) &&
|
||||
!!toHex;
|
||||
|
||||
const applyImmediate = () => {
|
||||
cancelColorTransition();
|
||||
applyColorsWith(effectiveColor);
|
||||
if (toHex) lastInterpolatedHex = toHex;
|
||||
};
|
||||
|
||||
if (!shouldAnimate) {
|
||||
applyImmediate();
|
||||
return;
|
||||
}
|
||||
|
||||
const fromHex = getFromHex();
|
||||
|
||||
if (!fromHex || !toHex || fromHex === toHex) {
|
||||
applyImmediate();
|
||||
return;
|
||||
}
|
||||
|
||||
const useSoftGradientOnFrames =
|
||||
!!adaptiveHex && !!settingsState.adaptiveThemeGradient;
|
||||
|
||||
cancelColorTransition();
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
const step = (now: number) => {
|
||||
const elapsed = now - start;
|
||||
const t = Math.min(1, elapsed / ADAPTIVE_THEME_TRANSITION_MS);
|
||||
const eased = easeInOutCubic(t);
|
||||
const interpolatedHex = Color(fromHex).mix(Color(toHex), eased).hex();
|
||||
const display = useSoftGradientOnFrames
|
||||
? toSoftGradient(interpolatedHex)
|
||||
: interpolatedHex;
|
||||
applyColorsWith(display);
|
||||
|
||||
if (t < 1) {
|
||||
colorTransitionRafId = requestAnimationFrame(step);
|
||||
} else {
|
||||
colorTransitionRafId = null;
|
||||
applyColorsWith(effectiveColor);
|
||||
lastInterpolatedHex = toHex;
|
||||
}
|
||||
};
|
||||
|
||||
colorTransitionRafId = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ export class StorageChangeHandler {
|
||||
settingsState.register("selectedColor", () => void updateAllColors());
|
||||
settingsState.register("adaptiveThemeColour", () => void updateAllColors());
|
||||
settingsState.register("adaptiveThemeGradient", () => void updateAllColors());
|
||||
settingsState.register("adaptiveThemeColourTransition", () =>
|
||||
void updateAllColors(),
|
||||
);
|
||||
settingsState.register("DarkMode", this.handleDarkModeChange.bind(this));
|
||||
settingsState.register("onoff", this.handleOnOffChange.bind(this));
|
||||
settingsState.register("shortcuts", this.handleShortcutsChange.bind(this));
|
||||
|
||||
Reference in New Issue
Block a user