Canvas简历编辑器-选中绘制与拖拽多选交互方案

在之前我们聊了聊如何基于Canvas与基本事件组合实现了轻量级DOM,并且在此基础上实现了如何进行管理事件以及多层级渲染的能力设计。那么此时我们就依然在轻量级DOM的基础上,关注于实现选中绘制与拖拽多选交互设计。

关于Canvas简历编辑器项目的相关文章:

选中绘制

我们先来聊一聊最基本的节点点击选中以及拖拽的交互,而在聊具体的代码实现之前,我们先来看一下对于图形的绘制问题。在Canvas中我们绘制路径的话,我们可以通过fill来填充路径,也可以通过stroke来描边路径,而在我们描边的时候,如果不注意的话可能会陷入一些绘制的问题。假如此时我们要绘制一条线,我们可以分别来看下使用strokefill的绘制方法实现,此时如果在高清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,这点可以通过再画一个1pxPath来对比。

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的绘制,在Firefoxinside 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的方式绘制的,但是这里需要讨论一下clipfillRule-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-stroke
const 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节点基本一致,只是我们绑定事件和绘制都是直接由类控制而已,而在drawingMaskShape.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>类型,以及Selectioncompose方法也是支持多选的,那么我们只需要在选中节点的时候,将节点的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来手动处理这个过程。当然思路是比较简单的,我们只需要维护一个booleanid标识来确定当前节点是否被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的交互设计、参考线能力的实现、富文本的绘制方案等等。

每日一题

参考

Canvas简历编辑器-选中绘制与拖拽多选交互方案的更多相关文章

  1. [WPF][ListBox]鼠标拖拽多选,(Shift Key、Ctrl Key多选有效)(转)

    <ListBox Name="listBox" SelectionMode="Extended"> <ListBox.Resources> ...

  2. 使用 Jquery-UI 实现一次拖拽多个选中的元素操作

    项目需要,实现一个拖放操作,要求每次可以拖拽选中的多个元素,释放到目标容器后可排序.考虑了一下,觉得jquery-ui比较合适,毕竟它提供了项目需要的交互性事件机制.拖拽.释放.排序.选择等效果.而在 ...

  3. 通过layout实现可拖拽自动排序的UICollectionView

    文/CenturyGuo(简书作者)原文链接:http://www.jianshu.com/p/8d1bf1838882著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. Translat ...

  4. Android Launcher拖拽事件详解【android4.0--Launcher系列二】

    AndroidICS4.0版本的launcher拖 拽的流程,基本和2.3的相似.就是比2.3写的封装的接口多了一些,比如删除类的写法就多了个类.等等.4.0的改变有一些,但是不是特别大.这个月一 直 ...

  5. 理解事件捕获。在限制范围内拖拽div+吸附+事件捕获

    一.实现的效果是在限制范围内拖拽div+吸附+事件捕获. 这里需要理解的是事件捕获,这个事件捕获也是为了兼容div在拖拽过程中,文本不被选中这个问题. 如此良辰美景,拖拽也可以很洒脱哈.先看看图, 二 ...

  6. Blend4精选案例图解教程(三):一键拖拽

    原文:Blend4精选案例图解教程(三):一键拖拽 拖拽效果,常规实现方法是定义MoveLeftDwon.MoveLeftUp.MouseMove事件,在Blend的世界里,实现对象的拖拽,可以不写一 ...

  7. Android4.0 Launcher拖拽原理分析

    在Android4.0源码自带的Launcher中,拖拽是由DragController进行控制的. 1) 先来看看类之间的继承关系 2)再来看看Launcher拖拽流程的时序图   1.基本流程: ...

  8. JS——拖拽盒子

    注意事项: 1.opacity是全部元素变透明,rgba只是背景色变透明 2.先是注册鼠标按下的事件,此时就需要记录鼠标在盒子中的坐标 3.再在鼠标按下事件中注册鼠标移动事件,此时鼠标的坐标是不断变化 ...

  9. div拖拽的问题

    今天看到一篇写的很好的文章,摘抄如下: 思路 父盒子相对定位,子元素,也就是被拖拽的元素绝对定位 当鼠标在子元素中按下时,绑定鼠标移动事件,根据鼠标位置改变元素位置 设置鼠标当前位置(offsetX, ...

  10. WPF MultiSelect模式下ListBox 实现多个ListBoxItem拖拽

    WPF 的ListBox不支持很多常见的用户习惯,如在Explorer中用鼠标可以选择多项Item,并且点击已经选择的Item,按住鼠标左键可以将所有已选择Item拖拽到指定的位置.本文简单的实现了这 ...

随机推荐

  1. .NET 8.0 开源在线考试系统(支持移动端)

    前言 推荐一款基于.NET 8.0 免费开源跨平台在线考试系统,系统不仅支持桌面端,还特别优化了移动端的用户体验. 通过本系统可以轻松搭建自己的在线考试平台,实现随时随地的测试与评估. 本文将详细介绍 ...

  2. 量子线路设计:减少CNOT和T门的意义。

    在量子线路的设计中,我们往往希望减少线路中的CNOT门和T门的数目,原因如下: 一般文献宣称减少T门的数量是为了更高效地执行容错量子计算(fault-tolerant quantum computat ...

  3. Wgpu图文详解(03)缓冲区Buffer

    在上一篇文章中,我们介绍了Wgpu中的渲染管线与着色器的概念以及基本用法.相信读者还记得,我们在渲染一个三角形的时候,使用了三角形的三个顶点的索引作为了顶点着色器的输入,并根据索引值计算了三个几何顶点 ...

  4. ARC121E Directed Tree

    ARC121E Directed Tree 有意思的容斥加树 dp. 思路 \(a_i\) 可以是除去 \(i\) 祖先之外的所有点,考虑 \(a_i\) 的逆排列. 每一个 \(i\) 在正排列里都 ...

  5. TSCTF-J2024 密码向WP(5/8)

    ezRSA part 1 #part1 p = getPrime(512) q = getPrime(512) n = p * q phi = (p-1) * (q-1) d = getPrime(2 ...

  6. SQLSERVER 2019数据库(可疑) 数据修复案例

    2019 数据库 , 在正常使用过程中 , 服务器突然断电 , 重新启动服务器后 , 数据库变为(可疑). [数据恢复故障分析] 由于数据库在正常读写操作过 , 服务器突然断电 , 导致数据库无法把所 ...

  7. VulnHub-Sick0s1.1解法二shellshock漏洞

    免责声明 本博客提供的所有信息仅供学习和研究目的,旨在提高读者的网络安全意识和技术能力.请在合法合规的前提下使用本文中提供的任何技术.方法或工具.如果您选择使用本博客中的任何信息进行非法活动,您将独自 ...

  8. 浅析Flie类getAbsolutePath()方法

    开发中,常常需要上传文件,并将文件存于远程服务器(如minio)或者本地,当存于本地时对存储路径的指定是常见的问题. 当然,你可以在本地写死静态资源路径,如"D:\static\fileUp ...

  9. spring 跟 诠释及区别

    <context:annotation-config> 和 <context:component-scan>的区别 Difference between <context ...

  10. python之pyexecjs

    pyexecjs是一个用Python来执行JavaScript代码的工具库,该库支持多种JavaScript运行时环境,如Node.js.PhantomJS.SlimerJS等,允许开发者在Pytho ...