diff --git a/apps/novel-builder/src/modals/map/PlaceEditModal.tsx b/apps/novel-builder/src/modals/map/PlaceEditModal.tsx index 14eac33d..aa51e930 100644 --- a/apps/novel-builder/src/modals/map/PlaceEditModal.tsx +++ b/apps/novel-builder/src/modals/map/PlaceEditModal.tsx @@ -8,7 +8,6 @@ import { closeModal } from '../../state/slices/inputSlice'; import { deletePlace, updatePlace } from '../../state/slices/novelFormSlice'; import { useAppSelector } from '../../state/store'; -import { useState } from 'react'; import { toast } from 'react-toastify'; import config, { MAX_FILE_SIZE } from '../../config'; import { checkFileType } from '../../libs/utils'; @@ -17,52 +16,143 @@ import './PlaceEditModal.scss'; import { AssetDisplayPrefix, AssetType } from '@mikugg/bot-utils'; function isBlackAndWhite(pixels: Uint8ClampedArray): boolean { + const tolerance = 50; + + const isNearBlack = (r: number, g: number, b: number) => { + return r <= tolerance && g <= tolerance && b <= tolerance; + }; + + const isNearWhite = (r: number, g: number, b: number) => { + return r >= 255 - tolerance && g >= 255 - tolerance && b >= 255 - tolerance; + }; + + let hasBlack = false; + let hasWhite = false; + for (let i = 0; i < pixels.length; i += 4) { const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; - const a = pixels[i + 3]; - // Check if the pixel is grayscale (r, g, b values are the same) and fully opaque (a is 255) - if (!(r === g && g === b && a === 255)) { - return false; + if (isNearBlack(r, g, b)) { + hasBlack = true; + } + + if (isNearWhite(r, g, b)) { + hasWhite = true; + } + + if (hasBlack && hasWhite) { + return true; } } - return true; + + return false; } export default function PlaceEditModal() { const dispatch = useDispatch(); const areYouSure = AreYouSure.useAreYouSure(); const map = useAppSelector(selectEditingMap); - const [selectSceneOpened, setSelectSceneOpened] = useState(false); const place = useAppSelector(selectEditingPlace); const backgrounds = useAppSelector((state) => state.novel.backgrounds); - const scenes = useAppSelector((state) => state.novel.scenes); + + const validateMaskImage = async (file: File) => { + const mapImageSrc = map?.source.png; + if (!mapImageSrc) { + toast.error('Please upload a map image first.'); + return false; + } + if (file.size > 2 * 1024 * 1024) { + toast.error('File size should be less than 1MB'); + return false; + } + if (!checkFileType(file, ['image/png', 'image/jpeg'])) { + toast.error('Invalid file type. Please upload a jpg file.'); + return false; + } + const image = new Image(); + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = (e) => { + image.src = e.target?.result as string; + }; + + return new Promise((resolve) => { + image.onload = () => { + // check if the mask contains only black and white pixels + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + resolve(false); + return; + } + + const mapImage = new Image(); + mapImage.src = config.genAssetLink(mapImageSrc, AssetDisplayPrefix.MAP_IMAGE); + + let maskRatio: number; + + if (image.width > 1024) { + const width = mapImage.width; + maskRatio = image.width / image.height; + const height = width / maskRatio; + canvas.width = width; + canvas.height = height; + } else { + canvas.width = image.width; + canvas.height = image.height; + maskRatio = image.width / image.height; + } + + ctx.drawImage(image, 0, 0, image.width, image.height); + const pixels = ctx.getImageData(0, 0, image.width, image.height).data; + + if (!isBlackAndWhite(pixels)) { + toast.error('Mask should be all black and have a white area for the place.'); + resolve(false); + return; + } + // check if the mask is the same size as the map image + + mapImage.onload = () => { + const mapRatio = mapImage.width / mapImage.height; + if (maskRatio !== mapRatio) { + toast.error('Mask should be the same size as the map image.'); + resolve(false); + return; + } + resolve(true); + }; + }; + }); + }; const handleUploadImage = async (file: File, source: 'preview' | 'mask') => { if (file && place) { try { - const asset = await config.uploadAsset( - file, - source === 'preview' ? AssetType.MAP_IMAGE_PREVIEW : AssetType.MAP_MASK, - ); switch (source) { case 'preview': + const { assetId } = await config.uploadAsset(file, AssetType.MAP_IMAGE_PREVIEW); dispatch( updatePlace({ mapId: map!.id, - place: { id: place.id, previewSource: asset.assetId }, + place: { id: place.id, previewSource: assetId }, }), ); return; case 'mask': + const validatedFile = await validateMaskImage(file); + if (!validatedFile) return; + + const uploadedAsset = await config.uploadAsset(file, AssetType.MAP_MASK); dispatch( updatePlace({ mapId: map!.id, - place: { id: place.id, maskSource: asset.assetId }, + place: { id: place.id, maskSource: uploadedAsset.assetId }, }), ); + return; } } catch (e) { @@ -88,12 +178,6 @@ export default function PlaceEditModal() { }); }; - const getSceneData = (sceneId: string) => { - const scene = scenes.find((s) => s.id === sceneId); - - return scene; - }; - return ( handleUploadImage(file, 'mask')} - onFileValidate={async (file) => { - const mapImageSrc = map?.source.png; - if (!mapImageSrc) { - toast.error('Please upload a map image first.'); - return false; - } - if (file.size > MAX_FILE_SIZE) { - toast.error('File size should be less than 5MB'); - return false; - } - if (!checkFileType(file, ['image/png', 'image/jpeg', 'image/webp'])) { - toast.error('Invalid file type. Please upload a jpg file.'); - return false; - } - const image = new Image(); - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = (e) => { - image.src = e.target?.result as string; - }; - return new Promise((resolve) => { - image.onload = () => { - // check if the mask contains only black and white pixels - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - if (!ctx) { - resolve(false); - return; - } - canvas.width = image.width; - canvas.height = image.height; - ctx.drawImage(image, 0, 0, image.width, image.height); - const pixels = ctx.getImageData(0, 0, image.width, image.height).data; - - if (!isBlackAndWhite(pixels)) { - toast.error('Mask should be all black and have a white area for the place.'); - resolve(false); - return; - } - // check if the mask is the same size as the map image - const mapImage = new Image(); - mapImage.src = config.genAssetLink(mapImageSrc, AssetDisplayPrefix.MAP_IMAGE); - mapImage.onload = () => { - if (mapImage.width / mapImage.height - image.width / image.height < 0.05) { - toast.error('Mask should be the same size as the map image.'); - resolve(false); - return; - } - resolve(true); - }; - }; - }); - }} />