Buchstabenwolke will nicht hinhauen...

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
johnkree
User
Beiträge: 4
Registriert: Sonntag 5. Februar 2023, 00:26

BildHallo!

Ich bin neu hier... ich sitz schon den halben Samstag an einem Problem:
Ich wollte für meinen Unterricht (ich bin Grundschullehrer) ein Script schreiben, dass mit automatisch die Buchstaben von Wörtern durcheinanderwürfelt. Das habe ich geschafft. Als nächste wollte ich, dass das Script die Wörter, die es per TXT einliest, als SVG ausgibt, damit ich es dann in Affinity Designer bearbeiten kann.
Soweit, sogut, aber ich schaffe es nicht, dass mir Wort für Wort als eigene Buchstabensuppe ausgegeben wird, sondern das Script legt die Buchstaben aller Eingabewörter immer wieder über dieselbe Stelle.

Code: Alles auswählen

import random
import svgwrite

def scramble_word_letters(word):
    letters = list(word)
    random.shuffle(letters)
    return "".join(letters)

def scramble_words_within_sentence(sentence):
    words = sentence.strip().split()
    scrambled_words = [scramble_word_letters(word) for word in words]
    return scrambled_words

def scramble_words_to_svg(file_in):
    with open(file_in, "r") as f_in:
        file_out = file_in.replace(".txt", "") + "_output.svg"
        dwg = svgwrite.Drawing(file_out, profile='tiny', size=(200, 200))

        x = 20
        y = 20  # initial y position
        for line in f_in:
            scrambled_sentences = scramble_words_within_sentence(line)
            for word in scrambled_sentences:
                for letter in word:
                    dwg.add(dwg.text(letter, insert=(x + random.uniform(-0.8, 0.8), y + random.uniform(-0.8, 0.8)), font_size=12))
                x += 20
                y += 20
                if x + 20 > 180:
                    x = 20
                if y + 20 > 180:
                    y = 20
        dwg.save()

file_name = input("Enter the name of the input file: ")
scramble_words_to_svg(file_name)
Das will ich erreichen (ohne die Wolke, die hab ich schon als Vektorgrafik):
https://imgur.com/C8ROIbv

Und das wird daraus:
https://imgur.com/UFAO4cg
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Deine Buchstaben scheinen ja ungefähr zehn Einheiten hoch zu sein. Du verteilst sie 1,6 Einheiten zufällig um einen Punkt herum. Natürlich liegen dann alle Buchstaben aufeinander, weil die Buchstaben im Verhältnis zu groß sind.
Es ist gar nicht so einfach, scheinbar zufällig Buchstaben gleichmäßig in einer Wolke zu verteilen.
Ich vermute dass auf all diesen Arbeitsblättern ein Mensch die Buchstaben positioniert hat. Der Aufwand, dafür ein Programm zu schreiben, steht, meiner Befürchtung nach, in keinem Verhältnis.
johnkree
User
Beiträge: 4
Registriert: Sonntag 5. Februar 2023, 00:26

Das Foto mit der Wolke ist aus einem Programm für Lehrer. Sie macht genau das. Ich dachte es wäre mit Python vielleicht möglich
Es muss nicht innerhalb einer Wolke sein, es wäre auch möglich wenn das Wort über drei Zeilen verteilt wird. Die Vektorgrafik der Wolke kann ich schon darüber anpassen.
Aber danke für deine Antwort. Dann Probier ich nicht sinnlos rum.
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

johnkree hat geschrieben: Sonntag 5. Februar 2023, 08:50 Das Foto mit der Wolke ist aus einem Programm für Lehrer. Sie macht genau das. Ich dachte es wäre mit Python vielleicht möglich
Möglich ist das schon, aber man muss sich einen etwas komplizierteren Algorithmus überlegen (z.B. Monospace Font nehmen, Buchstaben nacheinander auf Kreisbahn mit doppeltem Radius um den Vorgänger setzen und bei Überschneidungen mit einem Vorgänger neu würfeln/backtracken oder so etwas). Auf der Beispielgrafik ist ja nur ein Wort zu sehen, bei dir werden jedoch die Buchstabe aller Wörter in dieselbe Cloud geschrieben. Dadurch kann es natürlich auch mehr Überlappungen geben. Soll es denn ein Wort pro Wolke sein/bzw. eine Wolke pro Wort – Oder wie ist das gedacht?
johnkree
User
Beiträge: 4
Registriert: Sonntag 5. Februar 2023, 00:26

nezzcarth hat geschrieben: Sonntag 5. Februar 2023, 09:13
johnkree hat geschrieben: Sonntag 5. Februar 2023, 08:50 Das Foto mit der Wolke ist aus einem Programm für Lehrer. Sie macht genau das. Ich dachte es wäre mit Python vielleicht möglich
Möglich ist das schon, aber man muss sich einen etwas komplizierteren Algorithmus überlegen (z.B. Monospace Font nehmen, Buchstaben nacheinander auf Kreisbahn mit doppeltem Radius um den Vorgänger setzen und bei Überschneidungen mit einem Vorgänger neu würfeln/backtracken oder so etwas). Auf der Beispielgrafik ist ja nur ein Wort zu sehen, bei dir werden jedoch die Buchstabe aller Wörter in dieselbe Cloud geschrieben. Dadurch kann es natürlich auch mehr Überlappungen geben. Soll es denn ein Wort pro Wolke sein/bzw. eine Wolke pro Wort – Oder wie ist das gedacht?
Es ist eine A4 Seite mit 15 kleinen Wolken, in denen jeweils 1 Wort versteckt wird. Also ein Wort pro Wolke.
Mir würde es schon genügen, wenn ich es so hinbekomme:

u
M t t
r e

Es muss keine Kreisform oder zufällige rasterlose Verteilung sein.
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Hier mal eine naive Implementierung des Ansatzes, den ich skizziert hatte (alle Wörter landen in derselben "Wolke"). Kann man natürlich auch ganz anders machen. Aber vielleicht gibt es eine Inspiration, wie man so etwas angehen kann. Strukturell ist es m.M.n. wichtig, das SVG-Schreiben und das Erzeugen der Platzierung von einander zu trennen, auch wenn man einen ganzn anderen Ansatz wählt.

Code: Alles auswählen

#!/usr/bin/env python3
import random
import math
import svgwrite

FONT_FAMILY = "DejaVu Sans Mono"
FONT_SIZE = 16
RETRIES = 1_000


def scramble_word_letters(word):
    letters = list(word)
    random.shuffle(letters)
    return "".join(letters)


def scramble_words_within_sentence(sentence):
    words = sentence.strip().split()
    scrambled_words = [scramble_word_letters(word) for word in words]
    return scrambled_words


def overlap(first, second, radius=FONT_SIZE):
    distance = math.dist(first, second)
    return distance <= radius


def arrange_words(words, size):
    x = size[0] // 2
    y = size[1] // 2
    result = []
    for word in words:
        for letter in word:
            for _ in range(RETRIES):
                radius = random.randrange(FONT_SIZE * 2, FONT_SIZE * 3)
                angle = 2 * math.pi * random.random()
                new_x = round(radius * math.cos(angle) + x)
                new_y = round(radius * math.sin(angle) + y)
                if (
                    FONT_SIZE // 2 < new_x < size[0] - (FONT_SIZE // 2)
                    and (FONT_SIZE // 2) < new_y < size[1] - FONT_SIZE // 2
                ):
                    if not any(
                        overlap((new_x, new_y), (old_x, old_y))
                        for _, old_x, old_y in result
                    ):
                        break
            else:
                raise ValueError("Can't place letter.")
            x = new_x
            y = new_y
            result.append((letter, x, y))
    return result


def scramble_words_to_svg(file_in):
    with open(file_in, "r", encoding="utf-8") as f_in:
        words = [word for line in f_in for word in scramble_words_within_sentence(line)]
    characters = sum(len(word) for word in words)
    extent = FONT_SIZE * characters
    file_out = file_in.replace(".txt", "") + "_output.svg"
    dwg = svgwrite.Drawing(
        file_out, profile="tiny", size=(f"{extent}px", f"{extent}px")
    )
    positions = arrange_words(words, (extent, extent))
    for letter, x, y in positions:
        dwg.add(
            dwg.text(
                letter,
                insert=(
                    x,
                    y,
                ),
                font_size=FONT_SIZE,
                font_family=FONT_FAMILY,
                text_anchor="middle",
            )
        )
    dwg.save()


def main():
    file_name = "words.txt"  # = input("Enter the name of the input file: ")
    scramble_words_to_svg(file_name)


if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@johnkree: Ein paar Anmerkungen zum ursprünglichen Quelltext: `file_in` und `file_out` sind keine guten Namen für Datei*namen* beziehungsweise -pfade. Es ist nicht wirklich ersichtlich warum `file_in` ein Dateiname `f_in` dagegen ein Dateiobjekt sein sollte. Das könnte genau so gut umgekehrt sein. Bei `file` sollte man wirklich ein Dateiobjekt meinen und `f_in` ist an sich gar kein guter Name, weil der eine kryptische Abkürzung enthält. Genau wie `dwg` als Rapper wenn man `kwl` sein will `dog` zu ”normal” ist, sagen kann, aber wenn man `drawing` meint, sollte man das auch schreiben, damit man nicht erst die Definition suchen muss, um zu verstehen für was das stehen soll.

Pfade sollte man nicht mit Zeichenkettenoperationen bearbeiten. Dafür gibt es das `pathlib`-Modul. Das ersetzen von ".txt" in der *gesamten* Eingabe kann auch zu Fehlern führen falls das tatsächlich mal an mehr als einer Stelle im Dateiname oder -pfad vorkommen sollte, und nicht nur die Endung ist.

Beim öffnen von Textdateien sollte man immer explizit die Kodierung angeben.

`scrambled_sentences` ist als Name falsch für *einen* Satz.

Man muss nicht jedes kleine Zwischenergebnis an einen Namen binden. `scramble_words_within_sentence()` ist eigentlich ein Einzeiler. Und das `strip()` dort ist überflüssig, weil das vom `split()` mit erledigt wird.

Bei den Berechnungen könnte man das ganze besser Formulieren, so dass man sehen kann wovon die Position(en) tatsächlich abhängen. Zum Beispiel das jedes Wort um einen Punkt herum mit ein bisschen ”jitter” gesetzt wird, und das ab dem 9. Wort wieder mit dem Punkt links oben begonnen wird, also nicht nur die Buchstaben von einem Wort (zu dicht) um einen Punkt gesetzt werden, sondern das auch Worte übereinander gesetzt werden, wenn es mehr als 9 insgesamt in allen Sätzen in der Datei sind.

Ungetesteter Zwischenstand nach der Überarbeitung (sollte immer noch das gleiche machen):

Code: Alles auswählen

#!/usr/bin/env python3
import random
from functools import partial
from itertools import chain
from pathlib import Path

import svgwrite


def scramble_word_letters(word):
    letters = list(word)
    random.shuffle(letters)
    return "".join(letters)


def scramble_words_within_sentence(sentence):
    return map(scramble_word_letters, sentence.split())


def scramble_words_to_svg(sentences_file_path):
    delta = 20
    step_count = 9
    jitter_amount = 0.8
    size = delta * (step_count + 1)
    jitter = partial(random.uniform, -jitter_amount, jitter_amount)

    with sentences_file_path.open("r", encoding="ascii") as lines:
        drawing = svgwrite.Drawing(
            sentences_file_path.with_name(
                sentences_file_path.stem + "_output.svg"
            ),
            profile="tiny",
            size=(size, size),
        )
        words = chain.from_iterable(map(scramble_words_within_sentence, lines))
        for i, word in enumerate(words):
            offset = delta * (i % step_count + 1)
            for letter in word:
                drawing.add(
                    drawing.text(
                        letter,
                        insert=(offset + jitter(), offset + jitter()),
                        font_size=12,
                    )
                )

        drawing.save()


def main():
    scramble_words_to_svg(Path(input("Enter the name of the input file: ")))


if __name__ == "__main__":
    main()
Die Aufteilung des Code auf Funktionen ist nicht so gut gelöst. Die `scramble_words_to_svg()` macht letztlich alles, und man kann nur sehr wenig separat testen, oder austauschen. Zum Beispiel wenn die Sätze gar nicht aus einer Datei kommen sollen, oder wenn man das Setzen in eine SVG-Grafik mal für einzelne Worte testen möchte, muss immer *alles* ablaufen, oder es ist gar nicht so einfach möglich.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
johnkree
User
Beiträge: 4
Registriert: Sonntag 5. Februar 2023, 00:26

nezzcarth hat geschrieben: Sonntag 5. Februar 2023, 15:43 Hier mal eine naive Implementierung des Ansatzes, den ich skizziert hatte (alle Wörter landen in derselben "Wolke"). Kann man natürlich auch ganz anders machen. Aber vielleicht gibt es eine Inspiration, wie man so etwas angehen kann. Strukturell ist es m.M.n. wichtig, das SVG-Schreiben und das Erzeugen der Platzierung von einander zu trennen, auch wenn man einen ganzn anderen Ansatz wählt.
Danke! Das Ergebnis ist schon wolkenähnlich, wobei ich immer noch das Problem habe, dass die Wörter nicht in eigene Buchstabenhaufen getrennt sind. Aber es ist ein Ansatz auf dem ich aufbauen kann! Danke Danke.

https://imgur.com/a/KlesTvj


__blackjack__ hat geschrieben: Sonntag 5. Februar 2023, 15:53 @johnkree: Ein paar Anmerkungen zum ursprünglichen Quelltext...
Danke für die vielen Tipps. Ich stehe gerade erst am Anfang und denke oft zu kompliziert. Deine Einwände machen auf jeden Fall Sinn. Werd das in die Richtung bearbeiten!
Antworten