5
头图

前言

ios用户当更新到iOS14后,我们的iPhone等ios设备支持我们用户自定义桌面小物件(又或者称之为小组件、桌面挂件),利用这个特性,网上出现了许许多多诸如透明时钟、微博热搜、知乎热榜、网易云热评、特斯拉、BMW、名爵、奥迪等等的iPhone桌面,看如下实际效果图:

那这到底是怎么实现的,我们怎么才能制作一款自己的iPhone个性桌面?今天给大家分享的就是Scriptable的桌面玩法,对于javascript开发人员来说,看完这篇教程,上手小物件开发应用是信手拈来的事儿,而对于没有编程基础的同学不用担心看不懂,你所要做的就是复制粘贴,直接跳过开发教程,看文章末尾快速通道即可。

Scriptable介绍

这是一款可让您使用 JavaScript 自动化构建 iOS 的应用程序

以上是对Scriptable的官方解释,这对前端开发者来说无疑是一个福音,因为Scriptable 使用 Apple 的JavaScriptCore,它默认就支持ECMAScript 6对小组件进行开发构建。

如果您刚刚开始使用 JavaScript,您可能想看看 Codecademys Intro to Programming in JavaScript。有关 JavaScript 功能的快速参考,您可以参考 W3Schools 的JavaScript 教程

请注意,一些指南和教程会假设您在浏览器中运行 JavaScript,因此可以访问特定于浏览器的对象,例如文档。Scriptable 不在浏览器中运行 JavaScript,因此不存在此类对象。

更多对于Scriptable的解释请阅读官方文档

关键特性

先看一张图:

上面列举的是一些Scriptable的特性,这些特性包括:

  • 支持ES6语法
  • 可以使用JavaScript调用一些原生的API
  • Siri 快捷方式
  • 完善的文档支持
  • 共享表格扩展
  • 文件系统继承
  • 编辑器的自定义
  • 代码样例
  • 以及通过x-callback-url和其它APP交互

是不是感觉支持的特性还是挺多的,这些特性已经足够让我们去实现很多原生级底层的交互了。

第一个小物件程序

// 判断是否是运行在桌面的组件中
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);
}

通过以上简单的显示"Hello, World!"并设置背景色和文字样式的程序来看,有一个重要的概念需要javascript程序员去理解和从传统的web开发的概念中转换过来,如果你之前有开发过Flutter开发经验的话,那么对你来说,开发Scriptable应用应该是有共鸣的。因为对于我看来,Scriptable同样也是万物皆组件(widget)的概念,支撑这一点的一个重要思想就是面向对象。

万物皆组件

何为万物皆组件?无论是容器(div)还是样式(color、style)还是元素(font)等等全是Object,比如你要显示一行文字"Hello, World!",那么你首先必须要有一个容器(div)去装载这行文字(fonts),你还要去给文字设置样式(styles),那样式也不是说凭空生成,凡是对象,都要new出来。对照以上"Hello, World!"的例子再深入理解这个概念。

以上概念对Scriptable应用开发有极其重要的积极作用,尤其是对于初级前端开发者或没有原生app开发经验的开发者来说,他们很难脱离传统web这种mvvc或者mvc的开发模式去思考面向对象的开发模式。

高频常用的组件

ListWidge

显示元素列表的小部件,最常用的容器组件。一般组件应用的根元素都用ListWidget包裹,也只有用这个组件才能传递给 Script.setWidget() 将其显示在您的主屏幕上。

请注意,小部件会定期刷新,并且小部件刷新的速率很大程度上取决于操作系统。注意:利用这一点可以做很多需要基于定时刷新的应用,比如:节日纪念日,需要计算当前时间的应用。

另请注意,在小部件中运行脚本时存在内存限制。当使用太多内存时,小部件将崩溃并且无法正确呈现。

-addStack

addStack(): WidgetStack

添加堆栈。

ListWidget.addStack()返回值是WidgetStack(堆栈元素),将堆栈元素添加到ListWidget中是水平布局的,可以利用这个api实现类似于flex布局

-addSpacer

addSpacer(length: number): WidgetSpacer

向小部件添加间隔。这可用于在小部件中垂直偏移内容。类似于web开发中css的margin

-setPadding

setPadding(top: number, leading: number, bottom: number, trailing: number)

设置小部件每一侧的填充。类似web中css的padding

-addText

addText(text: string): WidgetText

将文本元素添加到小部件。使用返回元素的属性来设置文本样式。类比web开发中的向div中插入文本节点。

backgroundColor

backgroundColor: Color

设置容器的背景颜色,值必须是Color类型(new Color('#fff', 1)),Color构造函数的第一个参数为色值,第二个参数为透明度,类似web开发中的rgba(255,255,255,1)

backgroundImage

backgroundImage: Image

设置容器的背景图片。类似web中css的backgroud-image

Font

表示字体和文本大小。

new Font(name: string, size: number)

该字体可用于设置文本样式,例如在小部件中。

- regularSystemFont

创建常规系统字体。

static regularSystemFont(size: number): Font

-lightSystemFont

创建白天模式系统字体。

static lightSystemFont(size: number): Font

-thinSystemFont

创建细系统字体。

static thinSystemFont(size: number): Font

Keychain

钥匙串是凭据、密钥等的安全存储。使用该set()方法将值添加到钥匙串。然后,您可以稍后使用该get()方法检索该值。

-contains

检查钥匙串是否包含钥匙。

static contains(key: string): bool

检查钥匙串是否包含指定的钥匙。

-set

将指定键的值添加到钥匙串。

static set(key: string, value: string)

将值添加到钥匙串,将其分配给指定的键。如果密钥已存在于钥匙串中,则该值将被覆盖。

值安全地存储在加密数据库中。

-get

从钥匙串中读取一个值。

static get(key: string): string

读取指定键的值。如果密钥不存在,该方法将引发错误。使用该contains方法检查钥匙串中是否存在钥匙。

Alert

显示模态弹窗。类似web ui中的Modal组件

使用它来配置以模态或表单形式呈现的弹窗。配置弹窗后,调用 presentAlert() 或 presentSheet() 以呈现弹窗。这两种表示方法将返回一个值,该值携带完成时选择的操作的索引。比如你弹窗添加了两个操作按钮,先添加一个是确定,另一个是取消按钮,添加操作跟js中的数组一致,先添加的按钮索引就是 0,当用户点击确认按钮的时候,alert.presentAlert()返回的值就是'确认'在配置数组中的索引值,即为0。

个人认为这个组件也是非常高频的组件,因为在高级桌面组件或者复杂的组件,尤其是一些需要用户登录账号信息的桌面组件来说,需要弹窗让用户输入账号密码等交互行为,又或者让用户输入日期、名称等需要持久化存储的场景,Alert组件是不二之选。

-message

title: string

弹窗中显示的标题。通常是一个短字符串。

-addAction

向弹窗中添加操作按钮。要检查是否选择了某个操作,您应该使用在 presentAlert() 和 presentSheet() 返回的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)

向弹窗中添加取消操作。选择取消操作时,kidealert()或vistentheet()提供的索引将始终为-1。请注意,在 iPad 上运行并使用 presentSheet() 进行演示时,该操作不会显示在操作列表中。通过在工作表外点击可取消操作。

弹窗只能包含一个取消操作。尝试添加更多取消操作将删除之前添加的任何取消操作。

-presentAlert

显示模态弹出窗,类似elementuimodalvisible设置为true,此时弹窗显示。

-presentSheet

将弹窗以类似bottomSheet交互方式弹出。

Image

管理图像数据。

图像对象包含图像数据。Scriptable 中处理图像的 API(通过将图像作为输入或返回图像)将使用此 Image 类型。

-size

size: Size

图像的大小(以像素为单位)。只读

-fromFile

从指定的文件路径加载图像。如果无法读取图像,该函数将返回 null。类似web开发中读取本地(ios中还有iCloud)图片文件

-fromData

static fromData(data: Data): Image

从原始数据加载图像。如果无法读取图像,该函数将返回 null。

Data可以是字符串、文件和图像的原始数据表示。例如,Image中用的比较多的就是从base64字符串中读取图片,伪代码示例如下:

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

更多关于Data的其他api请参考文档

Photos

提供对您的照片库的访问。

为了从您的照片库中读取,您必须授予应用程序访问您的照片库的权限。首次使用 API 时,应用会提示访问,但如果您拒绝请求,所有 API 调用都会失败。在这种情况下,您必须从系统设置中启用对照片库的访问。

这个api用的也是相对高频的一个,因为大部分场景下,你的widget都需要用到图片或者背景,而使用图片的大部分场景(特别是背景图)都需要访问你的设备图库,也就是你的相册,当然使用相册功能必须在用户授权的前提下。

-fromLibrary

static fromLibrary(): Promise<Image>

显示用于选择图像的照片库,使用它从照片库中挑选图像。

使用它:

const img = await Photos.fromLibrary();
// 拿到Image对象后,可以对它做缓存、展示、传输等等用途
-latestPhoto

获取最新照片。

static latestPhoto(): Promise<Image>

从您的照片库中读取最新照片。如果没有可用的照片,则承诺将被拒绝。

-latestScreenshot

获取最新截图。

static latestScreenshot(): Promise<Image>

从您的照片库中读取最新的屏幕截图。如果没有可用的屏幕截图,则 Promise 将被拒绝。

Pasteboard

复制并粘贴字符串或图像。

从粘贴板复制和粘贴字符串和图像。

-copy

将字符串复制到粘贴板。

static copy(string: string)

-paste

从粘贴板粘贴字符串。

static paste(): string

-copyImage

将图像复制到粘贴板。

static copyImage(image: Image)

LinearGradient

线性渐变。

要在小部件中使用的线性渐变。

-colors

渐变的颜色。

locations颜色数组应包含与渐变属性相同数量的元素。

colors: [Color]

类似css中linear-gradient属性的第二、三个从参数,表示渐变的颜色范围

.horizontal-gradient {
  background: linear-gradient(to right, blue, pink);
}
-locations

每种颜色的位置。

每个位置应该是 0 到 1 范围内的值,并指示渐变colors数组中每种颜色的位置。

colors位置数组应包含与渐变属性相同数量的元素。

locations: [number]

const bg = new LinearGradient()
bg.locations = [0, 1]
bg.colors = [
  new Color('#f35942', 1),
  new Color('#e92d1d', 1)
]
w.backgroundGradient = bg

FileManager

此api适用于做缓存数据用,比较常用的api之一,使用频次较高

-local

创建一个本地 FileManager。

static local(): FileManager

创建一个文件管理器,用于操作本地存储的文件。

const files = FileManager.local();
-iCloud

创建一个 iCloud 文件管理器。

static iCloud(): FileManager

创建一个文件管理器,用于操作存储在 iCloud 中的文件。必须在设备上启用 iCloud 才能使用它。

-read

将文件的内容作为数据读取。

read(filePath: string): Data

读取文件路径指定的文件内容作为原始数据。要将文件作为字符串readString(filePath)读取,请参见并将其作为图像读取,请参见readImage(filePath).

如果文件不存在或存在于 iCloud 但尚未下载,该函数将出错。用于fileExists(filePath)检查文件是否存在并downloadFileFromiCloud(filePath)下载文件。请注意,调用 始终是安全的downloadFileFromiCloud(filePath),即使文件本地存储在设备上。

-readImage

将文件的内容作为图像读取。

readImage(filePath: string): Image

读取文件路径指定的文件内容并将其转换为图像。

// 读取自己在本地缓存的图片
const img = files.readImage(files.joinPath(files.documentsDirectory(), "avatar.jpg"))
-write

将数据写入文件。

write(filePath: string, content: Data)

-writeImage

将图像写入文件。

writeImage(filePath: string, image: Image)

将图像写入磁盘上的指定文件路径。如果该文件尚不存在,则会创建该文件。如果文件已经存在,则文件的内容将被新内容覆盖。

-fileExists

检查文件是否存在。

fileExists(filePath: string): bool

检查文件是否存在于指定的文件路径中。在移动或复制到目标之前检查这一点可能是一个好主意,因为这些操作将替换目标文件路径中的任何现有文件。

-documentsDirectory

文档目录的路径。

documentsDirectory(): string

用于检索文档目录的路径。您的脚本存储在此目录中。如果您启用了 iCloud,您的脚本将存储在 iCloud 的文档目录中,否则它们将存储在本地文档目录中。该目录可用于长期存储。可以使用“文件”应用程序访问存储在此目录中的文档。存储在本地文档目录中的文件不会出现在“文件”应用程序中。

-joinPath

连接两个路径组件。功能同node中的joinPath

joinPath(lhsPath: string, rhsPath: string): string

连接两条路径以创建一条路径。例如,用文件名连接到目录的路径。这是创建传递给 FileManager 的读取和写入函数的新文件路径的建议方法。

封装常用方法

网络请求

/**
   * 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
}

获取远程图片

/**
   * 获取远程图片内容
   * @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('加载图片失败');
  }
}

带透明度的背景图

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()
}

获取时间差

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;
}

所有支持的手机小物件像素大小和位置

常用来设置伪透明背景

// 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
}

获取截图中的组件剪裁图

/**
   * 获取截图中的组件剪裁图
   * 可用作透明背景
   * 返回图片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
  }

创建弹窗

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;
}

弹出一个通知

/**
   * 弹出一个通知
   * @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()
}

使用教程

  1. AppStore搜索下载Scriptable

  1. 打开Scriptable,点击右上角➕,粘贴从小物件屋小程序里复制的安装小组件代码

  1. 点击右下角▶️运行按钮进行下载安装组件代码,若需要配置小物件(如: 设置背景图片等),会弹出弹窗,根据提示下一步操作即可,若无任何反应则表示无需配置,接下去点击左上角的Done按钮即可

  1. 回到iPhone桌面,长按,添加组件,选择Scriptable应用,勾选刚刚添加的小组件代码,完成显示效果😃

快速通道

更多好玩的小物件戳下面

更多好文戳下面


MangoGoing
774 声望1.2k 粉丝

开源项目:详见个人详情