Spaces:
Sleeping
Sleeping
| ;(() => { | |
| const { createApp, reactive, computed, onMounted } = Vue | |
| // 简易可复现随机数(Mulberry32) | |
| function mulberry32(a) { | |
| return function () { | |
| let t = (a += 0x6D2B79F5) | |
| t = Math.imul(t ^ (t >>> 15), t | 1) | |
| t ^= t + Math.imul(t ^ (t >>> 7), t | 61) | |
| return ((t ^ (t >>> 14)) >>> 0) / 4294967296 | |
| } | |
| } | |
| // 字符串 -> 种子 | |
| function strToSeed(str) { | |
| let h = 2166136261 >>> 0 | |
| for (let i = 0; i < str.length; i++) { | |
| h ^= str.charCodeAt(i) | |
| h = Math.imul(h, 16777619) | |
| } | |
| return h >>> 0 | |
| } | |
| // 颜色工具 | |
| function hsl(h, s, l) { | |
| return `hsl(${h}, ${s}%, ${l}%)` | |
| } | |
| // 预设配色 | |
| const palettes = { | |
| Default: ['#0f172a', '#475569', '#94a3b8', '#e2e8f0', '#f59e0b'], | |
| Minimalist: ['#111827', '#6b7280', '#d1d5db', '#f3f4f6', '#10b981'], | |
| Dark: ['#0b1020', '#1f2937', '#334155', '#94a3b8', '#f8fafc'], | |
| Warm: ['#7f1d1d', '#b45309', '#f59e0b', '#fde68a', '#fef3c7'], | |
| Ocean: ['#0e7490', '#155e75', '#134e4a', '#10b981', '#67e8f9'], | |
| } | |
| // 算法集合 | |
| const algos = [ | |
| { | |
| value: 'flow', | |
| label: '流场线条', | |
| desc: '基于角度场的粒子流动,形成丝绸般纹理', | |
| draw: drawFlowField, | |
| }, | |
| { | |
| value: 'tri', | |
| label: '三角碎片', | |
| desc: '随机三角网格叠加,构成抽象拼贴', | |
| draw: drawTriangles, | |
| }, | |
| { | |
| value: 'circles', | |
| label: '近似圆填充', | |
| desc: '随机投点生成不重叠圆,层次丰富', | |
| draw: drawCircles, | |
| }, | |
| { | |
| value: 'stripes', | |
| label: '渐变条纹', | |
| desc: '多层渐变条纹旋转叠加,颜色过渡顺滑', | |
| draw: drawStripes, | |
| }, | |
| { | |
| value: 'stars', | |
| label: '星空点阵', | |
| desc: '夜空背景 + 高斯分布星点,轻微辉光', | |
| draw: drawStars, | |
| }, | |
| { | |
| value: 'neon', | |
| label: '赛博霓虹', | |
| desc: '暗色背景下的高亮几何与透视网格,赛博朋克风', | |
| draw: drawNeonCity, | |
| }, | |
| { | |
| value: 'matrix', | |
| label: '矩阵雨', | |
| desc: '经典的绿色字符雨下落效果', | |
| draw: drawMatrixRain, | |
| }, | |
| { | |
| value: 'mondrian', | |
| label: '蒙德里安', | |
| desc: '随机分割矩形与红黄蓝填色,几何抽象', | |
| draw: drawMondrian, | |
| }, | |
| { | |
| value: 'hex', | |
| label: '六边形马赛克', | |
| desc: '蜂巢结构平铺,随机透明度与大小变化', | |
| draw: drawHexMosaic, | |
| }, | |
| { | |
| value: 'mandala', | |
| label: '曼陀罗万花筒', | |
| desc: '中心对称旋转图案,多重几何叠加', | |
| draw: drawMandala, | |
| }, | |
| ] | |
| // 绘制函数们 | |
| function drawFlowField(ctx, W, H, rng, palette) { | |
| ctx.fillStyle = palette[0] | |
| ctx.fillRect(0, 0, W, H) | |
| const steps = 5000 | |
| ctx.globalAlpha = 0.08 | |
| ctx.lineWidth = 1 | |
| for (let i = 0; i < steps; i++) { | |
| let x = rng() * W | |
| let y = rng() * H | |
| const len = 80 + rng() * 150 | |
| const hue = Math.floor(rng() * 360) | |
| ctx.strokeStyle = hsl(hue, 60, 60) | |
| ctx.beginPath() | |
| ctx.moveTo(x, y) | |
| for (let j = 0; j < len; j++) { | |
| // 简易角度场:正弦/余弦混合 | |
| const ang = | |
| Math.sin((x * 0.01) + (y * 0.013)) * 2.0 + | |
| Math.cos((x * 0.02) - (y * 0.017)) * 1.8 | |
| x += Math.cos(ang) | |
| y += Math.sin(ang) | |
| ctx.lineTo(x, y) | |
| if (x < 0 || y < 0 || x > W || y > H) break | |
| } | |
| ctx.stroke() | |
| } | |
| ctx.globalAlpha = 1 | |
| } | |
| function drawTriangles(ctx, W, H, rng, palette) { | |
| // 背景 | |
| ctx.fillStyle = '#0b1020' | |
| ctx.fillRect(0, 0, W, H) | |
| const count = 220 | |
| for (let i = 0; i < count; i++) { | |
| const x1 = rng() * W, y1 = rng() * H | |
| const x2 = x1 + (rng() - 0.5) * 200 | |
| const x3 = x1 + (rng() - 0.5) * 200 | |
| const y2 = y1 + (rng() - 0.5) * 200 | |
| const y3 = y1 + (rng() - 0.5) * 200 | |
| const col = palette[1 + Math.floor(rng() * (palette.length - 1))] | |
| ctx.fillStyle = col | |
| ctx.globalAlpha = 0.6 + rng() * 0.4 | |
| ctx.beginPath() | |
| ctx.moveTo(x1, y1) | |
| ctx.lineTo(x2, y2) | |
| ctx.lineTo(x3, y3) | |
| ctx.closePath() | |
| ctx.fill() | |
| } | |
| ctx.globalAlpha = 1 | |
| } | |
| function drawCircles(ctx, W, H, rng, palette) { | |
| ctx.fillStyle = '#111827' | |
| ctx.fillRect(0, 0, W, H) | |
| const circles = [] | |
| const attempts = 8000 | |
| for (let i = 0; i < attempts; i++) { | |
| const r = 2 + Math.pow(rng(), 2) * 28 | |
| const x = r + rng() * (W - 2 * r) | |
| const y = r + rng() * (H - 2 * r) | |
| let ok = true | |
| for (const c of circles) { | |
| const dx = x - c.x, dy = y - c.y | |
| if (dx * dx + dy * dy < (r + c.r + 2) ** 2) { ok = false; break } | |
| } | |
| if (ok) circles.push({ x, y, r }) | |
| if (circles.length > 800) break | |
| } | |
| for (const c of circles) { | |
| ctx.beginPath() | |
| ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2) | |
| const col = palette[1 + Math.floor(rng() * (palette.length - 1))] | |
| ctx.fillStyle = col | |
| ctx.globalAlpha = 0.5 + rng() * 0.5 | |
| ctx.fill() | |
| } | |
| ctx.globalAlpha = 1 | |
| } | |
| function drawStripes(ctx, W, H, rng, palette) { | |
| // 渐变背景 | |
| const grad = ctx.createLinearGradient(0, 0, W, H) | |
| grad.addColorStop(0, palette[2]) | |
| grad.addColorStop(1, palette[4] || '#ffffff') | |
| ctx.fillStyle = grad | |
| ctx.fillRect(0, 0, W, H) | |
| // 多层条纹 | |
| const layers = 8 | |
| for (let i = 0; i < layers; i++) { | |
| ctx.save() | |
| const angle = (rng() * Math.PI / 2) - Math.PI / 4 | |
| ctx.translate(W / 2, H / 2) | |
| ctx.rotate(angle) | |
| const col = palette[1 + Math.floor(rng() * (palette.length - 1))] | |
| const stripeH = 8 + rng() * 24 | |
| ctx.fillStyle = col | |
| ctx.globalAlpha = 0.08 + rng() * 0.14 | |
| for (let y = -H; y < H; y += stripeH * 2) { | |
| ctx.fillRect(-W, y, W * 2, stripeH) | |
| } | |
| ctx.restore() | |
| } | |
| ctx.globalAlpha = 1 | |
| } | |
| function drawStars(ctx, W, H, rng, palette) { | |
| // 夜空 | |
| ctx.fillStyle = '#050914' | |
| ctx.fillRect(0, 0, W, H) | |
| // 星云轻微雾 | |
| const neb = ctx.createRadialGradient(W * 0.7, H * 0.3, 10, W * 0.7, H * 0.3, Math.max(W, H) * 0.8) | |
| neb.addColorStop(0, 'rgba(64, 99, 187, 0.2)') | |
| neb.addColorStop(1, 'rgba(5, 9, 20, 0)') | |
| ctx.fillStyle = neb | |
| ctx.fillRect(0, 0, W, H) | |
| // 星点 | |
| const count = 1200 | |
| for (let i = 0; i < count; i++) { | |
| const x = rng() * W | |
| const y = rng() * H | |
| const r = Math.pow(rng(), 3) * 2.2 + 0.3 | |
| const hue = 200 + rng() * 60 | |
| ctx.fillStyle = hsl(hue, 60, 80) | |
| ctx.beginPath() | |
| ctx.arc(x, y, r, 0, Math.PI * 2) | |
| ctx.fill() | |
| } | |
| } | |
| function drawNeonCity(ctx, W, H, rng, palette) { | |
| // 深色背景 | |
| ctx.fillStyle = '#050510' | |
| ctx.fillRect(0, 0, W, H) | |
| // 透视网格 | |
| ctx.save() | |
| ctx.strokeStyle = '#220033' | |
| ctx.lineWidth = 1 | |
| const horizon = H * 0.65 | |
| // 纵向线 (透视) | |
| for (let x = -W; x < W * 2; x += 40) { | |
| ctx.beginPath() | |
| ctx.moveTo(x, H) | |
| ctx.lineTo(W / 2 + (x - W / 2) * 0.1, horizon) | |
| ctx.stroke() | |
| } | |
| // 横向线 | |
| for (let y = H; y > horizon; y -= 20 * Math.pow((y - horizon) / (H - horizon), 0.5)) { | |
| ctx.beginPath() | |
| ctx.moveTo(0, y) | |
| ctx.lineTo(W, y) | |
| ctx.stroke() | |
| } | |
| ctx.restore() | |
| // 太阳/月亮 | |
| const sunX = W * 0.5 | |
| const sunY = horizon - 80 | |
| const sunR = 80 | |
| const sunGrad = ctx.createLinearGradient(sunX, sunY - sunR, sunX, sunY + sunR) | |
| sunGrad.addColorStop(0, '#ffcc00') | |
| sunGrad.addColorStop(0.5, '#ff0055') | |
| sunGrad.addColorStop(1, '#9900cc') | |
| ctx.fillStyle = sunGrad | |
| ctx.beginPath() | |
| ctx.arc(sunX, sunY, sunR, 0, Math.PI * 2) | |
| ctx.fill() | |
| // 割线 | |
| ctx.fillStyle = '#050510' | |
| for(let i=0; i<8; i++){ | |
| const h = 4 + i*2 | |
| const y = sunY + sunR * 0.2 + i * 15 | |
| if(y > sunY + sunR) break | |
| ctx.fillRect(sunX - sunR, y, sunR*2, h) | |
| } | |
| // 随机建筑 | |
| const bCount = 40 | |
| for (let i = 0; i < bCount; i++) { | |
| const w = 30 + rng() * 60 | |
| const h = 50 + rng() * 200 | |
| const x = rng() * W | |
| // 简单遮挡关系不处理,直接画 | |
| if (Math.abs(x - W/2) < 100) continue // 避开中间太阳 | |
| const y = horizon - h * 0.1 // 远景 | |
| if (rng() > 0.6) { | |
| // 近景 | |
| const nearY = horizon + rng() * (H - horizon) | |
| // 只是装饰线条 | |
| ctx.fillStyle = `rgba(0, 255, 255, ${rng() * 0.5})` | |
| ctx.fillRect(x, nearY, 2, 20) | |
| } | |
| } | |
| // 随机发光线条 | |
| for(let i=0; i<20; i++) { | |
| const x1 = rng() * W | |
| const y1 = rng() * (H * 0.6) | |
| const x2 = x1 + (rng() - 0.5) * 200 | |
| const y2 = y1 + (rng() - 0.5) * 200 | |
| ctx.strokeStyle = rng() > 0.5 ? '#00ffff' : '#ff00ff' | |
| ctx.lineWidth = 2 | |
| ctx.shadowBlur = 10 | |
| ctx.shadowColor = ctx.strokeStyle | |
| ctx.beginPath() | |
| ctx.moveTo(x1, y1) | |
| ctx.lineTo(x2, y2) | |
| ctx.stroke() | |
| ctx.shadowBlur = 0 | |
| } | |
| } | |
| function drawMatrixRain(ctx, W, H, rng, palette) { | |
| ctx.fillStyle = '#000000' | |
| ctx.fillRect(0, 0, W, H) | |
| const fontSize = 16 | |
| const columns = Math.floor(W / fontSize) | |
| ctx.font = `${fontSize}px monospace` | |
| // 每一列的当前y位置,基于随机种子初始化 | |
| const drops = [] | |
| for(let i=0; i<columns; i++) { | |
| drops[i] = Math.floor(rng() * H / fontSize) | |
| } | |
| // 静态渲染,模拟雨停留在某一帧的效果,或者我们画很多帧叠加? | |
| // 生成艺术通常是静态图。这里我们画出密集的字符 | |
| const iterations = Math.floor(H / fontSize) * 2 // 覆盖足够密度 | |
| for(let step=0; step<iterations; step++) { | |
| for(let i=0; i<columns; i++) { | |
| // 随机字符 | |
| const text = String.fromCharCode(0x30A0 + Math.floor(rng() * 96)) | |
| const x = i * fontSize | |
| const y = drops[i] * fontSize | |
| // 颜色:越下面越亮,或者随机高亮 | |
| const isHead = (rng() > 0.98) | |
| if (isHead) { | |
| ctx.fillStyle = '#fff' | |
| ctx.shadowBlur = 8 | |
| ctx.shadowColor = '#fff' | |
| } else { | |
| // 绿色渐变 | |
| const alpha = 0.1 + rng() * 0.9 | |
| ctx.fillStyle = `rgba(0, 255, 70, ${alpha})` | |
| ctx.shadowBlur = 0 | |
| } | |
| ctx.fillText(text, x, y) | |
| // 移动下落,如果超过高度随机重置 | |
| if (y > H && rng() > 0.975) { | |
| drops[i] = 0 | |
| } | |
| drops[i]++ | |
| } | |
| } | |
| } | |
| function drawMondrian(ctx, W, H, rng, palette) { | |
| // 蒙德里安经典色:红黄蓝黑白。这里为了适配 palette,我们取 palette 的颜色来做填色 | |
| // 但为了保持风格,我们固定线条为黑色,背景为白色 | |
| ctx.fillStyle = '#f8fafc' | |
| ctx.fillRect(0, 0, W, H) | |
| const rects = [{x: 20, y: 20, w: W-40, h: H-40}] | |
| const limit = 60 // 分割次数 | |
| for(let i=0; i<limit; i++) { | |
| // 随机取一个矩形 | |
| const idx = Math.floor(rng() * rects.length) | |
| const r = rects[idx] | |
| // 决定分割方向:宽大于高则垂直分割,否则水平 | |
| // 加一点随机性 | |
| const splitVert = r.w > r.h ? (rng() > 0.3) : (rng() > 0.7) | |
| if (splitVert && r.w > 60) { | |
| const splitX = Math.floor((0.3 + rng() * 0.4) * r.w) | |
| rects.splice(idx, 1) | |
| rects.push({x: r.x, y: r.y, w: splitX, h: r.h}) | |
| rects.push({x: r.x + splitX, y: r.y, w: r.w - splitX, h: r.h}) | |
| } else if (!splitVert && r.h > 60) { | |
| const splitY = Math.floor((0.3 + rng() * 0.4) * r.h) | |
| rects.splice(idx, 1) | |
| rects.push({x: r.x, y: r.y, w: r.w, h: splitY}) | |
| rects.push({x: r.x, y: r.y + splitY, w: r.w, h: r.h - splitY}) | |
| } | |
| } | |
| // 绘制 | |
| ctx.lineWidth = 6 | |
| ctx.strokeStyle = '#0f172a' // 接近黑色 | |
| for(const r of rects) { | |
| // 随机填色 | |
| const rand = rng() | |
| let fill = '#f8fafc' // 白 | |
| if (rand > 0.7) { | |
| // 使用 palette 中的颜色 | |
| const colIdx = 1 + Math.floor(rng() * (palette.length - 1)) | |
| fill = palette[colIdx] | |
| } | |
| ctx.fillStyle = fill | |
| ctx.fillRect(r.x, r.y, r.w, r.h) | |
| ctx.strokeRect(r.x, r.y, r.w, r.h) | |
| } | |
| } | |
| function drawHexMosaic(ctx, W, H, rng, palette) { | |
| ctx.fillStyle = palette[0] | |
| ctx.fillRect(0, 0, W, H) | |
| const size = 30 | |
| const w34 = size * 3/2 | |
| const h = Math.sqrt(3) * size | |
| const cols = Math.ceil(W / w34) + 2 | |
| const rows = Math.ceil(H / h) + 2 | |
| for(let i=-1; i<cols; i++) { | |
| for(let j=-1; j<rows; j++) { | |
| const cx = i * w34 | |
| const cy = j * h + (i % 2) * (h / 2) | |
| // 随机大小缩放 | |
| const scale = 0.5 + rng() * 0.5 | |
| ctx.beginPath() | |
| for(let k=0; k<6; k++) { | |
| const angle = Math.PI / 3 * k | |
| const px = cx + size * scale * Math.cos(angle) | |
| const py = cy + size * scale * Math.sin(angle) | |
| if (k===0) ctx.moveTo(px, py) | |
| else ctx.lineTo(px, py) | |
| } | |
| ctx.closePath() | |
| const col = palette[1 + Math.floor(rng() * (palette.length - 1))] | |
| ctx.fillStyle = col | |
| ctx.globalAlpha = 0.4 + rng() * 0.6 | |
| ctx.fill() | |
| // 偶尔描边 | |
| if (rng() > 0.7) { | |
| ctx.strokeStyle = 'rgba(255,255,255,0.5)' | |
| ctx.lineWidth = 1 | |
| ctx.stroke() | |
| } | |
| } | |
| } | |
| ctx.globalAlpha = 1 | |
| } | |
| function drawMandala(ctx, W, H, rng, palette) { | |
| // 径向背景 | |
| const grad = ctx.createRadialGradient(W/2, H/2, 10, W/2, H/2, W/1.4) | |
| grad.addColorStop(0, palette[0]) | |
| grad.addColorStop(1, '#000') | |
| ctx.fillStyle = grad | |
| ctx.fillRect(0, 0, W, H) | |
| const cx = W/2, cy = H/2 | |
| const slices = 8 + Math.floor(rng() * 4) * 2 // 8, 10, 12, 14 | |
| const angleStep = Math.PI * 2 / slices | |
| const layers = 6 + Math.floor(rng() * 4) | |
| ctx.translate(cx, cy) | |
| for(let l=0; l<layers; l++) { | |
| const rBase = (l+1) * (Math.min(W,H) / 2 / (layers+1)) | |
| const shapeType = Math.floor(rng() * 3) // 0: circle, 1: diamond, 2: line | |
| const col = palette[1 + Math.floor(rng() * (palette.length - 1))] | |
| ctx.fillStyle = col | |
| ctx.strokeStyle = col | |
| ctx.lineWidth = 2 | |
| for(let i=0; i<slices; i++) { | |
| ctx.save() | |
| ctx.rotate(i * angleStep) | |
| const dist = rBase + (rng()-0.5)*20 | |
| if (shapeType === 0) { | |
| ctx.beginPath() | |
| ctx.arc(dist, 0, 5 + rng()*10, 0, Math.PI*2) | |
| ctx.fill() | |
| } else if (shapeType === 1) { | |
| const s = 8 + rng()*12 | |
| ctx.beginPath() | |
| ctx.moveTo(dist, -s) | |
| ctx.lineTo(dist+s, 0) | |
| ctx.lineTo(dist, s) | |
| ctx.lineTo(dist-s, 0) | |
| ctx.fill() | |
| } else { | |
| ctx.beginPath() | |
| ctx.moveTo(dist-10, 0) | |
| ctx.lineTo(dist+10, 0) | |
| ctx.stroke() | |
| ctx.beginPath() | |
| ctx.arc(dist, 0, 2, 0, Math.PI*2) | |
| ctx.fillStyle = '#fff' | |
| ctx.fill() | |
| } | |
| // 连线到下一层 | |
| if (l > 0 && rng() > 0.5) { | |
| ctx.beginPath() | |
| ctx.moveTo(dist, 0) | |
| // 简单的装饰线,不一定连到真正的上一层点,而是视觉上的连线 | |
| ctx.lineTo(dist * 0.8, 5) | |
| ctx.strokeStyle = `rgba(255,255,255,0.3)` | |
| ctx.stroke() | |
| } | |
| ctx.restore() | |
| } | |
| } | |
| ctx.setTransform(1, 0, 0, 1, 0, 0) // reset transform | |
| } | |
| createApp({ | |
| setup() { | |
| const state = reactive({ | |
| algo: 'flow', | |
| width: 1024, | |
| height: 768, | |
| seed: Math.floor(Math.random() * 1e9), | |
| seedInput: '', | |
| palette: 'Default', | |
| }) | |
| const toast = reactive({ | |
| show: false, | |
| message: '' | |
| }) | |
| const currentAlgoLabel = computed(() => { | |
| const a = algos.find(x => x.value === state.algo) | |
| return a ? a.label : '' | |
| }) | |
| function getRng() { | |
| return mulberry32(state.seed >>> 0) | |
| } | |
| function render() { | |
| const canvas = document.getElementById('canvas') | |
| const ctx = canvas.getContext('2d') | |
| const rng = getRng() | |
| const palette = palettes[state.palette] || palettes.Default | |
| const algo = algos.find(a => a.value === state.algo) || algos[0] | |
| algo.draw(ctx, state.width, state.height, rng, palette) | |
| } | |
| function resize() { | |
| const canvas = document.getElementById('canvas') | |
| canvas.width = state.width | |
| canvas.height = state.height | |
| render() | |
| } | |
| function randomSeed() { | |
| state.seed = Math.floor(Math.random() * 1e9) | |
| state.seedInput = String(state.seed) | |
| render() | |
| } | |
| function applySeed() { | |
| const s = state.seedInput?.trim() | |
| if (!s) { | |
| randomSeed() | |
| return | |
| } | |
| const maybeNum = Number(s) | |
| state.seed = Number.isFinite(maybeNum) ? Math.floor(maybeNum) : strToSeed(s) | |
| render() | |
| } | |
| function exportPNG() { | |
| const canvas = document.getElementById('canvas') | |
| const link = document.createElement('a') | |
| const algoName = currentAlgoLabel.value.replace(/\s+/g, '') | |
| link.download = `GenArt_${algoName}_seed${state.seed}_${state.width}x${state.height}.png` | |
| link.href = canvas.toDataURL('image/png') | |
| link.click() | |
| } | |
| function showToast(msg) { | |
| toast.message = msg | |
| toast.show = true | |
| setTimeout(() => { | |
| toast.show = false | |
| }, 2000) | |
| } | |
| function copySeed() { | |
| if (!state.seedInput) return | |
| navigator.clipboard.writeText(state.seedInput).then(() => { | |
| showToast('种子已复制') | |
| }).catch(() => { | |
| showToast('复制失败') | |
| }) | |
| } | |
| onMounted(() => { | |
| state.seedInput = String(state.seed) | |
| render() | |
| }) | |
| return { | |
| state, | |
| toast, | |
| palettes, | |
| algos, | |
| currentAlgoLabel, | |
| render, | |
| resize, | |
| randomSeed, | |
| applySeed, | |
| exportPNG, | |
| copySeed | |
| } | |
| } | |
| }).mount('#app') | |
| })() | |