Scripter Script: CSS to Figma Fill
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// Creates a rectangle with a gradient fill based on the string value of the selected text layer | |
/// i.e. Given a text layer with the content `linear-gradient(#000, #f00 75%, #fff)`, this script will create a rectangle at the same x,y as the text layer, with a gradient with three stops: black at 0, red at 0.75, and white at 1. | |
const currentSelection = selection() | |
if ( | |
currentSelection.length !== 1 || | |
!("characters" in currentSelection[0]) | |
) { | |
alert("Select a single text layer containing a CSS gradient string") | |
return | |
} | |
const currentLayer = currentSelection[0] | |
const gradientString = currentLayer.characters | |
const position = { | |
x: currentLayer.x, | |
y: currentLayer.y, | |
} | |
interface Bookmark { | |
index: number | |
position: number | |
} | |
interface TemporaryColorStop { | |
color: string | |
position: number | |
} | |
// Extract color stops from gradient expression | |
const stops = /[a-z-]+\((.+)\)/.exec(gradientString)[1] | |
// Create variables to hold the known stop positions and indices to the positions we need to interpolate between | |
var namedStops: Array<Bookmark> = [] | |
var lastBookmarkIndex = -1 | |
var nextBookmarkIndex = 0 | |
// A linear interpolating function | |
const lerp = (v0: number, v1: number, t: number) => { | |
return (1 - t) * v0 + t * v1 | |
} | |
// Let's get to work | |
const gradientStops = stops | |
// Split the stops and trim whitespace | |
.split(",") | |
.map((stop) => stop.trim()) | |
// Transform each stop into its color and position | |
.map((stop) => { | |
let parts = stop.split(" ") | |
return { | |
color: parts[0], | |
// For unnamed/inferred positions, set to -1 | |
position: parts[1] ?? "-100", | |
} | |
}) | |
// Turn each percentage position into a number between 0-1 | |
.map((stop) => { | |
return { | |
...stop, | |
position: parseInt(stop.position) / 100, | |
} | |
}) | |
// Create a map of known positions | |
.map((stop, index, array) => { | |
if (stop.position > -1) { | |
namedStops.push({ index, position: stop.position }) | |
} | |
// If it's the last stop, we need to make sure we express it with `position: 1` in the named stops array | |
if (index == array.length - 1) { | |
namedStops.push({ index, position: 1 }) | |
} | |
return stop | |
}) | |
// Calculate the interpolated/inferred stop positions | |
.reduce((stops: Array<TemporaryColorStop>, currentStop, index, originalStops) => { | |
// Create a temporary copy of the color stop | |
let stop = { | |
...currentStop, | |
} | |
// Get the next named color stop | |
// When there are no named color stops, this will refer to the last color in the gradient, with a position of `1` | |
let nextBookmark = namedStops[nextBookmarkIndex] | |
// If the stop we're processing already has a position, we just increment the indices and move on | |
if (namedStops.length !== 0 && index == nextBookmark.index) { | |
// Set the next bookmark | |
lastBookmarkIndex += 1 | |
nextBookmarkIndex += 1 | |
} else { | |
// Otherwise, we need to interpolate between the last named position and the next named position | |
let nextIndex = nextBookmark.index ?? 0 | |
let lastIndex = | |
lastBookmarkIndex == -1 ? 0 : namedStops[lastBookmarkIndex].index | |
let lastStopPosition = | |
lastBookmarkIndex == -1 ? 0 : namedStops[lastBookmarkIndex].position | |
let stride = nextIndex - lastIndex | |
let delta = nextIndex - stride | |
let mappedIndex = index - delta | |
let v = lerp(lastStopPosition, nextBookmark.position ?? 1, mappedIndex / stride) | |
stop.position = v | |
} | |
stops.push(stop) | |
return stops | |
}, []) | |
// Make sure that positions are absolute values | |
.map((stop) => ({ | |
...stop, | |
position: Math.abs(stop.position) | |
})) | |
// Transform into Figma-friendly format | |
.map(stop => { | |
return { | |
...stop, | |
color: Color(stop.color.replace("#", "")) | |
} | |
}) | |
// Add an alpha component | |
.map(stop => { | |
return { | |
...stop, | |
color: { | |
...stop.color, | |
a: 1 | |
} | |
} | |
}) | |
// Create the gradient | |
const gradient: GradientPaint = { | |
type: "GRADIENT_LINEAR", | |
visible: true, | |
opacity: 1, | |
blendMode: "NORMAL", | |
gradientStops, | |
gradientTransform: [[1, 0, 0], [0, 1, 0]] | |
} | |
// Create a rectangle in the same x,y as the text layer | |
createRectangle({ | |
fills: [gradient], | |
...position | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment