iT邦幫忙

0

JS30 Day 19 - Webcam Fun學習筆記

今天這個章節主要是利用電腦的視訊鏡頭,以canvas為基底,去改變我們的畫面色彩、透明度等等。

而我就依照比較重要的重點來分享。

canplay:當影片播放時,觸發這個事件。


// 監聽是否video正在播放('canplay')
video.addEventListener('canplay', paintToCanvas);

navigator.mediaDevices.getUserMedia(constraints):

navigator.mediaDevices可用來串聯與麥克風、攝影機或共享螢幕的連結。
getUserMedia()可以用來顯示是否允許開啟連結至其設備。
getUserMedia(constraints)的constraints參數有2個,分別為video和audio,此處我們只需要影片不需要音檔
getUserMedia()會返回一個Promise物件,我們需要利用.then來取得其返回的MediaStream物件

取得MediaStream物件後,我們去處理video的來源,由於作者提示說Chorme、firefox已經不支援video.src = window.URL.createObjectURL(localMediaStream)這個方法,而是改採video.srcObject = localMediaStream;,但由於還要顧及其他瀏覽器,如IE因此我們利用try、catch來寫兼容寫法。


// 獲取video並開始播放
function getVideo() {
  // 要video但不要audio
  // MediaDevices.getUserMedia()
  // 並且會提示說是否要開啟視訊鏡頭
  navigator.mediaDevices.getUserMedia({
      video: true,
      audio: false
    })
    .then(localMediaStream => {

      console.log(localMediaStream);
      // MediaStream
      // active: true
      // id: "sbREnyqNcRJEaadztxfa2cbVz38AnbBmIyiJ"
      // onactive: null
      // onaddtrack: null
      // oninactive: null
      // onremovetrack: null

      try {
        video.srcObject = localMediaStream; 
      } catch (err) {
        console.error(`OH NO!!!`, err);
        video.src = window.URL.createObjectURL(localMediaStream);
      }
      // 原始畫面
      video.play();
    })
}

HTMLCanvasElement.getContext(): 返回canvas 的上下文,如果上下文沒有定義則返回 null。

利用上下文創建一個二維的畫布。

const canvas = document.querySelector('.photo');
// ctx為我們所要畫的對象
const ctx = canvas.getContext('2d');

CanvasRenderingContext2D.drawImage():將圖像,畫布或視頻繪製到畫布上。

void ctx.drawImage(image, dx, dy, dWidth, dHeight);

image:放canvas的圖像來源。
dx, dy:起始位置(x,y)。
dWidth, dHeight:設定寬高。

CanvasRenderingContext2D.getImageData():取出相片每個像素rgba,且會返回一個ImageData物件

ImageData ctx.getImageData(sx, sy, sw, sh);

sx:複製左上角的x座標。
sy:複製左上角的x座標。
sw:複製的寬度。
sh:複製的高度。

ctx.putImageData(imageData, dx, dy): 把某塊區域的像素值呈現在指定的位址上

getImageData 跟putImageData是一組的,它可以讓你取得每個像素的rgba值然後作變化放回去

此處我們設置一個名為pixels的變數,來觀察普通面積與pixels的差別,一個點會被拆分成rgba,也就是pixels為area的四倍,可觀察出面積與點的關係會差四倍,會看到我們儲存像素點的地方是一個叫做Uint8ClampedArray的array,其內共有1228800個數值所組成,4個為一組,表示一個rgba的像素點要呈現的顏色

let pixels = ctx.getImageData(0, 0, width, height);

  console.log(pixels); ImageData {data: Uint8ClampedArray(1228800), width: 640, height: 480}

  console.log(`Area: ${width * height},Pixels ${pixels.data.length}`); Area: 307200,Pixels 1228800

  console.log(pixels.data[0], pixels.data[1], pixels.data[2], pixels.data[3]);
  // 106 114 55 255
  console.log(pixels.data[0 + 4], pixels.data[1 + 4], pixels.data[2 + 4], pixels.data[3 + 4]);
  // 109 116 57 255

因為影片會一直更新,所以設置計時器讓他一直跑,我們所做的視訊效果也是在這邊呼叫。


  return setInterval(() => {
    // 畫一個video,從起始位置(0,0),畫width,height的寬高
    ctx.drawImage(video, 0, 0, width, height);
    // take the pixels out
    let pixels = ctx.getImageData(0, 0, width, height);
    // mess with them
    // pixels = redEffect(pixels);
    // console.log(pixels.data[0], pixels.data[1], pixels.data[2], pixels.data[3]);

    // pixels = rgbSplit(pixels);
    // ctx.globalAlpha = 0.8;

    pixels = greenScreen(pixels);
    // put them back
    ctx.putImageData(pixels, 0, 0);
  }, 16);

insertBefore(newnode,existingnode): 在特定的 DOM 元素前面插入新的子節點。

strip.insertBefore(link, strip.firstChild),在父元素下,將每次新產的link插入至第一個子元素的最前面

download: < a download="filename" >,為a標籤的一個屬性,當點擊其元素就會跳出另存新檔

canvas.toDataURL(type, encoderOptions): 這個語法可以把圖片轉成 base64,回傳含有圖像和參數設置特定格式的 data URIs (預設 PNG).,回傳的圖像解析度為 96 dpi。

type:圖像格式,預設為 image/png.,表示 image/jpeg或是image/webp的圖像品質,不加就為預設。

此處為拍照功能的部分。

function takePhoto() {
  // played the sound
  snap.currentTime = 0;
  snap.play();

  // 做截圖的功能
  // canvas.toDataURL(type, encoderOptions);
  // type:圖像格式  預設為 image/png. 表示 image/jpeg 或是 image/webp 的圖像品質 不加就為預設。 
  // 回傳含有圖像和參數設置特定格式的 data URIs (預設 PNG). 回傳的圖像解析度為 96 dpi
  const data = canvas.toDataURL('image/jpeg');
  const link = document.createElement('a');
  link.href = data;
  // 創建一個download屬性,其預設值為handsome,當點擊其元素就會跳出另存新檔
  link.setAttribute('download', 'handsome');
  // 將圖加至link中
  link.innerHTML = `<img src="${data}" alt="Handsome Man" />`;
  // Node.insertBefore() 方法將一個節點插入至參考節點之前
  // parentNode.insertBefore(newNode, referenceNode);

  // 將每次新產的link插入至第一個子元素的最前面
  strip.insertBefore(link, strip.firstChild);
}

以下幾種功能,主要都是以四個色板rgba對應0,1,2,3下去做調整,i+=4的原因是因為rgba四個為一組,所以每次跳四個才會到下一個rgba。

紅色效果

將r部分的顏色提高,其餘減少。


function redEffect(pixels) {
  // console.log(pixels.data.length); // 1228800
  for (let i = 0; i < pixels.data.length; i += 4) {
    // 在什麼位置對其做調色
    pixels.data[i + 0] = pixels.data[i + 0] + 200; // RED
    pixels.data[i + 1] = pixels.data[i + 1] - 50; // GREEN
    pixels.data[i + 2] = pixels.data[i + 2] * 0.5; // Blue
  }
  return pixels;
}

色彩分離

將rgb不同的顏色移動到不同的位置即可。


function rgbSplit(pixels) {
  for (let i = 0; i < pixels.data.length; i += 4) {
    // 固定的顏色對不同位置去做變化(漸層)
    pixels.data[i - canvas.width * 4 * 50] = pixels.data[i + 0]; // RED
    pixels.data[i - canvas.width * 4 * 30] = pixels.data[i + 1]; // GREEN
    pixels.data[i - canvas.width * 4 * 10] = pixels.data[i + 2]; // Blue
  }
  return pixels;
}

過濾去背

只要顏色落在指定區間,就讓他變透明(a變為0)。


function greenScreen(pixels) {
  const levels = {};

  document.querySelectorAll('.rgb input').forEach((input) => {
    levels[input.name] = input.value;
  });
  // 獲取對應色板
  for (i = 0; i < pixels.data.length; i = i + 4) {
    red = pixels.data[i + 0];
    green = pixels.data[i + 1];
    blue = pixels.data[i + 2];
    alpha = pixels.data[i + 3];
    // 如果紅綠藍介於最大與最小之間,
    if (red >= levels.rmin &&
      green >= levels.gmin &&
      blue >= levels.bmin &&
      red <= levels.rmax &&
      green <= levels.gmax &&
      blue <= levels.bmax) {
      // 將alpha設為0
      pixels.data[i + 3] = 0;
    }
  }

  return pixels;
}

retro風格

加重黃紅比例即可。


function oldStyle(pixels) {
    for (let i = 0; i < pixels.data.length; i += 4) {
        pixels.data[i + 0] += 150; //red
        pixels.data[i + 1] += 60; //green
        pixels.data[i + 2] += 60; //blue
        pixels.data[i + 3] *= 0.8; //alpha
    }
    return pixels;
}

負片效果

將顏色變成互補色,利用255去減掉原本取得的顏色即可。

function negativeEffect(pixels) {
  for (let i = 0; i < pixels.data.length; i += 4) {
    pixels.data[i + 0] = 255 - pixels.data[i + 0]; //red
    pixels.data[i + 1] = 255 - pixels.data[i + 1]; //green
    pixels.data[i + 2] = 255 - pixels.data[i + 2]; //blue
  }
  return pixels;
}


尚未有邦友留言

立即登入留言