使用react-konva制作在线photoshop(2)——字体的文本与样式的修改

3,041 阅读3分钟

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

相关阅读

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

字体

hoc 抽取共用部分

在上一篇我们学习了字体和图片元素,会发现其中有一些功能是相同的,比如:Transformer、拖拽、选择等。为了后期方便维护,使用 hoc 来抽离这些功能。

const withTransform = (Component: FC) => {
    const Inner = (props: {
        isSelected: boolean
        handleInfo: (a: any) => void
        handleSelected: () => void
        rotation?: number
        opacity?: number
    }) => {
        const {
            isSelected = false,
            handleInfo = () => {},
            rotation = 1,
            opacity = 0.8,
            handleSelected,
        } = props

        const [info, setInfo] = useState({
            x: 100,
            y: 100,
            isDragging: false,
        })
        const eleRef = useRef()
        const trRef = useRef()

        useEffect(() => {
            if (isSelected && trRef) {
                // @ts-ignore
                trRef.current.nodes([eleRef.current])
                // @ts-ignore
                trRef.current.getLayer().batchDraw()
            }
        }, [isSelected])

        const handleDragStart = () => {
            handleSelected()
            setInfo({
                ...info,
                isDragging: true,
            })
        }

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

        return (
            <>
                {/* @ts-ignore */}
                <Component
                    // @ts-ignore
                    onDragStart={handleDragStart}
                    onDragEnd={handleDragEnd}
                    shadowColor="black"
                    shadowBlur={10}
                    shadowOpacity={0.6}
                    shadowOffsetX={info.isDragging ? 10 : 5}
                    shadowOffsetY={info.isDragging ? 10 : 5}
                    scaleX={info.isDragging ? 1.2 : 1}
                    scaleY={info.isDragging ? 1.2 : 1}
                    opacity={opacity}
                    draggable
                    // rotation={info.rotation}
                    ref={eleRef}
                    myRef={eleRef}
                    onClick={handleSelected}
                    {...props}
                />
                {isSelected && (
                    <Transformer
                        // @ts-ignore
                        ref={trRef}
                        boundBoxFunc={(oldBox, newBox) => {
                            if (newBox.width < 5 || newBox.height < 5) {
                                return oldBox
                            }
                            const { width, height } = newBox
                            // @ts-ignore
                            // setInfo(newBox);
                            //  handleChange(newBox);
                            return newBox
                        }}
                    />
                )}
            </>
        )
    }
    return Inner
}

在 Image 组件中,只需绑定好 props 就 ok 了

const KonvaImage = ({ url = '', handleSelected = () => {}, ...props }) => {
    const [image, status] = useImage(url, 'anonymous')
    return myRef ? (
        // @ts-ignore
        <Image
            image={image}
            draggable
            ref={props.myRef}
            onClick={handleSelected}
            {...props}
        />
    ) : null
}

在画布上,只需这样使用

import MyImage from './KonvaImg'
const KonvaImage = withTransform(MyImage)

// ...
<KonvaImage
    url={i.value}
    isSelected={i.id === selectedId}
    handleInfo={handleInfo}
    handleSelected={setSelected.bind(null, i.id)}
/>

当然我们封装好的 hoc,在下一节处理字体组件的时候,就无需关系拖拽、选择和变形的相关逻辑了。

用上面的 hoc,包裹下 Text 组件

const KonvaText = withTransform(MyText)
// ...
<KonvaText
    // @ts-ignore
    stageRef={stageRef}
    isSelected={i.id === selectedId}
    handleSelected={setSelected.bind(null, i.id)}
/>

效果如下

demo

双击修改文本

现在组件已经可以拖拽和变形了,文字组件还要让用户输入,修改其上的文本。由于在 canvas 上不好实现这种效果,所以在全局生成一个 textarea 框,把画布上的文本中的坐标位置和宽高写到 textarea 上。这部分不太好做,从官网上正好找到示例代码,我们改造下:

const KonvaText: FC<ItextInfo> = ({
    stageRef,
    myRef,
    setShowTransformer,
    handleSelected,
    ...props
}) => {
    const [showText, setShowText] = useState(true)
    const onDblClick = (e: any) => {
        const textNode = e.target

        const textPosition = textNode.getAbsolutePosition()
        const stageBox = stageRef.current.container().getBoundingClientRect()

        const areaPosition = {
            x: stageBox.left + textPosition.x,
            y: stageBox.top + textPosition.y,
        }

        setShowText(false)
        const textarea = document.createElement('textarea')
        if (setShowTransformer) {
            setShowTransformer(false)
        }
        document.body.appendChild(textarea)
        textarea.value = textNode.text()
        textarea.style.position = 'absolute'
        textarea.style.top = areaPosition.y + 'px'
        textarea.style.left = areaPosition.x + 'px'
        textarea.style.width = textNode.width() - textNode.padding() * 2 + 'px'
        textarea.style.height =
            textNode.height() - textNode.padding() * 2 + 5 + 'px'
        textarea.style.fontSize = textNode.fontSize() + 'px'
        textarea.style.border = 'none'
        textarea.style.padding = '0px'
        textarea.style.margin = '0px'
        textarea.style.overflow = 'hidden'
        textarea.style.background = 'none'
        textarea.style.outline = 'none'
        textarea.style.resize = 'none'
        textarea.style.lineHeight = textNode.lineHeight()
        textarea.style.fontFamily = textNode.fontFamily()
        textarea.style.transformOrigin = 'left top'
        textarea.style.textAlign = textNode.align()
        textarea.style.color = textNode.fill()

        let rotation = textNode.rotation()
        let transform = ''
        if (rotation) {
            transform += 'rotateZ(' + rotation + 'deg)'
        }

        let px = 0
        const isFirefox =
            navigator.userAgent.toLowerCase().indexOf('firefox') > -1
        if (isFirefox) {
            px += 2 + Math.round(textNode.fontSize() / 20)
        }
        transform += 'translateY(-' + px + 'px)'
        textarea.style.transform = transform
        textarea.style.height = 'auto'
        textarea.style.height = textarea.scrollHeight + 3 + 'px'

        textarea.focus()

        function removeTextarea() {
            try {
                if (textarea) {
                    console.log(textarea)
                    document.body.removeChild(textarea)
                    setShowText(true)
                }
            } catch (err) {
                console.log(err)
            }
        }

        function setTextareaWidth(newWidth: number) {
            if (!newWidth) {
                // set width for placeholder
                newWidth = textNode.placeholder.length * textNode.fontSize()
            }
            // some extra fixes on different browsers
            const isSafari = /^((?!chrome|android).)*safari/i.test(
                navigator.userAgent
            )
            const isFirefox =
                navigator.userAgent.toLowerCase().indexOf('firefox') > -1
            if (isSafari || isFirefox) {
                newWidth = Math.ceil(newWidth)
            }
            const isEdge =
                document.documentMode || /Edge/.test(navigator.userAgent)
            if (isEdge) {
                newWidth += 1
            }
            textarea.style.width = newWidth + 'px'
        }

        textarea.addEventListener('keydown', function (e: any) {
            if (setShowTransformer) {
                setShowTransformer(false)
            }
            if (e.keyCode === 27) {
                removeTextarea()
            }

            const scale = textNode.getAbsoluteScale().x
            setTextareaWidth(textNode.width() * scale)
            textarea.style.height = 'auto'
            textarea.style.height =
                textarea.scrollHeight + textNode.fontSize() + 'px'
        })

        textarea.addEventListener('blur', function () {
            if (e.target !== textarea) {
                textNode.text(textarea.value) //  注意这里
                removeTextarea()
            }
        })
    }

    return (
        <>
            {showText && (
                <Text
                    text={props.value}
                    ref={myRef}
                    onDblClick={onDblClick}
                    onClick={() => {
                        setShowTransformer(true)
                        handleSelected()
                    }}
                    fontSize={40}
                    {...props}
                />
            )}
        </>
    )
}

demo

最终效果如上,发现已经可以成功编辑了,但是当光标移出 textarea 的时候,字体又还原回之前的位置了,明明照着官网代码改的,为什么不一样呢?这是因为没有把已经改变的数据传入 Text 组件中,画布中的数据没有和子组件进行关联

<Text
    text={props.value} // 注意这里
    {...props} // 注意这里
    // ....
/>

现在的文本和一些位置信息还是之前的 props,所以接下来我们在画布组件中绑定下数据

<Stage width={width} height={height} ref={stageRef}>
    {infos.map((i: Iinfo, idx: number) => (
        <Layer key={i.id}>
            <KonvaText
                {...i}
                stageRef={stageRef}
                isSelected={i.id === selectedId}
                handleInfo={handleInfo.bind(null, idx)}
                handleSelected={setSelected.bind(null, i.id)}
            />
        </Layer>
    ))}
</Stage>

这里的 info 存储的就是每个子组件被编辑的信息,如元素的坐标、宽高、透明度、字体的文本等等。。

因为这里面包含不少冗余信息,可以在 hoc 中过滤掉一些不需要的 props 或者在最后保存的时候做一次性的过滤。

字体的样式修改

比如字体的对齐、加粗、斜体等等

<Text
    // ...
    fontStyle="italic bold"
    align="center"
/>

最终效果为 demo