小智 23

文本选择有许多组件,一些是视觉的,一些是非视觉的.

首先,使文本可选择您必须保留一个数组,文本的位置,文本的内容以及使用的字体.您将使用Canvas函数measureText来使用此信息.

通过使用measureText和文本字符串,您可以识别单击图像时光标应该落在哪个字母上.

ctx.fillText("My String", 100, 100);
textWidth = ctx.measureText("My String").width;
Run Code Online (Sandbox Code Playgroud)

您仍然需要从"font"属性解析字体高度,因为它当前未包含在文本度量标准中.默认情况下,Canvas文本与基线对齐.

有了这些信息,您现在有一个边界框,您可以检查.如果光标在边界框内,你现在有一个不幸的任务,即推断出故意选择了哪个字母; 应该放置光标的开头.这可能涉及多次调用measureText.

那时你知道光标应该去哪里; 当然,您需要将文本字符串存储为变量中的文本字符串.

一旦定义了范围的起点和终点,就必须绘制选择指标.这可以在新图层(第二个画布元素)中完成,也可以使用XOR合成模式绘制矩形.它也可以通过简单地清除和重绘填充矩形顶部的文本来完成.

总而言之,在Canvas中进行文本选择,文本编辑是非常费力的,并且重新使用已经编写的组件是明智的,Bespin是一个很好的例子.

如果我遇到其他公开示例,我会编辑我的帖子.我相信Bespin使用基于网格的选择方法,可能需要等宽字体.字体渲染的连字,字距调整,双向性和其他高级功能需要额外的编程; 这是一个复杂的问题.


Jos*_*ner 6

TextInput 控件很复杂

首先让我说我不是文本控制方面的专家,但现在我确信这无关紧要,因为我可以帮助您安全进出树林。这些事情本质上很复杂,需要大量的直觉和关于事物如何运作的知识。但是,您可以在此处检查senpai-js/senpai-stage存储库中运行的代码。

我们应该预先定义一些事情:

  • 文本可以是任何有效的 unicode 字符。您可以使用正则表达式解析它:/^.$/u
  • 您需要跟踪三种不同类型的文本编辑模式:Insert, Selection, Basic(我使用SelectionState库中的枚举并insertMode在舞台上检查属性)
  • 你应该在每一个回合实施健全性检查,否则你将有未定义和意外的行为
  • 大多数人希望文本输入的宽度可以调整大小,因此如果您打算使用纹理,请确保为文本框的内部使用图案
  • 鼠标/触摸点碰撞检测很复杂,除非您保证文本输入控件不会旋转
  • 当文本在水平方向上大于文本框时,它应该滚动。我们将其称为textScrollwhich 始终为负数

现在我将详细介绍每个函数来描述它的行为,以准确描述文本框控件应该如何工作。

碰撞(宽相和窄相)

碰撞检测是一个怪物。标准化鼠标和触摸事件之间的点移动是本文未涵盖的复杂问题。处理点事件后,您必须对矩形执行某种常规碰撞检测。这意味着进行 AABB 碰撞。如果文本框精灵本身被旋转,你将不得不“取消旋转”点本身。但是,如果鼠标/触摸点已经在文本框上方,我们会绕过此检查。这是因为一旦您开始选择文本,您就希望此函数始终返回true。然后我们转到窄相碰撞,它实际上检查“未转换”的鼠标/触摸点是否在文本框的内边距内。如果是,或者文本框处于活动状态,我们将在此处返回一个真值。

一旦我们知道鼠标/触摸点在我们的文本框的边界内,我们就将画布的 css 更改为cursor: text;可视化。

点碰撞

当我们在文本框上按下鼠标按钮时,我们需要计算插入符号的移动位置。插入符号可以存在于从0text.length包含的范围内。请注意,这并不完全正确,因为 unicode 字符的长度可以为2. 您必须跟踪添加到数组中文本中的每个字符,以断言您没有测量错误的 unicode 字符。计算目标索引意味着遍历当前文本的每个字符并将其附加到一个临时字符串中,每次测量直到测量的宽度大于当前的 textScroll + 测量的 textWidth。

一旦我们确定该点已落在文本框的顶部并设置了起点,我们就可以启动“选择”模式。拖动该点应将选择从起始 caretIndex 移动到新计算的结束索引。这是双向的。

此处显示一个示例。

按键次数

Web 按键的解决方案是检查keyKeyEvent 上的属性。尽管每个人都说了很多,但可以通过针对上述 unicode regex 对其进行测试来测试该文本属性。如果匹配,则该键可能实际上已在键盘上按下。这种不考虑像组合键ctrl + c以及ctrl + v用于复制和粘贴。这些功能是微不足道的,由读者决定如何实现这些功能。

少数例外是箭头键:“ArrowLeft”、“ArrowRight”等。这些键实际上修改了控件的状态,并改变了它的功能。重要的是要记住,键事件应该只由当前focused控件处理。这意味着您应该检查并确保控件在文本输入期间获得焦点。这当然发生在比我在我的库中编码更高的级别,所以这是微不足道的。

下一个需要解决的问题是每个字符输入应该如何修改控件的状态。该keyDown方法识别selectionState并根据其状态调用不同的函数。这不是优化的伪代码,而是为了清晰起见,非常适合我们描述行为的目的。

按下选择键

  • 普通按键替换所选文本的内容
  • 从 拼接出selectionStart,并将新键插入到文本数组中
  • 如果按下“删除”或“退格”,则拼接选择并将选择模式返回到NormalCaret
  • 如果按下“左”或“右”键,则将光标分别移动到开头或结尾,除非按下 shift 键,否则选择模式将返回到正常
  • 如果按下 shift 键,那么我们实际上想要进一步扩展选择
    • 选择开始将始终位于 caretIndex,我们基本上使用此组合键向左或向右移动选择结束点
    • 如果选择结束返回插入符号索引,我们再次返回selectionStatetoNormal
  • "home" 和 "end" 键的工作方式相同,只有插入符号分别移动到0text.length索引
    • 另请注意,按住 shift 键会caretIndex再次从

正常模式下的按键(插入符号模式)

  • 在插入符号模式下,我们不替换任何文本,只在当前位置插入新字符
  • 使用 splice 方法插入与 unicode regex 匹配的 keydown
  • 拼接文本后向右移动插入符号(检查并确保您没有超过文本长度)
  • 退格键删除索引前的一个字符 caretIndex - 1
  • Delete 删除索引后的一个字符 caretIndex
  • 按住 shift 键时,文本选择适用于左右键
  • 当没有按下 shift 时,向左和向右分别将插入符号向左和向右移动
  • 主页键将 caretIndex 设置为 0
  • 结束键将 caretIndex 设置为 text.length

插入模式下的 keyDown

  • 在插入模式下,我们替换当前选定的字符 caretIndex
  • 使用 splice 方法插入与 unicode regex 匹配的 keydown
  • 拼接文本后向右移动插入符号(检查并确保您没有超过文本长度)
  • 退格键删除当前选择之前的字符
  • delete 删除当前选中的字符
  • 箭头键按预期工作并在正常模式下描述
  • 主页和结束键按预期工作并在正常模式下描述

每帧更新文本框

  • 如果文本框获得焦点,您应该开始闪烁插入符号,让用户知道他们正在编辑文本框中的文本
  • Caret模式中向左或向右移动插入符号时,您应该重新启动闪烁机制,以便每次插入符号移动时它都能准确显示它们的位置
  • 每 30 帧或半秒闪烁一次插入符号
  • 通过使用ctx.measureText插入符号索引来测量插入符号沿文本的距离,方法是将文本切片到插入符号位置,除非模式为Selection
  • 测量文本在选择模式下的距离仍然很有用Selection,因为我们总是希望文本选择的结尾对用户可见
  • 确保插入符号始终在文本框的可见边界内可见,同时考虑到当前的 textScroll

渲染文本框

  • 首先保存上下文ctx.save()(基本画布)
  • 如果不绘制带路径的文本框,则在第一层分别绘制文本框的左帽、中间图案和右帽
  • 使用由内边距和文本框大小定义的路径剪出一个正方形以防止文本溢出
  • 转换为textScroll应该是负数的 x值
  • 转换为 ymidline值,它应该是垂直文本框的中间
  • 设置字体属性
  • 将文本基线设置为middle并通过调用text.join("")文本数组来填充文本
  • 如果有选择或插入模式,请确保在所选文本后面绘制一个“蓝色”方块并反转所选文本的字体颜色(这很重要,留给读者作为练习)


Fab*_*ger 5

由于canvas标签的性质,无法选择在canvas元素中绘制的文本.但是有一些解决方法,比如在typefaceJS中使用的解决方法.

另一种解决方案是添加带有定位div元素的文本,而不是使用strokeText或fillText.