gray and brown mountain

Next初体验,做一个轮盘小游戏

Next很适合做多语言的项目,由于对SEO的友好,也很适合做电商等应用,多种渲染策略,使得它灵活性很高。这次我打算使用Next结合Canvas做一个轮盘小游戏

Next的核心特点

我们常用的Vue、React等严格来说不能叫做前端框架(Framework),最多只能叫做库(Library)。基于库的方案集成的项目,才能被称之为框架。当下,React最流行的框架是Next.js,Vue对标的则是Nuxt.js

Next.js 的几个特点

  • 多种渲染方式
  • 高效路由模式
  • 扩展性
  • 部署友好

上述几个特点是我认为Next最核心的优势,这些特点也帮助它成为当下前端最流行的框架,甚至没有之一。

渲染方式

服务端渲染(SSR)、静态站点生成(SSG)、增量静态生成(ISR)这三种渲染方式基本上能应对前端开发的大部分情况了。SSR是在服务器生成HTML内容,帮助提升SEO效果;SSG构建时预渲染HTML,适合博客、个人网站的开发;ISR能够动态的更新静态内容,而不会重新构建整个项目,对性能有比较好的优化

路由模式

比较值得说的是文件系统路由,自动读取/pages下的路径作为路由,无需额外配置路由文件。这个在开发上很好用。以及动态路由生成,通过URL参数来生成动态页面,比如pages/post/[id].js

扩展性

Tailwindcss的集成度友好、同时less、scss方案也没问题;自带i18n,也无需额外安装插件。

部署

这个相当于核心卖点,Next.js的开发商Vercel提供了云部署,更是重量级,可以几乎以白嫖的方式部署你的应用。当然这个模式也有缺点,后续再讲。

总结

结合上述特点,Next很适合做多语言的项目,由于对SEO的友好,也很适合做电商等应用,多种渲染策略,使得它灵活性很高。这次我打算使用Next结合Canvas做一个轮盘小游戏,感兴趣的往下看吧。

初始化

初始化就使用官方标准三板斧:

npx create-next-app@latest --next-name
初始化项目

解释一下安装设置:

  • TypeScript 选择Yes,我打算使用类型约束
  • 启用ESLint,选No,TypeScript本身就完成了规范约束,使用TS做单一约束系统
  • Tailwindcss,Yes,这一套样式语法,对开发效率来说有提升
  • code 路径,默认src,选择Yes,符合一贯开发习惯
  • AppRouter,应用路由系统,也是Next的特色,选择Yes
  • Turbopack,运行环境,Turbopack是对标Vite的工具,也是Next官方建议的,该工具是用Rust编写,自带热更新,性能很强,我们也选择Yes
  • 是否自定义倒入别名,默认为@,选择No

接下来就等待安装完毕即可。


安装完之后的目录如下,tailwind.config.js需要额外新增,我们创建这个文件并编辑,它的作用是告诉Next该项目的tailwind基础样式和配置以该文件为准:

module.exports = {
  darkMode: "class",
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors:{
        background: "var(--background)",
        foreground: "var(--foreground)",
      },
      fontFamily: {
        sans: ["var(--font-geist-sans)"],
        mono: ["var(--font-geist-mono)"],
      },
    },
  },
  plugins: [],
};

点开next.config.js,增加如下编辑内容

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  /** 开关开发者UI的选项 */
  devIndicators:false
};

export default nextConfig;

接下来,我们设置一下目录。


/src
	/app
		/picker-wheel
			/components
			/lib
			/style
			index.md
			page.tsx
		/setting
		/share
		/achieve

我们会在picker-wheel下制作轮盘,首先我们把根路径下的page清空,并导入picker-wheel。代码如下


/**根page**/
import Image from "next/image";
import Link from "next/link";
import PickerWheel from "./picker-wheel/page";

export default function Home() {
  return (
    <div className="
      grid grid-rows-[20px_1fr_20px] justify-items-center items-center min-h-screen p-8 pb-20 gap-16 sm:p-20
    ">
      <header>header</header>
      <main>
        <div>
          <PickerWheel/>
        </div>
      </main>
      <footer>footer</footer>
    </div>
  );
}

接着我们来做轮盘功能,代码如下


import WheelCanvas from "./components/wheelCanvas";

export default function PickerWheel() {
  return (
    <div className="container mx-auto p-4">
      <h1>yes or no wheel</h1>
      <WheelCanvas />
    </div>
  );
}

我使用的动画方案是Cavans的方式,我没有采用Anime.js+Div+css的方式,我发现在Next的环境下好像动画不生效,有懂的大佬可以在评论区指点一下。

接下来是WheelCanvas的具体实现


"use client";
import { useEffect, useRef, useState } from "react";

export default function WheelCanvas() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [isSpinning, setIsSpinning] = useState(false);
  const rotationAngle = useRef(0);

  // 绘制转盘(修复尺寸问题)
  const drawWheel = (ctx: CanvasRenderingContext2D, angle: number = 0) => {
    const prizes = ["Yes", "No"];
    const centerX = ctx.canvas.width / 2;
    const centerY = ctx.canvas.height / 2;
    const displaySize = Math.min(ctx.canvas.width, ctx.canvas.height); // 统一尺寸基准
    const radius = displaySize / 2 - 10; // 保留10px边距

    // 清除画布
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    
    // 应用旋转
    ctx.save();
    ctx.translate(centerX, centerY);
    ctx.rotate((angle * Math.PI) / 180);
    ctx.translate(-centerX, -centerY);

    // 绘制扇形
    prizes.forEach((text, i) => {
      const startAngle = (i * Math.PI * 2) / prizes.length;
      const endAngle = ((i + 1) * Math.PI * 2) / prizes.length;
      
      ctx.beginPath();
      ctx.moveTo(centerX, centerY);
      ctx.arc(centerX, centerY, radius, startAngle, endAngle);
      ctx.fillStyle = i === 0 ? "#FF5252" : "#4CAF50";
      ctx.fill();

      // 绘制文字
      ctx.save();
      ctx.translate(centerX, centerY);
      ctx.rotate(startAngle + Math.PI / prizes.length);
      ctx.textAlign = "center";
      ctx.fillStyle = "white";
      ctx.font = `bold ${radius * 0.15}px Arial`; // 动态字体大小
      ctx.fillText(text, radius * 0.5, radius * 0.05);
      ctx.restore();
    });

    ctx.restore();
  };

  const handleSpin = () => {
    if (isSpinning || !canvasRef.current) return;
    
    const ctx = canvasRef.current.getContext("2d");
    if (!ctx) return;

    setIsSpinning(true);
    const startAngle = rotationAngle.current;
    const targetAngle = startAngle + 1800 + Math.random() * 360;

    let startTime: number | null = null;
    const duration = 3000;
    // 手动动画
    const animateFrame = (timestamp: number) => {
      if (!startTime) startTime = timestamp;
      const elapsed = timestamp - startTime;
      const progress = Math.min(elapsed / duration, 1);
      
      // 弹性缓动
      const elasticProgress = progress < 0.5 
        ? 0.5 * Math.pow(2, 20 * progress - 10)
        : 0.5 * (2 - Math.pow(2, -20 * progress + 10));

      rotationAngle.current = startAngle + (targetAngle - startAngle) * elasticProgress;
      drawWheel(ctx, rotationAngle.current);

      if (progress < 1) {
        requestAnimationFrame(animateFrame);
      } else {
        // 转盘结束
        setIsSpinning(false);
        const prizeIndex = Math.floor(((rotationAngle.current % 360) + 90) % 360 / 180);
        alert(`Result: ${["Yes", "No"][prizeIndex]}`);
      }
    };

    requestAnimationFrame(animateFrame);
  };

  // 初始化Canvas(关键修复)
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    
    // 逻辑尺寸(保持300x300的工作坐标)
    const logicalSize = 300;
    canvas.width = logicalSize;
    canvas.height = logicalSize;
    
    // 显示尺寸(通过CSS控制)
    canvas.style.width = `${logicalSize}px`;
    canvas.style.height = `${logicalSize}px`;
    
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
    
    // 初始化绘制
    drawWheel(ctx);
  }, []);

  return (
    <div className="text-center">
      <canvas 
        ref={canvasRef}
        className="mx-auto border rounded-full shadow-lg"
      />
      <button
        onClick={handleSpin}
        disabled={isSpinning}
        className="mt-6 px-6 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {isSpinning ? "spinning..." : "start spinning"}
      </button>
    </div>
  );
}

效果呈现

最后我们可以来看一下效果。

点击旋转就会开始转。并且带有过度动画,和速度衰减,使得转盘更加真实。

后续我们还会增加额外的功能,音效的同步、抽奖结果的设置等等。

链接如下 : https://github.com/weihuanChen/yes-or-no-wheel

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注