mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-19 03:51:15 -06:00
246 lines
7.1 KiB
TypeScript
246 lines
7.1 KiB
TypeScript
import { Color, ColorSet, Fill } from './models';
|
|
import { colorToCssString } from './cssUtils';
|
|
|
|
const ICON_PROMISE_CACHE: { [key: string]: Promise<string> } = {};
|
|
|
|
export function applyColorSet(element: HTMLElement, colorSet: ColorSet): void {
|
|
// Remove all switcheroos
|
|
element.classList.remove(
|
|
'rio-switcheroo-primary',
|
|
'rio-switcheroo-secondary',
|
|
'rio-switcheroo-background',
|
|
'rio-switcheroo-neutral',
|
|
'rio-switcheroo-hud',
|
|
'rio-switcheroo-disabled',
|
|
'rio-switcheroo-success',
|
|
'rio-switcheroo-warning',
|
|
'rio-switcheroo-danger',
|
|
'rio-switcheroo-custom',
|
|
'rio-switcheroo-accent-to-plain'
|
|
);
|
|
|
|
// If no colorset is desired don't apply any new one
|
|
if (colorSet === 'keep') {
|
|
return;
|
|
}
|
|
|
|
// Otherwise find and apply the correct switcheroo
|
|
let switcheroo: string;
|
|
|
|
// Is this a color instance?
|
|
if (typeof colorSet !== 'string') {
|
|
// Expose the color as CSS variables
|
|
element.style.setProperty(
|
|
'--rio-local-custom-plain-bg',
|
|
colorToCssString(colorSet.plainBg)
|
|
);
|
|
element.style.setProperty(
|
|
'--rio-local-custom-plain-bg-variant',
|
|
colorToCssString(colorSet.plainBgVariant)
|
|
);
|
|
element.style.setProperty(
|
|
'--rio-local-custom-plain-bg-active',
|
|
colorToCssString(colorSet.plainBgActive)
|
|
);
|
|
element.style.setProperty(
|
|
'--rio-local-custom-plain-fg',
|
|
colorToCssString(colorSet.plainFg)
|
|
);
|
|
|
|
element.style.setProperty(
|
|
'--rio-local-custom-accent-bg',
|
|
colorToCssString(colorSet.accentBg)
|
|
);
|
|
element.style.setProperty(
|
|
'--rio-local-custom-accent-fg',
|
|
colorToCssString(colorSet.accentFg)
|
|
);
|
|
|
|
// Select the custom switcheroo
|
|
switcheroo = 'custom';
|
|
} else {
|
|
switcheroo = colorSet;
|
|
}
|
|
|
|
// Add the new switcheroo
|
|
element.classList.add(`rio-switcheroo-${switcheroo}`);
|
|
}
|
|
|
|
export function applyFillToSVG(svgRoot: SVGSVGElement, fill: Fill): void {
|
|
switch (fill.type) {
|
|
case 'solid':
|
|
applySolidFill(svgRoot, fill.color);
|
|
break;
|
|
|
|
case 'linearGradient':
|
|
applyLinearGradientFill(svgRoot, fill.angleDegrees, fill.stops);
|
|
break;
|
|
|
|
case 'image':
|
|
applyImageFill(svgRoot, fill.imageUrl, fill.fillMode);
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Invalid fill type: ${fill}`);
|
|
}
|
|
}
|
|
|
|
function applySolidFill(svgRoot: SVGSVGElement, color: Color): void {
|
|
const [r, g, b, a] = color;
|
|
svgRoot.style.fill = `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`;
|
|
}
|
|
|
|
function applyLinearGradientFill(
|
|
svgRoot: SVGSVGElement,
|
|
angleDegrees: number,
|
|
stops: [Color, number][]
|
|
): void {
|
|
// 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
|
|
svgRoot.style.fill = `url(#${gradientId})`;
|
|
}
|
|
|
|
function applyImageFill(
|
|
svgRoot: SVGSVGElement,
|
|
imageUrl: string,
|
|
fillMode: 'fit' | 'stretch' | 'tile' | 'zoom'
|
|
): void {
|
|
// 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%');
|
|
pattern.setAttribute('preserveAspectRatio', 'none');
|
|
|
|
// Create an image
|
|
const image = document.createElementNS(
|
|
'http://www.w3.org/2000/svg',
|
|
'image'
|
|
);
|
|
image.setAttribute('width', '100%');
|
|
image.setAttribute('height', '100%');
|
|
image.setAttribute('href', imageUrl);
|
|
image.setAttribute('preserveAspectRatio', 'none');
|
|
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
|
|
svgRoot.setAttribute('fill', `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 applyIcon(
|
|
target: HTMLElement,
|
|
iconName: string,
|
|
cssColor: string
|
|
): Promise<void> {
|
|
// Is the icon already in the cache?
|
|
let promise = ICON_PROMISE_CACHE[iconName];
|
|
|
|
// No, load it from the server
|
|
if (promise === undefined) {
|
|
console.log(`Fetching icon ${iconName} from server`);
|
|
|
|
promise = fetch(`/rio/icon/${iconName}`).then((response) =>
|
|
response.text()
|
|
);
|
|
|
|
ICON_PROMISE_CACHE[iconName] = 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);
|
|
|
|
// Await the future
|
|
let svgSource: string;
|
|
try {
|
|
svgSource = await promise;
|
|
} catch (err) {
|
|
console.error(`Error loading icon ${iconName}: ${err}`);
|
|
delete ICON_PROMISE_CACHE[iconName];
|
|
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;
|
|
svgRoot.style.fill = cssColor;
|
|
}
|