React + Canvas 像素風格取色器

前言

有時候咱們須要經過圖片去得到具體像素的顏色。而強大的 Canvas 爲咱們提供了現成的接口。 這個功能其實並不難,只不過咱們須要正確的理解 Canvas 並學會利用它的 API 。 若是你急於看到效果,能夠直接訪問css

演示地react

源碼地址git

我不會詳細得寫下每個步驟,可是你能夠一邊參照源碼,一邊配合這篇教程進行閱讀。github

繪製圖片(-)

首先,咱們須要基於圖片去繪製 Canvas。 操做步驟web

  1. 建立一個與圖片等寬高的canvas
  2. 得到canvas的 context
  3. 將圖片繪製到 canvas

咱們在React中用最小化模型展現出來 咱們在 React 的 DidMount 裏拿到 image 實例。固然,你也能夠直接建立一個 image 對象。canvas

import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'

export class TestPicker extends PureComponent {
  static propTypes = {
    src: PropTypes.string.isRequired,
    width: PropTypes.number.isRequired,
    height: PropTypes.number.isRequired,
  }
  static defaultProps = {
    width: 1300,
    height: 769,
    src: '/sec3.png'
  }
  // 在初始化階段註冊 ref 回調函數去得到 DOM 的實例
  constructor (props) {
    super(props)
    this.imageCanvasRef = ref => this.imageCanvas = ref
    this.image = new Image()
    this.image.src = props.src

  }
  // 請注意,必定要在圖片加載徹底以後纔開始繪製 Canvas
  componentDidMount () {
    this.image.onload = () => this.renderImageCanvas()
  }

  renderImageCanvas = () => {
    const { width, height } = this.props
    this.imageCtx = this.imageCanvas.getContext('2d')
    this.imageCtx.drawImage(this.image, 0, 0, width, height)
  }

  render () {
    const { width, height, src } = this.props
    return <div>
      <canvas
        width={width}
        height={height}
        style={{ width, height }}
        ref={this.imageCanvasRef}>
      </canvas>
    </div>
  }
}
複製代碼

只要將它掛載到相應的節點下,你能夠看到有一個和圖片同樣大小的 Canvas 而且繪製了圖片。bash

可是咱們須要注意圖片應該是同源的,若是不是同源,Canvas 繪製圖片時會報錯。具體如何設置能夠參考 使用圖像 Using images函數

Canvas 畫布與實際寬高

本質上canvas的寬高設定包含兩個層面,一個是畫布的大小,另一個則是Canvas 在文檔對象所佔據的寬高。因爲Canvas內部的繪製區域畫布大小默認是(width: 300px, height: 150px) ,好比當你 經過 css 設定 (width: 3000px;height: 1500px)的時候,內部的繪製區域大小會被強制與總體寬高保持統一,即內部的繪製區域會被放大十倍。像素級別的放大會致使實際的渲染效果變得更加模糊。由於要注意有時候你的繪製區域出現縮放現象。ui

實現放大鏡位移(二)

咱們須要讓放大鏡的位置在鼠標正中心,而且跟隨鼠標移動。 實現方式也比較簡單,經過 onmousemove 時得到當前 clientX 和 clientY, 而且減去當前 Canvas 視窗所佔據的 left 和 top 便可。this

首先,咱們在構造函數加了初始化的 state用來表示當前鼠標位移。 在鼠標移動時觸發 onmousemove 時去修改 state,經過改變 state 觸發 re-render,修改 left 和 top。

constructor() {
	this.glassCanvasRef = ref => this.glassCanvas = ref
	this.state = {
    left: 0,
    top: 0
  }
}
handleMouseMove = (e) => {
  // 計算當前鼠標相對 canvas 中的位置
  this.calculateCenterPoint({ clientX: e.clientX, clientY: e.clientY })
  const { centerX, centerY } = this.centerPoint
  this.setState({ left: centerX, top: centerY })
}

calculateCenterPoint = ({ clientX, clientY }) => {
  const { left, top } = this.imageCanvas.getBoundingClientRect()
  this.centerPoint = {
    centerX: Math.floor(clientX - left),
    centerY: Math.floor(clientY - top)
  }
}
render () {
	const { width, height, src } = this.props
	const { left, top } = this.state
	return <div style={{ position: 'relative' }}>
	  <canvas
	    width={width}
	    height={height}
	    style={{ width, height }}
	    onMouseMove={this.handleMouseMove}
	    ref={this.imageCanvasRef}>
	  </canvas>
	  <canvas 
	  ref={this.glassCanvasRef}
	  className="glass" 
	  style={{ left: left - glassWidth/2, top: top - glassHeight/2, width: glassWidth, height: glassHeight }}>
	  </canvas>
	</div>
}
const glassWidth = 100
const glassHeight = 100
複製代碼

繪製放大區域內容(三)

好了,其實咱們完成快一半了。接下來就是把放大區域部分的圖像放置到咱們的放大鏡中。 在繪製以前,咱們先清除一次畫布

handleMouseMove = (e) => {
    this.glassCtx.clearRect(0, 0, glassWidth, glassWidth)
}
複製代碼

咱們但願將放大鏡部分的元素放大, 我默認取了10倍放大效果。這種狀況呈現的樣式比較友好,若是你還須要對元素再放大,你只須要修改 scale 便可。

const INIT_NUMBER = 10
const finallyScale = INIT_NUMBER * (scale < 1 ? 1 : scale)
複製代碼

接下來咱們使用 canvas 提供的 drawImage 的複雜版本進行截取部分圖像。 CanvasRenderingContext2D.drawImage() 根據 MDN 中的演示圖片,咱們知道

  1. sx 和 sy 是原圖到咱們須要繪製的放大鏡放大的左側以及頂部距離
  2. sWidth 和 sHeight 是咱們要選擇放大的部分
  3. dx 和 dy 是當前繪製內容在放大鏡中的偏移量
  4. dWidth 和 dHeight 即爲放大鏡大小
drawImageSmoothingEnable(this.glassCtx, false)
this.glassCtx.drawImage(this.image,
  Math.floor(centerX - (glassWidth / 2) / finallyScale), Math.floor(centerY - (glassHeight / 2) / finallyScale),
  Math.floor(glassWidth / finallyScale), Math.floor(glassHeight / finallyScale),
  -INIT_NUMBER, -INIT_NUMBER,
  glassWidth, glassHeight
)

const drawImageSmoothingEnable = (context, enable) => {
  context.mozImageSmoothingEnabled = enable
  context.webkitImageSmoothingEnabled = enable
  context.msImageSmoothingEnabled = enable
  context.imageSmoothingEnabled = enable
}
複製代碼

咱們須要計算放大後的因素。此外,因爲在計算鼠標當前位置時,可能會有1像素誤差,但被放大了10倍。因此我增長了10個像素的偏移量。你能夠根據實際狀況來決定偏移。

經過drawImageSmoothingEnable函數讓咱們最終繪製的圖像產生鋸齒效果。這樣就會有真實的像素風格了。

繪製網格線(四)

關於繪製網格線,依然能夠參考 MDN 上的文檔。

const GRID_COLOR = 'lightgray'
drawGrid(this.glassCtx, GRID_COLOR, INIT_NUMBER, INIT_NUMBER)

const drawGrid = (context, color, stepx, stepy) => {
  context.strokeStyle = color
  context.lineWidth = 0.5

  for (let i = stepx + 0.5; i < context.canvas.width; i += stepx) {
    context.beginPath()
    context.moveTo(i, 0)
    context.lineTo(i, context.canvas.height)
    context.stroke()
  }

  for (let i = stepy + 0.5; i < context.canvas.height; i += stepy) {
    context.beginPath()
    context.moveTo(0, i)
    context.lineTo(context.canvas.width, i)
    context.stroke()
  }
}
複製代碼

實現取色(五)

咱們經過 getImageData 得到具體的像素點的數據,不過還須要轉換一下才能變成可用的數據。

getColor = () => {
  const { centerX, centerY } = this.centerPoint
  const { data } = this.imageCtx.getImageData(centerX, centerY, 1, 1)
  const color = transform2rgba(data)
}

const transform2rgba = (arr) => {
  arr[3] = parseFloat(arr[3] / 255)
  return `rgba(${arr.join(', ')})`
}

複製代碼

結語

本來我實現了一個在 Canvas 裏又繪製一個放大鏡去放大圖像。但這樣的問題就是放大鏡只能在 Canvas 內部活動,添加樣式之類的須要經過 Canvas 繪製,失去了 CSS 的能力。 如今這種分離的方式能夠支持自定義 CSS 樣式,並且減小了 Canvas 中繼續繪製 Canvas 放大倍數的複雜度。

固然,這只是一個 啓發性的demo,依然有許多粗糙的地方。但願能對你有用~

相關文章
相關標籤/搜索