/** * 首页场景 */ import BaseScene from './BaseScene'; export default class HomeScene extends BaseScene { constructor(main, params) { super(main, params); this.storyList = []; this.scrollY = 0; this.maxScrollY = 0; this.isDragging = false; this.lastTouchY = 0; this.scrollVelocity = 0; this.currentTab = 0; // 0: 首页, 1: 发现, 2: 我的 this.categories = ['全部', '都市言情', '悬疑推理', '古风宫廷', '校园青春', '修仙玄幻', '穿越重生', '职场商战', '科幻未来', '恐怖惊悚', '搞笑轻喜']; this.selectedCategory = 0; // 分类横向滚动 this.categoryScrollX = 0; this.maxCategoryScrollX = 0; this.isCategoryDragging = false; this.lastTouchX = 0; // 存储分类标签位置(用于点击判定) this.categoryRects = []; } async init() { // 加载故事列表 this.storyList = this.main.storyManager.storyList; this.calculateMaxScroll(); } // 获取过滤后的故事列表 getFilteredStories() { if (this.selectedCategory === 0) { return this.storyList; // 全部 } const categoryName = this.categories[this.selectedCategory]; return this.storyList.filter(s => s.category === categoryName); } calculateMaxScroll() { // 计算最大滚动距离 const stories = this.getFilteredStories(); const cardHeight = 120; const gap = 15; const startY = 150; // 故事列表起始位置 const tabHeight = 65; // 内容总高度 const contentBottom = startY + stories.length * (cardHeight + gap); // 可视区域底部(减去底部Tab栏) const visibleBottom = this.screenHeight - tabHeight; // 最大滚动距离 = 内容超出可视区域的部分 this.maxScrollY = Math.max(0, contentBottom - visibleBottom); } update() { // 滚动惯性 if (!this.isDragging && Math.abs(this.scrollVelocity) > 0.5) { this.scrollY += this.scrollVelocity; this.scrollVelocity *= 0.95; // 边界检查 this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY)); } } render(ctx) { // 绘制背景渐变(深紫蓝色调) const gradient = ctx.createLinearGradient(0, 0, 0, this.screenHeight); gradient.addColorStop(0, '#0f0c29'); gradient.addColorStop(0.5, '#302b63'); gradient.addColorStop(1, '#24243e'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, this.screenWidth, this.screenHeight); // 添加星空装饰点 this.renderStars(ctx); // 绘制顶部标题栏 this.renderHeader(ctx); // 绘制分类标签 this.renderCategories(ctx); // 绘制故事列表 this.renderStoryList(ctx); // 绘制底部Tab栏 this.renderTabBar(ctx); } renderStars(ctx) { ctx.fillStyle = 'rgba(255,255,255,0.3)'; const stars = [[30, 80], [80, 30], [150, 60], [200, 25], [280, 70], [320, 40]]; stars.forEach(([x, y]) => { ctx.beginPath(); ctx.arc(x, y, 1, 0, Math.PI * 2); ctx.fill(); }); } renderHeader(ctx) { // 标题带渐变 const titleGradient = ctx.createLinearGradient(20, 30, 200, 30); titleGradient.addColorStop(0, '#ffd700'); titleGradient.addColorStop(1, '#ff6b6b'); ctx.fillStyle = titleGradient; ctx.font = 'bold 26px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('星域故事汇', 20, 50); // 副标题 ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = '13px sans-serif'; ctx.fillText('每个选择,都是一个新世界', 20, 75); // AI创作入口按钮 const btnWidth = 80; const btnHeight = 32; const btnX = this.screenWidth - btnWidth - 15; const btnY = 35; const btnGradient = ctx.createLinearGradient(btnX, btnY, btnX + btnWidth, btnY); btnGradient.addColorStop(0, '#a855f7'); btnGradient.addColorStop(1, '#ec4899'); ctx.fillStyle = btnGradient; this.roundRect(ctx, btnX, btnY, btnWidth, btnHeight, 16); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('✨ AI创作', btnX + btnWidth / 2, btnY + 21); this.aiCreateBtnRect = { x: btnX, y: btnY, width: btnWidth, height: btnHeight }; } renderCategories(ctx) { const startY = 95; const tagHeight = 30; let x = 15 - this.categoryScrollX; const y = startY; // 计算总宽度用于滚动限制 ctx.font = '13px sans-serif'; let totalWidth = 15; this.categories.forEach((cat) => { totalWidth += ctx.measureText(cat).width + 28 + 12; }); this.maxCategoryScrollX = Math.max(0, totalWidth - this.screenWidth + 15); // 清空并重新记录位置 this.categoryRects = []; this.categories.forEach((cat, index) => { const isSelected = index === this.selectedCategory; const textWidth = ctx.measureText(cat).width + 28; // 记录每个标签的位置 this.categoryRects.push({ left: x + this.categoryScrollX, right: x + this.categoryScrollX + textWidth, index: index }); // 只渲染可见的标签 if (x + textWidth > 0 && x < this.screenWidth) { if (isSelected) { // 选中态:渐变背景 const tagGradient = ctx.createLinearGradient(x, y, x + textWidth, y); tagGradient.addColorStop(0, '#ff6b6b'); tagGradient.addColorStop(1, '#ffd700'); ctx.fillStyle = tagGradient; } else { ctx.fillStyle = 'rgba(255,255,255,0.08)'; } this.roundRect(ctx, x, y, textWidth, tagHeight, 15); ctx.fill(); // 未选中态加边框 if (!isSelected) { ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 1; this.roundRect(ctx, x, y, textWidth, tagHeight, 15); ctx.stroke(); } // 标签文字 ctx.fillStyle = isSelected ? '#ffffff' : 'rgba(255,255,255,0.7)'; ctx.font = isSelected ? 'bold 13px sans-serif' : '13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(cat, x + textWidth / 2, y + 20); } x += textWidth + 12; }); } renderStoryList(ctx) { const startY = 150; const cardHeight = 120; const cardPadding = 15; const cardMargin = 15; const stories = this.getFilteredStories(); if (stories.length === 0) { ctx.fillStyle = '#666666'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('暂无该分类的故事', this.screenWidth / 2, 250); return; } // 设置裁剪区域,防止卡片渲染到分类标签区域 ctx.save(); ctx.beginPath(); ctx.rect(0, startY, this.screenWidth, this.screenHeight - startY - 65); ctx.clip(); stories.forEach((story, index) => { const y = startY + index * (cardHeight + cardMargin) - this.scrollY; // 只渲染可见区域的卡片 if (y > startY - cardHeight && y < this.screenHeight - 65) { this.renderStoryCard(ctx, story, cardMargin, y, this.screenWidth - cardMargin * 2, cardHeight); } }); ctx.restore(); } renderStoryCard(ctx, story, x, y, width, height) { // 卡片背景 - 毛玻璃效果 ctx.fillStyle = 'rgba(255,255,255,0.06)'; this.roundRect(ctx, x, y, width, height, 16); ctx.fill(); // 卡片高光边框 ctx.strokeStyle = 'rgba(255,255,255,0.1)'; ctx.lineWidth = 1; this.roundRect(ctx, x, y, width, height, 16); ctx.stroke(); // 封面区域 - 渐变色 const coverWidth = 85; const coverHeight = height - 24; const coverGradient = ctx.createLinearGradient(x + 12, y + 12, x + 12 + coverWidth, y + 12 + coverHeight); const colors = this.getCategoryGradient(story.category); coverGradient.addColorStop(0, colors[0]); coverGradient.addColorStop(1, colors[1]); ctx.fillStyle = coverGradient; this.roundRect(ctx, x + 12, y + 12, coverWidth, coverHeight, 12); ctx.fill(); // 封面上的分类名 ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(story.category || '故事', x + 12 + coverWidth / 2, y + 12 + coverHeight / 2 + 4); // 故事标题 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 17px sans-serif'; ctx.textAlign = 'left'; const titleX = x + 110; const maxTextWidth = width - 120; // 可用文字宽度 const title = this.truncateText(ctx, story.title, maxTextWidth); ctx.fillText(title, titleX, y + 35); // 故事简介 ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.font = '12px sans-serif'; const desc = story.description || ''; const shortDesc = this.truncateText(ctx, desc, maxTextWidth); ctx.fillText(shortDesc, titleX, y + 58); // 统计信息带图标 ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.font = '11px sans-serif'; const playText = `▶ ${this.formatNumber(story.play_count)}`; const likeText = `♥ ${this.formatNumber(story.like_count)}`; ctx.fillText(playText, titleX, y + 90); ctx.fillText(likeText, titleX + 70, y + 90); // 精选标签 - 更醒目 if (story.is_featured) { const tagGradient = ctx.createLinearGradient(x + width - 55, y + 12, x + width - 10, y + 12); tagGradient.addColorStop(0, '#ff6b6b'); tagGradient.addColorStop(1, '#ffd700'); ctx.fillStyle = tagGradient; this.roundRect(ctx, x + width - 55, y + 12, 45, 22, 11); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('精选', x + width - 32, y + 27); } } getCategoryGradient(category) { const gradients = { '都市言情': ['#ff758c', '#ff7eb3'], '悬疑推理': ['#667eea', '#764ba2'], '古风宫廷': ['#f093fb', '#f5576c'], '校园青春': ['#4facfe', '#00f2fe'], '修仙玄幻': ['#43e97b', '#38f9d7'], '穿越重生': ['#fa709a', '#fee140'], '职场商战': ['#30cfd0', '#330867'], '科幻未来': ['#a8edea', '#fed6e3'], '恐怖惊悚': ['#434343', '#000000'], '搞笑轻喜': ['#f6d365', '#fda085'] }; return gradients[category] || ['#667eea', '#764ba2']; } renderTabBar(ctx) { const tabHeight = 65; const y = this.screenHeight - tabHeight; // Tab栏背景 - 毛玻璃 ctx.fillStyle = 'rgba(15, 12, 41, 0.95)'; ctx.fillRect(0, y, this.screenWidth, tabHeight); // 顶部高光线 const lineGradient = ctx.createLinearGradient(0, y, this.screenWidth, y); lineGradient.addColorStop(0, 'rgba(255,107,107,0.3)'); lineGradient.addColorStop(0.5, 'rgba(255,215,0,0.3)'); lineGradient.addColorStop(1, 'rgba(255,107,107,0.3)'); ctx.strokeStyle = lineGradient; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(this.screenWidth, y); ctx.stroke(); const tabs = [ { icon: '🏠', label: '首页' }, { icon: '🔍', label: '发现' }, { icon: '👤', label: '我的' } ]; const tabWidth = this.screenWidth / tabs.length; tabs.forEach((tab, index) => { const centerX = index * tabWidth + tabWidth / 2; const isActive = index === this.currentTab; // 选中态指示器 if (isActive) { const indicatorGradient = ctx.createLinearGradient(centerX - 20, y + 3, centerX + 20, y + 3); indicatorGradient.addColorStop(0, '#ff6b6b'); indicatorGradient.addColorStop(1, '#ffd700'); ctx.fillStyle = indicatorGradient; this.roundRect(ctx, centerX - 20, y + 2, 40, 3, 1.5); ctx.fill(); } // 图标 ctx.font = '22px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(tab.icon, centerX, y + 32); // 标签文字 ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.4)'; ctx.font = isActive ? 'bold 11px sans-serif' : '11px sans-serif'; ctx.fillText(tab.label, centerX, y + 52); }); } // 获取分类颜色 getCategoryColor(category) { const colors = { '都市言情': '#e94560', '悬疑推理': '#4a90d9', '古风宫廷': '#d4a574', '校园青春': '#7ed957', '修仙玄幻': '#9b59b6', '穿越重生': '#f39c12', '职场商战': '#3498db', '科幻未来': '#1abc9c', '恐怖惊悚': '#2c3e50', '搞笑轻喜': '#f1c40f' }; return colors[category] || '#666666'; } // 格式化数字 formatNumber(num) { if (num >= 10000) { return (num / 10000).toFixed(1) + 'w'; } if (num >= 1000) { return (num / 1000).toFixed(1) + 'k'; } return num.toString(); } // 截断文字以适应宽度 truncateText(ctx, text, maxWidth) { if (!text) return ''; if (ctx.measureText(text).width <= maxWidth) { return text; } let truncated = text; while (truncated.length > 0 && ctx.measureText(truncated + '...').width > maxWidth) { truncated = truncated.slice(0, -1); } return truncated + '...'; } // 绘制圆角矩形 roundRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); } onTouchStart(e) { const touch = e.touches[0]; this.lastTouchY = touch.clientY; this.lastTouchX = touch.clientX; this.touchStartX = touch.clientX; this.touchStartY = touch.clientY; this.hasMoved = false; // 判断是否在分类区域(y: 90-140) if (touch.clientY >= 90 && touch.clientY <= 140) { this.isCategoryDragging = true; this.isDragging = false; } else { this.isCategoryDragging = false; this.isDragging = true; } this.scrollVelocity = 0; } onTouchMove(e) { const touch = e.touches[0]; // 分类区域横向滑动 if (this.isCategoryDragging) { const deltaX = this.lastTouchX - touch.clientX; if (Math.abs(deltaX) > 2) { this.hasMoved = true; } this.categoryScrollX += deltaX; this.categoryScrollX = Math.max(0, Math.min(this.categoryScrollX, this.maxCategoryScrollX)); this.lastTouchX = touch.clientX; return; } // 故事列表纵向滑动 if (this.isDragging) { const deltaY = this.lastTouchY - touch.clientY; if (Math.abs(touch.clientY - this.touchStartY) > 5) { this.hasMoved = true; } this.scrollVelocity = deltaY; this.scrollY += deltaY; this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY)); this.lastTouchY = touch.clientY; } } onTouchEnd(e) { this.isDragging = false; this.isCategoryDragging = false; // 如果有滑动,不处理点击 if (this.hasMoved) { return; } // 检测点击 const touch = e.changedTouches[0]; const x = touch.clientX; const y = touch.clientY; // 检测AI创作按钮点击 if (this.aiCreateBtnRect) { const btn = this.aiCreateBtnRect; if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) { this.main.sceneManager.switchScene('aiCreate'); return; } } // 检测Tab栏点击 if (y > this.screenHeight - 65) { const tabWidth = this.screenWidth / 3; const tabIndex = Math.floor(x / tabWidth); this.handleTabClick(tabIndex); return; } // 检测分类标签点击 if (y >= 90 && y <= 140) { this.handleCategoryClick(x); return; } // 检测故事卡片点击 this.handleStoryClick(x, y); } handleCategoryClick(x) { // 考虑横向滚动偏移 const adjustedX = x + this.categoryScrollX; // 使用保存的实际位置判断 for (const rect of this.categoryRects) { if (adjustedX >= rect.left && adjustedX <= rect.right) { if (this.selectedCategory !== rect.index) { this.selectedCategory = rect.index; this.scrollY = 0; this.calculateMaxScroll(); console.log('选中分类:', this.categories[rect.index]); } return; } } } handleTabClick(tabIndex) { if (tabIndex === this.currentTab) return; this.currentTab = tabIndex; if (tabIndex === 2) { // 切换到个人中心 this.main.sceneManager.switchScene('profile'); } } handleStoryClick(x, y) { const startY = 150; const cardHeight = 120; const cardMargin = 15; const stories = this.getFilteredStories(); // 计算点击的是哪个故事 const adjustedY = y + this.scrollY; const index = Math.floor((adjustedY - startY) / (cardHeight + cardMargin)); if (index >= 0 && index < stories.length) { const story = stories[index]; // 跳转到故事播放场景 this.main.sceneManager.switchScene('story', { storyId: story.id }); } } }