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来描边路径,而在我们描边的时候,如果不注意的话可能会陷入一些绘制的问题。假如此时我们要绘制一条线,我们可以分别来看下使用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-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节点基本一致,只是我们绑定事件和绘制都是直接由类控制而已,而在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的交互设计、参考线能力的实现、富文本的绘制方案等等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/58736.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

iQOO手机怎样将屏幕投射到MacBook?可以同步音频吗?

众所周知&#xff0c;苹果品牌的设备自己有AirPlay的投屏功能&#xff0c;iPhone要投屏到MacBook只要连接同一网络&#xff0c;然后开启AirPlay就可以投屏。但其他品牌的手机没有AirPlay&#xff0c;怎么将手机屏幕投射到MacBook呢&#xff1f; 安卓系统的手机可以使用无线投屏…

机器人和智能的进化速度远超预期-ROS-AI-

危机 通常&#xff0c;有危险也有机遇才称之为危机。 从2020年启动转型自救&#xff0c;到2021年发现危险迫在眉睫&#xff0c;直到2024年也没有找到自己满意的出路。 共识 中产阶级知识分子共有的特性和一致的推断。 200年前的推断&#xff0c;在如今得到了验证。 机器人…

Idea、VS Code 如何安装Fitten Code插件使用

博主主页:【南鸢1.0】 本文专栏&#xff1a;JAVA 目录 ​编辑 简介 所用工具 1、Idea如何安装插件 1.idea下载插件 2.需要从外部下载然后在安装&#xff0c; 2、VS Code如何安装插件 总结 简介 Fitten Code是由非十大模型驱动的AI编程助手&#xff0c;它可以自动生成代…

助力抑郁症初筛!上海交大团队构建Agent心理诊所,论文一作在线展示demo,分享技术亮点

「我有动手打她&#xff0c;甚至好几次掐着她脖子把她按到墙角。每次动完手&#xff0c;我都会后悔&#xff0c;我为什么要动手&#xff0c;我为什么控制不住自己&#xff0c;我是不是就是一个混蛋、一个疯子、一个十恶不赦的人&#xff0c;但我真的不知道该怎么办。」这是 18 …

【优选算法篇】前缀之美,后缀之韵:于数列深处追寻算法的动与静

文章目录 C 前缀和详解&#xff1a;进阶题解与思维分析前言第二章&#xff1a;前缀和进阶应用2.1 和为 k 的子数组&#xff08;medium&#xff09;解法一&#xff08;前缀和 哈希表&#xff09;示例分析C代码实现易错点提示代码解读 2.2 和可被 K 整除的子数组&#xff08;med…

「Mac畅玩鸿蒙与硬件14」鸿蒙UI组件篇4 - Toggle 和 Checkbox 组件

在鸿蒙开发中,Toggle 和 Checkbox 是常用的交互组件,分别用于实现开关切换和多项选择。Toggle 提供多种类型以适应不同场景,而 Checkbox 支持自定义样式及事件回调。本篇将详细介绍这两个组件的基本用法,并通过实战展示它们的组合应用。 关键词 Toggle 组件Checkbox 组件开…

Unity计算二维向量夹角余弦值和正弦值的优化方法参考

如果不考虑优化问题&#xff0c;计算两个向量的余弦值或者正弦值可以直接使用类似的方法&#xff1a; [SerializeField] Vector2 v1, v2;void Start() {float valCos Mathf.Acos(Vector2.SignedAngle(v1, v2));float valSin Mathf.Asin(Vector2.SignedAngle(v1, v2)); } 但是…

编写一个README.md

一、README 在下载github上的代码的时候&#xff0c;通常会有一个README.md文件让你了解该仓库的做了什么&#xff0c;他如何安装等内容。写好README.md是和他人交流的重要环节。 二、README.md的语法逻辑 a、预览模式 在Vscode中编辑README.md的时候可以打开预览模式&#xf…

Android简单控件实现简易计算器

学了一些Android的简单控件&#xff0c;用这些布局和控件&#xff0c;设计并实现一个简单计算器。 计算器的界面分为两大部分&#xff0c;第一部分是上方的计算表达式&#xff0c;既包括用户的按键输入&#xff0c;也包括计算结果 数字&#xff1b;第二部分是下方的各个按键&a…

内容安全与系统构建加速,助力解决生成式AI时代的双重挑战

内容安全与系统构建加速&#xff0c;助力解决生成式AI时代的双重挑战 0. 前言1. PRCV 20241.1 大会简介1.2 生成式 Al 时代的内容安全与系统构建加速 2. 生成式 AI2.1 生成模型2.2 生成模型与判别模型的区别2.3 生成模型的发展 3. GAI 内容安全3.1 GAI 时代内容安全挑战3.2 图像…

ZeroNL2SQL:零样本 NL2SQL

发布于&#xff1a;2024 年 10 月 30 日 星期三 #RAG #NL2SQL # Zero-Shot 自然语言到 SQL&#xff08;NL2SQL&#xff09;的转换是一个重要的研究领域&#xff0c;它允许非技术用户轻松访问和分析数据&#xff0c;在商业智能、数据分析等领域具有广泛的应用前景。然而&#x…

前端部署指南:手把手教你部署 Vue 项目

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Vue篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来Vue篇专栏内容:Vue-部署项目 前言 嗨喽伙伴们大家好&#xff0c;我是依旧青山。作为一名前端开发工程师&#xff…

Vivo开奖了,劝退价。。

vivo 也开奖了&#xff0c;不过有小伙伴反馈是个劝退价&#xff0c;甚至不如隔壁的 oppo&#xff0c;要说这两家也是渊源颇深&#xff0c;一家是绿厂&#xff0c;一家是蓝厂&#xff0c;高管也都是早期步步高出来的。 给大家盘一下开奖的信息&#xff0c;方便大家横向做个对比&…

WPF+MVVM案例实战(八)- 自定义开关控件封装实现

文章目录 1、案例运行效果2、项目准备2、功能实现1、控件模板实现2、控件封装1、目录与文件创建2、各文件功能实现3、开关界面与主窗体菜单实现1、开关界面实现2、主窗体菜单实现4、源代码获取1、案例运行效果 2、项目准备 打开项目 Wpf_Examples,新建ToggleButtonWindow.xma…

【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper+代码——交叉注意力(Cross-Attention)

【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper代码——交叉注意力&#xff08;Cross-Attention&#xff09; 【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper代码——交叉注意力&#xff08;Cross-Attention&#xff09; 文章目录 【…

安宝特案例 | AR技术在院外心脏骤停急救中的革命性应用

00 案例背景 在院外心脏骤停 (OHCA) 的突发救援中&#xff0c;时间与效率直接决定着患者的生命。传统急救模式下&#xff0c;急救人员常通过视频或电话与医院医生进行沟通&#xff0c;以描述患者状况并依照指令行动。然而&#xff0c;这种信息传递方式往往因信息不完整或传递延…

Java如何实现PDF转高质量图片

大家好&#xff0c;我是 V 哥。在Java中&#xff0c;将PDF文件转换为高质量的图片可以使用不同的库&#xff0c;其中最常用的库之一是 Apache PDFBox。通过该库&#xff0c;你可以读取PDF文件&#xff0c;并将每一页转换为图像文件。为了提高图像的质量&#xff0c;你可以指定分…

论文略读:OneChart: Purify the Chart Structural Extraction via One Auxiliary Token

2024 旷视的work 图表解析模型 1 背景 对于之前的视觉语言模型&#xff0c;论文认为其有两点不足需要改进&#xff1a; 需要充分训练一个真正会看 chart 的 vision encoder单纯对文本输出算交叉熵损失&#xff0c;并不是最优的&#xff08;如上图所示&#xff0c;当ground-tr…

STM32CubeMX学习(三) SPI+DMA通信

STM32CubeMX学习&#xff08;三&#xff09; SPIDMA通信 一、简介二、新建STM32CubeMX项目并使用外部时钟三、SPI3配置四、相关代码五、测试 一、简介 本文将基于STM32F103RCT芯片介绍如何在STM32CubeMXKEIL5开发环境下进行SPIDMA通信。 操作系统&#xff1a;WIN10 x64硬件电…

iOS静态库(.a)及资源文件的生成与使用详解(OC版本)

引言 iOS静态库&#xff08;.a&#xff09;及资源文件的生成与使用详解&#xff08;Swift版本&#xff09;_xcode 合并 .a文件-CSDN博客 在前面的博客中我们已经介绍了关于iOS静态库的生成步骤以及关于资源文件的处理&#xff0c;在本篇博客中我们将会以Objective-C为基础语言…