diff --git a/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx b/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx index 8e2901db..f0f26f72 100644 --- a/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx @@ -4,6 +4,7 @@ import { SidebarPanelPopoverFormContainer } from '#app/components/layout/popover import { type IAssetImage } from '#app/models/asset/image/image.server' import { getAssetImgSrc } from '#app/models/asset/image/utils' import { AssetImageUpdateFit } from '#app/routes/resources+/api.v1+/asset.image.update.fit' +import { AssetImageUpdateHideOnDraw } from '#app/routes/resources+/api.v1+/asset.image.update.hide-on-draw' import { AssetUpdateName } from '#app/routes/resources+/api.v1+/asset.update.name' import { SidebarPanelRowValuesContainer } from '..' import { PanelEntityPopover } from './dashboard-entity-panel.popover' @@ -31,6 +32,10 @@ const EntityPopover = memo(({ image }: EntityProps) => { Fit + + Hide on Draw + + ) }) diff --git a/app/definitions/artwork-generator.ts b/app/definitions/artwork-generator.ts index a0eff123..83438129 100644 --- a/app/definitions/artwork-generator.ts +++ b/app/definitions/artwork-generator.ts @@ -1,8 +1,8 @@ import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IArtworkBranch } from '#app/models/artwork-branch/artwork-branch.server' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IAssetGenerationByType } from '#app/models/asset/asset.generation.server' import { type IAssetByType } from '#app/models/asset/asset.server' -import { type IAssetImageGeneration } from '#app/models/asset/image/image.generate.server' import { type IFill } from '#app/models/design-type/fill/fill.server' import { type ILayout } from '#app/models/design-type/layout/layout.server' import { type ILine } from '#app/models/design-type/line/line.server' @@ -72,6 +72,11 @@ export interface ILayerGenerator extends IGeneratorDesigns { name?: ILayer['name'] description?: ILayer['description'] + // layer always has access to the background color + // if drawing image to get pixel data + // this allows the background to be redrawn + background: string + // create this design type container: ILayerGeneratorContainer @@ -94,14 +99,10 @@ export interface ILayerGeneratorContainer { export interface IGenerationLayer { generator: ILayerGenerator - assets: IGenerationAssets + assets: IAssetGenerationByType items: IGenerationItem[] } -export interface IGenerationAssets { - image: IAssetImageGeneration | null -} - export interface IGenerationItem { id: string fillStyle: string diff --git a/app/models/asset/asset.generation.server.ts b/app/models/asset/asset.generation.server.ts new file mode 100644 index 00000000..45f5d750 --- /dev/null +++ b/app/models/asset/asset.generation.server.ts @@ -0,0 +1,5 @@ +import { type IAssetImageGeneration } from './image/image.generate.server' + +export interface IAssetGenerationByType { + assetImages: IAssetImageGeneration[] +} diff --git a/app/models/asset/image/image.generate.server.ts b/app/models/asset/image/image.generate.server.ts index d3344edf..e1f3fcb2 100644 --- a/app/models/asset/image/image.generate.server.ts +++ b/app/models/asset/image/image.generate.server.ts @@ -1,3 +1,10 @@ +import { type IAssetImage } from './image.server' + +export interface IAssetImageSrcGeneration { + id: IAssetImage['id'] + src: string +} + export interface IAssetImageDrawGeneration { x: number y: number @@ -5,6 +12,8 @@ export interface IAssetImageDrawGeneration { height: number } -export interface IAssetImageGeneration extends IAssetImageDrawGeneration { - src: string +export interface IAssetImageGeneration + extends IAssetImageSrcGeneration, + IAssetImageDrawGeneration { + hideOnDraw: boolean } diff --git a/app/models/asset/image/image.server.ts b/app/models/asset/image/image.server.ts index 5f454198..7d53debd 100644 --- a/app/models/asset/image/image.server.ts +++ b/app/models/asset/image/image.server.ts @@ -24,6 +24,7 @@ export interface IAssetImageFileData { lastModified?: number filename: string fit?: IAssetImageFit + hideOnDraw?: boolean } // when adding attributes to an asset type, diff --git a/app/models/asset/image/image.update.hide-on-draw.server.ts b/app/models/asset/image/image.update.hide-on-draw.server.ts new file mode 100644 index 00000000..16a33cc7 --- /dev/null +++ b/app/models/asset/image/image.update.hide-on-draw.server.ts @@ -0,0 +1,49 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditAssetImageHideOnDrawSchema } from '#app/schema/asset/image' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAssetImage, type IAssetImageFileData } from './image.server' +import { stringifyAssetImageAttributes } from './utils' + +export const validateEditHideOnDrawAssetImageSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditAssetImageHideOnDrawSchema, + strategy, + }) +} + +export interface IAssetImageUpdateHideOnDrawSubmission { + userId: IUser['id'] + id: IAssetImage['id'] + hideOnDraw: boolean +} + +interface IAssetImageUpdateHideOnDrawData { + attributes: IAssetImageFileData +} + +export const updateAssetImageHideOnDraw = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageUpdateHideOnDrawData +}) => { + const { attributes } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/utils.ts b/app/models/asset/utils.ts index be3cebd0..450567ad 100644 --- a/app/models/asset/utils.ts +++ b/app/models/asset/utils.ts @@ -65,6 +65,14 @@ export const validateAssetAttributes = ({ } } +export const filterAssetsVisible = ({ + assets, +}: { + assets: IAssetParsed[] +}): IAssetType[] => { + return assets.filter(asset => asset.visible) +} + export const filterAssetType = ({ assets, type, diff --git a/app/routes/resources+/api.v1+/asset.image.update.hide-on-draw.tsx b/app/routes/resources+/api.v1+/asset.image.update.hide-on-draw.tsx new file mode 100644 index 00000000..498b0609 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.update.hide-on-draw.tsx @@ -0,0 +1,101 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconButton } from '#app/components/templates/form/fetcher-icon-button' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { validateEditHideOnDrawAssetImageSubmission } from '#app/models/asset/image/image.update.hide-on-draw.server' +import { EditAssetImageHideOnDrawSchema } from '#app/schema/asset/image' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageUpdateHideOnDrawService } from '#app/services/asset.image.update.hide-on-draw.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.UPDATE.HIDE_ON_DRAW +const schema = EditAssetImageHideOnDrawSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let updateSuccess = false + let errorMessage = '' + const { status, submission } = + await validateEditHideOnDrawAssetImageSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageUpdateHideOnDrawService({ + userId, + ...submission.value, + }) + updateSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !updateSuccess ? 404 : 200, + }, + ) +} + +export const AssetImageUpdateHideOnDraw = ({ + image, + formLocation = '', +}: { + image: IAssetImage + formLocation?: string +}) => { + const assetId = image.id + const field = 'hideOnDraw' + const isHiddenOnDraw = image.attributes[field] || false + const icon = isHiddenOnDraw ? 'eye-none' : 'eye-open' + const iconText = `${isHiddenOnDraw ? 'Show' : 'Show'} on draw` + const fetcherKey = `asset-image-update-${field}-${assetId}` + const formId = `${fetcherKey}${formLocation ? `-${formLocation}` : ''}` + + let isHydrated = useHydrated() + const fetcher = useFetcher({ + key: fetcherKey, + }) + + return ( + +
+ +
+
+ ) +} diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index f4f98d60..8d57f801 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -61,6 +61,7 @@ export const AssetAttributesImageSchema = z.object({ lastModified: z.number().optional(), filename: z.string(), fit: z.nativeEnum(AssetImageFitTypeEnum).optional(), + hideOnDraw: z.boolean().optional(), }) // zod schema for blob Buffer/File is not working @@ -94,6 +95,10 @@ export const EditAssetImageFitSchema = z.object({ fit: z.nativeEnum(AssetImageFitTypeEnum), }) +export const EditAssetImageHideOnDrawSchema = z.object({ + id: z.string(), +}) + export const DeleteAssetImageSchema = z.object({ id: z.string(), }) diff --git a/app/services/artwork/version/generator/build.container.service.ts b/app/services/artwork/version/generator/build.container.service.ts new file mode 100644 index 00000000..77dbb630 --- /dev/null +++ b/app/services/artwork/version/generator/build.container.service.ts @@ -0,0 +1,20 @@ +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' + +export const getArtworkVersionContainer = ({ + version, +}: { + version: IArtworkVersionWithChildren +}) => { + const { width, height } = version + return { + width, + height, + top: 0, + left: 0, + margin: 0, + canvas: { + width, + height, + }, + } +} diff --git a/app/services/artwork/version/generator/build.layers.service.ts b/app/services/artwork/version/generator/build.layers.service.ts new file mode 100644 index 00000000..f0b26dd2 --- /dev/null +++ b/app/services/artwork/version/generator/build.layers.service.ts @@ -0,0 +1,156 @@ +import { + type IGeneratorDesigns, + type ILayerGenerator, +} from '#app/definitions/artwork-generator' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { filterAssetsVisible, groupAssetsByType } from '#app/models/asset/utils' +import { findManyDesignsWithType } from '#app/models/design/design.server' +import { getArtworkVersionVisiblePalettes } from '#app/models/design-artwork-version/design-artwork-version.server' +import { getLayerVisiblePalettes } from '#app/models/design-layer/design-layer.server' +import { type ILayerWithChildren } from '#app/models/layer/layer.server' +import { + filterSelectedDesignTypes, + findFirstDesignsByTypeInArray, +} from '#app/utils/design' +import { filterLayersVisible } from '#app/utils/layer.utils' +import { orderLinkedItems } from '#app/utils/linked-list.utils' +import { getArtworkVersionContainer } from './build.container.service' +import { getRotates } from './build.rotates.service' + +// default/global design settings for each layer +// layer can override any of these values +export const buildDefaultGeneratorLayer = async ({ + version, + defaultGeneratorDesigns, +}: { + version: IArtworkVersionWithChildren + defaultGeneratorDesigns: IGeneratorDesigns +}): Promise => { + const artworkVersionId = version.id + + // get all visible palettes to use for fill or stroke + const palettes = await getArtworkVersionVisiblePalettes({ + artworkVersionId, + }) + + // get all visible rotates to use for rotate if visible random + const rotates = await getRotates({ + artworkVersionId, + rotate: defaultGeneratorDesigns.rotate, + }) + + // container defaults to version dimensions + const container = getArtworkVersionContainer({ version }) + + const assets = groupAssetsByType({ assets: version.assets }) + + return { + ...defaultGeneratorDesigns, + background: version.background, + palette: palettes, + rotates, + container, + assets, + } +} + +export const buildGeneratorLayers = async ({ + version, + defaultGeneratorLayer, +}: { + version: IArtworkVersionWithChildren + defaultGeneratorLayer: ILayerGenerator +}) => { + const orderedLayers = await orderLinkedItems( + version.layers, + ) + const visibleLayers = filterLayersVisible({ + layers: orderedLayers, + }) as ILayerWithChildren[] + + return await Promise.all( + visibleLayers.map(layer => + buildGeneratorLayer({ + layer, + defaultGeneratorLayer, + }), + ), + ) +} + +const buildGeneratorLayer = async ({ + layer, + defaultGeneratorLayer, +}: { + layer: ILayerWithChildren + defaultGeneratorLayer: ILayerGenerator +}): Promise => { + const layerId = layer.id + + // Step 1: get all selected designs for the layer + const layerSelectedDesigns = await getLayerSelectedDesigns({ layerId }) + + // Step 2: split the selected designs into the first of each type + const selectedDesignTypes = findFirstDesignsByTypeInArray({ + designs: layerSelectedDesigns, + }) + + // Step 3: filter the selected designs that are present + // separate the palette from the rest of the layer generator designs + // if the layer has no palette we do not want to override the default palette + const { palette, ...layerGeneratorDesigns } = filterSelectedDesignTypes({ + selectedDesignTypes, + }) + + // Step 4: initialize the generator layer + // with the default generator layer + // and the layer generator designs as overrides + // and layer details + const { id, name, description } = layer + + const assets = groupAssetsByType({ + assets: filterAssetsVisible({ assets: layer.assets }), + }) + + const layerGenerator = { + ...defaultGeneratorLayer, + ...layerGeneratorDesigns, + id, + name, + description, + assets, + } + + // Step 5: get all visible palettes to use for fill or stroke + // if empty, then use the default palette + const palettes = await getLayerVisiblePalettes({ layerId }) + if (palettes.length > 0) { + layerGenerator.palette = palettes + } + + // Step 6: get all visible rotates to use for rotate if visible random + // if empty, then use the default rotate + const { rotate } = layerGeneratorDesigns + if (rotate) { + const rotates = await getRotates({ + layerId, + rotate, + }) + + if (rotates.length > 0) { + layerGenerator.rotates = rotates + } + } + + return layerGenerator +} + +const getLayerSelectedDesigns = async ({ + layerId, +}: { + layerId: ILayerWithChildren['id'] +}) => { + return await findManyDesignsWithType({ + where: { layerId, selected: true }, + }) +} diff --git a/app/services/artwork/version/generator/build.metadata.service.ts b/app/services/artwork/version/generator/build.metadata.service.ts new file mode 100644 index 00000000..001d9a31 --- /dev/null +++ b/app/services/artwork/version/generator/build.metadata.service.ts @@ -0,0 +1,77 @@ +import { invariant } from '@epic-web/invariant' +import { type IArtworkVersionGeneratorMetadata } from '#app/definitions/artwork-generator' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { prisma } from '#app/utils/db.server' + +export const buildGeneratorMetadata = async ({ + version, +}: { + version: IArtworkVersionWithChildren +}): Promise => { + const branch = await prisma.artworkBranch.findUnique({ + where: { id: version.branchId }, + select: { + id: true, + name: true, + slug: true, + description: true, + artwork: { + select: { + id: true, + name: true, + slug: true, + description: true, + project: { + select: { + id: true, + name: true, + slug: true, + description: true, + }, + }, + owner: { + select: { + id: true, + name: true, + username: true, + }, + }, + }, + }, + }, + }) + invariant(branch, `Branch not found for version ${version.id}`) + const artwork = branch.artwork + invariant(artwork, `Artwork not found for branch ${branch.id}`) + const project = artwork.project + invariant(project, `Project not found for artwork ${artwork.id}`) + const owner = artwork.owner + invariant(owner, `Owner not found for artwork ${artwork.id}`) + + return { + // version + versionId: version.id, + versionName: version.name, + versionSlug: version.slug, + versionDescription: version.description, + // branch + branchId: version.branchId, + branchName: branch.name, + branchSlug: branch.slug, + branchDescription: branch.description, + // artwork + artworkId: artwork.id, + artworkName: artwork.name, + artworkSlug: artwork.slug, + artworkDescription: artwork.description, + // project + projectId: project.id, + projectName: project.name, + projectSlug: project.slug, + projectDescription: project.description, + // owner + ownerId: owner.id, + ownerName: owner.name, + ownerUsername: owner.username, + } +} diff --git a/app/services/artwork/version/generator/build.rotates.service.ts b/app/services/artwork/version/generator/build.rotates.service.ts new file mode 100644 index 00000000..bc24083f --- /dev/null +++ b/app/services/artwork/version/generator/build.rotates.service.ts @@ -0,0 +1,28 @@ +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { getArtworkVersionVisibleRotates } from '#app/models/design-artwork-version/design-artwork-version.server' +import { getLayerVisibleRotates } from '#app/models/design-layer/design-layer.server' +import { type IRotate } from '#app/models/design-type/rotate/rotate.server' +import { type ILayerWithChildren } from '#app/models/layer/layer.server' +import { type rotateBasisTypeEnum } from '#app/schema/rotate' +import { isArrayRotateBasisType } from '#app/utils/rotate' + +export const getRotates = async ({ + artworkVersionId, + layerId, + rotate, +}: { + artworkVersionId?: IArtworkVersionWithChildren['id'] + layerId?: ILayerWithChildren['id'] + rotate: IRotate +}) => { + const allRotates = isArrayRotateBasisType(rotate.basis as rotateBasisTypeEnum) + + if (allRotates) { + if (artworkVersionId) { + return await getArtworkVersionVisibleRotates({ artworkVersionId }) + } else if (layerId) { + return await getLayerVisibleRotates({ layerId }) + } + } + return [] +} diff --git a/app/services/artwork/version/generator/build.service.ts b/app/services/artwork/version/generator/build.service.ts index 26ae44f3..b223ef0b 100644 --- a/app/services/artwork/version/generator/build.service.ts +++ b/app/services/artwork/version/generator/build.service.ts @@ -1,37 +1,12 @@ -import { invariant } from '@epic-web/invariant' -import { - type IGeneratorDesigns, - type ILayerGenerator, - type IArtworkVersionGenerator, - type IGeneratorWatermark, - type IArtworkVersionGeneratorMetadata, -} from '#app/definitions/artwork-generator' +import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' -import { groupAssetsByType } from '#app/models/asset/utils' -import { - findManyDesignsWithType, - type IDesignWithType, -} from '#app/models/design/design.server' -import { - getArtworkVersionVisiblePalettes, - getArtworkVersionVisibleRotates, -} from '#app/models/design-artwork-version/design-artwork-version.server' import { - getLayerVisiblePalettes, - getLayerVisibleRotates, -} from '#app/models/design-layer/design-layer.server' -import { type IRotate } from '#app/models/design-type/rotate/rotate.server' -import { type ILayerWithChildren } from '#app/models/layer/layer.server' -import { type rotateBasisTypeEnum } from '#app/schema/rotate' -import { prisma } from '#app/utils/db.server' -import { - filterSelectedDesignTypes, - findFirstDesignsByTypeInArray, - verifySelectedDesignTypesAllPresent, -} from '#app/utils/design' -import { filterLayersVisible } from '#app/utils/layer.utils' -import { orderLinkedItems } from '#app/utils/linked-list.utils' -import { isArrayRotateBasisType } from '#app/utils/rotate' + buildDefaultGeneratorLayer, + buildGeneratorLayers, +} from './build.layers.service' +import { buildGeneratorMetadata } from './build.metadata.service' +import { verifyDefaultGeneratorDesigns } from './build.verify.service' +import { buildGeneratorWatermark } from './build.watermark.service' // "build" since it is creating the version generator each time // later, if we like the generator, we can save it to the database @@ -71,11 +46,8 @@ export const artworkVersionGeneratorBuildService = async ({ // Step 4: build the generator layers // each layer can override any of the global settings - const orderedLayers = await orderLinkedItems( - version.layers, - ) - const generatorLayers = await buildGeneratorLayers({ - layers: orderedLayers, + const layers = await buildGeneratorLayers({ + version, defaultGeneratorLayer, }) @@ -92,7 +64,7 @@ export const artworkVersionGeneratorBuildService = async ({ height: version.height, background: version.background, }, - layers: generatorLayers, + layers, watermark, metadata, success: true, @@ -113,330 +85,3 @@ export const artworkVersionGeneratorBuildService = async ({ } } } - -const verifyDefaultGeneratorDesigns = async ({ - version, -}: { - version: IArtworkVersionWithChildren -}): Promise<{ - defaultGeneratorDesigns: IGeneratorDesigns | null - message: string -}> => { - // Step 1: get all selected designs for the version - // design table has `selected: boolean` field - const selectedDesigns = await getVersionSelectedDesigns({ - artworkVersionId: version.id, - }) - - // Step 2: split the selected designs into the first of each type - const selectedDesignTypes = findFirstDesignsByTypeInArray({ - designs: selectedDesigns, - }) - - // Step 3: validate that all selected design types are present - // message will indicate which design type is missing - const { success, message } = verifySelectedDesignTypesAllPresent({ - selectedDesignTypes, - }) - - // Step 4: return failure with message if selected designs are not valid - if (!success) { - return { - message, - defaultGeneratorDesigns: null, - } - } - - // Step 5: reformat the selected designs to be generator designs - // this is to ensure that the selected designs are not null - const defaultGeneratorDesigns = { - ...selectedDesignTypes, - palette: [selectedDesignTypes.palette], - } as IGeneratorDesigns - - return { - defaultGeneratorDesigns, - message: 'Version generator designs are present.', - } -} - -const getVersionSelectedDesigns = async ({ - artworkVersionId, -}: { - artworkVersionId: IArtworkVersionWithChildren['id'] -}): Promise => { - return await findManyDesignsWithType({ - where: { artworkVersionId, selected: true }, - }) -} - -// default/global design settings for each layer -// layer can override any of these values -const buildDefaultGeneratorLayer = async ({ - version, - defaultGeneratorDesigns, -}: { - version: IArtworkVersionWithChildren - defaultGeneratorDesigns: IGeneratorDesigns -}): Promise => { - const artworkVersionId = version.id - - // get all visible palettes to use for fill or stroke - const palettes = await getArtworkVersionVisiblePalettes({ - artworkVersionId, - }) - - // get all visible rotates to use for rotate if visible random - const rotates = await getRotates({ - artworkVersionId, - rotate: defaultGeneratorDesigns.rotate, - }) - - // container defaults to version dimensions - const container = getArtworkVersionContainer({ version }) - - const assets = groupAssetsByType({ assets: version.assets }) - - return { - ...defaultGeneratorDesigns, - palette: palettes, - rotates, - container, - assets, - } -} - -const getArtworkVersionContainer = ({ - version, -}: { - version: IArtworkVersionWithChildren -}) => { - const { width, height } = version - return { - width, - height, - top: 0, - left: 0, - margin: 0, - canvas: { - width, - height, - }, - } -} - -const buildGeneratorLayers = async ({ - layers, - defaultGeneratorLayer, -}: { - layers: ILayerWithChildren[] - defaultGeneratorLayer: ILayerGenerator -}) => { - const visibleLayers = filterLayersVisible({ layers }) as ILayerWithChildren[] - - return await Promise.all( - visibleLayers.map(layer => - buildGeneratorLayer({ - layer, - defaultGeneratorLayer, - }), - ), - ) -} - -const buildGeneratorLayer = async ({ - layer, - defaultGeneratorLayer, -}: { - layer: ILayerWithChildren - defaultGeneratorLayer: ILayerGenerator -}): Promise => { - const layerId = layer.id - - // Step 1: get all selected designs for the layer - const layerSelectedDesigns = await getLayerSelectedDesigns({ layerId }) - - // Step 2: split the selected designs into the first of each type - const selectedDesignTypes = findFirstDesignsByTypeInArray({ - designs: layerSelectedDesigns, - }) - - // Step 3: filter the selected designs that are present - // separate the palette from the rest of the layer generator designs - // if the layer has no palette we do not want to override the default palette - const { palette, ...layerGeneratorDesigns } = filterSelectedDesignTypes({ - selectedDesignTypes, - }) - - // Step 4: initialize the generator layer - // with the default generator layer - // and the layer generator designs as overrides - // and layer details - const { id, name, description } = layer - - const assets = groupAssetsByType({ assets: layer.assets }) - - const layerGenerator = { - ...defaultGeneratorLayer, - ...layerGeneratorDesigns, - id, - name, - description, - assets, - } - - // Step 5: get all visible palettes to use for fill or stroke - // if empty, then use the default palette - const palettes = await getLayerVisiblePalettes({ layerId }) - if (palettes.length > 0) { - layerGenerator.palette = palettes - } - - // Step 6: get all visible rotates to use for rotate if visible random - // if empty, then use the default rotate - const { rotate } = layerGeneratorDesigns - if (rotate) { - const rotates = await getRotates({ - layerId, - rotate, - }) - - if (rotates.length > 0) { - layerGenerator.rotates = rotates - } - } - - return layerGenerator -} - -const getLayerSelectedDesigns = async ({ - layerId, -}: { - layerId: ILayerWithChildren['id'] -}) => { - return await findManyDesignsWithType({ - where: { layerId, selected: true }, - }) -} - -const getRotates = async ({ - artworkVersionId, - layerId, - rotate, -}: { - artworkVersionId?: IArtworkVersionWithChildren['id'] - layerId?: ILayerWithChildren['id'] - rotate: IRotate -}) => { - const allRotates = isArrayRotateBasisType(rotate.basis as rotateBasisTypeEnum) - - if (allRotates) { - if (artworkVersionId) { - return await getArtworkVersionVisibleRotates({ artworkVersionId }) - } else if (layerId) { - return await getLayerVisibleRotates({ layerId }) - } - } - return [] -} - -const buildGeneratorWatermark = async ({ - version, -}: { - version: IArtworkVersionWithChildren -}): Promise => { - if (!version.watermark) return null - - const userInstagramUrl = await prisma.artworkBranch - .findUnique({ - where: { id: version.branchId }, - select: { - owner: { - select: { sm_url_instagram: true }, - }, - }, - }) - .then(branch => branch?.owner?.sm_url_instagram) - - const text = userInstagramUrl - ? `@${userInstagramUrl.split('/').pop()}` - : 'PPPAAATTT' - - return { - text, - color: version.watermarkColor, - } -} - -const buildGeneratorMetadata = async ({ - version, -}: { - version: IArtworkVersionWithChildren -}): Promise => { - const branch = await prisma.artworkBranch.findUnique({ - where: { id: version.branchId }, - select: { - id: true, - name: true, - slug: true, - description: true, - artwork: { - select: { - id: true, - name: true, - slug: true, - description: true, - project: { - select: { - id: true, - name: true, - slug: true, - description: true, - }, - }, - owner: { - select: { - id: true, - name: true, - username: true, - }, - }, - }, - }, - }, - }) - invariant(branch, `Branch not found for version ${version.id}`) - const artwork = branch.artwork - invariant(artwork, `Artwork not found for branch ${branch.id}`) - const project = artwork.project - invariant(project, `Project not found for artwork ${artwork.id}`) - const owner = artwork.owner - invariant(owner, `Owner not found for artwork ${artwork.id}`) - - return { - // version - versionId: version.id, - versionName: version.name, - versionSlug: version.slug, - versionDescription: version.description, - // branch - branchId: version.branchId, - branchName: branch.name, - branchSlug: branch.slug, - branchDescription: branch.description, - // artwork - artworkId: artwork.id, - artworkName: artwork.name, - artworkSlug: artwork.slug, - artworkDescription: artwork.description, - // project - projectId: project.id, - projectName: project.name, - projectSlug: project.slug, - projectDescription: project.description, - // owner - ownerId: owner.id, - ownerName: owner.name, - ownerUsername: owner.username, - } -} diff --git a/app/services/artwork/version/generator/build.verify.service.ts b/app/services/artwork/version/generator/build.verify.service.ts new file mode 100644 index 00000000..986528e9 --- /dev/null +++ b/app/services/artwork/version/generator/build.verify.service.ts @@ -0,0 +1,66 @@ +import { type IGeneratorDesigns } from '#app/definitions/artwork-generator' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { + type IDesignWithType, + findManyDesignsWithType, +} from '#app/models/design/design.server' +import { + findFirstDesignsByTypeInArray, + verifySelectedDesignTypesAllPresent, +} from '#app/utils/design' + +export const verifyDefaultGeneratorDesigns = async ({ + version, +}: { + version: IArtworkVersionWithChildren +}): Promise<{ + defaultGeneratorDesigns: IGeneratorDesigns | null + message: string +}> => { + // Step 1: get all selected designs for the version + // design table has `selected: boolean` field + const selectedDesigns = await getVersionSelectedDesigns({ + artworkVersionId: version.id, + }) + + // Step 2: split the selected designs into the first of each type + const selectedDesignTypes = findFirstDesignsByTypeInArray({ + designs: selectedDesigns, + }) + + // Step 3: validate that all selected design types are present + // message will indicate which design type is missing + const { success, message } = verifySelectedDesignTypesAllPresent({ + selectedDesignTypes, + }) + + // Step 4: return failure with message if selected designs are not valid + if (!success) { + return { + message, + defaultGeneratorDesigns: null, + } + } + + // Step 5: reformat the selected designs to be generator designs + // this is to ensure that the selected designs are not null + const defaultGeneratorDesigns = { + ...selectedDesignTypes, + palette: [selectedDesignTypes.palette], + } as IGeneratorDesigns + + return { + defaultGeneratorDesigns, + message: 'Version generator designs are present.', + } +} + +const getVersionSelectedDesigns = async ({ + artworkVersionId, +}: { + artworkVersionId: IArtworkVersionWithChildren['id'] +}): Promise => { + return await findManyDesignsWithType({ + where: { artworkVersionId, selected: true }, + }) +} diff --git a/app/services/artwork/version/generator/build.watermark.service.ts b/app/services/artwork/version/generator/build.watermark.service.ts new file mode 100644 index 00000000..3cbd4045 --- /dev/null +++ b/app/services/artwork/version/generator/build.watermark.service.ts @@ -0,0 +1,31 @@ +import { type IGeneratorWatermark } from '#app/definitions/artwork-generator' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { prisma } from '#app/utils/db.server' + +export const buildGeneratorWatermark = async ({ + version, +}: { + version: IArtworkVersionWithChildren +}): Promise => { + if (!version.watermark) return null + + const userInstagramUrl = await prisma.artworkBranch + .findUnique({ + where: { id: version.branchId }, + select: { + owner: { + select: { sm_url_instagram: true }, + }, + }, + }) + .then(branch => branch?.owner?.sm_url_instagram) + + const text = userInstagramUrl + ? `@${userInstagramUrl.split('/').pop()}` + : 'PPPAAATTT' + + return { + text, + color: version.watermarkColor, + } +} diff --git a/app/services/asset.image.update.hide-on-draw.service.ts b/app/services/asset.image.update.hide-on-draw.service.ts new file mode 100644 index 00000000..e35e327e --- /dev/null +++ b/app/services/asset.image.update.hide-on-draw.service.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { getAssetImage } from '#app/models/asset/image/image.get.server' +import { + type IAssetImageUpdateHideOnDrawSubmission, + updateAssetImageHideOnDraw, +} from '#app/models/asset/image/image.update.hide-on-draw.server' +import { type IAssetImageUpdatedResponse } from '#app/models/asset/image/image.update.server' +import { AssetAttributesImageSchema } from '#app/schema/asset/image' +import { prisma } from '#app/utils/db.server' + +export const assetImageUpdateHideOnDrawService = async ({ + userId, + id, +}: IAssetImageUpdateHideOnDrawSubmission): Promise => { + try { + // Step 1: verify the asset image exists + const assetImage = await getAssetImage({ + where: { id, ownerId: userId }, + }) + invariant(assetImage, 'Asset Image not found') + const { attributes: assetImageAttributes } = assetImage + + // Step 2: validate asset image data + const data = { + ...assetImageAttributes, + hideOnDraw: !assetImageAttributes.hideOnDraw, + } + const assetImageData = AssetAttributesImageSchema.parse(data) + + // Step 3: update the asset image via promise + const updateAssetImagePromise = updateAssetImageHideOnDraw({ + id, + data: { attributes: { ...assetImageData } }, + }) + + // Step 4: execute the transaction + const [updatedAssetImage] = await prisma.$transaction([ + updateAssetImagePromise, + ]) + + return { + updatedAssetImage, + success: true, + } + } catch (error) { + console.error(error) + return { + success: false, + message: 'Unknown error: assetImageUpdateHideOnDrawService', + } + } +} diff --git a/app/services/canvas/draw-background.service.ts b/app/services/canvas/draw-background.service.ts index c71bd3f0..8055557f 100644 --- a/app/services/canvas/draw-background.service.ts +++ b/app/services/canvas/draw-background.service.ts @@ -1,5 +1,11 @@ import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' +const canvasDimensions = ({ canvas }: { canvas: HTMLCanvasElement }) => { + const { width, height } = canvas + return { width, height, ratio: width / height } +} + +// do this at the beginning export const canvasDrawBackgroundService = ({ ctx, generator, @@ -12,3 +18,20 @@ export const canvasDrawBackgroundService = ({ ctx.fillStyle = `#${background}` ctx.fillRect(0, 0, width, height) } + +// do this if drawing an image to get pixel data +// then redraw the background to clear the canvas +export const canvasRedrawDrawBackgroundService = ({ + ctx, + background, +}: { + ctx: CanvasRenderingContext2D + background: string +}) => { + const { width: canvasWidth, height: canvasHeight } = canvasDimensions({ + canvas: ctx.canvas, + }) + + ctx.fillStyle = `#${background}` + ctx.fillRect(0, 0, canvasWidth, canvasHeight) +} diff --git a/app/services/canvas/draw.load-assets.service.ts b/app/services/canvas/draw.load-assets.service.ts new file mode 100644 index 00000000..6a5573ed --- /dev/null +++ b/app/services/canvas/draw.load-assets.service.ts @@ -0,0 +1,48 @@ +import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' +import { getAssetImgSrc } from '#app/models/asset/image/utils' +import { loadImage } from '#app/utils/image' + +// loaded assets: +// key: asset id +// value: image element, or something else when other assets are added + +export interface ILoadedAssets { + [key: string]: HTMLImageElement +} + +export const canvasDrawLoadAssetsService = async ({ + generator, +}: { + generator: IArtworkVersionGenerator +}): Promise => { + const { layers } = generator + const loadedAssets: ILoadedAssets = {} + + const imageLoadPromises: Promise[] = [] + + for (const layer of layers) { + const { + assets: { assetImages }, + } = layer + + for (const image of assetImages) { + const src = getAssetImgSrc({ image }) + const loadPromise = loadImage({ src }) + .then(img => { + loadedAssets[image.id] = img + }) + .catch(error => { + console.error( + `Failed to load image with ID ${image.id} from source: ${src}`, + error, + ) + }) + + imageLoadPromises.push(loadPromise) + } + } + + await Promise.all(imageLoadPromises) + + return loadedAssets +} diff --git a/app/services/canvas/draw.service.ts b/app/services/canvas/draw.service.ts index 82cde916..1bc16673 100644 --- a/app/services/canvas/draw.service.ts +++ b/app/services/canvas/draw.service.ts @@ -2,16 +2,20 @@ import { invariant } from '@epic-web/invariant' import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' import { canvasDrawBackgroundService } from './draw-background.service' import { canvasDrawWatermarkService } from './draw-watermark.service' +import { canvasDrawLoadAssetsService } from './draw.load-assets.service' import { canvasLayerBuildDrawLayersService } from './layer/build/build-draw-layers.service' import { canvasDrawLayersService } from './layer/draw/draw-layers.service' -export const canvasDrawService = ({ +export const canvasDrawService = async ({ canvas, generator, }: { canvas: HTMLCanvasElement generator: IArtworkVersionGenerator }) => { + // Step 0: load assets + const loadedAssets = await canvasDrawLoadAssetsService({ generator }) + // Step 1: get canvas const ctx = getContext(canvas) @@ -22,11 +26,11 @@ export const canvasDrawService = ({ const drawLayers = canvasLayerBuildDrawLayersService({ ctx, generator, + loadedAssets, }) - console.log('drawLayers count: ', drawLayers.length) // Step 4: draw layers to canvas - canvasDrawLayersService({ ctx, drawLayers }) + canvasDrawLayersService({ ctx, drawLayers, loadedAssets }) // Step 5: draw watermark canvasDrawWatermarkService({ ctx, generator }) diff --git a/app/services/canvas/layer/build/build-draw-layers.layer.assets.service.ts b/app/services/canvas/layer/build/build-draw-layers.layer.assets.service.ts new file mode 100644 index 00000000..dc8e8f71 --- /dev/null +++ b/app/services/canvas/layer/build/build-draw-layers.layer.assets.service.ts @@ -0,0 +1,17 @@ +import { type ILayerGenerator } from '#app/definitions/artwork-generator' +import { type IAssetGenerationByType } from '#app/models/asset/asset.generation.server' +import { canvasBuildLayerDrawImageService } from './build-layer-draw-image.service' + +export const buildLayerGenerationAssets = ({ + ctx, + layer, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator +}): IAssetGenerationByType => { + const assetImages = canvasBuildLayerDrawImageService({ ctx, layer }) + + return { + assetImages, + } +} diff --git a/app/services/canvas/layer/build/build-draw-layers.layer.items.service.ts b/app/services/canvas/layer/build/build-draw-layers.layer.items.service.ts new file mode 100644 index 00000000..5d5ec683 --- /dev/null +++ b/app/services/canvas/layer/build/build-draw-layers.layer.items.service.ts @@ -0,0 +1,86 @@ +import { + type IGenerationItem, + type ILayerGenerator, +} from '#app/definitions/artwork-generator' +import { type IAssetGenerationByType } from '#app/models/asset/asset.generation.server' +import { canvasRedrawDrawBackgroundService } from '../../draw-background.service' +import { type ILoadedAssets } from '../../draw.load-assets.service' +import { canvasDrawLayerAssets } from '../draw/draw-layers.asset.service' +import { canvasBuildLayerDrawCountService } from './build-layer-draw-count.service' +import { canvasBuildLayerDrawFillService } from './build-layer-draw-fill.service' +import { canvasBuildLayerDrawLineService } from './build-layer-draw-line.service' +import { shouldGetPixelHex } from './build-layer-draw-position.pixel.service' +import { canvasBuildLayerDrawPositionService } from './build-layer-draw-position.service' +import { canvasBuildLayerDrawRotateService } from './build-layer-draw-rotate.service' +import { canvasBuildLayerDrawSizeService } from './build-layer-draw-size.service' +import { canvasBuildLayerDrawStrokeService } from './build-layer-draw-stroke.service' +import { canvasBuildLayerDrawTemplateService } from './build-layer-draw-template.service' + +export const buildLayerGenerationItems = ({ + ctx, + layer, + assets, + loadedAssets, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator + assets: IAssetGenerationByType + loadedAssets: ILoadedAssets +}): IGenerationItem[] => { + const count = canvasBuildLayerDrawCountService({ layer }) + const drawAssets = shouldGetPixelHex({ layer }) + + if (drawAssets) { + canvasDrawLayerAssets({ ctx, assets, loadedAssets }) + } + + const layerGenerationItems = [] + for (let index = 0; index < count; index++) { + const generationItem = buildLayerGenerationItem({ + ctx, + layer, + index, + count, + }) + layerGenerationItems.push(generationItem) + } + + canvasRedrawDrawBackgroundService({ ctx, background: layer.background }) + + return layerGenerationItems +} + +const buildLayerGenerationItem = ({ + ctx, + layer, + index, + count, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator + index: number + count: number +}): IGenerationItem => { + const { x, y, pixelHex } = canvasBuildLayerDrawPositionService({ + ctx, + layer, + index, + }) + const size = canvasBuildLayerDrawSizeService({ layer, index }) + const fill = canvasBuildLayerDrawFillService({ layer, index, pixelHex }) + const stroke = canvasBuildLayerDrawStrokeService({ layer, index, pixelHex }) + const line = canvasBuildLayerDrawLineService({ layer, index }) + const rotate = canvasBuildLayerDrawRotateService({ layer, index }) + const template = canvasBuildLayerDrawTemplateService({ layer, index }) + + return { + id: `layer-${layer.id}-${index}-${count}`, + fillStyle: fill, + lineWidth: line, + position: { x, y }, + rotate, + size, + strokeStyle: stroke, + template, + } +} diff --git a/app/services/canvas/layer/build/build-draw-layers.layer.service.ts b/app/services/canvas/layer/build/build-draw-layers.layer.service.ts new file mode 100644 index 00000000..622e5ca2 --- /dev/null +++ b/app/services/canvas/layer/build/build-draw-layers.layer.service.ts @@ -0,0 +1,25 @@ +import { + type IGenerationLayer, + type ILayerGenerator, +} from '#app/definitions/artwork-generator' +import { type ILoadedAssets } from '../../draw.load-assets.service' +import { buildLayerGenerationAssets } from './build-draw-layers.layer.assets.service' +import { buildLayerGenerationItems } from './build-draw-layers.layer.items.service' + +export const canvasLayerBuildDrawLayersLayerService = ({ + ctx, + layer, + loadedAssets, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator + loadedAssets: ILoadedAssets +}): IGenerationLayer => { + const assets = buildLayerGenerationAssets({ ctx, layer }) + const items = buildLayerGenerationItems({ ctx, layer, assets, loadedAssets }) + return { + generator: layer, + assets, + items, + } +} diff --git a/app/services/canvas/layer/build/build-draw-layers.service.ts b/app/services/canvas/layer/build/build-draw-layers.service.ts index 32a9a69d..586723cc 100644 --- a/app/services/canvas/layer/build/build-draw-layers.service.ts +++ b/app/services/canvas/layer/build/build-draw-layers.service.ts @@ -1,107 +1,26 @@ import { type IGenerationLayer, type IArtworkVersionGenerator, - type IGenerationItem, - type ILayerGenerator, - type IGenerationAssets, } from '#app/definitions/artwork-generator' -import { canvasBuildLayerDrawCountService } from './build-layer-draw-count.service' -import { canvasBuildLayerDrawFillService } from './build-layer-draw-fill.service' -import { canvasBuildLayerDrawImageService } from './build-layer-draw-image.service' -import { canvasBuildLayerDrawLineService } from './build-layer-draw-line.service' -import { canvasBuildLayerDrawPositionService } from './build-layer-draw-position.service' -import { canvasBuildLayerDrawRotateService } from './build-layer-draw-rotate.service' -import { canvasBuildLayerDrawSizeService } from './build-layer-draw-size.service' -import { canvasBuildLayerDrawStrokeService } from './build-layer-draw-stroke.service' -import { canvasBuildLayerDrawTemplateService } from './build-layer-draw-template.service' +import { type ILoadedAssets } from '../../draw.load-assets.service' +import { canvasLayerBuildDrawLayersLayerService } from './build-draw-layers.layer.service' export const canvasLayerBuildDrawLayersService = ({ ctx, generator, + loadedAssets, }: { ctx: CanvasRenderingContext2D generator: IArtworkVersionGenerator + loadedAssets: ILoadedAssets }): IGenerationLayer[] => { const { layers } = generator - return layers.map(layer => { - const assets = buildLayerGenerationAssets({ ctx, layer }) - const items = buildLayerGenerationItems({ ctx, layer }) - return { - generator: layer, - assets, - items, - } - }) -} - -const buildLayerGenerationAssets = ({ - ctx, - layer, -}: { - ctx: CanvasRenderingContext2D - layer: ILayerGenerator -}): IGenerationAssets => { - const image = canvasBuildLayerDrawImageService({ ctx, layer }) - - return { - image, - } -} - -const buildLayerGenerationItems = ({ - ctx, - layer, -}: { - ctx: CanvasRenderingContext2D - layer: ILayerGenerator -}): IGenerationItem[] => { - const count = canvasBuildLayerDrawCountService({ layer }) - - const layerGenerationItems = [] - for (let index = 0; index < count; index++) { - const generationItem = buildLayerGenerationItem({ + return layers.map(layer => + canvasLayerBuildDrawLayersLayerService({ ctx, layer, - index, - count, - }) - layerGenerationItems.push(generationItem) - } - return layerGenerationItems -} - -const buildLayerGenerationItem = ({ - ctx, - layer, - index, - count, -}: { - ctx: CanvasRenderingContext2D - layer: ILayerGenerator - index: number - count: number -}): IGenerationItem => { - const { x, y, pixelHex } = canvasBuildLayerDrawPositionService({ - ctx, - layer, - index, - }) - const size = canvasBuildLayerDrawSizeService({ layer, index }) - const fill = canvasBuildLayerDrawFillService({ layer, index, pixelHex }) - const stroke = canvasBuildLayerDrawStrokeService({ layer, index, pixelHex }) - const line = canvasBuildLayerDrawLineService({ layer, index }) - const rotate = canvasBuildLayerDrawRotateService({ layer, index }) - const template = canvasBuildLayerDrawTemplateService({ layer, index }) - - return { - id: `layer-${layer.id}-${index}-${count}`, - fillStyle: fill, - lineWidth: line, - position: { x, y }, - rotate, - size, - strokeStyle: stroke, - template, - } + loadedAssets, + }), + ) } diff --git a/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts b/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts index d54ae54b..d56a4172 100644 --- a/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts +++ b/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts @@ -68,7 +68,7 @@ const imageFitCover = ({ }: { ctx: CanvasRenderingContext2D image: IAssetImage -}): IAssetImageDrawGeneration => { +}) => { const { // width: imageWidth, // height: imageHeight, @@ -168,7 +168,7 @@ export const canvasBuildLayerDrawImageFitService = ({ }: { ctx: CanvasRenderingContext2D image: IAssetImage -}) => { +}): IAssetImageDrawGeneration => { switch (image.attributes.fit) { case AssetImageFitTypeEnum.FILL: return imageFitFill({ ctx }) @@ -181,6 +181,6 @@ export const canvasBuildLayerDrawImageFitService = ({ case AssetImageFitTypeEnum.SCALE_DOWN: return imageFitScaleDown({ ctx, image }) default: - return null + return imageFitNone({ ctx, image }) } } diff --git a/app/services/canvas/layer/build/build-layer-draw-image.service.ts b/app/services/canvas/layer/build/build-layer-draw-image.service.ts index 13f74f52..f2436e21 100644 --- a/app/services/canvas/layer/build/build-layer-draw-image.service.ts +++ b/app/services/canvas/layer/build/build-layer-draw-image.service.ts @@ -12,23 +12,25 @@ export const canvasBuildLayerDrawImageService = ({ }: { ctx: CanvasRenderingContext2D layer: ILayerGenerator -}): IAssetImageGeneration | null => { +}): IAssetImageGeneration[] => { const { assets } = layer const { assetImages } = assets - if (!assetImages.length) return null + if (!assetImages.length) return [] - // just one image to start - const image = assetImages[0] + return assetImages.map(image => { + const src = getAssetImgSrc({ image }) + const fit = canvasBuildLayerDrawImageFitService({ + ctx, + image, + }) as IAssetImageDrawGeneration + const hideOnDraw = image.attributes.hideOnDraw || false - const src = getAssetImgSrc({ image }) - const fit = canvasBuildLayerDrawImageFitService({ - ctx, - image, - }) as IAssetImageDrawGeneration - - return { - src, - ...fit, - } + return { + id: image.id, + src, + ...fit, + hideOnDraw, + } + }) } diff --git a/app/services/canvas/layer/build/build-layer-draw-position.grid.service.ts b/app/services/canvas/layer/build/build-layer-draw-position.grid.service.ts new file mode 100644 index 00000000..b94f3f4b --- /dev/null +++ b/app/services/canvas/layer/build/build-layer-draw-position.grid.service.ts @@ -0,0 +1,36 @@ +import { type ILayerGenerator } from '#app/definitions/artwork-generator' + +export const getGridPosition = ({ + layer, + index, + ctx, +}: { + layer: ILayerGenerator + index: number + ctx: CanvasRenderingContext2D +}) => { + const { layout, container } = layer + const { rows, columns } = layout + const { width, height, top, left } = container + + // subtract 1 from denominator to get the proper step size + // for example, if columns is 10, we want 10 divide by 9 + // so that the first and last positions on the grid are the edges + // and not cells in the middle + const stepX = width / (columns - 1) + const stepY = height / (rows - 1) + + // quotient is the row index + const rowIndex = Math.floor(index / columns) + // remainder is the column index + const columnIndex = index % columns + + // calculate the x and y position based on the row and column index + // and the step size + // add the top and left values to get the actual position + // add margin later + const x = columnIndex * stepX + left + const y = rowIndex * stepY + top + + return { x, y } +} diff --git a/app/services/canvas/layer/build/build-layer-draw-position.pixel.service.ts b/app/services/canvas/layer/build/build-layer-draw-position.pixel.service.ts new file mode 100644 index 00000000..c3218d7f --- /dev/null +++ b/app/services/canvas/layer/build/build-layer-draw-position.pixel.service.ts @@ -0,0 +1,96 @@ +import { type ILayerGenerator } from '#app/definitions/artwork-generator' +import { FillBasisTypeEnum } from '#app/schema/fill' +import { StrokeBasisTypeEnum } from '#app/schema/stroke' + +export const shouldGetPixelHex = ({ layer }: { layer: ILayerGenerator }) => { + const { fill, stroke } = layer + return ( + fill.basis === FillBasisTypeEnum.PIXEL || + stroke.basis === StrokeBasisTypeEnum.PIXEL + ) +} + +// Function to get the hex color value at a specific pixel +export const getHexAtPixel = ({ + ctx, + x, + y, +}: { + ctx: CanvasRenderingContext2D + x: number + y: number +}) => { + // Adjust the position for the nearest pixel (see below) + const { xAdj, yAdj } = adjustPositionForNearestPixel({ x, y, ctx }) + // Get the image data at the pixel + const pixelData = buildPixelImageData({ ctx, x: xAdj, y: yAdj }) + // Extract RGB values from the pixel data + const { r, g, b } = buildPixelRGB({ data: pixelData.data }) + // Convert RGB to hex + const hex = componentToHex(r) + componentToHex(g) + componentToHex(b) + // Return the hex value in uppercase + return hex.toUpperCase() +} + +// if the x or y value are outside the canvas or on right/bottom edge +// there will be no pixel data, so we need to adjust the position +// this is a semi-temporary fix +// sometimes random positions are on the edge +// right/bottom of grid too +const adjustPositionForNearestPixel = ({ + x, + y, + ctx, +}: { + x: number + y: number + ctx: CanvasRenderingContext2D +}) => { + const { canvas } = ctx + const { width, height } = canvas + if (x < 0) { + x = 0 + } + if (x >= width) { + x = width - 1 + } + if (y < 0) { + y = 0 + } + if (y >= height) { + y = height - 1 + } + return { xAdj: x, yAdj: y } +} + +// Function to build the image data for a pixel +const buildPixelImageData = ({ + ctx, + x, + y, +}: { + ctx: CanvasRenderingContext2D + x: number + y: number +}): ImageData => { + // image data for a 1x1 pixel area, effectively getting data for a single pixel + return ctx.getImageData(x, y, 1, 1) +} + +// Function to build the RGB values from image data +const buildPixelRGB = ({ data }: { data: Uint8ClampedArray }) => { + const r = data[0] // Red value + const g = data[1] // Green value + const b = data[2] // Blue value + // The alpha value at index 3 is not included as we are only interested in the RGB values for color representation. + return { r, g, b } +} + +// Function to convert a component of RGB to hex +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString#description +function componentToHex(c: number) { + // Convert the number to hex + const hex = c.toString(16) + // Ensure 2 digits by adding a leading zero if necessary + return hex.length == 1 ? '0' + hex : hex +} diff --git a/app/services/canvas/layer/build/build-layer-draw-position.service.ts b/app/services/canvas/layer/build/build-layer-draw-position.service.ts index 0e3d9bc4..1cd4766c 100644 --- a/app/services/canvas/layer/build/build-layer-draw-position.service.ts +++ b/app/services/canvas/layer/build/build-layer-draw-position.service.ts @@ -1,8 +1,11 @@ import { type ILayerGenerator } from '#app/definitions/artwork-generator' -import { FillBasisTypeEnum } from '#app/schema/fill' import { LayoutStyleTypeEnum } from '#app/schema/layout' -import { StrokeBasisTypeEnum } from '#app/schema/stroke' import { randomInRange } from '#app/utils/random.utils' +import { getGridPosition } from './build-layer-draw-position.grid.service' +import { + getHexAtPixel, + shouldGetPixelHex, +} from './build-layer-draw-position.pixel.service' export const canvasBuildLayerDrawPositionService = ({ ctx, @@ -52,131 +55,3 @@ const getRandomPosition = ({ layer }: { layer: ILayerGenerator }) => { const y = randomInRange(top, height) return { x, y } } - -const getGridPosition = ({ - layer, - index, - ctx, -}: { - layer: ILayerGenerator - index: number - ctx: CanvasRenderingContext2D -}) => { - const { layout, container } = layer - const { rows, columns } = layout - const { width, height, top, left } = container - - // subtract 1 from denominator to get the proper step size - // for example, if columns is 10, we want 10 divide by 9 - // so that the first and last positions on the grid are the edges - // and not cells in the middle - const stepX = width / (columns - 1) - const stepY = height / (rows - 1) - - // quotient is the row index - const rowIndex = Math.floor(index / columns) - // remainder is the column index - const columnIndex = index % columns - - // calculate the x and y position based on the row and column index - // and the step size - // add the top and left values to get the actual position - // add margin later - const x = columnIndex * stepX + left - const y = rowIndex * stepY + top - - return { x, y } -} - -const shouldGetPixelHex = ({ layer }: { layer: ILayerGenerator }) => { - const { fill, stroke } = layer - return ( - fill.basis === FillBasisTypeEnum.PIXEL || - stroke.basis === StrokeBasisTypeEnum.PIXEL - ) -} - -// Function to get the hex color value at a specific pixel -const getHexAtPixel = ({ - ctx, - x, - y, -}: { - ctx: CanvasRenderingContext2D - x: number - y: number -}) => { - // Adjust the position for the nearest pixel (see below) - const { xAdj, yAdj } = adjustPositionForNearestPixel({ x, y, ctx }) - // Get the image data at the pixel - const pixelData = buildPixelImageData({ ctx, x: xAdj, y: yAdj }) - // Extract RGB values from the pixel data - const { r, g, b } = buildPixelRGB({ data: pixelData.data }) - // Convert RGB to hex - const hex = componentToHex(r) + componentToHex(g) + componentToHex(b) - // Return the hex value in uppercase - return hex.toUpperCase() -} - -// if the x or y value are outside the canvas or on right/bottom edge -// there will be no pixel data, so we need to adjust the position -// this is a semi-temporary fix -// sometimes random positions are on the edge -// right/bottom of grid too -const adjustPositionForNearestPixel = ({ - x, - y, - ctx, -}: { - x: number - y: number - ctx: CanvasRenderingContext2D -}) => { - const { canvas } = ctx - const { width, height } = canvas - if (x < 0) { - x = 0 - } - if (x >= width) { - x = width - 1 - } - if (y < 0) { - y = 0 - } - if (y >= height) { - y = height - 1 - } - return { xAdj: x, yAdj: y } -} - -// Function to build the image data for a pixel -const buildPixelImageData = ({ - ctx, - x, - y, -}: { - ctx: CanvasRenderingContext2D - x: number - y: number -}): ImageData => { - // image data for a 1x1 pixel area, effectively getting data for a single pixel - return ctx.getImageData(x, y, 1, 1) -} - -// Function to build the RGB values from image data -const buildPixelRGB = ({ data }: { data: Uint8ClampedArray }) => { - const r = data[0] // Red value - const g = data[1] // Green value - const b = data[2] // Blue value - // The alpha value at index 3 is not included as we are only interested in the RGB values for color representation. - return { r, g, b } -} - -// Function to convert a component of RGB to hex -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString#description -function componentToHex(c: number) { - // Convert the number to hex - const hex = c.toString(16) - // Ensure 2 digits by adding a leading zero if necessary - return hex.length == 1 ? '0' + hex : hex -} diff --git a/app/services/canvas/layer/draw/draw-layers.asset.service.ts b/app/services/canvas/layer/draw/draw-layers.asset.service.ts new file mode 100644 index 00000000..3982a327 --- /dev/null +++ b/app/services/canvas/layer/draw/draw-layers.asset.service.ts @@ -0,0 +1,27 @@ +import { type IAssetGenerationByType } from '#app/models/asset/asset.generation.server' +import { type ILoadedAssets } from '../../draw.load-assets.service' + +export const canvasDrawLayerAssets = ({ + ctx, + assets, + loadedAssets, + timeToDraw, +}: { + ctx: CanvasRenderingContext2D + assets: IAssetGenerationByType + loadedAssets: ILoadedAssets + timeToDraw?: boolean +}) => { + const { assetImages } = assets + + for (let i = 0; i < assetImages.length; i++) { + const image = assetImages[i] + + // may want to draw image to get pixel data on build + if (timeToDraw && image.hideOnDraw) continue + + const img = loadedAssets[image.id] + const { x, y, width, height } = image + ctx.drawImage(img, x, y, width, height) + } +} diff --git a/app/services/canvas/layer/draw/draw-layers.service.ts b/app/services/canvas/layer/draw/draw-layers.service.ts index cda10fc3..f1dbec28 100644 --- a/app/services/canvas/layer/draw/draw-layers.service.ts +++ b/app/services/canvas/layer/draw/draw-layers.service.ts @@ -1,44 +1,32 @@ import { type IGenerationLayer, type IGenerationItem, - type IGenerationAssets, } from '#app/definitions/artwork-generator' -import { loadImage } from '#app/utils/image' +import { type ILoadedAssets } from '../../draw.load-assets.service' import { drawLayerItemService } from './draw-layer-item.service' +import { canvasDrawLayerAssets } from './draw-layers.asset.service' export const canvasDrawLayersService = ({ ctx, drawLayers, + loadedAssets, }: { ctx: CanvasRenderingContext2D drawLayers: IGenerationLayer[] + loadedAssets: ILoadedAssets }) => { for (let i = 0; i < drawLayers.length; i++) { const layer = drawLayers[i] - drawLayerAssets({ ctx, assets: layer.assets }) + canvasDrawLayerAssets({ + ctx, + assets: layer.assets, + loadedAssets, + timeToDraw: true, + }) drawLayerItems({ ctx, items: layer.items }) } } -const drawLayerAssets = ({ - ctx, - assets, -}: { - ctx: CanvasRenderingContext2D - assets: IGenerationAssets -}) => { - const { image } = assets - if (image && image.src) { - console.log('assets.image: ', image) - const img = loadImage({ src: image.src }) - console.log('img: ', img) - // load image - // async - // const { x, y, width, height } = assets.image - // ctx.drawImage(img, x, y, width, height) - } -} - const drawLayerItems = ({ ctx, items, @@ -48,7 +36,6 @@ const drawLayerItems = ({ }) => { for (let i = 0; i < items.length; i++) { const layerDrawItem = items[i] - // console.log('count: ', i) drawLayerItemService({ ctx, layerDrawItem }) } } diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index b5a778c0..b5ec2112 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -58,6 +58,7 @@ export const Routes = { }, UPDATE: { FIT: `${pathBase}/asset/image/update/fit`, + HIDE_ON_DRAW: `${pathBase}/asset/image/update/hide-on-draw`, }, }, },