Canvas简历编辑器-选中绘制与拖拽多选交互方案
Canvas简历编辑器-选中绘制与拖拽多选交互方案
在之前我们聊了聊如何基于Canvas
与基本事件组合实现了轻量级DOM
,并且在此基础上实现了如何进行管理事件以及多层级渲染的能力设计。那么此时我们就依然在轻量级DOM
的基础上,关注于实现选中绘制与拖拽多选交互设计。
- 在线编辑: https://windrunnermax.github.io/CanvasEditor
- 开源地址: https://github.com/WindrunnerMax/CanvasEditor
关于Canvas
简历编辑器项目的相关文章:
- 社区老给我推Canvas,我也学习Canvas做了个简历编辑器
- Canvas图形编辑器-数据结构与History(undo/redo)
- Canvas图形编辑器-我的剪贴板里究竟有什么数据
- Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)
- Canvas简历编辑器-Monorepo+Rspack工程实践
- Canvas简历编辑器-层级渲染与事件管理能力设计
- Canvas简历编辑器-选中绘制与拖拽多选交互方案
选中绘制
我们先来聊一聊最基本的节点点击选中以及拖拽的交互,而在聊具体的代码实现之前,我们先来看一下对于图形的绘制问题。在Canvas
中我们绘制路径的话,我们可以通过fill
来填充路径,也可以通过stroke
来描边路径,而在我们描边的时候,如果不注意的话可能会陷入一些绘制的问题。假如此时我们要绘制一条线,我们可以分别来看下使用stroke
和fill
的绘制方法实现,此时如果在高清ctx.scale(devicePixel, devicePixel)
情况下,则能明显地看出来绘制位置差0.5px
,而如果基准为1px
的话则会出现1px
的差值以及色值偏差。
ctx.beginPath();
ctx.strokeStyle = "blue";
ctx.lineWidth = 1;
ctx.moveTo(5, 5);
ctx.lineTo(100, 5);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "red";
ctx.beginPath();
ctx.moveTo(100, 5);
ctx.lineTo(200, 5);
ctx.lineTo(200, 6);
ctx.lineTo(100, 6);
ctx.closePath();
ctx.fill();
在先前的选中图形frame
中,我们都是用stroke
来实现的,然后最近我想将其真正作为外边框来绘制,然后就发现想绘制inside stroke
确实不是一件容易的事。从MDN
上阅读stroke
的文档可以得到其是以路径的中心线为基准的,也就是说stroke
是由基准分别向内外扩展的,那么问题就来了,假如我们绘制了一条线,而这条线本身是存在1px
宽度的,那么初步理解按照文档所说其本身结构应该是以这1px
本身的中心点也就是0.5px
的位置为中心点向外发散,然而其实际效果是以1px
的外边缘为基准发散,那么就会导致1px
的线在stroke
之后会多出0.5px
的宽度,这个效果可以通过lineTo(0, 100)
外加lineWith=1
来测试,可以发现其可见宽度只有0.5px
,这点可以通过再画一个1px
的Path
来对比。
ctx.beginPath();
ctx.lineWidth = 6;
ctx.strokeStyle = "blue";
ctx.moveTo(0, 0);
ctx.lineTo(100, 0);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = "red";
ctx.moveTo(100, 3);
ctx.lineTo(200, 3);
ctx.closePath();
ctx.stroke();
那么这里的Strokes are aligned to the center of a path
可能与我理解的center of a path
并不相同,或许其只是想表达stroke
是分别向两侧绘制描边的,而并不是解释其基准位置。关于这个问题我咨询了一下,这里主要是理解有偏差,在我们使用API
绘制路径时,本身并没有设置宽度的信息,而坐标信息定义的是路径的轮廓或边界,因此我们在最开始定义的路径结构1px
是不成立的。在图形学的上下文中,路径path
通常是指一个几何形状的轮廓或线条,路径本身是数学上的抽象概念,没有宽度,只是一个由点和线段构成的轨迹,因此当我们提到描边stroke
时,指的是一个可视化过程,即在路径的周围绘制有宽度的线条。
实际上这里如果仅仅是处理frame
的问题的话,可能并没有太大的问题,然而在处理节点的时候,发现由于是使用stroke
绘制的操作节点,那么实际上其总是会超出原始宽度的,也就是上边说的描边问题,而因为超出的这0.5px
的边缘节点,使得我一直认为绘制节点的边缘与填充是没问题的,然而今天才发现这里的顺序反了,描边的内部会被填充覆盖掉,也就是说实现的border
宽度总是会被除以2
的,因此要先填充再描边才是正确的绘制方式。此外,无论是frame
节点的绘制还是类似border
的绘制,在Firefox
中inside stroke
总是会出现兼容性问题,仅有组合fill
以及使用fill
配合Path2D + clip
才能绘制正常的inside stroke
。
ctx.save();
ctx.beginPath();
ctx.arc(70, 75, 50, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "white";
ctx.fill();
ctx.closePath();
ctx.restore();ctx.save();
ctx.beginPath();
ctx.arc(200, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.closePath();
ctx.restore();
那么我们就可以利用三种方式绘制inside stroke
,当然还有借助lineTo/fillRect
分别绘制4
条边的方式我们没有列举,因为这种方式自然不会出现什么问题,其本身就是使用fill
的方式绘制的,而我们这里主要是讨论stroke
的绘制问题,只是借助Path2D
同样也是fill
的方式绘制的,但是这里需要讨论一下clip
的fillRule-nonzero/evenodd
的问题。那么借助stroke
的特性,方式1
是我们绘制两倍的lineWidth
,然后裁剪掉外部的描边部分,这样就能够正确保留内部的描边了,方式2
则是我们主动校准了描边的位置,将其向内缩小0.5px
的位置,由此来绘制完整的描边,方式3
是借助evenodd
的填充规则,通过clip
来生成规则保留内部的描边,再来实际填充即可实现。
<canvas id="canvas" width="800" height="800"></canvas>
<script>// https://stackoverflow.com/questions/36615592/canvas-inner-strokeconst canvas = document.getElementById("canvas");const ctx = canvas.getContext("2d");const devicePixelRatio = Math.ceil(window.devicePixelRatio || 1);const width = canvas.clientWidth;const height = canvas.clientHeight;canvas.width = width * devicePixelRatio;canvas.height = height * devicePixelRatio;canvas.style.width = width + "px";canvas.style.height = height + "px";ctx.scale(devicePixelRatio, devicePixelRatio);ctx.save();ctx.beginPath();ctx.rect(10, 10, 150, 100);ctx.clip();ctx.closePath();ctx.lineWidth = 2;ctx.strokeStyle = "blue";ctx.stroke();ctx.restore();ctx.save();ctx.beginPath();ctx.rect(170 + 0.5, 10 + 0.5, 150 - 1, 100 - 1);ctx.closePath();ctx.lineWidth = 1;ctx.strokeStyle = "blue";ctx.stroke();ctx.restore();ctx.save();ctx.beginPath();const region = new Path2D();region.rect(330, 10, 150, 100);region.rect(330 + 1, 10 + 1, 150 - 2, 100 - 2);ctx.clip(region, "evenodd");ctx.rect(330, 10, 150, 100);ctx.closePath();ctx.fillStyle = "blue";ctx.fill();ctx.restore();
</script>
那么先前我们也提到了在Firefox
浏览器的兼容性问题,那么我们将上述的实现方式在Firefox
中进行测试,可以发现inside stroke
的绘制是有些许问题的,第一个图形明显左上的线比右下的线细一些,第二个图形则明显会粗糙一些,第三个图形则看起来绘制更细致更符合1px
的绘制。因此我们如果想要兼容绘制inside stroke
的话最好的方式还是选择方式三,当然像最开始的实现中借助lineTo/fillRect
分别绘制4
条边的方式自然也是没问题的,两者的性能对比在后边也可以尝试实验一下。
那么接着我们就回到在轻量级DOM
上实现选中的绘制,首先我们对基本节点的事件做一些通用的实现,我们先来实现点击的选取。因为在之前我们已经定义好了事件的基本传递,那么我们此时只需要在Element
节点上实现事件的响应即可,那么在这里我们就可以直接操作选区模块,直接将当前的活跃节点id
设置为节点组的内容即可。
// packages/core/src/canvas/dom/element.ts
export class ElementNode extends Node {protected onMouseDown = (e: MouseEvent) => {this.editor.selection.setActiveDelta(this.id);};
}
而当我们触发选区的节点设置之后,在选区模块则会将此时所有的active
节点组合起来形成新的Range
,然后在新的Range
基础上判断当前是否应该触发选区变换的事件,这里的事件分发比较重要,整个编辑器的选区变化事件都会在此处分发。
// packages/core/src/selection/index.ts
export class Selection {public set(range: Range | null) {if (this.editor.state.get(EDITOR_STATE.READONLY)) return this;const previous = this.current;if (Range.isEqual(previous, range)) return this;this.current = range;this.editor.event.trigger(EDITOR_EVENT.SELECTION_CHANGE, {previous,current: range,});return this;}public setActiveDelta(...deltaIds: string[]) {this.active.clear();deltaIds.forEach(id => this.active.add(id));this.compose();}public compose() {const active = this.active;if (active.size === 0) {this.set(null);return void 0;}let range: Range | null = null;active.forEach(key => {const delta = this.editor.deltaSet.get(key);if (!delta) return void 0;const deltaRange = Range.from(delta);range = range ? range.compose(deltaRange) : deltaRange;});this.set(range);}
}
那么在事件分发之后,我们必须要在选区变换之后绘制新的选区,实际上在选区变换后我们理论上仅仅需要将节点绘制出来即可,而按照我们先前的调度设计而言,我们需要主动按需触发要绘制的区域,并且由于选区是由其他的位置变换到当前区域的,因此绘制时就需要将先前的区域同时绘制。那么按照我们先前的设计,SelectNode
本身既是事件处理器又是渲染器,基本与DOM
节点基本一致,只是我们绑定事件和绘制都是直接由类控制而已,而在drawingMask
的Shape.frame
绘制中,就是我们最开始聊的描边与填充绘制问题。
// packages/core/src/canvas/dom/node.ts
export class SelectNode extends Node {protected onSelectionChange = (e: SelectionChangeEvent) => {const { current, previous } = e;this.editor.logger.info("Selection Change", current);const range = current || previous;if (range) {const refresh = range.compose(previous).compose(current);this.editor.canvas.mask.drawingEffect(refresh.zoom(RESIZE_OFS));}};public drawingMask = (ctx: CanvasRenderingContext2D) => {const selection = this.editor.selection.get();if (selection) {const { x, y, width, height } = selection.rect();Shape.frame(ctx, { x, y, width, height, borderColor: BLUE_6 });}};
}
拖拽多选
当我们已经成功实现图形单选以及节点绘制之后,我们很容易想到两个交互问题,首先是图形的多选,因为我们在选中节点的时候可能不会仅仅选一个节点,例如全选的场景,其次则是选中图形的拖拽,这个就是常见的交互方式了,无论是单选还是多选的时候,都可以通过拖拽图形来调整位置。那么我们首先来看一下多选,实际上在上边我们的设计中本就是支持多选的,我们在选区的active
就是Set<string>
类型,以及Selection
的compose
方法也是支持多选的,那么我们只需要在选中节点的时候,将节点的id
添加到active
中即可。
// packages/core/src/canvas/dom/element.ts
export class ElementNode extends Node {protected onMouseDown = (e: MouseEvent) => {if (e.shiftKey) {this.editor.selection.addActiveDelta(this.id);} else {this.editor.selection.setActiveDelta(this.id);}};
}
除了按住shiftKey
键进行多选之外,我们使用鼠标以某个点为起点拖拽选区进行选择也是一种多选的方式,那么在这里我们将这个交互方式设计在了FrameNode
内,而这里有点不同的是我们的起始行为需要归并到Root
节点上,因为只有点击在Root
节点上的事件我们才认为是起始,否则是认为点击到了节点本身上,而框选这个交互的本身事件则主要是判断当前的选区大小,以及其覆盖的节点范围,将覆盖的节点id
全部放置于选区模块即可。
// packages/core/src/canvas/dom/frame.ts
export class FrameNode extends Node {private onRootMouseDown = (e: MouseEvent) => {this.savedRootMouseDown(e);this.unbindOpEvents();this.bindOpEvents();this.landing = Point.from(e.x, e.y);this.landingClient = Point.from(e.clientX, e.clientY);};private onMouseMoveBridge = (e: globalThis.MouseEvent) => {if (!this.landing || !this.landingClient) return void 0;const point = Point.from(e.clientX, e.clientY);const { x, y } = this.landingClient.diff(point);if (!this.isDragging && (Math.abs(x) > SELECT_BIAS || Math.abs(y) > SELECT_BIAS)) {// 拖拽阈值this.isDragging = true;}if (this.isDragging) {const latest = new Range({startX: this.landing.x,startY: this.landing.y,endX: this.landing.x + x,endY: this.landing.y + y,}).normalize();this.setRange(latest);// 获取获取与选区交叉的所有`State`节点const effects: string[] = [];this.editor.state.getDeltasMap().forEach(state => {if (latest.intersect(state.toRange())) effects.push(state.id);});this.editor.selection.setActiveDelta(...effects);// 重绘拖拽过的最大区域const zoomed = latest.zoom(RESIZE_OFS);this.dragged = this.dragged ? this.dragged.compose(zoomed) : zoomed;this.editor.canvas.mask.drawingEffect(this.dragged);}};private onMouseMoveController = throttle(this.onMouseMoveBridge, ...THE_CONFIG);private onMouseUpController = () => {this.unbindOpEvents();this.setRange(Range.reset());if (this.isDragging) {this.dragged && this.editor.canvas.mask.drawingEffect(this.dragged);}this.landing = null;this.isDragging = false;this.dragged = null;this.setRange(Range.reset());};public drawingMask = (ctx: CanvasRenderingContext2D) => {if (this.isDragging) {const { x, y, width, height } = this.range.rect();Shape.rect(ctx, { x, y, width, height, borderColor: BLUE_5, fillColor: BLUE_6_6 });}};
}
说到这里,在多选之外这里我们可能还需要关注一个交互,就是Hover
的效果。如果我们是CSS
实现的话,这个问题实际上很简单,无非是增加一个伪类的问题,然而在Canvas
中我们需要自己实现这个效果,也就是需要借助MouseEvent
来手动处理这个过程。当然思路是比较简单的,我们只需要维护一个boolean
的id
标识来确定当前节点是否被Hover
,然后根据选区状态来判断是否需要绘制当前节点的Range
即可。
// packages/core/src/canvas/dom/element.ts
export class ElementNode extends Node {protected onMouseEnter = () => {this.isHovering = true;if (this.editor.selection.has(this.id)) {return void 0;}this.editor.canvas.mask.drawingEffect(this.range);};protected onMouseLeave = () => {this.isHovering = false;if (this.editor.selection.has(this.id)) {return void 0;}this.editor.canvas.mask.drawingEffect(this.range);};public drawingMask = (ctx: CanvasRenderingContext2D) => {if (this.isHovering &&!this.editor.selection.has(this.id) &&!this.editor.state.get(EDITOR_STATE.MOUSE_DOWN)) {const { x, y, width, height } = this.range.rect();Shape.frame(ctx, {x: x,y: y,width: width,height: height,borderColor: BLUE_4,});}};
}
而事件的调度则是由Root
节点来实现的,这里主要是维护了一个互斥的hoverId
来实现的,当然这里的主要目的还是模拟OnMouseEnter
以及OnMouseLeave
事件。基本逻辑是遍历当前的节点,如果发现需要触发相关事件的节点,则判断鼠标是否在当前节点内,如果在节点内则作为命中的节点,判断当前Hover
的节点如果与先前不一致,则根据具体的条件来判断并且触发先前的节点MouseLeave
与当前节点MouseEnter
事件。
// packages/core/src/canvas/state/root.ts
export class Root extends Node {/** Hover 节点 */public hover: ElementNode | ResizeNode | null;private onMouseMoveBasic = (e: globalThis.MouseEvent) => {// 非默认状态下不执行事件if (!this.engine.isDefaultMode()) return void 0;// 按事件顺序获取节点const flatNode = this.getFlatNode();let next: ElementNode | ResizeNode | null = null;const point = Point.from(e, this.editor);for (const node of flatNode) {// 当前只有`ElementNode`和`ResizeNode`需要触发`Mouse Enter/Leave`事件const authorize = node instanceof ElementNode || node instanceof ResizeNode;if (authorize && node.range.include(point)) {next = node;break;}}// 如果命中的节点与先前 Hover 的节点不一致if (this.hover !== next) {const prev = this.hover;this.hover = next;if (prev !== null) {this.emit(prev, NODE_EVENT.MOUSE_LEAVE, MouseEvent.from(e, this.editor));if (prev instanceof ElementNode) {this.editor.event.trigger(EDITOR_EVENT.HOVER_LEAVE, { node: prev });}}if (next !== null) {this.emit(next, NODE_EVENT.MOUSE_ENTER, MouseEvent.from(e, this.editor));if (next instanceof ElementNode) {this.editor.event.trigger(EDITOR_EVENT.HOVER_ENTER, { node: next });}}}};
}
紧接着我们就来聊一聊选区节点的拖拽移动问题,关于这部分能力的实现我们将其作为了SelectNode
的一部分实现。对于拖拽这件事本身来说,我们只需要关注MouseDown
绑定事件、MouseMove
移动、MouseUp
取消绑定事件,那么这里我们同样也是类似的实现,只不过由于我们需要考虑节点的绘制,因此需要在其中穿插着图形的drawing
方法调用。在这里我们采用了最方便的按需绘制方案,即所有拖拽过的区域都重新绘制,当然最好的方案还是当前事件触发区域的重绘,这样性能会更好一些,且在这里我们只绘制拖拽的边框而不是将所有节点都拖拽着绘制。此外,在这里我们还实现了交互上的优化,即只有拖拽超过一定的阈值才会触发拖拽事件,这样可以避免误操作。
// packages/core/src/canvas/dom/select.ts
export class SelectNode extends Node {private onMouseDownController = (e: globalThis.MouseEvent) => {// 非默认状态下不执行事件if (!this.editor.canvas.isDefaultMode()) return void 0;// 取消已有事件绑定this.unbindDragEvents();const selection = this.editor.selection.get();// 选区 & 严格点击区域判定if (!selection || !this.isInSelectRange(Point.from(e, this.editor), this.range)) {return void 0;}this.dragged = selection;this.landing = Point.from(e.clientX, e.clientY);this.bindDragEvents();this.refer.onMouseDownController();};private onMouseMoveBasic = (e: globalThis.MouseEvent) => {const selection = this.editor.selection.get();if (!this.landing || !selection) return void 0;const point = Point.from(e.clientX, e.clientY);const { x, y } = this.landing.diff(point);// 超过阈值才认为正在触发拖拽if (!this._isDragging && (Math.abs(x) > SELECT_BIAS || Math.abs(y) > SELECT_BIAS)) {this._isDragging = true;}if (this._isDragging && selection) {const latest = selection.move(x, y);const zoomed = latest.zoom(RESIZE_OFS);// 重绘拖拽过的最大区域this.dragged = this.dragged ? this.dragged.compose(zoomed) : zoomed;this.editor.canvas.mask.drawingEffect(this.dragged);const offset = this.refer.onMouseMoveController(latest);this.setRange(offset ? latest.move(offset.x, offset.y) : latest);}};private onMouseMoveController = throttle(this.onMouseMoveBasic, ...THE_CONFIG);private onMouseUpController = () => {this.unbindDragEvents();this.refer.onMouseUpController();const selection = this.editor.selection.get();if (this._isDragging && selection) {const rect = this.range;const { startX, startY } = selection.flat();const ids = [...this.editor.selection.getActiveDeltaIds()];this.editor.state.apply(new Op(OP_TYPE.MOVE, { ids, x: rect.start.x - startX, y: rect.start.y - startY }));this.editor.selection.set(rect);this.dragged && this.editor.canvas.mask.drawingEffect(this.dragged);}this.landing = null;this.dragged = null;this._isDragging = false;};
}
最后
在这里我们就依然在轻量级DOM
的基础上,讨论了Canvas
中描边与填充的绘制问题,以及inside stroke
的实现方式,然后我们实现了基本的选中绘制以及拖拽多选的交互设计,并且实现了Hover
的效果,以及拖拽节点的移动。那么在后边我们可以聊一下fillRule
规则设计、按需绘制图形节点,也可以聊到更多的交互设计,例如Resize
的交互设计、参考线能力的实现、富文本的绘制方案等等。
每日一题
- https://github.com/WindRunnerMax/EveryDay
参考
- https://github.com/WindRunnerMax/CanvasEditor
- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas
- https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
相关文章:
Canvas简历编辑器-选中绘制与拖拽多选交互方案
Canvas简历编辑器-选中绘制与拖拽多选交互方案 在之前我们聊了聊如何基于Canvas与基本事件组合实现了轻量级DOM,并且在此基础上实现了如何进行管理事件以及多层级渲染的能力设计。那么此时我们就依然在轻量级DOM的基础上,关注于实现选中绘制与拖拽多选交…...
kotlin的dagger hilt依赖注入
依赖注入(dependency injection, di)是设计模式的一种,它的实际作用是给对象赋予实例变量。 基础认识 class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceSta…...
Flink类加载机制详解
1. 总览 在运行Flink应用时,它会加载各种类,另外我们用户代码也会引入依赖,由于他们依赖版本以及加载顺序等不同,就可能会导致冲突,所以很要必要了解Flink是如何加载类的。 根据加载的来源的不同,我们可以将类分为三种: Java Classpath:Java类路径下,这是Java通用的…...
在VSCode中设置bash命令行内容简写
在VSCode中设置bash命令行内容简写 1、打开shell配置文件2、在配置文件的末尾,添加以下行来创建别名3、重新加载配置文件以使更改生效4、在命令行使用缩写执行命令 比如,在VSCode的bash中输入 gc daily,而实际执行 git checkout daily. 1、…...
特制一个自己的UI库,只用CSS、图标、emoji图 第二版
图: 代码: index.html <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>M…...
51_Lua面向对象编程
面向对象编程(Object Oriented Programming,OOP)是一种非常流行的计算机编程架构。像C++、Java、Objective-C、Smalltalk、C#、Ruby等编程语言都支持面向对象编程。 1.面向对象编程特性 面向对象编程是一种编程范式,它使用“对象”来设计软件。对象是数据和行为的封装单元…...
Kafka——两种集群搭建详解 k8s
1、简介 Kafka是一个能够支持高并发以及流式消息处理的消息中间件,并且Kafka天生就是支持集群的,今天就主要来介绍一下如何搭建Kafka集群。 Kafka目前支持使用Zookeeper模式搭建集群以及KRaft模式(即无Zookeeper)模式这两种模式搭…...
OPT: Open Pre-trained Transformer语言模型
摘要 大规模语言模型通常需要数十万计算日的训练时间,展现了在零样本和小样本学习中的显著能力。鉴于其计算成本之高,这些模型在没有大量资本投入的情况下难以复现。对于那些通过API提供的少数模型,研究者无法获取完整的模型权重,…...
开源模型应用落地-LangChain高阶-记忆组件-RedisChatMessageHistory正确使用(八)
一、前言 LangChain 的记忆组件发挥着至关重要的作用,其旨在协助大语言模型(LLM)有效地留存历史对话信息。通过这一功能,使得大语言模型在对话过程中能够更出色地维持上下文的连贯性和一致性,进而能够像人类的记忆运作方式那样,进行更为自然、流畅且智能化的交互。 它仿佛…...
http和https有哪些不同
http和https有哪些不同 1.数据传输的安全性:http非加密,https加密 2.端口号:http默认80端口,https默认443端口 3.性能:http基于tcp三次握手建立连接,https在tcp三次握手后还有TLS协议的四次握手确认加密…...
UML系列之Rational Rose笔记七:状态图
一、新建状态图 依旧是新建statechart diagram; 二、工作台介绍 接着就是一个状态的开始:开始黑点依旧可以从左边进行拖动放置: 这就是状态的开始,和活动图泳道图是一样的;只能有一个开始,但是可以有多个…...
一个使用 Golang 编写的新一代网络爬虫框架,支持JS动态内容爬取
大家好,今天给大家分享一个由ProjectDiscovery组织开发的开源“下一代爬虫框架”Katana,旨在提供高效、灵活且功能丰富的网络爬取体验,适用于各种自动化管道和数据收集任务。 项目介绍 Katana 是 ProjectDiscovery 精心打造的命令行界面&…...
mycat介绍与操作步骤
文章目录 1.分库分表2.mycat 入门2.1 概述2.2 案例:水平分表1)准备工作2)配置3)启动并测试 3.mycat 配置详解3.1 schema.xml3.2 rule.xml3.3 server.xml 4.mycat 分片:垂直拆分1)准备工作2)配置…...
【Go】:图片上添加水印的全面指南——从基础到高级特性
前言 在数字内容日益重要的今天,保护版权和标识来源变得关键。为图片添加水印有助于声明所有权、提升品牌认知度,并防止未经授权的使用。本文将介绍如何用Go语言实现图片水印,包括静态图片和带旋转、倾斜效果的文字水印,帮助您有…...
R语言的语法糖
R语言的语法糖 引言 在编程语言中,所谓的“语法糖”是指那些使得程序员能够以更简洁、直观的方式书写代码的语法形式。R语言作为一种用于统计分析和数据可视化的编程语言,具有丰富的功能和灵活的语法。本文将深入探讨R语言中的语法糖,帮助读…...
乙游的尽头是虚拟偶像吗?
眼花了,竟然看到二次元乙游男主角走红毯了。 12月20日,某国际知名奢侈品品牌宣布,《恋与深空》四位男主将受邀出席品牌在上海举办的TF戏瘾之夜活动,并公开了四位男主的红毯照片。 没有真人实体的乙游男主走红毯?这是…...
【源码+文档+调试讲解】农产品研究报告管理系统
摘 要 农产品研究报告管理系统是一个旨在收集、整理、存储和分析农产品相关研究数据的综合性平台。农产品研究报告管理系统通常包含一个强大的数据库,它能够处理大量的研究数据,并对这些数据进行有效的管理和备份。农产品研究报告管理系统是现代农业科学…...
SQL UNION 操作符
SQL UNION 操作符 SQL UNION 操作符用于合并两个或多个 SELECT 语句的结果集。它将多个结果集组合成一个单独的结果集,并去除重复的行。为了使用 UNION,每个 SELECT 语句必须具有相同的列数,并且对应列的数据类型必须兼容。 语法 SELECT c…...
springboot vue uniapp 仿小红书 1:1 还原 (含源码演示)
线上预览: 移动端 http://8.146.211.120:8081/ 管理端 http://8.146.211.120:8088/ 小红书凭借优秀的产品体验 和超高人气 目前成为笔记类产品佼佼者 此项目将详细介绍如何使用Vue.js和Spring Boot 集合uniapp 开发一个仿小红书应用,凭借uniapp 可以在h5 小程序 app…...
扩散模型学习
扩散模型学习 DDPM(参考1) DDIM(参考1,参考2)...
【面试】MySQL 最左匹配原则
MySQL的最左匹配原则是在使用联合索引时非常重要的概念,理解并合理运用该原则能显著提升查询性能。以下从多个方面详细介绍: 1. 联合索引结构基础 联合索引是对多个列创建的索引。在MySQL中,联合索引以多列值的组合形式,按照创建…...
Mac 删除ABC 输入法
参考链接:百度安全验证 Mac下删除系统自带输入法ABC,正解!_mac删除abc输入法-CSDN博客 ABC 输入法和搜狗输入法等 英文有冲突~~ 切换后还会在英文状态,可以删除 ;可能会对DNS 输入有影响,但是可以通过复…...
Nginx代理同域名前后端分离项目的完整步骤
前后端分离项目,前后端共用一个域名。通过域名后的 url 前缀来区别前后端项目。 以 vue php 项目为例。直接上 server 模块的 nginx 配置。 server{ listen 80; #listen [::]:80 default_server ipv6onlyon; server_name demo.com;#二配置项目域名 index index.ht…...
21、Transformer Masked loss原理精讲及其PyTorch逐行实现
1. Transformer结构图 2. python import torch import torch.nn as nn import torch.nn.functional as Ftorch.set_printoptions(precision3, sci_modeFalse)if __name__ "__main__":run_code 0batch_size 2seq_length 3vocab_size 4logits torch.randn(batch…...
CNN张量输入形状和特征图
CNN张量输入形状和特征图 这个是比较容易理解的张量的解释,比较直观 卷积神经网络 在这个神经网络编程系列中,我们正在逐步构建一个卷积神经网络(CNN),所以让我们看看CNN的张量输入。 在最后两篇文章中&…...
RK3399开发板Linux实时性改造
本次测试基于NanoPC-T4开发板(国产化处理器RK3399),4.19.111内核Xenomai实时性改造测试。 Xenomai下载网站:https://xenomai.org/downloads/ NanoPC-T4网站:https://wiki.friendlyarm.com/wiki/index.php/NanoPC-T4/z…...
ASP.NET Core 中的高效后台任务管理
一、引言 在当今快速发展的 Web 开发领域,ASP.NET Core 凭借其卓越的性能、强大的功能和高度的灵活性,已然成为众多开发者构建现代 Web 应用程序的首选框架。它不仅能够高效地处理各种复杂的业务逻辑,还为开发者提供了丰富多样的工具和功能&…...
Spring Boot 2 学习指南与资料分享
Spring Boot 2 学习资料 Spring Boot 2 学习资料 Spring Boot 2 学习资料 在当今竞争激烈的 Java 后端开发领域,Spring Boot 2 凭借其卓越的特性,为开发者们开辟了一条高效、便捷的开发之路。如果你渴望深入学习 Spring Boot 2,以下这份精心…...
知识追踪模型DKT,DLKT详解及代码复现
定义与应用 知识追踪是一种 教育技术领域的重要方法 ,旨在通过分析学生的学习行为和表现,预测其掌握特定知识点的程度。这种方法的核心在于建立能够反映学习者认知状态的动态模型,从而实现对学生学习进度的实时监控和个性化指导。 DKT模型 DKT模型 是最早的知识追踪模型之…...
一类特殊积分的计算
一类特殊积分的计算 前言一、第一个引理二、第二个引理三、积分的计算后记 前言 今天讨论的这类积分是十分有趣的,在 Mathematics Stack Exchange 看见后,便打算在此将其中的计算过程完善一下。在本篇文章中,我们重点考虑求解如下积分&#…...
VScode 配置 C语言环境
遇到的问题集合 mingw官方下载网站(https://sourceforge.net/projects/mingw-w64/files/)更新之后,与网上大多数教程上写的界面不同了。 网上大多数教程让下载这个: 但是现在找不到这个文件。 写hello.c文件时,报错&…...
spring mvc源码学习笔记之十一
pom.xml 内容如下 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/P…...
小结:路由器和交换机的指令对比
路由器和交换机的指令有一定的相似性,但也有明显的区别。以下是两者指令的对比和主要差异: 相似之处 基本操作 两者都支持类似的基本管理命令,比如: 进入系统视图:system-view查看当前配置:display current…...
ffmpeg7.0 aac转pcm
#pragma once #define __STDC_CONSTANT_MACROS #define _CRT_SECURE_NO_WARNINGSextern "C" { #include "libavcodec/avcodec.h" }//缓冲区大小(缓存5帧数据) #define AUDIO_INBUF_SIZE 40960 /*name depthu8 8s16 …...
C#读取本地网络配置信息全攻略
一、引言 在当今数字化时代,网络已深度融入我们生活与工作的方方面面。对于软件开发而言,掌握本地计算机的网络配置信息显得尤为关键。想象一下,你正在开发一款网络诊断工具,需要精准定位网络连接问题,此时 IP 地址、…...
解决aerich init -t xx 报错ModuleNotFoundError: No module named ‘tomli_w‘
今天在学习fastapi的时候,发现一款数据库迁移工具,通过这个工具可以根据模型类来对数据库做出改变。 随跟着学: 在执行 aerich init -t settings.TORTOISE_ORM的时候, 彼其娘之。。 报了一些错误: Traceback (most recent ca…...
python检测gitlab中某个标签在一个月内添加和移除了多少次
可以通过 Python 脚本和 GitLab API 检测一个标签在一个月内被添加和移除的次数。以下是实现的步骤和示例代码: 步骤 获取 GitLab API 访问令牌:在 GitLab 中生成一个 Personal Access Token。设置时间范围:确定一个月的时间范围。调用 Git…...
学习模板之一
学习目标: 提示:这里可以添加学习目标 例如: 一周掌握 Java 入门知识 学习内容: 提示:这里可以添加要学的内容 例如: 搭建 Java 开发环境掌握 Java 基本语法掌握条件语句掌握循环语句 学习时间&#x…...
2025-微服务—SpringCloud-1~3
2025-微服务—SpringCloud 第一章、从Boot和Cloud版本选型开始说起1、Springboot版本2、Springcloud版本3、Springcloud Alibaba4、本次讲解定稿版 第二章 关于Cloud各种组件的停更/升级/替换1、微服务介绍2、SpringCloud是什么?能干吗?产生背景…...
centos7.6 安装nginx 1.21.3与配置ssl
1 安装依赖 yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel2 下载Nginx wget http://nginx.org/download/nginx-1.21.3.tar.gz3 安装目录 mkdir -p /data/apps/nginx4 安装 4.1 创建用户 创建用户nginx使用的nginx用户。 #添加www组 # groupa…...
PL/SQL语言的数据库交互
PL/SQL语言的数据库交互 引言 在当今的信息化时代,数据库管理系统(DBMS)在各行各业中扮演着至关重要的角色。为了高效地与数据库进行交互,许多程序员、数据库管理员和系统分析师选择使用PL/SQL(Procedural Language/…...
ARP欺骗
文章目录 ARP协议ARP欺骗原理断网攻击ARP欺骗(不断网) ARP协议 在局域网中,网络传输的是帧,帧里面有目标主机的MAC地址。arp协议负责将IP地址解析成对应的MAC地址 ARP欺骗原理 即通过伪造IP地址和MAC地址实现ARP欺骗,能够在网络中产生大量…...
Genymotion配套VirtualBox所在地址
在 Genymotion打开虚拟机前需要先打开VirtualBox中的虚拟机 C:\Program Files\Oracle\VirtualBox\VirtualBox.exe 再开启genymotion中的虚拟机开关...
TPS61022 PFM的机制以及TPS61xxx转换器的PFM与PWM之间的负载阈值
引言 TI 的大多数 TPS61xxx 低压升压转换器都配备了 PSM(省电模式),以帮助提高轻负载效率。但是,当它处于重负载状态时,输出纹波通常会高于 PWM。此外,PSM 和 PWM 之间的负载电流阈值不会直观地写入数据表中…...
1. npm 常用命令详解
npm 常用命令详解 npm(Node Package Manager)是 Node.js 的包管理工具,用于安装和管理 Node.js 应用中的依赖库。下面是 npm 的一些常用命令及其详细解释和示例代码。 镜像源 # 查询当前使用的镜像源 npm get registry# 设置为淘宝镜像源 …...
黑马linux入门笔记(01)初始Linux Linux基础命令 用户和权限 实用操作
B站 黑马程序员 的视频 BV1n84y1i7td 黑马程序员新版Linux零基础快速入门到精通,全涵盖linux系统知识、常用软件环境部署、Shell脚本、云平台实践、大数据集群项目实战等 增强自控力 冥想慢呼吸绿色锻炼充分休息减少决策次数优先做重要的事情(早晨)融入强自控群控…...
Markdown中甘特图的使用
Markdown中甘特图的使用 1. 前言2. 语法详解2.1 甘特图语法 3. 使用场景及实例4. 小结5. 其他文章快来试试吧🖊️ Markdown中甘特图的使用 👈点击这里也可查看 1. 前言 Markdown 的原生语法不支持绘制图形,但通过扩展模块,我们可…...
Django创建数据表、模型、ORM操作
1、创建项目 django-admin startproject PersonInfosProject 2、创建项目应用,进入PersonInfosProject文件夹,新建index应用,使用命令 cd PersonInfosProject python manage.py startapp 新建完成之后的目录结构 3、新建数据模型…...
No. 31 笔记 | Web安全-SQL手工注入技术学习 Part 2
一、研究背景 背景介绍 SQL注入是一种常见且高危的Web安全漏洞。攻击者可以通过构造恶意SQL查询语句来绕过验证机制,执行未授权操作,如获取敏感信息、篡改数据库内容甚至控制服务器。 研究内容 本笔记探讨以下数据库的手工注入技术: MySQLAc…...
Spring Boot中的扫描注解如何使用
在 Spring Boot 中,扫描注解是指通过注解来告诉 Spring 框架应该扫描哪些包、哪些类或哪些特定的组件,并将其作为 Spring 容器中的 bean 进行管理。Spring Boot 主要通过以下几种注解来实现自动扫描: ComponentScanSpringBootApplicationCom…...