mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: Smooth change in colour, no hard cut (#415)
Added option smoothing on colour change so there is no hard cut made when switching subjects
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "betterseqtaplus",
|
"name": "betterseqtaplus",
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development and add heaps more features!",
|
||||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"autoaudit": "npm audit && npm audit fix && npm run build",
|
"autoaudit": "npm audit && npm audit fix && npm run build",
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ function getDefaultValues(): SettingsState {
|
|||||||
iconOnlySidebar: false,
|
iconOnlySidebar: false,
|
||||||
adaptiveThemeColour: false,
|
adaptiveThemeColour: false,
|
||||||
adaptiveThemeGradient: false,
|
adaptiveThemeGradient: false,
|
||||||
|
adaptiveThemeColourTransition: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50">
|
||||||
|
<div class="pr-4">
|
||||||
|
<h2 class="text-sm font-bold">Smooth colour transition</h2>
|
||||||
|
<p class="text-xs">Ease between class/subject colours when navigating instead of switching instantly</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
state={$settingsState.adaptiveThemeColourTransition ?? true}
|
||||||
|
onChange={(isOn: boolean) => settingsState.adaptiveThemeColourTransition = isOn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,12 +9,80 @@ import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
|
|||||||
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
|
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
|
||||||
import lightLogo from "@/resources/icons/betterseqta-dark-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
|
// Helper functions
|
||||||
const setCSSVar = (varName: any, value: any) =>
|
const setCSSVar = (varName: any, value: any) =>
|
||||||
document.documentElement.style.setProperty(varName, value);
|
document.documentElement.style.setProperty(varName, value);
|
||||||
const applyProperties = (props: any) =>
|
const applyProperties = (props: any) =>
|
||||||
Object.entries(props).forEach(([key, value]) => setCSSVar(key, value));
|
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) {
|
function applyColorsWith(selectedColor: string) {
|
||||||
if (settingsState.transparencyEffects) {
|
if (settingsState.transparencyEffects) {
|
||||||
document.documentElement.classList.add("transparencyEffects");
|
document.documentElement.classList.add("transparencyEffects");
|
||||||
@@ -89,15 +157,71 @@ export async function updateAllColors() {
|
|||||||
? settingsState.selectedColor
|
? settingsState.selectedColor
|
||||||
: "#007bff";
|
: "#007bff";
|
||||||
|
|
||||||
|
let adaptiveHex: string | null = null;
|
||||||
|
|
||||||
if (settingsState.adaptiveThemeColour) {
|
if (settingsState.adaptiveThemeColour) {
|
||||||
const adaptiveColor = await getAdaptiveColour();
|
const adaptiveColor = await getAdaptiveColour();
|
||||||
if (adaptiveColor) {
|
if (adaptiveColor) {
|
||||||
effectiveColor =
|
adaptiveHex = adaptiveColor;
|
||||||
settingsState.adaptiveThemeGradient
|
effectiveColor = settingsState.adaptiveThemeGradient
|
||||||
? toSoftGradient(adaptiveColor)
|
? toSoftGradient(adaptiveColor)
|
||||||
: adaptiveColor;
|
: adaptiveColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
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("selectedColor", () => void updateAllColors());
|
||||||
settingsState.register("adaptiveThemeColour", () => void updateAllColors());
|
settingsState.register("adaptiveThemeColour", () => void updateAllColors());
|
||||||
settingsState.register("adaptiveThemeGradient", () => void updateAllColors());
|
settingsState.register("adaptiveThemeGradient", () => void updateAllColors());
|
||||||
|
settingsState.register("adaptiveThemeColourTransition", () =>
|
||||||
|
void updateAllColors(),
|
||||||
|
);
|
||||||
settingsState.register("DarkMode", this.handleDarkModeChange.bind(this));
|
settingsState.register("DarkMode", this.handleDarkModeChange.bind(this));
|
||||||
settingsState.register("onoff", this.handleOnOffChange.bind(this));
|
settingsState.register("onoff", this.handleOnOffChange.bind(this));
|
||||||
settingsState.register("shortcuts", this.handleShortcutsChange.bind(this));
|
settingsState.register("shortcuts", this.handleShortcutsChange.bind(this));
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface SettingsState {
|
|||||||
iconOnlySidebar?: boolean;
|
iconOnlySidebar?: boolean;
|
||||||
adaptiveThemeColour?: boolean;
|
adaptiveThemeColour?: boolean;
|
||||||
adaptiveThemeGradient?: boolean;
|
adaptiveThemeGradient?: boolean;
|
||||||
|
adaptiveThemeColourTransition?: boolean;
|
||||||
|
|
||||||
// depreciated keys
|
// depreciated keys
|
||||||
animatedbk: boolean;
|
animatedbk: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user