├── 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 | --------------------------------------------------------------------------------