5
头图

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

  1. AppStore search and download Scriptable

  1. Open Scriptable, click ➕ in the upper right corner, and paste the installation widget code copied from the widget house applet

  1. 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

  1. 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


MangoGoing
774 声望1.2k 粉丝

开源项目:详见个人详情