TraeBot
Merge branch 'main' of hf.co:spaces/duqing26/generative-art-lab
66284da
;(() => {
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')
})()