全国服务热线:4008-888-888

技术知识

Html5 Canvas完成照片标识、放缩、挪动和储存历史

哈哈哈俺又来啦,这次带来的是canvas完成1些画布作用的文章内容,期待大伙儿喜爱!

序言

由于也是大3了,近期俺也在找实习,以前有1个自身的小新项目:

https://github.com/zhcxk1998/School-Partners

招聘面试官说能够往深层次次思索1下,也许加1些新的作用来提升新项目的难度,他提了几个提议,在其中1个便是 试卷线上审阅,老师能够在上应对工作开展注释,圈圈点点等 俺当天夜里就刚开始科学研究这个东东哈哈哈,终究被我科学研究出来啦!

选用的是 canvas 绘图画笔,由css3的 transform 特性来开展平移与放缩,以后再详尽详细介绍详细介绍

(期待大伙儿能够留下珍贵的赞与star嘻嘻)

实际效果预览

动图是放cdn的,假如浏览不上,能够登陆线上尝试尝试: test.algbb.cn/#/admin/con…

公式推导 假如不想看公式怎样推导,能够立即绕过看后边的实际完成~ 1. 座标变换公式 变换公式详细介绍

实际上1刚开始也是想在网络上找1下有木有有关的材料,可是可是找不到,因此就自身渐渐地的推出来了。我就举1下横座标的事例吧!

通用性公式

这个公式是表明,根据公式来将电脑鼠标按下的座标变换为画布中的相对性座标,这1点尤其关键

(transformOrigin - downX) / scale * (scale⑴) + downX - translateX = pointX

主要参数解释

transformOrigin: transform转变的基点(根据这个特性来操纵元素以哪里开展转变)
downX: 电脑鼠标按下的座标(留意,用的情况下必须减去器皿左偏位间距,由于大家要的是相对器皿的座标)
scale: 放缩倍数,默认设置为1
translateX: 平移的间距

推导全过程

这个公式的话,实际上就较为通用性,能够用在其他运用到 transform 特性的情景,至于如何推导的话,我是用的笨方法

实际的检测编码,放在文末,必须自取~

1. 先做出两个同样的元素,随后标识上座标,而且设定器皿特性 overflow:hidden 来掩藏外溢內容

ok,如今就有两个1样的引流矩阵啦,大家为他标识上1些红点,随后大家对左侧的开展css3的款式转变 transform

矩形框的宽高是 360px * 360px 的,大家界定1下他的转变特性,转变基点挑选正管理中心,变大3倍

// css
transform-origin: 180px 180px;
transform: scale(3, 3);

获得以下結果

ok,大家如今比照1下上面的結果,就会发现,变大3倍的情况下,正好是正中间黑色方块占有了所有宽度。接下来大家便可以对这些点与本来沒有开展转变(右侧)的矩形框开展比照便可以获得她们座标的关联啦

2. 刚开始对两个座标开展比照,随后推出公式

如今举1个简易的事例吧,比如大家算1下左上角的座标(如今早已标识为黄色了)

实际上大家实际上便可以立即心算出来座标的关联啦

这里左侧测算座标的值是大家电脑鼠标按下的座标

这里左侧测算座标的值是大家电脑鼠标按下的座标

这里左侧测算座标的值是大家电脑鼠标按下的座标

  • 由于宽高是 360px ,因此分为3等份,每份宽度是 120px
  • 由于转变以后器皿的宽高是不会改变的,转变的仅有矩形框自身
  • 大家能够得出左侧的黄色标识座标是 x:120 y:0 ,右侧的黄色标识为 x:160 y:120 (这个实际上肉眼看应当就可以看出来了,确实不好能够用纸笔算1算)

这个座标将会有点独特,大家再换几个来测算测算(依据独特推1般)

蓝色标识:左侧: x:120 y:120 ,右侧: x: 160 y:160 翠绿色标识:左侧: x: 240 y:240 ,右侧: x: 200: y:200

好了,大家类似早已能够拿到座标之间的关联了,大家能够列1个表

还感觉不安心?大家能够换1下,放缩倍数与器皿宽高开展测算

不知道道大伙儿有木有觉得呢,随后大家便可以渐渐地依据座标推出通用性的公式啦

(transformOrigin - downX) / scale * (scale⑴) + down - translateX = point

自然,大家也许也有这个 translateX 沒有尝试,这个就较为简易1点了,脑内仿真模拟1下,就了解大家能够减去位移的间距就ok啦。大家检测1下

大家先改动1下款式,新增1下位移的间距

transform-origin: 180px 180px;
transform: scale(3, 3) translate(⑷0px,⑷0px);

還是大家上面的情况,ok,大家如今蓝色跟翠绿色的标识還是11对应的,那大家看看如今的座标状况

  • 蓝色:左侧: x:0 y:0 ,右侧: x:160 y:160
  • 翠绿色:左侧: x:120 y:120 ,右侧: x:200 y:200

大家各自应用公式算1下出来的座标是如何的 (下列为历经座标换算)

蓝色:左侧: x:120 y:120 ,右侧: x:160 y:160 翠绿色:左侧: x:160 y:160 ,右侧: x:200 y:200

不难发现,大家实际上就相差了与位移间距 translateX/translateY 的差值,因此,大家只必须减去位移的间距便可以完善的开展座标变换啦

检测公式

依据上面的公式,大家能够简易检测1下!这个公式究竟能不可以起效!!!

大家立即延用上面的demo,检测1下假如元素开展了转变,大家电脑鼠标点下的地区转化成1个标识,部位是不是显示信息正确。看起来很ok啊(手动式搞笑)

const wrap = document.getElementById('wrap')
wrap.onmousedown = function (e) {
  const downX = e.pageX - wrap.offsetLeft
  const downY = e.pageY - wrap.offsetTop

  const scale = 3
  const translateX = ⑷0
  const translateY = ⑷0
  const transformOriginX = 180
  const transformOriginY = 180

  const dot = document.getElementById('dot')
  dot.style.left = (transformOriginX - downX) / scale * (scale - 1) + downX - translateX + 'px'
  dot.style.top = (transformOriginY - downY) / scale * (scale - 1) + downY - translateY + 'px'
}

将会有人会问,为何要减去这个 offsetLeftoffsetTop 呢,由于大家上面不断强调,大家测算的是电脑鼠标点一下的座标,而这个座标還是相对大家展现器皿的座标,因此大家要减去器皿自身的偏位量才行。

组件设计方案

既然demo啥的都早已检测了ok了,大家接下来就逐1剖析1下这个组件应当咋设计方案好呢(现阶段仍为低配版,以后再开展提升健全)

1. 基础的画布组成

大家先简易剖析1下这个组成吧,实际上关键便是1个画布的器皿,右侧1个专用工具栏,仅此罢了

大致就这模样啦!

<div className="mark-paper__wrap" ref={wrapRef}>
  <canvas
    ref={canvasRef}
    className="mark-paper__canvas">
    <p>很可是,这个东东与您的电脑上不配!</p>
  </canvas>
  <div className="mark-paper__sider" />
</div>

大家唯1必须的1点便是,器皿必须设定特性 overflow: hidden 用来掩藏內部canvas画布外溢的內容,也便是说,大家要操纵大家可视性的地区。另外大家必须动态性获得器皿宽高来为canvas设定规格

2. 原始化canvas画布与填充照片

大家能够弄个方式来原始化而且填充画布,下列截取关键一部分,实际上便是为canvas画布设定规格与填充大家的照片

const fillImage = async () => {
  // 此处省略...
  
  const img: HTMLImageElement = new Image()

  img.src = await getURLBase64(fillImageSrc)
  img.onload = () => {
    canvas.width = img.width
    canvas.height = img.height
    context.drawImage(img, 0, 0)

    // 设定转变基点,为画布器皿中间
    canvas.style.transformOrigin = `${wrap?.offsetWidth / 2}px ${wrap?.offsetHeight / 2}px`
    // 消除上1次转变的实际效果
    canvas.style.transform = ''
  }
}

3. 监视canvas画布的各种各样电脑鼠标恶性事件

这个操纵挪动的话,大家最先能够弄1个方式来监视画布电脑鼠标的各种各样恶性事件,能够区别不一样的方式来开展不一样的恶性事件解决

const handleCanvas = () => {
  const { current: canvas } = canvasRef
  const { current: wrap } = wrapRef
  const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
  if (!context || !wrap) return

  // 消除上1次设定的监视,防止获得主要参数不正确
  wrap.onmousedown = null
  wrap.onmousedown = function (event: MouseEvent) {
    const downX: number = event.pageX
    const downY: number = event.pageY

    // 区别大家如今挑选的电脑鼠标方式:挪动、画笔、橡皮擦
    switch (mouseMode) {
      case MOVE_MODE:
        handleMoveMode(downX, downY)
        break
      case LINE_MODE:
        handleLineMode(downX, downY)
        break
      case ERASER_MODE:
        handleEraserMode(downX, downY)
        break
      default:
        break
    }
  }

4. 完成画布挪动

这个就较为好办啦,大家只必须运用电脑鼠标按下的座标,和大家拖拽的间距便可以完成画布的挪动啦,由于涉及到到每次挪动都必须测算全新的位移间距,大家能够界定几个自变量来开展测算。

这里监视的是器皿的电脑鼠标恶性事件,而并不是canvas画布的恶性事件,由于这模样大家能够再挪动超出界限的情况下还可以开展挪动实际操作

简易的总结1下:

  • 传入电脑鼠标按下的座标
  • 测算当今位移间距,并升级css转变实际效果
  • 电脑鼠标抬起时升级全新的位移情况
// 界定1些自变量,来储存当今/全新的挪动情况
// 当今位移的间距
const translatePointXRef: MutableRefObject<number> = useRef(0)
const translatePointYRef: MutableRefObject<number> = useRef(0)
// 上1次位移完毕的位移间距
const fillStartPointXRef: MutableRefObject<number> = useRef(0)
const fillStartPointYRef: MutableRefObject<number> = useRef(0)

// 挪动情况下的监视涵数
const handleMoveMode = (downX: number, downY: number) => {
  const { current: canvas } = canvasRef
  const { current: wrap } = wrapRef
  const { current: fillStartPointX } = fillStartPointXRef
  const { current: fillStartPointY } = fillStartPointYRef
  if (!canvas || !wrap || mouseMode !== 0) return

  // 为器皿加上挪动恶性事件,能够在空白处挪动照片
  wrap.onmousemove = (event: MouseEvent) => {
    const moveX: number = event.pageX
    const moveY: number = event.pageY

    // 升级如今的位移间距,值为:上1次位移完毕的座标+挪动的间距
    translatePointXRef.current = fillStartPointX + (moveX - downX)
    translatePointYRef.current = fillStartPointY + (moveY - downY)

    // 升级画布的css转变
    canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointXRef.current}px,${translatePointYRef.current}px)`
  }
  
  wrap.onmouseup = (event: MouseEvent) => {
    const upX: number = event.pageX
    const upY: number = event.pageY
    
    // 撤销恶性事件监视
    wrap.onmousemove = null
    wrap.onmouseup = null;

    // 电脑鼠标抬起情况下,升级“上1次唯1完毕的座标”
    fillStartPointXRef.current = fillStartPointX + (upX - downX)
    fillStartPointYRef.current = fillStartPointY + (upY - downY)
  }
}

5. 完成画布放缩

画布放缩我关键根据右边的拖动条和电脑鼠标滚轮来完成,最先大家再监视画布电脑鼠标恶性事件的涵数中加1下监视滚轮的恶性事件

总结1下:

  • 监视电脑鼠标滚轮的转变
  • 升级放缩倍数,并更改款式
// 监视电脑鼠标滚轮,升级画布放缩倍数
const handleCanvas = () => {
  const { current: wrap } = wrapRef

  // 省略1万字...

  wrap.onwheel = null
  wrap.onwheel = (e: MouseWheelEvent) => {
    const { deltaY } = e
    // 这里要留意1下,我是0.1来递增下降,可是由于JS应用IEEE 754,来测算,因此精度有难题,大家自身解决1下
    const newScale: number = deltaY > 0
      ? (canvasScale * 10 - 0.1 * 10) / 10
      : (canvasScale * 10 + 0.1 * 10) / 10
    if (newScale < 0.1 || newScale > 2) return
    setCanvasScale(newScale)
  }
}

// 监视拖动条来操纵放缩
<Slider
  min={0.1}
  max={2.01}
  step={0.1}
  value={canvasScale}
  tipFormatter={(value) => `${(value).toFixed(2)}x`}
  onChange={handleScaleChange} />
  
const handleScaleChange = (value: number) => {
  setCanvasScale(value)
}

接着大家应用hooks的不良反应涵数,依靠于画布放缩倍数来开展款式的升级

//监视放缩画布
useEffect(() => {
  const { current: canvas } = canvasRef
  const { current: translatePointX } = translatePointXRef
  const { current: translatePointY } = translatePointYRef
  canvas && (canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointX}px,${translatePointY}px)`)
}, [canvasScale])

6. 完成画笔绘图

这个就必须用到大家以前推导出来来的公式啦!由于呢,细心想1下,假如大家放缩位移以后,大家电脑鼠标按下的部位,他的座标将会就相对画布来讲会有转变, 因此大家必须变换1下才可以开展电脑鼠标按下的部位与画布的部位11对应的实际效果

略微总结1下:

  • 传入电脑鼠标按下的座标
  • 根据公式变换,刚开始在对应座标下绘图
  • 电脑鼠标抬起时,撤销恶性事件监视
// 运用公式变换1下座标
const generateLinePoint = (x: number, y: number) => {
  const { current: wrap } = wrapRef
  const { current: translatePointX } = translatePointXRef
  const { current: translatePointY } = translatePointYRef
  const wrapWidth: number = wrap?.offsetWidth || 0
  const wrapHeight: number = wrap?.offsetHeight || 0
  // 放缩位移座标转变规律性
  // (transformOrigin - downX) / scale * (scale⑴) + downX - translateX = pointX
  const pointX: number = ((wrapWidth / 2) - x) / canvasScale * (canvasScale - 1) + x - translatePointX
  const pointY: number = ((wrapHeight / 2) - y) / canvasScale * (canvasScale - 1) + y - translatePointY

  return {
    pointX,
    pointY
  }
}

// 监视电脑鼠标画笔恶性事件
const handleLineMode = (downX: number, downY: number) => {
  const { current: canvas } = canvasRef
  const { current: wrap } = wrapRef
  const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
  if (!canvas || !wrap || !context) return

  const offsetLeft: number = canvas.offsetLeft
  const offsetTop: number = canvas.offsetTop
  // 减去画布偏位的间距(以画布为标准开展测算座标)
  downX = downX - offsetLeft
  downY = downY - offsetTop

  const { pointX, pointY } = generateLinePoint(downX, downY)
  context.globalCompositeOperation = "source-over"
  context.beginPath()
  // 设定画笔起始点
  context.moveTo(pointX, pointY)

  canvas.onmousemove = null
  canvas.onmousemove = (event: MouseEvent) => {
    const moveX: number = event.pageX - offsetLeft
    const moveY: number = event.pageY - offsetTop
    const { pointX, pointY } = generateLinePoint(moveX, moveY)
    // 刚开始绘图画笔线条~
    context.lineTo(pointX, pointY)
    context.stroke()
  }
  canvas.onmouseup = () => {
    context.closePath()
    canvas.onmousemove = null
    canvas.onmouseup = null
  }
}

7. 橡皮擦的完成

橡皮擦现阶段也有点难题,如今的话是根据将 canvas 画布的情况照片 + globalCompositeOperation 这个特性来仿真模拟橡皮擦的完成,但是,这时候候照片转化成出来以后,橡皮擦的痕迹会变为白色,而并不是全透明

此流程与画笔完成类似,仅有1点点小变化

设定特性 context.globalCompositeOperation = "destination-out"

// 现阶段橡皮擦也有点难题,前端开发显示信息一切正常,储存照片下来,擦除的痕迹会变为白色
const handleEraserMode = (downX: number, downY: number) => {
  const { current: canvas } = canvasRef
  const { current: wrap } = wrapRef
  const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
  if (!canvas || !wrap || !context) return

  const offsetLeft: number = canvas.offsetLeft
  const offsetTop: number = canvas.offsetTop
  downX = downX - offsetLeft
  downY = downY - offsetTop

  const { pointX, pointY } = generateLinePoint(downX, downY)

  context.beginPath()
  context.moveTo(pointX, pointY)

  canvas.onmousemove = null
  canvas.onmousemove = (event: MouseEvent) => {
    const moveX: number = event.pageX - offsetLeft
    const moveY: number = event.pageY - offsetTop
    const { pointX, pointY } = generateLinePoint(moveX, moveY)
    context.globalCompositeOperation = "destination-out"
    context.lineWidth = lineWidth
    context.lineTo(pointX, pointY)
    context.stroke()
  }
  canvas.onmouseup = () => {
    context.closePath()
    canvas.onmousemove = null
    canvas.onmouseup = null
  }
}

8. 撤消与修复的作用完成

这个的话,大家最先必须掌握普遍的撤消与修复的作用的逻辑性 分几种状况吧

  • 若当今情况处在第1个部位,则不容许撤消
  • 若当今情况处在最终1个部位,则不容许修复
  • 假如当今撤消了,但是升级了情况,则取当今情况为全新的情况(也便是说不容许修复了,这个刚升级的情况便是全新的)

画布情况的升级

因此大家必须设定1些自变量来存,情况目录,与当今画笔的情况下标

// 界定主要参数存东东
const canvasHistroyListRef: MutableRefObject<ImageData[]> = useRef([])
const [canvasCurrentHistory, setCanvasCurrentHistory] = useState<number>(0)

大家还必须在原始化canvas的情况下,大家就加上入当今的情况存入目录中,做为最开始刚开始的空画布情况

const fillImage = async () => {
  // 省略1万字...

  img.src = await getURLBase64(fillImageSrc)
  img.onload = () => {
    const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
    canvasHistroyListRef.current = []
    canvasHistroyListRef.current.push(imageData)
    setCanvasCurrentHistory(1)
  }
}

随后大家就完成1下,画笔升级情况下,大家也必须将当今的情况加上入 画笔情况目录 ,而且升级当今情况对应的下标,还必须解决1下1些细节

总结1下:

  • 电脑鼠标抬起时,获得当今canvas画布情况
  • 加上进情况目录中,而且升级情况下标
  • 假如当今处在撤消情况,若应用画笔升级情况,则将当今的最为全新的情况,本来部位以后的情况所有清空
const handleLineMode = (downX: number, downY: number) => {
  // 省略1万字...
  canvas.onmouseup = () => {
    const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)

    // 假如此时处在撤消情况,此时再应用画笔,则将以后的情况清空,以刚画的做为全新的画布情况
    if (canvasCurrentHistory < canvasHistroyListRef.current.length) {
      canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory)
    }
    canvasHistroyListRef.current.push(imageData)
    setCanvasCurrentHistory(canvasCurrentHistory + 1)
    context.closePath()
    canvas.onmousemove = null
    canvas.onmouseup = null
  }
}

画布情况的撤消与修复

ok,实际上如今有关画布情况的升级,大家早已进行了。接下来大家必须解决1下情况的撤消与修复的作用啦

大家先界定1下这个专用工具栏吧

随后大家设定对应的恶性事件,各自是撤消,修复,与清空,实际上都很非常容易看懂,数最多便是解决1下界限状况。

const handleRollBack = () => {
  const isFirstHistory: boolean = canvasCurrentHistory === 1
  if (isFirstHistory) return
  setCanvasCurrentHistory(canvasCurrentHistory - 1)
}

const handleRollForward = () => {
  const { current: canvasHistroyList } = canvasHistroyListRef
  const isLastHistory: boolean = canvasCurrentHistory === canvasHistroyList.length
  if (isLastHistory) return
  setCanvasCurrentHistory(canvasCurrentHistory + 1)
}

const handleClearCanvasClick = () => {
  const { current: canvas } = canvasRef
  const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
  if (!canvas || !context || canvasCurrentHistory === 0) return

  // 清空画布历史时间
  canvasHistroyListRef.current = [canvasHistroyListRef.current[0]]
  setCanvasCurrentHistory(1)

  message.success('画布消除取得成功!')
}

恶性事件设定好以后,大家便可以刚开始监视1下这个 canvasCurrentHistory 当今情况下标,应用不良反应涵数开展解决

useEffect(() => {
  const { current: canvas } = canvasRef
  const { current: canvasHistroyList } = canvasHistroyListRef
  const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
  if (!canvas || !context || canvasCurrentHistory === 0) return
  context?.putImageData(canvasHistroyList[canvasCurrentHistory - 1], 0, 0)
}, [canvasCurrentHistory])

为canvas画布填充图象信息内容!

这样就大获全胜啦!!!

9. 完成电脑鼠标标志的转变

大家简易的解决1下,画笔方式则是画笔的标志,橡皮擦方式下电脑鼠标是橡皮擦,挪动方式下便是一般的挪动标志

切换方式情况下,设定1下不一样的标志

const handleMouseModeChange = (event: RadioChangeEvent) => {
  const { target: { value } } = event
  const { current: canvas } = canvasRef
  const { current: wrap } = wrapRef

  setmouseMode(value)

  if (!canvas || !wrap) return
  switch (value) {
    case MOVE_MODE:
      canvas.style.cursor = 'move'
      wrap.style.cursor = 'move'
      break
    case LINE_MODE:
      canvas.style.cursor = `url('http://cdn.algbb.cn/pencil.ico') 6 26, pointer`
      wrap.style.cursor = 'default'
      break
    case ERASER_MODE:
      message.warning('橡皮擦作用并未健全,储存照片会出現不正确')
      canvas.style.cursor = `url('http://cdn.algbb.cn/eraser.ico') 6 26, pointer`
      wrap.style.cursor = 'default'
      break
    default:
      canvas.style.cursor = 'default'
      wrap.style.cursor = 'default'
      break
  }
}

10. 切换照片

如今的话只是1个demo情况,根据点一下挑选框,切换不一样的照片

// 重设转换主要参数,再次绘图照片
useEffect(() => {
  setIsLoading(true)
  translatePointXRef.current = 0
  translatePointYRef.current = 0
  fillStartPointXRef.current = 0
  fillStartPointYRef.current = 0
  setCanvasScale(1)
  fillImage()
}, [fillImageSrc])

const handlePaperChange = (value: string) => {
  const fillImageList = {
    'xueshengjia': 'http://cdn.algbb.cn/test/canvasTest.jpg',
    'xueshengyi': 'http://cdn.algbb.cn/test/canvasTest2.png',
    'xueshengbing': 'http://cdn.algbb.cn/emoji/30.png',
  }
  setFillImageSrc(fillImageList[value])
}

留意事项

留意器皿的偏位量

大家必须留意1下,由于公式中的 downX 是相对性器皿的座标,也便是说,大家必须减去器皿的偏位量,这类状况会出現在应用了 margin 等主要参数,或说上方或左边有其他元素的状况

大家輸出1下大家鲜红色的元素的 offsetLeft 等特性,会发现他是早已自身就有50的偏位量了,大家测算电脑鼠标点一下的座标的情况下就要减去这1一部分的偏位量

window.onload = function () {
  const test = document.getElementById('test')
  console.log(`offsetLeft: ${test.offsetLeft}, offsetHeight: ${test.offsetTop}`)
}

html,
body {
  margin: 0;
  padding: 0;
}

#test {
  width: 50px;
  height: 50px;
  margin-left: 50px;
  background: red;
}

<div class="container">
  <div id="test"></div>
</div>

留意父组件应用relative相对性合理布局的状况

倘若大家如今有1种这类的合理布局,复印鲜红色元素的偏位量,看起来都挺一切正常的

可是假如大家总体目标元素的父元素(也便是黄色一部分)设定 relative 相对性合理布局

.wrap {
  position: relative;
  width: 400px;
  height: 300px;
  background: yellow;
}

<div class="container">
  <div class="sider"></div>
  <div class="wrap">
    <div id="test"></div>
  </div>
</div>

这时候候大家复印出来的偏位量会是是多少呢

两次回答不1样啊,由于大家的偏位量是依据相对性部位来测算的,假如父器皿应用相对性合理布局,则会危害大家子元素的偏位量

组件编码(低配版)

import React, { FC, ComponentType, useEffect, useRef, RefObject, useState, MutableRefObject } from 'react'
import { CustomBreadcrumb } from '@/admin/components'
import { RouteComponentProps } from 'react-router-dom';
import { FormComponentProps } from 'antd/lib/form';
import {
  Slider, Radio, Button, Tooltip, Icon, Select, Spin, message, Popconfirm
} from 'antd';

import './index.scss'
import { RadioChangeEvent } from 'antd/lib/radio';
import { getURLBase64 } from '@/admin/utils/getURLBase64'

const { Option, OptGroup } = Select;

type MarkPaperProps = RouteComponentProps & FormComponentProps

const MarkPaper: FC<MarkPaperProps> = (props: MarkPaperProps) => {
  const MOVE_MODE: number = 0
  const LINE_MODE: number = 1
  const ERASER_MODE: number = 2
  const canvasRef: RefObject<HTMLCanvasElement> = useRef(null)
  const containerRef: RefObject<HTMLDivElement> = useRef(null)
  const wrapRef: RefObject<HTMLDivElement> = useRef(null)
  const translatePointXRef: MutableRefObject<number> = useRef(0)
  const translatePointYRef: MutableRefObject<number> = useRef(0)
  const fillStartPointXRef: MutableRefObject<number> = useRef(0)
  const fillStartPointYRef: MutableRefObject<number> = useRef(0)
  const canvasHistroyListRef: MutableRefObject<ImageData[]> = useRef([])
  const [lineColor, setLineColor] = useState<string>('#fa4b2a')
  const [fillImageSrc, setFillImageSrc] = useState<string>('')
  const [mouseMode, setmouseMode] = useState<number>(MOVE_MODE)
  const [lineWidth, setLineWidth] = useState<number>(5)
  const [canvasScale, setCanvasScale] = useState<number>(1)
  const [isLoading, setIsLoading] = useState<boolean>(false)
  const [canvasCurrentHistory, setCanvasCurrentHistory] = useState<number>(0)

  useEffect(() => {
    setFillImageSrc('http://cdn.algbb.cn/test/canvasTest.jpg')
  }, [])

  // 重设转换主要参数,再次绘图照片
  useEffect(() => {
    setIsLoading(true)
    translatePointXRef.current = 0
    translatePointYRef.current = 0
    fillStartPointXRef.current = 0
    fillStartPointYRef.current = 0
    setCanvasScale(1)
    fillImage()
  }, [fillImageSrc])

  // 画布主要参数变化时,再次监视canvas
  useEffect(() => {
    handleCanvas()
  }, [mouseMode, canvasScale, canvasCurrentHistory])

  // 监视画笔色调转变
  useEffect(() => {
    const { current: canvas } = canvasRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!context) return

    context.strokeStyle = lineColor
    context.lineWidth = lineWidth
    context.lineJoin = 'round'
    context.lineCap = 'round'
  }, [lineWidth, lineColor])

  //监视放缩画布
  useEffect(() => {
    const { current: canvas } = canvasRef
    const { current: translatePointX } = translatePointXRef
    const { current: translatePointY } = translatePointYRef
    canvas && (canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointX}px,${translatePointY}px)`)
  }, [canvasScale])

  useEffect(() => {
    const { current: canvas } = canvasRef
    const { current: canvasHistroyList } = canvasHistroyListRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!canvas || !context || canvasCurrentHistory === 0) return
    context?.putImageData(canvasHistroyList[canvasCurrentHistory - 1], 0, 0)
  }, [canvasCurrentHistory])

  const fillImage = async () => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    const img: HTMLImageElement = new Image()

    if (!canvas || !wrap || !context) return

    img.src = await getURLBase64(fillImageSrc)
    img.onload = () => {
      // 取正中间3D渲染照片
      // const centerX: number = canvas && canvas.width / 2 - img.width / 2 || 0
      // const centerY: number = canvas && canvas.height / 2 - img.height / 2 || 0
      canvas.width = img.width
      canvas.height = img.height

      // 情况设定为照片,橡皮擦的实际效果才可以出来
      canvas.style.background = `url(${img.src})`
      context.drawImage(img, 0, 0)
      context.strokeStyle = lineColor
      context.lineWidth = lineWidth
      context.lineJoin = 'round'
      context.lineCap = 'round'

      // 设定转变基点,为画布器皿中间
      canvas.style.transformOrigin = `${wrap?.offsetWidth / 2}px ${wrap?.offsetHeight / 2}px`
      // 消除上1次转变的实际效果
      canvas.style.transform = ''
      const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
      canvasHistroyListRef.current = []
      canvasHistroyListRef.current.push(imageData)
      // canvasCurrentHistoryRef.current = 1
      setCanvasCurrentHistory(1)
      setTimeout(() => { setIsLoading(false) }, 500)
    }
  }

  const generateLinePoint = (x: number, y: number) => {
    const { current: wrap } = wrapRef
    const { current: translatePointX } = translatePointXRef
    const { current: translatePointY } = translatePointYRef
    const wrapWidth: number = wrap?.offsetWidth || 0
    const wrapHeight: number = wrap?.offsetHeight || 0
    // 放缩位移座标转变规律性
    // (transformOrigin - downX) / scale * (scale⑴) + downX - translateX = pointX
    const pointX: number = ((wrapWidth / 2) - x) / canvasScale * (canvasScale - 1) + x - translatePointX
    const pointY: number = ((wrapHeight / 2) - y) / canvasScale * (canvasScale - 1) + y - translatePointY

    return {
      pointX,
      pointY
    }
  }

  const handleLineMode = (downX: number, downY: number) => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!canvas || !wrap || !context) return

    const offsetLeft: number = canvas.offsetLeft
    const offsetTop: number = canvas.offsetTop
    // 减去画布偏位的间距(以画布为标准开展测算座标)
    downX = downX - offsetLeft
    downY = downY - offsetTop

    const { pointX, pointY } = generateLinePoint(downX, downY)
    context.globalCompositeOperation = "source-over"
    context.beginPath()
    context.moveTo(pointX, pointY)

    canvas.onmousemove = null
    canvas.onmousemove = (event: MouseEvent) => {
      const moveX: number = event.pageX - offsetLeft
      const moveY: number = event.pageY - offsetTop
      const { pointX, pointY } = generateLinePoint(moveX, moveY)
      context.lineTo(pointX, pointY)
      context.stroke()
    }
    canvas.onmouseup = () => {
      const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)

      // 假如此时处在撤消情况,此时再应用画笔,则将以后的情况清空,以刚画的做为全新的画布情况
      if (canvasCurrentHistory < canvasHistroyListRef.current.length) {
        canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory)
      }
      canvasHistroyListRef.current.push(imageData)
      setCanvasCurrentHistory(canvasCurrentHistory + 1)
      context.closePath()
      canvas.onmousemove = null
      canvas.onmouseup = null
    }
  }

  const handleMoveMode = (downX: number, downY: number) => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const { current: fillStartPointX } = fillStartPointXRef
    const { current: fillStartPointY } = fillStartPointYRef
    if (!canvas || !wrap || mouseMode !== 0) return

    // 为器皿加上挪动恶性事件,能够在空白处挪动照片
    wrap.onmousemove = (event: MouseEvent) => {
      const moveX: number = event.pageX
      const moveY: number = event.pageY

      translatePointXRef.current = fillStartPointX + (moveX - downX)
      translatePointYRef.current = fillStartPointY + (moveY - downY)

      canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointXRef.current}px,${translatePointYRef.current}px)`
    }

    wrap.onmouseup = (event: MouseEvent) => {
      const upX: number = event.pageX
      const upY: number = event.pageY

      wrap.onmousemove = null
      wrap.onmouseup = null;

      fillStartPointXRef.current = fillStartPointX + (upX - downX)
      fillStartPointYRef.current = fillStartPointY + (upY - downY)
    }
  }

  // 现阶段橡皮擦也有点难题,前端开发显示信息一切正常,储存照片下来,擦除的痕迹会变为白色
  const handleEraserMode = (downX: number, downY: number) => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!canvas || !wrap || !context) return

    const offsetLeft: number = canvas.offsetLeft
    const offsetTop: number = canvas.offsetTop
    downX = downX - offsetLeft
    downY = downY - offsetTop

    const { pointX, pointY } = generateLinePoint(downX, downY)

    context.beginPath()
    context.moveTo(pointX, pointY)

    canvas.onmousemove = null
    canvas.onmousemove = (event: MouseEvent) => {
      const moveX: number = event.pageX - offsetLeft
      const moveY: number = event.pageY - offsetTop
      const { pointX, pointY } = generateLinePoint(moveX, moveY)
      context.globalCompositeOperation = "destination-out"
      context.lineWidth = lineWidth
      context.lineTo(pointX, pointY)
      context.stroke()
    }
    canvas.onmouseup = () => {
      const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
      if (canvasCurrentHistory < canvasHistroyListRef.current.length) {
        canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory)
      }
      canvasHistroyListRef.current.push(imageData)
      setCanvasCurrentHistory(canvasCurrentHistory + 1)
      context.closePath()
      canvas.onmousemove = null
      canvas.onmouseup = null
    }
  }

  const handleCanvas = () => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!context || !wrap) return

    // 消除上1次设定的监视,防止获得主要参数不正确
    wrap.onmousedown = null
    wrap.onmousedown = function (event: MouseEvent) {
      const downX: number = event.pageX
      const downY: number = event.pageY

      switch (mouseMode) {
        case MOVE_MODE:
          handleMoveMode(downX, downY)
          break
        case LINE_MODE:
          handleLineMode(downX, downY)
          break
        case ERASER_MODE:
          handleEraserMode(downX, downY)
          break
        default:
          break
      }
    }

    wrap.onwheel = null
    wrap.onwheel = (e: MouseWheelEvent) => {
      const { deltaY } = e
      const newScale: number = deltaY > 0
        ? (canvasScale * 10 - 0.1 * 10) / 10
        : (canvasScale * 10 + 0.1 * 10) / 10
      if (newScale < 0.1 || newScale > 2) return
      setCanvasScale(newScale)
    }
  }

  const handleScaleChange = (value: number) => {
    setCanvasScale(value)
  }

  const handleLineWidthChange = (value: number) => {
    setLineWidth(value)
  }

  const handleColorChange = (color: string) => {
    setLineColor(color)
  }

  const handleMouseModeChange = (event: RadioChangeEvent) => {
    const { target: { value } } = event
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef

    setmouseMode(value)

    if (!canvas || !wrap) return
    switch (value) {
      case MOVE_MODE:
        canvas.style.cursor = 'move'
        wrap.style.cursor = 'move'
        break
      case LINE_MODE:
        canvas.style.cursor = `url('http://cdn.algbb.cn/pencil.ico') 6 26, pointer`
        wrap.style.cursor = 'default'
        break
      case ERASER_MODE:
        message.warning('橡皮擦作用并未健全,储存照片会出現不正确')
        canvas.style.cursor = `url('http://cdn.algbb.cn/eraser.ico') 6 26, pointer`
        wrap.style.cursor = 'default'
        break
      default:
        canvas.style.cursor = 'default'
        wrap.style.cursor = 'default'
        break
    }
  }

  const handleSaveClick = () => {
    const { current: canvas } = canvasRef
    // 可存入数据信息库或是立即转化成照片
    console.log(canvas?.toDataURL())
  }

  const handlePaperChange = (value: string) => {
    const fillImageList = {
      'xueshengjia': 'http://cdn.algbb.cn/test/canvasTest.jpg',
      'xueshengyi': 'http://cdn.algbb.cn/test/canvasTest2.png',
      'xueshengbing': 'http://cdn.algbb.cn/emoji/30.png',
    }
    setFillImageSrc(fillImageList[value])
  }

  const handleRollBack = () => {
    const isFirstHistory: boolean = canvasCurrentHistory === 1
    if (isFirstHistory) return
    setCanvasCurrentHistory(canvasCurrentHistory - 1)
  }

  const handleRollForward = () => {
    const { current: canvasHistroyList } = canvasHistroyListRef
    const isLastHistory: boolean = canvasCurrentHistory === canvasHistroyList.length
    if (isLastHistory) return
    setCanvasCurrentHistory(canvasCurrentHistory + 1)
  }

  const handleClearCanvasClick = () => {
    const { current: canvas } = canvasRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!canvas || !context || canvasCurrentHistory === 0) return

    // 清空画布历史时间
    canvasHistroyListRef.current = [canvasHistroyListRef.current[0]]
    setCanvasCurrentHistory(1)

    message.success('画布消除取得成功!')
  }

  return (
    <div>
      <CustomBreadcrumb list={['內容管理方法', '审阅工作']} />
      <div className="mark-paper__container" ref={containerRef}>
        <div className="mark-paper__wrap" ref={wrapRef}>
          <div
            className="mark-paper__mask"
            style={{ display: isLoading ? 'flex' : 'none' }}
          >
            <Spin
              tip="照片载入中..."
              indicator={<Icon type="loading" style={{ fontSize: 36 }} spin
              />}
            />
          </div>
          <canvas
            ref={canvasRef}
            className="mark-paper__canvas">
            <p>很可是,这个东东与您的电脑上不配!</p>
          </canvas>
        </div>
        <div className="mark-paper__sider">
          <div>
            挑选工作:
            <Select
              defaultValue="xueshengjia"
              style={{
                width: '100%', margin: '10px 0 20px 0'
              }}
              onChange={handlePaperChange} >
              <OptGroup label="17手机软件1班">
                <Option value="xueshengjia">学员甲</Option>
                <Option value="xueshengyi">学员乙</Option>
              </OptGroup>
              <OptGroup label="17手机软件2班">
                <Option value="xueshengbing">学员丙</Option>
              </OptGroup>
            </Select>
          </div>
          <div>
            画布实际操作:<br />
            <div className="mark-paper__action">
              <Tooltip title="撤消">
                <i
                  className={`icon iconfont icon-chexiao ${canvasCurrentHistory <= 1 && 'disable'}`}
                  onClick={handleRollBack} />
              </Tooltip>
              <Tooltip title="修复">
                <i
                  className={`icon iconfont icon-fanhui ${canvasCurrentHistory >= canvasHistroyListRef.current.length && 'disable'}`}
                  onClick={handleRollForward} />
              </Tooltip>
              <Popconfirm
                title="明确清空画布吗?"
                onConfirm={handleClearCanvasClick}
                okText="明确"
                cancelText="撤销"
              >
                <Tooltip title="清空">
                  <i className="icon iconfont icon-qingchu" />
                </Tooltip>
              </Popconfirm>
            </div>
          </div>
          <div>
            画布放缩:
            <Tooltip placement="top" title='能用电脑鼠标滚轮开展放缩'>
              <Icon type="question-circle" />
            </Tooltip>
            <Slider
              min={0.1}
              max={2.01}
              step={0.1}
              value={canvasScale}
              tipFormatter={(value) => `${(value).toFixed(2)}x`}
              onChange={handleScaleChange} />
          </div>
          <div>
            画笔尺寸:
            <Slider
              min={1}
              max={9}
              value={lineWidth}
              tipFormatter={(value) => `${value}px`}
              onChange={handleLineWidthChange} />
          </div>
          <div>
            方式挑选:
            <Radio.Group
              className="radio-group"
              onChange={handleMouseModeChange}
              value={mouseMode}>
              <Radio value={0}>挪动</Radio>
              <Radio value={1}>画笔</Radio>
              <Radio value={2}>橡皮擦</Radio>
            </Radio.Group>
          </div>
          <div>
            色调挑选:
            <div className="color-picker__container">
              {['#fa4b2a', '#ffff00', '#ee00ee', '#1890ff', '#333333', '#ffffff'].map(color => {
                return (
                  <Tooltip placement="top" title={color} key={color}>
                    <div
                      role="button"
                      className={`color-picker__wrap ${color === lineColor && 'color-picker__wrap--active'}`}
                      style={{ background: color }}
                      onClick={() => handleColorChange(color)}
                    />
                  </Tooltip>
                )
              })}
            </div>
          </div>
          <Button onClick={handleSaveClick}>储存照片</Button>
        </div>
      </div>
    </div >
  )
}

export default MarkPaper as ComponentType

总结

到此这篇有关Html5 Canvas完成照片标识、放缩、挪动和储存历史时间情况 (附变换公式)的文章内容就详细介绍到这了,更多有关Canvas 照片标识 放缩 挪动內容请检索脚本制作之家之前的文章内容或再次访问下面的有关文章内容,期待大伙儿之后多多适用脚本制作之家!



在线客服

关闭

客户服务热线
4008-888-888


点击这里给我发消息 在线客服

点击这里给我发消息 在线客服