====== Tramage lasercut ====== * Porteur du projet : Anthony P * Fichiers utiles : //mettre un lien vers un code ou un fichier// {{tag>processing, lasercut, svg, vector}} ===== Description ===== 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. {{projets:tramage_lasercut:start:screen_shot_10-20-26_at_08.18_am.png?900|Tramage lasercut }} ==== Codes ==== 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); } } ===== Notes techniques et matériaux ===== Liste de matériel et composants nécessaires (documents avec photo si possible...) ===== Photos et médias===== Code pour afficher les images du projet : {{gallery>?&crop&lightbox }}