返回 Skill 列表
extension
分类: 其它无需 API Key

psdtoh5-new

PSD源文件精确还原为带动画的H5页面,支持入场动画、呼吸效果、点击交互。触发词:PSD转H5、PSD转网页、Photoshop转HTML、psd to h5、psd转移动端、透底图导出、设计稿还原、设计师H5、PSD动效

person作者: user_5b12983fhubcommunity

PSD转H5动效专家 Skill v1.3

版本更新

  • v1.3: 新增呼吸动效、鼠标悬停交互效果模板
  • v1.2: 新增PNG透底图导出、透明通道检测
  • v1.1: 优化Artboard偏移处理、负坐标溢出自动裁剪
  • v1.0: 基础PSD解析和精确布局还原

功能概述

本Skill将PSD源文件像素级精确还原为带动画效果的H5移动端网页,核心能力包括:

| 功能模块 | 说明 | |---------|------| | 精确布局还原 | 像素级定位,保留原始图层顺序和层级关系 | | PNG透底图导出 | RGBA模式,完整保留透明通道 | | 入场动画 | 交错渐入效果,支持多种缓动曲线 | | 呼吸动效 | 鼠标悬停触发的循环放大/发光效果 | | 响应式缩放 | 自动适配移动端视口 | | 触摸适配 | 移动端触摸触发悬停效果 |


使用方法

基本语法

@skill://psd-to-animated-h5 @[PSD文件路径] [动效选项]

示例

| 用户输入 | 效果 | |---------|------| | @skill://psd-to-animated-h5 @xxx.psd | 基础PSD转H5,保留原始底色 | | @skill://psd-to-animated-h5 @xxx.psd 入场动画 | 带交错渐入动画 | | @skill://psd-to-animated-h5 @xxx.psd 呼吸动效 | 鼠标悬停呼吸效果 | | @skill://psd-to-animated-h5 @xxx.psd 保留底色 | 保留PSD原始背景色 |


核心算法

1. 坐标系统处理

PSD坐标系与HTML存在差异,必须正确处理:

┌─────────────────────────────────────────────────────┐
│  PSD坐标系              │  HTML坐标系               │
│                         │                           │
│  原点(0,0)在左上角       │  原点(0,0)在左上角         │
│  Y轴向下增加            │  Y轴向下增加               │
│                         │                           │
│  ⚠️ 关键差异:Artboard  │  子图层使用绝对定位        │
│  子图层坐标相对于画板    │  left/top基于容器          │
└─────────────────────────────────────────────────────┘

2. 像素级定位算法

def get_layer_absolute_position(layer, artboard):
    """
    计算图层在HTML中的绝对定位(像素级精度)
    
    参数:
        layer: PSD图层对象
        artboard: 画板对象
    
    返回: {
        'x': 容器left值,
        'y': 容器top值,
        'img_x': 图片偏移X,
        'img_y': 图片偏移Y,
        'width': 宽度,
        'height': 高度
    }
    """
    # Step 1: 获取Artboard在画布中的偏移
    artboard_offset_x = artboard.left
    artboard_offset_y = artboard.top
    
    # Step 2: 计算图层相对于Artboard的坐标
    rel_x = layer.left - artboard_offset_x
    rel_y = layer.top - artboard_offset_y
    
    # Step 3: 处理超出边界的图层(负坐标/溢出)
    if rel_x < 0 or rel_y < 0:
        # 容器定位在(0,0)
        container_x = 0
        container_y = 0
        # 图片使用负偏移实现精确位置
        image_x = -rel_x if rel_x < 0 else 0
        image_y = -rel_y if rel_y < 0 else 0
    else:
        container_x = rel_x
        container_y = rel_y
        image_x = 0
        image_y = 0
    
    return {
        'x': container_x,
        'y': container_y,
        'img_x': image_x,
        'img_y': image_y,
        'width': layer.width,
        'height': layer.height
    }

3. PNG透底图导出

from PIL import Image
import numpy as np

def ensure_rgba(img):
    """确保图片为RGBA模式,保留透明通道"""
    if img.mode != 'RGBA':
        if img.mode == 'P':  # 调色板模式
            img = img.convert('RGBA')
        elif img.mode == 'RGB':
            # RGB转RGBA,添加255透明度
            background = Image.new('RGBA', img.size, (255, 255, 255, 255))
            background.paste(img, (0, 0))
            img = background
        else:
            img = img.convert('RGBA')
    return img

def export_transparent_png(layer, artboard, output_path, index):
    """
    导出PNG透底图
    
    核心步骤:
    1. layer.composite(force=True) 获取混合后的图层
    2. ensure_rgba() 确保透明通道完整
    3. 处理负坐标溢出裁剪
    4. 保存PNG(optimize=False保留透明)
    """
    # 1. 获取图层渲染结果
    layer_img = layer.composite(force=True)
    
    # 2. 转换为RGBA
    layer_img = ensure_rgba(layer_img)
    
    # 3. 处理溢出裁剪
    rel_x = layer.left - artboard.left
    rel_y = layer.top - artboard.top
    
    if rel_x < 0 or rel_y < 0:
        crop_left = -rel_x if rel_x < 0 else 0
        crop_top = -rel_y if rel_y < 0 else 0
        layer_img = layer_img.crop((
            crop_left, crop_top,
            crop_left + layer.width,
            crop_top + layer.height
        ))
    
    # 4. 保存PNG
    filepath = f"{output_path}/images/layer_{index}_{sanitize_name(layer.name)}.png"
    layer_img.save(filepath, 'PNG', optimize=False)
    
    # 5. 检测透明通道
    alpha_channel = layer_img.split()[3]
    has_transparency = alpha_channel.getextrema()[0] < 255
    
    return {
        'path': filepath,
        'has_transparency': has_transparency,
        'size': layer_img.size
    }

动画效果模板

1. 入场动画(Stagger FadeInUp)

// 入场动画配置
const entranceConfig = {
    baseDuration: 600,           // 基础动画时长(ms)
    easing: 'cubic-bezier(0.4, 0, 0.2, 1)',  // 缓动曲线
    staggerDelay: 100,            // 交错延迟(ms)
    fromTransform: 'translateY(30px)',  // 起始状态
    fromOpacity: 0               // 起始透明度
};

// CSS动画
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.layer-enter {
    opacity: 0;
    animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

2. 呼吸动效(Hover Breath)

// 呼吸动效配置
const breathConfig = {
    scaleRange: [1, 1.05],        // 缩放范围
    duration: 2000,               // 单次呼吸周期(ms)
    easing: 'ease-in-out'
};

// CSS呼吸动画
@keyframes breathe {
    0%, 100% {
        transform: scale(1);
        filter: brightness(1);
    }
    50% {
        transform: scale(1.05);
        filter: brightness(1.1);
    }
}

// 悬停触发
.layer-hoverable:hover {
    animation: breathe 2s ease-in-out infinite;
}

3. 发光按钮动效

// 发光按钮配置
const glowConfig = {
    baseScale: 1,
    hoverScale: 1.05,
    glowSpread: '0 0 20px',
    glowColor: 'rgba(59, 130, 246, 0.6)',
    duration: 1500
};

// CSS
@keyframes glowPulse {
    0%, 100% {
        box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
    }
    50% {
        box-shadow: 0 0 20px rgba(59, 130, 246, 0.6),
                    0 0 30px rgba(59, 130, 246, 0.4);
    }
}

.btn-glow:hover {
    animation: glowPulse 1.5s ease-in-out infinite;
}

完整HTML模板

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>PSD转H5 - 精确还原</title>
    <style>
        /* ==================== 变量定义 ==================== */
        :root {
            --psd-width: 1080px;
            --psd-height: 1920px;
            --scale-ratio: 1;
            --bg-color: #4E3528;  /* PSD原始底色 */
            
            /* 动画配置 */
            --entrance-duration: 600ms;
            --entrance-delay: 100ms;
            --breath-duration: 2000ms;
        }

        /* ==================== 基础重置 ==================== */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: var(--bg-color);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: flex-start;
        }

        /* ==================== H5容器 ==================== */
        .h5-wrapper {
            width: 100%;
            max-width: var(--psd-width);
            padding: 20px;
        }

        .h5-container {
            position: relative;
            width: var(--psd-width);
            height: var(--psd-height);
            overflow: hidden;
            /* 响应式缩放 */
            transform-origin: top center;
        }

        /* ==================== 图层样式 ==================== */
        .layer {
            position: absolute;
            overflow: hidden;
            /* 入场动画初始状态 */
            opacity: 0;
            transform: translateY(30px);
        }

        .layer img {
            display: block;
            /* 精确还原图层偏移 */
            position: relative;
        }

        /* ==================== 入场动画 ==================== */
        @keyframes fadeInUp {
            from {
                opacity: 0;
                transform: translateY(30px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .layer.animate-enter {
            animation: fadeInUp var(--entrance-duration) 
                cubic-bezier(0.4, 0, 0.2, 1) forwards;
        }

        /* ==================== 呼吸动效 ==================== */
        @keyframes breathe {
            0%, 100% {
                transform: scale(1);
                filter: brightness(1);
            }
            50% {
                transform: scale(1.05);
                filter: brightness(1.1);
            }
        }

        .layer.breath-on-hover:hover {
            animation: breathe var(--breath-duration) 
                ease-in-out infinite;
        }

        /* Logo专属呼吸效果 */
        .layer.logo.breath-on-hover:hover {
            animation: breathe 2500ms ease-in-out infinite,
                       logoGlow 2500ms ease-in-out infinite;
        }

        @keyframes logoGlow {
            0%, 100% { filter: brightness(1) drop-shadow(0 0 0 transparent); }
            50% { filter: brightness(1.2) drop-shadow(0 0 10px rgba(255,255,255,0.3)); }
        }

        /* 产品图专属摇摆效果 */
        .layer.product.breath-on-hover:hover {
            animation: productSwing 3000ms ease-in-out infinite;
        }

        @keyframes productSwing {
            0%, 100% { transform: scale(1) rotate(0deg); }
            25% { transform: scale(1.03) rotate(0.5deg); }
            75% { transform: scale(1.03) rotate(-0.5deg); }
        }

        /* 按钮发光效果 */
        .layer.btn.breath-on-hover:hover {
            animation: btnGlow 1500ms ease-in-out infinite;
        }

        @keyframes btnGlow {
            0%, 100% { 
                transform: scale(1);
                box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
            }
            50% { 
                transform: scale(1.05);
                box-shadow: 0 0 20px rgba(59, 130, 246, 0.6),
                            0 0 30px rgba(59, 130, 246, 0.4);
            }
        }

        /* ==================== 响应式缩放 ==================== */
        @media (max-width: 750px) {
            :root {
                --scale-ratio: calc(100vw / var(--psd-width));
            }
            
            .h5-container {
                transform: scale(var(--scale-ratio));
                transform-origin: top center;
            }
        }

        @media (max-width: 375px) {
            :root {
                --psd-width: 375px;
                --psd-height: 667px;
            }
        }
    </style>
</head>
<body>
    <div class="h5-wrapper">
        <div class="h5-container">
            <!-- 图层将通过JS动态插入 -->
        </div>
    </div>

    <script>
        // ==================== 图层数据 ====================
        const layers = [
            {
                name: "背景底色",
                type: "background",
                color: "#4E3528",
                zIndex: 0
            },
            {
                name: "图层13",
                type: "image",
                x: 0,
                y: 0,
                width: 1177,
                height: 1457,
                offsetX: 0,
                offsetY: 0,
                src: "images/layer_1_图层_13.png",
                enterDelay: 0,
                hasBreath: false
            }
            // ... 更多图层
        ];

        // ==================== 渲染函数 ====================
        function renderLayers() {
            const container = document.querySelector('.h5-container');
            
            layers.forEach((layer, index) => {
                const el = document.createElement('div');
                el.className = `layer ${layer.name} animate-enter`;
                el.style.cssText = `
                    position: absolute;
                    left: ${layer.x || 0}px;
                    top: ${layer.y || 0}px;
                    width: ${layer.width}px;
                    height: ${layer.height}px;
                    z-index: ${layer.zIndex || index + 1};
                    animation-delay: ${layer.enterDelay || index * 100}ms;
                `;

                if (layer.hasBreath) {
                    el.classList.add('breath-on-hover');
                }

                if (layer.type === 'image') {
                    const img = document.createElement('img');
                    img.src = layer.src;
                    img.style.cssText = `
                        position: relative;
                        left: ${-layer.offsetX}px;
                        top: ${-layer.offsetY}px;
                    `;
                    el.appendChild(img);
                }

                if (layer.type === 'background') {
                    el.style.backgroundColor = layer.color;
                    el.style.width = '100%';
                    el.style.height = '100%';
                }

                container.appendChild(el);
            });
        }

        // ==================== 初始化 ====================
        document.addEventListener('DOMContentLoaded', renderLayers);
    </script>
</body>
</html>

布局还原策略

策略1:背景填充模式

适用于全尺寸背景图,填充整个容器:

.h5-container {
    position: relative;
    width: 100vw;
    height: 100vw * (设计稿高/设计稿宽);
    max-height: 100vh;
    overflow: hidden;
}

.layer.bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

策略2:绝对定位模式

适用于精确图层堆叠:

.layer {
    position: absolute;
    /* 像素级精确定位 */
    left: [精确X]px;
    top: [精确Y]px;
    width: [精确宽度]px;
    height: [精确高度]px;
}

策略3:等比缩放模式

适用于完整画板还原:

.h5-wrapper {
    position: relative;
    width: [设计稿宽度];
    height: [设计稿高度];
    transform-origin: top left;
}

@media (max-width: 750px) {
    .h5-wrapper {
        transform: scale(calc(100vw / [设计稿宽度]));
    }
}

常见问题与解决方案

| 问题 | 原因 | 解决方案 | |------|------|---------| | 图层位置偏移 | Artboard偏移未处理 | 使用相对坐标计算 | | 负坐标图层缺失 | 溢出被裁剪 | 调整容器定位 | | 透明区域显示黑色 | 图片未转RGBA | 使用ensure_rgba() | | 动画顺序错乱 | 图层遍历顺序问题 | 按z-index排序 | | 移动端显示异常 | 缩放比例问题 | 使用vw单位配合scale | | 悬停效果不触发 | 移动端无hover事件 | 使用@media (hover: hover) |


完整工作流程

┌────────────────────────────────────────────────────────────────┐
│                        PSD转H5工作流                            │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐ │
│  │ 1.解析PSD │───▶│ 2.提取图层│───▶│ 3.导出PNG │───▶│ 4.生成HTML│ │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘ │
│       │               │               │               │        │
│       ▼               ▼               ▼               ▼        │
│  psd-tools库    遍历Artboard    RGBA透明通道    绝对定位布局   │
│                                                                │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                 │
│  │ 5.添加动效│───▶│ 6.响应式 │───▶│ 7.预览测试│                 │
│  └──────────┘    └──────────┘    └──────────┘                 │
│       │               │               │                       │
│       ▼               ▼               ▼                       │
│  入场+呼吸动画    移动端适配      本地服务器预览                │
│                                                                │
└────────────────────────────────────────────────────────────────┘

依赖环境

Python 环境(推荐使用虚拟环境隔离)

# 方式一:使用虚拟环境(推荐,隔离系统环境)
python -m venv .venv
source .venv/bin/activate   # macOS/Linux
# .venv\Scripts\activate    # Windows

pip install psd-tools Pillow numpy
# 方式二:使用 pipx(仅适用于独立工具包)
pipx install psd-tools

Node.js 本地预览(可选)

# 使用 npx 直接运行,无需全局安装
npx serve .

# 或在项目中本地安装
npm install serve
npx serve .

示例输出

输入PSD后,生成的H5页面包含:

  • 像素级精确的图层定位
  • 原始底色保留(可配置)
  • 交错入场动画
  • 鼠标悬停呼吸效果
  • 移动端响应式适配
  • 触摸设备手势支持

扩展动画效果库 (v1.3 新增)

1. 滑动类动画 (Slide)

// ==================== 滑动动画配置 ====================
const slideConfig = {
    // 方向选项: left, right, top, bottom
    direction: 'left',
    distance: '100%',  // 或具体像素值
    duration: 800,
    easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
};

// 从左侧滑入
@keyframes slideInLeft {
    from {
        transform: translateX(-100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

// 从右侧滑入
@keyframes slideInRight {
    from {
        transform: translateX(100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

// 从顶部滑入
@keyframes slideInTop {
    from {
        transform: translateY(-100%);
        opacity: 0;
    }
    to {
        transform: translateY(0);
        opacity: 1;
    }
}

// 从底部滑入
@keyframes slideInBottom {
    from {
        transform: translateY(100%);
        opacity: 0;
    }
    to {
        transform: translateY(0);
        opacity: 1;
    }
}

// 滑出动画(用于退出)
@keyframes slideOutLeft {
    from {
        transform: translateX(0);
        opacity: 1;
    }
    to {
        transform: translateX(-100%);
        opacity: 0;
    }
}

2. 淡入淡出动画 (Fade)

// ==================== 淡入淡出配置 ====================
const fadeConfig = {
    duration: 600,
    easing: 'ease-out'
};

// 普通淡入
@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

// 淡入带缩放
@keyframes fadeInScale {
    from {
        opacity: 0;
        transform: scale(0.8);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

// 淡入带模糊
@keyframes fadeInBlur {
    from {
        opacity: 0;
        filter: blur(10px);
    }
    to {
        opacity: 1;
        filter: blur(0);
    }
}

// 淡出
@keyframes fadeOut {
    from { opacity: 1; }
    to { opacity: 0; }
}

// 交叉淡化(切换场景用)
@keyframes crossFade {
    0% { opacity: 0; transform: scale(1.05); }
    100% { opacity: 1; transform: scale(1); }
}

3. 组合动画 (Complex)

// ==================== 组合动画 ====================

// 弹跳入场 (Bounce In)
@keyframes bounceIn {
    0% {
        transform: scale(0);
        opacity: 0;
    }
    50% {
        transform: scale(1.1);
    }
    70% {
        transform: scale(0.9);
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

// 旋转淡入 (Spin Fade)
@keyframes spinFadeIn {
    from {
        transform: rotate(-180deg) scale(0.5);
        opacity: 0;
    }
    to {
        transform: rotate(0deg) scale(1);
        opacity: 1;
    }
}

// 视差滚动效果 (Parallax)
@keyframes parallaxScroll {
    0% { transform: translateY(0); }
    100% { transform: translateY(-20%); }
}

// 打字机效果 (Typewriter) - 用于文字
@keyframes typewriter {
    from { width: 0; }
    to { width: 100%; }
}

4. 交互动画 (Interactive)

// ==================== 交互动画 ====================

// 点击反馈
@keyframes clickPulse {
    0% { transform: scale(1); }
    50% { transform: scale(0.95); }
    100% { transform: scale(1); }
}

.layer-clickable:active {
    animation: clickPulse 150ms ease-out;
}

// 长按效果
.layer-longpress {
    transition: transform 0.3s ease;
}

.layer-longpress.longpressed {
    transform: scale(0.95);
    box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5);
}

// 拖拽效果
.layer-draggable {
    cursor: grab;
    transition: box-shadow 0.2s ease;
}

.layer-draggable:active {
    cursor: grabbing;
    box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}

// 滑动解锁
@keyframes swipeUnlock {
    0% { transform: translateX(0); }
    100% { transform: translateX(100%); }
}

分享功能模块 (v1.3 新增)

1. 微信分享配置

// ==================== 微信JSSDK分享 ====================
class WeChatShare {
    constructor(options) {
        this.config = {
            title: options.title || 'PSD转H5作品',
            desc: options.desc || '设计师分享',
            link: options.link || window.location.href,
            imgUrl: options.imgUrl || '',
            type: 'link',  // music/video/link
            dataUrl: ''
        };
    }

    // 分享到朋友圈
    onShareTimeline() {
        return {
            title: this.config.title,
            link: this.config.link,
            imgUrl: this.config.imgUrl
        };
    }

    // 分享给朋友
    onShareAppMessage() {
        return {
            title: this.config.title,
            desc: this.config.desc,
            link: this.config.link,
            imgUrl: this.config.imgUrl,
            type: this.config.type,
            dataUrl: this.config.dataUrl
        };
    }
}

// 使用示例
const share = new WeChatShare({
    title: '新年促销活动',
    desc: '限时优惠,错过等一年!',
    imgUrl: 'https://example.com/share.jpg'
});

2. 通用分享组件

<!-- ==================== 分享按钮组件 ==================== -->
<div class="share-container">
    <button class="share-btn" data-platform="weixin">
        <svg class="icon-weixin"></svg>
        <span>微信</span>
    </button>
    <button class="share-btn" data-platform="weibo">
        <svg class="icon-weibo"></svg>
        <span>微博</span>
    </button>
    <button class="share-btn" data-platform="qq">
        <svg class="icon-qq"></svg>
        <span>QQ</span>
    </button>
    <button class="share-btn" data-platform="copy">
        <svg class="icon-copy"></svg>
        <span>复制链接</span>
    </button>
</div>

<style>
.share-container {
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 12px;
    padding: 12px 20px;
    background: rgba(255,255,255,0.95);
    border-radius: 30px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.15);
    z-index: 9999;
}

.share-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 8px 12px;
    border: none;
    background: transparent;
    cursor: pointer;
    transition: transform 0.2s;
}

.share-btn:hover {
    transform: scale(1.1);
}

.share-btn svg {
    width: 28px;
    height: 28px;
}

.share-btn span {
    font-size: 11px;
    color: #666;
}
</style>
// ==================== 分享功能实现 ====================
class ShareManager {
    constructor() {
        this.platforms = {
            weixin: this.shareWeixin.bind(this),
            weibo: this.shareWeibo.bind(this),
            qq: this.shareQQ.bind(this),
            copy: this.copyLink.bind(this)
        };
        this.init();
    }

    init() {
        document.querySelectorAll('.share-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const platform = btn.dataset.platform;
                if (this.platforms[platform]) {
                    this.platforms[platform]();
                }
            });
        });
    }

    shareWeixin() {
        // 微信分享需要JSSDK,这里提示用户保存图片/截图分享
        this.showTip('请点击右上角"..."选择分享到朋友圈');
    }

    shareWeibo() {
        const text = encodeURIComponent(`${document.title} ${window.location.href}`);
        window.open(`https://service.weibo.com/share/share.php?url=${text}`, '_blank');
    }

    shareQQ() {
        const url = encodeURIComponent(window.location.href);
        const title = encodeURIComponent(document.title);
        window.open(`https://connect.qq.com/widget/shareqq/index.html?url=${url}&title=${title}`, '_blank');
    }

    async copyLink() {
        try {
            await navigator.clipboard.writeText(window.location.href);
            this.showTip('链接已复制到剪贴板');
        } catch (err) {
            // 降级方案
            const input = document.createElement('input');
            input.value = window.location.href;
            document.body.appendChild(input);
            input.select();
            document.execCommand('copy');
            document.body.removeChild(input);
            this.showTip('链接已复制到剪贴板');
        }
    }

    showTip(message) {
        const tip = document.createElement('div');
        tip.className = 'share-toast';
        tip.textContent = message;
        document.body.appendChild(tip);
        
        setTimeout(() => tip.classList.add('show'), 10);
        setTimeout(() => {
            tip.classList.remove('show');
            setTimeout(() => tip.remove(), 300);
        }, 2000);
    }
}

// 初始化分享管理器
document.addEventListener('DOMContentLoaded', () => {
    new ShareManager();
});

3. 海报生成与保存

// ==================== 海报生成功能 ====================
class PosterGenerator {
    constructor(canvas, options = {}) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.options = {
            width: options.width || 1080,
            height: options.height || 1920,
            backgroundColor: options.bgColor || '#ffffff',
            ...options
        };
    }

    // 添加背景
    addBackground(color) {
        this.ctx.fillStyle = color;
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
    }

    // 添加图片
    async addImage(src, x, y, width, height) {
        return new Promise((resolve) => {
            const img = new Image();
            img.crossOrigin = 'anonymous';
            img.onload = () => {
                this.ctx.drawImage(img, x, y, width, height);
                resolve();
            };
            img.src = src;
        });
    }

    // 添加文字
    addText(text, x, y, options = {}) {
        const {
            font = '48px sans-serif',
            color = '#000000',
            align = 'left',
            maxWidth = null
        } = options;

        this.ctx.font = font;
        this.ctx.fillStyle = color;
        this.ctx.textAlign = align;
        
        if (maxWidth) {
            this.ctx.fillText(text, x, y, maxWidth);
        } else {
            this.ctx.fillText(text, x, y);
        }
    }

    // 添加二维码
    async addQRCode(url, x, y, size = 200) {
        // 使用QRCode.js或类似库
        const qrcode = new QRCode(this.canvas, {
            text: url,
            width: size,
            height: size
        });
        // 二维码生成后绘制到画布
    }

    // 导出海报
    export() {
        return this.canvas.toDataURL('image/png', 1.0);
    }

    // 一键保存
    async save(filename = 'poster.png') {
        const dataUrl = this.export();
        const link = document.createElement('a');
        link.download = filename;
        link.href = dataUrl;
        link.click();
    }
}

// 使用示例
const poster = new PosterGenerator(document.createElement('canvas'), {
    width: 1080,
    height: 1920,
    bgColor: '#4E3528'
});

await poster.addBackground('#4E3528');
await poster.addImage('images/layer_1.png', 0, 0, 1080, 1920);
poster.addText('新年快乐', 540, 1000, {
    font: '72px bold sans-serif',
    color: '#ffffff',
    align: 'center'
});
await poster.save('my-poster.png');

4. 完整分享页面模板

<!-- ==================== 分享页面模板 ==================== -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>分享页面</title>
    <style>
        /* 分享遮罩 */
        .share-mask {
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,0.8);
            z-index: 10000;
            display: flex;
            justify-content: center;
            align-items: center;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.3s;
        }
        
        .share-mask.active {
            opacity: 1;
            pointer-events: auto;
        }
        
        .share-guide {
            width: 300px;
            padding: 40px;
            background: white;
            border-radius: 16px;
            text-align: center;
        }
        
        .share-guide h3 {
            font-size: 20px;
            margin-bottom: 20px;
        }
        
        .share-guide p {
            font-size: 14px;
            color: #666;
            line-height: 1.6;
        }
        
        /* 分享按钮 */
        .share-actions {
            position: fixed;
            bottom: 40px;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 20px;
            z-index: 100;
        }
        
        .share-action-btn {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 8px;
            padding: 16px 24px;
            background: white;
            border: none;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
            cursor: pointer;
            transition: transform 0.2s;
        }
        
        .share-action-btn:hover {
            transform: translateY(-4px);
        }
        
        .share-action-btn svg {
            width: 40px;
            height: 40px;
        }
        
        /* Toast提示 */
        .share-toast {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            padding: 16px 32px;
            background: rgba(0,0,0,0.8);
            color: white;
            border-radius: 8px;
            font-size: 14px;
            opacity: 0;
            transition: opacity 0.3s;
            z-index: 10001;
        }
        
        .share-toast.show {
            opacity: 1;
        }
    </style>
</head>
<body>
    <!-- 分享引导遮罩 -->
    <div class="share-mask" id="shareMask">
        <div class="share-guide">
            <h3>分享到朋友圈</h3>
            <p>1. 点击右上角"..."</p>
            <p>2. 选择"分享到朋友圈"</p>
        </div>
    </div>

    <!-- 分享操作栏 -->
    <div class="share-actions">
        <button class="share-action-btn" onclick="shareToFriends()">
            <svg viewBox="0 0 24 24" fill="#07C160">
                <path d="M8.5 11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
                <path d="M15.5 11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
                <path d="M12 2C6.48 2 2 6.48 2 12c0 1.82.49 3.53 1.34 5L2 22l5.12-1.34C8.48 21.51 10.18 22 12 22c5.52 0 10-4.48 10-10S17.52 2 12 2z"/>
            </svg>
            <span>分享好友</span>
        </button>
        
        <button class="share-action-btn" onclick="savePoster()">
            <svg viewBox="0 0 24 24" fill="#666">
                <path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2v9.67z"/>
            </svg>
            <span>保存图片</span>
        </button>
        
        <button class="share-action-btn" onclick="copyLink()">
            <svg viewBox="0 0 24 24" fill="#666">
                <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
            </svg>
            <span>复制链接</span>
        </button>
    </div>

    <script>
        // 分享到好友(触发微信JSSDK或显示引导)
        function shareToFriends() {
            // 检测是否为微信环境
            const isWeChat = /MicroMessenger/.test(navigator.userAgent);
            if (isWeChat) {
                // 调用微信JSSDK
                if (typeof wx !== 'undefined') {
                    wx.showOptionMenu();
                }
            } else {
                // 非微信环境显示引导
                document.getElementById('shareMask').classList.add('active');
            }
        }

        // 保存海报图片
        async function savePoster() {
            try {
                const posterData = await generatePoster();
                const link = document.createElement('a');
                link.download = 'h5-poster.png';
                link.href = posterData;
                link.click();
                showToast('图片已保存');
            } catch (err) {
                showToast('保存失败,请重试');
            }
        }

        // 复制链接
        async function copyLink() {
            try {
                await navigator.clipboard.writeText(window.location.href);
                showToast('链接已复制');
            } catch (err) {
                showToast('复制失败');
            }
        }

        // 显示提示
        function showToast(message) {
            const toast = document.createElement('div');
            toast.className = 'share-toast';
            toast.textContent = message;
            document.body.appendChild(toast);
            setTimeout(() => toast.classList.add('show'), 10);
            setTimeout(() => {
                toast.classList.remove('show');
                setTimeout(() => toast.remove(), 300);
            }, 2000);
        }

        // 点击遮罩关闭
        document.getElementById('shareMask').addEventListener('click', function(e) {
            if (e.target === this) {
                this.classList.remove('active');
            }
        });
    </script>
</body>
</html>

动画效果速查表

| 效果名称 | 触发方式 | CSS属性 | 适用场景 | |---------|---------|--------|---------| | fadeInUp | 入场/悬停 | translateY + opacity | 默认入场动画 | | slideInLeft | 入场 | translateX | 侧滑卡片 | | slideInRight | 入场 | translateX | 侧滑卡片 | | breathe | 悬停 | scale + filter | 呼吸发光 | | bounceIn | 入场 | scale + transform | 弹跳强调 | | fadeInScale | 入场 | scale + opacity | 缩放淡入 | | pulse | 悬停/循环 | scale | 脉冲强调 | | rotate | 点击 | rotate | 旋转反馈 | | shake | 点击 | translateX | 抖动警告 |


分享功能速查表

| 平台 | 方式 | 代码实现 | |------|------|---------| | 微信朋友圈 | JSSDK / 引导截图 | wx.onShareTimeline() | | 微信好友 | JSSDK | wx.onShareAppMessage() | | 微博 | URL Scheme | weibo.com/share | | QQ | URL Scheme | connect.qq.com | | 复制链接 | Clipboard API | navigator.clipboard | | 保存图片 | Canvas截图 | canvas.toDataURL() |