@jxstjh/jhvideo
Version:
HTML5 jhvideo base on MPEG2-TS Stream Player
472 lines (446 loc) • 18.7 kB
text/typescript
import { fabric } from 'fabric'
import {EventEmitter} from 'events'
const finishIcon = "data:image/svg+xml;base64,PHN2Zw0KICAgICAgICB0PSIxNjY1OTc1ODY3NzczIg0KICAgICAgICBjbGFzcz0iaWNvbiINCiAgICAgICAgdmlld0JveD0iMCAwIDEwMjQgMTAyNCINCiAgICAgICAgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEwMzQ2IiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+DQogICAgPHBhdGggZD0iTTc3Mi4yNjY2NjcgMzU2LjI2NjY2N2wtMzU5LjY4IDM2MC45NmEzNC45ODY2NjcgMzQuOTg2NjY3IDAgMCAxLTguNTMzMzM0IDYuODI2NjY2IDM1Ljg0IDM1Ljg0IDAgMCAxLTQ5LjA2NjY2Ni0xMy42NTMzMzNsLTEwOS4yMjY2NjctMTkwLjI5MzMzM2EzNS44NCAzNS44NCAwIDAgMSA2Mi4yOTMzMzMtMzUuODRsODUuMzMzMzM0IDE0OS4zMzMzMzMgMzI3LjY4LTMyOC41MzMzMzNhMzUuODQgMzUuODQgMCAwIDEgNTEuMiA1MC43NzMzMzN6TTUxMiAwYTUxMiA1MTIgMCAxIDAgNTEyIDUxMkE1MTIgNTEyIDAgMCAwIDUxMiAweiINCiAgICAgICAgICBmaWxsPSIjZmYyYTAwIiBwLWlkPSIxMDM0NyI+DQogICAgPC9wYXRoPg0KPC9zdmc+DQo="
const rectangleDefault = [{x: 0.001, y: 0.001}, {x: 0.998, y: 0.001}, {x: 0.998, y: 0.998}, {x: 0.001, y: 0.998}]
// 矩形框绘制
export class rectDraw {
private cardID: string
private canvas: any
private fabricDom: HTMLCanvasElement
private rectangle: HTMLDivElement
private currentType: string
private downObject: any
private drawing = false
private proportion: any = []
private initialPoint: any = []
private style = {
color: 'rgba(255, 42, 0, .9)',
border: 'rgba(255, 42, 0, .4)',
strokeWidth: 2
}
emitter: any
// 确认图标
renderIcon(icon) {
return function (ctx, left, top, styleOverride, fabricObject) {
const size = this.cornerSize;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(icon, -size / 2, -size / 2, size, size);
ctx.restore();
}
}
constructor(videoDom, cardId, dom) {
const [rectangle, fabricDom] = dom
this.cardID = cardId
this.rectangle = rectangle
this.fabricDom = fabricDom
videoDom.appendChild(rectangle)
this.emitter = new EventEmitter()
/* const handleIcon = (eventData, transform) => {
this.emitter.emit('complete')
}
const finishImg = document.createElement('img')
finishImg.src = finishIcon
fabric.Object.prototype.controls.deleteControl = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -15,
offsetX: 15,
cursorStyle: 'pointer',
mouseUpHandler: handleIcon,
render: this.renderIcon(finishImg),
cornerSize: 18
}) */
}
on(event:any, listener:any) {
this.emitter.addListener(event, listener);
}
off(event:any, listener:any) {
this.emitter.removeListener(event, listener);
}
/*创建绘画/编辑绘画*/
async begin() {
this.drawing = true
this.rectangle.style.zIndex = '9'
this.drawType('polygon')
// 赋值初始值
this.initialPoint = [...this.proportion]
}
setDom(width, height) {
if (this.canvas) {
this.canvas.dispose()
this.canvas = undefined
}
this.fabricDom.width = width
this.fabricDom.height = height
this.canvas = new fabric.Canvas(this.cardID, {
uniformScaling: false,
preserveObjectStacking: true
})
this.setInfo()
}
setInfo() {
/*
const canvasMouseDown = (e) => {
this.downObject = e
this.downPoint = e.absolutePointer
}
const canvasMouseUp = (e) => {
if (this.currentType === 'rect') {
// 创建矩形
createRect(e.absolutePointer)
}
}
// 创建矩形
const createRect = (pointer) => {
const {downPoint, canvas, style} = this
// 矩形参数计算
const top = Math.min(downPoint.y, pointer.y)
const left = Math.min(downPoint.x, pointer.x)
const width = Math.abs(downPoint.x - pointer.x)
const height = Math.abs(downPoint.y - pointer.y)
// 矩形对象
const rect = new fabric.Rect({
top,
left,
width,
height,
fill: 'transparent',
strokeWidth: style.strokeWidth,
stroke: style.color,
scaleX: 1,
scaleY: 1,
strokeUniform: true, // 变形后保持宽度
objectCaching: false // 高速缓存,可在变形时减少虚化
})
// 将矩形添加到画布上
rect.setControlsVisibility({mtr: false}) // 取消旋转手柄
canvas.add(rect)
this.downPoint = null
if (canvas.getObjects().length > 0) {
this.drawType('select')
}
}
this.canvas.on('mouse:down', canvasMouseDown) // 鼠标在画布上按下
this.canvas.on('mouse:up', canvasMouseUp) // 鼠标在画布上松开
*/
// 双击事件
const canvasDlclick = (e) => {
const ePointer = e.pointer
const oCoords = e.target.oCoords
const polygons = e.target.points
const offsetX = oCoords.p0.x - polygons[0].x
const offsetY = oCoords.p0.y - polygons[0].y
const offsetPointer = {
x: ePointer.x - offsetX,
y: ePointer.y - offsetY
}
// 双击位置是否与点重叠
const index = polygons.reduce((prv, v, i) => {
if (v.x + 3 > offsetPointer.x && offsetPointer.x > v.x - 3 && v.y + 3 > offsetPointer.y && offsetPointer.y > v.y - 3) {
prv = i
}
return prv
}, -1)
// 添加~删除~点
if (index < 0) {
// 添加点
const integers = polygons.map(v => {
return {
x: ~~v.x,
y: ~~v.y
}
})
const operateObj = pointIsAdd(offsetPointer, integers)
if (operateObj) {
polygons.splice(operateObj.index, 0, offsetPointer)
this.drawType('polygon')
}
} else if (polygons.length > 3) {
// 删除点
polygons.splice(index, 1)
this.drawType('polygon')
}
// 判断是否有添加点
function pointIsAdd(point, segments) {
const mistake = []
for (let i = 1; i < segments.length + 1; i++) {
const ps = segments[i - 1]
const pe = i === segments.length ? segments[0] : segments[i]
const extent: number = distanceOfPointAndLine(ps.x, ps.y, pe.x, pe.y, point.x, point.y)
// 距离在2.5之内
if (extent < 2.5) {
mistake.push({
extent,
pStart: ps,
pEnd: pe,
index: i
})
}
}
if (mistake.length > 0) {
return mistake.reduce((prv, cur) => {
return prv.extent < cur.extent ? prv : cur
}, mistake[0])
}
return null
}
// 点到线段距离
function distanceOfPointAndLine(x1, y1, x2, y2, x, y) {
const A = x - x1;
const B = y - y1;
const C = x2 - x1;
const D = y2 - y1;
const dot = A * C + B * D;
const len_sq = C * C + D * D;
let param = -1;
if (len_sq != 0) { //线段长度不能为0
param = dot / len_sq;
}
let xx, yy;
if (param < 0) {
xx = x1;
yy = y1;
} else if (param > 1) {
xx = x2;
yy = y2;
} else {
xx = x1 + param * C;
yy = y1 + param * D;
}
const dx = x - xx;
const dy = y - yy;
return Math.sqrt(dx * dx + dy * dy);
}
}
this.canvas.on('mouse:dblclick', canvasDlclick)
this.rectangle.style.zIndex = '1'
// 有参数时自动绘制
if (this.proportion.length > 0) {
this.resetRect()
}
}
// 绘画多边形
resetRect() {
const {canvas, style, proportion} = this
const {clientHeight, clientWidth} = this.fabricDom
canvas.remove(canvas.getActiveObject())
const points = proportion.map(v => {
return {
x: v.x * clientWidth,
y: v.y * clientHeight
}
})
const polygon = new fabric.Polygon(points, {
fill: 'transparent',
strokeWidth: style.strokeWidth,
stroke: style.color,
scaleX: 1,
scaleY: 1,
objectCaching: false,
transparentCorners: false,
cornerColor: 'blue',
});
canvas.add(polygon);
/*const {canvas, proportion, style} = this
const {clientHeight, clientWidth} = this.fabricDom
const {x, y, width, height} = proportion
const rect = new fabric.Rect({
top: y * clientHeight,
left: x * clientWidth,
width: width * clientWidth,
height: height * clientHeight,
fill: 'transparent',
strokeWidth: style.strokeWidth,
stroke: style.color,
scaleX: 1,
scaleY: 1,
strokeUniform: true, // 变形后保持宽度
objectCaching: false // 高速缓存,可在变形时减少虚化
})
rect.setControlsVisibility({mtr: false})
canvas.add(rect)*/
this.drawType('')
}
drawType(type: string) {
this.currentType = type
const {canvas, style} = this
switch (type) {
case '':
canvas.selection = false
canvas.skipTargetFind = true
break
case 'select':
canvas.selection = true
canvas.selectionColor = 'rgba(255, 255, 255, 0.01)'
canvas.selectionBorderColor = 'rgba(255, 255, 255, 0.5)'
canvas.skipTargetFind = false // 允许选中
break
case 'rect':
canvas.selectionColor = 'transparent'
canvas.selectionBorderColor = style.border
canvas.skipTargetFind = true // 禁止选中
break
case 'polygon':
polygonEdit()
canvas.skipTargetFind = false
break
}
// 定义一个可以定位控件的函数.
// 该函数将用于绘图和交互.
function polygonPositionHandler(dim, finalMatrix, fabricObject) {
const x = (fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x),
y = (fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y);
return fabric.util.transformPoint(
{x: x, y: y},
fabric.util.multiplyTransformMatrices(
fabricObject.canvas.viewportTransform,
fabricObject.calcTransformMatrix()
)
);
}
function getObjectSizeWithStroke(object) {
const stroke = new fabric.Point(
object.strokeUniform ? 1 / object.scaleX : 1,
object.strokeUniform ? 1 / object.scaleY : 1
).multiply(object.strokeWidth);
return new fabric.Point(object.width + stroke.x, object.height + stroke.y);
}
// 定义一个函数,该函数将定义控件的作用
// 该函数将在每次鼠标移动时调用
// 单击并正在拖动
// 函数接收鼠标事件作为参数,当前transformat对象
// 以及画布坐标中的当前位置
// transform.target是对正在转换的当前对象的引用,
function actionHandler(eventData, transform, x, y) {
const polygon = transform.target,
currentControl = polygon.controls[polygon.__corner],
mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), 'center', 'center'),
polygonBaseSize = getObjectSizeWithStroke(polygon),
size = polygon._getTransformedDimensions(0, 0),
finalPointPosition = {
x: mouseLocalPosition.x * polygonBaseSize.x / size.x + polygon.pathOffset.x,
y: mouseLocalPosition.y * polygonBaseSize.y / size.y + polygon.pathOffset.y
};
polygon.points[currentControl.pointIndex] = finalPointPosition;
return true;
}
// 定义一个函数,该函数可以在我们更改其
// width/height/top/left.
function anchorWrapper(anchorIndex, fn) {
return function (eventData, transform, x, y) {
const fabricObject = transform.target,
absolutePoint = fabric.util.transformPoint({
x: (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x),
y: (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y),
}, fabricObject.calcTransformMatrix()),
actionPerformed = fn(eventData, transform, x, y),
newDim = fabricObject._setPositionDimensions({}),
polygonBaseSize = getObjectSizeWithStroke(fabricObject),
newX = (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x,
newY = (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y;
fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
return actionPerformed;
}
}
function polygonEdit() {
// 克隆自你以来你在复制什么
// 可能需要在不同时刻复制和粘贴.
// 你不希望发生变化
// 稍后再反思副本.
const poly = canvas.getObjects()[0];
canvas.setActiveObject(poly);
const lastControl = poly.points.length - 1;
poly.cornerStyle = 'circle';
poly.cornerColor = 'rgba(0,43,255,0.9)';
poly.cornerSize = 12
poly.controls = poly.points.reduce(function (acc, point, index) {
acc['p' + index] = new fabric.Control({
positionHandler: polygonPositionHandler,
actionHandler: anchorWrapper(index > 0 ? index - 1 : lastControl, actionHandler),
actionName: 'modifyPolygon',
pointIndex: index
});
return acc;
}, {});
poly.hasBorders = false;
canvas.requestRenderAll();
}
}
finish() {
this.drawing = false
this.rectangle.style.zIndex = '1'
const {canvas} = this
this.drawType('')
if (canvas.getObjects().length > 0) {
canvas.discardActiveObject().renderAll()
this.setRatio()
return true
} else {
this.proportion = rectangleDefault
return false
}
}
remove() {
const {canvas, downObject} = this
if (downObject) {
canvas.remove(downObject.target)
}
if (canvas.getObjects().length === 0) {
this.drawType('rect')
}
}
async setRatio() { // 计算参数
const data = await this.canvas.getObjects()
if (data.length > 0) {
const {clientHeight, clientWidth} = this.fabricDom
const arr = Object.keys(data[0].oCoords)
const oCoords = arr[0] === 'p0' ? data[0].oCoords : data[0].points
this.proportion = []
for (let field in oCoords) {
this.proportion.push({x: oCoords[field].x / clientWidth, y: oCoords[field].y / clientHeight})
}
}
}
getRatio() {
return this.proportion
}
setProportion(shape?:{x:any,y:any}[]) {
let isErr = false
if (shape && shape instanceof Array && shape.length > 2) {
// 判断数据格式是否正确
isErr = shape.reduce((per, item) => {
const isNan = parseFloat(item.x).toString() !== "NaN" && parseFloat(item.y).toString() !== "NaN"
if (isNan) {
if ((item.x < 0 && item.x > 1) || (item.y < 0 && item.y > 1)) {
per = false
}
} else {
per = false
}
return per
}, true)
}
this.proportion = isErr ? shape : rectangleDefault
if (!isErr && shape !== undefined) {
console.log('错误:绘制多边形数据或格式不正确')
}
}
// 取消绘制
async cancelRatio() {
this.proportion = [...this.initialPoint]
this.resetRect()
}
destroy() {
if (this.canvas) {
this.canvas.dispose()
this.canvas = undefined
}
this.proportion = []
if(this.emitter){
this.emitter.removeAllListeners()
this.emitter = null;
}
}
}