Tuesday, July 23, 2024
HomeJavaScriptRendering Wrapped Textual content To A Canvas In JavaScript

Rendering Wrapped Textual content To A Canvas In JavaScript


Yesterday, I checked out utilizing the Vary class to detect line-breaks in a text-node inside the DOM (Doc Object Mannequin). Usually, you do not want to consider the road breaks that the person is seeing within the browser. Nonetheless, I’ve a use-case through which I must render mentioned textual content to a <canvas> ingredient. And, for the reason that <canvas> API has no inherent method to render line-wrapped textual content, all line-wrapping must be carried out progammatically. As such, I needed to publish a fast-follow demo through which I exploit the line-break detection from yesterday’s demo to render wrapped textual content to a Canvas ingredient in JavaScript.

Run this demo in my JavaScript Demos challenge on GitHub.

View this code in my JavaScript Demos challenge on GitHub.

CAUTION: Rendering textual content to the <canvas> ingredient is an advanced beast with many edge-cases and cross-browser compatibility points. This publish doesn’t try to unravel any of these issues. This publish is merely an indication of yesterday’s resolution being utilized to a selected problem-domain.

With the HTML <canvas> ingredient, you possibly can invoke the .fillText() methodology on the 2D context to render textual content. This renders the textual content as a single line at a specified X,Y coordinate. As such, to be able to render a number of strains of textual content as a cohesive block, we now have to loop over the person strains and render every line at an growing Y-offset:

LineY = ( initialY + ( lineHeight * lineIndex ) )

Once more, there’s quite a lot of edge-case / cross-browser points with this comparatively easy idea – none of which we’re going to tackle on this publish. That mentioned, let us take a look at how we are able to use the line-break detection to render wrapped textual content to a Canvas 2D context. Within the following demo, I’ve two side-by-side panels. On the left is a <p> ingredient; and, on the proper is a <canvas> ingredient. The decision-to-action button extracts the person strains from the paragraph after which renders them to the canvas, one line at a time.

This makes use of the extractLinesFromTextNode() from yesterday to extract the textual content after which calls a brand new methodology, renderSampleNodeToCanvas(), to render these strains to the Canvas:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>
		Rendering Wrapped Textual content To A Canvas In JavaScript
	</title>
	<hyperlink rel="stylesheet" kind="textual content/css" href="https://www.bennadel.com/weblog/./primary.css" />
</head>
<physique>

	<h1>
		Rendering Wrapped Textual content To A Canvas In JavaScript
	</h1>

	<div class="panels">
		<div class="panels__panel">

			<h2>
				P&mdash;Ingredient
			</h2>

			<p class="pattern">
				I am fairly positive there's much more to life than being actually, actually,
				ridiculously good wanting. And I plan on discovering out what that's. &mdash;
				Derek Zoolander
			</p>

			<button class="button">
				Render to Canvas
			</button>

		</div>
		<div class="panels__panel">

			<h2>
				Canvas&mdash;Ingredient
			</h2>

			<canvas class="canvas">
				<!-- Pattern textual content will likely be rendered right here as a number of strains of textual content. -->
			</canvas>

		</div>
	</div>

	<script kind="textual content/javascript">

		var pattern = doc.querySelector( ".pattern" );
		var button = doc.querySelector( ".button" );
		var canvas = doc.querySelector( ".canvas" );
		var context = canvas.getContext( "second" );

		// When the person clicks the button, render the textual content to the canvas.
		button.addEventListener( "click on", renderSampleNodeToCanvas );

		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //

		/**
		* I render the text-content of the pattern ingredient to the canvas.
		*/
		perform renderSampleNodeToCanvas() {

			// The canvas ingredient does not encode the idea of line-wrapping for textual content. As
			// such, after we wish to render wrapped textual content to the canvas, we now have to carry out
			// the calculations ourselves; after which, draw every line, in flip, to the
			// canvas on the acceptable offset. To do that, we'll extract the
			// rendered strains of textual content from the supply DOM node (utilizing the code from
			// yesterday's demo).
			var strains = extractLinesFromTextNode( pattern.firstChild );

			// Let's additionally extract the run-time kinds of the textual content.
			// --
			// CAUTION: For this demo, I am assuming that all the things in regards to the font is
			// outlined in PIXELS (font-size, line-height). This retains all the things as easy
			// as potential (and considerably inside my skill-set). Additionally, Canvas has no sense
			// of letter-spacing, so we're assuming the font has a pure letter-spacing.
			var kinds = getElementTextStyles( pattern );
			var field = getElementBox( pattern );

			// Resize the canvas to match textual content container.
			canvas.setAttribute( "width", field.width );
			canvas.setAttribute( "top", field.top );

			// Set the canvas fill kinds to match the supply textual content kinds.
			context.fillStyle = kinds.colour;
			context.textBaseline = "high";
			context.font = ( kinds.fontWeight + " " + kinds.fontSize + "px " + kinds.fontFamily );

			// Every line of textual content must be rendered individually, with the vertical offset
			// being manually set on every rendering. To assist heart the textual content inside the
			// line-height, we'll add some preliminary offset to the Y-coordinate.
			// --
			// CAUTION: This isn't a constant cross-browser resolution; however, that goes
			// past the scope of this publish (and my present skill-set).
			var offsetY = ( ( kinds.lineHeight - kinds.fontSize ) / 2 );

			strains.forEach(
				perform iterator( line, i ) {

					context.fillText( line, 0, ( ( i * kinds.lineHeight ) + offsetY ) );

				}
			);

		}


		/**
		* I get the bounding field of the given ingredient.
		*/
		perform getElementBox( ingredient ) {

			var rawBox = ingredient.getBoundingClientRect();

			return({
				high: rawBox.y,
				left: rawBox.x,
				width: rawBox.width,
				top: rawBox.top
			});

		}


		/**
		* I get the runtime CSS properties for the given ingredient textual content.
		* 
		* CAUTION: The whole lot right here is assumed to be PIXELS for the demo.
		*/
		perform getElementTextStyles( ingredient ) {

			var rawStyles = window.getComputedStyle( ingredient );

			return({
				colour: rawStyles[ "color" ],
				fontSize: parseInt( rawStyles[ "font-size" ], 10 ),
				fontFamily: rawStyles[ "font-family" ],
				fontWeight: rawStyles[ "font-weight" ],
				lineHeight: parseInt( rawStyles[ "line-height" ], 10 )
			});

		}


		/**
		* I extract the visually rendered strains of textual content from the given textNode because it
		* exists within the doc at this very second. Which means, it returns the strains of
		* textual content as seen by the person.
		*/
		perform extractLinesFromTextNode( textNode ) {

			if ( textNode.nodeType !== 3 ) {

				throw( new Error( "Strains can solely be extracted from textual content nodes." ) );

			}

			// BECAUSE SAFARI: Not one of the "trendy" browsers appear to care in regards to the precise
			// structure of the underlying markup. Nonetheless, Safari appears to create vary
			// rectangles based mostly on the bodily construction of the markup (even when it
			// makes no distinction within the rendering of the textual content). As such, let's rewrite
			// the textual content content material of the node to REMOVE SUPERFLUOS WHITE-SPACE. This may
			// permit Safari's .getClientRects() to work like the opposite trendy browsers.
			textNode.textContent = collapseWhiteSpace( textNode.textContent );

			// A Vary represents a fraction of the doc which incorporates nodes and
			// components of textual content nodes. One factor that is actually cool a couple of Vary is that we
			// can entry the bounding bins that comprise the contents of the Vary. By
			// incrementally including characters - from our textual content node - into the vary, and
			// then wanting on the Vary's consumer rectangles, we are able to decide which
			// characters belong through which rendered line.
			var textContent = textNode.textContent;
			var vary = doc.createRange();
			var strains = [];
			var lineCharacters = [];

			// Iterate over each character within the textual content node.
			for ( var i = 0 ; i < textContent.size ; i++ ) {

				// Set the vary to span from the start of the textual content node as much as and
				// together with the present character (offset).
				vary.setStart( textNode, 0 );
				vary.setEnd( textNode, ( i + 1 ) );

				// At this level, the Vary's consumer rectangles will embody a rectangle
				// for every visually-rendered line of textual content. Which suggests, the final
				// character in our Vary (the present character in our for-loop) will likely be
				// the final character within the final line of textual content (in our Vary). As such, we
				// can use the present rectangle depend to find out the road of textual content.
				var lineIndex = ( vary.getClientRects().size - 1 );

				// If that is the primary character on this line, create a brand new buffer for
				// this line.
				if ( ! strains[ lineIndex ] ) {

					strains.push( lineCharacters = [] );

				}

				// Add this character to the presently pending line of textual content.
				lineCharacters.push( textContent.charAt( i ) );

			}

			// At this level, we now have an array (strains) of arrays (characters). Let's
			// collapse the character buffers down right into a single textual content worth.
			strains = strains.map(
				perform operator( characters ) {

					return( collapseWhiteSpace( characters.be a part of( "" ) ) );

				}
			);

			return( strains );

		}


		/**
		* I normalize the white-space within the given worth such that the quantity of white-
		* area matches the rendered white-space (browsers collapse strings of white-space
		* right down to single area character, visually, and that is simply updating the textual content to
		* match that habits).
		*/
		perform collapseWhiteSpace( worth ) {

			return( worth.trim().change( /s+/g, " " ) );

		}

	</script>

</physique>
</html>

As you possibly can see, we’re extracting the strains of textual content (because the person sees them within the browser), then calling .forEach() on them to render every line, in flip, to the Canvas. And, after every .fillText(), we’re merely incrementing the Y-coordinate by the runtime line-height of the textual content. And, after we run this, we get the next output:

A text node in the DOM is rendered to a canvas element, with line-breaks / wrapped text, using JavaScript.

As you possibly can see, we have faithfully utilized the runtime line-breaks of the Paragraph tag to the text-rendering on the Canvas ingredient. This appears to be like actually good in Chrome; it appears to be like principally good in Firefox; and, it appears to be like janky in Safari, which pushes the textual content down a number of pixels. That mentioned, this wasn’t a publish about flawless canvas rendering – this was an indication of how detecting runtime line-breaks in a rendered text-node will be useful.

Wish to use code from this publish?
Take a look at the license.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments