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 = () => {
|
||||
window.removeEventListener('mousemove', handleChange)
|
||||
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)
|
||||
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
||||
}
|
||||
else if (background.type === 'gradient' && background.gradientColor) {
|
||||
const [color1, color2] = background.gradientColor
|
||||
else if (background.type === 'gradient' && background.gradient) {
|
||||
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 c = formatColor(color)
|
||||
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
||||
|
@ -120,9 +120,14 @@ export default () => {
|
||||
else if (type === 'gradient') {
|
||||
background = {
|
||||
type: 'gradient',
|
||||
gradientType: 'linear',
|
||||
gradientColor: [value.colors[0].color, value.colors[value.colors.length - 1].color],
|
||||
gradientRotate: value.rot,
|
||||
gradient: {
|
||||
type: 'linear',
|
||||
colors: value.colors.map(item => ({
|
||||
...item,
|
||||
pos: parseInt(item.pos),
|
||||
})),
|
||||
rotate: value.rot,
|
||||
},
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -11,9 +11,7 @@ export default (background: Ref<SlideBackground | undefined>) => {
|
||||
color,
|
||||
image,
|
||||
imageSize,
|
||||
gradientColor,
|
||||
gradientRotate,
|
||||
gradientType,
|
||||
gradient,
|
||||
} = background.value
|
||||
|
||||
// 纯色背景
|
||||
@ -38,13 +36,12 @@ export default (background: Ref<SlideBackground | undefined>) => {
|
||||
}
|
||||
|
||||
// 渐变色背景
|
||||
else if (type === 'gradient') {
|
||||
const rotate = gradientRotate || 0
|
||||
const color1 = gradientColor ? gradientColor[0] : '#fff'
|
||||
const color2 = gradientColor ? gradientColor[1] : '#fff'
|
||||
|
||||
if (gradientType === 'radial') return { backgroundImage: `radial-gradient(${color1}, ${color2}` }
|
||||
return { backgroundImage: `linear-gradient(${rotate}deg, ${color1}, ${color2}` }
|
||||
else if (type === 'gradient' && gradient) {
|
||||
const { type, colors, rotate } = gradient
|
||||
const list = colors.map(item => `${item.color} ${item.pos}%`)
|
||||
|
||||
if (type === 'radial') return { backgroundImage: `radial-gradient(${list.join(',')}` }
|
||||
return { backgroundImage: `linear-gradient(${rotate}deg, ${list.join(',')}` }
|
||||
}
|
||||
|
||||
return { backgroundColor: '#fff' }
|
||||
|
@ -30,10 +30,10 @@ export default () => {
|
||||
if (slide.background.type === 'solid' && slide.background.color) {
|
||||
backgroundColorValues.push({ area: 1, value: slide.background.color })
|
||||
}
|
||||
else if (slide.background.type === 'gradient' && slide.background.gradientColor) {
|
||||
backgroundColorValues.push(...slide.background.gradientColor.map(item => ({
|
||||
else if (slide.background.type === 'gradient' && slide.background.gradient) {
|
||||
backgroundColorValues.push(...slide.background.gradient.colors.map(item => ({
|
||||
area: 1,
|
||||
value: item,
|
||||
value: item.color,
|
||||
})))
|
||||
}
|
||||
else backgroundColorValues.push({ area: 1, value: theme.value.backgroundColor })
|
||||
|
@ -34,6 +34,17 @@ export const enum ElementTypes {
|
||||
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: 渐变角度(线性渐变)
|
||||
*/
|
||||
export interface ShapeGradient {
|
||||
type: 'linear' | 'radial'
|
||||
type: GradientType
|
||||
color: [string, string]
|
||||
rotate: number
|
||||
}
|
||||
@ -653,9 +664,7 @@ export interface SlideBackground {
|
||||
color?: string
|
||||
image?: string
|
||||
imageSize?: 'cover' | 'contain' | 'repeat'
|
||||
gradientType?: 'linear' | 'radial'
|
||||
gradientColor?: [string, string]
|
||||
gradientRotate?: number
|
||||
gradient?: Gradient
|
||||
}
|
||||
|
||||
|
||||
|
@ -41,7 +41,7 @@
|
||||
<Select
|
||||
style="flex: 1;"
|
||||
:value="gradient.type"
|
||||
@update:value="value => updateGradient({ type: value as 'linear' | 'radial' })"
|
||||
@update:value="value => updateGradient({ type: value as GradientType })"
|
||||
v-else
|
||||
:options="[
|
||||
{ label: '线性渐变', value: 'linear' },
|
||||
@ -133,7 +133,7 @@
|
||||
import { type Ref, ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
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 useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
import useShapeFormatPainter from '@/hooks/useShapeFormatPainter'
|
||||
|
@ -38,8 +38,8 @@
|
||||
|
||||
<Select
|
||||
style="flex: 1;"
|
||||
:value="background.gradientType || ''"
|
||||
@update:value="value => updateBackground({ gradientType: value as 'linear' | 'radial' })"
|
||||
:value="background.gradient?.type || ''"
|
||||
@update:value="value => updateGradientBackground({ type: value as GradientType })"
|
||||
v-else
|
||||
:options="[
|
||||
{ label: '线性渐变', value: 'linear' },
|
||||
@ -60,37 +60,32 @@
|
||||
|
||||
<div class="background-gradient-wrapper" v-if="background.type === 'gradient'">
|
||||
<div class="row">
|
||||
<div style="width: 40%;">起点颜色:</div>
|
||||
<Popover trigger="click" style="width: 60%;">
|
||||
<template #content>
|
||||
<ColorPicker
|
||||
:modelValue="background.gradientColor![0]"
|
||||
@update:modelValue="value => updateBackground({ gradientColor: [value, background.gradientColor![1]] })"
|
||||
/>
|
||||
</template>
|
||||
<ColorButton :color="background.gradientColor![0]" />
|
||||
</Popover>
|
||||
<GradientBar
|
||||
:value="background.gradient?.colors || []"
|
||||
@update:value="value => updateGradientBackground({ colors: value })"
|
||||
@update:index="index => currentGradientIndex = index"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="width: 40%;">终点颜色:</div>
|
||||
<div style="width: 40%;">当前色块:</div>
|
||||
<Popover trigger="click" style="width: 60%;">
|
||||
<template #content>
|
||||
<ColorPicker
|
||||
:modelValue="background.gradientColor![1]"
|
||||
@update:modelValue="value => updateBackground({ gradientColor: [background.gradientColor![0], value] })"
|
||||
:modelValue="background.gradient!.colors[currentGradientIndex].color"
|
||||
@update:modelValue="value => updateGradientBackgroundColors(value)"
|
||||
/>
|
||||
</template>
|
||||
<ColorButton :color="background.gradientColor![1]" />
|
||||
<ColorButton :color="background.gradient!.colors[currentGradientIndex].color" />
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="row" v-if="background.gradientType === 'linear'">
|
||||
<div class="row" v-if="background.gradient?.type === 'linear'">
|
||||
<div style="width: 40%;">渐变角度:</div>
|
||||
<Slider
|
||||
:min="0"
|
||||
:max="360"
|
||||
:step="15"
|
||||
:value="background.gradientRotate || 0"
|
||||
@update:value="value => updateBackground({ gradientRotate: value as number })"
|
||||
:value="background.gradient.rotate || 0"
|
||||
@update:value="value => updateGradientBackground({ rotate: value as number })"
|
||||
style="width: 60%;"
|
||||
/>
|
||||
</div>
|
||||
@ -306,7 +301,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
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 { WEB_FONTS } from '@/configs/font'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
@ -324,6 +319,7 @@ import Select from '@/components/Select.vue'
|
||||
import Popover from '@/components/Popover.vue'
|
||||
import NumberInput from '@/components/NumberInput.vue'
|
||||
import Modal from '@/components/Modal.vue'
|
||||
import GradientBar from '@/components/GradientBar.vue'
|
||||
|
||||
const slidesStore = useSlidesStore()
|
||||
const { availableFonts } = storeToRefs(useMainStore())
|
||||
@ -331,6 +327,7 @@ const { slides, currentSlide, viewportRatio, theme } = storeToRefs(slidesStore)
|
||||
|
||||
const moreThemeConfigsVisible = ref(false)
|
||||
const themeStylesExtractVisible = ref(false)
|
||||
const currentGradientIndex = ref(0)
|
||||
|
||||
const background = computed(() => {
|
||||
if (!currentSlide.value.background) {
|
||||
@ -372,21 +369,38 @@ const updateBackgroundType = (type: 'solid' | 'image' | 'gradient') => {
|
||||
const newBackground: SlideBackground = {
|
||||
...background.value,
|
||||
type: 'gradient',
|
||||
gradientType: background.value.gradientType || 'linear',
|
||||
gradientColor: background.value.gradientColor || ['#fff', '#fff'],
|
||||
gradientRotate: background.value.gradientRotate || 0,
|
||||
gradient: background.value.gradient || {
|
||||
type: 'linear',
|
||||
colors: [
|
||||
{ pos: 0, color: '#fff' },
|
||||
{ pos: 100, color: '#fff' },
|
||||
],
|
||||
rotate: 0,
|
||||
},
|
||||
}
|
||||
slidesStore.updateSlide({ background: newBackground })
|
||||
}
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
// 设置背景图片
|
||||
// 设置背景
|
||||
const updateBackground = (props: Partial<SlideBackground>) => {
|
||||
slidesStore.updateSlide({ background: { ...background.value, ...props } })
|
||||
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 imageFile = files[0]
|
||||
|
@ -19,9 +19,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { GradientType } from '@/types/slides'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
id: string
|
||||
type: 'linear' | 'radial'
|
||||
type: GradientType
|
||||
color1: string
|
||||
color2: string
|
||||
rotate?: number
|
||||
|
Loading…
x
Reference in New Issue
Block a user