duqing2026 commited on
Commit
13466f4
·
0 Parent(s):

Initial commit: SVG Pattern Lab MVP

Browse files
Files changed (5) hide show
  1. Dockerfile +18 -0
  2. README.md +47 -0
  3. app.py +20 -0
  4. requirements.txt +2 -0
  5. templates/index.html +343 -0
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # Create a non-root user for Hugging Face Spaces
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+ ENV HOME=/home/user \
14
+ PATH=/home/user/.local/bin:$PATH
15
+
16
+ EXPOSE 7860
17
+
18
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SVG Pattern Lab
3
+ emoji: 🕸️
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ short_description: Generate customizable SVG background patterns for web design.
9
+ ---
10
+
11
+ # SVG Pattern Lab (SVG 图案实验室)
12
+
13
+ 这是一个专为开发者和设计师打造的 SVG 背景图案生成工具。
14
+
15
+ ## 功能特点
16
+
17
+ - **多样的图案库**:提供网格、圆点、斜线、波浪、棋盘、蜂巢等多种基础几何纹理。
18
+ - **实时自定义**:
19
+ - 调整前景和背景颜色
20
+ - 控制透明度、缩放比例和间距
21
+ - **一键导出**:
22
+ - 复制 CSS `background-image` 代码,直接粘贴到你的项目中。
23
+ - 下载 SVG 文件,用于设计软件或进一步编辑。
24
+ - **极简设计**:无广告,无追踪,纯粹的工具体验。
25
+
26
+ ## 技术栈
27
+
28
+ - **后端**:Flask (Python) - 提供静态文件服务
29
+ - **前端**:Vue 3 + Tailwind CSS - 响应式 UI 和动态 SVG 生成
30
+ - **部署**:Docker - 适配 Hugging Face Spaces
31
+
32
+ ## 本地运行
33
+
34
+ 1. 克隆仓库
35
+ 2. 安装依赖:
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ ```
39
+ 3. 运行:
40
+ ```bash
41
+ python app.py
42
+ ```
43
+ 4. 访问 `http://localhost:7860`
44
+
45
+ ## License
46
+
47
+ MIT
app.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, render_template, send_from_directory, jsonify
3
+
4
+ app = Flask(__name__, static_folder='static', template_folder='templates')
5
+
6
+ @app.route('/')
7
+ def index():
8
+ return render_template('index.html')
9
+
10
+ @app.route('/healthz')
11
+ def healthz():
12
+ return jsonify({"status": "ok"}), 200
13
+
14
+ @app.route('/static/<path:path>')
15
+ def send_static(path):
16
+ return send_from_directory('static', path)
17
+
18
+ if __name__ == '__main__':
19
+ port = int(os.environ.get('PORT', 7860))
20
+ app.run(host='0.0.0.0', port=port, debug=True)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask
2
+ gunicorn
templates/index.html ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SVG 图案实验室 | SVG Pattern Lab</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <!-- Phosphor Icons -->
10
+ <script src="https://unpkg.com/@phosphor-icons/web"></script>
11
+ <style>
12
+ [v-cloak] { display: none; }
13
+ .preview-area {
14
+ transition: background-image 0.2s ease;
15
+ }
16
+ input[type="range"] {
17
+ @apply w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer;
18
+ }
19
+ input[type="range"]::-webkit-slider-thumb {
20
+ @apply w-4 h-4 bg-indigo-600 rounded-full appearance-none;
21
+ }
22
+ .scrollbar-hide::-webkit-scrollbar {
23
+ display: none;
24
+ }
25
+ </style>
26
+ </head>
27
+ <body class="bg-gray-50 text-gray-800 h-screen overflow-hidden">
28
+ <div id="app" v-cloak class="h-full flex flex-col md:flex-row">
29
+ <!-- Sidebar Controls -->
30
+ <div class="w-full md:w-80 bg-white border-r border-gray-200 flex flex-col h-full z-10 shadow-lg">
31
+ <div class="p-5 border-b border-gray-100">
32
+ <div class="flex items-center gap-2 mb-1">
33
+ <i class="ph ph-hexagon text-2xl text-indigo-600"></i>
34
+ <h1 class="font-bold text-xl tracking-tight text-gray-900">SVG Pattern Lab</h1>
35
+ </div>
36
+ <p class="text-xs text-gray-500">为开发者打造的背景生成器</p>
37
+ </div>
38
+
39
+ <div class="flex-1 overflow-y-auto p-5 space-y-6">
40
+ <!-- Pattern Selection -->
41
+ <div>
42
+ <label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">选择图案</label>
43
+ <div class="grid grid-cols-4 gap-2">
44
+ <button
45
+ v-for="p in patterns"
46
+ :key="p.id"
47
+ @click="currentPatternId = p.id"
48
+ class="aspect-square rounded-lg border-2 flex items-center justify-center transition-all hover:bg-indigo-50"
49
+ :class="currentPatternId === p.id ? 'border-indigo-600 bg-indigo-50 text-indigo-600' : 'border-gray-200 text-gray-400'"
50
+ :title="p.name"
51
+ >
52
+ <i :class="p.icon" class="text-xl"></i>
53
+ </button>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- Colors -->
58
+ <div>
59
+ <label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">配色</label>
60
+ <div class="space-y-3">
61
+ <div class="flex items-center justify-between">
62
+ <span class="text-sm">前景颜色</span>
63
+ <div class="flex items-center gap-2">
64
+ <span class="text-xs font-mono text-gray-500">{{ config.color }}</span>
65
+ <input type="color" v-model="config.color" class="w-8 h-8 rounded cursor-pointer border-0 p-0">
66
+ </div>
67
+ </div>
68
+ <div class="flex items-center justify-between">
69
+ <span class="text-sm">背景颜色</span>
70
+ <div class="flex items-center gap-2">
71
+ <span class="text-xs font-mono text-gray-500">{{ config.background }}</span>
72
+ <input type="color" v-model="config.background" class="w-8 h-8 rounded cursor-pointer border-0 p-0">
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Adjustments -->
79
+ <div>
80
+ <label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">参数调整</label>
81
+ <div class="space-y-4">
82
+ <div>
83
+ <div class="flex justify-between mb-1">
84
+ <span class="text-sm">透明度 (Opacity)</span>
85
+ <span class="text-xs text-gray-500">{{ config.opacity }}</span>
86
+ </div>
87
+ <input type="range" v-model.number="config.opacity" min="0.1" max="1" step="0.05">
88
+ </div>
89
+ <div>
90
+ <div class="flex justify-between mb-1">
91
+ <span class="text-sm">缩放 (Scale)</span>
92
+ <span class="text-xs text-gray-500">{{ config.scale }}</span>
93
+ </div>
94
+ <input type="range" v-model.number="config.scale" min="0.5" max="3" step="0.1">
95
+ </div>
96
+ <div v-if="currentPattern.hasSpacing">
97
+ <div class="flex justify-between mb-1">
98
+ <span class="text-sm">间距 (Spacing)</span>
99
+ <span class="text-xs text-gray-500">{{ config.spacing }}</span>
100
+ </div>
101
+ <input type="range" v-model.number="config.spacing" min="10" max="100" step="2">
102
+ </div>
103
+ <div>
104
+ <div class="flex justify-between mb-1">
105
+ <span class="text-sm">旋转 (Rotation)</span>
106
+ <span class="text-xs text-gray-500">{{ config.rotation }}°</span>
107
+ </div>
108
+ <input type="range" v-model.number="config.rotation" min="0" max="180" step="5">
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Actions -->
115
+ <div class="p-5 border-t border-gray-200 bg-gray-50 space-y-3">
116
+ <button @click="copyCSS" class="w-full py-2.5 px-4 bg-white border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-indigo-600 transition-colors flex items-center justify-center gap-2">
117
+ <i class="ph ph-code"></i>
118
+ {{ copyStatus || '复制 CSS 代码' }}
119
+ </button>
120
+ <button @click="downloadSVG" class="w-full py-2.5 px-4 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700 transition-colors flex items-center justify-center gap-2 shadow-sm">
121
+ <i class="ph ph-download-simple"></i>
122
+ 下载 SVG
123
+ </button>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- Preview Area -->
128
+ <div class="flex-1 relative overflow-hidden flex flex-col">
129
+ <!-- Toolbar -->
130
+ <div class="absolute top-4 right-4 z-20 flex gap-2">
131
+ <button @click="randomize" class="p-2 bg-white/90 backdrop-blur rounded-full shadow-sm border border-gray-200 text-gray-600 hover:text-indigo-600 transition-colors" title="随机生成">
132
+ <i class="ph ph-shuffle text-xl"></i>
133
+ </button>
134
+ <button @click="reset" class="p-2 bg-white/90 backdrop-blur rounded-full shadow-sm border border-gray-200 text-gray-600 hover:text-red-600 transition-colors" title="重置">
135
+ <i class="ph ph-arrow-counter-clockwise text-xl"></i>
136
+ </button>
137
+ </div>
138
+
139
+ <!-- Canvas -->
140
+ <div
141
+ class="w-full h-full preview-area relative"
142
+ :style="previewStyle"
143
+ >
144
+ <!-- Overlay content example -->
145
+ <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
146
+ <div class="bg-white/90 backdrop-blur-sm p-8 rounded-2xl shadow-xl max-w-md text-center border border-white/50">
147
+ <h2 class="text-3xl font-bold text-gray-900 mb-2">预览效果</h2>
148
+ <p class="text-gray-600">这是一个示例卡片,用于展示背景图案在实际 UI 组件下的效果。</p>
149
+ <div class="mt-6 flex justify-center gap-3">
150
+ <div class="h-2 w-20 bg-gray-200 rounded-full"></div>
151
+ <div class="h-2 w-12 bg-gray-200 rounded-full"></div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- CSS Code Preview (Bottom Sheet) -->
158
+ <div class="bg-gray-900 text-gray-300 text-xs p-3 font-mono overflow-x-auto whitespace-nowrap border-t border-gray-800">
159
+ {{ cssCode }}
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <script>
165
+ const { createApp, ref, computed, reactive, watch } = Vue;
166
+
167
+ createApp({
168
+ setup() {
169
+ const config = reactive({
170
+ color: '#4f46e5',
171
+ background: '#ffffff',
172
+ opacity: 0.5,
173
+ scale: 1,
174
+ spacing: 20,
175
+ rotation: 0
176
+ });
177
+
178
+ const currentPatternId = ref('grid');
179
+ const copyStatus = ref('');
180
+
181
+ const patterns = [
182
+ { id: 'grid', name: '网格', icon: 'ph-grid-four', hasSpacing: true },
183
+ { id: 'dots', name: '圆点', icon: 'ph-dots-nine', hasSpacing: true },
184
+ { id: 'lines', name: '斜线', icon: 'ph-line-segments', hasSpacing: true },
185
+ { id: 'cross', name: '十字', icon: 'ph-plus', hasSpacing: true },
186
+ { id: 'waves', name: '波浪', icon: 'ph-waves', hasSpacing: false },
187
+ { id: 'checkers', name: '棋盘', icon: 'ph-squares-four', hasSpacing: true },
188
+ { id: 'hex', name: '蜂巢', icon: 'ph-hexagon', hasSpacing: false },
189
+ { id: 'zigzag', name: '折线', icon: 'ph-chart-line-up', hasSpacing: true },
190
+ ];
191
+
192
+ const currentPattern = computed(() => patterns.find(p => p.id === currentPatternId.value));
193
+
194
+ // SVG Generators
195
+ const generateSVG = () => {
196
+ const { color, background, opacity, scale, spacing, rotation } = config;
197
+ const s = spacing * scale;
198
+ const r = rotation;
199
+
200
+ // Base Attributes
201
+ let width = s;
202
+ let height = s;
203
+ let content = '';
204
+
205
+ switch (currentPatternId.value) {
206
+ case 'grid':
207
+ content = `<path d="M ${s} 0 L 0 0 0 ${s}" fill="none" stroke="${color}" stroke-width="${1 * scale}" stroke-opacity="${opacity}"/>`;
208
+ break;
209
+ case 'dots':
210
+ content = `<circle cx="${s/2}" cy="${s/2}" r="${2 * scale}" fill="${color}" fill-opacity="${opacity}"/>`;
211
+ break;
212
+ case 'lines':
213
+ content = `<path d="M0 ${s} L${s} 0" stroke="${color}" stroke-width="${2 * scale}" stroke-opacity="${opacity}"/>`;
214
+ break;
215
+ case 'cross':
216
+ const cs = 4 * scale; // cross size
217
+ content = `<path d="M${s/2 - cs} ${s/2} h${cs*2} M${s/2} ${s/2 - cs} v${cs*2}" stroke="${color}" stroke-width="${1.5 * scale}" stroke-opacity="${opacity}" />`;
218
+ break;
219
+ case 'checkers':
220
+ width = s * 2;
221
+ height = s * 2;
222
+ content = `<rect x="0" y="0" width="${s}" height="${s}" fill="${color}" fill-opacity="${opacity}"/><rect x="${s}" y="${s}" width="${s}" height="${s}" fill="${color}" fill-opacity="${opacity}"/>`;
223
+ break;
224
+ case 'waves':
225
+ width = 100 * scale;
226
+ height = 20 * scale;
227
+ content = `<path d="M0 10 Q25 20 50 10 T100 10 V20 H0 Z" fill="${color}" fill-opacity="${opacity}"/>`;
228
+ break;
229
+ case 'hex':
230
+ width = 28 * scale;
231
+ height = 49 * scale;
232
+ // Hexagon path
233
+ content = `<path d="M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z" fill="${color}" fill-opacity="${opacity}"/>`;
234
+ break;
235
+ case 'zigzag':
236
+ width = s;
237
+ height = s;
238
+ content = `<path d="M0 ${s/2} L${s/2} 0 L${s} ${s/2}" fill="none" stroke="${color}" stroke-width="${2 * scale}" stroke-opacity="${opacity}"/>`;
239
+ break;
240
+ }
241
+
242
+ // Apply rotation if needed (complex for patterns, simple implementation via SVG transform if wrapped)
243
+ // For now, rotation is applied to the CSS background element, not internal SVG geometry to keep it simple,
244
+ // BUT CSS background-rotation is tricky.
245
+ // Better approach: wrap content in a group with transform if rotation is supported internally.
246
+ // However, patternUserSpaceOnUse handles tiling. Rotating the tile content might break seamlessness for some patterns.
247
+ // Let's stick to simple implementation: Rotation will rotate the internal element center.
248
+
249
+ let svgString = `<svg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}' viewBox='0 0 ${width} ${height}'>${content}</svg>`;
250
+
251
+ // Encode for Data URI
252
+ return svgString;
253
+ };
254
+
255
+ const svgDataUrl = computed(() => {
256
+ const svg = generateSVG();
257
+ const encoded = encodeURIComponent(svg)
258
+ .replace(/'/g, '%27')
259
+ .replace(/"/g, '%22');
260
+ return `data:image/svg+xml,${encoded}`;
261
+ });
262
+
263
+ const cssCode = computed(() => {
264
+ return `background-color: ${config.background}; background-image: url("${svgDataUrl.value}");`;
265
+ });
266
+
267
+ const previewStyle = computed(() => {
268
+ return {
269
+ backgroundColor: config.background,
270
+ backgroundImage: `url("${svgDataUrl.value}")`,
271
+ // Rotation handled via CSS transform on a pseudo-element usually, but here applied to container?
272
+ // Actually, let's keep rotation simple: apply to the preview div? No, that rotates content too.
273
+ // Let's ignore rotation for CSS output for now as it's complex for background-image (requires element rotation).
274
+ // Or we can rotate the SVG content itself.
275
+ };
276
+ });
277
+
278
+ const copyCSS = async () => {
279
+ try {
280
+ await navigator.clipboard.writeText(cssCode.value);
281
+ copyStatus.value = '已复制!';
282
+ setTimeout(() => copyStatus.value = '', 2000);
283
+ } catch (err) {
284
+ console.error('Copy failed', err);
285
+ copyStatus.value = '复制失败';
286
+ }
287
+ };
288
+
289
+ const downloadSVG = () => {
290
+ const svg = generateSVG();
291
+ const blob = new Blob([svg], { type: 'image/svg+xml' });
292
+ const url = URL.createObjectURL(blob);
293
+ const a = document.createElement('a');
294
+ a.href = url;
295
+ a.download = `pattern-${currentPatternId.value}.svg`;
296
+ document.body.appendChild(a);
297
+ a.click();
298
+ document.body.removeChild(a);
299
+ URL.revokeObjectURL(url);
300
+ };
301
+
302
+ const randomize = () => {
303
+ const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
304
+ const randomBg = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
305
+
306
+ config.color = randomColor;
307
+ config.background = '#ffffff'; // Keep bg simple usually
308
+ config.opacity = Number((Math.random() * 0.8 + 0.1).toFixed(2));
309
+ config.scale = Number((Math.random() * 1.5 + 0.5).toFixed(1));
310
+ config.spacing = Math.floor(Math.random() * 40 + 20);
311
+
312
+ const pIds = patterns.map(p => p.id);
313
+ currentPatternId.value = pIds[Math.floor(Math.random() * pIds.length)];
314
+ };
315
+
316
+ const reset = () => {
317
+ config.color = '#4f46e5';
318
+ config.background = '#ffffff';
319
+ config.opacity = 0.5;
320
+ config.scale = 1;
321
+ config.spacing = 20;
322
+ config.rotation = 0;
323
+ currentPatternId.value = 'grid';
324
+ };
325
+
326
+ return {
327
+ config,
328
+ currentPatternId,
329
+ patterns,
330
+ currentPattern,
331
+ cssCode,
332
+ previewStyle,
333
+ copyCSS,
334
+ downloadSVG,
335
+ copyStatus,
336
+ randomize,
337
+ reset
338
+ };
339
+ }
340
+ }).mount('#app');
341
+ </script>
342
+ </body>
343
+ </html>