Skip to content

Instantly share code, notes, and snippets.

@Normal-Tangerine8609
Created January 17, 2022 01:38
Show Gist options
  • Save Normal-Tangerine8609/33f3000a7ddb1960033c7b38276c75aa to your computer and use it in GitHub Desktop.
Save Normal-Tangerine8609/33f3000a7ddb1960033c7b38276c75aa to your computer and use it in GitHub Desktop.
Scriptable HTML Gradient
/*
* HTMLGradient
*
* HTMLGradient(gradient: string): Promis<gradient>
*
* example
* ------------
* const widget = new ListWidget()
* widget.backgroundGradient = await HTMLGradient("to left, red, green 25%, blue-yellow")
* widget.presentSmall()
* ------------
*
* All parameters for the gradient are in a string and separated by commas.
*
* The first parameter is optional and is a degree or direction. If the first parameter is not a degree or direction, the gradient will go from the top to the bottom.
*
* Degrees are made with a number and the keyword `deg` directly beside it. `0deg` is a gradient going from the top to the bottom. The numbers continue around a clock, 3 O’clock would be `90deg` (from the left to the right) and so on. Invalid degrees result in `0deg`.
*
* example
* ------------
* await HTMLGradient("0deg, red, green, blue") //valid
* await HTMLGradient("55deg, red, green, blue") //valid
* await HTMLGradient("720deg, red, green, blue") //valid
* await HTMLGradient("0 deg, red, green, blue") //invalid
* ------------
*
* Directions are word directions like `to top left`, witch is from the bottom right to the top left. The valid directions are `to left`, `to right`, `to top`, `to bottom`, `to top left`, `to top right`, `to bottom left`, `to bottom right`, `to left top`, `to right top`, `to left bottom` and `to right bottom`. Invalid directions result in `0deg`.
*
* example
* ------------
* await HTMLGradient("to left, red, green, blue") //valid
* await HTMLGradient("to top right, red, green, blue") //valid
* await HTMLGradient("to top right, red, green, blue") //invalid
* await HTMLGradient("left, red, green, blue") //invalid
* ------------
*
* All other parameters are colours. Colours can be any supported HTML colour. If it is not a HTML colour, it will be black. Colours can also have a light or dark mode variation by separating the colours respectively by hyphens (`-`).
*
* example
* ------------
* await HTMLGradient("red, green, blue") //valid
* await HTMLGradient("hsl(180, 50%,50%), rgb(100, 235, 22)") //valid
* await HTMLGradient("red, rgba(255, 0, 0, 0%)") //valid
* await HTMLGradient("lab(56.29% -10.93 16.58 / 50%), color(sRGB 0 0.5 1 / 50%), lch(56.29% 19.86 236.62 / 50%)") //valid but all are black
* ------------
*
* Following the colour you can have a space and specify a location. Locations should go in ascending order and be or be between 0 and 1. Locations can be a percentage. Colours without a specified location will be placed a equal distance between adjacent colours.
*
* example
* ------------
* await HTMLGradient("red 50%, green, blue") //valid
* await HTMLGradient("red, green 0.33, blue 1") //valid
* await HTMLGradient("red 80%, green 10%, blue") //invalid
* await HTMLGradient("red -1, green, blue 1.1") //invalid
* ------------
*/
async function HTMLGradient(gradient) {
const input = gradient
//split gradient in parts
gradient = gradient.split(/,(?![^(]*\))(?![^"']*["'](?:[^"']*["'][^"']*["'])*[^"']*$)/).map((e) => e.trim())
let gradientDirection
const wordDirections = {
"to left": 90,
"to right": 270,
"to top": 180,
"to bottom": 0,
"to top left": 135,
"to top right": 225,
"to bottom left": 45,
"to bottom right": 315,
"to left top": 135,
"to right top": 225,
"to left bottom": 45,
"to right bottom": 315,
}
//set gradient direction
if(Object.keys(wordDirections).includes(gradient[0])) {
//set if direction is a word and remove first item from gradient array
gradientDirection = wordDirections[gradient.shift()]
} else if(/\d+\s*deg/.test(gradient[0])) {
//set if direction is a degree and remove first item from gradient array
gradientDirection = Number(gradient.shift().match(/(\d+)\s*deg/)[1])
} else {
//set default direction
gradientDirection = 0
}
//math stuff to figure out gradient points
let startPoint = new Point(1-(0.5 + 0.5 * Math.cos((Math.PI * (gradientDirection + 90)) / 180.0)), 1-(0.5 + 0.5 * Math.sin((Math.PI * (gradientDirection + 90)) / 180.0)))
let endPoint = new Point(0.5 + 0.5 * Math.cos((Math.PI * (gradientDirection + 90)) / 180.0), 0.5 + 0.5 * Math.sin((Math.PI * (gradientDirection + 90)) / 180.0))
//get colours and replace the location
let colours = []
for(let colour of gradient) {
colour = colour.replace(/\d*(\.\d+)?%?$/, "")
colour = colour.split("-")
if (colour.length == 2) {
colours.push(Color.dynamic(await colorFromValue(colour[0]), await colorFromValue(colour[1])))
} else {
colours.push(await colorFromValue(colour[0]))
}
}
//get locations of colours and account for percentages
let locations = gradient.map((e) =>
/\d*(\.\d+)?%?$/.test(e) ? e.match(/\d*(\.\d+)?%?$/)[0] : null
).map((e) => {
if(e) {
if(e.endsWith("%")) {
e = Number(e.replace("%", "")) / 100
}
}
//validate numbers
return (!isNaN(e) && !isNaN(parseFloat(e))) || typeof e == "number" ? Number(e) : null
})
//set first and last locations if not specified
if(!locations[0]) {
locations[0] = 0
}
if(!locations[locations.length - 1]) {
locations[locations.length - 1] = 1
}
//repeat with each location
let minLocation = 0
for(let i = 0; i < locations.length; i++) {
let currentLocation = locations[i]
//if currentLocation is specified
if(currentLocation) {
//basic errors
if(minLocation > currentLocation) {
throw new Error("Gradient Locations must be in ascending order: " + input)
}
if(currentLocation < 0) {
throw new Error("Gradient Locations must be equal or greater than 0: " + input)
}
if(currentLocation > 1) {
throw new Error("Gradient Locations must be equal or less than 1: " + input)
}
//set new minLocation
minLocation = currentLocation
} else {
let counter = 0
let index = i
//find all following locations without value
while(locations[index] === null) {
counter++
index++
}
//get difference between previous and future locations with value and divide between locations without value
let difference = (locations[index] - locations[i - 1]) / (counter + 1)
//set locations
for(let count = 0; count < counter; count++) {
locations[count + i] = difference * (count + 1) + locations[i - 1]
}
}
}
//create and return gradient
gradient = new LinearGradient()
gradient.colors = colours
gradient.locations = locations
gradient.startPoint = startPoint
gradient.endPoint = endPoint
return gradient
//use WebView() to get a html colour
async function colorFromValue(c) {
let w = new WebView()
await w.loadHTML(`<div id="div"style="color:${c}"></div>`)
let result = await w.evaluateJavaScript('window.getComputedStyle(document.getElementById("div")).color')
return rgbaToScriptable(...result.match(/\d+(\.\d+)?/g).map((e) => Number(e)))
function rgbaToScriptable(r, g, b, a) {
r = r.toString(16)
g = g.toString(16)
b = b.toString(16)
if(r.length == 1) {
r = "0" + r
}
if(g.length == 1) {
g = "0" + g
}
if(b.length == 1) {
b = "0" + b
}
if(a) {
a = Number(a)
} else {
a = 1
}
return new Color("#" + r + g + b, a)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment