Any suggestion is highly appreciated.
Text selection has many components to it some visual, some non-visual.
First, make text selectable you must keep an array, of where the text is, what the text is, and what font was used. You will use this information with the Canvas function measureText.
By using measureText, with your text string, you can identify what letter the cursor should land on when you click on an image.
ctx.fillText("My String", 100, 100);
textWidth = ctx.measureText("My String").width;
You will still have to parse the font height from the "font" property, as it is not currently included in text metrics. Canvas text is aligned to the baseline by default.
With this information, you now have a bounding box, which you can check against. If the cursor is inside of the bounding box, you now have the unfortunate task of deducing which letter was intentionally selected; where the start of your cursor should be placed. This may involve calling measureText several times.
At that point you know where the cursor should go; you will need to store your text string as a text string, in a variable, of course.
Once you have defined the start and stop points of your range, you have to draw a selection indicator. This can be done in a new layer (a second canvas element), or by drawing a rectangle using the XOR composition mode. It can also be done by simply clearing and redrawing the text on top of a filled rectangle.
All told, text selection, text editing in Canvas are quite laborious to program, and it would be wise to re-use components already written, Bespin being an excellent example.
I'll edit my post should I come across other public examples. I believe that Bespin uses a grid-based selection method, possibly requiring a monospaced font. Ligatures, kerning, bi-directionality and other advanced features of font rendering require additional programming; it's a complex problem.
the text drawn in canvas elements cannot be selected, because of the nature of the canvas tag. But there are a few workarounds, like the one used in typefaceJS.
Another solution would be to add text with positioned div elements instead of useing strokeText or fillText.
If you need to have selectable text it would be a lot easier to just create a div or whatever, and position it on top of the canvas where you want the text to show.
canvas does not have any built-in mechanism for selecting text, so you would have to roll out your own text rendering and selecting code - which can be rather tricky to get right.
You may get some ideas from Bespin.
They implemented a text editor in javascript using canvas with text selection, scroll bars, cursor blinking, etc.
I would suggest using EaselJS library, you can add each letter as a child and even add mouse events to that object, its an amazing library, go check it out!
Let me begin by saying I am not an expert in text controls, but by now I'm sure that this doesn't matter, because I can help you get into the woods and out safely. These things are complicated in nature and require plenty of intuition and knowledge of how things work. However, you can inspect the code that runs in the senpai-js/senpai-stage
repository here.
We should define a few things up front:
/^.$/u
Insert
, Selection
, Basic
(I use the SelectionState
enum in my library and inspect the insertMode
property on stage)textScroll
which is always a negative numberNow I will go over each function to describe it's behavior to describe exactly how a textbox control should work.
Collision detection is a monster. Normalizing point movement between mouse and touch events is a complicated beast not covered in this text. Once you handle point events, you have to perform some sort of general collision detection for a rectangle. This means doing AABB collision. If the textbox sprite itself is rotated, you will have to "un-rotate" the point itself. However, we bypass this check if the mouse/touch point is already down over the textbox. This is because once you start selecting text, you want this function to always return true
. Then we move to narrowPhase collision, which actually checks to see if the "un-transformed" mouse/touch point is within the padding of the textbox. If it is, or the textbox is active, we return a truthy value here.
Once we know that the mouse/touch point is within the bounds of our textbox, we change the css of the canvas to cursor: text;
visually.
When we press the mouse button down over the textbox, we need to calculate where to move the caret. The caret can exist in a range from 0
to text.length
inclusive. Note that this isn't exactly right because unicode characters can have a length of 2
. You must keep track of each character added to your text inside an array to assert that you aren't measuring faulty unicode characters. Calculating the target index means looping over each character of the current text and appending it to a temporary string, measuring each time until the measured width is greater than the current textScroll + the measured textWidth.
Once we have garunteed that the point has gone down on top of the textbox and the starting point is set, we can start the "selection" mode. Dragging the point should move the selection from the starting caretIndex to the new calculated end index. This goes in both directions.
An example of this is shown here.
The solution for web key presses is to inspect the key
property on the KeyEvent. Despite a lot of what everyone says, it's possible to test that text property by testing it against the aforementioned unicode regex. If it matches, chances are that key has actually been pressed on the keyboard. This doesn't account for key combinations like ctrl + c
and ctrl + v
for copy and pasting. These features are trivial and are left up to the reader to decide how to implement these.
The few exceptions are the arrow keys: "ArrowLeft", "ArrowRight" etc. These keys actually modify the state of your control, and change how it functions. It's important to remember that key events should only be handled by the currently focused
control. This means you should check and make sure the control is focused during text input. This of course happens at a higher level than I have coded in my library, so this is trivial.
The next problem that needs to be solved is how each character input should modify the state of your control. The keyDown
method discerns the selectionState
and calls a different function based on it's state. This is not optimized pseudo-code, but is used for clarity, and is perfect for our purposes in describing the behavior.
selectionStart
, and insert the new key into the text arrayNormal
or Caret
selectionState
to Normal
again0
and text.length
indexes respectively
caretIndex
once againcaretIndex - 1
caretIndex
0
text.length
caretIndex
Caret
mode, you should restart the flash mechanism so that it shows exactly where they are each time the caret movesctx.measureText
to the caret index by slicing the text to the caret position unless the mode is Selection
Selection
, because we always want the end of the text selection to be visible to the userctx.save()
(basic canvas)textScroll
value which should be a negative numbermidline
value which should be the middle of the textbox verticallymiddle
and fill the text by calling text.join("")
on your text arrayYeah the task is monstrous, but sometimes you just want to give the world a visual novel engine, or a sprite stage with all sorts of features. Please visit the senpai-stage
repo and submit a pull request if you feel like you want senpai to notice you. Good luck on your endeavors!
~devil senpai
FabricJS now has the ability to interact with objects outside the canvas element - eg this demo shows a button that is linked to an image loaded in a canvas element.
The same approach can be used in other libs such as Raphael by hooking any move event, getting the bounding box of the element and re-positioning the HTML element.
canvas is just a drawing surface. You render and the result is pixels. So, you'd need to track the positions of all text you have rendered to the canvas in a some kind of data structure which you'd process during mouse events.
A simple answer would be: either use HTML or SVG instead of canvas. Unless you really need the degree of lowlevel control canvas offers.
©2020 All rights reserved.