Vamos conferir as principais estruturas de dados, que são entradas e saídas do pipeline de renderização.
Essas estruturas de dados são:
- As árvores de frames são compostas por nós locais e remotos que representam quais documentos da Web estão em qual processo de renderização e qual renderizador do Blink.
- A árvore de fragmentos imutáveis representa a saída (e entrada) do algoritmo de restrição de layout.
- As árvores de propriedades representam as hierarquias de transformação, clipe, efeito e rolagem de um documento da Web. Eles são usados em todo o pipeline.
- As listas de exibição e os blocos de pintura são as entradas para os algoritmos de rasterização e estratificação.
- Os frames do compositor encapsulam superfícies, renderizam superfícies e blocos de textura de GPU que são usados para desenhar usando a GPU.
Antes de analisar essas estruturas de dados, o exemplo a seguir é baseado em um da análise de arquitetura. Esse exemplo é usado ao longo deste documento com demonstrações de como as estruturas de dados se aplicam a ele.
<!-- Example code -->
<html>
<div style="overflow: hidden; width: 100px; height: 100px;">
<iframe style="filter: blur(3px);
transform: rotateZ(1deg);
width: 100px; height: 300px"
id="one" src="foo.com/etc"></iframe>
</div>
<iframe style="top:200px;
transform: scale(1.1) translateX(200px)"
id="two" src="bar.com"></iframe>
</html>
Árvores de frame
Às vezes, o Chrome pode renderizar um frame entre origens em um processo de renderização diferente do frame pai.
No código de exemplo, há três frames no total:
Com o isolamento de site, o Chromium usa dois processos de renderização para renderizar esta página da Web. Cada processo de renderização tem a própria representação da árvore de frames para essa página da Web:
Um frame renderizado em um processo diferente é representado como um frame remoto. Um frame remoto contém as informações mínimas necessárias para atuar como um marcador de posição na renderização, como as dimensões, por exemplo. Caso contrário, o frame remoto não contém as informações necessárias para renderizar o conteúdo real.
Em contraste, um frame local representa um frame que passa pelo pipeline padrão de renderização. O frame local contém todas as informações necessárias para transformar os dados desse frame (como a árvore DOM e os dados de estilo) em algo que possa ser renderizado e exibido.
O pipeline de renderização opera na granularidade de um
fragmento de árvore de frames local.
Considere um exemplo mais complicado com foo.com
como o frame principal:
<iframe src="bar.com"></iframe>
E o seguinte subframe bar.com
:
<iframe src="foo.com/etc"></iframe>
Embora ainda existam apenas dois renderizadores, agora há três fragmentos de árvore de
frames locais, com dois no processo de renderização para foo.com
e um no
processo de renderização para bar.com
:
Para produzir um frame de compositor para a página da Web, o Viz solicita simultaneamente um frame de compositor do frame raiz de cada uma das três árvores de frames locais e, em seguida, os agrega. Consulte também a seção de frames do compositor.
O frame principal foo.com
e o subframe foo.com/other-page
fazem parte da mesma árvore de frames e são renderizados no mesmo processo.
No entanto, os dois frames ainda têm
ciclos de vida de documentos independentes,
já que fazem parte de diferentes fragmentos de árvore de frames locais.
Por esse motivo, é impossível gerar um frame de compositor para ambos em uma atualização.
O processo de renderização não tem informações suficientes
para compor o frame do compositor gerado para foo.com/other-page
diretamente no frame do compositor do frame principal foo.com
.
Por exemplo, o frame pai bar.com
fora do processo pode afetar a exibição do iframe foo.com/other-url
,
transformando o iframe com CSS ou ocultando partes dele com outros elementos no DOM.
Fluxo de atualizações de propriedades visuais
Propriedades visuais, como o fator de escala do dispositivo e o tamanho da janela de visualização, afetam a saída renderizada e precisam ser sincronizadas entre os fragmentos da árvore de frames locais. A raiz de cada fragmento de árvore de frame local tem um objeto de widget associado a ele. As atualizações de propriedade visual vão para o widget do frame principal antes de se propagar para os widgets restantes de cima para baixo.
Por exemplo, quando o tamanho da janela de visualização muda:
Esse processo não é instantâneo, então as propriedades visuais replicadas também incluem um token de sincronização. O compositor de visualização usa esse token de sincronização para esperar que todos os fragmentos da árvore de frames locais enviem um frame de compositor com o token de sincronização atual. Esse processo evita misturar frames do compositor com propriedades visuais diferentes.
A árvore de fragmentos imutáveis
A árvore de fragmentos imutáveis é a saída da fase de layout do pipeline de renderização. Ele representa a posição e o tamanho de todos os elementos na página (sem transformações aplicadas).
Cada fragmento representa uma parte de um elemento DOM. Normalmente, há apenas um fragmento por elemento, mas pode haver mais se ele for dividido em diferentes páginas ao imprimir ou em colunas quando estiver em um contexto de várias colunas.
Depois do layout, cada fragmento se torna imutável e nunca mais é alterado. Também impomos algumas outras restrições. Não fazemos o seguinte:
- Permita todas as referências "up" na árvore. Uma criança não pode ter um ponteiro para o pai.
- "Bolha" de dados para baixo na árvore (um filho só lê informações dos filhos, não do pai).
Essas restrições permitem que um fragmento seja reutilizado para um layout posterior. Sem essas restrições, precisaríamos regenerar toda a árvore com frequência, o que é caro.
A maioria dos layouts são atualizações incrementais, por exemplo, um app da Web que atualiza uma pequena parte da interface em resposta ao usuário clicar em um elemento. O ideal é que o layout só faça o trabalho proporcional ao que realmente mudou na tela. Para isso, reutilizamos o máximo possível de partes da árvore anterior. Isso significa que, normalmente, só precisamos reconstruir a coluna da árvore.
No futuro, esse design imutável poderá permitir que façamos coisas interessantes, como transmitir a árvore de fragmentos imutáveis entre os limites da linha de execução, se necessário, para executar fases subsequentes em uma linha de execução diferente, gerar várias árvores para uma animação de layout suave ou executar layouts especulativos paralelos. Ele também oferece o potencial do layout de várias linhas de execução.
Itens de fragmento inline
O conteúdo inline (principalmente texto estilizado) usa uma representação um pouco diferente. Em vez de uma estrutura de árvore com caixas e ponteiros, representamos o conteúdo inline em uma lista simples que representa a árvore. O principal benefício é que uma representação de lista plana para inlines é rápida, útil para inspecionar ou consultar estruturas de dados inline e eficiente em termos de memória. Isso é extremamente importante para o desempenho da renderização da Web, porque a renderização de texto é muito complexa e pode se tornar facilmente a parte mais lenta do pipeline, a menos que seja altamente otimizada.
A lista plana é criada para cada contexto de formatação inline na ordem de uma pesquisa em profundidade do subárvore de layout inline. Cada entrada na lista é uma tupla de (objeto, número de descendentes). Por exemplo, considere este DOM:
<div style="width: 0;">
<span style="color: blue; position: relative;">Hi</span> <b>there</b>.
</div>
A propriedade width
é definida como 0 para que a linha seja concatenada entre "Olá" e "lá".
Quando o contexto de formatação inline para essa situação é representado como uma árvore, ele tem esta aparência:
{
"Line box": {
"Box <span>": {
"Text": "Hi"
}
},
"Line box": {
"Box <b>": {
"Text": "There"
}
},
{
"Text": "."
}
}
A lista plana tem esta aparência:
- (Caixa de linha, 2)
- (Box <span>, 1)
- (Text "Hi", 0)
- (Caixa de linha, 3)
- (Box <b>, 1)
- (Text "there", 0)
- (Texto ".", 0)
Há muitos consumidores dessa estrutura de dados: APIs de acessibilidade
e APIs de geometria, como
getClientRects
e contenteditable
.
Cada um tem requisitos diferentes.
Esses componentes acessam a estrutura de dados plana usando um cursor conveniente.
O cursor
tem APIs, como MoveToNext
, MoveToNextLine
e CursorForChildren
.
Essa representação do cursor é muito eficiente para conteúdo de texto por vários motivos:
- A iteração na ordem de pesquisa em profundidade é muito rápida. Ele é usado com frequência porque é semelhante aos movimentos de careta. Como é uma lista simples, a pesquisa em profundidade está apenas incrementando o deslocamento da matriz, proporcionando iterações rápidas e localidade de memória.
- Ele fornece a pesquisa em largura, que é necessária quando, por exemplo, pinta o plano de fundo de caixas de linha e inline.
- Saber o número de descendentes facilita a transição para o próximo irmão (basta incrementar o deslocamento da matriz por esse número).
Árvores de propriedades
O DOM é uma árvore de elementos (mais nós de texto), e o CSS pode aplicar vários estilos aos elementos.
Isso aparece de quatro maneiras:
- Layout:entradas para o algoritmo de restrição de layout.
- Paint:como pintar e rasterizar o elemento (mas não os elementos descendentes).
- Visual:efeitos de raster/desenho aplicados ao subárvore DOM, como transformações, filtros e recorte.
- Rolar:recorte e rolagem do subárvore contida, alinhada ao eixo e com cantos arredondados.
As árvores de propriedades são estruturas de dados que explicam como os efeitos visuais e de rolagem se aplicam aos elementos DOM. Eles fornecem os meios para responder a perguntas como: onde, em relação à tela, está um determinado elemento DOM, considerando o tamanho e a posição do layout? E: qual sequência de operações de GPU deve ser usada para aplicar efeitos visuais e de rolagem?
Os efeitos visuais e de rolagem na Web são muito complicados. A coisa mais importante que as árvores de propriedades fazem é traduzir essa complexidade em uma única estrutura de dados que representa com precisão a estrutura e o significado delas, ao mesmo tempo em que remove o restante da complexidade do DOM e do CSS. Isso nos permite implementar algoritmos para composição e rolagem com muito mais confiança. Especificamente, faça o seguinte:
- Geometrias com potencial de erro e outros cálculos podem ser centralizadas em um só lugar.
- A complexidade de criar e atualizar árvores de propriedades é isolada em um estágio do pipeline de renderização.
- É muito mais fácil e rápido enviar árvores de propriedades para diferentes linhas de execução e processos do que o estado completo do DOM, o que permite usá-las para muitos casos de uso.
- Quanto mais casos de uso houver, mais vitórias conseguiremos com o cache de geometria criado, já que eles podem reutilizar os caches uns dos outros.
O RenderingNG usa árvores de propriedades para várias finalidades, incluindo:
- Separar a composição da pintura e da composição da linha de execução principal.
- Como determinar uma estratégia de composição / desenho ideal.
- Medição da geometria do IntersectionObserver.
- Evitar trabalho para elementos fora da tela e blocos de textura da GPU.
- Invalidação eficiente e precisa de pintura e raster.
- Medir mudança de layout e Largest Contentful Paint nas Core Web Vitals.
Cada documento da Web tem quatro árvores de propriedades separadas: transformação, clipe, efeito e rolagem.(*) A árvore de transformação representa as transformações e rolagem do CSS. Uma transformação de rolagem é representada como uma matriz de transformação 2D. A árvore de clipes representa clipes de overflow. A árvore de efeitos representa todos os outros efeitos visuais: opacidade, filtros, máscaras, modos de mesclagem e outros tipos de clipes, como clip-path. A árvore de rolagem representa informações sobre a rolagem, como como as rolagens se encadeiam. Ela é necessária para realizar a rolagem na linha de execução do compositor. Cada nó em uma árvore de propriedades representa um rolagem ou efeito visual aplicado por um elemento DOM. Se houver vários efeitos, pode haver mais de um nó da árvore de propriedades em cada árvore para o mesmo elemento.
A topologia de cada árvore é como uma representação esparsa do DOM. Por exemplo, se houver três elementos DOM com clipes de overflow, haverá três nós da árvore de clipes, e a estrutura da árvore de clipes vai seguir a relação de bloco de contenção entre os clipes de overflow. Há também links entre as árvores. Esses links indicam a hierarquia relativa do DOM e, portanto, a ordem de aplicação dos nós. Por exemplo, se uma transformação em um elemento DOM estiver abaixo de outro elemento DOM com um filtro, a transformação será aplicada antes do filtro.
Cada elemento DOM tem um estado da árvore de propriedades, que é um 4-tuple (transformação, clipe, efeito, rolagem) que indica o clipe ancestral mais próximo, a transformação e os nós da árvore de efeitos que entram em vigor nesse elemento. Isso é muito conveniente, porque com essas informações, sabemos exatamente a lista de clipes, transformações e efeitos que se aplicam a esse elemento e em que ordem. Isso informa onde ele está na tela e como ele é renderizado.
Exemplo
(fonte)
<html>
<div style="overflow: scroll; width: 100px; height: 100px;">
<iframe style="filter: blur(3px);
transform: rotateZ(1deg);
width: 100px; height: 300px"
id="one" srcdoc="iframe one"></iframe>
</div>
<iframe style="top:200px;
transform: scale(1.1) translateX(200px)" id=two
srcdoc="iframe two"></iframe>
</html>
No exemplo anterior (que é um pouco diferente do exemplo da introdução), estes são os principais elementos das árvores de propriedades geradas:
Mostrar listas e pintar blocos
Um item de exibição contém comandos de desenho de baixo nível (confira aqui) que podem ser rasterizados com a Skia. Os itens de exibição geralmente são simples, com apenas alguns comandos de exibição, como desenhar uma borda ou um plano de fundo. A caminhada pela árvore de pintura itera sobre a árvore de layout e os fragmentos associados, seguindo a ordem de pintura do CSS para produzir uma lista de itens de exibição.
Exemplo:
<div id="green" style="background:green; width:80px;">
Hello world
</div>
<div id="blue" style="width:100px;
height:100px; background:blue;
position:absolute;
top:0; left:0; z-index:-1;">
</div>
Esse HTML e CSS produziriam a seguinte lista de exibição, em que cada célula é um item de exibição:
Plano de fundo da visualização | #blue em segundo plano |
#green em segundo plano |
#green texto inline |
---|---|---|---|
drawRect com tamanho 800x600 e cor branca. |
drawRect com tamanho 100 x 100 na posição 0,0 e cor azul. |
drawRect com tamanho 80x18 na posição 8,8 e cor verde. |
drawTextBlob com a posição 8,8 e o texto "Hello world". |
A lista de itens de exibição é ordenada de trás para frente. No exemplo acima, a div verde está antes da div azul na ordem do DOM, mas a ordem de pintura do CSS exige que a div azul com z-index negativo seja pintada antes (etapa 3) da div verde (etapa 4.1). Os itens de exibição correspondem aproximadamente a etapas atômicas da especificação de ordem de pintura do CSS. Um único elemento DOM pode resultar em vários itens de exibição, como a forma como #green tem um item de exibição para o plano de fundo e outro para o texto inline. Essa granularidade é importante para representar a complexidade total da especificação de ordem de pintura do CSS, como a intercalação criada por margem negativa:
<div id="green" style="background:green; width:80px;">
Hello world
</div>
<div id="gray" style="width:35px; height:20px;
background:gray;margin-top:-10px;"></div>
Isso produz a lista de exibição a seguir, em que cada célula é um item de exibição:
Plano de fundo da visualização | #green em segundo plano |
#gray em segundo plano |
#green texto inline |
---|---|---|---|
drawRect com tamanho 800x600 e cor branca. |
drawRect com tamanho 80x18 na posição 8,8 e cor verde. |
drawRect com tamanho 35x20 na posição 8,16 e cor cinza. |
drawTextBlob com a posição 8,8 e o texto "Hello world". |
A lista de itens de exibição é armazenada e reutilizada em atualizações posteriores. Se um objeto de layout não tiver mudado durante a árvore de pintura, os itens de exibição dele serão copiados da lista anterior. Uma otimização adicional depende de uma propriedade da especificação de ordem de pintura do CSS: os contextos de empilhamento são pintados de forma atômica. Se nenhum objeto de layout tiver mudado em um contexto de empilhamento, o percurso da árvore de pintura vai pular o contexto de empilhamento e copiar toda a sequência de itens de exibição da lista anterior.
O estado atual da árvore de propriedades é mantido durante a caminhada pela árvore de pintura e a lista de itens de exibição é agrupada em "porções" de itens de exibição que compartilham o mesmo estado da árvore de propriedades. Isso é demonstrado no exemplo abaixo:
<div id="scroll" style="background:pink; width:100px;
height:100px; overflow:scroll;
position:absolute; top:0; left:0;">
Hello world
<div id="orange" style="width:75px; height:200px;
background:orange; transform:rotateZ(25deg);">
I'm falling
</div>
</div>
Isso produz a lista de exibição a seguir, em que cada célula é um item de exibição:
Plano de fundo da visualização | #scroll em segundo plano |
#scroll texto inline |
#orange em segundo plano |
#orange texto inline |
---|---|---|---|---|
drawRect com tamanho 800x600 e cor branca. |
drawRect com tamanho 100 x 100 na posição 0,0 e cor rosa. |
drawTextBlob com posição 0,0 e texto "Hello world". |
drawRect com tamanho 75 x 200 na posição 0,0 e cor laranja. |
drawTextBlob com posição 0,0 e o texto "I'm falling". |
A árvore de propriedades de transformação e os blocos de pintura seriam (simplificados para brevidade):
A lista ordenada de blocos de pintura, que são grupos de itens de exibição e um estado de árvore de propriedades, são as entradas para a etapa de estratificação do pipeline de renderização. A lista inteira de blocos de pintura poderia ser mesclada em uma única camada composta e rasterizada em conjunto, mas isso exigiria uma rasterização cara sempre que o usuário rolasse a tela. Uma camada composta poderia ser criada para cada bloco de pintura e rasterizada individualmente para evitar toda a rasterização, mas isso esgotaria rapidamente a memória da GPU. A etapa de estratificação precisa fazer trocas entre a memória da GPU e reduzir os custos quando as coisas mudam. Uma boa abordagem geral é mesclar os blocos por padrão e não mesclar os blocos de pintura que têm estados de árvore de propriedades que devem mudar na linha de execução do compositor, como rolagem de linha de execução do compositor ou animações de transformação de linha de execução do compositor.
O exemplo anterior precisa produzir duas camadas compostas:
- Uma camada composta de 800 x 600 que contém os comandos de desenho:
drawRect
com tamanho 800x600 e cor brancadrawRect
com tamanho 100x100 na posição 0,0 e cor rosa
- Uma camada composta de 144 x 224 que contém os comandos de exibição:
drawTextBlob
com posição 0,0 e texto "Hello world"- traduzir 0,18
rotateZ(25deg)
drawRect
com tamanho 75 x 200 na posição 0,0 e cor laranjadrawTextBlob
com posição 0,0 e texto "I'm falling"
Se o usuário rolar #scroll
,
a segunda camada composta será movida, mas nenhuma rasterização será necessária.
No exemplo, da seção anterior sobre árvores de propriedades, há seis blocos de pintura. Junto com os estados da árvore de propriedades (transformar, clipar, efeito, rolar), eles são:
- Plano de fundo do documento: rolagem de documento, clipe de documento, raiz, rolagem de documento.
- Canto horizontal, vertical e de rolagem para div (três partes de pintura separadas):
rolagem de documento, clipe de documento, desfoque
#one
, rolagem de documento. - Iframe
#one
:#one
rotate, overflow scroll clip,#one
blur, div scroll. #two
do iframe:#two
escala, clipe de documento, raiz, rolagem de documento.
Frames do compositor: superfícies, renderização de superfícies e blocos de textura da GPU
Os processos de renderização e do navegador gerenciam a rasterização do conteúdo e enviam frames do compositor para o processo de visualização para apresentação na tela. Os frames do compositor representam como unir conteúdo rasterizado e renderizá-lo de forma eficiente usando a GPU.
Blocos
Em teoria, um compositor de processo de renderização ou de processo do navegador poderia rasterizar pixels em uma única textura o tamanho total da viewport do renderizador e enviar essa textura para a visualização. Para exibir, o compositor de exibição precisa copiar os pixels dessa única textura para a posição apropriada no frame buffer (por exemplo, a tela). No entanto, se esse compositor quisesse atualizar até um único pixel, seria necessário rasterizar novamente a viewport completa e enviar uma nova textura para a Viz.
Em vez disso, a viewport é dividida em blocos. Um bloco de textura da GPU separado oferece suporte a cada bloco com os pixels rasterizados para parte da viewport. O renderizador pode atualizar blocos individuais ou até mudar a posição deles na tela. Por exemplo, ao rolar um site, a posição dos blocos existentes mudava para cima e, apenas ocasionalmente, um novo bloco precisava ser rasterizado para o conteúdo mais abaixo na página.
Quads e superfícies
Os blocos de textura da GPU são um tipo especial de quadro, que é apenas um nome chique para uma categoria de textura ou outra. Um quad identifica a textura de entrada e indica como transformá-la e aplicar efeitos visuais a ela. Por exemplo, os blocos de conteúdo normais têm uma transformação que indica a posição x, y na grade de blocos.
Esses blocos rasterizados são agrupados em um passo de renderização, que é uma lista de quads. A passagem de renderização não contém informações de pixel. Em vez disso, ela tem instruções sobre onde e como desenhar cada quad para produzir a saída de pixel desejada. Há um quadro de desenho para cada bloco de textura da GPU. O compositor de exibição só precisa iterar pela lista de quadriláteros, desenhando cada um com os efeitos visuais especificados, para produzir a saída de pixels desejada para o passe de renderização. A composição de quadrados de exibição para uma passagem de renderização pode ser feita de maneira eficiente na GPU, porque os efeitos visuais permitidos são cuidadosamente escolhidos para serem mapeados diretamente para recursos da GPU.
Há outros tipos de quadrângulos de desenho além dos blocos rasterizados. Por exemplo, há quadrados de desenho de cores sólidas que não são apoiados por uma textura, ou quadrados de desenho de textura para texturas não de blocos, como vídeo ou tela.
Também é possível que um frame do compositor incorpore outro frame do compositor. Por exemplo, o compositor do navegador produz um frame de compositor com a interface do navegador e um retângulo vazio em que o conteúdo do compositor de renderização será incorporado. Outro exemplo são os iframes isolados do site. Essa incorporação é feita por superfícies.
Quando um compositor envia um frame, ele é acompanhado por um identificador, chamado de ID da superfície, permitindo que outros frames do compositor o incorporem por referência. O frame mais recente do compositor enviado com um ID de superfície específico é armazenado pelo Viz. Outro frame do compositor pode se referir a ele mais tarde usando um quadro de renderização de superfície, e, portanto, o Viz sabe o que renderizar. Os quadrados de renderização da superfície contêm apenas IDs de superfície, e não texturas.
Transmissões de renderização intermediárias
Alguns efeitos visuais, como muitos filtros ou modos de mesclagem avançados, exigem que dois ou mais quadrados sejam renderizados em uma textura intermediária. Em seguida, a textura intermediária é renderizada em um buffer de destino na GPU (ou possivelmente em outra textura intermediária), aplicando o efeito visual ao mesmo tempo. Para permitir isso, um frame do compositor contém uma lista de transmissões de renderização. Sempre há uma passagem de renderização raiz, que é renderizada por último e cujo destino corresponde ao frame buffer, e pode haver mais.
A possibilidade de vários passes de renderização explica o nome "passo de renderização". Cada passagem precisa ser executada sequencialmente na GPU, em várias "passagens", enquanto uma única passagem pode ser concluída em uma única computação de GPU massiva paralela.
Agregação
Vários frames do compositor são enviados para a visualização e precisam ser renderizados na tela juntos. Isso é feito por uma fase de agregação que os converte em um único frame de compositor agregado. A agregação substitui os quadrados de renderização da superfície pelos frames do compositor especificados. É também uma oportunidade para otimizar texturas intermediárias desnecessárias ou conteúdo fora da tela. Por exemplo, em muitos casos, o frame do compositor de um iframe isolado do site não precisa de uma textura intermediária própria e pode ser renderizado diretamente no frame buffer usando os quadrados de renderização adequados. A fase de agregação identifica essas otimizações e as aplica com base no conhecimento global que não é acessível a compositores de renderização individuais.
Exemplo
Confira os frames do compositor que representam o exemplo do início desta postagem.
foo.com/index.html
surface: id=0- Renderização 0:renderização para saída.
- Quad de renderização de desenho: renderização com desfoque de 3 pixels e recorte no passe de renderização 0.
- Passagem de renderização 1:
- Desenha quadrados para o conteúdo do bloco do iframe
#one
, com posições x e y para cada um.
- Desenha quadrados para o conteúdo do bloco do iframe
- Passagem de renderização 1:
- Quad de renderização de superfície: com o ID 2, renderizado com transformação de escala e de tradução.
- Quad de renderização de desenho: renderização com desfoque de 3 pixels e recorte no passe de renderização 0.
- Renderização 0:renderização para saída.
- Plataforma da interface do navegador: ID=1
- Renderização 0:renderização para saída.
- Desenhar quadriláteros para a interface do navegador (também em blocos)
- Renderização 0:renderização para saída.
bar.com/index.html
surface: ID=2- Renderização 0:renderização para saída.
- Desenha quadrados para conteúdos do iframe
#two
, com posições x e y para cada um.
- Desenha quadrados para conteúdos do iframe
- Renderização 0:renderização para saída.
Ilustrações de Una Kravets.