add radial gradients

This commit is contained in:
Jakob Pinterits
2025-03-01 16:44:52 +01:00
parent 75374871a2
commit 80a5a3ee14
6 changed files with 214 additions and 6 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -7,7 +7,7 @@
},
"devDependencies": {
"sass": "~1.76.0",
"vite": "^5.2.12",
"vite": "^5.4.14",
"vite-plugin-compression2": "^1.1.1"
}
}

View File

@@ -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
)