populus

populus 查看完整档案

北京编辑黑龙江科技大学  |  计算机科学与技术 编辑  |  填写所在公司/组织填写个人主网站
编辑

不断学习、努力工作、热爱技术

个人动态

populus 回答了问题 · 5月28日

解决请问一下elementui使用el-table怎么隐藏列

虽然提问很久了,还是强答一下。。。
通过事件,动态切换列类名就可以了。@header-click="handleHeaderClick" 回调函数中关于列的信息,有一个className,直接修改可以影响到当前列所有td元素。

handleHeaderClick(column, event){
    if (event.target.className.includes('el-icon-close')){
        column.className = 'none'
    }
}
// .... 另外需设置表格属性 table-layout: auto;不过这样初始列宽由浏览器根据内容自动渲染
.none {
  display: none!important;
}

关注 5 回答 6

populus 赞了文章 · 4月24日

2020年全新web前端学习路线图,学完就业20K!

第一阶段:HTML5+css

配套学习视频:

前端小白零基础入门HTML5+CSS3

第二阶段:移动web网页开发

移动web进阶教程

第三阶段:JavaScript网页编程

前端与移动开发基础入门到精通

javaScript零基础通关必备教程

第四阶段:Node.js与Ajax

Nodejs教程精讲

ajax从入门到精通

第五阶段:vue.js项目实战

4小时+5个拣选案例让你快速入门Vue.js

2018年Vue.js深入浅出教程

第六阶段:微信小程序

一天教你打造企业级微信小程序

微信小程序-个人语音接口功能

分分钟快速入门小程序开发

零基础玩转微信小程序

2小时轻松实现人脸识别的小程序

第七阶段:React.js项目实战

Reactjs入门教程

ReactJs精品教程

第八阶段:框架阶段与原理

第九阶段:移动APP开发

第十阶段:node.js进阶

第十一阶段:可视化游戏

第十二段阶段:架构与运维

还有前端免费工具下载

查看原文

赞 5 收藏 4 评论 0

populus 发布了文章 · 4月18日

用一个div及其伪类去完成各种css图形(The Shapes of CSS)

css可以渲染各种图形。我们只需要设定一个块级元素最基础的widthheight属性值,就可以实现正方形、长方形。border-radius属性可以实现圆形、椭圆等,如果再加上伪类元素及其它属性,则又有更多可能。重点是我们要学会拆分,将复杂的图形拆为简单的四边形或三角形,还要计算好图形的边边角角该如何取值。

预览地址https://nidusp.github.io/css3-demo.github.io/

:root {
    --base: aqua;
    --normal: darkorange;
    --triangle: deepskyblue;
    --strange: forestgreen;
}
.wrapper {
    min-height: 100vh;
    background-color: var(--base);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: space-around;
    flex-wrap: wrap;
}

// 简单的四边形及border-radius属性变成的圆或椭圆
.square {
    width: 5rem;
    height: 5rem;
}

.rectangle {
    width: 6rem;
    height: 3rem;
}

.circle{
    width: 5rem;
    height: 5rem;
    border-radius: 50%;
}

.oval {
    width: 6rem;
    height: 3rem;
    border-radius: 50%;
    border-radius: 3rem/ 1.5rem;
    border-radius: 3rem 3rem / 1.5rem 1.5rem;
}

// 伪类与标签垂直以绘制红十字
.cross {
    background-color: var(--normal);
    height: 5em;
    position: relative;
    width: 1em;
}
.cross::after {
    background-color: var(--normal);
    content: "";
    width: 5em;
    height: 1em;
    position: absolute;
    left: -2em;
    top: 2em;
}

.cone {
    width: 0;
    height: 0;
    border-left: 3.5em solid transparent;
    border-right: 3.5em solid transparent;
    border-top: 5em solid var(--normal);
    border-radius: 50%;
    background-color: transparent;
}


// 利用定位box-shadow属性画一个月亮
.moon {
    width: 5em;
    height: 5em;
    border-radius: 50%;
    box-shadow: 1em 1em 0 0  var(--normal);
    background-color: transparent;
}

// 利用标签画镜片,伪类画握把,组合成放大镜
.magnifying-glass {
    font-size: 10em;
    width: 0.4em;
    height: 0.4em;
    border: 0.1em solid var(--normal);
    position: relative;
    border-radius: 0.35em;
    background-color: transparent;
}
.magnifying-glass::before {
    content: "";
    position: absolute;
    right: -0.25em;
    bottom: -0.1em;
    background-color: var(--normal);
    width: 0.35em;
    height: 0.08em;
    transform: rotate(45deg);
}

.chevron {
    position: relative;
    text-align: center;
    padding: 12px;
    margin-bottom: 6px;
    height: 60px;
    width: 200px;
    background-color: transparent;
}
.chevron::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 50%;
    background-color: var(--normal);
    transform: skew(0deg, 8deg);
}
.chevron::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    height: 100%;
    width: 50%;
    background-color: var(--normal);
    transform: skew(0deg, -8deg);
}

.tv {
    position: relative;
    width: 200px;
    height: 150px;
    margin: 20px 0;
    background-color: var(--normal);
    border-radius: 50% / 10%;
    color: white;
    text-align: center;
    text-indent: .1em;
}
.tv::before {
    content: '';
    position: absolute;
    top: 10%;
    bottom: 10%;
    right: -5%;
    left: -5%;
    background-color: inherit;
    border-radius: 5% / 50%;
}



.base {
    background-color: var(--triangle);
    height: 2em;
    position: relative;
    width: 8em;
}
.base::before {
    border-bottom: 3em solid var(--triangle);
    border-left: 4em solid transparent;
    border-right: 4em solid transparent;
    content: "";
    height: 0;
    left: 0;
    position: absolute;
    top: -3em;
    width: 0;
}

.flag {
    width: 110px;
    height: 56px;
    box-sizing: content-box;
    padding-top: 15px;
    position: relative;
    background-color: var(--triangle);
}
.flag:after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 0;
    height: 0;
    border-bottom: 13px solid var(--base);
    border-left: 55px solid transparent;
    border-right: 55px solid transparent;
}

.curvedarrow {
    position: relative;
    width: 0;
    height: 0;
    border-top: .75em solid transparent;
    border-right: .75em solid deeppink;
    transform: rotate(10deg);
    background-color: transparent;
}
.curvedarrow:after {
    content: "";
    position: absolute;
    border: 0 solid transparent;
    border-top: .25em solid deeppink;
    border-radius: 1em 0 0 0;
    top: -.9em;
    left: -0.6em;
    width: 1em;
    height: 1em;
    transform: rotate(45deg);
}

.trapezoid {
    border-bottom: 6em solid var(--triangle);
    border-left: 2em solid transparent;
    border-right: 2em solid transparent;
    height: 0;
    width: 6em;
    background-color: transparent;
    position: relative;
}
.trapezoid::after {
    content: "";
    height: 6em;
    width: 6em;
    display: flex;
    align-items: center;
    justify-content: center;
}

.parallelogram {
    width: 150px;
    height: 100px;
    transform: skew(30deg);
    background-color: var(--strange);
}
.trapezoid::after {
    content: "";
    height: 6em;
    width: 6em;
    display: flex;
    align-items: center;
    justify-content: center;
}

.star-six {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 100px solid var(--strange);
    background-color: transparent;
    position: relative;
}
.star-six:after {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-top: 100px solid  var(--strange);
    position: absolute;
    content: "";
    top: 30px;
    left: -50px;
}

.star-five {
    position: relative;
    display: block;
    color: var(--strange);
    width: 0px;
    height: 0px;
    border-right: 100px solid transparent;
    border-bottom: 70px solid var(--strange);
    border-left: 100px solid transparent;
    background-color: transparent;
    transform: rotate(35deg) scale(.65);
}
.star-five::before {
    border-bottom: 80px solid var(--strange);
    border-left: 30px solid transparent;
    border-right: 30px solid transparent;
    position: absolute;
    height: 0;
    width: 0;
    top: -45px;
    left: -65px;
    display: block;
    content: '';
    transform: rotate(-35deg);
}
.star-five::after {
    position: absolute;
    display: block;
    color: var(--strange);
    top: 3px;
    left: -105px;
    width: 0px;
    height: 0px;
    border-right: 100px solid transparent;
    border-bottom: 70px solid var(--strange);
    border-left: 100px solid transparent;
    transform: rotate(-70deg);
    content: '';
}


.pentagon {
    position: relative;
    width: 54px;
    border-width: 50px 18px 0;
    border-style: solid;
    border-color: sienna transparent;
    background-color: transparent;
}
.pentagon::before {
    content: "";
    position: absolute;
    height: 0;
    width: 0;
    top: -85px;
    left: -18px;
    border-width: 0 45px 35px;
    border-style: solid;
    border-color: transparent transparent sienna;
}

.hexagon {
    width: 100px;
    height: 55px;
    color: cadetblue;
    background-color: currentColor;
    position: relative;
}
.hexagon:before {
    content: "";
    position: absolute;
    top: -25px;
    left: 0;
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 25px solid currentColor;
}
.hexagon:after {
    content: "";
    position: absolute;
    bottom: -25px;
    left: 0;
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-top: 25px solid currentColor;
}


.octagon {
    width: 100px;
    height: 100px;
    color: black;
    background-color: currentColor;
    position: relative;
}
.octagon:before {
    content: "";
    width: 100px;
    height: 0;
    position: absolute;
    top: 0;
    left: 0;
    border-bottom: 29px solid currentColor;
    border-left: 29px solid var(--base);
    border-right: 29px solid var(--base);
    box-sizing: border-box;
}
.octagon:after {
    content: "";
    width: 100px;
    height: 0;
    position: absolute;
    bottom: 0;
    left: 0;
    border-top: 29px solid currentColor;
    border-left: 29px solid var(--base);
    border-right: 29px solid var(--base);
    box-sizing: border-box;
}


.heart {
    position: relative;
    width: 5em;
    height: 4em;
    background-color: transparent;
}
.heart:before,
.heart:after {
    position: absolute;
    content: "";
    left: 2.5em;
    top: 0;
    width: 2.5em;
    height: 4em;
    background: darkred;
    border-radius: 2.5em 2.5em 0 0;
    transform: rotate(-45deg);
    transform-origin: 0 100%;
}
.heart::after {
    left: 0;
    transform: rotate(45deg);
    transform-origin: 100% 100%;
}


.egg {
    width: 6em;
    height: 8em;
    background-color: currentColor;
    border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
}

.pacman {
    width: 0;
    height: 0;
    color: black;
    border-right: 3.6em solid transparent;
    border-top: 3.6em solid currentColor;
    border-left: 3.6em solid currentColor;
    border-bottom: 3.6em solid currentColor;
    border-radius: 50%;
    background-color: var(--base);
}

.talkbubble {
    width: 8em;
    height: 6em;
    color: chartreuse;
    background-color: currentColor;
    border-radius: 10px;
    position: relative;
}
.talkbubble:before {
    content: "";
    position: absolute;
    right: 100%;
    top: 2em;
    width: 0;
    height: 0;
    border-top: 1em solid transparent;
    border-right: 2em solid currentColor;
    border-bottom: 1em solid transparent;
}

.burst-12 {
    color: var(--strange);
    background-color: currentColor;
    width: 80px;
    height: 80px;
    position: relative;
    text-align: center;
}
.burst-12:before, .burst-12:after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    height: 80px;
    width: 80px;
    background-color: currentColor;
    transform: rotate(30deg);
}
.burst-12:after {
    transform: rotate(60deg);
}

.burst-8 {
    color: var(--strange);
    background-color: currentColor;
    width: 5em;
    height: 5em;
    position: relative;
    text-align: center;
    transform: rotate(20deg);
}
.burst-8:before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    background-color: currentColor;
    transform: rotate(135deg);
    transform-origin: center center;
}

.badge-ribbon {
    position: relative;
    background-color: var(--strange);
    height: 6em;
    width: 6em;
    border-radius: 50%;
}
.badge-ribbon:before,
.badge-ribbon:after {
    content: '';
    position: absolute;
    border-bottom: 70px solid var(--strange);
    border-left: 2.5em solid transparent;
    border-right: 2.5em solid transparent;
    top: 70px;
    left: -10px;
    transform: rotate(-140deg);
}
.badge-ribbon:after {
    left: auto;
    right: -10px;
    transform: rotate(140deg);
}

利用border属性,渲染三角形

// 左右边透明,底边有颜色,则是向上的三角
.triangle-up {
    width: 0;
    height: 0;
    border-bottom: 6em solid var(--triangle);
    border-left: 3em solid transparent;
    border-right: 3em solid transparent;
    background-color: transparent;
}
// 左右边透明,顶边有颜色,则是向下的三角
.triangle-down {
    width: 0;
    height: 0;
    border-top: 6em solid var(--triangle);
    border-left: 3em solid transparent;
    border-right: 3em solid transparent;
    background-color: transparent;
}
// 上下边透明,右边有颜色,则是向左的三角
.triangle-left {
    width: 0;
    height: 0;
    border-top: 3em solid transparent;
    border-bottom: 3em solid transparent;
    border-right: 6em solid var(--triangle);
    background-color: transparent;
}
// 上下边透明,左边有颜色,则是向右的三角
.triangle-right {
    width: 0;
    height: 0;
    border-top: 3em solid transparent;
    border-bottom: 3em solid transparent;
    border-left: 6em solid var(--triangle);
    background-color: transparent;
}


// 一边透明,任一邻边有颜色,则是直角三角形
.triangle-topleft {
    width: 0;
    height: 0;
    border-top: 5em solid var(--triangle);
    border-right: 5em solid transparent;
    background-color: transparent;
}
.triangle-topright {
    width: 0;
    height: 0;
    border-top: 5em solid var(--triangle);
    border-left: 5em solid transparent;
    background-color: transparent;
}
.triangle-bottomright {
    width: 0;
    height: 0;
    border-bottom: 5em solid var(--triangle);
    border-left: 5em solid transparent;
    background-color: transparent;
}
.triangle-bottomleft {
    width: 0;
    height: 0;
    border-bottom: 5em solid var(--triangle);
    border-right: 5em solid transparent;
    background-color: transparent;
}

三角形拼接成菱形等各种形状

.pointer {
    width: 200px;
    height: 40px;
    position: relative;
    background-color: var(--strange);
}
.pointer:after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 0;
    height: 0;
    border-left: 20px solid var(--base);
    border-top: 20px solid transparent;
    border-bottom: 20px solid transparent;
}
.pointer::before {
    content: "";
    position: absolute;
    right: -20px;
    bottom: 0;
    width: 0;
    height: 0;
    border-left: 20px solid  var(--strange);
    border-top: 20px solid transparent;
    border-bottom: 20px solid transparent;
}

//
.diamond {
    color:  var(--triangle);
    width: 0;
    height: 0;
    border: 3.5em solid transparent;
    border-bottom-color: currentColor;
    position: relative;
    top: -3.5em;
    background-color: transparent;
}
.diamond:after {
    content: '';
    position: absolute;
    left: -3.5em;
    top: 3.5em;
    width: 0;
    height: 0;
    border: 3.5em solid transparent;
    border-top-color: currentColor;
}

//
.diamond-shield {
    color:  var(--triangle);
    width: 0;
    height: 0;
    border: 3em solid transparent;
    border-bottom: 1em solid;
    position: relative;
    top: -3em;
    background-color: transparent;
}
.diamond-shield::after {
    content: '';
    position: absolute;
    left: -3em;
    top: 1em;
    width: 0;
    height: 0;
    border: 3em solid transparent;
    border-top: 4.5em solid;
}

.diamond-narrow {
    width: 0;
    height: 0;
    color:  var(--triangle);
    border: 3em solid transparent;
    border-bottom: 4em solid;
    position: relative;
    top: -3em;
    background-color: transparent;
}
.diamond-narrow:after {
    content: '';
    position: absolute;
    left: -3em;
    top: 4em;
    width: 0;
    height: 0;
    border: 3em solid transparent;
    border-top: 4em solid;
}

// 梯形+三角形=钻石形状
.cut-diamond {
    margin-top: -3em;
    color: var(--triangle);
    border-style: solid;
    border-color: transparent transparent currentColor transparent;
    border-width: 0 2em 2em 2em;
    height: 0;
    width: 4em;
    position: relative;
    background-color: transparent;
}
.cut-diamond::after {
    content: "";
    position: absolute;
    top: 2em;
    left: -2em;
    width: 0;
    height: 0;
    border-style: solid;
    border-color: currentColor transparent transparent transparent;
    border-width: 6em 4em 0 4em;
}

利用伪类与标签的搭配,形成各种图案

无穷符号∞

很简单,就是伪类渲染两个对称的半圆及直角拼接在一起。调整好值即可

.infinity {
    position: relative;
    width: 11em;
    height: 6em;
    background-color: transparent;
}
.infinity:before,
.infinity:after {
    content: "";
    box-sizing: content-box;
    position: absolute;
    top: 0;
    left: 0;
    width: 3em;
    height: 3em;
    border: 1em solid #000;
    border-radius: 2.5em 2.5em 0 2.5em;
    transform: rotate(-45deg);
}
.infinity:after {
    left: auto;
    right: 0;
    border-radius: 2.5em 2.5em 2.5em 0;
    transform: rotate(45deg);
}

facebook-icon

主体是一个“f”字母,再加一个背景即可

// 利用伪类画一个“f”,错位形成“facebook-icon”
.facebook-icon {
    background-color: var(--normal);
    text-indent: -999em;
    width: 6em;
    height: 6.5em;
    border-radius: .5em;
    position: relative;
    overflow: hidden;
    border: 1em solid var(--normal);
    border-bottom: 0;
}
.facebook-icon::before {
    content: "";
    position: absolute;
    background-color: var(--normal);
    width: 2.5em;
    height: 5.6em;
    bottom: -1.8em;
    right: -2.2em;
    border: 1.25em solid #eee;
    border-radius: 1.5em;
}

.facebook-icon::after {
    content: "";
    position: absolute;
    top: 2.8em;
    width: 3.6em;
    height: 1.2em;
    background: #eee;
    right: .2em;
}

画一个锁

利用border-radius画一个半圆作为锁头,再画一个锁芯

.lock {
    font-size: .35rem;
    position: relative;
    width: 18em;
    height: 13em;
    border-radius: 2em;
    top: 10em;
    box-sizing: border-box;
    border: 3.5em solid var(--strange);
    border-right-width: 7.5em;
    border-left-width: 7.5em;
    margin: 0 0 6rem 0;
    background-color: transparent;
}
.lock::before {
    content: "";
    box-sizing: border-box;
    position: absolute;
    border: 2.5em solid var(--strange);
    width: 14em;
    height: 12em;
    left: 50%;
    margin-left: -7em;
    top: -12em;
    border-top-left-radius: 7em;
    border-top-right-radius: 7em;
}
.lock::after {
    content: "🔒";
    box-sizing: border-box;
    position: absolute;
    border: 1em solid var(--strange);
    width: 5em;
    height: 8em;
    border-radius: 2.5em;
    left: 50%;
    top: -1em;
    margin-left: -2.5em;
    display: flex;
    align-items: center;
    justify-content: center;
}

阴阳太极图

重点是利用两个伪类分别渲染两极的“小圆”及“中心”,还有标签border-colorbackground-color的属性渲染两极。


.yin-yang {
    width: 6em;
    height: 3em;
    background-color: #fff;
    color: #000;
    border: 0em solid;
    border-bottom-width: 3em;
    border-radius: 100%;
    position: relative;
}
// 伪类渲染的两点只是颜色及位置不同,所以写在一起
.yin-yang::before, .yin-yang::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 0;
    background-color: #fff;
    border: 1.1em solid currentColor;
    border-radius: 50%;
    width: .8em;
    height: .8em;
}
.yin-yang::after {
    left: 50%;
    background-color: currentColor;
    border: 1.1em solid #fff;
}

太空侵略者

利用box-shadow形成“像素点”,调整位置形成最终结果。也可渲染其它各种的像素点图。

.space-invader {
    box-shadow: 0 0 0 1em var(--strange),
    0 1em 0 1em var(--strange),
    -2.5em 1.5em 0 .5em currentColor,
    2.5em 1.5em 0 .5em currentColor,
    -3em -3em 0 0 currentColor,
    3em -3em 0 0 currentColor,
    -2em -2em 0 0 currentColor,
    2em -2em 0 0 currentColor,
    -3em -1em 0 0 currentColor,
    -2em -1em 0 0 currentColor,
    2em -1em 0 0 currentColor,
    3em -1em 0 0 currentColor,
    -4em 0 0 0 currentColor,
    -3em 0 0 0 currentColor,
    3em 0 0 0 currentColor,
    4em 0 0 0 currentColor,
    -5em 1em 0 0 currentColor,
    -4em 1em 0 0 currentColor,
    4em 1em 0 0 currentColor,
    5em 1em 0 0 currentColor,
    -5em 2em 0 0 currentColor,
    5em 2em 0 0 currentColor,
    -5em 3em 0 0 currentColor,
    -3em 3em 0 0 currentColor,
    3em 3em 0 0 currentColor,
    5em 3em 0 0 currentColor,
    -2em 4em 0 0 currentColor,
    -1em 4em 0 0 currentColor,
    1em 4em 0 0 currentColor,
    2em 4em 0 0 currentColor;
    background: currentColor;
    margin: 3em 6em;
    width: 1em;
    height: 1em;
    color: var(--strange);
    overflow: hidden;
}

// RSS,一个伪类渲染圆点,另一伪类渲染两条弧线

.rss {
    width: 20em;
    height: 20em;
    border-radius: 3em;
    color: deepskyblue;
    background-color: currentColor;
    font-size: .25em;
    position: relative;
}
.rss:before {
    content: '';
    z-index: 1;
    display: block;
    height: 5em;
    width: 5em;
    background: #fff;
    border-radius: 50%;
    position: absolute;
    top: 11.5em;
    left: 3.5em;
}
//  box-shadow 三个inset值错位渲染弧线
.rss:after {
    content: '';
    display: block;
    background: currentColor;
    width: 13em;
    height: 13em;
    top: 3em;
    left: 3.8em;
    border-radius: 2.5em;
    position: absolute;
    box-shadow:
            -2em 2em 0 0 #fff inset,
            -4em 4em 0 0 currentColor inset,
            -6em 6em 0 0 #fff inset
}

源自the-shapes-of-css

查看原文

赞 0 收藏 0 评论 0

populus 发布了文章 · 4月13日

学习、收集button各种[过渡/动画]效果

1.利用活动状态 .active 实现点击效果
:active 活动时向右下移动,使用transform属性,顺便添加transition 属性,优化一下过渡效果。(可以使用控制台,通过调试获得合适的贝塞尔曲线)
https://codepen.io/nidusp/pen/yLYNara

<label class="button"><button></button>按钮</label>

// 使用label,帮助区分hover、active、focus三种状态的区别
.button {
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}
button {
    margin: 0 .2em;
    width: 5em;
    height: 2em;
    border-radius: 1em;
    background-color: darkorange;
    box-shadow: 0 0 1em 0 darkorange, inset 0 0 .5em 0 rgba(0, 0, 0, 0.5);
    transition: transform .25s cubic-bezier(0, 0.88, 0.24, 0.47);
    cursor: pointer;
}
button:hover {
    background-color: aqua;
}
button:active {
    transform: translate3d(.5em, .5em, 0);
}
button:focus {
    box-shadow: 0 0 1em 0 aqua, inset 0 0 .5em 0 rgba(0, 0, 0, 0.5);
}

ps:不定时更新。。。

查看原文

赞 0 收藏 0 评论 0

populus 收藏了文章 · 4月9日

前端每日实战:37# 视频演示如何把握好 transition 和 animation 的时序,创作描边按钮特效

图片描述

效果预览

按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。

https://codepen.io/comehope/pen/mKdzZM

可交互视频教程

此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。

请用 chrome, safari, edge 打开观看。

https://scrimba.com/p/pEgDAM/cgnk6Sb

源代码下载

每日前端实战系列的全部源代码请从 github 下载:

https://github.com/comehope/front-end-daily-challenges

代码解读

定义 dom,标准的导航模式:

<nav>
    <ul>
        <li>Home</li>
    </ul>
</nav>

居中显示:

body {
    margin: 0;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: black;
}

定义文本和按钮边框样式:

nav ul {
    padding: 0;
}

nav ul li {
    color: white;
    list-style-type: none;
    font-size: 32px;
    font-family: sans-serif;
    text-transform: uppercase;
    width: 12em;
    height: 4em;
    border: 1px solid rgba(255, 255, 255, 0.2);
    text-align: center;
    line-height: 4em;
    letter-spacing: 0.2em;
}

用 before 伪元素定义上边框和右边框,其中边框颜色因会多次被用到,所以采用变量:

:root {
    --color: dodgerblue;
}

nav ul li::before {
    content: '';
    position: absolute;;
    width: 0;
    height: 0;
    visibility: hidden;
    top: 0;
    left: 0;
    border-top: 1px solid var(--color);
    border-right: 1px solid var(--color);
}

类似地,用 after 伪元素定义右边框和下边框:

nav ul li::after {
    content: '';
    position: absolute;;
    width: 0;
    height: 0;
    visibility: hidden;
    bottom: 0;
    right: 0;
    border-bottom: 1px solid var(--color);
    border-left: 1px solid var(--color);
}

设计边框入场的动画效果,按上、右、下、左的顺序依次显示边框,为了方便调整动画的速度设置了与时间相关的变量:

:root {
    --time-slot-length: 0.1s;
    --t1x: var(--time-slot-length);
    --t2x: calc(var(--time-slot-length) * 2);
    --t3x: calc(var(--time-slot-length) * 3);
    --t4x: calc(var(--time-slot-length) * 4);
}

nav ul li:hover::before,
nav ul li:hover::after {
    width: 100%;
    height: 100%;
    visibility: visible;
}

nav ul li:hover::before {
    transition:
        visibility 0s,
        width linear var(--t1x),
        height linear var(--t1x) var(--t1x);
}

nav ul li:hover::after {
    transition: 
        visibility 0s var(--t2x),
        width linear var(--t1x) var(--t2x),
        height linear var(--t1x) var(--t3x);
}

设计边框出场的动画效果,与入场的顺序相反:

nav ul li::before {
    transition:
        height linear var(--t1x) var(--t2x),
        width linear var(--t1x) var(--t3x),
        visibility 0s var(--t4x);
}

nav ul li::after {
    transition:
        height linear var(--t1x),
        width linear var(--t1x) var(--t1x),
        visibility 0s var(--t2x);
}

让按钮文字在描边期间变色:

nav ul li {
    transition: var(--t4x);
}

nav ul li:hover {
    color: var(--color);
}

最后,在描边结束后,在按钮四周增加一个脉冲动画,加强动感:

nav ul li:hover {
    animation: pulse ease-out 1s var(--t4x);
}

@keyframes pulse {
    from {
        box-shadow: 0 0 rgba(30, 144, 255, 0.4);
    }

    to {
        box-shadow: 0 0 0 1em rgba(30, 144, 255, 0);
    }
}

大功告成!

查看原文

populus 发布了文章 · 4月8日

composition events于v-model中的使用

前言:我们都知道vue中v-model指令可以实现双向数据绑定,但他本质是一个语法糖,比如组件上的v-model默认会利用名为value的 prop 和名为input的事件。在自定义实现的过程中,发现在使用输入法时,输入拼音选择候选词同样会触发原生input标签input事件,从而更新value。但element-ui的input组件便不会如此,其实就是用到了composition events

compositionend事件 当文本段落的组成完成或取消时, compositionend 事件将被触发 (具有特殊字符的触发, 需要一系列键和其他输入, 如语音识别或移动中的字词建议)。

compositionstart事件 触发于一段文字的输入之前(类似于keydown事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)。

compositionupdate 事件触发于字符被输入到一段文字的时候(这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)
内容来自MDN

<input
:tabindex="tabindex"
v-if="type !== 'textarea'"
class="el-input__inner"
v-bind="$attrs"
:type="showPassword ? (passwordVisible ? 'text': 'password') : type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
ref="input"

@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"

@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
\>


// 方法
handleCompositionStart() {
    this.isComposing = true;
},
handleCompositionUpdate(event) {
    const text = event.target.value;
    const lastCharacter = text[text.length - 1] || '';
    // 此处通过正则对最后一个字符是否韩文作一个判断,有懂韩文的请指教一下~
    this.isComposing = !isKorean(lastCharacter);
},

handleCompositionEnd(event) {
    if (this.isComposing) {
        this.isComposing = false;
        this.handleInput(event);
    }
},
handleInput(event) {
    // should not emit input during composition
    if (this.isComposing) return;
    // hack for https://github.com/ElemeFE/element/issues/8548
    // should remove the following line when we don't support IE
    if (event.target.value === this.nativeInputValue) return;
    this.$emit('input', event.target.value);
    // ensure native input value is controlled
    // see: https://github.com/ElemeFE/element/issues/12850
    this.$nextTick(this.setNativeInputValue);
},
handleChange(event) {
    this.$emit('change', event.target.value);
},

可以看到,原生的input绑定了许多事件,其中input事件中,先判断isComposing的布尔值,看是否触发了composition events的一系列方法,然后才决定是否执行下段代码this.$emit('input', event.target.value)

@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"`

总结:此事件本身较为简单,下分只有三个状态的事件,但是在双向数据绑定的实现中,又是比较重要的一环,避免在输入法选词时连续触发。看源码一时爽;一直看源码一直爽

查看原文

赞 0 收藏 0 评论 0

populus 回答了问题 · 3月22日

wavesurfer 在vue中加载网络音频资源跨域怎么解决呢?

audioscript标签一样,不受同源策略限制。https://www.cnblogs.com/liuarui/p/11451560.html

关注 2 回答 1

populus 关注了用户 · 3月13日

云音乐大前端团队 @musicfe

网易云音乐前端技术团队

关注 428

populus 收藏了文章 · 3月13日

用 Web 实现一个简易的音频编辑器

banner

前言

市面上,音频编辑软件非常多,比如 cubase、sonar 等等。虽然它们功能强大,但是在 Web 上的应用却显得心有余而力不足。因为 Web 应用的大多数资源都是存放在网络服务器中的,用 cubase 这些软件,首先要把音频文件下载下来,修改完之后再上传到服务器,最后还要作更新操作,操作效率极其低下。如果能让音频直接在 Web 端进行编辑并更新到服务器,则可以大大提高运营人员的工作效率。下面就为大家介绍一下如何运用 Web 技术实现高性能的音频编辑器。

本篇文章总共分为 3 章:

  • 第 1 章:声音相关的理论知识
  • 第 2 章:音频编辑器的实现方法
  • 第 3 章:音频编辑器的性能优化

第 1 章 - 声音相关的理论知识

理论是实践的依据和根基,了解理论可以更好的帮助我们实践,解决实践中遇到的问题。

1.1 什么是声音

物体振动时激励着它周围的空气质点振动,由于空气具有可压缩性,在质点的相互作用下,振动物体四周的空气就交替地产生压缩与膨胀,并且逐渐向外传播,从而形成声波。声波通过介质(空气、固体、液体)传入到人耳中,带动听小骨振动,经过一系列的神经信号传递后,被人所感知,形成声音。我们之所以能听到钢琴、二胡、大喇叭等乐器发出的声音,就是因为乐器里的某些部件通过振动产生声波,经过空气传播到我们人耳中。

1.2 声音的因素

为什么人们的声音都不一样,为什么有些人的声音很好听,有些人的声音却很猥琐呢?这节介绍一下声音的 3 大因素:频率、振幅和音色,了解这些因素之后大家就知道原因了。

1.2.1 频率

声音既然是声波,就会有振幅和频率。频率越大音高越高,声音就会越尖锐,比如女士的声音频率就普遍比男士的大,所以她们的声音会比较尖锐。人的耳朵通常只能听到 20Hz 到 20kHz 频率范围内的声波。

1.2.2 振幅

声波在空气中传播时,途经的空气会交替压缩和膨胀,从而引起大气压强变化。振幅越大,大气压强变化越大,人耳听到的声波就会越响。人耳可听的声压(声压:声波引起的大气压强变化值)范围为 (2 * 10 ^ - 5)Pa~20Pa,对应的分贝数为 0~120dB。它们之间的换算公式为 20 * log( X / (2 * 10 ^ -5) ),其中 X 为声压。相比较用大气压强来表示声音振幅强度,用分贝表示会更加直观。我们平时在形容物体的声音强度时,一般也都会用分贝,而不会说这个大喇叭发出了多少多少帕斯卡的声压(但听起来好像很厉害得样子)。

1.2.3 音色

频率和振幅都不是决定一个人声音是猥琐还是动听的主要因素,决定声音是否好听的主要因素为音色,音色是由声波中的谐波决定的。自然界中物体振动产生的声波,都不是单一频率单一振幅的波(如正弦波),而是可以分解为 1 个基波加上无数个谐波。基波和谐波都是正弦波,其中谐波的频率是基波的整数倍,振幅比基波小,相位也各不相同。如钢琴中央 dou,它的基波频率为 261,其他无数个谐波频率为 261 的整数倍。声音好听的人,在发声时,声带产生的谐波比较“好听”,而声音猥琐的人,声带产生的谐波比较“猥琐”。

1.3 声音的录制、编辑、回放

不管是欧美的钢琴、小提琴,还是中国的唢呐、二胡、大喇叭,我们不可能想听的时候都叫演奏家们去为我们现场演奏,如果能将这些好听声音存储起来,我们就可以在想听的时候进行回放了。传统的声音录制方法是通过话筒等设备把声音的振动转化成模拟的电流,经过放大和处理,然后记录到磁带或传至音箱等设备发声。这种方法失真较大, 且消除噪音困难, 也不易被编辑和修改,数字化技术可以帮我们解决模拟电流带来的问题。这节我们就来介绍下数字化技术是如何做到的。

1.3.1 录制

声音是一段连续无规则的声波,由无数个正弦波组成。数字化录制过程就是采集这段声波中离散的点的幅值,量化和编码后存储在计算机中。整个过程的基本原理为:声音经过麦克风后根据振幅的不同形成一段连续的电压变化信号,这时用脉冲信号采集到离散的电压变化,最后将这些采集到的结果进行量化和编码后存储到计算机中。采样脉冲频率一般为 44.1kHz,这是因为人耳一般只能听到声波中 20-20kHz 频率正弦波部分,根据采样定律,要从采样值序列完全恢复原始的波形,采样频率必须大于或等于原始信号最高频率的 2 倍。因此,如果要保留原始声波中 20kHz 以内的所有正弦波,采样频率一定要大于等于 40kHz。

1.3.2 编辑

声音数字化后就可以非常方便的对声音进行编辑,如展示声音波形图,截取音频,添加静音效果、渐入淡出效果,通过离散型傅里叶变换查看声音频谱图(各个谐波的分布图)或者进行滤波操作(滤除不想要的谐波部分),这些看似复杂的操作却只需要对量化后的数据简单进行的计算即可实现。

1.3.3 回放

回放过程就是录制过程的逆过程,将录制或者编辑过的音频数据进行解码,去量化还原成离散的电压信号送入大喇叭中。大喇叭如何将电压信号还原成具体的声波振幅,这个没有深入学习,只能到这了。

第2章-音频编辑器的实现方法

通过第 1 章的理论知识,我们知道了什么是声音以及声音的录制和回放,其中录制保存下来的声音数据就叫音频,通过编辑音频数据就能得到我们想要的回放声音效果。这章我们就开始介绍如何用浏览器实现音频编辑工具。浏览器提供了 AudioContext 对象用于处理音频数据,本章首先会介绍下 AudioContext 的基本使用方法,然后介绍如何用 svg 绘制音频波形以及如何对音频数据进行编辑。

2.1 AudioContext 介绍

AudioContext 对音频数据处理过程是一个流式处理过程,从音频数据获取、数据加工、音频数据播放,一步一步流式进行。AudioContext 对象则提供流式加工所需要的方法和属性,如 context.createBufferSource 方法返回一个音频数据缓存节点用于存储音频数据,这是整个流式的起点;context.destination 属性为整个流式的终点,用于播放音频。每个方法都会返回一个 AudioNode 节点对象,通过 AudioNode.connect 方法将所有 AudioNode 节点连接起来。

下面通过一个简单的例子来解锁 AudioContext:

  • 为了方便起见,我们不使用服务器上的音频文件,而使用 FileReader 读取本地音频文件
  • 使用 AudioContext 的 decodeAudioData 方法对读到的音频数据进行解码
  • 使用 AudioContext 的 createBufferSource 方法创建音频源节点,并将解码结果赋值给它
  • 使用 AudioContext 的 connect 方法连接音频源节点到播放终端节点 - AudioContext 的 destination 属性
  • 使用 AudioContext 的 start 方法开始播放
    // 读取音频文件.mp3 .flac .wav等等
    const reader = new FileReader();
    // file 为读取到的文件,可以通过<input type="file" />实现
    reader.readAsArrayBuffer(file);
    reader.onload = evt => {
        // 编码过的音频数据
        const encodedBuffer = evt.currentTarget.result;
        // 下面开始处理读取到的音频数据
        // 创建环境对象
        const context = new AudioContext();
        // 解码
        context.decodeAudioData(encodedBuffer, decodedBuffer => {
            // 创建数据缓存节点
            const dataSource = context.createBufferSource();
            // 加载缓存
            dataSource.buffer = decodedBuffer;
            // 连接播放器节点destination,中间可以连接其他节点,比如音量调节节点createGain(),
            // 频率分析节点(用于傅里叶变换)createAnalyser()等等
            dataSource.connect(context.destination);
            // 开始播放
            dataSource.start();
        })
    }

2.1 什么是音频波形

音频编辑器通过音频波形图形化音频数据,使用者只要编辑音频波形就能得到对应的音频数据,当然内部实现是将对波形的操作转为对音频数据的操作。所谓音频波形,就是时域上,音频(声波)振幅随着时间的变化情况,即 X 轴为时间,Y 轴为振幅。

2.2 绘制波形

我们知道,音频的采样频率为 44.1kHz,所以一段 10 分钟的音频总共会有 10 60 44100 = 26460000,超过 2500 万个数据点。
我们在绘制波形时,即使仅用 1 个像素代表 1 个点的振幅,波形的宽度也将近 2500 万像素,不仅绘制速度慢,而且非常不利于波形分析。
因此,下面介绍一种近似算法来减少绘制的像素点:我们首先将每秒钟采集的 44100 个点平均分成 100 份,相当于 10 毫秒一份,每一份有 441 个点,
算出它们的最大值和最小值。用最大值代表波峰,用最小值代表波谷,然后用线连接所有的波峰和波谷。音频数据在被量化后,值的范围为 [-1,1],
所以我们这里取到的波峰波谷都是在 [-1,1] 的区间内的。
由于数值太小,画出来的波形不美观,我们统一将这些值乘以一个系数比如 64,这样就能很清晰得观察到波形的变化了。
绘制波形可以用 canvas,也可以用 svg,这里我选择使用 svg 进行绘制,因为 svg 是矢量图,可以简化波形缩放算法。

代码实现

  • 为了方便使用 svg 进行绘制,引入 svg.js,并初始化 svg 对象 draw
  • 我们的绘制算法是将每秒钟采集的 44100 个点平均分成 100 份,每份是10毫秒共441个数据点,用它们的最大值和最小值作为这个时间点的波峰和波谷。

然后使用svg.js将所有的波峰波谷通过折线 polyline 连接起来形成最后的波形图。由于音频数据点经过量化处理,范围为[-1,1],为了让波形更加美观,我们
会把波峰、波谷统一乘上一个增幅系数来加大 polyline 线条的幅度

  • 初始化变量 perSecPx(每秒钟绘制像素点的个数)为100,height 波峰波谷的增幅系数为128
  • 以10毫秒为单位获取所有的波峰波谷数据点 peaks,计算方法就是简单得计算出它们各自的最大值和最小值
  • 初始化波形图的宽度 svgWidth = 音频时长(buff.duration) * 每秒钟绘制像素点的个数(perSecPx)
  • 遍历 peaks,将所有的波峰波谷乘上系数并通过 polyline(折线)连接起来
const SVG = require('svg.js');
// 创建svg对象
const draw = SVG(document.getElementById('draw'));
// 波形svg对象
let polyline;
// 波形宽度
let svgWidth;
// 展示波形函数
// buffer - 解码后的音频数据
function displayBuffer(buff) {
    // 每秒绘制100个点,就是将每秒44100个点分成100份,
    // 每一份算出最大值和最小值来代表每10毫秒内的波峰和波谷
    const perSecPx = 100;
    // 波峰波谷增幅系数
    const height = 128;
    const halfHight = height / 2;
    const absmaxHalf = 1 / halfHight;
    // 获取所有波峰波谷
    const peaks = getPeaks(buff, perSecPx);
    // 设置svg的宽度
    svgWidth = buff.duration * perSecPx;
    draw.size(svgWidth);
    const points = [];
    for (let i = 0; i < peaks.length; i += 2) {
        const peak1 = peaks[i] || 0;
        const peak2 = peaks[i + 1] || 0;
        // 波峰波谷乘上系数
        const h1 = Math.round(peak1 / absmaxHalf);
        const h2 = Math.round(peak2 / absmaxHalf);
        points.push([i, halfHight - h1]);
        points.push([i, halfHight - h2]);
    }
    // 连接所有的波峰波谷
    const  polyline = draw.polyline(points);
    polyline.fill('none').stroke({ width: 1 });
}
// 获取波峰波谷
function getPeaks(buffer, perSecPx) {
    const { numberOfChannels, sampleRate, length} = buffer;
    // 每一份的点数=44100 / 100 = 441
    const sampleSize = ~~(sampleRate / perSecPx);
    const first = 0;
    const last = ~~(length / sampleSize)
    const peaks = [];
    // 为方便起见只取左声道
    const chan = buffer.getChannelData(0);
    for (let i = first; i <= last; i++) {
        const start = i * sampleSize;
        const end = start + sampleSize;
        let min = 0;
        let max = 0;
        for (let j = start; j < end; j ++) {
            const value = chan[j];
            if (value > max) {
                max = value;
            }
            if (value < min) {
                min = value;
            }
        }
    }
    // 波峰
    peaks[2 * i] = max;
    // 波谷
    peaks[2 * i + 1] = min;
    return peaks;
}

2.3 缩放操作

有时候,需要对某些区域进行放大或者对整体波形进行缩小操作。由于音频波形是通过 svg 绘制的,缩放算法就会变得非常简单,只需直接对 svg 进行缩放即可。

代码实现

  • 利用svg矢量图特性,我们只要将连接波分波谷的折线宽度乘上系数 scaleX 即可实现缩放功能,scaleX 大于1则放大,scaleX 小于1则缩小。

其实这是一种伪缩放,因为波形的精度始终是10毫秒,只是将折线图拉开了。

function zoom(scaleX) {
    draw.width(svgWidth * scaleX);
    polyline.width(svgWidth * scaleX);
}

2.4 裁剪操作

这节主要介绍下裁剪操作的实现,其他的操作也都是类似的对音频数据作计算。
所谓裁剪,就是从原始音频中去除不要的部分,如噪音部分,或者截取想要的部分,如副歌部分。要实现对音频文件进行裁剪,
首先我们需要对它 有足够的认识。
解码后的音频数据其实是一个 AudioBuffer对象 ,
它会被赋值给 AudioBufferSourceNode 音频源节点的 buffer 属性,并由 AudioBufferSourceNode
将其带进 AudioContext 的处理流里,其中 AudioBufferSourceNode 节点可以通过 AudioContext 的 createBufferSource 方法生成。
看到这里有点懵的同学可以回到 2.1 一节再回顾一下 AudioContext 的基本用法。
AudioBuffer 对象有 sampleRate(采样速率,一般为44.1kHz)、numberOfChannels(声道数)、
duration(时长)、length(数据长度)4 个属性,还有 1 个比较重要的方法 getChannelData ,返回 1 个 Float32Array 类型的数组。我们就是通过改变这个 Float32Array 里的数据来对
音频进行裁剪或者其他操作。裁剪的具体步骤:

  • 首先获取到待处理音频的通道数和采样率
  • 根据裁剪的开始时间点、结束时间点、采样率算出被裁剪的长度:长度 lengthInSamples = (endTime - startTime) * sampleRate,然后通过 AudioContext 的 createBuffer

方法创建一个长度为 lengthInSamples 的 AudioBuffer cutAudioBuffer 用于存放裁剪下来的音频数据,再创建一个长度为原始音频长度减去 lengthInSamples 的 AudioBuffer newAudioBuffer 用于存放裁剪后的音频数据

  • 由于音频往往是多声道的,裁剪操作需要对所有声道都作裁剪,所以我们遍历所有声道,通过 AudioBuffer 的 getChannelData 方法返回各个声道 Float32Array 类型的音频数据
  • 通过 Float32Array 的 subarray 方法获取需要被裁剪的音频数据,并通过 set 方法将数据设置到 cutAudioBuffer,同时将被裁剪之后的音频数据 set 到 newAudioBuffer中
  • 返回 newAudioBuffer 和 cutAudioBuffer
function cut(originalAudioBuffer, start, end) {
    const { numberOfChannels, sampleRate } = originalAudioBuffer;
    const lengthInSamples = (end - start) * sampleRate;
    // offlineAudioContext相对AudioContext更加节省资源
    const offlineAudioContext = new OfflineAudioContext(numberOfChannels, numberOfChannels, sampleRate);
    // 存放截取的数据
    const cutAudioBuffer = offlineAudioContext.createBuffer(
        numberOfChannels,
        lengthInSamples,
        sampleRate
    );
    // 存放截取后的数据
    const newAudioBuffer = offlineAudioContext.createBuffer(
        numberOfChannels,
        originalAudioBuffer.length - cutSegment.length,
        originalAudioBuffer.sampleRate
    );
    // 将截取数据和截取后的数据放入对应的缓存中
    for (let channel = 0; channel < numberOfChannels; channel++) {
        const newChannelData = newAudioBuffer.getChannelData(channel);
        const cutChannelData = cutAudioBuffer.getChannelData(channel);
        const originalChannelData = originalAudioBuffer.getChannelData(channel);
        const beforeData = originalChannelData.subarray(0,
            start * sampleRate - 1);
        const midData = originalChannelData.subarray(start * sampleRate,
            end * sampleRate - 1);
        const afterData = originalChannelData.subarray(
            end * sampleRate
        );
        cutChannelData.set(midData);
        if (start > 0) {
            newChannelData.set(beforeData);
            newChannelData.set(afterData, (start * sampleRate));
        } else {
            newChannelData.set(afterData);
        }
    }
    return {
        // 截取后的数据
        newAudioBuffer,
        // 截取部分的数据
        cutSelection: cutAudioBuffer
    };
};

2.5 撤销和重做操作

每一次操作前,把当前的音频数据保存起来。撤销或者重做时,再把对应的音频数据加载进来。这种方式有不小的性能开销,在第 3 章 - 性能优化章节中作具体分析。

第 3 章-音频编辑器的性能优化

3.1 存在的问题

通过第 2 章介绍的近似法用比较少的点来绘制音频波形,已基本满足波形查看功能。但是仍存在以下 2 个性能问题:

  1. 如果对波形进行缩放分析,比如将波形拉大 10 倍或者更大的时候,即使 svg 绘制的波形可以自适应不失真放大,但由于整个波形放大了 10 倍以上,需要绘制的像素点也增加了 10 倍,导致整个缩放过程非常得卡顿。
  2. 撤销和重做功能此每次操作都需要保存修改后音频数据。一份音频数据,一般都在几 M 到十几 M 不等,每次操作都保存的话,势必会撑爆内存。

3.2 性能优化方案

3.2.1 懒加载

缩放波形卡顿的主要原因就是所需要绘制的像素点太多,因此我们可以通过懒加载的形式减少每次绘制波形时所需要绘制的像素点。
具体方案就是,根据当前波形的滚动位置,实时计算出当前视口需要绘制波形范围。
因此,需要对第 2 章获取波峰波谷的函数 getPeaks 进行一下改造, 增加 2 个参数:

  • buffer:解码后的音频数据 AudioBuffer
  • pxPerSec:每秒钟音频数据横向需要的像素点,这里为 100,每 10 毫秒数据对应 1 组波峰波谷
  • start:当前波形视口滚动起始位置 scrollLeft
  • end:当前波形视口滚动结束位置 scrollLeft + viewWidth。
  • 具体计算时,我们只会取当前视口内对应时间段的音频的波峰和波谷。
  • 比如 start 等于 10,end 等于 100,根据我们 1 个像素对应 1 个 10 毫秒数据量波峰波谷的近似算法,就是取第 10 个 10 毫秒到第 100 个 10 毫秒的波峰波谷,即时间段为 100 毫秒到 1 秒。
function getPeaks(buffer, pxPerSec, start, end) {
    const { numberOfChannels, sampleRate } = buffer;
    const sampleWidth = ~~(sampleRate / pxPerSec);
    const step = 1;
    const peaks = [];
    for (let c = 0; c < numberOfChannels; c++) {
        const chanData = buffer.getChannelData(c);
        for (let i = start, z = 0; i < end; i += step) {
            let max = 0;
            let min = 0;
            for (let j = i * sampleWidth; j < (i + 1) * sampleWidth; j++) {
                const value = chanData[j];
                max = Math.max(value, max);
                min = Math.min(value, min);
            }
            peaks[z * 2] = Math.max(max, peaks[z * 2] || 0);
            peaks[z * 2 + 1] = Math.min(min, peaks[z * 2 + 1] || 0);
            z++;
        }
    }
    return peaks;
}

3.2.2 撤销操作的优化

其实我们只需要保存一份原始未加工过的音频数据,然后在每次编辑前,把当前执行过的指令集全部保存下来,在撤销或者重做时,再把对应的指令集对原始音频数据操作一遍。比如:对波形进行 2 次操作:第 1 次操作时裁剪掉 0-1 秒的部分,保存指令集 A 为裁剪 0-1 秒;第二次操作时,再一次裁剪 2-3 秒的部分,保存指令集 B 为裁剪 0-1 秒、裁剪 2-3 秒。撤销第 2 次操作,只要用前一次指令集 A 对原始波形作一次操作即可。通过这种保存指令集的方式,极大降低了内存的消耗。

总结

声音实质就是声波在人耳中振动被人脑感知,决定音质的因素包括振幅、频率和音色(谐波),人耳只能识别 20-20kHz 频率和 0-120db 振幅的声音。
音频数字化处理过程为:脉冲抽样,量化,编码,解码,加工,回放。
用 canvas 或者 svg 绘制声音波形时,会随着绘制的像素点上升,性能急剧下降,通过懒加载按需绘制的方式可以有效的提高绘制性能。
通过保存指令集的方式进行撤销和重做操作,可以有效的节省内存消耗。
Web Audio API 所能做的事情还有很多很多,期待大家一起去深挖。

参考

本文发布自 网易云音乐前端团队,文章未经授权禁止任何形式的转载。我们一直在招人,如果你恰好准备换工作,又恰好喜欢云音乐,那就 加入我们
查看原文

populus 赞了文章 · 2月17日

《前端每日实战》第173号作品:纪念篮球巨星科比·布莱恩特

173.png

世事无常,巨星陨落。特以此作品纪念篮球巨星科比·布莱恩特。

效果预览

按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。

https://codepen.io/comehope/pen/OJPGGmV

源代码下载

每日前端实战系列的全部源代码请从 github 下载:

https://github.com/comehope/front-end-daily-challenges

代码解读

一、绘制篮球

定义 DOM 结构,只有一个名为 .ball<div> 元素,内含一个篮球图案的 unicode 字符:

<div class="ball">🏀</div>

令容器居中,设置页面背景色为由紫色到黑色的径向渐变:

body {
    margin: 0;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-image: radial-gradient(circle, #542482, black);
}

效果如下图:

01.png

设置容器尺寸、字号,放大篮球,这里用 vmin 单位,使图案占据窗口宽高的一半:

.ball {
    width: 50vmin;
    height: 50vmin;
    font-size: 50vmin;
    line-height: 1em;
}

效果如下图:

02.png

二、绘制光环

::before 伪元素绘制光环容器,比主元素宽20%,但高度只有主元素的30%,形状为一个矩形,边框为橙色:

.ball {
    position: relative;
}

.ball::before {
    content: '';
    position: absolute;
    width: 120%;
    height: 30%;
    left: -10%;
    top: -20%;
    border: 2vmin solid orange;
    box-sizing: border-box;
}

效果如下图:

03.png

为边框加圆角,使光环变圆:

.ball::before {
    border-radius: 50%;
}

效果如下图:

04.png

为光环加上光晕,光晕的颜色是半透明的黄色:

.ball::before {
    box-shadow: 0 0 0.1em hsla(60, 100%, 50%, 0.5);
}

效果如下图:

05.png

三、绘制光晕

接下来用 ::after 伪元素绘制阴影,阴影与主元素等宽,但高只有主元素的20%,定位到主元素的底部,背景色为半透明的黑色:

.ball::after {
    content: '';
    position: absolute;
    width: 100%;
    height: 20%;
    left: 0;
    bottom: 0;
    background-color: hsla(0, 0%, 0%, 0.6);
}

效果如下图:

06.png

为阴影容器加圆角属性使阴影变圆,并将阴影置于篮球后面:

.ball::after {
    border-radius: 50%;
    z-index: -1;
}

效果如下图:

07.png

大功告成!

参考

  • 径向渐变背景,《CSS3 艺术》第4.3节
  • 阴影,《CSS3 艺术》第5.1.1节
  • 主元素与伪元素的重叠关系,《CSS3 艺术》第1.9.3节

关于我、我的专栏和我的书

张偶,网络笔名 @comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。

《前端每日实战》专栏,是我近年来实践 PBL(project-based learning)方法的结果,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。

拙作《CSS3 艺术》一书于2020年1月由人民邮电出版社出版,全彩印刷,使用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示。京东/天猫/当当有售。配套资源请访问人民邮电出版社公众号:https://mp.weixin.qq.com/s/6X42QkZ5N8JNji8aQ2FFcQ

查看原文

赞 4 收藏 1 评论 4

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-05-30
个人主页被 462 人浏览