mirror of
https://github.com/palxiao/poster-design.git
synced 2025-07-15 16:02:19 +08:00
Merge pull request #80 from JeremyYu-cn/feat-upgrade-vue3
Feat: merge branch and convert wText component to composition API
This commit is contained in:
commit
d37c41dff5
@ -13,7 +13,6 @@ module.exports = {
|
||||
// 自定义你的规则
|
||||
'vue/component-tags-order': ['off'],
|
||||
'vue/no-multiple-template-root': ['off'],
|
||||
'max-params': ['off'],
|
||||
// 'no-undef': 'off', // 禁止使用未定义的变量,会把TS声明视为变量,暂时关闭
|
||||
},
|
||||
parserOptions: {
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,7 +15,6 @@ screenshot/_apidoc/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
|
18
README.md
18
README.md
@ -2,14 +2,14 @@
|
||||
|
||||
---
|
||||
|
||||
## 迅排设计
|
||||
## Poster Design
|
||||
|
||||
一款漂亮且功能强大的在线海报图片设计器,仿稿定设计。
|
||||
|
||||
适用于海报图片生成、电商分享图、文章长图、视频/公众号封面等多种场景。
|
||||
迅排设计是一款漂亮易用且功能强大的开源创意图片编辑器,是对标稿定设计、创客贴、Canva 等商业产品的免费在线设计工具。
|
||||
|
||||
[](https://design.palxp.cn/)
|
||||
|
||||
适用于多种场景:海报图片生成、电商分享图、文章长图、视频/公众号封面等,无需下载软件即可轻松实现云端编辑、迅速完成图文排版。
|
||||
|
||||
- 丝滑的页面操作体验,丰富的交互细节,基础功能完善
|
||||
- 采用服务端生成图片,能确保多端出图统一性,支持各种 CSS 特性
|
||||
- 简易 AI 抠图工具,上传图片一键去除背景
|
||||
@ -40,7 +40,7 @@ npm run serve
|
||||
|
||||

|
||||
|
||||
访问 http://127.0.0.1:3000/ 查看网页。点此查看[完整说明文档](https://xp.palxp.cn/#/articles/1689319644311)。
|
||||
访问 http://127.0.0.1:5173/ 查看网页。点此查看[完整说明文档](https://xp.palxp.cn/#/articles/1689319644311)。
|
||||
|
||||
### 图片生成服务
|
||||
|
||||
@ -56,7 +56,7 @@ npm run serve
|
||||
|
||||
本项目最早使用 Vue2 开发,现改用 Vue3 重构中。[一些迭代计划记录](https://xp.palxp.cn/#/articles/1689319986889?id=%e8%bf%ad%e4%bb%a3%e8%ae%a1%e5%88%92).
|
||||
|
||||
目前开源版仍在持续迭代中,还有很多的不足,我的目标是做一款能对标稿定设计、创客贴、Canva等商业产品的强大在线设计器。
|
||||
目前开源版仍在持续迭代中,还有很多的不足,可以将你遇到的问题在 Issues 中提出,或者提交 Pull Request 帮助完善。
|
||||
|
||||
### 感谢
|
||||
|
||||
@ -91,7 +91,9 @@ npm run serve
|
||||
|
||||
### 友情赞助商
|
||||
|
||||
[](https://dooring.vip/)
|
||||
| Dooring低代码 | DrawOn桌案 |
|
||||
| --- | --- |
|
||||
| <a href="https://dooring.vip/"> <img style="height: 90px" src="https://github.com/palxiao/poster-design/assets/21021314/2240801f-8484-4fd2-8505-8205daa6d53c" /></a> | <a href="https://www.drawon.cn?useSource=hb1"> <img style="height: 120px" src="https://github.com/palxiao/poster-design/assets/21021314/258bb6ec-4e1e-4c86-b45c-22946213f209" /></a> |
|
||||
|
||||
### `Contributions`
|
||||
|
||||
@ -101,5 +103,5 @@ npm run serve
|
||||
|
||||
### `LICENSE`
|
||||
|
||||
本项目完全免费,遵循 [MIT 开源许可证](https://github.com/palxiao/poster-design/blob/main/LICENSE)
|
||||
本项目完全免费,可在保留 [MIT 开源许可证](https://github.com/palxiao/poster-design/blob/main/LICENSE) 的前提下使用。
|
||||
|
||||
|
5539
package-lock.json
generated
5539
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -14,10 +14,9 @@
|
||||
"publish-fast": "git add . && git commit -m 'build: auto publish' && sh script/publish.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@palxp/color-picker": "^1.5.5",
|
||||
"@palxp/image-extraction": "^1.2.4",
|
||||
"@palxp/color-picker": "workspace:*",
|
||||
"@palxp/image-extraction": "workspace:*",
|
||||
"@scena/guides": "^0.18.1",
|
||||
"@types/cropperjs": "^1.3.0",
|
||||
"@webtoon/psd": "^0.4.0",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.6.5",
|
||||
@ -57,9 +56,12 @@
|
||||
"vite": "^5.1.4",
|
||||
"vue-tsc": "^1.8.27"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
"Chrome >= 90"
|
||||
],
|
||||
"website": "https://design.palxp.cn",
|
||||
"homepage": "https://xp.palxp.cn"
|
||||
}
|
||||
|
168
packages/color-picker/CHANGELOG.md
Normal file
168
packages/color-picker/CHANGELOG.md
Normal file
@ -0,0 +1,168 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.5.5](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.5.4...@palxp/color-picker@1.5.5) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.4](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.5.3...@palxp/color-picker@1.5.4) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.3](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.5.2...@palxp/color-picker@1.5.3) (2024-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **custom:** remove some configurations ([30f3257](https://github.com/palxiao/front-end-arsenal/commit/30f3257168bd22a15feb04e58cc0fb8fcac6807e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.2](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.5.1...@palxp/color-picker@1.5.2) (2023-11-30)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.1](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.5.0...@palxp/color-picker@1.5.1) (2023-11-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **component:** colorPicker gradient delete ([34b5d8b](https://github.com/palxiao/front-end-arsenal/commit/34b5d8b933a8e927802ba546528fdd1072d7de9b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.5.0](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.4.0...@palxp/color-picker@1.5.0) (2023-11-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **component:** add a angle handle ([635a62c](https://github.com/palxiao/front-end-arsenal/commit/635a62c379ae05d079cda7a9da0cf74ec0a81822))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.4.0](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.3.3...@palxp/color-picker@1.4.0) (2023-11-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **component:** add gradient colorPicker ([e0cbdd2](https://github.com/palxiao/front-end-arsenal/commit/e0cbdd20d9dad2ebc0de64e66958058bc4bf0cd6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.3](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.3.2...@palxp/color-picker@1.3.3) (2023-10-10)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.2](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.3.1...@palxp/color-picker@1.3.2) (2023-10-08)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.1](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.3.0...@palxp/color-picker@1.3.1) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.3.0](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.2.5...@palxp/color-picker@1.3.0) (2023-08-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **component:** color-picker add blur ([1a3d1b0](https://github.com/palxiao/front-end-arsenal/commit/1a3d1b073dcbbc1a8c30ad625cd7eed285665932))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.5](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.2.4...@palxp/color-picker@1.2.5) (2023-06-29)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.4](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.2.3...@palxp/color-picker@1.2.4) (2023-06-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复一个报错 ([41fdfc8](https://github.com/palxiao/front-end-arsenal/commit/41fdfc8a1a6ef32e221c7fae97b1aa5e24b63a0e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.3](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.2.2...@palxp/color-picker@1.2.3) (2023-06-21)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.2](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.2.1...@palxp/color-picker@1.2.2) (2023-05-29)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.1](https://github.com/palxiao/front-end-arsenal/compare/@palxp/color-picker@1.2.0...@palxp/color-picker@1.2.1) (2023-05-29)
|
||||
|
||||
**Note:** Version bump only for package @palxp/color-picker
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 1.2.0 (2023-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **component:** 新增 ColorPicker 组件 ([effebc9](https://github.com/palxiao/front-end-arsenal/commit/effebc9795ce1426f3126c1fe07e58673da5748a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 1.1.0 (2023-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **component:** 新增 ColorPicker 组件 ([effebc9](https://github.com/palxiao/front-end-arsenal/commit/effebc9795ce1426f3126c1fe07e58673da5748a))
|
32
packages/color-picker/README.md
Normal file
32
packages/color-picker/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
<!--
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-05-29 22:54:18
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2024-01-31 10:51:35
|
||||
-->
|
||||
<img style="display: inline-block;" src="https://img.shields.io/github/watchers/palxiao/front-end-arsenal?style=social" />
|
||||
<img style="display: inline-block;" src="https://img.shields.io/github/forks/palxiao/front-end-arsenal?style=social" />
|
||||
<img style="display: inline-block;" src="https://img.shields.io/github/stars/palxiao/front-end-arsenal?style=social" />
|
||||
|
||||
# color-picker
|
||||
|
||||
> TODO: 颜色取色器,适用于 Vue3
|
||||
|
||||
<img style="display: inline-block;" src="https://img.shields.io/npm/v/@palxp/color-picker" />
|
||||
<img style="display: inline-block;" src="https://img.shields.io/bundlephobia/min/@palxp/color-picker?color=%2344cc88" />
|
||||
<img style="display: inline-block;" src="https://img.shields.io/npm/dm/@palxp/color-picker" />
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
yarn add @palxp/color-picker
|
||||
|
||||
import colorPicker from '@palxp/color-picker'
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
[API Docs 链接](/#/docs)
|
||||
|
||||
<iframe src="/#/docs/color-picker/index?preview=true" frameborder="0"></iframe>
|
150
packages/color-picker/comps/AngleHandle.vue
Normal file
150
packages/color-picker/comps/AngleHandle.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<!--
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-11-29 10:34:54
|
||||
* @Description: 角度手柄
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-11-29 19:24:14
|
||||
-->
|
||||
<template>
|
||||
<div class="angle-input-box">
|
||||
<input ref="numInput" v-model="num" class="angle-input" @focus="visiable = true" @blur="visiable = false" @input="inputChange" />
|
||||
<div v-show="visiable" class="AngleHandle" @mousedown="touch($event, true)" @mouseup="touch($event, false)">
|
||||
<div class="angle" @mouseup="turn" @mousemove="turn">
|
||||
<div :style="`transform: rotate(${angleInDegrees}deg)`" class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, computed } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: ['modelValue'],
|
||||
emits: ['change', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const num = ref(90)
|
||||
const numInput = ref(null)
|
||||
const angleInDegrees = computed(() => {
|
||||
return num.value - 90
|
||||
})
|
||||
let inProcess = false
|
||||
const visiable = ref(false)
|
||||
|
||||
const inputChange = (e: any) => {
|
||||
emit('change', e)
|
||||
}
|
||||
watch(
|
||||
() => num.value,
|
||||
(v) => {
|
||||
props.modelValue !== num.value && emit('update:modelValue', v)
|
||||
emit('change')
|
||||
},
|
||||
)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
num.value = v
|
||||
},
|
||||
)
|
||||
|
||||
const turn = (e: any) => {
|
||||
if (!inProcess) {
|
||||
return
|
||||
}
|
||||
const origin = { x: 27, y: 27 }
|
||||
// 计算相对于原点的坐标差值
|
||||
const deltaX = e.offsetX - origin.x
|
||||
const deltaY = e.offsetY - origin.y
|
||||
// 计算夹角(弧度)
|
||||
const angleInRadians = Math.atan2(deltaY, deltaX)
|
||||
// 将弧度转换为角度
|
||||
const angleInDegrees = (angleInRadians * 180) / Math.PI
|
||||
num.value = Math.round(angleInDegrees + 90)
|
||||
}
|
||||
|
||||
const touch = (e: any, isHandle: boolean) => {
|
||||
e.preventDefault()
|
||||
inProcess = isHandle
|
||||
}
|
||||
return { inputChange, num, turn, touch, angleInDegrees, numInput, visiable }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.angle-input {
|
||||
width: 38px;
|
||||
margin-left: 5px;
|
||||
padding: 0 0 0 4px;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
.angle-input-box {
|
||||
position: relative;
|
||||
}
|
||||
.angle-input-box::after {
|
||||
content: '°';
|
||||
width: 5px;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.AngleHandle {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 2px;
|
||||
margin-top: 3px;
|
||||
background: #ffffff;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 0 2px rgb(0 0 0 / 60%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.angle {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #f1f2f4;
|
||||
border-radius: 50%;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
height: 1px;
|
||||
background: #999999;
|
||||
pointer-events: none;
|
||||
transform-origin: left top;
|
||||
}
|
||||
.line::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
left: -1px;
|
||||
top: -1px;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: #999999;
|
||||
}
|
||||
.line::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
right: 0;
|
||||
top: -2px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #999999;
|
||||
}
|
||||
}
|
||||
</style>
|
39
packages/color-picker/comps/TabPanel.vue
Normal file
39
packages/color-picker/comps/TabPanel.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="tab-panel" :style="rootStyle">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TabPanel',
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, ref } from 'vue'
|
||||
|
||||
const vm = getCurrentInstance()
|
||||
vm.parent.exposed.tabs.value.push(vm)
|
||||
|
||||
defineProps({
|
||||
// Tabs 会用到 label
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const active = ref(false)
|
||||
const rootStyle = computed(() => ({
|
||||
display: active.value ? 'block' : 'none',
|
||||
}))
|
||||
|
||||
function changeActive(value) {
|
||||
active.value = value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
changeActive,
|
||||
})
|
||||
</script>
|
121
packages/color-picker/comps/Tabs.vue
Normal file
121
packages/color-picker/comps/Tabs.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="my-tabs">
|
||||
<div class="my-tabs__header p-0.5 mb-3 rounded bg-gray-100 cursor-pointers">
|
||||
<div class="my-tabs__header-shell relative flex justify-between">
|
||||
<div v-for="(tab, index) in tabs" :key="tab.props.label" class="my-tab__title relative flex-auto py-1 text-center" :class="{ 'my-active': tab.props.label === value }" @click="onClickTab(tab, index)">
|
||||
{{ tab.props.label }}
|
||||
</div>
|
||||
|
||||
<div class="my-tab__slider" :style="sliderStyle"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-tabs__content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Tabs',
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:value', 'change'])
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
changeTab()
|
||||
},
|
||||
)
|
||||
|
||||
const tabs = ref([])
|
||||
const sliderStyle = reactive({ width: 0, left: 0 })
|
||||
let tabWidth = 0
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
tabWidth = 100 / tabs.value.length
|
||||
sliderStyle.width = `${tabWidth}%`
|
||||
|
||||
changeTab()
|
||||
})
|
||||
|
||||
let preActiveTabVM = null
|
||||
async function changeTab(index = -1) {
|
||||
if (index < 0) {
|
||||
index = tabs.value.findIndex((vm) => vm.props.label === props.value)
|
||||
}
|
||||
sliderStyle.left = `${tabWidth * index}%`
|
||||
|
||||
// 切换 tab 内容
|
||||
try {
|
||||
await nextTick()
|
||||
preActiveTabVM?.exposed?.changeActive?.(false)
|
||||
preActiveTabVM = tabs.value[index]
|
||||
preActiveTabVM.exposed?.changeActive?.(true)
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
function onClickTab(tab, index) {
|
||||
emit('update:value', tab.props.label)
|
||||
changeTab(index)
|
||||
}
|
||||
|
||||
defineExpose({ tabs })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-tabs__header {
|
||||
margin-bottom: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
padding: 0px;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
.my-tabs__header-shell {
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.my-tab__title {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
z-index: 1;
|
||||
}
|
||||
.my-tab__title.my-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.my-tab__slider {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
border-radius: 0.25rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
</style>
|
21
packages/color-picker/comps/svg.vue
Normal file
21
packages/color-picker/comps/svg.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<!--
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-05-29 15:41:22
|
||||
* @Description: 吸管 SVG
|
||||
* @LastEditors: ShawnPhang <site: m.palxp.cn>
|
||||
* @LastEditTime: 2023-05-29 15:48:17
|
||||
-->
|
||||
<template>
|
||||
<svg t="1685345224620" class="sd-xggj" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8489" width="20" height="20">
|
||||
<path d="M716.288 140.501333a42.666667 42.666667 0 0 1 60.373333 0l90.496 90.496a42.666667 42.666667 0 0 1 0 60.330667l-120.661333 120.704L595.626667 261.12l120.661333-120.661333zM520.192 185.770667l301.696 301.653333-60.330667 60.373333-301.653333-301.696 60.288-60.330666z" p-id="8490"></path>
|
||||
<path d="M580.565333 366.762667l-60.373333-60.330667-362.026667 362.026667V853.333333l181.034667-3.84 362.026667-362.026666-60.330667-60.373334-331.861333 331.904-60.373334-60.373333 331.904-331.861333z" p-id="8491"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {},
|
||||
})
|
||||
</script>
|
107
packages/color-picker/index.css
Normal file
107
packages/color-picker/index.css
Normal file
@ -0,0 +1,107 @@
|
||||
/* ! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com */
|
||||
|
||||
/*
|
||||
Some configurations are extracted, and tailwindcss are not used in this project
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
15
packages/color-picker/index.ts
Normal file
15
packages/color-picker/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-05-29 14:24:41
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <site: m.palxp.cn>
|
||||
* @LastEditTime: 2023-05-29 14:25:05
|
||||
*/
|
||||
import { App } from 'vue'
|
||||
import Comp from './index.vue'
|
||||
|
||||
Comp.install = (app: App): void => {
|
||||
app.component(Comp.name, Comp)
|
||||
}
|
||||
|
||||
export default Comp
|
729
packages/color-picker/index.vue
Normal file
729
packages/color-picker/index.vue
Normal file
@ -0,0 +1,729 @@
|
||||
<!--
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-05-26 17:42:26
|
||||
* @Description: 调色板
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2024-01-31 10:44:41
|
||||
-->
|
||||
<template>
|
||||
<div class="color-picker">
|
||||
<Tabs v-if="modes.length > 1" :value="mode" @update:value="onChangeMode">
|
||||
<TabPanel v-for="label in modes" :key="label" :label="label"> </TabPanel>
|
||||
</Tabs>
|
||||
<div v-else class="title">{{ mode }}</div>
|
||||
|
||||
<template v-if="showGradient">
|
||||
<div v-show="mode === '渐变'" class="cp__gradient flex-center">
|
||||
<div class="cp__gradient-bar">
|
||||
<div ref="elGradientTrack" class="cpgb__track" style="width: 100%" :style="{ background: value }">
|
||||
<!-- tabindex="-1" 是元素可以触发 keydown 事件 -->
|
||||
<div
|
||||
v-for="(gradient, index) in gradients"
|
||||
:key="index"
|
||||
:class="[
|
||||
'cpgb__pointer',
|
||||
{
|
||||
'cpgb__pointer--active': gradient === activeGradient,
|
||||
},
|
||||
]"
|
||||
:data-sort="index"
|
||||
:style="{
|
||||
left: `${gradient.offset * 100}%`,
|
||||
background: gradient.color,
|
||||
}"
|
||||
tabindex="-1"
|
||||
@mousedown="onMousedownGradientPointer(gradient)"
|
||||
@keydown.stop="onKeyupGradientPointer"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<AngleHandleVue v-model="angle" @change="angleChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="elPalette" class="cp__palette" :style="{ background: paletteBackground }">
|
||||
<div class="cpp__color-saturation"></div>
|
||||
<div class="cpp__color-value"></div>
|
||||
<div ref="elPalettePointer" class="cpp__pointer"></div>
|
||||
</div>
|
||||
|
||||
<div ref="elSliderHux" class="cp__slider cp__slider-hux">
|
||||
<div class="cps__track">
|
||||
<div ref="elSliderHuxPointer" class="cpst__pointer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="elSliderAlpha" class="cp__slider cp__slider-alpha">
|
||||
<div class="cpsa__background" :style="sliderAlphaBackgroundStyle"></div>
|
||||
<div class="cps__track">
|
||||
<div ref="elSliderAlphaPointer" class="cpst__pointer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cp__box">
|
||||
<div class="item" @click="onClickStraw">
|
||||
<xiguan v-if="hasEyeDrop" />
|
||||
<input v-else class="native" type="color" @input="onClickStraw" />
|
||||
</div>
|
||||
<!-- <input :value="value" @input="$emit('update:value', $event.target.value)" class="input" /> -->
|
||||
<input v-if="mode === '渐变'" class="input" :value="activeGradient.color" />
|
||||
<input v-else :value="value" class="input" @blur="onInputBlur" />
|
||||
<template v-if="mode === '纯色'">
|
||||
<div v-for="pc in predefine" :key="pc" class="item item-color" :style="{ background: pc }" @click="onClickStraw({ target: { value: pc } })"></div>
|
||||
</template>
|
||||
<!-- <input :value="alpha" class="w-12" size="small" :min="0" :max="100" @input="onChangeAlpha" @change="onChangeAlpha" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import './index.css'
|
||||
export default {
|
||||
name: 'ColorPicker',
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { registerMoveableElement } from './utils/moveable.ts'
|
||||
import { hexA2HSLA, HSLA2HexA, hex2RGB, RGB2HSL, hexA2RGBA, RGBA2HexA } from './utils/color.ts'
|
||||
import { toGradientString, parseBackgroundValue, toolTip } from './utils/helper.ts'
|
||||
import Tabs from './comps/Tabs.vue'
|
||||
import xiguan from './comps/svg.vue'
|
||||
import TabPanel from './comps/TabPanel.vue'
|
||||
import { debounce } from 'throttle-debounce'
|
||||
import AngleHandleVue from './comps/AngleHandle.vue'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '#ffffffff',
|
||||
},
|
||||
|
||||
modes: {
|
||||
type: Array,
|
||||
default: () => ['纯色', '渐变'], // 图案
|
||||
},
|
||||
|
||||
defaultColor: {
|
||||
type: String,
|
||||
default: '#ffffffff',
|
||||
},
|
||||
|
||||
defaultGradient: {
|
||||
type: String,
|
||||
default: 'linear-gradient(90deg, #fffae0ff 0%, #ffd1f1ff 100%)',
|
||||
},
|
||||
|
||||
defaultImage: {
|
||||
type: String,
|
||||
default: 'https://st0.dancf.com/csc/157/material-2d-textures/0/20190714-174653-ed3c.jpg',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:value', 'change', 'native-pick', 'blur'])
|
||||
|
||||
const mode = ref(parseBackgroundValue(props.value)) // 颜色、渐变、图片
|
||||
const angle = ref(90)
|
||||
const gradients = ref([])
|
||||
const hsla = reactive({ h: 0, s: 0, l: 0, a: 0 })
|
||||
const paletteBackground = ref('#f00')
|
||||
const hex = ref('#000')
|
||||
const alpha = ref(0)
|
||||
let activeGradient = ref({})
|
||||
const hasEyeDrop = 'EyeDropper' in window
|
||||
|
||||
const elGradientTrack = ref()
|
||||
const elPalette = ref()
|
||||
const elPalettePointer = ref()
|
||||
const elSliderHuxPointer = ref()
|
||||
const elSliderHux = ref()
|
||||
const elSliderAlphaPointer = ref()
|
||||
const elSliderAlpha = ref()
|
||||
// const elStrawCanvas = ref();
|
||||
|
||||
let gradientMoveable = null
|
||||
let paletteMoveable = null
|
||||
let sliderHuxMoveable = null
|
||||
let sliderAlphaMoveable = null
|
||||
let mousedownGradientPointer = null
|
||||
let backendHex = null
|
||||
// 是否可以改变 palette sliderHux sliderAlpha 的 pointer 位置
|
||||
let canChangeHSLAPointerPos = true
|
||||
let canChangeHSLAPointerPosTimer = null
|
||||
|
||||
const predefine = ref([]) // 历史记录
|
||||
|
||||
const record = {
|
||||
color: props.defaultColor,
|
||||
gradient: props.defaultGradient,
|
||||
image: props.defaultImage,
|
||||
}
|
||||
|
||||
const showGradient = computed(() => {
|
||||
return props.modes.includes('渐变')
|
||||
})
|
||||
|
||||
const sliderAlphaBackgroundStyle = computed(() => {
|
||||
const rgb = hex2RGB(hex.value).join(',')
|
||||
return {
|
||||
background: `linear-gradient(to right, rgba(${rgb}, 0) 0%, rgb(${rgb}) 100%)`,
|
||||
}
|
||||
})
|
||||
|
||||
watch(activeGradient, (value) => {
|
||||
setColor(value.color)
|
||||
})
|
||||
|
||||
watch(hex, (value) => {
|
||||
onChangeHex(value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
const _mode = parseBackgroundValue(value)
|
||||
if (_mode !== mode.value) {
|
||||
mode.value = _mode
|
||||
}
|
||||
changeMode(_mode)
|
||||
recordValue(value)
|
||||
addHistory(value)
|
||||
},
|
||||
)
|
||||
|
||||
// TODO: 添加选择历史记录
|
||||
const addHistory = debounce(300, async (value) => {
|
||||
const history = predefine.value
|
||||
// 如果已经存在就提到前面来,避免重复
|
||||
const index = history.indexOf(value)
|
||||
if (index !== -1) {
|
||||
predefine.value.splice(index, 1)
|
||||
}
|
||||
if (history.length >= 4) {
|
||||
predefine.value.splice(history.length - 1, 1)
|
||||
}
|
||||
// 把最新的颜色放在头部
|
||||
const head = [value]
|
||||
predefine.value = head.concat(history)
|
||||
})
|
||||
|
||||
const unwatchHSLA = watch(hsla, onChangeHSLA, { deep: true })
|
||||
function onChangeHSLA(newHsla) {
|
||||
const hexA = HSLA2HexA(...Object.values(newHsla))
|
||||
|
||||
let value
|
||||
if (mode.value === '纯色') {
|
||||
value = hexA
|
||||
} else if (mode.value === '渐变') {
|
||||
activeGradient.value.color = hexA
|
||||
value = toGradientString(angle.value, gradients.value)
|
||||
}
|
||||
updateColorData(hexA)
|
||||
updateValue(value)
|
||||
}
|
||||
|
||||
onMounted(onMountedCallback)
|
||||
async function onMountedCallback() {
|
||||
elPalettePointer.value.style.left = `${hsla.s}%`
|
||||
elPalettePointer.value.style.top = `${100 - hsla.l}%`
|
||||
elSliderHuxPointer.value.style.left = `${(hsla.h / 360) * 100}%`
|
||||
elSliderAlphaPointer.value.style.left = `${hsla.a * 100}%`
|
||||
|
||||
if (showGradient.value) {
|
||||
gradientMoveable = registerMoveableElement(elGradientTrack.value, {
|
||||
onmousedown: onMousedownGradient,
|
||||
onmousemove: onMousemoveGradient,
|
||||
onmouseup: onMouseupGradient,
|
||||
})
|
||||
}
|
||||
|
||||
function onMousedownGradient(position) {
|
||||
if (mousedownGradientPointer) {
|
||||
return
|
||||
}
|
||||
|
||||
const index = gradients.value.findIndex((stop) => stop.offset >= position.x)
|
||||
const start = gradients.value[index - 1]
|
||||
const startRGBA = hexA2RGBA(start.color)
|
||||
const end = gradients.value[index]
|
||||
const endRGBA = hexA2RGBA(end.color)
|
||||
|
||||
const rgb = []
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
rgb.push(startRGBA[i] + (endRGBA[i] - startRGBA[i]) * position.x)
|
||||
}
|
||||
|
||||
const a = end.offset - position.x - (position.x - start.offset) > 0 ? startRGBA[3] : endRGBA[3]
|
||||
|
||||
const color = RGBA2HexA(...rgb, a)
|
||||
activeGradient.value = {
|
||||
color,
|
||||
offset: position.x,
|
||||
}
|
||||
|
||||
gradients.value.splice(index, 0, activeGradient.value)
|
||||
}
|
||||
|
||||
function onMousemoveGradient(position) {
|
||||
if (!mousedownGradientPointer) return
|
||||
|
||||
activeGradient.value.offset = position.x
|
||||
gradients.value.sort((a, b) => a.offset - b.offset)
|
||||
|
||||
const value = toGradientString(angle.value, gradients.value)
|
||||
updateValue(value)
|
||||
}
|
||||
|
||||
function onMouseupGradient() {
|
||||
mousedownGradientPointer = false
|
||||
}
|
||||
|
||||
paletteMoveable = registerMoveableElement(elPalette.value, {
|
||||
onmousemove: onChangeSL,
|
||||
onmouseup: onChangeSL,
|
||||
})
|
||||
|
||||
function onChangeSL(position) {
|
||||
disableChangeHSLA()
|
||||
|
||||
const x = position.x * 100
|
||||
const y = position.y * 100
|
||||
|
||||
hsla.s = Math.round(x)
|
||||
hsla.l = Math.round(100 - y)
|
||||
|
||||
elPalettePointer.value.style.left = `${x}%`
|
||||
elPalettePointer.value.style.top = `${y}%`
|
||||
}
|
||||
|
||||
sliderHuxMoveable = registerMoveableElement(elSliderHux.value, {
|
||||
onmousemove: onChangeHux,
|
||||
onmouseup: onChangeHux,
|
||||
})
|
||||
|
||||
function onChangeHux(position) {
|
||||
disableChangeHSLA()
|
||||
|
||||
hsla.h = position.x * 360
|
||||
elSliderHuxPointer.value.style.left = `${position.x * 100}%`
|
||||
}
|
||||
|
||||
sliderAlphaMoveable = registerMoveableElement(elSliderAlpha.value, {
|
||||
onmousemove: onChangeAlpha,
|
||||
onmouseup: onChangeAlpha,
|
||||
})
|
||||
|
||||
function onChangeAlpha(position) {
|
||||
disableChangeHSLA()
|
||||
|
||||
hsla.a = position.x
|
||||
elSliderAlphaPointer.value.style.left = `${position.x * 100}%`
|
||||
}
|
||||
|
||||
changeMode(mode.value)
|
||||
recordValue(props.value)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
paletteMoveable?.destroy()
|
||||
sliderHuxMoveable?.destroy()
|
||||
sliderAlphaMoveable?.destroy()
|
||||
unwatchHSLA()
|
||||
|
||||
if (gradientMoveable) {
|
||||
gradientMoveable.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
function recordValue(value) {
|
||||
if (mode.value === '纯色') {
|
||||
record.color = value
|
||||
} else if (mode.value === '渐变') {
|
||||
record.gradient = value
|
||||
} else if (mode.value === '图案') {
|
||||
record.image = value
|
||||
}
|
||||
}
|
||||
|
||||
function updateValue(value) {
|
||||
if (value === props.value) return
|
||||
|
||||
recordValue(value)
|
||||
emit('update:value', value)
|
||||
|
||||
emit('change', {
|
||||
mode: mode.value,
|
||||
color: value,
|
||||
angle: Number(angle.value),
|
||||
stops: gradients.value,
|
||||
})
|
||||
}
|
||||
|
||||
async function onChangeMode(value) {
|
||||
if (value === mode.value) return
|
||||
mode.value = value
|
||||
|
||||
let color
|
||||
if (value === '纯色') {
|
||||
color = record.color
|
||||
} else if (value === '渐变') {
|
||||
color = record.gradient
|
||||
} else if (value === '图案') {
|
||||
color = record.image
|
||||
}
|
||||
updateValue(color)
|
||||
}
|
||||
|
||||
function changeMode(mode) {
|
||||
if (mode === '纯色') {
|
||||
setColor(props.value)
|
||||
} else if (mode === '渐变') {
|
||||
if (gradients.value.length === 0) {
|
||||
props.value.match(/[^,]+/g).forEach((item, index) => {
|
||||
if (index === 0) {
|
||||
angle.value = Number(item.match(/\d+/)[0])
|
||||
return
|
||||
}
|
||||
|
||||
let [color, offset] = item.trim().split(' ')
|
||||
if (!color.startsWith('#')) color = RGBA2HexA(color)
|
||||
|
||||
offset = offset.match(/\d+/)[0] / 100
|
||||
gradients.value.push({ color, offset })
|
||||
activeGradient.value = gradients.value[0]
|
||||
})
|
||||
} else {
|
||||
setColor(activeGradient.value.color)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 图案
|
||||
}
|
||||
|
||||
function updateColorData(hexA) {
|
||||
paletteBackground.value = `hsl(${hsla.h}, 100%, 50%)`
|
||||
hex.value = hexA.slice(0, 7)
|
||||
backendHex = hex.value
|
||||
alpha.value = Math.round((hsla.a ?? 1) * 100)
|
||||
}
|
||||
|
||||
function setColor(color) {
|
||||
// 通过 palette sliderHux sliderAlpha 交互改变 pointer 位置
|
||||
// 已经改变 hsla 的值并触发 update:value
|
||||
// watch props.value 再调用当前方法时无需再更新 hsla
|
||||
if (canChangeHSLAPointerPos) {
|
||||
const _hsla = hexA2HSLA(color)
|
||||
hsla.h = _hsla[0]
|
||||
hsla.s = _hsla[1]
|
||||
hsla.l = _hsla[2]
|
||||
hsla.a = _hsla[3]
|
||||
|
||||
updateColorData(color)
|
||||
|
||||
let x = hsla.s
|
||||
const y = Math.round(100 - hsla.l)
|
||||
elPalettePointer.value.style.left = `${x}%`
|
||||
elPalettePointer.value.style.top = `${y}%`
|
||||
|
||||
x = hsla.h / 360
|
||||
elSliderHuxPointer.value.style.left = `${x * 100}%`
|
||||
|
||||
elSliderAlphaPointer.value.style.left = `${hsla.a * 100}%`
|
||||
}
|
||||
}
|
||||
|
||||
function onMousedownGradientPointer(stop) {
|
||||
mousedownGradientPointer = true
|
||||
activeGradient.value = stop
|
||||
}
|
||||
|
||||
function onKeyupGradientPointer(event) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
if (!['Backspace', 'Delete'].includes(event.key)) return
|
||||
if (gradients.value.length === 2) return
|
||||
|
||||
const index = gradients.value.indexOf(activeGradient.value)
|
||||
gradients.value.splice(index, 1)
|
||||
activeGradient.value = gradients.value[0]
|
||||
}
|
||||
|
||||
function onChangeHex(value) {
|
||||
if (/^#(?:[0-9a-f]{3}){1,2}$/i.test(value)) {
|
||||
const rgb = hex2RGB(value)
|
||||
const [h, s, l] = RGB2HSL(...rgb)
|
||||
hsla.h = h
|
||||
hsla.s = s
|
||||
hsla.l = l
|
||||
|
||||
elPalettePointer.value.style.left = `${hsla.s}%`
|
||||
elPalettePointer.value.style.top = `${100 - hsla.l}%`
|
||||
elSliderHuxPointer.value.style.left = `${(hsla.h / 360) * 100}%`
|
||||
|
||||
hex.value = value
|
||||
} else {
|
||||
// hex.value = backendHex
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeAlpha(value) {
|
||||
hsla.a = value / 100
|
||||
elSliderAlphaPointer.value.style.left = `${value}%`
|
||||
}
|
||||
|
||||
function disableChangeHSLA() {
|
||||
canChangeHSLAPointerPos = false
|
||||
|
||||
if (canChangeHSLAPointerPosTimer) clearTimeout(canChangeHSLAPointerPosTimer)
|
||||
canChangeHSLAPointerPosTimer = setTimeout(() => {
|
||||
canChangeHSLAPointerPos = true
|
||||
}, 16)
|
||||
}
|
||||
|
||||
async function onClickStraw(val) {
|
||||
let result = ''
|
||||
if (val && val.target.value) {
|
||||
const color = val.target.value
|
||||
result = color + (color.length === 7 ? 'ff' : '')
|
||||
} else {
|
||||
const eyeDropper = new window.EyeDropper() // 初始化一个EyeDropper对象
|
||||
toolTip('按Esc可退出')
|
||||
try {
|
||||
const drop = await eyeDropper.open() // 开始拾取颜色
|
||||
const colorHexValue = drop.sRGBHex
|
||||
result = colorHexValue + 'ff'
|
||||
} catch (e) {
|
||||
console.log('用户取消了取色')
|
||||
}
|
||||
}
|
||||
if (mode.value === '渐变') {
|
||||
activeGradient.value.color = result
|
||||
activeGradient.value = { ...activeGradient.value }
|
||||
} else {
|
||||
emit('update:value', result)
|
||||
}
|
||||
emit('native-pick', result)
|
||||
}
|
||||
|
||||
const onInputBlur = (e) => {
|
||||
const fixColor = patchHexColor(e.target.value)
|
||||
emit('blur', fixColor)
|
||||
emit('update:value', fixColor)
|
||||
}
|
||||
|
||||
function patchHexColor(str) {
|
||||
let hex = str.replace(/\s/g, '') // 移除空格
|
||||
if (!str.startsWith('#')) {
|
||||
hex = '#' + hex
|
||||
}
|
||||
if (hex.length < 9) {
|
||||
hex = hex.padEnd(9, 'f')
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
function angleChange() {
|
||||
updateValue(toGradientString(angle.value, gradients.value))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.color-picker {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.cp__gradient {
|
||||
&-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.cpgb__track {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cpgb__pointer {
|
||||
cursor: grab;
|
||||
position: absolute;
|
||||
top: -0px;
|
||||
top: -0.125rem;
|
||||
height: 1.25rem;
|
||||
--tw-translate-x: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(255 255 255 / var(--tw-border-opacity));
|
||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
width: 18px;
|
||||
|
||||
&--active {
|
||||
z-index: 1;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 4px 0 rgb(0 0 0 / 20%), 0 0 0 1.2px #2254f4;
|
||||
}
|
||||
}
|
||||
|
||||
.cp__palette {
|
||||
height: 140px;
|
||||
position: relative;
|
||||
margin-top: 0.75rem;
|
||||
margin-top: 0.875rem;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
border-radius: 0.25rem;
|
||||
.cpp__color-saturation,
|
||||
.cpp__color-value {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cpp__color-saturation {
|
||||
background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
||||
--tw-gradient-from: #fff var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.cpp__color-value {
|
||||
background-image: linear-gradient(to top, var(--tw-gradient-stops));
|
||||
--tw-gradient-from: #000 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.cpp__pointer {
|
||||
position: absolute;
|
||||
height: 0.75rem;
|
||||
width: 0.75rem;
|
||||
--tw-translate-x: -0.25rem;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
--tw-translate-x: -0.375rem;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
--tw-translate-y: -0.25rem;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
border-radius: 9999px;
|
||||
border-width: 2px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(255 255 255 / var(--tw-border-opacity));
|
||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.cp__slider {
|
||||
position: relative;
|
||||
margin-top: 0.75rem;
|
||||
margin-top: 0.875rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
&-hux {
|
||||
background: linear-gradient(90deg, red 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red);
|
||||
}
|
||||
|
||||
&-alpha {
|
||||
background: linear-gradient(to top right, hsla(0, 0%, 80%, 0.4) 25%, transparent 0, transparent 75%, hsla(0, 0%, 80%, 0.4) 0, hsla(0, 0%, 80%, 0.4)), linear-gradient(to top right, hsla(0, 0%, 80%, 0.4) 25%, transparent 0, transparent 75%, hsla(0, 0%, 80%, 0.4) 0, hsla(0, 0%, 80%, 0.4));
|
||||
background-size: 6px 6px;
|
||||
background-position: 0 0, 3px 3px;
|
||||
}
|
||||
|
||||
.cpsa__background {
|
||||
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 6%);
|
||||
height: 100%;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
.cp__box {
|
||||
margin-top: 0.75rem;
|
||||
margin-top: 0.875rem;
|
||||
display: flex;
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-left: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.item-color {
|
||||
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 6%);
|
||||
}
|
||||
.item:first-of-type {
|
||||
margin: 0;
|
||||
}
|
||||
.item:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.input {
|
||||
width: 4.7rem;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.native {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cps__track {
|
||||
position: absolute;
|
||||
left: 0.25rem;
|
||||
right: 0.25rem;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.cpst__pointer {
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 2px rgb(0 0 0 / 60%);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
box-sizing: content-box;
|
||||
height: 0.5rem;
|
||||
width: 0.5rem;
|
||||
--tw-translate-x: -0.5rem;
|
||||
--tw-translate-y: -0.25rem;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
border-radius: 9999px;
|
||||
border-width: 4px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(255 255 255 / var(--tw-border-opacity));
|
||||
}
|
||||
</style>
|
28
packages/color-picker/package.json
Normal file
28
packages/color-picker/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@palxp/color-picker",
|
||||
"version": "1.5.5",
|
||||
"description": "TODO",
|
||||
"author": "ShawnPhang <palxiao@vip.qq.com>",
|
||||
"homepage": "https://fe-doc.palxp.cn/#/color-picker",
|
||||
"license": "ISC",
|
||||
"main": "index.ts",
|
||||
"module": "index.ts",
|
||||
"types": "index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/palxiao/front-end-arsenal.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/palxiao/front-end-arsenal/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"throttle-debounce": "^5.0.0"
|
||||
},
|
||||
"gitHead": "fcba4b7113a557f245ea8e8e64170d93bbbe1e57"
|
||||
}
|
130
packages/color-picker/utils/color.ts
Normal file
130
packages/color-picker/utils/color.ts
Normal file
@ -0,0 +1,130 @@
|
||||
export const RGB2Hex = (r: number, g: number, b: number) => {
|
||||
let _r = Math.round(r).toString(16)
|
||||
let _g = Math.round(g).toString(16)
|
||||
let _b = Math.round(b).toString(16)
|
||||
|
||||
if (_r.length === 1) _r = '0' + _r
|
||||
if (_g.length === 1) _g = '0' + _g
|
||||
if (_b.length === 1) _b = '0' + _b
|
||||
|
||||
return '#' + _r + _g + _b
|
||||
}
|
||||
|
||||
export const RGBA2HexA = (r: number, g: number, b: number, a = 1) => {
|
||||
const hex = RGB2Hex(r, g, b)
|
||||
|
||||
let _a = Math.round((a as number) * 255).toString(16)
|
||||
if (_a.length === 1) _a = '0' + _a
|
||||
|
||||
return hex + _a
|
||||
}
|
||||
|
||||
export const RGB2HSL = (r: number, g: number, b: number) => {
|
||||
r /= 255
|
||||
g /= 255
|
||||
b /= 255
|
||||
|
||||
const minVal = Math.min(r, g, b)
|
||||
const maxVal = Math.max(r, g, b)
|
||||
const delta = maxVal - minVal
|
||||
|
||||
let h = 0
|
||||
let s = 0
|
||||
const l = maxVal
|
||||
if (delta === 0) {
|
||||
h = s = 0
|
||||
} else {
|
||||
s = delta / maxVal
|
||||
const dr = ((maxVal - r) / 6 + delta / 2) / delta
|
||||
const dg = ((maxVal - g) / 6 + delta / 2) / delta
|
||||
const db = ((maxVal - b) / 6 + delta / 2) / delta
|
||||
|
||||
if (r === maxVal) {
|
||||
h = db - dg
|
||||
} else if (g === maxVal) {
|
||||
h = 1 / 3 + dr - db
|
||||
} else if (b === maxVal) {
|
||||
h = 2 / 3 + dg - dr
|
||||
}
|
||||
|
||||
if (h < 0) {
|
||||
h += 1
|
||||
} else if (h > 1) {
|
||||
h -= 1
|
||||
}
|
||||
}
|
||||
|
||||
return [h * 360, s * 100, l * 100]
|
||||
}
|
||||
|
||||
export const RGBA2HSLA = (r: number, g: number, b: number, a = 1) => [...RGB2HSL(r, g, b), a]
|
||||
|
||||
export function HSL2RGB(h: number, s: number, l: number) {
|
||||
h = (h / 360) * 6
|
||||
s /= 100
|
||||
l /= 100
|
||||
|
||||
const i = Math.floor(h)
|
||||
|
||||
const f = h - i
|
||||
const p = l * (1 - s)
|
||||
const q = l * (1 - f * s)
|
||||
const t = l * (1 - (1 - f) * s)
|
||||
|
||||
const mod = i % 6
|
||||
const r = [l, q, p, p, t, l][mod]
|
||||
const g = [t, l, l, q, p, p][mod]
|
||||
const b = [p, p, t, l, l, q][mod]
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]
|
||||
}
|
||||
|
||||
export const HSLA2RGBA = (h: number, s: number, l: number, a = 1) => [...HSL2RGB(h, s, l), a]
|
||||
|
||||
export const HSL2Hex = (h: number, s: number, l: number) => {
|
||||
const [r, g, b] = HSL2RGB(h, s, l)
|
||||
return RGB2Hex(r, g, b)
|
||||
}
|
||||
|
||||
export const HSLA2HexA = (h: number, s: number, l: number, a = 1) => {
|
||||
const hex = HSL2Hex(h, s, l)
|
||||
return `${hex}${a === 0 ? '00' : Math.round(a * 255).toString(16)}`
|
||||
}
|
||||
|
||||
export const hex2RGB = (hex: string) => {
|
||||
hex = hex.slice(0, 7)
|
||||
|
||||
let r = 0
|
||||
let g = 0
|
||||
let b = 0
|
||||
|
||||
if (hex.length == 4) {
|
||||
// 3 digits
|
||||
r = Number('0x' + hex[1] + hex[1])
|
||||
g = Number('0x' + hex[2] + hex[2])
|
||||
b = Number('0x' + hex[3] + hex[3])
|
||||
} else if (hex.length == 7) {
|
||||
// 6 digits
|
||||
r = Number('0x' + hex[1] + hex[2])
|
||||
g = Number('0x' + hex[3] + hex[4])
|
||||
b = Number('0x' + hex[5] + hex[6])
|
||||
}
|
||||
|
||||
return [r, g, b]
|
||||
}
|
||||
|
||||
export const hexA2RGBA = (hexA: string) => {
|
||||
const rgb = hex2RGB(hexA)
|
||||
const a = Number('0x' + hexA[7] + hexA[8])
|
||||
return [...rgb, Number((a / 255).toFixed(2))]
|
||||
}
|
||||
|
||||
export const hex2HSL = (hex: string) => {
|
||||
const [r, g, b] = hex2RGB(hex)
|
||||
return RGB2HSL(r, g, b)
|
||||
}
|
||||
|
||||
export const hexA2HSLA = (hexA: string) => {
|
||||
const [r, g, b, a] = hexA2RGBA(hexA)
|
||||
return RGBA2HSLA(r, g, b, a)
|
||||
}
|
58
packages/color-picker/utils/helper.ts
Normal file
58
packages/color-picker/utils/helper.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-04-26 11:30:10
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-11-28 11:03:14
|
||||
*/
|
||||
export const parseBackgroundValue = (value: string): string => {
|
||||
if (value.startsWith('#')) return '纯色'
|
||||
if (value.startsWith('linear-gradient')) return '渐变'
|
||||
return '图案'
|
||||
}
|
||||
|
||||
interface Stop {
|
||||
color: string
|
||||
offset: number
|
||||
}
|
||||
|
||||
export const toGradientString = (angle: number, stops: Stop[]) => {
|
||||
const s: string[] = []
|
||||
stops.forEach((stop) => {
|
||||
s.push(`${stop.color} ${stop.offset * 100}%`)
|
||||
})
|
||||
return `linear-gradient(${angle}deg, ${s.join(',')})`
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示全局提示
|
||||
* @param content
|
||||
* @param tooltipVisible
|
||||
* @returns
|
||||
*/
|
||||
export function toolTip(content: string) {
|
||||
const tooltip = drawTooltip(content)
|
||||
document.body.appendChild(tooltip)
|
||||
setTimeout(() => tooltip?.parentNode?.removeChild(tooltip), 2000)
|
||||
}
|
||||
|
||||
function drawTooltip(content: string, tooltipVisible = true) {
|
||||
const tooltip: any = document.createElement('div')
|
||||
tooltip.id = 'color-pipette-tooltip-container'
|
||||
tooltip.innerHTML = content
|
||||
tooltip.style = `
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 9%;
|
||||
z-index: 10002;
|
||||
display: ${tooltipVisible ? 'flex' : 'none'};
|
||||
align-items: center;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
pointer-events: none;
|
||||
`
|
||||
return tooltip
|
||||
}
|
70
packages/color-picker/utils/moveable.ts
Normal file
70
packages/color-picker/utils/moveable.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { toNumber } from './tool'
|
||||
|
||||
interface Position {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface RegisterMoveablePanelOptions {
|
||||
wrapEl?: HTMLElement
|
||||
onmousedown?(position: Position, event: MouseEvent): void
|
||||
onmousemove?(position: Position, event: MouseEvent): void
|
||||
onmouseup?(position: Position, event: MouseEvent): void
|
||||
}
|
||||
|
||||
export const registerMoveableElement = (el: HTMLElement, { onmousedown, onmousemove, onmouseup }: RegisterMoveablePanelOptions = {}) => {
|
||||
let elRect = el.getBoundingClientRect()
|
||||
const position = { x: 0, y: 0 }
|
||||
|
||||
const update = (event: MouseEvent) => {
|
||||
let dx = event.pageX - elRect.x
|
||||
let dy = event.pageY - elRect.y
|
||||
|
||||
if (dx < 0) dx = 0
|
||||
if (dx > elRect.width) dx = elRect.width
|
||||
if (dy < 0) dy = 0
|
||||
if (dy > elRect.height) dy = elRect.height
|
||||
|
||||
position.x = toNumber(dx / elRect.width, { decimal: 2 })
|
||||
position.y = toNumber(dy / elRect.height, { decimal: 2 })
|
||||
}
|
||||
|
||||
const _onmousemove = (event: MouseEvent) => {
|
||||
update(event)
|
||||
|
||||
if (onmousemove) {
|
||||
onmousemove(position, event)
|
||||
}
|
||||
}
|
||||
|
||||
const _onmouseup = (event: MouseEvent) => {
|
||||
document.removeEventListener('mousemove', _onmousemove)
|
||||
document.removeEventListener('mouseup', _onmouseup)
|
||||
|
||||
if (onmouseup) {
|
||||
onmouseup(position, event)
|
||||
}
|
||||
}
|
||||
|
||||
const _onmousedown = (event: MouseEvent) => {
|
||||
// elRect 可能不准确,这里更新一下
|
||||
elRect = el.getBoundingClientRect()
|
||||
|
||||
update(event)
|
||||
|
||||
document.addEventListener('mousemove', _onmousemove)
|
||||
document.addEventListener('mouseup', _onmouseup)
|
||||
|
||||
if (onmousedown) {
|
||||
onmousedown(position, event)
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener('mousedown', _onmousedown)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
el.removeEventListener('mousedown', _onmousedown)
|
||||
},
|
||||
}
|
||||
}
|
6
packages/color-picker/utils/tool.ts
Normal file
6
packages/color-picker/utils/tool.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const toNumber = (n: number, { decimal = 0 } = {}) => {
|
||||
if (decimal > 0) {
|
||||
return Number(n.toFixed(decimal))
|
||||
}
|
||||
return Math.round(n)
|
||||
}
|
68
packages/image-extraction/CHANGELOG.md
Normal file
68
packages/image-extraction/CHANGELOG.md
Normal file
@ -0,0 +1,68 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.4](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.2.3...@palxp/image-extraction@1.2.4) (2023-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **component:** Matting类型导出 ([02cdcf7](https://github.com/palxiao/front-end-arsenal/commit/02cdcf74dfddcd0e1cd0353f287eacb49d0c3db4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.3](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.2.2...@palxp/image-extraction@1.2.3) (2023-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **component:** Matting类型导出 ([21aa8ad](https://github.com/palxiao/front-end-arsenal/commit/21aa8ad75021056913c0e2cc548c15a073d79e5b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.2](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.2.1...@palxp/image-extraction@1.2.2) (2023-10-08)
|
||||
|
||||
**Note:** Version bump only for package @palxp/image-extraction
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.1](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.2.0...@palxp/image-extraction@1.2.1) (2023-10-08)
|
||||
|
||||
**Note:** Version bump only for package @palxp/image-extraction
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.2.0](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.1.1...@palxp/image-extraction@1.2.0) (2023-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **component:** 增加隐藏头部 ([406f1c5](https://github.com/palxiao/front-end-arsenal/commit/406f1c5ea0e38489e91a6b36982b773e5aad42d6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.1](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.1.0...@palxp/image-extraction@1.1.1) (2023-10-08)
|
||||
|
||||
**Note:** Version bump only for package @palxp/image-extraction
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 1.1.0 (2023-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **custom:** 增加Matting组件 ([b26b4dd](https://github.com/palxiao/front-end-arsenal/commit/b26b4dddd11a273adeb97104a6aa2707fb8be920))
|
342
packages/image-extraction/ImageExtraction.vue
Normal file
342
packages/image-extraction/ImageExtraction.vue
Normal file
@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div v-if="hasHeader" class="options-container">
|
||||
<slot>
|
||||
<div class="default-wrap">
|
||||
<div class="option">
|
||||
<label for="image">选择图片:</label>
|
||||
<input id="image" type="file" accept=".jpg,.png,.gif,.webp" @change="onFileChange" />
|
||||
</div>
|
||||
<div class="option">
|
||||
<span>画笔类型:</span>
|
||||
<label for="fix">修补</label>
|
||||
<input id="fix" v-model="isErasing" type="radio" :value="false" />
|
||||
<label for="matting">擦除</label>
|
||||
<input id="matting" v-model="isErasing" :value="true" type="radio" />
|
||||
</div>
|
||||
<div class="option">
|
||||
<label for="radius">画笔尺寸:</label>
|
||||
<input id="radius" v-model="radius" class="range-input" type="range" :max="RADIUS_SLIDER_MAX" :min="RADIUS_SLIDER_MIN" :step="RADIUS_SLIDER_STEP" />
|
||||
<span>{{ brushSize }}</span>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label for="hardness">画笔硬度:</label>
|
||||
<input id="hardness" v-model="hardness" class="range-input" type="range" :max="HARDNESS_SLIDER_MAX" :min="HARDNESS_SLIDER_MIN" :step="HARDNESS_SLIDER_STEP" />
|
||||
<span>{{ hardnessText }}</span>
|
||||
</div>
|
||||
<button :class="saveBtnClass" :disabled="cantSave" @click="onDownloadResult">{{ saveBtnText }}</button>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="board-container">
|
||||
<div class="matting-wrapper">
|
||||
<canvas ref="inputCvs" class="matting-board"></canvas>
|
||||
<img ref="inputCursor" class="matting-cursor" :style="mattingCursorStyle" :src="cursorImage" />
|
||||
</div>
|
||||
<div class="matting-wrapper">
|
||||
<canvas ref="outputCvs" class="result-board"></canvas>
|
||||
<img class="matting-cursor" :style="mattingCursorStyle" :src="cursorImage" />
|
||||
</div>
|
||||
</div>
|
||||
<a ref="resultLink" :href="resultURL" :download="downloadFileName"></a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { useMatting, useMattingBoard } from './composables/use-matting'
|
||||
import useMattingCursor from './composables/use-matting-cursor'
|
||||
import { RADIUS_SLIDER_MIN, RADIUS_SLIDER_MAX, RADIUS_SLIDER_STEP, HARDNESS_SLIDER_MAX, HARDNESS_SLIDER_STEP, HARDNESS_SLIDER_MIN, EventType, DEFAULT_MASK_COLOR } from './constants'
|
||||
import { ref, onMounted, Ref, computed, nextTick, watch, onUnmounted } from 'vue'
|
||||
import { generateResultImageURL, getLoadedImage } from './helpers/dom-helper'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
// 组件内是否存在头部工具栏
|
||||
hasHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['register'],
|
||||
setup(props, { emit }) {
|
||||
const inputCvs: Ref<null | HTMLCanvasElement> = ref(null)
|
||||
const outputCvs: Ref<null | HTMLCanvasElement> = ref(null)
|
||||
const resultURL: Ref<string> = ref('')
|
||||
const resultLink: Ref<null | HTMLAnchorElement> = ref(null)
|
||||
const generating: Ref<boolean> = ref(false)
|
||||
const { picFile, isErasing, radius, hardness, brushSize, hardnessText } = useMatting()
|
||||
const { width, height, inputCtx, inputHiddenCtx, outputCtx, outputHiddenCtx, draggingInputBoard, initialized, mattingSources, transformConfig, inputDrawingCtx } = useMattingBoard({ picFile, isErasing, radius, hardness })
|
||||
const { cursorImage, mattingCursorStyle, renderOutputCursor } = useMattingCursor({
|
||||
inputCtx,
|
||||
isDragging: draggingInputBoard,
|
||||
isErasing,
|
||||
radius,
|
||||
hardness,
|
||||
})
|
||||
const downloadFileName = computed(() => (picFile.value ? `matting_${picFile.value.name}` : 'null'))
|
||||
const cantSave = computed(() => generating.value || !initialized.value)
|
||||
const saveBtnClass = computed(() => ({ 'save-btn': true, disabled: cantSave.value }))
|
||||
const saveBtnText = computed(() => (generating.value ? '保存中...' : '保存'))
|
||||
|
||||
const onFileChange = (ev: Event) => {
|
||||
const { files } = ev.target as HTMLInputElement
|
||||
if (files && files[0] && /.+\.(jpg|jpeg|png|gif|webp)/.test(files[0].name)) {
|
||||
picFile.value = files[0]
|
||||
} else {
|
||||
alert('未选择图片或图片格式不正确(只支持jpg、png、gif、webp), 请重新选择')
|
||||
}
|
||||
}
|
||||
|
||||
const initLoadImages = (source: string, result: string) => {
|
||||
nextTick(() => {
|
||||
fetch(source)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
picFile.value = new File([blob], `image_${Math.random()}.jpg`, { type: 'image/jpeg' })
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('获取图片失败:', error)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => initialized.value,
|
||||
async () => {
|
||||
const image: any = await loadNetImg(result)
|
||||
outputHiddenCtx.value.drawImage(image, 0, 0)
|
||||
// TODO 将 ImageBitmap 绘制到 canvas 上
|
||||
let canvas = document.createElement('canvas')
|
||||
canvas.width = image.width
|
||||
canvas.height = image.height
|
||||
let ctx: any = canvas.getContext('2d')
|
||||
ctx.drawImage(image, 0, 0)
|
||||
let imageData = ctx.getImageData(0, 0, image.width, image.height)
|
||||
let data = imageData.data
|
||||
// 遍历每个像素,并将非透明像素点绘制成蒙版
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i + 3] > 0) {
|
||||
// 非透明像素
|
||||
data[i] = DEFAULT_MASK_COLOR[0] * 255
|
||||
data[i + 1] = DEFAULT_MASK_COLOR[1] * 255
|
||||
data[i + 2] = DEFAULT_MASK_COLOR[2] * 255
|
||||
data[i + 3] = (DEFAULT_MASK_COLOR[3] - 0.15) * 255 // 颜色更浅一些方便区分
|
||||
}
|
||||
}
|
||||
await nextTick()
|
||||
setTimeout(async () => {
|
||||
// 强制刷新
|
||||
// transformConfig.positionRange = { ...transformConfig.positionRange };
|
||||
transformConfig.scaleRatio += 0.0001
|
||||
transformConfig.scaleRatio -= 0.0001
|
||||
setTimeout(() => {
|
||||
inputDrawingCtx.putImageData(imageData, 0, 0)
|
||||
inputHiddenCtx.value.drawImage(inputDrawingCtx.canvas, 0, 0)
|
||||
console.log('ok')
|
||||
setTimeout(() => {
|
||||
transformConfig.scaleRatio += 0.0001
|
||||
transformConfig.scaleRatio -= 0.0001
|
||||
}, 100)
|
||||
}, 100)
|
||||
}, 100)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
// 加载网络图片
|
||||
function loadNetImg(src: string) {
|
||||
return new Promise((resolve) => {
|
||||
let image = new Image()
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.src = src
|
||||
image.onload = function () {
|
||||
resolve(image)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handlechangeRadius(e: any) {
|
||||
// if (e.code === 'Space') {
|
||||
// e.preventDefault();
|
||||
// }
|
||||
radius.value = Number(radius.value)
|
||||
if (e.code === 'BracketLeft') {
|
||||
radius.value > RADIUS_SLIDER_MIN + RADIUS_SLIDER_STEP && (radius.value -= RADIUS_SLIDER_STEP)
|
||||
} else if (e.code === 'BracketRight') {
|
||||
radius.value < RADIUS_SLIDER_MAX - RADIUS_SLIDER_STEP && (radius.value += RADIUS_SLIDER_STEP)
|
||||
}
|
||||
}
|
||||
|
||||
function initContextsAndSize() {
|
||||
const inputCanvas = inputCvs.value as HTMLCanvasElement
|
||||
const outputCanvas = outputCvs.value as HTMLCanvasElement
|
||||
inputCtx.value = inputCanvas.getContext('2d')
|
||||
outputCtx.value = outputCanvas.getContext('2d')
|
||||
const { clientWidth, clientHeight } = inputCanvas
|
||||
width.value = clientWidth
|
||||
height.value = clientHeight
|
||||
}
|
||||
|
||||
function onDownloadResult() {
|
||||
if (mattingSources.value && !generating.value) {
|
||||
generating.value = true
|
||||
const url = generateResultImageURL(mattingSources.value.orig, outputHiddenCtx.value)
|
||||
generating.value = false
|
||||
resultURL.value = url
|
||||
setTimeout(() => {
|
||||
resultLink.value?.click()
|
||||
})
|
||||
}
|
||||
}
|
||||
function getResult() {
|
||||
if (mattingSources.value && !generating.value) {
|
||||
return generateResultImageURL(mattingSources.value.orig, outputHiddenCtx.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emit('register', {
|
||||
isErasing,
|
||||
onDownloadResult,
|
||||
onFileChange,
|
||||
initLoadImages,
|
||||
radius,
|
||||
hardness,
|
||||
brushSize,
|
||||
hardnessText,
|
||||
constants: {
|
||||
RADIUS_SLIDER_MIN,
|
||||
RADIUS_SLIDER_MAX,
|
||||
RADIUS_SLIDER_STEP,
|
||||
HARDNESS_SLIDER_MAX,
|
||||
HARDNESS_SLIDER_STEP,
|
||||
HARDNESS_SLIDER_MIN,
|
||||
},
|
||||
getResult,
|
||||
})
|
||||
initContextsAndSize()
|
||||
renderOutputCursor()
|
||||
document.addEventListener('keydown', handlechangeRadius, false)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handlechangeRadius, false)
|
||||
})
|
||||
|
||||
return {
|
||||
onDownloadResult,
|
||||
onFileChange,
|
||||
inputCvs,
|
||||
outputCvs,
|
||||
resultURL,
|
||||
resultLink,
|
||||
isErasing,
|
||||
radius,
|
||||
hardness,
|
||||
brushSize,
|
||||
hardnessText,
|
||||
saveBtnText,
|
||||
downloadFileName,
|
||||
saveBtnClass,
|
||||
cursorImage,
|
||||
mattingCursorStyle,
|
||||
cantSave,
|
||||
RADIUS_SLIDER_MIN,
|
||||
RADIUS_SLIDER_MAX,
|
||||
RADIUS_SLIDER_STEP,
|
||||
HARDNESS_SLIDER_MAX,
|
||||
HARDNESS_SLIDER_STEP,
|
||||
HARDNESS_SLIDER_MIN,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.options-container {
|
||||
height: 50px;
|
||||
.default-wrap {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid #e3e7e9;
|
||||
}
|
||||
|
||||
.range-input {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-container {
|
||||
// position: fixed;
|
||||
top: 50px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
min-height: 600px;
|
||||
display: flex;
|
||||
|
||||
.matting-board,
|
||||
.result-board {
|
||||
flex: 1 50%;
|
||||
border: 1px solid #c3c7c9;
|
||||
background: #e3e7e9;
|
||||
background-image: linear-gradient(45deg, #f6fafc 25%, transparent 0), linear-gradient(45deg, transparent 75%, #f6fafc 0), linear-gradient(45deg, #f6fafc 25%, transparent 0), linear-gradient(45deg, transparent 75%, #f6fafc 0);
|
||||
background-position: 0 0, 12px 12px, 12px 12px, 24px 24px;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.matting-wrapper {
|
||||
position: relative;
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.matting-board,
|
||||
.result-board {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.matting-board {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.result-board {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.matting-cursor {
|
||||
/** 穿透画笔,触发画布点击事件 */
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
21
packages/image-extraction/LICENSE
Normal file
21
packages/image-extraction/LICENSE
Normal file
@ -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.
|
29
packages/image-extraction/README.md
Normal file
29
packages/image-extraction/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
<!--
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-07 23:50:21
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-10-08 16:02:58
|
||||
-->
|
||||
|
||||
<img style="display: inline-block;" src="https://img.shields.io/github/watchers/palxiao/front-end-arsenal?style=social" /> <img style="display: inline-block;" src="https://img.shields.io/github/forks/palxiao/front-end-arsenal?style=social" /> <img style="display: inline-block;" src="https://img.shields.io/github/stars/palxiao/front-end-arsenal?style=social" />
|
||||
|
||||
# image-extraction
|
||||
|
||||
> TODO:
|
||||
|
||||
<img style="display: inline-block;" src="https://img.shields.io/npm/v/@palxp/image-extraction" /> <img style="display: inline-block;" src="https://img.shields.io/bundlephobia/min/@palxp/image-extraction?color=%2344cc88" /> <img style="display: inline-block;" src="https://img.shields.io/npm/dm/@palxp/image-extraction" />
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
yarn add @palxp/image-extraction
|
||||
|
||||
import image-extraction from '@palxp/image-extraction'
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
[API Docs 链接](/#/docs)
|
||||
|
||||
<iframe src="/#/docs/image-extraction/-image-extraction?preview=true" frameborder="0"></iframe>
|
BIN
packages/image-extraction/assets/eraser.png
Normal file
BIN
packages/image-extraction/assets/eraser.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
77
packages/image-extraction/composables/use-init-listeners.ts
Normal file
77
packages/image-extraction/composables/use-init-listeners.ts
Normal file
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
62
packages/image-extraction/composables/use-init-matting.ts
Normal file
62
packages/image-extraction/composables/use-init-matting.ts
Normal file
@ -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)
|
||||
})
|
||||
}
|
111
packages/image-extraction/composables/use-matting-cursor.ts
Normal file
111
packages/image-extraction/composables/use-matting-cursor.ts
Normal file
@ -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<string | undefined> = ref('')
|
||||
inputCursorStyle: Ref<CursorStyle | null> = ref(null)
|
||||
mattingCursorStyle: UnwrapRef<CursorStyle> = 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<CanvasRenderingContext2D | null>, private isErasing: Ref<boolean>) {
|
||||
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),
|
||||
}
|
||||
}
|
78
packages/image-extraction/composables/use-matting.ts
Normal file
78
packages/image-extraction/composables/use-matting.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-05 16:33:07
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @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 | File>(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<CanvasRenderingContext2D | null> = ref(null)
|
||||
const outputCtx: Ref<CanvasRenderingContext2D | null> = ref(null)
|
||||
const initMattingResult: Ref<InitMattingResult | null> = ref(null)
|
||||
const draggingInputBoard = ref(false)
|
||||
const isDrawing = ref(false)
|
||||
const transformConfig: TransformConfig = reactive(INITIAL_TRANSFORM_CONFIG)
|
||||
const mattingSources: Ref<ImageSources | null> = ref(null)
|
||||
const boardRect: Ref<BoardRect | null> = 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,
|
||||
}
|
||||
}
|
122
packages/image-extraction/constants/index.ts
Normal file
122
packages/image-extraction/constants/index.ts
Normal file
@ -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
|
8
packages/image-extraction/env.d.ts
vendored
Normal file
8
packages/image-extraction/env.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
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
|
||||
}
|
186
packages/image-extraction/helpers/dom-helper.ts
Normal file
186
packages/image-extraction/helpers/dom-helper.ts
Normal file
@ -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<ImageBitmap> {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.src = isString(picFile) ? picFile : URL.createObjectURL(picFile)
|
||||
await new Promise<void>((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,
|
||||
}
|
57
packages/image-extraction/helpers/drawing-compute.ts
Normal file
57
packages/image-extraction/helpers/drawing-compute.ts
Normal file
@ -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
|
||||
}
|
105
packages/image-extraction/helpers/drawing-helper.ts
Normal file
105
packages/image-extraction/helpers/drawing-helper.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-05 16:33:07
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @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
|
||||
}
|
103
packages/image-extraction/helpers/init-compute.ts
Normal file
103
packages/image-extraction/helpers/init-compute.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-05 16:33:07
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @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,
|
||||
}
|
205
packages/image-extraction/helpers/init-drawing-listeners.ts
Normal file
205
packages/image-extraction/helpers/init-drawing-listeners.ts
Normal file
@ -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
|
||||
}
|
94
packages/image-extraction/helpers/init-matting.ts
Normal file
94
packages/image-extraction/helpers/init-matting.ts
Normal file
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-05 16:33:07
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @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<InitMattingResult> {
|
||||
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<ImageBitmap> {
|
||||
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<TransformConfig>) {
|
||||
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,
|
||||
})
|
||||
}
|
42
packages/image-extraction/helpers/init-transform-listener.ts
Normal file
42
packages/image-extraction/helpers/init-transform-listener.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-05 16:33:07
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @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)
|
||||
},
|
||||
})
|
||||
}
|
152
packages/image-extraction/helpers/listener-manager.ts
Normal file
152
packages/image-extraction/helpers/listener-manager.ts
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
109
packages/image-extraction/helpers/mask-renderer.ts
Normal file
109
packages/image-extraction/helpers/mask-renderer.ts
Normal file
@ -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
|
||||
}
|
103
packages/image-extraction/helpers/transform-helper.ts
Normal file
103
packages/image-extraction/helpers/transform-helper.ts
Normal file
@ -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,
|
||||
}
|
51
packages/image-extraction/helpers/util.ts
Normal file
51
packages/image-extraction/helpers/util.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-05 16:33:07
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @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,
|
||||
}
|
||||
}
|
41
packages/image-extraction/index.ts
Normal file
41
packages/image-extraction/index.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
137
packages/image-extraction/libs/cuon-utils.ts
Normal file
137
packages/image-extraction/libs/cuon-utils.ts
Normal file
@ -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 <cavnas> 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;
|
||||
}
|
708
packages/image-extraction/libs/webgl-debug.ts
Normal file
708
packages/image-extraction/libs/webgl-debug.ts
Normal file
@ -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<T> = {
|
||||
[P in keyof T]: T[P] extends Function ? P : never;
|
||||
}[keyof T];
|
||||
type DebugContextProp = { [x: number]: boolean };
|
||||
type WebGLContextFuncKey = FunctionKeys<WebGLRenderingContext>;
|
||||
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.<number, string>}
|
||||
*/
|
||||
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<string | number, any> | 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<string | number, any> = {};
|
||||
|
||||
// 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,
|
||||
};
|
||||
})();
|
220
packages/image-extraction/libs/webgl-utils.ts
Normal file
220
packages/image-extraction/libs/webgl-utils.ts
Normal file
@ -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 (
|
||||
'' +
|
||||
'<div style="margin: auto; width:500px;z-index:10000;margin-top:20em;text-align:center;">' +
|
||||
msg +
|
||||
'</div>'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mesasge for getting a webgl browser
|
||||
* @type {string}
|
||||
*/
|
||||
const GET_A_WEBGL_BROWSER =
|
||||
'' +
|
||||
'This page requires a browser that supports WebGL.<br/>' +
|
||||
'<a href="http://get.webgl.org">Click here to upgrade your browser.</a>';
|
||||
|
||||
/**
|
||||
* Mesasge for need better hardware
|
||||
* @type {string}
|
||||
*/
|
||||
const OTHER_PROBLEM =
|
||||
'' +
|
||||
"It doesn't appear your computer can support WebGL.<br/>" +
|
||||
'<a href="http://get.webgl.org">Click here for more information.</a>';
|
||||
|
||||
/**
|
||||
* Creates a webgl context. If creation fails it will
|
||||
* change the contents of the container of the <canvas>
|
||||
* tag to an error message with the correct links for WebGL.
|
||||
* @param {Element} canvas. The canvas element to create a
|
||||
* context from.
|
||||
* @param {WebGLContextCreationAttirbutes} opt_attribs Any
|
||||
* creation attributes you want to pass in.
|
||||
* @param {function:(msg)} opt_onError An function to call
|
||||
* if there is an error during creation.
|
||||
* @return {WebGLRenderingContext} The created context.
|
||||
*/
|
||||
const setupWebGL = function (
|
||||
canvas: HTMLCanvasElement,
|
||||
opt_attribs?: any,
|
||||
opt_onError?: Function
|
||||
) {
|
||||
function handleCreationError(msg: string) {
|
||||
const container = document.getElementsByTagName('body')[0];
|
||||
//const container = canvas.parentNode;
|
||||
if (container) {
|
||||
let str = window.WebGLRenderingContext
|
||||
? OTHER_PROBLEM
|
||||
: GET_A_WEBGL_BROWSER;
|
||||
if (msg) {
|
||||
str += '<br/><br/>Status: ' + msg;
|
||||
}
|
||||
container.innerHTML = makeFailHTML(str);
|
||||
}
|
||||
}
|
||||
|
||||
opt_onError = opt_onError || handleCreationError;
|
||||
|
||||
if (canvas.addEventListener) {
|
||||
canvas.addEventListener(
|
||||
'webglcontextcreationerror',
|
||||
(ev) => {
|
||||
opt_onError = opt_onError || handleCreationError;
|
||||
// WebGLContextEvent还不支持
|
||||
opt_onError((ev as WebGLContextEvent).statusMessage);
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
const context = create3DContext(canvas, opt_attribs);
|
||||
if (!context) {
|
||||
if (!window.WebGLRenderingContext) {
|
||||
opt_onError('');
|
||||
} else {
|
||||
opt_onError('');
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a webgl context.
|
||||
* @param {!Canvas} canvas The canvas tag to get context
|
||||
* from. If one is not passed in one will be created.
|
||||
* @return {!WebGLContext} The created context.
|
||||
*/
|
||||
const create3DContext = function (
|
||||
canvas: HTMLCanvasElement,
|
||||
glAttributes: any
|
||||
) {
|
||||
const names = ['webgl', 'experimental-webgl', 'webkit-3d', 'moz-webgl'];
|
||||
let context: WebGLRenderingContext | null = null;
|
||||
for (let ii = 0; ii < names.length; ++ii) {
|
||||
try {
|
||||
context = canvas.getContext(
|
||||
names[ii],
|
||||
glAttributes
|
||||
) as WebGLRenderingContext;
|
||||
} catch (e) {}
|
||||
if (context) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
return {
|
||||
create3DContext: create3DContext,
|
||||
setupWebGL: setupWebGL,
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Provides requestAnimationFrame in a cross browser
|
||||
* way.
|
||||
*/
|
||||
if (!window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame = (function () {
|
||||
return (
|
||||
window.requestAnimationFrame ||
|
||||
(window as any).webkitRequestAnimationFrame ||
|
||||
(window as any).mozRequestAnimationFrame ||
|
||||
(window as any).oRequestAnimationFrame ||
|
||||
(window as any).msRequestAnimationFrame ||
|
||||
function (
|
||||
/* function FrameRequestCallback */ callback: Function,
|
||||
/* DOMElement Element */ element: Element
|
||||
) {
|
||||
window.setTimeout(callback, 1000 / 60);
|
||||
}
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
/** * ERRATA: 'cancelRequestAnimationFrame' renamed to 'cancelAnimationFrame' to reflect an update to the W3C Animation-Timing Spec.
|
||||
*
|
||||
* Cancels an animation frame request.
|
||||
* Checks for cross-browser support, falls back to clearTimeout.
|
||||
* @param {number} Animation frame request. */
|
||||
if (!window.cancelAnimationFrame) {
|
||||
window.cancelAnimationFrame =
|
||||
(window as any).cancelRequestAnimationFrame ||
|
||||
(window as any).webkitCancelAnimationFrame ||
|
||||
(window as any).webkitCancelRequestAnimationFrame ||
|
||||
(window as any).mozCancelAnimationFrame ||
|
||||
(window as any).mozCancelRequestAnimationFrame ||
|
||||
(window as any).msCancelAnimationFrame ||
|
||||
(window as any).msCancelRequestAnimationFrame ||
|
||||
(window as any).oCancelAnimationFrame ||
|
||||
(window as any).oCancelRequestAnimationFrame ||
|
||||
window.clearTimeout;
|
||||
}
|
27
packages/image-extraction/package.json
Normal file
27
packages/image-extraction/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@palxp/image-extraction",
|
||||
"version": "1.2.4",
|
||||
"description": "TODO",
|
||||
"author": "ShawnPhang <palxiao@vip.qq.com>",
|
||||
"homepage": "https://fe-doc.palxp.cn/#/image-extraction",
|
||||
"license": "ISC",
|
||||
"main": "index.ts",
|
||||
"module": "index.ts",
|
||||
"types": "types/matting.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/palxiao/front-end-arsenal.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/palxiao/front-end-arsenal/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"throttle-debounce": "^5.0.0"
|
||||
}
|
||||
}
|
98
packages/image-extraction/types/common.d.ts
vendored
Normal file
98
packages/image-extraction/types/common.d.ts
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
import { Ref } from 'vue';
|
||||
|
||||
/** 鼠标指针移动距离 */
|
||||
export interface MouseMovements {
|
||||
/** 水平移动距离 */
|
||||
movementX: number;
|
||||
/** 垂直移动距离 */
|
||||
movementY: number;
|
||||
}
|
||||
|
||||
/** 像素坐标 */
|
||||
export interface PixelPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/** 变换配置对象 */
|
||||
export interface TransformConfig {
|
||||
positionRange: PositionRange;
|
||||
scaleRatio: number;
|
||||
}
|
||||
|
||||
/** 绘制位置范围 */
|
||||
export interface PositionRange {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minY: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
/** 矩形的尺寸 */
|
||||
export interface RectSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** 画板矩形的参数 */
|
||||
export interface BoardRect extends RectSize {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
/** 矩形的尺寸 */
|
||||
export interface RectSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** 画板绘制上下文 */
|
||||
export interface BoardDrawingContexts {
|
||||
/** 画板绘制上下文 */
|
||||
ctx: Ref<CanvasRenderingContext2D | null>;
|
||||
/** 绘制输入图像的隐藏画板的绘制上下文 */
|
||||
hiddenCtx: Ref<CanvasRenderingContext2D>;
|
||||
/** 绘制画笔形状的临时画板 */
|
||||
drawingCtx: CanvasRenderingContext2D;
|
||||
}
|
||||
|
||||
/** 画板绘制上下文对象 */
|
||||
export interface BoardContext2Ds {
|
||||
/** 输入画板绘制上下文 */
|
||||
inputCtx: Ref<CanvasRenderingContext2D | null>;
|
||||
/** 输出画板绘制上下文 */
|
||||
outputCtx: Ref<CanvasRenderingContext2D | null>;
|
||||
inputDrawingCtx: CanvasRenderingContext2D;
|
||||
outputDrawingCtx: CanvasRenderingContext2D;
|
||||
/** 绘制输入图像的隐藏画板的绘制上下文 */
|
||||
inputHiddenCtx: Ref<CanvasRenderingContext2D>;
|
||||
/** 绘制输出图像的隐藏画板的绘制上下文 */
|
||||
outputHiddenCtx: Ref<CanvasRenderingContext2D>;
|
||||
}
|
||||
|
||||
/** 绘制基础配置对象 */
|
||||
export interface MattingBoardBaseConfig {
|
||||
boardContexts: BoardContext2Ds;
|
||||
/** 画布目标尺寸 */
|
||||
targetSize: RectSize;
|
||||
/** 图像绘制时与画布边缘最小间隙 */
|
||||
gapSize?: GapSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航视窗区域内图片默认尺寸:以图片中心点为原点,进行等比例缩放
|
||||
* 图片上下边距至少各留80px,左右边距至少留白40px,上下边距优先级高于左右边距
|
||||
* 例如:当图片上下留白80px时,左右留白大于40px时,以上下留白80px为准
|
||||
*/
|
||||
export interface GapSize {
|
||||
horizontal: number;
|
||||
vertical: number;
|
||||
}
|
||||
|
||||
/** 初始化按照变换配置绘制图像的基础配置对象 */
|
||||
export interface InitTransformedDrawBaseConfig {
|
||||
/** 变换配置 */
|
||||
transformConfig: TransformConfig;
|
||||
/** 是否绘制图像边框 */
|
||||
withBorder?: boolean;
|
||||
}
|
19
packages/image-extraction/types/cursor.d.ts
vendored
Normal file
19
packages/image-extraction/types/cursor.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
import { Ref } from 'vue';
|
||||
|
||||
/** 鼠标指针的样式 */
|
||||
export interface CursorStyle {
|
||||
display?: string;
|
||||
left?: string;
|
||||
top?: string;
|
||||
cursor?: string;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
/** 使用鼠标指针组合API的配置对象 */
|
||||
export interface UseCursorConfig {
|
||||
inputCtx: Ref<CanvasRenderingContext2D | null>;
|
||||
isDragging: Ref<boolean>;
|
||||
isErasing: Ref<boolean>;
|
||||
radius: Ref<number>;
|
||||
hardness: Ref<number>;
|
||||
}
|
73
packages/image-extraction/types/dom.d.ts
vendored
Normal file
73
packages/image-extraction/types/dom.d.ts
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
import { InitTransformedDrawBaseConfig, PositionRange, RectSize, TransformConfig } from './common';
|
||||
|
||||
/** canvas尺寸重置配置对象 */
|
||||
export interface ResizeCanvasConfig extends InitTransformedDrawBaseConfig {
|
||||
/** canvas 2D上下文 */
|
||||
ctx: CanvasRenderingContext2D;
|
||||
hiddenCtx: CanvasRenderingContext2D;
|
||||
/** 尺寸重置的目标宽度 */
|
||||
targetWidth: number;
|
||||
/** 尺寸重置的目标高度 */
|
||||
targetHeight: number;
|
||||
}
|
||||
|
||||
/** 创建2D绘制上下文的配置对象 */
|
||||
export interface CreateContext2DConfig {
|
||||
targetSize?: RectSize;
|
||||
cloneCanvas?: HTMLCanvasElement;
|
||||
}
|
||||
|
||||
/** 初始化隐藏画板的配置对象 */
|
||||
export interface InitHiddenBoardConfig {
|
||||
targetSize: RectSize;
|
||||
hiddenCtx: CanvasRenderingContext2D;
|
||||
drawingCtx: CanvasRenderingContext2D;
|
||||
}
|
||||
|
||||
/** 初始化隐藏画布并返回图像的配置对象 */
|
||||
export interface InitHiddenBoardWithImageConfig extends InitHiddenBoardConfig {
|
||||
imageSource: ImageBitmap;
|
||||
}
|
||||
|
||||
/** 从画布获取图像资源的配置对象 */
|
||||
export interface GetImageSourceConfig {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
imageSource: ImageBitmap;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** 画板变换时绘制的上下文对象 */
|
||||
export interface DirectlyDrawingContext {
|
||||
/** 画板绘制上下文 */
|
||||
ctx: CanvasRenderingContext2D;
|
||||
/** 隐藏的绘制上下文 */
|
||||
hiddenCtx: CanvasRenderingContext2D;
|
||||
}
|
||||
|
||||
/** 画板变换时绘制图像的配置对象 */
|
||||
export interface TransformedDrawingImageConfig extends DirectlyDrawingContext, TransformConfig {
|
||||
clearOld?: boolean;
|
||||
withBorder?: boolean;
|
||||
}
|
||||
|
||||
/** 绘制图像边框的配置对象 */
|
||||
export interface DrawImageLineBorderConfig {
|
||||
/** 边框的上下左右位置信息 */
|
||||
positionRange: PositionRange;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
/** 边框颜色 */
|
||||
lineStyle: string;
|
||||
lineWidth: number;
|
||||
}
|
||||
|
||||
/** 绘制圆点的配置对象 */
|
||||
export interface DrawingCircularConfig {
|
||||
ctx: CanvasRenderingContext2D | CanvasRenderingContext2D;
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
hardness: number;
|
||||
innerColor?: string;
|
||||
outerColor?: string;
|
||||
}
|
101
packages/image-extraction/types/drawing-listeners.d.ts
vendored
Normal file
101
packages/image-extraction/types/drawing-listeners.d.ts
vendored
Normal file
@ -0,0 +1,101 @@
|
||||
import ListenerManager from '../helpers/listener-manager'
|
||||
import { Ref } from 'vue'
|
||||
import { BoardContext2Ds, BoardDrawingContexts, BoardRect, MouseMovements, PixelPosition, PositionRange, TransformConfig } from './common'
|
||||
import { ImageSources } from './init-matting'
|
||||
|
||||
/** 初始化抠图绘制的配置对象 */
|
||||
export interface InitDrawingListenerConfig {
|
||||
/** 监听器管理器 */
|
||||
listenerManager: ListenerManager
|
||||
/** 图像绘制源 */
|
||||
imageSources: ImageSources
|
||||
/** 画板绘制上下文 */
|
||||
boardContexts: BoardContext2Ds
|
||||
/** 初始化绘制的配置 */
|
||||
initDrawingConfig: InitDrawingConfig
|
||||
/** 是否为擦除画笔 */
|
||||
isErasing: boolean
|
||||
/** 是否正在拖动左侧输入区画板 */
|
||||
draggingInputBoard: boolean
|
||||
boardRect: BoardRect
|
||||
}
|
||||
|
||||
/** 画笔绘制的基础配置 */
|
||||
export interface BrushDrawingBaseConfig extends TransformConfig {
|
||||
/** 画笔半径 */
|
||||
radius: number
|
||||
/** 绘制距离间隔(移动时移动距离大于step才会进行绘制) */
|
||||
step: number
|
||||
/** 插值绘制时的间隔步长 */
|
||||
stepBase: number
|
||||
/** 画笔硬度 */
|
||||
hardness: number
|
||||
}
|
||||
|
||||
/** 初始化绘制的配置对象 */
|
||||
interface InitDrawingConfig {
|
||||
radius: Ref<number>
|
||||
hardness: Ref<number>
|
||||
transformConfig: TransformConfig
|
||||
}
|
||||
|
||||
/** 画板绘制配置 */
|
||||
export interface BoardDrawingConfig extends BoardDrawingContexts {
|
||||
mattingSource: ImageBitmap
|
||||
}
|
||||
|
||||
/** 执行绘制的配置对象 */
|
||||
export interface DrawingListenerConfig {
|
||||
/** 画笔绘制的基础配置 */
|
||||
brushDrawingBaseConfig: BrushDrawingBaseConfig
|
||||
/** 画板矩形的参数 */
|
||||
boardRect: BoardRect
|
||||
mattingSources: ImageSources
|
||||
/** 绘制输入上下文 */
|
||||
inputBoardDrawingConfig: BoardDrawingConfig
|
||||
/** 绘制输出上下文 */
|
||||
outputBoardDrawingConfig: BoardDrawingConfig
|
||||
/** 是否正在拖动左侧输入区画板 */
|
||||
draggingInputBoard: boolean
|
||||
/** 是否为擦除画笔 */
|
||||
isErasing: boolean
|
||||
}
|
||||
|
||||
/** 计算绘制点坐标位置及鼠标指针水平、垂直移动距离的配置对象 */
|
||||
export interface ComputePositionAndMovementConfig {
|
||||
ev: MouseEvent
|
||||
scaleRatio: number
|
||||
positionRange: PositionRange
|
||||
left: number
|
||||
top: number
|
||||
}
|
||||
|
||||
/** 像素坐标及鼠标指针移动量 */
|
||||
export type PositionAndMovements = MouseMovements & PixelPosition
|
||||
|
||||
/** 计算相对于真实图像尺寸的鼠标指针位置的配置对象 */
|
||||
export interface ComputeRealPositionConfig {
|
||||
/** 鼠标指针的pageX */
|
||||
pageX: number
|
||||
/** 鼠标指针的pageY */
|
||||
pageY: number
|
||||
/** 画布的pageX */
|
||||
left: number
|
||||
/** 画布的pageY */
|
||||
top: number
|
||||
/** 图像左边缘相对于画布左上角的x坐标 */
|
||||
minX: number
|
||||
/** 图像上边缘相对于画布左上角的y坐标 */
|
||||
minY: number
|
||||
/** 图像缩放比例 */
|
||||
scaleRatio: number
|
||||
}
|
||||
|
||||
/** 判断是否可以绘制并绑定鼠标事件监听器的配置对象 */
|
||||
export interface CanDrawAndBindMouseListenerConfig {
|
||||
ev: MouseEvent
|
||||
boardRect: BoardRect
|
||||
positionRange: PositionRange
|
||||
/** 是否正在拖动左侧输入区画板 */
|
||||
draggingInputBoard: boolean
|
||||
}
|
37
packages/image-extraction/types/drawing.d.ts
vendored
Normal file
37
packages/image-extraction/types/drawing.d.ts
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
import { BoardDrawingContexts, MouseMovements, PixelPosition, PositionRange } from './common';
|
||||
import { BrushDrawingBaseConfig } from './drawing-listeners';
|
||||
|
||||
/** 抠图绘制的配置对象 */
|
||||
export interface MattingDrawingConfig extends MouseMovements, BrushDrawingBaseConfig, BoardDrawingContexts, PixelPosition {
|
||||
stepBase: number;
|
||||
mattingSource: ImageBitmap;
|
||||
/** 是否为擦除画笔 */
|
||||
isErasing?: boolean;
|
||||
}
|
||||
|
||||
export interface ComputedMovements {
|
||||
unsignedMovementX: number;
|
||||
unsignedMovementY: number;
|
||||
maxMovement: number;
|
||||
}
|
||||
|
||||
/** 处理插值的绘制点的配置对象 */
|
||||
export interface RenderInterpolationConfig {
|
||||
/** 绘制点的配置对象 */
|
||||
drawingConfig: MattingDrawingConfig;
|
||||
/** 无符号水平移动距离 */
|
||||
unsignedMovementX: number;
|
||||
/** 无符号水平移动距离 */
|
||||
unsignedMovementY: number;
|
||||
/** 无符号水平/垂直移动距离较大的那个 */
|
||||
maxMovement: number;
|
||||
}
|
||||
|
||||
/** 判断是否在图像范围内 */
|
||||
export type InImageRangeConfig = PositionRange & PixelPosition;
|
||||
|
||||
/** 插值步长 */
|
||||
export interface InterpolationStep {
|
||||
stepX: number;
|
||||
stepY: number;
|
||||
}
|
108
packages/image-extraction/types/init-matting.d.ts
vendored
Normal file
108
packages/image-extraction/types/init-matting.d.ts
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
import ListenerManager from '../helpers/listener-manager'
|
||||
import { Ref } from 'vue'
|
||||
import { BoardContext2Ds, BoardRect, GapSize, MattingBoardBaseConfig, PositionRange, RectSize, TransformConfig } from './common'
|
||||
|
||||
/** 抠图画板配置 */
|
||||
export interface MattingProps {
|
||||
picFile: Ref<File | null>
|
||||
isErasing: Ref<boolean>
|
||||
radius: Ref<number>
|
||||
hardness: Ref<number>
|
||||
}
|
||||
|
||||
/** 初始化得到的作为绘制源的图像资源 */
|
||||
export interface ImageSources {
|
||||
/** 原始图片的绘制数据(原始图片初始化结果) */
|
||||
raw: ImageBitmap
|
||||
/** 蒙版图片的绘制数据(蒙版图片初始化结果) */
|
||||
mask: ImageBitmap
|
||||
orig: ImageBitmap
|
||||
}
|
||||
|
||||
/** 初始化画板得到的结果 */
|
||||
export type InitMattingResult = ImageSources & TransformConfig
|
||||
|
||||
/** 初始化抠图的组合API的基础配置对象 */
|
||||
export interface InitMattingBaseConfig {
|
||||
boardContexts: BoardContext2Ds
|
||||
initMattingResult: Ref<InitMattingResult | null>
|
||||
transformConfig: TransformConfig
|
||||
mattingSources: Ref<ImageSources | null>
|
||||
boardRect: Ref<BoardRect | null>
|
||||
initialized: Ref<boolean>
|
||||
}
|
||||
|
||||
/** 初始化抠图画板的组合API的基础配置对象 */
|
||||
export interface UseInitMattingBoardsConfig extends InitMattingBaseConfig {
|
||||
width: Ref<number>
|
||||
height: Ref<number>
|
||||
}
|
||||
|
||||
/** 初始化抠图绘制事件监听器的基础配置对象 */
|
||||
export interface UseInitListenersConfig extends InitMattingBaseConfig {
|
||||
/** 是否正在拖动左侧输入区画板 */
|
||||
draggingInputBoard: Ref<boolean>
|
||||
/** 是否正在绘制中 */
|
||||
isDrawing: Ref<boolean>
|
||||
/** 事件管理器 */
|
||||
listenerManager: ListenerManager
|
||||
}
|
||||
|
||||
/** 抠图画板初始化配置对象 */
|
||||
export interface InitMattingConfig extends MattingBoardBaseConfig {
|
||||
picFile: File
|
||||
transformConfig: Partial<TransformConfig>
|
||||
imageSources: Partial<ImageSources>
|
||||
}
|
||||
|
||||
/** 生成模板图像资源的配置对象 */
|
||||
export interface GenerateMaskSourceConfig {
|
||||
targetSize: RectSize
|
||||
imageSource: TexImageSource
|
||||
}
|
||||
|
||||
interface DrawingBoardsContexts {
|
||||
/** 隐藏的绘制上下文,用于绘制真实尺寸的图像 */
|
||||
hiddenCtx: CanvasRenderingContext2D
|
||||
drawingCtx: CanvasRenderingContext2D
|
||||
ctx: CanvasRenderingContext2D
|
||||
}
|
||||
|
||||
/** 画板初始化配置对象 */
|
||||
export interface InitDrawingBoardConfig extends DrawingBoardsContexts {
|
||||
/** 图片链接地址 */
|
||||
picFile: File
|
||||
targetSize: RectSize
|
||||
transformConfig: Partial<TransformConfig>
|
||||
withBorder?: boolean
|
||||
needDraw?: boolean
|
||||
}
|
||||
|
||||
/** 计算抠图画板变换配置的配置对象 */
|
||||
export interface ComputeTransformConfigConfig {
|
||||
imageSource: ImageBitmap
|
||||
targetSize: RectSize
|
||||
gapSize?: GapSize
|
||||
}
|
||||
|
||||
/** 获取有效变换参数对象的配置对象 */
|
||||
export interface GetValidTransformParametersConfig extends ComputeTransformConfigConfig {
|
||||
transformConfig: Partial<TransformConfig>
|
||||
}
|
||||
|
||||
/** 计算变换参数(缩放因子、偏移坐标)的配置对象 */
|
||||
export interface TransformParametersConfig {
|
||||
/** 图片原始尺寸 */
|
||||
imageSize: RectSize
|
||||
/** 间隙尺寸 */
|
||||
gapSize: GapSize
|
||||
/** 图片绘制的目标尺寸 */
|
||||
targetSize: RectSize
|
||||
}
|
||||
|
||||
/** 计算好的变换参数 */
|
||||
export interface TransformParameters {
|
||||
/** 缩放因子 */
|
||||
scaleRatio: number
|
||||
positionRange: PositionRange
|
||||
}
|
44
packages/image-extraction/types/listener-manager.d.ts
vendored
Normal file
44
packages/image-extraction/types/listener-manager.d.ts
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
export interface MouseListenerContext {
|
||||
/** 触发鼠标事件的目标DOM */
|
||||
mouseTarget: HTMLElement;
|
||||
/** mousemove监听器 */
|
||||
move: (ev: MouseEvent) => void;
|
||||
/** mousedown监听器 */
|
||||
down?: (ev: MouseEvent) => void | boolean;
|
||||
/** mouseup监听器 */
|
||||
up?: (ev: MouseEvent) => void;
|
||||
}
|
||||
|
||||
/** 用于解绑mousedown监听器、mouseup监听器的回调的配置对象 */
|
||||
export interface UnbindDownUpConfig {
|
||||
/** 解绑mousedown监听器的回调 */
|
||||
unbindDown: VoidFunction;
|
||||
/** 解绑mouseup监听器的回调 */
|
||||
unbindUp: VoidFunction;
|
||||
}
|
||||
|
||||
/** 事件监听配置对象 */
|
||||
export interface ListenerConfig {
|
||||
/** 事件类型 */
|
||||
eventType: string;
|
||||
/** 事件监听器 */
|
||||
listener: EventListener;
|
||||
stop?: boolean;
|
||||
prevent?: boolean;
|
||||
}
|
||||
|
||||
export interface WheelListenerContext {
|
||||
/** 输入端(左侧)画板 */
|
||||
mattingBoards: HTMLCanvasElement[];
|
||||
/** 滑动开始的监听器 */
|
||||
wheel: (ev: WheelEvent) => void;
|
||||
}
|
||||
|
||||
/** 存放UnbindDownUpConfig对象的容器 */
|
||||
export type UnbindDownUpCache = WeakMap<HTMLElement, UnbindDownUpConfig>;
|
||||
|
||||
/** 存放解绑mousemove监听器的回调的容器 */
|
||||
export type UnbindMoveCache = WeakMap<HTMLElement, VoidFunction>;
|
||||
|
||||
/** 解绑Wheel监听器的回调的容器 */
|
||||
export type UnbindWheelCache = Set<VoidFunction>;
|
1
packages/image-extraction/types/matting-drawing.d.ts
vendored
Normal file
1
packages/image-extraction/types/matting-drawing.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export type GLColor = [number, number, number, number];
|
32
packages/image-extraction/types/matting.d.ts
vendored
Normal file
32
packages/image-extraction/types/matting.d.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
}
|
34
packages/image-extraction/types/transform.d.ts
vendored
Normal file
34
packages/image-extraction/types/transform.d.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-05 16:33:07
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-10-08 11:11:01
|
||||
*/
|
||||
import ListenerManager from '../helpers/listener-manager'
|
||||
import { InitTransformedDrawBaseConfig, PositionRange } from './common'
|
||||
import { DirectlyDrawingContext } from './dom'
|
||||
|
||||
export interface InitMattingTransformConfig extends InitTransformedDrawBaseConfig {
|
||||
/** 输入画板变换时绘制的上下文对象 */
|
||||
inputContexts: DirectlyDrawingContext
|
||||
/** 输出画板变换时绘制的上下文对象 */
|
||||
outputContexts: DirectlyDrawingContext
|
||||
}
|
||||
|
||||
export interface InitMattingScaleConfig extends InitMattingTransformConfig {
|
||||
listenerManager: ListenerManager
|
||||
}
|
||||
|
||||
/** 初始化抠图画板变化的配置对象 */
|
||||
export interface InitMattingDragConfig extends InitMattingScaleConfig {
|
||||
/** 是否正在拖动左侧输入区画板 */
|
||||
draggingInputBoard: boolean
|
||||
}
|
||||
|
||||
/** 生成绘制返回偏移量的配置对象 */
|
||||
export interface GenerateRangeOffsetConfig {
|
||||
pageX: number
|
||||
pageY: number
|
||||
positionRange: PositionRange
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
|
||||
## Node截图服务
|
||||
|
||||
目录结构比较简单,主要就实现了三个接口,其中 `api/screenshots` 即是项目中所使用到的图片生成接口,在真实生产项目中可以把该服务单独部署,于内网调用,这样利于做一些鉴权之类的处理。
|
||||
@ -7,7 +6,17 @@
|
||||
|
||||
### 安装依赖
|
||||
|
||||
`npm install` 或 `yarn`
|
||||
`npm install`
|
||||
|
||||
安装依赖时可能会出现这个报错提示:
|
||||
|
||||
```
|
||||
ERROR: Failed to set up Chromium xxx! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.
|
||||
```
|
||||
|
||||
不用慌,这是因为 puppeteer 会自动下载 Chromium,国内会受到网络波动的影响。
|
||||
|
||||
如果跳过的话需要手动安装,比较麻烦所以并不推荐。解决方法是多尝试几次,或者更换国内的镜像源即可。
|
||||
|
||||
### 启动项目并热更新
|
||||
|
||||
@ -17,36 +26,24 @@
|
||||
|
||||
`npm run build`
|
||||
|
||||
#### 打包说明
|
||||
#### 打包部署步骤
|
||||
|
||||
直接打包可能会出现未知错误,本项目在 **webpack.config.js** 中过滤掉了依赖(打包出来的文件会非常小因为只包含项目代码),建议将 `package.json` 放到服务器上主动安装依赖来使用,具体的做法类似以下步骤:
|
||||
> 服务器环境需求:Node.js 16.18.1(版本不同则可能出现错误)、PM2(进程守护)
|
||||
|
||||
1. 将项目中的 `package-build.json` 上传到服务器中,重命名为 `package.json`
|
||||
2. 目录下执行 `yarn` 安装依赖
|
||||
3. 执行 `npm run build`
|
||||
4. 将打包的 `dist/server.js` 放在 `node_modules` 同级目录中
|
||||
5. 使用 `pm2 start server.js` 启动并守护服务
|
||||
1. 本地执行 `npm run build` 打包
|
||||
2. 打包后项目根目录 `dist/` 文件夹上传服务器,并执行 `npm install` 安装依赖
|
||||
3. 运行 `pm2 start dist/server.js` 启动并守护服务
|
||||
|
||||
### 服务器配置
|
||||
### 配置说明
|
||||
|
||||
在 Linux 环境下推荐主动安装浏览器,再给项目中的配置文件 `src/config.ts` 设置好路径:
|
||||
配置文件 `src/config.ts` 配置项说明:
|
||||
|
||||
```js
|
||||
exports.executablePath = '/opt/google/chrome-unstable/chrome'
|
||||
port // 端口号
|
||||
website // 编辑器项目的地址
|
||||
filePath // 生成图片保存的目录
|
||||
```
|
||||
|
||||
> `/opt/google` 为默认路径,一般不会变动
|
||||
|
||||
一些可能用到的 linux 命令参考(Debian GNU/Linux 9):
|
||||
|
||||
```shell
|
||||
google-chrome --version # 查看浏览器版本号
|
||||
apt-get update
|
||||
apt-get install -y google-chrome-stable // 安装最新稳定版谷歌浏览器
|
||||
```
|
||||
|
||||
> 其它系统自行搜索如何安装Chrome,推荐使用Docker部署,本项目部署[参考说明](https://xp.palxp.cn/#/articles/1689319644311?id=docker%e5%ae%b9%e5%99%a8)。
|
||||
|
||||
### 生成 API 文档
|
||||
|
||||
`build:apidoc`
|
@ -1,30 +0,0 @@
|
||||
var gulp = require('gulp');
|
||||
var exec = require('child_process').exec;
|
||||
var spawn = require('child_process').spawn;
|
||||
var path = require('path');
|
||||
|
||||
gulp.task('clean', function() {
|
||||
return spawn('rm', ['-rf', path.join(__dirname, 'dist')])
|
||||
});
|
||||
|
||||
gulp.task('build-ts', function(){
|
||||
return exec('webpack --watch',(error,stdout,stderr)=>{
|
||||
console.log(`build ts====>stdout: ${stdout}`);
|
||||
console.log(`build ts====>stderr: ${stderr}`);
|
||||
if (error !== null) {
|
||||
console.log(`exec error: ${error}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
//自动重启服务器
|
||||
gulp.task('restart',function(){
|
||||
return exec('supervisor -w dist ./dist/main.js',(error,stdout,stderr)=>{
|
||||
console.log(`restart=====>stdout: ${stdout}`);
|
||||
console.log(`restart=====>stderr: ${stderr}`);
|
||||
if (error !== null) {
|
||||
console.log(`exec error: ${error}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('default',gulp.series('clean',gulp.parallel('build-ts','restart')));
|
@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "screenshot-node",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "./src/main",
|
||||
"scripts": {},
|
||||
"author": "ShawnPhang",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"fontmin": "^1.0.1",
|
||||
"form-data": "^4.0.0",
|
||||
"moment": "^2.18.1",
|
||||
"multiparty": "^4.2.3",
|
||||
"mysql": "^2.13.0",
|
||||
"qiniu": "^7.4.0",
|
||||
"puppeteer": "^10.4.0",
|
||||
"images": "3.2.3"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
15634
screenshot/package-lock.json
generated
15634
screenshot/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,15 +4,12 @@
|
||||
"description": "",
|
||||
"main": "./src/main",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev src/main",
|
||||
"test": "cd ./static && hs --cors",
|
||||
"dev": "cross-env NODE_ENV=development ts-node-dev src/main",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"serve": "ts-node src/main",
|
||||
"serve-test": "gulp",
|
||||
"start": "webpack --watch",
|
||||
"serverstart": "pm2 start ./dist/server.js --watch",
|
||||
"tscstart": "tsc -w",
|
||||
"serverstart2": "supervisor -w www ./www/main.js",
|
||||
"build:apidoc": "apidoc -i src/ -o _apidoc/",
|
||||
"publish": "rm -rf ./static && rm -rf ./_apidoc && sh script/publish.sh",
|
||||
"publish-fast": "git add . && git commit -m 'build: auto publish' && npm run publish"
|
||||
@ -22,27 +19,18 @@
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"gif-encoder-2": "^1.0.5",
|
||||
"images": "^3.2.4",
|
||||
"png-js": "^1.0.0",
|
||||
"puppeteer": "^10.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.6.9",
|
||||
"@types/node": "^16.18.87",
|
||||
"cross-env": "^7.0.3",
|
||||
"express-graphql": "^0.9.0",
|
||||
"graphql": "^14.5.4",
|
||||
"gulp": "^4.0.2",
|
||||
"http-server": "^14.0.0",
|
||||
"speed-measure-webpack-plugin": "^1.5.0",
|
||||
"supervisor": "^0.12.0",
|
||||
"ts-loader": "^6.0.4",
|
||||
"ts-node": "^8.3.0",
|
||||
"ts-node-dev": "^1.0.0-pre.40",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^3.5.3",
|
||||
"webpack": "^4.39.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^3.3.6",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
},
|
||||
"apidoc": {
|
||||
|
@ -3,21 +3,36 @@
|
||||
* @Date: 2022-02-01 13:41:59
|
||||
* @Description: 配置文件
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-09-15 10:40:41
|
||||
* @LastEditTime: 2023-12-06 19:17:27
|
||||
*/
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
exports.servicePort = 7001
|
||||
// 服务器常用修改项
|
||||
const serviceComfig = {
|
||||
port: 7001, // 端口号
|
||||
website: 'https://design.palxp.cn', // 编辑器项目的地址
|
||||
filePath: '/cache/' // 生成图片保存的目录
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置服务器端的chrome浏览器位置
|
||||
* 端口号
|
||||
*/
|
||||
exports.executablePath = '/opt/google/chrome-unstable/chrome',
|
||||
exports.servicePort = serviceComfig.port
|
||||
|
||||
/**
|
||||
* 前端绘制页地址
|
||||
*/
|
||||
exports.drawLink = isDev ? 'http://localhost:3000/draw' : 'https://design.palxp.cn/draw'
|
||||
exports.drawLink = isDev ? 'http://localhost:5173/draw' : serviceComfig.website + '/draw'
|
||||
|
||||
/**
|
||||
* 图片缓存目录位置,根据实际情况调整
|
||||
*/
|
||||
exports.filePath = isDev ? process.cwd() + `/static/` : serviceComfig.filePath
|
||||
|
||||
/**
|
||||
* 配置服务器端的chrome浏览器位置
|
||||
*/
|
||||
exports.executablePath = isDev ? null : '/opt/google/chrome-unstable/chrome'
|
||||
|
||||
/**
|
||||
* 截图并发数上限
|
||||
@ -33,9 +48,3 @@ exports.upperLimit = 20
|
||||
* 多久释放浏览器驻留内存,单位:秒(多标签页版生效)
|
||||
*/
|
||||
exports.releaseTime = 300
|
||||
|
||||
/**
|
||||
* 图片缓存目录位置,根据实际情况调整
|
||||
*/
|
||||
exports.filePath = isDev ? process.cwd() + `/static/` : '/cache/'
|
||||
// exports.filePath = process.cwd() + `/static/`
|
@ -49,4 +49,4 @@ app.use(bodyParser.json())
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.listen(port, () => console.log(`devServer start on port:${port}`))
|
||||
app.listen(port, () => console.log(`Screenshot Server start on port:${port}`))
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @Date: 2020-07-22 20:13:14
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-10-16 10:03:51
|
||||
* @LastEditTime: 2023-12-07 12:23:57
|
||||
*/
|
||||
const { saveScreenshot } = require('../utils/download-single.ts')
|
||||
const uuid = require('../utils/uuid.ts')
|
||||
@ -68,7 +68,6 @@ module.exports = {
|
||||
return
|
||||
}
|
||||
const targetUrl = url + id + `${tempType?'&tempType='+tempType:''}`
|
||||
// console.log(targetUrl, path, thumbPath);
|
||||
queueRun(saveScreenshot, targetUrl, { width, height, path, thumbPath, size, quality })
|
||||
.then(() => {
|
||||
res.setHeader('Content-Type', 'image/jpg')
|
||||
|
@ -22,17 +22,22 @@ const saveScreenshot = async (url: string, { path, width, height, thumbPath, siz
|
||||
// 格式化浏览器宽高
|
||||
width = Number(width).toFixed(0)
|
||||
height = Number(height).toFixed(0)
|
||||
|
||||
const puppeteerArgs = {
|
||||
old: ['–no-first-run', '--no-sandbox', '--disable-setuid-sandbox', `--window-size=${width},${height}`, '–single-process', '–disable-gpu', '–no-zygote', '–disable-dev-shm-usage'],
|
||||
new: [ '–no-first-run', '--no-sandbox', '--disable-setuid-sandbox', `--window-size=${width},${height}` ]
|
||||
}
|
||||
// 启动浏览器
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
headless: true, // !isDev,
|
||||
executablePath: isDev ? null : executablePath,
|
||||
executablePath,
|
||||
ignoreHTTPSErrors: true, // 忽略https安全提示
|
||||
args: ['–no-first-run', '–single-process', '–disable-gpu', '–no-zygote', '–disable-dev-shm-usage', '--no-sandbox', '--disable-setuid-sandbox', `--window-size=${width},${height}`], // 优化配置
|
||||
args: puppeteerArgs.old, // 如puppeteer版本v20+报错请尝试使用新参数
|
||||
defaultViewport: null,
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('Puppeteer启动错误!', '窗口大小:', width, height);
|
||||
console.log('Puppeteer Error: ', error, '窗口大小:', width, height);
|
||||
}
|
||||
if (!browser) {
|
||||
reject()
|
||||
|
@ -10,6 +10,7 @@
|
||||
const path = require('path')
|
||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
const buildPlugin = require('./webpack.plugin.js');
|
||||
|
||||
module.exports = {
|
||||
mode: process.env.NODE_ENV,
|
||||
@ -39,4 +40,5 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
// plugins: [new BundleAnalyzerPlugin()],
|
||||
plugins: [ new buildPlugin() ]
|
||||
}
|
||||
|
24
screenshot/webpack.plugin.js
Normal file
24
screenshot/webpack.plugin.js
Normal file
@ -0,0 +1,24 @@
|
||||
const pkg = require("./package.json");
|
||||
const fs = require('fs');
|
||||
|
||||
class MyPlugin {
|
||||
apply(compiler) {
|
||||
compiler.hooks.emit.tap("BuildPackageJson", (compilation) => {
|
||||
console.log("构建 package.json ....");
|
||||
const myBuildPackageJson = `{
|
||||
name: ${pkg.name+'-builder'},
|
||||
version: ${pkg.version},
|
||||
dependencies: ${JSON.stringify(pkg.dependencies, null, 2)}
|
||||
}`;
|
||||
fs.writeFile('./dist/package.json', myBuildPackageJson, 'utf8', (err) => {
|
||||
if (err) {
|
||||
console.error('保存 package.json 文件时发生错误:', err);
|
||||
} else {
|
||||
console.log('package.json 文件构建完成!');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MyPlugin;
|
4984
screenshot/yarn.lock
4984
screenshot/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import { ElDialog } from 'element-plus'
|
||||
import { ref, defineEmits, reactive, nextTick, toRefs } from 'vue'
|
||||
import { ref, reactive, nextTick, toRefs } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
import Cropper from 'cropperjs'
|
||||
|
@ -40,7 +40,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, nextTick, defineEmits, ref } from 'vue'
|
||||
import { reactive, nextTick, ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElProgress } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
|
@ -31,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, defineEmits, defineExpose } from 'vue'
|
||||
import { reactive } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElTabPane, ElTabs, TabPaneName } from 'element-plus'
|
||||
import api from '@/api'
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2022-03-16 09:15:52
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @Date: 2024-03-04 18:50:00
|
||||
-->
|
||||
<template>
|
||||
@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch, nextTick, defineProps } from 'vue'
|
||||
import { onMounted, ref, watch, nextTick } from 'vue'
|
||||
import QRCodeStyling, {DotType, Options } from 'qr-code-styling'
|
||||
import { debounce } from 'throttle-debounce'
|
||||
import { generateOption } from './method'
|
||||
|
@ -2,8 +2,8 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2022-04-10 12:12:57
|
||||
* @Description: tooltip提示
|
||||
* @LastEditors: ShawnPhang
|
||||
* @LastEditTime: 2022-04-10 12:42:02
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2024-03-11 01:41:20
|
||||
-->
|
||||
<template>
|
||||
<el-popover ref="popover" :placement="position" :title="title" :width="width" trigger="hover" :content="content">
|
||||
@ -14,8 +14,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
type TProps = {
|
||||
title: string
|
||||
width: number
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2021-12-28 09:29:42
|
||||
* @Description: 百分比进度条
|
||||
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @Date: 2024-03-05 10:50:00
|
||||
-->
|
||||
<template>
|
||||
@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, defineProps, defineEmits } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { ElProgress } from 'element-plus'
|
||||
|
||||
type TProps = {
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2021-08-29 18:17:13
|
||||
* @Description: 二次封装上传组件
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @Date: 2024-03-05 10:50:00
|
||||
-->
|
||||
<template>
|
||||
@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, nextTick, defineProps, withDefaults, defineEmits } from 'vue'
|
||||
import { onMounted, nextTick, withDefaults } from 'vue'
|
||||
import { ElUpload, UploadRequestOptions } from 'element-plus'
|
||||
import Qiniu from '@/common/methods/QiNiu'
|
||||
import { getImage } from '@/common/methods/getImgDetail'
|
||||
|
@ -58,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, defineProps, onMounted, ref } from 'vue'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { mapGetters, mapActions, useStore } from 'vuex'
|
||||
import { getTarget } from '@/common/methods/target'
|
||||
|
||||
|
@ -2,15 +2,15 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2022-04-08 10:31:34
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||
* @LastEditTime: 2023-06-29 18:07:40
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2024-03-11 01:42:25
|
||||
-->
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, defineProps } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import Guides, { GuideOptions } from '@scena/guides'
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @Date: 2021-08-27 15:16:07
|
||||
* @Description: 背景图
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2024-01-24 16:39:27
|
||||
* @LastEditTime: 2024-03-11 01:42:36
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed, defineProps, defineExpose } from 'vue'
|
||||
import { reactive, computed } from 'vue'
|
||||
import api from '@/api'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElImage } from 'element-plus'
|
||||
|
@ -236,9 +236,12 @@ export default defineComponent({
|
||||
width: 100%;
|
||||
// padding: 20px 0 0 10px;
|
||||
padding: 3.1rem 0 0 1rem;
|
||||
gap: 0 !important;
|
||||
&__item {
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
&__img {
|
||||
cursor: grab;
|
||||
|
@ -21,7 +21,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, defineExpose } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import api from '@/api'
|
||||
import { useStore } from 'vuex'
|
||||
import { LocationQueryValue, useRoute, useRouter } from 'vue-router'
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @Date: 2022-02-13 22:18:35
|
||||
* @Description: 我的
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-12-11 11:50:34
|
||||
* @LastEditTime: 2024-03-11 01:42:44
|
||||
-->
|
||||
<template>
|
||||
<div class="wrap">
|
||||
@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs, watch, nextTick, ref, onMounted, defineProps, defineExpose } from 'vue'
|
||||
import { reactive, toRefs, watch, nextTick, ref, onMounted } from 'vue'
|
||||
import { ElTabPane, ElTabs, TabPaneName } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-04 02:04:04
|
||||
* @Description: 列表分类头部
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @Date: 2024-03-06 21:16:00
|
||||
-->
|
||||
<template>
|
||||
@ -22,8 +22,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
|
||||
export type TClassHeaderTypeData = {
|
||||
name: string
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2023-10-04 19:12:40
|
||||
* @Description: 图片描述ToolTip
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @Date: 2024-03-06 21:16:00
|
||||
-->
|
||||
<template>
|
||||
@ -18,7 +18,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
export type TImageTipDetailData = {
|
||||
author: string
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2021-12-16 16:20:16
|
||||
* @Description: 瀑布流组件
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @Date: 2024-03-06 21:16:00
|
||||
-->
|
||||
<template>
|
||||
@ -27,7 +27,7 @@
|
||||
<script lang="ts" setup>
|
||||
// const NAME = 'img-water-fall'
|
||||
import { IGetTempListData } from '@/api/home';
|
||||
import { reactive, watch, defineProps, defineExpose, defineEmits } from 'vue'
|
||||
import { reactive, watch } from 'vue'
|
||||
|
||||
type TProps = {
|
||||
listData: IGetTempListData[]
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2022-02-23 15:48:52
|
||||
* @Description: 图片列表组件 Bookshelf Layout
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @Date: 2024-03-06 21:16:00
|
||||
-->
|
||||
<template>
|
||||
@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch, nextTick, defineProps, defineExpose, defineEmits, ref } from 'vue'
|
||||
import { reactive, watch, nextTick, ref } from 'vue'
|
||||
import DragHelper from '@/common/hooks/dragHelper'
|
||||
import setImageData, { TItem2DataParam } from '@/common/methods/DesignFeatures/setImage'
|
||||
import { IGetTempListData } from '@/api/home';
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @Author: ShawnPhang
|
||||
* @Date: 2022-01-27 11:05:48
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @Date: 2024-03-06 21:16:00
|
||||
-->
|
||||
<template>
|
||||
@ -30,7 +30,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs, watch, defineProps, defineEmits, defineExpose } from 'vue'
|
||||
import { reactive, toRefs, watch } from 'vue'
|
||||
import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
import api from '@/api'
|
||||
|
@ -40,7 +40,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { toRefs, reactive, watch, onMounted, nextTick, defineProps, defineEmits, defineExpose } from 'vue'
|
||||
import { toRefs, reactive, watch, onMounted, nextTick } from 'vue'
|
||||
import { ElRadioGroup, ElRadioButton } from 'element-plus'
|
||||
import wSvg from '@/components/modules/widgets/wSvg/wSvg.vue'
|
||||
import { TGetListResult } from '@/api/material';
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @Date: 2021-08-09 14:00:23
|
||||
* @Description: 文字特效选择框组件
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-11-30 10:14:30
|
||||
* @LastEditTime: 2024-03-11 01:43:21
|
||||
-->
|
||||
<template>
|
||||
<el-card class="box-card" shadow="hover" :body-style="{ padding: '20px' }">
|
||||
@ -109,8 +109,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
reactive, watch, onMounted, nextTick, computed,
|
||||
defineProps, defineEmits, defineExpose
|
||||
reactive, watch, onMounted, nextTick, computed
|
||||
} from 'vue'
|
||||
import colorSelect from '../colorSelect.vue'
|
||||
import { ElInputNumber, ElCheckbox } from 'element-plus'
|
||||
|
@ -16,12 +16,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// const NAME = 'color-select'
|
||||
import {reactive, onMounted, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
// import { debounce } from 'throttle-debounce'
|
||||
// import { toolTip } from '@/common/methods/helper'
|
||||
// import colorPicker from '@/utils/plugins/color-picker/index.vue'
|
||||
import colorPicker from '@palxp/color-picker'
|
||||
|
||||
type TProps = {
|
||||
@ -64,9 +60,7 @@ let first = true
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue) {
|
||||
let fixColor = props.modelValue + (props.modelValue.length === 7 ? 'ff' : '')
|
||||
// 当前@palxp/color-picker对部分小写16进制颜色处理有异常,统一转为大写
|
||||
state.innerColor = fixColor.toLocaleUpperCase()
|
||||
state.innerColor = props.modelValue + (props.modelValue.length === 7 ? 'ff' : '')
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -91,7 +91,7 @@ type TState = {
|
||||
const props = withDefaults(defineProps<TProps>(), {
|
||||
label: '',
|
||||
modelValue: () => ({}),
|
||||
suffic: '',
|
||||
suffix: '',
|
||||
data: () => ({}),
|
||||
disable: true,
|
||||
inputWidth: '80px',
|
||||
|
@ -26,7 +26,7 @@
|
||||
// 组合组件
|
||||
const NAME = 'w-group'
|
||||
import { nextTick, onBeforeUnmount, onMounted, onUpdated, ref } from 'vue'
|
||||
import { mapGetters, mapActions, useStore } from 'vuex'
|
||||
import { useStore } from 'vuex'
|
||||
import { setTransformAttribute } from '@/common/methods/handleTransform'
|
||||
import { useSetupMapGetters } from '@/common/hooks/mapGetters';
|
||||
|
||||
@ -55,26 +55,26 @@ const widget = ref<HTMLElement | null>(null)
|
||||
const ratio = ref(0)
|
||||
const temp = ref<Record<string, any>>({})
|
||||
const compWidgetsRecord = ref<Record<string, any>>({})
|
||||
const setting = {
|
||||
name: '组合',
|
||||
type: NAME,
|
||||
uuid: -1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: '',
|
||||
opacity: 1,
|
||||
parent: '-1',
|
||||
isContainer: true,
|
||||
record: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
dir: 'none',
|
||||
},
|
||||
}
|
||||
// const setting = {
|
||||
// name: '组合',
|
||||
// type: NAME,
|
||||
// uuid: -1,
|
||||
// width: 0,
|
||||
// height: 0,
|
||||
// left: 0,
|
||||
// top: 0,
|
||||
// transform: '',
|
||||
// opacity: 1,
|
||||
// parent: '-1',
|
||||
// isContainer: true,
|
||||
// record: {
|
||||
// width: 0,
|
||||
// height: 0,
|
||||
// minWidth: 0,
|
||||
// minHeight: 0,
|
||||
// dir: 'none',
|
||||
// },
|
||||
// }
|
||||
|
||||
const timer = ref<number | null>(null)
|
||||
const { dActiveElement, dWidgets } = useSetupMapGetters(['dActiveElement', 'dWidgets'])
|
||||
@ -235,9 +235,9 @@ function keySetValue(uuid: string, key: keyof TParamsData, value: number) {
|
||||
}, 10)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setting
|
||||
})
|
||||
// defineExpose({
|
||||
// setting
|
||||
// })
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
:id="params.uuid"
|
||||
:id="`${params.uuid}`"
|
||||
ref="widget"
|
||||
v-loading="loading"
|
||||
:class="['w-text', { editing: editable, 'layer-lock': params.lock }, params.uuid]"
|
||||
v-loading="state.loading"
|
||||
:class="['w-text', { editing: state.editable, 'layer-lock': params.lock }, params.uuid]"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: params.left - parent.left + 'px',
|
||||
@ -27,17 +27,17 @@
|
||||
}"
|
||||
@dblclick="(e) => dblclickText(e)"
|
||||
>
|
||||
<template v-if="params.textEffects && !editable">
|
||||
<template v-if="params.textEffects && !state.editable">
|
||||
<div
|
||||
v-for="(ef, efi) in params.textEffects"
|
||||
:key="efi + 'effect'"
|
||||
:style="{
|
||||
fontFamily: `'${params.fontClass.value}'`,
|
||||
color: ef.filling && ef.filling.enable && ef.filling.type === 0 ? ef.filling.color : 'transparent',
|
||||
webkitTextStroke: ef.stroke && ef.stroke.enable ? `${ef.stroke.width}px ${ef.stroke.color}` : undefined,
|
||||
WebkitTextStroke: ef.stroke && ef.stroke.enable ? `${ef.stroke.width}px ${ef.stroke.color}` : undefined,
|
||||
textShadow: ef.shadow && ef.shadow.enable ? `${ef.shadow.offsetX}px ${ef.shadow.offsetY}px ${ef.shadow.blur}px ${ef.shadow.color}` : undefined,
|
||||
backgroundImage: ef.filling && ef.filling.enable ? (ef.filling.type === 0 ? undefined : getGradientOrImg(ef)) : undefined,
|
||||
webkitBackgroundClip: ef.filling && ef.filling.enable ? (ef.filling.type === 0 ? undefined : 'text') : undefined,
|
||||
WebkitBackgroundClip: ef.filling && ef.filling.enable ? (ef.filling.type === 0 ? undefined : 'text') : undefined,
|
||||
transform: ef.offset && ef.offset.enable ? `translate(${ef.offset.x}px, ${ef.offset.y}px)` : undefined,
|
||||
}"
|
||||
class="edit-text effect-text"
|
||||
@ -45,203 +45,188 @@
|
||||
v-html="params.text"
|
||||
></div>
|
||||
</template>
|
||||
<div ref="editWrap" :style="{ fontFamily: `'${params.fontClass.value}'` }" class="edit-text" spellcheck="false" :contenteditable="editable ? 'plaintext-only' : false" @input="writingText($event)" @blur="writeDone($event)" v-html="params.text"></div>
|
||||
<div
|
||||
ref="editWrap" :style="{ fontFamily: `'${params.fontClass.value}'` }"
|
||||
class="edit-text" spellcheck="false"
|
||||
:contenteditable="state.editable ? 'plaintext-only' : false"
|
||||
@input="writingText($event)"
|
||||
@blur="writeDone($event)"
|
||||
v-html="params.text"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
// 文本组件
|
||||
const NAME = 'w-text'
|
||||
// const NAME = 'w-text'
|
||||
|
||||
import { defineComponent, reactive, toRefs, computed, onUpdated, watch, onMounted, ref } from 'vue'
|
||||
import { reactive, toRefs, computed, onUpdated, watch, onMounted, ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { fontWithDraw } from '@/utils/widgets/loadFontRule'
|
||||
import getGradientOrImg from './getGradientOrImg.ts'
|
||||
import getGradientOrImg from './getGradientOrImg'
|
||||
import { wTextSetting } from './wTextSetting'
|
||||
|
||||
export default defineComponent({
|
||||
name: NAME,
|
||||
setting: {
|
||||
name: '文本',
|
||||
type: NAME,
|
||||
uuid: -1,
|
||||
editable: false,
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: '',
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: 0,
|
||||
fontSize: 24,
|
||||
zoom: 1,
|
||||
fontClass: {
|
||||
alias: '站酷快乐体',
|
||||
id: 543,
|
||||
value: 'zcool-kuaile-regular',
|
||||
url: 'https://lib.baomitu.com/fonts/zcool-kuaile/zcool-kuaile-regular.woff2',
|
||||
},
|
||||
fontFamily: 'SourceHanSansSC-Regular',
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'normal',
|
||||
writingMode: 'horizontal-tb',
|
||||
textDecoration: 'none',
|
||||
color: '#000000ff',
|
||||
textAlign: 'left',
|
||||
text: '',
|
||||
opacity: 1,
|
||||
backgroundColor: '',
|
||||
parent: '-1',
|
||||
record: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
dir: 'horizontal',
|
||||
},
|
||||
},
|
||||
props: ['params', 'parent'],
|
||||
setup(props) {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
editable: false,
|
||||
loadFontDone: false,
|
||||
})
|
||||
const widget = ref(null)
|
||||
const editWrap = ref(null)
|
||||
export type TwTextParams = {
|
||||
rotate?: number
|
||||
lock?: boolean
|
||||
width?: number
|
||||
height?: number
|
||||
} & typeof wTextSetting
|
||||
|
||||
const dActiveElement = computed(() => store.getters.dActiveElement)
|
||||
const isDraw = computed(() => route.name === 'Draw' && fontWithDraw)
|
||||
type TProps = {
|
||||
params: TwTextParams
|
||||
parent: {
|
||||
left: number
|
||||
top: number
|
||||
}
|
||||
}
|
||||
|
||||
onUpdated(() => {
|
||||
updateRecord()
|
||||
})
|
||||
const props = defineProps<TProps>()
|
||||
|
||||
onMounted(() => {
|
||||
updateRecord()
|
||||
props.params.transform && (widget.value.style.transform = props.params.transform)
|
||||
props.params.rotate && (widget.value.style.transform += `translate(0px, 0px) rotate(${props.params.rotate}) scale(1, 1)`)
|
||||
// store.commit('updateRect')
|
||||
})
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
editable: false,
|
||||
loadFontDone: '',
|
||||
})
|
||||
const widget = ref<HTMLElement | null>(null)
|
||||
const editWrap = ref<HTMLElement | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.params,
|
||||
async (nval) => {
|
||||
updateText()
|
||||
if (state.loading) {
|
||||
return
|
||||
}
|
||||
let font = nval.fontClass
|
||||
const isDone = font.value === state.loadFontDone
|
||||
const dActiveElement = computed(() => store.getters.dActiveElement)
|
||||
const isDraw = computed(() => route.name === 'Draw' && fontWithDraw)
|
||||
|
||||
if (font.url && !isDone) {
|
||||
if (font.id && isDraw.value) {
|
||||
// 如果为绘制模式,且开启了字体抽取,那么会跳过加载字体url的逻辑
|
||||
// 此前该功能在demo中存在换行bug,实际上是由于抽取字体时忽略了空格导致的
|
||||
state.loading = false
|
||||
return
|
||||
}
|
||||
state.loading = !isDraw.value
|
||||
const loadFont = new window.FontFace(font.value, `url(${font.url})`)
|
||||
await loadFont.load()
|
||||
document.fonts.add(loadFont)
|
||||
state.loadFontDone = font.value
|
||||
state.loading = false
|
||||
} else {
|
||||
state.loading = false
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
onUpdated(() => {
|
||||
updateRecord()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => state.editable,
|
||||
(value) => {
|
||||
store.dispatch('updateWidgetData', {
|
||||
uuid: props.params.uuid,
|
||||
key: 'editable',
|
||||
value,
|
||||
pushHistory: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
onMounted(() => {
|
||||
updateRecord()
|
||||
|
||||
function updateRecord() {
|
||||
if (dActiveElement.value.uuid === props.params.uuid) {
|
||||
let record = dActiveElement.value.record
|
||||
record.width = widget.value.offsetWidth
|
||||
record.height = widget.value.offsetHeight
|
||||
record.minWidth = props.params.fontSize
|
||||
record.minHeight = props.params.fontSize * props.params.lineHeight
|
||||
writingText()
|
||||
if (!widget.value) return
|
||||
props.params.transform && (widget.value.style.transform = props.params.transform)
|
||||
props.params.rotate && (widget.value.style.transform += `translate(0px, 0px) rotate(${props.params.rotate}) scale(1, 1)`)
|
||||
// store.commit('updateRect')
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.params,
|
||||
async (nval) => {
|
||||
updateText()
|
||||
if (state.loading) {
|
||||
return
|
||||
}
|
||||
let font = nval.fontClass
|
||||
const isDone = font.value === state.loadFontDone
|
||||
|
||||
if (font.url && !isDone) {
|
||||
if (font.id && isDraw.value) {
|
||||
// 如果为绘制模式,且开启了字体抽取,那么会跳过加载字体url的逻辑
|
||||
// 此前该功能在demo中存在换行bug,实际上是由于抽取字体时忽略了空格导致的
|
||||
state.loading = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function updateText(e) {
|
||||
const value = e ? e.target.innerHTML : props.params.text.replace(/\n/g, '<br/>')
|
||||
// const value = (e ? e.target.innerText : props.params.text).replace(/<br\/>/g, '\r\n').replace(/ /g, ' ')
|
||||
if (value !== props.params.text) {
|
||||
store.dispatch('updateWidgetData', {
|
||||
uuid: props.params.uuid,
|
||||
key: 'text',
|
||||
value,
|
||||
pushHistory: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function writingText(e) {
|
||||
// updateText(e)
|
||||
// TODO: 修正文字选框高度
|
||||
const el = editWrap.value || widget.value
|
||||
store.dispatch('updateWidgetData', {
|
||||
uuid: props.params.uuid,
|
||||
key: 'height',
|
||||
value: el.offsetHeight,
|
||||
pushHistory: false,
|
||||
})
|
||||
store.commit('updateRect')
|
||||
}
|
||||
|
||||
function writeDone(e) {
|
||||
state.editable = false
|
||||
setTimeout(() => {
|
||||
store.dispatch('pushHistory', '文字修改')
|
||||
}, 100)
|
||||
updateText(e)
|
||||
}
|
||||
|
||||
function dblclickText(e) {
|
||||
// store.commit('setShowMoveable', false)
|
||||
state.editable = true
|
||||
const el = editWrap.value || widget.value
|
||||
setTimeout(() => {
|
||||
el.focus()
|
||||
if (document.selection) {
|
||||
const range = document.body.createTextRange()
|
||||
range.moveToElementText(el)
|
||||
range.select()
|
||||
} else if (window.getSelection) {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(el)
|
||||
window.getSelection().removeAllRanges()
|
||||
window.getSelection().addRange(range)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
getGradientOrImg,
|
||||
updateRecord,
|
||||
writingText,
|
||||
updateText,
|
||||
writeDone,
|
||||
dblclickText,
|
||||
widget,
|
||||
editWrap,
|
||||
state.loading = !isDraw.value
|
||||
const loadFont = new window.FontFace(font.value, `url(${font.url})`)
|
||||
await loadFont.load()
|
||||
document.fonts.add(loadFont)
|
||||
state.loadFontDone = font.value
|
||||
state.loading = false
|
||||
} else {
|
||||
state.loading = false
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.editable,
|
||||
(value) => {
|
||||
store.dispatch('updateWidgetData', {
|
||||
uuid: props.params.uuid,
|
||||
key: 'editable',
|
||||
value,
|
||||
pushHistory: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
function updateRecord() {
|
||||
if (!widget.value) return
|
||||
if (dActiveElement.value.uuid === props.params.uuid) {
|
||||
let record = dActiveElement.value.record
|
||||
record.width = widget.value.offsetWidth
|
||||
record.height = widget.value.offsetHeight
|
||||
record.minWidth = props.params.fontSize
|
||||
record.minHeight = props.params.fontSize * props.params.lineHeight
|
||||
writingText()
|
||||
}
|
||||
}
|
||||
|
||||
function updateText(e?: Event) {
|
||||
const value = e && e.target ? (e.target as HTMLElement).innerHTML : props.params.text.replace(/\n/g, '<br/>')
|
||||
// const value = (e ? e.target.innerText : props.params.text).replace(/<br\/>/g, '\r\n').replace(/ /g, ' ')
|
||||
if (value !== props.params.text) {
|
||||
store.dispatch('updateWidgetData', {
|
||||
uuid: props.params.uuid,
|
||||
key: 'text',
|
||||
value,
|
||||
pushHistory: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function writingText(e?: Event) {
|
||||
// updateText(e)
|
||||
// TODO: 修正文字选框高度
|
||||
const el = editWrap.value || widget.value
|
||||
if (!el) return
|
||||
store.dispatch('updateWidgetData', {
|
||||
uuid: props.params.uuid,
|
||||
key: 'height',
|
||||
value: el.offsetHeight,
|
||||
pushHistory: false,
|
||||
})
|
||||
store.commit('updateRect')
|
||||
}
|
||||
|
||||
function writeDone(e: Event) {
|
||||
state.editable = false
|
||||
setTimeout(() => {
|
||||
store.dispatch('pushHistory', '文字修改')
|
||||
}, 100)
|
||||
updateText(e)
|
||||
}
|
||||
|
||||
function dblclickText(_: MouseEvent) {
|
||||
// store.commit('setShowMoveable', false)
|
||||
state.editable = true
|
||||
const el = editWrap.value || widget.value
|
||||
setTimeout(() => {
|
||||
if (!el) return
|
||||
el.focus()
|
||||
if (document.selection) {
|
||||
const range = document.body.createTextRange()
|
||||
range.moveToElementText(el)
|
||||
range.select()
|
||||
} else {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(el);
|
||||
window.getSelection()?.removeAllRanges();
|
||||
window.getSelection()?.addRange(range);
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getGradientOrImg,
|
||||
updateRecord,
|
||||
writingText,
|
||||
updateText,
|
||||
writeDone,
|
||||
dblclickText,
|
||||
widget,
|
||||
editWrap,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
101
src/components/modules/widgets/wText/wTextSetting.ts
Normal file
101
src/components/modules/widgets/wText/wTextSetting.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { StyleValue } from "vue"
|
||||
|
||||
export type TwTextData = {
|
||||
name: string
|
||||
type: string
|
||||
uuid: number
|
||||
editable: boolean,
|
||||
left: number
|
||||
top: number
|
||||
transform: string
|
||||
lineHeight: number
|
||||
letterSpacing: number
|
||||
fontSize: number
|
||||
zoom: number
|
||||
fontClass: {
|
||||
alias: string
|
||||
id: number
|
||||
value: string
|
||||
url: string
|
||||
},
|
||||
fontFamily: string
|
||||
fontWeight: string
|
||||
fontStyle: string
|
||||
writingMode: StyleProperty.WritingMode
|
||||
textDecoration: string
|
||||
color: string
|
||||
textAlign: StyleProperty.TextAlign
|
||||
text: string
|
||||
opacity: number
|
||||
backgroundColor: string
|
||||
parent: string
|
||||
record: {
|
||||
width: number
|
||||
height: number
|
||||
minWidth: number
|
||||
minHeight: number
|
||||
dir: string
|
||||
},
|
||||
textEffects?: {
|
||||
filling: {
|
||||
enable: boolean
|
||||
type: number
|
||||
color: string
|
||||
}
|
||||
stroke: {
|
||||
enable: boolean
|
||||
width: number
|
||||
color: string
|
||||
}
|
||||
shadow: {
|
||||
enable: boolean
|
||||
offsetY: number
|
||||
offsetX: number
|
||||
blur: number
|
||||
color: string
|
||||
}
|
||||
offset: {
|
||||
enable: boolean
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
export const wTextSetting: TwTextData = {
|
||||
name: '文本',
|
||||
type: 'w-text',
|
||||
uuid: -1,
|
||||
editable: false,
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: '',
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: 0,
|
||||
fontSize: 24,
|
||||
zoom: 1,
|
||||
fontClass: {
|
||||
alias: '站酷快乐体',
|
||||
id: 543,
|
||||
value: 'zcool-kuaile-regular',
|
||||
url: 'https://lib.baomitu.com/fonts/zcool-kuaile/zcool-kuaile-regular.woff2',
|
||||
},
|
||||
fontFamily: 'SourceHanSansSC-Regular',
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'normal',
|
||||
writingMode: 'horizontal-tb',
|
||||
textDecoration: 'none',
|
||||
color: '#000000ff',
|
||||
textAlign: 'left',
|
||||
text: '',
|
||||
opacity: 1,
|
||||
backgroundColor: '',
|
||||
parent: '-1',
|
||||
record: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
dir: 'horizontal',
|
||||
},
|
||||
}
|
@ -17,7 +17,8 @@ import Qiniu from '@/common/methods/QiNiu'
|
||||
import _config from '@/config'
|
||||
import { getImage } from '@/common/methods/getImgDetail'
|
||||
import wImage from '@/components/modules/widgets/wImage/wImage.vue'
|
||||
import wText from '@/components/modules/widgets/wText/wText.vue'
|
||||
// import wText from '@/components/modules/widgets/wText/wText.vue'
|
||||
import { wTextSetting } from '@/components/modules/widgets/wText/wTextSetting'
|
||||
|
||||
export default () => {
|
||||
navigator.clipboard
|
||||
@ -47,7 +48,7 @@ export default () => {
|
||||
break
|
||||
} else if (item.types.toString().indexOf('text') !== -1) {
|
||||
store.commit('setShowMoveable', false) // 清理掉上一次的选择
|
||||
const setting = JSON.parse(JSON.stringify(wText.setting))
|
||||
const setting = JSON.parse(JSON.stringify(wTextSetting))
|
||||
setting.text = await navigator.clipboard.readText()
|
||||
store.dispatch('addWidget', setting)
|
||||
break
|
||||
|
11
src/types/global.d.ts
vendored
11
src/types/global.d.ts
vendored
@ -63,3 +63,14 @@ interface MouseEvent {
|
||||
layerY: number
|
||||
}
|
||||
|
||||
interface Document {
|
||||
selection?: Selection
|
||||
}
|
||||
|
||||
interface HTMLElement {
|
||||
createTextRange(): {
|
||||
moveToElementText(el: HTMLElement): void
|
||||
select(): void
|
||||
}
|
||||
}
|
||||
|
||||
|
6
src/types/style.d.ts
vendored
Normal file
6
src/types/style.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
namespace StyleProperty {
|
||||
type TextAlign = "center" | "end" | "left" | "right" | "start";
|
||||
type WritingMode = Globals | "horizontal-tb" | "sideways-lr" | "sideways-rl" | "vertical-lr" | "vertical-rl";
|
||||
}
|
@ -60,7 +60,7 @@ import { useStore } from 'vuex'
|
||||
import RightClickMenu from '@/components/business/right-click-menu/RcMenu.vue'
|
||||
import Moveable from '@/components/business/moveable/Moveable.vue'
|
||||
import shortcuts from '@/mixins/shortcuts'
|
||||
import wText from '@/components/modules/widgets/wText/wText.vue'
|
||||
// import wText from '@/components/modules/widgets/wText/wText.vue'
|
||||
import wImage from '@/components/modules/widgets/wImage/wImage.vue'
|
||||
import useLoading from '@/common/methods/loading'
|
||||
import uploader from '@/components/common/Uploader/index.vue'
|
||||
@ -71,6 +71,7 @@ import ProgressLoading from '@/components/common/ProgressLoading/index.vue'
|
||||
// import MyWorker from '@/utils/plugins/webWorker'
|
||||
import { processPSD2Page } from '@/utils/plugins/psd'
|
||||
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
|
||||
import { wTextSetting } from '@/components/modules/widgets/wText/wTextSetting'
|
||||
|
||||
type TState = {
|
||||
isDone: boolean
|
||||
@ -124,7 +125,7 @@ async function loadPSD(file: File) {
|
||||
|
||||
setTimeout(async () => {
|
||||
const types: any = {
|
||||
text: wText.setting,
|
||||
text: wTextSetting,
|
||||
image: wImage.setting,
|
||||
}
|
||||
for (let i = 0; i < data.clouds.length; i++) {
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @Date: 2022-01-12 11:26:53
|
||||
* @Description: 顶部操作按钮组
|
||||
* @LastEditors: ShawnPhang <https://m.palxp.cn>
|
||||
* @LastEditTime: 2023-12-11 12:40:59
|
||||
* @LastEditTime: 2024-03-11 01:43:30
|
||||
-->
|
||||
<template>
|
||||
<div class="top-title"><el-input v-model="state.title" placeholder="未命名的设计" class="input-wrap" /></div>
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { reactive, toRefs, defineEmits, defineProps, ref } from 'vue'
|
||||
import { reactive, toRefs, ref } from 'vue'
|
||||
import { mapGetters, mapActions, useStore } from 'vuex'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import _dl from '@/common/methods/download'
|
||||
@ -175,10 +175,10 @@ async function load(id: number, tempId: number, type: number, cb: () => void) {
|
||||
cb()
|
||||
return
|
||||
}
|
||||
const { data: content, title, state, width, height } = await api.home[apiName]({ id: id || tempId, type })
|
||||
const { data: content, title, state: _state, width, height } = await api.home[apiName]({ id: id || tempId, type })
|
||||
if (content) {
|
||||
const data = JSON.parse(content)
|
||||
state.stateBollean = (!!state)
|
||||
state.stateBollean = !!_state
|
||||
state.title = title
|
||||
store.commit('setShowMoveable', false) // 清理掉上一次的选择框
|
||||
// this.$store.commit('setDWidgets', [])
|
||||
|
Loading…
x
Reference in New Issue
Block a user