| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- import { ref } from 'vue'
- import { nanoid } from 'nanoid'
- import type { ImageClipDataRange, PPTElement, PPTImageElement, PPTShapeElement, PPTTextElement, Slide, TextType } from '@/types/slides'
- import type { AIPPTSlide } from '@/types/AIPPT'
- import { useSlidesStore } from '@/store'
- import useAddSlidesOrElements from './useAddSlidesOrElements'
- import useSlideHandler from './useSlideHandler'
- interface ImgPoolItem {
- id: string
- src: string
- width: number
- height: number
- }
- export default () => {
- const slidesStore = useSlidesStore()
- const { addSlidesFromData } = useAddSlidesOrElements()
- const { isEmptySlide } = useSlideHandler()
- const imgPool = ref<ImgPoolItem[]>([])
- const transitionIndex = ref(0)
- const transitionTemplate = ref<Slide | null>(null)
- const checkTextType = (el: PPTElement, type: TextType) => {
- return (el.type === 'text' && el.textType === type) || (el.type === 'shape' && el.text && el.text.type === type)
- }
-
- const getUseableTemplates = (templates: Slide[], n: number, type: TextType) => {
- if (n === 1) {
- const list = templates.filter(slide => {
- const items = slide.elements.filter(el => checkTextType(el, type))
- const titles = slide.elements.filter(el => checkTextType(el, 'title'))
- const texts = slide.elements.filter(el => checkTextType(el, 'content'))
-
- return !items.length && titles.length === 1 && texts.length === 1
- })
-
- if (list.length) return list
- }
-
- let target: Slide | null = null
-
- const list = templates.filter(slide => {
- const len = slide.elements.filter(el => checkTextType(el, type)).length
- return len >= n
- })
- if (list.length === 0) {
- const sorted = templates.sort((a, b) => {
- const aLen = a.elements.filter(el => checkTextType(el, type)).length
- const bLen = b.elements.filter(el => checkTextType(el, type)).length
- return aLen - bLen
- })
- target = sorted[sorted.length - 1]
- }
- else {
- target = list.reduce((closest, current) => {
- const currentLen = current.elements.filter(el => checkTextType(el, type)).length
- const closestLen = closest.elements.filter(el => checkTextType(el, type)).length
- return (currentLen - n) <= (closestLen - n) ? current : closest
- })
- }
-
- return templates.filter(slide => {
- const len = slide.elements.filter(el => checkTextType(el, type)).length
- const targetLen = target!.elements.filter(el => checkTextType(el, type)).length
- return len === targetLen
- })
- }
-
- const getAdaptedFontsize = ({
- text,
- fontSize,
- fontFamily,
- width,
- maxLine,
- }: {
- text: string
- fontSize: number
- fontFamily: string
- width: number
- maxLine: number
- }) => {
- const canvas = document.createElement('canvas')
- const context = canvas.getContext('2d')!
-
- let newFontSize = fontSize
- const minFontSize = 10
-
- while (newFontSize >= minFontSize) {
- context.font = `${newFontSize}px ${fontFamily}`
- const textWidth = context.measureText(text).width
- const line = Math.ceil(textWidth / width)
-
- if (line <= maxLine) return newFontSize
-
- const step = newFontSize <= 22 ? 1 : 2
- newFontSize = newFontSize - step
- }
-
- return minFontSize
- }
-
- const getFontInfo = (htmlString: string) => {
- const fontSizeRegex = /font-size:\s*(\d+(?:\.\d+)?)\s*px/i
- const fontFamilyRegex = /font-family:\s*['"]?([^'";]+)['"]?\s*(?=;|>|$)/i
-
- const defaultInfo = {
- fontSize: 16,
- fontFamily: 'Microsoft Yahei',
- }
-
- const fontSizeMatch = htmlString.match(fontSizeRegex)
- const fontFamilyMatch = htmlString.match(fontFamilyRegex)
-
- return {
- fontSize: fontSizeMatch ? (+fontSizeMatch[1].trim()) : defaultInfo.fontSize,
- fontFamily: fontFamilyMatch ? fontFamilyMatch[1].trim() : defaultInfo.fontFamily,
- }
- }
-
- const getNewTextElement = ({
- el,
- text,
- maxLine,
- longestText,
- digitPadding,
- }: {
- el: PPTTextElement | PPTShapeElement
- text: string
- maxLine: number
- longestText?: string
- digitPadding?: boolean
- }): PPTTextElement | PPTShapeElement => {
- const padding = 10
- const width = el.width - padding * 2 - 2
-
- let content = el.type === 'text' ? el.content : el.text!.content
-
- const fontInfo = getFontInfo(content)
- const size = getAdaptedFontsize({
- text: longestText || text,
- fontSize: fontInfo.fontSize,
- fontFamily: fontInfo.fontFamily,
- width,
- maxLine,
- })
-
- const parser = new DOMParser()
- const doc = parser.parseFromString(content, 'text/html')
-
- const treeWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
-
- const firstTextNode = treeWalker.nextNode()
- if (firstTextNode) {
- if (digitPadding && firstTextNode.textContent && firstTextNode.textContent.length === 2 && text.length === 1) {
- firstTextNode.textContent = '0' + text
- }
- else firstTextNode.textContent = text
- }
-
- if (doc.body.innerHTML.indexOf('font-size') === -1) {
- const p = doc.querySelector('p')
- if (p) p.style.fontSize = '16px'
- }
-
- content = doc.body.innerHTML.replace(/font-size:(.+?)px/g, `font-size: ${size}px`)
-
- return el.type === 'text' ? { ...el, content, lineHeight: size < 15 ? 1.2 : el.lineHeight } : { ...el, text: { ...el.text!, content } }
- }
- const getUseableImage = (el: PPTImageElement): ImgPoolItem | null => {
- let img: ImgPoolItem | null = null
-
- let imgs = []
-
- if (el.width === el.height) imgs = imgPool.value.filter(img => img.width === img.height)
- else if (el.width > el.height) imgs = imgPool.value.filter(img => img.width > img.height)
- else imgs = imgPool.value.filter(img => img.width <= img.height)
- if (!imgs.length) imgs = imgPool.value
-
- img = imgs[Math.floor(Math.random() * imgs.length)]
- imgPool.value = imgPool.value.filter(item => item.id !== img!.id)
-
- return img
- }
-
- const getNewImgElement = (el: PPTImageElement): PPTImageElement => {
- const img = getUseableImage(el)
- if (!img) return el
-
- let scale = 1
- let w = el.width
- let h = el.height
- let range: ImageClipDataRange = [[0, 0], [0, 0]]
- const radio = el.width / el.height
- if (img.width / img.height >= radio) {
- scale = img.height / el.height
- w = img.width / scale
- const diff = (w - el.width) / 2 / w * 100
- range = [[diff, 0], [100 - diff, 100]]
- }
- else {
- scale = img.width / el.width
- h = img.height / scale
- const diff = (h - el.height) / 2 / h * 100
- range = [[0, diff], [100, 100 - diff]]
- }
- const clipShape = (el.clip && el.clip.shape) ? el.clip.shape : 'rect'
- const clip = { range, shape: clipShape }
- const src = img.src
-
- return { ...el, src, clip }
- }
-
- const getMdContent = (content: string) => {
- const regex = /```markdown([^```]*)```/
- const match = content.match(regex)
- if (match) return match[1].trim()
- return content.replace('```markdown', '').replace('```', '')
- }
-
- const getJSONContent = (content: string) => {
- const regex = /```json([^```]*)```/
- const match = content.match(regex)
- if (match) return match[1].trim()
- return content.replace('```json', '').replace('```', '')
- }
- const presetImgPool = (imgs: ImgPoolItem[]) => {
- imgPool.value = imgs
- }
- const AIPPT = (templateSlides: Slide[], _AISlides: AIPPTSlide[], imgs?: ImgPoolItem[]) => {
- slidesStore.updateSlideIndex(slidesStore.slides.length - 1)
- if (imgs) imgPool.value = imgs
- const AISlides: AIPPTSlide[] = []
- for (const template of _AISlides) {
- if (template.type === 'content') {
- const items = template.data.items
- if (items.length === 5 || items.length === 6) {
- const items1 = items.slice(0, 3)
- const items2 = items.slice(3)
- AISlides.push({ ...template, data: { ...template.data, items: items1 } })
- AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 3 })
- }
- else if (items.length === 7 || items.length === 8) {
- const items1 = items.slice(0, 4)
- const items2 = items.slice(4)
- AISlides.push({ ...template, data: { ...template.data, items: items1 } })
- AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 4 })
- }
- else if (items.length === 9 || items.length === 10) {
- const items1 = items.slice(0, 3)
- const items2 = items.slice(3, 6)
- const items3 = items.slice(6)
- AISlides.push({ ...template, data: { ...template.data, items: items1 } })
- AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 3 })
- AISlides.push({ ...template, data: { ...template.data, items: items3 }, offset: 6 })
- }
- else if (items.length > 10) {
- const items1 = items.slice(0, 4)
- const items2 = items.slice(4, 8)
- const items3 = items.slice(8)
- AISlides.push({ ...template, data: { ...template.data, items: items1 } })
- AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 4 })
- AISlides.push({ ...template, data: { ...template.data, items: items3 }, offset: 8 })
- }
- else {
- AISlides.push(template)
- }
- }
- else if (template.type === 'contents') {
- const items = template.data.items
- if (items.length === 11) {
- const items1 = items.slice(0, 6)
- const items2 = items.slice(6)
- AISlides.push({ ...template, data: { ...template.data, items: items1 } })
- AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 6 })
- }
- else if (items.length > 11) {
- const items1 = items.slice(0, 10)
- const items2 = items.slice(10)
- AISlides.push({ ...template, data: { ...template.data, items: items1 } })
- AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 10 })
- }
- else {
- AISlides.push(template)
- }
- }
- else AISlides.push(template)
- }
- const coverTemplates = templateSlides.filter(slide => slide.type === 'cover')
- const contentsTemplates = templateSlides.filter(slide => slide.type === 'contents')
- const transitionTemplates = templateSlides.filter(slide => slide.type === 'transition')
- const contentTemplates = templateSlides.filter(slide => slide.type === 'content')
- const endTemplates = templateSlides.filter(slide => slide.type === 'end')
- if (!transitionTemplate.value) {
- const _transitionTemplate = transitionTemplates[Math.floor(Math.random() * transitionTemplates.length)]
- transitionTemplate.value = _transitionTemplate
- }
- const slides = []
-
- for (const item of AISlides) {
- if (item.type === 'cover') {
- const coverTemplate = coverTemplates[Math.floor(Math.random() * coverTemplates.length)]
- const elements = coverTemplate.elements.map(el => {
- if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
- if (el.type !== 'text' && el.type !== 'shape') return el
- if (checkTextType(el, 'title') && item.data.title) {
- return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
- }
- if (checkTextType(el, 'content') && item.data.text) {
- return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
- }
- return el
- })
- slides.push({
- ...coverTemplate,
- id: nanoid(10),
- elements,
- })
- }
- else if (item.type === 'contents') {
- const _contentsTemplates = getUseableTemplates(contentsTemplates, item.data.items.length, 'item')
- const contentsTemplate = _contentsTemplates[Math.floor(Math.random() * _contentsTemplates.length)]
- const sortedNumberItems = contentsTemplate.elements.filter(el => checkTextType(el, 'itemNumber'))
- const sortedNumberItemIds = sortedNumberItems.sort((a, b) => {
- if (sortedNumberItems.length > 6) {
- let aContent = ''
- let bContent = ''
- if (a.type === 'text') aContent = a.content
- if (a.type === 'shape') aContent = a.text!.content
- if (b.type === 'text') bContent = b.content
- if (b.type === 'shape') bContent = b.text!.content
- if (aContent && bContent) {
- const aIndex = parseInt(aContent)
- const bIndex = parseInt(bContent)
- return aIndex - bIndex
- }
- }
- const aIndex = a.left + a.top * 2
- const bIndex = b.left + b.top * 2
- return aIndex - bIndex
- }).map(el => el.id)
- const sortedItems = contentsTemplate.elements.filter(el => checkTextType(el, 'item'))
- const sortedItemIds = sortedItems.sort((a, b) => {
- if (sortedItems.length > 6) {
- const aItemNumber = sortedNumberItems.find(item => item.groupId === a.groupId)
- const bItemNumber = sortedNumberItems.find(item => item.groupId === b.groupId)
- if (aItemNumber && bItemNumber) {
- let aContent = ''
- let bContent = ''
- if (aItemNumber.type === 'text') aContent = aItemNumber.content
- if (aItemNumber.type === 'shape') aContent = aItemNumber.text!.content
- if (bItemNumber.type === 'text') bContent = bItemNumber.content
- if (bItemNumber.type === 'shape') bContent = bItemNumber.text!.content
-
- if (aContent && bContent) {
- const aIndex = parseInt(aContent)
- const bIndex = parseInt(bContent)
-
- return aIndex - bIndex
- }
- }
- }
- const aIndex = a.left + a.top * 2
- const bIndex = b.left + b.top * 2
- return aIndex - bIndex
- }).map(el => el.id)
- const longestText = item.data.items.reduce((longest, current) => current.length > longest.length ? current : longest, '')
- const unusedElIds: string[] = []
- const unusedGroupIds: string[] = []
- const elements = contentsTemplate.elements.map(el => {
- if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
- if (el.type !== 'text' && el.type !== 'shape') return el
- if (checkTextType(el, 'item')) {
- const index = sortedItemIds.findIndex(id => id === el.id)
- const itemTitle = item.data.items[index]
- if (itemTitle) return getNewTextElement({ el, text: itemTitle, maxLine: 1, longestText })
- unusedElIds.push(el.id)
- if (el.groupId) unusedGroupIds.push(el.groupId)
- }
- if (checkTextType(el, 'itemNumber')) {
- const index = sortedNumberItemIds.findIndex(id => id === el.id)
- const offset = item.offset || 0
- return getNewTextElement({ el, text: index + offset + 1 + '', maxLine: 1, digitPadding: true })
- }
- return el
- }).filter(el => !unusedElIds.includes(el.id) && !(el.groupId && unusedGroupIds.includes(el.groupId)))
- slides.push({
- ...contentsTemplate,
- id: nanoid(10),
- elements,
- })
- }
- else if (item.type === 'transition') {
- transitionIndex.value = transitionIndex.value + 1
- const elements = transitionTemplate.value.elements.map(el => {
- if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
- if (el.type !== 'text' && el.type !== 'shape') return el
- if (checkTextType(el, 'title') && item.data.title) {
- return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
- }
- if (checkTextType(el, 'content') && item.data.text) {
- return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
- }
- if (checkTextType(el, 'partNumber')) {
- return getNewTextElement({ el, text: transitionIndex.value + '', maxLine: 1, digitPadding: true })
- }
- return el
- })
- slides.push({
- ...transitionTemplate.value,
- id: nanoid(10),
- elements,
- })
- }
- else if (item.type === 'content') {
- const _contentTemplates = getUseableTemplates(contentTemplates, item.data.items.length, 'item')
- const contentTemplate = _contentTemplates[Math.floor(Math.random() * _contentTemplates.length)]
- const sortedTitleItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'itemTitle')).sort((a, b) => {
- const aIndex = a.left + a.top * 2
- const bIndex = b.left + b.top * 2
- return aIndex - bIndex
- }).map(el => el.id)
- const sortedTextItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'item')).sort((a, b) => {
- const aIndex = a.left + a.top * 2
- const bIndex = b.left + b.top * 2
- return aIndex - bIndex
- }).map(el => el.id)
- const sortedNumberItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'itemNumber')).sort((a, b) => {
- const aIndex = a.left + a.top * 2
- const bIndex = b.left + b.top * 2
- return aIndex - bIndex
- }).map(el => el.id)
- const itemTitles = []
- const itemTexts = []
- for (const _item of item.data.items) {
- if (_item.title) itemTitles.push(_item.title)
- if (_item.text) itemTexts.push(_item.text)
- }
- const longestTitle = itemTitles.reduce((longest, current) => current.length > longest.length ? current : longest, '')
- const longestText = itemTexts.reduce((longest, current) => current.length > longest.length ? current : longest, '')
- const elements = contentTemplate.elements.map(el => {
- if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
- if (el.type !== 'text' && el.type !== 'shape') return el
- if (item.data.items.length === 1) {
- const contentItem = item.data.items[0]
- if (checkTextType(el, 'content') && contentItem.text) {
- return getNewTextElement({ el, text: contentItem.text, maxLine: 6 })
- }
- }
- else {
- if (checkTextType(el, 'itemTitle')) {
- const index = sortedTitleItemIds.findIndex(id => id === el.id)
- const contentItem = item.data.items[index]
- if (contentItem && contentItem.title) {
- return getNewTextElement({ el, text: contentItem.title, longestText: longestTitle, maxLine: 1 })
- }
- }
- if (checkTextType(el, 'item')) {
- const index = sortedTextItemIds.findIndex(id => id === el.id)
- const contentItem = item.data.items[index]
- if (contentItem && contentItem.text) {
- return getNewTextElement({ el, text: contentItem.text, longestText, maxLine: 4 })
- }
- }
- if (checkTextType(el, 'itemNumber')) {
- const index = sortedNumberItemIds.findIndex(id => id === el.id)
- const offset = item.offset || 0
- return getNewTextElement({ el, text: index + offset + 1 + '', maxLine: 1, digitPadding: true })
- }
- }
- if (checkTextType(el, 'title') && item.data.title) {
- return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
- }
- return el
- })
- slides.push({
- ...contentTemplate,
- id: nanoid(10),
- elements,
- })
- }
- else if (item.type === 'end') {
- const endTemplate = endTemplates[Math.floor(Math.random() * endTemplates.length)]
- const elements = endTemplate.elements.map(el => {
- if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
- return el
- })
- slides.push({
- ...endTemplate,
- id: nanoid(10),
- elements,
- })
- }
- }
- if (isEmptySlide.value) slidesStore.setSlides(slides)
- else addSlidesFromData(slides)
- }
- return {
- presetImgPool,
- AIPPT,
- getMdContent,
- getJSONContent,
- }
- }
|