剛好拍貼趣 v3.4 手繪整合版

iPad及大屏 多圖框拍貼分發與大數據收集系統

批量配置活動圖框

可多次上傳,新圖框會累加入清單,不影響已有圖框

點擊或拖曳多個圖框至此處

建議解析度:1024 x 768 (4:3比例)

沒有現成圖框?可用 AI 生成!先到「拍貼圖框設計師 GEM」依說明生成想要的圖框,再回到下方「去背工具」去背後匯入使用。

開啟 AI 圖框生成 GEM

當前載入圖框

0 張

雲端同步狀態

未設定 GAS

將下載的網頁傳至使用者 iPad 或佈署至 Github Pages 即可拍照

圖框管理與合成大預覽

滑入縮圖顯示刪除鈕

圖框清單:

尚未上傳任何圖框

請先於左側選取並上傳圖框圖片

上傳圖片開始去背

純瀏覽器 Canvas 技術,所有運算皆在本機執行,不上傳任何內容。

累計上傳總人次

0 人

今日收到照片

0 張

系統同步狀況

等待同步指令中...

已接收之使用者作品快照

上傳時間姓名作品快照預覽操作動作
點擊右上方「同步拍貼照資料夾」按鈕即可撈取雲端硬碟內的使用者作品

Google Apps Script (GAS) 連線密鑰設定

雲端後端 GAS 原始碼 (v4.0 - 原圖存雲端硬碟)

請在 Google 試算表點選「擴充功能」→「Apps Script」,貼上本代碼:(圖片將以原圖存入試算表同路徑的「拍貼照」資料夾,不再寫入試算表)

/**
 * 剛好拍貼趣 - 雲端後端控制 GAS 腳本 (v4.1)
 * 學生端以 no-cors 上傳「原圖 PNG」,後端存入試算表同路徑的「拍貼照」
 * 資料夾並設為「知道連結者可檢視」;分享連結改以 JSONP 回傳,
 * 以解決從本機 file:// 開啟時無法讀取回應的 CORS 限制。不再寫入試算表。
 */

// 取得(或建立)與本試算表同一層的「拍貼照」資料夾
function getPhotoFolder() {
  var ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
  var parents = DriveApp.getFileById(ssId).getParents();
  var parent = parents.hasNext() ? parents.next() : DriveApp.getRootFolder();
  var found = parent.getFoldersByName("拍貼照");
  return found.hasNext() ? found.next() : parent.createFolder("拍貼照");
}

// 學生端上傳圖片:存檔、設定分享、把分享連結暫存到 Cache 供 JSONP 取回
function doPost(e) {
  var lock = LockService.getScriptLock();
  var output = ContentService.createTextOutput().setMimeType(ContentService.MimeType.JSON);
  try {
    lock.waitLock(20000);
    var data = JSON.parse(e.postData.contents);
    var folder = getPhotoFolder();
    var b64 = (data.photoData || "").replace(/^data:image\/\w+;base64,/, "");
    var bytes = Utilities.base64Decode(b64);
    var name = (data.studentName || "匿名學生").replace(/[\/\\]/g, "_");
    var stamp = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyyMMdd_HHmmss");
    var file = folder.createFile(Utilities.newBlob(bytes, "image/png", name + "_" + stamp + ".png"));
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    if (data.uploadId) {
      CacheService.getScriptCache().put("url_" + data.uploadId, file.getUrl(), 600);
    }
    return output.setContent(JSON.stringify({ status: "success", fileUrl: file.getUrl() }));
  } catch (err) {
    return output.setContent(JSON.stringify({ status: "error", message: err.toString() }));
  } finally { lock.releaseLock(); }
}

// JSONP 端點:
//   ?action=geturl&uploadId=XXX&callback=cb → 回傳該次上傳的分享連結
//   ?callback=cb                            → 回傳「拍貼照」資料夾內所有圖片(教師端)
function doGet(e) {
  var p = (e && e.parameter) ? e.parameter : {};
  var payload;
  if (p.action === "geturl") {
    var url = CacheService.getScriptCache().get("url_" + p.uploadId);
    payload = url ? { status: "success", fileUrl: url } : { status: "pending" };
  } else {
    payload = listPhotos();
  }
  var json = JSON.stringify(payload);
  if (p.callback) {
    return ContentService.createTextOutput(p.callback + "(" + json + ")")
      .setMimeType(ContentService.MimeType.JAVASCRIPT);
  }
  return ContentService.createTextOutput(json).setMimeType(ContentService.MimeType.JSON);
}

// 列出「拍貼照」資料夾內所有圖片(供教師端預覽)
function listPhotos() {
  var folder = getPhotoFolder();
  var files = folder.getFiles();
  var records = [];
  while (files.hasNext()) {
    var f = files.next();
    var base = f.getName().replace(/\.png$/i, "");
    records.push({
      timestamp: Utilities.formatDate(f.getDateCreated(), Session.getScriptTimeZone(), "yyyy/MM/dd HH:mm:ss"),
      studentName: base.split("_")[0],
      photoData: "https://drive.google.com/thumbnail?id=" + f.getId() + "&sz=w1000",
      fileUrl: f.getUrl()
    });
  }
  records.sort(function(a, b) { return a.timestamp > b.timestamp ? -1 : 1; });
  return records;
}