feat: 支持复杂渐变背景

This commit is contained in:
zxc 2024-08-10 13:16:35 +08:00
parent fbc3905442
commit 6a54726af2
10 changed files with 223 additions and 50 deletions

View File

@ -68,7 +68,6 @@ const handleChange = (e: MouseEvent) => {
})
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)

View 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>

View File

@ -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 }

View File

@ -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 {

View File

@ -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' }

View File

@ -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 })

View File

@ -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
}

View File

@ -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'

View File

@ -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]

View File

@ -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