mirror of
https://github.com/rio-labs/rio.git
synced 2025-12-30 09:49:44 -06:00
add radial gradients
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
ColorSet,
|
||||
ImageFill,
|
||||
LinearGradientFill,
|
||||
RadialGradientFill,
|
||||
SolidFill,
|
||||
} from "../dataModels";
|
||||
import { ComponentBase, ComponentState } from "./componentBase";
|
||||
@@ -11,7 +12,14 @@ import { applyIcon, applyFillToSVG } from "../designApplication";
|
||||
export type IconState = ComponentState & {
|
||||
_type_: "Icon-builtin";
|
||||
icon: string;
|
||||
fill: SolidFill | LinearGradientFill | ImageFill | Color | ColorSet | "dim";
|
||||
fill:
|
||||
| SolidFill
|
||||
| LinearGradientFill
|
||||
| RadialGradientFill
|
||||
| ImageFill
|
||||
| Color
|
||||
| ColorSet
|
||||
| "dim";
|
||||
};
|
||||
|
||||
export class IconComponent extends ComponentBase {
|
||||
|
||||
@@ -5,7 +5,7 @@ export function colorToCssString(color: Color): string {
|
||||
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`;
|
||||
}
|
||||
|
||||
function gradientToCssString(
|
||||
function linearGradientToCssString(
|
||||
angleDegrees: number,
|
||||
stops: [Color, number][]
|
||||
): string {
|
||||
@@ -22,6 +22,25 @@ function gradientToCssString(
|
||||
)})`;
|
||||
}
|
||||
|
||||
function radialGradientToCssString(
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
stops: [Color, number][]
|
||||
): string {
|
||||
let stopStrings: string[] = [];
|
||||
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
let color = stops[i][0];
|
||||
let position = stops[i][1];
|
||||
stopStrings.push(`${colorToCssString(color)} ${position * 100}%`);
|
||||
}
|
||||
|
||||
const centerPosition = `${centerX * 100}% ${centerY * 100}%`;
|
||||
return `radial-gradient(circle at ${centerPosition}, ${stopStrings.join(
|
||||
", "
|
||||
)})`;
|
||||
}
|
||||
|
||||
export function fillToCss(fill: AnyFill): {
|
||||
background: string;
|
||||
"backdrop-filter": string;
|
||||
@@ -41,7 +60,19 @@ export function fillToCss(fill: AnyFill): {
|
||||
// that the first one is at 0 and the last one is at 1. No need to
|
||||
// verify any of that here.
|
||||
case "linearGradient":
|
||||
background = gradientToCssString(fill.angleDegrees, fill.stops);
|
||||
background = linearGradientToCssString(
|
||||
fill.angleDegrees,
|
||||
fill.stops
|
||||
);
|
||||
break;
|
||||
|
||||
// Radial Gradient
|
||||
case "radialGradient":
|
||||
background = radialGradientToCssString(
|
||||
fill.centerX,
|
||||
fill.centerY,
|
||||
fill.stops
|
||||
);
|
||||
break;
|
||||
|
||||
// Image
|
||||
|
||||
@@ -37,6 +37,12 @@ export type LinearGradientFill = {
|
||||
angleDegrees: number;
|
||||
stops: [Color, number][];
|
||||
};
|
||||
export type RadialGradientFill = {
|
||||
type: "radialGradient";
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
stops: [Color, number][];
|
||||
};
|
||||
export type ImageFill = {
|
||||
type: "image";
|
||||
imageUrl: string;
|
||||
@@ -51,6 +57,7 @@ export type FrostedGlassFill = {
|
||||
export type AnyFill =
|
||||
| SolidFill
|
||||
| LinearGradientFill
|
||||
| RadialGradientFill
|
||||
| ImageFill
|
||||
| FrostedGlassFill;
|
||||
|
||||
@@ -58,6 +65,7 @@ export type TextCompatibleFill =
|
||||
| Color
|
||||
| SolidFill
|
||||
| LinearGradientFill
|
||||
| RadialGradientFill
|
||||
| ImageFill
|
||||
| null;
|
||||
export type TextStyle = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ColorSetName,
|
||||
ImageFill,
|
||||
LinearGradientFill,
|
||||
RadialGradientFill,
|
||||
SolidFill,
|
||||
} from "./dataModels";
|
||||
import { colorToCssString } from "./cssUtils";
|
||||
@@ -71,6 +72,7 @@ export function applyFillToSVG(
|
||||
fillLike:
|
||||
| SolidFill
|
||||
| LinearGradientFill
|
||||
| RadialGradientFill
|
||||
| ImageFill
|
||||
| Color
|
||||
| ColorSet
|
||||
@@ -116,7 +118,11 @@ export function applyFillToSVG(
|
||||
}
|
||||
// Case: Actual Fill object
|
||||
else {
|
||||
fillLike = fillLike as SolidFill | LinearGradientFill | ImageFill;
|
||||
fillLike = fillLike as
|
||||
| SolidFill
|
||||
| LinearGradientFill
|
||||
| RadialGradientFill
|
||||
| ImageFill;
|
||||
|
||||
switch (fillLike.type) {
|
||||
case "solid":
|
||||
@@ -131,6 +137,15 @@ export function applyFillToSVG(
|
||||
);
|
||||
break;
|
||||
|
||||
case "radialGradient":
|
||||
styleFill = createRadialGradientFillAndReturnFill(
|
||||
svgRoot,
|
||||
fillLike.centerX,
|
||||
fillLike.centerY,
|
||||
fillLike.stops
|
||||
);
|
||||
break;
|
||||
|
||||
case "image":
|
||||
styleFill = createImageFillAndReturnFill(
|
||||
svgRoot,
|
||||
@@ -172,6 +187,30 @@ function createLinearGradientFillAndReturnFill(
|
||||
return `url(#${gradientId})`;
|
||||
}
|
||||
|
||||
function createRadialGradientFillAndReturnFill(
|
||||
svgRoot: SVGSVGElement,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
stops: [Color, number][]
|
||||
): string {
|
||||
// Create a new radial gradient
|
||||
const gradientId = generateUniqueId();
|
||||
const gradient = createRadialGradient(gradientId, centerX, centerY, 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,
|
||||
@@ -263,6 +302,45 @@ function createLinearGradient(
|
||||
return gradient;
|
||||
}
|
||||
|
||||
function createRadialGradient(
|
||||
gradientId: string,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
stops: [Color, number][]
|
||||
): SVGRadialGradientElement {
|
||||
const gradient = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"radialGradient"
|
||||
);
|
||||
gradient.setAttribute("id", gradientId);
|
||||
gradient.setAttribute("cx", `${centerX}`);
|
||||
gradient.setAttribute("cy", `${centerY}`);
|
||||
gradient.setAttribute("r", "0.5");
|
||||
gradient.setAttribute("fx", `${centerX}`);
|
||||
gradient.setAttribute("fy", `${centerY}`);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Though the browser caches the icons for us, accessing the cached data
|
||||
// requires an asynchronous function call, which can lead to noticeable delays.
|
||||
// We'll make our own cache so that we can access the icons without ever
|
||||
@@ -292,6 +370,7 @@ export function applyIcon(
|
||||
fill:
|
||||
| SolidFill
|
||||
| LinearGradientFill
|
||||
| RadialGradientFill
|
||||
| ImageFill
|
||||
| Color
|
||||
| ColorSet
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "~1.76.0",
|
||||
"vite": "^5.2.12",
|
||||
"vite": "^5.4.14",
|
||||
"vite-plugin-compression2": "^1.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
84
rio/fills.py
84
rio/fills.py
@@ -18,6 +18,7 @@ __all__ = [
|
||||
"Fill",
|
||||
"ImageFill",
|
||||
"LinearGradientFill",
|
||||
"RadialGradientFill",
|
||||
"SolidFill",
|
||||
"FrostedGlassFill",
|
||||
]
|
||||
@@ -219,6 +220,82 @@ class ImageFill(Fill):
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, eq=True)
|
||||
class RadialGradientFill(Fill):
|
||||
"""
|
||||
Fills a shape with a circular gradient.
|
||||
|
||||
`RadialGradientFill` fills the shape with a circular gradient that emanates
|
||||
from a center point. The gradient can have any number of stops, each with a
|
||||
color and a position. The gradient will smoothly transition between the
|
||||
colors at the given positions. The positions are given as fractions, where 0
|
||||
is the center of the gradient and 1 is the edge.
|
||||
|
||||
## Attributes
|
||||
|
||||
`stops`: The different colors that comprise the gradient, along with where
|
||||
they are positioned.
|
||||
|
||||
The stops are given as tuples. Each tuple contains a color and a
|
||||
position. The position is a fraction, where 0 is the center of the
|
||||
gradient and 1 is the edge.
|
||||
|
||||
The order of the stops has no effect.
|
||||
|
||||
There must be at least one stop.
|
||||
|
||||
`center_x`: The x-coordinate of the center of the gradient, as a fraction
|
||||
of the shape's width. 0.5 is the center of the shape.
|
||||
|
||||
`center_y`: The y-coordinate of the center of the gradient, as a fraction
|
||||
of the shape's height. 0.5 is the center of the shape.
|
||||
"""
|
||||
|
||||
stops: tuple[tuple[Color, float], ...]
|
||||
|
||||
center_x: float = 0.5
|
||||
center_y: float = 0.5
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*stops: rio.Color | tuple[rio.Color, float],
|
||||
center_x: float = 0.5,
|
||||
center_y: float = 0.5,
|
||||
) -> None:
|
||||
# Postprocess & store the stops
|
||||
vars(self).update(
|
||||
stops=utils.verify_and_interpolate_gradient_stops(stops),
|
||||
center_x=center_x,
|
||||
center_y=center_y,
|
||||
)
|
||||
|
||||
def _as_css_background(self, sess: rio.Session) -> str:
|
||||
# Special case: Just one color
|
||||
if len(self.stops) == 1:
|
||||
return f"#{self.stops[0][0].hexa}"
|
||||
|
||||
# Proper gradient
|
||||
stop_strings = []
|
||||
|
||||
for stop in self.stops:
|
||||
color = stop[0]
|
||||
position = stop[1]
|
||||
stop_strings.append(f"#{color.hexa} {position * 100}%")
|
||||
|
||||
center_position = f"{self.center_x * 100}% {self.center_y * 100}%"
|
||||
return f"radial-gradient(circle at {center_position}, {', '.join(stop_strings)})"
|
||||
|
||||
def _serialize(self, sess: rio.Session) -> Jsonable:
|
||||
return {
|
||||
"type": "radialGradient",
|
||||
"stops": [
|
||||
(color.srgba, position) for color, position in self.stops
|
||||
],
|
||||
"centerX": self.center_x,
|
||||
"centerY": self.center_y,
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, eq=True)
|
||||
class FrostedGlassFill(Fill):
|
||||
"""
|
||||
@@ -248,5 +325,10 @@ class FrostedGlassFill(Fill):
|
||||
|
||||
|
||||
_FillLike: te.TypeAlias = (
|
||||
SolidFill | LinearGradientFill | ImageFill | FrostedGlassFill | Color
|
||||
SolidFill
|
||||
| LinearGradientFill
|
||||
| RadialGradientFill
|
||||
| ImageFill
|
||||
| FrostedGlassFill
|
||||
| Color
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user