├── LICENSE.md
├── README.md
├── measurements.png
└── widget-blur.js
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Maxwell Zeryck
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 | This project uses Mario Klingemann's [StackBlur](https://underdestruction.com/2004/02/25/stackblur-2004/). The following license appears in the source code and is reproduced below:
24 |
25 | Copyright (c) 2010 Mario Klingemann
26 |
27 | Permission is hereby granted, free of charge, to any person
28 | obtaining a copy of this software and associated documentation
29 | files (the "Software"), to deal in the Software without
30 | restriction, including without limitation the rights to use,
31 | copy, modify, merge, publish, distribute, sublicense, and/or sell
32 | copies of the Software, and to permit persons to whom the
33 | Software is furnished to do so, subject to the following
34 | conditions:
35 |
36 | The above copyright notice and this permission notice shall be
37 | included in all copies or substantial portions of the Software.
38 |
39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
40 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
41 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
42 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
43 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
44 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
45 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
46 | OTHER DEALINGS IN THE SOFTWARE.
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Widget Blur
2 | This script for the Scriptable app creates widget backgrounds that appear to have a translucent blur effect. It's based my original [transparent widget code](https://gist.github.com/mzeryck/3a97ccd1e059b3afa3c6666d27a496c9), but it adds the ability to emulate the light or dark blur effect used in the Batteries widget from Apple.
3 |
4 | Before you start, go to your home screen and enter "jiggle mode" (where you can move apps around). Go to the home screen on the far right, which has no icons in it. Take a screenshot here. You'll be prompted for this screenshot when the script runs.
5 |
--------------------------------------------------------------------------------
/measurements.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzeryck/Widget-Blur/bd85184a3b93cf8c456c018ad81be07ef86102aa/measurements.png
--------------------------------------------------------------------------------
/widget-blur.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: yellow; icon-glyph: magic;
4 |
5 | // This script was created by Max Zeryck.
6 |
7 | // The amount of blurring. Default is 150.
8 | const blurRadius = 150
9 |
10 | // Determine if user has taken the screenshot.
11 | let message = "Before you start, go to your home screen and enter wiggle mode. Scroll to the empty page on the far right and take a screenshot."
12 | const actions = {select: "Continue to select image",exit: "Exit to take screenshot",update: "Update code"}
13 | const actionOptions = [actions.select, actions.exit, actions.update]
14 | const actionResponse = await generateAlert(message,actionOptions)
15 |
16 | if (actionResponse.value == actions.exit) return
17 | if (actionResponse.value == actions.update) {
18 |
19 | // Determine if the user is using iCloud.
20 | let files = FileManager.local()
21 | const iCloudInUse = files.isFileStoredIniCloud(module.filename)
22 | files = iCloudInUse ? FileManager.iCloud() : files
23 |
24 | // Try to download the file.
25 | try {
26 | const req = new Request("https://raw.githubusercontent.com/mzeryck/Widget-Blur/main/widget-blur.js")
27 | const codeString = await req.loadString()
28 | files.writeString(module.filename, codeString)
29 | message = "The code has been updated. If the script is open, close it for the change to take effect."
30 | } catch {
31 | message = "The update failed. Please try again later."
32 | }
33 | return await generateAlert(message,["OK"])
34 | }
35 |
36 | // Get screenshot and determine phone size.
37 | let img = await Photos.fromLibrary()
38 | const height = img.size.height
39 | let phone = phoneSizes(height)
40 | if (!phone) {
41 | message = "It looks like you selected an image that isn't an iPhone screenshot, or your iPhone is not supported. Try again with a different image."
42 | return await generateAlert(message,["OK"])
43 | }
44 |
45 | // Extra setup needed for 2436-sized phones.
46 | if (height == 2436) {
47 |
48 | const files = FileManager.local()
49 | const cachePath = files.joinPath(files.libraryDirectory(), "mz-phone-type")
50 |
51 | // If we already cached the phone size, load it.
52 | if (files.fileExists(cachePath)) {
53 | const type = files.readString(cachePath)
54 | phone = phone[type]
55 |
56 | // Otherwise, prompt the user.
57 | } else {
58 | message = "What type of iPhone do you have?"
59 | const typeOptions = [{key: "mini", value: "iPhone 13 mini or 12 mini"}, {key: "x", value: "iPhone 11 Pro, XS, or X"}]
60 | const typeResponse = await generateAlert(message, typeOptions)
61 | phone = phone[typeResponse.key]
62 | files.writeString(cachePath, typeResponse.key)
63 | }
64 | }
65 |
66 | // If supported, check whether home screen has text labels or not.
67 | if (phone.text) {
68 | message = "What size are your home screen icons?"
69 | const textOptions = [{key: "text", value: "Small (has labels)"},{key: "notext", value: "Large (no labels)"}]
70 | const textResponse = await generateAlert(message, textOptions)
71 | phone = phone[textResponse.key]
72 | }
73 |
74 | // Prompt for widget size.
75 | message = "What size of widget are you creating?"
76 | const sizes = {small: "Small", medium: "Medium", large: "Large"}
77 | const sizeOptions = [sizes.small, sizes.medium, sizes.large]
78 | const size = (await generateAlert(message,sizeOptions)).value
79 |
80 | // Prompt for position.
81 | message = "What position will it be in?"
82 | message += (height == 1136 ? " (Note that your device only supports two rows of widgets, so the middle and bottom options are the same.)" : "")
83 |
84 | let positions
85 | if (size == sizes.small) {
86 | positions = ["Top left","Top right","Middle left","Middle right","Bottom left","Bottom right"]
87 | } else if (size == sizes.medium) {
88 | positions = ["Top","Middle","Bottom"]
89 | } else if (size == sizes.large) {
90 | positions = [{key: "top", value: "Top"},{key: "middle", value: "Bottom"}]
91 | }
92 | const position = (await generateAlert(message,positions)).key
93 |
94 | // Determine image crop based on the size and position.
95 | const crop = {
96 | w: (size == sizes.small ? phone.small : phone.medium),
97 | h: (size == sizes.large ? phone.large : phone.small),
98 | x: (size == sizes.small ? phone[position.split(" ")[1]] : phone.left),
99 | y: phone[position.toLowerCase().split(" ")[0]]
100 | }
101 |
102 | // Prompt for blur style and blur the image.
103 | message = "Do you want a fully transparent widget, or a translucent blur effect?"
104 | const blurs = {none: "Transparent (no blur)",light: "Light mode blur",dark: "Dark mode blur",blur: "Just blur"}
105 | const blurOptions = [blurs.none, blurs.light, blurs.dark, blurs.blur]
106 | const blurResponse = await generateAlert(message,blurOptions)
107 |
108 | if (blurResponse.value != blurs.none) img = await blurImage(img,blurResponse.key)
109 |
110 | // Crop the image.
111 | const draw = new DrawContext()
112 | draw.size = new Size(crop.w, crop.h)
113 | draw.drawImageAtPoint(img,new Point(-crop.x, -crop.y))
114 | img = draw.getImage()
115 |
116 | // Finalize and export the image.
117 | message = "Your widget background is ready. Choose where to save the image:"
118 | const exports = {photos: "Export to Photos", files: "Export to Files"}
119 | const exportOptions = [exports.photos, exports.files]
120 | const exportValue = (await generateAlert(message,exportOptions)).value
121 |
122 | if (exportValue == exports.photos) {
123 | Photos.save(img)
124 | } else if (exportValue == exports.files) {
125 | await DocumentPicker.exportImage(img)
126 | }
127 |
128 | Script.complete()
129 |
130 | // Generate an alert with the provided array of options.
131 | async function generateAlert(message,options) {
132 |
133 | const alert = new Alert()
134 | alert.message = message
135 |
136 | const isObject = options[0].value
137 | for (const option of options) {
138 | alert.addAction(isObject ? option.value : option)
139 | }
140 |
141 | const index = await alert.presentAlert()
142 | return {
143 | index: index,
144 | value: isObject ? options[index].value : options[index],
145 | key: isObject ? options[index].key : options[index]
146 | }
147 | }
148 |
149 | // Blur an image using the optional specified style.
150 | async function blurImage(img,style) {
151 | const js = `
152 | /*
153 |
154 | StackBlur - a fast almost Gaussian Blur For Canvas
155 |
156 | Version: 0.5
157 | Author: Mario Klingemann
158 | Contact: mario@quasimondo.com
159 | Website: http://quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
160 | Twitter: @quasimondo
161 |
162 | In case you find this class useful - especially in commercial projects -
163 | I am not totally unhappy for a small donation to my PayPal account
164 | mario@quasimondo.de
165 |
166 | Or support me on flattr:
167 | https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript
168 |
169 | Copyright (c) 2010 Mario Klingemann
170 |
171 | Permission is hereby granted, free of charge, to any person
172 | obtaining a copy of this software and associated documentation
173 | files (the "Software"), to deal in the Software without
174 | restriction, including without limitation the rights to use,
175 | copy, modify, merge, publish, distribute, sublicense, and/or sell
176 | copies of the Software, and to permit persons to whom the
177 | Software is furnished to do so, subject to the following
178 | conditions:
179 |
180 | The above copyright notice and this permission notice shall be
181 | included in all copies or substantial portions of the Software.
182 |
183 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
184 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
185 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
186 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
187 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
188 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
189 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
190 | OTHER DEALINGS IN THE SOFTWARE.
191 | */
192 |
193 | var mul_table = [
194 | 512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,
195 | 454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,
196 | 482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,
197 | 437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,
198 | 497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,
199 | 320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,
200 | 446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,
201 | 329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,
202 | 505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,
203 | 399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,
204 | 324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,
205 | 268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,
206 | 451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,
207 | 385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,
208 | 332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,
209 | 289,287,285,282,280,278,275,273,271,269,267,265,263,261,259];
210 |
211 |
212 | var shg_table = [
213 | 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
214 | 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
215 | 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
216 | 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
217 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
218 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
219 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
220 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
221 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
222 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
223 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
224 | 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
225 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
226 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
227 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
228 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ];
229 |
230 | function stackBlurCanvasRGB( id, top_x, top_y, width, height, radius )
231 | {
232 | if ( isNaN(radius) || radius < 1 ) return;
233 | radius |= 0;
234 |
235 | var canvas = document.getElementById( id );
236 | var context = canvas.getContext("2d");
237 | var imageData;
238 |
239 | try {
240 | try {
241 | imageData = context.getImageData( top_x, top_y, width, height );
242 | } catch(e) {
243 |
244 | // NOTE: this part is supposedly only needed if you want to work with local files
245 | // so it might be okay to remove the whole try/catch block and just use
246 | // imageData = context.getImageData( top_x, top_y, width, height );
247 | try {
248 | netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
249 | imageData = context.getImageData( top_x, top_y, width, height );
250 | } catch(e) {
251 | alert("Cannot access local image");
252 | throw new Error("unable to access local image data: " + e);
253 | return;
254 | }
255 | }
256 | } catch(e) {
257 | alert("Cannot access image");
258 | throw new Error("unable to access image data: " + e);
259 | }
260 |
261 | var pixels = imageData.data;
262 |
263 | var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum,
264 | r_out_sum, g_out_sum, b_out_sum,
265 | r_in_sum, g_in_sum, b_in_sum,
266 | pr, pg, pb, rbs;
267 |
268 | var div = radius + radius + 1;
269 | var w4 = width << 2;
270 | var widthMinus1 = width - 1;
271 | var heightMinus1 = height - 1;
272 | var radiusPlus1 = radius + 1;
273 | var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2;
274 |
275 | var stackStart = new BlurStack();
276 | var stack = stackStart;
277 | for ( i = 1; i < div; i++ )
278 | {
279 | stack = stack.next = new BlurStack();
280 | if ( i == radiusPlus1 ) var stackEnd = stack;
281 | }
282 | stack.next = stackStart;
283 | var stackIn = null;
284 | var stackOut = null;
285 |
286 | yw = yi = 0;
287 |
288 | var mul_sum = mul_table[radius];
289 | var shg_sum = shg_table[radius];
290 |
291 | for ( y = 0; y < height; y++ )
292 | {
293 | r_in_sum = g_in_sum = b_in_sum = r_sum = g_sum = b_sum = 0;
294 |
295 | r_out_sum = radiusPlus1 * ( pr = pixels[yi] );
296 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] );
297 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] );
298 |
299 | r_sum += sumFactor * pr;
300 | g_sum += sumFactor * pg;
301 | b_sum += sumFactor * pb;
302 |
303 | stack = stackStart;
304 |
305 | for( i = 0; i < radiusPlus1; i++ )
306 | {
307 | stack.r = pr;
308 | stack.g = pg;
309 | stack.b = pb;
310 | stack = stack.next;
311 | }
312 |
313 | for( i = 1; i < radiusPlus1; i++ )
314 | {
315 | p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 );
316 | r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i );
317 | g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs;
318 | b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs;
319 |
320 | r_in_sum += pr;
321 | g_in_sum += pg;
322 | b_in_sum += pb;
323 |
324 | stack = stack.next;
325 | }
326 |
327 |
328 | stackIn = stackStart;
329 | stackOut = stackEnd;
330 | for ( x = 0; x < width; x++ )
331 | {
332 | pixels[yi] = (r_sum * mul_sum) >> shg_sum;
333 | pixels[yi+1] = (g_sum * mul_sum) >> shg_sum;
334 | pixels[yi+2] = (b_sum * mul_sum) >> shg_sum;
335 |
336 | r_sum -= r_out_sum;
337 | g_sum -= g_out_sum;
338 | b_sum -= b_out_sum;
339 |
340 | r_out_sum -= stackIn.r;
341 | g_out_sum -= stackIn.g;
342 | b_out_sum -= stackIn.b;
343 |
344 | p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2;
345 |
346 | r_in_sum += ( stackIn.r = pixels[p]);
347 | g_in_sum += ( stackIn.g = pixels[p+1]);
348 | b_in_sum += ( stackIn.b = pixels[p+2]);
349 |
350 | r_sum += r_in_sum;
351 | g_sum += g_in_sum;
352 | b_sum += b_in_sum;
353 |
354 | stackIn = stackIn.next;
355 |
356 | r_out_sum += ( pr = stackOut.r );
357 | g_out_sum += ( pg = stackOut.g );
358 | b_out_sum += ( pb = stackOut.b );
359 |
360 | r_in_sum -= pr;
361 | g_in_sum -= pg;
362 | b_in_sum -= pb;
363 |
364 | stackOut = stackOut.next;
365 |
366 | yi += 4;
367 | }
368 | yw += width;
369 | }
370 |
371 |
372 | for ( x = 0; x < width; x++ )
373 | {
374 | g_in_sum = b_in_sum = r_in_sum = g_sum = b_sum = r_sum = 0;
375 |
376 | yi = x << 2;
377 | r_out_sum = radiusPlus1 * ( pr = pixels[yi]);
378 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]);
379 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]);
380 |
381 | r_sum += sumFactor * pr;
382 | g_sum += sumFactor * pg;
383 | b_sum += sumFactor * pb;
384 |
385 | stack = stackStart;
386 |
387 | for( i = 0; i < radiusPlus1; i++ )
388 | {
389 | stack.r = pr;
390 | stack.g = pg;
391 | stack.b = pb;
392 | stack = stack.next;
393 | }
394 |
395 | yp = width;
396 |
397 | for( i = 1; i <= radius; i++ )
398 | {
399 | yi = ( yp + x ) << 2;
400 |
401 | r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i );
402 | g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs;
403 | b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs;
404 |
405 | r_in_sum += pr;
406 | g_in_sum += pg;
407 | b_in_sum += pb;
408 |
409 | stack = stack.next;
410 |
411 | if( i < heightMinus1 )
412 | {
413 | yp += width;
414 | }
415 | }
416 |
417 | yi = x;
418 | stackIn = stackStart;
419 | stackOut = stackEnd;
420 | for ( y = 0; y < height; y++ )
421 | {
422 | p = yi << 2;
423 | pixels[p] = (r_sum * mul_sum) >> shg_sum;
424 | pixels[p+1] = (g_sum * mul_sum) >> shg_sum;
425 | pixels[p+2] = (b_sum * mul_sum) >> shg_sum;
426 |
427 | r_sum -= r_out_sum;
428 | g_sum -= g_out_sum;
429 | b_sum -= b_out_sum;
430 |
431 | r_out_sum -= stackIn.r;
432 | g_out_sum -= stackIn.g;
433 | b_out_sum -= stackIn.b;
434 |
435 | p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2;
436 |
437 | r_sum += ( r_in_sum += ( stackIn.r = pixels[p]));
438 | g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1]));
439 | b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2]));
440 |
441 | stackIn = stackIn.next;
442 |
443 | r_out_sum += ( pr = stackOut.r );
444 | g_out_sum += ( pg = stackOut.g );
445 | b_out_sum += ( pb = stackOut.b );
446 |
447 | r_in_sum -= pr;
448 | g_in_sum -= pg;
449 | b_in_sum -= pb;
450 |
451 | stackOut = stackOut.next;
452 |
453 | yi += width;
454 | }
455 | }
456 |
457 | context.putImageData( imageData, top_x, top_y );
458 |
459 | }
460 |
461 | function BlurStack()
462 | {
463 | this.r = 0;
464 | this.g = 0;
465 | this.b = 0;
466 | this.a = 0;
467 | this.next = null;
468 | }
469 |
470 | // https://gist.github.com/mjackson/5311256
471 |
472 | function rgbToHsl(r, g, b){
473 | r /= 255, g /= 255, b /= 255;
474 | var max = Math.max(r, g, b), min = Math.min(r, g, b);
475 | var h, s, l = (max + min) / 2;
476 |
477 | if(max == min){
478 | h = s = 0; // achromatic
479 | }else{
480 | var d = max - min;
481 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
482 | switch(max){
483 | case r: h = (g - b) / d + (g < b ? 6 : 0); break;
484 | case g: h = (b - r) / d + 2; break;
485 | case b: h = (r - g) / d + 4; break;
486 | }
487 | h /= 6;
488 | }
489 |
490 | return [h, s, l];
491 | }
492 |
493 | function hslToRgb(h, s, l){
494 | var r, g, b;
495 |
496 | if(s == 0){
497 | r = g = b = l; // achromatic
498 | }else{
499 | var hue2rgb = function hue2rgb(p, q, t){
500 | if(t < 0) t += 1;
501 | if(t > 1) t -= 1;
502 | if(t < 1/6) return p + (q - p) * 6 * t;
503 | if(t < 1/2) return q;
504 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
505 | return p;
506 | }
507 |
508 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
509 | var p = 2 * l - q;
510 | r = hue2rgb(p, q, h + 1/3);
511 | g = hue2rgb(p, q, h);
512 | b = hue2rgb(p, q, h - 1/3);
513 | }
514 |
515 | return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
516 | }
517 |
518 | function lightBlur(hsl) {
519 |
520 | // Adjust the luminance.
521 | let lumCalc = 0.35 + (0.3 / hsl[2]);
522 | if (lumCalc < 1) { lumCalc = 1; }
523 | else if (lumCalc > 3.3) { lumCalc = 3.3; }
524 | const l = hsl[2] * lumCalc;
525 |
526 | // Adjust the saturation.
527 | const colorful = 2 * hsl[1] * l;
528 | const s = hsl[1] * colorful * 1.5;
529 |
530 | return [hsl[0],s,l];
531 |
532 | }
533 |
534 | function darkBlur(hsl) {
535 |
536 | // Adjust the saturation.
537 | const colorful = 2 * hsl[1] * hsl[2];
538 | const s = hsl[1] * (1 - hsl[2]) * 3;
539 |
540 | return [hsl[0],s,hsl[2]];
541 |
542 | }
543 |
544 | // Capture variables from Scriptable.
545 | const style = "${style}"
546 | const blurRadius = ${blurRadius}
547 |
548 | // Set up the canvas.
549 | const img = document.getElementById("blurImg");
550 | const canvas = document.getElementById("mainCanvas");
551 |
552 | const w = img.naturalWidth;
553 | const h = img.naturalHeight;
554 |
555 | canvas.style.width = w + "px";
556 | canvas.style.height = h + "px";
557 | canvas.width = w;
558 | canvas.height = h;
559 |
560 | const context = canvas.getContext("2d");
561 | context.clearRect( 0, 0, w, h );
562 | context.drawImage( img, 0, 0 );
563 |
564 | // Get the image data from the context.
565 | const imageData = context.getImageData(0,0,w,h);
566 | const pix = imageData.data;
567 |
568 | // Set the image function, if any.
569 | let imageFunc;
570 | if (style == "dark") { imageFunc = darkBlur; }
571 | else if (style == "light") { imageFunc = lightBlur; }
572 |
573 | for (let i=0; i < pix.length; i+=4) {
574 |
575 | // Convert to HSL.
576 | let hsl = rgbToHsl(pix[i],pix[i+1],pix[i+2]);
577 |
578 | // Apply the image function if it exists.
579 | if (imageFunc) { hsl = imageFunc(hsl); }
580 |
581 | // Convert back to RGB.
582 | const rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
583 |
584 | // Put the values back into the data.
585 | pix[i] = rgb[0];
586 | pix[i+1] = rgb[1];
587 | pix[i+2] = rgb[2];
588 |
589 | }
590 |
591 | // Draw over the old image.
592 | context.putImageData(imageData,0,0);
593 |
594 | // Blur the image.
595 | stackBlurCanvasRGB("mainCanvas", 0, 0, w, h, blurRadius);
596 |
597 | // Perform the additional processing for dark images.
598 | if (style == "dark") {
599 |
600 | // Draw the hard light box over it.
601 | context.globalCompositeOperation = "hard-light";
602 | context.fillStyle = "rgba(55,55,55,0.2)";
603 | context.fillRect(0, 0, w, h);
604 |
605 | // Draw the soft light box over it.
606 | context.globalCompositeOperation = "soft-light";
607 | context.fillStyle = "rgba(55,55,55,1)";
608 | context.fillRect(0, 0, w, h);
609 |
610 | // Draw the regular box over it.
611 | context.globalCompositeOperation = "source-over";
612 | context.fillStyle = "rgba(55,55,55,0.4)";
613 | context.fillRect(0, 0, w, h);
614 |
615 | // Otherwise process light images.
616 | } else if (style == "light") {
617 | context.fillStyle = "rgba(255,255,255,0.4)";
618 | context.fillRect(0, 0, w, h);
619 | }
620 |
621 | // Return a base64 representation.
622 | canvas.toDataURL();
623 | `
624 |
625 | // Convert the images and create the HTML.
626 | const blurImgData = Data.fromPNG(img).toBase64String()
627 | const html = `
628 |
629 |
630 | `
631 |
632 | // Make the web view and get its return value.
633 | const view = new WebView()
634 | await view.loadHTML(html)
635 | const returnValue = await view.evaluateJavaScript(js)
636 |
637 | // Remove the data type from the string and convert to data.
638 | const imageData = Data.fromBase64String(returnValue.slice(22))
639 |
640 | // Convert to image and return.
641 | return Image.fromData(imageData)
642 | }
643 |
644 | /*
645 |
646 | How phoneSizes() works
647 | ======================
648 | This function takes the pixel height value of an iPhone screenshot and provides information about the sizes and locations of widgets on that iPhone. The "text" and "notext" properties refer to whether the home screen is set to Small (with text labels) or Large (no text labels).
649 |
650 | The remaining properties can be determined using a single screenshot of a home screen with 6 small widgets on it. You can see a visual representation of these properties by viewing this image: https://github.com/mzeryck/Widget-Blur/blob/main/measurements.png
651 |
652 | * The following properties define widget sizes:
653 | - small: The height of a small widget.
654 | - medium: From the left of the leftmost widget to the right of the rightmost widget.
655 | - large: From the top of a widget in the top row to the bottom of a widget in the middle row.
656 |
657 | * The following properties measure the distance from the left edge of the screen:
658 | - left: The distance to the left edge of widgets in the left column.
659 | - right: The distance to the left edge of widgets in the right column.
660 |
661 | * The following properties measure the distance from the top edge of the screen:
662 | - top: The distance to the top edge of widgets in the top row.
663 | - middle: The distance to the top edge of widgets in the middle row.
664 | - bottom: The distance to the top edge of widgets in the bottom row.
665 |
666 | */
667 | function phoneSizes(inputHeight) {
668 | return {
669 |
670 | /*
671 |
672 | Supported devices
673 | =================
674 | The following device measurements have been confirmed in iOS 18.
675 |
676 | */
677 |
678 | // 16 Pro Max
679 | "2868": {
680 | text: {
681 | small: 510,
682 | medium: 1092,
683 | large: 1146,
684 | left: 114,
685 | right: 696,
686 | top: 276,
687 | middle: 912,
688 | bottom: 1548
689 | },
690 | notext: {
691 | small: 530,
692 | medium: 1138,
693 | large: 1136,
694 | left: 91,
695 | right: 699,
696 | top: 276,
697 | middle: 882,
698 | bottom: 1488
699 | }
700 | },
701 |
702 | // 16 Plus, 15 Plus, 15 Pro Max, 14 Pro Max
703 | "2796": {
704 | text: {
705 | small: 510,
706 | medium: 1092,
707 | large: 1146,
708 | left: 98,
709 | right: 681,
710 | top: 252,
711 | middle: 888,
712 | bottom: 1524
713 | },
714 | notext: {
715 | small: 530,
716 | medium: 1139,
717 | large: 1136,
718 | left: 75,
719 | right: 684,
720 | top: 252,
721 | middle: 858,
722 | bottom: 1464
723 | }
724 | },
725 |
726 | // 16 Pro
727 | "2622": {
728 | text: {
729 | small: 486,
730 | medium: 1032,
731 | large: 1098,
732 | left: 87,
733 | right: 633,
734 | top: 261,
735 | middle: 872,
736 | bottom: 1485
737 | },
738 | notext: {
739 | small: 495,
740 | medium: 1037,
741 | large: 1035,
742 | left: 84,
743 | right: 626,
744 | top: 270,
745 | middle: 810,
746 | bottom: 1350
747 | }
748 | },
749 |
750 | // 16, 15, 15 Pro, 14 Pro
751 | "2556": {
752 | text: {
753 | small: 474,
754 | medium: 1017,
755 | large: 1062,
756 | left: 81,
757 | right: 624,
758 | top: 240,
759 | middle: 828,
760 | bottom: 1416
761 | },
762 | notext: {
763 | small: 495,
764 | medium: 1047,
765 | large: 1047,
766 | left: 66,
767 | right: 618,
768 | top: 243,
769 | middle: 795,
770 | bottom: 1347
771 | }
772 | },
773 |
774 | // 13 mini, 12 mini / 11 Pro, XS, X
775 | // Note that only the mini has been confirmed for iOS 18
776 | "2436": {
777 | x: {
778 | small: 465,
779 | medium: 987,
780 | large: 1035,
781 | left: 69,
782 | right: 591,
783 | top: 213,
784 | middle: 783,
785 | bottom: 1353
786 | },
787 | mini: {
788 | small: 465,
789 | medium: 987,
790 | large: 1035,
791 | left: 69,
792 | right: 591,
793 | top: 231,
794 | middle: 801,
795 | bottom: 1371
796 | }
797 | },
798 |
799 | // 13 mini, 12 mini in Display Zoom mode
800 | "2079": {
801 | small: 423,
802 | medium: 875,
803 | large: 933,
804 | left: 42,
805 | right: 494,
806 | top: 186,
807 | middle: 696,
808 | bottom: 1206
809 | },
810 |
811 | // SE3, SE2
812 | "1334": {
813 | text: {
814 | small: 296,
815 | medium: 642,
816 | large: 648,
817 | left: 54,
818 | right: 400,
819 | top: 60,
820 | middle: 412,
821 | bottom: 764
822 | },
823 | notext: {
824 | small: 309,
825 | medium: 667,
826 | large: 667,
827 | left: 41,
828 | right: 399,
829 | top: 67,
830 | middle: 425,
831 | bottom: 783
832 | }
833 | },
834 |
835 | /*
836 |
837 | In-limbo devices
838 | =================
839 | The following device measurements were confirmed in older versions of iOS.
840 | Please comment if you can confirm these for iOS 18.
841 |
842 | */
843 |
844 | // 14 Plus, 13 Pro Max, 12 Pro Max
845 | "2778": {
846 | small: 510,
847 | medium: 1092,
848 | large: 1146,
849 | left: 96,
850 | right: 678,
851 | top: 246,
852 | middle: 882,
853 | bottom: 1518
854 | },
855 |
856 | // 11 Pro Max, XS Max
857 | "2688": {
858 | small: 507,
859 | medium: 1080,
860 | large: 1137,
861 | left: 81,
862 | right: 654,
863 | top: 228,
864 | middle: 858,
865 | bottom: 1488
866 | },
867 |
868 | // 14, 13, 13 Pro, 12, 12 Pro
869 | "2532": {
870 | small: 474,
871 | medium: 1014,
872 | large: 1062,
873 | left: 78,
874 | right: 618,
875 | top: 231,
876 | middle: 819,
877 | bottom: 1407
878 | },
879 |
880 | // 11, XR
881 | "1792": {
882 | small: 338,
883 | medium: 720,
884 | large: 758,
885 | left: 55,
886 | right: 437,
887 | top: 159,
888 | middle: 579,
889 | bottom: 999
890 | },
891 |
892 | // 11 and XR in Display Zoom mode
893 | "1624": {
894 | small: 310,
895 | medium: 658,
896 | large: 690,
897 | left: 46,
898 | right: 394,
899 | top: 142,
900 | middle: 522,
901 | bottom: 902
902 | },
903 |
904 | /*
905 |
906 | Older devices
907 | =================
908 | The following devices cannot be updated to iOS 18 or later.
909 |
910 | */
911 |
912 | // Home button Plus phones
913 | "2208": {
914 | small: 471,
915 | medium: 1044,
916 | large: 1071,
917 | left: 99,
918 | right: 672,
919 | top: 114,
920 | middle: 696,
921 | bottom: 1278
922 | },
923 |
924 | // Home button Plus in Display Zoom mode
925 | "2001" : {
926 | small: 444,
927 | medium: 963,
928 | large: 972,
929 | left: 81,
930 | right: 600,
931 | top: 90,
932 | middle: 618,
933 | bottom: 1146
934 | },
935 |
936 | // SE1
937 | "1136": {
938 | small: 282,
939 | medium: 584,
940 | large: 622,
941 | left: 30,
942 | right: 332,
943 | top: 59,
944 | middle: 399,
945 | bottom: 399
946 | }
947 | }[inputHeight]
948 | }
949 |
--------------------------------------------------------------------------------