/* * This file is part of TREB. * * TREB is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any * later version. * * TREB is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along * with TREB. If not, see . * * Copyright 2022-2026 trebco, llc. * info@treb.app * */ import { Measurement } from 'treb-utils'; import { type Color } from './style'; import { type Theme, ResolveThemeColor } from './theme'; import { ColorFunctions } from './color'; export interface GradientStop { value: number; color: Color; } export type ColorSpace = 'RGB'|'HSL'; export class Gradient { public mapped: Array; constructor(stops: GradientStop[], theme: Theme, public color_space: ColorSpace = 'RGB') { this.mapped = stops.map(stop => { if (stop.value < 0 || stop.value > 1) { throw new Error('invalid stop value'); } const rgb = Measurement.MeasureColor(ResolveThemeColor(theme, stop.color)); let resolved: number[] = []; if (color_space === 'HSL') { const hsl = ColorFunctions.RGBToHSL(rgb[0], rgb[1], rgb[2]); resolved = [hsl.h, hsl.s, hsl.l]; } else { resolved = [...rgb]; } return { ...stop, resolved, }; }); this.mapped.sort((a, b) => a.value - b.value); // FIXME: we should expand the gradient, but for now we'll just clamp if (this.mapped[0].value > 0) { this.mapped.unshift({ ...this.mapped[0], value: 0 }); } if (this.mapped[this.mapped.length - 1].value < 1) { this.mapped.push({ ...this.mapped[this.mapped.length - 1], value: 1 }); } } public RenderColor(values: number[]) { if (this.color_space === 'RGB') { return { text: `rgb(${values})` }; } return { text: `hsl(${values[0]},${values[1] * 100}%,${values[2] * 100}%)` }; } public Interpolate(value: number): Color { value = Math.min(1, Math.max(0, value)); for (const [index, stop] of this.mapped.entries()) { if (value === stop.value) { return this.RenderColor(stop.resolved); } if (value < stop.value) { const a = this.mapped[index - 1]; const b = stop; const range = b.value - a.value; // FIXME: cache const advance = value - a.value; const values = [0,1,2].map(index => { return a.resolved[index] + (b.resolved[index] - a.resolved[index]) / range * advance; }); return this.RenderColor(values); } } return { text: '' }; } }