+
+
+
+
+
+
diff --git a/packages/image-extraction/LICENSE b/packages/image-extraction/LICENSE
new file mode 100644
index 0000000..9ace606
--- /dev/null
+++ b/packages/image-extraction/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 科学家丶
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/image-extraction/README.md b/packages/image-extraction/README.md
new file mode 100644
index 0000000..e2a2469
--- /dev/null
+++ b/packages/image-extraction/README.md
@@ -0,0 +1,29 @@
+
+
+
+
+# image-extraction
+
+> TODO:
+
+
+
+## Usage
+
+```
+yarn add @palxp/image-extraction
+
+import image-extraction from '@palxp/image-extraction'
+```
+
+## API
+
+[API Docs 链接](/#/docs)
+
+
diff --git a/packages/image-extraction/assets/eraser.png b/packages/image-extraction/assets/eraser.png
new file mode 100644
index 0000000..2402dcc
Binary files /dev/null and b/packages/image-extraction/assets/eraser.png differ
diff --git a/packages/image-extraction/composables/use-init-listeners.ts b/packages/image-extraction/composables/use-init-listeners.ts
new file mode 100644
index 0000000..184e1c1
--- /dev/null
+++ b/packages/image-extraction/composables/use-init-listeners.ts
@@ -0,0 +1,77 @@
+import { transformedDrawImage } from '../helpers/dom-helper'
+import { initDrawingListeners } from '../helpers/init-drawing-listeners'
+import { initDragListener, initScaleListener } from '../helpers/init-transform-listener'
+import { BoardRect } from '../types/common'
+import { ImageSources, MattingProps, UseInitListenersConfig } from '../types/init-matting'
+import { onBeforeUnmount, watch, watchEffect } from 'vue'
+
+export function useInitDrawingListeners(props: MattingProps, config: UseInitListenersConfig) {
+ const { radius, hardness, isErasing } = props
+ const { boardContexts, transformConfig, mattingSources, draggingInputBoard, initialized, boardRect, listenerManager } = config
+ const { inputCtx } = boardContexts
+ watchEffect(() => {
+ if (initialized.value) {
+ initDrawingListeners({
+ listenerManager,
+ imageSources: mattingSources.value as ImageSources,
+ boardContexts,
+ initDrawingConfig: { radius, hardness, transformConfig },
+ isErasing: isErasing.value,
+ draggingInputBoard: draggingInputBoard.value,
+ boardRect: boardRect.value as BoardRect,
+ })
+ }
+ })
+ onBeforeUnmount(() => {
+ listenerManager.removeMouseListeners((inputCtx.value as CanvasRenderingContext2D).canvas)
+ })
+}
+
+export function useInitTransformListener(config: UseInitListenersConfig) {
+ const { boardContexts, initialized, draggingInputBoard, transformConfig, isDrawing, listenerManager } = config
+ const { inputCtx, inputHiddenCtx, outputCtx, outputHiddenCtx } = boardContexts
+ watch(
+ [initialized, draggingInputBoard, isDrawing],
+ () => {
+ if (initialized.value && !isDrawing.value) {
+ const initConfig = {
+ inputContexts: { ctx: inputCtx.value as CanvasRenderingContext2D, hiddenCtx: inputHiddenCtx.value },
+ outputContexts: { ctx: outputCtx.value as CanvasRenderingContext2D, hiddenCtx: outputHiddenCtx.value },
+ draggingInputBoard: draggingInputBoard.value,
+ listenerManager,
+ transformConfig,
+ }
+ initDragListener(initConfig)
+ initScaleListener(initConfig)
+ // 触发重新绑定绘制监听器,必须输入画板拖动结束时才能重新绑定,否则绘制监听器会覆盖拖动监听器
+ if (!draggingInputBoard.value) {
+ transformConfig.positionRange = { ...transformConfig.positionRange }
+ }
+ }
+ },
+ { deep: true },
+ )
+ watch([transformConfig], async () => {
+ if (initialized.value) {
+ const { positionRange, scaleRatio } = transformConfig
+ const commonConfig = { positionRange, scaleRatio }
+ transformedDrawImage({
+ ctx: inputCtx.value as CanvasRenderingContext2D,
+ hiddenCtx: inputHiddenCtx.value,
+ ...commonConfig,
+ })
+ transformedDrawImage({
+ ctx: outputCtx.value as CanvasRenderingContext2D,
+ hiddenCtx: outputHiddenCtx.value,
+ withBorder: true,
+ ...commonConfig,
+ })
+ }
+ })
+ onBeforeUnmount(() => {
+ if (initialized.value) {
+ listenerManager.removeMouseListeners((outputCtx.value as CanvasRenderingContext2D).canvas)
+ listenerManager.removeWheelListeners()
+ }
+ })
+}
diff --git a/packages/image-extraction/composables/use-init-matting.ts b/packages/image-extraction/composables/use-init-matting.ts
new file mode 100644
index 0000000..6377797
--- /dev/null
+++ b/packages/image-extraction/composables/use-init-matting.ts
@@ -0,0 +1,62 @@
+import { EventType, UPDATE_BOARDRECT_DEBOUNCE_TIME } from '../constants'
+import { resizeCanvas } from '../helpers/dom-helper'
+import { computeBoardRect } from '../helpers/init-compute'
+import { initMatting } from '../helpers/init-matting'
+import { MattingProps, UseInitMattingBoardsConfig } from '../types/init-matting'
+import { debounce } from 'throttle-debounce'
+import { onMounted, onUnmounted, watch } from 'vue'
+
+export function useInitMattingBoards(props: MattingProps, useInitMattingBoardsConfig: UseInitMattingBoardsConfig) {
+ const { picFile } = props
+ const {
+ boardContexts,
+ boardContexts: { inputCtx, outputCtx, inputHiddenCtx, outputHiddenCtx },
+ } = useInitMattingBoardsConfig
+ const { initMattingResult, width, height, initialized } = useInitMattingBoardsConfig
+ const { boardRect, transformConfig, mattingSources } = useInitMattingBoardsConfig
+ const updateBoardRect = () => {
+ boardRect.value = computeBoardRect((inputCtx.value as CanvasRenderingContext2D).canvas)
+ }
+ const resizeBoards = () => {
+ requestAnimationFrame(() => {
+ const commonConfig = { targetHeight: height.value, targetWidth: width.value, transformConfig }
+ resizeCanvas({
+ ctx: inputCtx.value as CanvasRenderingContext2D,
+ hiddenCtx: inputHiddenCtx.value,
+ ...commonConfig,
+ })
+ resizeCanvas({
+ ctx: outputCtx.value as CanvasRenderingContext2D,
+ hiddenCtx: outputHiddenCtx.value,
+ withBorder: true,
+ ...commonConfig,
+ })
+ })
+ }
+ watch([picFile], async () => {
+ if (picFile.value && width.value && height.value) {
+ initialized.value = false
+ initMattingResult.value = await initMatting({
+ boardContexts,
+ picFile: picFile.value,
+ targetSize: { width: width.value, height: height.value },
+ transformConfig: {},
+ imageSources: {},
+ })
+ const { raw, mask, orig, positionRange, scaleRatio } = initMattingResult.value
+ transformConfig.positionRange = positionRange
+ transformConfig.scaleRatio = scaleRatio
+ mattingSources.value = { raw, mask, orig }
+ updateBoardRect()
+ resizeBoards()
+ initialized.value = true
+ }
+ })
+ onMounted(() => {
+ window.addEventListener(EventType.Resize, resizeBoards)
+ window.addEventListener('scroll', debounce(UPDATE_BOARDRECT_DEBOUNCE_TIME, updateBoardRect))
+ })
+ onUnmounted(() => {
+ window.removeEventListener(EventType.Resize, resizeBoards)
+ })
+}
diff --git a/packages/image-extraction/composables/use-matting-cursor.ts b/packages/image-extraction/composables/use-matting-cursor.ts
new file mode 100644
index 0000000..0a54759
--- /dev/null
+++ b/packages/image-extraction/composables/use-matting-cursor.ts
@@ -0,0 +1,111 @@
+import { ERASE_POINT_INNER_COLOR, ERASE_POINT_OUTER_COLOR, EventType, INITIAL_HARDNESS, INITIAL_RADIUS, REPAIR_POINT_INNER_COLOR, REPAIR_POINT_OUTER_COLOR } from '../constants'
+import { drawBrushPoint, getLoadedImage } from '../helpers/dom-helper'
+import { DrawingCircularConfig } from '../types/dom'
+import { computed, reactive, ref, Ref, UnwrapRef, watch, watchEffect } from 'vue'
+import iconEraser from '../assets/eraser.png'
+import { CursorStyle, UseCursorConfig } from '../types/cursor'
+
+export class MattingCursor {
+ ctx: CanvasRenderingContext2D
+ cursorImage: Ref = ref('')
+ inputCursorStyle: Ref = ref(null)
+ mattingCursorStyle: UnwrapRef = reactive(Object.create(null))
+ radius = ref(INITIAL_RADIUS)
+ hardness = ref(INITIAL_HARDNESS)
+
+ inputCanvas = computed(() => (this.inputCtx.value as CanvasRenderingContext2D).canvas as HTMLElement)
+ pointInnerColor = computed(() => (this.isErasing.value ? ERASE_POINT_INNER_COLOR : REPAIR_POINT_INNER_COLOR))
+ pointOuterColor = computed(() => (this.isErasing.value ? ERASE_POINT_OUTER_COLOR : REPAIR_POINT_OUTER_COLOR))
+
+ constructor(private inputCtx: Ref, private isErasing: Ref) {
+ this.ctx = this.creatCursorCanvas()
+ this
+ }
+
+ creatCursorCanvas() {
+ const ctx = document.createElement('canvas').getContext('2d') as CanvasRenderingContext2D
+ return this.updateCtx(ctx)
+ }
+
+ updateCtx(ctx: CanvasRenderingContext2D): CanvasRenderingContext2D {
+ ctx.canvas.width = this.radius.value * 2
+ ctx.canvas.height = this.radius.value * 2
+ return ctx
+ }
+
+ async createCursorImage() {
+ this.ctx = this.updateCtx(this.ctx)
+ const drawingConfig: DrawingCircularConfig = {
+ ctx: this.ctx as CanvasRenderingContext2D,
+ x: this.radius.value,
+ y: this.radius.value,
+ radius: this.radius.value,
+ hardness: this.hardness.value,
+ innerColor: this.pointInnerColor.value,
+ outerColor: this.pointOuterColor.value,
+ }
+ drawBrushPoint(drawingConfig)
+ await this.drawIcon()
+ return await this.ctx.canvas.toDataURL()
+ }
+
+ async drawIcon() {
+ if (this.isErasing.value) {
+ const eraser = await getLoadedImage(iconEraser)
+ this.ctx.drawImage(eraser, 0, 0, this.radius.value * 2, this.radius.value * 2)
+ }
+ }
+
+ renderOutputCursor() {
+ const target = this.inputCanvas.value
+ target.addEventListener(EventType.Mouseover, this.onShowCursor.bind(this))
+ target.addEventListener(EventType.Mousemove, this.onRenderOutputCursor.bind(this))
+ target.addEventListener(EventType.Mouseout, this.onHideCursor.bind(this))
+ }
+
+ onShowCursor() {
+ this.mattingCursorStyle.display = 'initial'
+ }
+
+ onHideCursor() {
+ this.mattingCursorStyle.display = 'none'
+ }
+
+ onRenderOutputCursor(e: MouseEvent) {
+ this.mattingCursorStyle.left = e.offsetX - this.radius.value + 'px'
+ this.mattingCursorStyle.top = e.offsetY - this.radius.value + 'px'
+ }
+
+ changeOutputCursorByDrag([isDragging]: boolean[]) {
+ if (isDragging) {
+ this.onHideCursor()
+ } else {
+ this.onShowCursor()
+ }
+ }
+
+ updateCursorParams(currHardness: number, currRadius: number) {
+ this.hardness.value = currHardness
+ this.radius.value = currRadius
+ }
+}
+
+export default function useMattingCursor(config: UseCursorConfig) {
+ const { inputCtx, isDragging, isErasing, hardness, radius } = config
+ const mattingCursor = new MattingCursor(inputCtx, isErasing)
+ const { cursorImage, mattingCursorStyle, renderOutputCursor } = mattingCursor
+
+ watchEffect(async () => {
+ mattingCursor.updateCursorParams(hardness.value, radius.value)
+ cursorImage.value = await mattingCursor.createCursorImage()
+ })
+
+ watch([isDragging], mattingCursor.changeOutputCursorByDrag.bind(mattingCursor))
+
+ return {
+ mattingCursor,
+ mattingCursorStyle,
+ cursorImage,
+ renderOutputCursor: renderOutputCursor.bind(mattingCursor),
+ }
+}
diff --git a/packages/image-extraction/composables/use-matting.ts b/packages/image-extraction/composables/use-matting.ts
new file mode 100644
index 0000000..abf5ff5
--- /dev/null
+++ b/packages/image-extraction/composables/use-matting.ts
@@ -0,0 +1,78 @@
+/*
+ * @Author: ShawnPhang
+ * @Date: 2023-10-05 16:33:07
+ * @Description:
+ * @LastEditors: ShawnPhang
+ * @LastEditTime: 2023-10-08 11:09:17
+ */
+import { INITIAL_RADIUS, INITIAL_HARDNESS, RADIUS_TO_BRUSH_SIZE_RATIO, HARDNESS_ZOOM_TO_SLIDER_RATIO, INITIAL_TRANSFORM_CONFIG } from '../constants'
+import { createContext2D } from '../helpers/dom-helper'
+import ListenerManager from '../helpers/listener-manager'
+import { BoardRect, TransformConfig } from '../types/common'
+import { ImageSources, InitMattingBaseConfig, InitMattingResult, MattingProps } from '../types/init-matting'
+import { ref, computed, Ref, reactive } from 'vue'
+import { useInitDrawingListeners, useInitTransformListener } from './use-init-listeners'
+import { useInitMattingBoards } from './use-init-matting'
+
+export function useMatting() {
+ const picFile = ref(null)
+ const isErasing = ref(false)
+ const radius = ref(INITIAL_RADIUS)
+ const hardness = ref(INITIAL_HARDNESS)
+ const brushSize = computed(() => radius.value * RADIUS_TO_BRUSH_SIZE_RATIO)
+ const hardnessText = computed(() => `${Math.round((hardness.value as number) * HARDNESS_ZOOM_TO_SLIDER_RATIO)}%`)
+
+ return {
+ picFile,
+ isErasing,
+ radius,
+ hardness,
+ brushSize,
+ hardnessText,
+ }
+}
+
+const inputDrawingCtx: CanvasRenderingContext2D = createContext2D()
+const outputDrawingCtx: CanvasRenderingContext2D = createContext2D()
+
+export function useMattingBoard(props: MattingProps) {
+ const width = ref(0)
+ const height = ref(0)
+ const inputCtx: Ref = ref(null)
+ const outputCtx: Ref = ref(null)
+ const initMattingResult: Ref = ref(null)
+ const draggingInputBoard = ref(false)
+ const isDrawing = ref(false)
+ const transformConfig: TransformConfig = reactive(INITIAL_TRANSFORM_CONFIG)
+ const mattingSources: Ref = ref(null)
+ const boardRect: Ref = ref(null)
+ const initialized = ref(false)
+ const inputHiddenCtx = ref(createContext2D())
+ const outputHiddenCtx = ref(createContext2D())
+ const listenerManager = new ListenerManager()
+ const initMattingConfig: InitMattingBaseConfig = {
+ boardContexts: { inputCtx, outputCtx, inputDrawingCtx, outputDrawingCtx, inputHiddenCtx, outputHiddenCtx },
+ initMattingResult,
+ transformConfig,
+ mattingSources,
+ initialized,
+ boardRect,
+ }
+ const initListenersConfig = { ...initMattingConfig, draggingInputBoard, isDrawing, listenerManager }
+ useInitMattingBoards(props, { ...initMattingConfig, width, height })
+ useInitDrawingListeners(props, initListenersConfig)
+ useInitTransformListener(initListenersConfig)
+ return {
+ width,
+ height,
+ inputCtx,
+ outputCtx,
+ inputHiddenCtx,
+ outputHiddenCtx,
+ draggingInputBoard,
+ transformConfig,
+ initialized,
+ mattingSources,
+ inputDrawingCtx,
+ }
+}
diff --git a/packages/image-extraction/constants/index.ts b/packages/image-extraction/constants/index.ts
new file mode 100644
index 0000000..779484a
--- /dev/null
+++ b/packages/image-extraction/constants/index.ts
@@ -0,0 +1,122 @@
+import { GapSize, RectSize, TransformConfig } from '../types/common'
+import { GLColor } from '../types/matting-drawing'
+
+export enum EventType {
+ Mouseover = 'mouseover',
+ Mouseenter = 'mouseenter',
+ Mouseout = 'mouseout',
+ Mouseleave = 'mouseleave',
+ Mouseup = 'mouseup',
+ Mousemove = 'mousemove',
+ MouseDown = 'mousedown',
+ DblClick = 'dblclick',
+ Click = 'click',
+ ContextMenu = 'contextmenu',
+ KeyDown = 'keydown',
+ Keyup = 'keyup',
+ Keypress = 'keypress',
+ Scroll = 'scroll',
+ Resize = 'resize',
+ Wheel = 'wheel',
+ UndoRedoStateChanged = 'undoRedoStateChanged',
+}
+
+export const INITIAL_RADIUS = 12.5
+export const INITIAL_HARDNESS = 0.5
+/** */
+export const RADIUS_TO_BRUSH_SIZE_RATIO = 4
+
+export const RADIUS_SLIDER_MIN = 0.25
+export const RADIUS_SLIDER_STEP = 0.25
+export const RADIUS_SLIDER_MAX = 25
+/** 画笔绘制点最小半径像素 */
+export const MIN_RADIUS = 0.5
+
+export const HARDNESS_SLIDER_MIN = 0
+export const HARDNESS_SLIDER_STEP = 0.01
+export const HARDNESS_SLIDER_MAX = 1
+/** 硬度放大到滑动条显示的值范围的放大倍数 */
+export const HARDNESS_ZOOM_TO_SLIDER_RATIO = 100
+
+export const INITIAL_SCALE_RATIO = 1
+/** 默认的变换配置对象 */
+export const INITIAL_TRANSFORM_CONFIG: TransformConfig = {
+ scaleRatio: INITIAL_SCALE_RATIO,
+ positionRange: {
+ minX: 0,
+ maxX: 0,
+ minY: 0,
+ maxY: 0,
+ },
+}
+/**
+ * 导航视窗区域内图片默认尺寸:以图片中心点为原点,进行等比例缩放
+ * 图片上下边距至少各留80px,左右边距至少留白40px,上下边距优先级高于左右边距
+ * 例如:当图片上下留白80px时,左右留白大于40px时,以上下留白80px为准
+ */
+export const INITIAL_GAP_SIZE: GapSize = {
+ horizontal: 40,
+ vertical: 80,
+}
+
+/** 隐藏画板的间隙对象——隐藏画板不需要留白 */
+export const HIDDEN_BOARD_GAP_SIZE: GapSize = {
+ horizontal: 0,
+ vertical: 0,
+}
+/** 隐藏画板的最大尺寸——默认情况下与图片原始尺寸一致,但不能超过2000px,超过2000px会进行缩放以免影响性能 */
+export const HIDDEN_BOARD_MAX_SIZE: RectSize = {
+ width: 2000,
+ height: 2000,
+}
+
+/** 默认的图像平滑选项值 */
+export const DEFAULT_IMAGE_SMOOTH_CHOICE = false
+export const IMAGE_BORDER_STYLE = '#000000'
+export const INITIAL_IMAGE_BORDER_WIDTH = 1
+
+export const DEFAULT_MASK_COLOR: GLColor = [0.47, 0.42, 0.9, 0.5]
+
+/** 窗口滚动时更新boardRect的节流等待时间 */
+export const UPDATE_BOARDRECT_DEBOUNCE_TIME = 100
+/** 计算stepBase(绘制补帧线条的迭代中的增量,基于真实尺寸的半径得到)的系数的倒数 */
+export const DRAWING_STEP_BASE_BASE = 20
+/** 计算绘制圆点的节流步长的系数的倒数 */
+export const DRAWING_STEP_BASE = 3.5
+
+export const GLOBAL_COMPOSITE_OPERATION_SOURCE_OVER = 'source-over'
+export const GLOBAL_COMPOSITE_OPERATION_DESTINATION_IN = 'destination-in'
+export const GLOBAL_COMPOSITE_OPERATION_DESTINATION_OUT = 'destination-out'
+
+/** 计算绘制补帧线条的节流步长的系数的倒数 */
+export const DRAW_INTERPOLATION_STEP_BASE = 2.5
+/** 绘制补帧线条的画笔半径阈值 */
+export const DRAW_INTERPOLATION_RADIUS_THRESHOLD = 1
+/** 径向渐变开始圆形的半径 */
+export const GRADIENT_INNER_RADIUS = 0
+/** 渐变开始的偏移值 */
+export const GRADIENT_BEGIN_OFFSET = 0
+/** 渐变结束的偏移值 */
+export const GRADIENT_END_OFFSET = 1
+/** 修补渐变开始的颜色 */
+export const REPAIR_POINT_INNER_COLOR = 'rgba(119,106,230,1)'
+/** 修补渐变结束的颜色 */
+export const REPAIR_POINT_OUTER_COLOR = 'rgba(119,106,230,0)'
+/** 擦除渐变开始的颜色 */
+export const ERASE_POINT_INNER_COLOR = 'rgba(255,255,255,1)'
+/** 擦除结束的颜色 */
+export const ERASE_POINT_OUTER_COLOR = 'rgba(255,255,255,0)'
+/** 0° */
+export const ZERO_DEGREES = 0
+/** 360° */
+export const ONE_TURN_DEGREES = Math.PI * 2
+/** 执行前进后退动作时的防抖时间 */
+export const ACTION_THROTTLE_TIME = 300
+/** 放大的系数 */
+export const ZOOM_IN_COEFFICIENT = 1
+/** 缩小的系数 */
+export const ZOOM_OUT_COEFFICIENT = -1
+/** 缩放比率变化的步长 */
+export const SCALE_STEP = 0.04
+export const MIN_SCALE_RATIO = 0.15
+export const MAX_SCALE_RATIO = 10
diff --git a/packages/image-extraction/env.d.ts b/packages/image-extraction/env.d.ts
new file mode 100644
index 0000000..d27eb5a
--- /dev/null
+++ b/packages/image-extraction/env.d.ts
@@ -0,0 +1,8 @@
+///
+
+declare module '*.vue' {
+ import { DefineComponent } from 'vue'
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/packages/image-extraction/helpers/dom-helper.ts b/packages/image-extraction/helpers/dom-helper.ts
new file mode 100644
index 0000000..a8353b2
--- /dev/null
+++ b/packages/image-extraction/helpers/dom-helper.ts
@@ -0,0 +1,186 @@
+import { DEFAULT_IMAGE_SMOOTH_CHOICE, GLOBAL_COMPOSITE_OPERATION_DESTINATION_IN, GRADIENT_BEGIN_OFFSET, GRADIENT_END_OFFSET, GRADIENT_INNER_RADIUS, IMAGE_BORDER_STYLE, INITIAL_IMAGE_BORDER_WIDTH, ONE_TURN_DEGREES, REPAIR_POINT_INNER_COLOR, REPAIR_POINT_OUTER_COLOR, ZERO_DEGREES } from '../constants'
+import { PositionRange } from '../types/common'
+import { CreateContext2DConfig, DrawImageLineBorderConfig, DrawingCircularConfig, GetImageSourceConfig, InitHiddenBoardConfig, InitHiddenBoardWithImageConfig, ResizeCanvasConfig, TransformedDrawingImageConfig } from '../types/dom'
+// import { isString } from 'lodash'
+
+function isString(value: string) {
+ return typeof value === 'string'
+}
+
+export function resizeCanvas(config: ResizeCanvasConfig) {
+ const { ctx, targetWidth, targetHeight, hiddenCtx, transformConfig, withBorder = false } = config
+ const { positionRange, scaleRatio } = transformConfig
+ ctx.canvas.width = targetWidth
+ ctx.canvas.height = targetHeight
+ ctx.imageSmoothingEnabled = DEFAULT_IMAGE_SMOOTH_CHOICE
+ transformedDrawImage({ ctx, hiddenCtx, positionRange, scaleRatio, withBorder, clearOld: false })
+}
+
+/** 创建2D绘制上下文 */
+export function createContext2D(createConfig: CreateContext2DConfig = {}): CanvasRenderingContext2D {
+ const { targetSize, cloneCanvas } = createConfig
+ const canvas: HTMLCanvasElement = document.createElement('canvas')
+ const context2D: CanvasRenderingContext2D = canvas.getContext('2d') as CanvasRenderingContext2D
+ if (targetSize) {
+ canvas.width = targetSize.width
+ canvas.height = targetSize.height
+ }
+ if (cloneCanvas) {
+ domHelpers.copyImageInCanvas(context2D, cloneCanvas)
+ }
+ return context2D
+}
+
+/** 复制画布中的图像 */
+function copyImageInCanvas(hiddenContext: CanvasRenderingContext2D, cloneCanvas: HTMLCanvasElement) {
+ hiddenContext.canvas.width = cloneCanvas.width
+ hiddenContext.canvas.height = cloneCanvas.height
+ hiddenContext.drawImage(cloneCanvas, 0, 0)
+}
+
+/** 隐藏各个画布 */
+export function hideCanvas(...ctxs: CanvasRenderingContext2D[]) {
+ for (const ctx of ctxs) {
+ ctx.canvas.style.display = 'none'
+ }
+}
+
+/** 显示各个画布 */
+export function showCanvas(...ctxs: CanvasRenderingContext2D[]) {
+ for (const ctx of ctxs) {
+ ctx.canvas.style.display = 'initial'
+ }
+}
+
+/** 获取指定链接下的位图图像 */
+export async function getLoadedImage(picFile: File | string): Promise {
+ const img = new Image()
+ img.crossOrigin = 'anonymous'
+ img.src = isString(picFile) ? picFile : URL.createObjectURL(picFile)
+ await new Promise((resolve) => {
+ img.onload = () => resolve()
+ })
+ return createImageBitmap(img)
+}
+
+export function initHiddenBoardWithSource(initConfig: InitHiddenBoardWithImageConfig) {
+ initHiddenBoard(initConfig)
+ const {
+ hiddenCtx: ctx,
+ imageSource,
+ targetSize: { width, height },
+ } = initConfig
+ return getImageSourceFromCtx({ ctx, imageSource, width, height })
+}
+
+/** 初始化隐藏的绘制画板和成果图画板 */
+export function initHiddenBoard(initConfig: InitHiddenBoardConfig): void {
+ const { targetSize, hiddenCtx, drawingCtx } = initConfig
+ const { width, height } = targetSize
+ hiddenCtx.canvas.width = width
+ hiddenCtx.canvas.height = height
+ drawingCtx.canvas.width = width
+ drawingCtx.canvas.height = height
+}
+
+/** 获取画布全屏绘制后的图像 */
+export function getImageSourceFromCtx(config: GetImageSourceConfig) {
+ const { ctx, imageSource, width, height } = config
+ ctx.drawImage(imageSource, 0, 0, width, height)
+ return createImageBitmap(ctx.canvas)
+}
+
+/** 清除画布中之前的内容 */
+function clearCanvas(ctx: CanvasRenderingContext2D) {
+ const {
+ canvas: { width, height },
+ } = ctx
+ ctx.clearRect(0, 0, width, height)
+}
+
+/** 绘制抠图成果图的线框 */
+function drawImageBorder(borderConfig: DrawImageLineBorderConfig) {
+ const { ctx, lineStyle, lineWidth, positionRange } = borderConfig
+ const { minY: top, maxX: right, maxY: bottom, minX: left } = positionRange
+ ctx.imageSmoothingEnabled = !DEFAULT_IMAGE_SMOOTH_CHOICE
+ ctx.fillStyle = lineStyle
+ ctx.fillRect(left, top, right - left + lineWidth, lineWidth)
+ ctx.fillRect(left, bottom, right - left + lineWidth, lineWidth)
+ ctx.fillRect(left, top + lineWidth, lineWidth, bottom - top - lineWidth)
+ ctx.fillRect(right, top + lineWidth, lineWidth, bottom - top - lineWidth)
+ ctx.imageSmoothingEnabled = DEFAULT_IMAGE_SMOOTH_CHOICE
+}
+
+/** 绘制画板上图像的边框 */
+function drawBoardImageBorder(ctx: CanvasRenderingContext2D, hiddenCtx: CanvasRenderingContext2D) {
+ const { width, height } = hiddenCtx.canvas
+ const positionRange: PositionRange = { minX: 0, minY: 0, maxX: width, maxY: height }
+ drawImageBorder({
+ ctx,
+ positionRange,
+ lineStyle: IMAGE_BORDER_STYLE,
+ lineWidth: INITIAL_IMAGE_BORDER_WIDTH,
+ })
+}
+
+/** 进行变换和绘制 */
+export function transformedDrawImage(transformedConfig: TransformedDrawingImageConfig) {
+ const { ctx, positionRange, scaleRatio, hiddenCtx } = transformedConfig
+ const { clearOld = true, withBorder } = transformedConfig
+ const { minX: translateX, minY: translateY } = positionRange
+ if (clearOld) {
+ clearCanvas(ctx)
+ }
+ ctx.save()
+ ctx.translate(translateX, translateY)
+ ctx.scale(scaleRatio, scaleRatio)
+ ctx.drawImage(hiddenCtx.canvas, 0, 0)
+ if (withBorder) {
+ drawBoardImageBorder(ctx, hiddenCtx)
+ }
+ ctx.restore()
+}
+
+/** 绘制擦补画笔圆点 */
+export function drawBrushPoint(drawingConfig: DrawingCircularConfig) {
+ const { ctx, x, y, radius, hardness } = drawingConfig
+ const { innerColor = REPAIR_POINT_INNER_COLOR, outerColor = REPAIR_POINT_OUTER_COLOR } = drawingConfig
+ ctx.beginPath()
+ const gradient = ctx.createRadialGradient(x, y, GRADIENT_INNER_RADIUS, x, y, radius)
+ gradient.addColorStop(GRADIENT_BEGIN_OFFSET, innerColor)
+ gradient.addColorStop(hardness, innerColor)
+ gradient.addColorStop(GRADIENT_END_OFFSET, outerColor)
+ ctx.fillStyle = gradient
+ ctx.arc(x, y, radius, ZERO_DEGREES, ONE_TURN_DEGREES)
+ ctx.fill()
+}
+
+/** 生成结果图像的URL */
+export function generateResultImageURL(rawImage: ImageBitmap, resultCtx: CanvasRenderingContext2D) {
+ const resultImageCtx = createResultImageContext2D(rawImage, resultCtx)
+ return resultImageCtx.canvas.toDataURL()
+}
+
+/** 创建绘制了原始尺寸结果图的绘制上下文 */
+function createResultImageContext2D(imageSource: ImageBitmap, resultImageCtx: CanvasRenderingContext2D): CanvasRenderingContext2D {
+ const context2D = createRawImageContext2D(imageSource)
+ drawResultImageInContext2D(context2D, resultImageCtx, imageSource)
+ return context2D
+}
+
+/** 创建绘制了原始尺寸图片的绘制上下文 */
+function createRawImageContext2D(imageSource: ImageBitmap): CanvasRenderingContext2D {
+ const context2D = createContext2D({ targetSize: imageSource })
+ context2D.drawImage(imageSource, 0, 0)
+ return context2D
+}
+
+/** 在传入的绘制上下文上绘制原始尺寸的结果图 */
+function drawResultImageInContext2D(ctx: CanvasRenderingContext2D, resultImageCtx: CanvasRenderingContext2D, imageSource: ImageBitmap): void {
+ ctx.globalCompositeOperation = GLOBAL_COMPOSITE_OPERATION_DESTINATION_IN
+ ctx.drawImage(resultImageCtx.canvas, 0, 0, imageSource.width, imageSource.height)
+}
+
+export const domHelpers = {
+ copyImageInCanvas,
+}
diff --git a/packages/image-extraction/helpers/drawing-compute.ts b/packages/image-extraction/helpers/drawing-compute.ts
new file mode 100644
index 0000000..acb45a1
--- /dev/null
+++ b/packages/image-extraction/helpers/drawing-compute.ts
@@ -0,0 +1,57 @@
+import { DRAW_INTERPOLATION_RADIUS_THRESHOLD, DRAW_INTERPOLATION_STEP_BASE } from '../constants'
+import { MouseMovements } from '../types/common'
+import { ComputedMovements, InImageRangeConfig, InterpolationStep, RenderInterpolationConfig } from '../types/drawing'
+
+const { sign, abs, max } = Math
+
+/** 判断坐标位置是否在绘制的图像的范围内 */
+export function isInImageRange(inRangeConfig: InImageRangeConfig): boolean {
+ const { x, y, minX, maxX, minY, maxY } = inRangeConfig
+ return x >= minX && x <= maxX && y >= minY && y <= maxY
+}
+
+/** 计算x/y轴方向移动距离及水平/垂直方向上的最大移动距离 */
+export function computeMovements(movements: MouseMovements): ComputedMovements {
+ const { movementX, movementY } = movements
+ const unsignedMovementX = abs(movementX)
+ const unsignedMovementY = abs(movementY)
+ const maxMovement = max(unsignedMovementX, unsignedMovementY)
+ return { unsignedMovementX, unsignedMovementY, maxMovement }
+}
+
+/** 是否需要插值渲染 */
+export function needDrawInterpolation(maxMovement: number, radius: number): boolean {
+ return radius > DRAW_INTERPOLATION_RADIUS_THRESHOLD && maxMovement > radius / DRAW_INTERPOLATION_STEP_BASE
+}
+
+/** 计算插值的步长 */
+export function computeInterpolationStep(interpolationConfig: RenderInterpolationConfig): InterpolationStep {
+ const { drawingConfig, unsignedMovementX, unsignedMovementY, maxMovement } = interpolationConfig
+ const { movementX, movementY, stepBase } = drawingConfig
+ const rawStepX = computePivotRawStep(movementX, stepBase)
+ const rawStepY = computePivotRawStep(movementY, stepBase)
+ const movementXIsMaximum = isMaxMovement(unsignedMovementX, maxMovement)
+ const stepX = computePivotStep(movementXIsMaximum, rawStepX, unsignedMovementX, unsignedMovementY)
+ const stepY = computePivotStep(!movementXIsMaximum, rawStepY, unsignedMovementY, unsignedMovementX)
+ return { stepX, stepY }
+}
+
+/** 计算x/y轴方向上朝上一次鼠标指针位置的插值步长 */
+function computePivotRawStep(pivotMovement: number, stepBase: number) {
+ return -sign(pivotMovement) * stepBase
+}
+
+/** 是否为最大移动距离 */
+function isMaxMovement(pivotMovement: number, maxMovement: number) {
+ return pivotMovement === maxMovement
+}
+
+/** 计算x/y轴方向的累加步长 */
+function computePivotStep(isMaxMovement: boolean, rawStepOfPivot: number, unsignedPivotMovement: number, unsignedCrossedMovement: number) {
+ return isMaxMovement ? rawStepOfPivot : (unsignedPivotMovement / unsignedCrossedMovement) * rawStepOfPivot
+}
+
+/** 判断是否需要绘制插值圆点 */
+export function needDrawInterpolationPoint(movement: number, moved: number, step: number) {
+ return movement - moved > step
+}
diff --git a/packages/image-extraction/helpers/drawing-helper.ts b/packages/image-extraction/helpers/drawing-helper.ts
new file mode 100644
index 0000000..4a54739
--- /dev/null
+++ b/packages/image-extraction/helpers/drawing-helper.ts
@@ -0,0 +1,105 @@
+/*
+ * @Author: ShawnPhang
+ * @Date: 2023-10-05 16:33:07
+ * @Description:
+ * @LastEditors: ShawnPhang
+ * @LastEditTime: 2023-10-08 11:09:55
+ */
+import { GLOBAL_COMPOSITE_OPERATION_DESTINATION_IN, GLOBAL_COMPOSITE_OPERATION_SOURCE_OVER } from '../constants'
+import { MattingDrawingConfig, RenderInterpolationConfig } from '../types/drawing'
+import { createContext2D, drawBrushPoint, transformedDrawImage } from './dom-helper'
+import { computeInterpolationStep, computeMovements, needDrawInterpolation, needDrawInterpolationPoint } from './drawing-compute'
+import { fixed } from './util'
+
+/** 批量执行抠图(修补/擦除)绘制 */
+export function executeMattingDrawing(drawingConfigs: MattingDrawingConfig[]) {
+ for (const config of drawingConfigs) {
+ const { radius } = config
+ const { maxMovement, unsignedMovementX, unsignedMovementY } = computeMovements(config)
+ if (needDrawInterpolation(maxMovement, radius)) {
+ renderMattingInterpolation({ drawingConfig: config, unsignedMovementX, unsignedMovementY, maxMovement })
+ } else {
+ drawMattingPoint(config)
+ }
+ drawResultArea(config)
+ }
+}
+
+/** 隐藏的辅助插值绘制的绘制上下文对象 */
+const interpolationCtx = createContext2D()
+
+/** 渲染插值图像区域 */
+function renderMattingInterpolation(interpolationConfig: RenderInterpolationConfig) {
+ const { drawingConfig, maxMovement } = interpolationConfig
+ const { step, stepBase, drawingCtx, radius, hardness } = drawingConfig
+ let { x, y } = drawingConfig
+ const { stepX, stepY } = computeInterpolationStep(interpolationConfig)
+ resetInterpolationCtx(drawingCtx)
+ for (let movement = 0, moved = movement; movement < maxMovement; movement += stepBase, x += stepX, y += stepY) {
+ if (needDrawInterpolationPoint(movement, moved, step)) {
+ moved = movement
+ drawBrushPoint({ ctx: interpolationCtx, x: fixed(x), y: fixed(y), radius, hardness })
+ }
+ }
+ drawMattingInterpolationTrack(drawingConfig)
+}
+
+/** 绘制插值轨迹 */
+function drawMattingInterpolationTrack(drawingConfig: MattingDrawingConfig) {
+ const { isErasing, hiddenCtx, drawingCtx } = drawingConfig
+ if (isErasing) {
+ hiddenCtx.value.drawImage(interpolationCtx.canvas, 0, 0)
+ } else {
+ drawMattingTrack(drawingConfig, () => {
+ drawingCtx.drawImage(interpolationCtx.canvas, 0, 0)
+ })
+ }
+}
+
+/** 重置用于插值绘制的画板 */
+function resetInterpolationCtx(drawingCtx: CanvasRenderingContext2D) {
+ const { width, height } = drawingCtx.canvas
+ interpolationCtx.canvas.width = width
+ interpolationCtx.canvas.height = height
+ interpolationCtx.clearRect(0, 0, width, height)
+}
+
+/** 绘制擦补/抠图区域的圆点 */
+function drawMattingPoint(drawingConfig: MattingDrawingConfig) {
+ const { isErasing, hiddenCtx, drawingCtx } = drawingConfig
+ const { x, y, radius, hardness } = drawingConfig
+ if (isErasing) {
+ drawBrushPoint({ ctx: hiddenCtx.value, x, y, radius, hardness })
+ } else {
+ drawMattingTrack(drawingConfig, () => {
+ drawBrushPoint({ ctx: drawingCtx, x, y, radius, hardness })
+ })
+ }
+}
+
+/** TODO: 绘制修补/扣除的轨迹 */
+async function drawMattingTrack(drawingConfig: MattingDrawingConfig, drawingCallback: VoidFunction) {
+ const { hiddenCtx, drawingCtx, mattingSource } = drawingConfig
+ drawingCtx.globalCompositeOperation = GLOBAL_COMPOSITE_OPERATION_SOURCE_OVER
+ drawingCtx.drawImage(mattingSource, 0, 0)
+ drawingCtx.globalCompositeOperation = GLOBAL_COMPOSITE_OPERATION_DESTINATION_IN
+ drawingCallback()
+ hiddenCtx.value.drawImage(drawingCtx.canvas, 0, 0)
+}
+
+/** 在呈现的画布上绘制图像 */
+function drawResultArea(drawingConfig: MattingDrawingConfig) {
+ const { ctx, hiddenCtx, positionRange, scaleRatio, isErasing } = drawingConfig
+ transformedDrawImage({
+ ctx: ctx.value as CanvasRenderingContext2D,
+ hiddenCtx: hiddenCtx.value,
+ positionRange,
+ scaleRatio,
+ withBorder: isInResultBoard(isErasing),
+ })
+}
+
+/** 绘制图像的画板是否为输出画板 */
+export function isInResultBoard(isErasing: boolean | undefined) {
+ return isErasing !== undefined
+}
diff --git a/packages/image-extraction/helpers/init-compute.ts b/packages/image-extraction/helpers/init-compute.ts
new file mode 100644
index 0000000..13cc8e2
--- /dev/null
+++ b/packages/image-extraction/helpers/init-compute.ts
@@ -0,0 +1,103 @@
+/*
+ * @Author: ShawnPhang
+ * @Date: 2023-10-05 16:33:07
+ * @Description:
+ * @LastEditors: ShawnPhang
+ * @LastEditTime: 2023-10-08 11:09:59
+ */
+import { HIDDEN_BOARD_GAP_SIZE, HIDDEN_BOARD_MAX_SIZE, INITIAL_SCALE_RATIO } from '../constants'
+import { BoardRect, GapSize, RectSize } from '../types/common'
+import { TransformParameters, TransformParametersConfig } from '../types/init-matting'
+import { computeScaledImageSize, fixed } from './util'
+
+/** 计算画板的左上角坐标及宽高 */
+export function computeBoardRect(canvas: HTMLCanvasElement): BoardRect {
+ const inputBoardRect: DOMRect = canvas.getBoundingClientRect()
+ const domRect: DOMRect = document.documentElement.getBoundingClientRect()
+ return computeBoardRectSize(inputBoardRect, domRect)
+}
+
+export function computeBoardRectSize(inputBoardRect: DOMRect, domRect: DOMRect) {
+ const { width, height, left: boardLeft, top: boardTop } = inputBoardRect
+ const { left: domLeft, top: domTop } = domRect
+ const left = boardLeft - domLeft
+ const top = boardTop - domTop
+ return { left, top, width, height }
+}
+
+/** 计合法的图片尺寸(低于2k分辨率的尺寸) */
+export function computeValidImageSize(imageSource: ImageBitmap): RectSize {
+ let { width, height } = imageSource
+ const imageScaleRatio = computeScaleRatio({
+ imageSize: { width, height },
+ gapSize: HIDDEN_BOARD_GAP_SIZE,
+ targetSize: HIDDEN_BOARD_MAX_SIZE,
+ })
+ width *= imageScaleRatio
+ height *= imageScaleRatio
+ return { width, height }
+}
+
+/** 计算自适应缩放比例 */
+export function computeScaleRatio(transformParametersConfig: TransformParametersConfig): number {
+ const { imageSize, gapSize, targetSize } = transformParametersConfig
+ const drawingAreaSize = getDrawingAreaSize(targetSize, gapSize)
+ return Math.min(Math.min(drawingAreaSize.width / imageSize.width, drawingAreaSize.height / imageSize.height), INITIAL_SCALE_RATIO)
+}
+
+/** 默认最大绘制区的尺寸(即画框尺寸减去间隙) */
+export function getDrawingAreaSize(boardSize: RectSize, gapSize: GapSize): RectSize {
+ return {
+ width: boardSize.width - gapSize.horizontal * 2,
+ height: boardSize.height - gapSize.vertical * 2,
+ }
+}
+
+/** 计算自适应变换(缩放、平移)参数 */
+export function computeTransformParameters(transformParametersConfig: TransformParametersConfig): TransformParameters {
+ const scaleRatio = computeScaleRatio(transformParametersConfig)
+ const positionRange = computePositionRange(transformParametersConfig, scaleRatio)
+ return { scaleRatio, positionRange }
+}
+
+/** 计算自适应变换后的绘制区域 */
+function computePositionRange(transformParametersConfig: TransformParametersConfig, scaleRatio: number) {
+ const scaledImageSize = computeScaledImageSize(transformParametersConfig.imageSize, scaleRatio)
+ return {
+ minX: getPositionRangeMinX(transformParametersConfig, scaledImageSize),
+ maxX: getPositionRangeMaxX(transformParametersConfig, scaledImageSize),
+ minY: getPositionRangeMinY(transformParametersConfig, scaledImageSize),
+ maxY: getPositionRangeMaxY(transformParametersConfig, scaledImageSize),
+ }
+}
+
+/** 计算绘制区域范围最小x坐标(相对于画布左上角) */
+function getPositionRangeMinX(transformParametersConfig: TransformParametersConfig, scaledImageSize: RectSize) {
+ const { gapSize, targetSize } = transformParametersConfig
+ return fixed((getDrawingAreaSize(targetSize, gapSize).width - scaledImageSize.width) / 2) + gapSize.horizontal
+}
+
+function getPositionRangeMinY(transformParametersConfig: TransformParametersConfig, scaledImageSize: RectSize) {
+ const { gapSize, targetSize } = transformParametersConfig
+ return fixed((getDrawingAreaSize(targetSize, gapSize).height - scaledImageSize.height) / 2) + gapSize.vertical
+}
+
+function getPositionRangeMaxX(transformParametersConfig: TransformParametersConfig, scaledImageSize: RectSize) {
+ return fixed(getPositionRangeMinX(transformParametersConfig, scaledImageSize) + scaledImageSize.width)
+}
+
+function getPositionRangeMaxY(transformParametersConfig: TransformParametersConfig, scaledImageSize: RectSize) {
+ return fixed(getPositionRangeMinY(transformParametersConfig, scaledImageSize) + scaledImageSize.height)
+}
+
+export const computeHelpers = {
+ computeBoardRect,
+ computeTransformParameters,
+ computeScaleRatio,
+ computeValidImageSize,
+ computePositionRange,
+ getPositionRangeMinX,
+ getPositionRangeMaxX,
+ getPositionRangeMinY,
+ getPositionRangeMaxY,
+}
diff --git a/packages/image-extraction/helpers/init-drawing-listeners.ts b/packages/image-extraction/helpers/init-drawing-listeners.ts
new file mode 100644
index 0000000..1582630
--- /dev/null
+++ b/packages/image-extraction/helpers/init-drawing-listeners.ts
@@ -0,0 +1,205 @@
+import { EventType, GLOBAL_COMPOSITE_OPERATION_DESTINATION_OUT, GLOBAL_COMPOSITE_OPERATION_SOURCE_OVER } from '../constants'
+import { MouseMovements, PixelPosition } from '../types/common'
+import { BoardDrawingConfig, BrushDrawingBaseConfig, CanDrawAndBindMouseListenerConfig, ComputePositionAndMovementConfig, ComputeRealPositionConfig, DrawingListenerConfig, InitDrawingConfig, InitDrawingListenerConfig, PositionAndMovements } from '../types/drawing-listeners'
+import { isInImageRange } from './drawing-compute'
+import { executeMattingDrawing } from './drawing-helper'
+import { transformHelpers, updateRangeByMovements } from './transform-helper'
+import { computeRealRadius, computeStep, computeStepBase, getRawDistance } from './util'
+
+export function initDrawingListeners(config: InitDrawingListenerConfig) {
+ const { listenerManager, initDrawingConfig } = config
+ const listenerConfig = generateDrawingListenerConfig(config)
+ let spaceDown = false
+ const {
+ inputBoardDrawingConfig: { ctx: inputCtx, hiddenCtx: inputHiddenCtx },
+ outputBoardDrawingConfig: { hiddenCtx: outputHiddenCtx },
+ brushDrawingBaseConfig: { positionRange },
+ } = listenerConfig
+ const { boardRect, draggingInputBoard } = listenerConfig
+ resetPivotalOptions(listenerConfig)
+ const drawingListener = generateDrawingListener(listenerConfig)
+ let canDrawAndBindListener = false
+ // --- TODO: 临时快捷键测试 ----
+ document.removeEventListener('keydown', handleKeydown, false)
+ function handleKeydown(e: any) {
+ if (e.code === 'Space') {
+ e.preventDefault()
+ }
+ spaceDown = e.code === 'Space'
+ }
+ document.addEventListener('keydown', handleKeydown, false)
+ function handleKeyup(e: any) {
+ spaceDown = false
+ }
+ document.removeEventListener('keydown', handleKeyup, false)
+ document.addEventListener('keyup', handleKeyup, false)
+ // --- END ---
+ listenerManager.initMouseListeners({
+ mouseTarget: (inputCtx.value as CanvasRenderingContext2D).canvas,
+ down(ev) {
+ if (!spaceDown) {
+ canDrawAndBindListener = canDrawAndBindMoveListener({
+ ev,
+ boardRect,
+ positionRange,
+ draggingInputBoard,
+ })
+ if (canDrawAndBindListener) {
+ drawingListener(ev)
+ }
+ return canDrawAndBindListener
+ }
+ },
+ move(ev) {
+ const { positionRange } = initDrawingConfig.transformConfig
+ spaceDown && updateRangeByMovements(ev, positionRange)
+ if (!draggingInputBoard && canDrawAndBindListener) {
+ drawingListener(ev)
+ }
+ },
+ up(ev) {
+ if (!draggingInputBoard && canDrawAndBindListener) {
+ canDrawAndBindListener = false
+ drawingListener(ev)
+ }
+ },
+ })
+}
+
+/** 生成绘制监听器的配置对象 */
+function generateDrawingListenerConfig(config: InitDrawingListenerConfig): DrawingListenerConfig {
+ const {
+ imageSources,
+ imageSources: { raw, mask },
+ initDrawingConfig,
+ boardContexts,
+ ...restConfig
+ } = config
+ const { inputCtx, inputHiddenCtx, inputDrawingCtx, outputCtx, outputHiddenCtx, outputDrawingCtx } = boardContexts
+ const brushDrawingBaseConfig: BrushDrawingBaseConfig = generateBrushBaseConfig(initDrawingConfig)
+ const inputBoardDrawingConfig: BoardDrawingConfig = {
+ ctx: inputCtx,
+ hiddenCtx: inputHiddenCtx,
+ drawingCtx: inputDrawingCtx,
+ mattingSource: mask,
+ }
+ const outputBoardDrawingConfig: BoardDrawingConfig = {
+ ctx: outputCtx,
+ hiddenCtx: outputHiddenCtx,
+ drawingCtx: outputDrawingCtx,
+ mattingSource: raw,
+ }
+ return {
+ brushDrawingBaseConfig,
+ mattingSources: imageSources,
+ inputBoardDrawingConfig,
+ outputBoardDrawingConfig,
+ ...restConfig,
+ }
+}
+
+/** 重置画板配置对象中关键的选项 */
+function resetPivotalOptions(config: DrawingListenerConfig) {
+ const { inputBoardDrawingConfig, outputBoardDrawingConfig } = config
+ const {
+ mattingSources: { raw, mask },
+ isErasing,
+ } = config
+ if (isErasing) {
+ inputBoardDrawingConfig.mattingSource = raw
+ outputBoardDrawingConfig.hiddenCtx.value.globalCompositeOperation = GLOBAL_COMPOSITE_OPERATION_DESTINATION_OUT
+ } else {
+ inputBoardDrawingConfig.mattingSource = mask
+ outputBoardDrawingConfig.hiddenCtx.value.globalCompositeOperation = GLOBAL_COMPOSITE_OPERATION_SOURCE_OVER
+ }
+}
+
+/** 生成画笔的基础配置对象 */
+function generateBrushBaseConfig(config: InitDrawingConfig): BrushDrawingBaseConfig {
+ const {
+ radius: rawRadius,
+ hardness,
+ transformConfig: { scaleRatio, positionRange },
+ } = config
+ const radius = computeRealRadius(rawRadius.value, scaleRatio)
+ const stepBase = computeStepBase(radius)
+ const step = computeStep(radius)
+ return { radius, step, stepBase, scaleRatio, positionRange, hardness: hardness.value }
+}
+
+/** 生成擦补画笔的绘制监听器 */
+function generateDrawingListener(config: DrawingListenerConfig) {
+ const {
+ brushDrawingBaseConfig,
+ brushDrawingBaseConfig: { step, scaleRatio, positionRange },
+ boardRect: { left, top },
+ } = config
+ const { inputBoardDrawingConfig, outputBoardDrawingConfig, isErasing } = config
+ let totalMovement = 0
+ return (ev: MouseEvent) => {
+ // TODO: 绘制
+ const positionAndMovements = computePositionAndMovements({
+ ev,
+ scaleRatio,
+ positionRange,
+ left,
+ top,
+ })
+ const { movementX, movementY } = positionAndMovements
+ const commonPointConfig = {
+ ...brushDrawingBaseConfig,
+ ...positionAndMovements,
+ }
+ totalMovement += getRawDistance(movementX, movementY)
+ if (canDrawing(totalMovement, step, ev.type)) {
+ totalMovement = 0
+ executeMattingDrawing([
+ { ...commonPointConfig, ...inputBoardDrawingConfig },
+ { ...commonPointConfig, ...outputBoardDrawingConfig, isErasing },
+ ])
+ }
+ }
+}
+
+/** 判断是否可以绘制 */
+function canDrawing(totalMovement: number, step: number, eventType: string): boolean {
+ return totalMovement >= step || eventType === EventType.MouseDown
+}
+
+/** 计算绘制点坐标位置及鼠标指针水平、垂直移动距离 */
+function computePositionAndMovements(config: ComputePositionAndMovementConfig): PositionAndMovements {
+ const { ev, scaleRatio, positionRange, left, top } = config
+ const { minX, minY } = positionRange
+ const { movementX, movementY, pageX, pageY } = ev
+ const realPosition = computeRealPosition({ pageX, pageY, left, top, minX, minY, scaleRatio })
+ const realMovements = computeRealMovements({ movementX, movementY }, scaleRatio)
+ return { ...realPosition, ...realMovements }
+}
+
+/** 计算相对于真实图像尺寸的鼠标位置 */
+function computeRealPosition(config: ComputeRealPositionConfig): PixelPosition {
+ const { pageX, pageY, left, top, minX, minY, scaleRatio } = config
+ const x = (pageX - left - minX) / scaleRatio
+ const y = (pageY - top - minY) / scaleRatio
+ return { x, y }
+}
+
+/** 计算相对于真实图像尺寸的移动距离 */
+function computeRealMovements(rawMovements: MouseMovements, scaleRatio: number) {
+ const { movementX: rawMovementX, movementY: rawMovementY } = rawMovements
+ const movementX = rawMovementX / scaleRatio
+ const movementY = rawMovementY / scaleRatio
+ return { movementX, movementY }
+}
+
+/** 判断是否可以绘制且绑定鼠标移动绘制的监听器 */
+function canDrawAndBindMoveListener(canDrawAndBindConfig: CanDrawAndBindMouseListenerConfig) {
+ const { ev, boardRect, positionRange, draggingInputBoard } = canDrawAndBindConfig
+ const { pageX, pageY } = ev
+ const { left, top } = boardRect
+ const { minX, maxX, minY, maxY } = positionRange
+ const x = transformHelpers.computePivot(pageX, left)
+ const y = transformHelpers.computePivot(pageY, top)
+ const inImageRange = isInImageRange({ x, y, minX, maxX, minY, maxY })
+ return inImageRange && !draggingInputBoard
+}
diff --git a/packages/image-extraction/helpers/init-matting.ts b/packages/image-extraction/helpers/init-matting.ts
new file mode 100644
index 0000000..31c3b56
--- /dev/null
+++ b/packages/image-extraction/helpers/init-matting.ts
@@ -0,0 +1,94 @@
+/*
+ * @Author: ShawnPhang
+ * @Date: 2023-10-05 16:33:07
+ * @Description:
+ * @LastEditors: ShawnPhang
+ * @LastEditTime: 2023-10-08 11:10:05
+ */
+import { DEFAULT_IMAGE_SMOOTH_CHOICE, INITIAL_GAP_SIZE } from '../constants'
+import { TransformConfig } from '../types/common'
+import { ComputeTransformConfigConfig, GenerateMaskSourceConfig, GetValidTransformParametersConfig, InitMattingConfig, InitMattingResult } from '../types/init-matting'
+import { getLoadedImage, initHiddenBoard, initHiddenBoardWithSource, transformedDrawImage } from './dom-helper'
+import { computeTransformParameters, computeValidImageSize } from './init-compute'
+import { initMaskRenderer } from './mask-renderer'
+
+export async function initMatting(initMattingConfig: InitMattingConfig): Promise {
+ const {
+ boardContexts: { inputCtx, outputCtx, inputHiddenCtx, inputDrawingCtx, outputHiddenCtx, outputDrawingCtx },
+ picFile,
+ transformConfig,
+ targetSize,
+ gapSize,
+ } = initMattingConfig
+ // hideCanvas(inputContext2D, outputContext2D);
+ ;(inputCtx.value as CanvasRenderingContext2D).imageSmoothingEnabled = DEFAULT_IMAGE_SMOOTH_CHOICE
+ ;(outputCtx.value as CanvasRenderingContext2D).imageSmoothingEnabled = DEFAULT_IMAGE_SMOOTH_CHOICE
+ const imageSource = await getLoadedImage(picFile)
+ const { scaleRatio, positionRange } = getValidTransformConfig({ imageSource, transformConfig, targetSize, gapSize })
+ const validImageSize = computeValidImageSize(imageSource)
+ initHiddenBoardWithSource({
+ imageSource,
+ targetSize: validImageSize,
+ hiddenCtx: inputHiddenCtx.value,
+ drawingCtx: inputDrawingCtx,
+ })
+ transformedDrawImage({
+ hiddenCtx: inputHiddenCtx.value,
+ ctx: inputCtx.value as CanvasRenderingContext2D,
+ scaleRatio,
+ positionRange,
+ })
+ initHiddenBoard({
+ targetSize: validImageSize,
+ hiddenCtx: outputHiddenCtx.value,
+ drawingCtx: outputDrawingCtx,
+ })
+
+ const raw = await createImageBitmap(inputHiddenCtx.value.canvas)
+ const mask = await generateMaskImageSource({ targetSize: validImageSize, imageSource })
+ return { orig: imageSource, raw, mask, positionRange, scaleRatio }
+}
+
+/** 生成蒙版后的图像资源 */
+function generateMaskImageSource(config: GenerateMaskSourceConfig): Promise {
+ const {
+ targetSize: { width, height },
+ imageSource,
+ } = config
+ const cvs = document.createElement('canvas')
+ cvs.width = width
+ cvs.height = height
+ const render = initMaskRenderer(cvs)
+ if (render) {
+ render(imageSource)
+ }
+ return createImageBitmap(cvs)
+}
+
+/** 获取有效的变换配置 */
+function getValidTransformConfig(getParametersConfig: GetValidTransformParametersConfig): TransformConfig {
+ const { transformConfig, ...computeConfig } = getParametersConfig
+ if (isInvalidTransformConfig(transformConfig)) {
+ const { scaleRatio, positionRange } = computeTransformConfig(computeConfig)
+ transformConfig.scaleRatio = scaleRatio
+ transformConfig.positionRange = positionRange
+ }
+ return transformConfig as TransformConfig
+}
+
+/** 判断变换配置是否无效 */
+function isInvalidTransformConfig(transformConfig: Partial) {
+ const { scaleRatio, positionRange } = transformConfig
+ return !scaleRatio || !positionRange
+}
+
+/** 计算画板的变换配置对象 */
+function computeTransformConfig(computeConfig: ComputeTransformConfigConfig): TransformConfig {
+ const { imageSource, targetSize, gapSize = INITIAL_GAP_SIZE } = computeConfig
+ const imageSize = computeValidImageSize(imageSource)
+ return computeTransformParameters({
+ gapSize,
+ imageSize,
+ targetSize,
+ })
+}
diff --git a/packages/image-extraction/helpers/init-transform-listener.ts b/packages/image-extraction/helpers/init-transform-listener.ts
new file mode 100644
index 0000000..b2185af
--- /dev/null
+++ b/packages/image-extraction/helpers/init-transform-listener.ts
@@ -0,0 +1,42 @@
+/*
+ * @Author: ShawnPhang
+ * @Date: 2023-10-05 16:33:07
+ * @Description:
+ * @LastEditors: ShawnPhang
+ * @LastEditTime: 2023-10-08 11:10:07
+ */
+import { InitMattingDragConfig, InitMattingScaleConfig } from '../types/transform'
+import { redrawMattingBoardsWhileScaling, updateRangeByMovements } from './transform-helper'
+
+/** 初始化画板变换的监听器 */
+export function initDragListener(mattingTransformConfig: InitMattingDragConfig) {
+ const {
+ outputContexts: { ctx: outputCtx2D },
+ // inputContexts: { ctx: inputCtx2D },
+ transformConfig,
+ listenerManager,
+ } = mattingTransformConfig
+ listenerManager.initMouseListeners({
+ mouseTarget: outputCtx2D.canvas,
+ move(ev) {
+ const { positionRange } = transformConfig
+ updateRangeByMovements(ev, positionRange)
+ },
+ })
+}
+
+/** 初始化缩放监听器 */
+export function initScaleListener(mattingTransformConfig: InitMattingScaleConfig): VoidFunction {
+ const {
+ inputContexts: { ctx: inputCtx },
+ outputContexts: { ctx: outputCtx },
+ listenerManager,
+ } = mattingTransformConfig
+ return listenerManager.initWheelListener({
+ mattingBoards: [inputCtx.canvas, outputCtx.canvas],
+ wheel(ev) {
+ ev.preventDefault()
+ redrawMattingBoardsWhileScaling(ev, mattingTransformConfig)
+ },
+ })
+}
diff --git a/packages/image-extraction/helpers/listener-manager.ts b/packages/image-extraction/helpers/listener-manager.ts
new file mode 100644
index 0000000..121162d
--- /dev/null
+++ b/packages/image-extraction/helpers/listener-manager.ts
@@ -0,0 +1,152 @@
+import { EventType } from '../constants'
+import { UnbindDownUpCache, UnbindMoveCache, MouseListenerContext, UnbindDownUpConfig, ListenerConfig, UnbindWheelCache, WheelListenerContext } from '../types/listener-manager'
+
+const { MouseDown, Mouseup, Mousemove } = EventType
+
+export default class ListenerManager {
+ private unbindDownUpCache: UnbindDownUpCache = new WeakMap()
+ private unbindMoveCache: UnbindMoveCache = new WeakMap()
+ private unbindWheelCache: UnbindWheelCache = new Set()
+
+ /** 初始化鼠标相关事件监听器 */
+ initMouseListeners(ctx: MouseListenerContext) {
+ const { mouseTarget } = ctx
+ this.removeMouseListeners(mouseTarget)
+ const unbindConfig = this.bindMouseListeners(ctx)
+ this.unbindDownUpCache.set(mouseTarget, unbindConfig)
+ }
+
+ removeMouseListeners(mouseTarget: HTMLElement) {
+ const unbindConfig = this.unbindDownUpCache.get(mouseTarget)
+ if (unbindConfig) {
+ const { unbindDown, unbindUp } = unbindConfig
+ unbindDown()
+ unbindUp()
+ }
+ }
+
+ private bindMouseListeners(listenersContext: MouseListenerContext): UnbindDownUpConfig {
+ const { mouseTarget, down, move, up } = listenersContext
+ const moveListener = (ev: Event) => {
+ requestAnimationFrame(() => move(ev as MouseEvent))
+ }
+ const downListener = (ev: Event) => {
+ const isTarget = ev.target === mouseTarget
+ const extraCondition = down && down(ev as MouseEvent)
+ const shouldBindMove = extraCondition !== false
+ if (isTarget && shouldBindMove) {
+ const removeMove = this.listenEvent({
+ eventType: Mousemove,
+ listener: moveListener,
+ stop: true,
+ prevent: true,
+ })
+ this.unbindMoveCache.set(mouseTarget, removeMove)
+ }
+ }
+ const upListener = (ev: Event) => {
+ up && up(ev as MouseEvent)
+ this.unbindMoveListeners(mouseTarget)
+ }
+ const unbindDown = this.listenEvent({
+ eventType: MouseDown,
+ listener: downListener,
+ })
+ const unbindUp = this.listenEvent({
+ eventType: Mouseup,
+ listener: upListener,
+ })
+ return { unbindDown, unbindUp }
+ }
+
+ /** 移除mousemove监听器 */
+ private unbindMoveListeners(mouseTarget: HTMLElement) {
+ const unbindMove = this.unbindMoveCache.get(mouseTarget)
+ unbindMove && unbindMove()
+ this.unbindMoveCache.delete(mouseTarget)
+ }
+
+ /** 初始化wheel事件监听器 */
+ initWheelListener(listenersConfig: WheelListenerContext): VoidFunction {
+ this.removeWheelListeners()
+ const removeWheel = this.bindWheelListener(listenersConfig)
+ this.unbindWheelCache.add(removeWheel)
+ return removeWheel
+ }
+
+ /** 解绑wheel事件监听器 */
+ removeWheelListeners() {
+ this.unbindWheelCache.forEach((unbind) => unbind())
+ this.unbindWheelCache.clear()
+ }
+
+ /** 绑定wheel事件监听器 */
+ private bindWheelListener(listenersConfig: WheelListenerContext) {
+ const { mattingBoards, wheel } = listenersConfig
+ return this.listenEvent(
+ {
+ eventType: EventType.Wheel,
+ listener: (ev) => {
+ if (this.canWheel(ev, mattingBoards)) {
+ wheel(ev as WheelEvent)
+ }
+ },
+ },
+ false,
+ ...mattingBoards,
+ )
+ }
+
+ /** 是否可以滚动 */
+ private canWheel(ev: Event, mattingBoards: HTMLCanvasElement[]): boolean {
+ return mattingBoards.some((board) => ev.target === board)
+ }
+
+ /** 监听事件,返回移除监听器的回调 */
+ private listenEvent(listenerConfig: ListenerConfig, options: boolean | AddEventListenerOptions = false, ...targets: HTMLElement[]): VoidFunction {
+ const { eventType } = listenerConfig
+ const wrapListener = this.genWrapListener(listenerConfig)
+ let removeListenerCallback: VoidFunction
+ if (!this.isNeedToBindToTargets(targets)) {
+ removeListenerCallback = this.bindListener(window, eventType, wrapListener, options)
+ } else {
+ removeListenerCallback = this.bindListeners(targets, eventType, wrapListener, options)
+ }
+ return removeListenerCallback
+ }
+
+ private genWrapListener(listenerConfig: ListenerConfig) {
+ const { listener, stop, prevent } = listenerConfig
+ return (ev: Event) => {
+ if (stop) {
+ ev.stopPropagation()
+ }
+ if (prevent) {
+ ev.preventDefault()
+ }
+ listener(ev)
+ }
+ }
+
+ /** 是否需要绑定在目标元素上 */
+ private isNeedToBindToTargets(targets: HTMLElement[]) {
+ return targets.length !== 0
+ }
+
+ /** 为单个目标绑定监听器 */
+ private bindListener(target: Window | HTMLElement, eventType: string, listener: EventListener, options: boolean | AddEventListenerOptions): VoidFunction {
+ target.addEventListener(eventType, listener, options)
+ return () => target.removeEventListener(eventType, listener, options)
+ }
+
+ /** 为多个目标绑定监听器 */
+ private bindListeners(targets: HTMLElement[], eventType: string, listener: EventListener, options: boolean | AddEventListenerOptions): VoidFunction {
+ targets.forEach((target) => {
+ target.addEventListener(eventType, listener, options)
+ })
+ return () =>
+ targets.forEach((target) => {
+ target.removeEventListener(eventType, listener, options)
+ })
+ }
+}
diff --git a/packages/image-extraction/helpers/mask-renderer.ts b/packages/image-extraction/helpers/mask-renderer.ts
new file mode 100644
index 0000000..9575876
--- /dev/null
+++ b/packages/image-extraction/helpers/mask-renderer.ts
@@ -0,0 +1,109 @@
+import { DEFAULT_MASK_COLOR } from '../constants'
+import { getWebGLContext, initShaders } from '../libs/cuon-utils'
+import { GLColor } from '../types/matting-drawing'
+
+const VSHADER_SOURCE = `
+attribute vec4 a_Position;
+attribute vec2 a_TexCoord;
+varying vec2 v_TexCoord;
+void main() {
+ gl_Position = a_Position;
+ v_TexCoord = a_TexCoord;
+}
+`
+const FSHADER_SOURCE = `
+precision highp float;
+uniform sampler2D u_Sampler;
+uniform vec4 u_MaskColor;
+varying vec2 v_TexCoord;
+void main() {
+ vec4 color = texture2D(u_Sampler, v_TexCoord);
+ vec3 mixRGB = color.a > 0.0 ? mix(color.rgb, u_MaskColor.rgb, u_MaskColor.a) : color.rgb;
+ gl_FragColor = vec4(mixRGB, color.a);
+}
+`
+
+export function initMaskRenderer(cvs: HTMLCanvasElement, maskColor: GLColor = DEFAULT_MASK_COLOR) {
+ const gl = getWebGLContext(cvs)
+ if (!gl) {
+ return console.error('获取WebGL绘制上下文失败!')
+ }
+ if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
+ return console.error('着色器初始化失败!')
+ }
+
+ if (!initBuffers(gl)) {
+ return console.error('缓冲区初始化失败!')
+ }
+
+ initUniform(gl, 'u_MaskColor', maskColor)
+
+ return (image: TexImageSource) => {
+ console.time('draw mask image')
+ if (!initTexture(gl, image)) {
+ return console.error('纹理初始化失败!')
+ }
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
+ console.timeEnd('draw mask image')
+ }
+}
+
+function initUniform(gl: WebGLRenderingContext, location: string, val: number | GLColor) {
+ const u_Location = gl.getUniformLocation(gl.program, location)
+ if (!u_Location) {
+ return console.error('获取attribute变量存储位置失败!')
+ }
+ if (Array.isArray(val)) {
+ gl.uniform4fv(u_Location, val)
+ } else {
+ gl.uniform1i(u_Location, val)
+ }
+}
+
+function initBuffers(gl: WebGLRenderingContext) {
+ const aPosition = gl.getAttribLocation(gl.program, 'a_Position')
+ const aTexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord')
+ if (!~aPosition || !~aTexCoord) {
+ console.error('获取attribute变量存储位置失败!')
+ return false
+ }
+ const verticesBuffer = gl.createBuffer()
+ const coordsBuffer = gl.createBuffer()
+ if (!verticesBuffer || !coordsBuffer) {
+ console.error('创建缓冲区对象失败!')
+ return false
+ }
+ bindArrayBuffer(gl, verticesBuffer, aPosition, new Float32Array([-1, 1, 1, 1, -1, -1, 1, -1]))
+ bindArrayBuffer(gl, coordsBuffer, aTexCoord, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]))
+ return true
+}
+
+function bindArrayBuffer(gl: WebGLRenderingContext, buffer: WebGLBuffer, attrib: number, data: Float32Array, pointNum = 2) {
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
+ gl.vertexAttribPointer(attrib, pointNum, gl.FLOAT, false, 0, 0)
+ gl.enableVertexAttribArray(attrib)
+}
+
+function initTexture(gl: WebGLRenderingContext, image: TexImageSource) {
+ const texture = gl.createTexture()
+ if (!texture) {
+ console.error('创建纹理对象失败!')
+ return false
+ }
+
+ const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler')
+ if (!u_Sampler) {
+ console.error('获取取样器变量存储位置失败!')
+ return false
+ }
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
+ gl.activeTexture(gl.TEXTURE0)
+ gl.bindTexture(gl.TEXTURE_2D, texture)
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
+ gl.uniform1i(u_Sampler, 0)
+ return true
+}
diff --git a/packages/image-extraction/helpers/transform-helper.ts b/packages/image-extraction/helpers/transform-helper.ts
new file mode 100644
index 0000000..6df6075
--- /dev/null
+++ b/packages/image-extraction/helpers/transform-helper.ts
@@ -0,0 +1,103 @@
+import { ZOOM_OUT_COEFFICIENT, ZOOM_IN_COEFFICIENT, SCALE_STEP, MIN_SCALE_RATIO, MAX_SCALE_RATIO } from '../constants'
+import { PixelPosition, PositionRange, TransformConfig } from '../types/common'
+import { GenerateRangeOffsetConfig, InitMattingTransformConfig } from '../types/transform'
+import { computeHelpers } from './init-compute'
+
+const { sign } = Math
+
+/** 生成表示绘制范围各个值偏移量的对象 */
+export function generateRangeOffset(rangeOffsetConfig: GenerateRangeOffsetConfig): PositionRange {
+ const { pageX, pageY, positionRange } = rangeOffsetConfig
+ const { minX, maxX, minY, maxY } = positionRange
+ return { minX: minX - pageX, maxX: maxX - pageX, minY: minY - pageY, maxY: maxY - pageY }
+}
+
+/** 根据当前鼠标位置更新绘制范围 */
+export function updateRangeByMovements(ev: MouseEvent, positionRange: PositionRange) {
+ const { movementX: deltaX, movementY: deltaY } = ev
+ positionRange.minX += deltaX
+ positionRange.maxX += deltaX
+ positionRange.minY += deltaY
+ positionRange.maxY += deltaY
+}
+
+/** 变换(平移、缩放)时重绘画板中图像 */
+export function redrawMattingBoardsWhileScaling(ev: WheelEvent, scalingConfig: InitMattingTransformConfig) {
+ const { transformConfig, inputContexts: inputContext, outputContexts: outputContext } = scalingConfig
+ updateTransformConfigWhileScaling(ev, transformConfig)
+}
+
+/** 鼠标滚轮滚动缩放时更新变换参数 */
+function updateTransformConfigWhileScaling(ev: WheelEvent, transformConfig: TransformConfig) {
+ const { deltaY, pageX, pageY, target } = ev
+ const { positionRange, scaleRatio } = transformConfig
+ const { left, top } = computeHelpers.computeBoardRect(target as HTMLCanvasElement)
+ const x = transformHelpers.computePivot(pageX, left)
+ const y = transformHelpers.computePivot(pageY, top)
+ const deltaRatio = transformHelpers.computeDeltaRatio(deltaY)
+ const targetScaleRatio = transformHelpers.computeNewScaleRatio(scaleRatio, deltaRatio)
+ transformConfig.scaleRatio = transformHelpers.computeClampedTargetScaleRatio(targetScaleRatio)
+ // 不能直接使用deltaRatio,scaleRatio接近最大/最小值时,二者就不相等了。
+ const rangeScaleRatio = computeRangeScaleRatio(transformConfig.scaleRatio, scaleRatio)
+ transformConfig.positionRange = transformHelpers.computeNewPositionRange(positionRange, { x, y }, rangeScaleRatio)
+}
+
+/** 计算鼠标的位置对应的像素在图像中的位置 */
+function computePivot(pagePivot: number, leftOrTop: number) {
+ return pagePivot - leftOrTop
+}
+
+/** 计算变化比率 */
+function computeDeltaRatio(deltaY: number) {
+ const scaleCoefficient = transformHelpers.isZoomOut(deltaY) ? ZOOM_OUT_COEFFICIENT : ZOOM_IN_COEFFICIENT
+ return scaleCoefficient * SCALE_STEP
+}
+
+/** 是否为缩小 */
+function isZoomOut(deltaY: number): boolean {
+ return -sign(deltaY) === ZOOM_OUT_COEFFICIENT
+}
+
+/** 计算新的缩放比率 */
+function computeNewScaleRatio(scaleRatio: number, deltaRatio: number): number {
+ return scaleRatio + scaleRatio * deltaRatio
+}
+
+/** 计算绘制范围的变化比率 */
+function computeRangeScaleRatio(newRatio: number, oldRatio: number): number {
+ return (newRatio - oldRatio) / oldRatio
+}
+
+/** 夹住缩放比例使其不会超出范围 */
+function computeClampedTargetScaleRatio(scaleRatio: number): number {
+ return scaleRatio < MIN_SCALE_RATIO ? MIN_SCALE_RATIO : scaleRatio > MAX_SCALE_RATIO ? MAX_SCALE_RATIO : scaleRatio
+}
+
+/** 计算新的绘制范围 */
+function computeNewPositionRange(positionRange: PositionRange, position: PixelPosition, deltaRatio: number): PositionRange {
+ const { x, y } = position
+ let { minX, maxX, minY, maxY } = positionRange
+ minX = transformHelpers.computeNewSingleRange(minX, x, deltaRatio)
+ maxX = transformHelpers.computeNewSingleRange(maxX, x, deltaRatio)
+ minY = transformHelpers.computeNewSingleRange(minY, y, deltaRatio)
+ maxY = transformHelpers.computeNewSingleRange(maxY, y, deltaRatio)
+ return { minX, maxX, minY, maxY }
+}
+
+/** 计算缩放后x/y轴方向的新的绘制范围值 */
+function computeNewSingleRange(singleRange: number, pivot: number, deltaRatio: number): number {
+ const vectorDistance = singleRange - pivot
+ const deltaRange = vectorDistance * deltaRatio
+ return singleRange + deltaRange
+}
+
+export const transformHelpers = {
+ updateTransformConfigWhileScaling,
+ computePivot,
+ computeDeltaRatio,
+ isZoomOut,
+ computeNewScaleRatio,
+ computeClampedTargetScaleRatio,
+ computeNewPositionRange,
+ computeNewSingleRange,
+}
diff --git a/packages/image-extraction/helpers/util.ts b/packages/image-extraction/helpers/util.ts
new file mode 100644
index 0000000..39323a3
--- /dev/null
+++ b/packages/image-extraction/helpers/util.ts
@@ -0,0 +1,51 @@
+/*
+ * @Author: ShawnPhang
+ * @Date: 2023-10-05 16:33:07
+ * @Description:
+ * @LastEditors: ShawnPhang
+ * @LastEditTime: 2023-10-08 11:10:19
+ */
+import { DRAWING_STEP_BASE, DRAWING_STEP_BASE_BASE, MIN_RADIUS } from '../constants'
+import { RectSize, TransformConfig } from '../types/common'
+
+const { sqrt, max } = Math
+
+export function fixed(num: number): number {
+ return num | 0
+}
+// 比Math.hypot(x,y)快一些(在数量级较大的情况下)
+export function getRawDistance(xDistance: number, yDistance: number): number {
+ return sqrt(xDistance ** 2 + yDistance ** 2)
+}
+
+/** 计算插值绘制的间隔步长 */
+export function computeStepBase(radius: number) {
+ return radius / DRAWING_STEP_BASE_BASE
+}
+
+/** 计算真实(相对真实,如果图像分辨率会控制在2K以内以保证性能)尺寸的画笔绘制点的半径 */
+export function computeRealRadius(rawRadius: number, scaleRatio: number) {
+ return max(MIN_RADIUS, rawRadius) / scaleRatio
+}
+
+/** 计算移动绘制的节流步长 */
+export function computeStep(radius: number) {
+ return radius / DRAWING_STEP_BASE
+}
+
+/** 基于新的缩放比例计算新的绘制范围 */
+export function computeNewTransformConfigByScaleRatio(transformConfig: TransformConfig, pictureSize: RectSize, scaleRatio: number): TransformConfig {
+ const { minX, minY } = transformConfig.positionRange
+ const { width, height } = pictureSize
+ const maxX = minX + width * scaleRatio
+ const maxY = minY + height * scaleRatio
+ return { positionRange: { minX, maxX, minY, maxY }, scaleRatio }
+}
+
+/** 获取图片缩放到画框区域内的实际尺寸 */
+export function computeScaledImageSize(imageSize: RectSize, scaleRatio: number): RectSize {
+ return {
+ width: imageSize.width * scaleRatio,
+ height: imageSize.height * scaleRatio,
+ }
+}
diff --git a/packages/image-extraction/index.ts b/packages/image-extraction/index.ts
new file mode 100644
index 0000000..db1c420
--- /dev/null
+++ b/packages/image-extraction/index.ts
@@ -0,0 +1,41 @@
+import { App } from 'vue'
+import matting from './ImageExtraction.vue'
+
+matting.install = (app: App): void => {
+ app.component(matting.name, matting)
+}
+
+export default matting
+
+export interface MattingType {
+ value: {}
+ /** 是否为擦除画笔 */
+ isErasing: boolean
+ /** 下载结果图 */
+ onDownloadResult: Function
+ /** 返回结果图 */
+ getResult: Function
+ /** input表单选择文件的回调 */
+ onFileChange: Function
+ /**
+ * 初始化加载的图片,第一个参数为原始图像,第二个参数为裁剪图像
+ */
+ initLoadImages: Function
+ /** 画笔尺寸 */
+ radius: number | string
+ /** 画笔尺寸:计算属性,显示值 */
+ brushSize: any
+ /** 画笔硬度 */
+ hardness: number | string
+ /** 画笔硬度:计算属性,显示值 */
+ hardnessText: any
+ /** 常量 */
+ constants: {
+ RADIUS_SLIDER_MIN: number
+ RADIUS_SLIDER_MAX: number
+ RADIUS_SLIDER_STEP: number
+ HARDNESS_SLIDER_MAX: number
+ HARDNESS_SLIDER_STEP: number
+ HARDNESS_SLIDER_MIN: number
+ }
+}
diff --git a/packages/image-extraction/libs/cuon-utils.ts b/packages/image-extraction/libs/cuon-utils.ts
new file mode 100644
index 0000000..986069f
--- /dev/null
+++ b/packages/image-extraction/libs/cuon-utils.ts
@@ -0,0 +1,137 @@
+// cuon-utils.js (c) 2012 kanda and matsuda
+
+import { WebGLDebugUtils } from './webgl-debug';
+import { WebGLUtils } from './webgl-utils';
+
+/**
+ * Create a program object and make current
+ * @param gl GL context
+ * @param vshader a vertex shader program (string)
+ * @param fshader a fragment shader program (string)
+ * @return true, if the program object was created and successfully made current
+ */
+declare global {
+ interface WebGLRenderingContext {
+ program: WebGLProgram;
+ }
+}
+export function initShaders(
+ gl: WebGLRenderingContext,
+ vshader: string,
+ fshader: string
+) {
+ const program = createProgram(gl, vshader, fshader);
+ if (!program) {
+ console.error('Failed to create program');
+ return false;
+ }
+
+ gl.useProgram(program);
+ gl.program = program;
+
+ return true;
+}
+
+/**
+ * Create the linked program object
+ * @param gl GL context
+ * @param vshader a vertex shader program (string)
+ * @param fshader a fragment shader program (string)
+ * @return created program object, or null if the creation has failed
+ */
+export function createProgram(
+ gl: WebGLRenderingContext,
+ vshader: string,
+ fshader: string
+): WebGLProgram | null {
+ // Create shader object
+ const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
+ const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
+ if (!vertexShader || !fragmentShader) {
+ return null;
+ }
+
+ // Create a program object
+ const program = gl.createProgram();
+ if (!program) {
+ return null;
+ }
+
+ // Attach the shader objects
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+
+ // Link the program object
+ gl.linkProgram(program);
+
+ // Check the result of linking
+ const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
+ if (!linked) {
+ const error = gl.getProgramInfoLog(program);
+ console.error('Failed to link program: ' + error);
+ gl.deleteProgram(program);
+ gl.deleteShader(fragmentShader);
+ gl.deleteShader(vertexShader);
+ return null;
+ }
+ return program;
+}
+
+/**
+ * Create a shader object
+ * @param gl GL context
+ * @param type the type of the shader object to be created
+ * @param source shader program (string)
+ * @return created shader object, or null if the creation has failed.
+ */
+export function loadShader(
+ gl: WebGLRenderingContext,
+ type: number,
+ source: string
+) {
+ // Create shader object
+ const shader = gl.createShader(type);
+ if (shader == null) {
+ console.error('unable to create shader');
+ return null;
+ }
+
+ // Set the shader program
+ gl.shaderSource(shader, source);
+
+ // Compile the shader
+ gl.compileShader(shader);
+
+ // Check the result of compilation
+ const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
+ if (!compiled) {
+ const error = gl.getShaderInfoLog(shader);
+ console.error('Failed to compile shader: ' + error, source);
+ gl.deleteShader(shader);
+ return null;
+ }
+
+ return shader;
+}
+
+/**
+ * Initialize and get the rendering for WebGL
+ * @param canvas element
+ * @param opt_debug flag to initialize the context for debugging
+ * @return the rendering context for WebGL
+ */
+export function getWebGLContext(
+ canvas: HTMLCanvasElement,
+ opt_debug?: boolean
+) {
+ // Get the rendering context for WebGL
+ let gl = WebGLUtils.setupWebGL(canvas);
+ if (!gl) return null;
+
+ // if opt_debug is explicitly false, create the context for debugging
+ if (arguments.length < 2 || opt_debug) {
+ gl = WebGLDebugUtils.makeDebugContext(gl) as WebGLRenderingContext;
+ }
+
+ return gl;
+}
diff --git a/packages/image-extraction/libs/webgl-debug.ts b/packages/image-extraction/libs/webgl-debug.ts
new file mode 100644
index 0000000..aad4a7e
--- /dev/null
+++ b/packages/image-extraction/libs/webgl-debug.ts
@@ -0,0 +1,708 @@
+//Copyright (c) 2009 The Chromium Authors. All rights reserved.
+//Use of this source code is governed by a BSD-style license that can be
+//found in the LICENSE file.
+
+// Various functions for helping debug WebGL apps.
+
+type FunctionKeys = {
+ [P in keyof T]: T[P] extends Function ? P : never;
+}[keyof T];
+type DebugContextProp = { [x: number]: boolean };
+type WebGLContextFuncKey = FunctionKeys;
+type GLValidEnumContext = {
+ [prop in WebGLContextFuncKey]?: DebugContextProp;
+};
+type GLPartialContext = {
+ -readonly [prop in keyof WebGLRenderingContext]?: WebGLRenderingContext[prop];
+};
+type WebGLResource = (
+ | WebGLBuffer
+ | WebGLFramebuffer
+ | WebGLProgram
+ | WebGLRenderbuffer
+ | WebGLShader
+ | WebGLTexture
+) & { __webglDebugContextLostId__: number };
+
+export const WebGLDebugUtils = (function () {
+ /**
+ * Wrapped logging function.
+ * @param {string} msg Message to log.
+ */
+ const log = function (msg: string) {
+ if (window.console && window.console.log) {
+ window.console.log(msg);
+ }
+ };
+
+ /**
+ * Which arguments are enums.
+ * @type {!Object.}
+ */
+ const glValidEnumContexts: GLValidEnumContext = {
+ // Generic setters and getters
+
+ enable: { 0: true },
+ disable: { 0: true },
+ getParameter: { 0: true },
+
+ // Rendering
+
+ drawArrays: { 0: true },
+ drawElements: { 0: true, 2: true },
+
+ // Shaders
+
+ createShader: { 0: true },
+ getShaderParameter: { 1: true },
+ getProgramParameter: { 1: true },
+
+ // Vertex attributes
+
+ getVertexAttrib: { 1: true },
+ vertexAttribPointer: { 2: true },
+
+ // Textures
+
+ bindTexture: { 0: true },
+ activeTexture: { 0: true },
+ getTexParameter: { 0: true, 1: true },
+ texParameterf: { 0: true, 1: true },
+ texParameteri: { 0: true, 1: true, 2: true },
+ texImage2D: { 0: true, 2: true, 6: true, 7: true },
+ texSubImage2D: { 0: true, 6: true, 7: true },
+ copyTexImage2D: { 0: true, 2: true },
+ copyTexSubImage2D: { 0: true },
+ generateMipmap: { 0: true },
+
+ // Buffer objects
+
+ bindBuffer: { 0: true },
+ bufferData: { 0: true, 2: true },
+ bufferSubData: { 0: true },
+ getBufferParameter: { 0: true, 1: true },
+
+ // Renderbuffers and framebuffers
+
+ pixelStorei: { 0: true, 1: true },
+ readPixels: { 4: true, 5: true },
+ bindRenderbuffer: { 0: true },
+ bindFramebuffer: { 0: true },
+ checkFramebufferStatus: { 0: true },
+ framebufferRenderbuffer: { 0: true, 1: true, 2: true },
+ framebufferTexture2D: { 0: true, 1: true, 2: true },
+ getFramebufferAttachmentParameter: { 0: true, 1: true, 2: true },
+ getRenderbufferParameter: { 0: true, 1: true },
+ renderbufferStorage: { 0: true, 1: true },
+
+ // Frame buffer operations (clear, blend, depth test, stencil)
+
+ clear: { 0: true },
+ depthFunc: { 0: true },
+ blendFunc: { 0: true, 1: true },
+ blendFuncSeparate: { 0: true, 1: true, 2: true, 3: true },
+ blendEquation: { 0: true },
+ blendEquationSeparate: { 0: true, 1: true },
+ stencilFunc: { 0: true },
+ stencilFuncSeparate: { 0: true, 1: true },
+ stencilMaskSeparate: { 0: true },
+ stencilOp: { 0: true, 1: true, 2: true },
+ stencilOpSeparate: { 0: true, 1: true, 2: true, 3: true },
+
+ // Culling
+
+ cullFace: { 0: true },
+ frontFace: { 0: true },
+ };
+
+ /**
+ * Map of numbers to names.
+ * @type {Object}
+ */
+ let glEnums: Record | null = null;
+
+ /**
+ * Initializes this module. Safe to call more than once.
+ * @param {!WebGLRenderingContext} ctx A WebGL context. If
+ * you have more than one context it doesn't matter which one
+ * you pass in, it is only used to pull out constants.
+ */
+ function init(ctx: WebGLRenderingContext) {
+ if (glEnums == null) {
+ glEnums = {};
+ for (const propertyName in ctx) {
+ if (typeof (ctx as any)[propertyName] == 'number') {
+ glEnums[(ctx as any)[propertyName]] = propertyName;
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks the utils have been initialized.
+ */
+ function checkInit() {
+ if (glEnums == null) {
+ throw 'WebGLDebugUtils.init(ctx) not called';
+ }
+ }
+
+ /**
+ * Returns true or false if value matches any WebGL enum
+ * @param {*} glEnumKey Value to check if it might be an enum.
+ * @return {boolean} True if value matches one of the WebGL defined enums
+ */
+ function mightBeEnum(glEnumKey: string | number) {
+ checkInit();
+ return !!glEnums && glEnums[glEnumKey] !== undefined;
+ }
+
+ /**
+ * Gets an string version of an WebGL enum.
+ *
+ * Example:
+ * const str = WebGLDebugUtil.glEnumToString(ctx.getError());
+ *
+ * @param {number} glEnumKey Value to return an enum for
+ * @return {string} The string version of the enum.
+ */
+ function glEnumToString(glEnumKey: string | number) {
+ checkInit();
+ const name = glEnums && glEnums[glEnumKey];
+ return name !== undefined ? name : '*UNKNOWN WebGL ENUM (0x' + glEnumKey.toString(16) + ')';
+ }
+
+ /**
+ * Returns the string version of a WebGL argument.
+ * Attempts to convert enum arguments to strings.
+ * @param {string} functionName the name of the WebGL function.
+ * @param {number} argumentIndx the index of the argument.
+ * @param {*} value The value of the argument.
+ * @return {string} The value as a string.
+ */
+ function glFunctionArgToString(functionName: WebGLContextFuncKey, argumentIndex: number, value: string | number) {
+ const funcInfo = glValidEnumContexts[functionName];
+ if (funcInfo !== undefined) {
+ if (funcInfo[argumentIndex]) {
+ return glEnumToString(value);
+ }
+ }
+ return value.toString();
+ }
+
+ /**
+ * Given a WebGL context returns a wrapped context that calls
+ * gl.getError after every command and calls a function if the
+ * result is not gl.NO_ERROR.
+ *
+ * @param {!WebGLRenderingContext} ctx The webgl context to
+ * wrap.
+ * @param {!function(err, funcName, args): void} opt_onErrorFunc
+ * The function to call when gl.getError returns an
+ * error. If not specified the default function calls
+ * console.log with a message.
+ */
+ function makeDebugContext(
+ ctx: WebGLRenderingContext,
+ opt_onErrorFunc?: (err: any, funcName: keyof typeof glValidEnumContexts, args: any[]) => void,
+ ) {
+ init(ctx);
+ opt_onErrorFunc =
+ opt_onErrorFunc ||
+ function (err, functionName, args) {
+ // apparently we can't do args.join(",");
+ let argStr = '';
+ for (let ii = 0; ii < args.length; ++ii) {
+ argStr += (ii == 0 ? '' : ', ') + glFunctionArgToString(functionName, ii as never, args[ii]);
+ }
+ log('WebGL error ' + glEnumToString(err) + ' in ' + functionName + '(' + argStr + ')');
+ };
+
+ // Holds booleans for each GL error so after we get the error ourselves
+ // we can still return it to the client app.
+ const glErrorShadow: Record = {};
+
+ // Makes a function that calls a WebGL function and then calls getError.
+ function makeErrorWrapper(ctx: WebGLRenderingContext, functionName: WebGLContextFuncKey) {
+ return function () {
+ const result = (ctx as any)[functionName].apply(ctx, arguments);
+ const err = ctx.getError();
+ if (err != 0) {
+ glErrorShadow[err] = true;
+ opt_onErrorFunc!(err, functionName, arguments as any);
+ }
+ return result;
+ };
+ }
+
+ // Make a an object that has a copy of every property of the WebGL context
+ // but wraps all functions.
+ const wrapper: GLPartialContext = {};
+ for (const propertyName in ctx) {
+ const funcKey = propertyName as WebGLContextFuncKey;
+ if (typeof ctx[funcKey] === 'function') {
+ wrapper[funcKey] = makeErrorWrapper(ctx, funcKey);
+ } else {
+ wrapper[funcKey] = (ctx as any)[funcKey];
+ }
+ }
+
+ // Override the getError function with one that returns our saved results.
+ wrapper.getError = function () {
+ for (const err in glErrorShadow) {
+ if (glErrorShadow[err]) {
+ glErrorShadow[err] = false;
+ return +err;
+ }
+ }
+ return ctx.NO_ERROR;
+ };
+
+ return wrapper;
+ }
+
+ function resetToInitialState(ctx: WebGLRenderingContext) {
+ const numAttribs = ctx.getParameter(ctx.MAX_VERTEX_ATTRIBS);
+ const tmp = ctx.createBuffer();
+ ctx.bindBuffer(ctx.ARRAY_BUFFER, tmp);
+ for (let ii = 0; ii < numAttribs; ++ii) {
+ ctx.disableVertexAttribArray(ii);
+ ctx.vertexAttribPointer(ii, 4, ctx.FLOAT, false, 0, 0);
+ ctx.vertexAttrib1f(ii, 0);
+ }
+ ctx.deleteBuffer(tmp);
+
+ const numTextureUnits = ctx.getParameter(ctx.MAX_TEXTURE_IMAGE_UNITS);
+ for (let ii = 0; ii < numTextureUnits; ++ii) {
+ ctx.activeTexture(ctx.TEXTURE0 + ii);
+ ctx.bindTexture(ctx.TEXTURE_CUBE_MAP, null);
+ ctx.bindTexture(ctx.TEXTURE_2D, null);
+ }
+
+ ctx.activeTexture(ctx.TEXTURE0);
+ ctx.useProgram(null);
+ ctx.bindBuffer(ctx.ARRAY_BUFFER, null);
+ ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, null);
+ ctx.bindFramebuffer(ctx.FRAMEBUFFER, null);
+ ctx.bindRenderbuffer(ctx.RENDERBUFFER, null);
+ ctx.disable(ctx.BLEND);
+ ctx.disable(ctx.CULL_FACE);
+ ctx.disable(ctx.DEPTH_TEST);
+ ctx.disable(ctx.DITHER);
+ ctx.disable(ctx.SCISSOR_TEST);
+ ctx.blendColor(0, 0, 0, 0);
+ ctx.blendEquation(ctx.FUNC_ADD);
+ ctx.blendFunc(ctx.ONE, ctx.ZERO);
+ ctx.clearColor(0, 0, 0, 0);
+ ctx.clearDepth(1);
+ ctx.clearStencil(-1);
+ ctx.colorMask(true, true, true, true);
+ ctx.cullFace(ctx.BACK);
+ ctx.depthFunc(ctx.LESS);
+ ctx.depthMask(true);
+ ctx.depthRange(0, 1);
+ ctx.frontFace(ctx.CCW);
+ ctx.hint(ctx.GENERATE_MIPMAP_HINT, ctx.DONT_CARE);
+ ctx.lineWidth(1);
+ ctx.pixelStorei(ctx.PACK_ALIGNMENT, 4);
+ ctx.pixelStorei(ctx.UNPACK_ALIGNMENT, 4);
+ ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, false);
+ ctx.pixelStorei(ctx.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
+ // TODO: Delete this IF.
+ if (ctx.UNPACK_COLORSPACE_CONVERSION_WEBGL) {
+ ctx.pixelStorei(ctx.UNPACK_COLORSPACE_CONVERSION_WEBGL, ctx.BROWSER_DEFAULT_WEBGL);
+ }
+ ctx.polygonOffset(0, 0);
+ ctx.sampleCoverage(1, false);
+ ctx.scissor(0, 0, ctx.canvas.width, ctx.canvas.height);
+ ctx.stencilFunc(ctx.ALWAYS, 0, 0xffffffff);
+ ctx.stencilMask(0xffffffff);
+ ctx.stencilOp(ctx.KEEP, ctx.KEEP, ctx.KEEP);
+ ctx.viewport(0, 0, (ctx.canvas as HTMLCanvasElement).clientWidth, (ctx.canvas as HTMLCanvasElement).clientHeight);
+ ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT | ctx.STENCIL_BUFFER_BIT);
+
+ // TODO: This should NOT be needed but Firefox fails with 'hint'
+ while (ctx.getError());
+ }
+
+ function makeLostContextSimulatingContext(ctx: WebGLRenderingContext) {
+ const wrapper_: GLPartialContext & {
+ loseContext?: Function;
+ restoreContext?: Function;
+ registerOnContextLostListener?: Function;
+ registerOnContextRestoredListener?: Function;
+ } = {};
+ let contextId_ = 1;
+ let contextLost_ = false;
+ const resourceDb_: WebGLResource[] = [];
+ let onLost_: Function | undefined = undefined;
+ let onRestored_: Function | undefined = undefined;
+ let nextOnRestored_: Function | undefined = undefined;
+
+ // Holds booleans for each GL error so can simulate errors.
+ const glErrorShadow_: { [prop in number | string]: boolean } = {};
+
+ function isWebGLResource(obj: WebGLResource) {
+ //return false;
+ return (
+ obj instanceof WebGLBuffer ||
+ obj instanceof WebGLFramebuffer ||
+ obj instanceof WebGLProgram ||
+ obj instanceof WebGLRenderbuffer ||
+ obj instanceof WebGLShader ||
+ obj instanceof WebGLTexture
+ );
+ }
+
+ function checkResources(args: WebGLResource[]) {
+ for (let ii = 0; ii < args.length; ++ii) {
+ const arg = args[ii];
+ if (isWebGLResource(arg)) {
+ return arg.__webglDebugContextLostId__ == contextId_;
+ }
+ }
+ return true;
+ }
+
+ function clearErrors() {
+ const k = Object.keys(glErrorShadow_);
+ for (let ii = 0; ii < k.length; ++ii) {
+ delete glErrorShadow_[k[ii]];
+ }
+ }
+
+ // Makes a function that simulates WebGL when out of context.
+ function makeLostContextWrapper(ctx: WebGLRenderingContext, functionName: WebGLContextFuncKey) {
+ const f = ctx[functionName];
+ return function () {
+ // Only call the functions if the context is not lost.
+ if (!contextLost_) {
+ if (!checkResources(arguments as unknown as WebGLResource[])) {
+ glErrorShadow_[ctx.INVALID_OPERATION] = true;
+ return;
+ }
+ const result = (f as (this: WebGLRenderingContext, mask: number) => any).apply(
+ ctx,
+ arguments as unknown as [number],
+ );
+ return result;
+ }
+ };
+ }
+
+ for (const propertyName in ctx) {
+ const ctxFuncKey = propertyName as WebGLContextFuncKey;
+ if (typeof ctx[propertyName as keyof WebGLRenderingContext] === 'function') {
+ wrapper_[ctxFuncKey] = makeLostContextWrapper(ctx, ctxFuncKey);
+ } else {
+ wrapper_[ctxFuncKey] = ctx[ctxFuncKey] as any;
+ }
+ }
+
+ function makeWebGLContextEvent(statusMessage: string) {
+ return { statusMessage: statusMessage };
+ }
+
+ function freeResources() {
+ for (let ii = 0; ii < resourceDb_.length; ++ii) {
+ const resource = resourceDb_[ii];
+ if (resource instanceof WebGLBuffer) {
+ ctx.deleteBuffer(resource);
+ } else if (resource instanceof WebGLFramebuffer) {
+ ctx.deleteFramebuffer(resource);
+ } else if (resource instanceof WebGLProgram) {
+ ctx.deleteProgram(resource);
+ } else if (resource instanceof WebGLRenderbuffer) {
+ ctx.deleteRenderbuffer(resource);
+ } else if (resource instanceof WebGLShader) {
+ ctx.deleteShader(resource);
+ } else if (resource instanceof WebGLTexture) {
+ ctx.deleteTexture(resource);
+ }
+ }
+ }
+
+ wrapper_.loseContext = function () {
+ if (!contextLost_) {
+ contextLost_ = true;
+ ++contextId_;
+ while (ctx.getError());
+ clearErrors();
+ glErrorShadow_[ctx.CONTEXT_LOST_WEBGL] = true;
+ setTimeout(function () {
+ if (onLost_) {
+ onLost_(makeWebGLContextEvent('context lost'));
+ }
+ }, 0);
+ }
+ };
+
+ wrapper_.restoreContext = function () {
+ if (contextLost_) {
+ if (onRestored_) {
+ setTimeout(function () {
+ freeResources();
+ resetToInitialState(ctx);
+ contextLost_ = false;
+ if (onRestored_) {
+ const callback = onRestored_;
+ onRestored_ = nextOnRestored_;
+ nextOnRestored_ = undefined;
+ callback(makeWebGLContextEvent('context restored'));
+ }
+ }, 0);
+ } else {
+ throw 'You can not restore the context without a listener';
+ }
+ }
+ };
+
+ // Wrap a few functions specially.
+ wrapper_.getError = function () {
+ if (!contextLost_) {
+ let err;
+ while ((err = ctx.getError())) {
+ glErrorShadow_[err] = true;
+ }
+ }
+ for (const err in glErrorShadow_) {
+ if (glErrorShadow_[err]) {
+ delete glErrorShadow_[err];
+ return +err;
+ }
+ }
+ return ctx.NO_ERROR;
+ };
+
+ const creationFunctions: WebGLContextFuncKey[] = [
+ 'createBuffer',
+ 'createFramebuffer',
+ 'createProgram',
+ 'createRenderbuffer',
+ 'createShader',
+ 'createTexture',
+ ];
+ for (let ii = 0; ii < creationFunctions.length; ++ii) {
+ const functionName = creationFunctions[ii];
+ wrapper_[functionName] = (function (f) {
+ return function () {
+ if (contextLost_) {
+ return null;
+ }
+ const obj = (f as (this: WebGLRenderingContext, mask: number) => any).apply(
+ ctx,
+ arguments as unknown as [number],
+ );
+ obj.__webglDebugContextLostId__ = contextId_;
+ resourceDb_.push(obj);
+ return obj;
+ };
+ })(ctx[functionName]);
+ }
+
+ const functionsThatShouldReturnNull: WebGLContextFuncKey[] = [
+ 'getActiveAttrib',
+ 'getActiveUniform',
+ 'getBufferParameter',
+ 'getContextAttributes',
+ 'getAttachedShaders',
+ 'getFramebufferAttachmentParameter',
+ 'getParameter',
+ 'getProgramParameter',
+ 'getProgramInfoLog',
+ 'getRenderbufferParameter',
+ 'getShaderParameter',
+ 'getShaderInfoLog',
+ 'getShaderSource',
+ 'getTexParameter',
+ 'getUniform',
+ 'getUniformLocation',
+ 'getVertexAttrib',
+ ];
+ for (let ii = 0; ii < functionsThatShouldReturnNull.length; ++ii) {
+ const functionName = functionsThatShouldReturnNull[ii];
+ wrapper_[functionName] = (function (f) {
+ return function () {
+ if (contextLost_) {
+ return null;
+ }
+ return (f as (this: WebGLRenderingContext, mask: number) => any).apply(ctx, arguments as unknown as [number]);
+ };
+ })(wrapper_[functionName]);
+ }
+
+ const isFunctions: WebGLContextFuncKey[] = [
+ 'isBuffer',
+ 'isEnabled',
+ 'isFramebuffer',
+ 'isProgram',
+ 'isRenderbuffer',
+ 'isShader',
+ 'isTexture',
+ ];
+ for (let ii = 0; ii < isFunctions.length; ++ii) {
+ const functionName = isFunctions[ii];
+ wrapper_[functionName] = (function (f) {
+ return function () {
+ if (contextLost_) {
+ return false;
+ }
+ return (f as (this: WebGLRenderingContext, mask: number) => any).apply(ctx, arguments as unknown as [number]);
+ };
+ })(wrapper_[functionName]);
+ }
+
+ wrapper_.checkFramebufferStatus = (function (f) {
+ return function () {
+ if (contextLost_) {
+ return ctx.FRAMEBUFFER_UNSUPPORTED;
+ }
+ return (f as (this: WebGLRenderingContext, mask: number) => any).apply(ctx, arguments as unknown as [number]);
+ };
+ })(wrapper_.checkFramebufferStatus);
+
+ wrapper_.getAttribLocation = (function (f) {
+ return function () {
+ if (contextLost_) {
+ return -1;
+ }
+ return (f as (this: WebGLRenderingContext, mask: number) => any).apply(ctx, arguments as unknown as [number]);
+ };
+ })(wrapper_.getAttribLocation);
+
+ wrapper_.getVertexAttribOffset = (function (f) {
+ return function () {
+ if (contextLost_) {
+ return 0;
+ }
+ return (f as (this: WebGLRenderingContext, mask: number) => any).apply(ctx, arguments as unknown as [number]);
+ };
+ })(wrapper_.getVertexAttribOffset);
+
+ wrapper_.isContextLost = function () {
+ return contextLost_;
+ };
+ function wrapEvent(listener: Function | { handleEvent: Function }) {
+ if (typeof listener == 'function') {
+ return listener;
+ } else {
+ return function (info: any) {
+ listener.handleEvent(info);
+ };
+ }
+ }
+
+ wrapper_.registerOnContextLostListener = function (listener: Function) {
+ onLost_ = wrapEvent(listener);
+ };
+
+ wrapper_.registerOnContextRestoredListener = function (listener: Function) {
+ if (contextLost_) {
+ nextOnRestored_ = wrapEvent(listener);
+ } else {
+ onRestored_ = wrapEvent(listener);
+ }
+ };
+
+ return wrapper_;
+ }
+
+ return {
+ /**
+ * Initializes this module. Safe to call more than once.
+ * @param {!WebGLRenderingContext} ctx A WebGL context. If
+ * you have more than one context it doesn't matter which one
+ * you pass in, it is only used to pull out constants.
+ */
+ init: init,
+
+ /**
+ * Returns true or false if value matches any WebGL enum
+ * @param {*} value Value to check if it might be an enum.
+ * @return {boolean} True if value matches one of the WebGL defined enums
+ */
+ mightBeEnum: mightBeEnum,
+
+ /**
+ * Gets an string version of an WebGL enum.
+ *
+ * Example:
+ * WebGLDebugUtil.init(ctx);
+ * const str = WebGLDebugUtil.glEnumToString(ctx.getError());
+ *
+ * @param {number} value Value to return an enum for
+ * @return {string} The string version of the enum.
+ */
+ glEnumToString: glEnumToString,
+
+ /**
+ * Converts the argument of a WebGL function to a string.
+ * Attempts to convert enum arguments to strings.
+ *
+ * Example:
+ * WebGLDebugUtil.init(ctx);
+ * const str = WebGLDebugUtil.glFunctionArgToString('bindTexture', 0, gl.TEXTURE_2D);
+ *
+ * would return 'TEXTURE_2D'
+ *
+ * @param {string} functionName the name of the WebGL function.
+ * @param {number} argumentIndx the index of the argument.
+ * @param {*} value The value of the argument.
+ * @return {string} The value as a string.
+ */
+ glFunctionArgToString: glFunctionArgToString,
+
+ /**
+ * Given a WebGL context returns a wrapped context that calls
+ * gl.getError after every command and calls a function if the
+ * result is not NO_ERROR.
+ *
+ * You can supply your own function if you want. For example, if you'd like
+ * an exception thrown on any GL error you could do this
+ *
+ * function throwOnGLError(err, funcName, args) {
+ * throw WebGLDebugUtils.glEnumToString(err) + " was caused by call to" +
+ * funcName;
+ * };
+ *
+ * ctx = WebGLDebugUtils.makeDebugContext(
+ * canvas.getContext("webgl"), throwOnGLError);
+ *
+ * @param {!WebGLRenderingContext} ctx The webgl context to wrap.
+ * @param {!function(err, funcName, args): void} opt_onErrorFunc The function
+ * to call when gl.getError returns an error. If not specified the default
+ * function calls console.log with a message.
+ */
+ makeDebugContext: makeDebugContext,
+
+ /**
+ * Given a WebGL context returns a wrapped context that adds 4
+ * functions.
+ *
+ * ctx.loseContext:
+ * simulates a lost context event.
+ *
+ * ctx.restoreContext:
+ * simulates the context being restored.
+ *
+ * ctx.registerOnContextLostListener(listener):
+ * lets you register a listener for context lost. Use instead
+ * of addEventListener('webglcontextlostevent', listener);
+ *
+ * ctx.registerOnContextRestoredListener(listener):
+ * lets you register a listener for context restored. Use
+ * instead of addEventListener('webglcontextrestored',
+ * listener);
+ *
+ * @param {!WebGLRenderingContext} ctx The webgl context to wrap.
+ */
+ makeLostContextSimulatingContext: makeLostContextSimulatingContext,
+
+ /**
+ * Resets a context to the initial state.
+ * @param {!WebGLRenderingContext} ctx The webgl context to
+ * reset.
+ */
+ resetToInitialState: resetToInitialState,
+ };
+})();
diff --git a/packages/image-extraction/libs/webgl-utils.ts b/packages/image-extraction/libs/webgl-utils.ts
new file mode 100644
index 0000000..c89d7c1
--- /dev/null
+++ b/packages/image-extraction/libs/webgl-utils.ts
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2010, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @fileoverview This file contains functions every webgl program will need
+ * a version of one way or another.
+ *
+ * Instead of setting up a context manually it is recommended to
+ * use. This will check for success or failure. On failure it
+ * will attempt to present an approriate message to the user.
+ *
+ * gl = WebGLUtils.setupWebGL(canvas);
+ *
+ * For animated WebGL apps use of setTimeout or setInterval are
+ * discouraged. It is recommended you structure your rendering
+ * loop like this.
+ *
+ * function render() {
+ * window.requestAnimationFrame(render, canvas);
+ *
+ * // do rendering
+ * ...
+ * }
+ * render();
+ *
+ * This will call your rendering function up to the refresh rate
+ * of your display but will stop rendering if your app is not
+ * visible.
+ */
+
+export const WebGLUtils = (function () {
+ /**
+ * Creates the HTLM for a failure message
+ * @param {string} canvasContainerId id of container of th
+ * canvas.
+ * @return {string} The html.
+ */
+ const makeFailHTML = function (msg: string) {
+ return (
+ '' +
+ '
' +
+ msg +
+ '
'
+ );
+ };
+
+ /**
+ * Mesasge for getting a webgl browser
+ * @type {string}
+ */
+ const GET_A_WEBGL_BROWSER =
+ '' +
+ 'This page requires a browser that supports WebGL. ' +
+ 'Click here to upgrade your browser.';
+
+ /**
+ * Mesasge for need better hardware
+ * @type {string}
+ */
+ const OTHER_PROBLEM =
+ '' +
+ "It doesn't appear your computer can support WebGL. " +
+ 'Click here for more information.';
+
+ /**
+ * Creates a webgl context. If creation fails it will
+ * change the contents of the container of the