diff --git a/frontend/src/templates.js b/frontend/src/templates.js index 2a90c7163ed1068b377771efb3b883d13fd4f9c9..504e53afc94c1cd8b895336c81b7b7d78d23bf20 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 nakopnemeBasicPhotoBannerImage from "./assets/previews/nakopneme_basic_photo_banner.png"; import nakopnemeBasicPhotoBannerImageFZ from "./assets/previews/nakopneme_basic_photo_banner_fz.png"; +import nakopnemeBasicPhotoBannerImageFZBar from "./assets/previews/nakopneme_basic_photo_banner_fz.png"; import urgentBasicPhotoBannerImage from "./assets/previews/urgent_basic_photo_banner.png"; import makeawishPhotoBannerImage from "./assets/previews/make_a_wish_photo_banner.png"; import makeawishTourSocialImage from "./assets/previews/make_a_wish_tour_social.png"; @@ -71,8 +72,8 @@ const TEMPLATES = { title: "Nakopneme to! Základní banner s fotkou", }, }, - nakopneme_basic_photo_banner: { - name: "Nakopneme to! - Základní banner s fotkou F/Z pozadi", + nakopneme_basic_photo_banner_fz: { + name: "Nakopneme to! - Základní banner s fotkou F/Ž pozadí", image: nakopnemeBasicPhotoBannerImageFZ, path: "/nakopneme-basic-photo-banner-fz", component: () => @@ -80,7 +81,19 @@ const TEMPLATES = { "./views/nakopneme_basic_photo_banner_fz/NakopnemeBasicPhotoBannerFZ.vue" ), meta: { - title: "Nakopneme to! Základní banner s fotkou - F/Z pozadi", + title: "Nakopneme to! Základní banner s fotkou - F/Ž pozadí", + }, + }, + nakopneme_basic_photo_banner_fzbar: { + name: "Nakopneme to! - Základní banner s fotkou (F/Ž bar nahoře)", + image: nakopnemeBasicPhotoBannerImageFZBar, + path: "/nakopneme-basic-photo-banner-fz-bar", + component: () => + import( + "./views/nakopneme_basic_photo_banner/NakopnemeBasicPhotoBanner.vue" + ), + meta: { + title: "Nakopneme to! Základní banner s fotkou (F/Ž bar nahoře)", }, }, make_a_wish_banner: { diff --git a/frontend/src/views/nakopneme_basic_photo_banner_fzbar/NakopnemeBasicPhotoBannerFZBar.vue b/frontend/src/views/nakopneme_basic_photo_banner_fzbar/NakopnemeBasicPhotoBannerFZBar.vue new file mode 100644 index 0000000000000000000000000000000000000000..c6a9a68491434251f90a7ca3c2c0cc91a6f2651b --- /dev/null +++ b/frontend/src/views/nakopneme_basic_photo_banner_fzbar/NakopnemeBasicPhotoBannerFZBar.vue @@ -0,0 +1,254 @@ +<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 { + generateDefaultBadges, + LOGO_POSITIONS, + generateLogoPositions, +} from "../../logos"; +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: "Základní barvy", + colors: { + background: COLORS.black, + highlight: COLORS.yellow1, + baseText: COLORS.white, + highlightedText: COLORS.black, + contractedByText: COLORS.gray1, + }, + }, + }; + + return { + mainImage: null, + mainText: null, + personName: null, + personPosition: null, + contractedBy: DEFAULT_CONTRACTOR, + logoImage: null, + logoPosition: LOGO_POSITIONS.top_right, + logoOptions: generateLogoPositions(["top_right", "top_left"]), + gradientHeightMultiplier: 1, + colorLabels: { + background: "Pozadí", + highlight: "Zvýraznění", + baseText: "Text", + highlightedText: "Zvýrazněný text", + }, + predefinedColors: predefinedColors, + colors: predefinedColors.base.colors, + predefinedBadgeImages: generateDefaultBadges("defaultDark"), + autoRedraw: false, + }; + }, + async created() { + await loadCanvasStorage(this); + }, + methods: { + async reloadCanvasProperties() { + const canvasProperties = { + mainImage: this.mainImage, + mainText: this.mainText, + logoPosition: this.logoPosition, + personName: this.personName, + personPosition: this.personPosition, + contractedBy: this.contractedBy, + logoImage: this.logoImage, + gradientHeightMultiplier: this.gradientHeightMultiplier, + 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.logoPosition, + vm.personName, + vm.personPosition, + vm.contractedBy, + vm.logoImage, + vm.gradientHeightMultiplier, + 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="1080" + height="1350" + /> + </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 /> + + <ImageInput + name="Obrázek loga" + v-model="logoImage" + :important="false" + :predefinedImages="predefinedBadgeImages" + :mustSelectPredefinedImage="true" + :disableImageInput="true" + zIndex="7" + /> + + <SelectInput + name="Pozice loga" + :options="logoOptions" + v-model="logoPosition" + zIndex="6" + /> + + <RangeInput + name="Výška gradientu" + v-model="gradientHeightMultiplier" + min="0" + max="3" + /> + + <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/nakopneme_basic_photo_banner_fzbar/canvas.js b/frontend/src/views/nakopneme_basic_photo_banner_fzbar/canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..93dd7dc57605e8293e480e9f2d5e3e3768d804db --- /dev/null +++ b/frontend/src/views/nakopneme_basic_photo_banner_fzbar/canvas.js @@ -0,0 +1,277 @@ +import * as fabric from "fabric"; +import { + clearObjects, + sortObjects, + transformHighlightedText, + checkTextBoxHeight, + getSingleLineTextBoxWidth, +} from "../../components/canvas/utils"; +import { PaddedHighlightingTextbox } from "../../components/canvas/textbox"; +import leftQuoteImage from "../../assets/template/nakopneme_basic_photo_banner/nakopneme_quote_left.png"; +import rightQuoteImage from "../../assets/template/nakopneme_basic_photo_banner/nakopneme_quote_right.png"; + +let mainTextBox = null; +let mainTextBoxBackground = null; + +let personNameText = null; +let personInfoSeparator = null; +let personPositionText = null; + +let mainImage = null; +let logoImage = null; + +let contractedByTextbox = null; + +let mainImageSource = null; +let previousLogoPosition = null; + +let leftQuote = null; +let rightQuote = null; + +const redraw = async (canvas, options) => { + canvas.controlsAboveOverlay = true; + + clearObjects( + [ + mainTextBox, + mainTextBoxBackground, + personNameText, + personInfoSeparator, + personPositionText, + contractedByTextbox, + leftQuote, + rightQuote, + ], + canvas, + ); + + canvas.preserveObjectStacking = true; + + const textMarginLeft = Math.ceil(canvas.width * 0.14); + const textMarginRight = Math.ceil(canvas.width * 0.075); + + let mainTextMarginBottom = Math.ceil(canvas.height * 0.06); + const mainTextBackgroundMarginTop = Math.ceil(canvas.height * 0.1); + const mainTextSize = Math.ceil(canvas.height * 0.075); + const mainTextHeightLimit = Math.ceil(mainTextSize * 3.3); + const mainTextLineHeight = 0.9; + + const bottomTextSize = Math.ceil(canvas.height * 0.055); + const nameTextMarginBottom = Math.ceil(canvas.height * 0.065); + const positionTextSideGap = Math.ceil(canvas.width * 0.01); + const positionTextSeparatorWidth = Math.ceil(canvas.width * 0.0035); + const positionTextMaxWidth = Math.ceil(canvas.width * 0.4); + + const contractedByTextSize = Math.ceil(canvas.height * 0.02); + const contractedByTextMaxWidth = Math.ceil(canvas.width * 0.9); + const contractedByTextSidesMargin = Math.ceil(canvas.width * 0.03); + + const logoWidth = Math.ceil(canvas.width * 0.13); + const logoSideMargin = Math.ceil(canvas.width * 0.07); + + if (options.mainText !== null) { + /* BEGIN Main text render */ + + const mainText = options.mainText; + const mainTextWidth = canvas.width - textMarginLeft - textMarginRight; + + const highlightedData = transformHighlightedText( + mainText, + mainTextSize, + mainTextWidth, + "Bebas Neue", + options.colors.highlight.value, + options.colors.highlightedText.value, + { padWhenDiacritics: true }, + ); + + mainTextBox = new PaddedHighlightingTextbox(highlightedData.text, { + width: canvas.width, + left: 0, + textAlign: "center", + fontFamily: "Bebas Neue", + fontSize: mainTextSize, + lineHeight: mainTextLineHeight, + fill: "#000", + styles: highlightedData.styles, + selectable: false, + highlightPadding: canvas.height * 0.003, + zIndex: 10, + }); + + checkTextBoxHeight(mainTextBox, 4); + + canvas.add(mainTextBox); + + const mainTextBoxTop = + canvas.height - mainTextBox.height - mainTextMarginBottom; + + mainTextBox.top = mainTextBoxTop - highlightedData.paddingBottom; + + canvas.renderAll(); + + /* END Main text render */ + + /* BEGIN Main text background render */ + + const backgroundHeight = + canvas.height - mainTextBoxTop + mainTextBackgroundMarginTop; + + mainTextBoxBackground = new fabric.Rect({ + width: canvas.width + 30, // FIXME: Whhhhyyyyyy???? + height: backgroundHeight + 150 * options.gradientHeightMultiplier, + left: -20, + top: + mainTextBoxTop - + mainTextBackgroundMarginTop - + backgroundHeight * (options.gradientHeightMultiplier - 1) - + 150, + fill: new fabric.Gradient({ + type: "linear", + gradientUnits: "pixels", + coords: { + x1: 0, + y1: 0, + x2: 0, + y2: backgroundHeight * options.gradientHeightMultiplier, + }, + colorStops: [ + { + offset: 0, + color: "#ffffff00", + }, + { + offset: 0.5, + color: "#fdc800", + }, + { + offset: 0.6, + color: "#fdc800", + }, + { + offset: 1, + color: "#bd7eb4", + }, + ], + }), + selectable: false, + zIndex: 9, + }); + + canvas.add(mainTextBoxBackground); + + /* END Main text background render */ + } + + /* BEGIN Logo render */ + + // A logo is provided, and it either hasn't been rendered yet or is a new one. + const createNewLogo = + (options.logoImage !== null && + (logoImage === null || + (options.logoImage !== null && + options.logoImage !== logoImage._element))) || + previousLogoPosition != options.logoPosition.id; + + previousLogoPosition = options.logoPosition.id; + + if (createNewLogo) { + canvas.remove(logoImage); + + logoImage = new fabric.Image(options.logoImage, { selectable: false }); + logoImage.scaleToWidth(logoWidth); + + if (options.logoPosition.id == "top-right") { + logoImage.set({ + left: canvas.width - logoWidth - logoSideMargin, + top: logoSideMargin, + zIndex: 11, + }); + } else { + logoImage.set({ + left: logoSideMargin, + top: logoSideMargin, + zIndex: 11, + }); + } + + canvas.add(logoImage); + } + + /* END Logo render */ + + /* BEGIN Contracted by render */ + + if (options.contractedBy !== null) { + contractedByTextbox = new fabric.Textbox(options.contractedBy, { + left: + canvas.width - contractedByTextMaxWidth - contractedByTextSidesMargin, + top: canvas.height - contractedByTextSidesMargin - contractedByTextSize, + width: contractedByTextMaxWidth, + fontFamily: "Roboto Condensed", + fontSize: contractedByTextSize, + textAlign: "right", + fill: "#000", + selectable: false, + zIndex: 10, + }); + + 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.setControlsVisibility({ + // corners (uniform scale) + tl: true, + tr: true, + bl: true, + br: true, + // mids (scale X/Y independently) + ml: true, + mr: true, + mt: true, + mb: true, + // rotation + mtr: 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;