import React, { useState, useRef, useEffect } from 'react'; import styled from 'styled-components'; import { useTranslation } from '../js/i18n'; import { usePageLoading } from '../hooks/usePageLoading'; import LoadingOverlay from './LoadingOverlay'; import SEO from './SEO'; const Container = styled.div` min-height: 100vh; background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); padding: 4rem 2rem 2rem; position: relative; &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px), linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px); background-size: 20px 20px; pointer-events: none; } `; const ContentWrapper = styled.div` max-width: 1400px; margin: 0 auto; position: relative; z-index: 1; `; const Title = styled.h2` font-size: 1.8rem; margin-bottom: 1.5rem; background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; letter-spacing: -0.02em; text-align: center; `; const MainContent = styled.div` display: flex; gap: 2rem; @media (max-width: 768px) { flex-direction: column; } `; const SettingsPanel = styled.div` flex: 1; background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); border-radius: 16px; padding: 1.5rem; box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); `; const PreviewPanel = styled.div` flex: 1; text-align: center; `; const Button = styled.button` background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s ease; &:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2); } `; const DeleteButton = styled(Button)` background: #ef4444; padding: 0.4rem 0.8rem; font-size: 0.9rem; `; const SubtitleInput = styled.div` display: flex; gap: 0.5rem; margin-bottom: 1rem; align-items: center; input[type="text"] { flex: 1; padding: 0.5rem; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 0.9rem; } `; const NumberInput = styled.input` width: 80px; padding: 0.5rem; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 0.9rem; `; const Select = styled.select` width: 100%; padding: 0.5rem; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 0.9rem; margin-bottom: 1rem; `; const PreviewImage = styled.img` max-width: 100%; border-radius: 8px; margin-bottom: 1rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); `; const UploadButton = styled.div` border: 2px dashed rgba(99, 102, 241, 0.2); border-radius: 12px; padding: 2rem; text-align: center; cursor: pointer; transition: all 0.3s ease; background: rgba(255, 255, 255, 0.5); &:hover { border-color: rgba(99, 102, 241, 0.4); background: rgba(99, 102, 241, 0.05); } `; const HiddenFileInput = styled.input` display: none; `; const SettingGroup = styled.div` margin-bottom: 1.5rem; label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: #374151; } `; const RangeInput = styled.input` width: 100%; height: 6px; background: #e5e7eb; border-radius: 3px; outline: none; -webkit-appearance: none; margin: 10px 0; &::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: #6366F1; border-radius: 50%; cursor: pointer; transition: all 0.2s ease; &:hover { transform: scale(1.1); background: #4F46E5; } } &::-moz-range-thumb { width: 18px; height: 18px; background: #6366F1; border-radius: 50%; cursor: pointer; border: none; transition: all 0.2s ease; &:hover { transform: scale(1.1); background: #4F46E5; } } &::-ms-thumb { width: 18px; height: 18px; background: #6366F1; border-radius: 50%; cursor: pointer; border: none; transition: all 0.2s ease; &:hover { transform: scale(1.1); background: #4F46E5; } } `; const ColorPickerContainer = styled.div` display: flex; gap: 1rem; align-items: center; `; const ColorInput = styled.input` -webkit-appearance: none; width: 48px; height: 48px; border: none; border-radius: 10px; cursor: pointer; padding: 0; background: none; &::-webkit-color-swatch-wrapper { padding: 0; } &::-webkit-color-swatch { border: 2px solid #e5e7eb; border-radius: 8px; } `; const ColorPresets = styled.div` display: flex; gap: 0.5rem; align-items: center; `; const ColorPresetButton = styled.button` width: 32px; height: 32px; border: 2px solid #e5e7eb; border-radius: 8px; background-color: ${props => props.$color}; cursor: pointer; transition: all 0.2s ease; &:hover { transform: scale(1.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } `; const ControlPanel = styled.div` flex: 1; background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); border-radius: 16px; padding: 1.5rem; box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); `; const SubtitleMaker = () => { const { t } = useTranslation(); const isLoading = usePageLoading(); const [imageSrc, setImageSrc] = useState(null); const [subtitles, setSubtitles] = useState([{ text: '' }]); const canvasRef = useRef(null); const [finalImage, setFinalImage] = useState(null); const [globalSettings, setGlobalSettings] = useState({ fontSize: 48, lineHeight: 100, strokeWidth: 2, textColor: '#FFE135', strokeColor: '#000000' }); const fileInputRef = useRef(null); // 预设的字幕颜色选项 - 只保留最常用的几个 const presetColors = [ { name: t('tools.subtitleGenerator.presetColors.classicYellow'), value: '#FFE135' }, { name: t('tools.subtitleGenerator.presetColors.pureWhite'), value: '#FFFFFF' }, { name: t('tools.subtitleGenerator.presetColors.vividOrange'), value: '#FFA500' }, { name: t('tools.subtitleGenerator.presetColors.neonGreen'), value: '#ADFF2F' }, { name: t('tools.subtitleGenerator.presetColors.lightBlue'), value: '#00FFFF' }, { name: t('tools.subtitleGenerator.presetColors.brightPink'), value: '#FF69B4' }, ]; // 处理图片上传 const handleImageUpload = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = () => { setImageSrc(reader.result); } reader.readAsDataURL(file); } } // 更新字幕内容 const updateSubtitle = (index, text) => { const newSubtitles = [...subtitles]; newSubtitles[index] = { text }; setSubtitles(newSubtitles); }; // 删除字幕行 const removeSubtitleLine = (index) => { setSubtitles(subtitles.filter((_, i) => i !== index)); }; // 增加字幕行 const addSubtitleLine = () => { setSubtitles([...subtitles, { text: '' }]); }; // 绘制字幕到canvas useEffect(() => { if (imageSrc) { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); const image = new Image(); image.src = imageSrc; image.onload = () => { // 计算总高度 const totalHeight = image.height + subtitles.slice(1).length * (globalSettings.lineHeight + 1); canvas.width = image.width; canvas.height = totalHeight; // 绘制图片 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0); // 获取第一行字幕区域的背景图像数据 const subtitleBackgroundData = ctx.getImageData( 0, image.height - globalSettings.lineHeight, canvas.width, globalSettings.lineHeight ); // 设置字体和对齐方式 ctx.textAlign = 'center'; ctx.font = `${globalSettings.fontSize}px Arial`; ctx.lineWidth = globalSettings.strokeWidth; // 绘制第一行字幕 let yPosition = image.height - globalSettings.lineHeight; ctx.strokeStyle = globalSettings.strokeColor; ctx.fillStyle = globalSettings.textColor; const firstTextY = yPosition + globalSettings.lineHeight / 2 + globalSettings.fontSize / 3; ctx.strokeText(subtitles[0].text, canvas.width / 2, firstTextY); ctx.fillText(subtitles[0].text, canvas.width / 2, firstTextY); // 设置 yPosition 为图片底部,准备绘制后续字幕 yPosition = image.height; // 绘制后续字幕行 subtitles.slice(1).forEach((subtitle) => { // 绘制分隔线 ctx.fillStyle = '#e5e7eb'; ctx.fillRect(0, yPosition, canvas.width, 1); yPosition += 0; // 绘制背景(使用第一行字幕的背景) ctx.putImageData(subtitleBackgroundData, 0, yPosition); // 绘制字幕文字 ctx.strokeStyle = globalSettings.strokeColor; ctx.fillStyle = globalSettings.textColor; const textY = yPosition + globalSettings.lineHeight / 2 + globalSettings.fontSize / 3; ctx.strokeText(subtitle.text, canvas.width / 2, textY); ctx.fillText(subtitle.text, canvas.width / 2, textY); yPosition += globalSettings.lineHeight; }); setFinalImage(canvas.toDataURL('image/png')); }; } }, [imageSrc, subtitles, globalSettings]); // 下载最终的图片 const downloadImage = () => { if (finalImage) { const link = document.createElement('a'); link.href = finalImage; link.download = 'subtitle-image.png'; link.click(); } } return ( <> {isLoading && } {t('tools.subtitleGenerator.title')} {/* Left Panel */}

{t('tools.subtitleGenerator.uploadImage')}

fileInputRef.current.click()}> {t('tools.subtitleGenerator.dropOrClick')} {imageSrc && ( <>

{t('tools.subtitleGenerator.globalSettings')}

setGlobalSettings({ ...globalSettings, textColor: e.target.value })} /> {presetColors.map((color) => ( setGlobalSettings({ ...globalSettings, textColor: color.value })} title={color.name} /> ))} setGlobalSettings({ ...globalSettings, fontSize: parseInt(e.target.value) })} min="48" max="120" /> setGlobalSettings({ ...globalSettings, lineHeight: parseInt(e.target.value) })} min="60" max="160" />

{t('tools.subtitleGenerator.subtitleSettings')}

{subtitles.map((subtitle, index) => ( updateSubtitle(index, e.target.value)} placeholder={`字幕 ${index + 1}`} /> {index > 0 && ( removeSubtitleLine(index)}> 删除 )} ))} )}
{/* Right Panel */}

{t('tools.subtitleGenerator.preview')}

{finalImage && ( <> )}
); }; export default SubtitleMaker;