Text-to-Art is an interactive generative art project that transforms written text into animated, abstract visual compositions.
The main goal of the project is to explore how language can act as a semantic seed for algorithmic design and how textual properties such as characters, length, rhythm, and variability can influence visual parameters like color, symmetry, motion, and composition.
Another goal of the project was for users to be able to interact in the project area(Projektmesse) on 02.02.2026 and obtain individualized outputs with the input they provide.
The project is implemented using HTML, CSS, and JavaScript, with p5.js as the core creative coding library. It runs entirely in the browser and requires no backend. Each artwork is generated dynamically based on user input and includes controlled randomness, ensuring that no two results are ever exactly the same, even when the same text is entered multiple times.
This file is the entry point of the project. It presents the landing page where users can enter a word, sentence, or number. The page itself does not generate the artwork; instead, it collects the user input and stores it in localStorage. A secondary option, “Surprise me”, selects a predefined random prompt. After submission, the user is redirected to the result view.
- The following function retrieves the user input, validates it, stores it in localStorage, and redirects the user to the result page:
function createArt(surprise = false) {
const input = document.getElementById("userText");
const val = surprise
? generatePrompt()
: (input.value || "").trim();
if (!val) {
input.focus();
return alert("Please enter something first.");
}
// Store user input and a variation seed
localStorage.setItem("userText", val);
localStorage.setItem("variationSeed", Math.random().toString());
// Redirect to the result page
window.location.href = "result.html";
}- To support "surprise me" button, a set of predefined prompts is used:
function generatePrompt() {
const ideas = [
"Embrace the flow",
"42",
"You have to be patient",
"Mandala art",
"This was a random prompt",
"A secret you never said aloud",
"University of Bamberg",
"Matrix",
"I love programming!",
"This is Inf-Einf-B!",
"Projektmesse",
];
return ideas[Math.floor(Math.random() * ideas.length)];
}The generative system is implemented using p5.js and is driven by the user’s text input. Instead of rendering a static image, the system transforms semantic input into a dynamic visual seed, ensuring that the same input can still produce different results.
These are the core variables controlling canvas, text input, color, randomness, and animation state:
// Global variables controlling canvas, text input, color palette, randomness, and animation state.
let txt; // User's text input
let cnv; // Main canvas where artwork is drawn
let t = 0; // Time counter (frame-based)
let maxT = 50; // Duration of the render (number of frames)
let variationSeed = 0; // Additional random seed for non-deterministic variation
let glowGraphics; // Secondary canvas for glow and blur effects
let bgColor = 6; // Background hue in HSB color space
let seedBase = 1; // Main numeric seed derived from user text
let runEntropy = Math.floor(Math.random() * 1_000_000_000); // One-time random number for extra variation
let palette = []; // Color palette for the generative artwork
let artworkId = ""; // Unique identifier for each generated artworkThe generative process is divided into two main steps:
1️⃣ Transforming text into a numeric seed:
- The user’s input text is converted into a numeric value by iterating over its characters. This value is later combined with a random variation seed to avoid deterministic output.
function textToSeed(text) {
let seed = 0;
for (let i = 0; i < text.length; i++) {
seed += text.charCodeAt(i) * (i + 1);
}
return seed;
}2️⃣ Using this seed to control motion, color, and symmetry:
- The following function represents the core visual logic. Using rotation, symmetry, and color variation, it creates a mandala-like generative form that evolves over time.
function drawPattern(seed, t) {
randomSeed(seed);
noiseSeed(seed);
translate(width / 2, height / 2);
const layers = 24;
for (let i = 0; i < layers; i++) {
rotate(TWO_PI / layers);
const radius = noise(i, t) * 200;
const hue = (seed + i * 10) % 360;
stroke(hue, 80, 90, 60);
noFill();
beginShape();
for (let a = 0; a < TWO_PI; a += 0.2) {
const r = radius + noise(a, i) * 50;
vertex(cos(a) * r, sin(a) * r);
}
endShape(CLOSE);
}
}User Input (Text)
↓
Text-to-Seed Conversion
↓
Random Variation Seed
↓
Generative Drawing Logic (p5.js)
↓
Unique Animated Artwork
This section handles how each piece of generative art is saved and tracked.
When the user clicks "Save", the artwork is first converted into a thumbnail for the gallery, then stored in localStorage as a JSON object.
Each artwork is assigned a unique ID using the generateArtworkId() function, combining a timestamp with a random number to ensure uniqueness.
If the browser cannot use localStorage (for example, on file:// URLs), a fallback via window.name ensures the gallery still works.
Finally, the full-resolution image is downloaded as a PNG, and the page redirects to the gallery to immediately display the new artwork.
function saveArt() {
const thumbData = makeThumbnailDataURL(cnv.elt, 640, 0.84);
let arts = [];
try {
arts = JSON.parse(localStorage.getItem("artGallery") || "[]");
if (!Array.isArray(arts)) arts = [];
} catch (e) {
arts = [];
}
const entry = {
id: artworkId,
thumb: thumbData,
text: txt,
createdAt: Date.now()
};
arts.push(entry);
const serialized = JSON.stringify(arts);
// Save to localStorage
try { localStorage.setItem("artGallery", serialized); }
catch (e) { console.warn("localStorage failed, using window.name fallback"); }
// file:// fallback
try { window.name = JSON.stringify({ artGallery: arts }); }
catch (e) {}
// Save full-resolution PNG
saveCanvas(cnv, `textart-${artworkId}`, "png");
// Redirect to gallery after a short delay
setTimeout(() => {
window.location.href = `gallery.html?new=${encodeURIComponent(artworkId)}`;
}, 250);
}
// Generate unique ID for each artwork
function generateArtworkId() {
return (
Math.floor(Date.now() / 1000).toString(36) +
"-" +
Math.floor(Math.random() * 1e6).toString(36)
).toUpperCase();
}This script creates a subtle, animated background for the main page using p5.js instance mode.
- The background consists of soft flowing lines and small drifting particles, designed to be low-contrast so it doesn't distract from the main content.
dotsarray stores individual particles with random position, size, speed, and opacity.p.setup()initializes the canvas to match the page size and sets color mode to HSB for vibrant effects.p.draw()animates the lines and particles over time using Perlin noise, creating a slow-moving, natural flow effect.p.windowResized()ensures the background resizes dynamically with the browser window.
- Full-screen canvas attached to the #bg element.
p.setup = () => {
w = mount.clientWidth || p.windowWidth;
h = mount.clientHeight || p.windowHeight;
const c = p.createCanvas(w, h);
c.parent(mount);
p.colorMode(p.HSB, 360, 100, 100, 100);
init();
};- Generates soft, wavy lines across the background.
- Uses Perlin noise for natural, smooth motion.
for (let i = 0; i < lines; i++) {
p.beginShape();
for (let s = 0; s <= steps; s++) {
const n = p.noise(x * 0.002, y0 * 0.003, t);
const y = y0 + (n - 0.5) * 90;
p.curveVertex(x, y);
}
p.endShape();
}- Dots move smoothly with noise-based velocities.
- Motion across edges.
- Each dot has random size, speed, and opacity for a subtle effect.
for (const d of dots) {
d.x += (n - 0.5) * d.s;
d.y += (p.noise(...) - 0.5) * d.s;
// wrap-around screen edges
if (d.x < -10) d.x = w + 10;
if (d.x > w + 10) d.x = -10;
...
p.circle(d.x, d.y, d.r);
}This page displays the generative artwork created from the user’s input text using p5.js.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text-to-Art • Result</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
<script src="sketch.js"></script>
</head>
<body>
<div class="overlay">
<div class="pill" id="pill-text">Input: <strong id="pill-input"></strong></div>
<div class="controls">
<button class="save-btn" onclick="saveArt()">Save to gallery</button>
<a class="save-btn" href="gallery.html">Open gallery</a>
<a class="save-btn" href="index.html">← Back / Create new</a>
</div>
</div>
<div id="loading" class="loading">
<span>Blooming your art, wait a little…</span>
</div>
</body>
</html>- Uses
sketch.jsto render the art on the canvas. - Visuals are unique per input thanks to deterministic and random seeds.
- Shows a subtle loading message:
Blooming your art, wait a little…🌱
- Save button 💾 → saves the generated artwork to the gallery.
- Gallery link 🖼 → view all saved artworks.
- Back button ← → return to home page to create new art.
- Shows the user’s original text and generated artwork ID in a “pill” at the top.
- Keeps a clear connection between input and output.
This page shows all previously saved artworks from the user. It retrieves them from localStorage (or window.name as a fallback).
- Each artwork is shown as a card with:
- Thumbnail image
- Unique ID
- Original text input used to generate it
- Clicking a card opens a larger view in a new tab.
const gallery = document.getElementById("gallery");
const empty = document.getElementById("empty");
gallery.innerHTML = "";
if (!arts.length) {
empty.style.display = "block";
return;
}
empty.style.display = "none";
arts
.slice()
.reverse()
.forEach((art) => {
if (!art.thumb) return;
const card = document.createElement("div");
card.className = "art-card";
card.innerHTML = `
<img src="${art.thumb}" alt="Generated artwork">
<div class="meta"><strong>ID:</strong> ${art.id}</div>
<div class="meta"><strong>Input:</strong> ${art.text}</div>
`;
card.onclick = () => {
const w = window.open("", "_blank");
w.document.write(`
<img src="${art.thumb}" style="width:100%">
<p><strong>ID:</strong> ${art.id}</p>
<p><strong>Input:</strong> ${art.text}</p>
`);
};
gallery.appendChild(card);
});
}- Create new ➕ → back to the home page to generate a new artwork.
- Clear gallery 🗑️ → removes all saved artworks with confirmation.
//clear gallery function
function clearGallery() {
const confirmed = confirm("Clear all saved artworks?");
if (!confirmed) return;
localStorage.removeItem("artGallery");
try {
window.name = "";
} catch (e) {}
loadGallery();
}- Reads artwork data from
localStoragefirst. - If empty, tries
window.nameas a file:// fallback. - Keeps the gallery in sync with local storage .
/* localStorage JSON */
try {
const raw = localStorage.getItem("artGallery");
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) arts = parsed;
}
} catch (e) {
arts = [];
}
/* file:// fallback */
if (!arts.length && window.name) {
try {
const backup = JSON.parse(window.name);
if (backup && Array.isArray(backup.artGallery)) {
arts = backup.artGallery;
}
} catch (e) {
arts = [];
}
}The Text-to-Art project uses a dark, minimal aesthetic to highlight generative visuals. Most of the design, spacing, and layout decisions were made using AI tools (cursor + Figma) for rapid prototyping.
:root {
--bg: #050507; /* Main dark background */
--panel: rgba(255,255,255,0.04); /* Light translucent panels */
--accent: #7bf1c8; /* Primary gradient accent */
--accent-2: #82a0ff; /* Secondary gradient accent */
--text: #f8f8fb; /* Main text color */
--muted: #aeb1c5; /* Secondary, muted text */
--border: rgba(255,255,255,0.12);
--radius: 16px; /* Rounded corners */
--shadow: 0 20px 60px rgba(0,0,0,0.45); /* Soft shadows for cards */
}input {
padding: 14px 16px;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--panel);
transition: border 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(123,241,200,0.25);
transform: translateY(-1px);
}
button.primary {
background: linear-gradient(120deg, var(--accent), var(--accent-2));
color: #041014;
box-shadow: 0 12px 35px rgba(130,160,255,0.25);
}.hero {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 32px;
}
.hero-text h1 {
font-size: clamp(36px, 5vw, 52px);
line-height: 1.1;
letter-spacing: -0.02em;
}- Uses CSS Grid for responsive layout
- clamp() ensures text scales nicely on mobile and desktop
.art-card {
background: var(--panel);
border-radius: var(--radius);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 10px;
}
.art-card img {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 12px;
border: 1px solid var(--border);
}- Cards are modular and highlight artwork
- Rounded corners + shadows make images pop
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; }
.controls { flex-wrap: wrap; justify-content: flex-start; }
}- Layout adapts for smaller screens
- Buttons & text scale naturally