|
import tinycolor from 'tinycolor2' |
|
import { storeToRefs } from 'pinia' |
|
import { useSlidesStore } from '@/store' |
|
import type { Slide } from '@/types/slides' |
|
import type { PresetTheme } from '@/configs/theme' |
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot' |
|
import { getLineElementLength } from '@/utils/element' |
|
|
|
interface ThemeValueWithArea { |
|
area: number |
|
value: string |
|
} |
|
|
|
export default () => { |
|
const slidesStore = useSlidesStore() |
|
const { slides, theme } = storeToRefs(slidesStore) |
|
|
|
const { addHistorySnapshot } = useHistorySnapshot() |
|
|
|
|
|
const getSlidesThemeStyles = (slide: Slide | Slide[]) => { |
|
const slides = Array.isArray(slide) ? slide : [slide] |
|
|
|
const backgroundColorValues: ThemeValueWithArea[] = [] |
|
const themeColorValues: ThemeValueWithArea[] = [] |
|
const fontColorValues: ThemeValueWithArea[] = [] |
|
const fontNameValues: ThemeValueWithArea[] = [] |
|
|
|
for (const slide of slides) { |
|
if (slide.background) { |
|
if (slide.background.type === 'solid' && slide.background.color) { |
|
backgroundColorValues.push({ area: 1, value: slide.background.color }) |
|
} |
|
else if (slide.background.type === 'gradient' && slide.background.gradient) { |
|
const len = slide.background.gradient.colors.length |
|
backgroundColorValues.push(...slide.background.gradient.colors.map(item => ({ |
|
area: 1 / len, |
|
value: item.color, |
|
}))) |
|
} |
|
else backgroundColorValues.push({ area: 1, value: theme.value.backgroundColor }) |
|
} |
|
for (const el of slide.elements) { |
|
const elWidth = el.width |
|
let elHeight = 0 |
|
if (el.type === 'line') { |
|
const [startX, startY] = el.start |
|
const [endX, endY] = el.end |
|
elHeight = Math.sqrt(Math.pow(Math.abs(startX - endX), 2) + Math.pow(Math.abs(startY - endY), 2)) |
|
} |
|
else elHeight = el.height |
|
|
|
const area = elWidth * elHeight |
|
|
|
if (el.type === 'shape' || el.type === 'text') { |
|
if (el.fill) { |
|
themeColorValues.push({ area, value: el.fill }) |
|
} |
|
if (el.type === 'shape' && el.gradient) { |
|
const len = el.gradient.colors.length |
|
themeColorValues.push(...el.gradient.colors.map(item => ({ |
|
area: 1 / len * area, |
|
value: item.color, |
|
}))) |
|
} |
|
|
|
const text = (el.type === 'shape' ? el.text?.content : el.content) || '' |
|
if (!text) continue |
|
|
|
const plainText = text.replace(/<[^>]+>/g, '').replace(/\s*/g, '') |
|
const matchForColor = text.match(/<[^>]+color: .+?<\/.+?>/g) |
|
const matchForFont = text.match(/<[^>]+font-family: .+?<\/.+?>/g) |
|
|
|
let defaultColorPercent = 1 |
|
let defaultFontPercent = 1 |
|
|
|
if (matchForColor) { |
|
for (const item of matchForColor) { |
|
const ret = item.match(/color: (.+?);/) |
|
if (!ret) continue |
|
const text = item.replace(/<[^>]+>/g, '').replace(/\s*/g, '') |
|
const color = ret[1] |
|
const percentage = text.length / plainText.length |
|
defaultColorPercent = defaultColorPercent - percentage |
|
|
|
fontColorValues.push({ |
|
area: area * percentage, |
|
value: color, |
|
}) |
|
} |
|
} |
|
if (matchForFont) { |
|
for (const item of matchForFont) { |
|
const ret = item.match(/font-family: (.+?);/) |
|
if (!ret) continue |
|
const text = item.replace(/<[^>]+>/g, '').replace(/\s*/g, '') |
|
const font = ret[1] |
|
const percentage = text.length / plainText.length |
|
defaultFontPercent = defaultFontPercent - percentage |
|
|
|
fontNameValues.push({ |
|
area: area * percentage, |
|
value: font, |
|
}) |
|
} |
|
} |
|
|
|
if (defaultColorPercent) { |
|
const _defaultColor = el.type === 'shape' ? el.text?.defaultColor : el.defaultColor |
|
const defaultColor = _defaultColor || theme.value.fontColor |
|
fontColorValues.push({ |
|
area: area * defaultColorPercent, |
|
value: defaultColor, |
|
}) |
|
} |
|
if (defaultFontPercent) { |
|
const _defaultFont = el.type === 'shape' ? el.text?.defaultFontName : el.defaultFontName |
|
const defaultFont = _defaultFont || theme.value.fontName |
|
fontNameValues.push({ |
|
area: area * defaultFontPercent, |
|
value: defaultFont, |
|
}) |
|
} |
|
} |
|
else if (el.type === 'table') { |
|
const cellCount = el.data.length * el.data[0].length |
|
let cellWithFillCount = 0 |
|
for (const row of el.data) { |
|
for (const cell of row) { |
|
if (cell.style?.backcolor) { |
|
cellWithFillCount += 1 |
|
themeColorValues.push({ area: area / cellCount, value: cell.style?.backcolor }) |
|
} |
|
if (cell.text) { |
|
const percent = (cell.text.length >= 10) ? 1 : (cell.text.length / 10) |
|
if (cell.style?.color) { |
|
fontColorValues.push({ area: area / cellCount * percent, value: cell.style?.color }) |
|
} |
|
if (cell.style?.fontname) { |
|
fontColorValues.push({ area: area / cellCount * percent, value: cell.style?.fontname }) |
|
} |
|
} |
|
} |
|
} |
|
if (el.theme) { |
|
const percent = 1 - cellWithFillCount / cellCount |
|
themeColorValues.push({ area: area * percent, value: el.theme.color }) |
|
} |
|
} |
|
else if (el.type === 'chart') { |
|
if (el.fill) { |
|
themeColorValues.push({ area: area * 0.5, value: el.fill }) |
|
} |
|
themeColorValues.push({ area: area * 0.5, value: el.themeColors[0] }) |
|
} |
|
else if (el.type === 'line') { |
|
themeColorValues.push({ area, value: el.color }) |
|
} |
|
else if (el.type === 'audio') { |
|
themeColorValues.push({ area, value: el.color }) |
|
} |
|
else if (el.type === 'latex') { |
|
fontColorValues.push({ area, value: el.color }) |
|
} |
|
} |
|
} |
|
|
|
const backgroundColors: { [key: string]: number } = {} |
|
for (const item of backgroundColorValues) { |
|
const color = tinycolor(item.value).toRgbString() |
|
if (color === 'rgba(0, 0, 0, 0)') continue |
|
if (!backgroundColors[color]) backgroundColors[color] = item.area |
|
else backgroundColors[color] += item.area |
|
} |
|
|
|
const themeColors: { [key: string]: number } = {} |
|
for (const item of themeColorValues) { |
|
const color = tinycolor(item.value).toRgbString() |
|
if (color === 'rgba(0, 0, 0, 0)') continue |
|
if (!themeColors[color]) themeColors[color] = item.area |
|
else themeColors[color] += item.area |
|
} |
|
|
|
const fontColors: { [key: string]: number } = {} |
|
for (const item of fontColorValues) { |
|
const color = tinycolor(item.value).toRgbString() |
|
if (color === 'rgba(0, 0, 0, 0)') continue |
|
if (!fontColors[color]) fontColors[color] = item.area |
|
else fontColors[color] += item.area |
|
} |
|
|
|
const fontNames: { [key: string]: number } = {} |
|
for (const item of fontNameValues) { |
|
if (!fontNames[item.value]) fontNames[item.value] = item.area |
|
else fontNames[item.value] += item.area |
|
} |
|
|
|
return { |
|
backgroundColors: Object.keys(backgroundColors).sort((a, b) => backgroundColors[b] - backgroundColors[a]), |
|
themeColors: Object.keys(themeColors).sort((a, b) => themeColors[b] - themeColors[a]), |
|
fontColors: Object.keys(fontColors).sort((a, b) => fontColors[b] - fontColors[a]), |
|
fontNames: Object.keys(fontNames).sort((a, b) => fontNames[b] - fontNames[a]), |
|
} |
|
} |
|
|
|
|
|
const getSlideAllColors = (slide: Slide) => { |
|
const colorMap: { [key: string]: number } = {} |
|
|
|
const record = (color: string, area: number) => { |
|
const _color = tinycolor(color).setAlpha(1).toRgbString() |
|
if (!colorMap[_color]) colorMap[_color] = area |
|
else colorMap[_color] = colorMap[_color] + area |
|
} |
|
|
|
for (const el of slide.elements) { |
|
const width = el.width |
|
const height = el.type === 'line' ? getLineElementLength(el) : el.height |
|
const area = width * height |
|
|
|
if (el.type === 'shape' && tinycolor(el.fill).getAlpha() !== 0) { |
|
record(el.fill, area) |
|
} |
|
if (el.type === 'text' && el.fill && tinycolor(el.fill).getAlpha() !== 0) { |
|
record(el.fill, area) |
|
} |
|
if (el.type === 'image' && el.colorMask && tinycolor(el.colorMask).getAlpha() !== 0) { |
|
record(el.colorMask, area) |
|
} |
|
if (el.type === 'table' && el.theme && tinycolor(el.theme.color).getAlpha() !== 0) { |
|
record(el.theme.color, area) |
|
} |
|
if (el.type === 'chart' && el.themeColors[0] && tinycolor(el.themeColors[0]).getAlpha() !== 0) { |
|
record(el.themeColors[0], area) |
|
} |
|
if (el.type === 'line' && tinycolor(el.color).getAlpha() !== 0) { |
|
record(el.color, area) |
|
} |
|
if (el.type === 'audio' && tinycolor(el.color).getAlpha() !== 0) { |
|
record(el.color, area) |
|
} |
|
} |
|
const colors = Object.keys(colorMap).sort((a, b) => colorMap[b] - colorMap[a]) |
|
return colors |
|
} |
|
|
|
|
|
const createSlideThemeColorMap = (slide: Slide, _newColors: string[]): { [key: string]: string } => { |
|
const newColors = [..._newColors] |
|
const oldColors = getSlideAllColors(slide) |
|
const themeColorMap: { [key: string]: string } = {} |
|
|
|
if (oldColors.length > newColors.length) { |
|
const analogous = tinycolor(newColors[0]).analogous(oldColors.length - newColors.length + 10) |
|
const otherColors = analogous.map(item => item.toHexString()).slice(1) |
|
newColors.push(...otherColors) |
|
} |
|
for (let i = 0; i < oldColors.length; i++) { |
|
themeColorMap[oldColors[i]] = newColors[i] |
|
} |
|
|
|
return themeColorMap |
|
} |
|
|
|
|
|
const setSlideTheme = (slide: Slide, theme: PresetTheme) => { |
|
const colorMap = createSlideThemeColorMap(slide, theme.colors) |
|
|
|
const getColor = (color: string) => { |
|
const alpha = tinycolor(color).getAlpha() |
|
const _color = colorMap[tinycolor(color).setAlpha(1).toRgbString()] |
|
return _color ? tinycolor(_color).setAlpha(alpha).toRgbString() : color |
|
} |
|
|
|
if (!slide.background || slide.background.type !== 'image') { |
|
slide.background = { |
|
type: 'solid', |
|
color: theme.background, |
|
} |
|
} |
|
for (const el of slide.elements) { |
|
if (el.type === 'shape') { |
|
if (el.fill) el.fill = getColor(el.fill) |
|
if (el.gradient) delete el.gradient |
|
if (el.text) { |
|
el.text.defaultColor = theme.fontColor |
|
el.text.defaultFontName = theme.fontname |
|
if(el.text.content) el.text.content = el.text.content.replace(/color: .+?;/g, '').replace(/font-family: .+?;/g, '') |
|
} |
|
} |
|
if (el.type === 'text') { |
|
if (el.fill) el.fill = getColor(el.fill) |
|
el.defaultColor = theme.fontColor |
|
el.defaultFontName = theme.fontname |
|
if(el.content) el.content = el.content.replace(/color: .+?;/g, '').replace(/font-family: .+?;/g, '') |
|
} |
|
if (el.type === 'image' && el.colorMask) { |
|
el.colorMask = getColor(el.colorMask) |
|
} |
|
if (el.type === 'table') { |
|
if (el.theme) el.theme.color = getColor(el.theme.color) |
|
for (const rowCells of el.data) { |
|
for (const cell of rowCells) { |
|
if (cell.style) { |
|
cell.style.color = theme.fontColor |
|
cell.style.fontname = theme.fontname |
|
} |
|
} |
|
} |
|
} |
|
if (el.type === 'chart') { |
|
el.themeColors = getColor(el.themeColors[0]) ? [getColor(el.themeColors[0])] : el.themeColors |
|
el.textColor = theme.fontColor |
|
} |
|
if (el.type === 'line') el.color = getColor(el.color) |
|
if (el.type === 'audio') el.color = getColor(el.color) |
|
if (el.type === 'latex') el.color = theme.fontColor |
|
|
|
if ('outline' in el && el.outline) { |
|
el.outline.color = theme.borderColor |
|
} |
|
} |
|
} |
|
|
|
|
|
const applyPresetTheme = (theme: PresetTheme, resetSlides = false) => { |
|
slidesStore.setTheme({ |
|
backgroundColor: theme.background, |
|
themeColors: theme.colors, |
|
fontColor: theme.fontColor, |
|
outline: { |
|
width: 2, |
|
style: 'solid', |
|
color: theme.borderColor, |
|
}, |
|
fontName: theme.fontname, |
|
}) |
|
|
|
if (resetSlides) { |
|
const newSlides: Slide[] = JSON.parse(JSON.stringify(slides.value)) |
|
for (const slide of newSlides) { |
|
setSlideTheme(slide, theme) |
|
} |
|
slidesStore.setSlides(newSlides) |
|
addHistorySnapshot() |
|
} |
|
} |
|
|
|
|
|
const applyThemeToAllSlides = (applyAll = false) => { |
|
const newSlides: Slide[] = JSON.parse(JSON.stringify(slides.value)) |
|
const { themeColors, backgroundColor, fontColor, fontName, outline, shadow } = theme.value |
|
|
|
for (const slide of newSlides) { |
|
if (!slide.background || slide.background.type !== 'image') { |
|
slide.background = { |
|
type: 'solid', |
|
color: backgroundColor |
|
} |
|
} |
|
|
|
for (const el of slide.elements) { |
|
if (applyAll) { |
|
if ('outline' in el && el.outline) el.outline = outline |
|
if ('shadow' in el && el.shadow) el.shadow = shadow |
|
} |
|
|
|
if (el.type === 'shape') { |
|
const alpha = tinycolor(el.fill).getAlpha() |
|
if (alpha > 0) el.fill = themeColors[0] |
|
if (el.text) { |
|
el.text.defaultColor = fontColor |
|
el.text.defaultFontName = fontName |
|
if(el.text.content) el.text.content = el.text.content.replace(/color: .+?;/g, '').replace(/font-family: .+?;/g, '') |
|
} |
|
if (el.gradient) delete el.gradient |
|
} |
|
else if (el.type === 'line') el.color = themeColors[0] |
|
else if (el.type === 'text') { |
|
if (el.fill) { |
|
const alpha = tinycolor(el.fill).getAlpha() |
|
if (alpha > 0) el.fill = themeColors[0] |
|
} |
|
el.defaultColor = fontColor |
|
el.defaultFontName = fontName |
|
if(el.content) el.content = el.content.replace(/color: .+?;/g, '').replace(/font-family: .+?;/g, '') |
|
} |
|
else if (el.type === 'table') { |
|
if (el.theme) el.theme.color = themeColors[0] |
|
for (const rowCells of el.data) { |
|
for (const cell of rowCells) { |
|
if (cell.style) { |
|
cell.style.color = fontColor |
|
cell.style.fontname = fontName |
|
} |
|
} |
|
} |
|
} |
|
else if (el.type === 'chart') { |
|
el.themeColors = themeColors |
|
el.textColor = fontColor |
|
} |
|
else if (el.type === 'latex') el.color = fontColor |
|
else if (el.type === 'audio') el.color = themeColors[0] |
|
} |
|
} |
|
slidesStore.setSlides(newSlides) |
|
addHistorySnapshot() |
|
} |
|
|
|
return { |
|
getSlidesThemeStyles, |
|
applyPresetTheme, |
|
applyThemeToAllSlides, |
|
} |
|
} |