在学习完了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你会发现 强类型 会及时的提醒所缺是方法和缺少的 属性。
会更加规范你所写的程序 值得 多多使用