diff --git a/VERSION b/VERSION index 10c2c0c3d62137a46deee939bc5e6ad677a518c5..46b81d815a23b1a6b60bc9160f21295a5f9e4e75 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.10.0 +2.11.0 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0050f0def95fe6a6ad3a5e112f47ae714bbb47e8..55c1ae03038f525a97690561310e66eb4fe61f73 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1835,9 +1835,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001512", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz", - "integrity": "sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "dev": true, "funding": [ { diff --git a/frontend/src/assets/previews/event_poster.png b/frontend/src/assets/previews/event_poster.png new file mode 100644 index 0000000000000000000000000000000000000000..192353ea377fcf46577f48ffe49fd0de5c8833cf Binary files /dev/null and b/frontend/src/assets/previews/event_poster.png differ diff --git a/frontend/src/assets/previews/kamery.jpg b/frontend/src/assets/previews/kamery.jpg deleted file mode 100644 index 5765fc44b1c086bdde7970162a56acad5e2d5596..0000000000000000000000000000000000000000 Binary files a/frontend/src/assets/previews/kamery.jpg and /dev/null differ diff --git a/frontend/src/assets/template/event_poster/base.png b/frontend/src/assets/template/event_poster/base.png new file mode 100644 index 0000000000000000000000000000000000000000..f70d106785cde2183a924cbdd0e48381258f43fe Binary files /dev/null and b/frontend/src/assets/template/event_poster/base.png differ diff --git a/frontend/src/templates.js b/frontend/src/templates.js index 66ff3fb513cdadd17942a5083a79a4b4a67cd665..1a7c5d437f3a8b0ee28fdebd00dbafb090e1734e 100644 --- a/frontend/src/templates.js +++ b/frontend/src/templates.js @@ -7,6 +7,7 @@ import newspaperQuoteMiddleImage from "./assets/previews/newspaper_quote_middle. import facebookSurveyImage from "./assets/previews/facebook_survey.png"; import twitterBannerImage from "./assets/previews/twitter_banner.png"; 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"; @@ -99,6 +100,15 @@ const TEMPLATES = { title: "Plakát", }, }, + event_poster: { + name: "Plakát - událost", + image: eventPosterImage, + path: "/event-poster", + component: () => import("./views/event_poster/EventPoster.vue"), + meta: { + title: "Plakát - událost", + }, + }, twitter_banner: { name: "Twitter banner", image: twitterBannerImage, diff --git a/frontend/src/views/event_poster/EventPoster.vue b/frontend/src/views/event_poster/EventPoster.vue new file mode 100644 index 0000000000000000000000000000000000000000..7243d2f36f340b919cf01c1ee59735f9bd766a2e --- /dev/null +++ b/frontend/src/views/event_poster/EventPoster.vue @@ -0,0 +1,243 @@ +<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, + toRawDeep, +} 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.white, + baseText: COLORS.black, + contractedByText: COLORS.gray1, + arrow: COLORS.yellow1, + }, + }, + }; + + return { + mainText: null, + + mainImage: null, + + eventLocation: null, + eventDate: null, + + firstColumn: null, + secondColumn: null, + + contractedBy: DEFAULT_CONTRACTOR, + + colorLabels: { + background: "Pozadí", + baseText: "Text", + highlightedText: "Zvýrazněný text", + arrow: "Šipka", + }, + + predefinedColors: predefinedColors, + colors: predefinedColors.base.colors, + autoRedraw: false, + }; + }, + async created() { + await loadCanvasStorage(this); + }, + methods: { + async reloadCanvasProperties() { + const canvasProperties = { + mainImage: this.mainImage, + mainText: this.mainText, + eventLocation: this.eventLocation, + eventDate: this.eventDate, + firstColumn: this.firstColumn, + secondColumn: this.secondColumn, + contractedBy: this.contractedBy, + colors: this.colors, + }; + + if (canvasProperties.mainText) { + window.fileName = canvasProperties.mainText; + } + + await this.$refs.canvas.redraw(canvasProperties); + + let canvasPropertiesToSave = structuredClone(toRawDeep(canvasProperties)); + delete canvasPropertiesToSave.colors; + + setCanvasStorage(canvasPropertiesToSave); + }, + }, + mounted() { + this.$watch( + (vm) => [ + vm.mainText, + vm.mainImage, + vm.eventLocation, + vm.eventDate, + vm.firstColumn, + vm.secondColumn, + 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.event_poster"></Navbar> + </header> + <main> + <MainContainer> + <template v-slot:left> + <Canvas + ref="canvas" + :redrawFunction="redraw" + width="3625" + height="5078" + /> + </template> + + <template v-slot:right> + <ReloadButton :parentRefs="$refs" @click="reloadCanvasProperties" /> + <AutoReloadCheckbox v-model="autoRedraw" /> + + <ImageInput + name="Obrázek" + v-model="mainImage" + :important="true" + zIndex="11" + /> + + <ShortTextInput + name="Nadpis" + v-model="mainText" + v-model:relatedModel="mainText" + :important="true" + zIndex="10" + /> + + <ShortTextInput + name="Datum" + v-model="eventDate" + :important="false" + zIndex="9" + /> + <ShortTextInput + name="Lokace" + v-model="eventLocation" + :important="false" + zIndex="8" + /> + + <LongTextInput + name="Sloupec 1" + v-model="firstColumn" + :important="true" + :highlightable="true" + zIndex="7" + /> + <LongTextInput + name="Sloupec 2" + v-model="secondColumn" + :important="true" + :highlightable="true" + zIndex="6" + /> + + <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/event_poster/canvas.js b/frontend/src/views/event_poster/canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..9289ea4dac3ada26bdfd25a0a7468804c0b49dca --- /dev/null +++ b/frontend/src/views/event_poster/canvas.js @@ -0,0 +1,321 @@ +import { fabric } from "fabric"; +import { + clearObjects, + sortObjects, + transformHighlightedText, + checkTextBoxHeight, +} from "../../components/canvas/utils"; +import { PaddedHighlightingTextbox } from "../../components/canvas/textbox"; +import overlayURL from "../../assets/template/event_poster/base.png"; + +let mainImage = null; +let mainImageSource = null; +let overlayImage = null; +let pointerDownEventAssigned = false; +let eventDateText = null; +let eventLocationText = null; +let mainText = null; +let firstColumn = null; +let secondColumn = null; + +let contractedByTextbox = null; + +const removeDownEventListener = () => { + document + .getElementsByClassName("upper-canvas")[0] + .removeEventListener("pointerdown", canvasPointerDownEvent); +}; + +let upEventFunction = null; +let canvasPointerDownEvent = null; + +const redraw = async (canvas, options) => { + clearObjects( + [ + eventDateText, + eventLocationText, + contractedByTextbox, + mainText, + firstColumn, + secondColumn, + ], + canvas, + ); + + const bottomMarginText = 200; + const bottomFontSize = 90; + + const mainTextMarginBottom = 2200; + const mainTextSize = 600; + const mainTextMarginSides = 250; + + const secondaryTextSize = 300; + const secondaryTextMarginBottom = 1600; + const secondaryTextMarginSides = 325; + + const columnsMarginTop = -1000; + const columnsMarginBetween = 100; + const columnsMaxWidth = 1500; + const columnTextSize = 130; + const columnLineHeight = 1; + + const contractedByTextSize = Math.ceil(canvas.height * 0.014); + const contractedByTextMaxWidth = Math.ceil(canvas.width * 0.9); + const contractedByTextSidesMargin = Math.ceil(canvas.width * 0.03); + const contractedByTextBottomMargin = Math.ceil(canvas.height * 0.012); + + document + .getElementsByClassName("upper-canvas")[0] + .removeEventListener("pointerup", upEventFunction); + document + .getElementsByClassName("upper-canvas")[0] + .removeEventListener("pointerout", upEventFunction); + document + .getElementsByClassName("upper-canvas")[0] + .removeEventListener("pointercancel", upEventFunction); + + 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: 10, + }); + + 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) + + removeDownEventListener(); + pointerDownEventAssigned = false; + } else if (mainImage !== null && options.mainImage === null) { + canvas.remove(mainImage); + + removeDownEventListener(); + pointerDownEventAssigned = false; + } + + /* END Main image render */ + + /* BEGIN Overlay render */ + + if (overlayImage === null) { + overlayImage = new Image(); + + await new Promise((resolve) => { + overlayImage.onload = () => { + resolve(); + }; + + overlayImage.src = overlayURL; + }); + + overlayImage = new fabric.Image(overlayImage, { + top: -20, // FIXME: Why???? Fabric.js, what are you trying to tell me?! + left: -20, + zIndex: 20, + selectable: false, + }); + overlayImage.scaleToWidth(canvas.width + 22); + + canvas.add(overlayImage); + } + + /* END Overlay render */ + + /* BEGIN Main text render */ + + if (options.mainText !== null) { + mainText = new fabric.Textbox(options.mainText, { + left: mainTextMarginSides, + top: canvas.height - mainTextMarginBottom, + width: canvas.width - mainTextMarginSides * 2, + textAlign: "center", + fontFamily: "Bebas Neue", + fontSize: mainTextSize, + fill: "#fec900", + selectable: false, + zIndex: 40, + }); + + // Keep to a single line no matter what + canvas.add(mainText); + + while (mainText._textLines.length > 1) { + mainText.set({ + fontSize: mainText.fontSize - 20, + top: mainText.top + 10, + }); + canvas.renderAll(); + } + } + + /* END Main text render */ + + /* BEGIN Event date text render */ + + if (options.eventDate !== null) { + eventDateText = new fabric.Text(options.eventDate, { + left: secondaryTextMarginSides, + top: canvas.height - secondaryTextMarginBottom, + fontFamily: "Bebas Neue", + fontSize: secondaryTextSize, + fill: "#fec900", + selectable: false, + zIndex: 40, + }); + + canvas.add(eventDateText); + } + + /* END Event date text render */ + + /* BEGIN Event location text render */ + + if (options.eventLocation !== null) { + eventLocationText = new fabric.Text(options.eventLocation, { + left: canvas.width - secondaryTextMarginSides, + top: canvas.height - secondaryTextMarginBottom, + fontFamily: "Bebas Neue", + fontSize: secondaryTextSize, + textAlign: "right", + fill: "#fec900", + selectable: false, + zIndex: 40, + }); + + canvas.add(eventLocationText); + + eventLocationText.set({ + left: eventLocationText.left - eventLocationText.width + }); + canvas.renderAll(); + } + + /* END Event location text render */ + + /* BEGIN Column text render */ + + if (options.firstColumn !== null) { + firstColumn = new fabric.Textbox(options.firstColumn, { + left: mainTextMarginSides, + top: canvas.height + columnsMarginTop, + width: columnsMaxWidth, + fontFamily: "Roboto Condensed", + fontSize: columnTextSize, + lineHeight: columnLineHeight, + fill: "#000", + selectable: false, + zIndex: 40, + }); + + canvas.add(firstColumn); + } + + if (options.secondColumn !== null) { + secondColumn = new fabric.Textbox(options.secondColumn, { + left: mainTextMarginSides + columnsMarginBetween + columnsMaxWidth, + top: canvas.height + columnsMarginTop, + width: columnsMaxWidth, + fontFamily: "Roboto Condensed", + fontSize: columnTextSize, + lineHeight: columnLineHeight, + fill: "#000", + selectable: false, + zIndex: 40, + }); + + canvas.add(secondColumn); + } + + /* END Column text render */ + + /* BEGIN Contracted by render */ + + if (options.contractedBy !== null) { + contractedByTextbox = new fabric.Textbox(options.contractedBy, { + left: + canvas.width - contractedByTextMaxWidth - contractedByTextSidesMargin, + top: canvas.height - contractedByTextBottomMargin - contractedByTextSize, + width: contractedByTextMaxWidth, + fontFamily: "Roboto Condensed", + fontSize: contractedByTextSize, + textAlign: "right", + fill: "#505050", + selectable: false, + zIndex: 40, + }); + + checkTextBoxHeight(contractedByTextbox, 1); + + canvas.add(contractedByTextbox); + } + + /* END Contracted by render */ + + sortObjects(canvas); + + canvasPointerDownEvent = (event) => { + let activeObject = canvas.getActiveObject(); + + if (activeObject === null) { + return; + } + + // if (activeObject._element.src == mainImage._element.src) { + // return + // } + + canvas.remove(overlayImage); + overlayImage = null; + }; + + if (!pointerDownEventAssigned) { + document + .getElementsByClassName("upper-canvas")[0] + .addEventListener("pointerdown", canvasPointerDownEvent); + + pointerDownEventAssigned = true; + } + + upEventFunction = (event) => { + redraw(canvas, options); + }; + + document + .getElementsByClassName("upper-canvas")[0] + .addEventListener("pointerup", upEventFunction); + + document + .getElementsByClassName("upper-canvas")[0] + .addEventListener("pointerout", upEventFunction); + + document + .getElementsByClassName("upper-canvas")[0] + .addEventListener("pointercancel", upEventFunction); +}; + +export default redraw;