【问题标题】:How to Make Canvas Text Selectable?如何使画布文本可选?
【发布时间】:2010-11-29 21:55:46
【问题描述】:

非常感谢任何建议。

【问题讨论】:

  • 由于答案已经快 3 年了,还有其他可以使用的解决方案或现成的库吗?
  • @Cybrix - 是的,请参阅下面的答案

标签: javascript text html canvas


【解决方案1】:

canvas 只是一个绘图表面。您渲染,结果是像素。因此,您需要在鼠标事件期间处理的某种数据结构中跟踪渲染到画布的所有文本的位置。

【讨论】:

    【解决方案2】:

    如果您需要有可选择的文本,只需创建一个 div 或其他任何内容,并将其放置在您希望文本显示的画布顶部,会容易得多。

    canvas 没有任何用于选择文本的内置机制,因此您必须推出自己的文本渲染和选择代码——这可能相当棘手。

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

        您可能会从 Bespin 那里得到一些想法。

        他们使用带有文本选择、滚动条、光标闪烁等功能的画布在 javascript 中实现了文本编辑器

        Source Code

        【讨论】:

        【解决方案5】:

        一个简单的答案是:使用 HTML 或 SVG 代替画布。除非你真的需要画布提供的低级控制程度。

        【讨论】:

          【解决方案6】:

          文本选择有很多组成部分,有些是可视的,有些是非可视的。

          首先,使文本可选择,您必须保留一个数组,其中包含文本的位置、文本的内容以及使用的字体。您将将此信息与 Canvas 函数 measureText 一起使用。

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

          ctx.fillText("My String", 100, 100);
          textWidth = ctx.measureText("My String").width;
          

          您仍然需要从“font”属性中解析字体高度, 因为它目前不包含在文本度量中。画布文本对齐 默认到基线。

          有了这些信息,您现在就有了一个可以检查的边界框。 如果光标在边界框内,你现在有一个不幸的任务是推导 故意选择了哪个字母;光标的起点应该放在哪里。这可能涉及多次调用 measureText。

          这时你就知道光标应该去哪里了;你需要存储你的 文本字符串作为文本字符串,当然在变量中。

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

          总而言之,Canvas 中的文本选择、文本编辑编程相当费力,重用已编写的组件是明智之举,Bespin 就是一个很好的例子。

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

          【讨论】:

          • 是的,Bespin 需要等宽字体。
          【解决方案7】:

          FabricJS 现在能够与画布元素之外的对象进行交互 - 例如,this demo 显示了一个按钮,该按钮链接到画布元素中加载的图像。

          通过挂钩任何移动事件、获取元素的边界框并重新定位 HTML 元素,可以在 Raphael 等其他库中使用相同的方法。

          【讨论】:

            【解决方案8】:

            我建议使用EaselJS 库,您可以将每个字母添加为子对象,甚至可以将鼠标事件添加到该对象,这是一个了不起的库,去看看吧!

            【讨论】:

              【解决方案9】:

              TextInput 控件很复杂

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

              我们应该预先定义一些东西:

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

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

              碰撞(宽相和窄相)

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

              一旦我们知道鼠标/触摸点在我们的文本框的范围内,我们将画布的 css 更改为 cursor: text;

              点碰撞

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

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

              here 显示了这方面的一个示例。

              按键

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

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

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

              按键选择

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

              普通模式下的keydown(插入符号模式)

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

              插入模式下的keyDown

              • 在插入模式下,我们将当前选中的字符替换为caretIndex
              • 使用拼接方法插入匹配 unicode 正则表达式的按键
              • 在拼接文本后将插入符号向右移动(检查并确保您没有超出文本长度)
              • 退格键删除当前选择之前的字符
              • delete 删除当前选中的字符
              • 箭头键按预期工作并在正常模式下描述
              • home 键和 end 键按预期工作并在正常模式下描述

              每帧更新文本框

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

              渲染文本框

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-06-23
                • 2011-08-10
                • 1970-01-01
                • 2011-04-10
                相关资源
                最近更新 更多