使用konva制作在线photoshop(1)——元素拖拽、变形与导出

3,857 阅读3分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

前言

公司要做一个在线制图的东西,这个需求砸到我身上时真的想哭,还好老天帮我,发现了 konva,官方文档也非常良心,不仅描述清晰,也有在线的示例,直接上手开发还是非常方便的。

所以我们拆解需求,一点一点来吧!

基础应用

在线制图最基础的应用是拖拽元素,比如,在画布上拖拽一张图片或某种形状,对该图片进行缩放或旋转操作。

画布就是<Stage>,每个图层为<Layer>

拖拽元素

konva 中内置了很多形状的元素,比如圆形、矩形等,以下示例为“星型”,这里先用<Star>试一下:

import { Circle, Rect, Stage, Layer, Text, Star } from 'react-konva'
import Konva from 'konva'

const Shape = () => {
    const [star, setStar] = useState({
        x: 300,
        y: 300,
        rotation: 20,
        isDragging: false,
    })

    const handleDragStart = () => {
        setStar({
            ...star,
            isDragging: true,
        })
    }

    const handleDragEnd = (e: any) => {
        setStar({
            ...star,
            x: e.target.x(),
            y: e.target.y(),
            isDragging: false,
        })
    }

    return (
        <Stage width={1000} height={600}>
            <Layer>
                <Star
                    key="starid"
                    id="starid"
                    x={star.x}
                    y={star.y}
                    numPoints={5}
                    innerRadius={20}
                    outerRadius={40}
                    fill="#89b717"
                    opacity={0.8}
                    draggable
                    rotation={star.rotation}
                    shadowColor="black"
                    shadowBlur={10}
                    shadowOpacity={0.6}
                    shadowOffsetX={star.isDragging ? 10 : 5}
                    shadowOffsetY={star.isDragging ? 10 : 5}
                    scaleX={star.isDragging ? 1.2 : 1}
                    scaleY={star.isDragging ? 1.2 : 1}
                    onDragStart={handleDragStart}
                    onDragEnd={handleDragEnd}
                />
            </Layer>
        </Stage>
    )
}

其中,可以给 Star 配置一些基础的属性,如:x、y 指该元素在画布上的坐标位置,rotaition 指元素的旋转角度;fill 指元素的填充颜色,scaleX、scaleY 指元素在 x、y 轴上的放大比例等等。

在拖拽的时候,我们要给该元素添加一些拖拽事件,如上:添加 handleDragStart 更改isDragging属性,使其在拖动时产生形变;添加 onDragEnd 事件,更改isDragging和 x、y 属性,来改变拖动位置,关闭拖动形变特效等。

观察上面的代码发现某些属性和"react-dnd"类似,但在使用 drag 事件的时候,发现比 react-dnd 方便很多,可能因为底层是 canvas 的原因吧!

导入图片

有两种方式可以导入图片,一个是用 react-hooks,一个是调用 react 生命周期函数,这里为了图省事,用 hooks:

先安装 konva 的官方库use-image,之后我们来封装一下图片组件:

import { Image } from 'react-konva'
import useImage from 'use-image'

const KonvaImage = ({ url = '' }) => {
    const [image] = useImage(url)

    return <Image image={image} />
}

export default KonvaImage

变形

使元素变形,需要引用 konva 的Transformer组件,该组件可以使元素的缩放、旋转。如下代码,在选中某元素后,会展示 Transformer 组件,在该组件上存在boundBoxFunc属性,当用户触发元素的变形行为时,该函数会被调用,返回一个包含形变后元素的信息(下面代码中为 newBox)。

import React, { useState, useEffect, useRef } from 'react'
import { Image, Transformer } from 'react-konva'
import Konva from 'konva'
import useImage from 'use-image'

const KonvaImage = ({ url = '', isSelected = false }) => {
    const [image] = useImage(url)
    const imgRef = useRef()
    const trRef = useRef()

    useEffect(() => {
        if (isSelected) {
            trRef.current.nodes([imgRef.current])
            trRef.current.getLayer().batchDraw()
        }
    }, [isSelected])
    return (
        <>
            <Image image={image} draggable ref={imgRef} />
            {isSelected && (
                <Transformer
                    ref={trRef}
                    boundBoxFunc={(oldBox, newBox) => {
                        // limit resize
                        if (newBox.width < 5 || newBox.height < 5) {
                            return oldBox
                        }
                        const { width, height } = newBox
                        // console.log('width', width);
                        // console.log('height', height);
                        return newBox
                    }}
                />
            )}
        </>
    )
}

export default KonvaImage

示例

合成图片

在 Stage 上添加 ref,会把画布输出 base64,之后转为图片在浏览器中触发下载行为。

function downloadURI(uri: string, name: string) {
  var link = document.createElement('a');
  link.download = name;
  link.href = uri;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

  const exportToImage = () => {
    const uri = stageRef.current.toDataURL();
    downloadURI(uri, 'stage.png');
  };

....
      <button onClick={exportToImage}>export</button>
      <Stage width={1200} height={1000} ref={stageRef}>

这里,注意图片的跨域问题,如果图片地址跨域了,图片的 konva 组件不会显示,所以要给图片服务器设置下 cors 头,或是中间做一层转发。并且要在代码层添加 crossorigin 属性开启 cors。否则就会在 canvas 的 toBlob()、toDataURL()和 getImageData()上报错。

图片跨域设置

konva 封装好的use-image提供好了跨域属性,如下

import useImage from 'use-image'

const [image, status] = useImage(url, 'anonymous')

<Image image={image} />

如果仍显示跨域问题不能生成图片,需要在服务器端添加跨域头或者做一层转发了。

reference

  1. konva 官方文档
  2. img 元素中的 crossorigin 属性