From b8f63935ec2c798fac6f3b16b82f1c6882c5f7ca Mon Sep 17 00:00:00 2001 From: Asturur Date: Wed, 11 Dec 2024 00:33:09 +0100 Subject: [PATCH 01/22] documenting --- README.md | 6 +++--- TEMPLATES.md | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 TEMPLATES.md diff --git a/README.md b/README.md index 6e8accd..04a576e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# TapTo Designer +# Zaparoo Designer This is a companion app to the TapTo system -You can read more about TapTo [here](https://github.com/wizzomafizzo/tapto) +You can read more about TapTo [here](https://github.com/ZaparooProject/zaparoo-core) # How to use it -This app is deploted at [https://tapto-designer.netlify.app/](https://tapto-designer.netlify.app/) and you can just use it, providing your own images or using the embedded search functionality. +This app is deploted at [https://design.zaparoo.org/](https://design.zaparoo.org/) and you can just use it, providing your own images or using the embedded search functionality. It has no server, no login, no way to save progress. Is meant for producing labels and download the result right away. Everything runs locally, your images aren't unploaded to any server. diff --git a/TEMPLATES.md b/TEMPLATES.md new file mode 100644 index 0000000..40b14ad --- /dev/null +++ b/TEMPLATES.md @@ -0,0 +1,5 @@ +# Templates format + +Templates are SVG. +The SVG [format](https://www.w3.org/TR/SVG11/) is very flexible, SVGs come in many forms and shape and support a lot of features. +The Zaparoo designer does not support any SVG feature. Zaparoo designer uses FabricJS to parse and render templates on a canvas and export them on a printable PDF, and so it inherits all the FabricJS limitations. From 97e9df6436d952861ff76a7044ccb7162233d894 Mon Sep 17 00:00:00 2001 From: Asturur Date: Wed, 11 Dec 2024 00:44:12 +0100 Subject: [PATCH 02/22] more writing --- TEMPLATES.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/TEMPLATES.md b/TEMPLATES.md index 40b14ad..87c3883 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -1,5 +1,29 @@ -# Templates format +# Zaparoo templates format Templates are SVG. The SVG [format](https://www.w3.org/TR/SVG11/) is very flexible, SVGs come in many forms and shape and support a lot of features. The Zaparoo designer does not support any SVG feature. Zaparoo designer uses FabricJS to parse and render templates on a canvas and export them on a printable PDF, and so it inherits all the FabricJS limitations. + +## General guidelines + +Zaparoo designer supports the following svg tags: + +- Image +- Rect +- Path +- Gradient + +The SVG vector format can be stretched without issues, but as a generic reference and to help everyone understand the template is a good idea to use a viewport that starts from 0,0 and to have no content bleeding outside the viewport. + +The preferred unit for measures is in pixels, as many pixels as you would need to print at 300dpi. This is only for easyness of calculation and debugging. + +So for example if you are working for a 2 by 3 inch label, it is advised to work with a 600 by 900 canvas and a viewBox that is "0 0 600 900" + +If you want to contribute a template for the designer do not add any copyrighted material in it. + +If you are building a template for a specific console or videogame company, do not add the console logo or company logo, but uses placeholders and then users will search for their own logos on third party api or search engines and place them in the template. + +When submitting a template add a license in it with an xml comment. +You can write anything that you want in your own license, but if you can use one of those premade ones you will make easier to understand what is allowed to do with your template. A classic example is to allow or not allow other users to modify your template and create new ones, or sell it on etsy. + +## The canvas dimensions From 8cc7c3e1a1744e82fb5829911b1b2bbf01c8487c Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Wed, 11 Dec 2024 12:31:15 +0800 Subject: [PATCH 03/22] Update readme --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 04a576e..2eb978b 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # Zaparoo Designer -This is a companion app to the TapTo system -You can read more about TapTo [here](https://github.com/ZaparooProject/zaparoo-core) +A companion app to the Zaparoo project. The Designer allows you to easily generate Zaparoo labels from templates, ready to print and cut, right from your web browser using external sources for game media. Read more about the Zaparoo project at [Zaparoo.org](https://zaparoo.org/). -# How to use it +Zaparoo Designer is officially hosted at: [design.zaparoo.org](https://design.zaparoo.org/) -This app is deploted at [https://design.zaparoo.org/](https://design.zaparoo.org/) and you can just use it, providing your own images or using the embedded search functionality. -It has no server, no login, no way to save progress. -Is meant for producing labels and download the result right away. -Everything runs locally, your images aren't unploaded to any server. +# Usage -You can load files by drag and drop or by browsing your file system +1. Navigate to: [design.zaparoo.org](https://design.zaparoo.org/) +2. Select `Add files` to upload a local image or `Search image` to search and load an image from [IGDB](https://www.igdb.com/). +3. Make any template, color or media transformations you like to the final label. +4. Repeat steps 2 and 3 for each label you want to create this session. +5. Select `Print` and configure the output that works best for your printer or plotter. +6. Press `Download`. +7. Print the output PDF or PNG files on your printer. -image +## Trademarks -Once you load some files you are presented with a default grid of cards with the default template: - -image +This repository contains Zaparoo trademark assets which are explicitly licensed to this project in this location by the trademark owner. These trademarks must be removed from the project or replaced if you intend to redistribute or adapt the project in any form. See the Zaparoo [Terms of Use](https://zaparoo.org/terms/) for further details. From 904f796e405c5f0b5eaef79ad25caf5ed039459a Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Wed, 11 Dec 2024 12:45:25 +0800 Subject: [PATCH 04/22] Updated template guide --- TEMPLATES.md | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/TEMPLATES.md b/TEMPLATES.md index 87c3883..482c53d 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -1,29 +1,26 @@ -# Zaparoo templates format +# Templates Format -Templates are SVG. -The SVG [format](https://www.w3.org/TR/SVG11/) is very flexible, SVGs come in many forms and shape and support a lot of features. -The Zaparoo designer does not support any SVG feature. Zaparoo designer uses FabricJS to parse and render templates on a canvas and export them on a printable PDF, and so it inherits all the FabricJS limitations. +Templates are in [SVG format](https://www.w3.org/TR/SVG11/). + +Zaparoo Designer uses [FabricJS](https://fabricjs.com/) to parse/render templates on a canvas and export them on a printable PDF. It is limited by the features FabricJS supports. ## General guidelines -Zaparoo designer supports the following svg tags: +Designer supports the following SVG tags: - Image - Rect - Path - Gradient -The SVG vector format can be stretched without issues, but as a generic reference and to help everyone understand the template is a good idea to use a viewport that starts from 0,0 and to have no content bleeding outside the viewport. - -The preferred unit for measures is in pixels, as many pixels as you would need to print at 300dpi. This is only for easyness of calculation and debugging. +It's recommended to use a viewport that starts from 0,0 and to have no content bleeding outside the viewport. -So for example if you are working for a 2 by 3 inch label, it is advised to work with a 600 by 900 canvas and a viewBox that is "0 0 600 900" +The preferred unit for measurement is in pixels and assuming a final print of 300dpi. For example, if you are working for a 2 by 3 inch label, it is advised to work with a 600 by 900 canvas and a viewBox that is `0 0 600 900`. -If you want to contribute a template for the designer do not add any copyrighted material in it. +Templates contributed to the Zaparoo Designer repository must not contain and copyrighted material or infringe on any trademarks, including Zaparoo trademarks. -If you are building a template for a specific console or videogame company, do not add the console logo or company logo, but uses placeholders and then users will search for their own logos on third party api or search engines and place them in the template. +If you are building a template for a specific console or video game company, do not add the console logo or company logo, use placeholders instead. Users will search for their own logos on a third party API or search engines, and place them in the template. -When submitting a template add a license in it with an xml comment. -You can write anything that you want in your own license, but if you can use one of those premade ones you will make easier to understand what is allowed to do with your template. A classic example is to allow or not allow other users to modify your template and create new ones, or sell it on etsy. +A license must be included in all contributed templates using an XML comment. The license is your choice, but we recommend some variant of the [Creative Commons](https://creativecommons.org/) licenses that works best for your preferences. You can use the [Creative Common License Chooser](https://chooser-beta.creativecommons.org/) to easily pick the most appropriate one. -## The canvas dimensions +## Canvas dimensions From ebd7c4eefbf972055b989ab9eeacc837e54042df Mon Sep 17 00:00:00 2001 From: Asturur Date: Wed, 11 Dec 2024 09:13:22 +0100 Subject: [PATCH 05/22] more --- TEMPLATES.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/TEMPLATES.md b/TEMPLATES.md index 482c53d..1533e38 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -17,10 +17,12 @@ It's recommended to use a viewport that starts from 0,0 and to have no content b The preferred unit for measurement is in pixels and assuming a final print of 300dpi. For example, if you are working for a 2 by 3 inch label, it is advised to work with a 600 by 900 canvas and a viewBox that is `0 0 600 900`. -Templates contributed to the Zaparoo Designer repository must not contain and copyrighted material or infringe on any trademarks, including Zaparoo trademarks. +The template will be scaled anyway to fit the media you assign it to, more on that later. -If you are building a template for a specific console or video game company, do not add the console logo or company logo, use placeholders instead. Users will search for their own logos on a third party API or search engines, and place them in the template. +Templates contributed to the Zaparoo Designer repository must not contain and copyrighted material or infringe on any trademarks, including Zaparoo trademarks. A license must be included in all contributed templates using an XML comment. The license is your choice, but we recommend some variant of the [Creative Commons](https://creativecommons.org/) licenses that works best for your preferences. You can use the [Creative Common License Chooser](https://chooser-beta.creativecommons.org/) to easily pick the most appropriate one. -## Canvas dimensions +## The media dimensions + +Templates on this application are meant to be customized and printed and fit or stick somewhere. As a consequence the template is made with a single size in mind. There may be templates that can be scaled freely to different sizes of the same aspect ratio, but we do not handle that. From 096c32fb08692973b80211f8176013a4ca187bb6 Mon Sep 17 00:00:00 2001 From: Asturur Date: Wed, 11 Dec 2024 09:27:39 +0100 Subject: [PATCH 06/22] progress --- TEMPLATES.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/TEMPLATES.md b/TEMPLATES.md index 1533e38..906b540 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -23,6 +23,37 @@ Templates contributed to the Zaparoo Designer repository must not contain and co A license must be included in all contributed templates using an XML comment. The license is your choice, but we recommend some variant of the [Creative Commons](https://creativecommons.org/) licenses that works best for your preferences. You can use the [Creative Common License Chooser](https://chooser-beta.creativecommons.org/) to easily pick the most appropriate one. +## Tools + +Any application that let you edit SVG is good for this, one free application is [Inkscape](https://inkscape.org/), others are good as well and if you have examples to add to this guide you can contribute with a PR. + +[SVGOmg](https://svgomg.net/) is a tool that will let you improve your svg size and complexity in case Inkscape or your favorite application would add too many tags and attributes that we do not really need + ## The media dimensions Templates on this application are meant to be customized and printed and fit or stick somewhere. As a consequence the template is made with a single size in mind. There may be templates that can be scaled freely to different sizes of the same aspect ratio, but we do not handle that. + +The media dimensions are specified in the width and height of the main svg tag. +To make an example a standard nfc card is sized at 3.375 inches high by 2.125 inches wide. That make at 300dpi: 1012.5 by 637,5 pixels. Now if you are making a sticker for the nfc card you probably want to leave a bit of wiggle room for applying the sticker, or maybe no you want to go larger and then refine the borders with a knife. In this example we leave some border so that it can be applied safely and we pick up 994 by 619 that is a decent fit, and then we will leave some extra white space in the template itself. + +If you are creating an horizontal or vertical template that depends on your taste and you will have to use width and height accordingly. + +The resulting SVG initial tag would look like something like this: + +```xml + + + +``` + +## The template layers + +The SVG is a stack of graphic layer, From d9e4f50cf1769146bc5721104bebbc5e838db020 Mon Sep 17 00:00:00 2001 From: Asturur Date: Tue, 31 Dec 2024 00:04:32 +0100 Subject: [PATCH 07/22] reduction --- TEMPLATES.md | 9 +++++- src/assets/3by5_steam.svg | 65 ++++++++++++++------------------------- src/cardsTemplates.ts | 16 +++------- src/resourcesTypedef.ts | 12 ++++++++ 4 files changed, 47 insertions(+), 55 deletions(-) diff --git a/TEMPLATES.md b/TEMPLATES.md index 906b540..4c81524 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -56,4 +56,11 @@ The resulting SVG initial tag would look like something like this: ## The template layers -The SVG is a stack of graphic layer, +The SVG and so your template will be a stack of layers. +There will be a spot for one or more images between the stack. +Let's take for example our original basic template, the one with the old logo: + +```xml + + +``` diff --git a/src/assets/3by5_steam.svg b/src/assets/3by5_steam.svg index 1289974..57ec77d 100644 --- a/src/assets/3by5_steam.svg +++ b/src/assets/3by5_steam.svg @@ -1,44 +1,25 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/cardsTemplates.ts b/src/cardsTemplates.ts index 29e52d5..78f864a 100644 --- a/src/cardsTemplates.ts +++ b/src/cardsTemplates.ts @@ -24,12 +24,12 @@ import cassetTape from './assets/cassette_tape.svg'; import mininfcAlice from './assets/3by5_steam.svg'; import cardFront from './assets/cardfront.png'; import { Authors } from './templateAuthors'; -import { templateType } from './resourcesTypedef'; +import type { templateType, templateTypeV2 } from './resourcesTypedef'; import { logoResource } from './logos'; import { NFCCCsizeCard, TapeBoxCover, tapToPrePrinted, miniNfcCard } from './printMediaTypes'; -export const templates: Record = { +export const templates: Record = { blankH: { layout: 'horizontal', label: 'Blank H cover', @@ -489,18 +489,10 @@ export const templates: Record = { key: 'cassetteBoxV2', }, miniNfcAlice: { + version: 2, layout: 'vertical', label: 'Steam 3by5cm', - overlay: { - url: mininfcAlice, - isSvg: true, - layerWidth: 354, - layerHeight: 591, - height: 445/591, - width: 305/354, - y: 117/591, - x: 25/354, - }, + url: mininfcAlice, author: Authors.alice, media: miniNfcCard, key: 'miniNfcAlice', diff --git a/src/resourcesTypedef.ts b/src/resourcesTypedef.ts index a4a7fab..e0590f6 100644 --- a/src/resourcesTypedef.ts +++ b/src/resourcesTypedef.ts @@ -87,4 +87,16 @@ export type templateType = { media: MediaDefinition; printableAreas?: PrintableArea[], key: string; +}; + +export type templateTypeV2 = { + version: number; + layout: layoutOrientation; + url: string; + label: string; + /* a reference to the author data */ + author: Authors; + media: MediaDefinition; + printableAreas?: PrintableArea[], + key: string; }; \ No newline at end of file From 5da5925a19df8356de2e5afca98a92389fbc2735 Mon Sep 17 00:00:00 2001 From: Asturur Date: Thu, 2 Jan 2025 00:29:25 +0100 Subject: [PATCH 08/22] fix --- src/resourcesTypedef.ts | 3 +- src/utils/setTemplate.ts | 49 +------- src/utils/setTemplateV2.ts | 204 ++++++++++++++++++++++++++++++++++ src/utils/templateHandling.ts | 53 +++++++++ 4 files changed, 260 insertions(+), 49 deletions(-) create mode 100644 src/utils/setTemplateV2.ts create mode 100644 src/utils/templateHandling.ts diff --git a/src/resourcesTypedef.ts b/src/resourcesTypedef.ts index e0590f6..5d8ee94 100644 --- a/src/resourcesTypedef.ts +++ b/src/resourcesTypedef.ts @@ -1,4 +1,4 @@ -import { type SerializedGroupProps } from 'fabric'; +import { type Group, type SerializedGroupProps } from 'fabric'; import type { Authors } from './templateAuthors'; export type templateLayer = { @@ -90,6 +90,7 @@ export type templateType = { }; export type templateTypeV2 = { + parsed?: Promise; version: number; layout: layoutOrientation; url: string; diff --git a/src/utils/setTemplate.ts b/src/utils/setTemplate.ts index b2e1142..adc227e 100644 --- a/src/utils/setTemplate.ts +++ b/src/utils/setTemplate.ts @@ -3,11 +3,8 @@ import { util, Point, Shadow, - loadSVGFromURL, Group, FabricObject, - Color, - Gradient, type Canvas, type SerializedGroupProps, Rect, @@ -15,6 +12,7 @@ import { import { CardData } from '../contexts/fileDropper'; import type { templateType, templateOverlay } from '../resourcesTypedef'; import { processCustomizations } from './processCustomizations'; +import { extractUniqueColorsFromGroup, parseSvg } from './templateHandling'; FabricObject.ownDefaults.originX = 'center'; FabricObject.ownDefaults.originY = 'center'; @@ -104,51 +102,6 @@ export const scaleImageToOverlayArea = ( mainImage.setCoords(); }; -/** - * extract and normalizes to hex format colors in the objects - * remove opacity from colors and sets it on the objects - * @param group - */ -// TODO: supports gradients and objects with different opacity -const extractUniqueColorsFromGroup = (group: Group): string[] => { - const colors: string[] = []; - group.forEachObject((object) => { - (['stroke', 'fill'] as const).forEach((property) => { - if ( - object[property] && - object[property] !== 'transparent' && - !(object[property] as Gradient<'linear'>).colorStops - ) { - const colorInstance = new Color(object[property] as string); - const hexValue = `#${colorInstance.toHex()}`; - const opacity = colorInstance.getAlpha(); - object[property] = hexValue; - object.set({ - [property]: hexValue, - [`original_${property}`]: hexValue, - }); - object.opacity = opacity; - if (!colors.includes(hexValue)) { - colors.push(hexValue); - } - } - }); - }); - return colors; -}; - -const parseSvg = (url: string): Promise => - loadSVGFromURL(url).then(({ objects }) => { - const nonNullObjects = objects.filter( - (objects) => !!objects, - ) as FabricObject[]; - const group = new Group(nonNullObjects); - extractUniqueColorsFromGroup(group); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return group.toObject(['original_stroke', 'original_fill', 'id']); - }); - const reposition = ( fabricLayer: FabricObject, template: templateType, diff --git a/src/utils/setTemplateV2.ts b/src/utils/setTemplateV2.ts new file mode 100644 index 0000000..270f788 --- /dev/null +++ b/src/utils/setTemplateV2.ts @@ -0,0 +1,204 @@ +import { + FabricImage, + util, + Point, + loadSVGFromURL, + Group, + FabricObject, + type Canvas, + Rect, +} from 'fabric'; +import { CardData } from '../contexts/fileDropper'; +import type { templateType, templateTypeV2 } from '../resourcesTypedef'; +import { extractUniqueColorsFromGroup } from './templateHandling'; + +FabricObject.ownDefaults.originX = 'center'; +FabricObject.ownDefaults.originY = 'center'; +FabricObject.ownDefaults.objectCaching = false; +/* add the ability to parse 'id' to rects */ +Rect.ATTRIBUTE_NAMES = [...Rect.ATTRIBUTE_NAMES, 'id', 'zaparoo-placeholder', 'zaparoo-scale-strategy']; +FabricObject.customProperties = [ + 'zaparoo-placeholder', + 'id', + 'zaparoo-scale-strategy', + 'original_stroke', + 'original_fill' +]; + +// declare the methods for typescript +declare module "fabric" { + // to have the properties recognized on the instance and in the constructor + interface FabricObject { + "original_fill": string; + "original_stroke": string; + "zaparoo-placeholder"?: "main"; + "zaparoo-scale-strategy"?: "fit" | "cover"; + } +} + + +export const scaleImageToOverlayArea = ( + placeholder: FabricObject, + mainImage: FabricImage, +) => { + + // scale the art to the designed area in the template. to fit + // TODO: add option later for fit or cover + const isRotated = mainImage.angle % 180 !== 0; + const isCover = placeholder["zaparoo-scale-strategy"] === "cover"; + const scaler = isCover ? util.findScaleToCover : util.findScaleToFit; + const scaledOverlay = placeholder._getTransformedDimensions(); + + const scale = scaler( + { + width: isRotated ? mainImage.height : mainImage.width, + height: isRotated ? mainImage.width : mainImage.height, + }, + { + width: scaledOverlay.x, + height: scaledOverlay.y, + }, + ); + + if (isCover) { + const clipPath = new Rect({ width: scaledOverlay.x, height: scaledOverlay.y }); + clipPath.absolutePositioned = true; + mainImage.clipPath = clipPath; + } else { + mainImage.clipPath = undefined + } + + mainImage.set({ + scaleX: scale, + scaleY: scale, + }); + + const placeholderMatrix = placeholder.calcTransformMatrix(); + mainImage.setPositionByOrigin( + new Point( + placeholderMatrix[4], + placeholderMatrix[5], + ), + 'center', + 'center', + ); + if (mainImage.clipPath) { + mainImage.clipPath.left = mainImage.left; + mainImage.clipPath.top = mainImage.top; + } + mainImage.setCoords(); +}; + + + +const parseSvg = (url: string): Promise => + loadSVGFromURL(url).then(({ objects }) => { + const nonNullObjects = objects.filter( + (objects) => !!objects, + ) as FabricObject[]; + const group = new Group(nonNullObjects); + extractUniqueColorsFromGroup(group); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return group; + }); + +const reposition = ( + fabricLayer: FabricObject, + template: templateType, +): void => { + if (template.layout === 'horizontal') { + fabricLayer.left = template.media.width / 2; + fabricLayer.top = template.media.height / 2; + } else { + fabricLayer.left = template.media.height / 2; + fabricLayer.top = template.media.width / 2; + } + fabricLayer.setCoords(); +}; + +export const setTemplateV2OnCanvases = async ( + cards: CardData[], + template: templateTypeV2, +): Promise => { + const { layout, url, parsed, media } = template; + + const templateSource = await (parsed ?? (template.parsed = parseSvg(url))) + const fabricLayer = await templateSource.clone(); + + const isHorizontal = layout === 'horizontal'; + const { width, height } = template.media; + const finalWidth = isHorizontal ? width : height; + const finalHeight = isHorizontal ? height : width; + const colors = extractUniqueColorsFromGroup(fabricLayer); + + for (const card of cards) { + const { canvas } = card; + if (!canvas) { + continue; + } + card.template = template; + // resize only if necessary + if (finalHeight !== canvas.height || finalWidth !== canvas.width) { + canvas.setDimensions( + { + width: finalWidth, + height: finalHeight, + }, + { backstoreOnly: true }, + ); + const clipPath = new Rect(template.media); + clipPath.canvas = canvas as Canvas; + canvas.centerObject(clipPath); + canvas.clipPath = clipPath; + + canvas.setDimensions( + { + width: isHorizontal ? 'var(--cell-width)' : 'auto', + height: isHorizontal ? 'auto' : 'var(--cell-width)', + }, + { cssOnly: true }, + ); + } + const mainImage = canvas.getObjects('image')[0] as FabricImage; + + const placeholder = fabricLayer.getObjects().find((obj) => obj["zaparoo-placeholder"] === "main"); + if (!placeholder) { + continue; + } + + // remove strokewidth so the placeholder can clip the image + placeholder.strokeWidth = 0; + placeholder.visible = false; + const templateSize = fabricLayer._getTransformedDimensions(); + // find the layer that olds the image. + + // scale the overlay asset to fit the designed media + const templateScale = util.findScaleToFit({ + width: templateSize.x, + height: templateSize.y, + }, media); + + fabricLayer.set('canvas', canvas); + fabricLayer.scaleX = templateScale; + fabricLayer.scaleY = templateScale; + // set the overlay of the template in the center of the card + reposition(fabricLayer, template); + scaleImageToOverlayArea(placeholder, mainImage); + canvas.overlayImage = fabricLayer; + + const { clipPath } = canvas; + if (clipPath) { + if (template.layout === 'horizontal') { + clipPath.angle = 0; + } else { + clipPath.angle = 90; + } + reposition(clipPath, template); + } + + canvas.requestRenderAll(); + } + // this could returned by the promise right away + return colors; +}; diff --git a/src/utils/templateHandling.ts b/src/utils/templateHandling.ts new file mode 100644 index 0000000..fed9cd1 --- /dev/null +++ b/src/utils/templateHandling.ts @@ -0,0 +1,53 @@ +import { + loadSVGFromURL, + Group, + FabricObject, + Color, + Gradient, + type SerializedGroupProps, +} from 'fabric'; + +/** + * extract and normalizes to hex format colors in the objects + * remove opacity from colors and sets it on the objects + * @param group + */ +// TODO: supports gradients and objects with different opacity +export const extractUniqueColorsFromGroup = (group: Group): string[] => { + const colors: string[] = []; + group.forEachObject((object) => { + (['stroke', 'fill'] as const).forEach((property) => { + if ( + object[property] && + object[property] !== 'transparent' && + !(object[property] as Gradient<'linear'>).colorStops + ) { + const colorInstance = new Color(object[property] as string); + const hexValue = `#${colorInstance.toHex()}`; + const opacity = colorInstance.getAlpha(); + object[property] = hexValue; + object.set({ + [property]: hexValue, + [`original_${property}`]: hexValue, + }); + object.opacity = opacity; + if (!colors.includes(hexValue)) { + colors.push(hexValue); + } + } + }); + }); + return colors; +}; + +export const parseSvg = (url: string): Promise => + loadSVGFromURL(url).then(({ objects }) => { + const nonNullObjects = objects.filter( + (objects) => !!objects, + ) as FabricObject[]; + const group = new Group(nonNullObjects); + extractUniqueColorsFromGroup(group); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return group.toObject(['original_stroke', 'original_fill', 'id']); + }); \ No newline at end of file From acad119c9857db02110fe5c1404e9c813be76b55 Mon Sep 17 00:00:00 2001 From: Asturur Date: Thu, 2 Jan 2025 00:56:45 +0100 Subject: [PATCH 09/22] template working --- src/components/Carousel.tsx | 6 ++++-- src/utils/prepareTemplateCarousel.ts | 12 +++++++++--- src/utils/setTemplateV2.ts | 8 ++++---- src/utils/utils.ts | 4 ++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/components/Carousel.tsx b/src/components/Carousel.tsx index aa5a635..717d4a2 100644 --- a/src/components/Carousel.tsx +++ b/src/components/Carousel.tsx @@ -4,7 +4,7 @@ import { mediaTargetList } from '../printMediaTypes'; import './Carousel.css'; import { useAppDataContext } from '../contexts/appData'; import { templateAuthors } from '../templateAuthors'; -import type { templateType } from '../resourcesTypedef'; +import type { templateType, templateTypeV2 } from '../resourcesTypedef'; import { useState, useLayoutEffect, @@ -32,7 +32,9 @@ const TemplatesCarousel = memo(() => { setItems( availableTemplates.filter( (tData) => - (!!tData.overlay || !!tData.background) && + (!!tData.overlay || + !!tData.background || + (tData as templateTypeV2).version === 2) && !tData.key.includes('blank'), ), ); diff --git a/src/utils/prepareTemplateCarousel.ts b/src/utils/prepareTemplateCarousel.ts index bf4cf1c..0738803 100644 --- a/src/utils/prepareTemplateCarousel.ts +++ b/src/utils/prepareTemplateCarousel.ts @@ -1,9 +1,11 @@ -import { templateType } from "../resourcesTypedef"; +import type { templateType, templateTypeV2 } from "../resourcesTypedef"; import { StaticCanvas, FabricImage } from 'fabric'; import { setTemplateOnCanvases } from "./setTemplate"; +import { setTemplateV2OnCanvases } from "./setTemplateV2"; import { CardData } from "../contexts/fileDropper"; +import { isTemplateV2 } from "./utils"; -export const prepareTemplateCarousel = async (templates: templateType[], img: HTMLImageElement): Promise => { +export const prepareTemplateCarousel = async (templates: (templateType | templateTypeV2)[], img: HTMLImageElement): Promise => { const canvases = []; for (const template of templates) { const canvas = new StaticCanvas(undefined, { @@ -21,7 +23,11 @@ export const prepareTemplateCarousel = async (templates: templateType[], img: HT originalColors: [], key: 'x', } - await setTemplateOnCanvases([card], template) + if (isTemplateV2(template)) { + await setTemplateV2OnCanvases([card], template) + } else { + await setTemplateOnCanvases([card], template) + } canvases.push(canvas.lowerCanvasEl); } return canvases; diff --git a/src/utils/setTemplateV2.ts b/src/utils/setTemplateV2.ts index 270f788..af2570d 100644 --- a/src/utils/setTemplateV2.ts +++ b/src/utils/setTemplateV2.ts @@ -177,11 +177,11 @@ export const setTemplateV2OnCanvases = async ( const templateScale = util.findScaleToFit({ width: templateSize.x, height: templateSize.y, - }, media); - + }, canvas); +console.log({ templateScale, templateSize, media }) fabricLayer.set('canvas', canvas); - fabricLayer.scaleX = templateScale; - fabricLayer.scaleY = templateScale; + // fabricLayer.scaleX = templateScale; + // fabricLayer.scaleY = templateScale; // set the overlay of the template in the center of the card reposition(fabricLayer, template); scaleImageToOverlayArea(placeholder, mainImage); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 2c60dd0..b409d26 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,3 +1,5 @@ +import { templateType, templateTypeV2 } from "../resourcesTypedef"; + export const colorsDiffer = (colorsA: string[], colorsB: string[]): boolean => colorsA.some((color, index) => colorsB[index] !== color); @@ -15,3 +17,5 @@ export const downloadBlob = (blob: Blob, name: string): void => { } export const fromMMtoPxAt72DPI = (mm: number): number => mm / 25.4 * 72; + +export const isTemplateV2 = (t: templateType | templateTypeV2): t is templateTypeV2 => (t as templateTypeV2).version === 2; \ No newline at end of file From 009a86b2b39f936df7184fe69d565fa0393d8248 Mon Sep 17 00:00:00 2001 From: Asturur Date: Thu, 2 Jan 2025 12:22:41 +0100 Subject: [PATCH 10/22] duplicate code so cleaning will feel nice --- src/assets/3by5_steam.svg | 1 - src/components/DataToCanvasReconciler.tsx | 20 +++++++++++++++----- src/hooks/useLabelEditor.ts | 17 +++++++++++++---- src/utils/setTemplateV2.ts | 8 ++++---- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/assets/3by5_steam.svg b/src/assets/3by5_steam.svg index 57ec77d..a94f9d9 100644 --- a/src/assets/3by5_steam.svg +++ b/src/assets/3by5_steam.svg @@ -1,7 +1,6 @@ - diff --git a/src/components/DataToCanvasReconciler.tsx b/src/components/DataToCanvasReconciler.tsx index 49f0e75..de4baad 100644 --- a/src/components/DataToCanvasReconciler.tsx +++ b/src/components/DataToCanvasReconciler.tsx @@ -3,6 +3,8 @@ import { useAppDataContext } from '../contexts/appData'; import { CardData, useFileDropperContext } from '../contexts/fileDropper'; import { setTemplateOnCanvases } from '../utils/setTemplate'; import { updateColors } from '../utils/updateColors'; +import { isTemplateV2 } from '../utils/utils'; +import { setTemplateV2OnCanvases } from '../utils/setTemplateV2'; export const DataToCanvasReconciler = () => { const { cards } = useFileDropperContext(); @@ -20,11 +22,19 @@ export const DataToCanvasReconciler = () => { if (cards.current.length) { setIsIdle(false); const selectedCards = cards.current.filter((card) => card.isSelected); - setTemplateOnCanvases(selectedCards, template).then((colors) => { - setOriginalColors(colors); - setCustomColors(colors); - setIsIdle(true); - }); + if (isTemplateV2(template)) { + setTemplateV2OnCanvases(selectedCards, template).then((colors) => { + setOriginalColors(colors); + setCustomColors(colors); + setIsIdle(true); + }); + } else { + setTemplateOnCanvases(selectedCards, template).then((colors) => { + setOriginalColors(colors); + setCustomColors(colors); + setIsIdle(true); + }); + } } }, [template, setCustomColors, cards, setOriginalColors, setIsIdle]); diff --git a/src/hooks/useLabelEditor.ts b/src/hooks/useLabelEditor.ts index d53a1c8..db99439 100644 --- a/src/hooks/useLabelEditor.ts +++ b/src/hooks/useLabelEditor.ts @@ -6,6 +6,8 @@ import { import { util, FabricImage, type StaticCanvas } from 'fabric'; import { useAppDataContext } from '../contexts/appData'; import { updateColors } from '../utils/updateColors'; +import { isTemplateV2 } from '../utils/utils'; +import { setTemplateV2OnCanvases } from '../utils/setTemplateV2'; type useLabelEditorParams = { padderRef: MutableRefObject; @@ -66,10 +68,17 @@ export const useLabelEditor = ({ card.template = template; card.colors = customColors; card.originalColors = originalColors; - setTemplateOnCanvases([card], template).then(() => { - updateColors([card], customColors, originalColors); - fabricCanvas.requestRenderAll(); - }); + if (isTemplateV2(template)) { + setTemplateV2OnCanvases([card], template).then(() => { + updateColors([card], customColors, originalColors); + fabricCanvas.requestRenderAll(); + }); + } else { + setTemplateOnCanvases([card], template).then(() => { + updateColors([card], customColors, originalColors); + fabricCanvas.requestRenderAll(); + }); + } } // shouldn't retrigger for index change or template change or colors // the data reconciler does that diff --git a/src/utils/setTemplateV2.ts b/src/utils/setTemplateV2.ts index af2570d..c4f4d3a 100644 --- a/src/utils/setTemplateV2.ts +++ b/src/utils/setTemplateV2.ts @@ -127,7 +127,7 @@ export const setTemplateV2OnCanvases = async ( const fabricLayer = await templateSource.clone(); const isHorizontal = layout === 'horizontal'; - const { width, height } = template.media; + const { width, height } = media; const finalWidth = isHorizontal ? width : height; const finalHeight = isHorizontal ? height : width; const colors = extractUniqueColorsFromGroup(fabricLayer); @@ -178,10 +178,10 @@ export const setTemplateV2OnCanvases = async ( width: templateSize.x, height: templateSize.y, }, canvas); -console.log({ templateScale, templateSize, media }) + fabricLayer.set('canvas', canvas); - // fabricLayer.scaleX = templateScale; - // fabricLayer.scaleY = templateScale; + fabricLayer.scaleX = templateScale; + fabricLayer.scaleY = templateScale; // set the overlay of the template in the center of the card reposition(fabricLayer, template); scaleImageToOverlayArea(placeholder, mainImage); From 2a3667d084ae94a64ddf26c649be4d1f9ee7585b Mon Sep 17 00:00:00 2001 From: Asturur Date: Thu, 2 Jan 2025 13:05:23 +0100 Subject: [PATCH 11/22] more pain --- src/contexts/fileDropper.ts | 1 + src/extensions/fabricToPdfKit.ts | 3 +++ src/utils/setTemplateV2.ts | 19 +++++++++++++++---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/contexts/fileDropper.ts b/src/contexts/fileDropper.ts index bc92941..f3d55c9 100644 --- a/src/contexts/fileDropper.ts +++ b/src/contexts/fileDropper.ts @@ -4,6 +4,7 @@ import type { StaticCanvas } from 'fabric'; import type { templateType } from '../resourcesTypedef'; export type CardData = { + /* the source of the main image */ file: File | HTMLImageElement, canvas?: StaticCanvas; template?: templateType; diff --git a/src/extensions/fabricToPdfKit.ts b/src/extensions/fabricToPdfKit.ts index d47a4c4..21fdedc 100644 --- a/src/extensions/fabricToPdfKit.ts +++ b/src/extensions/fabricToPdfKit.ts @@ -220,6 +220,9 @@ const addGroupToPdf = async ( transformPdf(group, pdfDoc); const objs = group.getObjects(); for ( const object of objs) { + if (!object.visible) { + continue; + } if (object instanceof Path) { addPathToPdf(object, pdfDoc); } diff --git a/src/utils/setTemplateV2.ts b/src/utils/setTemplateV2.ts index c4f4d3a..b1676ad 100644 --- a/src/utils/setTemplateV2.ts +++ b/src/utils/setTemplateV2.ts @@ -7,6 +7,7 @@ import { FabricObject, type Canvas, Rect, + Textbox, } from 'fabric'; import { CardData } from '../contexts/fileDropper'; import type { templateType, templateTypeV2 } from '../resourcesTypedef'; @@ -25,6 +26,12 @@ FabricObject.customProperties = [ 'original_fill' ]; +FabricImage.customProperties = [ + 'resourceType', + 'original_stroke', + 'original_fill' +]; + // declare the methods for typescript declare module "fabric" { // to have the properties recognized on the instance and in the constructor @@ -34,6 +41,10 @@ declare module "fabric" { "zaparoo-placeholder"?: "main"; "zaparoo-scale-strategy"?: "fit" | "cover"; } + + interface FabricImage { + "resourceType"?: "main" | "screenshot" | "logo"; + } } @@ -123,14 +134,13 @@ export const setTemplateV2OnCanvases = async ( ): Promise => { const { layout, url, parsed, media } = template; - const templateSource = await (parsed ?? (template.parsed = parseSvg(url))) - const fabricLayer = await templateSource.clone(); - + const templateSource = await (parsed ?? (template.parsed = parseSvg(url))); + // fixme: avoid parsing colors more than once. + const colors = extractUniqueColorsFromGroup(templateSource); const isHorizontal = layout === 'horizontal'; const { width, height } = media; const finalWidth = isHorizontal ? width : height; const finalHeight = isHorizontal ? height : width; - const colors = extractUniqueColorsFromGroup(fabricLayer); for (const card of cards) { const { canvas } = card; @@ -161,6 +171,7 @@ export const setTemplateV2OnCanvases = async ( ); } const mainImage = canvas.getObjects('image')[0] as FabricImage; + const fabricLayer = await templateSource.clone(); const placeholder = fabricLayer.getObjects().find((obj) => obj["zaparoo-placeholder"] === "main"); if (!placeholder) { From 8e20068a4a85da63debba7547368f453a23b41e7 Mon Sep 17 00:00:00 2001 From: Asturur Date: Fri, 3 Jan 2025 10:02:42 +0100 Subject: [PATCH 12/22] more code --- src/hooks/useLabelEditor.ts | 2 +- src/utils/prepareTemplateCarousel.ts | 2 +- src/utils/setTemplateV2.ts | 56 +++++++++++++++------------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/hooks/useLabelEditor.ts b/src/hooks/useLabelEditor.ts index db99439..a4cc0b1 100644 --- a/src/hooks/useLabelEditor.ts +++ b/src/hooks/useLabelEditor.ts @@ -39,7 +39,7 @@ export const useLabelEditor = ({ } setImageReady(false); imagePromise.then((image) => { - const fabricImage = new FabricImage(image); + const fabricImage = new FabricImage(image, { resourceType: "main" }); // @ts-expect-error no originalFile fabricImage.originalFile = file; const scale = util.findScaleToCover(fabricImage, fabricCanvas); diff --git a/src/utils/prepareTemplateCarousel.ts b/src/utils/prepareTemplateCarousel.ts index 0738803..53ad147 100644 --- a/src/utils/prepareTemplateCarousel.ts +++ b/src/utils/prepareTemplateCarousel.ts @@ -13,7 +13,7 @@ export const prepareTemplateCarousel = async (templates: (templateType | templat enableRetinaScaling: false, backgroundColor: 'white', }); - canvas.add((new FabricImage(img))) + canvas.add((new FabricImage(img, { resourceType: "main" }))) const card: CardData = { file: img, canvas, diff --git a/src/utils/setTemplateV2.ts b/src/utils/setTemplateV2.ts index b1676ad..d1bcd28 100644 --- a/src/utils/setTemplateV2.ts +++ b/src/utils/setTemplateV2.ts @@ -84,15 +84,9 @@ export const scaleImageToOverlayArea = ( scaleY: scale, }); - const placeholderMatrix = placeholder.calcTransformMatrix(); - mainImage.setPositionByOrigin( - new Point( - placeholderMatrix[4], - placeholderMatrix[5], - ), - 'center', - 'center', - ); + mainImage.top = placeholder.top; + mainImage.left = placeholder.left; + if (mainImage.clipPath) { mainImage.clipPath.left = mainImage.left; mainImage.clipPath.top = mainImage.top; @@ -170,33 +164,45 @@ export const setTemplateV2OnCanvases = async ( { cssOnly: true }, ); } - const mainImage = canvas.getObjects('image')[0] as FabricImage; - const fabricLayer = await templateSource.clone(); + // save a reference to the original image + const mainImage = canvas.getObjects('image').find( + (fabricImage) => (fabricImage as FabricImage).resourceType === 'main' + ) as FabricImage; - const placeholder = fabricLayer.getObjects().find((obj) => obj["zaparoo-placeholder"] === "main"); - if (!placeholder) { - continue; - } - - // remove strokewidth so the placeholder can clip the image - placeholder.strokeWidth = 0; - placeholder.visible = false; + // copy the template for this card + const fabricLayer = await templateSource.clone(); + // find out how bit it is naturally const templateSize = fabricLayer._getTransformedDimensions(); - // find the layer that olds the image. - - // scale the overlay asset to fit the designed media + // scale the overlay asset to fit the designed media ( the card ) const templateScale = util.findScaleToFit({ width: templateSize.x, height: templateSize.y, }, canvas); - fabricLayer.set('canvas', canvas); fabricLayer.scaleX = templateScale; fabricLayer.scaleY = templateScale; + // set the overlay of the template in the center of the card reposition(fabricLayer, template); - scaleImageToOverlayArea(placeholder, mainImage); - canvas.overlayImage = fabricLayer; + + // remove the previous template from the canvas if any. + canvas.remove(...canvas.getObjects()); + // add the template to the canvas + canvas.add(...templateSource.removeAll()); + // find the layer that olds the image. + const placeholder = canvas.getObjects().find((obj) => obj["zaparoo-placeholder"] === "main"); + if (placeholder) { + // remove strokewidth so the placeholder can clip the image + placeholder.strokeWidth = 0; + // the placeholder stays with us but we don't want to see it + placeholder.visible = false; + // add the image on the placeholder + if (mainImage) { + const index = canvas.getObjects().indexOf(placeholder); + canvas.insertAt(index, mainImage); + scaleImageToOverlayArea(placeholder, mainImage); + } + } const { clipPath } = canvas; if (clipPath) { From b7c87f3f5b7a52b4a6275a4ffb09bddb54d76a20 Mon Sep 17 00:00:00 2001 From: Asturur Date: Fri, 3 Jan 2025 13:11:49 +0100 Subject: [PATCH 13/22] more code --- src/components/DataToCanvasReconciler.tsx | 18 ++++++++++++++---- src/hooks/useLabelEditor.ts | 1 + src/utils/setTemplate.ts | 1 + src/utils/setTemplateV2.ts | 4 +--- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/DataToCanvasReconciler.tsx b/src/components/DataToCanvasReconciler.tsx index de4baad..9da2bd3 100644 --- a/src/components/DataToCanvasReconciler.tsx +++ b/src/components/DataToCanvasReconciler.tsx @@ -48,12 +48,22 @@ export const DataToCanvasReconciler = () => { (card): card is Required => !!card.isSelected && !!card.canvas, ); setIsIdle(false); - setTemplateOnCanvases(selectedCardsWithDifferentTemplate, template).then( - () => { + if (isTemplateV2(template)) { + setTemplateV2OnCanvases( + selectedCardsWithDifferentTemplate, + template, + ).then(() => { updateColors(selectedCards, customColors, originalColors); setIsIdle(true); - }, - ); + }); + } else { + setTemplateOnCanvases(selectedCardsWithDifferentTemplate, template).then( + () => { + updateColors(selectedCards, customColors, originalColors); + setIsIdle(true); + }, + ); + } }, [cards, customColors, originalColors, setIsIdle, template]); return null; diff --git a/src/hooks/useLabelEditor.ts b/src/hooks/useLabelEditor.ts index a4cc0b1..9f3164e 100644 --- a/src/hooks/useLabelEditor.ts +++ b/src/hooks/useLabelEditor.ts @@ -69,6 +69,7 @@ export const useLabelEditor = ({ card.colors = customColors; card.originalColors = originalColors; if (isTemplateV2(template)) { + debugger; setTemplateV2OnCanvases([card], template).then(() => { updateColors([card], customColors, originalColors); fabricCanvas.requestRenderAll(); diff --git a/src/utils/setTemplate.ts b/src/utils/setTemplate.ts index adc227e..673dfe7 100644 --- a/src/utils/setTemplate.ts +++ b/src/utils/setTemplate.ts @@ -122,6 +122,7 @@ export const setTemplateOnCanvases = async ( cards: CardData[], template: templateType, ): Promise => { + console.trace(cards) const { overlay, background, shadow, layout } = template || {}; const [overlayImageSource, backgroundImageSource] = await Promise.all([ overlay && diff --git a/src/utils/setTemplateV2.ts b/src/utils/setTemplateV2.ts index d1bcd28..412305c 100644 --- a/src/utils/setTemplateV2.ts +++ b/src/utils/setTemplateV2.ts @@ -1,13 +1,11 @@ import { FabricImage, util, - Point, loadSVGFromURL, Group, FabricObject, type Canvas, Rect, - Textbox, } from 'fabric'; import { CardData } from '../contexts/fileDropper'; import type { templateType, templateTypeV2 } from '../resourcesTypedef'; @@ -188,7 +186,7 @@ export const setTemplateV2OnCanvases = async ( // remove the previous template from the canvas if any. canvas.remove(...canvas.getObjects()); // add the template to the canvas - canvas.add(...templateSource.removeAll()); + canvas.add(...fabricLayer.removeAll()); // find the layer that olds the image. const placeholder = canvas.getObjects().find((obj) => obj["zaparoo-placeholder"] === "main"); if (placeholder) { From 7f25369bb145405ce74ee24db55c09762cfd0fd1 Mon Sep 17 00:00:00 2001 From: Asturur Date: Mon, 6 Jan 2025 23:26:59 +0100 Subject: [PATCH 14/22] ok print --- src/extensions/fabricToPdfKit.ts | 45 ++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/extensions/fabricToPdfKit.ts b/src/extensions/fabricToPdfKit.ts index 21fdedc..c9b3484 100644 --- a/src/extensions/fabricToPdfKit.ts +++ b/src/extensions/fabricToPdfKit.ts @@ -76,14 +76,16 @@ const addRectToPdf = ( pdfDoc.save(); transformPdf(rect, pdfDoc); - if (rect.rx || rect.ry) { - console.warn('Missing code for Rect rounded corners') - } + const hasRounds = rect.rx || rect.ry; const paintStroke = () => { if (rect.stroke && rect.stroke !== 'transparent') { const stroke = toPdfColor(rect.stroke, pdfDoc, rect); pdfDoc.lineWidth(rect.strokeWidth); - pdfDoc.rect(-rect.width / 2, -rect.height / 2, rect.width, rect.height); + if (hasRounds) { + pdfDoc.roundedRect(-rect.width / 2, -rect.height / 2, rect.width, rect.height, rect.rx) + } else { + pdfDoc.rect(-rect.width / 2, -rect.height / 2, rect.width, rect.height,); + } // pdfDoc.strokeOpacity(this.opacity); if ( rect.strokeDashArray && @@ -102,7 +104,11 @@ const addRectToPdf = ( if (rect.fill && rect.fill !== 'transparent') { const fill = toPdfColor(rect.fill, pdfDoc, rect); // pdfDoc.fillOpacity(this.opacity); - pdfDoc.rect(-rect.width / 2, -rect.height / 2, rect.width, rect.height); + if (hasRounds) { + pdfDoc.roundedRect(-rect.width / 2, -rect.height / 2, rect.width, rect.height, rect.rx) + } else { + pdfDoc.rect(-rect.width / 2, -rect.height / 2, rect.width, rect.height,); + } pdfDoc.fill(fill); } }; @@ -162,10 +168,7 @@ const transformPdf = (fabricObject: FabricObject, pdfDoc: any) => { const handleAbsoluteClipPath = (clipPath: FabricObject, pdfDoc: any) => { transformPdf(clipPath as FabricObject, pdfDoc); if (clipPath instanceof Rect) { - if (clipPath.rx || clipPath.ry) { - console.warn('Missing code for rounded corners rect clipPath') - } - pdfDoc.roundedRect(-clipPath.width / 2, -clipPath.height / 2, clipPath.width, clipPath.height, 0).clip(); + pdfDoc.roundedRect(-clipPath.width / 2, -clipPath.height / 2, clipPath.width, clipPath.height, clipPath.rx).clip(); } const matrix = clipPath.calcOwnMatrix(); pdfDoc.transform(...util.invertTransform(matrix)); @@ -212,13 +215,7 @@ const addImageToPdf = async ( pdfDoc.restore(); }; -const addGroupToPdf = async ( - group: Group, - pdfDoc: any, -) => { - pdfDoc.save(); - transformPdf(group, pdfDoc); - const objs = group.getObjects(); +const addObjectsToPdf = async (objs: FabricObject[], pdfDoc: any) => { for ( const object of objs) { if (!object.visible) { continue; @@ -233,6 +230,16 @@ const addGroupToPdf = async ( await addImageToPdf(object, pdfDoc); } } +} + +const addGroupToPdf = async ( + group: Group, + pdfDoc: any, +) => { + pdfDoc.save(); + transformPdf(group, pdfDoc); + const objs = group.getObjects(); + await addObjectsToPdf(objs, pdfDoc); pdfDoc.restore(); }; @@ -278,15 +285,13 @@ export const addCanvasToPdfPage = async ( if (!template.background?.hidePrint) { if (canvas.backgroundImage instanceof Group) { await addGroupToPdf(canvas.backgroundImage, pdfDoc); - } else { + } else if (canvas.backgroundImage) { // add it as an image. // no usecase for this yet console.warn('Missing code to add images to pdf from background') } } - const mainImage = canvas.getObjects('image')[0] as FabricImage; - await addImageToPdf(mainImage, pdfDoc); - + await addObjectsToPdf(canvas.getObjects(), pdfDoc); if (canvas.overlayImage instanceof Group) { await addGroupToPdf(canvas.overlayImage, pdfDoc); } else { From 272843a1cd884b82f4121801d37acbbf503b81f7 Mon Sep 17 00:00:00 2001 From: Asturur Date: Mon, 6 Jan 2025 23:51:37 +0100 Subject: [PATCH 15/22] you wouldnt say but i fixed a bunch of bugs --- src/assets/tapto_vertical.svg | 107 +++++++--------------------------- src/cardsTemplates.ts | 18 +----- src/hooks/useLabelEditor.ts | 1 - src/utils/setTemplateV2.ts | 2 + 4 files changed, 26 insertions(+), 102 deletions(-) diff --git a/src/assets/tapto_vertical.svg b/src/assets/tapto_vertical.svg index 35b65d0..d2d6a5c 100644 --- a/src/assets/tapto_vertical.svg +++ b/src/assets/tapto_vertical.svg @@ -1,86 +1,23 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/cardsTemplates.ts b/src/cardsTemplates.ts index 78f864a..27c79c0 100644 --- a/src/cardsTemplates.ts +++ b/src/cardsTemplates.ts @@ -171,24 +171,10 @@ export const templates: Record = { key: 'tapto2', }, tapto3: { + version: 2, layout: 'vertical', - overlay: { - layerWidth: 619, - layerHeight: 994, - url: tapToVertical, - width: 1 - (37 * 2) / 619, - height: 1 - (37 + 144) / 994, - x: 37 / 619, - y: 37 / 994, - isSvg: true, - }, shadow: '0px 10px 20px rgba(0, 0, 0, 0.3)', - background: { - layerWidth: 619, - layerHeight: 994, - url: tapToBgV, - isSvg: true, - }, + url: tapToVertical, label: 'Tap-to V', author: Authors.tim, media: NFCCCsizeCard, diff --git a/src/hooks/useLabelEditor.ts b/src/hooks/useLabelEditor.ts index 9f3164e..a4cc0b1 100644 --- a/src/hooks/useLabelEditor.ts +++ b/src/hooks/useLabelEditor.ts @@ -69,7 +69,6 @@ export const useLabelEditor = ({ card.colors = customColors; card.originalColors = originalColors; if (isTemplateV2(template)) { - debugger; setTemplateV2OnCanvases([card], template).then(() => { updateColors([card], customColors, originalColors); fabricCanvas.requestRenderAll(); diff --git a/src/utils/setTemplateV2.ts b/src/utils/setTemplateV2.ts index 412305c..fca506f 100644 --- a/src/utils/setTemplateV2.ts +++ b/src/utils/setTemplateV2.ts @@ -185,6 +185,8 @@ export const setTemplateV2OnCanvases = async ( // remove the previous template from the canvas if any. canvas.remove(...canvas.getObjects()); + canvas.backgroundImage = undefined; + canvas.overlayImage = undefined; // add the template to the canvas canvas.add(...fabricLayer.removeAll()); // find the layer that olds the image. From 27efa412b5b9a419c0dabb12875a4556a2d69413 Mon Sep 17 00:00:00 2001 From: Asturur Date: Tue, 7 Jan 2025 08:02:47 +0100 Subject: [PATCH 16/22] works --- src/utils/setTemplate.ts | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/utils/setTemplate.ts b/src/utils/setTemplate.ts index 673dfe7..3af1b36 100644 --- a/src/utils/setTemplate.ts +++ b/src/utils/setTemplate.ts @@ -18,7 +18,35 @@ FabricObject.ownDefaults.originX = 'center'; FabricObject.ownDefaults.originY = 'center'; FabricObject.ownDefaults.objectCaching = false; /* add the ability to parse 'id' to rects */ -Rect.ATTRIBUTE_NAMES = [...Rect.ATTRIBUTE_NAMES, 'id']; +Rect.ATTRIBUTE_NAMES = [...Rect.ATTRIBUTE_NAMES, 'id', 'zaparoo-placeholder', 'zaparoo-scale-strategy']; +FabricObject.customProperties = [ + 'zaparoo-placeholder', + 'id', + 'zaparoo-scale-strategy', + 'original_stroke', + 'original_fill' +]; + +FabricImage.customProperties = [ + 'resourceType', + 'original_stroke', + 'original_fill' +]; + +// declare the methods for typescript +declare module "fabric" { + // to have the properties recognized on the instance and in the constructor + interface FabricObject { + "original_fill": string; + "original_stroke": string; + "zaparoo-placeholder"?: "main"; + "zaparoo-scale-strategy"?: "fit" | "cover"; + } + + interface FabricImage { + "resourceType"?: "main" | "screenshot" | "logo"; + } +} export const scaleImageToOverlayArea = ( template: templateType, @@ -184,7 +212,11 @@ export const setTemplateOnCanvases = async ( { cssOnly: true }, ); } - const mainImage = canvas.getObjects('image')[0] as FabricImage; + const mainImage = canvas.getObjects('image').find( + (fabricImage) => (fabricImage as FabricImage).resourceType === 'main' + ) as FabricImage; + canvas.remove(...canvas.getObjects()); + canvas.add(mainImage); if (mainImage && shadow) { mainImage.shadow = new Shadow({ ...Shadow.parseShadow(shadow), nonScaling: true }); } From f89eaf5765a456bc982e9b732107c56d788a6b63 Mon Sep 17 00:00:00 2001 From: Asturur Date: Tue, 7 Jan 2025 08:12:33 +0100 Subject: [PATCH 17/22] fix color changes --- src/utils/updateColors.ts | 63 +++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/src/utils/updateColors.ts b/src/utils/updateColors.ts index 2356e67..5e718a5 100644 --- a/src/utils/updateColors.ts +++ b/src/utils/updateColors.ts @@ -1,6 +1,26 @@ -import { Group } from 'fabric'; +import { FabricObject, Group } from 'fabric'; import { type CardData } from '../contexts/fileDropper'; + +const colorChanger = (object: FabricObject, colors: string[], originalColors: string[]) => { + (['stroke', 'fill'] as const).forEach((property) => { + if (object[property]) { + const objectOriginalColor = object[ + `original_${property}` as keyof typeof object + ] as string; + const originalIndex = originalColors.indexOf(objectOriginalColor); + if ( + originalIndex > -1 && + colors[originalIndex] !== object[property] + ) { + object.set({ + [property]: colors[originalIndex], + }); + } + } + }); +} + export const updateColors = ( cards: CardData[], colors: string[], @@ -14,47 +34,20 @@ export const updateColors = ( card.colors = colors; card.originalColors = originalColors; const { overlayImage, backgroundImage } = canvas; + const objects = canvas.getObjects(); + objects.forEach((object) => { + colorChanger(object, colors, originalColors); + }) if (overlayImage instanceof Group) { overlayImage.forEachObject((object) => { - (['stroke', 'fill'] as const).forEach((property) => { - if (object[property]) { - const objectOriginalColor = object[ - `original_${property}` as keyof typeof object - ] as string; - const originalIndex = originalColors.indexOf(objectOriginalColor); - if ( - originalIndex > -1 && - colors[originalIndex] !== object[property] - ) { - object.set({ - [property]: colors[originalIndex], - }); - canvas.requestRenderAll(); - } - } - }); + colorChanger(object, colors, originalColors); }); } if (backgroundImage instanceof Group) { backgroundImage.forEachObject((object) => { - (['stroke', 'fill'] as const).forEach((property) => { - if (object[property]) { - const objectOriginalColor = object[ - `original_${property}` as keyof typeof object - ] as string; - const originalIndex = originalColors.indexOf(objectOriginalColor); - if ( - originalIndex > -1 && - colors[originalIndex] !== object[property] - ) { - object.set({ - [property]: colors[originalIndex], - }); - canvas.requestRenderAll(); - } - } - }); + colorChanger(object, colors, originalColors); }); } + canvas.requestRenderAll(); }); }; From 90d47bf26ec37369cc4dd98e0adad3d596fc881b Mon Sep 17 00:00:00 2001 From: Asturur Date: Tue, 7 Jan 2025 09:49:44 +0100 Subject: [PATCH 18/22] test --- TEMPLATES.md | 47 +++++++++++++++++++++++++++++++++++- public/template_example.png | Bin 0 -> 55403 bytes 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 public/template_example.png diff --git a/TEMPLATES.md b/TEMPLATES.md index 4c81524..1f0ec2a 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -58,9 +58,54 @@ The resulting SVG initial tag would look like something like this: The SVG and so your template will be a stack of layers. There will be a spot for one or more images between the stack. -Let's take for example our original basic template, the one with the old logo: +Let's take for example a simple template for the mini nfc cards. +In the example i m going to omit the long path for the purpose of formatting and reading. So the xml you find here is a showcase and doesn't display correct graphics. ```xml + + + + + + + + + + + + + + +``` + +As you can see there are 10 elements in this SVG, some of them are in a group that is not important to us, is how the design application packaged it. +The elements are drawn top to bottom, meaning that the top element is drawn first, and so is behind the others. If you are not used to svgs it is the opposite of what you would think. Top elements are at the bottom. +In this case the first element is a special one: + +```xml + ``` + +It has a special attribute: `zaparoo-placeholder` of value `main`, that means that this is the object that is going to contain your card image and define its exentension and position. +x, y, width and height define where this placholder is. +This rect has also `zaparoo-fill-strategy="fit"` meaning that the image will be scaled to fit the rectangle. + +More attributes with different values are going to be implmented in the future ( image in a circle or custom path for example, more images, text placeholders ) + +The visual attributes of the placeholder are purely for visual reference and won't be displayed in the designer. + +The template file looks like this: + +![template example](/template_example.png) diff --git a/public/template_example.png b/public/template_example.png new file mode 100644 index 0000000000000000000000000000000000000000..f6953b1d72fe0f551fa33d63e28ff886efec248c GIT binary patch literal 55403 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%0AEQIz2Fg<-AJBYznJuI&GD1q6$|c!#Y5Oy zHtbm2`lS)6egF1X6BNay;y^DAo=WY7js1Vx(>2+@$Yyc}ByErsk9F>NS6Sn}ui zZ~n~|$<_9BPWu>8rk$oAmJSaM54hAeV$Rbyg^mDgD6z35zwwrNnEWf{E+}OA&AvO# zcB!u#TUo;x^w~;ey9iJ+q@xT^#!z^nK(t}6+iy;LiC_Z|xl50k%+;O03DeZbC<&9n zn6y|#&R9RG$HzD-CHRdy%P~xUvs*n-i`sX14$BcFcG3}>jn~z!@oB7!Wxc7B(PKkn zD8Y@h&aL0S!L)^&)CQ=6zt=+dht_Nk3YgUKgghOf2rHIs^1lCv4+E z$DeQ*L-${T!94>$lz;Uk@7&*cy!X~9 z)y7J;yEN`jTLu6<%H7=|Z6}X(k0vp6f_9vk z6Q(V=ttI@T?%%`R<+EQ{dnM7#|2_*H2kdIMliX8^GO>r4BM0W5>J zKjZa#``h@Uu>qIHt&aWXd>go4-mFcqe)r~g(L!Pz=>Lh=kvaTpZ*ZVf4ehCa{x2qm z;KYIbDpRW67k>4T*sqd}`@CA!qdl57D);*+Lj+$ceKSXTr)d1Wx0C) z*AVaF2EZ)3)dOPw=P!FUqgX-@puZ0jc+P&6boiLyuK{gBodhG=vmN$}2O=n{;b;I5 q&i7)!X1bZr8xnE3?f?53u0H8*sKr$$!sSii-z6jS^Tp>}@Bbg@%1e9z literal 0 HcmV?d00001 From 8407dd69f35eeb267947993991c3f714f9176067 Mon Sep 17 00:00:00 2001 From: Asturur Date: Tue, 7 Jan 2025 09:52:02 +0100 Subject: [PATCH 19/22] fix --- TEMPLATES.md | 2 +- {public => docs}/template_example.png | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename {public => docs}/template_example.png (100%) diff --git a/TEMPLATES.md b/TEMPLATES.md index 1f0ec2a..ad656b4 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -108,4 +108,4 @@ The visual attributes of the placeholder are purely for visual reference and won The template file looks like this: -![template example](/template_example.png) +![template example](/docs/template_example.png) diff --git a/public/template_example.png b/docs/template_example.png similarity index 100% rename from public/template_example.png rename to docs/template_example.png From 6a6ae49cf12f0c6a4c66898b909c44da14f3f649 Mon Sep 17 00:00:00 2001 From: Asturur Date: Tue, 7 Jan 2025 09:57:10 +0100 Subject: [PATCH 20/22] fix color parsing --- src/utils/setTemplateV2.ts | 11 +++++++---- src/utils/templateHandling.ts | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/utils/setTemplateV2.ts b/src/utils/setTemplateV2.ts index fca506f..5f3f121 100644 --- a/src/utils/setTemplateV2.ts +++ b/src/utils/setTemplateV2.ts @@ -127,6 +127,13 @@ export const setTemplateV2OnCanvases = async ( const { layout, url, parsed, media } = template; const templateSource = await (parsed ?? (template.parsed = parseSvg(url))); + const placeholder = templateSource.getObjects().find((obj) => obj["zaparoo-placeholder"] === "main"); + if (placeholder) { + // remove strokewidth so the placeholder can clip the image + placeholder.strokeWidth = 0; + // the placeholder stays with us but we don't want to see it + placeholder.visible = false; + } // fixme: avoid parsing colors more than once. const colors = extractUniqueColorsFromGroup(templateSource); const isHorizontal = layout === 'horizontal'; @@ -192,10 +199,6 @@ export const setTemplateV2OnCanvases = async ( // find the layer that olds the image. const placeholder = canvas.getObjects().find((obj) => obj["zaparoo-placeholder"] === "main"); if (placeholder) { - // remove strokewidth so the placeholder can clip the image - placeholder.strokeWidth = 0; - // the placeholder stays with us but we don't want to see it - placeholder.visible = false; // add the image on the placeholder if (mainImage) { const index = canvas.getObjects().indexOf(placeholder); diff --git a/src/utils/templateHandling.ts b/src/utils/templateHandling.ts index fed9cd1..9a3ba6c 100644 --- a/src/utils/templateHandling.ts +++ b/src/utils/templateHandling.ts @@ -16,6 +16,9 @@ import { export const extractUniqueColorsFromGroup = (group: Group): string[] => { const colors: string[] = []; group.forEachObject((object) => { + if (!object.visible) { + return; + } (['stroke', 'fill'] as const).forEach((property) => { if ( object[property] && From 9b26f4a40ce2e25cc84b87b6d673137d0f1b2812 Mon Sep 17 00:00:00 2001 From: Asturur Date: Sun, 12 Jan 2025 15:56:32 +0100 Subject: [PATCH 21/22] more instructions --- TEMPLATES.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/TEMPLATES.md b/TEMPLATES.md index ad656b4..88c1abc 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -109,3 +109,57 @@ The visual attributes of the placeholder are purely for visual reference and won The template file looks like this: ![template example](/docs/template_example.png) + +But When using it the template red dashed dot will not be visible. + +Once the template is ready you need to add it to the template list. + +```ts + minNfc: { + version: 2, // specify version as 2, fixed + layout: 'vertical', // vertical or horzintal + label: 'Steam 3by5cm', // a name of your liking + url: mininfcAlice, // the url of the file you create, more on this below. + author: Authors.alice, // Reference YOU as the author. more on this below + media: miniNfcCard, // reference the media size, create a new one if necessary + key: 'miniNfcAlice', // a unique string for some reason i really forgot + }, +``` + +This javascript object goes in this file here: + +src/cardsTemplates.ts + +And it looks like this: + +```ts +import mininfcAlice from './assets/3by5_steam.svg'; // where your SVG is placed +import { Authors } from './templateAuthors'; // where the authors are defined + +import { miniNfcCard } from './printMediaTypes'; // where the media types are defined + +export const templates: Record = { + blankH: { + layout: 'horizontal', + label: 'Blank H cover', + author: Authors.andrea, + media: NFCCCsizeCard, + key: 'blankH', + }, + // ... many templates after + miniNfcAlice: { + version: 2, + layout: 'vertical', + label: 'Steam 3by5cm', + url: mininfcAlice, + author: Authors.alice, + media: miniNfcCard, + key: 'miniNfcAlice', + }, +} as const; + +export const defaultTemplateKey = 'hucard'; +export const defaultTemplate = templates[defaultTemplateKey]; +``` + +If you want to add your templates and you are willing to do the work yourself, please read this guide and if it doesn't work ask help in discord or with an issue, explaining what part is unclear or where you are stuck at. From e6f16287f6068f3fd7b8968dac2b070d02f991fa Mon Sep 17 00:00:00 2001 From: Asturur Date: Sun, 12 Jan 2025 16:16:34 +0100 Subject: [PATCH 22/22] maybe i nailed it --- src/utils/preparePdfKit.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/preparePdfKit.ts b/src/utils/preparePdfKit.ts index 976327c..d44bfb1 100644 --- a/src/utils/preparePdfKit.ts +++ b/src/utils/preparePdfKit.ts @@ -52,8 +52,8 @@ export const preparePdf = async ( // take first card and compare with paper size, then try to guess best way to fit const firstCard = cards[0]; const firstCardTemplate = firstCard.template!; - let widthInPt = fromPixToPoint(firstCardTemplate.layout === 'horizontal' ? firstCardTemplate.media.width : firstCardTemplate.media.height); - let heightInPt = fromPixToPoint(firstCardTemplate.layout === 'horizontal' ? firstCardTemplate.media.height : firstCardTemplate.media.width); + let widthInPt = fromPixToPoint(firstCardTemplate.media.width); + let heightInPt = fromPixToPoint(firstCardTemplate.media.height); const availPaperWidth = paperWidthInPt - leftMarginInPt - rightMarginInPt; const availPaperHeight = paperHeightInPt - topMarginInPt - bottomMarginInPt; @@ -61,7 +61,7 @@ export const preparePdf = async ( // the template media is counted always as horizontal/landscape. // isRotated means that it fits more prints if rotated as vertical // the paper template instead is defined as you would insert in the printer. - let isRotated = false; + let neutralTemplate: 'horizontal' | 'vertical' = 'horizontal'; if (!_tmpColumns && !_tmpRows) { // naively divide width by width and height by height, find margins and spacing. const possibleRows = Math.floor(availPaperHeight / heightInPt); @@ -80,7 +80,7 @@ export const preparePdf = async ( if (Math.abs(marginsW - marginsH) > Math.abs(marginsWR - marginsHR)) { rows = possibleRowsRotated; columns = possibleColumsRotated; - isRotated = true; + neutralTemplate = 'vertical'; } else { rows = possibleRows; columns = possibleColums; @@ -90,13 +90,13 @@ export const preparePdf = async ( rows = possibleRows; columns = possibleColums; } else { - isRotated = true; + neutralTemplate = 'vertical'; rows = possibleRowsRotated; columns = possibleColumsRotated; } } - if (isRotated) { + if (neutralTemplate === 'vertical') { [widthInPt, heightInPt] = [heightInPt, widthInPt]; } @@ -169,7 +169,7 @@ export const preparePdf = async ( } } - const needsRotation = isRotated || cards[index].template!.layout !== firstCardTemplate.layout; + const needsRotation = cards[index].template!.layout !== neutralTemplate; await addCanvasToPdfPage( canvas!,