Add FrostedGlassFill classes and update CSS

This commit is contained in:
16amattice
2024-05-26 15:42:30 -05:00
committed by Aran-Fey
parent 22f9e79c0b
commit 6be02409a7
5 changed files with 167 additions and 53 deletions

View File

@@ -1,5 +1,5 @@
import { Color, ComponentId, Fill } from '../dataModels';
import { colorToCssString, fillToCssString } from '../cssUtils';
import { colorToCssString, fillToCss } from '../cssUtils';
import { ComponentBase, ComponentState } from './componentBase';
import { RippleEffect } from '../rippleEffect';
import { SingleContainer } from './singleContainer';
@@ -36,7 +36,7 @@ function numberToRem(num: number): string {
}
const JS_TO_CSS_VALUE = {
fill: fillToCssString,
fill: fillToCss,
stroke_color: colorToCssString,
stroke_width: numberToRem,
corner_radius: (radii: [number, number, number, number]) =>
@@ -96,26 +96,50 @@ export class RectangleComponent extends SingleContainer {
// Apply all the styling properties
for (let [attrName, js_to_css] of Object.entries(JS_TO_CSS_VALUE)) {
let value = deltaState[attrName];
if (value !== undefined) {
this.element.style.setProperty(
`--rio-rectangle-${attrName}`,
js_to_css(value)
);
if (value !== undefined && value !== null) {
let cssValues = js_to_css(value);
if (typeof cssValues === 'string') {
cssValues = { [attrName]: cssValues };
}
for (let [prop, val] of Object.entries(cssValues)) {
if (prop === 'backdropFilter') {
this.element.style.backdropFilter = val;
} else {
this.element.style.setProperty(
`--rio-rectangle-${prop}`,
val
);
}
}
}
let hoverValue = deltaState['hover_' + attrName];
if (hoverValue !== undefined) {
if (hoverValue === null) {
// No hover value? Use the corresponding non-hover value
this.element.style.setProperty(
`--rio-rectangle-hover-${attrName}`,
`var(--rio-rectangle-${attrName})`
);
if (value !== undefined && value !== null) {
let cssValues = js_to_css(value);
if (typeof cssValues === 'string') {
cssValues = { [attrName]: cssValues };
}
for (let [prop, val] of Object.entries(cssValues)) {
this.element.style.setProperty(
`--rio-rectangle-hover-${prop}`,
`var(--rio-rectangle-${prop})`
);
}
}
} else {
this.element.style.setProperty(
`--rio-rectangle-hover-${attrName}`,
js_to_css(hoverValue)
);
let cssValues = js_to_css(hoverValue);
if (typeof cssValues === 'string') {
cssValues = { [attrName]: cssValues };
}
for (let [prop, val] of Object.entries(cssValues)) {
this.element.style.setProperty(
`--rio-rectangle-hover-${prop}`,
val
);
}
}
}
}

View File

@@ -22,49 +22,61 @@ function gradientToCssString(
)})`;
}
export function fillToCssString(fill: Fill): string {
// Solid Color
if (fill.type === 'solid') {
return colorToCssString(fill.color);
}
export function fillToCss(fill: Fill): { [key: string]: string } {
const cssProps: { [key: string]: string } = {};
// Linear Gradient
else if (fill.type === 'linearGradient') {
if (fill.stops.length == 1) {
return colorToCssString(fill.stops[0][0]);
}
switch (fill.type) {
// Solid Color
case 'solid':
cssProps.fill = colorToCssString(fill.color);
break;
return gradientToCssString(fill.angleDegrees, fill.stops);
}
// Linear Gradient
case 'linearGradient':
if (fill.stops.length === 1) {
cssProps.fill = colorToCssString(fill.stops[0][0]);
} else {
cssProps.fill = gradientToCssString(
fill.angleDegrees,
fill.stops
);
}
break;
// Image
else if (fill.type === 'image') {
let cssUrl = `url('${fill.imageUrl}')`;
// Image
case 'image':
const cssUrl = `url('${fill.imageUrl}')`;
switch (fill.fillMode) {
case 'fit':
cssProps.fill = `${cssUrl} center/contain no-repeat`;
break;
case 'stretch':
cssProps.fill = `${cssUrl} top left / 100% 100%`;
break;
case 'tile':
cssProps.fill = `${cssUrl} left top repeat`;
break;
case 'zoom':
cssProps.fill = `${cssUrl} center/cover no-repeat`;
break;
default:
// Invalid fill mode
// @ts-ignore
throw `Invalid fill mode for image fill: ${fill.type}`;
}
break;
if (fill.fillMode == 'fit') {
return `${cssUrl} center/contain no-repeat`;
} else if (fill.fillMode == 'stretch') {
return `${cssUrl} top left / 100% 100%`;
} else if (fill.fillMode == 'tile') {
return `${cssUrl} left top repeat`;
} else if (fill.fillMode == 'zoom') {
return `${cssUrl} center/cover no-repeat`;
} else {
// Invalid fill mode
// Frosted Glass
case 'frostedGlass':
cssProps.fill = colorToCssString(fill.color);
cssProps.backdropFilter = `blur(${fill.blur / globalThis.pixelsPerRem}rem)`;
break;
default:
// Invalid fill type
// @ts-ignore
throw `Invalid fill mode for image fill: ${fill.type}`;
}
throw `Invalid fill type: ${fill.type}`;
}
// Invalid fill type
// @ts-ignore
throw `Invalid fill type: ${fill.type}`;
}
export function fillToCss(fill: Fill): { background: string } {
return {
background: fillToCssString(fill),
};
return cssProps;
}
export function textStyleToCss(
@@ -78,6 +90,7 @@ export function textStyleToCss(
'text-transform': string;
color: string;
background: string;
'backdrop-filter'?: string;
'-webkit-background-clip': string;
'-webkit-text-fill-color': string;
opacity: string;
@@ -90,6 +103,7 @@ export function textStyleToCss(
let textTransform: string;
let color: string;
let background: string;
let backdropFilter: string | undefined;
let backgroundClip: string;
let textFillColor: string;
let opacity: string;
@@ -166,7 +180,10 @@ export function textStyleToCss(
// Anything else
else {
color = 'unset';
background = fillToCssString(style.fill);
const cssProps = fillToCss(style.fill);
background = cssProps.fill;
backdropFilter = cssProps.backdropFilter;
opacity = cssProps.opacity;
backgroundClip = 'text';
textFillColor = 'transparent';
}
@@ -181,6 +198,7 @@ export function textStyleToCss(
'text-transform': textTransform,
color: color,
background: background,
'backdrop-filter': backdropFilter || '',
'-webkit-background-clip': backgroundClip,
'-webkit-text-fill-color': textFillColor,
opacity: opacity,

View File

@@ -74,6 +74,10 @@ export function applyFillToSVG(svgRoot: SVGSVGElement, fill: Fill): void {
applyImageFill(svgRoot, fill.imageUrl, fill.fillMode);
break;
case 'frostedGlass':
applyFrostedGlassFill(svgRoot, fill.color, fill.blur);
break;
default:
throw new Error(`Invalid fill type: ${fill}`);
}
@@ -148,6 +152,40 @@ function applyImageFill(
svgRoot.setAttribute('fill', `url(#${patternId})`);
}
function applyFrostedGlassFill(svgRoot, color, blur): void {
const [r, g, b, a] = color;
svgRoot.style.fill = `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`;
const filterId = 'frosted-glass-blur';
let filter = svgRoot.querySelector(`#${filterId}`);
if (!filter) {
filter = document.createElementNS(
'http://www.w3.org/2000/svg',
'filter'
);
filter.setAttribute('id', filterId);
const feGaussianBlur = document.createElementNS(
'http://www.w3.org/2000/svg',
'feGaussianBlur'
);
feGaussianBlur.setAttribute('stdDeviation', blur.toString());
filter.appendChild(feGaussianBlur);
let defs = svgRoot.querySelector('defs');
if (!defs) {
defs = document.createElementNS(
'http://www.w3.org/2000/svg',
'defs'
);
svgRoot.appendChild(defs);
}
defs.appendChild(filter);
}
svgRoot.style.filter = `url(#${filterId})`;
}
function generateUniqueId(): string {
return Math.random().toString(36);
}

View File

@@ -228,4 +228,29 @@ class ImageFill(Fill):
)
@dataclass(frozen=True, eq=True)
class FrostedGlassFill(Fill):
"""
Fills a shape with a frosted glass effect.
`FrostedGlassFill` fills the shape with a color and applies a blur effect to
create a frosted glass appearance.
## Attributes
`color`: The color to fill the shape with.
`blur`: The amount of blur applied to the fill.
"""
color: Color
blur: float = 4
def _serialize(self, sess: rio.Session) -> Jsonable:
return {
"type": "frostedGlass",
"color": self.color.rgba,
"blur": self.blur,
}
FillLike: TypeAlias = Fill | Color

View File

@@ -2068,6 +2068,15 @@ a.remove();
"fill-color": "unset",
}
if isinstance(fill, rio.FrostedGlassFill):
return {
"color": f"#{fill.color.hex}",
"background": "none",
"background-clip": "unset",
"fill-color": "unset",
"backdrop-filter": f"blur({fill.blur}rem)",
}
assert isinstance(fill, (rio.LinearGradientFill, rio.ImageFill)), fill
return {
"color": "var(--rio-local-text-color)",