DEV Community

Cover image for AI Tattoo Generator: How I Built a Placement-Aware Design Workflow
TuTuTu
TuTuTu

Posted on

AI Tattoo Generator: How I Built a Placement-Aware Design Workflow

The obvious architecture for an AI image product looks almost too simple:

form -> prompt -> image model -> result
Enter fullscreen mode Exit fullscreen mode

That is enough for a demo. It was not enough for the tattoo planning product I
wanted to build.

A generic image model can make a beautiful square illustration while ignoring
the things that make a tattoo concept useful: body placement, readable line
weight, negative space, silhouette, scale, privacy, and a clean handoff to a
professional artist.

An AI tattoo generator is a tool that turns a written idea and design
constraints into a tattoo concept. The important word is concept. The output
can help someone explore a direction and communicate it, but it is not a final
stencil or a substitute for an artist adapting the design to real skin.

This post explains the production decisions behind the placement-aware
workflow I built for
TattooIdeas.app. I am part of
the team behind the product, so this is an engineering retrospective, not an
independent review.

The real workflow is longer than one model call

The product journey eventually became:

Explore -> Describe -> Constrain -> Generate -> Refine -> Preview -> Artist
Enter fullscreen mode Exit fullscreen mode

Each arrow hides a different problem. The model needs a precise brief. The
request needs to survive a long-running job. The output must become a private,
durable project file. The artwork needs to work in a placement preview. And the
user needs to understand where software stops and professional judgment begins.

The current stack looks like this:

React 19 UI
  -> TanStack Start server function
     -> Cloudflare Worker
        -> D1 job record + credit ledger
        -> image provider queue
        <- signed webhook / status reconciliation
        -> private result copied to R2
  <- TanStack Query refreshes the job state
Enter fullscreen mode Exit fullscreen mode

TanStack Start gives the app server rendering and typed server functions.
Cloudflare Workers runs the application layer, D1 stores jobs and project
metadata, and R2 owns the generated files. The image provider performs the
expensive inference, but it does not own the product state.

Here are the lessons that made the largest difference.

1. Compile a prompt instead of concatenating one

A single textarea is flexible, but it leaves too many decisions implicit.
“A crane tattoo” does not say whether the user wants fine-line or blackwork,
an inner-forearm composition or a back piece, restrained detail or dense
ornament, black ink or color.

I model those decisions as typed inputs:

type TattooPromptInput = {
  prompt: string;
  style: TattooStyleId;
  placement: TattooPlacementId;
  color: TattooColorId;
  complexity: TattooComplexityId;
  aspectRatio: 'auto' | '1:1' | '2:3' | '3:2';
};
Enter fullscreen mode Exit fullscreen mode

The UI collects them separately, and a prompt compiler turns them into one
model brief. A simplified version looks like this:

function buildTattooPrompt(input: TattooPromptInput) {
  const idea = encodeUserIdea(input.prompt);
  const ratio = getTattooAspectRatio(
    input.placement,
    input.aspectRatio
  );

  return [
    'Create exactly one original tattoo design concept.',
    `<USER_IDEA>${idea}</USER_IDEA>`,
    `STYLE: ${getStyleDirection(input.style)}`,
    `PLACEMENT: ${getPlacementDirection(input.placement)}`,
    `COLOR: ${getColorDirection(input.color)}`,
    `DETAIL: ${getComplexityDirection(input.complexity)}`,
    `COMPOSITION: use a ${ratio} canvas with a complete silhouette`,
    'Use purposeful line weights, durable contrast, and negative space.',
    'Show isolated artwork on a plain background. Do not render skin.',
  ].join('\n\n');
}
Enter fullscreen mode Exit fullscreen mode

There are two useful ideas here.

First, structured controls make the request reproducible. If a user changes
only the style, the rest of the creative brief remains stable. That makes
comparison and refinement much easier.

Second, user text is data, not authority. I escape it, place it inside a clear
boundary, and explicitly tell the model how to interpret conflicts. Boundary
tags are not a security mechanism by themselves, but they reduce accidental
instruction mixing when combined with validation and a fixed higher-priority
brief.

2. Placement should change composition, not decorate metadata

Many AI image forms include a “body part” dropdown and then barely use it. I
found placement useful only when it affected the geometry of the requested
artwork.

An inner forearm usually benefits from a vertical, readable silhouette. A
shoulder can support a more radial composition. A sleeve needs continuity and
flow rather than a centered sticker. Placement can therefore choose a default
aspect ratio and add composition guidance before the request reaches the
model.

function getTattooAspectRatio(placement, requested = 'auto') {
  if (requested !== 'auto') return requested;
  return placementOptions.find((item) => item.id === placement)?.aspectRatio
    ?? '1:1';
}
Enter fullscreen mode Exit fullscreen mode

This does not make the model understand anatomy. It simply turns a vague label
into useful constraints: dominant direction, visual weight, focal hierarchy,
margin, and crop safety.

Three crane and moon tattoo concepts shown in fine-line, blackwork, and Japanese-inspired visual directions

One subject can produce very different concepts when style and composition are explicit rather than buried in a long free-form prompt.

The image above also shows why I prefer controlled variation to random
regeneration. A useful comparison keeps the subject recognizable while
changing one meaningful dimension.

3. Long-running generation is a state machine

The first production problem has little to do with image quality. Generation
takes long enough for requests to time out, tabs to close, providers to retry
webhooks, and users to click twice.

I store a job before submitting inference and treat these states as part of
the product:

queued -> processing -> succeeded
                    \-> failed
                    \-> canceled
Enter fullscreen mode Exit fullscreen mode

The start endpoint validates the input, reserves a job, records the charge,
and submits the provider request in background work. A webhook can reconcile
the result, while the client can also request status if the webhook is late.
Both paths call the same reconciliation logic.

The important detail is idempotency. Charging, refunding, and persisting output
must all be safe when the same event arrives more than once.

if (providerStatus === 'failed' || providerStatus === 'canceled') {
  await refundGenerationCredits({
    userId,
    generationId: jobId,
    amount: job.creditCost,
  });
  await markJobTerminal(jobId, providerStatus);
}
Enter fullscreen mode Exit fullscreen mode

In the real implementation, the credit ledger uses a stable generation ID so
a repeated failure event cannot create a repeated refund. Abandoned jobs are
also reconciled after a timeout. “The provider failed” should be a recoverable
state, not a support ticket.

4. A provider URL is not your storage layer

Image APIs commonly return temporary delivery URLs. Saving that URL in a
database is tempting, but it makes retention, access control, and deletion
someone else's responsibility.

When a result succeeds, the Worker:

  1. accepts only an HTTPS output URL;
  2. verifies the response type is JPEG, PNG, or WebP;
  3. enforces declared and actual byte limits;
  4. copies the image to a private R2 object;
  5. creates file and design records owned by the user; and
  6. returns an authenticated application URL instead of the provider URL.

That extra copy is where a transient model output becomes a product artifact.
It also means the app can implement deletion and access rules consistently
across multiple inference providers.

This ownership boundary is especially important for image-to-image workflows.
A pet photo, a handwritten note, or a photo of an existing tattoo may be far
more sensitive than a text prompt. Generated projects in this app are private
by default, and the UI states when an uploaded source is stored versus
processed locally.

5. Placement preview is a graphics problem of its own

Generating isolated artwork and making it look plausible on a curved body are
different tasks. I deliberately keep them separate.

The first step happens in the browser. Generated artwork often has a white
background, but removing every light pixel would also erase intentional white
highlights inside the design. Instead, a flood fill removes only a light,
neutral region connected to the image border, with a soft alpha transition at
the edge.

The preview renderer then combines four textures:

  • the tattoo artwork;
  • a depth map;
  • a skin mask; and
  • a body-surface map.

A small WebGL shader adjusts the artwork using local depth gradients and
surface continuity. Users can move, scale, rotate, and change opacity without
running another AI request. If WebGL or a texture fails, the interface falls
back to a simpler CSS contour transform instead of making the whole workflow
unavailable.

A side-by-side forearm preview with bare skin on the left and a botanical snake tattoo concept on the right

A placement preview is useful for comparing scale and direction. It is not a simulation of healing, ink spread, or how a specific body moves.

The fallback matters more than the shader. An advanced preview is an
enhancement; it should never block someone from downloading a concept or
continuing the project.

6. The model output needs a human last mile

The most important product decision was refusing to describe the result as
“tattoo-ready.”

A screen cannot inspect skin, choose a needle grouping, predict healing, or
decide how fine detail will age at a particular size. A professional tattoo
artist still needs to redraw and adapt the concept for anatomy and technique.

The generator can make that conversation better. A useful artist handoff
includes:

  • the meaning and must-have elements;
  • the intended body area and approximate size;
  • two or three visual directions rather than one demanded copy;
  • notes about preferred line weight, contrast, and color; and
  • explicit permission for the artist to simplify and redesign.

The prompt compiler also rejects the idea that selecting a style means copying
a living artist's recognizable composition. Style controls should describe a
visual language—fine-line, blackwork, geometric, neo-traditional—not impersonate
someone's portfolio.

What I would build first next time

If I were starting another production image-AI tool, I would implement these
pieces before adding more models:

  1. a typed input model for the decisions users actually understand;
  2. a prompt compiler with clear user-content boundaries;
  3. a persisted, idempotent async job state machine;
  4. automatic refunds or retries for terminal failures;
  5. private application-owned storage for results;
  6. a non-AI last-mile editor or preview; and
  7. honest language about what still requires human expertise.

Model quality matters, but reliability and constraint design are what turn a
model call into a product.

Frequently asked questions

Is an AI tattoo generator different from a general image generator?

The underlying model may be similar. The useful difference is the workflow:
tattoo-specific style, placement, color, detail, composition, isolated artwork,
project storage, and artist handoff constraints.

Is an AI-generated tattoo design ready to tattoo?

No. Treat it as a concept or visual brief. A qualified artist should adapt the
line weight, spacing, scale, contrast, and composition for the body and for
long-term readability.

Do users need drawing skills?

No. Structured text controls help someone express subject, meaning, style, and
placement. Being able to explain what matters is more useful than being able
to draw a polished sketch.

Does virtual try-on prove that a placement will work?

No. It can help compare approximate size, orientation, and visibility. It
cannot predict movement, healing, fading, or the feasibility of a cover-up.

Closing thought

Building this changed how I think about image AI. The inference call is the
smallest interesting part of the system. The real product lives in the
constraints before the call, the reliability around it, and the decisions a
human can make afterward.

That is true for tattoos, but it also applies to fashion previews, interior
design tools, product mockups, and almost every workflow where a generated
image has to survive contact with the physical world.

If you have built a production image-AI tool, what turned out to be your
hardest “last mile” problem?

Top comments (0)