跳到主要內容

照片轉Pixel Art動畫影片 PixAnim

 


[試試]

某天突然想到,我也可以利用之前作過的繪文字填圖EmojiPaint的演算法,作一個簡單的像素風動畫,然後可以存成GIF或者影片方便分享。

EmojiPaint裡面己經有一個叫作pixelart的函式,可以把一個己經畫好圖的canvas的內容,轉成像素風格。每一個像素根據指定tile大小(cw*ch),它的顏色就是這塊tile裡所有pixel的顏色平均值。這就是pixelart的原理,最簡單的作法。

現在,要作成動畫,只要從最大的tile開始,也就是整張圖的大小(縮放調整至400*400),不斷的呼叫pixelart。作完後再把tile縮小一半,也就是200*200,再作一次pixelart。一直持續這個過程,直到tile為1*1之前停止,因為1*1太多點要畫了。

經過修改的pixelart,會先把此次所有tile都計算好顏色和位置,儲存在一個陣列。接著才是畫圖階段,一個一個把tile畫出來。但是tile愈小的時候,數量就愈多,畫一個tile的時間也要跟著縮短,才不會讓人覺得過程太過冗長。其實,除了隨著tile縮小,畫tile的時間跟著縮短之外。每一個draw處理中畫的tile數量也會跟著增加,這都是為了減少動畫過程太冗長,提高觀看的人的體驗。

底下是關鍵的pixelart和drawblock函式。

function pixelart(id, cw, ch) {
  var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  var col = Math.floor(canvas.width / cw);
  var row = Math.floor(canvas.height / ch);
  var blocks = [];
  for (var i = 0; i < col; i++) {
    for (var j = 0; j < row; j++) {
      var x = cw * i;
      var y = ch * j;
      var rgb = calcRgbSum(imgData.data, x, y, cw, ch);
      blocks.push([x, y, rgb]);
    }
  }
  var canvas2 = document.getElementById(id);
  var ctx2 = canvas2.getContext('2d');
  displayBlocks(ctx2, blocks, cw, ch);
}

function displayBlock(ctx, blocks, index, cw, ch) {
  if (blocks.length <= index) {
    return;
  }
  const b = blocks[index];
  ctx.fillStyle = 'rgb(' + b[2][0] + ',' + b[2][1] + ',' + b[2][2] + ')';
  ctx.fillRect(b[0], b[1], cw, ch);
  progress += 1;
  ctx.fillStyle = 'blue';
  const px = Math.floor(canvas.width * (progress / total_progress));
  ctx.fillRect(0, canvas.height - 2, px, 2);
}

再來是如何把動畫過程製作影片?

非常簡單!我們可以利用瀏覽器內建的MediaRecorder撘配canvas.captureStream就能作到把canvas的變化內容作為串流輸入一個錄製器,輸出成WebM格式的影片。

底下為主要的相關程式碼。

function startRecAnim() {
  const canvas2 = document.getElementById('canvas2');
  const stream = canvas2.captureStream(25); // 25 FPS
  recorder = new MediaRecorder(stream);
  let chunks = [];
  recorder.ondataavailable = e => chunks.push(e.data);
  recorder.onstop = e => {
    const blob = new Blob(chunks, {type: "video/webm"});
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = getWebmFileName();
    a.click();
    recorder = null;
  }
  recorder.start();
}

另外,錄製結束時,只要呼叫recorder.stop()即可停止,並觸發onstop事件。在此事件中,將錄製完成的影片存檔。

最後再利用AI作些畫面美化,就完成了。前後大概只花不到一天。


留言

這個網誌中的熱門文章

單人撲克牌遊戲 - 蒙地卡羅

更多可在網頁玩的 單人撲克牌遊戲 ; 新增一個簡單的單人撲克牌遊戲: 蒙地卡羅 ,簡單介紹一下玩法。 下載 事先排列好5x5張牌。 每次移動一張可以配對的牌,並消除這對牌。在上下、左右及斜向相隣的二張牌,只要擁有同樣數字(不計花色),即可配對。 消除二張配對的牌後,剩餘的牌以往左往上的方式補滿空隙,接著在發新牌補滿後面的空格。 重覆步驟2~3,直到沒有牌可以配對及發完所有牌為止。 結果有二種。一個是勝利,成功的消除掉所有牌。另一個是Gameover沒有牌可以再作配對。

以lex/yacc實作算式計算機

前面我們透過 手工的方式 實作了一個簡易的算式計算機,現在我們要開始使用工具來作同樣的事,比較看看手工和使用工具有什麼不同的差別。首先要介紹的就是lex&yacc。 lex & yacc lex(Lexical Analyzar)及yacc(Yet Another Compiler Compiler)是用來輔助程式設計師製作語法剖析器的程式工具。lex的工作就是幫助我們將輸入的資料文字串流分解成一個個有意義的token,而yacc的工作就是幫我們分析這些token和我們定義的規則作匹配。下圖中所表示的是使用lex及yacc的一般工作流程。 首先看到yacc會讀入一個.y檔案,這裡.y檔案的內容就是我們使用類似(E)BNF語法定義的語法規則,yacc會分析這些語法規則後,幫我們產生可以用來解析這些規則的程式碼,而這個檔案一般名稱預設為y.tab.c,產生的程式碼裡面最重要的一個的函式叫作yyparse。 同yacc類似,lex也會讀入一個.l的檔案,這個檔案裡面定義的是如何從文字流裡解出token的規則,使用的方法是常規表示式(regular expression)。在圖的左側中間我們還可以看到有一個叫作y.tab.h的檔案從yacc產生出來並餵給lex作輸入,這個檔案是yacc根據在讀入的.y檔裡面所定義的token代號所產生出來的一個header,這樣yacc及lex產生出來的程式碼裡面就可以使用共通定義的代碼而不必各寫個的。lex分析過.l檔案後也會產生一個一般預設叫作lex.yy.c的原始碼檔案,裡頭最重要的一個函式叫作yylex。 最後,我們把yacc產生出來的y.tab.c還有lex產生出來的lex.yy.c,以及其它我們自己撰寫的原始碼檔案一起拿來編譯再作連結,最後產生出來的就是一個可以用來解析我們定義的語法的解析器工具。以上是整個lex及yacc的使用流程概觀。 常規表示式 在正式使用lex之前,我們首先來對常規表示法作一個基本的認識。常規表示法是一種用來表示字串樣式(pattern)的中繼語言,就好比前文所介紹的(E)BNF表示式一樣,都是用來描述其它語言的語言,只不過用途不太一樣罷了。 常規表示式使用一些中繼符號(meta-symbol)以及ASCII字元定義字串樣式,以下列出一些常規表示式所使用的符號。 . 表示除了換行字元...

窮人的 AI:自動漫畫分鏡切割

  ( 試試看 ) 在手機上看漫畫時,有一個體驗上的問題: 漫畫原本是「整頁設計」 手機最適合的是「一格一格往下滑」 與其強迫使用者縮放、拖曳、放大,更直覺的做法是: 直接把一頁漫畫自動切成多個分鏡,轉成瀑布流閱讀。 這篇文章分享一個不靠深度學習、完全在前端完成的實作方式: 使用 OpenCV.js 做分鏡偵測 輸出 rect list 再用 全畫面 Canvas 把每個分鏡當成一個「閱讀單位」 整個系統可以拆成三層: 漫畫圖片 ↓ 影像處理(找出 rects) ↓ 排序後的 rect list ↓ 全畫面 Canvas 逐格呈現(瀑布流) Step 1:灰階化 漫畫的資訊 90% 都在線條上,顏色反而是干擾。 cv.cvtColor(src, grayImage, cv.COLOR_RGBA2GRAY); 灰階化的好處: 降低維度 對邊緣偵測更穩定 對黑白漫畫特別有效 Step 2:邊緣偵測,抓出「分鏡的邊」 接下來用最經典、也最夠用的 Canny Edge Detection: cv.Canny(grayImage, edges, 50, 150); 在漫畫中,分鏡外框通常就是最明顯的邊界。 Step 3:形態學操作,把破碎邊框「補起來」 真實漫畫的線條並不完美,常常有斷線、陰影、留白。 所以要做一個很重要的步驟:膨脹(Dilation) const kernel = cv.Mat.ones( 5 , 5 , cv.CV_8U); cv.dilate(edges, dilatedEdges, kernel); 直覺理解就是: 把細線「抹粗一點」, 讓本來斷掉的邊界連成封閉區域。 這一步直接決定後面能不能成功抓到「一整格分鏡」。 Step 4:找輪廓,轉成矩形框(rect) 有了封閉區域之後,就可以找輪廓: cv.findContours( dilatedEdges, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE ); 每一個 contour,代表一個「可能的分鏡區塊」。 接著轉成矩形: const rect = cv.boundingRect(contour); rects.push([rect.x, rect.y, rect.widt...