这是我参与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)}
/>
效果如下
双击修改文本
现在组件已经可以拖拽和变形了,文字组件还要让用户输入,修改其上的文本。由于在 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}
/>
)}
</>
)
}
最终效果如上,发现已经可以成功编辑了,但是当光标移出 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"
/>
最终效果为