这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战
相关阅读
- 使用konva制作在线photoshop(1)——元素拖拽、变形与导出
- 使用react-konva制作在线photoshop(2)——字体的文本与样式的修改
- 使用react-konva制作在线制图应用(3)——在线字体文件的动态渲染
- 使用react-konva制作在线制图应用(4)——撤销/重做(踩坑篇)
- 使用react-konva制作在线制图应用(5)——撤销/重做(填坑篇)
- # 使用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>
)
}
效果如下:
但我发现,使用ChromePicker
没有确认取色这一按钮,只有PhotoshopPicker
有,但是PhotoshopPicker
长这样...
与主题不太配套,所以还是用 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)
}
效果如下 虽然忽略了一些操作,但是为什么撤回为什么撤了两步才回到上一步操作?
这是因为我们改变了当前 current 元素,存储的 A 颜色;而在离开当前取色器时又存了一次,所以在上一步监听器中,把发送事件删掉就 ok 了!
大功告成!