Media 103: Utilizar gráficos bitmap e vetoriais

Nesta secção/aula continuamos a mergulhar no código e nos conceitos. Nomeadamente nas primeiras variáveis complexas através de objetos “core” do processing (PImage, PShape e PFont) e de vetores (arrays) unidimensionais. Apesar de abrandar um pouco o ritmo em relação ao tópico anterior, utilizamos conceitos essenciais —e, pelo resultado da primeira turma de hoje, bastante difíceis de compreender?— para a progressão na aprendizagem:

  • importação de media
    • imagens bitmap (.jpg, .png, .tif…);
    • imagens vetoriais (.svg);
    • fontes (.ttf, otf, .vlw)
  • gravação / exportação do sketch
    • em modo bitmap (.jpg, .png, .tif…);
    • em modo vetorial (.pdf, .svg*);
  • vetores (arrays)
    • de números inteiros;
    • e de cores**;

[See processing.org tutorials]

Para um designer gráfico, a utilização de imagens deveria ser natural em Processing. Isto é, nós estamos habituados a usar imagens (p. ex.) no InDesign ou no Illustrator da mesma forma: “importamos” o ficheiro para utilizar no documento. Mas na realidade a imagem não está lá. Esta é “carregada” na memória sempre que abrimos o ficheiro. O que existe no documento é um link para o ficheiro original no disco.

O Processing faz o mesmo. Para mostrar imagens no ecrã é necessário ter a imagem dentro da estrutura de pastas do sketch. Tipicamente numa estrutura organizada. Os ficheiros que “entram” no processing devem ser guardados numa pasta chamada “data”. E, normarmalmente, uso uma outra pasta chamada “export” para os ficheiros que saem do processing.

Estrutura de uma pasta de Sketch PDE (falta a pasta de export)

Uma vez colocadas as imagens e gráficos na pasta data, é só usar as funções necessárias. Mas atenção aos nomes, pastas e extensões dos ficheiros! Verifiquem sempre se estão a escrever as coisas corretas.

PImage

Este é o objeto principal. Para carregar e mostrar uma imagem no Processing precisamos de uma variável complexa especial de imagem. Tenho explicado as variáveis simples como “caixas” onde armazenamos valores (para os mover e utilizar como quando fazemos mudanças de casa). No caso das imagens (hoje) lembrei-me da metáfora de uma moldura. Para mostrar uma imagem pedimos ao Processing para nos dar um contentor especial de imagens—uma moldura—, que é a variável PImage. Na realidade esta variável é um tipo especial—chamado de objeto—, pois contém métodos e propriedades (que é como quem diz funções e variáveis privadas) próprias e que vamos ver mais tarde. Para já ficamos só com a ideia que têm “poderes especiais”, como (p. ex.) saber automaticamente o tamanho (dimensão w e h) da imagem.

Então para mostrar a imagem, pedimos ao Processing uma moldura para mostrar imagens (declarar a variável de objeto PImage), colocamos a imagem dentro da moldura (inicializamos a variável fazendo a leitura da imagem do disco para dentro da variável) e, quando queremos mostrar, apresentamos a moldura que contém a imagem no ecrã (através do comando image)

// declarar variáveis e objetos
PImage minhaImagem;

// inicializar
minhaImagem = loadImage("data/imagens/han.jpg");

// desenhar
image(minhaImagem, 215, 100);

E é assim “tão simples”. Claro que podemos usar outros métodos incluindo construir uma imagem diretamente na memória a partir do createImage(), mas isto fica para mais tarde.

Coisas porreiras a saber para já:

Loading & Displaying

Mais à frente, para casos específicos pode ser útil saber:

Durante esta aula, vamos usar um par de conceitos de manipulação dos valores de cores armazenados nas imagens. Por isso, convém saber algumas destas funções e propriedades do objeto PImage

Pixels

No início, quando comecei a dar esta disciplina, obrigava a percorrer o array de pixels de imagem um-a-um, de forma “clássica”. Ou estou a ficar mole, ou… não sei. Desde que muitos começaram a estudar a a descobrir que o podiam fazer de forma mais fácil, que acho que é melhor “sacrificar o que é correto” por métodos mais imediatos e fáceis para se conseguir os resultados. Assim, nos últimos anos tenho explicado diretamente a função get() em detrimento de ler o valor individual de cor de cada entrada dos pixels[] da imagem.

É mais rápido ensinar e compreender (embora seja mais lento a correr no sketch) e permite uma ligação direta aos nested loops. O problema é que fico com a explicação das Arrays meia abstrata… sem grande aplicação imediata (até ao modo dinâmico). Pensando bem, não é um trade-off mau, tendo em vista que os estudantes apanham este método muito mais rapido.

De qualuqer forma, para aqueles que quiserem ir mais longe e “brincar” com as imagens tipo “glitch art” podem sempre usar mais métodos:

Durante o atendimento (em webinar) que fizémos na semana seguinte, reparei que duas ou três estudantes estavam a fazer a reamostragem (redimensionamento) da imagem. Mais concretamente um downsampling para poder fazer loops mais curtos e eficientes com o resize().

Foi bem utilizado, embora sem saber muito bem como nem porquê (tal como o transform/scale). Viram num tutorial online, mas não souberam dizer onde. Pedi-lhes para incluirem em comentário… vou ter que descobrir, mas para o ano, já vai fazer parte da matéria da aula.

E porque, basicamente, uma imagem é um vetor gigante de números de cores, também convém saber um pouco de como aceder e utilizar propriedades de cores.

Creating & Reading [Color]

À partida estes dois chegam para começar, porque normalmente uso imagens a preto e branco. Mas, este ano (2021) vamos produzir imagens a duas cores. Não sei se eles irão pensar em soluções a duas cores (sobreposições de fases ou separação de canais, fringing, ou estereoscopia?), mas pode ser interessante obter e tratar os canais em separado. Se assim for os restantes métodos são interessantes

Uma nota final para o lerp. É um conceito matemático que vamos usar quando passarmos a modo dinâmico / interativo. É uma das funções que mais gosto no Processing. Mas implica conhecer a normalização matemática e o mapeamento de valores com o map(). Sabendo isto, podemos sempre interpolar cores

Agora que já estão um pouco mais familiarizados com o conceito de mostrar imagens no ecrã, para mostrar imagens vetoriais, a lógica é a mesma. Declarar, Inicializar e Utilizar formas vetoriais.

O PShape usa o formato standard de gráficos vetoriais SVG (para a web). O único problema do formato SVG é que muitos dos atributos são ignorados, ou não são standards válidos/compatíveis com o parser do Processing. Mas isto é um problema que se ultrapassa com um pouco de prática e experiência—muitos alunos (todos?) nunca tinham utilizado um gráfico SVG o que complicou e atrasou um pouco a aula da manhã. A forma como recomendei o uso de SVG é imaginar que vão cortar um vinil para um wall text, ou para um logótipo para vidros. Formas em outline. Vazadas. Simples e rigorosas. Cores planas. Nem todos os SVGs se portam bem. Os logos que utilizo nos sites normalmente funcionam bem. O SVG da aliança rebelde também… 😉

PShape meuLogo;
meuLogo = loadShape("data/graphics/rebels.svg");
shape(meuLogo, 215, 250, 50, 50);

Shape

Loading & Displaying

O passo seguinte é utilizar tipografia. A lógica é a mesma. Na realidade quando utilizamos uma fonte num documento, estamos a pedir (p. ex.) ao InDesign ou ao Illustrator para, quando abrem ou manipulam o documento, carregarem uma fonte específica na memória para a poderem mostrar sempre que escrevemos.

No Processing é igual. Só que há uma pequena nuance. A forma de “carregar” fontes no sketch implica primeiro converter a fonte num formato específico — .vlw.

Mas o processing tem uma ferramenta que faz isto por nós: Menu Tools: Create Font…

Temos só que ter em atenção que devemos gerar um ficheiro adequado / otimizado ao tamanho do corpo que vão usar na aplicação. Uma vez criada a fonte e armazenada na pasta do sketch, é só utilizar. O método/abordagem é igual aos restantes tipos de media estáticos, só com um passo adicional. A fonte na realidade é um atributo configurável de um tipo de gráfico — o texto. Por isso, precisamos de declarar a variável de fonte, inicializar o ficheiro de fonte na variável e utilizar (dizendo que o atributo do próximo texto usa aquela fonte).

// declarar
PFont minhaFonte;

// inicializar
minhaFonte = loadFont("data/fonts/TradeGothicLTStd-BoldObl-40.vlw");

// configurar
textFont(minhaFonte, 40);
fill(0);

// desenhar
text("Rebels, why bother?!", 85, 450);

Ainda há uma outra forma de utilizar as fontes —criar e carregar a fonte de forma dinâmica em tempo real. Mas isto implica que a fonte (ttf, ou otf) exista na pasta do sketch, ou esteja instalada no computador. A grande vantagem deste método é que permite usar a fonte em formato vetorial

This function allows Processing to work with the font natively in the default renderer, so the letters are defined by vector geometry and are rendered quickly. In the P2D and P3D renderers, the function sets the project to render the font as a series of small textures.

https://processing.org/reference/createFont_.html

Para utilizar e manipular Tipografia, convém estar familizarizado com o seguinte

Typography

Loading & Displaying
Attributes
Metrics

Mas isto, para um designer, não deve ser nada de novo… 😉

Hoje durante a primeira aula, também vimos a diferença entre criar uma linha de texto e uma área de texto. Vejam a diferença entre o primeiro e o segundo

fill(0);
text("Rebels, why bother?!", 85, 450); // text line (from baseline)

noStroke(); fill(255, 0, 0); ellipse(85, 450, 10, 10); // baseline origin

// text box and descent line manual calculation ~20% below body
noFill(); stroke(150);
rect(85, 450-40+ 0.20*40, 350, 40- 0.20*40);
line(85, 450 + 0.20*40, 85+350, 450 + 0.20*40);

// text box --> notice the difference in the envelope reference/transformation point/origin
fill(0);
text("This ship is the Fastest in the gallaxy — it made the Kessel run in 12 parsecs!", 85, 550, 350, 240);

noStroke(); fill(255, 0, 0); ellipse(85, 550, 10, 10); // baseline origin

// text box and descent line manual calculation ~20% below body
noFill();
stroke(150);
rect(85, 550, 350, 40);
line(85, 550 + 40 + 0.20*40, 85+350, 550 + 40 + 0.20*40);

E pronto. Já conseguimos desenhar com gráficos, utilizar imagens, vetores e texto. Agora, precisamos de arranjar uma forma de guardar os resultados no disco para imprimir ou publicar online.

O método mais simples é gravar imagens no formato bitmap com o save / saveFrame. Quando o Processing desenha coisas no ecrã, na realidade ele cria uma imagem do tamanho do ecrã do sketch na memória, onde vai acrescentando gráficos e, quando está pronto ele atualiza o canvas do sketch com esta imagem. É como uma espécie de buffer de imagem. Como os videojogos fazem.

O método save é muito eficiente porque interceta esta imagem e, para além de mostrar no ecrã, o Processing grava-a no disco. Usando o formato que pretendemos. Simples.

Image

Para quando precisamos de fazer gráficos com maior detalhe para ampliar e reduzir sem perder qualidade, utilizamos documentos vetoriais. O processing usa SVG, mas… ei! —somos designers ou não?— Afinal de contas o nosso formato favorito não é o PDF? Pois bem…

A forma mais simples de exportar ou gravar documentos vetoriais dos nossos desenhos e aplicações é em formato PDF. Para isto, o Processing usa uma biblioteca. Um ficheiro ou conjunto de ficheiros adicionais que “ensina” o processing um novo conjunto de instruções. De linguagem. De métodos. Tal como fazem os livros numa biblioteca 😉

Está disponível através do menu Sketch: Import Library

O Processing já vem com um conjunto “core” de bibliotecas que estendem as suas funcionalidades. O PDF é uma delas (primeiro grupo da lista). Todas as outras (oficiais, ou feitas por contribuidores generosos) aparecem no segundo grupo. Para adicionar mais bibliotecas basta utilizar o comando Sketch: Import Library: Add Library e procurar a biblioteca ou funcionalidade desejada.

Depois de ter a biblioteca instalada (o Processing já traz a de PDFs), basta importar a biblioteca para o nosso sketch. Que é o mesmo que dizer que vamos “ensinar o nosso sketch a falar PDF”. Assim que pretendermos, criamos o ficheiro e começamos a gravar as formas que desenhos no ecrã ao mesmo tempo para o PDF. E, quando estivermos contentes, terminamos a gravação do ficheiro. Este último passo é muito importante, senão o ficheiro PDF não funciona

// importar Bibliotecas
import processing.pdf.*;

// começar a gravar PDF
beginRecord(PDF, "export/han-vector-document.pdf");

// desenhar
fill(255, 0, 0);
ellipse(200, 200, 10, 10);
// etc…

// if you create a PDF file, don't forget to "close" the file on the hard drive
endRecord();

Há outras formas de utilizar a gravação de PDF. Esta é a mais simples e direta. Vejam o reference desta biblioteca para as outras formas. PDF Export. Não muitas alternativas. É das bibliotecas mais simples.

Em segundo lugar, nesta aula utilizamos vetores. Mais concretamente, aprendemos apenas vetores (arrays) unidimensionais de números inteiros. Como dizia a Beatriz hoje, são “uma espécie de variáveis de variáveis”. Sim. É uma forma de entender. São uma variável que permite guardar uma lista, uma coleção de dados. Neste caso, de valores numéricos. De números. Tal como numa lista (ordenada) os valorese entram numa posição específica. Por exemplo um vetor / lista de 10 números pode ser descrito da seguinte forma: na primeira posição da lista está o valor 18; na segunda posição 37; na terceira 12; etc. até chegar à décima posição. Isto em pseudo-código.

Quando traduzimos isto para código em Processing precisamos de declarar o vetor, inicializar (construir a lista), e atribuir os valores às posições antes de poder utilizar cada um deles.

Na prática:

// declarar uma "lista" de valores
int[] myList; 

// inicializar (construir) a lista com 10 posições possíveis
myList = new int[10]; 

// fazer um loop para todas as posições na lista
for (int i  = 0; i < myList.length; i++) {
  
  // atribuir (guardar) um valor (aleatório) em cada posição da lista
  myList[i] = int( random(-100, 100) ); 
  
}

// utilizar o valor específico guardado na terceira posição da lista 
ellipse(myList[2], 100, 10, 10);  

// fazer um loop para utilizar todas as posições guardadas
for (int i  = 0; i < myList.length; i++) {
  
  println( myList[i] ); 
  
}

O desafio na aula para operacionalizar este conceito foi criar uma réplica do trabalho de Luciano Caggianello’s Reticolo/Grid.

Luciano Caggianello's Reticolo/Grid
Luciano Caggianello’s Reticolo/Grid

Este exempo permite explorar a forma de como conseguimos desenhar de um sítio para o outro, ou do “passado” para o “futuro”. Isto é, ao separar o momendo de definição (e armazenamento) das posições, do momento de desenho das formas nas posições, como as temos na memória, na prática, conseguimos saber toda e qualquer posição a qualquer altura. Assim, podemos traçar linhas, ou relações entre formas previamente desenhadas e as atuais. Mais concretamente ainda, permite-nos decidir a “ordem de pilha” do desenho.

// importar bibliotecas
import processing.pdf.*;

// declarar objetos

// declarar variaveis
int[] posY;
int[] posYright;
int n;

// inicializar
size(500, 500);
background(255);
strokeCap(ROUND);

// configurar
n = 5;
posY = new int[n];
posYright = new int[n];

// definir as posições das "bolas" de forma mais ou menos aleatória
for (int i = 0; i < n; i++) {
  posY[i] = i*100 + int ( random(-20, 20) ) +50;
  posYright[i] = i*100 + int ( random(-20, 20) ) +50;
}

// desenhar linhas 
strokeWeight(30);
stroke(200, 0, 0, 100);
line(50, posY[ int( random(n) ) ], 450, posYright[ int( random(5) ) ] );

// desenhar bolas
for (int i = 0; i < n; i++) {
  noStroke();
  fill(200);
  ellipse(50, posY[i], 50, 50);
  ellipse(450, posYright[i], 50, 50);
}

noFill();
stroke(0, 100, 0);
strokeWeight(4);

line(50, posY[2], 450, posYright[0]);

// pseudo-código + código da demonstração do "sorteio"
// desenha linha aleatoria da esquerda para direita

// escolher uma posição da esquerda
// x = 50
int myLeft = 50;

// sorteia (random) um numero vertical até ao limite possível (5)
int trl;
trl = int( random(n) );

// atribui esse numero à posição que quero
int myChoice = posY[trl];

// escolher um posição da direita

// desenho a linha da esquerda par direira
strokeWeight(2);
stroke(50);
line(myLeft, myChoice, 450, posYright[ int( random(n) ) ] );

Os vetores não são fáceis perceber. Isto é, são muito fáceis de perceber… mas como não são muito naturais para nós (humanos, designers,…) demoram um pouco a “encaixar” 😉 Especialmente o conceito de “índice de acesso”. O número que identifica a posição de cada valor na lista para o poder utilizar — aquele que aparece dentro de parêntesis retos e que se começa a contar a partir do 0.

Para usar Vetores (arrays) nada mais simples do que conhecer o básico de reference. Os arrays são os tijolos e a argamassa de aplicações mais complexas. É mesmo necessário saber utilizar

Composite

Structure

E os respetivos métodos e propriedades

Array Functions

Estes dois métodos são os mais fixes e úteis para os arrays. Em modo estático vai ser difícil de ver, mas na primeira aula do segundo módulo isto começa a ser interessante de usar! 😉

Logo de seguida funções mais avançadas e úteis (para grandes quantidades de dados ou manipulação de informação)

Output

E pronto. Introduzidos os conceitos de Media — importar, manipular e exportar formatos estáticos como imagens bitmap, vetoriais e texto — e introduzidos ao conceito de vetores, estamos prontos para um primeiro projeto gráfico. No final da aula começamos a desenvolver o código base do que vai ser o primeiro projeto: Criar um retrato generativo.

A forma mais simples de explicar isto é como se fosse uma imagem de ASCII art — ler todos os valores de cor do (array) da imagem e usar esse valor para decidir que formas, letras, ou outra coisa desenhar em vez de um pixel quadrado de cor.

Neste exemplo que começamos a programar na segunda turma, em 12 linhas conseguimos a funcionalidade básica de conversão de forma baseada no brilho da cor.

O truque agora é o “algoritmo” e as diferentes formas de “pixels” que cada um implementa. Para isto, não há nada melhor que conhecer a história e os autores de arte digital. E ajuda ter algumas referências visuais também dos anos anteriores [post em breve].

Deixo aqui um sketch simples. Um dos livros recomendados — o Generative Design — também tem este código de forma muito elegante.

// PAmado, LSI 2020-03-05
// P1: Generative Portrait "technical" draft sketch (from class)

// importar Bibliotecas
import processing.pdf.*;

// declarar Objetos e variáveis
PImage retrato;

// inicializar
size(800, 800);
background(255);
retrato = loadImage("data/images/han.jpg");

// desenhar

// gravar PDF
beginRecord(PDF, "export/han-vector-document.pdf");

// configurar desenho
fill(0);
stroke(0);

translate(100, 50);    // ajusta um pouco a "folha" para centrar o desenho. Ainda é cedo para isto, mas aqui vai para ir pensando: https://processing.org/reference/translate_.html

for (int i = 0; i < retrato.height; i++ ) {   // para cada linha na imagem
  for (int k = 0; k < retrato.width; k++) {   // e para cada coluna/célula
    color c = retrato.get(k, i);              // "lê" a cor do pixel do Array de pixels[] da imagem. Uma forma diferente seria correr a lista de forma linear com pixels[] https://processing.org/reference/PImage_pixels.html
    float mySize =  255-brightness(c) ;       // converte o valor do brilho da cor do pixel num número entre 0 e 255

    // altera a "forma" do pixel de um quadrado de cor para um desenho vetorial em 4 níveis diferentes
    if (mySize < 150) {                                // quando é claro (as cores estão invertidas no passo anterior)

      if (mySize > 50) {                               // se não for branco puro
        line(k*10 -10, i*10 + 10, k*10 +10, i*10-10);  // desenha uma linha
      }

      // cumulativamente…
      if (mySize > 100) {                              // verifica se for um pouco mais escuro
        line(k*10 -10, i*10 - 10, k*10 +10, i*10+10);  // acrescenta outra linha…
      }
    } else {                                           // se for escuro
      if (mySize < 200) {                              // escuro médio…
        ellipse(k*10, i*10, mySize/25, mySize/25);
      } else {
        // OK, let's insert some randomness here…
        // sorteia um número entre 0 e 1
        int tr = int ( random(2) );

        if (tr == 0) {                                                     // se for 0
          rect(k*10-10, i*10-10, mySize/25, mySize/25);                       // desenha um quadrado
        } else {                                                           // se for 1 
          triangle(k*10 -8, i*10 + 8, k*10 +8, i*10+8, k*10, i*10-8);    // desenha um triangulo
        }
      }
    }
  }
}

// if you create a PDF file, don't forget to "close" the file on the hard drive
endRecord();

// if you prefer to save optional image save
save("export/han-bitmap-picture.jpg");

Ah… hoje aprendi várias coisas novas (?), ou que pelo menos não me lembrava… Por exemplo—nunca dêm um nome ao vosso sketch que seja igual a uma palavra reservada ou variável “core”. Isto é, o primeiro sketch que fiz guardei-o como “PImage.pde”. Não me lembro se alguma vez o fiz e por isso, é claro que deu asneira… começaram logo a aparecer mensagens de erro na consola… demorei um pouco até “me cair a ficha”. Acho que já não me esqueço desta…

Este artigo é um documento em progresso. Editado a 2020-03-05