程序化几何背景生成器(html 开源)

🎨 程序化几何背景生成器

一个基于 HTML5 Canvas 的交互式几何图案背景生成器,可实时调整样式、颜色、动画效果,并能一键导出 PNG 或 CSS 代码。适用于网页设计、UI/UX 设计、创意项目或作为可视化学习工具。


✨ 核心特性

特性 说明
🔄 6 种几何图形 正方形、长方形、菱形、三角形、六边形、圆形
🎨 高级颜色控制 双主色 + 背景色,独立透明度,渐变/随机颜色模式
📏 精细尺寸调节 图案大小、间距、圆角、交错偏移(砖墙/蜂窝效果)、长宽比
🌀 实时动画 漂浮、脉冲、波浪三种动画模式,可控制速度、暂停/定格
💾 一键导出 导出 PNG 图片(保留透明度)或复制纯 CSS 代码
📱 响应式设计 移动端友好,可折叠控制面板

🚀 快速开始

在线使用

直接点击 [这里] 打开 HTML 文件,或部署到任意静态托管服务。

本地运行

  1. 将项目代码保存为 index.html

  2. 在浏览器中打开该文件

  3. 无需安装任何依赖,开箱即用


🎮 使用说明

控制面板详解

控制组 功能
图案类型 点击图标切换基本几何形状
颜色与透明度 分别设置主色、辅助色、背景色的 HEX 值及透明度滑块
尺寸与布局 调节图案尺寸、间距、圆角、交错偏移(实现砖墙/蜂窝布局)
动画控制 启用/关闭动画,选择模式,调节速度,通过滑块"冻结"某一帧
操作按钮 随机生成、重置、导出 PNG、复制 CSS

核心交互

  • 随机生成:点击"随机生成"按钮,快速获得创意灵感

  • 实时预览:所有参数调节实时反映在画布上

  • 动画控制

    • 启用动画后,可选择"漂浮"、"脉冲"、"波浪"模式

    • 通过"播放/暂停"按钮控制动画运行

    • 拖动"定格"滑块可在时间轴上选择特定帧并冻结

  • 导出选项

    • 导出 PNG:生成透明背景的高清图片

    • 复制 CSS:若图案为简单方格,可生成纯 CSS 代码(渐变背景)


🛠️ 技术栈

  • HTML5 Canvas​ - 2D 图形渲染

  • **纯 JavaScript (ES6)**​ - 无框架,轻量级

  • Tailwind CSS​ - 样式与布局

  • Inter 字体​ - 现代无衬线字体

无外部依赖,所有功能原生实现。


📁 项目结构

复制代码
几何背景生成器/
├── index.html          # 主文件(包含所有 HTML、CSS、JS)
├── README.md           # 说明文档
└── (未来可扩展)
    ├── assets/         # 图片、图标资源
    ├── js/             # 模块化 JS 文件
    └── css/            # 独立样式文件

🔧 开发与定制

扩展新图形

drawPattern()函数的 switch语句中添加新的图形绘制逻辑:

复制代码
case 'your-shape':
    // 自定义绘制代码
    break;

添加新动画模式

  1. config.animMode中添加新模式标识

  2. drawPattern()的动画计算部分实现位置/形变逻辑

  3. 在 HTML 中添加对应的控制按钮

样式定制

  • 修改 glass-panel类调整控制面板的毛玻璃效果

  • 调整 tailwind.config.js可扩展颜色与间距系统

  • 所有交互状态(悬停、激活)已在 CSS 中定义


💡 使用建议

设计应用场景

  • 网页背景:导出 PNG 或使用生成的 CSS

  • UI 元素:作为卡片、按钮的背景纹理

  • 品牌视觉:通过自定义颜色创建品牌专属图案

  • 创意素材:导出高分辨率图片用于平面设计

优化技巧

  • 对于复杂图案(圆角、交错、动画),建议导出 PNG

  • 简单方格图案可使用生成的 CSS,性能更优

  • 导出 PNG 前,可通过调整画布窗口大小控制导出分辨率

  • 使用"随机生成"功能快速探索配色与布局


🌐 浏览器兼容性

  • Chrome 90+ ✅

  • Firefox 88+ ✅

  • Safari 14+ ✅

  • Edge 90+ ✅

需支持 ES6 模块、Canvas API 及现代 CSS 特性。


📄 开源协议

本项目基于 MIT License开源,可自由使用、修改、分发。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>程序化几何背景生成器 Pro</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
        
        body {
            font-family: 'Inter', sans-serif;
            overflow: hidden;
            background: #0f172a;
        }
        
        #canvas-container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 0;
        }
        
        .glass-panel {
            background: rgba(15, 23, 42, 0.9);
            backdrop-filter: blur(16px);
            -webkit-backdrop-filter: blur(16px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
        }
        
        .control-group {
            transition: all 0.3s ease;
        }
        
        .control-group:hover {
            background: rgba(255, 255, 255, 0.05);
        }
        
        input[type="range"] {
            -webkit-appearance: none;
            appearance: none;
            background: transparent;
            cursor: pointer;
        }
        
        input[type="range"]::-webkit-slider-track {
            background: rgba(255, 255, 255, 0.2);
            height: 6px;
            border-radius: 3px;
        }
        
        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            background: #3b82f6;
            height: 18px;
            width: 18px;
            border-radius: 50%;
            margin-top: -6px;
            box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
            transition: all 0.2s;
        }
        
        input[type="range"]::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 20px rgba(59, 130, 246, 0.8);
        }
        
        input[type="color"] {
            -webkit-appearance: none;
            border: none;
            width: 32px;
            height: 32px;
            border-radius: 6px;
            cursor: pointer;
            background: none;
            padding: 0;
        }
        
        input[type="color"]::-webkit-color-swatch-wrapper {
            padding: 0;
        }
        
        input[type="color"]::-webkit-color-swatch {
            border: 2px solid rgba(255, 255, 255, 0.2);
            border-radius: 6px;
        }
        
        .pattern-btn {
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        }
        
        .pattern-btn.active {
            background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
            border-color: #60a5fa;
            box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
            transform: translateY(-2px);
        }
        
        .pattern-btn:hover:not(.active) {
            background: rgba(255, 255, 255, 0.1);
            transform: translateY(-1px);
        }
        
        .anim-mode-btn.active {
            background: rgba(59, 130, 246, 0.3);
            border-color: #3b82f6;
            color: #60a5fa;
        }
        
        /* Custom scrollbar */
        ::-webkit-scrollbar {
            width: 6px;
        }
        
        ::-webkit-scrollbar-track {
            background: rgba(255, 255, 255, 0.05);
        }
        
        ::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.2);
            border-radius: 3px;
        }
        
        ::-webkit-scrollbar-thumb:hover {
            background: rgba(255, 255, 255, 0.3);
        }
        
        .play-pause-btn {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.3s;
        }
        
        .play-pause-btn:hover {
            transform: scale(1.1);
            box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
        }
    </style>
</head>
<body class="text-white">

    <!-- Canvas Background -->
    <div id="canvas-container">
        <canvas id="patternCanvas"></canvas>
    </div>

    <!-- Control Panel -->
    <div class="fixed top-4 right-4 w-96 max-h-[92vh] overflow-y-auto glass-panel rounded-2xl p-5 z-10 transition-transform duration-300" id="controlPanel">
        <div class="flex justify-between items-center mb-5">
            <h1 class="text-xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
                图案生成器 Pro
            </h1>
            <button onclick="togglePanel()" class="p-2 hover:bg-white/10 rounded-lg transition-colors lg:hidden">
                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
            </button>
        </div>

        <!-- Pattern Type Selection -->
        <div class="mb-5">
            <label class="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">图案类型</label>
            <div class="grid grid-cols-3 gap-2">
                <button onclick="setPattern('square')" id="btn-square" class="pattern-btn active px-3 py-2 rounded-lg border border-white/10 bg-white/5 flex items-center justify-center gap-1 text-xs font-medium">
                    <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
                    正方形
                </button>
                <button onclick="setPattern('rectangle')" id="btn-rectangle" class="pattern-btn px-3 py-2 rounded-lg border border-white/10 bg-white/5 flex items-center justify-center gap-1 text-xs font-medium">
                    <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="10" rx="2"/></svg>
                    长方形
                </button>
                <button onclick="setPattern('diamond')" id="btn-diamond" class="pattern-btn px-3 py-2 rounded-lg border border-white/10 bg-white/5 flex items-center justify-center gap-1 text-xs font-medium">
                    <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L22 12L12 22L2 12Z"/></svg>
                    菱形
                </button>
                <button onclick="setPattern('triangle')" id="btn-triangle" class="pattern-btn px-3 py-2 rounded-lg border border-white/10 bg-white/5 flex items-center justify-center gap-1 text-xs font-medium">
                    <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3L22 20H2L12 3Z"/></svg>
                    三角形
                </button>
                <button onclick="setPattern('hexagon')" id="btn-hexagon" class="pattern-btn px-3 py-2 rounded-lg border border-white/10 bg-white/5 flex items-center justify-center gap-1 text-xs font-medium">
                    <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L20.66 7V17L12 22L3.34 17V7L12 2Z"/></svg>
                    六边形
                </button>
                <button onclick="setPattern('circle')" id="btn-circle" class="pattern-btn px-3 py-2 rounded-lg border border-white/10 bg-white/5 flex items-center justify-center gap-1 text-xs font-medium">
                    <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>
                    圆形
                </button>
            </div>
        </div>

        <!-- Colors with Opacity -->
        <div class="mb-5 control-group p-3 rounded-xl bg-white/5">
            <div class="flex justify-between items-center mb-3">
                <label class="text-xs font-semibold text-gray-400 uppercase tracking-wider">颜色与透明度</label>
                <button onclick="randomizeColors()" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">
                    随机颜色
                </button>
            </div>
            
            <div class="space-y-3">
                <!-- Color 1 -->
                <div class="flex items-center justify-between">
                    <span class="text-sm text-gray-300">主色调</span>
                    <div class="flex items-center gap-2">
                        <input type="text" id="color1Text" value="#3b82f6" class="bg-white/5 border border-white/10 rounded px-2 py-1 text-xs w-16 text-center font-mono" onchange="updateColorFromText(1)">
                        <input type="color" id="color1" value="#3b82f6" onchange="updateColor(1)">
                        <div class="flex flex-col gap-1">
                            <div class="flex items-center gap-1">
                                <span class="text-[10px] text-gray-500">透明</span>
                                <input type="range" id="opacity1" min="0" max="100" value="100" class="w-16 h-1" oninput="updateOpacity(1)">
                                <span class="text-[10px] text-gray-500 w-6 text-right" id="opacity1Value">100%</span>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Color 2 -->
                <div class="flex items-center justify-between">
                    <span class="text-sm text-gray-300">辅助色</span>
                    <div class="flex items-center gap-2">
                        <input type="text" id="color2Text" value="#1e40af" class="bg-white/5 border border-white/10 rounded px-2 py-1 text-xs w-16 text-center font-mono" onchange="updateColorFromText(2)">
                        <input type="color" id="color2" value="#1e40af" onchange="updateColor(2)">
                        <div class="flex flex-col gap-1">
                            <div class="flex items-center gap-1">
                                <span class="text-[10px] text-gray-500">透明</span>
                                <input type="range" id="opacity2" min="0" max="100" value="100" class="w-16 h-1" oninput="updateOpacity(2)">
                                <span class="text-[10px] text-gray-500 w-6 text-right" id="opacity2Value">100%</span>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Background Color -->
                <div class="flex items-center justify-between">
                    <span class="text-sm text-gray-300">背景色</span>
                    <div class="flex items-center gap-2">
                        <input type="text" id="bgColorText" value="#0f172a" class="bg-white/5 border border-white/10 rounded px-2 py-1 text-xs w-16 text-center font-mono" onchange="updateColorFromText('bg')">
                        <input type="color" id="bgColor" value="#0f172a" onchange="updateColor('bg')">
                        <div class="flex flex-col gap-1">
                            <div class="flex items-center gap-1">
                                <span class="text-[10px] text-gray-500">透明</span>
                                <input type="range" id="bgOpacity" min="0" max="100" value="100" class="w-16 h-1" oninput="updateOpacity('bg')">
                                <span class="text-[10px] text-gray-500 w-6 text-right" id="bgOpacityValue">100%</span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="flex items-center gap-3 mt-3 pt-3 border-t border-white/10">
                <label class="flex items-center gap-2 cursor-pointer">
                    <input type="checkbox" id="gradientMode" onchange="drawPattern()" class="w-4 h-4 rounded border-white/20 bg-white/5 text-blue-500 focus:ring-blue-500">
                    <span class="text-xs text-gray-300">渐变模式</span>
                </label>
                <label class="flex items-center gap-2 cursor-pointer">
                    <input type="checkbox" id="randomColors" onchange="toggleRandomColors()" class="w-4 h-4 rounded border-white/20 bg-white/5 text-blue-500 focus:ring-blue-500">
                    <span class="text-xs text-gray-300">随机变化</span>
                </label>
            </div>
        </div>

        <!-- Size Controls -->
        <div class="mb-5 control-group p-3 rounded-xl bg-white/5">
            <label class="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">尺寸与布局</label>
            
            <div class="mb-3">
                <div class="flex justify-between mb-1">
                    <span class="text-xs text-gray-400">图案大小</span>
                    <span id="sizeValue" class="text-xs text-blue-400 font-mono">60px</span>
                </div>
                <input type="range" id="patternSize" min="20" max="200" value="60" class="w-full" oninput="updateValue('size', this.value); drawPattern()">
            </div>

            <div class="mb-3">
                <div class="flex justify-between mb-1">
                    <span class="text-xs text-gray-400">间距</span>
                    <span id="gapValue" class="text-xs text-blue-400 font-mono">4px</span>
                </div>
                <input type="range" id="gapSize" min="0" max="50" value="4" class="w-full" oninput="updateValue('gap', this.value); drawPattern()">
            </div>

            <!-- Stagger Offset -->
            <div class="mb-3" id="staggerControl">
                <div class="flex justify-between mb-1">
                    <span class="text-xs text-gray-400">交错偏移 (砖墙效果)</span>
                    <span id="staggerValue" class="text-xs text-blue-400 font-mono">0%</span>
                </div>
                <input type="range" id="staggerOffset" min="0" max="100" value="0" class="w-full" oninput="updateValue('stagger', this.value); drawPattern()">
                <div class="flex justify-between mt-1 text-[10px] text-gray-500">
                    <span>对齐</span>
                    <span>砖墙(50%)</span>
                    <span>蜂窝(33%)</span>
                </div>
            </div>

            <div class="mb-3" id="aspectControl" style="display: none;">
                <div class="flex justify-between mb-1">
                    <span class="text-xs text-gray-400">长宽比</span>
                    <span id="aspectValue" class="text-xs text-blue-400 font-mono">1.0</span>
                </div>
                <input type="range" id="aspectRatio" min="0.3" max="3" step="0.1" value="1" class="w-full" oninput="updateValue('aspect', this.value); drawPattern()">
            </div>

            <div>
                <div class="flex justify-between mb-1">
                    <span class="text-xs text-gray-400">圆角/平滑度</span>
                    <span id="roundValue" class="text-xs text-blue-400 font-mono">0%</span>
                </div>
                <input type="range" id="roundness" min="0" max="50" value="0" class="w-full" oninput="updateValue('round', this.value); drawPattern()">
            </div>
        </div>

        <!-- Animation with Freeze Frame -->
        <div class="mb-5 control-group p-3 rounded-xl bg-white/5">
            <div class="flex justify-between items-center mb-3">
                <label class="text-xs font-semibold text-gray-400 uppercase tracking-wider">动画控制</label>
                <div class="flex items-center gap-2">
                    <button onclick="togglePlayPause()" id="playPauseBtn" class="play-pause-btn bg-blue-600 hover:bg-blue-500 text-white" disabled>
                        <svg id="playIcon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
                        <svg id="pauseIcon" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
                    </button>
                </div>
            </div>

            <div class="flex items-center justify-between mb-3">
                <span class="text-xs text-gray-400">启用动画</span>
                <label class="relative inline-flex items-center cursor-pointer">
                    <input type="checkbox" id="animationToggle" class="sr-only peer" onchange="toggleAnimation()">
                    <div class="w-10 h-5 bg-white/10 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
                </label>
            </div>

            <div id="animationControls" class="opacity-50 pointer-events-none transition-opacity space-y-3">
                <!-- Freeze Frame Slider -->
                <div>
                    <div class="flex justify-between mb-1">
                        <span class="text-xs text-gray-400">动画定格 (时间轴)</span>
                        <span id="freezeValue" class="text-xs text-blue-400 font-mono">0°</span>
                    </div>
                    <input type="range" id="freezeFrame" min="0" max="360" value="0" class="w-full" oninput="updateFreezeFrame(this.value)">
                    <div class="flex justify-between mt-1 text-[10px] text-gray-500">
                        <span>0°</span>
                        <span>90°</span>
                        <span>180°</span>
                        <span>270°</span>
                        <span>360°</span>
                    </div>
                </div>

                <div>
                    <div class="flex justify-between mb-1">
                        <span class="text-xs text-gray-400">播放速度</span>
                        <span id="speedValue" class="text-xs text-blue-400 font-mono">1x</span>
                    </div>
                    <input type="range" id="animSpeed" min="0.1" max="5" step="0.1" value="1" class="w-full" oninput="updateValue('speed', this.value)">
                </div>

                <div class="grid grid-cols-3 gap-2">
                    <button onclick="setAnimMode('float')" id="anim-float" class="anim-mode-btn active py-2 px-2 rounded-lg bg-white/5 border border-white/10 text-xs transition-colors">漂浮</button>
                    <button onclick="setAnimMode('pulse')" id="anim-pulse" class="anim-mode-btn py-2 px-2 rounded-lg bg-white/5 border border-white/10 text-xs transition-colors">脉冲</button>
                    <button onclick="setAnimMode('wave')" id="anim-wave" class="anim-mode-btn py-2 px-2 rounded-lg bg-white/5 border border-white/10 text-xs transition-colors">波浪</button>
                </div>
            </div>
        </div>

        <!-- Actions -->
        <div class="space-y-2">
            <div class="flex gap-2">
                <button onclick="randomize()" class="flex-1 py-2.5 px-4 rounded-xl bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white text-sm font-semibold transition-all transform hover:scale-105 active:scale-95 flex items-center justify-center gap-2">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
                    随机生成
                </button>
                <button onclick="resetAll()" class="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-gray-300 text-sm transition-all" title="重置">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
                </button>
            </div>
            
            <div class="flex gap-2">
                <button onclick="downloadImage()" class="flex-1 py-2.5 px-4 rounded-xl bg-white/10 hover:bg-white/20 border border-white/10 text-white text-sm font-semibold transition-all flex items-center justify-center gap-2">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
                    导出 PNG
                </button>
                <button onclick="copyCSS()" class="flex-1 py-2.5 px-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-gray-300 text-sm transition-all flex items-center justify-center gap-2">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
                    复制 CSS
                </button>
            </div>
        </div>
    </div>

    <!-- Toggle Button for Mobile -->
    <button onclick="togglePanel()" id="toggleBtn" class="fixed top-4 right-4 z-20 p-3 rounded-xl glass-panel lg:hidden">
        <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/></svg>
    </button>

    <script>
        const canvas = document.getElementById('patternCanvas');
        const ctx = canvas.getContext('2d');
        
        let config = {
            pattern: 'square',
            color1: '#3b82f6',
            color2: '#1e40af',
            bgColor: '#0f172a',
            opacity1: 1,
            opacity2: 1,
            bgOpacity: 1,
            size: 60,
            gap: 4,
            staggerOffset: 0,
            aspectRatio: 1,
            roundness: 0,
            gradientMode: false,
            randomColors: false,
            animation: false,
            animSpeed: 1,
            animMode: 'float',
            isPlaying: false,
            freezeFrame: 0,
            time: 0
        };

        let animationId = null;

        function resize() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
            drawPattern();
        }

        window.addEventListener('resize', resize);
        resize();

        function setPattern(type) {
            config.pattern = type;
            document.querySelectorAll('.pattern-btn').forEach(btn => btn.classList.remove('active'));
            document.getElementById(`btn-${type}`).classList.add('active');
            
            const aspectControl = document.getElementById('aspectControl');
            if (type === 'rectangle') {
                aspectControl.style.display = 'block';
            } else {
                aspectControl.style.display = 'none';
            }
            
            drawPattern();
        }

        function updateColor(index) {
            const color = document.getElementById(`color${index}`).value;
            document.getElementById(`color${index}Text`).value = color;
            config[`color${index}`] = color;
            drawPattern();
        }

        function updateColorFromText(index) {
            let color;
            if (index === 'bg') {
                color = document.getElementById('bgColorText').value;
                document.getElementById('bgColor').value = color;
                config.bgColor = color;
            } else {
                color = document.getElementById(`color${index}Text`).value;
                document.getElementById(`color${index}`).value = color;
                config[`color${index}`] = color;
            }
            drawPattern();
        }

        function updateColor(type) {
            if (type === 'bg') {
                config.bgColor = document.getElementById('bgColor').value;
                document.getElementById('bgColorText').value = config.bgColor;
            }
            drawPattern();
        }

        function updateOpacity(index) {
            if (index === 'bg') {
                config.bgOpacity = document.getElementById('bgOpacity').value / 100;
                document.getElementById('bgOpacityValue').textContent = document.getElementById('bgOpacity').value + '%';
            } else {
                config[`opacity${index}`] = document.getElementById(`opacity${index}`).value / 100;
                document.getElementById(`opacity${index}Value`).textContent = document.getElementById(`opacity${index}`).value + '%';
            }
            drawPattern();
        }

        function updateValue(type, value) {
            const displayValue = 
                type === 'size' ? `${value}px` : 
                type === 'gap' ? `${value}px` : 
                type === 'stagger' ? `${value}%` :
                type === 'aspect' ? parseFloat(value).toFixed(1) :
                type === 'round' ? `${value}%` :
                `${value}x`;
            
            document.getElementById(`${type}Value`).textContent = displayValue;
            
            if (type === 'size') config.size = parseInt(value);
            if (type === 'gap') config.gap = parseInt(value);
            if (type === 'stagger') config.staggerOffset = parseInt(value);
            if (type === 'aspect') config.aspectRatio = parseFloat(value);
            if (type === 'round') config.roundness = parseInt(value);
            if (type === 'speed') config.animSpeed = parseFloat(value);
        }

        function updateFreezeFrame(value) {
            config.freezeFrame = parseInt(value);
            document.getElementById('freezeValue').textContent = `${value}°`;
            
            if (!config.isPlaying) {
                config.time = (value / 360) * Math.PI * 2 * 1000;
                drawPattern();
            }
        }

        function toggleRandomColors() {
            config.randomColors = document.getElementById('randomColors').checked;
            drawPattern();
        }

        function toggleAnimation() {
            config.animation = document.getElementById('animationToggle').checked;
            const controls = document.getElementById('animationControls');
            const playPauseBtn = document.getElementById('playPauseBtn');
            
            if (config.animation) {
                controls.classList.remove('opacity-50', 'pointer-events-none');
                playPauseBtn.disabled = false;
                config.isPlaying = true;
                updatePlayPauseIcon();
                animate();
            } else {
                controls.classList.add('opacity-50', 'pointer-events-none');
                playPauseBtn.disabled = true;
                config.isPlaying = false;
                if (animationId) cancelAnimationFrame(animationId);
                config.time = 0;
                drawPattern();
            }
        }

        function togglePlayPause() {
            config.isPlaying = !config.isPlaying;
            updatePlayPauseIcon();
            
            if (config.isPlaying) {
                animate();
            } else {
                if (animationId) cancelAnimationFrame(animationId);
                const normalizedTime = (config.time % (Math.PI * 2 * 1000)) / (Math.PI * 2 * 1000);
                const degrees = Math.round(normalizedTime * 360);
                document.getElementById('freezeFrame').value = degrees;
                document.getElementById('freezeValue').textContent = `${degrees}°`;
                config.freezeFrame = degrees;
            }
        }

        function updatePlayPauseIcon() {
            const playIcon = document.getElementById('playIcon');
            const pauseIcon = document.getElementById('pauseIcon');
            
            if (config.isPlaying) {
                playIcon.classList.add('hidden');
                pauseIcon.classList.remove('hidden');
            } else {
                playIcon.classList.remove('hidden');
                pauseIcon.classList.add('hidden');
            }
        }

        function setAnimMode(mode) {
            config.animMode = mode;
            document.querySelectorAll('.anim-mode-btn').forEach(btn => {
                btn.classList.remove('active');
            });
            document.getElementById(`anim-${mode}`).classList.add('active');
        }

        function hexToRgba(hex, alpha) {
            const r = parseInt(hex.substr(1, 2), 16);
            const g = parseInt(hex.substr(3, 2), 16);
            const b = parseInt(hex.substr(5, 2), 16);
            return `rgba(${r}, ${g}, ${b}, ${alpha})`;
        }

        function getColorVariation(baseColor, baseOpacity, variation) {
            if (!config.randomColors) return hexToRgba(baseColor, baseOpacity);
            
            const r = parseInt(baseColor.substr(1, 2), 16);
            const g = parseInt(baseColor.substr(3, 2), 16);
            const b = parseInt(baseColor.substr(5, 2), 16);
            
            const newR = Math.max(0, Math.min(255, r + (Math.random() - 0.5) * variation));
            const newG = Math.max(0, Math.min(255, g + (Math.random() - 0.5) * variation));
            const newB = Math.max(0, Math.min(255, b + (Math.random() - 0.5) * variation));
            
            return `rgba(${Math.floor(newR)}, ${Math.floor(newG)}, ${Math.floor(newB)}, ${baseOpacity})`;
        }

        function interpolateColor(color1, color2, factor, opacity1, opacity2) {
            const r1 = parseInt(color1.substr(1, 2), 16);
            const g1 = parseInt(color1.substr(3, 2), 16);
            const b1 = parseInt(color1.substr(5, 2), 16);
            
            const r2 = parseInt(color2.substr(1, 2), 16);
            const g2 = parseInt(color2.substr(3, 2), 16);
            const b2 = parseInt(color2.substr(5, 2), 16);
            
            const r = Math.round(r1 + (r2 - r1) * factor);
            const g = Math.round(g1 + (g2 - g1) * factor);
            const b = Math.round(b1 + (b2 - b1) * factor);
            const a = opacity1 + (opacity2 - opacity1) * factor;
            
            return `rgba(${r}, ${g}, ${b}, ${a})`;
        }

        function drawPattern() {
            config.gradientMode = document.getElementById('gradientMode').checked;
            
            // Clear with transparent background
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // Only fill background if opacity > 0
            if (config.bgOpacity > 0) {
                ctx.fillStyle = hexToRgba(config.bgColor, config.bgOpacity);
                ctx.fillRect(0, 0, canvas.width, canvas.height);
            }
            
            const cols = Math.ceil(canvas.width / (config.size + config.gap)) + 3;
            const rows = Math.ceil(canvas.height / (config.size + config.gap)) + 3;
            
            const offsetX = (canvas.width - cols * (config.size + config.gap)) / 2;
            const offsetY = (canvas.height - rows * (config.size + config.gap)) / 2;
            
            for (let row = -1; row < rows; row++) {
                let rowStagger = 0;
                if (config.staggerOffset > 0 && row % 2 === 1) {
                    rowStagger = (config.size + config.gap) * (config.staggerOffset / 100);
                }
                
                for (let col = -1; col < cols; col++) {
                    let x = offsetX + col * (config.size + config.gap) + rowStagger;
                    let y = offsetY + row * (config.size + config.gap);
                    
                    if (config.animation && config.isPlaying) {
                        const time = config.time * config.animSpeed * 0.001;
                        if (config.animMode === 'float') {
                            x += Math.sin(time + row * 0.5) * 10;
                            y += Math.cos(time + col * 0.5) * 10;
                        } else if (config.animMode === 'wave') {
                            y += Math.sin(time + col * 0.3 + row * 0.3) * 20;
                        }
                    } else if (config.animation && !config.isPlaying) {
                        const time = config.time * 0.001;
                        if (config.animMode === 'float') {
                            x += Math.sin(time + row * 0.5) * 10;
                            y += Math.cos(time + col * 0.5) * 10;
                        } else if (config.animMode === 'wave') {
                            y += Math.sin(time + col * 0.3 + row * 0.3) * 20;
                        }
                    }
                    
                    let color;
                    if (config.gradientMode) {
                        const gradientPos = (col + row) / (cols + rows);
                        color = interpolateColor(config.color1, config.color2, gradientPos, config.opacity1, config.opacity2);
                    } else {
                        const isEven = (col + row) % 2 === 0;
                        color = hexToRgba(
                            isEven ? config.color1 : config.color2,
                            isEven ? config.opacity1 : config.opacity2
                        );
                    }
                    
                    if (config.randomColors) {
                        color = getColorVariation(
                            (col + row) % 2 === 0 ? config.color1 : config.color2,
                            (col + row) % 2 === 0 ? config.opacity1 : config.opacity2,
                            40
                        );
                    }
                    
                    ctx.fillStyle = color;
                    ctx.strokeStyle = hexToRgba(config.bgColor, config.bgOpacity);
                    ctx.lineWidth = config.gap / 2;
                    
                    let scale = 1;
                    if (config.animation) {
                        let time = config.isPlaying ? config.time * config.animSpeed * 0.002 : config.time * 0.002;
                        if (config.animMode === 'pulse') {
                            const dist = Math.sqrt((col - cols/2)**2 + (row - rows/2)**2);
                            scale = 0.8 + 0.2 * Math.sin(time + dist * 0.5);
                        }
                    }
                    
                    const size = config.size * scale;
                    const halfSize = size / 2;
                    
                    ctx.beginPath();
                    
                    switch(config.pattern) {
                        case 'square':
                            drawRoundedRect(ctx, x, y, size, size, size * config.roundness / 100);
                            break;
                            
                        case 'rectangle':
                            const w = size * config.aspectRatio;
                            const h = size;
                            drawRoundedRect(ctx, x - (w-size)/2, y, w, h, Math.min(w,h) * config.roundness / 100);
                            break;
                            
                        case 'diamond':
                            ctx.moveTo(x + halfSize, y);
                            ctx.lineTo(x + size, y + halfSize);
                            ctx.lineTo(x + halfSize, y + size);
                            ctx.lineTo(x, y + halfSize);
                            ctx.closePath();
                            break;
                            
                        case 'triangle':
                            ctx.moveTo(x + halfSize, y);
                            ctx.lineTo(x + size, y + size);
                            ctx.lineTo(x, y + size);
                            ctx.closePath();
                            break;
                            
                        case 'hexagon':
                            const r = halfSize;
                            for (let i = 0; i < 6; i++) {
                                const angle = (Math.PI / 3) * i;
                                const hx = x + halfSize + r * Math.cos(angle);
                                const hy = y + halfSize + r * Math.sin(angle);
                                if (i === 0) ctx.moveTo(hx, hy);
                                else ctx.lineTo(hx, hy);
                            }
                            ctx.closePath();
                            break;
                            
                        case 'circle':
                            const radius = halfSize * (1 - config.roundness / 200);
                            ctx.arc(x + halfSize, y + halfSize, radius, 0, Math.PI * 2);
                            break;
                    }
                    
                    ctx.fill();
                    if (config.gap > 0) ctx.stroke();
                }
            }
        }

        function drawRoundedRect(ctx, x, y, width, height, radius) {
            const r = Math.min(radius, width/2, height/2);
            ctx.moveTo(x + r, y);
            ctx.lineTo(x + width - r, y);
            ctx.quadraticCurveTo(x + width, y, x + width, y + r);
            ctx.lineTo(x + width, y + height - r);
            ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
            ctx.lineTo(x + r, y + height);
            ctx.quadraticCurveTo(x, y + height, x, y + height - r);
            ctx.lineTo(x, y + r);
            ctx.quadraticCurveTo(x, y, x + r, y);
            ctx.closePath();
        }

        function animate() {
            if (!config.animation || !config.isPlaying) return;
            config.time += 16;
            drawPattern();
            animationId = requestAnimationFrame(animate);
        }

        function randomizeColors() {
            const randomColor = () => '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
            document.getElementById('color1').value = randomColor();
            document.getElementById('color2').value = randomColor();
            updateColor(1);
            updateColor(2);
            
            document.getElementById('opacity1').value = 50 + Math.floor(Math.random() * 50);
            document.getElementById('opacity2').value = 50 + Math.floor(Math.random() * 50);
            updateOpacity(1);
            updateOpacity(2);
        }

        function randomize() {
            const patterns = ['square', 'rectangle', 'diamond', 'triangle', 'hexagon', 'circle'];
            const randomPattern = patterns[Math.floor(Math.random() * patterns.length)];
            setPattern(randomPattern);
            
            randomizeColors();
            
            document.getElementById('patternSize').value = 30 + Math.random() * 100;
            document.getElementById('gapSize').value = Math.floor(Math.random() * 20);
            document.getElementById('staggerOffset').value = Math.floor(Math.random() * 50);
            updateValue('size', document.getElementById('patternSize').value);
            updateValue('gap', document.getElementById('gapSize').value);
            updateValue('stagger', document.getElementById('staggerOffset').value);
            
            if (randomPattern === 'rectangle') {
                document.getElementById('aspectRatio').value = 0.5 + Math.random() * 2;
                updateValue('aspect', document.getElementById('aspectRatio').value);
            }
            
            drawPattern();
        }

        function resetAll() {
            config = {
                pattern: 'square',
                color1: '#3b82f6',
                color2: '#1e40af',
                bgColor: '#0f172a',
                opacity1: 1,
                opacity2: 1,
                bgOpacity: 1,
                size: 60,
                gap: 4,
                staggerOffset: 0,
                aspectRatio: 1,
                roundness: 0,
                gradientMode: false,
                randomColors: false,
                animation: false,
                animSpeed: 1,
                animMode: 'float',
                isPlaying: false,
                freezeFrame: 0,
                time: 0
            };
            
            document.getElementById('btn-square').click();
            document.getElementById('color1').value = config.color1;
            document.getElementById('color2').value = config.color2;
            document.getElementById('bgColor').value = config.bgColor;
            document.getElementById('opacity1').value = 100;
            document.getElementById('opacity2').value = 100;
            document.getElementById('bgOpacity').value = 100;
            document.getElementById('patternSize').value = 60;
            document.getElementById('gapSize').value = 4;
            document.getElementById('staggerOffset').value = 0;
            document.getElementById('roundness').value = 0;
            document.getElementById('animationToggle').checked = false;
            document.getElementById('gradientMode').checked = false;
            document.getElementById('randomColors').checked = false;
            
            updateOpacity(1);
            updateOpacity(2);
            updateOpacity('bg');
            updateValue('size', 60);
            updateValue('gap', 4);
            updateValue('stagger', 0);
            updateValue('round', 0);
            
            toggleAnimation();
            drawPattern();
        }

        function downloadImage() {
            // 直接导出当前canvas,保留原始透明度,不添加任何背景
            const link = document.createElement('a');
            link.download = `pattern-${config.pattern}-${Date.now()}.png`;
            link.href = canvas.toDataURL('image/png');
            link.click();
        }

        function copyCSS() {
            let css = '';
            if (config.pattern === 'square' && config.roundness === 0 && config.staggerOffset === 0) {
                css = `
.pattern-background {
    background-color: ${hexToRgba(config.bgColor, config.bgOpacity)};
    background-image: 
        linear-gradient(45deg, ${hexToRgba(config.color1, config.opacity1)} 25%, transparent 25%), 
        linear-gradient(-45deg, ${hexToRgba(config.color1, config.opacity1)} 25%, transparent 25%), 
        linear-gradient(45deg, transparent 75%, ${hexToRgba(config.color2, config.opacity2)} 75%), 
        linear-gradient(-45deg, transparent 75%, ${hexToRgba(config.color2, config.opacity2)} 75%);
    background-size: ${config.size + config.gap}px ${config.size + config.gap}px;
    background-position: 0 0, 0 ${(config.size + config.gap)/2}px, ${(config.size + config.gap)/2}px -${(config.size + config.gap)/2}px, -${(config.size + config.gap)/2}px 0px;
}`;
            } else {
                css = `/* 当前设置包含交错、圆角或复杂图案,建议使用导出的PNG作为背景 */
.background-pattern {
    background-image: url('pattern-export.png');
    background-size: cover;
}`;
            }
            navigator.clipboard.writeText(css).then(() => {
                alert('CSS 代码已复制到剪贴板!');
            });
        }

        function togglePanel() {
            const panel = document.getElementById('controlPanel');
            panel.classList.toggle('translate-x-full');
        }

        // Initialize
        drawPattern();
    </script>
</body>
</html>
相关推荐
浮笙若有梦2 小时前
我开源了一个比 Ant Design Table 更好用的高性能虚拟表格
前端·vue.js
一只程序熊2 小时前
vite-cool-unix-ctx] Unexpected token l in JSON at position 0
java·服务器·前端
张元清2 小时前
React Hooks vs Vue Composables:2026 年全面对比
前端·javascript·面试
yuki_uix2 小时前
从三个自定义 Hook 看 React 状态管理的设计思想
前端·javascript
大漠_w3cpluscom2 小时前
如何在 clamp() 中使用 auto 值
前端·css·html
Younglina2 小时前
🏸 从零打造一个羽毛球球线追踪网站:纯前端实战指南
前端
C澒2 小时前
微前端容器标准化:从碎片化到统一架构的渐进式改造
前端·架构
CyrusCJA2 小时前
JavaScript原型与super关键字
前端·javascript·js
左耳咚2 小时前
Claude Code 技术全景概览
前端·ai编程