From d6ea81b5540c2f4bf0f233e269ed98992ee8a551 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 28 Dec 2023 20:20:05 +0100 Subject: [PATCH 001/125] feat(AnimatedSprite): add AnimatedSprite, playground, docs --- docs/.vitepress/config.ts | 1 + .../components/AnimatedSpriteAnchorDemo.vue | 86 ++++++ .../theme/components/AnimatedSpriteDemo.vue | 18 ++ .../components/AnimatedSpriteNoAtlasDemo.vue | 18 ++ docs/guide/abstractions/animated-sprite.md | 131 +++++++++ .../src/pages/abstractions/AnimatedSprite.vue | 154 ++++++++++ playground/src/router/routes/abstractions.ts | 5 + src/core/abstractions/AnimatedSprite/Atlas.ts | 271 ++++++++++++++++++ .../AtlasAnimationDefinitionParser.ts | 218 ++++++++++++++ .../abstractions/AnimatedSprite/StringOps.ts | 14 + .../abstractions/AnimatedSprite/component.vue | 222 ++++++++++++++ src/core/abstractions/index.ts | 2 + 12 files changed, 1140 insertions(+) create mode 100644 docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue create mode 100644 docs/.vitepress/theme/components/AnimatedSpriteDemo.vue create mode 100644 docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue create mode 100644 docs/guide/abstractions/animated-sprite.md create mode 100644 playground/src/pages/abstractions/AnimatedSprite.vue create mode 100644 src/core/abstractions/AnimatedSprite/Atlas.ts create mode 100644 src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts create mode 100644 src/core/abstractions/AnimatedSprite/StringOps.ts create mode 100644 src/core/abstractions/AnimatedSprite/component.vue diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 67ed5361..20b3add0 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -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' }, ], }, { diff --git a/docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue b/docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue new file mode 100644 index 00000000..ca60d560 --- /dev/null +++ b/docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue @@ -0,0 +1,86 @@ + + + diff --git a/docs/.vitepress/theme/components/AnimatedSpriteDemo.vue b/docs/.vitepress/theme/components/AnimatedSpriteDemo.vue new file mode 100644 index 00000000..2c895e64 --- /dev/null +++ b/docs/.vitepress/theme/components/AnimatedSpriteDemo.vue @@ -0,0 +1,18 @@ + + + diff --git a/docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue b/docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue new file mode 100644 index 00000000..ae5ce344 --- /dev/null +++ b/docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue @@ -0,0 +1,18 @@ + + + diff --git a/docs/guide/abstractions/animated-sprite.md b/docs/guide/abstractions/animated-sprite.md new file mode 100644 index 00000000..f64f3dc1 --- /dev/null +++ b/docs/guide/abstractions/animated-sprite.md @@ -0,0 +1,131 @@ +# AnimatedSprite + + + + + +`` allows you to use 2D animations defined in a [texture atlas](https://en.wikipedia.org/wiki/Texture_atlas). A typical `` 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 +`` loads resources asynchronously, so it must be wrapped in a ``. +::: + +## Props + + + +## `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 `` 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 ``, "heroIdle", "heroRun", and "heroHeal" will be available as named animations. Animation names can be used as follows: + +```vue{3} + +``` + +### 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 ``. `[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`. + + + + +::: 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 + +``` + +## Compiling an atlas + +In typical usage, `` 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: + + + + + + + +<<< @/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue{12,13} \ No newline at end of file diff --git a/playground/src/pages/abstractions/AnimatedSprite.vue b/playground/src/pages/abstractions/AnimatedSprite.vue new file mode 100644 index 00000000..b67a9c74 --- /dev/null +++ b/playground/src/pages/abstractions/AnimatedSprite.vue @@ -0,0 +1,154 @@ + + + diff --git a/playground/src/router/routes/abstractions.ts b/playground/src/router/routes/abstractions.ts index d880700c..5e900b5e 100644 --- a/playground/src/router/routes/abstractions.ts +++ b/playground/src/router/routes/abstractions.ts @@ -49,4 +49,9 @@ export const abstractionsRoutes = [ name: 'Sampler', component: () => import('../../pages/abstractions/Sampler.vue'), }, + { + path: '/abstractions/animated-sprite', + name: 'AnimatedSprite', + component: () => import('../../pages/abstractions/AnimatedSprite.vue'), + }, ] diff --git a/src/core/abstractions/AnimatedSprite/Atlas.ts b/src/core/abstractions/AnimatedSprite/Atlas.ts new file mode 100644 index 00000000..1b122d6c --- /dev/null +++ b/src/core/abstractions/AnimatedSprite/Atlas.ts @@ -0,0 +1,271 @@ +import type { Texture } from "three"; +import { TextureLoader } from "three"; +import { useLoader, useLogger } from "@tresjs/core"; +import { getNumbersFromEnd, stripUnderscoresNumbersFromEnd } from "./StringOps"; +import { expand } from "./AtlasAnimationDefinitionParser"; + +export interface AtlasFrame { + name: string; + width: number; + height: number; + offsetX: number; + offsetY: number; + repeatX: number; + repeatY: number; +} + +export interface AtlasPage { + frames: AtlasFrame[]; + namedFrames: Record; + texture: Texture; +} + +export async function getAtlasPageAsync( + atlas: string | number | string[] | TexturePackerFrameDataArray | TexturePackerFrameDataObject, + image: string, + definitions?: Record +): Promise { + const texturePromise = useLoader(TextureLoader, image); + const atlasPromise = + typeof atlas === "string" + ? fetch(atlas) + .then((response) => response.json()) + .catch((e) => useLogger().logError("Cientos Atlas - " + e)) + : new Promise((resolve) => resolve(atlas)); + + const pagePromise = Promise.all([atlasPromise, texturePromise]).then( + (response) => { + const texture: Texture = response[1]; + const processingFn = (() => { + if (typeof atlas === "string" || atlas.hasOwnProperty("frames")) { + return framesFromTexturePackerData; + } else if (typeof atlas === "number") { + return framesFromNumColsWidthHeight; + } else { + return framesFromAnimationNamesWidthHeight; + } + })(); + const frames = processingFn( + response[0], + texture.image.width, + texture.image.height + ); + const namedFrames = groupFramesByKey(frames); + texture.matrixAutoUpdate = false; + const page: AtlasPage = { frames, namedFrames, texture }; + if (definitions) { + setDefinitions(page, definitions); + } + return page; + } + ); + + return pagePromise; +} + +export type TexturePackerFrameData = { + filename: string; + frame: { x: number; y: number; w: number; h: number }; +}; + +export type TexturePackerFrameDataArray = { + frames: TexturePackerFrameData[]; +}; + +export type TexturePackerFrameDataObject = { + frames: Record; +}; + +function framesFromTexturePackerData( + data: TexturePackerFrameDataArray | TexturePackerFrameDataObject, + width: number, + height: number +) { + return Array.isArray(data.frames) + ? framesFromTexturePackerDataArray( + data as TexturePackerFrameDataArray, + width, + height + ) + : framesFromTexturePackerDataObject( + data as TexturePackerFrameDataObject, + width, + height + ); +} + +function framesFromTexturePackerDataArray( + data: TexturePackerFrameDataArray, + width: number, + height: number +): AtlasFrame[] { + const invWidth = 1 / width; + const invHeight = 1 / height; + return data.frames.map((d) => ({ + name: d.filename, + offsetX: d.frame.x * invWidth, + offsetY: 1 - (d.frame.y + d.frame.h) * invHeight, + repeatX: d.frame.w * invWidth, + repeatY: d.frame.h * invHeight, + width: d.frame.w, + height: d.frame.h, + })); +} + +function framesFromTexturePackerDataObject( + data: TexturePackerFrameDataObject, + width: number, + height: number +): AtlasFrame[] { + const invWidth = 1 / width; + const invHeight = 1 / height; + return Object.entries(data.frames).map(([k, v]) => ({ + name: k, + offsetX: v.frame.x * invWidth, + offsetY: 1 - (v.frame.y + v.frame.h) * invHeight, + repeatX: v.frame.w * invWidth, + repeatY: v.frame.h * invHeight, + width: v.frame.w, + height: v.frame.h, + })); +} + +function framesFromNumColsWidthHeight( + numCols: number, + width: number, + height: number, + name = "default" +): AtlasFrame[] { + const frameWidth = width / numCols; + const invWidth = 1 / width; + const padAmount = numCols.toString().length; + return new Array(numCols).fill(0).map((_, i) => ({ + name: name + String(i).padStart(padAmount, "0"), + offsetX: i * frameWidth * invWidth, + offsetY: 0, + repeatX: 1 / numCols, + repeatY: 1, + width: width / numCols, + height, + })); +} + +function framesFromAnimationNamesWidthHeight( + animationNames: string[], + width: number, + height: number +): AtlasFrame[] { + const numCols = animationNames.length; + const frames = framesFromNumColsWidthHeight(numCols, width, height); + const padAmount = numCols.toString().length; + animationNames.forEach((name, i) => { + frames[i].name = name + "_" + String(i).padStart(padAmount, "0"); + }); + return frames; +} + +function setDefinitions(page: AtlasPage, definitions: Record) { + for (const [animationName, definitionStr] of Object.entries(definitions)) { + const frames: AtlasFrame[] = getFrames(page, animationName); + const expanded = expand(definitionStr); + for (const i of expanded) { + if (i < 0 || frames.length <= i) { + useLogger().logError( + `Cientos Atlas: Attempting to access frame index ${i} in animation ${animationName}, but it does not exist.` + ); + } + } + page.namedFrames[animationName] = expanded.map((i) => frames[i]); + } +} + +export function getFrames( + page: AtlasPage, + animationNameOrFrameNumber: string | number | [number, number] +): AtlasFrame[] { + if (typeof animationNameOrFrameNumber === "string") + return getFramesByName(page, animationNameOrFrameNumber); + else if (typeof animationNameOrFrameNumber === "number") + return getFramesByIndices( + page, + animationNameOrFrameNumber, + animationNameOrFrameNumber + ); + else { + return getFramesByIndices( + page, + animationNameOrFrameNumber[0], + animationNameOrFrameNumber[1] + ); + } +} + +function getFramesByName(page: AtlasPage, name: string): AtlasFrame[] { + if (!(name in page.namedFrames)) { + useLogger().logError( + `Cientos Atlas: getFramesByName – name ${name} does not exist in page. Available names: ${Object.keys( + page + )}` + ); + } + return page.namedFrames[name]; +} + +function getFramesByIndices( + page: AtlasPage, + startI: number, + endI: number +): AtlasFrame[] { + if ( + startI < 0 || + page.frames.length <= startI || + endI < 0 || + page.frames.length <= endI + ) { + useLogger().logError( + `Cientos Atlas: getFramesByIndex – [${startI}, ${endI}] is out of bounds.` + ); + } + const result = []; + const sign = Math.sign(endI - startI); + if (sign === 0) return [page.frames[startI]]; + for (let i = startI; i !== endI + sign; i += sign) { + result.push(page.frames[i]); + } + return result; +} + +/** + * @returns An object where all AtlasFrames with the same key are grouped in an ordered array by name in ascending value. + * A key is defined as an alphanumeric string preceding a trailing numeric string. + * E.g.: + * "hero0Idle" has no key as it does not have trailing numeric string. + * "heroIdle0" has the key "heroIdle". + * @example ``` + * groupFramesByKey([{name: hero, ...}, {name: heroJump3, ...}, {name: heroJump0, ...}, {name: heroIdle0, ...}, {name: heroIdle1, ...}]) returns + * { + * heroJump: [{name: heroJump0, ...}, {name: heroJump3, ...}], + * heroIdle: [{name: heroIdle0, ...}, {name: heroIdle1, ...}] + * } + * ``` + */ +function groupFramesByKey(frames: AtlasFrame[]): Record { + const result: Record = {}; + + for (const frame of frames) { + if (getNumbersFromEnd(frame.name) !== null) { + const key = stripUnderscoresNumbersFromEnd(frame.name); + if (result.hasOwnProperty(key)) { + result[key].push(frame); + } else { + result[key] = [frame]; + } + } + } + + for (const entry of Object.values(result)) { + entry.sort((a, b) => a.name.localeCompare(b.name)); + } + + return result; +} diff --git a/src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts b/src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts new file mode 100644 index 00000000..f6d499bf --- /dev/null +++ b/src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts @@ -0,0 +1,218 @@ +import { useLogger } from "@tresjs/core"; + +/** + * Expand an animation definition string into an array of numbers. + * @param definitionStr - A comma-separated string of frame numbers with optional parentheses-surrounded durations. + * @example - expand("0,2") === [0,2] + * @example - expand("2(10)") === [2,2,2,2,2,2,2,2,2,2] + * @example - expand("1-4") === [1,2,3,4] + * @example - expand("10-5(2)") === [10,10,9,9,8,8,7,7,6,6,5,5] + * @example - expand("1-4(3),10(2)") === [1,1,1,2,2,2,3,3,3,4,4,4,10,10] + */ + +export function expand(definitionStr: string) : number[] { + const parsed = parse(definitionStr) + const expanded: number[] = []; + for (const info of parsed) { + if (info.duration <= 0) { + } else if (info.endFrame < 0 || info.startFrame === info.endFrame) { + for (let _ = 0; _ < info.duration; _++) { + expanded.push(info.startFrame); + } + continue + } else { + const sign = Math.sign(info.endFrame - info.startFrame); + for ( + let frame = info.startFrame; + frame !== info.endFrame + sign; + frame += sign + ) { + for (let _ = 0; _ < info.duration; _++) { + expanded.push(frame); + } + } + } + } + return expanded +} + +type AnimationDefinition = { + startFrame: number, + endFrame: number, + duration: number +} + +/** + * Parse an animation defintion string into an array of AnimationDefinition. + * @param definitionStr - A comma-separated string of frame numbers with optional parentheses-surrounded durations. + * @example - parse("0,2") === [{startFrame:0, endFrame:0, delay:1}, {startFrame:2, endFrame:2, delay:1}] + * @example - parse("2(10)") === [{startFrame:2, endFrame:2, delay:10}] + * @example - parse("1-4") === [{startFrame:1, endFrame:4, delay:1}] + * @example - parse("10-5(2)") === [{startFrame:10, endFrame:5, delay:2}] + * @example - parse("1-4(3),10(2)") === [{startFrame:1, endFrame:4, delay:3}, {startFrame:10, endFrame:10, delay:2}] + */ + +export function parse(definitionStr: string) : AnimationDefinition[] { + let transition: Transition = "START_FRAME_IN"; + const parsed: AnimationDefinition[] = []; + for (const token of tokenize(definitionStr)) { + if (transition === "START_FRAME_IN") { + if (token.name === "NUMBER") { + parsed.push({ + startFrame: token.value, + endFrame: token.value, + duration: 1, + }); + transition = "START_FRAME_OUT"; + } else { + warnDefinitionSyntaxError( + "number", + token.name, + definitionStr, + token.startI + ); + } + } else if (transition === "START_FRAME_OUT") { + if (token.name === "COMMA") { + transition = "START_FRAME_IN"; + } else if (token.name === "HYPHEN") { + transition = "END_FRAME_IN"; + } else if (token.name === "OPEN_PAREN") { + transition = "DURATION_IN"; + } else { + warnDefinitionSyntaxError( + '",", "-", "("', + token.name, + definitionStr, + token.startI + ); + } + } else if (transition === "END_FRAME_IN") { + if (token.name === "NUMBER") { + parsed[parsed.length - 1].endFrame = token.value; + transition = "END_FRAME_OUT"; + } else { + warnDefinitionSyntaxError( + "number", + token.name, + definitionStr, + token.startI + ); + } + } else if (transition === "END_FRAME_OUT") { + if (token.name === "COMMA") { + transition = "START_FRAME_IN"; + } else if (token.name === "OPEN_PAREN") { + transition = "DURATION_IN"; + } else { + warnDefinitionSyntaxError( + "',' or '('", + token.name, + definitionStr, + token.startI + ); + } + } else if (transition === "DURATION_IN") { + if (token.name === "NUMBER") { + parsed[parsed.length - 1].duration = token.value; + transition = "DURATION_OUT"; + } else { + warnDefinitionSyntaxError( + "number", + token.name, + definitionStr, + token.startI + ); + } + } else if (transition === "DURATION_OUT") { + if (token.name === "CLOSE_PAREN") { + transition = "NEXT_OR_DONE"; + } else { + warnDefinitionSyntaxError('"("', token.name, definitionStr, token.startI); + } + } else if (transition === "NEXT_OR_DONE") { + if (token.name === "COMMA") { + transition = "START_FRAME_IN"; + } else { + warnDefinitionSyntaxError('","', token.name, definitionStr, token.startI); + } + } + } + + return parsed; +} + +type Transition = + | "START_FRAME_IN" + | "START_FRAME_OUT" + | "END_FRAME_IN" + | "END_FRAME_OUT" + | "DURATION_IN" + | "DURATION_OUT" + | "NEXT_OR_DONE"; + +type TokenName = "COMMA" | "HYPHEN" | "OPEN_PAREN" | "CLOSE_PAREN" | "NUMBER"; +interface Token { + name: TokenName; + value: number; + startI: number; +} + +function tokenize(definition: string): Token[] { + const tokenized: Token[] = []; + let ii = 0; + while (ii < definition.length) { + const c = definition[ii]; + if ("0123456789".indexOf(c) > -1) { + if ( + tokenized.length && + tokenized[tokenized.length - 1].name === "NUMBER" + ) { + tokenized[tokenized.length - 1].value *= 10; + tokenized[tokenized.length - 1].value += parseInt(c); + } else { + tokenized.push({ name: "NUMBER", value: parseInt(c), startI: ii }); + } + } else if (c === " ") { + } else if (c === ",") { + tokenized.push({ name: "COMMA", value: -1, startI: ii }); + } else if (c === "(") { + tokenized.push({ name: "OPEN_PAREN", value: -1, startI: ii }); + } else if (c === ")") { + tokenized.push({ name: "CLOSE_PAREN", value: -1, startI: ii }); + } else if (c === "-") { + tokenized.push({ name: "HYPHEN", value: -1, startI: ii }); + } else { + warnDefinitionBadCharacter("0123456789,-()", c, definition, ii); + } + ii++; + } + + return tokenized; +} + +function warnDefinitionBadCharacter( + expected: string, + found: string, + definition: string, + index: number +) { + useLogger().logError( + `Cientos AnimationDefinitionParser: Unexpected character while processing animation definition: expected ${expected}, got ${found}. +${definition} +${Array(index + 1).join(" ")}^` + ); +} + +function warnDefinitionSyntaxError( + expected: string, + found: string, + definition: string, + index: number +) { + useLogger().logError( + `Cientos AnimationDefinitionParser: Syntax error while processing animation definition: expected ${expected}, got ${found}. +${definition} +${Array(index + 1).join(" ")}^` + ); +} diff --git a/src/core/abstractions/AnimatedSprite/StringOps.ts b/src/core/abstractions/AnimatedSprite/StringOps.ts new file mode 100644 index 00000000..46d63d65 --- /dev/null +++ b/src/core/abstractions/AnimatedSprite/StringOps.ts @@ -0,0 +1,14 @@ +const numbersAtEnd = /[0-9]*$/; +const underscoresNumbersAtEnd = /_*[0-9]*$/; + +export function stripUnderscoresNumbersFromEnd(str: string) { + return str.replace(underscoresNumbersAtEnd, ""); +} + +export function getNumbersFromEnd(str: string) { + const matches = str.match(numbersAtEnd); + if (matches) { + return parseInt(matches[matches.length - 1]); + } + return null; +} \ No newline at end of file diff --git a/src/core/abstractions/AnimatedSprite/component.vue b/src/core/abstractions/AnimatedSprite/component.vue new file mode 100644 index 00000000..9f783782 --- /dev/null +++ b/src/core/abstractions/AnimatedSprite/component.vue @@ -0,0 +1,222 @@ + + + diff --git a/src/core/abstractions/index.ts b/src/core/abstractions/index.ts index 7fea5242..3baa545c 100644 --- a/src/core/abstractions/index.ts +++ b/src/core/abstractions/index.ts @@ -7,6 +7,7 @@ import { GlobalAudio } from './GlobalAudio' import Lensflare from './Lensflare/component.vue' import Fbo from './useFBO/component.vue' import Sampler from './useSurfaceSampler/component.vue' +import AnimatedSprite from './AnimatedSprite/component.vue' export * from './useFBO/' export * from './useSurfaceSampler' @@ -21,4 +22,5 @@ export { GlobalAudio, Fbo, Sampler, + AnimatedSprite, } From e7ff5a283d10bce293f8427d16f73f419ac80ace Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 11 Jan 2024 23:27:43 +0100 Subject: [PATCH 002/125] chore(AnimatedSprite): fix linter errors --- .../components/AnimatedSpriteAnchorDemo.vue | 50 +++- .../theme/components/AnimatedSpriteDemo.vue | 11 +- .../components/AnimatedSpriteNoAtlasDemo.vue | 8 +- docs/guide/abstractions/animated-sprite.md | 2 +- .../src/pages/abstractions/AnimatedSprite.vue | 49 ++-- src/core/abstractions/AnimatedSprite/Atlas.ts | 237 +++++++-------- .../AtlasAnimationDefinitionParser.ts | 277 ++++++++++-------- .../abstractions/AnimatedSprite/StringOps.ts | 12 +- .../abstractions/AnimatedSprite/component.vue | 70 +++-- 9 files changed, 406 insertions(+), 310 deletions(-) diff --git a/docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue b/docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue index ca60d560..dc6846ab 100644 --- a/docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue +++ b/docs/.vitepress/theme/components/AnimatedSpriteAnchorDemo.vue @@ -3,18 +3,18 @@ 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' +import type { TexturePackerFrameDataArray } from '../../../../src/core/abstractions/AnimatedSprite/Atlas' 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} + fps: { value: 5, min: 0, max: 30, step: 1 }, }) const anchorDemoAtlas: TexturePackerFrameDataArray = { frames: [] } const anchorDemoImgData = (() => { const NUM_ROWS_COLS = 64 - const rects: { x: number, y: number, w: number, h: number }[] = [] + const rects: { x: number; y: number; w: number; h: number }[] = [] let h = 0 for (let r = 0; r < NUM_ROWS_COLS; r += h) { let w = 0 @@ -43,12 +43,16 @@ const anchorDemoImgData = (() => { 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` + 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 + 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) @@ -70,15 +74,35 @@ const anchorDemoImgData = (() => { diff --git a/docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue b/docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue index ae5ce344..cec72cea 100644 --- a/docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue +++ b/docs/.vitepress/theme/components/AnimatedSpriteNoAtlasDemo.vue @@ -5,14 +5,14 @@ import { AnimatedSprite } from '@tresjs/cientos' diff --git a/docs/guide/abstractions/animated-sprite.md b/docs/guide/abstractions/animated-sprite.md index f64f3dc1..b64ef5cc 100644 --- a/docs/guide/abstractions/animated-sprite.md +++ b/docs/guide/abstractions/animated-sprite.md @@ -11,7 +11,7 @@ ## Usage -<<< @/.vitepress/theme/components/AnimatedSpriteDemo.vue{3,11-16} +<<< @/.vitepress/theme/components/AnimatedSpriteDemo.vue{3,11-19} ::: warning Suspense `` loads resources asynchronously, so it must be wrapped in a ``. diff --git a/playground/src/pages/abstractions/AnimatedSprite.vue b/playground/src/pages/abstractions/AnimatedSprite.vue index b67a9c74..58915702 100644 --- a/playground/src/pages/abstractions/AnimatedSprite.vue +++ b/playground/src/pages/abstractions/AnimatedSprite.vue @@ -5,7 +5,7 @@ import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from 'three' import { shallowReactive } from 'vue' import { TresLeches, useControls } from '@tresjs/leches' import '@tresjs/leches/styles' -import { TexturePackerFrameDataArray } from '../../../../src/core/abstractions/AnimatedSprite/Atlas' +import type { TexturePackerFrameDataArray } from '../../../../src/core/abstractions/AnimatedSprite/Atlas' const gl = { clearColor: '#82DBC5', @@ -18,19 +18,19 @@ const gl = { const animationState = shallowReactive({ fps: 10, - animation: "yes", + animation: 'yes', flipX: false, asSprite: false, loop: true, reversed: false, resetOnEnd: false, anchorX: 0.5, - anchorY: 0.5 + anchorY: 0.5, }) const { fps, animation, flipX, asSprite, loop, reversed, resetOnEnd, anchorX, anchorY } = useControls({ fps: { value: animationState.fps, min: 0, max: 120, step: 1 }, - animation: { label: "Suzanne animation", value: animationState.animation, options: ["yes", "no"] }, + animation: { label: 'Suzanne animation', value: animationState.animation, options: ['yes', 'no'] }, flipX: animationState.flipX, asSprite: animationState.asSprite, loop: animationState.loop, @@ -43,7 +43,7 @@ const { fps, animation, flipX, asSprite, loop, reversed, resetOnEnd, anchorX, an const anchorDemoAtlas: TexturePackerFrameDataArray = { frames: [] } const anchorDemoImgData = (() => { const NUM_ROWS_COLS = 32 - const rects: { x: number, y: number, w: number, h: number }[] = [] + const rects: { x: number; y: number; w: number; h: number }[] = [] let h = 1 for (let r = 0; r < NUM_ROWS_COLS; r += h) { let w = 1 @@ -77,18 +77,22 @@ const anchorDemoImgData = (() => { 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 }) + anchorDemoAtlas.frames.push({ filename: `rect_${i.toString().padStart(4, '0')}`, frame }) ctx.fillStyle = `hsl(${360 * i / rects.length}, 100%, 50%)` ctx.fillRect(x, y, w, h) ctx.fillStyle = 'rgba(0, 0, 0, 0.5)' - ctx.fillRect(x + w * 0.5 - CENTER_ANCHOR_SIZE * 0.5, y + h * 0.5 - CENTER_ANCHOR_SIZE * 0.5, CENTER_ANCHOR_SIZE, CENTER_ANCHOR_SIZE) + ctx.fillRect( + x + w * 0.5 - CENTER_ANCHOR_SIZE * 0.5, + y + h * 0.5 - CENTER_ANCHOR_SIZE * 0.5, + CENTER_ANCHOR_SIZE, + CENTER_ANCHOR_SIZE) ctx.fillStyle = '#FFF' ctx.textAlign = 'center' ctx.font = '12px monospace' ctx.textBaseline = 'middle' - ctx.fillText('Frame ' + i, x + w * 0.5, y + h * 0.5) + ctx.fillText(`Frame ${i}`, x + w * 0.5, y + h * 0.5) ctx.fillStyle = '#FFF' ctx.fillRect(x, y, EDGE_ANCHOR_SIZE, EDGE_ANCHOR_SIZE) @@ -115,15 +119,24 @@ const anchorDemoImgData = (() => { - + { :anchor="[anchorX.value, anchorY.value]" :as-sprite="asSprite.value" :reversed="reversed.value" - /> + /> { :anchor="[anchorX.value, anchorY.value]" :as-sprite="asSprite.value" :reversed="reversed.value" - animation="heart" - /> + /> diff --git a/src/core/abstractions/AnimatedSprite/Atlas.ts b/src/core/abstractions/AnimatedSprite/Atlas.ts index 1b122d6c..8431971a 100644 --- a/src/core/abstractions/AnimatedSprite/Atlas.ts +++ b/src/core/abstractions/AnimatedSprite/Atlas.ts @@ -1,107 +1,109 @@ -import type { Texture } from "three"; -import { TextureLoader } from "three"; -import { useLoader, useLogger } from "@tresjs/core"; -import { getNumbersFromEnd, stripUnderscoresNumbersFromEnd } from "./StringOps"; -import { expand } from "./AtlasAnimationDefinitionParser"; +import type { Texture } from 'three' +import { TextureLoader } from 'three' +import { useLoader, useLogger } from '@tresjs/core' +import { getNumbersFromEnd, stripUnderscoresNumbersFromEnd } from './StringOps' +import { expand } from './AtlasAnimationDefinitionParser' export interface AtlasFrame { - name: string; - width: number; - height: number; - offsetX: number; - offsetY: number; - repeatX: number; - repeatY: number; + name: string + width: number + height: number + offsetX: number + offsetY: number + repeatX: number + repeatY: number } export interface AtlasPage { - frames: AtlasFrame[]; - namedFrames: Record; - texture: Texture; + frames: AtlasFrame[] + namedFrames: Record + texture: Texture } export async function getAtlasPageAsync( atlas: string | number | string[] | TexturePackerFrameDataArray | TexturePackerFrameDataObject, image: string, - definitions?: Record + definitions?: Record, ): Promise { - const texturePromise = useLoader(TextureLoader, image); - const atlasPromise = - typeof atlas === "string" + const texturePromise = useLoader(TextureLoader, image) + const atlasPromise + = typeof atlas === 'string' ? fetch(atlas) - .then((response) => response.json()) - .catch((e) => useLogger().logError("Cientos Atlas - " + e)) - : new Promise((resolve) => resolve(atlas)); + .then(response => response.json()) + .catch(e => useLogger().logError(`Cientos Atlas - ${e}`)) + : new Promise(resolve => resolve(atlas)) const pagePromise = Promise.all([atlasPromise, texturePromise]).then( (response) => { - const texture: Texture = response[1]; + const texture: Texture = response[1] const processingFn = (() => { - if (typeof atlas === "string" || atlas.hasOwnProperty("frames")) { - return framesFromTexturePackerData; - } else if (typeof atlas === "number") { - return framesFromNumColsWidthHeight; - } else { - return framesFromAnimationNamesWidthHeight; + if (typeof atlas === 'string' || atlas.hasOwnProperty('frames')) { + return framesFromTexturePackerData + } + else if (typeof atlas === 'number') { + return framesFromNumColsWidthHeight + } + else { + return framesFromAnimationNamesWidthHeight } - })(); + })() const frames = processingFn( response[0], texture.image.width, - texture.image.height - ); - const namedFrames = groupFramesByKey(frames); - texture.matrixAutoUpdate = false; - const page: AtlasPage = { frames, namedFrames, texture }; + texture.image.height, + ) + const namedFrames = groupFramesByKey(frames) + texture.matrixAutoUpdate = false + const page: AtlasPage = { frames, namedFrames, texture } if (definitions) { - setDefinitions(page, definitions); + setDefinitions(page, definitions) } - return page; - } - ); + return page + }, + ) - return pagePromise; + return pagePromise } -export type TexturePackerFrameData = { - filename: string; - frame: { x: number; y: number; w: number; h: number }; -}; +export interface TexturePackerFrameData { + filename: string + frame: { x: number; y: number; w: number; h: number } +} -export type TexturePackerFrameDataArray = { - frames: TexturePackerFrameData[]; -}; +export interface TexturePackerFrameDataArray { + frames: TexturePackerFrameData[] +} -export type TexturePackerFrameDataObject = { - frames: Record; -}; +export interface TexturePackerFrameDataObject { + frames: Record +} function framesFromTexturePackerData( data: TexturePackerFrameDataArray | TexturePackerFrameDataObject, width: number, - height: number + height: number, ) { return Array.isArray(data.frames) ? framesFromTexturePackerDataArray( - data as TexturePackerFrameDataArray, - width, - height - ) + data as TexturePackerFrameDataArray, + width, + height, + ) : framesFromTexturePackerDataObject( - data as TexturePackerFrameDataObject, - width, - height - ); + data as TexturePackerFrameDataObject, + width, + height, + ) } function framesFromTexturePackerDataArray( data: TexturePackerFrameDataArray, width: number, - height: number + height: number, ): AtlasFrame[] { - const invWidth = 1 / width; - const invHeight = 1 / height; - return data.frames.map((d) => ({ + const invWidth = 1 / width + const invHeight = 1 / height + return data.frames.map(d => ({ name: d.filename, offsetX: d.frame.x * invWidth, offsetY: 1 - (d.frame.y + d.frame.h) * invHeight, @@ -109,16 +111,16 @@ function framesFromTexturePackerDataArray( repeatY: d.frame.h * invHeight, width: d.frame.w, height: d.frame.h, - })); + })) } function framesFromTexturePackerDataObject( data: TexturePackerFrameDataObject, width: number, - height: number + height: number, ): AtlasFrame[] { - const invWidth = 1 / width; - const invHeight = 1 / height; + const invWidth = 1 / width + const invHeight = 1 / height return Object.entries(data.frames).map(([k, v]) => ({ name: k, offsetX: v.frame.x * invWidth, @@ -127,76 +129,76 @@ function framesFromTexturePackerDataObject( repeatY: v.frame.h * invHeight, width: v.frame.w, height: v.frame.h, - })); + })) } function framesFromNumColsWidthHeight( numCols: number, width: number, height: number, - name = "default" + name = 'default', ): AtlasFrame[] { - const frameWidth = width / numCols; - const invWidth = 1 / width; - const padAmount = numCols.toString().length; + const frameWidth = width / numCols + const invWidth = 1 / width + const padAmount = numCols.toString().length return new Array(numCols).fill(0).map((_, i) => ({ - name: name + String(i).padStart(padAmount, "0"), + name: name + String(i).padStart(padAmount, '0'), offsetX: i * frameWidth * invWidth, offsetY: 0, repeatX: 1 / numCols, repeatY: 1, width: width / numCols, height, - })); + })) } function framesFromAnimationNamesWidthHeight( animationNames: string[], width: number, - height: number + height: number, ): AtlasFrame[] { - const numCols = animationNames.length; - const frames = framesFromNumColsWidthHeight(numCols, width, height); - const padAmount = numCols.toString().length; + const numCols = animationNames.length + const frames = framesFromNumColsWidthHeight(numCols, width, height) + const padAmount = numCols.toString().length animationNames.forEach((name, i) => { - frames[i].name = name + "_" + String(i).padStart(padAmount, "0"); - }); - return frames; + frames[i].name = `${name}_${String(i).padStart(padAmount, '0')}` + }) + return frames } function setDefinitions(page: AtlasPage, definitions: Record) { for (const [animationName, definitionStr] of Object.entries(definitions)) { - const frames: AtlasFrame[] = getFrames(page, animationName); - const expanded = expand(definitionStr); + const frames: AtlasFrame[] = getFrames(page, animationName) + const expanded = expand(definitionStr) for (const i of expanded) { if (i < 0 || frames.length <= i) { useLogger().logError( - `Cientos Atlas: Attempting to access frame index ${i} in animation ${animationName}, but it does not exist.` - ); + `Cientos Atlas: Attempting to access frame index ${i} in animation ${animationName}, but it does not exist.`, + ) } } - page.namedFrames[animationName] = expanded.map((i) => frames[i]); + page.namedFrames[animationName] = expanded.map(i => frames[i]) } } export function getFrames( page: AtlasPage, - animationNameOrFrameNumber: string | number | [number, number] + animationNameOrFrameNumber: string | number | [number, number], ): AtlasFrame[] { - if (typeof animationNameOrFrameNumber === "string") - return getFramesByName(page, animationNameOrFrameNumber); - else if (typeof animationNameOrFrameNumber === "number") + if (typeof animationNameOrFrameNumber === 'string') + return getFramesByName(page, animationNameOrFrameNumber) + else if (typeof animationNameOrFrameNumber === 'number') return getFramesByIndices( page, animationNameOrFrameNumber, - animationNameOrFrameNumber - ); + animationNameOrFrameNumber, + ) else { return getFramesByIndices( page, animationNameOrFrameNumber[0], - animationNameOrFrameNumber[1] - ); + animationNameOrFrameNumber[1], + ) } } @@ -204,35 +206,35 @@ function getFramesByName(page: AtlasPage, name: string): AtlasFrame[] { if (!(name in page.namedFrames)) { useLogger().logError( `Cientos Atlas: getFramesByName – name ${name} does not exist in page. Available names: ${Object.keys( - page - )}` - ); + page, + )}`, + ) } - return page.namedFrames[name]; + return page.namedFrames[name] } function getFramesByIndices( page: AtlasPage, startI: number, - endI: number + endI: number, ): AtlasFrame[] { if ( - startI < 0 || - page.frames.length <= startI || - endI < 0 || - page.frames.length <= endI + startI < 0 + || page.frames.length <= startI + || endI < 0 + || page.frames.length <= endI ) { useLogger().logError( - `Cientos Atlas: getFramesByIndex – [${startI}, ${endI}] is out of bounds.` - ); + `Cientos Atlas: getFramesByIndex – [${startI}, ${endI}] is out of bounds.`, + ) } - const result = []; - const sign = Math.sign(endI - startI); - if (sign === 0) return [page.frames[startI]]; + const result = [] + const sign = Math.sign(endI - startI) + if (sign === 0) return [page.frames[startI]] for (let i = startI; i !== endI + sign; i += sign) { - result.push(page.frames[i]); + result.push(page.frames[i]) } - return result; + return result } /** @@ -250,22 +252,23 @@ function getFramesByIndices( * ``` */ function groupFramesByKey(frames: AtlasFrame[]): Record { - const result: Record = {}; + const result: Record = {} for (const frame of frames) { if (getNumbersFromEnd(frame.name) !== null) { - const key = stripUnderscoresNumbersFromEnd(frame.name); + const key = stripUnderscoresNumbersFromEnd(frame.name) if (result.hasOwnProperty(key)) { - result[key].push(frame); - } else { - result[key] = [frame]; + result[key].push(frame) + } + else { + result[key] = [frame] } } } for (const entry of Object.values(result)) { - entry.sort((a, b) => a.name.localeCompare(b.name)); + entry.sort((a, b) => a.name.localeCompare(b.name)) } - return result; + return result } diff --git a/src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts b/src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts index f6d499bf..8990ce75 100644 --- a/src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts +++ b/src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts @@ -1,4 +1,4 @@ -import { useLogger } from "@tresjs/core"; +import { useLogger } from '@tresjs/core' /** * Expand an animation definition string into an array of numbers. @@ -10,35 +10,37 @@ import { useLogger } from "@tresjs/core"; * @example - expand("1-4(3),10(2)") === [1,1,1,2,2,2,3,3,3,4,4,4,10,10] */ -export function expand(definitionStr: string) : number[] { +export function expand(definitionStr: string): number[] { const parsed = parse(definitionStr) - const expanded: number[] = []; + const expanded: number[] = [] for (const info of parsed) { if (info.duration <= 0) { - } else if (info.endFrame < 0 || info.startFrame === info.endFrame) { + } + else if (info.endFrame < 0 || info.startFrame === info.endFrame) { + for (let _ = 0; _ < info.duration; _++) { + expanded.push(info.startFrame) + } + continue + } + else { + const sign = Math.sign(info.endFrame - info.startFrame) + for ( + let frame = info.startFrame; + frame !== info.endFrame + sign; + frame += sign + ) { for (let _ = 0; _ < info.duration; _++) { - expanded.push(info.startFrame); - } - continue - } else { - const sign = Math.sign(info.endFrame - info.startFrame); - for ( - let frame = info.startFrame; - frame !== info.endFrame + sign; - frame += sign - ) { - for (let _ = 0; _ < info.duration; _++) { - expanded.push(frame); - } + expanded.push(frame) } + } } } return expanded } -type AnimationDefinition = { - startFrame: number, - endFrame: number, +interface AnimationDefinition { + startFrame: number + endFrame: number duration: number } @@ -52,167 +54,192 @@ type AnimationDefinition = { * @example - parse("1-4(3),10(2)") === [{startFrame:1, endFrame:4, delay:3}, {startFrame:10, endFrame:10, delay:2}] */ -export function parse(definitionStr: string) : AnimationDefinition[] { - let transition: Transition = "START_FRAME_IN"; - const parsed: AnimationDefinition[] = []; +export function parse(definitionStr: string): AnimationDefinition[] { + let transition: Transition = 'START_FRAME_IN' + const parsed: AnimationDefinition[] = [] for (const token of tokenize(definitionStr)) { - if (transition === "START_FRAME_IN") { - if (token.name === "NUMBER") { + if (transition === 'START_FRAME_IN') { + if (token.name === 'NUMBER') { parsed.push({ startFrame: token.value, endFrame: token.value, duration: 1, - }); - transition = "START_FRAME_OUT"; - } else { + }) + transition = 'START_FRAME_OUT' + } + else { warnDefinitionSyntaxError( - "number", + 'number', token.name, definitionStr, - token.startI - ); - } - } else if (transition === "START_FRAME_OUT") { - if (token.name === "COMMA") { - transition = "START_FRAME_IN"; - } else if (token.name === "HYPHEN") { - transition = "END_FRAME_IN"; - } else if (token.name === "OPEN_PAREN") { - transition = "DURATION_IN"; - } else { + token.startI, + ) + } + } + else if (transition === 'START_FRAME_OUT') { + if (token.name === 'COMMA') { + transition = 'START_FRAME_IN' + } + else if (token.name === 'HYPHEN') { + transition = 'END_FRAME_IN' + } + else if (token.name === 'OPEN_PAREN') { + transition = 'DURATION_IN' + } + else { warnDefinitionSyntaxError( '",", "-", "("', token.name, definitionStr, - token.startI - ); - } - } else if (transition === "END_FRAME_IN") { - if (token.name === "NUMBER") { - parsed[parsed.length - 1].endFrame = token.value; - transition = "END_FRAME_OUT"; - } else { + token.startI, + ) + } + } + else if (transition === 'END_FRAME_IN') { + if (token.name === 'NUMBER') { + parsed[parsed.length - 1].endFrame = token.value + transition = 'END_FRAME_OUT' + } + else { warnDefinitionSyntaxError( - "number", + 'number', token.name, definitionStr, - token.startI - ); - } - } else if (transition === "END_FRAME_OUT") { - if (token.name === "COMMA") { - transition = "START_FRAME_IN"; - } else if (token.name === "OPEN_PAREN") { - transition = "DURATION_IN"; - } else { + token.startI, + ) + } + } + else if (transition === 'END_FRAME_OUT') { + if (token.name === 'COMMA') { + transition = 'START_FRAME_IN' + } + else if (token.name === 'OPEN_PAREN') { + transition = 'DURATION_IN' + } + else { warnDefinitionSyntaxError( - "',' or '('", + '\',\' or \'(\'', token.name, definitionStr, - token.startI - ); - } - } else if (transition === "DURATION_IN") { - if (token.name === "NUMBER") { - parsed[parsed.length - 1].duration = token.value; - transition = "DURATION_OUT"; - } else { + token.startI, + ) + } + } + else if (transition === 'DURATION_IN') { + if (token.name === 'NUMBER') { + parsed[parsed.length - 1].duration = token.value + transition = 'DURATION_OUT' + } + else { warnDefinitionSyntaxError( - "number", + 'number', token.name, definitionStr, - token.startI - ); + token.startI, + ) } - } else if (transition === "DURATION_OUT") { - if (token.name === "CLOSE_PAREN") { - transition = "NEXT_OR_DONE"; - } else { - warnDefinitionSyntaxError('"("', token.name, definitionStr, token.startI); + } + else if (transition === 'DURATION_OUT') { + if (token.name === 'CLOSE_PAREN') { + transition = 'NEXT_OR_DONE' } - } else if (transition === "NEXT_OR_DONE") { - if (token.name === "COMMA") { - transition = "START_FRAME_IN"; - } else { - warnDefinitionSyntaxError('","', token.name, definitionStr, token.startI); + else { + warnDefinitionSyntaxError('"("', token.name, definitionStr, token.startI) + } + } + else if (transition === 'NEXT_OR_DONE') { + if (token.name === 'COMMA') { + transition = 'START_FRAME_IN' + } + else { + warnDefinitionSyntaxError('","', token.name, definitionStr, token.startI) } } } - return parsed; + return parsed } type Transition = - | "START_FRAME_IN" - | "START_FRAME_OUT" - | "END_FRAME_IN" - | "END_FRAME_OUT" - | "DURATION_IN" - | "DURATION_OUT" - | "NEXT_OR_DONE"; + | 'START_FRAME_IN' + | 'START_FRAME_OUT' + | 'END_FRAME_IN' + | 'END_FRAME_OUT' + | 'DURATION_IN' + | 'DURATION_OUT' + | 'NEXT_OR_DONE' -type TokenName = "COMMA" | "HYPHEN" | "OPEN_PAREN" | "CLOSE_PAREN" | "NUMBER"; +type TokenName = 'COMMA' | 'HYPHEN' | 'OPEN_PAREN' | 'CLOSE_PAREN' | 'NUMBER' interface Token { - name: TokenName; - value: number; - startI: number; + name: TokenName + value: number + startI: number } function tokenize(definition: string): Token[] { - const tokenized: Token[] = []; - let ii = 0; + const tokenized: Token[] = [] + let ii = 0 while (ii < definition.length) { - const c = definition[ii]; - if ("0123456789".indexOf(c) > -1) { + const c = definition[ii] + if ('0123456789'.indexOf(c) > -1) { if ( - tokenized.length && - tokenized[tokenized.length - 1].name === "NUMBER" + tokenized.length + && tokenized[tokenized.length - 1].name === 'NUMBER' ) { - tokenized[tokenized.length - 1].value *= 10; - tokenized[tokenized.length - 1].value += parseInt(c); - } else { - tokenized.push({ name: "NUMBER", value: parseInt(c), startI: ii }); - } - } else if (c === " ") { - } else if (c === ",") { - tokenized.push({ name: "COMMA", value: -1, startI: ii }); - } else if (c === "(") { - tokenized.push({ name: "OPEN_PAREN", value: -1, startI: ii }); - } else if (c === ")") { - tokenized.push({ name: "CLOSE_PAREN", value: -1, startI: ii }); - } else if (c === "-") { - tokenized.push({ name: "HYPHEN", value: -1, startI: ii }); - } else { - warnDefinitionBadCharacter("0123456789,-()", c, definition, ii); - } - ii++; + tokenized[tokenized.length - 1].value *= 10 + tokenized[tokenized.length - 1].value += parseInt(c) + } + else { + tokenized.push({ name: 'NUMBER', value: parseInt(c), startI: ii }) + } + } + else if (c === ' ') { + } + else if (c === ',') { + tokenized.push({ name: 'COMMA', value: -1, startI: ii }) + } + else if (c === '(') { + tokenized.push({ name: 'OPEN_PAREN', value: -1, startI: ii }) + } + else if (c === ')') { + tokenized.push({ name: 'CLOSE_PAREN', value: -1, startI: ii }) + } + else if (c === '-') { + tokenized.push({ name: 'HYPHEN', value: -1, startI: ii }) + } + else { + warnDefinitionBadCharacter('0123456789,-()', c, definition, ii) + } + ii++ } - return tokenized; + return tokenized } function warnDefinitionBadCharacter( expected: string, found: string, definition: string, - index: number + index: number, ) { useLogger().logError( - `Cientos AnimationDefinitionParser: Unexpected character while processing animation definition: expected ${expected}, got ${found}. + 'Cientos AnimationDefinitionParser: ' + + `Unexpected character while processing animation definition: expected ${expected}, got ${found}. ${definition} -${Array(index + 1).join(" ")}^` - ); +${Array(index + 1).join(' ')}^`, + ) } function warnDefinitionSyntaxError( expected: string, found: string, definition: string, - index: number + index: number, ) { useLogger().logError( - `Cientos AnimationDefinitionParser: Syntax error while processing animation definition: expected ${expected}, got ${found}. + 'Cientos AnimationDefinitionParser: ' + + `Syntax error while processing animation definition: expected ${expected}, got ${found}. ${definition} -${Array(index + 1).join(" ")}^` - ); +${Array(index + 1).join(' ')}^`, + ) } diff --git a/src/core/abstractions/AnimatedSprite/StringOps.ts b/src/core/abstractions/AnimatedSprite/StringOps.ts index 46d63d65..7e73345c 100644 --- a/src/core/abstractions/AnimatedSprite/StringOps.ts +++ b/src/core/abstractions/AnimatedSprite/StringOps.ts @@ -1,14 +1,14 @@ -const numbersAtEnd = /[0-9]*$/; -const underscoresNumbersAtEnd = /_*[0-9]*$/; +const numbersAtEnd = /[0-9]*$/ +const underscoresNumbersAtEnd = /_*[0-9]*$/ export function stripUnderscoresNumbersFromEnd(str: string) { - return str.replace(underscoresNumbersAtEnd, ""); + return str.replace(underscoresNumbersAtEnd, '') } export function getNumbersFromEnd(str: string) { - const matches = str.match(numbersAtEnd); + const matches = str.match(numbersAtEnd) if (matches) { - return parseInt(matches[matches.length - 1]); + return parseInt(matches[matches.length - 1]) } - return null; + return null } \ No newline at end of file diff --git a/src/core/abstractions/AnimatedSprite/component.vue b/src/core/abstractions/AnimatedSprite/component.vue index 9f783782..797c1fa6 100644 --- a/src/core/abstractions/AnimatedSprite/component.vue +++ b/src/core/abstractions/AnimatedSprite/component.vue @@ -32,13 +32,13 @@ export interface AnimatedSpriteProps { /** If a string, the name of the animation to play. If `[number, number]`, the start and end frames of the animation. If `number` the frame number to display. */ animation?: string | [number, number] | number /** Event callback when the assets – atlas and image – are loaded. */ - onLoad?: (frameName:string) => void + onLoad?: (frameName: string) => void /** Event callback when the animation ends. */ - onEnd?: (frameName:string) => void + onEnd?: (frameName: string) => void /** Event callback when the animation loops. */ - onLoopEnd?: (frameName:string) => void + onLoopEnd?: (frameName: string) => void /** Event callback when each frame changes. Note: called *at most* once per tick. */ - onFrame?: (frameName:string) => void + onFrame?: (frameName: string) => void /** Whether the animation is paused. */ paused?: boolean /** Whether to play the animation in reverse. */ @@ -68,17 +68,21 @@ const props = withDefaults(defineProps(), { anchor: () => [0.5, 0.5], }) -const page:AtlasPage = await getAtlasPageAsync(props.atlas, props.image, props.definitions) - const animatedSpriteGroupRef = ref | null>() const animatedSpriteSpriteRef = ref | InstanceType | null>() -const animatedSpriteMaterialRef = ref | InstanceType | null>() +const animatedSpriteMaterialRef = ref | null>() const scaleX = ref(0) const scaleY = ref(0) const positionX = ref(0) const positionY = ref(0) const NOMINAL_PX_TO_WORLD_UNITS = 0.01 +defineExpose({ + value: animatedSpriteGroupRef, +}) + +const page: AtlasPage = await getAtlasPageAsync(props.atlas, props.image, props.definitions) + let frame: AtlasFrame | null = null let frameNum = 0 let cooldown = 1 @@ -109,10 +113,12 @@ useRenderLoop().onLoop(({ delta }) => { while (frameNum < 0) { frameNum += animation.length } - } else { + } + else { frameNum = Math.max(0, frameNum) } - } else { + } + else { frameNum++ if (props.onLoopEnd && props.loop && frameNum >= animation.length) props.onLoopEnd(frame!.name) if (!props.loop && frameNum >= animation.length) { @@ -122,7 +128,8 @@ useRenderLoop().onLoop(({ delta }) => { } if (props.loop) { frameNum %= animation.length - } else { + } + else { frameNum = Math.min(animation.length - 1, frameNum) } } @@ -186,7 +193,8 @@ watch(() => [props.resetOnEnd, props.reversed], () => { if (frameHeldOnLoopEnd) { if (props.reversed) { frameNum = props.resetOnEnd ? animation.length - 1 : 0 - } else { + } + else { frameNum = props.resetOnEnd ? 0 : animation.length - 1 } updateFrame(animation[frameNum]) @@ -194,26 +202,44 @@ watch(() => [props.resetOnEnd, props.reversed], () => { }) watch(() => [props.flipX, props.anchor, animatedSpriteSpriteRef], render) - -defineExpose({ - value: animatedSpriteGroupRef -})