package wordcram; import java.awt.Color; // awt: turns P5-land color into awt color, for renderers import java.awt.Shape; // awt: word->shape, skip if too small, pass it on import java.awt.geom.Rectangle2D; // awt: to get bounding box width & height import java.util.ArrayList; import java.util.ListIterator; import processing.core.PFont; import processing.core.PVector; class WordCramEngine { private final WordRenderer renderer; private final WordFonter fonter; private final WordSizer sizer; private final WordColorer colorer; private final WordAngler angler; private final WordPlacer placer; private final WordNudger nudger; private final ArrayList eWords; private final ListIterator eWordIter; private final ArrayList drawnWords = new ArrayList<>(); private final ArrayList skippedWords = new ArrayList<>(); private final RenderOptions renderOptions; private final Observer observer; // TODO Damn, really need to break down that list of arguments. WordCramEngine(WordRenderer renderer, Word[] words, WordFonter fonter, WordSizer sizer, WordColorer colorer, WordAngler angler, WordPlacer placer, WordNudger nudger, WordShaper shaper, BBTreeBuilder bbTreeBuilder, RenderOptions renderOptions, Observer observer) { this.renderer = renderer; this.fonter = fonter; this.sizer = sizer; this.colorer = colorer; this.angler = angler; this.placer = placer; this.nudger = nudger; this.observer = observer; this.renderOptions = renderOptions; this.eWords = wordsIntoEngineWords(words, shaper, bbTreeBuilder); this.eWordIter = eWords.listIterator(); } private ArrayList wordsIntoEngineWords(Word[] words, WordShaper wordShaper, BBTreeBuilder bbTreeBuilder) { ArrayList engineWords = new ArrayList<>(); int maxNumberOfWords = words.length; if (renderOptions.maxNumberOfWordsToDraw >= 0) { maxNumberOfWords = Math.min(maxNumberOfWords, renderOptions.maxNumberOfWordsToDraw); } for (int i = 0; i < maxNumberOfWords; i++) { Word word = words[i]; EngineWord eWord = new EngineWord(word, i, words.length, bbTreeBuilder); PFont wordFont = word.getFont(fonter); float wordSize = word.getSize(sizer, i, words.length); float wordAngle = word.getAngle(angler); Shape shape = wordShaper.getShapeFor(eWord.word.word, wordFont, wordSize, wordAngle); if (isTooSmall(shape, renderOptions.minShapeSize)) { skipWord(word, WordSkipReason.SHAPE_WAS_TOO_SMALL); } else { eWord.setShape(shape, renderOptions.wordPadding); engineWords.add(eWord); // DON'T add eWords with no shape. } } for (int i = maxNumberOfWords; i < words.length; i++) { skipWord(words[i], WordSkipReason.WAS_OVER_MAX_NUMBER_OF_WORDS); } return engineWords; } private boolean isTooSmall(Shape shape, int minShapeSize) { if (minShapeSize < 1) { minShapeSize = 1; } Rectangle2D r = shape.getBounds2D(); // Most words will be wider than tall, so this basically boils down to height. // For the odd word like "I", we check width, too. return r.getHeight() < minShapeSize || r.getWidth() < minShapeSize; } private void skipWord(Word word, WordSkipReason reason) { // TODO delete these properties when starting a sketch, in case it's a re-run w/ the same words. // NOTE: keep these as properties, because they (will be) deleted when the WordCramEngine re-runs. word.wasSkippedBecause(reason); skippedWords.add(word); observer.wordSkipped(word); } boolean hasMore() { return eWordIter.hasNext(); } void drawAll() { observer.beginDraw(); while(hasMore()) { drawNext(); } renderer.finish(); observer.endDraw(); } void drawNext() { if (!hasMore()) return; EngineWord eWord = eWordIter.next(); boolean wasPlaced = placeWord(eWord); if (wasPlaced) { // TODO unit test (somehow) drawWordImage(eWord); observer.wordDrawn(eWord.word); } } private boolean placeWord(EngineWord eWord) { Word word = eWord.word; Rectangle2D rect = eWord.getShape().getBounds2D(); // TODO can we move these into EngineWord.setDesiredLocation? Does that make sense? int wordImageWidth = (int)rect.getWidth(); int wordImageHeight = (int)rect.getHeight(); eWord.setDesiredLocation(placer, eWords.size(), wordImageWidth, wordImageHeight, renderer.getWidth(), renderer.getHeight()); // Set maximum number of placement trials int maxAttemptsToPlace = renderOptions.maxAttemptsToPlaceWord > 0 ? renderOptions.maxAttemptsToPlaceWord : calculateMaxAttemptsFromWordWeight(word); EngineWord lastCollidedWith = null; for (int attempt = 0; attempt < maxAttemptsToPlace; attempt++) { eWord.nudge(nudger.nudgeFor(word, attempt)); PVector loc = eWord.getCurrentLocation(); if (loc.x < 0 || loc.y < 0 || loc.x + wordImageWidth >= renderer.getWidth() || loc.y + wordImageHeight >= renderer.getHeight()) { continue; } if (lastCollidedWith != null && eWord.overlaps(lastCollidedWith)) { continue; } boolean foundOverlap = false; for (EngineWord otherWord : drawnWords) { if (eWord.overlaps(otherWord)) { foundOverlap = true; lastCollidedWith = otherWord; break; } } if (!foundOverlap) { eWord.finalizeLocation(); return true; } } skipWord(eWord.word, WordSkipReason.NO_SPACE); return false; } private int calculateMaxAttemptsFromWordWeight(Word word) { return (int)((1.0 - word.weight) * 600) + 100; } private void drawWordImage(EngineWord word) { drawnWords.add(word); renderer.drawWord(word, new Color(word.word.getColor(colorer), true)); } Word getWordAt(float x, float y) { int size = drawnWords.size() - 1; for (int i = size; i >= 0; i--) { EngineWord eWord = drawnWords.get(i); if (eWord.containsPoint(x, y)) { return eWord.word; } } return null; } Word[] getSkippedWords() { return skippedWords.toArray(new Word[0]); } float getProgress() { return (float) eWordIter.nextIndex() / eWords.size(); } }