行列

行列 查看完整档案

杭州编辑平顶山学院  |  计算机科学与技术 编辑阿里巴巴  |  前端技术专家 编辑 xinglie.github.io 编辑
编辑

专注于web可视化框架的开发与应用
相关作品:
https://github.com/xinglie/re...
https://github.com/xinglie/pr...
欢迎试用、star与fork

个人动态

行列 发布了文章 · 8月27日

可视化脚手架介绍

项目地址

https://github.com/xinglie/re...

本项目提供可视化设计所需要的基础功能,比如标尺、拖动、旋转、多选、复制等。在此基础之上,设计器中可设计、编辑的元素则由插件化的形式提供,比如需要表格、图片则只添加这2个插件即可,开发人员也可以很方便的定制自己的插件

两种布局

绝对定位

绝对定位布局要求页面宽和高是固定尺寸的,比如需要投放大屏场景,则根据大屏的尺寸设置好相应的编辑区的大小。再比如需要打印,可针对纸张大小,如A4纸设置好相应的编辑区尺寸

绝对定位demo示意:https://xinglie.github.io/rep...
基于绝对定位扩展的复杂物联网编辑器demo:https://xinglie.github.io/rep...

流式布局

流式布局只需要给定宽度或不设置宽度,高度无须设置,整体自适应页面,这种更适用于活动、报表、管理等一系列的线上展示页面

流式布局demo示意:https://xinglie.github.io/rep...

以物联网为示例,编辑器讲解

image

设计元素

image

可设计的元素在目录 tmpl/elements 下面,可根据需要添加或删除相应的元素,并更新到index.ts中即可。
在插件里面desinger.ts是针对设计器使用的,该文件中指示设计器能设计哪些属性,对元素能否改变宽高、旋转等功能。针对像流式布局需要对设计后的页面做展示时,最终打包的代码并不需要包含designer.ts文件,做到了设计和展示分离

工具栏

image

工具栏提供撤销、重做功能,同时也支持快捷键Ctrl+ZCtrl+YCtrl+Shift+Z
6种对齐方式,需要对齐操作时,需要在设计区中选中2个以上的设计元素对齐按钮才会高亮可用显示
2种同步尺寸的方式,需要同步尺寸时,需要在设计区中选中2个以上的设计元素对齐按钮才会高亮可用显示。默认按选中元素的最大宽或高度同步,如果按下Shift键,则按最小宽度或高度同步尺寸
2种分散对齐方式,同样需要选中2个以上的元素
4种调整z轴的方式,z轴调整只能选中1个元素,如果某个元素已经处于最顶层或最底层,则相应的顶层调整按钮并不会启用
1个删除按钮,需要选中1个以上的元素时高亮可用显示

标尺

标尺下方的阴影会指示当前设计区是否处于激活状态,当处于激活状态时,相应的快捷键如Ctrl+Z才能使用
以下是设计区未激活时,标尺下方带阴影的状态

image

以下是设计区激活时,标尺下方的阴影消失</de>
image

2020-8-24修改为:标尺统一带阴影,编辑区激活状态调整为整个页面,只要当前页面激活,则可响应相应的键盘事件

当鼠标在标尺上移动时,会显示相应的位置辅助线,在标尺上鼠标点击后,会在相应的位置留下一条固定的辅助线。固定的辅助线也可以拖动改变位置以及删除等操作。

设计区

可直接拖动页面顶部设计元素添加到设计区,也可点击设计元素,默认添加到设计区的左上角,然后再拖动到设计区中希望的位置上。
改变设计元素的位置时,支持选中1个或多个,可直接使用鼠标拖动,可以按下键盘UpRightDownLeft四个方向键改变位置。每次按下移动1px,如果在按下方向键的同时,按下Shift键,则每次移动10px

元素面板

image

可查看当前添加到设计区中的元素,同时也支持鼠标移上后,在设计区中高亮显示相应的可设计元素,支持鼠标拖动调整设计元素在设计区中的z轴。
在元素面板某个元素上单击时,则直接选中该设计元素。在单击的同时按下ShiftCtrl键时,可同时选中多个设计元素

概览面板

image

概览面板主要显示整体的布局情况,方便您对整体布局有一个全局观。

属性面板

image

根据设计区中选中的元素不同,属性面板中展示的可设计元素也不同。
可设计属性在elements/xx/desinger.ts中定义(xx表示相应的插件目录名称)
当设计区中选中2个以上元素时,属性面板显示设计区的属性

贴边滚动

image

当拖动元素时,会在编辑区的四周显示淡淡的主题色边框条,当拖动元素到边框条上时,编辑区则会向该方向滚动

网格

image
默认编辑区显示背景色及背景的配置,当网格选项打开时,则背景色与背景图隐藏,显示网格选项
网格默认10pxX10px,可自行调整大小,最小4px最大40px,宽与高的值可不同

当网格选项打开且拖动吸附时,拖动的元素则自动磁吸到网格上。
当拖动多个元素时,则以鼠标下的元素为吸附元素,其它元素跟随移动,但不吸附,主要是因为多个元素的间距并不一定是网格的整数倍。

磁吸效果只在拖动时有效,如果通过键盘或右侧的属性面板直接修改坐标信息,则仍以最小单位1px进行。

对齐操作

元素未处于组合状态

对于未处于组合状态的元素,如果对选中的元素使用工具栏中的对齐工具进行操作,则会根据要对齐的操作,如顶部对齐,找出当前编辑区处于最顶部的元素,然后所有其它元素与该元素进行顶部对齐,其它对齐操作同样的道理。

如果在某个元素上点击鼠标右键,使用右键菜单中的对齐操作,则该次的对齐动作以鼠标下的元素为基准。如顶部对齐,则所有其它元素以鼠标下的元素为对齐元素,进行顶部对齐,其它对齐操作同样的道理。

元素处于组合状态

如果所有选择的元素属于同一个组,则表示对组内的元素进行修改,该对齐操作无论是使用工具栏还是鼠标右键,均同未处于组合状态。

如果所选择的元素属于不同的组
使用工具栏中的对齐工具时,先找出当前对齐操作的参考元素。如顶部对齐,则找出当前选中的所有元素处于编辑区最上部的那一个,然后该元素所处的分组中的所有元素均不动。再从选中的元素中找出相应的顶部最大的元素,当对齐时,其它元素所处的组内元素也一起移动相应的距离。

使用鼠标右键时,参考元素直接变成鼠标下面的元素,后续对齐操作如工具栏中的动作

同步宽高操作

如果使用工具栏中的宽高同步工具,因为无法获知以哪个元素为准进行同步,所以会算出最大或最小尺寸然后同步,如工具上的提示

image

如果需要精确控制,则需要使用右键菜单

因为是精确控制,所以右键菜单只支持2个元素选中时的同步操作,如下所示

image

以鼠标下的元素为要同步到的目标元素,其它元素为来源元素。

  1. 同步宽
把其它元素的宽度同步给鼠标下的元素
  1. 同步高
把其它元素的高度同步给鼠标下的元素
  1. 同步宽作为高
把其它元素的宽同步给鼠标下元素的高
  1. 同步高作为宽
把其它元素的高同步给鼠标下元素的宽

不是所有元素都支持同步宽高操作,比如表格,因为宽和高都是动态的,则它无法与其它有固定宽高的元素同步,SVG元素也同样的道理,它们都不具备固定的宽高,所以不能使用同步宽高的功能

选择元素

拉框选择

鼠标在编辑区中按下,然后拖动。鼠标会拖出一个虚线矩形方框,当鼠标拖出的虚线方框与元素所在的矩形相交或包含关系时,则对应的元素进入被选中状态。
当元素被旋转后,部分元素所在的矩形也会随着旋转,同样旋转后的矩形也需要与鼠标拖出的虚线矩形相交或包含时,元素才进入被选中状态

鼠标在拉框选择的时候,按下了Shift键,则本次拉框选择会叠加之前选中的元素。

当元素处于编辑锁定状态时,拉框选择无法选中编辑锁定的元素

鼠标点选

鼠标直接点击目标元素,则目标元素进入选中状态。
在按下ShiftCtrl的同时,使用鼠标点击目标元素,当目标元素处于未选中状态时,则进入选中状态。如果目标元素已处于选中状态,则会取消选中状态。
当编辑区中只有一个元素被选中时,且鼠标点击该元素,无论是否按下ShiftCtrl,该元素的选中状态均不会消失。

当元素处于编辑锁定状态时,使用鼠标点选时,可以选中该元素。如果是按下ShiftCtrl进入多选状态下的选择,则无法选中编辑锁定的元素

tab键

z轴从小到大选择编辑区中的元素,当编辑区中的元素过多且有重叠,不方便使用鼠标选择时有用。如果按下Tab同时按下了Shift则按z轴从大到小的顺序依次选择元素

元素面板

elements
当鼠标hover在元素面板中的元素时,会在编辑区显示一下半透明主题色矩形,告诉使用者当前对应是编辑区哪个元素。
当鼠标单击元素时,则该元素进入选中状态。
在按下ShiftCtrl的同时点击元素面板中的元素,鼠标下的元素如果处于未选中状态时,则进入选中状态。否则会取消选中状态。
当元素面板中的元素只有一个元素被选中时,且鼠标点击该元素,无论是否按下ShiftCtrl,该元素的选中状态均不会消失。

当元素处于编辑锁定状态时,元素面板无法多选的时候选中编辑锁定的元素,但可以单击选择

当元素处于单选、多选、组合等状态时,其被选中状态会表现出不同的界面显示,方便使用人员进行区分。

物联网编辑器快捷键大全

按键描述条件
DeleteBackspace删除选中的元素需要编辑区选中1个以上的元素时按下有效
Ctrl+Z撤销操作需要有历史记录
Ctrl+Shift+ZCtrl+Y重做操作需要有撤销操作
Tab依z轴从小到大选择编辑区中的元素,当编辑区中的元素过多且有重叠,不方便使用鼠标选择时有用。如果按下Tab同时按下了Shift则按z轴从大到小的顺序依次选择元素编辑区处于激活状态
Left左箭头按下时,编辑区选中的元素向左移动1像素。如果按下Left同时按下了 Shift,则向左移动10像素。需要编辑区选中1个以上的元素时按下有效
Up上箭头按下时,编辑区选中的元素向上移动1像素。如果按下Up同时按下了 Shift,则向上移动10像素。需要编辑区选中1个以上的元素时按下有效
Right右箭头按下时,编辑区选中的元素向右移动1像素。如果按下Right同时按下了 Shift,则向右移动10像素。需要编辑区选中1个以上的元素时按下有效
Down下箭头按下时,编辑区选中的元素向下移动1像素。如果按下Down同时按下了 Shift,则向下移动10像素。需要编辑区选中1个以上的元素时按下有效
Ctrl+A全选编辑区中的元素
Ctrl+C复制编辑区中选中的元素需要编辑区有1个以上的元素处于选中状态
Ctrl+V粘贴剪切板中复制的元素需要先复制元素
Ctrl+X剪切编辑区中选中的元素需要编辑区有1个以上的元素处于选中状态
Ctrl+G组合选中的元素需要编辑区有2个以上的元素处于选中状态,且不属于同一个分组
Shift+G取消组合编辑区中选中的元素需要编辑区有1个以上的元素处于组合状态
U把选中的元素向上调整一个层级需要编辑区有且只有1个元素处于选中状态
T把选中的元素调整到最顶层级需要编辑区有且只有1个元素处于选中状态
D把选中的元素向下调整一个层级需要编辑区有且只有1个元素处于选中状态
B把选中的元素调整到最底层级需要编辑区有且只有1个元素处于选中状态
数字1打开或关闭元素面板
数字2打开或关闭预览面板
数字3打开或关闭数据面板
数字4打开或关闭属性面板
Shift+Z打开或关闭所有可拖动的面板,如属性、预览、元素、数据等面板
Ctrl+加号放大编辑区
Ctrl+减号缩小编辑区
Ctrl+数字0恢复编辑区缩放

相关示例

绝对定位布局

https://xinglie.github.io/rep...

流式布局

https://xinglie.github.io/rep...

iot应用demo

https://xinglie.github.io/rep...

iot展示demo

https://xinglie.github.io/rep...

获取源代码

绝对定位脚手架代码

https://github.com/xinglie/re... (代码较旧,仅供参考,最新代码请联系我获取)

流式布局脚手架代码

联系我获取

使用绝对定位脚手架初步完成的物联网编辑器

联系我获取

使用设计完后的物联网数据展示的页面

联系我获取

查看原文

赞 0 收藏 0 评论 0

行列 收藏了文章 · 8月6日

高仿 优酷 播放器 dash-player

预览

先看效果
http://yangchaojie.top/plugin/dash-player

image.png

why do it

如果你点了上面的地址。你就会发现,视频要加载几秒时间才能播放,虽然我已经使用的流媒体形式(下面会讲)。主要原因还是服务器带宽不够,像我这样个人服务器只有1M的速度,一个小短片15M的话就要加载十几秒才能开始播放。
像youku,B站 这种视频网站,虽然带宽高,但访问的人多,视频也更大,不会蠢蠢的放的静态MP4地址上去,等加载完2G的妇联4,妇联5都快出来了。

How to solve it

扯了这么多,进入正题,怎么解决大视频加载问题。

本文档内容使用的操作环境

系统 linux 
浏览器 firefox (`一定要使用火狐,先放弃一下chrome`)
编辑器 vscode 

有业务经验的人肯定知道,文件体积大就分割嘛,就像分片上传一样,浏览器不能一次上传太大文件那就分割上传。视频文件大就分段加载。

看看youku 怎么做的

来看下youku 上的video.src 上绑定的啥
image.png

什么是blob:https://...

Blob URL(参考W3C,官方名称)或Object-URL(参考MDN和方法名称)与BlobFile对象一起使用。

Blob URL只能由浏览器在内部生成。URL.createObjectURL()将创建一个特殊的
File 对象、Blob 对象或者 MediaSource 对的引用,

那么普通视频文件地址怎么转成blob:https://...

  • 创建一个 File对象 的引用

直接使用 input 选择本地视频即可

<input id="upload" type="file" />    
<video id="preview" data-original=""></video>
<script>const upload = document.querySelector("#upload");
    const preview = document.querySelector("#preview");
    upload.onchange = function () {
        const file = upload.files[0]; //File对象
        const src = URL.createObjectURL(file);
        // 此时video.scr上的地址就不在是文件路径,而是指向一块Blob对象(存储视频二进制数据对象的地址)
        preview.src = src;
    };
</script>
  • 创建一个 Blob对象 的引用

    Blob(Binary Large Object) 对象表示一个不可变、原始数据的类文件对象。
    Blob对象代表了一段二进制数据。其它操作二进制数据的接口都是建立在此对象的基础之上。

File对象其实继承自Blob对象,并提供了提供了name , lastModifiedDate, size ,type 等基础元数据。
所以他们互相转换很容易,File=>Blob

XMLHttpRequest第二版XHR2允许服务器返回二进制数据,创建blob对象的引用更适合加载网络视频

function ajax(url, cb) {
    const xhr = new XMLHttpRequest();
    xhr.open("get", url);
    xhr.responseType = "blob"; 
    // "text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象
    xhr.onload = function () {
        cb(xhr.response);
    };
    xhr.send();
}
ajax('video.mp4', function (res) {
    const src = URL.createObjectURL(res);
    video.src = src;
})

image.png

用调试工具查看视频标签的src属性已经变成一个Blob URL,表面上看起来是不是和各大视频网站形式一致了,但是考虑一个问题,这种形式要等到请求完全部视频数据才能播放,依然面临大视频加载缓慢的问题。

答案应该就在对MediaSource 的引用上。

  • 对MediaSource的引用

    什么是Media Source

    MediaSource包含在Media Source Extensions (MSE)标准中。
    MSE解决的问题:现有架构过于简单,只能满足一次播放整个曲目的需要,无法实现拆分/合并数个缓冲文件。
    MSE内容:MSE 使我们可以把通常的单个媒体文件的 src 值替换成引用 MediaSource 对象(一个包含即将播放的媒体文件的准备状态等信息的容器),以及引用多个 SourceBuffer 对象(代表多个组成整个串流的不同媒体块)的元素。MSE 让我们能够根据内容获取的大小和频率,或是内存占用详情(例如什么时候缓存被回收),进行更加精准地控制。 它是基于它可扩展的 API 建立自适应比特率流客户端(例如DASH 或 HLS 的客户端)的基础。

简单的说就是先让video 加载 MediaSource 对象,但MediaSource 对象没有视频具体内容,内容被分割在SourceBuffer 对象中,可能有音频SourceBuffer,视频SourceBuffer,字幕SourceBuffer,
image.png

看到buffer就有谱了,可以向缓存区一点点写入video数据
SourceBuffer.appendBuffer(source)

source

一个 BufferSource 对象(ArrayBufferView 或 ArrayBuffer),存储了你要添加到 SourceBuffer 中去的媒体片段数据。

值得注意的是这里使用的是追加ArrayBuffer
xhr.responseType 应该等于 "arraybuffer"
ArrayBuffer对象也代表储存二进制数据的一段内存,
Blob与ArrayBuffer的区别是,除了原始字节以外它还提供了mime type作为元数据,Blob和ArrayBuffer之间可以进行转换。

阅读文档,可以很快写出一个加载示例

我这里已经对视频做好了分割,后面会讲如何分割,下面chunk-stream0 代表240分辨率,如果感觉加载快可以换成chunk-stream1 代表480分辨率,甚至chunk-stream2 代表1280分辨率,相应init-stream0 也要变化
<video width="400" controls autoplay="autoplay"></video>

<script>
    const video = document.querySelector('video');
    //视频资源存放路径,假设下面有5个分段视频 video1.mp4 ~ video5.mp4,第一个段为初始化视频init.mp4
    const assetURL = "http://yangchaojie.top/allow_origin/mpd/";
    //视频格式和编码信息,主要为判断浏览器是否支持视频格式,但如果信息和视频不符可能会报错
    const mimeCodec = 'video/mp4';
    if ('MediaSource' in window) {
        const mediaSource = new MediaSource();
        video.pause();
        video.src = URL.createObjectURL(mediaSource); //将video与MediaSource绑定,此处生成一个Blob URL
        mediaSource.addEventListener('sourceopen', sourceOpen); //可以理解为容器打开
    } else {
        //浏览器不支持该视频格式
        console.error('Unsupported MIME type or codec: ', mimeCodec);
    }

    function sourceOpen() {
        const mediaSource = this;
        const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
        let i = 1;
        function getNextVideo(url) {
            //ajax代码实现翻看上文,数据请求类型为arraybuffer
            ajax(url, function (buf) {
                //往容器中添加请求到的数据,不会影响当下的视频播放。

                sourceBuffer.appendBuffer(new Uint8Array(buf));
            });
        }
        //每次appendBuffer数据更新完之后就会触发
        sourceBuffer.addEventListener("updateend", function () {
            if (i === 1) {
                //第一个初始化视频加载完就开始播放
                video.play();
            }
            if (i < 12) {
                //一段视频加载完成后,请求下一段视频
                getNextVideo(`${assetURL}/chunk-stream0-000${String(i).padStart(2, 0)}.m4s`);
            }
            if (i === 12) {
                //全部视频片段加载完关闭容器
                mediaSource.endOfStream();
                URL.revokeObjectURL(video.src); //Blob URL已经使用并加载,不需要再次使用的话可以释放掉。
            }
            i++;
        });
        //加载初始视频
        getNextVideo(`${assetURL}/init-stream0.m4s`);
    };

    function ajax(url, cb) {
        const xhr = new XMLHttpRequest();
        xhr.open("get", url);
        xhr.responseType = "arraybuffer"; // "text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象
        xhr.onload = function () {
            cb(xhr.response);
        };
        xhr.send();
    }
</script>

查看控制台 已经不断的请求分割的片段
image.png

但你肯定发现没有音频信息,对的,你音响坏了,
这里我们只加载了视频信息,并没有加载音频,所以我们还要加载音频

<video width="400" controls autoplay="autoplay"></video>
    <script>
        const video = document.querySelector('video');
        //视频资源存放路径,假设下面有5个分段视频 video1.mp4 ~ video5.mp4,第一个段为初始化视频init.mp4
        const assetURL = "http://yangchaojie.top/allow_origin/mpd/";
        //视频格式和编码信息,主要为判断浏览器是否支持视频格式,但如果信息和视频不符可能会报错
        const mimeCodec = 'video/mp4';
        if ('MediaSource' in window) {
            const mediaSource = new MediaSource();
            video.pause();
            video.src = URL.createObjectURL(mediaSource); //将video与MediaSource绑定,此处生成一个Blob URL
            mediaSource.addEventListener('sourceopen', sourceOpen); //可以理解为容器打开
        } else {
            //浏览器不支持该视频格式
            console.error('Unsupported MIME type or codec: ', mimeCodec);
        }
        function appendVideo(mediaSource) {
            const sourceBuffer = mediaSource.addSourceBuffer('video/mp4');
            let i = 1;
            function getNextVideo(url) {
                //ajax代码实现翻看上文,数据请求类型为arraybuffer
                ajax(url, function (buf) {
                    //往容器中添加请求到的数据,不会影响当下的视频播放。

                    sourceBuffer.appendBuffer(new Uint8Array(buf));
                });
            }
            //每次appendBuffer数据更新完之后就会触发
            sourceBuffer.addEventListener("updateend", function () {
                if (i === 1) {
                    //第一个初始化视频加载完就开始播放
                    video.play();
                }
                if (i < 12) {
                    //一段视频加载完成后,请求下一段视频
                    getNextVideo(`${assetURL}/chunk-stream0-000${String(i).padStart(2, 0)}.m4s`);
                }
                if (i === 12) {
                    //全部视频片段加载完关闭容器
                    mediaSource.endOfStream();
                    URL.revokeObjectURL(video.src); //Blob URL已经使用并加载,不需要再次使用的话可以释放掉。
                }
                i++;
            });
            //加载初始视频
            getNextVideo(`${assetURL}/init-stream0.m4s`);
        }
        function appendAudio(mediaSource) {
            const sourceBuffer = mediaSource.addSourceBuffer('audio/mp4');
            let i = 1;
            function getNextVideo(url) {
                //ajax代码实现翻看上文,数据请求类型为arraybuffer
                ajax(url, function (buf) {
                    //往容器中添加请求到的数据,不会影响当下的视频播放。

                    sourceBuffer.appendBuffer(new Uint8Array(buf));
                });
            }
            //每次appendBuffer数据更新完之后就会触发
            sourceBuffer.addEventListener("updateend", function () {
                if (i === 1) {
                    //第一个初始化视频加载完就开始播放
                    video.play();
                }
                if (i < 12) {
                    //一段视频加载完成后,请求下一段视频
                    getNextVideo(`${assetURL}/chunk-stream3-000${String(i).padStart(2, 0)}.m4s`);
                }
                if (i === 12) {
                    //全部视频片段加载完关闭容器
                    mediaSource.endOfStream();
                    URL.revokeObjectURL(video.src); //Blob URL已经使用并加载,不需要再次使用的话可以释放掉。
                }
                i++;
            });
            //加载初始视频
            getNextVideo(`${assetURL}/init-stream3.m4s`);
        }
        function sourceOpen() {
            const mediaSource = this;
            appendVideo(mediaSource)
            appendAudio(mediaSource)
        };

        function ajax(url, cb) {
            const xhr = new XMLHttpRequest();
            xhr.open("get", url);
            xhr.responseType = "arraybuffer"; // "text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象
            xhr.onload = function () {
                cb(xhr.response);
            };
            xhr.send();
        }
    </script>

稍微改动下代码,ok 现在一个基本流媒体播放 搞定
image.png

但是,还有一个问题,很严重的问题,就是没有办法拖动进度。
因为它不知道整个视频是什么样的,有多长,是否有声音轨,有几条等等。只知道加载过的片短视频是什么样的。
所以需要一个描述文件 mpd。

MPD

MPD是一个XML文件,描述了媒体的分段方式,类型和编解码器(此处为MP4),视频的比特率,长度和基本分段大小。MPD文件还能包含音频信息,您可以将内容拆分为视频和音频播放器的单独流(就像上面我拆成了多个流)。

DASH

那么MPD 文件和上面的分段文件 是怎么生成的?需要先了解DASH
DASH(Dynamic Adaptive Streaming over HTTP )是一个规范了自适应内容应当如何被获取的协议。它实际上是建立在 MSE 顶部的一个层,用来构建自适应比特率串流客户端。虽然已经有一个类似的协议了(例如 HTTP 串流直播(HLS)),但 DASH 有最好的跨平台兼容性。

是一种服务端、客户端的流媒体解决方案:
服务端:
将视频内容分割为一个个分片,每个分片可以存在不同的编码形式(不同的codec、profile、分辨率、码率等);
播放器端:
就可以根据自由选择需要播放的媒体分片;可以实现adaptive bitrate streaming技术。不同画质内容无缝切换,提供更好的播放体验。

个人理解:MSE是标准,描述了媒体文件可以流式接收,DASH是协议规范了媒体文件如何分轨,如何分段,如何接收。

概念太多,给大家看点实际的

ffmpeg -i  你的文件.mp4 -c copy -use_template 0 -single_file 0  -f dash index.mpd

执行上面命令可以得到 所需的 mpd 文件 和 分段后媒体文件
ffmpeg可以 面向搜索引擎安装。

有了描述文件MPD ,怎么解决拖动视频

很简单了,mpd已经描述了媒体长度提前告诉mediaSource就行了

MediaSource 接口的属性 duration 用来获取或者设置当前媒体展示的时长.
image.png

我使用的视频长度是1M8.2s,也就是1分钟+8.2秒=68.2秒

mediaSource.duration = 68.2;

image.png

现在再来看看视频就有长度,可以拖动进度了。
image.png

有点小激动

到这里还只是仅仅正常播放,还有许多问题没有解决,比如现在拖动进度虽然可以播放,但还是要加载完之前内容片段才行,需要改进成只加载当前,还有切换多分辨率没有实现,最头痛的是兼容性,如果你坚持使用chrome或其他浏览器没有使用火狐,应该是大部分代码没有办法运行。问题还很多,这时候就要找轮子了。(也没时间搞,因为公司倒闭了,要花点时间找工作,过段时间再深入研究)

投入 dash.js 怀抱

Dash.js是用JavaScript编写的开源MPEG-DASH视频播放器。其目标是提供一个健壮的跨平台播放器,可以在需要视频播放的应用程序中自由重用。
高仿优酷播放器就是应用了dash.js, 但是dash.js也存在一些问题(也不算问题,就是没有直接提供API),比如不能立即切换分辨率,必须等当前已加载片段播放完后才能切换,所以在dash.js 基础上稍加包裹,不敢说封装,人家已经相当完美。提供一些更易上手的播放器API。dash-player

资源下载

dash-demo

参考文档

查看原文

行列 赞了文章 · 8月6日

高仿 优酷 播放器 dash-player

预览

先看效果
http://yangchaojie.top/plugin/dash-player

image.png

why do it

如果你点了上面的地址。你就会发现,视频要加载几秒时间才能播放,虽然我已经使用的流媒体形式(下面会讲)。主要原因还是服务器带宽不够,像我这样个人服务器只有1M的速度,一个小短片15M的话就要加载十几秒才能开始播放。
像youku,B站 这种视频网站,虽然带宽高,但访问的人多,视频也更大,不会蠢蠢的放的静态MP4地址上去,等加载完2G的妇联4,妇联5都快出来了。

How to solve it

扯了这么多,进入正题,怎么解决大视频加载问题。

本文档内容使用的操作环境

系统 linux 
浏览器 firefox (`一定要使用火狐,先放弃一下chrome`)
编辑器 vscode 

有业务经验的人肯定知道,文件体积大就分割嘛,就像分片上传一样,浏览器不能一次上传太大文件那就分割上传。视频文件大就分段加载。

看看youku 怎么做的

来看下youku 上的video.src 上绑定的啥
image.png

什么是blob:https://...

Blob URL(参考W3C,官方名称)或Object-URL(参考MDN和方法名称)与BlobFile对象一起使用。

Blob URL只能由浏览器在内部生成。URL.createObjectURL()将创建一个特殊的
File 对象、Blob 对象或者 MediaSource 对的引用,

那么普通视频文件地址怎么转成blob:https://...

  • 创建一个 File对象 的引用

直接使用 input 选择本地视频即可

<input id="upload" type="file" />    
<video id="preview" data-original=""></video>
<script>const upload = document.querySelector("#upload");
    const preview = document.querySelector("#preview");
    upload.onchange = function () {
        const file = upload.files[0]; //File对象
        const src = URL.createObjectURL(file);
        // 此时video.scr上的地址就不在是文件路径,而是指向一块Blob对象(存储视频二进制数据对象的地址)
        preview.src = src;
    };
</script>
  • 创建一个 Blob对象 的引用

    Blob(Binary Large Object) 对象表示一个不可变、原始数据的类文件对象。
    Blob对象代表了一段二进制数据。其它操作二进制数据的接口都是建立在此对象的基础之上。

File对象其实继承自Blob对象,并提供了提供了name , lastModifiedDate, size ,type 等基础元数据。
所以他们互相转换很容易,File=>Blob

XMLHttpRequest第二版XHR2允许服务器返回二进制数据,创建blob对象的引用更适合加载网络视频

function ajax(url, cb) {
    const xhr = new XMLHttpRequest();
    xhr.open("get", url);
    xhr.responseType = "blob"; 
    // "text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象
    xhr.onload = function () {
        cb(xhr.response);
    };
    xhr.send();
}
ajax('video.mp4', function (res) {
    const src = URL.createObjectURL(res);
    video.src = src;
})

image.png

用调试工具查看视频标签的src属性已经变成一个Blob URL,表面上看起来是不是和各大视频网站形式一致了,但是考虑一个问题,这种形式要等到请求完全部视频数据才能播放,依然面临大视频加载缓慢的问题。

答案应该就在对MediaSource 的引用上。

  • 对MediaSource的引用

    什么是Media Source

    MediaSource包含在Media Source Extensions (MSE)标准中。
    MSE解决的问题:现有架构过于简单,只能满足一次播放整个曲目的需要,无法实现拆分/合并数个缓冲文件。
    MSE内容:MSE 使我们可以把通常的单个媒体文件的 src 值替换成引用 MediaSource 对象(一个包含即将播放的媒体文件的准备状态等信息的容器),以及引用多个 SourceBuffer 对象(代表多个组成整个串流的不同媒体块)的元素。MSE 让我们能够根据内容获取的大小和频率,或是内存占用详情(例如什么时候缓存被回收),进行更加精准地控制。 它是基于它可扩展的 API 建立自适应比特率流客户端(例如DASH 或 HLS 的客户端)的基础。

简单的说就是先让video 加载 MediaSource 对象,但MediaSource 对象没有视频具体内容,内容被分割在SourceBuffer 对象中,可能有音频SourceBuffer,视频SourceBuffer,字幕SourceBuffer,
image.png

看到buffer就有谱了,可以向缓存区一点点写入video数据
SourceBuffer.appendBuffer(source)

source

一个 BufferSource 对象(ArrayBufferView 或 ArrayBuffer),存储了你要添加到 SourceBuffer 中去的媒体片段数据。

值得注意的是这里使用的是追加ArrayBuffer
xhr.responseType 应该等于 "arraybuffer"
ArrayBuffer对象也代表储存二进制数据的一段内存,
Blob与ArrayBuffer的区别是,除了原始字节以外它还提供了mime type作为元数据,Blob和ArrayBuffer之间可以进行转换。

阅读文档,可以很快写出一个加载示例

我这里已经对视频做好了分割,后面会讲如何分割,下面chunk-stream0 代表240分辨率,如果感觉加载快可以换成chunk-stream1 代表480分辨率,甚至chunk-stream2 代表1280分辨率,相应init-stream0 也要变化
<video width="400" controls autoplay="autoplay"></video>

<script>
    const video = document.querySelector('video');
    //视频资源存放路径,假设下面有5个分段视频 video1.mp4 ~ video5.mp4,第一个段为初始化视频init.mp4
    const assetURL = "http://yangchaojie.top/allow_origin/mpd/";
    //视频格式和编码信息,主要为判断浏览器是否支持视频格式,但如果信息和视频不符可能会报错
    const mimeCodec = 'video/mp4';
    if ('MediaSource' in window) {
        const mediaSource = new MediaSource();
        video.pause();
        video.src = URL.createObjectURL(mediaSource); //将video与MediaSource绑定,此处生成一个Blob URL
        mediaSource.addEventListener('sourceopen', sourceOpen); //可以理解为容器打开
    } else {
        //浏览器不支持该视频格式
        console.error('Unsupported MIME type or codec: ', mimeCodec);
    }

    function sourceOpen() {
        const mediaSource = this;
        const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
        let i = 1;
        function getNextVideo(url) {
            //ajax代码实现翻看上文,数据请求类型为arraybuffer
            ajax(url, function (buf) {
                //往容器中添加请求到的数据,不会影响当下的视频播放。

                sourceBuffer.appendBuffer(new Uint8Array(buf));
            });
        }
        //每次appendBuffer数据更新完之后就会触发
        sourceBuffer.addEventListener("updateend", function () {
            if (i === 1) {
                //第一个初始化视频加载完就开始播放
                video.play();
            }
            if (i < 12) {
                //一段视频加载完成后,请求下一段视频
                getNextVideo(`${assetURL}/chunk-stream0-000${String(i).padStart(2, 0)}.m4s`);
            }
            if (i === 12) {
                //全部视频片段加载完关闭容器
                mediaSource.endOfStream();
                URL.revokeObjectURL(video.src); //Blob URL已经使用并加载,不需要再次使用的话可以释放掉。
            }
            i++;
        });
        //加载初始视频
        getNextVideo(`${assetURL}/init-stream0.m4s`);
    };

    function ajax(url, cb) {
        const xhr = new XMLHttpRequest();
        xhr.open("get", url);
        xhr.responseType = "arraybuffer"; // "text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象
        xhr.onload = function () {
            cb(xhr.response);
        };
        xhr.send();
    }
</script>

查看控制台 已经不断的请求分割的片段
image.png

但你肯定发现没有音频信息,对的,你音响坏了,
这里我们只加载了视频信息,并没有加载音频,所以我们还要加载音频

<video width="400" controls autoplay="autoplay"></video>
    <script>
        const video = document.querySelector('video');
        //视频资源存放路径,假设下面有5个分段视频 video1.mp4 ~ video5.mp4,第一个段为初始化视频init.mp4
        const assetURL = "http://yangchaojie.top/allow_origin/mpd/";
        //视频格式和编码信息,主要为判断浏览器是否支持视频格式,但如果信息和视频不符可能会报错
        const mimeCodec = 'video/mp4';
        if ('MediaSource' in window) {
            const mediaSource = new MediaSource();
            video.pause();
            video.src = URL.createObjectURL(mediaSource); //将video与MediaSource绑定,此处生成一个Blob URL
            mediaSource.addEventListener('sourceopen', sourceOpen); //可以理解为容器打开
        } else {
            //浏览器不支持该视频格式
            console.error('Unsupported MIME type or codec: ', mimeCodec);
        }
        function appendVideo(mediaSource) {
            const sourceBuffer = mediaSource.addSourceBuffer('video/mp4');
            let i = 1;
            function getNextVideo(url) {
                //ajax代码实现翻看上文,数据请求类型为arraybuffer
                ajax(url, function (buf) {
                    //往容器中添加请求到的数据,不会影响当下的视频播放。

                    sourceBuffer.appendBuffer(new Uint8Array(buf));
                });
            }
            //每次appendBuffer数据更新完之后就会触发
            sourceBuffer.addEventListener("updateend", function () {
                if (i === 1) {
                    //第一个初始化视频加载完就开始播放
                    video.play();
                }
                if (i < 12) {
                    //一段视频加载完成后,请求下一段视频
                    getNextVideo(`${assetURL}/chunk-stream0-000${String(i).padStart(2, 0)}.m4s`);
                }
                if (i === 12) {
                    //全部视频片段加载完关闭容器
                    mediaSource.endOfStream();
                    URL.revokeObjectURL(video.src); //Blob URL已经使用并加载,不需要再次使用的话可以释放掉。
                }
                i++;
            });
            //加载初始视频
            getNextVideo(`${assetURL}/init-stream0.m4s`);
        }
        function appendAudio(mediaSource) {
            const sourceBuffer = mediaSource.addSourceBuffer('audio/mp4');
            let i = 1;
            function getNextVideo(url) {
                //ajax代码实现翻看上文,数据请求类型为arraybuffer
                ajax(url, function (buf) {
                    //往容器中添加请求到的数据,不会影响当下的视频播放。

                    sourceBuffer.appendBuffer(new Uint8Array(buf));
                });
            }
            //每次appendBuffer数据更新完之后就会触发
            sourceBuffer.addEventListener("updateend", function () {
                if (i === 1) {
                    //第一个初始化视频加载完就开始播放
                    video.play();
                }
                if (i < 12) {
                    //一段视频加载完成后,请求下一段视频
                    getNextVideo(`${assetURL}/chunk-stream3-000${String(i).padStart(2, 0)}.m4s`);
                }
                if (i === 12) {
                    //全部视频片段加载完关闭容器
                    mediaSource.endOfStream();
                    URL.revokeObjectURL(video.src); //Blob URL已经使用并加载,不需要再次使用的话可以释放掉。
                }
                i++;
            });
            //加载初始视频
            getNextVideo(`${assetURL}/init-stream3.m4s`);
        }
        function sourceOpen() {
            const mediaSource = this;
            appendVideo(mediaSource)
            appendAudio(mediaSource)
        };

        function ajax(url, cb) {
            const xhr = new XMLHttpRequest();
            xhr.open("get", url);
            xhr.responseType = "arraybuffer"; // "text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象
            xhr.onload = function () {
                cb(xhr.response);
            };
            xhr.send();
        }
    </script>

稍微改动下代码,ok 现在一个基本流媒体播放 搞定
image.png

但是,还有一个问题,很严重的问题,就是没有办法拖动进度。
因为它不知道整个视频是什么样的,有多长,是否有声音轨,有几条等等。只知道加载过的片短视频是什么样的。
所以需要一个描述文件 mpd。

MPD

MPD是一个XML文件,描述了媒体的分段方式,类型和编解码器(此处为MP4),视频的比特率,长度和基本分段大小。MPD文件还能包含音频信息,您可以将内容拆分为视频和音频播放器的单独流(就像上面我拆成了多个流)。

DASH

那么MPD 文件和上面的分段文件 是怎么生成的?需要先了解DASH
DASH(Dynamic Adaptive Streaming over HTTP )是一个规范了自适应内容应当如何被获取的协议。它实际上是建立在 MSE 顶部的一个层,用来构建自适应比特率串流客户端。虽然已经有一个类似的协议了(例如 HTTP 串流直播(HLS)),但 DASH 有最好的跨平台兼容性。

是一种服务端、客户端的流媒体解决方案:
服务端:
将视频内容分割为一个个分片,每个分片可以存在不同的编码形式(不同的codec、profile、分辨率、码率等);
播放器端:
就可以根据自由选择需要播放的媒体分片;可以实现adaptive bitrate streaming技术。不同画质内容无缝切换,提供更好的播放体验。

个人理解:MSE是标准,描述了媒体文件可以流式接收,DASH是协议规范了媒体文件如何分轨,如何分段,如何接收。

概念太多,给大家看点实际的

ffmpeg -i  你的文件.mp4 -c copy -use_template 0 -single_file 0  -f dash index.mpd

执行上面命令可以得到 所需的 mpd 文件 和 分段后媒体文件
ffmpeg可以 面向搜索引擎安装。

有了描述文件MPD ,怎么解决拖动视频

很简单了,mpd已经描述了媒体长度提前告诉mediaSource就行了

MediaSource 接口的属性 duration 用来获取或者设置当前媒体展示的时长.
image.png

我使用的视频长度是1M8.2s,也就是1分钟+8.2秒=68.2秒

mediaSource.duration = 68.2;

image.png

现在再来看看视频就有长度,可以拖动进度了。
image.png

有点小激动

到这里还只是仅仅正常播放,还有许多问题没有解决,比如现在拖动进度虽然可以播放,但还是要加载完之前内容片段才行,需要改进成只加载当前,还有切换多分辨率没有实现,最头痛的是兼容性,如果你坚持使用chrome或其他浏览器没有使用火狐,应该是大部分代码没有办法运行。问题还很多,这时候就要找轮子了。(也没时间搞,因为公司倒闭了,要花点时间找工作,过段时间再深入研究)

投入 dash.js 怀抱

Dash.js是用JavaScript编写的开源MPEG-DASH视频播放器。其目标是提供一个健壮的跨平台播放器,可以在需要视频播放的应用程序中自由重用。
高仿优酷播放器就是应用了dash.js, 但是dash.js也存在一些问题(也不算问题,就是没有直接提供API),比如不能立即切换分辨率,必须等当前已加载片段播放完后才能切换,所以在dash.js 基础上稍加包裹,不敢说封装,人家已经相当完美。提供一些更易上手的播放器API。dash-player

资源下载

dash-demo

参考文档

查看原文

赞 2 收藏 2 评论 0

行列 收藏了文章 · 1月22日

两个文本相似度算法实现和对比

背景

最近做一个爬虫相关的项目,需要排除掉一些相似的链接,比如分页控件里上一页,下一页等等没什么用的链接.

编辑距离算法

编辑距离,又称Levenshtein距离(莱文斯坦距离也叫做Edit Distance),是指两个字串之间,由一个转成另一个所需的最少编辑操作次数,如果它们的距离越大,说明它们越是不同。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

这个概念是由俄罗斯科学家Vladimir Levenshtein在1965年提出来的,所以也叫 Levenshtein 距离。它可以用来做DNA分析,拼字检测,抄袭识别等等。总是比较相似的,或多或少我们可以考虑编辑距离。

如果你去搜索编辑距离算法的话可能会看到下面的例子

如果str1=”ivan”,str2=”ivan”,那么经过计算后等于 0。没有经过转换。相似度=1-0/Math.Max(str1.length,str2.length)=1

如果str1=”ivan1”,str2=”ivan2”,那么经过计算后等于1。str1的”1”转换”2”,转换了一个字符,所以距离是1,相似度=1-1/Math.Max(str1.length,str2.length)=0.8

注意算法中的1,其实是固定的值.我就是因为这个值总是算不对走了一些弯路.还有一些文章中介绍算法的时候会用矩阵计算.但是我数学功底很差,所以用了另一种方式实现.

通过算法描述中可以知道,字符串的编辑操作可以是插入,删除,修改.这三个方式的编辑操作,操作数都记为1.那么我的实现中,其实是移除了插入和删除的两个操作,全部是修改操作.实现如下,有什么问题的话还请指出.谢谢!

public static int Levenshtein(string str1, string str2)
{
    var maxLen = Math.Max(str1.Length, str2.Length);

    var tmp1 = str1.PadRight(maxLen);
    var tmp2 = str2.PadRight(maxLen);

    var interval = 0.0f;
    for (int i = 0; i < maxLen; i++)
    {
        if (tmp1[i] != tmp2[i])
        {
            interval += 1;
        }
    }

    return Convert.ToInt32((1 - interval / maxLen) * 100);
}

杰卡德相似系数

Jaccard index, 又称为Jaccard相似系数(Jaccard similarity coefficient)用于比较有限样本集之间的相似性与差异性。Jaccard系数值越大,样本相似度越高。

给定两个集合A,B,Jaccard 系数定义为A与B交集的大小与A与B并集的大小的比值

这个算法还是比较容易理解的,其实就是计算两个字符串中字符的交集和并集的比值.在实际使用时两个长度不相同的链接相似度过高的问题,而且在比对url这种场景下反斜线对于相似度的判断还是有一些影响的.经过简单的修改实现的代码如下

public static int Jaccard(string str1, string str2)
{
    var tmp1 = Regex.Replace(str1, "/", "");
    var tmp2 = Regex.Replace(str2, "/", "");
    var intersect = tmp1.Intersect(tmp2).Count();
    var union = tmp1.Union(tmp2).Count();
    var abs = Math.Abs(tmp2.Length - tmp1.Length);
    return Convert.ToInt32((double)intersect / (union + abs) * 100);
}

对比

分别使用两个算法计算以下两个网址的相似度

https://news.cnblogs.com/n/59...
https://news.cnblogs.com/n/59...

结果如下

算法相似度
Jaccard86%
Levenshtein94%

延展

通过相似度算法似乎也可以实现爬虫中只抓取列表页中的内容链接

image

查看原文

行列 收藏了文章 · 2019-12-13

js移动端双指缩放和旋转方法

首先感谢金大光的分享,本文在作者原有旋转缩放功能基础上支持在旋转后再叠加旋转功能。

    var initHeading = 0;
    var rotation = 0;
    var lastTime;
    function setGesture(el){
        var obj={}; 
        var istouch=false;
        var start=[];
        el.addEventListener("touchstart",function(e){
            if(e.touches.length>=2){  //判断是否有两个点在屏幕上
                istouch=true;
                start=e.touches;  //得到第一组两个点
                obj.gesturestart&&obj.gesturestart.call(el); //执行gesturestart方法
            };
        },false);
        document.addEventListener("touchmove",function(e){
            e.preventDefault();
            if(e.touches.length>=2&&istouch){
                var now=e.touches;  //得到第二组两个点
                var scale=getDistance(now[0],now[1])/getDistance(start[0],start[1]); //得到缩放比例,getDistance是勾股定理的一个方法
                var rotation=getAngle(now[0],now[1])-getAngle(start[0],start[1]);  //得到旋转角度,getAngle是得到夹角的一个方法
                e.scale=scale.toFixed(2);
                e.rotation=rotation.toFixed(2);
                obj.gesturemove&&obj.gesturemove.call(el,e);  //执行gesturemove方法
            };
        },false);
        document.addEventListener("touchend",function(e){
            if(istouch){
                istouch=false;
                obj.gestureend&&obj.gestureend.call(el);  //执行gestureend方法
            };
        },false);
        return obj;
    };
    function getDistance(p1, p2) {
        var x = p2.pageX - p1.pageX,
            y = p2.pageY - p1.pageY;
        return Math.sqrt((x * x) + (y * y));
    };
    function getAngle(p1, p2) {
        var x = p1.pageX - p2.pageX,
            y = p1.pageY- p2.pageY;
        return Math.atan2(y, x) * 180 / Math.PI;
    };
    var box=document.querySelector("#map3d");
    var boxGesture=setGesture(box);  //得到一个对象
    boxGesture.gesturestart=function(){  //双指开始
        /*box.style.backgroundColor="yellow";*/
        initHeading = map25D._coreMap.map.position.heading;
    };

    boxGesture.gesturemove=function(e){  //双指移动
        rotation = parseInt(e.rotation);
        var time = new Date().getTime();
        var realRotation = changeAngle(rotation,time);
        if(realRotation){
        //TODO 得到旋转角度后想实现的功能
map25D._coreMap.map.position.setHeading(realRotation);
            map25D._coreMap.map.renderer.update();
        }
    };
    boxGesture.gestureend=function(){  //双指结束

    };
    //通过时间判断解决叠加初始方向
    var changeAngle = function (heading,newTime) {
        if((newTime - lastTime) < 2){
            return false;
        }
        lastTime = newTime;
        return (initHeading + heading)
    }
查看原文

行列 收藏了文章 · 2019-11-28

Three.js - 走进3D的奇妙世界

摘要:本文将通过Three.js的介绍及示例带我们走进3D的奇妙世界。

文章来源:宜信技术学院 & 宜信支付结算团队技术分享第6期-支付结算部支付研发团队前端研发高级工程师-刘琳《three.js - 走进3D的奇妙世界》

分享者:宜信支付结算部支付研发团队前端研发高级工程师-刘琳

原文首发于支付结算团队公号-“野指针”

随着人们对用户体验越来越重视,Web开发已经不满足于2D效果的实现,而把目标放到了更加炫酷的3D效果上。Three.js是用于实现web端3D效果的JS库,它的出现让3D应用开发更简单,本文将通过Three.js的介绍及示例带我们走进3D的奇妙世界。

一、Three.js相关概念

1.1 Three.JS

Three.JS是基于WebGL的Javascript开源框架,简言之,就是能够实现3D效果的JS库。

1.2 WebGL

WebGL是一种Javascript的3D图形接口,把JavaScript和OpenGL ES 2.0结合在一起。

1.3 OpenGL

OpenGL是开放式图形标准,跨编程语言、跨平台,Javascript、Java 、C、C++ 、 python 等都能支持OpenG ,OpenGL的Javascript实现就是WebGL,另外很多CAD制图软件都采用这种标准。OpenGL ES 2.0是OpenGL的子集,针对手机、游戏主机等嵌入式设备而设计。

1.4 Canvas

Canvas是HTML5的画布元素,在使用Canvas时,需要用到Canvas的上下文,可以用2D上下文绘制二维的图像,也可以使用3D上下文绘制三维的图像,其中3D上下文就是指WebGL。

二、Three.js应用场景

利用Three.JS可以制作出很多酷炫的3D动画,并且Three.js还可以通过鼠标、键盘、拖拽等事件形成交互,在页面上增加一些3D动画和3D交互可以产生更好的用户体验。

通过Three.JS可以实现全景视图,这些全景视图应用在房产、家装行业能够带来更直观的视觉体验。在电商行业利用Three.JS可以实现产品的3D效果,这样用户就可以360度全方位地观察商品了,给用户带来更好的购物体验。另外,使用Three.JS还可以制作类似微信跳一跳那样的小游戏。随着技术的发展、基础网络的建设,web3D技术还能得到更广泛的应用。

三、主要组件

在Three.js中,有了场景(scene)、相机(camera)和渲染器(renderer) 这3个组建才能将物体渲染到网页中去。

1)场景

场景是一个容器,可以看做摄影的房间,在房间中可以布置背景、摆放拍摄的物品、添加灯光设备等。

2)相机

相机是用来拍摄的工具,通过控制相机的位置和方向可以获取不同角度的图像。

3)渲染器

渲染器利用场景和相机进行渲染,渲染过程好比摄影师拍摄图像,如果只渲染一次就是静态的图像,如果连续渲染就能得到动态的画面。在JS中可以使用requestAnimationFrame实现高效的连续渲染。

3.1 常用相机

1)透视相机

透视相机模拟的效果与人眼看到的景象最接近,在3D场景中也使用得最普遍,这种相机最大的特点就是近大远小,同样大小的物体离相机近的在画面上显得大,离相机远的物体在画面上显得小。透视相机的视锥体如上图左侧所示,从近端面到远端面构成的区域内的物体才能显示在图像上。

透视相机构造器

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )

  • fov — 摄像机视锥体垂直视野角度
  • aspect — 摄像机视锥体长宽比
  • near — 摄像机视锥体近端面
  • far — 摄像机视锥体远端面

2)正交相机

使用正交相机时无论物体距离相机远或者近,在最终渲染的图片中物体的大小都保持不变。正交相机的视锥体如上图右侧所示,和透视相机一样,从近端面到远端面构成的区域内的物体才能显示在图像上。

正交相机构造器

OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number )

  • left — 摄像机视锥体左侧面
  • right — 摄像机视锥体右侧面
  • top — 摄像机视锥体上侧面
  • bottom — 摄像机视锥体下侧面
  • near — 摄像机视锥体近端面
  • far — 摄像机视锥体远端面

3.2 坐标系

在场景中,可以放物品、相机、灯光,这些东西放置到什么位置就需要使用坐标系。Three.JS使用右手坐标系,这源于OpenGL默认情况下,也是右手坐标系。从初中、高中到大学的课堂上,教材中所涉及的几何基本都是右手坐标系。

上图右侧就是右手坐标系,五指并拢手指放平,指尖指向x轴的正方向,然后把四个手指垂直弯曲大拇指分开,并拢的四指指向y轴的正方向,大拇指指向的就是Z轴的正方向。

在Three.JS中提供了坐标轴工具(THREE.AxesHelper),在场景中添加坐标轴后,画面会出现3条垂直相交的直线,红色表示x轴,绿色表示y轴,蓝色表示z轴(如下图所示)。

3.3 示例代码

/* 场景 */
var scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(10)); // 添加坐标轴辅助线

/* 几何体 */
// 这是自定义的创建几何体方法,如果创建几何体后续会介绍
var kleinGeom = createKleinGeom(); 
scene.add(kleinGeom); // 场景中添加几何体

/* 相机 */
var camera = new THREE.PerspectiveCamera(45, width/height, 1, 100);
camera.position.set(5,10,25); // 设置相机的位置
camera.lookAt(new THREE.Vector3(0, 0, 0)); // 相机看向原点

/* 渲染器 */
var renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(width, height);
// 将canvas元素添加到body
document.body.appendChild(renderer.domElement);
// 进行渲染
renderer.render(scene, camera);

四、几何体

计算机内的3D世界是由点组成,两个点能够组成一条直线,三个不在一条直线上的点就能够组成一个三角形面,无数三角形面就能够组成各种形状的几何体。

以创建一个简单的立方体为例,创建简单的立方体需要添加8个顶点和12个三角形的面,创建顶点时需要指定顶点在坐标系中的位置,添加面的时候需要指定构成面的三个顶点的序号,第一个添加的顶点序号为0,第二个添加的顶点序号为1…

创建立方体的代码如下:

var geometry = new THREE.Geometry();

// 添加8个顶点
geometry.vertices.push(new THREE.Vector3(1, 1, 1));
geometry.vertices.push(new THREE.Vector3(1, 1, -1));
geometry.vertices.push(new THREE.Vector3(1, -1, 1));
geometry.vertices.push(new THREE.Vector3(1, -1, -1));
geometry.vertices.push(new THREE.Vector3(-1, 1, -1));
geometry.vertices.push(new THREE.Vector3(-1, 1, 1));
geometry.vertices.push(new THREE.Vector3(-1, -1, -1));
geometry.vertices.push(new THREE.Vector3(-1, -1, 1));

// 添加12个三角形的面
geometry.faces.push(new THREE.Face3(0, 2, 1));
geometry.faces.push(new THREE.Face3(2, 3, 1));
geometry.faces.push(new THREE.Face3(4, 6, 5));
geometry.faces.push(new THREE.Face3(6, 7, 5));
geometry.faces.push(new THREE.Face3(4, 5, 1));
geometry.faces.push(new THREE.Face3(5, 0, 1));
geometry.faces.push(new THREE.Face3(7, 6, 2));
geometry.faces.push(new THREE.Face3(6, 3, 2));
geometry.faces.push(new THREE.Face3(5, 7, 0));
geometry.faces.push(new THREE.Face3(7, 2, 0));
geometry.faces.push(new THREE.Face3(1, 3, 4));
geometry.faces.push(new THREE.Face3(3, 6, 4));

4.1 正面和反面

创建几何体的三角形面时,指定了构成面的三个顶点,如: new THREE.Face3(0, 2, 1),如果把顶点的顺序改成0,1,2会有区别吗?

通过下图可以看到,按照0,2,1添加顶点是顺时针方向的,而按0,1,2添加顶点则是逆时针方向的,通过添加顶点的方向就可以判断当前看到的面是正面还是反面,如果顶点是逆时针方向添加,当前看到的面是正面,如果顶点是顺时针方向添加,则当前面为反面。

下图所看到的面就是反面。如果不好记,可以使用右手沿顶点添加的方向握住,大拇指所在的面就是正面,很像我们上学时学的电磁感应定律。

五、材质

创建几何体时通过指定几何体的顶点和三角形的面确定了几何体的形状,另外还需要给几何体添加皮肤才能实现物体的效果,材质就像物体的皮肤,决定了物体的质感。常见的材质有如下几种:

  • 基础材质:以简单着色方式来绘制几何体的材质,不受光照影响。
  • 深度材质:按深度绘制几何体的材质。深度基于相机远近端面,离近端面越近就越白,离远端面越近就越黑。
  • 法向量材质:把法向量映射到RGB颜色的材质。
  • Lambert材质:是一种需要光源的材质,非光泽表面的材质,没有镜面高光,适用于石膏等表面粗糙的物体。
  • Phong材质:也是一种需要光源的材质,具有镜面高光的光泽表面的材质,适用于金属、漆面等反光的物体。
  • 材质捕获:使用存储了光照和反射等信息的贴图,然后利用法线方向进行采样。优点是可以用很低的消耗来实现很多特殊风格的效果;缺点是仅对于固定相机视角的情况较好。

下图是使用不同贴图实现的效果:

六、光源

前面提到的光敏材质(Lambert材质和Phong材质)需要使用光源来渲染出3D效果,在使用时需要将创建的光源添加到场景中,否则无法产生光照效果。下面介绍一下常用的光源及特点。

6.1 点光源

点光源类似蜡烛放出的光,不同的是蜡烛有底座,点光源没有底座,可以把点光源想象成悬浮在空中的火苗,点光源放出的光线来自同一点,且方向辐射向四面八方,点光源在传播过程中有衰弱,如下图所示,点光源在接近地面的位置,物体底部离点光源近,物体顶部离光源远,照到物体顶部的光就弱些,所以顶部会比底部暗些。

6.2 平行光

平行光模拟的是太阳光,光源发出的所有光线都是相互平行的,平行光没有衰减,被平行光照亮的整个区域接受到的光强是一样的。

6.3 聚光灯

类似舞台上的聚光灯效果,光源的光线从一个锥体中射出,在被照射的物体上产生聚光的效果。聚光灯在传播过程也是有衰弱的。

6.4 环境光

环境光是经过多次反射而来的光,环境光源放出的光线被认为来自任何方向,物体无论法向量如何,都将表现为同样的明暗程度。

环境光通常不会单独使用,通过使用多种光源能够实现更真实的光效,下图是将环境光与点光源混合后实现的效果,物体的背光面不像点光源那样是黑色的,而是呈现出深褐色,更自然。

七、纹理

在生活中纯色的物体还是比较少的,更多的是有凹凸不平的纹路或图案的物体,要用Three.JS实现这些物体的效果,就需要使用到纹理贴图。3D世界的纹理是由图片组成的,将纹理添加在材质上以一定的规则映射到几何体上,几何体就有了带纹理的皮肤。

7.1 普通纹理贴图

在这个示例中使用上图左侧的地球纹理,在球形几何体上进行贴图就能制作出一个地球。

代码如下:

/* 创建地球 */
function createGeom() {
    // 球体
    var geom = new THREE.SphereGeometry(1, 64, 64);
    // 纹理
    var loader = new THREE.TextureLoader();
    var texture = loader.load('./earth.jpg');
    // 材质
    var material = new THREE.MeshLambertMaterial({
        map: texture
    });
    var earth = new THREE.Mesh(geom, material);
    return earth;
}

7.2 反面贴图实现全景视图

这个例子是通过在球形几何体的反面进行纹理贴图实现的全景视图,实现原理是这样的:创建一个球体构成一个球形的空间,把相机放在球体的中心,相机就像在一个球形的房间中,在球体的里面(也就是反面)贴上图片,通过改变相机拍摄的方向,就能看到全景视图了。

材质默认是在几何体的正面进行贴图的,如果想要在反面贴图,需要在创建材质的时候设置side参数的值为THREE.BackSide,代码如下:

/* 创建反面贴图的球形 */
// 球体
var geom = new THREE.SphereGeometry(500, 64, 64);
// 纹理
var loader = new THREE.TextureLoader();
var texture = loader.load('./panorama.jpg');
// 材质
var material = new THREE.MeshBasicMaterial({
    map: texture,
    side: THREE.BackSide
});
var panorama = new THREE.Mesh(geom, material);

7.3 凹凸纹理贴图

凹凸纹理利用黑色和白色值映射到与光照相关的感知深度,不会影响对象的几何形状,只影响光照,用于光敏材质(Lambert材质和Phong材质)。

如果只用上图左上角的砖墙图片进行贴图的话,就像一张墙纸贴在上面,视觉效果很差,为了增强立体感,可以使用上图左下角的凹凸纹理,给物体增加凹凸不平的效果。

凹凸纹理贴图使用方式的代码如下:

// 纹理加载器
var loader = new THREE.TextureLoader();
// 纹理
var texture = loader.load( './stone.jpg');
// 凹凸纹理
var bumpTexture = loader.load( './stone-bump.jpg');
// 材质
var material =  new THREE.MeshPhongMaterial( {
    map: texture,
    bumpMap: bumpTexture
} );

7.4 法线纹理贴图

法线纹理也是通过影响光照实现凹凸不平视觉效果的,并不会影响物体的几何形状,用于光敏材质(Lambert材质和Phong材质)。上图左下角的法线纹理图片的RGB值会影响每个像素片段的曲面法线,从而改变物体的光照效果。

使用方式的代码如下:

// 纹理
var texture = loader.load( './metal.jpg');
// 法线纹理
var normalTexture = loader.load( './metal-normal.jpg');
var material =  new THREE.MeshPhongMaterial( {
    map: texture,
    normalMap: normalTexture
} );

7.5 环境贴图

环境贴图是将当前环境作为纹理进行贴图,能够模拟镜面的反光效果。在进行环境贴图时需要使用立方相机在当前场景中进行拍摄,从而获得当前环境的纹理。立方相机在拍摄环境纹理时,为避免反光效果的小球出现在环境纹理的画面上,需要将小球设为不可见。

环境贴图的主要代码如下:

/* 立方相机 */
var cubeCamera = new THREE.CubeCamera( 1, 10000, 128 );
/* 材质 */
var material = new THREE.MeshBasicMaterial( {
    envMap: cubeCamera.renderTarget.texture
});
/* 镜面反光的球体 */
var geom = new THREE.SphereBufferGeometry( 10, 32, 16 );
var ball = new THREE.Mesh( geom, material );
// 将立方相机添加到球体
ball.add( cubeCamera );
scene.add( ball );

// 立方相机生成环境纹理前将反光小球隐藏
ball.visible = false;
// 更新立方相机,生成环境纹理
cubeCamera.update( renderer, scene );
balls.visible = true;

// 渲染
renderer.render(scene, camera);

八、加载外部3D模型

Three.JS已经内置了很多常用的几何体,如:球体、立方体、圆柱体等等,但是在实际使用中往往需要用到一些特殊形状的几何体,这时可以使用3D建模软件制作出3D模型,导出obj、json、gltf等格式的文件,然后再加载到Three.JS渲染出效果。

上图的椅子是在3D制图软件绘制出来的,chair.mtl是导出的材质文件,chair.obj是导出的几何体文件,使用材质加载器加载材质文件,加载完成后得到材质对象,给几何体加载器设置材质,加载后得到几何体对象,然后再创建场景、光源、摄像机、渲染器等进行渲染,这样就等得到如图的效果。主要的代码如下:

// .mtl材质文件加载器
var mtlLoader = new THREE.MTLLoader();
// .obj几何体文件加载器
var objLoader = new THREE.OBJLoader();

mtlLoader.load('./chair.mtl', function (materials) {
    objLoader.setMaterials(materials)
        .load('./chair.obj', function (obj) {
            scene.add(obj);
            …
        });
});

九、说明

以上内容对Three.JS的基本使用进行了介绍,文中涉及到的示例源码已上传到github,感兴趣的同学可以下载查看,下载地址:https://github.com/liulinsp/t...。使用时如果有不清楚的地方可以查看Three.JS的官方文档:https://threejs.org/docs/inde...

查看原文

行列 收藏了文章 · 2019-11-04

使用代码列出金庸小说中使用过的所有成语

去年的今天,金庸与世长辞,当时Jerry在成都地铁一号线下班的路上得知了这个消息,回到家立即写了一篇文章来悼念:金庸的武侠世界和SAP的江湖

一年的时间转瞬即逝,大家都忙碌于各自的生活,很多人对金老的离世已经淡忘了,不过Jerry这种金庸的死忠粉,对于这个一周年忌日还是记得很清楚的。

因为Jerry手上事情很多,没时间在这个特殊的日子写文章纪念了,就发一小段代码吧。

需求:列出金庸任意一本小说里出现的所有成语。

实现:Jerry部署在Github上的一个web应用,链接如下:

http://jerrywang-sap.cn/Fiori...

首先点击超链接“成语全集”:

clipboard1,1

点击之后,存储于该web应用本地存储的一个文本文件里的全部19830个成语,以树的形式加载到内存中,并显示在网页上:

clipboard2,2

然后复制一本金庸小说的内容,粘贴到网页的“内容”区域,点击按钮“测试”:

clipboard3,3

可以看到仅仅用了246毫秒,就将这部一百多万字的《倚天屠龙记》里出现的所有成语,以红色高亮的方式高亮出来。

clipboard4,4

这个功能咋实现的?Chrome打开Jerry的网页,F12开启开发者工具,就能看到JavaScript源代码,当然也可以从我的Github上获得.

Jerry简单讲下实现原理。Web应用里有一个文本文件,里面维护了汉语里全部的成语,通过分号分隔。

clipboard5,5

运行时,这些内容会被加载到内存中,构建成一棵树,如下图所示:

clipboard6,6

其中叶节点以属性end为true区分。

成语检索的核心逻辑位于search函数里,让我们用《笑傲江湖》里一句响亮的口号“日月神教千秋万载,一统江湖”来单步调试,了解其实现逻辑。

clipboard7,7

进入165行的外层while循环,再进入173行的内层for循环,检测是否有测试字符串第一个字符“日”开头的成语。因为成语是由4个字符组成,所以需要用内层for循环逐一试探,如果遇到tblCur.end为true的元素,说明在测试字符串中发现了一个成语。

下图是内层for循环第一次执行后的tblCur内容:

clipboard8,8

内层循环执行第二次,此时tblCur指向一棵由所有“日月”开头的成语组成的树:

clipboard9,9

执行内层循环的第三次迭代,因为在树“日-月”这个分支下面没有“神”这个节点,所以结束当前的内层循环,通过break返回到外层的while循环,进行输入字符串第二个字符“月”的新一轮试探,以此类推。

clipboard10,10

最后从“千”这个字符出发,沿着内存中的树经过路径"秋-万",最后来到end属性为true的叶节点“载”,记下“千”在输入字符串中的偏移量,存到一个数组arrMatch中去。

clipboard11,11
clipboard12,12

待输入字符串全部试探完毕后,根据arrMatch中存放的偏移量,高亮显示对应的字符串,完成检索。

clipboard13,13
树这个数据结构在这个需求的实现里有着完美的表现。

金庸虽然离开了我们,但他笔下那些人物和发生的故事,将永远流传于这个世上。

更多阅读

要获取更多Jerry的原创文章,请关注公众号"汪子熙":
公众号截图

查看原文

行列 收藏了文章 · 2019-11-04

灵活运用CSS开发技巧

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

系列

前言

何为技巧,意指表现在文学、工艺、体育等方面的巧妙技能。代码作为一门现代高级工艺,推动着人类科学技术的发展,同时犹如文字一样承托着人类文化的进步。

每写好一篇文章,都会使用大量的写作技巧。烘托、渲染、悬念、铺垫、照应、伏笔、联想、想象、抑扬结合、点面结合、动静结合、叙议结合、情景交融、首尾呼应、衬托对比、白描细描、比喻象征、借古讽今、卒章显志、承上启下、开门见山、动静相衬、虚实相生、实写虚写、托物寓意、咏物抒情等,这些应该都是我们从小到大写文章而接触到的写作技巧。

作为程序猿的我们,写代码同样也需要大量的写作技巧。一份良好的代码能让人耳目一新,让人容易理解,让人舒服自然,同时也让自己成就感满满(哈哈,这个才是重点)。因此,我整理下三年来自己使用到的一些CSS开发技巧,希望能让你写出耳目一新、容易理解、舒服自然的代码。

目录

既然写文章有这么多的写作技巧,那么我也需要对CSS开发技巧整理一下,起个易记的名字。

  • Layout Skill:布局技巧
  • Behavior Skill:行为技巧
  • Color Skill:色彩技巧
  • Figure Skill:图形技巧
  • Component Skill:组件技巧

备注

  • 代码只作演示用途,不会详细说明语法
  • 部分技巧示例代码过长,使用CodePen进行保存,点击在线演示即可查看
  • 兼容项点击链接即可查看当前属性的浏览器兼容数据,自行根据项目兼容需求考虑是否使用
  • 以下代码全部基于CSS进行书写,没有任何JS代码,没有特殊说明的情况下所有属性和方法都是CSS类型
  • 一部分技巧是自己探讨出来的,另一部分技巧是参考各位前端大神们的,都是一个互相学习的过程,大家一起进步

Layout Skill

使用vw定制rem自适应布局
  • 要点:移动端使用rem布局需要通过JS设置不同屏幕宽高比的font-size,结合vw单位和calc()可脱离JS的控制
  • 场景:rem页面布局(不兼容低版本移动端系统)
  • 兼容:vwcalc())
/* 基于UI width=750px DPR=2的页面 */
html {
    font-size: calc(100vw / 7.5);
}
使用:nth-child()选择指定元素
  • 要点:通过:nth-child()筛选指定的元素设置样式
  • 场景:表格着色边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::nth-child())
  • 代码:在线演示

在线演示

使用writing-mode排版竖文
  • 要点:通过writing-mode调整文本排版方向
  • 场景:竖行文字文言文诗词
  • 兼容:writing-mode
  • 代码:在线演示

在线演示

使用text-align-last对齐两端文本
  • 要点:通过text-align-last:justify设置文本两端对齐
  • 场景:未知字数中文对齐
  • 兼容:text-align-last
  • 代码:在线演示

在线演示

使用:not()去除无用属性
  • 要点:通过:not()排除指定元素不使用设置样式
  • 场景:符号分割文字边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::not())
  • 代码:在线演示

在线演示

使用object-fit规定图像尺寸
  • 要点:通过object-fit使图像脱离background-size的约束,使用<img>来标记图像背景尺寸
  • 场景:图片尺寸自适应
  • 兼容:object-fit
  • 代码:在线演示

在线演示

使用overflow-x排版横向列表
  • 要点:通过flexboxinline-block的形式横向排列元素,对父元素设置overflow-x:auto横向滚动查看
  • 场景:横向滚动列表元素过多但位置有限的导航栏
  • 兼容:overflow-x
  • 代码:在线演示

在线演示

使用text-overflow控制文本溢出

在线演示

使用transform描绘1px边框
  • 要点:分辨率比较低的屏幕下显示1px的边框会显得模糊,通过::before::aftertransform模拟细腻的1px边框
  • 场景:容器1px边框
  • 兼容:transform
  • 代码:在线演示

在线演示

使用transform翻转内容
  • 要点:通过transform:scale3d()对内容进行翻转(水平翻转、垂直翻转、倒序翻转)
  • 场景:内容翻转
  • 兼容:transform
  • 代码:在线演示

在线演示

使用letter-spacing排版倒序文本
  • 要点:通过letter-spacing设置负值字体间距将文本倒序
  • 场景:文言文诗词
  • 兼容:letter-spacing
  • 代码:在线演示

在线演示

使用margin-left排版左重右轻列表
  • 要点:使用flexbox横向布局时,最后一个元素通过margin-left:auto实现向右对齐
  • 场景:右侧带图标的导航栏
  • 兼容:margin
  • 代码:在线演示

在线演示

Behavior Skill

使用overflow-scrolling支持弹性滚动
  • 要点:iOS页面非body元素的滚动操作会非常卡(Android不会出现此情况),通过overflow-scrolling:touch调用Safari原生滚动来支持弹性滚动,增加页面滚动的流畅度
  • 场景:iOS页面滚动
  • 兼容:iOS自带-webkit-overflow-scrolling
body {
    -webkit-overflow-scrolling: touch;
}
.elem {
    overflow: auto;
}
使用transform启动GPU硬件加速
  • 要点:有时执行动画可能会导致页面卡顿,可在特定元素中使用硬件加速来避免这个问题
  • 场景:动画元素(绝对定位、同级中超过6个以上使用动画)
  • 兼容:transform
.elem {
    transform: translate3d(0, 0, 0); /* translateZ(0)亦可 */
}
使用attr()抓取data-*
  • 要点:在标签上自定义属性data-*,通过attr()获取其内容赋值到content
  • 场景:提示框
  • 兼容:data-*attr())
  • 代码:在线演示

在线演示

使用:valid和:invalid校验表单

在线演示

使用pointer-events禁用事件触发
  • 要点:通过pointer-events:none禁用事件触发(默认事件、冒泡事件、鼠标事件、键盘事件等),相当于<button>disabled
  • 场景:限时点击按钮(发送验证码倒计时)、事件冒泡禁用(多个元素重叠且自带事件、a标签跳转)
  • 兼容:pointer-events
  • 代码:在线演示

在线演示

使用+或~美化选项框
  • 要点:<label>使用+~配合for绑定radiocheckbox的选择行为
  • 场景:选项框美化选中项增加选中样式
  • 兼容:+~
  • 代码:在线演示

在线演示

使用:focus-within分发冒泡响应

在线演示

使用:hover描绘鼠标跟随
  • 要点:将整个页面等比划分成小的单元格,每个单元格监听:hover,通过:hover触发单元格的样式变化来描绘鼠标运动轨迹
  • 场景:鼠标跟随轨迹水波纹怪圈
  • 兼容::hover
  • 代码:在线演示

在线演示

使用max-height切换自动高度
  • 要点:通过max-height定义收起的最小高度和展开的最大高度,设置两者间的过渡切换
  • 场景:隐藏式子导航栏悬浮式折叠面板
  • 兼容:max-height
  • 代码:在线演示

在线演示

使用transform模拟视差滚动

在线演示

使用animation-delay保留动画起始帧
  • 要点:通过transform-delayanimation-delay设置负值时延保留动画起始帧,让动画进入页面不用等待即可运行
  • 场景:开场动画
  • 兼容:transformanimation
  • 代码:在线演示

在线演示

使用resize拉伸分栏
  • 要点:通过resize设置横向自由拉伸来调整目标元素的宽度
  • 场景:富文本编辑器分栏阅读
  • 兼容:resize
  • 代码:在线演示

在线演示

Color Skill

使用color改变边框颜色
  • 要点:border没有定义border-color时,设置color后,border-color会被定义成color
  • 场景:边框颜色与文字颜色相同
  • 兼容:color
.elem {
    border: 1px solid;
    color: #f66;
}

在线演示

使用filter开启悼念模式
  • 要点:通过filter:grayscale()设置灰度模式来悼念某位去世的仁兄或悼念因灾难而去世的人们
  • 场景:网站悼念
  • 兼容:filter
  • 代码:在线演示

在线演示

使用::selection改变文本选择颜色
  • 要点:通过::selection根据主题颜色自定义文本选择颜色
  • 场景:主题化
  • 兼容:::selection
  • 代码:在线演示

在线演示

使用linear-gradient控制背景渐变
  • 要点:通过linear-gradient设置背景渐变色并放大背景尺寸,添加背景移动效果
  • 场景:主题化彩虹背景墙
  • 兼容:gradientanimation
  • 代码:在线演示

在线演示

使用linear-gradient控制文本渐变

在线演示

使用caret-color改变光标颜色
  • 要点:通过caret-color根据主题颜色自定义光标颜色
  • 场景:主题化
  • 兼容:caret-color
  • 代码:在线演示

在线演示

使用::scrollbar改变滚动条样式
  • 要点:通过scrollbarscrollbar-trackscrollbar-thumb等属性来自定义滚动条样式
  • 场景:主题化页面滚动
  • 兼容:::scrollbar
  • 代码:在线演示

在线演示

使用filter模拟Instagram滤镜
  • 要点:通过filter的滤镜组合起来模拟Instagram滤镜
  • 场景:图片滤镜
  • 兼容:filter
  • 代码:在线演示css-gram

在线演示

Figure Skill

使用div描绘各种图形
  • 要点:<div>配合其伪元素(::before::after)通过cliptransform等方式绘制各种图形
  • 场景:各种图形容器
  • 兼容:cliptransform
  • 代码:在线演示
使用mask雕刻镂空背景

在线演示

使用linear-gradient描绘波浪线
  • 要点:通过linear-gradient绘制波浪线
  • 场景:文字强化显示文字下划线内容分割线
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用linear-gradient描绘彩带
  • 要点:通过linear-gradient绘制间断颜色的彩带
  • 场景:主题化
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用conic-gradient描绘饼图
  • 要点:通过conic-gradient绘制多种色彩的饼图
  • 场景:项占比饼图
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用linear-gradient描绘方格背景
  • 要点:使用linear-gradient绘制间断颜色的彩带进行交互生成方格
  • 场景:格子背景占位图
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用box-shadow描绘单侧投影

在线演示

使用filter描绘头像彩色阴影
  • 要点:通过filter:blur() brightness() opacity()模拟阴影效果
  • 场景:头像阴影
  • 兼容:filter
  • 代码:在线演示

在线演示

使用box-shadow裁剪图像
  • 要点:通过box-shadow模拟蒙层实现中间镂空
  • 场景:图片裁剪新手引导背景镂空投射定位
  • 兼容:box-shadow
  • 代码:在线演示

在线演示

使用outline描绘内边框
  • 要点:通过outline设置轮廓进行描边,可设置outline-offset设置内描边
  • 场景:内描边外描边
  • 兼容:outline
  • 代码:在线演示

在线演示

Component Skill

迭代计数器

在线演示

下划线跟随导航栏
  • 要点:下划线跟随鼠标移动的导航栏
  • 场景:动态导航栏
  • 兼容:+
  • 代码:在线演示

在线演示

气泡背景墙

在线演示

滚动指示器

在线演示

故障文本

在线演示

换色器

在线演示

状态悬浮球

在线演示

粘粘球

在线演示

商城票券
  • 要点:边缘带孔和中间折痕的票劵
  • 场景:电影票代金券消费卡
  • 兼容:gradient
  • 代码:在线演示

在线演示

倒影加载条

在线演示

三维立方体

在线演示

动态边框
  • 要点:鼠标悬浮时动态渐变显示的边框
  • 场景:悬浮按钮边框动画
  • 兼容:gradient
  • 代码:在线演示

在线演示

标签页

在线演示

标签导航栏
  • 要点:可切换内容的导航栏
  • 场景:页面切换
  • 兼容:~
  • 代码:在线演示

在线演示

折叠面板
  • 要点:可折叠内容的面板
  • 场景:隐藏式子导航栏
  • 兼容:~
  • 代码:在线演示

在线演示

星级评分
  • 要点:点击星星进行评分的按钮
  • 场景:评分
  • 兼容:~
  • 代码:在线演示

在线演示

加载指示器

在线演示

自适应相册

在线演示

圆角进度条

在线演示

螺纹进度条

在线演示

立体按钮

在线演示

混沌加载圈

在线演示

蛇形边框

在线演示

自动打字
  • 要点:逐个字符自动打印出来的文字
  • 场景:代码演示文字输入动画
  • 兼容:chanimation
  • 代码:在线演示

在线演示

总结

写到最后总结得差不多了,如果后续我想起还有哪些遗漏的CSS开发技巧,会继续在这篇文章上补全。

最后送大家一个键盘!

(_=>[..."`1234567890-=~~QWERTYUIOP[]\\~ASDFGHJKL;'~~ZXCVBNM,./~"].map(x=>(o+=`/${b='_'.repeat(w=x<y?2:' 667699'[x=["Bs","Tab","Caps","Enter"][p++]||'Shift',p])}\\|`,m+=y+(x+'    ').slice(0,w)+y+y,n+=y+b+y+y,l+=' __'+b)[73]&&(k.push(l,m,n,o),l='',m=n=o=y),m=n=o=y='|',p=l=k=[])&&k.join`
`)()

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

行列 收藏了文章 · 2019-09-03

烧脑!JS+Canvas带你体验「偶消奇不消」的智商挑战

启逻辑之高妙,因想象而自由

层叠拼图Plus是一款需要空间想象力和逻辑推理能力完美结合的微信小游戏,偶消奇不消,在简单的游戏规则下却有着无数种可能性,需要你充分发挥想象力去探索,看似简单却具有极大的挑战性和趣味性,这就是其魅力所在!温馨提示,体验后再阅读此文体验更佳哦!

预览:

Talk is cheap. Show me the code

层叠拼图Plus微信小游戏采用js+canvas实现,没有使用任何游戏引擎,对于初学者来说,也比较容易入门。接下来,我将通过以下几个点循序渐进的讲解层叠拼图Plus微信小游戏的实现。

  • 如何解决Canvas绘图模糊?
  • 如何绘制任意多边形图形?
  • 1 + 1 = 0,「偶消奇不消」的效果如何实现?
  • 如何判断一个点是否在任意多边形内部 ?
  • 如何判断游戏结果是否正确?
  • 排行榜的展示
  • 游戏性能优化

如何解决Canvas绘图模糊?

canvas 绘图时,会从两个物理像素的中间位置开始绘制并向两边扩散 0.5 个物理像素。当设备像素比为 1 时,一个 1px 的线条实际上占据了两个物理像素(每个像素实际上只占一半),由于不存在 0.5 个像素,所以这两个像素本来不应该被绘制的部分也被绘制了,于是 1 物理像素的线条变成了 2 物理像素,视觉上就造成了模糊

绘图模糊的原因知道了,在微信小游戏里面又该如何解决呢?

const ratio = wx.getSystemInfoSync().pixelRatio
let ctx = canvas.getContext('2d')
canvas.width = screenWidth * ratio
canvas.height = screenHeight * ratio

ctx.fillStyle = 'black'
ctx.font = `${18 * ratio}px Arial`
ctx.fillText('我是清晰的文字', x * ratio, y * ratio)

ctx.fillStyle = 'red'
ctx.fillRect(x * ratio, y * ratio, width * ratio, height * ratio)

可以看到,我们先通过 wx.getSystemInfoSync().pixelRatio 获取设备的像素比ratio,然后将在屏 Canvas 的宽度和高度按照所获取的像素比ratio进行放大,在绘制文字、图片的时候,坐标点 xy 和所要绘制图形的 widthheight均需要按照像素比 ratio 进行缩放,这样我们就可以清晰的在高清屏中绘制想要的文字、图片。

可参考微信官方 缩放策略调整

另外,需要注意的是,这里的 canvas 是由 weapp-adapter 预先调用 wx.createCanvas() 创建一个上屏 Canvas,并暴露为一个全局变量 canvas

如何绘制任意多边形图形?

任意一个多边形图形,是由多个平面坐标点所组成的图形区域。

在游戏画布内,我们以左上角为坐标原点 {x: 0, y: 0} ,一个多边形包含多个单位长度的平面坐标点,如:[{ x: 1, y: 3 }, { x: 5, y: 3 }, { x: 3, y: 5 }] 表示为一个三角形的区域,需要注意的是,xy 并不是真实的平面坐标值,而是通过屏幕宽度计算出来的单位长度,在画布内的真实坐标值则为 {x: x * itemWidth, y: y * itemWidth}

绘制多边形代码实现如下:

/**
 * 绘制多边形
 */
export default class Block {
    constructor() { }
    init(points, itemWidth, ctx) {
        this.points = []
        this.itemWidth = itemWidth // 单位长度
        this.ctx = ctx
        for (let i = 0; i < points.length; i++) {
            let point = points[i]
            this.points.push({
                x: point.x * this.itemWidth,
                y: point.y * this.itemWidth
            })
        }
    }

    draw() {
        this.ctx.globalCompositeOperation = 'xor'
        this.ctx.fillStyle = 'black'
        this.ctx.beginPath()
        this.ctx.moveTo(this.points[0].x, this.points[0].y)
        for (let i = 1; i < this.points.length; i++) {
            let point = this.points[i]
            this.ctx.lineTo(point.x, point.y)
        }
        this.ctx.closePath()
        this.ctx.fill()
    }
}

使用:

let points = [
    [{ x: 4, y: 5 }, { x: 8, y: 9 }, { x: 4, y: 9 }],
    [{ x: 10, y: 8 }, { x: 10, y: 12 }, { x: 6, y: 12 }],
    [{ x: 7, y: 4 }, { x: 11, y: 4 }, { x: 11, y: 8 }]
]
points.map((sub_points) => {
    let block = new Block()
    block.init(sub_points, this.itemWidth, this.ctx)
    block.draw()
})

效果如下图:

CanvasRenderingContext2D其他使用方法可参考:CanvasRenderingContext2D API 列表

1 + 1 = 0,「偶消奇不消」的效果如何实现?

1 + 1 = 0,是层叠拼图Plus小游戏玩法的精髓所在。

有经验的同学,也许一眼就发现了,1 + 1 = 0 刚好符合通过 异或运算 得出的结果。当然,细心的同学也可能已经发现,在 如何绘制任意多边形图形 这一章节内,有一句特殊的代码:this.ctx.globalCompositeOperation = 'xor',也正是通过设置 CanvasContextglobalCompositeOperation 属性值为 xor 便实现了「偶消奇不消」的神奇效果。

globalCompositeOperation

globalCompositeOperation 是指 在绘制新形状时应用的合成操作的类型,其他效果可参考:globalCompositeOperation 示例

如何判断一个点是否在任意多边形内部?

当回转数为 0 时,点在闭合曲线外部。

讲到这里,我们已经知道如何在Canvas画布内绘制出偶消奇不消效果的层叠图形了,接下来我们来看下玩家如何移动选中的图形。我们发现绘制出的图形对象并没有提供点击事件绑定之类的操作,那又如何判断玩家选中了哪个图形呢?这里我们就需要去实现如何判断玩家触摸事件的xy坐标在哪个多边形图形内部区域,从而判断出玩家选中的是哪一个多边形图形。

判断一个点是否在任意多边形内部有多种方法,比如:

  • 射线法
  • 面积判别法
  • 叉乘判别法
  • 回转数法
  • ...

层叠拼图Plus小游戏内,采用的是 回转数 法来判断玩家触摸点是否在多边形内部。回转数 是拓扑学中的一个基本概念,具有很重要的性质和用途。当然,展开讨论 回转数 的概念并不在该文的讨论范围内,我们仅需了解一个概念:当回转数为 0 时,点在闭合曲线外部。

上面面这张图动态演示了回转数的概念:图中红色曲线关于点(人所在位置)的回转数为 2

对于给定的点和多边形,回转数应该怎么计算呢?

  • 用线段分别连接点和多边形的全部顶点

  • 计算所有点与相邻顶点连线的夹角

  • 计算所有夹角和。注意每个夹角都是有方向的,所以有可能是负值

最后根据角度累加值计算回转数。360°(2π)相当于一次回转。

在使用 JavaScript 实现时,需要注意以下问题:

  • JavaScript 的数只有 64 位双精度浮点这一种。对于三角函数产生的无理数,浮点数计算不可避免会造成一些误差,因此在最后计算回转数需要做取整操作。
  • 通常情况下,平面直角坐标系内一个角的取值范围是 -π 到 π 这个区间,这也是 JavaScript 三角函数 Math.atan2() 返回值的范围。但 JavaScript 并不能直接计算任意两条线的夹角,我们只能先计算两条线与 x 正轴夹角,再取两者差值。这个差值的结果就有可能超出 π 这个区间,因此我们还需要处理差值超出取值区间的情况。

代码实现:

/**
 * 判断点是否在多边形内/边上
 */
isPointInPolygon(p, poly) {
    let px = p.x,
        py = p.y,
        sum = 0

    for (let i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
        let sx = poly[i].x,
            sy = poly[i].y,
            tx = poly[j].x,
            ty = poly[j].y

        // 点与多边形顶点重合或在多边形的边上
        if ((sx - px) * (px - tx) >= 0 &&
            (sy - py) * (py - ty) >= 0 &&
            (px - sx) * (ty - sy) === (py - sy) * (tx - sx)) {
            return true
        }

        // 点与相邻顶点连线的夹角
        let angle = Math.atan2(sy - py, sx - px) - Math.atan2(ty - py, tx - px)

        // 确保夹角不超出取值范围(-π 到 π)
        if (angle >= Math.PI) {
            angle = angle - Math.PI * 2
        } else if (angle <= -Math.PI) {
            angle = angle + Math.PI * 2
        }
        sum += angle
    }

    // 计算回转数并判断点和多边形的几何关系
    return Math.round(sum / Math.PI) === 0 ? false : true
}

注:该章节内容图片均来自网络,如有侵权,请告知删除。另外有兴趣的同学可以使用其他方法来实现判断一个点是否在任意多边形内部。

如何判断游戏结果是否正确?

探索的过程固然精彩,而结果却更令我们期待

通过前面的介绍我们可以知道,判断游戏结果是否正确其实就是比对玩家组合图形的 xor 结果与目标图形的 xor 结果。那么如何求多个多边形 xor 的结果呢? polygon-clipping 正是为此而生的。它不仅支持 xor 操作,还有其他的比如:union, intersection, difference 等操作。
层叠拼图Plus游戏内通过 polygon-clipping 又是怎样实现游戏结果判断的呢?

  • 目标图形

多边形平面坐标点集合:

points = [
    [{ x: 6, y: 6 }, { x: 10, y: 6 }, { x: 10, y: 10 }, { x: 6, y: 10 }],
    [{ x: 8, y: 6 }, { x: 10, y: 8 }, { x: 8, y: 10 }, { x: 6, y: 8 }]
]
/**
 * 获取 多个多边形 xor 结果
 */
const polygonClipping = require('polygon-clipping')

polygonXor(points) {
    let poly = []
    points.forEach(function (sub_points) {
        let temp = []
        sub_points.forEach(function (point) {
            temp.push([point.x, point.y])
        })
        poly.push([temp])
    })

    let results = polygonClipping.xor(...poly)

    // 找出左上角的点
    let min_x = 100, min_y = 100
    results.forEach(function (sub_results) {
        sub_results.forEach(function (temps) {
            temps.forEach(function (point) {
                if (point[0] < min_x) min_x = point[0]
                if (point[1] < min_y) min_y = point[1]
            })
        })
    })

    // 以左上角为参考点 多边形平移至 原点 {x: 0, y: 0}
    results.forEach(function (sub_results) {
        sub_results.forEach(function (temps) {
            temps.forEach(function (point) {
                point[0] -= min_x
                point[1] -= min_y
            })
        })
    })
}
let result = this.polygonXor(points)

xor结果:

[
    [[[0, 0], [2, 0], [0, 2], [0, 0]]],
    [[[0, 2], [2, 4], [0, 4], [0, 2]]],
    [[[2, 0], [4, 0], [4, 2], [2, 0]]],
    [[[2, 4], [4, 2], [4, 4], [2, 4]]]
]

同理计算出玩家操作图形的xor结果进行比对即可得出答案正确与否。

需要注意的是,获取玩家的 xor 结果并不能直接拿来与目标图形xor 结果进行比较,我们需要将xor 的结果以左上角为参考点将图形平移至原点内,然后再进行比较,如果结果一致,则代表玩家答案正确。

排行榜的展示

有人的地方就有江湖,有江湖的地方就有排行

在看本章节内容之前,建议先浏览一遍排行榜相关的官方文档:好友排行榜关系链数据,以便对相关内容有个大概的了解。

  • 开放数据域

开放数据域是一个封闭、独立的 JavaScript 作用域。要让代码运行在开放数据域,需要在 game.json 中添加配置项 openDataContext 指定开放数据域的代码目录。添加该配置项表示小游戏启用了开放数据域,这将会导致一些限制。

// game.json
{
  "openDataContext": "src/myOpenDataContext"
}
  • 在游戏内使用 wx.setUserCloudStorage(obj) 对玩家游戏数据进行托管。
  • 在开放数据域内使用 wx.getFriendCloudStorage(obj)拉取当前用户所有同玩好友的托管数据
  • 展示关系链数据

如果想要展示通过关系链 API 获取到的用户数据,如绘制排行榜等业务场景,需要将排行榜绘制到 sharedCanvas 上,再在主域将 sharedCanvas 渲染上屏。

// src/myOpenDataContext/index.js
let sharedCanvas = wx.getSharedCanvas()

function drawRankList (data) {
  data.forEach((item, index) => {
    // ...
  })
}

wx.getFriendCloudStorage({
  success: res => {
    let data = res.data
    drawRankList(data)
  }
})

sharedCanvas 是主域和开放数据域都可以访问的一个离屏画布。在开放数据域调用 wx.getSharedCanvas() 将返回 sharedCanvas

// src/myOpenDataContext/index.js
let sharedCanvas = wx.getSharedCanvas()
let context = sharedCanvas.getContext('2d')
context.fillStyle = 'red'
context.fillRect(0, 0, 100, 100)

在主域中可以通过开放数据域实例访问 sharedCanvas,通过 drawImage() 方法可以将 sharedCanvas 绘制到上屏画布。

// game.js
let openDataContext = wx.getOpenDataContext()
let sharedCanvas = openDataContext.canvas

let canvas = wx.createCanvas()
let context = canvas.getContext('2d')
context.drawImage(sharedCanvas, 0, 0)

sharedCanvas 本质上也是一个离屏 Canvas,而重设 Canvas 的宽高会清空 Canvas 上的内容。所以要通知开放数据域去重绘 sharedCanvas

// game.js
openDataContext.postMessage({
  command: 'render'
})

// src/myOpenDataContext/index.js
openDataContext.onMessage(data => {
  if (data.command === 'render') {
    // 重绘 sharedCanvas
  }
})

需要注意的是:sharedCanvas 的宽高只能在主域设置,不能在开放数据域中设置。

游戏性能优化

性能优化,简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。

一款能让人心情愉悦的游戏,性能问题必然不能成为绊脚石。那么可以从哪些方面对游戏进行性能优化呢?

离屏 Canvas

层叠拼图Plus小游戏内,针对需要大量使用且绘图繁复的静态场景,都是使用离屏 Canvas进行绘制的,如首页网格背景、关卡列表、排名列表等。在微信内 wx.createCanvas() 首次调用创建的是显示在屏幕上的画布,之后调用创建的都是离屏画布。初始化时将静态场景绘制完备,需要时直接拷贝离屏Canvas的图像即可。Canvas 绘制本身就是不断的更新帧从而达到动画的效果,通过使用离屏 Canvas,就大大减少了一些静态内容在上屏Canvas的绘制,从而提升了绘制性能。

this.offScreenCanvas = wx.createCanvas()
this.offScreenCanvas.width = this.width * ratio
this.offScreenCanvas.height = this.height * ratio

this.ctx.drawImage(this.offScreenCanvas, x * ratio, y * ratio, this.offScreenCanvas.width, this.offScreenCanvas.height)

内存优化

玩家在游戏过程中拖动方块的移动其实就是不断更新多边形图形的坐标信息,然后不断的清空画布再重新绘制,可以想象,这个绘制是非常频繁的,按照普通的做法就需要不断去创建多个新的 Block 对象。针对游戏中需要频繁更新的对象,我们可以通过使用对象池的方法进行优化,对象池维护一个装着空闲对象的池子,如果需要对象的时候,不是直接new,而是从对象池中取出,如果对象池中没有空闲对象,则新建一个空闲对象,层叠拼图Plus小游戏内使用的是官方demo内已经实现的对象池类,实现如下:

const __ = {
  poolDic: Symbol('poolDic')
}

/**
 * 简易的对象池实现
 * 用于对象的存贮和重复使用
 * 可以有效减少对象创建开销和避免频繁的垃圾回收
 * 提高游戏性能
 */
export default class Pool {
  constructor() {
    this[__.poolDic] = {}
  }

  /**
   * 根据对象标识符
   * 获取对应的对象池
   */
  getPoolBySign(name) {
    return this[__.poolDic][name] || ( this[__.poolDic][name] = [] )
  }

  /**
   * 根据传入的对象标识符,查询对象池
   * 对象池为空创建新的类,否则从对象池中取
   */
  getItemByClass(name, className) {
    let pool = this.getPoolBySign(name)

    let result = (  pool.length
                  ? pool.shift()
                  : new className()  )

    return result
  }

  /**
   * 将对象回收到对象池
   * 方便后续继续使用
   */
  recover(name, instance) {
    this.getPoolBySign(name).push(instance)
  }
}

垃圾回收

小游戏中,JavaScript 中的每一个 CanvasImage 对象都会有一个客户端层的实际纹理储存,实际纹理储存中存放着 CanvasImage 的真实纹理,通常会占用相当一部分内存。

每个客户端实际纹理储存的回收时机依赖于 JavaScript 中的 CanvasImage 对象回收。在 JavaScriptCanvasImage 对象被回收之前,客户端对应的实际纹理储存不会被回收。通过调用 wx.triggerGC() 方法,可以加快触发 JavaScriptCore Garbage Collection(垃圾回收),从而触发 JavaScript 中没有引用的 CanvasImage 回收,释放对应的实际纹理储存。

GC 具体触发时机还要取决于 JavaScriptCore 自身机制,并不能保证调用 wx.triggerGC() 能马上触发回收,层叠拼图Plus小游戏在每局游戏开始或结束都会触发一下,及时回收内存垃圾,以保证最良好的游戏体验。

多线程 Worker

对于游戏来说,每帧 16ms 是极其宝贵的,如果有一些可以异步处理的任务,可以放置于 Worker 中运行,待运行结束后,再把结果返回到主线程。Worker 运行于一个单独的全局上下文与线程中,不能直接调用主线程的方法,Worker 也不具备渲染的能力。 Worker与主线程之间的数据传输,双方使用 Worker.postMessage() 来发送数据,Worker.onMessage() 来接收数据,传输的数据并不是直接共享,而是被复制的。

// game.json
{
  "workers": "workers"
}

// 创建worker线程
let worker = worker = wx.createWorker('workers/request/index.js') // 文件名指定 worker 的入口文件路径,绝对路径

// 主线程向 Worker 发送消息
worker.postMessage({
  msg: 'hello worker'
})

// 主线程监听 Worker 返回消息
worker.onMessage(function (res) {
  console.log(res)
})

需要注意的是:Worker 最大并发数量限制为 1 个,创建下一个前请用 Worker.terminate() 结束当前 Worker

其他 Worker 相关的内容请参考微信官方文档:多线程 Worker

结语

短短的一篇文章,定不能将层叠拼图Plus小游戏的前前后后讲明白讲透彻,加上文笔有限,有描述不当的地方还望多多海涵。其实最让人心累的还是软著的申请过程,由于各种原因前前后后花了将近三个月的时间,本来也想写一下软著申请相关的内容,最后发现篇幅有点长,无奈作罢,争取后面花点时间整理一下我这边的经验,希望可以帮助到需要的童鞋。

由于项目结构以及代码还比较混乱,个人觉得,目前暂时还不适合开源。好在,小游戏内的所有核心代码以及游戏实现思想均已呈上,有兴趣的同学如果有相关方面的疑问也可以与我多多交流,大家互相学习,共同进步。

江湖不远,我们游戏里见!

查看原文

行列 收藏了文章 · 2019-06-26

计算一点绕另一点旋转n度后的坐标(亲测)

遇到问题先网上找一找(因为自己已经忘完了……),搜到好几个如下的答案:
一、

假设o点为圆心(原点0,0),则有计算公式:

b.x = a.xcos(angle) - a.ysin(angle)

b.y = a.xsin(angle) + a.ycos(angle)

其中顺时针旋转为正,逆时针旋转为负,角度angle是弧度值,如旋转30度转换为弧度为:angle = pi/180 * 30。

二、

若o不是原点,则可先将a点坐标转换为相对坐标计算,计算结果再加上o点坐标。

参与计算的a点坐标实际应为 a - 0,由此得出最终计算公式如下:

b.x = ( a.x - o.x)cos(angle) - (a.y - o.y)sin(angle)

b.y = (a.x - o.x)sin(angle) + (a.y - o.y)cos(angle)

上面的内容是引用其他朋友的说明,结果坑了我半天,都是没做实验的吧,
直接上正解:
前面的步骤都是对的,但是!重点来了!!!!,最终还要加上中心点的坐标横纵坐标即:

b.x = ( a.x - o.x)cos(angle) - (a.y - o.y)sin(angle) + o.x

b.y = (a.x - o.x)sin(angle) + (a.y - o.y)cos(angle) + o.y

查看原文

认证与成就

  • 获得 305 次点赞
  • 获得 12 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Magix

    Magix适合用来构建大型的、交互复杂的应用。应用可以是前后端分离的单页应用,也可以是传统的交互复杂的页面。 Magix通过特有的Vframe帮你把页面按区块化拆分,同时拆分后的区块仍可以再拆分子区块,无限拆分下去。通过mx-view标签属性快速把区块组装起来形成最终的页面,区块可以被任意、多次复用。

注册于 2016-07-19
个人主页被 1.7k 人浏览