580 lines
17 KiB
JavaScript
580 lines
17 KiB
JavaScript
/**
|
||
* 镭射卡批量预览 — 合成逻辑对齐 laser-card-studio 导出链(人物清晰 + 强镭射叠层)
|
||
*/
|
||
|
||
const EXPORT_W = 450
|
||
const EXPORT_H = 600
|
||
|
||
const PRESET_VARIANTS = [
|
||
{
|
||
style: 'dream',
|
||
angle: 36,
|
||
beam: 'prism',
|
||
backdrop: 'liquidBlue',
|
||
laserStrength: 78,
|
||
diffractionDensity: 68,
|
||
marbleFlow: 52,
|
||
beamIntensity: 92,
|
||
grainDensity: 82,
|
||
},
|
||
{
|
||
style: 'classic',
|
||
angle: 24,
|
||
beam: 'aurora',
|
||
backdrop: 'liquidLavender',
|
||
laserStrength: 85,
|
||
diffractionDensity: 74,
|
||
marbleFlow: 38,
|
||
beamIntensity: 96,
|
||
grainDensity: 76,
|
||
},
|
||
{
|
||
style: 'holoFull',
|
||
angle: 58,
|
||
beam: 'neon',
|
||
backdrop: 'liquidPearl',
|
||
laserStrength: 90,
|
||
diffractionDensity: 80,
|
||
marbleFlow: 58,
|
||
beamIntensity: 100,
|
||
grainDensity: 88,
|
||
},
|
||
{
|
||
style: 'ice',
|
||
angle: 46,
|
||
beam: 'white',
|
||
backdrop: 'liquidBlue',
|
||
laserStrength: 72,
|
||
diffractionDensity: 60,
|
||
marbleFlow: 44,
|
||
beamIntensity: 88,
|
||
grainDensity: 74,
|
||
},
|
||
{
|
||
style: 'sunset',
|
||
angle: 30,
|
||
beam: 'sunset',
|
||
backdrop: 'liquidLavender',
|
||
laserStrength: 82,
|
||
diffractionDensity: 66,
|
||
marbleFlow: 50,
|
||
beamIntensity: 94,
|
||
grainDensity: 84,
|
||
},
|
||
]
|
||
|
||
const BACKDROP_MAP = {
|
||
liquidBlue: '/static/castlove/laser-bg/laser-bg-1.png',
|
||
liquidLavender: '/static/castlove/laser-bg/laser-bg-2.png',
|
||
liquidPearl: '/static/castlove/laser-bg/laser-bg-3.png',
|
||
}
|
||
|
||
const BLOB_PALETTES = {
|
||
dream: [
|
||
{ x: 0.1, y: 0.14, c0: 'rgba(186,104,255,0.55)', c1: 'rgba(186,104,255,0)' },
|
||
{ x: 0.9, y: 0.18, c0: 'rgba(255,105,180,0.48)', c1: 'rgba(255,105,180,0)' },
|
||
{ x: 0.14, y: 0.86, c0: 'rgba(0,229,255,0.42)', c1: 'rgba(0,229,255,0)' },
|
||
{ x: 0.82, y: 0.78, c0: 'rgba(255,224,140,0.38)', c1: 'rgba(255,224,140,0)' },
|
||
],
|
||
classic: [
|
||
{ x: 0.48, y: 0.1, c0: 'rgba(255,80,200,0.48)', c1: 'rgba(255,80,200,0)' },
|
||
{ x: 0.08, y: 0.58, c0: 'rgba(0,200,255,0.46)', c1: 'rgba(0,200,255,0)' },
|
||
{ x: 0.92, y: 0.76, c0: 'rgba(180,255,120,0.4)', c1: 'rgba(180,255,120,0)' },
|
||
],
|
||
ice: [
|
||
{ x: 0.18, y: 0.22, c0: 'rgba(100,180,255,0.52)', c1: 'rgba(100,180,255,0)' },
|
||
{ x: 0.88, y: 0.32, c0: 'rgba(160,140,255,0.44)', c1: 'rgba(160,140,255,0)' },
|
||
{ x: 0.5, y: 0.9, c0: 'rgba(200,240,255,0.4)', c1: 'rgba(200,240,255,0)' },
|
||
],
|
||
sunset: [
|
||
{ x: 0.12, y: 0.28, c0: 'rgba(255,150,120,0.5)', c1: 'rgba(255,150,120,0)' },
|
||
{ x: 0.9, y: 0.24, c0: 'rgba(255,200,140,0.46)', c1: 'rgba(255,200,140,0)' },
|
||
{ x: 0.52, y: 0.88, c0: 'rgba(255,100,160,0.42)', c1: 'rgba(255,100,160,0)' },
|
||
],
|
||
holoFull: [
|
||
{ x: 0.12, y: 0.16, c0: 'rgba(255,80,180,0.5)', c1: 'rgba(255,80,180,0)' },
|
||
{ x: 0.9, y: 0.22, c0: 'rgba(255,180,80,0.46)', c1: 'rgba(255,180,80,0)' },
|
||
{ x: 0.1, y: 0.78, c0: 'rgba(0,220,255,0.48)', c1: 'rgba(0,220,255,0)' },
|
||
{ x: 0.88, y: 0.82, c0: 'rgba(160,100,255,0.5)', c1: 'rgba(160,100,255,0)' },
|
||
{ x: 0.5, y: 0.5, c0: 'rgba(255,255,255,0.2)', c1: 'rgba(255,255,255,0)' },
|
||
],
|
||
}
|
||
|
||
const MARBLE_PALETTES = {
|
||
dream: [
|
||
{ x: 0.16, y: 0.26, rgb: [200, 170, 255] },
|
||
{ x: 0.84, y: 0.22, rgb: [255, 150, 210] },
|
||
{ x: 0.48, y: 0.88, rgb: [120, 210, 255] },
|
||
],
|
||
classic: [
|
||
{ x: 0.14, y: 0.2, rgb: [255, 100, 200] },
|
||
{ x: 0.86, y: 0.26, rgb: [200, 100, 255] },
|
||
{ x: 0.5, y: 0.86, rgb: [80, 220, 255] },
|
||
],
|
||
ice: [
|
||
{ x: 0.18, y: 0.24, rgb: [170, 210, 255] },
|
||
{ x: 0.82, y: 0.2, rgb: [110, 160, 255] },
|
||
{ x: 0.5, y: 0.88, rgb: [210, 235, 255] },
|
||
],
|
||
sunset: [
|
||
{ x: 0.14, y: 0.28, rgb: [255, 190, 160] },
|
||
{ x: 0.86, y: 0.24, rgb: [255, 130, 150] },
|
||
{ x: 0.5, y: 0.88, rgb: [255, 220, 180] },
|
||
],
|
||
holoFull: [
|
||
{ x: 0.12, y: 0.18, rgb: [255, 100, 200] },
|
||
{ x: 0.88, y: 0.2, rgb: [255, 180, 100] },
|
||
{ x: 0.1, y: 0.78, rgb: [80, 230, 255] },
|
||
{ x: 0.88, y: 0.8, rgb: [160, 100, 255] },
|
||
],
|
||
}
|
||
|
||
const backdropLayoutCache = {}
|
||
|
||
function getImageInfo(src) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.getImageInfo({
|
||
src,
|
||
success: resolve,
|
||
fail: (e) => reject(e || new Error('读取图片失败')),
|
||
})
|
||
})
|
||
}
|
||
|
||
function computeCover(iw, ih, cw, ch) {
|
||
const w = Number(iw) || 1
|
||
const h = Number(ih) || 1
|
||
const scale = Math.max(cw / w, ch / h)
|
||
const dw = w * scale
|
||
const dh = h * scale
|
||
return { dx: (cw - dw) / 2, dy: (ch - dh) / 2, dw, dh }
|
||
}
|
||
|
||
async function getBackdropLayout(key, cw, ch) {
|
||
const cacheKey = `${key}_${cw}_${ch}`
|
||
if (backdropLayoutCache[cacheKey]) {
|
||
return backdropLayoutCache[cacheKey]
|
||
}
|
||
const bd = BACKDROP_MAP[key] || BACKDROP_MAP.liquidBlue
|
||
const info = await getImageInfo(bd)
|
||
const lay = computeCover(info.width, info.height, cw, ch)
|
||
backdropLayoutCache[cacheKey] = { path: bd, lay }
|
||
return backdropLayoutCache[cacheKey]
|
||
}
|
||
|
||
function canvasDraw(ctx) {
|
||
return new Promise((resolve) => {
|
||
ctx.draw(false, () => setTimeout(resolve, 120))
|
||
})
|
||
}
|
||
|
||
function canvasToTemp(canvasId, w, h) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.canvasToTempFilePath({
|
||
canvasId,
|
||
width: w,
|
||
height: h,
|
||
destWidth: w * 2,
|
||
destHeight: h * 2,
|
||
fileType: 'jpg',
|
||
quality: 0.96,
|
||
success: (res) => resolve(res.tempFilePath),
|
||
fail: (e) => reject(e || new Error('导出失败')),
|
||
})
|
||
})
|
||
}
|
||
|
||
function clipRoundRect(ctx, cw, ch) {
|
||
const r = Math.min(32, cw * 0.065, ch * 0.048)
|
||
ctx.beginPath()
|
||
ctx.moveTo(r, 0)
|
||
ctx.lineTo(cw - r, 0)
|
||
ctx.arc(cw - r, r, r, -Math.PI / 2, 0, false)
|
||
ctx.lineTo(cw, ch - r)
|
||
ctx.arc(cw - r, ch - r, r, 0, Math.PI / 2, false)
|
||
ctx.lineTo(r, ch)
|
||
ctx.arc(r, ch - r, r, Math.PI / 2, Math.PI, false)
|
||
ctx.lineTo(0, r)
|
||
ctx.arc(r, r, r, Math.PI, Math.PI * 1.5, false)
|
||
ctx.closePath()
|
||
ctx.clip()
|
||
}
|
||
|
||
function setBlend(ctx, mode) {
|
||
if (typeof ctx.setGlobalCompositeOperation === 'function') {
|
||
ctx.setGlobalCompositeOperation(mode)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* uni-app 旧版 canvas(App/小程序)无 createRadialGradient,需兼容 createCircularGradient 或线性渐变
|
||
*/
|
||
function createRadialGradientCompat(ctx, cx, cy, innerR, outerR) {
|
||
const ox = Number(cx) || 0
|
||
const oy = Number(cy) || 0
|
||
const r0 = Math.max(0, Number(innerR) || 0)
|
||
const r1 = Math.max(r0 + 0.5, Number(outerR) || 1)
|
||
if (typeof ctx.createRadialGradient === 'function') {
|
||
return ctx.createRadialGradient(ox, oy, r0, ox, oy, r1)
|
||
}
|
||
if (typeof ctx.createCircularGradient === 'function') {
|
||
return ctx.createCircularGradient(ox, oy, r1)
|
||
}
|
||
return ctx.createLinearGradient(ox - r1, oy - r1, ox + r1, oy + r1)
|
||
}
|
||
|
||
function hslToRgba(h, s, l, a) {
|
||
const hh = ((Number(h) % 360) + 360) % 360
|
||
const ss = Math.max(0, Math.min(100, s)) / 100
|
||
const ll = Math.max(0, Math.min(100, l)) / 100
|
||
const c = (1 - Math.abs(2 * ll - 1)) * ss
|
||
const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
|
||
const m = ll - c / 2
|
||
let rp = 0
|
||
let gp = 0
|
||
let bp = 0
|
||
if (hh < 60) {
|
||
rp = c
|
||
gp = x
|
||
} else if (hh < 120) {
|
||
rp = x
|
||
gp = c
|
||
} else if (hh < 180) {
|
||
gp = c
|
||
bp = x
|
||
} else if (hh < 240) {
|
||
gp = x
|
||
bp = c
|
||
} else if (hh < 300) {
|
||
rp = x
|
||
bp = c
|
||
} else {
|
||
rp = c
|
||
bp = x
|
||
}
|
||
const r = Math.round(255 * (rp + m))
|
||
const g = Math.round(255 * (gp + m))
|
||
const b = Math.round(255 * (bp + m))
|
||
const aa = Math.max(0, Math.min(1, a))
|
||
return `rgba(${r},${g},${b},${aa.toFixed(3)})`
|
||
}
|
||
|
||
function linearGradientThroughPivot(ctx, cw, ch, pivotPctX, pivotPctY, deg, stops) {
|
||
const rad = (deg * Math.PI) / 180
|
||
const len = Math.sqrt(cw * cw + ch * ch) / 2
|
||
const px = (Math.max(0, Math.min(100, pivotPctX)) / 100) * cw
|
||
const py = (Math.max(0, Math.min(100, pivotPctY)) / 100) * ch
|
||
const x0 = px - Math.cos(rad) * len
|
||
const y0 = py - Math.sin(rad) * len
|
||
const x1 = px + Math.cos(rad) * len
|
||
const y1 = py + Math.sin(rad) * len
|
||
const g = ctx.createLinearGradient(x0, y0, x1, y1)
|
||
stops.forEach(([t, c]) => g.addColorStop(t, c))
|
||
return g
|
||
}
|
||
|
||
function drawSolidLaserBackdrop(ctx, cw, ch, preset) {
|
||
const strength = preset.laserStrength / 100
|
||
const vivid = 1.1
|
||
const list = BLOB_PALETTES[preset.style] || BLOB_PALETTES.dream
|
||
const R = Math.max(cw, ch)
|
||
setBlend(ctx, 'soft-light')
|
||
list.forEach((b) => {
|
||
try {
|
||
const bx = b.x * cw
|
||
const by = b.y * ch
|
||
const br = R * 0.62
|
||
const g = createRadialGradientCompat(ctx, bx, by, 0, br)
|
||
g.addColorStop(0, b.c0)
|
||
g.addColorStop(1, b.c1)
|
||
ctx.setGlobalAlpha(0.82 * strength * vivid * 0.42)
|
||
ctx.setFillStyle(g)
|
||
ctx.fillRect(0, 0, cw, ch)
|
||
} catch (e) {
|
||
ctx.setGlobalAlpha(0.35 * strength)
|
||
ctx.setFillStyle(b.c0)
|
||
ctx.fillRect(0, 0, cw, ch)
|
||
}
|
||
})
|
||
ctx.setGlobalAlpha(1)
|
||
setBlend(ctx, 'source-over')
|
||
}
|
||
|
||
function drawHslRainbowBase(ctx, cw, ch, preset, timeMs) {
|
||
const strength = preset.laserStrength / 100
|
||
const vivid = 1.1
|
||
const hueShift = ((timeMs * 0.055) % 360 + 360) % 360
|
||
const g = ctx.createLinearGradient(0, 0, cw * 1.05, ch * 0.92)
|
||
const stops = 12
|
||
for (let i = 0; i <= stops; i++) {
|
||
const p = i / stops
|
||
const h = (hueShift + p * 360) % 360
|
||
const alpha = (0.052 + strength * 0.1) * vivid * (0.55 + p * 0.22) * 0.65
|
||
g.addColorStop(p, hslToRgba(h, 88, 54, alpha))
|
||
}
|
||
setBlend(ctx, 'soft-light')
|
||
ctx.setGlobalAlpha(1)
|
||
ctx.setFillStyle(g)
|
||
ctx.fillRect(0, 0, cw, ch)
|
||
setBlend(ctx, 'source-over')
|
||
}
|
||
|
||
function drawMarbleFlow(ctx, cw, ch, preset) {
|
||
const mf = preset.marbleFlow / 100
|
||
if (mf < 0.04) return
|
||
const strength = preset.laserStrength / 100
|
||
const pts = MARBLE_PALETTES[preset.style] || MARBLE_PALETTES.dream
|
||
const phase = (preset.angle * Math.PI) / 180
|
||
const Rmax = Math.max(cw, ch) * 0.48
|
||
const baseA = (0.1 + mf * 0.38) * strength * 0.85
|
||
ctx.save()
|
||
setBlend(ctx, 'soft-light')
|
||
pts.forEach((p, i) => {
|
||
const gx = p.x * cw + Math.cos(phase * 0.35) * 10 * (i % 2)
|
||
const gy = p.y * ch + Math.sin(phase * 0.3) * 8 * ((i + 1) % 2)
|
||
const rad = Rmax * (0.28 + (i % 3) * 0.08)
|
||
const g = createRadialGradientCompat(ctx, gx, gy, 0, rad)
|
||
const [r, gg, b] = p.rgb
|
||
const a0 = Math.min(0.75, baseA * 0.95)
|
||
g.addColorStop(0, `rgba(${r},${gg},${b},${a0.toFixed(3)})`)
|
||
g.addColorStop(0.55, `rgba(${r},${gg},${b},${(baseA * 0.5).toFixed(3)})`)
|
||
g.addColorStop(1, `rgba(${r},${gg},${b},0)`)
|
||
ctx.setGlobalAlpha(1)
|
||
ctx.setFillStyle(g)
|
||
ctx.fillRect(0, 0, cw, ch)
|
||
})
|
||
ctx.restore()
|
||
setBlend(ctx, 'source-over')
|
||
}
|
||
|
||
function drawDiffractionStripes(ctx, cw, ch, preset) {
|
||
const density = preset.diffractionDensity / 100
|
||
if (density < 0.02) return
|
||
const strength = preset.laserStrength / 100
|
||
const deg = preset.angle + 90
|
||
const rad = (deg * Math.PI) / 180
|
||
const stripeW = Math.max(1.1, (3.2 - density * 2.4) * 0.42)
|
||
const span = Math.sqrt(cw * cw + ch * ch) * 1.2
|
||
const baseA = (0.08 + strength * 0.12) * 0.75
|
||
const hiA = (0.1 + strength * 0.14) * 0.75
|
||
const prefix = [
|
||
'rgba(255,60,180,',
|
||
'rgba(255,170,60,',
|
||
'rgba(180,255,80,',
|
||
'rgba(0,220,255,',
|
||
'rgba(120,90,255,',
|
||
'rgba(255,80,200,',
|
||
]
|
||
const alphas = [baseA, baseA, hiA, hiA, baseA, baseA * 0.85]
|
||
ctx.save()
|
||
ctx.translate(cw / 2, ch / 2)
|
||
ctx.rotate(rad)
|
||
setBlend(ctx, 'overlay')
|
||
ctx.setGlobalAlpha(Math.min(0.42, (0.28 + strength * 0.55 + density * 0.2) * 0.52))
|
||
const count = Math.ceil((span * 2) / stripeW)
|
||
for (let i = -count; i <= count; i++) {
|
||
const pos = i * stripeW
|
||
const ci = ((i % 6) + 6) % 6
|
||
ctx.setFillStyle(prefix[ci] + alphas[ci].toFixed(3) + ')')
|
||
ctx.fillRect(pos - span, -span, stripeW * 0.92, span * 2)
|
||
}
|
||
ctx.restore()
|
||
ctx.setGlobalAlpha(1)
|
||
setBlend(ctx, 'source-over')
|
||
}
|
||
|
||
function drawOilSweep(ctx, cw, ch, preset, timeMs) {
|
||
const strength = preset.laserStrength / 100
|
||
const bi = preset.beamIntensity / 100
|
||
const pvx = 0.5 * cw
|
||
const pvy = 0.48 * ch
|
||
const deg = preset.angle
|
||
const rad = (deg * Math.PI) / 180
|
||
const perp = rad + Math.PI / 2
|
||
const len = Math.sqrt(cw * cw + ch * ch) * 0.55
|
||
const phase = ((timeMs % 8000) / 8000 - 0.5) * len * 0.55 * 2
|
||
const cx = pvx + Math.cos(perp) * phase
|
||
const cy = pvy + Math.sin(perp) * phase
|
||
const x0 = cx - Math.cos(rad) * len
|
||
const y0 = cy - Math.sin(rad) * len
|
||
const x1 = cx + Math.cos(rad) * len
|
||
const y1 = cy + Math.sin(rad) * len
|
||
const g = ctx.createLinearGradient(x0, y0, x1, y1)
|
||
const peak = (0.2 + strength * 0.16 + bi * 0.12) * 1.1
|
||
const be = preset.beam
|
||
let cHi = `rgba(255,255,255,${Math.min(0.9, peak).toFixed(3)})`
|
||
let cMid = `rgba(220,240,255,${Math.min(0.6, peak * 0.72).toFixed(3)})`
|
||
if (be === 'prism') {
|
||
cHi = `rgba(255,240,200,${Math.min(0.88, peak).toFixed(3)})`
|
||
cMid = `rgba(200,80,255,${Math.min(0.55, peak * 0.65).toFixed(3)})`
|
||
} else if (be === 'aurora') {
|
||
cHi = `rgba(200,255,250,${Math.min(0.85, peak).toFixed(3)})`
|
||
cMid = `rgba(140,80,255,${Math.min(0.52, peak * 0.62).toFixed(3)})`
|
||
} else if (be === 'sunset') {
|
||
cHi = `rgba(255,230,180,${Math.min(0.88, peak).toFixed(3)})`
|
||
cMid = `rgba(255,120,120,${Math.min(0.54, peak * 0.64).toFixed(3)})`
|
||
} else if (be === 'neon') {
|
||
cHi = `rgba(0,255,255,${Math.min(0.82, peak).toFixed(3)})`
|
||
cMid = `rgba(255,80,200,${Math.min(0.52, peak * 0.62).toFixed(3)})`
|
||
}
|
||
g.addColorStop(0, 'rgba(255,255,255,0)')
|
||
g.addColorStop(0.38, 'rgba(255,255,255,0)')
|
||
g.addColorStop(0.48, cMid)
|
||
g.addColorStop(0.5, cHi)
|
||
g.addColorStop(0.52, cMid)
|
||
g.addColorStop(0.62, 'rgba(255,255,255,0)')
|
||
g.addColorStop(1, 'rgba(255,255,255,0)')
|
||
setBlend(ctx, 'soft-light')
|
||
ctx.setGlobalAlpha((0.42 + bi * 0.32) * 0.62)
|
||
ctx.setFillStyle(g)
|
||
ctx.fillRect(0, 0, cw, ch)
|
||
ctx.setGlobalAlpha(1)
|
||
setBlend(ctx, 'source-over')
|
||
}
|
||
|
||
function drawHueFlowBeams(ctx, cw, ch, preset, timeMs) {
|
||
const strength = preset.laserStrength / 100
|
||
const flow = ((timeMs * 0.00012) % 1) * 360
|
||
const layers = [
|
||
{ off: 0, span: 260 },
|
||
{ off: 58, span: 200 },
|
||
{ off: -44, span: 220 },
|
||
]
|
||
ctx.save()
|
||
setBlend(ctx, 'soft-light')
|
||
layers.forEach((layer, idx) => {
|
||
const deg = preset.angle + layer.off + Math.sin((timeMs + idx * 900) * 0.0017) * 14
|
||
const h0 = (flow + idx * 72) % 360
|
||
const stops = []
|
||
for (let i = 0; i <= 7; i++) {
|
||
const u = i / 7
|
||
const h = (h0 + u * layer.span) % 360
|
||
const a = (0.03 + strength * 0.065) * (0.5 + 0.5 * Math.sin(u * Math.PI)) * 0.7
|
||
stops.push([u, hslToRgba(h, 90, 58, a)])
|
||
}
|
||
const g = linearGradientThroughPivot(ctx, cw, ch, 50, 48, deg, stops)
|
||
ctx.setGlobalAlpha(0.28 + idx * 0.04)
|
||
ctx.setFillStyle(g)
|
||
ctx.fillRect(0, 0, cw, ch)
|
||
})
|
||
ctx.restore()
|
||
ctx.setGlobalAlpha(1)
|
||
setBlend(ctx, 'source-over')
|
||
}
|
||
|
||
function drawDensePixelNoise(ctx, cw, ch, preset, seed) {
|
||
const strength = preset.laserStrength / 100
|
||
const grain = preset.grainDensity / 100
|
||
const n = Math.floor(1800 + grain * 3500 + strength * 1500)
|
||
let s = seed
|
||
const rnd = () => {
|
||
s = (s * 9301 + 49297) % 233280
|
||
return s / 233280
|
||
}
|
||
ctx.save()
|
||
setBlend(ctx, 'overlay')
|
||
ctx.setGlobalAlpha(0.5)
|
||
for (let k = 0; k < n; k++) {
|
||
const x = Math.floor(rnd() * cw)
|
||
const y = Math.floor(rnd() * ch)
|
||
const lit = rnd() > 0.5
|
||
const v = lit ? 255 : 198 + Math.floor(rnd() * 40)
|
||
const a = 0.028 + rnd() * 0.08
|
||
ctx.setFillStyle(`rgba(${v},${v},${Math.min(255, v + 8)},${a.toFixed(3)})`)
|
||
ctx.fillRect(x, y, 1, 1)
|
||
}
|
||
ctx.restore()
|
||
ctx.setGlobalAlpha(1)
|
||
setBlend(ctx, 'source-over')
|
||
}
|
||
|
||
function drawFilmGrain(ctx, cw, ch, seed) {
|
||
let s = seed + 17
|
||
const rnd = () => {
|
||
s = (s * 1103515245 + 12345) & 0x7fffffff
|
||
return s / 0x7fffffff
|
||
}
|
||
for (let i = 0; i < 90; i++) {
|
||
const x = rnd() * cw
|
||
const y = rnd() * ch
|
||
ctx.setGlobalAlpha(0.08 + rnd() * 0.12)
|
||
ctx.setFillStyle('#ffffff')
|
||
ctx.beginPath()
|
||
ctx.arc(x, y, rnd() * 1.5 + 0.3, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
}
|
||
ctx.setGlobalAlpha(1)
|
||
}
|
||
|
||
async function drawBackdropImage(ctx, bdPath, lay) {
|
||
try {
|
||
ctx.drawImage(bdPath, lay.dx, lay.dy, lay.dw, lay.dh)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
}
|
||
|
||
async function drawLaserVariantComposite(ctx, cw, ch, imagePath, layout, preset, variantIndex) {
|
||
const timeMs = Date.now() + variantIndex * 1379
|
||
const { path: bdPath, lay: bdLay } = await getBackdropLayout(preset.backdrop, cw, ch)
|
||
|
||
ctx.setFillStyle('#141018')
|
||
ctx.fillRect(0, 0, cw, ch)
|
||
ctx.save()
|
||
clipRoundRect(ctx, cw, ch)
|
||
|
||
await drawBackdropImage(ctx, bdPath, bdLay)
|
||
|
||
try {
|
||
ctx.drawImage(imagePath, layout.dx, layout.dy, layout.dw, layout.dh)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
|
||
ctx.save()
|
||
setBlend(ctx, 'overlay')
|
||
ctx.setGlobalAlpha(0.48)
|
||
await drawBackdropImage(ctx, bdPath, bdLay)
|
||
ctx.restore()
|
||
|
||
ctx.save()
|
||
setBlend(ctx, 'screen')
|
||
ctx.setGlobalAlpha(0.28)
|
||
await drawBackdropImage(ctx, bdPath, bdLay)
|
||
ctx.restore()
|
||
|
||
drawSolidLaserBackdrop(ctx, cw, ch, preset)
|
||
drawHslRainbowBase(ctx, cw, ch, preset, timeMs)
|
||
drawMarbleFlow(ctx, cw, ch, preset)
|
||
drawHueFlowBeams(ctx, cw, ch, preset, timeMs)
|
||
drawDiffractionStripes(ctx, cw, ch, preset)
|
||
drawOilSweep(ctx, cw, ch, preset, timeMs)
|
||
drawDensePixelNoise(ctx, cw, ch, preset, variantIndex * 7919 + 42)
|
||
drawFilmGrain(ctx, cw, ch, variantIndex * 313)
|
||
|
||
ctx.setGlobalAlpha(1)
|
||
setBlend(ctx, 'source-over')
|
||
ctx.restore()
|
||
}
|
||
|
||
export async function generateLaserVariantBatch(imagePath, canvasId = 'laserBatchCanvas') {
|
||
const src = String(imagePath || '').trim()
|
||
if (!src) {
|
||
throw new Error('缺少上传图片')
|
||
}
|
||
const info = await getImageInfo(src)
|
||
const layout = computeCover(info.width, info.height, EXPORT_W, EXPORT_H)
|
||
const ctx = uni.createCanvasContext(canvasId)
|
||
const paths = []
|
||
|
||
for (let i = 0; i < PRESET_VARIANTS.length; i++) {
|
||
await drawLaserVariantComposite(ctx, EXPORT_W, EXPORT_H, src, layout, PRESET_VARIANTS[i], i)
|
||
await canvasDraw(ctx)
|
||
paths.push(await canvasToTemp(canvasId, EXPORT_W, EXPORT_H))
|
||
}
|
||
|
||
return paths
|
||
}
|
||
|
||
export const LASER_BATCH_CANVAS_SIZE = { w: EXPORT_W, h: EXPORT_H }
|