projets:tramage_lasercut:start

Tramage lasercut

  • Porteur du projet : Anthony P
  • Fichiers utiles : mettre un lien vers un code ou un fichier

Un utilitaire sous processing 4 qui permet de convertir en trame svg des images bitmap.

Tramage vectoriel – Générateur de motifs pour pochoir laser

Ce logiciel développé avec Processing permet de transformer une image en un tramage composé de formes géométriques vectorielles.

  • Il propose plusieurs formes (cercles, carrés, hexagones, losanges) et différents types de matrices, avec un réglage de l’espacement, de la taille, du contraste et de l’irrégularité.
  • L’image peut être inversée en noir et blanc pour adapter le rendu au résultat souhaité.
  • Une prévisualisation interactive permet de zoomer, de déplacer l’image et d’ajuster les paramètres en temps réel.
  • Le résultat peut être exporté aux formats SVG et PDF pour être retravaillé ou utilisé dans un flux de fabrication numérique, notamment pour la découpe laser.

Tramage lasercut

import processing.svg.*;
import processing.pdf.*;
import processing.event.MouseEvent;
import java.io.File;
 
// TRAMAGE VECTORIEL - Processing 4
// Import image, tramage, zoom/deplacement, export SVG/PDF.
 
PImage img;
 
int WIN_W = 1600;
int WIN_H = 900;
int ART_W = 1160;
int ART_H = 700;
 
final int VIEW_X = 20;
final int VIEW_Y = 52;
int PANEL_X = 1320;
final int PANEL_W = 460;
 
String[] shapes = {"Cercle", "Carré", "Hexagone", "Losange"};
String[] patterns = {
  "Horizontal", "Vertical", "Diagonal", "Radial", "Hexagonal"
};
 
int shapeIndex = 0;
int patternIndex = 0;
 
float spacing = 18;
float maxSize = 14;
float contrast = 1.0;
float randomness = 0;
 
boolean invert = false;
boolean showImage = true;
boolean showFrame = true;
 
float imgX, imgY, imgW, imgH;
float zoom = 1.0;
float panX = 0;
float panY = 0;
 
boolean panning = false;
float lastMouseX, lastMouseY;
 
int randomSeedValue = 23741;
String selectedSlider = "";
String message = "Importe une image pour commencer.";
PFont uiFont;
 
 
// ----------------------------------------------------
// INITIALISATION
// ----------------------------------------------------
 
void setup() {
  size(1600, 900);
  surface.setResizable(true);
  surface.setTitle("Tramage vectoriel - Atelier pochoir");
 
  updateLayout();
 
  smooth(4);
  uiFont = createFont("SansSerif", 15);
  textFont(uiFont);
}
 
 
// ----------------------------------------------------
// FENETRE ET MISE EN PAGE ADAPTATIVES
// ----------------------------------------------------
 
void updateLayout() {
  WIN_W = width;
  WIN_H = height;
 
  // Le panneau conserve une largeur fixe.
  PANEL_X = max(420, WIN_W - PANEL_W);
 
  // La zone de dessin prend l'espace restant.
  ART_W = max(360, PANEL_X - VIEW_X - 20);
  ART_H = max(300, WIN_H - VIEW_Y - 120);
}
 
 
// ----------------------------------------------------
// AFFICHAGE PRINCIPAL
// ----------------------------------------------------
 
void draw() {
  updateLayout();
 
  // Recalcule l'ajustement de l'image après redimensionnement.
  if (img != null) {
    calculateImageFit();
  }
 
  background(34, 36, 40);
 
  drawHeader();
  drawPreview();
  drawInterface();
}
 
 
void drawHeader() {
  fill(242, 244, 247);
  textSize(21);
  text("TRAMAGE VECTORIEL", 22, 30);
 
  fill(153, 160, 170);
  textSize(13);
  text("IMAGE  /  FORMES  /  POCHOIR LASER", 260, 29);
}
 
 
void drawPreview() {
  noStroke();
  fill(245, 245, 242);
  rect(VIEW_X, VIEW_Y, ART_W, ART_H);
 
  // Limite le dessin à la zone de prévisualisation.
  clip(VIEW_X, VIEW_Y, ART_W, ART_H);
 
  pushMatrix();
 
  translate(VIEW_X + panX, VIEW_Y + panY);
  translate(ART_W / 2.0, ART_H / 2.0);
  scale(zoom);
  translate(-ART_W / 2.0, -ART_H / 2.0);
 
  if (img != null) {
    if (showImage) {
      image(img, imgX, imgY, imgW, imgH);
    }
 
    renderArt(g);
 
    if (showFrame) {
      noFill();
      stroke(135, 140, 145);
      strokeWeight(1);
      rect(imgX, imgY, imgW, imgH);
    }
  } else {
    fill(92, 98, 105);
    textAlign(CENTER, CENTER);
    textSize(20);
    text(
      "IMPORTER UNE IMAGE POUR COMMENCER",
      ART_W / 2.0,
      ART_H / 2.0
    );
    textAlign(LEFT, BASELINE);
  }
 
  popMatrix();
  noClip();
 
  noFill();
  stroke(72, 77, 84);
  rect(VIEW_X, VIEW_Y, ART_W, ART_H);
 
  fill(160, 167, 177);
  textSize(12);
  text(
    "Zoom " + nf(zoom, 1, 2) +
    "x  -  Molette : zoom  -  Clic droit + glisser : déplacer",
    VIEW_X,
    min(WIN_H - 12, VIEW_Y + ART_H + 28)
  );
}
 
 
// ----------------------------------------------------
// IMAGE
// ----------------------------------------------------
 
void calculateImageFit() {
  if (img == null) return;
 
  float ratio = min(
    (float) ART_W / img.width,
    (float) ART_H / img.height
  );
 
  imgW = img.width * ratio;
  imgH = img.height * ratio;
 
  imgX = (ART_W - imgW) / 2.0;
  imgY = (ART_H - imgH) / 2.0;
}
 
 
void openImage() {
  selectInput("Choisir une image", "imageSelected");
}
 
 
void imageSelected(File selection) {
  if (selection == null) return;
 
  PImage loaded = loadImage(selection.getAbsolutePath());
 
  if (loaded != null) {
    img = loaded;
    img.loadPixels();
 
    calculateImageFit();
 
    zoom = 1.0;
    panX = 0;
    panY = 0;
 
    message = "Image chargée : " + selection.getName();
  } else {
    message = "Impossible de charger cette image.";
  }
}
 
 
// ----------------------------------------------------
// ANALYSE DE L'IMAGE
// ----------------------------------------------------
 
float getAmount(float x, float y) {
  if (img == null) return 0;
 
  int sx = constrain(
    int(map(x, imgX, imgX + imgW, 0, img.width - 1)),
    0,
    img.width - 1
  );
 
  int sy = constrain(
    int(map(y, imgY, imgY + imgH, 0, img.height - 1)),
    0,
    img.height - 1
  );
 
  color c = img.pixels[sy * img.width + sx];
 
  float brightnessValue =
    (red(c) * 0.299 +
     green(c) * 0.587 +
     blue(c) * 0.114) / 255.0;
 
  brightnessValue = pow(
    constrain(brightnessValue, 0, 1),
    contrast
  );
 
  // Inversion noir/blanc.
  return invert ? brightnessValue : 1.0 - brightnessValue;
}
 
 
float sizeAt(float x, float y) {
  return maxSize * getAmount(x, y);
}
 
 
boolean insideImage(float x, float y) {
  return x >= imgX &&
         x <= imgX + imgW &&
         y >= imgY &&
         y <= imgY + imgH;
}
 
 
// ----------------------------------------------------
// GENERATION VECTORIELLE
// ----------------------------------------------------
 
void renderArt(PGraphics pg) {
  if (img == null) return;
 
  pg.pushStyle();
  pg.noFill();
  pg.stroke(12);
  pg.strokeWeight(1);
 
  if (patternIndex == 3) {
    drawRadial(pg);
  } else {
    drawGrid(pg);
  }
 
  pg.popStyle();
}
 
 
void drawGrid(PGraphics pg) {
  randomSeed(randomSeedValue);
 
  float step = spacing;
 
  for (float y = imgY; y <= imgY + imgH; y += step) {
    for (float x = imgX; x <= imgX + imgW; x += step) {
 
      float px = x;
      float py = y;
 
      // Matrice décalée.
      if (patternIndex == 1 || patternIndex == 4) {
        px += (round((y - imgY) / step) % 2) * step * 0.5;
      } else if (patternIndex == 2) {
        py += (round((x - imgX) / step) % 2) * step * 0.5;
      }
 
      // Irrégularité organique réglable.
      float jitter = step * 0.48 * (randomness / 100.0);
 
      px += random(-jitter, jitter);
      py += random(-jitter, jitter);
 
      if (!insideImage(px, py)) continue;
 
      float s = sizeAt(px, py);
      if (s < 0.8) continue;
 
      drawShape(pg, px, py, s);
    }
  }
}
 
 
void drawRadial(PGraphics pg) {
  randomSeed(randomSeedValue);
 
  float cx = imgX + imgW / 2.0;
  float cy = imgY + imgH / 2.0;
 
  float maxRadius = sqrt(imgW * imgW + imgH * imgH) / 2.0;
  float ringStep = spacing;
 
  for (float r = 0; r <= maxRadius; r += ringStep) {
    float circumference = TWO_PI * max(r, ringStep);
    int count = max(1, round(circumference / ringStep));
 
    for (int i = 0; i < count; i++) {
      float a = TWO_PI * i / count;
 
      float jitter = ringStep * 0.48 * (randomness / 100.0);
 
      float rr = max(0, r + random(-jitter, jitter));
      float aa = a + random(-0.35, 0.35) * (randomness / 100.0);
 
      float x = cx + cos(aa) * rr + random(-jitter, jitter);
      float y = cy + sin(aa) * rr + random(-jitter, jitter);
 
      if (!insideImage(x, y)) continue;
 
      float s = sizeAt(x, y);
      if (s < 0.8) continue;
 
      drawShape(pg, x, y, s);
    }
  }
}
 
 
// ----------------------------------------------------
// FORMES GEOMETRIQUES
// ----------------------------------------------------
 
void drawShape(PGraphics pg, float x, float y, float s) {
  float r = s / 2.0;
 
  if (shapeIndex == 0) {
    pg.ellipse(x, y, s, s);
 
  } else if (shapeIndex == 1) {
    pg.rectMode(CENTER);
    pg.rect(x, y, s, s);
    pg.rectMode(CORNER);
 
  } else if (shapeIndex == 2) {
    pg.beginShape();
 
    for (int i = 0; i < 6; i++) {
      float a = TWO_PI * i / 6.0 - HALF_PI;
      pg.vertex(
        x + cos(a) * r,
        y + sin(a) * r
      );
    }
 
    pg.endShape(CLOSE);
 
  } else {
    pg.beginShape();
    pg.vertex(x, y - r);
    pg.vertex(x + r, y);
    pg.vertex(x, y + r);
    pg.vertex(x - r, y);
    pg.endShape(CLOSE);
  }
}
 
 
// ----------------------------------------------------
// EXPORT SVG / PDF
// ----------------------------------------------------
 
void exportSVG() {
  if (img == null) {
    message = "Importe d'abord une image.";
    return;
  }
 
  selectOutput("Enregistrer en SVG", "svgSelected");
}
 
 
void svgSelected(File selection) {
  if (selection == null) return;
 
  String path = selection.getAbsolutePath();
 
  if (!path.toLowerCase().endsWith(".svg")) {
    path += ".svg";
  }
 
  PGraphics vector = createGraphics(ART_W, ART_H, SVG, path);
 
  vector.beginDraw();
  renderArt(vector);
  vector.endDraw();
 
  message = "SVG exporté : " + path;
}
 
 
void exportPDF() {
  if (img == null) {
    message = "Importe d'abord une image.";
    return;
  }
 
  selectOutput("Enregistrer en PDF", "pdfSelected");
}
 
 
void pdfSelected(File selection) {
  if (selection == null) return;
 
  String path = selection.getAbsolutePath();
 
  if (!path.toLowerCase().endsWith(".pdf")) {
    path += ".pdf";
  }
 
  PGraphics vector = createGraphics(ART_W, ART_H, PDF, path);
 
  vector.beginDraw();
  vector.background(255);
  renderArt(vector);
  vector.endDraw();
 
  message = "PDF exporté : " + path;
}
 
 
// ----------------------------------------------------
// INTERFACE
// ----------------------------------------------------
 
void drawInterface() {
  noStroke();
  fill(43, 46, 51);
  rect(PANEL_X, 0, WIN_W - PANEL_X, WIN_H);
 
  stroke(65, 70, 77);
  line(PANEL_X, 0, PANEL_X, WIN_H);
 
  fill(245, 247, 250);
  textSize(22);
  text("PARAMÈTRES", PANEL_X + 24, 38);
 
  fill(155, 163, 174);
  textSize(13);
  text("Réglages en direct", PANEL_X + 24, 60);
 
  button(PANEL_X + 24, 82, PANEL_W - 48, 42,
         "Importer une image", true);
 
  label("GÉOMÉTRIE", 157);
 
  button(PANEL_X + 24, 169, 198, 38,
         "Forme : " + shapes[shapeIndex], false);
 
  button(PANEL_X + 234, 169, 202, 38,
         "Matrice : " + patterns[patternIndex], false);
 
  label("TRAME", 237);
 
  sliderUI("Espacement", spacing, 5, 40, 253, "spacing");
  sliderUI("Taille maximale", maxSize, 2, 40, 310, "size");
  sliderUI("Contraste", contrast, 0.3, 2.5, 367, "contrast");
  sliderUI("Irrégularité organique", randomness, 0, 100, 424, "randomness");
 
  button(PANEL_X + 24, 465, PANEL_W - 48, 34,
         "Nouveau hasard", false);
 
  button(PANEL_X + 24, 507, PANEL_W - 48, 34,
         "Inverser noir / blanc : " + (invert ? "OUI" : "NON"),
         invert);
 
  label("APERÇU", 574);
 
  button(PANEL_X + 24, 586, 128, 36, "- Zoom", false);
  button(PANEL_X + 164, 586, 128, 36, "Zoom +", false);
  button(PANEL_X + 304, 586, 132, 36, "Réinitialiser", false);
 
  button(PANEL_X + 24, 631, 198, 34,
         "Photo : " + (showImage ? "visible" : "masquée"), false);
 
  button(PANEL_X + 234, 631, 202, 34,
         "Cadre : " + (showFrame ? "oui" : "non"), false);
 
  label("EXPORT VECTORIEL", 704);
 
  button(PANEL_X + 24, 716, 198, 42, "Exporter SVG", true);
  button(PANEL_X + 234, 716, 202, 42, "Exporter PDF", true);
 
  fill(160, 168, 178);
  textSize(12);
  text("Export sans la photo ni l'interface.", PANEL_X + 24, 789);
  text("Vérifie les dimensions avant la découpe.", PANEL_X + 24, 808);
 
  fill(215, 220, 226);
  textSize(12);
  text("ÉTAT", PANEL_X + 24, 839);
 
  fill(157, 202, 176);
  text(message, PANEL_X + 24, 861, PANEL_W - 48, 35);
}
 
 
void label(String txt, float y) {
  fill(164, 172, 183);
  textSize(12);
  text(txt, PANEL_X + 24, y);
}
 
 
void button(float x, float y, float w, float h, String txt) {
  button(x, y, w, h, txt, false);
}
 
 
void button(float x, float y, float w, float h,
            String txt, boolean accent) {
  stroke(accent ? color(84, 151, 124) : color(77, 83, 92));
  fill(accent ? color(51, 104, 84) : color(55, 59, 65));
 
  rect(x, y, w, h, 6);
 
  fill(245, 247, 250);
  textAlign(CENTER, CENTER);
  textSize(14);
  text(txt, x + w / 2, y + h / 2);
 
  textAlign(LEFT, BASELINE);
}
 
 
void sliderUI(
  String title,
  float value,
  float minVal,
  float maxVal,
  float y,
  String id
) {
  float x = PANEL_X + 24;
  float w = PANEL_W - 48;
 
  fill(229, 232, 237);
  textSize(14);
 
  String valueText = id.equals("randomness")
    ? nf(value, 0, 0) + "%"
    : nf(value, 0, 1);
 
  text(title, x, y);
 
  textAlign(RIGHT, BASELINE);
  fill(172, 211, 190);
  text(valueText, x + w, y);
  textAlign(LEFT, BASELINE);
 
  float barY = y + 18;
 
  stroke(83, 89, 98);
  strokeWeight(4);
  line(x, barY, x + w, barY);
 
  float px = map(value, minVal, maxVal, x, x + w);
 
  stroke(101, 190, 151);
  line(x, barY, px, barY);
 
  strokeWeight(1);
  noStroke();
 
  fill(230, 245, 237);
  ellipse(px, barY, 12, 12);
}
 
 
// ----------------------------------------------------
// INTERACTIONS
// ----------------------------------------------------
 
void mousePressed() {
  if (mouseX < PANEL_X) {
    if (mouseButton == RIGHT) {
      panning = true;
      lastMouseX = mouseX;
      lastMouseY = mouseY;
    }
    return;
  }
 
  if (hit(PANEL_X + 24, 82, PANEL_W - 48, 42)) {
    openImage();
    return;
  }
 
  if (hit(PANEL_X + 24, 169, 198, 38)) {
    shapeIndex = (shapeIndex + 1) % shapes.length;
    return;
  }
 
  if (hit(PANEL_X + 234, 169, 202, 38)) {
    patternIndex = (patternIndex + 1) % patterns.length;
    return;
  }
 
  if (hit(PANEL_X + 24, 253, PANEL_W - 48, 25)) {
    selectedSlider = "spacing";
    updateSlider();
    return;
  }
 
  if (hit(PANEL_X + 24, 310, PANEL_W - 48, 25)) {
    selectedSlider = "size";
    updateSlider();
    return;
  }
 
  if (hit(PANEL_X + 24, 367, PANEL_W - 48, 25)) {
    selectedSlider = "contrast";
    updateSlider();
    return;
  }
 
  if (hit(PANEL_X + 24, 424, PANEL_W - 48, 25)) {
    selectedSlider = "randomness";
    updateSlider();
    return;
  }
 
  if (hit(PANEL_X + 24, 465, PANEL_W - 48, 34)) {
    randomSeedValue = (int) random(1, 2000000000);
    message = "Nouvelle variation aléatoire.";
    return;
  }
 
  if (hit(PANEL_X + 24, 507, PANEL_W - 48, 34)) {
    invert = !invert;
 
    message = invert
      ? "Inversion noir / blanc activée."
      : "Inversion noir / blanc désactivée.";
 
    return;
  }
 
  if (hit(PANEL_X + 24, 586, 128, 36)) {
    zoom = max(0.25, zoom / 1.2);
    return;
  }
 
  if (hit(PANEL_X + 164, 586, 128, 36)) {
    zoom = min(5.0, zoom * 1.2);
    return;
  }
 
  if (hit(PANEL_X + 304, 586, 132, 36)) {
    zoom = 1;
    panX = 0;
    panY = 0;
    return;
  }
 
  if (hit(PANEL_X + 24, 631, 198, 34)) {
    showImage = !showImage;
    return;
  }
 
  if (hit(PANEL_X + 234, 631, 202, 34)) {
    showFrame = !showFrame;
    return;
  }
 
  if (hit(PANEL_X + 24, 716, 198, 42)) {
    exportSVG();
    return;
  }
 
  if (hit(PANEL_X + 234, 716, 202, 42)) {
    exportPDF();
  }
}
 
 
void mouseDragged() {
  if (panning) {
    panX += mouseX - lastMouseX;
    panY += mouseY - lastMouseY;
 
    lastMouseX = mouseX;
    lastMouseY = mouseY;
 
  } else if (!selectedSlider.equals("")) {
    updateSlider();
  }
}
 
 
void mouseReleased() {
  panning = false;
  selectedSlider = "";
}
 
 
void mouseWheel(MouseEvent event) {
  if (
    mouseX < PANEL_X &&
    mouseX >= VIEW_X &&
    mouseY >= VIEW_Y &&
    mouseY <= VIEW_Y + ART_H
  ) {
    float factor = event.getCount() < 0 ? 1.12 : 1.0 / 1.12;
    zoom = constrain(zoom * factor, 0.25, 5.0);
  }
}
 
 
boolean hit(float x, float y, float w, float h) {
  return mouseX >= x &&
         mouseX <= x + w &&
         mouseY >= y &&
         mouseY <= y + h;
}
 
 
void updateSlider() {
  float x = constrain(
    mouseX,
    PANEL_X + 24,
    PANEL_X + PANEL_W - 24
  );
 
  float v;
 
  if (selectedSlider.equals("spacing")) {
    v = map(x, PANEL_X + 24, PANEL_X + PANEL_W - 24, 5, 40);
    spacing = round(v * 10) / 10.0;
 
  } else if (selectedSlider.equals("size")) {
    v = map(x, PANEL_X + 24, PANEL_X + PANEL_W - 24, 2, 40);
    maxSize = round(v * 10) / 10.0;
 
  } else if (selectedSlider.equals("contrast")) {
    v = map(x, PANEL_X + 24, PANEL_X + PANEL_W - 24, 0.3, 2.5);
    contrast = round(v * 100) / 100.0;
 
  } else if (selectedSlider.equals("randomness")) {
    v = map(x, PANEL_X + 24, PANEL_X + PANEL_W - 24, 0, 100);
    randomness = round(v);
  }
}

Liste de matériel et composants nécessaires (documents avec photo si possible…)

Code pour afficher les images du projet :

  • projets/tramage_lasercut/start.txt
  • Dernière modification: 2026/10/20 08:21
  • de admin