From b89b08407390695ffa2c7b068b79f038419bd599 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 00:00:14 +0200 Subject: [PATCH 01/12] new cmaps --- package.json | 1 + src/GlobalStates/GlobalStore.ts | 14 ++- src/components/plots/FlatMap.tsx | 6 +- src/components/plots/Sphere.tsx | 6 +- src/components/plots/UVCube.tsx | 6 +- src/components/textures/colormap.tsx | 60 ++++++++++- src/components/textures/index.ts | 7 +- src/components/textures/js-colormaps-es.d.ts | 4 - src/components/ui/MainPanel/Colormaps.tsx | 106 ++++++++++++++++++- src/utils/updateColorbar.tsx | 4 +- 10 files changed, 185 insertions(+), 29 deletions(-) delete mode 100644 src/components/textures/js-colormaps-es.d.ts diff --git a/package.json b/package.json index 46bcf1ab8..8fc9019d2 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "color-schemes-js": "^0.1.2", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "fflate": "^0.8.2", diff --git a/src/GlobalStates/GlobalStore.ts b/src/GlobalStates/GlobalStore.ts index 65d7ca661..94e1b4182 100644 --- a/src/GlobalStates/GlobalStore.ts +++ b/src/GlobalStates/GlobalStore.ts @@ -144,8 +144,18 @@ const createStore = () => create((set, get) => ({ setShape: (shape) => set({ shape }), setValueScales: (valueScales) => set({ valueScales }), setColormap: (colormap) => set({ colormap }), - setColormapName: (colormapName) => set({ colormapName }), - setFlipColormap: (flipColormap) => set({ flipColormap }), + setColormapName: (colormapName) => { + const prev = get().colormap; + const palette = (colormapName === 'Default') ? 'Spectral' : colormapName; + const tex = GetColorMapTexture(prev, palette, 1, '#000000', 0, get().flipColormap); + set({ colormapName, colormap: tex }); + }, + setFlipColormap: (flipColormap) => { + const palette = (get().colormapName === 'Default') ? 'Spectral' : get().colormapName; + const prev = get().colormap; + const tex = GetColorMapTexture(prev, palette, 1, '#000000', 0, flipColormap); + set({ flipColormap, colormap: tex }); + }, setTimeSeries: (timeSeries) => set({ timeSeries }), updateTimeSeries: (newEntries) => { const merged = { ...newEntries, ...get().timeSeries }; diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index 105f3c730..47d48594d 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -10,7 +10,7 @@ import { vertShader } from '@/components/computation/shaders' import { useShallow } from 'zustand/shallow' import { ThreeEvent } from '@react-three/fiber'; import { coarsenFlatArray, GetCurrentArray, GetTimeSeries, parseUVCoords, deg2rad } from '@/utils/HelperFuncs'; -import { evaluate_cmap } from 'js-colormaps-es'; +import { evaluateColorMap } from '@/components/textures'; import { useCoordBounds } from '@/hooks/useCoordBounds'; import { flatFrag } from '../textures/shaders'; import { SquareMeshes } from './TransectMeshes'; @@ -153,8 +153,8 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT dimCoords = dimCoords.filter(val => val !== null) const tsID = `${dimCoords[0]}_${dimCoords[1]}` const tsObj = { - color:evaluate_cmap(getColorIdx()/10,"Paired"), - data:tempTS, + color: evaluateColorMap(getColorIdx() / 10, 'Paired'), + data: tempTS, normal, uv: tsUV, } diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index cab3b9f14..12c10a917 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -5,7 +5,7 @@ import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow' import { parseUVCoords, GetTimeSeries, GetCurrentArray, deg2rad } from '@/utils/HelperFuncs'; -import { evaluate_cmap } from 'js-colormaps-es'; +import { evaluateColorMap } from '@/components/textures'; import { useCoordBounds } from '@/hooks/useCoordBounds' import { SquareMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; @@ -175,8 +175,8 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture dimCoords = dimCoords.filter(val => val !== null) const tsID = `${dimCoords[0]}_${dimCoords[1]}` const tsObj = { - color:evaluate_cmap(getColorIdx()/10,"Paired"), - data:tempTS, + color: evaluateColorMap(getColorIdx() / 10, 'Paired'), + data: tempTS, normal, uv: tsUV, } diff --git a/src/components/plots/UVCube.tsx b/src/components/plots/UVCube.tsx index 301caa687..53ce51ece 100644 --- a/src/components/plots/UVCube.tsx +++ b/src/components/plots/UVCube.tsx @@ -5,7 +5,7 @@ import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow'; -import { evaluate_cmap } from 'js-colormaps-es'; +import { evaluateColorMap } from '@/components/textures'; function normalizeUV(uv:number, scale:number, pos:number){ return (uv*scale) + (pos-0.5*scale+0.5) @@ -121,10 +121,10 @@ export const UVCube = ( {scale} : {scale?:THREE.Vector3} )=>{ dimCoords = dimCoords.filter(val => val !== null) const tsID = `${dimCoords[0]}_${dimCoords[1]}` const tsObj = { - color:evaluate_cmap(getColorIdx()/10,"Paired"), + color: evaluateColorMap(getColorIdx() / 10, 'Paired'), normal, uv, - data:tempTS + data: tempTS } updateTimeSeries({ [tsID] : tsObj}) incrementColorIdx(); diff --git a/src/components/textures/colormap.tsx b/src/components/textures/colormap.tsx index 4b4b26b35..0d6d5b01c 100644 --- a/src/components/textures/colormap.tsx +++ b/src/components/textures/colormap.tsx @@ -1,9 +1,59 @@ import * as THREE from 'three'; -// https://github.com/vasturiano/three-globe/blob/master/src/utils/color-utils.js -import { evaluate_cmap } from 'js-colormaps-es'; -// import { rgbToHex } from './updateColorbar'; +import { colorschemes, get, findColorScheme } from 'color-schemes-js'; -export const colormaps = ['magma', 'inferno', 'plasma', 'viridis', 'cividis', 'twilight', 'twilight_shifted', 'turbo', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Spectral', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'terrain', 'winter', 'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c'] +export const colormaps = ['magma', 'inferno', 'plasma', 'viridis', 'cividis', 'twilight', 'twilight_shifted', 'turbo', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Spectral', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'terrain', 'winter', 'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c']; +export const availableColorMapNames = Object.keys(colorschemes).sort(); + +const legacySchemeMap: Record = { + Spectral: 'Spectral_11', + Paired: 'Paired_12', + Set1: 'Set1_9', + Set2: 'Set2_8', + Set3: 'Set3_12', + Accent: 'Accent_8', + Dark2: 'Dark2_8', + Pastel1: 'Pastel1_9', + Pastel2: 'Pastel2_8', + twilight_shifted: 'twilight', + gray: 'grays', + winter: 'Winter', +}; + +export function resolveColorSchemeName(name: string): string { + const trimmed = name?.trim(); + if (!trimmed) return 'viridis'; + if (legacySchemeMap[trimmed]) return legacySchemeMap[trimmed]; + if (colorschemes[trimmed]) return trimmed; + const exact = Object.keys(colorschemes).find((key) => key.toLowerCase() === trimmed.toLowerCase()); + if (exact) return exact; + const results = findColorScheme(trimmed); + if (results.length > 0) { + return results.sort((a, b) => b.scheme.length - a.scheme.length)[0].name; + } + return 'viridis'; +} + +export function evaluateColorMap(value: number, palette: string, reverse = false): [number, number, number] { + const schemeName = resolveColorSchemeName(palette); + const scheme = colorschemes[schemeName]; + const color = get(scheme, reverse ? 1 - value : value); + const rgb = color.toRgb255(); + return [rgb.r, rgb.g, rgb.b]; +} + +export function getColormapGradientCss(name: string): string { + const schemeName = resolveColorSchemeName(name); + const scheme = colorschemes[schemeName]; + if (!scheme) { + return 'linear-gradient(to right, #222, #999)'; + } + const stops = Array.from({ length: 16 }, (_, idx) => { + const t = idx / 15; + const { r, g, b } = get(scheme, t).toRgb255(); + return `rgb(${r}, ${g}, ${b}) ${(t * 100).toFixed(1)}%`; + }); + return `linear-gradient(to right, ${stops.join(', ')})`; +} export function minMax(values: number[]): { min: number | undefined, max: number | undefined } { // Filter out NaN values @@ -24,7 +74,7 @@ export function GetColorMapTexture( ): THREE.DataTexture { let unitInterval = Array.from({ length: 255 }, (_, index) => index / 254); unitInterval = reverse ? unitInterval.reverse() : unitInterval - const rgbv = unitInterval.map(value => evaluate_cmap(value, palette, false)); + const rgbv = unitInterval.map(value => evaluateColorMap(value, palette, false)); const colData = new Uint8Array((rgbv.length + 1) * 4); for (let i = 0; i < rgbv.length; i++) { diff --git a/src/components/textures/index.ts b/src/components/textures/index.ts index d9d0cda0d..4ee4e5273 100644 --- a/src/components/textures/index.ts +++ b/src/components/textures/index.ts @@ -1,9 +1,12 @@ -import { GetColorMapTexture, colormaps } from './colormap'; -import {ArrayToTexture, CreateTexture} from './TextureMakers' +import { GetColorMapTexture, colormaps, evaluateColorMap, availableColorMapNames, getColormapGradientCss } from './colormap'; +import {ArrayToTexture, CreateTexture}from './TextureMakers' export { GetColorMapTexture, colormaps, + evaluateColorMap, + availableColorMapNames, + getColormapGradientCss, ArrayToTexture, CreateTexture } \ No newline at end of file diff --git a/src/components/textures/js-colormaps-es.d.ts b/src/components/textures/js-colormaps-es.d.ts deleted file mode 100644 index 6be745552..000000000 --- a/src/components/textures/js-colormaps-es.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'js-colormaps-es' { - export function evaluate_cmap(value: number, name: string, reverse?: boolean): [number, number, number]; - export const data: Record; - } \ No newline at end of file diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 8713d3511..e08b0d882 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -1,9 +1,8 @@ "use client"; -import React, {useEffect, useState} from 'react' -import { GetColorMapTexture } from '@/components/textures'; +import React, {useEffect, useState, useMemo} from 'react' +import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradientCss } from '@/components/textures'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; -import { colormaps } from '@/components/textures'; import { useShallow } from 'zustand/shallow'; import { MdOutlineSwapVert } from "react-icons/md"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" @@ -17,6 +16,7 @@ import { const Colormaps = () => { + const [searchQuery, setSearchQuery] = useState(''); const [hoveredCmap, setHoveredCmap] = useState(null); const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap } = useGlobalStore( useShallow((state) => ({ @@ -30,6 +30,17 @@ const Colormaps = () => { ); const [popoverSide, setPopoverSide] = useState<"left" | "top">("left"); + const [prevColormapName, setPrevColormapName] = useState(colormapName || ''); + + const filteredColormaps = useMemo(() => { + const query = searchQuery.trim().toLowerCase(); + if (!query) return colormaps; + return availableColorMapNames.filter((name) => name.toLowerCase().includes(query)); + }, [searchQuery]); + + const visibleMatches = useMemo(() => filteredColormaps.slice(0, 64), [filteredColormaps]); + const hasMoreResults = filteredColormaps.length > visibleMatches.length; + useEffect(() => { setColormap( GetColorMapTexture(colormap, (hoveredCmap || colormapName) === "Default" ? "Spectral" : (hoveredCmap || colormapName), 1, "#000000", 0, flipColormap) @@ -81,7 +92,56 @@ const Colormaps = () => { side={popoverSide} className="colormaps" > - {colormaps.map((val) => ( +
+ + +
+ {colormapName}{flipColormap ? ' (flipped)' : ''} +
+
+
+ setSearchQuery(event.target.value)} + placeholder="Search colormap names" + className="search-input" + style={{ + width: '100%', + padding: '0.85rem 1rem', + borderRadius: '0.75rem', + border: '1px solid var(--ui-border)', + background: 'var(--ui-bg-code)', + color: 'var(--ui-text-highlighted)', + }} + /> +
+ {searchQuery.trim() && ( +
+ Showing {visibleMatches.length} of {filteredColormaps.length} matches for{' "'}{searchQuery}{'"'} + {hasMoreResults && ' — first 64 shown'} +
+ )} +
+ {visibleMatches.map((val) => { + const hasIcon = colormaps.includes(val); + return hasIcon ? ( { onMouseEnter={() => setHoveredCmap(val)} onMouseLeave={() => setHoveredCmap(null)} onClick={() => { + setPrevColormapName(colormapName); setColormapName(val); setHoveredCmap(null); }} /> - ))} + ) : ( + + ) + })} +
+ {filteredColormaps.length === 0 && ( +
+ No colormaps found. Try a different search term. +
+ )} index / 31); - return unitInterval.map(value => evaluate_cmap(value, palette, false)); + return unitInterval.map((value) => evaluateColorMap(value, palette, false)); } export function rgbToHex(color: [number, number, number]): string { From 890bed519a59ba686d950b986e9815ab4b0d80d4 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 00:04:28 +0200 Subject: [PATCH 02/12] no icons --- src/components/ui/MainPanel/Colormaps.tsx | 84 +++++++++-------------- 1 file changed, 34 insertions(+), 50 deletions(-) diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index e08b0d882..8f96cb290 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -7,7 +7,7 @@ import { useShallow } from 'zustand/shallow'; import { MdOutlineSwapVert } from "react-icons/md"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" import { Button } from "@/components/ui/button-enhanced"; -import Image from 'next/image'; +// Render gradients directly instead of using pre-generated icon images import { Tooltip, TooltipContent, @@ -138,55 +138,39 @@ const Colormaps = () => { {hasMoreResults && ' — first 64 shown'} )} -
- {visibleMatches.map((val) => { - const hasIcon = colormaps.includes(val); - return hasIcon ? ( - {val} setHoveredCmap(val)} - onMouseLeave={() => setHoveredCmap(null)} - onClick={() => { - setPrevColormapName(colormapName); - setColormapName(val); - setHoveredCmap(null); - }} - /> - ) : ( - - ) - })} +
+ {visibleMatches.map((val) => ( + + ))}
{filteredColormaps.length === 0 && (
From 9a9036d91379983936931b1252cd68f587e7b0af Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 00:16:46 +0200 Subject: [PATCH 03/12] button icon --- src/components/ui/MainPanel/Colormaps.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 8f96cb290..e3555acb1 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -68,11 +68,14 @@ const Colormaps = () => { size="icon" className='cursor-pointer hover:scale-90 transition-transform duration-100 ease-out rounded-full' style={{ - backgroundImage: `url(./colormap_icons/${colormapName}.webp)` , - backgroundSize: "100%", + backgroundImage: getColormapGradientCss((colormapName === 'Default' ? 'Spectral' : colormapName) || 'Spectral'), + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + backgroundSize: '100% 100%', transform: flipColormap ? "scaleX(-1)" : "", width: "32px", height: "32px", + border: '1px solid var(--ui-border)' }} />
@@ -151,12 +154,15 @@ const Colormaps = () => { }} onMouseEnter={() => setHoveredCmap(val)} onMouseLeave={() => setHoveredCmap(null)} - style={{ + style={{ width: '100%', height: '34px', borderRadius: '0.5rem', border: '1px solid var(--ui-border)', - background: getColormapGradientCss(val), + backgroundImage: getColormapGradientCss(val), + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + backgroundSize: '100% 100%', color: 'var(--ui-text-highlighted)', display: 'flex', alignItems: 'center', From 7ff2d74d93993f21d683387386990b79d90c3129 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 00:50:53 +0200 Subject: [PATCH 04/12] shadcn --- src/components/ui/MainPanel/Colormaps.tsx | 105 ++++++++++------------ 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index e3555acb1..31fb136e1 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -5,8 +5,10 @@ import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradi import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { useShallow } from 'zustand/shallow'; import { MdOutlineSwapVert } from "react-icons/md"; +import { ButtonGroup } from "@/components/ui/button-group"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" import { Button } from "@/components/ui/button-enhanced"; +import { Input } from "@/components/ui/input"; // Render gradients directly instead of using pre-generated icon images import { Tooltip, @@ -66,7 +68,7 @@ const Colormaps = () => {
@@ -95,45 +96,46 @@ const Colormaps = () => { side={popoverSide} className="colormaps" > -
- - -
- {colormapName}{flipColormap ? ' (flipped)' : ''} + +
+
+ + + + + +
+ +
+
+ +
+ setSearchQuery(event.target.value)} + placeholder="Search colormap names" + className="search-input" + />
-
-
- setSearchQuery(event.target.value)} - placeholder="Search colormap names" - className="search-input" - style={{ - width: '100%', - padding: '0.85rem 1rem', - borderRadius: '0.75rem', - border: '1px solid var(--ui-border)', - background: 'var(--ui-bg-code)', - color: 'var(--ui-text-highlighted)', - }} - />
{searchQuery.trim() && (
@@ -141,7 +143,10 @@ const Colormaps = () => { {hasMoreResults && ' — first 64 shown'}
)} -
+ +
+
+
{visibleMatches.map((val) => ( ))} +
{filteredColormaps.length === 0 && (
No colormaps found. Try a different search term.
)} - setFlipColormap(!flipColormap)} - /> +
From 14d8805365915f749103ee9fedb2af4cb791448d Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 13:18:11 +0200 Subject: [PATCH 05/12] search --- src/components/ui/MainPanel/Colormaps.tsx | 53 +++++++++++++++++------ 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 31fb136e1..fe9e11b3d 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -9,6 +9,14 @@ import { ButtonGroup } from "@/components/ui/button-group"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" import { Button } from "@/components/ui/button-enhanced"; import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Search } from "lucide-react" +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/components/ui/input-group" + // Render gradients directly instead of using pre-generated icon images import { Tooltip, @@ -34,6 +42,10 @@ const Colormaps = () => { const [prevColormapName, setPrevColormapName] = useState(colormapName || ''); + const displayColormapName = (colormapName || '').length > 5 + ? `${(colormapName || '').slice(0, 5)}…` + : (colormapName || ''); + const filteredColormaps = useMemo(() => { const query = searchQuery.trim().toLowerCase(); if (!query) return colormaps; @@ -99,12 +111,13 @@ const Colormaps = () => {
- + @@ -116,26 +129,40 @@ const Colormaps = () => { setHoveredCmap(null); }} size="sm" - variant="ghost" + variant="secondary" + className="cursor-pointer" > Revert
- + + + + + {colormapName && colormapName.length > 5 && ( + + {colormapName} + + )} +
- -
- setSearchQuery(event.target.value)} - placeholder="Search colormap names" - className="search-input" - /> -
+ + + setSearchQuery(event.target.value)} + placeholder="Search..." + /> + + + + +
{searchQuery.trim() && (
From b4278f9dae8810862c50980728897f8ba2740e1f Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 15:05:42 +0200 Subject: [PATCH 06/12] fixes width --- src/components/ui/MainPanel/Colormaps.tsx | 101 +++++++++++++++++----- src/components/ui/css/MainPanel.css | 2 +- 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index fe9e11b3d..893f91c2e 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -10,7 +10,7 @@ import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover import { Button } from "@/components/ui/button-enhanced"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; -import { Search } from "lucide-react" +import { Search, X } from "lucide-react" import { InputGroup, InputGroupAddon, @@ -107,10 +107,18 @@ const Colormaps = () => { - -
-
+ +
+
@@ -152,32 +163,43 @@ const Colormaps = () => {
- + setSearchQuery(event.target.value)} - placeholder="Search..." - /> + value={searchQuery} + onChange={(event) => setSearchQuery(event.target.value)} + placeholder="Search..." + /> - + {searchQuery ? ( + + ) : null} +
{searchQuery.trim() && ( -
+
Showing {visibleMatches.length} of {filteredColormaps.length} matches for{' "'}{searchQuery}{'"'} {hasMoreResults && ' — first 64 shown'}
)} -
-
-
+
+
+
{visibleMatches.map((val) => ( ))}
diff --git a/src/components/ui/css/MainPanel.css b/src/components/ui/css/MainPanel.css index 2272319f2..b4dc06159 100644 --- a/src/components/ui/css/MainPanel.css +++ b/src/components/ui/css/MainPanel.css @@ -45,7 +45,7 @@ .cmap{ padding: 0px 0px; cursor: pointer; - max-width: 200px; + max-width: 95%; height: 36px; border-radius: 6px; transition: 0.25s; From db80319f1da6f1952f6b4bed3d8ff68219045100 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 15:47:54 +0200 Subject: [PATCH 07/12] by category --- src/components/textures/colormap.tsx | 17 +++++++++++++++ src/components/textures/index.ts | 3 ++- src/components/ui/MainPanel/Colormaps.tsx | 25 ++++++++++++++++++----- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/components/textures/colormap.tsx b/src/components/textures/colormap.tsx index 0d6d5b01c..361d3c71d 100644 --- a/src/components/textures/colormap.tsx +++ b/src/components/textures/colormap.tsx @@ -2,8 +2,25 @@ import * as THREE from 'three'; import { colorschemes, get, findColorScheme } from 'color-schemes-js'; export const colormaps = ['magma', 'inferno', 'plasma', 'viridis', 'cividis', 'twilight', 'twilight_shifted', 'turbo', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Spectral', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'terrain', 'winter', 'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c']; + export const availableColorMapNames = Object.keys(colorschemes).sort(); +export type ColormapEntry = { + name: string; + category?: string; + notes?: string; +}; + +// Single searchable index, built once at module load from the color-schemes-js data +export const colormapIndex: ColormapEntry[] = availableColorMapNames.map((name) => { + const scheme = colorschemes[name]; + return { + name, + category: scheme.category, + notes: scheme.notes, + }; +}); + const legacySchemeMap: Record = { Spectral: 'Spectral_11', Paired: 'Paired_12', diff --git a/src/components/textures/index.ts b/src/components/textures/index.ts index 4ee4e5273..435212ac3 100644 --- a/src/components/textures/index.ts +++ b/src/components/textures/index.ts @@ -1,4 +1,4 @@ -import { GetColorMapTexture, colormaps, evaluateColorMap, availableColorMapNames, getColormapGradientCss } from './colormap'; +import { GetColorMapTexture, colormaps, evaluateColorMap, availableColorMapNames, getColormapGradientCss, colormapIndex } from './colormap'; import {ArrayToTexture, CreateTexture}from './TextureMakers' export { @@ -7,6 +7,7 @@ export { evaluateColorMap, availableColorMapNames, getColormapGradientCss, + colormapIndex, ArrayToTexture, CreateTexture } \ No newline at end of file diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 893f91c2e..e7674d7a4 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -1,7 +1,7 @@ "use client"; import React, {useEffect, useState, useMemo} from 'react' -import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradientCss } from '@/components/textures'; +import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradientCss, colormapIndex } from '@/components/textures'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { useShallow } from 'zustand/shallow'; import { MdOutlineSwapVert } from "react-icons/md"; @@ -49,7 +49,20 @@ const Colormaps = () => { const filteredColormaps = useMemo(() => { const query = searchQuery.trim().toLowerCase(); if (!query) return colormaps; - return availableColorMapNames.filter((name) => name.toLowerCase().includes(query)); + + const nameMatches: string[] = []; + const otherMatches: string[] = []; + + for (const entry of colormapIndex) { + const nameHit = entry.name.toLowerCase().includes(query); + const categoryHit = entry.category?.toLowerCase().includes(query); + const notesHit = entry.notes?.toLowerCase().includes(query); + + if (nameHit) nameMatches.push(entry.name); + else if (categoryHit || notesHit) otherMatches.push(entry.name); + } + + return [...nameMatches, ...otherMatches]; }, [searchQuery]); const visibleMatches = useMemo(() => filteredColormaps.slice(0, 64), [filteredColormaps]); @@ -167,7 +180,7 @@ const Colormaps = () => { setSearchQuery(event.target.value)} - placeholder="Search..." + placeholder="Search by name or category..." /> @@ -184,14 +197,16 @@ const Colormaps = () => { ) : null} -
{searchQuery.trim() && (
Showing {visibleMatches.length} of {filteredColormaps.length} matches for{' "'}{searchQuery}{'"'} {hasMoreResults && ' — first 64 shown'}
- )} + + ) + } +
From 619c95e6bba17743a106fb888137dcc8cffd75e4 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 16:51:42 +0200 Subject: [PATCH 08/12] more h --- src/components/ui/MainPanel/Colormaps.tsx | 73 +++++++++++++++++------ src/components/ui/css/MainPanel.css | 2 +- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index e7674d7a4..275e85d6c 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -1,6 +1,6 @@ "use client"; -import React, {useEffect, useState, useMemo} from 'react' +import React, { useEffect, useState, useMemo } from 'react' import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradientCss, colormapIndex } from '@/components/textures'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { useShallow } from 'zustand/shallow'; @@ -16,6 +16,13 @@ import { InputGroupAddon, InputGroupInput, } from "@/components/ui/input-group" +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@/components/ui/select"; // Render gradients directly instead of using pre-generated icon images import { @@ -28,6 +35,7 @@ const Colormaps = () => { const [searchQuery, setSearchQuery] = useState(''); const [hoveredCmap, setHoveredCmap] = useState(null); + const [selectedCategory, setSelectedCategory] = useState('None'); const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap } = useGlobalStore( useShallow((state) => ({ setColormap: state.setColormap, @@ -42,18 +50,30 @@ const Colormaps = () => { const [prevColormapName, setPrevColormapName] = useState(colormapName || ''); - const displayColormapName = (colormapName || '').length > 5 - ? `${(colormapName || '').slice(0, 5)}…` - : (colormapName || ''); + const categories = useMemo(() => { + const set = new Set(); + colormapIndex.forEach((entry) => { + if (entry.category) set.add(entry.category); + }); + return ['None', ...Array.from(set).sort()]; + }, []); const filteredColormaps = useMemo(() => { const query = searchQuery.trim().toLowerCase(); - if (!query) return colormaps; + + // "None" + no query => original curated default list + if (selectedCategory === 'None' && !query) return colormaps; + + const scoped = selectedCategory === 'None' + ? colormapIndex + : colormapIndex.filter((entry) => entry.category === selectedCategory); + + if (!query) return scoped.map((entry) => entry.name); const nameMatches: string[] = []; const otherMatches: string[] = []; - for (const entry of colormapIndex) { + for (const entry of scoped) { const nameHit = entry.name.toLowerCase().includes(query); const categoryHit = entry.category?.toLowerCase().includes(query); const notesHit = entry.notes?.toLowerCase().includes(query); @@ -63,7 +83,7 @@ const Colormaps = () => { } return [...nameMatches, ...otherMatches]; - }, [searchQuery]); + }, [searchQuery, selectedCategory]); const visibleMatches = useMemo(() => filteredColormaps.slice(0, 64), [filteredColormaps]); const hasMoreResults = filteredColormaps.length > visibleMatches.length; @@ -127,6 +147,7 @@ const Colormaps = () => { overscrollBehavior: 'contain', padding: 0, boxSizing: 'border-box', + maxHeight: popoverSide === 'top' ? '80vh' : undefined, }} > @@ -157,21 +178,35 @@ const Colormaps = () => { -
+ + +
- {colormapName && colormapName.length > 5 && ( {colormapName} - )}
@@ -180,7 +215,7 @@ const Colormaps = () => { setSearchQuery(event.target.value)} - placeholder="Search by name or category..." + placeholder="Search..." /> @@ -197,16 +232,16 @@ const Colormaps = () => { ) : null} +
- {searchQuery.trim() && ( + {(searchQuery.trim() || selectedCategory !== 'None') && (
- Showing {visibleMatches.length} of {filteredColormaps.length} matches for{' "'}{searchQuery}{'"'} + Showing {visibleMatches.length} of {filteredColormaps.length} + {selectedCategory !== 'None' && <> in {selectedCategory}} + {searchQuery.trim() && <> matching "{searchQuery}"} {hasMoreResults && ' — first 64 shown'}
- - ) - } - + )}
diff --git a/src/components/ui/css/MainPanel.css b/src/components/ui/css/MainPanel.css index b4dc06159..962eae993 100644 --- a/src/components/ui/css/MainPanel.css +++ b/src/components/ui/css/MainPanel.css @@ -62,7 +62,7 @@ } .colormaps{ - max-height: 50vh; + max-height: 70vh; width: 234px; display:grid; gap:3px; From 345276eb07b8c57450ae6ef1d934aa7ba7fba7d3 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 17:35:07 +0200 Subject: [PATCH 09/12] filter --- src/components/ui/MainPanel/Colormaps.tsx | 116 ++++++++++++++-------- 1 file changed, 75 insertions(+), 41 deletions(-) diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 275e85d6c..0e1f7034a 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState, useMemo } from 'react' +import React, { useEffect, useState, useMemo, useRef } from 'react' import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradientCss, colormapIndex } from '@/components/textures'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { useShallow } from 'zustand/shallow'; @@ -10,7 +10,7 @@ import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover import { Button } from "@/components/ui/button-enhanced"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; -import { Search, X } from "lucide-react" +import { Search, X, Eye, EyeOff } from "lucide-react" import { InputGroup, InputGroupAddon, @@ -36,6 +36,7 @@ const Colormaps = () => { const [searchQuery, setSearchQuery] = useState(''); const [hoveredCmap, setHoveredCmap] = useState(null); const [selectedCategory, setSelectedCategory] = useState('None'); + const [showNames, setShowNames] = useState(true); const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap } = useGlobalStore( useShallow((state) => ({ setColormap: state.setColormap, @@ -49,6 +50,7 @@ const Colormaps = () => { const [popoverSide, setPopoverSide] = useState<"left" | "top">("left"); const [prevColormapName, setPrevColormapName] = useState(colormapName || ''); + const previousTextureRef = useRef(colormap); const categories = useMemo(() => { const set = new Set(); @@ -61,25 +63,31 @@ const Colormaps = () => { const filteredColormaps = useMemo(() => { const query = searchQuery.trim().toLowerCase(); - // "None" + no query => original curated default list - if (selectedCategory === 'None' && !query) return colormaps; + // No search: category filtering works normally + if (!query) { + if (selectedCategory === 'None') { + return colormaps; + } - const scoped = selectedCategory === 'None' - ? colormapIndex - : colormapIndex.filter((entry) => entry.category === selectedCategory); - - if (!query) return scoped.map((entry) => entry.name); + return colormapIndex + .filter((entry) => entry.category === selectedCategory) + .map((entry) => entry.name); + } + // Search ALWAYS uses the complete collection const nameMatches: string[] = []; const otherMatches: string[] = []; - for (const entry of scoped) { + for (const entry of colormapIndex) { const nameHit = entry.name.toLowerCase().includes(query); const categoryHit = entry.category?.toLowerCase().includes(query); const notesHit = entry.notes?.toLowerCase().includes(query); - if (nameHit) nameMatches.push(entry.name); - else if (categoryHit || notesHit) otherMatches.push(entry.name); + if (nameHit) { + nameMatches.push(entry.name); + } else if (categoryHit || notesHit) { + otherMatches.push(entry.name); + } } return [...nameMatches, ...otherMatches]; @@ -88,11 +96,15 @@ const Colormaps = () => { const visibleMatches = useMemo(() => filteredColormaps.slice(0, 64), [filteredColormaps]); const hasMoreResults = filteredColormaps.length > visibleMatches.length; + useEffect(() => { + previousTextureRef.current = colormap; + }, [colormap]); + useEffect(() => { setColormap( - GetColorMapTexture(colormap, (hoveredCmap || colormapName) === "Default" ? "Spectral" : (hoveredCmap || colormapName), 1, "#000000", 0, flipColormap) + GetColorMapTexture(previousTextureRef.current, (hoveredCmap || colormapName) === "Default" ? "Spectral" : (hoveredCmap || colormapName), 1, "#000000", 0, flipColormap) ); - }, [colormapName, flipColormap, hoveredCmap]); + }, [colormapName, flipColormap, hoveredCmap, setColormap]); useEffect(() => { const handleResize = () => { @@ -191,7 +203,27 @@ const Colormaps = () => { -
+
+ + + + + + {showNames ? "Hide names" : "Show names"} + + + - - {colormapName} - + + {colormapName} +
@@ -237,7 +269,7 @@ const Colormaps = () => { {(searchQuery.trim() || selectedCategory !== 'None') && (
Showing {visibleMatches.length} of {filteredColormaps.length} - {selectedCategory !== 'None' && <> in {selectedCategory}} + {selectedCategory !== 'None' && !searchQuery.trim() && <> in {selectedCategory}} {searchQuery.trim() && <> matching "{searchQuery}"} {hasMoreResults && ' — first 64 shown'}
@@ -259,7 +291,7 @@ const Colormaps = () => { onMouseEnter={() => setHoveredCmap(val)} onMouseLeave={() => setHoveredCmap(null)} style={{ - width: '100%', + width: '96%', minWidth: 0, height: '34px', borderRadius: '0.5rem', @@ -289,26 +321,28 @@ const Colormaps = () => { pointerEvents: 'none', }} /> - - {val} - + {showNames && ( + + {val} + + )} ))}
From 575113dc3cd3231d8ca60547e38035a09b7723ae Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 17:53:29 +0200 Subject: [PATCH 10/12] category --- src/components/ui/MainPanel/Colormaps.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 0e1f7034a..0770a005b 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -35,7 +35,7 @@ const Colormaps = () => { const [searchQuery, setSearchQuery] = useState(''); const [hoveredCmap, setHoveredCmap] = useState(null); - const [selectedCategory, setSelectedCategory] = useState('None'); + const [selectedCategory, setSelectedCategory] = useState(''); const [showNames, setShowNames] = useState(true); const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap } = useGlobalStore( useShallow((state) => ({ @@ -65,7 +65,7 @@ const Colormaps = () => { // No search: category filtering works normally if (!query) { - if (selectedCategory === 'None') { + if (!selectedCategory || selectedCategory === 'None') { return colormaps; } @@ -190,7 +190,10 @@ const Colormaps = () => { - setSelectedCategory(value === 'None' ? '' : value)} + > @@ -269,7 +272,7 @@ const Colormaps = () => { {(searchQuery.trim() || selectedCategory !== 'None') && (
Showing {visibleMatches.length} of {filteredColormaps.length} - {selectedCategory !== 'None' && !searchQuery.trim() && <> in {selectedCategory}} + {selectedCategory && selectedCategory !== 'None' && !searchQuery.trim() && <> in {selectedCategory}} {searchQuery.trim() && <> matching "{searchQuery}"} {hasMoreResults && ' — first 64 shown'}
From 27594a2afd7e89ef77944a8dc14c19e8bc782151 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 18:14:32 +0200 Subject: [PATCH 11/12] rm dep --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 8fc9019d2..e6a667bac 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "gsap": "^3.13.0", "icechunk-js": "^0.6.0", "input-otp": "^1.4.2", - "js-colormaps-es": "^0.0.5", "lucide-react": "^0.562.0", "next": "^16.2.7", "next-themes": "^0.4.6", From 08b3d6f4cd8e80832b170e1964b3c6ec3b2d2a7b Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 21 Jul 2026 21:08:47 +0200 Subject: [PATCH 12/12] performance --- src/components/textures/colormap.tsx | 39 ++++++++++++++++------ src/components/ui/MainPanel/Colormaps.tsx | 40 ++++++++++++++++++++--- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/components/textures/colormap.tsx b/src/components/textures/colormap.tsx index 361d3c71d..4e42e9c9e 100644 --- a/src/components/textures/colormap.tsx +++ b/src/components/textures/colormap.tsx @@ -36,24 +36,43 @@ const legacySchemeMap: Record = { winter: 'Winter', }; +const resolutionCache = new Map(); export function resolveColorSchemeName(name: string): string { const trimmed = name?.trim(); if (!trimmed) return 'viridis'; - if (legacySchemeMap[trimmed]) return legacySchemeMap[trimmed]; - if (colorschemes[trimmed]) return trimmed; - const exact = Object.keys(colorschemes).find((key) => key.toLowerCase() === trimmed.toLowerCase()); - if (exact) return exact; - const results = findColorScheme(trimmed); - if (results.length > 0) { - return results.sort((a, b) => b.scheme.length - a.scheme.length)[0].name; + + const cached = resolutionCache.get(trimmed); + if (cached) return cached; + + let resolved = 'viridis'; + if (legacySchemeMap[trimmed]) { + resolved = legacySchemeMap[trimmed]; + } else if (colorschemes[trimmed]) { + resolved = trimmed; + } else { + const exact = Object.keys(colorschemes).find((key) => key.toLowerCase() === trimmed.toLowerCase()); + if (exact) { + resolved = exact; + } else { + const results = findColorScheme(trimmed); + if (results.length > 0) { + resolved = results.sort((a, b) => b.scheme.length - a.scheme.length)[0].name; + } + } } - return 'viridis'; + + resolutionCache.set(trimmed, resolved); + return resolved; } export function evaluateColorMap(value: number, palette: string, reverse = false): [number, number, number] { const schemeName = resolveColorSchemeName(palette); - const scheme = colorschemes[schemeName]; - const color = get(scheme, reverse ? 1 - value : value); + const scheme = colorschemes[schemeName] || colorschemes['viridis']; + if (!scheme) { + return [0, 0, 0]; + } + const t = Math.max(0, Math.min(1, reverse ? 1 - value : value)); + const color = get(scheme, t); const rgb = color.toRgb255(); return [rgb.r, rgb.g, rgb.b]; } diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 0770a005b..7c1de3645 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -51,6 +51,9 @@ const Colormaps = () => { const [prevColormapName, setPrevColormapName] = useState(colormapName || ''); const previousTextureRef = useRef(colormap); + const colormapNameRef = useRef(colormapName); + const flipColormapRef = useRef(flipColormap); + const lastHoveredCmap = useRef(null); const categories = useMemo(() => { const set = new Set(); @@ -96,15 +99,44 @@ const Colormaps = () => { const visibleMatches = useMemo(() => filteredColormaps.slice(0, 64), [filteredColormaps]); const hasMoreResults = filteredColormaps.length > visibleMatches.length; + // Keep refs in sync with store state changes + useEffect(() => { + colormapNameRef.current = colormapName; + flipColormapRef.current = flipColormap; + }, [colormapName, flipColormap]); + useEffect(() => { previousTextureRef.current = colormap; }, [colormap]); useEffect(() => { - setColormap( - GetColorMapTexture(previousTextureRef.current, (hoveredCmap || colormapName) === "Default" ? "Spectral" : (hoveredCmap || colormapName), 1, "#000000", 0, flipColormap) - ); - }, [colormapName, flipColormap, hoveredCmap, setColormap]); + if (hoveredCmap !== null) { + // Show hovered colormap preview + setColormap( + GetColorMapTexture( + previousTextureRef.current, + hoveredCmap === "Default" ? "Spectral" : hoveredCmap, + 1, + "#000000", + 0, + flipColormapRef.current + ) + ); + } else if (lastHoveredCmap.current !== null) { + // Mouse left hover: revert to selected colormap + setColormap( + GetColorMapTexture( + previousTextureRef.current, + colormapNameRef.current === "Default" ? "Spectral" : colormapNameRef.current, + 1, + "#000000", + 0, + flipColormapRef.current + ) + ); + } + lastHoveredCmap.current = hoveredCmap; + }, [hoveredCmap, setColormap]); useEffect(() => { const handleResize = () => {