import { COLOR_SET_NAMES, Color, ColorSet, ColorSetName, ImageFill, LinearGradientFill, SolidFill, } from './dataModels'; import { colorToCssString } from './cssUtils'; /// Removes any switcheroos from the given element function removeSwitcheroos(element: HTMLElement): void { element.classList.remove( 'rio-switcheroo-background', 'rio-switcheroo-neutral', 'rio-switcheroo-hud', 'rio-switcheroo-primary', 'rio-switcheroo-secondary', 'rio-switcheroo-success', 'rio-switcheroo-warning', 'rio-switcheroo-danger', 'rio-switcheroo-disabled', 'rio-switcheroo-custom', 'rio-switcheroo-bump' ); } export function applySwitcheroo( element: HTMLElement, colorSet: ColorSet | 'bump' ): void { // Remove any preexisting switcheroos removeSwitcheroos(element); // If no color set is desired don't apply any new one if (colorSet === 'keep') { return; } // Is this a well-known switcheroo? if (typeof colorSet === 'string') { element.classList.add(`rio-switcheroo-${colorSet}`); return; } // Custom color sets need additional variables to be defined element.style.setProperty( '--rio-custom-local-bg', colorToCssString(colorSet.localBg) ); element.style.setProperty( '--rio-custom-local-bg-variant', colorToCssString(colorSet.localBgVariant) ); element.style.setProperty( '--rio-custom-local-bg-active', colorToCssString(colorSet.localBgActive) ); element.style.setProperty( '--rio-custom-local-fg', colorToCssString(colorSet.localFg) ); // Apply the switcheroo element.classList.add('rio-switcheroo-custom'); } export function applyFillToSVG( svgRoot: SVGSVGElement, fillLike: | SolidFill | LinearGradientFill | ImageFill | Color | ColorSet | 'dim' | string // A CSS color value ): void { // The svg element may already have a fill, so we must make sure that every // fill overwrites every other fill's style properties. let styleFill: string; let opacity: string = '1'; // Case: No fill was provided, so use the default foreground color if (fillLike === 'keep') { styleFill = 'var(--rio-local-text-color)'; } // Case: "dim". This is a special case, which is represented by also using // the foreground color, but with a reduced opacity. else if (fillLike === 'dim') { styleFill = 'var(--rio-local-text-color)'; opacity = '0.4'; } else if (typeof fillLike === 'string') { // Case: Well known, predefined colorset. // // Note that this uses the background rather than foreground color. The // foreground is intended to be used if the background was already set to // background color. if (COLOR_SET_NAMES.includes(fillLike as ColorSetName)) { styleFill = `var(--rio-global-${fillLike}-bg)`; } // Case: CSS color value else { styleFill = fillLike; } } // Case: single color else if (Array.isArray(fillLike)) { styleFill = colorToCssString(fillLike); } // Case: Colorset else if (fillLike['localBg'] !== undefined) { // @ts-ignore styleFill = colorToCssString(fillLike.localBg); } // Case: Actual Fill object else { fillLike = fillLike as SolidFill | LinearGradientFill | ImageFill; switch (fillLike.type) { case 'solid': styleFill = colorToCssString(fillLike.color); break; case 'linearGradient': styleFill = createLinearGradientFillAndReturnFill( svgRoot, fillLike.angleDegrees, fillLike.stops ); break; case 'image': styleFill = createImageFillAndReturnFill( svgRoot, fillLike.imageUrl, fillLike.fillMode ); break; default: throw new Error(`Invalid fill type: ${fillLike}`); } } svgRoot.style.fill = styleFill; svgRoot.style.opacity = opacity; } function createLinearGradientFillAndReturnFill( svgRoot: SVGSVGElement, angleDegrees: number, stops: [Color, number][] ): string { // Create a new linear gradient const gradientId = generateUniqueId(); const gradient = createLinearGradient(gradientId, angleDegrees, stops); // Add it to the "defs" section of the SVG let defs = svgRoot.querySelector('defs'); if (defs === null) { defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); svgRoot.appendChild(defs); } defs.appendChild(gradient); // Add the gradient to the path return `url(#${gradientId})`; } function createImageFillAndReturnFill( svgRoot: SVGSVGElement, imageUrl: string, fillMode: 'fit' | 'stretch' | 'zoom' ): string { let aspectRatio = { stretch: 'none', fit: 'xMidYMid meet', // FIXME zoom: 'xMidYMid slice', }[fillMode]; // Create a pattern const patternId = generateUniqueId(); const pattern = document.createElementNS( 'http://www.w3.org/2000/svg', 'pattern' ); pattern.setAttribute('id', patternId); pattern.setAttribute('width', '100%'); pattern.setAttribute('height', '100%'); // Create an image const image = document.createElementNS( 'http://www.w3.org/2000/svg', 'image' ); image.setAttribute('href', imageUrl); image.setAttribute('width', '100%'); image.setAttribute('height', '100%'); image.setAttribute('preserveAspectRatio', aspectRatio); pattern.appendChild(image); // Add the pattern to the "defs" section of the SVG let defs = svgRoot.querySelector('defs'); if (defs === null) { defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); svgRoot.appendChild(defs); } defs.appendChild(pattern); // Apply the pattern to the path return `url(#${patternId})`; } function generateUniqueId(): string { return Math.random().toString(36); } function createLinearGradient( gradientId: string, angleDegrees: number, stops: [Color, number][] ): SVGLinearGradientElement { const gradient = document.createElementNS( 'http://www.w3.org/2000/svg', 'linearGradient' ); gradient.setAttribute('id', gradientId); gradient.setAttribute('gradientTransform', `rotate(${angleDegrees})`); let ii = -1; for (const [color, offset] of stops) { ii += 1; const [r, g, b, a] = color; const stop = document.createElementNS( 'http://www.w3.org/2000/svg', 'stop' ); stop.setAttribute('offset', `${offset}`); stop.setAttribute( 'style', `stop-color: rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})` ); stop.setAttribute('id', `${gradientId}-stop-${ii}`); gradient.appendChild(stop); } return gradient; } export async function loadIconSvg(iconName: string): Promise { let response = await fetch(`/rio/icon/${iconName}`); return await response.text(); } export async function applyIcon( target: HTMLElement, iconName: string, fill: | SolidFill | LinearGradientFill | ImageFill | Color | ColorSet | 'dim' | string = 'currentColor' // A CSS color value ): Promise { // Avoid races: When calling this function multiple times on the same // element it can sometimes assign the first icon AFTER the second one, thus // ending up with the wrong icon in the end. // // To avoid this, assign the icon that's supposed to be loaded into the HTML // element as a data attribute. Once the icon's source has been fetched, // only apply it if the data attribute still matches the icon name. target.setAttribute('data-rio-icon', iconName); // Load the icon from the server let svgSource: string; try { svgSource = await loadIconSvg(iconName); } catch (err) { console.error(`Error loading icon ${iconName}: ${err}`); return; } // Check if the icon is still needed if (target.getAttribute('data-rio-icon') !== iconName) { return; } target.removeAttribute('data-rio-icon'); // Apply the icon target.innerHTML = svgSource; // Apply the color let svgRoot = target.querySelector('svg') as SVGSVGElement; applyFillToSVG(svgRoot, fill); }