import * as React from "react"; import { Button, Box, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, } from "@chakra-ui/core"; import { ExternalLinkIcon } from "@chakra-ui/icons"; /** * ItemLayerSupportUploadModal helps Support users create and upload PNGs for * broken appearance layers. Useful when the auto-converters are struggling, * e.g. the SWF uses a color filter our server-side Flash player can't support! */ function ItemLayerSupportUploadModal({ item, itemLayer, isOpen, onClose }) { const [step, setStep] = React.useState(1); const [imageOnBlackUrl, setImageOnBlackUrl] = React.useState(null); const [imageOnWhiteUrl, setImageOnWhiteUrl] = React.useState(null); const [imageWithAlphaUrl, setImageWithAlphaUrl] = React.useState(null); const [numWarnings, setNumWarnings] = React.useState(null); const onUpload = React.useCallback( (e) => { const file = e.target.files[0]; if (!file) { return; } const reader = new FileReader(); reader.onload = (re) => { switch (step) { case 1: setImageOnBlackUrl(re.target.result); setStep(2); return; case 2: setImageOnWhiteUrl(re.target.result); setStep(3); return; default: throw new Error(`unexpected step ${step}`); } }; reader.readAsDataURL(file); }, [step] ); // Once both images are ready, merge them! React.useEffect(() => { if (!imageOnBlackUrl || !imageOnWhiteUrl) { return; } setImageWithAlphaUrl(null); setNumWarnings(null); mergeIntoImageWithAlpha(imageOnBlackUrl, imageOnWhiteUrl).then( ([url, numWarnings]) => { setImageWithAlphaUrl(url); setNumWarnings(numWarnings); } ); }, [imageOnBlackUrl, imageOnWhiteUrl]); return ( Upload PNG for {item.name} {(step === 1 || step === 2) && ( )} {step === 3 && ( )} {step === 3 && ( )} ); } function ItemLayerSupportScreenshotStep({ itemLayer, step, onUpload }) { return ( <> Step {step}: Take a screenshot of exactly the 600×600 Flash region, then upload it below. ); } function ItemLayerSupportReviewStep({ imageWithAlphaUrl, numWarnings }) { if (imageWithAlphaUrl == null) { return Generating imageā€¦; } return ( <> Step 3: Does this look correct? If so, let's upload it! (Generated with {numWarnings} warnings.) {imageWithAlphaUrl && ( Generated layer PNG, on a checkered background )} ); } function ItemLayerSupportFlashPlayer({ swfUrl, backgroundColor }) { return ( ); } async function mergeIntoImageWithAlpha(imageOnBlackUrl, imageOnWhiteUrl) { const [imageOnBlack, imageOnWhite] = await Promise.all([ readImageDataFromUrl(imageOnBlackUrl), readImageDataFromUrl(imageOnWhiteUrl), ]); const [imageWithAlphaData, numWarnings] = mergeDataIntoImageWithAlpha( imageOnBlack, imageOnWhite ); const imageWithAlphaUrl = writeImageDataToUrl(imageWithAlphaData); return [imageWithAlphaUrl, numWarnings]; } function mergeDataIntoImageWithAlpha(imageOnBlack, imageOnWhite) { const imageWithAlpha = new ImageData(600, 600); let numWarnings = 0; for (let x = 0; x < 600; x++) { for (let y = 0; y < 600; y++) { const pixelIndex = (600 * y + x) << 2; const rOnBlack = imageOnBlack.data[pixelIndex]; const gOnBlack = imageOnBlack.data[pixelIndex + 1]; const bOnBlack = imageOnBlack.data[pixelIndex + 2]; const rOnWhite = imageOnWhite.data[pixelIndex]; const gOnWhite = imageOnWhite.data[pixelIndex + 1]; const bOnWhite = imageOnWhite.data[pixelIndex + 2]; if (rOnWhite < rOnBlack || gOnWhite < gOnBlack || bOnWhite < bOnBlack) { if (numWarnings < 100) { console.warn( `[${x}x${y}] color on white should be lighter than color on ` + `black, see pixel ${x}x${y}: ` + `#${rOnWhite.toString(16)}${bOnWhite.toString(16)}` + `${gOnWhite.toString(16)}` + ` vs ` + `#${rOnBlack.toString(16)}${bOnBlack.toString(16)}` + `${gOnWhite.toString(16)}. ` + `Falling back to the pixel on black, with alpha = 100%. ` ); } imageWithAlpha.data[pixelIndex] = rOnBlack; imageWithAlpha.data[pixelIndex + 1] = gOnBlack; imageWithAlpha.data[pixelIndex + 2] = bOnBlack; imageWithAlpha.data[pixelIndex + 3] = 255; numWarnings++; continue; } // The true alpha is how close together the on-white and on-black colors // are. If they're totally the same, it's 255 opacity. If they're totally // different, it's 0 opacity. In between, it scales linearly with the // difference! const alpha = 255 - (rOnWhite - rOnBlack); // Check that the alpha derived from other channels makes sense too. const alphaByB = 255 - (bOnWhite - bOnBlack); const alphaByG = 255 - (gOnWhite - gOnBlack); const highestAlpha = Math.max(Math.max(alpha, alphaByB), alphaByG); const lowestAlpha = Math.min(Math.min(alpha, alphaByB, alphaByG)); if (highestAlpha - lowestAlpha > 2) { if (numWarnings < 100) { console.warn( `[${x}x${y}] derived alpha values don't match: ` + `${alpha} vs ${alphaByB} vs ${alphaByG}. ` + `Colors: #${rOnWhite.toString(16)}${bOnWhite.toString(16)}` + `${gOnWhite.toString(16)}` + ` vs ` + `#${rOnBlack.toString(16)}${bOnBlack.toString(16)}` + `${gOnWhite.toString(16)}. ` + `Falling back to the pixel on black, with alpha = 100%. ` ); } imageWithAlpha.data[pixelIndex] = rOnBlack; imageWithAlpha.data[pixelIndex + 1] = gOnBlack; imageWithAlpha.data[pixelIndex + 2] = bOnBlack; imageWithAlpha.data[pixelIndex + 3] = 255; numWarnings++; continue; } // And the true color is the color on black, divided by the true alpha. // We can derive this from the definition of the color on black, which is // simply the true color times the true alpha. Divide to undo! const alphaRatio = alpha / 255; const rOnAlpha = Math.round(rOnBlack / alphaRatio); const gOnAlpha = Math.round(gOnBlack / alphaRatio); const bOnAlpha = Math.round(bOnBlack / alphaRatio); imageWithAlpha.data[pixelIndex] = rOnAlpha; imageWithAlpha.data[pixelIndex + 1] = gOnAlpha; imageWithAlpha.data[pixelIndex + 2] = bOnAlpha; imageWithAlpha.data[pixelIndex + 3] = alpha; } } return [imageWithAlpha, numWarnings]; } /** * readImageDataFromUrl reads an image URL to ImageData, by drawing it on a * canvas and reading ImageData back from it. */ async function readImageDataFromUrl(url) { const image = new Image(); await new Promise((resolve, reject) => { image.onload = resolve; image.onerror = reject; image.src = url; }); const canvas = document.createElement("canvas"); canvas.width = 600; canvas.height = 600; const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, 600, 600); return ctx.getImageData(0, 0, 600, 600); } /** * writeImageDataToUrl writes an ImageData to a data URL, by drawing it on a * canvas and reading a URL back from it. */ function writeImageDataToUrl(imageData) { const canvas = document.createElement("canvas"); canvas.width = 600; canvas.height = 600; const ctx = canvas.getContext("2d"); ctx.putImageData(imageData, 0, 0); return canvas.toDataURL("image/png"); } export default ItemLayerSupportUploadModal;