Last active
November 3, 2025 10:29
-
-
Save AloiSama/ee927b254a97b606721ddc119d1a49e3 to your computer and use it in GitHub Desktop.
Tree gen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Lumber Mill Simulator (Fixed & Debugged)</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background-color: #f0f0f0; | |
| padding: 15px; | |
| margin: 0; | |
| } | |
| #debug-text { | |
| font-size: 14px; | |
| color: #666; | |
| background-color: #eee; | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| } | |
| #canvas-container { | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| } | |
| #controls { | |
| margin-top: 15px; | |
| display: flex; | |
| gap: 15px; | |
| } | |
| button { | |
| font-size: 16px; | |
| padding: 10px 15px; | |
| border: none; | |
| border-radius: 5px; | |
| background-color: #007aff; | |
| color: white; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| } | |
| button:hover { | |
| background-color: #0056b3; | |
| } | |
| .generate-btn { | |
| background-color: #34c759 !important; | |
| } | |
| .generate-btn:hover { | |
| background-color: #2a9e48 !important; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <p id="debug-text">Debug: p5.js is loading...</p> | |
| <div id="canvas-container"></div> | |
| <div id="controls"></div> | |
| <script> | |
| // --- GLOBALS --- | |
| let tree; | |
| let currentBoard = null; | |
| let debugP; // Reference to the debug <p> tag | |
| // Colors | |
| const C_LIGHT = [227, 207, 177]; // Earlywood | |
| const C_DARK = [163, 131, 103]; // Latewood | |
| const C_BARK = [89, 58, 29]; | |
| const C_PITH = [112, 73, 37]; | |
| // Layout | |
| const W = 1000; // Canvas Width | |
| const H = 600; // Canvas Height | |
| const TREE_CX = W * 0.3; | |
| const TREE_CY = H / 2; | |
| const BOARD_CX = W * 0.7; | |
| const BOARD_CY = H / 2; | |
| // --- p5.js SETUP & DRAW --- | |
| function setup() { | |
| let canvas = createCanvas(W, H); | |
| canvas.parent('canvas-container'); | |
| // Get debug text reference | |
| debugP = document.getElementById('debug-text'); | |
| // --- BUTTONS --- | |
| let btn; | |
| btn = createButton('Generate New Tree'); | |
| btn.parent('controls'); | |
| btn.class('generate-btn'); // Add class for styling | |
| btn.mousePressed(generateNewTree); | |
| btn = createButton('Cut Flatsawn Board'); | |
| btn.parent('controls'); | |
| btn.mousePressed(() => cutBoard('flatsawn')); | |
| btn = createButton('Cut Quartersawn Board'); | |
| btn.parent('controls'); | |
| btn.mousePressed(() => cutBoard('quartersawn')); | |
| btn = createButton('Cut Riftsawn Board'); | |
| btn.parent('controls'); | |
| btn.mousePressed(() => cutBoard('riftsawn')); | |
| noiseDetail(8, 0.45); | |
| generateNewTree(); | |
| // --- DEBUG --- | |
| debugP.innerHTML = "Debug: p5.js setup complete. Code is running."; | |
| debugP.style.color = 'green'; | |
| } | |
| function draw() { | |
| try { | |
| background(250); | |
| // Draw the tree cross-section | |
| tree.drawCrossSection(TREE_CX, TREE_CY); | |
| // If a board is cut, draw its location on the tree | |
| if (currentBoard) { | |
| currentBoard.drawCutLocation(TREE_CX, TREE_CY); | |
| currentBoard.drawDisplay(BOARD_CX, BOARD_CY); | |
| } | |
| // Draw UI labels | |
| drawLabels(); | |
| } catch (e) { | |
| // --- DEBUG --- | |
| // If the draw loop fails, report it! | |
| debugP.innerHTML = `Debug: ERROR in draw() loop: ${e.message}`; | |
| debugP.style.color = 'red'; | |
| noLoop(); // Stop the loop from crashing | |
| } | |
| } | |
| function drawLabels() { | |
| fill(50); | |
| noStroke(); | |
| textAlign(CENTER, CENTER); | |
| textSize(18); | |
| text("Tree Cross-Section", TREE_CX, H - 30); | |
| if (currentBoard) { | |
| let name = currentBoard.type.charAt(0).toUpperCase() + currentBoard.type.slice(1); | |
| text(`${name} Board`, BOARD_CX, H - 30); | |
| textSize(14); | |
| fill(100); | |
| text("Face Grain", BOARD_CX, BOARD_CY - 135); | |
| text("End Grain", BOARD_CX, BOARD_CY + 115); | |
| } | |
| } | |
| // --- CORE FUNCTIONS --- | |
| function generateNewTree() { | |
| tree = new Tree(); | |
| currentBoard = null; | |
| } | |
| function cutBoard(type) { | |
| currentBoard = new Board(type, tree); | |
| } | |
| // --- TREE CLASS --- | |
| class Tree { | |
| constructor() { | |
| this.radius = random(120, 180); | |
| this.pith = createVector(random(-15, 15), random(-15, 15)); | |
| this.ellipticity = random(0.9, 1.1); // How oval-shaped | |
| this.numRings = int(random(30, 50)); | |
| this.noiseSeed = random(1000); // For grain wobble | |
| this.ringSpacing = this.radius / this.numRings; | |
| } | |
| // Main function to draw the tree's end grain | |
| drawCrossSection(cx, cy) { | |
| push(); | |
| translate(cx + this.pith.x, cy + this.pith.y); | |
| // Bark | |
| fill(C_BARK); | |
| noStroke(); | |
| this.drawRing(this.radius + 8, this.ellipticity); | |
| // Wood | |
| fill(C_LIGHT); | |
| noStroke(); | |
| this.drawRing(this.radius, this.ellipticity); | |
| // Rings | |
| noFill(); | |
| strokeWeight(1.5); | |
| for (let i = 1; i <= this.numRings; i++) { | |
| let r = i * this.ringSpacing; | |
| let c = (i % 2 === 0) ? color(C_DARK) : lerpColor(color(C_DARK), color(C_LIGHT), 0.35); | |
| stroke(c); | |
| this.drawRing(r, this.ellipticity); | |
| } | |
| // Pith | |
| fill(C_PITH); | |
| noStroke(); | |
| ellipse(0, 0, 5, 5); | |
| pop(); | |
| } | |
| // Helper to draw one (wobbly) ring | |
| drawRing(radius, ellipticity) { | |
| let noiseAmount = radius * 0.05; // Wobble is proportional to radius | |
| beginShape(); | |
| for (let a = 0; a < TWO_PI; a += PI / 30) { | |
| let r = radius + map(noise(this.noiseSeed + a * 5, radius * 0.1), 0, 1, -noiseAmount, noiseAmount); | |
| let x = cos(a) * r * ellipticity; | |
| let y = sin(a) * r; | |
| vertex(x, y); | |
| } | |
| endShape(CLOSE); | |
| } | |
| } | |
| // --- BOARD CLASS --- | |
| class Board { | |
| constructor(type, tree) { | |
| this.type = type; | |
| this.tree = tree; | |
| this.boardWidth = 100; | |
| this.boardThickness = 25; | |
| this.boardLength = 350; // For display | |
| this.cutAngle = 0; // Angle of the cut relative to horizontal | |
| // Define cut position relative to pith (0,0) | |
| this.cutPos = createVector(0, 0); // (x, y) offset from pith | |
| if (type === 'flatsawn') { | |
| this.cutAngle = 0; // 0 degrees | |
| this.cutPos.y = random(tree.radius * 0.1, tree.radius * 0.6); | |
| } else if (type === 'quartersawn') { | |
| this.cutAngle = PI / 2; // 90 degrees | |
| this.cutPos.x = random(-tree.radius * 0.1, tree.radius * 0.1); | |
| } else if (type === 'riftsawn') { | |
| this.cutAngle = PI / 4; // 45 degrees | |
| this.cutPos.y = random(tree.radius * 0.1, tree.radius * 0.3); | |
| } | |
| } | |
| // Draw the board (face and end) | |
| drawDisplay(cx, cy) { | |
| this.drawFaceGrain(cx, cy - 80); | |
| this.drawEndGrain(cx, cy + 150); | |
| } | |
| // --- Draw the "Face Grain" (FIXED LOGIC) --- | |
| drawFaceGrain(cx, cy) { | |
| push(); | |
| translate(cx - this.boardLength / 2, cy - this.boardWidth / 2); | |
| // Draw board background | |
| fill(C_LIGHT); | |
| stroke(C_DARK); | |
| strokeWeight(1); | |
| rect(0, 0, this.boardLength, this.boardWidth); | |
| // --- Procedural Grain Logic (Pixel-by-pixel) --- | |
| let grainNoiseSeed = this.tree.noiseSeed + 1000; | |
| let grainStretch = 0.015; // How long the grain streaks are | |
| strokeWeight(1.5); // Draw points | |
| for (let x = 0; x < this.boardLength; x += 1) { // Iterate along length | |
| for (let y = 0; y < this.boardWidth; y += 1) { // Iterate along width | |
| // y_rel is the distance from the center of the board's width | |
| let y_rel = y - this.boardWidth / 2; | |
| // Calculate this pixel's (x,y) position in the end-grain plane | |
| let x_pos = this.cutPos.x + cos(this.cutAngle) * y_rel; | |
| let y_pos = this.cutPos.y + sin(this.cutAngle) * y_rel; | |
| // Calculate distance to pith in 2D | |
| let dist = sqrt(x_pos * x_pos + y_pos * y_pos); | |
| // Add organic wobble | |
| let n = noise(x * grainStretch, dist * 0.1, grainNoiseSeed); | |
| let wobble = map(n, 0, 1, -this.tree.ringSpacing, this.tree.ringSpacing); | |
| let final_dist = dist + wobble; | |
| // Find what fraction of a ring we are in | |
| let frac = (final_dist % this.tree.ringSpacing) / this.tree.ringSpacing; | |
| // Check if we are in the "latewood" (dark) part of a ring | |
| if (frac < 0.5) { // 50% latewood | |
| let c = lerpColor(color(C_DARK), color(C_LIGHT), frac / 0.5); | |
| stroke(c); | |
| point(x, y); // Draw the pixel | |
| } | |
| } | |
| } | |
| pop(); | |
| } | |
| // --- Draw the "End Grain" (FIXED LOGIC) --- | |
| drawEndGrain(cx, cy) { | |
| push(); | |
| // Go to top-left of end-grain box | |
| translate(cx - this.boardWidth / 2, cy - this.boardThickness / 2); | |
| // 1. Create clipping region | |
| beginShape(); | |
| vertex(0, 0); | |
| vertex(this.boardWidth, 0); | |
| vertex(this.boardWidth, this.boardThickness); | |
| vertex(0, this.boardThickness); | |
| endShape(CLOSE); | |
| clip(); | |
| // 2. Draw the *entire* tree, but translated and rotated | |
| // to match the cut. | |
| // We move our coordinate system so the *center* of the board's | |
| // end grain (which is at x=this.boardWidth/2, y=this.boardThickness/2) | |
| // aligns with the tree's cut position. | |
| translate(this.boardWidth / 2, this.boardThickness / 2); | |
| rotate(-this.cutAngle); | |
| translate(-this.cutPos.x, -this.cutPos.y); | |
| // Now draw the tree relative to its visual center (pith included) | |
| this.tree.drawCrossSection(TREE_CX, TREE_CY); | |
| pop(); | |
| } | |
| // --- Draw a rectangle on the main tree (FIXED LOGIC) --- | |
| drawCutLocation(tree_cx, tree_cy) { | |
| push(); | |
| // Go to tree's visual center (including pith offset) | |
| translate(tree_cx + this.tree.pith.x, tree_cy + this.tree.pith.y); | |
| // Go to cut location | |
| translate(this.cutPos.x, this.cutPos.y); | |
| // Rotate to match the cut angle | |
| rotate(this.cutAngle); | |
| // Draw the end-grain box | |
| noFill(); | |
| stroke(255, 0, 0); // Bright red | |
| strokeWeight(2.5); | |
| rectMode(CENTER); | |
| rect(0, 0, this.boardWidth, this.boardThickness); | |
| pop(); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment