Commit 078d33fb by qlintonger xeno

添加截图标注组件+1

parent c6a268ca
import {ref, Ref} from "vue";
export enum AndroidBridgeInfoType {
Open,
Close,
Mark
}
/**
* 存储javascript:window.jsBridge({type: AndroidBridgeInfoType.Open, data: {}})传递过来的数据
*/
export const currentBridgeInfo = ref({}) as Ref
\ No newline at end of file
<template>
<i :class="displayedClass" @click="triggerDrawArrow"></i>
</template>
<script setup lang="ts">
import {CoreOperation} from "../objs/coreOperation";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {drawArrowsFunc} from "../funcs/drawArrows";
import {nextTick, computed} from "vue";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
let triggerDrawArrow = () => {
props.co.stopDraw()
props.co.triggerDraw('arrows')
}
nextTick(() => {
props.co.register(ProcessCycle.Start, 'arrows', drawArrowsFunc, {ctx: props.ctx})
props.co.register(ProcessCycle.Stamp, 'arrows', drawArrowsFunc, {ctx: props.ctx})
props.or.registerType('arrows', RecordType.Vector, 'A')
})
const displayedClass = computed(function () {
return props.co.getInternalState().currentDrawOption === 'arrows'
? 'iconfont icon-arrows activated': 'iconfont icon-arrows'
})
</script>
<template>
<i class="iconfont icon-delete" @click="triggerDrawClear"></i>
</template>
<script setup lang="ts">
import {CoreOperation} from "../objs/coreOperation";
import {nextTick} from "vue";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {DefaultNoopFunc} from "../types/HandlerFunc";
import {drawCanvas, resetCanvas} from "../objs/renderBasic";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
import {clearAll} from "../state/wholeTextInput";
import {restoreColorConfig, restoreLineWidthConfig} from "../state/operationConfigHisotry";
import {ConfigDrawAttachment} from "../types/AttachmentSpecifics";
import {OPERATION_CLEAR} from "../config/canvasConfig";
import {enableRecover} from "../state/canvasRecover";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
const triggerDrawClear = () => {
drawClear()
props.co.triggerDraw('clear')
props.or.removeRecord()
}
const clearAttachment: ConfigDrawAttachment = {
configType: OPERATION_CLEAR,
ctx: props.ctx
}
const drawClear: DefaultNoopFunc = () => {
clearAll()
restoreColorConfig()
restoreLineWidthConfig()
resetCanvas()
drawCanvas()
enableRecover.value = false
}
defineExpose({
'clear': triggerDrawClear
})
nextTick(() => {
props.co.register(ProcessCycle.Immediate, 'clear', drawClear, clearAttachment)
props.or.registerType('clear', RecordType.Immediate, 'C')
})
</script>
\ No newline at end of file
<template>
<n-popover trigger="click" v-model:show="showColorPicker">
<template #trigger>
<i class="iconfont icon-color" :style="{color: styledColor}"></i>
</template>
<div class="color-block-container">
<div v-for="(item, index) in colorBlock" :key="index" class="color-block"
:style="{backgroundColor: item}" @click="confirmColorPick(index)"></div>
</div>
</n-popover>
</template>
<script setup lang="ts">
import {NPopover} from "naive-ui";
import {nextTick} from "vue";
import {CoreOperation} from "../objs/coreOperation";
import {RecordRelatedFunc} from "../types/HandlerFunc";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {ConfigDrawAttachment} from "../types/AttachmentSpecifics";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
import {CONFIG_COLOR} from "../config/canvasConfig";
import {isDrawFromAction} from "../state/drawOperationType";
import {colorBlock, initialColorIndex, showColorPicker, styledColor} from "../state/operationConfig";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
let colorConfigAttachment: ConfigDrawAttachment = {
ctx: props.ctx,
configType: CONFIG_COLOR
}
let triggerDrawColorChanged: RecordRelatedFunc = (shouldRecord: boolean = true) => {
props.ctx.fillStyle = styledColor.value
props.ctx.strokeStyle = styledColor.value
if (shouldRecord) {
props.co.triggerDraw('colorConfig')
}
}
const confirmColorPick = (v: number) => {
initialColorIndex.value = v
showColorPicker.value = false
triggerDrawColorChanged(isDrawFromAction())
}
nextTick(() => {
props.co.register(ProcessCycle.Immediate, 'colorConfig', triggerDrawColorChanged, colorConfigAttachment)
props.or.registerType('colorConfig', RecordType.Index, 'Y')
})
</script>
<style scoped>
@import url("../resources/styles/color-block.css");
@import url("../resources/styles/opitem.css");
@import url("../resources/styles/media-adapt.css");
</style>
\ No newline at end of file
<template>
<i :class="displayedClass" @click="triggerDrawEllipse"></i>
</template>
<script setup lang="ts">
import {CoreOperation} from "../objs/coreOperation";
import {computed, nextTick} from "vue";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {drawEllipse} from "../funcs/drawEclipse";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
const triggerDrawEllipse = () => {
props.co.stopDraw()
props.co.triggerDraw('ellipse')
}
nextTick(() => {
props.co.register(ProcessCycle.Start, 'ellipse', drawEllipse, {ctx: props.ctx})
props.co.register(ProcessCycle.Stamp, 'ellipse', drawEllipse, {ctx: props.ctx})
props.or.registerType('ellipse', RecordType.Vector, 'E')
})
const displayedClass = computed(function () {
return props.co.getInternalState().currentDrawOption === 'ellipse'
? 'iconfont icon-circle activated': 'iconfont icon-circle'
})
</script>
\ No newline at end of file
<template>
<i :class="displayedClass" @click="triggerDrawErase"></i>
</template>
<script setup lang="ts">
import {CoreOperation} from "../objs/coreOperation";
import {computed, nextTick} from "vue";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {drawErase} from "../funcs/drawErase";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
// 存储持续绘制橡皮擦的attachment
let eraseAttachment = {
ctx: undefined as undefined | CanvasRenderingContext2D,
o_ctx: undefined as undefined | CanvasRenderingContext2D,
points: []
}
const triggerDrawErase = () => {
props.co.stopDraw()
props.co.triggerDraw('erase')
}
nextTick(() =>{
eraseAttachment.ctx = props.ctx
eraseAttachment.o_ctx = props.o_ctx
props.co.register(ProcessCycle.Start, 'erase', drawErase, eraseAttachment)
props.co.register(ProcessCycle.Stamp, 'erase', drawErase, eraseAttachment)
props.or.registerType('erase', RecordType.Continuous, 'X')
})
const displayedClass = computed(function () {
return props.co.getInternalState().currentDrawOption === 'erase'
? 'iconfont icon-eraser activated': 'iconfont icon-eraser'
})
</script>
<style scoped>
@import url("../resources/styles/media-adapt.css");
</style>
\ No newline at end of file
<template>
<n-popover trigger="click" v-model:show="showExpressionPicker" display-directive="if">
<template #trigger>
<i :class="displayedClass"></i>
</template>
<div class="color-block-container">
<img
v-for="(item, index) in pics"
@click="confirmExpressionPick(index)"
:key="index"
class="color-block"
alt="expression addon"
:src="item"
/>
</div>
</n-popover>
<div class="for-view-only" id="draw-box-image">
<img
v-for="(item, index) in pics"
:key="index"
class="color-block"
alt="expression addon"
:src="item"
/>
</div>
</template>
<script setup lang="ts">
// @ts-nocheck
import {NPopover} from "naive-ui";
import {computed, nextTick, ref} from "vue";
import {CoreOperation} from "../objs/coreOperation";
import {drawExpression} from "../funcs/drawExpression";
import {PayloadDrawAttachment} from "../types/AttachmentSpecifics";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
import E1 from "../resources/expressions/1.png"
import E2 from "../resources/expressions/2.png"
import E3 from "../resources/expressions/3.png"
import E4 from "../resources/expressions/4.png"
import E5 from "../resources/expressions/5.png"
import E6 from "../resources/expressions/6.png"
import E7 from "../resources/expressions/7.png"
import E8 from "../resources/expressions/8.png"
import E9 from "../resources/expressions/9.png"
import E10 from "../resources/expressions/10.png"
import E11 from "../resources/expressions/11.png"
import E12 from "../resources/expressions/12.png"
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
// 虚假数据,只是为了演示用
const pics = ref([
E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12
])
// 挑选表情的处理
const selectedIndex = ref(-1)
const showExpressionPicker = ref(false)
const confirmExpressionPick = (v: any) => {
showExpressionPicker.value = false
selectedIndex.value = v
triggerDrawExpression()
}
// 挑选表情的attachment
const expressionAttachment:PayloadDrawAttachment = {
ctx: props.ctx,
payload: null
}
const triggerDrawExpression = () => {
expressionAttachment.payload = selectedIndex.value
props.co.triggerDraw('expression')
}
nextTick(() => {
props.co.register(ProcessCycle.Start, 'expression', drawExpression, expressionAttachment)
props.co.register(ProcessCycle.Stamp, 'expression', drawExpression, expressionAttachment)
props.or.registerType('expression', RecordType.VectorPayload, 'H')
})
const displayedClass = computed(function () {
return props.co.getInternalState().currentDrawOption === 'expression'
? 'iconfont icon-expression activated': 'iconfont icon-expression'
})
</script>
<style scoped>
@import url("../resources/styles/color-block.css");
@import url("../resources/styles/media-adapt.css");
.for-view-only {
display: none !important;
}
</style>
\ No newline at end of file
<template>
<n-popselect v-model:value="picFont" trigger="click" :options="picFontOptions" size="small"
:on-update:value="confirmLineWidthPick">
<i class="iconfont">{{ picFont }}</i>
</n-popselect>
</template>
<script setup lang="ts">
import {nextTick} from "vue";
import {NPopselect} from "naive-ui";
import {CoreOperation} from "../objs/coreOperation";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {ConfigDrawAttachment} from "../types/AttachmentSpecifics";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
import {RecordRelatedFunc} from "../types/HandlerFunc";
import {CONFIG_LINE_WIDTH} from "../config/canvasConfig";
import {isDrawFromAction} from "../state/drawOperationType";
import {picFont, picFontOptions} from "../state/operationConfig";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
let lineWidthAttachment: ConfigDrawAttachment = {
ctx: props.ctx,
configType: CONFIG_LINE_WIDTH
}
const changeLineWidth: RecordRelatedFunc = (shouldRecord: boolean = true) => {
props.ctx.lineWidth = picFont.value
if (shouldRecord) {
props.co.triggerDraw('lineWidth')
}
}
const confirmLineWidthPick = (nv: any) => {
picFont.value = nv
changeLineWidth(isDrawFromAction())
}
nextTick(() => {
props.co.register(ProcessCycle.Immediate, 'lineWidth', changeLineWidth, lineWidthAttachment)
props.or.registerType('lineWidth', RecordType.Index, 'L')
})
</script>
<style scoped>
@import url("../resources/styles/opitem.css");
@import url("../resources/styles/media-adapt.css");
.iconfont {
margin-right: 0 !important;
}
</style>
\ No newline at end of file
<template></template>
<script setup>
import {useMessage} from 'naive-ui'
import {inject, watch} from "vue";
const message = useMessage()
const loadingInfo = inject('loadingInfo')
let currentNotifier = null
watch(loadingInfo, function () {
if (loadingInfo?.value?.display) {
currentNotifier = message.loading(loadingInfo?.value?.info, {
duration: 0
})
} else {
if (currentNotifier !== null) {
currentNotifier.destroy()
}
}
}, {deep: true, immediate: true})
</script>
\ No newline at end of file
<template>
<i class="iconfont icon-left_rotation" :class="{'history-disabled': !canRevoke}" @click="triggerRollback"></i>
<i class="iconfont icon-right_rotation" :class="{'history-disabled': !enableRecover}" @click="triggerRecover"></i>
</template>
<script setup lang="ts">
import {CoreOperation} from "../objs/coreOperation";
import {computed, nextTick, watch} from "vue";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {DefaultNoopFunc} from "../types/HandlerFunc";
import {unifyArray} from "../utils/object";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType, VectorRecord} from "../types/OperationRecordTyping";
import {drawRecord} from "../funcs/drawRecord";
import {setDrawFromAction, setDrawFromRecord} from "../state/drawOperationType";
import {enableRecover, rollbackStep} from "../state/canvasRecover";
import {clearAll} from "../state/wholeTextInput";
import {CoreInternalState} from "../types/CoreOperationTyping";
import {CONFIG_COLOR, CONFIG_LINE_WIDTH, OPERATION_CLEAR} from "../config/canvasConfig";
import {initialColorIndex, picFont} from "../state/operationConfig";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
start: {
type: Boolean,
default: true
},
or: {
type: OperationRecord,
required: true
}
})
const canRevoke = computed(() => {
const rawRecord = props.or.getInternalState().rawRecord
return rawRecord.length > 0 && rollbackStep.value < rawRecord.length
})
const storeRecord: DefaultNoopFunc = () => {
const internalState: CoreInternalState = props.co.getInternalState()
// 立即触发类
if (internalState.isHandleImmediate) {
// 清除
if (Reflect.get(internalState.currentDrawAttachment, 'configType') === OPERATION_CLEAR) {
props.or.storeRecord(internalState.currentImmediateOption, null)
}
// 设置颜色
else if (Reflect.get(internalState.currentDrawAttachment, 'configType') === CONFIG_COLOR) {
props.or.storeRecord(internalState.currentImmediateOption, initialColorIndex.value)
}
// 设置宽度
else if (Reflect.get(internalState.currentDrawAttachment, 'configType') === CONFIG_LINE_WIDTH) {
props.or.storeRecord(internalState.currentImmediateOption, picFont.value)
}
// 是否涉及到操作记录回溯
else if (Reflect.has(internalState.currentDrawAttachment, 'recordOpType')) {
props.or.storeRecord(internalState.currentImmediateOption, rollbackStep.value)
}
return
}
// 如果是连续性绘制,减少重复点
if (Reflect.has(internalState.currentDrawAttachment, 'points')) {
props.or.storeRecord(internalState.currentDrawOption, unifyArray(internalState.currentDrawAttachment.points))
}
// 如果存在清空,则直接清零,减少数据传输长度
else if (Reflect.has(internalState.currentDrawAttachment, 'clear')) {
props.or.storeRecord(internalState.currentImmediateOption, null)
}
// 其余线性矢量绘图,直接传入坐标即可
else {
const vectorData = {
sx: internalState.sx,
sy: internalState.sy,
ex: internalState.ex,
ey: internalState.ey,
payload: null
}
// 传输贴画类,是需要同时输入x和y坐标以及图像选择器
if (Reflect.has(internalState.currentDrawAttachment, 'payload')) {
vectorData.payload = internalState.currentDrawAttachment.payload
props.or.storeRecord(internalState.currentDrawOption, vectorData as VectorRecord)
} else {
props.or.storeRecord(internalState.currentDrawOption, vectorData as VectorRecord)
}
}
}
// 如果所有截图程序退出,则清空记录
watch(() => props.start, () => {
if (!props.start) {
props.or.removeRecord()
setDrawFromAction()
enableRecover.value = false
clearAll()
}
})
// 触发撤销操作,根据操作历史记录直接调用,注意需要在预定义的placeholder画板和canvas画板上同时进行操作
// 在这里进行绘图操作,是为了不触发storeRecord方法,并且可以主动侦测到其他绘制方法的调用
const triggerRollback = () => {
if (!canRevoke.value) return
enableRecover.value = true
setDrawFromRecord()
if (rollbackStep.value < props.or.getInternalState().rawRecord.length) {
rollbackStep.value++
const {rawRecord, operationRecordTypeSet} = props.or.getInternalState()
drawRecord(
rawRecord.slice(0, rawRecord.length - rollbackStep.value),
props.co,
operationRecordTypeSet,
props.ctx
)
}
rollBackFunc()
}
// 触发重绘操作,同上
const triggerRecover = () => {
if (!enableRecover.value) return
setDrawFromRecord()
rollbackStep.value--
const {rawRecord, operationRecordTypeSet} = props.or.getInternalState()
drawRecord(
rawRecord.slice(0, rawRecord.length - rollbackStep.value),
props.co,
operationRecordTypeSet,
props.ctx
)
if (rollbackStep.value == 0) {
enableRecover.value = false
}
recoverFunc()
}
const rollBackFunc = () => {
props.co.triggerDraw('rollback')
}
const recoverFunc = () => {
props.co.triggerDraw('recover')
}
nextTick(() => {
props.co.register(ProcessCycle.Immediate, 'record', storeRecord, {}, true)
props.co.register(ProcessCycle.Stop, 'record', storeRecord, {}, true)
// 涉及到撤销或者回溯的记录发送,视为空,制作记录的截取
props.or.registerType('rollback', RecordType.Revoke, 'B')
props.or.registerType('recover', RecordType.Recover, 'F')
props.co.register(ProcessCycle.Immediate, 'rollback', rollBackFunc, {recordOpType: RecordType.Revoke})
props.co.register(ProcessCycle.Immediate, 'recover', recoverFunc, {recordOpType: RecordType.Recover})
})
</script>
<style scoped>
@import url("../resources/styles/opitem.css");
@import url("../resources/styles/media-adapt.css");
.history-disabled {
cursor: not-allowed !important;
color: darkgray !important;
}
</style>
\ No newline at end of file
<template>
<i :class="displayedClass" @click="triggerDrawRect"></i>
</template>
<script setup lang="ts">
import {CoreOperation} from "../objs/coreOperation";
import {computed, nextTick} from "vue";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {drawRect} from "../funcs/drawRect";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
const triggerDrawRect = () => {
props.co.stopDraw()
props.co.triggerDraw('rect')
}
nextTick(() => {
props.co.register(ProcessCycle.Start, 'rect', drawRect, {ctx: props.ctx})
props.co.register(ProcessCycle.Stamp, 'rect', drawRect, {ctx: props.ctx})
props.or.registerType('rect', RecordType.Vector, 'R')
})
const displayedClass = computed(function () {
return props.co.getInternalState().currentDrawOption === 'rect'
? 'iconfont icon-rectangle activated': 'iconfont icon-rectangle'
})
</script>
\ No newline at end of file
<template>
<i :class="displayedClass" @click="triggerDrawStroke"></i>
</template>
<script setup lang="ts">
import {CoreOperation} from "../objs/coreOperation";
import {computed, nextTick} from "vue";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {drawStroke} from "../funcs/drawStroke";
import {OperationRecord} from "../objs/operationRecord";
import {RecordType} from "../types/OperationRecordTyping";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
let triggerDrawStroke = () => {
props.co.stopDraw()
props.co.triggerDraw('stroke')
}
// 存储持续绘制线条的attachment
let strokeAttachment = {
ctx: undefined as undefined | CanvasRenderingContext2D,
points: []
}
let clearStroke = () => {
strokeAttachment.points.length = 0
}
nextTick(()=>{
strokeAttachment.ctx = props.ctx
props.co.register(ProcessCycle.Start, 'stroke', drawStroke, strokeAttachment)
props.co.register(ProcessCycle.Stamp, 'stroke', drawStroke, strokeAttachment)
props.co.register(ProcessCycle.Stop, 'stroke', clearStroke, strokeAttachment)
props.or.registerType('stroke', RecordType.Continuous, 'S')
})
const displayedClass = computed(function () {
return props.co.getInternalState().currentDrawOption === 'stroke'
? 'iconfont icon-brush activated': 'iconfont icon-brush'
})
</script>
<template>
<i class="iconfont icon-text" @click="triggerDrawTextWidget"></i>
</template>
<script setup lang="ts">
import {CoreOperation} from "../objs/coreOperation";
import {OperationRecord} from "../objs/operationRecord";
import {nextTick} from "vue";
import {ComplexDrawHandleFunc} from "../types/HandlerFunc";
import {ProcessCycle} from "../types/HandleProcessCycle";
import {createUUID} from "../utils/math";
import {
addNewWidget,
pruneWidget,
setCurrentActionWidgetId,
wholeTextInputWidgets as inputAnchorDataSet,
} from "../state/wholeTextInput";
import {RecordType} from "../types/OperationRecordTyping";
import {textWidgetAttachment} from "../state/textWidgetAttachment";
const props = defineProps({
co: {
type: CoreOperation,
required: true
},
ctx: {
type: CanvasRenderingContext2D,
required: true
},
o_ctx: {
type: CanvasRenderingContext2D,
required: true
},
or: {
type: OperationRecord,
required: true
}
})
// 判断是否点击坐标在任何一个文本输入框之内
const isInAnyWidget = (sx: number, sy: number): boolean => {
for (const item of inputAnchorDataSet.value) {
if (sx >= item.sx && sx <= item.sx + item.width && sy >= item.sy && sy <= item.sy + item.height)
return true
}
return false
}
const triggerDrawTextWidget = () => {
props.co.stopDraw()
props.co.triggerDraw('text')
}
// 由于创建[TIWS]组件是独立在几何画板之外,只需要获取开始点坐标的,因此在此独立撰写添加逻辑
const drawTextWidget: ComplexDrawHandleFunc = (
sx: number | undefined,
sy: number | undefined,
ex: number | undefined,
ey: number | undefined,
attachment: any) => {
if (!isInAnyWidget(sx as number, sy as number)) {
pruneWidget()
const id = createUUID()
addNewWidget({
id,
sx: sx as number,
sy: sy as number,
width: 200,
height: 21,
text: attachment.payload ? attachment.payload : '',
display: true
})
setCurrentActionWidgetId(id)
}
}
const pruneTextWidget = () => {
if (props.co.getInternalState().currentDrawOption !== 'text') {
pruneWidget()
}
}
nextTick(() => {
props.co.register(ProcessCycle.Start, 'text', drawTextWidget, textWidgetAttachment)
props.co.register(ProcessCycle.BeforeStart, 'text', pruneTextWidget, {})
props.or.registerType('text', RecordType.VectorPayload, 'T')
})
</script>
\ No newline at end of file
export const color = 'red'
export const lineWidth = 5
export const CONFIG_COLOR = 'color'
export const OPERATION_CLEAR = 'clear'
export const CONFIG_LINE_WIDTH = 'lineWidth'
export const COLOR_SELECTION_SET = ['red', 'green', 'blue', 'yellow', 'pink', 'gray', 'white', 'aqua', 'brown']
export const COLOR_INITIAL_SELECTION_INDEX = 0
export const LINE_WIDTH_SELECTION_SET = [1, 2, 3, 4, 5, 6, 7]
export const LINE_WIDTH_CONFIG_SET = LINE_WIDTH_SELECTION_SET.map(i=>({value: i, label: `${i} 线宽`}))
export const LINE_WIDTH_INITIAL_SELECTION_INDEX = 4
\ No newline at end of file
export enum DrawRecordDirection {
BACKWARDS,
FORWARDS
}
\ No newline at end of file
import {ObjectDirective} from "vue";
export const vFocus:ObjectDirective = {
mounted(el:HTMLElement) {
setTimeout(()=>{
el.focus()
},0)
}
}
\ No newline at end of file
// @ts-nocheck
import {drawCanvas} from "../objs/renderBasic";
import {ComplexDrawHandleFunc} from "../types/HandlerFunc";
import {BaseDrawAttachment} from "../types/AttachmentSpecifics";
import {isDrawFromAction} from "../state/drawOperationType";
export const drawArrowsFunc: ComplexDrawHandleFunc = function (
sx,
sy,
ex,
ey,
attachment: BaseDrawAttachment) {
if (isDrawFromAction())
drawCanvas()
const ctx = attachment.ctx
arrowDrawAct(ctx, sx, sy, ex, ey)
}
export function arrowDrawAct(ctx, sx, sy, ex, ey) {
ctx.beginPath()
ctx.moveTo(sx, sy)
ctx.lineTo(ex, ey)
const angle = Math.atan((ey - sy) / (ex - sx))
if (ex - sx > 0) {
ctx.lineTo(30 * Math.cos(angle + 7 * Math.PI / 6) + ex, 30 * Math.sin(angle + 7 * Math.PI / 6) + ey)
ctx.moveTo(ex, ey)
ctx.lineTo(30 * Math.cos(angle + 5 * Math.PI / 6) + ex, 30 * Math.sin(angle + 5 * Math.PI / 6) + ey)
} else if (ex === sx) {
ctx.lineTo(30 * Math.cos(4 * Math.PI / 3) + ex, 30 * Math.sin(4 * Math.PI / 3) + ey)
ctx.moveTo(ex, ey)
ctx.lineTo(30 * Math.cos(5 * Math.PI / 3) + ex, 30 * Math.sin(5 * Math.PI / 3) + ey)
} else {
ctx.lineTo(30 * Math.cos(angle + Math.PI / 6) + ex, 30 * Math.sin(angle + Math.PI / 6) + ey)
ctx.moveTo(ex, ey)
ctx.lineTo(30 * Math.cos(angle - Math.PI / 6) + ex, 30 * Math.sin(angle - Math.PI / 6) + ey)
}
ctx.stroke()
}
\ No newline at end of file
// @ts-nocheck
import {drawCanvas} from "../objs/renderBasic";
import {ComplexDrawHandleFunc} from "../types/HandlerFunc";
import {BaseDrawAttachment} from "../types/AttachmentSpecifics";
import {isDrawFromAction} from "../state/drawOperationType";
export const drawEllipse: ComplexDrawHandleFunc = function (
sx,
sy,
ex,
ey,
attachment: BaseDrawAttachment
) {
if (typeof ex === undefined || typeof ey === undefined) return
if (isDrawFromAction())
drawCanvas()
const {ctx} = attachment
ellipseDrawAct(ctx, sx, sy, ex, ey)
}
export function ellipseDrawAct(ctx, sx, sy, ex, ey) {
ctx.beginPath()
ctx.ellipse((ex + sx) / 2, (ey + sy) / 2, Math.abs((ex - sx) / 2), Math.abs((ey - sy) / 2), 0, 0, 2 * Math.PI)
ctx.stroke()
}
\ No newline at end of file
import {drawCanvas, plantCanvas} from "../objs/renderBasic";
import {ComplexDrawHandleFunc} from "../types/HandlerFunc";
import {ComplexDrawAttachment, ContinuousDrawAttachment} from "../types/AttachmentSpecifics";
import {isDrawFromAction} from "../state/drawOperationType";
export const drawErase: ComplexDrawHandleFunc = function (
sx,
sy,
ex,
ey,
attachment: ComplexDrawAttachment & ContinuousDrawAttachment) {
if (typeof ex === 'undefined' || typeof ey === 'undefined') return
if (isDrawFromAction())
drawCanvas()
const {ctx, o_ctx, points} = attachment
points.push({ex, ey})
eraseDrawAct(ctx, o_ctx, ex, ey)
if (isDrawFromAction())
plantCanvas()
}
// @ts-ignore
export function eraseDrawAct(ctx, o_ctx, ex, ey) {
ctx.drawImage(o_ctx.canvas, ex - 6, ey - 6, 12, 12, ex - 6, ey - 6, 12, 12)
}
\ No newline at end of file
// @ts-nocheck
import {drawCanvas} from "../objs/renderBasic";
import {ComplexDrawHandleFunc} from "../types/HandlerFunc";
import {PayloadDrawAttachment} from "../types/AttachmentSpecifics";
import {isDrawFromAction} from "../state/drawOperationType";
export const drawExpression: ComplexDrawHandleFunc = function (
sx,
sy,
ex,
ey,
attachment: PayloadDrawAttachment) {
if (isDrawFromAction())
drawCanvas()
const ctx: CanvasRenderingContext2D = attachment.ctx
const payload = Number(attachment.payload) + 1
const selector = `#draw-box-image.for-view-only img:nth-of-type(${payload})`
const imageToBeDrawn = document.querySelector(selector) as HTMLImageElement
const width = parseInt(window.getComputedStyle(imageToBeDrawn).width)
const height = parseInt(window.getComputedStyle(imageToBeDrawn).height)
const cx = typeof ex === 'undefined' || Number.isNaN(ex) ? sx : ex
const cy = typeof ey === 'undefined' || Number.isNaN(ey) ? sy : ey
ctx.drawImage(
imageToBeDrawn,
0,
0,
imageToBeDrawn.naturalWidth,
imageToBeDrawn.naturalHeight,
cx - width / 2,
cy - height / 2,
width,
height
)
}
import {OperationRecordTyping, RawRecordItem, RecordType} from "../types/OperationRecordTyping";
import {CoreOperation} from "../objs/coreOperation";
import {drawCanvas, resetCanvas, setupCanvas} from "../objs/renderBasic";
import {ProcessCycleItem} from "../types/HandleProcessCycle";
import {setupColorConfig, setupLineWidthConfig} from "../state/operationConfigHisotry";
import {setDrawFromRecord} from "../state/drawOperationType";
export function drawOtherRecord (
records: Array<RawRecordItem>,
op: CoreOperation,
typing: OperationRecordTyping,
ctx: CanvasRenderingContext2D
) {
setDrawFromRecord()
drawRecord(
records,
op,
typing,
ctx
)
drawRecord(
// @ts-ignore
op.record?.getInternalState().rawRecord,
op,
op.record?.getInternalState().operationRecordTypeSet,
ctx,
true
)
}
export function drawRecord (
records: Array<RawRecordItem>,
op: CoreOperation,
typing: OperationRecordTyping,
ctx: CanvasRenderingContext2D,
keepCurrentScratch = false) {
if (!keepCurrentScratch) {
resetCanvas()
drawCanvas()
}
const operationInternalState = op.getInternalState()
setupCanvas(ctx)
records.forEach(({name, record}) => {
const operationTyping = Reflect.get(typing, name) as RecordType
if (operationTyping === RecordType.Immediate) {
const foundedImmediate = operationInternalState.immediatePs.find(e => e.name === name) as ProcessCycleItem
return foundedImmediate.func.call(null)
}
if (operationTyping === RecordType.Continuous) {
const foundedStamp = Reflect.get(operationInternalState.stampProcess, name) as ProcessCycleItem
foundedStamp.attachment.points.length = 0
const continuousPoints = (record as Array<{ ex: number, ey: number }>)
continuousPoints.forEach(point => {
foundedStamp.func.call(null, undefined, undefined, point.ex, point.ey, foundedStamp.attachment)
})
foundedStamp.attachment.points.length = 0
return
}
// 目前所有索引类记录只涉及到canvas的图像设置,并且操作均为Immediate
if (operationTyping === RecordType.Index) {
// 如果是回溯,则需要调整到前一个同类型操作
const foundedImmediate = operationInternalState.immediatePs.find(e => e.name === name) as ProcessCycleItem
if (name === 'colorConfig') {
setupColorConfig(record)
foundedImmediate.func.call(null, false)
} else if (name === 'lineWidth') {
setupLineWidthConfig(record)
foundedImmediate.func.call(null, false)
}
return
}
if (operationTyping === RecordType.VectorPayload) {
const foundedStamp = Reflect.get(operationInternalState.stampProcess, name) as ProcessCycleItem
foundedStamp.attachment.payload = record.payload
return foundedStamp.func.call(null, record.sx, record.sy, record.ex, record.ey, foundedStamp.attachment)
}
if (operationTyping === RecordType.Vector) {
const foundedStamp = Reflect.get(operationInternalState.stampProcess, name) as ProcessCycleItem
foundedStamp.func.call(null, record.sx, record.sy, record.ex, record.ey, foundedStamp.attachment)
}
})
}
\ No newline at end of file
import {drawCanvas} from "../objs/renderBasic";
import {ComplexDrawHandleFunc} from "../types/HandlerFunc";
import {BaseDrawAttachment} from "../types/AttachmentSpecifics";
import {drawOperationType, isDrawFromAction} from "../state/drawOperationType";
export const drawRect: ComplexDrawHandleFunc = function (
sx,
sy,
ex,
ey,
attachment: BaseDrawAttachment) {
if (typeof ex === 'undefined' || typeof ey === 'undefined') return
if (isDrawFromAction())
drawCanvas()
if (typeof sx === "number" && typeof sy === "number") {
const {ctx} = attachment
rectDrawAct(ctx, sx, sy, ex, ey)
}
}
// @ts-ignore
export function rectDrawAct(ctx, sx, sy, ex, ey) {
ctx.beginPath()
ctx.rect(Math.min(sx, ex), Math.min(sy, ey), Math.abs(sx - ex), Math.abs(sy - ey))
ctx.stroke()
}
\ No newline at end of file
import {drawCanvas} from "../objs/renderBasic";
import {ComplexDrawHandleFunc} from "../types/HandlerFunc";
import {ContinuousDrawAttachment} from "../types/AttachmentSpecifics";
import {isDrawFromAction} from "../state/drawOperationType";
export const drawStroke: ComplexDrawHandleFunc = function (
sx,
sy,
ex,
ey,
attachment: ContinuousDrawAttachment) {
if (typeof ex === 'undefined' || typeof ey === 'undefined') return
if (isDrawFromAction())
drawCanvas()
const {ctx, points} = attachment
points.push({ex, ey})
ctx.beginPath()
points.forEach((item, index) => {
if (index === 0) {
return ctx.moveTo(item.ex, item.ey)
}
ctx.lineTo(item.ex, item.ey)
})
ctx.stroke()
}
\ No newline at end of file
import {
ContinuousRecord,
GenericRecordType,
IndexRecord,
OperationRecordAlias,
OperationRecordInternalState,
OperationRecordTyping,
RawRecordItem,
RecordType,
VectorRecord
} from "../types/OperationRecordTyping";
import {deepClone} from "../utils/object";
export class OperationRecord {
private readonly operationRecordTypeSet: OperationRecordTyping
private readonly operationRecordAlias: OperationRecordAlias
private rawRecord: Array<RawRecordItem>
private operationRecord: string
private relativeWidth: number = 0
private relativeHeight: number = 0
private recordListener: Function
constructor(recordListener: Function) {
this.operationRecordTypeSet = {}
this.operationRecordAlias = {}
this.rawRecord = []
this.operationRecord = ''
this.recordListener = recordListener
}
registerType(name: string, type: RecordType, alias: string) {
Reflect.set(this.operationRecordTypeSet, name, type)
Reflect.set(this.operationRecordAlias, name, alias)
}
setRelativeGEO(width: number, height: number) {
this.relativeWidth = width
this.relativeHeight = height
}
public setNewRawRecord(records: Array<RawRecordItem>) {
this.rawRecord = deepClone(records)
this.operationRecord = ''
this.rawRecord.forEach(recordItem => {
this.storeRecord(recordItem.name, recordItem.record, false)
})
}
storeRecord(name: string, record: GenericRecordType, recording = true) {
const currentRecordType: RecordType = Reflect.get(this.operationRecordTypeSet, name)
const currentRecordAlias: string = Reflect.get(this.operationRecordAlias, name)
if (recording && ![RecordType.Revoke, RecordType.Recover].includes(currentRecordType)) {
this.rawRecord.push({
name,
record
})
}
this.operationRecord += this.makeRecordString(record, currentRecordAlias, currentRecordType)
if (currentRecordType === RecordType.Recover || currentRecordType === RecordType.Revoke) {
// @ts-ignore
const slicedRecords = this.rawRecord.slice(0, this.rawRecord.length - record)
.reduce((q, a)=>{
const recordType: RecordType = Reflect.get(this.operationRecordTypeSet, a.name)
const alias: string = Reflect.get(this.operationRecordAlias, a.name)
const record = this.makeRecordString(a.record, alias, recordType)
return q + record
}, '')
this.recordListener.call(null, slicedRecords)
}
else {
this.recordListener.call(null, this.operationRecord)
}
}
private makeRecordString(record: GenericRecordType, currentRecordAlias: string, currentRecordType: RecordType) {
switch (currentRecordType) {
case RecordType.Vector:
const vectorRecord = (record as VectorRecord)
return `${currentRecordAlias} ${(vectorRecord.sx / this.relativeWidth).toFixed(2)} ${(vectorRecord.sy / this.relativeHeight).toFixed(2)} ${(vectorRecord.ex / this.relativeWidth).toFixed(2)} ${(vectorRecord.ey / this.relativeHeight).toFixed(2)} `
case RecordType.Continuous:
const continuousRecord = (record as ContinuousRecord)
return `${currentRecordAlias} ${continuousRecord.map(item => [(item.ex / this.relativeWidth).toFixed(2), (item.ey / this.relativeHeight).toFixed(2)]).flat().join(' ')} `
case RecordType.Index:
const indexRecord = (record as IndexRecord)
return `${currentRecordAlias} ${indexRecord} `
case RecordType.Immediate:
return `${currentRecordAlias} `
case RecordType.VectorPayload:
const pointPayloadRecord = (record as VectorRecord)
return `${currentRecordAlias} ${(pointPayloadRecord.sx / this.relativeWidth).toFixed(2)} ${(pointPayloadRecord.sy / this.relativeHeight).toFixed(2)} ${(pointPayloadRecord.ex / this.relativeWidth).toFixed(2)} ${(pointPayloadRecord.ey / this.relativeHeight).toFixed(2)} ${pointPayloadRecord.payload} `
case RecordType.Recover:
case RecordType.Revoke:
case RecordType.UNSET:
default:
return ''
}
}
parseRecord(inputRecord: string): Array<RawRecordItem> {
let wholeRecord: Array<RawRecordItem> = []
let inputRecordSplit: Array<string> = inputRecord.trim().split(' ')
let paintCommandAlias: string
let paintCommand: string
try {
inputRecordSplit.forEach(span => {
if (!Object.values(this.operationRecordAlias).includes(span)) {
const lastInsertedRecord = wholeRecord[wholeRecord.length - 1]
const lastRecordType = this.operationRecordTypeSet[lastInsertedRecord.name]
if (lastRecordType === RecordType.Vector || lastRecordType === RecordType.VectorPayload) {
const objectKeyLength = Object.keys(lastInsertedRecord.record).length
if (objectKeyLength === 0) {
lastInsertedRecord.record.sx = Number(span) * this.relativeWidth
return
}
if (objectKeyLength === 1) {
lastInsertedRecord.record.sy = Number(span) * this.relativeHeight
return
}
if (objectKeyLength === 2) {
lastInsertedRecord.record.ex = Number(span) * this.relativeWidth
return
}
if (objectKeyLength === 3) {
lastInsertedRecord.record.ey = Number(span) * this.relativeHeight
}
if (lastRecordType === RecordType.VectorPayload) {
lastInsertedRecord.record.payload = span
} else {
lastInsertedRecord.record.payload = null
}
return;
}
if (lastRecordType === RecordType.Continuous) {
if (!Array.isArray(lastInsertedRecord.record)) {
lastInsertedRecord.record = []
}
const lastRecordExEyItem = lastInsertedRecord.record[lastInsertedRecord.record.length - 1]
if (!lastRecordExEyItem || lastRecordExEyItem.ey) {
const newlyAddedItem = {
ex: Number(span) * this.relativeWidth
}
lastInsertedRecord.record.push(newlyAddedItem)
return
}
if (!lastRecordExEyItem.ey) {
lastRecordExEyItem.ey = Number(span) * this.relativeHeight
}
return
}
if (lastRecordType === RecordType.Index) {
lastInsertedRecord.record = Number(span)
return
}
lastInsertedRecord.record = null
return
}
paintCommandAlias = span[0]
paintCommand = Object.keys(this.operationRecordAlias).find(key => this.operationRecordAlias[key] === paintCommandAlias) as string
let currentFoundRecord: RawRecordItem = {
name: '',
record: {}
}
currentFoundRecord.name = paintCommand
wholeRecord.push(currentFoundRecord)
})
} catch (e) {
console.log('error???', e)
return []
}
return wholeRecord
}
removeRecord() {
this.rawRecord.length = 0
this.operationRecord = ''
}
public getInternalState(): OperationRecordInternalState {
return {
operationRecordTypeSet: this.operationRecordTypeSet,
operationRecord: this.operationRecord,
operationRecordAlias: this.operationRecordAlias,
rawRecord: this.rawRecord
}
}
}
\ No newline at end of file
import {color, lineWidth} from "../config/canvasConfig";
import {restoreColorConfig, restoreLineWidthConfig} from "../state/operationConfigHisotry";
export const triggerRenderBasic = {
renderDrawArgs: [] as Array<any>,
renderResetArgs: [] as Array<any>,
renderPlantArgs: [] as Array<any>,
draw: 0,
reset: 0,
plant: 0
}
const renderBasic = function (
ctx: CanvasRenderingContext2D,
target: CanvasImageSource,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
) {
ctx.clearRect(0, 0, dw, dh)
ctx.drawImage(target, sx, sy, sw, sh, dx, dy, dw, dh)
}
let draw = 0
let plant = 0
let reset = 0
Object.defineProperty(triggerRenderBasic, 'reset', {
get() {
return reset
},
set(v) {
reset = v
// @ts-ignore
renderBasic.apply(null, triggerRenderBasic.renderResetArgs)
}
})
Object.defineProperty(triggerRenderBasic, 'plant', {
get() {
return plant
},
set(v) {
plant = v
// @ts-ignore
renderBasic.apply(null, triggerRenderBasic.renderPlantArgs)
}
})
Object.defineProperty(triggerRenderBasic, 'draw', {
get() {
return draw
},
set(v) {
draw = v
// @ts-ignore
renderBasic.apply(null, triggerRenderBasic.renderDrawArgs)
}
})
export function drawCanvas() {
triggerRenderBasic.draw++
}
export function plantCanvas() {
triggerRenderBasic.plant++
}
export function resetCanvas() {
triggerRenderBasic.reset++
}
export function setupCanvasColor(ctx: CanvasRenderingContext2D) {
ctx.strokeStyle = color
ctx.fillStyle = color
restoreColorConfig()
}
export function setupCanvasLineWidth(ctx: CanvasRenderingContext2D) {
ctx.lineWidth = lineWidth
restoreLineWidthConfig()
}
export function setupCanvas(ctx: CanvasRenderingContext2D) {
setupCanvasColor(ctx)
setupCanvasLineWidth(ctx)
}
\ No newline at end of file
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
.color-block-container {
display: flex;
align-items: center;
}
.color-block {
width: 20px;
height: 20px;
margin: 0 6px;
border-radius: 3px;
cursor: pointer;
}
\ No newline at end of file
.editor-wrapper {
display: flex;
flex-direction: column;
}
#clipboard-image {
display: none;
}
.lds-dual-ring {
display: inline-block;
width: 80px;
height: 80px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border-width: 6px;
border-style: solid;
border-color: red transparent red transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.editor-wrapper {
position: absolute;
z-index: 1000;
justify-content: center;
align-items: center;
background-color: rgba(144, 144, 144, .4);
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.editor-container {
border: 1px solid red;
align-items: stretch;
width: 90%;
height: 90%;
}
.editor-container-full {
width: 100% !important;
height: 100% !important;
}
.not-show {
visibility: hidden !important;
z-index: -100 !important;
opacity: 0 !important;
}
#canvas {
display: block;
width: 100%;
height: 100%;
}
.operation-branch {
background-color: #000;
height: 40px;
padding: 0 11px;
color: #fff;
box-sizing: border-box;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
}
#canvasWrapper {
position: relative;
width: 100%;
}
.cut-image {
display: none;
}
\ No newline at end of file
.input-dimension {
position: absolute;
z-index: 1000;
border: 1px solid red;
outline: none;
background: transparent;
color: red;
resize: none;
line-height: 1.5;
overflow: hidden;
height: auto;
}
.default-cursor {
cursor: text;
}
.move-cursor {
cursor: grabbing;
user-select: none;
}
\ No newline at end of file
@media screen and (max-width: 450px){
i.iconfont {
font-size: 12px;
margin-right: 8px;
}
.sm-hide {
font-size: 12px;
height: 18px;
padding: 0 4px;
}
}
\ No newline at end of file
i.iconfont, .op-button {
margin-right: 14px;
cursor: pointer;
}
@media screen and (max-width: 450px) {
.op-button {
margin-right: 8px;
}
}
\ No newline at end of file
import {ref} from "vue";
export const rollbackStep = ref(0)
export const enableRecover = ref(false)
\ No newline at end of file
import {ref} from "vue";
enum DrawOperationType {
FROM_ACTION,
FROM_RECORD
}
export const drawOperationType = ref(DrawOperationType.FROM_ACTION)
export const setDrawFromAction = () => drawOperationType.value = DrawOperationType.FROM_ACTION
export const setDrawFromRecord = () => drawOperationType.value = DrawOperationType.FROM_RECORD
export const isDrawFromAction = () => drawOperationType.value === DrawOperationType.FROM_ACTION
\ No newline at end of file
import {computed, ref} from "vue";
import {
COLOR_INITIAL_SELECTION_INDEX,
COLOR_SELECTION_SET,
LINE_WIDTH_CONFIG_SET,
LINE_WIDTH_INITIAL_SELECTION_INDEX
} from "../config/canvasConfig";
// 处理颜色选择
export const showColorPicker = ref(false)
export const colorBlock = ref(COLOR_SELECTION_SET)
export const initialColorIndex = ref(COLOR_INITIAL_SELECTION_INDEX)
export const styledColor = computed(() => colorBlock.value[initialColorIndex.value])
// 处理宽度选择
export const picFont = ref(LINE_WIDTH_INITIAL_SELECTION_INDEX)
export const picFontOptions = ref(LINE_WIDTH_CONFIG_SET)
\ No newline at end of file
import {COLOR_INITIAL_SELECTION_INDEX, LINE_WIDTH_INITIAL_SELECTION_INDEX} from "../config/canvasConfig";
import {initialColorIndex, picFont} from "./operationConfig";
export const setupColorConfig = function (newIndex: number) {
initialColorIndex.value = newIndex
}
export const setupLineWidthConfig = function (newIndex: number) {
picFont.value = newIndex
}
export const restoreLineWidthConfig = function () {
picFont.value = LINE_WIDTH_INITIAL_SELECTION_INDEX
}
export const restoreColorConfig = function () {
initialColorIndex.value = COLOR_INITIAL_SELECTION_INDEX
}
\ No newline at end of file
export const textWidgetAttachment = {realPoint: true, payload: ''}
\ No newline at end of file
import {Ref, ref} from "vue"
import {ExtendTextInputWidgetInputProps, OptionalTypeInputWidgetProps} from "../types/TextInputWidgetSpecific";
export const currentActionWidgetId = ref("") as Ref<string>
export const setCurrentActionWidgetId = (v: string) => {
currentActionWidgetId.value = v
}
export const wholeTextInputWidgets = ref([]) as Ref<Array<ExtendTextInputWidgetInputProps>>
export const addNewWidget = (v: ExtendTextInputWidgetInputProps) => {
wholeTextInputWidgets.value.push(v)
}
export const removeWidget = (v: string) => {
wholeTextInputWidgets.value = wholeTextInputWidgets.value.filter(i => i.id !== v)
}
export const updateCurrentOperatingWidget = (v: OptionalTypeInputWidgetProps) => {
const operatingWidget = wholeTextInputWidgets.value.find(i=>i.id === v.id)
if (typeof operatingWidget !== undefined) {
Object.assign(operatingWidget, v)
}
}
// 当有新组件生成之时,删除所有内容为空的输入组件
export const pruneWidget = () => {
wholeTextInputWidgets.value = wholeTextInputWidgets.value.filter(i => i.text.length > 0)
}
export const clearAll = () => {
wholeTextInputWidgets.value.length = 0
currentActionWidgetId.value = ""
}
\ No newline at end of file
export type BaseDrawAttachment = {
ctx: CanvasRenderingContext2D
}
export type ComplexDrawAttachment = {
o_ctx: CanvasRenderingContext2D
} & BaseDrawAttachment
export type ContinuousDrawAttachment = {
points: Array<any>
} & BaseDrawAttachment
export type ConfigDrawAttachment = {
configType: string
} & BaseDrawAttachment
export type PayloadDrawAttachment = {
payload: any
} & BaseDrawAttachment
\ No newline at end of file
import {ProcessCycleItem, ProcessCycleMapping} from "./HandleProcessCycle";
export type CoreInternalState = {
sx: number | undefined,
sy: number | undefined,
ex: number | undefined,
ey: number | undefined,
isMouseDown: boolean,
currentDrawOption: string,
currentDrawAttachment: any,
currentImmediateOption: any,
startProcess: ProcessCycleMapping,
stampProcess: ProcessCycleMapping,
stopProcess: ProcessCycleMapping
persistedStartPs: Array<ProcessCycleItem>,
persistedStampPs: Array<ProcessCycleItem>,
persistedStopPs: Array<ProcessCycleItem>
immediatePs: Array<ProcessCycleItem>,
isHandleImmediate: boolean
}
\ No newline at end of file
export enum ProcessCycle {
Immediate,
BeforeStart,
Start,
Stamp,
Stop
}
export type ProcessCycleItem = {
func: Function,
attachment?:any,
name?: string,
persist?: boolean
}
export interface ProcessCycleMapping {
[key: string]: ProcessCycleItem
}
\ No newline at end of file
export type ComplexDrawHandleFunc = (
sx: number | undefined,
sy: number | undefined,
ex: number | undefined,
ey: number | undefined,
attachment: any
) => void
export type DefaultNoopFunc = () => void
export type RecordRelatedFunc = (shouldRecord: boolean) => void
export type GenericDrawFunc = DefaultNoopFunc | ComplexDrawHandleFunc | RecordRelatedFunc
export interface OperationRecordTyping {
[name: string]: RecordType
}
export interface OperationRecordAlias {
[name: string]: string
}
export interface VectorRecord {
sx: number,
sy: number,
ex: number,
ey: number,
payload?:any
}
export type ContinuousRecordItem = {
ex: number,
ey: number
}
export type RawRecordItem = {
name: string,
record: any
}
export type PointPayloadItem = ContinuousRecordItem & {
payload: any
}
export type ContinuousRecord = Array<ContinuousRecordItem>
export type IndexRecord = number
export type RecordTypeOperation = number
export type GenericRecordType = VectorRecord | ContinuousRecord | IndexRecord | null | PointPayloadItem | RecordTypeOperation
export enum RecordType {
UNSET,
Vector,
Continuous,
Index,
Immediate,
VectorPayload,
Revoke,
Recover
}
export type OperationRecordInternalState = {
operationRecordTypeSet: OperationRecordTyping
operationRecordAlias: OperationRecordAlias
operationRecord: string,
rawRecord: Array<any>
}
\ No newline at end of file
export type ExtendTextInputWidgetInputProps = {
id: string,
display: boolean
} & TextInputWidgetInputProps
export type TextInputWidgetInputProps = {
sx: number,
sy: number,
text: string,
width: number,
height: number
}
export type OptionalTypeInputWidgetProps = {
id: string,
sx?: number | Number,
sy?: number | Number,
text?: string | String,
width?: number | Number,
height?: number | Number
}
\ No newline at end of file
export function clamp(value: number, min: number, max: number): number {
if (min >= value) return min
if (max <= value) return max
return value
}
// 创建唯一标识
export const createUUID = function () {
return window.crypto.getRandomValues(new Uint8Array(8)).reduce((a, v) => a + v.toString(16).padStart(2, '0'), '')
}
\ No newline at end of file
export const deepClone = (o: any): any => {
if (Array.isArray(o)) {
let cloned: Array<any> = []
o.forEach(item => {
cloned.push(deepClone(item))
})
return cloned
}
if (typeof o === 'object' && o !== null) {
let cloned: any = {}
for (const [k, v] of Object.entries(o)) {
cloned[k] = deepClone(v)
}
return cloned
}
return o
}
export const unifyArray = (o: Array<any>): Array<any> => {
let clonedArray: Array<any> = []
o.forEach(item => {
if (clonedArray.find(pushedItem => JSON.stringify(pushedItem) === JSON.stringify(item)) === undefined) {
clonedArray.push(item)
}
})
return clonedArray
}
\ No newline at end of file
<!--主要思路如下:-->
<!--1.该组件在几何上限制在外部容器内,不能超出-->
<!--2.当输入内容应当跨行之时,需要自动换行,高度自行填充-->
<!--3.为了解决在文字上绘画遮罩的不连续问题,当该元素获得焦点之时,隐藏真实canvas所绘制的文字,但仍旧保持记录;若该元素被删除(记录删除或者手动bs删除),重绘canvas并且去除文字记录-->
<template>
<div
:contenteditable="enableEdit"
ref="inputElement"
v-focus-mounted
:class="inputElementClass"
:style="{
top: currentY + 'px',
left: currentX + 'px',
width: currentInputWidth + 'px',
fontSize: currentFontSize + 'px',
maxHeight: currentInputMaxHeight + 'px',
maxWidth: currentInputMaxWidth + 'px',
minWidth: currentInputMinWidth + 'px'
}"
:id="props.id"
@click.stop=""
@mousedown.stop=""
@mousemove.stop=""
@mouseup.stop=""
@mouseleave.stop=""
@input.stop="userInput"
@dblclick.stop="setEditableWhenNotNull(true)"
@blur="setEditableWhenNotNull(false)"
@keydown.delete.stop="shouldDeleteCurrentComponent"
>{{ initialInputText }}
</div>
</template>
<script lang="ts">
import {vFocus} from "../directives/vFocus";
export default {
directives: {
"focus-mounted": vFocus
}
}
</script>
<script setup lang="ts">
import {computed, onMounted, Ref, ref, watch} from "vue";
import {pruneWidget, updateCurrentOperatingWidget} from "../state/wholeTextInput";
import {deepClone} from "../utils/object";
import {clamp} from "../utils/math";
const props = defineProps({
// 外部容器
parentElement: {
type: HTMLElement,
default: null
},
// 外部可以传入的输入文字
inputText: {
type: String,
default: ''
},
// 如下为几何点数据
sx: {
type: Number,
default: 0
},
sy: {
type: Number,
default: 0
},
id: {
type: String,
required: true,
default: ''
}
})
// 深克隆,防止对象属性修改之时影响到视图层
const extractedPropsClone = deepClone(props)
const initialInputText = ref(extractedPropsClone.inputText) as Ref<string>
const LINE_HEIGHT_RATIO = 1.5
const DEFAULT_WIDTH = 100
const emits = defineEmits(['input-cancel'])
const currentX = ref(props.sx) as Ref<number>
const currentY = ref(props.sy) as Ref<number>
const currentFontSize = ref(14) as Ref<number>
const inputElement = ref() as Ref<HTMLTextAreaElement>
const currentInput = ref(props.inputText) as Ref<String>
const currentInputWidth = ref() as Ref<number>
const currentInputMinWidth = ref(100) as Ref<number>
const enableEdit = ref(true) as Ref<boolean>
// 在这里要求外层容器宽度不变!!!!
const parentStyle = window.getComputedStyle(props.parentElement)
const parentWidth = ref(Number.parseInt(parentStyle.width)) as Ref<number>
const parentHeight = ref(Number.parseInt(parentStyle.height)) as Ref<number>
// 每次动态延展的高度
const expandedHeight = computed(() => LINE_HEIGHT_RATIO * currentFontSize.value)
const inputElementClass = computed(() => {
return enableEdit.value ? 'input-dimension default-cursor' : 'input-dimension move-cursor'
})
const currentInputMaxWidth = computed(() => {
return props.parentElement.clientWidth - currentX.value
})
const currentInputMaxHeight = computed(()=>{
return props.parentElement.clientHeight - currentY.value
})
const rX = ref(0) as Ref<number>
const rY = ref(0) as Ref<number>
// 当存在文字输入之时,需要检测是否存在换行,如果存在,则需要及时改变输入容器的宽度和高度
const dynamicConfigureGEO = () => {
if (currentY.value + inputElement.value.clientHeight >= parentHeight.value) {
if (currentY.value > expandedHeight.value) {
currentY.value -= expandedHeight.value
}
}
}
window.addEventListener('resize', ()=>{
currentX.value = rX.value * props.parentElement.clientWidth
currentY.value = rY.value * props.parentElement.clientHeight
})
const setEditableWhenNotNull = (v:boolean) => {
if (currentInput.value.length > 0) {
enableEdit.value = v
}
}
// 当初次插入DOM节点之时,需要将该元素强行限制在外层容器之内
const setupInsertedGEO = () => {
// 限制宽度
if (props.sx + DEFAULT_WIDTH >= parentWidth.value) {
currentInputWidth.value = parentWidth.value - props.sx
}
// 限制y坐标,保证至少有一行可以输入
if (props.sy + expandedHeight.value > parentHeight.value) {
currentY.value = parentHeight.value - expandedHeight.value
}
currentInputMinWidth.value = Math.min(currentInputMinWidth.value, parentWidth.value - props.sx)
let startX: number = 0
let startY: number = 0
let isActStart: boolean = false
const actionEndFunc = function () {
isActStart = false
}
inputElement.value.addEventListener('mousedown', e => {
e.stopPropagation()
if (enableEdit.value) return
pruneWidget()
if (!isActStart) {
isActStart = true
startX = e.offsetX
startY = e.offsetY
}
})
inputElement.value.addEventListener('mousemove', e => {
e.stopPropagation()
if (isActStart) {
const {offsetX, offsetY} = e
currentX.value = clamp(currentX.value + offsetX - startX, 0, props.parentElement.clientWidth - inputElement.value.clientWidth)
currentY.value = clamp(currentY.value + offsetY - startY, 0, props.parentElement.clientHeight - inputElement.value.clientHeight)
rX.value = currentX.value / props.parentElement.clientWidth
rY.value = currentY.value / props.parentElement.clientHeight
}
})
inputElement.value.addEventListener('mouseup', actionEndFunc)
inputElement.value.addEventListener('mouseleave', actionEndFunc)
rX.value = currentX.value / props.parentElement.clientWidth
rY.value = currentY.value / props.parentElement.clientHeight
}
const userInput = () => {
currentInput.value = inputElement.value.textContent as string
}
watch(currentInput, () => {
if (currentInput.value.length > 0) {
updateCurrentOperatingWidget({
id: props.id,
text: currentInput.value
})
dynamicConfigureGEO()
}
})
const shouldDeleteCurrentComponent = () => {
if (currentInput.value.length === 0) {
emits('input-cancel', props.id)
}
}
onMounted(setupInsertedGEO)
</script>
<style scoped>
@import url("../resources/styles/input-div.css");
</style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment