CommunityImagegithub.com

Atelier en code

Peindre des images fixes en code — lancer de rayons, aquarelle, bande dessinée, gravure et affiche typographique — sans modèle d’image.

Qu'est-ce que Atelier en code ?

Atelier est un studio d’image piloté par le code. On choisit un style, on écrit une courte scène, et on rend soi-même un PNG : personnages, paysages, lettrage, pixel art ou abstraction.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Wexop/atelier/tree/HEAD

Installed? Explore more Image skills: steipete/songsee, affaan-m/frontend-design, mattpocock/improve-codebase-architecture · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

atelier — studio d'images piloté par de courts scripts Python

Installation (Python ≥ 3.11) : pip install git+https://github.com/Wexop/atelier (mise à jour : pip install -U git+...) Vérifier : atelier palette · atelier brushes · atelier fx (signatures).

Boucle (toujours)

  1. Choisir le guide de style (voir Styles ci-dessous) et le lire ; aucun ne correspond → styles/new-style.md.
  2. Écrire scene.py (30–80 lignes, from atelier import *, construire un Canvas c, terminer par c.save(__file__[:-3] + ".png")).
  3. atelier render scene.py --preview → scene_prev.jpg (512 px, rendu basse résolution réel, sauvegardes ignorées). Lire l'image.
  4. atelier inspect scene.py → scene_sheet.jpg : vue, étude des 5 valeurs + tiers/point focal, carte de découpage, histogramme, palette, 4 zooms automatiques (crénelage, banding, bruit, hautes lumières) étiquetés --crop x,y,w,h, métriques + drapeaux rouges. La lire. Zoomer : atelier render scene.py --crop x,y,w,h.
  5. ≥3 cycles de regard-et-correction (atelier compare old.png new.png pour A/B), puis atelier render scene.py (final ; --scale 2, --seed N → SEED). Les erreurs affichent une ligne scene.py:LIGNE: Type: message (--debug pour la trace).

Conventions

  • Coordonnées de dessin = px du canvas (y vers le bas). Les champs utilisent x, y normalisés ∈ [0,1]. Couleurs : "#hex", noms CSS, (r,v,b) 0–1 ou 0–255.
  • Source de peinture (fill=, color=, stroke=) = couleur | Field | f(x,y) | tableau H×L×3/4. Masques = Field | Shape | tableau | f(x,y).
  • Tout ce qui est aléatoire prend seed=. Les aperçus sont rendus en basse résolution : l'image est identique, seulement plus nette au final.

Canvas & calques (noyau)

c = Canvas(l, h, bg=None, pixel=False)      # pixel=True : pas d'AA, échelle fixe, export au plus proche voisin
L = c.layer(blend="normal", opacity=1, mask=None)   # nouveau calque, DEVIENT LE COURANT ; c.use(L) pour revenir
blends : normal multiply screen overlay soft_light hard_light add subtract color_dodge color_burn darken lighten difference hue saturation color luminosity
c.fill(src, mask=None) · c.erase(mask) · c.place(img|chemin, x=None, y=None, scale=None(couvrir), rot=0, fit="cover"|"contain", blend, opacity) -> calque
c.apply(*fx, mask=None)   # FUSIONNE tous les calques, puis effets (gradation finale) ;  L.apply(*fx, mask=None) = un seul calque
c.save(chemin, quality=92, scale=1, smooth=None) · c.preview(chemin, width=512) · c.crop_preview(chemin, x, y, l, h)
c.flatten() -> H×L×4 float · L.rgba (get/set) · c.l c.h (logique) c.L c.H c.s (échelle raster)

Champs (field) — paresseux, composables : + - * / ** > <, np.sin(f), f.ramp() f.warp() f.scale() f.rotate() f.move() f.remap(lo,hi) f.clip() f.within(shape|(x0,y0,x1,y1))

X Y radial(cx,cy) angle(cx,cy) · Field(lambda x, y: ...)
perlin/simplex/value(scale=4, seed) · fbm(scale=4, octaves=5, lacunarity=2, gain=.5, seed, base="perlin")
ridged(scale=3, ...) · turbulence(scale=4, ...) · worley(scale=6, mode="F1"|"F2"|"F2-F1"|"id", seed)
domain_warp(f, strength=.1, scale=2, seed) · aastep(f, edge=.5) · smoothstep(e0, e1, v) · mix(a, b, t)
motifs [0,1] : wood(rings=14, angle) marble() clouds(scale, cover=.5, sharp) terrain_height() water_ripples() fabric(scale=60) paper() stars(density=60, size)
gradient(stops, angle=90 (haut→bas ; 0 = gauche→droite), kind="linear"|"radial"|"conic", center, radius)
ramp(f, stops) / f.ramp("sunset") · stops = nom de palette | ["#..", ...] | [(pos, col), ...]   (interpolation OKLab)
palette("nom") noms : sunset night ocean forest desert earth fire ice pastel neon vapor viridis wood marble mono retro16 green4
cosine_palette(a, b, c, d, n=8) · rgb2hsv hsv2rgb rgb2oklab oklab2rgb
shade(height, light=(azimuth, elevation), strength=1, color=None, ambient=.25, spec=0)   # pas de couleur → gris, flat=0.5 (utiliser sur calque overlay)
masques : top(f) bottom(f) left(f) right(f) band(y0, y1) zone(x0, y0, x1, y1) disc(cx, cy, r)  (tous soft=)
courbes : f.ridge_line(y=.6, amp=.15) / curve(lambda x: y) → c.fill_below(curve, src) / fill_above · curve.below()

Vecteur & texte (vector) — Shape = chemin skia, aussi un masque Field ; booléens | & - ^

rect(x, y, l, h, r=0) ellipse(cx, cy, rx, ry) circle(cx, cy, r) polygone(pts) line(x1,y1,x2,y2 | pts) regular(cx,cy,r,n)
star(cx, cy, r, r2, n=5) arc(cx, cy, r, start, sweep) spline(pts, closed) svg("M .. C .. A .. Z") union(*shapes)
shape.move/rotate(deg)/scale/outline(width, dash)/soft(plume)/points(n)/bounds/length
c.draw(shape, fill="black", stroke=None, width=2, cap="round", join="round", dash=(on, off), feather=0, opacity=1)
c.rect/ellipse/circle/polygon/line/spline/path(d)(..., **même style)
c.text(txt, x, y, size=32, fill, font="sans"|"mono"|"serif"|famille|chemin.ttf, weight="regular"|"semibold"|"bold"|int,
       align="left"|"center"|"right", valign="baseline"|"top"|"middle"|"bottom", spacing=em, line_height=1.2, italic, fit=largeur_px, kern=True)
text_shape(...) → Shape · text_size(txt, size) → (largeur, hauteur_cap) · c.text_on_path(txt, shape|points, size, align, lift, fit, fill)
c.text_box(txt, x, y, l, h=None, size=18, align="left"|"justify"|"center", line_height=1.4, fit=True)   # **b** *i* [#c33]couleur[/]
c.title(txt, x, y, size=96, style="shadow"|"extrude"|"outline"|"inline"|"neon", fill, color2)

Pinceaux (brush)

c.stroke(points|Shape, brush="round", size=12, color, pressure=[...]|f(t)|float, opacity, jitter, spacing, taper=(a, b), seed, **params)
pinceaux : round round_soft airbrush ink(vitesse) chalk(dent) bristle(n, var, gaps) oil(impasto, pickup) watercolor(bord, granulation, bleed)
c.paint_like(ref, brush="oil", sizes=(48,24,12), tol=.08, angle=0, seed)   # repeindre une référence (Canvas/Field/image) avec des traits
c.scatter(quoi, n, region=mask, size=(a, b), color=col|liste|palette|Field, color_jitter, rot=(a, b), seed)
   quoi : dot soft star grass leaf petal square person | Shape (taille unitaire à 0,0)
c.hatch(region, angle=45, spacing=8, width=1.2, color, tone=Field (0=foncé → épais), cross=False, wobble)
km_mix(a, b, t) mélange de type pigment (bleu+jaune = vert) · L.relief = 1.0 (force d'éclairage impasto huile)

Effets (fx) — c.apply(...) / L.apply(...) ; tailles en px du canvas

blur(r) motion_blur(longueur, angle) sharpen(amt) bloom(threshold=.75, radius=12, strength=.8, tint) glow(r, strength, color)
drop_shadow(dx, dy, r, color, opacity) inner_shadow(...) vignette(amount=.35, softness=.6) grain(amount=.04, size=1, seed)
chromatic_aberration(px) lens_distortion(k) exposure(stops) contrast(amt) levels(black, white, gamma) curves([(in, out), ...], channel)
hue_sat(deg_teinte, mult_sat, light) saturation(x) color_balance(shadows=(r,v,b), mids, highlights) gradient_map(stops) invert()
tone_map("agx"|"filmic"|"reinhard", exposure, look="punchy") displace(field, strength) posterize(niveaux)
pixelate(size, palette=nom|liste, colors=N, dither=None|"bayer"|"fs", strength=.6) halftone(size, angle, ink, paper, cmyk=False)
god_rays(x, y, rays=14, length=.9, strength=.45, color, threshold=.7, occlude=True) lens_flare(x, y, strength=.6, color, ghosts=6, streak=.5)
depth_of_field(depth, focus, aperture=8)   # profondeur à partir de r = c.render(scene)
match_palette("ref.jpg", amount) transfer_tone(ref, amount)   # transfert de couleur/ton depuis une image de référence

Styliser (stylize) — 3D → illustration + filtres picturaux (effets pour c.apply / L.apply)

r = c.render(s, alpha=True); c.cur.apply(toon(r, bands=3, shadow=.45, colors={id: col}), shadow_dots(r, size=5, lo=.3), outlines(r, width=2.5, inner=1))
gravure : L.fill(PAPER, mask=r.alpha); L.apply(render_hatch(r, s, spacing=4.5, layers=3), outlines(r, width=2))  # hachures suivent les surfaces 3D
kuwahara(radius=6) oil · xdog(sigma=1, eps=.9, ink, paper) ink lines · pencil() · linocut(lines=7, angle) · edge_smooth(4)
watercolorize(bleed=2, edge=1, granulation=.4, ink=0..1) · stained_glass(cell=26, lead=3) · mosaic(tile=10) · shading(r) → (H,L) lumière

3D (sdf) — y vers le haut, unités ≈ mètres, primitives centrées à l'origine

sphere(r) box(l, h, p) round_box(l, h, p, r) cylindre(r, h) capsule(a, b, r) torus(R, r) cone(r1, h, r2) plan(y, normal)
ellipsoide(rx, ry, rz) lathe([(radius, y), ...] bas→haut, thickness=None|coque) extrude([(x, y), ...], profondeur)
ops : a | b, a & b, a - b, a.union(b, k=smooth) .inter .sub · .move(x,y,z) .rotate(x,y,z deg) .scale(k|(kx,ky,kz))
     .onion(t) .round(r) .twist(k) .bend(k) .repeat(periode, count) .mirror("x") .elongate() .displace(noise3(scale, octaves, seed), amount)
Mat(color | 2D Field (proj="tri"|"xz"|"xy"|"zy", tex_scale) | f(p,n)->(N,3), rough=.5|Field|f(p,n), metal=0, glass=0, ior=1.45,
    sss=0, sss_color, emit=None, power=1, bump=.3, bump_scale=60, local=True (les textures suivent l'objet), sheen=.6,
    wear=[smudges(.5), scratches(.5, density=40), dust(.3), edge_wear(.5, width=.01, color)])
s = Scene(); id = s.add(sdf, Mat(...)); s.camera(pos, target, fov=40)
s.sun(dir_vers_soleil, color, power=3, soft=.04) · s.point(pos, color, power, radius) · s.area(pos, size=(l,h), normal, color, power, pattern="window"|"softbox", visible)
s.studio(key, target, size, power=45, window=True, fill=.2, bg) · s.sky(zénith, horizon, sol, power) · s.fog(color, density)
r = c.render(s, aa=1|2, exposure=0, look=None|"punchy", alpha=False)          # raymarcheur rapide (lumière directe + AO + 1 réflexion)
r = c.pathtrace(s, spp=16, bounces=4, denoise=True, aperture=0, focus=None|dist|(x,y), seed=SEED)  # photoréaliste : GI, multi-réfraction verre, AA adaptatif
r.depth r.normal r.id r.mask(id) r.focus_at(x, y, c.s) r.linear r.spp r.time · denoise(img, albedo, normal, depth)

Nature (nature, terrain)

h = terrain(384, kind="alpine"|"hills"|"mesa"|"island"|"dunes", seed, erosion=1, flow=False)   # carte de hauteurs érodée [0,1] ; erode() thermal()
c.terrain_map(h, style="relief"|"topo"|"shaded", water=.2)
v = c.landscape(h, cam=(x, alt, z), yaw, fov, horizon=.58, sun_elev=10, sun_az=30, haze=1, clouds=.35, water=.1, waves=.02, snow=.7, seed)
    v.depth v.water v.project(x, y, z) -> (px, py, px_par_m) v.ground(x, z)   # placer des arbres 2D sur le terrain 3D
c.sky(sun_elev=8, sun_az=0, horizon=.6, clouds=.3, haze=1) · c.sea(sun_elev=6, waves=.12, wave_len=60, foam=.5, clouds=.25)
c.tree(x, y, hauteur_px, kind="oak"|"pine"|"birch"|"palm"|"bush"|"dead", season="summer"|"spring"|"autumn"|"winter", haze=0..1, seed)
c.grass(x, y, l, h, n) c.flower(x, y, size, kind="daisy"|"poppy"|"tulip"|"bell") c.fern(x, y, size) · tree3d("oak", 4, seed) → (tronc, feuilles) SDFs
scene_sky(s, sun_elev, sun_az)   # ciel physique sur une scène 3D
c.anime_cloud(x, y, l, h=.55l, palette="day"|"sunset"|"dusk"|"storm"|(ombre,mi,lt,rim), light=(az, elev), bands=3, rim=.7, wisps, edge, bottom, seed)  # cumulus cel-shaded, base plate à (x, y)
c.anime_tree(x, y, hauteur, kind="round"|"tall"|"umbrella", palette="summer"|"spring"|"autumn"|"sakura"|"dusk"|(sh,mid,lt,rim,tronc), light, width, leafy=.6, seed)  # arbre cel-shaded
c.anime_mountains(y_pied, hauteur, peaks=4, x0, x1, palette="day"|"dusk"|"autumn"|(sh,mid,lt,neige,neige_om,haze), light, snow=.35, haze=.6, sharp=1, depth=.35, bands=3, seed)  # relief érodé, vue de face, tons cel
c.sparkles(n, region=mask, size=(6, 16), color, rays=4, glow=.6, seed)   # éclats d'étoiles (eau, neige)

Figures (figure) — pieds à (x, y), hauteur = px debout (animaux : garrot)

c.figure(x, y, h, pose="stand", style="flat"|"line"|"silhouette"|"stick"|"ink"|"chalk", kind="human"|"cat"|"dog"|"horse"|"bird", seed)
   poses : stand idle walk run sit wave point cheer jump dance think hips phone reach · phase=0-1 (cycle marche) view="front"|"3q"|"side"|"back"
   body=adult|teen|child|elder build=slim|heavy outfit="tee pants|coat skirt|dress" hair=short|bob|long|bun|curly skin=SKIN nom colors={...}
figure_shape(...) → Shape · figure_joints(...) → {"hand_r": (x, y), ...} (attacher accessoires) · figure_sdf(pose, height=1.75, style="mannequin")
c.crowd(n, box=(x0, y0, x1, y1), horizon=y, height=proche_px, tint=(haze, .5), dogs=.1, seed)   # foule à échelle perspective

Inspecter (inspect) — juger les chiffres, pas seulement la vue réduite

m = metrics(c|chemin|arr)   # bruit (<1 propre, >4 granuleux) crénelage (% marches dures, >35 dentelé) banding clip_ombres/clip_hautes_lumières flags[...]
contact_sheet(c, "sheet.jpg") · compare(a, b, "ab.jpg") · guides(c) / composition(c) → focal, balance, tilt · analyze_reference("ref.jpg")

Styles — lire le guide correspondant avant d'écrire la scène

Les guides se trouvent dans styles/ à côté de ce fichier (si manquant : https://raw.githubusercontent.com/Wexop/atelier/main/styles/<nom>.md). Chacun donne une recette, des règles, des pièges et un exemple de script. Plusieurs peuvent être mélangés (ex. anime + flat).

  • anime.md — paysages anime peints : nuages cel, arbres, montagnes, lumière de contour, rayons divins, étincelles
  • oil.md — huile (paint_like) et lavis aquarelle
  • photoreal.md — nature morte / produit en path-tracing, paysages de terrain, mer et ciel
  • comic.md — ligne claire / cartoon à partir de 3D, gravure, filtres encre
  • flat.md — vecteur plat, affiches, typographie, personnes, scènes silhouette
  • pixel.md — pixel art avec palettes et tramage
  • abstract.md — abstraits génératifs à partir de champs déformés
  • new-style.md — aucun guide ne correspond : étudier le rendu, le décomposer en primitives, peindre, puis enregistrer un nouveau styles/<nom>.md

Pièges

  • c.layer() change la cible courante : les c.text/... suivants vont sur ce calque (mauvais s'il est multiply). Appeler c.layer() ou c.use(L).
  • c.apply() fusionne tous les calques. Utiliser L.apply() pour garder les calques.
  • shade(strength) dépend de la fréquence du champ : ≈1 pour les grandes formes, ≈.05 pour les textures fines (tissu).
  • tone_map sur de l'art 2D grise le blanc pur — il est pour HDR/3D ; utiliser curves/levels sur les illustrations.
  • Les seuils nets crénèlent : utiliser aastep/smoothstep, pas f > .5, pour les bords visibles.
  • scale du bruit = motifs par hauteur du canvas. Coordonnées Shape = px du canvas ; coordonnées Field = 0–1.
  • Coût : pathtrace ≈ 30–60 µs/chemin (1200×800 à 8 spp ≈ 5 min sur 2 cœurs, aperçus 4× moins de spp) — ébaucher avec c.render ; faibles spp donnent des étincelles sur métal/verre brillant (augmenter spp). aa=2 ≈ 4×. Beaucoup de pièces SDF (figures, tree3d, displace) sont lentes à tracer.
  • Verre : rough=0 pour transparent (rough par défaut .5 = dépoli). Dans c.render le verre se courbe une fois ; utiliser pathtrace pour une vraie réfraction.

Skills associés