-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathhtml-gradient.js
More file actions
208 lines (189 loc) · 7.64 KB
/
html-gradient.js
File metadata and controls
208 lines (189 loc) · 7.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: yellow; icon-glyph: magic;
/*
* HTMLGradient
*
* HTMLGradient(gradient: string): Promise<LinearGradient>
*
* 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 (space)
* ------------
*
* Directions are word directions like `to top left`, which 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 (too many spaces)
* await HTMLGradient("left, red, green, blue") // invalid (missing `to`)
* ------------
*
* All other parameters are colours. Colours can be any supported HTML colour: colour name, hex, rgb, rgba, hsl and hsla. 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("#0000ff80, rgba(255, 0, 0, 0%)") // valid (supports alpha)
* 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 (location are not in acceding order)
* await HTMLGradient("red -1, green, blue 1.1") // invalid (locations must be between 0 and 1)
* ------------
*/
// Example
const widget = new ListWidget();
widget.backgroundGradient = await HTMLGradient(
"to top right, #8a2387, rgb(233, 64, 87), hsl(23, 89%, 54%)"
);
widget.presentSmall();
async function HTMLGradient(gradient) {
// store colours for faster reuse. If you use this function multiple times, it may be better to move the following line to the top of your scriptable script
const colorCache = new Map();
// split into parts by commas not in quotes or brackets
let splitGradient = gradient
.split(/,(?![^(]*\))(?![^"']*["'](?:[^"']*["'][^"']*["'])*[^"']*$)/)
.map((e) => e.trim());
// get the direction from the first item of gradient
let gradientDirection;
const wordDirections = {
"to top": 0,
"to top right": 45,
"to right": 90,
"to bottom right": 135,
"to bottom": 180,
"to bottom left": 225,
"to left": 270,
"to top left": 315,
};
// check if it is a word direction, degrees direction or none are provided
const first = splitGradient[0].toLowerCase();
if (first in wordDirections) {
splitGradient.shift();
gradientDirection = wordDirections[first];
} else if (/\d+\s*deg/.test(first)) {
splitGradient.shift();
gradientDirection = Number(first.match(/(\d+)\s*deg/)[1]);
} else {
gradientDirection = 0;
}
// Get colours and locations
const colours = [];
const locations = [];
for (const part of splitGradient) {
// Get the location
const locationMatch = part.match(/\s+(\d+(?:\.\d+)?%?)$/);
let location = null;
let colorPart = part;
if (locationMatch) {
const rawLocation = locationMatch[1];
// Locations ending in % are percentages
if (rawLocation.endsWith("%")) {
location = Number(rawLocation.slice(0, -1)) / 100;
} else {
location = Number(rawLocation);
}
colorPart = part.slice(0, locationMatch.index).trim();
}
locations.push(location);
// Get the colour of the part
let color;
const [first, second] = colorPart.split("-");
if (second != null) {
const [scriptableFirst, scriptableSecond] = await Promise.all([
colorFromValue(first),
colorFromValue(second),
]);
color = Color.dynamic(scriptableFirst, scriptableSecond);
} else {
color = await colorFromValue(first);
}
colours.push(color);
}
// Set default first and last locations
if (locations[0] === null) {
locations[0] = 0;
}
if (locations.at(-1) === null) {
locations[locations.length - 1] = 1;
}
// Set not specified locations
for (let i = 0; i < locations.length; i++) {
let currentLocation = locations[i];
// get next non-null location index
let index = i + 1;
while (index < locations.length && locations[index] === null) {
index++;
}
// calculate the difference between each null location for a linear transition
let difference = (locations[index] - locations[i]) / (index - i);
// set each between null location
for (let plusIndex = 1; plusIndex < index - i; plusIndex++) {
locations[i + plusIndex] = difference * plusIndex + currentLocation;
}
}
// calculate gradient points based on the direction
const rad = (Math.PI * (gradientDirection - 90)) / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
const t = 0.5 / Math.max(Math.abs(cos), Math.abs(sin));
const x1 = 0.5 - t * cos;
const y1 = 0.5 - t * sin;
const x2 = 0.5 + t * cos;
const y2 = 0.5 + t * sin;
// create and return gradient
gradient = new LinearGradient();
gradient.colors = colours;
gradient.locations = locations;
gradient.startPoint = new Point(x1, y1);
gradient.endPoint = new Point(x2, y2);
return gradient;
// use WebView() to get a html colour
async function colorFromValue(c) {
if (colorCache.has(c)) return colorCache.get(c);
// hex colours are supported by scriptable
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(c)) {
return new Color(c);
}
// non-hex colours need to be identified
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'
);
const rgba = result.match(/\d+(\.\d+)?/g).map(Number);
const scriptable = rgbaToScriptable(...rgba);
colorCache.set(c, scriptable);
return scriptable;
function rgbaToScriptable(r, g, b, a = 1) {
const hex = (n) => n.toString(16).padStart(2, "0");
return new Color(`#${hex(r)}${hex(g)}${hex(b)}`, a);
}
}
}