import { defaultConfig } from '../config'; import { reactive } from 'vue'; const loadedImages = new Map(); export const state = reactive({ bgImageUrl: null, squareImageUrl: null, bgColor: '#ffffff', textColor: '#eeeeee', watermarkColor: '#dddddd', iconColor: '#eeeeee', rotation: 0, shadowColor: '#646464', shadowBlur: 120, shadowOffsetX: 1, shadowOffsetY: 1, shadowStrength: 60, watermark: defaultConfig.watermark, textSize: 200, lineHeight: 1, text3D: 0, squareSize: 300, text: defaultConfig.text, bgBlur: 3, iconBgSize: 0, selectedFont: "默认全局", isFontMenuOpen: false, hasMultipleLines: false, canvasWidth: 1200, canvasHeight: 675 }); export let canvas = null; export let ctx = null; const PREVIEW_WIDTH = 1000; const PREVIEW_HEIGHT = 500; const createCanvas = (width, height) => { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return { canvas, ctx: canvas.getContext('2d') }; }; export const { canvas: bgCanvas, ctx: bgCtx } = createCanvas(PREVIEW_WIDTH, PREVIEW_HEIGHT); export const { canvas: textCanvas, ctx: textCtx } = createCanvas(PREVIEW_WIDTH, PREVIEW_HEIGHT); export const { canvas: watermarkCanvas, ctx: watermarkCtx } = createCanvas(PREVIEW_WIDTH, PREVIEW_HEIGHT); export const { canvas: squareCanvas, ctx: squareCtx } = createCanvas(PREVIEW_WIDTH, PREVIEW_HEIGHT); // --- 统一绘图逻辑 --- // 获取字体 function getFont(scale) { const htmlElement = document.documentElement; const computedStyle = getComputedStyle(htmlElement); const globalFont = computedStyle.fontFamily; const font = (state.selectedFont === "默认全局" || !state.selectedFont) ? globalFont : state.selectedFont; return font; } // 1. 绘制背景层 function renderBackground(tCtx, w, h) { const scale = w / PREVIEW_WIDTH; tCtx.clearRect(0, 0, w, h); if (state.bgImageUrl) { // 同步绘制已加载的图,或由外部传入 img 对象 const img = loadedImages.get('bg'); if (img) { const s = Math.max(w / img.width, h / img.height); const dw = img.width * s; const dh = img.height * s; tCtx.save(); tCtx.filter = `blur(${state.bgBlur * scale}px)`; tCtx.drawImage(img, (w - dw) / 2, (h - dh) / 2, dw, dh); tCtx.restore(); } } else { tCtx.fillStyle = state.bgColor; tCtx.fillRect(0, 0, w, h); } } // 2. 绘制文本层 function renderText(tCtx, w, h) { const scale = w / PREVIEW_WIDTH; tCtx.clearRect(0, 0, w, h); const fontSize = state.textSize * scale; tCtx.font = `600 ${fontSize}px ${getFont(scale)}`; tCtx.fillStyle = state.textColor; tCtx.textAlign = 'center'; tCtx.textBaseline = 'middle'; if (state.text3D > 0) { tCtx.shadowColor = 'rgba(0, 0, 0, .4)'; tCtx.shadowBlur = (state.text3D * 0.5) * scale; tCtx.shadowOffsetX = state.text3D * scale; tCtx.shadowOffsetY = state.text3D * scale; } const lines = state.text.split('\n'); const lineHeight = fontSize * state.lineHeight; const startY = (h - (lineHeight * lines.length)) / 2 + lineHeight / 2; lines.forEach((line, i) => { tCtx.fillText(line, w / 2, startY + i * lineHeight); }); } // 3. 绘制图标层 function renderSquare(tCtx, w, h) { const scale = w / PREVIEW_WIDTH; tCtx.clearRect(0, 0, w, h); const img = loadedImages.get('square'); if (img) { const totalSize = state.squareSize * scale; const borderWidth = 20 * scale; const radius = 30 * scale; const innerSize = totalSize - 2 * borderWidth; const temp = document.createElement('canvas'); temp.width = totalSize; temp.height = totalSize; const tc = temp.getContext('2d'); if (state.iconBgSize > 0) { tc.fillStyle = state.iconColor; tc.beginPath(); tc.roundRect(borderWidth - state.iconBgSize * scale, borderWidth - state.iconBgSize * scale, innerSize + (state.iconBgSize * scale * 2), innerSize + (state.iconBgSize * scale * 2), radius); tc.fill(); } tc.save(); tc.beginPath(); tc.roundRect(borderWidth, borderWidth, innerSize, innerSize, radius); tc.clip(); const ratio = img.width / img.height; let dw = innerSize, dh = innerSize; if (ratio > 1) dh = innerSize / ratio; else dw = innerSize * ratio; tc.drawImage(img, borderWidth + (innerSize - dw) / 2, borderWidth + (innerSize - dh) / 2, dw, dh); tc.restore(); tCtx.save(); tCtx.shadowColor = state.shadowColor; tCtx.shadowBlur = state.shadowBlur * scale; tCtx.shadowOffsetX = state.shadowOffsetX * scale; tCtx.shadowOffsetY = state.shadowOffsetY * scale; tCtx.translate(w / 2, h / 2); tCtx.rotate(state.rotation * Math.PI / 180); tCtx.drawImage(temp, -totalSize / 2, -totalSize / 2, totalSize, totalSize); tCtx.restore(); } } // 4. 绘制水印层 function renderWatermark(tCtx, w, h) { const scale = w / PREVIEW_WIDTH; tCtx.clearRect(0, 0, w, h); tCtx.font = `italic ${14 * scale}px ${getFont(scale)}`; tCtx.fillStyle = state.watermarkColor; tCtx.textAlign = 'right'; tCtx.fillText(state.watermark, w - 20 * scale, h - 20 * scale); } // 组合画布 export function composeCanvases() { if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(bgCanvas, 0, 0); // 最底层:背景 ctx.drawImage(textCanvas, 0, 0); // 第 3 层:文本 ctx.drawImage(squareCanvas, 0, 0); // 第 2 层:图标 ctx.drawImage(watermarkCanvas, 0, 0); // 第 1 层:水印 } // --- 暴露给 UI 的更新函数 --- export function drawBackground() { renderBackground(bgCtx, PREVIEW_WIDTH, PREVIEW_HEIGHT); composeCanvases(); } export function drawText() { renderText(textCtx, PREVIEW_WIDTH, PREVIEW_HEIGHT); composeCanvases(); } export function drawSquareImage() { renderSquare(squareCtx, PREVIEW_WIDTH, PREVIEW_HEIGHT); composeCanvases(); } export function drawWatermark() { renderWatermark(watermarkCtx, PREVIEW_WIDTH, PREVIEW_HEIGHT); composeCanvases(); } export function updateBackgroundImage(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { loadedImages.set('bg', img); state.bgImageUrl = e.target.result; drawBackground(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } } export function updateSquareImage(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { loadedImages.set('square', img); state.squareImageUrl = e.target.result; drawSquareImage(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } } // 快捷更新 export function updatePreview(type, event) { if (type === 'bgColor') { state.bgColor = event.target.value; state.bgImageUrl = null; drawBackground(); } if (type === 'textColor') { state.textColor = event.target.value; drawText(); } if (type === 'text') { state.text = event.target.value || defaultConfig.text; drawText(); } if (type === 'textSize') { state.textSize = event.target.value; drawText(); } if (type === 'squareSize') { state.squareSize = event.target.value; drawSquareImage(); } if (type === 'bgBlur') { state.bgBlur = event.target.value; drawBackground(); } if (type === 'rotation') { state.rotation = event.target.value; drawSquareImage(); } if (type === 'watermark') { state.watermark = event.target.value; drawWatermark(); } if (type === 'font') { state.selectedFont = event.target.value; drawText(); drawWatermark(); } } // --- 核心修复:保存函数 --- export function saveWebp() { const sw = Number(state.canvasWidth) || 1200; const sh = Number(state.canvasHeight) || 675; const outCanvas = document.createElement('canvas'); outCanvas.width = sw; outCanvas.height = sh; const oCtx = outCanvas.getContext('2d'); // 按照 物理层级 顺序直接画到输出画布 renderBackground(oCtx, sw, sh); // 底层 renderText(oCtx, sw, sh); // 3层 renderSquare(oCtx, sw, sh); // 2层 renderWatermark(oCtx, sw, sh); // 1层 const link = document.createElement('a'); link.href = outCanvas.toDataURL('image/webp', 0.9); link.download = `Ruom-${sw}x${sh}.webp`; link.click(); } export function initialize() { canvas = document.getElementById('canvasPreview'); if (canvas) { ctx = canvas.getContext('2d'); drawBackground(); drawText(); drawSquareImage(); drawWatermark(); } }