write up more tutorial

This commit is contained in:
Vivian Lim 2023-04-23 04:18:25 -07:00
parent 6b852d0fa3
commit 279a6b763a
12 changed files with 394 additions and 182 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
public/images/s4s_start.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
public/images/unzipped.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -15,24 +15,24 @@ import ReactCrop, {
import { useDebounceEffect } from 'ahooks';
import 'react-image-crop/dist/ReactCrop.css'
import { AppShell, Aside, Button, Container, FileButton, Grid, Header, NativeSelect, Navbar, NavLink, NumberInput, Paper, Switch, TextInput, Title } from '@mantine/core';
import { cropImageToTargetDimensions } from './imageCropper';
import { AppShell, Aside, Button, Container, FileButton, Grid, Header, NativeSelect, Navbar, NavLink, NumberInput, Paper, Switch, Image, TextInput, Title, Text, Center, Tooltip, Mark, Anchor } from '@mantine/core';
import CropCanvas, { OutputImage } from './CropCanvas';
import WallHeightCropControl from './WallHeightCropControl';
import { AccumulatingAwaitableEvent } from './CustomAwaitableEvent';
import { ExportCropsEvent } from './Events';
import { downloadZipWithFiles } from './MakeZip';
const wallChoices = ["small", "medium", "tall"];
const wallChoices = ["short", "medium", "tall"];
const pageList = [
"1. Load image",
"2. Adjust crop",
"3. s4s"
"1. Introduction",
"2. Select an image",
"3. Adjust crop for wall heights",
"4. Import images into Sims4Studio",
"5. Adjust UV scale"
];
const wallSizes = {
small: 768,
short: 768,
medium: 1024,
tall: 1280
}
@ -43,7 +43,14 @@ interface WallDimensions {
height: number,
totalWidth: number,
tileCount: number,
diffuseUvScale: number,
diffuseUvScale: number,
}
interface WallUvScaleHelpInfo {
label: string,
width: number,
numTiles: number,
diffuseUvScale: number,
}
// This is to demonstate how to make and center a % aspect crop
@ -70,6 +77,7 @@ function centerAspectCrop(
export default function App() {
const [activePage, setActivePage] = useState(0)
const [unlockedPage, setUnlockedPage] = useState(1)
const [imgSrc, setImgSrc] = useState('')
const fullWidthCanvasRef = useRef<HTMLCanvasElement>(null)
const scrunchedCanvasRef = useRef<HTMLCanvasElement>(null)
@ -79,43 +87,27 @@ export default function App() {
const [aspect, setAspect] = useState<number | undefined>(16 / 9)
const [selectedWallSize, setWallSize] = useState(wallChoices[0])
const [numTiles, setNumTiles] = useState<number | ''>(3)
const [dimensions, setDimensions] = useState<WallDimensions>({height: wallSizes.small, totalWidth: wallTileWidth, tileCount: 1, diffuseUvScale: 0})
const [dimensions, setDimensions] = useState<WallDimensions>({ height: wallSizes.short, totalWidth: wallTileWidth, tileCount: 1, diffuseUvScale: 0 })
const [inputFile, setInputFile] = useState<File | null>(null)
const [debugMode, setDebugMode] = useState<boolean>(false)
function downloadCanvas(canvas: HTMLCanvasElement) {
canvas.toBlob((blob) => {
if (!blob) {
throw new Error('Failed to create blob')
}
if (blobUrlRef.current) {
URL.revokeObjectURL(blobUrlRef.current)
}
blobUrlRef.current = URL.createObjectURL(blob)
hiddenAnchorRef.current!.href = blobUrlRef.current
hiddenAnchorRef.current!.click()
})
}
function onDownloadFullWidthClick() {
if (!fullWidthCanvasRef.current) {
throw new Error('Crop canvas does not exist')
}
downloadCanvas(fullWidthCanvasRef.current);
}
const [showCalculations, setShowCalculations] = useState<boolean>(false)
const [allImages, setAllImages] = useState<OutputImage[]>([]);
const [scaleHelpInfo, setScaleHelpInfo] = useState<WallUvScaleHelpInfo[]>([]);
async function onDoCropClick() {
const allImages = await ExportCropsEvent.signalAndWaitForAllProcessors();
console.log(`got ${allImages.length} images`);
setAllImages(allImages);
await downloadZipWithFiles("wall.zip", allImages);
setActivePage(3);
setUnlockedPage(3);
}
React.useEffect(() => {
var height: number | undefined;
switch(selectedWallSize) {
case 'small':
height = wallSizes.small;
switch (selectedWallSize) {
case 'short':
height = wallSizes.short;
break;
case 'medium':
height = wallSizes.medium;
@ -127,12 +119,12 @@ export default function App() {
break;
}
if (height === undefined){
if (height === undefined) {
throw new Error("unknown wall size");
}
if (!(Number.isInteger(numTiles))){
if (!(Number.isInteger(numTiles))) {
throw new Error("numtiles isn't set");
}
@ -147,26 +139,55 @@ export default function App() {
var aspect = newDimensions.totalWidth / newDimensions.height;
setAspect(aspect);
}, [numTiles, selectedWallSize])
}, [numTiles, selectedWallSize])
React.useEffect(() => {
if (inputFile !== null){
if (inputFile !== null) {
const reader = new FileReader()
reader.addEventListener('load', () =>
setImgSrc(reader.result?.toString() || ''),
)
reader.readAsDataURL(inputFile)
setActivePage(2);
setUnlockedPage(2);
}
}, [inputFile])
React.useEffect(() => {
let newScaleHelpInfo: WallUvScaleHelpInfo[] = [];
for (const image of allImages) {
let label = "";
if (image.name.includes("short")) {
label = "short";
} else if (image.name.includes("medium")) {
label = "medium";
} else if (image.name.includes("tall")) {
label = "tall";
} else {
continue;
}
newScaleHelpInfo.push({
label: label,
width: image.width,
numTiles: image.width / wallTileWidth,
diffuseUvScale: 1.0 / (image.width / wallTileWidth)
});
setScaleHelpInfo(newScaleHelpInfo);
}
}, [allImages])
const navItems = pageList.map((page, index) => (
<NavLink
key={page}
active={index===activePage}
active={index === activePage}
label={page}
onClick={() => setActivePage(index)}
/>
disabled={index > unlockedPage && !debugMode}
/>
))
@ -175,118 +196,221 @@ export default function App() {
<AppShell
padding="md"
navbar={<Navbar width={{ base: 300 }} height={500} p="xs">
<Navbar.Section grow>
{navItems}
</Navbar.Section>
<Navbar.Section>
<Switch checked={debugMode} onChange={(event) => setDebugMode(event.currentTarget.checked)} label="Show extra debug controls" />
</Navbar.Section>
</Navbar>}
header={<Header height={60} p="xs">{/* Header content */}</Header>}
<Navbar.Section>
<Title order={4}>Steps</Title>
</Navbar.Section>
<Navbar.Section grow>
{navItems}
</Navbar.Section>
<Navbar.Section>
<Switch p="sm" checked={showCalculations} onChange={(event) => setShowCalculations(event.currentTarget.checked)} label="Show calculations" />
<Switch p="sm" checked={debugMode} onChange={(event) => setDebugMode(event.currentTarget.checked)} label="Show extra debug controls" />
</Navbar.Section>
</Navbar>}
header={<Header height={60} p="xs">
<Center>
<Title order={2}>🖼 multi-tile mural factory 🏭</Title>
</Center>
</Header>}
styles={(theme) => ({
main: { backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0] },
})}
>
{ activePage == 0 && (
{activePage == 0 && (
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">
<p>Hi! This is a tool to help with creating wallpapers for The Sims 4 that span across multiple tiles.</p>
<p>The technique used is a <Tooltip label="specifically, I found that I didn't need to create the resized versions of the textures" inline><Mark>slight modification</Mark></Tooltip> of <Anchor href="https://anachrosims.tumblr.com/post/641336721000333312/ts4-cc-tutorial-how-to-make-a-multi-tile-mural" target="_blank">anachrosims' "How to make a multi-tile mural" tutorial</Anchor>, which I recommend reading before continuing.</p>
</Text>
</Paper>
)}
{activePage == 1 && (
<>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">
Any files you upload never leave your computer; all processing happens within your browser.
</Text>
</Paper>
<FileButton
onChange={setInputFile}
accept="image/png,image.jpeg">
{(props) => <Button {...props}>Upload image</Button>}
</FileButton>
</>
)}
{ activePage == 1 && (
<Grid>
<Grid.Col span={2}>
<NativeSelect
label="Wall height"
data={wallChoices}
value={selectedWallSize}
onChange={(event) => setWallSize(event.currentTarget.value)}
/>
<NumberInput
value={numTiles}
label="Number of tiles"
onChange={setNumTiles}
/>
<NumberInput
value={dimensions.totalWidth}
label="total width"
readOnly={true}
/>
<NumberInput
value={dimensions.height}
label="height"
readOnly={true}
/>
<TextInput
value={dimensions.diffuseUvScale}
label="DiffuseUVScale"
readOnly={true}
/>
{activePage == 2 && (
<Grid>
<Grid.Col span={6}>
{!!imgSrc && (
<>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">For each different wall height, select how many tiles to use, and which portion of your image will be used.</Text>
</Paper>
<WallHeightCropControl
sectionLabel='Short walls'
helpLabel='Move the slider to choose how many tiles to use for short walls. Drag the image to select a portion of the image to use.'
imgSrc={imgSrc}
tileWidth={wallTileWidth}
tileHeight={wallSizes.short}
outputFileLabel="short.png"
accumulator={ExportCropsEvent}
showDebugControls={debugMode}
showCalculations={showCalculations}
/>
<WallHeightCropControl
sectionLabel='Medium walls'
helpLabel='Move the slider to choose how many tiles to use for medium walls. Drag the image to select a portion of the image to use.'
imgSrc={imgSrc}
tileWidth={wallTileWidth}
tileHeight={wallSizes.medium}
outputFileLabel="medium.png"
accumulator={ExportCropsEvent}
showDebugControls={debugMode}
showCalculations={showCalculations}
/>
<WallHeightCropControl
sectionLabel='Tall walls'
helpLabel='Move the slider to choose how many tiles to use for tall walls. Drag the image to select a portion of the image to use.'
imgSrc={imgSrc}
tileWidth={wallTileWidth}
tileHeight={wallSizes.tall}
outputFileLabel="tall.png"
accumulator={ExportCropsEvent}
showDebugControls={debugMode}
showCalculations={showCalculations}
/>
<div>
<Button onClick={onDownloadFullWidthClick}>Download fullWidth</Button>
<Button onClick={onDoCropClick}>do it</Button>
</div>
</Grid.Col>
<Grid.Col span={6}>
{!!imgSrc && (
<>
<WallHeightCropControl
sectionLabel='Short walls'
helpLabel='The portion of the image to use for short walls'
imgSrc={imgSrc}
tileWidth={wallTileWidth}
tileHeight={wallSizes.small}
outputFileLabel="small.png"
accumulator={ExportCropsEvent}
showDebugControls={debugMode}
/>
<WallHeightCropControl
sectionLabel='Medium walls'
helpLabel='The portion of the image to use for medium walls'
imgSrc={imgSrc}
tileWidth={wallTileWidth}
tileHeight={wallSizes.medium}
outputFileLabel="medium.png"
accumulator={ExportCropsEvent}
showDebugControls={debugMode}
/>
<WallHeightCropControl
sectionLabel='Tall walls'
helpLabel='The portion of the image to use for tall walls'
imgSrc={imgSrc}
tileWidth={wallTileWidth}
tileHeight={wallSizes.tall}
outputFileLabel="tall.png"
accumulator={ExportCropsEvent}
showDebugControls={debugMode}
/>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Title order={3}>Catalog thumbnail</Title>
<Text fz="m">Drag to select a portion of the image to use as a catalog thumbnail.</Text>
<Paper shadow="xs" p="md">
<Title order={3}>Catalog thumbnail</Title>
<CropCanvas
imgSrc={imgSrc}
aspect={1}
accumulator={ExportCropsEvent}
outputSpecs={[{
width: 116,
height: 116,
name: "thumbnail.png"
}]}
showDebugControls={debugMode}
/>
</Paper>
</>
)}
</Grid.Col>
</Grid>)}
{activePage == 2 && (
<div>
todo: instructions
</div>
)}
</AppShell>
</ThemeProvider>
<Center>
<Paper shadow="xs" m="xs" withBorder>
<CropCanvas
imgSrc={imgSrc}
aspect={1}
accumulator={ExportCropsEvent}
outputSpecs={[{
width: 116,
height: 116,
name: "thumbnail.png"
}]}
showDebugControls={debugMode}
/>
</Paper></Center>
</Paper>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">When you're ready, click this to continue:</Text>
<Button onClick={onDoCropClick}>Download a zip file containing these cropped images</Button>
</Paper>
</>
)}
</Grid.Col>
</Grid>)}
{activePage == 3 && (
<>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">
Extract the .zip file that you just downloaded. It should contain the cropped images you selected just now.
</Text>
<img src="/images/unzipped.jpg" alt="Screenshot of an extracted zip that contained medium.png, short.png, tall.png, and thumbnail.png"/>
</Paper>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">
Start Sims 4 Studio, make sure that "Standalone Recolor" is selected, and click Build.
</Text>
<img src="/images/s4s_start.jpg" alt="Screenshot of Sims 4 Studio's start screen, with an arrow pointing at the Build button"/>
</Paper>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">
Choose an appropriate wallpaper to base yours on. If you just want a flat color, one of the "Pure Expressions" options from the base game works nicely.
</Text>
<img src="/images/s4s_select_base.jpg" alt="Selecting the gray Pure Expressions wall covering"/>
<Text fz="lg">
After selecting one, click Next and save your new .package file.
</Text>
</Paper>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">
Change the Name and Description to something you like. Then, click the Texture tab.
</Text>
<img src="/images/s4s_texture.jpg" alt="Moving to the Texture tab in Sims 4 Studio"/>
<Text fz="xl"> Make sure that 'Autoresize Textures' is unchecked, or your texture will be stretched.</Text>
<Text fz="lg">
Now we'll import the images that you cropped earlier. First, click 'Short', then 'Import', and pick the 'short.png' that you extracted from the .zip.
</Text>
<Text fz="lg">
Repeat this for 'Medium' and 'medium.png', as well as 'Tall' and 'tall.png.'
</Text>
<Text fz="lg">
Also, click 'Import' within the 'Catalog Thumbnail' section and select 'thumbnail.png'. Afterwards, Sims 4 Studio should look like this:
</Text>
<img src="/images/s4s_imported.jpg" alt="Sims 4 Studio after importing the images."/>
<Text fz="lg">
The images you imported will appear squashed and repeating in the preview viewport. This is OK.
</Text>
</Paper>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Button onClick={() => setActivePage(4)}>Continue to adjusting the UV scale</Button>
</Paper>
</>
)}
{activePage == 4 && (
<>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Text fz="lg">
To make the textures display at the desired width, we need to adjust the
horizontal <Tooltip label="The texture which controls the visible color of an object" inline><Mark>diffuse</Mark></Tooltip> <Tooltip label="controls how the texture is applied to the surface of an object" inline><Mark>UV scale</Mark></Tooltip>.
</Text>
<Text fz="lg">
Click the 'Warehouse' tab in Sims 4 Studio.
</Text>
<img src="/images/s4s_warehouse.jpg" alt="Navigating to the Warehouse tab"/>
<Text fz="lg">
There are three Material Definitions defined, each of these controls how the material is displayed on a different wall height. The first is for short walls, the second is for medium walls, and the third is for tall walls.
</Text>
<Text fz="lg">
For each Material Definition, click it, and then click the 'Edit Items...' button on the left.
</Text>
<img src="/images/s4s_edituvscale.jpg" alt="Navigating to the Warehouse tab"/>
<Text fz="lg">
Now, click on the '<Tooltip label="Scale factors that control how the texture is applied to each wall segment." inline><Mark>DiffuseUVScale</Mark></Tooltip>' row on the left. The right side will change to allow you to modify DiffuseUVScale.
</Text>
<Text fz="lg">
Click the box next to the [0], and replace the number there with one of the following, depending on whether you are looking at the first, second, or third Material Definition:
</Text>
{
scaleHelpInfo.map(help => (
<Text fz="lg">
{help.label}: <i>1 ÷ {help.numTiles} tiles</i> = <b>{help.diffuseUvScale}</b>
</Text>
))
}
<Text fz="lg">
be sure to add color tags to make it easier to find ingame
</Text>
<Paper shadow="xs" p="md" m="sm" withBorder>
<Title order={4}>🤔 How does this work?</Title>
<Text fz="lg">
<p>
DiffuseUVScale is an <Tooltip label="Collection of values, usually more than one" inline><Mark>array</Mark></Tooltip> of two <Tooltip label="Numbers that can contain a decimal point and be fractions, not restricted to whole numbers." inline><Mark>floating-point numbers</Mark></Tooltip>. We're interested in the 0th <Tooltip label="A number that represents a specific spot within the array, where the counting starts at 0. 0 is the first spot, 1 is the second spot, and so on." inline><Mark>index</Mark></Tooltip>, which should be 1 currently. The value at that index is the horizontal scale.
</p>
<p>
A horizontal scale value of 1 means that for each wall tile, the texture should be used exactly once. If you were to set the value to 2, the texture would be repeated twice horizontally for each tile, which is the opposite of what we want.
</p>
<p>
By making the scale <i>less than</i> 1, the texture will be able to occupy more than one tile. For example, a scale of 0.5 would result in half of the texture being drawn on a single tile - and that leaves the other half of the texture to be drawn on the <i>next</i> tile.
</p>
</Text>
</Paper>
</Paper>
</>
)}
</AppShell>
</ThemeProvider>
)
}

View File

@ -21,7 +21,7 @@ import { useDebounceEffect } from 'ahooks';
// This is to demonstate how to make and center a % aspect crop
// which is a bit trickier so we use some helper functions.
function centerAspectCrop(
function defaultCrop(
mediaWidth: number,
mediaHeight: number,
aspect: number,
@ -30,7 +30,27 @@ function centerAspectCrop(
makeAspectCrop(
{
unit: '%',
width: 90,
height: 95,
},
aspect,
mediaWidth,
mediaHeight,
),
mediaWidth,
mediaHeight,
)
}
function defaultFillHeightCrop(
mediaWidth: number,
mediaHeight: number,
aspect: number,
) {
return centerCrop(
makeAspectCrop(
{
unit: '%',
height: 100,
},
aspect,
mediaWidth,
@ -50,6 +70,8 @@ export interface OutputImageSpec {
export interface OutputImage {
name: string,
blob: Blob,
width: number,
height: number,
}
interface CanvasProps {
@ -68,7 +90,7 @@ export default function CropCanvas({imgSrc, aspect, accumulator, outputSpecs, sh
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
if (aspect) {
const { width, height } = e.currentTarget
setCrop(centerAspectCrop(width, height, aspect))
setCrop(defaultCrop(width, height, aspect))
}
}
@ -111,7 +133,9 @@ export default function CropCanvas({imgSrc, aspect, accumulator, outputSpecs, sh
return {
name: spec.name,
blob: blob
blob: blob,
width: spec.width,
height: spec.height,
}
}
@ -131,11 +155,23 @@ export default function CropCanvas({imgSrc, aspect, accumulator, outputSpecs, sh
const height = imgRef.current?.height;
if (aspect && width && height) {
setCrop(centerAspectCrop(width, height, aspect))
setCrop(defaultCrop(width, height, aspect))
}
}, [aspect])
React.useEffect(() => {
// Reset crop if the requested aspect changes
const width = imgRef.current?.width;
const height = imgRef.current?.height;
if (crop && (crop.height == 0 || crop.width == 0) && width && height) {
setCrop(defaultFillHeightCrop(width, height, aspect))
}
}, [crop])
return (
<Paper shadow="xs" p="md">
<ReactCrop

View File

@ -8,7 +8,7 @@ import 'react-image-crop/dist/ReactCrop.css'
import 'react-image-crop/dist/ReactCrop.css'
import { Paper, Title, Text, Slider } from '@mantine/core';
import { Paper, Title, Text, Slider, NumberInput, TextInput, Flex, Center } from '@mantine/core';
import CropCanvas, { OutputImage } from './CropCanvas';
import { ExportCropsEvent } from './Events';
import { AccumulatingAwaitableEvent } from './CustomAwaitableEvent';
@ -22,13 +22,14 @@ interface WallHeightControlProps {
outputFileLabel: string,
accumulator: AccumulatingAwaitableEvent<OutputImage>,
showDebugControls: boolean,
showCalculations: boolean
}
export default function WallHeightCropControl({sectionLabel, helpLabel, imgSrc, tileWidth, tileHeight, outputFileLabel, accumulator, showDebugControls}: WallHeightControlProps) {
export default function WallHeightCropControl({ sectionLabel, helpLabel, imgSrc, tileWidth, tileHeight, outputFileLabel, accumulator, showDebugControls, showCalculations }: WallHeightControlProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const imgRef = useRef<HTMLImageElement>(null)
const [numTiles, setNumTiles] = useState<number>(3)
const smallCanvasRef = useRef<typeof CropCanvas>(null);
const shortCanvasRef = useRef<typeof CropCanvas>(null);
const outputWidth = (numTiles * tileWidth);
const outputHeight = tileHeight;
@ -37,37 +38,91 @@ export default function WallHeightCropControl({sectionLabel, helpLabel, imgSrc,
const numTilesLabel = (value: number) => `${value} tile${value > 1 ? 's' : ''}`
return (
<Paper shadow="xs" p="md">
<Paper shadow="xs" p="md" m="sm" withBorder>
<Title order={3}>{sectionLabel}</Title>
<Text fz="m">{helpLabel}</Text>
<Title order={5}>Number of tiles</Title>
<Slider
value={numTiles}
onChange={setNumTiles}
defaultValue={3}
min={1}
max={15}
label={(numTilesLabel)}
labelTransition="skew-down"
labelTransitionDuration={150}
labelTransitionTimingFunction="ease"
step={1}
<Slider
value={numTiles}
onChange={setNumTiles}
defaultValue={3}
min={1}
max={15}
label={(numTilesLabel)}
labelTransition="skew-down"
labelTransitionDuration={150}
labelTransitionTimingFunction="ease"
step={1}
/>
<Text fz="m">{`The selected area will span ${numTilesLabel(numTiles)} at this height. The resulting image will be ${outputWidth}px wide and ${outputHeight}px tall.`}</Text>
/>
<Text fz="m">{`The selected area will span ${numTilesLabel(numTiles)} at this height. The resulting image will be ${outputWidth}px wide and ${outputHeight}px tall.`}</Text>
<Center>
<Paper shadow="xs" m="xs" withBorder>
<CropCanvas
imgSrc={imgSrc}
aspect={aspectRatio}
accumulator={accumulator}
outputSpecs={[{
width: outputWidth,
height: outputHeight,
name: `${outputFileLabel}`
}]}
showDebugControls={showDebugControls}
/>
</Paper>
</Center>
{showCalculations && (
<>
<Paper shadow="xs" p="md">
<Flex
mih={80}
bg="rgba(0, 0, 0, .3)"
gap="md"
justify="center"
align="center"
direction="row"
wrap="wrap"
>
<TextInput
label="Tile width"
value={tileWidth}
readOnly />
<Text fz="xl">x</Text>
<TextInput
label="Number of tiles"
value={numTiles}
readOnly />
<Text fz="xl">=</Text>
<TextInput
label="Image width"
value={tileWidth * numTiles}
readOnly />
</Flex>
<Flex
mih={80}
bg="rgba(0, 0, 0, .3)"
gap="md"
justify="center"
align="center"
direction="row"
wrap="wrap"
>
<CropCanvas
imgSrc={imgSrc}
aspect={aspectRatio}
accumulator={accumulator}
outputSpecs={[{
width: outputWidth,
height: outputHeight,
name: `${outputFileLabel}`
}]}
showDebugControls={showDebugControls}
/>
<Text fz="xl">1 /</Text>
<TextInput
label="Number of tiles"
value={numTiles}
readOnly />
<Text fz="xl">=</Text>
<TextInput
label="Horizontal UV scale"
value={1.0 / numTiles}
readOnly />
</Flex>
</Paper>
</>
)}
</Paper>
)
}

View File

@ -21,10 +21,10 @@ export function cropImageToTargetDimensions(
// Crop is expressed in percents
const cropX = crop.x / 100;
const cropY = crop.y / 100;
const cropWidth = crop.width / 100;
const cropHeight = crop.height / 100;
let cropX = crop.x / 100;
let cropY = crop.y / 100;
let cropWidth = crop.width / 100;
let cropHeight = crop.height / 100;
canvas.width = targetWidth;
canvas.height = height;

View File

@ -1,6 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;