Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MIK-311B] Map upload bugs #146

Merged
merged 5 commits into from
Sep 21, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 104 additions & 73 deletions apps/novel-builder/src/modals/map/PlaceEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand All @@ -88,12 +178,6 @@ export default function PlaceEditModal() {
});
};

const getSceneData = (sceneId: string) => {
const scene = scenes.find((s) => s.id === sceneId);

return scene;
};

return (
<Modal
opened={!!place}
Expand Down Expand Up @@ -134,59 +218,6 @@ export default function PlaceEditModal() {
className="PlaceEdit__maskImage__mask"
previewImage={place.maskSource && config.genAssetLink(place.maskSource, AssetDisplayPrefix.MAP_MASK)}
handleChange={(file) => 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);
};
};
});
}}
/>
</div>
<div className="PlaceEdit__form">
Expand Down
Loading