Canvas实现协同电影选座

技术栈

React + Canvas + Socket.IO + Express

效果图

架构设计

arduino 复制代码
┌─────────────────┐    WebSocket    ┌─────────────────┐
│   React Client  │ ←──────────────→ │  Express Server │
│                 │                 │                 │
│ ┌─────────────┐ │                 │ ┌─────────────┐ │
│ │   Canvas    │ │                 │ │  Socket.IO  │ │
│ │  Renderer   │ │                 │ │   Handler   │ │
│ └─────────────┘ │                 │ └─────────────┘ │
│                 │                 │                 │
│ ┌─────────────┐ │                 │ ┌─────────────┐ │
│ │ Socket.IO   │ │                 │ │ Seat Data   │ │
│ │   Client    │ │                 │ │  Manager    │ │
│ └─────────────┘ │                 │ └─────────────┘ │
└─────────────────┘                 └─────────────────┘

核心功能实现

1. Canvas渲染

设备像素比适配

现代设备的高分辨率屏幕(如Retina显示器)会导致Canvas绘制的图像显得模糊。我们通过适配设备像素比来解决这个问题:

javascript 复制代码
const drawSeatMap = useCallback(() => {
  if (!seatData || !canvasRef.current) return;

  const canvas = canvasRef.current;
  const ctx = canvas.getContext('2d');
  
  // 获取设备像素比
  const dpr = window.devicePixelRatio || 1;
  
  // 获取Canvas的显示尺寸
  const rect = canvas.getBoundingClientRect();
  const displayWidth = rect.width;
  const displayHeight = rect.height;
  
  // 设置Canvas的实际像素尺寸
  canvas.width = displayWidth * dpr;
  canvas.height = displayHeight * dpr;
  
  // 缩放绘图上下文以匹配设备像素比
  ctx.scale(dpr, dpr);
  
  // 设置Canvas的CSS尺寸
  canvas.style.width = displayWidth + 'px';
  canvas.style.height = displayHeight + 'px';
}, [seatData, hoveredSeat, userId]);

这种处理方式确保了在各种设备上都能获得清晰的显示效果。

座位布局

采用了符合真实影院布局的设计:

javascript 复制代码
// 座位配置常量
const SEAT_SIZE = 30;        // 座位大小
const SEAT_SPACING = 35;     // 座位间距
const ROW_SPACING = 40;      // 行间距
const CANVAS_PADDING = 50;   // 画布边距
const AISLE_WIDTH = 20;      // 过道宽度

// 绘制单个座位
const drawSeat = (ctx, row, seat, seatInfo, seatId) => {
  // 计算座位位置,第6座后添加过道
  const x = CANVAS_PADDING + seat * SEAT_SPACING + (seat >= 6 ? AISLE_WIDTH : 0);
  const y = CANVAS_PADDING + 60 + row * ROW_SPACING;

  // 根据座位状态确定颜色
  let color = SEAT_COLORS.available;
  if (seatInfo.status === 'occupied') {
    color = SEAT_COLORS.occupied;
  } else if (seatInfo.status === 'selected') {
    color = seatInfo.selectedBy === userId ? 
      SEAT_COLORS.selected : SEAT_COLORS.selectedByOther;
  } else if (hoveredSeat === seatId) {
    color = SEAT_COLORS.hover;
  }

  // 绘制圆角矩形座位
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.roundRect(x, y, SEAT_SIZE, SEAT_SIZE, 5);
  ctx.fill();
};

多状态定义

定义了五种座位状态,每种状态都有独特的视觉表现:

javascript 复制代码
const SEAT_COLORS = {
  available: '#4CAF50',      // 可选 - 绿色
  selected: '#2196F3',       // 已选 - 蓝色
  occupied: '#F44336',       // 已售 - 红色
  selectedByOther: '#FF9800', // 他人已选 - 橙色
  hover: '#81C784'           // 悬停 - 浅绿色
};

2. 实时协作

WebSocket通信

使用Socket.IO实现双向实时通信:

客户端连接管理:

javascript 复制代码
useEffect(() => {
  const newSocket = io('http://localhost:3001');
  setSocket(newSocket);

  // 接收初始座位数据
  newSocket.on('seatData', (data) => {
    setSeatData(data);
  });

  // 监听座位状态更新
  newSocket.on('seatUpdated', ({ seatId, seat }) => {
    setSeatData(prev => ({
      ...prev,
      seats: {
        ...prev.seats,
        [seatId]: seat
      }
    }));
  });

  return () => newSocket.close();
}, []);

服务端事件处理:

javascript 复制代码
io.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  
  // 发送当前座位数据
  socket.emit('seatData', seatData);
  
  // 处理座位选择
  socket.on('selectSeat', (data) => {
    const { seatId, userId } = data;
    const seat = seatData.seats[seatId];
    
    // 业务逻辑验证
    if (seat.status === 'occupied') {
      socket.emit('error', { message: 'Seat is already occupied' });
      return;
    }
    
    // 切换选择状态
    if (seat.status === 'selected' && seat.selectedBy === userId) {
      seat.status = 'available';
      seat.selectedBy = null;
    } else {
      seat.status = 'selected';
      seat.selectedBy = userId;
    }
    
    // 广播给所有客户端
    io.emit('seatUpdated', { seatId, seat });
  });
});

用户会话管理

每个用户获得唯一标识符,确保座位选择的准确归属:

javascript 复制代码
const [userId] = useState(() => Math.random().toString(36).substr(2, 9));

当用户断开连接时,系统自动清理其选择的座位:

javascript 复制代码
socket.on('disconnect', () => {
  console.log('User disconnected:', socket.id);
  
  // 清除该用户选择的座位
  Object.keys(seatData.seats).forEach(seatId => {
    const seat = seatData.seats[seatId];
    if (seat.status === 'selected' && seat.selectedBy === socket.id) {
      seat.status = 'available';
      seat.selectedBy = null;
      io.emit('seatUpdated', { seatId, seat });
    }
  });
});

3. 交互

像素级点击检测

实现了精确的鼠标事件处理,支持像素级的点击检测:

javascript 复制代码
const handleCanvasClick = (event) => {
  if (!seatData || !socket) return;

  const canvas = canvasRef.current;
  const rect = canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  // 遍历所有座位进行碰撞检测
  for (let row = 0; row < seatData.rows; row++) {
    for (let seat = 0; seat < seatData.seatsPerRow; seat++) {
      const seatX = CANVAS_PADDING + seat * SEAT_SPACING + (seat >= 6 ? AISLE_WIDTH : 0);
      const seatY = CANVAS_PADDING + 60 + row * ROW_SPACING;

      if (x >= seatX && x <= seatX + SEAT_SIZE && 
          y >= seatY && y <= seatY + SEAT_SIZE) {
        const seatId = `${row}-${seat}`;
        const seatInfo = seatData.seats[seatId];

        if (seatInfo && seatInfo.status !== 'occupied') {
          socket.emit('selectSeat', { seatId, userId });
        }
        return;
      }
    }
  }
};

实时悬停效果

实现了流畅的鼠标悬停效果,提供即时的视觉反馈:

javascript 复制代码
const handleCanvasMouseMove = (event) => {
  if (!seatData) return;

  const canvas = canvasRef.current;
  const rect = canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  let foundSeat = null;

  // 查找鼠标悬停的座位
  for (let row = 0; row < seatData.rows; row++) {
    for (let seat = 0; seat < seatData.seatsPerRow; seat++) {
      const seatX = CANVAS_PADDING + seat * SEAT_SPACING + (seat >= 6 ? AISLE_WIDTH : 0);
      const seatY = CANVAS_PADDING + 60 + row * ROW_SPACING;

      if (x >= seatX && x <= seatX + SEAT_SIZE && 
          y >= seatY && y <= seatY + SEAT_SIZE) {
        foundSeat = `${row}-${seat}`;
        break;
      }
    }
    if (foundSeat) break;
  }

  if (foundSeat !== hoveredSeat) {
    setHoveredSeat(foundSeat);
  }
};

4. 数据管理与API

RESTful API

javascript 复制代码
// 获取座位数据
app.get('/api/seats', (req, res) => {
  res.json(seatData);
});

// 选择座位
app.post('/api/seats/select', (req, res) => {
  const { seatId, userId } = req.body;
  // 业务逻辑处理...
});

// 购买座位
app.post('/api/seats/book', (req, res) => {
  const { seatIds, userId } = req.body;
  
  const bookedSeats = [];
  const errors = [];
  
  seatIds.forEach(seatId => {
    const seat = seatData.seats[seatId];
    if (seat.status === 'selected' && seat.selectedBy === userId) {
      seat.status = 'occupied';
      seat.selectedBy = null;
      bookedSeats.push(seat);
    } else {
      errors.push(`Seat ${seatId} is not selected by you`);
    }
  });
  
  if (errors.length > 0) {
    return res.status(400).json({ errors });
  }
  
  res.json({ success: true, bookedSeats });
});

状态管理策略

采用React Hooks进行客户端状态管理:

javascript 复制代码
const [seatData, setSeatData] = useState(null);
const [socket, setSocket] = useState(null);
const [hoveredSeat, setHoveredSeat] = useState(null);

// 获取选中座位信息
const getSelectedSeatsInfo = () => {
  if (!seatData) return [];
  
  return Object.values(seatData.seats)
    .filter(seat => seat.status === 'selected' && seat.selectedBy === userId)
    .sort((a, b) => a.row - b.row || a.seat - b.seat);
};
相关推荐
梦帮科技几秒前
AI 音乐产品的发布工程:验证门、数据发布、回滚与生产运维纪律
数据结构·数据库·架构·node.js·音视频·动态规划·推荐算法
西安栈上月明软件科技1 分钟前
从 Linux 0.01 到 AI 开源:星图邻的开源实践
人工智能·自然语言处理·架构·开源·fastapi
泯泷3 分钟前
那段文字是谁删的?Yjs 14 正式版之前,一套删除归属方案的实现与边界
前端·javascript·算法
野槐3 小时前
前端项目优化(有了我,会发生...)
前端
Lynne3093 小时前
工业网络,到底该集中还是自治?
网络·架构·数据
aa小小3 小时前
【无标题】
前端
计算机魔术师4 小时前
还在单线程跟 AI 聊天?Claude Code 并行多会话让你一个人干三个人的活
前端
志尊宝4 小时前
Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀
前端·javascript·vue·html·vue3
JavaGuide4 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
风骏时光牛马4 小时前
AI驱动自动化业务工作流搭建实践
前端