亚洲乱色熟女一区二区三区丝袜,天堂√中文最新版在线,亚洲精品乱码久久久久久蜜桃图片,香蕉久久久久久av成人,欧美丰满熟妇bbb久久久

LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

Canvas簡(jiǎn)歷編輯器-圖形繪制與狀態(tài)管理(輕量級(jí)DOM)

freeflydom
2024年8月7日 13:54 本文熱度 2700

在線編輯: https://windrunnermax.github.io/CanvasEditor

開源地址: https://github.com/WindrunnerMax/CanvasEditor

關(guān)于Canvas簡(jiǎn)歷編輯器項(xiàng)目的相關(guān)文章:

圖形繪制

我們做項(xiàng)目還是需要從需求出發(fā),首先我們需要明確我們要做的是簡(jiǎn)歷編輯器,那么簡(jiǎn)歷編輯器要求的圖形類型并不需要很多,只需要 矩形、圖片、富文本 圖形即可,那么我們就可以簡(jiǎn)單將其抽象一下,我們只需要認(rèn)為任何元素都是矩形就可以完成這件事了。

因?yàn)槔L制矩陣是比較簡(jiǎn)單的,我們可以直接從數(shù)據(jù)結(jié)構(gòu)來(lái)抽象這部分圖形,圖形元素基類的x, y, width, height屬性是確定的,再加上還有層級(jí)結(jié)構(gòu),那么就再加一個(gè)z,此外由于需要標(biāo)識(shí)圖形,所以還需要給其設(shè)置一個(gè)id。

class Delta {

  public readonly id: string;

  protected x: number;

  protected y: number;

  protected z: number;

  protected width: number;

  protected height: number;

}


那么我們的圖形肯定是有很多屬性的,例如矩形是會(huì)存在背景、邊框的大小和顏色,富文本也需要屬性來(lái)繪制具體的內(nèi)容,所以我們還需要一個(gè)對(duì)象來(lái)存儲(chǔ)內(nèi)容,而且我們是插件化的實(shí)現(xiàn),具體的圖形繪制應(yīng)該是由插件本身來(lái)實(shí)現(xiàn)的,這部分內(nèi)容需要子類來(lái)具體實(shí)現(xiàn)。

abstract class Delta {

  // ...

  public attrs: DeltaAttributes;

  public abstract drawing: (ctx: CanvasRenderingContext2D) => void;

}


那么繪制的時(shí)候,我們考慮分為兩層繪制的方式,內(nèi)層的Canvas是用來(lái)繪制具體圖形的,這里預(yù)計(jì)需要實(shí)現(xiàn)增量更新,而外層的Canvas是用來(lái)繪制中間狀態(tài)的,例如選中圖形、多選、調(diào)整圖形位置/大小等,在這里是會(huì)全量刷新的,并且后邊可能會(huì)在這里繪制標(biāo)尺。

 

在這里要注意一個(gè)很重要的問題,因?yàn)槲覀兊腃anvas并不是再是矢量圖形,如果我們是在1080P的顯示器上直接將編輯器的width x height設(shè)置到元素上,那是不會(huì)出什么問題的,但是如果此時(shí)是2K或者是4K的顯示器的話,就會(huì)出現(xiàn)模糊的問題,所以我們需要取得devicePixelRatio即物理像素/設(shè)備獨(dú)立像素,所以我們可以通過在window上取得這個(gè)值來(lái)控制Canvas元素的size屬性。

this.canvas.width = width * ratio;

this.canvas.height = height * ratio;

this.canvas.style.width = width + "px";

this.canvas.style.height = height + "px";


此時(shí)我們還需要處理resize的問題,我們可以使用resize-observer-polyfill來(lái)實(shí)現(xiàn)這部分功能,但是需要注意的是我們的width和height必須要是整數(shù),否則會(huì)導(dǎo)致編輯器的圖形模糊。

private onResizeBasic = (entries: ResizeObserverEntry[]) => {

  // COMPAT: `onResize`會(huì)觸發(fā)首次`render`

  const [entry] = entries;

  if (!entry) return void 0;

  // 置宏任務(wù)隊(duì)列

  setTimeout(() => {

    const { width, height } = entry.contentRect;

    this.width = width;

    this.height = height;

    this.reset();

    this.editor.event.trigger(EDITOR_EVENT.RESIZE, { width, height });

  }, 0);

};


實(shí)際上我們?cè)趯?shí)現(xiàn)完整的圖形編輯器的時(shí)候,可能并不是完整的矩形節(jié)點(diǎn),例如繪制云形狀的不規(guī)則圖形,我們需要將相關(guān)節(jié)點(diǎn)坐標(biāo)放置于attrs中,并且在實(shí)際繪制的過程中完成Bezier曲線的計(jì)算即可。但是實(shí)際上我們還需要注意到一個(gè)問題,當(dāng)我們點(diǎn)擊的時(shí)候如何判斷這個(gè)點(diǎn)是在圖形內(nèi)還是圖形外,如果是圖形內(nèi)則點(diǎn)擊時(shí)需要選中節(jié)點(diǎn),如果在圖形外不會(huì)選中節(jié)點(diǎn),那么因?yàn)槲覀兪情]合圖形,所以我們可以用射線法實(shí)現(xiàn)這個(gè)能力,我們將點(diǎn)向一個(gè)方向做射線,如果穿越的節(jié)點(diǎn)數(shù)量是奇數(shù),說明點(diǎn)在內(nèi)部圖形,如果穿越的節(jié)點(diǎn)數(shù)量是偶數(shù),則說明點(diǎn)在圖形外部。

我們僅僅實(shí)現(xiàn)圖形的繪制肯定是不行的,我們還需要實(shí)現(xiàn)圖形的相關(guān)交互能力。在實(shí)現(xiàn)交互的過程中我遇到了一個(gè)比較棘手的問題,因?yàn)椴淮嬖贒OM,所有的操作都是需要根據(jù)位置信息來(lái)計(jì)算的,比如選中圖形后調(diào)整大小的點(diǎn)就需要在選中狀態(tài)下并且點(diǎn)擊的位置恰好是那幾個(gè)點(diǎn)外加一定的偏移量,然后再根據(jù)MouseMove事件來(lái)調(diào)整圖形大小,而實(shí)際上在這里的交互會(huì)非常多,包括多選、拖拽框選、Hover效果,都是根據(jù)MouseDown、MouseMove、MouseUp三個(gè)事件完成的,所以如何管理狀態(tài)以及繪制UI交互就是個(gè)比較麻煩的問題,在這里我只能想到根據(jù)不同的狀態(tài)來(lái)攜帶不同的Payload,進(jìn)而繪制交互。

export enum CANVAS_OP {

  HOVER,

  RESIZE,

  TRANSLATE,

  FRAME_SELECT,

}

export enum CANVAS_STATE {

  OP = 10,

  HOVER = 11,

  RESIZE = 12,

  LANDING_POINT = 13,

  OP_RECT = 14,

}

export type SelectionState = {

  [CANVAS_STATE.OP]?:

    | CANVAS_OP.HOVER

    | CANVAS_OP.RESIZE

    | CANVAS_OP.TRANSLATE

    | CANVAS_OP.FRAME_SELECT

    | null;

  [CANVAS_STATE.HOVER]?: string | null;

  [CANVAS_STATE.RESIZE]?: RESIZE_TYPE | null;

  [CANVAS_STATE.LANDING_POINT]?: Point | null;

  [CANVAS_STATE.OP_RECT]?: Range | null;

};


狀態(tài)管理

在實(shí)現(xiàn)交互的時(shí)候,我思考了很久應(yīng)該如何比較好的實(shí)現(xiàn)這個(gè)能力,因?yàn)樯线呉舱f了這里是沒有DOM的,所以最開始的時(shí)候我通過MouseDown、MouseMove、MouseUp實(shí)現(xiàn)了一個(gè)非?;靵y的狀態(tài)管理,完全是基于事件的觸發(fā)然后執(zhí)行相關(guān)副作用從而調(diào)用Mask Canvas圖層的方法進(jìn)行重新繪制。

const point = this.editor.canvas.getState(CANVAS_STATE.LANDING_POINT);

const opType = this.editor.canvas.getState(CANVAS_STATE.OP);

// ...

this.editor.canvas.setState(CANVAS_STATE.HOVER, delta.id);

this.editor.canvas.setState(CANVAS_STATE.RESIZE, state);

this.editor.canvas.setState(CANVAS_STATE.OP, CANVAS_OP.RESIZE);

this.editor.canvas.setState(CANVAS_STATE.OP, CANVAS_OP.TRANSLATE);

this.editor.canvas.setState(CANVAS_STATE.OP, CANVAS_OP.FRAME_SELECT);

// ...

this.editor.canvas.setState(CANVAS_STATE.LANDING_POINT, new Point(e.offsetX, e.offsetY));

this.editor.canvas.setState(CANVAS_STATE.LANDING_POINT, null);

this.editor.canvas.setState(CANVAS_STATE.OP_RECT, null);

this.editor.canvas.setState(CANVAS_STATE.OP, null);

// ...


再后來(lái)我覺得這樣的代碼根本沒有辦法維護(hù),所以改動(dòng)了一下,將我所需要的狀態(tài)全部都存儲(chǔ)到一個(gè)Store中,通過我自定義的事件管理來(lái)通知狀態(tài)的改變,最終通過狀態(tài)改變的類型來(lái)嚴(yán)格控制將要繪制的內(nèi)容,也算是將相關(guān)的邏輯抽象了一層,只不過在這里相當(dāng)于是我維護(hù)了大量的狀態(tài),而且這些狀態(tài)是相互關(guān)聯(lián)的,所以會(huì)有很多的if/else去處理不同類型的狀態(tài)改變,而且因?yàn)楹芏喾椒〞?huì)比較復(fù)雜,傳遞了多層,導(dǎo)致狀態(tài)管理雖然比之前好了一些可以明確知道狀態(tài)是因?yàn)槟睦飳?dǎo)致變化的,但是實(shí)際上依舊不容易維護(hù)。

export const CANVAS_STATE = {

  OP: "OP",

  RECT: "RECT",

  HOVER: "HOVER",

  RESIZE: "RESIZE",

  LANDING: "LANDING",

} as const;


export type CanvasOp = keyof typeof CANVAS_OP;

export type ResizeType = keyof typeof RESIZE_TYPE;

export type CanvasStore = {

  [RESIZE_TYPE.L]?: Range | null;

  [RESIZE_TYPE.R]?: Range | null;

  [RESIZE_TYPE.T]?: Range | null;

  [RESIZE_TYPE.B]?: Range | null;

  [RESIZE_TYPE.LT]?: Range | null;

  [RESIZE_TYPE.RT]?: Range | null;

  [RESIZE_TYPE.LB]?: Range | null;

  [RESIZE_TYPE.RB]?: Range | null;

  [CANVAS_STATE.RECT]?: Range | null;

  [CANVAS_STATE.OP]?: CanvasOp | null;

  [CANVAS_STATE.HOVER]?: string | null;

  [CANVAS_STATE.LANDING]?: Point | null;

  [CANVAS_STATE.RESIZE]?: ResizeType | null;

};


最終我又思考了一下,我們?cè)跒g覽器中進(jìn)行DOM操作的時(shí)候,這個(gè)DOM是真正存在的嗎,或者說我們?cè)赑C上實(shí)現(xiàn)窗口管理的時(shí)候,這個(gè)窗口是真的存在的嗎,答案肯定是否定的,雖然我們可以通過系統(tǒng)或者瀏覽器提供的API來(lái)非常簡(jiǎn)單地實(shí)現(xiàn)各種操作,但是實(shí)際上些內(nèi)容是系統(tǒng)幫我們繪制出來(lái)的,本質(zhì)上還是圖形,事件、狀態(tài)、碰撞檢測(cè)等等都是系統(tǒng)模擬出來(lái)的,而我們的Canvas也擁有類似的圖形編程能力。

那么我們當(dāng)然可以在這里實(shí)現(xiàn)類似于DOM的能力,因?yàn)槲蚁雽?shí)現(xiàn)的能力似乎本質(zhì)上就是DOM與事件的關(guān)聯(lián),而DOM結(jié)構(gòu)是一種非常成熟的設(shè)計(jì)了,這其中有一些很棒的能力設(shè)計(jì),例如DOM的事件流,我們就不需要扁平化地調(diào)整每個(gè)Node的事件,而是只需要保證事件是從ROOT節(jié)點(diǎn)起始,最終又在ROOT上結(jié)束即可。并且整個(gè)樹形結(jié)構(gòu)以及狀態(tài)是靠用戶利用DOM的API來(lái)實(shí)現(xiàn)的,我們管理只需要處理ROOT就好了,這樣就會(huì)很方便,下個(gè)階段的狀態(tài)管理是準(zhǔn)備用這種方式來(lái)實(shí)現(xiàn)的,那么我們就先實(shí)現(xiàn)Node基類。

class Node {

  private _range: Range;

  private _parent: Node | null;

  public readonly children: Node[];


  // 盡可能簡(jiǎn)單地實(shí)現(xiàn)事件流

  // 直接通過`bubble`來(lái)決定捕獲/冒泡

  protected onMouseDown?: (event: MouseEvent) => void;

  protected onMouseUp?: (event: MouseEvent) => void;

  protected onMouseEnter?: (event: MouseEvent) => void;

  protected onMouseLeave?: (event: MouseEvent) => void;


  // `Canvas`繪制節(jié)點(diǎn)

  public drawingMask?: (ctx: CanvasRenderingContext2D) => void;


  constructor(range: Range) {

    this.children = [];

    this._range = range;

    this._parent = null;

  }


  // ====== Parent ======

  public get parent() {

    return this._parent;

  }

  public setParent(parent: Node | null) {

    this._parent = parent;

  }


  // ====== Range ======

  public get range() {

    return this._range;

  }

  public setRange(range: Range) {

    this._range = range;

  }


  // ====== DOM OP ======

  public append<T extends Node>(node: T | Empty) {

    // ...

  }

  public removeChild<T extends Node>(node: T | Empty) {

    // ...

  }

  public remove() {

    // ...

  }

  public clearNodes() {

    // ...

  }

}


那么接下來(lái)我們只需要定義好類似于HTML的Body元素,在這里我們將其設(shè)置為Root節(jié)點(diǎn),該元素繼承了Node節(jié)點(diǎn)。在這里我們接管了整個(gè)編輯器的事件分發(fā),繼承于此的事件都可以分發(fā)到子節(jié)點(diǎn),例如我們的點(diǎn)選事件,就可以在子節(jié)點(diǎn)上設(shè)置MouseDown事件處理即可。并且在這里我們還需要設(shè)計(jì)事件分發(fā)的能力,我們同樣可以實(shí)現(xiàn)事件的捕獲和冒泡機(jī)制,通過??梢院芊奖愕膶⑹录挠|發(fā)處理出來(lái)。

export class Root extends Node {

  constructor(private editor: Editor, private engine: Canvas) {

    super(Range.from(0, 0));

  }


  public getFlatNode(isEventCall = true): Node[] {

    // 非默認(rèn)狀態(tài)下不需要匹配

    if (!this.engine.isDefaultMode()) return [];

    // 事件調(diào)用實(shí)際順序 // 渲染順序則相反

    const flatNodes: Node[] = [...super.getFlatNode(), this];

    return isEventCall ? flatNodes.filter(node => !node.ignoreEvent) : flatNodes;

  }


  public onMouseDown = (e: MouseEvent) => {

    this.editor.canvas.mask.setCursorState(null);

    !e.shiftKey && this.editor.selection.clearActiveDeltas();

  };


  private emit<T extends keyof NodeEvent>(target: Node, type: T, event: NodeEvent[T]) {

    const stack: Node[] = [];

    let node: Node | null = target.parent;

    while (node) {

      stack.push(node);

      node = node.parent;

    }

    // 捕獲階段執(zhí)行的事件

    for (const node of stack.reverse()) {

      if (!event.capture) break;

      const eventFn = node[type as keyof NodeEvent];

      eventFn && eventFn(event);

    }

    // 節(jié)點(diǎn)本身 執(zhí)行即可

    const eventFn = target[type as keyof NodeEvent];

    eventFn && eventFn(event);

    // 冒泡階段執(zhí)行的事件

    for (const node of stack) {

      if (!event.bubble) break;

      const eventFn = node[type as keyof NodeEvent];

      eventFn && eventFn(event);

    }

  }


  private onMouseDownController = (e: globalThis.MouseEvent) => {

    this.cursor = Point.from(e, this.editor);

    // 非默認(rèn)狀態(tài)下不執(zhí)行事件

    if (!this.engine.isDefaultMode()) return void 0;

    // 按事件順序獲取節(jié)點(diǎn)

    const flatNode = this.getFlatNode();

    let hit: Node | null = null;

    const point = Point.from(e, this.editor);

    for (const node of flatNode) {

      if (node.range.include(point)) {

        hit = node;

        break;

      }

    }

    hit && this.emit(hit, NODE_EVENT.MOUSE_DOWN, MouseEvent.from(e, this.editor));

  };


  private onMouseMoveBasic = (e: globalThis.MouseEvent) => {

    this.cursor = Point.from(e, this.editor);

    // 非默認(rèn)狀態(tài)下不執(zhí)行事件

    if (!this.engine.isDefaultMode()) return void 0;

    // 按事件順序獲取節(jié)點(diǎn)

    const flatNode = this.getFlatNode();

    let next: ElementNode | ResizeNode | null = null;

    const point = Point.from(e, this.editor);

    for (const node of flatNode) {

      // 當(dāng)前只有`ElementNode`和`ResizeNode`需要觸發(fā)`Mouse Enter/Leave`事件

      const authorize = node instanceof ElementNode || node instanceof ResizeNode;

      if (authorize && node.range.include(point)) {

        next = node;

        break;

      }

    }

  };

  private onMouseMoveController = throttle(this.onMouseMoveBasic, ...THE_CONFIG);


  private onMouseUpController = (e: globalThis.MouseEvent) => {

    // 非默認(rèn)狀態(tài)下不執(zhí)行事件

    if (!this.engine.isDefaultMode()) return void 0;

    // 按事件順序獲取節(jié)點(diǎn)

    const flatNode = this.getFlatNode();

    let hit: Node | null = null;

    const point = Point.from(e, this.editor);

    for (const node of flatNode) {

      if (node.range.include(point)) {

        hit = node;

        break;

      }

    }

    hit && this.emit(hit, NODE_EVENT.MOUSE_UP, MouseEvent.from(e, this.editor));

  };

}


那么接下來(lái),我們只需要定義相關(guān)節(jié)點(diǎn)類型就可以了,并且通過區(qū)分不同類型就可以來(lái)實(shí)現(xiàn)不同的功能,例如圖形繪制使用ElementNode節(jié)點(diǎn),調(diào)整節(jié)點(diǎn)大小使用ResizeNode節(jié)點(diǎn),框選內(nèi)容使用FrameNode節(jié)點(diǎn)即可,那么在這里我們就先看一下ElementNode節(jié)點(diǎn),用來(lái)表示實(shí)際節(jié)點(diǎn)。

class ElementNode extends Node {

  private readonly id: string;

  private isHovering: boolean;


  constructor(private editor: Editor, state: DeltaState) {

    const range = state.toRange();

    super(range);

    this.id = state.id;

    const delta = state.toDelta();

    const rect = delta.getRect();

    this.setZ(rect.z);

    this.isHovering = false;

  }


  protected onMouseDown = (e: MouseEvent) => {

    if (e.shiftKey) {

      this.editor.selection.addActiveDelta(this.id);

    } else {

      this.editor.selection.setActiveDelta(this.id);

    }

  };


  protected onMouseEnter = () => {

    this.isHovering = true;

    if (this.editor.selection.has(this.id)) {

      return void 0;

    }

    this.editor.canvas.mask.drawingEffect(this.range);

  };


  protected onMouseLeave = () => {

    this.isHovering = false;

    if (!this.editor.selection.has(this.id)) {

      this.editor.canvas.mask.drawingEffect(this.range);

    }

  };


  public drawingMask = (ctx: CanvasRenderingContext2D) => {

    if (

      this.isHovering &&

      !this.editor.selection.has(this.id) &&

      !this.editor.state.get(EDITOR_STATE.MOUSE_DOWN)

    ) {

      const { x, y, width, height } = this.range.rect();

      Shape.rect(ctx, {

        x: x,

        y: y,

        width: width,

        height: height,

        borderColor: BLUE_3,

        borderWidth: 1,

      });

    }

  };

}


轉(zhuǎn)自https://www.cnblogs.com/WindrunnerMax/p/18346501 


該文章在 2024/8/8 8:41:46 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對(duì)中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國(guó)內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對(duì)港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場(chǎng)、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場(chǎng)作業(yè)而開發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉(cāng)儲(chǔ)管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購(gòu)管理,倉(cāng)儲(chǔ)管理,倉(cāng)庫(kù)管理,保質(zhì)期管理,貨位管理,庫(kù)位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號(hào)管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved