Marc Adrian's "Computer Texts" are among the earliest works of digital concrete poetry. In this piece, "G-1966-5," words that are spelled identically in German and English (homoglyphs) are placed on a surface in random sizes, orientations, and letter spacing. The dual legibility of the words creates a semantic shimmer between the languages.
The original code was first reimplemented in Python based on Adrian's project sketches (PIL/Pillow). The display here is based on a DOM-based real-time version.
from PIL import Image, ImageDraw, ImageFont
import random
import math
scale_factor = 4
paper_width, paper_height = 29.7 * 10 * scale_factor, 36.3 * 10 * scale_factor
border_offset = 5 * scale_factor
image = Image.new('RGB', (int(paper_width), int(paper_height)), 'white')
draw = ImageDraw.Draw(image)
same_spelling_words = [
'bitter', 'firm', 'kind', 'band', 'herb', 'boot', 'brat',
'brave', 'box', 'after', 'chance', 'born', 'die', 'block',
'also', 'handy', 'brand', 'hell', 'rat', 'halt', 'post',
'angler', 'fall', 'blind', 'brief', 'hut', 'lager', 'see',
'taste', 'wink', 'rock'
]
chosen_words = random.choices(same_spelling_words * 2, k=10)
character_distances = [0, 20, 50]
font_sizes = [20, 50, 100]
angles = [0, 0, 0, 15, 30, 45, 60]
font_path = "/Users/Sim/Library/Fonts/LabradorA-Black.ttf"
for word in chosen_words:
char_distance = random.choice(character_distances)
font_size = random.choice(font_sizes)
angle_size = random.choice(angles)
font = ImageFont.truetype(font_path, font_size)
if random.choice([True, False]):
angle_size = -angle_size
bounding_width = (font_size + char_distance) * len(word)
bounding_height = font_size
max_y_offset = (font_size + char_distance) * (len(word) - 1) * math.sin(math.radians(angle_size))
x_position = random.randint(
border_offset,
int(paper_width) - int(bounding_width) - border_offset
)
y_position = random.randint(
border_offset + abs(int(max_y_offset)),
int(paper_height) - int(bounding_height) - abs(int(max_y_offset)) - border_offset
)
for i, char in enumerate(word):
x_offset = i * (font_size + char_distance) * math.cos(math.radians(angle_size))
y_offset = i * (font_size + char_distance) * math.sin(math.radians(angle_size))
draw.text(
(x_position + x_offset, y_position + y_offset),
char, font=font, fill="black"
)
image.save('concrete_poem.png', quality=95) // Konkrete Poesie nach Marc Adrian — Browser-Portierung
// Platziert deutsch-englische Homographen auf einer Fläche
const WORDS = [
'bitter', 'firm', 'kind', 'band', 'herb', 'boot', 'brat',
'brave', 'box', 'after', 'chance', 'born', 'die', 'block',
'also', 'handy', 'brand', 'hell', 'rat', 'halt', 'post',
'angler', 'fall', 'blind', 'brief', 'hut', 'lager', 'see',
'taste', 'wink', 'rock'
];
const CHAR_DISTANCES = [0, 0.3, 0.8]; // em units
const FONT_SIZES = [14, 28, 48]; // px
const ANGLES = [0, 0, 0, 15, 30, 45, 60];
const WORD_COUNT = 10;
const DELAY_MS = 400;
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function generate(container) {
container.innerHTML = '';
const rect = container.getBoundingClientRect();
const W = rect.width;
const H = rect.height;
const pad = 20;
const words = Array.from({ length: WORD_COUNT }, () => pick(WORDS));
words.forEach((word, wi) => {
const fontSize = pick(FONT_SIZES);
const charDist = pick(CHAR_DISTANCES);
let angle = pick(ANGLES);
if (Math.random() < 0.5) angle = -angle;
const rad = angle * Math.PI / 180;
const step = fontSize * (1 + charDist);
const bw = step * word.length;
const maxYOff = Math.abs(step * (word.length - 1) * Math.sin(rad));
const x = pad + Math.random() * Math.max(0, W - bw - 2 * pad);
const y = pad + maxYOff + Math.random() * Math.max(0, H - fontSize - 2 * maxYOff - 2 * pad);
for (let i = 0; i < word.length; i++) {
const span = document.createElement('span');
span.className = 'gen-char';
span.textContent = word[i];
span.style.fontSize = fontSize + 'px';
span.style.left = (x + i * step * Math.cos(rad)) + 'px';
span.style.top = (y + i * step * Math.sin(rad)) + 'px';
span.style.opacity = '0';
span.style.transitionDelay = (wi * DELAY_MS + i * 30) + 'ms';
container.appendChild(span);
}
});
requestAnimationFrame(() => {
requestAnimationFrame(() => {
container.querySelectorAll('.gen-char').forEach(el => {
el.style.opacity = '1';
});
});
});
}