Konva.js 滚轮缩放与平移:以鼠标为中心缩放 Stage 的正确实现

scale() vs width()/height() 缩放单个图片节点有两种方式: // scale():保持节点尺寸不变,视觉上缩放 imageNode.scale({ x: 2, y: 2 });// width()/height():真正修改节点尺寸 imageNode.width(600); imageNode.height(400);两者的区别在于 scale() 不改变节点的逻辑尺寸(width()/height() 返回值不变),而后者实际修改了尺寸。大多数场景用 scale() 更灵活。 标注工具:缩放整个 Stage 如果 Stage 上同时有图片和标注框(Rect/Line),不要缩放单个图片节点,应缩放整个 Stage: stage.scale({ x: newScale, y: newScale }); stage.position(newPos); stage.batchDraw();这样图片、标注框、文字所有节点都会同步缩放,坐标关系保持一致。CVAT、LabelImg 等标注工具都使用这种方式。 滚轮缩放(以鼠标位置为中心) const SCALE_BY = 1.05;stage.on('wheel', (e) => { e.evt.preventDefault(); const oldScale = stage.scaleX(); const pointer = stage.getPointerPosition(); // 鼠标在画布坐标系中的位置(排除当前 scale 和偏移) const mousePointTo = { x: (pointer.x - stage.x()) / oldScale, y: (pointer.y - stage.y()) / oldScale, }; const newScale = e.evt.deltaY > 0 ? oldScale / SCALE_BY // 向下:缩小 : oldScale * SCALE_BY; // 向上:放大 // 限制缩放范围 const clampedScale = Math.max(0.1, Math.min(newScale, 20)); stage.scale({ x: clampedScale, y: clampedScale }); // 调整偏移,使鼠标下方的点保持不动 stage.position({ x: pointer.x - mousePointTo.x * clampedScale, y: pointer.y - mousePointTo.y * clampedScale, }); stage.batchDraw(); });核心思路:缩放前记录鼠标在画布坐标系(未缩放坐标)的位置,缩放后重新计算 Stage 偏移使该点回到鼠标位置,从而实现"以鼠标为中心"的缩放效果。 拖拽平移 stage.draggable(true);启用后可以直接拖动整个 Stage。如果需要区分拖拽图片和拖拽画布,禁用 stage.draggable() 改为手动处理: let isDragging = false; let lastPos = null;stage.on('mousedown', () => { isDragging = true; lastPos = stage.getPointerPosition(); });stage.on('mousemove', () => { if (!isDragging) return; const pos = stage.getPointerPosition(); stage.position({ x: stage.x() + pos.x - lastPos.x, y: stage.y() + pos.y - lastPos.y, }); lastPos = pos; stage.batchDraw(); });stage.on('mouseup', () => { isDragging = false; });坐标系转换:获取原图坐标 点击事件的 stage.getPointerPosition() 返回的是屏幕坐标,需要转换为原图坐标: stage.on('click', () => { const pointer = stage.getPointerPosition(); const scale = stage.scaleX(); const imageX = (pointer.x - stage.x()) / scale; const imageY = (pointer.y - stage.y()) / scale; console.log('原图坐标:', imageX, imageY); });

CIDR 表示法:/24、/32 的含义和只匹配单个 IP 的写法

CIDR(无类别域间路由)用"IP地址/前缀长度"表示一段 IP 范围,前缀长度决定有多少个 IP 被包含在内。 /24 不等于单个 IP 43.255.122.56/24 表示前 24 位固定,等价于: 43.255.122.0 - 43.255.122.255共 256 个地址,包含 43.255.122.56,但也包含同网段的所有其他地址。 只匹配单个 IP:使用 /32 IPv4 地址是 32 位,/32 表示所有 32 位都固定: 43.255.122.56/32 → 仅匹配 43.255.122.56配置防火墙、ACL、路由规则、Cloudflare IP 规则时,允许或封禁单个 IP 都应使用 /32: # iptables iptables -A INPUT -s 43.255.122.56/32 -j ACCEPT# Nginx allow 43.255.122.56/32;如果不要求 CIDR 格式,直接写 IP 不加后缀也等同于 /32: 43.255.122.56常用前缀长度对照写法 匹配范围 地址数x.x.x.x/32 仅这一个 IP 1x.x.x.x/31 2 个(常用于点对点链路) 2x.x.x.x/30 4 个 4x.x.x.x/29 8 个 8x.x.x.x/28 16 个 16x.x.x.0/24 256 个(常见局域网段) 256x.x.0.0/16 65536 个 65536x.0.0.0/8 16777216 个(A 类网络) 167772160.0.0.0/0 所有 IPv4 地址 全部前缀长度与地址数的计算 地址数 = 2^(32 - 前缀长度)/32 → 2^0 = 1 /24 → 2^8 = 256 /16 → 2^16 = 65536网络地址与广播地址 在标准 IPv4 子网中,最小地址(如 43.255.122.0)是网络地址,最大地址(43.255.122.255)是广播地址,可用主机地址是中间的 254 个。但在防火墙规则和 CIDR 匹配中,这个区分通常不重要,/24 就是匹配全部 256 个地址。

CIDR 记法要写对:43.255.122.56/24 匹配的是一整段

配置防火墙、白名单、路由规则的时候,经常见到有人这样写: 43.255.122.56/24本意是想匹配单个 IP,结果匹配了 256 个。CIDR 的斜杠语法是"前面多少位固定",不是"IP 加上标签"。 /24 到底表示什么 43.255.122.56/24 意思是:IP 的前 24 位固定。IPv4 一共 32 位,24 位固定就是前三段(43.255.122)不变,最后一段(.56)被忽略、整段 0-255 都算命中。 等价于: 43.255.122.0 – 43.255.122.255 (256 个地址)所以: 43.255.122.0 ✅ 命中 43.255.122.56 ✅ 命中 43.255.122.100 ✅ 命中 43.255.122.255 ✅ 命中 43.255.123.56 ❌ 未命中想匹配单个 IP:/32 要精确匹配一个 IP,写 /32——32 位全都固定,就是它自己: 43.255.122.56/32如果配置格式不要求必须有 CIDR 后缀,直接写 IP 也行: 43.255.122.56/32 是最严格的等价形式。 常见 CIDR 对照写法 匹配范围 地址数43.255.122.56/32 只 43.255.122.56 143.255.122.56/31 43.255.122.56 – 57 243.255.122.56/30 43.255.122.56 – 59 443.255.122.56/29 43.255.122.56 – 63 843.255.122.56/28 43.255.122.48 – 63 1643.255.122.56/27 43.255.122.32 – 63 3243.255.122.56/26 43.255.122.0 – 63 6443.255.122.56/25 43.255.122.0 – 127 12843.255.122.56/24 43.255.122.0 – 255 25643.255.122.56/16 43.255.0.0 – 43.255.255.255 65,53643.255.122.56/8 43.0.0.0 – 43.255.255.255 16M+43.255.122.56/0 整个 IPv4 空间 全部注意:CIDR 里"起点 IP"随便写,实际会向下对齐。43.255.122.56/24 和 43.255.122.99/24 等价,都指 43.255.122.0/24 这个网段。 计算方法 给一个 CIDR A.B.C.D/N,怎么算范围? 掩码位数 → 主机位数 = 32 - N 主机位数决定了段长:主机位 = 0:1 个 IP(/32) 主机位 = 1:2 个(/31) 主机位 = 8:256 个(/24) 主机位 = 16:65536 个(/16) 主机位 = N:2^N 个起点:把 A.B.C.D 的后 (32-N) 位置零。 例:10.0.5.37/28N=28,主机位=4 段长 = 2^4 = 16 后 4 位置零:37 二进制 00100101 → 后 4 位置零 00100000 = 32 范围:10.0.5.32 – 10.0.5.47Linux 命令算 ipcalc 是最快的: $ ipcalc 43.255.122.56/24 Address: 43.255.122.56 Netmask: 255.255.255.0 = 24 Network: 43.255.122.0/24 HostMin: 43.255.122.1 HostMax: 43.255.122.254 Broadcast: 43.255.122.255 Hosts/Net: 254或者 Python 一行: import ipaddress net = ipaddress.ip_network("43.255.122.56/24", strict=False) print(net.network_address, net.broadcast_address, net.num_addresses)常见误区 误区 1:以为 /24 加个数字更保险不是。任何斜杠后缀都是"网段掩码",你越加越大。 误区 2:/32 单独 IP 时可以省掉大多数系统能省,但**某些严格配置格式(RBAC 白名单、AWS SG)**要求必须写 /32,别偷懒。 误区 3:以为 /0 是"无匹配"反过来——0.0.0.0/0 匹配整个 IPv4 空间,路由表里就是默认路由。 IPv6 版 IPv6 也用 CIDR: 2001:db8::1/128 单个 IP 2001:db8::/32 一整段 ::/0 整个 IPv6 空间规则一样,只是位数从 32 变成 128。 一句话总结 IP 加 /N 是网段,前 N 位固定其余任意。单个 IP 要写 /32(IPv6 是 /128)。别偷懒不加 CIDR 后缀,也别把 /24 当成"某个 IP 的编号"。

HTML 自定义鼠标指针:十字准星、全屏辅助线与 CSS cursor 类型

跟随鼠标的十字准星 用 ::before 和 ::after 绘制十字线,mousemove 更新位置: <style> #crosshair { position: fixed; width: 20px; height: 20px; pointer-events: none; /* 不阻挡下层元素的鼠标事件 */ z-index: 99999; }#crosshair::before, #crosshair::after { content: ""; position: absolute; background: red; }/* 垂直线 */ #crosshair::before { left: 50%; top: 0; width: 1px; height: 100%; transform: translateX(-50%); }/* 水平线 */ #crosshair::after { top: 50%; left: 0; width: 100%; height: 1px; transform: translateY(-50%); } </style><div id="crosshair"></div><script> const crosshair = document.getElementById("crosshair");document.addEventListener("mousemove", (e) => { crosshair.style.left = `${e.clientX - 10}px`; crosshair.style.top = `${e.clientY - 10}px`; }); </script>pointer-events: none 让准星元素"透明"——鼠标事件穿透到下层元素。 全屏辅助线(贯穿整个视口) <style> .guide-h, .guide-v { position: fixed; pointer-events: none; z-index: 99999; opacity: 0.4; }.guide-h { width: 100vw; height: 1px; left: 0; background: #00aaff; }.guide-v { height: 100vh; width: 1px; top: 0; background: #00aaff; } </style><div class="guide-h" id="guideH"></div> <div class="guide-v" id="guideV"></div><script> const guideH = document.getElementById("guideH"); const guideV = document.getElementById("guideV");document.addEventListener("mousemove", (e) => { guideH.style.top = `${e.clientY}px`; guideV.style.left = `${e.clientX}px`; }); </script>水平线跟随 clientY,垂直线跟随 clientX,始终贯穿整个视口。 显示/隐藏辅助线(快捷键切换) let visible = true;document.addEventListener("keydown", (e) => { if (e.key === "h") { // 按 H 键切换 visible = !visible; guideH.style.display = visible ? "block" : "none"; guideV.style.display = visible ? "block" : "none"; } });CSS cursor 常用值 /* 标注/选择场景 */ cursor: crosshair; /* 十字准星 */ cursor: cell; /* 单元格十字 *//* 拖拽场景 */ cursor: grab; /* 手型(可拖拽) */ cursor: grabbing; /* 手型(拖拽中)*/ cursor: move; /* 四方向箭头 *//* 调整大小 */ cursor: col-resize; /* 左右调宽 */ cursor: row-resize; /* 上下调高 */ cursor: nwse-resize; /* 左上-右下对角 */ cursor: nesw-resize; /* 右上-左下对角 *//* 缩放 */ cursor: zoom-in; cursor: zoom-out;/* 禁止操作 */ cursor: not-allowed;自定义图片作为鼠标 .custom-cursor { cursor: url("/img/cursor.png") 8 8, crosshair; /* url(...) 8 8 表示热点坐标(点击点相对图片左上角的偏移) */ /* 第二个值是 fallback cursor,浏览器不支持时使用 */ }图片尺寸建议不超过 128x128px,PNG 格式,透明背景。 Canvas 中覆盖系统鼠标 // 隐藏系统鼠标指针 canvas.style.cursor = "none";// 在 Canvas 内自行绘制准星 canvas.addEventListener("mousemove", (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; ctx.clearRect(0, 0, canvas.width, canvas.height); // ... 重绘内容 ... // 绘制准星 ctx.strokeStyle = "red"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x - 10, y); ctx.lineTo(x + 10, y); ctx.moveTo(x, y - 10); ctx.lineTo(x, y + 10); ctx.stroke(); });Canvas 场景下用 cursor: none + 手动绘制是最常见的方式,可以精确控制准星样式和动画。

Python 通过 Shadowsocks SOCKS5 代理发送请求

核心原理 Python 不能直接解析 Shadowsocks 协议。标准做法是: Python ↓ SOCKS5 sslocal (本地 1080 端口) ↓ Shadowsocks 协议 远端 SS 服务器 ↓ 明文 目标网站先用 Shadowsocks 客户端在本地起一个 SOCKS5 代理,Python 通过这个代理访问外网。 方法一:requests[socks](推荐) 安装: pip install requests[socks]使用: import requestsproxies = { "http": "socks5://127.0.0.1:1080", "https": "socks5://127.0.0.1:1080" }r = requests.get( "https://httpbin.org/ip", proxies=proxies, timeout=10 )print(r.json())socks5:// 使用远端 DNS 解析;如果想让本地 DNS 解析,换成 socks5h://。 方法二:PySocks 全局劫持 安装: pip install pysocks设置全局默认代理,之后所有 socket 连接都走 SOCKS5: import socket import sockssocks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 1080) socket.socket = socks.socksocketimport requestsr = requests.get("https://httpbin.org/ip") print(r.json())适合需要代理所有网络调用(requests、httpx、urllib 等)的场景,但会影响进程内所有 socket,慎用。 方法三:环境变量 export ALL_PROXY=socks5://127.0.0.1:1080 export HTTPS_PROXY=socks5://127.0.0.1:1080 export HTTP_PROXY=socks5://127.0.0.1:1080或者在 Python 代码里设置: import osos.environ["ALL_PROXY"] = "socks5://127.0.0.1:1080"import requestsr = requests.get("https://httpbin.org/ip") print(r.json())requests 会自动读取 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY 环境变量。 在代码里启动 sslocal 如果想让 Python 程序自己管理 Shadowsocks 子进程: import subprocess import time import requestsproc = subprocess.Popen([ "sslocal", "-s", "服务器IP", "-p", "8388", "-k", "密码", "-m", "aes-256-gcm", "-l", "1080" ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)time.sleep(1) # 等待 sslocal 就绪proxies = { "http": "socks5://127.0.0.1:1080", "https": "socks5://127.0.0.1:1080" }try: r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10) print(r.json()) finally: proc.terminate()sslocal 来自 shadowsocks-libev 或 shadowsocks-rust 包,需要提前安装。 验证代理是否生效 import requestsproxies = { "http": "socks5://127.0.0.1:1080", "https": "socks5://127.0.0.1:1080" }r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10) print(r.json()) # 返回的 origin IP 应为代理服务器 IP,不是本机 IP