65.9K
CodeProject 正在变化。 阅读更多。
Home

Html5 图像标记

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (35投票s)

2014年7月26日

CPOL

8分钟阅读

viewsIcon

99079

downloadIcon

2345

轻松地为您的图像添加注释功能。

下载 ImageMarkup 源码 - 1.5 MB

Image Markup

图 1. Image Markup 插件演示,展示了绘图和文本功能。

引言

无数次,我需要从互联网上的某个地方下载一张图片,用某个图像编辑应用程序打开它,添加一些标记,保存图片,然后再在别处使用它。我认为,对于某些类型的网站来说,如果网站本身允许用户在线注解图片(包括文本和绘图),然后将组合后的图片本地保存或发送到互联网上的某个地方,那将是非常棒的。本文介绍了一个实现了这一功能的 jQuery 插件——至少是以一种简化的方式。

背景

Image Markup 插件的构想最近在与我们的 CEO Marco Raduan 在 ILang Educação 的一次谈话中产生,他向我们展示了一个很棒的 Mac OS 功能,该功能允许直接在图像本身上进行图像注解,而无需单独的应用程序。这种直接注解被认为是我们学习管理系统 (ILang) 的一项有价值的功能,教师可以在评估学生手写的图像扫描开放式答案的同时对其进行注解。这些视觉注解可以与分配给学生评估的分数一起持久化到数据库中,以代表开放式问题评估的整个过程。

系统依赖

该应用程序将 100% 驻留在客户端浏览器中。它使用 JavaScript 编写,并依赖于四个主要框架/库:

  • jQuery:不出所料,由于 Image Markup 是一个 jQuery 插件,没有预先加载的 jquery 框架它就永远无法工作。这里的简洁性确实是成功的关键。

  • Paper JSPaper JS 作为矢量图形库提供的工具箱让我惊叹不已。PaperJS 在 HTML5 canvas 上完成了绘图/文本的繁重工作,否则我将花费无数个小时的辛勤劳动。

  • ContextMenu:实现上下文菜单(通过右键鼠标按钮访问的那个)是一种不错且干净的方式来显示菜单选项,而无需污染屏幕上的工具和视觉快捷方式。我认为,Rodney Rehm、Christian Baartse 和 Addy Osmani 实现的上下文菜单 jQuery 插件 以其简洁和强大的自定义能力而著称

  • CommandManager:绘图和文本书写有时会导致错误,因此在用户进行修改时,拥有 撤销重做 命令非常重要。Alexander Brevig 以 JavaScript 实现了一个简单而有效的命令模式

使用代码

作为任何优秀的 jQuery 插件,Image Markup 只需要一行 JavaScript 初始化代码,那就是设置 IMAGE 元素,在这个元素之上将构建 CANVAS 注解层。例如:

var markup = $('#myImage').imageMarkup();
		

上面的代码行将创建一个 Image Markup 实例,该实例附加到 ID 为 myImageIMAGE 元素上。

用户可能会发现将 Image Markup 实例附加到多个 IMAGE 元素上很有帮助。在下面的例子中,一个 Image Markup 实例将被附加到“img-container”类元素内的每个 IMAGE 元素上。

var markup = $('.img-container img').imageMarkup();
		

覆盖默认选项

可以覆盖某些属性以自定义插件的工作方式。它们是:

 

  • color: 绘图和文本元素的颜色。这个可以在你开始工作时更改。
  • width: 你可以预定义你的绘图有多粗。这显然只适用于绘图,不适用于文本元素。
  • opacity: 因为图像上的绘图和文本可能会让人混淆,有时为注解添加一点透明度很有用。因此,你可以选择一个介于 0.0 到 1.0 之间的不透明度。

 

下面的代码行指定了一个 Image Markup 实例,使用红色笔,厚度为 4 像素,不透明度为 50%。

 

var markup = $('.img-container img').imageMarkup({color: 'red', width: 10, opacity: .5});
			

 

一切都关乎图层

注解不应修改底层图像。不是因为我们想保留原始图像,而是因为我们想将注解作为独立实体,然后将其保存到浏览器的本地存储,或通过 Web 服务下载或传输。

独立的注解图层还有其他优点:由于它将元素集封装在 JavaScript 对象结构中,你可以轻松地将其序列化为 JSON 格式,并将注解持久化到数据库表中的字符串列中。反过来,你以后可以检索相同的 JSON 字符串并恢复注解。

图 2. 图像层 (A) 在带有注解 (B) 的透明画布下生成了一个新的复合图像 (C)。

初始化后,Image Markup 插件会创建一个新的 HTML5 canvas 元素,该元素覆盖整个图像。这个 canvas 会作为图像元素的同胞元素附加,因此它们共享相同的父元素。

在图像上绘图

该插件提供自由手绘功能:在画布上拖动鼠标会留下直线段的痕迹,当用户释放鼠标按钮时,这些痕迹会变成平滑的涂鸦。用户可以绘制无限数量的点和独立的线条。

当用户按下鼠标按钮时,它就开始了。此时,将创建一个新的 Path 对象实例,并将默认设置应用于该路径(颜色、宽度和不透明度)。

tool.onMouseDown = function (event) {

	switch (event.event.button) {
		// leftclick
		case 0:
			// If we produced a path before, deselect it:
			if (path) {
				path.selected = false;
			}

			path = new paper.Path();
			path.data.id = generateUUID();
			path.strokeColor = settings.color;
			path.strokeWidth = settings.width;
			path.opacity = settings.opacity;
			break;
			// rightclick
		case 2:
			break;
	}
}	
		

下面,拖动效果由 Paper JS 库的 tool 对象的 onMouseDrag 事件的实现检测和处理:当用户拖动鼠标时,新的点会被添加到当前的 path 对象中。

tool.onMouseDrag = function (event) {
	switch (event.event.button) {
		// leftclick
		case 0:
			// Every drag event, add a point to the path at the current
			// position of the mouse:

			if (selectedItem) {
				.
				[DRAG (MOVE) THE SELECTED ITEM]
				.
			}
			else if (path)
				path.add(event.point);
			break;
			// rightclick
		case 2:
			break;
	}
}
		

现在订阅了 onMouseUp 事件,当用户释放鼠标按钮时,路径将被简化(即,段点之间的线条会被平滑处理)。

tool.onMouseUp = function (event) {
	switch (event.event.button) {
		// leftclick
		case 0:
			if (selectedItem) {
				.
				[STOP DRAGGING THE SELECTED ITEM]
				.
			}
			else {
				// When the mouse is released, simplify it:
				path.simplify();
				.
				[SAVE THE PATH COMMAND IN COMMAND MANAGER]
				.
			}
			break;
			// rightclick
			.
			.
			.
	}
}	
		

图 3. 每个“路径”或“涂鸦”仅由几个点组成,但它们之间的线段被平滑处理,看起来不像直线,而是更像手写体。

用户可以更改画笔颜色,但只能从有限的集合中选择:黑色、红色、绿色和黄色。

this.setPenColor = function (color) {
	self.setOptions({ color: color });
	$('.image-markup-canvas').css('cursor', "url(img/" + color + "-pen.png) 14 50, auto");
}
		

如前所述,透明度厚度属性是在 JavaScript 初始化代码中定义的。用户无法更改它们。

图像上的文本

此工具以“先创建,后修改”的方式工作。用户可以通过点击上下文菜单中的 **文本** 工具来添加文本注解,并在画布上放置一个带有默认消息的新文本元素。通过双击该元素并在浏览器的输入对话框中输入新文本,可以编辑该文本。文本颜色将与绘图笔颜色相同。

注意,文本工具是如何通过 setText 函数在 ContextMenu 中设置的。

$.contextMenu({
	selector: '.image-markup-canvas',
	callback: function (key, options) {
		switch (key) {
		.
		.
		.
			case 'text':
				self.setText();
				break;
		.
		.
		.
		}
	},
	items: {
	.
	.
	.
		"text": { name: "Text", icon: "text" },
	.
	.
	.
	}
});
		

默认设置将应用于新的 PointText 对象实例,并且 onDoubleClick 函数会准备浏览器的默认输入对话框,以向用户询问新文本。

this.setText = function () {
	var uid = generateUUID();
	var pos = contextPoint;
	CommandManager.execute({
		execute: function () {
			var TXT_DBL_CLICK = "<<double click to edit>>";
			var txt = TXT_DBL_CLICK;
			var text = new paper.PointText(pos);
			text.content = txt;
			text.fillColor = settings.color;
			text.fontSize = 18;
			text.fontFamily = 'Verdana';
			text.data.uid = uid;
			text.opacity = settings.opacity;

			text.onDoubleClick = function (event) {
				if (this.className == 'PointText') {
					var txt = prompt("Type in your text", this.content.replace(TXT_DBL_CLICK, ''));
					if (txt.length > 0)
						this.content = txt;
				}
			}
		},
		unexecute: function () {
			$(paper.project.activeLayer.children).each(function (index, item) {
				if (item.data && item.data.uid) {
					if (item.data.uid == uid) {
						item.remove();
					}
				}
			});
		}
	});
}
		

图 4. 通过选择文本菜单项然后双击文本,用户可以将文本注解放置在图像上。

选择项目

通过将鼠标悬停在项目上即可选择该项目:随后将显示一系列段处理程序,指示已选择的项目。

图 5. 显示所选元素的路径段。

当用户将鼠标悬停在元素(路径或文本)上时(不拖动),Paper JS 库的 tool 对象的 onMouseMove 函数会将该元素设置为选中状态,同时取消选中画布上的所有其他元素。

tool.onMouseMove = function (event) {
	if (!$('.context-menu-list').is(':visible')) {
		position = event.point;
		paper.project.activeLayer.selected = false;
		self.setPenColor(settings.color);
		if (event.item) {
			event.item.selected = true;
			selectedItem = event.item;
			self.setCursorHandOpen();
		}
		else {
			selectedItem = null;
		}
	}
}
		

目前,Image Markup 插件不支持多选。但是我打算在未来的版本中实现此功能。

删除项目

可以通过菜单轻松删除注解元素。 **擦除** 菜单将删除选中的项目(如果存在)。否则,它将删除所有画布元素。

在画布元素集合中搜索选中的元素,并在找到后将其移除。请注意,该操作是通过 CommandManager 对象完成的,以便以后可以根据用户请求撤销。

this.erase = function () {
	var strPathArray = new Array();
	$(paper.project.activeLayer.children).each(function (index, item) {
		if (contextSelectedItemId) {
			if (contextSelectedItemId.length == 0 || item.data.id == contextSelectedItemId) {
				var strPath = item.exportJSON({ asString: true });
				strPathArray.push(strPath);
			}
		}
	});

	CommandManager.execute({
		execute: function () {
			$(paper.project.activeLayer.children).each(function (index, item) {
				if (contextSelectedItemId) {
					if (contextSelectedItemId.length == 0 || item.data.id == contextSelectedItemId) {
						item.remove();
					}
				}
			});
		},
		unexecute: function () {
			$(strPathArray).each(function (index, strPath) {
				path = new paper.Path();
				path.importJSON(strPath);
			});
		}
	});
}
		

图 6. 擦除选定的注解项。

移动项目

您可以通过在整个画布上进行拖放来移动单个元素(绘图或文本)。但是,当前版本不允许同时移动多个元素。

当用户释放鼠标按钮时,选中的元素将被放置在其最终位置,CommandManager 将被告知记录此操作,以便以后可以撤销。

tool.onMouseUp = function (event) {
	switch (event.event.button) {
		// leftclick
		case 0:
			if (selectedItem) {
				if (mouseDownPoint) {
					var selectedItemId = selectedItem.id;
					var draggingStartPoint = { x: mouseDownPoint.x, y: mouseDownPoint.y };
					CommandManager.execute({
						execute: function () {
							//item was already moved, so do nothing
						},
						unexecute: function () {
							$(paper.project.activeLayer.children).each(function (index, item) {
								if (item.id == selectedItemId) {
									if (item.segments) {
										var middlePoint = new paper.Point(
												((item.segments[item.segments.length - 1].point.x) - item.segments[0].point.x) / 2,
												((item.segments[item.segments.length - 1].point.y) - item.segments[0].point.y) / 2
											);
										item.position =
											new paper.Point(draggingStartPoint.x, draggingStartPoint.y);
									}
									else {
										item.position = draggingStartPoint;
									}
									return false;
								}
							});
						}
					});
					mouseDownPoint = null;
					.
					.
					.
		

下载合并的图像

您可以通过在上下文菜单中点击 **下载** 菜单项,将合成图像(即源图像加上绘图和文本注解)下载为单个图像。图像将直接进入您的 **下载** 文件夹(或您分配为浏览器默认下载文件夹的任何文件夹)。

$.contextMenu({
	selector: '.image-markup-canvas',
	callback: function (key, options) {
		switch (key) {
			//COMMANDS
			.
			.
			.
			case 'download':
				self.download();
				break;
			.
			.
			.						
		}
	},
	items: {
	.
	.
	.
	"download": { name: "Download", icon: "download" },
	.
	.
	.
	}
});
		

在下面的代码中,mergedContext 被创建为 Canvas html 元素的实例(虽然没有附加到 HTML 页面,因此不可见)。然后,调用两次 drawImage 方法:一次用于绘制底层图像,另一次用于绘制包含图像注解的画布(这些注解之前已通过 Paper JS 库创建)。

this.download = function () {
	var canvas = paper.project.activeLayer.view.element;
	var img = $(canvas).parent().find('img')[0];
	var mergeCanvas = $('<canvas>')
	.attr({
		width: $(img).width(),
		height: $(img).height()
	});

	var mergedContext = mergeCanvas[0].getContext('2d');
	mergedContext.clearRect(0, 0, $(img).width(), $(img).height());
	mergedContext.drawImage(img, 0, 0);
	mergedContext.drawImage(canvas, 0, 0);
	self.downloadCanvas(mergeCanvas[0], "image-markup.png");
}
		

实际的下载代码是通过 Ken Fyrstenberg 提供的巧妙解决方案 实现的,该解决方案模拟了对 anchor (<a>) 元素的点击事件。

this.downloadCanvas = function (canvas, filename) {
	/// create an "off-screen" anchor tag
	var lnk = document.createElement('a'),
		e;

	/// the key here is to set the download attribute of the a tag
	lnk.download = filename;

	/// convert canvas content to data-uri for link. When download
	/// attribute is set the content pointed to by link will be
	/// pushed as "download" in HTML5 capable browsers
	lnk.href = canvas.toDataURL();

	/// create a "fake" click-event to trigger the download
	if (document.createEvent) {

		e = document.createEvent("MouseEvents");
		e.initMouseEvent("click", true, true, window,
						 0, 0, 0, 0, 0, false, false, false,
						 false, 0, null);

		lnk.dispatchEvent(e);

	} else if (lnk.fireEvent) {

		lnk.fireEvent("onclick");
	}
}
		

插件的下一个版本将允许下载注解图像(一个带有透明背景的绘图和文本的 .png 图像),以便程序员可以将其放置在原始图像之上并获得合成图像。

结论

正如您所见,应用程序还有很大的改进空间。Paper.js 框架证明了其在处理复杂的图形脚本和事件处理方面的能力,同时提供了简洁简化的类和事件集。

此外,该应用程序目前可能对于通用目的来说功能过多,但我认为最大的潜力在于您如何根据项目的特定需求来调整它。例如,它可以是一个严格的教育网站,教师可以在线评估学生完成的作品。或者它可以是一个娱乐游戏,孩子们需要找出成对图像之间的差异。甚至可以是一个协作应用程序(类似于 Google Hangouts),供在线会议中的远程用户使用。

如果您有任何评论、投诉或建议,请在下方留言。我很想听听您的意见,并愿意在新的想法出现时改进应用程序。

历史

2014-07-26: 第一个版本。

© . All rights reserved.