Skip to content

一、背景

Electron 可创建多个容器 (A 和 B), B 在 A 的上层,现在上层的 B(全屏且有背景颜色) 的某一区域,需要透露出A的内容。 为了实现这一诉求,我们找到一方案,通过CSS clip-path属性在 B 容器实现局部透明区域,使底部 A 内容自然透出。 课件 Demo 中 Frame 案例

二. 技术原理

2.1 CSS clip-path 基础

CSS clip-path 属性通过定义裁剪区域实现选择性内容展示:

  • polygon()函数:使用坐标点序列定义多边形裁剪区域

  • 闭合原则:自动连接首尾坐标点形成闭合区域

  • 混合坐标系:支持百分比相对坐标与像素绝对坐标混合使用

2.2 坐标系统

  • 坐标系基准:以目标元素左上角为原点(0,0)

  • 动态计算:通过getBoundingClientRect()实时获取元素物理坐标

  • 响应式支持:百分比值自动适应容器尺寸变化

2.3 裁剪原理

css
clip-path: polygon(x1 y1, x2 y2, x3 y3, ...);
  • 每个点由 x 和 y 坐标组成

  • 点按顺序连接形成多边形

  • 最后一个点会自动与第一个点连接,形成闭合区域

三. 实现方式

3.1 基础类实现

typescript
export class ClippyBase {
  public isDebug: boolean;


  // 获取裁剪路径
  public getPolygonFrameAll(config: { c1: IRect, c2?: IRect }) {
    if (!config.c2) {
      return this.getPolygonLeft(config.c1);
    }
    return this.frameLeftAndRight(config);
  }


  // 单区域裁剪
  private getPolygonLeft(c: IRect) {
    const { x, y, w, h } = c;
    const cx = x;
    const cy = y;
    const cx_cw = x + w;
    const cy_ch = y + h;


    // 计算关键点
    const a3 = a6 = a2 = a7 = `${cx + 1}px`;
    const b3 = b4 = `${cy + 1}px`;
    const a4 = a5 = `${cx_cw - 1}px`;
    const b6 = b5 = `${cy_ch - 2}px`;


    // 生成裁剪路径
    const result = [
      [0, 0],                // 左上角
      [0, '100%'],           // 左下角
      [a2, '100%'],          // 左下内角
      [a3, b3],              // 左边界点
      [a4, b4],              // 上边界点
      [a5, b5],              // 右边界点
      [a6, b6],              // 下边界点
      [a7, '100%'],          // 右下内角
      ['100%', '100%'],      // 右下角
      ['100%', 0],           // 右上角
    ];


    return this.getPolygon(result);
  }
  private frameLeftAndRight({ c1, c2 }: { c1: IRect, c2?: IRect }) {
    if (!c2) {
      return [];
    }
    const { x: x2, y: y2, w: w2, h: h2 } = c2;


    const singleFrame = this.frameLeft(c1);
    const last = singleFrame[singleFrame.length - 1];
    const rest = singleFrame.slice(0, -1);


    let b9, a10, b10, a11, b11, a12, b12, a13, b13, b14 = '';


    const cx = `${x2 + 1}px`;
    const cy = `${y2 + 1}px`;
    const cx_cw = `${x2 + w2 - 2}px`;
    const cy_ch = `${y2 + h2 - 2}px`;


    a10 = a11 = cx;
    b11 = b12 = cy;
    a12 = a13 = cx_cw;
    b9 = b10 = b13 = b14 = cy_ch;


    const result = [
      ...rest,
      ['100%', b9],
      [a10, b10],
      [a11, b11],
      [a12, b12],
      [a13, b13],
      ['100%', b14],
      last,
    ];
    return result;
  }


}

3.2 挖一个框实现方式

  • 已知所挖的框为 C1, 可计算出 C1 距离外层框的 x 轴、y 轴、和其本身的宽 w、 高 h

  • 按照如下 0 - 9个点位,可实现挖一个透底的框,点位坐标见下图右侧枚举

  • 具体实现方式见:getPolygonLeft 方法 图片

3.3 挖二个框实现方式

  • 已知所挖的框为 C1 和 C2 , 可计算出 C1、C2 距离外层框的 x 轴、y 轴、和其本身的宽 w、 高 h

  • 按照如下 0 - 15个点位,可实现挖二个透底的框,点位坐标见下图右侧枚举

  • 具体实现方式见:getPolygonLeft 方法 图片

四. 使用方法

4.1 基本使用

typescript
// 创建实例
const clippy = new ClippyBase();


// 定义裁剪区域
const rect = {
  x: 100,  // 左边界
  y: 100,  // 上边界
  w: 200,  // 宽度
  h: 150   // 高度
};


// 获取裁剪路径
const path = clippy.getPolygonFrameAll({ c1: rect });


// 应用裁剪
element.style.clipPath = path;

4.2 双区域裁剪

typescript
// 定义两个裁剪区域
const rect1 = { x: 100, y: 100, w: 200, h: 150 };
const rect2 = { x: 400, y: 100, w: 200, h: 150 };


// 获取双区域裁剪路径
const path = clippy.getPolygonFrameAll({ c1: rect1, c2: rect2 });


// 应用裁剪
element.style.clipPath = path;

4.3 实际应用示例

typescript
import { ClippyBase } from './base';
import { IClippyConfig, IRenderInfoFull, IRenderInfo, IClippyMain, IRect } from './interface';
import { isOpenD3D } from '@/common/gray';


class ClippyFactory extends ClippyBase {
  public readonly isOpen = isOpenD3D();
  public isFullScreen = false;
  public main: IClippyMain | null = null;
  public lecture: IRenderInfo | null = null;
  public lectureFull: IRenderInfoFull | null = null;
  public teacher: IRenderInfo | null = null;
  public hasTeacher = true;


  public getElement(id: string): HTMLElement | null {
    return document.getElementById(id);
  }


  public init(config: IClippyConfig) {
    const { main, lecture, hasTeacher, teacher } = config;
    this.hasTeacher = hasTeacher;
    this.log('初始化 init', config);


    const mainElement = this.getElement(main.id);
    if (!mainElement) {
      throw new Error(`Main element with id ${main.id} not found`);
    }


    this.main = {
      id: main.id,
      $ele: mainElement,
      clipPath: {
        default: '',
        full: '',
      },
    };


    this.lecture = this.setRenderInfo(lecture.id);


    this.lectureFull = {
      id: this.lecture.id,
      $ele: this.lecture.$ele,
      clipSize: { x: 0, y: 0, w: 0, h: 0 },
      size: { x: 0, y: 0, w: 0, h: 0 },
      isFullScreen: true,
    };






    if (hasTeacher && teacher && teacher.id) {
      this.teacher = this.setRenderInfo(teacher.id);
    }
    this.log(`画布 ${hasTeacher ? '有' : '无'} 老师区`, this.lecture, this.teacher);
    return {
      lecture: this.lecture,
      teacher: this.teacher,
    };
  }


  public setRenderInfo(id: string) {
    const $ele = document.getElementById(id) as any;
    const parentRect = this.main && this.main.$ele ? this.main.$ele.getBoundingClientRect() as any : { y: 0 };
    const { x, y: childRectY, width: w, height: h } = $ele.getBoundingClientRect();
    const ay = childRectY;
    const y = childRectY - parentRect.y;


    return {
      id,
      $ele,
      clipSize: { x, y, w, h },
      size: {x, y: ay, w, h},
    };
  }


  /** 打开 */
  public open() {
    if (!this.isOpen) {
      return;
    }
    if (this.isFullScreen) {
      return this.fullScreen();
    }
    return this.defaultScreen();
  }


  public change(config: { isFullScreen: boolean }): IRect | undefined {
    if (!this.isOpen) {
      return;
    }
    this.isFullScreen = config.isFullScreen;
    return this.open();
  }
  public close() {
    if (!this.isOpen) {
      return;
    }
    this.main && this.main.$ele && (this.main.$ele.style.clipPath = '');
  }
  private defaultScreen() {
    if (this.main && this.main.clipPath.default && this.lecture && this.lecture.size) {
      this.main.$ele.style.clipPath = this.main.clipPath.default;
      this.log('defaultScreen 使用 缓存');
      return this.lecture.size;
    }


    if (!this.lecture) {
      return;
    }
    if (!this.lecture.size.w && this.lecture.id) {
      this.lecture = this.setRenderInfo(this.lecture.id);
    }


    const polygon = this.getPolygonFrameAll(
      {
        c1: this.lecture.clipSize,
        ...(this.hasTeacher && this.teacher ? { c2: this.teacher.clipSize } : {}),
      },
    );


    this.log('默认尺寸', polygon);


    if (this.main) {
      this.main.clipPath.default = `polygon(${polygon})`;
      this.main.$ele.style.clipPath = `polygon(${polygon})`;
      return this.lecture.size;
    }
  }


  private fullScreen() {
    if (this.main && this.main.clipPath.full && this.lectureFull && this.lectureFull.size) {
      this.main.$ele.style.clipPath = this.main.clipPath.full;
      return this.lectureFull.size;
    }
    if (!this.lectureFull) {
      return;
    }


    this.lectureFull = {
      ...this.setRenderInfo(this.lectureFull.id),
      isFullScreen: true,
    };


    const polygon = this.getPolygonFrameAll(
      {
        c1: this.lectureFull.clipSize,
      },
    );
    this.log('全屏', polygon);


    if (this.main) {
      this.main.clipPath.full = `polygon(${polygon})`;
      this.main.$ele.style.clipPath = `polygon(${polygon})`;
      return this.lectureFull.size;
    }
  }
}




export const clippyFactory = new ClippyFactory();

Q、问题

问题一:挖洞的矩形,及底部视频窗体矩形都只能做到直角,但是用户看到的视频流,需要带圆角的

  • 解决方案: svg 绘制 圆角盖在最上层,详情可见

在 MIT 许可下发布