Skip to content

Instantly share code, notes, and snippets.

@daneden
Last active February 1, 2022 09:12
Show Gist options
  • Save daneden/0ab8d1aa2737ad46f4277a245b7677b7 to your computer and use it in GitHub Desktop.
Save daneden/0ab8d1aa2737ad46f4277a245b7677b7 to your computer and use it in GitHub Desktop.
Scripter Script: CSS to Figma Fill
/// 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