整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

php手把手教你做网站(二十一)vue使用wangEditor富文本编辑器

1 wang编辑器效果图

1、npm安装

安装过程比较简单,不做重复,说一下使用过程遇到的问题

  1. 如果编辑器放到了table td内,会发现插入分隔线(也就是插入hr)不好用,没有找到在哪里改,换一个方式去实现:直接使用editor.config.menus = []加载我们使用的菜单,主要是为了去掉splitLine(分隔线),然后使用自定义扩展菜单创建新的分隔线菜单;
  2. 添加查看源码扩展;
  3. 弹出的窗口关闭的时候,只是切换到了后边的菜单不能关闭菜单。

插入hr

import E from 'wangeditor'

mounted () {
    const editor = new E('#div1')
    const menuKey = 'hrMenuKey'
    const { BtnMenu } = E

    // 第一,菜单 class ,Button 菜单继承 BtnMenu class

    class HrMenu extends BtnMenu {
      constructor (editor) {
        // data-title属性表示当鼠标悬停在该按钮上时提示该按钮的功能简述
        const $elem = E.$(
          `<div class="w-e-menu" data-title="分割线">
            <i class='w-e-icon-split-line'></i>
          </div>`
        )
        super($elem, editor)
      }
      // 菜单点击事件
      clickHandler () {
        editor.cmd.do('insertHtml', '<hr>')
      }
      tryChangeActive () {
        // 激活菜单
        // 1. 菜单 DOM 节点会增加一个 .w-e-active 的 css class
        // 2. this.this.isActive === true
        this.active()
        // // 取消激活菜单
        // // 1. 菜单 DOM 节点会删掉 .w-e-active
        // // 2. this.this.isActive === false
        // this.unActive()
      }
    }
    // 注册菜单
    E.registerMenu(menuKey, HrMenu)

    editor.config.placeholder = ''
    editor.config.uploadImgServer = '/public/sss/admin.php/ajaxweb/uppic.html'
    editor.config.uploadImgMaxSize = 1024 * 1024
    editor.config.uploadImgAccept = ['jpg', 'jpeg', 'png', 'gif']
    editor.config.height = 300
    editor.config.focus = true
    editor.config.menus = [
      'source',
      'head',
      'bold',
      'fontSize',
      'fontName',
      'italic',
      'underline',
      'strikeThrough',
      'indent',
      'lineHeight',
      'foreColor',
      'backColor',
      'link',
      'list',
      'justify',
      'quote',
      'image',
      'video',
      'table',
      'undo',
      'redo']
    editor.create()
}

查看源码

图2 查看源码效果图

实现目标:

点击查看的时候,遮盖其它的按钮,防止查看源码的时候,点击了别的按钮进行了误操作。

新加的菜单默认都是在最后全屏前边,分割线还可以,但是查看源码我个人还是习惯在最前边,使用的是jquery prepend感觉更加简单一些,代码如下:

import $ from 'jquery'
mounted () {
  $(document).ready(function () {
      $('#div1 .w-e-toolbar').prepend('<div class=\'w-e-menu\' style=\'z-index:991;\' data-title=\'查看源码\'><a style=\' display:block;width:100%;height:100%;\' ct=1 id=\'viewsource\'><i class=\'fa fa-file-text-o\'></i></a></div>')
      $(document).delegate('#viewsource', 'click', function () {
        var editorHtml = editor.txt.html()
        // console.log($(this).attr('ct'))
        if (parseInt($(this).attr('ct')) === 1) {
          $('#div1 .w-e-toolbar').prepend('<div id=\'zzc\' style=\'position:absolute;left:0;top:0;z-index:99;background-color:rgba(0,0,0,0.5);width:100%;height:40px;\'></div>')
          $(this).parent().parent().parent().find('.w-e-text').css('width', $('.w-e-text').width() + 'px')
          editorHtml = editorHtml.replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ')
          $(this).attr('ct', '2')
          $(this).css({'background-color': '#EEE'})
        } else {
          editorHtml = editor.txt.text().replace(/</ig, '<').replace(/>/ig, '>').replace(/ /ig, ' ')
          $(this).attr('ct', '1')
          $(this).parent().parent().parent().find('.w-e-text').css('width', '100%')
          $(this).parent().parent().find('#zzc').remove()
          $(this).css({'background-color': '#FFF'})
        }
        editor.txt.html(editorHtml)
        // editor.change && editor.change()
      })
    })
}

说明:

  1. 使用jquery prepend向头部插入查看源码按钮;
  2. ct用来判断是查看源码,还是切换回原来的格式:1 查看源码 2切换回原来的;
  3. <div id="zzc"></div>是遮罩层,用于点击查看的时候遮盖其它按钮,防止误操作,查看的时候添加,切换的时候移除;
  4. 查看源码的时候,如果全是英文有可能出现不换行,页面被撑开的情况(因为宽度是100%),所以这里要获取编辑器所在div w-e-text的宽度(像素),通过jquery重新设置宽度;

弹出的窗口,点击关闭无效不能关闭

图3 弹出菜单无法关闭

如图,点击关闭的时候会切换到了网络图片的表单,这应该是菜单同级别,遮盖了关闭的按钮,所以我们要写css样式加上z-index使关闭的菜单在其他的上层;

css代码

.w-e-icon-close{
   display:block;
   z-index:999999 !important;
}

2、cdn引用js

我是下载到本地的

图4 多图上传F12查看效果

图片上传,如果选择多个图片,可能会出现以上图片的情况style="font-size: 14px; font-family: "Helvetica Neue", Helvetica, "PingFang SC", Tahoma, Arial, sans-serif; max-width: 100%;"

复制html发现是如下代码:

<img src="/public/upload/image/20211201/111.jpg" style="font-size: 14px; font-family: & quot;Helvetica Neue& quot;, Helvetica, & quot;PingFang SC& quot;, Tahoma, Arial, sans-serif; max-width: 100%;">

很明显style内的 css样式,不是应该出现的,没有找到在哪里修改。

双引号换成了& quot;如果不用查看源码,页面直接显示没有问题,但是查看源码,切换回来以后,会发现图片乱了,所以查看源码的时候,需要把& quot;替换成空。

以下使用jquery实现自定义菜单(查看源码、插入分割线):

import $ from 'jquery'
  mounted () {
      $(document).ready(function () {
      //  查看源码菜单
      $('#div1 .w-e-toolbar').prepend('<div class=\'w-e-menu\' style=\'z-index:991;\' data-title=\'查看源码\'><a style=\' display:block;width:100%;height:100%;\' ct=1 id=\'viewsource\'><i class=\'fa fa-file-text-o\'></i></a></div>')
      //  分割线菜单
      var menl=$('#div1 .w-e-toolbar .w-e-menu').length;
		  $("#div1  .w-e-toolbar .w-e-menu").eq(menl-1).before('<div class=\'w-e-menu\'  data-title=\'分割线\'><a  style=\'display:block;width:100%;height:100%;\' id=\'splitline\'><i class=\'w-e-icon-split-line\'></i></a></div>')
      //  查看源码点击
      $(document).delegate('#viewsource', 'click', function () {
        var editorHtml = editor.txt.html()
        // console.log($(this).attr('ct'))
        if (parseInt($(this).attr('ct')) === 1) {
          $('#div1 .w-e-toolbar').prepend('<div id=\'zzc\' style=\'position:absolute;left:0;top:0;z-index:99;background-color:rgba(0,0,0,0.5);width:100%;height:40px;\'></div>')
          $(this).parent().parent().parent().find('.w-e-text').css('width', $('.w-e-text').width() + 'px')
          editorHtml = editorHtml.replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ').replace(/& quot;/g, '')
          $(this).attr('ct', '2')
          $(this).css({'background-color': '#EEE'})
        } else {
          editorHtml = editor.txt.text().replace(/</ig, '<').replace(/>/ig, '>').replace(/ /ig, ' ')
          $(this).attr('ct', '1')
          $(this).css('border', '0px solid #DDD')
          $(this).parent().parent().parent().find('.w-e-text').css('width', '100%')
          $(this).parent().parent().find('#zzc').remove()
          $(this).css({'background-color': '#FFF'})
        }
        editor.txt.html(editorHtml)
        // editor.change && editor.change()
      })
      //分割线插入hr点击
	    $(document).delegate('#splitline', 'click', function () {
		    editor.cmd.do('insertHtml', '<hr>');
	    });
    })
  
  }

如果我们把插入分割线的代码单独拿出来,只是下面几行,相对使用官方提供的方法会简单很多

      //   插入分割线菜单  
      var menl=$('#div1 .w-e-toolbar .w-e-menu').length;
		  $('#div1  .w-e-toolbar .w-e-menu').eq(menl-1).before('<div class=\'w-e-menu\'  data-title=\'分割线\'><a  style=\'display:block;width:100%;height:100%;\' id=\'splitline\'><i class=\'w-e-icon-split-line\'></i></a></div>')
      // 分割线插入 hr点击事件
	    $(document).delegate('#splitline', 'click', function () {
		    editor.cmd.do('insertHtml', '<hr>');
	    });

说明:

  1. menl是菜单的个数;
  2. $("#div1 .w-e-toolbar .w-e-menu").eq(menl-1).before 使用jquery before向最后一个菜单(也就是全屏菜单)之前插入分割线按钮,使用after也可以;
  3. 查看源码的替换多加了.replace(/& quot;/g, ‘’) ;
  4. 如果是cdn引入的js可能出现菜单一些功能不好用的情况,点击了没反应,可以尝试把const editor = new E('#div1') 换成window.editor = new E('#div1')
  5. 注意:因为& quot;连起来会被转换成双引号,这里没法显示,实际替换的时候要连起来,不要有空格;


editorHtml = editorHtml.replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ').replace(/& quot;/g, '') 

附录一下jquery after,before,append,prepend用法:

向class=w-e-toolbar的div头部插入html

$(" .w-e-toolbar").prepend("<div >头部插入html</div>")

向class=w-e-toolbar的div底部插入html

$(" .w-e-toolbar").append("<div >底部插入html</div>")

向class=w-e-toolbar的div之前插入html

$(" .w-e-toolbar").before("<div >之前插入html</div>")

向class=w-e-toolbar的div之后插入html

$(" .w-e-toolbar").after("<div >后面插入html</div>")

可以做一下延伸:像我们上边用到的向第几个元素之后或者之前插入代码。

随着用于访问万维网的设备的数量和种类呈指数级增长,无论用于浏览网页的设备如何,确保正确呈现网页都是一项重要且具有挑战性的任务。当开发人员采用响应式网页设计(RWD)技术时,网页会修改其外观以适应设备的显示限制。但是,目前缺乏自动化支持意味着在针对不同窗口大小进行渲染时,可能无法检测到页面布局中的显示故障。一个核心问题是难以提供自动Oracle来验证RWD布局,这意味着在实践中检查故障主要是手动过程,这导致许多实时响应式网站的布局故障。本文介绍了一种自动故障检测技术,该技术可检查响应页面布局在一系列窗口宽度上的一致性,从而避免了对显式Oracle的需求。在一项实证研究中,该方法在所研究的26个真实产品页面中的16个中发现了故障,总共检测到33个不同的故障。

关键词

响应式网页设计,显示故障,布局故障

1 介绍

响应式网页设计(RWD)是最近的一种设计和实现方法,它使开发人员能够构建无论设备大小如何都能提供等效用户体验的网页。RWD使网页能够动态地修改其布局以适应或“响应”设备显示器的大小,而不是要求用户平移太宽而不适合较小屏幕的页面,或缩放在桌面显示器上清晰可读,但在手机上太小的部分。如果内容多于可用空间,则用户只需垂直滚动页面。因此,在RWD的上下文中,浏览器窗口宽度是关于网页布局应如何调整的关键决定因素。

鉴于RWD的明显优势,自动检查显示故障的问题——网页渲染中的视觉差异导致其偏离其预期外观——是一个重要的问题。由于网站的美学和布局已被证明会影响其感知的可用性[26]和可访问性,提高其可信度,并产生用户忠诚度,因此在一个组织的网站中可见的故障可能导致收入损失并不令人惊讶。

针对这些问题,我们提出了一种自动化技术,可以检测在实际生产页面中普遍存在的五种类型的响应式布局故障,而无需显式的Oracle,例如一系列模型映像,复杂的布局规范或要比较的页面的图形模型。我们的方法依赖于常见的响应故障类型的隐式Oracle知识,自动检查响应式网页的布局,并在不同的窗口宽度上比较元素相对于彼此的位置。

例如,两个网页元素可能由于预期的图形效果而重叠,或者因为它们已经“碰撞”,因为水平屏幕空间已经减少。我们的方法通过检查连续窗口宽度的布局行为来区分两者。如果元素总是重叠,则开发人员在手动检查中可能意图和/或容易注意到该效果。然而,如果元素经常重叠,则可能发生微妙的RLF。我们的方法应用类似的原则来检测不一致的元素包装;仅存在一个或两个窗口宽度的布局;和内容的间歇性突出到其他元素,或完全从窗口出来。总的来说,我们定义了四种检测这五种RLF的算法。

我们将自动化技术应用于26个真实产品网页。实验表明,我们的方法可以在16个网页中找到故障,总共检测到33个不同的故障。我们的评估进一步表明,在当前可用工具的帮助下应用手动检查过程错过了我们的技术可以自动检测的19%至34%的RLF。

总结来说,本位的重要贡献如下:

(1)可发现的五种不同类型的响应布局故障(RLF)的分类,而不需要明确的Oracle。

(2)四种算法,可以自动检测响应设计的网页中的五种类型的布局故障。

(3)包含26个随机选择的产品网页的实证研究,显示所识别的RLF类型在实时站点中普遍存在,并且我们的算法能够检测它们,总共发现了33个不同的故障。

2 响应式网页设计的自动化故障检测

本节定义了五种不同类型的响应式布局故障(RLF),它们对于RWD都是有问题的,并且可以通过不需要显式Oracle的算法自动识别,例如网页的替代参考版本,模型图像或布局约束的复杂规范。

相反,我们的方法通过自动提取网页响应布局的模型,然后通过在不同的窗口宽度交叉检查其布局来分析该模型的潜在故障。我们首先介绍网页模型(第3.1节)。然后,我们介绍了响应式网页中普遍存在的五种类型的RLF,以及可用于通过网页模型检测它们的四种算法的定义(第3.2节)。

2.1基本概念:rRLG

我们的RLF检测过程的基础是页面的响应布局模型,它是响应布局图(RLG)的改进。RLG不同于网页的其他布局图模型(例如,Choudhary等人的“对齐图”[38]和Alameer等人的“布局图”),因为它模拟了一个窗口宽度范围内的网页——而不是一个静态宽度——以捕获其响应式设计。

通过查询网页的文档对象模型(DOM)来自动获得RLG,以在不同的窗口宽度处找到页面中涉及的HTML元素及其坐标。RLG根据其响应式设计,组织此信息以跟踪这些HTML元素的动态可见性和相对对齐,因为页面布局根据窗口宽度进行调整。“精制RLG”(rRLG)与我们原始的RLG的不同之处在于它不通过“宽度约束”对网页元素的宽度进行建模。虽然设计用于在页面中捕获回归问题,但宽度约束无助于检测本文中介绍的五种常见类型的RLF;因此rRLG不对它们进行建模。

图2提供了一个rRLG的示例,用于通过线框在两个不同的窗口宽度上绘制的网页。该页面涉及HTML元素div[1]-div[3],这些元素相互堆叠用于窄窗口,要求用户滚动以将每个视图放入视图中。对于更宽的窗口,它们并排排列,并附有横幅图像img。

2.2响应式布局故障的常见类型及检测算法

一旦为响应式网页创建了rRLG,就可以检查我们接下来介绍的五种RLF中的每种类型以及可用于检测它们的算法。 每种算法的目的不仅是识别故障,还要识别涉及哪些HTML元素以及特定窗口宽度,以帮助开发人员诊断故障。

RLF1:元素碰撞。当响应设计的页面的窗口变得更窄时,考虑宽度损失的一种设计策略是将水平对齐的页面元素移动得更靠近。但是,当窗口收缩时,元素可能会相互碰撞,导致其内容重叠。这可能导致意外的效果,例如重叠的文本或图像,或隐藏的内容或功能元素,从而损害页面可用性。

图1和MidwayMeetup页面显示了一个示例。在更宽的窗口(第1d部分),输入框和按钮并排存在。然而,当窗口变得太窄时,元素发生碰撞,使最左边的按钮变暗(第1c部分)。

RLF2:元素突出。在实现响应式设计时,一个问题是确保随着窗口变得更窄,HTML元素的大小也会适应,因此它们仍然足以容纳其内容。当元素没有正确调整大小时,它们的内容可能不再“适合”,因此会突出到页面的周围部分。图1的PDFescape示例及其导航链接块显示在页面的右上角(第1f部分)。随着窗口变窄,水平空间不再足以适合徽标旁边的链接块。链接突出显示包含的HTML元素,对用户(第1e部分)不可见,因为容器具有CSS属性“overflow: hidden”集。因此,链接变得不可点亮,并且页面在特定大小的设备上是不可用的,并且窗口尺寸是固定的。

RLF3:窗口突出。当窗口空间被挤压时,元素可能不仅开始溢出其容器,而且还开始从页面的根表示HTML元素(即身体标签)中突出,从而出现在水平可视部分之外。这页纸。如第2节所介绍的图1的ConsumerReports网页展示了这种故障类型

RLF4:小范围布局。响应式设计的网站倾向于使用许多CSS规则,这些规则由不同的媒体查询激活和停用。对于给定的窗口宽度,CSS规则中的多个媒体查询可以同时评估为真。例如,两个规则,一个在窗口宽度超过768像素时激活,另一个规则在窗口低于1024像素时激活,将在769-1023像素范围内激活。对于给定的元素集和窗口大小,当一系列规则“开启”或“关闭”时,管理的逻辑可能很快变得复杂,开发人员可能经常犯错误导致在窗口中应用CSS规则宽度与意想的不同。当开发人员混合使用最小宽度和最大宽度限定符来定义布局中的更改时,就会出现这种类型的常见错误。例如,开发人员可以编码媒体查询“@media(max-width:768px){...} “和另一个”@media(min-width:768px){...}”。由于这两个表达式定义的窗口范围是包含的,因此两者都将在768像素窗口宽度处激活。媒体查询的这种冲突可能导致奇怪的布局效果,因为当只有一个集合时,将激活两组规则。开发人员难以发现这些类型的故障,因为它们仅出现在可以查看页面的整个窗口宽度范围的小范围内。图1的Cloudconvert示例是小范围RLF的示例。在单个窗口宽度处,页面的顶部菜单模糊了公司的徽标和标语(第1g部分)。

RLF5:包装元素。如果网页上的包含元素不足以容纳其子元素,但仍然“足够”或具有灵活的高度,则其中包含的水平对齐元素将“换行”以形成其他元素,不受欢迎的元素行 - 以及不需要的表现效果。图1和BugMeNot网页显示了错误包装的示例。在较宽的窗口(第1j部分),放大镜按钮出现在搜索框旁边。然而,随着窗口变窄,按钮会包裹到下一行(第1i部分)。

3 实证评估

为了评估我们技术的有效性和效率,我们将其应用于生产使用的26个真实网页,最终目的是回答以下三个研究问题:

RQ1:我们的算法在检测故障方面的效果如何?

我们的算法在响应式设计页面中检测常见类型的响应式布局故障的效果如何?

RQ2:使用“spotchecking”工具可以检测到多少次故障?

在窗口大小调整工具的帮助下,与我们的方法相比,“抽样检查”过程的效果如何?

RQ3:应用于响应式设计的网页时,我们的技术需要运行多长时间?

对于将在实际的RWD网页开发过程中应用该技术的开发人员来说,检测布局故障的时间是否合理?

RQ1:表1b使用每种检测算法分解了ReDeCheck针对每个网页生成的故障报告的分类。我们的每个算法都发现了TP(即实际的RLF),26个受试者中有16个至少有一个TP。鉴于对象是包括Duolingo和StumbleUpon等已建立的商业运营的实时和运营网站,这是一个令人信服的结果,因为我们推测,这些网站将进行内部测试过程,错过了由ReDeCheck。我们的算法报告的五个故障是我们用于激励示例的故障。这些故障是消费者端口离屏突出的内容示例(图1a,算法2检测到);表单元素相互遮挡,降低了MidwayMeetup的功能(图1c,由算法1检测到);导航链接由于其父元素为PDFescape的突出而消失(图1e,也被算法1检测到);为Cloudconvert打破媒体查询和模糊布局(图1g,由算法3检测),以及BugMeNot的包装元素(图1i,由算法4检测)。

TP的进一步分析显示,这些报告总共汇总了33个不同的故障,如表1b的“Distinct RLFs”栏所报告,从而揭示了ReDeCheck结果的重复程度。一个极端的例子是Accountkiller。在这里,由算法3生成的147个小范围报告是TP,但更仔细的分析显示所有报告都对应于单个明显的故障。该页面涉及图标网格,每个图标对应于rRLG节点,其中对齐约束连接每对。对于一个小窗口范围,ReDeCheck检测网格中未按一致排列的元素,导致相对对齐的变化和一些小范围约束,这些约束导致算法触发每个单独的故障报告。另外,可以通过不同的算法检测相同的不同故障。例如,对于Cloudconvert,元素仅在小范围内发生碰撞,从而触发算法1和算法3的报告。

RQ2:Spotcheck分析显示我们的方法已经发现RLF作为RQ1的一部分,但没有新的额外故障。表2报告了在每个工具建议的窗口宽度之一处或通过随机选择窗口宽度而显示的这些不同RLF的数量。如表所示,工具显示了66%到81%的这些故障,具体取决于他们建议检查的窗口宽度集。由于这些预设宽度对应于常用设备,因此该结果显示ReDeCheck能够显示将在这些设备上显示的故障以供用户查看。尽管点测工具提示了检查网页的窗口宽度,但仍然需要手动识别故障——相比之下,Re-DeCheck减轻了开发人员的一些努力。结果还表明,我们的技术确定的故障率为19%至34%。即使使用了所有的抽查工具,并通过一定程度的进一步随机抽查来补充,我们的方法最初确定的五种不同的RLF也找不到。

RQ3:图3显示了ReDeCheck在30个试验中每个网页的执行时间中位数。几乎所有页面(26个中的25个)都是由我们的工具在中位数大约三分钟内处理的,15页需要60秒或更少。 Airbnb花了将近4.5分钟——但它是最复杂的对象页面之一,如表1a所示。一般来说,图表显示,花费最多时间的对象要么是最复杂的一部分(即,就CSS元素而言是Airbnb,而在CSS声明方面是Duolingo),或者产生最多的问题(即Accountkiller和PepFeed),因此,在捕获故障屏幕截图和生成带注释的图形报告(针对TP,FP和NOI)或者网页复杂性和报告的问题数量(即,ConsumerReports)的混合时,会产生额外的时间成本。

4 结论以及未来工作

响应式网页设计(RWD)倡导创建具有跨多个窗口宽度的增强用户体验的网页[32]。由于根据RWD原则[20]实施网页具有挑战性,我们之前的工作提出了一种回归检查技术,该技术比较了页面的两个模型并提出了任何差异[40]。专注于处理与检查响应式网页的问题正交的问题,其他先前的工作(例如,[17,18])没有采用设计用于检查多个窗口的Oracle。相比之下,本文的自动化方法利用隐式Oracle信息来检测响应式布局故障。实验表明它是有效的:在8个网页中发现2个或更多的故障,它在一个网页中发现了6个故障,在26个对象中检测到总共33个不同的故障。

致谢

此文由南京大学软件学院2016级本科生虞圣呈翻译转述。

本文内容是笔者最近实现的 web 端弹幕组件—— Barrage UI(https://github.com/parksben/barrage) 的一个延伸。在阅读本文的实例和相关代码之前,不妨先浏览项目文档,对组件的使用方式和相关接口进行了解。



各位童鞋如果经常上 B 站(bilibili.com) ,应该对 蒙版弹幕 这个概念并不陌生。

蒙版弹幕是由知名弹幕视频网站 bilibili 于 2018 年中推出的一种弹幕渲染效果,可以有效减少弹幕文字对视频主体信息的干扰。

关于 B 站蒙版弹幕的实现原理,其实网上已经有很多细致的讨论和研究。个人总结了一下,大致要点如下:

  1. 基于用户数据和一些机器学习的相关应用,可以提炼出视频的关键主体
  2. 服务端预先对视频进行处理,并生成相应的蒙版数据
  3. 客户端播放视频时,实时地加载对应资源
  4. 通过一些前端的技术手段,实现弹幕的蒙版处理

客户端方面,由于 B 站弹幕是基于 div+css 的实现,因而采用了 svg 格式来传输矢量蒙版(至少目前是这样),通过 CSS 遮罩的方式实现渲染。

■乎上有一篇关于这个方案的讨论,感兴趣的童鞋可以移步 这里(https://www.zhihu.com/question/279446628) 进行了解。

Barrage UI

Barrage UI(https://github.com/parksben/barrage) 是个人最近实现的一个前端弹幕组件,主要用于在前端页面中挂载弹幕动画。

组件提供了一系列的操作接口以方便用户对弹幕的相关特性进行定制。你也可以在渲染层面对动画中的每一帧图像进行处理,比如:

  1. 实时读取视频信息
  2. 对每一帧视频图像进行实时处理,计算出抠图蒙版
  3. 将计算出的蒙版传给弹幕组件,以实现实时的蒙版弹幕

下面是基于 Barrage UI 组件实现的蒙版弹幕效果:

编者注:图片这么糊是微信公众平台的锅。

由于文中不方便嵌入视频,Demo 的实际效果请移步到 此处(https://parksben.github.io/masking-danmaku-demo) 查看。

下面我们来介绍如何实现上图的动画效果。

色键(色度键控)

Demo 中使用了初音小姐姐跳舞的视频。最主要的特点是除了人物外,视频的背景是比较一致的纯色。对于这种类型的图像,我们可以使用 色键 的方式进行抠图(生成“蒙版”)。

色度键控(https://zh.wikipedia.org/zh/%E8%89%B2%E9%94%AE),又称色彩嵌空,是一种去背合成技术。Chroma 为纯色之意,Key 则是抽离颜色之意。把被拍摄的人物或物体放置于绿幕的前面,并进行去背后,将其替换成其他的背景。此技术在电影、电视剧及游戏制作中被大量使用,色键也是虚拟摄影棚(Virtual studio)与视觉效果(Visual effects)当中的一个重要环节。

下图是色键技术的一个示例:在绿幕前穿着蓝色衣服的小姐姐,左图为去背前,右图为去背后的新背景。

如何扣取视频图像

在浏览器环境中,我们可以通过 canvas 画布实时地绘制视频的每一帧,并从画布中读取到图像中每个像素的 RGBA 信息,检测每个点的 R(red)、G(green)、B(blue) 值是否满足要求,最终将需要扣除的像素的 A(alpha) 值置为 0,即可得到用于合成蒙版弹幕的蒙版图像。

注意:

Barrage UI 组件的蒙版功能是基于 Canvas 2D API 的 CanvasRenderingContext2D.globalCompositeOperation 属性实现的(使用了 source-in 的混合模式),因而只需将不需要的像素设置为透明(alpha=0)即可,并不需要改变图像的 RGB 色值。

下面介绍此案例的代码实现。

具体实现

安装 Barrage UI 组件

直接使用 yarn 或 npm 安装此组件:

yarn add barrage-ui or npm install --save barrage-ui

HTML + CSS

准备一个 video 元素用于播放视频,video 的父级元素用于挂载弹幕:

<div id="container">
  <video id="video" src="videos/demo.mp4" controls>
</video>
</div>

根据视频的实际尺寸(880×540)设置 #container#video 的样式:

html,
body {
  font: 14px/18px Helvetica, Arial, 'Microsoft Yahei', Verdana, sans-serif;
  width: 100%;
  margin: 0;
  padding: 0;
  background: #eee;
  overflow: hidden;
}
#container,
#video {
  width: 880px;
  height: 540px;
}
#container {
  margin: 0 auto;
  margin-top: 50vh; 
  margin-left: 50vw; 
  transform: translate(-50%, -50%);
  background-color: #ddd;
}

创建弹幕

import Barrage from 'barrage-ui';
import data from 'utils/mockData';

// 获取父级容器
const container = document.getElementById('container');

// 创建弹幕实例
const barrage = new Barrage({
  container: container,
});

// 重置画布高度,避免弹幕遮挡视频播放控件
barrage.canvas.height = container.clientHeight - 80;

// 装填弹幕数据
barrage.setData(data);

其中,mockData 是用于生成随机弹幕数据的方法。

关于弹幕数据的内容与格式,详见 Barrage UI 项目文档(https://github.com/parksben/barrage#%E8%A3%85%E5%A1%AB%E5%BC%B9%E5%B9%95)

实时获取视频图像

// 获取 video 元素
const video = document.getElementById('video');

// 新建一个画布来实时绘制视频(纯绘图,不用添加进页面)
const vCanvas = document.createElement('canvas');
vCanvas.width = video.clientWidth;
vCanvas.height = video.clientHeight;const vContext = vCanvas.getContext('2d');

// 实时绘制视频到画布
barrage.afterRender = () => {
  vContext.drawImage(video, 0, 0, vCanvas.width, vCanvas.height);
};

使用组件提供的渲染周期钩子 .afterRender() 可以在弹幕动画的每一帧图像渲染后,将视频图像绘制到中间画布 vCanvas 上。注意这里的 vCanvas 画布主要用于实时地获取视频图像,并不需要添加到页面中。

实时计算蒙版信息

// 渲染前读取画布 vCanvas 的数据,并处理为蒙版图像
barrage.beforeRender = () => {

  // 读取图像
  const frame = vContext.getImageData(0, 0, vCanvas.width, vCanvas.height);
  
  // 图像总像素个数
  const pxCount = frame.data.length / 4; 
  
 // 将 frame 构造成我们需要的蒙版图像
  for (let i = 0; i < pxCount; i++) {
    // 这里不用 ES6 解构赋值的写法,主要为了保证性能
    // PS: 这里如果用解构赋值语法将导致大量新对象的创建,是个很耗时的过程
    const r = frame.data[i * 4 + 0];
    const g = frame.data[i * 4 + 1]; 
    const b = frame.data[i * 4 + 2];
    
    // 将黑色区域以外的内容设为透明
    if (r > 15 || g > 15 || b > 15) {
      frame.data[4 * i + 3] = 0;
    }
  }
    
  // 设置蒙版
  barrage.setMask(frame);
};

使用组件提供的渲染周期钩子 .beforeRender() 可以在弹幕动画的每一帧图像渲染前计算出蒙版图像。其中,用于更新蒙版的接口为 .setMask()

视频、弹幕的操作绑定

最后,为了让弹幕的行为与视频播放的操作协同,还需要进行一些绑定的操作:

// 绑定播放事件
video.addEventListener(  'play',
  ( ) => {
    barrage.play();
  },  false);

// 绑定暂停事件
video.addEventListener(  'pause',
  ( ) => {
    barrage.pause();
  },  false);

// 切换播放进度
video.addEventListener(  'seeked',
  ( ) => {
    barrage.goto(video.currentTime * 1000);
  },  false);

这里分别用到 Brrage UI 组件的 .play() .pause .goto() 三个接口,分别用于播放暂停切换弹幕动画的进度。需要注意的是,通过 video.currentTime 属性获取到的视频播放进度是一个单位为 的浮点数,需要转换为 毫秒数 再传给弹幕组件。

源码奉上

本文的案例已上传 Github,感兴趣的童鞋可以点击 这里(https://github.com/parksben/masking-danmaku-demo) 查看源码细节。

关于 Barrage UI 组件如果有什么建议和疑问,欢迎大家在项目中提 issue 给我,帮助我持续改进和迭代,更欢迎 star 和 PR。


作者:Parksben

来源:微信公众号:创宇前端

出处:https://mp.weixin.qq.com/s/0HwQh5nUaK6Vi0EWbU8ENQ