用js玩轉Scriptable,超簡單教程

MangoGoing發表於2022-01-16

前言

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應用,勾選剛剛新增的小元件程式碼,完成顯示效果?

快速通道

更多好玩的小物件戳下面

更多好文戳下面

相關文章