【第3章 文本】3.3 文本的定位

发布于:2025-06-04 ⋅ 阅读:(22) ⋅ 点赞:(0)


水平与垂直定位

在canvas中使用 strokeText() 或 fillText() 绘制文本时,需要指定所绘文本的 X 和 Y 的坐标,然而,浏览器具体将文本绘制在何处,则要看 textAlign 和 textBaseline 这两个属性。

属性 描述
textAlign 决定文本在水平方向上的对齐方式。start、left、center、right、end,默认值start
textBaseline 决定文本在垂直方向上的对齐方式。top、bottom、middle、alphabetic、ideographic、hanging,默认值alphabetic

示例

应用程序中的实心矩形表示传递给 fillText() 方法的 X 与 Y 坐标,显示的每个字符串都表示了一种 textAlign 与 textBaseline 属性值的组合。

在这里插入图片描述

<!DOCTYPE html>
<html lang="zh-CN">
	<head>
		<!-- 设置文档字符编码为UTF-8 -->
		<meta charset="UTF-8" />
		<title>3-4-利用textAlign与textBaseline属性来定位文本</title>
		<style>
			/* 页面背景色 */
			body {
				background: #eeeeee;
			}

			/* 画布样式设置 */
			#canvas {
				background: #ffffff;
				margin-top: 5px;
				margin-left: 10px;
				/* 兼容不同浏览器的阴影效果 */
				-webkit-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
				-moz-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
				box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
				/* 边框样式 */
				border: 1px solid rgba(0, 0, 0, 0.2);
			}
		</style>
	</head>
	<body>
		<!-- 画布元素,不支持时显示提示 -->
		<canvas id="canvas" width="780" height="440">canvas not supports</canvas>

		<script>
			// 使用严格模式
			'use strict'

			// 获取画布和绘图上下文
			let canvas = document.getElementById('canvas'),
				context = canvas.getContext('2d'),
				// 字体高度
				fontHeight = 24,
				// 文本水平对齐方式数组
				alignValues = ['start', 'center', 'end'],
				// 文本垂直对齐基线数组
				baseLineValue = ['top', 'middle', 'bottom', 'alphabetic', 'ideographic', 'hanging'],
				// 文本坐标
				x,
				y

			// 函数定义...

			/**
			 * 绘制网格线
			 * @param {string} color - 网格线颜色
			 * @param {number} stepx - 水平间距
			 * @param {number} stepy - 垂直间距
			 */
			let drawGrid = (color, stepx, stepy) => {
				context.save()

				// 设置网格样式
				context.strokeStyle = color
				context.fillStyle = '#ffffff'
				context.lineWidth = 0.5
				// 填充白色背景
				context.fillRect(0, 0, context.canvas.width, context.canvas.height)

				// 绘制垂直网格线
				for (let i = stepx + 0.5; i < context.canvas.width; i += stepx) {
					context.beginPath()
					context.moveTo(i, 0)
					context.lineTo(i, context.canvas.height)
					context.stroke()
				}

				// 绘制水平网格线
				for (let i = stepy + 0.5; i < context.canvas.height; i += stepy) {
					context.beginPath()
					context.moveTo(0, i)
					context.lineTo(context.canvas.width, i)
					context.stroke()
				}

				context.restore()
			}

			/**
			 * 在(x,y)位置绘制标记点
			 */
			let drawTextMarker = () => {
				context.fillStyle = 'yellow'
				// 绘制黄色小方块
				context.fillRect(x, y, 7, 7)
				// 绘制小方块边框
				context.strokeRect(x, y, 7, 7)
			}

			/**
			 * 绘制文本
			 * @param {string} text - 要绘制的文本
			 * @param {string} textAlign - 水平对齐方式
			 * @param {string} textBaseLine - 垂直基线对齐方式
			 */
			let drawText = (text, textAlign, textBaseLine) => {
				// 设置文本对齐方式
				if (textAlign) context.textAlign = textAlign
				if (textBaseLine) context.textBaseline = textBaseLine

				// 绘制蓝色文本
				context.fillStyle = 'cornflowerblue'
				context.fillText(text, x, y)
			}

			/**
			 * 绘制文本参考线
			 */
			let drawTextLine = () => {
				context.strokeStyle = 'gray'
				// 绘制水平参考线
				context.beginPath()
				context.moveTo(x, y)
				context.lineTo(x + 738, y)
				context.stroke()
			}

			// 初始化...
			// 设置字体样式
			context.font = 'oblique normal bold 24px palatino'
			// 绘制网格
			drawGrid('lightgray', 10, 10)

			// 遍历所有对齐组合
			for (let align = 0; align < alignValues.length; ++align) {
				for (let baseline = 0; baseline < baseLineValue.length; baseline++) {
					// 计算文本位置
					x = 20 + align * fontHeight * 15
					y = 20 + baseline * fontHeight * 3

					// 绘制文本和标记
					drawText(alignValues[align] + '/' + baseLineValue[baseline], alignValues[align], baseLineValue[baseline])
					drawTextMarker()
					drawTextLine()
				}
			}
		</script>
	</body>
</html>

textAlign

textAlign 属性默认值是 start,它对应了基于 X 的位置,类似于“向初始看齐”、“向中间看齐”、“向结尾看齐”。这里如果当 canvas 元素的 dir 属性值是ltr时,也就是说浏览器是按照从左至右的方向来显示文本时,left效果与start相同,right 效果与 end 相同。同理,如果 dir 属性值是 rtl,浏览器从右至左来显示文本,那么 right 效果与 start 相同,left 效果与 end 一致。图示应用程序是从左至右来显示文本的。

在这里插入图片描述

textBaseline

textBaseline 的属性默认值是 alphabetic,改值用于绘制由基于拉丁字母的语言所组成的字符串。ideographic 值则用于绘制中文字符串或日文。hanging 绘制各种印度语字符串。top、bottom、middle与特定语言不相关,代表文本周围的边界框之内的某个位置,这个边界框也叫做“字符方框”(em square)

在这里插入图片描述


将文本居中

下面应用程序分别将 textAlign 与 textBaseline 属性的值设置为 center 与 middle,然后对文本进行描边与填充。

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>3-5-将文本居中与某个点</title>
		<style>
			#canvas {
				background: #eee;
			}
		</style>
	</head>
	<body>
		<canvas id="canvas" width="600" height="400"> canvas not supported </canvas>

		<script>
			const canvas = document.getElementById('canvas'),
				context = canvas.getContext('2d')

			context.textAlign = 'center'
			context.textBaseline = 'middle' // 文本基线,默认是alphabetic,即文本的基线是字母的基线
			context.font = '64px Arial' // 字体大小和字体,必须在设置textAlig

			context.fillStyle = 'blue'
			context.strokeStyle = 'yellow'

			context.fillText('Centered', canvas.width / 2, canvas.height / 2)
			context.strokeText('Centered', canvas.width / 2, canvas.height / 2)
		</script>
	</body>
</html>

文本的度量

只要你做的事情与文本有关,你就得设法获取某个字符串的宽度和高度,Canvas 绘图环境对象提供了一个名为 measureText() 的方法,用以度量某个字符串的宽度。该方法所返回的 TextMetrics 对象中包含了一个名为 width 的属性,这个属性就是字符串的宽度。

警告:在调用 measureText() 方法之前先设置好字型
在使用measureText()方法时,常见的错误就是在调用完该方法之后,才去设置字型。因为measureText()方法是基于当前字型来计算字符串宽度的,因此调用方法后采取改变字型,所返回的宽度不能反映出那种字型来度量的是实际文本宽度。

在这里插入图片描述

<!DOCTYPE html>
<html lang="zh-CN">
	<head>
		<meta charset="UTF-8" />
		<title>3-6-计算文本行的宽度</title>
		<style>
			body {
				background: #eeeeee;
			}

			#canvas {
				background: #ffffff;
				margin-top: 5px;
				margin-left: 10px;
				-webkit-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
				-moz-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
				box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
				border: 1px solid rgba(0, 0, 0, 0.2);
			}
		</style>
	</head>
	<body>
		<canvas id="canvas">canvas not supports</canvas>
		<p>width展示的是在Canvas画布上的字符串'Hello'的宽度</p>

		<script>
			'use strict'

			let canvas = document.getElementById('canvas'),
				context = canvas.getContext('2d')

			context.font = '24px Arial'
			let word = 'Hello'
			context.fillText(word, 10, 50)
			context.fillText('width:' + context.measureText(word).width, 10, 100)
		</script>
	</body>
</html>

绘制坐标轴旁边的文本标签

绘制垂直于水平坐标轴的文本标签是非常简单的。应用程序根据坐标轴的位置、坐标轴上每条刻度线的长度,以及坐标轴于文本标签之间的距离,来决定每个文本标签的绘制坐标。

在这里插入图片描述

请注意,应用程序会设置textAlign和textBaseline的值,在绘制水平坐标轴上文本标签分别设置”center“和”top“,在绘制垂直坐标轴时,则设置“right”和”middle“。

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<title>3-7-为坐标轴加上文本标签</title>
	</head>
	<body>
		<!-- 创建画布元素 -->
		<canvas id="canvas" width="1200" height="600"></canvas>
	</body>
	<script>
		// 获取画布和绘图上下文
		let canvas = document.getElementById('canvas'),
			context = canvas.getContext('2d'),
			// 坐标轴外边距
			AXIS_MARGIN = 40.5,
			// 坐标原点位置(左下角)
			AXIS_ORIGIN = { x: AXIS_MARGIN, y: canvas.height - AXIS_MARGIN },
			// y轴顶点位置
			AXIS_TOP = AXIS_MARGIN,
			// x轴顶点位置
			AXIS_RIGHT = canvas.width - AXIS_MARGIN,
			// 横向刻度线间距
			HORIZONTAL_TICK_SPACING = 10,
			// 垂直刻度线间距
			VERTICAL_TICK_SPACING = 10,
			// X轴长度
			AXIS_WIDTH = AXIS_RIGHT - AXIS_ORIGIN.x,
			// y轴长度
			AXIS_HEIGHT = AXIS_ORIGIN.y - AXIS_TOP,
			// y轴刻度数量
			NUM_VERTICAL_TICKS = AXIS_HEIGHT / VERTICAL_TICK_SPACING,
			// x轴刻度数量
			NUM_HORIZONTAL_TICKS = AXIS_WIDTH / HORIZONTAL_TICK_SPACING,
			// 刻度线宽度
			TICK_WIDTH = 10,
			// 刻度线线宽
			TICKS_LINEWIDTH = 0.5,
			// 刻度线颜色
			TICK_COLOR = 'navy',
			// 坐标轴线宽
			AXIS_LINEWIDTH = 1.0,
			// 坐标轴颜色
			AXIS_COLOR = 'blue'

		// 标签与坐标轴的间距
		SPACE_BETWEEN_LABELS_AND_AXIS = 15

		/**
		 * 绘制背景网格线
		 * @param {string} color - 网格线颜色
		 * @param {number} stepX - 水平间距
		 * @param {number} stepY - 垂直间距
		 */
		let drawGrid = (color, stepX, stepY) => {
			context.strokeStyle = color
			context.lineWidth = 0.5

			// 绘制垂直网格线
			for (let i = stepX + 0.5; i < context.canvas.width; i += stepX) {
				context.beginPath()
				context.moveTo(i, 0)
				context.lineTo(i, context.canvas.height)
				context.stroke()
			}

			// 绘制水平网格线
			for (let i = stepY + 0.5; i < context.canvas.height; i += stepY) {
				context.beginPath()
				context.moveTo(0, i)
				context.lineTo(context.canvas.width, i)
				context.stroke()
			}
		}

		/**
		 * 绘制坐标轴(包含x轴和y轴)
		 */
		let drawAxis = () => {
			context.save()
			context.strokeStyle = AXIS_COLOR
			context.lineWidth = AXIS_LINEWIDTH

			// 绘制x轴和y轴
			drawHorizontalAxis()
			drawVerticalAxis()

			// 设置刻度线样式
			context.lineWidth = TICKS_LINEWIDTH
			context.strokeStyle = TICK_COLOR

			// 绘制刻度线
			drawVerticalAxisTicks()
			drawHorizontalAxisTicks()

			context.restore()
		}

		/**
		 * 绘制x轴
		 */
		let drawHorizontalAxis = () => {
			context.beginPath()
			context.moveTo(AXIS_ORIGIN.x, AXIS_ORIGIN.y)
			context.lineTo(AXIS_RIGHT, AXIS_ORIGIN.y)
			context.stroke()
		}

		/**
		 * 绘制y轴
		 */
		let drawVerticalAxis = () => {
			context.beginPath()
			context.moveTo(AXIS_ORIGIN.x, AXIS_ORIGIN.y)
			context.lineTo(AXIS_ORIGIN.x, AXIS_TOP)
			context.stroke()
		}

		/**
		 * 绘制y轴刻度线
		 */
		let drawVerticalAxisTicks = () => {
			let deltaY

			for (let i = 1; i < NUM_VERTICAL_TICKS; i++) {
				context.beginPath()
				// 每5个刻度画一个长刻度线
				deltaY = i % 5 === 0 ? TICK_WIDTH : TICK_WIDTH / 2
				context.moveTo(AXIS_ORIGIN.x - deltaY, AXIS_ORIGIN.y - i * VERTICAL_TICK_SPACING)
				context.lineTo(AXIS_ORIGIN.x + deltaY, AXIS_ORIGIN.y - i * VERTICAL_TICK_SPACING)
				context.stroke()
			}
		}

		/**
		 * 绘制x轴刻度线
		 */
		let drawHorizontalAxisTicks = () => {
			let deltaY

			for (let i = 1; i < NUM_HORIZONTAL_TICKS; i++) {
				context.beginPath()
				// 每5个刻度画一个长刻度线
				deltaY = i % 5 === 0 ? TICK_WIDTH : TICK_WIDTH / 2

				context.moveTo(AXIS_ORIGIN.x + i * HORIZONTAL_TICK_SPACING, AXIS_ORIGIN.y - deltaY)
				context.lineTo(AXIS_ORIGIN.x + i * HORIZONTAL_TICK_SPACING, AXIS_ORIGIN.y + deltaY)
				context.stroke()
			}
		}

		/**
		 * 绘制坐标轴标签
		 */
		let drawAxisLabels = () => {
			context.fillStyle = 'blue'
			drawHorizontalAxisLabels()
			drawVerticalAxisLabels()
		}

		/**
		 * 绘制x轴标签
		 */
		let drawHorizontalAxisLabels = () => {
			context.textAlign = 'center'
			context.textBaseline = 'top'

			// 每5个刻度添加一个标签
			for (let i = 0; i <= NUM_HORIZONTAL_TICKS; ++i) {
				if (i % 5 === 0) {
					context.fillText(
						i,
						AXIS_ORIGIN.x + i * HORIZONTAL_TICK_SPACING,
						AXIS_ORIGIN.y + SPACE_BETWEEN_LABELS_AND_AXIS
					)
				}
			}
		}

		/**
		 * 绘制y轴标签
		 */
		let drawVerticalAxisLabels = () => {
			context.textAlign = 'right'
			context.textBaseline = 'middle'

			// 每5个刻度添加一个标签
			for (let i = 0; i <= NUM_VERTICAL_TICKS; ++i) {
				if (i % 5 === 0) {
					context.fillText(i, AXIS_ORIGIN.x - SPACE_BETWEEN_LABELS_AND_AXIS, AXIS_ORIGIN.y - i * VERTICAL_TICK_SPACING)
				}
			}
		}

		// 初始化绘制
		drawGrid('lightgray', 10, 10) // 绘制背景网格
		drawAxis() // 绘制坐标轴
		drawAxisLabels() // 绘制坐标轴标签
	</script>
</html>

在圆弧周围绘制文本

应用程序通过如下步骤来绘制文本:

  1. 计算圆弧周围每个字符的绘制坐标
  2. 将坐标系平移至绘制字符的地点
  3. 将坐标系旋转 π/2 - angle 度
  4. 对字符进行描边或填充操作

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>3-9-沿着圆弧绘制文本</title>
	</head>
	<body>
		<canvas id="canvas" width="800" height="600"> canvas not supports </canvas>

		<script>
			const canvas = document.getElementById('canvas'),
				context = canvas.getContext('2d')

			const TEXT_FILL_STYLE = 'rgba(100, 130, 240, 0.5)',
				TEXT_STROKE_STYLE = 'rgba(255, 255, 255, 0.8)',
				TEXT_SIZE = 64

			const circle = {
				x: canvas.width / 2,
				y: canvas.height / 2,
				radius: 200,
			}

			// Functions ......
			const drawCircleCenterPoint = () => {
				context.save()

				context.beginPath()
				context.arc(circle.x, circle.y, 4, 0, Math.PI * 2, false)
				context.fillStyle = 'red'
				context.fill()

				context.restore()
			}

			/**
			 * 1. 计算圆弧周围每个字符的绘制坐标
			 * 2. 将坐标轴平移至绘制字符的地点
			 * 3. 旋转坐标轴,使字符绘制方向与圆弧方向一致 π/2 - angle
			 * 4. 对字符进行描边或填充操作
			 * */
			const drawCircleText = (text, startAngle, endAngle) => {
				const radius = circle.radius,
					angleDecrement = (startAngle - endAngle) / (text.length - 1)

				let character,
					angle = parseFloat(startAngle),
					index = 0

				context.save()

				context.fillStyle = TEXT_FILL_STYLE
				context.strokeStyle = TEXT_STROKE_STYLE
				context.font = `${TEXT_SIZE}px Arial`

				while (index < text.length) {
					character = text[index]

					context.save()
					context.beginPath()

					context.translate(circle.x + Math.cos(angle) * radius, circle.y - Math.sin(angle) * radius)
					context.rotate(Math.PI / 2 - angle)

					context.fillText(character, 0, 0)
					context.strokeText(character, 0, 0)

					angle -= angleDecrement
					index++

					context.restore()
				}
				context.restore()
			}

			// Initialization......
			drawCircleCenterPoint()

			context.textAlign = 'center'
			context.textBaseline = 'middle'

			drawCircleText('Clockwise around the circle', Math.PI * 2, Math.PI / 8)
		</script>
	</body>
</html>

网站公告

今日签到

点亮在社区的每一天
去签到