如何DIY一个独特的表情?居然如此简单!

不说废话,这是链接,速来围观「DIY 表情」:aicoding.juejin.cn/aicoding/wo...


前端小白也能轻松制作出一个网页小游戏,如今是ai的时代,我们只需要用语言描述出自己的想法,利用工具就能将它实现。以下是我用Trae DE实现的一个可以DIY独特表情的在线工具。(最后附上源代码)

在数字化沟通日益普及的今天,表情包已成为我们表达情绪、活跃聊天的不可或缺的工具。然而,千篇一律的通用表情是否已无法满足您独特的个性表达?现在,隆重推出------ "表情实验室"(Emoji Lab) ,一款旨在激发您无限创意、让您轻松打造专属表情包的强大在线工具!

"表情实验室"能做什么?

"表情实验室"是一个真正意义上的创意画布,它将复杂的设计过程简化为直观的拖拽和点击。在这里,您可以:

  • 自由组合,天马行空: 告别固定模板!从海量的基础表情、丰富的五官部件(眼睛、嘴巴、鼻子、眉毛)以及各种有趣的装饰元素中,任意选择、拖拽组合。想要一个哭笑不得又戴着墨镜的表情?没问题!
  • 一键"魔法"组合: 这是"表情实验室"的亮点功能!将两个您喜爱的表情拖拽到下方的"组合表情区",点击"组合"按钮,神奇的事情就会发生------两个表情将以独特的透明叠加方式融合,生成一个全新的、富有层次感的"新表情"图层!这就像为您的表情施加了魔法,创造出意想不到的视觉效果。
  • 精细调整,掌控细节: 不仅仅是组合,您还可以对画布上的每一个图层进行精细控制。移动、缩放、旋转、调整透明度、水平翻转......所有操作都可在实时预览中完成,确保您的创意完美呈现。
  • 图层管理,条理清晰: 左侧的"图层"面板让您的创作井然有序。您可以轻松选择、切换图层,调整它们的显示顺序(上移/下移),甚至隐藏或删除不需要的图层。
  • 保存分享,永留精彩: 完成创作后,您可以将作品保存为高质量的PNG(支持透明背景)或JPEG格式到本地。更棒的是,通过"我的创作"功能,您的所有杰作都将被安全保存,随时可供加载、编辑或分享。

如何使用"表情实验室"?

使用"表情实验室"非常简单,只需几步,您就能成为表情包大师:

  1. 选择基础: 从右侧的"基础表情"库中,将您喜欢的表情拖拽到中间的画布上。您也可以直接点击它。

  2. 添加个性: 切换到"五官"或"装饰"分类,挑选眼睛、嘴巴、帽子、闪光等元素,拖拽到画布上的基础表情上。

  3. 精细调整: 点击画布上的图层(或左侧图层列表中的图层),激活后,使用左侧工具栏的"移动"、"旋转"、"透明度"等工具调整其位置、大小、角度和透明度。使用鼠标滚轮可以快速缩放图层。

  4. 尝试"组合魔法":

    • 将两个您想要组合的表情,分别拖拽到页面底部的"拖拽表情1到此处"和"拖拽表情2到此处"区域。
    • 确保两个区域都显示了表情后,点击旁边的"组合"按钮。
    • 画布上会自动生成一个由这两个表情透明叠加而成的"组合表情"新图层,它就像是两个表情的独特结合体!
  5. 管理图层: 利用左侧"图层"面板,您可以调整图层顺序,或者选中一个图层后使用"复制"、"删除"、"合并"等工具进行高级操作。

  6. 保存收藏: 为您的作品取个响亮的名字和标签,选择保存格式,然后点击"保存"下载到本地,或点击"收藏"将其永久保存在"我的创作"中,方便下次使用。

项目体验

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>
相关推荐
墨夏6 分钟前
TS 高级类型
前端·typescript
程序猿师兄14 分钟前
若依框架前端调用后台服务报跨域错误
前端
前端小巷子18 分钟前
跨标签页通信(三):Web Storage
前端·面试·浏览器
工呈士18 分钟前
TCP 三次握手与四次挥手详解
前端·后端·面试
柠檬味拥抱19 分钟前
面向边缘智能的MCP Bridge轻量化适配策略研究与实现
人工智能
BillKu19 分钟前
Vue3 + TypeScript + Element Plus + el-input 输入框列表按回车聚焦到下一行
前端·javascript·typescript
复苏季风20 分钟前
前端程序员unity学习笔记01: 从c#开始的入门,using命名空间,MonoBehaviour,static,public
前端
阿古达木23 分钟前
沉浸式改 bug,步步深入
前端·javascript·github
学境思源AcademicIdeas27 分钟前
如何使用ChatGPT快速完成一篇论文初稿?
人工智能·chatgpt
stoneSkySpace32 分钟前
react 自定义状态管理库
前端·react.js·前端框架