diff --git a/examples/BasicExample/BasicExample.pde b/examples/BasicExample/BasicExample.pde index cb3d42d..fc0b771 100644 --- a/examples/BasicExample/BasicExample.pde +++ b/examples/BasicExample/BasicExample.pde @@ -1,119 +1,310 @@ import paperandpencil.*; +import processing.core.*; PaperAndPencil pp; +float margin = 30; +float size = 100; + +// Off-screen rendering (set true for deterministic full-size renders regardless of window) +boolean USE_OFFSCREEN = true; +PGraphics offscreen; +int OFFSCREEN_WIDTH = 450; +int OFFSCREEN_HEIGHT = 1200; +// Active rendering surface (either g or offscreen) +PGraphics pg; // Function to draw text with background void labelText(String txt, float x, float y) { - float textW = textWidth(txt); - fill(360, 80); // White background with some transparency - noStroke(); - rect(x - textW/2 - 5, y - 15, textW + 10, 20); - fill(0); - text(txt, x, y); + pg.pushMatrix(); + pg.translate(x, y); + float textW = pg.textWidth(txt); + pg.fill(360, 80); + pg.noStroke(); + pg.rect(-textW/2 - 5, -15, textW + 10, 20); + pg.fill(0); + pg.text(txt, 0, 0); + pg.popMatrix(); } -void setup() { - size(450, 850, P2D); - colorMode(HSB, 360, 100, 100, 100); - - pp = new PaperAndPencil(this); - - // Create paper background +void drawExample() { + println("Starting drawExample()"); + pg.beginDraw(); + // Ensure off-screen buffer uses same color mode (HSB) as main sketch + pg.colorMode(HSB, 360, 100, 100, 100); + pg.background(360); + // Route PaperAndPencil to this graphics + pp.setTarget(pg); pp.paper(); - // Set default pencil color + // Set text properties + pg.textAlign(CENTER); + pg.textSize(12); + + // Default pencil color pp.setPencilColor(color(0, 0, 0, 30)); - float margin = 30; - float size = 100; + println("Drawing 1st row: Lines"); + pg.pushMatrix(); + pg.translate(margin, margin); + { + // Simple line + println(" Drawing simple line"); + pp.line(0, 0, size, size, false); + labelText("Simple line", size/2, size + 20); + + // Line with fade + println(" Drawing line with fade"); + pg.translate(margin + size, 0); + pp.line(0, 0, size, size, true); + labelText("Line with fade", size/2, size + 20); + } + pg.popMatrix(); - // Set text properties - textAlign(CENTER); - textSize(12); + println("Drawing 2nd row: Circles"); + pg.pushMatrix(); + pg.translate(margin, 2*margin + size); + { + // Simple circle + println(" Drawing simple circle"); + pp.circle(size/2, size/2, size, false); + labelText("Simple circle", size/2, size + 20); + + // Circle with fade + println(" Drawing circle with fade"); + pg.translate(margin + size, 0); + pp.circle(size/2, size/2, size, true); + labelText("Circle with fade", size/2, size + 20); + + // Filled circle + println(" Drawing filled circle"); + pg.translate(margin + size, 0); + pp.fillCircle(size/2, size/2, size); + labelText("Filled circle", size/2, size + 20); + } + pg.popMatrix(); - // 1st row: Lines - pp.line(margin, margin, margin + size, margin + size); - labelText("Simple line", margin + size/2, margin + size + 20); + println("Drawing 3rd row: Arcs"); + pg.pushMatrix(); + pg.translate(margin, 3*margin + 2*size); + { + // Simple arc + println(" Drawing simple arc"); + pp.arc(size/2, size/2, size, -QUARTER_PI, PI, false); + labelText("Simple arc", size/2, size + 20); + + // Arc with fade + println(" Drawing arc with fade"); + pg.translate(margin + size, 0); + pp.arc(size/2, size/2, size, -QUARTER_PI, PI, true); + labelText("Arc with fade", size/2, size + 20); + } + pg.popMatrix(); - pp.line(2*margin + size, margin, 2*margin + 2*size, margin + size, true); - labelText("Line with fade", 2*margin + size + size/2, margin + size + 20); + println("Drawing 4th row: Rectangles"); + pg.pushMatrix(); + pg.translate(margin, 4*margin + 3*size); + { + // Simple rect + println(" Drawing simple rect"); + pp.rect(0, 0, size, size); + labelText("Simple rect", size/2, size + 20); + + // Filled rect + println(" Drawing filled rect"); + pg.translate(margin + size, 0); + pp.fillRect(0, 0, size, size); + labelText("Filled rect", size/2, size + 20); + } + pg.popMatrix(); - // 2nd row: Circles - pp.circle(margin + size/2, 2*margin + size + size/2, size, false); - labelText("Simple circle", margin + size/2, 2*margin + 2*size + 20); + println("Drawing 5th row: Bézier curves"); + pg.pushMatrix(); + pg.translate(margin, 5*margin + 4*size); + { + float curveHeight = size/2; + + // Simple bezier + println(" Drawing simple bezier"); + pp.bezier(0, size/2, // Start point + size/3, 0, // Control point 1 + 2*size/3, size, // Control point 2 + size, size/2, // End point + false); // No fade + labelText("Simple bezier", size/2, size + 20); + + // Bezier with fade + println(" Drawing bezier with fade"); + pg.translate(margin + size, 0); + pp.bezier(0, size/2, // Start point + size/3, 0, // Control point 1 + 2*size/3, size, // Control point 2 + size, size/2, // End point + true); // With fade + labelText("Bezier with fade", size/2, size + 20); + } + pg.popMatrix(); - pp.circle(2*margin + size + size/2, 2*margin + size + size/2, size, true); - labelText("Circle with fade", 2*margin + size + size/2, 2*margin + 2*size + 20); + println("Drawing 6th row: Splines"); + pg.pushMatrix(); + pg.translate(margin, 6*margin + 5*size); + { + // Simple spline + println(" Drawing simple spline"); + float[] points = { + 0, size/2, // First point + size/3, size/4, // Second point + 2*size/3, 3*size/4, // Third point + size, size/2 // Fourth point + }; + pp.spline(points, false, false); + labelText("Simple spline", size/2, size + 20); + + // Full fade spline + println(" Drawing full fade spline"); + pg.translate(margin + size, 0); + float[] fadePoints = { + 0, size/2, // First point + size/3, size/4, // Second point + 2*size/3, 3*size/4, // Third point + size, size/2 // Fourth point + }; + pp.spline(fadePoints, true, false); + labelText("Full fade", size/2, size + 20); + + // First segment fade spline + println(" Drawing first segment fade spline"); + pg.translate(margin + size, 0); + float[] firstSegmentFadePoints = { + 0, size/2, // First point + size/3, size/4, // Second point + 2*size/3, 3*size/4, // Third point + size, size/2 // Fourth point + }; + pp.spline(firstSegmentFadePoints, true, true); + labelText("First segment fade", size/2, size + 20); + } + pg.popMatrix(); + + println("Drawing 7th row: Masking examples"); + pg.pushMatrix(); + pg.translate(margin, 7*margin + 6*size); + { + // Hard mask (circular) + println(" Creating circular mask"); + PGraphics mask = pp.resetMask(); + mask.noStroke(); + mask.fill(255); + mask.circle(size/2, size/2, size); + println(" Enabling circular mask"); + pp.useMask(); + + // Draw diagonal lines inside mask + println(" Drawing vertical lines with circular mask"); + for (int i = 0; i <= 10; i++) { + float x = i*(size/10); + pp.line(x, 0, x, size, false); + } + labelText("Hard mask", size/2, size + 20); + + // Soft mask (gradient) + println(" Creating gradient mask"); + pg.translate(margin + size, 0); + mask = pp.resetMask(); + mask.beginDraw(); + for (int y = 0; y < size; y++) { + float alpha = map(y, 0, size, 255, 0); + mask.stroke(255, 255, 255, alpha); + mask.line(0, y, size, y); + } + mask.endDraw(); + println(" Enabling gradient mask"); + pp.useMask(); + + // Draw vertical lines with gradient mask + println(" Drawing vertical lines with gradient mask"); + for (int i = 0; i <= 10; i++) { + float x = i*(size/10); + pp.line(x, 0, x, size, false); + } + labelText("Soft mask", size/2, size + 20); + + // Reset to no mask for future drawings + mask = pp.resetMask(); + } + pg.popMatrix(); + + println(" Text drawing"); + pg.pushMatrix(); + pg.translate(margin, 8*margin + 7*size); + { + int textSize = 15; + int spacing = 5; + pp.text("0123456789", 0, 0, textSize); + pp.text("abcdefghijklmnopqrstuvwxyz", 0, textSize + spacing, textSize); + pp.text("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0, 2 * (textSize + spacing), textSize); + labelText("Text drawing example", size/2, size + 20); + } + pg.popMatrix(); + + println(" Hatch quadrilateral"); + pg.pushMatrix(); + pg.translate(margin, 9*margin + 8*size); + { + pp.hatchPolygon(size * 0.4, size * 0.4, + size, 0, + size * 0.8, size, + size * 0.2, size); + labelText("Hatch quadrilateral", size/2, size + 20); + } + pg.popMatrix(); + + pp.resetMask(); // Reset mask for future drawings + println("Finished drawExample()"); + pg.endDraw(); + // Detach target so accidental window drawing later isn't misrouted + pp.clearTarget(); +} + +void setup() { + colorMode(HSB, 360, 100, 100, 100); + + pp = new PaperAndPencil(this); + if (USE_OFFSCREEN) { + offscreen = createGraphics(OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT, P2D); + offscreen.beginDraw(); + offscreen.colorMode(HSB, 360, 100, 100, 100); + offscreen.endDraw(); + } + pg = USE_OFFSCREEN ? offscreen : g; + + // Generate and save for each quality mode + println("Drawing DRAFT quality..."); + pp.setQualityMode(PaperAndPencil.QualityMode.DRAFT); + drawExample(); + pg.save("BasicExample_DRAFT.png"); - pp.fillCircle(3*margin + 2*size + size/2, 2*margin + size + size/2, size); - labelText("Filled circle", 3*margin + 2*size + size/2, 2*margin + 2*size + 20); + println("Drawing SCREEN quality..."); + pp.setQualityMode(PaperAndPencil.QualityMode.SCREEN); + drawExample(); + pg.save("BasicExample_SCREEN.png"); - // 3rd row: Arcs - pp.arc(margin + size/2, 3*margin + 2*size + size/2, size, -QUARTER_PI, PI, false); - labelText("Simple arc", margin + size/2, 3*margin + 3*size + 20); + println("Drawing PRINT quality..."); + pp.setQualityMode(PaperAndPencil.QualityMode.PRINT); + drawExample(); + pg.save("BasicExample_PRINT.png"); - pp.arc(2*margin + size + size/2, 3*margin + 2*size + size/2, size, -QUARTER_PI, PI, true); - labelText("Arc with fade", 2*margin + size + size/2, 3*margin + 3*size + 20); - - // 4th row: Rectangles - pp.rect(margin, 4*margin + 3*size, size, size); - labelText("Simple rect", margin + size/2, 4*margin + 4*size + 20); - - pp.fillRect(2*margin + size, 4*margin + 3*size, size, size); - labelText("Filled rect", 2*margin + size + size/2, 4*margin + 4*size + 20); - - // 5th row: Bézier curves - float curveHeight = size/2; // Control height of the curve - - pp.bezier(margin, 5*margin + 4*size + size/2, // Start point - margin + size/3, 5*margin + 4*size, // Control point 1 - margin + 2*size/3, 5*margin + 5*size, // Control point 2 - margin + size, 5*margin + 4*size + size/2, // End point - false); // No fade - labelText("Simple bezier", margin + size/2, 5*margin + 5*size + 20); - - pp.bezier(2*margin + size, 5*margin + 4*size + size/2, // Start point - 2*margin + size + size/3, 5*margin + 4*size, // Control point 1 - 2*margin + size + 2*size/3, 5*margin + 5*size, // Control point 2 - 2*margin + 2*size, 5*margin + 4*size + size/2, // End point - true); // With fade - labelText("Bezier with fade", 2*margin + size + size/2, 5*margin + 5*size + 20); - - // 6th row: Splines - float[] points = { - margin, 6*margin + 5*size + size/2, // First point - margin + size/3, 6*margin + 5*size + size/4, // Second point - margin + 2*size/3, 6*margin + 5*size + 3*size/4, // Third point - margin + size, 6*margin + 5*size + size/2 // Fourth point - }; - pp.spline(points, false); - labelText("Simple spline", margin + size/2, 6*margin + 6*size + 20); - - float[] fadePoints = { - 2*margin + size, 6*margin + 5*size + size/2, // First point - 2*margin + size + size/3, 6*margin + 5*size + size/4, // Second point - 2*margin + size + 2*size/3, 6*margin + 5*size + 3*size/4, // Third point - 2*margin + 2*size, 6*margin + 5*size + size/2 // Fourth point - }; - pp.spline(fadePoints, true, false); - labelText("Full fade", 2*margin + size + size/2, 6*margin + 6*size + 20); - - float[] firstSegmentFadePoints = { - 3*margin + 2*size, 6*margin + 5*size + size/2, // First point - 3*margin + 2*size + size/3, 6*margin + 5*size + size/4, // Second point - 3*margin + 2*size + 2*size/3, 6*margin + 5*size + 3*size/4,// Third point - 3*margin + 3*size, 6*margin + 5*size + size/2 // Fourth point - }; - pp.spline(firstSegmentFadePoints, true, true); - labelText("First segment fade", 3*margin + 2*size + size/2, 6*margin + 6*size + 20); + println("All examples completed"); + exit(); } void draw() { // Static sketch, no animation needed + if (USE_OFFSCREEN && offscreen != null) { + image(offscreen, 0, 0, width, height); + } } -void keyPressed() { - if (key == 's' || key == 'S') { - save("BasicExample.png"); - } +// Required for external (non-PDE) runs: size() must be in settings() +void settings() { + size(OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT, P2D); } \ No newline at end of file diff --git a/examples/BasicExample/BasicExample.png b/examples/BasicExample/BasicExample.png deleted file mode 100644 index 984177c..0000000 Binary files a/examples/BasicExample/BasicExample.png and /dev/null differ diff --git a/examples/BasicExample/BasicExample_DRAFT.png b/examples/BasicExample/BasicExample_DRAFT.png new file mode 100644 index 0000000..07eab02 Binary files /dev/null and b/examples/BasicExample/BasicExample_DRAFT.png differ diff --git a/examples/BasicExample/BasicExample_PRINT.png b/examples/BasicExample/BasicExample_PRINT.png new file mode 100644 index 0000000..e12da40 Binary files /dev/null and b/examples/BasicExample/BasicExample_PRINT.png differ diff --git a/examples/BasicExample/BasicExample_SCREEN.png b/examples/BasicExample/BasicExample_SCREEN.png new file mode 100644 index 0000000..dc875a9 Binary files /dev/null and b/examples/BasicExample/BasicExample_SCREEN.png differ diff --git a/library/PaperAndPencil.jar b/library/PaperAndPencil.jar index 0eb54d2..87e5b4f 100644 Binary files a/library/PaperAndPencil.jar and b/library/PaperAndPencil.jar differ diff --git a/src/paperandpencil/PaperAndPencil.java b/src/paperandpencil/PaperAndPencil.java index c5a0e72..5b31630 100644 --- a/src/paperandpencil/PaperAndPencil.java +++ b/src/paperandpencil/PaperAndPencil.java @@ -1,6 +1,8 @@ package paperandpencil; import processing.core.PApplet; +import processing.core.PConstants; +import processing.core.PGraphics; /** * PaperAndPencil provides utilities for creating paper-like textures and pencil-like drawing effects @@ -8,11 +10,23 @@ * randomness and imperfection to standard geometric shapes. */ public class PaperAndPencil { - PApplet p; + public PApplet p; int pencilColor; boolean printMode; float pencilSpread; + PGraphics maskBuffer; + boolean useMask = false; + // Optional drawing target (off-screen). When non-null, all rendering goes there instead of the main surface + private PGraphics target; + public enum QualityMode { + DRAFT, // Fast rendering with fewer points + SCREEN, // Normal screen quality + PRINT // High quality for print output + } + + private QualityMode qualityMode = QualityMode.SCREEN; + /** * Creates a new PaperAndPencil instance. * @@ -24,24 +38,142 @@ public PaperAndPencil(PApplet p) { this.printMode = false; this.pencilSpread = 2f; } + + /** + * Sets an off-screen / alternative PGraphics target. All subsequent drawing commands + * will use this target until clearTarget() is called (or another target is set). + * Passing null reverts to the main sketch surface. + */ + public void setTarget(PGraphics pg) { + this.target = pg; + } + + /** Clear any previously set drawing target. */ + public void clearTarget() { this.target = null; } + + /** Returns currently active graphics context (target if set, else the main sketch surface). */ + private PGraphics g() { return target != null ? target : p.g; } + + /** + * Initializes and returns the mask buffer for drawing. + * When drawing, the mask's alpha channel is used to determine the opacity of the pencil strokes: + * Anything transparent on the mask will allow the pencil strokes to show through. + * Anything opaque will block the pencil strokes. + * The opacity of the mask is set to 0 (fully transparent). + * + * The mask won't actually be used until useMask() is called. + * + * @see #useMask() + * + * @return The PGraphics object for the mask buffer + */ + public PGraphics resetMask() { + int w = (target != null ? target.width : p.width); + int h = (target != null ? target.height : p.height); + if (this.maskBuffer == null || this.maskBuffer.width != w || this.maskBuffer.height != h) { + this.maskBuffer = p.createGraphics(w, h); + } + this.maskBuffer.beginDraw(); + this.maskBuffer.clear(); + this.useMask = false; + return this.maskBuffer; + } + + /** + * Ends the mask drawing. From this point, the mask will be used to control the opacity of pencil strokes. + * resetMask() must be called first to create the mask buffer. + */ + public void useMask() { + if (this.maskBuffer == null) { + System.err.println("Error: No mask buffer created. Call resetMask() first."); + return; + } + this.maskBuffer.endDraw(); + this.useMask = true; + } + /** + * Sets the rendering quality mode + * @param mode DRAFT for fast preview, SCREEN for normal display, PRINT for high quality output + */ + public void setQualityMode(QualityMode mode) { + this.qualityMode = mode; + this.printMode = (mode == QualityMode.PRINT); // Maintain backward compatibility + } + + /** + * Calculate increment size for drawing operations based on quality mode + */ + private float getIncrement(float baseSize) { + float base; + switch(qualityMode) { + case DRAFT: + base = 0.4f; // Fewer points for speed + break; + case PRINT: + base = 0.1f; // Slightly denser for print + break; + case SCREEN: + default: + base = 0.15f; // Normal density + break; + } + return base / baseSize; + } + + /** + * Calculate fill pattern increment based on quality mode + */ + private float getFillIncrement() { + switch(qualityMode) { + case DRAFT: + return 3.0f; // Larger gaps for speed + case PRINT: + return 0.8f; // Tighter gaps for more solid fill + case SCREEN: + default: + return 1.2f; // Normal gaps + } + } + + /** + * Calculate number of paper texture points based on quality mode + */ + private int getPaperTexturePoints() { + switch(qualityMode) { + case DRAFT: + return 10000; + case SCREEN: + default: + return 100000; + } + } + /** * Creates a textured paper background effect by randomly placing small colored circles * and then applying a lightening effect to create a natural paper texture. */ public void paper() { - p.noStroke(); - for (int i = 0; i < 100000; ++i) { - p.fill(p.random(360), p.random(100), p.random(100), p.random(20)); - float x = p.random(p.width); - float y = p.random(p.height); - p.circle(x, y, p.random(2)); + // Skip paper texture in PRINT mode since we'll be printing to real paper + if (qualityMode == QualityMode.PRINT) { + return; + } + PGraphics g = g(); + g.noStroke(); + int points = getPaperTexturePoints(); + + for (int i = 0; i < points; ++i) { + g.fill(p.random(360), p.random(100f), p.random(100f), p.random(20f)); + float x = p.random(g.width); + float y = p.random(g.height); + g.circle(x, y, p.random(2)); } - p.loadPixels(); - for (int i = 0; i < p.pixels.length; i += 1) { - p.pixels[i] = p.lerpColor(p.pixels[i], p.color(360), p.random(.5f)); + + g.loadPixels(); + for (int i = 0; i < g.pixels.length; i += 1) { + g.pixels[i] = p.lerpColor(g.pixels[i], p.color(360), p.random(0.5f)); } - p.updatePixels(); + g.updatePixels(); } /** @@ -62,6 +194,11 @@ public int getPencilColor() { return this.pencilColor; } + /** + * Sets the spread of the pencil strokes. Higher values create a more textured effect. + * + * @param spread The spread value (default is 2.0) + */ public void setPencilSpread(float spread) { this.pencilSpread = spread; } @@ -85,11 +222,85 @@ public void setPrintMode(boolean mode) { * @param fade if true, applies a fade effect to the stroke */ public void circle(float centerX, float centerY, float diameter, boolean fade) { - arc(centerX, centerY, diameter, 0, p.TWO_PI, fade); + arc(centerX, centerY, diameter, 0, PConstants.TWO_PI, fade); + } + + private float getPencilSpreadForMode() { + switch(qualityMode) { + case DRAFT: + return pencilSpread * 1.5f; // More spread for faster, rougher preview + case PRINT: + return pencilSpread * 0.3f; // Much less spread for cleaner print output + case SCREEN: + default: + return pencilSpread; // Normal spread + } } + /** + * Draws a dot with a pencil-like effect at the specified coordinates, with random spread and optional masking. + * + * @param x x-coordinate of the dot + * @param y y-coordinate of the dot + */ public void dot(float x, float y) { - p.circle(x + p.random(this.pencilSpread), y + p.random(this.pencilSpread), p.random(2)); + PGraphics g = g(); + if (x < 0 || x >= g.width || y < 0 || y >= g.height) return; + + float opacity = 1.0f; + if (useMask && maskBuffer != null) { + // Get the alpha channel value from the mask (0-255) + int maskAlpha = (maskBuffer.get((int)x, (int)y) >> 24) & 0xFF; + // Convert to 0-1 range and use it to scale our opacity + opacity = 1f - (maskAlpha / 255.0f); + if (opacity == 0) return; // Skip fully masked pixels + } + + // Save current fill color + int originalFill = g.fillColor; + + float spread = getPencilSpreadForMode(); + float size = qualityMode == QualityMode.PRINT ? 1.5f : p.random(2); + + if (opacity < 1f) { + // Get the current fill color's components + float h = p.hue(pencilColor); + float s = p.saturation(pencilColor); + float b = p.brightness(pencilColor); + float a = p.alpha(pencilColor) * opacity; // Blend the alpha with mask opacity + + // Apply the modified alpha + g.fill(h, s, b, a); + } + + g.circle(x + p.random(spread), y + p.random(spread), size); + + // Restore original fill color + g.fill(originalFill); + } + + /** + * Sets up the common drawing state used across drawing methods + */ + private void setupDrawingState() { + PGraphics g = g(); + g.noStroke(); + g.fill(pencilColor); + } + + /** + * Sets the fill color with a specific alpha value while preserving the current color's other components. + * In print mode, applies contrast enhancement to make lights lighter and darks darker. + */ + private void setFillWithAlpha(float alpha) { + // In print mode, apply non-linear contrast enhancement + if (qualityMode == QualityMode.PRINT) { + // Apply curve to increase contrast - makes lights lighter and darks darker + alpha = (float)Math.pow(alpha, 1.5); + } + PGraphics g = g(); + g.fill(p.hue(pencilColor), p.saturation(pencilColor), + p.brightness(pencilColor), p.alpha(pencilColor) * alpha); } /** @@ -103,20 +314,30 @@ public void dot(float x, float y) { * @param fade if true, applies a fade effect to the stroke */ public void arc(float centerX, float centerY, float diameter, float startAngle, float endAngle, boolean fade) { - p.noStroke(); - p.fill(pencilColor); + setupDrawingState(); float x, y; + float baseIncrement; + switch(qualityMode) { + case DRAFT: + baseIncrement = 0.6f; + break; + case PRINT: + baseIncrement = 0.15f; + break; + case SCREEN: + default: + baseIncrement = 0.3f; + break; + } - for (float theta = startAngle; theta < endAngle; theta += 0.3f/diameter) { + for (float theta = startAngle; theta < endAngle; theta += baseIncrement/diameter) { if (fade) { float fadeProgress = (theta - startAngle) / (endAngle - startAngle); - p.fill(p.hue(pencilColor), p.saturation(pencilColor), - p.brightness(pencilColor), - p.alpha(pencilColor) * fadeProgress); + setFillWithAlpha(fadeProgress); } - x = centerX + diameter/2 * p.cos(theta); - y = centerY + diameter/2 * p.sin(theta); + x = centerX + diameter/2 * PApplet.cos(theta); + y = centerY + diameter/2 * PApplet.sin(theta); dot(x, y); } } @@ -131,21 +352,18 @@ public void arc(float centerX, float centerY, float diameter, float startAngle, * @param fade if true, applies a fade effect from start to end */ public void line(float x1, float y1, float x2, float y2, boolean fade) { - p.noStroke(); - p.fill(pencilColor); + setupDrawingState(); float x, y; - float increment = 0.15f / p.dist(x1, y1, x2, y2); + float increment = getIncrement(PApplet.dist(x1, y1, x2, y2)); for (float amt = 0; amt < 1; amt += increment) { if (fade) { - p.fill(p.hue(pencilColor), p.saturation(pencilColor), - p.brightness(pencilColor), - p.alpha(pencilColor) * amt); + setFillWithAlpha(amt); } - x = p.lerp(x1, x2, amt); - y = p.lerp(y1, y2, amt); + x = PApplet.lerp(x1, x2, amt); + y = PApplet.lerp(y1, y2, amt); dot(x, y); } } @@ -166,10 +384,9 @@ public void rect(float leftX, float topY, float width, float height) { } public void fillRect(float leftX, float topY, float width, float height) { - float increment = 4f;//printMode ? 1.3f : 1.2f; + float increment = getFillIncrement(); float centerX = leftX + (width / 2); float centerY = topY + (height / 2); - // draw some bigger and bigger rectangles until the whole area is filled for (float size = 2; size < Math.max(width, height); size += increment) { float rectWidth = Math.min(size, width); float rectHeight = Math.min(size, height); @@ -179,19 +396,53 @@ public void fillRect(float leftX, float topY, float width, float height) { /** * Fills a circle with concentric pencil circles to create a filled effect. - * The spacing between circles is adjusted based on print mode. + * The spacing between circles is adjusted based on quality mode. * * @param centerX x-coordinate of the circle center * @param centerY y-coordinate of the circle center * @param diameter diameter of the circle */ public void fillCircle(float centerX, float centerY, float diameter) { - float increment = printMode ? 1.3f : 1.2f; + float increment = getFillIncrement(); for (float d = 2; d < diameter; d += increment) { circle(centerX, centerY, d, false); } } + /** + * Fills a polygon defined by four vertices with a hatching pattern. + * Hatching is different from solid fill - it creates a series of almost-parallel lines + * that give the impression of shading. + * Lines are drawn parallel to the edge (x1, y1) - (x2, y2), spaced based on quality mode. + * + * @param x1 x-coordinate of the first vertex + * @param y1 y-coordinate of the first vertex + * @param x2 x-coordinate of the second vertex + * @param y2 y-coordinate of the second vertex + * @param x3 x-coordinate of the third vertex + * @param y3 y-coordinate of the third vertex + * @param x4 x-coordinate of the fourth vertex + * @param y4 y-coordinate of the fourth vertex + */ + public void hatchPolygon(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) { + float increment = getFillIncrement() / PApplet.dist(x1, y1, x2, y2); + for (float t = 0; t <= 1; t += increment) { + float startX = PApplet.lerp(x1, x4, t); + float startY = PApplet.lerp(y1, y4, t); + float endX = PApplet.lerp(x2, x3, t); + float endY = PApplet.lerp(y2, y3, t); + + // sligthly randomize the start and end points y to avoid perfect parallel lines + // capped by polygon coords + startY += p.random(-getPencilSpreadForMode(), getPencilSpreadForMode()); + startY = PApplet.constrain(startY, Math.min(y1, y4), Math.max(y1, y4)); + endY += p.random(-getPencilSpreadForMode(), getPencilSpreadForMode()); + endY = PApplet.constrain(endY, Math.min(y2, y4), Math.max(y2, y4)); + + line(startX, startY, endX, endY, false); + } + } + /** * Helper method to calculate fade alpha value based on progress */ @@ -206,22 +457,26 @@ private float calculateFadeAlpha(float progress, float fadeStart, float fadeEnd) private void plotBezierCurve(float x1, float y1, float cx1, float cy1, float cx2, float cy2, float x2, float y2, boolean fade, float fadeStart, float fadeEnd) { - p.noStroke(); - p.fill(pencilColor); + PGraphics g = g(); + g.noStroke(); // Always disable stroke + + float approxLength = PApplet.dist(x1, y1, cx1, cy1) + + PApplet.dist(cx1, cy1, cx2, cy2) + + PApplet.dist(cx2, cy2, x2, y2); - // Approximate curve length by using the polygon length of control points - float approxLength = p.dist(x1, y1, cx1, cy1) + - p.dist(cx1, cy1, cx2, cy2) + - p.dist(cx2, cy2, x2, y2); + float increment = getIncrement(approxLength); - // Scale increment based on approximate curve length and print mode - float increment = (printMode ? 0.075f : 0.15f) / approxLength; + // If not fading, set the fill color once at the start + if (!fade) { + g.fill(pencilColor); + } for (float t = 0; t <= 1; t += increment) { if (fade) { - float alpha = calculateFadeAlpha(t, fadeStart, fadeEnd); - p.fill(p.hue(pencilColor), p.saturation(pencilColor), - p.brightness(pencilColor), alpha); + float fadeProgress = fadeStart + (fadeEnd - fadeStart) * t; + g.fill(p.hue(pencilColor), p.saturation(pencilColor), + p.brightness(pencilColor), + p.alpha(pencilColor) * fadeProgress); } float x = p.bezierPoint(x1, cx1, cx2, x2, t); @@ -310,4 +565,119 @@ public void spline(float[] points, boolean fade, boolean fadeFirstSegmentOnly) { currentSegment++; } } -} \ No newline at end of file + + /** + * Draws text at the specified position using the pencil effect. + * Alphabet is very limited at the moment and only supports 0-9 and some uppercase letters. + * + * @param txt The text string to draw + * @param x x-coordinate of the text position + * @param y y-coordinate of the text position + */ + public void text(String txt, float x, float y, float size) { + if (txt == null || txt.length() == 0) return; + float spacing = 2; // Spacing between characters + float currentX = x; + + for (int i = 0; i < txt.length(); i++) { + char c = txt.charAt(i); + currentX = x + i * (size * 0.75f + spacing); + if (c >= 'A' && c <= 'Z') { + // Uppercase letters + if (c == 'A') { + line(currentX + size*0.5f, y, currentX + size*0.25f, y + size, false); + line(currentX + size*0.5f, y, currentX + size*0.75f, y + size, false); + line(currentX + size*0.25f, y + size*0.75f, currentX + size*0.75f, y + size*0.75f, false); + continue; + } else if (c == 'B') { + line(currentX + size*0.25f, y, currentX + size*0.25f, y + size, false); + arc(currentX + size*0.5f, y + size*0.25f, size*0.5f, -PConstants.PI/2, PConstants.PI/2, false); + arc(currentX + size*0.5f, y + size*0.75f, size*0.5f, -PConstants.PI/2, PConstants.PI/2, false); + // horizontal lines to close the loops + line(currentX + size*0.25f, y, currentX + size*0.5f, y, false); + line(currentX + size*0.25f, y + size*0.5f, currentX + size*0.5f, y + size*0.5f, false); + line(currentX + size*0.25f, y + size, currentX + size*0.5f, y + size, false); + continue; + } else if (c == 'D') { + line(currentX + size*0.25f, y, currentX + size*0.25f, y + size, false); + arc(currentX + size/4, y + size/2, size, -PConstants.PI/2, PConstants.PI/2, false); + continue; + } else if (c == 'F') { + line(currentX + size*0.75f, y, currentX + size*0.25f, y, false); + line(currentX + size*0.25f, y, currentX + size*0.25f, y + size, false); + line(currentX + size*0.75f, y + size*0.5f, currentX + size*0.25f, y + size*0.5f, false); + continue; + } else if (c == 'L') { + line(currentX + size*0.25f, y, currentX + size*0.25f, y + size, false); + line(currentX + size*0.25f, y + size, currentX + size*0.75f, y + size, false); + continue; + } else if (c == 'R') { + line(currentX + size*0.25f, y, currentX + size*0.25f, y + size, false); + arc(currentX + size/2, y + size*0.25f, size/2, -PConstants.PI/2, PConstants.PI/2, false); + line(currentX + size*0.25f, y + size*0.5f, currentX + size*0.75f, y + size, false); + // horizontal lines to close the loop + line(currentX + size*0.25f, y, currentX + size*0.5f, y, false); + line(currentX + size*0.25f, y + size*0.5f, currentX + size*0.5f, y + size*0.5f, false); + continue; + } else if (c == 'U') { + line(currentX + size*0.25f, y, currentX + size*0.25f, y + size*0.75f, false); + line(currentX + size*0.75f, y, currentX + size*0.75f, y + size*0.75f, false); + arc(currentX + size/2, y + size*0.75f, size/2, 0, PConstants.PI, false); + continue; + } + fillRect(currentX, y, size, size); + } else if (c >= 'a' && c <= 'z') { + // Lowercase letters + fillRect(currentX, y, size, size); + } else if (c >= '0' && c <= '9') { + // Digits + if (c == '0') { + circle(currentX + size/2, y + size/2, size, false); + // diagonal line to distinguish from 'O' + line(currentX + size*0.3f, y + size*0.7f, currentX + size*0.7f, y + size*0.3f, false); + continue; + } else if (c == '1') { + line(currentX + size/2, y, currentX + size/2, y + size, false); + continue; + } else if (c == '2') { + arc(currentX + size/2, y + size/4, size/2, PConstants.PI, PConstants.PI * 2, false); + line(currentX + size*0.75f, y + size/4, currentX + size*0.25f, y + size, false); + line(currentX + size*0.25f, y + size, currentX + size*0.75f, y + size, false); + continue; + } else if (c == '3') { + arc(currentX + size/2, y + size/4, size/2, -3*PConstants.PI/4, PConstants.PI/2, false); + arc(currentX + size/2, y + size*0.75f, size/2, -PConstants.PI/2, 3*PConstants.PI/4, false); + continue; + } else if (c == '4') { + line(currentX + size*0.75f, y, currentX + size*0.75f, y + size, false); + line(currentX + size*0.25f, y + size*0.75f, currentX + size*0.75f, y + size*0.75f, false); + line(currentX + size*0.75f, y, currentX + size*0.25f, y + size*0.75f, false); + continue; + } else if (c == '5') { + line(currentX + size*0.25f, y, currentX + size*0.75f, y, false); + line(currentX + size*0.25f, y, currentX + size*0.25f, y + size*0.5f, false); + line(currentX + size*0.25f, y + size*0.5f, currentX + size*0.5f, y + size*0.5f, false); + arc(currentX + size/2, y + size*0.75f, size/2, -PConstants.PI/2, PConstants.PI, false); + continue; + } else if (c == '6') { + circle(currentX + size*0.5f, y + size*0.75f, size*0.5f, false); + line(currentX + size*0.3f, y + size*0.7f, currentX + size*0.5f, y, false); + continue; + } else if (c == '7') { + line(currentX + size*0.25f, y, currentX + size*0.75f, y, false); + line(currentX + size*0.75f, y, currentX + size*0.5f, y + size, false); + continue; + } else if (c == '8') { + circle(currentX + size*0.5f, y + size*0.25f, size*0.5f, false); + circle(currentX + size*0.5f, y + size*0.75f, size*0.5f, false); + continue; + } else if (c == '9') { + circle(currentX + size*0.5f, y + size*0.25f, size*0.5f, false); + line(currentX + size*0.7f, y + size*0.3f, currentX + size*0.5f, y + size, false); + continue; + } + fillRect(currentX, y, size, size); + } + } + } +}