te amo mi niña

This sketch creates a romantic countdown timer displaying days, hours, minutes, and seconds elapsed since a special date. It features a decorative background of randomly scattered sunflowers with a centered white box showing the elapsed time, with responsive text sizing that adapts smoothly to any screen size.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the intro text — The phrase 'te amo desde hace' at the top of the white box will change instantly to whatever you type.
  2. Make the background sunset colors — The blue sky will instantly become warm orange-pink for a romantic sunset theme.
  3. Increase flower density — Flowers will fill the background much more densely, creating a fuller garden look instead of scattered blooms.
  4. Make sunflowers bigger — The random flower size range increases dramatically, making some flowers huge and more dominant in the background.
  5. Change the start date to today — The timer resets and will show 0 days, 0 hours, 0 minutes, and 0 seconds (only applies on first load, since localStorage persists).
  6. Darken the white box — The white box background becomes more opaque and darker, making the text stand out with more contrast against sunflowers.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a visually charming countdown timer that displays how much time has passed since a special date. The sketch combines several core p5.js techniques: responsive canvas sizing that adjusts to any window, procedural background generation using createGraphics to draw decorative sunflowers once and reuse them efficiently, real-time date mathematics to calculate elapsed time, and dynamic text sizing that scales proportionally to the viewport. The result is a romantic, personalized display that works beautifully on phones, tablets, and desktops.

The code is organized into five main functions: setup() initializes the canvas and loads or creates a persistent start date using the browser's localStorage, draw() calculates elapsed time on every frame and renders the timer display, drawSunflowersBackground() generates the decorative background pattern, drawSunflower() draws individual flower graphics, and windowResized() ensures the display adapts when the window changes size. By studying this sketch you will learn how to persist data across browser sessions, generate procedural art with loops, calculate time differences in JavaScript, make responsive designs that scale gracefully, and use createGraphics as an offscreen canvas for performance.

⚙️ How It Works

  1. When the sketch first loads, setup() creates a full-window canvas and checks localStorage for a saved start date. If one exists, it loads it; otherwise, it calculates a start date from 128 days ago and saves it so the same countdown persists forever—even if the page is reloaded.
  2. Every frame, draw() retrieves the current time and calculates how many milliseconds have elapsed since the start date, then breaks this into days, hours, minutes, and seconds using integer division and modulo operations.
  3. The draw loop also calculates responsive text sizing using map() to scale smoothly between screen widths (320px to 1920px), ensuring the timer stays readable and proportional on any device.
  4. A white rounded rectangle box is drawn at the center of the canvas with height dynamically calculated based on text size, and the elapsed time is displayed as four columns of numbers with labels beneath them.
  5. The background consists of randomly placed sunflowers drawn once during setup onto a createGraphics object, avoiding the expensive work of redrawing 50+ flowers every frame—instead the predrawn background image is simply copied to the main canvas each frame.
  6. When the window is resized, windowResized() automatically adjusts the canvas size and regenerates the sunflower background to fill the new dimensions, keeping the display responsive.

🎓 Concepts You'll Learn

localStorage and data persistenceResponsive design with map()createGraphics offscreen renderingDate and time calculationsDynamic text sizingProcedural background generationwindowResized callbackTransform operations (translate, rotate)

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch loads. Use it to initialize your canvas, load data, and prepare graphics. The localStorage pattern here is powerful: it lets you save small pieces of data (strings) that survive even after closing the browser—perfect for persistent games, preferences, or counting events like this romantic timer.

🔬 This else block runs only once—the first time the sketch loads. What happens if you change 128 to 0? Or to 365? The timer will show a different starting point when you first refresh.

  } else {
    // Si no está almacenada (primera vez que se carga), la calculamos y la guardamos
    let now = new Date(); // Obtiene la fecha y hora actuales
    startDate = new Date(now.getTime() - (128 * 24 * 60 * 60 * 1000));
    localStorage.setItem('teAmoDesdeHaceStartDate', startDate.getTime().toString());
  }
function setup() {
  createCanvas(windowWidth, windowHeight); // Crea un canvas que ocupa toda la ventana

  // --- Lógica para la fecha de inicio persistente ---
  let storedStartDate = localStorage.getItem('teAmoDesdeHaceStartDate');

  if (storedStartDate) {
    // Si la fecha de inicio ya está almacenada, la recuperamos
    startDate = new Date(parseInt(storedStartDate));
  } else {
    // Si no está almacenada (primera vez que se carga), la calculamos y la guardamos
    let now = new Date(); // Obtiene la fecha y hora actuales
    startDate = new Date(now.getTime() - (128 * 24 * 60 * 60 * 1000));
    localStorage.setItem('teAmoDesdeHaceStartDate', startDate.getTime().toString());
  }
  // --- Fin de la lógica persistente ---

  textAlign(CENTER, CENTER); // Centra el texto horizontal y verticalmente
  fill(0); // Establece el color del texto a negro

  // Crea el objeto createGraphics para el fondo y dibuja los girasoles una vez
  backgroundGraphics = createGraphics(width, height);
  drawSunflowersBackground();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

function-call Canvas initialization createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire browser window, allowing the sketch to be responsive and full-screen

conditional localStorage persistence logic if (storedStartDate) {

Checks if a start date was previously saved; if yes, loads it; if no, creates and saves a new one so the countdown always references the same date

function-call Offscreen graphics setup backgroundGraphics = createGraphics(width, height);

Creates an invisible canvas object that will hold the predrawn sunflower background, improving performance by drawing flowers once instead of every frame

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas with dimensions matching the full browser window, enabling a full-screen responsive display
let storedStartDate = localStorage.getItem('teAmoDesdeHaceStartDate');
Retrieves a previously saved start date from the browser's localStorage; returns null if nothing was ever saved
if (storedStartDate) {
Checks if a start date exists in storage; if truthy (not null), enters the block to load the saved date
startDate = new Date(parseInt(storedStartDate));
Converts the stored string back into a number, then creates a new Date object from that timestamp, restoring the exact previous start date
let now = new Date();
Gets the current date and time from the system
startDate = new Date(now.getTime() - (128 * 24 * 60 * 60 * 1000));
Calculates a start date 128 days in the past by subtracting milliseconds (128 days × 24 hours × 60 minutes × 60 seconds × 1000 milliseconds) from the current time
localStorage.setItem('teAmoDesdeHaceStartDate', startDate.getTime().toString());
Saves the calculated start date as a string in localStorage so it persists across page reloads and browser sessions
textAlign(CENTER, CENTER);
Sets text alignment to center both horizontally and vertically, making it easier to position text precisely
backgroundGraphics = createGraphics(width, height);
Creates an offscreen canvas (graphics object) that has the same dimensions as the main canvas, used for drawing the background once
drawSunflowersBackground();
Calls the function that fills the backgroundGraphics object with a sky blue background and scattered sunflowers

draw()

draw() runs 60 times per second and is responsible for all animation and display updates. This sketch uses draw() to continuously recalculate the elapsed time and redraw the timer, making it update smoothly every frame. Notice how the background is drawn once (in setup) and simply copied here each frame using image()—this is a key performance pattern: precompute expensive graphics and reuse them.

🔬 This block converts milliseconds into increasingly larger time units by dividing by 1000, then 60, then 60 again, then 24. What happens if you change the first line to divide by 2000 instead of 1000? The timer will appear to run twice as slow. Why?

  let seconds = Math.floor(diff / 1000);
  let minutes = Math.floor(seconds / 60);
  let hours = Math.floor(minutes / 60);
  let days = Math.floor(hours / 24);

🔬 This array holds 4 segments (days, hours, minutes, seconds) and the loop displays all of them. What happens if you remove one object from the array—say, delete the seconds line? The display will show only 3 columns instead of 4.

  const segments = [
    { value: days, label: "dias" },
    { value: formattedHours, label: "horas" },
    { value: formattedMinutes, label: "minutos" },
    { value: formattedSeconds, label: "segundos" }
  ];

  for (let i = 0; i < segments.length; i++) {
function draw() {
  // Dibuja el fondo de girasoles
  image(backgroundGraphics, 0, 0);

  // Calcula la diferencia de tiempo
  let now = new Date(); // Obtiene la fecha y hora actuales en cada frame
  let diff = now.getTime() - startDate.getTime(); // Calcula la diferencia en milisegundos

  // Convierte la diferencia de milisegundos a días, horas, minutos y segundos
  let seconds = Math.floor(diff / 1000);
  let minutes = Math.floor(seconds / 60);
  let hours = Math.floor(minutes / 60);
  let days = Math.floor(hours / 24);

  // Calcula los segundos, minutos y horas restantes después de extraer días completos
  hours = hours % 24;
  minutes = minutes % 60;
  seconds = seconds % 60;

  // Formatea los números para que siempre tengan dos dígitos (ej. 05 en lugar de 5)
  let formattedHours = nf(hours, 2);
  let formattedMinutes = nf(minutes, 2);
  let formattedSeconds = nf(seconds, 2);

  // --- Calcula el tamaño del texto responsivamente ---
  let currentTextSize = map(width, 320, 1920, 18, 36);
  textSize(currentTextSize); // Establece el tamaño del texto antes de calcular las alturas

  // --- Calcula la altura del cuadro blanco dinámicamente ---
  let textLineHeight = textAscent() + textDescent(); // Altura de una sola línea de texto
  let boxPadding = currentTextSize * 0.6; // Relleno dentro del cuadro, basado en el tamaño del texto

  // Estimamos 3 líneas de contenido: Intro, Números, Etiquetas
  let dynamicBoxHeight = (3 * textLineHeight) + (2 * boxPadding);

  // Asegura que el cuadro tenga una altura mínima (ej. para 3 líneas + padding)
  dynamicBoxHeight = max(dynamicBoxHeight, currentTextSize * 3.5);

  // --- Dibuja el cuadro centrado con la altura dinámica ---
  let boxWidth = width * 0.8; // Ancho del cuadro (80% del ancho del canvas)
  let boxX = (width - boxWidth) / 2; // Posición X para centrar el cuadro
  let boxY = (height - dynamicBoxHeight) / 2; // Posición Y recalculada para centrar el cuadro

  fill(255, 250, 240, 220); // Color blanco casi opaco (para que se vea un poco el fondo)
  stroke(0); // Borde negro
  strokeWeight(2); // Grosor del borde
  rect(boxX, boxY, boxWidth, dynamicBoxHeight, 15); // Dibuja el cuadro con esquinas redondeadas y altura dinámica

  // --- Muestra el mensaje dentro del cuadro ---
  fill(0); // Color del texto a negro
  noStroke(); // Sin borde para el texto
  textAlign(CENTER, CENTER); // Centra el texto horizontal y verticalmente dentro de su posición

  // 1. Línea de introducción
  let introY = boxY + boxPadding + textLineHeight * 0.5; // Centrado en la primera "línea" del contenido
  text("te amo desde hace", width / 2, introY);

  // 2. Línea de números y 3. Línea de etiquetas
  let numbersY = boxY + boxPadding + textLineHeight * 1.5; // Centrado en la segunda "línea" del contenido
  let labelsY = boxY + boxPadding + textLineHeight * 2.5; // Centrado en la tercera "línea" del contenido

  // Dividir el ancho del cuadro en 4 columnas para los números y etiquetas
  let colWidth = boxWidth / 4;
  let startX = boxX + colWidth / 2; // Centro de la primera columna

  const segments = [
    { value: days, label: "dias" },
    { value: formattedHours, label: "horas" },
    { value: formattedMinutes, label: "minutos" },
    { value: formattedSeconds, label: "segundos" }
  ];

  for (let i = 0; i < segments.length; i++) {
    let currentX = startX + i * colWidth;
    text(segments[i].value, currentX, numbersY); // Dibuja el número
    text(segments[i].label, currentX, labelsY); // Dibuja la etiqueta debajo del número
  }
}
Line-by-line explanation (36 lines)

🔧 Subcomponents:

function-call Background image rendering image(backgroundGraphics, 0, 0);

Draws the precomputed sunflower background from the offscreen graphics object to the main canvas, very fast compared to redrawing flowers every frame

calculation Elapsed time conversion let diff = now.getTime() - startDate.getTime();

Calculates the difference in milliseconds between now and the stored start date, the foundation for all time unit conversions

calculation Remainder extraction hours = hours % 24;

Uses modulo (%) to extract only the hours portion after complete days, ensuring hours stay in the 0-23 range

function-call Responsive text sizing let currentTextSize = map(width, 320, 1920, 18, 36);

Maps the window width to a text size range, scaling text smoothly from 18pt on phones (320px) to 36pt on desktops (1920px)

calculation Dynamic box height calculation let dynamicBoxHeight = (3 * textLineHeight) + (2 * boxPadding);

Calculates the white box height based on actual text metrics and padding, ensuring the box always fits its content proportionally

function-call Centered box drawing rect(boxX, boxY, boxWidth, dynamicBoxHeight, 15);

Draws a white rounded rectangle positioned at the center of the canvas with height that adapts to text size

for-loop Time segment rendering for (let i = 0; i < segments.length; i++) {

Loops through four time units (days, hours, minutes, seconds), positioning each number and label in its own column

image(backgroundGraphics, 0, 0);
Copies the entire predrawn sunflower background from the offscreen graphics object onto the main canvas at position (0,0)—the top-left corner
let now = new Date();
Gets the current date and time from the system—this updates every frame, allowing the timer to tick
let diff = now.getTime() - startDate.getTime();
Subtracts the stored start date from the current time, giving the elapsed milliseconds since the special date
let seconds = Math.floor(diff / 1000);
Divides total milliseconds by 1000 to convert to seconds, using Math.floor to round down to whole seconds
let minutes = Math.floor(seconds / 60);
Divides total seconds by 60 to get total minutes, rounding down to whole minutes
let hours = Math.floor(minutes / 60);
Divides total minutes by 60 to get total hours, rounding down to whole hours
let days = Math.floor(hours / 24);
Divides total hours by 24 to get total days, rounding down—this is the big number shown first
hours = hours % 24;
Uses modulo (%) to keep only the remainder after dividing by 24, so if total hours is 50, this becomes 50 % 24 = 2 hours remaining
minutes = minutes % 60;
Keeps only the minute remainder after extracting complete hours, so minutes stay in the 0-59 range
seconds = seconds % 60;
Keeps only the second remainder, so seconds display as 0-59 instead of total elapsed seconds
let formattedHours = nf(hours, 2);
Uses nf() (number format) to pad the hours with a leading zero if needed, so 5 becomes '05'
let currentTextSize = map(width, 320, 1920, 18, 36);
Uses map() to scale text size proportionally: if width is 320px use 18pt, if 1920px use 36pt, and interpolate smoothly between
textSize(currentTextSize);
Sets the text rendering size to the calculated responsive value before measuring text height
let textLineHeight = textAscent() + textDescent();
Measures the actual height of a line of text by adding the distance above and below the baseline, ensuring proper spacing
let boxPadding = currentTextSize * 0.6;
Creates padding (interior spacing) around text inside the white box, proportional to text size so it scales responsively
let dynamicBoxHeight = (3 * textLineHeight) + (2 * boxPadding);
Calculates the white box height to fit 3 lines of text (intro, numbers, labels) plus top and bottom padding
dynamicBoxHeight = max(dynamicBoxHeight, currentTextSize * 3.5);
Ensures the box has a minimum height by taking whichever is larger: the calculated height or a minimum based on text size
let boxWidth = width * 0.8;
Sets the white box width to 80% of the canvas width, leaving 10% margin on each side
let boxX = (width - boxWidth) / 2;
Calculates the X position to center the box horizontally: subtract box width from canvas width and divide by 2
let boxY = (height - dynamicBoxHeight) / 2;
Calculates the Y position to center the box vertically on the canvas
fill(255, 250, 240, 220);
Sets the fill color to an off-white (almost white, slightly cream-colored) with alpha 220 for slight transparency
stroke(0);
Sets the border color to black
strokeWeight(2);
Makes the border 2 pixels thick
rect(boxX, boxY, boxWidth, dynamicBoxHeight, 15);
Draws the rounded rectangle at the calculated position with the dynamic height and 15-pixel corner radius
fill(0);
Changes the fill color to black for drawing text
noStroke();
Removes the stroke (border) for text so it doesn't have an outline
let introY = boxY + boxPadding + textLineHeight * 0.5;
Calculates the Y position for the first text line, starting from the box top plus padding, offset 0.5 lines down
text("te amo desde hace", width / 2, introY);
Draws the intro text centered horizontally at the canvas center (width / 2)
let numbersY = boxY + boxPadding + textLineHeight * 1.5;
Calculates the Y position for the second line (numbers), 1.5 line heights down from the box top
let labelsY = boxY + boxPadding + textLineHeight * 2.5;
Calculates the Y position for the third line (labels), 2.5 line heights down, so they appear below the numbers
let colWidth = boxWidth / 4;
Divides the box width into 4 equal columns, one for each time unit (days, hours, minutes, seconds)
let startX = boxX + colWidth / 2;
Calculates the X position of the first column's center, used as a reference point for all column positions
for (let i = 0; i < segments.length; i++) {
Loops 4 times (once for each time segment: days, hours, minutes, seconds)
let currentX = startX + i * colWidth;
Calculates the X position for the current segment's column by moving right by one column width for each iteration
text(segments[i].value, currentX, numbersY);
Draws the number (days, hours, etc.) centered in its column at the numbers row
text(segments[i].label, currentX, labelsY);
Draws the label (dias, horas, etc.) centered in its column below the number

drawSunflowersBackground()

This function creates the background pattern by drawing to the offscreen backgroundGraphics object (not the main canvas). The nested loops with random increments create a scattered garden effect—flowers are loosely arranged in a grid, but randomness ensures they never look mechanical or repetitive. This function is called only once in setup(), making it very efficient compared to redrawing 50+ complex sunflowers every frame.

🔬 These nested loops create a grid of flowers with random spacing. What happens if you change random(60, 120) to just 80 (a constant)? Flowers will align perfectly in vertical columns. What if you change random(70, 110) to random(30, 50)? Flowers will be packed much more densely.

  for (let i = 0; i < width + 100; i += random(60, 120)) { // Espaciado horizontal más pequeño
    for (let j = 0; j < height + 100; j += random(70, 110)) { // Espaciado vertical más pequeño

🔬 These lines add random noise to the grid positions so flowers don't align exactly. What happens if you change both to random(-80, 80)? Flowers will scatter much more wildly and may overlap significantly. What if you change both to random(-5, 5)? Flowers will stay almost perfectly on the grid.

      let x = i + random(-40, 40); // Pequeña variación en la posición X
      let y = j + random(-40, 40); // Pequeña variación en la posición Y
function drawSunflowersBackground() {
  backgroundGraphics.background(135, 206, 235); // Fondo azul cielo

  backgroundGraphics.noStroke(); // Los girasoles no tendrán borde

  // Dibuja múltiples girasoles con mayor densidad
  for (let i = 0; i < width + 100; i += random(60, 120)) { // Espaciado horizontal más pequeño
    for (let j = 0; j < height + 100; j += random(70, 110)) { // Espaciado vertical más pequeño
      let size = random(40, 90); // Tamaño aleatorio para cada girasol
      let x = i + random(-40, 40); // Pequeña variación en la posición X
      let y = j + random(-40, 40); // Pequeña variación en la posición Y
      drawSunflower(backgroundGraphics, x, y, size);
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-call Sky background color backgroundGraphics.background(135, 206, 235);

Fills the offscreen graphics object with sky blue, creating the background color behind all sunflowers

for-loop Horizontal grid loop for (let i = 0; i < width + 100; i += random(60, 120)) {

Iterates horizontally across the canvas with random spacing (60-120px apart), creating varied flower columns

for-loop Vertical grid loop for (let j = 0; j < height + 100; j += random(70, 110)) {

Iterates vertically within each column with random spacing (70-110px apart), creating a scattered garden effect

calculation Random flower size let size = random(40, 90);

Gives each sunflower a random diameter between 40 and 90 pixels for visual variety

calculation Position noise let x = i + random(-40, 40);

Adds random offset to grid position so flowers don't align perfectly in straight lines, looking more natural

backgroundGraphics.background(135, 206, 235);
Fills the offscreen graphics object with sky blue (RGB: 135, 206, 235), clearing any previous content and setting the background
backgroundGraphics.noStroke();
Removes borders from all shapes drawn afterward on the backgroundGraphics object, so sunflowers won't have outlines
for (let i = 0; i < width + 100; i += random(60, 120)) {
Outer loop that moves horizontally across the canvas and beyond, incrementing by a random value between 60-120 pixels each iteration, creating irregular column spacing
for (let j = 0; j < height + 100; j += random(70, 110)) {
Inner loop nested inside the outer loop, moving vertically with random spacing (70-110 pixels), so each column has varying row spacing too
let size = random(40, 90);
Calculates a random diameter for this sunflower between 40 and 90 pixels, making flowers different sizes for natural variety
let x = i + random(-40, 40);
Offsets the grid column position (i) by a random value from -40 to +40 pixels, jittering the X position so flowers don't align perfectly
let y = j + random(-40, 40);
Offsets the grid row position (j) by a random value from -40 to +40 pixels, jittering the Y position for natural scatter
drawSunflower(backgroundGraphics, x, y, size);
Calls the drawSunflower function to render a single flower at position (x, y) with the calculated random size onto the backgroundGraphics object

drawSunflower()

drawSunflower() is a helper function that draws a single flower at any position and size. It demonstrates the power of transformations: by using translate() to move the origin and rotate() to spin the coordinate system, we can draw complex shapes with simple relative coordinates. The push()/pop() pattern isolates these transformations so they don't affect other drawings. Notice how it's called from inside the nested loops in drawSunflowersBackground()—each flower is a self-contained unit built by this function.

🔬 The petal loop rotates the coordinate system by TWO_PI / 20 = 18 degrees each iteration and always draws a petal pointing right. What happens if you change numPetals to 8? You'll see only 8 petals, spaced 45 degrees apart. Try also changing the ellipse size from size * 0.3 to size * 0.5 to make longer petals.

  // Pétalos
  let numPetals = 20; // Número de pétalos
  for (let i = 0; i < numPetals; i++) {
    pg.rotate(TWO_PI / numPetals); // Gira para cada pétalo

    // Dibuja múltiples capas para cada pétalo
    pg.fill(255, 200, 0, 200); // Amarillo brillante con un poco de transparencia
    pg.ellipse(size * 0.3, 0, size * 0.3, size * 0.12);
function drawSunflower(pg, x, y, size) {
  pg.push(); // Guarda el estado actual de los estilos y transformaciones
  pg.translate(x, y); // Mueve el origen al centro del girasol

  // Tallo
  pg.fill(0, 150, 0); // Verde oscuro
  pg.rect(-size * 0.05, size * 0.2, size * 0.1, size * 0.6); // Rectángulo para el tallo

  // Hojas (ejemplo simple, puedes mejorar si quieres)
  pg.fill(0, 120, 0); // Verde ligeramente más oscuro para las hojas
  pg.ellipse(-size * 0.2, size * 0.4, size * 0.3, size * 0.15);
  pg.ellipse(size * 0.2, size * 0.5, size * 0.3, size * 0.15);

  // Centro oscuro del girasol (base)
  pg.fill(80, 50, 0); // Marrón oscuro
  pg.circle(0, 0, size * 0.4); // Círculo central base

  // Textura del centro (más círculos pequeños)
  pg.fill(120, 70, 0); // Marrón medio
  pg.circle(0, 0, size * 0.3);
  pg.fill(160, 90, 0); // Marrón claro/naranja
  pg.circle(0, 0, size * 0.2);

  // Pétalos
  let numPetals = 20; // Número de pétalos
  for (let i = 0; i < numPetals; i++) {
    pg.rotate(TWO_PI / numPetals); // Gira para cada pétalo

    // Dibuja múltiples capas para cada pétalo
    pg.fill(255, 200, 0, 200); // Amarillo brillante con un poco de transparencia
    pg.ellipse(size * 0.3, 0, size * 0.3, size * 0.12);

    pg.fill(255, 180, 0, 200); // Amarillo-naranja
    pg.ellipse(size * 0.28, 0, size * 0.28, size * 0.1);

    pg.fill(255, 220, 0, 200); // Amarillo más claro
    pg.ellipse(size * 0.32, 0, size * 0.32, size * 0.14);
  }

  pg.pop(); // Restaura el estado original
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

function-call State management pg.push(); ... pg.pop();

Saves the drawing state before transformations and restores it after, preventing transformations from affecting other drawings

function-call Translation to flower center pg.translate(x, y);

Moves the origin (0,0) to the flower's position so all subsequent drawing is relative to that point, simplifying coordinate calculations

function-call Stem shape pg.rect(-size * 0.05, size * 0.2, size * 0.1, size * 0.6);

Draws a thin green rectangle below the flower center to represent the stem

function-call Leaf shapes pg.ellipse(-size * 0.2, size * 0.4, size * 0.3, size * 0.15);

Draws two simple leaf ellipses on either side of the stem

function-call Flower center base pg.circle(0, 0, size * 0.4);

Draws a dark brown circle at the origin to form the flower's center hub

function-call Center texture layers pg.circle(0, 0, size * 0.3);

Layers progressively smaller circles on top to create depth and texture in the flower center

for-loop Petal rotation loop for (let i = 0; i < numPetals; i++) {

Loops 20 times, drawing 3 layered ellipse petals and rotating between each to create a full flower

function-call Rotation for petal placement pg.rotate(TWO_PI / numPetals);

Rotates the drawing context by 1/20th of a full circle (TWO_PI) so each petal is evenly spaced around the center

function drawSunflower(pg, x, y, size) {
Function definition taking 4 parameters: pg is the graphics object to draw on, x and y are the center position, and size controls the flower's diameter
pg.push();
Saves the current drawing state (colors, transformations, styles) onto a stack so we can restore it later without affecting other drawings
pg.translate(x, y);
Moves the origin (0,0) to coordinates (x,y), so all subsequent shapes draw relative to the flower's center position
pg.fill(0, 150, 0);
Sets fill color to dark green for the stem
pg.rect(-size * 0.05, size * 0.2, size * 0.1, size * 0.6);
Draws a thin vertical rectangle at (-size*0.05, size*0.2) with width size*0.1 and height size*0.6, creating the stem below the flower center
pg.fill(0, 120, 0);
Changes fill to a slightly darker green for the leaves
pg.ellipse(-size * 0.2, size * 0.4, size * 0.3, size * 0.15);
Draws a left-side leaf as an ellipse positioned left of the stem
pg.ellipse(size * 0.2, size * 0.5, size * 0.3, size * 0.15);
Draws a right-side leaf positioned right of the stem
pg.fill(80, 50, 0);
Sets fill to dark brown for the flower center
pg.circle(0, 0, size * 0.4);
Draws a dark brown circle at the origin (flower center) with diameter size*0.4
pg.fill(120, 70, 0);
Changes fill to medium brown for the first texture layer
pg.circle(0, 0, size * 0.3);
Draws a medium-brown circle smaller than the base, layered on top for texture depth
pg.fill(160, 90, 0);
Changes fill to lighter brown/orange for the innermost texture layer
pg.circle(0, 0, size * 0.2);
Draws the smallest inner circle, completing the textured center
let numPetals = 20;
Sets the number of petals the sunflower will have to 20
for (let i = 0; i < numPetals; i++) {
Loop that runs 20 times, once for each petal
pg.rotate(TWO_PI / numPetals);
Rotates the drawing context by 2π/20 = π/10 radians (18 degrees) so each petal is evenly distributed around a circle
pg.fill(255, 200, 0, 200);
Sets fill to bright yellow with alpha 200 (slightly transparent) for the first petal layer
pg.ellipse(size * 0.3, 0, size * 0.3, size * 0.12);
Draws a petal ellipse positioned at distance size*0.3 from center (always pointing right because rotate() turns the whole coordinate system)
pg.fill(255, 180, 0, 200);
Changes fill to yellow-orange for the second petal layer
pg.ellipse(size * 0.28, 0, size * 0.28, size * 0.1);
Draws a slightly smaller, slightly narrower yellow-orange ellipse layered on top of the first petal
pg.fill(255, 220, 0, 200);
Changes fill to lighter yellow for the third petal layer
pg.ellipse(size * 0.32, 0, size * 0.32, size * 0.14);
Draws a lighter-yellow ellipse slightly larger than the previous layers, creating a layered, textured petal effect
pg.pop();
Restores the saved drawing state, undoing all transformations and style changes so they don't affect future drawings

windowResized()

windowResized() is a special p5.js callback that runs automatically whenever the window is resized (e.g., when dragging the edge of the browser, rotating a phone, or using fullscreen). Without this function, the canvas would stay at its initial size and get cut off or letterboxed when the window changes. This sketch regenerates the background each time because the flower positions are random and tied to canvas dimensions—on resize, we want a fresh pattern that fills the new space.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // Ajusta el tamaño del canvas

  // Vuelve a crear y dibujar el fondo de girasoles para que se ajuste al nuevo tamaño
  backgroundGraphics = createGraphics(width, height);
  drawSunflowersBackground();
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Canvas dimension update resizeCanvas(windowWidth, windowHeight);

Adjusts the p5.js canvas to match the new window dimensions whenever the browser is resized

function-call Background regeneration drawSunflowersBackground();

Regenerates the sunflower background pattern to fill the new canvas size with new random flower positions

function windowResized() {
This is a p5.js special function that automatically runs whenever the browser window is resized—you don't call it manually
resizeCanvas(windowWidth, windowHeight);
Adjusts the canvas dimensions to match the new window size, preventing letterboxing or content overflow
backgroundGraphics = createGraphics(width, height);
Creates a new offscreen graphics object matching the new canvas size (necessary because the old one still had the old dimensions)
drawSunflowersBackground();
Regenerates the sunflower background pattern for the new canvas size, ensuring flowers fill the entire new space

📦 Key Variables

startDate Date object

Stores the date and time from which the countdown calculates elapsed time, persisted in localStorage so it doesn't change between sessions

let startDate; // Initialized in setup() from either localStorage or a calculated default 128 days ago
backgroundGraphics p5.Graphics object (offscreen canvas)

Holds the predrawn sunflower background image, created once in setup() and reused every frame to avoid expensive redrawing

let backgroundGraphics; // Initialized in setup() and regenerated in windowResized()

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG setup() - localStorage

If localStorage is disabled in the browser or the user clears browser data, the sketch will recalculate a new start date every reload, causing the timer to reset unexpectedly

💡 Add a try-catch block around localStorage calls: try { localStorage.setItem(...) } catch(e) { console.warn('localStorage unavailable') } and consider a fallback like a session variable

PERFORMANCE drawSunflowersBackground() - nested loops

On very large screens (4K+), the nested loops may create hundreds of sunflowers, some of which might extend off-screen, wasting computation

💡 Optimize the loop bounds or reduce the density range on large screens: if (width > 2000) reduce the flower count proportionally

STYLE draw() - time formatting

Days are displayed without zero-padding (e.g., '5' instead of '05'), making the number column alignment inconsistent

💡 Format days like the other units: let formattedDays = nf(days, 3) or nf(days, 2) to ensure consistent width and alignment in the display

FEATURE draw() - box layout

On very narrow screens (phones), four columns in one row may make numbers too small or crowded; the layout doesn't stack responsively

💡 Add a responsive check: if (width < 500) display in a 2×2 grid instead of 1×4, or use flexbox-like positioning to wrap on small screens

BUG draw() - responsive text sizing

The map() from 320-1920 assumes all width values fit this range; very small screens (< 320px) and very large screens (> 1920px) will clamp at the extremes

💡 Use constrain() after map() to ensure smooth scaling beyond the assumed range: let currentTextSize = constrain(map(width, 320, 1920, 18, 36), 12, 48);

🔄 Code Flow

Code flow showing setup, draw, drawsunflowersbackground, drawsunflower, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Creation] setup --> localstorage-check[LocalStorage Check] setup --> background-graphics-creation[Background Graphics Creation] setup --> drawsunflowersbackground[drawSunflowersBackground] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click localstorage-check href "#sub-localstorage-check" click background-graphics-creation href "#sub-background-graphics-creation" click drawsunflowersbackground href "#fn-drawsunflowersbackground" draw --> background-draw[Background Draw] draw --> time-calculation[Time Calculation] draw --> modulo-operation[Modulo Operation] draw --> responsive-sizing[Responsive Sizing] draw --> dynamic-box-height[Dynamic Box Height] draw --> centered-box[Centered Box] draw --> segment-loop[Segment Loop] click draw href "#fn-draw" click background-draw href "#sub-background-draw" click time-calculation href "#sub-time-calculation" click modulo-operation href "#sub-modulo-operation" click responsive-sizing href "#sub-responsive-sizing" click dynamic-box-height href "#sub-dynamic-box-height" click centered-box href "#sub-centered-box" click segment-loop href "#sub-segment-loop" segment-loop --> outer-loop[Outer Loop] outer-loop --> inner-loop[Inner Loop] inner-loop --> size-randomization[Size Randomization] inner-loop --> position-jitter[Position Jitter] click segment-loop href "#sub-segment-loop" click outer-loop href "#sub-outer-loop" click inner-loop href "#sub-inner-loop" click size-randomization href "#sub-size-randomization" click position-jitter href "#sub-position-jitter" drawsunflowersbackground --> sky-background[Sky Background] drawsunflowersbackground --> outer-loop click sky-background href "#sub-sky-background" windowresized --> canvas-resize[Canvas Resize] windowresized --> background-refresh[Background Refresh] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click background-refresh href "#sub-background-refresh" drawsunflowersbackground --> drawsunflower[drawSunflower] drawsunflower --> push-pop[Push/Pop] push-pop --> translate[Translate] translate --> stem-drawing[Stem Drawing] translate --> leaves-drawing[Leaves Drawing] translate --> center-base[Center Base] translate --> center-texture[Center Texture] translate --> petal-loop[Petal Loop] petal-loop --> petal-rotation[Petal Rotation] click drawsunflower href "#fn-drawsunflower" click push-pop href "#sub-push-pop" click translate href "#sub-translate" click stem-drawing href "#sub-stem-drawing" click leaves-drawing href "#sub-leaves-drawing" click center-base href "#sub-center-base" click center-texture href "#sub-center-texture" click petal-loop href "#sub-petal-loop" click petal-rotation href "#sub-petal-rotation"

❓ Frequently Asked Questions

What visual elements are featured in the 'te amo mi niña' sketch?

The sketch creates a vibrant background filled with sunflowers, accompanied by a countdown display that shows the time since a specific start date.

Is there any user interaction available in the 'te amo mi niña' sketch?

The sketch does not include interactive elements; it primarily focuses on displaying the animated countdown and sunflower background.

What creative coding techniques does the 'te amo mi niña' sketch demonstrate?

This sketch showcases the use of persistent data storage with localStorage to track time and utilizes createGraphics for layered visuals.

Preview

te amo mi niña - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of te amo mi niña - Code flow showing setup, draw, drawsunflowersbackground, drawsunflower, windowresized
Code Flow Diagram