Skip to content

Commit

Permalink
feat(AnimatedSprite): add AnimatedSprite, playground, docs
Browse files Browse the repository at this point in the history
  • Loading branch information
andretchen0 committed Jan 11, 2024
1 parent 19b08cc commit d6ea81b
Show file tree
Hide file tree
Showing 12 changed files with 1,140 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default defineConfig({
{ text: 'useFBO', link: '/guide/abstractions/use-fbo' },
{ text: 'useSurfaceSampler', link: '/guide/abstractions/use-surface-sampler' },
{ text: 'Sampler', link: '/guide/abstractions/sampler' },
{ text: 'AnimatedSprite', link: '/guide/abstractions/animated-sprite' },
],
},
{
Expand Down
86 changes: 86 additions & 0 deletions docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import { OrbitControls, Line2, AnimatedSprite } from '@tresjs/cientos'
import { TresLeches, useControls } from '@tresjs/leches'
import '@tresjs/leches/styles'
import { TexturePackerFrameDataArray } from '../../../../src/core/abstractions/AnimatedSprite/Atlas'

Check failure on line 6 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

All imports in the declaration are only used as types. Use `import type`
const { anchorX, anchorY, fps } = useControls({
anchorX: { value: 0.5, min: 0, max: 1, step: 0.1 },
anchorY: { value: 0.5, min: 0, max: 1, step: 0.1 },
fps: {value:5, min:0, max:30, step:1}

Check failure on line 11 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

A space is required after '{'

Check failure on line 11 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Missing space before value for key 'value'

Check failure on line 11 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Missing space before value for key 'min'

Check failure on line 11 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Missing space before value for key 'max'

Check failure on line 11 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Missing space before value for key 'step'

Check failure on line 11 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

A space is required before '}'

Check failure on line 11 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Missing trailing comma
})
const anchorDemoAtlas: TexturePackerFrameDataArray = { frames: [] }
const anchorDemoImgData = (() => {
const NUM_ROWS_COLS = 64
const rects: { x: number, y: number, w: number, h: number }[] = []

Check failure on line 17 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected a semicolon

Check failure on line 17 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected a semicolon
let h = 0
for (let r = 0; r < NUM_ROWS_COLS; r += h) {
let w = 0
h++
if (r + h >= NUM_ROWS_COLS) {
continue
}
for (let c = 0; c < NUM_ROWS_COLS; c += w) {
w++
if (c + w >= NUM_ROWS_COLS) {
continue
}
rects.push({ x: c, y: r, w, h })
}
}
const canvas = document.createElement('canvas')
const IMG_SIZE = 2048
const COL_SIZE = IMG_SIZE / NUM_ROWS_COLS
const ROW_SIZE = IMG_SIZE / NUM_ROWS_COLS
canvas.width = IMG_SIZE
canvas.height = IMG_SIZE
document.body.append(canvas)
const ctx = canvas.getContext('2d')!
const EDGE_ANCHOR_SIZE = 6
rects.forEach((rect, i) => {
const frame = { x: rect.x * COL_SIZE, y: rect.y * ROW_SIZE, w: rect.w * COL_SIZE, h: rect.h * ROW_SIZE }
const { x, y, w, h } = frame
anchorDemoAtlas.frames.push({ filename: 'rect_' + i.toString().padStart(4, '0'), frame })
ctx.fillStyle = `#222`
ctx.fillRect(x, y, w, h)
ctx.fillStyle = '#999'
ctx.fillRect(x + w * 0.5 - EDGE_ANCHOR_SIZE * 0.5, y + h * 0.5 - EDGE_ANCHOR_SIZE * 0.5, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
ctx.fillRect(x, y, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
ctx.fillRect(x + w * 0.5 - EDGE_ANCHOR_SIZE * 0.5, y, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
ctx.fillRect(x + w - EDGE_ANCHOR_SIZE, y, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
ctx.fillRect(x, y + h * 0.5 - EDGE_ANCHOR_SIZE * 0.5, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
ctx.fillRect(x + w - EDGE_ANCHOR_SIZE, y + h * 0.5 - EDGE_ANCHOR_SIZE * 0.5, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
ctx.fillRect(x, y + h - EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
ctx.fillRect(x + w * 0.5 - EDGE_ANCHOR_SIZE * 0.5, y + h - EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
ctx.fillRect(x + w - EDGE_ANCHOR_SIZE, y + h - EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE)
})
const imgData = canvas.toDataURL()
canvas.parentElement?.removeChild(canvas)
return imgData
})()
</script>

<template>
<TresLeches style="position:absolute; left:10px; top:10px;" />
<TresCanvas clear-color="#82DBC5">
<TresPerspectiveCamera :position="[5, 1, 5]" :look-at="[-2, 0, 0]" />
<OrbitControls />
<TresGroup :position-x="2">
<Line2 :points="[[-0.25, 0, 0], [0.25, 0, 0]]" :line-width="1" color="#FF0000" />
<Line2 :points="[[0, -0.25, 0], [0, 0.25, 0]]" :line-width="1" color="#00FF00" />
<Line2 :points="[[0, 0, -0.25], [0, 0, 0.25]]" :line-width="1" color="#0000FF" />
<Suspense>

Check warning on line 79 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected indentation of 6 spaces but found 3 spaces
<AnimatedSprite :image="anchorDemoImgData" :atlas="anchorDemoAtlas" animation="rect"

Check warning on line 80 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected a linebreak before this attribute
:anchor="[anchorX.value, anchorY.value]" :fps="fps.value" />

Check warning on line 81 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected indentation of 24 spaces but found 10 spaces

Check warning on line 81 in docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected 1 line break before closing bracket, but no line breaks found
</Suspense>
<TresGridHelper :args="[10, 10]" />
</TresGroup>
</TresCanvas>
</template>
18 changes: 18 additions & 0 deletions docs/.vitepress/theme/components/AnimatedSpriteDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import { AnimatedSprite, OrbitControls } from '@tresjs/cientos'
</script>

<template>
<TresLeches />
<TresCanvas clear-color="#82DBC5">
<OrbitControls />
<TresPerspectiveCamera :position="[0, 0, 5]"/>

Check warning on line 10 in docs/.vitepress/theme/components/AnimatedSpriteDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected a space before '/>', but not found
<Suspense>
<AnimatedSprite
image="https://raw.githubusercontent.com/Tresjs/assets/6c0b087768a0a2b76148c99fc87d7e6ddc3c6d66/textures/animated-sprite/namedAnimationsTexture.png"
atlas="https://raw.githubusercontent.com/Tresjs/assets/6c0b087768a0a2b76148c99fc87d7e6ddc3c6d66/textures/animated-sprite/namedAnimationsAtlas.json"
animation="yes" :fps="10" :loop="true" />

Check warning on line 15 in docs/.vitepress/theme/components/AnimatedSpriteDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected 1 line break before closing bracket, but no line breaks found
</Suspense>
</TresCanvas>
</template>
18 changes: 18 additions & 0 deletions docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import { AnimatedSprite } from '@tresjs/cientos'
</script>

<template>
<TresCanvas clear-color="#82DBC5">
<TresPerspectiveCamera :position="[0, 0, 5]"/>

Check warning on line 8 in docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected a space before '/>', but not found
<Suspense>
<AnimatedSprite
image="https://raw.githubusercontent.com/Tresjs/assets/6c0b087768a0a2b76148c99fc87d7e6ddc3c6d66/textures/animated-sprite/textureWithoutAtlas.png"
:atlas="16"
:animation="[0,15]"
:fps="10"
/>

Check warning on line 15 in docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue

View workflow job for this annotation

GitHub Actions / Lint (16)

Expected indentation of 6 spaces but found 8 spaces
</Suspense>
</TresCanvas>
</template>
131 changes: 131 additions & 0 deletions docs/guide/abstractions/animated-sprite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# AnimatedSprite

<DocsDemo>
<AnimatedSpriteDemo />
</DocsDemo>

`<AnimatedSprite />` allows you to use 2D animations defined in a [texture atlas](https://en.wikipedia.org/wiki/Texture_atlas). A typical `<AnimatedSprite />` will use:

* an image containing multiple sprites
* a JSON atlas containing the coordinates of the image

## Usage

<<< @/.vitepress/theme/components/AnimatedSpriteDemo.vue{3,11-16}

::: warning Suspense
`<AnimatedSprite />` loads resources asynchronously, so it must be wrapped in a `<Suspense />`.
:::

## Props

<CientosPropsTable
component-path="src/core/abstractions/AnimatedSprite/component.vue"
:fields="['name', 'description', 'default', 'required']"
:on-format-value="({valueFormatted, propName, fieldName, getFieldFormatted})=> {
if (fieldName === 'description') {
let type = getFieldFormatted('type')
if (type.indexOf('TSFunctionType') !== -1 && propName.startsWith('on')) {
type = '<code>(frameName:string) => void</code>'
}
return type + ' – ' + valueFormatted
}
}"
/>

## `animation`

The `animation` prop holds either the name of the currently playing animation or a range of frames to play, or a frame number to display.

### Named animations

Frames are automatically grouped into named animations, if you use either of the following naming conventions for your source images:

* `[key][frame number].[file_extension]`
* `[key]_[frame number].[file_extension]`

The `<AnimatedSprite />` will automatically make all [key] available for playback as named animations.

#### Example

Here are some example source image names, to be compiled into an image texture and atlas.

* heroIdle00.png
* heroIdle01.png
* heroIdle02.png
* heroRun00.png
* heroRun01.png
* heroRun02.png
* heroRun03.png
* heroHeal00.png
* heroHeal01.png

When the resulting image texture and atlas are provided to `<AnimatedSprite />`, "heroIdle", "heroRun", and "heroHeal" will be available as named animations. Animation names can be used as follows:

```vue{3}
<AnimatedSprite
atlas="..." image="..."
animation="heroRun"
/>
```

### Ranges

A `[number, number]` range can be supplied as the `animation` prop. The numbers correspond to the position of the frame in the `atlas` `frames` array, starting with `0`. The first `number` in the range represents the start frame of the animation. The last `number` represents the end frame.

### Single frame

To display a single animation frame, a `number` can be supplied as the `animation` prop. The `number` corresponds to the position of the frame in the `atlas` `frames` array, starting with `0`.

## `anchor`

The `anchor` allow you to control how differently sized source images "grow" and "shrink". Namely, they "grow out from" and "shrink towards" the anchor. `[0, 0]` places the anchor at the top left corner of the `<AnimatedSprite />`. `[1,1]` places the anchor at the bottom right corner. By default, the anchor is placed at `[0.5, 0.5]` i.e., the center.

Below is a simple animation containing differently sized source images. The anchor is visible at world position `0, 0, 0`.
<DocsDemo>
<AnimatedSpriteAnchorDemo />
</DocsDemo>

::: warning
Changing the anchor from the default can have unpredictable results if `asSprite` is `true`.
:::

## `definitions`

For each [named animation](#named-animations), you can supply a "definition" that specifies frame order and repeated frames (delays). For the [named animation example above](#named-animations), the `definitions` prop might look like this:

```vue
<AnimatedSprite
atlas="..." image="..."
:definitions="{
'heroIdle':'0(5),1-2', // repeat frame 0 five times, then play frames 1-2
'heroRun':'0-3,2-1', // play frames 0-3, then 2-1
// no "heroHeal" definition; the default frame order will be used: 0-1
}"
/>
```

## Compiling an atlas

In typical usage, `<AnimatedSprite/>` requires both the URL to a texture of compiled sprite images and a JSON atlas containing information about the sprites in the texture.

* [example compiled texture](https://raw.githubusercontent.com/Tresjs/assets/6c0b087768a0a2b76148c99fc87d7e6ddc3c6d66/textures/animated-sprite/namedAnimationsTexture.png)
* [example JSON atlas](https://raw.githubusercontent.com/Tresjs/assets/6c0b087768a0a2b76148c99fc87d7e6ddc3c6d66/textures/animated-sprite/namedAnimationsAtlas.json)

Compiling source images into a texture atlas is usually handled by third-party software. You may find [TexturePacker](https://www.codeandweb.com/texturepacker) useful.

## Without an atlas

There may be cases where you don't want to supply a generated JSON atlas as an `atlas` prop. This is possible if you compile your source images in a single row of equally sized columns *and* set the `atlas` prop to the number of columns.

### Example

This image is comprised of 16 source images, compiled into a single image, in a single row:

<img src="https://raw.githubusercontent.com/Tresjs/assets/6c0b087768a0a2b76148c99fc87d7e6ddc3c6d66/textures/animated-sprite/textureWithoutAtlas.png" />

<DocsDemo>
<AnimatedSpriteNoAtlasDemo />
</DocsDemo>

<<< @/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue{12,13}
Loading

0 comments on commit d6ea81b

Please sign in to comment.