Full Code of ragnarlotus/vue-flux for AI

main 211ba37a6741 cached
217 files
559.1 KB
164.5k tokens
272 symbols
1 requests
Download .txt
Showing preview only (614K chars total). Download the full file or copy to clipboard to get everything.
Repository: ragnarlotus/vue-flux
Branch: main
Commit: 211ba37a6741
Files: 217
Total size: 559.1 KB

Directory structure:
gitextract_8a1o24_7/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .markdownlint.cjs
├── .prettierrc.json
├── .vscode/
│   └── extensions.json
├── LICENSE
├── README.md
├── env.d.ts
├── eslint.config.ts
├── ia.txt
├── index.html
├── package.json
├── src/
│   ├── App.vue
│   ├── assets/
│   │   └── css/
│   │       ├── base.scss
│   │       └── main.css
│   ├── complements/
│   │   ├── FluxCaption/
│   │   │   ├── FluxCaption.test.ts
│   │   │   └── FluxCaption.vue
│   │   ├── FluxControls/
│   │   │   ├── FluxControls.test.ts
│   │   │   ├── FluxControls.vue
│   │   │   └── buttons/
│   │   │       ├── Next.vue
│   │   │       ├── Play.vue
│   │   │       ├── Prev.vue
│   │   │       ├── Stop.vue
│   │   │       └── index.ts
│   │   ├── FluxIndex/
│   │   │   ├── Button/
│   │   │   │   ├── Button.test.ts
│   │   │   │   └── Button.vue
│   │   │   ├── FluxIndex.vue
│   │   │   ├── List/
│   │   │   │   ├── List.test.ts
│   │   │   │   └── List.vue
│   │   │   └── Thumb/
│   │   │       ├── Thumb.vue
│   │   │       └── useThumbs.ts
│   │   ├── FluxPagination/
│   │   │   └── FluxPagination.vue
│   │   ├── FluxPreloader/
│   │   │   └── FluxPreloader.vue
│   │   ├── __test__/
│   │   │   └── PlayerHelper.ts
│   │   └── index.ts
│   ├── components/
│   │   ├── FluxButton/
│   │   │   ├── FluxButton.test.ts
│   │   │   └── FluxButton.vue
│   │   ├── FluxCube/
│   │   │   ├── FluxCube.vue
│   │   │   ├── Sides.ts
│   │   │   ├── Turns.ts
│   │   │   ├── __mocks__/
│   │   │   │   ├── FluxCube.vue
│   │   │   │   └── Side.vue
│   │   │   ├── factories/
│   │   │   │   ├── CubeFactory.test.ts
│   │   │   │   ├── CubeFactory.ts
│   │   │   │   ├── CubeSideFactory.ts
│   │   │   │   ├── SideTransformFactory.test.ts
│   │   │   │   └── SideTransformFactory.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   ├── FluxGrid/
│   │   │   ├── FluxGrid.vue
│   │   │   ├── __mocks__/
│   │   │   │   ├── FluxGrid.vue
│   │   │   │   └── Tile.vue
│   │   │   ├── factories/
│   │   │   │   ├── GridFactory.ts
│   │   │   │   ├── GridTileFactory.ts
│   │   │   │   └── index.ts
│   │   │   └── types.ts
│   │   ├── FluxImage/
│   │   │   ├── FluxImage.vue
│   │   │   ├── __mocks__/
│   │   │   │   └── FluxImage.vue
│   │   │   └── types.ts
│   │   ├── FluxParallax/
│   │   │   ├── FluxParallax.vue
│   │   │   └── types.ts
│   │   ├── FluxTransition/
│   │   │   ├── FluxTransition.vue
│   │   │   └── types.ts
│   │   ├── FluxVortex/
│   │   │   ├── FluxVortex.vue
│   │   │   ├── __mocks__/
│   │   │   │   ├── FluxVortex.vue
│   │   │   │   └── Tile.vue
│   │   │   ├── factories/
│   │   │   │   ├── VortexCircleFactory.ts
│   │   │   │   ├── VortexFactory.ts
│   │   │   │   └── index.ts
│   │   │   └── types.ts
│   │   ├── FluxWrapper/
│   │   │   ├── FluxWrapper.vue
│   │   │   ├── __mocks__/
│   │   │   │   └── FluxWrapper.vue
│   │   │   └── types.ts
│   │   ├── VueFlux/
│   │   │   ├── VueFlux.vue
│   │   │   ├── __test__/
│   │   │   │   └── emit.ts
│   │   │   └── types.ts
│   │   ├── index.ts
│   │   ├── types.ts
│   │   └── useComponent.ts
│   ├── controllers/
│   │   ├── Display/
│   │   │   └── Display.ts
│   │   ├── Keys/
│   │   │   └── Keys.ts
│   │   ├── Mouse/
│   │   │   └── Mouse.ts
│   │   ├── Player/
│   │   │   ├── Directions.ts
│   │   │   ├── Player.ts
│   │   │   ├── Resource.ts
│   │   │   ├── Statuses.ts
│   │   │   ├── Transition.ts
│   │   │   ├── __mocks__/
│   │   │   │   ├── Player.ts
│   │   │   │   ├── Resource.ts
│   │   │   │   └── Transitions.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   ├── Timers/
│   │   │   └── Timers.ts
│   │   ├── Touches/
│   │   │   └── Touches.ts
│   │   └── index.ts
│   ├── lib.ts
│   ├── main.ts
│   ├── module.d.ts
│   ├── playgrounds/
│   │   ├── PgFluxCaption.vue
│   │   ├── PgFluxControls.vue
│   │   ├── PgFluxCube.vue
│   │   ├── PgFluxGrid.vue
│   │   ├── PgFluxImage.vue
│   │   ├── PgFluxIndex.vue
│   │   ├── PgFluxPagination.vue
│   │   ├── PgFluxParallax.vue
│   │   ├── PgFluxParallaxOp.vue
│   │   ├── PgFluxPreloader.vue
│   │   ├── PgFluxTransition.vue
│   │   ├── PgVueFlux.vue
│   │   └── components/
│   │       └── PgButton.vue
│   ├── repositories/
│   │   ├── Resources/
│   │   │   ├── Resources.test.ts
│   │   │   ├── Resources.ts
│   │   │   ├── ResourcesMapper.test.ts
│   │   │   ├── ResourcesMapper.ts
│   │   │   └── types.ts
│   │   ├── Transitions/
│   │   │   ├── Transitions.test.ts
│   │   │   ├── Transitions.ts
│   │   │   ├── TransitionsMapper.test.ts
│   │   │   ├── TransitionsMapper.ts
│   │   │   └── types.ts
│   │   └── index.ts
│   ├── resources/
│   │   ├── Img/
│   │   │   ├── Img.test.ts
│   │   │   ├── Img.ts
│   │   │   └── __mocks__/
│   │   │       └── Img.ts
│   │   ├── ResizeTypes.ts
│   │   ├── Resource.ts
│   │   ├── Statuses.ts
│   │   ├── __test__/
│   │   │   └── ResourceFactory.ts
│   │   ├── index.ts
│   │   └── types.ts
│   ├── shared/
│   │   ├── Maths/
│   │   │   ├── Maths.test.ts
│   │   │   └── Maths.ts
│   │   ├── Position/
│   │   │   ├── Position.test.ts
│   │   │   └── Position.ts
│   │   ├── ResizeCalculator/
│   │   │   ├── ResizeCalculator.test.ts
│   │   │   └── ResizeCalculator.ts
│   │   ├── ResourceLoader/
│   │   │   ├── ResourceLoader.test.ts
│   │   │   ├── ResourceLoader.ts
│   │   │   ├── __mocks__/
│   │   │   │   └── ResourceLoader.ts
│   │   │   └── __test__/
│   │   │       └── ResourceLoaderFactory.ts
│   │   ├── Size/
│   │   │   ├── Size.test.ts
│   │   │   └── Size.ts
│   │   └── index.ts
│   └── transitions/
│       ├── Blinds2D/
│       │   ├── Blinds2D.test.ts
│       │   ├── Blinds2D.vue
│       │   └── types.ts
│       ├── Blinds3D/
│       │   ├── Blinds3D.test.ts
│       │   ├── Blinds3D.vue
│       │   └── types.ts
│       ├── Blocks1/
│       │   ├── Blocks1.test.ts
│       │   ├── Blocks1.vue
│       │   └── types.ts
│       ├── Blocks2/
│       │   ├── Blocks2.test.ts
│       │   ├── Blocks2.vue
│       │   └── types.ts
│       ├── Book/
│       │   ├── Book.test.ts
│       │   ├── Book.vue
│       │   └── types.ts
│       ├── Camera/
│       │   ├── Camera.test.ts
│       │   ├── Camera.vue
│       │   └── types.ts
│       ├── Concentric/
│       │   ├── Concentric.test.ts
│       │   ├── Concentric.vue
│       │   └── types.ts
│       ├── Cube/
│       │   ├── Cube.test.ts
│       │   ├── Cube.vue
│       │   └── types.ts
│       ├── Explode/
│       │   ├── Explode.test.ts
│       │   ├── Explode.vue
│       │   └── types.ts
│       ├── Fade/
│       │   ├── Fade.test.ts
│       │   ├── Fade.vue
│       │   └── types.ts
│       ├── Fall/
│       │   ├── Fall.test.ts
│       │   ├── Fall.vue
│       │   └── types.ts
│       ├── Kenburn/
│       │   ├── Kenburn.test.ts
│       │   ├── Kenburn.vue
│       │   └── types.ts
│       ├── Round1/
│       │   ├── Round1.test.ts
│       │   ├── Round1.vue
│       │   └── types.ts
│       ├── Round2/
│       │   ├── Round2.test.ts
│       │   ├── Round2.vue
│       │   └── types.ts
│       ├── Slide/
│       │   ├── Slide.test.ts
│       │   ├── Slide.vue
│       │   └── types.ts
│       ├── Swipe/
│       │   ├── Swipe.test.ts
│       │   ├── Swipe.vue
│       │   └── types.ts
│       ├── Warp/
│       │   ├── Warp.test.ts
│       │   ├── Warp.vue
│       │   └── types.ts
│       ├── Waterfall/
│       │   ├── Waterfall.test.ts
│       │   ├── Waterfall.vue
│       │   └── types.ts
│       ├── Wave/
│       │   ├── Wave.test.ts
│       │   ├── Wave.vue
│       │   └── types.ts
│       ├── Zip/
│       │   ├── Zip.test.ts
│       │   ├── Zip.vue
│       │   └── types.ts
│       ├── __test__/
│       │   └── AnimationWrapper.ts
│       ├── index.ts
│       ├── types.ts
│       └── useTransition.ts
├── tsconfig.app.json
├── tsconfig.build.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
├── vite.config.ts
└── vitest.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 3
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100


================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
ia.txt text eol=lf


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: [ragnarlotus]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

*.tsbuildinfo


================================================
FILE: .markdownlint.cjs
================================================
module.exports = {
	default: true,
	MD001: false,
	MD013: false,
	MD024: false,
	MD033: false,
	MD036: false,
	MD041: false,
};


================================================
FILE: .prettierrc.json
================================================
{
	"$schema": "https://json.schemastore.org/prettierrc",
	"semi": true,
	"singleQuote": true,
	"printWidth": 100,
	"useTabs": true,
	"tabWidth": 3,
	"vueIndentScriptAndStyle": true
}


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "Vue.volar",
    "vitest.explorer",
    "dbaeumer.vscode-eslint",
    "EditorConfig.EditorConfig",
    "esbenp.prettier-vscode"
  ]
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
## Documentation and demos

**[Version 5 documentation](https://ragnarlotus.github.io/vue-flux-docs/documentation/v5/overview)**

**[Version 6 documentation](https://ragnarlotus.github.io/vue-flux-docs/documentation/v6/overview)**

**[Version 7 documentation](https://ragnarlotus.github.io/vue-flux-docs/documentation/v7/overview)**

**[Version 7 demos](https://ragnarlotus.github.io/vue-flux-docs/demos/demos)**

# Overview

This is an image slider developed with [vue](https://vuejs.org/) 3 which comes with 20 cool transitions out of the box.

![npm](https://img.shields.io/npm/v/vue-flux/latest.svg?style=flat-square)
![npm](https://img.shields.io/npm/dt/vue-flux.svg?style=flat-square)
![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/vue-flux/latest.svg?style=flat-square)
![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/vue-flux/latest.svg?style=flat-square)
![GitHub issues](https://img.shields.io/github/issues-raw/ragnarlotus/vue-flux.svg?style=flat-square)
![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)

## Features

| Feature | Description |
|---------|-------------|
| Responsive | The slider and the images are adapted to container to fill it always |
| Compatibility | Supported by all major browsers |
| Expandable | You can add your custom transitions very easily |
| Customization | Total customizable to suit most needs |
| Gestures | Mobile friendly by gestures |
| Functionality | You can use arrow keys to navigate. Switch to full screen |
| Parallax | It includes a parallax component very easy to set up |

## Quick start

Install and save the package.

``` bash
npm install --save vue-flux@latest
```

Add component. This one has all the complements, so you can remove the ones you don't want.

``` html
<script setup>
   import { ref, shallowReactive } from 'vue';
   import {
      VueFlux,
      FluxCaption,
      FluxControls,
      FluxIndex,
      FluxPagination,
      FluxPreloader,
      Img,
      Book,
      Zip,
   } from 'vue-flux';
   import 'vue-flux/style.css';

   const $vueFlux = ref();

   const vfOptions = shallowReactive({
      autoplay: true,
   });

   const vfRscs = shallowReactive([
      new Img('URL1' 'img 1'),
      new Img('URL2' 'img 2'),
      new Img('URL3' 'img 3'),
   ]);

   const vfTransitions = shallowReactive([Book, Zip]);
</script>

<template>
   <VueFlux
      :options="vfOptions"
      :rscs="vfRscs"
      :transitions="vfTransitions"
      ref="$vueFlux">

      <template #preloader="preloaderProps">
         <FluxPreloader v-bind="preloaderProps" />
      </template>

      <template #caption="captionProps">
         <FluxCaption v-bind="captionProps" />
      </template>

      <template #controls="controlsProps">
         <FluxControls v-bind="controlsProps" />
      </template>

      <template #pagination="paginationProps">
         <FluxPagination v-bind="paginationProps" />
      </template>

      <template #index="indexProps">
         <FluxIndex v-bind="indexProps" />
      </template>
   </VueFlux>

   <button @click="$vueFlux.show('next')">NEXT</button>
</template>
```

## Performance

Weight is about 60 KB so is pretty light having only the essential CSS. It also does not require a high end computer as animations are performed with CSS3 hardware acceleration.

## Included transitions

#### 2D transitions

* Fade: fades from one image to next.
* Kenburn: fades, zoom and moves current image to next.
* Swipe: swipes the image to display next like uncovered with a curtain.
* Slide: slides the image horizontally revealing the next.
* Waterfall: divides the image in bars and drops them down in turns.
* Zip: divides the image in bars and slides them up and down alternately like a zip.
* Blinds 2D: divides the image in vertical bars that blinds and fades out.
* Blocks 1: the image is split in blocks that shrink and fade out randomly.
* Blocks 2: the image is split in blocks that shrink and fade out in wave from a corner to the opposite.
* Concentric: a concentric effect is performed by rotating the image converted into circles.
* Warp: a concentric effect is performed by rotating the image converted into circles in alternate direction.
* Camera: from outside to inside the image is being circled in black like a camera.

#### 3D transitions

* Cube: turns the image to a side like if place in a cube.
* Book: makes the effect of turning a page to display next image.
* Fall: the image falls in front displaying next image.
* Wave: makes the image 3D and divides it in slices that turn vertically to display the next image.
* Blinds 3D: divides the image in vertical bars that blinds 180 deg to form the next image.
* Round 1: the image is split in blocks that turn 180 deg horizontally to form next image.
* Round 2: panels start to round vertically revealing the next image in upper arrow form leaving trail.
* Explode: the image starts to explode from the center to outside.

## Parallax

As simple as this.

``` html
<script setup>
   import { FluxParallax, Img } from 'vue-flux';

   const rsc = new Img('URL1' 'img 1');
</script>

<template>
   <FluxParallax :rsc="rsc" style="height: 300px;">
      <div>CONTENT</div>
   </FluxParallax>
</template>
```

## Troubleshooting

If you find yourself running into issues during installation or running the slider, please check our [documentation](https://ragnarlotus.github.io/vue-flux-docs/documentation/v7/overview). If still needs help open an [issue](https://github.com/ragnarlotus/vue-flux/issues/new). I will be happy to discuss how they can be solved.

## Documentation

You can view the full documentation at the project's [documentation](https://ragnarlotus.github.io/vue-flux-docs/documentation/v7/overview) with examples and detailed information.

## Changelog

Check the [changelog](https://ragnarlotus.github.io/vue-flux-docs/documentation/v7/changelog) for update info.

## Inspiration

This slider was inspired by [Flux Slider](http://joelambert.co.uk/flux/).

## Contributing

Contributions, questions and comments are all welcome and encouraged.

Do not hesitate to send me your own transitions to add them to the slider.


================================================
FILE: env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: eslint.config.ts
================================================
import { globalIgnores } from 'eslint/config';
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
import pluginVue from 'eslint-plugin-vue';
import pluginVitest from '@vitest/eslint-plugin';
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';

// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup

export default defineConfigWithVueTs(
	{
		name: 'app/files-to-lint',
		files: ['**/*.{ts,mts,tsx,vue}'],
	},

	globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),

	pluginVue.configs['flat/essential'],
	vueTsConfigs.recommended,

	{
		...pluginVitest.configs.recommended,
		files: ['src/**/__tests__/*'],
	},
	skipFormatting,
	{
		rules: {
			'vue/multi-word-component-names': 'off',
		},
	},
);


================================================
FILE: ia.txt
================================================
=== vue-flux IA bundle ===
Generated on: Fri Dec 12 06:10:34     2025

===== FILE: package.json =====
{
	"name": "vue-flux",
	"version": "7.1.3",
	"type": "module",
	"description": "Vue image and other resources slider",
	"author": "ragnar lotus",
	"repository": {
		"type": "git",
		"url": "git+https://github.com/ragnarlotus/vue-flux.git"
	},
	"keywords": [
		"vue",
		"image",
		"slider",
		"carousel",
		"parallax"
	],
	"license": "MIT",
	"bugs": "https://github.com/ragnarlotus/vue-flux/issues",
	"homepage": "https://ragnarlotus.github.io/vue-flux-docs/",
	"main": "./dist/vue-flux.umd.cjs",
	"module": "./dist/vue-flux.js",
	"files": [
		"dist"
	],
	"types": "./dist/vue-flux.d.ts",
	"engines": {
		"node": "^20.19.0 || >=22.12.0"
	},
	"scripts": {
		"dev": "vite",
		"build": "run-p type-check \"build-only {@}\" --",
		"preview": "vite preview",
		"test:coverage": "vitest run --coverage --watch",
		"test:unit": "vitest",
		"build-only": "vite build",
		"type-check": "vue-tsc --build",
		"lint": "eslint . --fix",
		"format": "prettier --write src/"
	},
	"exports": {
		".": {
			"types": "./dist/vue-flux.d.ts",
			"import": "./dist/vue-flux.js",
			"require": "./dist/vue-flux.umd.cjs"
		},
		"./style.css": "./dist/vue-flux.css",
		"./complements": {
			"types": "./dist/complements/index.d.ts",
			"import": "./dist/complements/index.js"
		},
		"./transitions": {
			"types": "./dist/transitions/index.d.ts",
			"import": "./dist/transitions/index.js"
		}
	},
	"sideEffects": [
		"*.css"
	],
	"peerDependencies": {
		"vue": "^3.5.0"
	},
	"devDependencies": {
		"@tailwindcss/vite": "^4.1.17",
		"@tsconfig/node22": "^22.0.5",
		"@types/jsdom": "^27.0.0",
		"@types/node": "^25.0.0",
		"@vitejs/plugin-vue": "^6.0.2",
		"@vitest/coverage-v8": "^4.0.15",
		"@vitest/eslint-plugin": "^1.5.2",
		"@vue/eslint-config-prettier": "^10.2.0",
		"@vue/eslint-config-typescript": "^14.6.0",
		"@vue/test-utils": "^2.4.6",
		"@vue/tsconfig": "^0.8.1",
		"eslint": "^9.39.1",
		"eslint-plugin-vue": "~10.6.2",
		"jiti": "^2.6.1",
		"jsdom": "^27.3.0",
		"npm-run-all2": "^8.0.4",
		"prettier": "3.7.4",
		"sass": "^1.96.0",
		"tailwindcss": "^4.1.17",
		"typescript": "~5.9.3",
		"vite": "^7.2.7",
		"vite-plugin-dts": "^4.5.4",
		"vite-plugin-vue-devtools": "^8.0.5",
		"vitest": "^4.0.15",
		"vue": "^3.5.25",
		"vue-cosk": "^1.0.0",
		"vue-tsc": "^3.1.8"
	}
}


===== FILE: vite.config.ts =====
import { fileURLToPath, URL } from 'node:url';
import { resolve } from 'node:path';

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from '@tailwindcss/vite';
import vueDevTools from 'vite-plugin-vue-devtools';
import dts from 'vite-plugin-dts';

// https://vite.dev/config/
export default defineConfig({
	plugins: [
		vue(),
		vueDevTools(),
		tailwindcss(),
		dts({
			tsconfigPath: './tsconfig.build.json',
			rollupTypes: true,
		}),
	],
	resolve: {
		alias: {
			'@': fileURLToPath(new URL('./src', import.meta.url)),
		},
	},
	build: {
		copyPublicDir: false,
		lib: {
			entry: resolve(__dirname, 'src/lib.ts'),
			name: 'VueFlux',
			fileName: 'vue-flux',
		},
		rollupOptions: {
			external: ['vue'],
			output: {
				globals: {
					vue: 'Vue',
				},
			},
		},
	},
});


===== FILE: vitest.config.ts =====
import { fileURLToPath } from 'node:url';
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config';
import viteConfig from './vite.config';

export default mergeConfig(
	viteConfig,
	defineConfig({
		test: {
			globals: true,
			environment: 'jsdom',
			exclude: [...configDefaults.exclude, 'e2e/**'],
			root: fileURLToPath(new URL('./', import.meta.url)),
		},
	}),
);


===== FILE: tsconfig.json =====
{
	"files": [],
	"references": [
		{
			"path": "./tsconfig.node.json"
		},
		{
			"path": "./tsconfig.app.json"
		},
		{
			"path": "./tsconfig.vitest.json"
		}
	],
	"compilerOptions": {
		"types": ["vitest/globals"]
	},
	"exclude": [
		"src/App.vue",
		"src/main.ts",
		"node_modules",
		"dist",
		"src/**/*.test.ts",
		"src/**/*.test.tsx",
		"src/**/*.spec.ts",
		"src/**/*.spec.tsx"
	]
}


===== FILE: src/App.vue =====
<!-- eslint-disable @typescript-eslint/no-unused-vars -->
<script setup lang="ts">
	import { ref, type Ref } from 'vue';

	// Playgrounds
	import PgFluxImage from './playgrounds/PgFluxImage.vue';
	import PgFluxCube from './playgrounds/PgFluxCube.vue';
	import PgFluxGrid from './playgrounds/PgFluxGrid.vue';
	import PgFluxTransition from './playgrounds/PgFluxTransition.vue';
	import PgVueFlux from './playgrounds/PgVueFlux.vue';
	import PgFluxParallax from './playgrounds/PgFluxParallax.vue';
	import PgFluxParallaxOp from './playgrounds/PgFluxParallaxOp.vue';
	import PgFluxCaption from './playgrounds/PgFluxCaption.vue';
	import PgFluxControls from './playgrounds/PgFluxControls.vue';
	import PgFluxIndex from './playgrounds/PgFluxIndex.vue';
	import PgFluxPagination from './playgrounds/PgFluxPagination.vue';
	import PgFluxPreloader from './playgrounds/PgFluxPreloader.vue';

	const $wrapper: Ref<null | HTMLDivElement> = ref(null);
</script>

<template>
	<main class="container mx-auto mb-4">
		<div ref="$wrapper" class="relative mx-auto">
			<!-- <PgFluxImage /> -->
			<!-- <PgFluxCube /> -->
			<!-- <PgFluxGrid /> -->
			<!-- <PgFluxTransition /> -->
			<!-- <PgVueFlux /> -->
			<!-- <PgFluxParallax /> -->
			<PgFluxParallaxOp />
			<!-- <PgFluxCaption /> -->
			<!-- <PgFluxControls /> -->
			<!-- <PgFluxIndex /> -->
			<!-- <PgFluxPagination /> -->
			<!-- <PgFluxPreloader /> -->
		</div>
	</main>
</template>


===== FILE: src/assets/css/base.scss =====
label {
	margin-top: 12px;
	display: block;

	span {
		margin-right: 6px;
	}
}


===== FILE: src/assets/css/main.css =====
@import 'tailwindcss';
@import './base.scss';


===== FILE: src/complements/FluxCaption/FluxCaption.test.ts =====
import { Player, Timers } from '../../controllers';
import { mount } from '@vue/test-utils';
import FluxCaption from './FluxCaption.vue';
import emit from '../../components/VueFlux/__test__/emit';
import {
	vueFluxConfig,
	setCurrentResource,
	setCurrentTransition,
} from '../__test__/PlayerHelper';

vi.mock('../../controllers/Player/Player');

const defaultCaption = 'the caption';

describe('complements: FluxCaption', () => {
	const timers = new Timers();

	it('should mount properly without slot', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		expect(() => {
			mount(FluxCaption, {
				props: {
					player,
				},
			});
		}).not.toThrow();
	});

	it('should not be visible if no caption', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		setCurrentResource(player);

		const wrapper = mount(FluxCaption, {
			props: {
				player,
			},
		});

		expect(wrapper.html().includes('class="flux-caption"')).toBeTruthy();
	});

	it('should not be visible if caption has no length', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		setCurrentResource(player, '');

		const wrapper = mount(FluxCaption, {
			props: {
				player,
			},
		});

		expect(wrapper.html().includes('class="flux-caption"')).toBeTruthy();
	});

	it('should not be visible if transition running', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		setCurrentResource(player, defaultCaption);
		setCurrentTransition(player);

		const wrapper = mount(FluxCaption, {
			props: {
				player,
			},
		});

		expect(wrapper.html().includes('class="flux-caption"')).toBeTruthy();
	});

	it('should display the caption', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		setCurrentResource(player, defaultCaption);

		const wrapper = mount(FluxCaption, {
			props: {
				player,
			},
		});

		expect(
			wrapper.html().includes('class="flux-caption visible"')
		).toBeTruthy();
	});

	it('should mount properly with slot', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		setCurrentResource(player, defaultCaption);

		const wrapper = mount(FluxCaption, {
			props: {
				player,
			},
			slots: {
				default: `<h1>{{ params.caption }}</h1>`,
			},
		});

		expect(
			wrapper.html().includes(`<h1>${defaultCaption}</h1>`)
		).toBeTruthy();
	});
});


===== FILE: src/complements/FluxCaption/FluxCaption.vue =====
<script setup lang="ts">
	import { computed } from 'vue';
	import { Player } from '../../controllers';

	export interface Props {
		player: Player;
	}

	const props = defineProps<Props>();

	const { resource, transition } = props.player;

	const caption = computed<string>(() => {
		if (resource.current === null || resource.current.rsc.caption === null) {
			return '&nbsp;';
		}

		return resource.current.rsc.caption;
	});

	const cssClasses = computed<string[]>(() => {
		const classes = ['flux-caption'];

		if (
			transition.current === null &&
			resource.current !== null &&
			resource.current.rsc.caption.length > 0
		) {
			classes.push('visible');
		}

		return classes;
	});
</script>

<template>
	<div :class="cssClasses">
		<slot :caption="caption">{{ caption }}</slot>
	</div>
</template>

<style lang="scss">
	.vue-flux .flux-caption {
		flex: none;
		width: 100%;
		font-size: 0.8rem;
		line-height: 1.1rem;
		padding: 6px;
		box-sizing: border-box;
		color: white;
		text-align: center;
		background-color: rgba(0, 0, 0, 0.65);
		opacity: 0;

		&.visible {
			opacity: 1;
			transition: opacity 0.3s ease-in;
		}
	}
</style>


===== FILE: src/complements/FluxControls/FluxControls.test.ts =====
import { ref, type Ref } from 'vue';
import { Player, Timers } from '../../controllers';
import { Directions, Statuses } from '../../controllers/Player';
import * as Buttons from './buttons';
import FluxControls from './FluxControls.vue';
import { mount } from '@vue/test-utils';
import emit from '../../components/VueFlux/__test__/emit';
import { vueFluxConfig, setCurrentResource, setCurrentTransition } from '../__test__/PlayerHelper';

vi.mock('../../controllers/Player/Player');

describe('complements: FluxControls', () => {
	const timers = new Timers();
	const mouseOver: Ref<boolean> = ref(false);

	beforeEach(() => {
		mouseOver.value = false;
	});

	it('should mount properly without slot', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		expect(() => {
			mount(FluxControls, {
				props: {
					mouseOver,
					player,
				},
			});
		}).not.toThrow();
	});

	it('should not be visible if transition running', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		setCurrentResource(player);
		setCurrentTransition(player);

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		expect(wrapper.html().includes('class="flux-controls"')).toBeFalsy();
	});

	it('should not be visible if transition running and mouse not moving', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		setCurrentResource(player);

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		expect(wrapper.html().includes('class="flux-controls"')).toBeFalsy();
	});

	it('should be visible if no transition running and mouse moving', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		setCurrentResource(player);

		mouseOver.value = true;

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		expect(wrapper.html().includes('class="flux-controls"')).toBeTruthy();
	});

	it('should display play button', () => {
		const player = new Player(vueFluxConfig, timers, emit);
		player.status.value = Statuses.stopped;

		setCurrentResource(player);
		mouseOver.value = true;

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		expect(() => {
			wrapper.getComponent(Buttons.Play);
		}).not.toThrow();
	});

	it('should play when button pressed', async () => {
		const player = new Player(vueFluxConfig, timers, emit);
		player.status.value = Statuses.stopped;

		setCurrentResource(player);
		mouseOver.value = true;

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		await wrapper.getComponent(Buttons.Play).trigger('click');

		expect(player.play).toHaveBeenCalledWith(Directions.next, expect.any(Number));
	});

	it('should display stop button', () => {
		const player = new Player(vueFluxConfig, timers, emit);
		player.status.value = Statuses.playing;

		setCurrentResource(player);
		mouseOver.value = true;

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		expect(() => {
			wrapper.getComponent(Buttons.Stop);
		}).not.toThrow();
	});

	it('should stop when button pressed', async () => {
		const player = new Player(vueFluxConfig, timers, emit);
		player.status.value = Statuses.playing;

		setCurrentResource(player);
		mouseOver.value = true;

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		await wrapper.getComponent(Buttons.Stop).trigger('click');

		expect(player.stop).toHaveBeenCalledOnce();
	});

	it('should display previous resource when button pressed', async () => {
		const player = new Player(vueFluxConfig, timers, emit);
		player.status.value = Statuses.playing;

		setCurrentResource(player);
		mouseOver.value = true;

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		await wrapper.getComponent(Buttons.Prev).trigger('click');

		expect(player.show).toHaveBeenCalledWith(Directions.prev);
	});

	it('should display next resource when button pressed', async () => {
		const player = new Player(vueFluxConfig, timers, emit);
		player.status.value = Statuses.playing;

		setCurrentResource(player);
		mouseOver.value = true;

		const wrapper = mount(FluxControls, {
			props: {
				mouseOver,
				player,
			},
		});

		await wrapper.getComponent(Buttons.Next).trigger('click');

		expect(player.show).toHaveBeenCalledWith(Directions.next);
	});
});


===== FILE: src/complements/FluxControls/FluxControls.vue =====
<script setup lang="ts">
	import { type Ref, computed, unref } from 'vue';
	import { Player, Directions } from '../../controllers/Player';
	import * as Buttons from './buttons';
	import { default as PlayerStatuses } from '../../controllers/Player/Statuses';

	export interface Props {
		mouseOver?: Ref<boolean>;
		player: Player;
	}

	const props = withDefaults(defineProps<Props>(), {
		mouseOver: undefined,
	});

	const visible = computed<boolean>(() => {
		if (props.player.resource.current === null) {
			return false;
		}

		if (props.mouseOver !== undefined && unref(props.mouseOver) === false) {
			return false;
		}

		return true;
	});
</script>

<template>
	<transition name="fade">
		<div v-if="visible" class="flux-controls">
			<Buttons.Prev @click="player.show(Directions.prev)" />
			<Buttons.Play
				v-if="(player.status.value || player.status) === PlayerStatuses.stopped"
				@click="player.play(Directions.next, 1)"
			/>
			<Buttons.Stop
				v-if="(player.status.value || player.status) === PlayerStatuses.playing"
				@click="player.stop()"
			/>
			<Buttons.Next @click="player.show(Directions.next)" />
		</div>
	</transition>
</template>

<style lang="scss">
	.vue-flux .flux-controls {
		flex: none;
		display: flex;
		justify-content: space-between;

		&.fade-enter,
		&.fade-leave-to {
			opacity: 0;
		}

		&.fade-enter-active,
		&.fade-leave-active {
			transition: opacity 0.3s ease-in;
		}

		.prev {
			margin-left: 4%;
		}

		.next {
			margin-right: 4%;
		}
	}
</style>


===== FILE: src/complements/FluxControls/buttons/Next.vue =====
<script setup lang="ts">
	import { FluxButton } from '../../../components';
</script>

<template>
	<FluxButton class="next top right">
		<polyline points="36,18 78,50 36,82" />
	</FluxButton>
</template>


===== FILE: src/complements/FluxControls/buttons/Play.vue =====
<script setup lang="ts">
	import { FluxButton } from '../../../components';
</script>

<template>
	<FluxButton class="play">
		<polygon points="32,12 82,50 32,88" />
	</FluxButton>
</template>


===== FILE: src/complements/FluxControls/buttons/Prev.vue =====
<script setup lang="ts">
	import { FluxButton } from '../../../components';
</script>

<template>
	<FluxButton class="prev top left">
		<polyline points="64,18 22,50 64,82" />
	</FluxButton>
</template>


===== FILE: src/complements/FluxControls/buttons/Stop.vue =====
<script setup lang="ts">
	import { FluxButton } from '../../../components';
</script>

<template>
	<FluxButton class="pause">
		<line x1="32" y1="22" x2="32" y2="78" />
		<line x1="68" y1="22" x2="68" y2="78" />
	</FluxButton>
</template>


===== FILE: src/complements/FluxControls/buttons/index.ts =====
export { default as Prev } from './Prev.vue';
export { default as Play } from './Play.vue';
export { default as Stop } from './Stop.vue';
export { default as Next } from './Next.vue';


===== FILE: src/complements/FluxIndex/Button/Button.test.ts =====
import { type Ref, ref } from 'vue';
import { mount } from '@vue/test-utils';
import Button from './Button.vue';

describe('complements: FluxIndex Button', () => {
	const mouseOver: Ref<boolean> = ref(false);

	beforeEach(() => {
		mouseOver.value = false;
	});

	it('mounts properly', () => {
		expect(() => {
			mount(Button, {
				props: {
					mouseOver,
				},
			});
		}).not.toThrow();
	});

	it('is visible when mouse over', () => {
		mouseOver.value = true;

		const wrapper = mount(Button, {
			props: {
				mouseOver,
			},
		});

		expect(wrapper.html().includes('toggle bottom left')).toBeTruthy();
	});

	it('is NOT visible when mouse NOT over', () => {
		const wrapper = mount(Button, {
			props: {
				mouseOver,
			},
		});

		expect(wrapper.html().includes('toggle bottom left')).toBeFalsy();
	});
});


===== FILE: src/complements/FluxIndex/Button/Button.vue =====
<script setup lang="ts">
	import { type Ref, computed, unref } from 'vue';
	import { FluxButton } from '../../../components';

	interface Props {
		mouseOver?: Ref<boolean>;
	}

	const props = withDefaults(defineProps<Props>(), {
		mouseOver: undefined,
	});

	const visible = computed<boolean>(() => [true, undefined].includes(unref(props.mouseOver)));
</script>

<template>
	<transition name="fade">
		<FluxButton v-if="visible" class="toggle bottom left">
			<rect x="17.5" y="17.5" width="12px" height="12px" />
			<rect x="17.5" y="43" width="12px" height="12px" />
			<rect x="17.5" y="68.5" width="12px" height="12px" />
			<rect x="43" y="17.5" width="12px" height="12px" />
			<rect x="43" y="43" width="12px" height="12px" />
			<rect x="43" y="68.5" width="12px" height="12px" />
			<rect x="68.5" y="17.5" width="12px" height="12px" />
			<rect x="68.5" y="43" width="12px" height="12px" />
			<rect x="68.5" y="68.5" width="12px" height="12px" />
		</FluxButton>
	</transition>
</template>

<style lang="scss">
	.vue-flux .flux-index {
		.fade-enter,
		.fade-leave-to {
			opacity: 0;
		}

		.fade-enter-active,
		.fade-leave-active {
			transition: opacity 0.3s ease-in;
		}
	}
</style>


===== FILE: src/complements/FluxIndex/FluxIndex.vue =====
<script setup lang="ts">
	import { ref, computed, type Ref } from 'vue';
	import { Size } from '../../shared';
	import { Player } from '../../controllers';
	import Button from './Button/Button.vue';
	import List from './List/List.vue';

	export interface Props {
		mouseOver?: Ref<boolean>;
		displaySize: Size;
		player: Player;
	}

	const props = withDefaults(defineProps<Props>(), {
		mouseOver: undefined,
	});

	const $fluxIndexList: Ref<null | InstanceType<typeof List>> = ref(null);

	const visible = computed<boolean>(() => props.player.resources.list.length > 0);
</script>

<template>
	<div v-if="visible" class="flux-index">
		<Button v-if="mouseOver" :mouse-over="mouseOver" @click="$fluxIndexList?.show()" />

		<List
			ref="$fluxIndexList"
			:display-size="displaySize"
			:player="player"
			:mouse-over="mouseOver"
		/>
	</div>
</template>

<style lang="scss">
	.vue-flux .flux-index {
		flex: none;
		margin-bottom: 2%;
		font-size: 0;
		text-align: center;

		nav {
			position: absolute;
			top: 0;
			left: 0;
			right: 0;
			bottom: 0;
			display: block;
			margin: 0;
			overflow: hidden;
			visibility: hidden;
		}

		nav.visible {
			z-index: 101;
			visibility: visible;
		}

		ul {
			display: block;
			height: 100%;
			margin: 0;
			margin-top: 100%;
			padding: 24px 0 0 24px;
			list-style-type: none;
			text-align: center;
			overflow-y: auto;
			background-color: black;
			transition: all 0.5s linear;
			font-size: 0;
		}
	}
</style>


===== FILE: src/complements/FluxIndex/List/List.test.ts =====
import { type Ref, ref } from 'vue';
import { mount } from '@vue/test-utils';
import { Player, Timers } from '../../../controllers';
import List from './List.vue';
import { Size } from '../../../shared';
import emit from '../../../components/VueFlux/__test__/emit';
import { vueFluxConfig, setCurrentResource } from '../../__test__/PlayerHelper';
import Thumb from '../Thumb/Thumb.vue';
import ResourceFactory from '../../../resources/__test__/ResourceFactory';

vi.mock('../../../resources/Img/Img');
vi.mock('../../../shared/ResourceLoader/ResourceLoader');
vi.mock('../../../controllers/Player/Player');

describe('complements: FluxIndex List', () => {
	const timers = new Timers();
	const displaySize: Size = new Size({ width: 640, height: 360 });
	const mouseOver: Ref<boolean> = ref(false);

	beforeEach(() => {
		mouseOver.value = false;
	});

	it('mounts properly', () => {
		const player = new Player(vueFluxConfig, timers, emit);

		expect(() => {
			mount(List, {
				props: {
					displaySize,
					player,
					mouseOver,
				},
			});
		}).not.toThrow();
	});

	it('is not visible by default', async () => {
		mouseOver.value = true;
		const player = new Player(vueFluxConfig, timers, emit);

		const wrapper = mount(List, {
			props: {
				displaySize,
				player,
				mouseOver,
			},
		});

		expect(wrapper.html().includes('nav class=""')).toBeTruthy();
	});

	it('shows the list when button clicked', async () => {
		mouseOver.value = true;
		const player = new Player(vueFluxConfig, timers, emit);

		const wrapper = mount(List, {
			props: {
				displaySize,
				player,
				mouseOver,
			},
		});

		await wrapper.vm.show();

		expect(wrapper.html().includes('nav class="visible"')).toBeTruthy();
	});

	it('does nothing if clicked resource is the same as current resource', async () => {
		mouseOver.value = true;
		const player = new Player(vueFluxConfig, timers, emit);
		const resources = ResourceFactory.create(10);
		await player.resources.update(resources, 10, displaySize);

		setCurrentResource(player);

		const wrapper = mount(List, {
			props: {
				displaySize,
				player,
				mouseOver,
			},
		});

		await wrapper.find({ ref: '$list' }).findAllComponents(Thumb)[0].trigger('click');

		expect(player.show).not.toHaveBeenCalled();
	});
});


===== FILE: src/complements/FluxIndex/List/List.vue =====
<script setup lang="ts">
	import { type Ref, computed, nextTick, ref } from 'vue';
	import { Player } from '../../../controllers';
	import Thumb from '../Thumb/Thumb.vue';
	import { Size } from '../../../shared';
	import useThumbs from '../Thumb/useThumbs';

	export interface Props {
		displaySize: Size;
		player: Player;
		mouseOver?: Ref<boolean>;
	}

	const props = withDefaults(defineProps<Props>(), {
		mouseOver: undefined,
	});

	const $list: Ref<null | HTMLUListElement> = ref(null);

	const animationTime = 500;
	const visible: Ref<boolean> = ref(false);

	const listClass = computed<string[]>(() => {
		const classes = [];

		if (visible.value) {
			classes.push('visible');
		}

		return classes;
	});

	async function show() {
		if ($list.value === null) {
			return;
		}

		props.player.stop();
		visible.value = true;

		await nextTick();

		// eslint-disable-next-line @typescript-eslint/no-unused-expressions
		$list.value.clientHeight;
		$list.value.style.marginTop = '0';
	}

	function hide(resourceIndex: null | number) {
		if ($list.value === null) {
			return;
		}

		if (props.player.resource.current?.index === resourceIndex) {
			return;
		}

		if (props.mouseOver !== undefined) {
			// eslint-disable-next-line @typescript-eslint/no-unused-expressions
			$list.value.clientHeight;
			$list.value.style.marginTop = '100%';
		}

		setTimeout(() => {
			visible.value = false;

			if (resourceIndex !== null) {
				props.player.show(resourceIndex);
			}
		}, animationTime);
	}

	const thumbs = useThumbs(props.displaySize, props.player);

	defineExpose({ show });
</script>

<template>
	<nav :class="listClass" @click="hide(null)">
		<ul ref="$list">
			<Thumb
				v-for="(rsc, index) in player.resources!.list"
				:key="index"
				:rsc="rsc.resource"
				:size="thumbs.size"
				:class="thumbs.getClass(index)"
				@click="hide(index)"
			/>
		</ul>
	</nav>
</template>

<style lang="scss">
	.vue-flux .flux-index {
		nav {
			position: absolute;
			top: 0;
			left: 0;
			right: 0;
			bottom: 0;
			display: block;
			margin: 0;
			overflow: hidden;
			visibility: hidden;
		}

		nav.visible {
			z-index: 101;
			visibility: visible;
		}

		ul {
			display: block;
			height: 100%;
			margin: 0;
			margin-top: 100%;
			padding: 24px 0 0 24px;
			list-style-type: none;
			text-align: center;
			overflow-y: auto;
			background-color: black;
			transition: all 0.5s linear;
			font-size: 0;
		}
	}
</style>


===== FILE: src/complements/FluxIndex/Thumb/Thumb.vue =====
<script setup lang="ts">
	import type { Ref } from 'vue';
	import { Resource } from '../../../resources';
	import { Size } from '../../../shared';

	export interface Props {
		rsc: Resource;
		size: Ref<Size>;
	}

	defineProps<Props>();
</script>

<template>
	<li>
		<component
			:is="rsc.transition.component"
			:rsc="rsc"
			:size="size.value"
			:title="rsc.caption"
		/>
	</li>
</template>

<style lang="scss">
	.vue-flux .flux-index li {
		position: relative;
		display: inline-block;
		box-sizing: content-box;
		margin: 0 24px 24px 0;
		cursor: pointer;
		transition: all 0.3s ease;

		&:hover {
			box-shadow: 0px 0px 3px 2px rgba(255, 255, 255, 0.6);
		}

		&.current {
			cursor: auto;
			border: 1px solid white;
			box-shadow: none;
		}
	}
</style>


===== FILE: src/complements/FluxIndex/Thumb/useThumbs.ts =====
import { computed } from 'vue';
import { Player } from '../../../controllers';
import { Size } from '../../../shared';

export default function useThumbs(displaySize: Size, player: Player) {
	const size = computed<Size>(() => {
		let { width, height } = displaySize.toValue();

		width = width! / 4.2;
		height = (width * 90) / 160;

		if (width > 160) {
			width = 160;
			height = 90;
		}

		return new Size({
			width,
			height,
		});
	});

	function getClass(index: number) {
		const { current } = player.resource;

		if (current === null) {
			return '';
		}

		if (current.index !== index) {
			return '';
		}

		return 'current';
	}

	return { size, getClass };
}


===== FILE: src/complements/FluxPagination/FluxPagination.vue =====
<script setup lang="ts">
	import { computed } from 'vue';
	import type { ResourceWithOptions } from '../../resources';
	import { Player } from '../../controllers';

	export interface Props {
		player: Player;
	}

	const props = defineProps<Props>();

	const {
		player: { resources, resource, transition },
	} = props;

	const visible = computed<boolean>(() => resources.list.length > 0);

	const getTitle = (rsc: ResourceWithOptions) => {
		return rsc.resource.caption;
	};

	const getCssClass = (index: number, itemCLass: string) => {
		const classes = [itemCLass];

		let active = resource.current?.index === index;

		if (transition.current !== null) {
			active = false;
		}

		if (active === true) {
			classes.push('active');
		}

		return classes;
	};
</script>

<template>
	<nav v-if="visible" class="flux-pagination">
		<ul>
			<li v-for="(rsc, index) in player.resources.list" :key="index">
				<slot
					:index="index"
					:rsc="rsc"
					:title="getTitle(rsc)"
					:css-class="getCssClass(index, 'custom-pagination-item')"
				>
					<span
						:title="getTitle(rsc)"
						:class="getCssClass(index, 'pagination-item')"
						@click="player.show(index)"
					/>
				</slot>
			</li>
		</ul>
	</nav>
</template>

<style lang="scss">
	.vue-flux .flux-pagination {
		flex: none;

		ul {
			display: flex;
			flex-wrap: wrap;
			justify-content: center;
			margin: 0;
			padding: 0;
			list-style-type: none;
			text-align: center;
			position: relative;
		}

		li {
			display: block;
			margin: 0 1% 1.5% 1%;
			cursor: pointer;
			width: 2%;
			height: 0;
			min-width: 10px;
			min-height: 10px;
			padding-bottom: 2%;
			position: relative;
			box-sizing: border-box;
		}

		.pagination-item {
			position: absolute;
			top: 0;
			left: 0;
			right: 0;
			bottom: 0;
			box-sizing: border-box;
			border: 2px solid #fff;
			border-radius: 50%;
			background-color: rgba(0, 0, 0, 0.7);
			transition:
				background-color 0.2s ease-in,
				border 0.2s ease-in;

			&:hover {
				border-color: black;
				background-color: white;
			}

			&.active {
				border-color: white;
				background-color: white;
			}
		}
	}
</style>


===== FILE: src/complements/FluxPreloader/FluxPreloader.vue =====
<script setup lang="ts">
	import type { Ref } from 'vue';
	import { ResourceLoader } from '../../shared';

	export interface Props {
		loader: Ref<null | ResourceLoader>;
	}

	defineProps<Props>();
</script>

<template>
	<div class="preloader">
		<slot
			:loader="loader"
			:preloading="loader.value?.preLoading.length"
			:lazyloading="loader.value?.lazyLoading.length"
			:pct="loader.value?.progress"
		>
			<div v-if="loader.value?.preLoading.length" class="spinner">
				<div class="pct">{{ loader.value?.progress }}%</div>
				<div class="border" />
			</div>
		</slot>
	</div>
</template>

<style lang="scss">
	.vue-flux .preloader {
		position: absolute;
		top: 0;
		right: 0;
		bottom: 0;
		left: 0;
		z-index: -1;

		.spinner {
			position: absolute;
			top: 50%;
			left: 50%;
			margin-top: -40px;
			margin-left: -40px;
			width: 80px;
			height: 80px;
			z-index: 14;

			.pct {
				position: absolute;
				right: 0;
				left: 0;
				height: 80px;
				line-height: 80px;
				text-align: center;
				font-weight: bold;
				z-index: 1;
			}

			.border {
				box-sizing: border-box;
				width: 100%;
				height: 100%;
				border: 14px solid #f3f3f3;
				border-top-color: #3498db;
				border-bottom-color: #3498db;
				border-radius: 50%;
				background-color: #f3f3f3;
				animation: spin 2s linear infinite;
			}
		}

		@keyframes spin {
			0% {
				transform: rotate(0deg);
			}
			100% {
				transform: rotate(360deg);
			}
		}
	}
</style>


===== FILE: src/complements/__test__/PlayerHelper.ts =====
import type { VueFluxConfig } from '../../components/VueFlux/types';
import { Player } from '../../controllers/Player';
import type { ResourceIndex } from '../../repositories/Resources/types';
import type { TransitionIndex } from '../../repositories/Transitions/types';
import { Img } from '../../resources';
import { Blinds2D } from '../../transitions';

export const vueFluxConfig = {
	allowFullscreen: false,
	allowToSkipTransition: true,
	aspectRatio: '16:9',
	autohideTime: 2500,
	autoplay: false,
	bindKeys: false,
	delay: 5000,
	enableGestures: false,
	infinite: true,
	lazyLoad: true,
	lazyLoadAfter: 5,
} as VueFluxConfig;

export function setCurrentResource(player: Player, caption?: string) {
	player.resource.current = {
		index: 0,
		rsc: new Img('url', caption),
		options: {},
	} as ResourceIndex;
}

export function setCurrentTransition(player: Player) {
	player.transition.current = {
		index: 0,
		component: Blinds2D,
		options: {},
	} as TransitionIndex;
}


===== FILE: src/complements/index.ts =====
export { default as FluxCaption } from './FluxCaption/FluxCaption.vue';
export { default as FluxControls } from './FluxControls/FluxControls.vue';
export { default as FluxIndex } from './FluxIndex/FluxIndex.vue';
export { default as FluxPagination } from './FluxPagination/FluxPagination.vue';
export { default as FluxPreloader } from './FluxPreloader/FluxPreloader.vue';


===== FILE: src/components/FluxButton/FluxButton.test.ts =====
import FluxButton from './FluxButton.vue';
import { mount } from '@vue/test-utils';

describe('component: FluxButton', () => {
	it('should mount properly', () => {
		const nextLine = '<polyline points="36,18 78,50 36,82" />';

		const wrapper = mount(FluxButton, {
			slots: {
				default: nextLine,
			},
		});

		expect(
			wrapper.html().includes('<polyline points="36,18 78,50 36,82"')
		).toBeTruthy();
	});
});


===== FILE: src/components/FluxButton/FluxButton.vue =====
<template>
	<button type="button" class="flux-button" style="outline: 0">
		<svg
			viewBox="0 0 100 100"
			xmlns="http://www.w3.org/2000/svg"
			version="1.1"
		>
			<circle cx="50" cy="50" r="50" />
			<svg viewBox="-20 -20 140 140">
				<slot />
			</svg>
		</svg>
	</button>
</template>

<style lang="scss">
	.vue-flux .flux-button {
		padding: 0;
		width: 6%;
		min-width: 26px;
		min-height: 26px;
		max-width: 40px;
		max-height: 40px;
	}

	.flux-button {
		border: 0;
		cursor: pointer;
		background-color: transparent;

		&:hover {
			> svg {
				line,
				polyline {
					stroke: yellow;
				}

				rect,
				polygon {
					fill: yellow;
				}
			}
		}

		> svg {
			width: 100%;

			> circle {
				fill: rgba(0, 0, 0, 0.7);
			}

			line,
			polyline,
			rect,
			polygon {
				stroke-linecap: round;
				stroke-linejoin: round;
				stroke: white;
				stroke-width: 14;
				fill: none;
			}

			rect,
			polygon {
				fill: white;
				stroke-width: 0;
			}
		}
	}
</style>


===== FILE: src/components/FluxCube/FluxCube.vue =====
<script setup lang="ts">
	import { ref, reactive, computed, onBeforeUpdate } from 'vue';
	import useComponent from '../useComponent';
	import type { FluxCubeProps, SidesComponents, Turn } from './types';
	import { Size } from '../../shared';
	import type { ComponentStyles, FluxComponent } from '../types';
	import SideTransformFactory from './factories/SideTransformFactory';
	import CubeFactory from './factories/CubeFactory';
	import Sides from './Sides';

	const props = withDefaults(defineProps<FluxCubeProps>(), {
		rscs: () => ({}),
		colors: () => ({}),
		offsets: () => ({}),
		depth: 0,
		viewSize: () => new Size(),
	});

	const $el = ref(null);

	const transformOrigin = computed(() =>
		props.origin !== undefined ? props.origin : `center center -${props.depth / 2}px`,
	);

	const componentStyles: ComponentStyles = reactive({
		base: {
			transformStyle: 'preserve-3d',
			transformOrigin: transformOrigin,
		},
	});

	const { style, setCss, transform, show, hide } = useComponent($el, props, componentStyles);

	const sideTransformFactory = computed(
		() => new SideTransformFactory(props.depth, props.size, props.viewSize),
	);

	const sides = computed(() =>
		CubeFactory.getSidesProps(
			sideTransformFactory.value,
			props.color,
			props.colors,
			props.rsc,
			props.rscs,
			props.offset,
			props.offsets,
		),
	);

	const $sides: SidesComponents = reactive({});

	onBeforeUpdate(() => {
		Object.assign($sides, {
			[Sides.front]: undefined,
			[Sides.back]: undefined,
			[Sides.left]: undefined,
			[Sides.right]: undefined,
			[Sides.top]: undefined,
			[Sides.bottom]: undefined,
		});
	});

	const turn = (turn: Turn) =>
		transform({ transform: sideTransformFactory.value.getRotate(turn) });

	defineExpose({
		setCss,
		transform,
		show,
		hide,
		turn,
	});
</script>

<template>
	<div ref="$el" class="flux-cube" :style="style">
		<component
			:is="side!.component"
			v-for="side in sides"
			:ref="(el: FluxComponent) => ($sides[side!.name as keyof typeof Sides] = el)"
			:key="side!.name"
			v-bind="side"
		/>
	</div>
</template>


===== FILE: src/components/FluxCube/Sides.ts =====
enum Sides {
	front = 'front',
	back = 'back',
	left = 'left',
	right = 'right',
	top = 'top',
	bottom = 'bottom',
}

export default Sides;


===== FILE: src/components/FluxCube/Turns.ts =====
enum Turns {
	front = 'front',
	back = 'back',
	backr = 'backr',
	backl = 'backl',
	left = 'left',
	right = 'right',
	top = 'top',
	bottom = 'bottom',
}

export default Turns;


===== FILE: src/components/FluxCube/__mocks__/FluxCube.vue =====
<script setup lang="ts">
	import { ref, computed } from 'vue';
	import { vi } from 'vitest';
	import Side from './Side.vue';
	import type { FluxCubeProps, Turn } from '../types';
	import { Size } from '../../../shared';
	import CubeFactory from '../factories/CubeFactory';
	import SideTransformFactory from '../factories/SideTransformFactory';

	const props = withDefaults(defineProps<FluxCubeProps>(), {
		rscs: () => ({}),
		colors: () => ({}),
		offsets: () => ({}),
		depth: 0,
		viewSize: () => new Size(),
	});

	const $el = ref(null);

	const sideTransformFactory = computed(
		() => new SideTransformFactory(props.depth, props.size, props.viewSize),
	);

	const sides = computed(() =>
		CubeFactory.getSidesProps(
			sideTransformFactory.value,
			props.color,
			props.colors,
			props.rsc,
			props.rscs,
			props.offset,
			props.offsets,
		),
	);

	const turn = vi
		.fn()
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		.mockImplementation((_turn: Turn) => vi.fn());

	defineExpose({
		setCss: vi.fn(),
		transform: vi.fn(),
		show: vi.fn(),
		hide: vi.fn(),
		turn,
	});
</script>

<template>
	<div ref="$el" class="flux-cube">
		<Side :is="side!.component" v-for="side in sides" :key="side!.name" v-bind="side" />
	</div>
</template>


===== FILE: src/components/FluxCube/__mocks__/Side.vue =====
<script setup lang="ts">
	import { ref, type Ref } from 'vue';
	import { vi } from 'vitest';

	const $el: Ref<null | HTMLDivElement> = ref(null);

	defineExpose({
		setCss: vi.fn(),
		transform: vi.fn(),
		show: vi.fn(),
		hide: vi.fn(),
	});
</script>

<template>
	<div ref="$el" />
</template>


===== FILE: src/components/FluxCube/factories/CubeFactory.test.ts =====
import { Img } from '../../../resources';
import { Position, Size } from '../../../shared';
import { type SideProps } from '../types';
import CubeFactory from './CubeFactory';
import CubeSideFactory from './CubeSideFactory';
import SideTransformFactory from './SideTransformFactory';

describe('factory: CubeFactory', () => {
	let rsc, rscs, color, colors, offset, offsets;

	const depth = 160;
	const size = new Size({
		width: 640,
		height: 360,
	});
	const viewSize = new Size();

	const sideTransformFactory = new SideTransformFactory(depth, size, viewSize);

	vi.spyOn(CubeSideFactory, 'getProps').mockImplementation(() => ({}) as SideProps);

	beforeEach(() => {
		vi.clearAllMocks();
	});

	it('generates a cube using a color', () => {
		color = '#ccc';

		const cubeProps = CubeFactory.getSidesProps(sideTransformFactory, color);

		expect(CubeSideFactory.getProps).toHaveBeenCalledTimes(6);
		expect(Object.keys(cubeProps)).toHaveLength(6);
	});

	it('generates a cube using a colors', () => {
		colors = {
			top: '#ccc',
			left: '#ccc',
			back: '#ccc',
		};

		const cubeProps = CubeFactory.getSidesProps(sideTransformFactory, undefined, colors);

		expect(CubeSideFactory.getProps).toHaveBeenCalledTimes(3);
		expect(Object.keys(cubeProps)).toHaveLength(3);
	});

	it('generates a cube using a rsc', () => {
		rsc = new Img('url', 'caption');

		const cubeProps = CubeFactory.getSidesProps(sideTransformFactory, undefined, undefined, rsc);

		expect(CubeSideFactory.getProps).toHaveBeenCalledTimes(6);
		expect(Object.keys(cubeProps)).toHaveLength(6);
	});

	it('generates a cube using a rscs', () => {
		rscs = {
			bottom: new Img('url', 'caption'),
			right: new Img('url', 'caption'),
			front: new Img('url', 'caption'),
		};

		const cubeProps = CubeFactory.getSidesProps(
			sideTransformFactory,
			undefined,
			undefined,
			undefined,
			rscs,
		);

		expect(CubeSideFactory.getProps).toHaveBeenCalledTimes(3);
		expect(Object.keys(cubeProps)).toHaveLength(3);
	});

	it('generates a cube using a color with offset', () => {
		color = '#ccc';
		offset = new Position({ top: 160, left: 80 });

		const cubeProps = CubeFactory.getSidesProps(
			sideTransformFactory,
			color,
			undefined,
			undefined,
			undefined,
			offset,
		);

		expect(CubeSideFactory.getProps).toHaveBeenCalledTimes(6);
		expect(Object.keys(cubeProps)).toHaveLength(6);
	});

	it('generates a cube using a colors with offsets', () => {
		colors = {
			top: '#ccc',
			left: '#ccc',
			back: '#ccc',
		};

		offsets = {
			top: new Position({ top: 160, left: 80 }),
			left: new Position({ top: 160, left: 80 }),
			back: new Position({ top: 160, left: 80 }),
		};

		const cubeProps = CubeFactory.getSidesProps(
			sideTransformFactory,
			undefined,
			colors,
			undefined,
			undefined,
			undefined,
			offsets,
		);

		expect(CubeSideFactory.getProps).toHaveBeenCalledTimes(3);
		expect(Object.keys(cubeProps)).toHaveLength(3);
	});
});


===== FILE: src/components/FluxCube/factories/CubeFactory.ts =====
import type { Side, SidesColors, SidesResources, SidesOffsets, SidesProps } from '../types';
import CubeSideFactory from './CubeSideFactory';
import SideTransformFactory from './SideTransformFactory';
import { Position } from '../../../shared';
import Sides from '../Sides';
import { Resource } from '../../../resources';
import type { CSSProperties } from 'vue';

function isSideDefined(side: Side, colors?: SidesColors, rscs?: SidesResources) {
	if (colors && colors[side]) {
		return true;
	}

	if (rscs && rscs[side]) {
		return true;
	}

	return false;
}

function getDefinedSides(
	color?: CSSProperties['color'],
	colors?: SidesColors,
	rsc?: Resource,
	rscs?: SidesResources,
) {
	const sides = Object.values(Sides);

	if (color || rsc) {
		return sides;
	}

	return Object.values(Sides).filter((side) => isSideDefined(side, colors, rscs));
}

export default class CubeFactory {
	static getSidesProps(
		sideTransformFactory: SideTransformFactory,
		color?: CSSProperties['color'],
		colors?: SidesColors,
		rsc?: Resource,
		rscs?: SidesResources,
		offset?: Position,
		offsets?: SidesOffsets,
	) {
		const sides = getDefinedSides(color, colors, rsc, rscs);
		const props: SidesProps = {};

		sides.forEach((side: Side) => {
			props[side] = CubeSideFactory.getProps(
				sideTransformFactory,
				side,
				colors && colors[side] ? colors[side] : color,
				rscs && rscs[side] ? rscs[side] : rsc,
				offsets && offsets[side] ? offsets[side] : offset,
			);
		});

		return props;
	}
}


===== FILE: src/components/FluxCube/factories/CubeSideFactory.ts =====
import { Position } from '../../../shared';
import { Resource } from '../../../resources';
import type { Side, SideProps } from '../types';
import SideTransformFactory from './SideTransformFactory';
import { FluxImage } from '../../';
import type { CSSProperties } from 'vue';

export default class CubeSideFactory {
	static getProps(
		sideTransformFactory: SideTransformFactory,
		side: Side,
		color?: CSSProperties['color'],
		rsc?: Resource,
		offset?: Position,
	) {
		const { depth, size, viewSize } = sideTransformFactory;

		const props: SideProps = {
			name: side,
			component: rsc ? rsc.transition.component : FluxImage,
			color: color,
			rsc: rsc,
			size: size.clone(),
			viewSize: viewSize.clone(),
			offset: offset,
			style: {
				position: 'absolute',
				transform: sideTransformFactory.getSideCss(side),
				backfaceVisibility: 'hidden',
			},
		};

		if (['left', 'right'].includes(side)) {
			props.viewSize.width.value = depth;
			props.size.width.value = depth;
		}

		if (['top', 'bottom'].includes(side)) {
			props.viewSize.height.value = depth;
			props.size.height.value = depth;
		}

		return props;
	}
}


===== FILE: src/components/FluxCube/factories/SideTransformFactory.test.ts =====
import { Size } from '../../../shared';
import Turns from '../Turns';
import SideTransformFactory from './SideTransformFactory';

describe('factory: SideTransformFactory', () => {
	const depth = 160;
	const size = new Size({
		width: 640,
		height: 360,
	});
	const viewSize = new Size();
	const sideTransformFactory = new SideTransformFactory(depth, size, viewSize);

	it('should get the proper rotate angles', () => {
		const expectations = {
			front: 'rotateX(0deg) rotateY(0deg)',
			right: 'rotateX(0deg) rotateY(90deg)',
			left: 'rotateX(0deg) rotateY(-90deg)',
			top: 'rotateX(90deg) rotateY(0deg)',
			bottom: 'rotateX(-90deg) rotateY(0deg)',
			back: 'rotateX(0deg) rotateY(180deg)',
			backl: 'rotateX(0deg) rotateY(-180deg)',
			backr: 'rotateX(0deg) rotateY(180deg)',
		};

		Object.values(Turns).forEach((turn) => {
			expect(sideTransformFactory.getRotate(turn)).toBe(expectations[turn]);
		});
	});

	it('should get proper translate coordinates', () => {
		const expectations = {
			front: 'translate3d(0%, 0%, 0px)',
			right: 'translate3d(50%, 0%, 560px)',
			left: 'translate3d(-50%, 0%, 80px)',
			top: 'translate3d(0%, -50%, 80px)',
			bottom: 'translate3d(0%, 50%, 280px)',
			back: 'translate3d(0%, 0%, 160px)',
			backl: 'translate3d(0%, 0%, 160px)',
			backr: 'translate3d(0%, 0%, 160px)',
		};

		Object.values(Turns).forEach((turn) => {
			expect(sideTransformFactory.getTranslate(turn)).toBe(
				expectations[turn]
			);
		});
	});

	it('should get each side style', () => {
		const expectations = {
			front: 'rotateX(0deg) rotateY(0deg) translate3d(0%, 0%, 0px)',
			right: 'rotateX(0deg) rotateY(90deg) translate3d(50%, 0%, 560px)',
			left: 'rotateX(0deg) rotateY(-90deg) translate3d(-50%, 0%, 80px)',
			top: 'rotateX(90deg) rotateY(0deg) translate3d(0%, -50%, 80px)',
			bottom: 'rotateX(-90deg) rotateY(0deg) translate3d(0%, 50%, 280px)',
			back: 'rotateX(0deg) rotateY(180deg) translate3d(0%, 0%, 160px)',
			backl: 'rotateX(0deg) rotateY(-180deg) translate3d(0%, 0%, 160px)',
			backr: 'rotateX(0deg) rotateY(180deg) translate3d(0%, 0%, 160px)',
		};

		Object.values(Turns).forEach((turn) => {
			expect(sideTransformFactory.getSideCss(turn)).toBe(expectations[turn]);
		});
	});
});


===== FILE: src/components/FluxCube/factories/SideTransformFactory.ts =====
import { type Ref, computed } from 'vue';
import { Size } from '../../../shared';
import type { Side, Turn } from '../types';

const rotate: {
	x: {
		[key: string]: string;
	};
	y: {
		[key: string]: string;
	};
} = {
	x: {
		top: '90',
		bottom: '-90',
	},

	y: {
		back: '180',
		backr: '180',
		backl: '-180',
		left: '-90',
		right: '90',
	},
};

const translate: {
	x: {
		[key: string]: string;
	};
	y: {
		[key: string]: string;
	};
} = {
	x: {
		left: '-50',
		right: '50',
	},

	y: {
		top: '-50',
		bottom: '50',
	},
};

export default class SideTransformFactory {
	depth: number;
	size: Size;
	viewSize: Size;
	translateZ: Ref<{ [key: string]: number }> = computed(() => {
		const halfDepth = this.depth / 2;

		const { width, height } = this.size.toValue();
		const { width: viewWidth, height: viewHeight } = this.viewSize.toValue();

		return {
			front: 0,
			back: this.depth,
			backr: this.depth,
			backl: this.depth,
			left: halfDepth,
			right: (viewWidth ?? width!) - halfDepth,
			top: halfDepth,
			bottom: (viewHeight ?? height!) - halfDepth,
		};
	});

	constructor(depth: number, size: Size, viewSize: Size) {
		this.depth = depth;
		this.size = size;
		this.viewSize = viewSize;
	}

	public getRotate(turn: Side | Turn) {
		const rx = rotate.x[turn] ?? '0';
		const ry = rotate.y[turn] ?? '0';

		return `rotateX(${rx}deg) rotateY(${ry}deg)`;
	}

	public getTranslate(side: Side | Turn) {
		const tx = translate.x[side] ?? '0';
		const ty = translate.y[side] ?? '0';
		const tz = this.translateZ.value[side]!.toString();

		return `translate3d(${tx}%, ${ty}%, ${tz}px)`;
	}

	public getSideCss(side: Side | Turn) {
		return `${this.getRotate(side)} ${this.getTranslate(side)}`;
	}
}


===== FILE: src/components/FluxCube/index.ts =====
export { default as FluxCube } from './FluxCube.vue';
export { default as Sides } from './Sides';
export { default as Turns } from './Turns';


===== FILE: src/components/FluxCube/types.ts =====
import type { CSSProperties, Component } from 'vue';
import { Resource } from '../../resources';
import { Position, Size } from '../../shared';
import type { ComponentProps, FluxComponent } from '../types';
import Sides from './Sides';
import Turns from './Turns';

export interface FluxCubeProps extends ComponentProps {
	colors?: SidesColors;
	rscs?: SidesResources;
	offsets?: SidesOffsets;
	depth?: number;
	origin?: string;
}

export type Side = keyof typeof Sides;

export type Turn = keyof typeof Turns;

export interface SidesColors {
	[Sides.front]?: string;
	[Sides.back]?: string;
	[Sides.left]?: string;
	[Sides.right]?: string;
	[Sides.top]?: string;
	[Sides.bottom]?: string;
}

export interface SidesResources {
	[Sides.front]?: Resource;
	[Sides.back]?: Resource;
	[Sides.left]?: Resource;
	[Sides.right]?: Resource;
	[Sides.top]?: Resource;
	[Sides.bottom]?: Resource;
}

export interface SidesOffsets {
	[Sides.front]?: Position;
	[Sides.back]?: Position;
	[Sides.left]?: Position;
	[Sides.right]?: Position;
	[Sides.top]?: Position;
	[Sides.bottom]?: Position;
}

export interface SideProps {
	name: Side;
	component: Component;
	rsc?: Resource;
	size: Size;
	viewSize: Size;
	color?: CSSProperties['color'];
	offset?: Position;
	style: CSSProperties;
}

export interface SidesProps {
	[Sides.front]?: SideProps;
	[Sides.back]?: SideProps;
	[Sides.left]?: SideProps;
	[Sides.right]?: SideProps;
	[Sides.top]?: SideProps;
	[Sides.bottom]?: SideProps;
}

export interface SidesComponents {
	[Sides.front]?: FluxComponent;
	[Sides.back]?: FluxComponent;
	[Sides.left]?: FluxComponent;
	[Sides.right]?: FluxComponent;
	[Sides.top]?: FluxComponent;
	[Sides.bottom]?: FluxComponent;
}


===== FILE: src/components/FluxGrid/FluxGrid.vue =====
<script setup lang="ts">
	import { ref, reactive, computed, type Ref, onBeforeUpdate } from 'vue';
	import useComponent from '../useComponent';
	import type { FluxGridProps } from './types';
	import { FluxCube } from '../';
	import type { ComponentStyles, FluxComponent } from '../types';
	import { GridFactory, getRowNumber, getColNumber } from './factories';

	const props = withDefaults(defineProps<FluxGridProps>(), {
		rows: 1,
		cols: 1,
		depth: 0,
	});

	const $el: Ref<null | HTMLDivElement> = ref(null);

	const componentStyles: ComponentStyles = reactive({
		base: {
			position: 'relative',
		},
	});

	const { style, setCss, show, hide } = useComponent($el, props, componentStyles);

	const component = computed(() =>
		props.rscs !== undefined ? FluxCube : props.rsc!.transition.component,
	);

	const tiles = computed(() => GridFactory.getTilesProps(props));

	const $tiles: Ref<FluxComponent[]> = ref([]);

	onBeforeUpdate(() => {
		$tiles.value = [];
	});

	const transform = <T,>(cb: (tile: T, index: number) => void) => {
		$tiles.value.forEach((tile: unknown, index: number) => cb(tile as T, index));
	};

	defineExpose({
		setCss,
		transform,
		show,
		hide,
		getRowNumber,
		getColNumber,
	});
</script>

<template>
	<div ref="$el" class="flux-grid" :style="style">
		<component
			:is="component"
			v-for="(tile, index) in tiles"
			:ref="(el: FluxComponent) => $tiles.push(el)"
			:key="index"
			v-bind="tile"
		/>
	</div>
</template>


===== FILE: src/components/FluxGrid/__mocks__/FluxGrid.vue =====
<script setup lang="ts">
	import { onBeforeUpdate, ref, type Ref } from 'vue';
	import { vi } from 'vitest';
	import Tile from './Tile.vue';
	import type { FluxGridProps } from '../types';
	import { getRowNumber, getColNumber } from '../factories';

	const props = withDefaults(defineProps<FluxGridProps>(), {
		rows: 1,
		cols: 1,
		depth: 0,
	});

	const $el: Ref<null | HTMLDivElement> = ref(null);

	const numTiles = props.rows * props.cols;
	const $tiles: Ref<InstanceType<typeof Tile>[]> = ref([]);

	onBeforeUpdate(() => {
		$tiles.value = [];
	});

	const transform = vi
		.fn()
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		.mockImplementation((cb: (tile: any, index: number) => void) => {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			$tiles.value.forEach((tile: any, index: number) => cb(tile, index));
		});

	defineExpose({
		setCss: vi.fn(),
		transform,
		show: vi.fn(),
		hide: vi.fn(),
		getRowNumber: vi
			.fn()
			.mockImplementation((index: number, numCols: number) => getRowNumber(index, numCols)),
		getColNumber: vi
			.fn()
			.mockImplementation((index: number, numCols: number) => getColNumber(index, numCols)),
		$tiles,
	});
</script>

<template>
	<div ref="$el" class="flux-grid">
		<Tile v-for="index in numTiles" :ref="(el: any) => $tiles.push(el)" :key="index" />
	</div>
</template>


===== FILE: src/components/FluxGrid/__mocks__/Tile.vue =====
<script setup lang="ts">
	import { ref, type Ref } from 'vue';
	import { vi } from 'vitest';

	const $el: Ref<null | HTMLDivElement> = ref(null);

	defineExpose({
		turn: vi.fn(),
		setCss: vi.fn(),
		transform: vi.fn(),
		show: vi.fn(),
		hide: vi.fn(),
		getRowNumber: vi.fn(),
		getColNumber: vi.fn(),
	});
</script>

<template>
	<div ref="$el" />
</template>


===== FILE: src/components/FluxGrid/factories/GridFactory.ts =====
import { Size } from '../../../shared';
import GridTileFactory from './GridTileFactory';
import type { FluxGridProps, FluxGridTileProps } from '../types';

export default class GridFactory {
	static getTilesProps(props: FluxGridProps) {
		const { rows, cols, size, color, colors, rsc, rscs, depth } = props;

		const numRows = Math.ceil(rows!);
		const numCols = Math.ceil(cols!);

		const grid = {
			numRows,
			numCols,
			numTiles: numRows * numCols,
			size,
			depth: depth!,
			color,
			colors,
			rsc,
			rscs,
		};

		const tile = {
			number: 0,
			size: new Size({
				width: Math.floor(size.width.value! / numCols),
				height: Math.floor(size.height.value! / numRows),
			}),
			css: props.tileCss,
		};

		const tilesProps: FluxGridTileProps[] = [];

		for (let tileNumber = 0; tileNumber < grid.numTiles; tileNumber++) {
			tile.number = tileNumber;
			tilesProps.push(GridTileFactory.getProps(grid, tile));
		}

		return tilesProps;
	}
}


===== FILE: src/components/FluxGrid/factories/GridTileFactory.ts =====
import type { CSSProperties } from 'vue';
import { Resource } from '../../../resources';
import { Size, Position } from '../../../shared';
import type { SidesColors, SidesResources } from '../../FluxCube/types';
import type { FluxGridTileProps } from '../types';

export function getRowNumber(tileNumber: number, numCols: number) {
	return Math.floor(tileNumber / numCols);
}

export function getColNumber(tileNumber: number, numCols: number) {
	return tileNumber % numCols;
}

export default class GridTileFactory {
	static getProps(
		grid: {
			numRows: number;
			numCols: number;
			numTiles: number;
			size: Size;
			depth: number;
			color?: CSSProperties['color'];
			colors?: SidesColors;
			rsc?: Resource;
			rscs?: SidesResources;
		},
		tile: {
			number: number;
			size: Size;
			css?: CSSProperties;
		},
	) {
		let { width, height } = tile.size.toValue();

		const row = getRowNumber(tile.number, grid.numCols);
		const col = getColNumber(tile.number, grid.numCols);

		const props: FluxGridTileProps = {
			color: grid.color,
			colors: grid.colors,
			rsc: grid.rsc,
			rscs: grid.rscs,
			size: grid.size,
			depth: grid.depth,
			offset: new Position({
				top: row * height!,
				left: col * width!,
			}),
		};

		if (row + 1 === grid.numRows) {
			height = grid.size.height.value! - row * height!;
		}

		if (col + 1 === grid.numCols) {
			width = grid.size.width.value! - col * width!;
		}

		props.viewSize = new Size({
			width,
			height,
		});

		props.css = {
			...tile.css,
			position: 'absolute',
			...props.offset.toPx(),
			zIndex:
				tile.number + 1 < grid.numTiles / 2 ? tile.number + 1 : grid.numTiles - tile.number,
		};

		return props;
	}
}


===== FILE: src/components/FluxGrid/factories/index.ts =====
export { default as GridFactory } from './GridFactory';
export {
	default as GridTileFactory,
	getRowNumber,
	getColNumber,
} from './GridTileFactory';


===== FILE: src/components/FluxGrid/types.ts =====
import type { CSSProperties } from 'vue';
import { Position, Size } from '../../shared';
import { Resource } from '../../resources';
import type { SidesColors, SidesResources } from '../FluxCube/types';
import type { ComponentProps } from '../types';

export interface FluxGridProps extends ComponentProps {
	colors?: SidesColors;
	rscs?: SidesResources;
	rows?: number;
	cols?: number;
	depth?: number;
	tileCss?: CSSProperties;
}

export interface FluxGridTileProps {
	color?: CSSProperties['color'];
	colors?: SidesColors;
	rsc?: Resource;
	rscs?: SidesResources;
	size: Size;
	depth: number;
	offset: Position;
	viewSize?: Size;
	css?: CSSProperties;
}


===== FILE: src/components/FluxImage/FluxImage.vue =====
<script setup lang="ts">
	import { ref, type Ref, reactive, computed, type CSSProperties } from 'vue';
	import useComponent from '../useComponent';
	import type { FluxImageProps } from './types';
	import type { ComponentStyles } from '../types';
	import { Statuses } from '../../resources';

	const props = defineProps<FluxImageProps>();

	const $el: Ref<null | HTMLDivElement> = ref(null);

	const componentStyles: ComponentStyles = reactive({
		base: {
			overflow: 'hidden',
		},

		color: computed<CSSProperties>(() => {
			const colorStyle: CSSProperties = {};

			if (props.color !== undefined) {
				colorStyle.backgroundColor = props.color;
			}

			if (props.rsc?.backgroundColor !== null) {
				colorStyle.backgroundColor = props.rsc?.backgroundColor;
			}

			return colorStyle;
		}),

		rsc: computed<CSSProperties>(() => {
			const { rsc, size, offset } = props;

			if (!rsc) {
				return {};
			}

			if (rsc.status.value === Statuses.notLoaded) {
				rsc.load();
				return {};
			}

			if (!rsc.isLoaded() || !size.isValid() || !$el.value) {
				return {};
			}

			const { width, height, top, left } = rsc.getResizeProps(size, offset);

			return {
				backgroundImage: `url(${rsc.src})`,
				backgroundSize: `${width}px ${height}px`,
				backgroundPosition: `${left}px ${top}px`,
				backgroundRepeat: 'no-repeat',
			};
		}),
	});

	const { style, setCss, transform, show, hide } = useComponent($el, props, componentStyles);

	defineExpose({
		setCss,
		transform,
		show,
		hide,
	});
</script>

<template>
	<div ref="$el" class="flux-image" :style="style" />
</template>


===== FILE: src/components/FluxImage/__mocks__/FluxImage.vue =====
<script setup lang="ts">
	import { ref, type Ref } from 'vue';
	import { vi } from 'vitest';
	import type { FluxImageProps } from '../types';

	defineProps<FluxImageProps>();

	const $el: Ref<null | HTMLDivElement> = ref(null);

	defineExpose({
		setCss: vi.fn(),
		transform: vi.fn(),
		show: vi.fn(),
		hide: vi.fn(),
	});
</script>

<template>
	<div ref="$el" class="flux-image" />
</template>


===== FILE: src/components/FluxImage/types.ts =====
import type { ComponentProps } from '../types';

export interface FluxImageProps extends ComponentProps {}


===== FILE: src/components/FluxParallax/FluxParallax.vue =====
<script setup lang="ts">
	// holder (window), component, background

	import {
		ref,
		reactive,
		computed,
		unref,
		onMounted,
		onUnmounted,
		type Ref,
		type CSSProperties,
	} from 'vue';
	import { Maths } from '../../shared';
	import type { DisplayProps, FluxParallaxProps, FluxParallaxStyles, ViewProps } from './types';

	const { aspectRatio } = Maths;

	const props = withDefaults(defineProps<FluxParallaxProps>(), {
		holder: () => window,
		type: 'relative',
		offset: '100%',
	});

	const $el: Ref<null | HTMLDivElement> = ref(null);

	const { holder, rsc } = props;

	const style: FluxParallaxStyles = {
		base: {
			position: 'relative',
			background: `url("${rsc.src}") no-repeat`,
		},

		defined: reactive({}),

		final: computed(() => ({
			...style.base,
			...unref(style.defined),
		})),
	};

	const isIos =
		/iPad|iPhone|iPod/.test(navigator.userAgent) ||
		(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1);

	const display: DisplayProps = reactive({
		width: 0,
		height: 0,
		aspectRatio: computed(() => aspectRatio(display)),
	});

	const view: ViewProps = reactive({
		top: 0,
		width: 0,
		height: 0,
		aspectRatio: computed(() => aspectRatio(view)),
	});

	const background = reactive({
		top: 0,
		left: 0,
		width: 0,
		height: 0,
	});

	const fixedParentStyle: CSSProperties = {
		position: 'absolute',
		top: 0,
		left: 0,
		bottom: 0,
		right: 0,
		clip: 'rect(auto auto auto auto)',
	};

	const fixedChildStyle = computed<CSSProperties>(() => ({
		position: 'absolute',
		top: 0,
		bottom: 0,
		left: 0,
		right: 0,
		background: `url("${rsc.src}") no-repeat center center fixed`,
		backgroundSize: `${background.width}px ${background.height}px`,
	}));

	const offsetHeight = computed(() => {
		const { offset } = props;
		const offsetValue = parseFloat(offset);

		if (/^[0-9]+px$/.test(offset)) {
			return {
				px: offsetValue,
				pct: (offsetValue * 100) / background.height,
			};
		}

		if (/^[0-9]+%$/.test(offset)) {
			return {
				px: Math.ceil((view.height * offsetValue) / 100),
				pct: offsetValue,
			};
		}

		return {
			px: 0,
			pct: 0,
		};
	});

	const remainderHeight = computed(() => {
		const effectHeight = isIos ? display.height : view.height + offsetHeight.value.px;

		return background.height - effectHeight;
	});

	onMounted(() => {
		window.addEventListener('resize', resize, {
			passive: true,
		});

		if (props.type !== 'fixed' || isIos) {
			holder.addEventListener('scroll', onScroll, {
				passive: true,
			});
		}

		rsc.load().then(() => {
			resize();
		});
	});

	onUnmounted(() => {
		window.removeEventListener('resize', resize);
		holder.removeEventListener('scroll', onScroll);
	});

	const resize = () => {
		// @ts-expect-error:next-line
		display.width = holder.scrollWidth || holder.innerWidth;
		// @ts-expect-error:next-line
		display.height = holder.scrollHeight || holder.innerHeight;

		view.width = $el.value!.clientWidth;
		view.height = $el.value!.clientHeight;
		view.top = $el.value!.getBoundingClientRect().top + window.scrollY;

		rsc.displaySize.update(display);
		const fillProps = rsc.resizeProps.value;

		background.width = fillProps.width!;
		background.height = fillProps.height!;

		style.defined.backgroundSize = `${background.width}px ${background.height}px`;
		style.defined.backgroundPosition = `center 0`;

		onScroll();
	};

	const moveBackgroundByPct = (pct: number) => {
		if (remainderHeight.value > 0)
			pct = (pct * offsetHeight.value.pct) / 100 + 50 - offsetHeight.value.pct / 2;

		style.defined.backgroundPositionY = pct.toFixed(2) + '%';
	};

	const onScroll = () => {
		if (!rsc.isLoaded() || (!isIos && props.type === 'fixed')) {
			return;
		}

		// @ts-expect-error:next-line
		const scrollTop = holder.scrollY || holder.scrollTop || 0;

		if (holder !== window) {
			return handle.relative(scrollTop);
		}

		if (scrollTop + display.height < view.top) {
			return;
		}

		if (scrollTop > view.top + view.height) {
			return;
		}

		const positionY = scrollTop - view.top + display.height;

		handle[props.type](positionY);
	};

	const handle = {
		visible: (positionY: number) => {
			let pct = 0;

			if (positionY < view.height) {
				pct = 0;
			} else if (positionY > display.height) {
				pct = 100;
			} else {
				pct = ((positionY - view.height) * 100) / (display.height - view.height);
			}

			moveBackgroundByPct(pct);
		},

		relative: (positionY: number) => {
			let pct;

			if (holder === window) {
				pct = (positionY * 100) / (display.height + view.height);
			} else {
				// @ts-expect-error:next-line
				pct = (positionY * 100) / (display.height - holder.clientHeight);
			}

			moveBackgroundByPct(pct);
		},

		fixed: (positionY: number) => {
			style.defined.backgroundPositionY = positionY - display.height + 'px';
		},
	};

	defineExpose({
		resize,
	});
</script>

<template>
	<div ref="$el" class="flux-parallax" :style="style.final.value">
		<div v-if="props.type === 'fixed' && !isIos" :style="fixedParentStyle">
			<div class="image" :style="fixedChildStyle" />
		</div>

		<slot />
	</div>
</template>


===== FILE: src/components/FluxParallax/types.ts =====
import type { CSSProperties, ComputedRef } from 'vue';
import { Resource } from '../../resources';

export interface FluxParallaxProps {
	rsc: Resource;
	holder?: Window | Element;
	type?: 'visible' | 'relative' | 'fixed';
	offset?: string;
}

export interface FluxParallaxStyles {
	base: CSSProperties;
	defined: CSSProperties;
	final: ComputedRef<CSSProperties>;
}

export interface DisplayProps {
	width: number;
	height: number;
	aspectRatio: number;
}

export interface ViewProps {
	top: number;
	width: number;
	height: number;
	aspectRatio: number;
}


===== FILE: src/components/FluxTransition/FluxTransition.vue =====
<script setup lang="ts">
	import {
		ref,
		reactive,
		computed,
		onMounted,
		onUnmounted,
		nextTick,
		type Ref,
		type Component,
	} from 'vue';
	import type { FluxTransitionProps } from './types';
	import type { TransitionComponent } from '../../transitions';

	const props = withDefaults(defineProps<FluxTransitionProps>(), {
		options: () => ({}),
	});

	const $el: Ref<null | HTMLDivElement> = ref(null);
	const $transition: Ref<null | Component> = ref(null);

	const emit = defineEmits(['ready', 'start', 'end']);

	const styles = reactive({
		base: {
			overflow: 'hidden',
			perspective: 'none',
			zIndex: 3,
		},
	});

	const style = computed(() => {
		const { width, height } = props.size.toPx();

		return {
			...styles.base,
			width,
			height,
		};
	});

	const duration = ref(1);

	onMounted(async () => {
		await nextTick();

		if ($transition.value !== null) {
			duration.value = ($transition.value as TransitionComponent).totalDuration;
		}

		emit('ready', {
			transition: props.transition,
			from: props.from,
			to: props.to,
			options: props.options,
			duration: duration.value,
		});
	});

	async function start() {
		emit('start', {
			transition: props.transition,
			from: props.from,
			to: props.to,
			options: props.options,
			duration: duration.value,
		});

		await nextTick();

		if ($transition.value === null) {
			console.error('Transition component not available', props.transition);
		} else {
			($transition.value as TransitionComponent).onPlay();
		}

		setTimeout(() => end(), duration.value);
	}

	function end() {
		emit('end', {
			transition: props.transition,
			from: props.from,
			to: props.to,
			options: props.options,
			duration: duration.value,
		});
	}

	onUnmounted(() => {
		if (props.displayComponent) {
			props.displayComponent.show();
		}
	});

	defineExpose({ start });
</script>

<template>
	<div ref="$el" class="flux-transition" :style="style">
		<component
			:is="transition"
			ref="$transition"
			:size="size"
			:from="from"
			:to="to"
			:display-component="displayComponent"
			:options="options"
			:mask-style="styles.base"
		/>
	</div>
</template>

<style lang="scss">
	.flux-transition {
		position: relative;
	}
</style>


===== FILE: src/components/FluxTransition/types.ts =====
import { Resource } from '../../resources';
import { Size } from '../../shared';
import type { FluxComponent } from '../types';

export interface FluxTransitionProps {
	size: Size;
	transition: object;
	from: Resource;
	to: Resource;
	displayComponent?: null | FluxComponent;
	options?: object;
}


===== FILE: src/components/FluxVortex/FluxVortex.vue =====
<script setup lang="ts">
	import { ref, reactive, computed, type Ref, onBeforeUpdate } from 'vue';
	import useComponent from '../useComponent';
	import type { ComponentStyles, FluxComponent } from '../types';
	import type { FluxVortexProps } from './types';
	import { VortexFactory } from './factories';

	const props = withDefaults(defineProps<FluxVortexProps>(), {
		circles: 1,
	});

	const $el: Ref<null | HTMLDivElement> = ref(null);

	const componentStyles: ComponentStyles = reactive({
		base: {
			position: 'relative',
			overflow: 'hidden',
		},
	});

	const { style, setCss, show, hide } = useComponent($el, props, componentStyles);

	const tiles = computed(() => VortexFactory.getCirclesProps(props));

	const $tiles: Ref<FluxComponent[]> = ref([]);

	onBeforeUpdate(() => {
		$tiles.value = [];
	});

	const transform = <T,>(cb: (tile: T, index: number) => void) => {
		$tiles.value.forEach((tile: unknown, index: number) => cb(tile as T, index));
	};

	defineExpose({
		setCss,
		transform,
		show,
		hide,
	});
</script>

<template>
	<div ref="$el" class="flux-vortex" :style="style">
		<component
			:is="rsc.transition.component"
			v-for="(tile, index) in tiles"
			:ref="(el: any) => $tiles.push(el)"
			:key="index"
			:size="size"
			:rsc="rsc"
			:offset="tile.offset"
			:css="tile.css"
		/>
	</div>
</template>


===== FILE: src/components/FluxVortex/__mocks__/FluxVortex.vue =====
<script setup lang="ts">
	import { ref, type Ref, onBeforeUpdate } from 'vue';
	import { vi } from 'vitest';
	import Tile from './Tile.vue';
	import type { FluxVortexProps } from '../types';

	const props = withDefaults(defineProps<FluxVortexProps>(), {
		circles: 1,
	});

	const $el: Ref<null | HTMLDivElement> = ref(null);

	const numTiles = props.circles;
	const $tiles: Ref<InstanceType<typeof Tile>[]> = ref([]);

	onBeforeUpdate(() => {
		$tiles.value = [];
	});

	const transform = vi
		.fn()
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		.mockImplementation((cb: (tile: any, index: number) => void) => {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			$tiles.value.forEach((tile: any, index: number) => cb(tile, index));
		});

	defineExpose({
		setCss: vi.fn(),
		transform,
		show: vi.fn(),
		hide: vi.fn(),
		getRowNumber: vi.fn(),
		getColNumber: vi.fn(),
		$tiles,
	});
</script>

<template>
	<div ref="$el" class="flux-vortex">
		<Tile v-for="index in numTiles" :ref="(el: any) => $tiles.push(el)" :key="index" />
	</div>
</template>


===== FILE: src/components/FluxVortex/__mocks__/Tile.vue =====
<script setup lang="ts">
	import { ref, type Ref } from 'vue';
	import { vi } from 'vitest';

	const $el: Ref<null | HTMLDivElement> = ref(null);

	defineExpose({
		turn: vi.fn(),
		setCss: vi.fn(),
		transform: vi.fn(),
		show: vi.fn(),
		hide: vi.fn(),
		getRowNumber: vi.fn(),
		getColNumber: vi.fn(),
	});
</script>

<template>
	<div ref="$el" />
</template>


===== FILE: src/components/FluxVortex/factories/VortexCircleFactory.ts =====
import type { CSSProperties } from 'vue';
import { Position } from '../../../shared';
import type { FluxVortexCirclesProps } from '../types';

export default class VortexCircleFactory {
	static getProps(
		vortex: {
			numCircles: number;
			diagonal: number;
			radius: number;
			topGap: number;
			leftGap: number;
		},
		circleNumber: number,
		circleCss?: CSSProperties,
	) {
		const size = (vortex.numCircles - circleNumber) * vortex.radius * 2;

		const gap = vortex.radius * circleNumber;

		const offset = new Position({
			top: vortex.topGap + gap,
			left: vortex.leftGap + gap,
		});

		const circle: FluxVortexCirclesProps = {
			offset: offset,
			css: {
				...circleCss,
				...offset.toPx(),
				position: 'absolute',
				width: size + 'px',
				height: size + 'px',
				backgroundRepeat: 'repeat',
				borderRadius: '50%',
				zIndex: circleNumber,
			},
		};

		return circle;
	}
}


===== FILE: src/components/FluxVortex/factories/VortexFactory.ts =====
import { Maths } from '../../../shared';
import type { FluxVortexProps, FluxVortexCirclesProps } from '../types';
import VortexCircleFactory from './VortexCircleFactory';

export default class VortexFactory {
	static getCirclesProps(props: FluxVortexProps) {
		const { width, height } = props.size.toValue();

		const numCircles = Math.round(props.circles!);
		const diagonal = Maths.diag({ width: width!, height: height! });
		const radius = Math.ceil(diagonal / 2 / numCircles);
		const topGap = Math.ceil(height! / 2 - radius * numCircles);
		const leftGap = Math.ceil(width! / 2 - radius * numCircles);

		const vortex = {
			numCircles,
			diagonal,
			radius,
			topGap,
			leftGap,
		};

		const circlesProps: FluxVortexCirclesProps[] = [];

		for (let circleNumber = 0; circleNumber < numCircles; circleNumber++) {
			circlesProps.push(VortexCircleFactory.getProps(vortex, circleNumber, props.tileCss));
		}

		return circlesProps;
	}
}


===== FILE: src/components/FluxVortex/factories/index.ts =====
export { default as VortexFactory } from './VortexFactory';
export { default as VortexCircleFactory } from './VortexCircleFactory';


===== FILE: src/components/FluxVortex/types.ts =====
import type { CSSProperties } from 'vue';
import { Resource } from '../../resources';
import type { ComponentProps } from '../types';
import { Position } from '../../shared';

export interface FluxVortexProps extends ComponentProps {
	rsc: Resource;
	circles?: number;
	tileCss?: CSSProperties;
}

export interface FluxVortexCirclesProps {
	offset: Position;
	css: CSSProperties;
}


===== FILE: src/components/FluxWrapper/FluxWrapper.vue =====
<script setup lang="ts">
	import { ref, reactive, type Ref } from 'vue';
	import useComponent from '../useComponent';
	import type { ComponentStyles } from '../types';
	import type { FluxWrapperProps } from './types';

	const props = defineProps<FluxWrapperProps>();

	const $el: Ref<null | HTMLDivElement> = ref(null);

	const componentStyles: ComponentStyles = reactive({
		base: {
			overflow: 'hidden',
		},
	});

	const { style, setCss, transform, show, hide } = useComponent($el, props, componentStyles);

	defineExpose({
		setCss,
		transform,
		show,
		hide,
	});
</script>

<template>
	<div ref="$el" class="flux-wrapper" :style="style">
		<slot />
	</div>
</template>


===== FILE: src/components/FluxWrapper/__mocks__/FluxWrapper.vue =====
<script setup lang="ts">
	import { ref, type Ref } from 'vue';
	import { vi } from 'vitest';
	import type { FluxWrapperProps } from '../types';

	defineProps<FluxWrapperProps>();

	const $el: Ref<null | HTMLDivElement> = ref(null);

	defineExpose({
		setCss: vi.fn(),
		transform: vi.fn(),
		show: vi.fn(),
		hide: vi.fn(),
	});
</script>

<template>
	<div ref="$el" class="flux-wrapper">
		<slot />
	</div>
</template>


===== FILE: src/components/FluxWrapper/types.ts =====
import type { ComponentProps } from '../types';

export interface FluxWrapperProps extends ComponentProps {}


===== FILE: src/components/VueFlux/VueFlux.vue =====
<script setup lang="ts">
	import { onMounted, onUnmounted, ref, reactive, computed, watch, type Ref, toRaw } from 'vue';
	import * as Controllers from '../../controllers';
	import { type FluxComponent, FluxTransition } from '../';
	import type { VueFluxProps, VueFluxEmits, VueFluxConfig } from './types';
	import { default as PlayerStatuses } from '../../controllers/Player/Statuses';

	const props = withDefaults(defineProps<VueFluxProps>(), {
		options: () => ({}),
	});

	const emit = defineEmits<VueFluxEmits>();

	const $el: Ref<null | HTMLDivElement> = ref(null);
	const $transition: Ref<null | InstanceType<typeof FluxTransition>> = ref(null);
	const $displayComponent: Ref<null | FluxComponent> = ref(null);

	const config: VueFluxConfig = reactive({
		allowFullscreen: false,
		allowToSkipTransition: true,
		aspectRatio: '16:9',
		autohideTime: 2500,
		autoplay: false,
		bindKeys: false,
		delay: 5000,
		enableGestures: false,
		infinite: true,
		lazyLoad: true,
		lazyLoadAfter: 5,
	});

	const timers = new Controllers.Timers();
	const player = new Controllers.Player(config, timers, emit);
	const resources = player.resources;
	const transitions = player.transitions;
	const display = new Controllers.Display($el, config, emit);
	const keys = new Controllers.Keys(config, player);
	const mouse = new Controllers.Mouse();
	const touches = new Controllers.Touches();

	const setup = () => {
		Object.assign(config, props.options);
		mouse.setup(config, timers);
		keys.setup();
	};

	watch(props.options, () => {
		setup();
		emit('optionsUpdated');
	});

	async function updateProp(propName: 'rscs' | 'transitions') {
		const wasPlaying = player.status.value === PlayerStatuses.playing;

		if (wasPlaying) {
			await player.stop(true);
		}

		await {
			rscs: async () => await updateResources(),
			transitions: () => updateTransitions(),
		}[propName]();

		if (wasPlaying) {
			player.play();
		}
	}

	async function updateResources() {
		player.resource.reset();

		const numToPreload = config.lazyLoad ? config.lazyLoadAfter : props.rscs.length;

		try {
			await resources.update(toRaw(props.rscs), numToPreload, display.size);
		} catch (e) {
			console.error(e);
		}

		if (resources.list.length) {
			player.resource.init(resources);
		}
	}

	watch(
		() => props.rscs,
		async () => {
			await updateProp('rscs');
		},
		{ deep: false },
	);

	function updateTransitions() {
		player.transition.reset();

		transitions.update(toRaw(props.transitions));

		player.transition.init(transitions);
	}

	watch(
		props.transitions,
		async () => {
			await updateProp('transitions');
			emit('transitionsUpdated');
		},
		{ deep: false },
	);

	onMounted(async () => {
		setup();

		display.addResizeListener();

		player.setup($displayComponent);

		updateTransitions();

		await updateResources();

		if (config.autoplay === true) {
			player.play();
		}

		emit('mounted');
	});

	onUnmounted(() => {
		timers.clear();
		display.removeResizeListener();
		keys.removeKeyListener();

		emit('unmounted');
	});

	const style = computed(() => {
		if (!display.size.isValid()) {
			return {};
		}

		if (display.inFullScreen()) {
			return {
				width: '100% !important',
				height: '100% !important',
			};
		}

		return display.size.toPx();
	});

	defineExpose({
		show: player.show.bind(player),
		play: player.play.bind(player),
		stop: player.stop.bind(player),
		getPlayer: () => player as Controllers.Player,
		size: display.size,
	});

	emit('created');
</script>

<template>
	<div
		ref="$el"
		class="vue-flux"
		:style="style"
		@mousemove="mouse.toggle(config, timers, true)"
		@mouseleave="mouse.toggle(config, timers, false)"
		@dblclick="display.toggleFullScreen()"
		@touchstart="touches.start($event, config)"
		@touchend="touches.end($event, config, player, display, timers, mouse)"
	>
		<FluxTransition
			v-if="
				/* eslint-disable vue/html-indent */
				player.transition.current !== null &&
				display.size.isValid() &&
				player.resource.from !== null &&
				player.resource.to !== null
				/* eslint-enable */
			"
			ref="$transition"
			:transition="player.transition.current.component"
			:size="display.size"
			:from="player.resource.from.rsc"
			:to="player.resource.to.rsc"
			:display-component="$displayComponent"
			:options="player.transition.current.options"
			@ready="$transition?.start()"
			@start="player.start()"
			@end="player.end()"
		/>

		<component
			:is="player.resource.current.rsc.display.component"
			v-if="player.resource.current !== null"
			ref="$displayComponent"
			:size="display.size"
			:rsc="player.resource.current.rsc"
			v-bind="player.resource.current.rsc.display.props"
		/>

		<div v-if="display.size.isValid()" class="complements">
			<slot name="preloader" :loader="resources.loader" />

			<slot name="caption" :player="player" />

			<div class="remainder upper" />

			<slot name="controls" :mouse-over="mouse.isOver" :player="player" />

			<div class="remainder lower" />

			<slot
				name="index"
				:mouse-over="mouse.isOver"
				:display-size="display.size"
				:player="player"
			/>

			<slot name="pagination" :player="player" />
		</div>
	</div>
</template>

<style lang="scss">
	.vue-flux {
		position: relative;

		.flux-transition {
			position: absolute;
		}

		& > .flux-image {
			position: absolute;
			top: 0;
			left: 0;
		}

		.complements {
			position: absolute;
			top: 0;
			left: 0;
			right: 0;
			bottom: 0;
			display: flex;
			flex-direction: column;
			justify-content: space-between;
			z-index: 45;

			.remainder {
				flex-basis: 50%;
			}
		}
	}
</style>


===== FILE: src/components/VueFlux/__test__/emit.ts =====
import { vi } from 'vitest';
import type { VueFluxEmits } from '../types';

export default vi.fn() as unknown as VueFluxEmits;


===== FILE: src/components/VueFlux/types.ts =====
import { Resource, type ResourceWithOptions } from '../../resources';
import type { TransitionWithOptions } from '../../transitions';
import { type Direction, PlayerResource, PlayerTransition } from '../../controllers/Player';
import { type Component } from 'vue';

export interface VueFluxOptions {
	allowFullscreen?: boolean;
	allowToSkipTransition?: boolean;
	aspectRatio?: string;
	autohideTime?: number;
	autoplay?: boolean;
	bindKeys?: boolean;
	delay?: number;
	enableGestures?: boolean;
	infinite?: boolean;
	lazyLoad?: boolean;
	lazyLoadAfter?: number;
}

export interface VueFluxProps {
	options?: VueFluxOptions;
	rscs: (Resource | ResourceWithOptions)[];
	transitions: (Component | TransitionWithOptions)[];
}

export interface VueFluxEmits {
	(e: 'created'): void;
	(e: 'mounted'): void;
	(e: 'unmounted'): void;
	(e: 'play', resourceIndex: number | Direction, delay?: number): void;
	(e: 'stop'): void;
	(e: 'show', resource: PlayerResource, transition: PlayerTransition): void;
	(e: 'optionsUpdated'): void;
	(e: 'transitionsUpdated'): void;
	(e: 'resourcesPreloadStart'): void;
	(e: 'resourcesPreloadEnd'): void;
	(e: 'resourcesLazyloadStart'): void;
	(e: 'resourcesLazyloadEnd'): void;
	(e: 'fullscreenEnter'): void;
	(e: 'fullscreenExit'): void;
	(e: 'transitionStart', resource: PlayerResource, transition: PlayerTransition): void;
	(e: 'transitionCancel', resource: PlayerResource, transition: PlayerTransition): void;
	(e: 'transitionEnd', resource: PlayerResource, transition: PlayerTransition): void;
}

export interface VueFluxConfig {
	allowFullscreen: boolean;
	allowToSkipTransition: boolean;
	aspectRatio: string;
	autohideTime: number;
	autoplay: boolean;
	bindKeys: boolean;
	delay: number;
	enableGestures: boolean;
	infinite: boolean;
	lazyLoad: boolean;
	lazyLoadAfter: number;
}


===== FILE: src/components/index.ts =====
export { default as FluxButton } from './FluxButton/FluxButton.vue';
export * from './FluxCube';
export { default as FluxGrid } from './FluxGrid/FluxGrid.vue';
export { default as FluxImage } from './FluxImage/FluxImage.vue';
export { default as FluxParallax } from './FluxParallax/FluxParallax.vue';
export { default as FluxTransition } from './FluxTransition/FluxTransition.vue';
export { default as FluxVortex } from './FluxVortex/FluxVortex.vue';
export { default as FluxWrapper } from './FluxWrapper/FluxWrapper.vue';
export { default as VueFlux } from './VueFlux/VueFlux.vue';

export type * from './VueFlux/types';
export type * from './FluxCube/types';
export type * from './FluxGrid/types';
export type * from './FluxParallax/types';
export type * from './FluxTransition/types';
export type * from './FluxVortex/types';
export type * from './FluxWrapper/types';
export type * from './types';


===== FILE: src/components/types.ts =====
import type { CSSProperties, Component } from 'vue';
import { Resource } from '../resources';
import { Size, Position } from '../shared';

export interface ComponentProps {
	color?: CSSProperties['color'];
	rsc?: Resource;
	size: Size;
	viewSize?: Size;
	offset?: Position;
	css?: CSSProperties;
}

export interface ComponentStyles {
	base?: CSSProperties;
	color?: CSSProperties;
	rsc?: CSSProperties;
	size?: CSSProperties;
}

export type FluxComponent = Component & {
	setCss: (s: CSSProperties) => void;
	transform: (s: CSSProperties) => void;
	show: () => void;
	hide: () => void;
};


===== FILE: src/components/useComponent.ts =====
import { computed, type CSSProperties, type Ref, unref } from 'vue';
import { Size } from '../shared';
import type { ComponentProps, ComponentStyles } from './types';

export default function useComponent(
	$el: Ref<null | HTMLElement>,
	props: ComponentProps,
	css: ComponentStyles,
) {
	if (css.base === undefined) {
		css.base = {} as CSSProperties;
	}

	const size = computed<CSSProperties>(() => {
		const { size, viewSize = new Size() } = props;

		const { width = size.width.value, height = size.height.value } = viewSize.toValue();

		const finalSize = new Size({ width, height });

		if (!finalSize.isValid()) {
			return {};
		}

		return finalSize.toPx();
	});

	const style = computed(() => ({
		...unref(size),
		...unref(css.color),
		...unref(css.rsc),
		...unref(props.css),
		...unref(css.base),
	}));

	const setCss = (s: CSSProperties) => {
		Object.assign(css.base as CSSProperties, s);
	};

	const transform = (s: CSSProperties) => {
		if ($el.value === null) {
			return;
		}

		// eslint-disable-next-line @typescript-eslint/no-unused-expressions
		$el.value.clientHeight;
		setCss(s);
	};

	const show = () => {
		setCss({
			visibility: 'visible',
		});
	};

	const hide = () => {
		setCss({
			visibility: 'hidden',
		});
	};

	return {
		style,
		setCss,
		transform,
		show,
		hide,
	};
}


===== FILE: src/controllers/Display/Display.ts =====
import { nextTick, type Ref, type Component } from 'vue';
import { Size } from '../../shared';
import type { VueFluxConfig, VueFluxEmits } from '../../components';

export default class Display {
	node: Ref<null | HTMLElement | Component>;
	config: VueFluxConfig | null;
	emit: null | VueFluxEmits = null;
	size: Size = new Size();

	private readonly onResize = () => {
		this.updateSize();
	};

	constructor(
		node: Ref<null | HTMLElement | Component>,
		config: VueFluxConfig | null = null,
		emit: null | VueFluxEmits = null,
	) {
		this.node = node;
		this.config = config;
		this.emit = emit;
	}

	static async getSize(node: Ref<null | HTMLElement | Component>) {
		const display = new Display(node);
		await display.updateSize();

		return display.size;
	}

	addResizeListener() {
		window.addEventListener('resize', this.onResize, {
			passive: true,
		});

		void this.updateSize();
	}

	removeResizeListener() {
		window.removeEventListener('resize', this.onResize);
	}

	getAspectRatio() {
		if (this.config !== null) {
			const [width, height] = this.config.aspectRatio.split(':');

			return [parseFloat(width ?? ''), parseFloat(height ?? '')];
		}

		return [16, 9];
	}

	async updateSize() {
		this.size.reset();

		await nextTick();

		if (this.node.value === null) {
			return;
		}

		const computedStyle = getComputedStyle(this.node.value as HTMLElement);

		const width = parseFloat(computedStyle.width);
		let height = parseFloat(computedStyle.height);

		if (['0px', 'auto', null].includes(computedStyle.height)) {
			const [arWidth, arHeight] = this.getAspectRatio();

			if (arWidth === undefined || arHeight === undefined) {
				return;
			}

			height = (width / arWidth) * arHeight;
		}

		this.size.update({
			width,
			height,
		});
	}

	inFullScreen = () => !!document.fullscreenElement;

	toggleFullScreen() {
		// eslint-disable-next-line @typescript-eslint/no-unused-expressions
		this.inFullScreen() ? this.exitFullScreen() : this.enterFullScreen();
	}

	async enterFullScreen() {
		if (this.node?.value === null || !this.config?.allowFullscreen) {
			return;
		}

		await (this.node.value as HTMLElement).requestFullscreen();

		if (this.emit !== null) {
			this.emit('fullscreenEnter');
		}
	}

	async exitFullScreen() {
		await document.exitFullscreen();

		if (this.emit !== null) {
			this.emit('fullscreenExit');
		}
	}
}


===== FILE: src/controllers/Keys/Keys.ts =====
import type { VueFluxConfig } from '../../components';
import { Directions, Player } from '../';

export default class Keys {
	config: VueFluxConfig;
	player: Player;

	constructor(config: VueFluxConfig, player: Player) {
		this.config = config;
		this.player = player;
	}

	setup() {
		this.removeKeyListener();

		if (this.config.bindKeys) {
			window.addEventListener('keydown', this.keydown);
		}
	}

	removeKeyListener() {
		window.removeEventListener('keydown', this.keydown);
	}

	keydown = (event: KeyboardEvent) => {
		if (['ArrowLeft', 'Left'].includes(event.key)) {
			this.player.show(Directions.prev);
			return;
		}

		if (['ArrowRight', 'Right'].includes(event.key)) {
			this.player.show(Directions.next);
			return;
		}
	};
}


===== FILE: src/controllers/Mouse/Mouse.ts =====
import { type Ref, ref } from 'vue';
import Timers from '../Timers/Timers';
import type { VueFluxConfig } from '../../components';

export default class Mouse {
	isOver: Ref<boolean> = ref(false);

	setup(config: VueFluxConfig, timers: Timers) {
		timers.clear('mouseOver');

		if (config.autohideTime === 0) {
			this.isOver.value = true;
		}
	}

	toggle(config: VueFluxConfig, timers: Timers, over: boolean) {
		if (config.autohideTime === 0) {
			return;
		}

		this.isOver.value = over;

		this[over ? 'over' : 'out'](config, timers);
	}

	out(_config: VueFluxConfig, timers: Timers) {
		timers.clear('mouseOver');
	}

	over(config: VueFluxConfig, timers: Timers) {
		timers.set('mouseOver', config.autohideTime, () => (this.isOver.value = false));
	}
}


===== FILE: src/controllers/Player/Directions.ts =====
enum Directions {
	prev = 'prev',
	next = 'next',
}

export default Directions;


===== FILE: src/controllers/Player/Player.ts =====
import { shallowReactive, nextTick, type Ref, ref } from 'vue';
import {
	Resources,
	Transitions,
	type ResourceIndex,
	type TransitionIndex,
} from '../../repositories';
import { PlayerResource, PlayerTransition, Directions, type Direction, Statuses } from './';
import type { FluxComponent, VueFluxConfig, VueFluxEmits } from '../../components';
import { Timers } from '../';

export default class Player {
	resource: PlayerResource;
	transition: PlayerTransition;

	status: Ref<keyof typeof Statuses> = ref(Statuses.stopped);
	config: VueFluxConfig;
	timers: Timers;

	emit: VueFluxEmits;
	resources: Resources;
	transitions: Transitions;
	$displayComponent: Ref<null | FluxComponent> = ref(null);

	constructor(
		config: VueFluxConfig,
		timers: Timers,

		emit: VueFluxEmits,
	) {
		this.config = config;
		this.timers = timers;
		this.emit = emit;

		this.resources = new Resources(emit);
		this.transitions = new Transitions();
		this.resource = shallowReactive(new PlayerResource());
		this.transition = shallowReactive(new PlayerTransition());
	}

	setup($displayComponent: Ref<null | FluxComponent>) {
		this.$displayComponent = $displayComponent;
	}

	play(resourceIndex: number | Direction = Directions.next, delay?: number) {
		const { config, timers, resource } = this;

		this.status.value = Statuses.playing;

		if (this.transition.current !== null) {
			return;
		}

		const rsc = this.resources?.find(resourceIndex, resource.current?.index);

		timers.set('transition', delay || rsc?.options.delay || config.delay, () => {
			this.show(resourceIndex);
		});

		this.emit('play', resourceIndex, delay);
	}

	async stop(cancelTransition: boolean = false) {
		const { timers } = this;

		this.status.value = Statuses.stopped;

		timers.clear('transition');

		if (this.transition.current !== null && cancelTransition === true) {
			await this.end(cancelTransition);
		}

		this.emit('stop');
	}

	isReadyToShow() {
		if (this.resource.current === null) {
			throw new ReferenceError('Current resource not set');
		}

		if (this.resources === null) {
			throw new ReferenceError('Resources list not set');
		}

		if (this.resources.list.length === 0) {
			throw new RangeError('Resources list empty');
		}

		if (this.transition.last === null) {
			throw new ReferenceError('Last transition not set');
		}

		if (this.transitions === null) {
			throw new ReferenceError('Transitions list not set');
		}

		if (this.transitions.list.length === 0) {
			throw new RangeError('Transitions list empty');
		}

		if (this.$displayComponent.value === null) {
			throw new ReferenceError('Display component not set');
		}

		return true;
	}

	async show(
		resourceIndex: number | Direction = Directions.next,
		transitionIndex: number | Direction = Directions.next,
	) {
		if (!this.isReadyToShow()) {
			return;
		}

		const { resource, resources, config, transitions } = this;

		if (this.transition.current !== null) {
			if (config.allowToSkipTransition) {
				await this.end(true);

				this.show(resourceIndex, transitionIndex);
			}

			return;
		}

		const resourceTo: ResourceIndex = resources!.find(resourceIndex, resource.current!.index);

		if (resource.currentSameAs(resourceTo)) {
			return;
		}

		resource.prepareTo(resourceTo);

		this.timers.clear('transition');

		const transition: TransitionIndex =
			typeof transitionIndex === 'number'
				? transitions!.getByIndex(transitionIndex)
				: transitions!.getByOrder(transitionIndex, this.transition.last!.index);

		if (transition.options.direction === undefined) {
			if (typeof resourceIndex !== 'number') {
				transition.options.direction = resourceIndex;
			} else {
				transition.options.direction =
					this.resource.from!.index < this.resource.to!.index
						? Directions.next
						: Directions.prev;
			}
		}

		this.transition.current = transition;

		this.emit('show', this.resource, this.transition);
	}

	start() {
		this.resource.current = this.resource.to;
		this.emit('transitionStart', this.resource, this.transition);
	}

	async end(cancel: boolean = false) {
		const { config, resource, resources, timers, transition } = this;

		if (resource.current === null || resources === null) {
			return;
		}

		transition.setCurrentFinished();

		await nextTick();

		if (cancel === true) {
			this.emit('transitionCancel', this.resource, this.transition);
		} else {
			this.emit('transitionEnd', this.resource, this.transition);
		}

		if (this.shouldStopPlaying(config.infinite, resource.current, resources.list.length - 1)) {
			this.stop();
			return;
		}

		if (this.shouldPlayNext()) {
			timers.set('transition', resource.current.options.delay || config.delay, () => {
				this.show();
			});
		}
	}

	private shouldStopPlaying(
		infinite: boolean,
		currentResource: ResourceIndex,
		totalResources: number,
	) {
		if (
			infinite === false &&
			currentResource.index >= totalResources &&
			this.status.value === Statuses.playing
		) {
			return true;
		}

		if (currentResource.options.stop === true) {
			return true;
		}

		return false;
	}

	private shouldPlayNext() {
		if (this.status.value === Statuses.playing) {
			return true;
		}

		return false;
	}
}


===== FILE: src/controllers/Player/Resource.ts =====
import { Resources, type ResourceIndex } from '../../repositories';

export default class PlayerResource {
	current: ResourceIndex | null = null;
	from: ResourceIndex | null = null;
	to: ResourceIndex | null = null;

	reset() {
		this.current = null;
		this.from = null;
		this.to = null;
	}

	init(repository: Resources) {
		this.current = repository.getFirst();
	}

	currentSameAs(resourceTo: ResourceIndex) {
		if (this.current!.index === resourceTo.index) {
			return true;
		}

		return false;
	}

	prepareTo(resourceTo: ResourceIndex) {
		this.from = this.current;
		this.to = resourceTo;
	}
}


===== FILE: src/controllers/Player/Statuses.ts =====
enum Statuses {
	stopped = 'stopped',
	playing = 'playing',
}

export default Statuses;


===== FILE: src/controllers/Player/Transition.ts =====
import { Transitions, type TransitionIndex } from '../../repositories';

export default class PlayerTransition {
	current: TransitionIndex | null = null;
	last: TransitionIndex | null = null;

	reset() {
		this.current = null;
		this.last = null;
	}

	init(transitions: Transitions) {
		this.last = transitions.getLast();
	}

	setCurrentFinished() {
		this.last = this.current;
		this.current = null;
	}
}


===== FILE: src/controllers/Player/__mocks__/Player.ts =====
import { vi } from 'vitest';
import { type Ref, ref, shallowReactive } from 'vue';
import type { VueFluxConfig, VueFluxEmits } from '../../../components/VueFlux/types';
import { PlayerResource, PlayerTransition, Statuses, Timers } from '../..';
import { Resources, Transitions } from '../../../repositories';
import type { FluxComponent } from '../../../components/types';

export default class Player {
	resource: PlayerResource;
	transition: PlayerTransition;

	status: Ref<keyof typeof Statuses> = ref(Statuses.stopped);
	config: VueFluxConfig;
	timers: Timers;
	emit: VueFluxEmits;
	resources: Resources;
	transitions: Transitions;
	$displayComponent: Ref<null | FluxComponent> = ref(null);

	constructor(config: VueFluxConfig, timers: Timers, emit: VueFluxEmits) {
		this.config = config;
		this.timers = timers;
		this.emit = emit;

		this.resources = new Resources(emit);
		this.transitions = new Transitions();
		this.resource = shallowReactive(new PlayerResource());
		this.transition = shallowReactive(new PlayerTransition());
	}

	setup = vi.fn();
	play = vi.fn();
	stop = vi.fn();
	show = vi.fn();
	start = vi.fn();
	end = vi.fn();
}


===== FILE: src/controllers/Player/__mocks__/Resource.ts =====
import type { ResourceIndex } from '../../../repositories';
import { vi } from 'vitest';

export default class PlayerResource {
	current: ResourceIndex | null = null;
	from: ResourceIndex | null = null;
	to: ResourceIndex | null = null;

	reset = vi.fn();
	init = vi.fn();
	currentSameAs = vi.fn();
	prepareTo = vi.fn();
}


===== FILE: src/controllers/Player/__mocks__/Transitions.ts =====
import type { TransitionIndex } from '../../../repositories';
import { vi } from 'vitest';

export default class PlayerTransition {
	current: TransitionIndex | null = null;
	last: TransitionIndex | null = null;

	reset = vi.fn();
	init = vi.fn();
	setCurrentFinished = vi.fn();
}


===== FILE: src/controllers/Player/index.ts =====
export { default as Directions } from './Directions';
export { default as Statuses } from './Statuses';
export { default as PlayerResource } from './Resource';
export { default as PlayerTransition } from './Transition';
export { default as Player } from './Player';

export type * from './types';


===== FILE: src/controllers/Player/types.ts =====
import Directions from './Directions';

export type Direction = Directions.prev | Directions.next;


===== FILE: src/controllers/Timers/Timers.ts =====
export default class Timers {
	timers: {
		[index: string]: ReturnType<typeof setTimeout>;
	} = {};

	set(index: string, time: number, cb: () => void) {
		this.clear(index);
		this.timers[index] = setTimeout(cb, time);
	}

	clear(index?: string) {
		const keys = index !== undefined ? [index] : Object.keys(this.timers);

		keys.forEach((key) => {
			clearTimeout(this.timers[key]);
			delete this.timers[key];
		});
	}
}


===== FILE: src/controllers/Touches/Touches.ts =====
import type { VueFluxConfig } from '../../components';
import { Directions, Display, Mouse, Player, Timers } from '../';

export default class Touches {
	startX = 0;
	startY = 0;
	startTime = 0;
	endTime = 0;
	prevTouchTime = 0;

	// Max distance in pixels from start until end
	tapThreshold = 5;

	// Max time in ms from first to second tap
	doubleTapThreshold = 200;

	// Distance in percentage to trigger slide
	slideTrigger = 0.3;

	start(event: TouchEvent, config: VueFluxConfig) {
		if (!config.enableGestures) {
			return;
		}

		const touch = event.changedTouches[0];

		if (!touch) {
			return;
		}

		this.startTime = Date.now();
		this.startX = touch.clientX;
		this.startY = touch.clientY;
	}

	end(
		event: TouchEvent,
		config: VueFluxConfig,
		player: Player,
		display: Display,
		timers: Timers,
		mouse: Mouse,
	) {
		this.prevTouchTime = this.endTime;
		this.endTime = Date.now();

		const touch = event.changedTouches[0];

		if (!touch) {
			return;
		}

		const offsetX = touch.clientX - this.startX;
		const offsetY = touch.clientY - this.startY;

		if (this.tap(offsetX, offsetY)) {
			mouse.toggle(config, timers, true);
			return;
		}

		if (!config.enableGestures) {
			return;
		}

		if (this.slideRight(offsetX, display)) {
			player.show(Directions.prev);
		} else if (this.slideLeft(offsetX, display)) {
			player.show(Directions.next);
		}
	}

	tap = (offsetX: number, offsetY: number) =>
		Math.abs(offsetX) < this.tapThreshold && Math.abs(offsetY) < this.tapThreshold;

	doubleTap = () => this.endTime - this.prevTouchTime < this.doubleTapThreshold;

	slideLeft = (offsetX: number, display: Display) =>
		display.size.isValid() &&
		offsetX < 0 &&
		offsetX < -(display.size!.width.value! * this.slideTrigger);

	slideRight = (offsetX: number, display: Display) =>
		display.size.isValid() &&
		offsetX > 0 &&
		offsetX > display.size.width.value! * this.slideTrigger;

	slideUp = (offsetY: number, display: Display) =>
		display.size.isValid() &&
		offsetY < 0 &&
		offsetY < -(display.size.height.value! * this.slideTrigger);

	slideDown = (offsetY: number, display: Display) =>
		display.size.isValid() &&
		offsetY > 0 &&
		offsetY > display.size.height.value! * this.slideTrigger;
}


===== FILE: src/controllers/index.ts =====
export * from './Player';
export { default as Display } from './Display/Display';
export { default as Keys } from './Keys/Keys';
export { default as Mouse } from './Mouse/Mouse';
export { default as Timers } from './Timers/Timers';
export { default as Touches } from './Touches/Touches';


===== FILE: src/lib.ts =====
export * from './components';
export * from './complements';
export * from './resources';
export * from './transitions';

export {
	Player,
	Directions,
	Statuses,
	PlayerResource,
	PlayerTransition,
} from './controllers/Player';

export type * from './controllers/Player/types';

export { Size, Position } from './shared';


===== FILE: src/main.ts =====
import './assets/css/main.css';

import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');


===== FILE: src/module.d.ts =====
declare module '*';


===== FILE: src/playgrounds/PgFluxCaption.vue =====
<script setup lang="ts">
	import { ref, shallowReactive, onMounted, type Ref } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import { VueFlux } from '../components';
	import { FluxCaption } from '../complements';
	import { Img } from '../resources';
	import { Book, Zip } from '../transitions';
	import { Player } from '../controllers';

	const $vueFlux = ref();

	const options = shallowReactive({
		autoplay: true,
	});

	const rscs = shallowReactive([
		new Img(`/images/01.jpg`, 'img 01'),
		new Img(`/images/02.jpg`, 'img 02'),
		new Img(`/images/03.jpg`, 'img 03'),
		new Img(`/images/04.jpg`, 'img 04'),
		new Img(`/images/05.jpg`, 'img 05'),
		new Img(`/images/06.jpg`, 'img 06'),
	]);

	const transitions = shallowReactive([Book, Zip]);

	const player: Ref<null | Player> = ref(null);

	onMounted(() => {
		player.value = $vueFlux.value.getPlayer();
	});
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux :options="options" :rscs="rscs" :transitions="transitions">
			<template #caption="captionProps">
				<FluxCaption v-bind="captionProps" />
			</template>
		</VueFlux>

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux ref="$vueFlux" :options="options" :rscs="rscs" :transitions="transitions" />

		<FluxCaption v-if="player" :player="player" />

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxControls.vue =====
<script setup lang="ts">
	import { ref, shallowReactive, onMounted } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import { VueFlux } from '../components';
	import { FluxControls } from '../complements';
	import { Img } from '../resources';
	import { Book, Zip } from '../transitions';

	const $vueFlux = ref();

	const options = shallowReactive({
		autoplay: true,
	});

	const rscs = shallowReactive([
		new Img(`/images/01.jpg`, 'img 01'),
		new Img(`/images/02.jpg`, 'img 02'),
		new Img(`/images/03.jpg`, 'img 03'),
		new Img(`/images/04.jpg`, 'img 04'),
		new Img(`/images/05.jpg`, 'img 05'),
		new Img(`/images/06.jpg`, 'img 06'),
	]);

	const transitions = shallowReactive([Book, Zip]);

	const mounted = ref(false);

	onMounted(() => {
		mounted.value = true;
	});
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux :options="options" :rscs="rscs" :transitions="transitions">
			<template #controls="controlsProps">
				<FluxControls v-bind="controlsProps" />
			</template>
		</VueFlux>

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux
			ref="$vueFlux"
			:options="options"
			:rscs="rscs"
			:transitions="transitions"
		/>

		<FluxControls v-if="mounted" :player="$vueFlux.getPlayer()" />

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxCube.vue =====
<script setup lang="ts">
	import { ref, type Ref } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import Img from '../resources/Img/Img';
	import Size from '../shared/Size/Size';
	import { ResizeTypes } from '../resources';
	import { FluxCube, Turns } from '../components';
	import PgButton from './components/PgButton.vue';

	const $fluxCube: Ref<null | InstanceType<typeof FluxCube>> = ref(null);

	const origins = {
		['auto (undefined)']: undefined,
		['left top']: 'left top',
		['left center']: 'left center',
		['left bottom']: 'left bottom',
		['center top']: 'center top',
		['center center']: 'center center',
		['center bottom']: 'center bottom',
		['right top']: 'right top',
		['right center']: 'right center',
		['right bottom']: 'right bottom',
	};

	const colors = {
		front: '#ccc',
		left: '#ccc',
		right: '#ccc',
		top: '#ccc',
		bottom: '#ccc',
		back: '#ccc',
	};

	const rscs = {
		front: new Img(`/images/01.jpg`, 'img 01'),
		left: new Img(`/images/02.jpg`, 'img 02', ResizeTypes.fit),
		right: new Img(`/images/03.jpg`, 'img 03', ResizeTypes.fit),
		top: new Img(`/images/04.jpg`, 'img 04', ResizeTypes.fit),
		bottom: new Img(`/images/05.jpg`, 'img 05', ResizeTypes.fit),
		back: new Img(`/images/06.jpg`, 'img 06', ResizeTypes.fit),
	};

	const size = new Size({
		width: 640,
		height: 360,
	});

	const depth: Ref<number> = ref(160);
	const origin: Ref<undefined | string> = ref(undefined);
	const turnTo: Ref<Turns> = ref(Turns.right);

	function turn() {
		$fluxCube.value?.turn(turnTo.value);
	}
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<div style="perspective: 1600px" class="my-20">
			<FluxCube
				ref="$fluxCube"
				:colors="colors"
				:rscs="rscs"
				:depth="depth"
				:size="size"
				:origin="origin"
				style="transition: all 2000ms ease-out 0s"
			/>
		</div>

		<label>
			<span>Depth:</span>

			<input v-model.number="depth" type="number" style="width: 60px" />
		</label>

		<label>
			<span>Origin:</span>

			<select v-model="origin">
				<option v-for="(value, key) in origins" :key="key" :value="value">
					{{ key }}
				</option>
			</select>
		</label>

		<label>
			<span>Turn:</span>

			<select v-model="turnTo">
				<option v-for="(value, key) in Turns" :key="key" :value="value">
					{{ key }}
				</option>
			</select>

			<PgButton @click="turn()"> Turn </PgButton>
		</label>

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxGrid.vue =====
<script setup lang="ts">
	import { ref, type Ref } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import Img from '../resources/Img/Img';
	import Size from '../shared/Size/Size';
	import { FluxGrid } from '../components';
	import { ResizeTypes } from '../resources';

	const $fluxGridImage: Ref<null | InstanceType<typeof FluxGrid>> = ref(null);
	const $fluxGridCube: Ref<null | InstanceType<typeof FluxGrid>> = ref(null);

	const rsc = new Img(`/images/01.jpg`, 'img 01', ResizeTypes.fit);

	const size = new Size({
		width: 640,
		height: 360,
	});

	const colors = {
		front: '#ccc',
		left: '#ccc',
		right: '#ccc',
		top: '#ccc',
		bottom: '#ccc',
		back: '#ccc',
	};

	const rscs = {
		front: new Img(`/images/01.jpg`, 'img 01'),
		left: new Img(`/images/02.jpg`, 'img 02', ResizeTypes.fit),
		right: new Img(`/images/03.jpg`, 'img 03', ResizeTypes.fit),
		top: new Img(`/images/04.jpg`, 'img 04', ResizeTypes.fit),
		bottom: new Img(`/images/05.jpg`, 'img 05', ResizeTypes.fit),
		back: new Img(`/images/06.jpg`, 'img 06', ResizeTypes.fit),
	};

	const depth: Ref<number> = ref(160);
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxGrid ref="$fluxGridImage" :rsc="rsc" :size="size" :rows="10" :cols="5" color="#ccc" />

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxGrid
			ref="$fluxGridCube"
			:rscs="rscs"
			:size="size"
			:rows="10"
			:cols="5"
			:depth="depth"
			:colors="colors"
		/>

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxImage.vue =====
<script setup lang="ts">
	import { ref, type Ref } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import Img from '../resources/Img/Img';
	import Size from '../shared/Size/Size';
	import { ResizeTypes } from '../resources';
	import { FluxImage } from '../components';

	const $fluxImage: Ref<null | InstanceType<typeof FluxImage>> = ref(null);

	const rscLandscapeFill = new Img(`/images/01.jpg`, 'img 01 fill');

	const rscLandscapeFit = new Img(`/images/01.jpg`, 'img 01 fit', ResizeTypes.fit);

	const rscPortraitFill = new Img(`/images/00.jpg`, 'img 00 fill');

	const rscPortraitFit = new Img(`/images/00.jpg`, 'img 00 fit', ResizeTypes.fit, '#111');

	const sizeLandscape = new Size({
		width: 640,
		height: 360,
	});

	const sizePortrait = new Size({
		width: 211,
		height: 360,
	});
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxImage ref="$fluxImage" :rsc="rscLandscapeFill" :size="sizeLandscape" />

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxImage ref="$fluxImage" :rsc="rscLandscapeFit" :size="sizeLandscape" color="#ccc" />

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxImage ref="$fluxImage" :rsc="rscLandscapeFill" :size="sizePortrait" />

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxImage ref="$fluxImage" :rsc="rscLandscapeFit" :size="sizePortrait" color="#ccc" />

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxImage ref="$fluxImage" :rsc="rscPortraitFill" :size="sizePortrait" />

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxImage ref="$fluxImage" :rsc="rscPortraitFit" :size="sizePortrait" color="#ccc" />

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxImage ref="$fluxImage" :rsc="rscPortraitFill" :size="sizeLandscape" />

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxImage ref="$fluxImage" :rsc="rscPortraitFit" :size="sizeLandscape" color="#ccc" />

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxIndex.vue =====
<script setup lang="ts">
	import { ref, shallowReactive, onMounted, type Ref } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import { VueFlux } from '../components';
	import { FluxIndex } from '../complements';
	import { Img } from '../resources';
	import { Book, Zip } from '../transitions';
	import { Player } from '../controllers';

	const $vueFlux = ref();

	const options = shallowReactive({
		autoplay: false,
	});

	const rscs = shallowReactive([
		new Img(`/images/01.jpg`, 'img 01'),
		new Img(`/images/02.jpg`, 'img 02'),
		new Img(`/images/03.jpg`, 'img 03'),
		new Img(`/images/04.jpg`, 'img 04'),
		new Img(`/images/05.jpg`, 'img 05'),
		new Img(`/images/06.jpg`, 'img 06'),
	]);

	const transitions = shallowReactive([Book, Zip]);

	const player: Ref<null | Player> = ref(null);

	onMounted(() => {
		player.value = $vueFlux.value.getPlayer();
	});
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux :options="options" :rscs="rscs" :transitions="transitions">
			<template #index="indexProps">
				<FluxIndex v-bind="indexProps" />
			</template>
		</VueFlux>

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux ref="$vueFlux" :options="options" :rscs="rscs" :transitions="transitions" />

		<FluxIndex v-if="player" :display-size="$vueFlux.size" :player="player" />

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxPagination.vue =====
<script setup lang="ts">
	import { ref, shallowReactive, onMounted, type Ref } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import { VueFlux } from '../components';
	import { FluxPagination } from '../complements';
	import { Img } from '../resources';
	import { Book, Zip } from '../transitions';
	import { Player } from '../controllers';

	const $vueFlux = ref();

	const options = shallowReactive({
		autoplay: false,
	});

	const rscs = shallowReactive([
		new Img(`/images/01.jpg`, 'img 01'),
		new Img(`/images/02.jpg`, 'img 02'),
		new Img(`/images/03.jpg`, 'img 03'),
		new Img(`/images/04.jpg`, 'img 04'),
		new Img(`/images/05.jpg`, 'img 05'),
		new Img(`/images/06.jpg`, 'img 06'),
	]);

	const transitions = shallowReactive([Book, Zip]);

	const player: Ref<null | Player> = ref(null);

	onMounted(() => {
		player.value = $vueFlux.value.getPlayer();
	});
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux :options="options" :rscs="rscs" :transitions="transitions">
			<template #pagination="paginationProps">
				<FluxPagination v-bind="paginationProps" />
			</template>
		</VueFlux>

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux ref="$vueFlux" :options="options" :rscs="rscs" :transitions="transitions" />

		<FluxPagination v-if="player" :player="player">
			<template #default="pageProps">
				<span
					:title="pageProps.title"
					:class="pageProps.cssClass"
					@click="player.show(pageProps.index)"
				>
					{{ pageProps.index }}
				</span>
			</template>
		</FluxPagination>

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxParallax.vue =====
<script setup lang="ts">
	import { VcParagraph } from 'vue-cosk';
	import Img from '../resources/Img/Img';
	import { FluxParallax } from '../components';

	const rsc = new Img(`/images/01.jpg`, 'img 01');
</script>

<template>
	<div>
		<VcParagraph v-for="i of 6" :key="'a' + i" mode="fill" style="margin: 24px 0" />

		<FluxParallax type="fixed" :rsc="rsc" style="height: 200px" />

		<VcParagraph v-for="i of 6" :key="'b' + i" mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxParallaxOp.vue =====
<script lang="ts">
	import { defineComponent, markRaw } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import Img from '../resources/Img/Img';
	import { FluxParallax } from '../components';

	export default defineComponent({
		components: {
			VcParagraph,
			FluxParallax,
		},
		data() {
			return {
				rsc: markRaw(new Img(`/images/01.jpg`, 'img 01')),
			};
		},
	});
</script>

<template>
	<div>
		<VcParagraph v-for="i of 6" :key="'a' + i" mode="fill" style="margin: 24px 0" />

		<FluxParallax type="fixed" :rsc="rsc" style="height: 200px" />

		<VcParagraph v-for="i of 6" :key="'b' + i" mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgFluxPreloader.vue =====
<script setup lang="ts">
	import { shallowReactive } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import { VueFlux } from '../components';
	import { FluxPreloader } from '../complements';
	import { Img } from '../resources';
	import { Book, Zip } from '../transitions';

	const options = shallowReactive({
		autoplay: false,
	});

	const rscs = shallowReactive([
		new Img(`/images/01.jpg`, 'img 01'),
		new Img(`/images/02.jpg`, 'img 02'),
		new Img(`/images/03.jpg`, 'img 03'),
		new Img(`/images/04.jpg`, 'img 04'),
		new Img(`/images/05.jpg`, 'img 05'),
		new Img(`/images/06.jpg`, 'img 06'),
		new Img(`/images/07.jpg`, 'img 07'),
		new Img(`/images/08.jpg`, 'img 08'),
		new Img(`/images/09.jpg`, 'img 09'),
		new Img(`/images/10.jpg`, 'img 10'),
		new Img(`/images/11.jpg`, 'img 11'),
		new Img(`/images/12.jpg`, 'img 12'),
		new Img(`/images/13.jpg`, 'img 13'),
		new Img(`/images/14.jpg`, 'img 14'),
		new Img(`/images/15.jpg`, 'img 15'),
	]);

	const transitions = shallowReactive([Book, Zip]);
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux :options="options" :rscs="rscs" :transitions="transitions">
			<template #preloader="preloaderProps">
				<FluxPreloader v-bind="preloaderProps" />
			</template>
		</VueFlux>

		<VcParagraph mode="fill" style="margin: 24px 0" />

		<VueFlux :options="options" :rscs="rscs" :transitions="transitions">
			<template #preloader="preloaderProps">
				<FluxPreloader v-bind="preloaderProps">
					<template #default="props">
						<div v-if="props.preloading" class="custom-spinner">
							{{ props.pct }} %
						</div>
					</template>
				</FluxPreloader>
			</template>
		</VueFlux>

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>

<style lang="scss">
	@keyframes spinner {
		to {
			transform: rotate(360deg);
		}
	}

	.custom-spinner {
		position: absolute;
		top: 50%;
		left: 50%;
		text-align: center;
		line-height: 50px;
		margin-top: -25px;
		margin-left: -25px;
		width: 50px;
		height: 50px;
		z-index: 14;

		&:before {
			content: '';
			box-sizing: border-box;
			position: absolute;
			top: 50%;
			left: 50%;
			width: 50px;
			height: 50px;
			margin-top: -25px;
			margin-left: -25px;
			border-radius: 50%;
			border: 1px solid #ccc;
			border-top-color: #07d;
			animation: spinner 0.6s linear infinite;
		}
	}
</style>


===== FILE: src/playgrounds/PgFluxTransition.vue =====
<script setup lang="ts">
	import { nextTick, ref, type Ref, shallowRef } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import Img from '../resources/Img/Img';
	import Size from '../shared/Size/Size';
	import { FluxTransition } from '../components';
	import {
		Fade,
		Kenburn,
		Swipe,
		Slide,
		Waterfall,
		Zip,
		Blinds2D,
		Blocks1,
		Blocks2,
		Concentric,
		Warp,
		Camera,
		Cube,
		Book,
		Fall,
		Wave,
		Blinds3D,
		Round1,
		Round2,
		Explode,
	} from '../transitions';
	import PgButton from './components/PgButton.vue';

	const $fluxTransition: Ref<null | InstanceType<typeof FluxTransition>> = ref(null);

	const transitions = {
		Fade,
		Kenburn,
		Swipe,
		Slide,
		Waterfall,
		Zip,
		Blinds2D,
		Blocks1,
		Blocks2,
		Concentric,
		Warp,
		Camera,
		Cube,
		Book,
		Fall,
		Wave,
		Blinds3D,
		Round1,
		Round2,
		Explode,
	};

	const enabled = ref(true);

	const transition = shallowRef(Fade);
	const rscFrom = new Img(`/images/05.jpg`, 'img 05 fill');
	const rscTo = new Img(`/images/06.jpg`, 'img 06 fit');

	const size = new Size({
		width: 640,
		height: 360,
	});

	async function reset() {
		await nextTick();

		enabled.value = false;

		await nextTick();

		enabled.value = true;
	}
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0" />

		<FluxTransition
			v-if="enabled"
			ref="$fluxTransition"
			:size="size"
			:transition="transition"
			:from="rscFrom"
			:to="rscTo"
			@end="reset"
		/>

		<label v-if="$fluxTransition" class="mt-6">
			<span>Transition</span>

			<select v-model="transition">
				<option v-for="(component, name) in transitions" :key="name" :value="component">
					{{ name }}
				</option>
			</select>

			<PgButton @click="$fluxTransition.start()">Start</PgButton>
		</label>

		<VcParagraph mode="fill" style="margin: 24px 0" />
	</div>
</template>


===== FILE: src/playgrounds/PgVueFlux.vue =====
<script setup lang="ts">
	import { ref, type Ref, shallowReactive } from 'vue';
	import { VcParagraph } from 'vue-cosk';
	import { Img } from '../resources';
	import {
		Fade,
		Kenburn,
		Swipe,
		Slide,
		Waterfall,
		Zip,
		Blinds2D,
		Blocks1,
		Blocks2,
		Concentric,
		Warp,
		Camera,
		Cube,
		Book,
		Fall,
		Wave,
		Blinds3D,
		Round1,
		Round2,
		Explode,
	} from '../transitions';
	import * as Complements from '../complements';
	import { VueFlux } from '../components';
	import PgButton from './components/PgButton.vue';
	import ResizeTypes from '../resources/ResizeTypes';
	import { Directions } from '../controllers';

	const $vueFlux: Ref<null | InstanceType<typeof VueFlux>> = ref(null);

	const options = shallowReactive({
		allowFullscreen: true,
		autoplay: false,
		bindKeys: true,
		infinite: true,
		delay: 5000,
		lazyLoadAfter: 10,
	});

	const images = [];
	for (let i = 1; i <= 20; i++) {
		const fileName = i.toString().padStart(2, '0');
		const image = new Img(
			`/images/${fileName}.jpg`,
			'img ' + i,
			ResizeTypes.fill, //i % 2 === 0 ? ResizeTypes.fit : ResizeTypes.fill
		);
		images.push(image);
	}

	const rscs = shallowReactive(images);

	const transitions = {
		Blinds2D,
		Blinds3D,
		Blocks1,
		Blocks2,
		Book,
		Camera,
		Concentric,
		Cube,
		Explode,
		Fade,
		Fall,
		Kenburn,
		Round1,
		Round2,
		Slide,
		Swipe,
		Warp,
		Waterfall,
		Wave,
		Zip,
	};

	const transitionComponents = shallowReactive(Object.values(transitions));

	const transitionNames = Object.keys(transitions);

	const currentTransitionName = ref(null);

	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	function updateCurrentTransition(_rsc?: any, transition?: any) {
		if (transition.current !== null) {
			currentTransitionName.value = transition.current.component.__name;
		} else {
			currentTransitionName.value = null;
		}
	}
</script>

<template>
	<div>
		<VcParagraph mode="fill" style="margin: 24px 0; padding: 0" />

		<div class="block sm:block md:block lg:flex">
			<div class="lg:w-3/4">
				<VueFlux
					ref="$vueFlux"
					:transitions="transitionComponents"
					:rscs="rscs"
					:options="options"
					@transitionStart="updateCurrentTransition"
					@transitionEnd="updateCurrentTransition"
				>
					<template #preloader="preloaderProps">
						<Complements.FluxPreloader v-bind="preloaderProps" />
					</template>

					<template #caption="captionProps">
						<Complements.FluxCaption v-bind="captionProps" />
					</template>

					<template #controls="controlsProps">
						<Complements.FluxControls v-bind="controlsProps" />
					</template>

					<template #index="indexProps">
						<Complements.FluxIndex v-bind="indexProps" />
					</template>

					<template #pagination="paginationProps">
						<Complements.FluxPagination v-bind="paginationProps" />
					</template>
				</VueFlux>
			</div>

			<div class="lg:w-1/4 lg:ml-4 lg:mt-0 mt-6">
				<ul v-if="$vueFlux && $vueFlux.size.isValid()" class="flex flex-wrap">
					<li
						v-for="(name, index) in transitionNames"
						:key="name"
						class="odd:pr-4 mb-4 lg:w-1/2 lg:mr-0 mr-4"
					>
						<PgButton
							class="w-100"
							:active="currentTransitionName === name"
							@click="$vueFlux.show(Directions.next, index)"
						>
							{{ name }}
						</PgButton>
					</li>
				</ul>
			</div>
		</div>

		<div v-if="$vueFlux" class="mt-6 lg:flex">
			<PgButton class="mr-4 w-1/3" @click="$vueFlux.show()">Next</PgButton>
			<PgButton class="mr-4 w-1/3" @click="$vueFlux.play()">Play</PgButton>
			<PgButton class="w-1/3 mr-0" @click="$vueFlux.stop()">Stop</PgButton>
		</div>

		<VcParagraph mode="fill" style="margin: 24px 0; padding: 0" />
	</div>
</template>


===== FILE: src/playgrounds/components/PgButton.vue =====
<script setup lang="ts">
	const props = withDefaults(defineProps<{ active?: boolean }>(), {
		active: false,
	});
</script>

<template>
	<button
		class="hover:bg-sky-700 text-white rounded px-2 w-full cursor-pointer"
		:class="props.active ? 'bg-amber-500' : 'bg-sky-500'"
	>
		<slot />
	</button>
</template>


===== FILE: src/repositories/Resources/Resources.test.ts =====
import { Directions } from '../../controllers';
import { Resource } from '../../resources';
import { Size } from '../../shared';
import { default as ResourcesRepository } from './Resources';
import ResourceFactory from '../../resources/__test__/ResourceFactory';
import emit from '../../components/VueFlux/__test__/emit';

vi.mock('../../resources/Img/Img');
vi.mock('../../shared/ResourceLoader/ResourceLoader');

describe('repositories: Resources', () => {
	let repo: ResourcesRepository;
	let resources: Resource[];
	const size: Size = new Size({
		width: 640,
		height: 360,
	});

	describe('width preloading', () => {
		beforeEach(async () => {
			vi.clearAllMocks();

			repo = new ResourcesRepository(emit);

			resources = ResourceFactory.create(5);
			await repo.update(resources, 5, size);
		});

		it('updates the repository transitions', () => {
			expect(repo.list).toHaveLength(5);
		});

		it('emits when preload starts', () => {
			expect(emit).toHaveBeenCalledWith('resourcesPreloadStart');
		});

		it('emits when preload ends', () => {
			expect(emit).toHaveBeenCalledWith('resourcesPreloadEnd');
		});

		it('gets the first resource', () => {
			expect(repo.getFirst().rsc).toBe(resources[0]);
		});

		it('gets the last resource', () => {
			expect(repo.getLast().rsc).toBe(resources[4]);
		});

		it('get resource by index', () => {
			expect(repo.getByIndex(2).rsc).toBe(resources[2]);
		});

		it('throws error because the requested index does not exist', () => {
			const index = resources.length + 1;

			expect(() => repo.getByIndex(index)).toThrow(
				`Resource index ${index} not found`
			);
		});

		it('get resource by order next', () => {
			expect(
				repo.getByOrder(Directions.next, resources.length - 1).rsc
			).toBe(resources[0]);
		});

		it('get resource by order prev', () => {
			expect(repo.getByOrder(Directions.prev, 0).rsc).toBe(
				resources[resources.length - 1]
			);
		});

		it('throws an error when trying to find a resource by order without passing the current index', () => {
			expect(() => repo.find(Directions.next)).toThrow(
				'Missing currentIndex parameter'
			);
		});
	});

	describe('with lazy loading', () => {
		const numResources = 10;
		const resourcesToPreload = 5;

		beforeEach(async () => {
			vi.clearAllMocks();

			repo = new ResourcesRepository(emit);

			resources = ResourceFactory.create(numResources);
			await repo.update(resources, resourcesToPreload, size);
		});

		it('emits resourcesLazyloadStart when start lazy loading', () =>
			new Promise<void>((done) => {
				expect(emit).toHaveBeenCalledWith('resourcesLazyloadStart');
				done();
			}));

		it('emits resourcesLazyloadStart when start lazy loading', () =>
			new Promise<void>((done) => {
				expect(repo.list).toHaveLength(numResources);
				expect(emit).toHaveBeenCalledWith('resourcesLazyloadEnd');
				done();
			}));
	});
});


===== FILE: src/repositories/Resources/Resources.ts =====
import { type Ref, ref, shallowReactive } from 'vue';
import { Resource, type ResourceWithOptions } from '../../resources';
import { Size, ResourceLoader } from '../../shared';
import { type Direction, Directions } from '../../controllers/Player';
import type { ResourceIndex } from './types';
import ResourcesMapper from './ResourcesMapper';
import type { VueFluxEmits } from '../../components';

export default class Resources {
	list: ResourceWithOptions[] = shallowReactive([]);
	loader: Ref<ResourceLoader | null> = ref(null);
	emit: VueFluxEmits;

	constructor(emit: VueFluxEmits) {
		this.emit = emit;
	}

	private getPrev(currentIndex: number) {
		return this.getByIndex(currentIndex > 0 ? currentIndex - 1 : this.list.length - 1);
	}

	private getNext(currentIndex: number) {
		return this.getByIndex(currentIndex === this.list.length - 1 ? 0 : currentIndex + 1);
	}

	getFirst() {
		return this.getByIndex(0);
	}

	getLast() {
		return this.getByOrder(Directions.prev, 0);
	}

	getByIndex(index: number) {
		if (this.list[index] === undefined) {
			throw new ReferenceError(`Resource index ${index} not found`);
		}

		return {
			index,
			rsc: this.list[index].resource,
			options: JSON.parse(JSON.stringify(this.list[index].options)),
		} as ResourceIndex;
	}

	getByOrder(order: Direction, currentIndex: number) {
		return {
			prev: () => this.getPrev(currentIndex),
			next: () => this.getNext(currentIndex),
		}[order]();
	}

	find(by: number | Direction, currentIndex?: number) {
		if (typeof by === 'number') {
			return this.getByIndex(by);
		}

		if (currentIndex === undefined) {
			throw new ReferenceError('Missing currentIndex parameter');
		}

		return this.getByOrder(by, currentIndex);
	}

	update(rscs: (Resource | ResourceWithOptions)[], numToPreload: number, displaySize: Size) {
		if (this.loader.value?.hasFinished() === false) {
			this.loader.value?.cancel();
		}

		this.list.splice(0);

		const resources = ResourcesMapper.withOptions(rscs);

		const updatePromise = new Promise<void>((resolve, reject) => {
			this.loader.value = new ResourceLoader(
				resources,
				numToPreload,
				displaySize,
				() => this.preloadStart(),
				(loaded: ResourceWithOptions[]) => this.preloadEnd(loaded, resolve),
				() => this.lazyLoadStart(),
				(loaded: ResourceWithOptions[]) => this.lazyLoadEnd(loaded),
				reject,
			);
		});

		return updatePromise;
	}

	preloadStart() {
		this.emit('resourcesPreloadStart');
	}

	preloadEnd(loaded: ResourceWithOptions[], resolve: () => void) {
		this.list.push(...loaded);

		this.emit('resourcesPreloadEnd');

		resolve();
	}

	lazyLoadStart() {
		this.emit('resourcesLazyloadStart');
	}

	lazyLoadEnd(loaded: ResourceWithOptions[]) {
		this.list.push(...loaded);

		this.emit('resourcesLazyloadEnd');
	}
}


===== FILE: src/repositories/Resources/ResourcesMapper.test.ts =====
import { Img, type ResourceWithOptions } from '../../resources';
import ResourcesMapper from './ResourcesMapper';

describe('repositories: ResourcesMapper', () => {
	it('turns all the transitions array as transitions with options', () => {
		const resources = [
			new Img('url1'),
			{
				resource: new Img('url2'),
				options: {
					delay: 8000,
				},
			} as ResourceWithOptions,
			new Img('url3'),
		];

		const resourcesWithOptions = ResourcesMapper.withOptions(resources);

		expect(resourcesWithOptions[0]!.resource).toBe(resources[0]);
		// @ts-expect-error:next-line
		expect(resourcesWithOptions[1]!.resource).toBe(resources[1].resource);
		expect(resourcesWithOptions[1]!.options.delay).toBe(8000);
		expect(resourcesWithOptions[2]!.resource).toBe(resources[2]);
		expect(resourcesWithOptions[2]!.options).toStrictEqual({});
	});
});


===== FILE: src/repositories/Resources/ResourcesMapper.ts =====
import { Resource, type ResourceWithOptions } from '../../resources';

export default class ResourcesMapper {
	static withOptions(rscs: (Resource | ResourceWithOptions)[]) {
		return rscs.map((rsc) => {
			let resource = rsc;
			let options = {};

			if ('resource' in rsc) {
				resource = rsc.resource as Resource;

				if ('options' in rsc) {
					options = rsc.options as object;
				}
			}

			return { resource, options } as ResourceWithOptions;
		});
	}
}


===== FILE: src/repositories/Resources/types.ts =====
import Resource from '../../resources/Resource';

export interface ResourceIndex {
	index: number;
	rsc: Resource;
	options: {
		delay?: number;
		stop?: boolean;
	};
}


===== FILE: src/repositories/Transitions/Transitions.test.ts =====
import { Directions } from '../../controllers';
import { default as TransitionsRepository } from './Transitions';

function transitionsFactory(numTransitions: number) {
	return new Array(numTransitions).fill({});
}

describe('repositories: Transitions', () => {
	let repo: TransitionsRepository;
	let transitions: object[];

	beforeEach(() => {
		repo = new TransitionsRepository();
	});

	it('updates the repository transitions', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.list).toHaveLength(5);
	});

	it('removes the previous transitions on update', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.list).toHaveLength(5);

		transitions = transitionsFactory(2);
		repo.update(transitions);
		expect(repo.list).toHaveLength(2);
	});

	it('gets the first transition', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.getFirst().component).toBe(transitions[0]);
	});

	it('gets the last transition', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.getLast().component).toBe(
			transitions[transitions.length - 1]
		);
	});

	it('gets the transition by an index number', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.getByIndex(2).component).toBe(transitions[2]);
	});

	it('gets the transition by order next', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.getByOrder(Directions.next, 2).component).toBe(
			transitions[3]
		);
	});

	it('gets fist the transition by order next', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.getByOrder(Directions.next, 4).component).toBe(
			transitions[3]
		);
	});

	it('gets the transition by order previous', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.getByOrder(Directions.prev, 2).component).toBe(
			transitions[1]
		);
	});

	it('gets the last transition by order previous', () => {
		transitions = transitionsFactory(5);
		repo.update(transitions);

		expect(repo.getByOrder(Directions.prev, 0).component).toBe(
			transitions[1]
		);
	});
});


===== FILE: src/repositories/Transitions/Transitions.ts =====
import { type Component, shallowReactive } from 'vue';
import { Directions, type Direction } from '../../controllers/Player';
import type { TransitionIndex } from './types';
import type { TransitionWithOptions } from '../../transitions/types';
import TransitionsMapper from './TransitionsMapper';

export default class Transitions {
	list: TransitionWithOptions[] = shallowReactive([]);

	private getPrev(lastIndex: number) {
		return this.getByIndex(lastIndex > 0 ? lastIndex - 1 : this.list.length - 1);
	}

	private getNext(lastIndex: number) {
		return this.getByIndex(lastIndex === this.list.length - 1 ? 0 : lastIndex + 1);
	}

	getFirst() {
		return this.getByIndex(0);
	}

	getLast() {
		return this.getByOrder(Directions.prev, 0);
	}

	getByIndex(index: number) {
		const item = this.list[index];

		if (!item) {
			throw new Error(`Transition index ${index} out of range`);
		}

		return {
			index,
			component: item.component,
			options: JSON.parse(JSON.stringify(item.options)),
		} as TransitionIndex;
	}

	getByOrder(direction: Direction, lastIndex: number) {
		return {
			prev: () => this.getPrev(lastIndex),
			next: () => this.getNext(lastIndex),
		}[direction]();
	}

	update(transitions: (Component | TransitionWithOptions)[]) {
		this.list.splice(0);

		const transitionsWithOptions = TransitionsMapper.withOptions(transitions);

		this.list.push(...transitionsWithOptions);
	}
}


===== FILE: src/repositories/Transitions/TransitionsMapper.test.ts =====
import TransitionsMapper from './TransitionsMapper';
import { Fade, Kenburn, Swipe, Slide, type TransitionWithOptions } from '../../transitions';

describe('repositories: TransitionsMapper', () => {
	it('turns all the transitions array as transitions with options', () => {
		const transitions = [
			Fade,
			{
				component: Kenburn,
				options: { totalDuration: 1600 },
			} as TransitionWithOptions,
			Swipe,
			{
				component: Slide,
				options: { totalDuration: 4600 },
			} as TransitionWithOptions,
		];

		const transitionsWithOptions = TransitionsMapper.withOptions(transitions);

		expect(transitionsWithOptions[0]!.component).toBe(Fade);
		expect(transitionsWithOptions[1]!.component).toBe(Kenburn);
		// @ts-expect-error:next-line
		expect(transitionsWithOptions[1]!.options.totalDuration).toBe(1600);
		expect(transitionsWithOptions[2]!.component).toBe(Swipe);
		expect(transitionsWithOptions[3]!.component).toBe(Slide);
		// @ts-expect-error:next-line
		expect(transitionsWithOptions[3]!.options.totalDuration).toBe(4600);
	});
});


===== FILE: src/repositories/Transitions/TransitionsMapper.ts =====
import type { Component } from 'vue';
import type { TransitionComponent, TransitionWithOptions } from '../../transitions';

export default class TransitionsMapper {
	static withOptions(transitions: (Component | TransitionWithOptions)[]) {
		return transitions.map((transition) => {
			let component = transition;
			let options = {};

			if ('component' in transition) {
				component = transition.component as TransitionComponent;

				if ('options' in transition) {
					options = transition.options;
				}
			}

			return { component, options } as TransitionWithOptions;
		});
	}
}


===== FILE: src/repositories/Transitions/types.ts =====
import type { Component } from 'vue';
import type { Direction } from '../../controllers/Player';

export interface TransitionIndex {
	index: number;
	component: Component;
	options: {
		direction?: Direction;
	};
}


===== FILE: src/repositories/index.ts =====
export { default as Resources } from './Resources/Resources';
export { default as Transitions } from './Transitions/Transitions';

export type { ResourceIndex } from './Resources/types';
export type { TransitionIndex } from './Transitions/types';


===== FILE: src/resources/Img/Img.test.ts =====
import { FluxImage } from '../../components';
import ResizeTypes from '../ResizeTypes';
import Statuses from '../Statuses';
import type { DisplayParameter, ResizeType, TransitionParameter } from '../types';
import Img from './Img';

describe('resources: Img', () => {
	let img,
		src: string,
		caption: string,
		resizeType: ResizeType,
		backgroundColor: string,
		promise: Promise<void>,
		resolve: () => void,
		reject: (message: string) => void;

	beforeEach(() => {
		src = 'src';
		caption = 'caption';
		resizeType = ResizeTypes.fill;
		backgroundColor = '#ccc';
	});

	it('creates the instance properly with default params', () => {
		img = new Img(src);

		expect(img.src).toBe(src);
		expect(img.caption).toBe('');
		expect(img.resizeType).toBe(ResizeTypes.fill);
		expect(img.backgroundColor).toBeNull();
	});

	it('creates the instance properly with custom params', () => {
		img = new Img(src, caption, resizeType, backgroundColor);

		expect(img.src).toBe(src);
		expect(img.caption).toBe(caption);
		expect(img.resizeType).toBe(resizeType);
		expect(img.backgroundColor).toBe(backgroundColor);
	});

	it('creates the instance with the required parameters of abstract Resource', () => {
		img = new Img(src);

		expect(img.display).toStrictEqual({
			component: FluxImage,
			props: {},
		} as DisplayParameter);

		expect(img.transition).toStrictEqual({
			component: FluxImage,
			props: {},
		} as TransitionParameter);

		expect(img.errorMessage).toBe(`Image ${src} could not be loaded`);
	});

	it('returns a promise and sets it to property loader', () => {
		img = new Img(src);

		promise = img.load();
		expect(promise).toBeTypeOf('object');
		expect(img.loader).toBe(promise);
	});

	it('changes the status to loading', () => {
		img = new Img(src);
		img.load();

		expect(img.status.value).toBe(Statuses.loading);
	});

	it('returns the loader if already request to load', () => {
		img = new Img(src);
		promise = img.load();

		expect(img.load()).toBe(promise);
	});

	it.todo('calls onLoad when load success');

	it('sets reals size on load', () => {
		src = '/imgs/pixel.png';
		img = new Img(src);

		const htmlImage = new Image();
		htmlImage.width = 640;
		htmlImage.height = 480;

		resolve = vi.fn();
		img.onLoad(htmlImage, resolve);

		expect(img.realSize.toValue()).toStrictEqual({ width: 640, height: 480 });
	});

	it('sets status loaded on load', () => {
		src = '/imgs/pixel.png';
		img = new Img(src);

		const htmlImage = new Image();
		htmlImage.width = 640;
		htmlImage.height = 480;

		resolve = vi.fn();
		img.onLoad(htmlImage, resolve);

		expect(img.status.value).toBe(Statuses.loaded);
	});

	it('calls promise resolve on load', () => {
		src = '/imgs/pixel.png';
		img = new Img(src);

		const htmlImage = new Image();
		htmlImage.width = 640;
		htmlImage.height = 480;

		resolve = vi.fn();
		img.onLoad(htmlImage, resolve);

		expect(resolve).toHaveBeenCalledOnce();
	});

	it.todo('calls onError when load fails');

	it('sets the status to error on error', () => {
		img = new Img(src);

		reject = vi.fn();
		img.onError(reject);

		expect(img.status.value).toBe(Statuses.error);
	});

	it('performs promise reject on error', () => {
		img = new Img(src);

		reject = vi.fn();
		img.onError(reject);

		expect(reject).toHaveBeenCalledWith(img.errorMessage);
	});
});


===== FILE: src/resources/Img/Img.ts =====
import { FluxImage } from '../../components';
import { Resource, Statuses, ResizeTypes } from '../';
import { Size } from '../../shared';
import type { DisplayParameter, ResizeType, TransitionParameter } from '../types';

export default class Img extends Resource {
	constructor(
		src: string,
		caption: string = '',
		resizeType: ResizeType = ResizeTypes.fill,
		backgroundColor: null | string = null,
	) {
		const display: DisplayParameter = {
			component: FluxImage,
			props: {},
		};

		const transition: TransitionParameter = {
			component: FluxImage,
			props: {},
		};

		const errorMessage = `Image ${src} could not be loaded`;

		super(src, caption, resizeType, backgroundColor, display, transition, errorMessage);
	}

	load() {
		if (this.loader !== null) {
			return this.loader;
		}

		this.loader = new Promise<void>((resolve, reject) => {
			this.status.value = Statuses.loading;

			const img = new Image();

			img.onload = () => this.onLoad(img, resolve);
			img.onerror = () => this.onError(reject);

			img.src = this.src;
		});

		return this.loader;
	}

	onLoad(img: HTMLImageElement, resolve: () => void) {
		this.realSize = new Size({
			width: img.naturalWidth || img.width,
			height: img.naturalHeight || img.height,
		});

		this.status.value = Statuses.loaded;

		resolve();
	}

	onError(reject: (message: string) => void) {
		this.status.value = Statuses.error;

		reject(this.errorMessage);
	}
}


===== FILE: src/resources/Img/__mocks__/Img.ts =====
import { vi } from 'vitest';
import { ResizeTypes, Statuses, Resource } from '../../';
import { FluxImage } from '../../../components';

export default class Img extends Resource {
	constructor() {
		super(
			'',
			'',
			ResizeTypes.fill,
			null,
			{ component: FluxImage, props: {} },
			{ component: FluxImage, props: {} },
			''
		);
	}

	load = vi.fn().mockImplementation(() => {
		return new Promise<void>((resolve) => {
			this.status.value = Statuses.loading;
			this.onLoad(null, resolve);
		});
	});

	onLoad = vi
		.fn()
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		.mockImplementation((_el: unknown, resolve: () => void) => {
			this.status.value = Statuses.loaded;
			resolve();
		});

	onError = vi
		.fn()
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		.mockImplementation((_reject: (message: string) => void) => {});
}


===== FILE: src/resources/ResizeTypes.ts =====
export enum ResizeTypes {
	fill = 'fill',
	fit = 'fit',
}

export default ResizeTypes;


===== FILE: src/resources/Resource.ts =====
import { computed, ref, type Ref } from 'vue';
import { Size, Position, ResizeCalculator } from '../shared';
import type { DisplayParameter, ResizedProps, ResizeType, TransitionParameter } from './types';
import { Statuses, ResizeTypes } from './';

export default abstract class Resource {
	src: string;
	loader: Promise<void> | null = null;
	errorMessage: string;
	status: Ref<Statuses> = ref(Statuses.notLoaded);

	realSize: Size = new Size();
	displaySize: Size = new Size();
	caption: string = '';
	resizeType: ResizeType;
	backgroundColor: null | string = null;
	display: DisplayParameter;
	transition: TransitionParameter;

	constructor(
		src: string,
		caption: string,
		resizeType: ResizeType = ResizeTypes.fill,
		backgroundColor: null | string = null,
		display: DisplayParameter,
		transition: TransitionParameter,
		errorMessage: string,
	) {
		this.src = src;
		this.caption = caption;
		this.resizeType = resizeType;
		this.backgroundColor = backgroundColor;
		this.display = display;
		this.transition = transition;
		this.errorMessage = errorMessage;
	}

	isLoading = () => this.status.value === Statuses.loading;

	isLoaded = () => this.status.value === Statuses.loaded;

	isError = () => this.status.value === Statuses.error;

	abstract load(): Promise<void>;

	abstract onLoad(el: unknown, resolve: () => void): void;

	abstract onError(reject: (message: string) => void): void;

	calcResizeProps(displaySize: Size) {
		if ([displaySize.isValid(), this.realSize.isValid()].includes(false)) {
			return {};
		}

		const resCalc = new ResizeCalculator(this.realSize);
		const { size, position } = resCalc.resizeTo(displaySize, this.resizeType);

		return {
			...size.toValue(),
			...position.toValue(),
		};
	}

	resizeProps = computed<{
		top?: number;
		left?: number;
		width?: number;
		height?: number;
	}>(() => this.calcResizeProps(this.displaySize));

	getResizeProps(size: Size, offset?: Position) {
		const resizedProps: ResizedProps = {
			width: 0,
			height: 0,
			top: 0,
			left: 0,
		};

		if (!this.displaySize.isValid()) {
			this.displaySize.update(size.toValue());
		}

		Object.assign(
			resizedProps,
			size.equals(this.displaySize) ? this.resizeProps.value : this.calcResizeProps(size),
		);

		if (offset !== undefined) {
			resizedProps.top -= offset.top.value || 0;
			resizedProps.left -= offset.left.value || 0;
		}

		return resizedProps;
	}
}


===== FILE: src/resources/Statuses.ts =====
export enum Statuses {
	notLoaded = 'notLoaded',
	loading = 'loading',
	loaded = 'loaded',
	error = 'error',
}

export default Statuses;


===== FILE: src/resources/__test__/ResourceFactory.ts =====
import { Img } from '../';

export default class ResourceFactory {
	static create(amount: number) {
		return new Array(amount).fill(new Img(''));
	}
}


===== FILE: src/resources/index.ts =====
export { default as Resource } from './Resource';
export { default as Img } from './Img/Img';
export { default as Statuses } from './Statuses';
export { default as ResizeTypes } from './ResizeTypes';

export type * from './types';


===== FILE: src/resources/types.ts =====
import type { Component } from 'vue';
import { Resource } from '.';
import ResizeTypes from './ResizeTypes';

export type ResizeType = keyof typeof ResizeTypes;

export interface ResizedProps {
	width: number;
	height: number;
	top: number;
	left: number;
}

export interface DisplayParameter {
	component: Component;
	props: object;
}

export interface TransitionParameter {
	component: Component;
	props: object;
}

export interface ResourceWithOptions {
	resource: Resource;
	options: {
		delay?: number;
		stop?: boolean;
	};
}


===== FILE: src/shared/Maths/Maths.test.ts =====
import * as Maths from './Maths';

describe('shared: Maths', () => {
	it('calculates the diagonal', () => {
		const size = {
			width: 640,
			height: 360,
		};

		expect(Maths.diag(size)).toBe(735);
	});

	it('calculates the aspect ratio', () => {
		const size = {
			width: 640,
			height: 320,
		};

		expect(Maths.aspectRatio(size)).toBe(2);
	});
});


===== FILE: src/shared/Maths/Maths.ts =====
export const diag = ({ width, height }: { width: number; height: number }) =>
	Math.ceil(Math.sqrt(width * width + height * height));

export const aspectRatio = ({
	width,
	height,
}: {
	width: number;
	height: number;
}) => width / height;


===== FILE: src/shared/Position/Position.test.ts =====
import Position from './Position';

describe('shared: Position', () => {
	let pos: Position;
	let coords: object;

	it('initializes values to null without parameters', () => {
		pos = new Position();

		expect(pos.top.value).toBeNull();
		expect(pos.left.value).toBeNull();
	});

	it('sets param values', () => {
		pos = new Position({ top: 100 });
		expect(pos.top.value).toBe(100);

		pos = new Position({ left: 100 });
		expect(pos.left.value).toBe(100);

		pos = new Position({
			top: 100,
			left: 200,
		});
		expect(pos.top.value).toBe(100);
		expect(pos.left.value).toBe(200);
	});

	it('reset values', () => {
		pos = new Position({
			top: 100,
			left: 200,
		});

		pos.reset();

		expect(pos.top.value).toBeNull();
		expect(pos.left.value).toBeNull();
	});

	it('is invalid if top or left is null', () => {
		pos = new Position({ top: 100 });
		expect(pos.isValid()).toBeFalsy();

		pos = new Position({ left: 100 });
		expect(pos.isValid()).toBeFalsy();
	});

	it('is valid when top and left have values', () => {
		pos = new Position({
			top: 100,
			left: 200,
		});

		expect(pos.isValid()).toBeTruthy();
	});

	it('updates the values', () => {
		pos = new Position({
			top: 100,
			left: 200,
		});

		pos.update({
			top: 50,
		});

		expect(pos.top.value).toBe(50);
		expect(pos.left.value).toBeNull();

		pos.update({
			left: 100,
		});

		expect(pos.top.value).toBeNull();
		expect(pos.left.value).toBe(100);

		pos.update({
			top: 200,
			left: 400,
		});

		expect(pos.top.value).toBe(200);
		expect(pos.left.value).toBe(400);
	});

	it('returns the values as plain object', () => {
		coords = {
			top: 100,
			left: 200,
		};

		pos = new Position(coords);
		expect(pos.toValue()).toStrictEqual(coords);

		pos = new Position();
		expect(pos.toValue()).toStrictEqual({
			top: undefined,
			left: undefined,
		});
	});

	it('throws exception when trying to get the values with px suffix', () => {
		pos = new Position();
		expect(() => pos.toPx()).toThrow('Invalid position in pixels');
	});

	it('returns the values with px suffix', () => {
		coords = {
			top: 100,
			left: 200,
		};

		pos = new Position(coords);

		expect(pos.toPx()).toStrictEqual({
			top: coords['top' as keyof object] + 'px',
			left: coords['left' as keyof object] + 'px',
		});
	});
});


===== FILE: src/shared/Position/Position.ts =====
import { ref, type Ref } from 'vue';

export default class Position {
	top: Ref<null | number> = ref(null);
	left: Ref<null | number> = ref(null);

	constructor(
		{ top = null, left = null }: { top?: null | number; left?: null | number } = {
			top: null,
			left: null,
		},
	) {
		this.update({ top, left });
	}

	reset() {
		this.top.value = null;
		this.left.value = null;
	}

	isValid() {
		return ![this.top.value, this.left.value].includes(null);
	}

	update({ top, left }: { top?: null | number; left?: null | number }) {
		this.top.value = top ?? null;
		this.left.value = left ?? null;
	}

	toValue() {
		const rawPosition: {
			top?: number;
			left?: number;
		} = {
			top: undefined,
			left: undefined,
		};

		if (this.top.value !== null) {
			rawPosition.top = this.top.value;
		}

		if (this.left.value !== null) {
			rawPosition.left = this.left.value;
		}

		return rawPosition;
	}

	toPx() {
		if (!this.isValid()) {
			throw new RangeError('Invalid position in pixels');
		}

		return {
			top: this.top.value!.toString() + 'px',
			left: this.left.value!.toString() + 'px',
		};
	}
}


===== FILE: src/shared/ResizeCalculator/ResizeCalculator.test.ts =====
import { Size } from '../';
import { ResizeTypes } from '../../resources';
import ResizeCalculator, { Orientations } from './ResizeCalculator';

describe('shared: ResizeCalculator', () => {
	let calc: ResizeCalculator;
	let realSize: Size;
	let newSize: Size;

	beforeEach(() => {
		realSize = new Size();
		newSize = new Size();
	});

	it('if the size is invalid throws error', () => {
		vi.spyOn(realSize, 'isValid').mockImplementation(() => false);

		expect(() => {
			calc = new ResizeCalculator(realSize);
		}).toThrow('Invalid real size');

		expect(realSize.isValid).toHaveBeenCalledWith();
	});

	it('if the size is valid when creating the calculator', () => {
		vi.spyOn(realSize, 'isValid').mockImplementation(() => true);

		expect(() => {
			calc = new ResizeCalculator(realSize);
		}).not.toThrow();

		expect(realSize.isValid).toHaveBeenCalledWith();
	});

	it('detects the orientation', () => {
		realSize.update({
			width: 640,
			height: 360,
		});

		calc = new ResizeCalculator(realSize);

		expect(calc.realOrientation).toBe(Orientations.landscape);

		realSize.update({
			width: 360,
			height: 640,
		});

		calc = new ResizeCalculator(realSize);

		expect(calc.realOrientation).toBe(Orientations.portrait);
	});

	it('if the new size is valid', () => {
		vi.spyOn(newSize, 'isValid').mockImplementation(() => false);

		realSize.update({
			width: 640,
			height: 360,
		});

		calc = new ResizeCalculator(realSize);

		expect(() => {
			calc.resizeTo(newSize, ResizeTypes.fill);
		}).toThrow('Invalid size to resize');

		expect(newSize.isValid).toHaveBeenCalledWith();
	});

	it('new size L real size L and newAspectRatio >= realAspectRatio and type fill', () => {
		newSize.update({
			width: 280,
			height: 140,
		});

		realSize.update({
			width: 640,
			height: 360,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fill
		);

		expect(adaptedSize.toValue()).toStrictEqual({
			width: 280,
			height: 157.5,
		});
		expect(adaptedPosition.toValue()).toStrictEqual({ top: -8.75, left: 0 });
	});

	it('new size L real size L and newAspectRatio < realAspectRatio and type fill', () => {
		newSize.update({
			width: 280,
			height: 180,
		});

		realSize.update({
			width: 280,
			height: 140,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fit
		);

		expect(adaptedSize.toValue()).toStrictEqual({
			width: 280,
			height: 140,
		});
		expect(adaptedPosition.toValue()).toStrictEqual({ top: 20, left: 0 });
	});

	it('new size L real size L and newAspectRatio >= realAspectRatio and type fit', () => {
		newSize.update({
			width: 280,
			height: 140,
		});

		realSize.update({
			width: 280,
			height: 200,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fit
		);

		expect(adaptedSize.toValue()).toStrictEqual({ width: 196, height: 140 });
		expect(adaptedPosition.toValue()).toStrictEqual({ top: 0, left: 42 });
	});

	it('new size L real size L and newAspectRatio < realAspectRatio and type fit', () => {
		newSize.update({
			width: 280,
			height: 180,
		});

		realSize.update({
			width: 280,
			height: 140,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fit
		);

		expect(adaptedSize.toValue()).toStrictEqual({
			width: 280,
			height: 140,
		});
		expect(adaptedPosition.toValue()).toStrictEqual({ top: 20, left: 0 });
	});

	it('new size L real size P and type fill', () => {
		newSize.update({
			width: 280,
			height: 140,
		});

		realSize.update({
			width: 140,
			height: 280,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fill
		);

		expect(adaptedSize.toValue()).toStrictEqual({ width: 280, height: 560 });
		expect(adaptedPosition.toValue()).toStrictEqual({ top: -210, left: 0 });
	});

	it('new size L real size P and type fit', () => {
		newSize.update({
			width: 280,
			height: 140,
		});

		realSize.update({
			width: 140,
			height: 280,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fit
		);

		expect(adaptedSize.toValue()).toStrictEqual({
			width: 70,
			height: 140,
		});
		expect(adaptedPosition.toValue()).toStrictEqual({ top: 0, left: 105 });
	});

	it('new size P real size L and type fill', () => {
		newSize.update({
			width: 140,
			height: 280,
		});

		realSize.update({
			width: 280,
			height: 140,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fill
		);

		expect(adaptedSize.toValue()).toStrictEqual({ width: 560, height: 280 });
		expect(adaptedPosition.toValue()).toStrictEqual({ top: 0, left: -210 });
	});

	it('new size P real size L and type fit', () => {
		newSize.update({
			width: 140,
			height: 280,
		});

		realSize.update({
			width: 280,
			height: 140,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fit
		);

		expect(adaptedSize.toValue()).toStrictEqual({
			width: 140,
			height: 70,
		});
		expect(adaptedPosition.toValue()).toStrictEqual({
			top: 105,
			left: 0,
		});
	});

	it('new size P real size P and newAspectRatio >= realAspectRatio and type fill', () => {
		newSize.update({
			width: 140,
			height: 280,
		});

		realSize.update({
			width: 180,
			height: 280,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fill
		);

		expect(adaptedSize.toValue()).toStrictEqual({
			width: 180,
			height: 280,
		});
		expect(adaptedPosition.toValue()).toStrictEqual({ top: 0, left: -20 });
	});

	it('new size P real size P and newAspectRatio < realAspectRatio and type fill', () => {
		newSize.update({
			width: 180,
			height: 280,
		});

		realSize.update({
			width: 140,
			height: 280,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fill
		);

		expect(adaptedSize.toValue()).toStrictEqual({
			width: 180,
			height: 360,
		});
		expect(adaptedPosition.toValue()).toStrictEqual({ top: -40, left: 0 });
	});

	it('new size P real size P and newAspectRatio >= realAspectRatio and type fit', () => {
		newSize.update({
			width: 140,
			height: 280,
		});

		realSize.update({
			width: 200,
			height: 280,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fit
		);

		expect(adaptedSize.toValue()).toStrictEqual({ width: 140, height: 196 });
		expect(adaptedPosition.toValue()).toStrictEqual({ top: 42, left: 0 });
	});

	it('new size P real size P and newAspectRatio < realAspectRatio and type fit', () => {
		newSize.update({
			width: 180,
			height: 280,
		});

		realSize.update({
			width: 140,
			height: 280,
		});

		calc = new ResizeCalculator(realSize);

		const { size: adaptedSize, position: adaptedPosition } = calc.resizeTo(
			newSize,
			ResizeTypes.fit
		);

		expect(adaptedSize.toValue()).toStrictEqual({ width: 140, height: 280 });
		expect(adaptedPosition.toValue()).toStrictEqual({ top: 0, left: 20 });
	});
});


===== FILE: src/shared/ResizeCalculator/ResizeCalculator.ts =====
import { type ResizeType, ResizeTypes } from '../../resources';
import { Size, Position } from '../';

export enum Orientations {
	landscape = 'landscape',
	portrait = 'portrait',
}

const getOrientation = (aspectRatio: number) =>
	aspectRatio >= 1 ? Orientations.landscape : Orientations.portrait;

type Orientation = keyof typeof Orientations;

export default class ResizeCalculator {
	realSize: Size;
	realAspectRatio: number;
	realOrientation: Orientation;

	constructor(realSize: Size) {
		if (realSize.isValid() === false) {
			throw new RangeError('Invalid real size');
		}

		this.realSize = realSize;
		this.realAspectRatio = this.realSize.getAspectRatio();
		this.realOrientation = getOrientation(this.realAspectRatio);
	}

	public resizeTo(resizeSize: Size, resizeType: ResizeType) {
		if (resizeSize.isValid() === false) {
			throw new RangeError('Invalid size to resize');
		}

		const resizeAspectRatio = resizeSize.getAspectRatio();
		const resizeOrientation = getOrientation(resizeAspectRatio);

		const adaptedSize: Size = this.getAdaptedSize(
			resizeSize,
			resizeAspectRatio,
			resizeOrientation,
			resizeType,
		);

		const adaptedPosition: Position = this.getAdaptedPosition(
			resizeSize,
			resizeAspectRatio,
			adaptedSize,
			resizeType,
		);

		return {
			size: adaptedSize,
			position: adaptedPosition,
		};
	}

	private getAdaptedSize(
		resizeSize: Size,
		resizeAspectRatio: number,
		resizeOrientation: Orientation,
		resizeType: ResizeType,
	) {
		if (
			resizeOrientation === Orientations.landscape &&
			this.realOrientation === Orientations.portrait &&
			resizeType === ResizeTypes.fill
		) {
			return this.getAdaptedSizeByWith(resizeSize);
		}

		if (
			resizeOrientation === Orientations.landscape &&
			this.realOrientation === Orientations.landscape &&
			resizeAspectRatio >= this.realAspectRatio &&
			resizeType === ResizeTypes.fill
		) {
			return this.getAdaptedSizeByWith(resizeSize);
		}

		if (
			resizeOrientation === Orientations.landscape &&
			this.realOrientation === Orientations.landscape &&
			resizeAspectRatio < this.realAspectRatio &&
			resizeType === ResizeTypes.fit
		) {
			return this.getAdaptedSizeByWith(resizeSize);
		}

		if (
			resizeOrientation === Orientations.portrait &&
			this.realOrientation === Orientations.landscape &&
			resizeType === ResizeTypes.fit
		) {
			return this.getAdaptedSizeByWith(resizeSize);
		}

		if (
			resizeOrientation === Orientations.portrait &&
			this.realOrientation === Orientations.portrait &&
			resizeAspectRatio > this.realAspectRatio &&
			resizeType === ResizeTypes.fill
		) {
			return this.getAdaptedSizeByWith(resizeSize);
		}

		if (
			resizeOrientation === Orientations.portrait &&
			this.realOrientation === Orientations.portrait &&
			resizeAspectRatio <= this.realAspectRatio &&
			resizeType === ResizeTypes.fit
		) {
			return this.getAdaptedSizeByWith(resizeSize);
		}

		return this.getAdaptedSizeByHeight(resizeSize);
	}

	private getAdaptedSizeByWith(resizeSize: Size) {
		return new Size({
			width: resizeSize.width.value,
			height: resizeSize.width.value! / this.realAspectRatio,
		});
	}

	private getAdaptedSizeByHeight(resizeSize: Size) {
		return new Size({
			width: this.realAspectRatio * resizeSize.height.value!,
			height: resizeSize.height.value,
		});
	}

	private getAdaptedPosition(
		resizeSize: Size,
		resizeAspectRatio: number,
		adaptedSize: Size,
		resizeType: ResizeType,
	) {
		if (this.realAspectRatio <= resizeAspectRatio && resizeType === ResizeTypes.fill) {
			return this.getAdaptedPositionVertically(resizeSize, adaptedSize);
		}

		if (this.realAspectRatio > resizeAspectRatio && resizeType === ResizeTypes.fit) {
			return this.getAdaptedPositionVertically(resizeSize, adaptedSize);
		}

		return this.getAdaptedPositionHorizontally(resizeSize, adaptedSize);
	}

	getAdaptedPositionVertically(resizeSize: Size, adaptedSize: Size) {
		return new Position({
			top: (resizeSize.height.value! - adaptedSize.height.value!) / 2,
			left: 0,
		});
	}

	getAdaptedPositionHorizontally(resizeSize: Size, adaptedSize: Size) {
		return new Position({
			top: 0,
			left: (resizeSize.width.value! - adaptedSize.width.value!) / 2,
		});
	}
}


===== FILE: src/shared/ResourceLoader/ResourceLoader.test.ts =====
import { vi } from 'vitest';
import ResourceLoader from './ResourceLoader';
import ResourceLoaderFactory from './__test__/ResourceLoaderFactory';
import { Statuses } from '../../resources';

vi.mock('../../resources/Img/Img');

describe('shared: ResourceLoader', () => {
	let rscLoader: ResourceLoader;

	beforeEach(() => {
		vi.clearAllMocks();
	});

	it('calls onPreloadStart when preload starts', () => {
		rscLoader = ResourceLoaderFactory.create(10, 5);

		expect(rscLoader.onPreloadStart).toHaveBeenCalledOnce();
	});

	it('preloads all resources if num resources less than num to preload', () => {
		rscLoader = ResourceLoaderFactory.create(10, 15);

		expect(rscLoader.toPreload).toBe(10);
	});

	it('start preloading when created', () => {
		rscLoader = ResourceLoaderFactory.create(10, 10);

		expect(rscLoader.onPreloadStart).toHaveBeenCalledOnce();
	});

	it('start preloading the resources', () => {
		rscLoader = ResourceLoaderFactory.create(15, 10);

		expect(
			rscLoader.rscs.every((rsc) =>
				[Statuses.loading, Statuses.loaded].includes(rsc.resource.status.value),
			),
		).toBeTruthy();
	});

	it('checks if resources preloaded are less than to preload and preloads the remaining', () => {
		rscLoader = ResourceLoaderFactory.create(15, 6);

		rscLoader.counter.success = 4;
		rscLoader.counter.error = 2;
		rscLoader.counter.total = 6;

		rscLoader.preloadEnd();

		expect(rscLoader.preLoading).toHaveLength(8);
	});

	it('calls onPreloadEnd when all preloaded', () =>
		new Promise<void>((done) => {
			rscLoader = ResourceLoaderFactory.create(5, 5, undefined, () => {
				expect(rscLoader.onPreloadStart).toHaveBeenCalledOnce();
				expect(rscLoader.onPreloadEnd).toHaveBeenCalledWith(expect.any(Array));
				done();
			});
		}));

	it('starts lazy loading when preloading finish', () =>
		new Promise<void>((done) => {
			rscLoader = ResourceLoaderFactory.create(20, 5, undefined, undefined, () => {
				expect(rscLoader.onPreloadStart).toHaveBeenCalledOnce();
				expect(rscLoader.onPreloadEnd).toHaveBeenCalledOnce();
				expect(rscLoader.counter.total).toBe(5);
				expect(rscLoader.onLazyLoadStart).toHaveBeenCalledOnce();
				done();
			});
		}));

	it('calls onLazyLoadEnd when lazy loading finish', () =>
		new Promise<void>((done) => {
			rscLoader = ResourceLoaderFactory.create(20, 5, undefined, undefined, undefined, () => {
				expect(rscLoader.onPreloadStart).toHaveBeenCalledOnce();
				expect(rscLoader.onPreloadEnd).toHaveBeenCalledOnce();
				expect(rscLoader.onLazyLoadStart).toHaveBeenCalledOnce();
				expect(rscLoader.onLazyLoadEnd).toHaveBeenCalledWith(expect.any(Array));
				expect(rscLoader.counter.total).toBe(20);
				done();
			});
		}));

	it('does not update display size if cancelled', () => {
		rscLoader = ResourceLoaderFactory.create(10, 5);
		rscLoader.cancel();

		const rsc = rscLoader.rscs[0];
		vi.spyOn(rsc!.resource.displaySize, 'update');

		rscLoader.loadSuccess(rsc!);

		expect(rsc!.resource.displaySize.update).not.toHaveBeenCalled();
	});

	it('calculates the progress properly', () => {
		rscLoader = ResourceLoaderFactory.create(15, 6);

		rscLoader.counter.success = 4;
		rscLoader.counter.error = 2;
		rscLoader.counter.total = 6;

		rscLoader.updateProgress();

		expect(rscLoader.progress.value).toBe(67);
	});
});


===== FILE: src/shared/ResourceLoader/ResourceLoader.ts =====
import { type Ref, ref } from 'vue';
import { Size } from '../';
import type { ResourceWithOptions } from '../../resources';

export default class ResourceLoader {
	rscs: ResourceWithOptions[] = [];
	counter = {
		success: 0,
		error: 0,
		total: 0,
	};
	toPreload: number;
	preLoading: ResourceWithOptions[] = [];
	lazyLoading: ResourceWithOptions[] = [];
	progress: Ref<number> = ref(0);
	displaySize: Size;
	onPreloadStart: () => void;
	onPreloadEnd: (loaded: ResourceWithOptions[]) => void;
	onLazyLoadStart: () => void;
	onLazyLoadEnd: (loaded: ResourceWithOptions[]) => void;
	isCancelled: boolean = false;
	reject: (message: string, rscs: ResourceWithOptions[]) => void;

	constructor(
		rscs: ResourceWithOptions[],
		toPreload: number,
		displaySize: Size,
		onPreloadStart: () => void,
		onPreloadEnd: (loaded: ResourceWithOptions[]) => void,
		onLazyLoadStart: () => void,
		onLazyLoadEnd: (loaded: ResourceWithOptions[]) => void,
		reject: (message: string, rscs: ResourceWithOptions[]) => void,
	) {
		this.rscs = rscs;
		this.toPreload = toPreload > rscs.length ? rscs.length : toPreload;
		this.displaySize = displaySize;
		this.onPreloadStart = onPreloadStart;
		this.onPreloadEnd = onPreloadEnd;
		this.onLazyLoadStart = onLazyLoadStart;
		this.onLazyLoadEnd = onLazyLoadEnd;
		this.reject = reject;

		this.preloadStart();
	}

	preloadStart() {
		this.onPreloadStart();

		const { counter } = this;

		const toLoad = this.rscs.slice(
			counter.total,
			counter.total + this.toPreload - counter.success,
		);

		this.preLoading = this.preLoading.concat(toLoad);

		toLoad.forEach((rsc) => this.load(rsc));
	}

	preloadEnd() {
		const { counter, toPreload } = this;

		if (counter.success < toPreload && counter.total < this.rscs.length) {
			this.preloadStart();
			return;
		}

		const preloadedSuccessfully = this.preLoading.filter((rsc) => rsc.resource.isLoaded());

		this.onPreloadEnd(preloadedSuccessfully);

		this.preLoading.length = 0;

		if (counter.total < this.rscs.length) {
			this.lazyLoadStart();
		}
	}

	lazyLoadStart() {
		this.onLazyLoadStart();

		this.lazyLoading = this.rscs.slice(this.counter.total);

		this.lazyLoading.forEach((rsc) => this.load(rsc));
	}

	lazyLoadEnd() {
		const lazyLoadedSuccessfully = this.lazyLoading.filter((rsc) => rsc.resource.isLoaded());

		this.onLazyLoadEnd(lazyLoadedSuccessfully);

		this.lazyLoading.length = 0;
	}

	load(rsc: ResourceWithOptions) {
		rsc.resource
			.load()
			.then(() => {
				this.loadSuccess(rsc);
			})
			.catch((error) => {
				this.loadError(error);
			})
			.finally(() => {
				this.counter.total++;

				if (this.isCancelled) {
					return;
				}

				if (this.preLoading.length !== 0) {
					this.updateProgress();
				}

				if (this.counter.total === this.toPreload) {
					this.preloadEnd();
				} else if (this.counter.total === this.rscs.length) {
					this.lazyLoadEnd();
				}
			});
	}

	loadSuccess(rsc: ResourceWithOptions) {
		this.counter.success++;

		if (this.isCancelled) {
			return;
		}

		rsc.resource.displaySize.update(this.displaySize.toValue());
	}

	loadError(error: string) {
		this.counter.error++;

		if (this.isCancelled) {
			return;
		}

		console.error(error);
	}

	updateProgress() {
		this.progress.value = Math.ceil((this.counter.success * 100) / this.toPreload);
	}

	hasFinished() {
		return this.counter.total === this.rscs.length;
	}

	cancel() {
		this.isCancelled = true;
		this.reject('Resources loading cancelled', this.rscs);
	}
}


===== FILE: src/shared/ResourceLoader/__mocks__/ResourceLoader.ts =====
import { Size } from '../../';
import type { ResourceWithOptions } from '../../../resources';

export default class ResourceLoader {
	rscs: ResourceWithOptions[] = [];
	counter = {
		success: 0,
		error: 0,
		total: 0,
	};
	toPreload: number;
	preLoading: ResourceWithOptions[] = [];
	lazyLoading: ResourceWithOptions[] = [];
	displaySize: Size;
	onPreloadStart: () => void;
	onPreloadEnd: (loaded: ResourceWithOptions[]) => void;
	onLazyLoadStart: () => void;
	onLazyLoadEnd: (loaded: ResourceWithOptions[]) => void;
	reject: (message: string, rscs: ResourceWithOptions[]) => void;

	constructor(
		rscs: ResourceWithOptions[],
		toPreload: number,
		displaySize: Size,
		onPreloadStart: () => void,
		onPreloadEnd: (loaded: ResourceWithOptions[]) => void,
		onLazyLoadStart: () => void,
		onLazyLoadEnd: (loaded: ResourceWithOptions[]) => void,
		reject: (message: string, rscs: ResourceWithOptions[]) => void,
	) {
		this.rscs = rscs;
		this.toPreload = toPreload > rscs.length ? rscs.length : toPreload;
		this.displaySize = displaySize;
		this.onPreloadStart = onPreloadStart;
		this.onPreloadEnd = onPreloadEnd;
		this.onLazyLoadStart = onLazyLoadStart;
		this.onLazyLoadEnd = onLazyLoadEnd;
		this.reject = reject;

		this.preloadStart();
	}

	preloadStart() {
		this.onPreloadStart();

		const { counter } = this;

		const toLoad = this.rscs.slice(
			counter.total,
			counter.total + this.toPreload - counter.success,
		);

		this.preLoading = this.preLoading.concat(toLoad);

		toLoad.forEach((rsc) => this.load(rsc));
	}

	preloadEnd() {
		const { counter, toPreload } = this;

		if (counter.success < toPreload && counter.total < toPreload) {
			this.preloadStart();
			return;
		}

		const preloadedSuccessfully = this.preLoading.filter((rsc) => rsc.resource.isLoaded());

		this.onPreloadEnd(preloadedSuccessfully);

		this.preLoading.length = 0;

		if (counter.total < this.rscs.length) {
			this.lazyLoadStart();
		}
	}

	lazyLoadStart() {
		this.onLazyLoadStart();

		this.lazyLoading = this.rscs.slice(this.counter.total);

		this.lazyLoading.forEach((rsc) => this.load(rsc));
	}

	lazyLoadEnd() {
		const lazyLoadedSuccessfully = this.lazyLoading.filter((rsc) => rsc.resource.isLoaded());

		this.onLazyLoadEnd(lazyLoadedSuccessfully);

		this.lazyLoading.length = 0;
	}

	load(rsc: ResourceWithOptions) {
		rsc.resource
			.load()
			.then(() => {
				this.loadSuccess();
			})
			.finally(() => {
				this.counter.total++;

				if (this.counter.total === this.toPreload) {
					this.preloadEnd();
				} else if (this.counter.total === this.rscs.length) {
					this.lazyLoadEnd();
				}
			});
	}

	loadSuccess() {
		this.counter.success++;
	}

	hasFinished() {
		return this.counter.total === this.rscs.length;
	}
}


===== FILE: src/shared/ResourceLoader/__test__/ResourceLoaderFactory.ts =====
import { vi } from 'vitest';
import ResourceFactory from '../../../resources/__test__/ResourceFactory';
import ResourceLoader from '../ResourceLoader';
import Size from '../../Size/Size';
import type { ResourceWithOptions } from '../../../resources/types';

export default class ResourceLoaderFactory {
	static create(
		numResources: number,
		numToPreload: number,
		preloadStartMock?: () => void,
		preloadEndMock?: () => void,
		lazyLoadStartMock?: () => void,
		lazyLoadEndMock?: () => void,
	) {
		const displaySize = new Size({
			width: 640,
			height: 360,
		});

		const onPreloadStart = vi.fn();

		if (preloadStartMock) {
			onPreloadStart.mockImplementation(preloadStartMock);
		}

		const onPreloadEnd = vi.fn();

		if (preloadEndMock) {
			onPreloadEnd.mockImplementation(preloadEndMock);
		}

		const onLazyLoadStart = vi.fn();

		if (lazyLoadStartMock) {
			onLazyLoadStart.mockImplementation(lazyLoadStartMock);
		}

		const onLazyLoadEnd = vi.fn();

		if (lazyLoadEndMock) {
			onLazyLoadEnd.mockImplementation(lazyLoadEndMock);
		}

		const reject = vi.fn();

		const resources = ResourceFactory.create(numResources).map((resource) => {
			return {
				resource: resource,
				options: {},
			} as ResourceWithOptions;
		});

		return new ResourceLoader(
			resources,
			numToPreload,
			displaySize,
			onPreloadStart,
			onPreloadEnd,
			onLazyLoadStart,
			onLazyLoadEnd,
			reject,
		);
	}
}


===== FILE: src/shared/Size/Size.test.ts =====
import Size from './Size';

describe('shared: Size', () => {
	let size: Size;
	let params: object;

	it('initializes values to null without parameters', () => {
		size = new Size();

		expect(size.width.value).toBeNull();
		expect(size.height.value).toBeNull();
	});

	it('sets param values', () => {
		size = new Size({ width: 100 });
		expect(size.width.value).toBe(100);

		size = new Size({ height: 100 });
		expect(size.height.value).toBe(100);

		size = new Size({
			width: 100,
			height: 200,
		});
		expect(size.width.value).toBe(100);
		expect(size.height.value).toBe(200);
	});

	it('reset values', () => {
		size = new Size({
			width: 100,
			height: 200,
		});

		size.reset();

		expect(size.width.value).toBeNull();
		expect(size.height.value).toBeNull();
	});

	it('is invalid if width or height is null', () => {
		size = new Size({ width: 100 });
		expect(size.isValid()).toBeFalsy();

		size = new Size({ height: 100 });
		expect(size.isValid()).toBeFalsy();
	});

	it('is valid when width and height have values', () => {
		size = new Size({
			width: 100,
			height: 200,
		});

		expect(size.isValid()).toBeTruthy();
	});

	it('updates the values', () => {
		size = new Size({
			width: 100,
			height: 200,
		});

		size.update({
			width: 50,
		});

		expect(size.width.value).toBe(50);
		expect(size.height.value).toBeNull();

		size.update({
			height: 100,
		});

		expect(size.width.value).toBeNull();
		expect(size.height.value).toBe(100);

		size.update({
			width: 200,
			height: 400,
		});

		expect(size.width.value).toBe(200);
		expect(size.height.value).toBe(400);
	});

	it('throws an exception trying to calc aspect ratio when size is invalid', () => {
		size = new Size({ width: 100 });

		expect(() => size.getAspectRatio()).toThrow(
			'Could not get aspect ratio due to invalid size'
		);
	});

	it('gets the aspect ration when size is valid', () => {
		size = new Size({
			width: 100,
			height: 200,
		});

		expect(size.getAspectRatio()).toBeTypeOf('number');
	});

	it('clones the size', () => {
		params = {
			width: 100,
			height: 200,
		};

		size = new Size({
			width: 100,
			height: 200,
		});

		expect(size.clone().toValue()).toStrictEqual(params);
	});

	it('returns false when width does not match other size', () => {
		size = new Size({
			width: 100,
			height: 200,
		});

		expect(
			size.equals(
				new Size({
					width: 50,
					height: 200,
				})
			)
		).toBeFalsy();
	});

	it('returns false when height does not match other size', () => {
		size = new Size({
			width: 100,
			height: 200,
		});

		expect(
			size.equals(
				new Size({
					width: 100,
					height: 50,
				})
			)
		).toBeFalsy();
	});

	it('returns true when size equals another size', () => {
		params = {
			width: 100,
			height: 200,
		};

		size = new Size(params);

		expect(size.equals(new Size(params))).toBeTruthy();
	});

	it('returns the values as plain object', () => {
		params = {
			width: 100,
			height: 200,
		};

		size = new Size(params);
		expect(size.toValue()).toStrictEqual(params);

		size = new Size();
		expect(size.toValue()).toStrictEqual({});
	});

	it('throws exception when trying to get the values with px suffix', () => {
		size = new Size();
		expect(() => size.toPx()).toThrow('Invalid size in pixels');
	});

	it('returns the values with px suffix', () => {
		params = {
			width: 100,
			height: 200,
		};

		size = new Size(params);

		expect(size.toPx()).toStrictEqual({
			width: params['width' as keyof object] + 'px',
			height: params['height' as keyof object] + 'px',
		});
	});
});


===== FILE: src/shared/Size/Size.ts =====
import { ref, type Ref } from 'vue';
import { Maths } from '../';

export default class Size {
	width: Ref<null | number> = ref(null);
	height: Ref<null | number> = ref(null);

	constructor(
		{
			width = null,
			height = null,
		}: {
			width?: null | number;
			height?: null | number;
		} = { width: null, height: null },
	) {
		this.update({ width, height });
	}

	reset() {
		this.width.value = null;
		this.height.value = null;
	}

	isValid() {
		return ![this.width.value, this.height.value].includes(null);
	}

	update({ width, height }: { width?: null | number; height?: null | number }) {
		this.width.value = width ?? null;
		this.height.value = height ?? null;
	}

	getAspectRatio() {
		if (!this.isValid()) {
			throw new RangeError('Could not get aspect ratio due to invalid size');
		}

		return Maths.aspectRatio(this.toValue() as { width: number; height: number });
	}

	clone() {
		return new Size(this.toValue());
	}

	equals(otherSize: Size) {
		if (this.width.value !== otherSize.width.value) {
			return false;
		}

		if (this.height.value !== otherSize.height.value) {
			return false;
		}

		return true;
	}

	toValue() {
		const rawSize: {
			width?: number;
			height?: number;
		} = {};

		if (this.width.value !== null) {
			rawSize.width = this.width.value;
		}

		if (this.height.value !== null) {
			rawSize.height = this.height.value;
		}

		return rawSize;
	}

	toPx() {
		if (!this.isValid()) {
			throw new RangeError('Invalid size in pixels');
		}

		return {
			width: this.width.value!.toString() + 'px',
			height: this.height.value!.toString() + 'px',
		};
	}
}


===== FILE: src/shared/index.ts =====
export * as Maths from './Maths/Maths';
export { default as Position } from './Position/Position';
export { default as ResizeCalculator } from './ResizeCalculator/ResizeCalculator';
export { default as ResourceLoader } from './ResourceLoader/ResourceLoader';
export { default as Size } from './Size/Size';


===== FILE: src/transitions/Blinds2D/Blinds2D.test.ts =====
import Blinds2D from './Blinds2D.vue';
import AnimationWrapper from '../__test__/AnimationWrapper';
import { Directions } from '../../controllers/Player';

vi.mock('../../components/FluxGrid/FluxGrid.vue');

describe('transition: Blinds2D', () => {
	it('exposes onPlay and totalDuration', () => {
		const wrapper = AnimationWrapper(Blinds2D, {});

		const { onPlay, totalDuration } = wrapper.vm;

		expect(typeof onPlay).toBe('function');
		expect(typeof totalDuration).toBe('number');
	});

	it('performs the transition with default options', () => {
		const wrapper = AnimationWrapper(Blinds2D, {});

		const $grid = wrapper.getComponent({
			ref: '$grid',
		});

		wrapper.vm.onPlay();

		expect($grid.vm.transform).toHaveBeenCalledOnce();

		const { $tiles } = $grid.vm;

		expect($tiles[0].transform).toHaveBeenCalledWith({
			opacity: '0.1',
			transform: 'scaleX(0)',
			transition: 'all 800ms linear 0ms',
		});

		expect($tiles[9].transform).toHaveBeenCalledWith({
			opacity: '0.1',
			transform: 'scaleX(0)',
			transition: 'all 800ms linear 900ms',
		});

		expect(wrapper.vm.totalDuration).toBe(1800);
	});

	it('performs the transition with custom options prev', () => {
		const wrapper = AnimationWrapper(Blinds2D, {
			direction: Directions.prev,
			cols: 6,
			tileDuration: 400,
			tileDelay: 60,
			easing: 'ease-out',
		});

		const $grid = wrapper.getComponent({
			ref: '$grid',
		});

		wrapper.vm.onPlay();

		expect($grid.vm.transform).toHaveBeenCalledOnce();

		const { $tiles } = $grid.vm;

		expect($tiles[0].transform).toHaveBeenCalledWith({
			opacity: '0.1',
			transform: 'scaleX(0)',
			transition: 'all 400ms ease-out 300ms',
		});

		expect($tiles[5].transform).toHaveBeenCalledWith({
			opacity: '0.1',
			transform: 'scaleX(0)',
			transition: 'all 400ms ease-out 0ms',
		});

		expect(wrapper.vm.totalDuration).toBe(760);
	});

	it('performs the transition with custom options next', () => {
		const wrapper = AnimationWrapper(Blinds2D, {
			direction: Directions.next,
			cols: 6,
			tileDuration: 700,
			tileDelay: 90,
			easing: 'ease-in',
		});

		const $grid = wrapper.getComponent({
			ref: '$grid',
		});

		wrapper.vm.onPlay();

		expect($grid.vm.transform).toHaveBeenCalledOnce();

		const { $tiles } = $grid.vm;

		expect($tiles[0].transform).toHaveBeenCalledWith({
			opacity: '0.1',
			transform: 'scaleX(0)',
			transition: 'all 700ms ease-in 0ms',
		});

		expect($tiles[5].transform).toHaveBeenCalledWith({
			opacity: '0.1',
			transform: 'scaleX(0)',
			transition: 'all 700ms ease-in 450ms',
		});

		expect(wrapper.vm.totalDuration).toBe(1240);
	});
});


===== FILE: src/transitions/Blinds2D/Blinds2D.vue =====
<script setup lang="ts">
	import { ref, reactive, type Ref } from 'vue';
	import { type FluxComponent, FluxGrid } from '../../components';
	import useTransition from '../useTransition';
	import type { TransitionBlinds2DProps, TransitionBlinds2DConf } from './types';
	import { Sides } from '../../components/FluxCube';

	const props = defineProps<TransitionBlinds2DProps>();

	const $grid: Ref<null | InstanceType<typeof FluxGrid>> = ref(null);

	const conf: TransitionBlinds2DConf = reactive({
		rows: 1,
		cols: 10,
		tileDuration: 800,
		tileDelay: 100,
		easing: 'linear',
	});

	useTransition(conf, props.options);

	const rscs = {
		[Sides.front]: props.from,
	};

	const totalDuration = conf.tileDelay * conf.cols + conf.tileDuration;

	const getDelay = {
		prev: (index: number) => (conf.cols - index - 1) * conf.tileDelay,
		next: (index: number) => index * conf.tileDelay,
	};

	const onPlay = () => {
		$grid.value!.transform((tile: FluxComponent, index: number) => {
			const transition = `all ${conf.tileDuration}ms ${
				conf.easing
			} ${getDelay[conf.direction!](index)}ms`;

			tile.transform({
				transition,
				opacity: '0.1',
				transform: 'scaleX(0)',
			});
		});
	};

	defineExpose({
		onPlay,
		totalDuration,
	});
</script>

<template>
	<FluxGrid ref="$grid" :rows="conf.rows" :cols="conf.cols" :size="size" :rscs="rscs" />
</template>


===== FILE: src/transitions/Blinds2D/types.ts =====
import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';

export interface TransitionBlinds2DOptions extends TransitionOptions {
	cols?: number;
	tileDuration?: number;
	tileDelay?: number;
}

export interface TransitionBlinds2DProps extends TransitionProps {
	options?: TransitionBlinds2DOptions;
}

export interface TransitionBlinds2DConf extends TransitionConf {
	rows: number;
	cols: number;
	tileDuration: number;
	tileDelay: number;
}


===== FILE: src/transitions/Blinds3D/Blinds3D.test.ts =====
import Blinds3D from './Blinds3D.vue';
import AnimationWrapper from '../__test__/AnimationWrapper';
import { Directions } from '../../controllers';
import { Turns } from '../../components/FluxCube';

vi.mock('../../components/FluxGrid/FluxGrid.vue');

describe('transition: Blinds3D', () => {
	it('exposes onPlay and totalDuration', () => {
		const wrapper = AnimationWrapper(Blinds3D, {});

		const { onPlay, totalDuration } = wrapper.vm;

		expect(typeof onPlay).toBe('function');
		expect(typeof totalDuration).toBe('number');
	});

	it('expects to set proper CSS styles before animation', () => {
		const wrapper = AnimationWrapper(Blinds3D, {});

		const maskStyle = wrapper.props('maskStyle');
		expect(maskStyle.overflow).toBe('visible');

		const gridCss = wrapper
			.getComponent({
				ref: '$grid',
			})
			.props('css');
		expect(gridCss.perspective).toBeDefined();
	});

	it('performs the transition with default options', () => {
		const wrapper = AnimationWrapper(Blinds3D, {});

		const $grid = wrapper.getComponent({
			ref: '$grid',
		});

		wrapper.vm.onPlay();

		expect($grid.vm.transform).toHaveBeenCalledOnce();

		const { $tiles } = $grid.vm;

		expect($tiles[0].setCss).toHaveBeenCalledWith({
			transition: 'all 800ms ease-out 0ms',
		});
		expect($tiles[0].turn).toHaveBeenCalledWith(Turns.backr);

		expect($tiles[5].setCss).toHaveBeenCalledWith({
			transition: 'all 800ms ease-out 750ms',
		});
		expect($tiles[5].turn).toHaveBeenCalledWith(Turns.backr);

		expect(wrapper.vm.totalDuration).toBe(1700);
	});

	it('performs the transition with custom options prev', () => {
		const wrapper = AnimationWrapper(Blinds3D, {
			direction: Directions.prev,
			cols: 8,
			tileDuration: 400,
			tileDelay: 60,
			easing: 'ease-in',
		});

		const $grid = wrapper.getComponent({
			ref: '$grid',
		});

		wrapper.vm.onPlay();

		expect($grid.vm.transform).toHaveBeenCalledOnce();

		const { $tiles } = $grid.vm;

		expect($tiles[0].setCss).toHaveBeenCalledWith({
			transition: 'all 400ms ease-in 420ms',
		});
		expect($tiles[0].turn).toHaveBeenCalledWith(Turns.backl);

		expect($tiles[7].setCss).toHaveBeenCalledWith({
			transition: 'all 400ms ease-in 0ms',
		});
		expect($tiles[7].turn).toHaveBeenCalledWith(Turns.backl);

		expect(wrapper.vm.totalDuration).toBe(880);
	});

	it('performs the transition with custom options next', () => {
		const wrapper = AnimationWrapper(Blinds3D, {
			direction: Directions.next,
			cols: 10,
			tileDuration: 400,
			tileDelay: 60,
			easing: 'linear',
		});

		const $grid = wrapper.getComponent({
			ref: '$grid',
		});

		wrapper.vm.onPlay();

		expect($grid.vm.transform).toHaveBeenCalledOnce();

		const { $tiles } = $grid.vm;

		expect($tiles[0].setCss).toHaveBeenCalledWith({
			transition: 'all 400ms linear 0ms',
		});
		expect($tiles[0].turn).toHaveBeenCalledWith(Turns.backr);

		expect($tiles[9].setCss).toHaveBeenCalledWith({
			transition: 'all 400ms linear 540ms',
		});
		expect($tiles[9].turn).toHaveBeenCalledWith(Turns.backr);

		expect(wrapper.vm.totalDuration).toBe(1000);
	});
});


===== FILE: src/transitions/Blinds3D/Blinds3D.vue =====
<script setup lang="ts">
	import { ref, reactive, type Ref, type CSSProperties } from 'vue';
	import useTransition from '../useTransition';
	import { FluxGrid, Turns, FluxCube } from '../../components';
	import type { TransitionBlinds3DProps, TransitionBlinds3DConf } from './types';

	const props = defineProps<TransitionBlinds3DProps>();

	const $grid: Ref<null | InstanceType<typeof FluxGrid>> = ref(null);

	const conf: TransitionBlinds3DConf = reactive({
		rows: 1,
		cols: 6,
		tileDuration: 800,
		tileDelay: 150,
		easing: 'ease-out',
	});

	useTransition(conf, props
Download .txt
gitextract_8a1o24_7/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .markdownlint.cjs
├── .prettierrc.json
├── .vscode/
│   └── extensions.json
├── LICENSE
├── README.md
├── env.d.ts
├── eslint.config.ts
├── ia.txt
├── index.html
├── package.json
├── src/
│   ├── App.vue
│   ├── assets/
│   │   └── css/
│   │       ├── base.scss
│   │       └── main.css
│   ├── complements/
│   │   ├── FluxCaption/
│   │   │   ├── FluxCaption.test.ts
│   │   │   └── FluxCaption.vue
│   │   ├── FluxControls/
│   │   │   ├── FluxControls.test.ts
│   │   │   ├── FluxControls.vue
│   │   │   └── buttons/
│   │   │       ├── Next.vue
│   │   │       ├── Play.vue
│   │   │       ├── Prev.vue
│   │   │       ├── Stop.vue
│   │   │       └── index.ts
│   │   ├── FluxIndex/
│   │   │   ├── Button/
│   │   │   │   ├── Button.test.ts
│   │   │   │   └── Button.vue
│   │   │   ├── FluxIndex.vue
│   │   │   ├── List/
│   │   │   │   ├── List.test.ts
│   │   │   │   └── List.vue
│   │   │   └── Thumb/
│   │   │       ├── Thumb.vue
│   │   │       └── useThumbs.ts
│   │   ├── FluxPagination/
│   │   │   └── FluxPagination.vue
│   │   ├── FluxPreloader/
│   │   │   └── FluxPreloader.vue
│   │   ├── __test__/
│   │   │   └── PlayerHelper.ts
│   │   └── index.ts
│   ├── components/
│   │   ├── FluxButton/
│   │   │   ├── FluxButton.test.ts
│   │   │   └── FluxButton.vue
│   │   ├── FluxCube/
│   │   │   ├── FluxCube.vue
│   │   │   ├── Sides.ts
│   │   │   ├── Turns.ts
│   │   │   ├── __mocks__/
│   │   │   │   ├── FluxCube.vue
│   │   │   │   └── Side.vue
│   │   │   ├── factories/
│   │   │   │   ├── CubeFactory.test.ts
│   │   │   │   ├── CubeFactory.ts
│   │   │   │   ├── CubeSideFactory.ts
│   │   │   │   ├── SideTransformFactory.test.ts
│   │   │   │   └── SideTransformFactory.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   ├── FluxGrid/
│   │   │   ├── FluxGrid.vue
│   │   │   ├── __mocks__/
│   │   │   │   ├── FluxGrid.vue
│   │   │   │   └── Tile.vue
│   │   │   ├── factories/
│   │   │   │   ├── GridFactory.ts
│   │   │   │   ├── GridTileFactory.ts
│   │   │   │   └── index.ts
│   │   │   └── types.ts
│   │   ├── FluxImage/
│   │   │   ├── FluxImage.vue
│   │   │   ├── __mocks__/
│   │   │   │   └── FluxImage.vue
│   │   │   └── types.ts
│   │   ├── FluxParallax/
│   │   │   ├── FluxParallax.vue
│   │   │   └── types.ts
│   │   ├── FluxTransition/
│   │   │   ├── FluxTransition.vue
│   │   │   └── types.ts
│   │   ├── FluxVortex/
│   │   │   ├── FluxVortex.vue
│   │   │   ├── __mocks__/
│   │   │   │   ├── FluxVortex.vue
│   │   │   │   └── Tile.vue
│   │   │   ├── factories/
│   │   │   │   ├── VortexCircleFactory.ts
│   │   │   │   ├── VortexFactory.ts
│   │   │   │   └── index.ts
│   │   │   └── types.ts
│   │   ├── FluxWrapper/
│   │   │   ├── FluxWrapper.vue
│   │   │   ├── __mocks__/
│   │   │   │   └── FluxWrapper.vue
│   │   │   └── types.ts
│   │   ├── VueFlux/
│   │   │   ├── VueFlux.vue
│   │   │   ├── __test__/
│   │   │   │   └── emit.ts
│   │   │   └── types.ts
│   │   ├── index.ts
│   │   ├── types.ts
│   │   └── useComponent.ts
│   ├── controllers/
│   │   ├── Display/
│   │   │   └── Display.ts
│   │   ├── Keys/
│   │   │   └── Keys.ts
│   │   ├── Mouse/
│   │   │   └── Mouse.ts
│   │   ├── Player/
│   │   │   ├── Directions.ts
│   │   │   ├── Player.ts
│   │   │   ├── Resource.ts
│   │   │   ├── Statuses.ts
│   │   │   ├── Transition.ts
│   │   │   ├── __mocks__/
│   │   │   │   ├── Player.ts
│   │   │   │   ├── Resource.ts
│   │   │   │   └── Transitions.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   ├── Timers/
│   │   │   └── Timers.ts
│   │   ├── Touches/
│   │   │   └── Touches.ts
│   │   └── index.ts
│   ├── lib.ts
│   ├── main.ts
│   ├── module.d.ts
│   ├── playgrounds/
│   │   ├── PgFluxCaption.vue
│   │   ├── PgFluxControls.vue
│   │   ├── PgFluxCube.vue
│   │   ├── PgFluxGrid.vue
│   │   ├── PgFluxImage.vue
│   │   ├── PgFluxIndex.vue
│   │   ├── PgFluxPagination.vue
│   │   ├── PgFluxParallax.vue
│   │   ├── PgFluxParallaxOp.vue
│   │   ├── PgFluxPreloader.vue
│   │   ├── PgFluxTransition.vue
│   │   ├── PgVueFlux.vue
│   │   └── components/
│   │       └── PgButton.vue
│   ├── repositories/
│   │   ├── Resources/
│   │   │   ├── Resources.test.ts
│   │   │   ├── Resources.ts
│   │   │   ├── ResourcesMapper.test.ts
│   │   │   ├── ResourcesMapper.ts
│   │   │   └── types.ts
│   │   ├── Transitions/
│   │   │   ├── Transitions.test.ts
│   │   │   ├── Transitions.ts
│   │   │   ├── TransitionsMapper.test.ts
│   │   │   ├── TransitionsMapper.ts
│   │   │   └── types.ts
│   │   └── index.ts
│   ├── resources/
│   │   ├── Img/
│   │   │   ├── Img.test.ts
│   │   │   ├── Img.ts
│   │   │   └── __mocks__/
│   │   │       └── Img.ts
│   │   ├── ResizeTypes.ts
│   │   ├── Resource.ts
│   │   ├── Statuses.ts
│   │   ├── __test__/
│   │   │   └── ResourceFactory.ts
│   │   ├── index.ts
│   │   └── types.ts
│   ├── shared/
│   │   ├── Maths/
│   │   │   ├── Maths.test.ts
│   │   │   └── Maths.ts
│   │   ├── Position/
│   │   │   ├── Position.test.ts
│   │   │   └── Position.ts
│   │   ├── ResizeCalculator/
│   │   │   ├── ResizeCalculator.test.ts
│   │   │   └── ResizeCalculator.ts
│   │   ├── ResourceLoader/
│   │   │   ├── ResourceLoader.test.ts
│   │   │   ├── ResourceLoader.ts
│   │   │   ├── __mocks__/
│   │   │   │   └── ResourceLoader.ts
│   │   │   └── __test__/
│   │   │       └── ResourceLoaderFactory.ts
│   │   ├── Size/
│   │   │   ├── Size.test.ts
│   │   │   └── Size.ts
│   │   └── index.ts
│   └── transitions/
│       ├── Blinds2D/
│       │   ├── Blinds2D.test.ts
│       │   ├── Blinds2D.vue
│       │   └── types.ts
│       ├── Blinds3D/
│       │   ├── Blinds3D.test.ts
│       │   ├── Blinds3D.vue
│       │   └── types.ts
│       ├── Blocks1/
│       │   ├── Blocks1.test.ts
│       │   ├── Blocks1.vue
│       │   └── types.ts
│       ├── Blocks2/
│       │   ├── Blocks2.test.ts
│       │   ├── Blocks2.vue
│       │   └── types.ts
│       ├── Book/
│       │   ├── Book.test.ts
│       │   ├── Book.vue
│       │   └── types.ts
│       ├── Camera/
│       │   ├── Camera.test.ts
│       │   ├── Camera.vue
│       │   └── types.ts
│       ├── Concentric/
│       │   ├── Concentric.test.ts
│       │   ├── Concentric.vue
│       │   └── types.ts
│       ├── Cube/
│       │   ├── Cube.test.ts
│       │   ├── Cube.vue
│       │   └── types.ts
│       ├── Explode/
│       │   ├── Explode.test.ts
│       │   ├── Explode.vue
│       │   └── types.ts
│       ├── Fade/
│       │   ├── Fade.test.ts
│       │   ├── Fade.vue
│       │   └── types.ts
│       ├── Fall/
│       │   ├── Fall.test.ts
│       │   ├── Fall.vue
│       │   └── types.ts
│       ├── Kenburn/
│       │   ├── Kenburn.test.ts
│       │   ├── Kenburn.vue
│       │   └── types.ts
│       ├── Round1/
│       │   ├── Round1.test.ts
│       │   ├── Round1.vue
│       │   └── types.ts
│       ├── Round2/
│       │   ├── Round2.test.ts
│       │   ├── Round2.vue
│       │   └── types.ts
│       ├── Slide/
│       │   ├── Slide.test.ts
│       │   ├── Slide.vue
│       │   └── types.ts
│       ├── Swipe/
│       │   ├── Swipe.test.ts
│       │   ├── Swipe.vue
│       │   └── types.ts
│       ├── Warp/
│       │   ├── Warp.test.ts
│       │   ├── Warp.vue
│       │   └── types.ts
│       ├── Waterfall/
│       │   ├── Waterfall.test.ts
│       │   ├── Waterfall.vue
│       │   └── types.ts
│       ├── Wave/
│       │   ├── Wave.test.ts
│       │   ├── Wave.vue
│       │   └── types.ts
│       ├── Zip/
│       │   ├── Zip.test.ts
│       │   ├── Zip.vue
│       │   └── types.ts
│       ├── __test__/
│       │   └── AnimationWrapper.ts
│       ├── index.ts
│       ├── types.ts
│       └── useTransition.ts
├── tsconfig.app.json
├── tsconfig.build.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
├── vite.config.ts
└── vitest.config.ts
Download .txt
SYMBOL INDEX (272 symbols across 77 files)

FILE: src/complements/FluxIndex/Thumb/useThumbs.ts
  function useThumbs (line 5) | function useThumbs(displaySize: Size, player: Player) {

FILE: src/complements/__test__/PlayerHelper.ts
  function setCurrentResource (line 22) | function setCurrentResource(player: Player, caption?: string) {
  function setCurrentTransition (line 30) | function setCurrentTransition(player: Player) {

FILE: src/components/FluxCube/Sides.ts
  type Sides (line 1) | enum Sides {

FILE: src/components/FluxCube/Turns.ts
  type Turns (line 1) | enum Turns {

FILE: src/components/FluxCube/factories/CubeFactory.ts
  function isSideDefined (line 9) | function isSideDefined(side: Side, colors?: SidesColors, rscs?: SidesRes...
  function getDefinedSides (line 21) | function getDefinedSides(
  class CubeFactory (line 36) | class CubeFactory {
    method getSidesProps (line 37) | static getSidesProps(

FILE: src/components/FluxCube/factories/CubeSideFactory.ts
  class CubeSideFactory (line 8) | class CubeSideFactory {
    method getProps (line 9) | static getProps(

FILE: src/components/FluxCube/factories/SideTransformFactory.ts
  class SideTransformFactory (line 46) | class SideTransformFactory {
    method constructor (line 68) | constructor(depth: number, size: Size, viewSize: Size) {
    method getRotate (line 74) | public getRotate(turn: Side | Turn) {
    method getTranslate (line 81) | public getTranslate(side: Side | Turn) {
    method getSideCss (line 89) | public getSideCss(side: Side | Turn) {

FILE: src/components/FluxCube/types.ts
  type FluxCubeProps (line 8) | interface FluxCubeProps extends ComponentProps {
  type Side (line 16) | type Side = keyof typeof Sides;
  type Turn (line 18) | type Turn = keyof typeof Turns;
  type SidesColors (line 20) | interface SidesColors {
  type SidesResources (line 29) | interface SidesResources {
  type SidesOffsets (line 38) | interface SidesOffsets {
  type SideProps (line 47) | interface SideProps {
  type SidesProps (line 58) | interface SidesProps {
  type SidesComponents (line 67) | interface SidesComponents {

FILE: src/components/FluxGrid/factories/GridFactory.ts
  class GridFactory (line 5) | class GridFactory {
    method getTilesProps (line 6) | static getTilesProps(props: FluxGridProps) {

FILE: src/components/FluxGrid/factories/GridTileFactory.ts
  function getRowNumber (line 7) | function getRowNumber(tileNumber: number, numCols: number) {
  function getColNumber (line 11) | function getColNumber(tileNumber: number, numCols: number) {
  class GridTileFactory (line 15) | class GridTileFactory {
    method getProps (line 16) | static getProps(

FILE: src/components/FluxGrid/types.ts
  type FluxGridProps (line 7) | interface FluxGridProps extends ComponentProps {
  type FluxGridTileProps (line 16) | interface FluxGridTileProps {

FILE: src/components/FluxImage/types.ts
  type FluxImageProps (line 3) | interface FluxImageProps extends ComponentProps {}

FILE: src/components/FluxParallax/types.ts
  type FluxParallaxProps (line 4) | interface FluxParallaxProps {
  type FluxParallaxStyles (line 11) | interface FluxParallaxStyles {
  type DisplayProps (line 17) | interface DisplayProps {
  type ViewProps (line 23) | interface ViewProps {

FILE: src/components/FluxTransition/types.ts
  type FluxTransitionProps (line 5) | interface FluxTransitionProps {

FILE: src/components/FluxVortex/factories/VortexCircleFactory.ts
  class VortexCircleFactory (line 5) | class VortexCircleFactory {
    method getProps (line 6) | static getProps(

FILE: src/components/FluxVortex/factories/VortexFactory.ts
  class VortexFactory (line 5) | class VortexFactory {
    method getCirclesProps (line 6) | static getCirclesProps(props: FluxVortexProps) {

FILE: src/components/FluxVortex/types.ts
  type FluxVortexProps (line 6) | interface FluxVortexProps extends ComponentProps {
  type FluxVortexCirclesProps (line 12) | interface FluxVortexCirclesProps {

FILE: src/components/FluxWrapper/types.ts
  type FluxWrapperProps (line 3) | interface FluxWrapperProps extends ComponentProps {}

FILE: src/components/VueFlux/types.ts
  type VueFluxOptions (line 6) | interface VueFluxOptions {
  type VueFluxProps (line 20) | interface VueFluxProps {
  type VueFluxEmits (line 26) | interface VueFluxEmits {
  type VueFluxConfig (line 46) | interface VueFluxConfig {

FILE: src/components/types.ts
  type ComponentProps (line 5) | interface ComponentProps {
  type ComponentStyles (line 14) | interface ComponentStyles {
  type FluxComponent (line 21) | type FluxComponent = Component & {

FILE: src/components/useComponent.ts
  function useComponent (line 5) | function useComponent(

FILE: src/controllers/Display/Display.ts
  class Display (line 5) | class Display {
    method constructor (line 15) | constructor(
    method getSize (line 25) | static async getSize(node: Ref<null | HTMLElement | Component>) {
    method addResizeListener (line 32) | addResizeListener() {
    method removeResizeListener (line 40) | removeResizeListener() {
    method getAspectRatio (line 44) | getAspectRatio() {
    method updateSize (line 54) | async updateSize() {
    method toggleFullScreen (line 86) | toggleFullScreen() {
    method enterFullScreen (line 91) | async enterFullScreen() {
    method exitFullScreen (line 103) | async exitFullScreen() {

FILE: src/controllers/Keys/Keys.ts
  class Keys (line 4) | class Keys {
    method constructor (line 8) | constructor(config: VueFluxConfig, player: Player) {
    method setup (line 13) | setup() {
    method removeKeyListener (line 21) | removeKeyListener() {

FILE: src/controllers/Mouse/Mouse.ts
  class Mouse (line 5) | class Mouse {
    method setup (line 8) | setup(config: VueFluxConfig, timers: Timers) {
    method toggle (line 16) | toggle(config: VueFluxConfig, timers: Timers, over: boolean) {
    method out (line 26) | out(_config: VueFluxConfig, timers: Timers) {
    method over (line 30) | over(config: VueFluxConfig, timers: Timers) {

FILE: src/controllers/Player/Directions.ts
  type Directions (line 1) | enum Directions {

FILE: src/controllers/Player/Player.ts
  class Player (line 12) | class Player {
    method constructor (line 25) | constructor(
    method setup (line 41) | setup($displayComponent: Ref<null | FluxComponent>) {
    method play (line 45) | play(resourceIndex: number | Direction = Directions.next, delay?: numb...
    method stop (line 63) | async stop(cancelTransition: boolean = false) {
    method isReadyToShow (line 77) | isReadyToShow() {
    method show (line 109) | async show(
    method start (line 160) | start() {
    method end (line 165) | async end(cancel: boolean = false) {
    method shouldStopPlaying (line 194) | private shouldStopPlaying(
    method shouldPlayNext (line 214) | private shouldPlayNext() {

FILE: src/controllers/Player/Resource.ts
  class PlayerResource (line 3) | class PlayerResource {
    method reset (line 8) | reset() {
    method init (line 14) | init(repository: Resources) {
    method currentSameAs (line 18) | currentSameAs(resourceTo: ResourceIndex) {
    method prepareTo (line 26) | prepareTo(resourceTo: ResourceIndex) {

FILE: src/controllers/Player/Statuses.ts
  type Statuses (line 1) | enum Statuses {

FILE: src/controllers/Player/Transition.ts
  class PlayerTransition (line 3) | class PlayerTransition {
    method reset (line 7) | reset() {
    method init (line 12) | init(transitions: Transitions) {
    method setCurrentFinished (line 16) | setCurrentFinished() {

FILE: src/controllers/Player/__mocks__/Player.ts
  class Player (line 8) | class Player {
    method constructor (line 20) | constructor(config: VueFluxConfig, timers: Timers, emit: VueFluxEmits) {

FILE: src/controllers/Player/__mocks__/Resource.ts
  class PlayerResource (line 4) | class PlayerResource {

FILE: src/controllers/Player/__mocks__/Transitions.ts
  class PlayerTransition (line 4) | class PlayerTransition {

FILE: src/controllers/Player/types.ts
  type Direction (line 3) | type Direction = Directions.prev | Directions.next;

FILE: src/controllers/Timers/Timers.ts
  class Timers (line 1) | class Timers {
    method set (line 6) | set(index: string, time: number, cb: () => void) {
    method clear (line 11) | clear(index?: string) {

FILE: src/controllers/Touches/Touches.ts
  class Touches (line 4) | class Touches {
    method start (line 20) | start(event: TouchEvent, config: VueFluxConfig) {
    method end (line 36) | end(

FILE: src/repositories/Resources/Resources.ts
  class Resources (line 9) | class Resources {
    method constructor (line 14) | constructor(emit: VueFluxEmits) {
    method getPrev (line 18) | private getPrev(currentIndex: number) {
    method getNext (line 22) | private getNext(currentIndex: number) {
    method getFirst (line 26) | getFirst() {
    method getLast (line 30) | getLast() {
    method getByIndex (line 34) | getByIndex(index: number) {
    method getByOrder (line 46) | getByOrder(order: Direction, currentIndex: number) {
    method find (line 53) | find(by: number | Direction, currentIndex?: number) {
    method update (line 65) | update(rscs: (Resource | ResourceWithOptions)[], numToPreload: number,...
    method preloadStart (line 90) | preloadStart() {
    method preloadEnd (line 94) | preloadEnd(loaded: ResourceWithOptions[], resolve: () => void) {
    method lazyLoadStart (line 102) | lazyLoadStart() {
    method lazyLoadEnd (line 106) | lazyLoadEnd(loaded: ResourceWithOptions[]) {

FILE: src/repositories/Resources/ResourcesMapper.ts
  class ResourcesMapper (line 3) | class ResourcesMapper {
    method withOptions (line 4) | static withOptions(rscs: (Resource | ResourceWithOptions)[]) {

FILE: src/repositories/Resources/types.ts
  type ResourceIndex (line 3) | interface ResourceIndex {

FILE: src/repositories/Transitions/Transitions.test.ts
  function transitionsFactory (line 4) | function transitionsFactory(numTransitions: number) {

FILE: src/repositories/Transitions/Transitions.ts
  class Transitions (line 7) | class Transitions {
    method getPrev (line 10) | private getPrev(lastIndex: number) {
    method getNext (line 14) | private getNext(lastIndex: number) {
    method getFirst (line 18) | getFirst() {
    method getLast (line 22) | getLast() {
    method getByIndex (line 26) | getByIndex(index: number) {
    method getByOrder (line 40) | getByOrder(direction: Direction, lastIndex: number) {
    method update (line 47) | update(transitions: (Component | TransitionWithOptions)[]) {

FILE: src/repositories/Transitions/TransitionsMapper.ts
  class TransitionsMapper (line 4) | class TransitionsMapper {
    method withOptions (line 5) | static withOptions(transitions: (Component | TransitionWithOptions)[]) {

FILE: src/repositories/Transitions/types.ts
  type TransitionIndex (line 4) | interface TransitionIndex {

FILE: src/resources/Img/Img.ts
  class Img (line 6) | class Img extends Resource {
    method constructor (line 7) | constructor(
    method load (line 28) | load() {
    method onLoad (line 47) | onLoad(img: HTMLImageElement, resolve: () => void) {
    method onError (line 58) | onError(reject: (message: string) => void) {

FILE: src/resources/Img/__mocks__/Img.ts
  class Img (line 5) | class Img extends Resource {
    method constructor (line 6) | constructor() {

FILE: src/resources/ResizeTypes.ts
  type ResizeTypes (line 1) | enum ResizeTypes {

FILE: src/resources/Resource.ts
  method constructor (line 20) | constructor(
  method calcResizeProps (line 50) | calcResizeProps(displaySize: Size) {
  method getResizeProps (line 71) | getResizeProps(size: Size, offset?: Position) {

FILE: src/resources/Statuses.ts
  type Statuses (line 1) | enum Statuses {

FILE: src/resources/__test__/ResourceFactory.ts
  class ResourceFactory (line 3) | class ResourceFactory {
    method create (line 4) | static create(amount: number) {

FILE: src/resources/types.ts
  type ResizeType (line 5) | type ResizeType = keyof typeof ResizeTypes;
  type ResizedProps (line 7) | interface ResizedProps {
  type DisplayParameter (line 14) | interface DisplayParameter {
  type TransitionParameter (line 19) | interface TransitionParameter {
  type ResourceWithOptions (line 24) | interface ResourceWithOptions {

FILE: src/shared/Position/Position.ts
  class Position (line 3) | class Position {
    method constructor (line 7) | constructor(
    method reset (line 16) | reset() {
    method isValid (line 21) | isValid() {
    method update (line 25) | update({ top, left }: { top?: null | number; left?: null | number }) {
    method toValue (line 30) | toValue() {
    method toPx (line 50) | toPx() {

FILE: src/shared/ResizeCalculator/ResizeCalculator.ts
  type Orientations (line 4) | enum Orientations {
  type Orientation (line 12) | type Orientation = keyof typeof Orientations;
  class ResizeCalculator (line 14) | class ResizeCalculator {
    method constructor (line 19) | constructor(realSize: Size) {
    method resizeTo (line 29) | public resizeTo(resizeSize: Size, resizeType: ResizeType) {
    method getAdaptedSize (line 57) | private getAdaptedSize(
    method getAdaptedSizeByWith (line 118) | private getAdaptedSizeByWith(resizeSize: Size) {
    method getAdaptedSizeByHeight (line 125) | private getAdaptedSizeByHeight(resizeSize: Size) {
    method getAdaptedPosition (line 132) | private getAdaptedPosition(
    method getAdaptedPositionVertically (line 149) | getAdaptedPositionVertically(resizeSize: Size, adaptedSize: Size) {
    method getAdaptedPositionHorizontally (line 156) | getAdaptedPositionHorizontally(resizeSize: Size, adaptedSize: Size) {

FILE: src/shared/ResourceLoader/ResourceLoader.ts
  class ResourceLoader (line 5) | class ResourceLoader {
    method constructor (line 24) | constructor(
    method preloadStart (line 46) | preloadStart() {
    method preloadEnd (line 61) | preloadEnd() {
    method lazyLoadStart (line 80) | lazyLoadStart() {
    method lazyLoadEnd (line 88) | lazyLoadEnd() {
    method load (line 96) | load(rsc: ResourceWithOptions) {
    method loadSuccess (line 124) | loadSuccess(rsc: ResourceWithOptions) {
    method loadError (line 134) | loadError(error: string) {
    method updateProgress (line 144) | updateProgress() {
    method hasFinished (line 148) | hasFinished() {
    method cancel (line 152) | cancel() {

FILE: src/shared/ResourceLoader/__mocks__/ResourceLoader.ts
  class ResourceLoader (line 4) | class ResourceLoader {
    method constructor (line 21) | constructor(
    method preloadStart (line 43) | preloadStart() {
    method preloadEnd (line 58) | preloadEnd() {
    method lazyLoadStart (line 77) | lazyLoadStart() {
    method lazyLoadEnd (line 85) | lazyLoadEnd() {
    method load (line 93) | load(rsc: ResourceWithOptions) {
    method loadSuccess (line 110) | loadSuccess() {
    method hasFinished (line 114) | hasFinished() {

FILE: src/shared/ResourceLoader/__test__/ResourceLoaderFactory.ts
  class ResourceLoaderFactory (line 7) | class ResourceLoaderFactory {
    method create (line 8) | static create(

FILE: src/shared/Size/Size.ts
  class Size (line 4) | class Size {
    method constructor (line 8) | constructor(
    method reset (line 20) | reset() {
    method isValid (line 25) | isValid() {
    method update (line 29) | update({ width, height }: { width?: null | number; height?: null | num...
    method getAspectRatio (line 34) | getAspectRatio() {
    method clone (line 42) | clone() {
    method equals (line 46) | equals(otherSize: Size) {
    method toValue (line 58) | toValue() {
    method toPx (line 75) | toPx() {

FILE: src/transitions/Blinds2D/types.ts
  type TransitionBlinds2DOptions (line 3) | interface TransitionBlinds2DOptions extends TransitionOptions {
  type TransitionBlinds2DProps (line 9) | interface TransitionBlinds2DProps extends TransitionProps {
  type TransitionBlinds2DConf (line 13) | interface TransitionBlinds2DConf extends TransitionConf {

FILE: src/transitions/Blinds3D/types.ts
  type TransitionBlinds3DOptions (line 3) | interface TransitionBlinds3DOptions extends TransitionOptions {
  type TransitionBlinds3DProps (line 9) | interface TransitionBlinds3DProps extends TransitionProps {
  type TransitionBlinds3DConf (line 13) | interface TransitionBlinds3DConf extends TransitionConf {

FILE: src/transitions/Blocks1/types.ts
  type TransitionBlocks1Options (line 3) | interface TransitionBlocks1Options extends TransitionOptions {
  type TransitionBlocks1Props (line 10) | interface TransitionBlocks1Props extends TransitionProps {
  type TransitionBlocks1Conf (line 14) | interface TransitionBlocks1Conf extends TransitionConf {

FILE: src/transitions/Blocks2/types.ts
  type TransitionBlocks2Options (line 5) | interface TransitionBlocks2Options extends TransitionOptions {
  type TransitionBlocks2Props (line 12) | interface TransitionBlocks2Props extends TransitionProps {
  type TransitionBlocks2Conf (line 16) | interface TransitionBlocks2Conf extends TransitionConf {
  type BackgroundProps (line 23) | interface BackgroundProps {

FILE: src/transitions/Book/types.ts
  type TransitionBookOptions (line 3) | interface TransitionBookOptions extends TransitionOptions {
  type TransitionBookProps (line 7) | interface TransitionBookProps extends TransitionProps {
  type TransitionBookConf (line 11) | interface TransitionBookConf extends TransitionConf {

FILE: src/transitions/Camera/types.ts
  type TransitionCameraOptions (line 4) | interface TransitionCameraOptions extends TransitionOptions {
  type TransitionCameraProps (line 9) | interface TransitionCameraProps extends TransitionProps {
  type TransitionCameraConf (line 13) | interface TransitionCameraConf extends TransitionConf {

FILE: src/transitions/Concentric/types.ts
  type TransitionConcentricOptions (line 3) | interface TransitionConcentricOptions extends TransitionOptions {
  type TransitionConcentricProps (line 9) | interface TransitionConcentricProps extends TransitionProps {
  type TransitionConcentricConf (line 13) | interface TransitionConcentricConf extends TransitionConf {

FILE: src/transitions/Cube/types.ts
  type TransitionCubeOptions (line 3) | interface TransitionCubeOptions extends TransitionOptions {
  type TransitionCubeProps (line 7) | interface TransitionCubeProps extends TransitionProps {
  type TransitionCubeConf (line 11) | interface TransitionCubeConf extends TransitionConf {

FILE: src/transitions/Explode/types.ts
  type TransitionExplodeOptions (line 3) | interface TransitionExplodeOptions extends TransitionOptions {
  type TransitionExplodeProps (line 10) | interface TransitionExplodeProps extends TransitionProps {
  type TransitionExplodeConf (line 14) | interface TransitionExplodeConf extends TransitionConf {

FILE: src/transitions/Fade/types.ts
  type TransitionFadeOptions (line 3) | interface TransitionFadeOptions extends TransitionOptions {
  type TransitionFadeProps (line 7) | interface TransitionFadeProps extends TransitionProps {
  type TransitionFadeConf (line 11) | interface TransitionFadeConf extends TransitionConf {

FILE: src/transitions/Fall/types.ts
  type TransitionFallOptions (line 3) | interface TransitionFallOptions extends TransitionOptions {
  type TransitionFallProps (line 7) | interface TransitionFallProps extends TransitionProps {
  type TransitionFallConf (line 11) | interface TransitionFallConf extends TransitionConf {

FILE: src/transitions/Kenburn/types.ts
  type TransitionKenburnOptions (line 3) | interface TransitionKenburnOptions extends TransitionOptions {
  type TransitionKenburnProps (line 7) | interface TransitionKenburnProps extends TransitionProps {
  type TransitionKenburnConf (line 11) | interface TransitionKenburnConf extends TransitionConf {

FILE: src/transitions/Round1/types.ts
  type TransitionRound1Options (line 3) | interface TransitionRound1Options extends TransitionOptions {
  type TransitionRound1Props (line 10) | interface TransitionRound1Props extends TransitionProps {
  type TransitionRound1Conf (line 14) | interface TransitionRound1Conf extends TransitionConf {

FILE: src/transitions/Round2/types.ts
  type TransitionRound2Options (line 3) | interface TransitionRound2Options extends TransitionOptions {
  type TransitionRound2Props (line 11) | interface TransitionRound2Props extends TransitionProps {
  type TransitionRound2Conf (line 15) | interface TransitionRound2Conf extends TransitionConf {

FILE: src/transitions/Slide/types.ts
  type TransitionSlideOptions (line 3) | interface TransitionSlideOptions extends TransitionOptions {
  type TransitionSlideProps (line 7) | interface TransitionSlideProps extends TransitionProps {
  type TransitionSlideConf (line 11) | interface TransitionSlideConf extends TransitionConf {

FILE: src/transitions/Swipe/types.ts
  type TransitionSwipeOptions (line 3) | interface TransitionSwipeOptions extends TransitionOptions {
  type TransitionSwipeProps (line 7) | interface TransitionSwipeProps extends TransitionProps {
  type TransitionSwipeConf (line 11) | interface TransitionSwipeConf extends TransitionConf {

FILE: src/transitions/Warp/types.ts
  type TransitionWarpOptions (line 3) | interface TransitionWarpOptions extends TransitionOptions {
  type TransitionWarpProps (line 9) | interface TransitionWarpProps extends TransitionProps {
  type TransitionWarpConf (line 13) | interface TransitionWarpConf extends TransitionConf {

FILE: src/transitions/Waterfall/types.ts
  type TransitionWaterfallOptions (line 3) | interface TransitionWaterfallOptions extends TransitionOptions {
  type TransitionWaterfallProps (line 9) | interface TransitionWaterfallProps extends TransitionProps {
  type TransitionWaterfallConf (line 13) | interface TransitionWaterfallConf extends TransitionConf {

FILE: src/transitions/Wave/types.ts
  type TransitionWaveOptions (line 4) | interface TransitionWaveOptions extends TransitionOptions {
  type TransitionWaveProps (line 11) | interface TransitionWaveProps extends TransitionProps {
  type TransitionWaveConf (line 15) | interface TransitionWaveConf extends TransitionConf {

FILE: src/transitions/Zip/types.ts
  type TransitionZipOptions (line 3) | interface TransitionZipOptions extends TransitionOptions {
  type TransitionZipProps (line 9) | interface TransitionZipProps extends TransitionProps {
  type TransitionZipConf (line 13) | interface TransitionZipConf extends TransitionConf {

FILE: src/transitions/types.ts
  type TransitionProps (line 7) | interface TransitionProps {
  type TransitionOptions (line 16) | interface TransitionOptions {
  type TransitionConf (line 21) | interface TransitionConf {
  type TransitionComponent (line 27) | type TransitionComponent = Component & {
  type TransitionWithOptions (line 32) | interface TransitionWithOptions {

FILE: src/transitions/useTransition.ts
  function useTransition (line 4) | function useTransition(conf: TransitionConf, options?: object) {
Condensed preview — 217 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (649K chars).
[
  {
    "path": ".editorconfig",
    "chars": 214,
    "preview": "[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]\ncharset = utf-8\nindent_size = 3\nindent_style = tab\ninser"
  },
  {
    "path": ".gitattributes",
    "chars": 38,
    "preview": "* text=auto eol=lf\nia.txt text eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 724,
    "preview": "# These are supported funding model platforms\n\ngithub: [ragnarlotus]# Replace with up to 4 GitHub Sponsors-enabled usern"
  },
  {
    "path": ".gitignore",
    "chars": 317,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Stor"
  },
  {
    "path": ".markdownlint.cjs",
    "chars": 137,
    "preview": "module.exports = {\r\n\tdefault: true,\r\n\tMD001: false,\r\n\tMD013: false,\r\n\tMD024: false,\r\n\tMD033: false,\r\n\tMD036: false,\r\n\tMD"
  },
  {
    "path": ".prettierrc.json",
    "chars": 183,
    "preview": "{\n\t\"$schema\": \"https://json.schemastore.org/prettierrc\",\n\t\"semi\": true,\n\t\"singleQuote\": true,\n\t\"printWidth\": 100,\n\t\"useT"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 163,
    "preview": "{\n  \"recommendations\": [\n    \"Vue.volar\",\n    \"vitest.explorer\",\n    \"dbaeumer.vscode-eslint\",\n    \"EditorConfig.EditorC"
  },
  {
    "path": "LICENSE",
    "chars": 1056,
    "preview": "MIT License\n\nCopyright (c) 2025\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this so"
  },
  {
    "path": "README.md",
    "chars": 6202,
    "preview": "## Documentation and demos\n\n**[Version 5 documentation](https://ragnarlotus.github.io/vue-flux-docs/documentation/v5/ove"
  },
  {
    "path": "env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "eslint.config.ts",
    "chars": 1012,
    "preview": "import { globalIgnores } from 'eslint/config';\nimport { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-t"
  },
  {
    "path": "ia.txt",
    "chars": 286143,
    "preview": "=== vue-flux IA bundle ===\nGenerated on: Fri Dec 12 06:10:34     2025\n\n===== FILE: package.json =====\n{\n\t\"name\": \"vue-fl"
  },
  {
    "path": "index.html",
    "chars": 279,
    "preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width,"
  },
  {
    "path": "package.json",
    "chars": 2263,
    "preview": "{\n\t\"name\": \"vue-flux\",\n\t\"version\": \"7.1.3\",\n\t\"type\": \"module\",\n\t\"description\": \"Vue image and other resources slider\",\n\t"
  },
  {
    "path": "src/App.vue",
    "chars": 1427,
    "preview": "<!-- eslint-disable @typescript-eslint/no-unused-vars -->\n<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';"
  },
  {
    "path": "src/assets/css/base.scss",
    "chars": 87,
    "preview": "label {\r\n\tmargin-top: 12px;\r\n\tdisplay: block;\r\n\r\n\tspan {\r\n\t\tmargin-right: 6px;\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/assets/css/main.css",
    "chars": 46,
    "preview": "@import 'tailwindcss';\n@import './base.scss';\n"
  },
  {
    "path": "src/complements/FluxCaption/FluxCaption.test.ts",
    "chars": 2338,
    "preview": "import { Player, Timers } from '../../controllers';\nimport { mount } from '@vue/test-utils';\nimport FluxCaption from './"
  },
  {
    "path": "src/complements/FluxCaption/FluxCaption.vue",
    "chars": 1145,
    "preview": "<script setup lang=\"ts\">\n\timport { computed } from 'vue';\n\timport { Player } from '../../controllers';\n\n\texport interfac"
  },
  {
    "path": "src/complements/FluxControls/FluxControls.test.ts",
    "chars": 4469,
    "preview": "import { ref, type Ref } from 'vue';\nimport { Player, Timers } from '../../controllers';\nimport { Directions, Statuses }"
  },
  {
    "path": "src/complements/FluxControls/FluxControls.vue",
    "chars": 1506,
    "preview": "<script setup lang=\"ts\">\n\timport { type Ref, computed, unref } from 'vue';\n\timport { Player, Directions } from '../../co"
  },
  {
    "path": "src/complements/FluxControls/buttons/Next.vue",
    "chars": 207,
    "preview": "<script setup lang=\"ts\">\n\timport { FluxButton } from '../../../components';\n</script>\r\n\r\n<template>\n\t<FluxButton class=\""
  },
  {
    "path": "src/complements/FluxControls/buttons/Play.vue",
    "chars": 196,
    "preview": "<script setup lang=\"ts\">\n\timport { FluxButton } from '../../../components';\n</script>\r\n\r\n<template>\n\t<FluxButton class=\""
  },
  {
    "path": "src/complements/FluxControls/buttons/Prev.vue",
    "chars": 206,
    "preview": "<script setup lang=\"ts\">\n\timport { FluxButton } from '../../../components';\n</script>\r\n\r\n<template>\n\t<FluxButton class=\""
  },
  {
    "path": "src/complements/FluxControls/buttons/Stop.vue",
    "chars": 242,
    "preview": "<script setup lang=\"ts\">\n\timport { FluxButton } from '../../../components';\n</script>\r\n\r\n<template>\n\t<FluxButton class=\""
  },
  {
    "path": "src/complements/FluxControls/buttons/index.ts",
    "chars": 184,
    "preview": "export { default as Prev } from './Prev.vue';\nexport { default as Play } from './Play.vue';\nexport { default as Stop } f"
  },
  {
    "path": "src/complements/FluxIndex/Button/Button.test.ts",
    "chars": 820,
    "preview": "import { type Ref, ref } from 'vue';\nimport { mount } from '@vue/test-utils';\nimport Button from './Button.vue';\n\ndescri"
  },
  {
    "path": "src/complements/FluxIndex/Button/Button.vue",
    "chars": 1201,
    "preview": "<script setup lang=\"ts\">\n\timport { type Ref, computed, unref } from 'vue';\n\timport { FluxButton } from '../../../compone"
  },
  {
    "path": "src/complements/FluxIndex/FluxIndex.vue",
    "chars": 1471,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, computed, type Ref } from 'vue';\n\timport { Size } from '../../shared';\n\timport {"
  },
  {
    "path": "src/complements/FluxIndex/List/List.test.ts",
    "chars": 2278,
    "preview": "import { type Ref, ref } from 'vue';\nimport { mount } from '@vue/test-utils';\nimport { Player, Timers } from '../../../c"
  },
  {
    "path": "src/complements/FluxIndex/List/List.vue",
    "chars": 2437,
    "preview": "<script setup lang=\"ts\">\n\timport { type Ref, computed, nextTick, ref } from 'vue';\n\timport { Player } from '../../../con"
  },
  {
    "path": "src/complements/FluxIndex/Thumb/Thumb.vue",
    "chars": 763,
    "preview": "<script setup lang=\"ts\">\n\timport type { Ref } from 'vue';\n\timport { Resource } from '../../../resources';\n\timport { Size"
  },
  {
    "path": "src/complements/FluxIndex/Thumb/useThumbs.ts",
    "chars": 672,
    "preview": "import { computed } from 'vue';\nimport { Player } from '../../../controllers';\nimport { Size } from '../../../shared';\n\n"
  },
  {
    "path": "src/complements/FluxPagination/FluxPagination.vue",
    "chars": 2142,
    "preview": "<script setup lang=\"ts\">\n\timport { computed } from 'vue';\n\timport type { ResourceWithOptions } from '../../resources';\n\t"
  },
  {
    "path": "src/complements/FluxPreloader/FluxPreloader.vue",
    "chars": 1458,
    "preview": "<script setup lang=\"ts\">\n\timport type { Ref } from 'vue';\n\timport { ResourceLoader } from '../../shared';\n\n\texport inter"
  },
  {
    "path": "src/complements/__test__/PlayerHelper.ts",
    "chars": 977,
    "preview": "import type { VueFluxConfig } from '../../components/VueFlux/types';\nimport { Player } from '../../controllers/Player';\n"
  },
  {
    "path": "src/complements/index.ts",
    "chars": 372,
    "preview": "export { default as FluxCaption } from './FluxCaption/FluxCaption.vue';\nexport { default as FluxControls } from './FluxC"
  },
  {
    "path": "src/components/FluxButton/FluxButton.test.ts",
    "chars": 417,
    "preview": "import FluxButton from './FluxButton.vue';\nimport { mount } from '@vue/test-utils';\n\ndescribe('component: FluxButton', ("
  },
  {
    "path": "src/components/FluxButton/FluxButton.vue",
    "chars": 985,
    "preview": "<template>\n\t<button type=\"button\" class=\"flux-button\" style=\"outline: 0\">\n\t\t<svg\n\t\t\tviewBox=\"0 0 100 100\"\n\t\t\txmlns=\"http"
  },
  {
    "path": "src/components/FluxCube/FluxCube.vue",
    "chars": 2075,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, computed, onBeforeUpdate } from 'vue';\n\timport useComponent from '../u"
  },
  {
    "path": "src/components/FluxCube/Sides.ts",
    "chars": 140,
    "preview": "enum Sides {\n\tfront = 'front',\n\tback = 'back',\n\tleft = 'left',\n\tright = 'right',\n\ttop = 'top',\n\tbottom = 'bottom',\n}\n\nex"
  },
  {
    "path": "src/components/FluxCube/Turns.ts",
    "chars": 176,
    "preview": "enum Turns {\n\tfront = 'front',\n\tback = 'back',\n\tbackr = 'backr',\n\tbackl = 'backl',\n\tleft = 'left',\n\tright = 'right',\n\tto"
  },
  {
    "path": "src/components/FluxCube/__mocks__/FluxCube.vue",
    "chars": 1267,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, computed } from 'vue';\n\timport { vi } from 'vitest';\n\timport Side from './Side.v"
  },
  {
    "path": "src/components/FluxCube/__mocks__/Side.vue",
    "chars": 296,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';\n\timport { vi } from 'vitest';\n\n\tconst $el: Ref<null | HTM"
  },
  {
    "path": "src/components/FluxCube/factories/CubeFactory.test.ts",
    "chars": 2943,
    "preview": "import { Img } from '../../../resources';\nimport { Position, Size } from '../../../shared';\nimport { type SideProps } fr"
  },
  {
    "path": "src/components/FluxCube/factories/CubeFactory.ts",
    "chars": 1497,
    "preview": "import type { Side, SidesColors, SidesResources, SidesOffsets, SidesProps } from '../types';\nimport CubeSideFactory from"
  },
  {
    "path": "src/components/FluxCube/factories/CubeSideFactory.ts",
    "chars": 1140,
    "preview": "import { Position } from '../../../shared';\nimport { Resource } from '../../../resources';\nimport type { Side, SideProps"
  },
  {
    "path": "src/components/FluxCube/factories/SideTransformFactory.test.ts",
    "chars": 2226,
    "preview": "import { Size } from '../../../shared';\nimport Turns from '../Turns';\nimport SideTransformFactory from './SideTransformF"
  },
  {
    "path": "src/components/FluxCube/factories/SideTransformFactory.ts",
    "chars": 1712,
    "preview": "import { type Ref, computed } from 'vue';\nimport { Size } from '../../../shared';\nimport type { Side, Turn } from '../ty"
  },
  {
    "path": "src/components/FluxCube/index.ts",
    "chars": 142,
    "preview": "export { default as FluxCube } from './FluxCube.vue';\nexport { default as Sides } from './Sides';\nexport { default as Tu"
  },
  {
    "path": "src/components/FluxCube/types.ts",
    "chars": 1698,
    "preview": "import type { CSSProperties, Component } from 'vue';\nimport { Resource } from '../../resources';\nimport { Position, Size"
  },
  {
    "path": "src/components/FluxGrid/FluxGrid.vue",
    "chars": 1463,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, computed, type Ref, onBeforeUpdate } from 'vue';\n\timport useComponent "
  },
  {
    "path": "src/components/FluxGrid/__mocks__/FluxGrid.vue",
    "chars": 1361,
    "preview": "<script setup lang=\"ts\">\n\timport { onBeforeUpdate, ref, type Ref } from 'vue';\n\timport { vi } from 'vitest';\n\timport Til"
  },
  {
    "path": "src/components/FluxGrid/__mocks__/Tile.vue",
    "chars": 363,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';\n\timport { vi } from 'vitest';\n\n\tconst $el: Ref<null | HTM"
  },
  {
    "path": "src/components/FluxGrid/factories/GridFactory.ts",
    "chars": 955,
    "preview": "import { Size } from '../../../shared';\nimport GridTileFactory from './GridTileFactory';\nimport type { FluxGridProps, Fl"
  },
  {
    "path": "src/components/FluxGrid/factories/GridTileFactory.ts",
    "chars": 1686,
    "preview": "import type { CSSProperties } from 'vue';\nimport { Resource } from '../../../resources';\nimport { Size, Position } from "
  },
  {
    "path": "src/components/FluxGrid/factories/index.ts",
    "chars": 152,
    "preview": "export { default as GridFactory } from './GridFactory';\nexport {\n\tdefault as GridTileFactory,\n\tgetRowNumber,\n\tgetColNumb"
  },
  {
    "path": "src/components/FluxGrid/types.ts",
    "chars": 657,
    "preview": "import type { CSSProperties } from 'vue';\nimport { Position, Size } from '../../shared';\nimport { Resource } from '../.."
  },
  {
    "path": "src/components/FluxImage/FluxImage.vue",
    "chars": 1593,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref, reactive, computed, type CSSProperties } from 'vue';\n\timport useCompon"
  },
  {
    "path": "src/components/FluxImage/__mocks__/FluxImage.vue",
    "chars": 397,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';\n\timport { vi } from 'vitest';\n\timport type { FluxImagePro"
  },
  {
    "path": "src/components/FluxImage/types.ts",
    "chars": 107,
    "preview": "import type { ComponentProps } from '../types';\n\nexport interface FluxImageProps extends ComponentProps {}\n"
  },
  {
    "path": "src/components/FluxParallax/FluxParallax.vue",
    "chars": 5088,
    "preview": "<script setup lang=\"ts\">\n\t// holder (window), component, background\n\n\timport {\n\t\tref,\n\t\treactive,\n\t\tcomputed,\n\t\tunref,\n\t"
  },
  {
    "path": "src/components/FluxParallax/types.ts",
    "chars": 558,
    "preview": "import type { CSSProperties, ComputedRef } from 'vue';\nimport { Resource } from '../../resources';\n\nexport interface Flu"
  },
  {
    "path": "src/components/FluxTransition/FluxTransition.vue",
    "chars": 2216,
    "preview": "<script setup lang=\"ts\">\n\timport {\n\t\tref,\n\t\treactive,\n\t\tcomputed,\n\t\tonMounted,\n\t\tonUnmounted,\n\t\tnextTick,\n\t\ttype Ref,\n\t\t"
  },
  {
    "path": "src/components/FluxTransition/types.ts",
    "chars": 297,
    "preview": "import { Resource } from '../../resources';\nimport { Size } from '../../shared';\nimport type { FluxComponent } from '../"
  },
  {
    "path": "src/components/FluxVortex/FluxVortex.vue",
    "chars": 1335,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, computed, type Ref, onBeforeUpdate } from 'vue';\n\timport useComponent "
  },
  {
    "path": "src/components/FluxVortex/__mocks__/FluxVortex.vue",
    "chars": 1091,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref, onBeforeUpdate } from 'vue';\n\timport { vi } from 'vitest';\n\timport Til"
  },
  {
    "path": "src/components/FluxVortex/__mocks__/Tile.vue",
    "chars": 363,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';\n\timport { vi } from 'vitest';\n\n\tconst $el: Ref<null | HTM"
  },
  {
    "path": "src/components/FluxVortex/factories/VortexCircleFactory.ts",
    "chars": 901,
    "preview": "import type { CSSProperties } from 'vue';\nimport { Position } from '../../../shared';\nimport type { FluxVortexCirclesPro"
  },
  {
    "path": "src/components/FluxVortex/factories/VortexFactory.ts",
    "chars": 945,
    "preview": "import { Maths } from '../../../shared';\nimport type { FluxVortexProps, FluxVortexCirclesProps } from '../types';\nimport"
  },
  {
    "path": "src/components/FluxVortex/factories/index.ts",
    "chars": 132,
    "preview": "export { default as VortexFactory } from './VortexFactory';\nexport { default as VortexCircleFactory } from './VortexCirc"
  },
  {
    "path": "src/components/FluxVortex/types.ts",
    "chars": 382,
    "preview": "import type { CSSProperties } from 'vue';\nimport { Resource } from '../../resources';\nimport type { ComponentProps } fro"
  },
  {
    "path": "src/components/FluxWrapper/FluxWrapper.vue",
    "chars": 678,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref } from 'vue';\n\timport useComponent from '../useComponent';\n\ti"
  },
  {
    "path": "src/components/FluxWrapper/__mocks__/FluxWrapper.vue",
    "chars": 420,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';\n\timport { vi } from 'vitest';\n\timport type { FluxWrapperP"
  },
  {
    "path": "src/components/FluxWrapper/types.ts",
    "chars": 109,
    "preview": "import type { ComponentProps } from '../types';\n\nexport interface FluxWrapperProps extends ComponentProps {}\n"
  },
  {
    "path": "src/components/VueFlux/VueFlux.vue",
    "chars": 5587,
    "preview": "<script setup lang=\"ts\">\n\timport { onMounted, onUnmounted, ref, reactive, computed, watch, type Ref, toRaw } from 'vue';"
  },
  {
    "path": "src/components/VueFlux/__test__/emit.ts",
    "chars": 127,
    "preview": "import { vi } from 'vitest';\nimport type { VueFluxEmits } from '../types';\n\nexport default vi.fn() as unknown as VueFlux"
  },
  {
    "path": "src/components/VueFlux/types.ts",
    "chars": 1813,
    "preview": "import { Resource, type ResourceWithOptions } from '../../resources';\nimport type { TransitionWithOptions } from '../../"
  },
  {
    "path": "src/components/index.ts",
    "chars": 901,
    "preview": "export { default as FluxButton } from './FluxButton/FluxButton.vue';\nexport * from './FluxCube';\nexport { default as Flu"
  },
  {
    "path": "src/components/types.ts",
    "chars": 589,
    "preview": "import type { CSSProperties, Component } from 'vue';\nimport { Resource } from '../resources';\nimport { Size, Position } "
  },
  {
    "path": "src/components/useComponent.ts",
    "chars": 1317,
    "preview": "import { computed, type CSSProperties, type Ref, unref } from 'vue';\nimport { Size } from '../shared';\nimport type { Com"
  },
  {
    "path": "src/controllers/Display/Display.ts",
    "chars": 2362,
    "preview": "import { nextTick, type Ref, type Component } from 'vue';\nimport { Size } from '../../shared';\nimport type { VueFluxConf"
  },
  {
    "path": "src/controllers/Keys/Keys.ts",
    "chars": 743,
    "preview": "import type { VueFluxConfig } from '../../components';\nimport { Directions, Player } from '../';\n\nexport default class K"
  },
  {
    "path": "src/controllers/Mouse/Mouse.ts",
    "chars": 758,
    "preview": "import { type Ref, ref } from 'vue';\nimport Timers from '../Timers/Timers';\nimport type { VueFluxConfig } from '../../co"
  },
  {
    "path": "src/controllers/Player/Directions.ts",
    "chars": 80,
    "preview": "enum Directions {\n\tprev = 'prev',\n\tnext = 'next',\n}\n\nexport default Directions;\n"
  },
  {
    "path": "src/controllers/Player/Player.ts",
    "chars": 5169,
    "preview": "import { shallowReactive, nextTick, type Ref, ref } from 'vue';\nimport {\n\tResources,\n\tTransitions,\n\ttype ResourceIndex,\n"
  },
  {
    "path": "src/controllers/Player/Resource.ts",
    "chars": 600,
    "preview": "import { Resources, type ResourceIndex } from '../../repositories';\n\nexport default class PlayerResource {\n\tcurrent: Res"
  },
  {
    "path": "src/controllers/Player/Statuses.ts",
    "chars": 88,
    "preview": "enum Statuses {\n\tstopped = 'stopped',\n\tplaying = 'playing',\n}\n\nexport default Statuses;\n"
  },
  {
    "path": "src/controllers/Player/Transition.ts",
    "chars": 406,
    "preview": "import { Transitions, type TransitionIndex } from '../../repositories';\n\nexport default class PlayerTransition {\n\tcurren"
  },
  {
    "path": "src/controllers/Player/__mocks__/Player.ts",
    "chars": 1146,
    "preview": "import { vi } from 'vitest';\nimport { type Ref, ref, shallowReactive } from 'vue';\nimport type { VueFluxConfig, VueFluxE"
  },
  {
    "path": "src/controllers/Player/__mocks__/Resource.ts",
    "chars": 323,
    "preview": "import type { ResourceIndex } from '../../../repositories';\nimport { vi } from 'vitest';\n\nexport default class PlayerRes"
  },
  {
    "path": "src/controllers/Player/__mocks__/Transitions.ts",
    "chars": 280,
    "preview": "import type { TransitionIndex } from '../../../repositories';\nimport { vi } from 'vitest';\n\nexport default class PlayerT"
  },
  {
    "path": "src/controllers/Player/index.ts",
    "chars": 297,
    "preview": "export { default as Directions } from './Directions';\nexport { default as Statuses } from './Statuses';\nexport { default"
  },
  {
    "path": "src/controllers/Player/types.ts",
    "chars": 99,
    "preview": "import Directions from './Directions';\n\nexport type Direction = Directions.prev | Directions.next;\n"
  },
  {
    "path": "src/controllers/Timers/Timers.ts",
    "chars": 422,
    "preview": "export default class Timers {\n\ttimers: {\n\t\t[index: string]: ReturnType<typeof setTimeout>;\n\t} = {};\n\n\tset(index: string,"
  },
  {
    "path": "src/controllers/Touches/Touches.ts",
    "chars": 2222,
    "preview": "import type { VueFluxConfig } from '../../components';\nimport { Directions, Display, Mouse, Player, Timers } from '../';"
  },
  {
    "path": "src/controllers/index.ts",
    "chars": 288,
    "preview": "export * from './Player';\nexport { default as Display } from './Display/Display';\nexport { default as Keys } from './Key"
  },
  {
    "path": "src/lib.ts",
    "chars": 325,
    "preview": "export * from './components';\nexport * from './complements';\nexport * from './resources';\nexport * from './transitions';"
  },
  {
    "path": "src/main.ts",
    "chars": 126,
    "preview": "import './assets/css/main.css';\n\nimport { createApp } from 'vue';\nimport App from './App.vue';\n\ncreateApp(App).mount('#a"
  },
  {
    "path": "src/module.d.ts",
    "chars": 20,
    "preview": "declare module '*';\n"
  },
  {
    "path": "src/playgrounds/PgFluxCaption.vue",
    "chars": 1401,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, shallowReactive, onMounted, type Ref } from 'vue';\n\timport { VcParagraph } from "
  },
  {
    "path": "src/playgrounds/PgFluxControls.vue",
    "chars": 1348,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, shallowReactive, onMounted } from 'vue';\n\timport { VcParagraph } from 'vue-cosk'"
  },
  {
    "path": "src/playgrounds/PgFluxCube.vue",
    "chars": 2471,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';\n\timport { VcParagraph } from 'vue-cosk';\n\timport Img from"
  },
  {
    "path": "src/playgrounds/PgFluxGrid.vue",
    "chars": 1541,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';\n\timport { VcParagraph } from 'vue-cosk';\n\timport Img from"
  },
  {
    "path": "src/playgrounds/PgFluxImage.vue",
    "chars": 2014,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref } from 'vue';\n\timport { VcParagraph } from 'vue-cosk';\n\timport Img from"
  },
  {
    "path": "src/playgrounds/PgFluxIndex.vue",
    "chars": 1420,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, shallowReactive, onMounted, type Ref } from 'vue';\n\timport { VcParagraph } from "
  },
  {
    "path": "src/playgrounds/PgFluxPagination.vue",
    "chars": 1649,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, shallowReactive, onMounted, type Ref } from 'vue';\n\timport { VcParagraph } from "
  },
  {
    "path": "src/playgrounds/PgFluxParallax.vue",
    "chars": 487,
    "preview": "<script setup lang=\"ts\">\n\timport { VcParagraph } from 'vue-cosk';\n\timport Img from '../resources/Img/Img';\n\timport { Flu"
  },
  {
    "path": "src/playgrounds/PgFluxParallaxOp.vue",
    "chars": 662,
    "preview": "<script lang=\"ts\">\n\timport { defineComponent, markRaw } from 'vue';\n\timport { VcParagraph } from 'vue-cosk';\n\timport Img"
  },
  {
    "path": "src/playgrounds/PgFluxPreloader.vue",
    "chars": 2379,
    "preview": "<script setup lang=\"ts\">\n\timport { shallowReactive } from 'vue';\n\timport { VcParagraph } from 'vue-cosk';\n\timport { VueF"
  },
  {
    "path": "src/playgrounds/PgFluxTransition.vue",
    "chars": 1847,
    "preview": "<script setup lang=\"ts\">\n\timport { nextTick, ref, type Ref, shallowRef } from 'vue';\n\timport { VcParagraph } from 'vue-c"
  },
  {
    "path": "src/playgrounds/PgVueFlux.vue",
    "chars": 3701,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref, shallowReactive } from 'vue';\n\timport { VcParagraph } from 'vue-cosk';"
  },
  {
    "path": "src/playgrounds/components/PgButton.vue",
    "chars": 311,
    "preview": "<script setup lang=\"ts\">\n\tconst props = withDefaults(defineProps<{ active?: boolean }>(), {\n\t\tactive: false,\n\t});\n</scri"
  },
  {
    "path": "src/repositories/Resources/Resources.test.ts",
    "chars": 2881,
    "preview": "import { Directions } from '../../controllers';\nimport { Resource } from '../../resources';\nimport { Size } from '../../"
  },
  {
    "path": "src/repositories/Resources/Resources.ts",
    "chars": 2783,
    "preview": "import { type Ref, ref, shallowReactive } from 'vue';\nimport { Resource, type ResourceWithOptions } from '../../resource"
  },
  {
    "path": "src/repositories/Resources/ResourcesMapper.test.ts",
    "chars": 850,
    "preview": "import { Img, type ResourceWithOptions } from '../../resources';\nimport ResourcesMapper from './ResourcesMapper';\n\ndescr"
  },
  {
    "path": "src/repositories/Resources/ResourcesMapper.ts",
    "chars": 463,
    "preview": "import { Resource, type ResourceWithOptions } from '../../resources';\n\nexport default class ResourcesMapper {\n\tstatic wi"
  },
  {
    "path": "src/repositories/Resources/types.ts",
    "chars": 169,
    "preview": "import Resource from '../../resources/Resource';\n\nexport interface ResourceIndex {\n\tindex: number;\n\trsc: Resource;\n\topti"
  },
  {
    "path": "src/repositories/Transitions/Transitions.test.ts",
    "chars": 2238,
    "preview": "import { Directions } from '../../controllers';\nimport { default as TransitionsRepository } from './Transitions';\n\nfunct"
  },
  {
    "path": "src/repositories/Transitions/Transitions.ts",
    "chars": 1404,
    "preview": "import { type Component, shallowReactive } from 'vue';\nimport { Directions, type Direction } from '../../controllers/Pla"
  },
  {
    "path": "src/repositories/Transitions/TransitionsMapper.test.ts",
    "chars": 1051,
    "preview": "import TransitionsMapper from './TransitionsMapper';\nimport { Fade, Kenburn, Swipe, Slide, type TransitionWithOptions } "
  },
  {
    "path": "src/repositories/Transitions/TransitionsMapper.ts",
    "chars": 585,
    "preview": "import type { Component } from 'vue';\nimport type { TransitionComponent, TransitionWithOptions } from '../../transitions"
  },
  {
    "path": "src/repositories/Transitions/types.ts",
    "chars": 215,
    "preview": "import type { Component } from 'vue';\nimport type { Direction } from '../../controllers/Player';\n\nexport interface Trans"
  },
  {
    "path": "src/repositories/index.ts",
    "chars": 247,
    "preview": "export { default as Resources } from './Resources/Resources';\nexport { default as Transitions } from './Transitions/Tran"
  },
  {
    "path": "src/resources/Img/Img.test.ts",
    "chars": 3323,
    "preview": "import { FluxImage } from '../../components';\nimport ResizeTypes from '../ResizeTypes';\nimport Statuses from '../Statuse"
  },
  {
    "path": "src/resources/Img/Img.ts",
    "chars": 1431,
    "preview": "import { FluxImage } from '../../components';\nimport { Resource, Statuses, ResizeTypes } from '../';\nimport { Size } fro"
  },
  {
    "path": "src/resources/Img/__mocks__/Img.ts",
    "chars": 879,
    "preview": "import { vi } from 'vitest';\nimport { ResizeTypes, Statuses, Resource } from '../../';\nimport { FluxImage } from '../../"
  },
  {
    "path": "src/resources/ResizeTypes.ts",
    "chars": 87,
    "preview": "export enum ResizeTypes {\n\tfill = 'fill',\n\tfit = 'fit',\n}\n\nexport default ResizeTypes;\n"
  },
  {
    "path": "src/resources/Resource.ts",
    "chars": 2397,
    "preview": "import { computed, ref, type Ref } from 'vue';\nimport { Size, Position, ResizeCalculator } from '../shared';\nimport type"
  },
  {
    "path": "src/resources/Statuses.ts",
    "chars": 137,
    "preview": "export enum Statuses {\n\tnotLoaded = 'notLoaded',\n\tloading = 'loading',\n\tloaded = 'loaded',\n\terror = 'error',\n}\n\nexport d"
  },
  {
    "path": "src/resources/__test__/ResourceFactory.ts",
    "chars": 151,
    "preview": "import { Img } from '../';\n\nexport default class ResourceFactory {\n\tstatic create(amount: number) {\n\t\treturn new Array(a"
  },
  {
    "path": "src/resources/index.ts",
    "chars": 231,
    "preview": "export { default as Resource } from './Resource';\nexport { default as Img } from './Img/Img';\nexport { default as Status"
  },
  {
    "path": "src/resources/types.ts",
    "chars": 532,
    "preview": "import type { Component } from 'vue';\nimport { Resource } from '.';\nimport ResizeTypes from './ResizeTypes';\n\nexport typ"
  },
  {
    "path": "src/shared/Maths/Maths.test.ts",
    "chars": 355,
    "preview": "import * as Maths from './Maths';\n\ndescribe('shared: Maths', () => {\n\tit('calculates the diagonal', () => {\n\t\tconst size"
  },
  {
    "path": "src/shared/Maths/Maths.ts",
    "chars": 242,
    "preview": "export const diag = ({ width, height }: { width: number; height: number }) =>\n\tMath.ceil(Math.sqrt(width * width + heigh"
  },
  {
    "path": "src/shared/Position/Position.test.ts",
    "chars": 2296,
    "preview": "import Position from './Position';\n\ndescribe('shared: Position', () => {\n\tlet pos: Position;\n\tlet coords: object;\n\n\tit('"
  },
  {
    "path": "src/shared/Position/Position.ts",
    "chars": 1108,
    "preview": "import { ref, type Ref } from 'vue';\n\nexport default class Position {\n\ttop: Ref<null | number> = ref(null);\n\tleft: Ref<n"
  },
  {
    "path": "src/shared/ResizeCalculator/ResizeCalculator.test.ts",
    "chars": 7688,
    "preview": "import { Size } from '../';\nimport { ResizeTypes } from '../../resources';\nimport ResizeCalculator, { Orientations } fro"
  },
  {
    "path": "src/shared/ResizeCalculator/ResizeCalculator.ts",
    "chars": 4206,
    "preview": "import { type ResizeType, ResizeTypes } from '../../resources';\nimport { Size, Position } from '../';\n\nexport enum Orien"
  },
  {
    "path": "src/shared/ResourceLoader/ResourceLoader.test.ts",
    "chars": 3295,
    "preview": "import { vi } from 'vitest';\nimport ResourceLoader from './ResourceLoader';\nimport ResourceLoaderFactory from './__test_"
  },
  {
    "path": "src/shared/ResourceLoader/ResourceLoader.ts",
    "chars": 3481,
    "preview": "import { type Ref, ref } from 'vue';\nimport { Size } from '../';\nimport type { ResourceWithOptions } from '../../resourc"
  },
  {
    "path": "src/shared/ResourceLoader/__mocks__/ResourceLoader.ts",
    "chars": 2738,
    "preview": "import { Size } from '../../';\nimport type { ResourceWithOptions } from '../../../resources';\n\nexport default class Reso"
  },
  {
    "path": "src/shared/ResourceLoader/__test__/ResourceLoaderFactory.ts",
    "chars": 1417,
    "preview": "import { vi } from 'vitest';\nimport ResourceFactory from '../../../resources/__test__/ResourceFactory';\nimport ResourceL"
  },
  {
    "path": "src/shared/Size/Size.test.ts",
    "chars": 3576,
    "preview": "import Size from './Size';\n\ndescribe('shared: Size', () => {\n\tlet size: Size;\n\tlet params: object;\n\n\tit('initializes val"
  },
  {
    "path": "src/shared/Size/Size.ts",
    "chars": 1604,
    "preview": "import { ref, type Ref } from 'vue';\nimport { Maths } from '../';\n\nexport default class Size {\n\twidth: Ref<null | number"
  },
  {
    "path": "src/shared/index.ts",
    "chars": 306,
    "preview": "export * as Maths from './Maths/Maths';\nexport { default as Position } from './Position/Position';\nexport { default as R"
  },
  {
    "path": "src/transitions/Blinds2D/Blinds2D.test.ts",
    "chars": 2612,
    "preview": "import Blinds2D from './Blinds2D.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions "
  },
  {
    "path": "src/transitions/Blinds2D/Blinds2D.vue",
    "chars": 1366,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref } from 'vue';\n\timport { type FluxComponent, FluxGrid } from '"
  },
  {
    "path": "src/transitions/Blinds2D/types.ts",
    "chars": 468,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionBlinds2D"
  },
  {
    "path": "src/transitions/Blinds3D/Blinds3D.test.ts",
    "chars": 3070,
    "preview": "import Blinds3D from './Blinds3D.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions "
  },
  {
    "path": "src/transitions/Blinds3D/Blinds3D.vue",
    "chars": 1712,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Blinds3D/types.ts",
    "chars": 468,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionBlinds3D"
  },
  {
    "path": "src/transitions/Blocks1/Blocks1.test.ts",
    "chars": 2626,
    "preview": "import Blocks1 from './Blocks1.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } "
  },
  {
    "path": "src/transitions/Blocks1/Blocks1.vue",
    "chars": 1279,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref } from 'vue';\n\timport useTransition from '../useTransition';\n"
  },
  {
    "path": "src/transitions/Blocks1/types.ts",
    "chars": 480,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionBlocks1O"
  },
  {
    "path": "src/transitions/Blocks2/Blocks2.test.ts",
    "chars": 2640,
    "preview": "import Blocks2 from './Blocks2.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } "
  },
  {
    "path": "src/transitions/Blocks2/Blocks2.vue",
    "chars": 2694,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref } from 'vue';\n\timport useTransition from '../useTransition';\n"
  },
  {
    "path": "src/transitions/Blocks2/types.ts",
    "chars": 648,
    "preview": "import type { CSSProperties } from 'vue';\nimport { Resource } from '../../resources';\nimport type { TransitionConf, Tran"
  },
  {
    "path": "src/transitions/Book/Book.test.ts",
    "chars": 3937,
    "preview": "import Book from './Book.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } from '"
  },
  {
    "path": "src/transitions/Book/Book.vue",
    "chars": 2826,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Book/types.ts",
    "chars": 367,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionBookOpti"
  },
  {
    "path": "src/transitions/Camera/Camera.test.ts",
    "chars": 4027,
    "preview": "import Camera from './Camera.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } fr"
  },
  {
    "path": "src/transitions/Camera/Camera.vue",
    "chars": 2040,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, type Ref, reactive, type CSSProperties } from 'vue';\n\timport { Maths } from '../"
  },
  {
    "path": "src/transitions/Camera/types.ts",
    "chars": 502,
    "preview": "import type { CSSProperties } from 'vue';\nimport type { TransitionConf, TransitionOptions, TransitionProps } from '../ty"
  },
  {
    "path": "src/transitions/Concentric/Concentric.test.ts",
    "chars": 2684,
    "preview": "import Concentric from './Concentric.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directi"
  },
  {
    "path": "src/transitions/Concentric/Concentric.vue",
    "chars": 1311,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref } from 'vue';\n\timport useTransition from '../useTransition';\n"
  },
  {
    "path": "src/transitions/Concentric/types.ts",
    "chars": 467,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionConcentr"
  },
  {
    "path": "src/transitions/Cube/Cube.test.ts",
    "chars": 1869,
    "preview": "import Cube from './Cube.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } from '"
  },
  {
    "path": "src/transitions/Cube/Cube.vue",
    "chars": 1378,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Cube/types.ts",
    "chars": 367,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionCubeOpti"
  },
  {
    "path": "src/transitions/Explode/Explode.test.ts",
    "chars": 3002,
    "preview": "import Explode from './Explode.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } "
  },
  {
    "path": "src/transitions/Explode/Explode.vue",
    "chars": 1819,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Explode/types.ts",
    "chars": 480,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionExplodeO"
  },
  {
    "path": "src/transitions/Fade/Fade.test.ts",
    "chars": 1798,
    "preview": "import Fade from './Fade.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } from '"
  },
  {
    "path": "src/transitions/Fade/Fade.vue",
    "chars": 904,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Fade/types.ts",
    "chars": 367,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionFadeOpti"
  },
  {
    "path": "src/transitions/Fall/Fall.test.ts",
    "chars": 1943,
    "preview": "import Fall from './Fall.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } from '"
  },
  {
    "path": "src/transitions/Fall/Fall.vue",
    "chars": 1086,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Fall/types.ts",
    "chars": 367,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionFallOpti"
  },
  {
    "path": "src/transitions/Kenburn/Kenburn.test.ts",
    "chars": 1839,
    "preview": "import Kenburn from './Kenburn.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } "
  },
  {
    "path": "src/transitions/Kenburn/Kenburn.vue",
    "chars": 1653,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Kenburn/types.ts",
    "chars": 379,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionKenburnO"
  },
  {
    "path": "src/transitions/Round1/Round1.test.ts",
    "chars": 2996,
    "preview": "import Round1 from './Round1.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } fr"
  },
  {
    "path": "src/transitions/Round1/Round1.vue",
    "chars": 2172,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Round1/types.ts",
    "chars": 476,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionRound1Op"
  },
  {
    "path": "src/transitions/Round2/Round2.test.ts",
    "chars": 2921,
    "preview": "import Round2 from './Round2.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } fr"
  },
  {
    "path": "src/transitions/Round2/Round2.vue",
    "chars": 2146,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Round2/types.ts",
    "chars": 513,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionRound2Op"
  },
  {
    "path": "src/transitions/Slide/Slide.test.ts",
    "chars": 3552,
    "preview": "import Slide from './Slide.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } from"
  },
  {
    "path": "src/transitions/Slide/Slide.vue",
    "chars": 2140,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Slide/types.ts",
    "chars": 371,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionSlideOpt"
  },
  {
    "path": "src/transitions/Swipe/Swipe.test.ts",
    "chars": 2920,
    "preview": "import Swipe from './Swipe.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } from"
  },
  {
    "path": "src/transitions/Swipe/Swipe.vue",
    "chars": 1562,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref, type CSSProperties } from 'vue';\n\timport useTransition from "
  },
  {
    "path": "src/transitions/Swipe/types.ts",
    "chars": 371,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionSwipeOpt"
  },
  {
    "path": "src/transitions/Warp/Warp.test.ts",
    "chars": 2650,
    "preview": "import Warp from './Warp.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Directions } from '"
  },
  {
    "path": "src/transitions/Warp/Warp.vue",
    "chars": 1413,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref } from 'vue';\n\timport useTransition from '../useTransition';\n"
  },
  {
    "path": "src/transitions/Warp/types.ts",
    "chars": 443,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionWarpOpti"
  },
  {
    "path": "src/transitions/Waterfall/Waterfall.test.ts",
    "chars": 2727,
    "preview": "import Waterfall from './Waterfall.vue';\nimport AnimationWrapper from '../__test__/AnimationWrapper';\nimport { Direction"
  },
  {
    "path": "src/transitions/Waterfall/Waterfall.vue",
    "chars": 1387,
    "preview": "<script setup lang=\"ts\">\n\timport { ref, reactive, type Ref } from 'vue';\n\timport useTransition from '../useTransition';\n"
  },
  {
    "path": "src/transitions/Waterfall/types.ts",
    "chars": 472,
    "preview": "import type { TransitionConf, TransitionOptions, TransitionProps } from '../types';\n\nexport interface TransitionWaterfal"
  }
]

// ... and 17 more files (download for full content)

About this extraction

This page contains the full source code of the ragnarlotus/vue-flux GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 217 files (559.1 KB), approximately 164.5k tokens, and a symbol index with 272 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!