使用typescript去简单的写一个时间显示的canvas小球

本文介绍了如何使用TypeScript编写一个基础的时间小球项目,包括时间显示、小球收集与渲染,以及倒计时功能。通过定义类型如Ball、Times和Site,展示了TS在类、函数和结构上的应用,有助于初学者巩固实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     在学习完了ts后 一直想找一个项目练手 可网上开源的项目 写的ts项目 还是有点复杂,不太适合刚刚学完ts想练手的同学。

于是就打算自己写一个 ts小项目  

大概需求就是:1 可以根据当前时间 用canvas绘制一个时间

                          2 可以每隔一秒 收集一个小球

                          3 可以 将收集的小球 依次渲染出来

                          4 可以 有倒计时模式

实现效果大概是

从需求可以分析出 用到ts的地方 首先 需要定义的类型 一个小球的类型,数字数据结构 颜色结构等

实现上面的效果,大概需要这几个方法,一个是显示时间的函数,每间隔一秒收集小球 并让小球运动的方法

1,先定义一下结构类型

export type Ball = { // 小球的结构
  x: number,
  y: number,
  g: number,
  vx: number,
  vy: number,
  color: string
}

export type Times = { // 时间结构
  hour: number,
  minute: number,
  second: number
}

export type Site = { // 时间每一个位起始位置
  site: number,
  isSite?: boolean,
  times: number | null
}

 

2 定义一下 0-9 还有冒号的 二维数组 和可选择的时间常量

export const nums = [
  [
    [0, 0, 1, 1, 1, 0, 0],
    [0, 1, 1, 0, 1, 1, 0],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 1, 1, 0, 1, 1, 0],
    [0, 0, 1, 1, 1, 0, 0]
  ],//0
  [
    [0, 0, 0, 1, 1, 0, 0],
    [0, 1, 1, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [1, 1, 1, 1, 1, 1, 1]
  ],//1
  [
    [0, 1, 1, 1, 1, 1, 0],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 1, 1, 0, 0, 0],
    [0, 1, 1, 0, 0, 0, 0],
    [1, 1, 0, 0, 0, 0, 0],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 1, 1, 1, 1, 1]
  ],//2
  [
    [1, 1, 1, 1, 1, 1, 1],
    [0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 1, 1, 1, 0, 0],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 1, 1, 1, 1, 1, 0]
  ],//3
  [
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 1, 1, 1, 0],
    [0, 0, 1, 1, 1, 1, 0],
    [0, 1, 1, 0, 1, 1, 0],
    [1, 1, 0, 0, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 1, 1, 1, 1]
  ],//4
  [
    [1, 1, 1, 1, 1, 1, 1],
    [1, 1, 0, 0, 0, 0, 0],
    [1, 1, 0, 0, 0, 0, 0],
    [1, 1, 1, 1, 1, 1, 0],
    [0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 1, 1, 1, 1, 1, 0]
  ],//5
  [
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 1, 1, 0, 0, 0],
    [0, 1, 1, 0, 0, 0, 0],
    [1, 1, 0, 0, 0, 0, 0],
    [1, 1, 0, 1, 1, 1, 0],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 1, 1, 1, 1, 1, 0]
  ],//6
  [
    [1, 1, 1, 1, 1, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 0, 1, 1, 0, 0, 0],
    [0, 0, 1, 1, 0, 0, 0],
    [0, 0, 1, 1, 0, 0, 0],
    [0, 0, 1, 1, 0, 0, 0]
  ],//7
  [
    [0, 1, 1, 1, 1, 1, 0],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 1, 1, 1, 1, 1, 0],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 1, 1, 1, 1, 1, 0]
  ],//8
  [
    [0, 1, 1, 1, 1, 1, 0],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0, 1, 1],
    [0, 1, 1, 1, 0, 1, 1],
    [0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 1, 1, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [0, 1, 1, 0, 0, 0, 0]
  ],//9
  [
    [0, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 1, 1, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 1, 1, 0],
    [0, 1, 1, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0]
  ]//:
];

export const Colors = ['#C506F6', '#F60692', '#3906F6', '#06F62B', '#B7F606', '#A9E402', '#04F5FD', '#FD5604', '#0377B8', '#8FF5B8', '#F1F806']

然后我们就可以开始写了

我是定义了一个 timeBall 类,这个类里面有 start 开始函数,showTime显示时间函数,readerNum和 update函数

start 这个函数 有一个功能就是根据参数去确定是否开启倒计时

start(endTime?: Date) {
    Balls = []
    this.endTimes = endTime
    this.isState = endTime != undefined
    this.oldTime = getShowTimes(this.isState, endTime)

    clearInterval(Inters)
    Inters = setInterval(() => {
      this.showTime()
      this.update()
    }, 50)
  }

showTime,根据获取的时间 一次在指定位置渲染小球,同时也要渲染收集的小球

 /**
   * 展示具体的时间
   */
  private showTime() {
    this.ctx.clearRect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);

    let readerArr = getSiteTime(this.oldTime)

    readerArr.forEach((item) => {
      if (item.times != null) {
        if (item.isSite) {
          this.readerNum(MARGIN_LEFT + item.site * (RADIUS + 1), MARGIN_TOP, Math.floor(item.times / 10))
        } else {
          this.readerNum(MARGIN_LEFT + item.site * (RADIUS + 1), MARGIN_TOP, Math.floor(item.times % 10))
        }
      } else {
        this.readerNum(MARGIN_LEFT + item.site * (RADIUS + 1), MARGIN_TOP, 10)
      }
    })

    // 渲染每个小球
    for (let i = 0; i < Balls.length; i++) {
      readerBall(Balls[i].x, Balls[i].y, Balls[i].color, this.ctx)
    }
  }

readerNum 取出对应时间数字的 二维数组 并调用需要函数

  // 将时间用数据绘制出来
  private readerNum(x: number, y: number, num: number) {
    let textNum = nums[num]
    let color = '#B7F606'
    for (let i = 0; i < textNum.length; i++) {
      for (let j = 0; j < textNum[i].length; j++) {
        if (textNum[i][j] === 1) {
          readerBall(x + j * 2 * (RADIUS + 1) + (RADIUS + 1), y + i * 2 * (RADIUS + 1) + (RADIUS + 1), color, this.ctx)
        }
      }
    }
  }

update 函数则是将一秒内变化的数据收集起来,需要一次收集 时分秒上的数据,同时改变收集的小球位置

private update() {
    Nums_second -= 1

    if (Nums_second === 0) { // 说一秒到了
      Nums_second = 1000 / Interval

      let curTime = getShowTimes(this.isState, this.endTimes)
      let curTimeArr = getSiteTime(curTime).filter(tt => tt.times != null)
      let oldTimeArr = getSiteTime(this.oldTime).filter(tt => tt.times != null)

      for (let i = 0; i < curTimeArr.length; i++) {
        if (curTimeArr[i].isSite) { // 十位
          if ((curTimeArr[i].times as number / 10) != (oldTimeArr[i].times as number / 10)) {
            addBall(MARGIN_LEFT + curTimeArr[i].site * (RADIUS + 1), MARGIN_TOP, Math.floor(curTimeArr[i].times as number / 10))
          }
        } else { // 个位
          if ((curTimeArr[i].times as number % 10) != (oldTimeArr[i].times as number % 10)) {
            addBall(MARGIN_LEFT + curTimeArr[i].site * (RADIUS + 1), MARGIN_TOP, Math.floor(curTimeArr[i].times as number % 10))
          }
        }
      }
      this.oldTime = curTime // 需要更新 上一次记录的时间值
    }
    updateBall() // 无论到没到1秒都需要更新小球位置
  }

剩下的是几个公用的方法

首先 getShowTimes 这个方法会返回一个 十分秒每一位上具体显示的数据 同时也划分开 是否需要倒计时

/**
 * 是否开启倒计时
 * @param flag  
 */
function getShowTimes(flag: boolean, endTime?: Date): Times {
  if (!flag) { // 正常显示时间
    let curTime = new Date();
    return {
      hour: curTime.getHours(),
      minute: curTime.getMinutes(),
      second: curTime.getSeconds()
    }
  } else { // 到计时
    let oldTime = endTime as Date
    let newTime = new Date()
    let ret = oldTime.getTime() - newTime.getTime()
    ret = Math.floor(ret / 1000)
    let hours = ret >= 0 ? Math.floor(ret / 3600) : 0

    return {
      hour: hours,
      minute: ret >= 0 ? Math.floor((ret - hours * 3600) / 60) : 0,
      second: ret >= 0 ? Math.floor(ret % 60) : 0
    }
  }
}

然后 getSiteTime 这个函数 将每一个时间的十位和个位 还有每一个时间起点位置存储起来

/**
 * 返回 十分秒 每一个的起点坐标和数据
 * @param tt 
 */
function getSiteTime(tt: Times): Site[] {
  return [
    {
      site: 0,
      isSite: true, // true 十位
      times: tt.hour
    },
    {
      site: 15,
      isSite: false, // 个位
      times: tt.hour
    },
    {
      site: 30,
      times: null
    },
    {
      site: 39,
      isSite: true,
      times: tt.minute
    },
    {
      site: 54,
      isSite: false,
      times: tt.minute
    },
    {
      site: 69,
      times: null
    },
    {
      site: 78,
      isSite: true,
      times: tt.second
    },
    {
      site: 93,
      isSite: false,
      times: tt.second
    }
  ]
}

readerBall 是一个小球利用canvas绘制的

// 具体的绘制小圆
function readerBall(x: number, y: number, style: string, ctx: CanvasRenderingContext2D) {
  ctx.fillStyle = style
  ctx.beginPath()
  ctx.arc(x, y, RADIUS, 0, 2 * Math.PI, true)
  ctx.closePath()
  ctx.fill()
}

还有一个收集小球的函数,x,y是这个小球绘制所在的角标,g vx和vy 是一些小球位移的变化参数

/**
 * 收集每个小球 并为每个小球设置初始所在位置
 * @param x 
 * @param y 
 * @param num 
 */
function addBall(x: number, y: number, num: number) {
  let textNum = nums[num]
  for (let i = 0; i < textNum.length; i++) {
    for (let j = 0; j < textNum[i].length; j++) {
      if (textNum[i][j] === 1) {
        let oneBall: Ball = {
          x: x + j * 2 * (RADIUS + 1) + (RADIUS + 1), // 每个小球之间的距离
          y: y + i * 2 * (RADIUS + 1) + (RADIUS + 1),
          g: 1.2 + Math.random(), // 加速度
          vx: Math.pow(-1, Math.ceil(Math.random() * 1000)) * 4, // -4或+4
          vy: -5,
          color: Colors[Math.floor(Math.random() * Colors.length)]
        }
        Balls.push(oneBall)
      }
    }
  }
}

小球运动

让小球 呈现一个抛物线的运动轨迹一次想两边位移  同时需要去除掉移动到屏幕外的数据


/**
 * 让数组中的小球 开始运动
 */
function updateBall() {
  let balls = []
  for (let i = 0; i < Balls.length; i++) {
    Balls[i].x += Balls[i].vx
    Balls[i].y += Balls[i].vy
    Balls[i].vy += Balls[i].g
    if (Balls[i].y >= WINDOW_HEIGHT - RADIUS) { // 落地反弹
      Balls[i].vy = -Balls[i].vy * 0.75
    }
    if (Balls[i].x + RADIUS > 0 && Balls[i].x - RADIUS < WINDOW_WIDTH) { // 每次过滤掉移除屏幕外的数据
      balls.push(Balls[i])
    }
  }
  Balls = balls
}

timeBall 类

import { Ball, Times, Site } from '../basic/types'
import { nums, Colors } from '../basic/basicNum'

const Interval: number = 50
var WINDOW_WIDTH: number = 1024;
var WINDOW_HEIGHT: number = 768;
var RADIUS: number = 8;
var MARGIN_TOP = 60;
var MARGIN_LEFT = 30;
var Nums_second: number = 1000 / Interval

var Balls: Ball[] = [] // 存储所有的小球
let Inters: NodeJS.Timeout // 定时器



export class timeBall {
  private endTimes?: Date // 记录倒计时末端数据
  private isState: boolean = false  // 是否开启倒计时
  private oldTime: Times = {
    hour: 0,
    minute: 0,
    second: 0
  } // 收集老时间

  ctx: CanvasRenderingContext2D
  constructor(id: string) {
    var canvas = document.getElementById(id) as HTMLCanvasElement
    this.ctx = canvas.getContext('2d')!

    canvas.width = WINDOW_WIDTH = document.body.clientWidth
    canvas.height = WINDOW_HEIGHT = document.body.clientHeight

    RADIUS = Math.round((WINDOW_WIDTH - 500) / (7 * 6 + 4 * 2 + 7) * 2) // 每个数字7个球 加冒号 8 加 间隔 *2是为了求半径
    MARGIN_TOP = Math.round(WINDOW_HEIGHT / 5);
    MARGIN_LEFT = Math.round(WINDOW_WIDTH / 10);
  }

  start(endTime?: Date) {
    Balls = []
    this.endTimes = endTime
    this.isState = endTime != undefined
    this.oldTime = getShowTimes(this.isState, endTime)

    clearInterval(Inters)
    Inters = setInterval(() => {
      this.showTime()
      this.update()
    }, 50)
  }

  /**
   * 展示具体的时间
   */
  private showTime() {
    this.ctx.clearRect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);

    let readerArr = getSiteTime(this.oldTime)

    readerArr.forEach((item) => {
      if (item.times != null) {
        if (item.isSite) {
          this.readerNum(MARGIN_LEFT + item.site * (RADIUS + 1), MARGIN_TOP, Math.floor(item.times / 10))
        } else {
          this.readerNum(MARGIN_LEFT + item.site * (RADIUS + 1), MARGIN_TOP, Math.floor(item.times % 10))
        }
      } else {
        this.readerNum(MARGIN_LEFT + item.site * (RADIUS + 1), MARGIN_TOP, 10)
      }
    })

    // 渲染每个小球
    for (let i = 0; i < Balls.length; i++) {
      readerBall(Balls[i].x, Balls[i].y, Balls[i].color, this.ctx)
    }
  }

  private update() {
    Nums_second -= 1

    if (Nums_second === 0) { // 说一秒到了
      Nums_second = 1000 / Interval

      let curTime = getShowTimes(this.isState, this.endTimes)
      let curTimeArr = getSiteTime(curTime).filter(tt => tt.times != null)
      let oldTimeArr = getSiteTime(this.oldTime).filter(tt => tt.times != null)

      for (let i = 0; i < curTimeArr.length; i++) {
        if (curTimeArr[i].isSite) { // 十位
          if ((curTimeArr[i].times as number / 10) != (oldTimeArr[i].times as number / 10)) {
            addBall(MARGIN_LEFT + curTimeArr[i].site * (RADIUS + 1), MARGIN_TOP, Math.floor(curTimeArr[i].times as number / 10))
          }
        } else { // 个位
          if ((curTimeArr[i].times as number % 10) != (oldTimeArr[i].times as number % 10)) {
            addBall(MARGIN_LEFT + curTimeArr[i].site * (RADIUS + 1), MARGIN_TOP, Math.floor(curTimeArr[i].times as number % 10))
          }
        }
      }
      
      this.oldTime = curTime
    }
    updateBall()
  }



  // 将时间用数据绘制出来
  private readerNum(x: number, y: number, num: number) {
    let textNum = nums[num]
    let color = '#B7F606'
    for (let i = 0; i < textNum.length; i++) {
      for (let j = 0; j < textNum[i].length; j++) {
        if (textNum[i][j] === 1) {
          readerBall(x + j * 2 * (RADIUS + 1) + (RADIUS + 1), y + i * 2 * (RADIUS + 1) + (RADIUS + 1), color, this.ctx)
        }
      }
    }
  }


}

main 中调用

import { timeBall } from './build/fn/ball'
window.onload = function () {

  let Ball = new timeBall('canvas')
  Ball.start()

  let start_btn = document.getElementById('start')
  let stop_btn = document.getElementById('stops')

  start_btn.addEventListener('click', () => { // 开启倒计时
    let myTime = new Date()
    let YY = myTime.getFullYear()
    let MM = myTime.getMonth()
    let DD = myTime.getDate()

    let hh = parseInt(document.getElementById('hh').value)
    let mm = parseInt(document.getElementById('mm').value)
    let ss = parseInt(document.getElementById('mm').value)

    let endT = new Date(YY, MM, DD, hh, mm, ss)
    Ball.start(endT)
  })

  stop_btn.addEventListener('click', () => { // 正常显示
    Ball.start()
  })

}

index.html

<!--
 * @Author: your name
 * @Date: 2020-10-15 11:19:30
 * @LastEditTime: 2020-10-16 17:10:32
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: \ts写canvas时间小球\src\index.html
-->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>canvas小球</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      html,
      body {
        height: 100%;
        position: relative;
        overflow: hidden;
      }
      .form {
        position: absolute;
        top: 0;
        left: 0;
      }
      .form .inputs {
        margin-top: 10px;
      }
      .form .inputs p {
        margin-bottom: 10px;
      }
    </style>
  </head>
  <body>
    <div class="form">
      <h4>默认是显示时间,点击确定未倒计时</h4>
      <div class="inputs">
        <p>时:<input type="number" max="24" min="0" name="" id="hh" /></p>
        <p>分:<input type="number" max="60" min="0" name="" id="mm" /></p>
        <p>秒:<input type="number" max="60" min="0" name="" id="ss" /></p>
      </div>
      <div class="btn">
        <button id="start">开始</button>
        <button id="stops">取消</button>
      </div>
    </div>
    <canvas id="canvas" style="height: 100%">
      当前浏览器不支持Canvas,请更换浏览器后再试
    </canvas>
  </body>
</html>

同时,这里我是使用webpack去编辑的 

webpack.config.js

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')


module.exports = {
  entry: {
    app: './src/main.js'
  },
  output: {
    filename: '[name].builds.js',
    path: path.resolve(__dirname, 'dist')
  },
  devtool: 'inline-source-map',// 错误追踪
  devServer: { // 告诉web服务器去这里找文件
    contentBase: './dist',
    hot: true
  },
  plugins: [
    new CleanWebpackPlugin(), // 打包之前清空
    new HtmlWebpackPlugin({ template: './src/index.html' })
  ]
}

webpack打包后 会生成dist 直接使用dist里面的html就可以 运行了。因为我是用vscode编辑的 可以直接实时的建ts转换成 js

引用和调用都是 调用js的

 

总结一下 使用的ts

类型定义,类,函数 基本就是这些了。使用ts你会发现 强类型 会及时的提醒所缺是方法和缺少的 属性。

会更加规范你所写的程序  值得 多多使用

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值