作者: 凹凸曼-大力士


我的项目背景

羚珑平台在动态类的设计中,曾经获得了相应的问题。在这个根底上联合以后大环境,咱们认为能够去做一些动静类的设计,将动画和音效转化为可储存,可移植,可复用的数据。从而用户进行创作的时候,能够通过绝对很简略的形式去应用这些高品质的动画和成果。

视频编辑器解决了什么问题?

视频编辑器的次要作用是用户能够通过操作动态的PSD从而失去咱们想要的动静设计成果。比照AE等简单的视频编辑软件,学习老本大大降低,且动效的可复用性、移植性等也加重了用户的工作量。

以下为设计成果:

开发实录

如何让你的动态PSD"动"起来?

参考 AE 的制作动画的过程,首先会预设剧本和分镜,其次布局好分镜中的镜头如何静止,角色如何静止,以及解决和布局素材。咱们能够提炼出几个关键点:多场景、镜头挪动(即场景整体的动效)、布局素材(图层内容呈现时刻及工夫长短灵便可控)
视频编辑器操作次要波及性能点如下:

  • 多场景的切换与转场成果的交融,使视频成果更加活泼灵便;
  • 场景动效以及动效参数的设置,缩小了同类型动效的开发(如位挪动效合并为一个),也关上了设计师对动效应用的想象力,播种额定的视频成果;
  • 图层操作,调整呈现时刻及持续时间;

编辑器界面如下图:

状态治理

视频编辑器的实现次要分为 5 个局部,视频预览区、动效增加区、参数编辑区、图层操作区、场景操作区,如下图其余局部的每一个操作都会映射到视频预览区,且各个局部数据共享。除此之外,编辑器的每一步操作都须要被”记住“,便于编辑的人回退、还原其操作。

经剖析会波及到以下场景,如:

  • 预览区组件的状态须要共享
  • 其余操作区的变动会扭转预览区组件的状态
  • 组件状态都须要可撤销/还原

咱们能够采纳 redux 集中管理状态以缩小组件之间的数据流传递;对于撤销还原性能,咱们能够采纳 redux-undo,依据现有的 reducer 和配置对象,加强现有其吊销还原性能。

import ReduxUndo from 'redux-undo'//定义原有的 reducerconst editReducer = (state = null, action) => {  switch (action.type) {    case VIDEO_INIT: {       const { templates } = action.payload       return { templates }    }    case VIDEO_TPL_CLEAR: {      return {}    }}//通过 ReduxUndo 加强 reducer 的可撤销性能export const undoEditReducer = ReduxUndo(editReducer, {  initTypes: [VIDEO_TPL_CLEAR],  filter: function filterActions (action, currentState, previousHistory) {    const { isUndoIgnore = false } = action    return !isUndoIgnore  },  groupBy: groupByActionTypes([SOME_ACTION]),  /*  自定义分组  groupBy:(action, currentState, previousHistory) => {      },  */})

参数阐明

  • initTypes:历史记录将依据初始化操作类型进行设置(重置)
  • filter:过滤器, 能够帮忙过滤掉不想在吊销/重做历史中蕴含的操作;
  • groupBy:能够通过默认的 groupByActionTypes 办法将动作组合为单个吊销/重做步骤。也能够实现自定义分组行为,如果返回值不为 null,则新状态将按该返回值分组。如果下一个状态与上一个状态归为同一组,则这两个状态将在一个步骤中归为一组;如果返回值为 null,则 redux-undo 不会将下一个状态与前一个状态分组。

应用 store.dispatch() 和 Undo/Redo Actions 对你的状态执行吊销/重做操作

import { ActionCreators } from 'redux-undo'export const undo = () => (dispatch, getState) => {  dispatch(ActionCreators.undo())}export const redo = () => (dispatch, getState) => {  dispatch(ActionCreators.redo())}export const recovery = () => (dispatch, getState) => {  dispatch(ActionCreators.jumpToPast(0))  dispatch(ActionCreators.clearHistory())}

总结

对于状态治理,首先咱们能够从以下几点思考是否须要引入redux、mobx等工具:

  • 状态是否被多个组件或者跨页面共享;
  • 组件状态须要逾越生命周期;
  • 状态须要如长久化,可复原/撤销等操作。

在应用redux治理状态时,防止将所有状态抽离至redux store中,如

  • 组件的公有状态;
  • 组件状态传递层级较少;
  • 当组件被unmount后能够销毁的数据等

原则上是能放在组件外部就放在组件外部。其次为了状态的可读性和可操作性,在状态结构设计前,须要理分明各个数据对象的关系,均衡数据获取及操作复杂度,举荐扁平化数据结构以缩小嵌套和数据冗余。

图层交互

在应用编辑器的过程中,图层的交互操作是最多最频繁的,咱们参考了罕用的客户端视频编辑软件 AE、Final Cut 的交互,尽可能在 Web 上提供用户操作的便利性及图层可视化,具体成果如下:

梳理图层操作需要,次要蕴含:

  • 图层轨道须要伸缩(调整图层持续时间)
  • 图层上的动效轨道能够独自伸缩(调整动效持续时间)
  • 图层轨道须要左右挪动,且动效轨道追随挪动(调整呈现的时刻)
  • 动效轨道能够独自左右挪动(调整动效呈现的时刻)
  • 不同图层轨道能够上下调整程序,动效轨道追随图层轨道挪动(调整图层程序)
  • 拖动时显示不同的外观

初始的时候首先思考到须要挪动图层程序,咱们基于 react-sortable-hoc 实现了根本的图层程序拖曳挪动 , 然而对于图层的拉伸、左右拖动解决须要自定义鼠标事件进行解决,并须要自定义计算管制图层的挪动,而且最后没有思考到拖动过程中拖动源的外观须要调整,最终,咱们放弃这种实现。咱们须要一个可定制化水平更高的拖曳组件,通过一番比拟后,咱们最终选定了 react-dnd 拖拽组件,查看其官网阐明:

可帮忙您构建简单的拖放界面,同时放弃组件的拆散;且实用于拖动时在应用程序的不同局部之间传输数据,更完满的是组件能够响应拖放事件更改其外观和应用程序状态。

具体阐明下,react-dnd 建设在 HTML5 拖放 API 之上,它能够对已拖动的 DOM 节点进行屏幕快照,并间接将其用作“拖动预览”, 简化了咱们在光标挪动时进行绘制的操作。不过,HTML5 拖放 API 也有一些毛病。它在触摸屏上不起作用,并且在 IE 上提供的自定义机会少于其余浏览器。这就是为什么在 react-dnd 中以可插入方式实现 HTML5 拖放反对的起因,你也能够不应用它,依据触摸事件,鼠标事件等本人来编写其余实现。

上面,咱们从外到内,介绍根本的实现。

场景层面

引入所需组件

import { DndProvider } from 'react-dnd'import HTML5Backend from 'react-dnd-html5-backend'

将 DndProvider 放在整个场景的外层,设置 backend 为 HTML5Backend

<DndProvider backend={HTML5Backend}>   <TemplateViewer   // ----- 单个场景展现组件    template={tpl}    handleLayerSort={handleLayerSort}    onLayerDrop={onLayerDrop}    onLayerStretch={onLayerStretch}  />  <CustomDragLayer />  // --- 自定义拖拽预览图层</DndProvider>

TemplateViewer 里蕴含不同类型的图层组件。每个图层组件都提供一个纯渲染组件的办法 renderLayerContent,大抵构造如下:

export function renderLayerContent (layer) {  return <div style={{...}}>...</div>}export default function XxxxLayerComponent (layer) {  ...  return <div>{renderLayerContent(layer)}</div>}

CustomDragLayer 里依据以后拖拽的对象的组件类型,调用相应 renderLayerContent 绘制拖拽可视内容,以实现拖拽前后的视图统一。

图层层面


图层能够高低拖动,也能够左右拖动,象征它自身即是拖拽源,也是搁置的指标。

为了辨别拖拽的目标,咱们定义了两个拖拽源

  const [{ isHorizontalDragging }, horizontalDrag, preview] = useDrag({    item: {      type: DragTypes.Horizontal,    },    collect: monitor => ({      isHorizontalDragging: monitor.isDragging(),    }),  })  const [{ isVerticalDragging }, verticalDrag, verticalPreview] = useDrag({    item: {      type: DragTypes.Vertical,    },    collect: monitor => ({      isVerticalDragging: monitor.isDragging(),    }),  })

在搁置解决中依据拖拽类型进行判断解决

  const [, drop] = useDrop({    accept: [DragTypes.Horizontal, DragTypes.Vertical],    drop (item, monitor) {      // 解决左右拖动    },    hover: throttle(item => {      // 解决高低排序    }, 300),  })

将定义好的拖动源和搁置指标关联 DOM 。最外层 DIV 为图层可拖动区域即搁置指标,而后顺次为程度拖拽层,垂直拖拽层

<div ref={drop}> // --- 搁置指标 DOM  <div ref={verticalPreview}>    <div ref={horizontalDrag}> // --- 程度拖拽 DOM          <div ref={verticalDrag}> // --- 垂直拖拽 DOM        <Icon type='drag'/>      </div>      /* 图层内容展现 */      <div>{renderLayerContent(layer)}</div>    </div>  </div></div>

以上对于图层高低拖动、左右拖动的大体框架曾经实现。

高低拖动排序时,为了拖动过程中不展现拖动源只保留生成的屏幕快照,能够依据以后的拖动状态将拖动源的透明度设置为 0

<div ref={drop}> // --- 搁置指标 DOM  <div    ref={verticalPreview}    style={{ opacity: isVerticalDragging ? 0 : 1 }}  >   ...  </div></div>

程度拖动时,设置拖动源半透明,解决形式与高低拖动时同理。

图层内

图层内有两个区域,下方区域可通过左右两端的操作点进行拉伸,上方区域能够在下方区域的宽度内左右挪动以及同样通过左右两端的操作点进行拉伸。
挪动的实现形式后面曾经介绍过就不反复了,针对拉伸的操作,咱们封装一个 Stretch 类来对立解决

function Stretch ({  children,  left,  width,  onStretchEnd,  onStretchMove,}) {  function handleMouseDown (align) {    // 计算偏移  }  return (    <div>      {children}      <div        className={classnames(styles.stretch, styles.stretchHead)}        onMouseDown={handleMouseDown('head')}      />      <div        className={classnames(styles.stretch, styles.stretchEnd)}        onMouseDown={handleMouseDown('end')}      />    </div>  )}

将须要反对拉伸的区域作为作为 Stretch 的 children 传递进来

<div>  <div>    {motions.map((motion, i) => <Stretch key={i}>{/* 上方某个区域 */}</Stretch>)}  </div>  <div>    <Stretch>{/* 下方区域 */}</Stretch>  </div></div>

体验优化

增加快捷键

整个编辑器内容比拟的多,对频繁的操作,咱们能够保留罕用快捷键的操作习惯。如空格播放、delete 删除等等,该性能咱们能够应用 react-hot-keys 实现。

首先引入该快捷键库,而后指定绑定的快捷键,增加事件处理。

import Hotkeys from 'react-hot-keys'<Hotkeys  keyName='space'  onKeyDown={(keyName, e) => {    e.preventDefault()    play()  }}/>

文本转 SVG

另外图层内容展现时有个小技巧,产品需要中文案图层平铺展现。可怜我最后居然是通过文本长度以及轨道长度计算出文本展现次数,而后再放到 push 到节点中。经大佬革新后才明确能够将文本转化为 SVG 而后以背景图展现,真香!

<div  className={styles.contentText}  style={{    backgroundImage: `url("data:image/svg+xml;utf8,      <svg xmlns='http://www.w3.org/2000/svg' version='1.1'     width='${size(layer.text) * 12 + 15}px' height='35px'>      <text x='10' y='22' fill='black' font-size='12'>      ${layer.text}      </text>      </svg>")`,      }}/>

实现成果:

我的项目总结

本文讲述了视频编辑器中操作区次要模块的解决。对于状态治理,咱们次要须要明确引入管理工具的是否必要以及应用状态管理工具后是否所有状态都必须移入store中等等。另外对于简单的图层拖拽性能,要像剥洋葱一样,先层层拆解,从而层层欠缺其构造。
对我的项目而言,拿到需要后,咱们从整体到部分进行剖析,优先确定整体的框架、外围性能的实现形式等,进而思考如何进步用户体验度。需要分清主次,以便于咱们排列优先级从而开发提高效率。

参考资料

[1] react-dnd: https://react-dnd.github.io/r...


欢送关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

欢送关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。