首页 / 应用软件 / 详细指南,在WordPress中开发集成在线简易PSD文件查看与标注工具

详细指南,在WordPress中开发集成在线简易PSD文件查看与标注工具

详细指南:在WordPress中开发集成在线简易PSD文件查看与标注工具

摘要

本文提供了一份详细的技术指南,介绍如何在WordPress平台中通过代码二次开发,集成一个在线简易PSD文件查看与标注工具。我们将从需求分析开始,逐步讲解技术选型、开发流程、核心功能实现以及优化建议,帮助开发者掌握在WordPress中扩展专业功能的方法。


目录

  1. 引言:为什么在WordPress中集成PSD查看与标注工具
  2. 技术选型与准备工作
  3. WordPress插件架构设计
  4. 前端PSD查看器实现
  5. 标注功能开发
  6. 用户权限与文件管理
  7. 性能优化与安全考虑
  8. 测试与部署
  9. 扩展功能建议
  10. 结论

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文件的复杂性,我们需要选择合适的解析库:

  1. PSD.js - 基于JavaScript的PSD解析器,适合前端处理
  2. ImageMagick/GraphicsMagick - 服务器端处理方案
  3. Photoshop API - Adobe官方API(功能强大但成本较高)

对于简易查看器,我们推荐使用PSD.js,因为它:

  • 纯前端实现,减轻服务器负担
  • 开源免费,社区活跃
  • 支持图层提取和基本信息读取

2.2.2 标注工具库选择

  1. Fabric.js - 强大的Canvas操作库
  2. Konva.js - 另一个优秀的Canvas库
  3. 自定义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
本文来自网络,不代表柔性供应链服务中心立场,转载请注明出处:https://mall.org.cn/5256.html

EXCHANGES®作者

上一篇
下一篇

为您推荐

发表回复

联系我们

联系我们

18559313275

在线咨询: QQ交谈

邮箱: vip@exchanges.center

工作时间:周一至周五,9:00-17:30,节假日休息
返回顶部