diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 55c1ae03038f525a97690561310e66eb4fe61f73..25683bf359fff763767ac8f70c10819ed033bf0a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1835,9 +1835,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "dev": true, "funding": [ { @@ -1852,7 +1852,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/canvas": { "version": "2.11.2", diff --git a/frontend/src/assets/previews/make_a_wish_photo_banner.png b/frontend/src/assets/previews/make_a_wish_photo_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c680ea45777ce266151d363f101ee57fd0176a64 Binary files /dev/null and b/frontend/src/assets/previews/make_a_wish_photo_banner.png differ diff --git a/frontend/src/assets/template/make_a_wish_banner/background.png b/frontend/src/assets/template/make_a_wish_banner/background.png new file mode 100644 index 0000000000000000000000000000000000000000..5f95bacfb639fa51c190385aadbdf40afd3e77db Binary files /dev/null and b/frontend/src/assets/template/make_a_wish_banner/background.png differ diff --git a/frontend/src/templates.js b/frontend/src/templates.js index 6250f02b92db669151be7469dc8c203556795133..ac51345254e90c595aa82868715066f357d59e3a 100644 --- a/frontend/src/templates.js +++ b/frontend/src/templates.js @@ -1,5 +1,6 @@ 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 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"; @@ -45,6 +46,15 @@ const TEMPLATES = { title: "Urgentní banner s fotkou", }, }, + make_a_wish_banner: { + name: "Máte přání banner", + image: makeawishPhotoBannerImage, + path: "/make-a-wish-banner", + component: () => import("./views/make_a_wish_banner/MakeAWishBanner.vue"), + meta: { + title: "Máte přání banner", + }, + }, urgent_text_banner: { name: "Urgentní banner pouze s textem", image: urgentTextBannerImage, diff --git a/frontend/src/views/make_a_wish_banner/MakeAWishBanner.vue b/frontend/src/views/make_a_wish_banner/MakeAWishBanner.vue new file mode 100644 index 0000000000000000000000000000000000000000..34854062735854cba2b67761ea4589ebc8e7312a --- /dev/null +++ b/frontend/src/views/make_a_wish_banner/MakeAWishBanner.vue @@ -0,0 +1,214 @@ +<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: "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, + 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/make_a_wish_banner/canvas.js b/frontend/src/views/make_a_wish_banner/canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..41d2c33625944afd44f136a4c86d53adb3aaad40 --- /dev/null +++ b/frontend/src/views/make_a_wish_banner/canvas.js @@ -0,0 +1,252 @@ +import { fabric } from "fabric"; +import { + clearObjects, + sortObjects, + transformHighlightedText, + checkTextBoxHeight, +} from "../../components/canvas/utils"; +import { PaddedHighlightingTextbox } from "../../components/canvas/textbox"; +import backgroundURL from "../../assets/template/make_a_wish_banner/background.png"; + +let mainTextBox = null; + +let personNameText = null; +let personPositionText = null; + +let mainImage = null; +let backgroundImage = 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.2); + + let mainTextMarginBottom = Math.ceil(canvas.height * 0.05); + 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.1); + const nameTextMarginLeft = Math.ceil(canvas.width * 0.05); + const nameTextExtraBottomMargin = Math.ceil(canvas.height * 0.05); + const positionTextSideGap = nameTextMarginLeft; + const positionTextMarginBottom = Math.ceil(canvas.height * 0.06); + 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); + + if (options.mainText !== null) { + /* BEGIN Background render */ + + if (backgroundImage === null) { + backgroundImage = new Image(); + + await new Promise((resolve) => { + backgroundImage.onload = () => { + resolve(); + }; + + backgroundImage.src = backgroundURL; + }); + + backgroundImage = new fabric.Image(backgroundImage, { + top: canvas.height - backgroundImage.height, // FIXME: Why???? Fabric.js, what are you trying to tell me?! + left: -20, + zIndex: 0, + selectable: false, + }); + backgroundImage.scaleToWidth(canvas.width + 22); + + 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.Text(options.personName, { + left: nameTextMarginLeft, + top: canvas.height - bottomTextSize - nameTextMarginBottom, + fontFamily: "Bebas Neue", + fontSize: bottomTextSize, + styles: styles, + fill: options.colors.highlight.value, + selectable: false, + zIndex: 10, + }); + + 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: 10, + }); + + checkTextBoxHeight(personPositionText, 2); + + 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 - textMarginLeft - 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: 10, + }); + + checkTextBoxHeight(mainTextBox, 3); + + 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: 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.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;