foreword
After ios users update to iOS14, our iPhone and other ios devices support our user-defined desktop widgets (or called widgets, desktop pendants). Using this feature, many such as transparent clocks, micro For the iPhone desktops of Bo Hot Search, Zhihu Hot List, NetEase Cloud Hot Review, Tesla, BMW, MG, Audi, etc., see the actual renderings below:
So how is this achieved in the end, how can we make our own iPhone personalized desktop? What I'm sharing with you today is Scriptable's desktop gameplay. For javascript developers, after reading this tutorial, it's easy to get started with developing applications for small objects, and for students who have no programming foundation, don't worry about not understanding, all you need is All you have to do is copy and paste, skip the development tutorial directly, and look at the fast track at the end of the article.
Introduction to Scriptable
This is an app that lets you automate building iOS using JavaScript
The above is the official explanation of Scriptable, which is undoubtedly a boon for front-end developers, because Scriptable uses Apple's JavaScriptCore , which supports ECMAScript 6 default to develop and build widgets.
If you're just getting started with JavaScript, you might want to take a look at Codecademys Intro to Programming in JavaScript . For a quick reference on JavaScript functionality, you can refer to W3Schools' JavaScript Tutorial .
Note that some guides and tutorials will assume you are running JavaScript in your browser, so you can access browser-specific objects such as documents. Scriptable does not run JavaScript in the browser, so there is no such object.
For more explanation of Scriptable, please read official document
Key Features
Look at a picture first:
Listed above are some of the features of Scriptable
- supports ES6 syntax
- You can use JavaScript to call some native APIs
- Siri Shortcuts
- Complete documentation support
- Shared Table Extension
- Filesystem inheritance
- editor
- Code Sample
- and interacting with other apps through x-callback-url
Do you feel that there are still a lot of supported features, and these features are enough for us to implement many native-level underlying interactions.
first widget program
// 判断是否是运行在桌面的组件中
if (config.runsInWidget) {
// 创建一个显示元素列表的小部件
// 显示元素列表的小部件。将小部件传递给 Script.setWidget() 将其显示在您的主屏幕上。
// 请注意,小部件会定期刷新,小部件刷新的速率很大程度上取决于操作系统。
// 另请注意,在小部件中运行脚本时存在内存限制。当使用太多内存时,小部件将崩溃并且无法正确呈现。
const widget = new ListWidget();
// 添加文本物件
const text = widget.addText("Hello, World!");
// 设置字体颜色
text.textColor = new Color("#000000");
// 设置字体大小
text.font = Font.boldSystemFont(36);
// 设置文字对齐方式
text.centerAlignText();
// 新建线性渐变物件
const gradient = new LinearGradient();
// 每种颜色的位置,每个位置应该是 0 到 1 范围内的值,并指示渐变colors数组中每种颜色的位置
gradient.locations = [0, 1];
// 渐变的颜色。locations颜色数组应包含与渐变属性相同数量的元素。
gradient.colors = [new Color("#F5DB1A"), new Color("#F3B626")];
// 把设置好的渐变色配置给显示元素列表的小部件背景
widget.backgroundGradient = gradient;
// 设置部件
Script.setWidget(widget);
}
From the above simple program that displays "Hello, World!" and sets the background color and text style, there is an important concept that javascript programmers need to understand and convert from traditional web development concepts. If you have If you have developed Flutter
development experience, then for you, developing Scriptable applications should resonate. Because for me, Scriptable is also the concept that everything is a component (widget), and an important idea that supports this is object-oriented.
Everything is a component
Why is everything a component? Whether it is a container (div), a style (color, style) or an element (font), etc. are all objects, for example, if you want to display a line of text "Hello, World!", then you must first have a container (div) to load This line of text (fonts), you have to set the styles (styles) for the text, that style is not to say that it is created out of thin air, all objects must be new. Contrast the above "Hello, World!" example to further understand this concept.
The above concepts have an extremely important and positive effect on Scriptable application development, especially for junior front-end developers or developers without native app development experience, it is difficult for them to think about object-oriented without the traditional web development mode of mvvc or mvc development mode.
Components commonly used in high frequency
ListWidge
A widget that displays a list of elements, the most commonly used container component. The root element of the general component application is wrapped in ListWidget, and only this component can be passed to Script.setWidget() to display it on your home screen.
Note that widgets are refreshed periodically, and the rate at which widgets refresh is largely dependent on the operating system. Note: Using this, you can do many applications that need to be refreshed based on timing, such as: holiday anniversaries, applications that need to calculate the current time.
Also note that there are memory limitations when running scripts in widgets. When using too much memory, the widget will crash and not render properly.
-addStack
addStack(): WidgetStack
Add stack.
ListWidget.addStack()
return value of WidgetStack
is 061e3bce0c0925 (stack element). Adding stack elements to ListWidget is a horizontal layout. You can use this api
achieve a layout flex
-addSpacer
addSpacer(length: number): WidgetSpacer
Add spacers to widgets. This can be used to vertically offset content in widgets. margin
of css in web development
-setPadding
setPadding(top: number, leading: number, bottom: number, trailing: number)
Sets the padding on each side of the widget. padding
similar to css in web
-addText
addText(text: string): WidgetText
Add a text element to the widget. Use the attributes of the returned element to style the text. The analogy is to insert text nodes into divs in web development.
backgroundColor
backgroundColor: Color
Set the background color of the container, the value must be of Color type ( new Color('#fff', 1)
), the first parameter of the Color constructor is the color value, and the second parameter is the transparency, similar to rgba(255,255,255,1) in web development
backgroundImage
backgroundImage: Image
Sets the background image for the container. backgroud-image
similar to css in web
Font
Indicates font and text size.
new Font(name: string, size: number)
This font can be used to style text, for example in widgets.
- regularSystemFont
Create regular system fonts.
static regularSystemFont(size: number): Font
-lightSystemFont
Create a day mode system font.
static lightSystemFont(size: number): Font
-thinSystemFont
Create thin system fonts.
static thinSystemFont(size: number): Font
Keychain
A keychain is a secure store of credentials, keys, etc. Use the set()
method to add the value to the keychain. You can then retrieve the value get()
-contains
Check if the keychain contains the key.
static contains(key: string): bool
Checks if the keychain contains the specified key.
-set
Adds the value of the specified key to the keychain.
static set(key: string, value: string)
Adds a value to the keychain, assigning it to the specified key. If the key already exists in the keychain, the value will be overwritten.
Values are securely stored in an encrypted database.
-get
Read a value from the keychain.
static get(key: string): string
Read the value of the specified key. If the key does not exist, the method will throw an error. Use the contains
method to check if the key exists in the keychain.
Alert
Displays a modal popup. Similar to Modal components in web ui
Use it to configure popups that are presented as modals or forms. After configuring the popup, call presentAlert() or presentSheet() to present the popup. Both representations will return a value that carries the index of the operation selected when completed. For example, if you add two operation buttons to the pop-up window, first add one is confirm, the other is
cancel button, the adding operation is consistent with the array in js, the index of the button added first is 0, when the user clicks the
confirm button ,
alert.presentAlert()
is the index value of 'confirmation' in the configuration array, which is 0.
Personally think that this component is also a very high frequency component, because for advanced desktop components or complex components, especially some desktop components that require user login account information, a pop-up window is required to allow users to enter account passwords and other interactive behaviors, or The Alert component is the best choice for scenarios that require persistent storage of user input dates, names, etc.
-message
title: string
The title displayed in the popup. Usually a short string.
-addAction
Add action buttons to the popup. To check if an action is selected, you should use the first parameter provided when Promise
// 创建一个弹窗组件
let alert = new Alert();
// 设置弹窗中显示的content
alert.message = '弹窗中显示的内容,这里可以展示对操作的解释等文案信息...';
// 向弹窗中加入一个按钮-确定,索引为0
alert.addAction('确定');
// 向弹窗中加入一个按钮-取消,所以为1
alert.addAction('取消');
// 获取弹窗按钮被触发后拿到用户点击的具体某个按钮索引,如果点击确定,response === 0 否则 response === 1
let response = await alert.presentAlert();
-addCancelAction
addCancelAction(title: string)
Add cancel action to popup. When choosing to cancel the operation, the index provided by kidealert() or vistentheet() will always be -1. Note that when running on the iPad and presenting with presentSheet(), the action does not appear in the action list. Actions can be canceled by clicking outside the sheet.
A popup can only contain one cancel action. Attempting to add more cancellations will remove any previously added cancellations.
-presentAlert
Display a modal pop-up window, similar to elementui
of modal
in visible
set to true
, and the pop-up window is displayed at this time.
-presentSheet
Pop up the popup window in an interactive manner similar to bottomSheet.
Image
Manage image data.
Image objects contain image data. APIs in the Scriptable that process images (either by taking an image as input or returning an image) will use this Image type.
-size
size: Size
The size of the image in pixels. read only
-fromFile
Load the image from the specified file path. If the image cannot be read, the function will return null. Similar to reading local (and iCloud in ios) image files in web development
-fromData
static fromData(data: Data): Image
Load images from raw data. If the image cannot be read, the function will return null.
Data can be raw data representations of strings, files, and images. For example, Image is mostly used to read images from base64 strings. The pseudo code example is as follows:
let imageDataString = 'base64:xxxxx'
let imageData = Data.fromBase64String(imageDataString)
// Convert to image and crop before returning.
let imageFromData = Image.fromData(imageData)
// return Image(imageFromData)
return imageFromData
For more other APIs about Data, please refer to the document
Photos
Provides access to your photo library.
In order to read from your photo library, you must give the app permission to access your photo library. When using the API for the first time, the app will prompt for access, but if you deny the request, all API calls will fail. In this case, you must enable access to the photo library from the system settings.
This api is also used in a relatively high frequency, because in most scenarios, your widgets need to use images or backgrounds, and most scenarios using images (especially background images) need to access your device gallery, and also It is your album. Of course, the use of the album function must be under the premise of user authorization.
-fromLibrary
static fromLibrary(): Promise<Image>
Displays the photo library for selecting images, use it to pick images from the photo library.
use it:
const img = await Photos.fromLibrary();
// 拿到Image对象后,可以对它做缓存、展示、传输等等用途
-latestPhoto
Get the latest photos.
static latestPhoto(): Promise<Image>
Read the latest photos from your photo library. If no photos are available, the pledge will be rejected.
-latestScreenshot
Get the latest screenshots.
static latestScreenshot(): Promise<Image>
Read the latest screenshots from your photo library. If no screenshot is available, the Promise will be rejected.
Pasteboard
Copy and paste strings or images.
Copy and paste strings and images from the clipboard.
-copy
Copy the string to the pasteboard.
static copy(string: string)
-paste
Paste the string from the clipboard.
static paste(): string
-copyImage
Copy the image to the pasteboard.
static copyImage(image: Image)
LinearGradient
Linear gradient.
Linear gradient to use in the widget.
-colors
Gradient colors.
locations
color array should contain the same number of elements as the gradient property.
colors: [Color]
Similar to linear-gradient
attribute in CSS, indicating the color range of the gradient
.horizontal-gradient {
background: linear-gradient(to right, blue, pink);
}
-locations
position of each color.
Each position should be a value in the range 0 to 1 and indicates the position of each color in the colors
colors
position array should contain the same number of elements as the gradient attribute.
locations: [number]
const bg = new LinearGradient()
bg.locations = [0, 1]
bg.colors = [
new Color('#f35942', 1),
new Color('#e92d1d', 1)
]
w.backgroundGradient = bg
FileManager
This API is suitable for caching data. It is one of the more commonly used APIs and is used frequently.
-local
Create a local FileManager.
static local(): FileManager
Create a file manager for manipulating locally stored files.
const files = FileManager.local();
-iCloud
Create an iCloud file manager.
static iCloud(): FileManager
Create a file manager for working with files stored in iCloud. iCloud must be enabled on the device to use it.
-read
Read the contents of the file as data.
read(filePath: string): Data
Read the contents of the file specified by the file path as raw data. To read the file as the string readString(filePath)
, see and read it as an image, see readImage(filePath)
.
The function will error if the file does not exist or exists in iCloud but has not been downloaded. Used for fileExists(filePath)
check if the file exists and downloadFileFromiCloud(filePath)
download the file. Note that it is always safe to call downloadFileFromiCloud(filePath)
, even if the file is stored locally on the device.
-readImage
Read the contents of the file as an image.
readImage(filePath: string): Image
Reads the contents of the file specified by the file path and converts it to an image.
// 读取自己在本地缓存的图片
const img = files.readImage(files.joinPath(files.documentsDirectory(), "avatar.jpg"))
-write
Write data to a file.
write(filePath: string, content: Data)
-writeImage
Write the image to a file.
writeImage(filePath: string, image: Image)
Writes the image to the specified file path on disk. If the file does not already exist, it will be created. If the file already exists, the contents of the file will be overwritten with the new contents.
-fileExists
Check if the file exists.
fileExists(filePath: string): bool
Check if the file exists in the specified file path. It might be a good idea to check this before moving or copying to the destination, as these operations will replace any existing files in the destination file path.
-documentsDirectory
The path to the documentation directory.
documentsDirectory(): string
Path to retrieve the document directory. Your scripts are stored in this directory . If you have iCloud enabled, your scripts will be stored in iCloud's Documents directory, otherwise they will be stored in your local Documents directory. This directory can be used for long-term storage. Documents stored in this directory can be accessed using the Files application. Files stored in the local Documents directory do not appear in the Files application.
-joinPath
Connect the two path components. The function is the same as node
in joinPath
joinPath(lhsPath: string, rhsPath: string): string
Connect two paths to create one path. For example, concatenate the path to the directory with the filename. This is the recommended way to create new file paths that are passed to the FileManager's read and write functions.
Common methods of packaging
network request
/**
* HTTP 请求接口
* @param {string} url 请求的url
* @param {bool} json 返回数据是否为 json,默认 true
* @param {bool} useCache 是否采用离线缓存(请求失败后获取上一次结果),
* @return {string | json | null}
*/
async httpGet(url, json = true, useCache = false) {
let data = null
const cacheKey = this.md5(url)
if (useCache && Keychain.contains(cacheKey)) {
let cache = Keychain.get(cacheKey)
return json ? JSON.parse(cache) : cache
}
try {
let req = new Request(url)
data = await (json ? req.loadJSON() : req.loadString())
} catch (e) {}
// 判断数据是否为空(加载失败)
if (!data && Keychain.contains(cacheKey)) {
// 判断是否有缓存
let cache = Keychain.get(cacheKey)
return json ? JSON.parse(cache) : cache
}
// 存储缓存
Keychain.set(cacheKey, json ? JSON.stringify(data) : data)
return data
}
Get remote pictures
/**
* 获取远程图片内容
* @param {string} url 图片地址
* @param {bool} useCache 是否使用缓存(请求失败时获取本地缓存)
*/
async getImageByUrl(url, useCache = true) {
const cacheKey = this.md5(url)
const cacheFile = FileManager.local().joinPath(FileManager.local().temporaryDirectory(), cacheKey)
// 判断是否有缓存
if (useCache && FileManager.local().fileExists(cacheFile)) {
return Image.fromFile(cacheFile)
}
try {
const req = new Request(url)
const img = await req.loadImage()
// 存储到缓存
FileManager.local().writeImage(cacheFile, img)
return img
} catch (e) {
// 没有缓存+失败情况下,返回自定义的绘制图片(红色背景)
throw new Error('加载图片失败');
}
}
background image with transparency
async function shadowImage(img) {
let ctx = new DrawContext()
// 把画布的尺寸设置成图片的尺寸
ctx.size = img.size
// 把图片绘制到画布中
ctx.drawImageInRect(img, new Rect(0, 0, img.size['width'], img.size['height']))
// 设置绘制的图层颜色,为半透明的黑色
ctx.setFillColor(new Color('#000000', 0.5))
// 绘制图层
ctx.fillRect(new Rect(0, 0, img.size['width'], img.size['height']))
// 导出最终图片
return await ctx.getImage()
}
get time difference
function getDistanceSpecifiedTime(dateTime) {
// 指定日期和时间
var EndTime = new Date(dateTime);
// 当前系统时间
var NowTime = new Date();
var t = EndTime.getTime() - NowTime.getTime();
var d = Math.floor(t / 1000 / 60 / 60 / 24);
var h = Math.floor(t / 1000 / 60 / 60 % 24);
var m = Math.floor(t / 1000 / 60 % 60);
var s = Math.floor(t / 1000 % 60);
return d;
}
All supported mobile widget pixel sizes and positions
Commonly used to set a pseudo transparent background
// Pixel sizes and positions for widgets on all supported phones.
function phoneSizes() {
let phones = {
// 12 and 12 Pro
"2532": {
small: 474,
medium: 1014,
large: 1062,
left: 78,
right: 618,
top: 231,
middle: 819,
bottom: 1407
},
// 11 Pro Max, XS Max
"2688": {
small: 507,
medium: 1080,
large: 1137,
left: 81,
right: 654,
top: 228,
middle: 858,
bottom: 1488
},
// 11, XR
"1792": {
small: 338,
medium: 720,
large: 758,
left: 54,
right: 436,
top: 160,
middle: 580,
bottom: 1000
},
// 11 Pro, XS, X
"2436": {
small: 465,
medium: 987,
large: 1035,
left: 69,
right: 591,
top: 213,
middle: 783,
bottom: 1353
},
// Plus phones
"2208": {
small: 471,
medium: 1044,
large: 1071,
left: 99,
right: 672,
top: 114,
middle: 696,
bottom: 1278
},
// SE2 and 6/6S/7/8
"1334": {
small: 296,
medium: 642,
large: 648,
left: 54,
right: 400,
top: 60,
middle: 412,
bottom: 764
},
// SE1
"1136": {
small: 282,
medium: 584,
large: 622,
left: 30,
right: 332,
top: 59,
middle: 399,
bottom: 399
},
// 11 and XR in Display Zoom mode
"1624": {
small: 310,
medium: 658,
large: 690,
left: 46,
right: 394,
top: 142,
middle: 522,
bottom: 902
},
// Plus in Display Zoom mode
"2001" : {
small: 444,
medium: 963,
large: 972,
left: 81,
right: 600,
top: 90,
middle: 618,
bottom: 1146
}
}
return phones
}
Get the component clipping in the screenshot
/**
* 获取截图中的组件剪裁图
* 可用作透明背景
* 返回图片image对象
* 代码改自:https://gist.github.com/mzeryck/3a97ccd1e059b3afa3c6666d27a496c9
* @param {string} title 开始处理前提示用户截图的信息,可选(适合用在组件自定义透明背景时提示)
*/
async getWidgetScreenShot (title = null) {
// Generate an alert with the provided array of options.
async function generateAlert(message,options) {
let alert = new Alert()
alert.message = message
for (const option of options) {
alert.addAction(option)
}
let response = await alert.presentAlert()
return response
}
// Crop an image into the specified rect.
function cropImage(img,rect) {
let draw = new DrawContext()
draw.size = new Size(rect.width, rect.height)
draw.drawImageAtPoint(img,new Point(-rect.x, -rect.y))
return draw.getImage()
}
async function blurImage(img,style) {
const blur = 150
const js = `
var mul_table=[512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,289,287,285,282,280,278,275,273,271,269,267,265,263,261,259];var shg_table=[9,11,12,13,13,14,14,15,15,15,15,16,16,16,16,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24];function stackBlurCanvasRGB(id,top_x,top_y,width,height,radius){if(isNaN(radius)||radius<1)return;radius|=0;var canvas=document.getElementById(id);var context=canvas.getContext("2d");var imageData;try{try{imageData=context.getImageData(top_x,top_y,width,height)}catch(e){try{netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");imageData=context.getImageData(top_x,top_y,width,height)}catch(e){alert("Cannot access local image");throw new Error("unable to access local image data: "+e);return}}}catch(e){alert("Cannot access image");throw new Error("unable to access image data: "+e);}var pixels=imageData.data;var x,y,i,p,yp,yi,yw,r_sum,g_sum,b_sum,r_out_sum,g_out_sum,b_out_sum,r_in_sum,g_in_sum,b_in_sum,pr,pg,pb,rbs;var div=radius+radius+1;var w4=width<<2;var widthMinus1=width-1;var heightMinus1=height-1;var radiusPlus1=radius+1;var sumFactor=radiusPlus1*(radiusPlus1+1)/2;var stackStart=new BlurStack();var stack=stackStart;for(i=1;i<div;i++){stack=stack.next=new BlurStack();if(i==radiusPlus1)var stackEnd=stack}stack.next=stackStart;var stackIn=null;var stackOut=null;yw=yi=0;var mul_sum=mul_table[radius];var shg_sum=shg_table[radius];for(y=0;y<height;y++){r_in_sum=g_in_sum=b_in_sum=r_sum=g_sum=b_sum=0;r_out_sum=radiusPlus1*(pr=pixels[yi]);g_out_sum=radiusPlus1*(pg=pixels[yi+1]);b_out_sum=radiusPlus1*(pb=pixels[yi+2]);r_sum+=sumFactor*pr;g_sum+=sumFactor*pg;b_sum+=sumFactor*pb;stack=stackStart;for(i=0;i<radiusPlus1;i++){stack.r=pr;stack.g=pg;stack.b=pb;stack=stack.next}for(i=1;i<radiusPlus1;i++){p=yi+((widthMinus1<i?widthMinus1:i)<<2);r_sum+=(stack.r=(pr=pixels[p]))*(rbs=radiusPlus1-i);g_sum+=(stack.g=(pg=pixels[p+1]))*rbs;b_sum+=(stack.b=(pb=pixels[p+2]))*rbs;r_in_sum+=pr;g_in_sum+=pg;b_in_sum+=pb;stack=stack.next}stackIn=stackStart;stackOut=stackEnd;for(x=0;x<width;x++){pixels[yi]=(r_sum*mul_sum)>>shg_sum;pixels[yi+1]=(g_sum*mul_sum)>>shg_sum;pixels[yi+2]=(b_sum*mul_sum)>>shg_sum;r_sum-=r_out_sum;g_sum-=g_out_sum;b_sum-=b_out_sum;r_out_sum-=stackIn.r;g_out_sum-=stackIn.g;b_out_sum-=stackIn.b;p=(yw+((p=x+radius+1)<widthMinus1?p:widthMinus1))<<2;r_in_sum+=(stackIn.r=pixels[p]);g_in_sum+=(stackIn.g=pixels[p+1]);b_in_sum+=(stackIn.b=pixels[p+2]);r_sum+=r_in_sum;g_sum+=g_in_sum;b_sum+=b_in_sum;stackIn=stackIn.next;r_out_sum+=(pr=stackOut.r);g_out_sum+=(pg=stackOut.g);b_out_sum+=(pb=stackOut.b);r_in_sum-=pr;g_in_sum-=pg;b_in_sum-=pb;stackOut=stackOut.next;yi+=4}yw+=width}for(x=0;x<width;x++){g_in_sum=b_in_sum=r_in_sum=g_sum=b_sum=r_sum=0;yi=x<<2;r_out_sum=radiusPlus1*(pr=pixels[yi]);g_out_sum=radiusPlus1*(pg=pixels[yi+1]);b_out_sum=radiusPlus1*(pb=pixels[yi+2]);r_sum+=sumFactor*pr;g_sum+=sumFactor*pg;b_sum+=sumFactor*pb;stack=stackStart;for(i=0;i<radiusPlus1;i++){stack.r=pr;stack.g=pg;stack.b=pb;stack=stack.next}yp=width;for(i=1;i<=radius;i++){yi=(yp+x)<<2;r_sum+=(stack.r=(pr=pixels[yi]))*(rbs=radiusPlus1-i);g_sum+=(stack.g=(pg=pixels[yi+1]))*rbs;b_sum+=(stack.b=(pb=pixels[yi+2]))*rbs;r_in_sum+=pr;g_in_sum+=pg;b_in_sum+=pb;stack=stack.next;if(i<heightMinus1){yp+=width}}yi=x;stackIn=stackStart;stackOut=stackEnd;for(y=0;y<height;y++){p=yi<<2;pixels[p]=(r_sum*mul_sum)>>shg_sum;pixels[p+1]=(g_sum*mul_sum)>>shg_sum;pixels[p+2]=(b_sum*mul_sum)>>shg_sum;r_sum-=r_out_sum;g_sum-=g_out_sum;b_sum-=b_out_sum;r_out_sum-=stackIn.r;g_out_sum-=stackIn.g;b_out_sum-=stackIn.b;p=(x+(((p=y+radiusPlus1)<heightMinus1?p:heightMinus1)*width))<<2;r_sum+=(r_in_sum+=(stackIn.r=pixels[p]));g_sum+=(g_in_sum+=(stackIn.g=pixels[p+1]));b_sum+=(b_in_sum+=(stackIn.b=pixels[p+2]));stackIn=stackIn.next;r_out_sum+=(pr=stackOut.r);g_out_sum+=(pg=stackOut.g);b_out_sum+=(pb=stackOut.b);r_in_sum-=pr;g_in_sum-=pg;b_in_sum-=pb;stackOut=stackOut.next;yi+=width}}context.putImageData(imageData,top_x,top_y)}function BlurStack(){this.r=0;this.g=0;this.b=0;this.a=0;this.next=null}
// https://gist.github.com/mjackson/5311256
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function lightBlur(hsl) {
// Adjust the luminance.
let lumCalc = 0.35 + (0.3 / hsl[2]);
if (lumCalc < 1) { lumCalc = 1; }
else if (lumCalc > 3.3) { lumCalc = 3.3; }
const l = hsl[2] * lumCalc;
// Adjust the saturation.
const colorful = 2 * hsl[1] * l;
const s = hsl[1] * colorful * 1.5;
return [hsl[0],s,l];
}
function darkBlur(hsl) {
// Adjust the saturation.
const colorful = 2 * hsl[1] * hsl[2];
const s = hsl[1] * (1 - hsl[2]) * 3;
return [hsl[0],s,hsl[2]];
}
// Set up the canvas.
const img = document.getElementById("blurImg");
const canvas = document.getElementById("mainCanvas");
const w = img.naturalWidth;
const h = img.naturalHeight;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
canvas.width = w;
canvas.height = h;
const context = canvas.getContext("2d");
context.clearRect( 0, 0, w, h );
context.drawImage( img, 0, 0 );
// Get the image data from the context.
var imageData = context.getImageData(0,0,w,h);
var pix = imageData.data;
var isDark = "${style}" == "dark";
var imageFunc = isDark ? darkBlur : lightBlur;
for (let i=0; i < pix.length; i+=4) {
// Convert to HSL.
let hsl = rgbToHsl(pix[i],pix[i+1],pix[i+2]);
// Apply the image function.
hsl = imageFunc(hsl);
// Convert back to RGB.
const rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
// Put the values back into the data.
pix[i] = rgb[0];
pix[i+1] = rgb[1];
pix[i+2] = rgb[2];
}
// Draw over the old image.
context.putImageData(imageData,0,0);
// Blur the image.
stackBlurCanvasRGB("mainCanvas", 0, 0, w, h, ${blur});
// Perform the additional processing for dark images.
if (isDark) {
// Draw the hard light box over it.
context.globalCompositeOperation = "hard-light";
context.fillStyle = "rgba(55,55,55,0.2)";
context.fillRect(0, 0, w, h);
// Draw the soft light box over it.
context.globalCompositeOperation = "soft-light";
context.fillStyle = "rgba(55,55,55,1)";
context.fillRect(0, 0, w, h);
// Draw the regular box over it.
context.globalCompositeOperation = "source-over";
context.fillStyle = "rgba(55,55,55,0.4)";
context.fillRect(0, 0, w, h);
// Otherwise process light images.
} else {
context.fillStyle = "rgba(255,255,255,0.4)";
context.fillRect(0, 0, w, h);
}
// Return a base64 representation.
canvas.toDataURL();
`
// Convert the images and create the HTML.
let blurImgData = Data.fromPNG(img).toBase64String()
let html = `
<img id="blurImg" src="data:image/png;base64,${blurImgData}" />
<canvas id="mainCanvas" />
`
// Make the web view and get its return value.
let view = new WebView()
await view.loadHTML(html)
let returnValue = await view.evaluateJavaScript(js)
// Remove the data type from the string and convert to data.
let imageDataString = returnValue.slice(22)
let imageData = Data.fromBase64String(imageDataString)
// Convert to image and crop before returning.
let imageFromData = Image.fromData(imageData)
// return cropImage(imageFromData)
return imageFromData
}
Create a popup
async function generateAlert(message, options) {
let alert = new Alert();
alert.message = message;
for (const option of options) {
alert.addAction(option);
}
let response = await alert.presentAlert();
return response;
}
pop up a notification
/**
* 弹出一个通知
* @param {string} title 通知标题
* @param {string} body 通知内容
* @param {string} url 点击后打开的URL
*/
async notify (title, body, url, opts = {}) {
let n = new Notification()
n = Object.assign(n, opts);
n.title = title
n.body = body
if (url) n.openURL = url
return await n.schedule()
}
Use the tutorial
- AppStore search and download Scriptable
- Open Scriptable, click ➕ in the upper right corner, and paste the installation widget code copied from the widget house applet
- Click the ▶️ Run button in the lower right corner to download and install the component code. If you need to configure small objects (such as: setting a background image, etc.), a pop-up window will pop up. Follow the prompts to do the next step. If there is no response, it means that no configuration is required. Next Click the Done button in the upper left corner
Go back to the iPhone desktop, long press, add a component, select the Scriptable application, check the widget code you just added, and complete the display effect 😃
Expressway
More fun little objects click below
More good articles click below
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。