mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 支持复杂渐变背景
This commit is contained in:
parent
fbc3905442
commit
6a54726af2
@ -68,7 +68,6 @@ const handleChange = (e: MouseEvent) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const unbindEventListeners = () => {
|
const unbindEventListeners = () => {
|
||||||
window.removeEventListener('mousemove', handleChange)
|
window.removeEventListener('mousemove', handleChange)
|
||||||
window.removeEventListener('mouseup', unbindEventListeners)
|
window.removeEventListener('mouseup', unbindEventListeners)
|
||||||
|
145
src/components/GradientBar.vue
Normal file
145
src/components/GradientBar.vue
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<div class="gradient-bar">
|
||||||
|
<div class="bar" ref="barRef" :style="{ backgroundImage: gradientStyle }" @click="$event => addPoint($event)"></div>
|
||||||
|
<div class="point"
|
||||||
|
:class="{ 'active': activeIndex === index }"
|
||||||
|
v-for="(item, index) in points"
|
||||||
|
:key="item.pos + '-' + index"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
left: `calc(${item.pos}% - 5px)`,
|
||||||
|
}"
|
||||||
|
@mousedown.left="movePoint(index)"
|
||||||
|
@click.right="removePoint(index)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { GradientColor } from '@/types/slides'
|
||||||
|
import { ref, computed, watchEffect, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: GradientColor[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:value', payload: GradientColor[]): void
|
||||||
|
(event: 'update:index', payload: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const points = ref<GradientColor[]>([])
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
points.value = props.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const barRef = ref<HTMLElement>()
|
||||||
|
const activeIndex = ref(0)
|
||||||
|
|
||||||
|
watch(activeIndex, () => {
|
||||||
|
emit('update:index', activeIndex.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const gradientStyle = computed(() => {
|
||||||
|
const list = points.value.map(item => `${item.color} ${item.pos}%`)
|
||||||
|
return `linear-gradient(to right, ${list.join(',')})`
|
||||||
|
})
|
||||||
|
|
||||||
|
const removePoint = (index: number) => {
|
||||||
|
if (props.value.length <= 2) return
|
||||||
|
|
||||||
|
if (index === activeIndex.value) {
|
||||||
|
activeIndex.value = (index - 1 < 0) ? 0 : index - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = props.value.filter((item, _index) => _index !== index)
|
||||||
|
emit('update:value', values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const movePoint = (index: number) => {
|
||||||
|
let isMouseDown = true
|
||||||
|
|
||||||
|
document.onmousemove = e => {
|
||||||
|
if (!isMouseDown) return
|
||||||
|
if (!barRef.value) return
|
||||||
|
|
||||||
|
let pos = Math.round((e.clientX - barRef.value.getBoundingClientRect().left) / barRef.value.clientWidth * 100)
|
||||||
|
if (pos > 100) pos = 100
|
||||||
|
if (pos < 0) pos = 0
|
||||||
|
|
||||||
|
points.value = points.value.map((item, _index) => {
|
||||||
|
if (_index === index) return { ...item, pos }
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
document.onmouseup = () => {
|
||||||
|
isMouseDown = false
|
||||||
|
|
||||||
|
const point = points.value[index]
|
||||||
|
const _points = [...points.value]
|
||||||
|
_points.splice(index, 1)
|
||||||
|
|
||||||
|
let targetIndex = 0
|
||||||
|
for (let i = 0; i < _points.length; i++) {
|
||||||
|
if (point.pos > _points[i].pos) targetIndex = i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
activeIndex.value = targetIndex
|
||||||
|
_points.splice(targetIndex, 0, point)
|
||||||
|
|
||||||
|
emit('update:value', _points)
|
||||||
|
|
||||||
|
document.onmousemove = null
|
||||||
|
document.onmouseup = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPoint = (e: MouseEvent) => {
|
||||||
|
if (props.value.length >= 6) return
|
||||||
|
if (!barRef.value) return
|
||||||
|
const pos = Math.round((e.clientX - barRef.value.getBoundingClientRect().left) / barRef.value.clientWidth * 100)
|
||||||
|
|
||||||
|
let targetIndex = 0
|
||||||
|
for (let i = 0; i < props.value.length; i++) {
|
||||||
|
if (pos > props.value[i].pos) targetIndex = i + 1
|
||||||
|
}
|
||||||
|
const color = props.value[targetIndex - 1] ? props.value[targetIndex - 1].color : props.value[targetIndex].color
|
||||||
|
const values = [...props.value]
|
||||||
|
values.splice(targetIndex, 0, { pos, color })
|
||||||
|
activeIndex.value = targetIndex
|
||||||
|
emit('update:value', values)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.gradient-bar {
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
height: 18px;
|
||||||
|
padding: 1px 0;
|
||||||
|
margin: 3px 0;
|
||||||
|
position: relative;
|
||||||
|
left: 5px;
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
height: 16px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
.point {
|
||||||
|
width: 10px;
|
||||||
|
height: 18px;
|
||||||
|
background-color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
outline: 1px solid #d9d9d9;
|
||||||
|
box-shadow: 0 0 2px 2px #d9d9d9;
|
||||||
|
border-radius: 1px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
outline: 1px solid $themeColor;
|
||||||
|
box-shadow: 0 0 2px 2px $themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -396,8 +396,10 @@ export default () => {
|
|||||||
const c = formatColor(background.color)
|
const c = formatColor(background.color)
|
||||||
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
||||||
}
|
}
|
||||||
else if (background.type === 'gradient' && background.gradientColor) {
|
else if (background.type === 'gradient' && background.gradient) {
|
||||||
const [color1, color2] = background.gradientColor
|
const colors = background.gradient.colors
|
||||||
|
const color1 = colors[0].color
|
||||||
|
const color2 = colors[colors.length - 1].color
|
||||||
const color = tinycolor.mix(color1, color2).toHexString()
|
const color = tinycolor.mix(color1, color2).toHexString()
|
||||||
const c = formatColor(color)
|
const c = formatColor(color)
|
||||||
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
||||||
|
@ -120,9 +120,14 @@ export default () => {
|
|||||||
else if (type === 'gradient') {
|
else if (type === 'gradient') {
|
||||||
background = {
|
background = {
|
||||||
type: 'gradient',
|
type: 'gradient',
|
||||||
gradientType: 'linear',
|
gradient: {
|
||||||
gradientColor: [value.colors[0].color, value.colors[value.colors.length - 1].color],
|
type: 'linear',
|
||||||
gradientRotate: value.rot,
|
colors: value.colors.map(item => ({
|
||||||
|
...item,
|
||||||
|
pos: parseInt(item.pos),
|
||||||
|
})),
|
||||||
|
rotate: value.rot,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -11,9 +11,7 @@ export default (background: Ref<SlideBackground | undefined>) => {
|
|||||||
color,
|
color,
|
||||||
image,
|
image,
|
||||||
imageSize,
|
imageSize,
|
||||||
gradientColor,
|
gradient,
|
||||||
gradientRotate,
|
|
||||||
gradientType,
|
|
||||||
} = background.value
|
} = background.value
|
||||||
|
|
||||||
// 纯色背景
|
// 纯色背景
|
||||||
@ -38,13 +36,12 @@ export default (background: Ref<SlideBackground | undefined>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 渐变色背景
|
// 渐变色背景
|
||||||
else if (type === 'gradient') {
|
else if (type === 'gradient' && gradient) {
|
||||||
const rotate = gradientRotate || 0
|
const { type, colors, rotate } = gradient
|
||||||
const color1 = gradientColor ? gradientColor[0] : '#fff'
|
const list = colors.map(item => `${item.color} ${item.pos}%`)
|
||||||
const color2 = gradientColor ? gradientColor[1] : '#fff'
|
|
||||||
|
if (type === 'radial') return { backgroundImage: `radial-gradient(${list.join(',')}` }
|
||||||
if (gradientType === 'radial') return { backgroundImage: `radial-gradient(${color1}, ${color2}` }
|
return { backgroundImage: `linear-gradient(${rotate}deg, ${list.join(',')}` }
|
||||||
return { backgroundImage: `linear-gradient(${rotate}deg, ${color1}, ${color2}` }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { backgroundColor: '#fff' }
|
return { backgroundColor: '#fff' }
|
||||||
|
@ -30,10 +30,10 @@ export default () => {
|
|||||||
if (slide.background.type === 'solid' && slide.background.color) {
|
if (slide.background.type === 'solid' && slide.background.color) {
|
||||||
backgroundColorValues.push({ area: 1, value: slide.background.color })
|
backgroundColorValues.push({ area: 1, value: slide.background.color })
|
||||||
}
|
}
|
||||||
else if (slide.background.type === 'gradient' && slide.background.gradientColor) {
|
else if (slide.background.type === 'gradient' && slide.background.gradient) {
|
||||||
backgroundColorValues.push(...slide.background.gradientColor.map(item => ({
|
backgroundColorValues.push(...slide.background.gradient.colors.map(item => ({
|
||||||
area: 1,
|
area: 1,
|
||||||
value: item,
|
value: item.color,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
else backgroundColorValues.push({ area: 1, value: theme.value.backgroundColor })
|
else backgroundColorValues.push({ area: 1, value: theme.value.backgroundColor })
|
||||||
|
@ -34,6 +34,17 @@ export const enum ElementTypes {
|
|||||||
AUDIO = 'audio',
|
AUDIO = 'audio',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GradientType = 'linear' | 'radial'
|
||||||
|
export type GradientColor = {
|
||||||
|
pos: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
export interface Gradient {
|
||||||
|
type: GradientType
|
||||||
|
colors: GradientColor[]
|
||||||
|
rotate: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 元素阴影
|
* 元素阴影
|
||||||
*
|
*
|
||||||
@ -261,7 +272,7 @@ export interface PPTImageElement extends PPTBaseElement {
|
|||||||
* rotate: 渐变角度(线性渐变)
|
* rotate: 渐变角度(线性渐变)
|
||||||
*/
|
*/
|
||||||
export interface ShapeGradient {
|
export interface ShapeGradient {
|
||||||
type: 'linear' | 'radial'
|
type: GradientType
|
||||||
color: [string, string]
|
color: [string, string]
|
||||||
rotate: number
|
rotate: number
|
||||||
}
|
}
|
||||||
@ -653,9 +664,7 @@ export interface SlideBackground {
|
|||||||
color?: string
|
color?: string
|
||||||
image?: string
|
image?: string
|
||||||
imageSize?: 'cover' | 'contain' | 'repeat'
|
imageSize?: 'cover' | 'contain' | 'repeat'
|
||||||
gradientType?: 'linear' | 'radial'
|
gradient?: Gradient
|
||||||
gradientColor?: [string, string]
|
|
||||||
gradientRotate?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<Select
|
<Select
|
||||||
style="flex: 1;"
|
style="flex: 1;"
|
||||||
:value="gradient.type"
|
:value="gradient.type"
|
||||||
@update:value="value => updateGradient({ type: value as 'linear' | 'radial' })"
|
@update:value="value => updateGradient({ type: value as GradientType })"
|
||||||
v-else
|
v-else
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '线性渐变', value: 'linear' },
|
{ label: '线性渐变', value: 'linear' },
|
||||||
@ -133,7 +133,7 @@
|
|||||||
import { type Ref, ref, watch } from 'vue'
|
import { type Ref, ref, watch } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useMainStore, useSlidesStore } from '@/store'
|
import { useMainStore, useSlidesStore } from '@/store'
|
||||||
import type { PPTShapeElement, ShapeGradient, ShapeText } from '@/types/slides'
|
import type { GradientType, PPTShapeElement, ShapeGradient, ShapeText } from '@/types/slides'
|
||||||
import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
|
import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
|
||||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
import useShapeFormatPainter from '@/hooks/useShapeFormatPainter'
|
import useShapeFormatPainter from '@/hooks/useShapeFormatPainter'
|
||||||
|
@ -38,8 +38,8 @@
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
style="flex: 1;"
|
style="flex: 1;"
|
||||||
:value="background.gradientType || ''"
|
:value="background.gradient?.type || ''"
|
||||||
@update:value="value => updateBackground({ gradientType: value as 'linear' | 'radial' })"
|
@update:value="value => updateGradientBackground({ type: value as GradientType })"
|
||||||
v-else
|
v-else
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '线性渐变', value: 'linear' },
|
{ label: '线性渐变', value: 'linear' },
|
||||||
@ -60,37 +60,32 @@
|
|||||||
|
|
||||||
<div class="background-gradient-wrapper" v-if="background.type === 'gradient'">
|
<div class="background-gradient-wrapper" v-if="background.type === 'gradient'">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div style="width: 40%;">起点颜色:</div>
|
<GradientBar
|
||||||
<Popover trigger="click" style="width: 60%;">
|
:value="background.gradient?.colors || []"
|
||||||
<template #content>
|
@update:value="value => updateGradientBackground({ colors: value })"
|
||||||
<ColorPicker
|
@update:index="index => currentGradientIndex = index"
|
||||||
:modelValue="background.gradientColor![0]"
|
/>
|
||||||
@update:modelValue="value => updateBackground({ gradientColor: [value, background.gradientColor![1]] })"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<ColorButton :color="background.gradientColor![0]" />
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div style="width: 40%;">终点颜色:</div>
|
<div style="width: 40%;">当前色块:</div>
|
||||||
<Popover trigger="click" style="width: 60%;">
|
<Popover trigger="click" style="width: 60%;">
|
||||||
<template #content>
|
<template #content>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
:modelValue="background.gradientColor![1]"
|
:modelValue="background.gradient!.colors[currentGradientIndex].color"
|
||||||
@update:modelValue="value => updateBackground({ gradientColor: [background.gradientColor![0], value] })"
|
@update:modelValue="value => updateGradientBackgroundColors(value)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<ColorButton :color="background.gradientColor![1]" />
|
<ColorButton :color="background.gradient!.colors[currentGradientIndex].color" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" v-if="background.gradientType === 'linear'">
|
<div class="row" v-if="background.gradient?.type === 'linear'">
|
||||||
<div style="width: 40%;">渐变角度:</div>
|
<div style="width: 40%;">渐变角度:</div>
|
||||||
<Slider
|
<Slider
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="360"
|
:max="360"
|
||||||
:step="15"
|
:step="15"
|
||||||
:value="background.gradientRotate || 0"
|
:value="background.gradient.rotate || 0"
|
||||||
@update:value="value => updateBackground({ gradientRotate: value as number })"
|
@update:value="value => updateGradientBackground({ rotate: value as number })"
|
||||||
style="width: 60%;"
|
style="width: 60%;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -306,7 +301,7 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useMainStore, useSlidesStore } from '@/store'
|
import { useMainStore, useSlidesStore } from '@/store'
|
||||||
import type { SlideBackground, SlideTheme } from '@/types/slides'
|
import type { Gradient, GradientType, SlideBackground, SlideTheme } from '@/types/slides'
|
||||||
import { PRESET_THEMES } from '@/configs/theme'
|
import { PRESET_THEMES } from '@/configs/theme'
|
||||||
import { WEB_FONTS } from '@/configs/font'
|
import { WEB_FONTS } from '@/configs/font'
|
||||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
@ -324,6 +319,7 @@ import Select from '@/components/Select.vue'
|
|||||||
import Popover from '@/components/Popover.vue'
|
import Popover from '@/components/Popover.vue'
|
||||||
import NumberInput from '@/components/NumberInput.vue'
|
import NumberInput from '@/components/NumberInput.vue'
|
||||||
import Modal from '@/components/Modal.vue'
|
import Modal from '@/components/Modal.vue'
|
||||||
|
import GradientBar from '@/components/GradientBar.vue'
|
||||||
|
|
||||||
const slidesStore = useSlidesStore()
|
const slidesStore = useSlidesStore()
|
||||||
const { availableFonts } = storeToRefs(useMainStore())
|
const { availableFonts } = storeToRefs(useMainStore())
|
||||||
@ -331,6 +327,7 @@ const { slides, currentSlide, viewportRatio, theme } = storeToRefs(slidesStore)
|
|||||||
|
|
||||||
const moreThemeConfigsVisible = ref(false)
|
const moreThemeConfigsVisible = ref(false)
|
||||||
const themeStylesExtractVisible = ref(false)
|
const themeStylesExtractVisible = ref(false)
|
||||||
|
const currentGradientIndex = ref(0)
|
||||||
|
|
||||||
const background = computed(() => {
|
const background = computed(() => {
|
||||||
if (!currentSlide.value.background) {
|
if (!currentSlide.value.background) {
|
||||||
@ -372,21 +369,38 @@ const updateBackgroundType = (type: 'solid' | 'image' | 'gradient') => {
|
|||||||
const newBackground: SlideBackground = {
|
const newBackground: SlideBackground = {
|
||||||
...background.value,
|
...background.value,
|
||||||
type: 'gradient',
|
type: 'gradient',
|
||||||
gradientType: background.value.gradientType || 'linear',
|
gradient: background.value.gradient || {
|
||||||
gradientColor: background.value.gradientColor || ['#fff', '#fff'],
|
type: 'linear',
|
||||||
gradientRotate: background.value.gradientRotate || 0,
|
colors: [
|
||||||
|
{ pos: 0, color: '#fff' },
|
||||||
|
{ pos: 100, color: '#fff' },
|
||||||
|
],
|
||||||
|
rotate: 0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
slidesStore.updateSlide({ background: newBackground })
|
slidesStore.updateSlide({ background: newBackground })
|
||||||
}
|
}
|
||||||
addHistorySnapshot()
|
addHistorySnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置背景图片
|
// 设置背景
|
||||||
const updateBackground = (props: Partial<SlideBackground>) => {
|
const updateBackground = (props: Partial<SlideBackground>) => {
|
||||||
slidesStore.updateSlide({ background: { ...background.value, ...props } })
|
slidesStore.updateSlide({ background: { ...background.value, ...props } })
|
||||||
addHistorySnapshot()
|
addHistorySnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置渐变背景
|
||||||
|
const updateGradientBackground = (props: Partial<Gradient>) => {
|
||||||
|
updateBackground({ gradient: { ...background.value.gradient!, ...props } })
|
||||||
|
}
|
||||||
|
const updateGradientBackgroundColors = (color: string) => {
|
||||||
|
const colors = background.value.gradient!.colors.map((item, index) => {
|
||||||
|
if (index === currentGradientIndex.value) return { ...item, color }
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
updateGradientBackground({ colors })
|
||||||
|
}
|
||||||
|
|
||||||
// 上传背景图片
|
// 上传背景图片
|
||||||
const uploadBackgroundImage = (files: FileList) => {
|
const uploadBackgroundImage = (files: FileList) => {
|
||||||
const imageFile = files[0]
|
const imageFile = files[0]
|
||||||
|
@ -19,9 +19,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { GradientType } from '@/types/slides'
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
id: string
|
id: string
|
||||||
type: 'linear' | 'radial'
|
type: GradientType
|
||||||
color1: string
|
color1: string
|
||||||
color2: string
|
color2: string
|
||||||
rotate?: number
|
rotate?: number
|
||||||
|
Loading…
x
Reference in New Issue
Block a user