ShapeUtil.getGeometry
, selection rewrite (#1751)
This PR is a significant rewrite of our selection / hit testing logic. It - replaces our current geometric helpers (`getBounds`, `getOutline`, `hitTestPoint`, and `hitTestLineSegment`) with a new geometry API - moves our hit testing entirely to JS using geometry - improves selection logic, especially around editing shapes, groups and frames - fixes many minor selection bugs (e.g. shapes behind frames) - removes hit-testing DOM elements from ShapeFill etc. - adds many new tests around selection - adds new tests around selection - makes several superficial changes to surface editor APIs This PR is hard to evaluate. The `selection-omnibus` test suite is intended to describe all of the selection behavior, however all existing tests are also either here preserved and passing or (in a few cases around editing shapes) are modified to reflect the new behavior. ## Geometry All `ShapeUtils` implement `getGeometry`, which returns a single geometry primitive (`Geometry2d`). For example: ```ts class BoxyShapeUtil { getGeometry(shape: BoxyShape) { return new Rectangle2d({ width: shape.props.width, height: shape.props.height, isFilled: true, margin: shape.props.strokeWidth }) } } ``` This geometric primitive is used for all bounds calculation, hit testing, intersection with arrows, etc. There are several geometric primitives that extend `Geometry2d`: - `Arc2d` - `Circle2d` - `CubicBezier2d` - `CubicSpline2d` - `Edge2d` - `Ellipse2d` - `Group2d` - `Polygon2d` - `Rectangle2d` - `Stadium2d` For shapes that have more complicated geometric representations, such as an arrow with a label, the `Group2d` can accept other primitives as its children. ## Hit testing Previously, we did all hit testing via events set on shapes and other elements. In this PR, I've replaced those hit tests with our own calculation for hit tests in JavaScript. This removed the need for many DOM elements, such as hit test area borders and fills which only existed to trigger pointer events. ## Selection We now support selecting "hollow" shapes by clicking inside of them. This involves a lot of new logic but it should work intuitively. See `Editor.getShapeAtPoint` for the (thoroughly commented) implementation. ![Kapture 2023-07-23 at 23 27 27](https://github.com/tldraw/tldraw/assets/23072548/a743275c-acdb-42d9-a3fe-b3e20dce86b6) every sunset is actually the sun hiding in fear and respect of tldraw's quality of interactions This PR also fixes several bugs with scribble selection, in particular around the shift key modifier. ![Kapture 2023-07-24 at 23 34 07](https://github.com/tldraw/tldraw/assets/23072548/871d67d0-8d06-42ae-a2b2-021effba37c5) ...as well as issues with labels and editing. There are **over 100 new tests** for selection covering groups, frames, brushing, scribbling, hovering, and editing. I'll add a few more before I feel comfortable merging this PR. ## Arrow binding Using the same "hollow shape" logic as selection, arrow binding is significantly improved. ![Kapture 2023-07-22 at 07 46 25](https://github.com/tldraw/tldraw/assets/23072548/5aa724b3-b57d-4fb7-92d0-80e34246753c) a thousand wise men could not improve on this ## Moving focus between editing shapes Previously, this was handled in the `editing_shapes` state. This is moved to `useEditableText`, and should generally be considered an advanced implementation detail on a shape-by-shape basis. This addresses a bug that I'd never noticed before, but which can be reproduced by selecting an shape—but not focusing its input—while editing a different shape. Previously, the new shape became the editing shape but its input did not focus. ![Kapture 2023-07-23 at 23 19 09](https://github.com/tldraw/tldraw/assets/23072548/a5e157fb-24a8-42bd-a692-04ce769b1a9c) In this PR, you can select a shape by clicking on its edge or body, or select its input to transfer editing / focus. ![Kapture 2023-07-23 at 23 22 21](https://github.com/tldraw/tldraw/assets/23072548/7384e7ea-9777-4e1a-8f63-15de2166a53a) tldraw, glorious tldraw ### Change Type - [x] `major` — Breaking change ### Test Plan 1. Erase shapes 2. Select shapes 3. Calculate their bounding boxes - [ ] Unit Tests // todo - [ ] End to end tests // todo ### Release Notes - [editor] Remove `ShapeUtil.getBounds`, `ShapeUtil.getOutline`, `ShapeUtil.hitTestPoint`, `ShapeUtil.hitTestLineSegment` - [editor] Add `ShapeUtil.getGeometry` - [editor] Add `Editor.getShapeGeometry`
|
@ -10,11 +10,11 @@ export function sleep(ms: number) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// export async function expectToHaveNShapes(page: Page, numberOfShapes: number) {
|
// export async function expectToHaveNShapes(page: Page, numberOfShapes: number) {
|
||||||
// expect(await page.evaluate(() => editor.shapesArray.length)).toBe(numberOfShapes)
|
// expect(await page.evaluate(() => editor.shapesOnCurrentPage.length)).toBe(numberOfShapes)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// export async function expectToHaveNSelectedShapes(page: Page, numberOfSelectedShapes: number) {
|
// export async function expectToHaveNSelectedShapes(page: Page, numberOfSelectedShapes: number) {
|
||||||
// expect(await page.evaluate(() => editor.selectedIds.length)).toBe(numberOfSelectedShapes)
|
// expect(await page.evaluate(() => editor.selectedShapeIds.length)).toBe(numberOfSelectedShapes)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
declare const editor: Editor
|
declare const editor: Editor
|
||||||
|
|
|
@ -1,227 +1,229 @@
|
||||||
import test, { Page, expect } from '@playwright/test'
|
export {}
|
||||||
import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
|
|
||||||
import { rename, writeFile } from 'fs/promises'
|
|
||||||
import { setupPage } from '../shared-e2e'
|
|
||||||
|
|
||||||
declare const editor: Editor
|
// import test, { Page, expect } from '@playwright/test'
|
||||||
|
// import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
|
||||||
|
// import { rename, writeFile } from 'fs/promises'
|
||||||
|
// import { setupPage } from '../shared-e2e'
|
||||||
|
|
||||||
test.describe('Export snapshots', () => {
|
// declare const editor: Editor
|
||||||
const snapshots = {} as Record<string, TLShapePartial[]>
|
|
||||||
|
|
||||||
for (const fill of ['none', 'semi', 'solid', 'pattern']) {
|
// test.describe('Export snapshots', () => {
|
||||||
snapshots[`geo fill=${fill}`] = [
|
// const snapshots = {} as Record<string, TLShapePartial[]>
|
||||||
{
|
|
||||||
id: 'shape:testShape' as TLShapeId,
|
|
||||||
type: 'geo',
|
|
||||||
props: {
|
|
||||||
fill,
|
|
||||||
color: 'green',
|
|
||||||
w: 100,
|
|
||||||
h: 100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
snapshots[`arrow fill=${fill}`] = [
|
// for (const fill of ['none', 'semi', 'solid', 'pattern']) {
|
||||||
{
|
// snapshots[`geo fill=${fill}`] = [
|
||||||
id: 'shape:testShape' as TLShapeId,
|
// {
|
||||||
type: 'arrow',
|
// id: 'shape:testShape' as TLShapeId,
|
||||||
props: {
|
// type: 'geo',
|
||||||
color: 'light-green',
|
// props: {
|
||||||
fill: fill,
|
// fill,
|
||||||
arrowheadStart: 'square',
|
// color: 'green',
|
||||||
arrowheadEnd: 'dot',
|
// w: 100,
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
// h: 100,
|
||||||
end: { type: 'point', x: 100, y: 100 },
|
// },
|
||||||
bend: 20,
|
// },
|
||||||
},
|
// ]
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
snapshots[`draw fill=${fill}`] = [
|
// snapshots[`arrow fill=${fill}`] = [
|
||||||
{
|
// {
|
||||||
id: 'shape:testShape' as TLShapeId,
|
// id: 'shape:testShape' as TLShapeId,
|
||||||
type: 'draw',
|
// type: 'arrow',
|
||||||
props: {
|
// props: {
|
||||||
color: 'light-violet',
|
// color: 'light-green',
|
||||||
fill: fill,
|
// fill: fill,
|
||||||
segments: [
|
// arrowheadStart: 'square',
|
||||||
{
|
// arrowheadEnd: 'dot',
|
||||||
type: 'straight',
|
// start: { type: 'point', x: 0, y: 0 },
|
||||||
points: [{ x: 0, y: 0 }],
|
// end: { type: 'point', x: 100, y: 100 },
|
||||||
},
|
// bend: 20,
|
||||||
{
|
// },
|
||||||
type: 'straight',
|
// },
|
||||||
points: [
|
// ]
|
||||||
{ x: 0, y: 0 },
|
|
||||||
{ x: 100, y: 0 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'straight',
|
|
||||||
points: [
|
|
||||||
{ x: 100, y: 0 },
|
|
||||||
{ x: 0, y: 100 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'straight',
|
|
||||||
points: [
|
|
||||||
{ x: 0, y: 100 },
|
|
||||||
{ x: 100, y: 100 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'straight',
|
|
||||||
points: [
|
|
||||||
{ x: 100, y: 100 },
|
|
||||||
{ x: 0, y: 0 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isClosed: true,
|
|
||||||
isComplete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const font of ['draw', 'sans', 'serif', 'mono']) {
|
// snapshots[`draw fill=${fill}`] = [
|
||||||
snapshots[`geo font=${font}`] = [
|
// {
|
||||||
{
|
// id: 'shape:testShape' as TLShapeId,
|
||||||
id: 'shape:testShape' as TLShapeId,
|
// type: 'draw',
|
||||||
type: 'geo',
|
// props: {
|
||||||
props: {
|
// color: 'light-violet',
|
||||||
text: 'test',
|
// fill: fill,
|
||||||
color: 'blue',
|
// segments: [
|
||||||
font,
|
// {
|
||||||
w: 100,
|
// type: 'straight',
|
||||||
h: 100,
|
// points: [{ x: 0, y: 0 }],
|
||||||
},
|
// },
|
||||||
},
|
// {
|
||||||
]
|
// type: 'straight',
|
||||||
|
// points: [
|
||||||
|
// { x: 0, y: 0 },
|
||||||
|
// { x: 100, y: 0 },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'straight',
|
||||||
|
// points: [
|
||||||
|
// { x: 100, y: 0 },
|
||||||
|
// { x: 0, y: 100 },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'straight',
|
||||||
|
// points: [
|
||||||
|
// { x: 0, y: 100 },
|
||||||
|
// { x: 100, y: 100 },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'straight',
|
||||||
|
// points: [
|
||||||
|
// { x: 100, y: 100 },
|
||||||
|
// { x: 0, y: 0 },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// isClosed: true,
|
||||||
|
// isComplete: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
|
||||||
snapshots[`arrow font=${font}`] = [
|
// for (const font of ['draw', 'sans', 'serif', 'mono']) {
|
||||||
{
|
// snapshots[`geo font=${font}`] = [
|
||||||
id: 'shape:testShape' as TLShapeId,
|
// {
|
||||||
type: 'arrow',
|
// id: 'shape:testShape' as TLShapeId,
|
||||||
props: {
|
// type: 'geo',
|
||||||
color: 'blue',
|
// props: {
|
||||||
fill: 'solid',
|
// text: 'test',
|
||||||
arrowheadStart: 'square',
|
// color: 'blue',
|
||||||
arrowheadEnd: 'arrow',
|
// font,
|
||||||
font,
|
// w: 100,
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
// h: 100,
|
||||||
end: { type: 'point', x: 100, y: 100 },
|
// },
|
||||||
bend: 20,
|
// },
|
||||||
text: 'test',
|
// ]
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
snapshots[`arrow font=${font}`] = [
|
// snapshots[`arrow font=${font}`] = [
|
||||||
{
|
// {
|
||||||
id: 'shape:testShape' as TLShapeId,
|
// id: 'shape:testShape' as TLShapeId,
|
||||||
type: 'arrow',
|
// type: 'arrow',
|
||||||
props: {
|
// props: {
|
||||||
color: 'blue',
|
// color: 'blue',
|
||||||
fill: 'solid',
|
// fill: 'solid',
|
||||||
arrowheadStart: 'square',
|
// arrowheadStart: 'square',
|
||||||
arrowheadEnd: 'arrow',
|
// arrowheadEnd: 'arrow',
|
||||||
font,
|
// font,
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
// start: { type: 'point', x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 100, y: 100 },
|
// end: { type: 'point', x: 100, y: 100 },
|
||||||
bend: 20,
|
// bend: 20,
|
||||||
text: 'test',
|
// text: 'test',
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
]
|
// ]
|
||||||
|
|
||||||
snapshots[`note font=${font}`] = [
|
// snapshots[`arrow font=${font}`] = [
|
||||||
{
|
// {
|
||||||
id: 'shape:testShape' as TLShapeId,
|
// id: 'shape:testShape' as TLShapeId,
|
||||||
type: 'note',
|
// type: 'arrow',
|
||||||
props: {
|
// props: {
|
||||||
color: 'violet',
|
// color: 'blue',
|
||||||
font,
|
// fill: 'solid',
|
||||||
text: 'test',
|
// arrowheadStart: 'square',
|
||||||
},
|
// arrowheadEnd: 'arrow',
|
||||||
},
|
// font,
|
||||||
]
|
// start: { type: 'point', x: 0, y: 0 },
|
||||||
|
// end: { type: 'point', x: 100, y: 100 },
|
||||||
|
// bend: 20,
|
||||||
|
// text: 'test',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
|
||||||
snapshots[`text font=${font}`] = [
|
// snapshots[`note font=${font}`] = [
|
||||||
{
|
// {
|
||||||
id: 'shape:testShape' as TLShapeId,
|
// id: 'shape:testShape' as TLShapeId,
|
||||||
type: 'text',
|
// type: 'note',
|
||||||
props: {
|
// props: {
|
||||||
color: 'red',
|
// color: 'violet',
|
||||||
font,
|
// font,
|
||||||
text: 'test',
|
// text: 'test',
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
]
|
// ]
|
||||||
}
|
|
||||||
|
|
||||||
for (const [name, shapes] of Object.entries(snapshots)) {
|
// snapshots[`text font=${font}`] = [
|
||||||
test(`Exports with ${name}`, async ({ browser }) => {
|
// {
|
||||||
const page = await browser.newPage()
|
// id: 'shape:testShape' as TLShapeId,
|
||||||
await setupPage(page)
|
// type: 'text',
|
||||||
await page.evaluate((shapes) => {
|
// props: {
|
||||||
editor
|
// color: 'red',
|
||||||
.updateInstanceState({ exportBackground: false })
|
// font,
|
||||||
.selectAll()
|
// text: 'test',
|
||||||
.deleteShapes()
|
// },
|
||||||
.createShapes(shapes)
|
// },
|
||||||
}, shapes as any)
|
// ]
|
||||||
|
// }
|
||||||
|
|
||||||
snapshotTest(page)
|
// for (const [name, shapes] of Object.entries(snapshots)) {
|
||||||
})
|
// test(`Exports with ${name}`, async ({ browser }) => {
|
||||||
}
|
// const page = await browser.newPage()
|
||||||
|
// await setupPage(page)
|
||||||
|
// await page.evaluate((shapes) => {
|
||||||
|
// editor
|
||||||
|
// .updateInstanceState({ exportBackground: false })
|
||||||
|
// .selectAll()
|
||||||
|
// .deleteShapes(editor.selectedShapeIds)
|
||||||
|
// .createShapes(shapes)
|
||||||
|
// }, shapes as any)
|
||||||
|
|
||||||
for (const [name, shapes] of Object.entries(snapshots)) {
|
// snapshotTest(page)
|
||||||
test(`Exports with ${name} in dark mode`, async ({ browser }) => {
|
// })
|
||||||
const page = await browser.newPage()
|
// }
|
||||||
await setupPage(page)
|
|
||||||
await page.evaluate((shapes) => {
|
|
||||||
editor.user.updateUserPreferences({ isDarkMode: true })
|
|
||||||
editor
|
|
||||||
.updateInstanceState({ exportBackground: false })
|
|
||||||
.selectAll()
|
|
||||||
.deleteShapes()
|
|
||||||
.createShapes(shapes)
|
|
||||||
}, shapes as any)
|
|
||||||
|
|
||||||
snapshotTest(page)
|
// for (const [name, shapes] of Object.entries(snapshots)) {
|
||||||
})
|
// test(`Exports with ${name} in dark mode`, async ({ browser }) => {
|
||||||
}
|
// const page = await browser.newPage()
|
||||||
})
|
// await setupPage(page)
|
||||||
|
// await page.evaluate((shapes) => {
|
||||||
|
// editor.user.updateUserPreferences({ isDarkMode: true })
|
||||||
|
// editor
|
||||||
|
// .updateInstanceState({ exportBackground: false })
|
||||||
|
// .selectAll()
|
||||||
|
// .deleteShapes(editor.selectedShapeIds)
|
||||||
|
// .createShapes(shapes)
|
||||||
|
// }, shapes as any)
|
||||||
|
|
||||||
async function snapshotTest(page: Page) {
|
// snapshotTest(page)
|
||||||
const downloadEvent = page.waitForEvent('download')
|
// })
|
||||||
await page.click('[data-testid="main.menu"]')
|
// }
|
||||||
await page.click('[data-testid="menu-item.edit"]')
|
// })
|
||||||
await page.click('[data-testid="menu-item.export-as"]')
|
|
||||||
await page.click('[data-testid="menu-item.export-as-svg"]')
|
|
||||||
|
|
||||||
const download = await downloadEvent
|
// async function snapshotTest(page: Page) {
|
||||||
const path = (await download.path()) as string
|
// const downloadEvent = page.waitForEvent('download')
|
||||||
// assert(path)
|
// await page.click('[data-testid="main.menu"]')
|
||||||
await rename(path, path + '.svg')
|
// await page.click('[data-testid="menu-item.edit"]')
|
||||||
await writeFile(
|
// await page.click('[data-testid="menu-item.export-as"]')
|
||||||
path + '.html',
|
// await page.click('[data-testid="menu-item.export-as-svg"]')
|
||||||
`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<img src="${path}.svg" />
|
|
||||||
`,
|
|
||||||
'utf-8'
|
|
||||||
)
|
|
||||||
|
|
||||||
await page.goto(`file://${path}.html`)
|
// const download = await downloadEvent
|
||||||
const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
|
// const path = (await download.path()) as string
|
||||||
await expect(page).toHaveScreenshot({
|
// // assert(path)
|
||||||
omitBackground: true,
|
// await rename(path, path + '.svg')
|
||||||
clip,
|
// await writeFile(
|
||||||
})
|
// path + '.html',
|
||||||
}
|
// `
|
||||||
|
// <!DOCTYPE html>
|
||||||
|
// <meta charset="utf-8" />
|
||||||
|
// <meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
// <img src="${path}.svg" />
|
||||||
|
// `,
|
||||||
|
// 'utf-8'
|
||||||
|
// )
|
||||||
|
|
||||||
|
// await page.goto(`file://${path}.html`)
|
||||||
|
// const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
|
||||||
|
// await expect(page).toHaveScreenshot({
|
||||||
|
// omitBackground: true,
|
||||||
|
// clip,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 930 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 933 B After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 874 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 922 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 923 B |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 933 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 874 B After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 874 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 859 B |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 859 B |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 923 B |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 930 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 922 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 923 B After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 874 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 923 B After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 923 B After Width: | Height: | Size: 1.8 KiB |