TraeBot commited on
Commit
1044e8a
·
0 Parent(s):

init: generative-art-lab

Browse files
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-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
+ EXPOSE 7860
11
+
12
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 生成艺术实验室(Generative Art Lab)
2
+
3
+ 一个用于探索「生成艺术(Generative Art)」的轻量级实验项目。特性:
4
+ - 纯前端渲染(Canvas),隐私友好
5
+ - 可复现种子(Seed),每次结果可重现
6
+ - 多种算法:流场线条、三角碎片、近似圆填充、渐变条纹、星空点阵
7
+ - 一键导出 PNG 高清图片
8
+ - 基于 Flask 提供静态服务,适配 Hugging Face Spaces(端口 7860)
9
+
10
+ ## 本地运行
11
+
12
+ ```bash
13
+ pip install -r requirements.txt
14
+ python app.py
15
+ # 打开 http://localhost:7860
16
+ ```
17
+
18
+ 或使用 Docker:
19
+
20
+ ```bash
21
+ docker build -t generative-art-lab .
22
+ docker run -it --rm -p 7860:7860 generative-art-lab
23
+ ```
24
+
25
+ ## Hugging Face Spaces 部署
26
+
27
+ 1. 新建 Space(类型:Docker,SDK:Static/Other)
28
+ 2. 推送本项目到仓库(例如:spaces/duqing2026/generative-art-lab)
29
+ 3. 确保包含以下文件:
30
+ - `Dockerfile`(使用 gunicorn 启动)
31
+ - `requirements.txt`(Flask 与 gunicorn)
32
+ - `app.py`、`templates/`、`static/`
33
+
34
+ Docker 容器会在 7860 端口启动应用(Spaces 会自动映射)。
35
+
36
+ ## 使用说明
37
+
38
+ - 左侧面板可调参数:算法、配色方案、画布尺寸、随机种子
39
+ - 种子支持数字或任意字符串(字符串会被转换为稳定种子)
40
+ - 点击「导出 PNG」保存当前画布为无损图片
41
+
42
+ ## 目录结构
43
+
44
+ ```
45
+ generative-art-lab/
46
+ ├── app.py
47
+ ├── Dockerfile
48
+ ├── requirements.txt
49
+ ├── templates/
50
+ │ └── index.html
51
+ └── static/
52
+ └── js/
53
+ └── app.js
54
+ ```
55
+
56
+ ## 许可证
57
+
58
+ MIT
59
+
__pycache__/app.cpython-314.pyc ADDED
Binary file (916 Bytes). View file
 
app.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template
2
+
3
+ app = Flask(__name__)
4
+
5
+ @app.route("/")
6
+ def index():
7
+ return render_template("index.html")
8
+
9
+ @app.route("/healthz")
10
+ def healthz():
11
+ return "ok"
12
+
13
+ if __name__ == "__main__":
14
+ import os
15
+ port = int(os.environ.get("PORT", 7860))
16
+ app.run(host="0.0.0.0", port=port, debug=True)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ Flask==3.0.0
2
+ gunicorn==21.2.0
static/js/app.js ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ;(() => {
2
+ const { createApp, reactive, computed, onMounted } = Vue
3
+
4
+ // 简易可复现随机数(Mulberry32)
5
+ function mulberry32(a) {
6
+ return function () {
7
+ let t = (a += 0x6D2B79F5)
8
+ t = Math.imul(t ^ (t >>> 15), t | 1)
9
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
10
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
11
+ }
12
+ }
13
+ // 字符串 -> 种子
14
+ function strToSeed(str) {
15
+ let h = 2166136261 >>> 0
16
+ for (let i = 0; i < str.length; i++) {
17
+ h ^= str.charCodeAt(i)
18
+ h = Math.imul(h, 16777619)
19
+ }
20
+ return h >>> 0
21
+ }
22
+
23
+ // 颜色工具
24
+ function hsl(h, s, l) {
25
+ return `hsl(${h}, ${s}%, ${l}%)`
26
+ }
27
+
28
+ // 预设配色
29
+ const palettes = {
30
+ Default: ['#0f172a', '#475569', '#94a3b8', '#e2e8f0', '#f59e0b'],
31
+ Minimalist: ['#111827', '#6b7280', '#d1d5db', '#f3f4f6', '#10b981'],
32
+ Dark: ['#0b1020', '#1f2937', '#334155', '#94a3b8', '#f8fafc'],
33
+ Warm: ['#7f1d1d', '#b45309', '#f59e0b', '#fde68a', '#fef3c7'],
34
+ Ocean: ['#0e7490', '#155e75', '#134e4a', '#10b981', '#67e8f9'],
35
+ }
36
+
37
+ // 算法集合
38
+ const algos = [
39
+ {
40
+ value: 'flow',
41
+ label: '流场线条',
42
+ desc: '基于角度场的粒子流动,形成丝绸般纹理',
43
+ draw: drawFlowField,
44
+ },
45
+ {
46
+ value: 'tri',
47
+ label: '三角碎片',
48
+ desc: '随机三角网格叠加,构成抽象拼贴',
49
+ draw: drawTriangles,
50
+ },
51
+ {
52
+ value: 'circles',
53
+ label: '近似圆填充',
54
+ desc: '随机投点生成不重叠圆,层次丰富',
55
+ draw: drawCircles,
56
+ },
57
+ {
58
+ value: 'stripes',
59
+ label: '渐变条纹',
60
+ desc: '多层渐变条纹旋转叠加,颜色过渡顺滑',
61
+ draw: drawStripes,
62
+ },
63
+ {
64
+ value: 'stars',
65
+ label: '星空点阵',
66
+ desc: '夜空背景 + 高斯分布星点,轻微辉光',
67
+ draw: drawStars,
68
+ },
69
+ ]
70
+
71
+ // 绘制函数们
72
+ function drawFlowField(ctx, W, H, rng, palette) {
73
+ ctx.fillStyle = palette[0]
74
+ ctx.fillRect(0, 0, W, H)
75
+ const steps = 5000
76
+ ctx.globalAlpha = 0.08
77
+ ctx.lineWidth = 1
78
+ for (let i = 0; i < steps; i++) {
79
+ let x = rng() * W
80
+ let y = rng() * H
81
+ const len = 80 + rng() * 150
82
+ const hue = Math.floor(rng() * 360)
83
+ ctx.strokeStyle = hsl(hue, 60, 60)
84
+ ctx.beginPath()
85
+ ctx.moveTo(x, y)
86
+ for (let j = 0; j < len; j++) {
87
+ // 简易角度场:正弦/余弦混合
88
+ const ang =
89
+ Math.sin((x * 0.01) + (y * 0.013)) * 2.0 +
90
+ Math.cos((x * 0.02) - (y * 0.017)) * 1.8
91
+ x += Math.cos(ang)
92
+ y += Math.sin(ang)
93
+ ctx.lineTo(x, y)
94
+ if (x < 0 || y < 0 || x > W || y > H) break
95
+ }
96
+ ctx.stroke()
97
+ }
98
+ ctx.globalAlpha = 1
99
+ }
100
+
101
+ function drawTriangles(ctx, W, H, rng, palette) {
102
+ // 背景
103
+ ctx.fillStyle = '#0b1020'
104
+ ctx.fillRect(0, 0, W, H)
105
+ const count = 220
106
+ for (let i = 0; i < count; i++) {
107
+ const x1 = rng() * W, y1 = rng() * H
108
+ const x2 = x1 + (rng() - 0.5) * 200
109
+ const x3 = x1 + (rng() - 0.5) * 200
110
+ const y2 = y1 + (rng() - 0.5) * 200
111
+ const y3 = y1 + (rng() - 0.5) * 200
112
+ const col = palette[1 + Math.floor(rng() * (palette.length - 1))]
113
+ ctx.fillStyle = col
114
+ ctx.globalAlpha = 0.6 + rng() * 0.4
115
+ ctx.beginPath()
116
+ ctx.moveTo(x1, y1)
117
+ ctx.lineTo(x2, y2)
118
+ ctx.lineTo(x3, y3)
119
+ ctx.closePath()
120
+ ctx.fill()
121
+ }
122
+ ctx.globalAlpha = 1
123
+ }
124
+
125
+ function drawCircles(ctx, W, H, rng, palette) {
126
+ ctx.fillStyle = '#111827'
127
+ ctx.fillRect(0, 0, W, H)
128
+ const circles = []
129
+ const attempts = 8000
130
+ for (let i = 0; i < attempts; i++) {
131
+ const r = 2 + Math.pow(rng(), 2) * 28
132
+ const x = r + rng() * (W - 2 * r)
133
+ const y = r + rng() * (H - 2 * r)
134
+ let ok = true
135
+ for (const c of circles) {
136
+ const dx = x - c.x, dy = y - c.y
137
+ if (dx * dx + dy * dy < (r + c.r + 2) ** 2) { ok = false; break }
138
+ }
139
+ if (ok) circles.push({ x, y, r })
140
+ if (circles.length > 800) break
141
+ }
142
+ for (const c of circles) {
143
+ ctx.beginPath()
144
+ ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2)
145
+ const col = palette[1 + Math.floor(rng() * (palette.length - 1))]
146
+ ctx.fillStyle = col
147
+ ctx.globalAlpha = 0.5 + rng() * 0.5
148
+ ctx.fill()
149
+ }
150
+ ctx.globalAlpha = 1
151
+ }
152
+
153
+ function drawStripes(ctx, W, H, rng, palette) {
154
+ // 渐变背景
155
+ const grad = ctx.createLinearGradient(0, 0, W, H)
156
+ grad.addColorStop(0, palette[2])
157
+ grad.addColorStop(1, palette[4] || '#ffffff')
158
+ ctx.fillStyle = grad
159
+ ctx.fillRect(0, 0, W, H)
160
+ // 多层条纹
161
+ const layers = 8
162
+ for (let i = 0; i < layers; i++) {
163
+ ctx.save()
164
+ const angle = (rng() * Math.PI / 2) - Math.PI / 4
165
+ ctx.translate(W / 2, H / 2)
166
+ ctx.rotate(angle)
167
+ const col = palette[1 + Math.floor(rng() * (palette.length - 1))]
168
+ const stripeH = 8 + rng() * 24
169
+ ctx.fillStyle = col
170
+ ctx.globalAlpha = 0.08 + rng() * 0.14
171
+ for (let y = -H; y < H; y += stripeH * 2) {
172
+ ctx.fillRect(-W, y, W * 2, stripeH)
173
+ }
174
+ ctx.restore()
175
+ }
176
+ ctx.globalAlpha = 1
177
+ }
178
+
179
+ function drawStars(ctx, W, H, rng, palette) {
180
+ // 夜空
181
+ ctx.fillStyle = '#050914'
182
+ ctx.fillRect(0, 0, W, H)
183
+ // 星云轻微雾
184
+ const neb = ctx.createRadialGradient(W * 0.7, H * 0.3, 10, W * 0.7, H * 0.3, Math.max(W, H) * 0.8)
185
+ neb.addColorStop(0, 'rgba(64, 99, 187, 0.2)')
186
+ neb.addColorStop(1, 'rgba(5, 9, 20, 0)')
187
+ ctx.fillStyle = neb
188
+ ctx.fillRect(0, 0, W, H)
189
+ // 星点
190
+ const count = 1200
191
+ for (let i = 0; i < count; i++) {
192
+ const x = rng() * W
193
+ const y = rng() * H
194
+ const r = Math.pow(rng(), 3) * 2.2 + 0.3
195
+ const hue = 200 + rng() * 60
196
+ ctx.fillStyle = hsl(hue, 60, 80)
197
+ ctx.beginPath()
198
+ ctx.arc(x, y, r, 0, Math.PI * 2)
199
+ ctx.fill()
200
+ }
201
+ }
202
+
203
+ createApp({
204
+ setup() {
205
+ const state = reactive({
206
+ algo: 'flow',
207
+ width: 1024,
208
+ height: 768,
209
+ seed: Math.floor(Math.random() * 1e9),
210
+ seedInput: '',
211
+ palette: 'Default',
212
+ })
213
+
214
+ const currentAlgoLabel = computed(() => {
215
+ const a = algos.find(x => x.value === state.algo)
216
+ return a ? a.label : ''
217
+ })
218
+
219
+ function getRng() {
220
+ return mulberry32(state.seed >>> 0)
221
+ }
222
+
223
+ function render() {
224
+ const canvas = document.getElementById('canvas')
225
+ const ctx = canvas.getContext('2d')
226
+ const rng = getRng()
227
+ const palette = palettes[state.palette] || palettes.Default
228
+ const algo = algos.find(a => a.value === state.algo) || algos[0]
229
+ algo.draw(ctx, state.width, state.height, rng, palette)
230
+ }
231
+
232
+ function resize() {
233
+ const canvas = document.getElementById('canvas')
234
+ canvas.width = state.width
235
+ canvas.height = state.height
236
+ render()
237
+ }
238
+
239
+ function randomSeed() {
240
+ state.seed = Math.floor(Math.random() * 1e9)
241
+ state.seedInput = String(state.seed)
242
+ render()
243
+ }
244
+
245
+ function applySeed() {
246
+ const s = state.seedInput?.trim()
247
+ if (!s) {
248
+ randomSeed()
249
+ return
250
+ }
251
+ const maybeNum = Number(s)
252
+ state.seed = Number.isFinite(maybeNum) ? Math.floor(maybeNum) : strToSeed(s)
253
+ render()
254
+ }
255
+
256
+ function exportPNG() {
257
+ const canvas = document.getElementById('canvas')
258
+ const link = document.createElement('a')
259
+ const algoName = currentAlgoLabel.value.replace(/\s+/g, '')
260
+ link.download = `GenArt_${algoName}_seed${state.seed}_${state.width}x${state.height}.png`
261
+ link.href = canvas.toDataURL('image/png')
262
+ link.click()
263
+ }
264
+
265
+ onMounted(() => {
266
+ state.seedInput = String(state.seed)
267
+ render()
268
+ })
269
+
270
+ return {
271
+ state,
272
+ palettes,
273
+ algos,
274
+ currentAlgoLabel,
275
+ render,
276
+ resize,
277
+ randomSeed,
278
+ applySeed,
279
+ exportPNG,
280
+ }
281
+ }
282
+ }).mount('#app')
283
+ })()
284
+
templates/index.html ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>生成艺术实验室 · Generative Art Lab</title>
7
+ <script src="https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ html, body { height: 100%; }
11
+ canvas { image-rendering: crisp-edges; }
12
+ </style>
13
+ </head>
14
+ <body class="bg-slate-50 text-slate-900">
15
+ <div id="app" class="min-h-screen">
16
+ <header class="border-b bg-white sticky top-0 z-10">
17
+ <div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
18
+ <h1 class="text-xl sm:text-2xl font-bold">生成艺术实验室</h1>
19
+ <div class="text-sm text-slate-600">纯前端渲染 · 可复现种子 · 一键导出</div>
20
+ </div>
21
+ </header>
22
+
23
+ <main class="max-w-6xl mx-auto px-4 py-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
24
+ <section class="lg:col-span-1 bg-white border rounded-xl p-4 space-y-4">
25
+ <h2 class="font-semibold text-lg">参数设置</h2>
26
+ <div class="grid grid-cols-2 gap-3">
27
+ <label class="col-span-2 text-sm text-slate-600">算法</label>
28
+ <select v-model="state.algo" @change="render" class="col-span-2 border rounded px-2 py-1">
29
+ <option v-for="a in algos" :key="a.value" :value="a.value">{{ a.label }}</option>
30
+ </select>
31
+
32
+ <label class="col-span-2 text-sm text-slate-600">配色方案</label>
33
+ <select v-model="state.palette" @change="render" class="col-span-2 border rounded px-2 py-1">
34
+ <option v-for="(p, key) in palettes" :key="key" :value="key">{{ key }}</option>
35
+ </select>
36
+
37
+ <label class="text-sm text-slate-600">宽度</label>
38
+ <input type="number" v-model.number="state.width" @change="resize" class="border rounded px-2 py-1" min="256" max="2048" step="16" />
39
+ <label class="text-sm text-slate-600">高度</label>
40
+ <input type="number" v-model.number="state.height" @change="resize" class="border rounded px-2 py-1" min="256" max="2048" step="16" />
41
+
42
+ <label class="col-span-2 text-sm text-slate-600">种子</label>
43
+ <div class="col-span-2 flex gap-2">
44
+ <input v-model="state.seedInput" @change="applySeed" class="flex-1 border rounded px-2 py-1" placeholder="输入或留空随机" />
45
+ <button @click="randomSeed" class="border rounded px-3 py-1 hover:bg-slate-50">随机</button>
46
+ </div>
47
+
48
+ <div class="col-span-2 flex items-center justify-between pt-2">
49
+ <button @click="render" class="bg-slate-900 text-white px-3 py-1.5 rounded hover:opacity-90">重新生成</button>
50
+ <button @click="exportPNG" class="border px-3 py-1.5 rounded hover:bg-slate-50">导出 PNG</button>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="pt-4">
55
+ <h3 class="font-semibold text-base mb-2">当前信息</h3>
56
+ <p class="text-sm text-slate-600">算法:{{ currentAlgoLabel }}</p>
57
+ <p class="text-sm text-slate-600">种子:{{ state.seed }}</p>
58
+ <p class="text-sm text-slate-600">尺寸:{{ state.width }} × {{ state.height }}</p>
59
+ </div>
60
+ </section>
61
+
62
+ <section class="lg:col-span-2 bg-white border rounded-xl p-4">
63
+ <div class="flex items-center justify-between">
64
+ <h2 class="font-semibold text-lg">画布预览</h2>
65
+ <div class="text-xs text-slate-500">完全本地渲染,保障隐私</div>
66
+ </div>
67
+ <div class="mt-3 border rounded-lg overflow-hidden relative">
68
+ <canvas id="canvas" :width="state.width" :height="state.height" class="w-full h-auto block"></canvas>
69
+ </div>
70
+
71
+ <div class="mt-6">
72
+ <h3 class="font-semibold mb-2">算法说明</h3>
73
+ <ul class="list-disc pl-6 text-slate-700 space-y-1">
74
+ <li v-for="a in algos" :key="a.value">
75
+ <span class="font-medium">{{ a.label }}:</span>{{ a.desc }}
76
+ </li>
77
+ </ul>
78
+ </div>
79
+ </section>
80
+ </main>
81
+
82
+ <footer class="max-w-6xl mx-auto px-4 py-8 text-sm text-slate-500">
83
+ <div>Tips:每次更换种子或参数都会得到可复现的新图案;导出为无损 PNG。</div>
84
+ </footer>
85
+ </div>
86
+
87
+ <script src="/static/js/app.js"></script>
88
+ </body>
89
+ </html>
90
+