使用react-konva制作在线制图应用(7)——用户改变字体宽高和颜色

688 阅读4分钟

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

相关阅读

  1. 使用konva制作在线photoshop(1)——元素拖拽、变形与导出
  2. 使用react-konva制作在线photoshop(2)——字体的文本与样式的修改
  3. 使用react-konva制作在线制图应用(3)——在线字体文件的动态渲染
  4. 使用react-konva制作在线制图应用(4)——撤销/重做(踩坑篇)
  5. 使用react-konva制作在线制图应用(5)——撤销/重做(填坑篇)
  6. # 使用react-konva制作在线制图应用(6)——图层、缩放画布、删除元素

用户交互

获取元素形变后的宽高

当我们像下面这样拖动元素大小的时候 无法获取当前被拖动元素的宽高, attrs 中的 textWidth 没有变化:

但是我们仔细观察可以发现有两个属性改变了scaleX``scaleY,原来 konva 没有帮我们计算元素的宽高,只是记录了形变的倍数。那么在交互面板上,用户自己输入宽高,我们怎么转换呢?

文本元素

width=fontSize*text.length*scaleX

height=fontSize*行数*scaleY

行数=text.filter(t=>t==='\n').length

然而上面原理是对的,如下,但如下图,我的画布只有 375px,他看起来只占画布宽度的一半却显示元素的宽度有 333px,这显然是不对的。

可能是因为字号虽然固定,但每个字的宽度是不一样的,接着看 konva 内部有两个属性textWidth, textHeight,我们直接用这两个属性乘上 scale 试试:

 onTransformEnd={(a) => {
            const { attrs, textWidth, textHeight } = a.target;
            const { scaleX, scaleY, rotation, skewX, skewY, x, y, type } =
              attrs;
            const otherProperty: any = {};
            if (type === 'text') {
              const w = textWidth * scaleX;
              const h = textHeight * scaleY;
              otherProperty.w = w;
              otherProperty.h = h;
            }

            handleInfo({
              scaleX,
              scaleY,
              rotation,
              skewX,
              skewY,
              x,
              y,
              ...otherProperty,
            });
          }}

为了测试结果准确性,我把当前元素拉至画布大小,结果近似于画布宽度,说明这个公式成立!

所以反推,当用户输入宽度/高度,我们就可以直接算出 scaleX、scaleY,在把这两个值绑定到 step 中。

改变文本颜色

使用react-color作为取色器,在使用的过程中,发现不能拖动透明度的拖拽条,查找 github issue,发现是因为初始化的时候,给 color state 设初值是#000,应该给其带上透明度

import { FC, useState } from 'react'
import { ChromePicker } from 'react-color'
import { Wrapper, ColorBlockWrapper } from './style'

const decimalToHex = (alpha: number) =>
    alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)

const FontColor: FC<{
    color: string
    changeColor: (a: any) => void
}> = ({ color = '#000', changeColor }) => {
    const [visible, setVisible] = useState(false)
    const [acolor, setColor] = useState('#000000ff')
    return (
        <Wrapper>
            <div className="title">文本颜色</div>
            <div>
                <YHInput
                    type="text"
                    value={acolor.slice(0, -2)}
                    addonAfter={
                        <ColorBlockWrapper
                            onClick={setVisible.bind(null, true)}
                            style={{ backgroundColor: acolor }}
                        ></ColorBlockWrapper>
                    }
                />
                {visible && (
                    <ChromePicker
                        color={acolor}
                        onChange={(c) => {
                            const hexCode = `${c.hex}${decimalToHex(c.rgb.a)}`
                            setColor(hexCode)
                        }}
                    />
                )}
            </div>
        </Wrapper>
    )
}

效果如下: demo 但我发现,使用ChromePicker没有确认取色这一按钮,只有PhotoshopPicker有,但是PhotoshopPicker长这样... demo

与主题不太配套,所以还是用 chrome 的吧。至于,确认事件,只要加个监听就好,当点击事件在取色框外部时,就发送确认事件给上层。

const hideListener = (e: Event) => {
    if (ref && visible) {
        const ele = e.target
        const validArea = ref.current
        if (!validArea.contains(ele)) {
            setVisible(false)
            changeColor({ color: acolor }) // 已经发送为啥木有生效?
        }
    }
}

useEffect(() => {
    document.addEventListener('click', hideListener)
    return () => {
        document.removeEventListener('click', hideListener)
    }
}, [visible])

但把数据发给上层,并没有使画布更新,改一下字体颜色的属性值为fill

可以同步到渲染到画布上了,但这里有个问题是,用户如果频繁的改变颜色,撤回/重做的时候只会退回到上一个颜色,而不是退回到其他操作,所以我们在前几章编写的‘入队’事件就要修改一下。

默认是更改一步操作,即进行入队;这里加一个参数,如果有该参数,则只是修改当前的 current 指针所指的元素的属性,不进行入队操作,但要清空当前 current 指针之后的元素。

if (properties._ignore === true) {
    // 不入队,替换当前指针所指元素,并清空current之后的对内元素
    delete properties._ignore
    const ins = [...infos]
    ins[index] = properties
    stepCached.list[stepCached.current] = ins
    stepCached.clearAfterCurrent()
} else {
    const newInfos = [...infos]
    newInfos[index] = properties
    stepCached.enqueue(newInfos)
}

效果如下 demo 虽然忽略了一些操作,但是为什么撤回为什么撤了两步才回到上一步操作?

这是因为我们改变了当前 current 元素,存储的 A 颜色;而在离开当前取色器时又存了一次,所以在上一步监听器中,把发送事件删掉就 ok 了!

大功告成!