javascript设计模式:中介者模式

thbcm阅读(177)

中介者对象践行了最少知识原则,指一个对象尽可能少的了解别的对象,从而尽量减少对象间耦合程度。这样各个对象只需关注自身实现逻辑,对象间的交互关系交由中介者对象来实现和维护。

需求背景:

手机购买页面,在购买流程中,可以选择手机的颜色及输入购买数量,同时页面有两个展示区域,分别向用户展示刚选择好的颜色和数量。还有一个按钮动态显示下一步的操作,我们需要查询该颜色手机对应的库存,如果库存数量少于这次购买的数量,按钮将被禁用并显示库存不足,反之按钮可以点击并显示放入购物车。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>中介者模式 购买商品</title>
</head>
<body>
    选择颜色: 
    <select id="colorSelect">
        <option value="">请选择</option>
        <option value="red">红色</option>
        <option value="blue">蓝色</option>
    </select>

    输入购买数量:
    <input type="text" id="numberInput">

    您选择了颜色:<div id="colorInfo"></div><br>
    您输入了数量:<div id="numberInfo"></div><br>

    <button id="nextBtn" disabled>请选择手机颜色和购买数量</button>
    
</body>
<script>

// 最初级的写法
var colorSelect = document.getElementById('colorSelect'),
    numberInput = document.getElementById('numberInput'),
    colorInfo = document.getElementById('colorInfo'),
    numberInfo = document.getElementById('numberInfo'),
    nextBtn = document.getElementById('nextBtn');

var goods = {
    'red': 3,
    'blue': 6
}

colorSelect.onchange = function(){
    var color = this.value,
        number = numberInput.value,
        stock = goods[color]

    colorInfo.innerHTML = color;

    if(!color){
        nextBtn.disabled = true;
        nextBtn.innerHTML = '请选择手机颜色';
        return;
    }

    if( ( (number-0) | 0 ) !== number-0 ){      //用户输入的购买数量是否为正整数
        nextBtn.disabled = true;
        nextBtn.innerHTML = '请输入正确的购买数量';
        return;
    }

    if(number > stock){     //当前选择数量大于库存量
        nextBtn.disabled = true;
        nextBtn.innerHTML = '库存不足';
        return;
    }

    nextBtn.disabled = false;
    nextBtn.innerHTML = '放入购物车';
}

numberInput.oninput = function(){
    var color = colorSelect.value,
        number = this.value,
        stock = goods[color]

    colorInfo.innerHTML = color;

    if(!color){
        nextBtn.disabled = true;
        nextBtn.innerHTML = '请选择手机颜色';
        return;
    }

    if( ( (number-0) | 0 ) !== number-0 ){      //用户输入的购买数量是否为正整数
        nextBtn.disabled = true;
        nextBtn.innerHTML = '请输入正确的购买数量';
        return;
    }

    if(number > stock){     //当前选择数量大于库存量
        nextBtn.disabled = true;
        nextBtn.innerHTML = '库存不足';
        return;
    }

    nextBtn.disabled = false;
    nextBtn.innerHTML = '放入购物车';
}
</script>
</html>

在上个示例中,对象间联系高度耦合,只是两个输入框还好,但如果有多个的话,相互联系就非常复杂了,此时就要考虑用到中介者模式。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>中介者模式 购买商品</title>
</head>
<body>
    选择颜色: 
    <select id="colorSelect">
        <option value="">请选择</option>
        <option value="red">红色</option>
        <option value="blue">蓝色</option>
    </select>

    选择内存: 
    <select id="memorySelect">
        <option value="">请选择</option>
        <option value="32G">32G</option>
        <option value="16G">16G</option>
    </select>

    输入购买数量:
    <input type="text" id="numberInput">

    您选择了颜色:<div id="colorInfo"></div><br>
    您选择了内存:<div id="memoryInfo"></div><br>
    您输入了数量:<div id="numberInfo"></div><br>

    <button id="nextBtn" disabled>请选择手机颜色、内存和购买数量</button>
</body>
<script>
    var goods = {
        'red|32G': 3,
        'red|16G': 0,
        'blue|32G': 1,
        'blue|16G': 6
    }

    //引入中介者
    var mediator = (function(){
        var colorSelect = document.getElementById('colorSelect'),
            memorySelect = document.getElementById('memorySelect'),
            numberInput = document.getElementById('numberInput'),
            colorInfo = document.getElementById('colorInfo'),
            memoryInfo = document.getElementById('memoryInfo'),
            numberInfo = document.getElementById('numberInfo'),
            nextBtn = document.getElementById('nextBtn');

        return {
            changed: function(obj){
                var color = colorSelect.value,
                    memory = memorySelect.value,
                    number = numberInput.value,
                    stock = goods[color + '|' + memory];

                if(obj == colorSelect){      //如果改变的是选择颜色下拉框
                    colorInfo.innerHTML = color;
                }else if(obj == memorySelect){
                    memoryInfo.innerHTML = memory;
                }else if(obj == numberInput){
                    numberInfo.innerHTML = number;
                }

                if(!color){
                    nextBtn.disabled = true;
                    nextBtn.innerHTML = '请选择手机颜色';
                    return;
                }

                if(!memory){
                    nextBtn.disabled = true;
                    nextBtn.innerHTML = '请选择手机内存';
                    return;
                }

                if(!number){
                    nextBtn.disabled = true;
                    nextBtn.innerHTML = '请填写手机数量';
                    return;
                }

                if( ( (number-0) | 0 ) !== number-0 ){      //用户输入的购买数量是否为正整数
                    nextBtn.disabled = true;
                    nextBtn.innerHTML = '请输入正确的购买数量';
                    return;
                }

                if(number > stock){     //当前选择数量大于库存量
                    nextBtn.disabled = true;
                    nextBtn.innerHTML = '库存不足';
                    return;
                }

                nextBtn.disabled = false;
                nextBtn.innerHTML = '放入购物车';
            }
        }
    })()

    colorSelect.onchange = function(){
        mediator.changed(this)
    }

    memorySelect.onchange = function(){
        mediator.changed(this)
    }

    numberInput.oninput = function(){
        mediator.changed(this)
    }

    //以后如果想要再增加选项,如手机CPU之类的,只需在中介者对象里加上相应配置即可。
</script>
</html>

在实际开发中,还是要注意选择利弊,中介者对象因为包含对象间交互的复杂性,所以维护成本可能也会较高。在实际开发中,最优目的还是要快速完成项目交付,而非过度设计和堆砌模式。有时对象间的耦合也是有必要的,只有当对象间复杂耦合确实已经导致调用与维护难以为继,才考虑用中介者模式。

程序员的浪漫:利用Python 3D技术制作元宵节走马花灯,体验“花市灯如昼”的节日气氛

thbcm阅读(177)

一、前言

说起元宵节,各位有没有觉得这是咱们中国人最浪漫的节日呢?国人向来拘谨古板,一年到头都是小心谨慎地过日子,唯有元宵节这天可以纵情豪放一把。东风夜放花千树,宝马雕车香满路,火树银花霓虹闪烁,豪车遍地美女如云。细品,你甚至都能嗅到香奈儿的味道!月上柳梢头,人约黄昏后,这又是何等的浪漫!比起烛光晚宴、鲜花加持,这份浪漫更显纯真。晚至明清,民间元宵节的喜庆气氛,堪比西班牙的奔牛节、巴西的狂欢节、泰国的泼水节。

由于众所周知的原因,估计今年的趵突泉元宵节灯会又要黄了。去哪儿体验“花市灯如昼”的节日气氛呢?Don’t worry,没有什么事能够难倒程序员——用3D技术也可以做出下图这样的走马灯,算是聊胜于无吧。

二、原材料

2.1 花灯纸

如下所示,还可以加上自己喜欢的图案、文字等。

2.2 Python环境和模块

一台安装了 Python 环境的电脑,Python 环境需要安装以下模块。

  • numpy
  • pillow
  • wxgl

如果没有上述模块,请参考下面的命令安装。

pip install numpy
pip install pillow
pip install wxgl

NumPy 和 pillow 是 Python 旗下最常用的科学计算库和图像处理库,属于常用模块。WxGL 是一个基于 PyOpenGL 的三维数据可视化库,以 wx 为显示后端,提供 Matplotlib 风格的交互式应用模式,同时,也可以和 wxPython 无缝结合,在 wx 的窗体上绘制三维模型。

三、制作工序

花灯制作工序非常简单,只需要三十行代码,可以直接在 Python IDLE 中以交互方式逐行执行。

3.1 导入模块

>>> import numpy as np
>>> from PIL import Image
>>> import wxgl.wxplot as plt

3.2 打开花灯纸图像

>>> fn = r'D:\temp\light0115\res\paper.png'
>>> im = np.array(Image.open(fn))/255
>>> im.shape
(400, 942, 3)

fn 定义的是图像存储路径,请据实修改。​Image.open(fn)​打开文件,返回一个 PIL 对象,​np.array() ​将 PIL 对象转成 ​numpy.ndarray​ 数组对象。除以255,将图像数据从 0 到 255 的值域范围变成0到1,适应 WxGL 的接口要求。查看数组的 shape,显示图像分辨率为400 像素高、942 像素宽,每个像素有三种颜色(此处为RGB)。

3.3 根据花灯纸的大小制作龙骨

纸长 942 像素,卷成圆筒,半径就是 149.9 像素,如果把半径视为1个单位,则高度 400 像素相当于 2.668 个单位。

>>> rows, cols, deep = im.shape
>>> cols/(2*np.pi)
149.9239563925654
>>> r = 1
>>> h = 2*np.pi*rows/cols
>>> h
2.6680192387174464

接下来需要制作半径1个单位、高度2.668个单位的圆筒状龙骨了。

>>> theta = np.linspace(0, 2*np.pi, cols)
>>> x = r * np.cos(theta)
>>> y = r * np.sin(theta)
>>> z = np.linspace(0, h, rows)
>>> xs = np.tile(x, (rows,1))
>>> ys = np.tile(y, (rows,1))
>>> zs = z.repeat(cols).reshape((rows,cols))

这里的 ​xs​、​ys​、​zs​ 就是圆筒状龙骨上各个点的x坐标、y坐标、z坐标。下面的代码,每隔10个点抽取1个点,用 ​mesh​ 的方法画出龙骨形状。当然,也可以画出全部的点,那样顶点就会连成一片。

>>> plt.mesh(xs[::10,::10], ys[::10,::10], zs[::10,::10], mode='FLBL')
>>> plt.show()

用3D的方式画出来的龙骨,效果如下。

3.4 给龙骨贴上花灯纸

有了龙骨,接下来就可以把花灯纸贴在龙骨上了。继续操作之前,记得先把刚才弹出的3D龙骨窗口关闭。

>>> plt.mesh(xs, ys, zs, im)
>>> plt.show()

不过,你会立刻发现,花灯纸上下方向贴反了。没关系,我们可以像下面这样反转方向。

>>> plt.mesh(xs, ys, zs, im[::-1])
>>> plt.show()

怎么样,是不是有一点走马灯的雏形了呢?

3.5 制作旋转叶轮

走马灯之所以能够转动,是因为里面有蜡烛加热形成上升气流,推动顶部的叶轮旋转,从而带动花灯旋转。当然,这里的叶轮仅仅是个样子,花灯旋转依赖另外的机制实现。

>>> theta = np.linspace(0, 2*np.pi, 18, endpoint=False)
>>> x = r * np.cos(theta)
>>> y = r * np.sin(theta)
>>> x[2::3] = x[1::3]
>>> x[1::3] = 0
>>> y[2::3] = y[1::3]
>>> y[1::3] = 0
>>> z = np.ones(18) * h * 0.9
>>> vs = np.stack((x,y,z), axis=1)
>>> plt.mesh(xs, ys, zs, im[::-1])
>>> plt.surface(vs, color='#C03000', method='T', mode='FCBC', alpha=0.8)
>>> plt.show()

叶轮设计有6片,用三角形模拟,颜色深红,透明度0.8,整体效果略显粗糙了一点。

3.6 加上照明灯和提系

照明灯用一个白色的圆球表示,提系则是红色的一条直线,兼做照明灯的电源线。

>>> plt.mesh(xs, ys, zs, im[::-1])
>>> plt.surface(vs, color='#C03000', method='T', mode='FCBC', alpha=0.8)
>>> plt.sphere((0,0,h*0.4), 0.4, '#FFFFFF', slices=60, mode='FCBC')
>>> plt.plot((0,0), (0,0), (0.4*h, 1.5*h), width=3.0, style='solid', cmap='hsv', caxis='z')

3.7 让花灯转起来

花灯旋转的实现非常简单,只需要给 ​show​ 方法一个 ​rotation​ 参数就可以。

plt.show(rotation='h-')

最终的花灯效果如下。

四、完整源代码

有了上面的解说,完整的源代码就不用注释了。全部代码三十余行,各位可自行扩展,制作出更多的花灯来。

# -*- coding: utf-8 -*-

import numpy as np
from PIL import Image
import wxgl.wxplot as plt

im = np.array(Image.open('res/paper.png'))/255
rows, cols, deep = im.shape

r, h = 1, 2*np.pi*rows/cols
theta = np.linspace(0, 2*np.pi, cols)
x = r*np.cos(theta)
y = r*np.sin(theta)
z = np.linspace(0, h, rows)
xs = np.tile(x, (rows,1))
ys = np.tile(y, (rows,1))
zs = z.repeat(cols).reshape((rows,cols))

theta = np.linspace(0, 2*np.pi, 18, endpoint=False)
x = r*np.cos(theta)
y = r*np.sin(theta)
x[2::3] = x[1::3]
x[1::3] = 0
y[2::3] = y[1::3]
y[1::3] = 0
z = np.ones(18) * h * 0.9
vs = np.stack((x,y,z), axis=1)

plt.mesh(xs, ys, zs, im[::-1])
plt.surface(vs, color='#C03000', method='T', mode='FCBC', alpha=0.8)
plt.sphere((0,0,h*0.4), 0.4, '#FFFFFF', slices=60, mode='FCBC')
plt.plot((0,0), (0,0), (0.4*h, 1.5*h), width=3.0, style='solid', cmap='hsv', caxis='z')
plt.show(rotation='h-')

推荐好课:python3入门python3进阶

来源:CSDN

作者:天元浪子

原标题:元宵节就要到了,花灯要不要来一盏?3D的那种

原文地址:https://xufive.blog.csdn.net/article/details/114058247

蚂蚁金服:开源增强版 SpringBoot 的研发框架!

thbcm阅读(177)

iness Check 能力的情况,SOFABoot 增加了 Spring Boot 现有的健康检查的能力,提供了 Readiness Check 的能力。利用 Readiness Check 的能力,SOFA 中间件中的各个组件只有在 Readiness Check 通过之后,才将流量引入到应用的实例中,比如 RPC,只有在 Readiness Check 通过之后,才会向服务注册中心注册,后面来自上游应用的流量才会进入。

提供类隔离的能力

为了解决 Spring Boot 下的类依赖冲突的问题,SOFABoot 基于 SOFAArk 提供了 Spring Boot 上的类隔离的能力,在一个 SOFABoot 的系统中,只要引入 SOFAArk 相关的依赖,就可以将 SOFA 中间件相关的类和应用相关的类的 ClassLoader 进行隔离,防止出现类冲突。当然,用户也可以基于 SOFAArk,将其他的中间件、第三方的依赖和应用的类进行隔离。

日志空间隔离能力

为了统一大规模微服务场景下的中间件日志的打印,SOFABoot 提供了日志空间隔离的能力给 SOFA 中间件,SOFA 中间件中的各个组件采用日志空间隔离的能力之后,自动就会将本身的日志和应用的普通日志隔离开来,并且打印的日志的路径也是相对固定,非常方便进行统一地监控。

SOFA 中间件的集成管理

基于 Spring Boot 的自动配置能力,SOFABoot 提供了 SOFA 中间件统一易用的编程接口以及 Spring Boot 的 Starter,方便在 Spring Boot 环境下使用 SOFA 中间件,SOFA 中间件中的各个组件都是独立可插拔的,节约开发时间,和后期维护的成本。

模块化开发

SOFABoot 从 2.4.0 版本开始支持基于 Spring 上下文隔离的模块化开发能力,每个 SOFABoot 模块使用独立的 Spring 上下文,避免不同 SOFABoot 模块间的 BeanId 冲突,有效降低企业级多模块开发时团队间的沟通成本。

项目地址

https://gitee.com/sofastack/sofa-boot

推荐好课:Spring Boot基础入门SpringBoot从入门到精通

关于javascript跳转与返回和刷新页面的问题

thbcm阅读(177)

javascript 中​window.open()​与​window.location.href​的区别

1、window.open(‘index.html’) 表示新增一个窗口打开 index.html 这个页面,并不刷新

location.href(‘index.html’) 表示在当前窗口重定向到新页面,打开并刷新 index.html 这个页面

2、window.location 是 window 对象的属性,用来替换当前页,也就是重新定位当前页

而 window.open 是 window 对象的方法,是用来打开一个新窗口的函数

// 打开新页面
// 注意:有些浏览器的安全设置会将window.open()屏蔽,例如避免弹出广告窗
window.open('./index.html');

// 在原窗口打开新页面
window.location.href="./index.html";

window.open()详解

window.open ('page.html', 'newwindow', 'height=100, width=400, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, resizable=no,location=n o, status=no')

参数解释: 三个参数

  • window.open 弹出新窗口的命令;
  • ‘page.html’ 弹出窗口的文件名;
  • ‘newPage’ 弹出窗口的名字(不是文件名),非必须,可用空’’代替;
  • height=100 窗口高度;
  • width=400 窗口宽度;
  • top=0 窗口距离屏幕上方的象素值;
  • left=0 窗口距离屏幕左侧的象素值;
  • toolbar=no 是否显示工具栏,yes 为显示;
  • menubar=no 是否显示菜单栏,yes 为显示;
  • scrollbars=no 是否显示滚动栏,yes 为显示;
  • resizable=no 是否允许改变窗口大小,yes 为允许;
  • location=no 是否显示地址栏,yes 为允许;
  • status=no 是否显示状态栏内的信息(通常是文件已经打开),yes为允许;

常用的 js 返回与刷新页面

在此用 a 标签举例

<a href="javascript: history.back(-1)">返回上一页</a> 
<a href="javascript:history.go(-1)">返回上一页</a> 
<a href="javascript:history.go(-2)">返回前两页</a> 
<a href="javascript:location.reload()">刷新当前页面</a> 
<a href="javascript:" onclick="self.location=document.referrer;">返回上一页并刷新</a> 

JavaScript

// 刷新当前页面
window.location.Reload();
self.location.reload();
window.location.href = window.location.href; 

为什么CTO、技术总监、架构师都不写代码,还这么厉害?

thbcm阅读(169)

常常会被问到这样的问题:CTO、技术总监、架构师很少写具体代码,为什么还很牛逼的样子,拿这么高工资?

其实,这个问题本身就错了。就好比问:导演、制片人为什么不懂演戏,还能指导演员,好像比演员厉害似的?其实不难理解,导演、制片人的核心能力并不是演戏,又怎么能跟演员作比较呢?

回答前面的问题,逻辑也是一样的,拿 CTO、技术总监、架构师,跟程序员比写代码的能力,本身就是个错误。因为,他们的核心能力是不一样的。

CTO、技术总监、架构师的核心能力是技术判断力。简单来讲,就是判断一个项目、一个系统架构、某个技术方向,是否符合企业当前现状,是否对企业的未来产生价值。

程序员的核心能力是写代码的能力。就是做具体的代码实现。

所以 CTO/技术总监/架构师,跟程序员的核心能力,是完全不一样的能力,是没法作直接比较的。

通常我们说,CTO、技术总监、架构师们很牛逼,指的是他们的技术判断力牛逼,而不是他们写代码的能力牛逼。相反,他们写代码的能力可能还比不上一个资深程序员。

但是,他们所做的技术判断,给公司带来非常高的价值。比如,阿里云创始人王坚博士,在所有人反对的时候,他坚持云计算是未来,帮助阿里提前布局云计算,为阿里成长为万亿商业帝国,立下汗马功劳。这就是技术判断力,给企业带来的巨大价值。

大家之所以都很容易混淆这几个角色,以及它们的职责,其实很重要的一个原因就是,人们常常把 CTO、架构师、技术经理的头衔,乱授予技术负责人。在国内这种现象尤其严重。

比如,一个初创公司的技术负责人,实际干的活就是个技术经理。老板为了显得高大上,硬要给他安一个 CTO 的头衔。这种情况很普遍,特别是全民创业的那几年,遍地都是 CTO。

为了说清楚技术负责人的职责,我们以一个电商公司的成长为例,讲解企业在初创期、发展期、成熟期的不同阶段,都需要什么样的技术负责人,以及他们具体都干些什么。

第一阶段,高级程序员

实现复杂功能,解决技术难题

一个刚刚起步的创业公司,通常只有几个程序员,甚至连产品经理、项目经理都没有,老板自己就是产品经理,把想法跟开发人员一说,就快速地做出原型。

如果这个阶段对开发的能力不满,那么大概需要的只是一个高级开发人员,他能搞定一般的技术难题,实现复杂功能,思路清晰、干活利索。千万不要去大厂挖个技术总监,你家庙太小,供不起这么大的神,他真来了也发挥不了应有的作用。

老读者知道,老K有过一段创业经历,当时追随我的老领导出来创业,我就是名义上的 CTO,带了7、8人的团队,我还同时带了两个项目,每个项目里我都贡献了30%以上的代码量。其实,当时的我,就是个高级程序员而已。

小结一下,高级程序员的主要职责是:

1,实现复杂功能,编写核心代码;

2,处理线上bug,解决技术难题。

第二阶段,技术经理

交付效率提高、质量提升

当公司的业务发展起来后,就需要一支相对完善的技术团队,有了专职产品经理、测试人员等,团队规模在15人左右,专注于一条产品线。

复杂功能、技术难题,高级开发人员可以搞定,但是如果要解决开发团队效率、技术人员能力提升、代码质量和编码规范等,就需要技术经理了。技术经理通常写少量的代码,更多做技术管理、项目团队等工作。

这就是许多创业公司A轮融资前的情况。由技术经理总体负责技术团队,产品经理对接业务需求,做产品规划、竞品分析,而不是抄袭哪个App。

小结一下,技术经理的职责是:

1、开发任务分派。开发工作量评估、分派,最大化资源利用率;

2、代码质量提升。Code Review、编码规范、线上bug分析;

3、项目管理。确保项目的按时交付,建立管理机制;

4、团队管理。团队搭建、人员招聘、人员培养。

第三阶段,技术总监

技术规划、多产品线、项目群管理

当技术团队发展到30人左右,有了多条核心产品线、有了多个技术经理时,就需要一个技术总监了。

技术总监,作为领域专家,站在更高的层面思考技术如何建立壁垒,构建技术竞争力。逐步开始建立公共技术平台,协调多条产品线在统一的技术平台上快速迭代,让产品线跑得快、跑得稳。

技术总监,在领域内有多年沉淀,来自知名互联网企业,能够把技术团队带上一个新的台阶。技术总监,更多是做技术判断了,也有些技术型的技术总监仍然会写些核心代码、做架构设计。

技术总监的职责:

1、搭建公司技术平台部,统一技术栈;

2、建立产品研发体系,让技术团队可持续性地快速交付;

3、管理和协调多条产品线,打造明星产品;

4、建立技术壁垒,形成技术竞争力;

第四阶段,架构师

架构设计、架构实现、架构评审

公司如果“跑到”了B轮,技术团队应该要接近百人了,此时的技术团队跟初创时期相比,已经很不错了。

有技术总监协调着各产品线,有开发经理带领技术团队快速迭代产品。代码规范、最佳实践的总结和推广也在逐步开展。

此时,需要把架构规划和架构评审的职能从技术总监和开发经理身上剥离,即分离专业岗和管理岗,专业人做专业事。

这时候就需要设立架构师岗位,专注于技术架构分析、架构设计、架构实现、推动重构、推行架构原则等工作,让技术总监和技术经理侧重在项目管理、团队管理。

架构师的职责是:

1、业务架构设计和实现。根据业务规划和应用场景,设计切合当前业务要求,并且具备一定前瞻性的应用架构、类、接口、业务抽象及业务建模等。

2、架构设计和实现。识别非功能性需求,如性能、可扩展性、安全性、高可用及易部署等。

3、重构计划及执行。关注全链路监控数据、线上bug、系统预警等信息,识别架构缺陷,提出重构建议并推动执行。

第五阶段,CTO

技术产品战略规划,提升技术竞争力

当技术团队有了几名总监、架构师,人数达到几百人,是时候引入真正意义上的 CTO 了,除非 CTO 是联合创始人,否则这个 CTO 会有“虎落平阳”的感觉,公司也会觉得这个人“满嘴跑火车,却落不了地”。

国内的中大型互联网公司,一般有产品 VP 和技术 VP,有的技术 VP 就是 CTO。如果 CTO 统管技术和产品,那么产品 VP 就给 CTO 汇报,否则他们是平级的。

在国外,CTO 主要研究 3~5年的技术发展趋势,为公司做中长期的技术规划,是具有行业影响力的技术大咖,公司技术领域的精神领袖。CTO 较少关注当下的具体事务,这类工作主要由工程副总裁们处理。

以国内互联网公司CTO为例,总结一下CTO的主要职责:

1、技术赋能商业。敏锐的商业洞察、深入的产业研究、参与公司战略规划,技术引领业务增长,通过技术和产品实现战略落地。

2、技术趋势研究。思考未来3~5年的技术发展趋势,以及新技术发展给企业带来的机遇和风险,为企业提前布局。

3、技术治理体系。持续的过程改进、高效的研发流程、稳定的交付质量、高可用的系统。

4、组织与文化。建设学习型组织、自我完善型组织,建立符合企业特色的文化氛围。

结语

最后,不想当 CTO 的程序员,不是好骑手。从程序员到 CTO 的成长过程,需要不断提升技术能力、产品能力、项目能力、管理能力、商业视野、个人影响力、行业人脉等等。除了自身的奋斗之外,机会和运气同样重要,而且是可遇不可求的。但是,梦想还是要有的,万一见鬼了呢。

推荐适合快速成长的好课程:

JavaScript 游戏开发:手把手实现碰撞物理引擎

thbcm阅读(161)

v1nAfter 和 v2nAfter 分别是两小球碰撞后的速度,现在可以先判断一下,如果 v1nAfter 小于 v2nAfter,那么第 1 个小球和第 2 个小球会越来越远,此时不用处理碰撞:年前我看到合成大西瓜小游戏火了,想到之前从来没有研究过游戏方面的开发,这次就想趁着这个机会看看 JavaScript 游戏开发,从原生角度上如何实现游戏里的物理特性,例如运动、碰撞。虽然之前研究过物理相关的动画库,但是我打算试试不用框架编写一个简单的 JavaScript 物理引擎,实现小球的碰撞效果。

为什么不用现成的游戏库呢?因为我觉得在了解底层的实现原理之后,才能更有效的理解框架上的概念和使用方法,在解决 BUG 的时候能够更有效率,同时对自己的编码技能也是一种提升。在对 JavaScript 物理引擎的研究过程中,发现写代码是次要的,最主要的是理解相关的物理、数学公式和概念,虽然我是理科生,但是数学和物理从来不是我的强项,我不是把知识还给老师了,而是压根就没掌握过。过年期间花了有小半个月的时间在学习物理知识,现在仍然对某些概念和推导过程没有太大的自信,不过最后还算是做出了一个简单的、比较满意的结果,见下图。

接下来看一下怎么实现这样的效果。

基础结构

我们这里使用 canvas 来实现 JavaScript 物理引擎。首先准备项目的基础文件和样式,新建一个 index.html、index.js 和 style.css 文件,分别用于编写 canvas 的 html 结构、引擎代码和画布样式。

在 index.html 的 ​<head /> ​标签中引入样式文件:

<link rel="stylesheet" href="./style.css" />

在 <body /> 中,添加 canvas 元素、加载 index.js 文件:

<main>
  <canvas id="gameboard"></canvas>
</main>
<script src="./index.js"></script>

这段代码定义了​ id​ 为 ​gameboard​ 的 ​<canvas /> ​元素,并放在了 ​<main />​ 元素下, ​<main />​ 元素主要是用来设置背景色和画布大小。在 ​<main/>​ 元素的下方引入 index.js 文件,这样可以在 DOM 加载完成之后再执行 JS 中的代码。

style.css 中的代码如下:

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  font-family: sans-serif;
}

main {
  width: 100vw;
  height: 100vh;
  background: hsl(0deg, 0%, 10%);
}

样式很简单,去掉所有元素的外边距、内间距,并把 ​<main/>​ 元素的宽高设置为与浏览器可视区域相同,背景色为深灰色。

hsl(hue, saturation, brightness) 为 css 颜色表示法之一,参数分别为色相,饱和度和亮度。

绘制小球

接下来绘制小球,主要用到了 canvas 相关的 api。

在 index.js 中,编写如下代码:

const canvas = document.getElementById("gameboard");
const ctx = canvas.getContext("2d");

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

let width = canvas.width;
let height = canvas.height;

ctx.fillStyle = "hsl(170, 100%, 50%)";
ctx.beginPath();
ctx.arc(100, 100, 60, 0, 2 * Math.PI);
ctx.fill();

代码中主要利用了二维 context 进行绘图操作:

  • 通过 canvas 的 id 获取 canvas 元素对象。
  • 通过 canvas 元素对象获取绘图 context, ​getContext()​ 需要一个参数,用于表明是绘制 2d 图像,还是使用 webgl 绘制 3d 图象,这里选择 2d。context 就类似是一支画笔,可以改变它的颜色和绘制基本的形状。
  • 给 canvas 的宽高设置为浏览器可视区域的宽高,并保存到 ​width​ 和 ​height​ 变量中方便后续使用。
  • 给 context 设置颜色,然后调用 ​beginPath()​ 开始绘图。
  • 使用 ​arc()​ 方法绘制圆形,它接收 5 个参数,前两个为圆心的 x、y 坐标,第 3 个为半径长度, 第 4 个和第 5 个分别是起始角度和结束角度,因为 ​arc()​ 其实是用来绘制一段圆弧,这里让它画一段 0 到 360 度的圆弧,就形成了一个圆形。这里的角度是使用 radian 形式表示的,0 到 360 度可以用 0 到 2 * Math.PI 来表示。
  • 最后使用 ​ctx.fill()​ 给圆形填上颜色。

这样就成功的绘制了一个圆形,我们在这把它当作一个小球:

移动小球

不过,这个时候的小球还是静止的,如果想让它移动,那么得修改它的圆心坐标,具体修改的数值则与运动速度有关。在移动小球之前,先看一下 canvas 进行动画的原理:

Canvas 进行动画的原理与传统的电影胶片类似,在一段时间内,绘制图像、更新图像位置或形状、清除画布,重新绘制图像,当在 1 秒内连续执行 60 次或以上这样的操作时,即以 60 帧的速度,就可以产生连续的画面。

那么在 JavaScript 中,浏览器提供了 ​window.requestAnimationFrame()​ 方法,它接收一个回调函数作为参数,每一次执行回调函数就相当于 1 帧动画,我们需要通过递归或循环连续调用它,浏览器会尽可能的在 1 秒内执行 60 次回调函数。那么利用它,我们就可以对 canvas 进行重绘,以实现小球的移动效果。

由于 ​window.requestAnimationFrame() ​的调用基本是持续进行的,所以我们也可以把它称为游戏循环(Game loop)。

接下来我们来看如何编写动画的基础结构:

function process() {
  window.requestAnimationFrame(process);
}
window.requestAnimationFrame(process);

这里的 ​process() ​函数就是 1 秒钟要执行 60 次的回调函数,每次执行完毕后继续调用 ​window.requestAnimationFrame(process)​进行下一次循环。如果要移动小球,那么就需要把绘制小球和修改圆心 x、y 坐标的代码写到 ​process()​ 函数中。

为了方便更新坐标,我们把小球的圆心坐标保存到变量中,以方便对它们进行修改,然后再定义两个新的变量,分别表示在 x 轴方向上的速度​vx​,和 y 轴方向上的速度 ​vy​,然后把 context 相关的绘图操作放到 ​process() ​中:

let x = 100;
let y = 100;
let vx = 12;
let vy = 25;

function process() {
  ctx.fillStyle = "hsl(170, 100%, 50%)";
  ctx.beginPath();
  ctx.arc(x, y, 60, 0, 2 * Math.PI);
  ctx.fill();
  window.requestAnimationFrame(process);
}
window.requestAnimationFrame(process);

要计算圆心坐标 x、y 的移动距离,我们需要速度和时间,速度这里有了, 那么时间要怎么获取呢? ​window.requestAnimationFrame() ​会把当前时间的毫秒数(即时间戳)传递给回调函数,我们可以把本次调用的时间戳保存起来,然后在下一次调用时计算出执行这 1 帧动画消耗了多少秒,然后根据这个秒数和 x、y 轴方向上的速度去计算移动距离,分别加到 x 和 y 上,以获得最新的位置。注意这里的时间是上一次函数调用和本次函数调用的时间间隔,并不是第 1 次函数调用到当前函数调用总共过去了多少秒,所以相当于是时间增量,需要在之前 x 和 y 的值的基础上进行相加,代码如下:

let startTime;

function process(now) {
  if (!startTime) {
    startTime = now;
  }
  let seconds = (now - startTime) / 1000;
  startTime = now;

  // 更新位置
  x += vx * seconds;
  y += vy * seconds;

  // 清除画布
  ctx.clearRect(0, 0, width, height);
  // 绘制小球
  ctx.fillStyle = "hsl(170, 100%, 50%)";
  ctx.beginPath();
  ctx.arc(x, y, 60, 0, 2 * Math.PI);
  ctx.fill();

  window.requestAnimationFrame(process);
}


    

process() ​现在接收当前时间戳作为参数,然后做了下面这些操作:

  • 计算上次函数调用与本次函数调用的时间间隔,以秒计,记录本次调用的时间戳用于下一次计算。
  • 根据 x、y 方向上的速度,和刚刚计算出来的时间,计算出移动距离。
  • 调用 ​clearRect()​ 清除矩形区域画布,这里的参数,前两个是左上角坐标,后两个是宽高,把 canvas 的宽高传进去就会把整个画布清除。
  • 重新绘制小球。

现在小球就可以移动了:

重构代码

上边的代码适合只有一个小球的情况,如果有多个小球需要绘制,就得编写大量重复的代码,这时我们可以把小球抽象成一个类,里边有绘图、更新位置等操作,还有坐标、速度、半径等属性,重构后的代码如下:

class Circle {
  constructor(context, x, y, r, vx, vy) {
    this.context = context;
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = vx;
    this.vy = vy;
  }
  
    // 绘制小球
  draw() {
    this.context.fillStyle = "hsl(170, 100%, 50%)";
    this.context.beginPath();
    this.context.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
    this.context.fill();
  }

  /**
   * 更新画布
   * @param {number} seconds
   */
  update(seconds) {
    this.x += this.vx * seconds;
    this.y += this.vy * seconds;
  }
}

里边的代码跟之前的一样,这里就不再赘述了,需要注意的是,Circle 类的 context 画笔属性是通过构造函数传递进来的,更新位置的代码放到了 ​update() ​方法中。

对于整个 canvas 的绘制过程,也可以抽象成一个类,当作是游戏或引擎控制器,例如把它放到一个叫 ​Gameboard​ 的类中:

class Gameboard {
  constructor() {
    this.startTime;
    this.init();
  }

  init() {
    this.circles = [
      new Circle(ctx, 100, 100, 60, 12, 25),
      new Circle(ctx, 180, 180, 30, 70, 45),
    ];
    window.requestAnimationFrame(this.process.bind(this));
  }

  process(now) {
    if (!this.startTime) {
      this.startTime = now;
    }
    let seconds = (now - this.startTime) / 1000;
    this.startTime = now;

    for (let i = 0; i < this.circles.length; i++) {
      this.circles[i].update(seconds);
    }
    ctx.clearRect(0, 0, width, height);

    for (let i = 0; i < this.circles.length; i++) {
      this.circles[i].draw(ctx);
    }
    window.requestAnimationFrame(this.process.bind(this));
  }
}

new Gameboard();

在 Gameboard 类中:

  • startTime​ 保存了上次函数执行的时间戳的属性,放到了构造函数中。
  • init()​ 方法创建了一个 ​circles​ 数组,里边放了两个示例的小球,这里先不涉及碰撞问题。然后调用 ​window.requestAnimationFrame()​ 开启动画。注意这里使用了 ​bind()​ 来把 ​Gameboard​ 的 this 绑定到回调函数中,以便于访问 ​Gameboard​ 中的方法和属性。
  • process()​ 方法也写到了这里边,每次执行时会遍历小球数组,对每个小球进行位置更新,然后清除画布,再重新绘制每个小球。
  • 最后初始化 ​Gameboard​ 对象就可以开始执行动画了。

这个时候有两个小球在移动了。

碰撞检测

为了实现仿真的物理特性,多个物体间碰撞会有相应的反应,第一步就是要先检测碰撞。我们先再多加几个小球,以便于碰撞的发生,在 Gameboard 类的​ init()​ 方法中再添加几个小球:

this.circles = [
  new Circle(ctx, 30, 50, 30, -100, 390),
  new Circle(ctx, 60, 180, 20, 180, -275),
  new Circle(ctx, 120, 100, 60, 120, 262),
  new Circle(ctx, 150, 180, 10, -130, 138),
  new Circle(ctx, 190, 210, 10, 138, -280),
  new Circle(ctx, 220, 240, 10, 142, 350),
  new Circle(ctx, 100, 260, 10, 135, -460),
  new Circle(ctx, 120, 285, 10, -165, 370),
  new Circle(ctx, 140, 290, 10, 125, 230),
  new Circle(ctx, 160, 380, 10, -175, -180),
  new Circle(ctx, 180, 310, 10, 115, 440),
  new Circle(ctx, 100, 310, 10, -195, -325),
  new Circle(ctx, 60, 150, 10, -138, 420),
  new Circle(ctx, 70, 430, 45, 135, -230),
  new Circle(ctx, 250, 290, 40, -140, 335),
];

然后给小球添加一个碰撞状态,在碰撞时,给两个小球设置为不同的颜色:

class Circle {
  constructor(context, x, y, r, vx, vy) {
    // 其它代码
    this.colliding = false;
  }
  draw() {
    this.context.fillStyle = this.colliding
      ? "hsl(300, 100%, 70%)"
      : "hsl(170, 100%, 50%)";
    // 其它代码
  }
}

现在来判断小球之间是否发生了碰撞,这个条件很简单,判断两个小球圆心的距离是否小于两个小球的半径之和就可以了,如果小于等于则发生了碰撞,大于则没有发生碰撞。圆心的距离即计算两个坐标点的距离,可以用公式:

x1、y1 和 x2、y2 分别两个小球的圆心坐标。在比较时,可以对半径和进行平方运算,进而省略对距离的开方运算,也就是可以用下方的公式进行比较:

r1 和 r2 为两球的半径。

在 Circle 类中,先添加一个​isCircleCollided(other)​方法,接收另一个小球对象作为参数,返回比较结果:

isCircleCollided(other) {
  let squareDistance =
      (this.x - other.x) * (this.x - other.x) +
      (this.y - other.y) * (this.y - other.y);
  let squareRadius = (this.r + other.r) * (this.r + other.r);
  return squareDistance <= squareRadius;
}

再添加 checkCollideWith(other) 方法,调用 isCircleCollided(other) 判断碰撞后,把两球的碰撞状态设置为 true:

checkCollideWith(other) {
  if (this.isCircleCollided(other)) {
    this.colliding = true;
    other.colliding = true;
  }
}

接着我们需要使用双循环两两比对小球是否发生了碰撞,由于小球数组存放在 Gameboard 对象中,我们给它添加一个 ​checkCollision()​ 方法来检测碰撞:

checkCollision() {
  // 重置碰撞状态
  this.circles.forEach((circle) => (circle.colliding = false));

  for (let i = 0; i < this.circles.length; i++) {
    for (let j = i + 1; j < this.circles.length; j++) {
      this.circles[i].checkCollideWith(this.circles[j]);
    }
  }
}

因为小球在碰撞后就应立即弹开,所以我们一开始要把所有小球的碰撞状态设置为 false,之后在循环中,对每个小球进行检测。这里注意到内层循环是从 i + 1 开始的,这是因为在判断 1 球和 2 球是否碰撞后,就无须再判断 2 球 和 1 球了。

之后在​ process()​ 方法中,执行检测,注意检测应该发生在使用 for 循环更新小球位置的后边才准确:

for (let i = 0; i < this.circles.length; i++) {
  this.circles[i].update(seconds);
}
this.checkCollision();

现在,可以看到小球在碰撞时,会改变颜色了。

边界碰撞

上边的代码在执行之后,小球都会穿过边界跑到外边去,那么我们先处理一下边界碰撞的问题。检测边界碰撞需要把四个面全部都处理到,根据圆心坐标和半径来判断是否和边界发生了碰撞。例如跟左边界发生碰撞时,圆心的 x 坐标是小于或等于半径长度的,而跟右边界发生碰撞时,圆心 x 坐标应该大于或等于画布最右侧坐标(即宽度值)减去半径的长度。上边界和下边界类似,只是使用圆心 y 坐标和画布的高度值。在水平方向上(即左右边界)发生碰撞时,小球的运动方向发生改变,只需要把垂直方向上的速度 vy 值取反即可,在垂直方向上碰撞则把 vx 取反。

现在看一下代码的实现,在 Gameboard 类中添加一个 checkEdgeCollision() 方法,根据上边描述的规则编写如下代码:

checkEdgeCollision() {
  this.circles.forEach((circle) => {
    // 左右墙壁碰撞
    if (circle.x < circle.r) {
      circle.vx = -circle.vx;
      circle.x = circle.r;
    } else if (circle.x > width - circle.r) {
      circle.vx = -circle.vx;
      circle.x = width - circle.r;
    }

    // 上下墙壁碰撞
    if (circle.y < circle.r) {
      circle.vy = -circle.vy;
      circle.y = circle.r;
    } else if (circle.y > height - circle.r) {
      circle.vy = -circle.vy;
      circle.y = height - circle.r;
    }
  });
}

在代码中,碰撞时,除了对速度进行取反操作之外,还把小球的坐标修改为紧临边界,防止超出。接下来在 process() 中添加对边界碰撞的检测:

this.checkEdgeCollision();
this.checkCollision();

这时候可以看到小球在碰到边界时,可以反弹了:

但是小球间的碰撞还没有处理,在处理之前,先复习一下向量的基本操作,数学好的同学可以直接跳过,只看相关的代码。

向量的基本操作

由于在碰撞时,需要对速度向量(或称为矢量)进行操作,向量是使用类似坐标的形式表示的,例如 < 3, 5 > (这里用 <> 表示向量),它有长度和方向,对于它的运算有一定的规则,本教程中需要用到向量的加法、减法、乘法、点乘和标准化操作。

向量相加只需要把两个向量的 x 坐标和 y 坐标相加即可,例如:< 3 , 5 > + < 1 , 2 > = < 4 , 7 > <3, 5> + <1, 2> = <4, 7><3,5>+<1,2>=<4,7>

减法与加法类似,把 x 坐标和 y 坐标相减,例如:< 3 , 5 > − < 1 , 2 > = < 2 , 3 > <3, 5> – <1, 2> = <2, 3><3,5>−<1,2>=<2,3>

乘法,这里指的是向量和标量的乘法,标量指的就是普通的数字,结果是把 x 和 y 分别和标量相乘,例如:3 × < 3 , 5 > = < 9 , 15 > 3\times<3, 5> = <9, 15>3×<3,5>=<9,15>。

点乘是两个向量相乘的一种方式,类似的还有叉乘,但是在本示例中用不到,点乘其实计算的是一个向量在另一个向量上的投影,它的计算方式为两个向量的 x 的积加上 y 的积,它返回的是一个标量,即第 1 个向量在第 2 个向量上投影的长度,例如:< 3 , 5 > ⋅ < 1 , 2 > = 3 × 1 + 5 × 2 = 13 <3, 5> \cdot <1, 2> = 3 \times 1 + 5 \times 2 = 13<3,5>⋅<1,2>=3×1+5×2=13

标准化是除掉向量的长度,只剩下方向,这样的向量它的长度为 1,称为单位向量,标准化的过程是让 x 和 y 分别除以向量的长度,因为向量表示的是和原点(0, 0)的距离,所以可以直接使用  计算长度,例如 < 3, 4 > 标准化后的结果为:< 3 , 5 > ⋅ < 1 , 2 > = 3 × 1 + 5 × 2 = 13 <3, 5> \cdot <1, 2> = 3 \times 1 + 5 \times 2 = 13<3,5>⋅<1,2>=3×1+5×2=13。

了解了向量的基本运算后,我们来创建一个 Vector 工具类,来方便我们进行向量的运算,它的代码就是实现了这些运算规则:

class Vector {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  /**
   * 向量加法
   * @param {Vector} v
   */
  add(v) {
    return new Vector(this.x + v.x, this.y + v.y);
  }

  /**
   * 向量减法
   * @param {Vector} v
   */
  substract(v) {
    return new Vector(this.x - v.x, this.y - v.y);
  }

  /**
   * 向量与标量乘法
   * @param {Vector} s
   */
  multiply(s) {
    return new Vector(this.x * s, this.y * s);
  }

  /**
   * 向量与向量点乘(投影)
   * @param {Vector} v
   */
  dot(v) {
    return this.x * v.x + this.y * v.y;
  }

  /**
   * 向量标准化(除去长度)
   * @param {number} distance
   */
  normalize() {
    let distance = Math.sqrt(this.x * this.x + this.y * this.y);
    return new Vector(this.x / distance, this.y / distance);
  }
}

代码中没有什么特殊的语法和操作,这里就不再赘述了,接下来我们看一下小球的碰撞问题。

碰撞处理

碰撞处理最主要的部分就是计算碰撞后的速度和方向。通常最简单的碰撞问题是在同一个水平面上的两个物体的碰撞,称为一维碰撞,因为此时只需要计算同一方向上的速度,而我们现在的程序小球是在一个二维平面内运动的,小球之间发生正面相碰(即在同一运动方向)的概率很小,大部分是斜碰(在不同运动方向上擦肩相碰),需要同时计算水平和垂直方向上的速度和方向,这就属于是二维碰撞问题。不过,其实小球之间的碰撞,只有在连心线(两个圆心的连线)上有作用力,而在碰撞接触的切线方向上没有作用力,那么我们只需要知道连心线方向的速度变化就可以了,这样就转换成了一维碰撞。

计算碰撞后的速度时,遵守动量守恒定律和动能守恒定律,公式分别为:

动量守恒定律

动能守恒定律

m1、m2 分别为两小球的质量,v1 和 v2 为两小球碰撞前的速度向量,v1′ 和 v2′ 为碰撞后的速度向量。根据这两个公式可以推导出两小球碰撞后的速度公式:

如果不考虑小球的质量,或质量相同,其实就是两小球速度互换,即:

这里我们给小球加上质量,然后套用公式来计算小球碰撞后速度,先在 Circle 类中给小球加上质量 mass 属性:

class Circle {
  constructor(context, x, y, r, vx, vy, mass = 1) {
    // 其它代码
    this.mass = mass;
  }
}

然后在 Gameboard 类的初始化小球处,给每个小球添加质量:

this.circles = [
  new Circle(ctx, 30, 50, 30, -100, 390, 30),
  new Circle(ctx, 60, 180, 20, 180, -275, 20),
  new Circle(ctx, 120, 100, 60, 120, 262, 100),
  new Circle(ctx, 150, 180, 10, -130, 138, 10),
  new Circle(ctx, 190, 210, 10, 138, -280, 10),
  new Circle(ctx, 220, 240, 10, 142, 350, 10),
  new Circle(ctx, 100, 260, 10, 135, -460, 10),
  new Circle(ctx, 120, 285, 10, -165, 370, 10),
  new Circle(ctx, 140, 290, 10, 125, 230, 10),
  new Circle(ctx, 160, 380, 10, -175, -180, 10),
  new Circle(ctx, 180, 310, 10, 115, 440, 10),
  new Circle(ctx, 100, 310, 10, -195, -325, 10),
  new Circle(ctx, 60, 150, 10, -138, 420, 10),
  new Circle(ctx, 70, 430, 45, 135, -230, 45),
  new Circle(ctx, 250, 290, 40, -140, 335, 40),
];

在 Circle 类中加上 ​changeVelocityAndDirection(other)​ 方法来计算碰撞后的速度,它接收另一个小球对象作为参数,同时计算这两个小球碰撞厚的速度和方向,这个是整个引擎的核心,我们一点一点的来看它是如何实现的。首先把两个小球的速度使用 Vector 向量来表示:

  changeVelocityAndDirection(other) {
    // 创建两小球的速度向量
    let velocity1 = new Vector(this.vx, this.vy);
    let velocity2 = new Vector(other.vx, other.vy);
  }

因为我们本身就已经使用 vx 和 vy 来表示水平和垂直方向上的速度向量了,所以直接把它们传给 Vector 的构造函数就可以了。​velocity1​ 和 ​velocity2​ 分别代表当前小球和碰撞小球的速度向量。

接下来获取连心线方向的向量,也就是两个圆心坐标的差:

let vNorm = new Vector(this.x - other.x, this.y - other.y);

接下来获取连心线方向的单位向量和切线方向上的单位向量,这些单位向量代表的是连心线和切线的方向:

let unitVNorm = vNorm.normalize();
let unitVTan = new Vector(-unitVNorm.y, unitVNorm.x);

unitVNorm 是连心线方向单位向量,unitVTan 是切线方向单位向量,切线方向其实就是把连心线向量的 x、y 坐标互换,并把 y 坐标取反。根据这两个单位向量,使用点乘计算小球速度在这两个方向上的投影:

let v1n = velocity1.dot(unitVNorm);
let v1t = velocity1.dot(unitVTan);

let v2n = velocity2.dot(unitVNorm);
let v2t = velocity2.dot(unitVTan);

计算结果是一个标量,也就是没有方向的速度值。v1n 和 v1t 表示当前小球在连心线和切线方向的速度值,v2n 和 v2t 则表示的是碰撞小球 的速度值。在计算出两小球的速度值之后,我们就有了碰撞后的速度公式所需要的变量值了,直接用代码把公式套用进去:

let v1nAfter = (v1n * (this.mass - other.mass) + 2 * other.mass * v2n) / (this.mass + other.mass);
let v2nAfter = (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / (this.mass + other.mass);

v1nAfter 和 v2nAfter 分别是两小球碰撞后的速度,现在可以先判断一下,如果 v1nAfter 小于 v2nAfter,那么第 1 个小球和第 2 个小球会越来越远,此时不用处理碰撞:

if (v1nAfter < v2nAfter) {
  return;
}

然后再给碰撞后的速度加上方向,计算在连心线方向和切线方向上的速度,只需要让速度标量跟连心线单位向量和切线单位向量相乘:

let v1VectorNorm = unitVNorm.multiply(v1nAfter);
let v1VectorTan = unitVTan.multiply(v1t);

let v2VectorNorm = unitVNorm.multiply(v2nAfter);
let v2VectorTan = unitVTan.multiply(v2t);

这样有了两个小球连心线上的新速度向量和切线方向上的新速度向量,最后把连心线上的速度向量和切线方向的速度向量进行加法操作,就能获得碰撞后小球的速度向量:

let velocity1After = v1VectorNorm.add(v1VectorTan);
let velocity2After = v2VectorNorm.add(v2VectorTan);

之后我们把向量中的 x 和 y 分别还原到小球的 vx 和 vy 属性中:

this.vx = velocity1After.x;
this.vy = velocity1After.y;

other.vx = velocity2After.x;
other.vy = velocity2After.y;

最后在 checkCollideWith() 方法的 if 语句中调用此方法,即在检测到碰撞时:

checkCollideWith(other) {
  if (this.isCircleCollided(other)) {
    this.colliding = true;
    other.colliding = true;
    this.changeVelocityAndDirection(other); // 在这里调用
  }
}

这时,小球的碰撞效果就实现了。

非弹性碰撞

现在小球之间的碰撞属于完全弹性碰撞,即碰撞之后不会有能量损失,这样小球永远不会停止运动,我们可以让小球在碰撞之后损失一点能量,来模拟更真实的物理效果。要让小球碰撞后有能量损失,可以使用恢复系数,它是一个取值范围为 0 到 1 的数值,每次碰撞后,乘以它就可以减慢速度,如果恢复系数为 1 则为完全弹性碰撞,为 0 则是完全非弹性碰撞,之间的数值为非弹性碰撞,现实生活中的碰撞都是非弹性碰撞。

先看一下边界碰撞,这个比较简单,假设边界的恢复系数为 0.8,然后在每次对速度取反的时候乘以它就可以了,把 Gameboard ​checkEdgeCollision()​方法作如下改动:

  checkEdgeCollision() {
    const cor = 0.8;                  // 设置恢复系统
    this.circles.forEach((circle) => {
      // 左右墙壁碰撞
      if (circle.x < circle.r) {
        circle.vx = -circle.vx * cor; // 加上恢复系数
        circle.x = circle.r;
      } else if (circle.x > width - circle.r) {
        circle.vx = -circle.vx * cor; // 加上恢复系数
        circle.x = width - circle.r;
      }

      // 上下墙壁碰撞
      if (circle.y < circle.r) {
        circle.vy = -circle.vy * cor; // 加上恢复系数
        circle.y = circle.r;
      } else if (circle.y > height - circle.r) {
        circle.vy = -circle.vy * cor; // 加上恢复系数
        circle.y = height - circle.r;
      }
    });
  }

接下来设置小球的恢复系数,给 Circle 类再加上一个恢复系数 cor 属性,每个小球可以设置不同的数值,来让它们有不同的弹性,然后在初始化小球时设置随意的恢复系数:

class Circle {
  constructor(context, x, y, r, vx, vy, mass = 1, cor = 1) {
    // 其它代码
    this.cor = cor;
  }
}

class Gameboard {
  init() {
   this.circles = [
      new Circle(ctx, 30, 50, 30, -100, 390, 30, 0.7),
      new Circle(ctx, 60, 180, 20, 180, -275, 20, 0.7),
      new Circle(ctx, 120, 100, 60, 120, 262, 100, 0.3),
      new Circle(ctx, 150, 180, 10, -130, 138, 10, 0.7),
      new Circle(ctx, 190, 210, 10, 138, -280, 10, 0.7),
      new Circle(ctx, 220, 240, 10, 142, 350, 10, 0.7),
      new Circle(ctx, 100, 260, 10, 135, -460, 10, 0.7),
      new Circle(ctx, 120, 285, 10, -165, 370, 10, 0.7),
      new Circle(ctx, 140, 290, 10, 125, 230, 10, 0.7),
      new Circle(ctx, 160, 380, 10, -175, -180, 10, 0.7),
      new Circle(ctx, 180, 310, 10, 115, 440, 10, 0.7),
      new Circle(ctx, 100, 310, 10, -195, -325, 10, 0.7),
      new Circle(ctx, 60, 150, 10, -138, 420, 10, 0.7),
      new Circle(ctx, 70, 430, 45, 135, -230, 45, 0.7),
      new Circle(ctx, 250, 290, 40, -140, 335, 40, 0.7),
    ];
  }
}

加上恢复系数之后,小球碰撞后的速度计算也需要改变一下,可以简单的让 v1nAfter 和 v2nAfter 乘以小球的恢复系数,也可以使用带有恢复系数的速度公式(这两种方式我暂时还不太清楚区别,有兴趣的小伙伴可以自己研究一下),公式如下:

接着把公式转换为代码,在 Circle 类的 changeVelocityAndDirection() 方法中,替换掉 v1nAfter 和 v2nAfter 的计算公式:

let cor = Math.min(this.cor, other.cor);
let v1nAfter =
    (this.mass * v1n + other.mass * v2n + cor * other.mass * (v2n - v1n)) /
    (this.mass + other.mass);

let v2nAfter =
    (this.mass * v1n + other.mass * v2n + cor * this.mass * (v1n - v2n)) /
    (this.mass + other.mass);

这里要注意的是两小球碰撞时的恢复系数应取两者的最小值,按照常识,弹性小的无论是去撞别人还是别人撞它,都会有同样的效果。现在小球碰撞后速度会有所减慢,不过还差一点,我们可以加上重力来让小球自然下落。

重力

添加重力比较简单,先在全局定义重力加速度常量,然后在小球更新垂直方向上的速度时,累计重力加速度就可以了:

const gravity = 980;

class Circle {
  update(seconds) {
    this.vy += gravity * seconds; // 重力加速度
    this.x += this.vx * seconds;
    this.y += this.vy * seconds;
  }
}

重力加速度大约是  ,但是由于我们的画布是以象素为单位的,所以使用 9.8 看起来会像是没有重力,或者像是从很远的地方观察小球,这时候可以把重力加速度放大一定倍数来达到更逼真的效果。

总结

现在我们这个简单的 JavaScript 物理引擎就完成了,实现了物理引擎最基本的部分,可以有一个完整的掉落和碰撞的效果,要做一个更逼真的物理引擎还需要考虑更多的因素和更复杂的公式,例如考虑一下摩擦力、空气阻力、碰撞后的旋转角度等,并且这个 canvas 的帧率也会有一定的问题,如果有的小球速度过快,但是如果来不及执行下一次回调函数更新它的位置,那么它可能就直接穿过碰撞的小球到另一边了。

来总结一下开发过程:

  • 使用 context 绘制小球。
  • 搭建 Canvas 动画基础结构,主要使用 ​window.requestAnimationFrame​方法反复执行回调函数。
  • 移动小球,通过小球的速度和函数执行时的时间戳来计算移动距离。
  • 碰撞检测,通过比对两个小球的距离和它们半径的和。
  • 边界碰撞的检测和方向改变。
  • 小球之间的碰撞,应用速度公式和向量操作计算出碰撞后的速度和方向。
  • 利用恢复系数实现非弹性碰撞。
  • 添加重力效果。

代码可以在以下地址中查看:

https://github.com/zxuqian/html-css-examples/tree/master/35-collision-physics

推荐好课:JavaScript微课JavaScript基础实战JavaScript面向对象编程

纯CSS实现页面中的列表收拉效果

thbcm阅读(172)

你可能经常见到下面这样的效果:

没错,就是页面上常用的“展开收起”交互形式,通常的做法就是控制 display 属性值在 none 和其他值之间切换,但是虽说功能可以实现,效果却非常生硬,所以就会有这样的一个需求 —— 希望元素在展开收起时能够有明显的高度滑动效果。

以前的实现可以用 jQuery 的​slideUp()/slideDown()​方法,但是,在移动端,因为 CSS3 动画支持良好,所以移动端的 JavaScript框架都是没有提供动画模块的。这里自然而然就想到了CSS3技术。

笔者的第一反应就是用​height+overflow:hidden;​实现,既没有性能问题,也不必担心显示问题。但是转眼间就想到:很多时候我们需要展现的内容都是动态的,也就是说内容高度是不固定的(当然,你也可以用​overflow-y:auto;​ 暂且不论)。而要达到这种效果,height 就要使用“非固定值 auto”!

但是​auto​并不是数值,而是一个关键字,所以在一个隐性规定 —— 数值和关键字之间无法计算 下,如果我们使用​height​在​0px​和​auto​之间切换,是无法形成过渡或动画效果的。

同样的还有 css 中的​clip-path​属性:很多初学者习惯于在 none 和具体值之间形成动画效果,这是不可能的。

因此,想要达到文首的效果,笔者推荐 max-height 属性:

<div class="accordion">
	<input id="collapse1" type="radio" name="tap-input" hidden />
	<input id="collapse2" type="radio" name="tap-input" hidden />
	<input id="collapse3" type="radio" name="tap-input" hidden />
	<article>
		<label for="collapse1">列表1</label>
		<p>内容1<br>内容2<br>内容3<br>内容4</p>
	</article>
	<article>
		<label for="collapse2">列表2</label>
		<p>内容1<br>内容2<br>内容3<br>内容4</p>
	</article>
	<article>
		<label for="collapse3">列表3</label>
		<p>内容1<br>内容2<br>内容3<br>内容4</p>
	</article>
</div>
.accordion {
	width: 300px;
}
.accordion article {
	cursor: pointer;
}
label {
	display: block;
	padding: 0 20px;
	height: 40px;
	background-color: #f66;
	cursor: pointer;
	line-height: 40px;
	font-size: 16px;
	color: #fff;
}
p {
	overflow: hidden;
	padding: 0 20px;
	margin: 0;
	border: 1px solid #f66;
	border-top: none;
	border-bottom-width: 0;
	max-height: 0;
	line-height: 30px;
	transition: all .5s ease;
}
input:nth-child(1):checked ~ article:nth-of-type(1) p,
input:nth-child(2):checked ~ article:nth-of-type(2) p,
input:nth-child(3):checked ~ article:nth-of-type(3) p {
	border-bottom-width: 1px;
	max-height: 130px;
}

在css中,​min-height/max-height​出现的场景一定是自适应布局或者流体布局中。而对于展开后的​max-height​值,我们只需要保证设定值比内容高度大即可 —— 因为在max-height > height 时,元素高度就会以height属性的高度计算。

但是建议不要把max-height值设置的太大,毕竟transition或animation的时间是“完成动画的时间”而不是“内容展示出来的时间”

收拉效果还有一种展现形式:

其特点是鼠标悬浮到组件的某个部分,该部分就会扩张开来并挤压旁边的部分,当鼠标离开时就恢复原状。若鼠标快速在其上面略过,就会产生手风琴弹琴的效果。

使用 JS 实现手风琴效果,必须监听​mouseenter​和​mouseleave​两个鼠标事件,而CSS中的​:hover​可代替两者的效果。所以纯 CSS实现手风琴效果的关键就是​:hover ​,其核心代码如下:

li {
}
li:hover {
}

而对布局来说,这种以相同/不同宽度排列在一排的元素想要实现在一行内的展开收缩效果,比较好的方式就是 flex

<ul class="accordion">
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
</ul>
.accordion {
	display: flex;
	width: 600px;
	height: 200px;
}
li {
	flex: 1;
	cursor: pointer;
	transition: all 300ms;
}
li:nth-child(1) {
	background-color: #f66;
}
li:nth-child(2) {
	background-color: #66f;
}
li:nth-child(3) {
	background-color: #f90;
}
li:nth-child(4) {
	background-color: #09f;
}
li:nth-child(5) {
	background-color: #9c3;
}
li:nth-child(6) {
	background-color: #3c9;
}
li:hover {
	flex: 2;
	background-color: #ccc;
}

这里有一点值得注意:像有些“特殊”情况比如 animation 的延迟,可以以 内联style 的方式在HTML中插入 CSS自定义变量

如何用JavaScript做俄罗斯方块游戏?

thbcm阅读(249)

俄罗斯方块相信大家都玩过吧,虽然现在大型游戏众多,但它可谓是童年的美好回忆,经常能玩到茶饭不思

那么你知道如何自己做一个俄罗斯方块嘛?本篇文章就教你如何用 JavaScript 来做一个俄罗斯方块小游戏

最终游戏效果

一开始我们先搭个框架,以便后期使用

编写外部框架

<!DOCTYPE html>
<html>
	<head lang="en">
		<meta charset="UTF-8">
		<title></title>
		<style>
*{
    margin: 0;
    padding: 0;
}
#box{
	width:320px;
	height:450px;
	position:absolute;
	margin:0 auto;
	left:0;
	top:20px;
	right:0;
	bottom:0;
	background:gray;
	border-radius:10px;
}
#mainDiv{
	width:300px;
	height:400px;
	position:absolute;
	margin:0 auto;
	left:0;
	top:10px;
	right:0;
	bottom:0;
}
.bottom{
	width:600px;
    height:30px;
	position:absolute;
	bottom:1px;
	right:1px;
}
.bottom .button1{
	position: absolute;
    right: 55px;
    width: 50px;
    font-size: 14px;
}
.bottom .button2{
	position: absolute;
    right: 5px;
    width: 50px;
    font-size: 14px;
}
.bottom .span1{
	position: absolute;
    right: 155px;
    color: white;
    font-size: 8px;
}
.bottom .span2{
	position: absolute;
    right: 255px;
    color: white;
    font-size: 8px;
}
</style>
	</head>
	<body>
		<div id='box'>
			<div id='mainDiv'>
			</div>
			<div class='bottom'>
				<span id='score' class='span1'>分数:0</span>
				<span id='time' class='span2'>时间:0</span>
				<button onclick='start()' class='button1'>开始</button>
				<button onclick='stop()' class='button2'>结束</button>
			</div>
		</div>
		
	<script type="text/javascript" src='tetris.js'></script>
	<script type="text/javascript">
 
	
</script>
	</body>
</html>

框架效果如下:

添加内部画布,以及绘制地图

首先创建线的构造函数Line

function Line(ctx,o){
		this.x=0,//x坐标
		this.y=0,//y坐标
		this.startX=0,//开始点x位置
		this.startY=0, //开始点y位置
		this.endX=0,//结束点x位置
		this.endY=0;//结束点y位置
		this.thin=false;//设置变细系数
		this.ctx=ctx;
		
		this.init(o);
	}
	Line.prototype.init=function(o){
		for(var key in o){
			this[key]=o[key];
		}
	}
	Line.prototype.render=function(){
		innerRender(this);
		
		function innerRender(obj){
			var ctx=obj.ctx;
			ctx.save()
			ctx.beginPath();
			ctx.translate(obj.x,obj.y);
			if(obj.thin){
				ctx.translate(0.5,0.5);
			}
			if(obj.lineWidth){//设定线宽
				ctx.lineWidth=obj.lineWidth;
			}
			if(obj.strokeStyle){
				ctx.strokeStyle=obj.strokeStyle;
			}
			//划线
		  	ctx.moveTo(obj.startX, obj.startY);
		  	ctx.lineTo(obj.endX, obj.endY);
		  	ctx.stroke();
		  	ctx.restore();
		}
	  	
	  	return this;
	}

设定参数、执行绘制等相关方法

代码如下:

function Game(el){
		this.renderArr=[];//待渲染对象存储数组
		this.aliveModel=[];//用来存到底的model组合
		
		this.score=0;//分数
		this.time=0;//时间
		this.moveCount=1;//计时控制器
	}
	
	Game.prototype.init=function(el,score,time){
		if(!el) return ;
		this.el=el;
		this.scoreEL=score;
		this.timeEL=time;
		var canvas = document.createElement('canvas');//创建画布
		canvas.style.cssText="background:darkgrey;border:1px solid grey;";//设置样式
		var W = canvas.width = 300; //设置宽度
		var H = canvas.height = 400;//设置高度
		
		el.appendChild(canvas);//添加到指定的dom对象中
		
		this.ctx = canvas.getContext('2d');
		this.canvas=canvas;
		this.w=W;
		this.h=H;
		
		this.disX=20;//每个格子的x方向大小
		this.disY=20;//每个格子的y方向大小
		this.maxX=15;//x方向格子总数
		this.maxY=20;//y方向格子总数
		
		this.control();//
		this.draw();//绘制
	}
	
	//绘制地图
	Game.prototype.createMap=function(){
		var renderArr = this.renderArr;
		var disX = this.disX;
		var disY = this.disY;
		var maxX=this.maxX;
		var maxY=this.maxY;
		var rectW = this.w;
		var rectH = this.h;
		var rect=null;
		var color;
		
		for(var i=1;i<maxY;i++){//20行
			var line = new Line(this.ctx,{
				x:0,
				y:0,
			 	startX:0,
			 	startY:i*disY,
			 	endX:this.w,
			 	endY:i*disY,
			 	thin:true,
			 	strokeStyle:'white',
			 	lineWidth:0.2
			})
			renderArr.push(line);
		}
		
		for(var i=1;i<maxX;i++){//15列
			var line = new Line(this.ctx,{
				x:0,
				y:0,
			 	startX:i*disX,
			 	startY:0,
			 	endX:i*disX,
			 	endY:this.h,
			 	thin:true,
			 	strokeStyle:'white',
			 	lineWidth:0.2
			})
			renderArr.push(line);
		}
	}
	
	Game.prototype.draw=function(){
		this.createMap();//绘制地图
		
		this.render();//渲染
	}

此时游戏区域的格子以及绘制如下:

再来绘制模型

模型定义:分别是一字形、田字形、二字形2种、七字形2种、凸字形等共7种。

变形定义:1字形可以变形2种、田字形不能变形,其他的都可以变形4种

模型的组成:模型是有4个小方块来组成,每个模型里面有数组blocks来存取4个小方块的x、y坐标,然后绘制出来就是模型了。

模型的变形:变形的时候就只要切换每个方块的X\Y坐标就可以达到变形的效果。

下面来创建模型的构造函数

	//模型构造函数
	function Model(o){
		this.blocks=[],//存储方块的数组,绘制的时候根据数组来绘制
		this.type=1,//模型的形状,默认是一字形(共7种)
		this.dir=1,//方向默认为1,总共4种,其中一字形为2种,田字形为1种,其他为4种
		this.x=0,//x坐标(只传入第一个x,根据这个x来生成其他的x)
		this.y=0,//y坐标(只传入第一个y,根据这个y来生成其他的y)
		
		this.init(o);
	}
	//初始化
	Model.prototype.init=function(o){
		for(var key in o){
			this[key]=o[key];
		}
	}

举例

添加一个创建七字形的方法(因为七字形有4种摆放方式,所以有dir来区分,怎么摆放)

//创建七字形1
	Model.prototype.createQi1=function(){
		var blocks=this.blocks,x=this.x,y=this.y;
		switch(this.dir){
			case 1://
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y-2});
				blocks.push({x:x+1,y:y-2});
				break;
			case 2://
				blocks.push({x:x+2,y:y});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				break;	
			case 3://
				blocks.push({x:x+1,y:y-2});
				blocks.push({x:x+1,y:y-1});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x,y:y});
				break;	
			case 4://
				blocks.push({x:x-2,y:y-1});
				blocks.push({x:x-1,y:y-1});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y});
				break;	
		}
	}

创建一个七字形试试(传入的x、y是第一个放个的位置)

var model = new Model({//创建1字
	x:6,y:6,fillStyle:'#0370BD',fill:true,game:this,type:5,dir:1
});
this.renderArr.push(model);
//当前的模型
this.currentModel=model;

编写变形方法(每一次变形都是按前一个模样逆时针旋转90度,修改每个小方块x、y来修改就行)

   ———————— >>> 

左边这个图形要变成右边的图形,需要怎么变更呢?

——————–>>>   

标上号码就很容易明白,1还是对应的1,2还是对应的2,以此类推,只不过X\Y变了

1方块:只要x+2就可以移到指定的位置;

2方块:x、y都需要加1

3方块:y+2就可以

4方块:x-1和y+1即可

其他都是一样的道理,来写一下变形的方法

//七1变形
	Model.prototype.transformQi1=function(){
		var blocks = this.blocks,block2=blocks[1];
		switch(this.dir){
			case 1://竖着的
				tran1();
				this.dir=2;
				break;
			case 2://横着的
				tran2();
				this.dir=3;
				break;	
			case 3://竖着的
				tran3();
				this.dir=4;
				break;
			case 4://横着的
				tran4();
				this.dir=1;
				break;			
		}
 
		function tran1(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x+=2;
				}else if(i==1){
					block.x+=1;
					block.y+=1;
				}else if(i==2){
					block.y+=2;
				}else if(i==3){
					block.x-=1;
					block.y+=1
				}
			}
		}
		
		function tran2(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x-=1;
					block.y-=2;
				}else if(i==1){
					block.y-=1;
				}else if(i==2){
					block.x+=1;
				}else if(i==3){
					block.y+=1
				}
			}
		}
		
		function tran3(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x+=2;
				}else if(i==2){
					block.x+=1;
					block.y-=1;
				}else if(i==1){
					
				}else if(i==0){
					block.x-=1;
					block.y+=1
				}
			}
		}
		
		function tran4(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x-=1;
					block.y-=2;
				}else if(i==2){
					block.x-=2;
					block.y-=1;
				}else if(i==1){
					block.x-=1;
				}else if(i==0){
					block.y+=1;
				}
			}
		}
	}

给 w 和向上键添加为变形事件,同时左移动、右移动、下加速也添加好事件

//按键的控制
	Game.prototype.control=function(){
		var that=this;
		global.addEventListener('keydown',function(e){
			//if(!that.timmer) return ;
			switch (e.keyCode){
				case 87://w
				case 38://上
					that.currentModel.transform();//变形
					break;
				case 83://s
				case 40://下
					that.currentModel.move('d');//移动
					break;
				case 65://a
				case 37://左
					that.currentModel.move('l');//移动
					break;
				case 68://d
				case 39://右
					that.currentModel.move('r');//移动
					break;
			
			}
			//测试用,记得删除
			that.render();
		});
	}

接下来变形试试

添加移动方法

//移动
	Model.prototype.move=function(dir){
		var cur = this.game.currentModel,dis=1,blocks = this.blocks;
		if(dir=='r'||dir=='ld'){
			dis=1
		}else if(dir=='l'){
			dis=-1;
		}else if(dir=='d'){
			dis=3;
		}
		
		var stopMoveObj = this.stopMove(dir,dis),
			val=stopMoveObj.val,resDis=stopMoveObj.resDis;
		if(val) {
			if(dir=='d'||dir=='ld'){//到底了
				[].push.apply(this.game.aliveModel,cur.blocks);//放到已到底的组合中
				this.game.renderArr.pop();//当前模型弹出
				this.game.clearBlock();//消除
				this.game.createModel();//绘制一个新图形
			}
			
			return ;//如果返回true 则不能再往这个方向移动
		}
 
		if(resDis>0){
			dis=resDis;
		}
		//更新每一个block的位置
		for(var i=0;i<blocks.length;i++){
			block=blocks[i];
			if(dir=='d'||dir=='ld'){
				block.y+=dis;
			}else{
				block.x+=dis;
			}
		}
	}

加入边界判断和碰撞检测(边界检测比较简单、碰撞检测在快进的时候要注意处理一下,看代码吧)

//停止移动
	Model.prototype.stopMove=function(dir,dis){
		var cur = this.game.currentModel,blocks = this.blocks;
		
		var maxX = this.game.maxX,maxY = this.game.maxY,res,temp;
		for(var i=0;i<blocks.length;i++){
			block=blocks[i];
			if(dir=='d'||dir=='ld'){
				if(block.y>=maxY-1){//到底了
					return {val:true};
				}
			}else if(dir=='r'){
				if(block.x>=maxX-1){//到右边界了
					return {val:true};
				}
			}else if(dir=='l'){
				if(block.x<=0){//到左边界了
					return {val:true};
				}
			}
			//碰撞检测
			temp=this.collide(block,dis,dir);
			if(temp.val){
				return temp;
			}
			if(!res || res.resDis==0 || (temp.resDis!=0 && temp.resDis<res.resDis)){
				res=temp;
			}
		}
		
		return res;
	}
	//检查当前模型是否与其他存底的模型相触碰
	Model.prototype.collide=function(block,dis,dir){
		var aliveModel = this.game.aliveModel,item;
		var res={},val=false,resDis=0,maxY = this.game.maxY;
		
		if(dir=='r'){//向右判断
			for(var i=0;i<aliveModel.length;i++){
				item = aliveModel[i];
				if(!item) continue;
				if(item.y==block.y && item.x==block.x+1){
					val= true;
					break;
				}
			}
		}else if(dir=='l'){//向左判断
			for(var i=0;i<aliveModel.length;i++){
				item = aliveModel[i];
				if(!item) continue;
				if(item.y==block.y && item.x==block.x-1){
					val= true;
					break;
				}
			}
		}else {//向下判断
			if(aliveModel.length>0){
				for(var i=0;i<aliveModel.length;i++){
					item = aliveModel[i];
					if(!item) continue;
					if(item.x==block.x){//下方有存在的方块
						if(item.y==block.y+1){
							val= true;
							break;
						}else if(item.y<=block.y+Math.abs(dis)){
							var temp=item.y-block.y-1;
							if(resDis==0 || temp<resDis){
								resDis = temp;
							}
						}
					}else{//下发不存在方块
						if(maxY<=block.y+Math.abs(dis)){
							var temp=maxY-block.y-1;
							if(resDis==0 || temp<resDis){
								resDis = temp;
							}
						}
					} 
				}
			}else{//第一个模型
				if(maxY<=block.y+Math.abs(dis)){
					var temp=maxY-block.y-1;
					if(resDis==0 || temp<resDis){
						resDis = temp;
					}
				}
			}
		}
		res.resDis=resDis;
		res.val=val;
		return res;
	}

到底后的处理

  1. 给当前的游戏对象的aliveModel数组,存取当前到底的模型所对应的4个小方块(执行render方法的时候就绘制出来) 
  2. 在renderArr数组中此模型要删除
  3. 每次触底需要加入消除判断,符合条件就消除
  4. 绘制一个新的模型

绘制触底的模型方块

//绘制存底的图形
	Game.prototype.aliveModelRender=function(){
		var context=this.ctx;
		var disX=this.disX,disY=this.disY;
		context.save();
		context.beginPath();
		_.each(this.aliveModel,function(item){
			if(item){
				context.rect(item.x*disX+1,item.y*disY+1,disX-1,disY-1);
				//context.fillStyle='';
				context.fill();
			}
		});
		context.restore();
	}

消除行、积分、以及下降

//消除行
	Game.prototype.clearBlock=function(){
		var maxX=this.maxX,aliveModel=this.aliveModel;
		//将y相同的放在一起
		var rowArr=[],rowObj={};
		_.each(aliveModel,function(item,index){
			if(item) {
				if(!rowObj[item.y]){
					rowObj[item.y]=[];
				}
				rowObj[item.y].push(index);
			}
		});
		
		var that=this;
		var keys = Object.keys(rowObj),row,num=0;
		_.each(keys,function(k){
			row = rowObj[k];
			if(row.length>=maxX){//消除这行
				_.each(row,function(r){
					aliveModel.splice(r,1,undefined);//先用undefined代替
				})
				
				num++;//行数计数器
				that.down(k,1);//清楚当前行
			}
		})
		
		//完成消除
		for(var i=0;i<aliveModel.length;i++){
			if(!aliveModel[i]) {
				console.log(123)
				aliveModel.splice(i,1);
			}
		}
		
		var score = 0;
		switch (num){
			case 1:
				score=100;//1行100分
				break;
			case 2:
				score=300;//2行300分
				break;
			case 3:
				score=600;//3行600分
				break;
			case 4:
				score=1000;//4行1000分
				break;
		}
		//积分
		this.calcuScore(score);
	}
	
	//消除后的下降
	Game.prototype.down=function(y,num){
		var aliveModel=this.aliveModel;
		_.each(aliveModel,function(item){
			if(item && item.y<y){
				item.y+=num;
			}
		});
	}

自动往下移动、更新、显示时间、分数

//显示分数
	Game.prototype.calcuScore=function(s){
		this.score+=s;
		this.scoreEL.innerText='分数:'+this.score;
	}
	//显示时间
	Game.prototype.calcuTime=function(){
		if(this.moveCount%4==0){
			this.time++;
			this.time_flag=false;
			this.timeEL.innerText='时间:'+this.time;
		}
		
		this.moveCount++;
	}
	//向下移动
	Game.prototype.move=function(dir){
		var curModel= this.currentModel;
		this.calcuTime();
		
		var endFlag = this.end();
		if(endFlag) {
			this.stop();
			this.hasEnd=true;
			return ;
		} 
		
		this.update();
		this.render();
	}
	//更新
	Game.prototype.update=function(){
		this.currentModel.move('ld');
	}

给开始、结束按钮加入事件

	var mainDiv = document.getElementById('mainDiv');
	var score= document.getElementById('score');
	var time= document.getElementById('time');
	
	game.init(mainDiv,score,time);
	
	function start(){
		game.start()
	}
	
	function stop(){
		game.stop()
	}
Game.prototype.start=function(){
		if(this.timmer) return ;
		if(this.hasEnd){//如果是结束则需要重新开始,暂停的话就继续游戏
			this.restart();
		}
		this.hasEnd=false;
		this.timmer = setInterval(this.move.bind(this),250);//开始定时任务
	}
	
	//重新开始
	Game.prototype.restart=function(){
		this.renderArr=[];//待渲染对象存储数组
		this.aliveModel=[];//用来存到底的model组合
		
		this.score=0;//分数
		this.time=0;//时间
		this.moveCount=1;//计时控制器
		
		this.clearCanvas();
		this.draw();
	}
	//停止任务	
	Game.prototype.stop=function(){
		if(!this.timmer) return ;
		clearInterval(this.timmer);//清除定时任务
		this.timmer=null;
	}
	//结束
	Game.prototype.end=function(){
		var aliveModel = this.aliveModel;
		for(var i=0;i<aliveModel.length;i++){
			item = aliveModel[i];
			if(item&&item.y==0){
				alert('结束了')
				return true;
			}
		}
		return false
	}

源码如下:

HTML

<!DOCTYPE html>
<html>
	<head lang="en">
		<meta charset="UTF-8">
		<title></title>
		<style>
*{
    margin: 0;
    padding: 0;
}
#box{
	width:320px;
	height:450px;
	position:absolute;
	margin:0 auto;
	left:0;
	top:20px;
	right:0;
	bottom:0;
	background:gray;
	border-radius:10px;
}
#mainDiv{
	width:300px;
	height:400px;
	position:absolute;
	margin:0 auto;
	left:0;
	top:10px;
	right:0;
	bottom:0;
}
.bottom{
	width:600px;
    height:30px;
	position:absolute;
	bottom:1px;
	right:1px;
}
.bottom .button1{
	position: absolute;
    right: 55px;
    width: 50px;
    font-size: 14px;
}
.bottom .button2{
	position: absolute;
    right: 5px;
    width: 50px;
    font-size: 14px;
}
.bottom .span1{
	position: absolute;
    right: 155px;
    color: white;
    font-size: 8px;
}
.bottom .span2{
	position: absolute;
    right: 255px;
    color: white;
    font-size: 8px;
}
</style>
	</head>
	<body>
		<div id='box'>
			<div id='mainDiv'>
			</div>
			<div class='bottom'>
				<span id='score' class='span1'>分数:0</span>
				<span id='time' class='span2'>时间:0</span>
				<button onclick='start()' class='button1'>开始</button>
				<button onclick='stop()' class='button2'>结束</button>
			</div>
		</div>
		
	<script type="text/javascript" src='tetris.js'></script>
	<script type="text/javascript">
	var mainDiv = document.getElementById('mainDiv');
	var score= document.getElementById('score');
	var time= document.getElementById('time');
	
	game.init(mainDiv,score,time);
	
	function start(){
		game.start()
	}
	
	function stop(){
		game.stop()
	}
	
</script>
	</body>
</html>

JS

;(function(global){
	var game= new Game();
	
	function Game(el){
		this.renderArr=[];//待渲染对象存储数组
		this.aliveModel=[];//用来存到底的model组合
		
		this.score=0;//分数
		this.time=0;//时间
		this.moveCount=1;//计时控制器
	}
	
	Game.prototype.init=function(el,score,time){
		if(!el) return ;
		this.el=el;
		this.scoreEL=score;
		this.timeEL=time;
		var canvas = document.createElement('canvas');//创建画布
		canvas.style.cssText="background:darkgrey;border:1px solid grey;";//设置样式
		var W = canvas.width = 300; //设置宽度
		var H = canvas.height = 400;//设置高度
		
		el.appendChild(canvas);//添加到指定的dom对象中
		
		this.ctx = canvas.getContext('2d');
		this.canvas=canvas;
		this.w=W;
		this.h=H;
		
		this.disX=20;//每个格子的x方向大小
		this.disY=20;//每个格子的y方向大小
		this.maxX=15;//x方向格子总数
		this.maxY=20;//y方向格子总数
		
		this.control();//
		this.draw();//绘制
	}
	
	//绘制地图
	Game.prototype.createMap=function(){
		var renderArr = this.renderArr;
		var disX = this.disX;
		var disY = this.disY;
		var maxX=this.maxX;
		var maxY=this.maxY;
		var rectW = this.w;
		var rectH = this.h;
		var rect=null;
		var color;
		
		for(var i=1;i<maxY;i++){//20行
			var line = new Line(this.ctx,{
				x:0,
				y:0,
			 	startX:0,
			 	startY:i*disY,
			 	endX:this.w,
			 	endY:i*disY,
			 	thin:true,
			 	strokeStyle:'white',
			 	lineWidth:0.2
			})
			renderArr.push(line);
		}
		
		for(var i=1;i<maxX;i++){//15列
			var line = new Line(this.ctx,{
				x:0,
				y:0,
			 	startX:i*disX,
			 	startY:0,
			 	endX:i*disX,
			 	endY:this.h,
			 	thin:true,
			 	strokeStyle:'white',
			 	lineWidth:0.2
			})
			renderArr.push(line);
		}
	}
	
	Game.prototype.draw=function(){
		this.createMap();//绘制地图
		this.createModel();//绘制一个图形
		this.render();//渲染
	}
	//渲染图形
	Game.prototype.render=function(){
		var context=this.ctx;
		this.clearCanvas();	
		_.each(this.renderArr,function(item){
			item && item.render(context);
		});
		
		this.aliveModelRender();
	}
	//绘制存底的图形
	Game.prototype.aliveModelRender=function(){
		var context=this.ctx;
		var disX=this.disX,disY=this.disY;
		context.save();
		context.beginPath();
		_.each(this.aliveModel,function(item){
			if(item){
				context.rect(item.x*disX+1,item.y*disY+1,disX-1,disY-1);
				//context.fillStyle='';
				context.fill();
			}
		});
		context.restore();
	}
	//消除行
	Game.prototype.clearBlock=function(){
		var maxX=this.maxX,aliveModel=this.aliveModel;
		//将y相同的放在一起
		var rowArr=[],rowObj={};
		_.each(aliveModel,function(item,index){
			if(item) {
				if(!rowObj[item.y]){
					rowObj[item.y]=[];
				}
				rowObj[item.y].push(index);
			}
		});
		
		var that=this;
		var keys = Object.keys(rowObj),row,num=0;
		_.each(keys,function(k){
			row = rowObj[k];
			if(row.length>=maxX){//消除这行
				_.each(row,function(r){
					aliveModel.splice(r,1,undefined);//先用undefined代替
				})
				
				num++;//行数计数器
				that.down(k,1);//清楚当前行
			}
		})
		
		//完成消除
		for(var i=0;i<aliveModel.length;i++){
			if(!aliveModel[i]) {
				console.log(123)
				aliveModel.splice(i,1);
			}
		}
		
		var score = 0;
		switch (num){
			case 1:
				score=100;//1行100分
				break;
			case 2:
				score=300;//2行300分
				break;
			case 3:
				score=600;//3行600分
				break;
			case 4:
				score=1000;//4行1000分
				break;
		}
		//积分
		this.calcuScore(score);
	}
	
	//消除后的下降
	Game.prototype.down=function(y,num){
		var aliveModel=this.aliveModel;
		_.each(aliveModel,function(item){
			if(item && item.y<y){
				item.y+=num;
			}
		});
	}
	
	Game.prototype.start=function(){
		if(this.timmer) return ;
		if(this.hasEnd){//如果是结束则需要重新开始,暂停的话就继续游戏
			this.restart();
		}
		this.hasEnd=false;
		this.timmer = setInterval(this.move.bind(this),250);//开始定时任务
	}
	
	//重新开始
	Game.prototype.restart=function(){
		this.renderArr=[];//待渲染对象存储数组
		this.aliveModel=[];//用来存到底的model组合
		
		this.score=0;//分数
		this.time=0;//时间
		this.moveCount=1;//计时控制器
		
		this.clearCanvas();
		this.draw();
	}
	//停止任务	
	Game.prototype.stop=function(){
		if(!this.timmer) return ;
		clearInterval(this.timmer);//清除定时任务
		this.timmer=null;
	}
	//结束
	Game.prototype.end=function(){
		var aliveModel = this.aliveModel;
		for(var i=0;i<aliveModel.length;i++){
			item = aliveModel[i];
			if(item&&item.y==0){
				alert('结束了')
				return true;
			}
		}
		return false
	}
	//显示分数
	Game.prototype.calcuScore=function(s){
		this.score+=s;
		this.scoreEL.innerText='分数:'+this.score;
	}
	//显示时间
	Game.prototype.calcuTime=function(){
		if(this.moveCount%4==0){
			this.time++;
			this.time_flag=false;
			this.timeEL.innerText='时间:'+this.time;
		}
		
		this.moveCount++;
	}
	//向下移动
	Game.prototype.move=function(dir){
		var curModel= this.currentModel;
		this.calcuTime();
		
		var endFlag = this.end();
		if(endFlag) {
			this.stop();
			this.hasEnd=true;
			return ;
		} 
		
		this.update();
		this.render();
	}
	//更新
	Game.prototype.update=function(){
		this.currentModel.move('ld');
	}
	//按键的控制
	Game.prototype.control=function(){
		var that=this;
		global.addEventListener('keydown',function(e){
			//if(!that.timmer) return ;
			switch (e.keyCode){
				case 87://w
				case 38://上
					that.currentModel.transform();//变形
					break;
				case 83://s
				case 40://下
					that.currentModel.move('d');//移动
					break;
				case 65://a
				case 37://左
					that.currentModel.move('l');//移动
					break;
				case 68://d
				case 39://右
					that.currentModel.move('r');//移动
					break;
			
			}
			//测试用,记得删除
			//that.render();
		});
	}
	
	Game.prototype.clearCanvas=function(){
		this.ctx.clearRect(0,0,parseInt(this.w),parseInt(this.h));//清理画布
	}
	//创建模型
	Game.prototype.createModel=function(){
		var type = _.getRandom(1,7);//type有7种
		var dir =0;
		if(type=='1'){//一字 只2种
			dir = _.getRandom(1,2);
		}else if(type=='2'){//一字 只有1种
			dir = 1;
		}else{//其他有4种
			dir = _.getRandom(1,4);
		}

		var model = new Model({//随机创建
			x:6,y:-1,fillStyle:'#0370BD',fill:true,game:this,type:type,dir:dir
		});
		this.renderArr.push(model);
		//当前的模型
		this.currentModel=model;
		/*
		model = new Model({//创建横向1字
			x:5,y:4,fillStyle:'#0370BD',fill:true,game:this,type:1,dir:1
		});
		
		
		model = new Model({//创建田字
			x:5,y:4,fillStyle:'#0370BD',fill:true,game:this,type:2,dir:1
		});
		
		model = new Model({//创建二字1
			x:5,y:4,fillStyle:'#0370BD',fill:true,game:this,type:3,dir:4
		});
		
		model = new Model({//创建二字2
			x:5,y:4,fillStyle:'#0370BD',fill:true,game:this,type:4,dir:1
		});
		
		model = new Model({//创建七字1
			x:5,y:4,fillStyle:'#0370BD',fill:true,game:this,type:5,dir:4
		});
		
		model = new Model({//创建七字2
			x:5,y:4,fillStyle:'#0370BD',fill:true,game:this,type:6,dir:4
		});
		
		model = new Model({//创建凸字
			x:5,y:4,fillStyle:'#0370BD',fill:true,game:this,type:7,dir:4
		});
		*/
	}
	
	//模型构造函数
	function Model(o){
		this.blocks=[],//存储方块的数组,绘制的时候根据数组来绘制
		this.type=1,//模型的形状,默认是一字形(共7种)
		this.dir=1,//方向默认为1,总共4种,其中一字形为2种,田字形为1种,其他为4种
		this.x=0,//x坐标(只传入第一个x,根据这个x来生成其他的x)
		this.y=0,//y坐标(只传入第一个y,根据这个y来生成其他的y)
		
		this.init(o);
	}
	//初始化
	Model.prototype.init=function(o){
		for(var key in o){
			this[key]=o[key];
		}
		this.organ();
	}
	//组织图形
	Model.prototype.organ=function(){
		switch(this.type){
			case 1:
				this.createYi();//创建一字形
				break;
				
			case 2:
				this.createTian();//创建田字形
				break;
				
			case 3:
				this.createEr1();//创建字二形1
				break;
				
			case 4:
				this.createEr2();//创建二字形2
				break;
				
			case 5:
				this.createQi1();//创建七字形1
				break;
				
			case 6:
				this.createQi2();//创建七字形2
				break;	
			
			case 7:
				this.createTu();//创建凸字形
				break;		
		}
	}
	//创建一字形
	Model.prototype.createYi=function(){
		var blocks=this.blocks,x=this.x,y=this.y;
		switch(this.dir){
			case 1://横着的
				blocks.push({x:x,y:y});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x+2,y:y});
				blocks.push({x:x+3,y:y});
				break;
			case 2://竖着的
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y-2});
				blocks.push({x:x,y:y-3});
				break;	
				
		}
	}
	//创建田字形
	Model.prototype.createTian=function(){
		var blocks=this.blocks,x=this.x,y=this.y;
		switch(this.dir){
			case 1://横着的
				blocks.push({x:x,y:y});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x+1,y:y-1});
				break;
			case 2://竖着的
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y-2});
				blocks.push({x:x,y:y-3});;
				break;	
				
		}
	}
	//创建二字形1
	Model.prototype.createEr1=function(){
		var blocks=this.blocks,x=this.x,y=this.y;
		switch(this.dir){
			case 1://
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x+1,y:y-1});
				blocks.push({x:x+1,y:y-2});
				break;
			case 2://
				blocks.push({x:x+1,y:y});
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x-1,y:y-1});
				break;	
			case 3://
				blocks.push({x:x+1,y:y-2});
				blocks.push({x:x+1,y:y-1});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y});
				break;	
			case 4://
				blocks.push({x:x-1,y:y-1});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y});
				blocks.push({x:x+1,y:y});
				break;	
		}
	}
	//创建二字形2
	Model.prototype.createEr2=function(){
		var blocks=this.blocks,x=this.x,y=this.y;
		switch(this.dir){
			case 1://
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x-1,y:y-1});
				blocks.push({x:x-1,y:y-2});
				break;
			case 2://
				blocks.push({x:x+2,y:y-1});
				blocks.push({x:x+1,y:y-1});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x,y:y});
				break;	
			case 3://
				blocks.push({x:x-1,y:y-2});
				blocks.push({x:x-1,y:y-1});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y});
				break;	
			case 4://
				blocks.push({x:x,y:y});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x+1,y:y-1});
				blocks.push({x:x+2,y:y-1});
				
				break;	
		}
	}
	
	//创建七字形1
	Model.prototype.createQi1=function(){
		var blocks=this.blocks,x=this.x,y=this.y;
		switch(this.dir){
			case 1://
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y-2});
				blocks.push({x:x+1,y:y-2});
				break;
			case 2://
				blocks.push({x:x+2,y:y});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				break;	
			case 3://
				blocks.push({x:x+1,y:y-2});
				blocks.push({x:x+1,y:y-1});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x,y:y});
				break;	
			case 4://
				blocks.push({x:x-2,y:y-1});
				blocks.push({x:x-1,y:y-1});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y});
				break;	
		}
	}
	
	//创建七字形2
	Model.prototype.createQi2=function(){
		var blocks=this.blocks,x=this.x,y=this.y;
		switch(this.dir){
			case 1://
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y-2});
				blocks.push({x:x-1,y:y-2});
				break;
			case 2://
				blocks.push({x:x+2,y:y-1});
				blocks.push({x:x+1,y:y-1});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y});
				break;	
			case 3://
				blocks.push({x:x,y:y-2});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y});
				blocks.push({x:x+1,y:y});
				break;	
			case 4://
				blocks.push({x:x,y:y});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x+2,y:y});
				blocks.push({x:x+2,y:y-1});
				break;	
		}
	}
	//创建凸字形
	Model.prototype.createTu=function(){
		var blocks=this.blocks,x=this.x,y=this.y;
		switch(this.dir){
			case 1://
				blocks.push({x:x,y:y});
				blocks.push({x:x+1,y:y});
				blocks.push({x:x+2,y:y});
				blocks.push({x:x+1,y:y-1});
				break;
			case 2://
				blocks.push({x:x,y:y});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y-2});
				blocks.push({x:x-1,y:y-1});
				
				break;	
			case 3://
				blocks.push({x:x+1,y:y-1});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x-1,y:y-1});
				blocks.push({x:x,y:y});
				break;	
			case 4://
				blocks.push({x:x,y:y-2});
				blocks.push({x:x,y:y-1});
				blocks.push({x:x,y:y});
				blocks.push({x:x+1,y:y-1});
				break;	
		}
	}
	
	//变形
	Model.prototype.transform=function(){
		switch(this.type){
			case 1://一
				this.transformYi();
				break;
			case 2://田
				//无需变形
				break;	
			case 3://二(1)
				this.transformEr1();
				break;
			case 4://二(2)
				this.transformEr2();
				break;
			case 5://七(1)
				this.transformQi1();
				break;	
			case 6://七(2)
				this.transformQi2();
				break;
			case 7://凸
				this.transformTu();
				break;
		}
	}
	Model.prototype.transformYi=function(){
		var blocks = this.blocks,block2=blocks[1];
		switch(this.dir){
			case 1://横着的
				tran1();
				this.dir=2;
				break;
			case 2://竖着的
				tran2();
				this.dir=1;
				break;	
				
		}

		function tran1(){//变成竖着的
			for(var i=0;i<blocks.length;i++){
				if(i==1)continue;
				block=blocks[i];
				
				if(i==0){
					block.y=block2.y+1;
				}else if(i==2){
					block.y=block2.y-1;
				}else if(i==3){
					block.y=block2.y-2;
				}
				//x方向改成一样
				block.x=block2.x;
			}
		}
		
		function tran2(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				if(i==1)continue;
				block=blocks[i];
				
				if(i==0){
					block.x=block2.x-1;
				}else if(i==2){
					block.x=block2.x+1;
				}else if(i==3){
					block.x=block2.x+2;
				}
				//y方向改成一样
				block.y=block2.y;
			}
		}
	}
	
	Model.prototype.transformEr1=function(){
		var blocks = this.blocks,block2=blocks[1];
		switch(this.dir){
			case 1://竖着的
				tran1();
				this.dir=2;
				break;
			case 2://横着的
				tran2();
				this.dir=3;
				break;	
			case 3://竖着的
				tran3();
				this.dir=4;
				break;
			case 4://横着的
				tran4();
				this.dir=1;
				break;			
		}

		function tran1(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x+=2;
				}else if(i==1){
					block.x+=1;
					block.y+=1;
				}else if(i==2){
				}else if(i==3){
					block.x-=1;
					block.y+=1
				}
			}
		}
		
		function tran2(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x-=1;
					block.y-=2;
				}else if(i==1){
					block.y-=1;
				}else if(i==2){
					block.x-=1;
				}else if(i==3){
					block.y+=1
				}
			}
		}
		
		function tran3(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x+=2;
				}else if(i==2){
					block.x+=1;
					block.y+=1;
				}else if(i==1){
				}else if(i==0){
					block.x-=1;
					block.y+=1
				}
			}
		}
		
		function tran4(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x-=1;
					block.y-=2;
				}else if(i==2){
					block.y-=1;
				}else if(i==1){
					block.x-=1;
				}else if(i==0){
					block.y+=1
				}
			}
		}
	}
	
	Model.prototype.transformEr2=function(){
		var blocks = this.blocks,block2=blocks[1];
		switch(this.dir){
			case 1://竖着的
				tran1();
				this.dir=2;
				break;
			case 2://横着的
				tran2();
				this.dir=3;
				break;	
			case 3://竖着的
				tran3();
				this.dir=4;
				break;
			case 4://横着的
				tran4();
				this.dir=1;
				break;			
		}

		function tran1(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x+=1;
					block.y-=1;
				}else if(i==1){
				}else if(i==2){
					block.x+=1;
					block.y+=1;
				}else if(i==3){
					block.y+=2;
				}
			}
		}
		
		function tran2(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x-=2;
					block.y-=1;
				}else if(i==1){
					block.x-=1;
				}else if(i==2){
					block.y-=1;
				}else if(i==3){
					block.x+=1
				}
			}
		}
		
		function tran3(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x+=1;
					block.y-=1;
				}else if(i==2){
				}else if(i==1){
					block.x+=1;
					block.y+=1;	
				}else if(i==0){
					block.y+=2;	
				}
			}
		}
		
		function tran4(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x-=2;
					block.y-=1;
				}else if(i==2){
					block.x-=1;
				}else if(i==1){
					block.y-=1;
				}else if(i==0){
					block.x+=1
				}
			}
		}
	}
	//七1变形
	Model.prototype.transformQi1=function(){
		var blocks = this.blocks,block2=blocks[1];
		switch(this.dir){
			case 1://竖着的
				tran1();
				this.dir=2;
				break;
			case 2://横着的
				tran2();
				this.dir=3;
				break;	
			case 3://竖着的
				tran3();
				this.dir=4;
				break;
			case 4://横着的
				tran4();
				this.dir=1;
				break;			
		}

		function tran1(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x+=2;
				}else if(i==1){
					block.x+=1;
					block.y+=1;
				}else if(i==2){
					block.y+=2;
				}else if(i==3){
					block.x-=1;
					block.y+=1
				}
			}
		}
		
		function tran2(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x-=1;
					block.y-=2;
				}else if(i==1){
					block.y-=1;
				}else if(i==2){
					block.x+=1;
				}else if(i==3){
					block.y+=1
				}
			}
		}
		
		function tran3(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x+=2;
				}else if(i==2){
					block.x+=1;
					block.y-=1;
				}else if(i==1){
					
				}else if(i==0){
					block.x-=1;
					block.y+=1
				}
			}
		}
		
		function tran4(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x-=1;
					block.y-=2;
				}else if(i==2){
					block.x-=2;
					block.y-=1;
				}else if(i==1){
					block.x-=1;
				}else if(i==0){
					block.y+=1;
				}
			}
		}
	}
	
	//七2变形
	Model.prototype.transformQi2=function(){
		var blocks = this.blocks,block2=blocks[1];
		switch(this.dir){
			case 1://竖着的
				tran1();
				this.dir=2;
				break;
			case 2://横着的
				tran2();
				this.dir=3;
				break;	
			case 3://竖着的
				tran3();
				this.dir=4;
				break;
			case 4://横着的
				tran4();
				this.dir=1;
				break;			
		}

		function tran1(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x+=1;
					block.y-=1;
				}else if(i==1){
				}else if(i==2){
					block.x-=1;
					block.y+=1;
				}else if(i==3){
					block.y+=2;
				}
			}
		}
		
		function tran2(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x-=2;
					block.y-=1;
				}else if(i==1){
					block.x-=1;
				}else if(i==2){
					block.y+=1;
				}else if(i==3){
					block.x+=1;
				}
			}
		}
		
		function tran3(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x+=1;
					block.y-=1;
				}else if(i==2){
					block.x+=2;
				}else if(i==1){
					block.x+=1;
					block.y+=1;
				}else if(i==0){
					block.y+=2;
				}
			}
		}
		
		function tran4(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.x-=2;
					block.y-=1;
				}else if(i==2){
					block.x-=1;
					block.y-=2;
				}else if(i==1){
					block.y-=1;
				}else if(i==0){
					block.x+=1;
				}
			}
		}
	}
	
	
	//凸变形
	Model.prototype.transformTu=function(){
		var blocks = this.blocks,block2=blocks[1];
		switch(this.dir){
			case 1://横着的
				tran1();
				this.dir=2;
				break;
			case 2://竖着的
				tran2();
				this.dir=3;
				break;	
			case 3://横着的
				tran3();
				this.dir=4;
				break;
			case 4://竖着的
				tran4();
				this.dir=1;
				break;			
		}

		function tran1(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x+=1;
				}else if(i==1){
					block.y-=1;
				}else if(i==2){
					block.x-=1;
					block.y-=2;
				}else if(i==3){
					block.x-=1;
				}
			}
		}
		
		function tran2(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==0){
					block.x+=1;
					block.y-=1;
				}else if(i==1){
				}else if(i==2){
					block.x-=1;
					block.y+=1
				}else if(i==3){
					block.x+=1;
					block.y+=1
				}
			}
		}
		
		function tran3(){//变成横着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
					block.y-=1;
				}else if(i==2){
					block.y+=1;
				}else if(i==1){
					block.x-=1;
				}else if(i==0){
					block.x-=2;
					block.y-=1
				}
			}
		}
		
		function tran4(){//竖着的
			for(var i=0;i<blocks.length;i++){
				block=blocks[i];
				if(i==3){
				}else if(i==2){
					block.x+=2;
				}else if(i==1){
					block.x+=1;
					block.y+=1
				}else if(i==0){
					block.y+=2;
				}
			}
		}
	}
	
	//移动
	Model.prototype.move=function(dir){
		var cur = this.game.currentModel,dis=1,blocks = this.blocks;
		if(dir=='r'||dir=='ld'){
			dis=1
		}else if(dir=='l'){
			dis=-1;
		}else if(dir=='d'){
			dis=3;
		}
		
		var stopMoveObj = this.stopMove(dir,dis),
			val=stopMoveObj.val,resDis=stopMoveObj.resDis;
		if(val) {
			if(dir=='d'||dir=='ld'){//到底了
				[].push.apply(this.game.aliveModel,cur.blocks);//放到已到底的组合中
				this.game.renderArr.pop();//当前模型弹出
				this.game.clearBlock();//消除
				this.game.createModel();//绘制一个新图形
			}
			
			return ;//如果返回true 则不能再往这个方向移动
		}

		if(resDis>0){
			dis=resDis;
		}
		//更新每一个block的位置
		for(var i=0;i<blocks.length;i++){
			block=blocks[i];
			if(dir=='d'||dir=='ld'){
				block.y+=dis;
			}else{
				block.x+=dis;
			}
		}
	}
	
	//停止移动
	Model.prototype.stopMove=function(dir,dis){
		var cur = this.game.currentModel,blocks = this.blocks;
		
		var maxX = this.game.maxX,maxY = this.game.maxY,res,temp;
		for(var i=0;i<blocks.length;i++){
			block=blocks[i];
			if(dir=='d'||dir=='ld'){
				if(block.y>=maxY-1){//到底了
					return {val:true};
				}
			}else if(dir=='r'){
				if(block.x>=maxX-1){//到右边界了
					return {val:true};
				}
			}else if(dir=='l'){
				if(block.x<=0){//到左边界了
					return {val:true};
				}
			}
			//碰撞检测
			temp=this.collide(block,dis,dir);
			if(temp.val){
				return temp;
			}
			if(!res || res.resDis==0 || (temp.resDis!=0 && temp.resDis<res.resDis)){
				res=temp;
			}
		}
		
		return res;
	}
	//检查当前模型是否与其他存底的模型相触碰
	Model.prototype.collide=function(block,dis,dir){
		var aliveModel = this.game.aliveModel,item;
		var res={},val=false,resDis=0,maxY = this.game.maxY;
		
		if(dir=='r'){//向右判断
			for(var i=0;i<aliveModel.length;i++){
				item = aliveModel[i];
				if(!item) continue;
				if(item.y==block.y && item.x==block.x+1){
					val= true;
					break;
				}
			}
		}else if(dir=='l'){//向左判断
			for(var i=0;i<aliveModel.length;i++){
				item = aliveModel[i];
				if(!item) continue;
				if(item.y==block.y && item.x==block.x-1){
					val= true;
					break;
				}
			}
		}else {//向下判断
			if(aliveModel.length>0){
				for(var i=0;i<aliveModel.length;i++){
					item = aliveModel[i];
					if(!item) continue;
					if(item.x==block.x){//下方有存在的方块
						if(item.y==block.y+1){
							val= true;
							break;
						}else if(item.y<=block.y+Math.abs(dis)){
							var temp=item.y-block.y-1;
							if(resDis==0 || temp<resDis){
								resDis = temp;
							}
						}
					}else{//下发不存在方块
						if(maxY<=block.y+Math.abs(dis)){
							var temp=maxY-block.y-1;
							if(resDis==0 || temp<resDis){
								resDis = temp;
							}
						}
					} 
				}
			}else{//第一个模型
				if(maxY<=block.y+Math.abs(dis)){
					var temp=maxY-block.y-1;
					if(resDis==0 || temp<resDis){
						resDis = temp;
					}
				}
			}
		}
		res.resDis=resDis;
		res.val=val;
		return res;
	}
	
	//绘制
	Model.prototype.render=function(context){
		var ctx=context;
		ctx.save();
		ctx.beginPath();
		//ctx.translate(this.x,this.y);
		var blocks = this.blocks,block,game=this.game,disX=game.disX,disY=game.disY;
		for(var i=0;i<blocks.length;i++){
			block=blocks[i];
			ctx.rect(block.x*disX+1,block.y*disY+1,disX-1,disY-1);
		}
		
		if(this.lineWidth){//线宽
			ctx.lineWidth=this.lineWidth;
		}
		if(this.fill){//是否填充
			this.fillStyle?(ctx.fillStyle=this.fillStyle):null;
			ctx.fill();
		}
		if(this.stroke){//是否描边
			this.strokeStyle?(ctx.strokeStyle=this.strokeStyle):null;
			ctx.stroke();
		}	
		ctx.restore();
		
		return this;
	}
	
	
	//直线的构造
	function Line(ctx,o){
		this.x=0,//x坐标
		this.y=0,//y坐标
		this.startX=0,//开始点x位置
		this.startY=0, //开始点y位置
		this.endX=0,//结束点x位置
		this.endY=0;//结束点y位置
		this.thin=false;//设置变细系数
		this.ctx=ctx;
		
		this.init(o);
	}
	Line.prototype.init=function(o){
		for(var key in o){
			this[key]=o[key];
		}
	}
	Line.prototype.render=function(){
		innerRender(this);
		
		function innerRender(obj){
			var ctx=obj.ctx;
			ctx.save()
			ctx.beginPath();
			ctx.translate(obj.x,obj.y);
			if(obj.thin){
				ctx.translate(0.5,0.5);
			}
			if(obj.lineWidth){//设定线宽
				ctx.lineWidth=obj.lineWidth;
			}
			if(obj.strokeStyle){
				ctx.strokeStyle=obj.strokeStyle;
			}
			//划线
		  	ctx.moveTo(obj.startX, obj.startY);
		  	ctx.lineTo(obj.endX, obj.endY);
		  	ctx.stroke();
		  	ctx.restore();
		}
	  	
	  	return this;
	}
	
	var _= util = {
		//画直线
		drawLine:function (ctx, startX, startY, endX, endY) {
		  	ctx.beginPath();
		  	ctx.moveTo(startX, startY);
		  	ctx.lineTo(endX, endY);
		  	ctx.stroke();
		  	ctx.closePath();
		},
		//获取属性值
		getStyle:function (obj, prop) {
			var prevComputedStyle = document.defaultView ? document.defaultView.getComputedStyle( obj, null ) : obj.currentStyle;
			return prevComputedStyle[prop];
		},
		getRandom:function(min,max){
			return parseInt(Math.random()*(max-min)+min);
		},
		//获取鼠标信息
		getOffset:function(e){
			return {
					x:e.offsetX,
					y:e.offsetY
				};
		},
		//循环
		each:function(arr,fn){
			var len = arr.length;
			for(var i=0;i<len;i++){
				fn(arr[i],i);
			}
		},
		getDecimals:function(value){
			return (value!=Math.floor(value))?(value.toString()).split('.')[1].length:0;
		}
		
		}
	
	var class2type={};	
	_.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(name) {
		class2type[ "[object " + name + "]" ] = name;
	});

	function getType( obj ) {
		return obj == null ?
			String( obj ) :
			class2type[ Object.prototype.toString.call(obj) ] || "undefined";
	}
	
	global.game=game;
})(window)

Vue新手入门指南

thbcm阅读(175)

Vue快速入门

前言

对于一名初入编程的新手来说,JavaScript 的语法偏向复杂,选择 Vue 库可以说是一个较为不错的体验。在很多方面,Vue 简化了 JavaScrip 语法,并且实现数据与视图的双向绑定,达到响应式页面的目的。

博主是一位大二的编程小白,以下的内容都是自己对 Vue 的理解,写本篇博客的目的是为了巩固自己对 Vue 的基础知识,大家可以作为一种笔记来观看,如果能够给学校 Vue 的朋友带来帮助,不胜荣幸,对于存在许多表达不恰当以及逻辑错误的地方,还望大家斧正。

参考资料:《Vue.js 1.0教程》

1. Vue实例和模板语法

<body>
	<div id="app"> 
		<p>{{message}}</p> 
	</div>
<script>
	new Vue({
		el:'#app',	
		data:{
			message:'hello,word!'
		},
		methods:{
		},
	})
</script>
</body>

  • el

Vue 语法与 JavaScript 一样写在 ​script​ 中,通 过 ​id​ 选择器绑定 DOM,在 Vue 中,只需要在 ​el​ 中对 DOM 的 id 进行挂载,可以简单的理解为钩子,​el​ 通过​ id="app"​的特征钩住了<div> 中的所有内容,这样我们就可以在 Vue 中实现对 DOM 中的操作。

  • data

Vue 中的 ​data​ 用于声明我们所要使用的数据,这样操作有利于我们在维护或者操作文档的时候能够更容易的清晰某一板块所需要修改的数据,并且不需要直接对 DOM 进行操作,此时的数据与 DOM 是双向绑定的,当我们对 ​data​ 中所声明的数据进行修改时,DOM 中同时也会发生响应式的变化。

  • methods

Methods​ 中包含的是我们对这个页面的整个逻辑以及页面中的触发事件,其中的内容相当于 JavaScript 中的 ​function​ 内容

2. 内置指令

在 Vue 中有许多内置指令,通过这些指令替换 JavaScript 中对文档的以及事件的操作。

  • v-html

v-html​:将数据以 html 标签形式更新

<!DOCTYPE>

<html>

<head>

    <meta charset="UTF-8">

    <title>w3cschool</title>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

<div id="app"> 

<p>{{website}}</p>

<p v-html="message">{{website}}</p> 

</div>

<script>

new Vue({

el:'#app',

data:{

website:'Vue,js',

message:'<h1>hello,word!</h1>'

},

methods:{

},

})

</script>

</body>

</html>

可以发现,第二个​<p>​标签中所绑定的 Vue.js 被 hello,word!所更新,并且在声明的 message 中​'<h1>hello,word!<\h1>'​通过 html 标签更新了其中的内容,看到的这是一个一级标题的 hello,word!。

  • v-text

<!DOCTYPE>

<html>

<head>

    <meta charset="UTF-8">

    <title>w3cschool</title>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

<div id="app"> 

<p>{{website}}</p>

<p v-text="message">{{website}}</p> 

</div>

<script>

new Vue({

el:'#app',

data:{

website:'Vue,js',

message:'<h1>hello,word!</h1>'

},

methods:{

},

})

</script>

</body>

</html>

通过 v-txet 指令,尽管仍然替换掉了​<p>​标签中的内容,但是仅仅是通过字符串的形式显示了出来,而不是像 html 一样通过 html 标签的形式显示。

  • v-cloak

代码加载的时候先加载 HTML,把插值语法当做 HTML 内容加载到页面上,当加载完 js 后才把插值语法替换掉,所以我们会看到闪烁问题,而 v-clock 可以解决这个问题。

<div v-cloak>{{msg}}</div>
<style type="text/css">
 [v-cloak]{
  display: none;
 }
 </style>

  • v-once


v-once​ 指令只渲染元素和组件一次,随后的渲染,使用了此指令的元素、组件及其所有的子节点,都会当作静态内容并跳过,这个可以用于优化更新性能。

<!DOCTYPE>

<html>

<head>

    <meta charset="UTF-8">

    <title>w3cschool</title>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

<div id="app"> 

<p v-once>can not change:{{website}}</p>

<p>change: {{website}}</p>

<p ><input type="text" v-model = "website"></p> 

</div>

<script>

new Vue({

el:'#app',

data:{

website:"hello"

},

methods:{

},

})

</script>

</body>

</html>

  • v-on

对于 Vue 的事件绑定使用内置的 v-on 指令来完成,以及传递参数。

<!DOCTYPE>

<html>

<head>

    <meta charset="UTF-8">

    <title>w3cschool</title>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

<div id="app"> 

<input type="button" value="单击事件" v-on:click="alert">

</div>

<script>

new Vue({

el:'#app',

data:{

},

methods:{

alert:function() {

alert('触发了点击事件');

}

},

})

</script>

</body>

</html>

在 ​v-on:click ​点击事件后面添加了命名为 alert 的方法,在此之前我试过直接使用​v-on:click="alert('触发了点击事件')"​,但是点击之后控制台报错,不知道有没有大神明白为什么会这个样子。

使用​ v-on​ 指令时,不仅仅可以触发点击事件,譬如双击事件以及键盘敲击事件等等,只需要修改 v-on:click or(mousedown、mouseup等),同时我们可以将 ​v-on:click​ 简写为 ​@click​,触发事件的方法必须写在 methods 中。

  • v-if

v-if​、​v-show​ 可以实现条件渲染,Vue 会根据表达式值的真假条件来渲染元素。还有可以与 v-if 搭配的 v-else、v-else-if 指令,类似与 JavaScript 中的 if-else。

简单来说,​v-if​相当于 JavaScript 中我们对 DOM 的条件操作,根据表达值的真假,从而对 DOM 进行有条件的操作,让我们来看看是如何操作的把。

<!DOCTYPE>

<html>

<head>

    <meta charset="UTF-8">

    <title>w3cschool</title>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

<div id="app"> 

<input type="button" value="切换"  @click="go">

<p v-if="jump">我跳出来拉</p>

</div>

<script>

new Vue({

el:'#app',

data:{

jump:false

},

methods:{

go:function(){

this.jump=!this.jump;

}

},

})

</script>

</body>

</html>

注意,​v-if​ 的默认布尔值为​ false​,并且​ v-if​ 是直接对 ​DOM​ 的操作,而随后的​ v-show ​是对样式的操作。

  • v-show


v-show​用法与​
v-if​大致一样,不同的是带有 v-show 的元素始终会被渲染并且保留在 DOM中,​
v-show​只是简单地切换元素的 CSS 属性 display,当模板属性为 true 的时候,控制台显示为 ​
display:block​;属性值为 false 的时候,控制台显示​
display:none​。

v-show不支持 ​
<tempalte>​ 语法,也不支持 v-else。

<body>
	<div id="app"> 
		<input type="button" value="切换"  @click="go">
		<p v-show="jump">我跳出来拉</p>
	</div>
<script>
	new Vue({
		el:'#app',	
		data:{
			jump:false
		},
		methods:{
			go:function(){
				this.jump=!this.jump;
			}
		},
	})
</script>
</body>

v-show​与​v-if​的区别

  1. 都是根据表达式的真假判断元素显示与隐藏
  2. v-if​只有在条件为真时,才对元素进行渲染,​v-show​无论初始条件为何,元素总会被渲染。
  3. v-show​初始开销更高,​v-if​的切换开销更高
  4. 频繁切换时用​v-show​;运行条件很少改变时用​v-if

  • v-for

在 Vue 中,提供了 v-for 指令用来循环数据。

<!DOCTYPE>

<html>

<head>

    <meta charset="UTF-8">

    <title>w3cschool</title>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

<div id="app">

<h>开始循环</h>

<li v-for="index in item">

{{index}}

</li>

</div>

<script>

new Vue({

el:'#app',

data:{

item:[1,2,3,4,5],

},

methods:{

},

})

</script>

</body>

</html>

<body>

<!DOCTYPE>

<html>

<head>

    <meta charset="UTF-8">

    <title>w3cschool</title>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

<div id="app">

<h>开始循环</h>

<li v-for="(index,items) in item">//index表示数组中的元素,items表实元素的下标

{{index}},{{items}}

</li>

</div>

<script>

new Vue({

el:'#app',

data:{

item:[1,2,3,4,5],

},

methods:{

},

})

</script>

</body>

</html>

  • v-model

  • 关于 v-model 最重要的就是 双向数据绑定。使用 Vue 操作 DOM 元素时,视图与数据依照任何的一方同时发生改变。
<body>
	<div id="app">
        输入内容:<input type="text" v-model="message"><br/>    <!--v-model绑定了输入框与message中的内容-->
        反转内容:{{reversedMessage}}
    </div>
<script>
	new Vue({
            el:'#app',
            data:{
                message:''
            },  
            computed:{              //计算属性在computed选项中定义,当计算属性所依赖的值发生变化时,这个属性的值会自动更新
                                    //computed可以换做methods定义一个方法实现相同的功能
                reversedMessage: function(){
                    return this.message.split('').reverse().join('') //选中message中的内容,反转后添加
                }
            }
        })
</script>
</body>

当我们在 input 输入框里面输入值时,所绑定的 message 属性值也发生了变化,当绑定成功,我们在 input 中输入的任何合法字符串或者数字时,Vue 都会重新更新 message 的属性值,从而符合我们所输入的值,再通过 ​reversedMessage​ 方法将 message 颠倒过来重新打印在=={{reversedMessage}}==,由于是双向数据绑定,三者是同时发生的。

  • v-bind

v-bind ​的作用是为元素绑定属性,写法v-bind:属性名,可以简写为“:属性名”。

<body>
	<div id="app">
		<img :src="imgsrc" :title="imgtitle">
	</div>
<script>
	new Vue({
		el:'#app',	
		data:{
		imgsrc:"xxx",
		imgtitle:"获得图片",
		},
		methods:{
		},
	})
</script>
</body>

无论是 class 还是其他标签,都可以通过“:标签名”来为元素绑定属性。对于绑定的元素内容是作为一个 JavaScript 变量,故而可以对其进行编写 JavaScript 的表达式。

小编在此推荐几门适合小白高效学习的 Vue 好课:

csdn可以学编程吗?

thbcm阅读(194)

CSDN.NET – 全球最大中文IT社区,为IT专业技术人员提供最全面的信息传播和服务平台
想必绝大多数从事计算机行业的人都有访问过这个网站,这是个 IT 社区,csdn 意为“中国软件开发网”,里面包罗万象,细分为网站, 杂志、图书、电子商务、企业服务、教育培训等,你想写的应有尽有。站里活跃着很多热心的码农大神们(老司机),如果有技术问题可以在这个网站上提问,相信很快就可以得到解答。最妙的是如果有编程的作业需要借鉴,完全可以在资源帖里找到源程序,(下载需要节操币)质量良莠不齐,需要自己斟酌了。


近年来 csdn 还出了 “csdn学院”和“csdn”两个 app,csdn 学院主导学习,类似于慕课适合初学者。csdn 的知识库也是很好的学习工具。

程序员学院

用 CSDN 学习编程可以学到多方面的编程知识,如 Web 全栈、java 等编程语言、移动开发等等。

  • Web全栈

Web 全栈中的免费课 HTML5/CSS 从入门到精通各个知识点都有,马上让新手对 HTML 掌握了解,只不过没有实战,差了点火候。

除了最基本的 HTML 和 CSS,JavaScriptjquery、Vue也是都有的,免费和收费都有很多优质内容

  • 编程语言

当然 Java、C/C++ 的课程也是非常丰富,还有 Python、PHP、C# 更加贴合用户需求,让用户在前端后端中快速选择,有目的性的学习课程

  • 其他

还有类似移动开发:手游开发、IOS、微信开发、Android。人工智能:机器学习、深度学习、语音识别。多种类型的课程非常全面。

总结

CSDN 学习编程还是蛮好的,学到老活到老。骚年冲吧!

联系我们