diff --git a/frontend/src/assets/previews/reel.png b/frontend/src/assets/previews/reel.png new file mode 100644 index 0000000000000000000000000000000000000000..092322e17ecf6ce1d002522421a428380803651f Binary files /dev/null and b/frontend/src/assets/previews/reel.png differ diff --git a/frontend/src/components/canvas/utils.js b/frontend/src/components/canvas/utils.js index 49bc18776db46315476fa7b4752dbd66fee3f659..333a9d63ca85435fe0330d7903e081fe8e40758c 100644 --- a/frontend/src/components/canvas/utils.js +++ b/frontend/src/components/canvas/utils.js @@ -170,14 +170,24 @@ const transformHighlightedText = ( if (characterIsStar) { highlightIsActive = !highlightIsActive; - text = setCharAt(text, positionWithinString, " "); + text = setCharAt(text, positionWithinString, ( + (!options.invertHighlight) ? + // For now, just put an empty Unicode character that still registers + // as one. To do this properly, we would have to adjust the code so that + // it goes back one character. But I don't feel like doing that. + " " : "" + )); } let style = {}; if (highlightIsActive || characterIsStar) { - style.textBackgroundColor = highlightColor; - style.fill = highlightedTextColor; + if (!options.invertHighlight) { + style.textBackgroundColor = highlightColor; + style.fill = highlightedTextColor; + } else { + style.fill = highlightColor; + } } if ( diff --git a/frontend/src/templates.js b/frontend/src/templates.js index 1a7c5d437f3a8b0ee28fdebd00dbafb090e1734e..c59610d49ac648aa1784d408b5778bc8f72af3d9 100644 --- a/frontend/src/templates.js +++ b/frontend/src/templates.js @@ -10,6 +10,7 @@ import posterImage from "./assets/previews/poster.png"; import eventPosterImage from "./assets/previews/event_poster.png"; import regionalSuccessImage from "./assets/previews/regional_success.png"; import socialCoverLargeTextImage from "./assets/previews/social_cover_large_text.png"; +import reelImage from "./assets/previews/reel.png"; import baseEventImage from "./assets/previews/base_event.png"; import rightEventImage from "./assets/previews/right_event.png"; @@ -174,6 +175,16 @@ const TEMPLATES = { }, }, + reel: { + name: "Reel na sociální sítě", + image: reelImage, + path: "/reel", + component: () => import("./views/reel/Reel.vue"), + meta: { + title: "Reel na sociální sítě", + }, + }, + // NOTE: The following 2 templates have been disabled on request. // https://zulip.pirati.cz/#narrow/stream/1545-MoTo/topic/gener.C3.A1tor/near/3390404 // diff --git a/frontend/src/views/reel/Reel.vue b/frontend/src/views/reel/Reel.vue new file mode 100644 index 0000000000000000000000000000000000000000..e63f419900488a4581f72470d5141b499461f95a --- /dev/null +++ b/frontend/src/views/reel/Reel.vue @@ -0,0 +1,163 @@ +<script setup> +import { watch, ref } from "vue"; + +import COLORS from "../../colors"; +import TEMPLATES from "../../templates"; +import DEFAULT_CONTRACTOR from "../../contractors"; +import { + loadFonts, + loadCanvasStorage, + setCanvasStorage, + updateAutoRedrawStorage, +} from "../../utils"; + +import Canvas from "../../components/canvas/Canvas.vue"; +import redraw from "./canvas"; + +import Navbar from "../../components/Navbar.vue"; +import MainContainer from "../../components/MainContainer.vue"; +import ImageInput from "../../components/inputs/ImageInput.vue"; +import LongTextInput from "../../components/inputs/text/LongTextInput.vue"; +import ShortTextInput from "../../components/inputs/text/ShortTextInput.vue"; +import ReloadButton from "../../components/reload/ReloadButton.vue"; +import AutoReloadCheckbox from "../../components/reload/AutoReloadCheckbox.vue"; +</script> + +<script> +await loadFonts(["12px Bebas Neue", "12px Roboto Condensed"]); + +export default { + components: { + Canvas, + Navbar, + MainContainer, + LongTextInput, + ShortTextInput, + AutoReloadCheckbox, + ReloadButton, + }, + data() { + const predefinedColors = { + blackBackground: { + name: "Černý boxík, bílý text", + colors: { + background: COLORS.black, + mainText: COLORS.white, + highlightedText: COLORS.yellow1, + contractedByText: COLORS.black + }, + }, + }; + + return { + mainText: null, + mainImage: null, + contractedBy: DEFAULT_CONTRACTOR, + gradientHeightMultiplier: 1, + colorLabels: { + background: "Pozadí", + }, + predefinedColors: predefinedColors, + colors: predefinedColors.blackBackground.colors, + autoRedraw: false, + }; + }, + async created() { + await loadCanvasStorage(this); + }, + methods: { + async reloadCanvasProperties() { + const canvasProperties = { + mainImage: this.mainImage, + mainText: this.mainText, + contractedBy: this.contractedBy, + colors: this.colors, + }; + + if (canvasProperties.mainText) { + window.fileName = canvasProperties.mainText; + } + + await this.$refs.canvas.redraw(canvasProperties); + + delete canvasProperties.colors; + setCanvasStorage(canvasProperties); + }, + }, + mounted() { + this.$watch( + (vm) => [vm.mainText, vm.contractedBy, vm.colors], + async (value) => { + if (this.autoRedraw) { + await this.reloadCanvasProperties(); + } + }, + { + immediate: true, + deep: true, + }, + ); + + this.$watch( + (vm) => [vm.autoRedraw], + async (value) => { + updateAutoRedrawStorage(this.autoRedraw); + + if (this.autoRedraw) { + await this.reloadCanvasProperties(); + } + }, + ); + }, +}; +</script> + +<template> + <header> + <Navbar :defaultTemplate="TEMPLATES.reel"></Navbar> + </header> + <main> + <MainContainer> + <template v-slot:left> + <Canvas + ref="canvas" + :redrawFunction="redraw" + width="1080" + height="1920" + /> + </template> + + <template v-slot:right> + <ReloadButton :parentRefs="$refs" @click="reloadCanvasProperties" /> + <AutoReloadCheckbox v-model="autoRedraw" /> + + <ImageInput + name="Obrázek" + v-model="mainImage" + :important="true" + zIndex="10" + /> + + <LongTextInput + name="Text" + v-model="mainText" + :important="true" + :highlightable="true" + zIndex="9" + /> + + <ShortTextInput + name="Zadavatel a zpracovatel" + v-model="contractedBy" + :defaultValue="DEFAULT_CONTRACTOR" + :important="false" + zIndex="4" + /> + </template> + </MainContainer> + </main> +</template> + +<style> +@import "vue-select/dist/vue-select.css"; +</style> diff --git a/frontend/src/views/reel/canvas.js b/frontend/src/views/reel/canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..33dfd31bebffe6572ec981ae87a1914fd9ae17d2 --- /dev/null +++ b/frontend/src/views/reel/canvas.js @@ -0,0 +1,137 @@ +import { fabric } from "fabric"; +import { + clearObjects, + sortObjects, + transformHighlightedText, + checkTextBoxHeight, +} from "../../components/canvas/utils"; +import COLORS from "../../colors"; +import { PaddedHighlightingTextbox } from "../../components/canvas/textbox"; + +import bgImageSource from "../../assets/template/social_cover_large_text/background.png"; + +let mainImage = null; +let mainImageSource = null; +let contractedByTextbox = null; +let mainTextBox = null; + +const redraw = async (canvas, options) => { + clearObjects([contractedByTextbox, mainTextBox], canvas); + + const contractedByTextSize = Math.ceil(canvas.height * 0.01); + const contractedByTextMaxWidth = Math.ceil(canvas.width * 0.9); + const contractedByTextBottomMargin = Math.ceil(canvas.width * 0.02); + const contractedByTextSideMargin = Math.ceil(canvas.width * 0.03); + + const mainTextMarginTop = Math.ceil(canvas.height * 0.15); + const mainTextMarginLeft = Math.ceil(canvas.width * 0.05); + const mainTextWidth = Math.ceil(canvas.width * 0.9); + const mainTextSize = Math.ceil(canvas.height * 0.1); + const mainTextMaxLines = 7; + const mainTextLineHeight = 0.85; + + canvas.preserveObjectStacking = true; + + /* BEGIN Main image render */ + + if ( + options.mainImage !== null && + (!canvas.contains(mainImage) || + mainImage === null || + options.mainImage.src !== mainImageSource) + ) { + if (mainImage !== null) { + canvas.remove(mainImage); + } + + mainImage = new fabric.Image(options.mainImage, { + left: 0, + top: 0, + zIndex: 0, + }); + + mainImage.controls = { + ...fabric.Image.prototype.controls, + mtr: new fabric.Control({ visible: false }), + }; + + if (mainImage.width >= mainImage.height) { + mainImage.scaleToHeight(canvas.height); + } else { + mainImage.scaleToWidth(canvas.width); + } + + canvas.add(mainImage); + mainImageSource = options.mainImage.src; + // canvas.centerObject(mainImage) + } else if (mainImage !== null && options.mainImage === null) { + canvas.remove(mainImage); + } + + /* END Main image render */ + + /* BEGIN Main text render */ + + if (options.mainText !== null) { + const highlightedData = transformHighlightedText( + options.mainText, + mainTextSize, + mainTextWidth, + "Bebas Neue", + "#fec900", + "#000000", + { padWhenDiacritics: false, invertHighlight: true }, + ); + + mainTextBox = new PaddedHighlightingTextbox(highlightedData.text, { + width: mainTextWidth, + top: mainTextMarginTop, + left: mainTextMarginLeft, + textAlign: "left", + fontFamily: "Bebas Neue", + fontSize: mainTextSize, + lineHeight: mainTextLineHeight, + fill: options.colors.mainText.value, + styles: highlightedData.styles, + selectable: false, + highlightPadding: canvas.height * 0.003, + zIndex: 10, + }); + + checkTextBoxHeight(mainTextBox, mainTextMaxLines); + + canvas.add(mainTextBox); + } + + /* END Main text render */ + + /* BEGIN Contracted by render */ + + if (options.contractedBy !== null) { + contractedByTextbox = new fabric.Text(options.contractedBy, { + left: canvas.width - contractedByTextSideMargin, + top: canvas.height - contractedByTextBottomMargin - contractedByTextSize, + fontFamily: "Roboto Condensed", + fontSize: contractedByTextSize, + textAlign: "right", + fill: options.colors.contractedByText.value, + selectable: false, + zIndex: 10, + }); + + checkTextBoxHeight(contractedByTextbox, 1); + + canvas.add(contractedByTextbox); + + contractedByTextbox.set({ + left: contractedByTextbox.left - contractedByTextbox.width + }); + canvas.renderAll(); + } + + /* END Contracted by render */ + + sortObjects(canvas); +}; + +export default redraw;