🎨 程序化几何背景生成器
一个基于 HTML5 Canvas 的交互式几何图案背景生成器,可实时调整样式、颜色、动画效果,并能一键导出 PNG 或 CSS 代码。适用于网页设计、UI/UX 设计、创意项目或作为可视化学习工具。
✨ 核心特性
特性 说明 🔄 6 种几何图形 正方形、长方形、菱形、三角形、六边形、圆形 🎨 高级颜色控制 双主色 + 背景色,独立透明度,渐变/随机颜色模式 📏 精细尺寸调节 图案大小、间距、圆角、交错偏移(砖墙/蜂窝效果)、长宽比 🌀 实时动画 漂浮、脉冲、波浪三种动画模式,可控制速度、暂停/定格 💾 一键导出 导出 PNG 图片(保留透明度)或复制纯 CSS 代码 📱 响应式设计 移动端友好,可折叠控制面板
🚀 快速开始
在线使用
直接点击 [这里] 打开 HTML 文件,或部署到任意静态托管服务。
本地运行
将项目代码保存为
index.html在浏览器中打开该文件
无需安装任何依赖,开箱即用
🎮 使用说明
控制面板详解
控制组 功能 图案类型 点击图标切换基本几何形状 颜色与透明度 分别设置主色、辅助色、背景色的 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;添加新动画模式
在
config.animMode中添加新模式标识在
drawPattern()的动画计算部分实现位置/形变逻辑在 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>

