diff --git a/frontend/src/assets/previews/back_in_full_force_photo_banner.png b/frontend/src/assets/previews/back_in_full_force_photo_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c821ceb3f76319dbb1e13aa7ff712566d19ae277 Binary files /dev/null and b/frontend/src/assets/previews/back_in_full_force_photo_banner.png differ diff --git a/frontend/src/assets/template/back_in_full_force_banner/background.png b/frontend/src/assets/template/back_in_full_force_banner/background.png new file mode 100644 index 0000000000000000000000000000000000000000..c7f4b973095c5e50a8b2d87a049fd0d80fe99816 Binary files /dev/null and b/frontend/src/assets/template/back_in_full_force_banner/background.png differ diff --git a/frontend/src/assets/template/back_in_full_force_banner/background_inverted.ong b/frontend/src/assets/template/back_in_full_force_banner/background_inverted.ong new file mode 100644 index 0000000000000000000000000000000000000000..eed9968887345f7ad5eb0c0911070f51c3d4d7dd Binary files /dev/null and b/frontend/src/assets/template/back_in_full_force_banner/background_inverted.ong differ diff --git a/frontend/src/assets/template/back_in_full_force_banner/background_inverted.png b/frontend/src/assets/template/back_in_full_force_banner/background_inverted.png new file mode 100644 index 0000000000000000000000000000000000000000..95b688229ab6cfdfb788ea12ea07802e3017e834 Binary files /dev/null and b/frontend/src/assets/template/back_in_full_force_banner/background_inverted.png differ diff --git a/frontend/src/templates.js b/frontend/src/templates.js index ac51345254e90c595aa82868715066f357d59e3a..5dd5de7c8a41a59e64176c8deb5024451828af66 100644 --- a/frontend/src/templates.js +++ b/frontend/src/templates.js @@ -1,6 +1,7 @@ import basicPhotoBannerImage from "./assets/previews/basic_photo_banner.png"; import urgentBasicPhotoBannerImage from "./assets/previews/urgent_basic_photo_banner.png"; import makeawishPhotoBannerImage from "./assets/previews/make_a_wish_photo_banner.png"; +import backInFullForcePhotoBannerImage from "./assets/previews/back_in_full_force_photo_banner.png"; import urgentTextBannerImage from "./assets/previews/urgent_text_banner.png"; import textBannerImage from "./assets/previews/text_banner.png"; import newspaperQuoteBottomImage from "./assets/previews/newspaper_quote_bottom.png"; @@ -55,6 +56,15 @@ const TEMPLATES = { title: "Máte přání banner", }, }, + back_in_full_force_banner: { + name: "Zpátky v plné síle banner", + image: backInFullForcePhotoBannerImage, + path: "/back-in-full-force-banner", + component: () => import("./views/back_in_full_force_banner/BackInFullForceBanner.vue"), + meta: { + title: "Zpátky v plné síle banner", + }, + }, urgent_text_banner: { name: "Urgentní banner pouze s textem", image: urgentTextBannerImage, diff --git a/frontend/src/views/back_in_full_force_banner/BackInFullForceBanner.vue b/frontend/src/views/back_in_full_force_banner/BackInFullForceBanner.vue new file mode 100644 index 0000000000000000000000000000000000000000..af59fee8680b920a80f4ab487ed2dee923e339b5 --- /dev/null +++ b/frontend/src/views/back_in_full_force_banner/BackInFullForceBanner.vue @@ -0,0 +1,226 @@ +<script setup> +import { watch, ref } from "vue"; + +import COLORS from "../../colors"; +import PEOPLE from "../../people"; +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 RangeInput from "../../components/inputs/RangeInput.vue"; +import InputSeparator from "../../components/inputs/InputSeparator.vue"; +import SelectInput from "../../components/inputs/SelectInput.vue"; +import MultipleColorPicker from "../../components/inputs/colors/MultipleColorPicker.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", + "bold 12px Roboto Condensed", +]); + +export default { + components: { + Canvas, + Navbar, + MainContainer, + ImageInput, + LongTextInput, + ShortTextInput, + RangeInput, + SelectInput, + InputSeparator, + MultipleColorPicker, + }, + data() { + const predefinedColors = { + base: { + name: "Černé pozadí", + colors: { + background: COLORS.black, + highlight: COLORS.yellow1, + baseText: COLORS.white, + nameText: COLORS.yellow1, + highlightedText: COLORS.black, + contractedByText: COLORS.gray1, + }, + }, + white: { + name: "Bílé pozadí", + colors: { + background: COLORS.white, + highlight: COLORS.yellow1, + baseText: COLORS.black, + nameText: COLORS.black, + highlightedText: COLORS.black, + contractedByText: COLORS.gray1, + }, + }, + }; + + return { + mainImage: null, + mainText: null, + personName: null, + personPosition: null, + contractedBy: DEFAULT_CONTRACTOR, + colorLabels: { + background: "Pozadí", + highlight: "Zvýraznění", + baseText: "Text", + highlightedText: "Zvýrazněný text", + }, + predefinedColors: predefinedColors, + colors: predefinedColors.base.colors, + autoRedraw: false, + }; + }, + async created() { + await loadCanvasStorage(this); + }, + methods: { + async reloadCanvasProperties() { + const canvasProperties = { + mainImage: this.mainImage, + mainText: this.mainText, + personName: this.personName, + personPosition: this.personPosition, + 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.mainImage, + vm.mainText, + vm.personName, + vm.personPosition, + 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.basic_photo_banner"></Navbar> + </header> + <main> + <MainContainer> + <template v-slot:left> + <Canvas + ref="canvas" + :redrawFunction="redraw" + width="2000" + height="2000" + /> + </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="Hlavní text" + v-model="mainText" + :important="true" + :highlightable="true" + zIndex="9" + /> + <ShortTextInput + name="Jméno osoby" + v-model="personName" + v-model:relatedModel="personPosition" + :predefinedValues="PEOPLE" + :important="true" + zIndex="8" + /> + <LongTextInput + ref="refPersonPosition" + name="Pozice osoby" + v-model="personPosition" + :important="false" + zIndex="7" + /> + + <InputSeparator /> + + <MultipleColorPicker + name="Barvy" + v-model="colors" + :important="false" + :colorLabels="colorLabels" + :predefinedColors="predefinedColors" + :defaultPredefinedColors="predefinedColors.base" + zIndex="5" + ></MultipleColorPicker> + + <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/back_in_full_force_banner/canvas.js b/frontend/src/views/back_in_full_force_banner/canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..c13cbccc5cecf255a4ffad268904fd90fe74eb8d --- /dev/null +++ b/frontend/src/views/back_in_full_force_banner/canvas.js @@ -0,0 +1,265 @@ +import { fabric } from "fabric"; +import { + clearObjects, + sortObjects, + transformHighlightedText, + checkTextBoxHeight, +} from "../../components/canvas/utils"; +import { PaddedHighlightingTextbox } from "../../components/canvas/textbox"; +import backgroundURL from "../../assets/template/back_in_full_force_banner/background.png"; +import backgroundURLInverted from "../../assets/template/back_in_full_force_banner/background_inverted.png"; +import COLORS from "../../colors"; + +let mainTextBox = null; + +let personNameText = null; +let personPositionText = null; + +let mainImage = null; +let backgroundImage = null; +let previousBackgroundImageColor = null; + +let contractedByTextbox = null; + +let mainImageSource = null; + +const redraw = async (canvas, options) => { + clearObjects( + [ + mainTextBox, + personNameText, + personPositionText, + contractedByTextbox, + ], + canvas, + ); + + canvas.preserveObjectStacking = true; + + const textMarginLeft = Math.ceil(canvas.width * 0.24); + const textMarginRight = Math.ceil(canvas.width * 0.21); + + let mainTextMarginBottom = Math.ceil(canvas.height * 0.085); + const mainTextBackgroundMarginTop = Math.ceil(canvas.height * 0.14); + const mainTextSize = Math.ceil(canvas.height * 0.07); + const mainTextHeightLimit = Math.ceil(mainTextSize * 3.3); + const mainTextLineHeight = 1; + + const bottomTextSize = Math.ceil(canvas.height * 0.035); + const positionTextSize = Math.ceil(canvas.height * 0.025); + const nameTextMarginBottom = Math.ceil(canvas.height * 0.132); + const nameTextMarginLeft = Math.ceil(canvas.width * 0.05); + const nameTextExtraBottomMargin = Math.ceil(canvas.height * 0.05); + const nameTextMaxWidth = Math.ceil(canvas.width * 0.16); + const positionTextSideGap = nameTextMarginLeft; + const positionTextMarginBottom = Math.ceil(canvas.height * 0.073); + const positionTextSeparatorWidth = Math.ceil(canvas.width * 0.0035); + const positionTextMaxWidth = Math.ceil(canvas.width * 0.16); + + const contractedByTextSize = Math.ceil(canvas.height * 0.02); + const contractedByTextMaxWidth = Math.ceil(canvas.width * 0.9); + const contractedByTextSidesMargin = Math.ceil(canvas.width * 0.03); + + if (options.mainText !== null) { + /* BEGIN Background render */ + + if (backgroundImage === null || options.colors.background.value != previousBackgroundImageColor.value) { + backgroundImage = new Image(); + + await new Promise((resolve) => { + backgroundImage.onload = () => { + resolve(); + }; + + if (options.colors.background.value == COLORS.black.value) { + backgroundImage.src = backgroundURL; + } else { + backgroundImage.src = backgroundURLInverted; + } + }); + + backgroundImage = new fabric.Image(backgroundImage, { + top: canvas.height - backgroundImage.height, // FIXME: Why???? Fabric.js, what are you trying to tell me?! + left: -20, + zIndex: 10, + selectable: false, + }); + backgroundImage.scaleToWidth(canvas.width + 22); + + previousBackgroundImageColor = options.colors.background; + + canvas.add(backgroundImage); + } + + /* END Background render */ + + + /* BEGIN Name text render */ + + if (options.personName !== null) { + let styles = { + 0: {}, + }; + + /* + for (let position = 0; position < options.personName.length; position++) { + styles[0][position] = { + fontWeight: "bold", + }; + } + */ + + personNameText = new fabric.Textbox(options.personName, { + left: nameTextMarginLeft, + top: canvas.height - bottomTextSize - nameTextMarginBottom, + width: nameTextMaxWidth, + fontFamily: "Bebas Neue", + fontSize: bottomTextSize, + styles: styles, + fill: options.colors.nameText.value, + selectable: false, + zIndex: 20, + }); + + checkTextBoxHeight(personNameText, 1); + + if (options.personPosition !== null) { + personPositionText = new fabric.Textbox(options.personPosition, { + left: positionTextSideGap, + top: canvas.height - bottomTextSize - positionTextMarginBottom, + width: positionTextMaxWidth, + fontFamily: "Roboto Condensed", + fontSize: positionTextSize, + fill: options.colors.baseText.value, + selectable: false, + zIndex: 20, + }); + + checkTextBoxHeight(personPositionText, 1); + + if (personPositionText._textLines.length === 2) { + mainTextMarginBottom += nameTextExtraBottomMargin; + personNameText.set({ + top: personNameText.top - nameTextExtraBottomMargin, + }); + personPositionText.set({ + top: personPositionText.top - nameTextExtraBottomMargin, + }); + } + + canvas.add(personPositionText); + + canvas.renderAll(); + } + + canvas.add(personNameText); + } + + /* END Name text render */ + + /* BEGIN Main text render */ + + const mainTextWidth = canvas.width - textMarginRight; + + const highlightedData = transformHighlightedText( + options.mainText, + mainTextSize, + mainTextWidth, + "Bebas Neue", + options.colors.highlight.value, + options.colors.highlightedText.value, + { padWhenDiacritics: false, invertHighlight: true }, + ); + + mainTextBox = new PaddedHighlightingTextbox(highlightedData.text, { + width: canvas.width, + left: textMarginLeft, + textAlign: "left", + fontFamily: "Bebas Neue", + fontSize: mainTextSize, + lineHeight: mainTextLineHeight, + fill: options.colors.baseText.value, + styles: highlightedData.styles, + selectable: false, + highlightPadding: canvas.height * 0.003, + zIndex: 20, + }); + + checkTextBoxHeight(mainTextBox, 1); + + canvas.add(mainTextBox); + + const mainTextBoxTop = + canvas.height - mainTextBox.height - mainTextMarginBottom; + + mainTextBox.top = mainTextBoxTop - highlightedData.paddingBottom; + + canvas.renderAll(); + + /* END Main text render */ + } + + /* BEGIN Contracted by render */ + + if (options.contractedBy !== null) { + contractedByTextbox = new fabric.Textbox(options.contractedBy, { + left: contractedByTextSidesMargin, + top: contractedByTextSidesMargin, + width: contractedByTextMaxWidth, + fontFamily: "Roboto Condensed", + fontSize: contractedByTextSize, + textAlign: "left", + fill: options.colors.contractedByText.value, + selectable: false, + zIndex: 20, + }); + + checkTextBoxHeight(contractedByTextbox, 1); + + canvas.add(contractedByTextbox); + } + + /* END Contracted by render */ + + /* 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 */ + + sortObjects(canvas); +}; + +export default redraw;