"use client"; import { type ReactNode, useEffect, useRef, useState } from "react"; type PreviewState = | { status: "loading" } | { status: "ready" } | { status: "error"; message: string }; function loadImage(src: string): Promise { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error("The sample image could not be loaded. Try again")); image.src = src; }); } export default function MockupExample({ assetsBase = "/posts/making-an-mockup-with-pure-canvas/images", initialPreview, }: { assetsBase?: string; initialPreview?: ReactNode; }) { const canvasRef = useRef(null); const [started, setStarted] = useState(false); const [background, setBackground] = useState("#161616"); const [attempt, setAttempt] = useState(0); const [state, setState] = useState({ status: "loading" }); const [exportError, setExportError] = useState(""); useEffect(() => { if (!started) return; let cancelled = false; setState({ status: "loading" }); setExportError(""); async function draw() { try { const [screenshot, bezel] = await Promise.all([ loadImage( `${assetsBase}/iphone16_pro_lock_screen.png?attempt=${attempt}`, ), loadImage( `${assetsBase}/iphone16_pro_whitetitanium.png?attempt=${attempt}`, ), ]); if (cancelled) return; const canvas = canvasRef.current; if (!canvas) return; const context = canvas.getContext("2d"); if (!context) throw new Error("Your browser could not create the preview"); canvas.width = 1600; canvas.height = 1800; context.fillStyle = background; context.fillRect(0, 0, canvas.width, canvas.height); // Coordinates belong to the 1350 × 2760 bezel included with this example. const scale = Math.min( 1200 / bezel.naturalWidth, 1500 / bezel.naturalHeight, ); const x = (canvas.width - bezel.naturalWidth * scale) / 2; const y = (canvas.height - bezel.naturalHeight * scale) / 2; context.save(); context.translate(x, y); context.scale(scale, scale); context.beginPath(); context.roundRect(72, 62, 1206, 2636, 170); context.clip(); // Fill the screen without stretching; crop excess around the center. const fit = Math.max( 1206 / screenshot.naturalWidth, 2636 / screenshot.naturalHeight, ); const width = screenshot.naturalWidth * fit; const height = screenshot.naturalHeight * fit; context.drawImage( screenshot, 72 + (1206 - width) / 2, 62 + (2636 - height) / 2, width, height, ); context.restore(); context.drawImage( bezel, x, y, bezel.naturalWidth * scale, bezel.naturalHeight * scale, ); setState({ status: "ready" }); } catch (error) { if (!cancelled) setState({ status: "error", message: error instanceof Error ? error.message : "The preview could not be drawn", }); } } void draw(); return () => { cancelled = true; }; }, [assetsBase, background, attempt, started]); function download() { setExportError(""); const canvas = canvasRef.current; if (!canvas || state.status !== "ready") return; try { canvas.toBlob((blob) => { if (!blob) { setExportError("The image could not be exported. Try again"); return; } const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "iphone-mockup.png"; document.body.append(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); }, "image/png"); } catch { setExportError( "The image could not be exported. Use images hosted with your page and try again", ); } } return (
{!started && (
{initialPreview}
)} An iPhone mockup preview. Read the steps below for the full drawing process.
{state.status === "error" && ( )} {state.status === "loading" ? "Preparing preview" : state.status === "ready" ? "Ready to download" : state.message} {exportError &&

{exportError}

}
); }