mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
8 Commits
fix/cordov
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b0c74ed29f | ||
![]() |
de041e469c | ||
![]() |
d18615c43c | ||
![]() |
56bba125b1 | ||
![]() |
b36c65ea65 | ||
![]() |
f70adcf382 | ||
![]() |
33d26908ee | ||
![]() |
36aca017fb |
@@ -1,33 +1,13 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import optionalRequire from '../Utils/OptionalRequire';
|
|
||||||
|
|
||||||
const fs = optionalRequire('fs-extra');
|
|
||||||
|
|
||||||
export const getImageFromPath = (path: string): Promise<HTMLImageElement> => {
|
|
||||||
const imageElement = document.createElement('img');
|
|
||||||
|
|
||||||
const file = fs.readFileSync(path, { encoding: 'base64' });
|
|
||||||
|
|
||||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
|
||||||
imageElement.addEventListener('error', (event: Event) => {
|
|
||||||
reject(event);
|
|
||||||
});
|
|
||||||
imageElement.addEventListener('load', () => {
|
|
||||||
resolve(imageElement);
|
|
||||||
});
|
|
||||||
imageElement.src = `data:image/png;base64,${file}`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resizeImage = (
|
export const resizeImage = (
|
||||||
image: HTMLImageElement,
|
imageAsBlobDataUrl: string,
|
||||||
outputFile: string,
|
|
||||||
{
|
{
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
transparentBorderSize = 0,
|
transparentBorderSize = 0,
|
||||||
}: {| width: number, height: number, transparentBorderSize?: number |}
|
}: {| width: number, height: number, transparentBorderSize?: number |}
|
||||||
): Promise<boolean> => {
|
): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const canvasElement = document.createElement('canvas');
|
const canvasElement = document.createElement('canvas');
|
||||||
canvasElement.width = width;
|
canvasElement.width = width;
|
||||||
@@ -36,20 +16,27 @@ export const resizeImage = (
|
|||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
ctx.imageSmoothingEnabled = true;
|
||||||
ctx.imageSmoothingQuality = 'high';
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
const image = new Image();
|
||||||
|
image.addEventListener('load', () => {
|
||||||
|
try {
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
transparentBorderSize,
|
||||||
|
transparentBorderSize,
|
||||||
|
width - 2 * transparentBorderSize,
|
||||||
|
height - 2 * transparentBorderSize
|
||||||
|
);
|
||||||
|
|
||||||
ctx.drawImage(
|
canvasElement.toBlob(blob => {
|
||||||
image,
|
resolve(URL.createObjectURL(blob));
|
||||||
transparentBorderSize,
|
}, 'image/png');
|
||||||
transparentBorderSize,
|
} catch (error) {
|
||||||
width - 2 * transparentBorderSize,
|
reject('An error occurred while generating an icon');
|
||||||
height - 2 * transparentBorderSize
|
}
|
||||||
);
|
});
|
||||||
|
image.addEventListener('error', (e: Event) => {
|
||||||
canvasElement.toBlob(blob => {
|
reject('An error occurred while loading the input image');
|
||||||
blob.arrayBuffer().then(buffer => {
|
});
|
||||||
fs.writeFileSync(outputFile, Buffer.from(buffer));
|
image.src = imageAsBlobDataUrl;
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
}, 'image/png');
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -5,21 +5,18 @@ import { I18n } from '@lingui/react';
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import FlatButton from '../UI/FlatButton';
|
import FlatButton from '../UI/FlatButton';
|
||||||
import RaisedButton from '../UI/RaisedButton';
|
|
||||||
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
|
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
|
||||||
import { Line } from '../UI/Grid';
|
import { Line } from '../UI/Grid';
|
||||||
import ResourcesLoader from '../ResourcesLoader';
|
import ResourcesLoader from '../ResourcesLoader';
|
||||||
import ResourceSelectorWithThumbnail from '../ResourcesList/ResourceSelectorWithThumbnail';
|
import ResourceSelectorWithThumbnail from '../ResourcesList/ResourceSelectorWithThumbnail';
|
||||||
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||||
import { getImageFromPath, resizeImage } from './ImageResizer';
|
import { resizeImage } from './ImageResizer';
|
||||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||||
import optionalRequire from '../Utils/OptionalRequire';
|
|
||||||
import Text from '../UI/Text';
|
import Text from '../UI/Text';
|
||||||
import { ColumnStackLayout } from '../UI/Layout';
|
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout';
|
||||||
import AlertMessage from '../UI/AlertMessage';
|
import AlertMessage from '../UI/AlertMessage';
|
||||||
import ErrorBoundary from '../UI/ErrorBoundary';
|
import ErrorBoundary from '../UI/ErrorBoundary';
|
||||||
|
|
||||||
const path = optionalRequire('path');
|
|
||||||
const gd: libGDevelop = global.gd;
|
const gd: libGDevelop = global.gd;
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
@@ -70,6 +67,8 @@ const iosSizes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
class PlatformSpecificAssetsDialog extends React.Component<Props, State> {
|
class PlatformSpecificAssetsDialog extends React.Component<Props, State> {
|
||||||
|
inputRef: HTMLInputElement | null = null;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = this._loadFrom(props.project);
|
this.state = this._loadFrom(props.project);
|
||||||
@@ -105,118 +104,107 @@ class PlatformSpecificAssetsDialog extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateFromFile = async () => {
|
_generateFromFile = async e => {
|
||||||
const { project, resourceManagementProps } = this.props;
|
if (!this.inputRef || !this.inputRef.files || !this.inputRef.files[0]) {
|
||||||
|
console.error("Could't find selected file. Aborting icon generation.");
|
||||||
const resourceSource = resourceManagementProps.resourceSources
|
|
||||||
.filter(source => source.kind === 'image')
|
|
||||||
.filter(source => source.name.startsWith('local-file-opener'))[0];
|
|
||||||
|
|
||||||
if (!resourceSource) {
|
|
||||||
throw new Error(
|
|
||||||
'No supported resource source - only local files are supported.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resources = await resourceManagementProps.onChooseResource({
|
|
||||||
initialSourceName: resourceSource.name,
|
|
||||||
multiSelection: false,
|
|
||||||
resourceKind: 'image',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resources.length || !path) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const chosenFileAsBlobDataUrl = URL.createObjectURL(this.inputRef.files[0]);
|
||||||
|
const { project, resourceManagementProps } = this.props;
|
||||||
|
|
||||||
const resourcesManager = project.getResourcesManager();
|
const resourcesManager = project.getResourcesManager();
|
||||||
const projectPath = path.dirname(project.getProjectFile());
|
|
||||||
const fullPath = path.resolve(projectPath, resources[0].getFile());
|
|
||||||
|
|
||||||
const image = await getImageFromPath(fullPath);
|
try {
|
||||||
|
const results = await Promise.all([
|
||||||
|
...desktopSizes.map(async size => ({
|
||||||
|
resourceName: `desktop-icon-${size}.png`,
|
||||||
|
blobDataUrl: await resizeImage(chosenFileAsBlobDataUrl, {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
...androidSizes.map(async size => ({
|
||||||
|
resourceName: `android-icon-${size}.png`,
|
||||||
|
blobDataUrl: await resizeImage(chosenFileAsBlobDataUrl, {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
(async () => ({
|
||||||
|
resourceName: 'android-windowSplashScreenAnimatedIcon.png',
|
||||||
|
blobDataUrl: await resizeImage(chosenFileAsBlobDataUrl, {
|
||||||
|
width: androidWindowSplashScreenAnimatedIconRecommendedSize,
|
||||||
|
height: androidWindowSplashScreenAnimatedIconRecommendedSize,
|
||||||
|
transparentBorderSize:
|
||||||
|
androidWindowSplashScreenAnimatedIconRecommendedSize / 6,
|
||||||
|
}),
|
||||||
|
}))(),
|
||||||
|
...iosSizes.map(async size => ({
|
||||||
|
resourceName: `ios-icon-${size}.png`,
|
||||||
|
blobDataUrl: await resizeImage(chosenFileAsBlobDataUrl, {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
// Important, we are responsible for deleting the resources that were given to us.
|
results.forEach(({ resourceName, blobDataUrl }) => {
|
||||||
// Otherwise we have a memory leak.
|
const metadata = JSON.stringify({
|
||||||
resources.forEach(resource => resource.delete());
|
extension: '.png',
|
||||||
|
// TODO: Check it works on Windows
|
||||||
|
localFilePath: `./assets/${resourceName}`,
|
||||||
|
});
|
||||||
|
if (!resourcesManager.hasResource(resourceName)) {
|
||||||
|
const imageResource = new gd.ImageResource();
|
||||||
|
imageResource.setFile(blobDataUrl);
|
||||||
|
imageResource.setName(resourceName);
|
||||||
|
imageResource.setMetadata(metadata);
|
||||||
|
|
||||||
const results = await Promise.all([
|
resourcesManager.addResource(imageResource);
|
||||||
...desktopSizes.map(size =>
|
|
||||||
resizeImage(image, path.join(projectPath, `desktop-icon-${size}.png`), {
|
// Important, we are responsible for deleting the resources that we created
|
||||||
width: size,
|
// Otherwise we have a memory leak, as calling addResource is making a copy of the resource.
|
||||||
height: size,
|
imageResource.delete();
|
||||||
})
|
} else {
|
||||||
),
|
resourcesManager.getResource(resourceName).setFile(blobDataUrl);
|
||||||
...androidSizes.map(size =>
|
resourcesManager.getResource(resourceName).setMetadata(metadata);
|
||||||
resizeImage(image, path.join(projectPath, `android-icon-${size}.png`), {
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
resizeImage(
|
|
||||||
image,
|
|
||||||
path.join(projectPath, 'android-windowSplashScreenAnimatedIcon.png'),
|
|
||||||
{
|
|
||||||
width: androidWindowSplashScreenAnimatedIconRecommendedSize,
|
|
||||||
height: androidWindowSplashScreenAnimatedIconRecommendedSize,
|
|
||||||
transparentBorderSize:
|
|
||||||
androidWindowSplashScreenAnimatedIconRecommendedSize / 6,
|
|
||||||
}
|
}
|
||||||
),
|
});
|
||||||
...iosSizes.map(size =>
|
|
||||||
resizeImage(image, path.join(projectPath, `ios-icon-${size}.png`), {
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (results.indexOf(false) !== -1) {
|
await resourceManagementProps.onFetchNewlyAddedResources();
|
||||||
|
|
||||||
|
// Make sure the resources are (re)loaded.
|
||||||
|
ResourcesLoader.burstUrlsCacheForResources(
|
||||||
|
project,
|
||||||
|
results.map(({ resourceName }) => resourceName)
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({
|
||||||
|
desktopIconResourceNames: desktopSizes.map(
|
||||||
|
size => `desktop-icon-${size}.png`
|
||||||
|
),
|
||||||
|
androidIconResourceNames: androidSizes.map(
|
||||||
|
size => `android-icon-${size}.png`
|
||||||
|
),
|
||||||
|
androidWindowSplashScreenAnimatedIconResourceName:
|
||||||
|
'android-windowSplashScreenAnimatedIcon.png',
|
||||||
|
iosIconResourceNames: iosSizes.map(size => `ios-icon-${size}.png`),
|
||||||
|
});
|
||||||
|
}, 200 /* Let a bit of time so that image files can be found */);
|
||||||
|
} catch (e) {
|
||||||
showErrorBox({
|
showErrorBox({
|
||||||
message: 'Some icons could not be generated!',
|
message: 'Some icons could not be generated.',
|
||||||
rawError: undefined,
|
rawError: undefined,
|
||||||
errorId: 'icon-generation-error',
|
errorId: 'icon-generation-error',
|
||||||
doNotReport: true,
|
doNotReport: true,
|
||||||
});
|
});
|
||||||
return;
|
} finally {
|
||||||
}
|
// Reset input value so that if the user selects the same file again,
|
||||||
|
// the onChange callback is called.
|
||||||
// Add resources to the game
|
if (this.inputRef) {
|
||||||
const allResourcesNames = [
|
this.inputRef.value = '';
|
||||||
...desktopSizes.map(size => `desktop-icon-${size}.png`),
|
|
||||||
...androidSizes.map(size => `android-icon-${size}.png`),
|
|
||||||
'android-windowSplashScreenAnimatedIcon.png',
|
|
||||||
...iosSizes.map(size => `ios-icon-${size}.png`),
|
|
||||||
];
|
|
||||||
allResourcesNames.forEach(resourceName => {
|
|
||||||
if (!resourcesManager.hasResource(resourceName)) {
|
|
||||||
const imageResource = new gd.ImageResource();
|
|
||||||
imageResource.setFile(resourceName);
|
|
||||||
imageResource.setName(resourceName);
|
|
||||||
|
|
||||||
resourcesManager.addResource(imageResource);
|
|
||||||
|
|
||||||
// Important, we are responsible for deleting the resources that we created
|
|
||||||
// Otherwise we have a memory leak, as calling addResource is making a copy of the resource.
|
|
||||||
imageResource.delete();
|
|
||||||
} else {
|
|
||||||
resourcesManager.getResource(resourceName).setFile(resourceName);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Make sure the resources are (re)loaded.
|
|
||||||
ResourcesLoader.burstUrlsCacheForResources(project, allResourcesNames);
|
|
||||||
setTimeout(() => {
|
|
||||||
this.setState({
|
|
||||||
desktopIconResourceNames: desktopSizes.map(
|
|
||||||
size => `desktop-icon-${size}.png`
|
|
||||||
),
|
|
||||||
androidIconResourceNames: androidSizes.map(
|
|
||||||
size => `android-icon-${size}.png`
|
|
||||||
),
|
|
||||||
androidWindowSplashScreenAnimatedIconResourceName:
|
|
||||||
'android-windowSplashScreenAnimatedIcon.png',
|
|
||||||
iosIconResourceNames: iosSizes.map(size => `ios-icon-${size}.png`),
|
|
||||||
});
|
|
||||||
}, 200 /* Let a bit of time so that image files can be found */);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onApply = () => {
|
onApply = () => {
|
||||||
@@ -297,22 +285,20 @@ class PlatformSpecificAssetsDialog extends React.Component<Props, State> {
|
|||||||
onApply={this.onApply}
|
onApply={this.onApply}
|
||||||
>
|
>
|
||||||
<ColumnStackLayout noMargin>
|
<ColumnStackLayout noMargin>
|
||||||
<Line justifyContent="center" noMargin>
|
<ResponsiveLineStackLayout
|
||||||
{!!path ? (
|
alignItems="center"
|
||||||
<RaisedButton
|
noMargin
|
||||||
primary
|
noColumnMargin
|
||||||
label={<Trans>Generate icons from a file</Trans>}
|
>
|
||||||
onClick={this._generateFromFile}
|
<Text noMargin>
|
||||||
/>
|
<Trans>Generate icons from a file:</Trans>
|
||||||
) : (
|
</Text>
|
||||||
<Text>
|
<input
|
||||||
<Trans>
|
type="file"
|
||||||
Download GDevelop desktop version to generate the Android and
|
onChange={this._generateFromFile}
|
||||||
iOS icons of your game.
|
ref={_inputRef => (this.inputRef = _inputRef)}
|
||||||
</Trans>
|
/>
|
||||||
</Text>
|
</ResponsiveLineStackLayout>
|
||||||
)}
|
|
||||||
</Line>
|
|
||||||
<Text size="sub-title">
|
<Text size="sub-title">
|
||||||
<Trans>gd.games thumbnail</Trans>
|
<Trans>gd.games thumbnail</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
Reference in New Issue
Block a user