详细指南:在WordPress中开发集成在线简易PSD文件查看与标注工具
摘要
本文提供了一份详细的技术指南,介绍如何在WordPress平台中通过代码二次开发,集成一个在线简易PSD文件查看与标注工具。我们将从需求分析开始,逐步讲解技术选型、开发流程、核心功能实现以及优化建议,帮助开发者掌握在WordPress中扩展专业功能的方法。
目录
- 引言:为什么在WordPress中集成PSD查看与标注工具
- 技术选型与准备工作
- WordPress插件架构设计
- 前端PSD查看器实现
- 标注功能开发
- 用户权限与文件管理
- 性能优化与安全考虑
- 测试与部署
- 扩展功能建议
- 结论
1. 引言:为什么在WordPress中集成PSD查看与标注工具
1.1 WordPress作为内容管理平台的扩展性
WordPress作为全球最流行的内容管理系统,不仅用于博客和网站建设,其强大的插件机制和可扩展性使其成为各种专业应用的理想平台。通过二次开发,我们可以将专业工具集成到WordPress中,为用户提供一体化的解决方案。
1.2 PSD文件查看与标注的需求场景
对于设计团队、客户协作和在线教育等场景,能够直接在网页中查看PSD文件并进行标注可以极大提高工作效率:
- 设计师与客户之间的设计评审
- 团队内部的设计协作
- 在线设计课程的素材展示
- 设计稿版本对比与反馈收集
1.3 现有解决方案的局限性
虽然市场上有一些在线设计工具,但它们往往需要付费、功能过于复杂或无法与WordPress无缝集成。通过自主开发,我们可以创建轻量级、定制化的解决方案,完美融入现有WordPress环境。
2. 技术选型与准备工作
2.1 开发环境搭建
在开始开发前,需要准备以下环境:
# 本地开发环境
- WordPress 5.8+ 安装
- PHP 7.4+ 环境
- MySQL 5.6+ 或 MariaDB 10.1+
- 代码编辑器(VS Code、PHPStorm等)
- 浏览器开发者工具
2.2 核心技术选型
2.2.1 PSD解析库选择
考虑到PSD文件的复杂性,我们需要选择合适的解析库:
- PSD.js - 基于JavaScript的PSD解析器,适合前端处理
- ImageMagick/GraphicsMagick - 服务器端处理方案
- Photoshop API - Adobe官方API(功能强大但成本较高)
对于简易查看器,我们推荐使用PSD.js,因为它:
- 纯前端实现,减轻服务器负担
- 开源免费,社区活跃
- 支持图层提取和基本信息读取
2.2.2 标注工具库选择
- Fabric.js - 强大的Canvas操作库
- Konva.js - 另一个优秀的Canvas库
- 自定义Canvas实现 - 更轻量但开发成本高
我们选择Fabric.js,因为它提供了丰富的图形对象和交互功能。
2.2.3 WordPress开发框架
我们将采用标准的WordPress插件开发模式:
- 遵循WordPress编码标准
- 使用WordPress REST API进行前后端通信
- 利用WordPress的媒体库进行文件管理
2.3 插件基础结构
创建插件基础目录结构:
wp-psd-viewer-annotator/
├── wp-psd-viewer-annotator.php # 主插件文件
├── includes/ # 核心功能文件
│ ├── class-psd-handler.php # PSD处理类
│ ├── class-annotation-manager.php # 标注管理类
│ └── class-file-manager.php # 文件管理类
├── admin/ # 后台管理文件
│ ├── css/
│ ├── js/
│ └── views/
├── public/ # 前端文件
│ ├── css/
│ ├── js/
│ └── views/
├── assets/ # 静态资源
│ ├── psd.js # PSD解析库
│ └── fabric.js # 标注库
└── languages/ # 国际化文件
3. WordPress插件架构设计
3.1 主插件文件结构
<?php
/**
* Plugin Name: PSD Viewer & Annotator for WordPress
* Plugin URI: https://yourwebsite.com/
* Description: 在WordPress中查看和标注PSD文件的工具
* Version: 1.0.0
* Author: Your Name
* License: GPL v2 or later
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('PSD_VA_VERSION', '1.0.0');
define('PSD_VA_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('PSD_VA_PLUGIN_URL', plugin_dir_url(__FILE__));
define('PSD_VA_MAX_FILE_SIZE', 104857600); // 100MB
// 自动加载类文件
spl_autoload_register(function ($class_name) {
$prefix = 'PSD_VA_';
$base_dir = PSD_VA_PLUGIN_DIR . 'includes/';
if (strpos($class_name, $prefix) !== 0) {
return;
}
$relative_class = substr($class_name, strlen($prefix));
$file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// 初始化插件
function psd_va_init() {
// 检查依赖
if (!function_exists('gd_info')) {
add_action('admin_notices', function() {
echo '<div class="notice notice-error"><p>PSD查看器需要GD库支持,请启用PHP的GD扩展。</p></div>';
});
return;
}
// 初始化核心类
$psd_handler = new PSD_VA_PSD_Handler();
$annotation_manager = new PSD_VA_Annotation_Manager();
$file_manager = new PSD_VA_File_Manager();
// 注册短代码
add_shortcode('psd_viewer', array($psd_handler, 'shortcode_handler'));
// 注册REST API端点
add_action('rest_api_init', array($annotation_manager, 'register_rest_routes'));
// 注册管理菜单
add_action('admin_menu', 'psd_va_admin_menu');
}
add_action('plugins_loaded', 'psd_va_init');
// 管理菜单
function psd_va_admin_menu() {
add_menu_page(
'PSD查看器',
'PSD查看器',
'manage_options',
'psd-viewer',
'psd_va_admin_page',
'dashicons-format-image',
30
);
}
function psd_va_admin_page() {
include PSD_VA_PLUGIN_DIR . 'admin/views/admin-page.php';
}
// 激活/停用钩子
register_activation_hook(__FILE__, 'psd_va_activate');
register_deactivation_hook(__FILE__, 'psd_va_deactivate');
function psd_va_activate() {
// 创建必要的数据库表
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$annotations_table = $wpdb->prefix . 'psd_va_annotations';
$sql = "CREATE TABLE IF NOT EXISTS $annotations_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
psd_id bigint(20) NOT NULL,
user_id bigint(20) NOT NULL,
annotation_data longtext NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY psd_id (psd_id),
KEY user_id (user_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 设置默认选项
add_option('psd_va_max_file_size', PSD_VA_MAX_FILE_SIZE);
add_option('psd_va_allowed_roles', array('administrator', 'editor', 'author'));
}
function psd_va_deactivate() {
// 清理临时文件
$upload_dir = wp_upload_dir();
$temp_dir = $upload_dir['basedir'] . '/psd-va-temp/';
if (is_dir($temp_dir)) {
array_map('unlink', glob($temp_dir . '*'));
rmdir($temp_dir);
}
}
3.2 数据库设计
我们需要创建以下数据库表来存储标注信息:
-- 标注数据表
CREATE TABLE wp_psd_va_annotations (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
psd_id BIGINT(20) NOT NULL, -- 关联的PSD文件ID
user_id BIGINT(20) NOT NULL, -- 创建标注的用户ID
annotation_data LONGTEXT NOT NULL, -- 标注数据(JSON格式)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX psd_id_idx (psd_id),
INDEX user_id_idx (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4. 前端PSD查看器实现
4.1 引入必要的JavaScript库
// 在插件中注册脚本
function psd_va_enqueue_scripts() {
// 前端样式
wp_enqueue_style(
'psd-va-frontend',
PSD_VA_PLUGIN_URL . 'public/css/frontend.css',
array(),
PSD_VA_VERSION
);
// 核心库
wp_enqueue_script(
'psd-js',
PSD_VA_PLUGIN_URL . 'assets/js/psd.min.js',
array(),
'0.8.0',
true
);
wp_enqueue_script(
'fabric-js',
PSD_VA_PLUGIN_URL . 'assets/js/fabric.min.js',
array(),
'4.5.0',
true
);
// 主脚本
wp_enqueue_script(
'psd-va-main',
PSD_VA_PLUGIN_URL . 'public/js/main.js',
array('jquery', 'psd-js', 'fabric-js'),
PSD_VA_VERSION,
true
);
// 本地化脚本
wp_localize_script('psd-va-main', 'psd_va_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('psd_va_nonce'),
'rest_url' => rest_url('psd-va/v1/'),
'max_file_size' => get_option('psd_va_max_file_size', PSD_VA_MAX_FILE_SIZE)
));
}
add_action('wp_enqueue_scripts', 'psd_va_enqueue_scripts');
4.2 PSD文件解析与显示
// public/js/main.js - PSD查看器核心功能
class PSDViewer {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.options = Object.assign({
psdUrl: '',
width: 800,
height: 600,
showLayers: true,
allowDownload: true
}, options);
this.canvas = null;
this.psd = null;
this.layers = [];
this.currentScale = 1;
this.init();
}
async init() {
// 创建UI结构
this.createUI();
// 加载PSD文件
if (this.options.psdUrl) {
await this.loadPSD(this.options.psdUrl);
}
}
createUI() {
// 创建主容器
this.container.innerHTML = `
<div class="psd-viewer-container">
<div class="psd-toolbar">
<button class="tool-btn zoom-in" title="放大">+</button>
<button class="tool-btn zoom-out" title="缩小">-</button>
<button class="tool-btn reset-zoom" title="重置缩放">1:1</button>
<span class="zoom-level">100%</span>
<div class="tool-separator"></div>
<button class="tool-btn toggle-layers" title="显示/隐藏图层">图层</button>
<button class="tool-btn download-image" title="下载为PNG">下载</button>
</div>
<div class="psd-main-area">
<div class="psd-canvas-container">
<canvas id="psd-canvas-${this.container.id}"></canvas>
</div>
<div class="psd-layers-panel">
<h3>图层</h3>
<div class="layers-list"></div>
</div>
</div>
<div class="psd-status-bar">
<span class="file-info"></span>
<span class="canvas-size"></span>
</div>
</div>
`;
// 获取Canvas元素
this.canvas = document.getElementById(`psd-canvas-${this.container.id}`);
this.ctx = this.canvas.getContext('2d');
// 绑定事件
this.bindEvents();
}
async loadPSD(url) {
try {
// 显示加载状态
this.showLoading();
// 获取PSD文件
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
// 解析PSD
this.psd = PSD.fromArrayBuffer(arrayBuffer);
this.psd.parse();
// 渲染PSD
this.renderPSD();
// 提取图层信息
this.extractLayers();
// 更新UI
this.updateFileInfo();
} catch (error) {
console.error('加载PSD失败:', error);
this.showError('无法加载PSD文件: ' + error.message);
}
}
renderPSD() {
if (!this.psd) return;
// 获取PSD尺寸
const width = this.psd.header.width;
const height = this.psd.header.height;
// 设置Canvas尺寸
this.canvas.width = width;
this.canvas.height = height;
// 渲染到Canvas
const imageData = this.psd.image.toCanvas();
this.ctx.drawImage(imageData, 0, 0);
// 更新Canvas显示尺寸
this.fitToContainer();
}
extractLayers() {
if (!this.psd || !this.options.showLayers) return;
this.layers = [];
const extractLayerInfo = (layer, depth = 0) => {
if (layer.visible === false) return;
const layerInfo = {
id: layer.id || Math.random().toString(36).substr(2, 9),
name: layer.name || '未命名图层',
visible: layer.visible,
opacity: layer.opacity,
depth: depth,
children: []
};
if (layer.children && layer.children.length > 0) {
layer.children.forEach(child => {
extractLayerInfo(child, depth + 1);
});
}
this.layers.push(layerInfo);
};
extractLayerInfo(this.psd.tree());
this.renderLayersList();
}
renderLayersList() {
const layersList = this.container.querySelector('.layers-list');
layersList.innerHTML = '';
this.layers.forEach(layer => {
const layerItem = document.createElement('div');
layerItem.className = 'layer-item';
layerItem.style.paddingLeft = (layer.depth * 20) + 'px';
layerItem.innerHTML = `
<label>
<input type="checkbox" ${layer.visible ? 'checked' : ''}
data-layer-id="${layer.id}">
${layer.name}
</label>
`;
layersList.appendChild(layerItem);
});
}
fitToContainer() {
const container = this.canvas.parentElement;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const psdWidth = this.canvas.width;
const psdHeight = this.canvas.height;
// 计算适合容器的缩放比例
const scaleX = containerWidth / psdWidth;
const scaleY = containerHeight / psdHeight;
this.currentScale = Math.min(scaleX, scaleY, 1);
// 应用缩放
this.canvas.style.width = (psdWidth * this.currentScale) + 'px';
this.canvas.style.height = (psdHeight * this.currentScale) + 'px';
// 更新缩放显示
this.updateZoomDisplay();
}
updateZoomDisplay() {
const zoomElement = this.container.querySelector('.zoom-level');
if (zoomElement) {
zoomElement.textContent = Math.round(this.currentScale * 100) + '%';
}
}
bindEvents() {
// 缩放按钮
this.container.querySelector('.zoom-in').addEventListener('click', () => {
this.zoom(0.1);
});
this.container.querySelector('.zoom-out').addEventListener('click', () => {
this.zoom(-0.1);
});
this.container.querySelector('.reset-zoom').addEventListener('click', () => {
this.currentScale = 1;
this.canvas.style.width = this.canvas.width + 'px';
.style.height = this.canvas.height + 'px';
this.updateZoomDisplay();
});
// 图层显示/隐藏
this.container.querySelector('.toggle-layers').addEventListener('click', () => {
const panel = this.container.querySelector('.psd-layers-panel');
panel.classList.toggle('collapsed');
});
// 下载功能
this.container.querySelector('.download-image').addEventListener('click', () => {
this.downloadAsPNG();
});
// 图层复选框事件委托
this.container.querySelector('.layers-list').addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
const layerId = e.target.dataset.layerId;
this.toggleLayerVisibility(layerId, e.target.checked);
}
});
// Canvas拖拽和缩放
this.setupCanvasInteractions();
}
zoom(delta) {
this.currentScale = Math.max(0.1, Math.min(5, this.currentScale + delta));
this.canvas.style.width = (this.canvas.width * this.currentScale) + 'px';
this.canvas.style.height = (this.canvas.height * this.currentScale) + 'px';
this.updateZoomDisplay();
}
downloadAsPNG() {
const link = document.createElement('a');
link.download = 'psd-export.png';
link.href = this.canvas.toDataURL('image/png');
link.click();
}
toggleLayerVisibility(layerId, visible) {
// 这里可以实现图层显示/隐藏逻辑
console.log(`图层 ${layerId} 可见性: ${visible}`);
// 实际实现需要重新渲染PSD并隐藏/显示特定图层
}
setupCanvasInteractions() {
let isDragging = false;
let lastX = 0;
let lastY = 0;
this.canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastX = e.clientX;
lastY = e.clientY;
this.canvas.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - lastX;
const deltaY = e.clientY - lastY;
// 更新Canvas位置
const currentLeft = parseInt(this.canvas.style.left || 0);
const currentTop = parseInt(this.canvas.style.top || 0);
this.canvas.style.left = (currentLeft + deltaX) + 'px';
this.canvas.style.top = (currentTop + deltaY) + 'px';
lastX = e.clientX;
lastY = e.clientY;
});
document.addEventListener('mouseup', () => {
isDragging = false;
this.canvas.style.cursor = 'grab';
});
// 鼠标滚轮缩放
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
this.zoom(delta);
});
}
showLoading() {
this.container.querySelector('.psd-canvas-container').innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
<p>加载PSD文件中...</p>
</div>
`;
}
showError(message) {
this.container.querySelector('.psd-canvas-container').innerHTML = `
<div class="error-message">
<p>${message}</p>
<button class="retry-btn">重试</button>
</div>
`;
// 重试按钮事件
this.container.querySelector('.retry-btn').addEventListener('click', () => {
this.loadPSD(this.options.psdUrl);
});
}
updateFileInfo() {
if (!this.psd) return;
const fileInfo = this.container.querySelector('.file-info');
const canvasSize = this.container.querySelector('.canvas-size');
if (fileInfo) {
fileInfo.textContent = `尺寸: ${this.psd.header.width} × ${this.psd.header.height} 像素 | 颜色模式: ${this.psd.header.mode}`;
}
if (canvasSize) {
canvasSize.textContent = `缩放: ${Math.round(this.currentScale * 100)}%`;
}
}
}
// 初始化查看器
document.addEventListener('DOMContentLoaded', function() {
const psdContainers = document.querySelectorAll('.psd-viewer');
psdContainers.forEach(container => {
const psdUrl = container.dataset.psdUrl;
const options = {
psdUrl: psdUrl,
showLayers: container.dataset.showLayers !== 'false',
allowDownload: container.dataset.allowDownload !== 'false'
};
new PSDViewer(container.id, options);
});
});
### 4.3 前端样式设计
/ public/css/frontend.css /
.psd-viewer-container {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
.psd-toolbar {
background: #fff;
border-bottom: 1px solid #ddd;
padding: 10px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.tool-btn {
padding: 6px 12px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.tool-btn:hover {
background: #e0e0e0;
border-color: #999;
}
.tool-separator {
width: 1px;
height: 20px;
background: #ddd;
margin: 0 10px;
}
.zoom-level {
font-size: 14px;
color: #666;
min-width: 50px;
}
.psd-main-area {
flex: 1;
display: flex;
overflow: hidden;
}
.psd-canvas-container {
flex: 1;
position: relative;
overflow: auto;
background:
linear-gradient(45deg, #eee 25%, transparent 25%),
linear-gradient(-45deg, #eee 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #eee 75%),
linear-gradient(-45deg, transparent 75%, #eee 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
psd-canvas {
display: block;
position: absolute;
top: 0;
left: 0;
cursor: grab;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.psd-layers-panel {
width: 250px;
background: #fff;
border-left: 1px solid #ddd;
padding: 15px;
overflow-y: auto;
transition: width 0.3s;
}
.psd-layers-panel.collapsed {
width: 0;
padding: 0;
border: none;
overflow: hidden;
}
.psd-layers-panel h3 {
margin-top: 0;
margin-bottom: 15px;
font-size: 16px;
color: #333;
}
.layers-list {
max-height: 400px;
overflow-y: auto;
}
.layer-item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.layer-item label {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
}
.layer-item input[type="checkbox"] {
margin-right: 8px;
}
.psd-status-bar {
background: #fff;
border-top: 1px solid #ddd;
padding: 8px 15px;
display: flex;
justify-content: space-between;
font-size: 12px;
color: #666;
flex-shrink: 0;
}
.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #e74c3c;
}
.retry-btn {
margin-top: 10px;
padding: 8px 16px;
background: #3498db;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
/ 响应式设计 /
@media (max-width: 768px) {
.psd-viewer-container {
height: 400px;
}
.psd-layers-panel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
z-index: 100;
}
.psd-toolbar {
flex-wrap: wrap;
gap: 5px;
}
}
## 5. 标注功能开发
### 5.1 标注工具类实现
// public/js/annotation.js
class PSDAnnotator {
constructor(canvasElement, options = {}) {
this.canvas = canvasElement;
this.fabricCanvas = null;
this.annotations = [];
this.currentTool = 'select';
this.currentColor = '#ff0000';
this.currentStrokeWidth = 2;
this.options = Object.assign({
enableText: true,
enableArrow: true,
enableRectangle: true,
enableCircle: true,
enableFreeDraw: true
}, options);
this.initFabricCanvas();
this.setupAnnotationTools();
}
initFabricCanvas() {
// 创建Fabric.js Canvas
this.fabricCanvas = new fabric.Canvas(this.canvas, {
selection: true,
preserveObjectStacking: true,
backgroundColor: 'transparent'
});
// 设置Canvas尺寸与底层PSD Canvas一致
const psdCanvas = document.getElementById(this.canvas.id.replace('annotation-', ''));
if (psdCanvas) {
this.fabricCanvas.setWidth(psdCanvas.width);
this.fabricCanvas.setHeight(psdCanvas.height);
this.fabricCanvas.setDimensions({
width: psdCanvas.style.width,
height: psdCanvas.style.height
});
}
// 绑定事件
this.bindCanvasEvents();
}
setupAnnotationTools() {
this.tools = {
select: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = true;
this.fabricCanvas.defaultCursor = 'default';
},
text: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'text';
this.fabricCanvas.on('mouse:down', (options) => {
if (options.target) return;
const point = this.fabricCanvas.getPointer(options.e);
const text = new fabric.IText('输入文字', {
left: point.x,
top: point.y,
fontSize: 16,
fill: this.currentColor,
fontFamily: 'Arial'
});
this.fabricCanvas.add(text);
this.fabricCanvas.setActiveObject(text);
text.enterEditing();
text.selectAll();
});
},
rectangle: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'crosshair';
let rect, isDown, origX, origY;
this.fabricCanvas.on('mouse:down', (options) => {
isDown = true;
const pointer = this.fabricCanvas.getPointer(options.e);
origX = pointer.x;
origY = pointer.y;
rect = new fabric.Rect({
left: origX,
top: origY,
width: 0,
height: 0,
fill: 'transparent',
stroke: this.currentColor,
strokeWidth: this.currentStrokeWidth
});
this.fabricCanvas.add(rect);
});
this.fabricCanvas.on('mouse:move', (options) => {
if (!isDown) return;
const pointer = this.fabricCanvas.getPointer(options.e);
if (origX > pointer.x) {
rect.set({ left: pointer.x });
}
if (origY > pointer.y) {
rect.set({ top: pointer.y });
}
rect.set({
width: Math.abs(origX - pointer.x),
height: Math.abs(origY - pointer.y)
});
this.fabricCanvas.renderAll();
});
this.fabricCanvas.on('mouse:up', () => {
isDown = false;
this.saveAnnotation();
});
},
circle: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'crosshair';
let circle, isDown, origX, origY;
this.fabricCanvas.on('mouse:down', (options) => {
isDown = true;
const pointer = this.fabricCanvas.getPointer(options.e);
origX = pointer.x;
origY = pointer.y;
circle = new fabric.Circle({
left: origX,
top: origY,
radius: 0,
fill: 'transparent',
stroke: this.currentColor,
strokeWidth: this.currentStrokeWidth
});
this.fabricCanvas.add(circle);
});
this.fabricCanvas.on('mouse:move', (options) => {
if (!isDown) return;
const pointer = this.fabricCanvas.getPointer(options.e);
const radius = Math.sqrt(
Math.pow(origX - pointer.x, 2) +
Math.pow(origY - pointer.y, 2)
) / 2;
circle.set({
radius: radius,
left: origX - radius,
top: origY - radius
});
this.fabricCanvas.renderAll();
});
this.fabricCanvas.on('mouse:up', () => {
isDown = false;
this.saveAnnotation();
});
},
arrow: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'crosshair';
let line, isDown, origX, origY;
this.fabricCanvas.on('mouse:down', (options) => {
isDown = true;
const pointer = this.fabricCanvas.getPointer(options.e);
origX = pointer.x;
origY = pointer.y;
line = new fabric.Line([origX, origY, origX, origY], {
stroke: this.currentColor,
strokeWidth: this.currentStrokeWidth,
fill: this.currentColor,
strokeLineCap: 'round',
strokeLineJoin: 'round'
});
this.fabricCanvas.add(line);
});
this.fabricCanvas.on('mouse:move', (options) => {
if (!isDown) return;
const pointer = this.fabricCanvas.getPointer(options.e);
line.set({ x2: pointer.x, y2: pointer.y });
// 添加箭头头部
this.addArrowHead(line, origX, origY, pointer.x, pointer.y);
this.fabricCanvas.renderAll();
});
this.fabricCanvas.on('mouse:up', () => {
isDown = false;
this.saveAnnotation();
});
},
freedraw: () => {
this.fabricCanvas.isDrawingMode = true;
this.fabricCanvas.freeDrawingBrush = new fabric.PencilBrush(this.fabricCanvas);
this.fabricCanvas.freeDrawingBrush.color = this.currentColor;
this.fabricCanvas.freeDrawingBrush.width = this.currentStrokeWidth;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'crosshair';
this.fabricCanvas.on('path:created', () => {
this.saveAnnotation();
});
}
};
}
addArrowHead(line, x1, y1, x2, y2) {
// 移除旧的箭头头部
const objects = this.fabricCanvas.getObjects();
objects.forEach(obj => {
if (obj.arrowHead) {
this.fabricCanvas.remove(obj);
}
});
// 计算箭头角度
const angle = Math.atan2(y2 - y1, x2 - x1);
const headLength = 15;
// 创建箭头头部
const arrowHead = new fabric.Triangle({
left: x2,
top: y2,
angle: angle * 180 / Math.PI,
fill: this.currentColor,
width: headLength,
height: headLength,
originX: 'center',
originY: 'center',
arrowHead: true
});
this.fabricCanvas.add(arrowHead);
line.arrowHead
