Files
ai_game/client/js/scenes/HomeScene.js

565 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 首页场景
*/
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 });
}
}
}