mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 形状支持图片填充(#291)
This commit is contained in:
parent
ebf9b2b15f
commit
dedaf8b88b
@ -107,7 +107,7 @@ Browser access: http://127.0.0.1:5173/
|
||||
- Draw any polygon
|
||||
- Draw any line (unclosed shape simulation)
|
||||
- Replace shape
|
||||
- Fill color
|
||||
- Fill (solid color, gradient, image)
|
||||
- Border
|
||||
- Shadow
|
||||
- Transparency
|
||||
|
@ -102,7 +102,7 @@ npm run dev
|
||||
- 绘制任意多边形
|
||||
- 绘制任意线条(未封闭形状模拟)
|
||||
- 替换形状
|
||||
- 填充色
|
||||
- 填充(纯色、渐变、图片)
|
||||
- 边框
|
||||
- 阴影
|
||||
- 透明度
|
||||
|
@ -317,6 +317,8 @@ export default () => {
|
||||
rotate: el.fill.value.rot,
|
||||
} : undefined
|
||||
|
||||
const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
|
||||
|
||||
const fill = el.fill?.type === 'color' ? el.fill.value : ''
|
||||
|
||||
const element: PPTShapeElement = {
|
||||
@ -330,6 +332,7 @@ export default () => {
|
||||
path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
|
||||
fill,
|
||||
gradient,
|
||||
pattern,
|
||||
fixedRatio: false,
|
||||
rotate: el.rotate,
|
||||
outline: {
|
||||
|
@ -327,6 +327,8 @@ export interface ShapeText {
|
||||
*
|
||||
* gradient?: 渐变,该属性存在时将优先作为填充
|
||||
*
|
||||
* pattern?: 图案,该属性存在时将优先作为填充
|
||||
*
|
||||
* outline?: 边框
|
||||
*
|
||||
* opacity?: 不透明度
|
||||
@ -354,6 +356,7 @@ export interface PPTShapeElement extends PPTBaseElement {
|
||||
fixedRatio: boolean
|
||||
fill: string
|
||||
gradient?: Gradient
|
||||
pattern?: string
|
||||
outline?: PPTElementOutline
|
||||
opacity?: number
|
||||
flipH?: boolean
|
||||
|
@ -22,13 +22,14 @@
|
||||
<Select
|
||||
style="flex: 1;"
|
||||
:value="fillType"
|
||||
@update:value="value => updateFillType(value as 'fill' | 'gradient')"
|
||||
@update:value="value => updateFillType(value as 'fill' | 'gradient' | 'pattern')"
|
||||
:options="[
|
||||
{ label: '纯色填充', value: 'fill' },
|
||||
{ label: '渐变填充', value: 'gradient' },
|
||||
{ label: '图片填充', value: 'pattern' },
|
||||
]"
|
||||
/>
|
||||
<div style="width: 10px;"></div>
|
||||
<div style="width: 10px;" v-if="fillType !== 'pattern'"></div>
|
||||
<Popover trigger="click" v-if="fillType === 'fill'" style="flex: 1;">
|
||||
<template #content>
|
||||
<ColorPicker
|
||||
@ -42,7 +43,7 @@
|
||||
style="flex: 1;"
|
||||
:value="gradient.type"
|
||||
@update:value="value => updateGradient({ type: value as GradientType })"
|
||||
v-else
|
||||
v-else-if="fillType === 'gradient'"
|
||||
:options="[
|
||||
{ label: '线性渐变', value: 'linear' },
|
||||
{ label: '径向渐变', value: 'radial' },
|
||||
@ -83,6 +84,18 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="fillType === 'pattern'">
|
||||
<div class="pattern-image-wrapper">
|
||||
<FileInput @change="files => uploadPattern(files)">
|
||||
<div class="pattern-image">
|
||||
<div class="content" :style="{ backgroundImage: `url(${pattern})` }">
|
||||
<IconPlus />
|
||||
</div>
|
||||
</div>
|
||||
</FileInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElementFlip />
|
||||
|
||||
@ -131,6 +144,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { useMainStore, useSlidesStore } from '@/store'
|
||||
import type { GradientType, PPTShapeElement, Gradient, ShapeText } from '@/types/slides'
|
||||
import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
|
||||
import { getImageDataURL } from '@/utils/image'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
import useShapeFormatPainter from '@/hooks/useShapeFormatPainter'
|
||||
|
||||
@ -150,6 +164,7 @@ import RadioGroup from '@/components/RadioGroup.vue'
|
||||
import Select from '@/components/Select.vue'
|
||||
import Popover from '@/components/Popover.vue'
|
||||
import GradientBar from '@/components/GradientBar.vue'
|
||||
import FileInput from '@/components/FileInput.vue'
|
||||
|
||||
const mainStore = useMainStore()
|
||||
const slidesStore = useSlidesStore()
|
||||
@ -158,6 +173,7 @@ const { handleElement, handleElementId, shapeFormatPainter } = storeToRefs(mainS
|
||||
const handleShapeElement = handleElement as Ref<PPTShapeElement>
|
||||
|
||||
const fill = ref<string>('#000')
|
||||
const pattern = ref<string>('')
|
||||
const gradient = ref<Gradient>({
|
||||
type: 'linear',
|
||||
rotate: 0,
|
||||
@ -179,7 +195,8 @@ watch(handleElement, () => {
|
||||
{ pos: 100, color: '#fff' },
|
||||
]
|
||||
gradient.value = handleElement.value.gradient || { type: 'linear', rotate: 0, colors: defaultGradientColor }
|
||||
fillType.value = handleElement.value.gradient ? 'gradient' : 'fill'
|
||||
pattern.value = handleElement.value.pattern || ''
|
||||
fillType.value = (handleElement.value.pattern !== undefined) ? 'pattern' : (handleElement.value.gradient ? 'gradient' : 'fill')
|
||||
textAlign.value = handleElement.value?.text?.align || 'middle'
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
@ -196,15 +213,20 @@ const updateElement = (props: Partial<PPTShapeElement>) => {
|
||||
}
|
||||
|
||||
// 设置填充类型:渐变、纯色
|
||||
const updateFillType = (type: 'gradient' | 'fill') => {
|
||||
const updateFillType = (type: 'gradient' | 'fill' | 'pattern') => {
|
||||
if (type === 'fill') {
|
||||
slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
|
||||
slidesStore.removeElementProps({ id: handleElementId.value, propName: ['gradient', 'pattern'] })
|
||||
addHistorySnapshot()
|
||||
}
|
||||
else {
|
||||
else if (type === 'gradient') {
|
||||
currentGradientIndex.value = 0
|
||||
slidesStore.removeElementProps({ id: handleElementId.value, propName: 'pattern' })
|
||||
updateElement({ gradient: gradient.value })
|
||||
}
|
||||
else if (type === 'pattern') {
|
||||
slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
|
||||
updateElement({ pattern: '' })
|
||||
}
|
||||
}
|
||||
|
||||
// 设置渐变填充
|
||||
@ -221,6 +243,16 @@ const updateGradientColors = (color: string) => {
|
||||
updateGradient({ colors })
|
||||
}
|
||||
|
||||
// 上传填充图片
|
||||
const uploadPattern = (files: FileList) => {
|
||||
const imageFile = files[0]
|
||||
if (!imageFile) return
|
||||
getImageDataURL(imageFile).then(dataURL => {
|
||||
pattern.value = dataURL
|
||||
updateElement({ pattern: dataURL })
|
||||
})
|
||||
}
|
||||
|
||||
// 设置填充色
|
||||
const updateFill = (value: string) => {
|
||||
updateElement({ fill: value })
|
||||
@ -304,4 +336,33 @@ const updateTextAlign = (align: 'top' | 'middle' | 'bottom') => {
|
||||
padding-bottom: 14%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pattern-image-wrapper {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.pattern-image {
|
||||
height: 0;
|
||||
padding-bottom: 56.25%;
|
||||
border: 1px dashed $borderColor;
|
||||
border-radius: $borderRadius;
|
||||
position: relative;
|
||||
transition: all $transitionDelay;
|
||||
|
||||
&:hover {
|
||||
border-color: $themeColor;
|
||||
color: $themeColor;
|
||||
}
|
||||
|
||||
.content {
|
||||
@include absolute-0();
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -27,8 +27,14 @@
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
>
|
||||
<defs v-if="elementInfo.gradient">
|
||||
<defs>
|
||||
<PatternDefs
|
||||
v-if="elementInfo.pattern"
|
||||
:id="`base-pattern-${elementInfo.id}`"
|
||||
:src="elementInfo.pattern"
|
||||
/>
|
||||
<GradientDefs
|
||||
v-else-if="elementInfo.gradient"
|
||||
:id="`base-gradient-${elementInfo.id}`"
|
||||
:type="elementInfo.gradient.type"
|
||||
:colors="elementInfo.gradient.colors"
|
||||
@ -43,7 +49,7 @@
|
||||
stroke-linecap="butt"
|
||||
stroke-miterlimit="8"
|
||||
:d="elementInfo.path"
|
||||
:fill="elementInfo.gradient ? `url(#base-gradient-${elementInfo.id})` : (elementInfo.fill || 'none')"
|
||||
:fill="fill"
|
||||
:stroke="outlineColor"
|
||||
:stroke-width="outlineWidth"
|
||||
:stroke-dasharray="strokeDashArray"
|
||||
@ -65,13 +71,18 @@ import type { PPTShapeElement, ShapeText } from '@/types/slides'
|
||||
import useElementOutline from '@/views/components/element/hooks/useElementOutline'
|
||||
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||
import useElementFlip from '@/views/components/element/hooks/useElementFlip'
|
||||
import useElementFill from '@/views/components/element/hooks/useElementFill'
|
||||
|
||||
import GradientDefs from './GradientDefs.vue'
|
||||
import PatternDefs from './PatternDefs.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
elementInfo: PPTShapeElement
|
||||
}>()
|
||||
|
||||
const element = computed(() => props.elementInfo)
|
||||
const { fill } = useElementFill(element, 'base')
|
||||
|
||||
const outline = computed(() => props.elementInfo.outline)
|
||||
const { outlineWidth, outlineColor, strokeDashArray } = useElementOutline(outline)
|
||||
|
||||
|
12
src/views/components/element/ShapeElement/PatternDefs.vue
Normal file
12
src/views/components/element/ShapeElement/PatternDefs.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<pattern :id="id" patternContentUnits="objectBoundingBox" patternUnits="objectBoundingBox" width="1" height="1">
|
||||
<image :href="src" width="1" height="1" preserveAspectRatio="xMidYMid slice" />
|
||||
</pattern>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
id: string
|
||||
src: string
|
||||
}>()
|
||||
</script>
|
@ -36,9 +36,15 @@
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
>
|
||||
<defs v-if="elementInfo.gradient">
|
||||
<defs>
|
||||
<PatternDefs
|
||||
v-if="elementInfo.pattern"
|
||||
:id="`editable-pattern-${elementInfo.id}`"
|
||||
:src="elementInfo.pattern"
|
||||
/>
|
||||
<GradientDefs
|
||||
:id="`editabel-gradient-${elementInfo.id}`"
|
||||
v-else-if="elementInfo.gradient"
|
||||
:id="`editable-gradient-${elementInfo.id}`"
|
||||
:type="elementInfo.gradient.type"
|
||||
:colors="elementInfo.gradient.colors"
|
||||
:rotate="elementInfo.gradient.rotate"
|
||||
@ -53,7 +59,7 @@
|
||||
stroke-linecap="butt"
|
||||
stroke-miterlimit="8"
|
||||
:d="elementInfo.path"
|
||||
:fill="elementInfo.gradient ? `url(#editabel-gradient-${elementInfo.id})` : (elementInfo.fill || 'none')"
|
||||
:fill="fill"
|
||||
:stroke="outlineColor"
|
||||
:stroke-width="outlineWidth"
|
||||
:stroke-dasharray="strokeDashArray"
|
||||
@ -89,9 +95,11 @@ import type { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import useElementOutline from '@/views/components/element/hooks/useElementOutline'
|
||||
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||
import useElementFlip from '@/views/components/element/hooks/useElementFlip'
|
||||
import useElementFill from '@/views/components/element/hooks/useElementFill'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import GradientDefs from './GradientDefs.vue'
|
||||
import PatternDefs from './PatternDefs.vue'
|
||||
import ProsemirrorEditor from '@/views/components/element/ProsemirrorEditor.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -126,6 +134,9 @@ const execFormatPainter = () => {
|
||||
if (!keep) mainStore.setShapeFormatPainter(null)
|
||||
}
|
||||
|
||||
const element = computed(() => props.elementInfo)
|
||||
const { fill } = useElementFill(element, 'editable')
|
||||
|
||||
const outline = computed(() => props.elementInfo.outline)
|
||||
const { outlineWidth, outlineColor, strokeDashArray } = useElementOutline(outline)
|
||||
|
||||
@ -187,6 +198,7 @@ const startEdit = () => {
|
||||
.editable-element-shape {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background-size: contain;
|
||||
|
||||
&.lock .element-content {
|
||||
cursor: default;
|
||||
|
15
src/views/components/element/hooks/useElementFill.ts
Normal file
15
src/views/components/element/hooks/useElementFill.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { PPTShapeElement } from '@/types/slides'
|
||||
import { computed, type Ref } from 'vue'
|
||||
|
||||
// 计算元素的填充样式
|
||||
export default (element: Ref<PPTShapeElement>, source: string) => {
|
||||
const fill = computed(() => {
|
||||
if (element.value.pattern) return `url(#${source}-pattern-${element.value.id})`
|
||||
if (element.value.gradient) return `url(#${source}-gradient-${element.value.id})`
|
||||
return element.value.fill || 'none'
|
||||
})
|
||||
|
||||
return {
|
||||
fill,
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user