不说废话,这是链接,速来围观「DIY 表情」:aicoding.juejin.cn/aicoding/wo...
前端小白也能轻松制作出一个网页小游戏,如今是ai的时代,我们只需要用语言描述出自己的想法,利用工具就能将它实现。以下是我用Trae DE实现的一个可以DIY独特表情的在线工具。(最后附上源代码)
在数字化沟通日益普及的今天,表情包已成为我们表达情绪、活跃聊天的不可或缺的工具。然而,千篇一律的通用表情是否已无法满足您独特的个性表达?现在,隆重推出------ "表情实验室"(Emoji Lab) ,一款旨在激发您无限创意、让您轻松打造专属表情包的强大在线工具!
"表情实验室"能做什么?
"表情实验室"是一个真正意义上的创意画布,它将复杂的设计过程简化为直观的拖拽和点击。在这里,您可以:
- 自由组合,天马行空: 告别固定模板!从海量的基础表情、丰富的五官部件(眼睛、嘴巴、鼻子、眉毛)以及各种有趣的装饰元素中,任意选择、拖拽组合。想要一个哭笑不得又戴着墨镜的表情?没问题!
- 一键"魔法"组合: 这是"表情实验室"的亮点功能!将两个您喜爱的表情拖拽到下方的"组合表情区",点击"组合"按钮,神奇的事情就会发生------两个表情将以独特的透明叠加方式融合,生成一个全新的、富有层次感的"新表情"图层!这就像为您的表情施加了魔法,创造出意想不到的视觉效果。
- 精细调整,掌控细节: 不仅仅是组合,您还可以对画布上的每一个图层进行精细控制。移动、缩放、旋转、调整透明度、水平翻转......所有操作都可在实时预览中完成,确保您的创意完美呈现。
- 图层管理,条理清晰: 左侧的"图层"面板让您的创作井然有序。您可以轻松选择、切换图层,调整它们的显示顺序(上移/下移),甚至隐藏或删除不需要的图层。
- 保存分享,永留精彩: 完成创作后,您可以将作品保存为高质量的PNG(支持透明背景)或JPEG格式到本地。更棒的是,通过"我的创作"功能,您的所有杰作都将被安全保存,随时可供加载、编辑或分享。
如何使用"表情实验室"?
使用"表情实验室"非常简单,只需几步,您就能成为表情包大师:
-
选择基础: 从右侧的"基础表情"库中,将您喜欢的表情拖拽到中间的画布上。您也可以直接点击它。
-
添加个性: 切换到"五官"或"装饰"分类,挑选眼睛、嘴巴、帽子、闪光等元素,拖拽到画布上的基础表情上。
-
精细调整: 点击画布上的图层(或左侧图层列表中的图层),激活后,使用左侧工具栏的"移动"、"旋转"、"透明度"等工具调整其位置、大小、角度和透明度。使用鼠标滚轮可以快速缩放图层。
-
尝试"组合魔法":
- 将两个您想要组合的表情,分别拖拽到页面底部的"拖拽表情1到此处"和"拖拽表情2到此处"区域。
- 确保两个区域都显示了表情后,点击旁边的"组合"按钮。
- 画布上会自动生成一个由这两个表情透明叠加而成的"组合表情"新图层,它就像是两个表情的独特结合体!
-
管理图层: 利用左侧"图层"面板,您可以调整图层顺序,或者选中一个图层后使用"复制"、"删除"、"合并"等工具进行高级操作。
-
保存收藏: 为您的作品取个响亮的名字和标签,选择保存格式,然后点击"保存"下载到本地,或点击"收藏"将其永久保存在"我的创作"中,方便下次使用。
项目体验


AI 在"表情实验室"开发中的角色
利用ai工具,仅需要清晰的描述出你的想法它便可以帮你实现。

源代码
ini
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表情包制作器</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#7C3AED', // 主色调:紫色
secondary: '#EC4899', // 辅助色:粉色
neutral: '#1F2937', // 中性色:深灰
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 0.5rem;
}
.emoji-item {
aspect-ratio: 1/1;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem; /* 更大的表情符号 */
cursor: grab; /* 指示可拖拽 */
user-select: none;
}
.tool-btn.active {
@apply bg-primary text-white;
}
/* 自定义滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* 拖拽放置区样式 */
.drop-area {
@apply border-2 border-dashed border-gray-300 rounded-md p-4 text-center text-gray-500 transition-colors duration-200;
}
.drop-area.drag-over {
@apply border-primary bg-primary/10;
}
/* 画布拖拽样式 */
#emojiCanvas.drag-over {
@apply border-primary ring-2 ring-primary ring-opacity-50;
}
}
</style>
</head>
<body class="bg-gray-100 font-sans text-neutral antialiased min-h-screen flex flex-col">
<header class="bg-white shadow-sm p-4 flex items-center justify-between">
<div class="flex items-center space-x-3">
<button id="backBtn" class="text-primary hover:text-primary-dark transition-colors duration-200" title="返回主界面">
<i class="fa fa-arrow-left fa-lg"></i>
</button>
<h1 class="text-2xl font-bold">表情包制作器</h1>
</div>
<div class="flex items-center space-x-3">
<button id="saveBtn" class="bg-primary hover:bg-primary-dark text-white py-2 px-4 rounded-md text-sm font-medium transition-colors duration-200 shadow-md" title="保存表情到本地">
<i class="fa fa-download mr-2"></i>保存
</button>
<button id="saveToCollectionBtn" class="bg-secondary hover:bg-secondary-dark text-white py-2 px-4 rounded-md text-sm font-medium transition-colors duration-200 shadow-md" title="保存表情到我的创作">
<i class="fa fa-star mr-2"></i>收藏
</button>
</div>
</header>
<main class="flex-1 flex flex-col lg:flex-row p-4 gap-4">
<aside class="w-full lg:w-1/4 flex flex-col gap-4">
<div class="p-4 bg-white rounded-lg shadow">
<h3 class="text-lg font-semibold mb-3 text-neutral">工具</h3>
<div class="grid grid-cols-3 gap-2">
<button id="moveTool" class="tool-btn py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300 active" title="移动图层"><i class="fa fa-arrows-alt"></i> 移动</button>
<button id="rotateTool" class="tool-btn py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" title="旋转图层"><i class="fa fa-redo"></i> 旋转</button>
<button id="opacityTool" class="tool-btn py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" title="调整图层透明度"><i class="fa fa-adjust"></i> 透明度</button>
<button id="flipTool" class="tool-btn py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" title="水平翻转图层"><i class="fa fa-exchange"></i> 翻转</button>
<button id="duplicateTool" class="tool-btn py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" title="复制当前图层"><i class="fa fa-copy"></i> 复制</button>
<button id="mergeTool" class="tool-btn py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" title="向下合并当前图层"><i class="fa fa-object-group"></i> 合并</button>
<button id="deleteTool" class="tool-btn py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" title="删除当前图层"><i class="fa fa-trash"></i> 删除</button>
</div>
</div>
<div class="p-4 bg-white rounded-lg shadow flex-1 flex flex-col">
<h3 class="text-lg font-semibold mb-3 text-neutral">图层</h3>
<div id="layersList" class="flex-1 overflow-y-auto custom-scrollbar space-y-2">
</div>
</div>
</aside>
<section class="w-full lg:w-2/4 bg-white rounded-lg shadow flex items-center justify-center p-4 min-h-[400px]">
<canvas id="emojiCanvas" width="500" height="500" class="border border-gray-300 rounded-md"></canvas>
</section>
<aside class="w-full lg:w-1/4 flex flex-col gap-4">
<div class="p-4 bg-white rounded-lg shadow">
<h3 class="text-lg font-semibold mb-3 text-neutral">基础表情</h3>
<div id="baseEmojiGrid" class="emoji-grid h-48 overflow-y-auto custom-scrollbar mb-3">
</div>
<div class="flex justify-between mt-2">
<button id="prevBaseEmoji" class="bg-gray-200 hover:bg-gray-300 text-neutral py-1 px-3 rounded-md text-sm transition-colors duration-200" title="上一页"><i class="fa fa-arrow-left"></i></button>
<button id="nextBaseEmoji" class="bg-gray-200 hover:bg-gray-300 text-neutral py-1 px-3 rounded-md text-sm transition-colors duration-200" title="下一页"><i class="fa fa-arrow-right"></i></button>
</div>
</div>
<div class="p-4 bg-white rounded-lg shadow">
<h3 class="text-lg font-semibold mb-3 text-neutral">五官</h3>
<div class="flex flex-wrap gap-2 mb-4">
<button class="tool-btn flex-grow py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300 active" data-category="eyes" title="眼睛表情">眼睛</button>
<button class="tool-btn flex-grow py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" data-category="mouth" title="嘴巴表情">嘴巴</button>
<button class="tool-btn flex-grow py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" data-category="nose" title="鼻子表情">鼻子</button>
<button class="tool-btn flex-grow py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" data-category="eyebrows" title="眉毛表情">眉毛</button>
<button class="tool-btn flex-grow py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 bg-gray-200 hover:bg-gray-300" data-category="accessories" title="配饰表情">配饰</button>
</div>
<div id="featuresGrid" class="emoji-grid h-48 overflow-y-auto custom-scrollbar">
</div>
</div>
<div class="p-4 bg-white rounded-lg shadow">
<h3 class="text-lg font-semibold mb-3 text-neutral">装饰</h3>
<div id="decorationsGrid" class="emoji-grid h-48 overflow-y-auto custom-scrollbar">
</div>
</div>
<div class="p-4 bg-white rounded-lg shadow">
<h3 class="text-lg font-semibold mb-3 text-neutral">最近使用</h3>
<div id="recentEmojisGrid" class="emoji-grid h-24 overflow-y-auto custom-scrollbar">
</div>
</div>
<div class="p-4 bg-white rounded-lg shadow">
<h3 class="text-lg font-semibold mb-3 text-neutral">我的创作</h3>
<div id="myCreationsGrid" class="grid grid-cols-3 gap-2 h-48 overflow-y-auto custom-scrollbar">
</div>
</div>
</aside>
</main>
<footer class="bg-white shadow-sm p-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex-1 w-full sm:w-auto">
<h3 class="text-lg font-semibold mb-2 text-neutral">保存设置</h3>
<div class="flex flex-col sm:flex-row gap-3">
<div class="flex-1">
<label for="emojiName" class="block text-sm font-medium text-gray-700">名称</label>
<input type="text" id="emojiName" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary sm:text-sm p-2" placeholder="输入表情包名称" title="为您的表情包命名">
</div>
<div class="flex-1">
<label for="emojiTags" class="block text-sm font-medium text-gray-700">标签 (逗号分隔)</label>
<input type="text" id="emojiTags" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary sm:text-sm p-2" placeholder="例如:可爱, 搞怪" title="添加标签方便查找">
</div>
<div class="flex-1">
<label for="saveFormat" class="block text-sm font-medium text-gray-700">格式</label>
<select id="saveFormat" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary sm:text-sm p-2" title="选择保存图片格式">
<option value="png">PNG (支持透明背景)</option>
<option value="jpeg">JPEG (不支持透明背景)</option>
</select>
</div>
</div>
</div>
<div class="flex-1 w-full sm:w-auto mt-4 sm:mt-0">
<h3 class="text-lg font-semibold mb-2 text-neutral">组合表情 (拖拽表情到下方区域)</h3>
<div class="flex flex-col sm:flex-row gap-3 items-center">
<div id="dropArea1" class="drop-area flex-1 w-full sm:w-auto min-h-[60px] flex items-center justify-center relative">
<span class="text-sm text-gray-500" data-placeholder="拖拽表情1">拖拽表情1到此处</span>
<span class="absolute text-5xl"></span> </div>
<span class="text-neutral text-lg">+</span>
<div id="dropArea2" class="drop-area flex-1 w-full sm:w-auto min-h-[60px] flex items-center justify-center relative">
<span class="text-sm text-gray-500" data-placeholder="拖拽表情2">拖拽表情2到此处</span>
<span class="absolute text-5xl"></span> </div>
<button id="combineBtn" class="bg-primary hover:bg-primary-dark text-white py-2 px-4 rounded-md text-sm font-medium transition-colors duration-200 shadow-md w-full sm:w-auto" title="组合两个表情">
<i class="fa fa-magic mr-2"></i>组合
</button>
</div>
</div>
</footer>
<div id="notification" class="fixed bottom-4 right-4 bg-white p-4 rounded-lg shadow-lg flex items-center space-x-3 transform translate-x-full transition-transform duration-300 ease-out z-50">
<div id="notificationIcon" class="w-8 h-8 rounded-full flex items-center justify-center">
</div>
<div>
<h4 id="notificationTitle" class="font-semibold text-neutral"></h4>
<p id="notificationMessage" class="text-sm text-gray-600"></p>
</div>
<button id="closeNotification" class="text-gray-400 hover:text-gray-600 transition-colors duration-200" title="关闭通知">
<i class="fa fa-times"></i>
</button>
</div>
<script>
// 数据模型
const emojiCreator = {
canvas: null,
ctx: null,
currentLayer: null,
layers: [],
selectedTool: 'move', // 默认选中移动工具
dragInfo: null, // 用于存储拖拽的起始信息
currentCategory: 'eyes', // 默认选中眼睛分类
// 预加载的表情图片缓存
preloadedEmojis: {},
baseEmojis: [
'⬜', '⚪', '⚫', '🔺', '🔻', '⭐', // 新增的基础原型框架
'😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊', '😋', '😎',
'😍', '😘', '🥰', '😗', '😙', '😚', '🙂', '🤗', '🤔', '🤨', '😐', '😑',
'😶', '🙄', '😏', '😣', '😥', '😮', '🤐', '😯', '😪', '😫', '😴', '😌',
'😛', '😜', '🤪', '😝', '🤑', '🤗', '🤓', '😎', '🥳', '🥺', '😟', '😧',
'😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫',
'🥱', '😤', '😡', '😠', '🤬', '🤯', '😳', '🥵', '🥶', '😶🌫️', '😮💨', '🤭',
'👻', '💀', '☠️', '👽', '👾', '🤖', '🎃', '😺', '😸', '😹', '😻', '😼',
'😽', '🙀', '😿', '😾', '👐', '🤲', '👏', '🤝', '👍', '👎', '👊', '✊',
'🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '👈', '👉', '👆', '🖕', '👇',
'☝️', '✋', '🤚', '🖐️', '🖖', '👋', '🤙', '💪', '🦾', '🖥️', '💻', '⌨️',
'🖱️', '🖲️', '🕹️', '🎮', '📱', '📲', '☎️', '📞', '💬', '🗨️', '📧', '📨',
'📩', '📤', '📥', '📦', '💌', '🔒', '🔓', '🔏', '🔐', '🔑', '🔨', '⚒️',
'🔧', '🔩', '🗜️', '🔨', '⚒️', '🔧', '🔩', '🗜️', '⚙️', '🔧', '🪛', '🚪',
'🏠', '🏡', '🏘️', '🏫', '🏢', '🏬', '🏣', '🏥', '🏦', '🏪', '🏫', '🏨',
'🗼', '🏯', '🏰', '🕍', '🕌', '🕋', '⛪', '🩺', '⚕️', '🛠️', '🚑', '🚓',
'🚔', '🚒', '🚐', '🚚', '🚛', '🚜', '🛵', '🏍️', '🚲', '🛹', '🛶', '⛵',
'🚤', '🛳️', '⛴️', '🚢', '✈️', '🛫', '🛬', '💺', '🚀', '🚁', '🛸', '🚂',
'🚆', '🚄', '🚅', '🚈', '🚉', '🚊', '🚝', '🚞', '🚋', '🚌', '🚍', '🚎', '🚐'
],
// 五官数据,已按分类组织
features: {
eyes: ['👁️', '👀', '💧', '✨', '🔥', '💫', '💀', '👾', '🐱', '🐶', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🦄', '🐔', '🐧', '🐦', '🐤', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗', '🐴', '🦌', '🐭', '🐹', '🐰', '🦝', '🦡', '🦘', '🦃', '🦚', '🦜', '🐍', '🐢', '🦎', '🐊', '🐅', '🐆', '🦓', '🦍', '🦧', '🐘', '🦛', '🦏', '🐪', '🐫', '🦒', '🐃', '🐂', '🐄', '🐎', '🐖', '🐏', '🐑', '🦙', '🐐', '🐕', '🐩', '🐈', '🐓', '🦃', '🕊️', '🐇', '🐁', '🐀', '🐿️', '🦔', '🦇', '🐉', '🐲', '🐊', '🐢', '🐍', '🦎', '🐉', '🐲', '🦕', '🦖', '🐳', '🐋', '🐬', '🐟', '🐠', '🐡', '🦈', '🐚', '🐌', '🦋', '🐛', '🐜', '🐝', '🪰', '🪱', '🦠'],
mouth: ['👄', '😮', '😦', '😧', '😨', '😰', '😯', '😲', '😳', '😮💨', '😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊', '😋', '😎', '😍', '😘', '🥰', '😗', '😙', '😚', '🙂', '🤗', '🤔', '🤨', '😐', '😑', '😶', '🙄', '😏', '😣', '😥', '😮', '🤐', '😯', '😪', '😫', '😴', '😌', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤓', '😎', '🥳', '🥺', '😟', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '🤯', '😳', '🥵', '🥶', '😶🌫️', '😮💨', '🤭',
],
nose: ['👃', '🐽'], // 简化鼻子表情,您可以根据需要添加更多
eyebrows: ['〰️', '〽️', '⬆️', '⬇️', '↗️', '↘️', '↔️'], // 简化眉毛表情
accessories: ['👓', '🕶️', '🥽', '👑', '👒', '🎩', '🧢', '🎗️', '🎀', '🧣', '🧤', '💍', '💎']
},
decorations: ['✨', '🌟', '💫', '🌠', '🌙', '☀️', '⭐', '💥', '🔥', '💦', '💨', '💫', '🌸', '🌹', '🌺', '🌻', '🌼', '🌷', '💐', '🌱', '🌿', '🍃', '🍂', '🍁', '🎄', '🌲', '🌳', '🌴', '🌵', '🌾', '🌿', '🍀', '🍁', '🍂', '🍃', '🌿', '🌱', '🌼', '🌸', '🌹', '🌺', '🌻', '🌼', '🌷', '💐', '🌱', '🌿', '🍃', '🍂', '🍁', '🎄', '🌲', '🌳', '🌴', '🌵', '🌾', '🌿', '🍀', '✨', '🌟', '💫', '🌠', '🌙', '☀️', '⭐', '💥', '🔥', '💦', '💨', '💫'],
recentEmojis: [],
myCreations: JSON.parse(localStorage.getItem('myEmojiCreations')) || [],
baseEmojiPage: 0,
baseEmojisPerPage: 48,
// 组合表情区暂存的表情
combinedEmoji1: null,
combinedEmoji2: null,
// 将Emoji字符绘制到Image对象上
async drawEmojiToImage(emoji, size = 100) {
if (this.preloadedEmojis[emoji]) {
return this.preloadedEmojis[emoji];
}
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = size;
offscreenCanvas.height = size;
const offscreenCtx = offscreenCanvas.getContext('2d');
offscreenCtx.font = `${size * 0.8}px serif`; // 调整字体大小以适应图片
offscreenCtx.textAlign = 'center';
offscreenCtx.textBaseline = 'middle';
offscreenCtx.clearRect(0, 0, size, size);
offscreenCtx.fillText(emoji, size / 2, size / 2);
return new Promise(resolve => {
const img = new Image();
img.onload = () => {
this.preloadedEmojis[emoji] = img; // 缓存图片
resolve(img);
};
img.src = offscreenCanvas.toDataURL();
});
},
// 初始化
async init() {
this.canvas = document.getElementById('emojiCanvas');
this.ctx = this.canvas.getContext('2d');
// 预加载所有表情符号为图片
const allEmojis = new Set();
this.baseEmojis.forEach(e => allEmojis.add(e));
// 遍历 features 对象的每个分类数组
Object.values(this.features).forEach(arr => arr.forEach(f => allEmojis.add(f)));
this.decorations.forEach(d => allEmojis.add(d));
const preloadPromises = Array.from(allEmojis).map(emoji => this.drawEmojiToImage(emoji));
await Promise.all(preloadPromises);
console.log('所有表情图片已预加载');
// 绑定事件
this.bindEvents();
// 渲染UI
this.renderBaseEmojis();
this.renderFeatures(); // 渲染当前选中的五官分类
this.renderDecorations();
this.renderLayers();
this.renderRecentEmojis();
this.renderMyCreations();
// 默认添加一个空白图层
this.addLayer('背景', 'white');
},
// 绑定事件
bindEvents() {
// 事件委托处理表情点击(现在也支持拖拽)
document.getElementById('baseEmojiGrid').addEventListener('click', async (e) => {
if (e.target.classList.contains('emoji-item')) {
const emoji = e.target.textContent;
const img = await this.drawEmojiToImage(emoji);
this.addLayer(emoji, img);
this.renderLayers();
this.drawCanvas();
this.addToRecentEmojis(emoji);
}
});
document.getElementById('featuresGrid').addEventListener('click', async (e) => {
if (e.target.classList.contains('emoji-item')) {
const feature = e.target.textContent;
const img = await this.drawEmojiToImage(feature);
this.addLayer(`${this.currentCategory}:${feature}`, img);
this.renderLayers();
this.drawCanvas();
this.addToRecentEmojis(feature);
}
});
document.getElementById('decorationsGrid').addEventListener('click', async (e) => {
if (e.target.classList.contains('emoji-item')) {
const decoration = e.target.textContent;
const img = await this.drawEmojiToImage(decoration);
this.addLayer(`装饰:${decoration}`, img);
this.renderLayers();
this.drawCanvas();
this.addToRecentEmojis(decoration);
}
});
document.getElementById('recentEmojisGrid').addEventListener('click', async (e) => {
if (e.target.classList.contains('emoji-item')) {
const emoji = e.target.textContent;
const img = await this.drawEmojiToImage(emoji);
this.addLayer(emoji, img);
this.renderLayers();
this.drawCanvas();
}
});
// 基础表情翻页
document.getElementById('prevBaseEmoji').addEventListener('click', () => {
if (this.baseEmojiPage > 0) {
this.baseEmojiPage--;
this.renderBaseEmojis();
}
});
document.getElementById('nextBaseEmoji').addEventListener('click', () => {
if ((this.baseEmojiPage + 1) * this.baseEmojisPerPage < this.baseEmojis.length) {
this.baseEmojiPage++;
this.renderBaseEmojis();
}
});
// 五官分类选择
document.querySelectorAll('.tool-btn[data-category]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tool-btn[data-category]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.currentCategory = btn.dataset.category;
this.renderFeatures();
});
});
// 图层选择
document.getElementById('layersList').addEventListener('click', (e) => {
const layerItem = e.target.closest('.layer-item');
if (layerItem) {
const layerIndex = parseInt(layerItem.dataset.index);
this.selectLayer(layerIndex);
this.renderLayers();
this.drawCanvas();
}
});
// 编辑工具选择
document.querySelectorAll('#moveTool, #rotateTool, #flipTool, #opacityTool, #deleteTool, #duplicateTool, #mergeTool').forEach(tool => {
tool.addEventListener('click', () => {
document.querySelectorAll('.tool-btn:not([data-category])').forEach(t => t.classList.remove('active'));
tool.classList.add('active');
this.selectedTool = tool.id.replace('Tool', '').toLowerCase();
if (this.currentLayer !== null) {
const layer = this.layers[this.currentLayer];
switch (this.selectedTool) {
case 'flip':
layer.flipped = !layer.flipped;
this.drawCanvas();
break;
case 'delete':
if (this.currentLayer > 0) {
this.layers.splice(this.currentLayer, 1);
this.currentLayer = this.layers.length > 0 ? Math.min(this.layers.length - 1, this.currentLayer) : null;
this.renderLayers();
this.drawCanvas();
} else {
showNotification('提示', '不能删除背景图层!', 'warning');
}
break;
case 'duplicate':
const newLayer = JSON.parse(JSON.stringify(layer));
if (layer.content instanceof HTMLImageElement) {
newLayer.content = layer.content;
} else if (typeof newLayer.content === 'string' && newLayer.content.startsWith('#')) {
newLayer.content = layer.content;
}
newLayer.x += 10;
newLayer.y += 10;
newLayer.name += " (复制)";
this.layers.push(newLayer);
this.currentLayer = this.layers.length - 1;
this.renderLayers();
this.drawCanvas();
break;
case 'merge':
if (this.layers.length > 1 && this.currentLayer > 0) {
this.mergeLayers(this.currentLayer, this.currentLayer - 1);
} else {
showNotification('提示', '至少需要两个图层(非背景图层)才能合并。', 'warning');
}
break;
}
} else if (['flip', 'delete', 'duplicate', 'merge'].includes(this.selectedTool)) {
showNotification('提示', '请先选择一个图层进行操作。', 'info');
}
});
});
// 画布交互
this.canvas.addEventListener('mousedown', (e) => this.onCanvasMouseDown(e));
this.canvas.addEventListener('mousemove', (e) => this.onCanvasMouseMove(e));
window.addEventListener('mouseup', () => this.onCanvasMouseUp());
this.canvas.addEventListener('wheel', (e) => this.onCanvasWheel(e));
// ------------- 拖拽功能事件监听 -------------
// 使所有emoji-item可拖拽
document.querySelectorAll('.emoji-grid').forEach(grid => {
grid.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('emoji-item')) {
e.dataTransfer.setData('text/plain', e.target.textContent);
e.dataTransfer.effectAllowed = 'copy';
}
});
});
// 组合表情放置区
const dropArea1 = document.getElementById('dropArea1');
const dropArea2 = document.getElementById('dropArea2');
[dropArea1, dropArea2].forEach(area => {
area.addEventListener('dragover', (e) => {
e.preventDefault(); // 允许放置
e.dataTransfer.dropEffect = 'copy';
area.classList.add('drag-over');
});
area.addEventListener('dragleave', (e) => {
area.classList.remove('drag-over');
});
area.addEventListener('drop', async (e) => {
e.preventDefault();
area.classList.remove('drag-over');
const emoji = e.dataTransfer.getData('text/plain');
if (emoji) {
const displaySpan = area.querySelector('span:last-child');
const placeholderSpan = area.querySelector('span:first-child');
displaySpan.textContent = emoji; // 显示拖入的表情
placeholderSpan.classList.add('hidden'); // 隐藏placeholder
if (area.id === 'dropArea1') {
this.combinedEmoji1 = emoji;
} else {
this.combinedEmoji2 = emoji;
}
showNotification('提示', `表情 "${emoji}" 已放置到组合区`, 'info');
}
});
});
// 画布拖拽放置
this.canvas.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
this.canvas.classList.add('drag-over'); // 可以添加视觉反馈,比如边框变色
});
this.canvas.addEventListener('dragleave', (e) => {
this.canvas.classList.remove('drag-over');
});
this.canvas.addEventListener('drop', async (e) => {
e.preventDefault();
this.canvas.classList.remove('drag-over');
const emoji = e.dataTransfer.getData('text/plain');
if (emoji) {
const img = await this.drawEmojiToImage(emoji);
const rect = this.canvas.getBoundingClientRect();
// 将鼠标位置转换为画布坐标
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.addLayer(emoji, img);
const newLayer = this.layers[this.layers.length - 1];
newLayer.x = x; // 放置在鼠标点击位置
newLayer.y = y;
this.renderLayers();
this.drawCanvas();
this.addToRecentEmojis(emoji);
showNotification('成功', `表情 "${emoji}" 已添加到画布`, 'success');
}
});
// 组合表情按钮
document.getElementById('combineBtn').addEventListener('click', () => this.combineEmojis());
// 保存表情
document.getElementById('saveBtn').addEventListener('click', () => this.saveEmoji());
document.getElementById('saveToCollectionBtn').addEventListener('click', () => this.saveToCollection());
// 返回按钮
document.getElementById('backBtn').addEventListener('click', () => {
showNotification('提示', '点击左上角返回主界面', 'info');
});
// 关闭通知
document.getElementById('closeNotification').addEventListener('click', () => {
document.getElementById('notification').classList.remove('translate-x-0');
document.getElementById('notification').classList.add('translate-x-full');
});
},
// 鼠标滚轮事件处理(用于缩放)
onCanvasWheel(e) {
e.preventDefault(); // 阻止页面滚动
if (this.currentLayer === null || this.layers[this.currentLayer].name === '背景') return;
const layer = this.layers[this.currentLayer];
const scaleAmount = 0.05; // 每次滚动的缩放量
if (e.deltaY < 0) {
// 向上滚动,放大
layer.scale += scaleAmount;
} else {
// 向下滚动,缩小
layer.scale -= scaleAmount;
}
layer.scale = Math.max(0.1, Math.min(5, layer.scale)); // 限制缩放范围
this.drawCanvas();
},
// 合并图层功能 (完善:现在是真正的图片合并)
async mergeLayers(layerIndex1, layerIndex2) {
const layer1 = this.layers[layerIndex1];
const layer2 = this.layers[layerIndex2];
if (!layer1 || !layer2 || layer1.name === '背景' || layer2.name === '背景') {
showNotification('错误', '无法合并背景图层或不存在的图层。', 'error');
return;
}
const mergeCanvas = document.createElement('canvas');
mergeCanvas.width = this.canvas.width;
mergeCanvas.height = this.canvas.height;
const mergeCtx = mergeCanvas.getContext('2d');
[layer2, layer1].forEach(layer => {
if (!layer.visible || !(layer.content instanceof HTMLImageElement)) return;
mergeCtx.save();
mergeCtx.translate(layer.x, layer.y);
mergeCtx.rotate(layer.rotation * Math.PI / 180);
mergeCtx.scale(layer.flipped ? -layer.scale : layer.scale, layer.scale);
mergeCtx.globalAlpha = layer.opacity;
const imgWidth = layer.content.width;
const imgHeight = layer.content.height;
mergeCtx.drawImage(layer.content, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
mergeCtx.restore();
});
const mergedImage = new Image();
mergedImage.src = mergeCanvas.toDataURL('image/png');
mergedImage.onload = () => {
const indicesToDelete = [layerIndex1, layerIndex2].sort((a, b) => b - a);
indicesToDelete.forEach(idx => this.layers.splice(idx, 1));
const newLayerName = `${layer2.name.split(' ')[0]} & ${layer1.name.split(' ')[0]} (合并)`;
this.addLayer(newLayerName, mergedImage);
this.layers[this.layers.length - 1].x = this.canvas.width / 2;
this.layers[this.layers.length - 1].y = this.canvas.height / 2;
this.layers[this.layers.length - 1].scale = 1;
this.renderLayers();
this.drawCanvas();
showNotification('成功', '图层已合并!', 'success');
};
},
// 渲染基础表情 (添加 draggable 属性)
renderBaseEmojis() {
const start = this.baseEmojiPage * this.baseEmojisPerPage;
const end = Math.min(start + this.baseEmojisPerPage, this.baseEmojis.length);
const pageEmojis = this.baseEmojis.slice(start, end);
const grid = document.getElementById('baseEmojiGrid');
grid.innerHTML = '';
pageEmojis.forEach(emoji => {
const emojiItem = document.createElement('div');
emojiItem.className = 'emoji-item bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors';
emojiItem.textContent = emoji;
emojiItem.title = `添加 ${emoji}`;
emojiItem.draggable = true; // 使其可拖拽
grid.appendChild(emojiItem);
});
},
// 渲染五官 (添加 draggable 属性)
renderFeatures() {
const grid = document.getElementById('featuresGrid');
grid.innerHTML = '';
const emojisToShow = this.features[this.currentCategory] || [];
emojisToShow.forEach(feature => {
const featureItem = document.createElement('div');
featureItem.className = 'emoji-item bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors';
featureItem.textContent = feature;
featureItem.title = `添加 ${feature}`;
featureItem.draggable = true; // 使其可拖拽
grid.appendChild(featureItem);
});
},
// 渲染装饰 (添加 draggable 属性)
renderDecorations() {
const grid = document.getElementById('decorationsGrid');
grid.innerHTML = '';
this.decorations.forEach(decoration => {
const decorationItem = document.createElement('div');
decorationItem.className = 'emoji-item bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors';
decorationItem.textContent = decoration;
decorationItem.title = `添加 ${decoration}`;
decorationItem.draggable = true; // 使其可拖拽
grid.appendChild(decorationItem);
});
},
// 渲染图层
renderLayers() {
const layersList = document.getElementById('layersList');
layersList.innerHTML = '';
this.layers.slice().reverse().forEach((layer, originalIndex) => {
const index = this.layers.length - 1 - originalIndex;
const layerItem = document.createElement('div');
layerItem.className = `layer-item flex items-center justify-between ${this.currentLayer === index ? 'bg-primary/10 border-l-4 border-primary' : ''} p-2 rounded-md cursor-pointer hover:bg-gray-50`;
layerItem.dataset.index = index;
const layerInfo = document.createElement('div');
layerInfo.className = 'flex items-center flex-grow'; // flex-grow to make clickable area larger
const layerIcon = document.createElement('div');
layerIcon.className = 'w-6 h-6 rounded bg-gray-200 flex items-center justify-center mr-2 text-sm';
if (typeof layer.content === 'string' && layer.content.startsWith('#')) {
layerIcon.style.backgroundColor = layer.content;
layerIcon.textContent = '';
} else if (layer.content instanceof HTMLImageElement) {
const emojiChar = Object.keys(this.preloadedEmojis).find(key => this.preloadedEmojis[key] === layer.content);
layerIcon.textContent = emojiChar || '🖼️';
} else {
layerIcon.textContent = '⬜';
}
const layerName = document.createElement('span');
layerName.className = 'text-sm truncate'; // Add truncate for long names
layerName.textContent = layer.name;
const layerControls = document.createElement('div');
layerControls.className = 'flex items-center space-x-1 ml-2';
const moveUpBtn = document.createElement('button');
moveUpBtn.className = 'text-gray-500 hover:text-primary text-xs p-1 rounded-full hover:bg-gray-100';
moveUpBtn.innerHTML = '<i class="fa fa-arrow-up"></i>';
moveUpBtn.title = '上移图层';
moveUpBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.moveLayerUp(index);
});
const moveDownBtn = document.createElement('button');
moveDownBtn.className = 'text-gray-500 hover:text-primary text-xs p-1 rounded-full hover:bg-gray-100';
moveDownBtn.innerHTML = '<i class="fa fa-arrow-down"></i>';
moveDownBtn.title = '下移图层';
moveDownBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.moveLayerDown(index);
});
const visibilityBtn = document.createElement('button');
visibilityBtn.className = `text-gray-500 hover:text-primary text-xs p-1 rounded-full hover:bg-gray-100 ${layer.visible ? '' : 'opacity-50'}`;
visibilityBtn.innerHTML = `<i class="fa fa-${layer.visible ? 'eye' : 'eye-slash'}"></i>`;
visibilityBtn.title = layer.visible ? '隐藏图层' : '显示图层';
visibilityBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleLayerVisibility(index);
});
layerInfo.appendChild(layerIcon);
layerInfo.appendChild(layerName);
layerControls.appendChild(moveUpBtn);
layerControls.appendChild(moveDownBtn);
layerControls.appendChild(visibilityBtn);
layerItem.appendChild(layerInfo);
layerItem.appendChild(layerControls);
layersList.appendChild(layerItem);
});
},
// 渲染最近使用的表情 (添加 draggable 属性)
renderRecentEmojis() {
const grid = document.getElementById('recentEmojisGrid');
grid.innerHTML = '';
this.recentEmojis.slice(0, 12).forEach(emoji => { // 只显示最近12个
const emojiItem = document.createElement('div');
emojiItem.className = 'emoji-item bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors';
emojiItem.textContent = emoji;
emojiItem.title = `重新添加 ${emoji}`;
emojiItem.draggable = true; // 使其可拖拽
emojiItem.addEventListener('click', async () => {
const img = await this.drawEmojiToImage(emoji);
this.addLayer(emoji, img);
this.renderLayers();
this.drawCanvas();
});
grid.appendChild(emojiItem);
});
},
// 渲染我的创作
renderMyCreations() {
const grid = document.getElementById('myCreationsGrid');
grid.innerHTML = '';
this.myCreations.slice(-9).forEach((creation, index) => {
const creationItem = document.createElement('div');
creationItem.className = 'relative group';
const imgContainer = document.createElement('div');
imgContainer.className = 'aspect-square rounded-lg overflow-hidden bg-gray-100 flex items-center justify-center';
const img = document.createElement('img');
img.src = creation.image;
img.className = 'max-w-full max-h-full object-contain';
img.alt = creation.name;
const overlay = document.createElement('div');
overlay.className = 'absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center flex-col';
overlay.style.pointerEvents = 'none'; // 防止覆盖下方点击事件,只让按钮响应
const name = document.createElement('div');
name.className = 'text-white text-xs font-medium text-center mb-2';
name.textContent = creation.name;
const actions = document.createElement('div');
actions.className = 'flex space-x-2';
actions.style.pointerEvents = 'auto'; // 让按钮可点击
const useBtn = document.createElement('button');
useBtn.className = 'bg-white/20 hover:bg-white/30 text-white rounded-full p-1';
useBtn.innerHTML = '<i class="fa fa-pencil"></i>';
useBtn.title = '加载此创作到画布';
useBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡,避免触发父元素的其他事件
this.loadCreation(index)
});
const deleteBtn = document.createElement('button');
deleteBtn.className = 'bg-white/20 hover:bg-white/30 text-white rounded-full p-1';
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
deleteBtn.title = '删除此创作';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
this.deleteCreation(index)
});
actions.appendChild(useBtn);
actions.appendChild(deleteBtn);
overlay.appendChild(name);
overlay.appendChild(actions);
imgContainer.appendChild(img);
creationItem.appendChild(imgContainer);
creationItem.appendChild(overlay);
grid.appendChild(creationItem);
});
},
// 添加图层
addLayer(name, content) {
const layer = {
name,
content,
x: this.canvas.width / 2,
y: this.canvas.height / 2,
scale: 1,
rotation: 0,
opacity: 1,
flipped: false,
visible: true,
width: 100,
height: 100
};
if (content instanceof HTMLImageElement) {
layer.width = content.width;
layer.height = content.height;
const maxDim = Math.max(content.width, content.height);
const canvasMaxDim = Math.min(this.canvas.width, this.canvas.height) * 0.7;
if (maxDim > canvasMaxDim) {
layer.scale = canvasMaxDim / maxDim;
}
} else if (typeof content === 'string' && content.startsWith('#')) {
layer.x = this.canvas.width / 2;
layer.y = this.canvas.height / 2;
layer.width = this.canvas.width;
layer.height = this.canvas.height;
}
this.layers.push(layer);
this.currentLayer = this.layers.length - 1;
},
// 选择图层
selectLayer(index) {
if (index >= 0 && index < this.layers.length) {
this.currentLayer = index;
}
},
// 移动图层
moveLayerUp(index) {
if (index > 0 && this.layers[index].name !== '背景') {
[this.layers[index], this.layers[index - 1]] = [this.layers[index - 1], this.layers[index]];
this.currentLayer = index - 1;
this.renderLayers();
this.drawCanvas();
}
},
// 下移图层
moveLayerDown(index) {
const indexOfBackground = this.layers.findIndex(l => l.name === '背景');
if (index < this.layers.length - 1 && (indexOfBackground === -1 || index + 1 !== indexOfBackground)) {
[this.layers[index], this.layers[index + 1]] = [this.layers[index + 1], this.layers[index]];
this.currentLayer = index + 1;
this.renderLayers();
this.drawCanvas();
}
},
// 切换图层可见性
toggleLayerVisibility(index) {
this.layers[index].visible = !this.layers[index].visible;
this.renderLayers();
this.drawCanvas();
},
// 添加到最近使用
addToRecentEmojis(emoji) {
const index = this.recentEmojis.indexOf(emoji);
if (index !== -1) {
this.recentEmojis.splice(index, 1);
}
this.recentEmojis.unshift(emoji);
if (this.recentEmojis.length > 24) {
this.recentEmojis.pop();
}
this.renderRecentEmojis();
},
// 绘制画布
drawCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const backgroundLayer = this.layers.find(layer => layer.name === '背景');
if (backgroundLayer && backgroundLayer.visible && typeof backgroundLayer.content === 'string' && backgroundLayer.content.startsWith('#')) {
this.ctx.fillStyle = backgroundLayer.content;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
this.layers.forEach((layer, index) => {
if (!layer.visible) return;
if (layer.name === '背景' && typeof layer.content === 'string' && layer.content.startsWith('#')) return;
this.ctx.save();
this.ctx.translate(layer.x, layer.y);
this.ctx.rotate(layer.rotation * Math.PI / 180);
this.ctx.scale(layer.flipped ? -layer.scale : layer.scale, layer.scale);
this.ctx.globalAlpha = layer.opacity;
if (layer.content instanceof HTMLImageElement) {
const imgWidth = layer.content.width;
const imgHeight = layer.content.height;
this.ctx.drawImage(layer.content, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
}
this.ctx.restore();
});
if (this.currentLayer !== null && this.layers[this.currentLayer].visible) {
const layer = this.layers[this.currentLayer];
if (layer.name === '背景' && typeof layer.content === 'string' && layer.content.startsWith('#')) return;
this.ctx.save();
this.ctx.translate(layer.x, layer.y);
this.ctx.rotate(layer.rotation * Math.PI / 180);
this.ctx.scale(layer.flipped ? -layer.scale : layer.scale, layer.scale);
this.ctx.strokeStyle = '#7C3AED';
this.ctx.lineWidth = 2;
this.ctx.setLineDash([5, 3]);
const rectWidth = layer.content instanceof HTMLImageElement ? layer.content.width : layer.width;
const rectHeight = layer.content instanceof HTMLImageElement ? layer.content.height : layer.height;
this.ctx.strokeRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
this.ctx.restore();
}
},
getLayerLocalCoordinates(layer, canvasX, canvasY) {
const translateX = layer.x;
const translateY = layer.y;
const rotationRad = layer.rotation * Math.PI / 180;
const scaleX = layer.flipped ? -layer.scale : layer.scale;
const scaleY = layer.scale;
const tempX = canvasX - translateX;
const tempY = canvasY - translateY;
const cos = Math.cos(-rotationRad);
const sin = Math.sin(-rotationRad);
const rotatedX = tempX * cos - tempY * sin;
const rotatedY = tempX * sin + tempY * cos;
const localX = rotatedX / scaleX;
const localY = rotatedY / scaleY;
return { x: localX, y: localY };
},
isPointInLayer(layer, mouseX, mouseY) {
if (!layer.visible || layer.name === '背景') return false;
const { x: localX, y: localY } = this.getLayerLocalCoordinates(layer, mouseX, mouseY);
const halfWidth = (layer.content instanceof HTMLImageElement ? layer.content.width : layer.width) / 2;
const halfHeight = (layer.content instanceof HTMLImageElement ? layer.content.height : layer.height) / 2;
return localX >= -halfWidth && localX <= halfWidth &&
localY >= -halfHeight && localY <= halfHeight;
},
onCanvasMouseDown(e) {
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
for (let i = this.layers.length - 1; i >= 0; i--) {
const layer = this.layers[i];
if (this.isPointInLayer(layer, mouseX, mouseY)) {
this.selectLayer(i);
this.renderLayers();
const currentLayer = this.layers[this.currentLayer];
this.dragInfo = {
startX: mouseX,
startY: mouseY,
startLayerX: currentLayer.x,
startLayerY: currentLayer.y,
startRotation: currentLayer.rotation,
startOpacity: currentLayer.opacity
};
this.drawCanvas();
return;
}
}
this.currentLayer = null;
this.renderLayers();
this.drawCanvas();
},
onCanvasMouseMove(e) {
if (!this.dragInfo || this.currentLayer === null) return;
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const layer = this.layers[this.currentLayer];
switch (this.selectedTool) {
case 'move':
const dx = mouseX - this.dragInfo.startX;
const dy = mouseY - this.dragInfo.startY;
layer.x = this.dragInfo.startLayerX + dx;
layer.y = this.dragInfo.startLayerY + dy;
this.drawCanvas();
break;
case 'rotate':
const angleRad = Math.atan2(mouseY - layer.y, mouseX - layer.x);
const startAngleRad = Math.atan2(this.dragInfo.startY - layer.y, this.dragInfo.startX - layer.x);
const angleDiffRad = angleRad - startAngleRad;
layer.rotation = (this.dragInfo.startRotation + angleDiffRad * 180 / Math.PI) % 360;
this.drawCanvas();
break;
case 'opacity':
const dyOpacity = mouseY - this.dragInfo.startY;
layer.opacity = this.dragInfo.startOpacity - dyOpacity / 200;
layer.opacity = Math.max(0, Math.min(1, layer.opacity));
this.drawCanvas();
break;
}
},
onCanvasMouseUp() {
this.dragInfo = null;
},
// 组合表情 (修改为像素级混合)
async combineEmojis() {
const dropArea1 = document.getElementById('dropArea1');
const dropArea2 = document.getElementById('dropArea2');
let addedCount = 0;
// 如果没有选择任何表情,则提示
if (!this.combinedEmoji1 && !this.combinedEmoji2) {
showNotification('错误', '请拖拽至少一个表情到组合区', 'error');
return;
}
// 清除除了背景以外的所有图层
this.layers = this.layers.filter(layer => layer.name === '背景');
this.currentLayer = this.layers.length > 0 ? 0 : null; // 选中背景层(如果存在)
if (this.combinedEmoji1 && this.combinedEmoji2) {
// 如果两个表情都存在,则进行像素级组合
const img1 = await this.drawEmojiToImage(this.combinedEmoji1, 200); // 绘制大一点的图像以便组合
const img2 = await this.drawEmojiToImage(this.combinedEmoji2, 200);
if (!img1 || !img2) {
showNotification('警告', '无法加载一个或两个表情进行组合。', 'warning');
return;
}
// 创建一个离屏 Canvas 用于组合
const combinedCanvas = document.createElement('canvas');
combinedCanvas.width = this.canvas.width; // 使用主画布大小
combinedCanvas.height = this.canvas.height;
const combinedCtx = combinedCanvas.getContext('2d');
// 绘制背景(如果需要,例如白色背景)
const backgroundLayer = this.layers.find(layer => layer.name === '背景');
if (backgroundLayer && backgroundLayer.visible && typeof backgroundLayer.content === 'string' && backgroundLayer.content.startsWith('#')) {
combinedCtx.fillStyle = backgroundLayer.content;
combinedCtx.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height);
} else {
combinedCtx.fillStyle = '#FFFFFF'; // 默认白色背景
combinedCtx.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height);
}
// 将第一个表情绘制到中心
combinedCtx.globalAlpha = 1; // 完全不透明
combinedCtx.drawImage(img1, (combinedCanvas.width - img1.width) / 2, (combinedCanvas.height - img1.height) / 2, img1.width, img1.height);
// 将第二个表情以半透明方式绘制在第一个之上
combinedCtx.globalAlpha = 0.6; // 设置透明度
combinedCtx.drawImage(img2, (combinedCanvas.width - img2.width) / 2, (combinedCanvas.height - img2.height) / 2, img2.width, img2.height);
// 将组合后的图像转换为 Image 对象
const combinedImage = new Image();
combinedImage.src = combinedCanvas.toDataURL('image/png');
combinedImage.onload = () => {
const newLayerName = `${this.combinedEmoji1} + ${this.combinedEmoji2} (组合)`;
this.addLayer(newLayerName, combinedImage);
const newLayer = this.layers[this.layers.length - 1];
newLayer.x = this.canvas.width / 2;
newLayer.y = this.canvas.height / 2;
newLayer.scale = 0.8; // 适当缩小,因为我们绘制的图像较大
this.renderLayers();
this.drawCanvas();
this.addToRecentEmojis(this.combinedEmoji1);
this.addToRecentEmojis(this.combinedEmoji2);
showNotification('成功', '表情已组合成新图层!', 'success');
this.clearCombinationArea(); // 清空组合区
};
} else {
// 只有一个表情,直接添加到画布(保持原有逻辑)
const emojiToAdd = this.combinedEmoji1 || this.combinedEmoji2;
const img = await this.drawEmojiToImage(emojiToAdd);
if (img) {
this.addLayer(emojiToAdd, img);
const layer = this.layers[this.layers.length - 1];
layer.x = this.canvas.width / 2;
layer.y = this.canvas.height / 2;
this.renderLayers();
this.drawCanvas();
this.addToRecentEmojis(emojiToAdd);
showNotification('成功', `表情 "${emojiToAdd}" 已添加到画布`, 'success');
this.clearCombinationArea(); // 清空组合区
} else {
showNotification('警告', `无法加载表情 "${emojiToAdd}"`, 'warning');
}
}
},
// 新增:清空组合区内容并恢复placeholder
clearCombinationArea() {
this.combinedEmoji1 = null;
this.combinedEmoji2 = null;
const dropArea1 = document.getElementById('dropArea1');
const dropArea2 = document.getElementById('dropArea2');
dropArea1.querySelector('span:last-child').textContent = '';
dropArea1.querySelector('span:first-child').classList.remove('hidden');
dropArea2.querySelector('span:last-child').textContent = '';
dropArea2.querySelector('span:first-child').classList.remove('hidden');
},
// 保存表情
saveEmoji() {
const format = document.getElementById('saveFormat').value;
const name = document.getElementById('emojiName').value.trim() || '我的表情';
try {
const finalCanvas = document.createElement('canvas');
finalCanvas.width = this.canvas.width;
finalCanvas.height = this.canvas.height;
const finalCtx = finalCanvas.getContext('2d');
const backgroundLayer = this.layers.find(layer => layer.name === '背景');
if (backgroundLayer && backgroundLayer.visible && typeof backgroundLayer.content === 'string' && backgroundLayer.content.startsWith('#')) {
finalCtx.fillStyle = backgroundLayer.content;
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
} else if (format === 'jpeg') {
finalCtx.fillStyle = '#FFFFFF';
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
}
this.layers.forEach(layer => {
if (!layer.visible) return;
if (layer.name === '背景' && typeof layer.content === 'string' && layer.content.startsWith('#')) return;
finalCtx.save();
finalCtx.translate(layer.x, layer.y);
finalCtx.rotate(layer.rotation * Math.PI / 180);
finalCtx.scale(layer.flipped ? -layer.scale : layer.scale, layer.scale);
finalCtx.globalAlpha = layer.opacity;
if (layer.content instanceof HTMLImageElement) {
const imgWidth = layer.content.width;
const imgHeight = layer.content.height;
finalCtx.drawImage(layer.content, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
}
finalCtx.restore();
});
const dataURL = finalCanvas.toDataURL(`image/${format}`, 1.0);
const link = document.createElement('a');
link.download = `${name}.${format}`;
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showNotification('成功', '表情已保存到本地', 'success');
} catch (error) {
showNotification('错误', '保存表情失败: ' + error.message, 'error');
}
},
// 保存到收藏
saveToCollection() {
const name = document.getElementById('emojiName').value.trim() || '我的表情';
const tags = document.getElementById('emojiTags').value.trim().split(',').map(tag => tag.trim()).filter(tag => tag);
try {
const finalCanvas = document.createElement('canvas');
finalCanvas.width = this.canvas.width;
finalCanvas.height = this.canvas.height;
const finalCtx = finalCanvas.getContext('2d');
const backgroundLayer = this.layers.find(layer => layer.name === '背景');
if (backgroundLayer && backgroundLayer.visible && typeof backgroundLayer.content === 'string' && backgroundLayer.content.startsWith('#')) {
finalCtx.fillStyle = backgroundLayer.content;
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
} else {
finalCtx.fillStyle = '#FFFFFF';
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
}
this.layers.forEach(layer => {
if (!layer.visible) return;
if (layer.name === '背景' && typeof layer.content === 'string' && layer.content.startsWith('#')) return;
finalCtx.save();
finalCtx.translate(layer.x, layer.y);
finalCtx.rotate(layer.rotation * Math.PI / 180);
finalCtx.scale(layer.flipped ? -layer.scale : layer.scale, layer.scale);
finalCtx.globalAlpha = layer.opacity;
if (layer.content instanceof HTMLImageElement) {
const imgWidth = layer.content.width;
const imgHeight = layer.content.height;
finalCtx.drawImage(layer.content, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
}
finalCtx.restore();
});
const dataURL = finalCanvas.toDataURL('image/png', 1.0);
const creation = {
name,
tags,
image: dataURL,
createdAt: new Date().toISOString()
};
this.myCreations.unshift(creation);
if (this.myCreations.length > 50) {
this.myCreations.pop();
}
localStorage.setItem('myEmojiCreations', JSON.stringify(this.myCreations));
this.renderMyCreations();
showNotification('成功', '表情已保存到收藏夹', 'success');
} catch (error) {
showNotification('错误', '保存表情失败: ' + error.message, 'error');
}
} ,
// 加载创作
loadCreation(index) {
if (index >= 0 && index < this.myCreations.length) {
const creation = this.myCreations[index];
this.layers = [];
this.addLayer('背景', 'white');
const img = new Image();
img.src = creation.image;
img.onload = () => {
this.addLayer(creation.name, img);
const newLayer = this.layers[this.layers.length - 1];
newLayer.x = this.canvas.width / 2;
newLayer.y = this.canvas.height / 2;
newLayer.scale = Math.min(this.canvas.width / img.width, this.canvas.height / img.height, 1);
this.renderLayers();
this.drawCanvas();
document.getElementById('emojiName').value = creation.name;
document.getElementById('emojiTags').value = creation.tags.join(', ');
showNotification('成功', '已加载表情: ' + creation.name, 'success');
};
}
},
// 删除创作
deleteCreation(index) {
if (index >= 0 && index < this.myCreations.length) {
if (confirm(`确定要删除 "${this.myCreations[index].name}" 吗?`)) {
this.myCreations.splice(index, 1);
localStorage.setItem('myEmojiCreations', JSON.stringify(this.myCreations));
this.renderMyCreations();
showNotification('成功', '表情已删除', 'success');
}
}
}
};
// 显示通知
function showNotification(title, message, type = 'info') {
const notification = document.getElementById('notification');
const notificationTitle = document.getElementById('notificationTitle');
const notificationMessage = document.getElementById('notificationMessage');
const notificationIcon = document.getElementById('notificationIcon');
notificationTitle.textContent = title;
notificationMessage.textContent = message;
notificationIcon.className = 'w-8 h-8 rounded-full flex items-center justify-center';
if (type === 'success') {
notificationIcon.className += ' bg-green-100 text-green-500';
notificationIcon.innerHTML = '<i class="fa fa-check"></i>';
} else if (type === 'error') {
notificationIcon.className += ' bg-red-100 text-red-500';
notificationIcon.innerHTML = '<i class="fa fa-times"></i>';
} else if (type === 'warning') {
notificationIcon.className += ' bg-yellow-100 text-yellow-500';
notificationIcon.innerHTML = '<i class="fa fa-exclamation-triangle"></i>';
} else {
notificationIcon.className += ' bg-blue-100 text-blue-500';
notificationIcon.innerHTML = '<i class="fa fa-info-circle"></i>';
}
notification.classList.remove('translate-x-full');
notification.classList.add('translate-x-0');
setTimeout(() => {
notification.classList.remove('translate-x-0');
notification.classList.add('translate-x-full');
}, 3000);
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
emojiCreator.init();
});
</script>
</body>
</html>