Skip to main content
Version: 0.x.x

Image Loader

The engine includes a built-in ImageLoader utility for asynchronous image loading, caching, and deduplication. Access it through engine.images.

Features

  • In-Memory Caching: Loaded images are stored in memory. Subsequent requests return instantly.
  • Request Deduplication: Multiple calls for the same URL share a single network request.
  • Automatic Retries: Failed loads are automatically retried (default: 3 times).

Loading Images

Use engine.loadImage() when engine.isReady is true:

function MapWithImages() {
const engine = useCanvasTileEngine();
const [images, setImages] = useState<Record<string, HTMLImageElement>>({});

useEffect(() => {
if (!engine.isReady) return;

const loadImages = async () => {
const tree = await engine.loadImage("/assets/tree.png");
const rock = await engine.loadImage("/assets/rock.png");
setImages({ tree, rock });
};

loadImages();
}, [engine.isReady]);

const imageItems = useMemo(
() =>
[
{ x: 5, y: 5, size: 1, img: images.tree },
{ x: 8, y: 3, size: 0.8, img: images.rock },
].filter((item) => item.img),
[images]
);

return (
<CanvasTileEngine engine={engine} config={config}>
<CanvasTileEngine.GridLines cellSize={1} />
{imageItems.length > 0 && <CanvasTileEngine.Image items={imageItems} layer={2} />}
</CanvasTileEngine>
);
}

Preloading Multiple Images

Load all assets in parallel before rendering:

function MapWithPreloadedAssets() {
const engine = useCanvasTileEngine();
const [loaded, setLoaded] = useState(false);
const [assets, setAssets] = useState<Record<string, HTMLImageElement>>({});

useEffect(() => {
if (!engine.isReady) return;

const preload = async () => {
const urls = {
grass: "/assets/grass.png",
water: "/assets/water.png",
tree: "/assets/tree.png",
house: "/assets/house.png",
};

const entries = await Promise.all(
Object.entries(urls).map(async ([key, url]) => {
const img = await engine.loadImage(url);
return [key, img] as const;
})
);

setAssets(Object.fromEntries(entries));
setLoaded(true);
};

preload();
}, [engine.isReady]);

if (!loaded) {
return <div>Loading assets...</div>;
}

return (
<CanvasTileEngine engine={engine} config={config}>
{/* Use assets.grass, assets.water, etc. */}
</CanvasTileEngine>
);
}

Custom Hook for Image Loading

Create a reusable hook for image loading:

function useLoadedImage(engine: EngineHandle, url: string) {
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
if (!engine.isReady) return;

setLoading(true);
setError(null);

engine
.loadImage(url)
.then(setImage)
.catch(setError)
.finally(() => setLoading(false));
}, [engine.isReady, url]);

return { image, loading, error };
}

// Usage - component must be rendered inside CanvasTileEngine
function TreeMarker({ engine, x, y }: { engine: EngineHandle; x: number; y: number }) {
const { image, loading } = useLoadedImage(engine, "/assets/tree.png");

if (loading || !image) return null;

return <CanvasTileEngine.Image items={{ x, y, size: 1, img: image }} layer={2} />;
}

// In parent component:
function App() {
const engine = useCanvasTileEngine();

return (
<CanvasTileEngine engine={engine} config={config}>
<TreeMarker engine={engine} x={5} y={10} />
</CanvasTileEngine>
);
}

Complete Example: Rendering a Map

interface MapObject {
id: string;
x: number;
y: number;
type: "tree" | "rock" | "house";
}

const ASSET_URLS: Record<string, string> = {
tree: "/assets/tree.png",
rock: "/assets/rock.png",
house: "/assets/house.png",
};

function GameMap({ objects }: { objects: MapObject[] }) {
const engine = useCanvasTileEngine();
const [assets, setAssets] = useState<Record<string, HTMLImageElement>>({});
const [ready, setReady] = useState(false);

// Load all required assets
useEffect(() => {
if (!engine.isReady) return;

const requiredTypes = [...new Set(objects.map((o) => o.type))];

Promise.all(
requiredTypes.map(async (type) => {
const img = await engine.loadImage(ASSET_URLS[type]);
return [type, img] as const;
})
).then((entries) => {
setAssets(Object.fromEntries(entries));
setReady(true);
});
}, [engine.isReady, objects]);

// Build image items
const imageItems = useMemo(() => {
if (!ready) return [];

return objects.map((obj) => ({
x: obj.x,
y: obj.y,
size: 1,
img: assets[obj.type],
}));
}, [objects, assets, ready]);

const config = {
scale: 50,
size: { width: 800, height: 600 },
eventHandlers: { drag: true, zoom: true },
};

return (
<CanvasTileEngine engine={engine} config={config}>
<CanvasTileEngine.GridLines cellSize={1} strokeStyle="rgba(0,0,0,0.1)" />
{ready && <CanvasTileEngine.Image items={imageItems} layer={2} />}
</CanvasTileEngine>
);
}

With Static Caching

For static terrain or decorations, combine with <StaticImage>:

function TerrainMap({ tiles }: { tiles: TileData[] }) {
const engine = useCanvasTileEngine();
const [tileImages, setTileImages] = useState<Record<string, HTMLImageElement>>({});
const [ready, setReady] = useState(false);

useEffect(() => {
if (!engine.isReady) return;

const types = [...new Set(tiles.map((t) => t.type))];

Promise.all(
types.map(async (type) => {
const img = await engine.loadImage(`/assets/terrain/${type}.png`);
return [type, img] as const;
})
).then((entries) => {
setTileImages(Object.fromEntries(entries));
setReady(true);
});
}, [engine.isReady, tiles]);

const terrainItems = useMemo(() => {
if (!ready) return [];

return tiles.map((tile) => ({
x: tile.x,
y: tile.y,
size: 1,
img: tileImages[tile.type],
}));
}, [tiles, tileImages, ready]);

return (
<CanvasTileEngine engine={engine} config={config}>
{ready && <CanvasTileEngine.StaticImage items={terrainItems} cacheKey="terrain" layer={0} />}
</CanvasTileEngine>
);
}
Performance Tips
  • Preload assets: Load all images before first render to avoid flicker
  • Use useMemo: Memoize image item arrays to prevent unnecessary re-renders
  • Batch similar images: Group items by image type for better cache utilization
  • Static for terrain: Use <StaticImage> for non-changing terrain tiles