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() |
扫码联系在线客服