终端侧墨迹¶
在 0.50.0 版本加入.
使用笔绘图的程序收到 数位板指针协议 报告后,会向 矢量图像 的笔划添加一点,再等待终端绘制。因此每个笔采样都必须从终端到程序再返回,墨迹才显示;程序位于网络另一端时,需要两次跨越网络。线条会落后于笔,这正是绘图程序最不能容忍的情况。
终端侧墨迹允许终端自行绘制笔划。程序为某幅矢量图像提供画笔,此后终端随笔移动立即向图像添加各笔划的点,同时仍照常向程序发送报告。终端不绘制独立蒙层,而是向图像应用程序本来会发送的完全相同记录。因此笔划是程序场景中的普通元素,ID 由程序选择,程序可保留、替换为更好的笔划或删除。矢量图像的其他行为不变。它与其他扩展一样,是小巧、低干扰、易于实现的开放终端扩展,不改变 TTY 的本质。
消息封装¶
帧使用带有 ink 选择器的 OSC 777::
<OSC> 777 ; ink ; v=1 : op=query : id=42 <ST>
帧遵循 OSC 777 扩展封装 规定的 OSC 777 共用规则:字段语法、拒绝哪些帧,以及单位。除画笔的宽度、指数和颜色外,数值均为十进制整数;宽度和指数遵循矢量记录的数值格式,可带小数。协议版本 v 为 1,op 为 query、cap、brush 或 mode。终端仅发送回复,回复绝不合并或丢弃。
能力探测¶
程序询问终端是否能绘制墨迹::
<OSC> 777 ; ink ; v=1 : op=query : id=42 <ST>
id 是程序自行选择的非负 31 位整数,回复原样返回,以便匹配查询。支持协议的终端回答::
<OSC> 777 ; ink ; v=1 : op=cap : id=42 : caps=1 : mode=0 <ST>
caps 是位集合,目前仅定义 1,表示终端绘制笔划。墨迹启用时 mode 为 1,否则为 0。终端保存画笔时,回复还携带 画笔 规定的当前画笔字段,其中 k 为下一笔将取得的 ID,n 为剩余 ID 数量::
<OSC> 777 ; ink ; v=1 : op=cap : id=42 : caps=1 : mode=1 : i=7 : g=0 : k=1003 : n=47 : fill=#1a1a80 : w0=1 : w1=5 : e=1 : tools=58 <ST>
不支持本协议的终端会忽略查询,因此程序必须设置超时,并继续自行绘制笔划;无论如何程序都必须具备自行绘制能力。查询不改变状态。
画笔¶
程序通过向终端提供画笔来开启墨迹::
<OSC> 777 ; ink ; v=1 : op=brush : i=7 : k=1000 : n=50 : fill=#1a1a80 : w0=1 : w1=5 <ST>
|
必需。本窗口中一幅矢量图像( |
|
笔划所属图像分组的 ID,默认为根分组 |
|
必需。第一笔对应元素的 ID,至少为 |
|
程序从 |
|
笔划颜色,采用矢量记录格式: |
|
必需。满压力时的笔划宽度,以分组单位表示,为不小于 0 的有限数 |
|
无压力时的宽度,默认与 |
|
两个宽度之间压力曲线的指数,必须为大于 0 的有限数,默认 |
|
参与绘制的工具位集合,数位板指针协议每个 |
新画笔替换旧画笔(含 ID 范围),并开启墨迹。默认不包括橡皮,因为擦除含义由程序定义;若希望笔的另一端也绘制,例如用页面颜色涂抹,可提供包含橡皮的画笔。
模式¶
关闭墨迹::
<OSC> 777 ; ink ; v=1 : op=mode : mode=0 <ST>
终端保留画笔,mode=1 可用其剩余 ID 重新启用。因此程序在使用自己的橡皮或选择工具时可暂时停止终端绘制,之后无须重传画笔即可恢复。没有画笔时,mode=1 不产生效果。不带 mode 字段的 mode 请求读取状态,回复为::
<OSC> 777 ; ink ; v=1 : op=mode : mode=1 <ST>
模式和画笔均局限于当前窗口。RIS 关闭墨迹并清除画笔。程序必须在退出、挂起或失败时关闭墨迹,恢复时重新提供画笔;重连后应通过查询确认保留了什么,不要假定状态仍在。终端可允许用户针对全部或部分程序拒绝墨迹。被拒绝时读回为关闭且不绘制,因此程序应读回模式,不要假定已获准。
墨迹依赖工具报告:窗口必须开启 数位板指针协议 模式的位 1。否则笔作为鼠标使用,没有笔划,也不绘制。未发送画笔的程序完全不受影响。
笔划¶
墨迹启用期间,从工具 down 到 up 的数位板指针笔划,在满足以下条件时转为墨迹笔划:
工具在画笔指定的集合中;
down位于目标图像的一次放置上;若笔下有同一图像的多次放置,则选择最上层,即z最大、相同z时最后放置的那个;目标分组存在且确为分组,从根分组到目标分组的所有变换均可逆;
还有剩余 ID,且终端有空间容纳新笔划;终端可能限制同时为多少个工具绘制。
随后终端向图像应用以下记录,效果与程序通过矢量格式编辑操作发送该记录完全相同::
k id=<the next id> g=<the group> pts=X,Y,W fill=<the fill>
使用 down 报告给出的点(见 从报告换算到点),并消耗该 ID。程序收到的 down 报告会额外带有 ink 字段,值为元素 ID::
<OSC> 777 ; tablet-pointer ; v=1 : op=event : kind=1 : phase=1 : device=3 : t=408211 : contact=0 : x=25600 : y=51200 : buttons=1 : mods=0 : tool=1 : valid=1 : p=13107 : ink=1000 <ST>
该笔划后续的每次 move 和最终 up 都会追加相应的点::
a id=<that id> pts=X,Y,W
up 结束笔划。不带 ink 字段的 down 表示终端因某种原因未绘制此笔划;程序应像不支持本协议时一样自行绘制。ID 耗尽时程序也通过这种方式得知,并可提供带有更多 ID 的新画笔。
元素自创建起就属于程序。通常程序原样保留;也可在 up 之后,用相同 ID 的 k 记录替换为平滑或简化后的笔划,或用 x 记录删除。如果在绘制期间替换元素,终端仍会继续向占用该 ID 的对象追加后续点。
数位板指针协议通过 phase=4 取消的笔划视为从未发生:终端在发送取消报告前删除元素,等同于 x id=<that id>。该 ID 不会重新使用。
悬停报告不绘制,手指也不产生墨迹。
从报告换算到点¶
添加的点仅根据报告及终端生成报告时所显示的状态计算,因此程序或镜像场景的中继可以算出相同结果。结果是双精度实数,末位数字差异无需考虑。
设报告位置为 x、y,单位是相对字符单元区域左上角的物理像素 1/256。笔划开始的图像放置,其显示矩形左上角距字符区域左上角为 L、T 物理像素,宽 W、高 H;所显示的图像区域左上角为 sx、sy,尺寸 sw 乘 sh,单位为图像用户单位;未指定源矩形时就是整幅图像。换算到图像用户单位的位置为::
X = sx + (x / 256 - L) * sw / W
Y = sy + (y / 256 - T) * sh / H
设 M 为根分组到笔划分组的变换乘积,按根分组在先的顺序,每个矩阵取自其 g 记录的 t,缺省为单位矩阵。点在该分组坐标系中的位置为::
(X', Y') = inverse(M) applied to (X, Y)
宽度随压力变化。设报告压力为 p::
f = p / 65535 when the report has a pressure, bit 1 of valid
f = 1 when it has none
width = w0 + (w1 - w0) * f ^ e
因此,不测量压力的工具按满压力宽度绘制。能测量压力的工具,其 up 报告压力为 0,所以笔划以宽度 w0 结束。
放置的显示位置由图形协议决定。若放置位于当前可见屏幕从零开始的列 c0、行 r0,单元内偏移为 X、Y,字符单元尺寸为 cw 乘 ch 像素,则 kitty 使用::
L = c0 * cw + X
T = r0 * ch + Y
W, H = c * cw - X, r * ch - Y with both c and r
W, H = H * sw / sh, r * ch - Y with r alone
W, H = c * cw - X, W * sh / sw with c alone
W, H = sw, sh with neither
笔划进行期间的变化¶
笔划始终附着于起始放置。数位板指针协议在笔离开窗口时仍将笔划捕获到原窗口,墨迹也同样处理:放置区域外的位置按相同公式算成图像外的点,超出区域的部分不可见。
每个点都依据生成相应报告时的状态计算:
屏幕滚动或用户回滚使放置移动时,
L和T使用当前显示位置,使墨迹仍位于笔下。目标分组或其祖先分组的变换改变时,后续点使用新变换;已有点随分组一起移动,与其他元素一样。
若分组消失或变换不再可逆,导致报告位置无法换算到分组,则不添加该点,下一条报告仍可继续笔划。
图像或放置被删除、滚出回滚缓冲区,或因重新传输图像而替换时,该墨迹笔划结束。不会因此删除已有内容,笔划报告仍继续交给程序。
窗口在主屏幕与备用屏幕间切换时,被离开屏幕上的图像笔划同样结束。
笔划期间提供的新画笔从下一笔开始应用,当前笔划继续使用起始画笔。中途关闭墨迹会保留已有绘制,不再添加新点。
示例¶
程序有一幅 7 号矢量图像,尺寸 400 乘 300 用户单位,以原尺寸放置,左上角位于字符区域向右 30、向下 40 像素处。探测到终端支持墨迹后,提供画笔:深蓝色,宽度一至五个用户单位,从 1000 起保留五十个 ID::
<OSC> 777 ; ink ; v=1 : op=query : id=1 <ST>
<OSC> 777 ; ink ; v=1 : op=cap : id=1 : caps=1 : mode=0 <ST>
<OSC> 777 ; tablet-pointer ; v=1 : op=mode : mode=1 <ST>
<OSC> 777 ; ink ; v=1 : op=brush : i=7 : k=1000 : n=50 : fill=#1a1a80 : w0=1 : w1=5 <ST>
笔在向右 100、向下 200 像素处,以满压力的五分之一落下;随后压力增大,向右移动五像素、向下移动两像素,再向前移动一像素后抬起。程序收到::
<OSC> 777 ; tablet-pointer ; v=1 : op=event : kind=1 : phase=1 : device=3 : t=408211 : contact=0 : x=25600 : y=51200 : buttons=1 : mods=0 : tool=1 : valid=1 : p=13107 : ink=1000 <ST>
<OSC> 777 ; tablet-pointer ; v=1 : op=event : kind=1 : phase=2 : device=3 : t=408215 : contact=0 : x=26880 : y=51712 : buttons=1 : mods=0 : tool=1 : valid=1 : p=39321 <ST>
<OSC> 777 ; tablet-pointer ; v=1 : op=event : kind=1 : phase=3 : device=3 : t=408219 : contact=0 : x=27136 : y=51712 : buttons=0 : mods=0 : tool=1 : valid=1 : p=0 <ST>
程序收到报告时,终端已经对图像 7 应用并绘制了::
k id=1000 pts=70,160,1.8 fill=#1a1a80
a id=1000 pts=75,162,3.4
a id=1000 pts=76,162,1
因为 70 = (25600 / 256 - 30) * 400 / 400,且 1.8 = 1 + (5 - 1) * 13107 / 65535。程序在自己的场景副本中,以 ID 1000 添加相同三点。如果需要更平滑的线条,可通过编辑操作发送相同 ID、使用自选点的 k 记录;如果判断该笔划实际是选择操作的点击,则发送 x id=1000。否则无需发送内容:笔划已存在。
中继与多路复用器¶
双向透传字节的中继或多路复用器不必了解本协议,墨迹由链路末端、即笔所在位置的终端绘制;这正是本协议的主要用途。
将矢量场景保存为记录表、以便稍后发送给新连接终端的中继,必须维持记录表与实际场景一致。它可以自行用 caps=0 回答查询、不转发画笔;也可根据转发的报告、画笔及客户端显示的放置,计算并加入终端按本页规则应用的记录。若为窗格转换报告位置,应先转换再计算;移动或调整窗格大小时,按数位板指针协议取消其中笔划,从而移除对应墨迹。
多路复用器与其他状态一起保存每个窗格的模式与画笔,脱离连接时关闭墨迹。
实现情况¶
silkie,即此 kitty 分支,在能报告工具的所有平台上实现。
silkie 实现说明¶
终端在向程序写入报告前,将报告转换为记录并应用;图像在下一帧通过 矢量图像 的局部重绘显示,只重绘新增笔划周围的矩形。Silkie 随工具开始绘制而动态扩展活动笔划存储,同时工具数量没有固定上限。笔划中的点与程序发送的点一样,计入图像存储配额。