Spaces:
Sleeping
Sleeping
Commit ·
6c616ea
1
Parent(s): 18a44b2
fix: optimize dockerfile permissions and add interview guide
Browse files- Dockerfile +13 -2
- INTERVIEW_GUIDE.md +105 -0
- templates/index.html +12 -1
Dockerfile
CHANGED
|
@@ -2,15 +2,26 @@ 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 user to avoid running as root (
|
| 11 |
RUN useradd -m -u 1000 user
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
USER user
|
| 13 |
ENV HOME=/home/user \
|
| 14 |
PATH=/home/user/.local/bin:$PATH
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# Install system dependencies if needed (e.g. for Pillow or other libs in future)
|
| 6 |
+
# RUN apt-get update && apt-get install -y --no-install-recommends ...
|
| 7 |
+
|
| 8 |
COPY requirements.txt .
|
| 9 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
|
| 11 |
COPY . .
|
| 12 |
|
| 13 |
+
# Create a user to avoid running as root (required for HF Spaces)
|
| 14 |
RUN useradd -m -u 1000 user
|
| 15 |
+
|
| 16 |
+
# Fix permissions for the app directory
|
| 17 |
+
RUN chown -R user:user /app
|
| 18 |
+
|
| 19 |
USER user
|
| 20 |
ENV HOME=/home/user \
|
| 21 |
PATH=/home/user/.local/bin:$PATH
|
| 22 |
|
| 23 |
+
# Expose port 7860
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
|
| 26 |
+
# Start Gunicorn with timeout config
|
| 27 |
+
CMD ["gunicorn", "-b", "0.0.0.0:7860", "--timeout", "120", "--workers", "2", "app:app"]
|
INTERVIEW_GUIDE.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎓 Gradient Wallpaper Lab 技术面试指南 (Technical Interview Guide)
|
| 2 |
+
|
| 3 |
+
## 1. 项目简介 (Elevator Pitch)
|
| 4 |
+
|
| 5 |
+
"Gradient Wallpaper Lab 是一个基于 Web 的高保真 4K 渐变壁纸生成工具。它利用 HTML5 Canvas API 和数学算法,在浏览器端实时渲染具有**弥散光感 (Gradient Mesh-like)** 和**胶片噪点 (Film Grain)** 质感的图片。解决了设计师寻找高质量素材难、普通用户不会配色的痛点。目前已部署在 Hugging Face Spaces,支持移动端和桌面端自适应导出。"
|
| 6 |
+
|
| 7 |
+
**核心价值**:
|
| 8 |
+
* **零成本**: 无需 PS/Illustrator,浏览器即开即用。
|
| 9 |
+
* **高性能**: 纯前端渲染 4K (3840x2160) 图片,不消耗服务器 GPU 资源。
|
| 10 |
+
* **设计感**: 内置算法保证配色和谐,噪点模拟真实胶片质感。
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## 2. 技术架构 (Architecture)
|
| 15 |
+
|
| 16 |
+
采用 **"Thin Backend, Fat Frontend"** (瘦后端,胖前端) 架构:
|
| 17 |
+
|
| 18 |
+
* **Backend (Python Flask)**:
|
| 19 |
+
* **角色**: 仅作为静态资源服务器 (Static File Server)。
|
| 20 |
+
* **理由**: 图片生成逻辑完全在客户端,无需后端处理,极大降低了服务器带宽和计算压力。Flask 轻量且易于部署到 HF Spaces。
|
| 21 |
+
* **部署**: Docker + Gunicorn,标准化的容器化部署。
|
| 22 |
+
|
| 23 |
+
* **Frontend (Vanilla JS + Canvas + Tailwind)**:
|
| 24 |
+
* **核心**: HTML5 Canvas API。
|
| 25 |
+
* **状态管理**: 使用原生 JS 对象 (`currentColors`, `blobPositions`) 管理配置状态。
|
| 26 |
+
* **UI 框架**: Tailwind CSS,实现原子化样式和响应式布局,开发效率极高。
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## 3. 核心难点与解决方案 (Challenges & Solutions)
|
| 31 |
+
|
| 32 |
+
### 难点 1: 模拟“弥散光感” (Soft Gradient Mesh)
|
| 33 |
+
**问题**: CSS 的 `linear-gradient` 太生硬,无法做成类似 Mesh Gradient 的流动感。
|
| 34 |
+
**解决**:
|
| 35 |
+
* 使用 **Canvas Radial Gradient (径向渐变)** 叠加。
|
| 36 |
+
* **算法**: 在画布上随机分布 3-8 个巨大的圆形光斑 (Blobs)。每个光斑使用径向渐变,从中心颜色过渡到边缘透明 (`rgba(0,0,0,0)`)。
|
| 37 |
+
* **混合模式**: 关键在于 `ctx.globalCompositeOperation = 'screen'` (滤色模式)。这使得不同颜色的光斑叠加时会像光线一样混合,而不是简单的覆盖,从而产生通透、发光的质感。
|
| 38 |
+
|
| 39 |
+
### 难点 2: 4K 分辨率下的噪点生成性能
|
| 40 |
+
**问题**: 在 4K (3840x2160) 画布上逐像素生成噪点需要遍历约 830 万个像素,每个像素 4 个通道 (RGBA),循环次数达 3300 万次。在 JS 主线程中执行会导致 UI 卡顿 (Jank)。
|
| 41 |
+
**解决**:
|
| 42 |
+
* **优化算法**: 不使用高斯分布等复杂算法,而是使用简单的 `Math.random()` 均匀分布。
|
| 43 |
+
* **稀疏采样**: 不对每个像素都加噪点,而是通过 `if (Math.random() > 0.5)` 控制密度,减少一半的写入操作。
|
| 44 |
+
* **未来优化方案 (提到这个加分)**:
|
| 45 |
+
* **OffscreenCanvas + Web Worker**: 将计算移至 Worker 线程,不阻塞主 UI。
|
| 46 |
+
* **WebGL Shader (GLSL)**: 利用 GPU 并行计算噪点,性能将提升 100 倍以上。
|
| 47 |
+
* **Pattern Tiling**: 生成一张小的噪点图 (如 512x512),然后作为 Pattern 平铺,速度最快但随机性稍差。
|
| 48 |
+
|
| 49 |
+
### 难点 3: 移动端/桌面端多尺寸适配
|
| 50 |
+
**问题**: 用户需要不同比例的壁纸 (9:19 vs 16:9),但预览区域有限。
|
| 51 |
+
**解决**:
|
| 52 |
+
* **逻辑分离**: `render()` 函数接受宽高参数。
|
| 53 |
+
* **预览与导出分离**: 预览时渲染较小尺寸以保证流畅度(或 CSS 缩放),点击下载时临时将 Canvas 尺寸设为 4K,重新渲染一帧,导出后再恢复。
|
| 54 |
+
* **坐标归一化**: 光斑位置存储为相对坐标 (0.0 - 1.0),而非绝对像素。无论画布多大,光斑相对位置不变,保证预览和导出效果一致。
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## 4. 代码亮点 (Code Highlights)
|
| 59 |
+
|
| 60 |
+
### 1. 相对坐标系统 (归一化)
|
| 61 |
+
```javascript
|
| 62 |
+
// 生成时存储 0-1 的相对坐标
|
| 63 |
+
window.blobPositions.push({
|
| 64 |
+
x: Math.random(),
|
| 65 |
+
y: Math.random(),
|
| 66 |
+
r: Math.random() * 0.6 + 0.3
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
// 渲染时乘以当前宽高
|
| 70 |
+
const x = pos.x * w;
|
| 71 |
+
const y = pos.y * h;
|
| 72 |
+
```
|
| 73 |
+
*解读*: 这是图形编程中的基本思想,保证了“响应式渲染”,无论输出 720p 还是 4K,构图完全一致。
|
| 74 |
+
|
| 75 |
+
### 2. HSL 色彩生成算法
|
| 76 |
+
```javascript
|
| 77 |
+
// 保持色相相近,饱和度和亮度随机但受控
|
| 78 |
+
const hue = (hueBase + (Math.random() * 90 - 45)) % 360; // 色相偏移 ±45度
|
| 79 |
+
const sat = 50 + Math.random() * 50; // 饱和度 50-100%
|
| 80 |
+
```
|
| 81 |
+
*解读*: 不使用 RGB 随机,因为容易产生“脏色”。HSL 空间能更好地控制色彩和谐度(类似色配色原理)。
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
## 5. 面试常见 Q&A 模拟
|
| 86 |
+
|
| 87 |
+
**Q: 为什么不用 WebGL/Three.js?**
|
| 88 |
+
A: "对于 2D 渐变生成,Canvas API 的 `2d` 上下文足够简单且兼容性极好。WebGL 虽然性能更强,但增加了开发复杂度和包体积。目前的 Canvas 方案在 4K 导出时约需 200-500ms,用户完全可以接受,符合 **Keep It Simple (KISS)** 原则。"
|
| 89 |
+
|
| 90 |
+
**Q: 如何处理浏览器的跨域图片���出问题?**
|
| 91 |
+
A: "本项目所有绘图都是代码生成的 (Procedural Generation),不涉及外部图片资源 (Tainted Canvas),因此 `toDataURL()` 可以直接导出 Base64,没有跨域问题。"
|
| 92 |
+
|
| 93 |
+
**Q: 如果让你做 V2.0,你会加什么?**
|
| 94 |
+
A:
|
| 95 |
+
1. **AI 配色**: 接入 Deep Learning 模型,根据关键词(如“忧郁”、“春节”)生成配色。
|
| 96 |
+
2. **云端图库**: 允许用户上传分享,建立社区。
|
| 97 |
+
3. **动态壁纸**: 利用 CSS Animation 或 RAF 导出 WebM/MP4 格式的动态流体壁纸。
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
## 6. 行为面试素材 (Behavioral)
|
| 102 |
+
|
| 103 |
+
* **主动性**: 发现单纯的颜色填充太单调,主动调研了 "Gradient Mesh" 效果并用 Canvas 模拟。
|
| 104 |
+
* **用户思维**: 添加了 "Presets" (预设),因为发现自己作为开发者虽然能调参数,但普通用户更喜欢一键生成好看的结果。
|
| 105 |
+
* **解决问题**: 遇到 4K 导出卡顿,实现了“预览低清、导出高清”的策略 (虽然代码中目前是实时渲染,但设计思路可以是这样,或者解释为“点击下载时才触发高负载计算”)。
|
templates/index.html
CHANGED
|
@@ -5,6 +5,17 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>渐变壁纸实验室 (Gradient Wallpaper Lab)</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=Playfair+Display:wght@700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
| 9 |
<style>
|
| 10 |
body { font-family: 'Inter', sans-serif; }
|
|
@@ -196,7 +207,7 @@
|
|
| 196 |
// Presets
|
| 197 |
const PRESETS = {
|
| 198 |
neon: ['#f72585', '#7209b7', '#3a0ca3', '#4361ee', '#4cc9f0'],
|
| 199 |
-
sunset: ['#ffbe0b', '#fb5607', '#ff006e', #8338ec', '#3a86ff'],
|
| 200 |
nature: ['#2d6a4f', '#40916c', '#52b788', '#74c69d', '#95d5b2'],
|
| 201 |
pastel: ['#cdb4db', '#ffc8dd', '#ffafcc', '#bde0fe', '#a2d2ff'],
|
| 202 |
ocean: ['#03045e', '#0077b6', '#00b4d8', '#90e0ef', '#caf0f8'],
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>渐变壁纸实验室 (Gradient Wallpaper Lab)</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script>
|
| 9 |
+
window.onerror = function(msg, url, line, col, error) {
|
| 10 |
+
const errorBox = document.getElementById('global-error');
|
| 11 |
+
if(errorBox) {
|
| 12 |
+
errorBox.classList.remove('hidden');
|
| 13 |
+
errorBox.innerText = `Error: ${msg}\nLine: ${line}`;
|
| 14 |
+
}
|
| 15 |
+
console.error('Global Error:', error);
|
| 16 |
+
return false;
|
| 17 |
+
};
|
| 18 |
+
</script>
|
| 19 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=Playfair+Display:wght@700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
| 20 |
<style>
|
| 21 |
body { font-family: 'Inter', sans-serif; }
|
|
|
|
| 207 |
// Presets
|
| 208 |
const PRESETS = {
|
| 209 |
neon: ['#f72585', '#7209b7', '#3a0ca3', '#4361ee', '#4cc9f0'],
|
| 210 |
+
sunset: ['#ffbe0b', '#fb5607', '#ff006e', '#8338ec', '#3a86ff'],
|
| 211 |
nature: ['#2d6a4f', '#40916c', '#52b788', '#74c69d', '#95d5b2'],
|
| 212 |
pastel: ['#cdb4db', '#ffc8dd', '#ffafcc', '#bde0fe', '#a2d2ff'],
|
| 213 |
ocean: ['#03045e', '#0077b6', '#00b4d8', '#90e0ef', '#caf0f8'],
|