机缘
- 百度上搜索问题基本都是
CSDN
的答疑,就想把自己遇到的问题也像做笔记一样记录下来,同时或许也能帮助到大家 - 本来也想写掘金,但是感觉掘金上更巨专业性,对于文章的质量要求更高,不是进行碎片化写作的地方
- 对于微信公众号,其实自己也有想写过,但是感觉更像是私域流量,正常办公用浏览器其实都找不到公众号的文章
- 想通过边笔记式的写文章,来锻炼自己学会技术文章
收获
2023年07月10 日
开始第一篇文章,到今天2024年03月29日
,将近8
个月的时间里,我创作了158
篇文章
- 获得了
前端领域新星创作者
身份 - 获得了
2000+
粉丝的关注 - 获得了
2300+
点赞 - 获得了
1900+
收藏 - 获得了
1400+
代码片分享 - 获得了
14.8W+
访问量 - 获得了
20+
次评论 - 获得了
70+
铁粉 - 获得了
7000+
排名 - 获得了持续学习
30
天榜单 2
个月获得了2
次常驻地原力榜第2
名2
个月获得了2
次常驻地原力榜第3
名
其实被同行夸奖了,还是挺开心的
日常
- 遇到问题会查询资料,并且把解决方案存在书签,阶段性整理并且输出文章
- 工作中使用了新的技术,也会整理成文章
- 阅读了有助于自身成长的文章,也会学习总结输出学习成果
成就
- 使用
Konva
在舞台上绘制图形,给图形添加事件,移动、缩放和旋转图形并且支持高性能的动画即使包含数千个图形。用到的react-konva
是基于react
封装的图形绘制。Konva
是一个HTML5 Canvas JavaScript
框架,它通过对2d context
的扩展实现了在桌面端和移动端的可交互性。Konva
提供了高性能的动画,补间,节点嵌套,布局,滤镜,缓存,事件绑定(桌面/移动端)等等功能。
/**
* @Description: KonvaContainer图片框选区域组件
* @props url 需要框选的图片的URL地址
* @props width 宽度
* @props height 高度
* @props defaultValue 默认框选起来区域的数据
* @onChange 回调方法,通知父组件框选的内容信息
* @author 小马甲丫
* @date 2023-12-05 03:22:27
*/
import React from 'react';
import useImage from 'use-image';
import { Stage, Layer, Rect, Image, Transformer } from 'react-konva';
import useKeyPress from '@/hooks/use-key-press';
/**
* 框选的图片
* @param url
* @constructor
*/
const BackgroundImage = ({ url }) => {
const [image] = useImage(url);
return <Image image={image} />;
};
/**
* 背景白板
* @param width
* @param height
* @constructor
*/
const BackgroundWhite = ({ width, height }) => {
return (
<Rect
x={0}
y={0}
width={width}
height={height}
fill="#fff"
id="rectangleBg"
name="rectangleBg"
/>
);
};
/**
* 框选出来的框
* @param canvas
* @param shapeProps
* @param onSelect
* @param onChange
* @constructor
*/
const Rectangle = ({ canvas, shapeProps, onSelect, onChange }) => {
const shapeRef = React.useRef();
return (
<Rect
onClick={() => onSelect(shapeRef)}
onTap={() => onSelect(shapeRef)}
ref={shapeRef}
{...shapeProps}
name="rectangle"
draggable
onMouseOver={() => {
document.body.style.cursor = 'move';
}}
onMouseOut={() => {
document.body.style.cursor = 'default';
}}
onDragEnd={(e) => {
onChange({
...shapeProps,
x: e.target.x(),
y: e.target.y(),
});
}}
dragBoundFunc={(pos) => {
const shapeWidth = shapeRef.current.attrs.width;
const shapeHeight = shapeRef.current.attrs.height;
let x = pos.x;
if (x <= 0) {
x = 0;
} else if (x + shapeWidth >= canvas.width) {
x = canvas.width - shapeWidth;
}
let y = pos.y;
if (y < 0) {
y = 0;
} else if (y + shapeHeight > canvas.height) {
y = canvas.height - shapeHeight;
}
return {
x,
y,
};
}}
onTransformEnd={() => {
// transformer is changing scale of the node
// and NOT its width or height
// but in the store we have only width and height
// to match the data better we will reset scale on transform end
const node = shapeRef.current;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
// we will reset it back
node.scaleX(1);
node.scaleY(1);
onChange({
...shapeProps,
x: node.x(),
y: node.y(),
// set minimal value
width: Math.max(5, node.width() * scaleX),
height: Math.max(node.height() * scaleY),
});
}}
/>
);
};
/**
* 主容器
* @param props
* @constructor
*/
const KonvaContainer = (props) => {
const [imageObject, setImageObject] = React.useState({
width: props.width,
height: props.height,
url: props.url,
});
const [rectanglesField, setRectanglesField] = React.useState([]);
const [selectedId, selectShape] = React.useState(null);
const trRef = React.useRef();
const layerRef = React.useRef();
const Konva = window.Konva;
const hideTransformer = () => {
trRef.current.nodes([]);
};
/**
* 初始化框选框
* @param list
*/
const initRectangles = (list) => {
const rects = list.map((item, index) => ({
...item,
id: `rect_${index}`,
fill: 'rgb(160, 76,4, 0.3)',
}));
setRectanglesField(rects);
};
/**
* 监听prop值变换
*/
React.useEffect(() => {
const {
url = '',
width = 0,
height = 0,
defaultValue = [],
} = props || {};
setImageObject({
width,
height,
url,
});
hideTransformer();
// 图片地址不一致说明变更图片,需要重置选框
if (url !== imageObject.url) {
setRectanglesField([]);
selectShape(null);
}
initRectangles(defaultValue);
}, [props.url, props.width, props.height, props.defaultValue]);
/**
* 更新框选框数据
* @param rects
*/
const updateRectangles = (rects) => {
setRectanglesField(rects);
props.onChange(rects);
};
/**
* 添加框选框
*/
const addRec = () => {
const data = rectanglesField;
const rects = data.slice();
const id = `rect_${rects.length}`;
rects[rects.length] = {
id,
...getSelectionObj(),
};
updateRectangles(rects);
selectShape(id);
};
/**
* 删除框选框
*/
const delRec = () => {
const data = rectanglesField;
const rects = data.slice().filter((rect) => rect.id !== selectedId);
updateRectangles(rects);
hideTransformer();
document.body.style.cursor = 'default';
selectShape(null);
};
const selectionRectRef = React.useRef();
const selection = React.useRef({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
/**
* 高亮框选框
* @param id
*/
const activeTransformer = (id) => {
const activeRect =
layerRef.current.find('.rectangle').find((elementNode) => elementNode.attrs.id === id) ||
selectionRectRef.current;
trRef.current.nodes([activeRect]);
};
/**
* useKeyPress监听键盘按键删除键del和返回键backspace
* 8 返回键
* 46 删除键
*/
useKeyPress([8, 46], (e) => {
// disable click event
Konva.listenClickTap = false;
if (e.target.style[0] === 'cursor') delRec();
});
/**
* 获取选中的框选框的信息
*/
const getSelectionObj = () => {
return {
x: Math.min(selection.current.x1, selection.current.x2),
y: Math.min(selection.current.y1, selection.current.y2),
width: Math.abs(selection.current.x1 - selection.current.x2),
height: Math.abs(selection.current.y1 - selection.current.y2),
fill: 'rgb(160, 76,4, 0.3)',
};
};
/**
* 更新框选框
*/
const updateSelectionRect = () => {
const node = selectionRectRef.current;
node.setAttrs({
...getSelectionObj(),
visible: selection.current.visible,
});
node.getLayer().batchDraw();
};
/**
* 开始绘制框选框
* @param e
*/
const onMouseDown = (e) => {
const isTransformer = e.target.findAncestor('Transformer');
if (isTransformer) {
return;
}
hideTransformer();
const pos = e.target.getStage().getPointerPosition();
selection.current.visible = true;
selection.current.x1 = pos.x;
selection.current.y1 = pos.y;
selection.current.x2 = pos.x;
selection.current.y2 = pos.y;
updateSelectionRect();
};
/**
* 绘制框选框中
* @param e
*/
const onMouseMove = (e) => {
if (!selection.current.visible) {
return;
}
const pos = e.target.getStage().getPointerPosition();
selection.current.x2 = pos.x;
selection.current.y2 = pos.y;
updateSelectionRect();
};
/**
* 结束绘制框选框
* @param e
*/
const onMouseUp = (e) => {
// 点击Rect框时,会返回该Rect的id
// 画框时鼠标在Rect上松开,会返回该Rect的id
const dragId = e.target.getId();
if (!selection.current.visible) {
return;
}
// 是否鼠标拖动,并且偏移量大于10时才算拖动。拖动Rect没有偏移量,画框才有偏移量
const { current: { x1 = 0, x2 = 0, y1 = 0, y2 = 0 } = {} } = selection || {};
const isMove = (x1 !== x2 && Math.abs(x1 - x2) > 10) || (y1 !== y2 && Math.abs(y1 - y2) > 10);
// 点击后有拖动就添加Rect框,并且偏移量大于10时才算拖动
if (isMove) {
addRec();
}
// 设置可调节大小节点
if (!!dragId && !isMove) {
// 点击已有的Rect框才设置,并且拖动小于10,也就是没有拖动
activeTransformer(dragId);
} else if (isMove) {
// 拖动大于10,生成新的Rect框
activeTransformer();
}
selection.current.visible = false;
// disable click event
Konva.listenClickTap = false;
updateSelectionRect();
};
return (
<Stage
width={imageObject.width}
height={imageObject.height}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
>
<Layer ref={layerRef}>
<BackgroundWhite {...imageObject} />
<BackgroundImage {...imageObject} />
{
rectanglesField.map((rect, i) => {
return (
<Rectangle
key={i}
getKey={i}
canvas={imageObject}
shapeProps={rect}
isSelected={rect.id === selectedId}
getLength={rectanglesField.length}
onSelect={() => {
selectShape(rect.id);
}}
onChange={(newAttrs) => {
const rects = rectanglesField.slice();
rects[i] = newAttrs;
updateRectangles(rects);
}}
/>
);
})
}
<Transformer
ref={trRef}
rotationSnaps={[0, 90, 180, 270]}
keepRatio={false}
anchorSize={4}
anchorStroke='#a04c04'
anchorFill="#fff"
borderStroke='#a04c04'
borderDash={[1, 1]}
enabledAnchors={['top-left', 'top-right', 'bottom-left', 'bottom-right']}
boundBoxFunc={(oldBox, newBox) => {
// limit resize
// newBox.rotation !== 0进入return oldBox,就可实现不让旋转
if (newBox.width < 20 || newBox.height < 20) {
return oldBox;
}
return newBox;
}}
/>
<Rect ref={selectionRectRef} />
</Layer>
</Stage>
);
};
export default KonvaContainer;
/**
* @Description: use-key-press hook
* @author 小马甲丫
* @date 2023-12-05 03:22:27
*/
import { useCallback, useEffect, MutableRefObject } from 'react';
type keyType = KeyboardEvent['keyCode'] | KeyboardEvent['key'];
type keyFilter = keyType | keyType[];
type EventHandler = (event: KeyboardEvent) => void;
type keyEvent = 'keydown' | 'keyup';
type BasicElement = HTMLElement | Element | Document | Window;
type TargetElement = BasicElement | MutableRefObject<null | undefined>;
type EventOptions = {
events?: keyEvent[];
target?: TargetElement;
};
const modifierKey: any = {
ctrl: (event: KeyboardEvent) => event.ctrlKey,
shift: (event: KeyboardEvent) => event.shiftKey,
alt: (event: KeyboardEvent) => event.altKey,
meta: (event: KeyboardEvent) => event.metaKey,
};
const defaultEvents: keyEvent[] = ['keydown'];
/**
* 判断对象类型
* @param obj 参数对象
* @returns String
*/
function isType<T>(obj: T): string {
return Object.prototype.toString
.call(obj)
.replace(/^\[object (.+)\]$/, '$1')
.toLowerCase();
}
/**
* 获取当前元素
* @param target TargetElement
* @param defaultElement 默认绑定的元素
*/
function getTargetElement(target?: TargetElement, defaultElement?: BasicElement) {
if (!target) {
return defaultElement;
}
if ('current' in target) {
return target.current;
}
return target;
}
/**
* 按键是否激活
* @param event 键盘事件
* @param keyFilter 当前键
*/
const keyActivated = (event: KeyboardEvent, keyFilter: any) => {
const type = isType(keyFilter);
const { keyCode } = event;
if (type === 'number') {
return keyCode === keyFilter;
}
const keyCodeArr = keyFilter.split('.');
// 符合条件的长度
let genLen = 0;
// 组合键
keyCodeArr.forEach((key) => {
const genModifier = modifierKey[key];
if ((genModifier && genModifier) || keyCode === key) {
genLen++;
}
});
return genLen === keyCodeArr.length;
};
/**
* 键盘按下预处理方法
* @param event 键盘事件
* @param keyFilter 键码集
*/
const genKeyFormate = (event: KeyboardEvent, keyFilter: any) => {
const type = isType(keyFilter);
if (type === 'string' || type === 'number') {
return keyActivated(event, keyFilter);
}
// 多个键
if (type === 'array') {
return keyFilter.some((item: keyFilter) => keyActivated(event, item));
}
return false;
};
/**
* 监听键盘按下/松开
* @param keyCode
* @param eventHandler
* @param options
*/
const useKeyPress = (
keyCode: keyFilter,
eventHandler?: EventHandler,
options: EventOptions = {},
) => {
const { target, events = defaultEvents } = options;
const callbackHandler = useCallback(
(event) => {
if (genKeyFormate(event, keyCode)) {
typeof eventHandler === 'function' && eventHandler(event);
}
},
[keyCode],
);
useEffect(() => {
const el = getTargetElement(target, window)!;
events.forEach((eventName) => {
el.addEventListener(eventName, callbackHandler);
});
return () => {
events.forEach((eventName) => {
el.removeEventListener(eventName, callbackHandler);
});
};
}, [keyCode, events, callbackHandler]);
};
export default useKeyPress;
憧憬
- 希望自己能更具深度和广度地学习
- 提高自身价值和影响力
- 加强方案落地的能力
- 团队建设能力