当前位置: 首页 > news >正文

基于 Konva 实现Web PPT 编辑器(二)

动画系统

        为了实现演示中复杂的动画效果,使用 Animation 类统一管理;切换动画通过 css animation 实现,并且是应用在 konvajs-content 上,动画则通过 gsap 实现,应用在 Konva.Node 上,实现思路如下:

/*** 动画相关实现*  1. 切换动画通过 css animation 实现*      1.1 应用在 konvajs-content 上*      1.2 对于 animation 来说,动画就是个类名,通过 style 来设置动画*      1.3 动画结束后,需要移除动画类名,通过 style.animation = '' 实现*      1.4 动画应用是通过 Layer.setAttrs 记录状态,在预览时,动态添加即可**  2. 元素动画则通过 gsap 库实现,应用在 Konva.Node 上*      2.1 这里的动画其实是一个时间线,从某个节点状态运行到另一个节点状态*      2.2 官网: https://gsap.com/docs/v3/*      2.3 官网上提供了很多动画,这里只使用 gsap 的 timeline 来实现动画*      2.4 同样需要记录 动画状态,在预览时,通过动画状态来应用动画;**  3. 切换应用动画的流程:*      3.1 调用 setAnimation 给当前layer标记动画属性;*      3.2 在预览时 调用 applyAnimation 应用动画;*      3.3 动画结束后,调用clearTimeOut清除动画*      3.4 如果是应用全部动画的话,通过 global 标识,每次新增幻灯片,都判断有没有全局动画,如果有,则应用全局动画*/

        应用到实例上效果如下:

        经过研究探讨,发现原生的 Konva.Tween 难以满足应用中复杂的强调、入场、退场动画,对节点的属性控制、播放节点控制能力较弱,而Konva.Animation 则是通过request Animation实现的动画,对时间的控制不够精确,整个属性过渡的效果需要自行实现,因此,决定采用 gsap 动画实现。gasp 官网​​​​​​​ 具体的API我就不介绍了哈,大家自行前往官网查看,我就举个例子说明项目中的应用:

const timeline = gsap.timeline();
timeline.pause();// 淡入
[KonvaAnimationMap.fadeIn]: () => {const tween = gsap.fromTo(node,{opacity: 0,scaleX: 0,scaleY: 0,x: node.x() + node.width() / 2,y: node.y() + node.height() / 2,},{opacity: 1,scaleX: 1,scaleY: 1,x: node.x(),y: node.y(),onComplete,});timeline.add(tween);return timeline;
}

        这里只是创建了补间动画及时间轴,并且默认是暂停状态,因为幻灯片中的元素动画应用是有’单击时‘、’上个动画同时‘、’上个动画结束‘,因此,将动画对象返回,根据合适的时机进行执行是最合适的(这里仅是对动画进行应用展示哈,后续的如何在预览时,根据时机播放,我们放在预览中说明

对齐辅助线

        我们参考官网的实现案例:How to snap canvas shape to other shapes,通过监听 dragmove dragend 来实现对齐辅助线:

        大致思路就是取拖动的六条线坐标,与当前画布节点进行比较,看是否有满足条件的节点,绘制直线即可,konva中的宽高在缩放后是不会改变的,因此,需要取其缩放比例,手动计算实际的宽高:

  // 元素真实的宽高与缩放比例也有关系const node = e.target;const scaleX = node.scaleX() || 1;const scaleY = node.scaleY() || 1;const x = node.x();const y = node.y();const width = node.width() * scaleX;const height = node.height() * scaleY;

 

        而磁吸效果实现的核心,就是在一定范围内,自动修正节点位置:

单选、多选、框选

        单选通过监听 group.click 事件实现,多选就是用户按住 Ctrl Shift 键的时候,不清空之前的形变节点即可。

 // 左键选中元素if (e.evt.button === 0) {const layer = draw.getLayer();if (!layer) return;const group = e.target.parent!;const { shiftKey, ctrlKey } = e.evt;// 是否多选if (shiftKey || ctrlKey) {// 标记激活状态group.setAttr("selected", true);groupMultiple(e, draw);return;}// 清空所有的形变 包括取消选中节点属性draw.clearTransformer({ selected: true });// 标记激活状态group.setAttr("selected", true);// 为当前节点创建形变节点groupTransformer(draw, group);}

通过创建的 Konva.Transformer ,会自动识别宽高尺寸,旋转角度等,不需要我们处理:

 

        框选实现,则通过监听鼠标 mousedown mousemove mouseup 事件,绘制合适的选区,计算位置关系,得出被框选的元素:

// mousedown//   记录初始位置
const ssx = e.evt.offsetX;
const ssy = e.evt.offsetY;
stage.setAttrs({selecting: true,ssx, // stage start xssy, // stage start y
});
const selectBoxCss = ".konva-root-container-frame_selected";
const selectBox = root.querySelector(selectBoxCss) as HTMLDivElement;
selectBox.style.left = ssx + "px";
selectBox.style.top = ssy + "px";
// mousemove// 解析位置
const { offsetX, offsetY } = e.evt;
//   计算偏移量
const dx = offsetX - ssx;
const dy = offsetY - ssy;const selectBoxCss = ".konva-root-container-frame_selected";
const selectBox = root.querySelector(selectBoxCss) as HTMLDivElement;if (dx > 0) selectBox.style.width = dx + "px";
else {// 如果小于 0 则反向框选,需要修改 left widthselectBox.style.left = offsetX + "px";selectBox.style.width = Math.abs(dx) + "px";
}if (dy > 0) selectBox.style.height = dy + "px";
else {selectBox.style.top = offsetY + "px";selectBox.style.height = Math.abs(dy) + "px";
}
// mouseup 
stage.setAttrs({selecting: null,ssx: null,ssy: null,
});const selectBoxCss = ".konva-root-container-frame_selected";
const selectBox = root.querySelector(selectBoxCss) as HTMLDivElement;
selectBox.style.top = "0px";
selectBox.style.left = "0px";
selectBox.style.width = "0";
selectBox.style.height = "0";

         框选中判断节点是否被选中实现如下,则直接使用 Konva.Util.haveIntersection 工具函数即可:

/*** 判断节点是否在范围内* @param node 传入需要判断的Konva节点* @param range 传入范围 {x,y,width,height}*/
export function haveInterSection(node: Konva.Node,range: { x: number; y: number; width: number; height: number }
) {return Konva.Util.haveIntersection({ x: node.x(), y: node.y(), width: node.width(), height: node.height() },range);
}// mouseup 判断节点是否选中layer.find(".konva-base-group").filter((node) => {const range = { x: left, y: top, width, height };const isIntersection = haveInterSection(node, range);if (isIntersection) {// 给节点添加selected状态node.setAttrs({ selected: true });// 选中节点groupTransformer(draw, node);}});

拓展konva类型

图片

        图片的处理上篇已经讲述过实现原理了哈,这里不再赘述;

媒体资源

        媒体资源实现的方案是Konva.Image 实现的,因为Image的image属性支持类型如下,可参考官网案例:How to display video on Canvas:

表格

        表格实现是基于Konva自定义图形,通过定义形状绘制方法与事件响应范围来实现:

// table - 表格public Table(payload: IKonvaTableConfig & Konva.ShapeConfig) {const group = this.getGroup(payload);const data = [["Name", "Age", "Gender"],["Alice", 25, "Female"],["Bob", 30, "Male"],["Charlie", 35, "Male"],["Diana", 28, "Female"],];const columnWidths = [100, 100, 100]; // 列宽const rowHeight = 30; // 行高const width = 300;const height = 150;// 自定义表格var table = new Konva.Shape({...payload,width,height,fill: "#fff",stroke: "#000",/*** 定义绘制方法 数据属性放置在 attrs 上,调用 render 即可重新渲染* @param ctx* @param shape*/sceneFunc: (ctx, shape) => {ctx.beginPath();for (let i = 0; i < data.length; i++) {for (let j = 0; j < data[i].length; j++) {const cellX = 100 * j;const cellY = i * rowHeight;const cellWidth = columnWidths[j];const cellHeight = rowHeight;// 绘制单元格边框 - 奇偶行实现斑马纹ctx.fillStyle = i % 2 ? "#fafafa" : "#fff";if (i === 0) ctx.fillStyle = "rgba(0,0,0,0.2)";ctx.fillRect(cellX, cellY, cellWidth, cellHeight);// 是否启用边框ctx.strokeRect(cellX, cellY, cellWidth, cellHeight);// 绘制单元格内容;ctx.fillStyle = "#000"; // 设置文本颜色为黑色ctx.font = "14px Arial"; // 设置字体样式ctx.fillText(data[i][j].toString(), cellX + 5, cellY + 20); // 绘制文本内容}}ctx.fillStrokeShape(shape); // (!) Konva specific method, it is very important},// 绘制事件响应区域hitFunc: function (ctx, shape) {ctx.beginPath();ctx.rect(0, 0, width, height);ctx.closePath();ctx.fillStrokeShape(shape);},});// 处理事件 - 双击进行数据编辑group.on("dblclick", () => {console.log("table dblclick");});group.add(table);this.overwriteGraph(group);return group;}

表格的更新,通过自定义属性,记录初始数据,动态生成table,确认后再转为 shape 的属性:

统计图

        本应用使用的统计图较为简单,就不引入其他库了,手动绘制实现,还是通过 Konva.Shaph 自定义图形哈,具体的绘制过程就不展示了,就是基础的canvas绘图能力。

文本

        文本这里的处理有一个难点哈,也是前期的一个坑,现在来填一下,在我们最初设计的时候,文本是跟随group一起移动的对吧,但是缩放也会跟着缩放!如下:

        但是,WPS 的效果可不是这样的哦:

        因此,实现文本自适应是最重要的!网上查阅了相关资料,都没有好的办法处理,都说需要动态设置文本的缩放比例,太过复杂! 既然是缩放过程中文本被迫缩放了,那么,不让他跟随缩放不就完了嘛

        大家要理解这里面的层级关系哈:外层 Group 负责draggable,Rect 负责缩放,文本才能自适应.

公式

        公式的实现比较难,大家有什么好的库推荐下,这里使用的是 mathlive,很多的插件,都是提供 latex 编辑器,然后提供预览功能,其实这样是非常难用的,我们无法直观的查看及编辑公式内容;而 mathlive 则能够直接提供直观的公式编辑页面:

mathLive功能演示:

        但是目前没有比较完善的文章将mathlive的使用、导出等功能进行讲解,只能自我摸索,边看文档边实践;同时,本文也不重点关注这块,如果感兴趣,后期可以出一篇文章讲解mathlive 的使用哈,我只重点讲述遇到的难点:

隐藏菜单、键盘:

        我是不使用默认的功能哈,公式的插入手动实现,当然,大家也可以直接使用默认功能。

/** 隐藏公示栏的菜单及键盘按钮 **/
@media not (pointer: coarse) {math-field::part(virtual-keyboard-toggle) {display: none;}math-field::part(menu-toggle) {display: none;}
}

引入样式文件:

转成 html:

        这里是为了转图片做准备的哈

 const html= convertLatexToMarkup(mfe.value);

转图片:

        这里使用的库是mathlive官网使用的:html-to-image :

toPng(div).then(function (dataUrl) {/* do something */console.log(dataUrl);});

插入公式:

        官网给我们提供了 command 命令,可以执行一些常用的操作,比例插入,撤销重做等。同时,还提供了所有 LaTex command,将常见的公式也封装了,只需要执行响应的命令,就可以插入公式:

        比如现在要插入(根号3):

mfe.executeCommand("insert", "\\sqrt{3}");

 

注意:一定是 \ 转义一次哈,不然得到的可就不是 根号3 而是字符串了

 

效果如下:

历史记录

        历史记录的实现,大家可以参考官网给出的案例,在大型应用中,如果直接使用 toJSON 保存历史记录的话,会丢失很多关键信息,例如事件、过滤器等,因此我们不适用json保存。

        看了官网的案例,是通过保存整个stage,然后撤销重做时,重新进行事件绑定,这种方案在我们的应用中,也是不可取的。我们封装了group ,为group 添加了这么多事件,如果都重写,那工作量可多了。

       【解决方案】:在图层管理中,缓存的是layer数据,layer是可以直接进行赋值操作,事件也不会丢失,因此,采用layer进行历史记录缓存。

  // undo 栈private undoStack: Konva.Layer[] = [];// redo 栈private redoStack: Konva.Layer[] = [];// 最大历史记录数private maxRecordCount = maxRecordCount;
  /** 添加记录 */public patchHistory() {const layer = this.draw.getLayer()?.clone()!;// 当前图层的JSON串 - 不直接使用 toJSON(),避免后续修改const layerJson = JSON.stringify(layer?.toObject());// 被添加图层与最后缓存的记录是否一致const lastLayer = this.undoStack[this.undoStack.length - 1];const lastLayerJson = JSON.stringify(lastLayer?.toObject());if (layerJson === lastLayerJson) return console.error("记录已存在");layer.find("Transformer").forEach((tr) => tr.destroy());// 不然直接添加到 undoStackconsole.log("patchHistory");this.undoStack.push(layer);// 如果记录数大于 maxRecordCount,则删除最前的记录while (this.undoStack.length > this.maxRecordCount) this.undoStack.shift();}
  // 撤销动作public undo() {// 需要保留背景图层,因此 撤销栈不能为0if (this.undoStack.length <= 1) return console.log("不可撤销");// 获取当前图层,放到 redoStack,并删除 undoStackconst layer = this.undoStack.pop()!;this.redoStack.push(layer);// 重新渲染图层this.render();}
  // 重做动作public redo() {if (this.redoStack.length <= 0) return console.log("不可重做");// 获取当前图层,放到 undoStack,并删除 redoStackconst layer = this.redoStack.pop()!.clone();this.undoStack.push(layer.clone());// 重新渲染图层this.render();}
  /** 重新渲染图层 */private render() {// 取出上一次的图层,并添加到当前图层const lastLayer = this.undoStack[this.undoStack.length - 1];const layerClone = lastLayer.clone(); // 这里一定要 clone 避免图层引用导致的历史记录异常问题// 进行图层更新this.draw.clearLayer(); // 删除layerthis.draw.setLayer(layerClone); // 修正 layerthis.draw.getStage().add(layerClone);this.draw.render({ patchHistory: false });}

总结

        经过这么多天的沉淀,现在已经大体实现了幻灯片新增、删除,支持元素-矩形、箭头、文字、统计图、表格、图片多媒体、公式类型的新增删除编辑操作;实现了基本的动画系统、历史记录管理器。

        下一步将是对预览系统进行完善,并考虑预览中的动画播放问题,同时,还将提供设计功能,为整个幻灯片提供基础的配色方案及预设功能;将完善相关功能,优化用户体验。

相关文章:

基于 Konva 实现Web PPT 编辑器(二)

动画系统 为了实现演示中复杂的动画效果&#xff0c;使用 Animation 类统一管理&#xff1b;切换动画通过 css animation 实现&#xff0c;并且是应用在 konvajs-content 上&#xff0c;动画则通过 gsap 实现&#xff0c;应用在 Konva.Node 上&#xff0c;实现思路如下&#xf…...

【开源免费】基于SpringBoot+Vue.JS在线竞拍系统(JAVA毕业设计)

本文项目编号 T 013 &#xff0c;文末自助获取源码 \color{red}{T013&#xff0c;文末自助获取源码} T013&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…...

Qt TabWidget添加多个窗口,实现分页窗体布局

Qt TabWidget添加多个窗口窗体&#xff0c;可关闭与打开 点击按钮可判断是否打开&#xff0c;避免重复打开 使用Qt中的TabWidget组件创建一个简单的分页窗体布局。点击按钮时&#xff0c;会新增一个窗体并添加到TabWidget中。每个子窗体能动态获取父窗体指针以进行操作 分别…...

HarmonyOS开发实战( Beta5版)合理使用动画丢帧规范实践

本文列举了部分用于优化动画时延的正反案例&#xff0c;帮助开发者在遇到相似场景时进行优化&#xff0c;解决构建页面动画时遇到动画时延较长的问题。 减少动画丢帧 在播放动画或者生成动画时&#xff0c;画面产生停滞而导致帧率过低的现象&#xff0c;称为动画丢帧。 播放…...

基于BiLSTM-CRF的医学命名实体识别研究(下)模型构建

一.生成映射字典 接下来需要将每个汉字、边界、拼音、偏旁部首等映射成向量。所以&#xff0c;我们首先需要来构造字典&#xff0c;统计多少个不同的字、边界、拼音、偏旁部首等&#xff0c;然后再构建模型将不同的汉字、拼音等映射成不同的向量。 在prepare_data.py中自定义…...

5.sklearn-朴素贝叶斯算法、决策树、随机森林

文章目录 环境配置&#xff08;必看&#xff09;头文件引用1.朴素贝叶斯算法代码运行结果优缺点 2.决策树代码运行结果决策树可视化图片优缺点 3.随机森林代码RandomForestClassifier()运行结果总结 本章学习资源 环境配置&#xff08;必看&#xff09; Anaconda-创建虚拟环境…...

VMWARE VCENTER6.7 VCSA通过Web5480进行版本升级

VCENTER当前版本如下图 操作前先给VCENTER打一个快照&#xff0c;出问题可以立即回退 1、先下载VCSA镜像&#xff0c;并将VCSA镜像上传至DataStore中&#xff1b; 2、选中VCSA虚拟机&#xff0c;编辑配置 3、挂载新上传的VCSA镜像&#xff0c;一定要勾选“已连接”和“打开电源…...

GIT使用常见问题

如何安装Git&#xff1f; 在Windows操作系统中&#xff0c;可以从Git官方网站&#xff08;https://git-scm.com&#xff09;下载最新的Git安装程序&#xff0c;然后按照提示进行安装。在Mac操作系统中&#xff0c;可以使用Homebrew或者直接从Git官方网站下载安装程序进行安装。…...

内核链表

一、特点 灵活性 内核链表可以连接各种不同类型的数据结构&#xff0c;因为它只包含指向下一个和上一个节点的指针&#xff0c;不依赖特定的数据类型&#xff0c;这使得内核开发者可以根据不同的需求灵活地使用它。你可以将不同类型的结构体通过内核链表连接起来&#xff0c;实…...

行空板上YOLO和Mediapipe视频物体检测的测试

Introduction 经过前面三篇教程帖子&#xff08;yolov8n在行空板上的运行&#xff08;中文&#xff09;&#xff0c;yolov10n在行空板上的运行&#xff08;中文&#xff09;&#xff0c;Mediapipe在行空板上的运行&#xff08;中文&#xff09;&#xff09;的介绍&#xff0c;…...

【Spring Boot 3】【Web】ProblemDetail

【Spring Boot 3】【Web】ProblemDetail 背景介绍开发环境开发步骤及源码工程目录结构总结背景 软件开发是一门实践性科学,对大多数人来说,学习一种新技术不是一开始就去深究其原理,而是先从做出一个可工作的DEMO入手。但在我个人学习和工作经历中,每次学习新技术总是要花…...

市占率最高的显示器件,TFT_LCD的驱动系统设计--Part 1

目录 一、简介 二、TFT-LCD驱动系统概述 &#xff08;一&#xff09;系统概述 &#xff08;二&#xff09;设计要点 二、扫描驱动电路设计 &#xff08;一&#xff09;概述 扫描驱动电路的功能 扫描驱动电路的组成部分 设计挑战 驱动模式 &#xff08;二&#xff09…...

Linux基础 -- 获取CPU负载信息

Linux Kernel 获取当前负载情况 本文档介绍了如何在 Linux 内核中获取系统的负载情况。我们将从用户态程序、内核模块开发等角度展示相关方法。 1. 通过 /proc/loadavg 文件获取负载 /proc/loadavg 文件包含了系统的负载信息&#xff0c;通常包括过去 1 分钟、5 分钟和 15 分…...

Django 中的用户界面 - 创建速度计算器

在 Django 中创建一个用户界面来计算速度&#xff0c;可以通过以下步骤完成。这个速度计算器将允许用户输入距离和时间&#xff0c;计算并显示速度。 一、问题背景 一位 Django 新手希望使用 Django 构建一个用户界面&#xff0c;以便能够计算速度&#xff08;速度 距离/时间…...

spring security 如何解决跨域的

一、什么是 CORS CORS(Cross-Origin Resource Sharing) 是由 W3C制定的一种跨域资源共享技术标准&#xff0c;其目就是为了解决前端的跨域请求。在JavaEE 开发中&#xff0c;最常见的前端跨域请求解决方案是早期的JSONP&#xff0c;但是JSONP 只支持 GET 请求&#xff0c;这是一…...

日志系统前置知识

日志&#xff1a;程序运行过程中所记录的程序运行状态信息。通过这些信息&#xff0c;以便于程序员能够随时根据状态信息&#xff0c;对系统的运行状态进行分析。功能&#xff1a;能够让用户非常简便的进行日志的输出以及控制。 同步写日志 同步日志是指当输出日志时&#xff…...

【Spring Boot 3】【Web】全局异常处理

【Spring Boot 3】【Web】全局异常处理 背景介绍开发环境开发步骤及源码工程目录结构总结背景 软件开发是一门实践性科学,对大多数人来说,学习一种新技术不是一开始就去深究其原理,而是先从做出一个可工作的DEMO入手。但在我个人学习和工作经历中,每次学习新技术总是要花费…...

Dcoker 运行es

1&#xff0c;创建network docker network create my-network 2&#xff0c;docker运行es容器 docker run -d --name es-container --net my-network -p 9200:9200 -p 9300:9300 -e "discovery.typesingle-node" docker.elastic.co/elasticsearch/elasticsearch:7…...

7系列FPGA HR/HP I/O区别

HR High Range I/O with support for I/O voltage from 1.2V to 3.3V. HP High Performance I/O with support for I/O voltage from 1.2V to 1.8V. UG865&#xff1a;Zynq-7000 All Programmable SoC Packaging and Pinout...

sqli-labs靶场通关攻略(五十一到六十关)

sqli-labs-master靶场第五十一关 步骤一&#xff0c;尝试输入?sort1 我们发现这关可以报错注入 步骤二&#xff0c;爆库名 ?sort1 and updatexml(1,concat(0x7e,database(),0x7e),1)-- 步骤三&#xff0c;爆表名 ?sort1 and updatexml(1,concat(0x7e,(select group_conc…...

应用升级/灾备测试时使用guarantee 闪回点迅速回退

1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间&#xff0c; 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点&#xff0c;不需要开启数据库闪回。…...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)

宇树机器人多姿态起立控制强化学习框架论文解析 论文解读&#xff1a;交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架&#xff08;一&#xff09; 论文解读&#xff1a;交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

SpringTask-03.入门案例

一.入门案例 启动类&#xff1a; package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中&#xff0c;新增了一个本地验证码接口 /code&#xff0c;使用函数式路由&#xff08;RouterFunction&#xff09;和 Hutool 的 Circle…...

HashMap中的put方法执行流程(流程图)

1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中&#xff0c;其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下&#xff1a; 初始判断与哈希计算&#xff1a; 首先&#xff0c;putVal 方法会检查当前的 table&#xff08;也就…...

排序算法总结(C++)

目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指&#xff1a;同样大小的样本 **&#xff08;同样大小的数据&#xff09;**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

R 语言科研绘图第 55 期 --- 网络图-聚类

在发表科研论文的过程中&#xff0c;科研绘图是必不可少的&#xff0c;一张好看的图形会是文章很大的加分项。 为了便于使用&#xff0c;本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中&#xff0c;获取方式&#xff1a; R 语言科研绘图模板 --- sciRplothttps://mp.…...

高考志愿填报管理系统---开发介绍

高考志愿填报管理系统是一款专为教育机构、学校和教师设计的学生信息管理和志愿填报辅助平台。系统基于Django框架开发&#xff0c;采用现代化的Web技术&#xff0c;为教育工作者提供高效、安全、便捷的学生管理解决方案。 ## &#x1f4cb; 系统概述 ### &#x1f3af; 系统定…...

Python常用模块:time、os、shutil与flask初探

一、Flask初探 & PyCharm终端配置 目的: 快速搭建小型Web服务器以提供数据。 工具: 第三方Web框架 Flask (需 pip install flask 安装)。 安装 Flask: 建议: 使用 PyCharm 内置的 Terminal (模拟命令行) 进行安装,避免频繁切换。 PyCharm Terminal 配置建议: 打开 Py…...